MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - FolderPropertiesDialog.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 96.4 % 223 215
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 20 20
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 43.9 % 492 216

             Branch data     Line data    Source code
       1                 :             : #include "FolderPropertiesDialog.h"
       2                 :             : 
       3                 :             : #include "ui/ThemeManager.h"
       4                 :             : #include "MdiIconProvider.h"
       5                 :             : #include "data/MailCache.h"
       6                 :             : #include "data/FolderPredictor.h"
       7                 :             : 
       8                 :             : #include <QCheckBox>
       9                 :             : #include <QColorDialog>
      10                 :             : #include <QDialogButtonBox>
      11                 :             : #include <QGroupBox>
      12                 :             : #include <QHBoxLayout>
      13                 :             : #include <QLabel>
      14                 :             : #include <QLineEdit>
      15                 :             : #include <QListWidget>
      16                 :             : #include <QPushButton>
      17                 :             : #include <QVBoxLayout>
      18                 :             : #include <QEvent>
      19                 :             : 
      20                 :             : static constexpr int ICON_SIZE   = 32; // display size in picker
      21                 :             : static constexpr int ICON_RENDER = 28; // rendered glyph size
      22                 :             : 
      23                 :          10 : FolderPropertiesDialog::FolderPropertiesDialog(const Options &opts,
      24                 :             :                                                MailCache *cache,
      25                 :             :                                                FolderPredictor *predictor,
      26                 :             :                                                const QString &account,
      27                 :          10 :                                                QWidget *parent)
      28                 :          20 :     : QDialog(parent), m_opts(opts), m_cache(cache), m_predictor(predictor),
      29                 :          10 :       m_account(account),
      30         [ +  - ]:          20 :       m_selectedIcon(opts.currentIcon), m_selectedColor(opts.currentColor) {
      31   [ +  -  +  -  :          20 :   setWindowTitle(tr("Edit Folder – %1").arg(opts.folderPath));
                   +  - ]
      32         [ +  - ]:          10 :   setMinimumWidth(480);
      33                 :             : 
      34   [ +  -  +  -  :          10 :   auto *mainLayout = new QVBoxLayout(this);
             -  +  -  - ]
      35         [ +  - ]:          10 :   mainLayout->setSpacing(12);
      36                 :             : 
      37         [ +  - ]:          10 :   buildAppearanceSection();
      38         [ +  - ]:          10 :   buildSubscriptionSection();
      39         [ +  - ]:          10 :   buildCacheSection();
      40         [ +  - ]:          10 :   buildStatsSection();  // T-288
      41                 :             : 
      42                 :             :   auto *buttonBox =
      43   [ +  -  +  -  :          10 :       new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
             -  +  -  - ]
      44         [ +  - ]:          10 :   connect(buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept);
      45         [ +  - ]:          10 :   connect(buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject);
      46         [ +  - ]:          10 :   mainLayout->addWidget(buttonBox);
      47                 :          10 : }
      48                 :             : 
      49                 :          10 : void FolderPropertiesDialog::buildAppearanceSection() {
      50   [ +  -  +  -  :          10 :   auto *group = new QGroupBox(tr("Appearance"), this);
             -  +  -  - ]
      51   [ +  -  -  +  :          10 :   auto *vbox  = new QVBoxLayout(group);
                   -  - ]
      52                 :             : 
      53                 :             :   // ── Icon picker ──────────────────────────────────────────────────────────
      54   [ +  -  +  -  :          10 :   vbox->addWidget(new QLabel(tr("Icon:")));
          +  -  +  -  -  
                +  -  - ]
      55                 :             : 
      56                 :             :   // Search field
      57   [ +  -  -  +  :          10 :   m_iconFilter = new QLineEdit;
                   -  - ]
      58   [ +  -  +  - ]:          10 :   m_iconFilter->setPlaceholderText(tr("Search icon… (e.g. folder, star, email)"));
      59                 :          10 :   m_iconFilter->setClearButtonEnabled(true);
      60         [ +  - ]:          10 :   vbox->addWidget(m_iconFilter);
      61                 :             : 
      62                 :             :   // QListWidget in IconMode — only visible cells are rendered (virtual scroll)
      63   [ +  -  -  +  :          10 :   m_iconList = new QListWidget;
                   -  - ]
      64                 :          10 :   m_iconList->setViewMode(QListView::IconMode);
      65                 :          10 :   m_iconList->setResizeMode(QListView::Adjust);
      66                 :          10 :   m_iconList->setMovement(QListView::Static);
      67         [ +  - ]:          10 :   m_iconList->setIconSize(QSize(ICON_RENDER, ICON_RENDER));
      68         [ +  - ]:          10 :   m_iconList->setGridSize(QSize(ICON_SIZE + 4, ICON_SIZE + 4));
      69                 :          10 :   m_iconList->setUniformItemSizes(true);
      70                 :          10 :   m_iconList->setWordWrap(false);
      71                 :          10 :   m_iconList->setFixedHeight(240);
      72                 :          10 :   m_iconList->setSelectionMode(QAbstractItemView::SingleSelection);
      73                 :             :   // Styled via main.qss generic QListWidget rules (67.B3)
      74         [ +  - ]:          10 :   vbox->addWidget(m_iconList);
      75                 :             : 
      76                 :             :   // Populate with all icons (lazy: QListWidget only paints visible ones)
      77                 :          10 :   auto &mdi = MdiIconProvider::instance();
      78                 :          10 :   mdi.ensureInit();
      79                 :          10 :   const auto &icons = mdi.allIcons();
      80         [ +  + ]:       74490 :   for (const auto &e : icons) {
      81   [ +  -  +  -  :       74480 :     auto *item = new QListWidgetItem;
             -  +  -  - ]
      82   [ +  -  +  - ]:       74480 :     item->setIcon(mdi.icon(e.name, ICON_RENDER));
      83         [ +  - ]:       74480 :     item->setData(Qt::UserRole, e.name);
      84         [ +  - ]:       74480 :     item->setToolTip(e.name);
      85         [ +  - ]:       74480 :     item->setSizeHint(QSize(ICON_SIZE + 4, ICON_SIZE + 4));
      86         [ +  - ]:       74480 :     m_iconList->addItem(item);
      87         [ -  + ]:       74480 :     if (e.name == m_selectedIcon) {
      88         [ #  # ]:           0 :       m_iconList->setCurrentItem(item);
      89                 :             :     }
      90                 :             :   }
      91                 :             : 
      92                 :          10 :   connect(m_iconFilter, &QLineEdit::textChanged,
      93         [ +  - ]:          10 :           this, &FolderPropertiesDialog::filterIcons);
      94                 :             : 
      95                 :          10 :   connect(m_iconList, &QListWidget::currentItemChanged, this,
      96         [ +  - ]:          10 :           [this](QListWidgetItem *item) {
      97   [ +  -  +  -  :           1 :             m_selectedIcon = item ? item->data(Qt::UserRole).toString() : QString{};
          +  -  +  -  -  
                      - ]
      98                 :           1 :           });
      99                 :             : 
     100                 :             :   // ── Color picker ─────────────────────────────────────────────────────────
     101   [ +  -  -  +  :          10 :   auto *colorRow = new QHBoxLayout;
                   -  - ]
     102   [ +  -  +  -  :          10 :   colorRow->addWidget(new QLabel(tr("Color:")));
          +  -  +  -  -  
                +  -  - ]
     103   [ +  -  -  +  :          10 :   m_colorBtn = new QPushButton;
                   -  - ]
     104                 :          10 :   m_colorBtn->setFixedSize(80, 28);
     105                 :          10 :   updateColorPreview();
     106         [ +  - ]:          10 :   connect(m_colorBtn, &QPushButton::clicked, this, [this]() {
     107         [ +  - ]:           1 :     QColor initial = m_selectedColor.isEmpty() ? Qt::white : QColor(m_selectedColor);
     108   [ +  -  +  - ]:           1 :     QColor color = QColorDialog::getColor(initial, this, tr("Choose Folder Color"));
     109         [ -  + ]:           1 :     if (color.isValid()) {
     110         [ #  # ]:           0 :       m_selectedColor = color.name();
     111         [ #  # ]:           0 :       updateColorPreview();
     112                 :             :     }
     113                 :           1 :   });
     114         [ +  - ]:          10 :   colorRow->addWidget(m_colorBtn);
     115                 :          10 :   colorRow->addStretch();
     116                 :             : 
     117   [ +  -  +  -  :          10 :   auto *resetBtn = new QPushButton(tr("Reset"));
             -  +  -  - ]
     118         [ +  - ]:          10 :   connect(resetBtn, &QPushButton::clicked, this, [this]() {
     119                 :           1 :     m_selectedIcon.clear();
     120                 :           1 :     m_selectedColor.clear();
     121                 :           1 :     m_iconList->clearSelection();
     122                 :           1 :     updateColorPreview();
     123                 :           1 :   });
     124         [ +  - ]:          10 :   colorRow->addWidget(resetBtn);
     125                 :          10 :   vbox->addLayout(colorRow);
     126                 :             : 
     127         [ +  - ]:          10 :   static_cast<QVBoxLayout *>(layout())->addWidget(group);
     128                 :          10 : }
     129                 :             : 
     130                 :           7 : void FolderPropertiesDialog::filterIcons(const QString &text) {
     131                 :             :   // Hide/show items based on name match — O(n) but instant for 7k items
     132                 :           7 :   auto &mdi = MdiIconProvider::instance();
     133                 :           7 :   const auto &icons = mdi.allIcons();
     134                 :             : 
     135                 :             :   // We iterate items and icons in lock-step (same order)
     136                 :           7 :   int count = m_iconList->count();
     137                 :           7 :   int iconCount = icons.size();
     138   [ +  +  +  - ]:       52143 :   for (int i = 0; i < count && i < iconCount; ++i) {
     139   [ +  +  +  + ]:       52136 :     bool visible = text.isEmpty() || icons[i].name.contains(text, Qt::CaseInsensitive);
     140                 :       52136 :     m_iconList->item(i)->setHidden(!visible);
     141                 :             :   }
     142                 :           7 : }
     143                 :             : 
     144                 :          10 : void FolderPropertiesDialog::buildSubscriptionSection() {
     145   [ +  -  +  -  :          10 :   auto *group = new QGroupBox(tr("Subscription"), this);
             -  +  -  - ]
     146   [ +  -  -  +  :          10 :   auto *vbox  = new QVBoxLayout(group);
                   -  - ]
     147                 :             : 
     148   [ +  -  +  -  :          10 :   m_subscribedCheck = new QCheckBox(tr("Subscribe folder for badges"), group);
             -  +  -  - ]
     149                 :          10 :   m_subscribedCheck->setChecked(m_opts.isSubscribed);
     150         [ +  - ]:          10 :   m_subscribedCheck->setToolTip(
     151         [ +  - ]:          20 :       tr("Subscribed folders are included in badge polling"));
     152         [ +  - ]:          10 :   vbox->addWidget(m_subscribedCheck);
     153                 :             : 
     154   [ +  -  +  -  :          10 :   m_hiddenCheck = new QCheckBox(tr("Hide in folder tree"), group);
             -  +  -  - ]
     155                 :          10 :   m_hiddenCheck->setChecked(m_opts.isHidden);
     156         [ +  - ]:          10 :   m_hiddenCheck->setToolTip(
     157         [ +  - ]:          20 :       tr("Hidden folders and their subfolders are not displayed"));
     158         [ +  - ]:          10 :   vbox->addWidget(m_hiddenCheck);
     159                 :             : 
     160         [ +  - ]:          10 :   static_cast<QVBoxLayout *>(layout())->addWidget(group);
     161                 :          10 : }
     162                 :             : 
     163                 :          10 : void FolderPropertiesDialog::buildCacheSection() {
     164   [ +  -  +  -  :          10 :   auto *group = new QGroupBox(tr("Cache"), this);
             -  +  -  - ]
     165   [ +  -  -  +  :          10 :   auto *vbox  = new QVBoxLayout(group);
                   -  - ]
     166                 :             : 
     167   [ +  -  -  +  :          10 :   m_headerCountLabel = new QLabel(group);
                   -  - ]
     168   [ +  -  -  +  :          10 :   m_bodyCountLabel   = new QLabel(group);
                   -  - ]
     169                 :          10 :   refreshCacheLabels();
     170         [ +  - ]:          10 :   vbox->addWidget(m_headerCountLabel);
     171         [ +  - ]:          10 :   vbox->addWidget(m_bodyCountLabel);
     172                 :             : 
     173   [ +  -  +  -  :          10 :   auto *purgeBtn = new QPushButton(tr("Clear Cache"));
             -  +  -  - ]
     174   [ +  -  +  - ]:          10 :   purgeBtn->setToolTip(tr("Delete all cached data for this folder"));
     175         [ +  - ]:          10 :   connect(purgeBtn, &QPushButton::clicked, this, [this]() {
     176         [ +  - ]:           1 :     if (m_cache) {
     177                 :           1 :       m_cache->purgeFolderByPath(m_account, m_opts.folderPath);
     178                 :           1 :       refreshCacheLabels();
     179                 :             :     }
     180                 :           1 :   });
     181         [ +  - ]:          10 :   vbox->addWidget(purgeBtn);
     182                 :             : 
     183         [ +  - ]:          10 :   static_cast<QVBoxLayout *>(layout())->addWidget(group);
     184                 :          10 : }
     185                 :             : 
     186                 :          11 : void FolderPropertiesDialog::updateColorPreview() {
     187         [ +  + ]:          11 :   if (m_selectedColor.isEmpty()) {
     188         [ +  - ]:          10 :     m_colorBtn->setStyleSheet(QString());
     189   [ +  -  +  - ]:          10 :     m_colorBtn->setText(tr("None"));
     190                 :             :   } else {
     191         [ +  - ]:           2 :     m_colorBtn->setStyleSheet(
     192                 :           2 :         QStringLiteral("background-color: %1; border: 1px solid %2;")
     193         [ +  - ]:           2 :             .arg(m_selectedColor,
     194   [ +  -  +  - ]:           3 :                  ThemeManager::instance().color(
     195                 :           2 :                      QStringLiteral("@border_medium"))));
     196                 :           1 :     m_colorBtn->setText(m_selectedColor);
     197                 :             :   }
     198                 :          11 : }
     199                 :             : 
     200                 :          12 : void FolderPropertiesDialog::refreshCacheLabels() {
     201                 :          12 :   int headers = 0, bodies = 0;
     202         [ +  + ]:          12 :   if (m_cache) {
     203                 :          11 :     headers = m_cache->cachedHeaderCount(m_account, m_opts.folderPath);
     204                 :          11 :     bodies  = m_cache->cachedBodyCount(m_account, m_opts.folderPath);
     205                 :             :   }
     206   [ +  -  +  -  :          24 :   m_headerCountLabel->setText(tr("Cached Headers: %1").arg(headers));
                   +  - ]
     207   [ +  -  +  -  :          24 :   m_bodyCountLabel->setText(tr("Body Cache: %1").arg(bodies));
                   +  - ]
     208                 :          12 : }
     209                 :             : 
     210                 :           7 : bool FolderPropertiesDialog::isSubscribed() const {
     211                 :           7 :   return m_subscribedCheck->isChecked();
     212                 :             : }
     213                 :             : 
     214                 :           7 : bool FolderPropertiesDialog::isHidden() const {
     215                 :           7 :   return m_hiddenCheck->isChecked();
     216                 :             : }
     217                 :             : 
     218                 :             : // T-288: Human-readable file size formatting
     219                 :          33 : static QString formatSize(qint64 bytes) {
     220         [ +  - ]:          33 :   if (bytes < 1024)
     221         [ +  - ]:          66 :     return QStringLiteral("%1 B").arg(bytes);
     222         [ #  # ]:           0 :   if (bytes < 1024 * 1024)
     223         [ #  # ]:           0 :     return QStringLiteral("%1 KB").arg(bytes / 1024.0, 0, 'f', 1);
     224         [ #  # ]:           0 :   if (bytes < 1024LL * 1024 * 1024)
     225         [ #  # ]:           0 :     return QStringLiteral("%1 MB").arg(bytes / (1024.0 * 1024), 0, 'f', 1);
     226         [ #  # ]:           0 :   return QStringLiteral("%1 GB").arg(bytes / (1024.0 * 1024 * 1024), 0, 'f', 2);
     227                 :             : }
     228                 :             : 
     229                 :          10 : void FolderPropertiesDialog::buildStatsSection() {
     230   [ +  -  +  -  :          10 :   auto *group = new QGroupBox(tr("Statistics"), this);
             -  +  -  - ]
     231   [ +  -  -  +  :          10 :   auto *vbox = new QVBoxLayout(group);
                   -  - ]
     232                 :             : 
     233   [ +  -  -  +  :          10 :   m_diskUsageLabel = new QLabel(group);
                   -  - ]
     234   [ +  -  -  +  :          10 :   m_serverSizeLabel = new QLabel(group);
                   -  - ]
     235   [ +  -  -  +  :          10 :   m_avgSizeLabel = new QLabel(group);
                   -  - ]
     236   [ +  -  -  +  :          10 :   m_predictorDocsLabel = new QLabel(group);
                   -  - ]
     237   [ +  -  -  +  :          10 :   m_predictorTokensLabel = new QLabel(group);
                   -  - ]
     238                 :             : 
     239         [ +  - ]:          10 :   vbox->addWidget(m_diskUsageLabel);
     240         [ +  - ]:          10 :   vbox->addWidget(m_serverSizeLabel);
     241         [ +  - ]:          10 :   vbox->addWidget(m_avgSizeLabel);
     242                 :             : 
     243                 :          10 :   vbox->addSpacing(4);
     244         [ +  - ]:          10 :   vbox->addWidget(m_predictorDocsLabel);
     245         [ +  - ]:          10 :   vbox->addWidget(m_predictorTokensLabel);
     246                 :             : 
     247                 :             :   // Body-only purge button
     248   [ +  -  +  -  :          10 :   auto *bodyPurgeBtn = new QPushButton(tr("Clear Body Cache Only"));
             -  +  -  - ]
     249         [ +  - ]:          10 :   bodyPurgeBtn->setToolTip(
     250         [ +  - ]:          20 :       tr("Removes cached mail content but keeps headers"));
     251         [ +  - ]:          10 :   connect(bodyPurgeBtn, &QPushButton::clicked, this, [this]() {
     252         [ +  - ]:           1 :     if (m_cache) {
     253                 :           1 :       m_cache->purgeBodyCache(m_account, m_opts.folderPath);
     254                 :           1 :       refreshCacheLabels();
     255                 :           1 :       refreshStatsLabels();
     256                 :             :     }
     257                 :           1 :   });
     258         [ +  - ]:          10 :   vbox->addWidget(bodyPurgeBtn);
     259                 :             : 
     260                 :             :   // Predictor reset button
     261   [ +  -  +  -  :          10 :   auto *resetPredBtn = new QPushButton(tr("Reset Predictor Data"));
             -  +  -  - ]
     262         [ +  - ]:          10 :   resetPredBtn->setToolTip(
     263         [ +  - ]:          20 :       tr("Resets the training data for this folder"));
     264         [ +  - ]:          10 :   connect(resetPredBtn, &QPushButton::clicked, this, [this]() {
     265         [ +  - ]:           1 :     if (m_predictor) {
     266                 :           1 :       m_predictor->resetFolder(m_opts.folderPath);
     267                 :           1 :       refreshStatsLabels();
     268                 :             :     }
     269                 :           1 :   });
     270         [ +  - ]:          10 :   vbox->addWidget(resetPredBtn);
     271                 :             : 
     272                 :          10 :   refreshStatsLabels();
     273         [ +  - ]:          10 :   static_cast<QVBoxLayout *>(layout())->addWidget(group);
     274                 :          10 : }
     275                 :             : 
     276                 :          12 : void FolderPropertiesDialog::refreshStatsLabels() {
     277         [ +  + ]:          12 :   if (m_cache) {
     278                 :          11 :     qint64 diskUsage = m_cache->cachedDiskUsage(m_account, m_opts.folderPath);
     279                 :          11 :     qint64 serverSize = m_cache->totalServerSize(m_account, m_opts.folderPath);
     280                 :          11 :     qint64 avgSize = m_cache->averageMailSize(m_account, m_opts.folderPath);
     281         [ +  - ]:          11 :     m_diskUsageLabel->setText(
     282   [ +  -  +  -  :          33 :         tr("Cache Storage: %1").arg(formatSize(diskUsage)));
                   +  - ]
     283         [ +  - ]:          11 :     m_serverSizeLabel->setText(
     284   [ +  -  +  -  :          33 :         tr("Server Size: %1").arg(formatSize(serverSize)));
                   +  - ]
     285         [ +  - ]:          11 :     m_avgSizeLabel->setText(
     286   [ +  -  +  -  :          33 :         tr("Avg. Mail Size: %1").arg(formatSize(avgSize)));
                   +  - ]
     287                 :             :   } else {
     288   [ +  -  +  - ]:           1 :     m_diskUsageLabel->setText(tr("Cache Storage: n/a"));
     289   [ +  -  +  - ]:           1 :     m_serverSizeLabel->setText(tr("Server Size: n/a"));
     290   [ +  -  +  - ]:           1 :     m_avgSizeLabel->setText(tr("Avg. Mail Size: n/a"));
     291                 :             :   }
     292                 :             : 
     293         [ +  + ]:          12 :   if (m_predictor) {
     294                 :          11 :     int docs = m_predictor->documentsForFolder(m_opts.folderPath);
     295                 :          11 :     int tokens = m_predictor->tokenCountForFolder(m_opts.folderPath);
     296                 :          11 :     bool sufficient = m_predictor->hasSufficientData(m_opts.folderPath);
     297         [ +  - ]:          11 :     m_predictorDocsLabel->setText(
     298         [ +  - ]:          11 :         tr("Training Documents: %1%2")
     299         [ +  - ]:          22 :             .arg(docs)
     300   [ -  +  +  -  :          22 :             .arg(sufficient ? "" : " (unzureichend)"));
                   +  - ]
     301         [ +  - ]:          11 :     m_predictorTokensLabel->setText(
     302   [ +  -  +  - ]:          33 :         tr("Learned Tokens: %1").arg(tokens));
     303                 :             :   } else {
     304   [ +  -  +  - ]:           1 :     m_predictorDocsLabel->setText(tr("Training Documents: n/a"));
     305   [ +  -  +  - ]:           1 :     m_predictorTokensLabel->setText(tr("Learned Tokens: n/a"));
     306                 :             :   }
     307                 :          12 : }
     308                 :             : 
     309                 :             : // T-304: Runtime language switching
     310                 :          23 : void FolderPropertiesDialog::changeEvent(QEvent *event) {
     311         [ +  + ]:          23 :   if (event->type() == QEvent::LanguageChange)
     312                 :           1 :     retranslateUi();
     313                 :          23 :   QDialog::changeEvent(event);
     314                 :          23 : }
     315                 :             : 
     316                 :           1 : void FolderPropertiesDialog::retranslateUi() {
     317                 :             :   // Most labels are created once in setupUi; title is dynamic
     318                 :             :   // Nothing to retranslate since labels are inline in layout
     319                 :           1 : }
        

Generated by: LCOV version 2.0-1