presentation/QuickTrayController.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "QuickTrayController.h" | ||
| 2 | #include "TrayIconRenderer.h" | ||
| 3 | #include <QApplication> | ||
| 4 | #include <QDBusConnection> | ||
| 5 | #include <QDBusServiceWatcher> | ||
| 6 | #include <QLoggingCategory> | ||
| 7 | #include <QPalette> | ||
| 8 | |||
| 9 | #include "quick/DesktopActivation.h" | ||
| 10 | |||
| 11 | #include "service/DesktopIntegration.h" | ||
| 12 | |||
| 13 | #include <QSystemTrayIcon> | ||
| 14 | #include <QMenu> | ||
| 15 | #include <QTimer> | ||
| 16 | |||
| 17 | #ifdef MAILJD_KDE_INTEGRATION | ||
| 18 | #include <KStatusNotifierItem> | ||
| 19 | #endif | ||
| 20 | |||
| 21 |
4/7✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 7 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 2 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
9 | Q_LOGGING_CATEGORY(lcTray, "mailjd.tray") |
| 22 | |||
| 23 | // ─── backends ─── | ||
| 24 | |||
| 25 | // KDE StatusNotifierItem backend: activate toggles, secondary activate | ||
| 26 | // composes. Compiled only with the KDE integration; the runtime check | ||
| 27 | // mirrors the Widgets MainWindow selection. | ||
| 28 | class QuickTrayController::KdeBackend final { | ||
| 29 | public: | ||
| 30 | 795 | explicit KdeBackend(QuickTrayController *owner) { | |
| 31 | #ifdef MAILJD_KDE_INTEGRATION | ||
| 32 |
1/2✓ Branch 3 → 4 taken 795 times.
✗ Branch 3 → 5 not taken.
|
795 | if (!DesktopIntegration::current().kdeStatusNotifier) return; |
| 33 | ✗ | item = new KStatusNotifierItem(QStringLiteral("mailjd"), owner); | |
| 34 | ✗ | item->setStandardActionsEnabled(false); | |
| 35 | ✗ | item->setContextMenu(owner->contextMenu()); | |
| 36 | ✗ | item->setTitle(QStringLiteral("MailJD")); | |
| 37 | ✗ | item->setIconByName(QStringLiteral("mailjd")); | |
| 38 | ✗ | item->setToolTip(QStringLiteral("mailjd"), QStringLiteral("MailJD"), | |
| 39 | ✗ | QString()); | |
| 40 | ✗ | item->setStatus(KStatusNotifierItem::Active); | |
| 41 | ✗ | QObject::connect(item, &KStatusNotifierItem::activateRequested, owner, | |
| 42 | ✗ | [owner] { owner->toggleWindow(); }); | |
| 43 | ✗ | QObject::connect(item, &KStatusNotifierItem::secondaryActivateRequested, | |
| 44 | ✗ | owner, [owner] { owner->secondaryActivate(); }); | |
| 45 | #endif | ||
| 46 | } | ||
| 47 | 795 | ~KdeBackend() { | |
| 48 | #ifdef MAILJD_KDE_INTEGRATION | ||
| 49 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 795 times.
|
795 | delete item; |
| 50 | #endif | ||
| 51 | 795 | } | |
| 52 | 795 | bool valid() const { | |
| 53 | #ifdef MAILJD_KDE_INTEGRATION | ||
| 54 | 795 | return item != nullptr; | |
| 55 | #else | ||
| 56 | return false; | ||
| 57 | #endif | ||
| 58 | } | ||
| 59 | ✗ | void setOverlay(const QIcon &icon, const QString &tooltip) { | |
| 60 | #ifdef MAILJD_KDE_INTEGRATION | ||
| 61 | ✗ | if (!item) return; | |
| 62 | ✗ | item->setIconByPixmap(icon); | |
| 63 | ✗ | item->setToolTip(QStringLiteral("mailjd"), QStringLiteral("MailJD"), tooltip); | |
| 64 | #else | ||
| 65 | Q_UNUSED(icon); | ||
| 66 | Q_UNUSED(tooltip); | ||
| 67 | #endif | ||
| 68 | } | ||
| 69 | #ifdef MAILJD_KDE_INTEGRATION | ||
| 70 | KStatusNotifierItem *item = nullptr; | ||
| 71 | #endif | ||
| 72 | }; | ||
| 73 | |||
| 74 | // QSystemTrayIcon fallback for non-KDE sessions. | ||
| 75 | class QuickTrayController::FallbackBackend final { | ||
| 76 | public: | ||
| 77 | 795 | explicit FallbackBackend(QuickTrayController *owner) { | |
| 78 |
1/2✓ Branch 3 → 4 taken 795 times.
✗ Branch 3 → 5 not taken.
|
795 | if (!QSystemTrayIcon::isSystemTrayAvailable()) return; |
| 79 | ✗ | icon = new QSystemTrayIcon(QIcon(QStringLiteral(":/icons/mailjd.svg")), owner); | |
| 80 | ✗ | icon->setContextMenu(owner->contextMenu()); | |
| 81 | ✗ | QObject::connect(icon, &QSystemTrayIcon::activated, owner, | |
| 82 | ✗ | [owner](QSystemTrayIcon::ActivationReason reason) { | |
| 83 | ✗ | if (reason == QSystemTrayIcon::MiddleClick) | |
| 84 | ✗ | owner->secondaryActivate(); | |
| 85 | ✗ | else if (reason == QSystemTrayIcon::DoubleClick || | |
| 86 | reason == QSystemTrayIcon::Trigger) | ||
| 87 | ✗ | owner->toggleWindow(); | |
| 88 | ✗ | }); | |
| 89 | ✗ | icon->show(); | |
| 90 | } | ||
| 91 | 795 | ~FallbackBackend() { | |
| 92 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 795 times.
|
795 | delete icon; |
| 93 | 795 | } | |
| 94 | 2368 | bool valid() const { return icon != nullptr; } | |
| 95 | ✗ | void setOverlay(const QIcon &badge, const QString &tooltip) { | |
| 96 | ✗ | if (!icon) return; | |
| 97 | ✗ | icon->setIcon(badge); | |
| 98 | ✗ | icon->setToolTip(tooltip); | |
| 99 | } | ||
| 100 | QSystemTrayIcon *icon = nullptr; | ||
| 101 | }; | ||
| 102 | |||
| 103 | // ─── controller ─── | ||
| 104 | |||
| 105 | 795 | QuickTrayController::QuickTrayController(QQuickWindow *window, QObject *parent) | |
| 106 | 795 | : QObject(parent), m_window(window), | |
| 107 |
4/4✓ Branch 3 → 4 taken 795 times.
✓ Branch 6 → 7 taken 795 times.
✓ Branch 7 → 8 taken 795 times.
✓ Branch 9 → 10 taken 795 times.
|
795 | m_badgeColor(QGuiApplication::palette().color(QPalette::Highlight)) { |
| 108 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | m_overlayTimer.setSingleShot(true); |
| 109 |
1/1✓ Branch 11 → 12 taken 795 times.
|
795 | m_overlayTimer.setInterval(OverlayCoalesceMs); |
| 110 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(&m_overlayTimer, &QTimer::timeout, this, &QuickTrayController::updateOverlay); |
| 111 |
1/1✓ Branch 14 → 15 taken 795 times.
|
795 | rebuildBackend(); |
| 112 | // S106-213: the tray icons need the StatusNotifierWatcher; follow it. | ||
| 113 |
3/4✓ Branch 15 → 16 taken 795 times.
✓ Branch 16 → 17 taken 795 times.
✓ Branch 18 → 19 taken 795 times.
✗ Branch 18 → 36 not taken.
|
795 | if (QDBusConnection::sessionBus().isConnected()) { |
| 114 | auto *watcher = new QDBusServiceWatcher( | ||
| 115 |
1/1✓ Branch 21 → 22 taken 795 times.
|
2385 | QStringLiteral("org.kde.StatusNotifierWatcher"), QDBusConnection::sessionBus(), |
| 116 |
3/6✓ Branch 19 → 20 taken 795 times.
✓ Branch 25 → 26 taken 795 times.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 795 times.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
|
2385 | QDBusServiceWatcher::WatchForRegistration | QDBusServiceWatcher::WatchForUnregistration, this); |
| 117 |
1/1✓ Branch 31 → 32 taken 795 times.
|
799 | connect(watcher, &QDBusServiceWatcher::serviceRegistered, this, [this] { setTrayHostPresent(true); }); |
| 118 |
1/1✓ Branch 33 → 34 taken 795 times.
|
799 | connect(watcher, &QDBusServiceWatcher::serviceUnregistered, this, [this] { setTrayHostPresent(false); }); |
| 119 | } | ||
| 120 | 795 | } | |
| 121 | |||
| 122 | 11 | void QuickTrayController::setTrayHostPresent(bool present) { | |
| 123 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 9 times.
|
11 | if (present == m_hostPresent) return; |
| 124 | 9 | m_hostPresent = present; | |
| 125 |
8/8✓ Branch 4 → 5 taken 9 times.
✓ Branch 8 → 9 taken 9 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 10 → 12 taken 5 times.
✓ Branch 13 → 14 taken 9 times.
✓ Branch 17 → 6 taken 9 times.
✓ Branch 17 → 18 taken 9 times.
|
18 | qCInfo(lcTray) << "Tray host" << (present ? "appeared" : "left the session"); |
| 126 | 9 | emit trayPresenceChanged(); | |
| 127 | } | ||
| 128 | |||
| 129 | 795 | QuickTrayController::~QuickTrayController() { | |
| 130 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 5 taken 795 times.
|
795 | delete m_kde; |
| 131 |
1/2✓ Branch 5 → 6 taken 795 times.
✗ Branch 5 → 8 not taken.
|
795 | delete m_fallback; |
| 132 |
2/2✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 11 taken 793 times.
|
795 | delete m_menu.data(); |
| 133 | 795 | } | |
| 134 | |||
| 135 | 2 | QMenu *QuickTrayController::contextMenu() { | |
| 136 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
|
2 | if (m_menu) return m_menu; |
| 137 | // Model-only QGuiApplication test hosts have no native tray. The product | ||
| 138 | // uses QApplication for the platform menu, while every app view stays Quick. | ||
| 139 |
2/3✓ Branch 6 → 7 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
|
2 | if (!qobject_cast<QApplication *>(QCoreApplication::instance())) return nullptr; |
| 140 |
4/7✓ Branch 9 → 10 taken 2 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 11 → 12 taken 2 times.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 2 times.
✗ Branch 68 → 69 not taken.
✗ Branch 68 → 70 not taken.
|
2 | m_menu = new QMenu; |
| 141 |
1/1✓ Branch 18 → 19 taken 2 times.
|
4 | m_menu->setObjectName(QStringLiteral("mailjdTrayMenu")); |
| 142 | 10 | const auto add = [this](const QString &id, const QString &label, auto action) { | |
| 143 |
2/2auto QuickTrayController::contextMenu()::{lambda(QString const&, QString const&, auto:1)#1}::operator()<void (QuickTrayController::*)()>(QString const&, QString const&, void (QuickTrayController::*)()) const:
✓ Branch 3 → 4 taken 8 times.
auto QuickTrayController::contextMenu()::{lambda(QString const&, QString const&, auto:1)#1}::operator()<QuickTrayController::contextMenu()::{lambda()#1}>(QString const&, QString const&, QuickTrayController::contextMenu()::{lambda()#1}) const:
✓ Branch 3 → 4 taken 2 times.
|
10 | auto *entry = m_menu->addAction(label, this, action); |
| 144 | 10 | entry->setObjectName(id); | |
| 145 | 12 | }; | |
| 146 |
2/2✓ Branch 21 → 22 taken 2 times.
✓ Branch 25 → 26 taken 2 times.
|
4 | add(QStringLiteral("tray.open"), tr("Open MailJD"), &QuickTrayController::showWindow); |
| 147 |
2/2✓ Branch 29 → 30 taken 2 times.
✓ Branch 33 → 34 taken 2 times.
|
5 | add(QStringLiteral("tray.inbox"), tr("Inbox"), [this] { showWindow(); emit inboxRequested(); }); |
| 148 |
1/1✓ Branch 38 → 39 taken 2 times.
|
2 | m_menu->addSeparator(); |
| 149 |
2/2✓ Branch 39 → 40 taken 2 times.
✓ Branch 43 → 44 taken 2 times.
|
4 | add(QStringLiteral("tray.check"), tr("Check Mail"), &QuickTrayController::checkMailRequested); |
| 150 |
2/2✓ Branch 47 → 48 taken 2 times.
✓ Branch 51 → 52 taken 2 times.
|
4 | add(QStringLiteral("tray.compose"), tr("New Message"), &QuickTrayController::secondaryActivate); |
| 151 |
1/1✓ Branch 56 → 57 taken 2 times.
|
2 | m_menu->addSeparator(); |
| 152 |
2/2✓ Branch 57 → 58 taken 2 times.
✓ Branch 61 → 62 taken 2 times.
|
4 | add(QStringLiteral("tray.quit"), tr("Quit"), &QuickTrayController::quitRequested); |
| 153 | 2 | return m_menu; | |
| 154 | } | ||
| 155 | |||
| 156 | 5 | void QuickTrayController::showWindow() { | |
| 157 |
2/3✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 9 not taken.
✓ Branch 6 → 7 taken 5 times.
|
5 | if (m_window) DesktopActivation::activateWindow(m_window.data()); |
| 158 | 5 | } | |
| 159 | |||
| 160 | 4 | void QuickTrayController::setSimulatedTray(bool simulated) { | |
| 161 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 2 times.
|
4 | if (simulated == m_simulated) return; |
| 162 | 2 | m_simulated = simulated; | |
| 163 | 2 | emit trayPresenceChanged(); | |
| 164 | } | ||
| 165 | |||
| 166 | 795 | void QuickTrayController::rebuildBackend() { | |
| 167 |
2/5✓ Branch 3 → 4 taken 795 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 795 times.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
|
795 | m_kde = new KdeBackend(this); |
| 168 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 10 taken 795 times.
|
795 | if (m_kde->valid()) { updateOverlay(); return; } |
| 169 |
1/2✓ Branch 10 → 11 taken 795 times.
✗ Branch 10 → 13 not taken.
|
795 | delete m_kde; |
| 170 | 795 | m_kde = nullptr; | |
| 171 |
2/5✓ Branch 14 → 15 taken 795 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 795 times.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
|
795 | m_fallback = new FallbackBackend(this); |
| 172 | 795 | updateOverlay(); | |
| 173 | } | ||
| 174 | |||
| 175 | 795 | bool QuickTrayController::trayPresent() const { | |
| 176 |
5/8✓ Branch 2 → 3 taken 781 times.
✓ Branch 2 → 11 taken 14 times.
✓ Branch 3 → 4 taken 768 times.
✓ Branch 3 → 10 taken 13 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 768 times.
✗ Branch 6 → 7 not taken.
✗ Branch 6 → 10 not taken.
|
1563 | return m_hostPresent && (m_simulated || (m_kde && m_kde->valid()) || |
| 177 |
2/4✓ Branch 7 → 8 taken 768 times.
✗ Branch 7 → 11 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 768 times.
|
1563 | (m_fallback && m_fallback->valid())); |
| 178 | } | ||
| 179 | |||
| 180 | 868 | void QuickTrayController::setUnreadCount(int count) { | |
| 181 | 868 | count = qMax(0, count); | |
| 182 |
2/2✓ Branch 3 → 4 taken 673 times.
✓ Branch 3 → 5 taken 195 times.
|
868 | if (count == m_unread) return; |
| 183 | 195 | m_unread = count; | |
| 184 | 195 | emit unreadChanged(); | |
| 185 | // Reading a mail changes the INBOX count in the same frame as the click; | ||
| 186 | // the tray badge follows shortly after, once per burst (J/K reading). | ||
| 187 |
2/2✓ Branch 7 → 8 taken 55 times.
✓ Branch 7 → 9 taken 140 times.
|
195 | if (!m_overlayTimer.isActive()) m_overlayTimer.start(); |
| 188 | } | ||
| 189 | |||
| 190 | 783 | void QuickTrayController::setCloseToTray(bool on) { | |
| 191 |
2/2✓ Branch 2 → 3 taken 774 times.
✓ Branch 2 → 4 taken 9 times.
|
783 | if (on == m_closeToTray) return; |
| 192 | 9 | m_closeToTray = on; | |
| 193 | 9 | emit closeToTrayChanged(); | |
| 194 | } | ||
| 195 | |||
| 196 | 1867 | void QuickTrayController::setBadgeColor(const QColor &color) { | |
| 197 |
5/6✓ Branch 3 → 4 taken 1867 times.
✗ Branch 3 → 6 not taken.
✓ Branch 5 → 6 taken 991 times.
✓ Branch 5 → 7 taken 876 times.
✓ Branch 8 → 9 taken 991 times.
✓ Branch 8 → 10 taken 876 times.
|
1867 | if (!color.isValid() || color == m_badgeColor) return; |
| 198 | 876 | m_badgeColor = color; | |
| 199 |
2/2✓ Branch 11 → 12 taken 846 times.
✓ Branch 11 → 13 taken 30 times.
|
876 | if (!m_overlayTimer.isActive()) m_overlayTimer.start(); |
| 200 | } | ||
| 201 | |||
| 202 | 1600 | void QuickTrayController::updateOverlay() { | |
| 203 |
4/4✓ Branch 2 → 3 taken 1600 times.
✓ Branch 5 → 6 taken 230 times.
✓ Branch 5 → 8 taken 1370 times.
✓ Branch 7 → 8 taken 230 times.
|
1600 | if (!QGuiApplication::desktopFileName().isEmpty()) qGuiApp->setBadgeNumber(m_unread); |
| 204 | 1600 | const QString tooltip = m_unread > 0 | |
| 205 |
2/2✓ Branch 8 → 9 taken 174 times.
✓ Branch 8 → 10 taken 1426 times.
|
1600 | ? tr("MailJD — %n unread message(s)", nullptr, m_unread) |
| 206 |
2/2✓ Branch 9 → 11 taken 174 times.
✓ Branch 10 → 11 taken 1426 times.
|
1600 | : tr("MailJD"); |
| 207 |
1/1✓ Branch 11 → 12 taken 1600 times.
|
1600 | m_icon = TrayIconRenderer::badgedIcon(m_unread, m_badgeColor); |
| 208 |
2/6✗ Branch 14 → 15 not taken.
✓ Branch 14 → 18 taken 1600 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 1600 times.
|
1600 | if (m_kde && m_kde->valid()) |
| 209 | ✗ | m_kde->setOverlay(m_icon, tooltip); | |
| 210 |
3/6✓ Branch 21 → 22 taken 1600 times.
✗ Branch 21 → 25 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 1600 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 1600 times.
|
1600 | else if (m_fallback && m_fallback->valid()) |
| 211 | ✗ | m_fallback->setOverlay(m_icon, tooltip); | |
| 212 | 1600 | } | |
| 213 | |||
| 214 | 3 | void QuickTrayController::toggleWindow() { | |
| 215 | 3 | auto *window = m_window.data(); | |
| 216 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 3 times.
|
3 | if (!window) return; |
| 217 |
5/6✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 10 taken 2 times.
✓ Branch 8 → 9 taken 1 time.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 2 times.
|
3 | if (window->isVisible() && window->visibility() != QWindow::Minimized) |
| 218 | 1 | window->hide(); | |
| 219 | else | ||
| 220 |
1/1✓ Branch 14 → 15 taken 2 times.
|
2 | DesktopActivation::activateWindow(window); |
| 221 | } | ||
| 222 | |||
| 223 | 2 | void QuickTrayController::secondaryActivate() { | |
| 224 |
1/2✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 9 not taken.
|
2 | if (m_window) |
| 225 |
1/1✓ Branch 6 → 7 taken 2 times.
|
2 | DesktopActivation::activateWindow(m_window.data()); |
| 226 | 2 | emit composeRequested(); | |
| 227 | 2 | } | |
| 228 | |||
| 229 | 18 | bool QuickTrayController::interceptClose() const { | |
| 230 | // Without a tray, hiding would make the session unreachable: closing | ||
| 231 | // must quit. (Widgets-MainWindow contract.) | ||
| 232 |
4/4✓ Branch 2 → 3 taken 17 times.
✓ Branch 2 → 6 taken 1 time.
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 6 taken 9 times.
|
18 | return m_closeToTray && trayPresent(); |
| 233 | } | ||
| 234 |