MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - MailThreadModel.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 91.3 % 298 272
Test Date: 2026-07-27 17:53:44 Functions: 100.0 % 30 30
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 61.2 % 397 243

             Branch data     Line data    Source code
       1                 :             : #include "MailThreadModel.h"
       2                 :             : 
       3                 :             : #include <QColor>
       4                 :             : #include <QDataStream>
       5                 :             : #include <QFont>
       6                 :             : #include <QIODevice>
       7                 :             : #include <QRegularExpression>
       8                 :             : 
       9                 :             : #include "service/ImapResponseParser.h"
      10                 :             : #include "ui/MailListModel.h"
      11                 :             : #include "ui/ThemeManager.h"
      12                 :             : 
      13                 :         148 : MailThreadModel::MailThreadModel(QObject *parent)
      14                 :         148 :     : QAbstractItemModel(parent) {}
      15                 :             : 
      16                 :         258 : MailThreadModel::~MailThreadModel() = default;
      17                 :             : 
      18                 :          55 : void MailThreadModel::setHeaders(const QList<MailHeader> &headers) {
      19                 :          55 :   beginResetModel();
      20                 :          55 :   m_roots.clear();
      21                 :          55 :   m_uidIndex.clear();
      22                 :          55 :   m_suggestionCache.clear();
      23                 :          55 :   m_headers = headers;
      24         [ +  - ]:          55 :   m_roots = ThreadBuilder::buildThreads(m_headers);
      25                 :          55 :   buildUidIndex();
      26                 :          55 :   endResetModel();
      27                 :          55 : }
      28                 :             : 
      29                 :           2 : void MailThreadModel::appendHeaders(const QList<MailHeader> &headers) {
      30                 :           2 :   beginResetModel();
      31                 :           2 :   m_roots.clear();
      32                 :           2 :   m_uidIndex.clear();
      33                 :           2 :   m_headers.append(headers);
      34         [ +  - ]:           2 :   m_roots = ThreadBuilder::buildThreads(m_headers);
      35                 :           2 :   buildUidIndex();
      36                 :           2 :   endResetModel();
      37                 :           2 : }
      38                 :             : 
      39                 :           2 : void MailThreadModel::clear() {
      40                 :           2 :   beginResetModel();
      41                 :           2 :   m_roots.clear();
      42                 :           2 :   m_uidIndex.clear();
      43                 :           2 :   m_headers.clear();
      44                 :           2 :   m_suggestionCache.clear();
      45                 :           2 :   endResetModel();
      46                 :           2 : }
      47                 :             : 
      48                 :          36 : const MailHeader *MailThreadModel::headerAt(const QModelIndex &index) const {
      49                 :             :   // T-79.B1/H7: nodes own their headers, so this pointer stays valid for
      50                 :             :   // the lifetime of the node (until the next tree rebuild) regardless of
      51                 :             :   // what happens to any flat header list elsewhere.
      52                 :          36 :   auto *node = nodeFromIndex(index);
      53         [ +  + ]:          36 :   return node ? &node->header : nullptr;
      54                 :             : }
      55                 :             : 
      56                 :           2 : qint64 MailThreadModel::uidAt(const QModelIndex &index) const {
      57                 :           2 :   auto *node = nodeFromIndex(index);
      58         [ +  + ]:           2 :   return node ? node->header.uid : -1;
      59                 :             : }
      60                 :             : 
      61                 :          11 : QModelIndex MailThreadModel::indexForUid(qint64 uid, qint64 folderId) const {
      62                 :          11 :   auto *node = m_uidIndex.value(MailKey{folderId, uid}, nullptr);
      63         [ +  + ]:          11 :   return node ? indexFromNode(node) : QModelIndex();
      64                 :             : }
      65                 :             : 
      66                 :         141 : void MailThreadModel::updateFlags(qint64 uid, quint32 flags, qint64 folderId) {
      67                 :             :   // T-79.B1/H7: the node owns its header — update it directly via the
      68                 :             :   // (folderId, uid) index so the view reads the new value.
      69                 :         141 :   auto *node = m_uidIndex.value(MailKey{folderId, uid}, nullptr);
      70         [ +  + ]:         141 :   if (node) {
      71                 :           5 :     node->header.flags = flags;
      72         [ +  - ]:           5 :     auto topLeft = indexFromNode(node, 0);
      73         [ +  - ]:           5 :     auto bottomRight = indexFromNode(node, ColumnCount - 1);
      74         [ +  - ]:           5 :     emit dataChanged(topLeft, bottomRight);
      75                 :             :   }
      76                 :             :   // Keep the flat source list in sync so a later rebuild
      77                 :             :   // (appendHeaders/removeByUid) does not resurrect stale flags.
      78   [ +  -  +  -  :         161 :   for (auto &h : m_headers) {
                   +  + ]
      79   [ +  +  +  - ]:          25 :     if (h.uid == uid && h.folderId == folderId) {
      80                 :           5 :       h.flags = flags;
      81                 :           5 :       break;
      82                 :             :     }
      83                 :             :   }
      84                 :         141 : }
      85                 :             : 
      86                 :             : // T-261: Update labels for a specific UID
      87                 :          25 : void MailThreadModel::updateLabels(qint64 uid, const QStringList &labels, qint64 folderId) {
      88                 :          25 :   auto *node = m_uidIndex.value(MailKey{folderId, uid}, nullptr);
      89         [ +  + ]:          25 :   if (node) {
      90                 :           4 :     node->header.labels = labels;
      91         [ +  - ]:           4 :     auto topLeft = indexFromNode(node, 0);
      92         [ +  - ]:           4 :     auto bottomRight = indexFromNode(node, ColumnCount - 1);
      93         [ +  - ]:           4 :     emit dataChanged(topLeft, bottomRight);
      94                 :             :   }
      95   [ +  -  +  -  :          27 :   for (auto &h : m_headers) {
                   +  + ]
      96   [ +  +  +  - ]:           6 :     if (h.uid == uid && h.folderId == folderId) {
      97                 :           4 :       h.labels = labels;
      98                 :           4 :       break;
      99                 :             :     }
     100                 :             :   }
     101                 :          25 : }
     102                 :             : 
     103                 :          22 : void MailThreadModel::removeByUid(qint64 uid, qint64 folderId) {
     104                 :             :   // Remove the header from our flat list
     105         [ +  + ]:          25 :   for (int i = 0; i < m_headers.size(); ++i) {
     106   [ +  +  +  -  :           8 :     if (m_headers[i].uid == uid && m_headers[i].folderId == folderId) {
                   +  + ]
     107                 :             :       // Rebuild the entire thread tree (simple & correct)
     108                 :           5 :       beginResetModel();
     109                 :           5 :       m_headers.removeAt(i);
     110                 :           5 :       m_roots.clear();
     111                 :           5 :       m_uidIndex.clear();
     112         [ +  - ]:           5 :       m_roots = ThreadBuilder::buildThreads(m_headers);
     113                 :           5 :       buildUidIndex();
     114                 :           5 :       endResetModel();
     115                 :           5 :       return;
     116                 :             :     }
     117                 :             :   }
     118                 :             : }
     119                 :             : 
     120                 :             : // --- QAbstractItemModel interface ---
     121                 :             : 
     122                 :        7683 : QModelIndex MailThreadModel::index(int row, int column,
     123                 :             :                                     const QModelIndex &parent) const {
     124   [ +  -  +  -  :        7683 :   if (row < 0 || column < 0 || column >= ColumnCount)
                   -  + ]
     125                 :           0 :     return {};
     126                 :             : 
     127         [ +  + ]:        7683 :   if (!parent.isValid()) {
     128                 :             :     // Root level
     129         [ -  + ]:        6625 :     if (row >= static_cast<int>(m_roots.size()))
     130                 :           0 :       return {};
     131                 :        6625 :     return createIndex(row, column, m_roots[row].get());
     132                 :             :   }
     133                 :             : 
     134                 :             :   // Child level
     135                 :        1058 :   auto *parentNode = nodeFromIndex(parent);
     136   [ +  -  -  +  :        1058 :   if (!parentNode || row >= static_cast<int>(parentNode->children.size()))
                   -  + ]
     137                 :           0 :     return {};
     138                 :        1058 :   return createIndex(row, column, parentNode->children[row].get());
     139                 :             : }
     140                 :             : 
     141                 :         119 : QModelIndex MailThreadModel::parent(const QModelIndex &child) const {
     142         [ -  + ]:         119 :   if (!child.isValid())
     143                 :           0 :     return {};
     144                 :             : 
     145                 :         119 :   auto *node = nodeFromIndex(child);
     146   [ +  -  +  + ]:         119 :   if (!node || !node->parent)
     147                 :          71 :     return {};
     148                 :             : 
     149                 :          48 :   auto *parentNode = node->parent;
     150                 :             :   // Find the row of parentNode in its parent's children (or roots)
     151         [ +  - ]:          48 :   if (!parentNode->parent) {
     152                 :             :     // parentNode is a root
     153                 :          48 :     int row = findRootIndex(parentNode);
     154                 :          48 :     return createIndex(row, 0, parentNode);
     155                 :             :   }
     156                 :             : 
     157                 :           0 :   int row = findChildIndex(parentNode);
     158                 :           0 :   return createIndex(row, 0, parentNode);
     159                 :             : }
     160                 :             : 
     161                 :         165 : int MailThreadModel::rowCount(const QModelIndex &parent) const {
     162         [ +  + ]:         165 :   if (!parent.isValid())
     163                 :          77 :     return static_cast<int>(m_roots.size());
     164                 :             : 
     165                 :          88 :   auto *node = nodeFromIndex(parent);
     166                 :             :   // Only column 0 items have children
     167         [ -  + ]:          88 :   if (parent.column() != 0)
     168                 :           0 :     return 0;
     169         [ +  - ]:          88 :   return node ? static_cast<int>(node->children.size()) : 0;
     170                 :             : }
     171                 :             : 
     172                 :         106 : int MailThreadModel::columnCount(const QModelIndex &parent) const {
     173                 :             :   Q_UNUSED(parent)
     174                 :         106 :   return ColumnCount;
     175                 :             : }
     176                 :             : 
     177                 :        5662 : QVariant MailThreadModel::data(const QModelIndex &index, int role) const {
     178                 :        5662 :   auto *node = nodeFromIndex(index);
     179         [ -  + ]:        5662 :   if (!node)
     180                 :           0 :     return {};
     181                 :             : 
     182                 :        5662 :   const auto &h = node->header;
     183                 :        5662 :   int col = index.column();
     184                 :             : 
     185         [ +  + ]:        5662 :   if (role == Qt::DisplayRole) {
     186   [ +  +  +  +  :         824 :     switch (col) {
             +  +  +  - ]
     187                 :         118 :     case Star:
     188   [ +  +  +  +  :         236 :       return h.isFlagged() ? QStringLiteral("★") : QStringLiteral("☆");
                   +  + ]
     189                 :         117 :     case Attachment:
     190                 :         117 :       return {};  // painted by dedicated delegate
     191                 :         231 :     case Subject: {
     192   [ -  +  -  - ]:         231 :       QString subject = h.subject.isEmpty() ? tr("(No subject)") : h.subject;
     193                 :             :       // T-547: Show unseen count for root nodes with children
     194   [ +  +  +  +  :         231 :       if (node->depth == 0 && !node->children.empty()) {
                   +  + ]
     195         [ +  - ]:          28 :         int total = node->descendantCount();
     196         [ +  - ]:          28 :         int unseen = node->unseenDescendantCount();
     197         [ +  + ]:          28 :         if (unseen > 0)
     198   [ +  -  +  -  :          81 :           subject += QStringLiteral(" (%1/%2)").arg(unseen).arg(total);
                   +  - ]
     199                 :             :         else
     200   [ +  -  +  - ]:           2 :           subject += QStringLiteral(" (%1)").arg(total);
     201                 :             :       }
     202                 :             :       // T-128/T-136: Strip Re:/Fwd: for child messages
     203         [ +  + ]:         231 :       if (node->depth > 0) {
     204                 :             :         static const QRegularExpression rePrefix(
     205                 :           6 :             QStringLiteral("^(Re|Fwd|Aw|Wg):\\s*"),
     206   [ +  +  +  -  :          45 :             QRegularExpression::CaseInsensitiveOption);
             +  -  -  - ]
     207         [ +  - ]:          39 :         subject = subject.replace(rePrefix, QString());
     208                 :             :       }
     209                 :             :       // Show ↩ prefix for answered mails
     210   [ +  +  +  - ]:         232 :       if (h.isAnswered()) subject.prepend(QStringLiteral("↩ "));
     211                 :             :       // Bug 2: Visual indentation for threaded replies
     212         [ +  + ]:         231 :       if (node->depth > 0)
     213   [ +  -  +  - ]:          39 :         subject.prepend(QString(node->depth * 4, QChar(' ')));
     214                 :         231 :       return subject;
     215                 :         231 :     }
     216                 :         121 :     case From:
     217                 :         121 :       return h.from;
     218                 :         118 :     case Date:
     219                 :             :       // 67.B4: same humanized format as the flat list
     220         [ +  - ]:         118 :       return MailListModel::formatDate(h.date);
     221                 :         117 :     case Size:
     222         [ +  + ]:         117 :       if (h.size < 1024)
     223         [ +  - ]:         232 :         return QStringLiteral("%1 B").arg(h.size);
     224         [ +  - ]:           1 :       if (h.size < 1024 * 1024)
     225         [ +  - ]:           2 :         return QStringLiteral("%1 KB").arg(h.size / 1024.0, 0, 'f', 1);
     226         [ #  # ]:           0 :       return QStringLiteral("%1 MB").arg(h.size / (1024.0 * 1024.0), 0, 'f', 1);
     227                 :           2 :     case Suggestion: {
     228                 :           2 :       auto it = m_suggestionCache.constFind(MailKey{h.folderId, h.uid});
     229         [ +  + ]:           2 :       return it != m_suggestionCache.constEnd() ? it->text : QString();
     230                 :             :     }
     231                 :             :     }
     232                 :             :   }
     233                 :             : 
     234                 :             :   // T-508/T-547: Bold font for unread messages.
     235                 :             :   // For root nodes with children, also check if any descendant is unread
     236                 :             :   // so collapsed threads show as bold when they contain unread mail.
     237         [ +  + ]:        4838 :   if (role == Qt::FontRole) {
     238                 :         802 :     bool unread = !h.isSeen();
     239   [ +  +  +  -  :         802 :     if (!unread && node->depth == 0 && !node->children.empty())
             -  +  -  + ]
     240                 :           0 :       unread = node->hasUnseenDescendant();
     241         [ +  + ]:         802 :     if (unread) {
     242                 :             :       // 67.B4: medium weight (paired with the accent unread dot)
     243         [ +  - ]:         404 :       QFont font;
     244         [ +  - ]:         404 :       font.setWeight(QFont::DemiBold);
     245         [ +  - ]:         404 :       return font;
     246                 :         404 :     }
     247                 :         398 :     return {};  // Use view's default font for read messages
     248                 :             :   }
     249                 :             : 
     250   [ +  +  +  + ]:        4036 :   if (role == Qt::ForegroundRole && col == Star) {
     251                 :         116 :     auto &tm = ThemeManager::instance();
     252   [ -  +  -  -  :         348 :     return h.isFlagged() ? QColor(tm.color(QStringLiteral("@star_active")))
          -  +  -  +  -  
             -  -  -  -  
                      - ]
     253   [ +  -  +  -  :         232 :                          : QColor(tm.color(QStringLiteral("@star_inactive")));
          +  -  +  -  +  
          -  -  +  -  -  
             -  -  -  - ]
     254                 :             :   }
     255                 :             : 
     256                 :             :   // T-232: Suggestion column color by confidence (shared palette)
     257   [ +  +  +  + ]:        3920 :   if (role == Qt::ForegroundRole && col == Suggestion) {
     258                 :           4 :     auto it = m_suggestionCache.constFind(MailKey{h.folderId, h.uid});
     259   [ +  -  +  - ]:           8 :     return ThemeManager::confidenceColor(
     260         [ +  - ]:          16 :         it != m_suggestionCache.constEnd() ? it->confidence : 0.0);
     261                 :             :   }
     262                 :             : 
     263                 :             :   // Text alignment for Star column
     264   [ +  +  +  + ]:        3916 :   if (role == Qt::TextAlignmentRole && col == Star) {
     265                 :         117 :     return static_cast<int>(Qt::AlignCenter);
     266                 :             :   }
     267                 :             : 
     268         [ +  + ]:        3799 :   if (role == SortRole) {
     269   [ -  +  -  +  :         131 :     switch (col) {
                -  -  - ]
     270                 :           0 :     case Star:
     271         [ #  # ]:           0 :       return h.isFlagged() ? 1 : 0;
     272                 :           1 :     case Subject:
     273                 :           1 :       return h.subject;
     274                 :           0 :     case From:
     275                 :           0 :       return h.from;
     276                 :         130 :     case Date:
     277                 :         130 :       return h.date;
     278                 :           0 :     case Size:
     279                 :           0 :       return h.size;
     280                 :           0 :     case Suggestion:
     281                 :           0 :       return 0.0;
     282                 :             :     }
     283                 :             :   }
     284                 :             : 
     285         [ +  + ]:        3668 :   if (role == FlagsRole)
     286                 :         128 :     return h.flags;
     287         [ +  + ]:        3540 :   if (role == HasAttachmentsRole)
     288                 :         108 :     return h.hasAttachments;
     289         [ +  + ]:        3432 :   if (role == LabelsRole) {
     290                 :         108 :     QStringList filtered;
     291         [ +  + ]:         114 :     for (const auto &label : h.labels) {
     292   [ +  -  +  - ]:           6 :       if (!ImapResponseParser::isInternalKeyword(label))
     293         [ +  - ]:           6 :         filtered.append(label);
     294                 :             :     }
     295                 :         108 :     return filtered;
     296                 :         108 :   }
     297         [ +  + ]:        3324 :   if (role == ThreadCountRole)
     298                 :           1 :     return node->descendantCount();
     299         [ +  + ]:        3323 :   if (role == DepthRole)  // T-128
     300                 :           3 :     return node->depth;
     301                 :             : 
     302                 :        3320 :   return {};
     303                 :             : }
     304                 :             : 
     305                 :        1085 : QVariant MailThreadModel::headerData(int section, Qt::Orientation orientation,
     306                 :             :                                       int role) const {
     307   [ +  +  +  + ]:        1085 :   if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
     308                 :         865 :     return {};
     309                 :             : 
     310   [ +  +  +  +  :         220 :   switch (section) {
             +  +  +  - ]
     311                 :          36 :   case Star:
     312                 :          36 :     return QStringLiteral("★");
     313                 :          36 :   case Attachment:
     314                 :          36 :     return QStringLiteral("");
     315                 :          37 :   case Subject:
     316         [ +  - ]:          37 :     return tr("Subject");
     317                 :           1 :   case Suggestion:
     318                 :           1 :     return QStringLiteral("→");
     319                 :          37 :   case From:
     320         [ +  - ]:          37 :     return tr("From");
     321                 :          37 :   case Date:
     322         [ +  - ]:          37 :     return tr("Date");
     323                 :          36 :   case Size:
     324         [ +  - ]:          36 :     return tr("Size");
     325                 :             :   }
     326                 :           0 :   return {};
     327                 :             : }
     328                 :             : 
     329                 :             : // Sprint 76 (T-76.B3): refresh translated headers on a live language switch.
     330                 :           2 : void MailThreadModel::retranslateUi() {
     331   [ +  -  +  - ]:           2 :   if (columnCount() > 0)
     332   [ +  -  +  - ]:           2 :     emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1);
     333                 :           2 : }
     334                 :             : 
     335                 :             : // ═══════════════════════════════════════════════════════
     336                 :             : // Drag & Drop (T-102)
     337                 :             : // ═══════════════════════════════════════════════════════
     338                 :             : 
     339                 :         738 : Qt::ItemFlags MailThreadModel::flags(const QModelIndex &index) const {
     340         [ +  - ]:         738 :   auto defaultFlags = QAbstractItemModel::flags(index);
     341         [ +  - ]:         738 :   if (index.isValid())
     342                 :         738 :     return defaultFlags | Qt::ItemIsDragEnabled;
     343                 :           0 :   return defaultFlags;
     344                 :             : }
     345                 :             : 
     346                 :           2 : QStringList MailThreadModel::mimeTypes() const {
     347   [ +  +  -  - ]:           4 :   return {QStringLiteral("application/x-mailjd-uids")};
     348   [ +  -  -  -  :           4 : }
                   -  - ]
     349                 :             : 
     350                 :           1 : QMimeData *MailThreadModel::mimeData(const QModelIndexList &indexes) const {
     351                 :           1 :   QSet<qint64> uidSet;
     352         [ +  + ]:           2 :   for (const auto &idx : indexes) {
     353         [ #  # ]:           1 :     auto *node = nodeFromIndex(idx);
     354         [ +  - ]:           1 :     if (node)
     355         [ +  - ]:           1 :       uidSet.insert(node->header.uid);
     356                 :             :   }
     357                 :             : 
     358                 :           1 :   QByteArray encoded;
     359         [ +  - ]:           1 :   QDataStream stream(&encoded, QIODevice::WriteOnly);
     360   [ +  -  +  -  :           2 :   for (qint64 uid : uidSet)
                   +  + ]
     361         [ +  - ]:           1 :     stream << uid;
     362                 :             : 
     363   [ +  -  +  -  :           1 :   auto *mimeData = new QMimeData;
             -  +  -  - ]
     364         [ +  - ]:           2 :   mimeData->setData(QStringLiteral("application/x-mailjd-uids"), encoded);
     365   [ +  -  +  -  :           2 :   mimeData->setText(QString("%1 Mail(s)").arg(uidSet.size()));
                   +  - ]
     366                 :           1 :   return mimeData;
     367                 :           1 : }
     368                 :             : 
     369                 :           2 : Qt::DropActions MailThreadModel::supportedDragActions() const {
     370                 :           2 :   return Qt::MoveAction;
     371                 :             : }
     372                 :             : 
     373                 :             : // --- Private helpers ---
     374                 :             : 
     375                 :        6966 : ThreadNode *MailThreadModel::nodeFromIndex(const QModelIndex &index) const {
     376         [ +  + ]:        6966 :   if (!index.isValid())
     377                 :           2 :     return nullptr;
     378                 :        6964 :   return static_cast<ThreadNode *>(index.internalPointer());
     379                 :             : }
     380                 :             : 
     381                 :          32 : QModelIndex MailThreadModel::indexFromNode(ThreadNode *node, int column) const {
     382         [ -  + ]:          32 :   if (!node)
     383                 :           0 :     return {};
     384                 :             : 
     385         [ +  + ]:          32 :   if (!node->parent) {
     386                 :          26 :     int row = findRootIndex(node);
     387         [ -  + ]:          26 :     if (row < 0)
     388                 :           0 :       return {};
     389                 :          26 :     return createIndex(row, column, node);
     390                 :             :   }
     391                 :             : 
     392                 :           6 :   int row = findChildIndex(node);
     393         [ -  + ]:           6 :   if (row < 0)
     394                 :           0 :     return {};
     395                 :           6 :   return createIndex(row, column, node);
     396                 :             : }
     397                 :             : 
     398                 :          74 : int MailThreadModel::findRootIndex(ThreadNode *node) const {
     399         [ +  - ]:          99 :   for (size_t i = 0; i < m_roots.size(); ++i) {
     400         [ +  + ]:          99 :     if (m_roots[i].get() == node)
     401                 :          74 :       return i;
     402                 :             :   }
     403                 :           0 :   return -1;
     404                 :             : }
     405                 :             : 
     406                 :           6 : int MailThreadModel::findChildIndex(ThreadNode *node) const {
     407   [ +  -  -  + ]:           6 :   if (!node || !node->parent)
     408                 :           0 :     return -1;
     409         [ +  - ]:           9 :   for (size_t i = 0; i < node->parent->children.size(); ++i) {
     410         [ +  + ]:           9 :     if (node->parent->children[i].get() == node)
     411                 :           6 :       return i;
     412                 :             :   }
     413                 :           0 :   return -1;
     414                 :             : }
     415                 :             : 
     416                 :          62 : void MailThreadModel::buildUidIndex() {
     417                 :          62 :   m_uidIndex.clear();
     418                 :         283 :   std::function<void(ThreadNode *)> walk = [&](ThreadNode *node) {
     419         [ +  - ]:         159 :     m_uidIndex.insert(MailKey{node->header.folderId, node->header.uid}, node);
     420         [ +  + ]:         173 :     for (auto &child : node->children)
     421         [ +  - ]:          14 :       walk(child.get());
     422                 :         221 :   };
     423         [ +  + ]:         207 :   for (auto &root : m_roots)
     424         [ +  - ]:         145 :     walk(root.get());
     425                 :          62 : }
     426                 :             : 
     427                 :             : // T-232: Set suggestion for a specific UID
     428                 :           6 : void MailThreadModel::setSuggestion(qint64 uid, qint64 folderId,
     429                 :             :                                      const QString &text,
     430                 :             :                                      double confidence) {
     431         [ +  - ]:           6 :   m_suggestionCache[MailKey{folderId, uid}] = {text, confidence};
     432                 :           6 :   auto *node = m_uidIndex.value(MailKey{folderId, uid}, nullptr);
     433         [ +  - ]:           6 :   if (node) {
     434         [ +  - ]:           6 :     auto idx = indexFromNode(node, Suggestion);
     435   [ +  -  +  - ]:           6 :     emit dataChanged(idx, idx, {Qt::DisplayRole, Qt::ForegroundRole});
     436                 :             :   }
     437         [ -  - ]:          12 : }
     438                 :             : 
     439                 :             : // T-232: Clear all suggestion data
     440                 :           4 : void MailThreadModel::clearSuggestions() {
     441         [ +  + ]:           4 :   if (m_suggestionCache.isEmpty())
     442                 :           2 :     return;
     443                 :           2 :   m_suggestionCache.clear();
     444                 :             :   // Use dataChanged for the suggestion column range instead of dangerous
     445                 :             :   // bare layoutChanged() (which causes crash without layoutAboutToBeChanged).
     446         [ +  - ]:           2 :   if (!m_roots.empty()) {
     447   [ +  -  +  -  :           6 :     emit dataChanged(index(0, Suggestion),
                   +  - ]
     448   [ +  -  +  - ]:           4 :                      index(rowCount() - 1, Suggestion),
     449                 :             :                      {Qt::DisplayRole, Qt::ForegroundRole});
     450                 :             :   }
     451                 :             : }
        

Generated by: LCOV version 2.0-1