Browse Source

feat: customize shortcut UX

Weihua Lu 4 years ago
parent
commit
64d4b472b1

+ 2 - 1
src/main/frontend/components/onboarding.cljs

@@ -228,7 +228,8 @@
        (t :help/shortcuts)
        [:br]
        (ui/button
-        "Learn more"
+        "Full-list && Customize"
+        :class "text-sm p-1"
         :on-click
         (fn []
           (route-handler/redirect! {:to :shortcut})))

+ 67 - 23
src/main/frontend/components/shortcut.cljs

@@ -1,26 +1,70 @@
 (ns frontend.components.shortcut
   (:require [frontend.context.i18n :as i18n]
+            [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.data-helper :as dh]
             [frontend.state :as state]
+            [frontend.ui :as ui]
             [rum.core :as rum]))
 (def *shortcut-config (rum/cursor-in state/state [:config (state/get-current-repo) :shortcuts]))
 
+(rum/defcs customize-shortcut-dialog-inner <
+  (rum/local "")
+  (shortcut/record!)
+  [state _]
+  (let [keypress (:rum/local state)]
+    [:div.w-full.sm:max-w-lg.sm:w-96
+     [:div.sm:flex.sm:items-start
+      [:div.mt-3.text-center.sm:mt-0.sm:text-left
+       [:h3#modal-headline.text-lg.leading-6.font-medium
+        "Press any key and Esc to finish"]
+       [:h4.text-lg.leading-6.font-medium
+        @keypress]]]]))
+
+(defn customize-shortcut-dialog [k]
+  (fn [_]
+    (customize-shortcut-dialog-inner k)))
+
+(rum/defc shortcut-col [k binding configurable?]
+  (let [conflict?         (dh/potential-confilct? k)
+        displayed-binding (dh/binding-for-display k binding)]
+    (if configurable?
+      [:td.text-right
+       (ui/button
+        displayed-binding
+        :title (if conflict?
+                 "Shortcut conflict!"
+                 "Click to modify")
+        :background (when conflict? "pink")
+        :on-click
+        #(state/set-modal! (customize-shortcut-dialog k)))
+       (ui/button "❌"
+                  :title "Reset to default"
+                  :class "transform motion-safe:hover:scale-125"
+                  :background "none"
+                  :on-click
+                  (fn [_]
+                    (dh/remove-shortcut k)
+                    (shortcut/refresh!)))]
+      [:td.text-right displayed-binding])))
+
 (rum/defc shortcut-table < rum/reactive
-  [name]
-  (let [_ (rum/react *shortcut-config)]
-    (rum/with-context [[t] i18n/*tongue-context*]
-      [:div
-       [:table
-        [:thead
-         [:tr
-          [:th.text-left [:b (t name)]]
-          [:th.text-right [:b (t :help/shortcut)]]]]
-        [:tbody
-         (map (fn [[k {:keys [binding]}]]
-                [:tr {:key k}
-                 [:td.text-left (t (dh/decorate-namespace k))]
-                 [:td.text-right (dh/binding-for-display k binding)]])
-              (dh/binding-by-category name))]]])))
+  ([name]
+   (shortcut-table name false))
+  ([name configurable?]
+   (let [_ (rum/react *shortcut-config)]
+     (rum/with-context [[t] i18n/*tongue-context*]
+       [:div
+        [:table
+         [:thead
+          [:tr
+           [:th.text-left [:b (t name)]]
+           [:th.text-right [:b (t :help/shortcut)]]]]
+         [:tbody
+          (map (fn [[k {:keys [binding]}]]
+                 [:tr {:key k}
+                  [:td.text-left (t (dh/decorate-namespace k))]
+                  (shortcut-col k binding configurable?)])
+               (dh/binding-by-category name))]]]))))
 
 (rum/defc trigger-table []
   (rum/with-context [[t] i18n/*tongue-context*]
@@ -55,11 +99,11 @@
     [:div
      [:h1.title (t :help/shortcut-page-title)]
      (trigger-table)
-     (shortcut-table :shortcut.category/basics)
-     (shortcut-table :shortcut.category/navigating)
-     (shortcut-table :shortcut.category/block-editing)
-     (shortcut-table :shortcut.category/block-command-editing)
-     (shortcut-table :shortcut.category/block-selection)
-     (shortcut-table :shortcut.category/formatting)
-     (shortcut-table :shortcut.category/toggle)
-     (shortcut-table :shortcut.category/others)]))
+     (shortcut-table :shortcut.category/basics true)
+     (shortcut-table :shortcut.category/navigating true)
+     (shortcut-table :shortcut.category/block-editing true)
+     (shortcut-table :shortcut.category/block-command-editing true)
+     (shortcut-table :shortcut.category/block-selection true)
+     (shortcut-table :shortcut.category/formatting true)
+     (shortcut-table :shortcut.category/toggle true)
+     (shortcut-table :shortcut.category/others true)]))

+ 53 - 1
src/main/frontend/modules/shortcut/core.cljs

@@ -1,5 +1,6 @@
 (ns frontend.modules.shortcut.core
   (:require [clojure.string :as str]
+            [frontend.handler.config :as config]
             [frontend.handler.notification :as notification]
             [frontend.modules.shortcut.data-helper :as dh]
             [frontend.util :as util]
@@ -7,7 +8,7 @@
             [goog.ui.KeyboardShortcutHandler.EventType :as EventType]
             [lambdaisland.glogi :as log]
             [medley.core :as medley])
-  (:import [goog.events KeyCodes]
+  (:import [goog.events KeyCodes KeyHandler KeyNames]
            [goog.ui KeyboardShortcutHandler]))
 
 (def *installed (atom {}))
@@ -18,6 +19,8 @@
    KeyCodes/BACKSPACE KeyCodes/DELETE
    KeyCodes/UP KeyCodes/LEFT KeyCodes/DOWN KeyCodes/RIGHT])
 
+(def key-names (js->clj KeyNames))
+
 (defn install-shortcut!
   [handler-id {:keys [set-global-keys?
                       prevent-default?
@@ -43,6 +46,7 @@
             (log/debug :shortcut/register-shortcut {:id id :binding k})
             (.registerShortcut handler (util/keyname id) k)
             (catch js/Object e
+              (def e e)
               (log/error :shortcut/register-shortcut {:id id
                                                       :binding k
                                                       :error e})
@@ -137,3 +141,51 @@
   (doseq [id (keys @*installed)]
     (uninstall-shortcut! id))
   (install-shortcuts!))
+
+(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
+      ("ctrl" "shift" "alt" "esc") nil
+      (str " " (name-with-meta e)))))
+
+(defn record! []
+  {:did-mount
+   (fn [state]
+     (let [handler (KeyHandler. js/document)
+           keystroke (:rum/local state)]
+
+       (doseq [id (keys @*installed)]
+         (uninstall-shortcut! id))
+
+       (events/listen handler "key"
+                      (fn [e]
+                        (.preventDefault e)
+                        (swap! keystroke #(str % (keyname e)))))
+
+       (assoc state ::key-record-handler handler)))
+
+   :will-unmount
+   (fn [{:rum/keys [args local] :as state}]
+     (let [k (first args)
+           keystroke (str/trim @local)]
+       (when-not (empty? keystroke)
+         (config/set-config! [:shortcuts k] keystroke)))
+
+     (when-let [^js handler (::key-record-handler state)]
+       (.dispose handler))
+
+     (js/setTimeout #(refresh!) 500)
+
+     (dissoc state ::key-record-handler))})

+ 55 - 2
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -1,9 +1,14 @@
 (ns frontend.modules.shortcut.data-helper
-  (:require [clojure.string :as str]
+  (:require [borkdude.rewrite-edn :as rewrite]
+            [clojure.string :as str]
+            [frontend.config :as cfg]
+            [frontend.db :as db]
+            [frontend.handler.file :as file]
             [frontend.modules.shortcut.config :as config]
             [frontend.state :as state]
             [frontend.util :as util]
-            [lambdaisland.glogi :as log]))
+            [lambdaisland.glogi :as log])
+  (:import [goog.ui KeyboardShortcutHandler]))
 (defonce default-binding
   (->> (vals config/default-config)
        (into {})
@@ -110,3 +115,51 @@
     (->> binding
          (map decorate-binding)
          (str/join " | "))))
+
+
+(defn remove-shortcut [k]
+  (let [repo (state/get-current-repo)
+        path (cfg/get-config-path)]
+    (when-let [content (db/get-file-no-sub path)]
+      (let [result (try
+                     (rewrite/parse-string content)
+                     (catch js/Error e
+                       (println "Parsing config file failed: ")
+                       (js/console.dir e)
+                       {}))
+            new-result (rewrite/update
+                        result
+                        :shortcuts
+                        #(dissoc (rewrite/sexpr %) k))]
+        (state/set-config! repo new-result)
+        (let [new-content (str new-result)]
+          (file/set-file-content! repo path new-content))))))
+
+(defn get-group
+  "Given shortcut key, return handler group
+  eg: :editor/new-line -> :shortcut.handler/block-editing-only"
+  [k]
+  (->> config/default-config
+       (filter (fn [[_ v]] (contains? v k)))
+       (map key)
+       (first)))
+
+(defn potential-confilct? [k]
+  (if-not (shortcut-binding k)
+    false
+    (let [handler-id    (get-group k)
+          shortcut-m    (shortcut-map handler-id)
+          bindings      (->> (shortcut-binding k)
+                            (map mod-key)
+                            (map KeyboardShortcutHandler/parseStringShortcut)
+                            (map js->clj))
+          rest-bindings (->> (map key shortcut-m)
+                             (remove #{k})
+                             (map shortcut-binding)
+                             (filter vector?)
+                             (mapcat identity)
+                             (map mod-key)
+                             (map KeyboardShortcutHandler/parseStringShortcut)
+                             (map js->clj))]
+
+      (some? (some (fn [b] (some #{b} rest-bindings)) bindings)))))