core/menu: add QsMenuAnchor for more control of platform menus

This commit is contained in:
outfoxxed 2024-07-25 20:44:26 -07:00
parent 54350277be
commit 6b9b1fcb53
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
10 changed files with 245 additions and 21 deletions

View file

@ -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}")

View file

@ -26,5 +26,6 @@ headers = [
"retainable.hpp",
"popupanchor.hpp",
"types.hpp",
"qsmenuanchor.hpp",
]
-----

View file

@ -16,6 +16,7 @@
#include <qwindow.h>
#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;

View file

@ -13,6 +13,7 @@
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#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<void(PlatformMenuQMenu*)> hook);

View file

@ -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;

105
src/core/qsmenuanchor.cpp Normal file
View file

@ -0,0 +1,105 @@
#include "qsmenuanchor.hpp"
#include <qlogging.h>
#include <qobject.h>
#include <qtmetamacros.h>
#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

86
src/core/qsmenuanchor.hpp Normal file
View file

@ -0,0 +1,86 @@
#pragma once
#include <qqmlintegration.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#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

View file

@ -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();
}

View file

@ -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);

View file

@ -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); }