|
11 | 11 |
|
12 | 12 | // Qt includes |
13 | 13 | #include <QApplication> |
| 14 | +#include <QCoreApplication> |
14 | 15 | #include <QCursor> |
15 | 16 | #include <QDBusConnection> |
16 | 17 | #include <QDBusInterface> |
|
21 | 22 | #include <QMenu> |
22 | 23 | #include <QPixmap> |
23 | 24 | #include <QScreen> |
| 25 | +#include <QStyle> |
24 | 26 | #include <QSystemTrayIcon> |
25 | 27 | #include <QTimer> |
26 | 28 | #include <QUrl> |
@@ -69,6 +71,39 @@ namespace { |
69 | 71 | uint g_notification_id = 0; // NOSONAR(cpp:S5421) - tracks last D-Bus notification ID for proper cleanup |
70 | 72 | void (*g_log_cb)(int, const char *) = nullptr; // NOSONAR(cpp:S5421) - mutable state, not const |
71 | 73 |
|
| 74 | + bool is_wayland_session() { |
| 75 | + const QString platform = QGuiApplication::platformName().toLower(); |
| 76 | + if (platform.contains(QStringLiteral("wayland"))) { |
| 77 | + return true; |
| 78 | + } |
| 79 | + return !qgetenv("WAYLAND_DISPLAY").isEmpty(); |
| 80 | + } |
| 81 | + |
| 82 | + QPoint screen_anchor_point(const QScreen *screen) { |
| 83 | + if (screen == nullptr) { |
| 84 | + return QPoint(); |
| 85 | + } |
| 86 | + |
| 87 | + const QRect full = screen->geometry(); |
| 88 | + const QRect avail = screen->availableGeometry(); |
| 89 | + |
| 90 | + if (avail.top() > full.top()) { |
| 91 | + return QPoint(avail.right(), avail.top()); |
| 92 | + } |
| 93 | + if (avail.bottom() < full.bottom()) { |
| 94 | + return QPoint(avail.right(), avail.bottom()); |
| 95 | + } |
| 96 | + if (avail.left() > full.left()) { |
| 97 | + return QPoint(avail.left(), avail.bottom()); |
| 98 | + } |
| 99 | + if (avail.right() < full.right()) { |
| 100 | + return QPoint(avail.right(), avail.bottom()); |
| 101 | + } |
| 102 | + |
| 103 | + // Some compositors report no reserved panel area; top-right is a safer fallback than (0, 0). |
| 104 | + return avail.topRight(); |
| 105 | + } |
| 106 | + |
72 | 107 | /** |
73 | 108 | * @brief Qt message handler that forwards to the registered log callback. |
74 | 109 | * @param type The Qt message type. |
@@ -114,48 +149,124 @@ namespace { |
114 | 149 | * |
115 | 150 | * @return The point at which to show the context menu. |
116 | 151 | */ |
117 | | - QPoint calculateMenuPosition() { |
| 152 | + QPoint calculateMenuPosition(const QPoint &preferred_pos = QPoint()) { |
118 | 153 | if (g_tray_icon != nullptr) { |
119 | 154 | const QRect iconGeo = g_tray_icon->geometry(); |
120 | 155 | if (iconGeo.isValid()) { |
121 | 156 | return iconGeo.bottomLeft(); |
122 | 157 | } |
123 | 158 | } |
124 | 159 |
|
| 160 | + if (!preferred_pos.isNull() && !is_wayland_session()) { |
| 161 | + return preferred_pos; |
| 162 | + } |
| 163 | + |
125 | 164 | // When running under a Wayland compositor, XWayland cursor coordinates are stale |
126 | 165 | // for events originating from Wayland-native surfaces (e.g., the GNOME top bar). |
127 | 166 | // Detect a Wayland session regardless of the Qt platform plugin in use. |
128 | | - const bool wayland_session = !qgetenv("WAYLAND_DISPLAY").isEmpty(); |
| 167 | + const bool wayland_session = is_wayland_session(); |
129 | 168 | if (!wayland_session) { |
130 | 169 | // Pure Xorg: QCursor::pos() is accurate. |
131 | 170 | return QCursor::pos(); |
132 | 171 | } |
133 | 172 |
|
134 | | - // Wayland session fallback: infer the panel edge from available vs full screen |
135 | | - // geometry and anchor the menu to that edge. popup() keeps the menu on-screen. |
136 | | - QScreen *screen = QGuiApplication::primaryScreen(); |
137 | | - if (screen != nullptr) { |
138 | | - const QRect full = screen->geometry(); |
139 | | - const QRect avail = screen->availableGeometry(); |
140 | | - if (avail.top() > full.top()) { |
141 | | - // Panel at top (e.g., GNOME default): anchor below the panel at the right edge. |
142 | | - return QPoint(avail.right(), avail.top()); |
| 173 | + const QPoint cursor_pos = QCursor::pos(); |
| 174 | + if (!cursor_pos.isNull()) { |
| 175 | + QScreen *cursor_screen = QGuiApplication::screenAt(cursor_pos); |
| 176 | + if (cursor_screen != nullptr) { |
| 177 | + return cursor_pos; |
143 | 178 | } |
144 | | - if (avail.bottom() < full.bottom()) { |
145 | | - // Panel at the bottom (e.g., KDE Plasma default): popup() flips upward automatically. |
146 | | - return QPoint(avail.right(), avail.bottom()); |
| 179 | + } |
| 180 | + |
| 181 | + // Wayland session fallback: infer panel anchor from the relevant screen. |
| 182 | + QScreen *screen = QGuiApplication::screenAt(cursor_pos); |
| 183 | + if (screen == nullptr) { |
| 184 | + screen = QGuiApplication::primaryScreen(); |
| 185 | + } |
| 186 | + const QPoint anchored = screen_anchor_point(screen); |
| 187 | + if (!anchored.isNull()) { |
| 188 | + return anchored; |
| 189 | + } |
| 190 | + |
| 191 | + return cursor_pos; |
| 192 | + } |
| 193 | + |
| 194 | + QIcon icon_from_source(const QString &icon_source) { |
| 195 | + if (icon_source.isEmpty()) { |
| 196 | + return QIcon(); |
| 197 | + } |
| 198 | + |
| 199 | + const QFileInfo icon_fi(icon_source); |
| 200 | + if (icon_fi.exists()) { |
| 201 | + const QString file_path = icon_fi.absoluteFilePath(); |
| 202 | + const QIcon file_icon(file_path); |
| 203 | + if (!file_icon.isNull()) { |
| 204 | + return file_icon; |
147 | 205 | } |
148 | | - if (avail.left() > full.left()) { |
149 | | - // Panel on the left. |
150 | | - return QPoint(avail.left(), avail.bottom()); |
| 206 | + |
| 207 | + const QPixmap pixmap(file_path); |
| 208 | + if (!pixmap.isNull()) { |
| 209 | + QIcon icon; |
| 210 | + icon.addPixmap(pixmap); |
| 211 | + return icon; |
151 | 212 | } |
152 | | - if (avail.right() < full.right()) { |
153 | | - // Panel on the right. |
154 | | - return QPoint(avail.right(), avail.bottom()); |
| 213 | + } |
| 214 | + |
| 215 | + const QIcon themed = QIcon::fromTheme(icon_source); |
| 216 | + if (!themed.isNull()) { |
| 217 | + return themed; |
| 218 | + } |
| 219 | + |
| 220 | + return QIcon(); |
| 221 | + } |
| 222 | + |
| 223 | + QIcon resolve_tray_icon(const struct tray *tray_data) { |
| 224 | + if (tray_data == nullptr) { |
| 225 | + return QIcon(); |
| 226 | + } |
| 227 | + |
| 228 | + if (tray_data->icon != nullptr) { |
| 229 | + const QIcon icon = icon_from_source(QString::fromUtf8(tray_data->icon)); |
| 230 | + if (!icon.isNull()) { |
| 231 | + return icon; |
155 | 232 | } |
156 | 233 | } |
157 | 234 |
|
158 | | - return QCursor::pos(); |
| 235 | + if (tray_data->iconPathCount > 0 && tray_data->iconPathCount < 64) { |
| 236 | + for (int i = 0; i < tray_data->iconPathCount; i++) { |
| 237 | + if (tray_data->allIconPaths[i] == nullptr) { |
| 238 | + continue; |
| 239 | + } |
| 240 | + const QIcon icon = icon_from_source(QString::fromUtf8(tray_data->allIconPaths[i])); |
| 241 | + if (!icon.isNull()) { |
| 242 | + return icon; |
| 243 | + } |
| 244 | + } |
| 245 | + } |
| 246 | + |
| 247 | + return QIcon(); |
| 248 | + } |
| 249 | + |
| 250 | + void popup_menu_for_activation(const QPoint &preferred_pos, int retries_left = 3) { |
| 251 | + if (g_tray_icon == nullptr) { |
| 252 | + return; |
| 253 | + } |
| 254 | + |
| 255 | + QMenu *menu = g_tray_icon->contextMenu(); |
| 256 | + if (menu == nullptr || menu->isVisible()) { |
| 257 | + return; |
| 258 | + } |
| 259 | + |
| 260 | + menu->activateWindow(); |
| 261 | + menu->setWindowFlag(Qt::Popup, true); |
| 262 | + menu->popup(calculateMenuPosition(preferred_pos)); |
| 263 | + menu->setFocus(Qt::PopupFocusReason); |
| 264 | + |
| 265 | + if (!menu->isVisible() && retries_left > 0) { |
| 266 | + QTimer::singleShot(30, g_tray_icon, [preferred_pos, retries_left]() { |
| 267 | + popup_menu_for_activation(preferred_pos, retries_left - 1); |
| 268 | + }); |
| 269 | + } |
159 | 270 | } |
160 | 271 |
|
161 | 272 | void close_notification() { |
@@ -260,28 +371,43 @@ extern "C" { |
260 | 371 | return -1; |
261 | 372 | } |
262 | 373 |
|
| 374 | + if (QCoreApplication::applicationName().isEmpty()) { |
| 375 | + QCoreApplication::setApplicationName(QStringLiteral("tray")); |
| 376 | + } |
| 377 | + if (QCoreApplication::applicationDisplayName().isEmpty()) { |
| 378 | + const QString display_name = |
| 379 | + (tray != nullptr && tray->tooltip != nullptr) ? QString::fromUtf8(tray->tooltip) : QStringLiteral("tray"); |
| 380 | + QCoreApplication::setApplicationDisplayName(display_name); |
| 381 | + } |
| 382 | + if (QGuiApplication::desktopFileName().isEmpty()) { |
| 383 | + QString desktop_name = QCoreApplication::applicationName(); |
| 384 | + if (!desktop_name.endsWith(QStringLiteral(".desktop"))) { |
| 385 | + desktop_name += QStringLiteral(".desktop"); |
| 386 | + } |
| 387 | + QGuiApplication::setDesktopFileName(desktop_name); |
| 388 | + } |
| 389 | + |
263 | 390 | // Show the context menu on left-click (Trigger). |
264 | 391 | // Qt handles right-click natively via setContextMenu on both X11/XEmbed and |
265 | 392 | // SNI (Wayland/AppIndicators), so we do not handle Context here. |
266 | | - // The menu position is captured immediately before deferring to the next |
267 | | - // event-loop iteration via QTimer::singleShot(0). Deferring allows any |
| 393 | + // The menu position is captured immediately before deferring by a short timer. |
| 394 | + // Deferring allows any |
268 | 395 | // platform pointer grab from the tray click to be released before the menu |
269 | 396 | // establishes its own grab. |
270 | 397 | // activateWindow() gives the menu window X11 focus so that the subsequent |
271 | 398 | // XGrabPointer inside popup() succeeds, enabling click-outside dismissal on Xorg. |
272 | 399 | QObject::connect(g_tray_icon, &QSystemTrayIcon::activated, [](QSystemTrayIcon::ActivationReason reason) { |
273 | | - if (reason == QSystemTrayIcon::Trigger) { |
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 | | - menu->activateWindow(); |
280 | | - menu->popup(pos); |
281 | | - } |
282 | | - } |
283 | | - }); |
| 400 | + const bool left_click_activation = |
| 401 | + (reason == QSystemTrayIcon::Trigger || reason == QSystemTrayIcon::Context); |
| 402 | + |
| 403 | + if (!left_click_activation) { |
| 404 | + return; |
284 | 405 | } |
| 406 | + |
| 407 | + const QPoint click_pos = QCursor::pos(); |
| 408 | + QTimer::singleShot(30, g_tray_icon, [click_pos]() { |
| 409 | + popup_menu_for_activation(click_pos); |
| 410 | + }); |
285 | 411 | }); |
286 | 412 |
|
287 | 413 | // Defer D-Bus ActionInvoked handler setup to the first event-loop iteration. |
@@ -336,26 +462,18 @@ extern "C" { |
336 | 462 | return; |
337 | 463 | } |
338 | 464 |
|
339 | | - const QString icon_str = QString::fromUtf8(tray->icon); |
340 | | - QIcon icon; |
341 | | - const QFileInfo icon_fi(icon_str); |
342 | | - if (icon_fi.exists()) { |
343 | | - // Explicitly load via QPixmap so that the icon engine has pixmap data and |
344 | | - // availableSizes() is populated immediately. QIcon(filename) lazy-loads the |
345 | | - // pixmap, which leaves availableSizes() empty; Qt6's SNI tray backend then |
346 | | - // sees no sizes and sends no icon data, causing the tray icon to be blank. |
347 | | - const QPixmap pixmap(icon_fi.absoluteFilePath()); |
348 | | - if (!pixmap.isNull()) { |
349 | | - icon.addPixmap(pixmap); |
350 | | - } |
351 | | - } else { |
352 | | - icon = QIcon::fromTheme(icon_str); |
| 465 | + QIcon tray_icon = resolve_tray_icon(tray); |
| 466 | + if (tray_icon.isNull() && !g_tray_icon->icon().isNull()) { |
| 467 | + tray_icon = g_tray_icon->icon(); |
| 468 | + } |
| 469 | + if (tray_icon.isNull()) { |
| 470 | + tray_icon = QApplication::style()->standardIcon(QStyle::SP_ComputerIcon); |
353 | 471 | } |
354 | 472 | // Only update the icon when the resolved icon is valid. Setting a null icon |
355 | 473 | // clears the tray icon and triggers "No Icon set" warnings (Qt6 is stricter |
356 | 474 | // about QIcon::fromTheme when the name is not found in the active theme). |
357 | | - if (!icon.isNull()) { |
358 | | - g_tray_icon->setIcon(icon); |
| 475 | + if (!tray_icon.isNull()) { |
| 476 | + g_tray_icon->setIcon(tray_icon); |
359 | 477 | } |
360 | 478 |
|
361 | 479 | if (tray->tooltip != nullptr) { |
@@ -447,7 +565,7 @@ extern "C" { |
447 | 565 | if (g_tray_icon != nullptr) { |
448 | 566 | QMenu *menu = g_tray_icon->contextMenu(); |
449 | 567 | if (menu != nullptr) { |
450 | | - menu->popup(calculateMenuPosition()); |
| 568 | + popup_menu_for_activation(QPoint()); |
451 | 569 | QApplication::processEvents(); |
452 | 570 | } |
453 | 571 | } |
|
0 commit comments