summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/mainwindow.cpp113
-rw-r--r--src/mainwindow.h36
-rw-r--r--src/savequerydialog.cpp60
-rw-r--r--src/savequerydialog.h16
4 files changed, 218 insertions, 7 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index 62f4751..14e4202 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1698,6 +1698,7 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout)
button->setObjectName(QStringLiteral("sentButton"));
connect(button, &QPushButton::clicked, this,
[this, saved]() { runSavedQuery(saved); });
+ addSavedQueryActions(button, saved);
box->addWidget(button);
}
@@ -1718,6 +1719,12 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout)
QAction *action = menu->addAction(saved.name);
connect(action, &QAction::triggered, this,
[this, saved]() { runSavedQuery(saved); });
+ // A menu entry has no context menu of its own, so its own submenu
+ // carries the same three actions; an unpinned query would
+ // otherwise be the one thing that cannot be edited or deleted.
+ auto *entryMenu = new QMenu(menu);
+ addSavedQueryActions(entryMenu, saved);
+ action->setMenu(entryMenu);
}
menuButton->setMenu(menu);
box->addWidget(menuButton);
@@ -1733,6 +1740,105 @@ void MainWindow::buildSavedQueryRow(QWidget *parent, QVBoxLayout *layout)
row->hide();
}
+void MainWindow::addSavedQueryActions(QWidget *target, const SavedQuery &saved)
+{
+ target->setContextMenuPolicy(Qt::ActionsContextMenu);
+
+ auto *edit = new QAction(tr("Edit..."), target);
+ edit->setObjectName(QStringLiteral("editQuery"));
+ connect(edit, &QAction::triggered, this,
+ [this, saved]() { editSavedQuery(saved); });
+ target->addAction(edit);
+
+ auto *pin = new QAction(saved.pinned ? tr("Move to menu")
+ : tr("Show as a button"),
+ target);
+ pin->setObjectName(QStringLiteral("pinQuery"));
+ connect(pin, &QAction::triggered, this, [this, saved]() {
+ SavedQuery toggled = saved;
+ toggled.pinned = !saved.pinned;
+ replaceSavedQuery(saved.name, toggled);
+ });
+ target->addAction(pin);
+
+ auto *separator = new QAction(target);
+ separator->setSeparator(true);
+ target->addAction(separator);
+
+ auto *remove = new QAction(tr("Delete"), target);
+ remove->setObjectName(QStringLiteral("deleteQuery"));
+ connect(remove, &QAction::triggered, this,
+ [this, saved]() { deleteSavedQuery(saved); });
+ target->addAction(remove);
+}
+
+void MainWindow::editSavedQuery(const SavedQuery &saved)
+{
+ SaveQueryDialog dialog(m_config, saved, this);
+ if (dialog.exec() != QDialog::Accepted)
+ return;
+
+ // Matched on the name the dialog OPENED with. Using the returned name would
+ // leave the original entry in place and add a second one under the new
+ // name, which is a duplicate rather than a rename.
+ replaceSavedQuery(saved.name, dialog.savedQuery());
+}
+
+void MainWindow::deleteSavedQuery(const SavedQuery &saved)
+{
+ // One of the few places in this application that confirms. The rule against
+ // confirmation dialogs covers tag mutations, which are undoable through the
+ // undo stack; this writes user config, is not on that stack, and cannot be
+ // taken back.
+ if (m_confirmDelete) {
+ const auto answer = QMessageBox::question(
+ this, tr("Delete saved query"),
+ tr("Delete the saved query '%1'?").arg(saved.name),
+ QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
+ if (answer != QMessageBox::Yes)
+ return;
+ }
+
+ replaceSavedQuery(saved.name, SavedQuery());
+}
+
+void MainWindow::replaceSavedQuery(const QString &originalName,
+ const SavedQuery &replacement)
+{
+ QList<SavedQuery> queries = m_config.savedQueries();
+ const bool removing = replacement.name.isEmpty();
+
+ for (int i = 0; i < queries.size(); ++i) {
+ if (queries.at(i).name.compare(originalName, Qt::CaseInsensitive) != 0)
+ continue;
+
+ if (removing) {
+ queries.removeAt(i);
+ } else {
+ // The unknown fields belong to the STORED entry: a field written by
+ // a later build survives an edit made here rather than being
+ // dropped on the next save.
+ SavedQuery merged = replacement;
+ merged.unknown = queries.at(i).unknown;
+ queries[i] = merged;
+ }
+ break;
+ }
+
+ m_config.setSavedQueries(queries);
+ if (!m_config.saveSavedQueries()) {
+ QMessageBox::warning(this, tr("Saved queries"),
+ tr("Could not write the saved queries file."));
+ return;
+ }
+
+ rebuildSavedQueryRow();
+ statusBar()->showMessage(
+ removing ? tr("Deleted saved query '%1'.").arg(originalName)
+ : tr("Updated saved query '%1'.").arg(replacement.name),
+ kStatusMessageMs);
+}
+
void MainWindow::runSavedQuery(const SavedQuery &saved)
{
// Through the dropdown, never by pre-scoping the text: runQuery() applies
@@ -1817,6 +1923,13 @@ void MainWindow::rebuildSavedQueryRow()
const int index = layout->indexOf(old);
layout->removeWidget(old);
+ // Reparented out NOW, not merely scheduled for deletion. deleteLater()
+ // defers destruction to the event loop, so the old row goes on answering
+ // findChild() until it runs, and findChild returns the FIRST match: every
+ // lookup after a rebuild found the stale row and reported the state from
+ // before the edit. Nothing visible was wrong, which is why this only
+ // showed up as three tests failing on a row that had in fact been rebuilt.
+ old->setParent(nullptr);
old->deleteLater();
buildSavedQueryRow(centralWidget(), layout);
diff --git a/src/mainwindow.h b/src/mainwindow.h
index e6ee322..6e8501a 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -125,6 +125,25 @@ public:
static void setLocksPathForTesting(const QString &path);
static QString locksPath();
+ /// Suppresses the delete confirmation.
+ ///
+ /// A test seam. Deleting a saved query is destructive and not on the undo
+ /// stack, so it asks first; a test cannot answer a modal dialog without
+ /// hanging, and driving one through QTest would assert the dialog rather
+ /// than the deletion.
+ void setConfirmDeleteForTesting(bool confirm) { m_confirmDelete = confirm; }
+
+ /// Renames or replaces a stored query, as the edit dialog would on accept.
+ ///
+ /// A test seam for the rename path specifically: the dialog is modal, and
+ /// the property worth asserting is that a rename REPLACES rather than
+ /// duplicating, which is decided after the dialog returns.
+ void replaceSavedQueryForTesting(const QString &originalName,
+ const SavedQuery &replacement)
+ {
+ replaceSavedQuery(originalName, replacement);
+ }
+
/// How many commands are on the undo stack.
///
/// A test seam. The undo QAction is always enabled and checks canUndo()
@@ -253,6 +272,21 @@ private:
/// Rebuilds the saved-query row in place after the stored list changed.
void rebuildSavedQueryRow();
+ /// Hangs Edit, Pin/Unpin and Delete on a saved query's button or menu
+ /// entry. The only route to changing a stored query from the UI.
+ void addSavedQueryActions(QWidget *target, const SavedQuery &saved);
+
+ /// Replaces the entry named `originalName`, writes the file and rebuilds
+ /// the row. An empty `replacement.name` deletes it instead.
+ ///
+ /// Matched on the ORIGINAL name, not the replacement's: a rename otherwise
+ /// leaves the old entry in place and adds a second one.
+ void replaceSavedQuery(const QString &originalName,
+ const SavedQuery &replacement);
+
+ void editSavedQuery(const SavedQuery &saved);
+ void deleteSavedQuery(const SavedQuery &saved);
+
private slots:
void runCurrentQuery() { runQuery(FlatResult::No); }
@@ -628,6 +662,8 @@ private:
QLineEdit *m_queryEdit = nullptr;
/// Save query, beside the field. Driven by the save_query action.
QToolButton *m_saveQueryButton = nullptr;
+ /// Whether deleting a saved query asks first. Always true outside tests.
+ bool m_confirmDelete = true;
QueryCompleter *m_queryCompleter = nullptr;
/// Its own type, not the QTreeView base. The strip painting and the
/// expander column are ThreadListView's, and holding the base here only
diff --git a/src/savequerydialog.cpp b/src/savequerydialog.cpp
index 1ea97c2..2d09986 100644
--- a/src/savequerydialog.cpp
+++ b/src/savequerydialog.cpp
@@ -42,19 +42,51 @@ SaveQueryDialog::SaveQueryDialog(const Config &config, const QString &query,
: QDialog(parent)
, m_config(config)
{
+ SavedQuery initial;
+ initial.query = query;
+ initial.account = accountKey;
+ initial.pinned = true;
setWindowTitle(tr("Save query"));
+ build(initial);
+}
+SaveQueryDialog::SaveQueryDialog(const Config &config,
+ const SavedQuery &existing, QWidget *parent)
+ : QDialog(parent)
+ , m_config(config)
+ , m_originalName(existing.name)
+ , m_generated(existing.generated)
+ , m_flat(existing.flat)
+{
+ setWindowTitle(tr("Edit saved query"));
+ build(existing);
+}
+
+void SaveQueryDialog::build(const SavedQuery &initial)
+{
auto *layout = new QVBoxLayout(this);
auto *form = new QFormLayout;
- m_name = new QLineEdit(this);
+ m_name = new QLineEdit(initial.name, this);
m_name->setObjectName(QStringLiteral("saveQueryName"));
m_name->setPlaceholderText(tr("A name for this query"));
form->addRow(tr("Name"), m_name);
- m_query = new QLineEdit(query, this);
+ m_query = new QLineEdit(initial.query, this);
m_query->setObjectName(QStringLiteral("saveQueryQuery"));
- form->addRow(tr("Query"), m_query);
+ if (initial.isGenerated()) {
+ // A generated entry has no stored query: it is composed from the
+ // accounts every time it runs. Shown, so the user can see what it will
+ // do, but read-only, since editing it would change nothing.
+ m_query->setText(m_config.resolvedQuery(initial));
+ m_query->setReadOnly(true);
+ m_query->setToolTip(tr("Built from your accounts and not editable. "
+ "It follows the sent folder each account "
+ "configures."));
+ form->addRow(tr("Query"), m_query);
+ } else {
+ form->addRow(tr("Query"), m_query);
+ }
// The scope is stored as an account KEY, so the entries carry the key as
// data exactly as the main window's dropdown does. "All accounts" is the
@@ -63,15 +95,15 @@ SaveQueryDialog::SaveQueryDialog(const Config &config, const QString &query,
m_account = new QComboBox(this);
m_account->setObjectName(QStringLiteral("saveQueryAccount"));
m_account->addItem(tr("All accounts"), QString());
- for (const Account &account : config.accounts())
+ for (const Account &account : m_config.accounts())
m_account->addItem(account.key, account.key);
- const int index = m_account->findData(accountKey);
+ const int index = m_account->findData(initial.account);
m_account->setCurrentIndex(index >= 0 ? index : 0);
form->addRow(tr("Account"), m_account);
m_pinned = new QCheckBox(tr("Show as a button"), this);
m_pinned->setObjectName(QStringLiteral("saveQueryPinned"));
- m_pinned->setChecked(true);
+ m_pinned->setChecked(initial.pinned);
form->addRow(QString(), m_pinned);
layout->addLayout(form);
@@ -108,7 +140,14 @@ void SaveQueryDialog::updateOkState()
const bool usable = !name.isEmpty() && !m_query->text().trimmed().isEmpty();
m_ok->setEnabled(usable);
- if (!name.isEmpty() && namesAnExistingQuery(m_config, name)) {
+ // Ignores the entry being edited: warning that "Inbox" already exists
+ // while editing Inbox is noise, and the real case worth catching is a
+ // rename onto a name something else already holds.
+ const bool isItsOwnName =
+ !m_originalName.isEmpty()
+ && name.compare(m_originalName, Qt::CaseInsensitive) == 0;
+ if (!name.isEmpty() && !isItsOwnName
+ && namesAnExistingQuery(m_config, name)) {
m_notice->setText(
tr("A saved query named '%1' already exists and will be "
"replaced.").arg(name));
@@ -124,5 +163,12 @@ SavedQuery SaveQueryDialog::savedQuery() const
saved.query = m_query->text().trimmed();
saved.account = m_account->currentData().toString();
saved.pinned = m_pinned->isChecked();
+ // Carried through rather than re-derived: an edit must not turn a
+ // generated entry into a plain one holding a snapshot of what it happened
+ // to resolve to today.
+ saved.generated = m_generated;
+ saved.flat = m_flat;
+ if (saved.isGenerated())
+ saved.query.clear();
return saved;
}
diff --git a/src/savequerydialog.h b/src/savequerydialog.h
index be5a2af..d859254 100644
--- a/src/savequerydialog.h
+++ b/src/savequerydialog.h
@@ -42,6 +42,11 @@ public:
SaveQueryDialog(const Config &config, const QString &query,
const QString &accountKey, QWidget *parent = nullptr);
+ /// Edits an entry that already exists, prefilled from it rather than from
+ /// the query bar.
+ SaveQueryDialog(const Config &config, const SavedQuery &existing,
+ QWidget *parent = nullptr);
+
/// The query as edited. Only meaningful after exec() returned Accepted.
SavedQuery savedQuery() const;
@@ -51,8 +56,19 @@ public:
static bool namesAnExistingQuery(const Config &config, const QString &name);
private:
+ void build(const SavedQuery &initial);
void updateOkState();
+ /// The name the dialog was opened on, empty when creating. The caller
+ /// matches on this rather than on the returned name, so a rename replaces
+ /// the entry instead of adding a second one beside it.
+ QString m_originalName;
+
+ /// Set for a generated entry, whose query is composed from the accounts
+ /// and cannot be edited here.
+ QString m_generated;
+ bool m_flat = false;
+
const Config &m_config;
QLineEdit *m_name = nullptr;
QLineEdit *m_query = nullptr;