Files
keepassxc/src/autotype/AutoTypeSelectDialog.cpp
2022-06-05 22:02:23 -04:00

411 lines
14 KiB
C++

/*
* Copyright (C) 2021 KeePassXC Team <team@keepassxc.org>
* 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 "AutoTypeSelectDialog.h"
#include "ui_AutoTypeSelectDialog.h"
#include <QCloseEvent>
#include <QMenu>
#include <QShortcut>
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
#include <QScreen>
#else
#include <QDesktopWidget>
#endif
#include "core/Config.h"
#include "core/Database.h"
#include "core/Entry.h"
#include "core/EntrySearcher.h"
#include "gui/Clipboard.h"
#include "gui/Icons.h"
const auto MENU_FIELD_PROP_NAME = "menu_field";
enum MENU_FIELD
{
USERNAME = 1,
PASSWORD,
TOTP,
};
AutoTypeSelectDialog::AutoTypeSelectDialog(QWidget* parent)
: QDialog(parent)
, m_ui(new Ui::AutoTypeSelectDialog())
, m_lastMatch(nullptr, QString())
{
setAttribute(Qt::WA_DeleteOnClose);
// Places the window on the active (virtual) desktop instead of where the main window is.
setAttribute(Qt::WA_X11BypassTransientForHint);
setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint) & ~Qt::WindowContextHelpButtonHint);
setWindowIcon(icons()->applicationIcon());
buildActionMenu();
m_ui->setupUi(this);
connect(m_ui->view, &AutoTypeMatchView::matchActivated, this, &AutoTypeSelectDialog::submitAutoTypeMatch);
connect(m_ui->view, &AutoTypeMatchView::currentMatchChanged, this, &AutoTypeSelectDialog::updateActionMenu);
connect(m_ui->view, &QWidget::customContextMenuRequested, this, [this](const QPoint& pos) {
if (m_ui->view->currentMatch().first) {
m_actionMenu->popup(m_ui->view->viewport()->mapToGlobal(pos));
}
});
m_ui->helpButton->setIcon(icons()->icon("system-help"));
m_ui->search->installEventFilter(this);
m_searchTimer.setInterval(0);
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()));
m_ui->searchCheckBox->setShortcut(Qt::CTRL + Qt::Key_F);
connect(m_ui->searchCheckBox, &QCheckBox::toggled, this, [this](bool checked) {
setDelayedSearch(checked);
performSearch();
});
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,
const AutoTypeMatch& lastMatch)
{
m_matches = matches;
m_dbs = dbs;
m_lastMatch = lastMatch;
bool noMatches = m_matches.isEmpty();
// disable changing search scope if we have no direct matches
m_ui->searchCheckBox->setDisabled(noMatches);
// changing check also performs search so block signals temporarily
bool blockSignals = m_ui->searchCheckBox->blockSignals(true);
m_ui->searchCheckBox->setChecked(noMatches);
m_ui->searchCheckBox->blockSignals(blockSignals);
// always perform search when updating matches to refresh view
performSearch();
setDelayedSearch(noMatches);
}
void AutoTypeSelectDialog::setSearchString(const QString& search)
{
m_ui->search->setText(search);
m_ui->searchCheckBox->setChecked(true);
}
void AutoTypeSelectDialog::setDelayedSearch(bool state)
{
m_searchTimer.setInterval(state ? 150 : 0);
}
void AutoTypeSelectDialog::submitAutoTypeMatch(AutoTypeMatch match)
{
if (match.first) {
m_accepted = true;
accept();
emit matchActivated(std::move(match), m_virtualMode);
}
}
void AutoTypeSelectDialog::performSearch()
{
if (!m_ui->searchCheckBox->isChecked()) {
m_ui->view->setMatchList(m_matches);
m_ui->view->filterList(m_ui->search->text());
} else {
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);
}
bool selected = false;
if (m_lastMatch.first) {
selected = m_ui->view->selectMatch(m_lastMatch);
}
if (!selected && !m_ui->search->text().isEmpty()) {
m_ui->view->selectFirstMatch();
}
m_ui->search->setFocus();
}
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:
m_ui->view->moveSelection(-1);
return true;
case Qt::Key_Down:
m_ui->view->moveSelection(1);
return true;
case Qt::Key_PageUp:
m_ui->view->moveSelection(-5);
return true;
case Qt::Key_PageDown:
m_ui->view->moveSelection(5);
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();
for (auto action : m_actionMenu->actions()) {
auto prop = action->property(MENU_FIELD_PROP_NAME);
if (prop.isValid()) {
switch (prop.toInt()) {
case MENU_FIELD::USERNAME:
action->setEnabled(hasUsername);
break;
case MENU_FIELD::PASSWORD:
action->setEnabled(hasPassword);
break;
case MENU_FIELD::TOTP:
action->setEnabled(hasTotp);
break;
}
}
}
}
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);
typeUsernameAction->setShortcut(Qt::CTRL + Qt::Key_1);
typeUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
connect(typeUsernameAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{USERNAME}";
submitAutoTypeMatch(match);
});
typePasswordAction->setShortcut(Qt::CTRL + Qt::Key_2);
typePasswordAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::PASSWORD);
connect(typePasswordAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{PASSWORD}";
submitAutoTypeMatch(match);
});
typeTotpAction->setShortcut(Qt::CTRL + Qt::Key_3);
typeTotpAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::TOTP);
connect(typeTotpAction, &QAction::triggered, this, [&] {
auto match = m_ui->view->currentMatch();
match.second = "{TOTP}";
submitAutoTypeMatch(match);
});
#ifdef Q_OS_WIN
auto typeVirtualAction = new QAction(icons()->icon("auto-type"), tr("Use Virtual Keyboard"));
m_actionMenu->insertAction(copyUsernameAction, typeVirtualAction);
typeVirtualAction->setShortcut(Qt::CTRL + Qt::Key_4);
connect(typeVirtualAction, &QAction::triggered, this, [&] {
m_virtualMode = true;
activateCurrentMatch();
});
#endif
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
// Qt 5.10 introduced a new "feature" to hide shortcuts in context menus
// Unfortunately, Qt::AA_DontShowShortcutsInContextMenus is broken, have to manually enable them
typeUsernameAction->setShortcutVisibleInContextMenu(true);
typePasswordAction->setShortcutVisibleInContextMenu(true);
typeTotpAction->setShortcutVisibleInContextMenu(true);
#ifdef Q_OS_WIN
typeVirtualAction->setShortcutVisibleInContextMenu(true);
#endif
#endif
copyUsernameAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::USERNAME);
connect(copyUsernameAction, &QAction::triggered, this, [&] {
auto entry = m_ui->view->currentMatch().first;
if (entry) {
clipboard()->setText(entry->resolvePlaceholder(entry->username()));
reject();
}
});
copyPasswordAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::PASSWORD);
connect(copyPasswordAction, &QAction::triggered, this, [&] {
auto entry = m_ui->view->currentMatch().first;
if (entry) {
clipboard()->setText(entry->resolvePlaceholder(entry->password()));
reject();
}
});
copyTotpAction->setProperty(MENU_FIELD_PROP_NAME, MENU_FIELD::TOTP);
connect(copyTotpAction, &QAction::triggered, this, [&] {
auto entry = m_ui->view->currentMatch().first;
if (entry) {
clipboard()->setText(entry->totp());
reject();
}
});
}
void AutoTypeSelectDialog::showEvent(QShowEvent* event)
{
QDialog::showEvent(event);
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
auto screen = QApplication::screenAt(QCursor::pos());
if (!screen) {
// screenAt can return a nullptr, default to the primary screen
screen = QApplication::primaryScreen();
}
QRect screenGeometry = screen->availableGeometry();
#else
QRect screenGeometry = QApplication::desktop()->availableGeometry(QCursor::pos());
#endif
// Resize to last used size
QSize size = config()->get(Config::GUI_AutoTypeSelectDialogSize).toSize();
size.setWidth(qMin(size.width(), screenGeometry.width()));
size.setHeight(qMin(size.height(), screenGeometry.height()));
resize(size);
// move dialog to the center of the screen
move(screenGeometry.center().x() - (size.width() / 2), screenGeometry.center().y() - (size.height() / 2));
}
void AutoTypeSelectDialog::hideEvent(QHideEvent* event)
{
config()->set(Config::GUI_AutoTypeSelectDialogSize, size());
if (!m_accepted) {
emit rejected();
}
QDialog::hideEvent(event);
}