MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - ThemeManager.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 96.6 % 118 114
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 23 23
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 53.2 % 342 182

             Branch data     Line data    Source code
       1                 :             : #include "ThemeManager.h"
       2                 :             : #include <QApplication>
       3                 :             : #include <QFile>
       4                 :             : #include <QLoggingCategory>
       5                 :             : #include <QSettings>
       6                 :             : #include <QStyleHints>
       7                 :             : #include <QTextStream>
       8                 :             : #include <QDebug>
       9                 :             : 
      10                 :             : #include <algorithm>
      11                 :             : 
      12   [ +  -  +  -  :           1 : Q_LOGGING_CATEGORY(lcTheme, "mailjd.theme")
             +  -  -  - ]
      13                 :             : 
      14                 :       90032 : ThemeManager& ThemeManager::instance() {
      15   [ +  +  +  -  :       90032 :     static ThemeManager inst;
             +  -  -  - ]
      16                 :       90032 :     return inst;
      17                 :             : }
      18                 :             : 
      19                 :          32 : ThemeManager::ThemeManager(QObject *parent) : QObject(parent) {}
      20                 :             : 
      21                 :          41 : void ThemeManager::applyTheme(Theme theme) {
      22                 :          41 :     m_currentTheme = theme;
      23         [ +  - ]:          41 :     m_currentVars.clear();
      24                 :             : 
      25   [ +  +  +  -  :          41 :     m_currentVars = theme == Light ? lightPalette() : darkPalette();
                   +  - ]
      26                 :             : 
      27   [ +  -  +  - ]:          41 :     QString qss = loadAndProcessQss(":/themes/main.qss", m_currentVars);
      28         [ +  - ]:          41 :     qApp->setStyleSheet(qss);
      29                 :             : 
      30         [ +  - ]:          41 :     emit themeChanged(theme);
      31                 :          41 : }
      32                 :             : 
      33                 :             : // Tokens are documented in docs/DESIGN.md §2 — change them there first.
      34                 :          48 : QMap<QString, QString> ThemeManager::lightPalette() {
      35                 :             :     return {
      36                 :             :         {"@bg_main", "#ffffff"},
      37                 :             :         {"@bg_sidebar", "#f1f3f5"},
      38                 :             :         {"@bg_selected", "#e7f1ff"},
      39                 :             :         {"@bg_hover", "#e9ecef"},
      40                 :             :         {"@text_primary", "#212529"},
      41                 :             :         {"@text_secondary", "#6c757d"},
      42                 :             :         {"@text_muted", "#adb5bd"},
      43                 :             :         {"@accent", "#007bff"},
      44                 :             :         {"@accent_hover", "#0069d9"},
      45                 :             :         {"@accent_muted", "#cfe2ff"},
      46                 :             :         {"@border_light", "#dee2e6"},
      47                 :             :         {"@border_medium", "#ced4da"},
      48                 :             :         {"@unread_dot", "#007bff"},
      49                 :             :         {"@star_active", "#e8a700"},
      50                 :             :         {"@star_inactive", "#ced4da"},
      51                 :             :         {"@danger", "#d92d2d"},
      52                 :             :         {"@danger_bg", "#fff0ef"},
      53                 :             :         {"@success", "#28a745"},
      54                 :             :         {"@warning", "#e8a700"},
      55                 :             :         {"@link", "#007bff"},
      56                 :             :         // Markdown editor/preview palette (MarkdownHighlighter/Renderer)
      57                 :             :         {"@md_heading", "#1a73e8"},
      58                 :             :         {"@md_heading2", "#1967d2"},
      59                 :             :         {"@md_heading3", "#4285f4"},
      60                 :             :         {"@md_text", "#24292e"},
      61                 :             :         {"@md_quote_fg", "#5f6368"},
      62                 :             :         {"@md_quote_bg", "#f8f9fa"},
      63                 :             :         {"@md_list_marker", "#4285f4"},
      64                 :             :         {"@md_cb_unchecked", "#757575"},
      65                 :             :         {"@md_cb_checked", "#4caf50"},
      66                 :             :         {"@md_hr", "#b0b0b0"},
      67                 :             :         {"@md_code_fg", "#6a9955"},
      68                 :             :         {"@md_code_bg", "#f6f8fa"},
      69                 :             :         {"@md_inline_code", "#d63384"},
      70                 :             :         {"@md_inline_code_bg", "#f0f0f0"},
      71                 :             :         {"@md_strike", "#9e9e9e"},
      72                 :             :         {"@md_table_border", "#e1e4e8"},
      73                 :             :         {"@md_quote_border", "#4285f4"},
      74                 :             :         // Shortcut-help overlay (dark canvas in both themes)
      75                 :             :         {"@overlay_fg", "#d4d4d4"},
      76                 :             :         {"@overlay_key", "#569cd6"},
      77                 :             :         {"@overlay_value", "#ce9178"},
      78                 :             :         {"@overlay_muted", "#808080"}
      79   [ +  +  -  - ]:        2016 :     };
      80   [ +  -  +  -  :          48 : }
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
             -  -  -  - ]
      81                 :             : 
      82                 :          13 : QMap<QString, QString> ThemeManager::darkPalette() {
      83                 :             :     return {
      84                 :             :         {"@bg_main", "#1e1e1e"},
      85                 :             :         {"@bg_sidebar", "#252525"},
      86                 :             :         {"@bg_selected", "#2c3e50"},
      87                 :             :         {"@bg_hover", "#323232"},
      88                 :             :         {"@text_primary", "#e0e0e0"},
      89                 :             :         {"@text_secondary", "#a0a0a0"},
      90                 :             :         {"@text_muted", "#606060"},
      91                 :             :         {"@accent", "#3daee9"},
      92                 :             :         {"@accent_hover", "#2d9ed9"},
      93                 :             :         {"@accent_muted", "#1d4f72"},
      94                 :             :         {"@border_light", "#323232"},
      95                 :             :         {"@border_medium", "#454545"},
      96                 :             :         {"@unread_dot", "#3daee9"},
      97                 :             :         {"@star_active", "#ffc107"},
      98                 :             :         {"@star_inactive", "#454545"},
      99                 :             :         {"@danger", "#e05555"},
     100                 :             :         {"@danger_bg", "#3a2424"},
     101                 :             :         {"@success", "#4caf50"},
     102                 :             :         {"@warning", "#ffc107"},
     103                 :             :         {"@link", "#3daee9"},
     104                 :             :         // Markdown editor/preview palette (MarkdownHighlighter/Renderer)
     105                 :             :         {"@md_heading", "#6cb2ff"},
     106                 :             :         {"@md_heading2", "#5a9fe8"},
     107                 :             :         {"@md_heading3", "#82b6f2"},
     108                 :             :         {"@md_text", "#e0e0e0"},
     109                 :             :         {"@md_quote_fg", "#a0a0a0"},
     110                 :             :         {"@md_quote_bg", "#2a2a2a"},
     111                 :             :         {"@md_list_marker", "#82b6f2"},
     112                 :             :         {"@md_cb_unchecked", "#909090"},
     113                 :             :         {"@md_cb_checked", "#66bb6a"},
     114                 :             :         {"@md_hr", "#555555"},
     115                 :             :         {"@md_code_fg", "#6a9955"},
     116                 :             :         {"@md_code_bg", "#2a2a2a"},
     117                 :             :         {"@md_inline_code", "#e083b6"},
     118                 :             :         {"@md_inline_code_bg", "#333333"},
     119                 :             :         {"@md_strike", "#777777"},
     120                 :             :         {"@md_table_border", "#454545"},
     121                 :             :         {"@md_quote_border", "#5a9fe8"},
     122                 :             :         // Shortcut-help overlay (dark canvas in both themes)
     123                 :             :         {"@overlay_fg", "#d4d4d4"},
     124                 :             :         {"@overlay_key", "#569cd6"},
     125                 :             :         {"@overlay_value", "#ce9178"},
     126                 :             :         {"@overlay_muted", "#808080"}
     127   [ +  +  -  - ]:         546 :     };
     128   [ +  -  +  -  :          13 : }
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
             -  -  -  - ]
     129                 :             : 
     130                 :       92021 : QString ThemeManager::color(const QString &key) const {
     131         [ +  + ]:       92021 :     if (m_currentVars.isEmpty()) {
     132                 :             :         // No theme applied yet (early startup, unit tests) — Light values.
     133   [ +  +  +  -  :       10578 :         static const QMap<QString, QString> fallback = lightPalette();
             +  -  -  - ]
     134         [ +  - ]:       10578 :         return fallback.value(key);
     135                 :             :     }
     136         [ +  - ]:      162886 :     return m_currentVars.value(key);
     137                 :             : }
     138                 :             : 
     139                 :             : // ── 67.B3: shared programmatic palettes ───────────────────
     140                 :             : 
     141                 :        1643 : QColor ThemeManager::confidenceColor(double confidence) {
     142         [ +  + ]:        1643 :     if (confidence >= 0.97)
     143                 :           4 :         return QColor("#2d7d46"); // green — very confident
     144         [ +  + ]:        1639 :     if (confidence >= 0.93)
     145                 :           4 :         return QColor("#4a90d9"); // blue — confident
     146         [ +  + ]:        1635 :     if (confidence >= 0.88)
     147                 :           6 :         return QColor("#cc8a1e"); // orange — medium
     148         [ +  + ]:        1629 :     if (confidence > 0.0)
     149                 :           3 :         return QColor("#c44f4f"); // red — unsure
     150                 :        1626 :     return QColor("#888888");    // neutral fallback
     151                 :             : }
     152                 :             : 
     153                 :          92 : QStringList ThemeManager::calendarPalette() {
     154                 :          92 :     return {QStringLiteral("#4285F4"), QStringLiteral("#0B8043"),
     155                 :          92 :             QStringLiteral("#D50000"), QStringLiteral("#F4511E"),
     156                 :          92 :             QStringLiteral("#8E24AA"), QStringLiteral("#039BE5"),
     157   [ +  +  -  - ]:         920 :             QStringLiteral("#E67C73"), QStringLiteral("#616161")};
     158   [ +  -  -  -  :         828 : }
                   -  - ]
     159                 :             : 
     160                 :           4 : QColor ThemeManager::mutedConfidenceColor(double confidence) {
     161         [ +  + ]:           4 :     if (confidence > 0.8)
     162                 :           1 :         return QColor("#5b8abf"); // muted blue
     163         [ +  + ]:           3 :     if (confidence > 0.6)
     164                 :           2 :         return QColor("#9a8c7a"); // warm grey
     165                 :           1 :     return QColor("#aaaaaa");     // light grey
     166                 :             : }
     167                 :             : 
     168                 :          27 : QList<QColor> ThemeManager::labelPalette() {
     169                 :             :     return {QColor(0xCC, 0x33, 0x33),  // red
     170                 :             :             QColor(0xE8, 0x7A, 0x00),  // orange
     171                 :             :             QColor(0x33, 0x99, 0x33),  // green
     172                 :             :             QColor(0x33, 0x66, 0xCC),  // blue
     173                 :             :             QColor(0x99, 0x33, 0xCC),  // purple
     174                 :             :             QColor(0xCC, 0x99, 0x00),  // gold
     175                 :          27 :             QColor(0x88, 0x88, 0x88)}; // gray
     176                 :             : }
     177                 :             : 
     178                 :          84 : QColor ThemeManager::avatarColor(const QString &key) {
     179                 :             :     // Deterministic hue from the (lowercased) address; fixed saturation/
     180                 :             :     // lightness keep white initials readable in both themes.
     181   [ +  -  +  - ]:          84 :     const uint hash = qHash(key.trimmed().toLower());
     182                 :          84 :     return QColor::fromHsl(int(hash % 360u), 140, 110);
     183                 :             : }
     184                 :             : 
     185                 :           7 : QColor ThemeManager::overlayScrimColor() {
     186                 :           7 :     return QColor(0, 0, 0, 180);
     187                 :             : }
     188                 :             : 
     189                 :           7 : QColor ThemeManager::overlayCardColor() {
     190                 :           7 :     return QColor(30, 30, 30, 240);
     191                 :             : }
     192                 :             : 
     193                 :             : // ── 67.B2: mode handling ──────────────────────────────────
     194                 :             : 
     195                 :          11 : ThemeManager::Theme ThemeManager::resolveTheme(Mode mode) const {
     196         [ +  + ]:          11 :     if (mode == ModeLight)
     197                 :           4 :         return Light;
     198         [ +  + ]:           7 :     if (mode == ModeDark)
     199                 :           4 :         return Dark;
     200                 :             : #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
     201         [ +  - ]:           3 :     if (qApp) {
     202                 :           3 :         return qApp->styleHints()->colorScheme() == Qt::ColorScheme::Dark
     203         [ -  + ]:           3 :                    ? Dark
     204                 :           3 :                    : Light;
     205                 :             :     }
     206                 :             : #endif
     207                 :             :     // Qt < 6.5 has no colorSchemeChanged — System degrades to Light.
     208                 :           0 :     return Light;
     209                 :             : }
     210                 :             : 
     211                 :           6 : void ThemeManager::setMode(Mode mode) {
     212                 :           6 :     applyModeInternal(mode, /*persist=*/true);
     213                 :           6 : }
     214                 :             : 
     215                 :           2 : void ThemeManager::initFromSettings() {
     216         [ +  - ]:           2 :     QSettings settings;
     217                 :             :     const QString stored =
     218         [ +  - ]:           6 :         settings.value(QStringLiteral("appearance/theme"),
     219         [ +  - ]:           6 :                        QStringLiteral("system")).toString();
     220   [ +  -  +  - ]:           2 :     applyModeInternal(modeFromString(stored), /*persist=*/false);
     221                 :           2 : }
     222                 :             : 
     223                 :           8 : void ThemeManager::applyModeInternal(Mode mode, bool persist) {
     224                 :           8 :     m_mode = mode;
     225         [ +  + ]:           8 :     if (persist) {
     226         [ +  - ]:           6 :         QSettings settings;
     227         [ +  - ]:          12 :         settings.setValue(QStringLiteral("appearance/theme"),
     228         [ +  - ]:          12 :                           modeToString(mode));
     229                 :           6 :     }
     230                 :             : 
     231                 :             : #if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
     232                 :             :     // Watch the OS scheme once; the handler only acts in System mode.
     233   [ +  -  +  +  :           8 :     if (qApp && !m_systemWatchActive) {
                   +  + ]
     234                 :           3 :         m_systemWatchActive = true;
     235   [ +  -  +  - ]:           3 :         connect(qApp->styleHints(), &QStyleHints::colorSchemeChanged, this,
     236                 :           6 :                 [this](Qt::ColorScheme) {
     237         [ #  # ]:           0 :                     if (m_mode == ModeSystem)
     238                 :           0 :                         applyTheme(resolveTheme(ModeSystem));
     239                 :           0 :                 });
     240                 :             :     }
     241                 :             : #endif
     242                 :             : 
     243                 :           8 :     applyTheme(resolveTheme(mode));
     244                 :           8 : }
     245                 :             : 
     246                 :          10 : ThemeManager::Mode ThemeManager::modeFromString(const QString &value) {
     247         [ +  + ]:          10 :     if (value.compare(QStringLiteral("dark"), Qt::CaseInsensitive) == 0)
     248                 :           3 :         return ModeDark;
     249         [ +  + ]:           7 :     if (value.compare(QStringLiteral("light"), Qt::CaseInsensitive) == 0)
     250                 :           3 :         return ModeLight;
     251                 :           4 :     return ModeSystem;
     252                 :             : }
     253                 :             : 
     254                 :           9 : QString ThemeManager::modeToString(Mode mode) {
     255   [ +  +  +  - ]:           9 :     switch (mode) {
     256                 :           8 :     case ModeLight: return QStringLiteral("light");
     257                 :           6 :     case ModeDark: return QStringLiteral("dark");
     258                 :           2 :     case ModeSystem: break;
     259                 :             :     }
     260                 :           2 :     return QStringLiteral("system");
     261                 :             : }
     262                 :             : 
     263                 :          45 : QString ThemeManager::loadAndProcessQss(const QString &path, const QMap<QString, QString> &vars) {
     264         [ +  - ]:          45 :     QFile file(path);
     265   [ +  -  +  + ]:          45 :     if (!file.open(QFile::ReadOnly | QFile::Text)) {
     266   [ +  -  +  -  :           2 :         qCWarning(lcTheme) << "Could not open QSS file" << path;
          +  -  +  -  +  
                      + ]
     267                 :           1 :         return QString();
     268                 :             :     }
     269                 :             : 
     270         [ +  - ]:          44 :     QTextStream stream(&file);
     271         [ +  - ]:          44 :     QString content = stream.readAll();
     272         [ +  - ]:          44 :     file.close();
     273                 :             : 
     274                 :             :     // Variable interpolation — longest keys first, so prefix-sharing
     275                 :             :     // tokens ("@accent" vs "@accent_hover") never corrupt each other.
     276         [ +  - ]:          44 :     QStringList keys = vars.keys();
     277   [ +  -  +  -  :          44 :     std::sort(keys.begin(), keys.end(),
                   +  - ]
     278                 :        9392 :               [](const QString &a, const QString &b) {
     279                 :        9392 :                   return a.size() > b.size();
     280                 :             :               });
     281   [ +  -  +  -  :        1730 :     for (const QString &key : keys) {
                   +  + ]
     282   [ +  -  +  - ]:        1686 :         content.replace(key, vars.value(key));
     283                 :             :     }
     284                 :             : 
     285                 :          44 :     return content;
     286                 :          45 : }
        

Generated by: LCOV version 2.0-1