2
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
3
* Copyright (C) 2012 Felix Geyer <debfx@fobos.de>
5
* This program is free software: you can redistribute it and/or modify
6
* it under the terms of the GNU General Public License as published by
7
* the Free Software Foundation, either version 2 or (at your option)
8
* version 3 of the License.
10
* This program is distributed in the hope that it will be useful,
11
* but WITHOUT ANY WARRANTY; without even the implied warranty of
12
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
* GNU General Public License for more details.
15
* You should have received a copy of the GNU General Public License
16
* along with this program. If not, see <http://www.gnu.org/licenses/>.
19
#include "AutoTypeSelectDialog.h"
20
#include "ui_AutoTypeSelectDialog.h"
25
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
28
#include <QDesktopWidget>
31
#include "core/Config.h"
32
#include "core/Database.h"
33
#include "core/Entry.h"
34
#include "core/EntrySearcher.h"
35
#include "gui/Clipboard.h"
38
const auto MENU_FIELD_PROP_NAME = "menu_field";
46
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
48
, m_ui(new Ui::AutoTypeSelectDialog())
49
, m_lastMatch(nullptr, QString())
51
setAttribute(Qt::WA_DeleteOnClose);
52
// Places the window on the active (virtual) desktop instead of where the main window is.
53
setAttribute(Qt::WA_X11BypassTransientForHint);
54
setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowContextHelpButtonHint);
55
setWindowIcon(icons()->applicationIcon());
61
connect(m_ui->view, &AutoTypeMatchView::matchActivated, this, &AutoTypeSelectDialog::submitAutoTypeMatch);
62
connect(m_ui->view, &AutoTypeMatchView::currentMatchChanged, this, &AutoTypeSelectDialog::updateActionMenu);
63
connect(m_ui->view, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
64
if (m_ui->view->currentMatch().first) {
65
m_actionMenu->popup(m_ui->view->viewport()->mapToGlobal(pos));
69
m_ui->helpButton->setIcon(icons()->icon("system-help"));
71
m_ui->search->installEventFilter(this);
73
m_searchTimer.setInterval(0);
74
m_searchTimer.setSingleShot(true);
76
connect(m_ui->search, SIGNAL(textChanged(QString)), &m_searchTimer, SLOT(start()));
77
connect(m_ui->search, SIGNAL(returnPressed()), SLOT(activateCurrentMatch()));
78
connect(&m_searchTimer, SIGNAL(timeout()), SLOT(performSearch()));
80
m_ui->searchCheckBox->setShortcut(Qt::CTRL + Qt::Key_F);
81
connect(m_ui->searchCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
82
setDelayedSearch(checked);
86
m_actionMenu->installEventFilter(this);
87
m_ui->action->setMenu(m_actionMenu);
88
m_ui->action->installEventFilter(this);
89
connect(m_ui->action, &QToolButton::clicked, this, &AutoTypeSelectDialog::activateCurrentMatch);
91
connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject()));
94
// Required for QScopedPointer
95
AutoTypeSelectDialog::~AutoTypeSelectDialog() = default;
97
void AutoTypeSelectDialog::setMatches(const QList<AutoTypeMatch>& matches,
98
const QList<QSharedPointer<Database>>& dbs,
99
const AutoTypeMatch& lastMatch)
103
m_lastMatch = lastMatch;
104
bool noMatches = m_matches.isEmpty();
106
// disable changing search scope if we have no direct matches
107
m_ui->searchCheckBox->setDisabled(noMatches);
109
// changing check also performs search so block signals temporarily
110
bool blockSignals = m_ui->searchCheckBox->blockSignals(true);
111
m_ui->searchCheckBox->setChecked(noMatches);
112
m_ui->searchCheckBox->blockSignals(blockSignals);
114
// always perform search when updating matches to refresh view
116
setDelayedSearch(noMatches);
119
void AutoTypeSelectDialog::setSearchString(const QString& search)
121
m_ui->search->setText(search);
122
m_ui->searchCheckBox->setChecked(true);
125
void AutoTypeSelectDialog::setDelayedSearch(bool state)
127
m_searchTimer.setInterval(state ? 150 : 0);
130
void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match)
135
emit matchActivated(std::move(match), m_virtualMode);
139
void AutoTypeSelectDialog::performSearch()
141
if (!m_ui->searchCheckBox->isChecked()) {
142
m_ui->view->setMatchList(m_matches);
143
m_ui->view->filterList(m_ui->search->text());
145
auto searchText = m_ui->search->text();
146
// If no search text, find all entries
147
if (searchText.isEmpty()) {
148
searchText.append("*");
151
EntrySearcher searcher;
152
QList<AutoTypeMatch> matches;
153
for (const auto& db : m_dbs) {
154
auto found = searcher.search(searchText, db->rootGroup());
155
for (auto* entry : found) {
156
QSet<QString> sequences;
157
auto defSequence = entry->effectiveAutoTypeSequence();
158
if (!defSequence.isEmpty()) {
159
matches.append({entry, defSequence});
160
sequences << defSequence;
162
for (const auto& assoc : entry->autoTypeAssociations()->getAll()) {
163
if (!sequences.contains(assoc.sequence) && !assoc.sequence.isEmpty()) {
164
matches.append({entry, assoc.sequence});
165
sequences << assoc.sequence;
171
m_ui->view->setMatchList(matches);
174
bool selected = false;
175
if (m_lastMatch.first) {
176
selected = m_ui->view->selectMatch(m_lastMatch);
179
if (!selected && !m_ui->search->text().isEmpty()) {
180
m_ui->view->selectFirstMatch();
183
m_ui->search->setFocus();
186
void AutoTypeSelectDialog::activateCurrentMatch()
188
submitAutoTypeMatch(m_ui->view->currentMatch());
191
bool AutoTypeSelectDialog::eventFilter(QObject* obj, QEvent* event)
193
if (obj == m_ui->action) {
194
if (event->type() == QEvent::FocusIn) {
195
m_ui->action->showMenu();
197
} else if (event->type() == QEvent::KeyPress && static_cast<QKeyEvent*>(event)->key() == Qt::Key_Return) {
198
// handle case where the menu is closed but the button has focus
199
activateCurrentMatch();
202
} else if (obj == m_actionMenu) {
203
if (event->type() == QEvent::KeyPress) {
204
auto* keyEvent = static_cast<QKeyEvent*>(event);
205
switch (keyEvent->key()) {
207
m_actionMenu->close();
208
focusNextPrevChild(true);
210
case Qt::Key_Backtab:
211
m_actionMenu->close();
212
focusNextPrevChild(false);
215
// accept the dialog with default sequence if no action selected
216
if (!m_actionMenu->activeAction()) {
217
activateCurrentMatch();
224
} else if (obj == m_ui->search) {
225
if (event->type() == QEvent::KeyPress) {
226
auto* keyEvent = static_cast<QKeyEvent*>(event);
227
switch (keyEvent->key()) {
229
m_ui->view->moveSelection(-1);
232
m_ui->view->moveSelection(1);
235
m_ui->view->moveSelection(-5);
237
case Qt::Key_PageDown:
238
m_ui->view->moveSelection(5);
241
if (m_ui->search->text().isEmpty()) {
244
m_ui->search->clear();
253
return QDialog::eventFilter(obj, event);
256
void AutoTypeSelectDialog::updateActionMenu(const AutoTypeMatch& match)
259
m_ui->action->setEnabled(false);
263
m_ui->action->setEnabled(true);
265
bool hasUsername = !match.first->username().isEmpty();
266
bool hasPassword = !match.first->password().isEmpty();
267
bool hasTotp = match.first->hasTotp();
269
for (auto action : m_actionMenu->actions()) {
270
auto prop = action->property(MENU_FIELD_PROP_NAME);
271
if (prop.isValid()) {
272
switch (prop.toInt()) {
273
case MENU_FIELD::USERNAME:
274
action->setEnabled(hasUsername);
276
case MENU_FIELD::PASSWORD:
277
action->setEnabled(hasPassword);
279
case MENU_FIELD::TOTP:
280
action->setEnabled(hasTotp);
287
void AutoTypeSelectDialog::buildActionMenu()
289
m_actionMenu = new QMenu(this);
290
auto typeUsernameAction = new QAction(icons()->icon("auto-type"), tr("Type {USERNAME}"), this);
291
auto typePasswordAction = new QAction(icons()->icon("auto-type"), tr("Type {PASSWORD}"), this);
292
auto typeTotpAction = new QAction(icons()->icon("auto-type"), tr("Type {TOTP}"), this);
293
auto copyUsernameAction = new QAction(icons()->icon("username-copy"), tr("Copy Username"), this);
294
auto copyPasswordAction = new QAction(icons()->icon("password-copy"), tr("Copy Password"), this);
295
auto copyTotpAction = new QAction(icons()->icon("totp"), tr("Copy TOTP"), this);
296
m_actionMenu->addAction(typeUsernameAction);
297
m_actionMenu->addAction(typePasswordAction);
298
m_actionMenu->addAction(typeTotpAction);
299
m_actionMenu->addAction(copyUsernameAction);
300
m_actionMenu->addAction(copyPasswordAction);
301
m_actionMenu->addAction(copyTotpAction);
303
typeUsernameAction->setShortcut(Qt::CTRL + Qt::Key_1);
304
typeUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
305
connect(typeUsernameAction, &QAction::triggered, this, [&] {
306
auto match = m_ui->view->currentMatch();
307
match.second = "{USERNAME}";
308
submitAutoTypeMatch(match);
311
typePasswordAction->setShortcut(Qt::CTRL + Qt::Key_2);
312
typePasswordAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::PASSWORD);
313
connect(typePasswordAction, &QAction::triggered, this, [&] {
314
auto match = m_ui->view->currentMatch();
315
match.second = "{PASSWORD}";
316
submitAutoTypeMatch(match);
319
typeTotpAction->setShortcut(Qt::CTRL + Qt::Key_3);
320
typeTotpAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::TOTP);
321
connect(typeTotpAction, &QAction::triggered, this, [&] {
322
auto match = m_ui->view->currentMatch();
323
match.second = "{TOTP}";
324
submitAutoTypeMatch(match);
327
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
328
auto typeVirtualAction = new QAction(icons()->icon("auto-type"), tr("Use Virtual Keyboard"), nullptr);
329
m_actionMenu->insertAction(copyUsernameAction, typeVirtualAction);
330
typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_4);
331
connect(typeVirtualAction, &QAction::triggered, this, [&] {
332
m_virtualMode = true;
333
activateCurrentMatch();
337
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
338
// Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
339
// Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them
340
typeUsernameAction->setShortcutVisibleInContextMenu(true);
341
typePasswordAction->setShortcutVisibleInContextMenu(true);
342
typeTotpAction->setShortcutVisibleInContextMenu(true);
343
#if defined(Q_OS_WIN) || defined(Q_OS_MAC)
344
typeVirtualAction->setShortcutVisibleInContextMenu(true);
348
copyUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
349
connect(copyUsernameAction, &QAction::triggered, this, [&] {
350
auto entry = m_ui->view->currentMatch().first;
352
clipboard()->setText(entry->resolvePlaceholder(entry->username()));
357
copyPasswordAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::PASSWORD);
358
connect(copyPasswordAction, &QAction::triggered, this, [&] {
359
auto entry = m_ui->view->currentMatch().first;
361
clipboard()->setText(entry->resolvePlaceholder(entry->password()));
366
copyTotpAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::TOTP);
367
connect(copyTotpAction, &QAction::triggered, this, [&] {
368
auto entry = m_ui->view->currentMatch().first;
370
clipboard()->setText(entry->totp());
376
void AutoTypeSelectDialog::showEvent(QShowEvent* event)
378
QDialog::showEvent(event);
380
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
381
auto screen = QApplication::screenAt(QCursor::pos());
383
// screenAt can return a nullptr, default to the primary screen
384
screen = QApplication::primaryScreen();
386
QRect screenGeometry = screen->availableGeometry();
388
QRect screenGeometry = QApplication::desktop()->availableGeometry(QCursor::pos());
391
// Resize to last used size
392
QSize size = config()->get(Config::GUI_AutoTypeSelectDialogSize).toSize();
393
size.setWidth(qMin(size.width(), screenGeometry.width()));
394
size.setHeight(qMin(size.height(), screenGeometry.height()));
397
// move dialog to the center of the screen
398
move(screenGeometry.center().x() - (size.width() / 2), screenGeometry.center().y() - (size.height() / 2));
401
void AutoTypeSelectDialog::hideEvent(QHideEvent* event)
403
config()->set(Config::GUI_AutoTypeSelectDialogSize, size());
407
QDialog::hideEvent(event);