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: 87.2% 130 / 0 / 149
Functions: 96.0% 24 / 0 / 25
Branches: 66.5% 135 / 0 / 203

presentation/KeyOverlayController.cpp
Line Branch Exec Source
1 #include "KeyOverlayController.h"
2
3 #include "KeyHint.h"
4 #include "KeyTipAssigner.h"
5 #include "QuickCommandRouter.h"
6 #include "app/CommandRegistry.h"
7
8 #include <QCoreApplication>
9 #include <QFontMetricsF>
10 #include <QKeyEvent>
11 #include <QLoggingCategory>
12 #include <QSettings>
13 #include <algorithm>
14
15
4/7
✓ Branch 2 → 3 taken 24 times.
✓ Branch 2 → 8 taken 10596 times.
✓ Branch 4 → 5 taken 24 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 24 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
10620 Q_LOGGING_CATEGORY(lcKeyOverlay, "mailjd.keys.overlay", QtWarningMsg)
16
17 namespace {
18 constexpr int kActionSettleMs = 60; // D-21: let the activated element update first
19
20 7719 QChar letterOf(const QKeyEvent *event) {
21 7719 const int key = event->key();
22
4/4
✓ Branch 3 → 4 taken 7155 times.
✓ Branch 3 → 8 taken 564 times.
✓ Branch 4 → 5 taken 1846 times.
✓ Branch 4 → 8 taken 5309 times.
7719 if (key >= Qt::Key_A && key <= Qt::Key_Z) return QChar('A' + (key - Qt::Key_A));
23
4/4
✓ Branch 8 → 9 taken 5709 times.
✓ Branch 8 → 13 taken 164 times.
✓ Branch 9 → 10 taken 95 times.
✓ Branch 9 → 13 taken 5614 times.
5873 if (key >= Qt::Key_0 && key <= Qt::Key_9) return QChar('0' + (key - Qt::Key_0));
24 5778 return {};
25 }
26 } // namespace
27
28
1/1
✓ Branch 5 → 6 taken 874 times.
874 KeyOverlayController::KeyOverlayController(QuickCommandRouter *router) : QObject(router), m_router(router) {
29 // Settings › General › Keyboard (keys/holdAlt, keys/holdDelay, keys/explanations).
30
1/1
✓ Branch 14 → 15 taken 874 times.
874 const QSettings settings;
31 874 AltHoldDetector::Settings detector;
32
2/2
✓ Branch 20 → 21 taken 874 times.
✓ Branch 21 → 22 taken 874 times.
1748 detector.holdEnabled = settings.value(QStringLiteral("keys/holdAlt"), true).toBool();
33
3/3
✓ Branch 31 → 32 taken 874 times.
✓ Branch 32 → 33 taken 874 times.
✓ Branch 33 → 34 taken 874 times.
1748 detector.delayMs = std::clamp(settings.value(QStringLiteral("keys/holdDelay"), 300).toInt(), 0, 1000);
34 874 m_detector.setSettings(detector);
35
3/3
✓ Branch 47 → 48 taken 874 times.
✓ Branch 48 → 49 taken 874 times.
✓ Branch 49 → 50 taken 874 times.
1748 setExplanations(settings.value(QStringLiteral("keys/explanations"), QStringLiteral("after")).toString());
36 874 m_clock.start();
37
1/1
✓ Branch 58 → 59 taken 874 times.
874 m_tick.setSingleShot(true);
38
3/3
✓ Branch 3 → 4 taken 13 times.
✓ Branch 4 → 5 taken 13 times.
✓ Branch 59 → 60 taken 874 times.
887 connect(&m_tick, &QTimer::timeout, this, [this] { apply(m_detector.tick(m_clock.elapsed())); });
39 874 }
40
41 1141 void KeyOverlayController::setFontPixelSize(qreal size) {
42
5/6
✓ Branch 3 → 4 taken 148 times.
✓ Branch 3 → 5 taken 993 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 148 times.
✓ Branch 7 → 8 taken 993 times.
✓ Branch 7 → 9 taken 148 times.
1141 if (qFuzzyCompare(m_fontPixelSize, size) || size <= 0) return;
43 148 m_fontPixelSize = size;
44 148 emit metricsChanged();
45 }
46
47 1141 void KeyOverlayController::setTextScale(qreal scale) {
48
5/6
✓ Branch 3 → 4 taken 148 times.
✓ Branch 3 → 5 taken 993 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 148 times.
✓ Branch 7 → 8 taken 993 times.
✓ Branch 7 → 9 taken 148 times.
1141 if (qFuzzyCompare(m_textScale, scale) || scale <= 0) return;
49 148 m_textScale = scale;
50 148 emit metricsChanged();
51 }
52
53 ✗ void KeyOverlayController::setHoldEnabled(bool enabled) {
54 ✗ auto settings = m_detector.settings();
55 ✗ if (settings.holdEnabled == enabled) return;
56 ✗ settings.holdEnabled = enabled;
57 ✗ m_detector.setSettings(settings);
58 ✗ QSettings().setValue(QStringLiteral("keys/holdAlt"), enabled);
59 ✗ emit settingsChanged();
60 }
61
62 10 void KeyOverlayController::setHoldDelay(int delay) {
63 10 auto settings = m_detector.settings();
64
1/1
✓ Branch 3 → 4 taken 10 times.
10 delay = std::clamp(delay, 0, 1000);
65
1/2
✓ Branch 4 → 5 taken 10 times.
✗ Branch 4 → 6 not taken.
10 if (settings.delayMs == delay) return;
66 ✗ settings.delayMs = delay;
67 ✗ m_detector.setSettings(settings);
68 ✗ QSettings().setValue(QStringLiteral("keys/holdDelay"), delay);
69 ✗ emit settingsChanged();
70 }
71
72 21 QString KeyOverlayController::explanations() const {
73
1/4
✗ Branch 3 → 4 not taken.
✗ Branch 3 → 9 not taken.
✓ Branch 3 → 14 taken 21 times.
✗ Branch 3 → 15 not taken.
21 switch (m_detector.settings().explain) {
74 ✗ case AltHoldDetector::ExplainMode::AtOnce: return QStringLiteral("atOnce");
75 ✗ case AltHoldDetector::ExplainMode::Never: return QStringLiteral("never");
76 21 case AltHoldDetector::ExplainMode::AfterHold: break;
77 }
78 21 return QStringLiteral("after");
79 }
80
81 874 void KeyOverlayController::setExplanations(const QString &mode) {
82 874 auto settings = m_detector.settings();
83
1/2
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 874 times.
1748 const auto value = mode == QLatin1String("atOnce") ? AltHoldDetector::ExplainMode::AtOnce
84
1/2
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 874 times.
874 : mode == QLatin1String("never") ? AltHoldDetector::ExplainMode::Never
85 874 : AltHoldDetector::ExplainMode::AfterHold;
86
1/2
✓ Branch 13 → 14 taken 874 times.
✗ Branch 13 → 15 not taken.
874 if (settings.explain == value) return;
87 ✗ settings.explain = value;
88 ✗ m_detector.setSettings(settings);
89 ✗ QSettings().setValue(QStringLiteral("keys/explanations"), explanations());
90 ✗ emit settingsChanged();
91 }
92
93 7719 bool KeyOverlayController::keyPress(QKeyEvent *event) {
94 7719 const QChar letter = letterOf(event);
95
5/6
✓ Branch 3 → 4 taken 10 times.
✓ Branch 3 → 9 taken 7709 times.
✓ Branch 5 → 6 taken 9 times.
✓ Branch 5 → 9 taken 1 time.
✓ Branch 7 → 8 taken 9 times.
✗ Branch 7 → 9 not taken.
7719 const bool tip = m_stage > 0 && !letter.isNull() && m_tips.contains(letter);
96
2/2
✓ Branch 12 → 13 taken 7719 times.
✓ Branch 14 → 15 taken 7719 times.
7719 const auto result = m_detector.keyPress(event->key(), event->modifiers(), event->isAutoRepeat(), m_clock.elapsed(), letter, tip);
97
1/1
✓ Branch 15 → 16 taken 7719 times.
7719 apply(result);
98 7719 return result.consume;
99 }
100
101 7241 void KeyOverlayController::keyRelease(QKeyEvent *event) {
102
8/8
✓ Branch 2 → 3 taken 7241 times.
✓ Branch 6 → 7 taken 12 times.
✓ Branch 7 → 8 taken 12 times.
✓ Branch 9 → 10 taken 12 times.
✓ Branch 11 → 12 taken 12 times.
✓ Branch 12 → 13 taken 12 times.
✓ Branch 18 → 4 taken 12 times.
✓ Branch 18 → 19 taken 7241 times.
7253 qCDebug(lcKeyOverlay) << "release" << event->key() << event->modifiers();
103
3/3
✓ Branch 21 → 22 taken 7241 times.
✓ Branch 23 → 24 taken 7241 times.
✓ Branch 24 → 25 taken 7241 times.
7241 apply(m_detector.keyRelease(event->key(), event->modifiers(), event->isAutoRepeat(), m_clock.elapsed()));
104 7241 }
105
106 489 void KeyOverlayController::pointerPress() {
107
2/5
✓ Branch 2 → 3 taken 489 times.
✗ Branch 6 → 7 not taken.
✗ Branch 7 → 8 not taken.
✗ Branch 11 → 4 not taken.
✓ Branch 11 → 12 taken 489 times.
489 qCDebug(lcKeyOverlay) << "pointer press";
108
2/2
✓ Branch 12 → 13 taken 489 times.
✓ Branch 13 → 14 taken 489 times.
489 apply(m_detector.pointerPress());
109 489 }
110 31 void KeyOverlayController::windowDeactivated() {
111
2/5
✓ Branch 2 → 3 taken 31 times.
✗ Branch 6 → 7 not taken.
✗ Branch 7 → 8 not taken.
✗ Branch 11 → 4 not taken.
✓ Branch 11 → 12 taken 31 times.
31 qCDebug(lcKeyOverlay) << "window deactivated";
112
2/2
✓ Branch 12 → 13 taken 31 times.
✓ Branch 13 → 14 taken 31 times.
31 apply(m_detector.windowDeactivated());
113 31 }
114 1021 void KeyOverlayController::cancel() {
115
5/5
✓ Branch 2 → 3 taken 1021 times.
✓ Branch 6 → 7 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 11 → 4 taken 2 times.
✓ Branch 11 → 12 taken 1021 times.
1023 qCDebug(lcKeyOverlay) << "cancel";
116
2/2
✓ Branch 12 → 13 taken 1021 times.
✓ Branch 13 → 14 taken 1021 times.
1021 apply(m_detector.cancel());
117 1021 }
118
2/2
✓ Branch 2 → 3 taken 62 times.
✓ Branch 3 → 4 taken 62 times.
62 void KeyOverlayController::toggle() { apply(m_detector.toggle()); }
119
2/2
✓ Branch 2 → 3 taken 61 times.
✓ Branch 3 → 4 taken 61 times.
61 void KeyOverlayController::hide() { apply(m_detector.cancel()); }
120
121 16639 void KeyOverlayController::scheduleTick() {
122 16639 const qint64 deadline = m_detector.nextDeadline();
123
2/2
✓ Branch 3 → 4 taken 16604 times.
✓ Branch 3 → 6 taken 35 times.
16639 if (deadline < 0) { m_tick.stop(); return; }
124
1/1
✓ Branch 8 → 9 taken 35 times.
35 m_tick.start(int(std::max<qint64>(0, deadline - m_clock.elapsed())));
125 }
126
127 16639 void KeyOverlayController::apply(const AltHoldDetector::Result &result) {
128 using A = AltHoldDetector::Action;
129
2/2
✓ Branch 2 → 3 taken 169 times.
✓ Branch 2 → 18 taken 16470 times.
16639 if (result.action != A::None)
130
2/8
✓ Branch 3 → 4 taken 169 times.
✗ Branch 7 → 8 not taken.
✗ Branch 8 → 9 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 10 → 11 not taken.
✗ Branch 12 → 13 not taken.
✗ Branch 16 → 5 not taken.
✓ Branch 16 → 17 taken 169 times.
169 qCDebug(lcKeyOverlay) << "detector action" << int(result.action) << "state" << int(m_detector.state());
131 16639 const bool holding = m_detector.state() == AltHoldDetector::State::Holding;
132
2/2
✓ Branch 19 → 20 taken 26 times.
✓ Branch 19 → 21 taken 16613 times.
16639 if (m_holding != holding) {
133 26 m_holding = holding;
134 26 emit holdingChanged();
135 }
136
5/7
✓ Branch 21 → 22 taken 16487 times.
✓ Branch 21 → 23 taken 9 times.
✓ Branch 21 → 24 taken 63 times.
✗ Branch 21 → 25 not taken.
✓ Branch 21 → 30 taken 71 times.
✓ Branch 21 → 31 taken 9 times.
✗ Branch 21 → 64 not taken.
16639 switch (result.action) {
137 16487 case A::None:
138 case A::HoldStarted:
139 case A::HoldCancelled:
140 16487 break;
141 9 case A::Show: compute(1); break;
142 63 case A::ShowExplained:
143 63 case A::Explain: compute(2); break;
144 ✗ case A::Recompute: compute(m_detector.explained() ? 2 : 1); break;
145 71 case A::Hide: clear(); break;
146 9 case A::Activate: {
147 9 const Entry entry = m_tips.value(result.letter);
148 9 m_pulse = entry.id;
149
1/1
✓ Branch 33 → 34 taken 9 times.
9 emit pulseChanged();
150
2/9
✓ Branch 34 → 35 taken 9 times.
✗ Branch 38 → 39 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 40 → 41 not taken.
✗ Branch 41 → 42 not taken.
✗ Branch 42 → 43 not taken.
✗ Branch 43 → 44 not taken.
✗ Branch 47 → 36 not taken.
✓ Branch 47 → 48 taken 9 times.
9 qCInfo(lcKeyOverlay) << "tip" << result.letter << "activates" << entry.id << entry.command;
151
2/8
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 53 taken 9 times.
✗ Branch 50 → 51 not taken.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 9 times.
✗ Branch 55 → 60 not taken.
9 if (!entry.command.isEmpty() && m_router->isAvailable(entry.command)) m_router->invoke(entry.command);
152
2/3
✓ Branch 57 → 58 taken 9 times.
✗ Branch 57 → 60 not taken.
✓ Branch 59 → 60 taken 9 times.
9 else if (entry.hint) entry.hint->activate();
153
1/1
✓ Branch 60 → 61 taken 9 times.
9 QTimer::singleShot(kActionSettleMs, this, &KeyOverlayController::finishAction);
154 9 break;
155 9 }
156 }
157 16639 scheduleTick();
158 16639 }
159
160
2/2
✓ Branch 2 → 3 taken 2 times.
✓ Branch 3 → 4 taken 2 times.
2 void KeyOverlayController::finishAction() { apply(m_detector.actionDone()); }
161
162 71 void KeyOverlayController::clear() {
163 71 m_tips.clear();
164
2/6
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 71 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 71 times.
71 if (m_stage == 0 && m_badges.isEmpty()) return;
165 71 m_stage = 0;
166 71 m_badges.clear();
167 71 m_callouts.clear();
168 71 m_spotlights.clear();
169 71 m_legend.clear();
170 71 m_paneLegends.clear();
171 71 m_announcement.clear();
172 71 m_pulse.clear();
173 71 ++m_revision;
174 71 emit changed();
175 }
176
177 1583 QSizeF KeyOverlayController::badgeSize(const QString &text, bool alt) const {
178
1/1
✓ Branch 2 → 3 taken 1583 times.
1583 QFont font;
179
1/1
✓ Branch 3 → 4 taken 1583 times.
1583 font.setPixelSize(int(m_fontPixelSize));
180
1/1
✓ Branch 4 → 5 taken 1583 times.
1583 const QFontMetricsF metrics(font);
181
7/11
✓ Branch 3 → 4 taken 410 times.
✓ Branch 5 → 6 taken 410 times.
✓ Branch 5 → 13 taken 1173 times.
✓ Branch 6 → 7 taken 410 times.
✓ Branch 7 → 8 taken 410 times.
✓ Branch 20 → 21 taken 410 times.
✓ Branch 20 → 22 taken 1173 times.
✗ Branch 40 → 41 not taken.
✗ Branch 40 → 42 not taken.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
3576 const qreal mark = alt ? QFontMetricsF([&] { QFont small = font; small.setPixelSize(int(m_fontPixelSize * 0.6) + 1); return small; }())
182
7/11
✓ Branch 11 → 12 taken 410 times.
✓ Branch 14 → 15 taken 410 times.
✓ Branch 14 → 16 taken 1173 times.
✓ Branch 16 → 17 taken 410 times.
✓ Branch 16 → 18 taken 1173 times.
✓ Branch 18 → 19 taken 410 times.
✓ Branch 18 → 20 taken 1173 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
✗ Branch 36 → 37 not taken.
✗ Branch 36 → 38 not taken.
1583 .horizontalAdvance(QStringLiteral("Alt ")) : 0;
183
1/1
✓ Branch 22 → 23 taken 1583 times.
1583 const qreal height = std::max(20.0 * m_textScale, metrics.height() + 6);
184
1/1
✓ Branch 24 → 25 taken 1583 times.
3166 return {std::max(height, metrics.horizontalAdvance(text) + mark + 12), height};
185 1583 }
186
187 171 QSizeF KeyOverlayController::calloutSize(const QString &text) const {
188
1/1
✓ Branch 2 → 3 taken 171 times.
171 QFont font;
189
1/1
✓ Branch 3 → 4 taken 171 times.
171 font.setPixelSize(int(m_fontPixelSize * 13.0 / 12.0));
190
1/1
✓ Branch 4 → 5 taken 171 times.
171 const QFontMetricsF metrics(font);
191 171 const qreal maxWidth = 260 * m_textScale;
192
1/1
✓ Branch 5 → 6 taken 171 times.
171 const qreal width = std::min(maxWidth, metrics.horizontalAdvance(text) + 20);
193
1/1
✓ Branch 8 → 9 taken 171 times.
171 const QRectF bounds = metrics.boundingRect(QRectF(0, 0, width - 20, 10000), Qt::TextWordWrap, text);
194 342 return {width, bounds.height() + 16};
195 171 }
196
197 2 QVariantMap KeyOverlayController::tipLetters() const {
198 2 QVariantMap result;
199
2/2
✓ Branch 19 → 3 taken 6 times.
✓ Branch 19 → 20 taken 2 times.
8 for (auto it = m_tips.cbegin(); it != m_tips.cend(); ++it)
200
3/4
✓ Branch 5 → 6 taken 6 times.
✗ Branch 5 → 8 not taken.
✓ Branch 12 → 13 taken 6 times.
✓ Branch 13 → 14 taken 6 times.
6 result.insert(QString(it.key()), it.value().command.isEmpty() ? it.value().id : it.value().command);
201 2 return result;
202 ✗ }
203