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