Skip to content

Commit c06fa40

Browse files
Improve tray menu positioning and icon handling
Detect Wayland sessions and improve context-menu placement by prioritizing tray icon geometry, then using QCursor on Xorg, and falling back to a screen-geometry heuristic on Wayland (inferring panel edge from full vs available geometry). Defer showing the menu with QTimer::singleShot(0) and call QApplication::setActiveWindow before popup() so pointer grabs and XGrabPointer behavior work reliably; avoid showing the menu if it's already visible. Also avoid clearing the tray icon by only setting it when the resolved QIcon is valid, preventing spurious "No Icon set" warnings.
1 parent d43d397 commit c06fa40

1 file changed

Lines changed: 75 additions & 21 deletions

File tree

src/tray_linux.cpp

Lines changed: 75 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)