diff --git a/frontend/packages/mousetrap/index.js b/frontend/packages/mousetrap/index.js index 4d0576e417..4558a758b0 100644 --- a/frontend/packages/mousetrap/index.js +++ b/frontend/packages/mousetrap/index.js @@ -178,6 +178,16 @@ function _addEvent(object, type, callback) { object.attachEvent("on" + type, callback); } +/** + * true for the letter (A-Z) and main-row digit (0-9) keycodes + * + * @param {number} which + * @return {boolean} + */ +function _isLetterOrDigit(which) { + return (which >= 48 && which <= 57) || (which >= 65 && which <= 90); +} + /** * takes the event and returns the key character * @@ -222,6 +232,12 @@ function _characterFromEvent(e) { return _KEYCODE_MAP[e.which]; } + // keydown bindings name the key, not the glyph it types under modifiers + // ("fi" for Option+Shift+5, ")" for Shift+0) + if (_isLetterOrDigit(e.which)) { + return String.fromCharCode(e.which).toLowerCase(); + } + // if it is not in the special map if (typeof e.key === "string") { return e.key.toLowerCase(); diff --git a/frontend/src/app/main/ui/shortcuts.cljs b/frontend/src/app/main/ui/shortcuts.cljs index 40a8f53aec..74035a119f 100644 --- a/frontend/src/app/main/ui/shortcuts.cljs +++ b/frontend/src/app/main/ui/shortcuts.cljs @@ -53,8 +53,9 @@ ;; aren't in that map. Recording punctuation from `code` therefore matches ;; what Mousetrap triggers regardless of layout (e.g. the key next to Right ;; Shift on a US layout is always "Period", whatever glyph it types on a -;; given ISO layout). Letters/digits must stay glyph-based to match -;; Mousetrap's own resolution for them. +;; given ISO layout). With modifiers, letters and digits come from keyCode +;; like Mousetrap's keydown match (see `letter-or-digit`); otherwise from the +;; glyph, like its keypress match. (def ^:private code-name-map {"Minus" "-" "Equal" "=" @@ -76,12 +77,24 @@ (or (.getBrowserEvent event) event) event)) +(defn- letter-or-digit + "Letter or digit named by keyCode when a modifier is held, ignoring the glyph + it makes the key type (Option+Shift+5 types \"fi\" on macOS)." + [^js event] + (let [key-code (.-keyCode event)] + (when (and (or (.-ctrlKey event) (.-metaKey event) + (.-altKey event) (.-shiftKey event)) + (or (<= 48 key-code 57) + (<= 65 key-code 90))) + (.toLowerCase (js/String.fromCharCode key-code))))) + (defn- normalize-key [^js event] (let [key (.-key event)] (when (and key (not (contains? modifier-keys key))) (or (get key-name-map key) (get code-name-map (.-code (event->native event))) + (letter-or-digit event) (some-> key .toLowerCase))))) (defn- event->modifier-parts diff --git a/frontend/test/frontend_tests/runner.cljs b/frontend/test/frontend_tests/runner.cljs index bbde1e17fd..8ab9b7ac6b 100644 --- a/frontend/test/frontend_tests/runner.cljs +++ b/frontend/test/frontend_tests/runner.cljs @@ -121,6 +121,7 @@ [frontend-tests.ui.settings-shortcuts-test] [frontend-tests.ui.shape-filters-test] [frontend-tests.ui.shortcuts-labels-test] + [frontend-tests.ui.shortcuts-recording-test] [frontend-tests.ui.sidebar-scroll-test] [frontend-tests.ui.stroke-menu-test] [frontend-tests.ui.workspace-libraries-test] @@ -265,6 +266,7 @@ 'frontend-tests.ui.settings-shortcuts-test 'frontend-tests.ui.shape-filters-test 'frontend-tests.ui.shortcuts-labels-test + 'frontend-tests.ui.shortcuts-recording-test 'frontend-tests.ui.sidebar-scroll-test 'frontend-tests.ui.stroke-menu-test 'frontend-tests.ui.workspace-libraries-test diff --git a/frontend/test/frontend_tests/ui/shortcuts_recording_test.cljs b/frontend/test/frontend_tests/ui/shortcuts_recording_test.cljs new file mode 100644 index 0000000000..9bb4479fbe --- /dev/null +++ b/frontend/test/frontend_tests/ui/shortcuts_recording_test.cljs @@ -0,0 +1,60 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) KALEIDOS SUBSIDIARY SL + +(ns frontend-tests.ui.shortcuts-recording-test + (:require + [app.main.ui.shortcuts :as ss] + [cljs.test :as t :include-macros true])) + +(def ^:private keyboard-event->mousetrap + @#'ss/keyboard-event->mousetrap) + +(defn- key-event + [props] + (clj->js (merge {:altKey false :shiftKey false :ctrlKey false :metaKey false} + props))) + +(t/deftest records-alt-digit-from-key-code + ;; macOS Option+Shift+5 types "fi"; Mousetrap matches it as "5" + (t/is (= "alt+shift+5" + (keyboard-event->mousetrap + (key-event {:key "fi" :code "Digit5" :keyCode 53 :altKey true :shiftKey true}))))) + +(t/deftest records-alt-letter-from-key-code + ;; macOS Option+A types "å"; Mousetrap matches it as "a" + (t/is (= "alt+a" + (keyboard-event->mousetrap + (key-event {:key "å" :code "KeyA" :keyCode 65 :altKey true}))))) + +(t/deftest records-shift-digit-from-key-code + ;; US Shift+0 types ")"; Mousetrap matches it as "0" + (t/is (= "shift+0" + (keyboard-event->mousetrap + (key-event {:key ")" :code "Digit0" :keyCode 48 :shiftKey true}))))) + +(t/deftest records-modified-letter-from-key-code + ;; Russian layout V types "м"; Mousetrap matches Alt+V as "v" + (t/is (= "alt+v" + (keyboard-event->mousetrap + (key-event {:key "м" :code "KeyV" :keyCode 86 :altKey true}))))) + +(t/deftest records-unmodified-key-from-glyph + ;; without modifiers Mousetrap matches the typed glyph on keypress + (t/is (= "&" + (keyboard-event->mousetrap + (key-event {:key "&" :code "Digit1" :keyCode 49})))) + (t/is (= "ñ" + (keyboard-event->mousetrap + (key-event {:key "ñ" :code "" :keyCode 0}))))) + +(t/deftest records-shift-punctuation-from-code + ;; Shift+. types ":" and Shift+= types "+" (US); the key is recorded + (t/is (= "shift+." + (keyboard-event->mousetrap + (key-event {:key ":" :code "Period" :keyCode 190 :shiftKey true})))) + (t/is (= "shift+=" + (keyboard-event->mousetrap + (key-event {:key "+" :code "Equal" :keyCode 187 :shiftKey true})))))