You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
517 lines
16 KiB
517 lines
16 KiB
/*****************************************************************************
|
|
* Copyright (C) 2020 VideoLAN and AUTHORS
|
|
*
|
|
* This program is free software; you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation; either version 2 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program; if not, write to the Free Software
|
|
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
|
|
*****************************************************************************/
|
|
#include "interface_window_handler.hpp"
|
|
#include "mainctx.hpp"
|
|
#include "compositor.hpp"
|
|
#include <player/player_controller.hpp>
|
|
#include <playlist/playlist_controller.hpp>
|
|
#include "util/keyhelper.hpp"
|
|
#include "dialogs/systray/systray.hpp"
|
|
#include "widgets/native/qvlcframe.hpp"
|
|
#include "dialogs/dialogs/dialogmodel.hpp"
|
|
#include <QScreen>
|
|
#include <QQmlProperty>
|
|
#include <cmath>
|
|
|
|
namespace
|
|
{
|
|
|
|
void setWindowState(QWindow *window, Qt::WindowState state)
|
|
{
|
|
// make sure to preserve original state, Qt saves this info
|
|
// in underlying platform window but we need this in top level
|
|
// so that our window handling code works.
|
|
// see issue #28071
|
|
const auto original = window->windowStates();
|
|
window->setWindowStates(original | state);
|
|
}
|
|
|
|
}
|
|
|
|
const Qt::Key InterfaceWindowHandler::kc[10] =
|
|
{
|
|
Qt::Key_Up, Qt::Key_Up,
|
|
Qt::Key_Down, Qt::Key_Down,
|
|
Qt::Key_Left, Qt::Key_Right, Qt::Key_Left, Qt::Key_Right,
|
|
Qt::Key_B, Qt::Key_A
|
|
};
|
|
|
|
InterfaceWindowHandler::InterfaceWindowHandler(qt_intf_t *_p_intf, MainCtx* mainCtx, QWindow* window, QObject *parent)
|
|
: QObject(parent)
|
|
, p_intf(_p_intf)
|
|
, m_window(window)
|
|
, m_mainCtx(mainCtx)
|
|
{
|
|
assert(m_window);
|
|
assert(m_mainCtx);
|
|
|
|
if (m_mainCtx->useClientSideDecoration())
|
|
m_window->setFlag(Qt::FramelessWindowHint);
|
|
|
|
/* */
|
|
m_pauseOnMinimize = var_InheritBool( p_intf, "qt-pause-minimized" );
|
|
|
|
m_window->setIcon( QApplication::windowIcon() );
|
|
m_window->setOpacity( var_InheritFloat( p_intf, "qt-opacity" ) );
|
|
|
|
QVLCTools::restoreWindowPosition( getSettings(), m_window, QSize(600, 420) );
|
|
|
|
// this needs to be called asynchronously
|
|
// otherwise QQuickWidget won't initialize properly
|
|
QMetaObject::invokeMethod(this, [this]()
|
|
{
|
|
WindowStateHolder::holdOnTop( m_window, WindowStateHolder::INTERFACE, m_mainCtx->isInterfaceAlwaysOnTop() );
|
|
WindowStateHolder::holdFullscreen( m_window, WindowStateHolder::INTERFACE, m_window->visibility() == QWindow::FullScreen );
|
|
|
|
if (m_mainCtx->isHideAfterCreation())
|
|
m_window->hide();
|
|
}, Qt::QueuedConnection);
|
|
|
|
m_window->setTitle("");
|
|
|
|
if( var_InheritBool( p_intf, "qt-name-in-title" ) )
|
|
{
|
|
connect( THEMIM, &PlayerController::nameChanged, m_window, &QWindow::setTitle );
|
|
}
|
|
|
|
connect( m_window, &QWindow::screenChanged, m_mainCtx, &MainCtx::updateIntfScaleFactor );
|
|
|
|
const auto updateMinimumSize = [this]()
|
|
{
|
|
if (m_mainCtx->isMinimalView())
|
|
{
|
|
m_window->setMinimumSize({128, 16});
|
|
return;
|
|
}
|
|
|
|
int margin = m_mainCtx->windowExtendedMargin() * 2;
|
|
int width = 320 + margin;
|
|
int height = 360 + margin;
|
|
|
|
double intfScaleFactor = m_mainCtx->getIntfScaleFactor();
|
|
int scaledWidth = std::ceil( intfScaleFactor * width );
|
|
int scaledHeight = std::ceil( intfScaleFactor * height );
|
|
|
|
m_window->setMinimumSize( QSize(scaledWidth, scaledHeight) );
|
|
};
|
|
connect( m_mainCtx, &MainCtx::intfScaleFactorChanged, this, updateMinimumSize );
|
|
connect( m_mainCtx, &MainCtx::windowExtendedMarginChanged, this, updateMinimumSize );
|
|
connect( m_mainCtx, &MainCtx::minimalViewChanged, this, updateMinimumSize );
|
|
m_mainCtx->updateIntfScaleFactor();
|
|
updateMinimumSize();
|
|
|
|
m_mainCtx->onWindowVisibilityChanged(m_window->visibility());
|
|
connect( m_window, &QWindow::visibilityChanged,
|
|
m_mainCtx, &MainCtx::onWindowVisibilityChanged);
|
|
|
|
connect( m_mainCtx, &MainCtx::askBoss,
|
|
this, &InterfaceWindowHandler::setBoss, Qt::QueuedConnection );
|
|
connect( m_mainCtx, &MainCtx::askRaise,
|
|
this, &InterfaceWindowHandler::setRaise, Qt::QueuedConnection );
|
|
|
|
connect( m_mainCtx, &MainCtx::interfaceAlwaysOnTopChanged,
|
|
this, &InterfaceWindowHandler::setInterfaceAlwaysOnTop);
|
|
|
|
connect( m_mainCtx, &MainCtx::setInterfaceFullScreen,
|
|
this, &InterfaceWindowHandler::setInterfaceFullScreen);
|
|
|
|
connect( m_mainCtx, &MainCtx::toggleWindowVisibility,
|
|
this, &InterfaceWindowHandler::toggleWindowVisibility);
|
|
|
|
connect( m_mainCtx, &MainCtx::setInterfaceVisibible,
|
|
this, &InterfaceWindowHandler::setInterfaceVisible);
|
|
|
|
connect(this, &InterfaceWindowHandler::incrementIntfUserScaleFactor,
|
|
m_mainCtx, &MainCtx::incrementIntfUserScaleFactor);
|
|
|
|
connect( m_mainCtx, &MainCtx::useClientSideDecorationChanged,
|
|
this, &InterfaceWindowHandler::updateCSDWindowSettings );
|
|
|
|
connect(m_mainCtx, &MainCtx::requestInterfaceMaximized,
|
|
this, &InterfaceWindowHandler::setInterfaceMaximized);
|
|
|
|
connect(m_mainCtx, &MainCtx::requestInterfaceNormal,
|
|
this, &InterfaceWindowHandler::setInterfaceNormal);
|
|
|
|
connect(m_mainCtx, &MainCtx::requestInterfaceMinimized,
|
|
this, &InterfaceWindowHandler::setInterfaceMinimized);
|
|
|
|
connect(this, &InterfaceWindowHandler::kc_pressed,
|
|
m_mainCtx, &MainCtx::kc_pressed);
|
|
|
|
const auto dem = DialogErrorModel::getInstance<false>(); // expected to be already created
|
|
assert(dem);
|
|
connect(dem, &DialogErrorModel::rowsInserted, this, [w = QPointer(m_window)]() {
|
|
if (Q_LIKELY(w))
|
|
w->alert(0);
|
|
});
|
|
|
|
m_window->installEventFilter(this);
|
|
}
|
|
|
|
InterfaceWindowHandler::~InterfaceWindowHandler()
|
|
{
|
|
m_window->removeEventFilter(this);
|
|
/* Save this size */
|
|
QVLCTools::saveWindowPosition(getSettings(), m_window);
|
|
assert(qApp);
|
|
if (!qApp->property("isDying").toBool())
|
|
{
|
|
// If application is dying, we don't need to adjust the state
|
|
WindowStateHolder::holdOnTop( m_window, WindowStateHolder::INTERFACE, false );
|
|
WindowStateHolder::holdFullscreen( m_window, WindowStateHolder::INTERFACE, false );
|
|
}
|
|
}
|
|
|
|
void InterfaceWindowHandler::updateCSDWindowSettings()
|
|
{
|
|
m_window->setFlag(Qt::FramelessWindowHint, m_mainCtx->useClientSideDecoration());
|
|
}
|
|
|
|
bool InterfaceWindowHandler::eventFilter(QObject*, QEvent* event)
|
|
{
|
|
switch ( event->type() )
|
|
{
|
|
case QEvent::WindowStateChange:
|
|
{
|
|
QWindowStateChangeEvent *windowStateChangeEvent = static_cast<QWindowStateChangeEvent*>(event);
|
|
Qt::WindowStates newState = m_window->windowStates();
|
|
Qt::WindowStates oldState = windowStateChangeEvent->oldState();
|
|
|
|
/* b_maximizedView stores if the window was maximized before entering fullscreen.
|
|
* It is set when entering maximized mode, unset when leaving it to normal mode.
|
|
* Upon leaving full screen, if b_maximizedView is set,
|
|
* the window should be maximized again. */
|
|
if( newState & Qt::WindowMaximized &&
|
|
!( oldState & Qt::WindowMaximized ) )
|
|
m_maximizedView = true;
|
|
|
|
if( !( newState & Qt::WindowMaximized ) &&
|
|
oldState & Qt::WindowMaximized ) //FIXME && !b_videoFullScreen )
|
|
m_maximizedView = false;
|
|
|
|
if( !( newState & Qt::WindowFullScreen ) &&
|
|
oldState & Qt::WindowFullScreen &&
|
|
m_maximizedView )
|
|
{
|
|
setInterfaceMaximized();
|
|
return false;
|
|
}
|
|
|
|
if( newState & Qt::WindowMinimized )
|
|
{
|
|
|
|
m_hasPausedWhenMinimized = false;
|
|
|
|
if( THEMIM->getPlayingState() == PlayerController::PLAYING_STATE_PLAYING &&
|
|
THEMIM->hasVideoOutput() && !THEMIM->hasAudioVisualization() &&
|
|
m_pauseOnMinimize )
|
|
{
|
|
m_hasPausedWhenMinimized = true;
|
|
THEMPL->pause();
|
|
}
|
|
}
|
|
else if( oldState & Qt::WindowMinimized && !( newState & Qt::WindowMinimized ) )
|
|
{
|
|
if( m_hasPausedWhenMinimized )
|
|
{
|
|
THEMPL->play();
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case QEvent::KeyPress:
|
|
{
|
|
QKeyEvent * keyEvent = static_cast<QKeyEvent *> (event);
|
|
|
|
/* easter eggs sequence handling */
|
|
if ( keyEvent->key() == kc[ i_kc_offset ] )
|
|
i_kc_offset++;
|
|
else
|
|
i_kc_offset = 0;
|
|
|
|
if ( i_kc_offset == ARRAY_SIZE( kc ) )
|
|
{
|
|
i_kc_offset = 0;
|
|
emit kc_pressed();
|
|
}
|
|
|
|
if (applyKeyEvent(keyEvent) == false)
|
|
return false;
|
|
|
|
m_mainCtx->sendHotkey(static_cast<Qt::Key> (keyEvent->key()), keyEvent->modifiers());
|
|
|
|
return true;
|
|
}
|
|
case QEvent::KeyRelease:
|
|
{
|
|
return applyKeyEvent(static_cast<QKeyEvent *> (event));
|
|
}
|
|
case QEvent::Wheel:
|
|
{
|
|
QWheelEvent* wheelEvent = static_cast<QWheelEvent*>(event);
|
|
if (wheelEvent->modifiers() == Qt::ControlModifier)
|
|
{
|
|
emit incrementIntfUserScaleFactor(wheelEvent->angleDelta().y() > 0);
|
|
wheelEvent->accept();
|
|
return true;
|
|
}
|
|
break;
|
|
}
|
|
case QEvent::Close:
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
|
|
// Before Qt 6.9.0, `QWindow`'s `wl_surface` gets deleted when `QWindow::hide()` is called.
|
|
static bool platformIsWayland = []() {
|
|
assert(qGuiApp);
|
|
return qGuiApp->platformName().startsWith(QLatin1String("wayland"));
|
|
}();
|
|
|
|
if (!platformIsWayland)
|
|
setInterfaceHiden();
|
|
#else
|
|
setInterfaceHiden();
|
|
#endif
|
|
|
|
if (var_InheritBool(p_intf, "qt-close-to-system-tray"))
|
|
{
|
|
if (const QSystemTrayIcon* const sysTrayIcon = m_mainCtx->getSysTray())
|
|
{
|
|
if (sysTrayIcon->isSystemTrayAvailable() && sysTrayIcon->isVisible())
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
|
|
if (platformIsWayland)
|
|
setInterfaceHiden();
|
|
#endif
|
|
event->ignore();
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
|
|
m_mainCtx->onWindowClose(m_window);
|
|
event->ignore();
|
|
return true;
|
|
}
|
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 6, 0)
|
|
case QEvent::DevicePixelRatioChange:
|
|
{
|
|
m_mainCtx->intfDevicePixelRatioChanged();
|
|
}
|
|
#endif
|
|
|
|
default:
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void InterfaceWindowHandler::onVideoEmbedChanged(bool embed)
|
|
{
|
|
if (embed)
|
|
{
|
|
m_interfaceGeometry = m_window->geometry();
|
|
}
|
|
else if (!m_interfaceGeometry.isNull())
|
|
{
|
|
m_window->setGeometry(m_interfaceGeometry);
|
|
m_interfaceGeometry = QRect();
|
|
}
|
|
}
|
|
|
|
|
|
void InterfaceWindowHandler::toggleWindowVisibility()
|
|
{
|
|
switch ( m_window->visibility() )
|
|
{
|
|
case QWindow::Hidden:
|
|
/* If hidden, show it */
|
|
setInterfaceShown();
|
|
requestActivate();
|
|
break;
|
|
case QWindow::Minimized:
|
|
setInterfaceNormal();
|
|
requestActivate();
|
|
break;
|
|
default:
|
|
setInterfaceHiden();
|
|
break;
|
|
}
|
|
}
|
|
|
|
|
|
void InterfaceWindowHandler::setInterfaceVisible(bool visible)
|
|
{
|
|
if (visible)
|
|
{
|
|
switch ( m_window->visibility() )
|
|
{
|
|
case QWindow::Hidden:
|
|
setInterfaceShown();
|
|
break;
|
|
case QWindow::Minimized:
|
|
setInterfaceNormal();
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
requestActivate();
|
|
}
|
|
else
|
|
{
|
|
setInterfaceHiden();
|
|
}
|
|
}
|
|
|
|
|
|
void InterfaceWindowHandler::setFullScreen( bool fs )
|
|
{
|
|
WindowStateHolder::holdFullscreen(m_window, WindowStateHolder::INTERFACE, fs);
|
|
}
|
|
|
|
void InterfaceWindowHandler::setInterfaceFullScreen( bool fs )
|
|
{
|
|
setFullScreen(fs);
|
|
emit interfaceFullScreenChanged( fs );
|
|
}
|
|
|
|
void InterfaceWindowHandler::setRaise()
|
|
{
|
|
requestActivate();
|
|
m_window->raise();
|
|
}
|
|
|
|
void InterfaceWindowHandler::setBoss()
|
|
{
|
|
THEMPL->pause();
|
|
if( m_mainCtx->getSysTray() )
|
|
{
|
|
setInterfaceHiden();
|
|
}
|
|
else
|
|
{
|
|
setInterfaceMinimized();
|
|
}
|
|
}
|
|
|
|
void InterfaceWindowHandler::setInterfaceHiden()
|
|
{
|
|
#if QT_VERSION < QT_VERSION_CHECK(6, 9, 0)
|
|
// `CompositorWayland` depends on the promise that the window resources are not deleted.
|
|
// Window's `wl_surface` gets deleted when the window gets hidden before Qt 6.9.0. So,
|
|
// here we should inhibit hiding the window. Note that intercepting `QEvent::hide` does
|
|
// not make it possible to inhibit hiding the window.
|
|
assert(p_intf);
|
|
assert(p_intf->p_compositor);
|
|
static bool inhibitHide = [intf = p_intf]() {
|
|
assert(qGuiApp);
|
|
// type() == WaylandCompositor is not used because any video embedding methodology may
|
|
// depend on window resources:
|
|
const bool ret = qGuiApp->platformName().startsWith(QLatin1String("wayland")) &&
|
|
dynamic_cast<vlc::CompositorVideo*>(intf->p_compositor.get());
|
|
if (ret)
|
|
msg_Warn(intf, "In this configuration, the interface window can not get hidden.");
|
|
return ret;
|
|
}();
|
|
|
|
if (inhibitHide)
|
|
{
|
|
// Instead of doing nothing, minimize the window:
|
|
setInterfaceMinimized();
|
|
return;
|
|
}
|
|
#endif
|
|
|
|
m_window->hide();
|
|
}
|
|
|
|
void InterfaceWindowHandler::setInterfaceShown()
|
|
{
|
|
m_window->setVisible(true);
|
|
}
|
|
|
|
void InterfaceWindowHandler::setInterfaceMinimized()
|
|
{
|
|
setWindowState(m_window, Qt::WindowMinimized);
|
|
}
|
|
|
|
void InterfaceWindowHandler::setInterfaceMaximized()
|
|
{
|
|
setWindowState(m_window, Qt::WindowMaximized);
|
|
}
|
|
|
|
void InterfaceWindowHandler::setInterfaceNormal()
|
|
{
|
|
m_window->showNormal();
|
|
}
|
|
|
|
|
|
void InterfaceWindowHandler::requestActivate()
|
|
{
|
|
m_window->requestActivate();
|
|
}
|
|
|
|
void InterfaceWindowHandler::setInterfaceAlwaysOnTop( bool on_top )
|
|
{
|
|
WindowStateHolder::holdOnTop(m_window, WindowStateHolder::INTERFACE, on_top);
|
|
}
|
|
|
|
// Functions private
|
|
|
|
bool InterfaceWindowHandler::applyKeyEvent(QKeyEvent * event) const
|
|
{
|
|
int key = event->key();
|
|
|
|
// NOTE: We have to make sure tab and backtab are never used as hotkeys. When browsing the
|
|
// MediaLibrary, we let the view handle the navigation keys.
|
|
if (key == Qt::Key_Tab || key == Qt::Key_Backtab || m_mainCtx->preferHotkeys() == false)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
QQuickItem * item = p_intf->p_compositor->activeFocusItem();
|
|
|
|
// NOTE: When the item has visual focus we let it handle the key. When the item does not
|
|
// inherit from QQuickControl we have to declare the 'visualFocus' property ourselves.
|
|
if (item)
|
|
{
|
|
|
|
QVariant visualFocus = QQmlProperty::read(item, "visualFocus", qmlContext(item));
|
|
if (visualFocus.isValid())
|
|
{
|
|
if (visualFocus.toBool())
|
|
return false;
|
|
}
|
|
//while being QuickControls TextField and TextArea don't provide visualFocus property
|
|
//here we check their (non control) parent class, this should cover all text input widgets
|
|
else if (item->inherits("QQuickTextInput") || item->inherits("QQuickTextEdit"))
|
|
{
|
|
QVariant activeFocus = QQmlProperty::read(item, "activeFocus", qmlContext(item));
|
|
if (activeFocus.isValid())
|
|
{
|
|
if (activeFocus.toBool())
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
event->accept();
|
|
|
|
return true;
|
|
}
|
|
|