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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,195 +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 "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.entry->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.entry->group()) {
return match.entry->group()->name();
}
break;
case Title:
return match.entry->resolveMultiplePlaceholders(match.entry->title());
case Username:
return match.entry->resolveMultiplePlaceholders(match.entry->username());
case Sequence:
return match.sequence;
}
} else if (role == Qt::DecorationRole) {
switch (index.column()) {
case ParentGroup:
if (match.entry->group()) {
return match.entry->group()->iconPixmap();
}
break;
case Title:
return match.entry->iconPixmap();
}
} else if (role == Qt::FontRole) {
QFont font;
if (match.entry->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.entry == 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.entry == 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

@ -1,66 +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/>.
*/
#ifndef KEEPASSX_AUTOTYPEMATCHMODEL_H
#define KEEPASSX_AUTOTYPEMATCHMODEL_H
#include <QAbstractTableModel>
#include "core/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

@ -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

@ -1,60 +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/>.
*/
#ifndef KEEPASSX_AUTOTYPEMATCHVIEW_H
#define KEEPASSX_AUTOTYPEMATCHVIEW_H
#include <QTreeView>
#include "core/AutoTypeMatch.h"
#include "gui/entry/AutoTypeMatchModel.h"
class SortFilterHideProxyModel;
class AutoTypeMatchView : public QTreeView
{
Q_OBJECT
public:
explicit AutoTypeMatchView(QWidget* parent = nullptr);
AutoTypeMatch currentMatch();
void setCurrentMatch(const AutoTypeMatch& match);
AutoTypeMatch matchFromIndex(const QModelIndex& index);
void setMatchList(const QList<AutoTypeMatch>& matches);
void setFirstMatchActive();
signals:
void matchActivated(AutoTypeMatch match);
void matchSelectionChanged();
void matchTextCopied();
protected:
void keyPressEvent(QKeyEvent* event) override;
private slots:
void emitMatchActivated(const QModelIndex& index);
void userNameCopied();
void passwordCopied();
private:
AutoTypeMatchModel* const m_model;
SortFilterHideProxyModel* const m_sortModel;
};
#endif // KEEPASSX_AUTOTYPEMATCHVIEW_H