From 813ab47e2997f31eae5cb8c3daef8128e6923ff7 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 13 Feb 2021 22:08:29 -0500 Subject: [PATCH] Implement Auto-Type {PICKCHARS} * Closes #725 Support Auto-Type {PICKCHARS} placeholder. Open a dialog that lets you pick characters of an entry's password by their position. Supports typing {TAB} in between characters to move between fields (if necessary). Also supports using arrow keys to quickly navigate around the choice grid. --- docs/topics/AutoType.adoc | 1 + src/CMakeLists.txt | 1 + src/autotype/AutoType.cpp | 22 ++++ src/autotype/PickcharsDialog.cpp | 172 +++++++++++++++++++++++++++++++ src/autotype/PickcharsDialog.h | 52 ++++++++++ src/autotype/PickcharsDialog.ui | 87 ++++++++++++++++ 6 files changed, 335 insertions(+) create mode 100644 src/autotype/PickcharsDialog.cpp create mode 100644 src/autotype/PickcharsDialog.h create mode 100644 src/autotype/PickcharsDialog.ui diff --git a/docs/topics/AutoType.adoc b/docs/topics/AutoType.adoc index eb6a76909..b58830598 100644 --- a/docs/topics/AutoType.adoc +++ b/docs/topics/AutoType.adoc @@ -47,6 +47,7 @@ image::autotype_entry_sequences.png[] |{DELAY X} |Delay typing start by X milliseconds |{CLEARFIELD} |Clear the input field before typing |{TOTP} |Insert calculated TOTP value (if configured) +|{PICKCHARS} |Pick specific password characters from a dialog |{ X} |Repeat X times (e.g., {SPACE 5} inserts five spaces) |=== + diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index ba8acdc77..80ce5247c 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -273,6 +273,7 @@ set(autotype_SOURCES autotype/AutoTypeMatchModel.cpp autotype/AutoTypeMatchView.cpp autotype/AutoTypeSelectDialog.cpp + autotype/PickcharsDialog.cpp autotype/ShortcutWidget.cpp autotype/WindowSelectComboBox.cpp) diff --git a/src/autotype/AutoType.cpp b/src/autotype/AutoType.cpp index 3373342ef..0fd79c13e 100644 --- a/src/autotype/AutoType.cpp +++ b/src/autotype/AutoType.cpp @@ -27,6 +27,7 @@ #include "autotype/AutoTypePlatformPlugin.h" #include "autotype/AutoTypeSelectDialog.h" +#include "autotype/PickcharsDialog.h" #include "core/Config.h" #include "core/Database.h" #include "core/Entry.h" @@ -564,6 +565,27 @@ AutoType::parseActions(const QString& entrySequence, const Entry* entry, QString for (const auto& ch : totp) { actions << QSharedPointer::create(ch); } + } else if (placeholder == "pickchars") { + if (error) { + // Ignore this if we are syntax checking + continue; + } + // Show pickchars dialog for entry's password + auto password = entry->resolvePlaceholder(entry->password()); + if (!password.isEmpty()) { + PickcharsDialog pickcharsDialog(password); + if (pickcharsDialog.exec() == QDialog::Accepted && !pickcharsDialog.selectedChars().isEmpty()) { + auto chars = pickcharsDialog.selectedChars(); + auto iter = chars.begin(); + while (iter != chars.end()) { + actions << QSharedPointer::create(*iter); + ++iter; + if (pickcharsDialog.pressTab() && iter != chars.end()) { + actions << QSharedPointer::create(g_placeholderToKey["tab"]); + } + } + } + } } else if (placeholder == "beep" || placeholder.startsWith("vkey") || placeholder.startsWith("appactivate")) { // Ignore these commands diff --git a/src/autotype/PickcharsDialog.cpp b/src/autotype/PickcharsDialog.cpp new file mode 100644 index 000000000..4735e1ec9 --- /dev/null +++ b/src/autotype/PickcharsDialog.cpp @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2021 Team KeePassXC + * + * 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 . + */ + +#include "PickcharsDialog.h" +#include "ui_PickcharsDialog.h" + +#include "core/Entry.h" +#include "gui/Icons.h" + +#include +#include +#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0) +#include +#else +#include +#endif + +PickcharsDialog::PickcharsDialog(const QString& string, QWidget* parent) + : QDialog(parent) + , m_ui(new Ui::PickcharsDialog()) +{ + if (string.isEmpty()) { + reject(); + } + + // Places the window on the active (virtual) desktop instead of where the main window is. + setAttribute(Qt::WA_X11BypassTransientForHint); + setWindowFlags((windowFlags() | Qt::WindowStaysOnTopHint | Qt::MSWindowsFixedSizeDialogHint) + & ~Qt::WindowContextHelpButtonHint); + setWindowIcon(icons()->applicationIcon()); + + m_ui->setupUi(this); + + // Increase max columns with longer passwords for better display + int width = 10; + if (string.length() >= 100) { + width = 20; + } else if (string.length() >= 60) { + width = 15; + } + + int count = 0; + for (const auto& ch : string) { + auto btn = new QPushButton(QString::number(count + 1)); + btn->setProperty("char", ch); + btn->setProperty("count", count); + connect(btn, &QPushButton::clicked, this, &PickcharsDialog::charSelected); + m_ui->charsGrid->addWidget(btn, count / width, count % width); + m_lastSelected = count; + ++count; + } + // Prevent stretched buttons + if (m_ui->charsGrid->rowCount() == 1 && m_ui->charsGrid->columnCount() < 5) { + m_ui->charsGrid->addItem(new QSpacerItem(5, 5, QSizePolicy::MinimumExpanding), count / width, count % width); + } + m_ui->charsGrid->itemAtPosition(0, 0)->widget()->setFocus(); + + connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + + // Navigate grid layout using up/down/left/right motion + new QShortcut(Qt::Key_Up, this, SLOT(upPressed())); + new QShortcut(Qt::Key_Down, this, SLOT(downPressed())); + // Remove last selected character + auto shortcut = new QShortcut(Qt::Key_Backspace, this); + connect(shortcut, &QShortcut::activated, this, [this] { + auto text = m_ui->selectedChars->text(); + m_ui->selectedChars->setText(text.left(text.size() - 1)); + }); + // Submit the form + shortcut = new QShortcut(Qt::CTRL + Qt::Key_S, this); + connect(shortcut, &QShortcut::activated, this, [this] { accept(); }); +} + +void PickcharsDialog::upPressed() +{ + auto focus = focusWidget(); + if (!focus) { + return; + } + + auto count = focus->property("count"); + if (count.isValid()) { + // Lower bound not checked by QGridLayout::itemAt https://bugreports.qt.io/browse/QTBUG-91261 + auto upCount = count.toInt() - m_ui->charsGrid->columnCount(); + if (upCount >= 0) { + m_ui->charsGrid->itemAt(upCount)->widget()->setFocus(); + } + } else if (focus == m_ui->selectedChars) { + // Move back to the last selected button + auto item = m_ui->charsGrid->itemAt(m_lastSelected); + if (item) { + item->widget()->setFocus(); + } + } else if (focus == m_ui->pressTab) { + m_ui->selectedChars->setFocus(); + } +} + +void PickcharsDialog::downPressed() +{ + auto focus = focusWidget(); + if (!focus) { + return; + } + + auto count = focus->property("count"); + if (count.isValid()) { + auto item = m_ui->charsGrid->itemAt(count.toInt() + m_ui->charsGrid->columnCount()); + if (item) { + item->widget()->setFocus(); + } else { + // Store the currently selected button and move to the line edit + m_lastSelected = count.toInt(); + m_ui->selectedChars->setFocus(); + } + } else if (focus == m_ui->selectedChars) { + m_ui->pressTab->setFocus(); + } +} + +QString PickcharsDialog::selectedChars() +{ + return m_ui->selectedChars->text(); +} + +bool PickcharsDialog::pressTab() +{ + return m_ui->pressTab->isChecked(); +} + +void PickcharsDialog::charSelected() +{ + auto btn = qobject_cast(sender()); + if (!btn) { + return; + } + + m_ui->selectedChars->setText(m_ui->selectedChars->text() + btn->property("char").toChar()); +} + +void PickcharsDialog::showEvent(QShowEvent* event) +{ + QDialog::showEvent(event); + + // Center on active screen +#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 + move(screenGeometry.center().x() - (size().width() / 2), screenGeometry.center().y() - (size().height() / 2)); +} diff --git a/src/autotype/PickcharsDialog.h b/src/autotype/PickcharsDialog.h new file mode 100644 index 000000000..dc35ae0bf --- /dev/null +++ b/src/autotype/PickcharsDialog.h @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2021 Team KeePassXC + * + * 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 . + */ + +#ifndef KEEPASSXC_PICKCHARSDIALOG_H +#define KEEPASSXC_PICKCHARSDIALOG_H + +#include +#include +#include + +namespace Ui +{ + class PickcharsDialog; +} + +class PickcharsDialog : public QDialog +{ + Q_OBJECT + +public: + explicit PickcharsDialog(const QString& string, QWidget* parent = nullptr); + QString selectedChars(); + bool pressTab(); + +protected: + void showEvent(QShowEvent*) override; + +private slots: + void charSelected(); + void upPressed(); + void downPressed(); + +private: + QSharedPointer m_ui; + int m_lastSelected; +}; + +#endif // KEEPASSXC_PICKCHARSDIALOG_H diff --git a/src/autotype/PickcharsDialog.ui b/src/autotype/PickcharsDialog.ui new file mode 100644 index 000000000..de9704eb0 --- /dev/null +++ b/src/autotype/PickcharsDialog.ui @@ -0,0 +1,87 @@ + + + PickcharsDialog + + + + 0 + 0 + 418 + 188 + + + + KeePassXC - Pick Characters + + + + QLayout::SetFixedSize + + + + + + 0 + 0 + + + + Select characters to type, navigate with arrow keys, Ctrl + S submits. + + + + + + + + + + + 0 + 0 + + + + + 150 + 0 + + + + + + + + + + Press &Tab between characters + + + true + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + PasswordEdit + QLineEdit +
gui/PasswordEdit.h
+
+
+ + selectedChars + + + +