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 <support@dmapps.us>
This commit is contained in:
Waqar Ahmed 2024-02-04 16:29:04 +05:00 committed by GitHub
parent d03f5e4977
commit a472ef8a93
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1238 additions and 233 deletions

View File

@ -5156,34 +5156,18 @@ Are you sure you want to continue with this file?</source>
<source>&amp;New Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Create a new database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Merge From Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Merge from another KDBX database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;New Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Add a new entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Edit Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>View or edit entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Delete Entry</source>
<translation type="unfinished"></translation>
@ -5192,10 +5176,6 @@ Are you sure you want to continue with this file?</source>
<source>&amp;New Group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Add a new group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Edit Group</source>
<translation type="unfinished"></translation>
@ -5228,18 +5208,10 @@ Are you sure you want to continue with this file?</source>
<source>Database &amp;Reports</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Statistics, health check, etc.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Database Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Database settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Clone Entry</source>
<translation type="unfinished"></translation>
@ -5248,34 +5220,18 @@ Are you sure you want to continue with this file?</source>
<source>Move u&amp;p</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Move entry one step up</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Move do&amp;wn</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Move entry one step down</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy &amp;Username</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy username to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy &amp;Password</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy password to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Settings</source>
<translation type="unfinished"></translation>
@ -5308,26 +5264,14 @@ Are you sure you want to continue with this file?</source>
<source>&amp;Title</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy title to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy &amp;URL</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy URL to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy notes to clipboard</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;CSV File</source>
<translation type="unfinished"></translation>
@ -5340,26 +5284,14 @@ Are you sure you want to continue with this file?</source>
<source>KeePass 1 Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import a KeePass 1 database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>1Password Vault</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import a 1Password Vault</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>CSV File</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import a CSV file</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show TOTP</source>
<translation type="unfinished"></translation>
@ -5404,10 +5336,6 @@ Are you sure you want to continue with this file?</source>
<source>&amp;Online Help</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Go to online documentation</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>&amp;User Guide</source>
<translation type="unfinished"></translation>
@ -5480,10 +5408,6 @@ Are you sure you want to continue with this file?</source>
<source>&amp;XML File</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>XML File</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Clear history</source>
<translation type="unfinished"></translation>
@ -5575,11 +5499,251 @@ We recommend you use the AppImage available on our downloads page.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Passkeys</source>
<source>Import Passkey</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import Passkey</source>
<source>Quit Application</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open About Dialog</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Create Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Merge From Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Create Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Edit Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Delete Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Create Group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Edit Group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Delete Group</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Download All Favicons</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Sort Groups A-Z</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Sort Groups Z-A</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save Database As</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show Database Security</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show Database Reports</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show Database Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show Passkeys</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Clone Entry</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Move Entry Up</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Move Entry Down</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy Username</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy Password</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show Application Settings</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show Password Generator</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Perform Auto-Type: {USERNAME}</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Perform Auto-Type: {USERNAME}{ENTER}</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Perform Auto-Type: {PASSWORD}</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Perform Auto-Type: {PASSWORD}{ENTER}</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Perform Auto-Type: {TOTP}</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy Title</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy URL</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Copy Notes</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Export to CSV</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Export to HTML</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import KeePass1 Database</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import 1Password Vault</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Import CSV File</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show TOTP QR Code</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set up TOTP</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Empty Recycle Bin</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open Donation Website</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open Bug Report</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open Online Documentation</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Open Keyboard Shortcuts Guide</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Save Database Backup</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SSH Agent: Add Key</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>SSH Agent: Remove Key</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle Compact Mode</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set Theme: Automatic</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set Theme: Light</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set Theme: Dark</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Set Theme: Classic</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle Show Toolbar</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle Show Preview Panel</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle Always on Top</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle Hide Usernames</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle Hide Passwords</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Export to XML</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Toggle Allow Screen Capture</source>
<translation type="unfinished"></translation>
</message>
</context>
@ -8220,63 +8384,15 @@ Kernel: %3 %4</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES initialization failed</source>
<source>Enter Shortcut</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES encrypt failed</source>
<source>Action</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to store in Linux Keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not locate key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not read key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES decrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Polkit authentication agent was available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit authorization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Quick Unlock provider is available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit returned an error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to encrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Passkeys</source>
<source>Shortcuts</source>
<translation type="unfinished"></translation>
</message>
<message>
@ -8307,6 +8423,66 @@ Kernel: %3 %4</source>
<source>Resident Keys are not supported</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Passkeys</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES initialization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES encrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to store in Linux Keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit returned an error: %1</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not locate key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not read key in keyring</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>AES decrypt failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Polkit authentication agent was available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Polkit authorization failed</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No Quick Unlock provider is available</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to init KeePassXC crypto.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to encrypt key data.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to get Windows Hello credential.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Failed to decrypt key data.</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>QtIOCompressor</name>
@ -9147,6 +9323,29 @@ Kernel: %3 %4</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>ShortcutSettingsWidget</name>
<message>
<source>Double click an action to change its shortcut</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Shortcut Conflict</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Filter...</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Shortcut %1 conflicts with &apos;%2&apos;. Overwrite shortcut?</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Reset Shortcuts</source>
<translation type="unfinished"></translation>
</message>
</context>
<context>
<name>TagModel</name>
<message>

View File

@ -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)

View File

@ -597,4 +597,28 @@ void Config::createTempFileInstance()
tmpFile->setParent(m_instance);
}
QList<Config::ShortcutEntry> Config::getShortcuts() const
{
m_settings->beginGroup("Shortcuts");
const auto keys = m_settings->childKeys();
QList<ShortcutEntry> 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<ShortcutEntry>& 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

View File

@ -21,6 +21,7 @@
#include <QPointer>
#include <QVariant>
#include <QVector>
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<ShortcutEntry> getShortcuts() const;
void setShortcuts(const QList<ShortcutEntry>& shortcuts);
static Config* instance();
static void createConfigFromFile(const QString& configFileName, const QString& localConfigFileName = {});
static void createTempFileInstance();

View File

@ -0,0 +1,123 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include "ActionCollection.h"
#include "core/Config.h"
#include <QDebug>
ActionCollection* ActionCollection::instance()
{
static ActionCollection ac;
return &ac;
}
QList<QAction*> ActionCollection::actions() const
{
return m_actions;
}
void ActionCollection::addAction(QAction* action)
{
if (!m_actions.contains(action)) {
m_actions << action;
}
}
void ActionCollection::addActions(const QList<QAction*>& 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<QKeySequence> ActionCollection::defaultShortcuts(const QAction* action) const
{
return action->property("defaultShortcuts").value<QList<QKeySequence>>();
}
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<QKeySequence>& shortcuts)
{
action->setShortcuts(shortcuts);
action->setProperty("defaultShortcuts", QVariant::fromValue(shortcuts));
}
void ActionCollection::restoreShortcuts()
{
const auto shortcuts = Config::instance()->getShortcuts();
QHash<QString, QAction*> 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<Config::ShortcutEntry> 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;
}

View File

@ -0,0 +1,61 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_ACTION_COLLECTION_H
#define KEEPASSXC_ACTION_COLLECTION_H
#include <QAction>
#include <QKeySequence>
#include <QObject>
/**
* 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<QAction*> actions() const;
void addAction(QAction* action);
void addActions(const QList<QAction*>& actions);
QKeySequence defaultShortcut(const QAction* a) const;
QList<QKeySequence> 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<QKeySequence>& 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<QAction*> m_actions;
};
#endif

View File

@ -21,6 +21,7 @@
#include "ui_ApplicationSettingsWidgetSecurity.h"
#include <QDesktopServices>
#include <QDir>
#include <QToolTip>
#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);

View File

@ -58,8 +58,8 @@
<rect>
<x>0</x>
<y>0</y>
<width>564</width>
<height>930</height>
<width>566</width>
<height>975</height>
</rect>
</property>
<layout class="QVBoxLayout" name="verticalLayout_8">
@ -1260,7 +1260,7 @@
<customwidget>
<class>ShortcutWidget</class>
<extends>QLineEdit</extends>
<header>autotype/ShortcutWidget.h</header>
<header>gui/widgets/ShortcutWidget.h</header>
</customwidget>
</customwidgets>
<tabstops>

View File

@ -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)

View File

@ -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<Ui::MainWindow> m_ui;
SignalMultiplexer m_actionMultiplexer;

View File

@ -443,6 +443,9 @@
<property name="text">
<string>&amp;Quit</string>
</property>
<property name="toolTip">
<string>Quit Application</string>
</property>
<property name="menuRole">
<enum>QAction::QuitRole</enum>
</property>
@ -451,6 +454,9 @@
<property name="text">
<string>&amp;About</string>
</property>
<property name="toolTip">
<string>Open About Dialog</string>
</property>
<property name="menuRole">
<enum>QAction::AboutRole</enum>
</property>
@ -467,6 +473,9 @@
<property name="text">
<string>&amp;Open Database…</string>
</property>
<property name="toolTip">
<string>Open Database</string>
</property>
</action>
<action name="actionDatabaseSave">
<property name="enabled">
@ -489,7 +498,7 @@
<string>&amp;New Database…</string>
</property>
<property name="toolTip">
<string>Create a new database</string>
<string>Create Database</string>
</property>
</action>
<action name="actionDatabaseMerge">
@ -497,7 +506,7 @@
<string>&amp;Merge From Database…</string>
</property>
<property name="toolTip">
<string>Merge from another KDBX database</string>
<string>Merge From Database</string>
</property>
</action>
<action name="actionEntryNew">
@ -508,7 +517,7 @@
<string>&amp;New Entry…</string>
</property>
<property name="toolTip">
<string>Add a new entry</string>
<string>Create Entry</string>
</property>
</action>
<action name="actionEntryEdit">
@ -519,7 +528,7 @@
<string>&amp;Edit Entry…</string>
</property>
<property name="toolTip">
<string>View or edit entry</string>
<string>Edit Entry</string>
</property>
</action>
<action name="actionEntryDelete">
@ -529,6 +538,9 @@
<property name="text">
<string>&amp;Delete Entry…</string>
</property>
<property name="toolTip">
<string>Delete Entry</string>
</property>
</action>
<action name="actionGroupNew">
<property name="enabled">
@ -538,7 +550,7 @@
<string>&amp;New Group…</string>
</property>
<property name="toolTip">
<string>Add a new group</string>
<string>Create Group</string>
</property>
</action>
<action name="actionGroupEdit">
@ -548,6 +560,9 @@
<property name="text">
<string>&amp;Edit Group…</string>
</property>
<property name="toolTip">
<string>Edit Group</string>
</property>
</action>
<action name="actionGroupDelete">
<property name="enabled">
@ -556,6 +571,9 @@
<property name="text">
<string>&amp;Delete Group…</string>
</property>
<property name="toolTip">
<string>Delete Group</string>
</property>
</action>
<action name="actionGroupDownloadFavicons">
<property name="enabled">
@ -564,6 +582,9 @@
<property name="text">
<string>Download All &amp;Favicons…</string>
</property>
<property name="toolTip">
<string>Download All Favicons</string>
</property>
</action>
<action name="actionGroupSortAsc">
<property name="enabled">
@ -572,6 +593,9 @@
<property name="text">
<string>Sort &amp;A-Z</string>
</property>
<property name="toolTip">
<string>Sort Groups A-Z</string>
</property>
</action>
<action name="actionGroupSortDesc">
<property name="enabled">
@ -580,6 +604,9 @@
<property name="text">
<string>Sort &amp;Z-A</string>
</property>
<property name="toolTip">
<string>Sort Groups Z-A</string>
</property>
</action>
<action name="actionDatabaseSaveAs">
<property name="enabled">
@ -588,6 +615,9 @@
<property name="text">
<string>Sa&amp;ve Database As…</string>
</property>
<property name="toolTip">
<string>Save Database As</string>
</property>
</action>
<action name="actionDatabaseSecurity">
<property name="enabled">
@ -596,6 +626,9 @@
<property name="text">
<string>Database &amp;Security…</string>
</property>
<property name="toolTip">
<string>Show Database Security</string>
</property>
</action>
<action name="actionReports">
<property name="enabled">
@ -605,7 +638,7 @@
<string>Database &amp;Reports…</string>
</property>
<property name="toolTip">
<string>Statistics, health check, etc.</string>
<string>Show Database Reports</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
@ -619,7 +652,7 @@
<string>&amp;Database Settings…</string>
</property>
<property name="toolTip">
<string>Database settings</string>
<string>Show Database Settings</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
@ -633,7 +666,7 @@
<string>Passkeys…</string>
</property>
<property name="toolTip">
<string>Passkeys</string>
<string>Show Passkeys</string>
</property>
<property name="menuRole">
<enum>QAction::NoRole</enum>
@ -660,6 +693,9 @@
<property name="text">
<string>&amp;Clone Entry…</string>
</property>
<property name="toolTip">
<string>Clone Entry</string>
</property>
</action>
<action name="actionEntryMoveUp">
<property name="enabled">
@ -669,7 +705,7 @@
<string>Move u&amp;p</string>
</property>
<property name="toolTip">
<string>Move entry one step up</string>
<string>Move Entry Up</string>
</property>
</action>
<action name="actionEntryMoveDown">
@ -680,7 +716,7 @@
<string>Move do&amp;wn</string>
</property>
<property name="toolTip">
<string>Move entry one step down</string>
<string>Move Entry Down</string>
</property>
</action>
<action name="actionEntryCopyUsername">
@ -691,7 +727,7 @@
<string>Copy &amp;Username</string>
</property>
<property name="toolTip">
<string>Copy username to clipboard</string>
<string>Copy Username</string>
</property>
</action>
<action name="actionEntryCopyPassword">
@ -702,7 +738,7 @@
<string>Copy &amp;Password</string>
</property>
<property name="toolTip">
<string>Copy password to clipboard</string>
<string>Copy Password</string>
</property>
</action>
<action name="actionSettings">
@ -712,6 +748,9 @@
<property name="text">
<string>&amp;Settings</string>
</property>
<property name="toolTip">
<string>Show Application Settings</string>
</property>
<property name="menuRole">
<enum>QAction::PreferencesRole</enum>
</property>
@ -723,6 +762,9 @@
<property name="text">
<string>&amp;Password Generator</string>
</property>
<property name="toolTip">
<string>Show Password Generator</string>
</property>
</action>
<action name="actionEntryAutoType">
<property name="enabled">
@ -751,7 +793,7 @@
<string notr="true">{USERNAME}</string>
</property>
<property name="toolTip">
<string notr="true">{USERNAME}</string>
<string>Perform Auto-Type: {USERNAME}</string>
</property>
</action>
<action name="actionEntryAutoTypeUsernameEnter">
@ -765,7 +807,7 @@
<string notr="true">{USERNAME}{ENTER}</string>
</property>
<property name="toolTip">
<string notr="true">{USERNAME}{ENTER}</string>
<string>Perform Auto-Type: {USERNAME}{ENTER}</string>
</property>
</action>
<action name="actionEntryAutoTypePassword">
@ -779,7 +821,7 @@
<string notr="true">{PASSWORD}</string>
</property>
<property name="toolTip">
<string notr="true">{PASSWORD}</string>
<string>Perform Auto-Type: {PASSWORD}</string>
</property>
</action>
<action name="actionEntryAutoTypePasswordEnter">
@ -793,7 +835,7 @@
<string notr="true">{PASSWORD}{ENTER}</string>
</property>
<property name="toolTip">
<string notr="true">{PASSWORD}{ENTER}</string>
<string>Perform Auto-Type: {PASSWORD}{ENTER}</string>
</property>
</action>
<action name="actionEntryAutoTypeTOTP">
@ -807,7 +849,7 @@
<string notr="true">{TOTP}</string>
</property>
<property name="toolTip">
<string notr="true">{TOTP}</string>
<string>Perform Auto-Type: {TOTP}</string>
</property>
</action>
<action name="actionEntryDownloadIcon">
@ -847,7 +889,7 @@
<string>&amp;Title</string>
</property>
<property name="toolTip">
<string>Copy title to clipboard</string>
<string>Copy Title</string>
</property>
</action>
<action name="actionEntryCopyURL">
@ -858,7 +900,7 @@
<string>Copy &amp;URL</string>
</property>
<property name="toolTip">
<string>Copy URL to clipboard</string>
<string>Copy URL</string>
</property>
</action>
<action name="actionEntryCopyNotes">
@ -869,7 +911,7 @@
<string>&amp;Notes</string>
</property>
<property name="toolTip">
<string>Copy notes to clipboard</string>
<string>Copy Notes</string>
</property>
</action>
<action name="actionExportCsv">
@ -879,6 +921,9 @@
<property name="text">
<string>&amp;CSV File…</string>
</property>
<property name="toolTip">
<string>Export to CSV</string>
</property>
</action>
<action name="actionExportHtml">
<property name="enabled">
@ -887,13 +932,16 @@
<property name="text">
<string>&amp;HTML File…</string>
</property>
<property name="toolTip">
<string>Export to HTML</string>
</property>
</action>
<action name="actionImportKeePass1">
<property name="text">
<string>KeePass 1 Database…</string>
</property>
<property name="toolTip">
<string>Import a KeePass 1 database</string>
<string>Import KeePass1 Database</string>
</property>
</action>
<action name="actionImportOpVault">
@ -901,7 +949,7 @@
<string>1Password Vault…</string>
</property>
<property name="toolTip">
<string>Import a 1Password Vault</string>
<string>Import 1Password Vault</string>
</property>
</action>
<action name="actionImportCsv">
@ -909,7 +957,7 @@
<string>CSV File…</string>
</property>
<property name="toolTip">
<string>Import a CSV file</string>
<string>Import CSV File</string>
</property>
</action>
<action name="actionEntryTotp">
@ -921,11 +969,17 @@
<property name="text">
<string>Show QR Code</string>
</property>
<property name="toolTip">
<string>Show TOTP QR Code</string>
</property>
</action>
<action name="actionEntrySetupTotp">
<property name="text">
<string>Set up TOTP…</string>
</property>
<property name="toolTip">
<string>Set up TOTP</string>
</property>
</action>
<action name="actionEntryCopyTotp">
<property name="text">
@ -941,6 +995,9 @@
<property name="text">
<string>E&amp;mpty recycle bin</string>
</property>
<property name="toolTip">
<string>Empty Recycle Bin</string>
</property>
<property name="visible">
<bool>false</bool>
</property>
@ -949,11 +1006,17 @@
<property name="text">
<string>&amp;Donate</string>
</property>
<property name="toolTip">
<string>Open Donation Website</string>
</property>
</action>
<action name="actionBugReport">
<property name="text">
<string>Report a &amp;Bug</string>
</property>
<property name="toolTip">
<string>Open Bug Report</string>
</property>
</action>
<action name="actionGettingStarted">
<property name="text">
@ -968,7 +1031,7 @@
<string>&amp;Online Help</string>
</property>
<property name="toolTip">
<string>Go to online documentation</string>
<string>Open Online Documentation</string>
</property>
</action>
<action name="actionUserGuide">
@ -983,6 +1046,9 @@
<property name="text">
<string>&amp;Keyboard Shortcuts</string>
</property>
<property name="toolTip">
<string>Open Keyboard Shortcuts Guide</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+/</string>
</property>
@ -994,16 +1060,25 @@
<property name="text">
<string>Save Database Backup…</string>
</property>
<property name="toolTip">
<string>Save Database Backup</string>
</property>
</action>
<action name="actionEntryAddToAgent">
<property name="text">
<string>Add key to SSH Agent</string>
</property>
<property name="toolTip">
<string>SSH Agent: Add Key</string>
</property>
</action>
<action name="actionEntryRemoveFromAgent">
<property name="text">
<string>Remove key from SSH Agent</string>
</property>
<property name="toolTip">
<string>SSH Agent: Remove Key</string>
</property>
</action>
<action name="actionCompactMode">
<property name="checkable">
@ -1012,6 +1087,9 @@
<property name="text">
<string>Compact Mode</string>
</property>
<property name="toolTip">
<string>Toggle Compact Mode</string>
</property>
</action>
<action name="actionThemeAuto">
<property name="checkable">
@ -1023,6 +1101,9 @@
<property name="text">
<string>Automatic</string>
</property>
<property name="toolTip">
<string>Set Theme: Automatic</string>
</property>
</action>
<action name="actionThemeLight">
<property name="checkable">
@ -1031,6 +1112,9 @@
<property name="text">
<string>Light</string>
</property>
<property name="toolTip">
<string>Set Theme: Light</string>
</property>
</action>
<action name="actionThemeDark">
<property name="checkable">
@ -1039,6 +1123,9 @@
<property name="text">
<string>Dark</string>
</property>
<property name="toolTip">
<string>Set Theme: Dark</string>
</property>
</action>
<action name="actionThemeClassic">
<property name="checkable">
@ -1047,6 +1134,9 @@
<property name="text">
<string>Classic (Platform-native)</string>
</property>
<property name="toolTip">
<string>Set Theme: Classic</string>
</property>
</action>
<action name="actionShowToolbar">
<property name="checkable">
@ -1058,6 +1148,9 @@
<property name="text">
<string>Show Toolbar</string>
</property>
<property name="toolTip">
<string>Toggle Show Toolbar</string>
</property>
</action>
<action name="actionShowPreviewPanel">
<property name="checkable">
@ -1069,6 +1162,9 @@
<property name="text">
<string>Show Preview Panel</string>
</property>
<property name="toolTip">
<string>Toggle Show Preview Panel</string>
</property>
</action>
<action name="actionAlwaysOnTop">
<property name="checkable">
@ -1077,6 +1173,9 @@
<property name="text">
<string>Always on Top</string>
</property>
<property name="toolTip">
<string>Toggle Always on Top</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+Shift+A</string>
</property>
@ -1088,6 +1187,9 @@
<property name="text">
<string>Hide Usernames</string>
</property>
<property name="toolTip">
<string>Toggle Hide Usernames</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+Shift+B</string>
</property>
@ -1102,6 +1204,9 @@
<property name="text">
<string>Hide Passwords</string>
</property>
<property name="toolTip">
<string>Toggle Hide Passwords</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+Shift+C</string>
</property>
@ -1114,7 +1219,7 @@
<string notr="true">{USERNAME}{TAB}{PASSWORD}{ENTER}</string>
</property>
<property name="toolTip">
<string notr="true">{USERNAME}{TAB}{PASSWORD}{ENTER}</string>
<string notr="true">Perform Auto-Type: Entry Default</string>
</property>
</action>
<action name="actionGroupClone">
@ -1130,7 +1235,7 @@
<string notr="true" extracomment="Translatable string with plural form set in CPP file">Restore Entry(s)</string>
</property>
<property name="toolTip">
<string notr="true" extracomment="Translatable string with plural form set in CPP file">Restore Entry(s)</string>
<string notr="true" extracomment="Translatable string with plural form set in CPP file">Restore Entry</string>
</property>
<property name="shortcut">
<string notr="true">Ctrl+R</string>
@ -1152,7 +1257,7 @@
<string>&amp;XML File…</string>
</property>
<property name="toolTip">
<string>XML File…</string>
<string>Export to XML</string>
</property>
</action>
<action name="actionAllowScreenCapture">
@ -1162,6 +1267,9 @@
<property name="text">
<string>Allow Screen Capture</string>
</property>
<property name="toolTip">
<string>Toggle Allow Screen Capture</string>
</property>
</action>
</widget>
<customwidgets>

View File

@ -0,0 +1,282 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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 <QAbstractButton>
#include <QDebug>
#include <QDialog>
#include <QDialogButtonBox>
#include <QHeaderView>
#include <QLabel>
#include <QLineEdit>
#include <QPushButton>
#include <QSortFilterProxyModel>
#include <QStandardItemModel>
#include <QTableView>
#include <QVBoxLayout>
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<QAction*>();
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<QAction*, QKeySequence> 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<ShortcutSettingsWidget*>(widget)->loadSettings();
}
void ShortcutSettingsPage::saveSettings(QWidget* widget)
{
static_cast<ShortcutSettingsWidget*>(widget)->saveSettings();
}

View File

@ -0,0 +1,36 @@
/*
* Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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

View File

@ -20,13 +20,8 @@
#include <QKeyEvent>
#include <QToolTip>
#include "autotype/AutoType.h"
ShortcutWidget::ShortcutWidget(QWidget* parent)
: QLineEdit(parent)
, m_key(static_cast<Qt::Key>(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<Qt::Key>(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<Qt::Key>(0), modifiers);

View File

@ -18,6 +18,7 @@
#ifndef KEEPASSX_SHORTCUTWIDGET_H
#define KEEPASSX_SHORTCUTWIDGET_H
#include <QKeySequence>
#include <QLineEdit>
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

View File

@ -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

View File

@ -67,6 +67,7 @@ private slots:
void testSortGroups();
void testAutoType();
void testTrayRestoreHide();
void testShortcutConfig();
private:
void addCannedEntries();