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