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?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -5192,10 +5176,6 @@ Are you sure you want to continue with this file?
-
-
-
-
@@ -5228,18 +5208,10 @@ Are you sure you want to continue with this file?
-
-
-
-
-
-
-
-
@@ -5248,34 +5220,18 @@ Are you sure you want to continue with this file?
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -5308,26 +5264,14 @@ Are you sure you want to continue with this file?
-
-
-
-
-
-
-
-
-
-
-
-
@@ -5340,26 +5284,14 @@ Are you sure you want to continue with this file?
-
-
-
-
-
-
-
-
-
-
-
-
@@ -5404,10 +5336,6 @@ Are you sure you want to continue with this file?
-
-
-
-
@@ -5480,10 +5408,6 @@ Are you sure you want to continue with this file?
-
-
-
-
@@ -5575,11 +5499,251 @@ We recommend you use the AppImage available on our downloads page.
-
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -8220,63 +8384,15 @@ Kernel: %3 %4
-
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
@@ -8307,6 +8423,66 @@ Kernel: %3 %4
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
QtIOCompressor
@@ -9147,6 +9323,29 @@ Kernel: %3 %4
+
+ ShortcutSettingsWidget
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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();