From 7986a170c4fc49fcd05083f3d4b0c97b75409d4e Mon Sep 17 00:00:00 2001 From: Pierre Lamot Date: Fri, 11 Oct 2024 15:15:29 +0200 Subject: [PATCH] qt: introduce CSD menu this menu is usually shown by window manager as contextual menu in the topbar --- modules/gui/qt/Makefile.am | 2 + modules/gui/qt/maininterface/mainui.cpp | 3 +- modules/gui/qt/meson.build | 3 + modules/gui/qt/util/csdmenu.cpp | 323 ++++++++++++++++++++++++ modules/gui/qt/util/csdmenu.hpp | 57 +++++ modules/gui/qt/util/csdmenu_module.h | 91 +++++++ po/POTFILES.in | 1 + 7 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 modules/gui/qt/util/csdmenu.cpp create mode 100644 modules/gui/qt/util/csdmenu.hpp create mode 100644 modules/gui/qt/util/csdmenu_module.h diff --git a/modules/gui/qt/Makefile.am b/modules/gui/qt/Makefile.am index 255e432c7e..b75161407c 100644 --- a/modules/gui/qt/Makefile.am +++ b/modules/gui/qt/Makefile.am @@ -296,6 +296,7 @@ libqt_plugin_la_SOURCES = \ util/covergenerator.hpp \ util/imageluminanceextractor.cpp util/imageluminanceextractor.hpp \ util/csdbuttonmodel.cpp util/csdbuttonmodel.hpp \ + util/csdmenu.cpp util/csdmenu.hpp \ util/imagehelper.cpp util/imagehelper.hpp \ util/keyhelper.cpp util/keyhelper.hpp \ util/listcache.hxx util/listcache.hpp \ @@ -470,6 +471,7 @@ nodist_libqt_plugin_la_SOURCES = \ util/vlcaccess_image_provider.moc.cpp \ util/imageluminanceextractor.moc.cpp \ util/csdbuttonmodel.moc.cpp \ + util/csdmenu.moc.cpp \ util/keyhelper.moc.cpp \ util/listcache.moc.cpp \ util/locallistcacheloader.moc.cpp \ diff --git a/modules/gui/qt/maininterface/mainui.cpp b/modules/gui/qt/maininterface/mainui.cpp index d6016db69a..0a7fa17711 100644 --- a/modules/gui/qt/maininterface/mainui.cpp +++ b/modules/gui/qt/maininterface/mainui.cpp @@ -33,6 +33,7 @@ #include "playlist/playlist_model.hpp" #include "playlist/playlist_controller.hpp" +#include "util/csdmenu.hpp" #include "util/item_key_event_filter.hpp" #include "util/imageluminanceextractor.hpp" #include "util/keyhelper.hpp" @@ -182,8 +183,8 @@ void MainUI::registerQMLTypes() qmlRegisterUncreatableType(uri, versionMajor, versionMinor, "VLCVarChoiceModel", "generic variable with choice model" ); qmlRegisterUncreatableType(uri, versionMajor, versionMinor, "CSDButton", ""); qmlRegisterUncreatableType(uri, versionMajor, versionMinor, "CSDButtonModel", "has CSD buttons and provides for communicating CSD events between UI and backend"); + qmlRegisterTypesAndRevisions( uri, versionMajor); qmlRegisterUncreatableType( uri, versionMajor, versionMinor, "Navigation", "Navigation is only available via attached properties"); - qmlRegisterModule(uri, versionMajor, versionMinor); qmlProtectModule(uri, versionMajor); } diff --git a/modules/gui/qt/meson.build b/modules/gui/qt/meson.build index 9b9cc35fa6..fd0ddfdaf8 100644 --- a/modules/gui/qt/meson.build +++ b/modules/gui/qt/meson.build @@ -128,6 +128,7 @@ moc_headers = files( 'util/color_svg_image_provider.hpp', 'util/vlcaccess_image_provider.hpp', 'util/csdbuttonmodel.hpp', + 'util/csdmenu.hpp', 'util/imageluminanceextractor.hpp', 'util/keyhelper.hpp', 'util/listcache.hpp', @@ -431,6 +432,8 @@ some_sources = files( 'util/covergenerator.hpp', 'util/csdbuttonmodel.cpp', 'util/csdbuttonmodel.hpp', + 'util/csdmenu.cpp', + 'util/csdmenu.hpp', 'util/imageluminanceextractor.cpp', 'util/imageluminanceextractor.hpp', 'util/imagehelper.cpp', diff --git a/modules/gui/qt/util/csdmenu.cpp b/modules/gui/qt/util/csdmenu.cpp new file mode 100644 index 0000000000..9ad3586f4d --- /dev/null +++ b/modules/gui/qt/util/csdmenu.cpp @@ -0,0 +1,323 @@ +/***************************************************************************** + * Copyright (C) 2024 VLC authors and VideoLAN + * + * 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 "csdmenu.hpp" + +#include +#include +#include + +#ifdef QT_GUI_PRIVATE +#include +#include +#include +#endif + +#include "maininterface/mainctx.hpp" +#include "util/csdmenu_module.h" +#include "menus/menus.hpp" + +#include +#include + +struct xdg_surface; + +namespace { + +//vlc module callback +static int csdMenuInfoOpen(void *func, bool forced, va_list ap) +{ + VLC_UNUSED(forced); + qt_csd_menu_open open = reinterpret_cast(func); + qt_csd_menu_t* obj = va_arg(ap, qt_csd_menu_t*); + qt_csd_menu_info* info = va_arg(ap, qt_csd_menu_info*); + return open(obj, info); +} + +bool isWindowFixedSize(const QWindow *window) +{ + if (window->flags() & Qt::MSWindowsFixedSizeDialogHint) + return true; + + const auto minSize = window->minimumSize(); + const auto maxSize = window->maximumSize(); + + return minSize.isValid() && maxSize.isValid() && minSize == maxSize; +} + +} + +class CSDMenuPrivate +{ + Q_DECLARE_PUBLIC(CSDMenu) +public: + + CSDMenuPrivate(CSDMenu* parent) + : q_ptr(parent) + { + const QString& platform = qApp->platformName();; + if (platform == QLatin1String("windows") || platform == QLatin1String("direct2d")) + m_plateform = QT_CSD_PLATFORM_WINDOWS; + else if (platform.startsWith( QLatin1String("wayland"))) + m_plateform = QT_CSD_PLATFORM_WAYLAND; + else if (platform == QLatin1String("xcb")) + m_plateform = QT_CSD_PLATFORM_X11; + } + + static void notifyMenuVisible(void* data, bool visible) + { + auto that = static_cast(data); + that->m_menuVisible = visible; + emit that->q_func()->menuVisibleChanged(visible); + } + + void fallbackMenu(QPoint pos) + { + auto menu = new VLCMenu(m_ctx->getIntf()); + menu->setAttribute(Qt::WA_DeleteOnClose); + + QObject::connect(menu, &QMenu::aboutToShow, q_ptr, [this](){ + notifyMenuVisible(this, true); + }); + + QObject::connect(menu, &QMenu::aboutToHide, q_ptr, [this](){ + notifyMenuVisible(this, false); + }); + + QWindow::Visibility visibility = m_ctx->interfaceVisibility(); + QAction* action; + action = menu->addAction(qtr("Restore")); + action->setEnabled(visibility != QWindow::Windowed); + action->connect(action, &QAction::triggered, q_ptr, [this]() { + m_ctx->requestInterfaceNormal(); + }); + action = menu->addAction(qtr("Minimize")); + action->connect(action, &QAction::triggered, q_ptr, [this]() { + m_ctx->requestInterfaceMinimized(); + }); + action = menu->addAction(qtr("Maximized")); + action->setEnabled(visibility != QWindow::Maximized); + action->connect(action, &QAction::triggered, q_ptr, [this]() { + m_ctx->requestInterfaceMaximized(); + }); + action = menu->addAction(qtr("Always on top")); + action->setCheckable(true); + action->setChecked(m_ctx->isInterfaceAlwaysOnTop()); + action->connect(action, &QAction::triggered, q_ptr, [this](bool checked) { + m_ctx->setInterfaceAlwaysOnTop(checked); + }); + action = menu->addAction(qtr("Close")); + action->connect(action, &QAction::triggered, q_ptr, [this]() { + m_ctx->intfMainWindow()->close(); + }); + + /* Ideally we should allow moving and resizing the window through startSystemXXX + * But, startSystemXXX is usually active from a mouse press until a mouse release, + * so calling it from a menu is broken with some desktop environments + */ + + menu->popup(pos); + } + + void loadModule() + { + QWindow* window = m_ctx->intfMainWindow(); + assert(window); + + qt_csd_menu_info info = {}; + info.platform = QT_CSD_PLATFORM_UNKNOWN; +#ifdef _WIN32 + if (m_plateform == QT_CSD_PLATFORM_WINDOWS) + { + info.platform = QT_CSD_PLATFORM_WINDOWS; + }; +#else +#ifdef QT_GUI_PRIVATE + if (m_plateform == QT_CSD_PLATFORM_X11) + { + QPlatformNativeInterface* native = qApp->platformNativeInterface(); + info.platform = QT_CSD_PLATFORM_X11; + info.data.x11.rootwindow = reinterpret_cast(native->nativeResourceForIntegration(QByteArrayLiteral("rootwindow"))); + info.data.x11.connection = reinterpret_cast(native->nativeResourceForIntegration(QByteArrayLiteral("connection"))); + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + if (m_plateform == QT_CSD_PLATFORM_WAYLAND) + { + info.platform = QT_CSD_PLATFORM_WAYLAND; + + auto appNative = qApp->nativeInterface(); + auto windowNative = window->nativeInterface(); + + info.data.wayland.display = appNative->display(); + info.data.wayland.toplevel = windowNative->surfaceRole(); + } +#endif // QT 6.5 +#endif // QT_GUI_PRIVATE +#endif // _WIN32 + info.isRtl = QGuiApplication::isRightToLeft(); + + info.userData = this; + info.notifyMenuVisible = ¬ifyMenuVisible; + + m_systemMenuState = UNAVAILABLE; + m_systemMenu = vlc_object_create(m_ctx->getIntf()); + if (!m_systemMenu) + return; + m_module = vlc_module_load(vlc_object_logger(m_systemMenu), "qtcsdmenu", nullptr, false, &csdMenuInfoOpen, m_systemMenu, &info); + if (!m_module) + { + vlc_object_delete(m_systemMenu); + m_systemMenu = nullptr; + return; + } + m_systemMenuState = AVAILABLE; + } + + void unloadModule() + { + if (m_module) + { + module_unneed(m_systemMenu, m_module); + m_module = nullptr; + } + if (m_systemMenu) + { + vlc_object_delete(m_systemMenu); + m_systemMenu = nullptr; + } + m_systemMenuState = CSDMenuPrivate::NEED_INITIALISATION; + } + + enum { + NEED_INITIALISATION, + UNAVAILABLE, + AVAILABLE + } m_systemMenuState = NEED_INITIALISATION; + + module_t* m_module = nullptr; + qt_csd_menu_t* m_systemMenu = nullptr; + + qt_csd_menu_platform m_plateform = QT_CSD_PLATFORM_UNKNOWN; + MainCtx* m_ctx = nullptr; + bool m_menuVisible = false; + + CSDMenu* q_ptr; +}; + +CSDMenu::CSDMenu(QObject* parent) + : QObject(parent) + , d_ptr(new CSDMenuPrivate(this)) +{ +} + +CSDMenu::~CSDMenu() +{ + Q_D(CSDMenu); + d->unloadModule(); +} + +MainCtx* CSDMenu::getCtx() const +{ + Q_D(const CSDMenu); + return d->m_ctx; +} + +void CSDMenu::setCtx(MainCtx* ctx) +{ + Q_D(CSDMenu); + + if (d->m_ctx == ctx) + return; + d->m_ctx = ctx; + emit ctxChanged(ctx); +} + +bool CSDMenu::getMenuVisible() const +{ + Q_D(const CSDMenu); + return d->m_menuVisible; +} + +void CSDMenu::popup(const QPoint &pos) +{ + Q_D(CSDMenu); + + assert(d->m_ctx); + + QWindow* window = d->m_ctx->intfMainWindow(); + assert(window); + + if (d->m_systemMenuState == CSDMenuPrivate::NEED_INITIALISATION) + d->loadModule(); + + if (d->m_systemMenuState == CSDMenuPrivate::AVAILABLE) { + assert(d->m_systemMenu); + + qreal qtDpr = window->devicePixelRatio(); + QPlatformWindow* nativeWindow = window->handle(); + qreal nativeDpr = nativeWindow->devicePixelRatio(); + + qt_csd_menu_event event = {}; + event.platform = QT_CSD_PLATFORM_UNKNOWN; + event.x = (pos.x() * qtDpr) / nativeDpr; + event.y = (pos.y() * qtDpr) / nativeDpr; + + const auto winState = window->windowStates(); + int windowFlags = (winState.testFlag(Qt::WindowMaximized) ? QT_CSD_WINDOW_MAXIMIZED : 0) + | (winState.testFlag(Qt::WindowFullScreen) ? QT_CSD_WINDOW_FULLSCREEN : 0) + | (winState.testFlag(Qt::WindowMinimized) ? QT_CSD_WINDOW_MINIMIZED: 0) + | (isWindowFixedSize(window) ? QT_CSD_WINDOW_FIXED_SIZE : 0); + + event.windowState = static_cast(windowFlags); + +#ifdef _WIN32 + if (d->m_plateform == QT_CSD_PLATFORM_WINDOWS) + { + event.platform = QT_CSD_PLATFORM_WINDOWS; + event.data.win32.hwnd = window->winId() + } +#else + +#ifdef QT_GUI_PRIVATE + if (d->m_plateform == QT_CSD_PLATFORM_X11) + { + event.platform = QT_CSD_PLATFORM_X11; + event.data.x11.window = window->winId(); + } + +#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0) + if (d->m_plateform == QT_CSD_PLATFORM_WAYLAND) + { + event.platform = QT_CSD_PLATFORM_WAYLAND; + auto appNative = qApp->nativeInterface(); + assert(appNative); + event.data.wayland.seat = appNative->lastInputSeat(); + event.data.wayland.serial = appNative->lastInputSerial(); + } +#endif // Qt 6.5 +#endif // QT_GUI_PRIVATE +#endif // _WIN32 + + bool ret = d->m_systemMenu->popup(d->m_systemMenu, &event); + if (ret) + return; + } + + d->fallbackMenu(pos); +} + diff --git a/modules/gui/qt/util/csdmenu.hpp b/modules/gui/qt/util/csdmenu.hpp new file mode 100644 index 0000000000..0b5fabe8ef --- /dev/null +++ b/modules/gui/qt/util/csdmenu.hpp @@ -0,0 +1,57 @@ +/***************************************************************************** + * Copyright (C) 2024 VLC authors and VideoLAN + * + * 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. + *****************************************************************************/ +#ifndef CSDMENU_HPP +#define CSDMENU_HPP + +#include +#include + +Q_MOC_INCLUDE("maininterface/mainctx.hpp") +class MainCtx; + +class CSDMenuPrivate; +class CSDMenu : public QObject +{ + Q_OBJECT + QML_ELEMENT + + Q_PROPERTY(MainCtx* ctx READ getCtx WRITE setCtx NOTIFY ctxChanged FINAL) + Q_PROPERTY(bool menuVisible READ getMenuVisible NOTIFY menuVisibleChanged FINAL) + +public: + explicit CSDMenu(QObject* parent = nullptr); + ~CSDMenu(); + +public: + Q_INVOKABLE void popup(const QPoint &windowpos); + + MainCtx* getCtx() const; + void setCtx(MainCtx* ctx); + + bool getMenuVisible() const; + +signals: + void ctxChanged(MainCtx*); + void menuVisibleChanged(bool); + +protected: + QScopedPointer d_ptr; + Q_DECLARE_PRIVATE(CSDMenu) +}; + +#endif // CSDMENU_HPP diff --git a/modules/gui/qt/util/csdmenu_module.h b/modules/gui/qt/util/csdmenu_module.h new file mode 100644 index 0000000000..7bffa18be3 --- /dev/null +++ b/modules/gui/qt/util/csdmenu_module.h @@ -0,0 +1,91 @@ +#ifndef QTCSDMENU_MODULE_H +#define QTCSDMENU_MODULE_H + +#ifdef HAVE_CONFIG_H +# include "config.h" +#endif + +#include +#include +#include + +#include + +struct xcb_connection_t; + +struct wl_display; +struct wl_seat; +struct xdg_toplevel; + +typedef enum { + QT_CSD_PLATFORM_UNKNOWN, + QT_CSD_PLATFORM_WAYLAND, + QT_CSD_PLATFORM_X11, + QT_CSD_PLATFORM_WINDOWS +} qt_csd_menu_platform; + +typedef enum { + QT_CSD_WINDOW_FULLSCREEN = (1 << 0), + QT_CSD_WINDOW_MAXIMIZED = (1 << 1), + QT_CSD_WINDOW_MINIMIZED = (1 << 2), + QT_CSD_WINDOW_FIXED_SIZE = (1 << 3) +} qt_csd_menu_window_state; + + +typedef void (*notifyMenuVisibleCallback)(void* userData, bool visible); + +//information about the window, plateform dependent +typedef struct { + qt_csd_menu_platform platform; + union { + struct { + struct xcb_connection_t* connection; + uint32_t rootwindow; + } x11; + struct { + struct wl_display* display; + struct xdg_toplevel* toplevel; + } wayland; + struct { + void* hwnd; + } windows; + } data; + + //is the UI rtl or ltr + bool isRtl; + + //callback called to notify that the menu is visible/hidden + notifyMenuVisibleCallback notifyMenuVisible; + //user data passed to callbacks + void* userData; +} qt_csd_menu_info; + +typedef struct { + qt_csd_menu_platform platform; + union { + struct { + struct wl_seat* seat; + uint32_t serial; + } wayland; + struct { + uint32_t window; + } x11; + struct { + void* hwnd; + } win32; + } data; + int x; + int y; + qt_csd_menu_window_state windowState; +} qt_csd_menu_event; + +typedef struct qt_csd_menu_t { + struct vlc_object_t obj; + void* p_sys; + + bool (*popup)(struct qt_csd_menu_t* menu, qt_csd_menu_event* event); +} qt_csd_menu_t; + +typedef int (*qt_csd_menu_open)(qt_csd_menu_t* obj, qt_csd_menu_info* info); + +#endif // QTCSDMENU_MODULE_H diff --git a/po/POTFILES.in b/po/POTFILES.in index 2363f983d3..f8e129a40e 100644 --- a/po/POTFILES.in +++ b/po/POTFILES.in @@ -884,6 +884,7 @@ modules/gui/qt/qt.hpp modules/gui/qt/util/color_scheme_model.cpp modules/gui/qt/util/covergenerator.cpp modules/gui/qt/util/covergenerator.hpp +modules/gui/qt/util/csdmenu.cpp modules/gui/qt/util/imagehelper.cpp modules/gui/qt/util/imagehelper.hpp modules/gui/qt/util/qt_dirs.cpp