mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2024-12-24 23:09:44 -05:00
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:
parent
d3d7bd7b81
commit
606096278b
@ -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%]
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
|
@ -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)
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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()
|
||||||
|
@ -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;
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user