Skip to content

Commit 998ffb0

Browse files
Improve tray menu positioning and popup
Revise tray context-menu positioning and activation handling. calculateMenuPosition() now prefers tray icon geometry, then a non-zero cursor position, and finally a screen-geometry heuristic to infer the panel edge (fixes Wayland/XWayland cursor (0,0) issues). The tray activation handler now responds to both Trigger and Context reasons, captures the anchor point, and defers menu->popup() via QTimer::singleShot(0) to avoid platform grab conflicts on Xorg; it also avoids reopening an already visible menu.
1 parent d43d397 commit 998ffb0

File tree

1 file changed

+68
-20
lines changed

1 file changed

+68
-20
lines changed

src/tray_linux.cpp

Lines changed: 68 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -98,22 +98,62 @@ namespace {
9898
/**
9999
* @brief Calculate the best position to show the context menu.
100100
*
101-
* Uses the tray icon geometry when available (reliable on X11/XEmbed and
102-
* some SNI desktops). Falls back to the current cursor position on systems
103-
* where the icon geometry cannot be determined. Qt's QMenu::popup() will
104-
* adjust the final position to keep the menu fully on-screen.
101+
* Priority:
102+
* 1. Tray icon geometry (reliable on X11/XEmbed, sometimes on SNI).
103+
* 2. Cursor position when non-zero (reliable on Xorg; on Wayland and some
104+
* XWayland configurations QCursor::pos() returns (0,0) even though the
105+
* click occurred elsewhere, so zero is treated as unavailable).
106+
* 3. Screen-geometry heuristic: the panel edge is inferred from the
107+
* difference between the screen's full and available geometries.
108+
*
109+
* Qt's QMenu::popup() will adjust the final position to keep the menu fully
110+
* on-screen, including flipping it above the anchor point when needed.
105111
*
106112
* @return The point at which to show the context menu.
107113
*/
108114
QPoint calculateMenuPosition() {
109115
if (g_tray_icon != nullptr) {
110-
const QRect iconGeometry = g_tray_icon->geometry();
111-
if (iconGeometry.isValid()) {
112-
// Qt's popup() will flip the menu above the icon if it would go off-screen.
113-
return iconGeometry.bottomLeft();
116+
const QRect iconGeo = g_tray_icon->geometry();
117+
if (iconGeo.isValid()) {
118+
return iconGeo.bottomLeft();
119+
}
120+
}
121+
122+
// QCursor::pos() returns (0, 0) on Wayland and some XWayland setups because
123+
// the compositor does not expose the global cursor position to applications.
124+
// Use it only when it reports a non-zero position.
125+
const QPoint cursor = QCursor::pos();
126+
if (!cursor.isNull()) {
127+
return cursor;
128+
}
129+
130+
// Wayland fallback: infer the panel edge from available vs full screen geometry
131+
// and anchor the menu to the corner of that edge. Qt's popup() will ensure the
132+
// menu is kept fully on-screen (flipping above/below the anchor as needed).
133+
QScreen *screen = QGuiApplication::primaryScreen();
134+
if (screen != nullptr) {
135+
const QRect full = screen->geometry();
136+
const QRect avail = screen->availableGeometry();
137+
if (avail.top() > full.top()) {
138+
// Panel at top (e.g. GNOME default): anchor below the panel at the right edge.
139+
return QPoint(avail.right(), avail.top());
140+
}
141+
if (avail.bottom() < full.bottom()) {
142+
// Panel at bottom (e.g. KDE Plasma default): anchor at the bottom-right edge;
143+
// popup() will flip the menu upward automatically.
144+
return QPoint(avail.right(), avail.bottom());
145+
}
146+
if (avail.left() > full.left()) {
147+
// Panel on the left: anchor at the panel's right edge.
148+
return QPoint(avail.left(), avail.bottom());
149+
}
150+
if (avail.right() < full.right()) {
151+
// Panel on the right: anchor at the panel's left edge.
152+
return QPoint(avail.right(), avail.bottom());
114153
}
115154
}
116-
return QCursor::pos();
155+
156+
return cursor;
117157
}
118158

119159
void close_notification() {
@@ -218,19 +258,27 @@ extern "C" {
218258
return -1;
219259
}
220260

221-
// Show the context menu on the default trigger (clicked).
222-
// QSystemTrayIcon::setContextMenu only handles right-click on X11/XEmbed;
223-
// SNI-based desktops may not show the menu at all without this explicit connection.
224-
// The menu is positioned using the tray icon geometry rather than the cursor position,
225-
// which is unreliable on Wayland.
261+
// Show the context menu on left-click (Trigger) and right-click (Context).
262+
// On X11/XEmbed, setContextMenu handles right-click natively. On SNI-based
263+
// desktops (GNOME Wayland with AppIndicators) the shell may emit activated(Context)
264+
// instead, so we handle it here explicitly as a fallback.
265+
// The menu position is captured immediately (cursor pos changes after the event),
266+
// then the popup is deferred to the next event-loop iteration via QTimer::singleShot.
267+
// Deferring ensures the platform grab from the tray click (X11 button grab) is
268+
// released before the menu creates its own grab, which fixes menu dismissal on Xorg.
269+
// The isVisible() guard prevents a duplicate popup when Qt already showed the menu
270+
// natively (e.g. via setContextMenu on right-click).
226271
QObject::connect(g_tray_icon, &QSystemTrayIcon::activated, [](QSystemTrayIcon::ActivationReason reason) {
227-
if (reason == QSystemTrayIcon::Trigger) {
228-
if (g_tray_icon != nullptr) {
229-
QMenu *menu = g_tray_icon->contextMenu();
230-
if (menu != nullptr) {
231-
menu->popup(calculateMenuPosition());
272+
if (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::Context) {
273+
const QPoint pos = calculateMenuPosition();
274+
QTimer::singleShot(0, g_tray_icon, [pos]() {
275+
if (g_tray_icon != nullptr) {
276+
QMenu *menu = g_tray_icon->contextMenu();
277+
if (menu != nullptr && !menu->isVisible()) {
278+
menu->popup(pos);
279+
}
232280
}
233-
}
281+
});
234282
}
235283
});
236284

0 commit comments

Comments
 (0)