diff --git a/src/main/frontend/components/shortcut.cljs b/src/main/frontend/components/shortcut.cljs index e51d0e8410..cc56ee4f2a 100644 --- a/src/main/frontend/components/shortcut.cljs +++ b/src/main/frontend/components/shortcut.cljs @@ -52,11 +52,14 @@ ;; goog.events.listen in bubble phase). On the same element, ;; bubble-phase listeners fire in registration order, so KeyHandler ;; processes the key first, then this blocker calls stopPropagation - ;; to prevent the event from reaching js/window. preventDefault - ;; suppresses browser-native actions (Cmd+F, Cmd+S, etc.). + ;; to prevent the event from reaching js/window. preventDefault + ;; is intentionally omitted here — the recording key-handler-fn + ;; already calls (.preventDefault e) via the KeyHandler KEY event, + ;; and calling it again on the raw keydown would suppress the + ;; subsequent keypress that goog.events.KeyHandler may need to + ;; resolve character keys. bubble-blocker (fn [^js e] - (.stopPropagation e) - (.preventDefault e)) + (.stopPropagation e)) _ (.addEventListener el "keydown" bubble-blocker false) _ (.addEventListener el "keypress" bubble-blocker false) _ (.addEventListener el "keyup" bubble-blocker false) @@ -420,6 +423,7 @@ :when m] (dh/get-shortcut-desc m)) (distinct) + (map #(str "\u201c" % "\u201d")) (string/join ", "))) (rum/defc ^:large-vars/cleanup-todo customize-shortcut-dialog-inner @@ -727,7 +731,7 @@ :conflict-cross [:div.shortcut-feedback.shortcut-feedback--error [:span (t :keymap/used-by) - [:span.shortcut-feedback-name (str "\u201c" (conflict-action-names key-conflicts) "\u201d")]] + [:span.shortcut-feedback-name (conflict-action-names key-conflicts)]] (ui/tooltip (shui/button {:variant :destructive :size :xs @@ -745,14 +749,14 @@ [:div.shortcut-feedback.shortcut-feedback--warning [:span (t :keymap/also-used-for) [:span.shortcut-feedback-name - (str "\u201c" (:cross-action-name accepted-info) "\u201d")] + (:cross-action-name accepted-info)] (when-let [ctx (:cross-context-label accepted-info)] (str (t :keymap/in-context) ctx))]] (:from accepted-info) [:div.shortcut-feedback.shortcut-feedback--success [:span (t :keymap/reassigned-from) - [:span.shortcut-feedback-name (str "\u201c" (:from accepted-info) "\u201d")]] + [:span.shortcut-feedback-name (:from accepted-info)]] undo-link] :else diff --git a/src/main/frontend/modules/shortcut/data_helper.cljs b/src/main/frontend/modules/shortcut/data_helper.cljs index 9194d450ee..d3cced6556 100644 --- a/src/main/frontend/modules/shortcut/data_helper.cljs +++ b/src/main/frontend/modules/shortcut/data_helper.cljs @@ -181,29 +181,38 @@ #{from-handler-id :shortcut.handler/global-prevent-default} #{from-handler-id})) +(defn- handlers-co-active? + "Two handler groups conflict (can be active simultaneously) unless one is + editing-only and the other is non-editing-only — those are mutually exclusive + at runtime." + [h1 h2] + (let [editing-only #{:shortcut.handler/editor-global + :shortcut.handler/block-editing-only} + non-editing-only #{:shortcut.handler/global-non-editing-only}] + (not (or (and (contains? editing-only h1) (contains? non-editing-only h2)) + (and (contains? non-editing-only h1) (contains? editing-only h2)))))) + (defn get-conflicts-by-keys ([ks] (get-conflicts-by-keys ks :shortcut.handler/global-prevent-default {:group-global? true})) ([ks handler-id] (get-conflicts-by-keys ks handler-id {:group-global? true})) ([ks handler-id {:keys [exclude-ids group-global?]}] (let [global-handlers #{:shortcut.handler/editor-global :shortcut.handler/global-non-editing-only - :shortcut.handler/global-prevent-default - :shortcut.handler/misc} + :shortcut.handler/global-prevent-default} ks-bindings (get-bindings-keys-map) handler-ids (should-be-included-to-global-handler handler-id) global? (when group-global? (seq (set/intersection global-handlers handler-ids)))] (->> (if (string? ks) [ks] ks) (map (fn [k] (when-let [k' (shortcut-utils/undecorate-binding k)] - (let [k (shortcut-utils/safe-parse-string-binding k') - k (bean/->clj k) + (let [input-binding (bean/->clj (shortcut-utils/safe-parse-string-binding k')) same-leading-key? (fn [[k' _]] - (when (sequential? k) - (or (= k k') - (and (> (count k') (count k)) - (= (first k) (first k')))))) + (when (sequential? input-binding) + (or (= input-binding k') + (and (> (count k') (count input-binding)) + (= (first input-binding) (first k')))))) into-conflict-refs (fn [[k o]] @@ -213,7 +222,14 @@ (not (contains? exclude-ids id)) (or (= handler-ids #{handler-id'}) (and (set? handler-ids) (contains? handler-ids handler-id')) - (and global? (contains? global-handlers handler-id')))) + (and global? + (contains? global-handlers handler-id') + (every? #(handlers-co-active? % handler-id') handler-ids) + ;; For cross-handler conflicts, only exact key + ;; matches are blocking. Chord prefix matches + ;; (e.g., mod+c vs mod+c mod+s) live on separate + ;; handler instances and don't conflict at runtime. + (= input-binding k)))) (assoc r id handler-id') r)) {} refs)]]))] @@ -244,8 +260,7 @@ [ks handler-id {:keys [exclude-ids]}] (let [global-handlers #{:shortcut.handler/editor-global :shortcut.handler/global-non-editing-only - :shortcut.handler/global-prevent-default - :shortcut.handler/misc} + :shortcut.handler/global-prevent-default} ks-bindings (get-bindings-keys-map) caller-handlers (should-be-included-to-global-handler handler-id) caller-is-global? (seq (set/intersection global-handlers caller-handlers))]