|
3 | 3 | * @brief Definitions for Qt tray menu implemenation |
4 | 4 | */ |
5 | 5 | // standard includes |
| 6 | +#include <chrono> |
6 | 7 | #include <filesystem> |
| 8 | +#include <thread> |
7 | 9 |
|
8 | 10 | // qt includes |
9 | 11 | #include <QApplication> |
10 | 12 | #include <QCursor> |
11 | 13 | #include <QDebug> |
12 | 14 | #include <QMouseEvent> |
| 15 | +#include <QScreen> |
13 | 16 | #include <QStyle> |
14 | 17 |
|
15 | 18 | // local includes |
|
19 | 22 | #include "WindowsAppearance.h" |
20 | 23 | #endif |
21 | 24 |
|
| 25 | +namespace { |
| 26 | + constexpr int DEFAULT_PANEL_THICKNESS = 24; |
| 27 | + constexpr int CURSOR_POSITION_POLL_INTERVAL_MS = 10; |
| 28 | + constexpr int CURSOR_POSITION_TIMEOUT_MS = 500; |
| 29 | + constexpr int CURSOR_POSITION_TOLERANCE = 2; |
| 30 | + |
| 31 | + bool positionsAreClose(const QPoint &first, const QPoint &second) { |
| 32 | + return (first - second).manhattanLength() <= CURSOR_POSITION_TOLERANCE; |
| 33 | + } |
| 34 | + |
| 35 | + bool waitForCursorPosition(const QPoint &targetPosition, const QRect &targetGeometry = {}) { |
| 36 | + const auto deadline = std::chrono::steady_clock::now() + std::chrono::milliseconds(CURSOR_POSITION_TIMEOUT_MS); |
| 37 | + do { |
| 38 | + if (const QPoint currentPosition = QCursor::pos(); targetGeometry.isValid() ? targetGeometry.contains(currentPosition) : positionsAreClose(currentPosition, targetPosition)) { |
| 39 | + return true; |
| 40 | + } |
| 41 | + std::this_thread::sleep_for(std::chrono::milliseconds(CURSOR_POSITION_POLL_INTERVAL_MS)); |
| 42 | + } while (std::chrono::steady_clock::now() < deadline); |
| 43 | + |
| 44 | + const QPoint currentPosition = QCursor::pos(); |
| 45 | + return targetGeometry.isValid() ? targetGeometry.contains(currentPosition) : positionsAreClose(currentPosition, targetPosition); |
| 46 | + } |
| 47 | + |
| 48 | + bool fallbackTrayIconPosition(QPoint *position) { |
| 49 | + const QScreen *screen = QGuiApplication::primaryScreen(); |
| 50 | + if (screen == nullptr) { |
| 51 | + return false; |
| 52 | + } |
| 53 | + |
| 54 | + const QRect screenGeometry = screen->geometry(); |
| 55 | + const QRect availableGeometry = screen->availableGeometry(); |
| 56 | + const int topInset = availableGeometry.top() - screenGeometry.top(); |
| 57 | + const int bottomInset = screenGeometry.bottom() - availableGeometry.bottom(); |
| 58 | + const int rightInset = screenGeometry.right() - availableGeometry.right(); |
| 59 | + const int leftInset = availableGeometry.left() - screenGeometry.left(); |
| 60 | + |
| 61 | + if (topInset > 0) { |
| 62 | + *position = QPoint(screenGeometry.right() - (topInset / 2), screenGeometry.top() + (topInset / 2)); |
| 63 | + } else if (bottomInset > 0) { |
| 64 | + *position = QPoint(screenGeometry.right() - (bottomInset / 2), screenGeometry.bottom() - (bottomInset / 2)); |
| 65 | + } else if (rightInset > 0) { |
| 66 | + *position = QPoint(screenGeometry.right() - (rightInset / 2), screenGeometry.bottom() - (rightInset / 2)); |
| 67 | + } else if (leftInset > 0) { |
| 68 | + *position = QPoint(screenGeometry.left() + (leftInset / 2), screenGeometry.bottom() - (leftInset / 2)); |
| 69 | + } else { |
| 70 | +#if defined(_WIN32) |
| 71 | + *position = QPoint(screenGeometry.right() - (DEFAULT_PANEL_THICKNESS / 2), screenGeometry.bottom() - (DEFAULT_PANEL_THICKNESS / 2)); |
| 72 | +#else |
| 73 | + *position = QPoint(screenGeometry.right() - (DEFAULT_PANEL_THICKNESS / 2), screenGeometry.top() + (DEFAULT_PANEL_THICKNESS / 2)); |
| 74 | +#endif |
| 75 | + } |
| 76 | + return true; |
| 77 | + } |
| 78 | +} // namespace |
| 79 | + |
22 | 80 | QtTrayMenu::QtTrayMenu(QObject *parent, const bool debug): |
23 | 81 | QtTrayMenu(-1, nullptr, parent, debug) { |
24 | 82 | }; |
@@ -342,3 +400,45 @@ void QtTrayMenu::clickMessage() const { |
342 | 400 | void QtTrayMenu::clearMessageCallback() const { |
343 | 401 | notificationCallback = nullptr; |
344 | 402 | } |
| 403 | + |
| 404 | +bool QtTrayMenu::positionMouseOverIcon() { |
| 405 | + if (!trayIcon) { |
| 406 | + return false; |
| 407 | + } |
| 408 | + |
| 409 | + const QRect iconGeometry = trayIcon->geometry(); |
| 410 | + QPoint targetPosition; |
| 411 | + if (iconGeometry.isValid()) { |
| 412 | + targetPosition = iconGeometry.center(); |
| 413 | + } else if (!fallbackTrayIconPosition(&targetPosition)) { |
| 414 | + qWarning("QtTrayMenu: tray icon geometry and screen-edge fallback are unavailable"); |
| 415 | + return false; |
| 416 | + } else { |
| 417 | + qWarning("QtTrayMenu: tray icon geometry is unavailable; using the system panel edge"); |
| 418 | + } |
| 419 | + |
| 420 | + if (!mousePositionSaved) { |
| 421 | + savedMousePosition = QCursor::pos(); |
| 422 | + mousePositionSaved = true; |
| 423 | + } |
| 424 | + QCursor::setPos(targetPosition); |
| 425 | + const bool positioned = waitForCursorPosition(targetPosition, iconGeometry); |
| 426 | + if (!positioned) { |
| 427 | + qWarning("QtTrayMenu: could not position the mouse over the tray icon"); |
| 428 | + } |
| 429 | + return positioned; |
| 430 | +} |
| 431 | + |
| 432 | +bool QtTrayMenu::restoreMousePosition() { |
| 433 | + if (!mousePositionSaved) { |
| 434 | + return false; |
| 435 | + } |
| 436 | + |
| 437 | + QCursor::setPos(savedMousePosition); |
| 438 | + const bool restored = waitForCursorPosition(savedMousePosition); |
| 439 | + mousePositionSaved = false; |
| 440 | + if (!restored) { |
| 441 | + qWarning("QtTrayMenu: could not restore the saved mouse position"); |
| 442 | + } |
| 443 | + return restored; |
| 444 | +} |
0 commit comments