/***************************************************************************** * 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 #include #include "util/keyhelper.hpp" #include "dialogs/systray/systray.hpp" #include "widgets/native/qvlcframe.hpp" #include "dialogs/dialogs/dialogmodel.hpp" #include #include #include 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(); // 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(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 (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 (keyEvent->key()), keyEvent->modifiers()); return true; } case QEvent::KeyRelease: { return applyKeyEvent(static_cast (event)); } case QEvent::Wheel: { QWheelEvent* wheelEvent = static_cast(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(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; }