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();