aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/tagrulesdialog.cpp59
-rw-r--r--src/tagrulesdialog.h12
-rw-r--r--tests/test_tagrules.cpp69
3 files changed, 135 insertions, 5 deletions
diff --git a/src/tagrulesdialog.cpp b/src/tagrulesdialog.cpp
index f208527..e037e37 100644
--- a/src/tagrulesdialog.cpp
+++ b/src/tagrulesdialog.cpp
@@ -45,8 +45,21 @@
namespace {
/// Columns of the rule list.
+///
+/// Item 102 added ColumnNote, and the user asked for it LAST, after Matches:
+/// a note is prose and the widest thing here, so it belongs at the end where
+/// it can run on without pushing the narrow columns off screen.
+///
+/// "Matches" is the counts column and is the one that is NOT in this enum: it
+/// sits at index ColumnCount, appended past the end. That is why Note is
+/// declared before ColumnCount and still draws after Matches, and why
+/// ColumnNote must be given its index explicitly rather than left to follow
+/// ColumnTags. Get this wrong and the counts land in the Note column, which
+/// no test would call an error since both hold text.
enum Column { ColumnEnabled = 0, ColumnStage, ColumnId, ColumnTags,
- ColumnCount };
+ ColumnCount, ///< "Matches", filled by the preview
+ ColumnNote, ///< last, per the user
+ ColumnTotal };
QStringList splitTags(const QString &text)
{
@@ -112,9 +125,11 @@ TagRulesDialog::TagRulesDialog(const TagRule &seed, QWidget *parent)
layout->addWidget(intro);
m_list = new QTreeWidget(this);
- m_list->setColumnCount(ColumnCount + 1);
+ // ColumnTotal, not ColumnCount + 1: the counts column used to be the one
+ // past the end, and since item 102 put Note after it there are two.
+ m_list->setColumnCount(ColumnTotal);
m_list->setHeaderLabels({ tr("On"), tr("Stage"), tr("Rule"), tr("Tags"),
- tr("Matches") });
+ tr("Matches"), tr("Note") });
m_list->setRootIsDecorated(false);
m_list->setUniformRowHeights(true);
@@ -390,8 +405,14 @@ void TagRulesDialog::restoreUiState()
const QByteArray header =
state.value(QStringLiteral("tagrules/header")).toByteArray();
- if (!header.isEmpty()) {
- m_list->header()->restoreState(header);
+ // Only when the restore actually took. QHeaderView::restoreState REFUSES a
+ // state saved with a different column count, returning false and leaving
+ // the header untouched, which is exactly what every existing state file
+ // does now that item 102 added a column. Setting the sized flags anyway
+ // would spend the one auto-size each column gets on a restore that did
+ // nothing, and the new Note column would open at whatever width it
+ // defaulted to, once, for good.
+ if (!header.isEmpty() && m_list->header()->restoreState(header)) {
// Counts as the one auto-size each column gets, so the restore is not
// immediately overwritten. reloadList() runs before this in the
// constructor and has already sized the first two; the count column
@@ -482,6 +503,26 @@ void TagRulesDialog::setColumnWidthForTest(int column, int width)
m_list->setColumnWidth(column, width);
}
+QString TagRulesDialog::listCellTextForTest(int row, int column) const
+{
+ const QTreeWidgetItem *item = m_list->topLevelItem(row);
+ return item ? item->text(column) : QString();
+}
+
+QString TagRulesDialog::listCellToolTipForTest(int row, int column) const
+{
+ const QTreeWidgetItem *item = m_list->topLevelItem(row);
+ return item ? item->toolTip(column) : QString();
+}
+
+QStringList TagRulesDialog::listHeaderLabelsForTest() const
+{
+ QStringList labels;
+ for (int column = 0; column < m_list->columnCount(); ++column)
+ labels.append(m_list->headerItem()->text(column));
+ return labels;
+}
+
void TagRulesDialog::reloadListForTest()
{
reloadList();
@@ -531,6 +572,14 @@ void TagRulesDialog::fillItem(QTreeWidgetItem *item, const TagRule &rule) const
for (const QString &tag : rule.remove)
tags.append(QStringLiteral("-") + tag);
item->setText(ColumnTags, tags.join(QStringLiteral(" ")));
+
+ // Item 102. The note explains why a rule is shaped the way it is, and was
+ // reachable only by selecting the rule and reading the form. simplified()
+ // because a note is free text and a newline in a tree cell truncates the
+ // row at it; the full text stays in the tooltip and in the editor.
+ const QString note = rule.note.simplified();
+ item->setText(ColumnNote, note);
+ item->setToolTip(ColumnNote, rule.note);
}
void TagRulesDialog::reloadList()
diff --git a/src/tagrulesdialog.h b/src/tagrulesdialog.h
index 1001745..3bfb3d5 100644
--- a/src/tagrulesdialog.h
+++ b/src/tagrulesdialog.h
@@ -143,6 +143,18 @@ public:
int columnWidthForTest(int column) const;
void setColumnWidthForTest(int column, int width);
+ /// The text and tooltip of one cell of the rule list, by top-level row.
+ ///
+ /// Reads the LIST rather than the editor form, which is the distinction
+ /// item 102 is about: the note was always in the form and invisible until
+ /// a rule was selected.
+ QString listCellTextForTest(int row, int column) const;
+ QString listCellToolTipForTest(int row, int column) const;
+
+ /// The header labels, so a test can assert where a column landed without
+ /// hardcoding the enum it is checking.
+ QStringList listHeaderLabelsForTest() const;
+
/// Repopulates the rule list, as adding or deleting a rule does. Exposed
/// because a restored column width has to survive one of these, not only
/// a close and reopen: `resizeColumnToContents` on every reload discarded
diff --git a/tests/test_tagrules.cpp b/tests/test_tagrules.cpp
index 1779245..c831864 100644
--- a/tests/test_tagrules.cpp
+++ b/tests/test_tagrules.cpp
@@ -57,6 +57,7 @@ private slots:
void aNewerVersionIsRefused();
void openingARuleFillsTheBuilderRows();
void switchingRulesDoesNotLeakRowsBetweenThem();
+ void theRuleListShowsEachRulesNote();
void openingARuleWithoutEditingLeavesItByteIdentical();
void anUnrepresentableRuleOpensInTextMode();
void editingARowRewritesTheQuery();
@@ -506,6 +507,74 @@ void TestTagRules::switchingRulesDoesNotLeakRowsBetweenThem()
QCOMPARE(dialog.queryLineForTest(), QStringLiteral("from:one.example.org"));
}
+void TestTagRules::theRuleListShowsEachRulesNote()
+{
+ // Item 102. `note` is the field explaining why a rule is shaped the way it
+ // is, and it was reachable only by selecting the rule and reading the
+ // editor form. The LIST is what the item is about, so this asserts on the
+ // list and never on the form, which was always correct.
+ QTemporaryDir configHome;
+ QVERIFY(configHome.isValid());
+ qputenv("XDG_CONFIG_HOME", configHome.path().toUtf8());
+ QVERIFY(QDir().mkpath(configHome.filePath(QStringLiteral("mailrules"))));
+
+ QFile out(configHome.filePath(QStringLiteral("mailrules/rules.json")));
+ QVERIFY(out.open(QIODevice::WriteOnly));
+ out.write(R"({
+ "version": 1,
+ "rules": [
+ {"id": "one", "query": "from:one.example.org",
+ "add": ["one"], "stage": 50, "enabled": true,
+ "note": "Only the digest,\nnot the alerts."},
+ {"id": "two", "query": "from:two.example.org",
+ "add": ["two"], "stage": 50, "enabled": true}
+ ]
+ })");
+ out.close();
+
+ TagRulesDialog dialog;
+
+ const QStringList headers = dialog.listHeaderLabelsForTest();
+ const int noteColumn = headers.indexOf(QStringLiteral("Note"));
+ QVERIFY2(noteColumn >= 0,
+ qPrintable(QStringLiteral("no Note column in the rule list: %1")
+ .arg(headers.join(QStringLiteral(", ")))));
+
+ // Note is LAST, per the user: it is prose and the widest thing here, so it
+ // runs on at the end rather than pushing the narrow columns off screen.
+ //
+ // The trap this guards is that "Matches" is NOT in the Column enum, it is
+ // appended past the end, so Note has to be declared before ColumnCount and
+ // still draw after it. Both columns hold text, so a mix-up puts the counts
+ // under Note and looks perfectly plausible.
+ QCOMPARE(headers.constLast(), QStringLiteral("Note"));
+ QCOMPARE(headers.at(headers.size() - 2), QStringLiteral("Matches"));
+
+ // simplified(), because a newline in a tree cell truncates the row at it
+ // and a note is free text. The full text stays in the tooltip.
+ QCOMPARE(dialog.listCellTextForTest(0, noteColumn),
+ QStringLiteral("Only the digest, not the alerts."));
+ QCOMPARE(dialog.listCellToolTipForTest(0, noteColumn),
+ QStringLiteral("Only the digest,\nnot the alerts."));
+
+ // A rule with no note leaves the cell empty rather than showing anything.
+ QVERIFY(dialog.listCellTextForTest(1, noteColumn).isEmpty());
+
+ // And the counts still land under Matches, not under Note. Asserting the
+ // header order alone would pass with the two columns swapped in the enum,
+ // since both hold text and nothing else would complain.
+ const int matchesColumn = headers.indexOf(QStringLiteral("Matches"));
+ QVERIFY(matchesColumn >= 0);
+ dialog.setCounts({ 7, 9 });
+ QCOMPARE(dialog.listCellTextForTest(0, matchesColumn), QStringLiteral("7"));
+ QCOMPARE(dialog.listCellTextForTest(1, matchesColumn), QStringLiteral("9"));
+
+ // The note survived the counts arriving: setCounts writes by column index,
+ // so an off-by-one there would overwrite the note with a number.
+ QCOMPARE(dialog.listCellTextForTest(0, noteColumn),
+ QStringLiteral("Only the digest, not the alerts."));
+}
+
void TestTagRules::openingARuleWithoutEditingLeavesItByteIdentical()
{
// Recompiling on open would rewrite the shared file for no reason, and