MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - ShortcutHelpOverlay.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 98.3 % 116 114
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 10 10
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 37.5 % 554 208

             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 : }
        

Generated by: LCOV version 2.0-1