diff --git a/docs/topics/AutoType.adoc b/docs/topics/AutoType.adoc index 896278b4f..6d54e34e3 100644 --- a/docs/topics/AutoType.adoc +++ b/docs/topics/AutoType.adoc @@ -65,7 +65,7 @@ image::autotype_entry_sequences.png[] === Performing Global Auto-Type The global Auto-Type keyboard shortcut is used when you have focus on the window you want to type into. To make use of this feature, you must have previously configured an Auto-Type hotkey. -When you press the global Auto-Type hotkey, KeePassXC searches all unlocked databases for entries that match the focused window title. The Auto-Type selection dialog will appear in the following circumstances: there are no matches found, there are multiple matches found, or the setting "Always ask before performing Auto-Type" is enabled. +When you press the global Auto-Type hotkey, KeePassXC searches all unlocked databases for entries that match the focused window title. The Auto-Type selection dialog will appear in the following circumstances: there are no matches found, there are multiple matches found, or the setting "Always ask before performing Auto-Type" is enabled. The selection is remembered for a short while to help retype with the same entry in quick succession. .Auto-Type sequence selection image::autotype_selection_dialog.png[,70%] diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index d12663650..8b9089d6b 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -111,6 +111,7 @@ namespace {"f14", Qt::Key_F14}, {"f15", Qt::Key_F15}, {"f16", Qt::Key_F16}}; + static constexpr int rememberLastEntrySecs = 30; } // namespace AutoType* AutoType::m_instance = nullptr; @@ -122,6 +123,8 @@ AutoType::AutoType(QObject* parent, bool test) , m_executor(nullptr) , m_windowState(WindowState::Normal) , m_windowForGlobal(0) + , m_lastMatch(nullptr, QString()) + , m_lastMatchTime(0) { // prevent crash when the plugin has unresolved symbols m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint); @@ -423,6 +426,11 @@ void AutoType::performGlobalAutoType(const QList>& dbLi return; } + // Invalidate last match if it's old enough + if (m_lastMatch.first && (Clock::currentSecondsSinceEpoch() - m_lastMatchTime) > rememberLastEntrySecs) { + m_lastMatch = {nullptr, QString()}; + } + QList matchList; bool hideExpired = config()->get(Config::AutoTypeHideExpiredEntry).toBool(); @@ -451,7 +459,7 @@ void AutoType::performGlobalAutoType(const QList>& dbLi getMainWindow()->closeModalWindow(); auto* selectDialog = new AutoTypeSelectDialog(); - selectDialog->setMatches(matchList, dbList); + selectDialog->setMatches(matchList, dbList, m_lastMatch); if (!search.isEmpty()) { selectDialog->setSearchString(search); @@ -459,6 +467,8 @@ void AutoType::performGlobalAutoType(const QList>& dbLi connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject); connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) { + m_lastMatch = match; + m_lastMatchTime = Clock::currentSecondsSinceEpoch(); executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal); resetAutoTypeState(); }); diff --git a/src/autotype/AutoType.h b/src/autotype/AutoType.h index 2ede0bfa5..dd7e92582 100644 --- a/src/autotype/AutoType.h +++ b/src/autotype/AutoType.h @@ -23,6 +23,8 @@ #include #include +#include "AutoTypeMatch.h" + class AutoTypeAction; class AutoTypeExecutor; class AutoTypePlatformInterface; @@ -95,6 +97,8 @@ private: QString m_windowTitleForGlobal; WindowState m_windowState; WId m_windowForGlobal; + AutoTypeMatch m_lastMatch; + qint64 m_lastMatchTime; Q_DISABLE_COPY(AutoType) }; diff --git a/src/autotype/AutoTypeMatchModel.cpp b/src/autotype/AutoTypeMatchModel.cpp index 207d09b02..2188380aa 100644 --- a/src/autotype/AutoTypeMatchModel.cpp +++ b/src/autotype/AutoTypeMatchModel.cpp @@ -45,6 +45,24 @@ QModelIndex AutoTypeMatchModel::indexFromMatch(const AutoTypeMatch& match) const return index(row, 1); } +QModelIndex AutoTypeMatchModel::closestIndexFromMatch(const AutoTypeMatch& match) const +{ + int row = -1; + + for (int i = m_matches.size() - 1; i >= 0; --i) { + const auto& currentMatch = m_matches.at(i); + if (currentMatch.first == match.first) { + row = i; + + if (currentMatch.second == match.second) { + break; + } + } + } + + return (row > -1) ? index(row, 1) : QModelIndex(); +} + void AutoTypeMatchModel::setMatchList(const QList& matches) { beginResetModel(); diff --git a/src/autotype/AutoTypeMatchModel.h b/src/autotype/AutoTypeMatchModel.h index e5c86e84d..13d632d4c 100644 --- a/src/autotype/AutoTypeMatchModel.h +++ b/src/autotype/AutoTypeMatchModel.h @@ -42,6 +42,7 @@ public: explicit AutoTypeMatchModel(QObject* parent = nullptr); AutoTypeMatch matchFromIndex(const QModelIndex& index) const; QModelIndex indexFromMatch(const AutoTypeMatch& match) const; + QModelIndex closestIndexFromMatch(const AutoTypeMatch& match) const; int rowCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override; diff --git a/src/autotype/AutoTypeMatchView.cpp b/src/autotype/AutoTypeMatchView.cpp index c5d9eb36a..91f9ce083 100644 --- a/src/autotype/AutoTypeMatchView.cpp +++ b/src/autotype/AutoTypeMatchView.cpp @@ -18,6 +18,7 @@ #include "AutoTypeMatchView.h" #include "AutoTypeMatchModel.h" +#include "core/Entry.h" #include #include @@ -78,21 +79,36 @@ void AutoTypeMatchView::keyPressEvent(QKeyEvent* event) } } -void AutoTypeMatchView::setMatchList(const QList& matches, bool selectFirst) +void AutoTypeMatchView::setMatchList(const QList& matches) { m_model->setMatchList(matches); m_sortModel->setFilterWildcard({}); horizontalHeader()->resizeSections(QHeaderView::ResizeToContents); - if (selectFirst) { - selectionModel()->setCurrentIndex(m_sortModel->index(0, 0), + selectionModel()->clear(); + emit currentMatchChanged(currentMatch()); +} + +void AutoTypeMatchView::selectFirstMatch() +{ + selectionModel()->setCurrentIndex(m_sortModel->index(0, 0), + QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); + emit currentMatchChanged(currentMatch()); +} + +bool AutoTypeMatchView::selectMatch(const AutoTypeMatch& match) +{ + QModelIndex index = m_model->closestIndexFromMatch(match); + + if (index.isValid()) { + selectionModel()->setCurrentIndex(m_sortModel->mapFromSource(index), QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows); - } else { - selectionModel()->clear(); + emit currentMatchChanged(currentMatch()); + return true; } - emit currentMatchChanged(currentMatch()); + return false; } void AutoTypeMatchView::filterList(const QString& filter) diff --git a/src/autotype/AutoTypeMatchView.h b/src/autotype/AutoTypeMatchView.h index cc4cf8e97..52fc686bf 100644 --- a/src/autotype/AutoTypeMatchView.h +++ b/src/autotype/AutoTypeMatchView.h @@ -35,7 +35,9 @@ public: explicit AutoTypeMatchView(QWidget* parent = nullptr); AutoTypeMatch currentMatch(); AutoTypeMatch matchFromIndex(const QModelIndex& index); - void setMatchList(const QList& matches, bool selectFirst); + void setMatchList(const QList& matches); + void selectFirstMatch(); + bool selectMatch(const AutoTypeMatch& match); void filterList(const QString& filter); void moveSelection(int offset); diff --git a/src/autotype/AutoTypeSelectDialog.cpp b/src/autotype/AutoTypeSelectDialog.cpp index 6b32306ca..d58e33151 100644 --- a/src/autotype/AutoTypeSelectDialog.cpp +++ b/src/autotype/AutoTypeSelectDialog.cpp @@ -38,6 +38,7 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) : QDialog(parent) , m_ui(new Ui::AutoTypeSelectDialog()) + , m_lastMatch(nullptr, QString()) { setAttribute(Qt::WA_DeleteOnClose); // Places the window on the active (virtual) desktop instead of where the main window is. @@ -57,7 +58,6 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) } }); - m_ui->search->setFocus(); m_ui->search->installEventFilter(this); m_searchTimer.setInterval(300); @@ -69,15 +69,8 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) m_ui->searchCheckBox->setShortcut(Qt::CTRL + Qt::Key_F); connect(m_ui->searchCheckBox, &QCheckBox::toggled, this, [this](bool checked) { - if (checked) { - performSearch(); - m_ui->search->setFocus(); - } else { - // Reset to original match list - m_ui->view->setMatchList(m_matches, true); - performSearch(); - m_ui->search->setFocus(); - } + Q_UNUSED(checked); + performSearch(); }); m_actionMenu->installEventFilter(this); @@ -93,13 +86,25 @@ AutoTypeSelectDialog::~AutoTypeSelectDialog() { } -void AutoTypeSelectDialog::setMatches(const QList& matches, const QList>& dbs) +void AutoTypeSelectDialog::setMatches(const QList& matches, + const QList>& dbs, + const AutoTypeMatch& lastMatch) { m_matches = matches; m_dbs = dbs; + m_lastMatch = lastMatch; + bool noMatches = m_matches.isEmpty(); - m_ui->view->setMatchList(m_matches, !m_matches.isEmpty() || !m_ui->search->text().isEmpty()); - m_ui->searchCheckBox->setChecked(m_matches.isEmpty()); + // disable changing search scope if we have no direct matches + m_ui->searchCheckBox->setDisabled(noMatches); + + // changing check also performs search so block signals temporarily + bool blockSignals = m_ui->searchCheckBox->blockSignals(true); + m_ui->searchCheckBox->setChecked(noMatches); + m_ui->searchCheckBox->blockSignals(blockSignals); + + // always perform search when updating matches to refresh view + performSearch(); } void AutoTypeSelectDialog::setSearchString(const QString& search) @@ -120,37 +125,48 @@ void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match) void AutoTypeSelectDialog::performSearch() { if (!m_ui->searchCheckBox->isChecked()) { + m_ui->view->setMatchList(m_matches); m_ui->view->filterList(m_ui->search->text()); - return; - } + } else { + auto searchText = m_ui->search->text(); + // If no search text, find all entries + if (searchText.isEmpty()) { + searchText.append("*"); + } - auto searchText = m_ui->search->text(); - // If no search text, find all entries - if (searchText.isEmpty()) { - searchText.append("*"); - } - - EntrySearcher searcher; - QList matches; - for (const auto& db : m_dbs) { - auto found = searcher.search(searchText, db->rootGroup()); - for (auto* entry : found) { - QSet sequences; - auto defSequence = entry->effectiveAutoTypeSequence(); - if (!defSequence.isEmpty()) { - matches.append({entry, defSequence}); - sequences << defSequence; - } - for (const auto& assoc : entry->autoTypeAssociations()->getAll()) { - if (!sequences.contains(assoc.sequence) && !assoc.sequence.isEmpty()) { - matches.append({entry, assoc.sequence}); - sequences << assoc.sequence; + EntrySearcher searcher; + QList matches; + for (const auto& db : m_dbs) { + auto found = searcher.search(searchText, db->rootGroup()); + for (auto* entry : found) { + QSet sequences; + auto defSequence = entry->effectiveAutoTypeSequence(); + if (!defSequence.isEmpty()) { + matches.append({entry, defSequence}); + sequences << defSequence; + } + for (const auto& assoc : entry->autoTypeAssociations()->getAll()) { + if (!sequences.contains(assoc.sequence) && !assoc.sequence.isEmpty()) { + matches.append({entry, assoc.sequence}); + sequences << assoc.sequence; + } } } } + + m_ui->view->setMatchList(matches); } - m_ui->view->setMatchList(matches, !m_ui->search->text().isEmpty()); + bool selected = false; + if (m_lastMatch.first) { + selected = m_ui->view->selectMatch(m_lastMatch); + } + + if (!selected && !m_ui->search->text().isEmpty()) { + m_ui->view->selectFirstMatch(); + } + + m_ui->search->setFocus(); } void AutoTypeSelectDialog::activateCurrentMatch() diff --git a/src/autotype/AutoTypeSelectDialog.h b/src/autotype/AutoTypeSelectDialog.h index a8428ec89..fec596b34 100644 --- a/src/autotype/AutoTypeSelectDialog.h +++ b/src/autotype/AutoTypeSelectDialog.h @@ -39,7 +39,9 @@ public: explicit AutoTypeSelectDialog(QWidget* parent = nullptr); ~AutoTypeSelectDialog() override; - void setMatches(const QList& matchList, const QList>& dbs); + void setMatches(const QList& matchList, + const QList>& dbs, + const AutoTypeMatch& lastMatch); void setSearchString(const QString& search); signals: @@ -63,6 +65,7 @@ private: QList> m_dbs; QList m_matches; + AutoTypeMatch m_lastMatch; QTimer m_searchTimer; QPointer m_actionMenu;