Skip to content

Commit

Permalink
Auto-Type: Remember previous selected global match
Browse files Browse the repository at this point in the history
This makes using multi-stage login forms slightly easier as you
can avoid typing the search terms multiple times.
  • Loading branch information
hifi authored and droidmonkey committed Nov 26, 2021
1 parent d3d7bd7 commit 6060962
Show file tree
Hide file tree
Showing 9 changed files with 117 additions and 47 deletions.
2 changes: 1 addition & 1 deletion docs/topics/AutoType.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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%]
Expand Down
12 changes: 11 additions & 1 deletion src/autotype/AutoType.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -451,14 +459,16 @@ 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);
}

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();
});
Expand Down
4 changes: 4 additions & 0 deletions src/autotype/AutoType.h
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
#include <QObject>
#include <QWidget>

#include "AutoTypeMatch.h"

class AutoTypeAction;
class AutoTypeExecutor;
class AutoTypePlatformInterface;
Expand Down Expand Up @@ -95,6 +97,8 @@ private slots:
QString m_windowTitleForGlobal;
WindowState m_windowState;
WId m_windowForGlobal;
AutoTypeMatch m_lastMatch;
qint64 m_lastMatchTime;

Q_DISABLE_COPY(AutoType)
};
Expand Down
18 changes: 18 additions & 0 deletions src/autotype/AutoTypeMatchModel.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
1 change: 1 addition & 0 deletions src/autotype/AutoTypeMatchModel.h
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ class AutoTypeMatchModel : public QAbstractTableModel
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;
Expand Down
28 changes: 22 additions & 6 deletions src/autotype/AutoTypeMatchView.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

#include "AutoTypeMatchView.h"
#include "AutoTypeMatchModel.h"
#include "core/Entry.h"

#include <QHeaderView>
#include <QKeyEvent>
Expand Down Expand Up @@ -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)
Expand Down
4 changes: 3 additions & 1 deletion src/autotype/AutoTypeMatchView.h
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ class AutoTypeMatchView : public QTableView
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);

Expand Down
90 changes: 53 additions & 37 deletions src/autotype/AutoTypeSelectDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -57,7 +58,6 @@ AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
}
});

m_ui->search->setFocus();
m_ui->search->installEventFilter(this);

m_searchTimer.setInterval(300);
Expand All @@ -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);
Expand All @@ -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();

// disable changing search scope if we have no direct matches
m_ui->searchCheckBox->setDisabled(noMatches);

m_ui->view->setMatchList(m_matches, !m_matches.isEmpty() || !m_ui->search->text().isEmpty());
m_ui->searchCheckBox->setChecked(m_matches.isEmpty());
// 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)
Expand All @@ -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;
}

auto searchText = m_ui->search->text();
// If no search text, find all entries
if (searchText.isEmpty()) {
searchText.append("*");
}
} else {
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);
}

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->view->setMatchList(matches, !m_ui->search->text().isEmpty());
m_ui->search->setFocus();
}

void AutoTypeSelectDialog::activateCurrentMatch()
Expand Down
5 changes: 4 additions & 1 deletion src/autotype/AutoTypeSelectDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@ class AutoTypeSelectDialog : public QDialog
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:
Expand All @@ -63,6 +65,7 @@ private slots:

QList<QSharedPointer<Database>> m_dbs;
QList<AutoTypeMatch> m_matches;
AutoTypeMatch m_lastMatch;
QTimer m_searchTimer;
QPointer<QMenu> m_actionMenu;

Expand Down

0 comments on commit 6060962

Please sign in to comment.