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
|
||||
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%]
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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)
|
||||
};
|
||||
|
@ -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();
|
||||
|
@ -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;
|
||||
|
@ -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)
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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;
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user