Enhance/shortcuts (#9803)

* refactor(shortcuts): simplify to build handler category map

* fix(shortcuts): redundant re-mount for the pdf shortcuts

* refactor(shortcuts): simplify names

* refactor(shortcuts): simplify user keynames

* fix(shortcuts): persist inited state for dev mode

* refactor(shortcuts): simplify handlers installation

* refactor(shortcuts): optimize shortcuts mixin

* fix: incorrect function ref

* refactor(shortcuts): shortcuts mixin

* fix(shortcuts): incorrect initialization for the pdf shortcut handler

* refactor(shortcuts): optimize binding keys map

* refactor(shortcuts): optimize shortcuts conflicts detection

* refactor(shortcuts): optimize binding ids map

* refactor(shortcuts): WIP the new keymap page

* refactor(shortcuts): WIP the new keymap related components

* feat(shortcuts): WIP fuzzy search for the shortcuts

* refactor(shortcuts): WIP the new keymap related components

* feat(shortcuts): WIP the new shorcuts record component

* feat(shortcuts): WIP the new shorcuts record component

* feat(shortcuts): WIP check shortcut conflicts component

* feat(shortcuts): WIP the new shorcuts record component

* refactor(shortcuts): WIP persist user shortcuts

* fix(shortcuts): detection for the conflicts

* feat(shortcuts): WIP detection for the conflicts

* feat(shortcuts): WIP persist user shortcuts

* refactor(shortcuts): add unit tests

* enhance(ux): search pane for the shortcuts

* feat(shortcuts): remove the existent shortcut item

* feat(shortcuts): fold/unfold categories

* feat(shortcuts): add shortcuts filters

* enhance(shortcuts): resove binding map description

* enhance(shortcuts): reactive category shortcuts

* enhance(shortcuts): register api for plugins

* feat(shortcuts): add keyboard shortcuts filters

* feat(shortcuts): impl keyboard shortcuts filters

* enhance(shortcuts): leader keys for the shortcut conflicts detection

* enhance(tests): leader keys conflicts for the shortucts

* enhance(shortcuts): parse conflicts from current binding list

* enhance(ui): polish the component of the restore shortcut action

* enhance(shortcuts): get conflicts with specific handler id

* enhance(shortcuts): polish the confilts component

* enhance(shortcuts): polish keymap conflicts component

* enhance(shortcuts): ux for handling shorcuts conflicts

* enhance(ui): polish notifications cp

* fix(shortcuts): remove reduplicate shortcuts for category

* enhance(shortcuts): polish ux for handling shorcuts conflicts

* chore(plugin): build libs core

* enhance(plugin): support shortcut command lifecycle hooks

* enhance(plugin): support shortcut command lifecycle hooks

* chore(plugin): build libs core

* enhance(shortcuts): support shortcuts saved to global config

* enhance(shortcuts): support shortcuts be saved to global config

* feat(shortcuts): support keymap manager to global settings

* enhance(shortcuts): shortcut to open keymap settings

* fix(units): tests

* fix: lints

* enhance(shortcuts): unlisten all shortcuts

* fix: lints

* fix: lints

* fix(units): tests

* fix(units): tests

* fix(units): tests

* enhance(shortcuts): unlisten/listen all shortcuts

* enhance(shortcuts): polish conflicts component

* fix(ui): modal size

* fix(ui): modal panel container

* enhance(shortcuts): i18n

* enhance(ui): layout of the shortcuts recorder component

* fix(lint): i18n

* enhance(ui): keyboard icon for the keymap settings tab

* fix(shortcuts): incorrect filters for the collaspsed shortcuts

* enhance(ui): polish details for the keymap settings

* enhance(ui): polish details for the keymap settings

* fix(shortcuts): get shortcut description error when the associated handler-id not exist

* fix(ui): the shortcut disabled label overlaps with section headers.

* refactor(shortcuts): names

* enhance(ui): filter icons
This commit is contained in:
Charlie
2023-08-29 19:33:48 +08:00
committed by GitHub
parent 99865a5eef
commit 6d6da2046c
44 changed files with 2374 additions and 1336 deletions

View File

@@ -1,7 +1,7 @@
(ns frontend.components.command-palette
(:require [frontend.handler.command-palette :as cp]
[frontend.modules.shortcut.core :as shortcut]
[frontend.modules.shortcut.data-helper :as shortcut-helper]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.context.i18n :refer [t]]
[frontend.search :as search]
[frontend.ui :as ui]
@@ -11,7 +11,7 @@
(defn translate [t {:keys [id desc]}]
(when id
(let [desc-i18n (t (shortcut-helper/decorate-namespace id))]
(let [desc-i18n (t (shortcut-utils/decorate-namespace id))]
(if (string/starts-with? desc-i18n "{Missing key")
desc
desc-i18n))))

View File

@@ -609,7 +609,7 @@
.cp__settings-inner {
aside {
@apply max-h-[70vh] overflow-auto mb-[-17px] p-3;
@apply max-h-[70vh] overflow-auto p-3;
ul {
@apply list-none p-0 m-0;
@@ -991,7 +991,7 @@ html[data-theme='dark'] {
.ui__modal[label=plugins-dashboard] {
.panel-content {
overflow-y: auto;
max-height: calc(100vh - 100px);
max-height: calc(100vh - 50px);
}
}

View File

@@ -29,7 +29,7 @@
[val {:keys [key type title default description inputAs]} update-setting!]
[:div.desc-item.as-input
{:data-key key}
{:data-key key :key key}
[:h2 [:code key] (ui/icon "caret-right") [:strong title]]
[:label.form-control

View File

@@ -23,6 +23,7 @@
[frontend.mobile.util :as mobile-util]
[frontend.modules.instrumentation.core :as instrument]
[frontend.modules.shortcut.data-helper :as shortcut-helper]
[frontend.components.shortcut2 :as shortcut2]
[frontend.spec.storage :as storage-spec]
[frontend.state :as state]
[frontend.storage :as storage]
@@ -578,13 +579,13 @@
(rum/defc user-proxy-settings
[{:keys [type protocol host port] :as agent-opts}]
(ui/button [:span.flex.items-center
[:strong.pr-1
[:span.pr-1
(case type
"system" "System Default"
"direct" "Direct"
(and protocol host port (str protocol "://" host ":" port)))]
(ui/icon "edit")]
:small? true
:class "text-sm p-1"
:on-click #(state/set-sub-modal!
(fn [_] (plugins/user-proxy-settings-panel agent-opts))
{:id :https-proxy-panel :center? true})))
@@ -1037,18 +1038,41 @@
(def DEFAULT-ACTIVE-TAB-STATE (if config/ENABLE-SETTINGS-ACCOUNT-TAB [:account :account] [:general :general]))
(rum/defc settings-effect
< rum/static
[active]
(rum/use-effect!
(fn []
(let [active (and (sequential? active) (name (first active)))
^js ds (.-dataset js/document.body)]
(if active
(set! (.-settingsTab ds) active)
(js-delete ds "settingsTab"))
#(js-delete ds "settingsTab")))
[active])
[:<>])
(rum/defcs settings
< (rum/local DEFAULT-ACTIVE-TAB-STATE ::active)
{:will-mount
(fn [state]
(state/load-app-user-cfgs)
state)
:did-mount
(fn [state]
(let [active-tab (first (:rum/args state))
*active (::active state)]
(when (keyword? active-tab)
(reset! *active [active-tab nil])))
state)
:will-unmount
(fn [state]
(state/close-settings!)
state)}
rum/reactive
[state]
[state _active-tab]
(let [current-repo (state/sub :git/current-repo)
;; enable-block-timestamps? (state/enable-block-timestamps?)
_installed-plugins (state/sub :plugin/installed-plugins)
@@ -1056,9 +1080,8 @@
*active (::active state)]
[:div#settings.cp__settings-main
(settings-effect @*active)
[:div.cp__settings-inner
[:aside.md:w-64 {:style {:min-width "10rem"}}
[:header.cp__settings-header
(ui/icon "settings")
@@ -1069,6 +1092,7 @@
[:account "account" (t :settings-page/tab-account) (ui/icon "user-circle")])
[:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")]
[:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")]
[:keymap "keymap" (t :settings-page/tab-keymap) (ui/icon "keyboard")]
(when (util/electron?)
[:version-control "git" (t :settings-page/tab-version-control) (ui/icon "history")])
@@ -1114,6 +1138,9 @@
:editor
(settings-editor current-repo)
:keymap
(shortcut2/shortcut-keymap-x)
:version-control
(settings-git)

View File

@@ -1,23 +1,35 @@
.cp__settings {
&-main {
aside {
&-inner {
@apply flex flex-col md:flex-row;
> aside {
@apply bg-gray-400/5 p-4;
}
article {
@apply p-4 flex-1 min-h-[12rem] w-auto overflow-y-auto;
@apply md:max-h-[70vh] md:w-[40rem];
/* margin-right: -17px; */
/* margin-bottom: -17px; */
> ul > li {
> a {
@apply mb-2;
@screen md {
/* max-height: 70vh; */
/* width: 680px; */
> strong {
font-size: 14px;
font-weight: normal;
padding-left: 5px;
opacity: .9;
}
}
&.active {
background-color: var(--ls-quaternary-background-color);
}
}
}
aside > .cp__settings-header,
article > .cp__settings-header {
> article {
@apply p-4 flex-1 min-h-[12rem] w-auto overflow-y-auto;
@apply md:max-h-[70vh] md:w-[40rem];
}
> aside > .cp__settings-header,
> article > .cp__settings-header {
@apply h-10 py-2 flex flex-row items-center justify-start gap-2;
}
@@ -41,13 +53,13 @@
@apply text-xl lowercase;
}
h1.cp__settings-modal-title:first-letter,
h1.cp__settings-modal-title:first-letter,
h1.cp__settings-category-title:first-letter {
@apply uppercase;
}
.settings-menu {
@apply p-0 m-0 mt-4 pr-3;
@apply p-0 m-0 mt-4;
}
.settings-menu-item {
@@ -56,46 +68,10 @@
}
.settings-menu-link {
@apply px-2 py-1.5 select-none;
@apply px-2 py-1.5 select-none;
color: var(--ls-primary-text-color);
}
}
&-inner {
@apply flex flex-col md:flex-row;
> aside {
ul {
> li {
> a {
> i {
overflow: hidden;
opacity: .9;
}
> strong {
font-size: 14px;
font-weight: normal;
padding-left: 5px;
margin-top: 2px;
opacity: .9;
}
}
&.active {
background-color: var(--ls-quaternary-background-color);
i {
opacity: 1;
}
}
}
}
}
&.no-aside {
> article {
@@ -392,7 +368,7 @@
z-index: 1;
width: 100px;
max-height: 180px;
border:1px solid var(--ls-border-color);
border: 1px solid var(--ls-border-color);
border-radius: 4px;
overflow: auto;
overflow: overlay;
@@ -465,3 +441,15 @@ svg.git {
svg.cmd {
margin-left: -1px;
}
body[data-settings-tab=keymap] {
.cp__settings-inner {
> article {
@apply md:w-[70vw] xl:max-w-[850px] p-0;
> header {
@apply p-4 pb-2 h-auto;
}
}
}
}

View File

@@ -3,6 +3,7 @@
[frontend.context.i18n :refer [t]]
[frontend.modules.shortcut.core :as shortcut]
[frontend.modules.shortcut.data-helper :as dh]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.state :as state]
[frontend.ui :as ui]
[frontend.extensions.latex :as latex]
@@ -104,7 +105,7 @@
[:code.text-xs (namespace k)]
[:small.pl-1 (:desc cmd)]]
(not plugin?) (-> k (dh/decorate-namespace) (t))
(not plugin?) (-> k (shortcut-utils/decorate-namespace) (t))
:else (str k))]
[:tr {:key (str k)}
[:td.text-left.flex.items-center label]
@@ -204,23 +205,11 @@
(shortcut-table :shortcut.category/block-selection true)
(shortcut-table :shortcut.category/formatting true)
(shortcut-table :shortcut.category/toggle true)
(when (state/enable-whiteboards?) (shortcut-table :shortcut.category/whiteboard true))
(when (state/enable-whiteboards?)
(shortcut-table :shortcut.category/whiteboard true))
(shortcut-table :shortcut.category/plugins true)
(shortcut-table :shortcut.category/others true)])
(rum/defc keymap-pane
[]
(let [[ready?, set-ready!] (rum/use-state false)]
(rum/use-effect!
(fn [] (js/setTimeout #(set-ready! true) 32))
[])
[:div.cp__keymap-pane
[:h1.pb-2.text-3xl.pt-2 "Keymap"]
(if ready?
(keymap-tables)
[:p.flex.justify-center.py-20 (ui/loading "")])]))
(rum/defc shortcut-page
[{:keys [show-title?]
:or {show-title? true}}]

View File

@@ -25,4 +25,169 @@
}
}
}
}
.cp__shortcut-page-x {
@apply relative;
&-pane-controls {
@apply flex space-x-3 absolute top-[-4px] right-4 items-center;
.search-input-wrap {
@apply pr-1 relative;
a.x {
@apply flex items-center absolute right-1 top-0 py-[7px] px-1 opacity-60
hover:opacity-90;
}
}
input.form-input {
@apply py-1;
}
a.icon-link {
@apply opacity-80 hover:opacity-100 active:opacity-40 select-none;
color: var(--ls-secondary-text-color);
}
.keyboard-filter {
.dropdown-wrapper {
@apply shadow-lg w-[18rem];
}
.keyboard-filter-record {
> h2 {
@apply flex items-center justify-between px-1.5 py-1;
background-color: var(--ls-secondary-background-color);
> strong {
@apply text-[12px] opacity-80;
font-weight: 400;
}
}
}
}
}
> header {
@apply px-4 pb-4 pt-2;
> h2 {
@apply relative top-[-6px];
}
}
> article {
@apply relative pb-4 overflow-y-auto;
max-height: calc(70vh - 100px);
overflow-y: overlay;
> ul {
@apply px-4 m-0 py-0;
li {
@apply text-[15px] px-1;
&.th {
@apply rounded mb-2 sticky top-0 cursor-pointer
select-none active:opacity-80 px-2 py-1 z-[1];
background-color: var(--ls-tertiary-background-color);
}
.label-wrap {
@apply flex flex-1;
}
.action-wrap {
@apply flex space-x-2 items-center flex-nowrap
select-none active:opacity-70;
&.disabled {
@apply opacity-60 cursor-default;
}
}
}
}
}
&-record-dialog-inner {
@apply py-[28px] m-[-30px] px-[20px];
h1 {
@apply relative top-[-8px];
}
&:active, &:focus, &:focus-within {
outline: burlywood hidden medium;
}
.shortcuts-keys-wrap {
@apply flex items-center my-4 flex-wrap;
.shortcut-record-control {
@apply flex space-x-1 items-center select-none
rounded border-[2px] py-[2px] px-[2px];
}
.keyboard-shortcut {
> code {
@apply relative select-none tracking-wider;
a.x {
@apply hidden absolute right-[-8px] top-[-6px] h-[16px] w-[16px]
rounded-full bg-red-500 text-white leading-none items-center
justify-center cursor-pointer opacity-80 hover:opacity-100 active:opacity-50;
}
&:hover a.x {
@apply flex;
}
}
}
}
&.keypressed {
.shortcut-record-control {
@apply pt-0
}
}
.action-btns {
.keyboard-shortcut code {
@apply rounded-[3px];
}
}
.reset-btn {
@apply ml-4 opacity-50 cursor-default;
}
&.dirty {
.reset-btn {
@apply opacity-100 cursor-pointer;
}
}
}
}
.cp__shortcut-conflicts-list {
&-wrap {
> section {
@apply bg-gray-3 border-[2px] mb-3 dark:bg-transparent;
> ul {
@apply px-2 pb-2 m-0 list-none;
}
> h2 {
@apply flex items-center p-2 text-red-9 text-sm space-x-1 font-extrabold;
}
}
}
}

View File

@@ -0,0 +1,476 @@
(ns frontend.components.shortcut2
(:require [clojure.string :as string]
[rum.core :as rum]
[frontend.context.i18n :refer [t]]
[cljs-bean.core :as bean]
[frontend.state :as state]
[frontend.search :as search]
[frontend.ui :as ui]
[frontend.rum :as r]
[goog.events :as events]
[promesa.core :as p]
[frontend.handler.notification :as notification]
[frontend.modules.shortcut.core :as shortcut]
[frontend.modules.shortcut.data-helper :as dh]
[frontend.util :as util]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.modules.shortcut.config :as shortcut-config])
(:import [goog.events KeyHandler]))
(defonce categories
(vector :shortcut.category/basics
:shortcut.category/navigating
:shortcut.category/block-editing
:shortcut.category/block-command-editing
:shortcut.category/block-selection
:shortcut.category/formatting
:shortcut.category/toggle
:shortcut.category/whiteboard
:shortcut.category/plugins
:shortcut.category/others))
(defonce *refresh-sentry (atom 0))
(defn refresh-shortcuts-list! [] (reset! *refresh-sentry (inc @*refresh-sentry)))
(defonce *global-listener-setup? (atom false))
(defonce *customize-modal-life-sentry (atom 0))
(defn- to-vector [v]
(when-not (nil? v)
(if (sequential? v) (vec v) [v])))
(declare customize-shortcut-dialog-inner)
(rum/defc keyboard-filter-record-inner
[keystroke set-keystroke! close-fn]
(let [keypressed? (not= "" keystroke)]
(rum/use-effect!
(fn []
(let [key-handler (KeyHandler. js/document)]
;; setup
(util/profile
"[shortcuts] unlisten*"
(shortcut/unlisten-all! true))
(events/listen key-handler "key"
(fn [^js e]
(.preventDefault e)
(set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
;; teardown
#(do
(util/profile
"[shortcuts] listen*"
(shortcut/listen-all!))
(.dispose key-handler))))
[])
[:div.keyboard-filter-record
[:h2
[:strong (t :keymap/keystroke-filter)]
[:span.flex.space-x-2
(when keypressed?
[:a.flex.items-center
{:on-click #(set-keystroke! "")} (ui/icon "zoom-reset" {:size 12})])
[:a.flex.items-center
{:on-click #(do (close-fn) (set-keystroke! ""))} (ui/icon "x" {:size 12})]]]
[:div.wrap.p-2
(if-not keypressed?
[:small (t :keymap/keystroke-record-desc)]
(when-not (string/blank? keystroke)
(ui/render-keyboard-shortcut [keystroke])))]]))
(rum/defc pane-controls
[q set-q! filters set-filters! keystroke set-keystroke! toggle-categories-fn]
(let [*search-ref (rum/use-ref nil)]
[:div.cp__shortcut-page-x-pane-controls
[:a.flex.items-center.icon-link
{:on-click toggle-categories-fn
:title "Toggle categories pane"}
(ui/icon "fold")]
[:a.flex.items-center.icon-link
{:on-click refresh-shortcuts-list!
:title "Refresh all"}
(ui/icon "refresh")]
[:span.search-input-wrap
[:input.form-input.is-small
{:placeholder (t :keymap/search)
:ref *search-ref
:value (or q "")
:auto-focus true
:on-key-down #(when (= 27 (.-keyCode %))
(util/stop %)
(if (string/blank? q)
(some-> (rum/deref *search-ref) (.blur))
(set-q! "")))
:on-change #(let [v (util/evalue %)]
(set-q! v))}]
(when-not (string/blank? q)
[:a.x
{:on-click (fn []
(set-q! "")
(js/setTimeout #(some-> (rum/deref *search-ref) (.focus)) 50))}
(ui/icon "x" {:size 14})])]
;; keyboard filter
(ui/dropdown
(fn [{:keys [toggle-fn]}]
[:a.flex.items-center.icon-link
{:on-click toggle-fn} (ui/icon "keyboard")
(when-not (string/blank? keystroke)
(ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
(fn [{:keys [close-fn]}]
(keyboard-filter-record-inner keystroke set-keystroke! close-fn))
{:outside? true
:trigger-class "keyboard-filter"})
;; other filter
(ui/dropdown-with-links
(fn [{:keys [toggle-fn]}]
[:a.flex.items-center.icon-link.relative
{:on-click toggle-fn}
(ui/icon "filter")
(when (seq filters)
(ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
(for [k [:All :Disabled :Unset :Custom]
:let [all? (= k :All)
checked? (or (contains? filters k) (and all? (nil? (seq filters))))]]
{:title (if all? (t :keymap/all) (t (keyword :keymap (string/lower-case (name k)))))
:icon (ui/icon (if checked? "checkbox" "square"))
:options {:on-click #(set-filters! (if all? #{} (let [f (if checked? disj conj)] (f filters k))))}})
nil)]))
(rum/defc shortcut-desc-label
[id binding-map]
(when-let [id' (and id binding-map (some-> (str id) (string/replace "plugin." "")))]
[:span {:title (str id' "#" (some-> (:handler-id binding-map) (name)))}
[:span.pl-1 (dh/get-shortcut-desc (assoc binding-map :id id))]
[:small.pl-1 [:code.text-xs (str id')]]]))
(defn- open-customize-shortcut-dialog!
[id]
(when-let [{:keys [binding user-binding] :as m} (dh/shortcut-item id)]
(let [binding (to-vector binding)
user-binding (and user-binding (to-vector user-binding))
modal-id (str :customize-shortcut id)
label (shortcut-desc-label id m)
args [id label binding user-binding
{:saved-cb (fn [] (-> (p/delay 500) (p/then refresh-shortcuts-list!)))
:modal-id modal-id}]]
(state/set-sub-modal!
(fn [] (apply customize-shortcut-dialog-inner args))
{:center? true
:id modal-id
:payload args}))))
(rum/defc shortcut-conflicts-display
[_k conflicts-map]
[:div.cp__shortcut-conflicts-list-wrap
(for [[g ks] conflicts-map]
[:section.relative
[:h2 (ui/icon "alert-triangle" {:size 15})
[:span (t :keymap/conflicts-for-label)]
[:code (shortcut-utils/decorate-binding g)]]
[:ul
(for [v (vals ks)
:let [k (first v)
vs (second v)]]
(for [[id' handler-id] vs
:let [m (dh/shortcut-item id')]
:when (not (nil? m))]
[:li
{:key (str id')}
[:a.select-none.hover:underline
{:on-click #(open-customize-shortcut-dialog! id')
:title (str handler-id)}
[:code.inline-block.mr-1.text-xs
(shortcut-utils/decorate-binding k)]
[:span
(dh/get-shortcut-desc m)
(ui/icon "external-link" {:size 18})]
[:code [:small (str id')]]]]))]])])
(rum/defc ^:large-vars/cleanup-todo customize-shortcut-dialog-inner
[k action-name binding user-binding {:keys [saved-cb modal-id]}]
(let [*ref-el (rum/use-ref nil)
[modal-life _] (r/use-atom *customize-modal-life-sentry)
[keystroke set-keystroke!] (rum/use-state "")
[current-binding set-current-binding!] (rum/use-state (or user-binding binding))
[key-conflicts set-key-conflicts!] (rum/use-state nil)
handler-id (rum/use-memo #(dh/get-group k))
dirty? (not= (or user-binding binding) current-binding)
keypressed? (not= "" keystroke)
save-keystroke-fn!
(fn []
;; parse current binding conflicts
(if-let [current-conflicts (seq (dh/parse-conflicts-from-binding current-binding keystroke))]
(notification/show!
(str "Shortcut conflicts from existing binding: "
(pr-str (some->> current-conflicts (map #(shortcut-utils/decorate-binding %)))))
:error true :shortcut-conflicts/warning 5000)
;; get conflicts from the existed bindings map
(let [conflicts-map (dh/get-conflicts-by-keys keystroke handler-id)]
(if-not (seq conflicts-map)
(do (set-current-binding! (conj current-binding keystroke))
(set-keystroke! "")
(set-key-conflicts! nil))
;; show conflicts
(set-key-conflicts! conflicts-map)))))]
(rum/use-effect!
(fn []
(let [mid (state/sub :modal/id)
mid' (some-> (state/sub :modal/subsets) (last) (:modal/id))
el (rum/deref *ref-el)]
(when (or (and (not mid') (= mid modal-id))
(= mid' modal-id))
(some-> el (.focus))
(js/setTimeout
#(some-> (.querySelector el ".shortcut-record-control a.submit")
(.click)) 200))))
[modal-life])
(rum/use-effect!
(fn []
(let [^js el (rum/deref *ref-el)
key-handler (KeyHandler. el)
teardown-global!
(when-not @*global-listener-setup?
(shortcut/unlisten-all! true)
(reset! *global-listener-setup? true)
(fn []
(shortcut/listen-all!)
(reset! *global-listener-setup? false)))]
;; setup
(events/listen key-handler "key"
(fn [^js e]
(.preventDefault e)
(set-key-conflicts! nil)
(set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
;; active
(.focus el)
;; teardown
#(do (some-> teardown-global! (apply nil))
(.dispose key-handler)
(swap! *customize-modal-life-sentry inc))))
[])
[:div.cp__shortcut-page-x-record-dialog-inner
{:class (util/classnames [{:keypressed keypressed? :dirty dirty?}])
:tab-index -1
:ref *ref-el}
[:div.sm:w-lsm
[:h1.text-2xl.pb-2
(t :keymap/customize-for-label)]
[:p.mb-4.text-md [:b action-name]]
[:div.shortcuts-keys-wrap
[:span.keyboard-shortcut.flex.flex-wrap.mr-2.space-x-2
(for [x current-binding]
[:code.tracking-wider
(-> x (string/trim) (string/lower-case) (shortcut-utils/decorate-binding))
[:a.x {:on-click (fn [] (set-current-binding!
(->> current-binding (remove #(= x %)) (into []))))}
(ui/icon "x" {:size 12})]])]
;; add shortcut
[:div.shortcut-record-control
;; keypressed state
(if keypressed?
[:<>
(when-not (string/blank? keystroke)
(ui/render-keyboard-shortcut [keystroke]))
[:a.flex.items-center.active:opacity-90.submit
{:on-click save-keystroke-fn!}
(ui/icon "check" {:size 14})]
[:a.flex.items-center.text-red-600.hover:text-red-700.active:opacity-90.cancel
{:on-click (fn []
(set-keystroke! "")
(set-key-conflicts! nil))}
(ui/icon "x" {:size 14})]]
[:code.flex.items-center
[:small.pr-1 (t :keymap/keystroke-record-setup-label)] (ui/icon "keyboard" {:size 14})])]]]
;; conflicts results
(when (seq key-conflicts)
(shortcut-conflicts-display k key-conflicts))
[:div.action-btns.text-right.mt-6.flex.justify-between.items-center
;; restore default
(when (sequential? binding)
[:a.flex.items-center.space-x-1.text-sm.opacity-70.hover:opacity-100
{:on-click #(set-current-binding! binding)}
(t :keymap/restore-to-default)
(for [it (some->> binding (map #(some->> % (dh/mod-key) (shortcut-utils/decorate-binding))))]
[:span.keyboard-shortcut.ml-1 [:code it]])])
[:span
(ui/button
(t :save)
:disabled (not dirty?)
:on-click (fn []
;; TODO: check conflicts for the single same leader key
(let [binding' (if (nil? current-binding) [] current-binding)
conflicts (dh/get-conflicts-by-keys binding' handler-id {:exclude-ids #{k}})]
(if (seq conflicts)
(set-key-conflicts! conflicts)
(let [binding' (if (= binding binding') nil binding')]
(shortcut/persist-user-shortcut! k binding')
;(notification/show! "Saved!" :success)
(state/close-modal!)
(saved-cb))))))
[:a.reset-btn
{:on-click (fn [] (set-current-binding! (or user-binding binding)))}
(t :reset)]]]]))
(defn build-categories-map
[]
(->> categories
(map #(vector % (into (sorted-map) (dh/binding-by-category %))))))
(rum/defc ^:large-vars/cleanup-todo shortcut-keymap-x
[]
(let [_ (r/use-atom shortcut-config/*category)
_ (r/use-atom *refresh-sentry)
[ready?, set-ready!] (rum/use-state false)
[filters, set-filters!] (rum/use-state #{})
[keystroke, set-keystroke!] (rum/use-state "")
[q set-q!] (rum/use-state nil)
categories-list-map (build-categories-map)
all-categories (into #{} (map first categories-list-map))
in-filters? (boolean (seq filters))
in-query? (not (string/blank? (util/trim-safe q)))
in-keystroke? (not (string/blank? keystroke))
[folded-categories set-folded-categories!] (rum/use-state #{})
matched-list-map
(when (and in-query? (not in-keystroke?))
(->> categories-list-map
(map (fn [[c binding-map]]
[c (search/fuzzy-search
binding-map q
:extract-fn
#(let [[id m] %]
(str (name id) " " (dh/get-shortcut-desc (assoc m :id id)))))]))))
result-list-map (or matched-list-map categories-list-map)
toggle-categories! #(if (= folded-categories all-categories)
(set-folded-categories! #{})
(set-folded-categories! all-categories))]
(rum/use-effect!
(fn []
(js/setTimeout #(set-ready! true) 100))
[])
[:div.cp__shortcut-page-x
[:header.relative
[:h2.text-xs.opacity-70
(str (t :keymap/total)
" "
(if ready?
(apply + (map #(count (second %)) result-list-map))
" ..."))]
(pane-controls q set-q! filters set-filters! keystroke set-keystroke! toggle-categories!)]
[:article
(when-not ready?
[:p.py-8.flex.justify-center (ui/loading "")])
(when ready?
[:ul.list-none.m-0.py-3
(for [[c binding-map] result-list-map
:let [folded? (contains? folded-categories c)]]
[:<>
;; category row
(when (and (not in-query?)
(not in-filters?)
(not in-keystroke?))
[:li.flex.justify-between.th
{:key (str c)
:on-click #(let [f (if folded? disj conj)]
(set-folded-categories! (f folded-categories c)))}
[:strong.font-semibold (t c)]
[:i.flex.items-center
(ui/icon (if folded? "chevron-left" "chevron-down"))]])
;; binding row
(when (or in-query? in-filters? (not folded?))
(for [[id {:keys [binding user-binding] :as m}] binding-map
:let [binding (to-vector binding)
user-binding (and user-binding (to-vector user-binding))
label (shortcut-desc-label id m)
custom? (not (nil? user-binding))
disabled? (or (false? user-binding)
(false? (first binding)))
unset? (and (not disabled?)
(= user-binding []))]]
(when (or (nil? (seq filters))
(when (contains? filters :Custom) custom?)
(when (contains? filters :Disabled) disabled?)
(when (contains? filters :Unset) unset?))
;; keystrokes filter
(when (or (not in-keystroke?)
(and (not disabled?)
(not unset?)
(let [binding' (or user-binding binding)
keystroke' (some-> (shortcut-utils/safe-parse-string-binding keystroke) (bean/->clj))]
(when (sequential? binding')
(some #(when-let [s (some-> % (dh/mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
(or (= s keystroke')
(and (sequential? s) (sequential? keystroke')
(apply = (map first [s keystroke']))))) binding')))))
[:li.flex.items-center.justify-between.text-sm
{:key (str id)}
[:span.label-wrap label]
[:a.action-wrap
{:class (util/classnames [{:disabled disabled?}])
:on-click (when-not disabled?
#(open-customize-shortcut-dialog! id))}
(cond
(or user-binding (false? user-binding))
[:code.dark:bg-green-800.bg-green-300
(if unset?
(t :keymap/unset)
(str (t :keymap/custom) ": "
(if disabled?
(t :keymap/disabled)
(bean/->js
(map #(if (false? %)
(t :keymap/disabled)
(shortcut-utils/decorate-binding %)) user-binding)))))]
(not unset?)
(for [x binding]
[:code.tracking-wide
{:key (str x)}
(dh/binding-for-display id x)]))]]))))])])]]))

View File

@@ -92,7 +92,7 @@
(rum/use-effect!
#(state/set-modal!
(when settings-open?
(fn [] [:div.settings-modal (settings/settings)])))
(fn [] [:div.settings-modal (settings/settings settings-open?)])))
[settings-open?])
(rum/use-effect!

View File

@@ -292,7 +292,7 @@
(tldraw-app page-name block-id)]))
(rum/defc whiteboard-route <
(shortcut/mixin :shortcut.handler/whiteboard)
(shortcut/mixin :shortcut.handler/whiteboard false)
[route-match]
(let [name (get-in route-match [:parameters :path :name])
{:keys [block-id]} (get-in route-match [:parameters :query])]

View File

@@ -988,6 +988,11 @@
{:set-dirty-hls! set-dirty-hls!
:set-hls-extra! set-hls-extra!}) "pdf-viewer")])))])))
(rum/defc pdf-container-outer
< (shortcut/mixin :shortcut.handler/pdf false)
[child]
[:<> child])
(rum/defc pdf-container
[{:keys [identity] :as pdf-current}]
(let [[prepared set-prepared!] (rum/use-state false)
@@ -1029,8 +1034,7 @@
(rum/defcs default-embed-playground
< rum/static rum/reactive
(shortcut/mixin :shortcut.handler/pdf)
[]
[state]
(let [pdf-current (state/sub :pdf/current)
system-win? (state/sub :pdf/system-win?)]
[:div.extensions__pdf-playground
@@ -1040,8 +1044,9 @@
(when (and (not system-win?) pdf-current)
(js/ReactDOM.createPortal
(pdf-container pdf-current)
(js/document.querySelector "#app-single-container")))]))
(pdf-container-outer
(pdf-container pdf-current))
(js/document.querySelector "#app-single-container")))]))
(rum/defcs system-embed-playground
< rum/reactive

View File

@@ -512,7 +512,7 @@
[:div.my-3 (ui/button "Review cards" :small? true)])]))))
(rum/defc view-modal <
(shortcut/mixin :shortcut.handler/cards)
(shortcut/mixin :shortcut.handler/cards false)
[blocks option card-index]
[:div#cards-modal
(if (seq blocks)

View File

@@ -2,6 +2,7 @@
"System-component-like ns for command palette's functionality"
(:require [cljs.spec.alpha :as s]
[frontend.modules.shortcut.data-helper :as shortcut-helper]
[frontend.handler.plugin :as plugin-handler]
[frontend.spec :as spec]
[frontend.state :as state]
[lambdaisland.glogi :as log]
@@ -50,10 +51,10 @@
(defn add-history [{:keys [id]}]
(storage/set "commands-history" (conj (history) {:id id :timestamp (.getTime (js/Date.))})))
(defn invoke-command [{:keys [action] :as cmd}]
(defn invoke-command [{:keys [id action] :as cmd}]
(add-history cmd)
(state/close-modal!)
(action))
(plugin-handler/hook-lifecycle-fn! id action))
(defn top-commands [limit]
(->> (get-commands)

View File

@@ -18,6 +18,9 @@
(repo-config-handler/read-repo-config content)
(let [result (parse-repo-config content)
ks (if (vector? k) k [k])
v (cond->> v
(map? v)
(reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
new-result (rewrite/assoc-in result ks v)
new-content (str new-result)]
(file-handler/set-file-content! repo path new-content) nil))))

View File

@@ -22,7 +22,6 @@
[frontend.components.shell :as shell]
[frontend.components.whiteboard :as whiteboard]
[frontend.components.user.login :as login]
[frontend.components.shortcut :as shortcut]
[frontend.config :as config]
[frontend.context.i18n :refer [t]]
[frontend.db :as db]
@@ -457,8 +456,8 @@
(commands/exec-plugin-simple-command! pid cmd action))
(defmethod handle :shortcut-handler-refreshed [[_]]
(when-not @st/*inited?
(reset! st/*inited? true)
(when-not @st/*pending-inited?
(reset! st/*pending-inited? true)
(st/consume-pending-shortcuts!)))
(defmethod handle :mobile/keyboard-will-show [[_ keyboard-height]]
@@ -936,10 +935,8 @@
(defmethod handle :editor/quick-capture [[_ args]]
(quick-capture/quick-capture args))
(defmethod handle :modal/keymap-manager [[_]]
(state/set-modal!
#(shortcut/keymap-pane)
{:label "keymap-manager"}))
(defmethod handle :modal/keymap [[_]]
(state/open-settings! :keymap))
(defmethod handle :editor/toggle-own-number-list [[_ blocks]]
(let [batch? (sequential? blocks)

View File

@@ -8,6 +8,7 @@
[shadow.resource :as rc]
[clojure.edn :as edn]
[electron.ipc :as ipc]
[borkdude.rewrite-edn :as rewrite]
[logseq.common.path :as path]))
;; Use defonce to avoid broken state on dev reload
@@ -38,7 +39,7 @@
(defn set-global-config-state!
[content]
(let [config (edn/read-string content)]
(state/set-global-config! config)
(state/set-global-config! config content)
config))
(def default-content (rc/inline "templates/global-config.edn"))
@@ -59,6 +60,22 @@
(p/let [config-content (fs/read-file nil config-path)]
(set-global-config-state! config-content))))
(defn set-global-config-kv!
[k v]
(let [result (rewrite/parse-string
(or (state/get-global-config-str-content) "{}"))
ks (if (sequential? k) k [k])
v (cond->> v
(map? v)
(reduce-kv (fn [a k v] (rewrite/assoc a k v)) (rewrite/parse-string "{}")))
new-result (if (and (= 1 (count ks))
(nil? v))
(rewrite/dissoc result (first ks))
(rewrite/assoc-in result ks v))
new-str-content (str new-result)]
(fs/write-file! nil nil (global-config-path) new-str-content {:skip-compare? true})
(state/set-global-config! (rewrite/sexpr new-result) new-str-content)))
(defn start
"This component has four responsibilities on start:
- Fetch root-dir for later use with config paths

View File

@@ -18,11 +18,13 @@
([content]
(show! content :info true nil 2000 nil))
([content status]
(show! content status true nil 1500 nil))
(show! content status (not= status :error) nil 1500 nil))
([content status clear?]
(show! content status clear? nil 1500 nil))
([content status clear? uid]
(show! content status clear? uid 1500 nil))
([content status clear? uid timeout]
(show! content status clear? uid timeout nil))
([content status clear? uid timeout close-cb]
(let [contents (state/get-notification-contents)
uid (or uid (keyword (util/unique-id)))]
@@ -31,7 +33,7 @@
:status status
:close-cb close-cb}))
(when (and clear? (not= status :error))
(js/setTimeout #(clear! uid) (or timeout 1500)))
(when (and clear? (or timeout (not= status :error)))
(js/setTimeout #(clear! uid) (or timeout 2000)))
uid)))

View File

@@ -7,6 +7,7 @@
[logseq.graph-parser.mldoc :as gp-mldoc]
[frontend.handler.notification :as notification]
[frontend.handler.common.plugin :as plugin-common-handler]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.storage :as storage]
[camel-snake-kebab.core :as csk]
[frontend.state :as state]
@@ -175,7 +176,7 @@
(defn has-setting-schema?
[id]
(when-let [pl (and id (get-plugin-inst (name id)))]
(when-let [^js pl (and id (get-plugin-inst (name id)))]
(boolean (.-settingsSchema pl))))
(defn get-enabled-plugins-if-setting-schema
@@ -297,7 +298,7 @@
(let [id (keyword (str "plugin." pid "/" key))
binding (:binding keybinding)
binding (some->> (if (string? binding) [binding] (seq binding))
(map util/normalize-user-keyname))
(map shortcut-utils/undecorate-binding))
binding (if util/mac?
(or (:mac keybinding) binding) binding)
mode (or (:mode keybinding) :global)
@@ -658,6 +659,15 @@
:remove disj)]
(save-plugin-preferences! {:pinnedToolbarItems (op-fn pinned (name key))}))))
(defn hook-lifecycle-fn!
[type f & args]
(when (and type (fn? f))
(when config/lsp-enabled?
(hook-plugin-app (str :before-command-invoked type) nil))
(apply f args)
(when config/lsp-enabled?
(hook-plugin-app (str :after-command-invoked type) nil))))
;; components
(rum/defc lsp-indicator < rum/reactive
[]
@@ -788,7 +798,6 @@
(callback)
(init-plugins! callback)))
(comment
{:pending (count (:plugin/updates-pending @state/state))
:auto-checking? (boolean (:plugin/updates-auto-checking? @state/state))

View File

@@ -29,31 +29,33 @@
(defn hide-when-esc-or-outside
[state & {:keys [on-hide node visibilitychange? outside?]}]
(try
(let [dom-node (rum/dom-node state)]
(when-let [dom-node (or node dom-node)]
(let [click-fn (fn [e]
(let [target (.. e -target)]
;; If the click target is outside of current node
(when (and
(not (dom/contains dom-node target))
(not (.contains (.-classList target) "ignore-outside-event")))
(on-hide state e :click))))]
(when-not (false? outside?)
(listen state js/window "mousedown" click-fn)))
(listen state js/window "keydown"
(fn [e]
(case (.-keyCode e)
;; Esc
27 (on-hide state e :esc)
nil)))
(when visibilitychange?
(listen state js/window "visibilitychange"
(let [opts (last (:rum/args state))
outside? (cond-> opts (nil? outside?) (:outside?))]
(try
(let [dom-node (rum/dom-node state)]
(when-let [dom-node (or node dom-node)]
(let [click-fn (fn [e]
(let [target (.. e -target)]
;; If the click target is outside of current node
(when (and
(not (dom/contains dom-node target))
(not (.contains (.-classList target) "ignore-outside-event")))
(on-hide state e :click))))]
(when-not (false? outside?)
(listen state js/window "mousedown" click-fn)))
(listen state js/window "keydown"
(fn [e]
(on-hide state e :visibilitychange))))))
(catch :default _e
;; TODO: Unable to find node on an unmounted component.
nil)))
(case (.-keyCode e)
;; Esc
27 (on-hide state e :esc)
nil)))
(when visibilitychange?
(listen state js/window "visibilitychange"
(fn [e]
(on-hide state e :visibilitychange))))))
(catch :default _e
;; TODO: Unable to find node on an unmounted component.
nil))))
(defn on-enter
[state & {:keys [on-enter node]}]

File diff suppressed because it is too large Load Diff

View File

@@ -1,9 +1,12 @@
(ns frontend.modules.shortcut.core
(:require [clojure.string :as str]
[frontend.handler.config :as config-handler]
[frontend.handler.global-config :as global-config-handler]
[frontend.handler.plugin :as plugin-handler]
[frontend.handler.notification :as notification]
[frontend.modules.shortcut.data-helper :as dh]
[frontend.modules.shortcut.config :as shortcut-config]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.state :as state]
[frontend.util :as util]
[goog.events :as events]
@@ -13,15 +16,15 @@
(:import [goog.events KeyCodes KeyHandler KeyNames]
[goog.ui KeyboardShortcutHandler]))
(def *installed (atom {}))
(def *inited? (atom false))
(def *pending (atom []))
(defonce *installed-handlers (atom {}))
(defonce *pending-inited? (atom false))
(defonce *pending-shortcuts (atom []))
(def global-keys #js
[KeyCodes/TAB
KeyCodes/ENTER
KeyCodes/BACKSPACE KeyCodes/DELETE
KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
[KeyCodes/TAB
KeyCodes/ENTER
KeyCodes/BACKSPACE KeyCodes/DELETE
KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
(def key-names (js->clj KeyNames))
@@ -29,16 +32,25 @@
(defn consume-pending-shortcuts!
[]
(when (and @*inited? (seq @*pending))
(doseq [[handler-id id shortcut] @*pending]
(when (and @*pending-inited? (seq @*pending-shortcuts))
(doseq [[handler-id id shortcut] @*pending-shortcuts]
(register-shortcut! handler-id id shortcut))
(reset! *pending [])))
(reset! *pending-shortcuts [])))
(defn- get-handler-by-id
[handler-id]
(-> (filter #(= (:group %) handler-id) (vals @*installed))
first
:handler))
(->> (vals @*installed-handlers)
(filter #(= (:group %) handler-id))
first
:handler))
(defn- get-installed-ids-by-handler-id
[handler-id]
(some->> @*installed-handlers
(filter #(= (:group (second %)) handler-id))
(map first)
(remove nil?)
(vec)))
(defn register-shortcut!
"Register a shortcut, notice the id need to be a namespaced keyword to avoid
@@ -50,14 +62,14 @@
([handler-id id]
(register-shortcut! handler-id id nil))
([handler-id id shortcut-map]
(if (and (keyword? handler-id) (not @*inited?))
(swap! *pending conj [handler-id id shortcut-map])
(when-let [handler (if (or (string? handler-id) (keyword? handler-id))
(let [handler-id (keyword handler-id)]
(get-handler-by-id handler-id))
(if (and (keyword? handler-id) (not @*pending-inited?))
(swap! *pending-shortcuts conj [handler-id id shortcut-map])
(when-let [^js handler (if (or (string? handler-id) (keyword? handler-id))
(let [handler-id (keyword handler-id)]
(get-handler-by-id handler-id))
;; handler
handler-id)]
;; as Handler instance
handler-id)]
(when shortcut-map
(shortcut-config/add-shortcut! handler-id id shortcut-map))
@@ -66,7 +78,7 @@
(doseq [k (dh/shortcut-binding id)]
(try
(log/debug :shortcut/register-shortcut {:id id :binding k})
(.registerShortcut handler (util/keyname id) (util/normalize-user-keyname k))
(.registerShortcut handler (util/keyname id) (shortcut-utils/undecorate-binding k))
(catch :default e
(log/error :shortcut/register-shortcut {:id id
:binding k
@@ -81,15 +93,17 @@
(when-let [handler (get-handler-by-id handler-id)]
(when-let [ks (dh/shortcut-binding shortcut-id)]
(doseq [k ks]
(.unregisterShortcut ^js handler (util/normalize-user-keyname k))))
(.unregisterShortcut ^js handler (shortcut-utils/undecorate-binding k))))
(shortcut-config/remove-shortcut! handler-id shortcut-id)))
(defn uninstall-shortcut-handler!
[install-id]
(when-let [handler (-> (get @*installed install-id)
:handler)]
(.dispose ^js handler)
(swap! *installed dissoc install-id)))
([install-id] (uninstall-shortcut-handler! install-id false))
([install-id refresh?]
(when-let [handler (-> (get @*installed-handlers install-id)
:handler)]
(.dispose ^js handler)
(js/console.debug "[shortcuts]" "uninstall handler" (-> @*installed-handlers (get install-id) :group str) (if refresh? "*" ""))
(swap! *installed-handlers dissoc install-id))))
(defn install-shortcut-handler!
[handler-id {:keys [set-global-keys?
@@ -97,11 +111,15 @@
state]
:or {set-global-keys? true
prevent-default? false}}]
(when-let [install-id (get-handler-by-id handler-id)]
(uninstall-shortcut-handler! install-id))
;; force uninstall existed handler
(some->>
(get-installed-ids-by-handler-id handler-id)
(map #(uninstall-shortcut-handler! % true))
(doall))
(let [shortcut-map (dh/shortcut-map handler-id state)
handler (new KeyboardShortcutHandler js/window)]
handler (new KeyboardShortcutHandler js/window)]
;; set arrows enter, tab to global
(when set-global-keys?
(.setGlobalKeys handler global-keys))
@@ -114,66 +132,109 @@
(register-shortcut! handler id))
(let [f (fn [e]
(let [shortcut-map (dh/shortcut-map handler-id state)
dispatch-fn (get shortcut-map (keyword (.-identifier e)))]
(let [id (keyword (.-identifier e))
shortcut-map (dh/shortcut-map handler-id state) ;; required to get shortcut map dynamically
dispatch-fn (get shortcut-map id)]
;; trigger fn
(when dispatch-fn (dispatch-fn e))))
(when dispatch-fn
(plugin-handler/hook-lifecycle-fn! id dispatch-fn e))))
install-id (random-uuid)
data {install-id
{:group handler-id
:dispatch-fn f
:handler handler}}]
data {install-id
{:group handler-id
:dispatch-fn f
:handler handler}}]
(.listen handler EventType/SHORTCUT_TRIGGERED f)
(swap! *installed merge data)
(js/console.debug "[shortcuts] install handler" (str handler-id))
(swap! *installed-handlers merge data)
install-id)))
(defn- install-shortcuts!
[]
(->> [:shortcut.handler/misc
:shortcut.handler/editor-global
:shortcut.handler/global-non-editing-only
:shortcut.handler/global-prevent-default]
[handler-ids]
(->> (or (seq handler-ids)
[:shortcut.handler/misc
:shortcut.handler/editor-global
:shortcut.handler/global-non-editing-only
:shortcut.handler/global-prevent-default])
(map #(install-shortcut-handler! % {}))
doall))
(defn mixin [handler-id]
(defn mixin
([handler-id] (mixin handler-id true))
([handler-id remount-reinstall?]
(cond->
{:did-mount
(fn [state]
(let [install-id (install-shortcut-handler! handler-id {:state state})]
(assoc state ::install-id install-id)))
:will-unmount
(fn [state]
(when-let [install-id (::install-id state)]
(uninstall-shortcut-handler! install-id))
state)}
remount-reinstall?
(assoc
:will-remount
(fn [old-state new-state]
(util/profile "[shortcuts] reinstalled:"
(uninstall-shortcut-handler! (::install-id old-state))
(when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
(assoc new-state ::install-id install-id))))))))
(defn mixin*
"This is an optimized version compared to (mixin).
And the shortcuts will not be frequently loaded and unloaded.
As well as ensuring unnecessary updates of components."
[handler-id]
{:did-mount
(fn [state]
(let [install-id (install-shortcut-handler! handler-id {:state state})]
(assoc state ::install-id install-id)))
(let [*state (volatile! state)
install-id (install-shortcut-handler! handler-id {:state *state})]
(assoc state ::install-id install-id
::*state *state)))
:will-remount
(fn [old-state new-state]
(when-let [*state (::*state old-state)]
(vreset! *state new-state))
new-state)
:will-remount (fn [old-state new-state]
(uninstall-shortcut-handler! (::install-id old-state))
(when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
(assoc new-state ::install-id install-id)))
:will-unmount
(fn [state]
(when-let [install-id (::install-id state)]
(uninstall-shortcut-handler! install-id))
(uninstall-shortcut-handler! install-id)
(some-> (::*state state) (vreset! nil)))
state)})
(defn unlisten-all []
(doseq [{:keys [handler group]} (vals @*installed)
:when (not= group :shortcut.handler/misc)]
(.removeAllListeners handler)))
(defn unlisten-all!
([] (unlisten-all! false))
([dispose?]
(doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
:when (not= group :shortcut.handler/misc)]
(if dispose?
(.dispose handler)
(events/unlisten handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))))
(defn listen-all []
(doseq [{:keys [handler group dispatch-fn]} (vals @*installed)
(defn listen-all! []
(doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
:when (not= group :shortcut.handler/misc)]
(events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))
(if (.isDisposed handler)
(install-shortcut-handler! group {})
(events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn))))
(def disable-all-shortcuts
{:will-mount
(fn [state]
(unlisten-all)
(unlisten-all!)
state)
:will-unmount
(fn [state]
(listen-all)
(listen-all!)
state)})
(defn refresh-internal!
@@ -182,27 +243,29 @@
(when-not (:ui/shortcut-handler-refreshing? @state/state)
(state/set-state! :ui/shortcut-handler-refreshing? true)
(doseq [id (keys @*installed)]
(uninstall-shortcut-handler! id))
(install-shortcuts!)
(let [ids (keys @*installed-handlers)
_handler-ids (set (map :group (vals @*installed-handlers)))]
(doseq [id ids] (uninstall-shortcut-handler! id))
;; TODO: should re-install existed handlers
(install-shortcuts! nil))
(state/pub-event! [:shortcut-handler-refreshed])
(state/set-state! :ui/shortcut-handler-refreshing? false)))
(def refresh! (debounce refresh-internal! 1000))
(defn- name-with-meta [e]
(let [ctrl (.-ctrlKey e)
alt (.-altKey e)
meta (.-metaKey e)
shift (.-shiftKey e)
(let [ctrl (.-ctrlKey e)
alt (.-altKey e)
meta (.-metaKey e)
shift (.-shiftKey e)
keyname (get key-names (str (.-keyCode e)))]
(cond->> keyname
ctrl (str "ctrl+")
alt (str "alt+")
meta (str "meta+")
shift (str "shift+"))))
ctrl (str "ctrl+")
alt (str "alt+")
meta (str "meta+")
shift (str "shift+"))))
(defn- keyname [e]
(defn keyname [e]
(let [name (get key-names (str (.-keyCode e)))]
(case name
nil nil
@@ -215,7 +278,7 @@
(let [handler (KeyHandler. js/document)
keystroke (:rum/local state)]
(doseq [id (keys @*installed)]
(doseq [id (keys @*installed-handlers)]
(uninstall-shortcut-handler! id))
(events/listen handler "key"
@@ -240,6 +303,27 @@
(when-let [^js handler (::key-record-handler state)]
(.dispose handler))
;; force re-install shortcut handlers
(js/setTimeout #(refresh!) 500)
(dissoc state ::key-record-handler))})
(defn persist-user-shortcut!
[id binding]
(let [graph-shortcuts (or (:shortcuts (state/get-graph-config)) {})
global-shortcuts (or (:shortcuts (state/get-global-config)) {})
global? true]
(letfn [(into-shortcuts [shortcuts]
(cond-> shortcuts
(nil? binding)
(dissoc id)
(and global?
(or (string? binding)
(vector? binding)
(boolean? binding)))
(assoc id binding)))]
;; TODO: exclude current graph config shortcuts
(when (nil? binding)
(config-handler/set-config! :shortcuts (into-shortcuts graph-shortcuts)))
(global-config-handler/set-global-config-kv! :shortcuts (into-shortcuts global-shortcuts)))))

View File

@@ -1,11 +1,14 @@
(ns frontend.modules.shortcut.data-helper
(:require [borkdude.rewrite-edn :as rewrite]
[clojure.set :refer [rename-keys] :as set]
[clojure.string :as str]
[clojure.set :refer [rename-keys]]
[cljs-bean.core :as bean]
[frontend.context.i18n :refer [t]]
[frontend.config :as config]
[frontend.db :as db]
[frontend.handler.file :as file]
[frontend.modules.shortcut.config :as shortcut-config]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.state :as state]
[frontend.util :as util]
[lambdaisland.glogi :as log]
@@ -13,29 +16,74 @@
[frontend.handler.config :as config-handler])
(:import [goog.ui KeyboardShortcutHandler]))
(declare get-group)
;; function vals->bindings is too time-consuming. Here we cache the results.
(defn- flatten-key-bindings
[config]
(->> config
(into {})
(map (fn [[k {:keys [binding]}]]
{k binding}))
(defn- flatten-bindings-by-id
[config user-shortcuts binding-only?]
(->> (vals config)
(apply merge)
(map (fn [[id {:keys [binding] :as opts}]]
{id (if binding-only?
(get user-shortcuts id binding)
(assoc opts :user-binding (get user-shortcuts id)
:handler-id (get-group id)
:id id))}))
(into {})))
(def m-flatten-key-bindings (util/memoize-last flatten-key-bindings))
(defn- flatten-bindings-by-key
[config user-shortcuts]
(reduce-kv
(fn [r handler-id vs]
(reduce-kv
(fn [r id {:keys [binding]}]
(if-let [ks (get user-shortcuts id binding)]
(let [ks (if (sequential? ks) ks [ks])]
(reduce (fn [a k]
(let [k (shortcut-utils/undecorate-binding k)
k' (shortcut-utils/safe-parse-string-binding k)
k' (bean/->clj k')]
(-> a
(assoc-in [k' :key] k)
(assoc-in [k' :refs id] handler-id)))) r ks))
r)) r vs))
{} config))
(def m-flatten-bindings-by-id
(util/memoize-last flatten-bindings-by-id))
(def m-flatten-bindings-by-key
(util/memoize-last flatten-bindings-by-key))
(defn get-bindings
[]
(m-flatten-key-bindings (vals @shortcut-config/config)))
(m-flatten-bindings-by-id @shortcut-config/*config (state/shortcuts) true))
(defn- mod-key [shortcut]
(str/replace shortcut #"(?i)mod"
(if util/mac? "meta" "ctrl")))
(defn get-bindings-keys-map
[]
(m-flatten-bindings-by-key @shortcut-config/*config (state/shortcuts)))
(defn get-bindings-ids-map
[]
(m-flatten-bindings-by-id @shortcut-config/*config (state/shortcuts) false))
(defn get-shortcut-desc
[binding-map]
(let [{:keys [id desc cmd]} binding-map
desc (or desc (:desc cmd) (some-> id (shortcut-utils/decorate-namespace) (t)))]
(if (or (nil? desc)
(and (string? desc) (str/starts-with? desc "{Missing")))
(str id) desc)))
(defn mod-key [shortcut]
(when (string? shortcut)
(str/replace shortcut #"(?i)mod"
(if util/mac? "meta" "ctrl"))))
(defn shortcut-binding
"override by user custom binding"
[id]
(let [shortcut (get (state/shortcuts) id
(get (get-bindings) id))]
(let [shortcut (get (get-bindings) id)]
(cond
(nil? shortcut)
(log/warn :shortcut/binding-not-found {:id id})
@@ -47,62 +95,48 @@
:else
(->>
(if (string? shortcut)
[shortcut]
shortcut)
(mapv mod-key)))))
(if (string? shortcut)
[shortcut]
shortcut)
(mapv mod-key)))))
(defn shortcut-cmd
[id]
(get @shortcut-config/*shortcut-cmds id))
(defn shortcut-item
[id]
(get (get-bindings-ids-map) id))
;; returns a vector to preserve order
(defn binding-by-category [name]
(let [dict (->> (vals @shortcut-config/config)
(apply merge)
(map (fn [[k _]]
{k {:binding (shortcut-binding k)}}))
(into {}))
(let [dict (get-bindings-ids-map)
plugin? (= name :shortcut.category/plugins)]
(->> (if plugin?
(->> (keys dict) (filter #(str/starts-with? (str %) ":plugin.")))
(shortcut-config/category name))
(mapv (fn [k] [k (k dict)])))))
(shortcut-config/get-category-shortcuts name))
(mapv (fn [k] [k (assoc (get dict k) :category name)])))))
(defn shortcut-map
([handler-id]
(shortcut-map handler-id nil))
([handler-id state]
(let [raw (get @shortcut-config/config handler-id)
(let [raw (get @shortcut-config/*config handler-id)
handler-m (->> raw
(map (fn [[k {:keys [fn]}]]
{k fn}))
(into {}))
before (-> raw meta :before)]
before (-> raw meta :before)]
(cond->> handler-m
state (reduce-kv (fn [r k handle-fn]
(assoc r k (partial handle-fn state)))
{})
before (reduce-kv (fn [r k v]
(assoc r k (before v)))
{})))))
(defn decorate-namespace [k]
(let [n (name k)
ns (namespace k)]
(keyword (str "command." ns) n)))
(defn decorate-binding [binding]
(-> (if (string? binding) binding (str/join "+" binding))
(str/replace "mod" (if util/mac? "⌘" "ctrl"))
(str/replace "alt" (if util/mac? "opt" "alt"))
(str/replace "shift+/" "?")
(str/replace "left" "←")
(str/replace "right" "→")
(str/replace "shift" "⇧")
(str/replace "open-square-bracket" "[")
(str/replace "close-square-bracket" "]")
(str/lower-case)))
state (reduce-kv (fn [r k handle-fn]
(let [handle-fn' (if (volatile? state)
(fn [*state & args] (apply handle-fn (cons @*state args)))
handle-fn)]
(assoc r k (partial handle-fn' state))))
{})
before (reduce-kv (fn [r k v]
(assoc r k (before v)))
{})))))
;; if multiple bindings, gen seq for first binding only for now
(defn gen-shortcut-seq [id]
@@ -111,24 +145,24 @@
[]
(-> bindings
first
(str/split #" |\+")))))
(str/split #" |\+")))))
(defn binding-for-display [k binding]
(let [tmp (cond
(false? binding)
(cond
(and util/mac? (= k :editor/kill-line-after)) "system default: ctrl+k"
(and util/mac? (= k :editor/kill-line-after)) "system default: ctrl+k"
(and util/mac? (= k :editor/beginning-of-block)) "system default: ctrl+a"
(and util/mac? (= k :editor/end-of-block)) "system default: ctrl+e"
(and util/mac? (= k :editor/end-of-block)) "system default: ctrl+e"
(and util/mac? (= k :editor/backward-kill-word)) "system default: opt+delete"
:else "disabled")
:else (t :keymap/disabled))
(string? binding)
(decorate-binding binding)
(shortcut-utils/decorate-binding binding)
:else
(->> binding
(map decorate-binding)
(map shortcut-utils/decorate-binding)
(str/join " | ")))]
;; Display "cmd" rather than "meta" to the user to describe the Mac
@@ -157,26 +191,92 @@
"Given shortcut key, return handler group
eg: :editor/new-line -> :shortcut.handler/block-editing-only"
[k]
(->> @shortcut-config/config
(->> @shortcut-config/*config
(filter (fn [[_ v]] (contains? v k)))
(map key)
(first)))
(defn potential-conflict? [k]
(if-not (shortcut-binding k)
(defn should-be-included-to-global-handler
[from-handler-id]
(if (contains? #{:shortcut.handler/pdf} from-handler-id)
#{from-handler-id :shortcut.handler/global-prevent-default}
#{from-handler-id}))
(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}
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)
same-leading-key?
(fn [[k' _]]
(when (sequential? k)
(or (= k k')
(and (> (count k') (count k))
(= (first k) (first k'))))))
into-conflict-refs
(fn [[k o]]
(when-let [{:keys [key refs]} o]
[k [key (reduce-kv (fn [r id handler-id']
(if (and
(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'))))
(assoc r id handler-id')
r)
) {} refs)]]))]
[k' (->> ks-bindings
(filterv same-leading-key?)
(mapv into-conflict-refs)
(remove #(empty? (second (second %1))))
(into {}))]
))))
(remove #(empty? (vals (second %1))))
(into {})))))
(defn parse-conflicts-from-binding
[from-binding target]
(when-let [from-binding (and (string? target)
(sequential? from-binding)
(seq from-binding))]
(when-let [target (some-> target (mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
(->> from-binding
(filterv
#(when-let [from (some-> % (mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
(or (= from target)
(and (or (= (count from) 1)
(= (count target) 1))
(= (first target) (first from))))))))))
(defn potential-conflict? [shortcut-id]
(if-not (shortcut-binding shortcut-id)
false
(let [handler-id (get-group k)
shortcut-m (shortcut-map handler-id)
(let [handler-id (get-group shortcut-id)
shortcut-m (shortcut-map handler-id)
parse-shortcut #(try
(KeyboardShortcutHandler/parseStringShortcut %)
(catch :default e
(js/console.error "[shortcut/parse-error]" (str % " - " (.-message e)))))
bindings (->> (shortcut-binding k)
(map mod-key)
(map parse-shortcut)
(map js->clj))
(KeyboardShortcutHandler/parseStringShortcut %)
(catch :default e
(js/console.error "[shortcut/parse-error]" (str % " - " (.-message e)))))
bindings (->> (shortcut-binding shortcut-id)
(map mod-key)
(map parse-shortcut)
(map js->clj))
rest-bindings (->> (map key shortcut-m)
(remove #{k})
(remove #{shortcut-id})
(map shortcut-binding)
(filter vector?)
(mapcat identity)
@@ -188,16 +288,16 @@
(defn shortcut-data-by-id [id]
(let [binding (shortcut-binding id)
data (->> (vals @shortcut-config/config)
(into {})
id)]
data (->> (vals @shortcut-config/*config)
(into {})
id)]
(assoc
data
:binding
(binding-for-display id binding))))
(defn shortcuts->commands [handler-id]
(let [m (get @shortcut-config/config handler-id)]
(let [m (get @shortcut-config/*config handler-id)]
(->> m
(map (fn [[id _]] (-> (shortcut-data-by-id id)
(assoc :id id :handler-id handler-id)

View File

@@ -0,0 +1,58 @@
(ns frontend.modules.shortcut.utils
(:require [clojure.string :as str]
[frontend.util :as util])
(:import [goog.ui KeyboardShortcutHandler]))
(defn safe-parse-string-binding
[binding]
(try
(KeyboardShortcutHandler/parseStringShortcut binding)
(catch js/Error e
(js/console.warn "[shortcuts] parse key error: " e) binding)))
(defn mod-key [binding]
(str/replace binding #"(?i)mod"
(if util/mac? "meta" "ctrl")))
(defn undecorate-binding
[binding]
(when (string? binding)
(let [keynames {";" "semicolon"
"=" "equals"
"-" "dash"
"[" "open-square-bracket"
"]" "close-square-bracket"
"'" "single-quote"
"(" "shift+9"
")" "shift+0"
"~" "shift+`"
"⇧" "shift"
"←" "left"
"→" "right"}]
(-> binding
(str/replace #"[;=-\[\]'\(\)\~\→\←\⇧]" #(get keynames %))
(str/replace #"\s+" " ")
(mod-key)
(str/lower-case)))))
(defn decorate-namespace [k]
(let [n (name k)
ns (namespace k)]
(keyword (str "command." ns) n)))
(defn decorate-binding [binding]
(when (or (string? binding)
(sequential? binding))
(-> (if (string? binding) binding (str/join "+" binding))
(str/replace "mod" (if util/mac? "⌘" "ctrl"))
(str/replace "meta" (if util/mac? "⌘" "⊞ win"))
(str/replace "alt" (if util/mac? "opt" "alt"))
(str/replace "shift+/" "?")
(str/replace "left" "←")
(str/replace "right" "→")
(str/replace "shift" "⇧")
(str/replace "open-square-bracket" "[")
(str/replace "close-square-bracket" "]")
(str/replace "equals" "=")
(str/replace "semicolon" ";")
(str/lower-case))))

View File

@@ -9,7 +9,6 @@
[frontend.components.repo :as repo]
[frontend.components.search :as search]
[frontend.components.settings :as settings]
[frontend.components.shortcut :as shortcut]
[frontend.components.whiteboard :as whiteboard]
[frontend.extensions.zotero :as zotero]
[frontend.components.bug-report :as bug-report]
@@ -69,10 +68,6 @@
{:name :settings
:view settings/settings}]
["/settings/shortcut"
{:name :shortcut-setting
:view shortcut/shortcut-page}]
["/settings/zotero"
{:name :zotero-setting
:view zotero/settings}]

View File

@@ -61,6 +61,7 @@
:modal/label ""
:modal/show? false
:modal/panel-content nil
:modal/payload nil
:modal/fullscreen? false
:modal/close-btn? nil
:modal/close-backdrop? true
@@ -344,6 +345,18 @@
(merge current new)
new)))))
(defn get-global-config
[]
(get-in @state [:config ::global-config]))
(defn get-global-config-str-content
[]
(get-in @state [:config ::global-config-str-content]))
(defn get-graph-config
([] (get-graph-config (get-current-repo)))
([repo-url] (get-in @state [:config repo-url])))
(defn get-config
"User config for the given repo or current repo if none given. All config fetching
should be done through this fn in order to get global config and config defaults"
@@ -352,8 +365,8 @@ should be done through this fn in order to get global config and config defaults
([repo-url]
(merge-configs
default-config
(get-in @state [:config ::global-config])
(get-in @state [:config repo-url]))))
(get-global-config)
(get-graph-config repo-url))))
(defonce publishing? (atom nil))
@@ -1341,7 +1354,7 @@ Similar to re-frame subscriptions"
([panel-content]
(set-sub-modal! panel-content
{:close-btn? true}))
([panel-content {:keys [id label close-btn? close-backdrop? show? center?] :as opts}]
([panel-content {:keys [id label payload close-btn? close-backdrop? show? center?] :as opts}]
(if (not (modal-opened?))
(set-modal! panel-content opts)
(let [modals (:modal/subsets @state)
@@ -1351,6 +1364,7 @@ Similar to re-frame subscriptions"
#(not (nil? %1))
{:modal/id id
:modal/label (or label (if center? "ls-modal-align-center" ""))
:modal/payload payload
:modal/show? (if (boolean? show?) show? true)
:modal/panel-content panel-content
:modal/close-btn? close-btn?
@@ -1380,7 +1394,7 @@ Similar to re-frame subscriptions"
(set-modal! modal-panel-content
{:fullscreen? false
:close-btn? true}))
([modal-panel-content {:keys [id label fullscreen? close-btn? close-backdrop? center?]}]
([modal-panel-content {:keys [id label payload fullscreen? close-btn? close-backdrop? center?]}]
(let [opened? (modal-opened?)]
(when opened?
(close-modal!))
@@ -1395,6 +1409,7 @@ Similar to re-frame subscriptions"
:modal/label (or label (if center? "ls-modal-align-center" ""))
:modal/show? (boolean modal-panel-content)
:modal/panel-content modal-panel-content
:modal/payload payload
:modal/fullscreen? fullscreen?
:modal/close-btn? close-btn?
:modal/close-backdrop? (if (boolean? close-backdrop?) close-backdrop? true))))
@@ -1408,6 +1423,7 @@ Similar to re-frame subscriptions"
(swap! state assoc
:modal/id nil
:modal/label ""
:modal/payload nil
:modal/show? false
:modal/fullscreen? false
:modal/panel-content nil
@@ -1477,9 +1493,11 @@ Similar to re-frame subscriptions"
(when value (set-state! [:config repo-url] value)))
(defn set-global-config!
[value]
[value str-content]
;; Placed under :config so cursors can work seamlessly
(when value (set-config! ::global-config value)))
(when value
(set-config! ::global-config value)
(set-config! ::global-config-str-content str-content)))
(defn get-wide-mode?
[]
@@ -1499,13 +1517,13 @@ Similar to re-frame subscriptions"
(defn get-plugins-commands-with-type
[type]
(filterv #(= (keyword (first %)) (keyword type))
(apply concat (vals (:plugin/simple-commands @state)))))
(->> (apply concat (vals (:plugin/simple-commands @state)))
(filterv #(= (keyword (first %)) (keyword type)))))
(defn get-plugins-ui-items-with-type
[type]
(filterv #(= (keyword (first %)) (keyword type))
(apply concat (vals (:plugin/installed-ui-items @state)))))
(->> (apply concat (vals (:plugin/installed-ui-items @state)))
(filterv #(= (keyword (first %)) (keyword type)))))
(defn get-plugin-resources-with-type
[pid type]
@@ -1734,8 +1752,8 @@ Similar to re-frame subscriptions"
(set-state! :ui/settings-open? false))
(defn open-settings!
[]
(set-state! :ui/settings-open? true))
([] (open-settings! true))
([active-tab] (set-state! :ui/settings-open? active-tab)))
;; TODO: Move those to the uni `state`

View File

@@ -21,7 +21,7 @@
[frontend.mobile.util :as mobile-util]
[frontend.modules.shortcut.config :as shortcut-config]
[frontend.modules.shortcut.core :as shortcut]
[frontend.modules.shortcut.data-helper :as shortcut-helper]
[frontend.modules.shortcut.utils :as shortcut-utils]
[frontend.rum :as r]
[frontend.state :as state]
[frontend.storage :as storage]
@@ -167,7 +167,7 @@
sequence)]
[:span.keyboard-shortcut
(map-indexed (fn [i key]
(let [key' (shortcut-helper/decorate-binding (str key))]
(let [key' (shortcut-utils/decorate-binding (str key))]
[:code {:key i}
;; Display "cmd" rather than "meta" to the user to describe the Mac
;; mod key, because that's what the Mac keyboards actually say.
@@ -507,7 +507,7 @@
(rum/defcs auto-complete <
(rum/local 0 ::current-idx)
(shortcut/mixin :shortcut.handler/auto-complete)
(shortcut/mixin* :shortcut.handler/auto-complete)
[state
matched
{:keys [on-chosen
@@ -567,9 +567,10 @@
:aria-hidden "true"}]]]))
(defn keyboard-shortcut-from-config [shortcut-name]
(let [default-binding (:binding (get shortcut-config/all-default-keyboard-shortcuts shortcut-name))
custom-binding (when (state/shortcuts) (get (state/shortcuts) shortcut-name))]
(or custom-binding default-binding)))
(let [built-in-binding (:binding (get shortcut-config/all-built-in-keyboard-shortcuts shortcut-name))
custom-binding (when (state/shortcuts) (get (state/shortcuts) shortcut-name))
binding (or custom-binding built-in-binding)]
(shortcut-utils/decorate-binding binding)))
(rum/defc modal-overlay
[state close-fn close-backdrop?]

View File

@@ -172,7 +172,7 @@
{:init (fn [state]
(reset! *internal-model (first (:rum/args state)))
state)}
(shortcut/mixin :shortcut.handler/date-picker)
(shortcut/mixin :shortcut.handler/date-picker false)
[_model {:keys [on-change disabled? start-of-week class style attr]
:or {start-of-week (state/get-start-of-week)} ;; Default to Sunday
:as args}]

View File

@@ -67,19 +67,6 @@
[parts]
(string/join "/" parts))
(defn normalize-user-keyname
[k]
(let [keynames {";" "semicolon"
"=" "equals"
"-" "dash"
"[" "open-square-bracket"
"]" "close-square-bracket"
"'" "single-quote"}]
(some-> (str k)
(string/lower-case)
(string/replace #"[;=-\[\]']" (fn [s]
(get keynames s))))))
#?(:cljs
(defn safe-re-find
{:malli/schema [:=> [:cat :any :string] [:or :nil :string [:vector [:maybe :string]]]]}

View File

@@ -367,9 +367,9 @@
(if palette?
(palette-handler/invoke-command palette-cmd)
(action')))
[handler-id id shortcut-map] (update shortcut-args 2 assoc :fn dispatch-cmd :cmd palette-cmd)]
(println :shortcut/register-shortcut [handler-id id shortcut-map])
(st/register-shortcut! handler-id id shortcut-map)))))))
[mode-id id shortcut-map] (update shortcut-args 2 merge cmd {:fn dispatch-cmd :cmd palette-cmd})]
(println :shortcut/register-shortcut [mode-id id shortcut-map])
(st/register-shortcut! mode-id id shortcut-map)))))))
(defn ^:export unregister_plugin_simple_command
[pid]
@@ -422,7 +422,7 @@
(util/safe-lower-case)
(keyword)))]
(when-let [action (get-in (palette-handler/get-commands-unique) [id :action])]
(apply action args)))))
(apply plugin-handler/hook-lifecycle-fn! id action args)))))
;; flag - boolean | 'toggle'
(def ^:export set_left_sidebar_visible

View File

@@ -320,6 +320,7 @@
:settings-page/current-version "Current version"
:settings-page/tab-general "General"
:settings-page/tab-editor "Editor"
:settings-page/tab-keymap "Keymap"
:settings-page/tab-version-control "Version control"
:settings-page/tab-account "Account"
:settings-page/tab-advanced "Advanced"
@@ -359,6 +360,7 @@
:close "Close"
:delete "Delete"
:save "Save"
:reset "Reset"
:type "Type"
:host "Host"
:port "Port"
@@ -636,9 +638,23 @@
:shortcut.category/block-command-editing "Block command editing"
:shortcut.category/block-selection "Block selection (press Esc to quit selection)"
:shortcut.category/toggle "Toggle"
:shortcut.category/whiteboard "Whiteboard"
:shortcut.category/others "Others"
:shortcut.category/plugins "Plugins"
:shortcut.category/whiteboard "Whiteboard"
:keymap/all "All"
:keymap/disabled "Disabled"
:keymap/unset "Unset"
:keymap/custom "Custom"
:keymap/search "Search"
:keymap/total "Total shortcuts"
:keymap/keystroke-filter "Keystroke filter"
:keymap/keystroke-record-desc "Press any sequence of keys to filter shortcuts"
:keymap/keystroke-record-setup-label "Press any sequence of keys to set a shortcut"
:keymap/restore-to-default "Restore to system default"
:keymap/customize-for-label "Customize shortcuts"
:keymap/conflicts-for-label "Keymap conflicts for"
:window/minimize "Minimize"
:window/maximize "Maximize"
:window/restore "Restore"

View File

@@ -233,6 +233,7 @@
:settings-page/show-full-blocks "显示块引用的所有行"
:settings-page/tab-general "常规"
:settings-page/tab-editor "编辑器"
:settings-page/tab-keymap "快捷键"
:settings-page/tab-assets "附件设置"
:settings-page/tab-advanced "高级设置"
:settings-page/tab-features "更多功能"
@@ -293,6 +294,7 @@
:close "关闭"
:delete "删除"
:save "保存"
:reset "重设"
:type "类型"
:host "主机"
:port "端口"
@@ -486,6 +488,22 @@
:shortcut.category/block-selection "块选择操作"
:shortcut.category/toggle "切换"
:shortcut.category/others "其他"
:shortcut.category/plugins "插件"
:shortcut.category/whiteboard "白板"
:keymap/all "全部"
:keymap/disabled "已禁用"
:keymap/unset "未设置"
:keymap/custom "自定义"
:keymap/search "搜索"
:keymap/total "共计条目"
:keymap/keystroke-filter "按键过滤"
:keymap/keystroke-record-desc "请敲击键盘提供按键组合, 以过滤快捷键。"
:keymap/keystroke-record-setup-label "请敲击键盘提供按键组合"
:keymap/restore-to-default "恢复到默认映射"
:keymap/customize-for-label "自定义快捷键"
:keymap/conflicts-for-label "发现映射冲突 "
:command.auto-complete/complete "自动完成:选择当前项"
:command.auto-complete/next "自动完成:选择下一项"
:command.auto-complete/open-link "自动完成:在浏览器中打开当前项"

View File

@@ -0,0 +1,49 @@
(ns frontend.modules.shortcut.core-test
(:require [cljs.test :refer [deftest is testing]]
[clojure.string :as string]
[frontend.modules.shortcut.data-helper :as dh]
[frontend.util :as util]))
(deftest test-core-basic
(testing "get handler id"
(is (= (dh/get-group :editor/copy) :shortcut.handler/editor-global))))
(deftest test-shortcut-conflicts-detection
(testing "get conflicts with shortcut id")
(testing "get conflicts with binding keys"
(is (= (count (dh/get-conflicts-by-keys "mod+c")) 1))
(is (contains?
(->> (dh/get-conflicts-by-keys
"mod+c" :shortcut.handler/editor-global
{:exclude-ids #{:editor/copy} :group-global? true})
(vals) (mapcat #(vals %)) (some #(when (= (first %) (if util/mac? "meta+c" "ctrl+c")) (second %))))
:misc/copy))
(is (->> (dh/get-conflicts-by-keys ["t"])
(vals)
(first)
(vals)
(map first)
(every? #(string/starts-with? % "t")))
"get the conflicts from the only leader key")
(is (nil? (seq (dh/get-conflicts-by-keys ["g"] :shortcut.handler/cards)))
"specific handler with the global conflicting key"))
(testing "parse conflicts from the string binding list"
(is (= (dh/parse-conflicts-from-binding ["g" "g t"] "g")
["g" "g t"]))
(is (= (dh/parse-conflicts-from-binding ["g" "g t" "t r"] "g t")
["g" "g t"]))
(is (= (dh/parse-conflicts-from-binding ["g" "g t" "t r"] "g x")
["g"]))
(is (= (dh/parse-conflicts-from-binding ["meta+x" "meta+x t" "t r"] "meta+x x")
["meta+x"]))))
(comment
(cljs.test/run-tests))

View File

@@ -25,9 +25,9 @@
(deftest test-memoize-last
(testing "memoize-last add test"
(let [actual-ops (atom 0)
m+ (util/memoize-last (fn [x1 x2]
(swap! actual-ops inc) ;; side effect for counting
(+ x1 x2)))]
m+ (util/memoize-last (fn [x1 x2]
(swap! actual-ops inc) ;; side effect for counting
(+ x1 x2)))]
(is (= (m+ 1 1) 2))
(is (= @actual-ops 1))
(is (= (m+ 1 1) 2))
@@ -44,58 +44,58 @@
(testing "memoize-last nested mapping test"
(let [actual-ops (atom 0)
flatten-f (util/memoize-last (fn [& args]
(swap! actual-ops inc) ;; side effect for counting
(apply #'shortcut-data-helper/flatten-key-bindings args)))
target (atom {:part1 {:date-picker/complete {:binding "enter"
:fn "ui-handler/shortcut-complete"}
:date-picker/prev-day {:binding "left"
:fn "ui-handler/shortcut-prev-day"}}
:part2 {:date-picker/next-day {:binding "right"
:fn "ui-handler/shortcut-next-day"}
:date-picker/prev-week {:binding ["up" "ctrl+p"]
:fn "ui-handler/shortcut-prev-week"}}})]
(is (= (flatten-f (vals @target)) {:date-picker/complete "enter"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
flatten-f (util/memoize-last (fn [& args]
(swap! actual-ops inc) ;; side effect for counting
(apply #'shortcut-data-helper/flatten-bindings-by-id (conj (vec args) nil true))))
target (atom {:part1 {:date-picker/complete {:binding "enter"
:fn "ui-handler/shortcut-complete"}
:date-picker/prev-day {:binding "left"
:fn "ui-handler/shortcut-prev-day"}}
:part2 {:date-picker/next-day {:binding "right"
:fn "ui-handler/shortcut-next-day"}
:date-picker/prev-week {:binding ["up" "ctrl+p"]
:fn "ui-handler/shortcut-prev-week"}}})]
(is (= (flatten-f @target) {:date-picker/complete "enter"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= @actual-ops 1))
(is (= (flatten-f (vals @target)) {:date-picker/complete "enter"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= (flatten-f @target) {:date-picker/complete "enter"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= @actual-ops 1))
;; edit value
(swap! target assoc-in [:part1 :date-picker/complete :binding] "tab")
(is (= (flatten-f (vals @target)) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= (flatten-f @target) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= @actual-ops 2))
(is (= (flatten-f (vals @target)) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= (flatten-f @target) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= @actual-ops 2))
(is (= (flatten-f (vals @target)) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= (flatten-f @target) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]}))
(is (= @actual-ops 2))
;; edit key
(swap! target assoc :part3 {:date-picker/next-week {:binding "down"
:fn "ui-handler/shortcut-next-week"}})
(is (= (flatten-f (vals @target)) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]
:date-picker/next-week "down"}))
(is (= (flatten-f @target) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]
:date-picker/next-week "down"}))
(is (= @actual-ops 3))
(is (= (flatten-f (vals @target)) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]
:date-picker/next-week "down"}))
(is (= (flatten-f @target) {:date-picker/complete "tab"
:date-picker/prev-day "left"
:date-picker/next-day "right"
:date-picker/prev-week ["up" "ctrl+p"]
:date-picker/next-week "down"}))
(is (= @actual-ops 3)))))
(deftest test-media-format-from-input