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
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%]

View File

@ -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<QSharedPointer<Database>>& dbLi
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;
bool hideExpired = config()->get(Config::AutoTypeHideExpiredEntry).toBool();
@ -451,7 +459,7 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& 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<QSharedPointer<Database>>& 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();
});

View File

@ -23,6 +23,8 @@
#include <QObject>
#include <QWidget>
#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)
};

View File

@ -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<AutoTypeMatch>& matches)
{
beginResetModel();

View File

@ -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;

View File

@ -18,6 +18,7 @@
#include "AutoTypeMatchView.h"
#include "AutoTypeMatchModel.h"
#include "core/Entry.h"
#include <QHeaderView>
#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_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)

View File

@ -35,7 +35,9 @@ public:
explicit AutoTypeMatchView(QWidget* parent = nullptr);
AutoTypeMatch currentMatch();
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 moveSelection(int offset);

View File

@ -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<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_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<AutoTypeMatch> matches;
for (const auto& db : m_dbs) {
auto found = searcher.search(searchText, db->rootGroup());
for (auto* entry : found) {
QSet<QString> 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<AutoTypeMatch> matches;
for (const auto& db : m_dbs) {
auto found = searcher.search(searchText, db->rootGroup());
for (auto* entry : found) {
QSet<QString> 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()

View File

@ -39,7 +39,9 @@ public:
explicit AutoTypeSelectDialog(QWidget* parent = nullptr);
~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);
signals:
@ -63,6 +65,7 @@ private:
QList<QSharedPointer<Database>> m_dbs;
QList<AutoTypeMatch> m_matches;
AutoTypeMatch m_lastMatch;
QTimer m_searchTimer;
QPointer<QMenu> m_actionMenu;