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

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

View file

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

View file

@ -11,6 +11,7 @@
#include <QSharedPointer>
#include <QSystemTrayIcon>
#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;

View file

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

View file

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

View file

@ -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);
}
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;
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<QKeySequence> 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<int, QByteArray> ShortcutRegistry::roleNames() const
QHash<int, QByteArray>
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();
}

View file

@ -1,7 +1,11 @@
// SPDX-FileCopyrightText: Nheko Contributors
//
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QAbstractListModel>
#include <QAction>
#include <QKeySequence>
#include <QQmlEngine>
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<int, QByteArray> 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;
Q_INVOKABLE void changeShortcut(const QString &name, const QString &newShortcut);
Q_INVOKABLE QString keycodeToChar(int keycode) const;
private:
explicit ShortcutRegistry(QObject *parent = nullptr);
void registerShortcut(EditableShortcut *action);
static ShortcutRegistry *s_instance;