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
No known key found for this signature in database
GPG Key ID: 440FC65F2E0C6E01
38 changed files with 830 additions and 1047 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

View File

@ -69,12 +69,22 @@ image::autotype_entry_sequences.png[]
=== Performing Global Auto-Type === Performing Global Auto-Type
The global Auto-Type keyboard shortcut is used when you have focus on the window you want to type into. To make use of this feature, you must have previously configured an Auto-Type hotkey. The global Auto-Type keyboard shortcut is used when you have focus on the window you want to type into. To make use of this feature, you must have previously configured an Auto-Type hotkey.
Pressing the global Auto-Type hotkey cause KeePassXC to search the database for entries that match the window title. Multiple matches may be returned and will cause the sequence selection dialog to appear. Click on a sequence line will immediately execute the Auto-Type action. A search box is also available in case numerous matches are returned. When you press the global Auto-Type hotkey, KeePassXC searches all unlocked databases for entries that match the focused window title. The Auto-Type selection dialog will appear in the following circumstances: there are no matches found, there are multiple matches found, or the setting "Always ask before performing Auto-Type" is enabled.
.Auto-Type sequence selection .Auto-Type sequence selection
image::autotype_selection_dialog.png[,70%] image::autotype_selection_dialog.png[,70%]
TIP: The _Sequence_ column will only appear when there are different sequences defined by one or more entries displayed in the selection dialog. Perform the selected Auto-Type sequence by double clicking the desired row or pressing _Enter_. Press the up and down arrows to navigate the list. Sequences can be filtered through the text edit field.
.Auto-Type search database
image::autotype_selection_dialog_search.png[,70%]
Search the unlocked databases by activating Search Database radio button. Use the text edit field to issue search queries using the same syntax as database searching.
.Additional Auto-Type choices
image::autotype_selection_dialog_type_menu.png[,70%]
The option to type just the username, password, or current TOTP value is available by right clicking the desired row or expanding the Type Sequence button options.
=== Performing Entry-Level Auto-Type === Performing Entry-Level Auto-Type
You can quickly activate the default Auto-Type sequence for a particular entry using Entry-Level Auto-Type. For this operation, the KeePassXC window will be minimized and the Auto-Type sequence occurs in the previously selected window. You can perform Entry-Level Auto-Type from the toolbar icon *(A)*, entry context menu *(B)*, or by pressing `Ctrl+Shift+V`. You can quickly activate the default Auto-Type sequence for a particular entry using Entry-Level Auto-Type. For this operation, the KeePassXC window will be minimized and the Auto-Type sequence occurs in the previously selected window. You can perform Entry-Level Auto-Type from the toolbar icon *(A)*, entry context menu *(B)*, or by pressing `Ctrl+Shift+V`.

View File

@ -60,3 +60,5 @@ To install the KeePassXC app on macOS, double click on the downloaded DMG file a
image::macos_install.png[,80%] image::macos_install.png[,80%]
// end::content[] // end::content[]
// tag::advanced[]
// end::advanced[]

View File

@ -44,3 +44,5 @@ KeePassXC has numerous features for novice and power users alike. This guide wil
** FreeDesktop.org Secret Service (replace Gnome keyring, etc.) ** FreeDesktop.org Secret Service (replace Gnome keyring, etc.)
** Additional encryption choices: Twofish and ChaCha20 ** Additional encryption choices: Twofish and ChaCha20
// end::content[] // end::content[]
// tag::advanced[]
// end::advanced[]

View File

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

View File

@ -21,13 +21,12 @@
#include <QApplication> #include <QApplication>
#include <QPluginLoader> #include <QPluginLoader>
#include <QRegularExpression> #include <QRegularExpression>
#include <QWindow>
#include "config-keepassx.h" #include "config-keepassx.h"
#include "autotype/AutoTypePlatformPlugin.h" #include "autotype/AutoTypePlatformPlugin.h"
#include "autotype/AutoTypeSelectDialog.h" #include "autotype/AutoTypeSelectDialog.h"
#include "autotype/WildcardMatcher.h"
#include "core/AutoTypeMatch.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/Database.h" #include "core/Database.h"
#include "core/Entry.h" #include "core/Entry.h"
@ -250,12 +249,10 @@ void AutoType::performAutoType(const Entry* entry, QWidget* hideWindow)
return; return;
} }
QList<QString> sequences = autoTypeSequences(entry); auto sequences = entry->autoTypeSequences();
if (sequences.isEmpty()) { if (!sequences.isEmpty()) {
return;
}
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() void AutoType::startGlobalAutoType()
{ {
// Never Auto-Type into KeePassXC itself
if (qApp->focusWindow()) {
return;
}
m_windowForGlobal = m_plugin->activeWindow(); m_windowForGlobal = m_plugin->activeWindow();
m_windowTitleForGlobal = m_plugin->activeWindowTitle(); m_windowTitleForGlobal = m_plugin->activeWindowTitle();
#ifdef Q_OS_MACOS #ifdef Q_OS_MACOS
@ -331,58 +333,62 @@ void AutoType::performGlobalAutoType(const QList<QSharedPointer<Database>>& dbLi
for (const auto& db : dbList) { for (const auto& db : dbList) {
const QList<Entry*> dbEntries = db->rootGroup()->entriesRecursive(); 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()) { if (hideExpired && entry->isExpired()) {
continue; continue;
} }
const QSet<QString> sequences = autoTypeSequences(entry, m_windowTitleForGlobal).toSet(); auto sequences = entry->autoTypeSequences(m_windowTitleForGlobal).toSet();
for (const QString& sequence : sequences) { for (const auto& sequence : sequences) {
if (!sequence.isEmpty()) {
matchList << AutoTypeMatch(entry, sequence); matchList << AutoTypeMatch(entry, sequence);
} }
} }
} }
// 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();
} }
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();
}
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(); auto* selectDialog = new AutoTypeSelectDialog();
selectDialog->setMatches(matchList, dbList);
// connect slots, both of which must unlock the m_inGlobalAutoTypeDialog mutex connect(getMainWindow(), &MainWindow::databaseLocked, selectDialog, &AutoTypeSelectDialog::reject);
connect(selectDialog, SIGNAL(matchActivated(AutoTypeMatch)), SLOT(performAutoTypeFromGlobal(AutoTypeMatch))); connect(selectDialog, &AutoTypeSelectDialog::matchActivated, this, [this](AutoTypeMatch match) {
connect(selectDialog, SIGNAL(rejected()), SLOT(autoTypeRejectedFromGlobal())); 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 #ifdef Q_OS_MACOS
m_plugin->raiseOwnWindow(); m_plugin->raiseOwnWindow();
Tools::wait(200); Tools::wait(200);
#endif #endif
selectDialog->show(); selectDialog->show();
selectDialog->raise(); selectDialog->raise();
// necessary when the main window is minimized
selectDialog->activateWindow(); 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 #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_windowForGlobal = 0;
m_windowTitleForGlobal.clear(); m_windowTitleForGlobal.clear();
Q_UNUSED(m_inGlobalAutoTypeDialog.tryLock());
restoreWindowState(); m_inGlobalAutoTypeDialog.unlock();
emit autotypeRejected();
} }
/** /**
@ -622,101 +611,6 @@ QList<AutoTypeAction*> AutoType::createActionFromTemplate(const QString& tmpl, c
return list; 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 * Checks if the overall syntax of an autotype sequence is fine
*/ */

View File

@ -24,7 +24,7 @@
#include <QStringList> #include <QStringList>
#include <QWidget> #include <QWidget>
#include "core/AutoTypeMatch.h" #include "autotype/AutoTypeMatch.h"
class AutoTypeAction; class AutoTypeAction;
class AutoTypeExecutor; class AutoTypeExecutor;
@ -68,8 +68,6 @@ signals:
private slots: private slots:
void startGlobalAutoType(); void startGlobalAutoType();
void performAutoTypeFromGlobal(AutoTypeMatch match);
void autoTypeRejectedFromGlobal();
void unloadPlugin(); void unloadPlugin();
private: private:
@ -89,11 +87,8 @@ private:
WId window = 0); WId window = 0);
bool parseActions(const QString& sequence, const Entry* entry, QList<AutoTypeAction*>& actions); bool parseActions(const QString& sequence, const Entry* entry, QList<AutoTypeAction*>& actions);
QList<AutoTypeAction*> createActionFromTemplate(const QString& tmpl, const Entry* entry); 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 restoreWindowState();
void resetAutoTypeState();
QMutex m_inAutoType; QMutex m_inAutoType;
QMutex m_inGlobalAutoTypeDialog; 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) 2021 KeePassXC Team <team@keepassxc.org>
* Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
* *
* This program is free software: you can redistribute it and/or modify * 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 * 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/>. * along with this program. If not, see <http://www.gnu.org/licenses/>.
*/ */
#ifndef KEEPASSX_AUTOTYPEMATCH_H #ifndef KPXC_AUTOTYPEMATCH_H
#define KEEPASSX_AUTOTYPEMATCH_H #define KPXC_AUTOTYPEMATCH_H
#include <QObject> #include <QPair>
#include <QPointer>
#include <QString> #include <QString>
class Entry; class Entry;
typedef QPair<QPointer<Entry>, QString> AutoTypeMatch;
struct AutoTypeMatch #endif // KPXC_AUTOTYPEMATCH_H
{
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

View File

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

View File

@ -21,7 +21,7 @@
#include <QAbstractTableModel> #include <QAbstractTableModel>
#include "core/AutoTypeMatch.h" #include "autotype/AutoTypeMatch.h"
class Entry; class Entry;
class Group; 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 #ifndef KEEPASSX_AUTOTYPEMATCHVIEW_H
#define 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 QTableView
class AutoTypeMatchView : public QTreeView
{ {
Q_OBJECT Q_OBJECT
public: public:
explicit AutoTypeMatchView(QWidget* parent = nullptr); explicit AutoTypeMatchView(QWidget* parent = nullptr);
AutoTypeMatch currentMatch(); AutoTypeMatch currentMatch();
void setCurrentMatch(const AutoTypeMatch& match);
AutoTypeMatch matchFromIndex(const QModelIndex& index); AutoTypeMatch matchFromIndex(const QModelIndex& index);
void setMatchList(const QList<AutoTypeMatch>& matches); void setMatchList(const QList<AutoTypeMatch>& matches);
void setFirstMatchActive(); void filterList(const QString& filter);
signals: signals:
void currentMatchChanged(AutoTypeMatch match);
void matchActivated(AutoTypeMatch match); void matchActivated(AutoTypeMatch match);
void matchSelectionChanged();
void matchTextCopied();
protected: protected:
void keyPressEvent(QKeyEvent* event) override; void keyPressEvent(QKeyEvent* event) override;
private slots: protected slots:
void emitMatchActivated(const QModelIndex& index); void currentChanged(const QModelIndex& current, const QModelIndex& previous) override;
void userNameCopied();
void passwordCopied();
private: private:
AutoTypeMatchModel* const m_model; AutoTypeMatchModel* const m_model;
SortFilterHideProxyModel* const m_sortModel; QSortFilterProxyModel* const m_sortModel;
}; };
#endif // KEEPASSX_AUTOTYPEMATCHVIEW_H #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) 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 * 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 * it under the terms of the GNU General Public License as published by
@ -17,168 +17,324 @@
*/ */
#include "AutoTypeSelectDialog.h" #include "AutoTypeSelectDialog.h"
#include "ui_AutoTypeSelectDialog.h"
#include <QApplication> #include <QCloseEvent>
#include <QMenu>
#include <QShortcut>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) #if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
#include <QScreen> #include <QScreen>
#else #else
#include <QDesktopWidget> #include <QDesktopWidget>
#endif #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/Config.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/EntrySearcher.h"
#include "gui/Clipboard.h"
#include "gui/Icons.h" #include "gui/Icons.h"
#include "gui/entry/AutoTypeMatchModel.h"
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent) AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
: QDialog(parent) : QDialog(parent)
, m_view(new AutoTypeSelectView(this)) , m_ui(new Ui::AutoTypeSelectDialog())
, m_filterLineEdit(new AutoTypeFilterLineEdit(this))
, m_matchActivatedEmitted(false)
, m_rejected(false)
{ {
setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_DeleteOnClose);
// Places the window on the active (virtual) desktop instead of where the main window is. // Places the window on the active (virtual) desktop instead of where the main window is.
setAttribute(Qt::WA_X11BypassTransientForHint); setAttribute(Qt::WA_X11BypassTransientForHint);
setWindowFlags(windowFlags() | Qt::WindowStaysOnTopHint); setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowContextHelpButtonHint);
setWindowTitle(tr("Auto-Type - KeePassXC"));
setWindowIcon(icons()->applicationIcon()); 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) #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 #else
QRect screenGeometry = QApplication::desktop()->availableGeometry(QCursor::pos()); QRect screenGeometry = QApplication::desktop()->availableGeometry(QCursor::pos());
#endif #endif
// Resize to last used size
QSize size = config()->get(Config::GUI_AutoTypeSelectDialogSize).toSize(); QSize size = config()->get(Config::GUI_AutoTypeSelectDialogSize).toSize();
size.setWidth(qMin(size.width(), screenGeometry.width())); size.setWidth(qMin(size.width(), screenGeometry.width()));
size.setHeight(qMin(size.height(), screenGeometry.height())); size.setHeight(qMin(size.height(), screenGeometry.height()));
resize(size); resize(size);
// move dialog to the center of the screen // move dialog to the center of the screen
QPoint screenCenter = screenGeometry.center(); move(screenGeometry.center().x() - (size.width() / 2), screenGeometry.center().y() - (size.height() / 2));
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();
} }
void AutoTypeSelectDialog::setMatchList(const QList<AutoTypeMatch>& matchList) void AutoTypeSelectDialog::hideEvent(QHideEvent* event)
{
m_view->setMatchList(matchList);
m_view->header()->resizeSections(QHeaderView::ResizeToContents);
}
void AutoTypeSelectDialog::done(int r)
{ {
config()->set(Config::GUI_AutoTypeSelectDialogSize, size()); config()->set(Config::GUI_AutoTypeSelectDialogSize, size());
if (!m_accepted) {
QDialog::done(r); emit rejected();
}
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;
} }
m_matchActivatedEmitted = true; QDialog::hideEvent(event);
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());
} }

View File

@ -1,4 +1,5 @@
/* /*
* Copyright (C) 2021 Team KeePassXC <team@keepassxc.org>
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de> * Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
@ -18,14 +19,17 @@
#ifndef KEEPASSX_AUTOTYPESELECTDIALOG_H #ifndef KEEPASSX_AUTOTYPESELECTDIALOG_H
#define KEEPASSX_AUTOTYPESELECTDIALOG_H #define KEEPASSX_AUTOTYPESELECTDIALOG_H
#include <QAbstractItemModel> #include "autotype/AutoTypeMatch.h"
#include <QDialog> #include <QDialog>
#include <QHash> #include <QTimer>
#include "autotype/AutoTypeFilterLineEdit.h" class Database;
#include "core/AutoTypeMatch.h" class QMenu;
class AutoTypeSelectView; namespace Ui
{
class AutoTypeSelectDialog;
}
class AutoTypeSelectDialog : public QDialog class AutoTypeSelectDialog : public QDialog
{ {
@ -33,28 +37,37 @@ class AutoTypeSelectDialog : public QDialog
public: public:
explicit AutoTypeSelectDialog(QWidget* parent = nullptr); 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: signals:
void matchActivated(AutoTypeMatch match); void matchActivated(AutoTypeMatch match);
public slots: protected:
void done(int r) override; bool eventFilter(QObject* obj, QEvent* event) override;
void reject() override; void showEvent(QShowEvent* event) override;
void hideEvent(QHideEvent* event) override;
private slots: private slots:
void emitMatchActivated(const QModelIndex& index); void submitAutoTypeMatch(AutoTypeMatch match);
void matchRemoved(); void performSearch();
void filterList(QString filterString);
void moveSelectionUp(); void moveSelectionUp();
void moveSelectionDown(); void moveSelectionDown();
void activateCurrentIndex(); void activateCurrentMatch();
void updateActionMenu(const AutoTypeMatch& match);
private: private:
AutoTypeSelectView* const m_view; void buildActionMenu();
AutoTypeFilterLineEdit* const m_filterLineEdit;
bool m_matchActivatedEmitted; QScopedPointer<Ui::AutoTypeSelectDialog> m_ui;
bool m_rejected;
QList<QSharedPointer<Database>> m_dbs;
QList<AutoTypeMatch> m_matches;
QTimer m_searchTimer;
QPointer<QMenu> m_actionMenu;
bool m_accepted = false;
}; };
#endif // KEEPASSX_AUTOTYPESELECTDIALOG_H #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")) , m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224"))
{ {
connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage); 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()); 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 "config-keepassx.h"
#include "core/Clock.h" #include "core/Clock.h"
#include "core/Config.h"
#include "core/Database.h" #include "core/Database.h"
#include "core/DatabaseIcons.h" #include "core/DatabaseIcons.h"
#include "core/Group.h" #include "core/Group.h"
@ -280,6 +281,75 @@ QString Entry::effectiveAutoTypeSequence() const
return sequence; 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() AutoTypeAssociations* Entry::autoTypeAssociations()
{ {
return m_autoTypeAssociations; return m_autoTypeAssociations;

View File

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

View File

@ -82,7 +82,7 @@ namespace FdoSecrets
}); });
// make default alias track current activated database // 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; return true;
} }

View File

@ -57,8 +57,8 @@ DatabaseTabWidget::DatabaseTabWidget(QWidget* parent)
// clang-format off // clang-format off
connect(this, SIGNAL(tabCloseRequested(int)), SLOT(closeDatabaseTab(int))); connect(this, SIGNAL(tabCloseRequested(int)), SLOT(closeDatabaseTab(int)));
connect(this, SIGNAL(currentChanged(int)), SLOT(emitActivateDatabaseChanged())); connect(this, SIGNAL(currentChanged(int)), SLOT(emitActiveDatabaseChanged()));
connect(this, SIGNAL(activateDatabaseChanged(DatabaseWidget*)), connect(this, SIGNAL(activeDatabaseChanged(DatabaseWidget*)),
m_dbWidgetStateSync, SLOT(setActive(DatabaseWidget*))); m_dbWidgetStateSync, SLOT(setActive(DatabaseWidget*)));
connect(autoType(), SIGNAL(globalAutoTypeTriggered()), SLOT(performGlobalAutoType())); connect(autoType(), SIGNAL(globalAutoTypeTriggered()), SLOT(performGlobalAutoType()));
connect(autoType(), SIGNAL(autotypePerformed()), SLOT(relockPendingDatabase())); 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() void DatabaseTabWidget::emitDatabaseLockChanged()

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -149,9 +149,6 @@ add_unit_test(NAME testkeepass1reader SOURCES TestKeePass1Reader.cpp
add_unit_test(NAME testopvaultreader SOURCES TestOpVaultReader.cpp add_unit_test(NAME testopvaultreader SOURCES TestOpVaultReader.cpp
LIBS ${TEST_LIBRARIES}) LIBS ${TEST_LIBRARIES})
add_unit_test(NAME testwildcardmatcher SOURCES TestWildcardMatcher.cpp
LIBS ${TEST_LIBRARIES})
if(WITH_XC_NETWORKING) if(WITH_XC_NETWORKING)
add_unit_test(NAME testupdatecheck SOURCES TestUpdateCheck.cpp add_unit_test(NAME testupdatecheck SOURCES TestUpdateCheck.cpp
LIBS ${TEST_LIBRARIES}) LIBS ${TEST_LIBRARIES})

View File

@ -1,88 +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 "TestWildcardMatcher.h"
#include "TestGlobal.h"
#include "autotype/WildcardMatcher.h"
QTEST_GUILESS_MAIN(TestWildcardMatcher)
const QString TestWildcardMatcher::DefaultText = QString("some text");
const QString TestWildcardMatcher::AlternativeText = QString("some other text");
void TestWildcardMatcher::testMatcher_data()
{
QTest::addColumn<QString>("text");
QTest::addColumn<QString>("pattern");
QTest::addColumn<bool>("match");
QTest::newRow("MatchWithoutWildcard") << DefaultText << DefaultText << true;
QTest::newRow("NoMatchWithoutWildcard") << DefaultText << QString("no match text") << false;
QTest::newRow("MatchWithOneWildcardInTheMiddle") << AlternativeText << QString("some*text") << true;
QTest::newRow("NoMatchWithOneWildcardInTheMiddle") << AlternativeText << QString("differen*text") << false;
QTest::newRow("MatchWithOneWildcardAtBegin") << DefaultText << QString("*text") << true;
QTest::newRow("NoMatchWithOneWildcardAtBegin") << DefaultText << QString("*text something else") << false;
QTest::newRow("NatchWithOneWildcardAtEnd") << DefaultText << QString("some*") << true;
QTest::newRow("NoMatchWithOneWildcardAtEnd") << DefaultText << QString("some other*") << false;
QTest::newRow("MatchWithMultipleWildcards") << AlternativeText << QString("some*th*text") << true;
QTest::newRow("NoMatchWithMultipleWildcards") << AlternativeText << QString("some*abc*text") << false;
QTest::newRow("MatchJustWildcard") << DefaultText << QString("*") << true;
QTest::newRow("MatchFollowingWildcards") << DefaultText << QString("some t**t") << true;
QTest::newRow("CaseSensitivity") << DefaultText.toUpper() << QString("some t**t") << true;
}
void TestWildcardMatcher::testMatcher()
{
QFETCH(QString, text);
QFETCH(QString, pattern);
QFETCH(bool, match);
initMatcher(text);
verifyMatchResult(pattern, match);
cleanupMatcher();
}
void TestWildcardMatcher::initMatcher(QString text)
{
m_matcher = new WildcardMatcher(text);
}
void TestWildcardMatcher::cleanupMatcher()
{
delete m_matcher;
}
void TestWildcardMatcher::verifyMatchResult(QString pattern, bool expected)
{
if (expected) {
verifyMatch(pattern);
} else {
verifyNoMatch(pattern);
}
}
void TestWildcardMatcher::verifyMatch(QString pattern)
{
bool matchResult = m_matcher->match(pattern);
QVERIFY(matchResult);
}
void TestWildcardMatcher::verifyNoMatch(QString pattern)
{
bool matchResult = m_matcher->match(pattern);
QVERIFY(!matchResult);
}

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_TESTWILDCARDMATCHER_H
#define KEEPASSX_TESTWILDCARDMATCHER_H
#include <QObject>
class WildcardMatcher;
class TestWildcardMatcher : public QObject
{
Q_OBJECT
private slots:
void testMatcher();
void testMatcher_data();
private:
static const QString DefaultText;
static const QString AlternativeText;
void initMatcher(QString text);
void cleanupMatcher();
void verifyMatchResult(QString pattern, bool expected);
void verifyMatch(QString pattern);
void verifyNoMatch(QString pattern);
WildcardMatcher* m_matcher;
};
#endif // KEEPASSX_TESTWILDCARDMATCHER_H