| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293 |
- (ns frontend.modules.shortcut.core
- (:require [clojure.string :as string]
- [frontend.handler.config :as config-handler]
- [frontend.handler.global-config :as global-config-handler]
- [frontend.handler.notification :as notification]
- [frontend.handler.plugin :as plugin-handler]
- [frontend.modules.shortcut.config :as shortcut-config]
- [frontend.modules.shortcut.data-helper :as dh]
- [frontend.modules.shortcut.utils :as shortcut-utils]
- [frontend.state :as state]
- [frontend.util :as util]
- [goog.events :as events]
- [goog.ui.KeyboardShortcutHandler.EventType :as EventType]
- [lambdaisland.glogi :as log])
- (:import [goog.events KeyCodes KeyNames]
- [goog.ui KeyboardShortcutHandler]))
- (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])
- (def key-names (js->clj KeyNames))
- (declare register-shortcut!)
- (defn consume-pending-shortcuts!
- []
- (when (and @*pending-inited? (seq @*pending-shortcuts))
- (doseq [[handler-id id shortcut] @*pending-shortcuts]
- (register-shortcut! handler-id id shortcut))
- (reset! *pending-shortcuts [])))
- (defn- get-handler-by-id
- [handler-id]
- (->> (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
- conflicts.
- Example:
- (register-shortcut! :shortcut.handler/misc :foo/bar {:binding \"mod+shift+8\"
- :fn (fn [_state _event]
- (js/alert \"test shortcut\"))})"
- ([handler-id id]
- (register-shortcut! handler-id id nil))
- ([handler-id id shortcut-map]
- (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))
- ;; as Handler instance
- handler-id)]
- (when shortcut-map
- (shortcut-config/add-shortcut! handler-id id shortcut-map))
- (when-not (false? (dh/shortcut-binding id))
- (doseq [k (dh/shortcut-binding id)]
- (try
- (log/debug :shortcut/register-shortcut {:id id :binding k})
- (.registerShortcut handler (util/keyname id) (shortcut-utils/undecorate-binding k))
- (catch :default e
- (log/error :shortcut/register-shortcut {:id id
- :binding k
- :error e})
- (notification/show! (string/join " " [id k (.-message e)]) :error false)))))))))
- (defn unregister-shortcut!
- "Unregister a shortcut.
- Example:
- (unregister-shortcut! :shortcut.handler/misc :foo/bar)"
- [handler-id shortcut-id]
- (when-let [handler (get-handler-by-id handler-id)]
- (when-let [ks (dh/shortcut-binding shortcut-id)]
- (doseq [k ks]
- (.unregisterShortcut ^js handler (shortcut-utils/undecorate-binding k)))))
- (when shortcut-id
- (shortcut-config/remove-shortcut! handler-id shortcut-id)))
- (defn uninstall-shortcut-handler!
- ([install-id] (uninstall-shortcut-handler! install-id false))
- ([install-id refresh?]
- (when-let [handler (-> (get @*installed-handlers install-id)
- :handler)]
- (.dispose ^js handler)
- (log/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?
- prevent-default?
- state]
- :or {set-global-keys? true
- prevent-default? false}}]
- ;; force uninstall existed handler
- (some->>
- (get-installed-ids-by-handler-id handler-id)
- (map #(uninstall-shortcut-handler! % true))
- (doall))
- (let [shortcut-map (dh/shortcuts-map-by-handler-id handler-id state)
- handler (new KeyboardShortcutHandler js/window)]
- ;; set arrows enter, tab to global
- (when set-global-keys?
- (.setGlobalKeys handler global-keys))
- (.setAlwaysPreventDefault handler prevent-default?)
- ;; register shortcuts
- (doseq [[id _] shortcut-map]
- ;; (log/info :shortcut/install-shortcut {:id id :shortcut (str (dh/shortcut-binding id))})
- (register-shortcut! handler id))
- (let [f (fn [e]
- (let [id (keyword (.-identifier e))
- shortcut-map (dh/shortcuts-map-by-handler-id handler-id state) ;; required to get shortcut map dynamically
- dispatch-fn (get shortcut-map id)]
- (state/set-state! :editor/latest-shortcut id)
- ;; trigger fn
- (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}}]
- (.listen handler EventType/SHORTCUT_TRIGGERED f)
- (log/debug :shortcuts/install-handler (str handler-id))
- (swap! *installed-handlers merge data)
- install-id)))
- (defn- install-shortcuts!
- [handler-ids]
- (->> (or (seq handler-ids)
- [:shortcut.handler/misc
- :shortcut.handler/editor-global
- :shortcut.handler/global-non-editing-only
- :shortcut.handler/global-prevent-default
- :shortcut.handler/block-editing-only])
- (map #(install-shortcut-handler! % {}))
- doall))
- (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 [*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-unmount
- (fn [state]
- (when-let [install-id (::install-id state)]
- (uninstall-shortcut-handler! install-id)
- (some-> (::*state state) (vreset! nil)))
- state)})
- (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-handlers)
- :when (not= group :shortcut.handler/misc)]
- (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!)
- state)
- :will-unmount
- (fn [state]
- (listen-all!)
- state)})
- (defn refresh!
- "Always use this function to refresh shortcuts"
- []
- (when-not (:ui/shortcut-handler-refreshing? @state/state)
- (state/set-state! :ui/shortcut-handler-refreshing? true)
- (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)))
- (defn- name-with-meta [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+"))))
- (defn keyname [e]
- (let [name (get key-names (str (.-keyCode e)))]
- (case name
- nil nil
- ("ctrl" "shift" "alt" "esc") nil
- (str " " (name-with-meta e)))))
- (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
- (config-handler/set-config! :shortcuts (into-shortcuts graph-shortcuts))
- (when (util/electron?)
- (global-config-handler/set-global-config-kv! :shortcuts (into-shortcuts global-shortcuts))))))
|