From 6b9b1fcb53a1ff5c2e8bf8b6e55e02a4b24827f8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 25 Jul 2024 20:44:26 -0700 Subject: [PATCH] core/menu: add QsMenuAnchor for more control of platform menus --- src/core/CMakeLists.txt | 1 + src/core/module.md | 1 + src/core/platformmenu.cpp | 28 +++++++ src/core/platformmenu.hpp | 2 + src/core/popupanchor.cpp | 12 +-- src/core/qsmenuanchor.cpp | 105 +++++++++++++++++++++++++++ src/core/qsmenuanchor.hpp | 86 ++++++++++++++++++++++ src/services/status_notifier/qml.cpp | 8 +- src/services/status_notifier/qml.hpp | 2 + src/wayland/platformmenu.cpp | 21 +++--- 10 files changed, 245 insertions(+), 21 deletions(-) create mode 100644 src/core/qsmenuanchor.cpp create mode 100644 src/core/qsmenuanchor.hpp diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index b70681bc..fbf006b2 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -35,6 +35,7 @@ qt_add_library(quickshell-core STATIC retainable.cpp popupanchor.cpp types.cpp + qsmenuanchor.cpp ) set_source_files_properties(main.cpp PROPERTIES COMPILE_DEFINITIONS GIT_REVISION="${GIT_REVISION}") diff --git a/src/core/module.md b/src/core/module.md index 9bf7bf25..411b1d49 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -26,5 +26,6 @@ headers = [ "retainable.hpp", "popupanchor.hpp", "types.hpp", + "qsmenuanchor.hpp", ] ----- diff --git a/src/core/platformmenu.cpp b/src/core/platformmenu.cpp index 7b31c871..0f416a20 100644 --- a/src/core/platformmenu.cpp +++ b/src/core/platformmenu.cpp @@ -16,6 +16,7 @@ #include #include "generation.hpp" +#include "popupanchor.hpp" #include "proxywindow.hpp" #include "qsmenu.hpp" #include "windowinterface.hpp" @@ -111,6 +112,33 @@ bool PlatformMenuEntry::display(QObject* parentWindow, int relativeX, int relati return true; } +bool PlatformMenuEntry::display(PopupAnchor* anchor) { + if (!anchor->backingWindow() || !anchor->backingWindow()->isVisible()) { + qCritical() << "Cannot display PlatformMenuEntry on anchor without visible window."; + return false; + } + + if (ACTIVE_MENU && this->qmenu != ACTIVE_MENU) { + ACTIVE_MENU->close(); + } + + ACTIVE_MENU = this->qmenu; + + this->qmenu->createWinId(); + this->qmenu->windowHandle()->setTransientParent(anchor->backingWindow()); + + // Update the window geometry to the menu's actual dimensions so reposition + // can accurately adjust it if applicable for the current platform. + this->qmenu->windowHandle()->setGeometry({{0, 0}, this->qmenu->sizeHint()}); + + PopupPositioner::instance()->reposition(anchor, this->qmenu->windowHandle(), false); + + // Open the menu at the position determined by the popup positioner. + this->qmenu->popup(this->qmenu->windowHandle()->position()); + + return true; +} + void PlatformMenuEntry::relayout() { if (this->menu->hasChildren()) { delete this->qaction; diff --git a/src/core/platformmenu.hpp b/src/core/platformmenu.hpp index c1e39096..85aaffac 100644 --- a/src/core/platformmenu.hpp +++ b/src/core/platformmenu.hpp @@ -13,6 +13,7 @@ #include #include +#include "popupanchor.hpp" #include "qsmenu.hpp" namespace qs::menu::platform { @@ -38,6 +39,7 @@ public: Q_DISABLE_COPY_MOVE(PlatformMenuEntry); bool display(QObject* parentWindow, int relativeX, int relativeY); + bool display(PopupAnchor* anchor); static void registerCreationHook(std::function hook); diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index c0e60ca6..2534c114 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -184,9 +184,9 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only auto effectiveY = calcEffectiveY(); if (adjustment.testFlag(PopupAdjustment::FlipX)) { - bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left()) - || (anchorGravity.testFlag(Edges::Right) - && effectiveX + windowGeometry.width() > screenGeometry.right()); + const bool flip = (anchorGravity.testFlag(Edges::Left) && effectiveX < screenGeometry.left()) + || (anchorGravity.testFlag(Edges::Right) + && effectiveX + windowGeometry.width() > screenGeometry.right()); if (flip) { anchorGravity ^= Edges::Left | Edges::Right; @@ -200,9 +200,9 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only } if (adjustment.testFlag(PopupAdjustment::FlipY)) { - bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top()) - || (anchorGravity.testFlag(Edges::Bottom) - && effectiveY + windowGeometry.height() > screenGeometry.bottom()); + const bool flip = (anchorGravity.testFlag(Edges::Top) && effectiveY < screenGeometry.top()) + || (anchorGravity.testFlag(Edges::Bottom) + && effectiveY + windowGeometry.height() > screenGeometry.bottom()); if (flip) { anchorGravity ^= Edges::Top | Edges::Bottom; diff --git a/src/core/qsmenuanchor.cpp b/src/core/qsmenuanchor.cpp new file mode 100644 index 00000000..e6af7865 --- /dev/null +++ b/src/core/qsmenuanchor.cpp @@ -0,0 +1,105 @@ +#include "qsmenuanchor.hpp" + +#include +#include +#include + +#include "platformmenu.hpp" +#include "popupanchor.hpp" +#include "qsmenu.hpp" + +using qs::menu::platform::PlatformMenuEntry; + +namespace qs::menu { + +QsMenuAnchor::~QsMenuAnchor() { this->onClosed(); } + +void QsMenuAnchor::open() { + if (this->mOpen) { + qCritical() << "Cannot call QsMenuAnchor.open() as it is already open."; + return; + } + + if (!this->mMenu) { + qCritical() << "Cannot open QsMenuAnchor with no menu attached."; + return; + } + + this->mOpen = true; + + if (this->mMenu->menu()) this->onMenuChanged(); + QObject::connect(this->mMenu, &QsMenuHandle::menuChanged, this, &QsMenuAnchor::onMenuChanged); + this->mMenu->refHandle(); + + emit this->visibleChanged(); +} + +void QsMenuAnchor::onMenuChanged() { + // close menu if the path changes + if (this->platformMenu || !this->mMenu->menu()) { + this->onClosed(); + return; + } + + this->platformMenu = new PlatformMenuEntry(this->mMenu->menu()); + QObject::connect(this->platformMenu, &PlatformMenuEntry::closed, this, &QsMenuAnchor::onClosed); + + auto success = this->platformMenu->display(&this->mAnchor); + if (!success) this->onClosed(); + else emit this->opened(); +} + +void QsMenuAnchor::close() { + if (!this->mOpen) { + qCritical() << "Cannot close QsMenuAnchor as it isn't open."; + return; + } + + this->onClosed(); +} + +void QsMenuAnchor::onClosed() { + if (!this->mOpen) return; + + this->mOpen = false; + + if (this->platformMenu) { + this->platformMenu->deleteLater(); + this->platformMenu = nullptr; + } + + QObject::disconnect(this->mMenu, &QsMenuHandle::menuChanged, this, &QsMenuAnchor::onMenuChanged); + this->mMenu->unrefHandle(); + emit this->closed(); + emit this->visibleChanged(); +} + +PopupAnchor* QsMenuAnchor::anchor() { return &this->mAnchor; } + +QsMenuHandle* QsMenuAnchor::menu() const { return this->mMenu; } + +void QsMenuAnchor::setMenu(QsMenuHandle* menu) { + if (menu == this->mMenu) return; + + if (this->mMenu != nullptr) { + if (this->platformMenu != nullptr) this->platformMenu->deleteLater(); + QObject::disconnect(this->mMenu, nullptr, this, nullptr); + } + + this->mMenu = menu; + + if (menu != nullptr) { + QObject::connect(menu, &QObject::destroyed, this, &QsMenuAnchor::onMenuDestroyed); + } + + emit this->menuChanged(); +} + +bool QsMenuAnchor::isVisible() const { return this->mOpen; } + +void QsMenuAnchor::onMenuDestroyed() { + this->mMenu = nullptr; + emit this->menuChanged(); +} + +} // namespace qs::menu diff --git a/src/core/qsmenuanchor.hpp b/src/core/qsmenuanchor.hpp new file mode 100644 index 00000000..683895ab --- /dev/null +++ b/src/core/qsmenuanchor.hpp @@ -0,0 +1,86 @@ +#pragma once + +#include +#include +#include + +#include "platformmenu.hpp" +#include "popupanchor.hpp" +#include "qsmenu.hpp" + +namespace qs::menu { + +///! Display anchor for platform menus. +class QsMenuAnchor: public QObject { + Q_OBJECT; + /// The menu's anchor / positioner relative to another window. The menu will not be + /// shown until it has a valid anchor. + /// + /// > [!INFO] *The following is subject to change and NOT a guarantee of future behavior.* + /// > + /// > A snapshot of the anchor at the time @@opened(s) is emitted will be + /// > used to position the menu. Additional changes to the anchor after this point + /// > will not affect the placement of the menu. + /// + /// You can set properties of the anchor like so: + /// ```qml + /// QsMenuAnchor { + /// anchor.window: parentwindow + /// // or + /// anchor { + /// window: parentwindow + /// } + /// } + /// ``` + Q_PROPERTY(PopupAnchor* anchor READ anchor CONSTANT); + /// The menu that should be displayed on this anchor. + /// + /// See also: @@Quickshell.Services.SystemTray.SystemTrayItem.menu. + Q_PROPERTY(QsMenuHandle* menu READ menu WRITE setMenu NOTIFY menuChanged); + /// If the menu is currently open and visible. + /// + /// See also: @@open(), @@close(). + Q_PROPERTY(bool visible READ isVisible NOTIFY visibleChanged); + QML_ELEMENT; + +public: + explicit QsMenuAnchor(QObject* parent = nullptr): QObject(parent) {} + ~QsMenuAnchor() override; + Q_DISABLE_COPY_MOVE(QsMenuAnchor); + + /// Open the given menu on this menu Requires that @@anchor is valid. + Q_INVOKABLE void open(); + /// Close the open menu. + Q_INVOKABLE void close(); + + [[nodiscard]] PopupAnchor* anchor(); + + [[nodiscard]] QsMenuHandle* menu() const; + void setMenu(QsMenuHandle* menu); + + [[nodiscard]] bool isVisible() const; + +signals: + /// Sent when the menu is displayed onscreen which may be after @@visible + /// becomes true. + void opened(); + /// Sent when the menu is closed. + void closed(); + + void menuChanged(); + void visibleChanged(); + +private slots: + void onMenuChanged(); + void onMenuDestroyed(); + +private: + void onClosed(); + + PopupAnchor mAnchor {this}; + QsMenuHandle* mMenu = nullptr; + bool mOpen = false; + platform::PlatformMenuEntry* platformMenu = nullptr; +}; + +} // namespace qs::menu diff --git a/src/services/status_notifier/qml.cpp b/src/services/status_notifier/qml.cpp index 1530e5f9..d39963f4 100644 --- a/src/services/status_notifier/qml.cpp +++ b/src/services/status_notifier/qml.cpp @@ -141,12 +141,8 @@ void SystemTrayItem::display(QObject* parentWindow, qint32 relativeX, qint32 rel if (!success) delete platform; }; - if (handle->menu()) { - onMenuChanged(); - } else { - QObject::connect(handle, &DBusMenuHandle::menuChanged, this, onMenuChanged); - } - + if (handle->menu()) onMenuChanged(); + QObject::connect(handle, &DBusMenuHandle::menuChanged, this, onMenuChanged); handle->refHandle(); } diff --git a/src/services/status_notifier/qml.hpp b/src/services/status_notifier/qml.hpp index b6aa8366..343fa5b6 100644 --- a/src/services/status_notifier/qml.hpp +++ b/src/services/status_notifier/qml.hpp @@ -70,6 +70,8 @@ class SystemTrayItem: public QObject { /// If this tray item has an associated menu accessible via @@display() or @@menu. Q_PROPERTY(bool hasMenu READ hasMenu NOTIFY hasMenuChanged); /// A handle to the menu associated with this tray item, if any. + /// + /// Can be displayed with @@Quickshell.QsMenuAnchor or @@Quickshell.QsMenuOpener. Q_PROPERTY(QsMenuHandle* menu READ menu NOTIFY hasMenuChanged); /// If this tray item only offers a menu and activation will do nothing. Q_PROPERTY(bool onlyMenu READ onlyMenu NOTIFY onlyMenuChanged); diff --git a/src/wayland/platformmenu.cpp b/src/wayland/platformmenu.cpp index ffe9548d..80f9854e 100644 --- a/src/wayland/platformmenu.cpp +++ b/src/wayland/platformmenu.cpp @@ -15,15 +15,6 @@ using namespace qs::menu::platform; void platformMenuHook(PlatformMenuQMenu* menu) { auto* window = menu->windowHandle(); - auto constraintAdjustment = QtWayland::xdg_positioner::constraint_adjustment_flip_x - | QtWayland::xdg_positioner::constraint_adjustment_flip_y - | QtWayland::xdg_positioner::constraint_adjustment_slide_x - | QtWayland::xdg_positioner::constraint_adjustment_slide_y - | QtWayland::xdg_positioner::constraint_adjustment_resize_x - | QtWayland::xdg_positioner::constraint_adjustment_resize_y; - - window->setProperty("_q_waylandPopupConstraintAdjustment", constraintAdjustment); - Qt::Edges anchor; Qt::Edges gravity; @@ -43,6 +34,9 @@ void platformMenuHook(PlatformMenuQMenu* menu) { anchor = Qt::TopEdge | sideEdge; gravity = Qt::BottomEdge | sideEdge; } else if (auto* parent = window->transientParent()) { + // abort if already set by a PopupAnchor + if (window->property("_q_waylandPopupAnchorRect").isValid()) return; + // The menu geometry will be adjusted to flip internally by qt already, but it ends up off by // one pixel which causes the compositor to also flip which results in the menu being placed // left of the edge by its own width. To work around this the intended position is stored prior @@ -56,6 +50,15 @@ void platformMenuHook(PlatformMenuQMenu* menu) { window->setProperty("_q_waylandPopupAnchor", QVariant::fromValue(anchor)); window->setProperty("_q_waylandPopupGravity", QVariant::fromValue(gravity)); + + auto constraintAdjustment = QtWayland::xdg_positioner::constraint_adjustment_flip_x + | QtWayland::xdg_positioner::constraint_adjustment_flip_y + | QtWayland::xdg_positioner::constraint_adjustment_slide_x + | QtWayland::xdg_positioner::constraint_adjustment_slide_y + | QtWayland::xdg_positioner::constraint_adjustment_resize_x + | QtWayland::xdg_positioner::constraint_adjustment_resize_y; + + window->setProperty("_q_waylandPopupConstraintAdjustment", constraintAdjustment); } void installPlatformMenuHook() { PlatformMenuEntry::registerCreationHook(&platformMenuHook); }