diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/mainwindow.cpp | 50 | ||||
| -rw-r--r-- | src/mainwindow.h | 12 | ||||
| -rw-r--r-- | src/querycompleter.cpp | 30 | ||||
| -rw-r--r-- | src/querycompleter.h | 8 |
4 files changed, 99 insertions, 1 deletions
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 517941a..8ca6aa9 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -47,6 +47,7 @@ #include "messageview.h" #include "mimeparser.h" #include "notmuchworker.h" +#include "querycompleter.h" #include "tagchip.h" #include "threadlistmodel.h" #include "version.h" @@ -221,6 +222,7 @@ void MainWindow::buildUi() m_queryEdit->setPlaceholderText(tr("notmuch query, e.g. tag:inbox")); connect(m_queryEdit, &QLineEdit::returnPressed, this, &MainWindow::runCurrentQuery); + m_queryCompleter = new QueryCompleter(m_queryEdit, m_config, this); m_syncLog = new QPlainTextEdit(central); m_syncLog->setReadOnly(true); @@ -449,6 +451,13 @@ void MainWindow::registerActions() if (m_sync->isAvailable()) m_sync->start(); }); + addAction(QStringLiteral("complete_query"), tr("&Complete query"), + tr("Offer completions for the query bar"), [this]() { + // Focus first: the popup anchors on the line edit, and the binding is + // reachable from the thread list where the bar has no focus at all. + m_queryEdit->setFocus(); + m_queryCompleter->triggerCompletion(); + }); addAction(QStringLiteral("quit"), tr("&Quit"), tr("Quit qtmaildir"), [this]() { close(); }); @@ -469,6 +478,7 @@ void MainWindow::buildMenus() editMenu->addAction(m_actions.value(QStringLiteral("undo"))); editMenu->addSeparator(); editMenu->addAction(m_actions.value(QStringLiteral("focus_query"))); + editMenu->addAction(m_actions.value(QStringLiteral("complete_query"))); auto *messageMenu = menuBar()->addMenu(tr("&Message")); messageMenu->addAction(m_actions.value(QStringLiteral("archive"))); @@ -656,15 +666,51 @@ void MainWindow::wireWorker() this, &MainWindow::onThreadLoaded); connect(m_worker, &NotmuchWorker::errorOccurred, this, &MainWindow::onWorkerError); + connect(m_worker, &NotmuchWorker::allTagsReady, + this, &MainWindow::onAllTagsReady); // A confirmed write clears the pending revert: without this, a later // unrelated error would roll back a change that actually succeeded. - connect(m_worker, &NotmuchWorker::tagsApplied, this, [this](const TagChange &) { + connect(m_worker, &NotmuchWorker::tagsApplied, + this, [this](const TagChange &change) { m_pendingChange = {}; m_pendingThreadIds.clear(); + + // A tag the user has just created is the one they are most likely to + // type again, so do not wait for the next sync to offer it. A set + // membership test, not a query. + for (const QString &tag : change.added) { + if (!m_knownTags.contains(tag)) { + requestAllTags(); + break; + } + } }); m_workerThread.start(); + + // Queued behind the thread start, so the completer has real tags as soon + // as the database can be read. Nothing waits on the answer: requestAllTags + // stays silent when the database cannot be opened. + requestAllTags(); +} + +void MainWindow::requestAllTags() +{ + // The generation is unused by the tag path, see onAllTagsReady(). + QMetaObject::invokeMethod(m_worker, "requestAllTags", Qt::QueuedConnection, + Q_ARG(quint64, 0)); +} + +void MainWindow::onAllTagsReady(const QStringList &tags) +{ + // The signal carries a generation, this slot deliberately does not take + // it. A tag list is not an ordered query result: a later one is always at + // least as good as an earlier one, and there is no partial state a stale + // arrival could corrupt. Discarding on generation would only be able to + // throw away a good list. + m_knownTags = tags; + m_queryCompleter->setTags(tags); } void MainWindow::showWarnings() @@ -827,6 +873,8 @@ void MainWindow::onSyncFinished(bool success, int exitCode) if (success) { m_statusLabel->setText(tr("Sync complete")); runCurrentQuery(); + // A sync is the usual way new tags enter the database. + requestAllTags(); } else { m_statusLabel->setText(tr("Sync failed (exit %1)").arg(exitCode)); m_syncLog->show(); diff --git a/src/mainwindow.h b/src/mainwindow.h index f4186ff..3623b04 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -44,6 +44,7 @@ class ThreadListModel; class MessageView; class MailSync; class NotmuchWorker; +class QueryCompleter; class MainWindow : public QMainWindow { @@ -81,6 +82,7 @@ private slots: void onThreadLoaded(const QVector<MessageRef> &messages, quint64 generation); void onWorkerError(const QString &message); void onSyncFinished(bool success, int exitCode); + void onAllTagsReady(const QStringList &tags); private: void buildUi(); @@ -93,6 +95,10 @@ private: void registerActions(); void buildMenus(); void wireWorker(); + + /// Asks the worker to re-enumerate the database tags for the completer. + void requestAllTags(); + void showWarnings(); void showShortcutReference(); void showAbout(); @@ -131,6 +137,7 @@ private: QUndoStack m_undoStack; QLineEdit *m_queryEdit = nullptr; + QueryCompleter *m_queryCompleter = nullptr; QTableView *m_threadView = nullptr; QSplitter *m_splitter = nullptr; QComboBox *m_accountBox = nullptr; @@ -147,6 +154,11 @@ private: /// parallel with them. QHash<QString, QString> m_actionDescriptions; + /// The tag list last received from the worker. Held here and not only in + /// the completer so a mutation can ask whether it introduced a tag the + /// completer does not yet offer, without a round trip. + QStringList m_knownTags; + quint64 m_generation = 0; QString m_lastQuery; QString m_currentThreadId; diff --git a/src/querycompleter.cpp b/src/querycompleter.cpp index 8457776..1a6f6de 100644 --- a/src/querycompleter.cpp +++ b/src/querycompleter.cpp @@ -360,6 +360,36 @@ QueryCompleter::QueryCompleter(QLineEdit *edit, const Config &config, this, [this](const QModelIndex &index) { acceptCompletion(index.data(Qt::DisplayRole).toString()); }); + + if (m_config.completionOnFocus()) + m_edit->installEventFilter(this); +} + +void QueryCompleter::triggerCompletion() +{ + if (!m_edit || !m_completer) + return; + + updateContext(); + if (m_context.kind == CompletionContext::None) + return; + + // The prefix must be set explicitly: complete() filters against whatever + // prefix QCompleter last derived from the line edit's full text, which is + // not the stem once a keyword or a range bound is in play. + m_completer->setCompletionPrefix(m_context.stem); + m_completer->complete(); +} + +bool QueryCompleter::eventFilter(QObject *watched, QEvent *event) +{ + // Only the empty-bar case: once there is text, ordinary typing has + // already driven completion. + if (watched == m_edit && event->type() == QEvent::FocusIn + && m_edit->text().isEmpty()) { + triggerCompletion(); + } + return QObject::eventFilter(watched, event); } void QueryCompleter::acceptCompletion(const QString &value) diff --git a/src/querycompleter.h b/src/querycompleter.h index 374fe84..61b2555 100644 --- a/src/querycompleter.h +++ b/src/querycompleter.h @@ -110,6 +110,14 @@ public: /// synthetic key events instead would test the keyboard layout, not this. void acceptCompletion(const QString &value); +public slots: + /// Opens the popup regardless of what has been typed. Bound to + /// complete_query, and the only trigger when completion_on_focus is off. + void triggerCompletion(); + +protected: + bool eventFilter(QObject *watched, QEvent *event) override; + private: QList<CompletionEntry> entriesFor(const CompletionContext &context) const; void rebuildModel(const CompletionContext &context); |
