aboutsummaryrefslogtreecommitdiffstats
path: root/src/tagrulesdialog.h
diff options
context:
space:
mode:
Diffstat (limited to 'src/tagrulesdialog.h')
-rw-r--r--src/tagrulesdialog.h164
1 files changed, 164 insertions, 0 deletions
diff --git a/src/tagrulesdialog.h b/src/tagrulesdialog.h
index 0350bd5..e8979fe 100644
--- a/src/tagrulesdialog.h
+++ b/src/tagrulesdialog.h
@@ -19,17 +19,24 @@
#pragma once
#include <QDialog>
+#include <QStringList>
+#include "rulequery.h"
#include "tagrules.h"
class QCheckBox;
+class QComboBox;
class QLabel;
class QLineEdit;
class QPlainTextEdit;
class QPushButton;
+class QRadioButton;
+class QScrollArea;
+class QSplitter;
class QSpinBox;
class QTreeWidget;
class QTreeWidgetItem;
+class QVBoxLayout;
/// Views and edits the shared auto-tagging rules.
///
@@ -51,10 +58,94 @@ public:
/// handle and notmuch permits one per process.
QStringList countQueries() const;
+ /// Maildir folder paths, for the Folder row's dropdown, relative to the
+ /// database root. Supplied by the caller rather than read here: they come
+ /// from a scan of the tree under notmuch's database.path, which only
+ /// NotmuchWorker can answer for, and this dialog reaches neither it nor
+ /// Config.
+ ///
+ /// Arrives AFTER the dialog is on screen, since the scan crosses to the
+ /// worker on a queued call, so this refills the rows that already exist.
+ void setFolders(const QStringList &folders);
+
+ /// Test seams. The builder's state is otherwise reachable only through
+ /// synthetic clicks on widgets whose geometry the offscreen platform does
+ /// not guarantee.
+ int rowCountForTest() const { return m_rows.size(); }
+ QString queryLineForTest() const;
+
+ /// Selects the rule at `index` as a click on the list would.
+ void selectRuleForTest(int index);
+
+ bool textModeForTest() const;
+ void setRowValueForTest(int index, const QString &value);
+ /// Runs the Save path without showing the dialog.
+ void saveForTest() { onSave(); }
+
+ /// Types into the query field and toggles the mode, so the refusal path
+ /// can be reached without a synthetic click. The refusal reports through
+ /// the warning label rather than a modal precisely so this is testable.
+ void setQueryTextForTest(const QString &text);
+ void setTextModeForTest(bool on);
+ QString warningTextForTest() const;
+
+ /// Whether the text-mode toggle would be on screen. A toggle that hides
+ /// itself when switched on is a one-way trip, and asserting only on the
+ /// checked STATE passes against that, since the state is still readable
+ /// when the widget is not.
+ bool textModeToggleIsReachableForTest() const;
+
+ /// Width of one rule-list column. The geometry restore is asserted on the
+ /// saved and reread VALUE rather than on the resulting frame: item 46
+ /// records that the offscreen platform does not honour a window resize,
+ /// so a test comparing frames there passes or fails for reasons that have
+ /// nothing to do with the code.
+ int columnWidthForTest(int column) const;
+ void setColumnWidthForTest(int column, int width);
+
+ /// 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
+ /// the width the user had dragged.
+ void reloadListForTest();
+
+ /// Presses Preview, which is otherwise reachable only through a click on
+ /// a button whose geometry the offscreen platform does not guarantee.
+ void previewForTest();
+
+ /// The height the condition-row editor asks for. This is what squeezed
+ /// the rule list: a stretch factor only shares out space ABOVE each
+ /// widget's minimum, so every row added here came out of the list.
+ /// Measured at 120px for one row and 414px for eight before the scroll
+ /// area capped it.
+ ///
+ /// Asserted on instead of the list's rendered height because the
+ /// offscreen platform does not honour a window size, so the rendered
+ /// height there is not evidence of anything (see CLAUDE.md). Note the
+ /// size HINT, not minimumSizeHint: a QFormLayout's minimum does not track
+ /// its rows and reads the same either way, which passed against the bug.
+ int heightDemandedBelowListForTest() const;
+
+ /// How tall the condition-row area may become. The scroll area caps it;
+ /// without the cap the rows grow without bound and a long rule fills the
+ /// window again, scrolling instead of squeezing. Asserted separately
+ /// because removing the cap leaves heightDemandedBelowListForTest
+ /// unchanged, so that measure alone does not cover it.
+ int conditionAreaHeightForTest() const;
+
signals:
/// Asks the owner to run countQueries() through the worker.
void countsRequested();
+ /// Asks the owner to run one rule's query in the main window, so the user
+ /// can see WHICH mail a rule collects rather than how much.
+ ///
+ /// The query goes out exactly as stored: no `tag:new`, and no wrapping
+ /// parentheses. The post-new hook adds both when it applies a rule, and a
+ /// preview that copied them would match nothing outside a sync window,
+ /// since `tag:new` is set only on mail that has just arrived.
+ void previewRequested(const QString &query);
+
public slots:
/// Corpus counts, positionally paired with countQueries().
void setCounts(const QVector<int> &counts);
@@ -67,8 +158,16 @@ private slots:
void applyEditsToCurrentRule();
void onSave();
+public slots:
+ /// Saves the window's size and column widths on the way out. Overridden
+ /// here rather than closeEvent because Save and Cancel do not send a
+ /// close event at all, only the window manager's X button does.
+ void done(int result) override;
+
private:
void reloadList();
+ void restoreUiState();
+ void saveUiState();
void showWarnings();
int currentIndex() const;
@@ -76,6 +175,44 @@ private:
/// applyEditsToCurrentRule() so the two cannot render a rule differently.
void fillItem(QTreeWidgetItem *item, const TagRule &rule) const;
+ /// One builder row's widgets, so a row can be removed as a unit.
+ struct Row
+ {
+ QWidget *container = nullptr;
+ QComboBox *field = nullptr;
+ QComboBox *op = nullptr;
+ QLineEdit *value = nullptr;
+ /// Shown in place of `value` for a Folder row. Never visible together.
+ QComboBox *folder = nullptr;
+ };
+
+ /// Reads a row's value from whichever of its two widgets the field selects,
+ /// so the read and write paths cannot disagree about where it lives.
+ QString rowValue(const Row &row) const;
+ void setRowValue(Row *row, const QString &value);
+
+ Row *addRow(bool exclusion);
+ void removeRow(bool exclusion, int index);
+ void populateOperators(bool exclusion);
+ void updateExclusionsVisibility();
+ void syncQueryLine();
+
+ void setTextMode(bool on);
+
+ void rebuildRows(const RuleQuery &query);
+ void applyTermToRow(Row *row, const RuleTerm &term);
+ RuleQuery currentQueryFromRows() const;
+
+ /// The query as parsed when the current rule was loaded. Task 10's save
+ /// path compares against this to decide whether the stored string may be
+ /// left alone.
+ RuleQuery m_loadedQuery;
+
+ QStringList m_folders;
+
+ QList<Row> m_rows;
+ QList<Row> m_exclusionRows;
+
TagRules m_rules;
QList<TagRule> m_working; ///< Edited copy; written only on Save.
@@ -85,6 +222,15 @@ private:
/// row's text, which applyEditsToCurrentRule() has no chance to flush.
bool m_reloading = false;
+ /// Each column is auto-sized to its contents ONCE, on its first fill.
+ /// After that its width belongs to the user, whether it came from a
+ /// restored header or from a drag, and resizeColumnToContents on every
+ /// repopulate threw both away on the next add or delete. Two flags rather
+ /// than one because the count column is filled later than the rest, by a
+ /// reply from the worker.
+ bool m_columnsSized = false;
+ bool m_countColumnSized = false;
+
QTreeWidget *m_list = nullptr;
QLineEdit *m_id = nullptr;
QLineEdit *m_add = nullptr;
@@ -95,4 +241,22 @@ private:
QCheckBox *m_enabled = nullptr;
QLabel *m_warningLabel = nullptr;
QPushButton *m_saveButton = nullptr;
+
+ QRadioButton *m_matchAll = nullptr;
+ QRadioButton *m_matchAny = nullptr;
+ QCheckBox *m_textMode = nullptr;
+ QWidget *m_builder = nullptr;
+ /// Scrolls the condition rows, so a rule with many of them cannot grow
+ /// the editor without bound. Shown and hidden in place of m_builder for
+ /// text mode: hiding the inner widget would leave an empty scroll area.
+ QScrollArea *m_builderScroll = nullptr;
+ /// Divides the rule list from the editor. The list had stretch 1 and was
+ /// still squeezed, because a stretch factor only shares out space above
+ /// each widget's minimum and the form's grew with every condition row.
+ QSplitter *m_splitter = nullptr;
+ QPushButton *m_previewButton = nullptr;
+ QVBoxLayout *m_rowsLayout = nullptr;
+ QVBoxLayout *m_exclusionsLayout = nullptr;
+ QLabel *m_exclusionsHeader = nullptr;
+ QPushButton *m_addExclusion = nullptr;
};