shortcut.cljs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. (ns frontend.components.shortcut
  2. (:require [clojure.string :as string]
  3. [rum.core :as rum]
  4. [frontend.context.i18n :refer [t]]
  5. [cljs-bean.core :as bean]
  6. [frontend.state :as state]
  7. [frontend.search :as search]
  8. [frontend.ui :as ui]
  9. [frontend.rum :as r]
  10. [goog.events :as events]
  11. [promesa.core :as p]
  12. [frontend.handler.notification :as notification]
  13. [frontend.modules.shortcut.core :as shortcut]
  14. [frontend.modules.shortcut.data-helper :as dh]
  15. [frontend.util :as util]
  16. [frontend.modules.shortcut.utils :as shortcut-utils]
  17. [frontend.modules.shortcut.config :as shortcut-config]
  18. [logseq.shui.core :as shui]
  19. [frontend.shui :refer [make-shui-context]])
  20. (:import [goog.events KeyHandler]))
  21. (defonce categories
  22. (vector :shortcut.category/basics
  23. :shortcut.category/navigating
  24. :shortcut.category/block-editing
  25. :shortcut.category/block-command-editing
  26. :shortcut.category/block-selection
  27. :shortcut.category/formatting
  28. :shortcut.category/toggle
  29. :shortcut.category/whiteboard
  30. :shortcut.category/plugins
  31. :shortcut.category/others))
  32. (defonce *refresh-sentry (atom 0))
  33. (defn refresh-shortcuts-list! [] (reset! *refresh-sentry (inc @*refresh-sentry)))
  34. (defonce *global-listener-setup? (atom false))
  35. (defonce *customize-modal-life-sentry (atom 0))
  36. (defn- to-vector [v]
  37. (when-not (nil? v)
  38. (if (sequential? v) (vec v) [v])))
  39. (declare customize-shortcut-dialog-inner)
  40. (rum/defc keyboard-filter-record-inner
  41. [keystroke set-keystroke! close-fn]
  42. (let [keypressed? (not= "" keystroke)]
  43. (rum/use-effect!
  44. (fn []
  45. (let [key-handler (KeyHandler. js/document)]
  46. ;; setup
  47. (util/profile
  48. "[shortcuts] unlisten*"
  49. (shortcut/unlisten-all! true))
  50. (events/listen key-handler "key"
  51. (fn [^js e]
  52. (.preventDefault e)
  53. (set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
  54. ;; teardown
  55. #(do
  56. (util/profile
  57. "[shortcuts] listen*"
  58. (shortcut/listen-all!))
  59. (.dispose key-handler))))
  60. [])
  61. [:div.keyboard-filter-record
  62. [:h2
  63. [:strong (t :keymap/keystroke-filter)]
  64. [:span.flex.space-x-2
  65. (when keypressed?
  66. [:a.flex.items-center
  67. {:on-click #(set-keystroke! "")} (ui/icon "zoom-reset" {:size 12})])
  68. [:a.flex.items-center
  69. {:on-click #(do (close-fn) (set-keystroke! ""))} (ui/icon "x" {:size 12})]]]
  70. [:div.wrap.p-2
  71. (if-not keypressed?
  72. [:small (t :keymap/keystroke-record-desc)]
  73. (when-not (string/blank? keystroke)
  74. (ui/render-keyboard-shortcut [keystroke])))]]))
  75. (rum/defc pane-controls
  76. [q set-q! filters set-filters! keystroke set-keystroke! toggle-categories-fn]
  77. (let [*search-ref (rum/use-ref nil)]
  78. [:div.cp__shortcut-page-x-pane-controls
  79. [:a.flex.items-center.icon-link
  80. {:on-click toggle-categories-fn
  81. :title "Toggle categories pane"}
  82. (ui/icon "fold")]
  83. [:a.flex.items-center.icon-link
  84. {:on-click refresh-shortcuts-list!
  85. :title "Refresh all"}
  86. (ui/icon "refresh")]
  87. [:span.search-input-wrap
  88. [:input.form-input.is-small
  89. {:placeholder (t :keymap/search)
  90. :ref *search-ref
  91. :value (or q "")
  92. :auto-focus true
  93. :on-key-down #(when (= 27 (.-keyCode %))
  94. (util/stop %)
  95. (if (string/blank? q)
  96. (some-> (rum/deref *search-ref) (.blur))
  97. (set-q! "")))
  98. :on-change #(let [v (util/evalue %)]
  99. (set-q! v))}]
  100. (when-not (string/blank? q)
  101. [:a.x
  102. {:on-click (fn []
  103. (set-q! "")
  104. (js/setTimeout #(some-> (rum/deref *search-ref) (.focus)) 50))}
  105. (ui/icon "x" {:size 14})])]
  106. ;; keyboard filter
  107. (ui/dropdown
  108. (fn [{:keys [toggle-fn]}]
  109. [:a.flex.items-center.icon-link
  110. {:on-click toggle-fn} (ui/icon "keyboard")
  111. (when-not (string/blank? keystroke)
  112. (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
  113. (fn [{:keys [close-fn]}]
  114. (keyboard-filter-record-inner keystroke set-keystroke! close-fn))
  115. {:outside? true
  116. :trigger-class "keyboard-filter"})
  117. ;; other filter
  118. (ui/dropdown-with-links
  119. (fn [{:keys [toggle-fn]}]
  120. [:a.flex.items-center.icon-link.relative
  121. {:on-click toggle-fn}
  122. (ui/icon "filter")
  123. (when (seq filters)
  124. (ui/point "bg-red-600.absolute" 4 {:style {:right -2 :top -2}}))])
  125. (for [k [:All :Disabled :Unset :Custom]
  126. :let [all? (= k :All)
  127. checked? (or (contains? filters k) (and all? (nil? (seq filters))))]]
  128. {:title (if all? (t :keymap/all) (t (keyword :keymap (string/lower-case (name k)))))
  129. :icon (ui/icon (if checked? "checkbox" "square"))
  130. :options {:on-click #(set-filters! (if all? #{} (let [f (if checked? disj conj)] (f filters k))))}})
  131. nil)]))
  132. (rum/defc shortcut-desc-label
  133. [id binding-map]
  134. (when-let [id' (and id binding-map (some-> (str id) (string/replace "plugin." "")))]
  135. [:span {:title (str id' "#" (some-> (:handler-id binding-map) (name)))}
  136. [:span.pl-1 (dh/get-shortcut-desc (assoc binding-map :id id))]
  137. [:small.pl-1 [:code.text-xs (str id')]]]))
  138. (defn- open-customize-shortcut-dialog!
  139. [id]
  140. (when-let [{:keys [binding user-binding] :as m} (dh/shortcut-item id)]
  141. (let [binding (to-vector binding)
  142. user-binding (and user-binding (to-vector user-binding))
  143. modal-id (str :customize-shortcut id)
  144. label (shortcut-desc-label id m)
  145. args [id label binding user-binding
  146. {:saved-cb (fn [] (-> (p/delay 500) (p/then refresh-shortcuts-list!)))
  147. :modal-id modal-id}]]
  148. (state/set-sub-modal!
  149. (fn [] (apply customize-shortcut-dialog-inner args))
  150. {:center? true
  151. :id modal-id
  152. :payload args}))))
  153. (rum/defc shortcut-conflicts-display
  154. [_k conflicts-map]
  155. [:div.cp__shortcut-conflicts-list-wrap
  156. (for [[g ks] conflicts-map]
  157. [:section.relative
  158. [:h2 (ui/icon "alert-triangle" {:size 15})
  159. [:span (t :keymap/conflicts-for-label)]
  160. [:code (shortcut-utils/decorate-binding g)]]
  161. [:ul
  162. (for [v (vals ks)
  163. :let [k (first v)
  164. vs (second v)]]
  165. (for [[id' handler-id] vs
  166. :let [m (dh/shortcut-item id')]
  167. :when (not (nil? m))]
  168. [:li
  169. {:key (str id')}
  170. [:a.select-none.hover:underline
  171. {:on-click #(open-customize-shortcut-dialog! id')
  172. :title (str handler-id)}
  173. [:code.inline-block.mr-1.text-xs
  174. (shortcut-utils/decorate-binding k)]
  175. [:span
  176. (dh/get-shortcut-desc m)
  177. (ui/icon "external-link" {:size 18})]
  178. [:code [:small (str id')]]]]))]])])
  179. (rum/defc ^:large-vars/cleanup-todo customize-shortcut-dialog-inner
  180. [k action-name binding user-binding {:keys [saved-cb modal-id]}]
  181. (let [*ref-el (rum/use-ref nil)
  182. [modal-life _] (r/use-atom *customize-modal-life-sentry)
  183. [keystroke set-keystroke!] (rum/use-state "")
  184. [current-binding set-current-binding!] (rum/use-state (or user-binding binding))
  185. [key-conflicts set-key-conflicts!] (rum/use-state nil)
  186. handler-id (rum/use-memo #(dh/get-group k))
  187. dirty? (not= (or user-binding binding) current-binding)
  188. keypressed? (not= "" keystroke)
  189. save-keystroke-fn!
  190. (fn []
  191. ;; parse current binding conflicts
  192. (if-let [current-conflicts (seq (dh/parse-conflicts-from-binding current-binding keystroke))]
  193. (notification/show!
  194. (str "Shortcut conflicts from existing binding: "
  195. (pr-str (some->> current-conflicts (map #(shortcut-utils/decorate-binding %)))))
  196. :error true :shortcut-conflicts/warning 5000)
  197. ;; get conflicts from the existed bindings map
  198. (let [conflicts-map (dh/get-conflicts-by-keys keystroke handler-id)]
  199. (if-not (seq conflicts-map)
  200. (do (set-current-binding! (conj current-binding keystroke))
  201. (set-keystroke! "")
  202. (set-key-conflicts! nil))
  203. ;; show conflicts
  204. (set-key-conflicts! conflicts-map)))))]
  205. (rum/use-effect!
  206. (fn []
  207. (let [mid (state/sub :modal/id)
  208. mid' (some-> (state/sub :modal/subsets) (last) (:modal/id))
  209. el (rum/deref *ref-el)]
  210. (when (or (and (not mid') (= mid modal-id))
  211. (= mid' modal-id))
  212. (some-> el (.focus))
  213. (js/setTimeout
  214. #(some-> (.querySelector el ".shortcut-record-control a.submit")
  215. (.click)) 200))))
  216. [modal-life])
  217. (rum/use-effect!
  218. (fn []
  219. (let [^js el (rum/deref *ref-el)
  220. key-handler (KeyHandler. el)
  221. teardown-global!
  222. (when-not @*global-listener-setup?
  223. (shortcut/unlisten-all! true)
  224. (reset! *global-listener-setup? true)
  225. (fn []
  226. (shortcut/listen-all!)
  227. (reset! *global-listener-setup? false)))]
  228. ;; setup
  229. (events/listen key-handler "key"
  230. (fn [^js e]
  231. (.preventDefault e)
  232. (set-key-conflicts! nil)
  233. (set-keystroke! #(util/trim-safe (str % (shortcut/keyname e))))))
  234. ;; active
  235. (.focus el)
  236. ;; teardown
  237. #(do (some-> teardown-global! (apply nil))
  238. (.dispose key-handler)
  239. (swap! *customize-modal-life-sentry inc))))
  240. [])
  241. [:div.cp__shortcut-page-x-record-dialog-inner
  242. {:class (util/classnames [{:keypressed keypressed? :dirty dirty?}])
  243. :tab-index -1
  244. :ref *ref-el}
  245. [:div.sm:w-lsm
  246. [:h1.text-2xl.pb-2
  247. (t :keymap/customize-for-label)]
  248. [:p.mb-4.text-md [:b action-name]]
  249. [:div.shortcuts-keys-wrap
  250. [:span.keyboard-shortcut.flex.flex-wrap.mr-2.space-x-2
  251. (for [x current-binding
  252. :when (string? x)]
  253. [:code.tracking-wider
  254. (-> x (string/trim) (string/lower-case) (shortcut-utils/decorate-binding))
  255. [:a.x {:on-click (fn [] (set-current-binding!
  256. (->> current-binding (remove #(= x %)) (into []))))}
  257. (ui/icon "x" {:size 12})]])]
  258. ;; add shortcut
  259. [:div.shortcut-record-control
  260. ;; keypressed state
  261. (if keypressed?
  262. [:<>
  263. (when-not (string/blank? keystroke)
  264. (ui/render-keyboard-shortcut [keystroke]))
  265. [:a.flex.items-center.active:opacity-90.submit
  266. {:on-click save-keystroke-fn!}
  267. (ui/icon "check" {:size 14})]
  268. [:a.flex.items-center.text-red-600.hover:text-red-700.active:opacity-90.cancel
  269. {:on-click (fn []
  270. (set-keystroke! "")
  271. (set-key-conflicts! nil))}
  272. (ui/icon "x" {:size 14})]]
  273. [:code.flex.items-center
  274. [:small.pr-1 (t :keymap/keystroke-record-setup-label)] (ui/icon "keyboard" {:size 14})])]]]
  275. ;; conflicts results
  276. (when (seq key-conflicts)
  277. (shortcut-conflicts-display k key-conflicts))
  278. [:div.action-btns.text-right.mt-6.flex.justify-between.items-center
  279. ;; restore default
  280. (if (and dirty? (or user-binding binding))
  281. [:a.flex.items-center.space-x-1.text-sm.fade-link
  282. {:on-click #(set-current-binding! (or user-binding binding))}
  283. (t :keymap/restore-to-default)
  284. (for [it (some->> (or binding user-binding) (map #(some->> % (dh/mod-key) (shortcut-utils/decorate-binding))))]
  285. [:span.keyboard-shortcut.ml-1 [:code it]])]
  286. [:div])
  287. [:div.flex.flex-row.items-center.gap-2
  288. (ui/button
  289. (t :save)
  290. :disabled (not dirty?)
  291. :on-click (fn []
  292. ;; TODO: check conflicts for the single same leader key
  293. (let [binding' (if (nil? current-binding) [] current-binding)
  294. conflicts (dh/get-conflicts-by-keys binding' handler-id {:exclude-ids #{k}})]
  295. (if (seq conflicts)
  296. (set-key-conflicts! conflicts)
  297. (let [binding' (if (= binding binding') nil binding')]
  298. (shortcut/persist-user-shortcut! k binding')
  299. ;(notification/show! "Saved!" :success)
  300. (state/close-modal!)
  301. (saved-cb))))))]]]))
  302. (defn build-categories-map
  303. []
  304. (->> categories
  305. (map #(vector % (into (sorted-map) (dh/binding-by-category %))))))
  306. (rum/defc ^:large-vars/cleanup-todo shortcut-keymap-x
  307. []
  308. (let [_ (r/use-atom shortcut-config/*category)
  309. _ (r/use-atom *refresh-sentry)
  310. [ready?, set-ready!] (rum/use-state false)
  311. [filters, set-filters!] (rum/use-state #{})
  312. [keystroke, set-keystroke!] (rum/use-state "")
  313. [q set-q!] (rum/use-state nil)
  314. categories-list-map (build-categories-map)
  315. all-categories (into #{} (map first categories-list-map))
  316. in-filters? (boolean (seq filters))
  317. in-query? (not (string/blank? (util/trim-safe q)))
  318. in-keystroke? (not (string/blank? keystroke))
  319. [folded-categories set-folded-categories!] (rum/use-state #{})
  320. matched-list-map
  321. (when (and in-query? (not in-keystroke?))
  322. (->> categories-list-map
  323. (map (fn [[c binding-map]]
  324. [c (search/fuzzy-search
  325. binding-map q
  326. :extract-fn
  327. #(let [[id m] %]
  328. (str (name id) " " (dh/get-shortcut-desc (assoc m :id id)))))]))))
  329. result-list-map (or matched-list-map categories-list-map)
  330. toggle-categories! #(if (= folded-categories all-categories)
  331. (set-folded-categories! #{})
  332. (set-folded-categories! all-categories))]
  333. (rum/use-effect!
  334. (fn []
  335. (js/setTimeout #(set-ready! true) 100))
  336. [])
  337. [:div.cp__shortcut-page-x
  338. [:header.relative
  339. [:h2.text-xs.opacity-70
  340. (str (t :keymap/total)
  341. " "
  342. (if ready?
  343. (apply + (map #(count (second %)) result-list-map))
  344. " ..."))]
  345. (pane-controls q set-q! filters set-filters! keystroke set-keystroke! toggle-categories!)]
  346. [:article
  347. (when-not ready?
  348. [:p.py-8.flex.justify-center (ui/loading "")])
  349. (when ready?
  350. [:ul.list-none.m-0.py-3
  351. (for [[c binding-map] result-list-map
  352. :let [folded? (contains? folded-categories c)]]
  353. [:<>
  354. ;; category row
  355. (when (and (not in-query?)
  356. (not in-filters?)
  357. (not in-keystroke?))
  358. [:li.flex.justify-between.th
  359. {:key (str c)
  360. :on-click #(let [f (if folded? disj conj)]
  361. (set-folded-categories! (f folded-categories c)))}
  362. [:strong.font-semibold (t c)]
  363. [:i.flex.items-center
  364. (ui/icon (if folded? "chevron-left" "chevron-down"))]])
  365. ;; binding row
  366. (when (or in-query? in-filters? (not folded?))
  367. (for [[id {:keys [binding user-binding] :as m}] binding-map
  368. :let [binding (to-vector binding)
  369. user-binding (and user-binding (to-vector user-binding))
  370. label (shortcut-desc-label id m)
  371. custom? (not (nil? user-binding))
  372. disabled? (or (false? user-binding)
  373. (false? (first binding)))
  374. unset? (and (not disabled?)
  375. (or (= user-binding [])
  376. (and (= binding [])
  377. (nil? user-binding))))]]
  378. (when (or (nil? (seq filters))
  379. (when (contains? filters :Custom) custom?)
  380. (when (contains? filters :Disabled) disabled?)
  381. (when (contains? filters :Unset) unset?))
  382. ;; keystrokes filter
  383. (when (or (not in-keystroke?)
  384. (and (not disabled?)
  385. (not unset?)
  386. (let [binding' (or user-binding binding)
  387. keystroke' (some-> (shortcut-utils/safe-parse-string-binding keystroke) (bean/->clj))]
  388. (when (sequential? binding')
  389. (some #(when-let [s (some-> % (dh/mod-key) (shortcut-utils/safe-parse-string-binding) (bean/->clj))]
  390. (or (= s keystroke')
  391. (and (sequential? s) (sequential? keystroke')
  392. (apply = (map first [s keystroke']))))) binding')))))
  393. [:li.flex.items-center.justify-between.text-sm
  394. {:key (str id)}
  395. [:span.label-wrap label]
  396. [:a.action-wrap
  397. {:class (util/classnames [{:disabled disabled?}])
  398. :on-click (when (and id (not disabled?))
  399. #(open-customize-shortcut-dialog! id))}
  400. (cond
  401. (or unset? user-binding (false? user-binding))
  402. [:code.dark:bg-green-800.bg-green-300
  403. (if unset?
  404. (t :keymap/unset)
  405. (str (t :keymap/custom) ": "
  406. (if disabled?
  407. (t :keymap/disabled)
  408. (bean/->js
  409. (map #(if (false? %)
  410. (t :keymap/disabled)
  411. (shortcut-utils/decorate-binding %)) user-binding)))))]
  412. (not unset?)
  413. [:code.flex.items-center.bg-transparent
  414. (shui/shortcut-v1 (string/join " | " (map #(dh/binding-for-display id %) binding))
  415. (make-shui-context)
  416. {:size :md})])]]))))])])]]))