From 8de26c11a10462e2e56efb8ac5f9af1b577beeb7 Mon Sep 17 00:00:00 2001 From: miruka Date: Thu, 22 Aug 2019 13:03:26 -0400 Subject: [PATCH] HShortcutHandler & HShortcut components Provide more powerful shortcuts handling than what's available with QML's Shortcut component, notably being able to react differently to key presses, releases and auto-repeats. --- src/python/config_files.py | 2 +- src/qml/Base/HScrollableTextArea.qml | 2 + src/qml/Base/HShortcut.qml | 10 ++++ src/qml/Base/HShortcutHandler.qml | 56 ++++++++++++++++++ src/qml/Base/HTextField.qml | 1 + src/qml/Shortcuts.qml | 88 ++++++++++------------------ src/qml/UI.qml | 4 ++ src/qml/Window.qml | 3 +- src/qml/utils.js | 12 ++++ 9 files changed, 118 insertions(+), 60 deletions(-) create mode 100644 src/qml/Base/HShortcut.qml create mode 100644 src/qml/Base/HShortcutHandler.qml diff --git a/src/python/config_files.py b/src/python/config_files.py index d4f5fecb..4e40296c 100644 --- a/src/python/config_files.py +++ b/src/python/config_files.py @@ -115,7 +115,7 @@ class UISettings(JSONConfigFile): "theme": "Default.qpl", "writeAliases": {}, "keys": { - "reloadConfig": ["Alt+Shift+R"], + "reloadConfig": "Alt+Shift+R", "scrollUp": ["Alt+Up", "Alt+K"], "scrollDown": ["Alt+Down", "Alt+J"], "focusSidePane": ["Alt+S", "Ctrl+S"], diff --git a/src/qml/Base/HScrollableTextArea.qml b/src/qml/Base/HScrollableTextArea.qml index 3fde8d64..47ed1bf7 100644 --- a/src/qml/Base/HScrollableTextArea.qml +++ b/src/qml/Base/HScrollableTextArea.qml @@ -26,5 +26,7 @@ ScrollView { id: textAreaBackground color: theme.controls.textArea.background } + + Keys.forwardTo: mainUI.shortcuts } } diff --git a/src/qml/Base/HShortcut.qml b/src/qml/Base/HShortcut.qml new file mode 100644 index 00000000..f65705d2 --- /dev/null +++ b/src/qml/Base/HShortcut.qml @@ -0,0 +1,10 @@ +import QtQuick 2.12 + +QtObject { + signal pressed(var event) + signal held(var event) + signal released(var event) + + property bool enabled: true + property var sequences: "" // string or array of strings +} diff --git a/src/qml/Base/HShortcutHandler.qml b/src/qml/Base/HShortcutHandler.qml new file mode 100644 index 00000000..f285d644 --- /dev/null +++ b/src/qml/Base/HShortcutHandler.qml @@ -0,0 +1,56 @@ +import QtQuick 2.12 + +Item { + id: shortcutHandler + + Keys.onPressed: { + let shortcut = match(event) + if (! shortcut) return + event.isAutoRepeat ? shortcut.held(event) : shortcut.pressed(event) + } + Keys.onReleased: { + let shortcut = match(event) + if (shortcut && ! event.isAutoRepeat) shortcut.released(event) + } + + + readonly property var modifierDict: ({ + Ctrl: Qt.ControlModifier, + Shift: Qt.ShiftModifier, + Alt: Qt.AltModifier, + Meta: Qt.MetaModifier, + }) + + + function sequenceMatches(event, sequence) { + let [key, ...mods] = sequence.split("+").reverse() + + key = key.charAt(0).toUpperCase() + key.slice(1) + + if (event.key !== Qt["Key_" + key]) return false + + for (let [name, code] of Object.entries(modifierDict)) { + if (mods.includes(name) && ! (event.modifiers & code)) return false + if (! mods.includes(name) && event.modifiers & code) return false + } + + return true + } + + function match(event) { + for (let i = 0; i < shortcutHandler.resources.length; i++) { + let shortcut = shortcutHandler.resources[i] + + if (! shortcut.enabled) continue + + if (typeof(shortcut.sequences) == "string" && + sequenceMatches(event, shortcut.sequences)) return shortcut + + for (let i = 0; i < shortcut.sequences.length; i++) { + if (sequenceMatches(event, shortcut.sequences[i])) + return shortcut + } + } + return null + } +} diff --git a/src/qml/Base/HTextField.qml b/src/qml/Base/HTextField.qml index 687ff039..dddeab10 100644 --- a/src/qml/Base/HTextField.qml +++ b/src/qml/Base/HTextField.qml @@ -28,4 +28,5 @@ TextField { } selectByMouse: true + Keys.forwardTo: mainUI.shortcuts } diff --git a/src/qml/Shortcuts.qml b/src/qml/Shortcuts.qml index 19f688d0..5908f266 100644 --- a/src/qml/Shortcuts.qml +++ b/src/qml/Shortcuts.qml @@ -1,80 +1,54 @@ import QtQuick 2.12 +import "Base" +import "utils.js" as Utils -Item { +HShortcutHandler { property Item flickTarget: Item {} - function smartVerticalFlick(baseVelocity, fastMultiply=3) { - if (! flickTarget.interactive) { return } - - baseVelocity = -baseVelocity - let vel = -flickTarget.verticalVelocity - let fast = (baseVelocity < 0 && vel < baseVelocity / 2) || - (baseVelocity > 0 && vel > baseVelocity / 2) - - flickTarget.flick(0, baseVelocity * (fast ? fastMultiply : 1)) + HShortcut { + enabled: debugMode + sequences: settings.keys.startDebugger + onPressed: py.call("APP.pdb") } - - Shortcut { - sequences: settings.keys ? settings.keys.startDebugger : [] - onActivated: if (debugMode) { py.call("APP.pdb") } + HShortcut { + sequences: settings.keys.reloadConfig + onPressed: py.loadSettings(() => { mainUI.pressAnimation.start() }) } - Shortcut { - sequences: settings.keys ? settings.keys.reloadConfig : [] - onActivated: py.loadSettings(() => { mainUI.pressAnimation.start() }) + HShortcut { + sequences: settings.keys.scrollUp + onPressed: Utils.smartVerticalFlick(flickTarget, -335) } - Shortcut { - sequences: settings.keys ? settings.keys.scrollUp : [] - onActivated: smartVerticalFlick(-335) + HShortcut { + sequences: settings.keys.scrollDown + onPressed: Utils.smartVerticalFlick(flickTarget, 335) } - Shortcut { - sequences: settings.keys ? settings.keys.scrollDown : [] - onActivated: smartVerticalFlick(335) + HShortcut { + sequences: settings.keys.focusSidePane + onPressed: mainUI.sidePane.setFocus() } - Shortcut { - sequences: settings.keys ? settings.keys.focusSidePane : [] - onActivated: mainUI.sidePane.setFocus() + HShortcut { + sequences: settings.keys.clearRoomFilter + onPressed: mainUI.sidePane.paneToolBar.roomFilter = "" } - Shortcut { - sequences: settings.keys ? settings.keys.clearRoomFilter : [] - onActivated: mainUI.sidePane.paneToolBar.roomFilter = "" + HShortcut { + sequences: settings.keys.goToPreviousRoom + onPressed: mainUI.sidePane.accountRoomList.previous() } - Shortcut { - sequences: settings.keys ? settings.keys.goToPreviousRoom : [] - onActivated: mainUI.sidePane.accountRoomList.previous() + HShortcut { + sequences: settings.keys.goToNextRoom + onPressed: mainUI.sidePane.accountRoomList.next() } - Shortcut { - sequences: settings.keys ? settings.keys.goToNextRoom : [] - onActivated: mainUI.sidePane.accountRoomList.next() + HShortcut { + sequences: settings.keys.toggleCollapseAccount + onPressed: mainUI.sidePane.accountRoomList.toggleCollapseAccount() } - - Shortcut { - sequences: settings.keys ? settings.keys.toggleCollapseAccount : [] - onActivated: mainUI.sidePane.accountRoomList.toggleCollapseAccount() - } - - /* - Shortcut { - sequence: "Ctrl+-" - onActivated: theme.fontScale = Math.max(0.1, theme.fontScale - 0.1) - } - - Shortcut { - sequence: "Ctrl++" - onActivated: theme.fontScale = Math.min(10, theme.fontScale + 0.1) - } - - Shortcut { - sequence: "Ctrl+=" - onActivated: theme.fontScale = 1.0 - } - */ } diff --git a/src/qml/UI.qml b/src/qml/UI.qml index a0243808..75b83b4f 100644 --- a/src/qml/UI.qml +++ b/src/qml/UI.qml @@ -10,6 +10,7 @@ HRectangle { color: theme.ui.background Component.onCompleted: window.mainUI = mainUI + property alias shortcuts: shortcuts property alias sidePane: sidePane property alias pageLoader: pageLoader property alias pressAnimation: _pressAnimation @@ -28,6 +29,9 @@ HRectangle { (modelSources["Account"] || []).length > 0 || py.startupAnyAccountsSaved + + Shortcuts { id: shortcuts } + HImage { id: mainUIBackground visible: Boolean(Qt.resolvedUrl(source)) diff --git a/src/qml/Window.qml b/src/qml/Window.qml index e3ff75de..9cba4c24 100644 --- a/src/qml/Window.qml +++ b/src/qml/Window.qml @@ -27,8 +27,7 @@ ApplicationWindow { property var theme: null - Shortcuts { id: shortcuts} - Python { id: py } + Python { id: py } HLoader { anchors.fill: parent diff --git a/src/qml/utils.js b/src/qml/utils.js index 34b1724b..c5c85b20 100644 --- a/src/qml/utils.js +++ b/src/qml/utils.js @@ -162,3 +162,15 @@ function getItem(array, mainKey, value) { } return undefined } + + +function smartVerticalFlick(flickTarget, baseVelocity, fastMultiply=3) { + if (! flickTarget.interactive) { return } + + baseVelocity = -baseVelocity + let vel = -flickTarget.verticalVelocity + let fast = (baseVelocity < 0 && vel < baseVelocity / 2) || + (baseVelocity > 0 && vel > baseVelocity / 2) + + flickTarget.flick(0, baseVelocity * (fast ? fastMultiply : 1)) +}