From 72410c499dc821db99feaacabbc3ae8095ea9480 Mon Sep 17 00:00:00 2001 From: Loren Burkholder Date: Tue, 26 Sep 2023 10:19:32 -0400 Subject: [PATCH] Add UI to allow editing shortcuts dynamically --- CMakeLists.txt | 1 + resources/qml/dialogs/ShortcutEditor.qml | 92 ++++++++++++++++++++++++ resources/qml/pages/UserSettingsPage.qml | 18 +++++ src/MainWindow.cpp | 1 + src/MainWindow.h | 2 + src/UserSettingsPage.cpp | 5 ++ src/UserSettingsPage.h | 2 + src/ui/ShortcutRegistry.cpp | 92 ++++++++++++++---------- src/ui/ShortcutRegistry.h | 27 +++---- 9 files changed, 191 insertions(+), 49 deletions(-) create mode 100644 resources/qml/dialogs/ShortcutEditor.qml diff --git a/CMakeLists.txt b/CMakeLists.txt index 9eb8df67..cd6ac90b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -779,6 +779,7 @@ set(QML_SOURCES resources/qml/dialogs/RoomMembers.qml resources/qml/dialogs/AllowedRoomsSettingsDialog.qml resources/qml/dialogs/RoomSettings.qml + resources/qml/dialogs/ShortcutEditor.qml resources/qml/dialogs/UserProfile.qml resources/qml/emoji/StickerPicker.qml resources/qml/pages/LoginPage.qml diff --git a/resources/qml/dialogs/ShortcutEditor.qml b/resources/qml/dialogs/ShortcutEditor.qml new file mode 100644 index 00000000..0bd8a00b --- /dev/null +++ b/resources/qml/dialogs/ShortcutEditor.qml @@ -0,0 +1,92 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + +import ".." +import "../ui" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import QtQuick.Window +import im.nheko + +ApplicationWindow { + id: shortcutEditorDialog + + minimumWidth: 500 + minimumHeight: 450 + width: 500 + height: 680 + color: palette.window + modality: Qt.NonModal + flags: Qt.Dialog | Qt.WindowCloseButtonHint | Qt.WindowTitleHint + title: qsTr("Keyboard shortcuts") + + ScrollView { + padding: Nheko.paddingMedium + ScrollBar.horizontal.visible: false + anchors.fill: parent + + ListView { + model: ShortcutRegistry + + delegate: RowLayout { + id: del + + required property string name + required property string description + required property string shortcut + + spacing: Nheko.paddingMedium + width: ListView.view.width + height: implicitHeight + Nheko.paddingSmall * 2 + + ColumnLayout { + spacing: Nheko.paddingSmall + + Label { + text: del.name + font.bold: true + font.pointSize: fontMetrics.font.pointSize * 1.1 + } + + Label { + text: del.description + } + } + + Item { Layout.fillWidth: true } + + Button { + property bool selectingNewShortcut: false + + text: selectingNewShortcut ? qsTr("Input..") : del.shortcut + onClicked: selectingNewShortcut = !selectingNewShortcut + Keys.onPressed: event => { + if (!selectingNewShortcut) + return; + event.accepted = true; + + let keySequence = ""; + if (event.modifiers & Qt.ControlModifier) + keySequence += "Ctrl+"; + if (event.modifiers & Qt.AltModifier) + keySequence += "Alt+"; + if (event.modifiers & Qt.MetaModifier) + keySequence += "Meta+"; + if (event.modifiers & Qt.ShiftModifier) + keySequence += "Shift+"; + + if (event.key === 0 || event.key === Qt.Key_unknown || event.key === Qt.Key_Control || event.key === Qt.Key_Alt || event.key === Qt.Key_AltGr || event.key === Qt.Key_Meta || event.key === Qt.Key_Shift) + keySequence += "..."; + else { + keySequence += ShortcutRegistry.keycodeToChar(event.key); + ShortcutRegistry.changeShortcut(del.name, keySequence); + selectingNewShortcut = false; + } + } + } + } + } + } +} diff --git a/resources/qml/pages/UserSettingsPage.qml b/resources/qml/pages/UserSettingsPage.qml index f23095b6..1407ead7 100644 --- a/resources/qml/pages/UserSettingsPage.qml +++ b/resources/qml/pages/UserSettingsPage.qml @@ -5,6 +5,7 @@ pragma ComponentBehavior: Bound import ".." import "../ui" +import "../dialogs" import Qt.labs.platform 1.1 as Platform import QtQuick 2.15 import QtQuick.Controls 2.15 @@ -215,6 +216,23 @@ Rectangle { } } } + DelegateChoice { + roleValue: UserSettingsModel.ConfigureKeyboardShortcuts + Button { + text: qsTr("CONFIGURE") + onClicked: { + var dialog = keyboardShortcutsDialog.createObject(); + dialog.show(); + destroyOnClose(dialog); + } + + Component { + id: keyboardShortcutsDialog + + ShortcutEditor {} + } + } + } DelegateChoice { Text { text: model.value diff --git a/src/MainWindow.cpp b/src/MainWindow.cpp index d06171de..a003dee3 100644 --- a/src/MainWindow.cpp +++ b/src/MainWindow.cpp @@ -57,6 +57,7 @@ MainWindow *MainWindow::instance_ = nullptr; MainWindow::MainWindow(QWindow *parent) : QQuickView(parent) , userSettings_{UserSettings::instance()} + , shortcuts_{new ShortcutRegistry} { instance_ = this; diff --git a/src/MainWindow.h b/src/MainWindow.h index fc06e183..e5b395cb 100644 --- a/src/MainWindow.h +++ b/src/MainWindow.h @@ -11,6 +11,7 @@ #include #include +#include "ShortcutRegistry.h" #include "UserSettingsPage.h" #include "dock/Dock.h" @@ -140,6 +141,7 @@ private: //! Tray icon that shows the unread message count. TrayIcon *trayIcon_; Dock *dock_; + ShortcutRegistry *shortcuts_; MxcImageProvider *imgProvider = nullptr; diff --git a/src/UserSettingsPage.cpp b/src/UserSettingsPage.cpp index 5caa4838..67ab9bab 100644 --- a/src/UserSettingsPage.cpp +++ b/src/UserSettingsPage.cpp @@ -1145,6 +1145,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const return tr("Periodically update community routing information"); case ExpireEvents: return tr("Periodically delete expired events"); + case KeyboardShortcuts: + return tr("Configure keyboard shortcuts"); } } else if (role == Value) { switch (index.row()) { @@ -1444,6 +1446,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const case LoginInfoSection: case SessionKeys: case CrossSigningSecrets: + case KeyboardShortcuts: return {}; case OnlineBackupKey: return tr( @@ -1562,6 +1565,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const case UserSigningKey: case MasterKey: return KeyStatus; + case KeyboardShortcuts: + return ConfigureKeyboardShortcuts; } } else if (role == ValueLowerBound) { switch (index.row()) { diff --git a/src/UserSettingsPage.h b/src/UserSettingsPage.h index 71eb039b..61d66d4e 100644 --- a/src/UserSettingsPage.h +++ b/src/UserSettingsPage.h @@ -475,6 +475,7 @@ class UserSettingsModel : public QAbstractListModel #endif UpdateSpaceVias, ExpireEvents, + KeyboardShortcuts, AccessibilitySection, ReducedMotion, @@ -562,6 +563,7 @@ public: KeyStatus, SessionKeyImportExport, XSignKeysRequestDownload, + ConfigureKeyboardShortcuts, }; Q_ENUM(Types); diff --git a/src/ui/ShortcutRegistry.cpp b/src/ui/ShortcutRegistry.cpp index a9a47e3f..5c32b8cb 100644 --- a/src/ui/ShortcutRegistry.cpp +++ b/src/ui/ShortcutRegistry.cpp @@ -1,13 +1,27 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #include "ShortcutRegistry.h" ShortcutRegistry *ShortcutRegistry::s_instance = nullptr; EditableShortcut::EditableShortcut(QObject *parent) - : QObject{parent} + : QObject{parent} +{ + ShortcutRegistry::instance()->registerShortcut(this); +} + +EditableShortcut::EditableShortcut(const QString &name, const QString &description, QObject *parent) + : QObject{parent} + , m_name{name} + , m_description{description} { + ShortcutRegistry::instance()->registerShortcut(this); } -const QStringList EditableShortcut::shortcuts() const +const QStringList +EditableShortcut::shortcuts() const { QStringList dest; dest.resize(m_shortcuts.size()); @@ -17,7 +31,8 @@ const QStringList EditableShortcut::shortcuts() const return dest; } -void EditableShortcut::setName(const QString &name) +void +EditableShortcut::setName(const QString &name) { if (name == m_name) return; @@ -25,7 +40,8 @@ void EditableShortcut::setName(const QString &name) emit nameChanged(); } -void EditableShortcut::setDescription(const QString &description) +void +EditableShortcut::setDescription(const QString &description) { if (description == m_description) return; @@ -33,12 +49,14 @@ void EditableShortcut::setDescription(const QString &description) emit descriptionChanged(); } -void EditableShortcut::setShortcut(const QString &shortcut) +void +EditableShortcut::setShortcut(const QString &shortcut) { setShortcuts({shortcut}); } -void EditableShortcut::setShortcuts(const QStringList &shortcuts) +void +EditableShortcut::setShortcuts(const QStringList &shortcuts) { QList temp; temp.resize(shortcuts.size()); @@ -52,12 +70,13 @@ void EditableShortcut::setShortcuts(const QStringList &shortcuts) emit shortcutsChanged(); } -EditableShortcut::EditableShortcut(const QString &name, const QString &description, QObject *parent) - : QObject{parent} - , m_name{name} - , m_description{description} +ShortcutRegistry::ShortcutRegistry(QObject *parent) + : QAbstractListModel{parent} { - ShortcutRegistry::instance()->registerShortcut(this); + if (s_instance) + m_shortcuts = s_instance->m_shortcuts; + + s_instance = this; } ShortcutRegistry * @@ -66,7 +85,8 @@ ShortcutRegistry::instance() return s_instance; } -ShortcutRegistry *ShortcutRegistry::create(QQmlEngine *qmlEngine, QJSEngine *) +ShortcutRegistry * +ShortcutRegistry::create(QQmlEngine *qmlEngine, QJSEngine *) { // The instance has to exist before it is used. We cannot replace it. Q_ASSERT(s_instance); @@ -85,20 +105,20 @@ ShortcutRegistry *ShortcutRegistry::create(QQmlEngine *qmlEngine, QJSEngine *) return s_instance; } -QHash ShortcutRegistry::roleNames() const +QHash +ShortcutRegistry::roleNames() const { - return {{Roles::Name, "name"}, - {Roles::Description, "description"}, - {Roles::Shortcut, "shortcut"}}; + return { + {Roles::Name, "name"}, {Roles::Description, "description"}, {Roles::Shortcut, "shortcut"}}; } -QVariant ShortcutRegistry::data(const QModelIndex &index, int role) const +QVariant +ShortcutRegistry::data(const QModelIndex &index, int role) const { if (!index.isValid() || index.row() >= m_shortcuts.size() || index.row() < 0) return {}; - switch (role) - { + switch (role) { case Roles::Name: return m_shortcuts[index.row()]->name(); case Roles::Description: @@ -110,31 +130,29 @@ QVariant ShortcutRegistry::data(const QModelIndex &index, int role) const } } -bool ShortcutRegistry::setData(const QModelIndex &index, const QVariant &value, int role) +void +ShortcutRegistry::changeShortcut(const QString &name, const QString &newShortcut) { - if (!index.isValid() || index.row() >= m_shortcuts.size() || index.row() < 0) - return false; - - switch (role) - { - case Roles::Shortcut: - if (auto shortcut = QKeySequence(value.toString()); !shortcut.isEmpty()) { - m_shortcuts[index.row()]->setShortcut(shortcut.toString()); - return true; - } else - return false; - default: - return false; + for (int i = 0; i < m_shortcuts.size(); ++i) { + if (m_shortcuts[i]->name() == name) { + qDebug() << "new:" << newShortcut; + m_shortcuts[i]->setShortcut(newShortcut); + emit dataChanged(index(i), index(i), {Roles::Shortcut}); + return; + } } } -ShortcutRegistry::ShortcutRegistry(QObject *parent) - : QAbstractListModel{parent} +QString +ShortcutRegistry::keycodeToChar(int keycode) const { - s_instance = this; + return QString((char)keycode); } -void ShortcutRegistry::registerShortcut(EditableShortcut *action) +void +ShortcutRegistry::registerShortcut(EditableShortcut *action) { + beginInsertRows({}, m_shortcuts.size(), m_shortcuts.size()); m_shortcuts.push_back(action); + endInsertRows(); } diff --git a/src/ui/ShortcutRegistry.h b/src/ui/ShortcutRegistry.h index c075b50c..c2fe6265 100644 --- a/src/ui/ShortcutRegistry.h +++ b/src/ui/ShortcutRegistry.h @@ -1,7 +1,11 @@ +// SPDX-FileCopyrightText: Nheko Contributors +// +// SPDX-License-Identifier: GPL-3.0-or-later + #pragma once #include -#include +#include #include class EditableShortcut : public QObject @@ -10,15 +14,15 @@ class EditableShortcut : public QObject QML_ELEMENT Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged FINAL) - Q_PROPERTY(QString description READ description WRITE setDescription NOTIFY descriptionChanged FINAL) + Q_PROPERTY( + QString description READ description WRITE setDescription NOTIFY descriptionChanged FINAL) Q_PROPERTY(QString shortcut READ shortcut WRITE setShortcut NOTIFY shortcutsChanged FINAL) - Q_PROPERTY(QStringList shortcuts READ shortcuts WRITE setShortcuts NOTIFY shortcutsChanged FINAL) + Q_PROPERTY( + QStringList shortcuts READ shortcuts WRITE setShortcuts NOTIFY shortcutsChanged FINAL) public: EditableShortcut(QObject *parent = nullptr); EditableShortcut(const QString &name, const QString &description, QObject *parent = nullptr); - EditableShortcut(const QString &name, const QString &description, const QString &text, QObject *parent = nullptr); - EditableShortcut(const QString &name, const QString &description, const QIcon &icon, const QString &text, QObject *parent = nullptr); const QString &name() const { return m_name; } const QString &description() const { return m_description; } @@ -58,20 +62,19 @@ public: Shortcut, }; + explicit ShortcutRegistry(QObject *parent = nullptr); + static ShortcutRegistry *instance(); static ShortcutRegistry *create(QQmlEngine *qmlEngine, QJSEngine *); QHash roleNames() const override; - int rowCount(const QModelIndex & = QModelIndex()) const override - { - return m_shortcuts.size(); - } + int rowCount(const QModelIndex & = QModelIndex()) const override { return m_shortcuts.size(); } QVariant data(const QModelIndex &index, int role) const override; - bool setData(const QModelIndex &index, const QVariant &value, int role) override; -private: - explicit ShortcutRegistry(QObject *parent = nullptr); + Q_INVOKABLE void changeShortcut(const QString &name, const QString &newShortcut); + Q_INVOKABLE QString keycodeToChar(int keycode) const; +private: void registerShortcut(EditableShortcut *action); static ShortcutRegistry *s_instance;