Inspector number boxes edit as you type, and are one undo step per focus
The boxes wrote nothing until blur, so a spinner click showed nothing on screen. Now every keystroke and step is an edit, and history holds the step open from focus to blur, so typing 4 then 5 is seen as 4, then 45, and undone once. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
277c0c3b63
commit
7aaf0a15bd
5 changed files with 82 additions and 25 deletions
|
|
@ -69,17 +69,34 @@
|
||||||
|
|
||||||
(defn record
|
(defn record
|
||||||
"History `h` with an edit from leaves `before` to `after` at time `now`."
|
"History `h` with an edit from leaves `before` to `after` at time `now`."
|
||||||
[{:keys [done] :as h} before after now]
|
[{:keys [done held?] :as h} before after now]
|
||||||
(let [[b a] (changes before after)
|
(let [[b a] (changes before after)
|
||||||
top (peek done)]
|
top (peek done)]
|
||||||
(cond
|
(cond
|
||||||
(empty? a) h
|
(empty? a) h
|
||||||
(and top (< (- now (:at top)) gap-ms) (= b (:after top)))
|
(and top (not (:closed? top)) (= b (:after top))
|
||||||
{:done (conj (pop done) (assoc top :after a :at now)) :undone []}
|
(or held? (< (- now (:at top)) gap-ms)))
|
||||||
|
(assoc h :done (conj (pop done) (assoc top :after a :at now)) :undone [])
|
||||||
:else
|
:else
|
||||||
{:done (conj (vec (take-last (dec depth) done))
|
(assoc h
|
||||||
{:before b :after a :at now :label (label before after (keys a))})
|
:done (conj (vec (take-last (dec depth) done))
|
||||||
:undone []})))
|
{:before b :after a :at now :label (label before after (keys a))})
|
||||||
|
:undone []))))
|
||||||
|
|
||||||
|
(defn- close [{:keys [done] :as h}]
|
||||||
|
(cond-> h (seq done) (assoc :done (conj (pop done) (assoc (peek done) :closed? true)))))
|
||||||
|
|
||||||
|
(defn hold
|
||||||
|
"While a field has focus, everything typed into it is one step, however slowly
|
||||||
|
— the digits of 45 are seen as 4 and then 45, and undone as one. It starts a
|
||||||
|
step of its own rather than joining whatever came before."
|
||||||
|
[h]
|
||||||
|
(assoc (close h) :held? true))
|
||||||
|
|
||||||
|
(defn settle
|
||||||
|
"The field is done with: its step is finished, and nothing joins it."
|
||||||
|
[h]
|
||||||
|
(dissoc (close h) :held?))
|
||||||
|
|
||||||
(defn steps
|
(defn steps
|
||||||
"The labels, newest first: `:done` is what undo would take off, `:undone`
|
"The labels, newest first: `:done` is what undo would take off, `:undone`
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,14 @@
|
||||||
(not= (:fps clip) (get-in db [:clip :fps]))
|
(not= (:fps clip) (get-in db [:clip :fps]))
|
||||||
(update :clip merge {:fps (:fps clip) :display-fps (:fps clip)})))
|
(update :clip merge {:fps (:fps clip) :display-fps (:fps clip)})))
|
||||||
|
|
||||||
|
(defn history
|
||||||
|
"Apply `f` to the loaded entry's undo history, which is not an edit: nothing
|
||||||
|
is redrawn and nothing becomes unsaved."
|
||||||
|
[db f]
|
||||||
|
(if-let [id (store/edit-entry! (:clip/current db) #(update % :history f))]
|
||||||
|
(assoc db :clip/current id)
|
||||||
|
db))
|
||||||
|
|
||||||
(defn edit
|
(defn edit
|
||||||
"Apply `f` to the loaded clip and return the new db."
|
"Apply `f` to the loaded clip and return the new db."
|
||||||
[db f]
|
[db f]
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,9 @@
|
||||||
(rf/reg-event-fx ::undo (fn [{:keys [db]} [_ n]] (steps db history/undo "undone" (or n 1))))
|
(rf/reg-event-fx ::undo (fn [{:keys [db]} [_ n]] (steps db history/undo "undone" (or n 1))))
|
||||||
(rf/reg-event-fx ::redo (fn [{:keys [db]} [_ n]] (steps db history/redo "redone" (or n 1))))
|
(rf/reg-event-fx ::redo (fn [{:keys [db]} [_ n]] (steps db history/redo "redone" (or n 1))))
|
||||||
|
|
||||||
|
(rf/reg-event-db ::hold (fn [db _] (edit/history db history/hold)))
|
||||||
|
(rf/reg-event-db ::settle (fn [db _] (edit/history db history/settle)))
|
||||||
|
|
||||||
(rf/reg-sub
|
(rf/reg-sub
|
||||||
::steps
|
::steps
|
||||||
;; The history is on the entry, outside app-db; the revision is what moves
|
;; The history is on the entry, outside app-db; the revision is what moves
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@
|
||||||
[arthur.domain.node :as node]
|
[arthur.domain.node :as node]
|
||||||
[arthur.domain.paint :as paint]
|
[arthur.domain.paint :as paint]
|
||||||
[arthur.domain.params :as params]
|
[arthur.domain.params :as params]
|
||||||
|
[arthur.events.history :as history]
|
||||||
[arthur.events.paint :as paint-events]
|
[arthur.events.paint :as paint-events]
|
||||||
[arthur.events.playback :as pb]
|
[arthur.events.playback :as pb]
|
||||||
[arthur.events.project :as project]
|
[arthur.events.project :as project]
|
||||||
|
|
@ -20,7 +21,8 @@
|
||||||
[arthur.subs.playback :as playback]
|
[arthur.subs.playback :as playback]
|
||||||
[arthur.subs.render :as render]
|
[arthur.subs.render :as render]
|
||||||
[arthur.subs.ui :as sub]
|
[arthur.subs.ui :as sub]
|
||||||
[re-frame.core :as rf]))
|
[re-frame.core :as rf]
|
||||||
|
[reagent.core :as r]))
|
||||||
|
|
||||||
(defn- brief
|
(defn- brief
|
||||||
"A value that fits the column. The `facts` list puts the whole thing in a
|
"A value that fits the column. The `facts` list puts the whole thing in a
|
||||||
|
|
@ -39,14 +41,37 @@
|
||||||
(defn- section [title & body]
|
(defn- section [title & body]
|
||||||
(into [:section.section [:h2 title]] body))
|
(into [:section.section [:h2 title]] body))
|
||||||
|
|
||||||
|
(defn- number-input
|
||||||
|
"A number box that edits as it is typed in or stepped, and is one undo step
|
||||||
|
from focus to blur. What is typed is kept while it has focus — \"-\" or
|
||||||
|
\"4.\" is no number yet, and the value it would round-trip to must not
|
||||||
|
replace it under the caret."
|
||||||
|
[_]
|
||||||
|
(let [draft (r/atom nil)]
|
||||||
|
(fn [{:keys [value parse on-number] :as attrs}]
|
||||||
|
[:input (merge (dissoc attrs :value :parse :on-number)
|
||||||
|
{:type "number"
|
||||||
|
:value (or @draft value "")
|
||||||
|
:on-focus (fn [e]
|
||||||
|
(reset! draft (.. e -target -value))
|
||||||
|
(rf/dispatch [::history/hold]))
|
||||||
|
:on-blur (fn [_]
|
||||||
|
(reset! draft nil)
|
||||||
|
(rf/dispatch [::history/settle]))
|
||||||
|
:on-key-down #(when (= "Enter" (.-key %)) (.. % -target blur))
|
||||||
|
:on-change (fn [e]
|
||||||
|
(let [s (.. e -target -value)
|
||||||
|
n (parse s)]
|
||||||
|
(when @draft (reset! draft s))
|
||||||
|
(when-not (js/isNaN n) (on-number n))))})])))
|
||||||
|
|
||||||
(defn- number-field [label value on-change & [placeholder disabled?]]
|
(defn- number-field [label value on-change & [placeholder disabled?]]
|
||||||
[:label.inspector-field label
|
[:label.inspector-field label
|
||||||
[:input {:type "number" :min 1 :step 1 :value (or value "")
|
[number-input {:min 1 :step 1 :value value
|
||||||
:placeholder placeholder
|
:placeholder placeholder
|
||||||
:disabled disabled?
|
:disabled disabled?
|
||||||
:on-change (fn [e]
|
:parse #(if (seq %) (js/parseInt % 10) nil)
|
||||||
(let [v (.. e -target -value)]
|
:on-number on-change}]])
|
||||||
(on-change (when (seq v) (js/parseInt v 10)))))}]])
|
|
||||||
|
|
||||||
;; ---------------------------------------------------------------------------
|
;; ---------------------------------------------------------------------------
|
||||||
;; the clip
|
;; the clip
|
||||||
|
|
@ -151,19 +176,12 @@
|
||||||
off? (and keyed? (nil? frame))
|
off? (and keyed? (nil? frame))
|
||||||
deg? (= path [:xform :rot])
|
deg? (= path [:xform :rot])
|
||||||
put #(rf/dispatch [::project/set-channel sid id path frame %])
|
put #(rf/dispatch [::project/set-channel sid id path frame %])
|
||||||
;; Written on blur or Enter rather than per keystroke, so a typed
|
|
||||||
;; number is one edit. Keyed by what it shows, so a seek or an undo
|
|
||||||
;; that changes the value puts the new one in the box.
|
|
||||||
field (fn [i x on-number]
|
field (fn [i x on-number]
|
||||||
(let [shown (if deg? (/ (js/Math.round (* x (/ 18000 js/Math.PI))) 100) x)]
|
^{:key i}
|
||||||
^{:key (str i " " shown)}
|
[number-input {:step "any" :disabled off?
|
||||||
[:input {:type "number" :step "any" :disabled off?
|
:value (if deg? (/ (js/Math.round (* x (/ 18000 js/Math.PI))) 100) x)
|
||||||
:default-value shown
|
:parse js/parseFloat
|
||||||
:on-key-down #(when (= "Enter" (.-key %)) (.. % -target blur))
|
:on-number #(on-number (if deg? (* % (/ js/Math.PI 180)) %))}])]
|
||||||
:on-blur (fn [e]
|
|
||||||
(let [n (js/parseFloat (.. e -target -value))]
|
|
||||||
(when-not (or (js/isNaN n) (= n shown))
|
|
||||||
(on-number (if deg? (* n (/ js/Math.PI 180)) n)))))}]))]
|
|
||||||
[:dd.channel
|
[:dd.channel
|
||||||
[:button.key {:class (cond (contains? (:keys ch) frame) "on" keyed? "keyed")
|
[:button.key {:class (cond (contains? (:keys ch) frame) "on" keyed? "keyed")
|
||||||
:disabled (nil? frame)
|
:disabled (nil? frame)
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,17 @@
|
||||||
(is (= 1 (count (:done h))))
|
(is (= 1 (count (:done h))))
|
||||||
(is (= {"v" 0} (:leaves (history/undo h {"v" 3}))))))
|
(is (= {"v" 0} (:leaves (history/undo h {"v" 3}))))))
|
||||||
|
|
||||||
|
(deftest typing-into-a-field-is-one-step-however-slow
|
||||||
|
(let [h (-> nil
|
||||||
|
(history/record {"w" 1} {"w" 2} 0)
|
||||||
|
history/hold
|
||||||
|
(history/record {"w" 2} {"w" 4} 100)
|
||||||
|
(history/record {"w" 4} {"w" 45} 9000)
|
||||||
|
history/settle
|
||||||
|
(history/record {"w" 45} {"w" 46} 9100))]
|
||||||
|
(is (= 3 (count (:done h))) "the edit before focus and the one after blur stand apart")
|
||||||
|
(is (= {"w" 2} (:leaves (history/undo (:history (history/undo h {"w" 46})) {"w" 45}))))))
|
||||||
|
|
||||||
(deftest their-write-between-two-of-mine-keeps-them-apart
|
(deftest their-write-between-two-of-mine-keeps-them-apart
|
||||||
(let [h (-> nil
|
(let [h (-> nil
|
||||||
(history/record {"fps" 30} {"fps" 12} 0)
|
(history/record {"fps" 30} {"fps" 12} 0)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue