diff options
Diffstat (limited to 'src/mainwindow.cpp')
| -rw-r--r-- | src/mainwindow.cpp | 141 |
1 files changed, 138 insertions, 3 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 4377870..8b1c537 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -118,6 +118,21 @@ namespace { /// Overridden only by setLocksPathForTesting(); "/proc/locks" in every real run. QString g_locksPath = QStringLiteral("/proc/locks"); +/// A rule as single-tag operations: `+a`, `+b`, `-c`, each as (add, remove). +/// Item 212: the worker's effective set is per MESSAGE, not per tag, so one +/// `+promo -inbox` write reports a message that already had promo (it lost +/// inbox) and its undo would strip promo from it. One operation per tag makes +/// each effective set, and therefore each undo, exact. +QList<QPair<QStringList, QStringList>> ruleOperations(const TagRule &rule) +{ + QList<QPair<QStringList, QStringList>> ops; + for (const QString &tag : rule.add) + ops.append({ { tag }, {} }); + for (const QString &tag : rule.remove) + ops.append({ {}, { tag } }); + return ops; +} + } // namespace /// Doc comment on the declaration. Separators and control characters are @@ -2808,6 +2823,9 @@ void MainWindow::showTagRulesDialog(const TagRule &seed) connect(dialog, &TagRulesDialog::previewRequested, this, &MainWindow::onRulePreviewRequested); + connect(dialog, &TagRulesDialog::applyRequested, + this, &MainWindow::applyRuleToExistingMail); + connect(dialog, &TagRulesDialog::countsRequested, this, [this, dialog]() { QMetaObject::invokeMethod( m_worker, "requestMessageCounts", Qt::QueuedConnection, @@ -2821,6 +2839,16 @@ void MainWindow::showTagRulesDialog(const TagRule &seed) void MainWindow::onRuleCountsReady(const QVector<int> &counts, quint64 generation) { + // Item 212. Apply's count first: it shares this signal and the generation + // sequence, and needs no dialog, since the test and a closed dialog's + // last Apply both arrive here without one. + if (m_pendingApplyGeneration != 0 + && generation == m_pendingApplyGeneration) { + m_pendingApplyGeneration = 0; + confirmRuleApply(counts); + return; + } + // Stale reply, or the dialog closed while the count was in flight. Both // are ordinary rather than rare: counting every rule against a cold index // takes seconds, which is long enough for the user to close the dialog or @@ -2831,6 +2859,108 @@ void MainWindow::onRuleCountsReady(const QVector<int> &counts, m_tagRulesDialog->setCounts(counts); } +void MainWindow::applyRuleToExistingMail(const TagRule &rule) +{ + if (!m_worker) + return; + + // Refused rather than held: HeldEdit replays ids, and a backfill has none + // until the worker resolves the query. A deliberate deviation from the + // spec, recorded there. + if (aSyncHoldsTheWriteLock()) { + showTransientStatus( + tr("A sync is running; apply the rule again when it finishes.")); + return; + } + + // One count for the whole rule (what the confirmation names), then one per + // single-tag operation, in the order ruleOperations() lists them. The + // per-operation counts decide which writes are sent at all; see + // confirmRuleApply() for why the rule is split. + QStringList queries{ SearchTerm::changes(rule.query, rule.add, rule.remove) }; + for (const auto &op : ruleOperations(rule)) + queries.append(SearchTerm::changes(rule.query, op.first, op.second)); + + m_pendingApplyRule = rule; + m_pendingApplyGeneration = ++m_ruleCountGeneration; + QMetaObject::invokeMethod( + m_worker, "requestMessageCounts", Qt::QueuedConnection, + Q_ARG(QStringList, queries), + Q_ARG(quint64, m_pendingApplyGeneration)); +} + +void MainWindow::confirmRuleApply(const QVector<int> &counts) +{ + const TagRule rule = m_pendingApplyRule; + const int count = counts.value(0, 0); + if (count <= 0) { + showTransientStatus( + tr("Rule \"%1\" has nothing to change.").arg(rule.id)); + return; + } + + // The one confirmation for a reversible write, and why: the scope is the + // whole index, so the user cannot see what it will touch. + QStringList what; + if (!rule.add.isEmpty()) + what.append(tr("Adds: %1.").arg(rule.add.join(QStringLiteral(", ")))); + if (!rule.remove.isEmpty()) + what.append(tr("Removes: %1.").arg(rule.remove.join(QStringLiteral(", ")))); + + QWidget *parent = m_tagRulesDialog ? static_cast<QWidget *>(m_tagRulesDialog) + : this; + QMessageBox box(parent); + box.setIcon(QMessageBox::Question); + box.setWindowTitle(tr("Apply rule")); + box.setText(tr("Apply rule \"%1\" to %n message(s)?", "", count).arg(rule.id)); + box.setInformativeText(what.join(QStringLiteral(" ")) + + QStringLiteral("\n") + + tr("You can undo this from the main window.")); + box.setStandardButtons(QMessageBox::Yes | QMessageBox::Cancel); + box.setDefaultButton(QMessageBox::Cancel); + if (box.exec() != QMessageBox::Yes) + return; + + // The box ran a nested event loop: a sync may have started meanwhile, and + // a read-write open would then block the worker behind its lock. + if (aSyncHoldsTheWriteLock()) { + showTransientStatus( + tr("A sync is running; apply the rule again when it finishes.")); + return; + } + + const QString description = tr("Apply rule \"%1\"").arg(rule.id); + + // One write per single-tag operation, grouped so one Ctrl+Z reverts the + // whole rule (see ruleOperations() for why it is split). An operation + // whose count was 0 is NOT sent: applyTags() stays silent on an empty + // write, and an armed command that never hears back would steal the next + // write's confirmation. Commands are built with no ids; the worker + // resolves them and the confirmation fills each one (item 176). + const auto ops = ruleOperations(rule); + m_undoStack.beginMacro(description); + bool first = true; + for (int i = 0; i < ops.size(); ++i) { + if (counts.value(i + 1, 0) <= 0) + continue; + auto *command = new MessageTagCommand(this, {}, ops.at(i).first, + ops.at(i).second, description); + awaitTagConfirmation(command, first); + first = false; + QMetaObject::invokeMethod( + m_worker, "applyTagsToQuery", Qt::QueuedConnection, + Q_ARG(QString, rule.query), Q_ARG(QStringList, ops.at(i).first), + Q_ARG(QStringList, ops.at(i).second), Q_ARG(QString, description)); + m_undoStack.push(command); + } + m_undoStack.endMacro(); + + // The rows a backfill touches are mostly not in the model, so the list is + // re-queried once the write has run rather than updated optimistically. + announceAction(tr("%1: %n message(s)", "", count).arg(description), + [this]() { refreshCurrentQuery(); }); +} + void MainWindow::showAbout() { QDialog dialog(this); @@ -5877,11 +6007,14 @@ void MainWindow::onExternalSyncStateChanged(SyncMonitor::State state) } -void MainWindow::announceAction(const QString &text) +void MainWindow::announceAction(const QString &text, + const std::function<void()> &onDone) { if (!aSyncHoldsTheWriteLock()) { if (!m_worker) { showTransientStatus(text); + if (onDone) + onDone(); return; } @@ -5899,8 +6032,10 @@ void MainWindow::announceAction(const QString &text) m_statusTimer->stop(); m_statusLabel->setText(tr("%1, applying…").arg(text)); const quint64 ticket = ++m_announceTicket; - QMetaObject::invokeMethod(m_worker, [this, text, ticket]() { - QMetaObject::invokeMethod(this, [this, text, ticket]() { + QMetaObject::invokeMethod(m_worker, [this, text, ticket, onDone]() { + QMetaObject::invokeMethod(this, [this, text, ticket, onDone]() { + if (onDone) + onDone(); // A newer announcement, or an error, owns the label now. if (ticket == m_announceTicket) showTransientStatus(tr("%1, done").arg(text)); |
