From a472ef8a931dd64990d7f735394e949638e880d5 Mon Sep 17 00:00:00 2001 From: Waqar Ahmed Date: Sun, 4 Feb 2024 16:29:04 +0500 Subject: [PATCH] Allow configuring keyboard shortcuts (#9643) Closes #2689 The design of the respective code is loosely based on KDE's KActionCollection. The ActionCollection manages all actions that can be shortcut configured. These actions are then exposed in the config and a user can assign a different shortcut. Actions inside the MainWindow have been added to the ActionCollection. --------- Co-authored-by: Jonathan White --- share/translations/keepassxc_en.ts | 457 +++++++++++++----- src/CMakeLists.txt | 4 +- src/core/Config.cpp | 24 + src/core/Config.h | 10 + src/gui/ActionCollection.cpp | 123 +++++ src/gui/ActionCollection.h | 61 +++ src/gui/ApplicationSettingsWidget.cpp | 17 + src/gui/ApplicationSettingsWidgetGeneral.ui | 6 +- src/gui/MainWindow.cpp | 196 ++++++-- src/gui/MainWindow.h | 3 +- src/gui/MainWindow.ui | 162 +++++-- src/gui/ShortcutSettingsPage.cpp | 282 +++++++++++ src/gui/ShortcutSettingsPage.h | 36 ++ .../widgets}/ShortcutWidget.cpp | 27 +- .../widgets}/ShortcutWidget.h | 14 +- tests/gui/TestGui.cpp | 48 ++ tests/gui/TestGui.h | 1 + 17 files changed, 1238 insertions(+), 233 deletions(-) create mode 100644 src/gui/ActionCollection.cpp create mode 100644 src/gui/ActionCollection.h create mode 100644 src/gui/ShortcutSettingsPage.cpp create mode 100644 src/gui/ShortcutSettingsPage.h rename src/{autotype => gui/widgets}/ShortcutWidget.cpp (83%) rename src/{autotype => gui/widgets}/ShortcutWidget.h (82%) diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index 34c07934e..886d5551f 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -5156,34 +5156,18 @@ Are you sure you want to continue with this file? &New Database… - - Create a new database - - &Merge From Database… - - Merge from another KDBX database - - &New Entry… - - Add a new entry - - &Edit Entry… - - View or edit entry - - &Delete Entry… @@ -5192,10 +5176,6 @@ Are you sure you want to continue with this file? &New Group… - - Add a new group - - &Edit Group… @@ -5228,18 +5208,10 @@ Are you sure you want to continue with this file? Database &Reports… - - Statistics, health check, etc. - - &Database Settings… - - Database settings - - &Clone Entry… @@ -5248,34 +5220,18 @@ Are you sure you want to continue with this file? Move u&p - - Move entry one step up - - Move do&wn - - Move entry one step down - - Copy &Username - - Copy username to clipboard - - Copy &Password - - Copy password to clipboard - - &Settings @@ -5308,26 +5264,14 @@ Are you sure you want to continue with this file? &Title - - Copy title to clipboard - - Copy &URL - - Copy URL to clipboard - - &Notes - - Copy notes to clipboard - - &CSV File… @@ -5340,26 +5284,14 @@ Are you sure you want to continue with this file? KeePass 1 Database… - - Import a KeePass 1 database - - 1Password Vault… - - Import a 1Password Vault - - CSV File… - - Import a CSV file - - Show TOTP @@ -5404,10 +5336,6 @@ Are you sure you want to continue with this file? &Online Help - - Go to online documentation - - &User Guide @@ -5480,10 +5408,6 @@ Are you sure you want to continue with this file? &XML File… - - XML File… - - Clear history @@ -5575,11 +5499,251 @@ We recommend you use the AppImage available on our downloads page. - Passkeys + Import Passkey - Import Passkey + Quit Application + + + + Open About Dialog + + + + Open Database + + + + Create Database + + + + Merge From Database + + + + Create Entry + + + + Edit Entry + + + + Delete Entry + + + + Create Group + + + + Edit Group + + + + Delete Group + + + + Download All Favicons + + + + Sort Groups A-Z + + + + Sort Groups Z-A + + + + Save Database As + + + + Show Database Security + + + + Show Database Reports + + + + Show Database Settings + + + + Show Passkeys + + + + Clone Entry + + + + Move Entry Up + + + + Move Entry Down + + + + Copy Username + + + + Copy Password + + + + Show Application Settings + + + + Show Password Generator + + + + Perform Auto-Type: {USERNAME} + + + + Perform Auto-Type: {USERNAME}{ENTER} + + + + Perform Auto-Type: {PASSWORD} + + + + Perform Auto-Type: {PASSWORD}{ENTER} + + + + Perform Auto-Type: {TOTP} + + + + Copy Title + + + + Copy URL + + + + Copy Notes + + + + Export to CSV + + + + Export to HTML + + + + Import KeePass1 Database + + + + Import 1Password Vault + + + + Import CSV File + + + + Show TOTP QR Code + + + + Set up TOTP + + + + Empty Recycle Bin + + + + Open Donation Website + + + + Open Bug Report + + + + Open Online Documentation + + + + Open Keyboard Shortcuts Guide + + + + Save Database Backup + + + + SSH Agent: Add Key + + + + SSH Agent: Remove Key + + + + Toggle Compact Mode + + + + Set Theme: Automatic + + + + Set Theme: Light + + + + Set Theme: Dark + + + + Set Theme: Classic + + + + Toggle Show Toolbar + + + + Toggle Show Preview Panel + + + + Toggle Always on Top + + + + Toggle Hide Usernames + + + + Toggle Hide Passwords + + + + Export to XML + + + + Toggle Allow Screen Capture @@ -8220,63 +8384,15 @@ Kernel: %3 %4 - AES initialization failed + Enter Shortcut - AES encrypt failed + Action - Failed to store in Linux Keyring - - - - Could not locate key in keyring - - - - Could not read key in keyring - - - - AES decrypt failed - - - - No Polkit authentication agent was available - - - - Polkit authorization failed - - - - No Quick Unlock provider is available - - - - Polkit returned an error: %1 - - - - Failed to init KeePassXC crypto. - - - - Failed to encrypt key data. - - - - Failed to get Windows Hello credential. - - - - Failed to decrypt key data. - - - - Passkeys + Shortcuts @@ -8307,6 +8423,66 @@ Kernel: %3 %4 Resident Keys are not supported + + Passkeys + + + + AES initialization failed + + + + AES encrypt failed + + + + Failed to store in Linux Keyring + + + + Polkit returned an error: %1 + + + + Could not locate key in keyring + + + + Could not read key in keyring + + + + AES decrypt failed + + + + No Polkit authentication agent was available + + + + Polkit authorization failed + + + + No Quick Unlock provider is available + + + + Failed to init KeePassXC crypto. + + + + Failed to encrypt key data. + + + + Failed to get Windows Hello credential. + + + + Failed to decrypt key data. + + QtIOCompressor @@ -9147,6 +9323,29 @@ Kernel: %3 %4 + + ShortcutSettingsWidget + + Double click an action to change its shortcut + + + + Shortcut Conflict + + + + Filter... + + + + Shortcut %1 conflicts with '%2'. Overwrite shortcut? + + + + Reset Shortcuts + + + TagModel diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 2f7bebd87..ab02f3278 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -98,6 +98,7 @@ set(keepassx_SOURCES gui/styles/dark/DarkStyle.cpp gui/styles/light/LightStyle.cpp gui/AboutDialog.cpp + gui/ActionCollection.cpp gui/Application.cpp gui/CategoryListWidget.cpp gui/Clipboard.cpp @@ -130,6 +131,7 @@ set(keepassx_SOURCES gui/SearchWidget.cpp gui/SortFilterHideProxyModel.cpp gui/SquareSvgWidget.cpp + gui/ShortcutSettingsPage.cpp gui/TotpSetupDialog.cpp gui/TotpDialog.cpp gui/TotpExportSettingsDialog.cpp @@ -180,6 +182,7 @@ set(keepassx_SOURCES gui/widgets/ElidedLabel.cpp gui/widgets/KPToolBar.cpp gui/widgets/PopupHelpWidget.cpp + gui/widgets/ShortcutWidget.cpp gui/wizard/NewDatabaseWizard.cpp gui/wizard/NewDatabaseWizardPage.cpp gui/wizard/NewDatabaseWizardPageMetaData.cpp @@ -319,7 +322,6 @@ set(autotype_SOURCES autotype/AutoTypeMatchView.cpp autotype/AutoTypeSelectDialog.cpp autotype/PickcharsDialog.cpp - autotype/ShortcutWidget.cpp autotype/WindowSelectComboBox.cpp) if(WIN32) diff --git a/src/core/Config.cpp b/src/core/Config.cpp index 41943ae80..0c4551fef 100644 --- a/src/core/Config.cpp +++ b/src/core/Config.cpp @@ -597,4 +597,28 @@ void Config::createTempFileInstance() tmpFile->setParent(m_instance); } +QList Config::getShortcuts() const +{ + m_settings->beginGroup("Shortcuts"); + const auto keys = m_settings->childKeys(); + QList ret; + ret.reserve(keys.size()); + for (const auto& key : keys) { + const auto shortcut = m_settings->value(key).toString(); + ret.push_back(ShortcutEntry{key, shortcut}); + } + m_settings->endGroup(); + return ret; +} + +void Config::setShortcuts(const QList& shortcuts) +{ + m_settings->beginGroup("Shortcuts"); + m_settings->remove(""); // clear previous + for (const auto& shortcutEntry : shortcuts) { + m_settings->setValue(shortcutEntry.name, shortcutEntry.shortcut); + } + m_settings->endGroup(); +} + #undef QS diff --git a/src/core/Config.h b/src/core/Config.h index 31bd261c9..53cc66742 100644 --- a/src/core/Config.h +++ b/src/core/Config.h @@ -21,6 +21,7 @@ #include #include +#include class QSettings; @@ -198,6 +199,12 @@ public: Deleted }; + struct ShortcutEntry + { + QString name; + QString shortcut; + }; + ~Config() override; QVariant get(ConfigKey key); QVariant getDefault(ConfigKey key); @@ -208,6 +215,9 @@ public: void sync(); void resetToDefaults(); + QList getShortcuts() const; + void setShortcuts(const QList& shortcuts); + static Config* instance(); static void createConfigFromFile(const QString& configFileName, const QString& localConfigFileName = {}); static void createTempFileInstance(); diff --git a/src/gui/ActionCollection.cpp b/src/gui/ActionCollection.cpp new file mode 100644 index 000000000..d18fb4ce3 --- /dev/null +++ b/src/gui/ActionCollection.cpp @@ -0,0 +1,123 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 "ActionCollection.h" +#include "core/Config.h" + +#include + +ActionCollection* ActionCollection::instance() +{ + static ActionCollection ac; + return ∾ +} + +QList ActionCollection::actions() const +{ + return m_actions; +} + +void ActionCollection::addAction(QAction* action) +{ + if (!m_actions.contains(action)) { + m_actions << action; + } +} + +void ActionCollection::addActions(const QList& actions) +{ + for (auto a : actions) { + addAction(a); + } +} + +QKeySequence ActionCollection::defaultShortcut(const QAction* action) const +{ + auto shortcuts = defaultShortcuts(action); + return shortcuts.isEmpty() ? QKeySequence() : shortcuts.first(); +} + +QList ActionCollection::defaultShortcuts(const QAction* action) const +{ + return action->property("defaultShortcuts").value>(); +} + +void ActionCollection::setDefaultShortcut(QAction* action, const QKeySequence& shortcut) +{ + setDefaultShortcuts(action, {shortcut}); +} + +void ActionCollection::setDefaultShortcut(QAction* action, + QKeySequence::StandardKey standard, + const QKeySequence& fallback) +{ + if (!QKeySequence::keyBindings(standard).isEmpty()) { + setDefaultShortcuts(action, QKeySequence::keyBindings(standard)); + } else if (fallback != 0) { + setDefaultShortcut(action, QKeySequence(fallback)); + } +} + +void ActionCollection::setDefaultShortcuts(QAction* action, const QList& shortcuts) +{ + action->setShortcuts(shortcuts); + action->setProperty("defaultShortcuts", QVariant::fromValue(shortcuts)); +} + +void ActionCollection::restoreShortcuts() +{ + const auto shortcuts = Config::instance()->getShortcuts(); + QHash actionsByName; + for (auto action : m_actions) { + actionsByName.insert(action->objectName(), action); + } + for (const auto& shortcut : shortcuts) { + if (actionsByName.contains(shortcut.name)) { + const auto key = QKeySequence::fromString(shortcut.shortcut); + actionsByName.value(shortcut.name)->setShortcut(key); + } + } +} + +void ActionCollection::saveShortcuts() +{ + QList shortcuts; + shortcuts.reserve(m_actions.size()); + for (auto a : m_actions) { + // Only store non-default shortcut assignments + if (a->shortcut() != defaultShortcut(a)) { + shortcuts << Config::ShortcutEntry{a->objectName(), a->shortcut().toString()}; + } + } + Config::instance()->setShortcuts(shortcuts); +} + +QAction* ActionCollection::isConflictingShortcut(const QAction* action, const QKeySequence& seq) const +{ + // Empty sequences don't conflict with anything + if (seq.isEmpty()) { + return nullptr; + } + + for (auto a : m_actions) { + if (a != action && a->shortcut() == seq) { + return a; + } + } + + return nullptr; +} diff --git a/src/gui/ActionCollection.h b/src/gui/ActionCollection.h new file mode 100644 index 000000000..ab0d40029 --- /dev/null +++ b/src/gui/ActionCollection.h @@ -0,0 +1,61 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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_ACTION_COLLECTION_H +#define KEEPASSXC_ACTION_COLLECTION_H + +#include +#include +#include + +/** + * This class manages all actions that are shortcut configurable. + * It also allows you to access the actions inside it from anywhere + * in the gui code. + */ +class ActionCollection : public QObject +{ + Q_OBJECT + ActionCollection() = default; + +public: + static ActionCollection* instance(); + + QList actions() const; + + void addAction(QAction* action); + void addActions(const QList& actions); + + QKeySequence defaultShortcut(const QAction* a) const; + QList defaultShortcuts(const QAction* a) const; + + void setDefaultShortcut(QAction* a, const QKeySequence& shortcut); + void setDefaultShortcut(QAction* a, QKeySequence::StandardKey standard, const QKeySequence& fallback); + void setDefaultShortcuts(QAction* a, const QList& shortcut); + + // Check if any action conflicts with @p seq and return the conflicting action + QAction* isConflictingShortcut(const QAction* action, const QKeySequence& seq) const; + +public slots: + void restoreShortcuts(); + void saveShortcuts(); + +private: + QList m_actions; +}; + +#endif diff --git a/src/gui/ApplicationSettingsWidget.cpp b/src/gui/ApplicationSettingsWidget.cpp index 8c78cecef..1f1632e11 100644 --- a/src/gui/ApplicationSettingsWidget.cpp +++ b/src/gui/ApplicationSettingsWidget.cpp @@ -21,6 +21,7 @@ #include "ui_ApplicationSettingsWidgetSecurity.h" #include #include +#include #include "config-keepassx.h" @@ -138,6 +139,22 @@ ApplicationSettingsWidget::ApplicationSettingsWidget(QWidget* parent) m_secUi->lockDatabaseMinimizeCheckBox->setEnabled(!state); }); + // Set Auto-Type shortcut when changed + connect( + m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutChanged, this, [this](auto key, auto modifiers) { + QString error; + if (autoType()->registerGlobalShortcut(key, modifiers, &error)) { + m_generalUi->autoTypeShortcutWidget->setStyleSheet(""); + } else { + QToolTip::showText(mapToGlobal(rect().bottomLeft()), error); + m_generalUi->autoTypeShortcutWidget->setStyleSheet("background-color: #FF9696;"); + } + }); + connect(m_generalUi->autoTypeShortcutWidget, &ShortcutWidget::shortcutReset, this, [this] { + autoType()->unregisterGlobalShortcut(); + m_generalUi->autoTypeShortcutWidget->setStyleSheet(""); + }); + // Disable mouse wheel grab when scrolling // This prevents combo box and spinner values from changing without explicit focus auto mouseWheelFilter = new MouseWheelEventFilter(this); diff --git a/src/gui/ApplicationSettingsWidgetGeneral.ui b/src/gui/ApplicationSettingsWidgetGeneral.ui index 53bf9f724..dfd8f7018 100644 --- a/src/gui/ApplicationSettingsWidgetGeneral.ui +++ b/src/gui/ApplicationSettingsWidgetGeneral.ui @@ -58,8 +58,8 @@ 0 0 - 564 - 930 + 566 + 975 @@ -1260,7 +1260,7 @@ ShortcutWidget QLineEdit -
autotype/ShortcutWidget.h
+
gui/widgets/ShortcutWidget.h
diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index a41f631e4..2b92894f3 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -40,9 +40,11 @@ #include "core/Resources.h" #include "core/Tools.h" #include "gui/AboutDialog.h" +#include "gui/ActionCollection.h" #include "gui/Icons.h" #include "gui/MessageBox.h" #include "gui/SearchWidget.h" +#include "gui/ShortcutSettingsPage.h" #include "gui/entry/EntryView.h" #include "gui/osutils/OSUtils.h" @@ -189,6 +191,11 @@ MainWindow::MainWindow() connect(m_ui->tabWidget, &DatabaseTabWidget::databaseUnlocked, this, &MainWindow::databaseUnlocked); connect(m_ui->tabWidget, &DatabaseTabWidget::activeDatabaseChanged, this, &MainWindow::activeDatabaseChanged); + initViewMenu(); + initActionCollection(); + + m_ui->settingsWidget->addSettingsPage(new ShortcutSettingsPage()); + #ifdef WITH_XC_BROWSER m_ui->settingsWidget->addSettingsPage(new BrowserSettingsPage()); connect( @@ -201,8 +208,6 @@ MainWindow::MainWindow() m_ui->settingsWidget->addSettingsPage(new AgentSettingsPage()); #endif - initViewMenu(); - #if defined(WITH_XC_KEESHARE) KeeShare::init(this); m_ui->settingsWidget->addSettingsPage(new SettingsPageKeeShare(m_ui->tabWidget)); @@ -262,45 +267,6 @@ MainWindow::MainWindow() connect(m_inactivityTimer, SIGNAL(inactivityDetected()), this, SLOT(lockDatabasesAfterInactivity())); applySettingsChanges(); - m_ui->actionDatabaseNew->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_N); - setShortcut(m_ui->actionDatabaseOpen, QKeySequence::Open, Qt::CTRL + Qt::Key_O); - setShortcut(m_ui->actionDatabaseSave, QKeySequence::Save, Qt::CTRL + Qt::Key_S); - setShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs, Qt::CTRL + Qt::SHIFT + Qt::Key_S); - setShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W); - m_ui->actionDatabaseSettings->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_Comma); - m_ui->actionReports->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_R); - setShortcut(m_ui->actionSettings, QKeySequence::Preferences, Qt::CTRL + Qt::Key_Comma); - m_ui->actionLockDatabase->setShortcut(Qt::CTRL + Qt::Key_L); - m_ui->actionLockAllDatabases->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_L); - setShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q); - setShortcut(m_ui->actionEntryNew, QKeySequence::New, Qt::CTRL + Qt::Key_N); - m_ui->actionEntryEdit->setShortcut(Qt::CTRL + Qt::Key_E); - m_ui->actionEntryDelete->setShortcut(Qt::CTRL + Qt::Key_D); - m_ui->actionEntryDelete->setShortcut(Qt::Key_Delete); - m_ui->actionEntryClone->setShortcut(Qt::CTRL + Qt::Key_K); - m_ui->actionEntryTotp->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_T); - m_ui->actionEntryDownloadIcon->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_D); - m_ui->actionEntryCopyTotp->setShortcut(Qt::CTRL + Qt::Key_T); - m_ui->actionEntryCopyPasswordTotp->setShortcut(Qt::CTRL + Qt::Key_Y); - m_ui->actionEntryMoveUp->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_Up); - m_ui->actionEntryMoveDown->setShortcut(Qt::CTRL + Qt::ALT + Qt::Key_Down); - m_ui->actionEntryCopyUsername->setShortcut(Qt::CTRL + Qt::Key_B); - m_ui->actionEntryCopyPassword->setShortcut(Qt::CTRL + Qt::Key_C); - m_ui->actionEntryCopyTitle->setShortcut(Qt::CTRL + Qt::Key_I); - m_ui->actionEntryAutoTypeSequence->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_V); - m_ui->actionEntryOpenUrl->setShortcut(Qt::CTRL + Qt::SHIFT + Qt::Key_U); - m_ui->actionEntryCopyURL->setShortcut(Qt::CTRL + Qt::Key_U); - m_ui->actionEntryRestore->setShortcut(Qt::CTRL + Qt::Key_R); - - // Prevent conflicts with global Mac shortcuts (force Control on all platforms) -#ifdef Q_OS_MAC - auto modifier = Qt::META; -#else - auto modifier = Qt::CTRL; -#endif - m_ui->actionEntryAddToAgent->setShortcut(modifier + Qt::Key_H); - m_ui->actionEntryRemoveFromAgent->setShortcut(modifier + Qt::SHIFT + Qt::Key_H); - #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 @@ -1675,15 +1641,6 @@ void MainWindow::showGroupContextMenu(const QPoint& globalPos) m_ui->menuGroups->popup(globalPos); } -void MainWindow::setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback) -{ - if (!QKeySequence::keyBindings(standard).isEmpty()) { - action->setShortcuts(standard); - } else if (fallback != 0) { - action->setShortcut(QKeySequence(fallback)); - } -} - void MainWindow::applySettingsChanges() { int timeout = config()->get(Config::Security_LockDatabaseIdleSeconds).toInt() * 1000; @@ -2094,6 +2051,145 @@ void MainWindow::initViewMenu() }); } +void MainWindow::initActionCollection() +{ + auto ac = ActionCollection::instance(); + ac->addActions({// Database Menu + m_ui->actionDatabaseNew, + m_ui->actionDatabaseOpen, + m_ui->actionDatabaseSave, + m_ui->actionDatabaseSaveAs, + m_ui->actionDatabaseSaveBackup, + m_ui->actionDatabaseClose, + m_ui->actionLockDatabase, + m_ui->actionLockAllDatabases, + m_ui->actionDatabaseSettings, + m_ui->actionDatabaseSecurity, + m_ui->actionReports, + m_ui->actionPasskeys, + m_ui->actionDatabaseMerge, + m_ui->actionImportPasskey, + m_ui->actionImportCsv, + m_ui->actionImportOpVault, + m_ui->actionImportKeePass1, + m_ui->actionExportCsv, + m_ui->actionExportHtml, + m_ui->actionExportXML, + m_ui->actionQuit, + // Entry Menu + m_ui->actionEntryNew, + m_ui->actionEntryEdit, + m_ui->actionEntryClone, + m_ui->actionEntryDelete, + m_ui->actionEntryCopyUsername, + m_ui->actionEntryCopyPassword, + m_ui->actionEntryCopyURL, + m_ui->actionEntryCopyTitle, + m_ui->actionEntryCopyNotes, + m_ui->actionEntryTotp, + m_ui->actionEntryTotpQRCode, + m_ui->actionEntrySetupTotp, + m_ui->actionEntryCopyTotp, + m_ui->actionEntryCopyPasswordTotp, + m_ui->actionEntryAutoTypeSequence, + m_ui->actionEntryAutoTypeUsername, + m_ui->actionEntryAutoTypeUsernameEnter, + m_ui->actionEntryAutoTypePassword, + m_ui->actionEntryAutoTypePasswordEnter, + m_ui->actionEntryAutoTypeTOTP, + m_ui->actionEntryDownloadIcon, + m_ui->actionEntryOpenUrl, + m_ui->actionEntryMoveUp, + m_ui->actionEntryMoveDown, + m_ui->actionEntryAddToAgent, + m_ui->actionEntryRemoveFromAgent, + m_ui->actionEntryRestore, + // Group Menu + m_ui->actionGroupNew, + m_ui->actionGroupEdit, + m_ui->actionGroupClone, + m_ui->actionGroupDelete, + m_ui->actionGroupDownloadFavicons, + m_ui->actionGroupSortAsc, + m_ui->actionGroupSortDesc, + m_ui->actionGroupEmptyRecycleBin, + // Tools Menu + m_ui->actionPasswordGenerator, + m_ui->actionSettings, + // View Menu + m_ui->actionThemeAuto, + m_ui->actionThemeLight, + m_ui->actionThemeDark, + m_ui->actionThemeClassic, + m_ui->actionCompactMode, + m_ui->actionShowToolbar, + m_ui->actionShowPreviewPanel, + m_ui->actionAllowScreenCapture, + m_ui->actionAlwaysOnTop, + m_ui->actionHideUsernames, + m_ui->actionHidePasswords, + // Help Menu + m_ui->actionGettingStarted, + m_ui->actionUserGuide, + m_ui->actionKeyboardShortcuts, + m_ui->actionOnlineHelp, + m_ui->actionCheckForUpdates, + m_ui->actionDonate, + m_ui->actionBugReport, + m_ui->actionAbout}); + + // Add actions whose shortcuts were set in the .ui file + for (const auto action : ac->actions()) { + if (!action->shortcut().isEmpty()) { + ac->setDefaultShortcut(action, action->shortcut()); + } + } + + // Actions with standard shortcuts + ac->setDefaultShortcut(m_ui->actionDatabaseOpen, QKeySequence::Open, Qt::CTRL + Qt::Key_O); + ac->setDefaultShortcut(m_ui->actionDatabaseSave, QKeySequence::Save, Qt::CTRL + Qt::Key_S); + ac->setDefaultShortcut(m_ui->actionDatabaseSaveAs, QKeySequence::SaveAs, Qt::CTRL + Qt::SHIFT + Qt::Key_S); + ac->setDefaultShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W); + ac->setDefaultShortcut(m_ui->actionSettings, QKeySequence::Preferences, Qt::CTRL + Qt::Key_Comma); + ac->setDefaultShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q); + ac->setDefaultShortcut(m_ui->actionEntryNew, QKeySequence::New, Qt::CTRL + Qt::Key_N); + + // Prevent conflicts with global Mac shortcuts (force Control on all platforms) +#ifdef Q_OS_MAC + auto modifier = Qt::META; +#else + auto modifier = Qt::CTRL; +#endif + + // All other actions with default shortcuts + ac->setDefaultShortcut(m_ui->actionDatabaseNew, Qt::CTRL + Qt::SHIFT + Qt::Key_N); + ac->setDefaultShortcut(m_ui->actionDatabaseSettings, Qt::CTRL + Qt::SHIFT + Qt::Key_Comma); + ac->setDefaultShortcut(m_ui->actionReports, Qt::CTRL + Qt::SHIFT + Qt::Key_R); + ac->setDefaultShortcut(m_ui->actionLockDatabase, Qt::CTRL + Qt::Key_L); + ac->setDefaultShortcut(m_ui->actionLockAllDatabases, Qt::CTRL + Qt::SHIFT + Qt::Key_L); + ac->setDefaultShortcut(m_ui->actionEntryEdit, Qt::CTRL + Qt::Key_E); + ac->setDefaultShortcut(m_ui->actionEntryDelete, Qt::CTRL + Qt::Key_D); + ac->setDefaultShortcut(m_ui->actionEntryDelete, Qt::Key_Delete); + ac->setDefaultShortcut(m_ui->actionEntryClone, Qt::CTRL + Qt::Key_K); + ac->setDefaultShortcut(m_ui->actionEntryTotp, Qt::CTRL + Qt::SHIFT + Qt::Key_T); + ac->setDefaultShortcut(m_ui->actionEntryDownloadIcon, Qt::CTRL + Qt::SHIFT + Qt::Key_D); + ac->setDefaultShortcut(m_ui->actionEntryCopyTotp, Qt::CTRL + Qt::Key_T); + ac->setDefaultShortcut(m_ui->actionEntryCopyPasswordTotp, Qt::CTRL + Qt::Key_Y); + ac->setDefaultShortcut(m_ui->actionEntryMoveUp, Qt::CTRL + Qt::ALT + Qt::Key_Up); + ac->setDefaultShortcut(m_ui->actionEntryMoveDown, Qt::CTRL + Qt::ALT + Qt::Key_Down); + ac->setDefaultShortcut(m_ui->actionEntryCopyUsername, Qt::CTRL + Qt::Key_B); + ac->setDefaultShortcut(m_ui->actionEntryCopyPassword, Qt::CTRL + Qt::Key_C); + ac->setDefaultShortcut(m_ui->actionEntryCopyTitle, Qt::CTRL + Qt::Key_I); + ac->setDefaultShortcut(m_ui->actionEntryAutoTypeSequence, Qt::CTRL + Qt::SHIFT + Qt::Key_V); + ac->setDefaultShortcut(m_ui->actionEntryOpenUrl, Qt::CTRL + Qt::SHIFT + Qt::Key_U); + ac->setDefaultShortcut(m_ui->actionEntryCopyURL, Qt::CTRL + Qt::Key_U); + ac->setDefaultShortcut(m_ui->actionEntryRestore, Qt::CTRL + Qt::Key_R); + ac->setDefaultShortcut(m_ui->actionEntryAddToAgent, modifier + Qt::Key_H); + ac->setDefaultShortcut(m_ui->actionEntryRemoveFromAgent, modifier + Qt::SHIFT + Qt::Key_H); + + QTimer::singleShot(1, ac, &ActionCollection::restoreShortcuts); +} + #if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) MainWindowEventFilter::MainWindowEventFilter(QObject* parent) diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 53f9add15..5d6e07062 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -154,8 +154,6 @@ private slots: void focusSearchWidget(); private: - static void setShortcut(QAction* action, QKeySequence::StandardKey standard, int fallback = 0); - static const QString BaseWindowTitle; void saveWindowInformation(); @@ -168,6 +166,7 @@ private: void dropEvent(QDropEvent* event) override; void initViewMenu(); + void initActionCollection(); const QScopedPointer m_ui; SignalMultiplexer m_actionMultiplexer; diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index a0b8d6e68..d34d802b2 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -443,6 +443,9 @@ &Quit + + Quit Application + QAction::QuitRole @@ -451,6 +454,9 @@ &About + + Open About Dialog + QAction::AboutRole @@ -467,6 +473,9 @@ &Open Database… + + Open Database + @@ -489,7 +498,7 @@ &New Database… - Create a new database + Create Database @@ -497,7 +506,7 @@ &Merge From Database… - Merge from another KDBX database + Merge From Database @@ -508,7 +517,7 @@ &New Entry… - Add a new entry + Create Entry @@ -519,7 +528,7 @@ &Edit Entry… - View or edit entry + Edit Entry @@ -529,6 +538,9 @@ &Delete Entry… + + Delete Entry + @@ -538,7 +550,7 @@ &New Group… - Add a new group + Create Group @@ -548,6 +560,9 @@ &Edit Group… + + Edit Group + @@ -556,6 +571,9 @@ &Delete Group… + + Delete Group + @@ -564,6 +582,9 @@ Download All &Favicons… + + Download All Favicons + @@ -572,6 +593,9 @@ Sort &A-Z + + Sort Groups A-Z + @@ -580,6 +604,9 @@ Sort &Z-A + + Sort Groups Z-A + @@ -588,6 +615,9 @@ Sa&ve Database As… + + Save Database As + @@ -596,6 +626,9 @@ Database &Security… + + Show Database Security + @@ -605,7 +638,7 @@ Database &Reports… - Statistics, health check, etc. + Show Database Reports QAction::NoRole @@ -619,7 +652,7 @@ &Database Settings… - Database settings + Show Database Settings QAction::NoRole @@ -633,7 +666,7 @@ Passkeys… - Passkeys + Show Passkeys QAction::NoRole @@ -660,6 +693,9 @@ &Clone Entry… + + Clone Entry + @@ -669,7 +705,7 @@ Move u&p - Move entry one step up + Move Entry Up @@ -680,7 +716,7 @@ Move do&wn - Move entry one step down + Move Entry Down @@ -691,7 +727,7 @@ Copy &Username - Copy username to clipboard + Copy Username @@ -702,7 +738,7 @@ Copy &Password - Copy password to clipboard + Copy Password @@ -712,6 +748,9 @@ &Settings + + Show Application Settings + QAction::PreferencesRole @@ -723,6 +762,9 @@ &Password Generator + + Show Password Generator + @@ -751,7 +793,7 @@ {USERNAME} - {USERNAME} + Perform Auto-Type: {USERNAME} @@ -765,7 +807,7 @@ {USERNAME}{ENTER} - {USERNAME}{ENTER} + Perform Auto-Type: {USERNAME}{ENTER} @@ -779,7 +821,7 @@ {PASSWORD} - {PASSWORD} + Perform Auto-Type: {PASSWORD} @@ -793,7 +835,7 @@ {PASSWORD}{ENTER} - {PASSWORD}{ENTER} + Perform Auto-Type: {PASSWORD}{ENTER} @@ -807,7 +849,7 @@ {TOTP} - {TOTP} + Perform Auto-Type: {TOTP} @@ -847,7 +889,7 @@ &Title - Copy title to clipboard + Copy Title @@ -858,7 +900,7 @@ Copy &URL - Copy URL to clipboard + Copy URL @@ -869,7 +911,7 @@ &Notes - Copy notes to clipboard + Copy Notes @@ -879,6 +921,9 @@ &CSV File… + + Export to CSV + @@ -887,13 +932,16 @@ &HTML File… + + Export to HTML + KeePass 1 Database… - Import a KeePass 1 database + Import KeePass1 Database @@ -901,7 +949,7 @@ 1Password Vault… - Import a 1Password Vault + Import 1Password Vault @@ -909,7 +957,7 @@ CSV File… - Import a CSV file + Import CSV File @@ -921,11 +969,17 @@ Show QR Code + + Show TOTP QR Code + Set up TOTP… + + Set up TOTP + @@ -941,6 +995,9 @@ E&mpty recycle bin + + Empty Recycle Bin + false @@ -949,11 +1006,17 @@ &Donate + + Open Donation Website + Report a &Bug + + Open Bug Report + @@ -968,7 +1031,7 @@ &Online Help - Go to online documentation + Open Online Documentation @@ -983,6 +1046,9 @@ &Keyboard Shortcuts + + Open Keyboard Shortcuts Guide + Ctrl+/ @@ -994,16 +1060,25 @@ Save Database Backup… + + Save Database Backup + Add key to SSH Agent + + SSH Agent: Add Key + Remove key from SSH Agent + + SSH Agent: Remove Key + @@ -1012,6 +1087,9 @@ Compact Mode + + Toggle Compact Mode + @@ -1023,6 +1101,9 @@ Automatic + + Set Theme: Automatic + @@ -1031,6 +1112,9 @@ Light + + Set Theme: Light + @@ -1039,6 +1123,9 @@ Dark + + Set Theme: Dark + @@ -1047,6 +1134,9 @@ Classic (Platform-native) + + Set Theme: Classic + @@ -1058,6 +1148,9 @@ Show Toolbar + + Toggle Show Toolbar + @@ -1069,6 +1162,9 @@ Show Preview Panel + + Toggle Show Preview Panel + @@ -1077,6 +1173,9 @@ Always on Top + + Toggle Always on Top + Ctrl+Shift+A @@ -1088,6 +1187,9 @@ Hide Usernames + + Toggle Hide Usernames + Ctrl+Shift+B @@ -1102,6 +1204,9 @@ Hide Passwords + + Toggle Hide Passwords + Ctrl+Shift+C @@ -1114,7 +1219,7 @@ {USERNAME}{TAB}{PASSWORD}{ENTER} - {USERNAME}{TAB}{PASSWORD}{ENTER} + Perform Auto-Type: Entry Default @@ -1130,7 +1235,7 @@ Restore Entry(s) - Restore Entry(s) + Restore Entry Ctrl+R @@ -1152,7 +1257,7 @@ &XML File… - XML File… + Export to XML @@ -1162,6 +1267,9 @@ Allow Screen Capture + + Toggle Allow Screen Capture + diff --git a/src/gui/ShortcutSettingsPage.cpp b/src/gui/ShortcutSettingsPage.cpp new file mode 100644 index 000000000..fe00bfd8e --- /dev/null +++ b/src/gui/ShortcutSettingsPage.cpp @@ -0,0 +1,282 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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 "ShortcutSettingsPage.h" + +#include "core/Config.h" +#include "gui/ActionCollection.h" +#include "gui/Icons.h" +#include "gui/MessageBox.h" +#include "gui/widgets/ShortcutWidget.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class KeySequenceDialog final : public QDialog +{ +public: + explicit KeySequenceDialog(QWidget* parent = nullptr) + : QDialog(parent) + , m_keySeqEdit(new ShortcutWidget(this)) + , m_btnBox(new QDialogButtonBox(QDialogButtonBox::Save | QDialogButtonBox::Cancel + | QDialogButtonBox::RestoreDefaults, + this)) + { + auto* l = new QVBoxLayout(this); + connect(m_btnBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(m_btnBox, &QDialogButtonBox::rejected, this, &QDialog::reject); + connect(m_btnBox, &QDialogButtonBox::clicked, this, &KeySequenceDialog::restoreDefault); + + auto hLayout = new QHBoxLayout(); + l->addLayout(hLayout); + hLayout->addWidget(new QLabel(QObject::tr("Enter Shortcut"))); + hLayout->addWidget(m_keySeqEdit); + + l->addStretch(); + l->addWidget(m_btnBox); + + setFocusProxy(m_keySeqEdit); + } + + QKeySequence keySequence() const + { + return m_keySeqEdit->sequence(); + } + + bool shouldRestoreDefault() const + { + return m_restoreDefault; + } + +private: + void restoreDefault(QAbstractButton* btn) + { + if (m_btnBox->standardButton(btn) == QDialogButtonBox::RestoreDefaults) { + m_restoreDefault = true; + reject(); + } + } + +private: + bool m_restoreDefault = false; + ShortcutWidget* const m_keySeqEdit; + QDialogButtonBox* const m_btnBox; +}; + +class ShortcutSettingsWidget final : public QWidget +{ +public: + explicit ShortcutSettingsWidget(QWidget* parent = nullptr) + : QWidget(parent) + , m_tableView(new QTableView(this)) + , m_filterLineEdit(new QLineEdit(this)) + , m_resetShortcutsButton(new QPushButton(tr("Reset Shortcuts"), this)) + { + auto h = new QHBoxLayout(); + h->addWidget(m_filterLineEdit); + h->addWidget(m_resetShortcutsButton); + h->setStretch(0, 1); + + auto l = new QVBoxLayout(this); + l->addWidget(new QLabel(tr("Double click an action to change its shortcut"))); + l->addLayout(h); + l->addWidget(m_tableView); + + m_model.setColumnCount(2); + m_model.setHorizontalHeaderLabels({QObject::tr("Action"), QObject::tr("Shortcuts")}); + + m_proxy.setFilterKeyColumn(-1); + m_proxy.setFilterCaseSensitivity(Qt::CaseInsensitive); + m_proxy.setSourceModel(&m_model); + + m_filterLineEdit->setPlaceholderText(tr("Filter...")); + connect(m_filterLineEdit, &QLineEdit::textChanged, &m_proxy, &QSortFilterProxyModel::setFilterFixedString); + + connect(m_resetShortcutsButton, &QPushButton::clicked, this, [this]() { + auto ac = ActionCollection::instance(); + for (auto action : ac->actions()) { + action->setShortcut(ac->defaultShortcut(action)); + } + loadSettings(); + }); + + m_tableView->setModel(&m_proxy); + m_tableView->setSortingEnabled(true); + m_tableView->sortByColumn(0, Qt::AscendingOrder); + m_tableView->horizontalHeader()->setSectionResizeMode(0, QHeaderView::ResizeToContents); + m_tableView->horizontalHeader()->setSectionResizeMode(1, QHeaderView::Stretch); + m_tableView->verticalHeader()->hide(); + m_tableView->setEditTriggers(QAbstractItemView::NoEditTriggers); + m_tableView->setSelectionMode(QAbstractItemView::SingleSelection); + m_tableView->setSelectionBehavior(QAbstractItemView::SelectRows); + + connect(m_tableView, &QTableView::doubleClicked, this, &ShortcutSettingsWidget::onDoubleClicked); + } + + void loadSettings() + { + m_changedActions.clear(); + m_filterLineEdit->clear(); + m_model.setRowCount(0); + const auto& actions = ActionCollection::instance()->actions(); + for (auto a : actions) { + auto name = a->toolTip().isEmpty() ? acceleratorsStrippedText(a->text()) : a->toolTip(); + auto col1 = new QStandardItem(name); + col1->setData(QVariant::fromValue(a), Qt::UserRole); + auto col2 = new QStandardItem(a->shortcut().toString()); + m_model.appendRow({col1, col2}); + } + } + + void saveSettings() + { + if (m_changedActions.count()) { + for (const auto& action : m_changedActions.keys()) { + action->setShortcut(m_changedActions.value(action)); + } + ActionCollection::instance()->saveShortcuts(); + } + m_changedActions.clear(); + m_filterLineEdit->clear(); + } + +private: + static QString acceleratorsStrippedText(QString text) + { + for (int i = 0; i < text.size(); ++i) { + if (text.at(i) == QLatin1Char('&') && i + 1 < text.size() && text.at(i + 1) != QLatin1Char('&')) { + text.remove(i, 1); + } + } + return text; + } + + void onDoubleClicked(QModelIndex index) + { + if (index.column() != 0) { + index = index.sibling(index.row(), 0); + } + index = m_proxy.mapToSource(index); + auto action = index.data(Qt::UserRole).value(); + + KeySequenceDialog dialog(this); + int ret = dialog.exec(); + + QKeySequence change; + if (ret == QDialog::Accepted) { + change = dialog.keySequence(); + } else if (dialog.shouldRestoreDefault()) { + change = ActionCollection::instance()->defaultShortcut(action); + } else { + // Rejected + return; + } + + auto conflict = ActionCollection::instance()->isConflictingShortcut(action, change); + bool hasConflict = false; + if (conflict) { + // we conflicted with an action inside action collection + // check if the conflicted action is updated here + if (!m_changedActions.contains(conflict)) { + hasConflict = true; + } else { + if (m_changedActions.value(conflict) == change) { + hasConflict = true; + } + } + } else if (!change.isEmpty()) { + // we did not conflict with any shortcut inside action collection + // check if we conflict with any locally modified action + for (auto chAction : m_changedActions.keys()) { + if (m_changedActions.value(chAction) == change) { + hasConflict = true; + conflict = chAction; + break; + } + } + } + + if (hasConflict) { + auto conflictName = + conflict->toolTip().isEmpty() ? acceleratorsStrippedText(conflict->text()) : conflict->toolTip(); + auto conflictSeq = change.toString(); + + auto ans = MessageBox::question( + this, + tr("Shortcut Conflict"), + tr("Shortcut %1 conflicts with '%2'. Overwrite shortcut?").arg(conflictSeq, conflictName), + MessageBox::Overwrite | MessageBox::Discard, + MessageBox::Discard); + if (ans == MessageBox::Discard) { + // Bail out before making any changes + return; + } + + // Reset the conflict shortcut + m_changedActions[conflict] = {}; + for (auto item : m_model.findItems(conflictSeq, Qt::MatchExactly, 1)) { + item->setText(""); + } + } + + m_changedActions[action] = change; + auto item = m_model.itemFromIndex(index.sibling(index.row(), 1)); + item->setText(change.toString()); + } + + QTableView* m_tableView; + QLineEdit* m_filterLineEdit; + QPushButton* m_resetShortcutsButton; + QStandardItemModel m_model; + QSortFilterProxyModel m_proxy; + QHash m_changedActions; +}; + +QString ShortcutSettingsPage::name() +{ + return QObject::tr("Shortcuts"); +} + +QIcon ShortcutSettingsPage::icon() +{ + return icons()->icon("auto-type"); +} + +QWidget* ShortcutSettingsPage::createWidget() +{ + return new ShortcutSettingsWidget(); +} + +void ShortcutSettingsPage::loadSettings(QWidget* widget) +{ + static_cast(widget)->loadSettings(); +} + +void ShortcutSettingsPage::saveSettings(QWidget* widget) +{ + static_cast(widget)->saveSettings(); +} diff --git a/src/gui/ShortcutSettingsPage.h b/src/gui/ShortcutSettingsPage.h new file mode 100644 index 000000000..c5ce5fa38 --- /dev/null +++ b/src/gui/ShortcutSettingsPage.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2024 KeePassXC Team + * + * 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 3 of the License, or + * (at your option) any later version. + * + * 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_SHORTCUT_SETTINGSPAGE_H +#define KEEPASSXC_SHORTCUT_SETTINGSPAGE_H + +#include "gui/ApplicationSettingsWidget.h" + +class ShortcutSettingsPage : public ISettingsPage +{ +public: + explicit ShortcutSettingsPage() = default; + ~ShortcutSettingsPage() override = default; + + QString name() override; + QIcon icon() override; + QWidget* createWidget() override; + void loadSettings(QWidget* widget) override; + void saveSettings(QWidget* widget) override; +}; + +#endif // KEEPASSXC_BROWSERSETTINGSPAGE_H diff --git a/src/autotype/ShortcutWidget.cpp b/src/gui/widgets/ShortcutWidget.cpp similarity index 83% rename from src/autotype/ShortcutWidget.cpp rename to src/gui/widgets/ShortcutWidget.cpp index 9728fcc4f..4f9c14476 100644 --- a/src/autotype/ShortcutWidget.cpp +++ b/src/gui/widgets/ShortcutWidget.cpp @@ -20,13 +20,8 @@ #include #include -#include "autotype/AutoType.h" - ShortcutWidget::ShortcutWidget(QWidget* parent) : QLineEdit(parent) - , m_key(static_cast(0)) - , m_modifiers(nullptr) - , m_locked(false) { setReadOnly(true); } @@ -41,6 +36,11 @@ Qt::KeyboardModifiers ShortcutWidget::modifiers() const return m_modifiers; } +QKeySequence ShortcutWidget::sequence() const +{ + return (m_key == Qt::Key_unknown) ? QKeySequence() : QKeySequence(m_key | m_modifiers); +} + void ShortcutWidget::setShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) { m_key = key; @@ -48,22 +48,15 @@ void ShortcutWidget::setShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers) m_locked = true; displayShortcut(m_key, m_modifiers); - - QString error; - if (autoType()->registerGlobalShortcut(m_key, m_modifiers, &error)) { - setStyleSheet(""); - } else { - QToolTip::showText(mapToGlobal(rect().bottomLeft()), error); - setStyleSheet("background-color: #FF9696;"); - } + emit shortcutChanged(m_key, m_modifiers); } void ShortcutWidget::resetShortcut() { - m_key = static_cast(0); - m_modifiers = nullptr; + m_key = Qt::Key_unknown; + m_modifiers = Qt::NoModifier; m_locked = false; - autoType()->unregisterGlobalShortcut(); + emit shortcutReset(); } void ShortcutWidget::keyPressEvent(QKeyEvent* event) @@ -116,13 +109,11 @@ void ShortcutWidget::keyEvent(QKeyEvent* event) setShortcut(key, modifiers); } else { resetShortcut(); - setStyleSheet(""); displayShortcut(key, modifiers); } } else { if (m_locked) { resetShortcut(); - setStyleSheet(""); } displayShortcut(static_cast(0), modifiers); diff --git a/src/autotype/ShortcutWidget.h b/src/gui/widgets/ShortcutWidget.h similarity index 82% rename from src/autotype/ShortcutWidget.h rename to src/gui/widgets/ShortcutWidget.h index 60898ab7e..6ff9b3d77 100644 --- a/src/autotype/ShortcutWidget.h +++ b/src/gui/widgets/ShortcutWidget.h @@ -18,6 +18,7 @@ #ifndef KEEPASSX_SHORTCUTWIDGET_H #define KEEPASSX_SHORTCUTWIDGET_H +#include #include class ShortcutWidget : public QLineEdit @@ -26,10 +27,17 @@ class ShortcutWidget : public QLineEdit public: explicit ShortcutWidget(QWidget* parent = nullptr); + Qt::Key key() const; Qt::KeyboardModifiers modifiers() const; + QKeySequence sequence() const; + void setShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers); +signals: + void shortcutChanged(Qt::Key key, Qt::KeyboardModifiers modifiers); + void shortcutReset(); + protected: void keyPressEvent(QKeyEvent* event) override; void keyReleaseEvent(QKeyEvent* event) override; @@ -39,9 +47,9 @@ private: void displayShortcut(Qt::Key key, Qt::KeyboardModifiers modifiers); void resetShortcut(); - Qt::Key m_key; - Qt::KeyboardModifiers m_modifiers; - bool m_locked; + Qt::Key m_key = Qt::Key_unknown; + Qt::KeyboardModifiers m_modifiers = Qt::NoModifier; + bool m_locked = false; }; #endif // KEEPASSX_SHORTCUTWIDGET_H diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index f52e30629..aaa02bc82 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -33,6 +33,7 @@ #include "config-keepassx-tests.h" #include "core/Tools.h" #include "crypto/Crypto.h" +#include "gui/ActionCollection.h" #include "gui/ApplicationSettingsWidget.h" #include "gui/CategoryListWidget.h" #include "gui/CloneDialog.h" @@ -43,6 +44,7 @@ #include "gui/PasswordGeneratorWidget.h" #include "gui/PasswordWidget.h" #include "gui/SearchWidget.h" +#include "gui/ShortcutSettingsPage.h" #include "gui/TotpDialog.h" #include "gui/TotpSetupDialog.h" #include "gui/databasekey/KeyFileEditWidget.h" @@ -1851,6 +1853,52 @@ void TestGui::testTrayRestoreHide() #endif } +void TestGui::testShortcutConfig() +{ + // Action collection should not be empty + QVERIFY(!ActionCollection::instance()->actions().isEmpty()); + + // Add an action, make sure it gets added + QAction* a = new QAction(ActionCollection::instance()); + a->setObjectName("MyAction1"); + ActionCollection::instance()->addAction(a); + QVERIFY(ActionCollection::instance()->actions().contains(a)); + + const QKeySequence seq(Qt::CTRL + Qt::SHIFT + Qt::ALT + Qt::Key_N); + ActionCollection::instance()->setDefaultShortcut(a, seq); + QCOMPARE(ActionCollection::instance()->defaultShortcut(a), seq); + + bool v = false; + m_mainWindow->addAction(a); + connect(a, &QAction::triggered, ActionCollection::instance(), [&v] { v = !v; }); + QTest::keyClick(m_mainWindow.data(), Qt::Key_N, Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier); + QVERIFY(v); + + // Change shortcut and save + const QKeySequence newSeq(Qt::CTRL + Qt::SHIFT + Qt::ALT + Qt::Key_M); + a->setShortcut(newSeq); + QVERIFY(a->shortcut() != ActionCollection::instance()->defaultShortcut(a)); + ActionCollection::instance()->saveShortcuts(); + QCOMPARE(a->shortcut(), newSeq); + const auto shortcuts = Config::instance()->getShortcuts(); + Config::ShortcutEntry entryForA; + for (const auto& s : shortcuts) { + if (s.name == a->objectName()) { + entryForA = s; + break; + } + } + QCOMPARE(entryForA.name, a->objectName()); + QCOMPARE(QKeySequence::fromString(entryForA.shortcut), a->shortcut()); + + // trigger the old shortcut + QTest::keyClick(m_mainWindow.data(), Qt::Key_N, Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier); + QVERIFY(v); // value of v should not change + QTest::keyClick(m_mainWindow.data(), Qt::Key_M, Qt::ControlModifier | Qt::ShiftModifier | Qt::AltModifier); + QVERIFY(!v); + disconnect(a, nullptr, nullptr, nullptr); +} + void TestGui::testAutoType() { // Clear entries from root group to guarantee order diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index c631e5bdc..057dfb3c2 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -67,6 +67,7 @@ private slots: void testSortGroups(); void testAutoType(); void testTrayRestoreHide(); + void testShortcutConfig(); private: void addCannedEntries();