forked from quickshell/quickshell
core/menu: add QsMenuAnchor for more control of platform menus
This commit is contained in:
parent
54350277be
commit
6b9b1fcb53
10 changed files with 245 additions and 21 deletions
|
@ -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}")
|
||||
|
|
|
@ -26,5 +26,6 @@ headers = [
|
|||
"retainable.hpp",
|
||||
"popupanchor.hpp",
|
||||
"types.hpp",
|
||||
"qsmenuanchor.hpp",
|
||||
]
|
||||
-----
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
105
src/core/qsmenuanchor.cpp
Normal 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
86
src/core/qsmenuanchor.hpp
Normal 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
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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); }
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue