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: 96.8% 245 / 0 / 253
Functions: 97.8% 45 / 0 / 46
Branches: 81.4% 319 / 0 / 392

presentation/QuickCommandRouter.cpp
Line Branch Exec Source
1 #include "QuickCommandRouter.h"
2 #include <QScopedValueRollback>
3
4 #include <algorithm>
5 #include "WorkspaceState.h"
6 #include "app/NamedCommandCatalog.h"
7 #include "GraphLinesItem.h"
8 #include "TextMarksItem.h"
9 #include "KeyHint.h"
10 #include <QCoreApplication>
11 #include <QKeyEvent>
12 #include <QLoggingCategory>
13 #include <QQuickItem>
14 #include <QQuickWindow>
15
16
4/7
✓ Branch 2 → 3 taken 18 times.
✓ Branch 2 → 8 taken 473 times.
✓ Branch 4 → 5 taken 18 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 18 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
491 Q_LOGGING_CATEGORY(lcKeys, "mailjd.keys", QtWarningMsg)
17
18 namespace {
19 constexpr int kPrefixWindowMs = 1000; // D-14: one window for every "g …"
20 } // namespace
21
22
4/4
✓ Branch 9 → 10 taken 874 times.
✓ Branch 10 → 11 taken 874 times.
✓ Branch 13 → 14 taken 874 times.
✓ Branch 15 → 16 taken 874 times.
874 QuickCommandRouter::QuickCommandRouter(QObject *parent) : QAbstractListModel(parent) {
23 // The QML module of the keys overlay must exist before any QML loads.
24
1/1
✓ Branch 17 → 18 taken 874 times.
874 KeyHint::registerTypes();
25
1/1
✓ Branch 18 → 19 taken 874 times.
874 GraphLinesItem::registerTypes(); // Sprint 105 S105.4.2: MailJD.Graph
26
1/1
✓ Branch 19 → 20 taken 874 times.
874 TextMarksItem::registerTypes(); // F108-20: MailJD.Text (wavy marks)
27
3/6
✓ Branch 20 → 21 taken 874 times.
✓ Branch 21 → 22 taken 874 times.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 874 times.
✗ Branch 30 → 31 not taken.
✗ Branch 30 → 32 not taken.
874 m_overlay = new KeyOverlayController(this);
28
1/1
✓ Branch 24 → 25 taken 874 times.
874 m_prefixTimeout.setSingleShot(true);
29
1/1
✓ Branch 25 → 26 taken 874 times.
874 m_prefixTimeout.setInterval(kPrefixWindowMs);
30
1/1
✓ Branch 26 → 27 taken 874 times.
874 connect(&m_prefixTimeout, &QTimer::timeout, this, &QuickCommandRouter::cancelSequence);
31
1/1
✓ Branch 28 → 29 taken 874 times.
874 rebuild();
32 874 }
33
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 22303 times.
22304 int QuickCommandRouter::rowCount(const QModelIndex &parent) const { return parent.isValid() ? 0 : m_commands.size(); }
34 3971 QString QuickCommandRouter::defaultShortcutFor(const QString &id) const {
35 3971 const auto *command = CommandRegistry::find(id);
36
1/2
✓ Branch 5 → 6 taken 3969 times.
✗ Branch 5 → 8 not taken.
3969 return command && !command->defaultKeys.isEmpty()
37
2/2
✓ Branch 3 → 4 taken 3969 times.
✓ Branch 3 → 8 taken 2 times.
7940 ? command->defaultKeys.first().toString(QKeySequence::PortableText) : QString{};
38 }
39 887 QHash<int, QByteArray> QuickCommandRouter::roleNames() const {
40 ✗ return {{Qt::UserRole, "commandId"}, {Qt::UserRole + 1, "description"},
41 ✗ {Qt::UserRole + 2, "keys"}, {Qt::UserRole + 3, "available"}, {Qt::UserRole + 4, "category"},
42
2/4
✓ Branch 11 → 12 taken 6209 times.
✓ Branch 11 → 13 taken 887 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
7096 {Qt::UserRole + 5, "changedKey"}, {Qt::UserRole + 6, "explanation"}};
43 887 }
44 9223 QVariant QuickCommandRouter::data(const QModelIndex &index, int role) const {
45
6/8
✓ Branch 3 → 4 taken 9218 times.
✓ Branch 3 → 9 taken 5 times.
✓ Branch 5 → 6 taken 9218 times.
✗ Branch 5 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 9218 times.
✓ Branch 11 → 12 taken 5 times.
✓ Branch 11 → 13 taken 9218 times.
9223 if (!index.isValid() || index.row() < 0 || index.row() >= m_commands.size()) return {};
46 9218 const auto &command = *m_commands[index.row()];
47
8/8
✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 17 taken 1201 times.
✓ Branch 15 → 21 taken 1201 times.
✓ Branch 15 → 25 taken 2653 times.
✓ Branch 15 → 27 taken 1766 times.
✓ Branch 15 → 34 taken 1197 times.
✓ Branch 15 → 35 taken 1197 times.
✓ Branch 15 → 39 taken 1 time.
9218 switch (role) {
48 2 case Qt::UserRole: return command.id;
49
1/1
✓ Branch 17 → 18 taken 1201 times.
1201 case Qt::UserRole + 1: return CommandRegistry::translatedDescription(command);
50
1/1
✓ Branch 21 → 22 taken 1201 times.
1201 case Qt::UserRole + 2: return CommandRegistry::keyText(command);
51 2653 case Qt::UserRole + 3: return isAvailable(command.id);
52 // S105.9.5: the help opens on the keys that work here and now.
53
4/4
✓ Branch 28 → 29 taken 394 times.
✓ Branch 28 → 30 taken 1372 times.
✓ Branch 29 → 31 taken 394 times.
✓ Branch 30 → 31 taken 1372 times.
3532 case Qt::UserRole + 4: return index.row() < m_screenRows ? QCoreApplication::translate("QuickCommandRouter", "On this screen")
54 1766 : CommandRegistry::translatedCategory(command);
55 1197 case Qt::UserRole + 5: return command.changedKey;
56
1/1
✓ Branch 35 → 36 taken 1197 times.
1197 case Qt::UserRole + 6: return CommandRegistry::translatedExplanation(command);
57 1 default: return {};
58 }
59 }
60 904 void QuickCommandRouter::rebuild() {
61
1/1
✓ Branch 2 → 3 taken 904 times.
904 beginResetModel();
62
1/1
✓ Branch 3 → 4 taken 904 times.
904 m_commands.clear();
63 // F93-15: the help overlay searches id, description, key and category;
64 // every word of the query has to match somewhere. Grouped by category
65 // (stable, registry order inside a category) for the section headers.
66
1/1
✓ Branch 7 → 8 taken 904 times.
904 const QStringList words = m_query.split(QLatin1Char(' '), Qt::SkipEmptyParts);
67
3/3
✓ Branch 8 → 9 taken 904 times.
✓ Branch 51 → 11 taken 185320 times.
✓ Branch 51 → 52 taken 904 times.
186224 for (const auto &command : CommandRegistry::specs()) {
68
3/3
✓ Branch 18 → 19 taken 185320 times.
✓ Branch 21 → 22 taken 185320 times.
✓ Branch 22 → 23 taken 185320 times.
370640 const QString haystack = command.id + QLatin1Char(' ') + CommandRegistry::translatedDescription(command) +
69
4/4
✓ Branch 15 → 16 taken 185320 times.
✓ Branch 23 → 24 taken 185320 times.
✓ Branch 24 → 25 taken 185320 times.
✓ Branch 25 → 26 taken 185320 times.
741280 QLatin1Char(' ') + CommandRegistry::keyText(command) + QLatin1Char(' ') +
70
2/2
✓ Branch 12 → 13 taken 185320 times.
✓ Branch 26 → 27 taken 185320 times.
555960 CommandRegistry::translatedCategory(command);
71 185320 bool matches = true;
72
7/7
✓ Branch 38 → 39 taken 4364 times.
✓ Branch 38 → 42 taken 1581 times.
✓ Branch 39 → 40 taken 4364 times.
✓ Branch 40 → 41 taken 442 times.
✓ Branch 40 → 42 taken 3922 times.
✓ Branch 45 → 37 taken 5945 times.
✓ Branch 45 → 46 taken 185320 times.
191265 for (const auto &word : words) matches = matches && haystack.contains(word, Qt::CaseInsensitive);
73
3/3
✓ Branch 46 → 47 taken 181398 times.
✓ Branch 46 → 48 taken 3922 times.
✓ Branch 47 → 48 taken 181398 times.
185320 if (matches) m_commands.append(&command);
74 185320 }
75 904 QStringList order;
76
4/4
✓ Branch 52 → 53 taken 904 times.
✓ Branch 53 → 54 taken 904 times.
✓ Branch 68 → 55 taken 181398 times.
✓ Branch 68 → 69 taken 904 times.
182302 for (const auto *command : m_commands)
77
5/5
✓ Branch 57 → 58 taken 181398 times.
✓ Branch 60 → 61 taken 14183 times.
✓ Branch 60 → 66 taken 167215 times.
✓ Branch 62 → 63 taken 14183 times.
✓ Branch 63 → 64 taken 14183 times.
181398 if (!order.contains(QString::fromLatin1(command->category))) order.append(QString::fromLatin1(command->category));
78
3/3
✓ Branch 69 → 70 taken 904 times.
✓ Branch 70 → 71 taken 904 times.
✓ Branch 71 → 72 taken 904 times.
904 std::stable_sort(m_commands.begin(), m_commands.end(), [&order](const CommandSpec *a, const CommandSpec *b) {
79
2/2
✓ Branch 3 → 4 taken 783644 times.
✓ Branch 6 → 7 taken 783644 times.
783644 return order.indexOf(QString::fromLatin1(a->category)) < order.indexOf(QString::fromLatin1(b->category));
80 });
81 // S105.9.5: without a query, "On this screen" first — the commands
82 // with a key that acts here now (the overlay's content as text).
83 904 QList<const CommandSpec *> screen;
84
2/2
✓ Branch 73 → 74 taken 883 times.
✓ Branch 73 → 92 taken 21 times.
904 if (words.isEmpty())
85
2/2
✓ Branch 90 → 77 taken 181015 times.
✓ Branch 90 → 91 taken 883 times.
181898 for (const auto *command : std::as_const(m_commands))
86
9/13
✓ Branch 78 → 79 taken 181015 times.
✗ Branch 78 → 83 not taken.
✓ Branch 79 → 80 taken 181015 times.
✓ Branch 80 → 81 taken 181015 times.
✓ Branch 81 → 82 taken 601 times.
✓ Branch 81 → 83 taken 180414 times.
✓ Branch 84 → 85 taken 181015 times.
✗ Branch 84 → 86 not taken.
✓ Branch 86 → 87 taken 601 times.
✓ Branch 86 → 88 taken 180414 times.
✓ Branch 87 → 88 taken 601 times.
✗ Branch 146 → 147 not taken.
✗ Branch 146 → 148 not taken.
181015 if (command->showInHelp && !effectiveKey(command->id).isEmpty()) screen.append(command);
87 904 m_screenRows = int(screen.size());
88
1/1
✓ Branch 93 → 94 taken 904 times.
904 m_commands = screen + m_commands;
89
1/1
✓ Branch 96 → 97 taken 904 times.
904 endResetModel();
90 904 }
91 34 void QuickCommandRouter::setQuery(const QString &query) {
92
2/2
✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 5 taken 23 times.
34 if (m_query == query) return;
93
1/1
✓ Branch 5 → 6 taken 23 times.
23 m_query = query.left(1024);
94 23 rebuild();
95 23 emit queryChanged();
96 }
97 842 void QuickCommandRouter::setSupported(const QSet<QString> &commands) {
98
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 841 times.
842 if (m_supported == commands) return;
99 841 m_supported = commands;
100 841 cancelSequence();
101 841 notifyAvailability();
102 }
103 833 void QuickCommandRouter::setNamedSupported(const QSet<QString> &commands) {
104
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 833 times.
833 if (m_namedSupported == commands) return;
105 833 m_namedSupported = commands;
106 833 notifyAvailability();
107 }
108 2293 QVariantMap QuickCommandRouter::namedTarget(const QString &text) const {
109
1/1
✓ Branch 2 → 3 taken 2293 times.
2293 const auto parsed = NamedCommandCatalog::parse(text);
110
2/2
✓ Branch 3 → 4 taken 951 times.
✓ Branch 3 → 5 taken 1342 times.
2293 if (!parsed.spec) return {};
111 1342 QStringList values; // D-15a: "filter unread important" → unread, important
112
7/7
✓ Branch 5 → 6 taken 1342 times.
✓ Branch 6 → 7 taken 1342 times.
✓ Branch 7 → 8 taken 1342 times.
✓ Branch 11 → 12 taken 348 times.
✓ Branch 12 → 13 taken 348 times.
✓ Branch 16 → 9 taken 348 times.
✓ Branch 16 → 17 taken 1342 times.
1690 for (const auto *value : parsed.values()) values << QString::fromLatin1(value->word);
113 1342 return {{"command", parsed.spec->name}, {"argument", parsed.argument},
114 1342 {"value", values.value(0)}, {"values", values}, {"words", parsed.words()},
115 1342 {"rest", parsed.rest()}, {"registryId", parsed.registryId()},
116
1/1
✓ Branch 31 → 33 taken 38 times.
2684 {"stateKey", parsed.spec->stateKey ? QString::fromLatin1(parsed.spec->stateKey) : QString{}},
117
1/1
✓ Branch 37 → 38 taken 1342 times.
2684 {"complete", parsed.complete()}, {"syntax", QString::fromUtf8(parsed.spec->syntax)},
118
2/4
✓ Branch 43 → 44 taken 14762 times.
✓ Branch 43 → 45 taken 1342 times.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 65 not taken.
16104 {"inContext", parsed.spec->contexts.testFlag(m_context)}};
119 6319 }
120 765 void QuickCommandRouter::setCompletionSources(const CommandCompletion::Sources &sources) {
121 765 m_sources = sources;
122 765 m_sourcesInstalled = true;
123 765 }
124 1075 CommandCompletion::Sources QuickCommandRouter::effectiveSources(const QVariantMap &states) const {
125 1075 CommandCompletion::Sources sources = m_sources;
126 1075 sources.context = m_context;
127 1075 sources.designPreview = !m_sourcesInstalled;
128 4092 sources.connected = [this](const QString &name) { return m_namedSupported.contains(name); };
129 2891 sources.shortcut = [](const QString &id) {
130 1816 const auto *command = CommandRegistry::find(id);
131
1/2
✓ Branch 3 → 4 taken 1816 times.
✗ Branch 3 → 5 not taken.
1816 return command ? CommandRegistry::keyText(*command) : QString{};
132 1075 };
133
1/2
✓ Branch 6 → 7 taken 1075 times.
✗ Branch 6 → 9 not taken.
1539 if (!sources.history) sources.history = [this] { return m_history.lines(); };
134 2150 sources.state = [states](const QString &key) {
135
11/15
✓ Branch 3 → 4 taken 308 times.
✓ Branch 3 → 11 taken 1 time.
✓ Branch 5 → 6 taken 308 times.
✓ Branch 6 → 7 taken 308 times.
✓ Branch 7 → 8 taken 57 times.
✓ Branch 7 → 9 taken 251 times.
✓ Branch 10 → 12 taken 308 times.
✓ Branch 13 → 14 taken 308 times.
✓ Branch 13 → 15 taken 1 time.
✓ Branch 15 → 16 taken 308 times.
✓ Branch 15 → 17 taken 1 time.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
✗ Branch 23 → 24 not taken.
✗ Branch 23 → 25 not taken.
309 return states.contains(key) ? QString(states.value(key).toBool() ? "on" : "off") : QString{};
136
1/1
✓ Branch 10 → 11 taken 1075 times.
1075 };
137 1075 return sources;
138 ✗ }
139 1060 QVariantMap QuickCommandRouter::completions(const QString &text, int cursor, const QVariantMap &states) const {
140
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1059 times.
1060 if (text.size() > CommandCompletion::MaxInput) return {};
141
3/3
✓ Branch 5 → 6 taken 1059 times.
✓ Branch 6 → 7 taken 1059 times.
✓ Branch 7 → 8 taken 1059 times.
1059 return CommandCompletion::complete(text, cursor, effectiveSources(states)).toMap();
142 }
143 173 bool QuickCommandRouter::recordCommand(const QString &line) {
144
2/2
✓ Branch 3 → 4 taken 33 times.
✓ Branch 3 → 5 taken 140 times.
173 if (!m_history.record(line)) return false;
145 140 m_clearedHistory.clear();
146 140 emit historyChanged();
147 140 return true;
148 }
149 3 void QuickCommandRouter::clearHistory() {
150
7/31
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 12 taken 3 times.
✗ Branch 4 → 5 not taken.
✗ Branch 6 → 7 not taken.
✗ Branch 7 → 8 not taken.
✗ Branch 8 → 9 not taken.
✗ Branch 9 → 10 not taken.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 3 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 3 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 3 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 3 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 3 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 3 times.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 33 not taken.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
✗ Branch 43 → 44 not taken.
✗ Branch 43 → 45 not taken.
✗ Branch 47 → 48 not taken.
✗ Branch 47 → 49 not taken.
3 if (m_history.isEmpty() && m_history.save().value("recentFolders").toMap().isEmpty()) return;
151
1/1
✓ Branch 25 → 26 taken 3 times.
3 m_clearedHistory = m_history.save();
152 3 m_history.clear();
153 3 emit historyChanged();
154 }
155 4 bool QuickCommandRouter::restoreHistory() {
156
2/2
✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 1 time.
4 if (m_clearedHistory.isEmpty()) return false;
157 1 m_history.restore(m_clearedHistory);
158 1 m_clearedHistory.clear();
159 1 emit historyChanged();
160 1 return true;
161 }
162 133 void QuickCommandRouter::recordFolder(const QString &account, const QString &path) {
163
3/3
✓ Branch 3 → 4 taken 133 times.
✓ Branch 7 → 8 taken 9 times.
✓ Branch 7 → 9 taken 124 times.
133 if (m_history.recentFolders(account).value(0) == path) return;
164 124 m_history.recordFolder(account, path);
165 124 emit historyChanged();
166 }
167 16 QString QuickCommandRouter::correction(const QString &text) const {
168
2/2
✓ Branch 3 → 4 taken 16 times.
✓ Branch 4 → 5 taken 16 times.
16 return CommandCompletion::correction(text, effectiveSources({}));
169 }
170 190 bool QuickCommandRouter::canInvokeNamed(const QString &text) const {
171
1/1
✓ Branch 2 → 3 taken 190 times.
190 const auto parsed = NamedCommandCatalog::parse(text);
172
12/14
✓ Branch 6 → 7 taken 188 times.
✓ Branch 7 → 8 taken 187 times.
✓ Branch 7 → 22 taken 1 time.
✓ Branch 9 → 10 taken 187 times.
✓ Branch 10 → 11 taken 186 times.
✓ Branch 10 → 22 taken 1 time.
✓ Branch 12 → 13 taken 186 times.
✓ Branch 13 → 14 taken 186 times.
✓ Branch 14 → 15 taken 185 times.
✓ Branch 14 → 22 taken 1 time.
✓ Branch 24 → 25 taken 186 times.
✓ Branch 24 → 26 taken 4 times.
✗ Branch 29 → 30 not taken.
✗ Branch 29 → 31 not taken.
378 return m_window && m_window->isVisible() && m_window->isActive() && m_window->property("modalDepth").toInt() == 0
173
4/4
✓ Branch 16 → 17 taken 183 times.
✓ Branch 16 → 22 taken 2 times.
✓ Branch 18 → 19 taken 182 times.
✓ Branch 18 → 22 taken 1 time.
185 && parsed.complete() && m_namedSupported.contains(parsed.spec->name)
174
4/4
✓ Branch 4 → 5 taken 188 times.
✓ Branch 4 → 22 taken 2 times.
✓ Branch 20 → 21 taken 181 times.
✓ Branch 20 → 22 taken 1 time.
568 && parsed.spec->contexts.testFlag(m_context);
175 190 }
176 2 QString QuickCommandRouter::namedCommandName(const QString &text) const {
177
1/1
✓ Branch 2 → 3 taken 2 times.
2 const auto parsed = NamedCommandCatalog::parse(text);
178
2/2
✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 1 time.
4 return parsed.spec ? parsed.spec->name : QString{};
179 2 }
180 11 bool QuickCommandRouter::invokeNamed(const QString &text) {
181
3/3
✓ Branch 2 → 3 taken 11 times.
✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 5 taken 3 times.
11 if (!canInvokeNamed(text)) return false;
182
1/1
✓ Branch 5 → 6 taken 3 times.
3 const auto parsed = NamedCommandCatalog::parse(text);
183
1/1
✓ Branch 6 → 7 taken 3 times.
3 cancelSequence();
184
1/1
✓ Branch 7 → 8 taken 3 times.
3 emit namedInvoked(parsed.spec->name, parsed.argument);
185 3 return true;
186 3 }
187 2186 void QuickCommandRouter::setContext(CommandContext context) {
188
2/2
✓ Branch 2 → 3 taken 1173 times.
✓ Branch 2 → 4 taken 1013 times.
2186 if (m_context == context) return;
189 1013 m_context = context;
190 1013 cancelSequence();
191 1013 notifyAvailability();
192 }
193 861 void QuickCommandRouter::bindWorkspace(WorkspaceState *workspace) {
194
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 861 times.
861 if (m_workspace == workspace) return;
195
1/2
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 9 taken 861 times.
861 if (m_workspace) disconnect(m_workspace, nullptr, this, nullptr);
196 861 m_workspace = workspace;
197
1/2
✓ Branch 10 → 11 taken 861 times.
✗ Branch 10 → 20 not taken.
861 if (workspace) {
198
1/1
✓ Branch 11 → 12 taken 861 times.
861 connect(workspace, &WorkspaceState::activeTabChanged, this, &QuickCommandRouter::updateWorkspaceContext);
199
1/1
✓ Branch 13 → 14 taken 861 times.
861 connect(workspace, &WorkspaceState::areaChanged, this, &QuickCommandRouter::updateWorkspaceContext);
200
1/1
✓ Branch 15 → 16 taken 861 times.
861 connect(workspace, &WorkspaceState::selectionChanged, this, &QuickCommandRouter::cancelSequence);
201
1/1
✓ Branch 17 → 18 taken 861 times.
861 connect(workspace, &QObject::destroyed, this, [this] { setContext(CommandContext::None); });
202 }
203 861 updateWorkspaceContext();
204 }
205 2169 void QuickCommandRouter::updateWorkspaceContext() {
206 2169 cancelSequence();
207
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 7 taken 2169 times.
2169 if (!m_workspace) { setContext(CommandContext::None); return; }
208
2/2
✓ Branch 11 → 12 taken 141 times.
✓ Branch 11 → 13 taken 2028 times.
2169 if (m_workspace->area() == "calendar") setContext(CommandContext::Calendar);
209
2/2
✓ Branch 17 → 18 taken 89 times.
✓ Branch 17 → 19 taken 1939 times.
2028 else if (m_workspace->area() == "tasks") setContext(CommandContext::Task);
210
2/2
✓ Branch 23 → 24 taken 88 times.
✓ Branch 23 → 25 taken 1851 times.
1939 else if (m_workspace->area() == "contacts") setContext(CommandContext::Contact);
211
2/2
✓ Branch 29 → 30 taken 57 times.
✓ Branch 29 → 31 taken 1794 times.
1851 else if (m_workspace->area() == "rules")
212 57 setContext(CommandContext::Rules); // F98-01: the rule manager's tab
213
2/2
✓ Branch 35 → 36 taken 153 times.
✓ Branch 35 → 37 taken 1641 times.
1794 else if (m_workspace->area() == "settings") setContext(CommandContext::Settings); // Sprint 100
214
1/2
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 1641 times.
1641 else if (m_workspace->area() != "mail") setContext(CommandContext::None);
215
2/2
✓ Branch 45 → 46 taken 1137 times.
✓ Branch 45 → 47 taken 504 times.
1641 else if (m_workspace->activeTab() == 0) setContext(CommandContext::MainMail);
216
2/2
✓ Branch 49 → 50 taken 105 times.
✓ Branch 49 → 51 taken 399 times.
504 else setContext(m_workspace->activeIsSearch() ? CommandContext::Search : CommandContext::MailTab); // Sprint 101 CB-06
217 }
218 872 void QuickCommandRouter::setRulesOpen(bool open) {
219
2/2
✓ Branch 2 → 3 taken 831 times.
✓ Branch 2 → 4 taken 41 times.
872 if (m_rulesOpen == open) return;
220 41 m_rulesOpen = open;
221 41 updateWorkspaceContext();
222 41 emit rulesOpenChanged();
223 }
224 14930028 bool QuickCommandRouter::isAvailable(const QString &id) const {
225 14930028 const auto *spec = CommandRegistry::find(id);
226
5/6
✓ Branch 3 → 4 taken 14930028 times.
✗ Branch 3 → 11 not taken.
✓ Branch 5 → 6 taken 14228485 times.
✓ Branch 5 → 11 taken 701543 times.
✓ Branch 9 → 10 taken 4877687 times.
✓ Branch 9 → 11 taken 9350798 times.
14930028 return spec && m_supported.contains(id) && (spec->contexts & activeContexts());
227 }
228 14228485 CommandContexts QuickCommandRouter::activeContexts() const {
229
2/2
✓ Branch 3 → 4 taken 201066 times.
✓ Branch 3 → 5 taken 14027419 times.
14228485 return exclusiveContext(m_focusContext) ? m_focusContext : (CommandContexts(m_context) | m_focusContext);
230 }
231 66325 QVariantMap QuickCommandRouter::availability() const {
232 66325 QVariantMap result;
233
5/5
✓ Branch 2 → 3 taken 66325 times.
✓ Branch 6 → 7 taken 13596625 times.
✓ Branch 8 → 9 taken 13596625 times.
✓ Branch 12 → 5 taken 13596625 times.
✓ Branch 12 → 13 taken 66325 times.
13662950 for (const auto &spec : CommandRegistry::specs()) result.insert(spec.id, isAvailable(spec.id));
234 66325 return result;
235 ✗ }
236 3148 void QuickCommandRouter::notifyAvailability() {
237
5/6
✓ Branch 3 → 4 taken 3148 times.
✗ Branch 3 → 13 not taken.
✓ Branch 4 → 5 taken 3148 times.
✓ Branch 7 → 8 taken 3148 times.
✓ Branch 9 → 10 taken 3148 times.
✓ Branch 10 → 11 taken 3148 times.
3148 if (!m_commands.isEmpty()) emit dataChanged(index(0), index(m_commands.size() - 1), {Qt::UserRole + 3});
238 3148 emit availabilityChanged();
239 3148 }
240 22096 void QuickCommandRouter::cancelSequence() {
241 22096 m_prefixTimeout.stop();
242
2/2
✓ Branch 4 → 5 taken 22015 times.
✓ Branch 4 → 6 taken 81 times.
22096 if (m_prefix.isEmpty()) return;
243
1/1
✓ Branch 6 → 7 taken 81 times.
81 m_prefix = {};
244 81 emit prefixChanged();
245 }
246 838 void QuickCommandRouter::attach(QQuickWindow *window) {
247 838 cancelSequence();
248
1/2
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 9 taken 838 times.
838 if (m_window) {
249 ✗ m_window->removeEventFilter(this);
250 ✗ disconnect(m_window, nullptr, this, nullptr);
251 }
252 838 m_window = window;
253
1/2
✓ Branch 11 → 12 taken 838 times.
✗ Branch 11 → 19 not taken.
838 if (m_window) {
254 838 m_window->installEventFilter(this);
255
1/1
✓ Branch 14 → 15 taken 838 times.
838 connect(window, &QQuickWindow::activeFocusItemChanged, this, [this] {
256 9498 cancelSequence();
257 9498 updateFocusContext();
258 9498 });
259
1/1
✓ Branch 16 → 17 taken 838 times.
838 connect(window, &QWindow::activeChanged, this, [this] {
260 933 cancelSequence();
261
2/2
✓ Branch 5 → 6 taken 31 times.
✓ Branch 5 → 7 taken 902 times.
933 if (!m_window->isActive()) m_overlay->windowDeactivated();
262 933 });
263 }
264 838 }
265 2416 bool QuickCommandRouter::invoke(const QString &id) {
266
11/14
✓ Branch 5 → 6 taken 2414 times.
✓ Branch 6 → 7 taken 2413 times.
✓ Branch 6 → 16 taken 1 time.
✓ Branch 8 → 9 taken 2413 times.
✓ Branch 9 → 10 taken 2410 times.
✓ Branch 9 → 16 taken 3 times.
✓ Branch 11 → 12 taken 2410 times.
✓ Branch 12 → 13 taken 2410 times.
✓ Branch 13 → 14 taken 2410 times.
✗ Branch 13 → 16 not taken.
✓ Branch 20 → 21 taken 15 times.
✓ Branch 20 → 22 taken 2401 times.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
4830 if (!m_window || !m_window->isVisible() || !m_window->isActive() || m_window->property("modalDepth").toInt() > 0
267
7/7
✓ Branch 3 → 4 taken 2414 times.
✓ Branch 3 → 16 taken 2 times.
✓ Branch 14 → 15 taken 2410 times.
✓ Branch 15 → 16 taken 9 times.
✓ Branch 15 → 17 taken 2401 times.
✓ Branch 18 → 19 taken 2410 times.
✓ Branch 18 → 20 taken 6 times.
4845 || !isAvailable(id)) return false;
268 2401 cancelSequence();
269
2/2
✓ Branch 23 → 24 taken 781 times.
✓ Branch 23 → 25 taken 1620 times.
2401 if (m_dryRun) emit dryRunInvoked(id);
270 1620 else emit invoked(id);
271 2401 return true;
272 }
273 7 QString QuickCommandRouter::commandAt(int row) const {
274
3/4
✓ Branch 2 → 3 taken 5 times.
✓ Branch 2 → 7 taken 2 times.
✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 7 not taken.
7 return row >= 0 && row < m_commands.size() ? m_commands[row]->id : QString{};
275 }
276 4 QVariantMap QuickCommandRouter::entryAt(int row) const {
277 ✗ return {{"id", commandAt(row)},
278
4/4
✓ Branch 15 → 16 taken 4 times.
✓ Branch 16 → 17 taken 4 times.
✓ Branch 17 → 18 taken 4 times.
✓ Branch 18 → 19 taken 4 times.
8 {"label", data(index(row), Qt::UserRole + 1).toString() + QStringLiteral(" [") +
279
5/5
✓ Branch 8 → 9 taken 4 times.
✓ Branch 9 → 10 taken 4 times.
✓ Branch 10 → 11 taken 4 times.
✓ Branch 19 → 20 taken 4 times.
✓ Branch 20 → 21 taken 4 times.
12 data(index(row), Qt::UserRole + 2).toString() + QStringLiteral("]")},
280
4/6
✓ Branch 23 → 24 taken 4 times.
✓ Branch 24 → 25 taken 4 times.
✓ Branch 28 → 29 taken 12 times.
✓ Branch 28 → 30 taken 4 times.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 48 not taken.
24 {"available", data(index(row), Qt::UserRole + 3)}};
281 12 }
282
1/1
✓ Branch 3 → 4 taken 1 time.
1 bool QuickCommandRouter::invokeRow(int row) { return invoke(commandAt(row)); }
283 // F-32 (SPRINT-104): keypad keys carry the KeypadModifier and compare
284 // unequal to their registered plain equivalents — strip it so Del, 1–5
285 // and Enter of the numeric keypad trigger the same commands (no
286 // Ctrl/Alt/Meta combination loses its modifier). Single-letter shortcuts
287 // must fire on every keyboard layout: QKeySequence(keyCombination())
288 // compares the LAYOUT-DEPENDENT text, so a Russian or Greek layout never
289 // matched "D"/"R"/"A" — with no modifiers held, an unmodified letter
290 // matches its physical (US-layout) key. Entry punctuation (:, /, ?)
291 // stays layout-normalized as before.
292 6772 QKeySequence QuickCommandRouter::normalizedShortcut(const QKeyEvent *key) {
293 6772 QKeySequence sequence(key->keyCombination());
294
3/3
✓ Branch 7 → 8 taken 6772 times.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 24 taken 6770 times.
6772 if (key->modifiers() & Qt::KeypadModifier) {
295
1/1
✓ Branch 11 → 12 taken 2 times.
2 const auto combination = key->keyCombination();
296 2 const auto modifiers = combination.keyboardModifiers() & ~Qt::KeypadModifier;
297 2 sequence = modifiers
298
2/4
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 2 times.
✗ Branch 18 → 21 not taken.
✓ Branch 20 → 21 taken 2 times.
6 ? QKeySequence(combination.key() | int(modifiers))
299 4 : QKeySequence(combination.key());
300 }
301
5/5
✓ Branch 26 → 27 taken 6772 times.
✓ Branch 29 → 30 taken 6202 times.
✓ Branch 29 → 40 taken 570 times.
✓ Branch 32 → 33 taken 5912 times.
✓ Branch 32 → 39 taken 290 times.
18886 if (!(key->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier)) &&
302
12/18
✓ Branch 35 → 36 taken 5878 times.
✓ Branch 35 → 39 taken 34 times.
✓ Branch 38 → 39 taken 3 times.
✓ Branch 38 → 40 taken 5875 times.
✓ Branch 41 → 42 taken 5878 times.
✓ Branch 41 → 43 taken 894 times.
✓ Branch 43 → 44 taken 5912 times.
✓ Branch 43 → 45 taken 860 times.
✓ Branch 45 → 46 taken 6202 times.
✓ Branch 45 → 47 taken 570 times.
✓ Branch 47 → 48 taken 327 times.
✓ Branch 47 → 54 taken 6445 times.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
✗ Branch 103 → 104 not taken.
✗ Branch 103 → 105 not taken.
✗ Branch 108 → 109 not taken.
✗ Branch 108 → 110 not taken.
18886 (key->text() == ":" || key->text() == "/" || key->text() == "?"))
303
1/1
✓ Branch 49 → 50 taken 327 times.
327 sequence = QKeySequence(key->text());
304 // Sprint 105 D-12: "?" needs Shift on most layouts; Ctrl+? is one key.
305
7/7
✓ Branch 55 → 56 taken 4 times.
✓ Branch 55 → 61 taken 6768 times.
✓ Branch 56 → 57 taken 4 times.
✓ Branch 59 → 60 taken 2 times.
✓ Branch 59 → 61 taken 2 times.
✓ Branch 62 → 63 taken 2 times.
✓ Branch 62 → 72 taken 6770 times.
6772 if (key->key() == Qt::Key_Question && (key->modifiers() & Qt::ControlModifier))
306
1/1
✓ Branch 68 → 69 taken 2 times.
2 sequence = QKeySequence(QKeyCombination(Qt::ControlModifier, Qt::Key_Question));
307 // AltGr (GroupSwitch) types a character (AltGr+Q is "@", AltGr+E "€" on
308 // a German layout): never the plain letter's command.
309
1/1
✓ Branch 76 → 77 taken 6772 times.
20316 if (!(key->modifiers() & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier | Qt::ShiftModifier |
310
2/2
✓ Branch 81 → 82 taken 5627 times.
✓ Branch 81 → 85 taken 484 times.
19655 Qt::GroupSwitchModifier)) &&
311
6/6
✓ Branch 79 → 80 taken 6111 times.
✓ Branch 79 → 85 taken 661 times.
✓ Branch 83 → 84 taken 1324 times.
✓ Branch 83 → 85 taken 4303 times.
✓ Branch 86 → 87 taken 1324 times.
✓ Branch 86 → 92 taken 5448 times.
12883 key->key() >= Qt::Key_A && key->key() <= Qt::Key_Z)
312
1/1
✓ Branch 88 → 89 taken 1324 times.
1324 sequence = QKeySequence(key->key());
313 6772 return sequence;
314 ✗ }
315
316 2199 bool QuickCommandRouter::invokeFromKeyboard(const QString &id) {
317 2199 const QScopedValueRollback<bool> keyboard(m_fromKeyboard, true);
318
1/1
✓ Branch 3 → 4 taken 2199 times.
4398 return invoke(id);
319 2199 }
320