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: 91.0% 111 / 0 / 122
Functions: 83.3% 20 / 0 / 24
Branches: 79.8% 95 / 0 / 119

ui/ThemeManager.cpp
Line Branch Exec Source
1 #include "ThemeManager.h"
2 #include <QApplication>
3 #include <QFile>
4 #include <QLoggingCategory>
5 #include <QSettings>
6 #include <QStyleHints>
7 #include <QTextStream>
8 #include <QDebug>
9
10 #include <algorithm>
11
12
3/7
✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 8 not taken.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
1 Q_LOGGING_CATEGORY(lcTheme, "mailjd.theme")
13
14 44 ThemeManager& ThemeManager::instance() {
15
4/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 8 taken 40 times.
✓ Branch 4 → 5 taken 4 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 4 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
44 static ThemeManager inst;
16 44 return inst;
17 }
18
19 4 ThemeManager::ThemeManager(QObject *parent) : QObject(parent) {}
20
21 20 void ThemeManager::applyTheme(Theme theme) {
22 20 m_currentTheme = theme;
23
1/1
✓ Branch 2 → 3 taken 20 times.
20 m_currentVars.clear();
24
25
4/4
✓ Branch 3 → 4 taken 12 times.
✓ Branch 3 → 5 taken 8 times.
✓ Branch 4 → 6 taken 12 times.
✓ Branch 5 → 6 taken 8 times.
20 m_currentVars = theme == Light ? lightPalette() : darkPalette();
26
27
2/2
✓ Branch 8 → 9 taken 20 times.
✓ Branch 9 → 10 taken 20 times.
20 QString qss = loadAndProcessQss(":/themes/main.qss", m_currentVars);
28
1/1
✓ Branch 12 → 13 taken 20 times.
20 qApp->setStyleSheet(qss);
29
30
1/1
✓ Branch 13 → 14 taken 20 times.
20 emit themeChanged(theme);
31 20 }
32
33 // Tokens are documented in docs/DESIGN.md §2 — change them there first.
34 15 QMap<QString, QString> ThemeManager::lightPalette() {
35 return {
36 {"@bg_main", "#ffffff"},
37 {"@bg_sidebar", "#f1f3f5"},
38 {"@bg_selected", "#e7f1ff"},
39 {"@bg_hover", "#e9ecef"},
40 {"@text_primary", "#212529"},
41 {"@text_secondary", "#6c757d"},
42 {"@text_muted", "#adb5bd"},
43 {"@accent", "#007bff"},
44 {"@accent_hover", "#0069d9"},
45 {"@accent_muted", "#cfe2ff"},
46 {"@border_light", "#dee2e6"},
47 {"@border_medium", "#ced4da"},
48 {"@unread_dot", "#007bff"},
49 {"@star_active", "#e8a700"},
50 {"@star_inactive", "#ced4da"},
51 {"@danger", "#d92d2d"},
52 {"@danger_bg", "#fff0ef"},
53 {"@success", "#28a745"},
54 {"@warning", "#e8a700"},
55 {"@taskRing", "#30434d"},
56 {"@taskDoneFill", "#00636a"},
57 {"@priorityHigh", "#855000"},
58 {"@dueToday", "#00636a"},
59 {"@groupHeaderText", "#101923"},
60 {"@link", "#007bff"},
61 // Markdown editor/preview palette (MarkdownHighlighter/Renderer)
62 {"@md_heading", "#1a73e8"},
63 {"@md_heading2", "#1967d2"},
64 {"@md_heading3", "#4285f4"},
65 {"@md_text", "#24292e"},
66 {"@md_quote_fg", "#5f6368"},
67 {"@md_quote_bg", "#f8f9fa"},
68 {"@md_list_marker", "#4285f4"},
69 {"@md_cb_unchecked", "#757575"},
70 {"@md_cb_checked", "#4caf50"},
71 {"@md_hr", "#b0b0b0"},
72 {"@md_code_fg", "#6a9955"},
73 {"@md_code_bg", "#f6f8fa"},
74 {"@md_inline_code", "#d63384"},
75 {"@md_inline_code_bg", "#f0f0f0"},
76 {"@md_strike", "#9e9e9e"},
77 {"@md_table_border", "#e1e4e8"},
78 {"@md_quote_border", "#4285f4"},
79 // Shortcut-help overlay (dark canvas in both themes)
80 {"@overlay_fg", "#d4d4d4"},
81 {"@overlay_key", "#569cd6"},
82 {"@overlay_value", "#ce9178"},
83 {"@overlay_muted", "#808080"}
84
2/4
✓ Branch 50 → 51 taken 690 times.
✓ Branch 50 → 52 taken 15 times.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
705 };
85 15 }
86
87 8 QMap<QString, QString> ThemeManager::darkPalette() {
88 return {
89 {"@bg_main", "#1e1e1e"},
90 {"@bg_sidebar", "#252525"},
91 {"@bg_selected", "#2c3e50"},
92 {"@bg_hover", "#323232"},
93 {"@text_primary", "#e0e0e0"},
94 {"@text_secondary", "#a0a0a0"},
95 {"@text_muted", "#606060"},
96 {"@accent", "#3daee9"},
97 {"@accent_hover", "#2d9ed9"},
98 {"@accent_muted", "#1d4f72"},
99 {"@border_light", "#323232"},
100 {"@border_medium", "#454545"},
101 {"@unread_dot", "#3daee9"},
102 {"@star_active", "#ffc107"},
103 {"@star_inactive", "#454545"},
104 {"@danger", "#e05555"},
105 {"@danger_bg", "#3a2424"},
106 {"@success", "#4caf50"},
107 {"@warning", "#ffc107"},
108 {"@taskRing", "#ccd9e1"},
109 {"@taskDoneFill", "#79d6d2"},
110 {"@priorityHigh", "#efc175"},
111 {"@dueToday", "#79d6d2"},
112 {"@groupHeaderText", "#f6f8fa"},
113 {"@link", "#3daee9"},
114 // Markdown editor/preview palette (MarkdownHighlighter/Renderer)
115 {"@md_heading", "#6cb2ff"},
116 {"@md_heading2", "#5a9fe8"},
117 {"@md_heading3", "#82b6f2"},
118 {"@md_text", "#e0e0e0"},
119 {"@md_quote_fg", "#a0a0a0"},
120 {"@md_quote_bg", "#2a2a2a"},
121 {"@md_list_marker", "#82b6f2"},
122 {"@md_cb_unchecked", "#909090"},
123 {"@md_cb_checked", "#66bb6a"},
124 {"@md_hr", "#555555"},
125 {"@md_code_fg", "#6a9955"},
126 {"@md_code_bg", "#2a2a2a"},
127 {"@md_inline_code", "#e083b6"},
128 {"@md_inline_code_bg", "#333333"},
129 {"@md_strike", "#777777"},
130 {"@md_table_border", "#454545"},
131 {"@md_quote_border", "#5a9fe8"},
132 // Shortcut-help overlay (dark canvas in both themes)
133 {"@overlay_fg", "#d4d4d4"},
134 {"@overlay_key", "#569cd6"},
135 {"@overlay_value", "#ce9178"},
136 {"@overlay_muted", "#808080"}
137
2/4
✓ Branch 50 → 51 taken 368 times.
✓ Branch 50 → 52 taken 8 times.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
376 };
138 8 }
139
140 196 QString ThemeManager::color(const QString &key) const {
141
2/2
✓ Branch 3 → 4 taken 24 times.
✓ Branch 3 → 15 taken 172 times.
196 if (m_currentVars.isEmpty()) {
142 // No theme applied yet (early startup, unit tests) — Light values.
143
4/7
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 10 taken 21 times.
✓ Branch 6 → 7 taken 3 times.
✗ Branch 6 → 10 not taken.
✓ Branch 7 → 8 taken 3 times.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 23 not taken.
24 static const QMap<QString, QString> fallback = lightPalette();
144
1/1
✓ Branch 11 → 12 taken 24 times.
24 return fallback.value(key);
145 }
146
1/1
✓ Branch 16 → 17 taken 172 times.
344 return m_currentVars.value(key);
147 }
148
149 // ── 67.B3: shared programmatic palettes ───────────────────
150
151 8 QColor ThemeManager::confidenceColor(double confidence) {
152
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 6 taken 6 times.
8 if (confidence >= 0.97)
153 2 return QColor("#2d7d46"); // green — very confident
154
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 10 taken 4 times.
6 if (confidence >= 0.93)
155 2 return QColor("#4a90d9"); // blue — confident
156
2/2
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 14 taken 2 times.
4 if (confidence >= 0.88)
157 2 return QColor("#cc8a1e"); // orange — medium
158
2/2
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 18 taken 1 time.
2 if (confidence > 0.0)
159 1 return QColor("#c44f4f"); // red — unsure
160 1 return QColor("#888888"); // neutral fallback
161 }
162
163 2 QString ThemeManager::birthdayLayerColor() {
164 2 return QStringLiteral("#a32c6c");
165 }
166
167 1245 QStringList ThemeManager::calendarPalette() {
168 // Sprint 93: 12 colors; the first eight keep their order so stored
169 // defaults of existing calendars stay meaningful.
170 1245 return {QStringLiteral("#4285F4"), QStringLiteral("#0B8043"),
171 1245 QStringLiteral("#D50000"), QStringLiteral("#F4511E"),
172 1245 QStringLiteral("#8E24AA"), QStringLiteral("#039BE5"),
173 1245 QStringLiteral("#E67C73"), QStringLiteral("#616161"),
174 1245 QStringLiteral("#F6BF26"), QStringLiteral("#33B679"),
175
2/4
✓ Branch 40 → 41 taken 14940 times.
✓ Branch 40 → 42 taken 1245 times.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
17430 QStringLiteral("#3F51B5"), QStringLiteral("#7986CB")};
176 16185 }
177
178 4 QColor ThemeManager::mutedConfidenceColor(double confidence) {
179
2/2
✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 6 taken 3 times.
4 if (confidence > 0.8)
180 1 return QColor("#5b8abf"); // muted blue
181
2/2
✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 10 taken 1 time.
3 if (confidence > 0.6)
182 2 return QColor("#9a8c7a"); // warm grey
183 1 return QColor("#aaaaaa"); // light grey
184 }
185
186 2947 QList<QColor> ThemeManager::labelPalette() {
187 return {QColor(0xCC, 0x33, 0x33), // red
188 QColor(0xE8, 0x7A, 0x00), // orange
189 QColor(0x33, 0x99, 0x33), // green
190 QColor(0x33, 0x66, 0xCC), // blue
191 QColor(0x99, 0x33, 0xCC), // purple
192 QColor(0xCC, 0x99, 0x00), // gold
193 2947 QColor(0x88, 0x88, 0x88)}; // gray
194 }
195
196 ✗ QColor ThemeManager::avatarColor(const QString &key) {
197 // Deterministic hue from the (lowercased) address; fixed saturation/
198 // lightness keep white initials readable in both themes.
199 ✗ const uint hash = qHash(key.trimmed().toLower());
200 ✗ return QColor::fromHsl(int(hash % 360u), 140, 110);
201 }
202
203 ✗ QColor ThemeManager::overlayScrimColor() {
204 ✗ return QColor(0, 0, 0, 180);
205 }
206
207 ✗ QColor ThemeManager::overlayCardColor() {
208 ✗ return QColor(30, 30, 30, 240);
209 }
210
211 // ── 67.B2: mode handling ──────────────────────────────────
212
213 7 ThemeManager::Theme ThemeManager::resolveTheme(Mode mode) const {
214
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 5 times.
7 if (mode == ModeLight)
215 2 return Light;
216
2/2
✓ Branch 4 → 5 taken 3 times.
✓ Branch 4 → 6 taken 2 times.
5 if (mode == ModeDark)
217 3 return Dark;
218 #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
219
1/2
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 15 not taken.
2 if (qApp) {
220 2 return qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark
221
1/2
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2 times.
2 ? Dark
222 2 : Light;
223 }
224 #endif
225 // Qt < 6.5 has no colorSchemeChanged — System degrades to Light.
226 ✗ return Light;
227 }
228
229 2 void ThemeManager::setMode(Mode mode) {
230 2 applyModeInternal(mode, /*persist=*/true);
231 2 }
232
233 2 void ThemeManager::initFromSettings() {
234
1/1
✓ Branch 2 → 3 taken 2 times.
2 QSettings settings;
235 const QString stored =
236
1/1
✓ Branch 11 → 12 taken 2 times.
6 settings.value(QStringLiteral("appearance/theme"),
237
1/1
✓ Branch 12 → 13 taken 2 times.
6 QStringLiteral("system")).toString();
238
2/2
✓ Branch 19 → 20 taken 2 times.
✓ Branch 20 → 21 taken 2 times.
2 applyModeInternal(modeFromString(stored), /*persist=*/false);
239 2 }
240
241 4 void ThemeManager::applyModeInternal(Mode mode, bool persist) {
242 4 m_mode = mode;
243
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 17 taken 2 times.
4 if (persist) {
244
1/1
✓ Branch 3 → 4 taken 2 times.
2 QSettings settings;
245
1/1
✓ Branch 10 → 11 taken 2 times.
4 settings.setValue(QStringLiteral("appearance/theme"),
246
1/1
✓ Branch 4 → 5 taken 2 times.
4 modeToString(mode));
247 2 }
248
249 #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
250 // Watch the OS scheme once; the handler only acts in System mode.
251
5/6
✓ Branch 18 → 19 taken 4 times.
✗ Branch 18 → 21 not taken.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 21 taken 3 times.
✓ Branch 22 → 23 taken 1 time.
✓ Branch 22 → 28 taken 3 times.
4 if (qApp && !m_systemWatchActive) {
252 1 m_systemWatchActive = true;
253
2/2
✓ Branch 24 → 25 taken 1 time.
✓ Branch 25 → 26 taken 1 time.
1 connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this,
254 2 [this](Qt::ColorScheme) {
255 ✗ if (m_mode == ModeSystem)
256 ✗ applyTheme(resolveTheme(ModeSystem));
257 ✗ });
258 }
259 #endif
260
261 4 applyTheme(resolveTheme(mode));
262 4 }
263
264 6 ThemeManager::Mode ThemeManager::modeFromString(const QString &value) {
265
2/2
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 4 times.
6 if (value.compare(QStringLiteral("dark"), Qt::CaseInsensitive) == 0)
266 2 return ModeDark;
267
2/2
✓ Branch 16 → 17 taken 1 time.
✓ Branch 16 → 18 taken 3 times.
4 if (value.compare(QStringLiteral("light"), Qt::CaseInsensitive) == 0)
268 1 return ModeLight;
269 3 return ModeSystem;
270 }
271
272 5 QString ThemeManager::modeToString(Mode mode) {
273
3/4
✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 8 taken 2 times.
✓ Branch 2 → 13 taken 1 time.
✗ Branch 2 → 14 not taken.
5 switch (mode) {
274 4 case ModeLight: return QStringLiteral("light");
275 4 case ModeDark: return QStringLiteral("dark");
276 1 case ModeSystem: break;
277 }
278 1 return QStringLiteral("system");
279 }
280
281 24 QString ThemeManager::loadAndProcessQss(const QString &path, const QMap<QString, QString> &vars) {
282
1/1
✓ Branch 2 → 3 taken 24 times.
24 QFile file(path);
283
3/3
✓ Branch 4 → 5 taken 24 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 5 → 18 taken 23 times.
24 if (!file.open(QFile::ReadOnly | QFile::Text)) {
284
6/6
✓ Branch 6 → 7 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 16 → 8 taken 1 time.
✓ Branch 16 → 17 taken 1 time.
2 qCWarning(lcTheme) << "Could not open QSS file" << path;
285 1 return QString();
286 }
287
288
1/1
✓ Branch 18 → 19 taken 23 times.
23 QTextStream stream(&file);
289
1/1
✓ Branch 19 → 20 taken 23 times.
23 QString content = stream.readAll();
290
1/1
✓ Branch 20 → 21 taken 23 times.
23 file.close();
291
292 // Variable interpolation — longest keys first, so prefix-sharing
293 // tokens ("@accent" vs "@accent_hover") never corrupt each other.
294
1/1
✓ Branch 21 → 22 taken 23 times.
23 QStringList keys = vars.keys();
295
3/3
✓ Branch 22 → 23 taken 23 times.
✓ Branch 23 → 24 taken 23 times.
✓ Branch 24 → 25 taken 23 times.
23 std::sort(keys.begin(), keys.end(),
296 5103 [](const QString &a, const QString &b) {
297 5103 return a.size() > b.size();
298 });
299
4/4
✓ Branch 25 → 26 taken 23 times.
✓ Branch 26 → 27 taken 23 times.
✓ Branch 36 → 28 taken 925 times.
✓ Branch 36 → 37 taken 23 times.
948 for (const QString &key : keys) {
300
2/2
✓ Branch 30 → 31 taken 925 times.
✓ Branch 31 → 32 taken 925 times.
925 content.replace(key, vars.value(key));
301 }
302
303 23 return content;
304 24 }
305