core.cljs 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293
  1. (ns frontend.modules.shortcut.core
  2. (:require [clojure.string :as string]
  3. [frontend.handler.config :as config-handler]
  4. [frontend.handler.global-config :as global-config-handler]
  5. [frontend.handler.notification :as notification]
  6. [frontend.handler.plugin :as plugin-handler]
  7. [frontend.modules.shortcut.config :as shortcut-config]
  8. [frontend.modules.shortcut.data-helper :as dh]
  9. [frontend.modules.shortcut.utils :as shortcut-utils]
  10. [frontend.state :as state]
  11. [frontend.util :as util]
  12. [goog.events :as events]
  13. [goog.ui.KeyboardShortcutHandler.EventType :as EventType]
  14. [lambdaisland.glogi :as log])
  15. (:import [goog.events KeyCodes KeyNames]
  16. [goog.ui KeyboardShortcutHandler]))
  17. (defonce *installed-handlers (atom {}))
  18. (defonce *pending-inited? (atom false))
  19. (defonce *pending-shortcuts (atom []))
  20. (def global-keys #js
  21. [KeyCodes/TAB
  22. KeyCodes/ENTER
  23. KeyCodes/BACKSPACE KeyCodes/DELETE
  24. KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
  25. (def key-names (js->clj KeyNames))
  26. (declare register-shortcut!)
  27. (defn consume-pending-shortcuts!
  28. []
  29. (when (and @*pending-inited? (seq @*pending-shortcuts))
  30. (doseq [[handler-id id shortcut] @*pending-shortcuts]
  31. (register-shortcut! handler-id id shortcut))
  32. (reset! *pending-shortcuts [])))
  33. (defn- get-handler-by-id
  34. [handler-id]
  35. (->> (vals @*installed-handlers)
  36. (filter #(= (:group %) handler-id))
  37. first
  38. :handler))
  39. (defn- get-installed-ids-by-handler-id
  40. [handler-id]
  41. (some->> @*installed-handlers
  42. (filter #(= (:group (second %)) handler-id))
  43. (map first)
  44. (remove nil?)
  45. (vec)))
  46. (defn register-shortcut!
  47. "Register a shortcut, notice the id need to be a namespaced keyword to avoid
  48. conflicts.
  49. Example:
  50. (register-shortcut! :shortcut.handler/misc :foo/bar {:binding \"mod+shift+8\"
  51. :fn (fn [_state _event]
  52. (js/alert \"test shortcut\"))})"
  53. ([handler-id id]
  54. (register-shortcut! handler-id id nil))
  55. ([handler-id id shortcut-map]
  56. (if (and (keyword? handler-id) (not @*pending-inited?))
  57. (swap! *pending-shortcuts conj [handler-id id shortcut-map])
  58. (when-let [^js handler (if (or (string? handler-id) (keyword? handler-id))
  59. (let [handler-id (keyword handler-id)]
  60. (get-handler-by-id handler-id))
  61. ;; as Handler instance
  62. handler-id)]
  63. (when shortcut-map
  64. (shortcut-config/add-shortcut! handler-id id shortcut-map))
  65. (when-not (false? (dh/shortcut-binding id))
  66. (doseq [k (dh/shortcut-binding id)]
  67. (try
  68. (log/debug :shortcut/register-shortcut {:id id :binding k})
  69. (.registerShortcut handler (util/keyname id) (shortcut-utils/undecorate-binding k))
  70. (catch :default e
  71. (log/error :shortcut/register-shortcut {:id id
  72. :binding k
  73. :error e})
  74. (notification/show! (string/join " " [id k (.-message e)]) :error false)))))))))
  75. (defn unregister-shortcut!
  76. "Unregister a shortcut.
  77. Example:
  78. (unregister-shortcut! :shortcut.handler/misc :foo/bar)"
  79. [handler-id shortcut-id]
  80. (when-let [handler (get-handler-by-id handler-id)]
  81. (when-let [ks (dh/shortcut-binding shortcut-id)]
  82. (doseq [k ks]
  83. (.unregisterShortcut ^js handler (shortcut-utils/undecorate-binding k)))))
  84. (when shortcut-id
  85. (shortcut-config/remove-shortcut! handler-id shortcut-id)))
  86. (defn uninstall-shortcut-handler!
  87. ([install-id] (uninstall-shortcut-handler! install-id false))
  88. ([install-id refresh?]
  89. (when-let [handler (-> (get @*installed-handlers install-id)
  90. :handler)]
  91. (.dispose ^js handler)
  92. (log/debug :shortcuts/uninstall-handler (-> @*installed-handlers (get install-id) :group (str (if refresh? "*" ""))))
  93. (swap! *installed-handlers dissoc install-id))))
  94. (defn install-shortcut-handler!
  95. [handler-id {:keys [set-global-keys?
  96. prevent-default?
  97. state]
  98. :or {set-global-keys? true
  99. prevent-default? false}}]
  100. ;; force uninstall existed handler
  101. (some->>
  102. (get-installed-ids-by-handler-id handler-id)
  103. (map #(uninstall-shortcut-handler! % true))
  104. (doall))
  105. (let [shortcut-map (dh/shortcuts-map-by-handler-id handler-id state)
  106. handler (new KeyboardShortcutHandler js/window)]
  107. ;; set arrows enter, tab to global
  108. (when set-global-keys?
  109. (.setGlobalKeys handler global-keys))
  110. (.setAlwaysPreventDefault handler prevent-default?)
  111. ;; register shortcuts
  112. (doseq [[id _] shortcut-map]
  113. ;; (log/info :shortcut/install-shortcut {:id id :shortcut (str (dh/shortcut-binding id))})
  114. (register-shortcut! handler id))
  115. (let [f (fn [e]
  116. (let [id (keyword (.-identifier e))
  117. shortcut-map (dh/shortcuts-map-by-handler-id handler-id state) ;; required to get shortcut map dynamically
  118. dispatch-fn (get shortcut-map id)]
  119. (state/set-state! :editor/latest-shortcut id)
  120. ;; trigger fn
  121. (when dispatch-fn
  122. (plugin-handler/hook-lifecycle-fn! id dispatch-fn e))))
  123. install-id (random-uuid)
  124. data {install-id
  125. {:group handler-id
  126. :dispatch-fn f
  127. :handler handler}}]
  128. (.listen handler EventType/SHORTCUT_TRIGGERED f)
  129. (log/debug :shortcuts/install-handler (str handler-id))
  130. (swap! *installed-handlers merge data)
  131. install-id)))
  132. (defn- install-shortcuts!
  133. [handler-ids]
  134. (->> (or (seq handler-ids)
  135. [:shortcut.handler/misc
  136. :shortcut.handler/editor-global
  137. :shortcut.handler/global-non-editing-only
  138. :shortcut.handler/global-prevent-default
  139. :shortcut.handler/block-editing-only])
  140. (map #(install-shortcut-handler! % {}))
  141. doall))
  142. (defn mixin
  143. ([handler-id] (mixin handler-id true))
  144. ([handler-id remount-reinstall?]
  145. (cond->
  146. {:did-mount
  147. (fn [state]
  148. (let [install-id (install-shortcut-handler! handler-id {:state state})]
  149. (assoc state ::install-id install-id)))
  150. :will-unmount
  151. (fn [state]
  152. (when-let [install-id (::install-id state)]
  153. (uninstall-shortcut-handler! install-id))
  154. state)}
  155. remount-reinstall?
  156. (assoc
  157. :will-remount
  158. (fn [old-state new-state]
  159. (util/profile "[shortcuts] reinstalled:"
  160. (uninstall-shortcut-handler! (::install-id old-state))
  161. (when-let [install-id (install-shortcut-handler! handler-id {:state new-state})]
  162. (assoc new-state ::install-id install-id))))))))
  163. (defn mixin*
  164. "This is an optimized version compared to (mixin).
  165. And the shortcuts will not be frequently loaded and unloaded.
  166. As well as ensuring unnecessary updates of components."
  167. [handler-id]
  168. {:did-mount
  169. (fn [state]
  170. (let [*state (volatile! state)
  171. install-id (install-shortcut-handler! handler-id {:state *state})]
  172. (assoc state ::install-id install-id
  173. ::*state *state)))
  174. :will-remount
  175. (fn [old-state new-state]
  176. (when-let [*state (::*state old-state)]
  177. (vreset! *state new-state))
  178. new-state)
  179. :will-unmount
  180. (fn [state]
  181. (when-let [install-id (::install-id state)]
  182. (uninstall-shortcut-handler! install-id)
  183. (some-> (::*state state) (vreset! nil)))
  184. state)})
  185. (defn unlisten-all!
  186. ([] (unlisten-all! false))
  187. ([dispose?]
  188. (doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
  189. :when (not= group :shortcut.handler/misc)]
  190. (if dispose?
  191. (.dispose handler)
  192. (events/unlisten handler EventType/SHORTCUT_TRIGGERED dispatch-fn)))))
  193. (defn listen-all! []
  194. (doseq [{:keys [handler group dispatch-fn]} (vals @*installed-handlers)
  195. :when (not= group :shortcut.handler/misc)]
  196. (if (.isDisposed handler)
  197. (install-shortcut-handler! group {})
  198. (events/listen handler EventType/SHORTCUT_TRIGGERED dispatch-fn))))
  199. (def disable-all-shortcuts
  200. {:will-mount
  201. (fn [state]
  202. (unlisten-all!)
  203. state)
  204. :will-unmount
  205. (fn [state]
  206. (listen-all!)
  207. state)})
  208. (defn refresh!
  209. "Always use this function to refresh shortcuts"
  210. []
  211. (when-not (:ui/shortcut-handler-refreshing? @state/state)
  212. (state/set-state! :ui/shortcut-handler-refreshing? true)
  213. (let [ids (keys @*installed-handlers)
  214. _handler-ids (set (map :group (vals @*installed-handlers)))]
  215. (doseq [id ids] (uninstall-shortcut-handler! id))
  216. ;; TODO: should re-install existed handlers
  217. (install-shortcuts! nil))
  218. (state/pub-event! [:shortcut-handler-refreshed])
  219. (state/set-state! :ui/shortcut-handler-refreshing? false)))
  220. (defn- name-with-meta [e]
  221. (let [ctrl (.-ctrlKey e)
  222. alt (.-altKey e)
  223. meta (.-metaKey e)
  224. shift (.-shiftKey e)
  225. keyname (get key-names (str (.-keyCode e)))]
  226. (cond->> keyname
  227. ctrl (str "ctrl+")
  228. alt (str "alt+")
  229. meta (str "meta+")
  230. shift (str "shift+"))))
  231. (defn keyname [e]
  232. (let [name (get key-names (str (.-keyCode e)))]
  233. (case name
  234. nil nil
  235. ("ctrl" "shift" "alt" "esc") nil
  236. (str " " (name-with-meta e)))))
  237. (defn persist-user-shortcut!
  238. [id binding]
  239. (let [graph-shortcuts (or (:shortcuts (state/get-graph-config)) {})
  240. global-shortcuts (or (:shortcuts (state/get-global-config)) {})
  241. global? true]
  242. (letfn [(into-shortcuts [shortcuts]
  243. (cond-> shortcuts
  244. (nil? binding)
  245. (dissoc id)
  246. (and global?
  247. (or (string? binding)
  248. (vector? binding)
  249. (boolean? binding)))
  250. (assoc id binding)))]
  251. ;; TODO: exclude current graph config shortcuts
  252. (config-handler/set-config! :shortcuts (into-shortcuts graph-shortcuts))
  253. (when (util/electron?)
  254. (global-config-handler/set-global-config-kv! :shortcuts (into-shortcuts global-shortcuts))))))