MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 76.7% 112 / 0 / 146
Functions: 84.4% 27 / 0 / 32
Branches: 61.8% 118 / 0 / 191

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/2
auto 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