ui/MailListModel.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "MailListModel.h" | ||
| 2 | |||
| 3 | #include <QColor> | ||
| 4 | #include <QDataStream> | ||
| 5 | #include <QFont> | ||
| 6 | #include <QIODevice> | ||
| 7 | #include <QLocale> | ||
| 8 | #include <QLoggingCategory> | ||
| 9 | #include <QPointer> | ||
| 10 | #include <QScopedValueRollback> | ||
| 11 | #include <QTimer> | ||
| 12 | #include <QSet> | ||
| 13 | #include <algorithm> | ||
| 14 | |||
| 15 | #include "service/ImapResponseParser.h" | ||
| 16 | #include "ui/ThemeManager.h" | ||
| 17 | |||
| 18 |
4/7✓ Branch 2 → 3 taken 1 time.
✓ Branch 2 → 8 taken 4 times.
✓ Branch 4 → 5 taken 1 time.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 1 time.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
5 | Q_LOGGING_CATEGORY(lcMailModel, "mailjd.mailmodel") |
| 19 | |||
| 20 | 1265 | MailListModel::MailListModel(QObject *parent) : QAbstractTableModel(parent) {} | |
| 21 | |||
| 22 | 1600872 | int MailListModel::rowCount(const QModelIndex &parent) const { | |
| 23 |
2/2✓ Branch 3 → 4 taken 307 times.
✓ Branch 3 → 5 taken 1600565 times.
|
1600872 | return parent.isValid() ? 0 : m_headers.size(); |
| 24 | } | ||
| 25 | |||
| 26 | 1599605 | int MailListModel::columnCount(const QModelIndex &parent) const { | |
| 27 |
2/2✓ Branch 3 → 4 taken 16 times.
✓ Branch 3 → 5 taken 1599589 times.
|
1599605 | return parent.isValid() ? 0 : ColumnCount; |
| 28 | } | ||
| 29 | |||
| 30 | 41536 | QVariant MailListModel::data(const QModelIndex &index, int role) const { | |
| 31 |
3/6✓ Branch 3 → 4 taken 41536 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 41536 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 41536 times.
|
41536 | if (!index.isValid() || index.row() >= m_headers.size()) |
| 32 | ✗ | return {}; | |
| 33 | |||
| 34 | 41536 | const auto &h = m_headers.at(index.row()); | |
| 35 | |||
| 36 |
2/2✓ Branch 13 → 14 taken 698 times.
✓ Branch 13 → 82 taken 40838 times.
|
41536 | if (role == Qt::DisplayRole) { |
| 37 |
3/8✓ Branch 15 → 16 taken 40 times.
✗ Branch 15 → 31 not taken.
✓ Branch 15 → 32 taken 331 times.
✗ Branch 15 → 62 not taken.
✓ Branch 15 → 73 taken 327 times.
✗ Branch 15 → 74 not taken.
✗ Branch 15 → 78 not taken.
✗ Branch 15 → 82 not taken.
|
698 | switch (index.column()) { |
| 38 | 40 | case Star: | |
| 39 |
6/6✓ Branch 17 → 18 taken 2 times.
✓ Branch 17 → 21 taken 38 times.
✓ Branch 26 → 27 taken 38 times.
✓ Branch 26 → 28 taken 2 times.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 28 → 30 taken 38 times.
|
80 | return h.isFlagged() ? QStringLiteral("★") : QStringLiteral("☆"); |
| 40 | ✗ | case Attachment: | |
| 41 | ✗ | return {}; // painted by dedicated delegate | |
| 42 | 331 | case Subject: { | |
| 43 |
1/3✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 331 times.
✗ Branch 34 → 36 not taken.
|
331 | QString subj = h.subject.isEmpty() ? tr("(No Subject)") : h.subject; |
| 44 | // T-79.E5/M16: search-mode folder badge is prepended for display | ||
| 45 | // only — MailHeader::subject stays clean for replies/forwards. | ||
| 46 | 331 | const QString badge = m_folderBadges.value(h.folderId); | |
| 47 |
1/2✗ Branch 38 → 39 not taken.
✓ Branch 38 → 49 taken 331 times.
|
331 | if (!badge.isEmpty()) |
| 48 | ✗ | subj.prepend(QStringLiteral("[%1] ").arg(badge)); | |
| 49 |
3/3✓ Branch 50 → 51 taken 1 time.
✓ Branch 50 → 58 taken 330 times.
✓ Branch 54 → 55 taken 1 time.
|
332 | if (h.isAnswered()) subj.prepend(QStringLiteral("↩ ")); |
| 50 | 331 | return subj; | |
| 51 | 331 | } | |
| 52 | ✗ | case Suggestion: { | |
| 53 | ✗ | auto it = m_suggestionCache.constFind(MailKey{h.folderId, h.uid}); | |
| 54 | ✗ | return it != m_suggestionCache.constEnd() ? it->text : QString(); | |
| 55 | } | ||
| 56 | 327 | case From: | |
| 57 | 327 | return h.from; | |
| 58 | ✗ | case Date: | |
| 59 | ✗ | return formatDate(h.date); | |
| 60 | ✗ | case Size: | |
| 61 | ✗ | return formatSize(h.size); | |
| 62 | } | ||
| 63 | } | ||
| 64 | |||
| 65 | // Star column: gold for flagged, light gray for unflagged | ||
| 66 |
5/6✓ Branch 82 → 83 taken 22 times.
✓ Branch 82 → 86 taken 40816 times.
✓ Branch 84 → 85 taken 22 times.
✗ Branch 84 → 86 not taken.
✓ Branch 87 → 88 taken 22 times.
✓ Branch 87 → 116 taken 40816 times.
|
40838 | if (role == Qt::ForegroundRole && index.column() == Star) { |
| 67 | 22 | auto &tm = ThemeManager::instance(); | |
| 68 |
7/13✓ Branch 90 → 91 taken 1 time.
✓ Branch 90 → 96 taken 21 times.
✓ Branch 94 → 95 taken 1 time.
✓ Branch 111 → 112 taken 1 time.
✓ Branch 111 → 113 taken 21 times.
✓ Branch 113 → 114 taken 1 time.
✓ Branch 113 → 115 taken 21 times.
✗ Branch 265 → 266 not taken.
✗ Branch 265 → 267 not taken.
✗ Branch 269 → 270 not taken.
✗ Branch 269 → 271 not taken.
✗ Branch 273 → 274 not taken.
✗ Branch 273 → 275 not taken.
|
66 | return h.isFlagged() ? QColor(tm.color(QStringLiteral("@star_active"))) |
| 69 |
10/16✓ Branch 99 → 100 taken 21 times.
✓ Branch 101 → 102 taken 22 times.
✓ Branch 103 → 104 taken 21 times.
✓ Branch 103 → 105 taken 1 time.
✓ Branch 105 → 106 taken 21 times.
✓ Branch 105 → 107 taken 1 time.
✓ Branch 107 → 108 taken 21 times.
✓ Branch 107 → 109 taken 1 time.
✓ Branch 109 → 110 taken 1 time.
✓ Branch 109 → 111 taken 21 times.
✗ Branch 253 → 254 not taken.
✗ Branch 253 → 255 not taken.
✗ Branch 257 → 258 not taken.
✗ Branch 257 → 259 not taken.
✗ Branch 261 → 262 not taken.
✗ Branch 261 → 263 not taken.
|
43 | : QColor(tm.color(QStringLiteral("@star_inactive"))); |
| 70 | } | ||
| 71 | |||
| 72 | // T-232: Suggestion column color based on confidence (shared palette) | ||
| 73 |
2/6✗ Branch 116 → 117 not taken.
✓ Branch 116 → 120 taken 40816 times.
✗ Branch 118 → 119 not taken.
✗ Branch 118 → 120 not taken.
✗ Branch 121 → 122 not taken.
✓ Branch 121 → 134 taken 40816 times.
|
40816 | if (role == Qt::ForegroundRole && index.column() == Suggestion) { |
| 74 | ✗ | auto it = m_suggestionCache.constFind(MailKey{h.folderId, h.uid}); | |
| 75 | ✗ | return ThemeManager::confidenceColor( | |
| 76 | ✗ | it != m_suggestionCache.constEnd() ? it->confidence : 0.0); | |
| 77 | } | ||
| 78 | |||
| 79 | // Tooltip for Star column | ||
| 80 |
5/6✓ Branch 134 → 135 taken 20 times.
✓ Branch 134 → 138 taken 40796 times.
✓ Branch 136 → 137 taken 20 times.
✗ Branch 136 → 138 not taken.
✓ Branch 139 → 140 taken 20 times.
✓ Branch 139 → 147 taken 40796 times.
|
40816 | if (role == Qt::ToolTipRole && index.column() == Star) { |
| 81 |
2/4✗ Branch 141 → 142 not taken.
✓ Branch 141 → 143 taken 20 times.
✗ Branch 142 → 144 not taken.
✓ Branch 143 → 144 taken 20 times.
|
20 | return h.isFlagged() ? tr("Remove Flag") : tr("Flag"); |
| 82 | } | ||
| 83 | |||
| 84 | // Tooltip for Subject column: show full subject on hover | ||
| 85 |
2/6✗ Branch 147 → 148 not taken.
✓ Branch 147 → 151 taken 40796 times.
✗ Branch 149 → 150 not taken.
✗ Branch 149 → 151 not taken.
✗ Branch 152 → 153 not taken.
✓ Branch 152 → 154 taken 40796 times.
|
40796 | if (role == Qt::ToolTipRole && index.column() == Subject) { |
| 86 | ✗ | return h.subject; | |
| 87 | } | ||
| 88 | |||
| 89 | // Tooltip for Attachment column | ||
| 90 |
2/6✗ Branch 154 → 155 not taken.
✓ Branch 154 → 158 taken 40796 times.
✗ Branch 156 → 157 not taken.
✗ Branch 156 → 158 not taken.
✗ Branch 159 → 160 not taken.
✓ Branch 159 → 166 taken 40796 times.
|
40796 | if (role == Qt::ToolTipRole && index.column() == Attachment) { |
| 91 | ✗ | return h.hasAttachments ? tr("Has attachment") : QString(); | |
| 92 | } | ||
| 93 | |||
| 94 | // 67.B4: unread = medium weight (paired with the accent unread dot; | ||
| 95 | // replaces the former bold-only signal) — applied to all columns | ||
| 96 |
6/6✓ Branch 166 → 167 taken 20 times.
✓ Branch 166 → 170 taken 40776 times.
✓ Branch 168 → 169 taken 15 times.
✓ Branch 168 → 170 taken 5 times.
✓ Branch 171 → 172 taken 15 times.
✓ Branch 171 → 178 taken 40781 times.
|
40796 | if (role == Qt::FontRole && !h.isSeen()) { |
| 97 |
1/1✓ Branch 172 → 173 taken 15 times.
|
15 | QFont font; |
| 98 |
1/1✓ Branch 173 → 174 taken 15 times.
|
15 | font.setWeight(QFont::DemiBold); |
| 99 |
1/1✓ Branch 174 → 175 taken 15 times.
|
15 | return font; |
| 100 | 15 | } | |
| 101 | |||
| 102 | // Text alignment: Star column centered | ||
| 103 |
5/6✓ Branch 178 → 179 taken 20 times.
✓ Branch 178 → 182 taken 40761 times.
✓ Branch 180 → 181 taken 20 times.
✗ Branch 180 → 182 not taken.
✓ Branch 183 → 184 taken 20 times.
✓ Branch 183 → 185 taken 40761 times.
|
40781 | if (role == Qt::TextAlignmentRole && index.column() == Star) { |
| 104 | 20 | return static_cast<int>(Qt::AlignCenter); | |
| 105 | } | ||
| 106 | |||
| 107 | // Sort role: return raw values for proper sorting | ||
| 108 |
2/2✓ Branch 185 → 186 taken 4 times.
✓ Branch 185 → 206 taken 40757 times.
|
40761 | if (role == SortRole) { |
| 109 |
1/5✓ Branch 187 → 188 taken 4 times.
✗ Branch 187 → 193 not taken.
✗ Branch 187 → 194 not taken.
✗ Branch 187 → 195 not taken.
✗ Branch 187 → 205 not taken.
|
4 | switch (index.column()) { |
| 110 | 4 | case Star: | |
| 111 |
2/2✓ Branch 189 → 190 taken 2 times.
✓ Branch 189 → 191 taken 2 times.
|
4 | return h.isFlagged() ? 1 : 0; |
| 112 | ✗ | case Date: | |
| 113 | ✗ | return h.date; | |
| 114 | ✗ | case Size: | |
| 115 | ✗ | return h.size; | |
| 116 | ✗ | case Suggestion: { | |
| 117 | ✗ | auto it = m_suggestionCache.constFind(MailKey{h.folderId, h.uid}); | |
| 118 | ✗ | return it != m_suggestionCache.constEnd() ? it->confidence : 0.0; | |
| 119 | } | ||
| 120 | ✗ | default: | |
| 121 | ✗ | return data(index, Qt::DisplayRole); | |
| 122 | } | ||
| 123 | } | ||
| 124 | |||
| 125 | // Custom FlagsRole: return the raw flags bitmask (for filtering) | ||
| 126 |
1/2✗ Branch 206 → 207 not taken.
✓ Branch 206 → 208 taken 40757 times.
|
40757 | if (role == FlagsRole) { |
| 127 | ✗ | return h.flags; | |
| 128 | } | ||
| 129 | |||
| 130 | // HasAttachmentsRole: from MailHeader (populated by cache/body parser) | ||
| 131 |
2/2✓ Branch 208 → 209 taken 2 times.
✓ Branch 208 → 210 taken 40755 times.
|
40757 | if (role == HasAttachmentsRole) { |
| 132 | 2 | return h.hasAttachments; | |
| 133 | } | ||
| 134 | |||
| 135 | // LabelsRole: IMAP keywords parsed from FLAGS | ||
| 136 | // Filter internal keywords (e.g. NonJunk) at display time for cached data | ||
| 137 |
2/2✓ Branch 210 → 211 taken 1 time.
✓ Branch 210 → 223 taken 40754 times.
|
40755 | if (role == LabelsRole) { |
| 138 | 1 | QStringList filtered; | |
| 139 |
2/2✓ Branch 219 → 213 taken 2 times.
✓ Branch 219 → 220 taken 1 time.
|
3 | for (const auto &label : h.labels) { |
| 140 |
2/3✓ Branch 214 → 215 taken 2 times.
✓ Branch 215 → 216 taken 2 times.
✗ Branch 215 → 217 not taken.
|
2 | if (!ImapResponseParser::isInternalKeyword(label)) { |
| 141 |
1/1✓ Branch 216 → 217 taken 2 times.
|
2 | filtered.append(label); |
| 142 | } | ||
| 143 | } | ||
| 144 | 1 | return filtered; | |
| 145 | 1 | } | |
| 146 | |||
| 147 |
2/2✓ Branch 223 → 224 taken 40589 times.
✓ Branch 223 → 228 taken 165 times.
|
40754 | if (role == FolderBadgeRole) |
| 148 | 40589 | return m_folderBadges.value(h.folderId); | |
| 149 | |||
| 150 | 165 | return {}; | |
| 151 | } | ||
| 152 | |||
| 153 | 1 | QVariant MailListModel::headerData(int section, Qt::Orientation orientation, | |
| 154 | int role) const { | ||
| 155 |
2/4✓ Branch 2 → 3 taken 1 time.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
|
1 | if (orientation != Qt::Horizontal || role != Qt::DisplayRole) |
| 156 | ✗ | return {}; | |
| 157 | |||
| 158 |
1/8✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 13 not taken.
✗ Branch 5 → 20 not taken.
✗ Branch 5 → 24 not taken.
✗ Branch 5 → 31 not taken.
✗ Branch 5 → 35 not taken.
✗ Branch 5 → 39 not taken.
✗ Branch 5 → 43 not taken.
|
1 | switch (section) { |
| 159 | 1 | case Star: | |
| 160 | 1 | return QStringLiteral(""); // narrow column, no text header | |
| 161 | ✗ | case Attachment: | |
| 162 | ✗ | return QStringLiteral(""); // icon-only column | |
| 163 | ✗ | case Subject: | |
| 164 | ✗ | return tr("Subject"); | |
| 165 | ✗ | case Suggestion: | |
| 166 | ✗ | return QStringLiteral("→"); // arrow symbol | |
| 167 | ✗ | case From: | |
| 168 | ✗ | return tr("From"); | |
| 169 | ✗ | case Date: | |
| 170 | ✗ | return tr("Date"); | |
| 171 | ✗ | case Size: | |
| 172 | ✗ | return tr("Size"); | |
| 173 | } | ||
| 174 | ✗ | return {}; | |
| 175 | } | ||
| 176 | |||
| 177 | // Sprint 76 (T-76.B3): models do not receive LanguageChange events, so the | ||
| 178 | // language-switch path (MainWindow) calls this to make views re-read the | ||
| 179 | // translated headers via headerData(). | ||
| 180 | ✗ | void MailListModel::retranslateUi() { | |
| 181 | ✗ | if (columnCount() > 0) | |
| 182 | ✗ | emit headerDataChanged(Qt::Horizontal, 0, columnCount() - 1); | |
| 183 | ✗ | } | |
| 184 | |||
| 185 | // ═══════════════════════════════════════════════════════ | ||
| 186 | // Drag & Drop (T-102) | ||
| 187 | // ═══════════════════════════════════════════════════════ | ||
| 188 | |||
| 189 | 26 | Qt::ItemFlags MailListModel::flags(const QModelIndex &index) const { | |
| 190 |
1/1✓ Branch 2 → 3 taken 26 times.
|
26 | auto defaultFlags = QAbstractTableModel::flags(index); |
| 191 |
1/2✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 26 times.
|
26 | if (index.isValid()) |
| 192 | ✗ | return defaultFlags | Qt::ItemIsDragEnabled; | |
| 193 | 26 | return defaultFlags; | |
| 194 | } | ||
| 195 | |||
| 196 | 26 | QStringList MailListModel::mimeTypes() const { | |
| 197 | ✗ | return {QStringLiteral("application/x-mailjd-mail-ids"), | |
| 198 |
2/4✓ Branch 10 → 11 taken 52 times.
✓ Branch 10 → 12 taken 26 times.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
|
78 | QStringLiteral("application/x-mailjd-uids")}; |
| 199 | 78 | } | |
| 200 | |||
| 201 | 1 | QMimeData *MailListModel::mimeData(const QModelIndexList &indexes) const { | |
| 202 | // Collect unique mail identities from the selected rows. UIDs are only | ||
| 203 | // unique within a folder, so drag payloads must carry the source folderId. | ||
| 204 | 1 | QList<MailIdentity> mails; | |
| 205 | 1 | QSet<qint64> uidSet; | |
| 206 | 1 | QSet<QString> identitySet; | |
| 207 |
2/2✓ Branch 43 → 6 taken 2 times.
✓ Branch 43 → 44 taken 1 time.
|
3 | for (const auto &idx : indexes) { |
| 208 |
3/6✓ Branch 8 → 9 taken 2 times.
✗ Branch 8 → 12 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 2 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 2 times.
|
2 | if (!idx.isValid() || idx.row() >= m_headers.size()) |
| 209 | ✗ | continue; | |
| 210 | 2 | const auto &header = m_headers.at(idx.row()); | |
| 211 | const QString key = | ||
| 212 |
2/2✓ Branch 22 → 23 taken 2 times.
✓ Branch 24 → 25 taken 2 times.
|
6 | QStringLiteral("%1:%2").arg(header.folderId).arg(header.uid); |
| 213 |
1/2✗ Branch 29 → 30 not taken.
✓ Branch 29 → 31 taken 2 times.
|
2 | if (identitySet.contains(key)) |
| 214 | ✗ | continue; | |
| 215 |
1/1✓ Branch 31 → 32 taken 2 times.
|
2 | identitySet.insert(key); |
| 216 |
1/1✓ Branch 32 → 33 taken 2 times.
|
2 | mails.append({QString(), header.folderId, header.uid}); |
| 217 |
1/1✓ Branch 34 → 35 taken 2 times.
|
2 | uidSet.insert(header.uid); |
| 218 | 2 | } | |
| 219 | |||
| 220 | 1 | QByteArray identityEncoded; | |
| 221 |
1/1✓ Branch 45 → 46 taken 1 time.
|
1 | QDataStream identityStream(&identityEncoded, QIODevice::WriteOnly); |
| 222 |
4/4✓ Branch 46 → 47 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 55 → 49 taken 2 times.
✓ Branch 55 → 56 taken 1 time.
|
3 | for (const auto &mail : mails) |
| 223 |
3/3✓ Branch 50 → 51 taken 2 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 52 → 53 taken 2 times.
|
2 | identityStream << mail.account << mail.folderId << mail.uid; |
| 224 | |||
| 225 | 1 | QByteArray encoded; | |
| 226 |
1/1✓ Branch 57 → 58 taken 1 time.
|
1 | QDataStream stream(&encoded, QIODevice::WriteOnly); |
| 227 |
4/4✓ Branch 58 → 59 taken 1 time.
✓ Branch 59 → 60 taken 1 time.
✓ Branch 65 → 61 taken 1 time.
✓ Branch 65 → 66 taken 1 time.
|
2 | for (qint64 uid : uidSet) |
| 228 |
1/1✓ Branch 62 → 63 taken 1 time.
|
1 | stream << uid; |
| 229 | |||
| 230 |
3/6✓ Branch 66 → 67 taken 1 time.
✓ Branch 67 → 68 taken 1 time.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 70 taken 1 time.
✗ Branch 122 → 123 not taken.
✗ Branch 122 → 124 not taken.
|
1 | auto *mimeData = new QMimeData; |
| 231 |
1/1✓ Branch 73 → 74 taken 1 time.
|
2 | mimeData->setData(QStringLiteral("application/x-mailjd-mail-ids"), |
| 232 | identityEncoded); | ||
| 233 |
1/1✓ Branch 79 → 80 taken 1 time.
|
2 | mimeData->setData(QStringLiteral("application/x-mailjd-uids"), encoded); |
| 234 | |||
| 235 | // Set human-readable text for drag tooltip | ||
| 236 |
1/1✓ Branch 86 → 87 taken 1 time.
|
1 | mimeData->setText( |
| 237 |
2/2✓ Branch 82 → 83 taken 1 time.
✓ Branch 85 → 86 taken 1 time.
|
3 | QString("%1 Mail(s)").arg(mails.size())); |
| 238 | |||
| 239 | 1 | return mimeData; | |
| 240 | 1 | } | |
| 241 | |||
| 242 | ✗ | Qt::DropActions MailListModel::supportedDragActions() const { | |
| 243 | ✗ | return Qt::MoveAction; | |
| 244 | } | ||
| 245 | |||
| 246 | 450 | void MailListModel::setHeaders(const QList<MailHeader> &headers) { | |
| 247 | 450 | ++m_pageGeneration; | |
| 248 | 450 | m_pagePending = false; | |
| 249 | 450 | m_pageFailed = false; | |
| 250 | 450 | m_previousPageCursor = {}; | |
| 251 | 450 | beginResetModel(); | |
| 252 | 450 | m_headers = headers; | |
| 253 | 450 | m_totalHeaderCount = headers.size(); | |
| 254 | 450 | m_pageLoader = {}; | |
| 255 | 450 | m_previousPageLoader = {}; | |
| 256 | 450 | m_pageCursor = {}; | |
| 257 | 450 | m_suggestionCache.clear(); // T-232 | |
| 258 | 450 | m_retainedBytes = 0; | |
| 259 |
5/5✓ Branch 14 → 15 taken 450 times.
✓ Branch 15 → 16 taken 450 times.
✓ Branch 18 → 19 taken 29987 times.
✓ Branch 21 → 17 taken 29987 times.
✓ Branch 21 → 22 taken 450 times.
|
30437 | for (const auto &header : m_headers) m_retainedBytes += headerCharge(header); |
| 260 | 450 | rebuildUidIndex(); | |
| 261 | 450 | ++m_unreadBasis; // the total is now the loaded rows' (S106-184) | |
| 262 | 450 | endResetModel(); | |
| 263 | 450 | } | |
| 264 | |||
| 265 | ✗ | void MailListModel::setPagedHeaders(const QList<MailHeader> &firstPage, | |
| 266 | int totalCount, int totalUnread, | ||
| 267 | PageLoader loader, PageLoader previousLoader) { | ||
| 268 | ✗ | const auto adapt = [](PageLoader source, bool previous) -> AsyncPageLoader { | |
| 269 | ✗ | if (!source) return {}; | |
| 270 | ✗ | return [source = std::move(source), previous](const PageCursor &anchor, int limit, PageCallback done) { | |
| 271 | ✗ | MailHeader header; | |
| 272 | ✗ | header.uid = anchor.uid; | |
| 273 | ✗ | header.date = anchor.date; | |
| 274 | ✗ | PageResult result; | |
| 275 | ✗ | result.headers = source(header, limit); | |
| 276 | ✗ | result.endOfData = result.headers.isEmpty(); | |
| 277 | ✗ | if (!result.endOfData) | |
| 278 | ✗ | result.cursor = previous ? result.headers.constFirst() : result.headers.constLast(); | |
| 279 | ✗ | done(std::move(result)); | |
| 280 | ✗ | }; | |
| 281 | }; | ||
| 282 | ✗ | setAsyncPagedHeaders(firstPage, totalCount, totalUnread, | |
| 283 | ✗ | adapt(std::move(loader), false), adapt(std::move(previousLoader), true)); | |
| 284 | ✗ | } | |
| 285 | |||
| 286 | 710 | void MailListModel::setAsyncPagedHeaders(const QList<MailHeader> &firstPage, | |
| 287 | int totalCount, int totalUnread, AsyncPageLoader loader, AsyncPageLoader previousLoader) { | ||
| 288 | 710 | ++m_pageGeneration; | |
| 289 | 710 | m_pagePending = false; | |
| 290 | 710 | m_pageFailed = false; | |
| 291 |
2/2✓ Branch 3 → 4 taken 85 times.
✓ Branch 3 → 5 taken 625 times.
|
710 | m_previousPageCursor = firstPage.isEmpty() ? PageCursor{} : PageCursor(firstPage.constFirst()); |
| 292 |
8/8✓ Branch 10 → 11 taken 100 times.
✓ Branch 10 → 16 taken 610 times.
✓ Branch 12 → 13 taken 77 times.
✓ Branch 12 → 16 taken 23 times.
✓ Branch 14 → 15 taken 30 times.
✓ Branch 14 → 16 taken 47 times.
✓ Branch 17 → 18 taken 30 times.
✓ Branch 17 → 39 taken 680 times.
|
710 | if (!m_headers.isEmpty() && !firstPage.isEmpty() && applyPageDiff(firstPage)) { |
| 293 | 30 | m_totalHeaderCount = std::max(totalCount, static_cast<int>(firstPage.size())); | |
| 294 | 30 | resetUnread(totalUnread); | |
| 295 | 30 | m_pageLoader = std::move(loader); | |
| 296 | 30 | m_previousPageLoader = std::move(previousLoader); | |
| 297 | 30 | m_pageCursor = PageCursor(firstPage.constLast()); | |
| 298 | 30 | m_suggestionCache.clear(); | |
| 299 | 30 | m_retainedBytes = 0; | |
| 300 |
5/5✓ Branch 30 → 31 taken 30 times.
✓ Branch 31 → 32 taken 30 times.
✓ Branch 34 → 35 taken 479 times.
✓ Branch 37 → 33 taken 479 times.
✓ Branch 37 → 38 taken 30 times.
|
509 | for (const auto &header : m_headers) m_retainedBytes += headerCharge(header); |
| 301 | 30 | return; | |
| 302 | } | ||
| 303 | 680 | beginResetModel(); | |
| 304 | 680 | m_headers = firstPage; | |
| 305 | 680 | m_totalHeaderCount = std::max(totalCount, static_cast<int>(firstPage.size())); | |
| 306 | 680 | resetUnread(totalUnread); | |
| 307 | 680 | m_pageLoader = std::move(loader); | |
| 308 | 680 | m_previousPageLoader = std::move(previousLoader); | |
| 309 |
2/2✓ Branch 49 → 50 taken 85 times.
✓ Branch 49 → 51 taken 595 times.
|
680 | m_pageCursor = firstPage.isEmpty() ? PageCursor{} : PageCursor(firstPage.constLast()); |
| 310 | 680 | m_suggestionCache.clear(); | |
| 311 | 680 | m_retainedBytes = 0; | |
| 312 |
5/5✓ Branch 56 → 57 taken 680 times.
✓ Branch 57 → 58 taken 680 times.
✓ Branch 60 → 61 taken 19571 times.
✓ Branch 63 → 59 taken 19571 times.
✓ Branch 63 → 64 taken 680 times.
|
20251 | for (const auto &header : m_headers) m_retainedBytes += headerCharge(header); |
| 313 | 680 | m_uidIndex.clear(); | |
| 314 |
2/2✓ Branch 71 → 66 taken 19571 times.
✓ Branch 71 → 72 taken 680 times.
|
20251 | for (int i = 0; i < m_headers.size(); ++i) { |
| 315 | 19571 | const auto &header = m_headers.at(i); | |
| 316 |
1/1✓ Branch 68 → 69 taken 19571 times.
|
19571 | m_uidIndex.insert(MailKey{header.folderId, header.uid}, i); |
| 317 | } | ||
| 318 | 680 | endResetModel(); | |
| 319 | } | ||
| 320 | |||
| 321 | // A refreshed first page that keeps the relative order of the rows it shares | ||
| 322 | // with the current window (the common case after a sync: new mail on top, | ||
| 323 | // streamed rows beyond the page dropped) is applied as row removals, bounded | ||
| 324 | // single-row moves and insertions, so views keep their delegates instead of | ||
| 325 | // rebuilding the whole list. Returns false when a reset is needed. | ||
| 326 | 91 | bool MailListModel::applyPageDiff(const QList<MailHeader> &page) { | |
| 327 | 14363 | const auto keyOf = [](const MailHeader &header) { return MailKey{header.folderId, header.uid}; }; | |
| 328 | 91 | QSet<MailKey> pageKeys, oldKeys; | |
| 329 |
1/1✓ Branch 5 → 6 taken 91 times.
|
91 | pageKeys.reserve(page.size()); |
| 330 |
3/3✓ Branch 10 → 11 taken 1896 times.
✓ Branch 13 → 8 taken 1896 times.
✓ Branch 13 → 14 taken 91 times.
|
1987 | for (const auto &header : page) pageKeys.insert(keyOf(header)); |
| 331 | 91 | QList<MailKey> survivingOld; | |
| 332 |
2/2✓ Branch 28 → 17 taken 4151 times.
✓ Branch 28 → 29 taken 91 times.
|
4242 | for (const auto &header : std::as_const(m_headers)) { |
| 333 |
1/1✓ Branch 19 → 20 taken 4151 times.
|
4151 | oldKeys.insert(keyOf(header)); |
| 334 |
3/3✓ Branch 22 → 23 taken 505 times.
✓ Branch 22 → 26 taken 3646 times.
✓ Branch 24 → 25 taken 505 times.
|
4151 | if (pageKeys.contains(keyOf(header))) survivingOld.append(keyOf(header)); |
| 335 | } | ||
| 336 |
2/2✓ Branch 30 → 31 taken 47 times.
✓ Branch 30 → 32 taken 44 times.
|
91 | if (survivingOld.isEmpty()) return false; |
| 337 | 44 | QList<MailKey> survivingNew; | |
| 338 |
2/2✓ Branch 43 → 34 taken 524 times.
✓ Branch 43 → 44 taken 44 times.
|
568 | for (const auto &header : page) |
| 339 |
3/3✓ Branch 37 → 38 taken 505 times.
✓ Branch 37 → 41 taken 19 times.
✓ Branch 39 → 40 taken 505 times.
|
524 | if (oldKeys.contains(keyOf(header))) survivingNew.append(keyOf(header)); |
| 340 | 44 | int moves = 0; | |
| 341 |
2/2✓ Branch 57 → 45 taken 505 times.
✓ Branch 57 → 58 taken 44 times.
|
549 | for (int at = 0; at < survivingNew.size(); ++at) { |
| 342 |
2/2✓ Branch 48 → 49 taken 496 times.
✓ Branch 48 → 50 taken 9 times.
|
505 | if (survivingOld.at(at) == survivingNew.at(at)) continue; |
| 343 |
1/1✓ Branch 52 → 53 taken 9 times.
|
9 | survivingOld.move(survivingOld.indexOf(survivingNew.at(at), at + 1), at); |
| 344 |
1/2✗ Branch 53 → 54 not taken.
✓ Branch 53 → 55 taken 9 times.
|
9 | if (++moves > MaximumPageDiffMoves) return false; |
| 345 | } | ||
| 346 |
2/2✓ Branch 80 → 60 taken 516 times.
✓ Branch 80 → 81 taken 44 times.
|
560 | for (int row = m_headers.size() - 1; row >= 0;) { |
| 347 |
2/2✓ Branch 63 → 64 taken 505 times.
✓ Branch 63 → 65 taken 11 times.
|
516 | if (pageKeys.contains(keyOf(m_headers.at(row)))) { --row; continue; } |
| 348 | 11 | const int last = row; | |
| 349 |
6/6✓ Branch 67 → 68 taken 1078 times.
✓ Branch 67 → 73 taken 6 times.
✓ Branch 71 → 72 taken 1073 times.
✓ Branch 71 → 73 taken 5 times.
✓ Branch 74 → 66 taken 1073 times.
✓ Branch 74 → 75 taken 11 times.
|
1084 | while (row >= 0 && !pageKeys.contains(keyOf(m_headers.at(row)))) --row; |
| 350 |
1/1✓ Branch 76 → 77 taken 11 times.
|
11 | beginRemoveRows({}, row + 1, last); |
| 351 |
1/1✓ Branch 77 → 78 taken 11 times.
|
11 | m_headers.remove(row + 1, last - row); |
| 352 |
1/1✓ Branch 78 → 79 taken 11 times.
|
11 | rebuildUidIndex(); |
| 353 |
1/1✓ Branch 79 → 80 taken 11 times.
|
11 | endRemoveRows(); |
| 354 | } | ||
| 355 |
2/2✓ Branch 98 → 82 taken 505 times.
✓ Branch 98 → 99 taken 44 times.
|
549 | for (int at = 0; at < survivingNew.size(); ++at) { |
| 356 |
2/2✓ Branch 86 → 87 taken 496 times.
✓ Branch 86 → 88 taken 9 times.
|
505 | if (keyOf(m_headers.at(at)) == survivingNew.at(at)) continue; |
| 357 | 9 | const int from = m_uidIndex.value(survivingNew.at(at), -1); | |
| 358 |
1/1✓ Branch 92 → 93 taken 9 times.
|
9 | beginMoveRows({}, from, from, {}, at); |
| 359 |
1/1✓ Branch 93 → 94 taken 9 times.
|
9 | m_headers.move(from, at); |
| 360 |
1/1✓ Branch 94 → 95 taken 9 times.
|
9 | rebuildUidIndex(); |
| 361 |
1/1✓ Branch 95 → 96 taken 9 times.
|
9 | endMoveRows(); |
| 362 | } | ||
| 363 |
2/2✓ Branch 127 → 100 taken 517 times.
✓ Branch 127 → 128 taken 44 times.
|
561 | for (int at = 0; at < page.size();) { |
| 364 |
2/2✓ Branch 103 → 104 taken 505 times.
✓ Branch 103 → 105 taken 12 times.
|
517 | if (oldKeys.contains(keyOf(page.at(at)))) { ++at; continue; } |
| 365 | 12 | int end = at; | |
| 366 |
6/6✓ Branch 108 → 109 taken 15 times.
✓ Branch 108 → 114 taken 4 times.
✓ Branch 112 → 113 taken 7 times.
✓ Branch 112 → 114 taken 8 times.
✓ Branch 115 → 106 taken 7 times.
✓ Branch 115 → 116 taken 12 times.
|
19 | while (end + 1 < page.size() && !oldKeys.contains(keyOf(page.at(end + 1)))) ++end; |
| 367 |
1/1✓ Branch 117 → 118 taken 12 times.
|
12 | beginInsertRows({}, at, end); |
| 368 |
3/3✓ Branch 120 → 121 taken 19 times.
✓ Branch 122 → 119 taken 19 times.
✓ Branch 122 → 123 taken 12 times.
|
31 | for (int row = at; row <= end; ++row) m_headers.insert(row, page.at(row)); |
| 369 |
1/1✓ Branch 123 → 124 taken 12 times.
|
12 | rebuildUidIndex(); |
| 370 |
1/1✓ Branch 124 → 125 taken 12 times.
|
12 | endInsertRows(); |
| 371 | 12 | at = end + 1; | |
| 372 | } | ||
| 373 | // Identities now match the page; publish its (possibly changed) content. | ||
| 374 | // The caller restores the authoritative unread total afterwards. | ||
| 375 | 44 | m_headers = page; | |
| 376 |
1/2✓ Branch 130 → 131 taken 44 times.
✗ Branch 130 → 140 not taken.
|
44 | if (!m_headers.isEmpty()) |
| 377 |
3/3✓ Branch 134 → 135 taken 44 times.
✓ Branch 136 → 137 taken 44 times.
✓ Branch 137 → 138 taken 44 times.
|
44 | emit dataChanged(index(0, 0), index(m_headers.size() - 1, ColumnCount - 1)); |
| 378 | 44 | return true; | |
| 379 | 91 | } | |
| 380 | |||
| 381 | 35 | void MailListModel::setAuthoritativeCounts(int totalCount, int totalUnread) { | |
| 382 | 35 | m_totalHeaderCount = std::max(totalCount, static_cast<int>(m_headers.size())); | |
| 383 | 35 | resetUnread(std::max(0, totalUnread)); | |
| 384 | 35 | } | |
| 385 | |||
| 386 | 5256 | bool MailListModel::canFetchMore(const QModelIndex &parent) const { | |
| 387 |
6/8✓ Branch 3 → 4 taken 5256 times.
✗ Branch 3 → 11 not taken.
✓ Branch 5 → 6 taken 2878 times.
✓ Branch 5 → 11 taken 2378 times.
✓ Branch 6 → 7 taken 2864 times.
✓ Branch 6 → 11 taken 14 times.
✓ Branch 7 → 8 taken 2864 times.
✗ Branch 7 → 11 not taken.
|
8120 | return !parent.isValid() && m_pageLoader && !m_pagePending && !m_pageFailed && |
| 388 |
2/2✓ Branch 9 → 10 taken 283 times.
✓ Branch 9 → 11 taken 2581 times.
|
8120 | m_headers.size() < m_totalHeaderCount; |
| 389 | } | ||
| 390 | |||
| 391 | 161 | void MailListModel::fetchMore(const QModelIndex &parent) { | |
| 392 |
2/3✓ Branch 3 → 4 taken 161 times.
✗ Branch 3 → 8 not taken.
✓ Branch 5 → 6 taken 161 times.
|
161 | if (!parent.isValid()) requestPage(false); |
| 393 | 161 | } | |
| 394 | |||
| 395 | 19 | bool MailListModel::canFetchPrevious() const { | |
| 396 | // A seek leaves an empty window with a valid anchor cursor: it is still | ||
| 397 | // fetchable (T-87.D1 random seek). | ||
| 398 |
3/4✓ Branch 4 → 5 taken 18 times.
✓ Branch 4 → 12 taken 1 time.
✓ Branch 5 → 6 taken 18 times.
✗ Branch 5 → 12 not taken.
|
38 | return m_previousPageLoader && !m_pagePending && !m_pageFailed && |
| 399 |
2/6✓ Branch 3 → 4 taken 19 times.
✗ Branch 3 → 12 not taken.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 18 times.
✗ Branch 8 → 9 not taken.
✗ Branch 8 → 12 not taken.
|
56 | (!m_headers.isEmpty() || m_pageCursor.uid > 0) && |
| 400 |
2/2✓ Branch 10 → 11 taken 15 times.
✓ Branch 10 → 12 taken 3 times.
|
37 | m_headers.size() < m_totalHeaderCount; |
| 401 | } | ||
| 402 | |||
| 403 | 5 | void MailListModel::fetchPrevious(std::function<void()> completed) { | |
| 404 |
1/1✓ Branch 4 → 5 taken 5 times.
|
5 | requestPage(true, std::move(completed)); |
| 405 | 5 | } | |
| 406 | |||
| 407 | ✗ | void MailListModel::seekToUid(qint64 uid, std::function<void()> completed) { | |
| 408 | // A seek needs both directions so the anchored window can grow around the | ||
| 409 | // target; a uid-only anchor (invalid date) uses the keyset's uid fallback. | ||
| 410 | ✗ | if (!m_pageLoader || !m_previousPageLoader || m_pagePending || uid <= 0) | |
| 411 | ✗ | return; | |
| 412 | ✗ | MailHeader anchor; | |
| 413 | ✗ | anchor.uid = uid; | |
| 414 | ✗ | anchor.folderId = m_headers.isEmpty() ? 0 : m_headers.constFirst().folderId; | |
| 415 | ✗ | ++m_pageGeneration; // invalidate any in-flight page | |
| 416 | ✗ | m_pagePending = false; | |
| 417 | ✗ | m_pageFailed = false; | |
| 418 | ✗ | m_previousPageCursor = PageCursor(anchor); | |
| 419 | ✗ | m_pageCursor = PageCursor(anchor); | |
| 420 | ✗ | m_pageCursor.includeAnchor = true; // seek target must appear in the list | |
| 421 | ✗ | beginResetModel(); | |
| 422 | ✗ | m_headers.clear(); | |
| 423 | ✗ | const int unread = m_unreadCount; | |
| 424 | ✗ | rebuildUidIndex(); | |
| 425 | ✗ | m_unreadCount = unread; | |
| 426 | ✗ | m_retainedBytes = 0; | |
| 427 | ✗ | endResetModel(); | |
| 428 | ✗ | requestPage(true, [this, completed = std::move(completed)]() mutable { | |
| 429 | ✗ | requestPage(false, std::move(completed)); | |
| 430 | ✗ | }); | |
| 431 | ✗ | } | |
| 432 | |||
| 433 | 166 | void MailListModel::requestPage(bool previous, std::function<void()> completed) { | |
| 434 |
3/4✓ Branch 2 → 3 taken 47 times.
✓ Branch 2 → 4 taken 119 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 47 times.
|
166 | const bool hasAnchor = m_pageCursor.uid > 0 || m_previousPageCursor.uid > 0; |
| 435 |
5/6✓ Branch 7 → 8 taken 38 times.
✓ Branch 7 → 10 taken 128 times.
✓ Branch 8 → 9 taken 38 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 38 times.
✓ Branch 11 → 13 taken 128 times.
|
189 | if (m_headers.isEmpty() && !hasAnchor) return; |
| 436 |
5/5✓ Branch 13 → 14 taken 5 times.
✓ Branch 13 → 16 taken 123 times.
✓ Branch 17 → 18 taken 123 times.
✓ Branch 19 → 20 taken 23 times.
✓ Branch 19 → 21 taken 105 times.
|
128 | if (!(previous ? canFetchPrevious() : canFetchMore({}))) return; |
| 437 | 105 | const auto generation = m_pageGeneration; | |
| 438 |
2/2✓ Branch 21 → 22 taken 5 times.
✓ Branch 21 → 23 taken 100 times.
|
105 | const auto anchor = previous ? m_previousPageCursor : m_pageCursor; |
| 439 |
3/3✓ Branch 25 → 26 taken 5 times.
✓ Branch 25 → 27 taken 100 times.
✓ Branch 28 → 29 taken 105 times.
|
105 | const auto loader = previous ? m_previousPageLoader : m_pageLoader; |
| 440 | 105 | m_pagePending = true; | |
| 441 |
1/1✓ Branch 29 → 30 taken 105 times.
|
105 | const QPointer<MailListModel> guard(this); |
| 442 |
2/6✓ Branch 34 → 35 taken 105 times.
✓ Branch 35 → 36 taken 105 times.
✗ Branch 49 → 50 not taken.
✗ Branch 49 → 51 not taken.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
|
105 | loader(anchor, PageSize, [guard, generation, previous, anchor, completed = std::move(completed)] |
| 443 | (PageResult result) mutable { | ||
| 444 |
3/6✓ Branch 3 → 4 taken 96 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 96 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 96 times.
|
105 | if (!guard || generation != guard->m_pageGeneration) return; |
| 445 | 96 | auto &page = result.headers; | |
| 446 | 96 | auto &error = result.error; | |
| 447 | 96 | const auto cursor = result.cursor; | |
| 448 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 20 taken 96 times.
|
96 | if (page.size() > PageSize) error = QStringLiteral("Cached page exceeds its row budget"); |
| 449 |
6/6✓ Branch 20 → 21 taken 88 times.
✓ Branch 20 → 26 taken 8 times.
✓ Branch 21 → 22 taken 87 times.
✓ Branch 21 → 25 taken 1 time.
✓ Branch 27 → 28 taken 1 time.
✓ Branch 27 → 35 taken 95 times.
|
183 | if (!result.endOfData && (cursor.uid <= 0 || |
| 450 |
1/5✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 87 times.
✗ Branch 23 → 24 not taken.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
|
87 | (cursor.uid == anchor.uid && cursor.date == anchor.date))) |
| 451 | 1 | error = QStringLiteral("Cached page did not advance its anchor"); | |
| 452 |
2/2✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 50 taken 95 times.
|
96 | if (!error.isEmpty()) { |
| 453 | 1 | guard->m_pagePending = false; | |
| 454 | 1 | guard->m_pageFailed = true; | |
| 455 |
1/1✓ Branch 40 → 41 taken 1 time.
|
1 | emit guard->pageLoadFailed(error); |
| 456 |
3/7✓ Branch 42 → 43 taken 1 time.
✗ Branch 42 → 46 not taken.
✗ Branch 44 → 45 not taken.
✓ Branch 44 → 46 taken 1 time.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 49 taken 1 time.
✗ Branch 48 → 49 not taken.
|
1 | if (guard && completed) completed(); |
| 457 | 1 | return; | |
| 458 | } | ||
| 459 |
5/6✓ Branch 50 → 51 taken 8 times.
✓ Branch 50 → 54 taken 87 times.
✓ Branch 52 → 53 taken 8 times.
✗ Branch 52 → 54 not taken.
✓ Branch 55 → 56 taken 8 times.
✓ Branch 55 → 73 taken 87 times.
|
95 | if (result.endOfData && page.isEmpty()) { |
| 460 |
1/2✗ Branch 56 → 57 not taken.
✓ Branch 56 → 59 taken 8 times.
|
8 | if (previous) guard->m_previousPageLoader = {}; |
| 461 | else { | ||
| 462 | 8 | guard->m_pageLoader = {}; | |
| 463 |
1/2✗ Branch 63 → 64 not taken.
✓ Branch 63 → 68 taken 8 times.
|
8 | if (!guard->m_previousPageLoader) guard->m_totalHeaderCount = guard->m_headers.size(); |
| 464 | } | ||
| 465 | 8 | guard->m_pagePending = false; | |
| 466 |
1/3✗ Branch 70 → 71 not taken.
✓ Branch 70 → 72 taken 8 times.
✗ Branch 71 → 72 not taken.
|
8 | if (completed) completed(); |
| 467 | 8 | return; | |
| 468 | } | ||
| 469 |
2/2✓ Branch 73 → 74 taken 5 times.
✓ Branch 73 → 76 taken 82 times.
|
87 | if (previous) guard->m_previousPageCursor = cursor; |
| 470 | 82 | else guard->m_pageCursor = cursor; | |
| 471 | 87 | QList<MailHeader> fresh; | |
| 472 | 87 | QSet<MailKey> seen; | |
| 473 |
4/4✓ Branch 79 → 80 taken 87 times.
✓ Branch 80 → 81 taken 87 times.
✓ Branch 97 → 82 taken 15233 times.
✓ Branch 97 → 98 taken 87 times.
|
15320 | for (auto &header : page) { |
| 474 | 15233 | const MailKey key{header.folderId, header.uid}; | |
| 475 |
3/6✓ Branch 86 → 87 taken 15233 times.
✗ Branch 86 → 90 not taken.
✓ Branch 88 → 89 taken 15233 times.
✗ Branch 88 → 90 not taken.
✓ Branch 91 → 92 taken 15233 times.
✗ Branch 91 → 95 not taken.
|
15233 | if (!guard->m_uidIndex.contains(key) && !seen.contains(key)) { |
| 476 |
2/2✓ Branch 92 → 93 taken 15233 times.
✓ Branch 94 → 95 taken 15233 times.
|
15233 | seen.insert(key); fresh.append(std::move(header)); |
| 477 | } | ||
| 478 | } | ||
| 479 |
1/2✗ Branch 99 → 100 not taken.
✓ Branch 99 → 108 taken 87 times.
|
87 | if (fresh.isEmpty()) { |
| 480 | ✗ | guard->m_pagePending = false; | |
| 481 | ✗ | QTimer::singleShot(0, guard, [guard, generation, previous, completed = std::move(completed)]() mutable { | |
| 482 | ✗ | if (!guard || generation != guard->m_pageGeneration) return; | |
| 483 | ✗ | if (previous ? guard->canFetchPrevious() : guard->canFetchMore({})) | |
| 484 | ✗ | guard->requestPage(previous, std::move(completed)); | |
| 485 | ✗ | else if (completed) completed(); | |
| 486 | }); | ||
| 487 | ✗ | return; | |
| 488 | } | ||
| 489 |
2/2✓ Branch 108 → 109 taken 5 times.
✓ Branch 108 → 110 taken 82 times.
|
87 | const int first = previous ? 0 : int(guard->m_headers.size()); |
| 490 |
1/1✓ Branch 116 → 117 taken 87 times.
|
87 | guard->beginInsertRows({}, first, first + fresh.size() - 1); |
| 491 |
2/2✓ Branch 117 → 118 taken 5 times.
✓ Branch 117 → 137 taken 82 times.
|
87 | if (previous) { |
| 492 |
1/1✓ Branch 119 → 120 taken 5 times.
|
5 | guard->m_headers = fresh + guard->m_headers; |
| 493 |
5/5✓ Branch 123 → 124 taken 5 times.
✓ Branch 124 → 125 taken 5 times.
✓ Branch 127 → 128 taken 900 times.
✓ Branch 131 → 126 taken 900 times.
✓ Branch 131 → 132 taken 5 times.
|
905 | for (const auto &header : fresh) guard->m_retainedBytes += headerCharge(header); |
| 494 | 5 | const int unread = guard->m_unreadCount; | |
| 495 |
1/1✓ Branch 134 → 135 taken 5 times.
|
5 | guard->rebuildUidIndex(); |
| 496 | 5 | guard->m_unreadCount = unread; | |
| 497 | } else { | ||
| 498 |
1/1✓ Branch 138 → 139 taken 82 times.
|
82 | guard->m_headers.append(fresh); |
| 499 |
5/5✓ Branch 139 → 140 taken 82 times.
✓ Branch 140 → 141 taken 82 times.
✓ Branch 143 → 144 taken 14333 times.
✓ Branch 147 → 142 taken 14333 times.
✓ Branch 147 → 148 taken 82 times.
|
14415 | for (const auto &header : fresh) guard->m_retainedBytes += headerCharge(header); |
| 500 |
2/2✓ Branch 156 → 149 taken 14333 times.
✓ Branch 156 → 157 taken 82 times.
|
14415 | for (int i = 0; i < fresh.size(); ++i) |
| 501 |
3/3✓ Branch 150 → 151 taken 14333 times.
✓ Branch 151 → 152 taken 14333 times.
✓ Branch 153 → 154 taken 14333 times.
|
14333 | guard->m_uidIndex.insert({fresh[i].folderId, fresh[i].uid}, first + i); |
| 502 | } | ||
| 503 | 87 | guard->m_pagePending = false; | |
| 504 |
1/1✓ Branch 159 → 160 taken 87 times.
|
87 | guard->endInsertRows(); |
| 505 | // T-87.D1: slide the retained window so paging cannot grow the resident | ||
| 506 | // row set without bound. Only the side opposite the fetch direction is | ||
| 507 | // evicted, keeping the retained range contiguous and both cursors valid. | ||
| 508 |
1/1✓ Branch 161 → 162 taken 87 times.
|
87 | guard->evictDistantRetainedRows(!previous); |
| 509 |
7/9✓ Branch 163 → 164 taken 87 times.
✗ Branch 163 → 169 not taken.
✓ Branch 165 → 166 taken 87 times.
✗ Branch 165 → 169 not taken.
✓ Branch 167 → 168 taken 2 times.
✓ Branch 167 → 169 taken 85 times.
✓ Branch 170 → 171 taken 2 times.
✓ Branch 170 → 172 taken 85 times.
✓ Branch 171 → 172 taken 2 times.
|
87 | if (guard && generation == guard->m_pageGeneration && completed) completed(); |
| 510 |
4/6✓ Branch 174 → 175 taken 87 times.
✗ Branch 174 → 176 not taken.
✓ Branch 178 → 179 taken 87 times.
✗ Branch 178 → 180 not taken.
✓ Branch 182 → 183 taken 87 times.
✓ Branch 182 → 185 taken 9 times.
|
96 | }); |
| 511 | 105 | } | |
| 512 | |||
| 513 | // Conservative decoded-size charge mirroring the writer's header-batch | ||
| 514 | // budget: decoded text at UTF-16 width plus container entries. | ||
| 515 | 68451 | qint64 MailListModel::headerCharge(const MailHeader &header) { | |
| 516 | 68451 | qint64 bytes = 256 + 2LL * (header.subject.size() + header.from.size() + | |
| 517 | 68451 | header.to.size() + header.messageId.size() + | |
| 518 | 68451 | header.inReplyTo.size()); | |
| 519 |
2/2✓ Branch 20 → 12 taken 205353 times.
✓ Branch 20 → 21 taken 68451 times.
|
479157 | for (const auto &list : {header.labels, header.references, header.inReplyToIds}) |
| 520 |
2/2✓ Branch 18 → 14 taken 4929 times.
✓ Branch 18 → 19 taken 205353 times.
|
210282 | for (const auto &value : list) |
| 521 |
2/2✓ Branch 22 → 23 taken 205353 times.
✓ Branch 22 → 26 taken 68451 times.
|
278733 | bytes += 32 + 2LL * value.size(); |
| 522 | 68451 | return bytes; | |
| 523 | } | ||
| 524 | |||
| 525 | 87 | void MailListModel::evictDistantRetainedRows(bool fetchedForward) { | |
| 526 | // Never evict a side that cannot be re-fetched afterwards: an exhausted | ||
| 527 | // (cleared) loader would strand the window's edge permanently. | ||
| 528 |
4/6✓ Branch 2 → 3 taken 82 times.
✓ Branch 2 → 6 taken 5 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 82 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 87 times.
|
169 | if (fetchedForward && !m_previousPageLoader) return; |
| 529 |
6/6✓ Branch 9 → 10 taken 5 times.
✓ Branch 9 → 13 taken 82 times.
✓ Branch 11 → 12 taken 2 times.
✓ Branch 11 → 13 taken 3 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 85 times.
|
87 | if (!fetchedForward && !m_pageLoader) return; |
| 530 | 85 | qint64 freed = 0; | |
| 531 | 85 | int evict = 0; | |
| 532 | 85 | const int rows = m_headers.size(); | |
| 533 |
2/2✓ Branch 23 → 24 taken 1015 times.
✓ Branch 23 → 26 taken 70 times.
|
1085 | while (rows - evict > MinimumRetainedRows && |
| 534 |
3/4✓ Branch 24 → 18 taken 1000 times.
✓ Branch 24 → 25 taken 15 times.
✗ Branch 25 → 18 not taken.
✓ Branch 25 → 26 taken 15 times.
|
1015 | (rows - evict > MaximumRetainedRows || m_retainedBytes - freed > MaximumRetainedBytes)) { |
| 535 |
3/3✓ Branch 18 → 19 taken 600 times.
✓ Branch 18 → 20 taken 400 times.
✓ Branch 21 → 22 taken 1000 times.
|
1400 | freed += headerCharge(fetchedForward ? m_headers.at(evict) |
| 536 | 400 | : m_headers.at(rows - 1 - evict)); | |
| 537 | 1000 | ++evict; | |
| 538 | } | ||
| 539 |
2/2✓ Branch 26 → 27 taken 80 times.
✓ Branch 26 → 28 taken 5 times.
|
85 | if (evict <= 0) return; |
| 540 | 5 | const QScopedValueRollback<bool> eviction(m_evictingRetainedRows, true); | |
| 541 | 5 | const int unread = m_unreadCount; // folder totals stay authoritative in paged mode | |
| 542 |
2/2✓ Branch 29 → 30 taken 3 times.
✓ Branch 29 → 45 taken 2 times.
|
5 | if (fetchedForward) { |
| 543 |
1/1✓ Branch 30 → 31 taken 3 times.
|
3 | emit windowSlid(0, evict - 1); |
| 544 |
1/1✓ Branch 32 → 33 taken 3 times.
|
3 | beginRemoveRows({}, 0, evict - 1); |
| 545 |
4/4✓ Branch 33 → 34 taken 3 times.
✓ Branch 34 → 35 taken 3 times.
✓ Branch 36 → 37 taken 3 times.
✓ Branch 38 → 39 taken 3 times.
|
3 | m_headers.erase(m_headers.begin(), m_headers.begin() + evict); |
| 546 | 3 | m_retainedBytes -= freed; | |
| 547 |
1/1✓ Branch 39 → 40 taken 3 times.
|
3 | rebuildUidIndex(); |
| 548 | 3 | m_unreadCount = unread; | |
| 549 | 3 | m_previousPageCursor = PageCursor(m_headers.constFirst()); | |
| 550 |
1/1✓ Branch 44 → 64 taken 3 times.
|
3 | endRemoveRows(); |
| 551 | } else { | ||
| 552 |
1/1✓ Branch 47 → 48 taken 2 times.
|
2 | emit windowSlid(m_headers.size() - evict, m_headers.size() - 1); |
| 553 |
1/1✓ Branch 51 → 52 taken 2 times.
|
2 | beginRemoveRows({}, m_headers.size() - evict, m_headers.size() - 1); |
| 554 |
4/4✓ Branch 52 → 53 taken 2 times.
✓ Branch 54 → 55 taken 2 times.
✓ Branch 55 → 56 taken 2 times.
✓ Branch 57 → 58 taken 2 times.
|
2 | m_headers.erase(m_headers.end() - evict, m_headers.end()); |
| 555 | 2 | m_retainedBytes -= freed; | |
| 556 |
1/1✓ Branch 58 → 59 taken 2 times.
|
2 | rebuildUidIndex(); |
| 557 | 2 | m_unreadCount = unread; | |
| 558 | 2 | m_pageCursor = PageCursor(m_headers.constLast()); | |
| 559 |
1/1✓ Branch 63 → 64 taken 2 times.
|
2 | endRemoveRows(); |
| 560 | } | ||
| 561 |
1/2✗ Branch 64 → 65 not taken.
✓ Branch 64 → 81 taken 5 times.
|
5 | if (m_retainedBytes > MaximumRetainedBytes) |
| 562 | ✗ | qCWarning(lcMailModel) << "Retained window exceeds its byte budget at the liveness floor:" | |
| 563 | ✗ | << m_retainedBytes << "bytes in" << m_headers.size() << "rows"; | |
| 564 | else | ||
| 565 |
7/7✓ Branch 81 → 82 taken 5 times.
✓ Branch 85 → 86 taken 5 times.
✓ Branch 86 → 87 taken 5 times.
✓ Branch 87 → 88 taken 5 times.
✓ Branch 88 → 89 taken 5 times.
✓ Branch 97 → 83 taken 5 times.
✓ Branch 97 → 98 taken 5 times.
|
10 | qCInfo(lcMailModel) << "Retained window slid by" << evict << "rows;" |
| 566 |
4/4✓ Branch 90 → 91 taken 5 times.
✓ Branch 91 → 92 taken 5 times.
✓ Branch 92 → 93 taken 5 times.
✓ Branch 93 → 94 taken 5 times.
|
5 | << m_headers.size() << "rows," << m_retainedBytes << "bytes retained"; |
| 567 | 5 | } | |
| 568 | |||
| 569 | 33 | void MailListModel::appendHeaders(const QList<MailHeader> &headers) { | |
| 570 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 33 times.
|
33 | if (headers.isEmpty()) |
| 571 | ✗ | return; | |
| 572 | |||
| 573 |
1/1✓ Branch 9 → 10 taken 33 times.
|
33 | beginInsertRows({}, m_headers.size(), m_headers.size() + headers.size() - 1); |
| 574 | 33 | int baseRow = m_headers.size(); | |
| 575 | 33 | m_headers.append(headers); | |
| 576 | 33 | m_totalHeaderCount += headers.size(); | |
| 577 |
3/3✓ Branch 16 → 17 taken 1705 times.
✓ Branch 19 → 15 taken 1705 times.
✓ Branch 19 → 20 taken 33 times.
|
1738 | for (const auto &header : headers) m_retainedBytes += headerCharge(header); |
| 578 | // T-514: extend composite UID index for new rows | ||
| 579 |
2/2✓ Branch 26 → 21 taken 1705 times.
✓ Branch 26 → 27 taken 33 times.
|
1738 | for (int i = 0; i < headers.size(); ++i) { |
| 580 | 1705 | const auto &hdr = headers.at(i); | |
| 581 |
1/1✓ Branch 23 → 24 taken 1705 times.
|
1705 | m_uidIndex.insert(MailKey{hdr.folderId, hdr.uid}, baseRow + i); |
| 582 | } | ||
| 583 | // T-116: update unread count for new headers | ||
| 584 |
2/2✓ Branch 35 → 29 taken 1705 times.
✓ Branch 35 → 36 taken 33 times.
|
1738 | for (const auto &h : headers) { |
| 585 |
2/2✓ Branch 31 → 32 taken 324 times.
✓ Branch 31 → 33 taken 1381 times.
|
1705 | if (!h.isSeen()) |
| 586 | 324 | ++m_unreadCount; | |
| 587 | } | ||
| 588 | 33 | endInsertRows(); | |
| 589 | } | ||
| 590 | |||
| 591 | 8 | int MailListModel::appendStreamedHeaders(const QList<MailHeader> &headers) { | |
| 592 | 8 | const int rowBudget = qMax(0, MaximumRetainedRows - int(m_headers.size())); | |
| 593 | 8 | int accepted = 0; | |
| 594 | 8 | qint64 bytes = m_retainedBytes; | |
| 595 |
2/2✓ Branch 19 → 6 taken 16 times.
✓ Branch 19 → 20 taken 8 times.
|
24 | for (const auto &header : headers) { |
| 596 |
1/2✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 16 times.
|
16 | if (accepted >= rowBudget) break; |
| 597 |
1/1✓ Branch 9 → 10 taken 16 times.
|
16 | const qint64 charge = headerCharge(header); |
| 598 | // Keep the same liveness floor as page eviction: unusually large | ||
| 599 | // headers must not leave a short, still-visible list empty. | ||
| 600 |
2/4✗ Branch 10 → 11 not taken.
✓ Branch 10 → 14 taken 16 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 16 times.
|
16 | if (bytes + charge > MaximumRetainedBytes && |
| 601 | ✗ | m_headers.size() + accepted >= MinimumRetainedRows) break; | |
| 602 | 16 | bytes += charge; | |
| 603 | 16 | ++accepted; | |
| 604 | } | ||
| 605 |
3/4✓ Branch 20 → 21 taken 8 times.
✗ Branch 20 → 25 not taken.
✓ Branch 21 → 22 taken 8 times.
✓ Branch 22 → 23 taken 8 times.
|
8 | if (accepted > 0) insertStreamedHeaders(headers.first(accepted)); |
| 606 | 8 | return accepted; | |
| 607 | } | ||
| 608 | |||
| 609 | // Streamed batches arrive in fetch order (newest chunk first, ascending UIDs | ||
| 610 | // inside a chunk). Inserting each run at its page-order position keeps the | ||
| 611 | // window sorted like a cached page, so the rows neither reorder at the end of | ||
| 612 | // a sync nor force a model reset when the first page is refreshed. | ||
| 613 | 8 | void MailListModel::insertStreamedHeaders(QList<MailHeader> batch) { | |
| 614 | 8 | markKeptWindowChanged(); | |
| 615 | // The cache page order of the current sort (MailSort mirrors the SQL). | ||
| 616 | 45 | const auto pageOrderBefore = [this](const MailHeader &left, const MailHeader &right) { | |
| 617 | 45 | return MailSort::listedBefore(left, right, m_sort); | |
| 618 | 8 | }; | |
| 619 |
3/3✓ Branch 3 → 4 taken 8 times.
✓ Branch 4 → 5 taken 8 times.
✓ Branch 5 → 6 taken 8 times.
|
8 | std::stable_sort(batch.begin(), batch.end(), pageOrderBefore); |
| 620 | 8 | qsizetype from = 0; | |
| 621 |
2/2✓ Branch 47 → 7 taken 10 times.
✓ Branch 47 → 48 taken 8 times.
|
18 | while (from < batch.size()) { |
| 622 |
1/1✓ Branch 11 → 12 taken 10 times.
|
10 | const qsizetype position = std::upper_bound(m_headers.cbegin(), m_headers.cend(), |
| 623 | 20 | batch.at(from), pageOrderBefore) - m_headers.cbegin(); | |
| 624 | 10 | qsizetype to = from + 1; | |
| 625 |
6/6✓ Branch 16 → 17 taken 8 times.
✓ Branch 16 → 24 taken 8 times.
✓ Branch 18 → 19 taken 5 times.
✓ Branch 18 → 23 taken 3 times.
✓ Branch 25 → 14 taken 6 times.
✓ Branch 25 → 26 taken 10 times.
|
29 | while (to < batch.size() && |
| 626 |
3/3✓ Branch 21 → 22 taken 5 times.
✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 24 taken 2 times.
|
13 | (position >= m_headers.size() || pageOrderBefore(batch.at(to), m_headers.at(position)))) |
| 627 | 6 | ++to; | |
| 628 | 10 | const qsizetype count = to - from; | |
| 629 |
1/1✓ Branch 27 → 28 taken 10 times.
|
10 | beginInsertRows({}, int(position), int(position + count - 1)); |
| 630 |
2/2✓ Branch 36 → 29 taken 16 times.
✓ Branch 36 → 37 taken 10 times.
|
26 | for (qsizetype i = 0; i < count; ++i) { |
| 631 | 16 | const auto &header = batch.at(from + i); | |
| 632 |
1/1✓ Branch 30 → 31 taken 16 times.
|
16 | m_headers.insert(position + i, header); |
| 633 |
1/1✓ Branch 31 → 32 taken 16 times.
|
16 | m_retainedBytes += headerCharge(header); |
| 634 |
1/2✓ Branch 33 → 34 taken 16 times.
✗ Branch 33 → 35 not taken.
|
16 | if (!header.isSeen()) ++m_unreadCount; |
| 635 | } | ||
| 636 | 10 | m_totalHeaderCount += int(count); | |
| 637 |
2/2✓ Branch 43 → 38 taken 56 times.
✓ Branch 43 → 44 taken 10 times.
|
66 | for (qsizetype row = position; row < m_headers.size(); ++row) { |
| 638 | 56 | const auto &header = m_headers.at(row); | |
| 639 |
1/1✓ Branch 40 → 41 taken 56 times.
|
56 | m_uidIndex.insert(MailKey{header.folderId, header.uid}, int(row)); |
| 640 | } | ||
| 641 |
1/1✓ Branch 44 → 45 taken 10 times.
|
10 | endInsertRows(); |
| 642 | 10 | from = to; | |
| 643 | } | ||
| 644 | 8 | } | |
| 645 | |||
| 646 | 32 | void MailListModel::keepUnfilteredWindow() { | |
| 647 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 32 times.
|
32 | if (m_unfilteredWindow) return; |
| 648 |
0/4✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
|
64 | m_unfilteredWindow = WindowState{m_headers, m_totalHeaderCount, m_unreadCount, |
| 649 |
2/2✓ Branch 6 → 7 taken 32 times.
✓ Branch 7 → 8 taken 32 times.
|
32 | m_pageLoader, m_previousPageLoader, |
| 650 | 32 | m_pageCursor, m_previousPageCursor}; | |
| 651 | } | ||
| 652 | |||
| 653 | 24 | bool MailListModel::restoreUnfilteredWindow(bool *changed) { | |
| 654 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 24 times.
|
24 | if (!m_unfilteredWindow) return false; |
| 655 | 24 | auto saved = std::move(*m_unfilteredWindow); | |
| 656 |
1/2✓ Branch 8 → 9 taken 24 times.
✗ Branch 8 → 10 not taken.
|
24 | if (changed) *changed = saved.dataChanged; |
| 657 | 24 | m_unfilteredWindow.reset(); | |
| 658 |
1/2✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 24 times.
|
24 | if (saved.headers.isEmpty()) return false; |
| 659 | 24 | ++m_pageGeneration; | |
| 660 | 24 | m_pagePending = false; | |
| 661 | 24 | m_pageFailed = false; | |
| 662 |
6/7✓ Branch 15 → 16 taken 14 times.
✓ Branch 15 → 18 taken 10 times.
✓ Branch 16 → 17 taken 14 times.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 14 times.
✓ Branch 20 → 21 taken 10 times.
✓ Branch 20 → 33 taken 14 times.
|
24 | if (m_headers.isEmpty() || !applyPageDiff(saved.headers)) { |
| 663 |
1/1✓ Branch 21 → 22 taken 10 times.
|
10 | beginResetModel(); |
| 664 | 10 | m_headers = saved.headers; | |
| 665 | 10 | m_uidIndex.clear(); | |
| 666 |
2/2✓ Branch 31 → 25 taken 27 times.
✓ Branch 31 → 32 taken 10 times.
|
37 | for (int i = 0; i < m_headers.size(); ++i) |
| 667 |
1/1✓ Branch 28 → 29 taken 27 times.
|
27 | m_uidIndex.insert(MailKey{m_headers.at(i).folderId, m_headers.at(i).uid}, i); |
| 668 |
1/1✓ Branch 32 → 33 taken 10 times.
|
10 | endResetModel(); |
| 669 | } | ||
| 670 | 24 | m_totalHeaderCount = saved.totalCount; | |
| 671 | 24 | resetUnread(saved.unreadCount); | |
| 672 | 24 | m_pageLoader = std::move(saved.pageLoader); | |
| 673 | 24 | m_previousPageLoader = std::move(saved.previousPageLoader); | |
| 674 | 24 | m_pageCursor = saved.pageCursor; | |
| 675 | 24 | m_previousPageCursor = saved.previousPageCursor; | |
| 676 | 24 | m_suggestionCache.clear(); | |
| 677 | 24 | m_retainedBytes = 0; | |
| 678 |
5/5✓ Branch 41 → 42 taken 24 times.
✓ Branch 42 → 43 taken 24 times.
✓ Branch 45 → 46 taken 72 times.
✓ Branch 48 → 44 taken 72 times.
✓ Branch 48 → 49 taken 24 times.
|
96 | for (const auto &header : m_headers) m_retainedBytes += headerCharge(header); |
| 679 | 24 | return true; | |
| 680 | 24 | } | |
| 681 | |||
| 682 | 1049 | void MailListModel::clear() { | |
| 683 | 1049 | m_unfilteredWindow.reset(); | |
| 684 | 1049 | ++m_pageGeneration; | |
| 685 | 1049 | m_pagePending = false; | |
| 686 | 1049 | m_pageFailed = false; | |
| 687 | 1049 | m_previousPageCursor = {}; | |
| 688 | 1049 | beginResetModel(); | |
| 689 | 1049 | m_headers.clear(); | |
| 690 | 1049 | m_uidIndex.clear(); | |
| 691 | 1049 | resetUnread(0); | |
| 692 | 1049 | m_totalHeaderCount = 0; | |
| 693 | 1049 | m_pageLoader = {}; | |
| 694 | 1049 | m_previousPageLoader = {}; | |
| 695 | 1049 | m_pageCursor = {}; | |
| 696 | 1049 | m_suggestionCache.clear(); // T-232 | |
| 697 | 1049 | m_retainedBytes = 0; | |
| 698 | 1049 | endResetModel(); | |
| 699 | 1049 | } | |
| 700 | |||
| 701 | 758 | void MailListModel::updateFlags(qint64 uid, quint32 newFlags, qint64 folderId) { | |
| 702 | 758 | markKeptWindowChanged(); | |
| 703 | 758 | int row = rowForUid(uid, folderId); | |
| 704 |
2/2✓ Branch 4 → 5 taken 74 times.
✓ Branch 4 → 6 taken 684 times.
|
758 | if (row < 0) |
| 705 | 87 | return; | |
| 706 |
2/2✓ Branch 7 → 8 taken 13 times.
✓ Branch 7 → 9 taken 671 times.
|
684 | if (m_headers.at(row).flags == newFlags) |
| 707 | 13 | return; | |
| 708 | |||
| 709 | // T-116: adjust unread count based on Seen flag transition | ||
| 710 | 671 | bool wasSeen = m_headers.at(row).isSeen(); | |
| 711 | // Only the bitmask changes; keyword labels remain on the loaded header. | ||
| 712 |
1/1✓ Branch 11 → 12 taken 671 times.
|
671 | m_headers[row].flags = newFlags; |
| 713 | 671 | bool nowSeen = m_headers.at(row).isSeen(); | |
| 714 |
4/4✓ Branch 14 → 15 taken 431 times.
✓ Branch 14 → 17 taken 240 times.
✓ Branch 15 → 16 taken 30 times.
✓ Branch 15 → 17 taken 401 times.
|
671 | if (wasSeen && !nowSeen) |
| 715 | 30 | ++m_unreadCount; | |
| 716 |
4/4✓ Branch 17 → 18 taken 240 times.
✓ Branch 17 → 20 taken 401 times.
✓ Branch 18 → 19 taken 111 times.
✓ Branch 18 → 20 taken 129 times.
|
641 | else if (!wasSeen && nowSeen) |
| 717 | 111 | --m_unreadCount; | |
| 718 | |||
| 719 |
1/1✓ Branch 21 → 22 taken 671 times.
|
671 | auto topLeft = index(row, 0); |
| 720 |
1/1✓ Branch 23 → 24 taken 671 times.
|
671 | auto bottomRight = index(row, ColumnCount - 1); |
| 721 |
2/2✓ Branch 24 → 25 taken 671 times.
✓ Branch 25 → 26 taken 671 times.
|
671 | emit dataChanged(topLeft, bottomRight, |
| 722 | {Qt::FontRole, Qt::DisplayRole, Qt::DecorationRole, | ||
| 723 | FlagsRole}); | ||
| 724 | } | ||
| 725 | |||
| 726 | 10 | void MailListModel::setHasAttachments(qint64 uid, qint64 folderId, bool has) { | |
| 727 | 10 | markKeptWindowChanged(); | |
| 728 | 10 | int row = rowForUid(uid, folderId); | |
| 729 |
2/2✓ Branch 4 → 5 taken 5 times.
✓ Branch 4 → 6 taken 5 times.
|
10 | if (row < 0) |
| 730 | 5 | return; | |
| 731 |
2/2✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 3 times.
|
5 | if (m_headers.at(row).hasAttachments == has) |
| 732 | 2 | return; | |
| 733 | 3 | m_headers[row].hasAttachments = has; | |
| 734 |
4/4✓ Branch 10 → 11 taken 3 times.
✓ Branch 12 → 13 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
|
3 | emit dataChanged(index(row, 0), index(row, ColumnCount - 1), |
| 735 | {HasAttachmentsRole}); | ||
| 736 | } | ||
| 737 | |||
| 738 | 271 | void MailListModel::removeByUid(qint64 uid, qint64 folderId) { | |
| 739 | 271 | markKeptWindowChanged(); | |
| 740 | 271 | int row = rowForUid(uid, folderId); | |
| 741 |
2/2✓ Branch 4 → 5 taken 13 times.
✓ Branch 4 → 6 taken 258 times.
|
271 | if (row < 0) |
| 742 | 13 | return; | |
| 743 | |||
| 744 | // T-116: adjust unread count if removed mail was unread | ||
| 745 |
2/2✓ Branch 8 → 9 taken 70 times.
✓ Branch 8 → 10 taken 188 times.
|
258 | if (!m_headers.at(row).isSeen()) |
| 746 | 70 | --m_unreadCount; | |
| 747 | 258 | m_totalHeaderCount = std::max(0, m_totalHeaderCount - 1); | |
| 748 | 258 | m_retainedBytes -= headerCharge(m_headers.at(row)); | |
| 749 | 258 | const int authoritativeUnread = m_unreadCount; | |
| 750 | 258 | const bool paged = static_cast<bool>(m_pageLoader); | |
| 751 | |||
| 752 |
1/1✓ Branch 15 → 16 taken 258 times.
|
258 | beginRemoveRows({}, row, row); |
| 753 | 258 | m_headers.removeAt(row); | |
| 754 | |||
| 755 | // T-115: rebuild index from scratch (rows shift after remove) | ||
| 756 | // Cost: O(n), but removeByUid is rare compared to rowForUid | ||
| 757 | 258 | rebuildUidIndex(); | |
| 758 |
2/2✓ Branch 18 → 19 taken 131 times.
✓ Branch 18 → 20 taken 127 times.
|
258 | if (paged) |
| 759 | 131 | m_unreadCount = authoritativeUnread; | |
| 760 | |||
| 761 | 258 | endRemoveRows(); | |
| 762 | } | ||
| 763 | |||
| 764 | // T-525/Cx8: Batch remove — single reset instead of per-UID rebuild | ||
| 765 | 10 | void MailListModel::removeByUids(const QList<qint64> &uids, qint64 folderId) { | |
| 766 | 10 | markKeptWindowChanged(); | |
| 767 |
2/2✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 9 times.
|
10 | if (uids.isEmpty()) |
| 768 | 1 | return; | |
| 769 | |||
| 770 |
1/1✓ Branch 8 → 9 taken 9 times.
|
9 | QSet<qint64> removeSet(uids.begin(), uids.end()); |
| 771 | 9 | const bool paged = static_cast<bool>(m_pageLoader); | |
| 772 | 9 | const int oldSize = m_headers.size(); | |
| 773 | 9 | int removedUnread = 0; | |
| 774 |
2/2✓ Branch 26 → 14 taken 1064 times.
✓ Branch 26 → 27 taken 9 times.
|
1073 | for (const MailHeader &header : std::as_const(m_headers)) { |
| 775 |
5/6✓ Branch 15 → 16 taken 1064 times.
✗ Branch 15 → 21 not taken.
✓ Branch 17 → 18 taken 114 times.
✓ Branch 17 → 21 taken 950 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 24 taken 1062 times.
|
1178 | if (header.folderId == folderId && removeSet.contains(header.uid) && |
| 776 |
2/2✓ Branch 19 → 20 taken 2 times.
✓ Branch 19 → 21 taken 112 times.
|
114 | !header.isSeen()) |
| 777 | 2 | ++removedUnread; | |
| 778 | } | ||
| 779 | 9 | const int authoritativeUnread = m_unreadCount - removedUnread; | |
| 780 |
1/1✓ Branch 27 → 28 taken 9 times.
|
9 | beginResetModel(); |
| 781 |
2/2✓ Branch 42 → 31 taken 1064 times.
✓ Branch 42 → 43 taken 9 times.
|
1073 | for (const MailHeader &header : std::as_const(m_headers)) { |
| 782 |
5/6✓ Branch 32 → 33 taken 1064 times.
✗ Branch 32 → 36 not taken.
✓ Branch 34 → 35 taken 114 times.
✓ Branch 34 → 36 taken 950 times.
✓ Branch 37 → 38 taken 114 times.
✓ Branch 37 → 40 taken 950 times.
|
1064 | if (header.folderId == folderId && removeSet.contains(header.uid)) |
| 783 |
1/1✓ Branch 38 → 39 taken 114 times.
|
114 | m_retainedBytes -= headerCharge(header); |
| 784 | } | ||
| 785 |
5/5✓ Branch 43 → 44 taken 9 times.
✓ Branch 45 → 46 taken 9 times.
✓ Branch 46 → 47 taken 9 times.
✓ Branch 47 → 48 taken 9 times.
✓ Branch 49 → 50 taken 9 times.
|
9 | m_headers.erase( |
| 786 | std::remove_if(m_headers.begin(), m_headers.end(), | ||
| 787 | 1064 | [&removeSet, folderId](const MailHeader &h) { | |
| 788 |
3/4✓ Branch 2 → 3 taken 1064 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 114 times.
✓ Branch 4 → 6 taken 950 times.
|
2128 | return h.folderId == folderId && |
| 789 | 2128 | removeSet.contains(h.uid); | |
| 790 | }), | ||
| 791 | m_headers.end()); | ||
| 792 |
1/1✓ Branch 50 → 51 taken 9 times.
|
9 | rebuildUidIndex(); |
| 793 | 9 | const int removed = oldSize - m_headers.size(); | |
| 794 | 9 | m_totalHeaderCount = std::max(0, m_totalHeaderCount - removed); | |
| 795 |
2/2✓ Branch 53 → 54 taken 7 times.
✓ Branch 53 → 55 taken 2 times.
|
9 | if (paged) { |
| 796 | // Cache/controller authoritative badge updates follow bulk removals; keep | ||
| 797 | // the pre-reset total until that signal arrives instead of replacing it | ||
| 798 | // with the loaded-page count. | ||
| 799 | 7 | m_unreadCount = authoritativeUnread; | |
| 800 | } | ||
| 801 |
1/1✓ Branch 55 → 56 taken 9 times.
|
9 | endResetModel(); |
| 802 | 9 | } | |
| 803 | |||
| 804 | 1591938 | const MailHeader *MailListModel::headerAt(int row) const { | |
| 805 |
5/6✓ Branch 2 → 3 taken 1591897 times.
✓ Branch 2 → 5 taken 41 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 1591897 times.
✓ Branch 7 → 8 taken 41 times.
✓ Branch 7 → 9 taken 1591897 times.
|
1591938 | if (row < 0 || row >= m_headers.size()) |
| 806 | 41 | return nullptr; | |
| 807 | 1591897 | return &m_headers.at(row); | |
| 808 | } | ||
| 809 | |||
| 810 | 62 | MailHeader *MailListModel::mutableHeaderAt(int row) { | |
| 811 |
5/6✓ Branch 2 → 3 taken 47 times.
✓ Branch 2 → 5 taken 15 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 47 times.
✓ Branch 7 → 8 taken 15 times.
✓ Branch 7 → 9 taken 47 times.
|
62 | if (row < 0 || row >= m_headers.size()) |
| 812 | 15 | return nullptr; | |
| 813 | 47 | return &m_headers[row]; | |
| 814 | } | ||
| 815 | |||
| 816 | 39 | qint64 MailListModel::uidAt(int row) const { | |
| 817 | 39 | auto *h = headerAt(row); | |
| 818 |
1/2✓ Branch 3 → 4 taken 39 times.
✗ Branch 3 → 5 not taken.
|
39 | return h ? h->uid : -1; |
| 819 | } | ||
| 820 | |||
| 821 | 32735 | int MailListModel::rowForUid(qint64 uid, qint64 folderId) const { | |
| 822 | // T-514: O(1) composite hash lookup | ||
| 823 | 32735 | auto it = m_uidIndex.constFind(MailKey{folderId, uid}); | |
| 824 |
2/2✓ Branch 6 → 7 taken 30177 times.
✓ Branch 6 → 9 taken 2558 times.
|
32735 | return it != m_uidIndex.constEnd() ? it.value() : -1; |
| 825 | } | ||
| 826 | |||
| 827 | 2421 | std::optional<quint32> MailListModel::flagsFor(qint64 uid, qint64 folderId) const { | |
| 828 | 2421 | const int row = rowForUid(uid, folderId); | |
| 829 |
2/2✓ Branch 3 → 4 taken 352 times.
✓ Branch 3 → 7 taken 2069 times.
|
2421 | if (row < 0) return std::nullopt; |
| 830 | 2069 | const auto *header = headerAt(row); | |
| 831 |
1/2✓ Branch 8 → 9 taken 2069 times.
✗ Branch 8 → 10 not taken.
|
2069 | return header ? std::optional<quint32>(header->flags) : std::nullopt; |
| 832 | } | ||
| 833 | |||
| 834 | 237 | int MailListModel::unreadCount() const { | |
| 835 | // T-116: O(1) cached count | ||
| 836 | 237 | return m_unreadCount; | |
| 837 | } | ||
| 838 | |||
| 839 | 759 | void MailListModel::rebuildUidIndex() { | |
| 840 | 759 | m_uidIndex.clear(); | |
| 841 | 759 | m_uidIndex.reserve(m_headers.size()); | |
| 842 | 759 | m_unreadCount = 0; | |
| 843 |
2/2✓ Branch 14 → 6 taken 130885 times.
✓ Branch 14 → 15 taken 759 times.
|
131644 | for (int i = 0; i < m_headers.size(); ++i) { |
| 844 | 130885 | const auto &h = m_headers.at(i); | |
| 845 |
1/1✓ Branch 8 → 9 taken 130885 times.
|
130885 | m_uidIndex.insert(MailKey{h.folderId, h.uid}, i); |
| 846 |
2/2✓ Branch 10 → 11 taken 44177 times.
✓ Branch 10 → 12 taken 86708 times.
|
130885 | if (!h.isSeen()) |
| 847 | 44177 | ++m_unreadCount; | |
| 848 | } | ||
| 849 | 759 | } | |
| 850 | |||
| 851 | // 67.B4: humanized date column (DESIGN.md "MailList date format"), with | ||
| 852 | // the time since F100-52: today "14:32" · yesterday "Yesterday 14:32" · | ||
| 853 | // this week "Mon 14:32" · this year "12. May 14:32" · older "12.05.2024" | ||
| 854 | 40702 | QString MailListModel::formatDate(const QDateTime &dt) { | |
| 855 |
3/3✓ Branch 2 → 3 taken 40702 times.
✓ Branch 3 → 4 taken 56 times.
✓ Branch 3 → 5 taken 40646 times.
|
40702 | if (!dt.isValid()) |
| 856 | 56 | return {}; | |
| 857 | |||
| 858 | // F3.5: headers are stored in server (often UTC) time; format in local time | ||
| 859 | // so a same-day message shows the local HH:mm instead of a stale date, and | ||
| 860 | // the list matches the reader's local-time header. | ||
| 861 |
1/1✓ Branch 5 → 6 taken 40646 times.
|
40646 | const QDateTime local = dt.toLocalTime(); |
| 862 |
1/1✓ Branch 6 → 7 taken 40646 times.
|
40646 | const QDate today = QDate::currentDate(); |
| 863 |
1/1✓ Branch 7 → 8 taken 40646 times.
|
40646 | const QDate date = local.date(); |
| 864 | |||
| 865 |
2/2✓ Branch 9 → 10 taken 196 times.
✓ Branch 9 → 19 taken 40450 times.
|
40646 | if (date == today) |
| 866 |
2/2✓ Branch 10 → 11 taken 196 times.
✓ Branch 14 → 15 taken 196 times.
|
392 | return local.time().toString(QStringLiteral("HH:mm")); |
| 867 |
2/2✓ Branch 19 → 20 taken 40450 times.
✓ Branch 23 → 24 taken 40450 times.
|
80900 | const QString time = local.time().toString(QStringLiteral("HH:mm")); |
| 868 |
3/3✓ Branch 26 → 27 taken 40450 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 38 taken 40449 times.
|
40450 | if (date == today.addDays(-1)) |
| 869 |
3/3✓ Branch 31 → 32 taken 1 time.
✓ Branch 32 → 33 taken 1 time.
✓ Branch 33 → 34 taken 1 time.
|
1 | return tr("Yesterday") + QLatin1Char(' ') + time; |
| 870 |
3/7✓ Branch 38 → 39 taken 40449 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 46 taken 40449 times.
✗ Branch 44 → 45 not taken.
✗ Branch 44 → 46 not taken.
✗ Branch 47 → 48 not taken.
✓ Branch 47 → 60 taken 40449 times.
|
40449 | if (date > today.addDays(-7) && date < today) |
| 871 | ✗ | return QLocale().dayName(date.dayOfWeek(), QLocale::ShortFormat) + QLatin1Char(' ') + time; | |
| 872 |
4/4✓ Branch 60 → 61 taken 40449 times.
✓ Branch 61 → 62 taken 40449 times.
✓ Branch 62 → 63 taken 26051 times.
✓ Branch 62 → 79 taken 14398 times.
|
40449 | if (date.year() == today.year()) |
| 873 |
4/4✓ Branch 65 → 66 taken 26051 times.
✓ Branch 69 → 70 taken 26051 times.
✓ Branch 70 → 71 taken 26051 times.
✓ Branch 71 → 72 taken 26051 times.
|
52102 | return QLocale().toString(date, QStringLiteral("d. MMM")) + QLatin1Char(' ') + time; |
| 874 |
1/1✓ Branch 82 → 83 taken 14398 times.
|
14398 | return date.toString(QStringLiteral("dd.MM.yyyy")); |
| 875 | 40646 | } | |
| 876 | |||
| 877 | ✗ | QString MailListModel::formatSize(qint64 bytes) { | |
| 878 | ✗ | if (bytes < 1024) | |
| 879 | ✗ | return QString::number(bytes) + " B"; | |
| 880 | ✗ | if (bytes < 1024 * 1024) | |
| 881 | ✗ | return QString::number(bytes / 1024.0, 'f', 1) + " KB"; | |
| 882 | ✗ | return QString::number(bytes / (1024.0 * 1024.0), 'f', 1) + " MB"; | |
| 883 | } | ||
| 884 | |||
| 885 | // T-232: Set suggestion text + confidence for a specific UID | ||
| 886 | ✗ | void MailListModel::setSuggestion(qint64 uid, qint64 folderId, | |
| 887 | const QString &text, | ||
| 888 | double confidence) { | ||
| 889 | ✗ | m_suggestionCache[MailKey{folderId, uid}] = {text, confidence}; | |
| 890 | ✗ | int row = rowForUid(uid, folderId); | |
| 891 | ✗ | if (row >= 0) { | |
| 892 | ✗ | auto idx = index(row, Suggestion); | |
| 893 | ✗ | emit dataChanged(idx, idx, {Qt::DisplayRole, Qt::ForegroundRole, SortRole}); | |
| 894 | } | ||
| 895 | ✗ | } | |
| 896 | |||
| 897 | // T-232: Clear all cached suggestions | ||
| 898 | ✗ | void MailListModel::clearSuggestions() { | |
| 899 | ✗ | if (m_suggestionCache.isEmpty()) | |
| 900 | ✗ | return; | |
| 901 | ✗ | m_suggestionCache.clear(); | |
| 902 | ✗ | if (!m_headers.isEmpty()) { | |
| 903 | ✗ | emit dataChanged(index(0, Suggestion), | |
| 904 | ✗ | index(m_headers.size() - 1, Suggestion), | |
| 905 | {Qt::DisplayRole, Qt::ForegroundRole, SortRole}); | ||
| 906 | } | ||
| 907 | } | ||
| 908 | |||
| 909 | // T-79.E5/M16: register a search-mode folder badge (display-only prefix). | ||
| 910 | 45879 | void MailListModel::setFolderBadge(qint64 folderId, const QString &folderName) { | |
| 911 | 45879 | m_folderBadges.insert(folderId, folderName); | |
| 912 | 45879 | } | |
| 913 | |||
| 914 | 198 | void MailListModel::clearFolderBadges() { | |
| 915 |
2/2✓ Branch 3 → 4 taken 7 times.
✓ Branch 3 → 5 taken 191 times.
|
198 | if (m_folderBadges.isEmpty()) |
| 916 | 7 | return; | |
| 917 | 191 | m_folderBadges.clear(); | |
| 918 |
2/2✓ Branch 7 → 8 taken 90 times.
✓ Branch 7 → 17 taken 101 times.
|
191 | if (!m_headers.isEmpty()) { |
| 919 |
4/4✓ Branch 8 → 9 taken 90 times.
✓ Branch 11 → 12 taken 90 times.
✓ Branch 13 → 14 taken 90 times.
✓ Branch 14 → 15 taken 90 times.
|
90 | emit dataChanged(index(0, Subject), index(m_headers.size() - 1, Subject), |
| 920 | {Qt::DisplayRole}); | ||
| 921 | } | ||
| 922 | } | ||
| 923 |