diff --git a/common/app/common/data/undo_stack.cljc b/common/app/common/data/undo_stack.cljc new file mode 100644 index 000000000..57f71d128 --- /dev/null +++ b/common/app/common/data/undo_stack.cljc @@ -0,0 +1,60 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.common.data.undo-stack + (:refer-clojure :exclude [peek]) + (:require + #?(:cljs [cljs.core :as core] + :clj [clojure.core :as core]))) + +(defonce MAX-UNDO-SIZE 100) + +(defn make-stack + [] + {:index -1 + :items []}) + +(defn peek + [{index :index items :items :as stack}] + (when (and (>= index 0) (< index (count items))) + (nth items index))) + +(defn append + [{index :index items :items :as stack} value] + + (if (and (some? stack) (not= value (peek stack))) + (let [items (cond-> items + (> index 0) + (subvec 0 (inc index)) + + (> (+ index 2) MAX-UNDO-SIZE) + (subvec 1 (inc index)) + + :always + (conj value)) + + index (min (dec MAX-UNDO-SIZE) (inc index))] + {:index index + :items items}) + stack)) + +(defn fixup + [{index :index :as stack} value] + (assoc-in stack [:items index] value)) + +(defn undo + [{index :index items :items :as stack}] + (update stack :index dec)) + +(defn redo + [{index :index items :items :as stack}] + (cond-> stack + (< index (dec (count items))) + (update :index inc))) + +(defn size + [{index :index items :items :as stack}] + (inc index)) diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index d479b5b83..9c3807563 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -356,13 +356,15 @@ (>= index 0) (accumulate-undo-entry (get-in state [:workspace-undo :items index])) (>= index 0) (update-in [:workspace-undo :index] dec)))))) +;; If these functions change modules review /src/app/main/data/workspace/path/undo.cljs (def undo (ptk/reify ::undo ptk/WatchEvent (watch [_ state stream] - (let [edition (get-in state [:workspace-local :edition])] + (let [edition (get-in state [:workspace-local :edition]) + drawing (get state :workspace-drawing)] ;; Editors handle their own undo's - (when-not (some? edition) + (when-not (or (some? edition) (some? drawing)) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -375,8 +377,9 @@ (ptk/reify ::redo ptk/WatchEvent (watch [_ state stream] - (let [edition (get-in state [:workspace-local :edition])] - (when-not (some? edition) + (let [edition (get-in state [:workspace-local :edition]) + drawing (get state :workspace-drawing)] + (when-not (or (some? edition) (some? drawing)) (let [undo (:workspace-undo state) items (:items undo) index (or (:index undo) (dec (count items)))] @@ -543,6 +546,7 @@ (rx/take 1) (rx/map (constantly clear-edition-mode))))))) +;; If these event change modules review /src/app/main/data/workspace/path/undo.cljs (def clear-edition-mode (ptk/reify ::clear-edition-mode ptk/UpdateEvent diff --git a/frontend/src/app/main/data/workspace/path/drawing.cljs b/frontend/src/app/main/data/workspace/path/drawing.cljs index f4621fbc8..c0945a33b 100644 --- a/frontend/src/app/main/data/workspace/path/drawing.cljs +++ b/frontend/src/app/main/data/workspace/path/drawing.cljs @@ -18,6 +18,7 @@ [app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.streams :as streams] [app.main.data.workspace.path.tools :as tools] + [app.main.data.workspace.path.undo :as undo] [app.main.streams :as ms] [app.util.geom.path :as ugp] [beicon.core :as rx] @@ -245,6 +246,7 @@ (make-drag-stream stream snap-toggled zoom points %))))] (rx/concat + (rx/of (undo/start-path-undo)) (rx/of (common/init-path)) (rx/merge mousemove-events mousedown-events) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 60335af17..f11e6c86d 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -17,6 +17,7 @@ [app.main.data.workspace.path.state :as st] [app.main.data.workspace.path.streams :as streams] [app.main.data.workspace.path.drawing :as drawing] + [app.main.data.workspace.path.undo :as undo] [app.main.streams :as ms] [app.util.geom.path :as ugp] [beicon.core :as rx] @@ -221,6 +222,7 @@ (watch [_ state stream] (let [mode (get-in state [:workspace-local :edit-path id :edit-mode])] (rx/concat + (rx/of (undo/start-path-undo)) (rx/of (drawing/change-edit-mode mode)) (->> stream (rx/take-until (->> stream (rx/filter (ptk/type? ::start-path-edit)))) diff --git a/frontend/src/app/main/data/workspace/path/undo.cljs b/frontend/src/app/main/data/workspace/path/undo.cljs new file mode 100644 index 000000000..2c490ecba --- /dev/null +++ b/frontend/src/app/main/data/workspace/path/undo.cljs @@ -0,0 +1,140 @@ +;; This Source Code Form is subject to the terms of the Mozilla Public +;; License, v. 2.0. If a copy of the MPL was not distributed with this +;; file, You can obtain one at http://mozilla.org/MPL/2.0/. +;; +;; Copyright (c) UXBOX Labs SL + +(ns app.main.data.workspace.path.undo + (:require + [app.common.data :as d] + [app.common.data.undo-stack :as u] + [app.common.uuid :as uuid] + [app.main.data.workspace.path.state :as st] + [app.main.store :as store] + [beicon.core :as rx] + [okulary.core :as l] + [potok.core :as ptk])) + +(defn undo-event? + [event] + (= :app.main.data.workspace.common/undo (ptk/type event))) + +(defn redo-event? + [event] + (= :app.main.data.workspace.common/redo (ptk/type event))) + +(defn- make-entry [state] + (let [id (st/get-path-id state)] + {:content (get-in state (st/get-path state :content)) + :preview (get-in state [:workspace-local :edit-path id :preview]) + :last-point (get-in state [:workspace-local :edit-path id :last-point]) + :prev-handler (get-in state [:workspace-local :edit-path id :prev-handler])})) + +(defn- load-entry [state {:keys [content preview last-point prev-handler]}] + (let [id (st/get-path-id state)] + (-> state + (d/assoc-in-when (st/get-path state :content) content) + (d/update-in-when + [:workspace-local :edit-path id] + assoc + :preview preview + :last-point last-point + :prev-handler prev-handler)))) + +(defn undo [] + (ptk/reify ::undo + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + undo-stack (-> (get-in state [:workspace-local :edit-path id :undo-stack]) + (u/undo)) + entry (u/peek undo-stack)] + (cond-> state + (some? entry) + (-> (load-entry entry) + (d/assoc-in-when + [:workspace-local :edit-path id :undo-stack] + undo-stack))))))) + +(defn redo [] + (ptk/reify ::redo + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + undo-stack (-> (get-in state [:workspace-local :edit-path id :undo-stack]) + (u/redo)) + entry (u/peek undo-stack)] + (-> state + (load-entry entry) + (d/assoc-in-when + [:workspace-local :edit-path id :undo-stack] + undo-stack)))))) + +(defn add-undo-entry [] + (ptk/reify ::add-undo-entry + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + entry (make-entry state)] + (-> state + (d/update-in-when + [:workspace-local :edit-path id :undo-stack] + u/append entry)))))) + +(defn end-path-undo + [] + (ptk/reify ::end-path-undo + ptk/UpdateEvent + (update [_ state] + (-> state + (d/update-in-when + [:workspace-local :edit-path (st/get-path-id state)] + dissoc :undo-lock :undo-stack))))) + +(defn- stop-undo? [event] + (= :app.main.data.workspace.common/clear-edition-mode (ptk/type event))) + +(def path-content-ref + (letfn [(selector [state] + (get-in state (st/get-path state :content)))] + (l/derived selector store/state))) + +(defn start-path-undo + [] + (let [lock (uuid/next)] + (ptk/reify ::start-path-undo + ptk/UpdateEvent + (update [_ state] + (let [undo-lock (get-in state [:workspace-local :edit-path (st/get-path-id state) :undo-lock])] + (cond-> state + (not undo-lock) + (update-in [:workspace-local :edit-path (st/get-path-id state)] + assoc + :undo-lock lock + :undo-stack (u/make-stack))))) + + ptk/WatchEvent + (watch [_ state stream] + (let [undo-lock (get-in state [:workspace-local :edit-path (st/get-path-id state) :undo-lock])] + (when (= undo-lock lock) + (let [stop-undo-stream (->> stream + (rx/filter stop-undo?) + (rx/take 1))] + (rx/concat + (->> (rx/merge + (->> (rx/from-atom path-content-ref {:emit-current-value? true}) + (rx/filter (comp not nil?)) + (rx/map #(add-undo-entry))) + + (->> stream + (rx/filter undo-event?) + (rx/map #(undo))) + + (->> stream + (rx/filter redo-event?) + (rx/map #(redo)))) + + (rx/take-until stop-undo-stream)) + + (rx/of (end-path-undo)))))))))) + diff --git a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs index ea81ec037..cb471cad7 100644 --- a/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/path/editor.cljs @@ -205,8 +205,10 @@ (->> points (remove selected-points) (into #{}))]) show-snap? (and snap-toggled - (empty? hover-points) - (or (some? drag-handler) (some? preview) (some? moving-handler) moving-nodes)) + (or (some? drag-handler) + (some? preview) + (some? moving-handler) + moving-nodes)) handle-double-click-outside (fn [event] diff --git a/frontend/src/app/util/debug.cljs b/frontend/src/app/util/debug.cljs index 80fdc4783..d83a3e054 100644 --- a/frontend/src/app/util/debug.cljs +++ b/frontend/src/app/util/debug.cljs @@ -13,7 +13,7 @@ #{:app.main.data.workspace.notifications/handle-pointer-update :app.main.data.workspace.selection/change-hover-state}) -(defonce ^:dynamic *debug* (atom #{})) +(defonce ^:dynamic *debug* (atom #{#_:events})) (defn debug-all! [] (reset! *debug* debug-options)) (defn debug-none! [] (reset! *debug* #{}))