MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - FolderTree.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 93.6 % 535 501
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 43 43
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 53.3 % 1280 682

             Branch data     Line data    Source code
       1                 :             : #include "FolderTree.h"
       2                 :             : #include "ui/MdiIconProvider.h"
       3                 :             : #include "ui/ThemeManager.h"
       4                 :             : 
       5                 :             : #include <QHeaderView>
       6                 :             : #include <QIcon>
       7                 :             : #include <QColorDialog>
       8                 :             : #include <QLoggingCategory>
       9                 :             : #include <QMenu>
      10                 :             : #include <QMimeData>
      11                 :             : #include <QPainter>
      12                 :             : #include <QPalette>
      13                 :             : #include <QSettings>
      14                 :             : #include <QStandardItemModel>
      15                 :             : #include <QStyledItemDelegate>
      16                 :             : #include <QDragEnterEvent>
      17                 :             : #include <QDataStream>
      18                 :             : 
      19                 :             : #include "data/Models.h"
      20                 :             : 
      21   [ +  +  +  -  :         243 : Q_LOGGING_CATEGORY(lcFolderTree, "mailjd.foldertree")
             +  -  -  - ]
      22                 :             : 
      23                 :             : // 67.B2: Folder icons are monochrome, tinted with the theme's secondary
      24                 :             : // text color (ThemeManager falls back to the Light palette before any
      25                 :             : // applyTheme(), so this is always a valid color).
      26                 :         191 : static QColor themedIconColor() {
      27                 :         191 :   return QColor(
      28   [ +  -  +  - ]:         382 :       ThemeManager::instance().color(QStringLiteral("@text_secondary")));
      29                 :             : }
      30                 :             : 
      31                 :             : // 67.B4: Paints the unread count as a modern badge pill (accent
      32                 :             : // background, white text) right-aligned in the row. The base item
      33                 :             : // (icon, name, hover/selected states) is painted by the style/QSS.
      34                 :             : class FolderBadgeDelegate : public QStyledItemDelegate {
      35                 :             : public:
      36                 :             :   using QStyledItemDelegate::QStyledItemDelegate;
      37                 :             : 
      38                 :        2077 :   void paint(QPainter *painter, const QStyleOptionViewItem &option,
      39                 :             :              const QModelIndex &index) const override {
      40         [ +  - ]:        2077 :     QStyledItemDelegate::paint(painter, option, index);
      41                 :             : 
      42                 :             :     // T-71.2: Paint the accent bar directly instead of relying on QSS
      43                 :             :     // border-left. Qt's stylesheet engine draws duplicate border geometries
      44                 :             :     // for item-view ::item:selected states on Qt 6.4 (shorthand/longhand
      45                 :             :     // and radius differences all trigger it). Painting here is reliable —
      46                 :             :     // same approach as StarDelegate/LabelDelegate.
      47                 :             :     // Paint at the very left edge of the viewport (x=0), not at
      48                 :             :     // option.rect.left() which is offset by the tree indentation.
      49         [ +  + ]:        2077 :     if (option.state & QStyle::State_Selected) {
      50                 :             :       const QColor accent =
      51   [ +  -  +  - ]:         570 :           ThemeManager::instance().color(QStringLiteral("@accent"));
      52         [ +  - ]:         285 :       painter->fillRect(
      53                 :         570 :           QRect(0, option.rect.top(), 3, option.rect.height()),
      54                 :             :           accent);
      55                 :             :     }
      56                 :             : 
      57   [ +  -  +  - ]:        2077 :     const int count = index.data(FolderTree::UnreadCountRole).toInt();
      58         [ +  + ]:        2077 :     if (count <= 0)
      59                 :        1914 :       return;
      60                 :             : 
      61   [ -  +  -  - ]:         163 :     const QString text = count > 999 ? QStringLiteral("999+")
      62   [ -  +  +  - ]:         163 :                                      : QString::number(count);
      63         [ +  - ]:         163 :     QFont badgeFont = option.font;
      64   [ +  -  +  - ]:         163 :     badgeFont.setPointSizeF(qMax(7.5, badgeFont.pointSizeF() - 2));
      65         [ +  - ]:         163 :     badgeFont.setBold(true);
      66         [ +  - ]:         163 :     const QFontMetrics fm(badgeFont);
      67                 :             : 
      68                 :         163 :     const int hPad = 6;
      69         [ +  - ]:         163 :     const int pillH = fm.height() + 2;
      70         [ +  - ]:         163 :     const int pillW = qMax(pillH, fm.horizontalAdvance(text) + 2 * hPad);
      71                 :         326 :     QRect pill(option.rect.right() - pillW - 6,
      72                 :         163 :                option.rect.center().y() - pillH / 2, pillW, pillH);
      73                 :             : 
      74         [ +  - ]:         163 :     painter->save();
      75         [ +  - ]:         163 :     painter->setRenderHint(QPainter::Antialiasing, true);
      76         [ +  - ]:         163 :     painter->setPen(Qt::NoPen);
      77   [ +  -  +  - ]:         163 :     painter->setBrush(QColor(
      78   [ +  -  +  - ]:         489 :         ThemeManager::instance().color(QStringLiteral("@accent"))));
      79         [ +  - ]:         163 :     painter->drawRoundedRect(pill, pillH / 2.0, pillH / 2.0);
      80         [ +  - ]:         163 :     painter->setPen(Qt::white);
      81         [ +  - ]:         163 :     painter->setFont(badgeFont);
      82         [ +  - ]:         163 :     painter->drawText(pill, Qt::AlignCenter, text);
      83         [ +  - ]:         163 :     painter->restore();
      84                 :         163 :   }
      85                 :             : };
      86                 :             : 
      87                 :          91 : FolderTree::FolderTree(QWidget *parent)
      88   [ +  -  +  -  :          91 :     : QTreeView(parent), m_model(new QStandardItemModel(this)) {
          -  +  +  -  +  
                -  -  - ]
      89         [ +  - ]:          91 :   setupUi();
      90                 :             :   // 67.B2: live theme switch re-tints all code-colored folder icons
      91         [ +  - ]:          91 :   connect(&ThemeManager::instance(), &ThemeManager::themeChanged, this,
      92         [ +  - ]:         102 :           [this](ThemeManager::Theme) { refreshFolderIcons(); });
      93                 :          91 : }
      94                 :             : 
      95                 :          91 : void FolderTree::setupUi() {
      96                 :          91 :   setModel(m_model);
      97                 :          91 :   setHeaderHidden(true);
      98         [ +  - ]:          91 :   setEditTriggers(QAbstractItemView::NoEditTriggers);
      99                 :          91 :   setSelectionMode(QAbstractItemView::SingleSelection);
     100                 :          91 :   setAnimated(true);
     101                 :          91 :   setExpandsOnDoubleClick(true);
     102                 :          91 :   setIndentation(14);
     103                 :          91 :   setUniformRowHeights(true);
     104                 :             :   // 67.B4: unread badge pill
     105   [ +  -  +  -  :          91 :   setItemDelegate(new FolderBadgeDelegate(this));
             -  +  -  - ]
     106                 :             : 
     107                 :             :   // T-103: Enable drop target
     108                 :          91 :   setAcceptDrops(true);
     109                 :          91 :   setDropIndicatorShown(true);
     110                 :             : 
     111                 :             :   // T-133: Explicit border: none prevents Breeze/system theme from adding
     112                 :             :   // borders on hover/selected states, which shifts content by ~1px.
     113                 :             :   // Sprint 64: Now handled by global ThemeManager (main.qss).
     114                 :             : 
     115                 :             :   // Selection → emit folderSelected signal
     116         [ +  - ]:          91 :   connect(selectionModel(), &QItemSelectionModel::currentChanged, this,
     117         [ +  - ]:          91 :           [this](const QModelIndex &current, const QModelIndex &) {
     118         [ +  + ]:          55 :             if (current.isValid()) {
     119   [ +  -  +  - ]:          40 :               auto path = current.data(FolderPathRole).toString();
     120         [ +  + ]:          40 :               if (path == QLatin1String(SearchNodePath)) {
     121                 :             :                 // Virtual search node: restore the search results view instead
     122                 :             :                 // of switching to a real folder.
     123         [ +  - ]:           1 :                 emit searchNodeSelected();
     124         [ +  - ]:          39 :               } else if (!path.isEmpty()) {
     125         [ +  - ]:          39 :                 emit folderSelected(path);
     126                 :             :               }
     127                 :          40 :             }
     128                 :          55 :           });
     129                 :             : 
     130                 :             :   // T-069/T-289: Context menu for folder management
     131                 :          91 :   setContextMenuPolicy(Qt::CustomContextMenu);
     132                 :          91 :   connect(this, &QWidget::customContextMenuRequested, this,
     133         [ +  - ]:          91 :           [this](const QPoint &pos) {
     134         [ +  - ]:           4 :             auto idx = indexAt(pos);
     135                 :             : 
     136                 :             :             // T-289: Right-click on empty area → "Neuer Ordner…" only
     137         [ +  + ]:           4 :             if (!idx.isValid()) {
     138         [ +  - ]:           1 :               QMenu menu(this);
     139   [ +  -  +  - ]:           1 :               menu.addAction(tr("New Folder…"), [this]() {
     140         [ +  - ]:           1 :                 emit createFolderRequested(QString()); // empty = top-level
     141                 :           1 :               });
     142   [ +  -  +  -  :           1 :               menu.exec(viewport()->mapToGlobal(pos));
                   +  - ]
     143                 :           1 :               return;
     144                 :           1 :             }
     145                 :             : 
     146   [ +  -  +  - ]:           3 :             auto path = idx.data(FolderPathRole).toString();
     147         [ -  + ]:           3 :             if (path.isEmpty())
     148                 :           0 :               return;
     149                 :             : 
     150                 :             :             // Virtual search node: offer to close (cancel) the search.
     151         [ +  + ]:           3 :             if (path == QLatin1String(SearchNodePath)) {
     152         [ +  - ]:           1 :               QMenu menu(this);
     153   [ +  -  +  - ]:           1 :               menu.addAction(tr("Close Search"), [this]() {
     154                 :           1 :                 emit searchNodeCloseRequested();
     155                 :           1 :               });
     156   [ +  -  +  -  :           1 :               menu.exec(viewport()->mapToGlobal(pos));
                   +  - ]
     157                 :           1 :               return;
     158                 :           1 :             }
     159                 :             : 
     160   [ +  -  +  - ]:           2 :             auto flags = idx.data(FolderFlagsRole).toStringList();
     161         [ +  - ]:           2 :             bool special = isSpecialFolder(path, flags);
     162                 :             : 
     163         [ +  - ]:           2 :             QMenu menu(this);
     164                 :             : 
     165                 :             :             // T-134: Properties dialog
     166   [ +  -  +  - ]:           2 :             menu.addAction(tr("Edit Folder…"), [this, path]() {
     167                 :           1 :               emit folderPropertiesRequested(path);
     168                 :           1 :             });
     169                 :             : 
     170         [ +  - ]:           2 :             menu.addSeparator();
     171                 :             : 
     172                 :             :             // T-289: Create subfolder
     173   [ +  -  +  - ]:           2 :             menu.addAction(tr("New Subfolder…"), [this, path]() {
     174                 :           1 :               emit createFolderRequested(path);
     175                 :           1 :             });
     176                 :             : 
     177                 :             :             // T-289: Rename (disabled for special folders)
     178         [ +  - ]:           2 :             auto *renameAction = menu.addAction(
     179         [ +  - ]:           4 :                 tr("Rename Folder…"), [this, path]() {
     180                 :           1 :                   emit renameFolderRequested(path);
     181                 :           1 :                 });
     182         [ +  - ]:           2 :             renameAction->setEnabled(!special);
     183                 :             : 
     184                 :             :             // T-289: Move (disabled for special folders)
     185         [ +  - ]:           2 :             auto *moveAction = menu.addAction(
     186         [ +  - ]:           4 :                 tr("Move Folder to…"), [this, path]() {
     187                 :           1 :                   emit moveFolderRequested(path);
     188                 :           1 :                 });
     189         [ +  - ]:           2 :             moveAction->setEnabled(!special);
     190                 :             : 
     191         [ +  - ]:           2 :             menu.addSeparator();
     192                 :             : 
     193                 :             :             // T-069: Quick action for hiding
     194   [ +  -  +  - ]:           2 :             menu.addAction(tr("Hide Folder"), [this, path]() {
     195                 :           1 :               emit folderHideRequested(path);
     196                 :           1 :             });
     197                 :             : 
     198                 :             :             // T-200: Mark all mails in folder as read
     199   [ +  -  +  - ]:           2 :             menu.addAction(tr("Mark All as Read"),
     200                 :           4 :                            [this, path]() {
     201                 :           1 :                              emit markAllReadRequested(path);
     202                 :           1 :                            });
     203                 :             : 
     204         [ +  - ]:           2 :             menu.addSeparator();
     205                 :             : 
     206                 :             :             // T-289: Delete (disabled for special folders, red text)
     207         [ +  - ]:           2 :             auto *deleteAction = menu.addAction(
     208         [ +  - ]:           4 :                 tr("Delete Folder…"), [this, path]() {
     209                 :           1 :                   emit deleteFolderRequested(path);
     210                 :           1 :                 });
     211         [ +  - ]:           2 :             deleteAction->setEnabled(!special);
     212         [ +  + ]:           2 :             if (!special) {
     213         [ +  - ]:           1 :               auto f = deleteAction->font();
     214                 :             :               // No red color — just disable for special folders
     215                 :           1 :             }
     216                 :             : 
     217   [ +  -  +  -  :           2 :             menu.exec(viewport()->mapToGlobal(pos));
                   +  - ]
     218         [ +  + ]:           3 :           });
     219                 :          91 : }
     220                 :             : 
     221                 :             : // T-289: Check if a folder is a special system folder that should not be
     222                 :             : // deleted, renamed, or moved. Checks both IMAP flags and path heuristics.
     223                 :          61 : bool FolderTree::isSpecialFolder(const QString &path,
     224                 :             :                                   const QStringList &flags) {
     225                 :             :   // INBOX is always special
     226         [ +  + ]:          61 :   if (path.compare(QStringLiteral("INBOX"), Qt::CaseInsensitive) == 0)
     227                 :          13 :     return true;
     228                 :             : 
     229                 :             :   // Check IMAP special-use flags (RFC 6154)
     230                 :             :   static const QStringList specialFlags = {
     231                 :           7 :       QStringLiteral("\\Sent"), QStringLiteral("\\Drafts"),
     232                 :           7 :       QStringLiteral("\\Trash"), QStringLiteral("\\Archive"),
     233                 :           7 :       QStringLiteral("\\Junk"), QStringLiteral("\\All"),
     234   [ +  +  +  -  :         111 :       QStringLiteral("\\Flagged")};
          +  +  -  -  -  
                      - ]
     235         [ +  + ]:          52 :   for (const auto &flag : flags) {
     236         [ +  + ]:          81 :     for (const auto &sf : specialFlags) {
     237         [ +  + ]:          77 :       if (flag.compare(sf, Qt::CaseInsensitive) == 0)
     238                 :          20 :         return true;
     239                 :             :     }
     240                 :             :   }
     241                 :             : 
     242                 :          28 :   return false;
     243   [ +  -  -  -  :          56 : }
                   -  - ]
     244                 :             : 
     245                 :          47 : void FolderTree::setFolders(const QList<FolderInfo> &folders) {
     246                 :          47 :   m_searchItem = nullptr;
     247         [ +  - ]:          47 :   m_model->clear();
     248                 :          47 :   m_pathCache.clear(); // T-525: Reset path cache
     249                 :             : 
     250                 :             :   // T-069/T-078: Filter out hidden folders AND their children
     251   [ +  -  +  -  :          47 :   QSet<QString> hiddenSet(m_hiddenFolders.begin(), m_hiddenFolders.end());
                   +  - ]
     252                 :          47 :   QList<FolderInfo> visible;
     253         [ +  + ]:         177 :   for (const auto &f : folders) {
     254                 :         130 :     bool isHidden = hiddenSet.contains(f.path);
     255                 :             :     // T-078: Also hide children of hidden folders (prefix match)
     256         [ +  + ]:         130 :     if (!isHidden) {
     257   [ +  -  +  -  :         130 :       for (const auto &h : m_hiddenFolders) {
                   +  + ]
     258   [ -  +  -  - ]:           4 :         QString delimiter = f.delimiter.isEmpty() ? "." : f.delimiter;
     259   [ +  -  +  -  :           4 :         if (f.path.startsWith(h + delimiter)) {
                   -  + ]
     260                 :           0 :           isHidden = true;
     261                 :           0 :           break;
     262                 :             :         }
     263         [ +  - ]:           4 :       }
     264                 :             :     }
     265         [ +  + ]:         130 :     if (!isHidden)
     266         [ +  - ]:         126 :       visible.append(f);
     267                 :             :   }
     268                 :             : 
     269                 :             :   // T-078: Store hidden set for findOrCreateParent to check
     270                 :          47 :   m_hiddenSet = hiddenSet;
     271                 :             : 
     272                 :             :   // Sort folders: special folders first, then by path depth, then alphabetical
     273                 :          47 :   auto sorted = visible;
     274   [ +  -  +  -  :          47 :   std::sort(sorted.begin(), sorted.end(),
                   +  - ]
     275                 :         241 :             [](const FolderInfo &a, const FolderInfo &b) {
     276                 :             :               // INBOX always first
     277         [ +  + ]:         241 :               if (a.path == "INBOX")
     278                 :           1 :                 return true;
     279         [ +  + ]:         240 :               if (b.path == "INBOX")
     280                 :         113 :                 return false;
     281                 :             : 
     282                 :             :               // Special folders before regular ones
     283   [ +  -  +  -  :         251 :               auto isSpecialA = a.isSent() || a.isDrafts() || a.isTrash() ||
          +  -  +  -  +  
                      + ]
     284   [ +  +  +  -  :         251 :                                 a.isArchive() || a.isJunk();
          +  -  +  -  -  
                      + ]
     285   [ +  -  +  -  :         252 :               auto isSpecialB = b.isSent() || b.isDrafts() || b.isTrash() ||
          +  -  +  -  +  
                      + ]
     286   [ +  +  +  -  :         252 :                                 b.isArchive() || b.isJunk();
          +  -  +  -  -  
                      + ]
     287   [ +  +  +  + ]:         127 :               if (isSpecialA && !isSpecialB)
     288                 :           3 :                 return true;
     289   [ +  +  +  + ]:         124 :               if (!isSpecialA && isSpecialB)
     290                 :           3 :                 return false;
     291                 :             : 
     292                 :             :               // T-064: Sort by path depth (shorter paths first = parents
     293                 :             :               // before children)
     294   [ -  +  -  - ]:         121 :               auto delimA = a.delimiter.isEmpty() ? "." : a.delimiter;
     295   [ -  +  -  - ]:         121 :               auto delimB = b.delimiter.isEmpty() ? "." : b.delimiter;
     296         [ +  - ]:         121 :               int depthA = a.path.count(delimA);
     297         [ +  - ]:         121 :               int depthB = b.path.count(delimB);
     298         [ +  + ]:         121 :               if (depthA != depthB)
     299                 :          32 :                 return depthA < depthB;
     300                 :             : 
     301                 :          89 :               return a.name.compare(b.name, Qt::CaseInsensitive) < 0;
     302                 :         121 :             });
     303                 :             : 
     304                 :             :   // Track whether we need a separator after special folders
     305                 :          47 :   bool lastWasSpecial = false;
     306                 :          47 :   bool separatorInserted = false;
     307                 :             : 
     308   [ +  -  +  -  :         173 :   for (const auto &folder : sorted) {
                   +  + ]
     309   [ +  -  +  + ]:         210 :     bool isSpecial = (folder.path == "INBOX") || folder.isSent() ||
     310   [ +  -  +  -  :          77 :                      folder.isDrafts() || folder.isTrash() ||
             +  -  +  + ]
     311   [ +  +  +  -  :         210 :                      folder.isArchive() || folder.isJunk();
          +  +  +  -  -  
                      + ]
     312                 :             : 
     313                 :             :     // Insert visible separator between special and regular folders (top-level)
     314   [ +  +  +  +  :         149 :     if (lastWasSpecial && !isSpecial && !separatorInserted &&
             +  +  +  + ]
     315   [ +  -  +  + ]:          23 :         !folder.path.contains(folder.delimiter)) {
     316   [ +  -  +  -  :          20 :       auto *separator = new QStandardItem();
             -  +  -  - ]
     317         [ +  - ]:          20 :       separator->setSelectable(false);
     318         [ +  - ]:          20 :       separator->setEnabled(false);
     319         [ +  - ]:          20 :       separator->setData(true, SeparatorRole);
     320         [ +  - ]:          20 :       separator->setSizeHint(QSize(0, 8));
     321                 :             :       // Thin horizontal line
     322         [ +  - ]:          40 :       separator->setData(QStringLiteral("────────────────────"),
     323                 :             :                          Qt::DisplayRole);
     324   [ +  -  +  -  :          60 :       separator->setForeground(QColor(ThemeManager::instance().color(
             +  -  +  - ]
     325                 :          60 :           QStringLiteral("@border_medium"))));
     326         [ +  - ]:          20 :       QFont sepFont;
     327         [ +  - ]:          20 :       sepFont.setPointSize(4);
     328         [ +  - ]:          20 :       separator->setFont(sepFont);
     329         [ +  - ]:          20 :       m_model->appendRow(separator);
     330                 :          20 :       separatorInserted = true;
     331                 :          20 :     }
     332                 :             : 
     333         [ +  - ]:         126 :     auto *parent = findOrCreateParent(folder.path, folder.delimiter);
     334                 :             : 
     335   [ +  -  +  -  :         126 :     auto *item = new QStandardItem(folder.name);
             -  +  -  - ]
     336         [ +  - ]:         126 :     item->setData(folder.path, FolderPathRole);
     337         [ +  - ]:         126 :     item->setData(folder.name, DisplayNameRole);
     338         [ +  - ]:         126 :     item->setData(folder.delimiter, DelimiterRole);
     339         [ +  - ]:         126 :     item->setData(folder.flags, FolderFlagsRole); // T-289
     340                 :             :     // T-126/67.B2: theme-tinted icon incl. custom icon/color settings
     341   [ +  -  +  - ]:         126 :     item->setIcon(themedItemIcon(folder.path, folder));
     342                 :             : 
     343                 :             :     // \Noselect folders are not selectable
     344   [ +  -  +  + ]:         126 :     if (folder.isNoselect()) {
     345         [ +  - ]:           1 :       item->setSelectable(false);
     346   [ +  -  +  - ]:           1 :       item->setForeground(Qt::gray);
     347                 :             :     }
     348                 :             : 
     349                 :             :     // Check if a placeholder already exists for this path (created by a child)
     350         [ +  - ]:         126 :     auto *existing = findItemByPath(folder.path);
     351         [ -  + ]:         126 :     if (existing) {
     352                 :             :       // Replace placeholder data with real folder data
     353         [ #  # ]:           0 :       existing->setText(folder.name);
     354         [ #  # ]:           0 :       existing->setData(folder.name, DisplayNameRole);
     355         [ #  # ]:           0 :       existing->setData(folder.delimiter, DelimiterRole);
     356         [ #  # ]:           0 :       existing->setData(folder.flags, FolderFlagsRole);
     357                 :             :       // T-126/67.B2: theme-tinted icon incl. custom icon/color settings
     358   [ #  #  #  # ]:           0 :       existing->setIcon(themedItemIcon(folder.path, folder));
     359   [ #  #  #  # ]:           0 :       if (!folder.isNoselect()) {
     360         [ #  # ]:           0 :         existing->setSelectable(true);
     361   [ #  #  #  #  :           0 :         existing->setForeground(QPalette().color(QPalette::Text));
             #  #  #  # ]
     362                 :             :       }
     363         [ +  + ]:         126 :     } else if (parent) {
     364         [ +  - ]:          21 :       parent->appendRow(item);
     365         [ +  - ]:          21 :       m_pathCache.insert(folder.path, item); // T-525
     366                 :             :     } else {
     367         [ +  - ]:         105 :       m_model->appendRow(item);
     368         [ +  - ]:         105 :       m_pathCache.insert(folder.path, item); // T-525
     369                 :             :     }
     370                 :             : 
     371   [ +  +  +  -  :         126 :     if (isSpecial && !folder.path.contains(folder.delimiter)) {
             +  -  +  + ]
     372                 :          52 :       lastWasSpecial = true;
     373                 :             :     }
     374                 :             :   }
     375                 :             : 
     376                 :             :   // T-546: Don't expandAll() here — the caller (restoreSessionFolder,
     377                 :             :   // refreshTreeWithBadges) is responsible for setting the expand state.
     378   [ +  -  +  -  :          94 :   qCInfo(lcFolderTree) << "Populated" << folders.size() << "folders";
          +  -  +  -  +  
                -  +  + ]
     379                 :          47 : }
     380                 :             : 
     381                 :         174 : void FolderTree::setUnreadCount(const QString &folderPath, int count) {
     382         [ +  - ]:         174 :   auto *item = findItemByPath(folderPath);
     383         [ +  + ]:         174 :   if (!item)
     384                 :          11 :     return;
     385                 :             : 
     386                 :             :   // T-064: Use stored display name instead of delimiter heuristic
     387   [ +  -  +  - ]:         163 :   QString baseName = item->data(DisplayNameRole).toString();
     388         [ +  + ]:         163 :   if (baseName.isEmpty()) {
     389                 :             :     // Fallback: extract from path using stored delimiter
     390   [ +  -  +  - ]:           1 :     QString delim = item->data(DelimiterRole).toString();
     391         [ -  + ]:           1 :     if (delim.isEmpty())
     392         [ #  # ]:           0 :       delim = ".";
     393         [ +  - ]:           1 :     baseName = folderPath.section(delim, -1);
     394         [ -  + ]:           1 :     if (baseName.isEmpty())
     395                 :           0 :       baseName = folderPath;
     396                 :           1 :   }
     397                 :             : 
     398                 :             :   // 67.B4: the count lives in UnreadCountRole (badge pill painted by
     399                 :             :   // FolderBadgeDelegate); the text stays the plain folder name.
     400         [ +  - ]:         163 :   item->setText(baseName);
     401   [ +  +  +  - ]:         163 :   item->setData(count > 0 ? count : QVariant(), UnreadCountRole);
     402         [ +  - ]:         163 :   QFont f = item->font();
     403         [ +  - ]:         163 :   f.setBold(count > 0);
     404         [ +  - ]:         163 :   item->setFont(f);
     405                 :         163 : }
     406                 :             : 
     407                 :             : // ═══════════════════════════════════════════════════════
     408                 :             : // T-197: Virtual search node
     409                 :             : // ═══════════════════════════════════════════════════════
     410                 :             : 
     411                 :          45 : void FolderTree::showSearchNode(int resultCount) {
     412         [ +  + ]:          45 :   if (m_searchItem) {
     413         [ +  - ]:          19 :     updateSearchCount(resultCount);
     414                 :          19 :     return;
     415                 :             :   }
     416                 :             : 
     417   [ +  -  +  -  :          26 :   m_searchItem = new QStandardItem();
             -  +  -  - ]
     418   [ +  -  +  - ]:          26 :   m_searchItem->setData(QLatin1String(SearchNodePath), FolderPathRole);
     419         [ +  - ]:          52 :   m_searchItem->setData(QStringLiteral("Suche"), DisplayNameRole);
     420   [ +  -  +  -  :          52 :   m_searchItem->setIcon(MdiIconProvider::instance().icon(
                   +  - ]
     421   [ +  -  +  -  :          78 :       QStringLiteral("magnify"), 20, QColor(ThemeManager::instance().color("@text_secondary"))));
                   +  - ]
     422         [ +  - ]:          26 :   m_searchItem->setDropEnabled(false);
     423         [ +  - ]:          26 :   m_searchItem->setDragEnabled(false);
     424                 :             : 
     425         [ +  - ]:          26 :   QFont f = m_searchItem->font();
     426         [ +  - ]:          26 :   f.setBold(true);
     427         [ +  - ]:          26 :   m_searchItem->setFont(f);
     428                 :             : 
     429         [ +  - ]:          26 :   updateSearchCount(resultCount);
     430                 :             : 
     431                 :             :   // Insert at row 0 (above everything)
     432         [ +  - ]:          26 :   m_model->insertRow(0, m_searchItem);
     433                 :             : 
     434                 :             :   // Select the search node. Block selection signals so this programmatic
     435                 :             :   // selection does not emit searchNodeSelected (which would re-trigger the
     436                 :             :   // search). Only an actual user click on the node should restore the search.
     437         [ +  - ]:          26 :   auto idx = m_model->indexFromItem(m_searchItem);
     438                 :             :   {
     439         [ +  - ]:          26 :     QSignalBlocker block(selectionModel());
     440         [ +  - ]:          26 :     setCurrentIndex(idx);
     441                 :          26 :   }
     442         [ +  - ]:          26 :   scrollToTop();
     443                 :             : 
     444   [ +  -  +  -  :          52 :   qCInfo(lcFolderTree) << "Search node shown with" << resultCount << "results";
          +  -  +  -  +  
                -  +  + ]
     445                 :          26 : }
     446                 :             : 
     447                 :          53 : void FolderTree::updateSearchCount(int count) {
     448         [ +  + ]:          53 :   if (!m_searchItem)
     449                 :           2 :     return;
     450         [ +  + ]:          51 :   if (count > 0) {
     451   [ +  -  +  - ]:         150 :     m_searchItem->setText(QStringLiteral("Suche  %1").arg(count));
     452                 :             :   } else {
     453         [ +  - ]:           2 :     m_searchItem->setText(QStringLiteral("Suche"));
     454                 :             :   }
     455                 :             : }
     456                 :             : 
     457                 :          24 : void FolderTree::hideSearchNode() {
     458         [ +  + ]:          24 :   if (!m_searchItem)
     459                 :           3 :     return;
     460                 :             : 
     461         [ +  - ]:          21 :   auto idx = m_model->indexFromItem(m_searchItem);
     462         [ +  - ]:          21 :   if (idx.isValid()) {
     463         [ +  - ]:          21 :     m_model->removeRow(idx.row());
     464                 :             :   }
     465                 :          21 :   m_searchItem = nullptr;
     466                 :             : 
     467   [ +  -  +  -  :          42 :   qCInfo(lcFolderTree) << "Search node hidden";
             +  -  +  + ]
     468                 :             : }
     469                 :             : 
     470                 :           8 : bool FolderTree::isSearchNodeSelected() const {
     471         [ +  - ]:           8 :   auto idx = currentIndex();
     472   [ +  +  +  - ]:          14 :   return idx.isValid() &&
     473   [ +  -  +  -  :          14 :          idx.data(FolderPathRole).toString() == QLatin1String(SearchNodePath);
          +  +  +  +  -  
                -  -  - ]
     474                 :             : }
     475                 :             : 
     476                 :           5 : void FolderTree::clear() {
     477                 :           5 :   m_searchItem = nullptr;
     478                 :           5 :   m_model->clear();
     479                 :           5 :   m_pathCache.clear(); // T-525
     480                 :           5 : }
     481                 :             : 
     482                 :         135 : void FolderTree::selectFolder(const QString &folderPath) {
     483                 :         135 :   auto *item = findItemByPath(folderPath);
     484         [ +  + ]:         135 :   if (item) {
     485         [ +  - ]:         128 :     auto idx = m_model->indexFromItem(item);
     486         [ +  - ]:         128 :     setCurrentIndex(idx);
     487         [ +  - ]:         128 :     scrollTo(idx);
     488   [ +  -  +  -  :         256 :     qCInfo(lcFolderTree) << "Restored folder selection:" << folderPath;
          +  -  +  -  +  
                      + ]
     489                 :             :   } else {
     490   [ +  -  +  -  :          14 :     qCInfo(lcFolderTree) << "Could not find folder for restore:" << folderPath;
          +  -  +  -  +  
                      + ]
     491                 :             :   }
     492                 :         135 : }
     493                 :             : 
     494                 :          22 : QString FolderTree::selectedFolderPath() const {
     495         [ +  - ]:          22 :   auto idx = currentIndex();
     496         [ +  + ]:          22 :   if (idx.isValid()) {
     497   [ +  -  +  - ]:          14 :     return idx.data(FolderPathRole).toString();
     498                 :             :   }
     499                 :           8 :   return {};
     500                 :             : }
     501                 :             : 
     502                 :          16 : QStringList FolderTree::expandedFolderPaths() const {
     503                 :          16 :   QStringList paths;
     504                 :          16 :   std::function<void(const QModelIndex &)> collectExpanded;
     505                 :           0 :   collectExpanded = [&](const QModelIndex &parent) {
     506                 :          89 :     int rows = m_model->rowCount(parent);
     507         [ +  + ]:         162 :     for (int i = 0; i < rows; ++i) {
     508         [ +  - ]:          73 :       auto idx = m_model->index(i, 0, parent);
     509   [ +  -  +  + ]:          73 :       if (isExpanded(idx)) {
     510   [ +  -  +  - ]:          44 :         auto path = idx.data(FolderPathRole).toString();
     511         [ +  + ]:          44 :         if (!path.isEmpty()) {
     512         [ +  - ]:          42 :           paths.append(path);
     513                 :             :         }
     514                 :          44 :       }
     515         [ +  - ]:          73 :       collectExpanded(idx);
     516                 :             :     }
     517         [ +  - ]:         105 :   };
     518         [ +  - ]:          16 :   collectExpanded(QModelIndex());
     519                 :          16 :   return paths;
     520                 :          16 : }
     521                 :             : 
     522                 :           8 : void FolderTree::restoreExpandedFolders(const QStringList &paths) {
     523                 :             :   // Collapse all first, then expand only the saved ones
     524                 :           8 :   collapseAll();
     525         [ +  + ]:          37 :   for (const auto &path : paths) {
     526         [ +  - ]:          29 :     auto *item = findItemByPath(path);
     527         [ +  - ]:          29 :     if (item) {
     528   [ +  -  +  - ]:          29 :       expand(m_model->indexFromItem(item));
     529                 :             :     }
     530                 :             :   }
     531   [ +  -  +  -  :          16 :   qCInfo(lcFolderTree) << "Restored" << paths.size() << "expanded folders";
          +  -  +  -  +  
                -  +  + ]
     532                 :           8 : }
     533                 :             : 
     534                 :         130 : QStandardItem *FolderTree::findOrCreateParent(const QString &path,
     535                 :             :                                                const QString &delimiter) {
     536   [ +  -  +  -  :         130 :   if (delimiter.isEmpty() || !path.contains(delimiter)) {
             +  +  +  + ]
     537                 :         108 :     return nullptr; // Top-level item
     538                 :             :   }
     539                 :             : 
     540                 :             :   // path = "Arbeit/UCC/Projekte" → parentPath = "Arbeit/UCC"
     541         [ +  - ]:          22 :   auto parentPath = path.section(delimiter, 0, -2);
     542         [ -  + ]:          22 :   if (parentPath.isEmpty()) {
     543                 :           0 :     return nullptr;
     544                 :             :   }
     545                 :             : 
     546                 :             :   // Try to find existing parent
     547         [ +  - ]:          22 :   auto *existing = findItemByPath(parentPath);
     548         [ +  + ]:          22 :   if (existing) {
     549                 :          18 :     return existing;
     550                 :             :   }
     551                 :             : 
     552                 :             :   // T-078: Don't create placeholders for hidden folders
     553         [ -  + ]:           4 :   if (m_hiddenSet.contains(parentPath)) {
     554                 :           0 :     return nullptr;
     555                 :             :   }
     556                 :             : 
     557                 :             :   // T-064: Parent doesn't exist → create it recursively
     558                 :             :   // First ensure the grandparent exists
     559         [ +  - ]:           4 :   auto *grandParent = findOrCreateParent(parentPath, delimiter);
     560                 :             : 
     561                 :             :   // Create placeholder node for the missing parent
     562         [ +  - ]:           4 :   auto parentName = parentPath.section(delimiter, -1);
     563   [ +  -  +  -  :           4 :   auto *placeholder = new QStandardItem(parentName);
             -  +  -  - ]
     564         [ +  - ]:           4 :   placeholder->setData(parentPath, FolderPathRole);
     565         [ +  - ]:           4 :   placeholder->setData(parentName, DisplayNameRole);
     566         [ +  - ]:           4 :   placeholder->setData(delimiter, DelimiterRole);
     567   [ +  -  +  -  :           8 :   placeholder->setIcon(MdiIconProvider::instance().icon(
                   +  - ]
     568         [ +  - ]:          12 :       QStringLiteral("folder-outline"), 20, themedIconColor()));
     569         [ +  - ]:           4 :   placeholder->setSelectable(false);
     570   [ +  -  +  - ]:           4 :   placeholder->setForeground(Qt::gray);
     571                 :             : 
     572         [ +  + ]:           4 :   if (grandParent) {
     573         [ +  - ]:           1 :     grandParent->appendRow(placeholder);
     574                 :             :   } else {
     575         [ +  - ]:           3 :     m_model->appendRow(placeholder);
     576                 :             :   }
     577         [ +  - ]:           4 :   m_pathCache.insert(parentPath, placeholder); // T-525
     578                 :             : 
     579   [ +  -  +  -  :           8 :   qCInfo(lcFolderTree) << "Created intermediate node:" << parentPath;
          +  -  +  -  +  
                      + ]
     580                 :           4 :   return placeholder;
     581                 :          22 : }
     582                 :             : 
     583                 :             : // 67.B2: One resolution point for an item's icon — special/heuristic MDI
     584                 :             : // icon, optional custom icon and custom tint from QSettings — so both
     585                 :             : // setFolders() and refreshFolderIcons() produce identical results.
     586                 :         184 : QIcon FolderTree::themedItemIcon(const QString &path,
     587                 :             :                                  const FolderInfo &folder) {
     588         [ +  - ]:         184 :   QIcon icon = iconForFolder(folder);
     589         [ +  - ]:         184 :   QSettings s;
     590   [ +  -  +  -  :         184 :   const QString customIcon = s.value("folder/icon/" + path).toString();
                   +  - ]
     591         [ +  + ]:         184 :   if (!customIcon.isEmpty()) {
     592                 :             :     // T-199b: Use MdiIconProvider (font-based, 7448 icons)
     593                 :             :     QIcon mdiIcon =
     594   [ +  -  +  -  :           3 :         MdiIconProvider::instance().icon(customIcon, 20, themedIconColor());
                   +  - ]
     595         [ +  - ]:           3 :     icon = mdiIcon.isNull()
     596   [ +  -  +  -  :           9 :                ? QIcon::fromTheme(customIcon,
                   -  - ]
     597   [ +  -  +  -  :           6 :                                   QIcon::fromTheme(QStringLiteral("folder")))
          +  -  +  -  -  
             -  -  -  -  
                      - ]
     598                 :           3 :                : mdiIcon;
     599                 :           3 :   }
     600   [ +  -  +  -  :         184 :   const QString colorStr = s.value("folder/color/" + path).toString();
                   +  - ]
     601         [ +  + ]:         184 :   if (!colorStr.isEmpty())
     602         [ +  - ]:           3 :     icon = tintedIcon(icon, QColor(colorStr));
     603                 :         184 :   return icon;
     604                 :         184 : }
     605                 :             : 
     606                 :          11 : void FolderTree::refreshFolderIcons() {
     607         [ +  + ]:          69 :   for (auto it = m_pathCache.constBegin(); it != m_pathCache.constEnd();
     608                 :          58 :        ++it) {
     609                 :          58 :     QStandardItem *item = it.value();
     610         [ -  + ]:          58 :     if (!item)
     611                 :           0 :       continue;
     612                 :          58 :     FolderInfo folder;
     613                 :          58 :     folder.path = it.key();
     614   [ +  -  +  - ]:          58 :     folder.name = item->data(DisplayNameRole).toString();
     615   [ +  -  +  - ]:          58 :     folder.delimiter = item->data(DelimiterRole).toString();
     616   [ +  -  +  - ]:          58 :     folder.flags = item->data(FolderFlagsRole).toStringList();
     617   [ +  -  +  - ]:          58 :     item->setIcon(themedItemIcon(folder.path, folder));
     618                 :          58 :   }
     619         [ -  + ]:          11 :   if (m_searchItem) {
     620   [ #  #  #  #  :           0 :     m_searchItem->setIcon(MdiIconProvider::instance().icon(
                   #  # ]
     621         [ #  # ]:           0 :         QStringLiteral("magnify"), 20, themedIconColor()));
     622                 :             :   }
     623                 :          11 : }
     624                 :             : 
     625                 :         184 : QIcon FolderTree::iconForFolder(const FolderInfo &folder) {
     626         [ +  - ]:         184 :   auto &mdi = MdiIconProvider::instance();
     627         [ +  - ]:         184 :   const QColor color = themedIconColor();
     628                 :             : 
     629                 :             :   // Special folders (detected by IMAP flags)
     630         [ +  + ]:         184 :   if (folder.path == "INBOX")
     631         [ +  - ]:          52 :     return mdi.icon(QStringLiteral("inbox"), 20, color);
     632   [ +  -  +  + ]:         132 :   if (folder.isSent())
     633         [ +  - ]:           7 :     return mdi.icon(QStringLiteral("send"), 20, color);
     634   [ +  -  -  + ]:         125 :   if (folder.isDrafts())
     635         [ #  # ]:           0 :     return mdi.icon(QStringLiteral("file-edit-outline"), 20, color);
     636   [ +  -  +  + ]:         125 :   if (folder.isTrash())
     637         [ +  - ]:           2 :     return mdi.icon(QStringLiteral("trash-can-outline"), 20, color);
     638   [ +  -  +  + ]:         123 :   if (folder.isArchive())
     639         [ +  - ]:           1 :     return mdi.icon(QStringLiteral("archive-outline"), 20, color);
     640   [ +  -  -  + ]:         122 :   if (folder.isJunk())
     641         [ #  # ]:           0 :     return mdi.icon(QStringLiteral("alert-octagon-outline"), 20, color);
     642                 :             : 
     643                 :             :   // T-125: Name-based heuristics for common folder names
     644         [ +  - ]:         122 :   QString name = folder.path.contains('.')
     645         [ +  + ]:         156 :       ? folder.path.section('.', -1)
     646   [ +  -  +  - ]:         156 :       : folder.path.section('/', -1);
     647                 :             : 
     648                 :             :   // MDI icon names — always available via font rendering
     649                 :             :   static const QHash<QString, QString> s_nameIcons = {
     650                 :             :       {"notifications",       "bell-outline"},
     651                 :             :       {"benachrichtigungen",  "bell-outline"},
     652                 :             :       {"newsletter",          "newspaper-variant-outline"},
     653                 :             :       {"newsletters",         "newspaper-variant-outline"},
     654                 :             :       {"work",                "briefcase-outline"},
     655                 :             :       {"arbeit",              "briefcase-outline"},
     656                 :             :       {"important",           "star-outline"},
     657                 :             :       {"wichtig",             "star-outline"},
     658                 :             :       {"lists",               "format-list-bulleted"},
     659                 :             :       {"mailinglisten",       "format-list-bulleted"},
     660                 :             :       {"mailing-listen",      "format-list-bulleted"},
     661                 :             :       {"receipts",            "receipt-text-outline"},
     662                 :             :       {"rechnungen",          "receipt-text-outline"},
     663                 :             :       {"invoices",            "receipt-text-outline"},
     664                 :             :       {"travel",              "airplane"},
     665                 :             :       {"reisen",              "airplane"},
     666                 :             :       // Additional heuristics
     667                 :             :       {"server",              "server"},
     668                 :             :       {"monitoring",          "monitor-eye"},
     669                 :             :       {"backup",              "cloud-upload-outline"},
     670                 :             :       {"git",                 "source-branch"},
     671                 :             :       {"pipelines",           "pipe"},
     672                 :             :       {"geld",                "currency-eur"},
     673                 :             :       {"money",               "currency-eur"},
     674                 :             :       {"steuern",             "calculator"},
     675                 :             :       {"bitcoin",             "bitcoin"},
     676                 :             :       {"versicherungen",      "shield-check-outline"},
     677                 :             :       {"privat",              "account-outline"},
     678                 :             :       {"personal",            "account-outline"},
     679                 :             :       {"bestellungen",        "cart-outline"},
     680                 :             :       {"orders",              "cart-outline"},
     681                 :             :       {"uni",                 "school-outline"},
     682                 :             :       {"university",          "school-outline"},
     683                 :             :       {"schule",              "school-outline"},
     684                 :             :       {"games",               "gamepad-variant-outline"},
     685                 :             :       {"tracking",            "map-marker-path"},
     686                 :             :       {"startups",            "rocket-launch-outline"},
     687                 :             :       {"onlinekurse",         "book-open-page-variant-outline"},
     688                 :             :       {"spam",                "alert-octagon-outline"},
     689                 :             :       {"spam2",               "alert-octagon-outline"},
     690   [ +  +  +  -  :         402 :   };
          +  +  -  -  -  
                      - ]
     691                 :             : 
     692         [ +  - ]:         122 :   auto it = s_nameIcons.find(name.toLower());
     693         [ +  + ]:         122 :   if (it != s_nameIcons.end()) {
     694         [ +  - ]:          31 :     return mdi.icon(*it, 20, color);
     695                 :             :   }
     696                 :             : 
     697         [ +  - ]:          91 :   return mdi.icon(QStringLiteral("folder-outline"), 20, color);
     698   [ +  -  +  -  :         129 : }
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
          -  +  -  +  -  
          +  -  +  -  +  
             -  -  -  -  
                      - ]
     699                 :             : 
     700                 :         486 : QStandardItem *FolderTree::findItemByPath(const QString &folderPath) const {
     701                 :             :   // T-525: O(1) cache lookup (replaces O(N) recursive tree search)
     702                 :         486 :   auto it = m_pathCache.constFind(folderPath);
     703         [ +  + ]:         486 :   if (it != m_pathCache.constEnd())
     704                 :         338 :     return *it;
     705                 :             :   // Fallback to recursive search for items not yet in cache
     706         [ +  - ]:         148 :   return findItemByPathRecursive(nullptr, folderPath);
     707                 :             : }
     708                 :             : 
     709                 :         497 : QStandardItem *FolderTree::findItemByPathRecursive(QStandardItem *root,
     710                 :             :                                                    const QString &path) const {
     711   [ +  +  +  -  :         497 :   int rows = root ? root->rowCount() : m_model->rowCount();
                   +  - ]
     712         [ +  + ]:         846 :   for (int i = 0; i < rows; ++i) {
     713         [ +  + ]:         349 :     auto *child = root ? root->child(i) : m_model->item(i);
     714   [ +  -  +  -  :         349 :     if (child && child->data(FolderPathRole).toString() == path) {
          +  -  -  +  +  
          -  +  -  -  +  
             -  -  -  - ]
     715                 :           0 :       return child;
     716                 :             :     }
     717                 :         349 :     auto *found = findItemByPathRecursive(child, path);
     718         [ -  + ]:         349 :     if (found)
     719                 :           0 :       return found;
     720                 :             :   }
     721                 :         497 :   return nullptr;
     722                 :             : }
     723                 :             : 
     724                 :             : // ═══════════════════════════════════════════════════════
     725                 :             : // Drop Target (T-103) — visual-only highlight, no folder switch
     726                 :             : // ═══════════════════════════════════════════════════════
     727                 :             : 
     728                 :           4 : void FolderTree::dragEnterEvent(QDragEnterEvent *event) {
     729                 :           4 :   if (event->mimeData()->hasFormat(
     730   [ +  -  +  +  :          14 :           QStringLiteral("application/x-mailjd-mail-ids")) ||
          +  -  +  +  -  
                -  -  - ]
     731   [ +  -  +  + ]:           4 :       event->mimeData()->hasFormat(
     732   [ +  +  +  +  :           6 :           QStringLiteral("application/x-mailjd-uids"))) {
          +  -  -  -  -  
                      - ]
     733                 :             :     // Save current selection so we can restore it after drop/cancel
     734   [ +  -  +  - ]:           3 :     m_preDragIndex = currentIndex();
     735                 :           3 :     event->acceptProposedAction();
     736                 :             :   } else {
     737                 :           1 :     event->ignore();
     738                 :             :   }
     739                 :           4 : }
     740                 :             : 
     741                 :           4 : void FolderTree::dragMoveEvent(QDragMoveEvent *event) {
     742         [ +  - ]:           4 :   auto idx = indexAt(event->position().toPoint());
     743         [ +  + ]:           4 :   if (!idx.isValid()) {
     744                 :           2 :     event->ignore();
     745         [ +  - ]:           2 :     m_dragHighlightIndex = QPersistentModelIndex();
     746   [ +  -  +  - ]:           2 :     viewport()->update();
     747                 :           2 :     return;
     748                 :             :   }
     749                 :             : 
     750   [ +  -  +  - ]:           2 :   auto path = idx.data(FolderPathRole).toString();
     751         [ -  + ]:           2 :   if (path.isEmpty()) {
     752                 :             :     // Separator or non-folder item
     753                 :           0 :     event->ignore();
     754         [ #  # ]:           0 :     m_dragHighlightIndex = QPersistentModelIndex();
     755   [ #  #  #  # ]:           0 :     viewport()->update();
     756                 :           0 :     return;
     757                 :             :   }
     758                 :             : 
     759                 :             :   // Accept the drop on this folder — visual highlight only, NO setCurrentIndex
     760                 :           2 :   event->acceptProposedAction();
     761         [ +  - ]:           2 :   if (m_dragHighlightIndex != idx) {
     762         [ +  - ]:           2 :     m_dragHighlightIndex = QPersistentModelIndex(idx);
     763   [ +  -  +  - ]:           2 :     viewport()->update();
     764                 :             :   }
     765         [ +  - ]:           2 : }
     766                 :             : 
     767                 :           2 : void FolderTree::dropEvent(QDropEvent *event) {
     768         [ +  - ]:           2 :   auto idx = indexAt(event->position().toPoint());
     769                 :             : 
     770                 :             :   // Clear drag highlight
     771         [ +  - ]:           2 :   m_dragHighlightIndex = QPersistentModelIndex();
     772   [ +  -  +  - ]:           2 :   viewport()->update();
     773                 :             : 
     774         [ -  + ]:           2 :   if (!idx.isValid()) {
     775                 :           0 :     event->ignore();
     776                 :           1 :     return;
     777                 :             :   }
     778                 :             : 
     779   [ +  -  +  - ]:           2 :   auto targetFolder = idx.data(FolderPathRole).toString();
     780         [ -  + ]:           2 :   if (targetFolder.isEmpty()) {
     781                 :           0 :     event->ignore();
     782                 :           0 :     return;
     783                 :             :   }
     784                 :             : 
     785         [ +  - ]:           4 :   if (event->mimeData()->hasFormat(
     786         [ +  + ]:           4 :           QStringLiteral("application/x-mailjd-mail-ids"))) {
     787                 :             :     auto encoded = event->mimeData()->data(
     788         [ +  - ]:           2 :         QStringLiteral("application/x-mailjd-mail-ids"));
     789         [ +  - ]:           1 :     QDataStream stream(&encoded, QIODevice::ReadOnly);
     790                 :           1 :     QList<MailIdentity> mails;
     791   [ +  -  +  + ]:           3 :     while (!stream.atEnd()) {
     792                 :           2 :       MailIdentity mail;
     793   [ +  -  +  -  :           2 :       stream >> mail.account >> mail.folderId >> mail.uid;
                   +  - ]
     794         [ +  - ]:           2 :       if (mail.isValid())
     795         [ +  - ]:           2 :         mails.append(mail);
     796                 :           2 :     }
     797                 :             : 
     798         [ +  - ]:           1 :     if (!mails.isEmpty()) {
     799   [ +  -  +  -  :           2 :       qCInfo(lcFolderTree) << "Drop:" << mails.size() << "mail(s) onto"
          +  -  +  -  +  
                -  +  + ]
     800         [ +  - ]:           1 :                            << targetFolder;
     801         [ +  - ]:           1 :       emit moveMailIdsRequested(mails, targetFolder);
     802                 :           1 :       event->acceptProposedAction();
     803                 :             :     } else {
     804                 :           0 :       event->ignore();
     805                 :             :     }
     806                 :           1 :     return;
     807                 :           1 :   }
     808                 :             : 
     809                 :             :   auto encoded =
     810         [ +  - ]:           2 :       event->mimeData()->data(QStringLiteral("application/x-mailjd-uids"));
     811         [ +  - ]:           1 :   QDataStream stream(&encoded, QIODevice::ReadOnly);
     812                 :           1 :   QList<qint64> uids;
     813   [ +  -  +  + ]:           3 :   while (!stream.atEnd()) {
     814                 :             :     qint64 uid;
     815         [ +  - ]:           2 :     stream >> uid;
     816         [ +  - ]:           2 :     uids.append(uid);
     817                 :             :   }
     818                 :             : 
     819         [ +  - ]:           1 :   if (!uids.isEmpty()) {
     820   [ +  -  +  -  :           2 :     qCInfo(lcFolderTree) << "Drop:" << uids.size() << "mail(s) onto"
          +  -  +  -  +  
                -  +  + ]
     821         [ +  - ]:           1 :                          << targetFolder;
     822         [ +  - ]:           1 :     emit moveRequested(uids, targetFolder);
     823                 :           1 :     event->acceptProposedAction();
     824                 :             :   } else {
     825                 :           0 :     event->ignore();
     826                 :             :   }
     827                 :             : 
     828                 :             :   // Restore original folder selection (not the drag target)
     829   [ +  -  -  + ]:           1 :   if (m_preDragIndex.isValid()) {
     830   [ #  #  #  # ]:           0 :     setCurrentIndex(m_preDragIndex);
     831         [ #  # ]:           0 :     m_preDragIndex = QPersistentModelIndex();
     832                 :             :   }
     833         [ +  + ]:           2 : }
     834                 :             : 
     835                 :           1 : void FolderTree::dragLeaveEvent(QDragLeaveEvent *event) {
     836         [ +  - ]:           1 :   m_dragHighlightIndex = QPersistentModelIndex();
     837                 :           1 :   viewport()->update();
     838                 :           1 :   QTreeView::dragLeaveEvent(event);
     839                 :           1 : }
     840                 :             : 
     841                 :         577 : void FolderTree::paintEvent(QPaintEvent *event) {
     842                 :             :   // Draw the tree normally
     843                 :         577 :   QTreeView::paintEvent(event);
     844                 :             : 
     845                 :             :   // Overlay a drag highlight rect if active
     846         [ +  + ]:         577 :   if (m_dragHighlightIndex.isValid()) {
     847   [ +  -  +  - ]:           1 :     auto rect = visualRect(m_dragHighlightIndex);
     848         [ +  - ]:           1 :     if (rect.isValid()) {
     849   [ +  -  +  -  :           1 :       QPainter painter(viewport());
                   +  - ]
     850         [ +  - ]:           1 :       painter.setRenderHint(QPainter::Antialiasing, false);
     851   [ +  -  +  - ]:           1 :       QColor highlight = palette().highlight().color();
     852         [ +  - ]:           1 :       highlight.setAlpha(50);
     853         [ +  - ]:           1 :       painter.fillRect(rect, highlight);
     854   [ +  -  +  -  :           1 :       painter.setPen(QPen(palette().highlight().color(), 1));
          +  -  +  -  +  
                      - ]
     855         [ +  - ]:           1 :       painter.drawRect(rect.adjusted(0, 0, -1, -1));
     856                 :           1 :     }
     857                 :             :   }
     858                 :         577 : }
     859                 :             : 
     860                 :             : // T-126: Tint an icon with a solid color (preserves alpha/shape)
     861                 :           3 : QIcon FolderTree::tintedIcon(const QIcon &icon, const QColor &color) {
     862         [ +  - ]:           3 :   QPixmap pix = icon.pixmap(24, 24);
     863         [ +  - ]:           3 :   QPainter p(&pix);
     864         [ +  - ]:           3 :   p.setCompositionMode(QPainter::CompositionMode_SourceIn);
     865   [ +  -  +  - ]:           3 :   p.fillRect(pix.rect(), color);
     866         [ +  - ]:           3 :   p.end();
     867         [ +  - ]:           6 :   return QIcon(pix);
     868                 :           3 : }
        

Generated by: LCOV version 2.0-1