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 |