@@ -98,21 +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. On a pure Xorg session, QCursor::pos() is accurate.
104+ * 3. On a Wayland session (detected via WAYLAND_DISPLAY), QCursor::pos() goes
105+ * through XWayland and reflects the last X11 cursor position, which is NOT
106+ * updated when the pointer interacts with Wayland-native surfaces such as the
107+ * GNOME Shell top bar. A screen-geometry heuristic is used instead: the panel
108+ * edge is inferred from the difference between the screen's full and available
109+ * geometries.
110+ *
111+ * Qt's QMenu::popup() will adjust the final position to keep the menu fully
112+ * on-screen, including flipping it above the anchor point when needed.
105113 *
106114 * @return The point at which to show the context menu.
107115 */
108116 QPoint calculateMenuPosition () {
109117 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 ();
118+ const QRect iconGeo = g_tray_icon->geometry ();
119+ if (iconGeo.isValid ()) {
120+ return iconGeo.bottomLeft ();
121+ }
122+ }
123+
124+ // When running under a Wayland compositor, XWayland cursor coordinates are stale
125+ // for events originating from Wayland-native surfaces (e.g., the GNOME top bar).
126+ // Detect a Wayland session regardless of the Qt platform plugin in use.
127+ const bool wayland_session = !qgetenv (" WAYLAND_DISPLAY" ).isEmpty ();
128+ if (!wayland_session) {
129+ // Pure Xorg: QCursor::pos() is accurate.
130+ return QCursor::pos ();
131+ }
132+
133+ // Wayland session fallback: infer the panel edge from available vs full screen
134+ // geometry and anchor the menu to that edge. popup() keeps the menu on-screen.
135+ QScreen *screen = QGuiApplication::primaryScreen ();
136+ if (screen != nullptr ) {
137+ const QRect full = screen->geometry ();
138+ const QRect avail = screen->availableGeometry ();
139+ if (avail.top () > full.top ()) {
140+ // Panel at top (e.g., GNOME default): anchor below the panel at the right edge.
141+ return QPoint (avail.right (), avail.top ());
142+ }
143+ if (avail.bottom () < full.bottom ()) {
144+ // Panel at the bottom (e.g., KDE Plasma default): popup() flips upward automatically.
145+ return QPoint (avail.right (), avail.bottom ());
146+ }
147+ if (avail.left () > full.left ()) {
148+ // Panel on the left.
149+ return QPoint (avail.left (), avail.bottom ());
150+ }
151+ if (avail.right () < full.right ()) {
152+ // Panel on the right.
153+ return QPoint (avail.right (), avail.bottom ());
114154 }
115155 }
156+
116157 return QCursor::pos ();
117158 }
118159
@@ -218,19 +259,28 @@ extern "C" {
218259 return -1 ;
219260 }
220261
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.
262+ // Show the context menu on left-click (Trigger).
263+ // Qt handles right-click natively via setContextMenu on both X11/XEmbed and
264+ // SNI (Wayland/AppIndicators), so we do not handle Context here.
265+ // The menu position is captured immediately before deferring to the next
266+ // event-loop iteration via QTimer::singleShot(0). Deferring allows any
267+ // platform pointer grab from the tray click to be released before the menu
268+ // establishes its own grab.
269+ // QApplication::setActiveWindow gives the menu window X11 focus so that the
270+ // subsequent XGrabPointer inside popup() succeeds, enabling click-outside
271+ // dismissal on Xorg.
226272 QObject::connect (g_tray_icon, &QSystemTrayIcon::activated, [](QSystemTrayIcon::ActivationReason reason) {
227273 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 ());
274+ const QPoint pos = calculateMenuPosition ();
275+ QTimer::singleShot (0 , g_tray_icon, [pos]() {
276+ if (g_tray_icon != nullptr ) {
277+ QMenu *menu = g_tray_icon->contextMenu ();
278+ if (menu != nullptr && !menu->isVisible ()) {
279+ QApplication::setActiveWindow (menu);
280+ menu->popup (pos);
281+ }
232282 }
233- }
283+ });
234284 }
235285 });
236286
@@ -287,9 +337,13 @@ extern "C" {
287337 }
288338
289339 const QString icon_str = QString::fromUtf8 (tray->icon );
290- g_tray_icon->setIcon (
291- QFileInfo (icon_str).exists () ? QIcon (icon_str) : QIcon::fromTheme (icon_str)
292- );
340+ const QIcon icon = QFileInfo (icon_str).exists () ? QIcon (icon_str) : QIcon::fromTheme (icon_str);
341+ // Only update the icon when the resolved icon is valid. Setting a null icon
342+ // clears the tray icon and triggers "No Icon set" warnings (Qt6 is stricter
343+ // about QIcon::fromTheme when the name is not found in the active theme).
344+ if (!icon.isNull ()) {
345+ g_tray_icon->setIcon (icon);
346+ }
293347
294348 if (tray->tooltip != nullptr ) {
295349 g_tray_icon->setToolTip (QString::fromUtf8 (tray->tooltip ));
0 commit comments