Improve Auto-Type Select Dialog

Significant improvements to the Auto-Type select dialog. Reduce stale and unnecessary code paths.

* Close select dialog when databases are locked.
* Close open modal dialogs prior to showing the Auto-Type select dialog to prevent interference.
* Never perform Auto-Type on the KeePassXC window.
* Only filter match list based on Group, Title, and Username column data (ie, ignore sequence column)
* Always show the sequence column (revert feature)
* Show selection dialog if there are no matches to allow for a database search

* Close #3630 - Allow typing {USERNAME} and {PASSWORD} from selection dialog (right-click menu).
* Close #429 - Ability to search open databases for an entry from the Auto-Type selection dialog.
* Fix #5361 - Default size of selection dialog doesn't cut off matches
This commit is contained in:
Jonathan White
2021-02-15 17:28:16 -05:00
parent 7ce35f81de
commit d9ae449f04
38 changed files with 830 additions and 1047 deletions

View File

@@ -33,7 +33,6 @@ endif(NOT ZXCVBN_LIBRARIES)
set(keepassx_SOURCES
core/Alloc.cpp
core/AutoTypeAssociations.cpp
core/AutoTypeMatch.cpp
core/Base32.cpp
core/Bootstrap.cpp
core/Clock.cpp
@@ -139,8 +138,6 @@ set(keepassx_SOURCES
gui/csvImport/CsvImportWizard.cpp
gui/csvImport/CsvParserModel.cpp
gui/entry/AutoTypeAssociationsModel.cpp
gui/entry/AutoTypeMatchModel.cpp
gui/entry/AutoTypeMatchView.cpp
gui/entry/EditEntryWidget.cpp
gui/entry/EntryAttachmentsModel.cpp
gui/entry/EntryAttachmentsWidget.cpp
@@ -273,11 +270,10 @@ set(autotype_SOURCES
core/Tools.cpp
autotype/AutoType.cpp
autotype/AutoTypeAction.cpp
autotype/AutoTypeFilterLineEdit.cpp
autotype/AutoTypeMatchModel.cpp
autotype/AutoTypeMatchView.cpp
autotype/AutoTypeSelectDialog.cpp
autotype/AutoTypeSelectView.cpp
autotype/ShortcutWidget.cpp
autotype/WildcardMatcher.cpp
autotype/WindowSelectComboBox.cpp)
if(MINGW)

View File

@@ -21,13 +21,12 @@
#include <QApplication>
#include <QPluginLoader>
#include <QRegularExpression>
#include <QWindow>
#include "config-keepassx.h"
#include "autotype/AutoTypePlatformPlugin.h"
#include "autotype/AutoTypeSelectDialog.h"
#include "autotype/WildcardMatcher.h"
#include "core/AutoTypeMatch.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/Entry.h"
@@ -250,12 +249,10 @@ void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow)
return;
}
QList<QString> sequences = autoTypeSequences(entry);
if (sequences.isEmpty()) {
return;
auto sequences = entry->autoTypeSequences();
if (!sequences.isEmpty()) {
executeAutoTypeActions(entry, hideWindow, sequences.first());
}
executeAutoTypeActions(entry, hideWindow, sequences.first());
}
/**
@@ -273,6 +270,11 @@ void AutoType::performAutoTypeWithSequence(const Entry* entry, const QString& se
void AutoType::startGlobalAutoType()
{
// Never Auto-Type into KeePassXC itself
if (qApp->focusWindow()) {
return;
}
m_windowForGlobal = m_plugin->activeWindow();
m_windowTitleForGlobal = m_plugin->activeWindowTitle();
#ifdef Q_OS_MACOS
@@ -331,58 +333,62 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
for (const auto& db : dbList) {
const QList<Entry*> dbEntries = db->rootGroup()->entriesRecursive();
for (Entry* entry : dbEntries) {
for (auto entry : dbEntries) {
auto group = entry->group();
if (!group || !group->resolveAutoTypeEnabled() || !entry->autoTypeEnabled()) {
continue;
}
if (hideExpired && entry->isExpired()) {
continue;
}
const QSet<QString> sequences = autoTypeSequences(entry, m_windowTitleForGlobal).toSet();
for (const QString& sequence : sequences) {
if (!sequence.isEmpty()) {
matchList << AutoTypeMatch(entry, sequence);
}
auto sequences = entry->autoTypeSequences(m_windowTitleForGlobal).toSet();
for (const auto& sequence : sequences) {
matchList << AutoTypeMatch(entry, sequence);
}
}
}
if (matchList.isEmpty()) {
if (qobject_cast<QApplication*>(QCoreApplication::instance())) {
auto* msgBox = new QMessageBox();
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->setWindowTitle(tr("Auto-Type - KeePassXC"));
msgBox->setText(tr("Couldn't find an entry that matches the window title:")
.append("\n\n")
.append(m_windowTitleForGlobal));
msgBox->setIcon(QMessageBox::Information);
msgBox->setStandardButtons(QMessageBox::Ok);
#ifdef Q_OS_MACOS
m_plugin->raiseOwnWindow();
Tools::wait(200);
#endif
msgBox->exec();
restoreWindowState();
// Show the selection dialog if we always ask, have multiple matches, or no matches
if (config()->get(Config::Security_AutoTypeAsk).toBool() || matchList.size() > 1 || matchList.isEmpty()) {
// Close any open modal windows that would interfere with the process
if (qApp->modalWindow()) {
qApp->modalWindow()->close();
}
m_inGlobalAutoTypeDialog.unlock();
emit autotypeRejected();
} else if ((matchList.size() == 1) && !config()->get(Config::Security_AutoTypeAsk).toBool()) {
executeAutoTypeActions(matchList.first().entry, nullptr, matchList.first().sequence, m_windowForGlobal);
m_inGlobalAutoTypeDialog.unlock();
} else {
auto* selectDialog = new AutoTypeSelectDialog();
selectDialog->setMatches(matchList, dbList);
// connect slots, both of which must unlock the m_inGlobalAutoTypeDialog mutex
connect(selectDialog, SIGNAL(matchActivated(AutoTypeMatch)), SLOT(performAutoTypeFromGlobal(AutoTypeMatch)));
connect(selectDialog, SIGNAL(rejected()), SLOT(autoTypeRejectedFromGlobal()));
connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject);
connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](AutoTypeMatch match) {
restoreWindowState();
QApplication::processEvents();
m_plugin->raiseWindow(m_windowForGlobal);
executeAutoTypeActions(match.first, nullptr, match.second, m_windowForGlobal);
resetAutoTypeState();
});
connect(selectDialog, &QDialog::rejected, this, [this] {
restoreWindowState();
resetAutoTypeState();
emit autotypeRejected();
});
selectDialog->setMatchList(matchList);
#ifdef Q_OS_MACOS
m_plugin->raiseOwnWindow();
Tools::wait(200);
#endif
selectDialog->show();
selectDialog->raise();
// necessary when the main window is minimized
selectDialog->activateWindow();
} else if (!matchList.isEmpty()) {
// Only one match and not asking, do it!
executeAutoTypeActions(matchList.first().first, nullptr, matchList.first().second, m_windowForGlobal);
resetAutoTypeState();
} else {
// We should never get here
Q_ASSERT(false);
resetAutoTypeState();
emit autotypeRejected();
}
}
@@ -399,29 +405,12 @@ void AutoType::restoreWindowState()
#endif
}
void AutoType::performAutoTypeFromGlobal(AutoTypeMatch match)
void AutoType::resetAutoTypeState()
{
restoreWindowState();
m_plugin->raiseWindow(m_windowForGlobal);
executeAutoTypeActions(match.entry, nullptr, match.sequence, m_windowForGlobal);
// make sure the mutex is definitely locked before we unlock it
Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock());
m_inGlobalAutoTypeDialog.unlock();
}
void AutoType::autoTypeRejectedFromGlobal()
{
// this slot can be called twice when the selection dialog is deleted,
// so make sure the mutex is locked before we try unlocking it
Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock());
m_inGlobalAutoTypeDialog.unlock();
m_windowForGlobal = 0;
m_windowTitleForGlobal.clear();
restoreWindowState();
emit autotypeRejected();
Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock());
m_inGlobalAutoTypeDialog.unlock();
}
/**
@@ -622,101 +611,6 @@ QList<AutoTypeAction*> AutoType::createActionFromTemplate(const QString& tmpl, c
return list;
}
/**
* Retrive the autotype sequences matches for a given windowTitle
* This returns a list with priority ordering. If you don't want duplicates call .toSet() on it.
*/
QList<QString> AutoType::autoTypeSequences(const Entry* entry, const QString& windowTitle)
{
QList<QString> sequenceList;
const Group* group = entry->group();
if (!group || !entry->autoTypeEnabled()) {
return sequenceList;
}
do {
if (group->autoTypeEnabled() == Group::Disable) {
return sequenceList;
} else if (group->autoTypeEnabled() == Group::Enable) {
break;
}
group = group->parentGroup();
} while (group);
if (!windowTitle.isEmpty()) {
const QList<AutoTypeAssociations::Association> assocList = entry->autoTypeAssociations()->getAll();
for (const AutoTypeAssociations::Association& assoc : assocList) {
const QString window = entry->resolveMultiplePlaceholders(assoc.window);
if (windowMatches(windowTitle, window)) {
if (!assoc.sequence.isEmpty()) {
sequenceList.append(assoc.sequence);
} else {
sequenceList.append(entry->effectiveAutoTypeSequence());
}
}
}
if (config()->get(Config::AutoTypeEntryTitleMatch).toBool()
&& windowMatchesTitle(windowTitle, entry->resolvePlaceholder(entry->title()))) {
sequenceList.append(entry->effectiveAutoTypeSequence());
}
if (config()->get(Config::AutoTypeEntryURLMatch).toBool()
&& windowMatchesUrl(windowTitle, entry->resolvePlaceholder(entry->url()))) {
sequenceList.append(entry->effectiveAutoTypeSequence());
}
if (sequenceList.isEmpty()) {
return sequenceList;
}
} else {
sequenceList.append(entry->effectiveAutoTypeSequence());
}
return sequenceList;
}
/**
* Checks if a window title matches a pattern
*/
bool AutoType::windowMatches(const QString& windowTitle, const QString& windowPattern)
{
if (windowPattern.startsWith("//") && windowPattern.endsWith("//") && windowPattern.size() >= 4) {
QRegExp regExp(windowPattern.mid(2, windowPattern.size() - 4), Qt::CaseInsensitive, QRegExp::RegExp2);
return (regExp.indexIn(windowTitle) != -1);
}
return WildcardMatcher(windowTitle).match(windowPattern);
}
/**
* Checks if a window title matches an entry Title
* The entry title should be Spr-compiled by the caller
*/
bool AutoType::windowMatchesTitle(const QString& windowTitle, const QString& resolvedTitle)
{
return !resolvedTitle.isEmpty() && windowTitle.contains(resolvedTitle, Qt::CaseInsensitive);
}
/**
* Checks if a window title matches an entry URL
* The entry URL should be Spr-compiled by the caller
*/
bool AutoType::windowMatchesUrl(const QString& windowTitle, const QString& resolvedUrl)
{
if (!resolvedUrl.isEmpty() && windowTitle.contains(resolvedUrl, Qt::CaseInsensitive)) {
return true;
}
QUrl url(resolvedUrl);
if (url.isValid() && !url.host().isEmpty()) {
return windowTitle.contains(url.host(), Qt::CaseInsensitive);
}
return false;
}
/**
* Checks if the overall syntax of an autotype sequence is fine
*/

View File

@@ -24,7 +24,7 @@
#include <QStringList>
#include <QWidget>
#include "core/AutoTypeMatch.h"
#include "autotype/AutoTypeMatch.h"
class AutoTypeAction;
class AutoTypeExecutor;
@@ -68,8 +68,6 @@ signals:
private slots:
void startGlobalAutoType();
void performAutoTypeFromGlobal(AutoTypeMatch match);
void autoTypeRejectedFromGlobal();
void unloadPlugin();
private:
@@ -89,11 +87,8 @@ private:
WId window = 0);
bool parseActions(const QString& sequence, const Entry* entry, QList<AutoTypeAction*>& actions);
QList<AutoTypeAction*> createActionFromTemplate(const QString& tmpl, const Entry* entry);
QList<QString> autoTypeSequences(const Entry* entry, const QString& windowTitle = QString());
bool windowMatchesTitle(const QString& windowTitle, const QString& resolvedTitle);
bool windowMatchesUrl(const QString& windowTitle, const QString& resolvedUrl);
bool windowMatches(const QString& windowTitle, const QString& windowPattern);
void restoreWindowState();
void resetAutoTypeState();
QMutex m_inAutoType;
QMutex m_inGlobalAutoTypeDialog;

View File

@@ -1,39 +0,0 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AutoTypeFilterLineEdit.h"
#include <QKeyEvent>
void AutoTypeFilterLineEdit::keyPressEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Up) {
emit keyUpPressed();
} else if (event->key() == Qt::Key_Down) {
emit keyDownPressed();
} else {
QLineEdit::keyPressEvent(event);
}
}
void AutoTypeFilterLineEdit::keyReleaseEvent(QKeyEvent* event)
{
if (event->key() == Qt::Key_Escape) {
emit escapeReleased();
} else {
QLineEdit::keyReleaseEvent(event);
}
}

View File

@@ -1,42 +0,0 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_AUTOTYPEFILTERLINEEDIT_H
#define KEEPASSX_AUTOTYPEFILTERLINEEDIT_H
#include <QLineEdit>
class AutoTypeFilterLineEdit : public QLineEdit
{
Q_OBJECT
public:
AutoTypeFilterLineEdit(QWidget* widget)
: QLineEdit(widget)
{
}
protected:
virtual void keyPressEvent(QKeyEvent* event);
virtual void keyReleaseEvent(QKeyEvent* event);
signals:
void keyUpPressed();
void keyDownPressed();
void escapeReleased();
};
#endif // KEEPASSX_AUTOTYPEFILTERLINEEDIT_H

View File

@@ -1,6 +1,5 @@
/*
* Copyright (C) 2015 David Wu <lightvector@gmail.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -16,26 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_AUTOTYPEMATCH_H
#define KEEPASSX_AUTOTYPEMATCH_H
#ifndef KPXC_AUTOTYPEMATCH_H
#define KPXC_AUTOTYPEMATCH_H
#include <QObject>
#include <QPair>
#include <QPointer>
#include <QString>
class Entry;
typedef QPair<QPointer<Entry>, QString> AutoTypeMatch;
struct AutoTypeMatch
{
Entry* entry;
QString sequence;
AutoTypeMatch();
AutoTypeMatch(Entry* entry, QString sequence);
bool operator==(const AutoTypeMatch& other) const;
bool operator!=(const AutoTypeMatch& other) const;
};
Q_DECLARE_TYPEINFO(AutoTypeMatch, Q_MOVABLE_TYPE);
#endif // KEEPASSX_AUTOTYPEMATCH_H
#endif // KPXC_AUTOTYPEMATCH_H

View File

@@ -56,7 +56,7 @@ void AutoTypeMatchModel::setMatchList(const QList<AutoTypeMatch>& matches)
QSet<Database*> databases;
for (AutoTypeMatch& match : m_matches) {
databases.insert(match.entry->group()->database());
databases.insert(match.first->group()->database());
}
for (Database* db : asConst(databases)) {
@@ -88,7 +88,6 @@ int AutoTypeMatchModel::rowCount(const QModelIndex& parent) const
int AutoTypeMatchModel::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return 4;
}
@@ -103,30 +102,30 @@ QVariant AutoTypeMatchModel::data(const QModelIndex& index, int role) const
if (role == Qt::DisplayRole) {
switch (index.column()) {
case ParentGroup:
if (match.entry->group()) {
return match.entry->group()->name();
if (match.first->group()) {
return match.first->group()->name();
}
break;
case Title:
return match.entry->resolveMultiplePlaceholders(match.entry->title());
return match.first->resolveMultiplePlaceholders(match.first->title());
case Username:
return match.entry->resolveMultiplePlaceholders(match.entry->username());
return match.first->resolveMultiplePlaceholders(match.first->username());
case Sequence:
return match.sequence;
return match.second;
}
} else if (role == Qt::DecorationRole) {
switch (index.column()) {
case ParentGroup:
if (match.entry->group()) {
return match.entry->group()->iconPixmap();
if (match.first->group()) {
return match.first->group()->iconPixmap();
}
break;
case Title:
return match.entry->iconPixmap();
return match.first->iconPixmap();
}
} else if (role == Qt::FontRole) {
QFont font;
if (match.entry->isExpired()) {
if (match.first->isExpired()) {
font.setStrikeOut(true);
}
return font;
@@ -157,7 +156,7 @@ void AutoTypeMatchModel::entryDataChanged(Entry* entry)
{
for (int row = 0; row < m_matches.size(); ++row) {
AutoTypeMatch match = m_matches[row];
if (match.entry == entry) {
if (match.first == entry) {
emit dataChanged(index(row, 0), index(row, columnCount() - 1));
}
}
@@ -167,7 +166,7 @@ void AutoTypeMatchModel::entryAboutToRemove(Entry* entry)
{
for (int row = 0; row < m_matches.size(); ++row) {
AutoTypeMatch match = m_matches[row];
if (match.entry == entry) {
if (match.first == entry) {
beginRemoveRows(QModelIndex(), row, row);
m_matches.removeAt(row);
endRemoveRows();

View File

@@ -21,7 +21,7 @@
#include <QAbstractTableModel>
#include "core/AutoTypeMatch.h"
#include "autotype/AutoTypeMatch.h"
class Entry;
class Group;

View File

@@ -0,0 +1,120 @@
/*
* Copyright (C) 2015 David Wu <lightvector@gmail.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AutoTypeMatchView.h"
#include "core/Entry.h"
#include "gui/Clipboard.h"
#include "gui/Icons.h"
#include <QAction>
#include <QHeaderView>
#include <QKeyEvent>
#include <QSortFilterProxyModel>
class CustomSortFilterProxyModel : public QSortFilterProxyModel
{
public:
explicit CustomSortFilterProxyModel(QObject* parent = nullptr)
: QSortFilterProxyModel(parent){};
~CustomSortFilterProxyModel() override = default;
// Only search the first three columns (ie, ignore sequence column)
bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override
{
auto index0 = sourceModel()->index(sourceRow, 0, sourceParent);
auto index1 = sourceModel()->index(sourceRow, 1, sourceParent);
auto index2 = sourceModel()->index(sourceRow, 2, sourceParent);
return sourceModel()->data(index0).toString().contains(filterRegExp())
|| sourceModel()->data(index1).toString().contains(filterRegExp())
|| sourceModel()->data(index2).toString().contains(filterRegExp());
}
};
AutoTypeMatchView::AutoTypeMatchView(QWidget* parent)
: QTableView(parent)
, m_model(new AutoTypeMatchModel(this))
, m_sortModel(new CustomSortFilterProxyModel(this))
{
m_sortModel->setSourceModel(m_model);
m_sortModel->setDynamicSortFilter(true);
m_sortModel->setSortLocaleAware(true);
m_sortModel->setSortCaseSensitivity(Qt::CaseInsensitive);
m_sortModel->setFilterKeyColumn(-1);
m_sortModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
setModel(m_sortModel);
sortByColumn(0, Qt::AscendingOrder);
setContextMenuPolicy(Qt::CustomContextMenu);
connect(this, &QTableView::doubleClicked, this, [this](const QModelIndex& index) {
emit matchActivated(matchFromIndex(index));
});
}
void AutoTypeMatchView::keyPressEvent(QKeyEvent* event)
{
if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && currentIndex().isValid()) {
emit matchActivated(matchFromIndex(currentIndex()));
}
QTableView::keyPressEvent(event);
}
void AutoTypeMatchView::setMatchList(const QList<AutoTypeMatch>& matches)
{
m_model->setMatchList(matches);
m_sortModel->setFilterWildcard({});
horizontalHeader()->resizeSections(QHeaderView::ResizeToContents);
selectionModel()->setCurrentIndex(m_sortModel->index(0, 0),
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
emit currentMatchChanged(currentMatch());
}
void AutoTypeMatchView::filterList(const QString& filter)
{
m_sortModel->setFilterWildcard(filter);
setCurrentIndex(m_sortModel->index(0, 0));
}
AutoTypeMatch AutoTypeMatchView::currentMatch()
{
QModelIndexList list = selectionModel()->selectedRows();
if (list.size() == 1) {
return m_model->matchFromIndex(m_sortModel->mapToSource(list.first()));
}
return {};
}
AutoTypeMatch AutoTypeMatchView::matchFromIndex(const QModelIndex& index)
{
if (index.isValid()) {
return m_model->matchFromIndex(m_sortModel->mapToSource(index));
}
return {};
}
void AutoTypeMatchView::currentChanged(const QModelIndex& current, const QModelIndex& previous)
{
auto match = matchFromIndex(current);
emit currentMatchChanged(match);
QTableView::currentChanged(current, previous);
}

View File

@@ -19,42 +19,37 @@
#ifndef KEEPASSX_AUTOTYPEMATCHVIEW_H
#define KEEPASSX_AUTOTYPEMATCHVIEW_H
#include <QTreeView>
#include <QTableView>
#include "core/AutoTypeMatch.h"
#include "autotype/AutoTypeMatch.h"
#include "autotype/AutoTypeMatchModel.h"
#include "gui/entry/AutoTypeMatchModel.h"
class QSortFilterProxyModel;
class SortFilterHideProxyModel;
class AutoTypeMatchView : public QTreeView
class AutoTypeMatchView : public QTableView
{
Q_OBJECT
public:
explicit AutoTypeMatchView(QWidget* parent = nullptr);
AutoTypeMatch currentMatch();
void setCurrentMatch(const AutoTypeMatch& match);
AutoTypeMatch matchFromIndex(const QModelIndex& index);
void setMatchList(const QList<AutoTypeMatch>& matches);
void setFirstMatchActive();
void filterList(const QString& filter);
signals:
void currentMatchChanged(AutoTypeMatch match);
void matchActivated(AutoTypeMatch match);
void matchSelectionChanged();
void matchTextCopied();
protected:
void keyPressEvent(QKeyEvent* event) override;
private slots:
void emitMatchActivated(const QModelIndex& index);
void userNameCopied();
void passwordCopied();
protected slots:
void currentChanged(const QModelIndex& current, const QModelIndex& previous) override;
private:
AutoTypeMatchModel* const m_model;
SortFilterHideProxyModel* const m_sortModel;
QSortFilterProxyModel* const m_sortModel;
};
#endif // KEEPASSX_AUTOTYPEMATCHVIEW_H

View File

@@ -1,6 +1,6 @@
/*
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* Copyright (C) 2020 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
@@ -17,168 +17,324 @@
*/
#include "AutoTypeSelectDialog.h"
#include "ui_AutoTypeSelectDialog.h"
#include <QApplication>
#include <QCloseEvent>
#include <QMenu>
#include <QShortcut>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
#include <QScreen>
#else
#include <QDesktopWidget>
#endif
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QSortFilterProxyModel>
#include <QVBoxLayout>
#include "autotype/AutoTypeSelectView.h"
#include "core/AutoTypeMatch.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/EntrySearcher.h"
#include "gui/Clipboard.h"
#include "gui/Icons.h"
#include "gui/entry/AutoTypeMatchModel.h"
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
: QDialog(parent)
, m_view(new AutoTypeSelectView(this))
, m_filterLineEdit(new AutoTypeFilterLineEdit(this))
, m_matchActivatedEmitted(false)
, m_rejected(false)
, m_ui(new Ui::AutoTypeSelectDialog())
{
setAttribute(Qt::WA_DeleteOnClose);
// Places the window on the active (virtual) desktop instead of where the main window is.
setAttribute(Qt::WA_X11BypassTransientForHint);
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint);
setWindowTitle(tr("Auto-Type - KeePassXC"));
setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowContextHelpButtonHint);
setWindowIcon(icons()->applicationIcon());
buildActionMenu();
m_ui->setupUi(this);
connect(m_ui->view, &AutoTypeMatchView::matchActivated, this, &AutoTypeSelectDialog::submitAutoTypeMatch);
connect(m_ui->view, &AutoTypeMatchView::currentMatchChanged, this, &AutoTypeSelectDialog::updateActionMenu);
connect(m_ui->view, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
if (m_ui->view->currentMatch().first) {
m_actionMenu->popup(m_ui->view->viewport()->mapToGlobal(pos));
}
});
m_ui->search->setFocus();
m_ui->search->installEventFilter(this);
m_searchTimer.setInterval(300);
m_searchTimer.setSingleShot(true);
connect(m_ui->search, SIGNAL(textChanged(QString)), &m_searchTimer, SLOT(start()));
connect(m_ui->search, SIGNAL(returnPressed()), SLOT(activateCurrentMatch()));
connect(&m_searchTimer, SIGNAL(timeout()), SLOT(performSearch()));
connect(m_ui->filterRadio, &QRadioButton::toggled, this, [this](bool checked) {
if (checked) {
// Reset to original match list
m_ui->view->setMatchList(m_matches);
performSearch();
m_ui->search->setFocus();
}
});
connect(m_ui->searchRadio, &QRadioButton::toggled, this, [this](bool checked) {
if (checked) {
performSearch();
m_ui->search->setFocus();
}
});
m_actionMenu->installEventFilter(this);
m_ui->action->setMenu(m_actionMenu);
m_ui->action->installEventFilter(this);
connect(m_ui->action, &QToolButton::clicked, this, &AutoTypeSelectDialog::activateCurrentMatch);
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
}
// Required for QScopedPointer
AutoTypeSelectDialog::~AutoTypeSelectDialog()
{
}
void AutoTypeSelectDialog::setMatches(const QList<AutoTypeMatch>& matches, const QList<QSharedPointer<Database>>& dbs)
{
m_matches = matches;
m_dbs = dbs;
m_ui->view->setMatchList(m_matches);
if (m_matches.isEmpty()) {
m_ui->searchRadio->setChecked(true);
} else {
m_ui->filterRadio->setChecked(true);
}
}
void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match)
{
m_accepted = true;
accept();
emit matchActivated(std::move(match));
}
void AutoTypeSelectDialog::performSearch()
{
if (m_ui->filterRadio->isChecked()) {
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("*");
}
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);
}
void AutoTypeSelectDialog::moveSelectionUp()
{
auto current = m_ui->view->currentIndex();
auto previous = current.sibling(current.row() - 1, 0);
if (previous.isValid()) {
m_ui->view->setCurrentIndex(previous);
}
}
void AutoTypeSelectDialog::moveSelectionDown()
{
auto current = m_ui->view->currentIndex();
auto next = current.sibling(current.row() + 1, 0);
if (next.isValid()) {
m_ui->view->setCurrentIndex(next);
}
}
void AutoTypeSelectDialog::activateCurrentMatch()
{
submitAutoTypeMatch(m_ui->view->currentMatch());
}
bool AutoTypeSelectDialog::eventFilter(QObject* obj, QEvent* event)
{
if (obj == m_ui->action) {
if (event->type() == QEvent::FocusIn) {
m_ui->action->showMenu();
return true;
} else if (event->type() == QEvent::KeyPress && static_cast<QKeyEvent*>(event)->key() == Qt::Key_Return) {
// handle case where the menu is closed but the button has focus
activateCurrentMatch();
return true;
}
} else if (obj == m_actionMenu) {
if (event->type() == QEvent::KeyPress) {
auto* keyEvent = static_cast<QKeyEvent*>(event);
switch (keyEvent->key()) {
case Qt::Key_Tab:
m_actionMenu->close();
focusNextPrevChild(true);
return true;
case Qt::Key_Backtab:
m_actionMenu->close();
focusNextPrevChild(false);
return true;
case Qt::Key_Return:
// accept the dialog with default sequence if no action selected
if (!m_actionMenu->activeAction()) {
activateCurrentMatch();
return true;
}
default:
break;
}
}
} else if (obj == m_ui->search) {
if (event->type() == QEvent::KeyPress) {
auto* keyEvent = static_cast<QKeyEvent*>(event);
switch (keyEvent->key()) {
case Qt::Key_Up:
moveSelectionUp();
return true;
case Qt::Key_Down:
moveSelectionDown();
return true;
case Qt::Key_Escape:
if (m_ui->search->text().isEmpty()) {
reject();
} else {
m_ui->search->clear();
}
return true;
default:
break;
}
}
}
return QDialog::eventFilter(obj, event);
}
void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
{
if (!match.first) {
m_ui->action->setEnabled(false);
return;
}
m_ui->action->setEnabled(true);
bool hasUsername = !match.first->username().isEmpty();
bool hasPassword = !match.first->password().isEmpty();
bool hasTotp = match.first->hasTotp();
auto actions = m_actionMenu->actions();
Q_ASSERT(actions.size() >= 6);
actions[0]->setEnabled(hasUsername);
actions[1]->setEnabled(hasPassword);
actions[2]->setEnabled(hasTotp);
actions[3]->setEnabled(hasUsername);
actions[4]->setEnabled(hasPassword);
actions[5]->setEnabled(hasTotp);
}
void AutoTypeSelectDialog::buildActionMenu()
{
m_actionMenu = new QMenu(this);
auto typeUsernameAction = new QAction(icons()->icon("auto-type"), tr("Type {USERNAME}"), this);
auto typePasswordAction = new QAction(icons()->icon("auto-type"), tr("Type {PASSWORD}"), this);
auto typeTotpAction = new QAction(icons()->icon("auto-type"), tr("Type {TOTP}"), this);
auto copyUsernameAction = new QAction(icons()->icon("username-copy"), tr("Copy Username"), this);
auto copyPasswordAction = new QAction(icons()->icon("password-copy"), tr("Copy Password"), this);
auto copyTotpAction = new QAction(icons()->icon("chronometer"), tr("Copy TOTP"), this);
m_actionMenu->addAction(typeUsernameAction);
m_actionMenu->addAction(typePasswordAction);
m_actionMenu->addAction(typeTotpAction);
m_actionMenu->addAction(copyUsernameAction);
m_actionMenu->addAction(copyPasswordAction);
m_actionMenu->addAction(copyTotpAction);
connect(typeUsernameAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{USERNAME}";
submitAutoTypeMatch(match);
});
connect(typePasswordAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{PASSWORD}";
submitAutoTypeMatch(match);
});
connect(typeTotpAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{TOTP}";
submitAutoTypeMatch(match);
});
connect(copyUsernameAction, &QAction::triggered, this, [&] {
clipboard()->setText(m_ui->view->currentMatch().first->username());
reject();
});
connect(copyPasswordAction, &QAction::triggered, this, [&] {
clipboard()->setText(m_ui->view->currentMatch().first->password());
reject();
});
connect(copyTotpAction, &QAction::triggered, this, [&] {
clipboard()->setText(m_ui->view->currentMatch().first->totp());
reject();
});
}
void AutoTypeSelectDialog::showEvent(QShowEvent* event)
{
QDialog::showEvent(event);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
QRect screenGeometry = QApplication::screenAt(QCursor::pos())->availableGeometry();
auto screen = QApplication::screenAt(QCursor::pos());
if (!screen) {
// screenAt can return a nullptr, default to the primary screen
screen = QApplication::primaryScreen();
}
QRect screenGeometry = screen->availableGeometry();
#else
QRect screenGeometry = QApplication::desktop()->availableGeometry(QCursor::pos());
#endif
// Resize to last used size
QSize size = config()->get(Config::GUI_AutoTypeSelectDialogSize).toSize();
size.setWidth(qMin(size.width(), screenGeometry.width()));
size.setHeight(qMin(size.height(), screenGeometry.height()));
resize(size);
// move dialog to the center of the screen
QPoint screenCenter = screenGeometry.center();
move(screenCenter.x() - (size.width() / 2), screenCenter.y() - (size.height() / 2));
QVBoxLayout* layout = new QVBoxLayout(this);
QLabel* descriptionLabel = new QLabel(tr("Select entry to Auto-Type:"), this);
layout->addWidget(descriptionLabel);
// clang-format off
connect(m_view, SIGNAL(activated(QModelIndex)), SLOT(emitMatchActivated(QModelIndex)));
connect(m_view, SIGNAL(clicked(QModelIndex)), SLOT(emitMatchActivated(QModelIndex)));
connect(m_view->model(), SIGNAL(rowsRemoved(QModelIndex,int,int)), SLOT(matchRemoved()));
connect(m_view, SIGNAL(rejected()), SLOT(reject()));
connect(m_view, SIGNAL(matchTextCopied()), SLOT(reject()));
// clang-format on
QSortFilterProxyModel* proxy = qobject_cast<QSortFilterProxyModel*>(m_view->model());
if (proxy) {
proxy->setFilterKeyColumn(-1);
proxy->setFilterCaseSensitivity(Qt::CaseInsensitive);
}
layout->addWidget(m_view);
connect(m_filterLineEdit, SIGNAL(textChanged(QString)), SLOT(filterList(QString)));
connect(m_filterLineEdit, SIGNAL(returnPressed()), SLOT(activateCurrentIndex()));
connect(m_filterLineEdit, SIGNAL(keyUpPressed()), SLOT(moveSelectionUp()));
connect(m_filterLineEdit, SIGNAL(keyDownPressed()), SLOT(moveSelectionDown()));
connect(m_filterLineEdit, SIGNAL(escapeReleased()), SLOT(reject()));
m_filterLineEdit->setPlaceholderText(tr("Search…"));
layout->addWidget(m_filterLineEdit);
QDialogButtonBox* buttonBox = new QDialogButtonBox(QDialogButtonBox::Cancel, Qt::Horizontal, this);
connect(buttonBox, SIGNAL(rejected()), SLOT(reject()));
layout->addWidget(buttonBox);
m_filterLineEdit->setFocus();
move(screenGeometry.center().x() - (size.width() / 2), screenGeometry.center().y() - (size.height() / 2));
}
void AutoTypeSelectDialog::setMatchList(const QList<AutoTypeMatch>& matchList)
{
m_view->setMatchList(matchList);
m_view->header()->resizeSections(QHeaderView::ResizeToContents);
}
void AutoTypeSelectDialog::done(int r)
void AutoTypeSelectDialog::hideEvent(QHideEvent* event)
{
config()->set(Config::GUI_AutoTypeSelectDialogSize, size());
QDialog::done(r);
}
void AutoTypeSelectDialog::reject()
{
m_rejected = true;
QDialog::reject();
}
void AutoTypeSelectDialog::emitMatchActivated(const QModelIndex& index)
{
// make sure we don't emit the signal twice when both activated() and clicked() are triggered
if (m_matchActivatedEmitted) {
return;
if (!m_accepted) {
emit rejected();
}
m_matchActivatedEmitted = true;
AutoTypeMatch match = m_view->matchFromIndex(index);
accept();
emit matchActivated(match);
}
void AutoTypeSelectDialog::matchRemoved()
{
if (m_rejected) {
return;
}
if (m_view->model()->rowCount() == 0 && m_filterLineEdit->text().isEmpty()) {
reject();
}
}
void AutoTypeSelectDialog::filterList(QString filterString)
{
QSortFilterProxyModel* proxy = qobject_cast<QSortFilterProxyModel*>(m_view->model());
if (proxy) {
proxy->setFilterWildcard(filterString);
if (!m_view->currentIndex().isValid()) {
m_view->setCurrentIndex(m_view->model()->index(0, 0));
}
}
}
void AutoTypeSelectDialog::moveSelectionUp()
{
auto current = m_view->currentIndex();
auto previous = current.sibling(current.row() - 1, 0);
if (previous.isValid()) {
m_view->setCurrentIndex(previous);
}
}
void AutoTypeSelectDialog::moveSelectionDown()
{
auto current = m_view->currentIndex();
auto next = current.sibling(current.row() + 1, 0);
if (next.isValid()) {
m_view->setCurrentIndex(next);
}
}
void AutoTypeSelectDialog::activateCurrentIndex()
{
emitMatchActivated(m_view->currentIndex());
QDialog::hideEvent(event);
}

View File

@@ -1,4 +1,5 @@
/*
* Copyright (C) 2021 Team KeePassXC <team@keepassxc.org>
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
@@ -18,14 +19,17 @@
#ifndef KEEPASSX_AUTOTYPESELECTDIALOG_H
#define KEEPASSX_AUTOTYPESELECTDIALOG_H
#include <QAbstractItemModel>
#include "autotype/AutoTypeMatch.h"
#include <QDialog>
#include <QHash>
#include <QTimer>
#include "autotype/AutoTypeFilterLineEdit.h"
#include "core/AutoTypeMatch.h"
class Database;
class QMenu;
class AutoTypeSelectView;
namespace Ui
{
class AutoTypeSelectDialog;
}
class AutoTypeSelectDialog : public QDialog
{
@@ -33,28 +37,37 @@ class AutoTypeSelectDialog : public QDialog
public:
explicit AutoTypeSelectDialog(QWidget* parent = nullptr);
void setMatchList(const QList<AutoTypeMatch>& matchList);
~AutoTypeSelectDialog() override;
void setMatches(const QList<AutoTypeMatch>& matchList, const QList<QSharedPointer<Database>>& dbs);
signals:
void matchActivated(AutoTypeMatch match);
public slots:
void done(int r) override;
void reject() override;
protected:
bool eventFilter(QObject* obj, QEvent* event) override;
void showEvent(QShowEvent* event) override;
void hideEvent(QHideEvent* event) override;
private slots:
void emitMatchActivated(const QModelIndex& index);
void matchRemoved();
void filterList(QString filterString);
void submitAutoTypeMatch(AutoTypeMatch match);
void performSearch();
void moveSelectionUp();
void moveSelectionDown();
void activateCurrentIndex();
void activateCurrentMatch();
void updateActionMenu(const AutoTypeMatch& match);
private:
AutoTypeSelectView* const m_view;
AutoTypeFilterLineEdit* const m_filterLineEdit;
bool m_matchActivatedEmitted;
bool m_rejected;
void buildActionMenu();
QScopedPointer<Ui::AutoTypeSelectDialog> m_ui;
QList<QSharedPointer<Database>> m_dbs;
QList<AutoTypeMatch> m_matches;
QTimer m_searchTimer;
QPointer<QMenu> m_actionMenu;
bool m_accepted = false;
};
#endif // KEEPASSX_AUTOTYPESELECTDIALOG_H

View File

@@ -0,0 +1,197 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>AutoTypeSelectDialog</class>
<widget class="QDialog" name="AutoTypeSelectDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>418</width>
<height>295</height>
</rect>
</property>
<property name="windowTitle">
<string>Auto-Type - KeePassXC</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string>Double click a row to perform Auto-Type or find an entry using the search:</string>
</property>
</widget>
</item>
<item>
<widget class="AutoTypeMatchView" name="view">
<property name="sizePolicy">
<sizepolicy hsizetype="Expanding" vsizetype="MinimumExpanding">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>0</width>
<height>175</height>
</size>
</property>
<property name="cursor" stdset="0">
<cursorShape>PointingHandCursor</cursorShape>
</property>
<property name="tabKeyNavigation">
<bool>false</bool>
</property>
<property name="showDropIndicator" stdset="0">
<bool>false</bool>
</property>
<property name="alternatingRowColors">
<bool>true</bool>
</property>
<property name="selectionMode">
<enum>QAbstractItemView::SingleSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
<property name="sortingEnabled">
<bool>true</bool>
</property>
<attribute name="horizontalHeaderShowSortIndicator" stdset="0">
<bool>true</bool>
</attribute>
<attribute name="horizontalHeaderStretchLastSection">
<bool>true</bool>
</attribute>
<attribute name="verticalHeaderVisible">
<bool>false</bool>
</attribute>
</widget>
</item>
<item>
<widget class="QWidget" name="buttonBox_2" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="spacing">
<number>10</number>
</property>
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QRadioButton" name="filterRadio">
<property name="text">
<string>&amp;Filter Matches</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="searchRadio">
<property name="text">
<string>&amp;Search Database</string>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
</layout>
</widget>
</item>
<item>
<widget class="QLineEdit" name="search">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>400</width>
<height>0</height>
</size>
</property>
<property name="placeholderText">
<string>Filter or Search…</string>
</property>
<property name="clearButtonEnabled">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<spacer name="horizontalSpacer_2">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>40</width>
<height>20</height>
</size>
</property>
</spacer>
</item>
<item>
<widget class="QToolButton" name="action">
<property name="text">
<string>Type Sequence</string>
</property>
<property name="popupMode">
<enum>QToolButton::MenuButtonPopup</enum>
</property>
</widget>
</item>
<item>
<widget class="QPushButton" name="cancelButton">
<property name="text">
<string>Cancel</string>
</property>
<property name="autoDefault">
<bool>false</bool>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>AutoTypeMatchView</class>
<extends>QTableView</extends>
<header>autotype/AutoTypeMatchView.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>view</tabstop>
<tabstop>filterRadio</tabstop>
<tabstop>searchRadio</tabstop>
<tabstop>search</tabstop>
</tabstops>
<resources/>
<connections/>
</ui>

View File

@@ -1,62 +0,0 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AutoTypeSelectView.h"
#include <QKeyEvent>
#include <QMouseEvent>
AutoTypeSelectView::AutoTypeSelectView(QWidget* parent)
: AutoTypeMatchView(parent)
{
setMouseTracking(true);
setAllColumnsShowFocus(true);
connect(model(), SIGNAL(modelReset()), SLOT(selectFirstMatch()));
}
void AutoTypeSelectView::mouseMoveEvent(QMouseEvent* event)
{
QModelIndex index = indexAt(event->pos());
if (index.isValid()) {
setCurrentIndex(index);
setCursor(Qt::PointingHandCursor);
} else {
unsetCursor();
}
AutoTypeMatchView::mouseMoveEvent(event);
}
void AutoTypeSelectView::selectFirstMatch()
{
QModelIndex index = model()->index(0, 0);
if (index.isValid()) {
setCurrentIndex(index);
}
}
void AutoTypeSelectView::keyReleaseEvent(QKeyEvent* e)
{
if (e->key() == Qt::Key_Escape) {
emit rejected();
} else {
e->ignore();
}
}

View File

@@ -1,41 +0,0 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_AUTOTYPESELECTVIEW_H
#define KEEPASSX_AUTOTYPESELECTVIEW_H
#include "gui/entry/AutoTypeMatchView.h"
class AutoTypeSelectView : public AutoTypeMatchView
{
Q_OBJECT
public:
explicit AutoTypeSelectView(QWidget* parent = nullptr);
protected:
void mouseMoveEvent(QMouseEvent* event) override;
void keyReleaseEvent(QKeyEvent* e) override;
private slots:
void selectFirstMatch();
signals:
void rejected();
};
#endif // KEEPASSX_AUTOTYPESELECTVIEW_H

View File

@@ -1,96 +0,0 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "WildcardMatcher.h"
#include <QStringList>
#include <utility>
const QChar WildcardMatcher::Wildcard = '*';
const Qt::CaseSensitivity WildcardMatcher::Sensitivity = Qt::CaseInsensitive;
WildcardMatcher::WildcardMatcher(QString text)
: m_text(std::move(text))
{
}
bool WildcardMatcher::match(const QString& pattern)
{
m_pattern = pattern;
if (patternContainsWildcard()) {
return matchWithWildcards();
} else {
return patternEqualsText();
}
}
bool WildcardMatcher::patternContainsWildcard()
{
return m_pattern.contains(Wildcard);
}
bool WildcardMatcher::patternEqualsText()
{
return m_text.compare(m_pattern, Sensitivity) == 0;
}
bool WildcardMatcher::matchWithWildcards()
{
QStringList parts = m_pattern.split(Wildcard, QString::KeepEmptyParts);
Q_ASSERT(parts.size() >= 2);
if (startOrEndDoesNotMatch(parts)) {
return false;
}
return partsMatch(parts);
}
bool WildcardMatcher::startOrEndDoesNotMatch(const QStringList& parts)
{
return !m_text.startsWith(parts.first(), Sensitivity) || !m_text.endsWith(parts.last(), Sensitivity);
}
bool WildcardMatcher::partsMatch(const QStringList& parts)
{
int index = 0;
for (const QString& part : parts) {
int matchIndex = getMatchIndex(part, index);
if (noMatchFound(matchIndex)) {
return false;
}
index = calculateNewIndex(matchIndex, part.length());
}
return true;
}
int WildcardMatcher::getMatchIndex(const QString& part, int startIndex)
{
return m_text.indexOf(part, startIndex, Sensitivity);
}
bool WildcardMatcher::noMatchFound(int index)
{
return index == -1;
}
int WildcardMatcher::calculateNewIndex(int matchIndex, int partLength)
{
return matchIndex + partLength;
}

View File

@@ -1,46 +0,0 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_WILDCARDMATCHER_H
#define KEEPASSX_WILDCARDMATCHER_H
#include <QStringList>
class WildcardMatcher
{
public:
explicit WildcardMatcher(QString text);
bool match(const QString& pattern);
static const QChar Wildcard;
private:
bool patternEqualsText();
bool patternContainsWildcard();
bool matchWithWildcards();
bool startOrEndDoesNotMatch(const QStringList& parts);
bool partsMatch(const QStringList& parts);
int getMatchIndex(const QString& part, int startIndex);
bool noMatchFound(int index);
int calculateNewIndex(int matchIndex, int partLength);
static const Qt::CaseSensitivity Sensitivity;
const QString m_text;
QString m_pattern;
};
#endif // KEEPASSX_WILDCARDMATCHER_H

View File

@@ -70,6 +70,10 @@ BrowserService::BrowserService()
, m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224"))
{
connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage);
connect(getMainWindow(), &MainWindow::databaseUnlocked, this, &BrowserService::databaseUnlocked);
connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked);
connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged);
setEnabled(browserSettings()->isEnabled());
}

View File

@@ -1,43 +0,0 @@
/*
* Copyright (C) 2015 David Wu <lightvector@gmail.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AutoTypeMatch.h"
#include <utility>
AutoTypeMatch::AutoTypeMatch()
: entry(nullptr)
, sequence()
{
}
AutoTypeMatch::AutoTypeMatch(Entry* entry, QString sequence)
: entry(entry)
, sequence(std::move(sequence))
{
}
bool AutoTypeMatch::operator==(const AutoTypeMatch& other) const
{
return entry == other.entry && sequence == other.sequence;
}
bool AutoTypeMatch::operator!=(const AutoTypeMatch& other) const
{
return entry != other.entry || sequence != other.sequence;
}

View File

@@ -20,6 +20,7 @@
#include "config-keepassx.h"
#include "core/Clock.h"
#include "core/Config.h"
#include "core/Database.h"
#include "core/DatabaseIcons.h"
#include "core/Group.h"
@@ -280,6 +281,75 @@ QString Entry::effectiveAutoTypeSequence() const
return sequence;
}
/**
* Retrive the autotype sequences matches for a given windowTitle
* This returns a list with priority ordering. If you don't want duplicates call .toSet() on it.
*/
QList<QString> Entry::autoTypeSequences(const QString& windowTitle) const
{
// If no window just return the effective sequence
if (windowTitle.isEmpty()) {
return {effectiveAutoTypeSequence()};
}
// Define helper functions to match window titles
auto windowMatches = [&](const QString& pattern) {
// Regex searching
if (pattern.startsWith("//") && pattern.endsWith("//") && pattern.size() >= 4) {
QRegExp regExp(pattern.mid(2, pattern.size() - 4), Qt::CaseInsensitive, QRegExp::RegExp2);
return (regExp.indexIn(windowTitle) != -1);
}
// Wildcard searching
auto regex = Tools::convertToRegex(pattern, true, false, false);
return windowTitle.contains(regex);
};
auto windowMatchesTitle = [&](const QString& entryTitle) {
return !entryTitle.isEmpty() && windowTitle.contains(entryTitle, Qt::CaseInsensitive);
};
auto windowMatchesUrl = [&](const QString& entryUrl) {
if (!entryUrl.isEmpty() && windowTitle.contains(entryUrl, Qt::CaseInsensitive)) {
return true;
}
QUrl url(entryUrl);
if (url.isValid() && !url.host().isEmpty()) {
return windowTitle.contains(url.host(), Qt::CaseInsensitive);
}
return false;
};
QList<QString> sequenceList;
// Add window association matches
const auto assocList = autoTypeAssociations()->getAll();
for (const auto& assoc : assocList) {
auto window = resolveMultiplePlaceholders(assoc.window);
if (windowMatches(window)) {
if (!assoc.sequence.isEmpty()) {
sequenceList << assoc.sequence;
} else {
sequenceList << effectiveAutoTypeSequence();
}
}
}
// Try to match window title
if (config()->get(Config::AutoTypeEntryTitleMatch).toBool() && windowMatchesTitle(resolvePlaceholder(title()))) {
sequenceList << effectiveAutoTypeSequence();
}
// Try to match url in window title
if (config()->get(Config::AutoTypeEntryURLMatch).toBool() && windowMatchesUrl(resolvePlaceholder(url()))) {
sequenceList << effectiveAutoTypeSequence();
}
return sequenceList;
}
AutoTypeAssociations* Entry::autoTypeAssociations()
{
return m_autoTypeAssociations;

View File

@@ -95,6 +95,7 @@ public:
QString defaultAutoTypeSequence() const;
QString effectiveAutoTypeSequence() const;
QString effectiveNewAutoTypeSequence() const;
QList<QString> autoTypeSequences(const QString& pattern = {}) const;
AutoTypeAssociations* autoTypeAssociations();
const AutoTypeAssociations* autoTypeAssociations() const;
QString title() const;

View File

@@ -82,7 +82,7 @@ namespace FdoSecrets
});
// make default alias track current activated database
connect(m_databases.data(), &DatabaseTabWidget::activateDatabaseChanged, this, &Service::ensureDefaultAlias);
connect(m_databases.data(), &DatabaseTabWidget::activeDatabaseChanged, this, &Service::ensureDefaultAlias);
return true;
}

View File

@@ -57,8 +57,8 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
// clang-format off
connect(this, SIGNAL(tabCloseRequested(int)), SLOT(closeDatabaseTab(int)));
connect(this, SIGNAL(currentChanged(int)), SLOT(emitActivateDatabaseChanged()));
connect(this, SIGNAL(activateDatabaseChanged(DatabaseWidget*)),
connect(this, SIGNAL(currentChanged(int)), SLOT(emitActiveDatabaseChanged()));
connect(this, SIGNAL(activeDatabaseChanged(DatabaseWidget*)),
m_dbWidgetStateSync, SLOT(setActive(DatabaseWidget*)));
connect(autoType(), SIGNAL(globalAutoTypeTriggered()), SLOT(performGlobalAutoType()));
connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase()));
@@ -715,9 +715,9 @@ void DatabaseTabWidget::updateLastDatabases(const QString& filename)
}
}
void DatabaseTabWidget::emitActivateDatabaseChanged()
void DatabaseTabWidget::emitActiveDatabaseChanged()
{
emit activateDatabaseChanged(currentDatabaseWidget());
emit activeDatabaseChanged(currentDatabaseWidget());
}
void DatabaseTabWidget::emitDatabaseLockChanged()

View File

@@ -89,7 +89,7 @@ signals:
void databaseClosed(const QString& filePath);
void databaseUnlocked(DatabaseWidget* dbWidget);
void databaseLocked(DatabaseWidget* dbWidget);
void activateDatabaseChanged(DatabaseWidget* dbWidget);
void activeDatabaseChanged(DatabaseWidget* dbWidget);
void tabNameChanged();
void tabVisibilityChanged(bool tabsVisible);
void messageGlobal(const QString&, MessageWidget::MessageType type);
@@ -98,7 +98,7 @@ signals:
private slots:
void toggleTabbar();
void emitActivateDatabaseChanged();
void emitActiveDatabaseChanged();
void emitDatabaseLockChanged();
private:

View File

@@ -160,14 +160,13 @@ MainWindow::MainWindow()
restoreGeometry(config()->get(Config::GUI_MainWindowGeometry).toByteArray());
restoreState(config()->get(Config::GUI_MainWindowState).toByteArray());
connect(m_ui->tabWidget, &DatabaseTabWidget::databaseLocked, this, &MainWindow::databaseLocked);
connect(m_ui->tabWidget, &DatabaseTabWidget::databaseUnlocked, this, &MainWindow::databaseUnlocked);
connect(m_ui->tabWidget, &DatabaseTabWidget::activeDatabaseChanged, this, &MainWindow::activeDatabaseChanged);
#ifdef WITH_XC_BROWSER
m_ui->settingsWidget->addSettingsPage(new BrowserSettingsPage());
connect(m_ui->tabWidget, &DatabaseTabWidget::databaseLocked, browserService(), &BrowserService::databaseLocked);
connect(m_ui->tabWidget, &DatabaseTabWidget::databaseUnlocked, browserService(), &BrowserService::databaseUnlocked);
connect(m_ui->tabWidget,
&DatabaseTabWidget::activateDatabaseChanged,
browserService(),
&BrowserService::activeDatabaseChanged);
connect(
browserService(), &BrowserService::requestUnlock, m_ui->tabWidget, &DatabaseTabWidget::performBrowserUnlock);
#endif
@@ -411,7 +410,7 @@ MainWindow::MainWindow()
// Notify search when the active database changes or gets locked
connect(m_ui->tabWidget,
SIGNAL(activateDatabaseChanged(DatabaseWidget*)),
SIGNAL(activeDatabaseChanged(DatabaseWidget*)),
m_searchWidget,
SLOT(databaseChanged(DatabaseWidget*)));
connect(m_ui->tabWidget, SIGNAL(databaseLocked(DatabaseWidget*)), m_searchWidget, SLOT(databaseChanged()));

View File

@@ -60,6 +60,11 @@ public:
PasswordGeneratorScreen = 3
};
signals:
void databaseUnlocked(DatabaseWidget* dbWidget);
void databaseLocked(DatabaseWidget* dbWidget);
void activeDatabaseChanged(DatabaseWidget* dbWidget);
public slots:
void openDatabase(const QString& filePath, const QString& password = {}, const QString& keyfile = {});
void appExit();
@@ -136,8 +141,6 @@ private slots:
void obtainContextFocusLock();
void releaseContextFocusLock();
void agentEnabled(bool enabled);
private slots:
void updateTrayIcon();
private:

View File

@@ -118,7 +118,7 @@
</widget>
</item>
<item row="2" column="0">
<widget class="QLabel" name="label_16">
<widget class="QLabel" name="label_25">
<property name="minimumSize">
<size>
<width>10</width>

View File

@@ -51,8 +51,8 @@ SearchWidget::SearchWidget(QWidget* parent)
connect(m_clearSearchTimer, SIGNAL(timeout()), m_ui->searchEdit, SLOT(clear()));
connect(this, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear()));
new QShortcut(QKeySequence::Find, this, SLOT(searchFocus()), nullptr, Qt::ApplicationShortcut);
new QShortcut(Qt::Key_Escape, m_ui->searchEdit, SLOT(clear()), nullptr, Qt::ApplicationShortcut);
new QShortcut(QKeySequence::Find, this, SLOT(searchFocus()));
new QShortcut(Qt::Key_Escape, m_ui->searchEdit, SLOT(clear()));
m_ui->searchEdit->setPlaceholderText(tr("Search (%1)…", "Search placeholder text, %1 is the keyboard shortcut")
.arg(QKeySequence(QKeySequence::Find).toString(QKeySequence::NativeText)));

View File

@@ -1,154 +0,0 @@
/*
* Copyright (C) 2015 David Wu <lightvector@gmail.com>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 2 or (at your option)
* version 3 of the License.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "AutoTypeMatchView.h"
#include "core/Entry.h"
#include "gui/Clipboard.h"
#include "gui/SortFilterHideProxyModel.h"
#include <QAction>
#include <QHeaderView>
#include <QKeyEvent>
AutoTypeMatchView::AutoTypeMatchView(QWidget* parent)
: QTreeView(parent)
, m_model(new AutoTypeMatchModel(this))
, m_sortModel(new SortFilterHideProxyModel(this))
{
m_sortModel->setSourceModel(m_model);
m_sortModel->setDynamicSortFilter(true);
m_sortModel->setSortLocaleAware(true);
m_sortModel->setSortCaseSensitivity(Qt::CaseInsensitive);
setModel(m_sortModel);
setUniformRowHeights(true);
setRootIsDecorated(false);
setAlternatingRowColors(true);
setDragEnabled(false);
setSortingEnabled(true);
setSelectionMode(QAbstractItemView::SingleSelection);
header()->setDefaultSectionSize(150);
setContextMenuPolicy(Qt::ActionsContextMenu);
auto* copyUserNameAction = new QAction(tr("Copy &username"), this);
auto* copyPasswordAction = new QAction(tr("Copy &password"), this);
addAction(copyUserNameAction);
addAction(copyPasswordAction);
connect(copyUserNameAction, SIGNAL(triggered()), this, SLOT(userNameCopied()));
connect(copyPasswordAction, SIGNAL(triggered()), this, SLOT(passwordCopied()));
connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitMatchActivated(QModelIndex)));
// clang-format off
connect(selectionModel(),
SIGNAL(selectionChanged(QItemSelection,QItemSelection)),
SIGNAL(matchSelectionChanged()));
// clang-format on
}
void AutoTypeMatchView::userNameCopied()
{
clipboard()->setText(currentMatch().entry->username());
emit matchTextCopied();
}
void AutoTypeMatchView::passwordCopied()
{
clipboard()->setText(currentMatch().entry->password());
emit matchTextCopied();
}
void AutoTypeMatchView::keyPressEvent(QKeyEvent* event)
{
if ((event->key() == Qt::Key_Enter || event->key() == Qt::Key_Return) && currentIndex().isValid()) {
emitMatchActivated(currentIndex());
#ifdef Q_OS_MACOS
// Pressing return does not emit the QTreeView::activated signal on mac os
emit activated(currentIndex());
#endif
}
QTreeView::keyPressEvent(event);
}
void AutoTypeMatchView::setMatchList(const QList<AutoTypeMatch>& matches)
{
m_model->setMatchList(matches);
bool sameSequences = true;
if (matches.count() > 1) {
QString sequenceTest = matches[0].sequence;
for (const auto& match : matches) {
if (match.sequence != sequenceTest) {
sameSequences = false;
break;
}
}
}
setColumnHidden(AutoTypeMatchModel::Sequence, sameSequences);
for (int i = 0; i < m_model->columnCount(); ++i) {
resizeColumnToContents(i);
if (columnWidth(i) > 250) {
setColumnWidth(i, 250);
}
}
setFirstMatchActive();
}
void AutoTypeMatchView::setFirstMatchActive()
{
if (m_model->rowCount() > 0) {
QModelIndex index = m_sortModel->mapToSource(m_sortModel->index(0, 0));
setCurrentMatch(m_model->matchFromIndex(index));
} else {
emit matchSelectionChanged();
}
}
void AutoTypeMatchView::emitMatchActivated(const QModelIndex& index)
{
AutoTypeMatch match = matchFromIndex(index);
emit matchActivated(match);
}
AutoTypeMatch AutoTypeMatchView::currentMatch()
{
QModelIndexList list = selectionModel()->selectedRows();
if (list.size() == 1) {
return m_model->matchFromIndex(m_sortModel->mapToSource(list.first()));
}
return AutoTypeMatch();
}
void AutoTypeMatchView::setCurrentMatch(const AutoTypeMatch& match)
{
selectionModel()->setCurrentIndex(m_sortModel->mapFromSource(m_model->indexFromMatch(match)),
QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
}
AutoTypeMatch AutoTypeMatchView::matchFromIndex(const QModelIndex& index)
{
if (index.isValid()) {
return m_model->matchFromIndex(m_sortModel->mapToSource(index));
}
return AutoTypeMatch();
}