presentation/QuickMailBackend.cpp
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #include "PimEditorTime.h" | ||
| 2 | #include "ComposeDraftStore.h" | ||
| 3 | #include "MailPermanentDelete.h" | ||
| 4 | #include "ComposeRecipients.h" | ||
| 5 | #include "DavAccountSettings.h" | ||
| 6 | #include "QuickMailBackend.h" | ||
| 7 | #include "RuleDraftFactory.h" | ||
| 8 | #include "data/MailRuleCandidates.h" | ||
| 9 | #include "performance/ThreadSelectionPerformanceTracker.h" | ||
| 10 | #include "WorkspaceState.h" | ||
| 11 | #include <QFutureWatcher> | ||
| 12 | #include <QPointer> | ||
| 13 | #include <QtConcurrent> | ||
| 14 | #include "app/core/MailSessionCore.h" | ||
| 15 | #include "app/core/AccountSession.h" | ||
| 16 | #include "data/MailCache.h" | ||
| 17 | #include "app/core/CalendarSyncCoordinator.h" | ||
| 18 | #include "data/CalendarStore.h" | ||
| 19 | #include "ui/ThemeManager.h" | ||
| 20 | #include <QColor> | ||
| 21 | #include <QCryptographicHash> | ||
| 22 | #include <QCoreApplication> | ||
| 23 | #include "app/core/ContactSyncCoordinator.h" | ||
| 24 | #include "app/core/FolderPredictionSession.h" | ||
| 25 | #include "app/SearchCoordinator.h" | ||
| 26 | #include "ui/LabelDelegate.h" | ||
| 27 | #include "app/SearchQuery.h" | ||
| 28 | #include "app/FtsIndexCoordinator.h" | ||
| 29 | #include "app/NamedCommandCatalog.h" | ||
| 30 | #include "service/MailSearchReadService.h" | ||
| 31 | #include "controller/MailController.h" | ||
| 32 | #include "controller/UndoManager.h" | ||
| 33 | #include "data/FolderSessionSnapshot.h" | ||
| 34 | #include "data/DavCredentials.h" | ||
| 35 | #include <QUuid> | ||
| 36 | #include <limits> | ||
| 37 | #include <optional> | ||
| 38 | #include "service/ImapService.h" | ||
| 39 | #include "service/ConnectionHealthMonitor.h" | ||
| 40 | #include "service/ImapResponseParser.h" | ||
| 41 | #include "service/MailDisplayReadService.h" | ||
| 42 | #include "service/MailPayloadExportService.h" | ||
| 43 | #include "service/Rfc2822Builder.h" | ||
| 44 | #include "service/SmtpService.h" | ||
| 45 | #include "service/MimeParser.h" | ||
| 46 | #include "data/MailSelectionMutation.h" | ||
| 47 | #include "data/FolderPreferences.h" | ||
| 48 | #include "data/AccountConfig.h" | ||
| 49 | #include "data/CredentialStore.h" | ||
| 50 | #include "util/MessageId.h" | ||
| 51 | #include "quick/ExternalWhitelistModel.h" | ||
| 52 | #include "ui/ExternalContentPolicy.h" | ||
| 53 | #include "ui/MailThreadModel.h" | ||
| 54 | #include <QDir> | ||
| 55 | #include <QFile> | ||
| 56 | #include <QSettings> | ||
| 57 | #include <QWindow> | ||
| 58 | #include <QMenu> | ||
| 59 | #include <QApplication> | ||
| 60 | #include <QStandardPaths> | ||
| 61 | #include <QElapsedTimer> | ||
| 62 | #include <QTimer> | ||
| 63 | #include <QPointer> | ||
| 64 | #include <QJsonArray> | ||
| 65 | #include <QJsonObject> | ||
| 66 | #include <QRegularExpression> | ||
| 67 | #include <QSaveFile> | ||
| 68 | #include <QScopedValueRollback> | ||
| 69 | #include <QLocale> | ||
| 70 | #include <algorithm> | ||
| 71 | #include <cmath> | ||
| 72 | #include <utility> | ||
| 73 | #include <QQuickTextDocument> | ||
| 74 | #include "service/NativeResourceDocument.h" | ||
| 75 | #include <QLoggingCategory> | ||
| 76 | |||
| 77 |
4/7✓ Branch 2 → 3 taken 20 times.
✓ Branch 2 → 8 taken 4094 times.
✓ Branch 4 → 5 taken 20 times.
✗ Branch 4 → 8 not taken.
✓ Branch 5 → 6 taken 20 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 12 not taken.
|
4114 | Q_LOGGING_CATEGORY(lcQuickBackend, "mailjd.quick.backend") |
| 78 | |||
| 79 | 795 | QuickMailBackend::QuickMailBackend(MailSessionCore &session, WorkspaceState &workspace, | |
| 80 | 795 | QString profileDirectory, QObject *parent, MailSearchReadService *searchReader) | |
| 81 | 795 | : QObject(parent), m_session(session), m_workspace(workspace), | |
| 82 |
1/1✓ Branch 9 → 10 taken 795 times.
|
795 | m_profileDirectory(std::move(profileDirectory)), m_mail(session.headers(), {}, this), |
| 83 |
1/1✓ Branch 13 → 14 taken 795 times.
|
795 | m_threadList(session.threads(), {}, this), |
| 84 | 795 | m_pim(session, workspace, m_preferences, | |
| 85 | 2588 | {[this](const MailId &id) { return acceptsIdentity(id); }, | |
| 86 | 24 | [this] { return accountName(); }, | |
| 87 | 100 | [this](const QString &status) { setStatus(status); }}, | |
| 88 | this), | ||
| 89 |
1/1✓ Branch 20 → 21 taken 795 times.
|
795 | m_folders(this), |
| 90 | 795 | m_settings(session, workspace, m_preferences, m_pim, m_profileDirectory, | |
| 91 | 39 | {[this] { return activeAccountId(); }, | |
| 92 |
2/4✓ Branch 2 → 3 taken 24 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 24 times.
|
24 | [this] { return m_actionPending || m_compose.draftCleanupActive(); }, |
| 93 | ✗ | [this] { loadLocalFolders(); }, | |
| 94 | 2474 | [this] { return activeAccountState(); }}, | |
| 95 | this), | ||
| 96 |
2/2✓ Branch 38 → 39 taken 795 times.
✓ Branch 40 → 41 taken 795 times.
|
795 | m_folderSubscriptions(m_profileDirectory, this), m_messageCopies(session, this), |
| 97 | 795 | m_source(session, workspace, m_profileDirectory, | |
| 98 | 12 | {[this] { return configured(); }, | |
| 99 | 8 | [this](const QString &status) { setStatus(status); }}, | |
| 100 | this), | ||
| 101 | 795 | m_folderActions(session, m_folders, m_folderInfos, m_availableFolders, m_folderPath, | |
| 102 | 795 | m_profileDirectory, | |
| 103 | 34 | {[this] { return configured(); }, | |
| 104 | 2 | [this] { return accountName(); }, | |
| 105 | 27 | [this](const QString &status) { setStatus(status); }, | |
| 106 | 3 | [this](const QString &text) { m_statusState.error(text); }, | |
| 107 | ✗ | [this] { applyFolderList(m_folderInfos, false); }, | |
| 108 | ✗ | [this](const QString &path) { | |
| 109 | 1 | ++m_threadDataGeneration; | |
| 110 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | m_threadSnapshots.remove(folderIdForPath(path)); |
| 111 | 1 | }}, | |
| 112 | this), | ||
| 113 | 795 | m_compose(session, workspace, m_profileDirectory, m_threadPool, m_draftsFolder, | |
| 114 | 795 | m_sentFolder, m_folderPathById, m_folderEpochById, | |
| 115 | 11 | {[this](const MailId &id) { return acceptsIdentity(id); }, | |
| 116 | 32 | [this] { return accountName(); }, | |
| 117 | 40 | [this] { return accountEmail(); }, | |
| 118 | ✗ | [this] { return activeAccountId(); }, | |
| 119 | 60 | [this](const QString &status) { setStatus(status); }, | |
| 120 | 16 | [this](const QString &text) { m_statusState.error(text); }}, | |
| 121 |
27/49✓ Branch 3 → 4 taken 795 times.
✓ Branch 27 → 28 taken 795 times.
✓ Branch 29 → 30 taken 795 times.
✓ Branch 36 → 37 taken 795 times.
✓ Branch 49 → 50 taken 795 times.
✓ Branch 52 → 53 taken 795 times.
✓ Branch 53 → 54 taken 795 times.
✓ Branch 78 → 79 taken 795 times.
✓ Branch 87 → 88 taken 795 times.
✓ Branch 89 → 90 taken 795 times.
✓ Branch 95 → 96 taken 795 times.
✓ Branch 98 → 99 taken 795 times.
✓ Branch 101 → 102 taken 795 times.
✓ Branch 109 → 110 taken 795 times.
✓ Branch 120 → 121 taken 795 times.
✓ Branch 121 → 122 taken 795 times.
✓ Branch 122 → 123 taken 795 times.
✓ Branch 123 → 124 taken 795 times.
✓ Branch 125 → 126 taken 795 times.
✓ Branch 127 → 128 taken 795 times.
✓ Branch 128 → 129 taken 795 times.
✓ Branch 129 → 130 taken 795 times.
✓ Branch 131 → 132 taken 795 times.
✓ Branch 133 → 134 taken 795 times.
✓ Branch 134 → 135 taken 795 times.
✓ Branch 136 → 137 taken 795 times.
✓ Branch 137 → 138 taken 795 times.
✗ Branch 259 → 260 not taken.
✗ Branch 259 → 261 not taken.
✗ Branch 262 → 263 not taken.
✗ Branch 262 → 264 not taken.
✗ Branch 265 → 266 not taken.
✗ Branch 265 → 267 not taken.
✗ Branch 271 → 272 not taken.
✗ Branch 271 → 273 not taken.
✗ Branch 274 → 275 not taken.
✗ Branch 274 → 276 not taken.
✗ Branch 283 → 284 not taken.
✗ Branch 283 → 285 not taken.
✗ Branch 286 → 287 not taken.
✗ Branch 286 → 288 not taken.
✗ Branch 289 → 290 not taken.
✗ Branch 289 → 291 not taken.
✗ Branch 292 → 293 not taken.
✗ Branch 292 → 294 not taken.
✗ Branch 298 → 299 not taken.
✗ Branch 298 → 300 not taken.
✗ Branch 301 → 302 not taken.
✗ Branch 301 → 303 not taken.
|
4770 | this) { |
| 122 | 829 | m_selectionSummary.setHeaderLookup([this](const QList<MailId> &ids) { return m_session.cache()->headersByIds(ids); }); | |
| 123 |
2/2✓ Branch 142 → 143 taken 795 times.
✓ Branch 143 → 144 taken 795 times.
|
795 | m_mailSort = MailSortOrder::fromString(m_preferences.mailSort()); |
| 124 |
1/1✓ Branch 145 → 146 taken 795 times.
|
795 | applyMailSort(); |
| 125 |
1/1✓ Branch 146 → 147 taken 795 times.
|
795 | wireProjectionTimers(); |
| 126 |
1/1✓ Branch 147 → 148 taken 795 times.
|
795 | m_settings.connectSettingsSync(); |
| 127 |
1/1✓ Branch 148 → 149 taken 795 times.
|
795 | wireThreadInvalidation(); |
| 128 |
1/1✓ Branch 149 → 150 taken 795 times.
|
795 | wireIndexedProviders(); |
| 129 |
1/1✓ Branch 150 → 151 taken 795 times.
|
795 | wireHeaderProjection(); |
| 130 |
1/1✓ Branch 151 → 152 taken 795 times.
|
795 | wireProjectionSignals(); |
| 131 |
1/1✓ Branch 152 → 153 taken 795 times.
|
795 | m_settings.connectDavDiscovery(); |
| 132 |
1/1✓ Branch 153 → 154 taken 795 times.
|
795 | wireComposeSource(); |
| 133 |
1/1✓ Branch 154 → 155 taken 795 times.
|
795 | wireFolderSubscriptionsAndFilters(); |
| 134 |
1/1✓ Branch 155 → 156 taken 795 times.
|
795 | wireFolderOperations(); |
| 135 |
1/1✓ Branch 156 → 157 taken 795 times.
|
795 | m_compose.connectServerDrafts(); |
| 136 | 795 | m_compose.addressPicker().setDatabasePath(m_pim.contactModel().databasePath()); | |
| 137 |
1/1✓ Branch 162 → 163 taken 795 times.
|
795 | m_pim.connectSignals(); |
| 138 |
1/1✓ Branch 163 → 164 taken 795 times.
|
795 | m_conflicts.connectSources(); |
| 139 | // Sprint 100 S100.1.5: the Accounts page shows the active account's state. | ||
| 140 |
1/1✓ Branch 165 → 166 taken 795 times.
|
795 | connect(session.imap(), &ImapService::stateChanged, &m_settings, &QuickAccountSettings::notifyOverviewChanged); |
| 141 |
1/1✓ Branch 167 → 168 taken 795 times.
|
795 | connect(&m_rules, &QuickRulesBackend::stateChanged, &m_settings, &QuickAccountSettings::notifyOverviewChanged); |
| 142 |
1/1✓ Branch 169 → 170 taken 795 times.
|
795 | connect(&m_rules, &QuickRulesBackend::documentChanged, &m_settings, &QuickAccountSettings::notifyOverviewChanged); |
| 143 |
1/1✓ Branch 171 → 172 taken 795 times.
|
795 | wirePredictions(); |
| 144 |
1/1✓ Branch 172 → 173 taken 795 times.
|
795 | wireRemovalFollowUp(); |
| 145 |
1/1✓ Branch 173 → 174 taken 795 times.
|
795 | wireFolderChangeResets(); |
| 146 |
1/1✓ Branch 174 → 175 taken 795 times.
|
795 | wireSearch(searchReader); |
| 147 |
1/1✓ Branch 175 → 176 taken 795 times.
|
795 | wireSearchTabs(searchReader); |
| 148 |
1/1✓ Branch 176 → 177 taken 795 times.
|
795 | wireSearchSuggestions(); |
| 149 |
1/1✓ Branch 177 → 178 taken 795 times.
|
795 | wireSelection(); |
| 150 |
1/1✓ Branch 178 → 179 taken 795 times.
|
795 | wireAccountReload(); |
| 151 |
1/1✓ Branch 179 → 180 taken 795 times.
|
795 | wireSelectionTrace(); |
| 152 |
1/1✓ Branch 180 → 181 taken 795 times.
|
795 | m_source.connectSignals(); |
| 153 |
1/1✓ Branch 181 → 182 taken 795 times.
|
795 | wireNavigation(); |
| 154 |
1/1✓ Branch 182 → 183 taken 795 times.
|
795 | wireControllerStatus(); |
| 155 |
1/1✓ Branch 183 → 184 taken 795 times.
|
795 | wireConnectionStatus(); |
| 156 | 4770 | } | |
| 157 | |||
| 158 | // Thread-projection timers: provisional page and slow-rebuild flag. | ||
| 159 | 795 | void QuickMailBackend::wireProjectionTimers() { | |
| 160 | 795 | m_provisionalPageTimer.setSingleShot(true); | |
| 161 | 795 | m_provisionalPageTimer.setInterval(ProvisionalPageDelayMs); | |
| 162 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(&m_provisionalPageTimer, &QTimer::timeout, this, &QuickMailBackend::requestProvisionalPage); |
| 163 | 795 | m_threadProjectionSlowTimer.setSingleShot(true); | |
| 164 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | connect(&m_threadProjectionSlowTimer, &QTimer::timeout, this, [this] { |
| 165 |
2/4✓ Branch 2 → 3 taken 2 times.
✗ Branch 2 → 4 not taken.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
|
2 | if (!m_threadProjectionPending || m_threadProjectionSlow) return; |
| 166 | 2 | m_threadList.releaseHeldRows(); | |
| 167 | 2 | m_threadProjectionSlow = true; | |
| 168 | 2 | emit threadProjectionSlowChanged(); | |
| 169 | }); | ||
| 170 | 795 | } | |
| 171 | |||
| 172 | // Thread projection: cache/controller invalidation of retained folder snapshots and visual moves. | ||
| 173 | 795 | void QuickMailBackend::wireThreadInvalidation() { | |
| 174 | 795 | MailSessionCore &session = m_session; | |
| 175 | 719 | const auto invalidateThreadFolder = [this](qint64 folderId) { | |
| 176 | 719 | ++m_threadDataGeneration; | |
| 177 | 719 | m_threadSnapshots.remove(folderId); | |
| 178 |
6/8✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 9 taken 717 times.
✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 9 not taken.
✓ Branch 10 → 11 taken 2 times.
✓ Branch 10 → 13 taken 717 times.
|
719 | if (m_threadList.indexed() && m_threadList.indexedFolder() == folderId && !m_threadRefreshQueued) { |
| 179 | 2 | m_threadRefreshQueued = true; | |
| 180 |
1/1✓ Branch 11 → 12 taken 2 times.
|
2 | QTimer::singleShot(0, this, [this] { |
| 181 | 2 | m_threadRefreshQueued = false; | |
| 182 | 2 | syncThreadProjection(); | |
| 183 | 2 | }); | |
| 184 | } | ||
| 185 | 719 | }; | |
| 186 |
1/1✓ Branch 3 → 4 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerStateReplaced, this, invalidateThreadFolder); |
| 187 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerFlagsChanged, this, invalidateThreadFolder); |
| 188 |
1/1✓ Branch 9 → 10 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerLabelChanged, this, invalidateThreadFolder); |
| 189 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerRemoved, this, invalidateThreadFolder); |
| 190 | // P90.16: journal phase transitions change the pending-move admission | ||
| 191 | // set without touching the folder revision (e.g. a failed move must | ||
| 192 | // re-admit its row, a queued one must hide it). Force a projection | ||
| 193 | // refresh for the indexed folder only. | ||
| 194 |
1/1✓ Branch 15 → 16 taken 795 times.
|
795 | connect(session.cache(), &MailCache::mutationPhaseChanged, this, |
| 195 | 795 | [this](const QString &account, qint64 folderId, qint64, qint64 uid, | |
| 196 | bool pending) { | ||
| 197 | ✗ | if (account != accountName() || !m_threadList.indexed() || | |
| 198 | ✗ | m_threadList.indexedFolder() != folderId) return; | |
| 199 | ✗ | if (pending) m_threadList.markIndexedMovePending(folderId, uid); | |
| 200 | ✗ | queueIndexedMoveExclusionRefresh(folderId); | |
| 201 | }); | ||
| 202 | // Visual-first moves: hide the rows before the durable writer answers and | ||
| 203 | // keep them hidden across any exclusion refresh until it has (the journal | ||
| 204 | // then decides: journaled rows stay excluded, rolled-back rows return). | ||
| 205 |
1/1✓ Branch 18 → 19 taken 795 times.
|
795 | connect(session.controller(), &MailController::movesVisuallyPending, this, |
| 206 | 795 | [this](qint64 folderId, const QList<qint64> &uids) { | |
| 207 | 47 | const auto selected = m_workspace.mailId(); | |
| 208 | 47 | bool removedSelection = false; | |
| 209 |
2/2✓ Branch 16 → 6 taken 131 times.
✓ Branch 16 → 17 taken 47 times.
|
178 | for (const qint64 uid : uids) { |
| 210 |
2/2✓ Branch 7 → 8 taken 131 times.
✓ Branch 8 → 9 taken 131 times.
|
131 | m_visualMoveUids[folderId].insert(uid); |
| 211 |
1/1✓ Branch 9 → 10 taken 131 times.
|
131 | m_threadList.markIndexedMovePending(folderId, uid); |
| 212 |
3/4✓ Branch 10 → 11 taken 131 times.
✗ Branch 10 → 13 not taken.
✓ Branch 11 → 12 taken 47 times.
✓ Branch 11 → 13 taken 84 times.
|
131 | removedSelection |= selected.folderId == folderId && selected.uid == uid; |
| 213 | } | ||
| 214 | // The reserved successor (P90.16 token) takes over in the same visual | ||
| 215 | // commit instead of after the journal-driven exclusion refresh. | ||
| 216 |
4/6✓ Branch 17 → 18 taken 47 times.
✗ Branch 17 → 27 not taken.
✓ Branch 19 → 20 taken 46 times.
✓ Branch 19 → 27 taken 1 time.
✓ Branch 25 → 26 taken 46 times.
✗ Branch 25 → 27 not taken.
|
93 | if (removedSelection && m_workspace.activeTab() == 0 && |
| 217 |
8/8✓ Branch 28 → 29 taken 46 times.
✓ Branch 28 → 30 taken 1 time.
✓ Branch 30 → 31 taken 46 times.
✓ Branch 30 → 32 taken 1 time.
✓ Branch 32 → 33 taken 46 times.
✓ Branch 32 → 34 taken 1 time.
✓ Branch 34 → 35 taken 46 times.
✓ Branch 34 → 37 taken 1 time.
|
93 | m_workspace.area() == QStringLiteral("mail")) |
| 218 |
1/1✓ Branch 35 → 36 taken 46 times.
|
46 | QTimer::singleShot(0, this, &QuickMailBackend::reconcileSelection); |
| 219 | 47 | }); | |
| 220 |
1/1✓ Branch 21 → 22 taken 795 times.
|
795 | connect(session.controller(), &MailController::movesVisuallySettled, this, |
| 221 | 795 | [this](qint64 folderId, const QList<qint64> &uids) { | |
| 222 |
1/1✓ Branch 2 → 3 taken 43 times.
|
43 | auto visual = m_visualMoveUids.find(folderId); |
| 223 |
1/2✓ Branch 5 → 6 taken 43 times.
✗ Branch 5 → 20 not taken.
|
43 | if (visual != m_visualMoveUids.end()) { |
| 224 |
3/3✓ Branch 10 → 11 taken 127 times.
✓ Branch 13 → 8 taken 127 times.
✓ Branch 13 → 14 taken 43 times.
|
170 | for (const qint64 uid : uids) visual->remove(uid); |
| 225 |
3/3✓ Branch 16 → 17 taken 40 times.
✓ Branch 16 → 20 taken 3 times.
✓ Branch 18 → 19 taken 40 times.
|
43 | if (visual->isEmpty()) m_visualMoveUids.erase(visual); |
| 226 | } | ||
| 227 |
5/6✓ Branch 21 → 22 taken 1 time.
✓ Branch 21 → 25 taken 42 times.
✓ Branch 23 → 24 taken 1 time.
✗ Branch 23 → 25 not taken.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 28 taken 42 times.
|
43 | if (m_threadList.indexed() && m_threadList.indexedFolder() == folderId) |
| 228 |
1/1✓ Branch 27 → 28 taken 1 time.
|
1 | queueIndexedMoveExclusionRefresh(folderId); |
| 229 | 43 | }); | |
| 230 |
1/1✓ Branch 24 → 25 taken 795 times.
|
795 | connect(session.cache(), &MailCache::folderHeadersRemoved, this, invalidateThreadFolder); |
| 231 |
1/1✓ Branch 27 → 28 taken 795 times.
|
795 | connect(session.controller(), &MailController::folderIdentityInvalidated, this, invalidateThreadFolder); |
| 232 | 795 | } | |
| 233 | // Indexed-page providers the controller reads (visible flags, row headers, adjacent headers). | ||
| 234 | 795 | void QuickMailBackend::wireIndexedProviders() { | |
| 235 | 795 | MailSessionCore &session = m_session; | |
| 236 |
1/1✓ Branch 2 → 3 taken 795 times.
|
795 | const QPointer<QuickMailBackend> indexedOwner(this); |
| 237 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | session.controller()->flagSync()->setVisibleIndexedFlagsProvider([indexedOwner](qint64 folderId) { |
| 238 | ✗ | QHash<qint64, quint32> flags; | |
| 239 | ✗ | if (!indexedOwner || !indexedOwner->threadedPresentation() || | |
| 240 | ✗ | !indexedOwner->m_threadList.indexed() || | |
| 241 | ✗ | indexedOwner->m_threadList.indexedFolder() != folderId) return flags; | |
| 242 | ✗ | const int rows = indexedOwner->m_threadList.rowCount(); | |
| 243 | ✗ | flags.reserve(rows); | |
| 244 | ✗ | for (int row = 0; row < rows; ++row) { | |
| 245 | ✗ | const auto key = indexedOwner->m_threadList.keyAt(row); | |
| 246 | ✗ | if (const auto *header = indexedOwner->m_threadList.indexedHeader(folderId, key.uid)) | |
| 247 | ✗ | flags.insert(key.uid, header->flags); | |
| 248 | ✗ | } | |
| 249 | ✗ | return flags; | |
| 250 | ✗ | }); | |
| 251 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | session.controller()->setIndexedHeaderProvider([indexedOwner](const MailId &id) -> std::optional<MailHeader> { |
| 252 |
2/4✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 11 not taken.
✓ Branch 6 → 7 taken 1 time.
✗ Branch 6 → 11 not taken.
|
2 | if (!indexedOwner || !indexedOwner->m_threadList.indexed() || |
| 253 |
3/6✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 13 → 14 taken 1 time.
✗ Branch 13 → 15 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 1 time.
|
2 | id.accountId != indexedOwner->accountName()) return std::nullopt; |
| 254 |
1/2✓ Branch 19 → 20 taken 1 time.
✗ Branch 19 → 21 not taken.
|
1 | if (const auto *header = indexedOwner->m_threadList.indexedHeader(id.folderId, id.uid)) return *header; |
| 255 | ✗ | return std::nullopt; | |
| 256 | }); | ||
| 257 |
1/1✓ Branch 19 → 20 taken 795 times.
|
795 | session.controller()->bodies()->setIndexedAdjacentHeadersProvider([indexedOwner](const MailId &id) { |
| 258 | ✗ | QList<MailHeader> headers; | |
| 259 | ✗ | if (!indexedOwner || !indexedOwner->threadedPresentation() || | |
| 260 | ✗ | !indexedOwner->m_threadList.indexed() || | |
| 261 | ✗ | indexedOwner->m_threadList.indexedFolder() != id.folderId || | |
| 262 | ✗ | id.accountId != indexedOwner->accountName()) return headers; | |
| 263 | ✗ | const int row = indexedOwner->m_threadList.rowForIdentity(WorkspaceState::encode(id)); | |
| 264 | ✗ | if (row < 0) return headers; | |
| 265 | ✗ | for (int offset : {1, -1, 2}) { | |
| 266 | ✗ | const auto key = indexedOwner->m_threadList.keyAt(row + offset); | |
| 267 | ✗ | if (key.uid <= 0) continue; | |
| 268 | ✗ | if (const auto *header = indexedOwner->m_threadList.indexedHeader(id.folderId, key.uid)) | |
| 269 | ✗ | headers.append(*header); | |
| 270 | ✗ | } | |
| 271 | ✗ | return headers; | |
| 272 | ✗ | }); | |
| 273 | 795 | } | |
| 274 | // Header-projection catch-up timer and indexed flag/label/seen/star updates. | ||
| 275 | 795 | void QuickMailBackend::wireHeaderProjection() { | |
| 276 | 795 | MailSessionCore &session = m_session; | |
| 277 | // Header batches commit through the worker-owned cache connection, so the | ||
| 278 | // GUI cache emits no per-row signal for them. The controller forwards only | ||
| 279 | // revision-changing commits. Coalesce those projection catch-ups while | ||
| 280 | // keeping the published thread page and selection mounted. | ||
| 281 | 795 | m_headerProjectionTimer.setSingleShot(true); | |
| 282 | 795 | m_headerProjectionTimer.setInterval(150); | |
| 283 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(&m_headerProjectionTimer, &QTimer::timeout, this, [this] { |
| 284 | ✗ | const QString folder = m_headerProjectionFolder; | |
| 285 | ✗ | m_headerProjectionFolder.clear(); | |
| 286 | ✗ | if (threadedPresentation() && folder == m_folderPath && | |
| 287 | ✗ | m_session.controller()->currentFolder() == m_folderPath) | |
| 288 | ✗ | syncThreadProjection(); | |
| 289 | ✗ | }); | |
| 290 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | connect(session.controller(), &MailController::headerProjectionAdvanced, this, |
| 291 | 795 | [this](const QString &path, qint64 folderId, int totalCount) { | |
| 292 | ✗ | if (path != m_session.controller()->currentFolder() || | |
| 293 | ✗ | folderId != m_session.controller()->currentFolderId()) return; | |
| 294 | ✗ | if (folderId <= 0) return; | |
| 295 | // A cold stream can cross the indexed threshold after folder selection. | ||
| 296 | // Stop publishing further batches into the hidden flat model before the | ||
| 297 | // controller handles this very batch. | ||
| 298 | ✗ | if (threadedPresentation() && m_session.controller()->streamedHeadersVisible() && | |
| 299 | totalCount > 200) | ||
| 300 | ✗ | m_session.controller()->setStreamedHeadersVisible(false); | |
| 301 | ✗ | ++m_threadDataGeneration; | |
| 302 | ✗ | m_threadSnapshots.remove(folderId); | |
| 303 | ✗ | if (!threadedPresentation() || path != m_folderPath) return; | |
| 304 | ✗ | m_headerProjectionFolder = path; | |
| 305 | ✗ | if (!m_headerProjectionTimer.isActive()) m_headerProjectionTimer.start(); | |
| 306 | }); | ||
| 307 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerFlagsChanged, &m_threadList, &QuickThreadListModel::updateIndexedFlags); |
| 308 | // The controller's loaded-row Seen commit reaches the indexed list before | ||
| 309 | // its background journal write. The same signal restores it on rejection. | ||
| 310 |
1/1✓ Branch 13 → 14 taken 795 times.
|
795 | connect(session.controller(), &MailController::seenVisualChanged, &m_threadList, |
| 311 | 795 | [this](qint64 folderId, qint64 uid, bool seen) { | |
| 312 | 73 | m_threadList.applyIndexedSeen(folderId, uid, seen); | |
| 313 | 73 | }); | |
| 314 |
1/1✓ Branch 16 → 17 taken 795 times.
|
795 | connect(session.controller(), &MailController::starVisualChanged, &m_threadList, |
| 315 | 795 | [this](qint64 folderId, qint64 uid, bool starred) { | |
| 316 | 298 | m_threadList.applyIndexedStar(folderId, uid, starred); | |
| 317 | 298 | }); | |
| 318 |
1/1✓ Branch 19 → 20 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerLabelChanged, &m_threadList, &QuickThreadListModel::updateIndexedLabel); |
| 319 | // The reader header's marks follow the same visual-first signals. | ||
| 320 |
1/1✓ Branch 22 → 23 taken 795 times.
|
795 | connect(session.controller(), &MailController::starVisualChanged, this, [this](qint64 folderId, qint64 uid, bool starred) { |
| 321 |
1/1✓ Branch 3 → 4 taken 298 times.
|
339 | readerMarkChanged(folderId, uid, [starred](ReaderMarks &marks) { marks.importantOverride = starred; }); |
| 322 | 298 | }); | |
| 323 |
1/1✓ Branch 25 → 26 taken 795 times.
|
795 | connect(session.controller(), &MailController::labelVisualChanged, this, |
| 324 | 795 | [this](qint64 folderId, qint64 uid, const QString &label, bool present) { | |
| 325 |
1/1✓ Branch 3 → 4 taken 18 times.
|
34 | readerMarkChanged(folderId, uid, [&label, present](ReaderMarks &marks) { marks.labelOverrides.insert(label, present); }); |
| 326 | 18 | }); | |
| 327 | // A mail tab's mail outside the loaded rows changes in the cache only | ||
| 328 | // (the cross-folder flag path and its undo). | ||
| 329 |
1/1✓ Branch 28 → 29 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerFlagsChanged, this, [this](qint64 folderId, qint64 uid, quint32 flags) { |
| 330 |
1/1✓ Branch 3 → 4 taken 112 times.
|
216 | readerMarkChanged(folderId, uid, [flags](ReaderMarks &marks) { marks.importantOverride = (flags & MailFlag::Flagged) != 0; }); |
| 331 | 112 | }); | |
| 332 |
1/1✓ Branch 31 → 32 taken 795 times.
|
795 | connect(session.cache(), &MailCache::headerLabelChanged, this, |
| 333 | 795 | [this](qint64 folderId, qint64 uid, const QString &label, bool present) { | |
| 334 |
1/1✓ Branch 3 → 4 taken 21 times.
|
40 | readerMarkChanged(folderId, uid, [&label, present](ReaderMarks &marks) { marks.labelOverrides.insert(label, present); }); |
| 335 | 21 | }); | |
| 336 |
1/1✓ Branch 33 → 34 taken 795 times.
|
795 | connect(&m_workspace, &WorkspaceState::selectionChanged, this, &QuickMailBackend::readerMarksChanged); |
| 337 |
1/1✓ Branch 35 → 36 taken 795 times.
|
795 | connect(&m_workspace, &WorkspaceState::activeTabChanged, this, &QuickMailBackend::readerMarksChanged); |
| 338 | // A flag change from the server (another client) reaches the cache and | ||
| 339 | // the loaded rows; the marks are read again. | ||
| 340 |
1/1✓ Branch 38 → 39 taken 795 times.
|
795 | connect(session.headers(), &QAbstractItemModel::dataChanged, this, [this] { |
| 341 |
1/2✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 399 times.
|
399 | if (!m_readerMarks.loaded) return; |
| 342 | 399 | m_readerMarks.loaded = false; | |
| 343 | 399 | emit readerMarksChanged(); | |
| 344 | }); | ||
| 345 | // Visual-first labels reach the indexed list before their journal write | ||
| 346 | // (and return there on rollback). | ||
| 347 |
1/1✓ Branch 41 → 42 taken 795 times.
|
795 | connect(session.controller(), &MailController::labelVisualChanged, &m_threadList, |
| 348 | 795 | &QuickThreadListModel::updateIndexedLabel); | |
| 349 | 795 | } | |
| 350 | // Thread-list projection validators and projection state signals. | ||
| 351 | 795 | void QuickMailBackend::wireProjectionSignals() { | |
| 352 | 795 | m_threadList.setProjectionValidator([this](const ThreadProjectionRequest &request, qint64 revision) { | |
| 353 |
4/10✓ Branch 3 → 4 taken 29 times.
✗ Branch 3 → 9 not taken.
✓ Branch 6 → 7 taken 29 times.
✗ Branch 6 → 9 not taken.
✓ Branch 11 → 12 taken 29 times.
✗ Branch 11 → 13 not taken.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 29 times.
✗ Branch 24 → 25 not taken.
✗ Branch 24 → 26 not taken.
|
58 | if (!threadedPresentation() || request.account != accountName() || |
| 354 |
2/3✓ Branch 7 → 8 taken 29 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 29 times.
|
29 | request.folderId != folderIdForPath(m_folderPath)) return false; |
| 355 | // The worker page must match one coherent folder snapshot. Two separate | ||
| 356 | // GUI queries also paid twice for SQLite admission on every page. | ||
| 357 |
1/1✓ Branch 16 → 17 taken 29 times.
|
29 | const auto state = m_session.cache()->folderPresentationState(request.folderId); |
| 358 |
2/4✓ Branch 17 → 18 taken 29 times.
✗ Branch 17 → 20 not taken.
✓ Branch 18 → 19 taken 29 times.
✗ Branch 18 → 20 not taken.
|
29 | return request.uidValidity == state.uidValidity && revision == state.threadRevision; |
| 359 | }); | ||
| 360 | 795 | m_threadList.setProjectionIdentityValidator([this](const ThreadProjectionRequest &request) { | |
| 361 | ✗ | return threadedPresentation() && request.account == accountName() && | |
| 362 | ✗ | request.folderId == folderIdForPath(m_folderPath) && | |
| 363 | ✗ | request.uidValidity == m_session.cache()->folderPresentationState(request.folderId).uidValidity; | |
| 364 | }); | ||
| 365 |
1/1✓ Branch 8 → 9 taken 795 times.
|
795 | connect(&m_threadList, &QuickThreadListModel::projectionChanged, this, [this] { |
| 366 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 79 times.
|
79 | if (!m_threadList.indexed()) return; |
| 367 | 79 | m_threadProjectionPending = m_threadList.projectionPending(); | |
| 368 | // Only a page the user waits for may announce itself as slow; a | ||
| 369 | // background refresh keeps the published rows without a placeholder, | ||
| 370 | // which would also shift the list while it appears and disappears. | ||
| 371 |
2/2✓ Branch 7 → 8 taken 28 times.
✓ Branch 7 → 18 taken 51 times.
|
79 | if (m_threadList.projectionBlocking()) { |
| 372 | 28 | armThreadProjectionSlowFlag(); | |
| 373 |
4/4✓ Branch 11 → 12 taken 19 times.
✓ Branch 11 → 15 taken 9 times.
✓ Branch 16 → 17 taken 19 times.
✓ Branch 16 → 22 taken 9 times.
|
47 | if (m_threadList.provisionalPageWanted(m_threadList.indexedFolder()) && |
| 374 |
1/2✓ Branch 13 → 14 taken 19 times.
✗ Branch 13 → 15 not taken.
|
19 | !m_provisionalPageTimer.isActive()) |
| 375 | 19 | m_provisionalPageTimer.start(); | |
| 376 | } else { | ||
| 377 | 51 | m_provisionalPageTimer.stop(); | |
| 378 | 51 | m_threadProjectionSlowTimer.stop(); | |
| 379 | // P90.1/P90.11 hysteresis: a synchronous folder switch re-enters | ||
| 380 | // pending (new projection) before any frame is painted, so the clear | ||
| 381 | // decision is deferred one turn — the placeholder cannot blink off | ||
| 382 | // for the handover gap and still clears when a page truly landed. | ||
| 383 | // Rows shown and no blocking load: a background refresh (including the | ||
| 384 | // catch-up after an older-revision page) keeps no placeholder. | ||
| 385 |
1/1✓ Branch 20 → 21 taken 51 times.
|
51 | QTimer::singleShot(0, this, [this] { |
| 386 |
6/7✓ Branch 3 → 4 taken 36 times.
✓ Branch 3 → 8 taken 12 times.
✓ Branch 5 → 6 taken 36 times.
✓ Branch 6 → 7 taken 36 times.
✗ Branch 6 → 8 not taken.
✓ Branch 9 → 10 taken 36 times.
✓ Branch 9 → 11 taken 12 times.
|
48 | if (!m_threadList.projectionBlocking() && m_threadList.rowCount() > 0) |
| 387 | 36 | clearThreadProjectionSlowFlag(); | |
| 388 | 48 | }); | |
| 389 | } | ||
| 390 | 79 | emit threadedChanged(); | |
| 391 | }); | ||
| 392 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | connect(&m_threadList, &QuickThreadListModel::projectionReady, this, [this] { |
| 393 | 33 | m_threadProjectionFailed = false; | |
| 394 | 33 | clearThreadProjectionSlowFlag(); | |
| 395 | 33 | reconcileSelection(); | |
| 396 | 33 | }); | |
| 397 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(&m_threadList, &QuickThreadListModel::projectionStale, this, [this] { |
| 398 | ✗ | QTimer::singleShot(0, this, &QuickMailBackend::syncThreadProjection); | |
| 399 | ✗ | }); | |
| 400 |
1/1✓ Branch 14 → 15 taken 795 times.
|
795 | connect(&m_threadList, &QuickThreadListModel::projectionFailed, this, [this](const QString &error) { |
| 401 | ✗ | m_threadProjectionFailed = true; | |
| 402 | ✗ | clearThreadProjectionSlowFlag(); | |
| 403 | ✗ | m_statusState.error(tr("Conversations could not be loaded: %1").arg(error)); | |
| 404 | ✗ | }); | |
| 405 | 795 | } | |
| 406 | |||
| 407 | // Worker pool of compose (send and server-draft builds), folder-epoch cache | ||
| 408 | // maintenance and the compose source's cancellation (formerly one block; S91.3.1 split the | ||
| 409 | // compose part into QuickComposeBackend::connectComposeSource()). | ||
| 410 | 795 | void QuickMailBackend::wireComposeSource() { | |
| 411 | 795 | MailSessionCore &session = m_session; | |
| 412 | 795 | m_threadPool.setMaxThreadCount(1); | |
| 413 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(session.cache(), &MailCache::folderIdentityInvalidated, this, |
| 414 | 795 | [this](qint64 folderId) { | |
| 415 | 16 | m_folderEpochById.remove(folderId); | |
| 416 | 16 | }); | |
| 417 | 795 | m_compose.connectComposeSource(); | |
| 418 |
1/1✓ Branch 8 → 9 taken 795 times.
|
795 | connect(session.cache(), &MailCache::folderUidValidityChanged, this, |
| 419 | 795 | [this](qint64 folderId, quint32 uidValidity) { | |
| 420 |
2/2✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 5 taken 1 time.
|
16 | if (m_folderPathById.contains(folderId)) |
| 421 | 15 | m_folderEpochById.insert(folderId, uidValidity); | |
| 422 | 16 | }); | |
| 423 | 795 | } | |
| 424 | |||
| 425 | // Folder subscriptions, status forwarding, flat-list filter and suggestion providers, image policy. | ||
| 426 | 795 | void QuickMailBackend::wireFolderSubscriptionsAndFilters() { | |
| 427 | 795 | connect(&m_folderSubscriptions, &FolderSubscriptionEditor::saved, this, | |
| 428 |
1/1✓ Branch 2 → 3 taken 795 times.
|
795 | [this](const QString &account, const QStringList &paths) { |
| 429 |
1/2✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 8 not taken.
|
1 | if (account == accountName()) m_session.controller()->setSubscribedFolders(paths); |
| 430 | 1 | }); | |
| 431 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(&m_statusState, &StatusMessageState::changed, this, &QuickMailBackend::statusChanged); |
| 432 | // The flat list's quick filter also narrows the cached pages (whole | ||
| 433 | // folder), not only the rows already loaded into the window. | ||
| 434 | 116 | const auto flatFilter = [this] { | |
| 435 | 116 | MailHeaderFilter filter; | |
| 436 |
1/1✓ Branch 4 → 5 taken 116 times.
|
116 | filter.text = m_mail.filterText().toCaseFolded(); |
| 437 | 116 | filter.unread = m_mail.showUnreadOnly(); | |
| 438 | 116 | filter.starred = m_mail.showStarredOnly(); | |
| 439 | 116 | filter.attachments = m_mail.showWithAttachments(); | |
| 440 | 116 | return filter; | |
| 441 | ✗ | }; | |
| 442 | // One commit per filter intent: the controller's filtered page replaces | ||
| 443 | // the rows (whole folder); the proxy does not filter the loaded window | ||
| 444 | // first (that showed an interim, window-only result). | ||
| 445 | 795 | m_mail.setSourceFilterDriver([this, flatFilter] { | |
| 446 |
2/2✓ Branch 3 → 4 taken 58 times.
✓ Branch 4 → 5 taken 58 times.
|
58 | return m_session.controller()->setFlatFilter(flatFilter()); |
| 447 | }); | ||
| 448 |
1/1✓ Branch 9 → 10 taken 795 times.
|
795 | connect(&m_mail, &MailFilterProxyModel::mailFilterChanged, this, [this, flatFilter] { |
| 449 |
2/2✓ Branch 3 → 4 taken 58 times.
✓ Branch 4 → 5 taken 58 times.
|
58 | m_session.controller()->setFlatFilter(flatFilter()); |
| 450 | 58 | }); | |
| 451 | 795 | m_session.controller()->setRetainedUnreadProvider([this](qint64 folderId) { | |
| 452 | 9 | return m_mail.shownUnread(folderId); | |
| 453 | }); | ||
| 454 | 376253 | m_mail.setSuggestionProvider([this](const MailId &id) { return predictionText(id); }); | |
| 455 | 16389 | m_threadList.setSuggestionProvider([this](const MailId &id) { return predictionText(id); }); | |
| 456 | // S94.5.5: a task glyph on mails with an open linked task. The set holds | ||
| 457 | // canonical Message-IDs; an empty set skips the canonicalisation. | ||
| 458 | 25032 | const auto hasOpenTask = [this](const QString &rawMessageId) { | |
| 459 |
7/9✓ Branch 3 → 4 taken 1407 times.
✓ Branch 3 → 8 taken 23625 times.
✓ Branch 4 → 5 taken 1407 times.
✓ Branch 6 → 7 taken 240 times.
✓ Branch 6 → 8 taken 1167 times.
✓ Branch 10 → 11 taken 1407 times.
✓ Branch 10 → 12 taken 23625 times.
✗ Branch 14 → 15 not taken.
✗ Branch 14 → 16 not taken.
|
25032 | return !m_openTaskMessageIds.isEmpty() && m_openTaskMessageIds.contains(MessageId::canonicalFirst(rawMessageId)); |
| 460 | 795 | }; | |
| 461 | 795 | m_mail.setTaskLinkProvider(hasOpenTask); | |
| 462 | 795 | m_threadList.setTaskLinkProvider(hasOpenTask); | |
| 463 | // F95-14: rows that a server rule matches show a marker. | ||
| 464 | 25407 | const auto ruleMatches = [this](const MailHeader &header) { return m_rules.matchesHeader(header); }; | |
| 465 | 795 | m_mail.setRuleMatchProvider(ruleMatches); | |
| 466 | 795 | m_threadList.setRuleMatchProvider(ruleMatches); | |
| 467 |
1/1✓ Branch 33 → 34 taken 795 times.
|
795 | connect(&m_rules, &QuickRulesBackend::matchesChanged, this, [this] { |
| 468 | 35 | m_mail.ruleMatchesChanged(); | |
| 469 | 35 | m_threadList.ruleMatchesChanged(); | |
| 470 | 35 | }); | |
| 471 |
1/1✓ Branch 35 → 36 taken 795 times.
|
795 | connect(&m_pim, &QuickPimBackend::taskLinksChanged, this, &QuickMailBackend::refreshTaskLinks); |
| 472 |
1/1✓ Branch 37 → 38 taken 795 times.
|
795 | refreshTaskLinks(); |
| 473 | // Any writer of the shared preference must refresh the QML mode binding. | ||
| 474 |
1/1✓ Branch 39 → 40 taken 795 times.
|
795 | connect(ExternalContentPolicy::instance(), &ExternalContentPolicy::changed, |
| 475 |
1/1✓ Branch 38 → 39 taken 795 times.
|
795 | &m_settings, &QuickAccountSettings::imagePolicyModeChanged); |
| 476 | 795 | } | |
| 477 | |||
| 478 | // IMAP folder operation results (create/delete/rename/error). | ||
| 479 | // Folder-prediction debounce, quick-move fallback and invalidation sources. | ||
| 480 | 795 | void QuickMailBackend::wirePredictions() { | |
| 481 | 795 | MailSessionCore &session = m_session; | |
| 482 | 795 | WorkspaceState &workspace = m_workspace; | |
| 483 | 795 | m_predictionDebounce.setSingleShot(true); | |
| 484 | // The debounce only coalesces synchronous invalidation bursts (header | ||
| 485 | // dataChanged storms); a selected-mail hint must not pay a fixed delay. | ||
| 486 | 795 | m_predictionDebounce.setInterval(20); | |
| 487 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(&m_predictionDebounce, &QTimer::timeout, this, &QuickMailBackend::requestPredictions); |
| 488 | // F4.2(b): a pressed s that outran the prediction retries once the batch | ||
| 489 | // lands; if nothing arrives in the bounded window, the shell falls back | ||
| 490 | // to the manual move console like the old synchronous rejection did. | ||
| 491 | 795 | m_quickMoveFallback.setSingleShot(true); | |
| 492 | 795 | m_quickMoveFallback.setInterval(750); | |
| 493 |
1/1✓ Branch 8 → 9 taken 795 times.
|
795 | connect(&m_quickMoveFallback, &QTimer::timeout, this, [this] { |
| 494 | ✗ | if (!m_quickMoveArmed.isValid()) return; | |
| 495 | ✗ | disarmQuickMove(); | |
| 496 | ✗ | emit manualMoveRequested(); | |
| 497 | }); | ||
| 498 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | connect(this, &QuickMailBackend::suggestionsChanged, this, &QuickMailBackend::tryArmedQuickMove); |
| 499 |
1/1✓ Branch 13 → 14 taken 795 times.
|
795 | connect(session.predictions(), &FolderPredictionSession::changed, this, &QuickMailBackend::invalidatePredictions); |
| 500 | // S105.3.5: the evidence model was (re)built — provisional (stored) | ||
| 501 | // suggestions are replaced by fresh ones. | ||
| 502 |
1/1✓ Branch 16 → 17 taken 795 times.
|
795 | connect(FolderPredictionService::shared(), &FolderPredictionService::evidenceRebuilt, this, |
| 503 |
1/1✓ Branch 15 → 16 taken 795 times.
|
795 | [this](const QString &account) { |
| 504 | ✗ | if (account != accountName()) return; | |
| 505 | ✗ | m_predictions.clear(); | |
| 506 | ✗ | invalidatePredictions(); | |
| 507 | }); | ||
| 508 |
1/1✓ Branch 19 → 20 taken 795 times.
|
795 | connect(session.predictions(), &FolderPredictionSession::trainingFinished, this, |
| 509 | 795 | [this](bool completed, int, const QString &error) { | |
| 510 |
1/5✗ Branch 2 → 3 not taken.
✓ Branch 2 → 10 taken 659 times.
✗ Branch 3 → 4 not taken.
✗ Branch 5 → 6 not taken.
✗ Branch 6 → 7 not taken.
|
659 | if (!completed) setStatus(tr("Folder suggestions unavailable: %1").arg(error)); |
| 511 | 659 | }); | |
| 512 |
1/1✓ Branch 21 → 22 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::selectionChanged, this, &QuickMailBackend::invalidatePredictions); |
| 513 |
1/1✓ Branch 23 → 24 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::activeTabChanged, this, &QuickMailBackend::invalidatePredictions); |
| 514 |
1/1✓ Branch 25 → 26 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::areaChanged, this, &QuickMailBackend::invalidatePredictions); |
| 515 |
1/1✓ Branch 27 → 28 taken 795 times.
|
795 | connect(&m_mail, &QAbstractItemModel::modelReset, this, &QuickMailBackend::invalidatePredictions); |
| 516 |
1/1✓ Branch 29 → 30 taken 795 times.
|
795 | connect(&m_mail, &QAbstractItemModel::layoutChanged, this, &QuickMailBackend::invalidatePredictions); |
| 517 |
1/1✓ Branch 32 → 33 taken 795 times.
|
795 | connect(session.headers(), &QAbstractItemModel::dataChanged, this, &QuickMailBackend::invalidatePredictions); |
| 518 |
1/1✓ Branch 35 → 36 taken 795 times.
|
795 | connect(session.headers(), &QAbstractItemModel::rowsInserted, this, &QuickMailBackend::invalidatePredictions); |
| 519 |
1/1✓ Branch 38 → 39 taken 795 times.
|
795 | connect(session.headers(), &QAbstractItemModel::rowsRemoved, this, &QuickMailBackend::invalidatePredictions); |
| 520 | 795 | } | |
| 521 | |||
| 522 | // Selection follow-up after row removals (indexed, flat proxy, thread reset). | ||
| 523 | 795 | void QuickMailBackend::wireRemovalFollowUp() { | |
| 524 | 795 | MailSessionCore &session = m_session; | |
| 525 | // P90.16: mirror optimistic flat-model removals into the indexed thread | ||
| 526 | // list. Journaled moves remove rows from the flat/tree models immediately | ||
| 527 | // while the canonical header survives until the remote confirmation; the | ||
| 528 | // indexed page must drop the same identity visually so the freed slot and | ||
| 529 | // the reserved successor become visible in large folders too. The source | ||
| 530 | // model's own rowsAboutToBeRemoved still exposes the per-row identities. | ||
| 531 |
1/1✓ Branch 3 → 4 taken 795 times.
|
795 | connect(session.headers(), &QAbstractItemModel::rowsAboutToBeRemoved, this, |
| 532 | 795 | [this](const QModelIndex &parent, int first, int last) { | |
| 533 | // A flat-filter page hides rows; hidden is not deleted (S106-961). A | ||
| 534 | // window-slide eviction drops rows that still exist (S106-290). | ||
| 535 |
4/4✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 12 taken 144 times.
✓ Branch 8 → 9 taken 3 times.
✓ Branch 8 → 12 taken 1 time.
|
300 | if (parent.isValid() || !m_threadList.indexed() || |
| 536 |
5/6✓ Branch 3 → 4 taken 148 times.
✗ Branch 3 → 12 not taken.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 2 times.
✓ Branch 14 → 15 taken 146 times.
✓ Branch 14 → 16 taken 2 times.
|
300 | m_session.headers()->isHidingFilteredRows() || |
| 537 | 149 | m_session.headers()->isEvictingRetainedRows()) return; | |
| 538 | 2 | bool removedSelection = false; | |
| 539 | 2 | QSet<qint64> removedIndexedUids; | |
| 540 | 2 | const auto selected = m_workspace.mailId(); | |
| 541 |
2/2✓ Branch 37 → 20 taken 2 times.
✓ Branch 37 → 38 taken 2 times.
|
4 | for (int row = first; row <= last; ++row) { |
| 542 |
1/1✓ Branch 21 → 22 taken 2 times.
|
2 | const auto *header = m_session.headers()->headerAt(row); |
| 543 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2 times.
|
2 | if (!header) continue; |
| 544 |
3/6✓ Branch 25 → 26 taken 2 times.
✗ Branch 25 → 29 not taken.
✓ Branch 26 → 27 taken 2 times.
✗ Branch 26 → 29 not taken.
✓ Branch 30 → 31 taken 2 times.
✗ Branch 30 → 32 not taken.
|
4 | if (selected.isValid() && selected.folderId == header->folderId && |
| 545 |
1/2✓ Branch 27 → 28 taken 2 times.
✗ Branch 27 → 29 not taken.
|
2 | selected.uid == header->uid) |
| 546 | 2 | removedSelection = true; | |
| 547 |
1/2✓ Branch 33 → 34 taken 2 times.
✗ Branch 33 → 36 not taken.
|
2 | if (header->folderId == m_threadList.indexedFolder()) |
| 548 |
1/1✓ Branch 34 → 35 taken 2 times.
|
2 | removedIndexedUids.insert(header->uid); |
| 549 | } | ||
| 550 |
1/1✓ Branch 39 → 40 taken 2 times.
|
2 | m_threadList.removeIndexedUids(m_threadList.indexedFolder(), removedIndexedUids); |
| 551 | // The removal freed the selected slot: converge selection once per | ||
| 552 | // batched mutation, after all ranges settled (same contract as the flat | ||
| 553 | // follow-up below). The reserved successor wins over any row-0 fallback. | ||
| 554 | // (Multi-range successor mapping stays open: the asynchronous bulk | ||
| 555 | // selection move resolves through MailSelectionService, whose chunked | ||
| 556 | // removal sequencing deserves its own continuity contract.) | ||
| 557 |
3/6✓ Branch 40 → 41 taken 2 times.
✗ Branch 40 → 50 not taken.
✓ Branch 42 → 43 taken 2 times.
✗ Branch 42 → 50 not taken.
✓ Branch 48 → 49 taken 2 times.
✗ Branch 48 → 50 not taken.
|
4 | if (removedSelection && m_workspace.activeTab() == 0 && |
| 558 |
4/8✓ Branch 51 → 52 taken 2 times.
✗ Branch 51 → 53 not taken.
✓ Branch 53 → 54 taken 2 times.
✗ Branch 53 → 55 not taken.
✓ Branch 55 → 56 taken 2 times.
✗ Branch 55 → 57 not taken.
✓ Branch 57 → 58 taken 2 times.
✗ Branch 57 → 60 not taken.
|
4 | m_workspace.area() == QStringLiteral("mail")) |
| 559 |
1/1✓ Branch 58 → 59 taken 2 times.
|
2 | QTimer::singleShot(0, this, &QuickMailBackend::reconcileSelection); |
| 560 | 2 | }); | |
| 561 | // Removal follow-up on the PROXY signal: the proxy's own | ||
| 562 | // rowsAboutToBeRemoved fires while its mapping is still intact, so the | ||
| 563 | // selected mail's visual row is readable. The source-level signal runs | ||
| 564 | // after QSortFilterProxy already tore the mapping down (rowForIdentity | ||
| 565 | // would answer -1 there, and the old clamped lookup landed on row zero). | ||
| 566 | 795 | connect(&m_mail, &QAbstractItemModel::rowsAboutToBeRemoved, this, | |
| 567 |
1/1✓ Branch 5 → 6 taken 795 times.
|
795 | [this](const QModelIndex &parent, int first, int last) { |
| 568 | // A filtered page hides rows; a hidden selection stays (reconcileSelection). | ||
| 569 |
4/4✓ Branch 6 → 7 taken 143 times.
✓ Branch 6 → 15 taken 5 times.
✓ Branch 9 → 10 taken 135 times.
✓ Branch 9 → 15 taken 8 times.
|
291 | if (parent.isValid() || m_session.headers()->isEvictingRetainedRows() || |
| 570 |
2/2✓ Branch 11 → 12 taken 50 times.
✓ Branch 11 → 15 taken 85 times.
|
278 | m_session.headers()->isHidingFilteredRows() || |
| 571 |
6/8✓ Branch 3 → 4 taken 148 times.
✗ Branch 3 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 50 times.
✓ Branch 17 → 18 taken 50 times.
✓ Branch 17 → 19 taken 98 times.
✓ Branch 19 → 20 taken 98 times.
✓ Branch 19 → 21 taken 50 times.
|
437 | m_workspace.activeTab() != 0 || m_workspace.area() != "mail") return; |
| 572 | // Threaded mode: the visible projection spans the complete folder and is | ||
| 573 | // rebuilt by the controller's thread-model reset, so reconcileSelection | ||
| 574 | // owns the selection follow-up after real removals. | ||
| 575 | 50 | const bool threadedActive = threadedPresentation(); | |
| 576 |
2/2✓ Branch 22 → 23 taken 4 times.
✓ Branch 22 → 24 taken 46 times.
|
50 | if (threadedActive) return; |
| 577 | 46 | const auto selected = m_workspace.mailId(); | |
| 578 | // Membership check on the visible model, not acceptsIdentity(): an | ||
| 579 | // optimistic move already relocated the cached header when the model row | ||
| 580 | // is removed, so identity admission would wrongly skip the follow-up. | ||
| 581 |
3/3✓ Branch 27 → 28 taken 46 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 45 times.
|
46 | if (m_session.headers()->rowForUid(selected.uid, selected.folderId) < 0) return; |
| 582 | // The removal handler on the SOURCE model fires after QSortFilterProxy | ||
| 583 | // already tore down its mapping (rowForIdentity would answer -1 and the | ||
| 584 | // old clamped lookup landed on row zero). This handler therefore runs on | ||
| 585 | // the proxy's own rowsAboutToBeRemoved, where the selected mail's visual | ||
| 586 | // row is still readable and only visible rows are reported at all. | ||
| 587 |
2/2✓ Branch 30 → 31 taken 45 times.
✓ Branch 31 → 32 taken 45 times.
|
45 | const int visualRow = m_mail.rowForIdentity(WorkspaceState::encode(selected)); |
| 588 |
3/4✓ Branch 33 → 34 taken 44 times.
✓ Branch 33 → 35 taken 1 time.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 44 times.
|
45 | if (visualRow < first || visualRow > last) return; |
| 589 | // TEST-LOAD-TIMING: the visible neighbour, remembered by key. A page that | ||
| 590 | // arrives before the follow-up runs can slide the retained window, shift | ||
| 591 | // every row and evict the neighbour itself; an evicted (not removed) | ||
| 592 | // neighbour still exists and stays the target. | ||
| 593 |
3/3✓ Branch 37 → 38 taken 44 times.
✓ Branch 38 → 39 taken 42 times.
✓ Branch 38 → 40 taken 2 times.
|
44 | const int neighbourRow = last + 1 < m_mail.rowCount() ? last + 1 : first - 1; |
| 594 |
3/3✓ Branch 41 → 42 taken 43 times.
✓ Branch 41 → 43 taken 1 time.
✓ Branch 42 → 44 taken 43 times.
|
44 | const MailSelectionKey neighbour = neighbourRow >= 0 ? m_mail.keyAt(neighbourRow) : MailSelectionKey{}; |
| 595 |
1/1✓ Branch 44 → 45 taken 44 times.
|
44 | auto neighbourEvicted = std::make_shared<bool>(false); |
| 596 | 44 | const auto slid = connect(m_session.headers(), &MailListModel::windowSlid, this, | |
| 597 |
0/2✗ Branch 76 → 77 not taken.
✗ Branch 76 → 78 not taken.
|
88 | [this, neighbour, neighbourEvicted](int slidFirst, int slidLast) { |
| 598 | ✗ | for (int row = slidFirst; row <= slidLast; ++row) | |
| 599 | ✗ | if (const auto *header = m_session.headers()->headerAt(row); | |
| 600 | ✗ | header && header->uid == neighbour.uid && header->folderId == neighbour.folderId) | |
| 601 | ✗ | *neighbourEvicted = true; | |
| 602 |
1/1✓ Branch 48 → 49 taken 44 times.
|
44 | }); |
| 603 |
1/1✓ Branch 50 → 51 taken 44 times.
|
44 | const auto generation = m_workspace.generation(); |
| 604 | // A multi-message mutation may remove several ranges synchronously. Wait | ||
| 605 | // for the final surviving visual order; never select/read intermediate rows. | ||
| 606 |
2/10✓ Branch 54 → 55 taken 44 times.
✓ Branch 56 → 57 taken 44 times.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
✗ Branch 85 → 86 not taken.
✗ Branch 85 → 87 not taken.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 93 not taken.
|
44 | QTimer::singleShot(0, this, [this, selected, visualRow, neighbour, neighbourEvicted, slid, generation] { |
| 607 | 43 | disconnect(slid); | |
| 608 |
4/8✓ Branch 8 → 9 taken 39 times.
✗ Branch 8 → 17 not taken.
✓ Branch 10 → 11 taken 39 times.
✗ Branch 10 → 17 not taken.
✓ Branch 23 → 24 taken 4 times.
✓ Branch 23 → 25 taken 39 times.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
|
121 | if (m_workspace.generation() != generation || m_workspace.mailId() != selected || |
| 609 |
7/11✓ Branch 3 → 4 taken 43 times.
✓ Branch 5 → 6 taken 39 times.
✓ Branch 5 → 17 taken 4 times.
✓ Branch 13 → 14 taken 39 times.
✗ Branch 13 → 17 not taken.
✓ Branch 19 → 20 taken 39 times.
✓ Branch 19 → 21 taken 4 times.
✓ Branch 21 → 22 taken 43 times.
✗ Branch 21 → 23 not taken.
✗ Branch 57 → 58 not taken.
✗ Branch 57 → 59 not taken.
|
160 | m_workspace.activeTab() != 0 || m_workspace.area() != "mail" || |
| 610 |
2/3✓ Branch 15 → 16 taken 39 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 39 times.
|
43 | m_session.headers()->rowForUid(selected.uid, selected.folderId) >= 0) return; |
| 611 | // The surviving neighbour takes over the removed visual row: the | ||
| 612 | // selection advances to it, never back to the top of the folder. | ||
| 613 |
1/2✓ Branch 25 → 26 taken 39 times.
✗ Branch 25 → 30 not taken.
|
39 | const int neighbourNow = neighbour.uid < 0 ? -1 |
| 614 |
4/14✓ Branch 27 → 28 taken 39 times.
✓ Branch 28 → 29 taken 39 times.
✓ Branch 31 → 32 taken 39 times.
✗ Branch 31 → 33 not taken.
✓ Branch 33 → 34 taken 39 times.
✗ Branch 33 → 35 not taken.
✗ Branch 66 → 67 not taken.
✗ Branch 66 → 68 not taken.
✗ Branch 70 → 71 not taken.
✗ Branch 70 → 72 not taken.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 77 not taken.
✗ Branch 75 → 76 not taken.
✗ Branch 75 → 77 not taken.
|
39 | : m_mail.rowForIdentity(WorkspaceState::encode(MailId{accountName(), neighbour.folderId, neighbour.uid})); |
| 615 |
2/2✓ Branch 35 → 36 taken 38 times.
✓ Branch 35 → 37 taken 1 time.
|
39 | if (neighbourNow >= 0) selectMailRow(neighbourNow); |
| 616 |
1/3✗ Branch 38 → 39 not taken.
✓ Branch 38 → 44 taken 1 time.
✗ Branch 41 → 42 not taken.
|
1 | else if (*neighbourEvicted) m_selection.select(neighbour); |
| 617 |
1/4✗ Branch 45 → 46 not taken.
✓ Branch 45 → 50 taken 1 time.
✗ Branch 46 → 47 not taken.
✗ Branch 48 → 49 not taken.
|
1 | else if (activeRowCount()) selectMailRow(qMin(visualRow, activeRowCount() - 1)); |
| 618 | else { | ||
| 619 | 1 | m_selection.clear(); | |
| 620 | 1 | m_mainCurrent = {}; | |
| 621 |
1/1✓ Branch 53 → 54 taken 1 time.
|
1 | m_workspace.select({}); |
| 622 | } | ||
| 623 | }); | ||
| 624 |
2/2✓ Branch 64 → 65 taken 44 times.
✓ Branch 64 → 67 taken 2 times.
|
46 | }); |
| 625 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | connect(&m_threadList, &QAbstractItemModel::modelAboutToBeReset, this, [this] { |
| 626 |
3/4✓ Branch 4 → 5 taken 41 times.
✓ Branch 4 → 10 taken 22 times.
✓ Branch 6 → 7 taken 41 times.
✗ Branch 6 → 10 not taken.
|
104 | if (!threadedPresentation() || m_threadProjectionPending || |
| 627 |
8/8✓ Branch 3 → 4 taken 63 times.
✓ Branch 3 → 10 taken 2515 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 38 times.
✓ Branch 12 → 13 taken 41 times.
✓ Branch 12 → 14 taken 2537 times.
✓ Branch 14 → 15 taken 2540 times.
✓ Branch 14 → 16 taken 38 times.
|
2718 | m_workspace.activeTab() != 0 || m_workspace.area() != "mail") return; |
| 628 | 38 | const auto selected = m_workspace.mailId(); | |
| 629 |
2/2✓ Branch 18 → 19 taken 38 times.
✓ Branch 19 → 20 taken 38 times.
|
38 | const int row = m_threadList.rowForIdentity(WorkspaceState::encode(selected)); |
| 630 |
6/6✓ Branch 22 → 23 taken 28 times.
✓ Branch 22 → 24 taken 10 times.
✓ Branch 23 → 24 taken 26 times.
✓ Branch 23 → 25 taken 2 times.
✓ Branch 26 → 27 taken 36 times.
✓ Branch 26 → 28 taken 2 times.
|
38 | if (!selected.isValid() || row < 0) return; |
| 631 |
1/1✓ Branch 28 → 29 taken 2 times.
|
2 | const auto generation = m_workspace.generation(); |
| 632 |
1/3✓ Branch 31 → 32 taken 2 times.
✗ Branch 46 → 47 not taken.
✗ Branch 46 → 48 not taken.
|
2 | QTimer::singleShot(0, this, [this, selected, row, generation] { |
| 633 |
1/2✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 16 not taken.
|
2 | if (!threadedPresentation() || m_threadProjectionPending || |
| 634 |
5/11✓ Branch 5 → 6 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 16 not taken.
✓ Branch 10 → 11 taken 2 times.
✗ Branch 10 → 16 not taken.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 16 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 2 times.
✗ Branch 54 → 55 not taken.
✗ Branch 54 → 56 not taken.
|
6 | m_workspace.generation() != generation || m_workspace.mailId() != selected || |
| 635 |
4/10✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 16 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 2 times.
✓ Branch 18 → 19 taken 2 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 22 not taken.
✗ Branch 49 → 50 not taken.
✗ Branch 49 → 51 not taken.
|
6 | m_workspace.activeTab() != 0 || m_workspace.area() != "mail") return; |
| 636 | // Filtering/collapsing only hides a row. A real removal also removes | ||
| 637 | // its canonical identity from the owning tree. Coalesce bulk resets | ||
| 638 | // before selecting the final surviving neighbour. | ||
| 639 |
1/3✗ Branch 25 → 26 not taken.
✓ Branch 25 → 28 taken 2 times.
✗ Branch 26 → 27 not taken.
|
2 | if (m_threadList.indexed() ? acceptsIdentity(selected) |
| 640 |
2/3✓ Branch 29 → 30 taken 2 times.
✗ Branch 32 → 33 not taken.
✓ Branch 32 → 34 taken 2 times.
|
2 | : m_session.threads()->indexForUid(selected.uid, selected.folderId).isValid()) return; |
| 641 |
4/5✓ Branch 35 → 36 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
✗ Branch 36 → 42 not taken.
✓ Branch 38 → 39 taken 2 times.
✓ Branch 40 → 41 taken 2 times.
|
2 | if (m_threadList.rowCount()) selectMailRow(qMin(row, m_threadList.rowCount() - 1)); |
| 642 | else { | ||
| 643 | ✗ | m_selection.clear(); | |
| 644 | ✗ | m_mainCurrent = {}; | |
| 645 | ✗ | m_workspace.select({}); | |
| 646 | } | ||
| 647 | }); | ||
| 648 |
2/2✓ Branch 36 → 37 taken 2 times.
✓ Branch 36 → 39 taken 36 times.
|
38 | }); |
| 649 | 795 | } | |
| 650 | |||
| 651 | // Prediction resets on folder changes and row selection providers. | ||
| 652 | 795 | void QuickMailBackend::wireFolderChangeResets() { | |
| 653 | 795 | MailSessionCore &session = m_session; | |
| 654 |
1/1✓ Branch 2 → 3 taken 795 times.
|
795 | connect(this, &QuickMailBackend::folderChanged, this, [this] { |
| 655 | // F95-15: the suggestion column (Ctrl+S) is a persistent view setting; | ||
| 656 | // it survives folder switches and restarts. | ||
| 657 | 925 | m_alternatePredictions.clear(); | |
| 658 | 925 | disarmQuickMove(); | |
| 659 | 925 | invalidatePredictions(); | |
| 660 | 925 | }); | |
| 661 |
1/1✓ Branch 5 → 6 taken 795 times.
|
795 | connect(session.controller(), &MailController::folderIdentityInvalidated, this, [this] { |
| 662 | 11 | m_alternatePredictions.clear(); | |
| 663 | 11 | invalidatePredictions(); | |
| 664 | 11 | }); | |
| 665 | 39790 | m_mail.setSelectionProvider([this](qint64, qint64 uid) { return m_selection.contains(uid); }); | |
| 666 | 7570 | m_threadList.setSelectionProvider([this](qint64, qint64 uid) { return m_selection.contains(uid); }); | |
| 667 | 795 | } | |
| 668 | |||
| 669 | // Search coordinator, cached-page presentation and FTS readiness. | ||
| 670 | 795 | void QuickMailBackend::wireSearch(MailSearchReadService *searchReader) { | |
| 671 | 795 | MailSessionCore &session = m_session; | |
| 672 | // The command bar's previews (the full search runs in search tabs). | ||
| 673 | 795 | SearchCoordinator::Deps searchDeps; | |
| 674 | 795 | searchDeps.cache = session.cache(); | |
| 675 | 865 | searchDeps.localIndexReady = [&session] { return session.fts()->isSearchReady(); }; | |
| 676 | 865 | searchDeps.localIndexGeneration = [&session] { return session.fts()->completionGeneration(); }; | |
| 677 | 865 | searchDeps.accountScope = [this] { return accountName(); }; | |
| 678 |
3/3✓ Branch 7 → 8 taken 793 times.
✓ Branch 7 → 10 taken 2 times.
✓ Branch 8 → 9 taken 793 times.
|
795 | searchDeps.readService = searchReader ? searchReader : MailSearchReadService::shared(); |
| 679 | 1734 | searchDeps.acceptCachedResult = [this](const MailId &id, const QString &path) { | |
| 680 |
5/10✓ Branch 3 → 4 taken 144 times.
✗ Branch 3 → 12 not taken.
✓ Branch 5 → 6 taken 144 times.
✗ Branch 5 → 12 not taken.
✓ Branch 8 → 9 taken 144 times.
✗ Branch 8 → 12 not taken.
✓ Branch 10 → 11 taken 144 times.
✗ Branch 10 → 12 not taken.
✓ Branch 14 → 15 taken 144 times.
✗ Branch 14 → 16 not taken.
|
144 | return configured() && id.isValid() && id.accountId == accountName() && m_availableFolders.contains(path); |
| 681 | 795 | }; | |
| 682 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | m_search = std::make_unique<SearchCoordinator>(searchDeps); |
| 683 | // T-87.D1: the cached first page now arrives asynchronously from the | ||
| 684 | // foreground reader; reconcile the workspace selection once it committed. | ||
| 685 |
1/1✓ Branch 16 → 17 taken 795 times.
|
795 | connect(session.controller(), &MailController::cachedFolderPresented, this, |
| 686 | 795 | [this](qint64 folderId, int, quint64) { | |
| 687 | // The controller may have just ensured a folder that was absent | ||
| 688 | // from the account's startup identity snapshot. Retain that | ||
| 689 | // account-scoped identity for subsequent Quick interactions. | ||
| 690 | 695 | const QString path = m_session.controller()->currentFolder(); | |
| 691 |
3/6✓ Branch 4 → 5 taken 695 times.
✗ Branch 4 → 13 not taken.
✓ Branch 6 → 7 taken 695 times.
✗ Branch 6 → 13 not taken.
✓ Branch 11 → 12 taken 695 times.
✗ Branch 11 → 13 not taken.
|
1390 | if (folderId > 0 && !path.isEmpty() && |
| 692 |
3/6✓ Branch 14 → 15 taken 695 times.
✗ Branch 14 → 16 not taken.
✓ Branch 16 → 17 taken 695 times.
✗ Branch 16 → 18 not taken.
✓ Branch 18 → 19 taken 695 times.
✗ Branch 18 → 41 not taken.
|
1390 | m_session.controller()->accountId() == accountName()) { |
| 693 | 695 | const auto previousPath = m_folderPathById.value(folderId); | |
| 694 |
4/6✓ Branch 21 → 22 taken 694 times.
✓ Branch 21 → 25 taken 1 time.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 694 times.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 28 taken 695 times.
|
695 | if (!previousPath.isEmpty() && previousPath != path) |
| 695 | ✗ | m_folderIdByPath.remove(previousPath); | |
| 696 | 695 | const auto previousId = m_folderIdByPath.value(path, -1); | |
| 697 |
3/4✓ Branch 29 → 30 taken 694 times.
✓ Branch 29 → 33 taken 1 time.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 33 taken 694 times.
|
695 | if (previousId > 0 && previousId != folderId) { |
| 698 | ✗ | m_folderPathById.remove(previousId); | |
| 699 | ✗ | m_folderEpochById.remove(previousId); | |
| 700 | } | ||
| 701 |
1/1✓ Branch 33 → 34 taken 695 times.
|
695 | m_folderIdByPath.insert(path, folderId); |
| 702 |
1/1✓ Branch 34 → 35 taken 695 times.
|
695 | m_folderPathById.insert(folderId, path); |
| 703 |
2/2✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 39 taken 694 times.
|
695 | if (!m_folderEpochById.contains(folderId)) |
| 704 |
1/1✓ Branch 37 → 38 taken 1 time.
|
1 | m_folderEpochById.insert(folderId, 0); |
| 705 | 695 | } | |
| 706 | 695 | m_waitingCachedPage = false; | |
| 707 |
6/6✓ Branch 41 → 42 taken 695 times.
✓ Branch 45 → 46 taken 695 times.
✓ Branch 46 → 47 taken 695 times.
✓ Branch 47 → 48 taken 695 times.
✓ Branch 51 → 43 taken 695 times.
✓ Branch 51 → 52 taken 695 times.
|
1390 | qCDebug(lcQuickBackend) << "cached page presented for folder" << folderId; |
| 708 |
1/1✓ Branch 52 → 53 taken 695 times.
|
695 | syncThreadProjection(); |
| 709 | // The cached page is the flat thread list's complete content; an | ||
| 710 | // empty folder ends the previous folder's hold here. | ||
| 711 |
3/3✓ Branch 54 → 55 taken 686 times.
✓ Branch 54 → 56 taken 9 times.
✓ Branch 55 → 56 taken 686 times.
|
695 | if (!m_threadList.indexed()) m_threadList.releaseHeldRows(); |
| 712 |
3/3✓ Branch 56 → 57 taken 692 times.
✓ Branch 56 → 58 taken 3 times.
✓ Branch 57 → 58 taken 692 times.
|
695 | if (!m_reconciling) reconcileSelection(); |
| 713 | 695 | }); | |
| 714 |
1/1✓ Branch 19 → 20 taken 795 times.
|
795 | connect(m_search.get(), &SearchCoordinator::statusMessage, this, &QuickMailBackend::setStatus); |
| 715 |
1/1✓ Branch 22 → 23 taken 795 times.
|
795 | connect(session.fts(), &FtsIndexCoordinator::rebuildFinished, this, [this](bool success, quint64) { |
| 716 | ✗ | if (success && configured() && !m_searchPreviewQuery.isEmpty()) m_searchDebounce.start(); | |
| 717 | ✗ | }); | |
| 718 | 795 | } | |
| 719 | |||
| 720 | // Sprint 101: the search tabs — one MailSearchSession per search tab. | ||
| 721 | 795 | void QuickMailBackend::wireSearchTabs(MailSearchReadService *searchReader) { | |
| 722 | 795 | MailSessionCore &session = m_session; | |
| 723 | 795 | MailSearchSessions::Deps deps; | |
| 724 | 795 | deps.workspace = &m_workspace; | |
| 725 | 795 | deps.controller = session.controller(); | |
| 726 |
1/2✓ Branch 3 → 4 taken 93 times.
✗ Branch 3 → 5 not taken.
|
888 | deps.currentAccount = [this] { return configured() ? accountName() : QString(); }; |
| 727 | 894 | deps.acceptIdentity = [this](const MailId &id) { return acceptsIdentity(id); }; | |
| 728 | ✗ | deps.sessionDeps = [this, &session, searchReader](const QString &account) { | |
| 729 | 89 | MailSearchSession::Deps d; | |
| 730 | 89 | d.cache = session.cache(); | |
| 731 |
3/3✓ Branch 4 → 5 taken 85 times.
✓ Branch 4 → 7 taken 4 times.
✓ Branch 5 → 6 taken 85 times.
|
89 | d.reader = searchReader ? searchReader : MailSearchReadService::shared(); |
| 732 | 89 | d.server = session.controller()->search(); | |
| 733 | 365 | d.indexReady = [&session] { return session.fts()->isSearchReady(); }; | |
| 734 | 479 | d.indexGeneration = [&session] { return session.fts()->completionGeneration(); }; | |
| 735 | 178 | d.acceptResult = [this, account](const MailId &id, const QString &path) { | |
| 736 |
5/10✓ Branch 3 → 4 taken 14751 times.
✗ Branch 3 → 14 not taken.
✓ Branch 5 → 6 taken 14751 times.
✗ Branch 5 → 14 not taken.
✓ Branch 7 → 8 taken 14751 times.
✗ Branch 7 → 14 not taken.
✓ Branch 10 → 11 taken 14751 times.
✗ Branch 10 → 14 not taken.
✓ Branch 12 → 13 taken 14751 times.
✗ Branch 12 → 14 not taken.
|
44253 | return configured() && id.isValid() && id.accountId == account && account == accountName() && |
| 737 |
1/2✓ Branch 15 → 16 taken 14751 times.
✗ Branch 15 → 17 not taken.
|
29502 | m_availableFolders.contains(path); |
| 738 |
1/1✓ Branch 13 → 14 taken 89 times.
|
89 | }; |
| 739 | 244 | d.allowedFolders = [this] { return m_availableFolders; }; | |
| 740 | 694 | d.folderTree = [this] { | |
| 741 | 516 | QList<SearchMenuModel::FolderRow> rows; | |
| 742 |
5/5✓ Branch 2 → 3 taken 516 times.
✓ Branch 3 → 4 taken 516 times.
✓ Branch 4 → 5 taken 516 times.
✓ Branch 13 → 6 taken 1558 times.
✓ Branch 13 → 14 taken 516 times.
|
2074 | for (const auto &row : m_folders.treeRows()) rows.append({row.path, row.name, row.depth}); |
| 743 | 516 | return rows; | |
| 744 |
1/5✓ Branch 9 → 10 taken 1558 times.
✗ Branch 19 → 20 not taken.
✗ Branch 19 → 21 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
|
1647 | }; |
| 745 | 230 | d.serverEnabled = [&session] { return session.accounts()->networkEnabled(); }; | |
| 746 | 2440 | d.lookups = [this] { | |
| 747 | 2262 | SearchTermModel::Lookups lookups; | |
| 748 | 2633 | lookups.label = [](const QString &keyword) { return LabelDelegate::displayName(keyword); }; | |
| 749 | 2262 | return lookups; | |
| 750 | 89 | }; | |
| 751 | 89 | return d; | |
| 752 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | }; |
| 753 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | m_searchTabs = std::make_unique<MailSearchSessions>(deps); |
| 754 |
1/1✓ Branch 11 → 12 taken 795 times.
|
795 | m_workspace.setActiveAccount(accountName()); |
| 755 |
1/1✓ Branch 14 → 15 taken 795 times.
|
795 | connect(session.fts(), &FtsIndexCoordinator::rebuildFinished, this, [this](bool success, quint64) { |
| 756 | ✗ | if (success && configured()) m_searchTabs->refreshAfterIndexReady(); | |
| 757 | ✗ | }); | |
| 758 | 795 | } | |
| 759 | |||
| 760 | // Search preview debounce and quick result suggestions. | ||
| 761 | 795 | void QuickMailBackend::wireSearchSuggestions() { | |
| 762 | 795 | m_searchDebounce.setSingleShot(true); | |
| 763 | 795 | m_searchDebounce.setInterval(20); | |
| 764 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(&m_searchDebounce, &QTimer::timeout, this, [this] { |
| 765 |
3/6✓ Branch 3 → 4 taken 37 times.
✗ Branch 3 → 7 not taken.
✓ Branch 5 → 6 taken 37 times.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 37 times.
✗ Branch 8 → 11 not taken.
|
37 | if (configured() && !m_availableFolders.isEmpty()) m_search->updateQuickResults(m_searchPreviewQuery); |
| 766 | 37 | }); | |
| 767 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | connect(m_search.get(), &SearchCoordinator::quickResultsChanged, this, [this] { |
| 768 | 2105 | m_searchSuggestions.clear(); | |
| 769 |
2/2✓ Branch 55 → 4 taken 144 times.
✓ Branch 55 → 56 taken 2105 times.
|
2249 | for (int i = 0; i < m_search->quickResultCount(); ++i) { |
| 770 |
1/1✓ Branch 5 → 6 taken 144 times.
|
144 | const auto result = m_search->quickResultAt(i); |
| 771 | 144 | const MailId id{accountName(), result.folderId, result.uid}; | |
| 772 |
3/6✓ Branch 8 → 9 taken 144 times.
✗ Branch 8 → 11 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 144 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 144 times.
|
144 | if (!id.isValid() || !m_availableFolders.contains(result.folderPath)) continue; |
| 773 | // S98.1.11: structured fields for the command bar's result rows; the | ||
| 774 | // preformatted label stays for the legacy list until Phase 4. | ||
| 775 |
4/4✓ Branch 16 → 17 taken 144 times.
✓ Branch 17 → 18 taken 144 times.
✓ Branch 18 → 19 taken 144 times.
✓ Branch 29 → 30 taken 144 times.
|
432 | m_searchSuggestions.append(QVariantMap{{"label", QString("[%1] %2 — %3").arg(result.folderPath, result.subject.left(120), result.from.left(80))}, |
| 776 | 144 | {"identity", WorkspaceState::encode(id)}, {"subject", result.subject.left(240)}, | |
| 777 |
2/4✓ Branch 33 → 34 taken 720 times.
✓ Branch 33 → 35 taken 144 times.
✗ Branch 63 → 64 not taken.
✗ Branch 63 → 65 not taken.
|
864 | {"from", result.from.left(160)}, {"folderPath", result.folderPath}}); |
| 778 | 144 | } | |
| 779 | 2105 | emit searchSuggestionsChanged(); | |
| 780 |
10/14✓ Branch 15 → 16 taken 144 times.
✓ Branch 19 → 20 taken 144 times.
✓ Branch 20 → 21 taken 144 times.
✓ Branch 21 → 22 taken 144 times.
✓ Branch 22 → 23 taken 144 times.
✓ Branch 23 → 24 taken 144 times.
✓ Branch 24 → 25 taken 144 times.
✓ Branch 25 → 26 taken 144 times.
✓ Branch 26 → 27 taken 144 times.
✓ Branch 27 → 28 taken 144 times.
✗ Branch 87 → 88 not taken.
✗ Branch 87 → 91 not taken.
✗ Branch 89 → 90 not taken.
✗ Branch 89 → 91 not taken.
|
2393 | }); |
| 781 | 795 | } | |
| 782 | |||
| 783 | // Mail selection model, folder expansion and workspace selection reconcile. | ||
| 784 | 795 | void QuickMailBackend::wireSelection() { | |
| 785 | 795 | WorkspaceState &workspace = m_workspace; | |
| 786 |
1/1✓ Branch 3 → 4 taken 795 times.
|
795 | connect(&m_selection, &QuickMailSelection::changed, this, |
| 787 | 1590 | [this, previousSimple = true, previous = MailSelectionKey{}]() mutable { | |
| 788 |
6/6✓ Branch 3 → 4 taken 2786 times.
✓ Branch 3 → 8 taken 37 times.
✓ Branch 6 → 7 taken 2717 times.
✓ Branch 6 → 8 taken 69 times.
✓ Branch 9 → 10 taken 2786 times.
✓ Branch 9 → 11 taken 37 times.
|
2823 | const bool simple = !m_selection.pending() && !m_selection.snapshot(); |
| 789 |
2/2✓ Branch 11 → 12 taken 2717 times.
✓ Branch 11 → 13 taken 106 times.
|
2823 | const MailSelectionKey current = simple ? m_selection.single() : MailSelectionKey{}; |
| 790 |
4/4✓ Branch 14 → 15 taken 2731 times.
✓ Branch 14 → 27 taken 92 times.
✓ Branch 15 → 16 taken 2703 times.
✓ Branch 15 → 27 taken 28 times.
|
2823 | if (previousSimple && simple) { |
| 791 | // A plain single click changes only the previous and current rows. | ||
| 792 | // Snapshot/range selection still compares the bounded visible window. | ||
| 793 |
1/1✓ Branch 18 → 19 taken 2703 times.
|
2703 | m_mail.singleSelectionChanged(previous, current); |
| 794 |
1/1✓ Branch 23 → 24 taken 2703 times.
|
2703 | m_threadList.singleSelectionChanged(previous, current); |
| 795 | } else { | ||
| 796 |
1/1✓ Branch 27 → 28 taken 120 times.
|
120 | m_mail.selectionChanged(); |
| 797 |
1/1✓ Branch 28 → 29 taken 120 times.
|
120 | m_threadList.selectionChanged(); |
| 798 | } | ||
| 799 | 2823 | previousSimple = simple; | |
| 800 | 2823 | previous = current; | |
| 801 |
1/1✓ Branch 30 → 31 taken 2823 times.
|
2823 | emit selectionChanged(); |
| 802 | 2823 | }); | |
| 803 | // F100-77: a search tab's marked rows show their count only. | ||
| 804 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | connect(this, &QuickMailBackend::selectionChanged, this, [this] { |
| 805 |
3/3✓ Branch 4 → 5 taken 26 times.
✓ Branch 4 → 6 taken 2815 times.
✓ Branch 7 → 8 taken 2841 times.
|
2841 | m_selectionSummary.update(m_workspace.activeIsSearch() ? nullptr : m_selection.snapshot(), selectionCount()); |
| 806 | 2841 | }); | |
| 807 |
1/1✓ Branch 8 → 9 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::folderExpansionChanged, this, &QuickMailBackend::folderExpansionChanged); |
| 808 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | connect(this, &QuickMailBackend::accountChanged, this, &QuickMailBackend::folderExpansionChanged); |
| 809 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(&m_selection, &QuickMailSelection::failed, this, [this] { |
| 810 | ✗ | setStatus(tr("Selection failed. Select again.")); | |
| 811 | ✗ | }); | |
| 812 |
1/1✓ Branch 14 → 15 taken 795 times.
|
795 | connect(&m_selection, &QuickMailSelection::currentRequested, this, [this](MailSelectionKey key) { |
| 813 |
4/8✓ Branch 3 → 4 taken 893 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 893 times.
✓ Branch 9 → 10 taken 893 times.
✗ Branch 9 → 11 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 893 times.
|
893 | if (m_workspace.activeTab() != 0 || m_workspace.area() != "mail") return; |
| 814 | 893 | QScopedValueRollback guard(m_applyingSelection, true); | |
| 815 | 893 | m_mainCurrent = {accountName(), key.folderId, key.uid}; | |
| 816 |
1/1✓ Branch 17 → 18 taken 893 times.
|
893 | m_workspace.select(m_mainCurrent); |
| 817 | 1786 | }); | |
| 818 |
1/1✓ Branch 16 → 17 taken 795 times.
|
795 | connect(this, &QuickMailBackend::folderChanged, this, &QuickMailBackend::refreshSelectionScope); |
| 819 |
1/1✓ Branch 18 → 19 taken 795 times.
|
795 | connect(&m_mail, &MailPresentationModel::mailFilterChanged, this, &QuickMailBackend::refreshSelectionScope); |
| 820 |
1/1✓ Branch 20 → 21 taken 795 times.
|
795 | connect(&m_threadList, &QuickThreadListModel::mailFilterChanged, this, &QuickMailBackend::refreshSelectionScope); |
| 821 |
1/1✓ Branch 22 → 23 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::activeTabChanged, &m_selection, &QuickMailSelection::cancelPending); |
| 822 |
1/1✓ Branch 24 → 25 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::areaChanged, &m_selection, &QuickMailSelection::cancelPending); |
| 823 |
1/1✓ Branch 26 → 27 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::selectionChanged, this, [this] { |
| 824 |
8/8✓ Branch 2 → 3 taken 1433 times.
✓ Branch 2 → 8 taken 136 times.
✓ Branch 4 → 5 taken 894 times.
✓ Branch 4 → 8 taken 539 times.
✓ Branch 7 → 8 taken 94 times.
✓ Branch 7 → 9 taken 800 times.
✓ Branch 10 → 11 taken 769 times.
✓ Branch 10 → 12 taken 800 times.
|
1569 | if (m_applyingSelection || m_workspace.activeTab() != 0 || m_workspace.mailId() == m_mainCurrent) return; |
| 825 |
2/2✓ Branch 12 → 13 taken 800 times.
✓ Branch 13 → 14 taken 800 times.
|
800 | const int row = activeRowForIdentity(m_workspace.selection()); |
| 826 |
4/4✓ Branch 15 → 16 taken 721 times.
✓ Branch 15 → 21 taken 79 times.
✓ Branch 17 → 18 taken 721 times.
✓ Branch 18 → 19 taken 721 times.
|
800 | if (row >= 0) m_selection.select(activeKeyAt(row)); |
| 827 | }); | ||
| 828 | 795 | } | |
| 829 | |||
| 830 | // Account list changes and the per-account state reset. | ||
| 831 | 795 | void QuickMailBackend::wireAccountReload() { | |
| 832 | 795 | MailSessionCore &session = m_session; | |
| 833 |
1/1✓ Branch 3 → 4 taken 795 times.
|
795 | connect(session.accounts(), &AccountSession::availableAccountsChanged, this, &QuickMailBackend::accountChanged); |
| 834 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | connect(session.accounts(), &AccountSession::configurationChanged, &m_rules, |
| 835 | 795 | &QuickRulesBackend::configurationChanged); | |
| 836 | // Sprint 95: the folder menu's rule entries follow rule availability (the | ||
| 837 | // "Rules" button sits with the sidebar actions, F100-67). | ||
| 838 | 892 | const auto syncRulesNode = [this] { | |
| 839 | 892 | m_folderActions.setRulesMenuEnabled(m_rules.available()); | |
| 840 | 1687 | }; | |
| 841 |
1/1✓ Branch 8 → 9 taken 795 times.
|
795 | connect(&m_rules, &QuickRulesBackend::stateChanged, this, syncRulesNode); |
| 842 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | connect(&m_rules, &QuickRulesBackend::documentChanged, this, syncRulesNode); |
| 843 | // F98-01 (revises F95-02): the rule manager is a document tab of the mail | ||
| 844 | // area, shown as the "rules" area. Opening it (folder-tree node, Ctrl+Shift+L, | ||
| 845 | // :rules, a draft) shows it; switching to a mail tab or another area keeps | ||
| 846 | // it open like a mail tab; closing it returns to mail. | ||
| 847 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(&m_rules, &QuickRulesBackend::managerOpenChanged, this, [this] { |
| 848 |
8/8✓ Branch 3 → 4 taken 26 times.
✓ Branch 3 → 9 taken 15 times.
✓ Branch 7 → 8 taken 12 times.
✓ Branch 7 → 9 taken 14 times.
✓ Branch 10 → 11 taken 26 times.
✓ Branch 10 → 12 taken 15 times.
✓ Branch 12 → 13 taken 12 times.
✓ Branch 12 → 20 taken 29 times.
|
41 | if (m_rules.managerOpen() && m_workspace.area() != QLatin1String("rules")) |
| 849 |
1/1✓ Branch 16 → 17 taken 12 times.
|
24 | m_workspace.setArea(QStringLiteral("rules")); |
| 850 |
7/8✓ Branch 21 → 22 taken 15 times.
✓ Branch 21 → 27 taken 14 times.
✓ Branch 25 → 26 taken 15 times.
✗ Branch 25 → 27 not taken.
✓ Branch 28 → 29 taken 15 times.
✓ Branch 28 → 30 taken 14 times.
✓ Branch 30 → 31 taken 15 times.
✓ Branch 30 → 38 taken 14 times.
|
29 | else if (!m_rules.managerOpen() && m_workspace.area() == QLatin1String("rules")) |
| 851 |
1/1✓ Branch 34 → 35 taken 15 times.
|
30 | m_workspace.setArea(QStringLiteral("mail")); |
| 852 | 41 | }); | |
| 853 |
1/1✓ Branch 14 → 15 taken 795 times.
|
795 | connect(&m_workspace, &WorkspaceState::areaChanged, this, [this] { |
| 854 |
7/8✓ Branch 5 → 6 taken 28 times.
✓ Branch 5 → 9 taken 627 times.
✓ Branch 7 → 8 taken 14 times.
✓ Branch 7 → 9 taken 14 times.
✓ Branch 10 → 11 taken 655 times.
✗ Branch 10 → 12 not taken.
✓ Branch 12 → 13 taken 14 times.
✓ Branch 12 → 17 taken 641 times.
|
655 | if (m_workspace.area() == QLatin1String("rules") && !m_rules.managerOpen()) |
| 855 |
1/1✓ Branch 14 → 15 taken 14 times.
|
14 | m_rules.openManager(); |
| 856 | 655 | }); | |
| 857 |
1/1✓ Branch 17 → 18 taken 795 times.
|
795 | connect(session.accounts(), &AccountSession::accountsLoaded, this, [this] { |
| 858 | 793 | m_rules.accountChanged(); | |
| 859 | 793 | m_compose.resetForAccountReload(); | |
| 860 | 793 | m_threadSnapshots.clear(); | |
| 861 | 793 | ++m_threadDataGeneration; | |
| 862 | 793 | m_headerProjectionTimer.stop(); | |
| 863 | 793 | m_headerProjectionFolder.clear(); | |
| 864 | 793 | cancelSearchPreview(); | |
| 865 | 793 | m_search->cancelQuickResults(); | |
| 866 | 793 | cancelNavigation(); | |
| 867 | 793 | m_selection.clear(); | |
| 868 | 793 | m_mainCurrent = {}; | |
| 869 | 793 | clearActionContinuity(); | |
| 870 |
1/1✓ Branch 16 → 17 taken 793 times.
|
793 | m_mail.setAccountId(accountName()); |
| 871 |
1/1✓ Branch 19 → 20 taken 793 times.
|
793 | m_threadList.setAccountId(accountName()); |
| 872 |
1/1✓ Branch 22 → 23 taken 793 times.
|
793 | m_workspace.setActiveAccount(accountName()); // D-15: other accounts' search tabs sleep |
| 873 | 793 | m_folderPath.clear(); | |
| 874 | 793 | m_previousFolder.clear(); | |
| 875 | 793 | m_availableFolders.clear(); | |
| 876 | 793 | m_folderIdByPath.clear(); | |
| 877 | 793 | m_folderPathById.clear(); | |
| 878 | 793 | m_folderEpochById.clear(); | |
| 879 | 793 | m_waitingCachedPage = false; | |
| 880 | 793 | cancelThreadProjection(); | |
| 881 | 793 | m_threadProjectionPending = false; | |
| 882 | 793 | clearThreadProjectionSlowFlag(); | |
| 883 | 793 | m_session.threads()->clear(); | |
| 884 | 793 | emit threadedChanged(); | |
| 885 | 793 | emit accountChanged(); | |
| 886 | 793 | loadLocalFolders(); | |
| 887 | 793 | }); | |
| 888 | 795 | } | |
| 889 | |||
| 890 | // Selection publishing and the P90.0 selection trace probe. | ||
| 891 | 795 | void QuickMailBackend::wireSelectionTrace() { | |
| 892 | 795 | MailSessionCore &session = m_session; | |
| 893 | 795 | WorkspaceState &workspace = m_workspace; | |
| 894 |
1/1✓ Branch 2 → 3 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::selectionChanged, this, &QuickMailBackend::publishSelection); |
| 895 | // P90.0 trace: one probe per selection until the confirmed Quick frame. | ||
| 896 | // The visible page phase settles as soon as the selected row resolves in | ||
| 897 | // the published model; catch-up and authoritative sync follow their own | ||
| 898 | // owners. Zero overhead unless a THREAD_SELECTION.* scenario is active. | ||
| 899 | 795 | auto &selectionTrace = MailJD::Performance::ThreadSelectionPerformanceTracker::instance(); | |
| 900 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::selectionChanged, this, |
| 901 | 1590 | [this, &selectionTrace, lastSelection = m_workspace.mailId()]() mutable { | |
| 902 | 1569 | const auto selected = m_workspace.mailId(); | |
| 903 |
2/2✓ Branch 5 → 6 taken 172 times.
✓ Branch 5 → 7 taken 1397 times.
|
1569 | if (selected == lastSelection) return; |
| 904 | 1397 | lastSelection = selected; | |
| 905 |
2/3✓ Branch 8 → 9 taken 1397 times.
✓ Branch 9 → 10 taken 1397 times.
✗ Branch 9 → 11 not taken.
|
1397 | if (!selectionTrace.isActive()) return; |
| 906 | ✗ | if (!selected.isValid()) return; | |
| 907 | ✗ | selectionTrace.beginSelection(selected); // origin from input classifier | |
| 908 | ✗ | if (m_threadList.indexed() && | |
| 909 | ✗ | m_threadList.rowForIdentity(m_workspace.selection()) >= 0) | |
| 910 | ✗ | selectionTrace.visiblePagePublished(m_threadList.indexRevision(), | |
| 911 | ✗ | m_threadList.rowCount()); | |
| 912 |
1/2✗ Branch 34 → 35 not taken.
✓ Branch 34 → 37 taken 1569 times.
|
1569 | }); |
| 913 | 795 | connect(&m_threadList, &QuickThreadListModel::projectionChanged, this, | |
| 914 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | [this, &selectionTrace] { |
| 915 |
3/7✓ Branch 2 → 3 taken 79 times.
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 6 taken 79 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
✓ Branch 8 → 9 taken 79 times.
✗ Branch 8 → 10 not taken.
|
79 | if (!selectionTrace.isActive() || !m_threadList.indexed()) return; |
| 916 | ✗ | const auto selected = m_workspace.mailId(); | |
| 917 | ✗ | if (selected.isValid() && | |
| 918 | ✗ | m_threadList.rowForIdentity(m_workspace.selection()) >= 0) | |
| 919 | ✗ | selectionTrace.visiblePagePublished(m_threadList.indexRevision(), | |
| 920 | ✗ | m_threadList.rowCount()); | |
| 921 | ✗ | }); | |
| 922 | 795 | connect(&m_threadList, &QuickThreadListModel::projectionReady, this, | |
| 923 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | [this, &selectionTrace] { |
| 924 |
2/6✗ Branch 3 → 4 not taken.
✓ Branch 3 → 7 taken 33 times.
✗ Branch 5 → 6 not taken.
✗ Branch 5 → 7 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 11 taken 33 times.
|
33 | if (selectionTrace.isActive() && m_threadList.indexed()) |
| 925 | ✗ | selectionTrace.indexCatchupFinished(m_threadList.indexRevision()); | |
| 926 | 33 | }); | |
| 927 |
1/1✓ Branch 15 → 16 taken 795 times.
|
795 | connect(session.controller(), &MailController::authoritativeFolderPresented, |
| 928 | 795 | this, [&selectionTrace](qint64 folderId, int, quint64) { | |
| 929 | ✗ | if (selectionTrace.isActive()) | |
| 930 | ✗ | selectionTrace.authoritativeSyncFinished(folderId); | |
| 931 | ✗ | }); | |
| 932 | 795 | } | |
| 933 | |||
| 934 | // Keyboard navigation cancellation and selection reconcile triggers. | ||
| 935 | 795 | void QuickMailBackend::wireNavigation() { | |
| 936 | 795 | MailSessionCore &session = m_session; | |
| 937 | 795 | WorkspaceState &workspace = m_workspace; | |
| 938 |
1/1✓ Branch 2 → 3 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::selectionChanged, this, [this] { |
| 939 |
2/2✓ Branch 2 → 3 taken 1561 times.
✓ Branch 2 → 4 taken 8 times.
|
1569 | if (!m_applyingNavigation) cancelNavigation(); |
| 940 | 1569 | }); | |
| 941 |
1/1✓ Branch 4 → 5 taken 795 times.
|
795 | connect(&workspace, &WorkspaceState::areaChanged, this, [this] { |
| 942 | 655 | cancelNavigation(); | |
| 943 |
2/2✓ Branch 11 → 12 taken 173 times.
✓ Branch 11 → 14 taken 482 times.
|
655 | if (m_workspace.area() == QStringLiteral("mail")) |
| 944 |
1/1✓ Branch 12 → 13 taken 173 times.
|
173 | QTimer::singleShot(0, this, &QuickMailBackend::reconcileSelection); |
| 945 | 655 | }); | |
| 946 |
1/1✓ Branch 7 → 8 taken 795 times.
|
795 | connect(session.headers(), &QAbstractItemModel::modelReset, this, [this] { |
| 947 |
1/1✓ Branch 2 → 3 taken 1661 times.
|
1661 | QTimer::singleShot(0, this, &QuickMailBackend::reconcileSelection); |
| 948 | 1661 | }); | |
| 949 | 795 | } | |
| 950 | |||
| 951 | // S92.5.7: one constant status notice while the primary connection | ||
| 952 | // recovers from an outage, cleared when it is back (or no longer | ||
| 953 | // recovering, e.g. offline mode). It is a pending action with a reserved | ||
| 954 | // token above every journal token, so a mutation notice keeps priority and | ||
| 955 | // repeated reconnect attempts never change the text (no countdown flicker). | ||
| 956 | namespace { | ||
| 957 | constexpr qint64 ConnectionStatusToken = std::numeric_limits<qint64>::max(); | ||
| 958 | } | ||
| 959 | |||
| 960 | 795 | void QuickMailBackend::wireConnectionStatus() { | |
| 961 | 795 | ConnectionHealthMonitor *health = m_session.health(); | |
| 962 |
3/6✓ Branch 3 → 4 taken 795 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 795 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 795 times.
|
795 | if (!health || !m_session.imap()) return; |
| 963 | // Queued: the monitor reacts to the same state change first. | ||
| 964 | 13 | const auto refresh = [this] { | |
| 965 |
1/1✓ Branch 2 → 3 taken 13 times.
|
13 | QTimer::singleShot(0, this, &QuickMailBackend::refreshConnectionStatus); |
| 966 | 13 | }; | |
| 967 |
1/1✓ Branch 10 → 11 taken 795 times.
|
795 | connect(health, &ConnectionHealthMonitor::reconnectScheduled, this, refresh); |
| 968 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(health, &ConnectionHealthMonitor::connectionRestored, this, refresh); |
| 969 |
1/1✓ Branch 15 → 16 taken 795 times.
|
795 | connect(m_session.imap(), &ImapService::stateChanged, this, refresh); |
| 970 | } | ||
| 971 | |||
| 972 | 11 | void QuickMailBackend::refreshConnectionStatus() { | |
| 973 |
3/4✓ Branch 3 → 4 taken 11 times.
✗ Branch 3 → 8 not taken.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 6 → 8 taken 7 times.
|
11 | const bool recovering = m_session.health() && m_session.health()->isRecovering(); |
| 974 |
1/1✓ Branch 12 → 13 taken 11 times.
|
11 | m_statusState.pendingAction(ConnectionStatusToken, |
| 975 |
3/3✓ Branch 9 → 10 taken 4 times.
✓ Branch 9 → 11 taken 7 times.
✓ Branch 10 → 12 taken 4 times.
|
22 | recovering ? tr("Connection lost — reconnecting…") : QString()); |
| 976 | 11 | } | |
| 977 | |||
| 978 | // Controller status, folder identity, unread counts and IMAP folder list/state. | ||
| 979 | 795 | void QuickMailBackend::wireControllerStatus() { | |
| 980 | 795 | MailSessionCore &session = m_session; | |
| 981 |
1/1✓ Branch 3 → 4 taken 795 times.
|
795 | connect(session.controller(), &MailController::statusMessage, this, &QuickMailBackend::setStatus); |
| 982 |
1/1✓ Branch 6 → 7 taken 795 times.
|
795 | connect(session.controller(), &MailController::actionError, this, |
| 983 | 795 | [this](const QString &text) { m_statusState.error(text); }); | |
| 984 |
1/1✓ Branch 9 → 10 taken 795 times.
|
795 | connect(session.controller(), &MailController::pendingActionChanged, |
| 985 | 795 | &m_statusState, &StatusMessageState::pendingAction); | |
| 986 |
1/1✓ Branch 12 → 13 taken 795 times.
|
795 | connect(session.controller(), &MailController::pendingActionsReset, |
| 987 | 795 | &m_statusState, &StatusMessageState::clearPendingActions); | |
| 988 |
1/1✓ Branch 15 → 16 taken 795 times.
|
795 | connect(session.controller(), &MailController::backgroundStatusMessage, this, |
| 989 | 1390 | [this](const QString &text) { m_statusState.progress(text); }); | |
| 990 |
1/1✓ Branch 18 → 19 taken 795 times.
|
795 | connect(session.controller(), &MailController::folderIdentityInvalidated, this, [this](qint64 folderId) { |
| 991 | 11 | const auto account = m_session.controller()->accountId(); | |
| 992 | { | ||
| 993 | 11 | QScopedValueRollback reconciling(m_reconciling, true); | |
| 994 | 11 | QScopedValueRollback applying(m_applyingSelection, true); | |
| 995 |
2/2✓ Branch 7 → 8 taken 9 times.
✓ Branch 7 → 12 taken 2 times.
|
11 | if (m_selection.folderId() == folderId) { |
| 996 |
1/1✓ Branch 8 → 9 taken 9 times.
|
9 | m_selection.clear(); |
| 997 | 9 | m_mainCurrent = {}; | |
| 998 | } | ||
| 999 |
1/1✓ Branch 12 → 13 taken 11 times.
|
11 | m_workspace.invalidateFolder(account, folderId); |
| 1000 | 11 | } | |
| 1001 |
1/1✓ Branch 15 → 16 taken 11 times.
|
11 | publishSelection(); |
| 1002 | 11 | }); | |
| 1003 |
1/1✓ Branch 21 → 22 taken 795 times.
|
795 | connect(session.controller(), &MailController::cachedUnreadCountChanged, |
| 1004 | 795 | &m_folders, &FolderPresentationModel::setUnread); | |
| 1005 |
1/1✓ Branch 24 → 25 taken 795 times.
|
795 | connect(session.controller(), &MailController::unreadCountChanged, |
| 1006 | 795 | &m_folders, &FolderPresentationModel::setUnread); | |
| 1007 |
1/1✓ Branch 27 → 28 taken 795 times.
|
795 | connect(session.imap(), &ImapService::folderListReceived, this, |
| 1008 | 828 | [this](const QList<FolderInfo> &folders) { applyFolderList(folders, true); }); | |
| 1009 |
1/1✓ Branch 30 → 31 taken 795 times.
|
795 | connect(session.imap(), &ImapService::errorOccurred, this, &QuickMailBackend::setStatus); |
| 1010 |
1/1✓ Branch 33 → 34 taken 795 times.
|
795 | connect(session.imap(), &ImapService::errorOccurred, this, [this](const QString &error) { |
| 1011 | ✗ | m_lastImapError = error; | |
| 1012 | ✗ | m_settings.notifyOverviewChanged(); | |
| 1013 | ✗ | }); | |
| 1014 |
1/1✓ Branch 36 → 37 taken 795 times.
|
795 | connect(session.imap(), &ImapService::stateChanged, this, [this](ImapService::State state) { |
| 1015 |
2/2✓ Branch 2 → 3 taken 2 times.
✓ Branch 2 → 4 taken 8 times.
|
10 | if (state == ImapService::State::Authenticated) m_lastImapError.clear(); |
| 1016 |
2/2✓ Branch 4 → 5 taken 2 times.
✓ Branch 4 → 10 taken 8 times.
|
10 | if (state == ImapService::State::Authenticated) { |
| 1017 |
2/2✓ Branch 5 → 6 taken 2 times.
✓ Branch 6 → 7 taken 2 times.
|
2 | setStatus(tr("Loading folders…")); |
| 1018 | 2 | m_session.imap()->listFolders(); | |
| 1019 |
2/2✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 15 taken 7 times.
|
8 | } else if (state == ImapService::State::Connecting) { |
| 1020 |
2/2✓ Branch 11 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
|
1 | setStatus(tr("Connecting…")); |
| 1021 | } | ||
| 1022 | 10 | }); | |
| 1023 | 795 | } | |
| 1024 | |||
| 1025 | |||
| 1026 | 795 | QuickMailBackend::~QuickMailBackend() { | |
| 1027 | 795 | m_compose.shutdown(); | |
| 1028 | 795 | m_settings.destroyPendingRequests(); | |
| 1029 | 795 | cancelThreadProjection(); | |
| 1030 | 795 | m_threadPool.waitForDone(); | |
| 1031 | 795 | FolderPredictionService::shared()->cancel(this); | |
| 1032 | 795 | cancelNavigation(); | |
| 1033 | 795 | } | |
| 1034 | |||
| 1035 | 17949 | qint64 QuickMailBackend::selectionCount() const { | |
| 1036 |
2/2✓ Branch 3 → 4 taken 657 times.
✓ Branch 3 → 20 taken 17292 times.
|
17949 | if (m_workspace.activeIsSearch()) { |
| 1037 | 657 | const MailSearchSession *search = m_searchTabs->active(); | |
| 1038 |
6/6✓ Branch 6 → 7 taken 439 times.
✓ Branch 6 → 10 taken 218 times.
✓ Branch 8 → 9 taken 84 times.
✓ Branch 8 → 10 taken 355 times.
✓ Branch 11 → 12 taken 84 times.
✓ Branch 11 → 14 taken 573 times.
|
657 | if (search && search->markedCount() > 1) return search->markedCount(); |
| 1039 |
2/2✓ Branch 16 → 17 taken 336 times.
✓ Branch 16 → 18 taken 237 times.
|
573 | return m_workspace.mailId().isValid() ? 1 : 0; |
| 1040 | } | ||
| 1041 | 17292 | return m_selection.count(); | |
| 1042 | } | ||
| 1043 | 4704 | QStringList QuickMailBackend::selectedLabels() const { | |
| 1044 | 4704 | const auto selected = m_workspace.mailId(); | |
| 1045 |
2/2✓ Branch 5 → 6 taken 4580 times.
✓ Branch 5 → 7 taken 124 times.
|
4704 | if (!selected.isValid()) return {}; |
| 1046 |
2/3✓ Branch 7 → 8 taken 124 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 124 times.
|
124 | if (const auto *visible = m_threadList.indexedHeader(selected.folderId, selected.uid)) |
| 1047 | ✗ | return visible->labels; | |
| 1048 |
1/1✓ Branch 11 → 12 taken 124 times.
|
124 | const int row = m_session.headers()->rowForUid(selected.uid, selected.folderId); |
| 1049 |
3/3✓ Branch 13 → 14 taken 124 times.
✓ Branch 14 → 15 taken 100 times.
✓ Branch 14 → 16 taken 24 times.
|
124 | if (const auto *visible = m_session.headers()->headerAt(row)) |
| 1050 | 100 | return visible->labels; | |
| 1051 | // A selected tab may point outside the bounded visible page. | ||
| 1052 |
1/1✓ Branch 17 → 18 taken 24 times.
|
24 | const auto header = m_session.cache()->header(selected.folderId, selected.uid); |
| 1053 |
1/2✓ Branch 19 → 20 taken 24 times.
✗ Branch 19 → 22 not taken.
|
24 | return header ? header->labels : QStringList{}; |
| 1054 | 4704 | } | |
| 1055 | 2 | QVariantList QuickMailBackend::pendingActionDetails() const { | |
| 1056 | // Read on demand when the status details open; bounded so a large offline | ||
| 1057 | // backlog cannot stall the GUI thread. | ||
| 1058 | 2 | constexpr int MaximumRows = 200; | |
| 1059 | 2 | QVariantList rows; | |
| 1060 | 2 | auto *cache = m_session.cache(); | |
| 1061 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2 times.
|
2 | if (!cache) return rows; |
| 1062 |
1/1✓ Branch 8 → 9 taken 2 times.
|
2 | const auto records = cache->journal().pendingMutations(m_session.controller()->accountId()); |
| 1063 | 2 | const qint64 now = QDateTime::currentSecsSinceEpoch(); | |
| 1064 |
2/2✓ Branch 170 → 13 taken 2 times.
✓ Branch 170 → 171 taken 2 times.
|
4 | for (const auto &record : records) { |
| 1065 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 2 times.
|
2 | if (record.phase == MutationPhase::Confirmed) continue; |
| 1066 |
1/2✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 2 times.
|
2 | if (rows.size() >= MaximumRows) break; |
| 1067 | 2 | QString action; | |
| 1068 |
1/8✓ Branch 19 → 20 taken 2 times.
✗ Branch 19 → 26 not taken.
✗ Branch 19 → 32 not taken.
✗ Branch 19 → 46 not taken.
✗ Branch 19 → 53 not taken.
✗ Branch 19 → 60 not taken.
✗ Branch 19 → 67 not taken.
✗ Branch 19 → 71 not taken.
|
2 | switch (record.kind) { |
| 1069 | 2 | case MutationKind::Seen: | |
| 1070 |
2/4✓ Branch 20 → 21 taken 2 times.
✗ Branch 20 → 22 not taken.
✓ Branch 21 → 23 taken 2 times.
✗ Branch 22 → 23 not taken.
|
2 | action = record.desiredState ? tr("Mark as read") : tr("Mark as unread"); break; |
| 1071 | ✗ | case MutationKind::Flagged: | |
| 1072 | ✗ | action = record.desiredState ? tr("Star") : tr("Remove star"); break; | |
| 1073 | ✗ | case MutationKind::Keyword: | |
| 1074 | ✗ | action = record.desiredState ? tr("Add label %1").arg(record.flag) | |
| 1075 | ✗ | : tr("Remove label %1").arg(record.flag); break; | |
| 1076 | ✗ | case MutationKind::Move: action = tr("Move to %1").arg(record.targetFolder); break; | |
| 1077 | ✗ | case MutationKind::Copy: action = tr("Copy to %1").arg(record.targetFolder); break; | |
| 1078 | ✗ | case MutationKind::Append: action = tr("Save copy in %1").arg(record.targetFolder); break; | |
| 1079 | ✗ | case MutationKind::Undo: action = tr("Undo move"); break; | |
| 1080 | } | ||
| 1081 | 2 | QString state; | |
| 1082 |
1/8✓ Branch 71 → 72 taken 2 times.
✗ Branch 71 → 76 not taken.
✗ Branch 71 → 80 not taken.
✗ Branch 71 → 84 not taken.
✗ Branch 71 → 88 not taken.
✗ Branch 71 → 92 not taken.
✗ Branch 71 → 96 not taken.
✗ Branch 71 → 97 not taken.
|
2 | switch (record.phase) { |
| 1083 |
1/1✓ Branch 72 → 73 taken 2 times.
|
2 | case MutationPhase::Queued: state = tr("Waiting to be sent"); break; |
| 1084 | ✗ | case MutationPhase::Dispatched: state = tr("Sent, waiting for the server"); break; | |
| 1085 | ✗ | case MutationPhase::AwaitingVerification: state = tr("Sent, server result being checked"); break; | |
| 1086 | ✗ | case MutationPhase::Ambiguous: state = tr("Server result unclear, will be checked again"); break; | |
| 1087 | ✗ | case MutationPhase::Failed: state = tr("Failed"); break; | |
| 1088 | ✗ | case MutationPhase::Quarantined: state = tr("Stopped, needs attention"); break; | |
| 1089 | ✗ | case MutationPhase::Confirmed: break; | |
| 1090 | } | ||
| 1091 | ✗ | QString subject; | |
| 1092 |
1/2✓ Branch 97 → 98 taken 2 times.
✗ Branch 97 → 105 not taken.
|
2 | if (record.mailId.uid > 0) |
| 1093 |
2/3✓ Branch 98 → 99 taken 2 times.
✓ Branch 100 → 101 taken 2 times.
✗ Branch 100 → 103 not taken.
|
2 | if (const auto header = cache->header(record.mailId.folderId, record.mailId.uid)) |
| 1094 | 2 | subject = header->subject; | |
| 1095 |
1/2✓ Branch 105 → 106 taken 2 times.
✗ Branch 105 → 108 not taken.
|
2 | const qint64 minutes = record.createdAt > 0 ? qMax<qint64>(0, now - record.createdAt) / 60 : 0; |
| 1096 |
1/1✓ Branch 143 → 144 taken 2 times.
|
2 | rows.append(QVariantMap{ |
| 1097 | 4 | {QStringLiteral("action"), action}, | |
| 1098 |
1/3✗ Branch 117 → 118 not taken.
✓ Branch 117 → 119 taken 2 times.
✗ Branch 118 → 120 not taken.
|
4 | {QStringLiteral("subject"), subject.isEmpty() ? tr("(no subject)") : subject}, |
| 1099 | 4 | {QStringLiteral("folder"), record.folderPath}, | |
| 1100 | 4 | {QStringLiteral("state"), state}, | |
| 1101 | 4 | {QStringLiteral("error"), record.error}, | |
| 1102 |
2/4✓ Branch 136 → 137 taken 2 times.
✗ Branch 136 → 138 not taken.
✓ Branch 137 → 140 taken 2 times.
✗ Branch 139 → 140 not taken.
|
4 | {QStringLiteral("age"), minutes < 1 ? tr("just now") : tr("%n min ago", nullptr, int(qMin<qint64>(minutes, 100000)))}, |
| 1103 |
2/4✓ Branch 147 → 148 taken 12 times.
✓ Branch 147 → 149 taken 2 times.
✗ Branch 218 → 219 not taken.
✗ Branch 218 → 220 not taken.
|
16 | }); |
| 1104 | 2 | } | |
| 1105 | 2 | return rows; | |
| 1106 | 16 | } | |
| 1107 | |||
| 1108 | // ─── Q09: CardDAV contact sync ─── | ||
| 1109 | |||
| 1110 | 44 | void QuickMailBackend::previewSearch(const QString &query) { | |
| 1111 | 44 | cancelSearchPreview(); | |
| 1112 |
4/4✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 9 taken 43 times.
✓ Branch 5 → 6 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
|
44 | if (query.size() > 4096) { setStatus(tr("Search query is too long.")); return; } |
| 1113 |
6/13✓ Branch 10 → 11 taken 43 times.
✗ Branch 10 → 16 not taken.
✓ Branch 12 → 13 taken 43 times.
✗ Branch 12 → 16 not taken.
✓ Branch 13 → 14 taken 43 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 43 times.
✓ Branch 18 → 19 taken 43 times.
✗ Branch 18 → 20 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 43 times.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
|
43 | if (!configured() || m_availableFolders.isEmpty() || query.trimmed().isEmpty()) return; |
| 1114 | 43 | m_searchPreviewQuery = query; | |
| 1115 | 43 | m_searchDebounce.start(); | |
| 1116 | } | ||
| 1117 | 1242 | void QuickMailBackend::cancelSearchPreview() { | |
| 1118 | 1242 | m_search->cancelQuickResults(); | |
| 1119 | 1242 | m_searchDebounce.stop(); | |
| 1120 | 1242 | m_searchPreviewQuery.clear(); | |
| 1121 | 1242 | m_searchSuggestions.clear(); | |
| 1122 | 1242 | emit searchSuggestionsChanged(); | |
| 1123 | 1242 | } | |
| 1124 | 77 | bool QuickMailBackend::submitSearch(const QString &query, int suggestion) { | |
| 1125 |
4/4✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 8 taken 76 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 5 → 6 taken 1 time.
|
77 | if (query.size() > 4096) { setStatus(tr("Search query is too long.")); return false; } |
| 1126 |
5/6✓ Branch 9 → 10 taken 74 times.
✓ Branch 9 → 12 taken 2 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 74 times.
✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 16 taken 74 times.
|
76 | if (!configured() || m_availableFolders.isEmpty()) return false; |
| 1127 |
2/2✓ Branch 16 → 17 taken 5 times.
✓ Branch 16 → 58 taken 69 times.
|
74 | if (suggestion >= 0) { |
| 1128 |
3/6✓ Branch 18 → 19 taken 5 times.
✗ Branch 18 → 21 not taken.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 5 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 5 times.
|
5 | if (query != m_searchPreviewQuery || suggestion >= m_searchSuggestions.size()) return false; |
| 1129 |
1/1✓ Branch 26 → 27 taken 5 times.
|
5 | const auto entry = m_searchSuggestions.at(suggestion).toMap(); |
| 1130 |
3/3✓ Branch 28 → 29 taken 5 times.
✓ Branch 29 → 30 taken 5 times.
✓ Branch 30 → 31 taken 5 times.
|
5 | const auto identity = entry.value("identity").toMap(); |
| 1131 |
3/4✓ Branch 34 → 35 taken 5 times.
✓ Branch 35 → 36 taken 5 times.
✗ Branch 37 → 38 not taken.
✓ Branch 37 → 39 taken 5 times.
|
5 | if (!acceptsIdentity(WorkspaceState::decode(identity))) return false; |
| 1132 |
6/6✓ Branch 40 → 41 taken 5 times.
✓ Branch 41 → 42 taken 5 times.
✓ Branch 42 → 43 taken 5 times.
✓ Branch 43 → 44 taken 5 times.
✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 53 taken 4 times.
|
5 | if (m_workspace.openMail(identity, entry.value("subject").toString()) < 0) { |
| 1133 |
2/2✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
|
1 | setStatus(tr("Cannot open another tab. Close a tab and retry.")); |
| 1134 | 1 | return false; | |
| 1135 | } | ||
| 1136 |
1/1✓ Branch 53 → 54 taken 4 times.
|
4 | cancelSearchPreview(); |
| 1137 | 4 | return true; | |
| 1138 | 5 | } | |
| 1139 | 69 | return openSearch(query, false); | |
| 1140 | } | ||
| 1141 | 93 | bool QuickMailBackend::openSearch(const QString &query, bool newTab) { | |
| 1142 |
1/4✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 93 times.
✗ Branch 4 → 5 not taken.
✗ Branch 5 → 6 not taken.
|
93 | if (query.size() > WorkspaceState::MaximumQueryCharacters) { setStatus(tr("Search query is too long.")); return false; } |
| 1143 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 93 times.
|
93 | if (!configured()) return false; |
| 1144 | 93 | cancelSearchPreview(); | |
| 1145 |
1/2✓ Branch 14 → 15 taken 93 times.
✗ Branch 14 → 16 not taken.
|
93 | if (m_searchTabs->open(query, newTab)) return true; |
| 1146 | ✗ | setStatus(tr("Cannot open another tab. Close a tab and retry.")); | |
| 1147 | ✗ | return false; | |
| 1148 | } | ||
| 1149 | 872 | QStringList QuickMailBackend::expandedFolders() const { | |
| 1150 |
1/1✓ Branch 3 → 4 taken 872 times.
|
872 | return m_workspace.expandedFolders(accountName()); |
| 1151 | } | ||
| 1152 | 23 | bool QuickMailBackend::setFolderExpanded(const QString &path, bool expanded) { | |
| 1153 |
1/1✓ Branch 2 → 3 taken 23 times.
|
23 | const auto index = m_folders.indexForPath(path); |
| 1154 |
7/9✓ Branch 4 → 5 taken 22 times.
✓ Branch 4 → 9 taken 1 time.
✓ Branch 6 → 7 taken 22 times.
✗ Branch 6 → 9 not taken.
✓ Branch 7 → 8 taken 22 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 22 times.
✓ Branch 11 → 12 taken 1 time.
✓ Branch 11 → 13 taken 22 times.
|
23 | if (!configured() || !index.isValid() || m_folders.rowCount(index) == 0) return false; |
| 1155 |
2/3✓ Branch 14 → 15 taken 22 times.
✓ Branch 16 → 17 taken 22 times.
✗ Branch 16 → 18 not taken.
|
22 | if (m_workspace.setFolderExpanded(accountName(), path, expanded)) return true; |
| 1156 | ✗ | setStatus(tr("Expanded folder state exceeds storage limits.")); | |
| 1157 | ✗ | return false; | |
| 1158 | } | ||
| 1159 | |||
| 1160 | 1035 | void QuickMailBackend::refreshSelectionScope() { | |
| 1161 | 1035 | MailSelectionQuery query; | |
| 1162 | 1035 | query.accountId = accountName(); | |
| 1163 |
1/1✓ Branch 6 → 7 taken 1035 times.
|
1035 | query.folderId = folderIdForPath(m_folderPath); |
| 1164 |
2/2✓ Branch 8 → 9 taken 51 times.
✓ Branch 8 → 10 taken 984 times.
|
1035 | query.text = threadedPresentation() ? m_threadList.filterText() : m_mail.filterText(); |
| 1165 |
2/2✓ Branch 14 → 15 taken 51 times.
✓ Branch 14 → 17 taken 984 times.
|
1035 | query.unreadOnly = threadedPresentation() ? m_threadList.showUnreadOnly() : m_mail.showUnreadOnly(); |
| 1166 |
2/2✓ Branch 20 → 21 taken 51 times.
✓ Branch 20 → 23 taken 984 times.
|
1035 | query.starredOnly = threadedPresentation() ? m_threadList.showStarredOnly() : m_mail.showStarredOnly(); |
| 1167 |
2/2✓ Branch 26 → 27 taken 51 times.
✓ Branch 26 → 29 taken 984 times.
|
1035 | query.attachmentsOnly = threadedPresentation() ? m_threadList.showWithAttachments() : m_mail.showWithAttachments(); |
| 1168 | 1035 | const auto generation = m_session.accounts()->generation(); | |
| 1169 | 1035 | const auto databasePath = m_session.cache()->databasePath(); | |
| 1170 | 1035 | const auto folder = m_folderPath; | |
| 1171 | 1035 | MailSelectionKey initialSingle; | |
| 1172 |
2/2✓ Branch 38 → 39 taken 1029 times.
✓ Branch 38 → 48 taken 6 times.
|
1035 | if (m_workspace.activeTab() == 0) { |
| 1173 |
2/2✓ Branch 39 → 40 taken 1029 times.
✓ Branch 40 → 41 taken 1029 times.
|
1029 | const int row = activeRowForIdentity(m_workspace.selection()); |
| 1174 |
1/1✓ Branch 42 → 43 taken 1029 times.
|
1029 | const auto key = activeKeyAt(row); |
| 1175 |
4/4✓ Branch 43 → 44 taken 298 times.
✓ Branch 43 → 46 taken 731 times.
✓ Branch 44 → 45 taken 83 times.
✓ Branch 44 → 46 taken 215 times.
|
1029 | if (key.folderId == query.folderId && key.uid >= 0) initialSingle = key; |
| 1176 | 1029 | } | |
| 1177 |
4/4✓ Branch 51 → 52 taken 82 times.
✓ Branch 51 → 54 taken 1 time.
✓ Branch 57 → 58 taken 83 times.
✓ Branch 57 → 59 taken 952 times.
|
1118 | m_mainCurrent = initialSingle.uid >= 0 && m_workspace.area() == "mail" |
| 1178 |
2/2✓ Branch 48 → 49 taken 83 times.
✓ Branch 48 → 54 taken 952 times.
|
2153 | ? MailId{accountName(), initialSingle.folderId, initialSingle.uid} |
| 1179 | 1035 | : MailId{}; | |
| 1180 | // F-41: Shift+click ranges follow the presented order. | ||
| 1181 | 1035 | m_selection.setVisibleRangeProvider( | |
| 1182 | 1035 | [this](const MailSelectionKey &anchor, const MailSelectionKey &key) { | |
| 1183 | 6 | return visibleRangeBetween(anchor, key); | |
| 1184 | }); | ||
| 1185 |
1/1✓ Branch 69 → 70 taken 1035 times.
|
2070 | m_selection.configure(databasePath, query, |
| 1186 |
1/5✓ Branch 66 → 67 taken 1035 times.
✗ Branch 94 → 95 not taken.
✗ Branch 94 → 96 not taken.
✗ Branch 97 → 98 not taken.
✗ Branch 97 → 99 not taken.
|
2070 | [this, query, generation, databasePath, folder](const MailSelectionSnapshot &snapshot) { |
| 1187 |
1/4✓ Branch 6 → 7 taken 40 times.
✗ Branch 6 → 27 not taken.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
|
80 | return m_workspace.activeTab() == 0 && m_workspace.area() == "mail" && |
| 1188 |
2/4✓ Branch 9 → 10 taken 40 times.
✗ Branch 9 → 27 not taken.
✓ Branch 13 → 14 taken 40 times.
✗ Branch 13 → 27 not taken.
|
80 | m_session.accounts()->generation() == generation && |
| 1189 |
3/8✓ Branch 15 → 16 taken 40 times.
✗ Branch 15 → 27 not taken.
✓ Branch 17 → 18 taken 40 times.
✗ Branch 17 → 27 not taken.
✓ Branch 30 → 31 taken 40 times.
✗ Branch 30 → 32 not taken.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 36 not taken.
|
160 | m_session.cache()->databasePath() == databasePath && m_folderPath == folder && |
| 1190 |
1/2✓ Branch 19 → 20 taken 40 times.
✗ Branch 19 → 27 not taken.
|
80 | m_availableFolders.contains(folder) && snapshot.accountId == query.accountId && |
| 1191 |
3/6✓ Branch 3 → 4 taken 40 times.
✗ Branch 3 → 27 not taken.
✓ Branch 20 → 21 taken 40 times.
✗ Branch 20 → 27 not taken.
✓ Branch 22 → 23 taken 40 times.
✗ Branch 22 → 27 not taken.
|
120 | snapshot.folderId == query.folderId && snapshot.folderPath == folder && |
| 1192 |
3/5✓ Branch 24 → 25 taken 40 times.
✓ Branch 25 → 26 taken 40 times.
✗ Branch 25 → 27 not taken.
✓ Branch 28 → 29 taken 40 times.
✗ Branch 28 → 30 not taken.
|
120 | m_session.cache()->uidValidity(query.folderId) == snapshot.uidValidity; |
| 1193 | }, initialSingle); | ||
| 1194 | 1035 | } | |
| 1195 | 239 | void QuickMailBackend::beginMailRowSelection(int row, int modifiers) { | |
| 1196 |
1/1✓ Branch 2 → 3 taken 239 times.
|
239 | auto &trace = MailJD::Performance::ThreadSelectionPerformanceTracker::instance(); |
| 1197 | ✗ | if (!trace.isActive() || modifiers != 0 || m_workspace.activeTab() != 0 || | |
| 1198 |
4/13✓ Branch 3 → 4 taken 239 times.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 13 taken 239 times.
✗ Branch 10 → 11 not taken.
✗ Branch 10 → 13 not taken.
✗ Branch 12 → 13 not taken.
✗ Branch 12 → 14 not taken.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 17 taken 239 times.
✓ Branch 17 → 18 taken 239 times.
✗ Branch 17 → 19 not taken.
✗ Branch 37 → 38 not taken.
✗ Branch 37 → 39 not taken.
|
239 | m_workspace.area() != "mail" || !configured()) return; |
| 1199 | ✗ | const auto key = activeKeyAt(row); | |
| 1200 | ✗ | const MailId id{accountName(), key.folderId, key.uid}; | |
| 1201 | ✗ | if (id.isValid() && id != m_workspace.mailId()) trace.beginSelection(id); | |
| 1202 | ✗ | } | |
| 1203 | |||
| 1204 | 187 | void QuickMailBackend::selectMailRow(int row, int modifiers) { | |
| 1205 |
8/10✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 10 taken 172 times.
✓ Branch 6 → 7 taken 15 times.
✗ Branch 6 → 10 not taken.
✓ Branch 8 → 9 taken 15 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 15 times.
✓ Branch 11 → 13 taken 172 times.
✓ Branch 13 → 14 taken 15 times.
✓ Branch 13 → 39 taken 172 times.
|
187 | if (m_workspace.activeIsSearch() && m_workspace.area() == "mail" && configured()) { |
| 1206 | // Sprint 101: a result of the active search tab (single selection). | ||
| 1207 |
1/1✓ Branch 15 → 16 taken 15 times.
|
15 | MailSearchSession *search = m_searchTabs->active(); |
| 1208 |
2/3✓ Branch 16 → 17 taken 15 times.
✗ Branch 16 → 18 not taken.
✓ Branch 17 → 19 taken 15 times.
|
15 | const MailId id = search ? search->mailIdAt(row) : MailId{}; |
| 1209 |
4/7✓ Branch 19 → 20 taken 15 times.
✗ Branch 19 → 22 not taken.
✓ Branch 20 → 21 taken 15 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 15 times.
✗ Branch 24 → 25 not taken.
✓ Branch 24 → 26 taken 15 times.
|
15 | if (!search || !acceptsIdentity(id)) return; |
| 1210 | // S101.6.1 (D-13): Ctrl/Shift mark results for a bulk action. | ||
| 1211 | 15 | const auto keys = Qt::KeyboardModifiers(modifiers); | |
| 1212 |
3/3✓ Branch 31 → 32 taken 4 times.
✓ Branch 31 → 33 taken 11 times.
✓ Branch 32 → 34 taken 4 times.
|
15 | if (keys & (Qt::ControlModifier | Qt::ShiftModifier)) search->mark(row, keys); |
| 1213 |
1/1✓ Branch 33 → 34 taken 11 times.
|
11 | else search->clearMarks(); |
| 1214 |
1/1✓ Branch 34 → 35 taken 15 times.
|
15 | m_workspace.select(id); |
| 1215 |
1/1✓ Branch 35 → 36 taken 15 times.
|
15 | emit selectionChanged(); |
| 1216 | 15 | return; | |
| 1217 | 15 | } | |
| 1218 |
5/10✓ Branch 40 → 41 taken 172 times.
✗ Branch 40 → 46 not taken.
✓ Branch 43 → 44 taken 172 times.
✗ Branch 43 → 46 not taken.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 47 taken 172 times.
✓ Branch 48 → 49 taken 172 times.
✗ Branch 48 → 50 not taken.
✗ Branch 50 → 51 not taken.
✓ Branch 50 → 52 taken 172 times.
|
172 | if (m_workspace.activeTab() != 0 || m_workspace.area() != "mail" || !configured()) return; |
| 1219 | 172 | beginMailRowSelection(row, modifiers); | |
| 1220 |
2/2✓ Branch 55 → 56 taken 172 times.
✓ Branch 56 → 57 taken 172 times.
|
172 | m_selection.select(activeKeyAt(row), Qt::KeyboardModifiers(modifiers)); |
| 1221 | } | ||
| 1222 | 23 | void QuickMailBackend::selectAllMails() { | |
| 1223 |
2/2✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 10 taken 21 times.
|
23 | if (m_workspace.activeIsSearch()) { |
| 1224 |
1/2✓ Branch 6 → 7 taken 2 times.
✗ Branch 6 → 8 not taken.
|
2 | if (MailSearchSession *search = m_searchTabs->active()) search->markAllLoaded(); // the loaded rows (D-13) |
| 1225 | 2 | emit selectionChanged(); | |
| 1226 | 2 | return; | |
| 1227 | } | ||
| 1228 |
5/10✓ Branch 11 → 12 taken 21 times.
✗ Branch 11 → 18 not taken.
✓ Branch 14 → 15 taken 21 times.
✗ Branch 14 → 18 not taken.
✓ Branch 16 → 17 taken 21 times.
✗ Branch 16 → 18 not taken.
✓ Branch 19 → 20 taken 21 times.
✗ Branch 19 → 21 not taken.
✓ Branch 21 → 22 taken 21 times.
✗ Branch 21 → 23 not taken.
|
21 | if (m_workspace.activeTab() == 0 && m_workspace.area() == "mail" && configured()) m_selection.selectAll(); |
| 1229 | } | ||
| 1230 | 812 | void QuickMailBackend::prunePredictionsToScope() { | |
| 1231 | 812 | QSet<MailId> keep; | |
| 1232 | 812 | const auto selected = m_workspace.mailId(); | |
| 1233 |
3/3✓ Branch 6 → 7 taken 773 times.
✓ Branch 6 → 9 taken 39 times.
✓ Branch 7 → 8 taken 773 times.
|
812 | if (selected.isValid()) keep.insert(selected); |
| 1234 |
5/6✓ Branch 10 → 11 taken 20 times.
✓ Branch 10 → 13 taken 792 times.
✓ Branch 11 → 12 taken 20 times.
✗ Branch 11 → 13 not taken.
✓ Branch 14 → 15 taken 20 times.
✓ Branch 14 → 25 taken 792 times.
|
812 | if (suggestionColumnShown() && m_predictionFirst >= 0) |
| 1235 |
3/3✓ Branch 22 → 23 taken 535 times.
✓ Branch 24 → 16 taken 515 times.
✓ Branch 24 → 25 taken 20 times.
|
535 | for (int row = m_predictionFirst; row <= qMin(m_predictionLast, activeRowCount() - 1); ++row) { |
| 1236 |
1/1✓ Branch 16 → 17 taken 515 times.
|
515 | const auto key = activeKeyAt(row); |
| 1237 | 515 | keep.insert({accountName(), key.folderId, key.uid}); | |
| 1238 | 515 | } | |
| 1239 |
3/3✓ Branch 25 → 26 taken 812 times.
✓ Branch 36 → 27 taken 259 times.
✓ Branch 36 → 37 taken 812 times.
|
1071 | for (auto it = m_predictions.begin(); it != m_predictions.end();) |
| 1240 |
3/3✓ Branch 29 → 30 taken 205 times.
✓ Branch 29 → 33 taken 54 times.
✓ Branch 31 → 32 taken 205 times.
|
259 | if (!keep.contains(it.key())) it = m_predictions.erase(it); else ++it; |
| 1241 | 1327 | } | |
| 1242 | |||
| 1243 | 1647 | void QuickMailBackend::watchSelectionRows(int first, int last) { | |
| 1244 |
1/1✓ Branch 2 → 3 taken 1647 times.
|
1647 | emit viewportWatched(first, last); |
| 1245 |
1/2✓ Branch 3 → 4 taken 1647 times.
✗ Branch 3 → 6 not taken.
|
1647 | if (last >= first) m_pageRows = qMax(1, last - first + 1); |
| 1246 |
4/4✓ Branch 6 → 7 taken 942 times.
✓ Branch 6 → 8 taken 705 times.
✓ Branch 7 → 8 taken 107 times.
✓ Branch 7 → 12 taken 835 times.
|
1647 | if (m_predictionFirst != first || m_predictionLast != last) { |
| 1247 | 812 | m_predictionFirst = first; | |
| 1248 | 812 | m_predictionLast = last; | |
| 1249 |
1/1✓ Branch 8 → 9 taken 812 times.
|
812 | prunePredictionsToScope(); |
| 1250 |
3/3✓ Branch 10 → 11 taken 20 times.
✓ Branch 10 → 12 taken 792 times.
✓ Branch 11 → 12 taken 20 times.
|
812 | if (suggestionColumnShown()) invalidatePredictions(); |
| 1251 | } | ||
| 1252 |
4/5✓ Branch 12 → 13 taken 1024 times.
✓ Branch 12 → 14 taken 623 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 18 taken 1024 times.
✓ Branch 15 → 16 taken 623 times.
|
1647 | if (first < 0 || last < first) { m_selection.watch({}); return; } |
| 1253 |
1/4✗ Branch 18 → 19 not taken.
✓ Branch 18 → 23 taken 1024 times.
✗ Branch 19 → 20 not taken.
✗ Branch 20 → 21 not taken.
|
1024 | if (qint64(last) - first >= 4096) { setStatus(tr("Selection failed. Select again.")); return; } |
| 1254 | 1024 | QList<qint64> uids; | |
| 1255 |
5/5✓ Branch 24 → 25 taken 6263 times.
✓ Branch 25 → 26 taken 6263 times.
✓ Branch 28 → 29 taken 7287 times.
✓ Branch 30 → 24 taken 6263 times.
✓ Branch 30 → 31 taken 1024 times.
|
7287 | for (int row = first; row <= qMin(last, activeRowCount() - 1); ++row) uids.append(activeKeyAt(row).uid); |
| 1256 |
1/1✓ Branch 31 → 32 taken 1024 times.
|
1024 | m_selection.watch(uids); |
| 1257 | 1024 | } | |
| 1258 | |||
| 1259 | 2414 | bool QuickMailBackend::canFetchMoreMails() const { | |
| 1260 |
3/3✓ Branch 3 → 4 taken 55 times.
✓ Branch 3 → 6 taken 2359 times.
✓ Branch 8 → 9 taken 2359 times.
|
2414 | return threadedPresentation() ? m_threadList.hasMoreIndexed() : m_session.headers()->canFetchMore({}); |
| 1261 | } | ||
| 1262 | |||
| 1263 | 21 | void QuickMailBackend::fetchMoreMails() { | |
| 1264 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 21 times.
|
21 | if (!canFetchMoreMails()) return; |
| 1265 |
2/2✓ Branch 6 → 7 taken 1 time.
✓ Branch 6 → 8 taken 20 times.
|
21 | if (threadedPresentation()) m_threadList.fetchIndexed(); |
| 1266 |
1/1✓ Branch 10 → 11 taken 20 times.
|
20 | else m_session.headers()->fetchMore({}); |
| 1267 | } | ||
| 1268 | |||
| 1269 | 1488 | bool QuickMailBackend::canFetchPreviousMails() const { | |
| 1270 |
4/4✓ Branch 3 → 4 taken 87 times.
✓ Branch 3 → 7 taken 1401 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 5 → 7 taken 85 times.
|
1488 | return threadedPresentation() && m_threadList.hasMoreIndexed(true); |
| 1271 | } | ||
| 1272 | |||
| 1273 | 1 | void QuickMailBackend::fetchPreviousMails() { | |
| 1274 |
1/2✓ Branch 3 → 4 taken 1 time.
✗ Branch 3 → 5 not taken.
|
1 | if (canFetchPreviousMails()) m_threadList.fetchIndexed(true); |
| 1275 | 1 | } | |
| 1276 | |||
| 1277 | 3804 | void QuickMailBackend::cancelNavigation() { | |
| 1278 | 3804 | const bool pending = navigationPending(); | |
| 1279 | 3804 | MailDisplayReadService::shared()->cancel(this); | |
| 1280 | 3804 | m_navigation.clear(); | |
| 1281 | 3804 | m_navigationActive = false; | |
| 1282 |
2/2✓ Branch 6 → 7 taken 2 times.
✓ Branch 6 → 8 taken 3802 times.
|
3804 | if (pending) emit navigationChanged(); |
| 1283 | 3804 | } | |
| 1284 | |||
| 1285 | 24 | void QuickMailBackend::startNavigation() { | |
| 1286 |
7/8✓ Branch 2 → 3 taken 21 times.
✓ Branch 2 → 7 taken 3 times.
✓ Branch 4 → 5 taken 13 times.
✓ Branch 4 → 7 taken 8 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 13 times.
✓ Branch 9 → 10 taken 11 times.
✓ Branch 9 → 11 taken 13 times.
|
24 | if (m_navigationActive || m_navigation.isEmpty() || m_workspace.activeTab() == 0) return; |
| 1287 | 13 | const auto selected = m_workspace.mailId(); | |
| 1288 |
1/1✓ Branch 13 → 14 taken 13 times.
|
13 | const auto generation = m_workspace.generation(); |
| 1289 | 13 | const auto accountGeneration = m_session.accounts()->generation(); | |
| 1290 | 13 | const auto databasePath = m_session.cache()->databasePath(); | |
| 1291 |
1/1✓ Branch 18 → 19 taken 13 times.
|
13 | const int direction = m_navigation.takeFirst(); |
| 1292 | 13 | m_navigationActive = true; | |
| 1293 |
1/1✓ Branch 19 → 20 taken 13 times.
|
13 | emit navigationChanged(); |
| 1294 |
2/2✓ Branch 20 → 21 taken 13 times.
✓ Branch 25 → 26 taken 13 times.
|
26 | MailDisplayReadService::shared()->navigate(this, databasePath, selected, direction, |
| 1295 |
1/5✓ Branch 24 → 25 taken 13 times.
✗ Branch 38 → 39 not taken.
✗ Branch 38 → 40 not taken.
✗ Branch 41 → 42 not taken.
✗ Branch 41 → 43 not taken.
|
26 | [this, selected, generation, accountGeneration, databasePath](MailDisplaySnapshot result) { |
| 1296 |
2/6✓ Branch 7 → 8 taken 11 times.
✗ Branch 7 → 17 not taken.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 26 taken 11 times.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
|
22 | if (m_workspace.generation() != generation || m_workspace.mailId() != selected || |
| 1297 |
1/2✓ Branch 10 → 11 taken 11 times.
✗ Branch 10 → 17 not taken.
|
11 | m_session.accounts()->generation() != accountGeneration || |
| 1298 |
6/13✓ Branch 2 → 3 taken 11 times.
✓ Branch 4 → 5 taken 11 times.
✗ Branch 4 → 17 not taken.
✓ Branch 14 → 15 taken 11 times.
✗ Branch 14 → 17 not taken.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 11 times.
✓ Branch 19 → 20 taken 11 times.
✗ Branch 19 → 21 not taken.
✓ Branch 21 → 22 taken 11 times.
✗ Branch 21 → 23 not taken.
✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
|
22 | m_session.cache()->databasePath() != databasePath || m_workspace.activeTab() == 0) { |
| 1299 | ✗ | cancelNavigation(); | |
| 1300 | ✗ | return; | |
| 1301 | } | ||
| 1302 | 11 | m_navigationActive = false; | |
| 1303 |
6/6✓ Branch 26 → 27 taken 9 times.
✓ Branch 26 → 30 taken 2 times.
✓ Branch 28 → 29 taken 1 time.
✓ Branch 28 → 30 taken 8 times.
✓ Branch 31 → 32 taken 1 time.
✓ Branch 31 → 33 taken 10 times.
|
11 | if (result.outcome == MailDisplaySnapshot::Outcome::Ready && !m_availableFolders.contains(result.folderPath)) |
| 1304 | 1 | result.outcome = MailDisplaySnapshot::Outcome::HeaderMissing; | |
| 1305 |
2/2✓ Branch 33 → 34 taken 8 times.
✓ Branch 33 → 40 taken 3 times.
|
11 | if (result.outcome == MailDisplaySnapshot::Outcome::Ready) { |
| 1306 | 8 | QScopedValueRollback guard(m_applyingNavigation, true); | |
| 1307 | 8 | m_workspace.replaceActiveMail({selected.accountId, selected.folderId, result.header.uid}, result.header.subject); | |
| 1308 |
2/2✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 48 taken 1 time.
|
11 | } else if (result.outcome != MailDisplaySnapshot::Outcome::Boundary) { |
| 1309 | 2 | m_navigation.clear(); | |
| 1310 |
1/1✓ Branch 45 → 46 taken 2 times.
|
2 | setStatus(result.outcome == MailDisplaySnapshot::Outcome::HeaderMissing |
| 1311 |
2/4✓ Branch 42 → 43 taken 2 times.
✗ Branch 42 → 44 not taken.
✓ Branch 43 → 45 taken 2 times.
✗ Branch 44 → 45 not taken.
|
4 | ? tr("This message is no longer available. Select another message.") |
| 1312 | : tr("Could not navigate locally. Retry.")); | ||
| 1313 | } | ||
| 1314 | 11 | startNavigation(); | |
| 1315 | 11 | emit navigationChanged(); | |
| 1316 |
1/3✓ Branch 36 → 37 taken 8 times.
✗ Branch 61 → 62 not taken.
✗ Branch 61 → 63 not taken.
|
21 | }, m_mailSort); // F100-66: next/previous in the list order |
| 1317 | 13 | } | |
| 1318 | |||
| 1319 | 3753 | QString QuickMailBackend::activeAccountId() const { | |
| 1320 | 3753 | const auto &account = m_session.accounts()->primary(); | |
| 1321 |
2/2✓ Branch 5 → 6 taken 3692 times.
✓ Branch 5 → 8 taken 61 times.
|
3753 | return account ? account->id : QString{}; |
| 1322 | } | ||
| 1323 | 2474 | AccountOverview::Active QuickMailBackend::activeAccountState() { | |
| 1324 | 2474 | AccountOverview::Active active; | |
| 1325 | 2474 | active.accountId = activeAccountId(); | |
| 1326 |
2/2✓ Branch 6 → 7 taken 27 times.
✓ Branch 6 → 8 taken 2447 times.
|
2474 | if (active.accountId.isEmpty()) return active; |
| 1327 | 2447 | const auto state = m_session.imap()->state(); | |
| 1328 |
4/5✓ Branch 11 → 12 taken 2447 times.
✗ Branch 11 → 16 not taken.
✓ Branch 13 → 14 taken 2447 times.
✓ Branch 14 → 15 taken 4 times.
✓ Branch 14 → 16 taken 2443 times.
|
2447 | const bool recovering = m_session.health() && m_session.health()->isRecovering(); |
| 1329 |
5/6✓ Branch 17 → 18 taken 2445 times.
✓ Branch 17 → 20 taken 2 times.
✓ Branch 18 → 19 taken 2443 times.
✓ Branch 18 → 20 taken 2 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 27 taken 2443 times.
|
2447 | if (state == ImapService::State::Authenticated || state == ImapService::State::Selected || |
| 1330 | state == ImapService::State::Idling) | ||
| 1331 | 4 | active.mailState = QStringLiteral("connected"); | |
| 1332 |
4/7✓ Branch 27 → 28 taken 2443 times.
✗ Branch 27 → 31 not taken.
✓ Branch 29 → 30 taken 2443 times.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 2443 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 50 taken 2443 times.
|
2443 | else if (state == ImapService::State::Error || m_lastImapError.startsWith(QLatin1String("Authentication failed"))) |
| 1333 | ✗ | active.mailState = m_lastImapError.startsWith(QLatin1String("Authentication failed")) ? QStringLiteral("needs-password") | |
| 1334 | ✗ | : QStringLiteral("error"); | |
| 1335 |
3/4✓ Branch 50 → 51 taken 10 times.
✓ Branch 50 → 59 taken 2433 times.
✓ Branch 51 → 52 taken 10 times.
✗ Branch 51 → 59 not taken.
|
2443 | else if (state != ImapService::State::Disconnected && !recovering) |
| 1336 | 10 | active.mailState = QStringLiteral("connecting"); | |
| 1337 | else | ||
| 1338 | 2433 | active.mailState = QStringLiteral("offline"); | |
| 1339 | 2447 | active.mailError = m_lastImapError; | |
| 1340 | 2447 | active.rulesState = m_rules.state(); | |
| 1341 |
1/1✓ Branch 70 → 71 taken 2447 times.
|
2447 | active.enabledRules = m_rules.enabledRuleCount(); |
| 1342 |
3/3✓ Branch 71 → 72 taken 2447 times.
✓ Branch 76 → 77 taken 2447 times.
✓ Branch 77 → 78 taken 2447 times.
|
4894 | active.vacation = m_rules.vacation().value(QStringLiteral("active")).toBool(); |
| 1343 | 2447 | return active; | |
| 1344 | ✗ | } | |
| 1345 | |||
| 1346 | 4015 | QVariantList QuickMailBackend::accountChoices() const { | |
| 1347 | 4015 | return m_session.accounts()->availableAccounts(); | |
| 1348 | } | ||
| 1349 | 7 | bool QuickMailBackend::selectAccount(const QString &id) { | |
| 1350 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 7 times.
|
7 | if (id == activeAccountId()) return true; |
| 1351 |
4/8✓ Branch 7 → 8 taken 7 times.
✗ Branch 7 → 13 not taken.
✓ Branch 9 → 10 taken 7 times.
✗ Branch 9 → 13 not taken.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 7 times.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 20 taken 7 times.
|
7 | if (m_actionPending || m_compose.draftCleanupActive() || m_session.undo()->busy()) { |
| 1352 | ✗ | m_statusState.error(tr("Wait for the current send or action to finish before changing accounts.")); | |
| 1353 | ✗ | return false; | |
| 1354 | } | ||
| 1355 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 27 taken 7 times.
|
7 | if (!m_session.accounts()->selectAccount(id)) { |
| 1356 | ✗ | m_statusState.error(tr("The account could not be selected or its selection could not be saved.")); | |
| 1357 | ✗ | return false; | |
| 1358 | } | ||
| 1359 | 7 | m_compose.resetForAccountSwitch(); | |
| 1360 | 7 | m_workspace.clearHtmlConsent(); | |
| 1361 |
1/1✓ Branch 30 → 31 taken 7 times.
|
7 | m_workspace.retainMailIdentities([this](const MailId &mail) { return acceptsIdentity(mail); }); |
| 1362 | 7 | m_workspace.activateTab(0); | |
| 1363 | 7 | return true; | |
| 1364 | } | ||
| 1365 | |||
| 1366 | 841 | QString QuickMailBackend::accountEmail() const { | |
| 1367 | 841 | const auto &account = m_session.accounts()->primary(); | |
| 1368 |
2/2✓ Branch 5 → 6 taken 833 times.
✓ Branch 5 → 8 taken 8 times.
|
841 | return account ? account->email : QString{}; |
| 1369 | } | ||
| 1370 | |||
| 1371 | 234987 | QString QuickMailBackend::accountName() const { | |
| 1372 | 234987 | const auto &account = m_session.accounts()->primary(); | |
| 1373 |
2/2✓ Branch 5 → 6 taken 233816 times.
✓ Branch 5 → 8 taken 1171 times.
|
234987 | return account ? account->name : QString{}; |
| 1374 | } | ||
| 1375 | |||
| 1376 | 42818 | bool QuickMailBackend::configured() const { return m_session.accounts()->primary().has_value(); } | |
| 1377 | |||
| 1378 | 1277 | QAbstractItemModel *QuickMailBackend::mailList() { | |
| 1379 |
2/2✓ Branch 3 → 4 taken 204 times.
✓ Branch 3 → 5 taken 1073 times.
|
1277 | return threadedPresentation() ? static_cast<QAbstractItemModel *>(&m_threadList) |
| 1380 | 1277 | : static_cast<QAbstractItemModel *>(&m_mail); | |
| 1381 | } | ||
| 1382 | |||
| 1383 | 57372 | bool QuickMailBackend::threadedPresentation() const { | |
| 1384 | 57372 | return m_threaded; | |
| 1385 | } | ||
| 1386 | |||
| 1387 | 846 | void QuickMailBackend::setThreaded(bool on) { | |
| 1388 |
2/2✓ Branch 2 → 3 taken 796 times.
✓ Branch 2 → 4 taken 50 times.
|
846 | if (m_threaded == on) return; |
| 1389 | 50 | m_threaded = on; | |
| 1390 |
2/2✓ Branch 4 → 5 taken 35 times.
✓ Branch 4 → 6 taken 15 times.
|
50 | if (on) { |
| 1391 | 35 | syncThreadProjection(); | |
| 1392 | } else { | ||
| 1393 | 15 | m_session.controller()->setStreamedHeadersVisible(true); | |
| 1394 | 15 | m_headerProjectionTimer.stop(); | |
| 1395 | 15 | m_headerProjectionFolder.clear(); | |
| 1396 | 15 | cancelThreadProjection(); | |
| 1397 | 15 | m_threadList.releaseHeldRows(); | |
| 1398 | 15 | m_threadList.leaveIndexedFolder(); | |
| 1399 | 15 | m_threadProjectionPending = false; | |
| 1400 | 15 | clearThreadProjectionSlowFlag(); | |
| 1401 | 15 | m_session.threads()->clear(); | |
| 1402 | 15 | m_session.controller()->refreshFlatPageIfNeeded(true); | |
| 1403 | } | ||
| 1404 | 50 | refreshSelectionScope(); | |
| 1405 | 50 | emit threadedChanged(); | |
| 1406 | // The identity-based selection state survives the model swap; the list view | ||
| 1407 | // re-derives its current row through rowForIdentity after the swap. | ||
| 1408 |
5/5✓ Branch 20 → 21 taken 35 times.
✓ Branch 20 → 22 taken 15 times.
✓ Branch 21 → 23 taken 35 times.
✓ Branch 22 → 23 taken 15 times.
✓ Branch 23 → 24 taken 50 times.
|
50 | setStatus(on ? tr("Thread view enabled.") : tr("Thread view disabled.")); |
| 1409 | } | ||
| 1410 | |||
| 1411 | 28 | void QuickMailBackend::armThreadProjectionSlowFlag() { | |
| 1412 | // One timer belongs to the current pending interval. Repeated projection | ||
| 1413 | // changes cannot queue stale callbacks that mark a new folder slow early. | ||
| 1414 |
3/6✓ Branch 2 → 3 taken 28 times.
✗ Branch 2 → 6 not taken.
✓ Branch 4 → 5 taken 28 times.
✗ Branch 4 → 6 not taken.
✓ Branch 7 → 8 taken 28 times.
✗ Branch 7 → 9 not taken.
|
28 | if (!m_threadProjectionSlow && !m_threadProjectionSlowTimer.isActive()) |
| 1415 | 28 | m_threadProjectionSlowTimer.start(m_threadProjectionSlowThresholdMs); | |
| 1416 | 28 | } | |
| 1417 | |||
| 1418 | 905 | void QuickMailBackend::clearThreadProjectionSlowFlag() { | |
| 1419 | 905 | m_threadProjectionSlowTimer.stop(); | |
| 1420 |
2/2✓ Branch 3 → 4 taken 903 times.
✓ Branch 3 → 5 taken 2 times.
|
905 | if (!m_threadProjectionSlow) return; |
| 1421 | 2 | m_threadProjectionSlow = false; | |
| 1422 | 2 | emit threadProjectionSlowChanged(); | |
| 1423 | } | ||
| 1424 | |||
| 1425 | 1 | void QuickMailBackend::setThreadProjectionSlowThresholdMs(int ms) { | |
| 1426 | 1 | m_threadProjectionSlowThresholdMs = qMax(0, ms); | |
| 1427 | 1 | } | |
| 1428 | |||
| 1429 | 2373 | void QuickMailBackend::cancelThreadProjection() { | |
| 1430 | 2373 | m_threadProjectionSlowTimer.stop(); | |
| 1431 | 2373 | m_threadList.cancelIndexedRequest(); | |
| 1432 | 2373 | ++m_threadProjectionGeneration; | |
| 1433 | // S106-291: m_threadPool is the compose worker (send build, server-draft | ||
| 1434 | // build) since the S91 split; clear() here deleted a queued send without | ||
| 1435 | // running it. The projection is fenced by the generation alone. | ||
| 1436 | 2373 | } | |
| 1437 | |||
| 1438 | 16 | void QuickMailBackend::retainThreadSnapshot(qint64 folderId, qint64 externalVersion) { | |
| 1439 | 16 | const auto data = m_session.threads()->prepared(); | |
| 1440 |
3/6✓ Branch 5 → 6 taken 16 times.
✗ Branch 5 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 16 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 16 times.
|
16 | if (!data || externalVersion < 0) return; |
| 1441 | 16 | const qint64 cost = qMax(qint64(1), (data->retainedBytes + 1023) / 1024); | |
| 1442 |
1/2✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 16 times.
|
16 | if (cost > m_threadSnapshots.maxCost()) return; |
| 1443 |
1/4✗ Branch 21 → 22 not taken.
✓ Branch 21 → 24 taken 16 times.
✗ Branch 34 → 35 not taken.
✗ Branch 34 → 37 not taken.
|
16 | m_threadSnapshots.insert(folderId, new ThreadSnapshot{data, externalVersion, |
| 1444 |
3/5✓ Branch 16 → 17 taken 16 times.
✓ Branch 19 → 20 taken 16 times.
✓ Branch 20 → 21 taken 16 times.
✗ Branch 31 → 32 not taken.
✗ Branch 31 → 33 not taken.
|
16 | m_session.cache()->uidValidity(folderId)}, int(cost)); |
| 1445 | 16 | } | |
| 1446 | |||
| 1447 | 24 | bool QuickMailBackend::restoreThreadSnapshot(qint64 folderId) { | |
| 1448 | 24 | const auto *snapshot = m_threadSnapshots.object(folderId); | |
| 1449 |
2/2✓ Branch 3 → 4 taken 20 times.
✓ Branch 3 → 5 taken 4 times.
|
24 | if (!snapshot) return false; |
| 1450 |
2/4✓ Branch 7 → 8 taken 4 times.
✗ Branch 7 → 11 not taken.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 16 taken 4 times.
|
8 | if (snapshot->externalVersion != m_session.cache()->externalDataVersion() || |
| 1451 |
1/2✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 4 times.
|
4 | snapshot->uidValidity != m_session.cache()->uidValidity(folderId)) { |
| 1452 | ✗ | m_threadSnapshots.remove(folderId); | |
| 1453 | ✗ | return false; | |
| 1454 | } | ||
| 1455 | 4 | cancelThreadProjection(); | |
| 1456 | 4 | m_threadProjectionPending = false; | |
| 1457 | 4 | clearThreadProjectionSlowFlag(); | |
| 1458 |
2/2✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 28 taken 1 time.
|
4 | if (m_session.threads()->prepared() != snapshot->data) |
| 1459 |
1/1✓ Branch 25 → 26 taken 3 times.
|
3 | m_session.threads()->setPrepared(snapshot->data); |
| 1460 | 4 | emit threadedChanged(); | |
| 1461 | 4 | return true; | |
| 1462 | } | ||
| 1463 | |||
| 1464 | 732 | void QuickMailBackend::syncThreadProjection() { | |
| 1465 |
2/2✓ Branch 3 → 4 taken 682 times.
✓ Branch 3 → 5 taken 50 times.
|
766 | if (!threadedPresentation()) return; |
| 1466 |
1/1✓ Branch 5 → 6 taken 50 times.
|
50 | const auto folderId = folderIdForPath(m_folderPath); |
| 1467 |
2/2✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 14 taken 45 times.
|
50 | if (folderId < 0) { |
| 1468 |
1/1✓ Branch 7 → 8 taken 5 times.
|
5 | cancelThreadProjection(); |
| 1469 |
1/1✓ Branch 8 → 9 taken 5 times.
|
5 | m_threadList.leaveIndexedFolder(); |
| 1470 | 5 | m_threadProjectionPending = false; | |
| 1471 |
1/1✓ Branch 9 → 10 taken 5 times.
|
5 | clearThreadProjectionSlowFlag(); |
| 1472 |
1/1✓ Branch 11 → 12 taken 5 times.
|
5 | m_session.threads()->clear(); |
| 1473 |
1/1✓ Branch 12 → 13 taken 5 times.
|
5 | emit threadedChanged(); |
| 1474 | 5 | return; | |
| 1475 | } | ||
| 1476 |
1/1✓ Branch 15 → 16 taken 45 times.
|
45 | const auto state = m_session.cache()->folderPresentationState(folderId); |
| 1477 |
6/6✓ Branch 17 → 18 taken 34 times.
✓ Branch 17 → 19 taken 11 times.
✓ Branch 18 → 19 taken 15 times.
✓ Branch 18 → 20 taken 19 times.
✓ Branch 21 → 22 taken 26 times.
✓ Branch 21 → 46 taken 19 times.
|
45 | if (m_threadList.indexed() || state.headerCount > 200) { |
| 1478 | 26 | m_session.controller()->setStreamedHeadersVisible(false); | |
| 1479 | 26 | const auto selected = m_workspace.mailId(); | |
| 1480 |
3/3✓ Branch 32 → 33 taken 15 times.
✓ Branch 32 → 35 taken 11 times.
✓ Branch 38 → 39 taken 26 times.
|
67 | m_threadList.configureIndexedFolder(m_session.cache()->databasePath(), folderId, |
| 1481 | 26 | state.uidValidity, state.threadRevision, | |
| 1482 |
2/6✓ Branch 33 → 34 taken 15 times.
✗ Branch 33 → 35 not taken.
✓ Branch 40 → 41 taken 26 times.
✗ Branch 40 → 42 not taken.
✗ Branch 69 → 70 not taken.
✗ Branch 69 → 71 not taken.
|
67 | selected.accountId == accountName() && selected.folderId == folderId ? selected.uid : 0, |
| 1483 |
1/1✓ Branch 29 → 30 taken 26 times.
|
52 | m_session.cache()->journal().pendingMoveUids(accountName(), folderId)); |
| 1484 | 26 | return; | |
| 1485 | 26 | } | |
| 1486 | 19 | m_session.controller()->setStreamedHeadersVisible(true); | |
| 1487 |
2/2✓ Branch 48 → 49 taken 1 time.
✓ Branch 48 → 50 taken 18 times.
|
19 | if (m_waitingCachedPage) { m_threadProjectionPending = true; return; } |
| 1488 |
3/3✓ Branch 50 → 51 taken 18 times.
✓ Branch 51 → 52 taken 2 times.
✓ Branch 51 → 53 taken 16 times.
|
18 | if (restoreThreadSnapshot(folderId)) return; |
| 1489 |
1/1✓ Branch 53 → 54 taken 16 times.
|
16 | cancelThreadProjection(); |
| 1490 | 16 | m_threadProjectionPending = false; | |
| 1491 |
1/1✓ Branch 54 → 55 taken 16 times.
|
16 | clearThreadProjectionSlowFlag(); |
| 1492 |
1/1✓ Branch 58 → 59 taken 16 times.
|
16 | m_session.threads()->setHeaders(m_session.headers()->allHeaders()); |
| 1493 |
2/2✓ Branch 60 → 61 taken 16 times.
✓ Branch 61 → 62 taken 16 times.
|
16 | retainThreadSnapshot(folderId, m_session.cache()->externalDataVersion()); |
| 1494 |
1/1✓ Branch 62 → 63 taken 16 times.
|
16 | emit threadedChanged(); |
| 1495 | } | ||
| 1496 | |||
| 1497 | 1 | void QuickMailBackend::queueIndexedMoveExclusionRefresh(qint64 folderId) { | |
| 1498 | // A burst of Move journal transitions in one event turn refreshes the | ||
| 1499 | // exclusion set once. The visible row was already hidden by the caller. | ||
| 1500 | // The read is one small indexed journal query; WAL readers never wait | ||
| 1501 | // for another connection's write lock (mailjd-bench data.gui_read_*). | ||
| 1502 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 1 time.
|
1 | if (m_moveExclusionRefreshQueued.contains(folderId)) return; |
| 1503 |
1/1✓ Branch 5 → 6 taken 1 time.
|
1 | m_moveExclusionRefreshQueued.insert(folderId); |
| 1504 | 1 | const auto generation = m_session.accounts()->generation(); | |
| 1505 |
1/1✓ Branch 8 → 9 taken 1 time.
|
1 | QTimer::singleShot(0, this, [this, folderId, generation] { |
| 1506 |
1/1✓ Branch 2 → 3 taken 1 time.
|
1 | m_moveExclusionRefreshQueued.remove(folderId); |
| 1507 | 1 | if (generation != m_session.accounts()->generation() || | |
| 1508 |
4/8✓ Branch 5 → 6 taken 1 time.
✗ Branch 5 → 10 not taken.
✓ Branch 7 → 8 taken 1 time.
✗ Branch 7 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 1 time.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
|
1 | !m_threadList.indexed() || m_threadList.indexedFolder() != folderId) return; |
| 1509 | 1 | bool ok = false; | |
| 1510 | auto exclusions = | ||
| 1511 |
1/1✓ Branch 17 → 18 taken 1 time.
|
1 | m_session.cache()->journal().pendingMoveUids(accountName(), folderId, &ok); |
| 1512 |
1/1✓ Branch 20 → 21 taken 1 time.
|
1 | exclusions.unite(m_visualMoveUids.value(folderId)); |
| 1513 |
1/2✗ Branch 22 → 23 not taken.
✓ Branch 22 → 28 taken 1 time.
|
1 | if (!ok) { |
| 1514 | // Keep the conservative visible exclusion; release a waiting page. | ||
| 1515 | ✗ | m_threadList.abandonIndexedMoveRefresh(); | |
| 1516 | ✗ | setStatus(tr("Folder move state could not be refreshed")); | |
| 1517 | ✗ | return; | |
| 1518 | } | ||
| 1519 |
1/1✓ Branch 28 → 29 taken 1 time.
|
1 | m_threadList.refreshIndexedExclusions(exclusions); |
| 1520 |
1/2✓ Branch 31 → 32 taken 1 time.
✗ Branch 31 → 34 not taken.
|
1 | }); |
| 1521 | } | ||
| 1522 | |||
| 1523 | 11530 | int QuickMailBackend::activeRowCount() const { | |
| 1524 |
4/4✓ Branch 3 → 4 taken 1007 times.
✓ Branch 3 → 7 taken 10523 times.
✓ Branch 5 → 6 taken 1007 times.
✓ Branch 8 → 9 taken 10523 times.
|
11530 | return threadedPresentation() ? m_threadList.rowCount() : m_mail.rowCount(); |
| 1525 | } | ||
| 1526 | |||
| 1527 | 663 | QVariantMap QuickMailBackend::activeMailAt(int row) const { | |
| 1528 |
2/2✓ Branch 3 → 4 taken 27 times.
✓ Branch 3 → 5 taken 636 times.
|
663 | return threadedPresentation() ? m_threadList.mailAt(row) : m_mail.mailAt(row); |
| 1529 | } | ||
| 1530 | |||
| 1531 | 17641 | int QuickMailBackend::activeRowForIdentity(const QVariantMap &identity) const { | |
| 1532 |
2/2✓ Branch 3 → 4 taken 983 times.
✓ Branch 3 → 6 taken 16658 times.
|
17641 | return threadedPresentation() ? m_threadList.rowForIdentity(identity) |
| 1533 | 17641 | : m_mail.rowForIdentity(identity); | |
| 1534 | } | ||
| 1535 | |||
| 1536 | 5486 | int QuickMailBackend::loadedRowForIdentity(const MailId &id) const { | |
| 1537 |
4/8✓ Branch 3 → 4 taken 5486 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 5486 times.
✓ Branch 9 → 10 taken 5486 times.
✗ Branch 9 → 11 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 5486 times.
|
5486 | if (!id.isValid() || id.accountId != accountName()) return -1; |
| 1538 |
2/2✓ Branch 13 → 14 taken 5486 times.
✓ Branch 14 → 15 taken 5486 times.
|
5486 | return activeRowForIdentity(WorkspaceState::encode(id)); |
| 1539 | } | ||
| 1540 | |||
| 1541 | 12190 | MailSelectionKey QuickMailBackend::activeKeyAt(int row) const { | |
| 1542 |
2/2✓ Branch 3 → 4 taken 1656 times.
✓ Branch 3 → 5 taken 10534 times.
|
12190 | return threadedPresentation() ? m_threadList.keyAt(row) : m_mail.keyAt(row); |
| 1543 | } | ||
| 1544 | |||
| 1545 | // F-41 (SPRINT-104): the identities the active list visibly shows between | ||
| 1546 | // two rows — the range Shift+click must select, in the presented (sort or | ||
| 1547 | // thread) order. Empty when an endpoint is not currently presented (the | ||
| 1548 | // flat window slid past it) or the span exceeds the explicit capture | ||
| 1549 | // bound; the caller then falls back to the date/UID range. | ||
| 1550 | 6 | QList<qint64> QuickMailBackend::visibleRangeBetween(const MailSelectionKey &anchor, | |
| 1551 | const MailSelectionKey &key) const { | ||
| 1552 | 6 | QList<qint64> uids; | |
| 1553 |
4/8✓ Branch 3 → 4 taken 6 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 6 times.
✓ Branch 9 → 10 taken 6 times.
✗ Branch 9 → 11 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 6 times.
|
6 | if (m_workspace.activeTab() != 0 || m_workspace.area() != "mail") |
| 1554 | ✗ | return uids; | |
| 1555 |
1/1✓ Branch 13 → 14 taken 6 times.
|
6 | const int count = activeRowCount(); |
| 1556 | 6 | int anchorRow = -1, keyRow = -1; | |
| 1557 |
5/6✓ Branch 26 → 27 taken 253 times.
✗ Branch 26 → 29 not taken.
✓ Branch 27 → 15 taken 220 times.
✓ Branch 27 → 28 taken 33 times.
✓ Branch 28 → 15 taken 27 times.
✓ Branch 28 → 29 taken 6 times.
|
253 | for (int row = 0; row < count && (anchorRow < 0 || keyRow < 0); ++row) { |
| 1558 |
1/1✓ Branch 15 → 16 taken 247 times.
|
247 | const auto rowKey = activeKeyAt(row); |
| 1559 |
5/6✓ Branch 16 → 17 taken 220 times.
✓ Branch 16 → 20 taken 27 times.
✓ Branch 17 → 18 taken 220 times.
✗ Branch 17 → 20 not taken.
✓ Branch 18 → 19 taken 6 times.
✓ Branch 18 → 20 taken 214 times.
|
247 | if (anchorRow < 0 && rowKey.folderId == anchor.folderId && rowKey.uid == anchor.uid) |
| 1560 | 6 | anchorRow = row; | |
| 1561 |
4/6✓ Branch 20 → 21 taken 247 times.
✗ Branch 20 → 24 not taken.
✓ Branch 21 → 22 taken 247 times.
✗ Branch 21 → 24 not taken.
✓ Branch 22 → 23 taken 6 times.
✓ Branch 22 → 24 taken 241 times.
|
247 | if (keyRow < 0 && rowKey.folderId == key.folderId && rowKey.uid == key.uid) |
| 1562 | 6 | keyRow = row; | |
| 1563 | 247 | } | |
| 1564 |
2/4✓ Branch 29 → 30 taken 6 times.
✗ Branch 29 → 31 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 6 times.
|
6 | if (anchorRow < 0 || keyRow < 0) return uids; |
| 1565 | 6 | const int from = qMin(anchorRow, keyRow), to = qMax(anchorRow, keyRow); | |
| 1566 |
1/2✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 6 times.
|
6 | if (to - from + 1 > MailSelectionQuery::MaximumExplicitUids) return uids; |
| 1567 |
1/1✓ Branch 36 → 37 taken 6 times.
|
6 | uids.reserve(to - from + 1); |
| 1568 |
2/2✓ Branch 42 → 38 taken 33 times.
✓ Branch 42 → 43 taken 6 times.
|
39 | for (int row = from; row <= to; ++row) |
| 1569 |
2/2✓ Branch 38 → 39 taken 33 times.
✓ Branch 39 → 40 taken 33 times.
|
33 | uids.append(activeKeyAt(row).uid); |
| 1570 | 6 | return uids; | |
| 1571 | ✗ | } | |
| 1572 | |||
| 1573 | 9951 | bool QuickMailBackend::acceptsIdentity(const MailId &id) const { | |
| 1574 |
10/10✓ Branch 3 → 4 taken 9911 times.
✓ Branch 3 → 9 taken 40 times.
✓ Branch 5 → 6 taken 5493 times.
✓ Branch 5 → 9 taken 4418 times.
✓ Branch 8 → 9 taken 25 times.
✓ Branch 8 → 10 taken 5468 times.
✓ Branch 11 → 12 taken 5493 times.
✓ Branch 11 → 13 taken 4458 times.
✓ Branch 13 → 14 taken 4483 times.
✓ Branch 13 → 15 taken 5468 times.
|
9951 | if (!configured() || !id.isValid() || id.accountId != accountName()) return false; |
| 1575 | // P90.17: folder admission answers from the in-memory identity cache — | ||
| 1576 | // no per-call SQLite round trips on the click/selection paths. Only the | ||
| 1577 | // canonical header-existence probe (one indexed lookup) remains for | ||
| 1578 | // identities without loaded-row evidence. | ||
| 1579 | 5468 | const auto path = m_folderPathById.constFind(id.folderId); | |
| 1580 |
2/2✓ Branch 18 → 19 taken 7 times.
✓ Branch 18 → 20 taken 5461 times.
|
5468 | if (path == m_folderPathById.constEnd()) return false; |
| 1581 |
2/2✓ Branch 22 → 23 taken 5453 times.
✓ Branch 22 → 26 taken 8 times.
|
10914 | if (!m_availableFolders.contains(path.value()) || |
| 1582 |
3/4✗ Branch 25 → 26 not taken.
✓ Branch 25 → 27 taken 5453 times.
✓ Branch 28 → 29 taken 8 times.
✓ Branch 28 → 30 taken 5453 times.
|
10914 | m_folderIdByPath.value(path.value(), -1) != id.folderId) return false; |
| 1583 | // The active projection is already scoped to the current account and | ||
| 1584 | // exposes this exact identity. Do not re-read its header on every click. | ||
| 1585 |
3/3✓ Branch 30 → 31 taken 5453 times.
✓ Branch 31 → 32 taken 4477 times.
✓ Branch 31 → 33 taken 976 times.
|
5453 | if (loadedRowForIdentity(id) >= 0) return true; |
| 1586 | // Restored selections and notifications can target a row outside the | ||
| 1587 | // loaded page; retain the canonical existence check for those paths. | ||
| 1588 |
1/1✓ Branch 34 → 35 taken 976 times.
|
976 | return m_session.cache()->header(id.folderId, id.uid).has_value(); |
| 1589 | } | ||
| 1590 | |||
| 1591 | 2406 | void QuickMailBackend::publishSelection() { | |
| 1592 |
2/2✓ Branch 2 → 3 taken 92 times.
✓ Branch 2 → 4 taken 2314 times.
|
2406 | if (m_reconciling) return; |
| 1593 | 2314 | const auto id = m_workspace.mailId(); | |
| 1594 |
4/4✓ Branch 6 → 7 taken 2314 times.
✓ Branch 7 → 8 taken 1245 times.
✓ Branch 7 → 9 taken 1069 times.
✓ Branch 8 → 11 taken 1245 times.
|
2314 | if (acceptsIdentity(id)) requestSelectedMail(id); |
| 1595 |
1/1✓ Branch 10 → 11 taken 1069 times.
|
1069 | else m_session.controller()->clearDisplaySelection(); |
| 1596 | 2314 | } | |
| 1597 | |||
| 1598 | 1767 | void QuickMailBackend::requestSelectedMail(const MailId &id) { | |
| 1599 | 1767 | const MailHeader *preview = threadedPresentation() | |
| 1600 |
2/2✓ Branch 3 → 4 taken 77 times.
✓ Branch 3 → 6 taken 1690 times.
|
1767 | ? m_threadList.indexedHeader(id.folderId, id.uid) : nullptr; |
| 1601 |
2/2✓ Branch 7 → 8 taken 50 times.
✓ Branch 7 → 13 taken 1717 times.
|
1767 | if (preview) { |
| 1602 | // Selection tracing may publish signals before the controller copies the | ||
| 1603 | // hint. Keep the indexed page's storage out of that reentrant interval. | ||
| 1604 | 50 | const MailHeader stable = *preview; | |
| 1605 |
1/1✓ Branch 10 → 11 taken 50 times.
|
50 | m_session.controller()->onMailSelected(id, stable); |
| 1606 | 50 | } else { | |
| 1607 | 1717 | m_session.controller()->onMailSelected(id); | |
| 1608 | } | ||
| 1609 | 1767 | } | |
| 1610 | |||
| 1611 | 1218 | void QuickMailBackend::setStatus(const QString &status) { | |
| 1612 | 1218 | m_statusState.feedback(status); | |
| 1613 | 1218 | } | |
| 1614 | |||
| 1615 | 793 | void QuickMailBackend::loadLocalFolders() { | |
| 1616 |
2/2✓ Branch 3 → 4 taken 8 times.
✓ Branch 3 → 12 taken 785 times.
|
793 | if (!configured()) { |
| 1617 |
1/1✓ Branch 5 → 6 taken 8 times.
|
8 | applyFolderList({}, false); |
| 1618 |
2/2✓ Branch 7 → 8 taken 8 times.
✓ Branch 8 → 9 taken 8 times.
|
8 | setStatus(tr("No accounts configured")); |
| 1619 |
1/1✓ Branch 10 → 11 taken 8 times.
|
8 | emit folderChanged(); |
| 1620 | 12 | return; | |
| 1621 | } | ||
| 1622 | 785 | const auto &account = *m_session.accounts()->primary(); | |
| 1623 |
3/3✓ Branch 15 → 16 taken 785 times.
✓ Branch 16 → 17 taken 785 times.
✓ Branch 17 → 18 taken 785 times.
|
785 | const QString snapshotPath = QDir(m_profileDirectory).filePath("folder-session.json"); |
| 1624 |
1/1✓ Branch 20 → 21 taken 785 times.
|
785 | const auto snapshot = FolderSessionSnapshot::load(snapshotPath, account); |
| 1625 | 785 | QList<FolderInfo> folders; | |
| 1626 |
2/2✓ Branch 22 → 23 taken 9 times.
✓ Branch 22 → 25 taken 776 times.
|
785 | if (snapshot) folders = *snapshot; |
| 1627 |
3/3✓ Branch 25 → 26 taken 776 times.
✓ Branch 26 → 27 taken 772 times.
✓ Branch 26 → 42 taken 4 times.
|
776 | else if (!QFile::exists(snapshotPath)) { |
| 1628 |
5/5✓ Branch 28 → 29 taken 772 times.
✓ Branch 29 → 30 taken 772 times.
✓ Branch 30 → 31 taken 772 times.
✓ Branch 39 → 32 taken 1831 times.
✓ Branch 39 → 40 taken 772 times.
|
2603 | for (const auto &path : m_session.cache()->allFolderPaths(account.name)) |
| 1629 | 2603 | folders.append({ImapResponseParser::decodeMailboxName(path), path, {}, {}}); | |
| 1630 | } | ||
| 1631 | // A rejected snapshot is not permission to reuse a different server's cache. | ||
| 1632 |
1/1✓ Branch 42 → 43 taken 785 times.
|
785 | applyFolderList(folders, false); |
| 1633 |
7/7✓ Branch 44 → 45 taken 776 times.
✓ Branch 44 → 48 taken 9 times.
✓ Branch 45 → 46 taken 776 times.
✓ Branch 46 → 47 taken 4 times.
✓ Branch 46 → 48 taken 772 times.
✓ Branch 49 → 50 taken 4 times.
✓ Branch 49 → 54 taken 781 times.
|
785 | if (!snapshot && QFile::exists(snapshotPath)) { |
| 1634 |
2/2✓ Branch 50 → 51 taken 4 times.
✓ Branch 51 → 52 taken 4 times.
|
4 | setStatus(tr("Local folder state is unavailable or belongs to another account. Connect to refresh folders.")); |
| 1635 | 4 | return; | |
| 1636 | } | ||
| 1637 |
5/5✓ Branch 56 → 57 taken 2 times.
✓ Branch 56 → 58 taken 779 times.
✓ Branch 57 → 59 taken 2 times.
✓ Branch 58 → 59 taken 779 times.
✓ Branch 59 → 60 taken 781 times.
|
781 | setStatus(m_session.accounts()->networkEnabled() ? tr("Loading account…") |
| 1638 | : tr("Offline · Local mail")); | ||
| 1639 | 2624 | } | |
| 1640 | |||
| 1641 | 826 | void QuickMailBackend::rebuildFolderIdentityCache() { | |
| 1642 | 826 | m_folderIdByPath.clear(); | |
| 1643 | 826 | m_folderPathById.clear(); | |
| 1644 | 826 | m_folderEpochById.clear(); | |
| 1645 |
2/2✓ Branch 6 → 7 taken 8 times.
✓ Branch 6 → 8 taken 818 times.
|
826 | if (!configured()) return; |
| 1646 |
5/5✓ Branch 10 → 11 taken 818 times.
✓ Branch 12 → 13 taken 818 times.
✓ Branch 13 → 14 taken 818 times.
✓ Branch 21 → 15 taken 2136 times.
✓ Branch 21 → 22 taken 818 times.
|
2954 | for (const auto &identity : m_session.cache()->folderIdentities(accountName())) { |
| 1647 |
1/1✓ Branch 16 → 17 taken 2136 times.
|
2136 | m_folderIdByPath.insert(identity.path, identity.id); |
| 1648 |
1/1✓ Branch 17 → 18 taken 2136 times.
|
2136 | m_folderPathById.insert(identity.id, identity.path); |
| 1649 |
1/1✓ Branch 18 → 19 taken 2136 times.
|
2136 | m_folderEpochById.insert(identity.id, identity.uidValidity); |
| 1650 | 818 | } | |
| 1651 | } | ||
| 1652 | |||
| 1653 | 1886 | qint64 QuickMailBackend::folderIdForPath(const QString &path) const { | |
| 1654 |
5/6✓ Branch 3 → 4 taken 1667 times.
✓ Branch 3 → 6 taken 219 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 1667 times.
✓ Branch 8 → 9 taken 219 times.
✓ Branch 8 → 10 taken 1667 times.
|
1886 | if (path.isEmpty() || !configured()) return -1; |
| 1655 | // The map was loaded for the active account. The controller may still | ||
| 1656 | // retain its previous folder ID during an account handover. | ||
| 1657 | 1667 | const auto known = m_folderIdByPath.constFind(path); | |
| 1658 |
2/2✓ Branch 13 → 14 taken 1665 times.
✓ Branch 13 → 16 taken 2 times.
|
1667 | if (known != m_folderIdByPath.cend()) return known.value(); |
| 1659 | // A folder from a fresh server list may not have been ensured yet. Keep | ||
| 1660 | // that cold-path lookup; known folders use the in-memory identity map. | ||
| 1661 |
1/1✓ Branch 18 → 19 taken 2 times.
|
2 | return m_session.cache()->findFolderId(accountName(), path); |
| 1662 | } | ||
| 1663 | |||
| 1664 | // Sprint 95: what the rule backend reads from the mail session. | ||
| 1665 | 795 | QuickRulesBackend::Deps QuickMailBackend::rulesDeps() { | |
| 1666 | 795 | QuickRulesBackend::Deps deps; | |
| 1667 | 15877 | deps.account = [this]() -> std::optional<AccountConfig> { return m_session.accounts()->primary(); }; | |
| 1668 | 795 | deps.configuration = [this](const QString &id) { return m_session.accounts()->configuration(id); }; | |
| 1669 | 1621 | deps.imapCapabilities = [this] { | |
| 1670 |
1/2✓ Branch 3 → 4 taken 31 times.
✗ Branch 3 → 6 not taken.
|
31 | return m_session.imap() ? m_session.imap()->serverCapabilities() : QStringList{}; |
| 1671 | 795 | }; | |
| 1672 | 1744 | deps.folders = [this] { return m_folderInfos; }; | |
| 1673 | 5731 | deps.decodeMailbox = [](const QString &path) { return ImapResponseParser::decodeMailboxName(path); }; | |
| 1674 | 795 | deps.profileDirectory = m_profileDirectory; | |
| 1675 | 259 | const auto headerFields = [this](qint64 folderId, qint64 uid) -> QMap<QString, QString> { | |
| 1676 |
1/1✓ Branch 3 → 4 taken 259 times.
|
259 | const auto snapshot = m_session.cache()->replySnapshot(folderId, uid); |
| 1677 |
3/6✓ Branch 5 → 6 taken 259 times.
✗ Branch 5 → 9 not taken.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 259 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 259 times.
|
259 | if (!snapshot || snapshot->rawSource.isEmpty()) return {}; |
| 1678 | 259 | QMap<QString, QString> fields; | |
| 1679 |
1/1✓ Branch 14 → 15 taken 259 times.
|
259 | const auto raw = MimeParser::messageHeaders(snapshot->rawSource); |
| 1680 |
4/4✓ Branch 15 → 16 taken 259 times.
✓ Branch 25 → 26 taken 1817 times.
✓ Branch 27 → 17 taken 1558 times.
✓ Branch 27 → 28 taken 259 times.
|
1817 | for (auto it = raw.cbegin(); it != raw.cend(); ++it) |
| 1681 |
3/3✓ Branch 18 → 19 taken 1558 times.
✓ Branch 20 → 21 taken 1558 times.
✓ Branch 21 → 22 taken 1558 times.
|
1558 | fields.insert(it.key().toLower(), ImapResponseParser::decodeRfc2047(it.value())); |
| 1682 | 259 | return fields; | |
| 1683 | 259 | }; | |
| 1684 | 795 | deps.cachedHeaderFields = headerFields; | |
| 1685 | 798 | deps.suggestionsEnabled = [this] { return m_preferences.ruleSuggestions(); }; | |
| 1686 | 1816 | deps.displayedMail = [this, headerFields] { | |
| 1687 | 226 | QuickRulesBackend::Deps::DisplayedMail mail; | |
| 1688 | 226 | const MailId id = m_workspace.mailId(); | |
| 1689 |
3/3✓ Branch 4 → 5 taken 226 times.
✓ Branch 5 → 6 taken 17 times.
✓ Branch 5 → 7 taken 209 times.
|
226 | if (!acceptsIdentity(id)) return mail; |
| 1690 | 209 | mail.folderId = id.folderId; | |
| 1691 | 209 | mail.uid = id.uid; | |
| 1692 | 209 | mail.folderPath = m_folderPathById.value(id.folderId, m_folderPath); | |
| 1693 |
1/1✓ Branch 10 → 11 taken 209 times.
|
209 | mail.headers = headerFields(id.folderId, id.uid); |
| 1694 |
2/3✓ Branch 13 → 14 taken 209 times.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 77 taken 209 times.
|
209 | if (mail.headers.isEmpty()) |
| 1695 | ✗ | for (const auto &header : m_session.cache()->headersByIds({id})) { | |
| 1696 | ✗ | mail.headers.insert(QStringLiteral("from"), header.from); | |
| 1697 | ✗ | mail.headers.insert(QStringLiteral("to"), header.to); | |
| 1698 | ✗ | mail.headers.insert(QStringLiteral("subject"), header.subject); | |
| 1699 | // S95.1.11: the list chip without the raw source. | ||
| 1700 | ✗ | if (header.recipientsKnown) { | |
| 1701 | ✗ | if (!header.cc.isEmpty()) mail.headers.insert(QStringLiteral("cc"), header.cc); | |
| 1702 | ✗ | if (!header.listId.isEmpty()) mail.headers.insert(QStringLiteral("list-id"), QLatin1Char('<') + header.listId + QLatin1Char('>')); | |
| 1703 | } | ||
| 1704 | ✗ | } | |
| 1705 |
1/1✓ Branch 77 → 78 taken 209 times.
|
209 | mail.predictedFolder = visiblePrediction(id).first; |
| 1706 | 209 | return mail; | |
| 1707 |
0/5✗ Branch 17 → 18 not taken.
✗ Branch 91 → 92 not taken.
✗ Branch 91 → 95 not taken.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
|
1021 | }; |
| 1708 | 1615 | deps.cachedHeaders = [this](const QString &path) { | |
| 1709 | // G16: only the columns a rule reads (MailRuleCandidates), not headers(). | ||
| 1710 | 25 | const qint64 folderId = folderIdForPath(path); | |
| 1711 |
1/2✓ Branch 3 → 4 taken 25 times.
✗ Branch 3 → 7 not taken.
|
25 | return folderId > 0 ? MailRuleCandidates::read(m_session.cache()->database(), folderId) : QList<MailHeader>{}; |
| 1712 | 795 | }; | |
| 1713 | 1591 | deps.moveMails = [this](const QString &source, const QList<qint64> &uids, const QString &target) { | |
| 1714 | 1 | m_session.controller()->moves()->moveMailsToFolderFrom(uids, folderIdForPath(source), source, target); | |
| 1715 | 795 | }; | |
| 1716 | 1590 | deps.flagMail = [this](const QString &source, qint64 uid, const QString &kind, const QString &flag) { | |
| 1717 | ✗ | const qint64 folderId = folderIdForPath(source); | |
| 1718 | ✗ | const bool add = kind == QLatin1String("add"); | |
| 1719 | ✗ | if (flag == QLatin1String("\\Seen")) { | |
| 1720 | ✗ | if (add) m_session.controller()->markMailAsSeenInFolder(uid, folderId); | |
| 1721 | ✗ | else m_session.controller()->markMailAsUnseenInFolder(uid, folderId); | |
| 1722 | ✗ | } else if (flag == QLatin1String("\\Flagged")) { | |
| 1723 | ✗ | m_session.controller()->setStarredInFolder(uid, folderId, add); | |
| 1724 | ✗ | } else if (!flag.startsWith(QLatin1Char('\\'))) { | |
| 1725 | ✗ | if (add) m_session.controller()->addLabelInFolder(uid, folderId, flag); | |
| 1726 | ✗ | else m_session.controller()->removeLabelInFolder(uid, folderId, flag); | |
| 1727 | } | ||
| 1728 | 795 | }; | |
| 1729 | 795 | return deps; | |
| 1730 | } | ||
| 1731 | |||
| 1732 | 826 | void QuickMailBackend::applyFolderList(const QList<FolderInfo> &folders, bool authoritative) { | |
| 1733 | 826 | m_folderInfos = folders; | |
| 1734 |
2/2✓ Branch 3 → 4 taken 33 times.
✓ Branch 3 → 5 taken 793 times.
|
826 | if (authoritative) |
| 1735 |
1/1✓ Branch 4 → 5 taken 33 times.
|
33 | m_rules.foldersReady(); |
| 1736 | // F-49 (SPRINT-104): special folders resolve in three steps — the | ||
| 1737 | // user's assignment (settings, per account), the server's special-use | ||
| 1738 | // flag, and the common names matched on the LAST path component (a | ||
| 1739 | // prefixed "INBOX.Trash" or "INBOX/Gesendete Elemente" answers like the | ||
| 1740 | // bare name; the exact whole-path match of before missed them). | ||
| 1741 |
1/1✓ Branch 5 → 6 taken 826 times.
|
826 | const QString delimiter = m_folderActions.folderDelimiter(); |
| 1742 | 4130 | const auto resolve = [this, &folders, &delimiter](const char *settingKey, | |
| 1743 | bool (FolderInfo::*flag)() const, | ||
| 1744 | std::initializer_list<const char *> names) { | ||
| 1745 |
1/1✓ Branch 3 → 4 taken 4130 times.
|
8260 | return QuickFolderActions::resolveSpecialFolder(folders, delimiter, accountName(), |
| 1746 |
1/1✓ Branch 5 → 6 taken 4130 times.
|
12390 | QLatin1String(settingKey), flag, names); |
| 1747 | 826 | }; | |
| 1748 |
1/1✓ Branch 6 → 7 taken 826 times.
|
826 | m_junkFolder = resolve("junk", &FolderInfo::isJunk, {"Junk", "Spam", "Junk-E-Mail"}); |
| 1749 |
1/1✓ Branch 9 → 10 taken 826 times.
|
826 | m_archiveFolder = resolve("archive", &FolderInfo::isArchive, {"Archive", "Archiv"}); |
| 1750 |
1/1✓ Branch 12 → 13 taken 826 times.
|
1652 | m_trashFolder = resolve("trash", &FolderInfo::isTrash, |
| 1751 | 826 | {"Trash", "Deleted", "Deleted Items", "Papierkorb", "Gelöschte Objekte", "Gelöschte Elemente"}); | |
| 1752 |
1/1✓ Branch 15 → 16 taken 826 times.
|
826 | m_draftsFolder = resolve("drafts", &FolderInfo::isDrafts, {"Drafts", "Entwürfe", "Draft Messages"}); |
| 1753 |
1/1✓ Branch 18 → 19 taken 826 times.
|
1652 | m_sentFolder = resolve("sent", &FolderInfo::isSent, |
| 1754 | 826 | {"Sent", "Gesendet", "Sent Items", "Gesendete Objekte", "Gesendete Elemente", "Sent Messages"}); | |
| 1755 |
1/1✓ Branch 21 → 22 taken 826 times.
|
826 | invalidatePredictions(); |
| 1756 | { | ||
| 1757 | // Reconciliation may change tabs and the list together. Publish only the | ||
| 1758 | // final identity, never an intermediate tab from a rejected account snapshot. | ||
| 1759 | 826 | QScopedValueRollback guard(m_reconciling, true); | |
| 1760 |
1/1✓ Branch 25 → 26 taken 826 times.
|
826 | const auto account = m_session.accounts()->primary(); |
| 1761 |
1/1✓ Branch 26 → 27 taken 826 times.
|
826 | const auto hidden = FolderPreferences::loadHidden(m_profileDirectory); |
| 1762 |
3/3✓ Branch 28 → 29 taken 818 times.
✓ Branch 28 → 30 taken 8 times.
✓ Branch 31 → 32 taken 826 times.
|
826 | m_folders.setFolders(account ? folders : QList<FolderInfo>{}, hidden); |
| 1763 |
1/1✓ Branch 33 → 34 taken 826 times.
|
826 | m_folderActions.refreshFolderAppearance(); |
| 1764 | 826 | QStringList selectable; | |
| 1765 |
4/4✓ Branch 35 → 36 taken 818 times.
✓ Branch 35 → 46 taken 8 times.
✓ Branch 44 → 38 taken 2338 times.
✓ Branch 44 → 45 taken 818 times.
|
3164 | if (account) for (const auto &folder : folders) |
| 1766 |
4/4✓ Branch 39 → 40 taken 2338 times.
✓ Branch 40 → 41 taken 2326 times.
✓ Branch 40 → 42 taken 12 times.
✓ Branch 41 → 42 taken 2326 times.
|
2338 | if (!folder.isNoselect()) selectable.append(folder.path); |
| 1767 |
3/3✓ Branch 46 → 47 taken 826 times.
✓ Branch 47 → 48 taken 826 times.
✓ Branch 48 → 49 taken 826 times.
|
826 | m_availableFolders = QSet<QString>(selectable.begin(), selectable.end()); |
| 1768 |
1/1✓ Branch 51 → 52 taken 826 times.
|
826 | rebuildFolderIdentityCache(); |
| 1769 |
1/1✓ Branch 53 → 54 taken 826 times.
|
826 | const auto subscribed = FolderPreferences::accountSubscriptions(m_profileDirectory, accountName(), selectable); |
| 1770 |
3/3✓ Branch 57 → 58 taken 8 times.
✓ Branch 57 → 59 taken 818 times.
✓ Branch 60 → 61 taken 826 times.
|
826 | m_session.controller()->setSubscribedFolders(!account ? QStringList{} : subscribed); |
| 1771 |
2/2✓ Branch 63 → 64 taken 818 times.
✓ Branch 63 → 98 taken 8 times.
|
826 | if (account) { |
| 1772 |
1/1✓ Branch 66 → 67 taken 818 times.
|
818 | const auto badges = m_session.cache()->loadAllBadges(account->name); |
| 1773 |
4/4✓ Branch 67 → 68 taken 818 times.
✓ Branch 73 → 74 taken 838 times.
✓ Branch 75 → 69 taken 20 times.
✓ Branch 75 → 76 taken 818 times.
|
838 | for (auto it = badges.cbegin(); it != badges.cend(); ++it) |
| 1774 |
1/1✓ Branch 71 → 72 taken 20 times.
|
20 | m_folders.setUnread(it.key(), it.value()); |
| 1775 |
2/2✓ Branch 76 → 77 taken 33 times.
✓ Branch 76 → 96 taken 785 times.
|
818 | if (authoritative) { |
| 1776 |
2/2✓ Branch 77 → 78 taken 33 times.
✓ Branch 79 → 80 taken 33 times.
|
33 | m_workspace.retainFolderBranches(account->name, m_folders.branchPaths()); |
| 1777 |
1/1✓ Branch 82 → 83 taken 33 times.
|
33 | m_session.controller()->reconcileAvailableFolders(selectable); |
| 1778 |
5/6✓ Branch 84 → 85 taken 33 times.
✓ Branch 85 → 86 taken 33 times.
✓ Branch 86 → 87 taken 33 times.
✓ Branch 87 → 88 taken 33 times.
✗ Branch 91 → 92 not taken.
✓ Branch 91 → 96 taken 33 times.
|
33 | if (!FolderSessionSnapshot::save(QDir(m_profileDirectory).filePath("folder-session.json"), |
| 1779 | *account, folders)) | ||
| 1780 | ✗ | setStatus(tr("Could not save local folder state")); | |
| 1781 | } | ||
| 1782 | 818 | } | |
| 1783 | // Runtime reconciliation must retain ephemeral consent for surviving tabs; | ||
| 1784 | // serializing/restoring a session deliberately discards that consent. | ||
| 1785 |
1/1✓ Branch 99 → 100 taken 826 times.
|
848 | m_workspace.retainMailIdentities([this](const MailId &id) { return acceptsIdentity(id); }); |
| 1786 | const auto mainIdentity = WorkspaceState::decode( | ||
| 1787 |
4/4✓ Branch 102 → 103 taken 826 times.
✓ Branch 103 → 104 taken 826 times.
✓ Branch 104 → 105 taken 826 times.
✓ Branch 105 → 106 taken 826 times.
|
826 | m_workspace.data(m_workspace.index(0), WorkspaceState::IdentityRole).toMap()); |
| 1788 |
1/1✓ Branch 109 → 110 taken 826 times.
|
826 | const auto mainPath = m_session.cache()->folderPath(mainIdentity.folderId); |
| 1789 | 826 | const auto previousPath = m_folderPath; | |
| 1790 |
3/3✓ Branch 111 → 112 taken 826 times.
✓ Branch 112 → 113 taken 801 times.
✓ Branch 112 → 120 taken 25 times.
|
826 | if (!m_folders.isSelectable(m_folderPath)) |
| 1791 |
4/4✓ Branch 113 → 114 taken 801 times.
✓ Branch 114 → 115 taken 4 times.
✓ Branch 114 → 116 taken 797 times.
✓ Branch 116 → 117 taken 797 times.
|
801 | m_folderPath = m_folders.isSelectable(mainPath) ? mainPath : m_folders.firstSelectable(); |
| 1792 |
2/2✓ Branch 121 → 122 taken 197 times.
✓ Branch 121 → 127 taken 629 times.
|
826 | if (m_folderPath.isEmpty()) { |
| 1793 |
1/1✓ Branch 123 → 124 taken 197 times.
|
197 | m_session.headers()->clear(); |
| 1794 |
1/1✓ Branch 125 → 126 taken 197 times.
|
197 | m_session.threads()->clear(); |
| 1795 |
1/1✓ Branch 126 → 150 taken 197 times.
|
197 | emit folderChanged(); |
| 1796 |
4/6✓ Branch 127 → 128 taken 597 times.
✓ Branch 127 → 130 taken 32 times.
✓ Branch 129 → 130 taken 597 times.
✗ Branch 129 → 131 not taken.
✓ Branch 132 → 133 taken 629 times.
✗ Branch 132 → 150 not taken.
|
629 | } else if (authoritative || m_folderPath != previousPath) { |
| 1797 |
5/6✓ Branch 134 → 135 taken 6 times.
✓ Branch 134 → 138 taken 623 times.
✓ Branch 136 → 137 taken 6 times.
✗ Branch 136 → 138 not taken.
✓ Branch 139 → 140 taken 6 times.
✓ Branch 139 → 142 taken 623 times.
|
629 | if (mainIdentity.isValid() && mainPath == m_folderPath) |
| 1798 |
1/1✓ Branch 141 → 142 taken 6 times.
|
6 | m_session.controller()->setRestoreAnchor(m_folderPath, mainIdentity.uid); |
| 1799 |
2/2✓ Branch 143 → 144 taken 604 times.
✓ Branch 143 → 149 taken 25 times.
|
629 | if (m_folderPath != previousPath) { |
| 1800 |
1/1✓ Branch 145 → 146 taken 604 times.
|
604 | m_mail.filter({}, false); |
| 1801 |
1/1✓ Branch 147 → 148 taken 604 times.
|
604 | m_threadList.clearFilters(); |
| 1802 |
1/1✓ Branch 148 → 149 taken 604 times.
|
604 | emit folderChanged(); |
| 1803 | } | ||
| 1804 |
1/1✓ Branch 149 → 150 taken 629 times.
|
629 | presentCachedFolder(m_folderPath); |
| 1805 | } | ||
| 1806 | 826 | } | |
| 1807 |
1/1✓ Branch 158 → 159 taken 826 times.
|
826 | reconcileSelection(); |
| 1808 |
1/1✓ Branch 159 → 160 taken 826 times.
|
826 | publishSelection(); |
| 1809 | 826 | } | |
| 1810 | |||
| 1811 | 6327 | QString QuickMailBackend::specialFolder(const QString &role) const { | |
| 1812 |
7/7✓ Branch 4 → 5 taken 1804 times.
✓ Branch 4 → 20 taken 4523 times.
✓ Branch 8 → 9 taken 1804 times.
✓ Branch 9 → 10 taken 1789 times.
✓ Branch 9 → 13 taken 15 times.
✓ Branch 15 → 16 taken 1789 times.
✓ Branch 15 → 17 taken 15 times.
|
9920 | if (role == QLatin1String("inbox")) return m_folders.isSelectable(QStringLiteral("INBOX")) ? QStringLiteral("INBOX") : QString(); |
| 1813 |
2/2✓ Branch 22 → 23 taken 1195 times.
✓ Branch 22 → 24 taken 3328 times.
|
4523 | if (role == QLatin1String("sent")) return m_sentFolder; |
| 1814 |
2/2✓ Branch 26 → 27 taken 1186 times.
✓ Branch 26 → 28 taken 2142 times.
|
3328 | if (role == QLatin1String("drafts")) return m_draftsFolder; |
| 1815 |
2/2✓ Branch 30 → 31 taken 1172 times.
✓ Branch 30 → 32 taken 970 times.
|
2142 | if (role == QLatin1String("archive")) return m_archiveFolder; |
| 1816 |
1/2✓ Branch 34 → 35 taken 970 times.
✗ Branch 34 → 36 not taken.
|
970 | if (role == QLatin1String("junk")) return m_junkFolder; |
| 1817 | ✗ | return {}; | |
| 1818 | } | ||
| 1819 | |||
| 1820 | 298 | bool QuickMailBackend::selectFolder(const QString &pathArgument) { | |
| 1821 | // Copy: callers may pass a backend member by reference (folder.previous | ||
| 1822 | // passes m_previousFolder), and the assignments below would otherwise | ||
| 1823 | // mutate the argument mid-function and re-select the previous folder. | ||
| 1824 | 298 | const QString path = pathArgument; | |
| 1825 |
7/7✓ Branch 4 → 5 taken 296 times.
✓ Branch 4 → 7 taken 2 times.
✓ Branch 5 → 6 taken 296 times.
✓ Branch 6 → 7 taken 7 times.
✓ Branch 6 → 8 taken 289 times.
✓ Branch 9 → 10 taken 9 times.
✓ Branch 9 → 11 taken 289 times.
|
298 | if (!configured() || !m_folders.isSelectable(path)) return false; |
| 1826 |
1/1✓ Branch 11 → 12 taken 289 times.
|
289 | cancelSearchPreview(); |
| 1827 | 289 | clearActionContinuity(); | |
| 1828 |
2/4✓ Branch 17 → 18 taken 289 times.
✗ Branch 17 → 22 not taken.
✓ Branch 19 → 20 taken 289 times.
✗ Branch 19 → 22 not taken.
|
867 | const bool flatFilterActive = !m_mail.filterText().isEmpty() || m_mail.showUnreadOnly() || |
| 1829 |
3/6✓ Branch 15 → 16 taken 289 times.
✗ Branch 15 → 22 not taken.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 289 times.
✓ Branch 24 → 25 taken 289 times.
✗ Branch 24 → 26 not taken.
|
867 | m_mail.showStarredOnly() || m_mail.showWithAttachments(); |
| 1830 |
1/2✓ Branch 30 → 31 taken 289 times.
✗ Branch 30 → 35 not taken.
|
578 | const bool threadFilterActive = !m_threadList.filterText().isEmpty() || |
| 1831 |
3/6✓ Branch 28 → 29 taken 289 times.
✗ Branch 28 → 35 not taken.
✓ Branch 32 → 33 taken 289 times.
✗ Branch 32 → 35 not taken.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 36 taken 289 times.
|
867 | m_threadList.showUnreadOnly() || m_threadList.showStarredOnly() || |
| 1832 |
1/2✓ Branch 37 → 38 taken 289 times.
✗ Branch 37 → 39 not taken.
|
578 | m_threadList.showWithAttachments(); |
| 1833 | // The settled main folder already owns its cached page and controller | ||
| 1834 | // generation. Clear local filters in place; re-entering the folder would | ||
| 1835 | // also clear the reader and start an unrelated cached-page read. | ||
| 1836 |
2/2✓ Branch 44 → 45 taken 194 times.
✓ Branch 44 → 61 taken 3 times.
|
197 | if (path == m_folderPath && |
| 1837 |
3/6✓ Branch 46 → 47 taken 194 times.
✗ Branch 46 → 61 not taken.
✓ Branch 66 → 67 taken 177 times.
✓ Branch 66 → 75 taken 112 times.
✗ Branch 113 → 114 not taken.
✗ Branch 113 → 115 not taken.
|
680 | m_workspace.area() == QLatin1String("mail") && |
| 1838 |
2/2✓ Branch 47 → 48 taken 181 times.
✓ Branch 47 → 61 taken 13 times.
|
388 | m_workspace.activeTab() == 0 && !m_waitingCachedPage && |
| 1839 |
3/4✓ Branch 48 → 49 taken 180 times.
✓ Branch 48 → 61 taken 1 time.
✓ Branch 49 → 50 taken 180 times.
✗ Branch 49 → 61 not taken.
|
181 | !m_threadProjectionPending && !m_threadProjectionFailed && |
| 1840 |
2/4✓ Branch 51 → 52 taken 180 times.
✗ Branch 51 → 61 not taken.
✓ Branch 55 → 56 taken 180 times.
✗ Branch 55 → 61 not taken.
|
360 | !m_selection.pending() && |
| 1841 |
6/8✓ Branch 40 → 41 taken 197 times.
✓ Branch 40 → 61 taken 92 times.
✓ Branch 62 → 63 taken 180 times.
✓ Branch 62 → 64 taken 109 times.
✓ Branch 64 → 65 taken 197 times.
✓ Branch 64 → 66 taken 92 times.
✗ Branch 109 → 110 not taken.
✗ Branch 109 → 111 not taken.
|
666 | m_session.controller()->currentFolder() == path && |
| 1842 |
5/7✓ Branch 56 → 57 taken 180 times.
✓ Branch 57 → 58 taken 3 times.
✓ Branch 57 → 60 taken 177 times.
✓ Branch 58 → 59 taken 3 times.
✗ Branch 58 → 60 not taken.
✗ Branch 59 → 60 not taken.
✓ Branch 59 → 61 taken 3 times.
|
180 | (activeRowCount() > 0 || flatFilterActive || threadFilterActive)) { |
| 1843 |
2/4✓ Branch 67 → 68 taken 177 times.
✗ Branch 67 → 69 not taken.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 74 taken 177 times.
|
177 | if (flatFilterActive || threadFilterActive) { |
| 1844 | ✗ | m_mail.filter({}, false); | |
| 1845 | ✗ | m_threadList.clearFilters(); | |
| 1846 | ✗ | reconcileSelection(); | |
| 1847 | } | ||
| 1848 | 177 | return true; | |
| 1849 | } | ||
| 1850 | { | ||
| 1851 | 112 | QScopedValueRollback guard(m_reconciling, true); | |
| 1852 |
2/2✓ Branch 77 → 78 taken 92 times.
✓ Branch 77 → 79 taken 20 times.
|
112 | if (path != m_folderPath) m_previousFolder = m_folderPath; |
| 1853 |
2/2✓ Branch 80 → 81 taken 92 times.
✓ Branch 80 → 83 taken 20 times.
|
112 | if (path != m_folderPath) |
| 1854 |
2/2✓ Branch 81 → 82 taken 92 times.
✓ Branch 82 → 83 taken 92 times.
|
92 | MailJD::Performance::ThreadSelectionPerformanceTracker::instance().folderSwitched(); |
| 1855 | 112 | m_folderPath = path; | |
| 1856 |
2/2✓ Branch 84 → 85 taken 106 times.
✓ Branch 84 → 88 taken 6 times.
|
112 | if (!m_folderSwitchInProgress) { |
| 1857 | // The list rebuilds only its visible delegates in this frame; the | ||
| 1858 | // cache buffer refills on the next event-loop turn. | ||
| 1859 | 106 | m_folderSwitchInProgress = true; | |
| 1860 |
1/1✓ Branch 85 → 86 taken 106 times.
|
106 | emit folderSwitchInProgressChanged(); |
| 1861 |
1/1✓ Branch 86 → 87 taken 106 times.
|
106 | QTimer::singleShot(0, this, [this] { |
| 1862 | 88 | m_folderSwitchInProgress = false; | |
| 1863 | 88 | emit folderSwitchInProgressChanged(); | |
| 1864 | 88 | }); | |
| 1865 | } | ||
| 1866 |
1/1✓ Branch 91 → 92 taken 112 times.
|
224 | m_workspace.setArea(QStringLiteral("mail")); |
| 1867 |
1/1✓ Branch 94 → 95 taken 112 times.
|
112 | m_workspace.activateTab(0); |
| 1868 |
1/1✓ Branch 95 → 96 taken 112 times.
|
112 | m_workspace.select({}); |
| 1869 |
1/1✓ Branch 98 → 99 taken 112 times.
|
112 | m_mail.filter({}, false); |
| 1870 |
1/1✓ Branch 100 → 101 taken 112 times.
|
112 | m_threadList.clearFilters(); |
| 1871 |
1/1✓ Branch 101 → 102 taken 112 times.
|
112 | emit folderChanged(); |
| 1872 |
1/1✓ Branch 102 → 103 taken 112 times.
|
112 | presentCachedFolder(path); |
| 1873 | 112 | } | |
| 1874 |
1/1✓ Branch 104 → 105 taken 112 times.
|
112 | reconcileSelection(); |
| 1875 | 112 | return true; | |
| 1876 | 298 | } | |
| 1877 | |||
| 1878 | 2 | bool QuickMailBackend::markFolderRead(const QString &path) { | |
| 1879 | // Folder context menu (F3.7): reuse the Widgets controller path | ||
| 1880 | // (FolderTree::markAllReadRequested -> MailController::markFolderAllSeen). | ||
| 1881 |
3/6✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 2 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 2 times.
|
2 | if (!configured() || !m_folders.isSelectable(path)) return false; |
| 1882 | 2 | m_session.controller()->markFolderAllSeen(path); | |
| 1883 | 2 | return true; | |
| 1884 | } | ||
| 1885 | |||
| 1886 | 768 | void QuickMailBackend::restoreWorkspaceSelection() { | |
| 1887 | 768 | const auto selected = m_workspace.mailId(); | |
| 1888 |
3/3✓ Branch 4 → 5 taken 768 times.
✓ Branch 5 → 6 taken 763 times.
✓ Branch 5 → 7 taken 5 times.
|
768 | if (!acceptsIdentity(selected)) return; |
| 1889 |
2/2✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 20 taken 1 time.
|
5 | if (m_workspace.activeTab() == 0) { |
| 1890 |
1/1✓ Branch 10 → 11 taken 4 times.
|
4 | const auto path = m_session.cache()->folderPath(selected.folderId); |
| 1891 |
2/3✓ Branch 11 → 12 taken 4 times.
✓ Branch 12 → 13 taken 4 times.
✗ Branch 12 → 18 not taken.
|
4 | if (m_folders.isSelectable(path)) { |
| 1892 | 4 | m_folderPath = path; | |
| 1893 |
1/1✓ Branch 14 → 15 taken 4 times.
|
4 | emit folderChanged(); |
| 1894 |
1/1✓ Branch 16 → 17 taken 4 times.
|
4 | m_session.controller()->setRestoreAnchor(path, selected.uid); |
| 1895 |
1/1✓ Branch 17 → 18 taken 4 times.
|
4 | presentCachedFolder(path); |
| 1896 | } | ||
| 1897 | 4 | } | |
| 1898 |
1/1✓ Branch 20 → 21 taken 5 times.
|
5 | requestSelectedMail(selected); |
| 1899 | 768 | } | |
| 1900 | |||
| 1901 | 45 | void QuickMailBackend::reserveActionContinuity(int lastRemovedVisualRow) { | |
| 1902 | 45 | m_actionAnchor = m_workspace.mailId(); | |
| 1903 | 45 | m_actionVisualRow = lastRemovedVisualRow; | |
| 1904 | 45 | m_actionSuccessor = MailId{}; | |
| 1905 |
6/12✓ Branch 7 → 8 taken 45 times.
✗ Branch 7 → 14 not taken.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 45 times.
✓ Branch 16 → 17 taken 45 times.
✗ Branch 16 → 18 not taken.
✓ Branch 18 → 19 taken 45 times.
✗ Branch 18 → 20 not taken.
✓ Branch 20 → 21 taken 45 times.
✗ Branch 20 → 22 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 26 taken 45 times.
|
90 | if (m_workspace.activeTab() != 0 || m_workspace.area() != QStringLiteral("mail")) { |
| 1906 | ✗ | m_actionAnchor = MailId{}; m_actionVisualRow = -1; | |
| 1907 | ✗ | return; | |
| 1908 | } | ||
| 1909 | 45 | const int count = activeRowCount(); | |
| 1910 |
4/8✓ Branch 27 → 28 taken 45 times.
✗ Branch 27 → 31 not taken.
✓ Branch 28 → 29 taken 45 times.
✗ Branch 28 → 31 not taken.
✗ Branch 30 → 31 not taken.
✓ Branch 30 → 32 taken 45 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 37 taken 45 times.
|
45 | if (count <= 0 || lastRemovedVisualRow < 0 || !m_actionAnchor.isValid()) { |
| 1911 | ✗ | m_actionAnchor = MailId{}; m_actionVisualRow = -1; | |
| 1912 | ✗ | return; | |
| 1913 | } | ||
| 1914 | // Prefer the mail that takes over the freed visual slot; at the list end | ||
| 1915 | // fall back to the predecessor. Identity wins over the row number later. | ||
| 1916 | 45 | const int target = lastRemovedVisualRow + 1 < count | |
| 1917 |
2/2✓ Branch 37 → 38 taken 43 times.
✓ Branch 37 → 39 taken 2 times.
|
45 | ? lastRemovedVisualRow + 1 : qMax(0, lastRemovedVisualRow - 1); |
| 1918 |
5/5✓ Branch 41 → 42 taken 45 times.
✓ Branch 43 → 44 taken 45 times.
✓ Branch 44 → 45 taken 45 times.
✓ Branch 45 → 46 taken 45 times.
✓ Branch 46 → 47 taken 45 times.
|
45 | m_actionSuccessor = WorkspaceState::decode(activeMailAt(target).value("identity").toMap()); |
| 1919 | } | ||
| 1920 | |||
| 1921 | 2713 | void QuickMailBackend::clearActionContinuity() { | |
| 1922 | 2713 | m_actionAnchor = MailId{}; | |
| 1923 | 2713 | m_actionSuccessor = MailId{}; | |
| 1924 | 2713 | m_actionVisualRow = -1; | |
| 1925 | 2713 | } | |
| 1926 | |||
| 1927 | 3400 | void QuickMailBackend::reconcileSelection() { | |
| 1928 | // An action that removed the selected mail reserved its successor (P90.16). | ||
| 1929 | // The exclusion refresh it starts is a background page load with the rows | ||
| 1930 | // still mounted; the successor is taken from them at once instead of after | ||
| 1931 | // that page (which takes long in huge folders: 0.76 s at 1M mails). | ||
| 1932 |
3/4✓ Branch 2 → 3 taken 49 times.
✓ Branch 2 → 6 taken 3351 times.
✓ Branch 4 → 5 taken 49 times.
✗ Branch 4 → 6 not taken.
|
3400 | const bool actionWindow = m_actionVisualRow >= 0 && m_actionAnchor.isValid(); |
| 1933 |
1/2✓ Branch 10 → 11 taken 71 times.
✗ Branch 10 → 18 not taken.
|
3471 | const bool rowsMountedDuringRefresh = m_threadList.indexed() && threadedPresentation() && |
| 1934 |
5/6✓ Branch 8 → 9 taken 71 times.
✓ Branch 8 → 18 taken 3329 times.
✓ Branch 12 → 13 taken 48 times.
✓ Branch 12 → 18 taken 23 times.
✓ Branch 14 → 15 taken 48 times.
✗ Branch 14 → 18 not taken.
|
3519 | !m_threadList.projectionBlocking() && !m_threadList.holdingPreviousFolder() && |
| 1935 |
2/3✓ Branch 15 → 16 taken 48 times.
✓ Branch 16 → 17 taken 48 times.
✗ Branch 16 → 18 not taken.
|
48 | activeRowCount() > 0; |
| 1936 |
2/2✓ Branch 20 → 21 taken 2064 times.
✓ Branch 20 → 34 taken 1333 times.
|
3397 | if (m_reconciling || m_waitingCachedPage || |
| 1937 |
7/8✓ Branch 21 → 22 taken 17 times.
✓ Branch 21 → 24 taken 2047 times.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 22 → 34 taken 15 times.
✓ Branch 23 → 24 taken 2 times.
✗ Branch 23 → 34 not taken.
✓ Branch 25 → 26 taken 1932 times.
✓ Branch 25 → 34 taken 117 times.
|
4113 | (m_threadProjectionPending && !(actionWindow && rowsMountedDuringRefresh)) || |
| 1938 |
12/12✓ Branch 19 → 20 taken 3397 times.
✓ Branch 19 → 34 taken 3 times.
✓ Branch 31 → 32 taken 1640 times.
✓ Branch 31 → 34 taken 292 times.
✓ Branch 36 → 37 taken 1932 times.
✓ Branch 36 → 38 taken 1468 times.
✓ Branch 38 → 39 taken 1932 times.
✓ Branch 38 → 40 taken 1468 times.
✓ Branch 40 → 41 taken 1932 times.
✓ Branch 40 → 42 taken 1468 times.
✓ Branch 42 → 43 taken 1769 times.
✓ Branch 42 → 63 taken 1631 times.
|
10486 | m_workspace.activeTab() != 0 || m_workspace.area() != QStringLiteral("mail") || |
| 1939 |
2/2✓ Branch 33 → 34 taken 9 times.
✓ Branch 33 → 35 taken 1631 times.
|
1640 | !configured()) { |
| 1940 |
7/7✓ Branch 43 → 44 taken 1769 times.
✓ Branch 47 → 48 taken 1769 times.
✓ Branch 48 → 49 taken 1769 times.
✓ Branch 49 → 50 taken 1769 times.
✓ Branch 50 → 51 taken 1769 times.
✓ Branch 61 → 45 taken 1769 times.
✓ Branch 61 → 62 taken 1769 times.
|
3538 | qCDebug(lcQuickBackend) << "reconcile deferred: reconciling" << m_reconciling << "waitingCachedPage" |
| 1941 |
3/3✓ Branch 51 → 52 taken 1769 times.
✓ Branch 52 → 53 taken 1769 times.
✓ Branch 53 → 54 taken 1769 times.
|
1769 | << m_waitingCachedPage << "projectionPending" << m_threadProjectionPending |
| 1942 |
4/4✓ Branch 54 → 55 taken 1769 times.
✓ Branch 55 → 56 taken 1769 times.
✓ Branch 56 → 57 taken 1769 times.
✓ Branch 57 → 58 taken 1769 times.
|
1769 | << "actionWindow" << actionWindow << "mounted" << rowsMountedDuringRefresh; |
| 1943 | 1780 | return; | |
| 1944 | } | ||
| 1945 | 1631 | const auto selected = m_workspace.mailId(); | |
| 1946 | // P90.16: a reserved action token whose anchor is still selected marks | ||
| 1947 | // exactly the removal window: the selection points at a mail that was just | ||
| 1948 | // moved away, so "same indexed folder" must not keep it authoritative. | ||
| 1949 |
3/4✓ Branch 65 → 66 taken 49 times.
✓ Branch 65 → 73 taken 1582 times.
✓ Branch 67 → 68 taken 49 times.
✗ Branch 67 → 73 not taken.
|
1680 | const bool actionRemovedSelection = m_actionVisualRow >= 0 && m_actionAnchor.isValid() && |
| 1950 |
3/4✓ Branch 69 → 70 taken 49 times.
✗ Branch 69 → 72 not taken.
✓ Branch 71 → 72 taken 13 times.
✓ Branch 71 → 73 taken 36 times.
|
49 | (!selected.isValid() || selected == m_actionAnchor); |
| 1951 | // The flat filter is a page read (MailController::setFlatFilter): its page | ||
| 1952 | // holds only the matching rows, so the selected mail of the open folder is | ||
| 1953 | // absent from the header model without being deleted. | ||
| 1954 |
3/4✓ Branch 76 → 77 taken 52 times.
✓ Branch 76 → 81 taken 1579 times.
✓ Branch 79 → 80 taken 52 times.
✗ Branch 79 → 81 not taken.
|
1683 | const bool hiddenByFlatFilter = m_session.controller()->flatFilterActive() && |
| 1955 | 52 | selected.folderId == m_session.controller()->currentFolderId(); | |
| 1956 |
5/5✓ Branch 82 → 83 taken 1618 times.
✓ Branch 82 → 97 taken 13 times.
✓ Branch 83 → 84 taken 1618 times.
✓ Branch 84 → 85 taken 521 times.
✓ Branch 84 → 97 taken 1097 times.
|
2152 | if (!actionRemovedSelection && acceptsIdentity(selected) && |
| 1957 |
8/10✓ Branch 85 → 86 taken 521 times.
✓ Branch 86 → 87 taken 521 times.
✓ Branch 87 → 88 taken 48 times.
✓ Branch 87 → 96 taken 473 times.
✓ Branch 98 → 99 taken 521 times.
✓ Branch 98 → 100 taken 1110 times.
✓ Branch 100 → 101 taken 517 times.
✓ Branch 100 → 103 taken 1114 times.
✗ Branch 174 → 175 not taken.
✗ Branch 174 → 176 not taken.
|
2152 | (activeRowForIdentity(WorkspaceState::encode(selected)) >= 0 || |
| 1958 |
6/7✓ Branch 89 → 90 taken 48 times.
✓ Branch 90 → 91 taken 43 times.
✓ Branch 90 → 96 taken 5 times.
✓ Branch 91 → 92 taken 4 times.
✓ Branch 91 → 96 taken 39 times.
✗ Branch 93 → 94 not taken.
✓ Branch 93 → 97 taken 4 times.
|
52 | m_session.headers()->rowForUid(selected.uid, selected.folderId) >= 0 || hiddenByFlatFilter || |
| 1959 |
0/2✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
|
4 | (m_threadList.indexed() && selected.folderId == m_threadList.indexedFolder()))) { |
| 1960 | // A queued source reset / first-page completion may arrive after a user | ||
| 1961 | // filters the proxy. Hidden is not deleted: keep the current reader and | ||
| 1962 | // never implicitly select/read the first surviving filtered result. | ||
| 1963 |
1/1✓ Branch 101 → 102 taken 517 times.
|
517 | requestSelectedMail(selected); |
| 1964 | // Continuity is already established by a valid selection; a stale token | ||
| 1965 | // from an older action must never redirect a later reconcile. | ||
| 1966 | 517 | clearActionContinuity(); | |
| 1967 |
3/3✓ Branch 103 → 104 taken 1114 times.
✓ Branch 104 → 105 taken 596 times.
✓ Branch 104 → 158 taken 518 times.
|
1114 | } else if (activeRowCount() > 0) { |
| 1968 | // P90.16: an action-removal token reserves the surviving neighbour. It | ||
| 1969 | // applies only while the workspace still points at the removed mail (or | ||
| 1970 | // was cleared by exactly that removal); row 0 stays a last resort for | ||
| 1971 | // genuinely anchor-less cases (fresh folder, filter, search handoff). | ||
| 1972 |
2/4✓ Branch 107 → 108 taken 11 times.
✗ Branch 107 → 116 not taken.
✓ Branch 110 → 111 taken 11 times.
✗ Branch 110 → 116 not taken.
|
22 | if (m_actionVisualRow >= 0 && m_actionAnchor.isValid() && |
| 1973 |
6/6✓ Branch 105 → 106 taken 11 times.
✓ Branch 105 → 116 taken 585 times.
✓ Branch 117 → 118 taken 11 times.
✓ Branch 117 → 119 taken 585 times.
✓ Branch 119 → 120 taken 11 times.
✓ Branch 119 → 145 taken 585 times.
|
618 | m_actionAnchor.accountId == accountName() && |
| 1974 |
2/4✓ Branch 112 → 113 taken 11 times.
✗ Branch 112 → 115 not taken.
✓ Branch 114 → 115 taken 11 times.
✗ Branch 114 → 116 not taken.
|
11 | (!selected.isValid() || selected == m_actionAnchor)) { |
| 1975 | 11 | int row = m_actionSuccessor.isValid() | |
| 1976 |
4/8✓ Branch 121 → 122 taken 11 times.
✗ Branch 121 → 125 not taken.
✓ Branch 122 → 123 taken 11 times.
✓ Branch 123 → 124 taken 11 times.
✓ Branch 126 → 127 taken 11 times.
✗ Branch 126 → 128 not taken.
✗ Branch 178 → 179 not taken.
✗ Branch 178 → 180 not taken.
|
11 | ? activeRowForIdentity(WorkspaceState::encode(m_actionSuccessor)) : -1; |
| 1977 |
1/4✗ Branch 128 → 129 not taken.
✓ Branch 128 → 132 taken 11 times.
✗ Branch 129 → 130 not taken.
✗ Branch 130 → 131 not taken.
|
11 | if (row < 0) row = qBound(0, m_actionVisualRow, activeRowCount() - 1); |
| 1978 | 11 | clearActionContinuity(); | |
| 1979 |
5/5✓ Branch 133 → 134 taken 11 times.
✓ Branch 135 → 136 taken 11 times.
✓ Branch 136 → 137 taken 11 times.
✓ Branch 137 → 138 taken 11 times.
✓ Branch 138 → 139 taken 11 times.
|
11 | m_workspace.selectIdentity(activeMailAt(row).value("identity").toMap()); |
| 1980 | 11 | return; | |
| 1981 | } | ||
| 1982 | 585 | clearActionContinuity(); | |
| 1983 |
5/5✓ Branch 146 → 147 taken 585 times.
✓ Branch 148 → 149 taken 585 times.
✓ Branch 149 → 150 taken 585 times.
✓ Branch 150 → 151 taken 585 times.
✓ Branch 151 → 152 taken 585 times.
|
585 | m_workspace.selectIdentity(activeMailAt(0).value("identity").toMap()); |
| 1984 | } else { | ||
| 1985 | 518 | clearActionContinuity(); | |
| 1986 |
1/1✓ Branch 159 → 160 taken 518 times.
|
518 | m_workspace.select({}); |
| 1987 | } | ||
| 1988 | 1631 | } | |
| 1989 | |||
| 1990 | 745 | void QuickMailBackend::presentCachedFolder(const QString &path) { | |
| 1991 | // A queued model-reset callback can run before the foreground first page. | ||
| 1992 | // An empty transient model is not evidence that a restored mail disappeared. | ||
| 1993 |
1/1✓ Branch 2 → 3 taken 745 times.
|
745 | m_statusState.reset(); |
| 1994 |
1/1✓ Branch 3 → 4 taken 745 times.
|
745 | m_headerProjectionTimer.stop(); |
| 1995 | 745 | m_headerProjectionFolder.clear(); | |
| 1996 | 745 | m_threadProjectionFailed = false; | |
| 1997 | 745 | m_waitingCachedPage = true; | |
| 1998 |
6/6✓ Branch 5 → 6 taken 745 times.
✓ Branch 9 → 10 taken 745 times.
✓ Branch 10 → 11 taken 745 times.
✓ Branch 11 → 12 taken 745 times.
✓ Branch 15 → 7 taken 745 times.
✓ Branch 15 → 16 taken 745 times.
|
1490 | qCDebug(lcQuickBackend) << "waiting for cached page of" << path; |
| 1999 |
1/1✓ Branch 16 → 17 taken 745 times.
|
745 | cancelThreadProjection(); |
| 2000 | 745 | m_threadProjectionPending = m_threaded; | |
| 2001 |
2/2✓ Branch 17 → 18 taken 14 times.
✓ Branch 17 → 21 taken 731 times.
|
745 | if (m_threadProjectionPending) { |
| 2002 | // Keep the previous folder's rows until this folder has content (no | ||
| 2003 | // empty list frame); a slow folder drops them for its loading state. | ||
| 2004 |
1/1✓ Branch 18 → 19 taken 14 times.
|
14 | m_threadList.holdRowsForFolderSwitch(); |
| 2005 | 14 | const auto hold = ++m_folderHoldGeneration; | |
| 2006 |
1/1✓ Branch 19 → 20 taken 14 times.
|
14 | QTimer::singleShot(m_threadProjectionSlowThresholdMs, this, [this, hold] { |
| 2007 |
2/2✓ Branch 2 → 3 taken 4 times.
✓ Branch 2 → 4 taken 2 times.
|
6 | if (hold == m_folderHoldGeneration) m_threadList.releaseHeldRows(); |
| 2008 | 6 | }); | |
| 2009 | } | ||
| 2010 |
1/1✓ Branch 21 → 22 taken 745 times.
|
745 | const qint64 targetFolder = folderIdForPath(path); |
| 2011 |
1/2✓ Branch 23 → 24 taken 14 times.
✗ Branch 23 → 27 not taken.
|
14 | const auto state = m_threadProjectionPending && targetFolder >= 0 |
| 2012 |
3/3✓ Branch 22 → 23 taken 14 times.
✓ Branch 22 → 27 taken 731 times.
✓ Branch 25 → 26 taken 14 times.
|
759 | ? m_session.cache()->folderPresentationState(targetFolder) |
| 2013 | 745 | : MailCache::FolderPresentationState{}; | |
| 2014 |
3/4✓ Branch 28 → 29 taken 14 times.
✓ Branch 28 → 32 taken 731 times.
✓ Branch 29 → 30 taken 14 times.
✗ Branch 29 → 32 not taken.
|
759 | const bool indexedFolder = m_threadProjectionPending && targetFolder >= 0 && |
| 2015 |
2/2✓ Branch 30 → 31 taken 8 times.
✓ Branch 30 → 32 taken 6 times.
|
14 | state.headerCount > 200; |
| 2016 |
2/2✓ Branch 33 → 34 taken 8 times.
✓ Branch 33 → 35 taken 737 times.
|
745 | const qint64 targetRevision = indexedFolder ? state.threadRevision : -1; |
| 2017 |
2/2✓ Branch 36 → 37 taken 8 times.
✓ Branch 36 → 38 taken 737 times.
|
745 | const quint32 targetEpoch = indexedFolder ? state.uidValidity : 0; |
| 2018 | 745 | m_session.controller()->setStreamedHeadersVisible(!indexedFolder); | |
| 2019 |
2/2✓ Branch 41 → 42 taken 8 times.
✓ Branch 41 → 67 taken 737 times.
|
745 | if (indexedFolder) { |
| 2020 | 8 | const auto selected = m_workspace.mailId(); | |
| 2021 |
2/3✗ Branch 50 → 51 not taken.
✓ Branch 50 → 53 taken 8 times.
✓ Branch 56 → 57 taken 8 times.
|
8 | m_threadList.configureIndexedFolder(m_session.cache()->databasePath(), targetFolder, |
| 2022 | targetEpoch, targetRevision, | ||
| 2023 |
1/6✗ Branch 51 → 52 not taken.
✗ Branch 51 → 53 not taken.
✓ Branch 58 → 59 taken 8 times.
✗ Branch 58 → 60 not taken.
✗ Branch 126 → 127 not taken.
✗ Branch 126 → 128 not taken.
|
16 | selected.accountId == accountName() && selected.folderId == targetFolder ? selected.uid : 0, |
| 2024 |
1/1✓ Branch 47 → 48 taken 8 times.
|
16 | m_session.cache()->journal().pendingMoveUids(accountName(), targetFolder)); |
| 2025 | 8 | m_threadProjectionPending = m_threadList.projectionPending(); | |
| 2026 |
1/1✓ Branch 64 → 65 taken 8 times.
|
8 | m_session.threads()->clear(); |
| 2027 | 8 | } else { | |
| 2028 |
1/1✓ Branch 67 → 68 taken 737 times.
|
737 | m_threadList.leaveIndexedFolder(); |
| 2029 |
8/8✓ Branch 68 → 69 taken 6 times.
✓ Branch 68 → 71 taken 731 times.
✓ Branch 69 → 70 taken 6 times.
✓ Branch 70 → 71 taken 4 times.
✓ Branch 70 → 72 taken 2 times.
✓ Branch 73 → 74 taken 735 times.
✓ Branch 73 → 76 taken 2 times.
✓ Branch 75 → 76 taken 735 times.
|
737 | if (!m_threadProjectionPending || !restoreThreadSnapshot(targetFolder)) m_session.threads()->clear(); |
| 2030 | } | ||
| 2031 |
1/1✓ Branch 76 → 77 taken 745 times.
|
745 | emit threadedChanged(); |
| 2032 | // A revision-current published indexed page already owns the visible | ||
| 2033 | // headers. The controller needs only identity and folder totals to start | ||
| 2034 | // IMAP synchronization; its hidden flat page can load on demand later. | ||
| 2035 |
2/4✓ Branch 79 → 80 taken 8 times.
✗ Branch 79 → 90 not taken.
✓ Branch 81 → 82 taken 8 times.
✗ Branch 81 → 90 not taken.
|
16 | const bool indexedPageReady = indexedFolder && m_threadList.indexed() && |
| 2036 | 8 | m_threadList.indexedFolder() == targetFolder && | |
| 2037 |
7/9✓ Branch 77 → 78 taken 8 times.
✓ Branch 77 → 90 taken 737 times.
✓ Branch 83 → 84 taken 3 times.
✓ Branch 83 → 90 taken 5 times.
✓ Branch 85 → 86 taken 3 times.
✓ Branch 86 → 87 taken 3 times.
✗ Branch 86 → 90 not taken.
✓ Branch 88 → 89 taken 3 times.
✗ Branch 88 → 90 not taken.
|
756 | !m_threadList.projectionPending() && m_threadList.rowCount() > 0 && |
| 2038 | 3 | m_threadList.indexRevision() == targetRevision; | |
| 2039 | 745 | std::optional<MailController::PublishedFolderSummary> published; | |
| 2040 | 748 | if (indexedPageReady && | |
| 2041 |
3/5✓ Branch 94 → 95 taken 3 times.
✓ Branch 95 → 96 taken 3 times.
✗ Branch 95 → 102 not taken.
✓ Branch 97 → 98 taken 3 times.
✗ Branch 97 → 102 not taken.
|
6 | m_threadList.indexedTotalCount() >= m_threadList.rowCount() && |
| 2042 |
5/6✓ Branch 91 → 92 taken 3 times.
✓ Branch 91 → 102 taken 742 times.
✓ Branch 100 → 101 taken 3 times.
✗ Branch 100 → 102 not taken.
✓ Branch 103 → 104 taken 3 times.
✓ Branch 103 → 109 taken 742 times.
|
751 | m_threadList.indexedUnreadCount() >= 0 && |
| 2043 | 3 | m_threadList.indexedUnreadCount() <= m_threadList.indexedTotalCount()) { | |
| 2044 | 3 | published = MailController::PublishedFolderSummary{ | |
| 2045 | targetFolder, targetEpoch, | ||
| 2046 | 3 | m_threadList.indexRevision(), m_threadList.indexedTotalCount(), | |
| 2047 | 3 | m_threadList.indexedUnreadCount()}; | |
| 2048 | } | ||
| 2049 |
1/1✓ Branch 110 → 111 taken 745 times.
|
745 | m_session.controller()->onFolderSelected(path, indexedPageReady, published, targetFolder); |
| 2050 |
1/2✗ Branch 113 → 114 not taken.
✓ Branch 113 → 116 taken 745 times.
|
745 | if (m_session.controller()->currentFolderId() < 0) { |
| 2051 | ✗ | m_waitingCachedPage = false; | |
| 2052 | ✗ | m_threadProjectionPending = false; | |
| 2053 | ✗ | clearThreadProjectionSlowFlag(); | |
| 2054 | ✗ | emit threadedChanged(); | |
| 2055 | } | ||
| 2056 | 745 | } | |
| 2057 | |||
| 2058 | 4 | QStringList QuickMailBackend::matchingFolders(const QString &prefix) const { | |
| 2059 | 4 | return m_folders.matchingPaths(prefix); | |
| 2060 | } | ||
| 2061 | |||
| 2062 | 57 | bool QuickMailBackend::moveSelectedMail(const QString &path) { | |
| 2063 |
5/6✓ Branch 2 → 3 taken 55 times.
✓ Branch 2 → 6 taken 2 times.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 55 times.
✓ Branch 8 → 9 taken 2 times.
✓ Branch 8 → 10 taken 55 times.
|
57 | if (m_actionPending || m_session.undo()->busy()) return false; |
| 2064 |
4/4✓ Branch 10 → 11 taken 55 times.
✓ Branch 11 → 12 taken 55 times.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 12 → 100 taken 53 times.
|
55 | if (const auto groups = searchBulkGroups(); !groups.isEmpty()) { |
| 2065 | // S101.6.1: the marked results leave per source folder (one Undo each). | ||
| 2066 | // S106-293: every group is admitted before anything happens; when none | ||
| 2067 | // can move, the marks stay and nothing is announced. | ||
| 2068 | 2 | QList<std::pair<qint64, QString>> admitted; | |
| 2069 |
4/4✓ Branch 13 → 14 taken 2 times.
✓ Branch 34 → 35 taken 4 times.
✓ Branch 36 → 15 taken 2 times.
✓ Branch 36 → 37 taken 2 times.
|
4 | for (auto it = groups.cbegin(); it != groups.cend(); ++it) { |
| 2070 |
1/1✓ Branch 17 → 18 taken 2 times.
|
2 | const QString source = m_session.cache()->folderPath(it.key()); |
| 2071 |
8/10✓ Branch 19 → 20 taken 2 times.
✗ Branch 19 → 25 not taken.
✓ Branch 21 → 22 taken 2 times.
✗ Branch 21 → 25 not taken.
✓ Branch 22 → 23 taken 2 times.
✓ Branch 23 → 24 taken 1 time.
✓ Branch 23 → 25 taken 1 time.
✓ Branch 26 → 27 taken 1 time.
✓ Branch 26 → 32 taken 1 time.
✓ Branch 29 → 30 taken 1 time.
|
2 | if (!source.isEmpty() && source != path && m_folders.isSelectable(path)) admitted.append({it.key(), source}); |
| 2072 | 2 | } | |
| 2073 |
2/2✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 53 taken 1 time.
|
2 | if (admitted.isEmpty()) { |
| 2074 |
5/5✓ Branch 39 → 40 taken 1 time.
✓ Branch 43 → 44 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 48 → 41 taken 1 time.
✓ Branch 48 → 49 taken 1 time.
|
2 | qCInfo(lcQuickBackend) << "Search bulk move refused: no source folder can move to the target"; |
| 2075 |
2/2✓ Branch 49 → 50 taken 1 time.
✓ Branch 50 → 51 taken 1 time.
|
1 | setStatus(tr("The marked messages cannot be moved there.")); |
| 2076 | 1 | return false; | |
| 2077 | } | ||
| 2078 | 1 | int moved = 0; | |
| 2079 |
2/2✓ Branch 73 → 56 taken 1 time.
✓ Branch 73 → 74 taken 1 time.
|
2 | for (const auto &[folderId, source] : std::as_const(admitted)) { |
| 2080 |
2/2✓ Branch 62 → 63 taken 1 time.
✓ Branch 63 → 64 taken 1 time.
|
1 | m_session.controller()->moves()->moveMailsToFolderFrom(groups.value(folderId), folderId, source, path); |
| 2081 |
1/1✓ Branch 67 → 68 taken 1 time.
|
1 | moved += int(groups.value(folderId).size()); |
| 2082 | } | ||
| 2083 |
3/4✓ Branch 75 → 76 taken 1 time.
✓ Branch 76 → 77 taken 1 time.
✗ Branch 76 → 78 not taken.
✓ Branch 77 → 78 taken 1 time.
|
1 | if (MailSearchSession *search = m_searchTabs->active()) search->clearMarks(); |
| 2084 |
1/1✓ Branch 78 → 79 taken 1 time.
|
1 | emit selectionChanged(); |
| 2085 |
10/10✓ Branch 79 → 80 taken 1 time.
✓ Branch 83 → 84 taken 1 time.
✓ Branch 84 → 85 taken 1 time.
✓ Branch 85 → 86 taken 1 time.
✓ Branch 86 → 87 taken 1 time.
✓ Branch 87 → 88 taken 1 time.
✓ Branch 88 → 89 taken 1 time.
✓ Branch 89 → 90 taken 1 time.
✓ Branch 93 → 81 taken 1 time.
✓ Branch 93 → 94 taken 1 time.
|
2 | qCInfo(lcQuickBackend) << "Search bulk move:" << moved << "messages from" << groups.size() << "folders"; |
| 2086 |
2/2✓ Branch 94 → 95 taken 1 time.
✓ Branch 95 → 96 taken 1 time.
|
1 | setStatus(tr("Moving %n message(s)…", nullptr, moved)); |
| 2087 | 1 | return moved > 0; | |
| 2088 | 57 | } | |
| 2089 |
4/6✓ Branch 105 → 107 taken 51 times.
✓ Branch 105 → 110 taken 2 times.
✗ Branch 108 → 109 not taken.
✓ Branch 108 → 110 taken 51 times.
✗ Branch 111 → 112 not taken.
✓ Branch 111 → 116 taken 53 times.
|
53 | if (m_workspace.activeTab() == 0 && m_selection.pending()) { |
| 2090 | ✗ | setStatus(tr("Wait for the selection to finish.")); | |
| 2091 | ✗ | return false; | |
| 2092 | } | ||
| 2093 |
8/8✓ Branch 117 → 118 taken 51 times.
✓ Branch 117 → 122 taken 2 times.
✓ Branch 120 → 121 taken 16 times.
✓ Branch 120 → 122 taken 35 times.
✓ Branch 123 → 124 taken 51 times.
✓ Branch 123 → 125 taken 2 times.
✓ Branch 125 → 126 taken 16 times.
✓ Branch 125 → 305 taken 37 times.
|
53 | if (m_workspace.activeTab() == 0 && m_selection.snapshot()) { |
| 2094 | 16 | const auto selection = m_selection.snapshot(); | |
| 2095 |
7/7✓ Branch 127 → 128 taken 16 times.
✓ Branch 131 → 132 taken 16 times.
✓ Branch 132 → 133 taken 16 times.
✓ Branch 134 → 135 taken 16 times.
✓ Branch 135 → 136 taken 16 times.
✓ Branch 143 → 129 taken 16 times.
✓ Branch 143 → 144 taken 16 times.
|
32 | qCDebug(lcQuickBackend) << "move via selection snapshot: count" << selection->count << "folder" |
| 2096 |
2/2✓ Branch 137 → 138 taken 16 times.
✓ Branch 139 → 140 taken 16 times.
|
16 | << selection->folderId << selection->folderPath; |
| 2097 |
4/9✓ Branch 148 → 149 taken 16 times.
✗ Branch 148 → 163 not taken.
✓ Branch 149 → 150 taken 16 times.
✓ Branch 150 → 151 taken 16 times.
✗ Branch 150 → 163 not taken.
✗ Branch 169 → 170 not taken.
✓ Branch 169 → 171 taken 16 times.
✗ Branch 420 → 421 not taken.
✗ Branch 420 → 422 not taken.
|
32 | if (m_workspace.area() != "mail" || !configured() || !m_folders.isSelectable(path) || |
| 2098 |
4/10✓ Branch 154 → 155 taken 16 times.
✗ Branch 154 → 163 not taken.
✓ Branch 157 → 158 taken 16 times.
✗ Branch 157 → 163 not taken.
✓ Branch 160 → 161 taken 16 times.
✗ Branch 160 → 163 not taken.
✓ Branch 167 → 168 taken 16 times.
✗ Branch 167 → 169 not taken.
✗ Branch 416 → 417 not taken.
✗ Branch 416 → 418 not taken.
|
48 | selection->accountId != accountName() || selection->folderPath != m_folderPath || |
| 2099 |
3/6✓ Branch 146 → 147 taken 16 times.
✗ Branch 146 → 163 not taken.
✗ Branch 162 → 163 not taken.
✓ Branch 162 → 164 taken 16 times.
✓ Branch 165 → 166 taken 16 times.
✗ Branch 165 → 167 not taken.
|
48 | path == selection->folderPath || selection->count <= 0) return false; |
| 2100 | 16 | const auto generation = m_session.accounts()->generation(); | |
| 2101 | 16 | const auto database = m_session.cache()->databasePath(); | |
| 2102 |
1/1✓ Branch 175 → 176 taken 16 times.
|
16 | const QPointer<QuickMailBackend> owner(this); |
| 2103 | 16 | m_actionPending = true; | |
| 2104 |
1/1✓ Branch 176 → 177 taken 16 times.
|
16 | emit actionChanged(); |
| 2105 |
1/2✗ Branch 178 → 179 not taken.
✓ Branch 178 → 180 taken 16 times.
|
16 | if (!owner) return false; |
| 2106 | // P90.16: capture the visible selected range BEFORE queueing — the | ||
| 2107 | // successor is the first surviving row after the last selected visible | ||
| 2108 | // row (identity wins over row numbers later). The uids drive the | ||
| 2109 | // optimistic removal once the journal accepted the move. | ||
| 2110 | 16 | QList<qint64> visibleSelectedUids; | |
| 2111 | 16 | int lastSelectedRow = -1; | |
| 2112 |
1/1✓ Branch 180 → 181 taken 16 times.
|
16 | const int rows = activeRowCount(); |
| 2113 |
2/2✓ Branch 194 → 182 taken 2312 times.
✓ Branch 194 → 195 taken 16 times.
|
2328 | for (int row = 0; row < rows; ++row) { |
| 2114 |
1/1✓ Branch 182 → 183 taken 2312 times.
|
2312 | const auto key = activeKeyAt(row); |
| 2115 |
5/5✓ Branch 183 → 184 taken 2312 times.
✓ Branch 184 → 185 taken 237 times.
✓ Branch 184 → 188 taken 2075 times.
✓ Branch 189 → 190 taken 237 times.
✓ Branch 189 → 192 taken 2075 times.
|
2549 | if (m_selection.contains(key.uid) && |
| 2116 |
1/2✓ Branch 186 → 187 taken 237 times.
✗ Branch 186 → 188 not taken.
|
237 | key.folderId == selection->folderId) { |
| 2117 |
1/1✓ Branch 190 → 191 taken 237 times.
|
237 | visibleSelectedUids.append(key.uid); |
| 2118 | 237 | lastSelectedRow = row; | |
| 2119 | } | ||
| 2120 | 2312 | } | |
| 2121 | // No selected row is known in the viewport (the list has not followed | ||
| 2122 | // the selection yet, or it was scrolled away): the current mail — the | ||
| 2123 | // row the range was extended to — stands for the end of the range. | ||
| 2124 |
2/2✓ Branch 195 → 196 taken 3 times.
✓ Branch 195 → 211 taken 13 times.
|
16 | if (lastSelectedRow < 0) { |
| 2125 | 3 | const MailId current = m_workspace.mailId(); | |
| 2126 |
3/6✓ Branch 199 → 200 taken 3 times.
✗ Branch 199 → 203 not taken.
✓ Branch 201 → 202 taken 3 times.
✗ Branch 201 → 203 not taken.
✓ Branch 204 → 205 taken 3 times.
✗ Branch 204 → 209 not taken.
|
3 | if (current.isValid() && current.folderId == selection->folderId) |
| 2127 |
2/2✓ Branch 205 → 206 taken 3 times.
✓ Branch 206 → 207 taken 3 times.
|
3 | lastSelectedRow = activeRowForIdentity(WorkspaceState::encode(current)); |
| 2128 | 3 | } | |
| 2129 | { | ||
| 2130 | 16 | MailId anchor = m_workspace.mailId(); | |
| 2131 | 16 | MailId successor; | |
| 2132 | 16 | int visualRow = lastSelectedRow; | |
| 2133 |
4/4✓ Branch 213 → 214 taken 14 times.
✓ Branch 213 → 229 taken 2 times.
✓ Branch 214 → 215 taken 8 times.
✓ Branch 214 → 229 taken 6 times.
|
16 | if (lastSelectedRow >= 0 && lastSelectedRow + 1 < rows) |
| 2134 |
1/1✓ Branch 220 → 221 taken 8 times.
|
16 | successor = WorkspaceState::decode( |
| 2135 |
4/4✓ Branch 215 → 216 taken 8 times.
✓ Branch 217 → 218 taken 8 times.
✓ Branch 218 → 219 taken 8 times.
✓ Branch 219 → 220 taken 8 times.
|
24 | activeMailAt(lastSelectedRow + 1).value("identity").toMap()); |
| 2136 |
2/2✓ Branch 229 → 230 taken 6 times.
✓ Branch 229 → 244 taken 2 times.
|
8 | else if (lastSelectedRow > 0) |
| 2137 |
1/1✓ Branch 235 → 236 taken 6 times.
|
12 | successor = WorkspaceState::decode( |
| 2138 |
4/4✓ Branch 230 → 231 taken 6 times.
✓ Branch 232 → 233 taken 6 times.
✓ Branch 233 → 234 taken 6 times.
✓ Branch 234 → 235 taken 6 times.
|
18 | activeMailAt(lastSelectedRow - 1).value("identity").toMap()); |
| 2139 |
2/6✗ Branch 245 → 246 not taken.
✓ Branch 245 → 249 taken 16 times.
✗ Branch 247 → 248 not taken.
✗ Branch 247 → 249 not taken.
✗ Branch 250 → 251 not taken.
✓ Branch 250 → 257 taken 16 times.
|
16 | if (!anchor.isValid() && !visibleSelectedUids.isEmpty()) { |
| 2140 | ✗ | const auto key = activeKeyAt(lastSelectedRow); | |
| 2141 | ✗ | anchor = MailId{accountName(), key.folderId, key.uid}; | |
| 2142 | ✗ | } | |
| 2143 |
5/6✓ Branch 258 → 259 taken 16 times.
✗ Branch 258 → 261 not taken.
✓ Branch 259 → 260 taken 14 times.
✓ Branch 259 → 261 taken 2 times.
✓ Branch 262 → 263 taken 14 times.
✓ Branch 262 → 266 taken 2 times.
|
16 | if (anchor.isValid() && visualRow >= 0) { |
| 2144 | 14 | m_actionAnchor = anchor; | |
| 2145 | 14 | m_actionSuccessor = successor; | |
| 2146 | 14 | m_actionVisualRow = visualRow; | |
| 2147 | } | ||
| 2148 | 16 | } | |
| 2149 | 16 | const qint64 folderId = selection->folderId; | |
| 2150 | { | ||
| 2151 | 16 | QList<MailId> moved; | |
| 2152 |
5/7✓ Branch 269 → 270 taken 16 times.
✓ Branch 270 → 271 taken 16 times.
✓ Branch 274 → 275 taken 237 times.
✓ Branch 278 → 272 taken 237 times.
✓ Branch 278 → 279 taken 16 times.
✗ Branch 474 → 475 not taken.
✗ Branch 474 → 476 not taken.
|
253 | for (const qint64 uid : visibleSelectedUids) moved.append(MailId{accountName(), folderId, uid}); |
| 2153 |
1/1✓ Branch 279 → 280 taken 16 times.
|
16 | reportManualMove(moved, path); |
| 2154 | 16 | } | |
| 2155 |
2/2✓ Branch 282 → 283 taken 16 times.
✓ Branch 283 → 284 taken 16 times.
|
16 | setStatus(tr("Queuing move of %n message(s)…", nullptr, selection->count)); |
| 2156 |
1/2✗ Branch 286 → 287 not taken.
✓ Branch 286 → 288 taken 16 times.
|
16 | if (!owner) return false; |
| 2157 |
1/1✓ Branch 295 → 296 taken 16 times.
|
32 | m_session.controller()->moves()->moveSelection(selection, path, |
| 2158 |
1/7✓ Branch 293 → 294 taken 16 times.
✗ Branch 492 → 493 not taken.
✗ Branch 492 → 494 not taken.
✗ Branch 495 → 496 not taken.
✗ Branch 495 → 497 not taken.
✗ Branch 498 → 499 not taken.
✗ Branch 498 → 500 not taken.
|
32 | [owner, database, generation, visibleSelectedUids, folderId](MailSelectionMoveResult result) { |
| 2159 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 5 taken 13 times.
|
14 | if (!owner) return; |
| 2160 | // Accepted writes remain durable even after navigation/account changes; | ||
| 2161 | // only the matching account may adopt their undo and display feedback. | ||
| 2162 |
3/4✓ Branch 8 → 9 taken 12 times.
✓ Branch 8 → 15 taken 1 time.
✓ Branch 13 → 14 taken 12 times.
✗ Branch 13 → 15 not taken.
|
25 | if (owner->m_session.accounts()->generation() == generation && |
| 2163 |
4/4✓ Branch 16 → 17 taken 12 times.
✓ Branch 16 → 18 taken 1 time.
✓ Branch 18 → 19 taken 12 times.
✓ Branch 18 → 58 taken 1 time.
|
25 | owner->m_session.cache()->databasePath() == database) { |
| 2164 |
2/2✓ Branch 19 → 20 taken 3 times.
✓ Branch 19 → 29 taken 9 times.
|
12 | if (result.outcome != MailSelectionMoveResult::Outcome::Journaled) { |
| 2165 |
1/1✓ Branch 26 → 27 taken 3 times.
|
6 | owner->m_statusState.error(result.outcome == MailSelectionMoveResult::Outcome::Conflict |
| 2166 |
4/5✓ Branch 21 → 22 taken 2 times.
✓ Branch 21 → 23 taken 1 time.
✓ Branch 22 → 26 taken 2 times.
✗ Branch 24 → 26 not taken.
✓ Branch 25 → 26 taken 1 time.
|
7 | ? tr("A selected message already has a pending move. Nothing was queued.") |
| 2167 | 1 | : result.outcome == MailSelectionMoveResult::Outcome::Limited | |
| 2168 |
1/2✗ Branch 23 → 24 not taken.
✓ Branch 23 → 25 taken 1 time.
|
1 | ? tr("The complete selection exceeds action capacity. Nothing was queued.") |
| 2169 | : tr("The selection move could not be queued. Nothing was queued; retry.")); | ||
| 2170 | } else { | ||
| 2171 | // P90.16: the journal owns the intent — remove the visible rows | ||
| 2172 | // optimistically (differential, no reset) and exclude the pending | ||
| 2173 | // moves from projection pages; the reservation token converges | ||
| 2174 | // the selection in the next reconcile. | ||
| 2175 | // Rows outside the viewport leave through the exclusion refresh, | ||
| 2176 | // which runs even when no selected row was visible. | ||
| 2177 |
1/2✓ Branch 29 → 30 taken 9 times.
✗ Branch 29 → 58 not taken.
|
9 | if (folderId >= 0) { |
| 2178 |
2/2✓ Branch 31 → 32 taken 8 times.
✓ Branch 31 → 35 taken 1 time.
|
9 | if (!visibleSelectedUids.isEmpty()) |
| 2179 | 8 | owner->m_session.headers()->removeByUids(visibleSelectedUids, folderId); | |
| 2180 | // S106-292: UIDs are per folder — the exclusions of the source | ||
| 2181 | // folder apply only while that folder is the indexed one. | ||
| 2182 |
6/6✓ Branch 37 → 38 taken 3 times.
✓ Branch 37 → 42 taken 6 times.
✓ Branch 40 → 41 taken 2 times.
✓ Branch 40 → 42 taken 1 time.
✓ Branch 43 → 44 taken 2 times.
✓ Branch 43 → 55 taken 7 times.
|
9 | if (owner->m_threadList.indexed() && owner->m_threadList.indexedFolder() == folderId) |
| 2183 |
1/1✓ Branch 51 → 52 taken 2 times.
|
4 | owner->m_threadList.refreshIndexedExclusions( |
| 2184 |
1/1✓ Branch 50 → 51 taken 2 times.
|
4 | owner->m_session.cache()->journal().pendingMoveUids(owner->accountName(), folderId)); |
| 2185 |
1/1✓ Branch 56 → 57 taken 9 times.
|
9 | QTimer::singleShot(0, owner, &QuickMailBackend::reconcileSelection); |
| 2186 | } | ||
| 2187 | } | ||
| 2188 | } | ||
| 2189 |
1/2✓ Branch 59 → 60 taken 13 times.
✗ Branch 59 → 63 not taken.
|
13 | if (owner) { |
| 2190 | 13 | owner->m_actionPending = false; | |
| 2191 | 13 | emit owner->actionChanged(); | |
| 2192 | } | ||
| 2193 | }); | ||
| 2194 | 16 | return true; | |
| 2195 | 16 | } | |
| 2196 |
7/7✓ Branch 306 → 307 taken 35 times.
✓ Branch 306 → 310 taken 2 times.
✓ Branch 307 → 308 taken 35 times.
✓ Branch 308 → 309 taken 2 times.
✓ Branch 308 → 310 taken 33 times.
✓ Branch 311 → 312 taken 2 times.
✓ Branch 311 → 313 taken 35 times.
|
37 | if (m_workspace.activeTab() == 0 && m_selection.count() == 0) return false; |
| 2197 | 35 | const auto selected = m_workspace.mailId(); | |
| 2198 |
7/8✓ Branch 315 → 316 taken 35 times.
✓ Branch 316 → 317 taken 33 times.
✓ Branch 316 → 319 taken 2 times.
✓ Branch 317 → 318 taken 33 times.
✗ Branch 318 → 319 not taken.
✓ Branch 318 → 320 taken 33 times.
✓ Branch 321 → 322 taken 2 times.
✓ Branch 321 → 323 taken 33 times.
|
35 | if (!acceptsIdentity(selected) || !m_folders.isSelectable(path)) |
| 2199 | 2 | return false; | |
| 2200 |
2/3✓ Branch 323 → 324 taken 33 times.
✗ Branch 324 → 325 not taken.
✓ Branch 324 → 329 taken 33 times.
|
33 | if (hasPendingMove(selected)) { |
| 2201 | ✗ | m_statusState.error(tr("A selected message already has a pending move. Nothing was queued.")); | |
| 2202 | ✗ | return false; | |
| 2203 | } | ||
| 2204 |
3/3✓ Branch 329 → 330 taken 33 times.
✓ Branch 330 → 331 taken 33 times.
✓ Branch 331 → 332 taken 33 times.
|
33 | reserveActionContinuity(activeRowForIdentity(WorkspaceState::encode(selected))); |
| 2205 |
8/8✓ Branch 333 → 334 taken 33 times.
✓ Branch 337 → 338 taken 33 times.
✓ Branch 338 → 339 taken 33 times.
✓ Branch 339 → 340 taken 33 times.
✓ Branch 340 → 341 taken 33 times.
✓ Branch 341 → 342 taken 33 times.
✓ Branch 347 → 335 taken 33 times.
✓ Branch 347 → 348 taken 33 times.
|
66 | qCDebug(lcQuickBackend) << "move single mail" << selected.uid << "token row" << m_actionVisualRow |
| 2206 |
2/2✓ Branch 342 → 343 taken 33 times.
✓ Branch 343 → 344 taken 33 times.
|
33 | << "successor" << m_actionSuccessor.uid; |
| 2207 |
2/2✓ Branch 352 → 353 taken 33 times.
✓ Branch 353 → 354 taken 33 times.
|
66 | m_session.controller()->moves()->moveMailsToFolderFrom({selected.uid}, selected.folderId, |
| 2208 |
1/1✓ Branch 351 → 352 taken 33 times.
|
66 | m_session.cache()->folderPath(selected.folderId), path); |
| 2209 |
3/5✓ Branch 358 → 359 taken 33 times.
✓ Branch 361 → 362 taken 33 times.
✓ Branch 361 → 363 taken 33 times.
✗ Branch 533 → 534 not taken.
✗ Branch 533 → 535 not taken.
|
66 | reportManualMove({selected}, path); |
| 2210 | 33 | return true; | |
| 2211 | 68 | } | |
| 2212 | |||
| 2213 | // S95.4.5: moves the user makes feed the rule suggestions, one count per | ||
| 2214 | // message; moves of "apply to existing" go through Deps::moveMails instead. | ||
| 2215 | 3418 | QVariantMap QuickMailBackend::readerMarks() const { | |
| 2216 | 3418 | const MailId id = m_workspace.mailId(); | |
| 2217 |
8/8✓ Branch 5 → 6 taken 2099 times.
✓ Branch 5 → 9 taken 1319 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 10 taken 2095 times.
✓ Branch 11 → 12 taken 2099 times.
✓ Branch 11 → 13 taken 1319 times.
✓ Branch 13 → 14 taken 1323 times.
✓ Branch 13 → 15 taken 2095 times.
|
3418 | if (!id.isValid() || id.accountId != accountName()) return {}; |
| 2218 |
2/2✓ Branch 16 → 17 taken 1147 times.
✓ Branch 16 → 21 taken 948 times.
|
2095 | if (m_readerMarks.id != id) m_readerMarks = ReaderMarks{id}; |
| 2219 |
2/2✓ Branch 21 → 22 taken 1546 times.
✓ Branch 21 → 46 taken 549 times.
|
2095 | if (!m_readerMarks.loaded) { |
| 2220 | // One row by key; a mail tab's mail is often outside the loaded list. | ||
| 2221 |
3/5✓ Branch 25 → 26 taken 1546 times.
✓ Branch 28 → 29 taken 1546 times.
✓ Branch 28 → 30 taken 1546 times.
✗ Branch 82 → 83 not taken.
✗ Branch 82 → 84 not taken.
|
4638 | const auto headers = m_session.cache()->headersByIds({id}); |
| 2222 |
4/4✓ Branch 31 → 32 taken 1545 times.
✓ Branch 31 → 36 taken 1 time.
✓ Branch 34 → 35 taken 266 times.
✓ Branch 34 → 36 taken 1279 times.
|
1546 | m_readerMarks.important = !headers.isEmpty() && headers.first().isFlagged(); |
| 2223 |
2/2✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 40 taken 1545 times.
|
1546 | m_readerMarks.labels = headers.isEmpty() ? QStringList{} : headers.first().labels; |
| 2224 | 1546 | m_readerMarks.loaded = true; | |
| 2225 | 1546 | } | |
| 2226 | 2095 | QStringList labels = m_readerMarks.labels; | |
| 2227 |
2/2✓ Branch 65 → 48 taken 64 times.
✓ Branch 65 → 66 taken 2095 times.
|
2159 | for (auto it = m_readerMarks.labelOverrides.cbegin(); it != m_readerMarks.labelOverrides.cend(); ++it) { |
| 2228 |
7/7✓ Branch 49 → 50 taken 37 times.
✓ Branch 49 → 54 taken 27 times.
✓ Branch 52 → 53 taken 22 times.
✓ Branch 52 → 54 taken 15 times.
✓ Branch 55 → 56 taken 22 times.
✓ Branch 55 → 58 taken 42 times.
✓ Branch 57 → 58 taken 22 times.
|
64 | if (it.value() && !labels.contains(it.key())) labels.append(it.key()); |
| 2229 |
3/3✓ Branch 59 → 60 taken 27 times.
✓ Branch 59 → 62 taken 37 times.
✓ Branch 61 → 62 taken 27 times.
|
64 | if (!it.value()) labels.removeAll(it.key()); |
| 2230 | } | ||
| 2231 | ✗ | return {{"important", m_readerMarks.importantOverride.value_or(m_readerMarks.important)}, | |
| 2232 |
2/4✓ Branch 72 → 73 taken 4190 times.
✓ Branch 72 → 74 taken 2095 times.
✗ Branch 95 → 96 not taken.
✗ Branch 95 → 97 not taken.
|
6285 | {"labels", MailPresentationModel::labelChipsFor(labels)}}; |
| 2233 | 7059 | } | |
| 2234 | 449 | void QuickMailBackend::readerMarkChanged(qint64 folderId, qint64 uid, const std::function<void(ReaderMarks &)> &apply) { | |
| 2235 | 449 | const MailId id = m_workspace.mailId(); | |
| 2236 |
9/10✓ Branch 4 → 5 taken 447 times.
✓ Branch 4 → 9 taken 2 times.
✓ Branch 5 → 6 taken 180 times.
✓ Branch 5 → 9 taken 267 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 180 times.
✓ Branch 11 → 12 taken 180 times.
✓ Branch 11 → 13 taken 269 times.
✓ Branch 13 → 14 taken 269 times.
✓ Branch 13 → 15 taken 180 times.
|
449 | if (id.folderId != folderId || id.uid != uid || id.accountId != accountName()) return; |
| 2237 |
1/2✗ Branch 16 → 17 not taken.
✓ Branch 16 → 21 taken 180 times.
|
180 | if (m_readerMarks.id != id) m_readerMarks = ReaderMarks{id}; |
| 2238 |
1/1✓ Branch 21 → 22 taken 180 times.
|
180 | apply(m_readerMarks); |
| 2239 |
1/1✓ Branch 22 → 23 taken 180 times.
|
180 | emit readerMarksChanged(); |
| 2240 | 449 | } | |
| 2241 | 49 | void QuickMailBackend::reportManualMove(const QList<MailId> &mails, const QString &targetPath) { | |
| 2242 | 49 | const bool filing = !FolderPredictionSession::excludedFolder(targetPath); | |
| 2243 |
8/8✓ Branch 4 → 5 taken 46 times.
✓ Branch 4 → 8 taken 3 times.
✓ Branch 6 → 7 taken 43 times.
✓ Branch 6 → 9 taken 3 times.
✓ Branch 7 → 8 taken 13 times.
✓ Branch 7 → 9 taken 30 times.
✓ Branch 10 → 11 taken 16 times.
✓ Branch 10 → 12 taken 33 times.
|
49 | if (mails.isEmpty() || (!m_rules.available() && !filing)) return; |
| 2244 |
5/5✓ Branch 13 → 14 taken 33 times.
✓ Branch 14 → 15 taken 33 times.
✓ Branch 15 → 16 taken 33 times.
✓ Branch 39 → 17 taken 39 times.
✓ Branch 39 → 40 taken 33 times.
|
72 | for (const auto &header : m_session.cache()->headersByIds(mails)) { |
| 2245 | // S105.3.3: the user's choice counts for the very next suggestion. | ||
| 2246 |
1/2✓ Branch 18 → 19 taken 39 times.
✗ Branch 18 → 27 not taken.
|
39 | if (filing) |
| 2247 |
2/2✓ Branch 19 → 20 taken 39 times.
✓ Branch 23 → 24 taken 39 times.
|
39 | FolderPredictionService::shared()->recordFiling(m_session.cache()->databasePath(), accountName(), header, targetPath); |
| 2248 |
2/2✓ Branch 28 → 29 taken 36 times.
✓ Branch 28 → 30 taken 3 times.
|
39 | if (!m_rules.available()) continue; |
| 2249 |
1/1✓ Branch 30 → 31 taken 3 times.
|
3 | const QString address = RuleDraftFactory::bareAddress(header.from); |
| 2250 |
2/3✓ Branch 32 → 33 taken 3 times.
✗ Branch 32 → 34 not taken.
✓ Branch 33 → 34 taken 3 times.
|
3 | if (!address.isEmpty()) m_rules.recordManualMove(address, header.listId, targetPath); |
| 2251 | 36 | } | |
| 2252 | } | ||
| 2253 | |||
| 2254 | // The opened mail can outlive its own (bulk) move while the journal is | ||
| 2255 | // pending; a second move of it must report the conflict instead of | ||
| 2256 | // announcing a move that the journal will never admit. | ||
| 2257 | 33 | bool QuickMailBackend::hasPendingMove(const MailId &mail) const { | |
| 2258 |
1/2✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 33 times.
|
33 | if (m_visualMoveUids.value(mail.folderId).contains(mail.uid)) return true; |
| 2259 |
1/2✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 33 times.
|
33 | if (m_session.controller()->hasSessionPendingMove(mail)) return true; |
| 2260 | // P90.17: a loaded row is never a journaled move (page reads and the | ||
| 2261 | // indexed exclusion set drop them), and this session's moves are known | ||
| 2262 | // in memory above. Only a mail outside the loaded rows needs the journal. | ||
| 2263 |
1/2✓ Branch 12 → 13 taken 33 times.
✗ Branch 12 → 14 not taken.
|
33 | if (loadedRowForIdentity(mail) >= 0) return false; |
| 2264 | ✗ | return m_session.cache()->journal().pendingMoveUids(accountName(), mail.folderId).contains(mail.uid); | |
| 2265 | } | ||
| 2266 | |||
| 2267 | 3 | void QuickMailBackend::beginFolderSubscriptions() { | |
| 2268 | 3 | const auto generation = m_session.accounts()->generation(); | |
| 2269 | 3 | const QStringList subscribed = m_session.controller()->subscribedFolders(); | |
| 2270 |
1/1✓ Branch 8 → 9 taken 3 times.
|
3 | const QSet<QString> already(subscribed.cbegin(), subscribed.cend()); |
| 2271 |
3/3✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✓ Branch 13 → 14 taken 3 times.
|
3 | m_folderSubscriptions.begin(accountName(), m_availableFolders.values(), subscribed, |
| 2272 | 6 | [this, generation, already](const QString &account, const QStringList &paths) { | |
| 2273 |
7/8✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 8 taken 1 time.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 1 time.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 12 taken 1 time.
✓ Branch 12 → 13 taken 1 time.
✓ Branch 12 → 14 taken 1 time.
|
2 | if (generation != m_session.accounts()->generation() || account != accountName()) return false; |
| 2274 | // A path stays admissible when it was subscribed already (S106-239). | ||
| 2275 |
1/2✗ Branch 27 → 16 not taken.
✓ Branch 27 → 28 taken 1 time.
|
1 | for (const auto &path : paths) |
| 2276 | ✗ | if (!m_availableFolders.contains(path) && !already.contains(path)) return false; | |
| 2277 | 1 | return true; | |
| 2278 | }); | ||
| 2279 | 3 | } | |
| 2280 | |||
| 2281 | 3 | MailId QuickMailBackend::displayedMailId() const { return m_workspace.mailId(); } | |
| 2282 | |||
| 2283 | // Sprint 98: availability of a command-bar line ("folder rename", "mark | ||
| 2284 | // read"); the first word is the command, the second its value. Commands the | ||
| 2285 | // window handles alone (areas, settings, line commands) are always available. | ||
| 2286 | 3367 | bool QuickMailBackend::canExecuteNamedCommand(const QString &line) const { | |
| 2287 |
2/2✓ Branch 5 → 6 taken 3367 times.
✓ Branch 6 → 7 taken 3367 times.
|
3367 | const QString name = line.section(QLatin1Char(' '), 0, 0, QString::SectionSkipEmpty).toLower(); |
| 2288 |
2/2✓ Branch 11 → 12 taken 3367 times.
✓ Branch 12 → 13 taken 3367 times.
|
3367 | const QString value = line.section(QLatin1Char(' '), 1, 1, QString::SectionSkipEmpty).toLower(); |
| 2289 | static const QSet<QString> anywhere = {"settings", "quit", "help", "calendar", "tasks", "contacts", "mail", | ||
| 2290 |
5/10✓ Branch 14 → 15 taken 10 times.
✓ Branch 14 → 47 taken 3357 times.
✓ Branch 16 → 17 taken 10 times.
✗ Branch 16 → 47 not taken.
✓ Branch 44 → 45 taken 230 times.
✓ Branch 44 → 46 taken 10 times.
✗ Branch 255 → 256 not taken.
✗ Branch 255 → 257 not taken.
✗ Branch 264 → 265 not taken.
✗ Branch 264 → 266 not taken.
|
3607 | "agenda", "import", "event", "contact", "met", "link", "relate", "map", "graph", "how", "me", "family", "theme", "notifications", "sync", "push"}; |
| 2291 |
2/2✓ Branch 48 → 49 taken 769 times.
✓ Branch 48 → 50 taken 2598 times.
|
3367 | if (anywhere.contains(name)) return true; |
| 2292 | // Sprint 100 D-29: "account new" works without an account; the others | ||
| 2293 | // need one (the active one when no address follows). | ||
| 2294 |
4/6✓ Branch 51 → 52 taken 38 times.
✓ Branch 51 → 60 taken 2560 times.
✓ Branch 53 → 54 taken 38 times.
✗ Branch 53 → 56 not taken.
✓ Branch 55 → 56 taken 38 times.
✗ Branch 55 → 57 not taken.
|
2598 | if (name == "account") return value == "new" || configured(); |
| 2295 |
5/8✓ Branch 61 → 62 taken 13 times.
✓ Branch 61 → 73 taken 2547 times.
✓ Branch 63 → 64 taken 13 times.
✗ Branch 63 → 68 not taken.
✓ Branch 66 → 67 taken 13 times.
✗ Branch 66 → 68 not taken.
✓ Branch 70 → 71 taken 13 times.
✗ Branch 70 → 72 not taken.
|
2560 | if (name == "suggestions") return configured() && m_workspace.area() == "mail"; |
| 2296 |
11/12✓ Branch 74 → 75 taken 2519 times.
✓ Branch 74 → 79 taken 28 times.
✓ Branch 76 → 77 taken 2484 times.
✓ Branch 76 → 79 taken 35 times.
✓ Branch 78 → 79 taken 35 times.
✓ Branch 78 → 80 taken 2449 times.
✓ Branch 81 → 82 taken 98 times.
✓ Branch 81 → 89 taken 2449 times.
✓ Branch 83 → 84 taken 98 times.
✗ Branch 83 → 87 not taken.
✓ Branch 85 → 86 taken 24 times.
✓ Branch 85 → 87 taken 74 times.
|
2547 | if (name == "rules" || name == "vacation" || name == "rule") return configured() && m_rules.available(); |
| 2297 |
2/2✓ Branch 90 → 91 taken 2 times.
✓ Branch 90 → 92 taken 2447 times.
|
2449 | if (!configured()) return false; |
| 2298 |
8/8✓ Branch 93 → 94 taken 2367 times.
✓ Branch 93 → 98 taken 80 times.
✓ Branch 95 → 96 taken 2346 times.
✓ Branch 95 → 98 taken 21 times.
✓ Branch 97 → 98 taken 209 times.
✓ Branch 97 → 99 taken 2137 times.
✓ Branch 100 → 101 taken 310 times.
✓ Branch 100 → 102 taken 2137 times.
|
2447 | if (name == "compose" || name == "detach" || name == "task") return true; |
| 2299 |
2/2✓ Branch 105 → 106 taken 8 times.
✓ Branch 105 → 107 taken 2129 times.
|
2137 | if (m_workspace.area() != "mail") return false; |
| 2300 | 2129 | const bool folderView = m_workspace.activeTab() == 0; | |
| 2301 | // Sprint 101 CB-06: the folder view's list commands act on search results too. | ||
| 2302 |
4/4✓ Branch 108 → 109 taken 421 times.
✓ Branch 108 → 111 taken 1708 times.
✓ Branch 110 → 111 taken 37 times.
✓ Branch 110 → 112 taken 384 times.
|
2129 | const bool listView = folderView || m_workspace.activeIsSearch(); |
| 2303 |
2/4✓ Branch 113 → 114 taken 2129 times.
✗ Branch 113 → 117 not taken.
✓ Branch 115 → 116 taken 2129 times.
✗ Branch 115 → 117 not taken.
|
2129 | const bool idleSelection = !m_actionPending && !selectionPending(); |
| 2304 |
8/8✓ Branch 119 → 120 taken 1861 times.
✓ Branch 119 → 124 taken 268 times.
✓ Branch 121 → 122 taken 1843 times.
✓ Branch 121 → 124 taken 18 times.
✓ Branch 123 → 124 taken 63 times.
✓ Branch 123 → 125 taken 1780 times.
✓ Branch 126 → 127 taken 349 times.
✓ Branch 126 → 128 taken 1780 times.
|
2129 | if (name == "filter" || name == "go" || name == "search") return listView; |
| 2305 |
2/2✓ Branch 129 → 130 taken 29 times.
✓ Branch 129 → 131 taken 1751 times.
|
1780 | if (name == "threads") return folderView; |
| 2306 |
2/2✓ Branch 132 → 133 taken 3 times.
✓ Branch 132 → 134 taken 1748 times.
|
1751 | if (name == "timeline") return m_workspace.activeIsSearch(); |
| 2307 |
2/2✓ Branch 135 → 136 taken 33 times.
✓ Branch 135 → 162 taken 1715 times.
|
1748 | if (name == "folder") { |
| 2308 |
5/7✓ Branch 137 → 138 taken 2 times.
✓ Branch 137 → 144 taken 31 times.
✓ Branch 138 → 139 taken 2 times.
✗ Branch 138 → 142 not taken.
✓ Branch 139 → 140 taken 2 times.
✓ Branch 140 → 141 taken 2 times.
✗ Branch 140 → 142 not taken.
|
33 | if (value == "properties") return folderView && m_folders.isSelectable(m_folderPath); |
| 2309 |
8/8✓ Branch 145 → 146 taken 27 times.
✓ Branch 145 → 150 taken 4 times.
✓ Branch 147 → 148 taken 24 times.
✓ Branch 147 → 150 taken 3 times.
✓ Branch 149 → 150 taken 7 times.
✓ Branch 149 → 151 taken 17 times.
✓ Branch 152 → 153 taken 14 times.
✓ Branch 152 → 161 taken 17 times.
|
31 | if (value == "rename" || value == "delete" || value == "move") |
| 2310 |
6/7✓ Branch 153 → 154 taken 12 times.
✓ Branch 153 → 159 taken 2 times.
✓ Branch 155 → 156 taken 12 times.
✗ Branch 155 → 159 not taken.
✓ Branch 156 → 157 taken 12 times.
✓ Branch 157 → 158 taken 4 times.
✓ Branch 157 → 159 taken 8 times.
|
14 | return folderView && !m_folderPath.isEmpty() && !m_folderActions.folderIsProtected(m_folderPath); |
| 2311 | 17 | return folderView; | |
| 2312 | } | ||
| 2313 |
8/8✓ Branch 163 → 164 taken 1472 times.
✓ Branch 163 → 168 taken 243 times.
✓ Branch 165 → 166 taken 1447 times.
✓ Branch 165 → 168 taken 25 times.
✓ Branch 167 → 168 taken 31 times.
✓ Branch 167 → 169 taken 1416 times.
✓ Branch 170 → 171 taken 299 times.
✓ Branch 170 → 193 taken 1416 times.
|
1715 | if (name == "archive" || name == "delete" || name == "junk") { |
| 2314 |
4/4✓ Branch 172 → 173 taken 243 times.
✓ Branch 172 → 174 taken 56 times.
✓ Branch 175 → 176 taken 25 times.
✓ Branch 175 → 177 taken 31 times.
|
299 | const auto target = name == "archive" ? m_archiveFolder : name == "delete" ? m_trashFolder : m_junkFolder; |
| 2315 |
9/12✓ Branch 180 → 181 taken 299 times.
✗ Branch 180 → 189 not taken.
✓ Branch 181 → 182 taken 299 times.
✗ Branch 181 → 189 not taken.
✓ Branch 182 → 183 taken 299 times.
✓ Branch 183 → 184 taken 277 times.
✓ Branch 183 → 189 taken 22 times.
✓ Branch 185 → 186 taken 209 times.
✓ Branch 185 → 189 taken 68 times.
✓ Branch 186 → 187 taken 209 times.
✓ Branch 187 → 188 taken 209 times.
✗ Branch 187 → 189 not taken.
|
299 | return listView && idleSelection && selectionCount() > 0 && !target.isEmpty() && m_folders.isSelectable(target); |
| 2316 | 299 | } | |
| 2317 |
7/9✓ Branch 194 → 195 taken 202 times.
✓ Branch 194 → 202 taken 1214 times.
✓ Branch 195 → 196 taken 202 times.
✗ Branch 195 → 200 not taken.
✓ Branch 196 → 197 taken 202 times.
✗ Branch 196 → 200 not taken.
✓ Branch 197 → 198 taken 202 times.
✓ Branch 198 → 199 taken 189 times.
✓ Branch 198 → 200 taken 13 times.
|
1416 | if (name == "move") return listView && idleSelection && selectionCount() > 0; |
| 2318 |
6/9✓ Branch 203 → 204 taken 15 times.
✓ Branch 203 → 212 taken 1199 times.
✓ Branch 204 → 205 taken 15 times.
✗ Branch 204 → 210 not taken.
✓ Branch 205 → 206 taken 15 times.
✗ Branch 205 → 210 not taken.
✓ Branch 207 → 208 taken 15 times.
✓ Branch 208 → 209 taken 15 times.
✗ Branch 208 → 210 not taken.
|
1214 | if (name == "open") return listView && idleSelection && acceptsIdentity(m_workspace.mailId()); |
| 2319 |
2/2✓ Branch 213 → 214 taken 34 times.
✓ Branch 213 → 215 taken 1165 times.
|
1199 | if (name == "undo") return true; |
| 2320 |
14/14✓ Branch 216 → 217 taken 922 times.
✓ Branch 216 → 227 taken 243 times.
✓ Branch 218 → 219 taken 883 times.
✓ Branch 218 → 227 taken 39 times.
✓ Branch 220 → 221 taken 504 times.
✓ Branch 220 → 227 taken 379 times.
✓ Branch 222 → 223 taken 169 times.
✓ Branch 222 → 227 taken 335 times.
✓ Branch 224 → 225 taken 89 times.
✓ Branch 224 → 227 taken 80 times.
✓ Branch 226 → 227 taken 20 times.
✓ Branch 226 → 228 taken 69 times.
✓ Branch 229 → 230 taken 1096 times.
✓ Branch 229 → 237 taken 69 times.
|
1165 | if (name == "reply" || name == "forward" || name == "mark" || name == "label" || name == "unlabel" || name == "reading") |
| 2321 |
4/5✓ Branch 230 → 231 taken 1096 times.
✗ Branch 230 → 235 not taken.
✓ Branch 232 → 233 taken 1096 times.
✓ Branch 233 → 234 taken 1008 times.
✓ Branch 233 → 235 taken 88 times.
|
1096 | return idleSelection && acceptsIdentity(m_workspace.mailId()); |
| 2322 | 69 | return false; | |
| 2323 | 3377 | } | |
| 2324 | |||
| 2325 | 3040 | QString QuickMailBackend::namedCommandUnavailableReason(const QString &line) const { | |
| 2326 |
3/3✓ Branch 2 → 3 taken 3040 times.
✓ Branch 3 → 4 taken 2715 times.
✓ Branch 3 → 5 taken 325 times.
|
3040 | if (canExecuteNamedCommand(line)) return {}; |
| 2327 |
2/2✓ Branch 8 → 9 taken 325 times.
✓ Branch 9 → 10 taken 325 times.
|
325 | const QString name = line.section(QLatin1Char(' '), 0, 0, QString::SectionSkipEmpty).toLower(); |
| 2328 |
2/2✓ Branch 14 → 15 taken 325 times.
✓ Branch 15 → 16 taken 325 times.
|
325 | const QString value = line.section(QLatin1Char(' '), 1, 1, QString::SectionSkipEmpty).toLower(); |
| 2329 |
1/3✗ Branch 18 → 19 not taken.
✓ Branch 18 → 21 taken 325 times.
✗ Branch 19 → 20 not taken.
|
325 | if (!configured()) return tr("Configure a mail account first."); |
| 2330 |
9/10✓ Branch 22 → 23 taken 307 times.
✓ Branch 22 → 27 taken 18 times.
✓ Branch 24 → 25 taken 277 times.
✓ Branch 24 → 27 taken 30 times.
✓ Branch 26 → 27 taken 17 times.
✓ Branch 26 → 30 taken 260 times.
✓ Branch 28 → 29 taken 65 times.
✗ Branch 28 → 30 not taken.
✓ Branch 31 → 32 taken 65 times.
✓ Branch 31 → 34 taken 260 times.
|
325 | if ((name == "rules" || name == "vacation" || name == "rule") && !m_rules.available()) |
| 2331 |
1/1✓ Branch 32 → 33 taken 65 times.
|
65 | return tr("Server rules are not available for this account."); |
| 2332 |
3/3✓ Branch 37 → 38 taken 7 times.
✓ Branch 37 → 40 taken 253 times.
✓ Branch 38 → 39 taken 7 times.
|
260 | if (m_workspace.area() != "mail") return tr("Open a mail folder to use this command."); |
| 2333 |
3/7✓ Branch 40 → 41 taken 253 times.
✗ Branch 40 → 43 not taken.
✗ Branch 42 → 43 not taken.
✓ Branch 42 → 44 taken 253 times.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 48 taken 253 times.
✗ Branch 46 → 47 not taken.
|
253 | if (m_actionPending || selectionPending()) return tr("Wait for the current selection or action to finish."); |
| 2334 |
8/8✓ Branch 49 → 50 taken 216 times.
✓ Branch 49 → 54 taken 37 times.
✓ Branch 51 → 52 taken 197 times.
✓ Branch 51 → 54 taken 19 times.
✓ Branch 53 → 54 taken 28 times.
✓ Branch 53 → 55 taken 169 times.
✓ Branch 56 → 57 taken 84 times.
✓ Branch 56 → 59 taken 169 times.
|
253 | if (name == "archive" || name == "delete" || name == "junk") |
| 2335 |
1/1✓ Branch 57 → 58 taken 84 times.
|
84 | return tr("Select messages and configure the destination folder."); |
| 2336 |
9/10✓ Branch 60 → 61 taken 8 times.
✓ Branch 60 → 70 taken 161 times.
✓ Branch 62 → 63 taken 6 times.
✓ Branch 62 → 67 taken 2 times.
✓ Branch 64 → 65 taken 3 times.
✓ Branch 64 → 67 taken 3 times.
✓ Branch 66 → 67 taken 3 times.
✗ Branch 66 → 70 not taken.
✓ Branch 71 → 72 taken 8 times.
✓ Branch 71 → 74 taken 161 times.
|
177 | if (name == "folder" && (value == "rename" || value == "delete" || value == "move") && |
| 2337 |
2/3✓ Branch 67 → 68 taken 8 times.
✓ Branch 68 → 69 taken 8 times.
✗ Branch 68 → 70 not taken.
|
8 | m_folderActions.folderIsProtected(m_folderPath)) |
| 2338 |
1/1✓ Branch 72 → 73 taken 8 times.
|
8 | return tr("This folder is protected."); |
| 2339 |
6/7✓ Branch 75 → 76 taken 148 times.
✓ Branch 75 → 78 taken 13 times.
✗ Branch 77 → 78 not taken.
✓ Branch 77 → 79 taken 148 times.
✓ Branch 80 → 81 taken 13 times.
✓ Branch 80 → 83 taken 148 times.
✓ Branch 81 → 82 taken 13 times.
|
161 | if (name == "move" || name == "open") return tr("Select a message first."); |
| 2340 |
1/3✗ Branch 84 → 85 not taken.
✓ Branch 84 → 87 taken 148 times.
✗ Branch 85 → 86 not taken.
|
148 | if (name == "timeline") return tr("Open a search first."); |
| 2341 |
1/1✓ Branch 87 → 88 taken 148 times.
|
148 | return tr("Select a message or open the main folder view."); |
| 2342 | 325 | } | |
| 2343 | |||
| 2344 | // The commands whose effect lives in the backend (selection flags, labels, | ||
| 2345 | // threads, search paging, reading mode). Area, folder and line commands are | ||
| 2346 | // dispatched by the window (Main.qml executeNamed). | ||
| 2347 | 79 | bool QuickMailBackend::executeNamedCommand(const QString &name, const QString &argument) { | |
| 2348 |
5/5✓ Branch 3 → 4 taken 3 times.
✓ Branch 3 → 5 taken 76 times.
✓ Branch 5 → 6 taken 76 times.
✓ Branch 6 → 7 taken 79 times.
✓ Branch 7 → 8 taken 79 times.
|
79 | const auto parsed = NamedCommandCatalog::parse(name + (argument.isEmpty() ? QString{} : " " + argument)); |
| 2349 |
11/14✓ Branch 11 → 12 taken 71 times.
✓ Branch 11 → 17 taken 8 times.
✓ Branch 13 → 14 taken 71 times.
✗ Branch 13 → 17 not taken.
✓ Branch 14 → 15 taken 71 times.
✓ Branch 15 → 16 taken 71 times.
✓ Branch 16 → 17 taken 8 times.
✓ Branch 16 → 18 taken 63 times.
✓ Branch 19 → 20 taken 71 times.
✓ Branch 19 → 21 taken 8 times.
✓ Branch 21 → 22 taken 16 times.
✓ Branch 21 → 23 taken 63 times.
✗ Branch 253 → 254 not taken.
✗ Branch 253 → 255 not taken.
|
79 | if (!parsed.complete() || parsed.spec->name != name || !canExecuteNamedCommand(parsed.text())) return false; |
| 2350 |
5/5✓ Branch 23 → 24 taken 63 times.
✓ Branch 24 → 25 taken 45 times.
✓ Branch 24 → 28 taken 18 times.
✓ Branch 25 → 26 taken 45 times.
✓ Branch 27 → 29 taken 45 times.
|
63 | const QString value = parsed.value() ? QString::fromLatin1(parsed.value()->word) : QString{}; |
| 2351 | // F-09 (SPRINT-104, D-4): ":delete forever" deletes the OPEN folder's | ||
| 2352 | // selection for good (the command bar asked once). | ||
| 2353 |
2/6✗ Branch 30 → 31 not taken.
✓ Branch 30 → 34 taken 63 times.
✗ Branch 32 → 33 not taken.
✗ Branch 32 → 34 not taken.
✗ Branch 35 → 36 not taken.
✓ Branch 35 → 68 taken 63 times.
|
63 | if (name == "delete" && value == "forever") { |
| 2354 | ✗ | if (m_actionPending || m_selection.pending()) return false; | |
| 2355 | ✗ | const auto current = m_workspace.mailId(); | |
| 2356 | ✗ | if (!acceptsIdentity(current) || current.folderId <= 0 || | |
| 2357 | ✗ | m_session.cache()->folderPath(current.folderId) != m_folderPath) { | |
| 2358 | ✗ | setStatus(tr("Delete for good works in the open folder.")); | |
| 2359 | ✗ | return true; | |
| 2360 | } | ||
| 2361 | ✗ | ensureDeleteForever(); | |
| 2362 | ✗ | m_deleteForever->deleteNow(current, deleteForeverUids(current)); | |
| 2363 | ✗ | return true; | |
| 2364 | ✗ | } | |
| 2365 |
2/2✓ Branch 69 → 70 taken 3 times.
✓ Branch 69 → 80 taken 60 times.
|
63 | if (name == "threads") { |
| 2366 |
3/5✗ Branch 71 → 72 not taken.
✓ Branch 71 → 73 taken 3 times.
✗ Branch 74 → 75 not taken.
✓ Branch 74 → 76 taken 3 times.
✓ Branch 78 → 79 taken 3 times.
|
3 | setThreaded(value == "on" ? true : value == "off" ? false : !m_threaded); |
| 2367 | 3 | return true; | |
| 2368 | } | ||
| 2369 | // D-15a: "mark read important" and "label Work Later" apply each part. | ||
| 2370 |
1/1✓ Branch 80 → 81 taken 60 times.
|
60 | QStringList parts = parsed.words(); |
| 2371 |
2/2✓ Branch 82 → 83 taken 44 times.
✓ Branch 82 → 98 taken 16 times.
|
60 | if (name == "mark") { |
| 2372 |
1/1✓ Branch 83 → 84 taken 44 times.
|
44 | parts.clear(); |
| 2373 |
7/7✓ Branch 84 → 85 taken 44 times.
✓ Branch 85 → 86 taken 44 times.
✓ Branch 86 → 87 taken 44 times.
✓ Branch 90 → 91 taken 46 times.
✓ Branch 91 → 92 taken 46 times.
✓ Branch 95 → 88 taken 46 times.
✓ Branch 95 → 96 taken 44 times.
|
90 | for (const auto *chosen : parsed.values()) parts << QString::fromLatin1(chosen->word); |
| 2374 | } | ||
| 2375 |
6/6✓ Branch 99 → 100 taken 16 times.
✓ Branch 99 → 104 taken 44 times.
✓ Branch 101 → 102 taken 6 times.
✓ Branch 101 → 104 taken 10 times.
✓ Branch 103 → 104 taken 5 times.
✓ Branch 103 → 105 taken 1 time.
|
60 | const bool selectionCommand = name == "mark" || name == "label" || name == "unlabel"; |
| 2376 |
6/7✓ Branch 106 → 107 taken 60 times.
✓ Branch 107 → 108 taken 11 times.
✓ Branch 107 → 110 taken 49 times.
✓ Branch 108 → 109 taken 11 times.
✗ Branch 108 → 110 not taken.
✓ Branch 111 → 112 taken 11 times.
✓ Branch 111 → 154 taken 49 times.
|
60 | if (bulkSelection() && selectionCommand) { |
| 2377 | 11 | bool accepted = true; | |
| 2378 |
4/4✓ Branch 112 → 113 taken 11 times.
✓ Branch 113 → 114 taken 11 times.
✓ Branch 152 → 115 taken 12 times.
✓ Branch 152 → 153 taken 11 times.
|
23 | for (const QString &part : parts) { |
| 2379 |
3/4✓ Branch 117 → 118 taken 11 times.
✓ Branch 117 → 120 taken 1 time.
✗ Branch 119 → 120 not taken.
✓ Branch 119 → 121 taken 11 times.
|
12 | const bool seen = part == "read" || part == "unread"; |
| 2380 |
1/5✓ Branch 135 → 136 taken 11 times.
✗ Branch 285 → 286 not taken.
✗ Branch 285 → 287 not taken.
✗ Branch 291 → 292 not taken.
✗ Branch 291 → 293 not taken.
|
56 | accepted = (name == "mark" ? actOnSelection(seen ? MutationKind::Seen : MutationKind::Flagged, {}, |
| 2381 |
6/6✓ Branch 126 → 127 taken 10 times.
✓ Branch 126 → 129 taken 1 time.
✓ Branch 128 → 129 taken 9 times.
✓ Branch 128 → 130 taken 1 time.
✓ Branch 148 → 149 taken 11 times.
✓ Branch 148 → 150 taken 1 time.
|
23 | part == "read" || part == "important") |
| 2382 |
12/17✓ Branch 123 → 124 taken 11 times.
✓ Branch 123 → 137 taken 1 time.
✓ Branch 132 → 133 taken 1 time.
✓ Branch 132 → 134 taken 10 times.
✓ Branch 136 → 141 taken 11 times.
✗ Branch 136 → 143 not taken.
✓ Branch 139 → 140 taken 1 time.
✓ Branch 140 → 141 taken 1 time.
✗ Branch 140 → 143 not taken.
✓ Branch 141 → 142 taken 12 times.
✗ Branch 141 → 143 not taken.
✓ Branch 144 → 145 taken 1 time.
✓ Branch 144 → 146 taken 11 times.
✓ Branch 146 → 147 taken 11 times.
✓ Branch 146 → 148 taken 1 time.
✗ Branch 281 → 282 not taken.
✗ Branch 281 → 283 not taken.
|
45 | : actOnSelection(MutationKind::Keyword, part, name == "label")) && accepted; |
| 2383 | } | ||
| 2384 | 11 | return accepted; | |
| 2385 | } | ||
| 2386 | 49 | const auto selected = m_workspace.mailId(); | |
| 2387 |
2/3✓ Branch 156 → 157 taken 49 times.
✗ Branch 157 → 158 not taken.
✓ Branch 157 → 159 taken 49 times.
|
49 | if (!acceptsIdentity(selected)) return false; |
| 2388 |
3/3✓ Branch 160 → 161 taken 1 time.
✓ Branch 160 → 163 taken 48 times.
✓ Branch 161 → 162 taken 1 time.
|
49 | if (name == "reading") return m_workspace.setReadingMode(value); |
| 2389 |
1/2✗ Branch 163 → 164 not taken.
✓ Branch 163 → 165 taken 48 times.
|
48 | if (!selectionCommand) return false; |
| 2390 |
4/4✓ Branch 165 → 166 taken 48 times.
✓ Branch 166 → 167 taken 48 times.
✓ Branch 239 → 168 taken 51 times.
✓ Branch 239 → 240 taken 48 times.
|
99 | for (const QString &part : parts) { |
| 2391 |
3/3✓ Branch 170 → 171 taken 10 times.
✓ Branch 170 → 176 taken 41 times.
✓ Branch 173 → 174 taken 10 times.
|
61 | if (name == "label") m_session.controller()->addLabelInFolder(selected.uid, selected.folderId, part); |
| 2392 |
3/3✓ Branch 177 → 178 taken 6 times.
✓ Branch 177 → 183 taken 35 times.
✓ Branch 180 → 181 taken 6 times.
|
47 | else if (name == "unlabel") m_session.controller()->removeLabelInFolder(selected.uid, selected.folderId, part); |
| 2393 |
6/6✓ Branch 184 → 185 taken 24 times.
✓ Branch 184 → 187 taken 11 times.
✓ Branch 186 → 187 taken 4 times.
✓ Branch 186 → 188 taken 20 times.
✓ Branch 189 → 190 taken 15 times.
✓ Branch 189 → 212 taken 20 times.
|
35 | else if (part == "important" || part == "unimportant") { |
| 2394 | 15 | std::optional<bool> visibleStarred; | |
| 2395 |
2/6✗ Branch 191 → 192 not taken.
✓ Branch 191 → 195 taken 15 times.
✗ Branch 193 → 194 not taken.
✗ Branch 193 → 195 not taken.
✗ Branch 196 → 197 not taken.
✓ Branch 196 → 208 taken 15 times.
|
15 | if (threadedPresentation() && m_threadList.indexed()) { |
| 2396 | ✗ | const int row = m_threadList.rowForIdentity(WorkspaceState::encode(selected)); | |
| 2397 | ✗ | if (row >= 0) | |
| 2398 | ✗ | visibleStarred = m_threadList.data(m_threadList.index(row, 0), QuickThreadListModel::Starred).toBool(); | |
| 2399 | } | ||
| 2400 |
1/1✓ Branch 210 → 211 taken 15 times.
|
15 | m_session.controller()->setStarredInFolder(selected.uid, selected.folderId, part == "important", visibleStarred); |
| 2401 | } else { | ||
| 2402 | 20 | std::optional<bool> visibleSeen; | |
| 2403 |
2/6✗ Branch 213 → 214 not taken.
✓ Branch 213 → 217 taken 20 times.
✗ Branch 215 → 216 not taken.
✗ Branch 215 → 217 not taken.
✗ Branch 218 → 219 not taken.
✓ Branch 218 → 230 taken 20 times.
|
20 | if (threadedPresentation() && m_threadList.indexed()) { |
| 2404 | ✗ | const int row = m_threadList.rowForIdentity(WorkspaceState::encode(selected)); | |
| 2405 | ✗ | if (row >= 0) | |
| 2406 | ✗ | visibleSeen = !m_threadList.data(m_threadList.index(row, 0), QuickThreadListModel::Unread).toBool(); | |
| 2407 | } | ||
| 2408 |
3/3✓ Branch 231 → 232 taken 4 times.
✓ Branch 231 → 234 taken 16 times.
✓ Branch 233 → 236 taken 4 times.
|
20 | if (part == "read") m_session.controller()->markMailAsSeenInFolder(selected.uid, selected.folderId, visibleSeen); |
| 2409 |
1/1✓ Branch 235 → 236 taken 16 times.
|
16 | else m_session.controller()->markMailAsUnseenInFolder(selected.uid, selected.folderId, visibleSeen); |
| 2410 | } | ||
| 2411 | } | ||
| 2412 | 48 | return true; | |
| 2413 | 79 | } | |
| 2414 | |||
| 2415 | // executeCommand handler: Suggestion column commands and the quick move (F4.2). | ||
| 2416 | 1184 | std::optional<bool> QuickMailBackend::handleSuggestionCommand(const QString &id) { | |
| 2417 |
8/10✓ Branch 3 → 4 taken 15 times.
✓ Branch 3 → 10 taken 1169 times.
✓ Branch 5 → 6 taken 15 times.
✗ Branch 5 → 10 not taken.
✓ Branch 8 → 9 taken 15 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 15 times.
✓ Branch 11 → 13 taken 1169 times.
✓ Branch 13 → 14 taken 15 times.
✓ Branch 13 → 18 taken 1169 times.
|
1184 | if (id == "suggestion.toggle_all" && m_workspace.activeTab() == 0 && m_workspace.area() == "mail") { |
| 2418 | 15 | setSuggestionsVisible(!m_suggestionsVisible); | |
| 2419 | 15 | return true; | |
| 2420 | } | ||
| 2421 |
8/10✓ Branch 19 → 20 taken 8 times.
✓ Branch 19 → 26 taken 1161 times.
✓ Branch 21 → 22 taken 8 times.
✗ Branch 21 → 26 not taken.
✓ Branch 24 → 25 taken 8 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 8 times.
✓ Branch 27 → 29 taken 1161 times.
✓ Branch 29 → 30 taken 8 times.
✓ Branch 29 → 45 taken 1161 times.
|
1169 | if (id == "suggestion.alternate" && m_workspace.activeTab() == 0 && m_workspace.area() == "mail") { |
| 2422 | 8 | const auto selected = m_workspace.mailId(); | |
| 2423 |
1/3✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 8 times.
✗ Branch 34 → 39 not taken.
|
8 | if (m_alternatePredictions.contains(selected)) m_alternatePredictions.remove(selected); |
| 2424 |
2/3✓ Branch 36 → 37 taken 8 times.
✗ Branch 36 → 39 not taken.
✓ Branch 37 → 38 taken 8 times.
|
8 | else if (m_predictions.contains(selected)) m_alternatePredictions.insert(selected); |
| 2425 |
1/1✓ Branch 39 → 40 taken 8 times.
|
8 | m_mail.suggestionsChanged(); |
| 2426 |
1/1✓ Branch 40 → 41 taken 8 times.
|
8 | emit suggestionsChanged(); |
| 2427 | 8 | return true; | |
| 2428 | 8 | } | |
| 2429 |
8/10✓ Branch 46 → 47 taken 33 times.
✓ Branch 46 → 53 taken 1128 times.
✓ Branch 48 → 49 taken 33 times.
✗ Branch 48 → 53 not taken.
✓ Branch 51 → 52 taken 33 times.
✗ Branch 51 → 53 not taken.
✓ Branch 54 → 55 taken 33 times.
✓ Branch 54 → 56 taken 1128 times.
✓ Branch 56 → 57 taken 33 times.
✓ Branch 56 → 89 taken 1128 times.
|
1161 | if (id == "mail.quick_move" && m_workspace.activeTab() == 0 && m_workspace.area() == "mail") { |
| 2430 | 33 | const auto selected = m_workspace.mailId(); | |
| 2431 |
1/1✓ Branch 59 → 60 taken 33 times.
|
33 | const auto target = visiblePrediction(selected).first; |
| 2432 |
5/6✓ Branch 63 → 64 taken 23 times.
✓ Branch 63 → 70 taken 10 times.
✓ Branch 65 → 66 taken 23 times.
✗ Branch 65 → 70 not taken.
✓ Branch 72 → 73 taken 10 times.
✓ Branch 72 → 82 taken 23 times.
|
56 | if (target.isEmpty() || !m_predictionEpochs.contains(selected.folderId) || |
| 2433 |
2/3✓ Branch 68 → 69 taken 23 times.
✗ Branch 69 → 70 not taken.
✓ Branch 69 → 71 taken 23 times.
|
23 | m_predictionEpochs.value(selected.folderId) != m_session.cache()->uidValidity(selected.folderId)) { |
| 2434 | // F4.2(b): after a quick move the follow-up selection's prediction is | ||
| 2435 | // often still behind the 20 ms debounce or its worker batch. A rapid | ||
| 2436 | // second s must retry that prediction instead of opening the manual | ||
| 2437 | // move console; only a genuinely prediction-less state falls back. | ||
| 2438 |
3/3✓ Branch 73 → 74 taken 10 times.
✓ Branch 74 → 75 taken 1 time.
✓ Branch 74 → 79 taken 9 times.
|
10 | if (canArmQuickMoveRetry(selected)) { |
| 2439 |
1/1✓ Branch 75 → 76 taken 1 time.
|
1 | armQuickMoveRetry(selected); |
| 2440 | 1 | return true; | |
| 2441 | } | ||
| 2442 | 9 | return false; | |
| 2443 | } | ||
| 2444 | // A derived result is never authority: reuse live target/selection admission. | ||
| 2445 |
1/1✓ Branch 82 → 83 taken 23 times.
|
23 | moveSelectedMail(target); |
| 2446 | 23 | return true; | |
| 2447 | 33 | } | |
| 2448 | 1128 | return std::nullopt; | |
| 2449 | } | ||
| 2450 | |||
| 2451 | 796 | void QuickMailBackend::setSuggestionsVisible(bool on) { | |
| 2452 |
2/2✓ Branch 2 → 3 taken 773 times.
✓ Branch 2 → 4 taken 23 times.
|
796 | if (m_suggestionsVisible == on) return; |
| 2453 | 23 | m_suggestionsVisible = on; | |
| 2454 | 23 | invalidatePredictions(); | |
| 2455 | 23 | emit suggestionsVisibleChanged(); | |
| 2456 | } | ||
| 2457 | |||
| 2458 | // executeCommand handler: Escape leaves an active search. The suggestion | ||
| 2459 | // column is a persistent view setting (F95-15) and only Ctrl+S toggles it. | ||
| 2460 | 1128 | std::optional<bool> QuickMailBackend::handleSurfaceClose(const QString &id) { | |
| 2461 | // F100-77: Esc on a multiple selection keeps only the current mail. | ||
| 2462 |
6/8✓ Branch 5 → 6 taken 14 times.
✓ Branch 5 → 16 taken 8 times.
✓ Branch 8 → 9 taken 7 times.
✓ Branch 8 → 16 taken 7 times.
✓ Branch 19 → 20 taken 1 time.
✓ Branch 19 → 29 taken 1127 times.
✗ Branch 52 → 53 not taken.
✗ Branch 52 → 54 not taken.
|
1150 | if (id == "surface.close" && m_workspace.activeTab() == 0 && m_workspace.area() == "mail" && |
| 2463 |
9/11✓ Branch 3 → 4 taken 22 times.
✓ Branch 3 → 16 taken 1106 times.
✓ Branch 10 → 11 taken 7 times.
✗ Branch 10 → 16 not taken.
✓ Branch 12 → 13 taken 7 times.
✗ Branch 12 → 16 not taken.
✓ Branch 13 → 14 taken 7 times.
✓ Branch 14 → 15 taken 1 time.
✓ Branch 14 → 16 taken 6 times.
✓ Branch 17 → 18 taken 14 times.
✓ Branch 17 → 19 taken 1114 times.
|
1150 | !m_workspace.readerFocused() && !m_selection.pending() && m_selection.count() > 1) { |
| 2464 |
2/2✓ Branch 21 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
|
1 | const int row = activeRowForIdentity(WorkspaceState::encode(m_workspace.mailId())); |
| 2465 |
1/2✓ Branch 24 → 25 taken 1 time.
✗ Branch 24 → 29 not taken.
|
1 | if (row >= 0) { |
| 2466 | 1 | selectMailRow(row); | |
| 2467 | 1 | return true; | |
| 2468 | } | ||
| 2469 | } | ||
| 2470 | // Sprint 101 D-18, amended by F100-49 (user, 2026-10-05): Esc with nothing | ||
| 2471 | // else to cancel closes the search tab and returns to the tab used before it. | ||
| 2472 |
9/10✓ Branch 30 → 31 taken 21 times.
✓ Branch 30 → 39 taken 1106 times.
✓ Branch 32 → 33 taken 8 times.
✓ Branch 32 → 39 taken 13 times.
✓ Branch 35 → 36 taken 8 times.
✗ Branch 35 → 39 not taken.
✓ Branch 40 → 41 taken 8 times.
✓ Branch 40 → 42 taken 1119 times.
✓ Branch 42 → 43 taken 8 times.
✓ Branch 42 → 48 taken 1119 times.
|
1135 | if (id == "surface.close" && m_workspace.activeIsSearch() && m_workspace.area() == "mail" && |
| 2473 |
1/2✓ Branch 37 → 38 taken 8 times.
✗ Branch 37 → 39 not taken.
|
8 | !m_workspace.readerFocused()) { |
| 2474 | 8 | m_workspace.closeTab(m_workspace.activeTab()); | |
| 2475 | 8 | return true; | |
| 2476 | } | ||
| 2477 | 1119 | return std::nullopt; | |
| 2478 | } | ||
| 2479 | |||
| 2480 | // executeCommand handler: Keyboard navigation in the indexed (large-folder) thread list. | ||
| 2481 | 1119 | std::optional<bool> QuickMailBackend::handleIndexedNavigation(const QString &id) { | |
| 2482 |
6/6✓ Branch 6 → 7 taken 697 times.
✓ Branch 6 → 24 taken 312 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 24 taken 693 times.
✓ Branch 27 → 28 taken 3 times.
✓ Branch 27 → 138 taken 1116 times.
|
2825 | if (m_workspace.activeTab() == 0 && m_workspace.area() == "mail" && |
| 2483 |
5/6✓ Branch 3 → 4 taken 1009 times.
✓ Branch 3 → 24 taken 110 times.
✓ Branch 10 → 11 taken 3 times.
✓ Branch 10 → 24 taken 1 time.
✓ Branch 12 → 13 taken 3 times.
✗ Branch 12 → 23 not taken.
|
2831 | threadedPresentation() && m_threadList.indexed() && |
| 2484 |
4/6✓ Branch 14 → 15 taken 2 times.
✓ Branch 14 → 23 taken 1 time.
✓ Branch 16 → 17 taken 2 times.
✗ Branch 16 → 23 not taken.
✓ Branch 18 → 19 taken 2 times.
✗ Branch 18 → 23 not taken.
|
8 | (id == "mail.next" || id == "mail.previous" || id == "mail.page_down" || |
| 2485 |
5/6✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 23 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
✗ Branch 22 → 24 not taken.
✓ Branch 25 → 26 taken 1009 times.
✓ Branch 25 → 27 taken 110 times.
|
1123 | id == "mail.page_up" || id == "mail.first" || id == "mail.last")) { |
| 2486 | // Nothing usable is shown yet (first page, user-announced reload, or the | ||
| 2487 | // previous folder's inert rows): the key has no row to act on. | ||
| 2488 |
3/6✓ Branch 29 → 30 taken 3 times.
✗ Branch 29 → 32 not taken.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 33 taken 3 times.
✗ Branch 34 → 35 not taken.
✓ Branch 34 → 38 taken 3 times.
|
3 | if (m_threadList.projectionBlocking() || m_threadList.holdingPreviousFolder()) return true; |
| 2489 | 3 | const auto selected = m_workspace.mailId(); | |
| 2490 |
1/1✓ Branch 40 → 41 taken 3 times.
|
3 | const auto generation = m_workspace.generation(); |
| 2491 |
4/4✓ Branch 42 → 43 taken 2 times.
✓ Branch 42 → 45 taken 1 time.
✓ Branch 44 → 45 taken 1 time.
✓ Branch 44 → 46 taken 1 time.
|
3 | const bool boundary = id == "mail.first" || id == "mail.last"; |
| 2492 |
5/6✓ Branch 48 → 49 taken 2 times.
✓ Branch 48 → 53 taken 1 time.
✓ Branch 50 → 51 taken 2 times.
✗ Branch 50 → 53 not taken.
✓ Branch 52 → 53 taken 1 time.
✓ Branch 52 → 54 taken 1 time.
|
3 | const bool before = id == "mail.previous" || id == "mail.page_up" || id == "mail.first"; |
| 2493 |
4/6✓ Branch 55 → 56 taken 2 times.
✓ Branch 55 → 57 taken 1 time.
✓ Branch 59 → 60 taken 3 times.
✗ Branch 59 → 62 not taken.
✗ Branch 61 → 62 not taken.
✓ Branch 61 → 64 taken 3 times.
|
3 | const int delta = (before ? -1 : 1) * ((id == "mail.page_up" || id == "mail.page_down") ? qMax(1, m_pageRows) : 1); |
| 2494 |
2/2✓ Branch 65 → 66 taken 3 times.
✓ Branch 66 → 67 taken 3 times.
|
3 | const int row = activeRowForIdentity(WorkspaceState::encode(selected)); |
| 2495 |
5/5✓ Branch 68 → 69 taken 2 times.
✓ Branch 68 → 74 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
✓ Branch 69 → 71 taken 1 time.
✓ Branch 71 → 72 taken 1 time.
|
3 | const int target = boundary ? (before ? 0 : activeRowCount()-1) : row+delta; |
| 2496 |
5/7✓ Branch 75 → 76 taken 1 time.
✓ Branch 75 → 79 taken 2 times.
✓ Branch 76 → 77 taken 1 time.
✗ Branch 76 → 79 not taken.
✓ Branch 77 → 78 taken 1 time.
✗ Branch 78 → 79 not taken.
✓ Branch 78 → 80 taken 1 time.
|
3 | const bool needsPage = boundary || target<0 || target>=activeRowCount(); |
| 2497 |
5/8✓ Branch 82 → 83 taken 1 time.
✓ Branch 82 → 86 taken 2 times.
✓ Branch 83 → 84 taken 1 time.
✗ Branch 83 → 85 not taken.
✗ Branch 84 → 85 not taken.
✓ Branch 84 → 86 taken 1 time.
✗ Branch 87 → 88 not taken.
✓ Branch 87 → 109 taken 3 times.
|
3 | if (m_threadList.projectionPending() && (row < 0 || needsPage)) { |
| 2498 | // A background refresh (e.g. the Seen commit of the opened mail) owns | ||
| 2499 | // the paging state. Within the published rows the key acts at once | ||
| 2500 | // (below); a key that needs another page runs once that refresh landed | ||
| 2501 | // instead of being dropped. | ||
| 2502 | ✗ | qCDebug(lcQuickBackend) << "navigation" << id << "waits for the pending thread page"; | |
| 2503 | ✗ | connect(&m_threadList, &QuickThreadListModel::projectionReady, this, | |
| 2504 | ✗ | [this, id, selected, generation] { | |
| 2505 | ✗ | if (m_workspace.generation() != generation || m_workspace.mailId() != selected) return; | |
| 2506 | ✗ | executeCommand(id); | |
| 2507 | }, Qt::SingleShotConnection); | ||
| 2508 | ✗ | return true; | |
| 2509 | } | ||
| 2510 |
6/8✓ Branch 109 → 110 taken 3 times.
✗ Branch 109 → 113 not taken.
✓ Branch 110 → 111 taken 2 times.
✓ Branch 110 → 114 taken 1 time.
✓ Branch 112 → 113 taken 2 times.
✗ Branch 112 → 114 not taken.
✓ Branch 115 → 116 taken 2 times.
✓ Branch 115 → 126 taken 1 time.
|
3 | if (row<0 || (needsPage && m_threadList.hasMoreIndexed(before))) { |
| 2511 | 2 | connect(&m_threadList, &QuickThreadListModel::projectionReady, this, | |
| 2512 |
1/5✓ Branch 118 → 119 taken 2 times.
✗ Branch 162 → 163 not taken.
✗ Branch 162 → 164 not taken.
✗ Branch 165 → 166 not taken.
✗ Branch 165 → 167 not taken.
|
4 | [this, selected, generation, boundary, before, delta] { |
| 2513 |
6/13✓ Branch 2 → 3 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 10 not taken.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 10 not taken.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 2 times.
✓ Branch 12 → 13 taken 2 times.
✗ Branch 12 → 14 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 2 times.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
|
2 | if (m_workspace.generation()!=generation || m_workspace.mailId()!=selected || !threadedPresentation()) return; |
| 2514 |
2/2✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
|
2 | const int current = activeRowForIdentity(WorkspaceState::encode(selected)); |
| 2515 |
4/5✓ Branch 19 → 20 taken 2 times.
✗ Branch 19 → 25 not taken.
✓ Branch 20 → 21 taken 1 time.
✓ Branch 20 → 22 taken 1 time.
✓ Branch 22 → 23 taken 1 time.
|
2 | const int target = boundary ? (before ? 0 : activeRowCount()-1) : current+delta; |
| 2516 |
5/6✓ Branch 26 → 27 taken 2 times.
✓ Branch 27 → 28 taken 2 times.
✗ Branch 27 → 32 not taken.
✓ Branch 28 → 29 taken 2 times.
✓ Branch 29 → 30 taken 2 times.
✓ Branch 30 → 31 taken 2 times.
|
2 | if (activeRowCount()>0) selectMailRow(qBound(0, target, activeRowCount()-1)); |
| 2517 | }, Qt::SingleShotConnection); | ||
| 2518 |
2/3✓ Branch 121 → 122 taken 2 times.
✗ Branch 121 → 123 not taken.
✓ Branch 122 → 132 taken 2 times.
|
2 | if (boundary) m_threadList.jumpIndexed(!before); |
| 2519 | ✗ | else if (row<0) m_threadList.revealIndexed(selected.uid,before); | |
| 2520 | ✗ | else m_threadList.fetchIndexed(before); | |
| 2521 |
5/6✓ Branch 126 → 127 taken 1 time.
✓ Branch 127 → 128 taken 1 time.
✗ Branch 127 → 132 not taken.
✓ Branch 128 → 129 taken 1 time.
✓ Branch 129 → 130 taken 1 time.
✓ Branch 130 → 131 taken 1 time.
|
1 | } else if (activeRowCount()>0) selectMailRow(qBound(0, target, activeRowCount()-1)); |
| 2522 | 3 | return true; | |
| 2523 | 3 | } | |
| 2524 | 1116 | return std::nullopt; | |
| 2525 | } | ||
| 2526 | |||
| 2527 | // executeCommand handler (Sprint 101): j/k, pages, first/last and next | ||
| 2528 | // unread move through the results of the active search tab. | ||
| 2529 | 1116 | std::optional<bool> QuickMailBackend::handleSearchTabNavigation(const QString &id) { | |
| 2530 | static const QSet<QString> navigation = {"mail.next", "mail.previous", "mail.page_down", "mail.page_up", | ||
| 2531 |
5/10✓ Branch 2 → 3 taken 16 times.
✓ Branch 2 → 19 taken 1100 times.
✓ Branch 4 → 5 taken 16 times.
✗ Branch 4 → 19 not taken.
✓ Branch 16 → 17 taken 112 times.
✓ Branch 16 → 18 taken 16 times.
✗ Branch 106 → 107 not taken.
✗ Branch 106 → 108 not taken.
✗ Branch 115 → 116 not taken.
✗ Branch 115 → 117 not taken.
|
1244 | "mail.first", "mail.last", "mail.next_unread"}; |
| 2532 |
9/10✓ Branch 20 → 21 taken 16 times.
✓ Branch 20 → 26 taken 1100 times.
✓ Branch 23 → 24 taken 16 times.
✗ Branch 23 → 26 not taken.
✓ Branch 25 → 26 taken 14 times.
✓ Branch 25 → 27 taken 2 times.
✓ Branch 28 → 29 taken 16 times.
✓ Branch 28 → 30 taken 1100 times.
✓ Branch 30 → 31 taken 1114 times.
✓ Branch 30 → 34 taken 2 times.
|
1116 | if (!m_workspace.activeIsSearch() || m_workspace.area() != "mail" || !navigation.contains(id)) return std::nullopt; |
| 2533 |
1/1✓ Branch 35 → 36 taken 2 times.
|
2 | MailSearchSession *search = m_searchTabs->active(); |
| 2534 |
4/7✓ Branch 36 → 37 taken 2 times.
✗ Branch 36 → 39 not taken.
✓ Branch 37 → 38 taken 2 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 2 times.
✗ Branch 41 → 42 not taken.
✓ Branch 41 → 45 taken 2 times.
|
2 | if (!search || search->loadedCount() == 0) return true; |
| 2535 |
1/1✓ Branch 45 → 46 taken 2 times.
|
2 | const int count = search->loadedCount(); |
| 2536 |
1/1✓ Branch 47 → 48 taken 2 times.
|
2 | const int current = search->rowOf(m_workspace.mailId()); |
| 2537 | 2 | const int page = qMax(1, m_pageRows); | |
| 2538 | 2 | int target = current; | |
| 2539 |
1/2✓ Branch 50 → 51 taken 2 times.
✗ Branch 50 → 52 not taken.
|
2 | if (id == "mail.next") target = current + 1; |
| 2540 | ✗ | else if (id == "mail.previous") target = current - 1; | |
| 2541 | ✗ | else if (id == "mail.page_down") target = current + page; | |
| 2542 | ✗ | else if (id == "mail.page_up") target = current - page; | |
| 2543 | ✗ | else if (id == "mail.first") target = 0; | |
| 2544 | ✗ | else if (id == "mail.last") target = count - 1; | |
| 2545 | else { | ||
| 2546 | ✗ | for (int step = 1; step <= count; ++step) { | |
| 2547 | ✗ | const int row = (qMax(current, 0) + step) % count; | |
| 2548 | ✗ | if (search->results()->data(search->results()->index(row, 0), MailPresentationModel::Unread).toBool()) { | |
| 2549 | ✗ | target = row; | |
| 2550 | ✗ | break; | |
| 2551 | } | ||
| 2552 | } | ||
| 2553 | ✗ | if (target == current) { setStatus(tr("No unread messages")); return true; } | |
| 2554 | } | ||
| 2555 |
1/1✓ Branch 87 → 88 taken 2 times.
|
2 | target = qBound(0, target, count - 1); |
| 2556 |
1/3✗ Branch 88 → 89 not taken.
✓ Branch 88 → 90 taken 2 times.
✗ Branch 89 → 90 not taken.
|
2 | if (target >= count - 20) search->fetchMore(); // infinite paging ahead of the cursor |
| 2557 |
1/1✓ Branch 90 → 91 taken 2 times.
|
2 | const MailId mail = search->mailIdAt(target); |
| 2558 |
5/8✓ Branch 91 → 92 taken 2 times.
✓ Branch 92 → 93 taken 2 times.
✗ Branch 92 → 97 not taken.
✓ Branch 95 → 96 taken 2 times.
✗ Branch 95 → 97 not taken.
✓ Branch 98 → 99 taken 2 times.
✗ Branch 98 → 100 not taken.
✓ Branch 99 → 100 taken 2 times.
|
2 | if (acceptsIdentity(mail) && mail != m_workspace.mailId()) m_workspace.select(mail); |
| 2559 | 2 | return true; | |
| 2560 | 18 | } | |
| 2561 | |||
| 2562 | // executeCommand handler: Next/previous inside a mail tab and the previous-folder jump. | ||
| 2563 | 1114 | std::optional<bool> QuickMailBackend::handleTabNavigation(const QString &id) { | |
| 2564 |
8/8✓ Branch 3 → 4 taken 1094 times.
✓ Branch 3 → 6 taken 20 times.
✓ Branch 5 → 6 taken 16 times.
✓ Branch 5 → 9 taken 1078 times.
✓ Branch 7 → 8 taken 13 times.
✓ Branch 7 → 9 taken 23 times.
✓ Branch 10 → 11 taken 13 times.
✓ Branch 10 → 50 taken 1101 times.
|
1114 | if ((id == "mail.next" || id == "mail.previous") && m_workspace.activeTab() != 0) { |
| 2565 | // This command is handled even at a boundary/rejection: never fall through | ||
| 2566 | // to the QML main-list navigation. Queue only small input intents, not mails. | ||
| 2567 |
2/4✓ Branch 15 → 16 taken 13 times.
✗ Branch 15 → 23 not taken.
✗ Branch 29 → 30 not taken.
✓ Branch 29 → 33 taken 13 times.
|
26 | if (m_workspace.area() != "mail" || !configured() || |
| 2568 |
5/10✓ Branch 13 → 14 taken 13 times.
✗ Branch 13 → 23 not taken.
✓ Branch 19 → 20 taken 13 times.
✗ Branch 19 → 23 not taken.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 24 taken 13 times.
✓ Branch 25 → 26 taken 13 times.
✗ Branch 25 → 27 not taken.
✓ Branch 27 → 28 taken 13 times.
✗ Branch 27 → 29 not taken.
|
26 | m_workspace.mailId().accountId != accountName() || !m_workspace.mailId().isValid()) return true; |
| 2569 |
1/4✗ Branch 34 → 35 not taken.
✓ Branch 34 → 41 taken 13 times.
✗ Branch 35 → 36 not taken.
✗ Branch 36 → 37 not taken.
|
13 | if (m_navigation.size() >= 64) { setStatus(tr("Navigation is busy. Wait and retry.")); return true; } |
| 2570 |
3/3✓ Branch 42 → 43 taken 6 times.
✓ Branch 42 → 44 taken 7 times.
✓ Branch 45 → 46 taken 13 times.
|
13 | m_navigation.append(id == "mail.next" ? 1 : -1); |
| 2571 | 13 | startNavigation(); | |
| 2572 | 13 | return true; | |
| 2573 | } | ||
| 2574 |
3/3✓ Branch 51 → 52 taken 3 times.
✓ Branch 51 → 56 taken 1098 times.
✓ Branch 52 → 53 taken 3 times.
|
1101 | if (id == "folder.previous") return selectFolder(m_previousFolder); |
| 2575 | 1098 | return std::nullopt; | |
| 2576 | } | ||
| 2577 | |||
| 2578 | // executeCommand handler: First/last/page navigation in the main list (B4). | ||
| 2579 | 1098 | std::optional<bool> QuickMailBackend::handleMainListNavigation(const QString &id) { | |
| 2580 |
6/6✓ Branch 5 → 6 taken 1092 times.
✓ Branch 5 → 10 taken 3 times.
✓ Branch 7 → 8 taken 1091 times.
✓ Branch 7 → 10 taken 1 time.
✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 13 taken 1089 times.
|
3284 | if ((id == "mail.first" || id == "mail.last" || id == "mail.page_down" || |
| 2581 |
6/6✓ Branch 3 → 4 taken 1095 times.
✓ Branch 3 → 10 taken 3 times.
✓ Branch 11 → 12 taken 8 times.
✓ Branch 11 → 13 taken 1 time.
✓ Branch 14 → 15 taken 8 times.
✓ Branch 14 → 84 taken 1090 times.
|
3287 | id == "mail.page_up") && |
| 2582 | 9 | m_workspace.activeTab() == 0) { | |
| 2583 | // Main-list global navigation (B4): first/last/page operate on the | ||
| 2584 | // connected mail projection; the router owns the G-G prefix timing. | ||
| 2585 |
6/13✓ Branch 17 → 18 taken 8 times.
✗ Branch 17 → 22 not taken.
✓ Branch 19 → 20 taken 8 times.
✗ Branch 19 → 22 not taken.
✓ Branch 20 → 21 taken 8 times.
✗ Branch 21 → 22 not taken.
✓ Branch 21 → 23 taken 8 times.
✓ Branch 24 → 25 taken 8 times.
✗ Branch 24 → 26 not taken.
✗ Branch 26 → 27 not taken.
✓ Branch 26 → 30 taken 8 times.
✗ Branch 88 → 89 not taken.
✗ Branch 88 → 90 not taken.
|
8 | if (m_workspace.area() != "mail" || !configured() || activeRowCount() == 0) return true; |
| 2586 |
1/1✓ Branch 30 → 31 taken 8 times.
|
8 | const int last = activeRowCount() - 1; |
| 2587 | 8 | int target = -1; | |
| 2588 |
2/2✓ Branch 32 → 33 taken 3 times.
✓ Branch 32 → 34 taken 5 times.
|
8 | if (id == "mail.first") target = 0; |
| 2589 |
2/2✓ Branch 35 → 36 taken 2 times.
✓ Branch 35 → 37 taken 3 times.
|
5 | else if (id == "mail.last") target = last; |
| 2590 | else { | ||
| 2591 |
2/2✓ Branch 38 → 39 taken 3 times.
✓ Branch 39 → 40 taken 3 times.
|
3 | const int current = activeRowForIdentity(WorkspaceState::encode(m_workspace.mailId())); |
| 2592 |
1/2✗ Branch 41 → 42 not taken.
✓ Branch 41 → 45 taken 3 times.
|
3 | if (current < 0) return true; |
| 2593 | 3 | const int rows = qMax(1, m_pageRows); | |
| 2594 |
3/3✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 2 times.
✓ Branch 50 → 51 taken 3 times.
|
3 | target = qBound(0, current + (id == "mail.page_down" ? rows : -rows), last); |
| 2595 | } | ||
| 2596 |
2/4✓ Branch 52 → 53 taken 8 times.
✗ Branch 52 → 54 not taken.
✗ Branch 53 → 54 not taken.
✓ Branch 53 → 57 taken 8 times.
|
8 | if (target < 0 || target > last) return true; |
| 2597 | const auto identity = | ||
| 2598 |
5/5✓ Branch 57 → 58 taken 8 times.
✓ Branch 59 → 60 taken 8 times.
✓ Branch 60 → 61 taken 8 times.
✓ Branch 61 → 62 taken 8 times.
✓ Branch 62 → 63 taken 8 times.
|
8 | WorkspaceState::decode(activeMailAt(target).value("identity").toMap()); |
| 2599 |
5/6✓ Branch 69 → 70 taken 8 times.
✗ Branch 69 → 74 not taken.
✓ Branch 72 → 73 taken 4 times.
✓ Branch 72 → 74 taken 4 times.
✓ Branch 75 → 76 taken 4 times.
✓ Branch 75 → 80 taken 4 times.
|
8 | if (identity.isValid() && identity != m_workspace.mailId()) |
| 2600 |
2/2✓ Branch 76 → 77 taken 4 times.
✓ Branch 77 → 78 taken 4 times.
|
4 | m_workspace.selectIdentity(WorkspaceState::encode(identity)); |
| 2601 | 8 | return true; | |
| 2602 | 8 | } | |
| 2603 | 1090 | return std::nullopt; | |
| 2604 | } | ||
| 2605 | |||
| 2606 | // executeCommand handler: Thread view toggle and next-unread navigation. | ||
| 2607 | 1090 | std::optional<bool> QuickMailBackend::handleViewCommand(const QString &id) { | |
| 2608 |
8/10✓ Branch 3 → 4 taken 11 times.
✓ Branch 3 → 10 taken 1079 times.
✓ Branch 5 → 6 taken 11 times.
✗ Branch 5 → 10 not taken.
✓ Branch 8 → 9 taken 11 times.
✗ Branch 8 → 10 not taken.
✓ Branch 11 → 12 taken 11 times.
✓ Branch 11 → 13 taken 1079 times.
✓ Branch 13 → 14 taken 11 times.
✓ Branch 13 → 18 taken 1079 times.
|
1090 | if (id == "view.thread" && m_workspace.activeTab() == 0 && m_workspace.area() == "mail") { |
| 2609 | 11 | setThreaded(!m_threaded); | |
| 2610 | 11 | return true; | |
| 2611 | } | ||
| 2612 |
8/10✓ Branch 19 → 20 taken 6 times.
✓ Branch 19 → 26 taken 1073 times.
✓ Branch 21 → 22 taken 6 times.
✗ Branch 21 → 26 not taken.
✓ Branch 24 → 25 taken 6 times.
✗ Branch 24 → 26 not taken.
✓ Branch 27 → 28 taken 6 times.
✓ Branch 27 → 29 taken 1073 times.
✓ Branch 29 → 30 taken 6 times.
✓ Branch 29 → 72 taken 1073 times.
|
1079 | if (id == "mail.next_unread" && m_workspace.activeTab() == 0 && m_workspace.area() == "mail") { |
| 2613 | // Widgets parity (Space): advance to the next unread mail in the visible | ||
| 2614 | // order, wrapping once. The cross-folder jump stays open parity work. | ||
| 2615 |
6/7✓ Branch 31 → 32 taken 5 times.
✓ Branch 31 → 34 taken 1 time.
✓ Branch 32 → 33 taken 5 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 35 taken 5 times.
✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 40 taken 5 times.
|
6 | if (!configured() || activeRowCount() == 0) return true; |
| 2616 | // P90.16: unread state comes from the loaded projection role, not from | ||
| 2617 | // one synchronous SQLite header query per candidate. | ||
| 2618 | 5 | QAbstractItemModel *visible = threadedPresentation() | |
| 2619 |
1/2✗ Branch 41 → 42 not taken.
✓ Branch 41 → 43 taken 5 times.
|
5 | ? static_cast<QAbstractItemModel *>(&m_threadList) |
| 2620 | 5 | : static_cast<QAbstractItemModel *>(&m_mail); | |
| 2621 | 5 | const int unreadRole = threadedPresentation() ? int(QuickThreadListModel::Unread) | |
| 2622 | 5 | : int(MailPresentationModel::Unread); | |
| 2623 |
2/2✓ Branch 46 → 47 taken 5 times.
✓ Branch 47 → 48 taken 5 times.
|
5 | const int current = activeRowForIdentity(WorkspaceState::encode(m_workspace.mailId())); |
| 2624 |
3/3✓ Branch 63 → 64 taken 49 times.
✓ Branch 64 → 50 taken 48 times.
✓ Branch 64 → 65 taken 1 time.
|
49 | for (int step = 1; step <= activeRowCount(); ++step) { |
| 2625 |
1/1✓ Branch 51 → 52 taken 48 times.
|
48 | const int row = (qMax(current, 0) + step) % activeRowCount(); |
| 2626 |
5/5✓ Branch 53 → 54 taken 48 times.
✓ Branch 54 → 55 taken 48 times.
✓ Branch 55 → 56 taken 48 times.
✓ Branch 57 → 58 taken 4 times.
✓ Branch 57 → 62 taken 44 times.
|
48 | if (visible->data(visible->index(row, 0), unreadRole).toBool()) { |
| 2627 |
1/1✓ Branch 58 → 59 taken 4 times.
|
4 | selectMailRow(row); |
| 2628 | 4 | return true; | |
| 2629 | } | ||
| 2630 | } | ||
| 2631 |
2/2✓ Branch 65 → 66 taken 1 time.
✓ Branch 66 → 67 taken 1 time.
|
1 | setStatus(tr("No unread messages")); |
| 2632 | 1 | return true; | |
| 2633 | } | ||
| 2634 | 1073 | return std::nullopt; | |
| 2635 | } | ||
| 2636 | |||
| 2637 | // executeCommand handler: Undo of the last mail action. | ||
| 2638 | 1073 | std::optional<bool> QuickMailBackend::handleUndoCommand(const QString &id) { | |
| 2639 |
6/6✓ Branch 3 → 4 taken 1071 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 5 → 6 taken 5 times.
✓ Branch 5 → 7 taken 1066 times.
✓ Branch 8 → 9 taken 7 times.
✓ Branch 8 → 27 taken 1066 times.
|
1073 | if (id == "undo.normal" || id == "undo.standard") { |
| 2640 |
2/2✓ Branch 9 → 10 taken 2 times.
✓ Branch 9 → 13 taken 5 times.
|
7 | if (m_actionPending) return false; |
| 2641 | // Sprint 98 principle 10: the key and the `undo` command answer either way. | ||
| 2642 |
4/4✓ Branch 15 → 16 taken 2 times.
✓ Branch 15 → 22 taken 3 times.
✓ Branch 16 → 17 taken 2 times.
✓ Branch 17 → 18 taken 2 times.
|
5 | if (!m_session.undo()->canUndo()) { setStatus(tr("Nothing to undo")); return false; } |
| 2643 | 3 | m_session.undo()->undo(); | |
| 2644 | 3 | return true; | |
| 2645 | } | ||
| 2646 | 1066 | return std::nullopt; | |
| 2647 | } | ||
| 2648 | |||
| 2649 | // MAIL-SELECTION-ACTIONS: with more than one message selected in the main | ||
| 2650 | // folder view, the mail actions act on the whole selection. | ||
| 2651 | 125 | std::shared_ptr<const MailSelectionSnapshot> QuickMailBackend::actionSelection() const { | |
| 2652 |
7/8✓ Branch 3 → 4 taken 90 times.
✓ Branch 3 → 8 taken 35 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 90 times.
✓ Branch 10 → 11 taken 90 times.
✓ Branch 10 → 12 taken 35 times.
✓ Branch 12 → 13 taken 35 times.
✓ Branch 12 → 14 taken 90 times.
|
125 | if (m_workspace.activeTab() != 0 || m_workspace.area() != QLatin1String("mail")) return nullptr; |
| 2653 | 90 | const auto selection = m_selection.snapshot(); | |
| 2654 |
4/6✓ Branch 16 → 17 taken 25 times.
✓ Branch 16 → 23 taken 65 times.
✓ Branch 18 → 19 taken 25 times.
✗ Branch 18 → 23 not taken.
✓ Branch 21 → 22 taken 25 times.
✗ Branch 21 → 23 not taken.
|
90 | return selection && selection->count > 1 && selection->folderPath == m_folderPath ? selection : nullptr; |
| 2655 | 90 | } | |
| 2656 | |||
| 2657 | // S101.6.1 (D-13): the marked results of a search tab, grouped by folder. | ||
| 2658 | 168 | QMap<qint64, QList<qint64>> QuickMailBackend::searchBulkGroups() const { | |
| 2659 | 168 | QMap<qint64, QList<qint64>> groups; | |
| 2660 |
3/3✓ Branch 3 → 4 taken 6 times.
✓ Branch 3 → 7 taken 162 times.
✓ Branch 5 → 6 taken 6 times.
|
168 | const MailSearchSession *search = m_workspace.activeIsSearch() ? m_searchTabs->active() : nullptr; |
| 2661 |
6/7✓ Branch 8 → 9 taken 6 times.
✓ Branch 8 → 11 taken 162 times.
✓ Branch 9 → 10 taken 6 times.
✗ Branch 10 → 11 not taken.
✓ Branch 10 → 12 taken 6 times.
✓ Branch 13 → 14 taken 162 times.
✓ Branch 13 → 16 taken 6 times.
|
168 | if (!search || search->markedCount() < 2) return groups; |
| 2662 | // S106-294: a search tab keeps its marks across an account switch; folder | ||
| 2663 | // ids and UIDs only mean something in their own account's cache, so marks | ||
| 2664 | // of another account make no bulk group (the move and state commands then | ||
| 2665 | // have nothing to act on). | ||
| 2666 |
5/5✓ Branch 16 → 17 taken 6 times.
✓ Branch 17 → 18 taken 6 times.
✓ Branch 18 → 19 taken 6 times.
✓ Branch 31 → 20 taken 975 times.
✓ Branch 31 → 32 taken 5 times.
|
980 | for (const MailId &id : search->markedIds()) { |
| 2667 |
2/2✓ Branch 24 → 25 taken 1 time.
✓ Branch 24 → 27 taken 974 times.
|
975 | if (id.accountId != accountName()) return {}; |
| 2668 |
2/2✓ Branch 27 → 28 taken 974 times.
✓ Branch 28 → 29 taken 974 times.
|
974 | groups[id.folderId].append(id.uid); |
| 2669 | 6 | } | |
| 2670 | 5 | return groups; | |
| 2671 | 168 | } | |
| 2672 | |||
| 2673 |
9/14✓ Branch 4 → 5 taken 96 times.
✓ Branch 4 → 8 taken 13 times.
✓ Branch 5 → 6 taken 96 times.
✓ Branch 6 → 7 taken 96 times.
✓ Branch 7 → 8 taken 2 times.
✓ Branch 7 → 9 taken 94 times.
✓ Branch 11 → 12 taken 96 times.
✓ Branch 11 → 13 taken 13 times.
✓ Branch 13 → 14 taken 109 times.
✗ Branch 13 → 15 not taken.
✗ Branch 17 → 18 not taken.
✗ Branch 17 → 19 not taken.
✗ Branch 21 → 22 not taken.
✗ Branch 21 → 23 not taken.
|
109 | bool QuickMailBackend::bulkSelection() const { return actionSelection() || !searchBulkGroups().isEmpty(); } |
| 2674 | |||
| 2675 | 17 | bool QuickMailBackend::actOnSelection(MutationKind kind, const QString &label, bool desired, | |
| 2676 | std::function<void(bool)> done) { | ||
| 2677 |
4/4✓ Branch 2 → 3 taken 17 times.
✓ Branch 3 → 4 taken 17 times.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 33 taken 16 times.
|
17 | if (const auto groups = searchBulkGroups(); !groups.isEmpty()) { |
| 2678 | // One state change per source folder; `done` runs once, after all. | ||
| 2679 |
2/2✓ Branch 5 → 6 taken 1 time.
✓ Branch 6 → 7 taken 1 time.
|
1 | auto remaining = std::make_shared<int>(int(groups.size())); |
| 2680 |
1/1✓ Branch 7 → 8 taken 1 time.
|
1 | auto journaled = std::make_shared<bool>(true); |
| 2681 |
1/1✓ Branch 9 → 10 taken 1 time.
|
1 | auto finish = std::make_shared<std::function<void(bool)>>(std::move(done)); |
| 2682 |
4/4✓ Branch 10 → 11 taken 1 time.
✓ Branch 26 → 27 taken 3 times.
✓ Branch 28 → 12 taken 2 times.
✓ Branch 28 → 29 taken 1 time.
|
3 | for (auto it = groups.cbegin(); it != groups.cend(); ++it) |
| 2683 |
1/1✓ Branch 21 → 22 taken 2 times.
|
4 | m_session.controller()->selectionStates()->setState(nullptr, it.key(), it.value(), kind, label, desired, true, |
| 2684 |
1/5✓ Branch 17 → 18 taken 2 times.
✗ Branch 71 → 72 not taken.
✗ Branch 71 → 73 not taken.
✗ Branch 74 → 75 not taken.
✗ Branch 74 → 76 not taken.
|
4 | [remaining, journaled, finish](bool ok) { |
| 2685 |
2/4✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 6 not taken.
✓ Branch 4 → 5 taken 2 times.
✗ Branch 4 → 6 not taken.
|
2 | *journaled = *journaled && ok; |
| 2686 |
4/6✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 14 taken 1 time.
✗ Branch 12 → 13 not taken.
✓ Branch 12 → 14 taken 1 time.
✗ Branch 15 → 16 not taken.
✓ Branch 15 → 19 taken 2 times.
|
2 | if (--*remaining == 0 && *finish) (*finish)(*journaled); |
| 2687 | 2 | }); | |
| 2688 | 1 | return true; | |
| 2689 | 18 | } | |
| 2690 | 16 | const auto selection = actionSelection(); | |
| 2691 |
2/2✓ Branch 39 → 41 taken 4 times.
✓ Branch 39 → 42 taken 12 times.
|
16 | if (!selection) return false; |
| 2692 |
1/2✗ Branch 43 → 44 not taken.
✓ Branch 43 → 48 taken 12 times.
|
12 | if (m_selection.pending()) { |
| 2693 | ✗ | setStatus(tr("Wait for the selection to finish.")); | |
| 2694 | ✗ | return true; | |
| 2695 | } | ||
| 2696 |
1/1✓ Branch 54 → 55 taken 12 times.
|
12 | m_session.controller()->selectionStates()->setState(selection, -1, {}, kind, label, desired, true, std::move(done)); |
| 2697 | 12 | return true; | |
| 2698 | 16 | } | |
| 2699 | |||
| 2700 | // executeCommand handler: Junk, archive and delete of the selected mail. | ||
| 2701 | 864 | std::optional<bool> QuickMailBackend::handleMailMoveCommand(const QString &id, const MailId &selected) { | |
| 2702 |
2/2✓ Branch 3 → 4 taken 1 time.
✓ Branch 3 → 76 taken 863 times.
|
864 | if (id == "mail.junk") { |
| 2703 | // Widgets parity (T-407): $Junk label plus a journaled move into the | ||
| 2704 | // Junk folder. The removal follow-up selects the neighbouring mail. | ||
| 2705 |
1/4✗ Branch 5 → 6 not taken.
✓ Branch 5 → 12 taken 1 time.
✗ Branch 6 → 7 not taken.
✗ Branch 7 → 8 not taken.
|
1 | if (m_junkFolder.isEmpty()) { setStatus(tr("No Junk folder found")); return true; } |
| 2706 |
3/6✓ Branch 12 → 13 taken 1 time.
✗ Branch 12 → 15 not taken.
✗ Branch 14 → 15 not taken.
✓ Branch 14 → 16 taken 1 time.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 21 taken 1 time.
|
1 | if (m_actionPending || m_selection.pending()) return true; |
| 2707 |
2/3✓ Branch 21 → 22 taken 1 time.
✗ Branch 22 → 23 not taken.
✓ Branch 22 → 41 taken 1 time.
|
1 | if (bulkSelection()) { |
| 2708 | // The label is journaled before the move (different writers). | ||
| 2709 | ✗ | const QPointer<QuickMailBackend> owner(this); | |
| 2710 | ✗ | const QString junk = m_junkFolder; | |
| 2711 | ✗ | actOnSelection(MutationKind::Keyword, QStringLiteral("$Junk"), true, [owner, junk](bool journaled) { | |
| 2712 | ✗ | if (owner && journaled) owner->moveSelectedMail(junk); | |
| 2713 | ✗ | }); | |
| 2714 | ✗ | return true; | |
| 2715 | ✗ | } | |
| 2716 |
3/5✓ Branch 44 → 45 taken 1 time.
✓ Branch 47 → 48 taken 1 time.
✓ Branch 47 → 49 taken 1 time.
✗ Branch 181 → 182 not taken.
✗ Branch 181 → 183 not taken.
|
3 | const auto junkHeaders = m_session.cache()->headersByIds({selected}); |
| 2717 |
1/1✓ Branch 54 → 55 taken 1 time.
|
2 | m_session.controller()->addLabelInFolder(selected.uid, selected.folderId, QStringLiteral("$Junk")); |
| 2718 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | moveCurrentMailToFolder(m_junkFolder); |
| 2719 |
3/3✓ Branch 59 → 60 taken 1 time.
✓ Branch 61 → 62 taken 1 time.
✓ Branch 62 → 63 taken 1 time.
|
2 | setStatus(tr("Marked as junk → %1").arg(m_junkFolder)); |
| 2720 | // S95.5.2: offer a rule for the sender (only with a rule server). | ||
| 2721 |
3/4✓ Branch 66 → 67 taken 1 time.
✗ Branch 66 → 72 not taken.
✓ Branch 68 → 69 taken 1 time.
✓ Branch 69 → 70 taken 1 time.
|
1 | if (!junkHeaders.isEmpty()) m_rules.offerJunkRule(RuleDraftFactory::bareAddress(junkHeaders.first().from)); |
| 2722 | 1 | return true; | |
| 2723 | 1 | } | |
| 2724 |
2/2✓ Branch 77 → 78 taken 10 times.
✓ Branch 77 → 104 taken 853 times.
|
863 | if (id == "mail.archive") { |
| 2725 |
1/4✗ Branch 79 → 80 not taken.
✓ Branch 79 → 86 taken 10 times.
✗ Branch 80 → 81 not taken.
✗ Branch 81 → 82 not taken.
|
10 | if (m_archiveFolder.isEmpty()) { setStatus(tr("No Archive folder found")); return true; } |
| 2726 |
2/2✓ Branch 87 → 88 taken 2 times.
✓ Branch 87 → 92 taken 8 times.
|
10 | if (bulkSelection()) { moveSelectedMail(m_archiveFolder); return true; } |
| 2727 |
1/2✓ Branch 93 → 94 taken 8 times.
✗ Branch 93 → 101 not taken.
|
8 | if (moveCurrentMailToFolder(m_archiveFolder)) |
| 2728 |
3/3✓ Branch 94 → 95 taken 8 times.
✓ Branch 96 → 97 taken 8 times.
✓ Branch 97 → 98 taken 8 times.
|
16 | setStatus(tr("Archived → %1").arg(m_archiveFolder)); |
| 2729 | 8 | return true; | |
| 2730 | } | ||
| 2731 |
2/2✓ Branch 105 → 106 taken 4 times.
✓ Branch 105 → 151 taken 849 times.
|
853 | if (id == "mail.delete") { |
| 2732 |
1/4✗ Branch 107 → 108 not taken.
✓ Branch 107 → 114 taken 4 times.
✗ Branch 108 → 109 not taken.
✗ Branch 109 → 110 not taken.
|
4 | if (m_trashFolder.isEmpty()) { setStatus(tr("No Trash folder found")); return true; } |
| 2733 | // F-09 (SPRINT-104, D-4): `d` IN the Trash deletes for good — with | ||
| 2734 | // an explicit second press as the confirmation and no Undo (IMAP | ||
| 2735 | // cannot undo an EXPUNGE). The first press only arms. | ||
| 2736 |
1/2✗ Branch 115 → 116 not taken.
✓ Branch 115 → 133 taken 4 times.
|
4 | if (m_folderPath == m_trashFolder) { |
| 2737 | ✗ | ensureDeleteForever(); | |
| 2738 | ✗ | if (!m_deleteForever->armOrDelete(selected, deleteForeverUids(selected))) | |
| 2739 | ✗ | setStatus(tr("Delete for good? Press %1 again — this cannot be undone.").arg(tr("d"))); | |
| 2740 | ✗ | return true; | |
| 2741 | } | ||
| 2742 |
2/2✓ Branch 134 → 135 taken 1 time.
✓ Branch 134 → 139 taken 3 times.
|
4 | if (bulkSelection()) { moveSelectedMail(m_trashFolder); return true; } |
| 2743 |
1/2✓ Branch 140 → 141 taken 3 times.
✗ Branch 140 → 148 not taken.
|
3 | if (moveCurrentMailToFolder(m_trashFolder)) |
| 2744 |
3/3✓ Branch 141 → 142 taken 3 times.
✓ Branch 143 → 144 taken 3 times.
✓ Branch 144 → 145 taken 3 times.
|
6 | setStatus(tr("Deleted → %1").arg(m_trashFolder)); |
| 2745 | 3 | return true; | |
| 2746 | } | ||
| 2747 | 849 | return std::nullopt; | |
| 2748 | 1 | } | |
| 2749 | |||
| 2750 | // executeCommand handler: Open the selected mail and cycle its reading mode. | ||
| 2751 | 849 | std::optional<bool> QuickMailBackend::handleMailViewCommand(const QString &id, const MailId &selected) { | |
| 2752 |
1/2✗ Branch 3 → 4 not taken.
✓ Branch 3 → 8 taken 849 times.
|
849 | if (id == "mail.open") { |
| 2753 | // Explicit open/read trigger (o): identical to the selection path. | ||
| 2754 | ✗ | requestSelectedMail(selected); | |
| 2755 | ✗ | return true; | |
| 2756 | } | ||
| 2757 |
5/6✓ Branch 9 → 10 taken 846 times.
✓ Branch 9 → 12 taken 3 times.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 846 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 14 → 50 taken 846 times.
|
849 | if (id == "mail.reading_mode" || id == "mail.reading_mode_back") { |
| 2758 | // Cycle the effective reading mode: text → light → html → text | ||
| 2759 | // (Sprint 105: Shift+H the other way round). | ||
| 2760 |
5/10✓ Branch 15 → 16 taken 1 time.
✓ Branch 15 → 37 taken 2 times.
✓ Branch 17 → 18 taken 1 time.
✗ Branch 17 → 37 not taken.
✓ Branch 31 → 32 taken 3 times.
✓ Branch 31 → 33 taken 1 time.
✗ Branch 55 → 56 not taken.
✗ Branch 55 → 57 not taken.
✗ Branch 73 → 74 not taken.
✗ Branch 73 → 75 not taken.
|
7 | static const QStringList modes = {QStringLiteral("text"), QStringLiteral("light"), QStringLiteral("html")}; |
| 2761 | 3 | const int current = std::max<int>(0, int(modes.indexOf(m_workspace.readingMode()))); | |
| 2762 |
1/2✓ Branch 42 → 43 taken 3 times.
✗ Branch 42 → 44 not taken.
|
3 | const int step = id == "mail.reading_mode" ? 1 : 2; |
| 2763 | 3 | m_workspace.setReadingMode(modes.at((current + step) % 3)); | |
| 2764 | 3 | return true; | |
| 2765 | } | ||
| 2766 | 846 | return std::nullopt; | |
| 2767 | 4 | } | |
| 2768 | |||
| 2769 | // executeCommand handler: Toggle a standard label or clear all labels of the selected mail. | ||
| 2770 | 846 | std::optional<bool> QuickMailBackend::handleLabelCommand(const QString &id, const MailId &selected) { | |
| 2771 |
7/10✓ Branch 6 → 7 taken 5 times.
✓ Branch 6 → 15 taken 3 times.
✓ Branch 8 → 9 taken 4 times.
✓ Branch 8 → 15 taken 1 time.
✓ Branch 10 → 11 taken 4 times.
✗ Branch 10 → 15 not taken.
✓ Branch 18 → 19 taken 4 times.
✓ Branch 18 → 54 taken 842 times.
✗ Branch 127 → 128 not taken.
✗ Branch 127 → 129 not taken.
|
858 | if (id.startsWith("label.") && id != "label.clear" && id != "label.menu" && |
| 2772 |
6/8✓ Branch 2 → 3 taken 846 times.
✓ Branch 3 → 4 taken 846 times.
✓ Branch 4 → 5 taken 8 times.
✓ Branch 4 → 15 taken 838 times.
✓ Branch 13 → 14 taken 4 times.
✗ Branch 13 → 15 not taken.
✓ Branch 16 → 17 taken 846 times.
✗ Branch 16 → 18 not taken.
|
858 | id.size() == 7 && id.at(6).isDigit()) { |
| 2773 | // Toggle one standard IMAP label ($label1..5). | ||
| 2774 |
1/1✓ Branch 23 → 24 taken 4 times.
|
8 | const QString keyword = QStringLiteral("$label") + id.at(6); |
| 2775 |
1/1✓ Branch 26 → 27 taken 4 times.
|
4 | const QStringList labels = selectedLabels(); |
| 2776 | // A selection follows the current message: it has the label → remove it from all. | ||
| 2777 |
2/3✓ Branch 29 → 30 taken 4 times.
✗ Branch 31 → 32 not taken.
✓ Branch 31 → 35 taken 4 times.
|
4 | if (actOnSelection(MutationKind::Keyword, keyword, !labels.contains(keyword, Qt::CaseInsensitive))) return true; |
| 2778 |
2/2✓ Branch 36 → 37 taken 1 time.
✓ Branch 36 → 42 taken 3 times.
|
4 | if (labels.contains(keyword, Qt::CaseInsensitive)) |
| 2779 |
1/1✓ Branch 39 → 40 taken 1 time.
|
1 | m_session.controller()->removeLabelInFolder(selected.uid, selected.folderId, keyword, labels); |
| 2780 | else | ||
| 2781 |
1/1✓ Branch 44 → 45 taken 3 times.
|
3 | m_session.controller()->addLabelInFolder(selected.uid, selected.folderId, keyword, labels); |
| 2782 |
1/1✓ Branch 47 → 48 taken 4 times.
|
4 | emit selectedLabelsChanged(); |
| 2783 | 4 | return true; | |
| 2784 | 4 | } | |
| 2785 |
2/2✓ Branch 55 → 56 taken 3 times.
✓ Branch 55 → 120 taken 839 times.
|
842 | if (id == "label.clear") { |
| 2786 |
1/1✓ Branch 56 → 57 taken 3 times.
|
3 | const QStringList labels = selectedLabels(); |
| 2787 |
2/3✓ Branch 57 → 58 taken 3 times.
✗ Branch 58 → 59 not taken.
✓ Branch 58 → 99 taken 3 times.
|
3 | if (bulkSelection()) { |
| 2788 | // The standard labels and every label of the current message, from all. | ||
| 2789 | ✗ | QStringList keywords; | |
| 2790 | ✗ | for (int i = 1; i <= 5; ++i) keywords << QStringLiteral("$label%1").arg(i); | |
| 2791 | ✗ | for (const auto &label : labels) | |
| 2792 | ✗ | if (!ImapResponseParser::isInternalKeyword(label) && !keywords.contains(label, Qt::CaseInsensitive)) keywords << label; | |
| 2793 | ✗ | for (const auto &keyword : keywords) actOnSelection(MutationKind::Keyword, keyword, false); | |
| 2794 | ✗ | return true; | |
| 2795 | ✗ | } | |
| 2796 | // Each removal commits visually at once; the next call sees the rest. | ||
| 2797 | 3 | QStringList remaining = labels; | |
| 2798 |
2/2✓ Branch 113 → 102 taken 2 times.
✓ Branch 113 → 114 taken 3 times.
|
5 | for (const auto &label : labels) { |
| 2799 |
2/3✓ Branch 103 → 104 taken 2 times.
✗ Branch 104 → 105 not taken.
✓ Branch 104 → 106 taken 2 times.
|
2 | if (ImapResponseParser::isInternalKeyword(label)) continue; |
| 2800 |
1/1✓ Branch 108 → 109 taken 2 times.
|
2 | m_session.controller()->removeLabelInFolder(selected.uid, selected.folderId, label, remaining); |
| 2801 |
1/1✓ Branch 110 → 111 taken 2 times.
|
2 | remaining.removeAll(label); |
| 2802 | } | ||
| 2803 |
1/1✓ Branch 114 → 115 taken 3 times.
|
3 | emit selectedLabelsChanged(); |
| 2804 | 3 | return true; | |
| 2805 | 3 | } | |
| 2806 | 839 | return std::nullopt; | |
| 2807 | } | ||
| 2808 | |||
| 2809 | // executeCommand handler: Seen/starred toggles of the selected mail (last handler). | ||
| 2810 | 839 | std::optional<bool> QuickMailBackend::handleFlagCommand(const QString &id, const MailId &selected) { | |
| 2811 |
9/9✓ Branch 3 → 4 taken 837 times.
✓ Branch 3 → 6 taken 2 times.
✓ Branch 5 → 6 taken 29 times.
✓ Branch 5 → 9 taken 808 times.
✓ Branch 6 → 7 taken 31 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 30 times.
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 37 taken 838 times.
|
839 | if ((id == "mail.toggle_read" || id == "mail.toggle_star") && bulkSelection()) { |
| 2812 | // A selection follows the current message: read → all unread, and so on. | ||
| 2813 |
1/1✓ Branch 12 → 13 taken 1 time.
|
1 | const auto header = m_session.cache()->header(selected.folderId, selected.uid); |
| 2814 | 1 | const bool read = id == "mail.toggle_read"; | |
| 2815 |
3/8✓ Branch 15 → 16 taken 1 time.
✗ Branch 15 → 24 not taken.
✓ Branch 16 → 17 taken 1 time.
✗ Branch 16 → 20 not taken.
✓ Branch 19 → 23 taken 1 time.
✗ Branch 19 → 24 not taken.
✗ Branch 22 → 23 not taken.
✗ Branch 22 → 24 not taken.
|
1 | const bool current = header && (read ? header->isSeen() : header->isFlagged()); |
| 2816 |
2/3✓ Branch 27 → 28 taken 1 time.
✗ Branch 27 → 29 not taken.
✓ Branch 30 → 31 taken 1 time.
|
1 | return actOnSelection(read ? MutationKind::Seen : MutationKind::Flagged, {}, !current); |
| 2817 | 1 | } | |
| 2818 |
2/2✓ Branch 38 → 39 taken 1 time.
✓ Branch 38 → 60 taken 837 times.
|
838 | if (id == "mail.toggle_read") { |
| 2819 | 1 | std::optional<bool> visibleSeen; | |
| 2820 |
2/6✗ Branch 40 → 41 not taken.
✓ Branch 40 → 44 taken 1 time.
✗ Branch 42 → 43 not taken.
✗ Branch 42 → 44 not taken.
✗ Branch 45 → 46 not taken.
✓ Branch 45 → 57 taken 1 time.
|
1 | if (threadedPresentation() && m_threadList.indexed()) { |
| 2821 | ✗ | const int row = m_threadList.rowForIdentity(WorkspaceState::encode(selected)); | |
| 2822 | ✗ | if (row >= 0) | |
| 2823 | ✗ | visibleSeen = !m_threadList.data(m_threadList.index(row, 0), | |
| 2824 | ✗ | QuickThreadListModel::Unread).toBool(); | |
| 2825 | } | ||
| 2826 |
1/1✓ Branch 58 → 59 taken 1 time.
|
1 | m_session.controller()->toggleReadStatusInFolder(selected.uid, selected.folderId, visibleSeen); |
| 2827 | } | ||
| 2828 |
2/2✓ Branch 61 → 62 taken 29 times.
✓ Branch 61 → 83 taken 808 times.
|
837 | else if (id == "mail.toggle_star") { |
| 2829 | 29 | std::optional<bool> visibleStarred; | |
| 2830 |
2/6✗ Branch 63 → 64 not taken.
✓ Branch 63 → 67 taken 29 times.
✗ Branch 65 → 66 not taken.
✗ Branch 65 → 67 not taken.
✗ Branch 68 → 69 not taken.
✓ Branch 68 → 80 taken 29 times.
|
29 | if (threadedPresentation() && m_threadList.indexed()) { |
| 2831 | ✗ | const int row = m_threadList.rowForIdentity(WorkspaceState::encode(selected)); | |
| 2832 | ✗ | if (row >= 0) | |
| 2833 | ✗ | visibleStarred = m_threadList.data(m_threadList.index(row, 0), | |
| 2834 | ✗ | QuickThreadListModel::Starred).toBool(); | |
| 2835 | } | ||
| 2836 |
1/1✓ Branch 81 → 82 taken 29 times.
|
29 | m_session.controller()->toggleStarredInFolder(selected.uid, selected.folderId, |
| 2837 | visibleStarred); | ||
| 2838 | } | ||
| 2839 | 808 | else return false; | |
| 2840 | 30 | return true; | |
| 2841 | } | ||
| 2842 | |||
| 2843 | // Ordered command handlers: the first handler that recognizes the command | ||
| 2844 | // decides (the order is part of the behaviour — e.g. Escape first closes the | ||
| 2845 | // suggestions, then a search). Mail handlers run only for an admitted | ||
| 2846 | // selected mail. S91.2.7: formerly one 235-line if-chain. | ||
| 2847 | 1184 | bool QuickMailBackend::executeCommand(const QString &id) { | |
| 2848 | using Handler = std::optional<bool> (QuickMailBackend::*)(const QString &); | ||
| 2849 | static constexpr Handler beforeSelection[] = { | ||
| 2850 | &QuickMailBackend::handleSuggestionCommand, | ||
| 2851 | &QuickMailBackend::handleSurfaceClose, | ||
| 2852 | &QuickMailBackend::handleIndexedNavigation, | ||
| 2853 | &QuickMailBackend::handleSearchTabNavigation, | ||
| 2854 | &QuickMailBackend::handleTabNavigation, | ||
| 2855 | &QuickMailBackend::handleMainListNavigation, | ||
| 2856 | &QuickMailBackend::handleViewCommand, | ||
| 2857 | &QuickMailBackend::handleUndoCommand, | ||
| 2858 | }; | ||
| 2859 |
2/2✓ Branch 13 → 3 taken 8922 times.
✓ Branch 13 → 14 taken 1066 times.
|
9988 | for (const Handler handler : beforeSelection) |
| 2860 |
4/5✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 8922 times.
✓ Branch 6 → 7 taken 8922 times.
✓ Branch 8 → 9 taken 118 times.
✓ Branch 8 → 11 taken 8804 times.
|
8922 | if (const auto handled = (this->*handler)(id)) return *handled; |
| 2861 | 1066 | const auto selected = m_workspace.mailId(); | |
| 2862 |
3/3✓ Branch 16 → 17 taken 1066 times.
✓ Branch 17 → 18 taken 202 times.
✓ Branch 17 → 19 taken 864 times.
|
1066 | if (!acceptsIdentity(selected)) return false; |
| 2863 | using MailHandler = std::optional<bool> (QuickMailBackend::*)(const QString &, const MailId &); | ||
| 2864 | static constexpr MailHandler forSelectedMail[] = { | ||
| 2865 | &QuickMailBackend::handleMailMoveCommand, | ||
| 2866 | &QuickMailBackend::handleMailViewCommand, | ||
| 2867 | &QuickMailBackend::handleLabelCommand, | ||
| 2868 | &QuickMailBackend::handleFlagCommand, | ||
| 2869 | }; | ||
| 2870 |
1/2✓ Branch 30 → 20 taken 3398 times.
✗ Branch 30 → 31 not taken.
|
3398 | for (const MailHandler handler : forSelectedMail) |
| 2871 |
4/5✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 3398 times.
✓ Branch 23 → 24 taken 3398 times.
✓ Branch 25 → 26 taken 864 times.
✓ Branch 25 → 28 taken 2534 times.
|
3398 | if (const auto handled = (this->*handler)(id, selected)) return *handled; |
| 2872 | ✗ | return false; | |
| 2873 | 1066 | } | |
| 2874 | |||
| 2875 | 12 | bool QuickMailBackend::moveCurrentMailToFolder(const QString &targetFolder) { | |
| 2876 |
4/8✓ Branch 3 → 4 taken 12 times.
✗ Branch 3 → 7 not taken.
✓ Branch 4 → 5 taken 12 times.
✗ Branch 4 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 12 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 12 times.
|
12 | if (targetFolder.isEmpty() || m_actionPending || m_selection.pending()) return false; |
| 2877 | 12 | const auto selected = m_workspace.mailId(); | |
| 2878 |
5/8✓ Branch 13 → 14 taken 12 times.
✓ Branch 14 → 15 taken 12 times.
✗ Branch 14 → 17 not taken.
✓ Branch 15 → 16 taken 12 times.
✗ Branch 16 → 17 not taken.
✓ Branch 16 → 18 taken 12 times.
✗ Branch 19 → 20 not taken.
✓ Branch 19 → 21 taken 12 times.
|
12 | if (!acceptsIdentity(selected) || !m_folders.isSelectable(targetFolder)) return false; |
| 2879 | // P90.16: reserve the surviving neighbour before the removal lands. | ||
| 2880 |
3/3✓ Branch 21 → 22 taken 12 times.
✓ Branch 22 → 23 taken 12 times.
✓ Branch 23 → 24 taken 12 times.
|
12 | reserveActionContinuity(activeRowForIdentity(WorkspaceState::encode(selected))); |
| 2881 |
2/2✓ Branch 29 → 30 taken 12 times.
✓ Branch 30 → 31 taken 12 times.
|
24 | m_session.controller()->moves()->moveMailsToFolderFrom({selected.uid}, selected.folderId, |
| 2882 |
1/1✓ Branch 28 → 29 taken 12 times.
|
24 | m_session.cache()->folderPath(selected.folderId), targetFolder); |
| 2883 | 12 | return true; | |
| 2884 | 12 | } | |
| 2885 | |||
| 2886 | // ─── Q06: independent compose sessions ─── | ||
| 2887 | |||
| 2888 | |||
| 2889 | |||
| 2890 | |||
| 2891 | // ─── Q06: per-session attachments ─── | ||
| 2892 | |||
| 2893 | // ── Folder properties (appearance, subscription, cache) ── | ||
| 2894 | |||
| 2895 | // P90 "first usable page" (cold/repair p95 <= 200 ms): a first indexed page | ||
| 2896 | // that is not ready after ProvisionalPageDelayMs is a thread-index build. | ||
| 2897 | // Show the folder's cached first page (date order) meanwhile; the indexed | ||
| 2898 | // page replaces it. | ||
| 2899 | 5 | void QuickMailBackend::requestProvisionalPage() { | |
| 2900 | 5 | const qint64 folderId = m_threadList.indexedFolder(); | |
| 2901 |
4/7✓ Branch 4 → 5 taken 5 times.
✗ Branch 4 → 7 not taken.
✓ Branch 5 → 6 taken 5 times.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 5 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 5 times.
|
5 | if (!threadedPresentation() || !m_threadList.provisionalPageWanted(folderId)) return; |
| 2902 |
1/1✓ Branch 12 → 13 taken 5 times.
|
5 | const auto state = m_session.cache()->folderPresentationState(folderId); |
| 2903 |
1/1✓ Branch 14 → 15 taken 5 times.
|
5 | const QString path = m_session.cache()->folderPath(folderId); |
| 2904 |
3/6✓ Branch 16 → 17 taken 5 times.
✗ Branch 16 → 18 not taken.
✗ Branch 17 → 18 not taken.
✓ Branch 17 → 19 taken 5 times.
✗ Branch 20 → 21 not taken.
✓ Branch 20 → 22 taken 5 times.
|
5 | if (path.isEmpty() || state.uidValidity == 0) return; |
| 2905 | 10 | MailHeaderPageRequest request{accountName(), path, folderId, state.uidValidity, 0, {}, false, | |
| 2906 | 5 | MailHeaderPageSnapshot::MaximumRows}; | |
| 2907 |
1/1✓ Branch 26 → 27 taken 5 times.
|
5 | const QPointer<QuickMailBackend> owner(this); |
| 2908 | 5 | const auto epoch = state.uidValidity; | |
| 2909 | 5 | const auto generation = m_session.accounts()->generation(); | |
| 2910 |
2/2✓ Branch 29 → 30 taken 5 times.
✓ Branch 35 → 36 taken 5 times.
|
20 | MailDisplayReadService::shared()->readHeaderPage(&m_provisionalPageOwner, |
| 2911 | 10 | m_session.cache()->databasePath(), request, | |
| 2912 |
1/3✓ Branch 31 → 32 taken 5 times.
✗ Branch 59 → 60 not taken.
✗ Branch 59 → 61 not taken.
|
10 | [owner, folderId, epoch, generation](MailHeaderPageSnapshot snapshot) { |
| 2913 |
3/6✓ Branch 3 → 4 taken 5 times.
✗ Branch 3 → 9 not taken.
✓ Branch 7 → 8 taken 5 times.
✗ Branch 7 → 9 not taken.
✗ Branch 11 → 12 not taken.
✓ Branch 11 → 13 taken 5 times.
|
10 | if (!owner || generation != owner->m_session.accounts()->generation() || |
| 2914 |
1/2✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 5 times.
|
5 | snapshot.outcome != MailHeaderPageSnapshot::Outcome::Ready) return; |
| 2915 |
4/4✓ Branch 13 → 14 taken 5 times.
✓ Branch 14 → 15 taken 5 times.
✓ Branch 19 → 16 taken 1000 times.
✓ Branch 19 → 20 taken 5 times.
|
1005 | for (auto &header : snapshot.headers) header.folderId = folderId; |
| 2916 |
2/2✓ Branch 22 → 23 taken 3 times.
✓ Branch 22 → 25 taken 2 times.
|
5 | if (owner->m_threadList.showProvisionalPage(folderId, epoch, snapshot.headers)) |
| 2917 | 3 | owner->clearThreadProjectionSlowFlag(); | |
| 2918 | }); | ||
| 2919 | 5 | } | |
| 2920 | |||
| 2921 | 981 | void QuickMailBackend::refreshTaskLinks() { | |
| 2922 |
1/1✓ Branch 2 → 3 taken 981 times.
|
981 | auto ids = m_pim.openTaskMessageIds(); |
| 2923 |
2/2✓ Branch 4 → 5 taken 952 times.
✓ Branch 4 → 6 taken 29 times.
|
981 | if (ids == m_openTaskMessageIds) return; |
| 2924 | 29 | m_openTaskMessageIds = std::move(ids); | |
| 2925 |
1/1✓ Branch 8 → 9 taken 29 times.
|
29 | m_mail.taskLinksChanged(); |
| 2926 |
1/1✓ Branch 9 → 10 taken 29 times.
|
29 | m_threadList.taskLinksChanged(); |
| 2927 | 981 | } | |
| 2928 |