Branch data Line data Source code
1 : : #include "ShortcutHelpOverlay.h"
2 : :
3 : : #include "ui/ThemeManager.h"
4 : :
5 : : #include <QEvent>
6 : : #include <QKeyEvent>
7 : : #include <QPainter>
8 : : #include <QWheelEvent>
9 : :
10 : : // 67.B3: overlay palette (dark canvas in both themes) — tokens
11 : : // @overlay_* in ThemeManager (docs/DESIGN.md §2).
12 : 777 : static QColor ovColor(const char *token) {
13 [ + - + - : 777 : return QColor(ThemeManager::instance().color(QLatin1String(token)));
+ - ]
14 : : }
15 : :
16 : 61 : ShortcutHelpOverlay::ShortcutHelpOverlay(QWidget *parent)
17 [ + - ]: 61 : : QWidget(parent) {
18 [ + - ]: 61 : setFocusPolicy(Qt::StrongFocus);
19 [ + - ]: 61 : setAttribute(Qt::WA_TransparentForMouseEvents, false);
20 : : // Re-fit the overlay whenever the parent is resized. showOverlay()
21 : : // snapshots parentWidget()->rect() once; without this filter a parent
22 : : // that is still settling (or resized while open) leaves the overlay at
23 : : // a stale size and exposes a gap at the bottom — a non-deterministic
24 : : // E2E screenshot and a real UX bug. Same pattern as CommandBar's
25 : : // Sprint-75 overlay Resize filter.
26 [ + - ]: 61 : if (parent)
27 [ + - ]: 61 : parent->installEventFilter(this);
28 [ + - ]: 61 : hide();
29 : 61 : }
30 : :
31 : 9 : void ShortcutHelpOverlay::showOverlay() {
32 : 9 : m_scrollOffset = 0;
33 [ + - ]: 9 : if (parentWidget()) {
34 [ + - ]: 9 : setGeometry(parentWidget()->rect());
35 : : }
36 : 9 : show();
37 : 9 : raise();
38 : 9 : setFocus();
39 : 9 : }
40 : :
41 : 2041 : bool ShortcutHelpOverlay::eventFilter(QObject *obj, QEvent *event) {
42 [ + - + + : 2041 : if (obj == parentWidget() && event->type() == QEvent::Resize)
+ + ]
43 [ + - ]: 16 : if (auto *p = parentWidget())
44 [ + - ]: 16 : setGeometry(p->rect());
45 : 2041 : return QWidget::eventFilter(obj, event);
46 : : }
47 : :
48 : 7 : void ShortcutHelpOverlay::paintEvent(QPaintEvent *) {
49 [ + - ]: 7 : QPainter p(this);
50 : : // Dark semi-transparent background
51 [ + - + - ]: 7 : p.fillRect(rect(), ThemeManager::overlayScrimColor());
52 : :
53 : : // Content area (card)
54 : 7 : int cw = qMin(width() - 80, 620);
55 : 7 : int ch = qMin(height() - 80, 780);
56 : 7 : QRect contentRect((width() - cw) / 2, (height() - ch) / 2, cw, ch);
57 [ + - ]: 7 : p.setPen(Qt::NoPen);
58 [ + - + - : 7 : p.setBrush(ThemeManager::overlayCardColor());
+ - ]
59 [ + - ]: 7 : p.drawRoundedRect(contentRect, 8, 8); /* Sprint 69: Radius L (cards) */
60 : :
61 : : // Shortcuts data: {category, {key, action}...}
62 : : struct Entry { QString key; QString action; };
63 : : struct Category { QString name; QList<Entry> entries; };
64 : :
65 : : QList<Category> categories = {
66 : : {tr("Navigation"),
67 : : {{"j / k", tr("Next / Previous Mail")},
68 : : {"J / K", tr("Page Down / Up")},
69 : : {"G", tr("Last Mail")},
70 : : {"g g", tr("First Mail")},
71 : : {"o / Enter", tr("Open Mail")},
72 : : {"B", tr("Previous Folder")},
73 : : {"c", tr("Calendar")}}},
74 : : {tr("Actions"),
75 : : {{"r", tr("Read/Unread")},
76 : : {"m", tr("Flag \u2605")},
77 : : {"n", tr("New Message")},
78 : : {"d", tr("Delete (\u2192 Trash)")},
79 : : {"a", tr("Archive")},
80 : : {"s", tr("Quick-Move (Suggestion)")},
81 : : {"S", tr("Manual Move")},
82 : : {"x", tr("Mark as Junk")},
83 : : {"b", tr("Switch Folder")}}},
84 : : {tr("Sorting"),
85 : : {{"Alt+S", tr("Suggestions for all mails")},
86 : : {"e", tr("Toggle alternate folder")}}},
87 : : {tr("Undo"),
88 : : {{"Ctrl+Z", tr("Undo (always active)")},
89 : : {"u", tr("Undo (normal mode)")}}},
90 : : {tr("Tabs"),
91 : : {{"t / Dblclick", tr("Open mail in tab")},
92 : : {"Ctrl+W", tr("Close Tab")},
93 : : {"Ctrl+Tab", tr("Next Tab")},
94 : : {"Ctrl+Shift+Tab", tr("Previous Tab")},
95 : : {"Ctrl+0-9", tr("Jump to tab n")},
96 : : {"Esc", tr("Close tab (in tab)")}}},
97 : : {tr("Console"),
98 : : {{":", tr("Command Bar")},
99 : : {"/", tr("Quick Filter")},
100 : : {"f", tr("Global Search")}}},
101 : : {tr("Labels"),
102 : : {{"1-5", tr("Toggle label 1-5")},
103 : : {"0", tr("Remove all labels")},
104 : : {"L", tr("Label menu")}}},
105 : : {tr("Folders"),
106 : : {{"Ctrl+Shift+N", tr("New Subfolder")},
107 : : {"F2", tr("Rename Folder")},
108 : : {"Shift+D", tr("Delete Folder")},
109 : : {"V", tr("Move Folder")},
110 : : {":create", tr("Create folder by command")},
111 : : {":rename", tr("Rename folder by command")}}},
112 : : {tr("Other"),
113 : : {{"Ctrl+R", tr("Reply")},
114 : : {"Ctrl+Shift+R", tr("Reply All")},
115 : : {"Ctrl+Shift+F", tr("Forward")},
116 : : {"Ctrl+T", tr("Thread View")},
117 : : {"Tab", tr("Next Panel")},
118 : : {"?", tr("This Help")},
119 [ + + - - ]: 385 : {"Esc", tr("Close")}}}};
120 : :
121 : : // Measure total content height (title + all entries + gaps)
122 : 7 : constexpr int kTitleH = 36 + 32; // title top margin + gap below
123 : 7 : constexpr int kCatH = 22; // category heading
124 : 7 : constexpr int kEntryH = 20; // single entry row
125 : 7 : constexpr int kCatGap = 8; // gap after category
126 : 7 : constexpr int kFooterH = 28; // footer line
127 : :
128 : 7 : int totalH = kTitleH;
129 [ + - + + ]: 70 : for (const auto &cat : categories) {
130 : 63 : totalH += kCatH;
131 : 63 : totalH += cat.entries.size() * kEntryH;
132 : 63 : totalH += kCatGap;
133 : : }
134 : 7 : totalH += kFooterH;
135 : 7 : m_contentHeight = totalH;
136 : :
137 : : // Available inner height (inside the card, with padding)
138 : 7 : int innerTop = contentRect.top() + 12;
139 : 7 : int innerBottom = contentRect.bottom() - 12;
140 : 7 : int visibleH = innerBottom - innerTop;
141 : :
142 : : // Clamp scroll offset
143 : 7 : int maxScroll = qMax(0, m_contentHeight - visibleH);
144 [ + - ]: 7 : m_scrollOffset = qBound(0, m_scrollOffset, maxScroll);
145 : :
146 : : // Clip to content area (so nothing draws outside the card)
147 [ + - ]: 7 : p.setClipRect(contentRect.adjusted(0, 12, 0, -12));
148 : :
149 : : // Starting y, adjusted by scroll offset
150 : 7 : int y = innerTop + 36 - m_scrollOffset;
151 : :
152 : : // Title
153 [ + - + - ]: 7 : p.setPen(ovColor("@overlay_fg"));
154 [ + - + - ]: 7 : QFont titleFont("Sans", 16, QFont::Bold);
155 [ + - ]: 7 : p.setFont(titleFont);
156 [ + - ]: 7 : p.drawText(contentRect.adjusted(24, 0, -24, 0).translated(0, -m_scrollOffset + 12),
157 : 7 : Qt::AlignTop | Qt::AlignHCenter,
158 [ + - ]: 14 : tr("Keyboard Shortcuts"));
159 : :
160 [ + - + - ]: 7 : QFont catFont("Sans", 12, QFont::Bold);
161 [ + - + - ]: 7 : QFont entryFont("Monospace", 11);
162 [ + - + - ]: 7 : QFont descFont("Sans", 11);
163 : 7 : int col1x = contentRect.left() + 24;
164 : 7 : int col2x = contentRect.left() + 170;
165 : :
166 : 7 : y += 32; // gap below title
167 : :
168 [ + - + - : 70 : for (const auto &cat : categories) {
+ + ]
169 [ + - ]: 63 : p.setFont(catFont);
170 [ + - + - ]: 63 : p.setPen(ovColor("@overlay_key"));
171 [ + - ]: 63 : p.drawText(col1x, y, cat.name);
172 : 63 : y += kCatH;
173 : :
174 [ + - + - ]: 63 : p.setPen(ovColor("@overlay_fg"));
175 [ + + ]: 378 : for (const auto &e : cat.entries) {
176 [ + - ]: 315 : p.setFont(entryFont);
177 [ + - + - ]: 315 : p.setPen(ovColor("@overlay_value"));
178 [ + - ]: 315 : p.drawText(col1x + 8, y, e.key);
179 [ + - ]: 315 : p.setFont(descFont);
180 [ + - + - ]: 315 : p.setPen(ovColor("@overlay_fg"));
181 [ + - ]: 315 : p.drawText(col2x, y, e.action);
182 : 315 : y += kEntryH;
183 : : }
184 : 63 : y += kCatGap;
185 : : }
186 : :
187 : : // Footer (always at bottom of content, or visible area)
188 [ + - ]: 7 : p.setFont(descFont);
189 [ + - + - ]: 7 : p.setPen(ovColor("@overlay_muted"));
190 : 7 : int footerY = y + 4;
191 [ + - ]: 7 : p.drawText(col1x, footerY,
192 : 14 : QStringLiteral("Beliebige Taste drücken zum Schließen"));
193 : :
194 [ + - ]: 7 : p.setClipping(false);
195 : :
196 : : // Scroll indicator: show small arrows if content is scrollable
197 [ + - ]: 7 : if (maxScroll > 0) {
198 [ + - + - ]: 7 : p.setPen(ovColor("@overlay_muted"));
199 [ + - ]: 7 : p.setFont(descFont);
200 [ - + ]: 7 : if (m_scrollOffset > 0) {
201 : : // Up arrow at top
202 [ # # ]: 0 : p.drawText(contentRect.adjusted(0, 4, 0, 0),
203 : 0 : Qt::AlignTop | Qt::AlignHCenter, QStringLiteral("▲"));
204 : : }
205 [ + - ]: 7 : if (m_scrollOffset < maxScroll) {
206 : : // Down arrow at bottom
207 [ + - ]: 7 : p.drawText(contentRect.adjusted(0, 0, 0, -4),
208 : 14 : Qt::AlignBottom | Qt::AlignHCenter, QStringLiteral("▼"));
209 : : }
210 : : }
211 [ + - + - : 336 : }
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
- + - + -
+ - + - +
+ + + + +
+ + + + +
+ + + + +
+ + + - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - - -
- - - -
- ]
212 : :
213 : 1 : void ShortcutHelpOverlay::keyPressEvent(QKeyEvent *) {
214 : 1 : hide();
215 : 1 : }
216 : :
217 : 1 : void ShortcutHelpOverlay::mousePressEvent(QMouseEvent *) {
218 : 1 : hide();
219 : 1 : }
220 : :
221 : 2 : void ShortcutHelpOverlay::wheelEvent(QWheelEvent *event) {
222 : : // Scroll by ~3 lines per notch (60 pixels per 120-degree step)
223 : 2 : int delta = event->angleDelta().y();
224 : 2 : m_scrollOffset -= delta / 2; // positive delta = scroll up
225 : 2 : m_scrollOffset = qMax(0, m_scrollOffset);
226 : 2 : update();
227 : 2 : event->accept();
228 : 2 : }
229 : :
230 : : // T-76.B3: Runtime language switching. Every visible label (title,
231 : : // category headings, key/action pairs) is generated from tr() inside
232 : : // paintEvent(), so triggering a repaint is sufficient — no stored
233 : : // widgets or labels to setText().
234 : 399 : void ShortcutHelpOverlay::changeEvent(QEvent *event) {
235 [ + + ]: 399 : if (event->type() == QEvent::LanguageChange)
236 : 86 : retranslateUi();
237 : 399 : QWidget::changeEvent(event);
238 : 399 : }
239 : :
240 : 86 : void ShortcutHelpOverlay::retranslateUi() {
241 : 86 : update(); // paintEvent re-evaluates all tr() strings
242 : 86 : }
|