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

@@ -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,5 +1,5 @@
/*
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* 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
@@ -15,27 +15,14 @@
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSX_AUTOTYPESELECTVIEW_H
#define KEEPASSX_AUTOTYPESELECTVIEW_H
#ifndef KPXC_AUTOTYPEMATCH_H
#define KPXC_AUTOTYPEMATCH_H
#include "gui/entry/AutoTypeMatchView.h"
#include <QPair>
#include <QPointer>
#include <QString>
class AutoTypeSelectView : public AutoTypeMatchView
{
Q_OBJECT
class Entry;
typedef QPair<QPointer<Entry>, QString> AutoTypeMatch;
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
#endif // KPXC_AUTOTYPEMATCH_H

View File

@@ -0,0 +1,194 @@
/*
* 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 "AutoTypeMatchModel.h"
#include <QFont>
#include "core/DatabaseIcons.h"
#include "core/Entry.h"
#include "core/Global.h"
#include "core/Group.h"
#include "core/Metadata.h"
AutoTypeMatchModel::AutoTypeMatchModel(QObject* parent)
: QAbstractTableModel(parent)
{
}
AutoTypeMatch AutoTypeMatchModel::matchFromIndex(const QModelIndex& index) const
{
Q_ASSERT(index.isValid() && index.row() < m_matches.size());
return m_matches.at(index.row());
}
QModelIndex AutoTypeMatchModel::indexFromMatch(const AutoTypeMatch& match) const
{
int row = m_matches.indexOf(match);
Q_ASSERT(row != -1);
return index(row, 1);
}
void AutoTypeMatchModel::setMatchList(const QList<AutoTypeMatch>& matches)
{
beginResetModel();
severConnections();
m_allGroups.clear();
m_matches = matches;
QSet<Database*> databases;
for (AutoTypeMatch& match : m_matches) {
databases.insert(match.first->group()->database());
}
for (Database* db : asConst(databases)) {
Q_ASSERT(db);
for (const Group* group : db->rootGroup()->groupsRecursive(true)) {
m_allGroups.append(group);
}
if (db->metadata()->recycleBin()) {
m_allGroups.removeOne(db->metadata()->recycleBin());
}
}
for (const Group* group : asConst(m_allGroups)) {
makeConnections(group);
}
endResetModel();
}
int AutoTypeMatchModel::rowCount(const QModelIndex& parent) const
{
if (parent.isValid()) {
return 0;
}
return m_matches.size();
}
int AutoTypeMatchModel::columnCount(const QModelIndex& parent) const
{
Q_UNUSED(parent);
return 4;
}
QVariant AutoTypeMatchModel::data(const QModelIndex& index, int role) const
{
if (!index.isValid()) {
return {};
}
AutoTypeMatch match = matchFromIndex(index);
if (role == Qt::DisplayRole) {
switch (index.column()) {
case ParentGroup:
if (match.first->group()) {
return match.first->group()->name();
}
break;
case Title:
return match.first->resolveMultiplePlaceholders(match.first->title());
case Username:
return match.first->resolveMultiplePlaceholders(match.first->username());
case Sequence:
return match.second;
}
} else if (role == Qt::DecorationRole) {
switch (index.column()) {
case ParentGroup:
if (match.first->group()) {
return match.first->group()->iconPixmap();
}
break;
case Title:
return match.first->iconPixmap();
}
} else if (role == Qt::FontRole) {
QFont font;
if (match.first->isExpired()) {
font.setStrikeOut(true);
}
return font;
}
return {};
}
QVariant AutoTypeMatchModel::headerData(int section, Qt::Orientation orientation, int role) const
{
if (orientation == Qt::Horizontal && role == Qt::DisplayRole) {
switch (section) {
case ParentGroup:
return tr("Group");
case Title:
return tr("Title");
case Username:
return tr("Username");
case Sequence:
return tr("Sequence");
}
}
return {};
}
void AutoTypeMatchModel::entryDataChanged(Entry* entry)
{
for (int row = 0; row < m_matches.size(); ++row) {
AutoTypeMatch match = m_matches[row];
if (match.first == entry) {
emit dataChanged(index(row, 0), index(row, columnCount() - 1));
}
}
}
void AutoTypeMatchModel::entryAboutToRemove(Entry* entry)
{
for (int row = 0; row < m_matches.size(); ++row) {
AutoTypeMatch match = m_matches[row];
if (match.first == entry) {
beginRemoveRows(QModelIndex(), row, row);
m_matches.removeAt(row);
endRemoveRows();
--row;
}
}
}
void AutoTypeMatchModel::entryRemoved()
{
}
void AutoTypeMatchModel::severConnections()
{
for (const Group* group : asConst(m_allGroups)) {
disconnect(group, nullptr, this, nullptr);
}
}
void AutoTypeMatchModel::makeConnections(const Group* group)
{
connect(group, SIGNAL(entryAboutToRemove(Entry*)), SLOT(entryAboutToRemove(Entry*)));
connect(group, SIGNAL(entryRemoved(Entry*)), SLOT(entryRemoved()));
connect(group, SIGNAL(entryDataChanged(Entry*)), SLOT(entryDataChanged(Entry*)));
}

View File

@@ -0,0 +1,66 @@
/*
* 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/>.
*/
#ifndef KEEPASSX_AUTOTYPEMATCHMODEL_H
#define KEEPASSX_AUTOTYPEMATCHMODEL_H
#include <QAbstractTableModel>
#include "autotype/AutoTypeMatch.h"
class Entry;
class Group;
class AutoTypeMatchModel : public QAbstractTableModel
{
Q_OBJECT
public:
enum ModelColumn
{
ParentGroup = 0,
Title = 1,
Username = 2,
Sequence = 3
};
explicit AutoTypeMatchModel(QObject* parent = nullptr);
AutoTypeMatch matchFromIndex(const QModelIndex& index) const;
QModelIndex indexFromMatch(const AutoTypeMatch& match) const;
int rowCount(const QModelIndex& parent = QModelIndex()) const override;
int columnCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override;
void setMatchList(const QList<AutoTypeMatch>& matches);
private slots:
void entryAboutToRemove(Entry* entry);
void entryRemoved();
void entryDataChanged(Entry* entry);
private:
void severConnections();
void makeConnections(const Group* group);
QList<AutoTypeMatch> m_matches;
QList<const Group*> m_allGroups;
};
#endif // KEEPASSX_AUTOTYPEMATCHMODEL_H

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

@@ -0,0 +1,55 @@
/*
* 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/>.
*/
#ifndef KEEPASSX_AUTOTYPEMATCHVIEW_H
#define KEEPASSX_AUTOTYPEMATCHVIEW_H
#include <QTableView>
#include "autotype/AutoTypeMatch.h"
#include "autotype/AutoTypeMatchModel.h"
class QSortFilterProxyModel;
class AutoTypeMatchView : public QTableView
{
Q_OBJECT
public:
explicit AutoTypeMatchView(QWidget* parent = nullptr);
AutoTypeMatch currentMatch();
AutoTypeMatch matchFromIndex(const QModelIndex& index);
void setMatchList(const QList<AutoTypeMatch>& matches);
void filterList(const QString& filter);
signals:
void currentMatchChanged(AutoTypeMatch match);
void matchActivated(AutoTypeMatch match);
protected:
void keyPressEvent(QKeyEvent* event) override;
protected slots:
void currentChanged(const QModelIndex& current, const QModelIndex& previous) override;
private:
AutoTypeMatchModel* const m_model;
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,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