Auto-Type: Remember previous selected global match

This makes using multi-stage login forms slightly easier as you
can avoid typing the search terms multiple times.
This commit is contained in:
Toni Spets 2021-11-22 15:15:05 +02:00 committed by Jonathan White
parent d3d7bd7b81
commit 606096278b
9 changed files with 117 additions and 47 deletions

View File

@ -65,7 +65,7 @@ image::autotype_entry_sequences.png[]
=== Performing Global Auto-Type === 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. 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 .Auto-Type sequence selection
image::autotype_selection_dialog.png[,70%] image::autotype_selection_dialog.png[,70%]

View File

@ -111,6 +111,7 @@ namespace
{"f14", Qt::Key_F14}, {"f14", Qt::Key_F14},
{"f15", Qt::Key_F15}, {"f15", Qt::Key_F15},
{"f16", Qt::Key_F16}}; {"f16", Qt::Key_F16}};
static constexpr int rememberLastEntrySecs = 30;
} // namespace } // namespace
AutoType* AutoType::m_instance = nullptr; AutoType* AutoType::m_instance = nullptr;
@ -122,6 +123,8 @@ AutoType::AutoType(QObject* parent, bool test)
, m_executor(nullptr) , m_executor(nullptr)
, m_windowState(WindowState::Normal) , m_windowState(WindowState::Normal)
, m_windowForGlobal(0) , m_windowForGlobal(0)
, m_lastMatch(nullptr, QString())
, m_lastMatchTime(0)
{ {
// prevent crash when the plugin has unresolved symbols // prevent crash when the plugin has unresolved symbols
m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint); m_pluginLoader->setLoadHints(QLibrary::ResolveAllSymbolsHint);
@ -423,6 +426,11 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
return; return;
} }
// Invalidate last match if it's old enough
if (m_lastMatch.first && (Clock::currentSecondsSinceEpoch() - m_lastMatchTime) > rememberLastEntrySecs) {
m_lastMatch = {nullptr, QString()};
}
QList<AutoTypeMatch> matchList; QList<AutoTypeMatch> matchList;
bool hideExpired = config()->get(Config::AutoTypeHideExpiredEntry).toBool(); bool hideExpired = config()->get(Config::AutoTypeHideExpiredEntry).toBool();
@ -451,7 +459,7 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
getMainWindow()->closeModalWindow(); getMainWindow()->closeModalWindow();
auto* selectDialog = new AutoTypeSelectDialog(); auto* selectDialog = new AutoTypeSelectDialog();
selectDialog->setMatches(matchList, dbList); selectDialog->setMatches(matchList, dbList, m_lastMatch);
if (!search.isEmpty()) { if (!search.isEmpty()) {
selectDialog->setSearchString(search); selectDialog->setSearchString(search);
@ -459,6 +467,8 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject); connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject);
connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) { connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](const AutoTypeMatch& match) {
m_lastMatch = match;
m_lastMatchTime = Clock::currentSecondsSinceEpoch();
executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal); executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal);
resetAutoTypeState(); resetAutoTypeState();
}); });

View File

@ -23,6 +23,8 @@
#include <QObject> #include <QObject>
#include <QWidget> #include <QWidget>
#include "AutoTypeMatch.h"
class AutoTypeAction; class AutoTypeAction;
class AutoTypeExecutor; class AutoTypeExecutor;
class AutoTypePlatformInterface; class AutoTypePlatformInterface;
@ -95,6 +97,8 @@ private:
QString m_windowTitleForGlobal; QString m_windowTitleForGlobal;
WindowState m_windowState; WindowState m_windowState;
WId m_windowForGlobal; WId m_windowForGlobal;
AutoTypeMatch m_lastMatch;
qint64 m_lastMatchTime;
Q_DISABLE_COPY(AutoType) Q_DISABLE_COPY(AutoType)
}; };

View File

@ -45,6 +45,24 @@ QModelIndex AutoTypeMatchModel::indexFromMatch(const AutoTypeMatch& match) const
return index(row, 1); 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<AutoTypeMatch>& matches) void AutoTypeMatchModel::setMatchList(const QList<AutoTypeMatch>& matches)
{ {
beginResetModel(); beginResetModel();

View File

@ -42,6 +42,7 @@ public:
explicit AutoTypeMatchModel(QObject* parent = nullptr); explicit AutoTypeMatchModel(QObject* parent = nullptr);
AutoTypeMatch matchFromIndex(const QModelIndex& index) const; AutoTypeMatch matchFromIndex(const QModelIndex& index) const;
QModelIndex indexFromMatch(const AutoTypeMatch& match) const; QModelIndex indexFromMatch(const AutoTypeMatch& match) const;
QModelIndex closestIndexFromMatch(const AutoTypeMatch& match) const;
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override; int columnCount(const QModelIndex& parent = QModelIndex()) const override;

View File

@ -18,6 +18,7 @@
#include "AutoTypeMatchView.h" #include "AutoTypeMatchView.h"
#include "AutoTypeMatchModel.h" #include "AutoTypeMatchModel.h"
#include "core/Entry.h"
#include <QHeaderView> #include <QHeaderView>
#include <QKeyEvent> #include <QKeyEvent>
@ -78,21 +79,36 @@ void AutoTypeMatchView::keyPressEvent(QKeyEvent* event)
} }
} }
void AutoTypeMatchView::setMatchList(const QList<AutoTypeMatch>& matches, bool selectFirst) void AutoTypeMatchView::setMatchList(const QList<AutoTypeMatch>& matches)
{ {
m_model->setMatchList(matches); m_model->setMatchList(matches);
m_sortModel->setFilterWildcard({}); m_sortModel->setFilterWildcard({});
horizontalHeader()->resizeSections(QHeaderView::ResizeToContents); horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
if (selectFirst) { selectionModel()->clear();
selectionModel()->setCurrentIndex(m_sortModel->index(0, 0), 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); QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
} else { emit currentMatchChanged(currentMatch());
selectionModel()->clear(); return true;
} }
emit currentMatchChanged(currentMatch()); return false;
} }
void AutoTypeMatchView::filterList(const QString& filter) void AutoTypeMatchView::filterList(const QString& filter)

View File

@ -35,7 +35,9 @@ public:
explicit AutoTypeMatchView(QWidget* parent = nullptr); explicit AutoTypeMatchView(QWidget* parent = nullptr);
AutoTypeMatch currentMatch(); AutoTypeMatch currentMatch();
AutoTypeMatch matchFromIndex(const QModelIndex& index); AutoTypeMatch matchFromIndex(const QModelIndex& index);
void setMatchList(const QList<AutoTypeMatch>& matches, bool selectFirst); void setMatchList(const QList<AutoTypeMatch>& matches);
void selectFirstMatch();
bool selectMatch(const AutoTypeMatch& match);
void filterList(const QString& filter); void filterList(const QString& filter);
void moveSelection(int offset); void moveSelection(int offset);

View File

@ -38,6 +38,7 @@
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
: QDialog(parent) : QDialog(parent)
, m_ui(new Ui::AutoTypeSelectDialog()) , m_ui(new Ui::AutoTypeSelectDialog())
, m_lastMatch(nullptr, QString())
{ {
setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_DeleteOnClose);
// Places the window on the active (virtual) desktop instead of where the main window is. // 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_ui->search->installEventFilter(this);
m_searchTimer.setInterval(300); m_searchTimer.setInterval(300);
@ -69,15 +69,8 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
m_ui->searchCheckBox->setShortcut(Qt::CTRL + Qt::Key_F); m_ui->searchCheckBox->setShortcut(Qt::CTRL + Qt::Key_F);
connect(m_ui->searchCheckBox, &QCheckBox::toggled, this, [this](bool checked) { connect(m_ui->searchCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
if (checked) { Q_UNUSED(checked);
performSearch(); performSearch();
m_ui->search->setFocus();
} else {
// Reset to original match list
m_ui->view->setMatchList(m_matches, true);
performSearch();
m_ui->search->setFocus();
}
}); });
m_actionMenu->installEventFilter(this); m_actionMenu->installEventFilter(this);
@ -93,13 +86,25 @@ AutoTypeSelectDialog::~AutoTypeSelectDialog()
{ {
} }
void AutoTypeSelectDialog::setMatches(const QList<AutoTypeMatch>& matches, const QList<QSharedPointer<Database>>& dbs) void AutoTypeSelectDialog::setMatches(const QList<AutoTypeMatch>& matches,
const QList<QSharedPointer<Database>>& dbs,
const AutoTypeMatch& lastMatch)
{ {
m_matches = matches; m_matches = matches;
m_dbs = dbs; 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()); // disable changing search scope if we have no direct matches
m_ui->searchCheckBox->setChecked(m_matches.isEmpty()); 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) void AutoTypeSelectDialog::setSearchString(const QString& search)
@ -120,37 +125,48 @@ void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match)
void AutoTypeSelectDialog::performSearch() void AutoTypeSelectDialog::performSearch()
{ {
if (!m_ui->searchCheckBox->isChecked()) { if (!m_ui->searchCheckBox->isChecked()) {
m_ui->view->setMatchList(m_matches);
m_ui->view->filterList(m_ui->search->text()); 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(); EntrySearcher searcher;
// If no search text, find all entries QList<AutoTypeMatch> matches;
if (searchText.isEmpty()) { for (const auto& db : m_dbs) {
searchText.append("*"); auto found = searcher.search(searchText, db->rootGroup());
} for (auto* entry : found) {
QSet<QString> sequences;
EntrySearcher searcher; auto defSequence = entry->effectiveAutoTypeSequence();
QList<AutoTypeMatch> matches; if (!defSequence.isEmpty()) {
for (const auto& db : m_dbs) { matches.append({entry, defSequence});
auto found = searcher.search(searchText, db->rootGroup()); sequences << defSequence;
for (auto* entry : found) { }
QSet<QString> sequences; for (const auto& assoc : entry->autoTypeAssociations()->getAll()) {
auto defSequence = entry->effectiveAutoTypeSequence(); if (!sequences.contains(assoc.sequence) && !assoc.sequence.isEmpty()) {
if (!defSequence.isEmpty()) { matches.append({entry, assoc.sequence});
matches.append({entry, defSequence}); sequences << assoc.sequence;
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() void AutoTypeSelectDialog::activateCurrentMatch()

View File

@ -39,7 +39,9 @@ public:
explicit AutoTypeSelectDialog(QWidget* parent = nullptr); explicit AutoTypeSelectDialog(QWidget* parent = nullptr);
~AutoTypeSelectDialog() override; ~AutoTypeSelectDialog() override;
void setMatches(const QList<AutoTypeMatch>& matchList, const QList<QSharedPointer<Database>>& dbs); void setMatches(const QList<AutoTypeMatch>& matchList,
const QList<QSharedPointer<Database>>& dbs,
const AutoTypeMatch& lastMatch);
void setSearchString(const QString& search); void setSearchString(const QString& search);
signals: signals:
@ -63,6 +65,7 @@ private:
QList<QSharedPointer<Database>> m_dbs; QList<QSharedPointer<Database>> m_dbs;
QList<AutoTypeMatch> m_matches; QList<AutoTypeMatch> m_matches;
AutoTypeMatch m_lastMatch;
QTimer m_searchTimer; QTimer m_searchTimer;
QPointer<QMenu> m_actionMenu; QPointer<QMenu> m_actionMenu;