aboutsummaryrefslogtreecommitdiffstats
path: root/src/mainwindow.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/mainwindow.cpp')
-rw-r--r--src/mainwindow.cpp141
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));