Add UI to allow editing shortcuts dynamically

This commit is contained in:
Loren Burkholder 2023-09-26 10:19:32 -04:00
parent 66ade755eb
commit 72410c499d
9 changed files with 191 additions and 49 deletions

View file

@ -779,6 +779,7 @@ set(QML_SOURCES
resources/qml/dialogs/RoomMembers.qml resources/qml/dialogs/RoomMembers.qml
resources/qml/dialogs/AllowedRoomsSettingsDialog.qml resources/qml/dialogs/AllowedRoomsSettingsDialog.qml
resources/qml/dialogs/RoomSettings.qml resources/qml/dialogs/RoomSettings.qml
resources/qml/dialogs/ShortcutEditor.qml
resources/qml/dialogs/UserProfile.qml resources/qml/dialogs/UserProfile.qml
resources/qml/emoji/StickerPicker.qml resources/qml/emoji/StickerPicker.qml
resources/qml/pages/LoginPage.qml resources/qml/pages/LoginPage.qml

View file

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

View file

@ -5,6 +5,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import ".." import ".."
import "../ui" import "../ui"
import "../dialogs"
import Qt.labs.platform 1.1 as Platform import Qt.labs.platform 1.1 as Platform
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 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 { DelegateChoice {
Text { Text {
text: model.value text: model.value

View file

@ -57,6 +57,7 @@ MainWindow *MainWindow::instance_ = nullptr;
MainWindow::MainWindow(QWindow *parent) MainWindow::MainWindow(QWindow *parent)
: QQuickView(parent) : QQuickView(parent)
, userSettings_{UserSettings::instance()} , userSettings_{UserSettings::instance()}
, shortcuts_{new ShortcutRegistry}
{ {
instance_ = this; instance_ = this;

View file

@ -11,6 +11,7 @@
#include <QSharedPointer> #include <QSharedPointer>
#include <QSystemTrayIcon> #include <QSystemTrayIcon>
#include "ShortcutRegistry.h"
#include "UserSettingsPage.h" #include "UserSettingsPage.h"
#include "dock/Dock.h" #include "dock/Dock.h"
@ -140,6 +141,7 @@ private:
//! Tray icon that shows the unread message count. //! Tray icon that shows the unread message count.
TrayIcon *trayIcon_; TrayIcon *trayIcon_;
Dock *dock_; Dock *dock_;
ShortcutRegistry *shortcuts_;
MxcImageProvider *imgProvider = nullptr; MxcImageProvider *imgProvider = nullptr;

View file

@ -1145,6 +1145,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
return tr("Periodically update community routing information"); return tr("Periodically update community routing information");
case ExpireEvents: case ExpireEvents:
return tr("Periodically delete expired events"); return tr("Periodically delete expired events");
case KeyboardShortcuts:
return tr("Configure keyboard shortcuts");
} }
} else if (role == Value) { } else if (role == Value) {
switch (index.row()) { switch (index.row()) {
@ -1444,6 +1446,7 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case LoginInfoSection: case LoginInfoSection:
case SessionKeys: case SessionKeys:
case CrossSigningSecrets: case CrossSigningSecrets:
case KeyboardShortcuts:
return {}; return {};
case OnlineBackupKey: case OnlineBackupKey:
return tr( return tr(
@ -1562,6 +1565,8 @@ UserSettingsModel::data(const QModelIndex &index, int role) const
case UserSigningKey: case UserSigningKey:
case MasterKey: case MasterKey:
return KeyStatus; return KeyStatus;
case KeyboardShortcuts:
return ConfigureKeyboardShortcuts;
} }
} else if (role == ValueLowerBound) { } else if (role == ValueLowerBound) {
switch (index.row()) { switch (index.row()) {

View file

@ -475,6 +475,7 @@ class UserSettingsModel : public QAbstractListModel
#endif #endif
UpdateSpaceVias, UpdateSpaceVias,
ExpireEvents, ExpireEvents,
KeyboardShortcuts,
AccessibilitySection, AccessibilitySection,
ReducedMotion, ReducedMotion,
@ -562,6 +563,7 @@ public:
KeyStatus, KeyStatus,
SessionKeyImportExport, SessionKeyImportExport,
XSignKeysRequestDownload, XSignKeysRequestDownload,
ConfigureKeyboardShortcuts,
}; };
Q_ENUM(Types); Q_ENUM(Types);

View file

@ -1,3 +1,7 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#include "ShortcutRegistry.h" #include "ShortcutRegistry.h"
ShortcutRegistry *ShortcutRegistry::s_instance = nullptr; ShortcutRegistry *ShortcutRegistry::s_instance = nullptr;
@ -5,9 +9,19 @@ ShortcutRegistry *ShortcutRegistry::s_instance = nullptr;
EditableShortcut::EditableShortcut(QObject *parent) EditableShortcut::EditableShortcut(QObject *parent)
: QObject{parent} : QObject{parent}
{ {
ShortcutRegistry::instance()->registerShortcut(this);
} }
const QStringList EditableShortcut::shortcuts() const 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
{ {
QStringList dest; QStringList dest;
dest.resize(m_shortcuts.size()); dest.resize(m_shortcuts.size());
@ -17,7 +31,8 @@ const QStringList EditableShortcut::shortcuts() const
return dest; return dest;
} }
void EditableShortcut::setName(const QString &name) void
EditableShortcut::setName(const QString &name)
{ {
if (name == m_name) if (name == m_name)
return; return;
@ -25,7 +40,8 @@ void EditableShortcut::setName(const QString &name)
emit nameChanged(); emit nameChanged();
} }
void EditableShortcut::setDescription(const QString &description) void
EditableShortcut::setDescription(const QString &description)
{ {
if (description == m_description) if (description == m_description)
return; return;
@ -33,12 +49,14 @@ void EditableShortcut::setDescription(const QString &description)
emit descriptionChanged(); emit descriptionChanged();
} }
void EditableShortcut::setShortcut(const QString &shortcut) void
EditableShortcut::setShortcut(const QString &shortcut)
{ {
setShortcuts({shortcut}); setShortcuts({shortcut});
} }
void EditableShortcut::setShortcuts(const QStringList &shortcuts) void
EditableShortcut::setShortcuts(const QStringList &shortcuts)
{ {
QList<QKeySequence> temp; QList<QKeySequence> temp;
temp.resize(shortcuts.size()); temp.resize(shortcuts.size());
@ -52,12 +70,13 @@ void EditableShortcut::setShortcuts(const QStringList &shortcuts)
emit shortcutsChanged(); emit shortcutsChanged();
} }
EditableShortcut::EditableShortcut(const QString &name, const QString &description, QObject *parent) ShortcutRegistry::ShortcutRegistry(QObject *parent)
: QObject{parent} : QAbstractListModel{parent}
, m_name{name}
, m_description{description}
{ {
ShortcutRegistry::instance()->registerShortcut(this); if (s_instance)
m_shortcuts = s_instance->m_shortcuts;
s_instance = this;
} }
ShortcutRegistry * ShortcutRegistry *
@ -66,7 +85,8 @@ ShortcutRegistry::instance()
return s_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. // The instance has to exist before it is used. We cannot replace it.
Q_ASSERT(s_instance); Q_ASSERT(s_instance);
@ -85,20 +105,20 @@ ShortcutRegistry *ShortcutRegistry::create(QQmlEngine *qmlEngine, QJSEngine *)
return s_instance; return s_instance;
} }
QHash<int, QByteArray> ShortcutRegistry::roleNames() const QHash<int, QByteArray>
ShortcutRegistry::roleNames() const
{ {
return {{Roles::Name, "name"}, return {
{Roles::Description, "description"}, {Roles::Name, "name"}, {Roles::Description, "description"}, {Roles::Shortcut, "shortcut"}};
{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) if (!index.isValid() || index.row() >= m_shortcuts.size() || index.row() < 0)
return {}; return {};
switch (role) switch (role) {
{
case Roles::Name: case Roles::Name:
return m_shortcuts[index.row()]->name(); return m_shortcuts[index.row()]->name();
case Roles::Description: 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) for (int i = 0; i < m_shortcuts.size(); ++i) {
return false; if (m_shortcuts[i]->name() == name) {
qDebug() << "new:" << newShortcut;
switch (role) m_shortcuts[i]->setShortcut(newShortcut);
{ emit dataChanged(index(i), index(i), {Roles::Shortcut});
case Roles::Shortcut: return;
if (auto shortcut = QKeySequence(value.toString()); !shortcut.isEmpty()) { }
m_shortcuts[index.row()]->setShortcut(shortcut.toString());
return true;
} else
return false;
default:
return false;
} }
} }
ShortcutRegistry::ShortcutRegistry(QObject *parent) QString
: QAbstractListModel{parent} 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); m_shortcuts.push_back(action);
endInsertRows();
} }

View file

@ -1,7 +1,11 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once #pragma once
#include <QAbstractListModel> #include <QAbstractListModel>
#include <QAction> #include <QKeySequence>
#include <QQmlEngine> #include <QQmlEngine>
class EditableShortcut : public QObject class EditableShortcut : public QObject
@ -10,15 +14,15 @@ class EditableShortcut : public QObject
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged FINAL) 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(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: public:
EditableShortcut(QObject *parent = nullptr); EditableShortcut(QObject *parent = nullptr);
EditableShortcut(const QString &name, const QString &description, 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 &name() const { return m_name; }
const QString &description() const { return m_description; } const QString &description() const { return m_description; }
@ -58,20 +62,19 @@ public:
Shortcut, Shortcut,
}; };
explicit ShortcutRegistry(QObject *parent = nullptr);
static ShortcutRegistry *instance(); static ShortcutRegistry *instance();
static ShortcutRegistry *create(QQmlEngine *qmlEngine, QJSEngine *); static ShortcutRegistry *create(QQmlEngine *qmlEngine, QJSEngine *);
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex & = QModelIndex()) const override int rowCount(const QModelIndex & = QModelIndex()) const override { return m_shortcuts.size(); }
{
return m_shortcuts.size();
}
QVariant data(const QModelIndex &index, int role) const override; QVariant data(const QModelIndex &index, int role) const override;
bool setData(const QModelIndex &index, const QVariant &value, int role) override;
Q_INVOKABLE void changeShortcut(const QString &name, const QString &newShortcut);
Q_INVOKABLE QString keycodeToChar(int keycode) const;
private: private:
explicit ShortcutRegistry(QObject *parent = nullptr);
void registerShortcut(EditableShortcut *action); void registerShortcut(EditableShortcut *action);
static ShortcutRegistry *s_instance; static ShortcutRegistry *s_instance;