This commit is contained in:
alonso.torres 2026-05-25 23:59:20 +02:00 committed by Eva Marco
parent 392caa4ec1
commit fac7e457bd
14 changed files with 702 additions and 67 deletions

View File

@ -59,7 +59,9 @@
[:release-notes-viewed {:optional true}
[::sm/text {:max 100}]]
[:notifications {:optional true} schema:props-notifications]
[:workspace-visited {:optional true} ::sm/boolean]])
[:workspace-visited {:optional true} ::sm/boolean]
[:custom-shortcuts {:optional true}
[:map-of {:gen/max 10} :keyword :string]]])
(def schema:profile
[:map {:title "Profile"}

View File

@ -121,6 +121,60 @@
[sc]
(str/split sc #"\+| "))))
(defn command->tooltip
"Converts a Mousetrap command string (e.g. \"command+shift+z\") to
a human-readable display string in the same format used by :tooltip
fields (e.g. \"⌘⇧Z\" on macOS or \"Ctrl+Shift+Z\" on Windows).
Returns nil for empty or unbound commands."
[command]
(when (and command (not= command ""))
(let [is-macos? (cf/check-platform? :macos)
parts (str/split command #"\+")
display-part (fn [p]
(case p
"ctrl" (if is-macos? mac-control "Ctrl+")
"command" (if is-macos? mac-command "Ctrl+")
"alt" (if is-macos? mac-option "Alt+")
"shift" (if is-macos? mac-shift "Shift+")
"up" up-arrow
"down" down-arrow
"left" left-arrow
"right" right-arrow
"del" (if is-macos? mac-delete "Del")
"backspace" (if is-macos? mac-delete "Backspace")
"escape" (if is-macos? mac-esc "Escape")
"enter" (if is-macos? mac-enter "Enter")
"space" "Space"
"tab" tab
(str/upper p)))]
(str/join "" (map display-part parts)))))
(defn apply-custom-overrides
[shortcuts custom-overrides]
(if (empty? custom-overrides)
shortcuts
(reduce-kv
(fn [acc sc-key new-command]
(if (and (contains? acc sc-key)
(not (:disabled (get acc sc-key))))
(-> acc
(assoc-in [sc-key :command] new-command)
(update sc-key dissoc :show-command))
acc))
shortcuts
custom-overrides)))
(defn build-command-index
[shortcuts]
(reduce-kv
(fn [acc sc-key sc-def]
(let [commands (:command sc-def)]
(if (vector? commands)
(reduce #(assoc %1 %2 sc-key) acc commands)
(assoc acc commands sc-key))))
{}
shortcuts))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Events
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -180,8 +234,10 @@
(update state :shortcuts conj* [key shortcuts]))
ptk/EffectEvent
(effect [_ _ _]
(reset! shortcuts)))))
(effect [_ state _]
(let [custom-overrides (get-in state [:profile :props :custom-shortcuts])
effective (apply-custom-overrides shortcuts custom-overrides)]
(reset! effective))))))
(defn pop-shortcuts
[key]
@ -193,5 +249,18 @@
ptk/EffectEvent
(effect [_ state _]
(let [[_key shortcuts] (last (:shortcuts state))]
(reset! shortcuts)))))
(let [[_key shortcuts] (last (:shortcuts state))
custom-overrides (get-in state [:profile :props :custom-shortcuts])
effective (apply-custom-overrides shortcuts custom-overrides)]
(reset! effective)))))
(defn rebind-shortcuts
[]
(ptk/reify ::rebind-shortcuts
ptk/EffectEvent
(effect [_ state _]
(let [[_key shortcuts] (last (:shortcuts state))
custom-overrides (get-in state [:profile :props :custom-shortcuts])
effective (apply-custom-overrides shortcuts custom-overrides)]
(when shortcuts
(reset! effective))))))

View File

@ -634,6 +634,24 @@
*assert*
(merge debug-shortcuts)))
(defn get-tooltip [shortcut]
(defn get-tooltip
"Returns the tooltip string for a shortcut, using any custom binding
from the user's profile props if one exists, falling back to the
default :tooltip field."
[shortcut]
(assert (contains? shortcuts shortcut) (str shortcut))
(get-in shortcuts [shortcut :tooltip]))
(let [custom-shortcuts (get-in @st/state [:profile :props :custom-shortcuts])
custom-command (get custom-shortcuts shortcut)]
(if (and custom-command (not= custom-command ""))
(ds/command->tooltip custom-command)
(get-in shortcuts [shortcut :tooltip]))))
(defn get-effective-tooltip
"Returns the tooltip string for a shortcut given an already-resolved
custom-shortcuts map. Use this when you already have the custom
shortcuts derefed for the current render cycle."
[shortcut custom-shortcuts]
(let [custom-command (get custom-shortcuts shortcut)]
(if (and custom-command (not= custom-command ""))
(ds/command->tooltip custom-command)
(get-in shortcuts [shortcut :tooltip]))))

View File

@ -0,0 +1,42 @@
;; 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 INC
(ns app.main.data.workspace.shortcuts.customize
(:require
[app.main.data.profile :as du]
[app.main.data.shortcuts :as ds]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defn set-custom-shortcut
[shortcut-key new-command conflicting-key]
(ptk/reify ::set-custom-shortcut
ptk/WatchEvent
(watch [_ state _]
(let [current-customs (get-in state [:profile :props :custom-shortcuts] {})
new-customs (-> current-customs
(assoc shortcut-key new-command)
(cond-> conflicting-key (assoc conflicting-key "")))]
(rx/of (du/update-profile-props {:custom-shortcuts new-customs})
(ds/rebind-shortcuts))))))
(defn reset-custom-shortcut
[shortcut-key]
(ptk/reify ::reset-custom-shortcut
ptk/WatchEvent
(watch [_ state _]
(let [current-customs (get-in state [:profile :props :custom-shortcuts] {})
new-customs (not-empty (dissoc current-customs shortcut-key))]
(rx/of (du/update-profile-props {:custom-shortcuts new-customs})
(ds/rebind-shortcuts))))))
(defn reset-all-custom-shortcuts
[]
(ptk/reify ::reset-all-custom-shortcuts
ptk/WatchEvent
(watch [_ _ _]
(rx/of (du/update-profile-props {:custom-shortcuts nil})
(ds/rebind-shortcuts)))))

View File

@ -33,6 +33,9 @@
(def profile
(l/derived (l/key :profile) st/state))
(def custom-shortcuts
(l/derived (fn [state] (get-in state [:profile :props :custom-shortcuts])) st/state))
(def current-page-id
(l/derived (l/key :current-page-id) st/state))

View File

@ -226,7 +226,7 @@
(mf/defc preferences-menu*
{::mf/private true
::mf/wrap [mf/memo]}
[{:keys [layout profile toggle-flag on-close toggle-theme toggle-render]}]
[{:keys [layout profile toggle-flag on-close toggle-theme toggle-render show-shortcuts]}]
(let [renderer (or (-> profile :props :renderer) :svg)
show-nudge-options
@ -333,7 +333,18 @@
[:span {:class (stl/css :item-name)}
(if (= renderer :wasm)
(tr "workspace.header.menu.disable-webgl")
(tr "workspace.header.menu.enable-webgl"))]])]))
(tr "workspace.header.menu.enable-webgl"))]])
[:> dropdown-menu-item* {:on-click show-shortcuts
:class (stl/css :base-menu-item :submenu-item)
:on-key-down (fn [event]
(when (kbd/enter? event)
(show-shortcuts event)))
:data-testid "change-shortcuts"
:id "file-menu-change-shortcuts"}
[:span {:class (stl/css :item-name)}
(tr "workspace.header.menu.change-shortcuts")]
[:> shortcuts* {:id :show-shortcuts}]]]))
(mf/defc view-menu*
{::mf/private true
@ -967,6 +978,19 @@
::ev/origin "workspace:menu"})
(modal/show :plugin-management {}))))
show-shortcuts
(mf/use-fn
(mf/deps layout)
(fn [event]
(dom/stop-propagation event)
(reset! show-menu* false)
(reset! selected-sub-menu* nil)
(when (contains? layout :collapse-left-sidebar)
(st/emit! (dw/toggle-layout-flag :collapse-left-sidebar)))
(st/emit!
(-> (dw/toggle-layout-flag :shortcuts)
(vary-meta assoc ::ev/origin "workspace-menu")))))
subscription (:subscription (:props profile))
subscription-type (get-subscription-type subscription)]
@ -1133,6 +1157,7 @@
:toggle-flag toggle-flag
:toggle-theme toggle-theme
:toggle-render toggle-render
:show-shortcuts show-shortcuts
:on-close close-sub-menu}]
:plugins

View File

@ -51,9 +51,12 @@
(mf/defc palette*
[{:keys [layout on-change-size]}]
(let [color-palette? (:colorpalette layout)
text-palette? (:textpalette layout)
hide-palettes? (:hide-palettes layout)
(let [color-palette? (:colorpalette layout)
text-palette? (:textpalette layout)
hide-palettes? (:hide-palettes layout)
custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
read-only? (mf/use-ctx ctx/workspace-read-only?)
container (mf/use-ref nil)
@ -179,16 +182,16 @@
[:ul {:class (dm/str size-classname " " (stl/css-case :palette-btn-list true
:hidden-bts hide-palettes?))}
[:li {:class (stl/css :palette-item)}
[:button {:title (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
:aria-label (tr "workspace.toolbar.color-palette" (sc/get-tooltip :toggle-colorpalette))
[:button {:title (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette))
:aria-label (tr "workspace.toolbar.color-palette" (get-tt :toggle-colorpalette))
:class (stl/css-case :palette-btn true
:selected color-palette?)
:on-click on-select-color-palette}
deprecated-icon/drop-icon]]
[:li {:class (stl/css :palette-item)}
[:button {:title (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
:aria-label (tr "workspace.toolbar.text-palette" (sc/get-tooltip :toggle-textpalette))
[:button {:title (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette))
:aria-label (tr "workspace.toolbar.text-palette" (get-tt :toggle-textpalette))
:class (stl/css-case :palette-btn true
:selected text-palette?)
:on-click on-select-text-palette}

View File

@ -40,8 +40,10 @@
{::mf/wrap [mf/memo]
::mf/wrap-props false}
[{:keys [zoom on-increase on-decrease on-zoom-reset on-zoom-fit on-zoom-selected]}]
(let [open* (mf/use-state false)
open? (deref open*)
(let [open* (mf/use-state false)
open? (deref open*)
custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
open-dropdown
(mf/use-fn
@ -97,14 +99,14 @@
:on-click on-zoom-fit}
(tr "workspace.header.zoom-fit-all")
[:span {:class (stl/css :shortcuts)}
(for [sc (scd/split-sc (sc/get-tooltip :fit-all))]
(for [sc (scd/split-sc (get-tt :fit-all))]
[:span {:class (stl/css :shortcut-key)
:key (str "zoom-fit-" sc)} sc])]]
[:li {:class (stl/css :zoom-option)
:on-click on-zoom-selected}
(tr "workspace.header.zoom-selected")
[:span {:class (stl/css :shortcuts)}
(for [sc (scd/split-sc (sc/get-tooltip :zoom-selected))]
(for [sc (scd/split-sc (get-tt :zoom-selected))]
[:span {:class (stl/css :shortcut-key)
:key (str "zoom-selected-" sc)} sc])]]]]]))
@ -118,6 +120,9 @@
read-only? (mf/use-ctx ctx/workspace-read-only?)
selected-drawtool (mf/deref refs/selected-drawing-tool)
custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
on-increase (mf/use-fn #(st/emit! (dw/increase-zoom nil)))
on-decrease (mf/use-fn #(st/emit! (dw/decrease-zoom nil)))
on-zoom-reset (mf/use-fn #(st/emit! dw/reset-zoom))
@ -211,8 +216,8 @@
:on-zoom-selected on-zoom-selected}]]
[:div {:class (stl/css :comments-section)}
[:button {:title (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
:aria-label (tr "workspace.toolbar.comments" (sc/get-tooltip :add-comment))
[:button {:title (tr "workspace.toolbar.comments" (get-tt :add-comment))
:aria-label (tr "workspace.toolbar.comments" (get-tt :add-comment))
:class (stl/css-case :comments-btn true
:selected (= selected-drawtool :comments))
:on-click toggle-comments
@ -239,7 +244,7 @@
deprecated-icon/share])
[:a {:class (stl/css :viewer-btn)
:title (tr "workspace.header.viewer" (sc/get-tooltip :open-viewer))
:title (tr "workspace.header.viewer" (get-tt :open-viewer))
:on-click nav-to-viewer}
deprecated-icon/play]]))

View File

@ -11,15 +11,20 @@
[app.common.data.macros :as dm]
[app.config :as cf]
[app.main.data.dashboard.shortcuts]
[app.main.data.modal :as modal]
[app.main.data.shortcuts :as ds]
[app.main.data.viewer.shortcuts]
[app.main.data.workspace :as dw]
[app.main.data.workspace.path.shortcuts]
[app.main.data.workspace.shortcuts]
[app.main.data.workspace.shortcuts.customize :as customize]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.search-bar :refer [search-bar*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i :refer [icon*]]
[app.main.ui.ds.product.panel-title :refer [panel-title*]]
[app.main.ui.workspace.sidebar.shortcuts.edit-modal]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[app.util.strings :refer [matches-search]]
@ -262,7 +267,8 @@
:command command}])])]))
(mf/defc shortcut-row*
[{:keys [elements filter-term is-match-section is-match-subsection]}]
[{:keys [elements filter-term is-match-section is-match-subsection
editable? custom-shortcuts on-edit on-reset]}]
(let [shortcut-name (keys elements)
shortcut-translations (map #(translation-keyname :sc %) shortcut-name)
match-shortcut? (some #(matches-search % @filter-term) shortcut-translations)
@ -275,13 +281,33 @@
(for [command-translate sorted-filtered]
(let [sc-by-translate (first (filter #(= (:translation (second %)) command-translate) elements))
[command comand-info] sc-by-translate
content (or (:show-command comand-info) (:command comand-info))]
[:li {:class (stl/css :shortcuts-name)
content (or (:show-command comand-info) (:command comand-info))
customized? (and editable? (contains? custom-shortcuts command))]
[:li {:class (stl/css-case :shortcuts-name true
:customized customized?)
:key command-translate}
[:span {:class (stl/css :command-name)}
command-translate]
[:> shortcuts-keys* {:content content
:command command}]]))]))
[:div {:class (stl/css :shortcut-actions)}
[:> shortcuts-keys* {:content content
:command command}]
(when editable?
[:div {:class (stl/css :edit-buttons)}
(when customized?
[:> icon-button* {:variant "ghost"
:aria-label (tr "shortcuts.reset")
:on-click (fn [e]
(dom/stop-propagation e)
(on-reset command))
:icon i/reload
:icon-size "s"}])
[:> icon-button* {:variant "ghost"
:aria-label (tr "shortcuts.edit")
:on-click (fn [e]
(dom/stop-propagation e)
(on-edit command))
:icon i/curve
:icon-size "s"}]])]]))]))
(mf/defc section-title*
[{:keys [name is-visible is-sub]}]
@ -295,7 +321,8 @@
(stl/css :section-name))} name]])
(mf/defc shortcut-subsection*
[{:keys [subsections manage-sections filter-term is-match-section open-sections]}]
[{:keys [subsections manage-sections filter-term is-match-section open-sections
editable? custom-shortcuts on-edit on-reset]}]
(let [subsections-names (keys subsections)
subsection-translations (if (= :none (first subsections-names))
(map #(translation-keyname :sc %) subsections-names)
@ -307,7 +334,11 @@
[:> shortcut-row* {:elements (:children basic-shortcuts)
:filter-term filter-term
:is-match-section is-match-section
:is-match-subsection true}])
:is-match-subsection true
:editable? editable?
:custom-shortcuts custom-shortcuts
:on-edit on-edit
:on-reset on-reset}])
[:ul {:class (stl/css :subsection-menu)}
(for [sub-translated sorted-translations]
@ -328,10 +359,15 @@
[:> shortcut-row* {:elements (:children sub-info)
:filter-term filter-term
:is-match-section is-match-section
:is-match-subsection match-subsection?}]]])))])))
:is-match-subsection match-subsection?
:editable? editable?
:custom-shortcuts custom-shortcuts
:on-edit on-edit
:on-reset on-reset}]]])))])))
(mf/defc shortcut-section*
[{:keys [section manage-sections open-sections filter-term]}]
[{:keys [section manage-sections open-sections filter-term
editable? custom-shortcuts on-edit on-reset]}]
(let [[section-key section-info] section
section-id (:id section-info)
section-translation (translation-keyname :sec section-key)
@ -359,13 +395,24 @@
:open-sections open-sections
:manage-sections manage-sections
:is-match-section match-section?
:filter-term filter-term}]]])))
:filter-term filter-term
:editable? editable?
:custom-shortcuts custom-shortcuts
:on-edit on-edit
:on-reset on-reset}]]])))
(def ^:private workspace-shortcuts-raw
(d/deep-merge app.main.data.workspace.path.shortcuts/shortcuts
app.main.data.workspace.shortcuts/shortcuts))
(mf/defc shortcuts-container*
[{:keys [class]}]
(let [workspace-shortcuts app.main.data.workspace.shortcuts/shortcuts
path-shortcuts app.main.data.workspace.path.shortcuts/shortcuts
all-workspace-shortcuts (->> (d/deep-merge path-shortcuts workspace-shortcuts)
(let [profile (mf/deref refs/profile)
custom-shortcuts (get-in profile [:props :custom-shortcuts])
workspace-shortcuts-custom (ds/apply-custom-overrides workspace-shortcuts-raw custom-shortcuts)
all-workspace-shortcuts (->> workspace-shortcuts-custom
(add-translation :sc)
(into {}))
@ -484,7 +531,30 @@
(mf/use-callback
(fn [_]
(reset! open-sections [[1]])
(reset! filter-term "")))]
(reset! filter-term "")))
on-edit-shortcut
(mf/use-callback
(mf/deps custom-shortcuts workspace-shortcuts-custom)
(fn [shortcut-key]
(let [default-command (:command (get workspace-shortcuts-raw shortcut-key))
current-command (or (get custom-shortcuts shortcut-key) default-command)]
(st/emit! (modal/show :shortcut-edit
{:shortcut-key shortcut-key
:shortcut-name (translation-keyname :sc shortcut-key)
:current-command current-command
:default-command default-command
:all-shortcuts workspace-shortcuts-custom})))))
on-reset-shortcut
(mf/use-callback
(fn [shortcut-key]
(st/emit! (customize/reset-custom-shortcut shortcut-key))))
on-reset-all
(mf/use-callback
(fn [_]
(st/emit! (customize/reset-all-custom-shortcuts))))]
[:div {:class (dm/str class " " (stl/css :shortcuts))}
[:> panel-title* {:class (stl/css :shortcuts-title)
@ -497,14 +567,26 @@
:value @filter-term
:placeholder (tr "shortcuts.title")
:icon-id i/search
:auto-focus true}]]
:auto-focus true}]
(when (seq custom-shortcuts)
[:> icon-button* {:variant "ghost"
:aria-label (tr "shortcuts.reset-all")
:on-click on-reset-all
:icon i/reload
:icon-size "s"}])]
(if match-any?
[:div {:class (stl/css :shortcuts-list)}
(for [section all-shortcuts]
[:> shortcut-section* {:key (->> section second :id first)
:section section
:manage-sections manage-sections
:open-sections open-sections
:filter-term filter-term}])]
(let [[section-key _] section
ws-editable? (contains? #{:basics :workspace} section-key)]
[:> shortcut-section* {:key (->> section second :id first)
:section section
:manage-sections manage-sections
:open-sections open-sections
:filter-term filter-term
:editable? ws-editable?
:custom-shortcuts custom-shortcuts
:on-edit on-edit-shortcut
:on-reset on-reset-shortcut}]))]
[:div {:class (stl/css :not-found)} (tr "shortcuts.not-found")])]))

View File

@ -16,7 +16,16 @@
}
.search-field {
display: flex;
align-items: center;
gap: deprecated.$s-4;
margin: deprecated.$s-16 deprecated.$s-12 deprecated.$s-4 deprecated.$s-12;
// The search bar should fill available space; the reset-all button sits at the end
& > :first-child {
flex: 1;
min-width: 0;
}
}
.shortcuts-title {
@ -91,6 +100,33 @@
margin-left: deprecated.$s-2;
color: var(--pill-foreground-color);
flex: 1;
min-width: 0;
}
.shortcut-actions {
display: flex;
align-items: center;
gap: deprecated.$s-2;
}
.edit-buttons {
display: flex;
align-items: center;
opacity: 0;
transition: opacity 0.15s ease;
}
&:hover .edit-buttons {
opacity: 1;
}
&.customized {
background-color: var(--pill-background-color-hover, var(--pill-background-color));
.edit-buttons {
opacity: 1;
}
}
.keys {

View File

@ -0,0 +1,190 @@
;; 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 INC
(ns app.main.ui.workspace.sidebar.shortcuts.edit-modal
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.config :as cf]
[app.main.data.modal :as modal]
[app.main.data.shortcuts :as ds]
[app.main.data.workspace.shortcuts.customize :as customize]
[app.main.store :as st]
[app.main.ui.ds.buttons.button :refer [button*]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.main.ui.ds.notifications.context-notification :refer [context-notification*]]
[app.util.dom :as dom]
[app.util.i18n :refer [tr]]
[cuerdas.core :as str]
[goog.events :as events]
[rumext.v2 :as mf])
(:import
goog.events.EventType))
(def ^:private modifier-keys
#{"Control" "Shift" "Alt" "Meta"})
(def ^:private key-name-map
{"ArrowUp" "up"
"ArrowDown" "down"
"ArrowLeft" "left"
"ArrowRight" "right"
"Escape" "escape"
"Enter" "enter"
"Backspace" "backspace"
"Delete" "del"
"Tab" "tab"
" " "space"})
(defn- shortcut-translation
[keyname]
(tr (str "shortcuts." (d/name keyname))))
(defn- keyboard-event->mousetrap
[^js event]
(let [parts (cond-> []
(and (.-ctrlKey event)
(not (cf/check-platform? :macos)))
(conj "ctrl")
(and (.-metaKey event)
(cf/check-platform? :macos))
(conj "command")
(.-altKey event)
(conj "alt")
(.-shiftKey event)
(conj "shift"))
key (.-key event)
key (if (contains? modifier-keys key)
nil
(or (get key-name-map key)
(.toLowerCase key)))]
(when key
(str/join "+" (conj parts key)))))
(defn- keyboard-event->display-parts
[^js event]
(let [parts (cond-> []
(and (.-ctrlKey event)
(not (cf/check-platform? :macos)))
(conj "ctrl")
(and (.-metaKey event)
(cf/check-platform? :macos))
(conj "command")
(.-altKey event)
(conj "alt")
(.-shiftKey event)
(conj "shift"))
key (.-key event)]
(if (contains? modifier-keys key)
{:modifiers parts :finalized? false}
{:modifiers parts
:final-key (or (get key-name-map key) (.toLowerCase key))
:finalized? true})))
(defn- find-conflict
[new-command all-shortcuts current-key]
(let [command-index (ds/build-command-index all-shortcuts)]
(when-let [conflicting-key (get command-index new-command)]
(when (not= conflicting-key current-key)
{:key conflicting-key
:name (shortcut-translation conflicting-key)}))))
(mf/defc shortcut-edit-dialog
{::mf/register modal/components
::mf/register-as :shortcut-edit}
[{:keys [shortcut-key shortcut-name current-command default-command all-shortcuts]}]
(let [recorded-command (mf/use-state nil)
display-parts (mf/use-state nil)
conflict (mf/use-state nil)
has-recording? (some? @recorded-command)
cancel-fn
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(st/emit! (modal/hide))))
save-fn
(mf/use-callback
(mf/deps @recorded-command @conflict shortcut-key)
(fn [event]
(dom/prevent-default event)
(when @recorded-command
(st/emit! (modal/hide)
(customize/set-custom-shortcut
shortcut-key
@recorded-command
(:key @conflict))))))]
(mf/with-effect []
(letfn [(on-keydown [^js event]
(.preventDefault event)
(.stopPropagation event)
(let [command (keyboard-event->mousetrap event)
parts (keyboard-event->display-parts event)]
(reset! display-parts parts)
(when command
(reset! recorded-command command)
(reset! conflict (find-conflict command all-shortcuts shortcut-key)))))]
(->> (events/listen js/document EventType.KEYDOWN on-keydown)
(partial events/unlistenByKey))))
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)} (tr "shortcuts.edit-modal.title")]
[:div {:class (stl/css :modal-close-btn)}
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click cancel-fn
:icon i/close}]]]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :shortcut-info)}
[:span {:class (stl/css :shortcut-label)} shortcut-name]
[:div {:class (stl/css :current-keys)}
[:span {:class (stl/css :current-label)} (tr "shortcuts.edit-modal.current")]
[:span {:class (stl/css :current-value)}
(if (vector? current-command)
(str/join " / " current-command)
current-command)]]]
[:div {:class (stl/css :recording-area)}
(if (nil? @display-parts)
[:span {:class (stl/css :recording-hint)}
(tr "shortcuts.edit-modal.press-keys")]
[:div {:class (stl/css :recorded-keys)}
(for [mod (:modifiers @display-parts)]
[:span {:class (stl/css :key) :key mod} mod])
(when (:final-key @display-parts)
[:span {:class (stl/css :key)} (:final-key @display-parts)])
(when-not (:finalized? @display-parts)
[:span {:class (stl/css :recording-ellipsis)}
(tr "shortcuts.edit-modal.recording")])])]
(when @conflict
[:> context-notification* {:level :warning
:appearance :ghost}
(tr "shortcuts.edit-modal.conflict" (:name @conflict))])]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:> button* {:variant "secondary"
:on-click cancel-fn}
(tr "shortcuts.edit-modal.cancel")]
[:> button* {:variant "primary"
:disabled (not has-recording?)
:on-click save-fn}
(tr "shortcuts.edit-modal.save")]]]]]))

View File

@ -0,0 +1,125 @@
// 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 INC
@use "refactor/common-refactor.scss" as deprecated;
.modal-overlay {
@extend %modal-overlay-base;
}
.modal-container {
@extend %modal-container-base;
display: flex;
flex-direction: column;
gap: var(--sp-xl);
}
.modal-header {
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
@include deprecated.headline-medium-typography;
color: var(--modal-title-foreground-color);
}
.modal-close-btn {
position: absolute;
top: var(--sp-m);
right: var(--sp-m);
}
.modal-content {
display: flex;
flex-direction: column;
gap: var(--sp-l);
}
.shortcut-info {
display: flex;
flex-direction: column;
gap: var(--sp-s);
}
.shortcut-label {
@include deprecated.body-large-typography;
color: var(--modal-title-foreground-color);
font-weight: bold;
}
.current-keys {
display: flex;
align-items: center;
gap: var(--sp-s);
}
.current-label {
@include deprecated.body-small-typography;
color: var(--color-foreground-secondary);
}
.current-value {
@include deprecated.body-small-typography;
color: var(--color-foreground-secondary);
text-transform: capitalize;
}
.recording-area {
@include deprecated.flex-center;
min-height: deprecated.$s-60;
padding: var(--sp-l);
border-radius: deprecated.$br-8;
border: 2px dashed var(--color-foreground-secondary);
background-color: var(--pill-background-color);
}
.recording-hint {
@include deprecated.body-large-typography;
color: var(--color-foreground-secondary);
}
.recorded-keys {
@include deprecated.flex-center;
gap: deprecated.$s-4;
}
.key {
@include deprecated.body-small-typography;
@include deprecated.flex-center;
text-transform: capitalize;
height: deprecated.$s-28;
padding: deprecated.$s-4 deprecated.$s-8;
border-radius: deprecated.$s-6;
background-color: var(--menu-shortcut-background-color);
color: var(--pill-foreground-color);
}
.recording-ellipsis {
@include deprecated.body-small-typography;
color: var(--color-foreground-secondary);
margin-left: var(--sp-s);
}
.modal-footer {
display: flex;
justify-content: flex-end;
}
.action-buttons {
@extend %modal-action-btns;
}

View File

@ -87,8 +87,10 @@
(mf/defc image-upload*
{::mf/wrap [mf/memo]}
[]
(let [ref (mf/use-ref nil)
file-id (mf/use-ctx ctx/current-file-id)
(let [ref (mf/use-ref nil)
file-id (mf/use-ctx ctx/current-file-id)
custom-shortcuts (mf/deref refs/custom-shortcuts)
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
on-click
(mf/use-fn
@ -111,8 +113,8 @@
(st/emit! (dwm/upload-media-workspace params)))))]
[:li
[:button
{:title (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image))
:aria-label (tr "workspace.toolbar.image" (sc/get-tooltip :insert-image))
{:title (tr "workspace.toolbar.image" (get-tt :insert-image))
:aria-label (tr "workspace.toolbar.image" (get-tt :insert-image))
:on-click on-click
:class (stl/css :main-toolbar-options-button)}
deprecated-icon/img
@ -142,8 +144,9 @@
(let [drawtool (mf/deref refs/selected-drawing-tool)
edition (mf/deref refs/selected-edition)
profile (mf/deref refs/profile)
props (get profile :props)
profile (mf/deref refs/profile)
props (get profile :props)
custom-shortcuts (mf/deref refs/custom-shortcuts)
read-only? (mf/use-ctx ctx/workspace-read-only?)
rulers? (mf/deref refs/rulers?)
@ -181,10 +184,12 @@
(dom/blur! (dom/get-target event))
(st/emit! (dwc/toggle-toolbar-visibility))))
get-tt #(sc/get-effective-tooltip % custom-shortcuts)
test-tooltip-board-text
(if (not (:workspace-visited props))
(tr "workspace.toolbar.frame-first-time" (sc/get-tooltip :draw-frame))
(tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame)))]
(tr "workspace.toolbar.frame-first-time" (get-tt :draw-frame))
(tr "workspace.toolbar.frame" (get-tt :draw-frame)))]
(when-not ^boolean read-only?
[:aside {:class (stl/css-case :main-toolbar true
@ -194,8 +199,8 @@
:data-testid "toolbar-options"}
[:li
[:button
{:title (tr "workspace.toolbar.move" (sc/get-tooltip :move))
:aria-label (tr "workspace.toolbar.move" (sc/get-tooltip :move))
{:title (tr "workspace.toolbar.move" (get-tt :move))
:aria-label (tr "workspace.toolbar.move" (get-tt :move))
:class (stl/css-case :main-toolbar-options-button true
:selected (and (nil? drawtool)
(not edition)))
@ -205,7 +210,7 @@
[:li
[:button
{:title test-tooltip-board-text
:aria-label (tr "workspace.toolbar.frame" (sc/get-tooltip :draw-frame))
:aria-label (tr "workspace.toolbar.frame" (get-tt :draw-frame))
:class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :frame))
:on-click select-drawtool
:data-tool "frame"
@ -213,8 +218,8 @@
deprecated-icon/board]]
[:li
[:button
{:title (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
:aria-label (tr "workspace.toolbar.rect" (sc/get-tooltip :draw-rect))
{:title (tr "workspace.toolbar.rect" (get-tt :draw-rect))
:aria-label (tr "workspace.toolbar.rect" (get-tt :draw-rect))
:class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :rect))
:on-click select-drawtool
:data-tool "rect"
@ -222,8 +227,8 @@
deprecated-icon/rectangle]]
[:li
[:button
{:title (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse))
:aria-label (tr "workspace.toolbar.ellipse" (sc/get-tooltip :draw-ellipse))
{:title (tr "workspace.toolbar.ellipse" (get-tt :draw-ellipse))
:aria-label (tr "workspace.toolbar.ellipse" (get-tt :draw-ellipse))
:class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :circle))
:on-click select-drawtool
:data-tool "circle"
@ -231,8 +236,8 @@
deprecated-icon/ellipse]]
[:li
[:button
{:title (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text))
:aria-label (tr "workspace.toolbar.text" (sc/get-tooltip :draw-text))
{:title (tr "workspace.toolbar.text" (get-tt :draw-text))
:aria-label (tr "workspace.toolbar.text" (get-tt :draw-text))
:class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :text))
:on-click select-drawtool
:data-tool "text"}
@ -242,8 +247,8 @@
[:li
[:button
{:title (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve))
:aria-label (tr "workspace.toolbar.curve" (sc/get-tooltip :draw-curve))
{:title (tr "workspace.toolbar.curve" (get-tt :draw-curve))
:aria-label (tr "workspace.toolbar.curve" (get-tt :draw-curve))
:class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :curve))
:on-click select-drawtool
:data-tool "curve"
@ -251,8 +256,8 @@
deprecated-icon/curve]]
[:li
[:button
{:title (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path))
:aria-label (tr "workspace.toolbar.path" (sc/get-tooltip :draw-path))
{:title (tr "workspace.toolbar.path" (get-tt :draw-path))
:aria-label (tr "workspace.toolbar.path" (get-tt :draw-path))
:class (stl/css-case :main-toolbar-options-button true :selected (= drawtool :path))
:on-click select-drawtool
:data-tool "path"
@ -262,8 +267,8 @@
(when (features/active-feature? @st/state "plugins/runtime")
[:li
[:button
{:title (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins))
:aria-label (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins))
{:title (tr "workspace.toolbar.plugins" (get-tt :plugins))
:aria-label (tr "workspace.toolbar.plugins" (get-tt :plugins))
:class (stl/css :main-toolbar-options-button)
:on-click #(st/emit!
(ev/event {::ev/name "open-plugins-manager"

View File

@ -5565,7 +5565,37 @@ msgstr "Set thumbnails"
msgid "shortcuts.title"
msgstr "Keyboard shortcuts"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:185
msgid "shortcuts.edit"
msgstr "Edit shortcut"
msgid "shortcuts.reset"
msgstr "Reset to default"
msgid "shortcuts.reset-all"
msgstr "Reset all shortcuts"
msgid "shortcuts.edit-modal.title"
msgstr "Edit shortcut"
msgid "shortcuts.edit-modal.current"
msgstr "Current shortcut"
msgid "shortcuts.edit-modal.press-keys"
msgstr "Press the desired key combination"
msgid "shortcuts.edit-modal.recording"
msgstr "Recording..."
msgid "shortcuts.edit-modal.conflict"
msgstr "This shortcut is already assigned to \"%s\". The old shortcut will be unbound."
msgid "shortcuts.edit-modal.save"
msgstr "Save"
msgid "shortcuts.edit-modal.cancel"
msgstr "Cancel"
#: src/app/main/ui/workspace/sidebar/shortcuts.cljs:182
msgid "shortcuts.toggle-alignment"
msgstr "Toggle dynamic alignment"