diff --git a/CHANGES.md b/CHANGES.md index d66afd622..a6395d0cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -28,6 +28,17 @@ - Fix problem with pan and space [#811](https://github.com/penpot/penpot/issues/811) - Fix issue when parsing exponential numbers in paths - Remove legacy system user and team [#843](https://github.com/penpot/penpot/issues/843) +- Fix ordering of copy pasted objects [Taiga #1618](https://tree.taiga.io/project/penpot/issue/1617) +- Fix problems with blending modes [#837](https://github.com/penpot/penpot/issues/837) +- Fix problem with zoom an selection rect [#845](https://github.com/penpot/penpot/issues/845) +- Fix problem displaying team statistics [#859](https://github.com/penpot/penpot/issues/859) +- Fix problems with text editor selection [Taiga #1546](https://tree.taiga.io/project/penpot/issue/1546) +- Fix problem when opening the context menu in dashboard at the bottom [#856](https://github.com/penpot/penpot/issues/856) +- Fix problem when clicking an interactive group in view mode [#863](https://github.com/penpot/penpot/issues/863) +- Fix visibility of pages in sitemap when changing page [Taiga #1618](https://tree.taiga.io/project/penpot/issue/1618) +- Fix visual problem with group invite [Taiga #1290](https://tree.taiga.io/project/penpot/issue/1290) +- Fix issues with promote owner panel [Taiga #763](https://tree.taiga.io/project/penpot/issue/763) +- Allow use library colors when defining gradients [Taiga #1614](https://tree.taiga.io/project/penpot/issue/1614) ### :arrow_up: Deps updates diff --git a/common/app/common/geom/shapes.cljc b/common/app/common/geom/shapes.cljc index 28e110b73..a93c20ad3 100644 --- a/common/app/common/geom/shapes.cljc +++ b/common/app/common/geom/shapes.cljc @@ -17,28 +17,6 @@ [app.common.geom.shapes.intersect :as gin] [app.common.spec :as us])) -;; --- Relative Movement - -(defn move - "Move the shape relativelly to its current - position applying the provided delta." - [shape {dx :x dy :y}] - (let [dx (d/check-num dx) - dy (d/check-num dy)] - (-> shape - (assoc-in [:modifiers :displacement] (gmt/translate-matrix (gpt/point dx dy))) - (gtr/transform-shape)))) - -;; --- Absolute Movement - -(declare absolute-move-rect) - -(defn absolute-move - "Move the shape to the exactly specified position." - [shape {:keys [x y]}] - (let [dx (- (d/check-num x) (-> shape :selrect :x)) - dy (- (d/check-num y) (-> shape :selrect :y))] - (move shape (gpt/point dx dy)))) ;; --- Resize (Dimensions) (defn resize-modifiers @@ -120,38 +98,8 @@ (gpr/join-selrects))) (defn translate-to-frame - [{:keys [type x y] :as shape} {:keys [x y]}] - (let [move-point - (fn [point] - (-> point - (update :x - x) - (update :y - y))) - - move-segment - (fn [segment] - (-> segment - (d/update-in-when [:params :x] - x) - (d/update-in-when [:params :y] - y) - (d/update-in-when [:params :c1x] - x) - (d/update-in-when [:params :c1y] - y) - (d/update-in-when [:params :c2x] - x) - (d/update-in-when [:params :c2y] - y)))] - - (-> shape - (d/update-when :x - x) - (d/update-when :y - y) - (update-in [:selrect :x] - x) - (update-in [:selrect :y] - y) - (update-in [:selrect :x1] - x) - (update-in [:selrect :y1] - y) - (update-in [:selrect :x2] - x) - (update-in [:selrect :y2] - y) - - (d/update-when :points #(mapv move-point %)) - - (cond-> (= :path type) - (d/update-when :content #(mapv move-segment %)))))) - + [shape {:keys [x y]}] + (gtr/move shape (gpt/negate (gpt/point x y))) ) ;; --- Helpers @@ -244,6 +192,8 @@ (d/export gtr/update-group-selrect) (d/export gtr/transform-points) (d/export gtr/calculate-adjust-matrix) +(d/export gtr/move) +(d/export gtr/absolute-move) ;; PATHS (d/export gsp/content->points) diff --git a/common/app/common/geom/shapes/path.cljc b/common/app/common/geom/shapes/path.cljc index 33c041cee..0339cd652 100644 --- a/common/app/common/geom/shapes/path.cljc +++ b/common/app/common/geom/shapes/path.cljc @@ -139,6 +139,23 @@ (update :width #(if (mth/almost-zero? %) 1 %)) (update :height #(if (mth/almost-zero? %) 1 %))))) +(defn move-content [content move-vec] + (let [set-tr (fn [params px py] + (let [tr-point (-> (gpt/point (get params px) (get params py)) + (gpt/add move-vec))] + (assoc params + px (:x tr-point) + py (:y tr-point)))) + + transform-params + (fn [{:keys [x c1x c2x] :as params}] + (cond-> params + (not (nil? x)) (set-tr :x :y) + (not (nil? c1x)) (set-tr :c1x :c1y) + (not (nil? c2x)) (set-tr :c2x :c2y)))] + + (mapv #(update % :params transform-params) content))) + (defn transform-content [content transform] (let [set-tr (fn [params px py] (let [tr-point (-> (gpt/point (get params px) (get params py)) diff --git a/common/app/common/geom/shapes/transforms.cljc b/common/app/common/geom/shapes/transforms.cljc index 78f78dc88..cf9845741 100644 --- a/common/app/common/geom/shapes/transforms.cljc +++ b/common/app/common/geom/shapes/transforms.cljc @@ -14,6 +14,49 @@ [app.common.math :as mth] [app.common.data :as d])) +;; --- Relative Movement + +(defn move-selrect [selrect {dx :x dy :y}] + (-> selrect + (d/update-when :x + dx) + (d/update-when :y + dy) + (d/update-when :x1 + dx) + (d/update-when :y1 + dy) + (d/update-when :x2 + dx) + (d/update-when :y2 + dy))) + +(defn move-points [points move-vec] + (->> points + (mapv #(gpt/add % move-vec)))) + +(defn move + "Move the shape relativelly to its current + position applying the provided delta." + [shape {dx :x dy :y}] + (let [dx (d/check-num dx) + dy (d/check-num dy) + move-vec (gpt/point dx dy)] + + (-> shape + (update :selrect move-selrect move-vec) + (update :points move-points move-vec) + (d/update-when :x + dx) + (d/update-when :y + dy) + (cond-> (= :path (:type shape)) + (update :content gpa/move-content move-vec))))) + +;; --- Absolute Movement + +(declare absolute-move-rect) + +(defn absolute-move + "Move the shape to the exactly specified position." + [shape {:keys [x y]}] + (let [dx (- (d/check-num x) (-> shape :selrect :x)) + dy (- (d/check-num y) (-> shape :selrect :y))] + (move shape (gpt/point dx dy)))) + + (defn- modif-rotation [shape] (let [cur-rotation (d/check-num (:rotation shape)) delta-angle (d/check-num (get-in shape [:modifiers :rotation]))] @@ -272,12 +315,27 @@ (and rx (< rx 0)) (update :flip-x not) (and ry (< ry 0)) (update :flip-y not)))) -(defn transform-shape [shape] - (let [center (gco/center-shape shape)] - (if (and (:modifiers shape) center) - (let [transform (modifiers->transform center (:modifiers shape))] +(defn apply-displacement [shape] + (let [modifiers (:modifiers shape)] + (if (contains? modifiers :displacement) + (let [mov-vec (-> (gpt/point 0 0) + (gpt/transform (:displacement modifiers))) + shape (move shape mov-vec) + modifiers (dissoc modifiers :displacement)] (-> shape - (set-flip (:modifiers shape)) + (assoc :modifiers modifiers) + (cond-> (empty? modifiers) + (dissoc :modifiers)))) + shape))) + +(defn transform-shape [shape] + (let [shape (apply-displacement shape) + center (gco/center-shape shape) + modifiers (:modifiers shape)] + (if (and modifiers center) + (let [transform (modifiers->transform center modifiers)] + (-> shape + (set-flip modifiers) (apply-transform transform) (dissoc :modifiers))) shape))) diff --git a/common/app/common/pages/changes.cljc b/common/app/common/pages/changes.cljc index 713ab73c9..0a792233c 100644 --- a/common/app/common/pages/changes.cljc +++ b/common/app/common/pages/changes.cljc @@ -31,7 +31,7 @@ ;; When verify? false we spec the schema validation. Currently used to make just ;; 1 validation even if the changes are applied twice (when verify? - (us/verify ::spec/changes items)) + (us/assert ::spec/changes items)) (let [pages (into #{} (map :page-id) items) result (->> items diff --git a/frontend/resources/styles/main/partials/dashboard-team.scss b/frontend/resources/styles/main/partials/dashboard-team.scss index 6c0f4fa01..6688ffebe 100644 --- a/frontend/resources/styles/main/partials/dashboard-team.scss +++ b/frontend/resources/styles/main/partials/dashboard-team.scss @@ -24,7 +24,8 @@ } .custom-select { - width: 103px + width: 103px; + overflow: hidden; } .action-buttons { diff --git a/frontend/resources/styles/main/partials/modal.scss b/frontend/resources/styles/main/partials/modal.scss index fee4b62d6..d67985d15 100644 --- a/frontend/resources/styles/main/partials/modal.scss +++ b/frontend/resources/styles/main/partials/modal.scss @@ -134,6 +134,11 @@ margin-bottom: 0px; } } + + } + + .btn-disabled { + opacity: 0.5; } } diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index dbd7eca00..92f68d2de 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -7,7 +7,6 @@ (ns app.main.data.workspace (:require [app.common.data :as d] - [app.common.exceptions :as ex] [app.common.geom.align :as gal] [app.common.geom.matrix :as gmt] [app.common.geom.point :as gpt] @@ -19,37 +18,31 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.config :as cfg] - [app.main.constants :as c] - [app.main.data.workspace.colors :as mdc] [app.main.data.messages :as dm] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.drawing :as dwd] - [app.main.data.workspace.path :as dwdp] [app.main.data.workspace.groups :as dwg] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.notifications :as dwn] + [app.main.data.workspace.path :as dwdp] [app.main.data.workspace.persistence :as dwp] [app.main.data.workspace.selection :as dws] - [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.svg-upload :as svg] + [app.main.data.workspace.transforms :as dwt] + [app.main.data.workspace.undo :as dwu] [app.main.repo :as rp] - [app.main.store :as st] [app.main.streams :as ms] - [app.main.worker :as uw] - [app.util.dom :as dom] [app.util.http :as http] - [app.util.i18n :refer [tr] :as i18n] + [app.util.i18n :as i18n] [app.util.logging :as log] - [app.util.object :as obj] [app.util.router :as rt] - [app.util.timers :as ts] [app.util.transit :as t] [app.util.webapi :as wapi] [beicon.core :as rx] [cljs.spec.alpha :as s] [clojure.set :as set] [cuerdas.core :as str] - [goog.string.path :as path] [potok.core :as ptk])) ;; (log/set-level! :trace) @@ -269,7 +262,7 @@ :name name} uchange {:type :del-page :id id}] - (rx/of (dwc/commit-changes [rchange] [uchange] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchange] [uchange] {:commit-local? true})))))) (defn duplicate-page [page-id] (ptk/reify ::duplicate-page @@ -287,7 +280,7 @@ :page page} uchange {:type :del-page :id id}] - (rx/of (dwc/commit-changes [rchange] [uchange] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchange] [uchange] {:commit-local? true})))))) (s/def ::rename-page (s/keys :req-un [::id ::name])) @@ -306,7 +299,7 @@ uchg {:type :mod-page :id id :name (:name page)}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) (declare purge-page) (declare go-to-file) @@ -323,7 +316,7 @@ :id id} uchg {:type :add-page :page page}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) (when (= id (:current-page-id state)) go-to-file)))))) @@ -604,7 +597,7 @@ (ptk/reify ::update-shape ptk/WatchEvent (watch [_ state stream] - (rx/of (dwc/update-shapes [id] #(merge % attrs)))))) + (rx/of (dch/update-shapes [id] #(merge % attrs)))))) (defn start-rename-shape [id] @@ -712,7 +705,7 @@ :index (cp/position-on-parent id objects)})) selected)] ;; TODO: maybe missing the :reg-objects event? - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) ;; --- Change Shape Order (D&D Ordering) @@ -986,7 +979,7 @@ shapes-to-detach shapes-to-reroot shapes-to-deroot)] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/expand-collapse parent-id)))))) (defn relocate-selected-shapes @@ -1039,7 +1032,7 @@ uchg {:type :mov-page :id id :index cidx}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) ;; --- Shape / Selection Alignment and Distribution @@ -1057,30 +1050,13 @@ selected (get-in state [:workspace-local :selected]) moved (if (= 1 (count selected)) (align-object-to-frame objects (first selected) axis) - (align-objects-list objects selected axis))] - (loop [moved (seq moved) - rchanges [] - uchanges []] - (if (nil? moved) - (do - ;; (println "================ rchanges") - ;; (cljs.pprint/pprint rchanges) - ;; (println "================ uchanges") - ;; (cljs.pprint/pprint uchanges) - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))) - (let [curr (first moved) - prev (get objects (:id curr)) - ops1 (dwc/generate-operations prev curr) - ops2 (dwc/generate-operations curr prev true)] - (recur (next moved) - (conj rchanges {:type :mod-obj - :page-id page-id - :operations ops1 - :id (:id curr)}) - (conj uchanges {:type :mod-obj - :page-id page-id - :operations ops2 - :id (:id curr)}))))))))) + (align-objects-list objects selected axis)) + + moved-objects (->> moved (group-by :id)) + ids (keys moved-objects) + update-fn (fn [shape] (first (get moved-objects (:id shape))))] + + (rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))) (defn align-object-to-frame [objects object-id axis] @@ -1104,30 +1080,12 @@ objects (dwc/lookup-page-objects state page-id) selected (get-in state [:workspace-local :selected]) moved (-> (map #(get objects %) selected) - (gal/distribute-space axis objects))] - (loop [moved (seq moved) - rchanges [] - uchanges []] - (if (nil? moved) - (do - ;; (println "================ rchanges") - ;; (cljs.pprint/pprint rchanges) - ;; (println "================ uchanges") - ;; (cljs.pprint/pprint uchanges) - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}))) - (let [curr (first moved) - prev (get objects (:id curr)) - ops1 (dwc/generate-operations prev curr) - ops2 (dwc/generate-operations curr prev true)] - (recur (next moved) - (conj rchanges {:type :mod-obj - :page-id page-id - :operations ops1 - :id (:id curr)}) - (conj uchanges {:type :mod-obj - :page-id page-id - :operations ops2 - :id (:id curr)}))))))))) + (gal/distribute-space axis objects)) + + moved-objects (->> moved (group-by :id)) + ids (keys moved-objects) + update-fn (fn [shape] (first (get moved-objects (:id shape))))] + (rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))) ;; --- Shape Proportions @@ -1141,7 +1099,7 @@ (assoc shape :proportion-lock false) (-> (assoc shape :proportion-lock true) (gpr/assign-proportions))))] - (rx/of (dwc/update-shapes [id] assign-proportions)))))) + (rx/of (dch/update-shapes [id] assign-proportions)))))) ;; --- Update Shape Position @@ -1183,7 +1141,7 @@ (cond-> obj (boolean? blocked) (assoc :blocked blocked) (boolean? hidden) (assoc :hidden hidden)))] - (rx/of (dwc/update-shapes-recursive [id] update-fn)))))) + (rx/of (dch/update-shapes-recursive [id] update-fn)))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -1410,7 +1368,7 @@ (catch :default e (let [data (ex-data e)] (if (:not-implemented data) - (rx/of (dm/warn (tr "errors.clipboard-not-implemented"))) + (rx/of (dm/warn (i18n/tr "errors.clipboard-not-implemented"))) (js/console.error "ERROR" e)))))))) (defn paste-from-event @@ -1582,7 +1540,7 @@ (map #(get-in % [:obj :id])) (into (d/ordered-set)))] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes selected))))] (ptk/reify ::paste-shape ptk/WatchEvent @@ -1629,10 +1587,10 @@ :height height :grow-type (if (> (count text) 100) :auto-height :auto-width) :content (as-content text)})] - (rx/of (dwc/start-undo-transaction) + (rx/of (dwu/start-undo-transaction) (dws/deselect-all) (dwc/add-shape shape) - (dwc/commit-undo-transaction)))))) + (dwu/commit-undo-transaction)))))) (defn- paste-svg [text] @@ -1743,7 +1701,7 @@ (let [page-id (get state :current-page-id) options (dwc/lookup-page-options state page-id) previus-color (:background options)] - (rx/of (dwc/commit-changes + (rx/of (dch/commit-changes [{:type :set-option :page-id page-id :option :background diff --git a/frontend/src/app/main/data/workspace/changes.cljs b/frontend/src/app/main/data/workspace/changes.cljs new file mode 100644 index 000000000..37a95099a --- /dev/null +++ b/frontend/src/app/main/data/workspace/changes.cljs @@ -0,0 +1,222 @@ +;; 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.changes + (:require + [app.common.data :as d] + [app.common.pages :as cp] + [app.common.pages.spec :as spec] + [app.common.spec :as us] + [app.main.data.workspace.undo :as dwu] + [app.main.worker :as uw] + [app.util.logging :as log] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [clojure.set :as set] + [potok.core :as ptk])) + +;; Change this to :info :debug or :trace to debug this module +(log/set-level! :warn) + +(s/def ::coll-of-uuid + (s/every ::us/uuid)) + +(defonce page-change? #{:add-page :mod-page :del-page :mov-page}) + +(declare commit-changes) + +(def commit-changes? (ptk/type? ::commit-changes)) + +(defn- generate-operations + ([ma mb] (generate-operations ma mb false)) + ([ma mb undo?] + (let [ops (let [ma-keys (set (keys ma)) + mb-keys (set (keys mb)) + added (set/difference mb-keys ma-keys) + removed (set/difference ma-keys mb-keys) + both (set/intersection ma-keys mb-keys)] + (d/concat + (mapv #(array-map :type :set :attr % :val (get mb %)) added) + (mapv #(array-map :type :set :attr % :val nil) removed) + (loop [items (seq both) + result []] + (if items + (let [k (first items) + vma (get ma k) + vmb (get mb k)] + (if (= vma vmb) + (recur (next items) result) + (recur (next items) + (conj result {:type :set + :attr k + :val vmb + :ignore-touched undo?})))) + result))))] + (if undo? + (conj ops {:type :set-touched :touched (:touched mb)}) + ops)))) + +(defn update-shapes + ([ids f] (update-shapes ids f nil)) + ([ids f {:keys [reg-objects?] :or {reg-objects? false}}] + (us/assert ::coll-of-uuid ids) + (us/assert fn? f) + (ptk/reify ::update-shapes + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + objects (get-in state [:workspace-data :pages-index page-id :objects]) + reg-objects {:type :reg-objects :page-id page-id :shapes (vec ids)}] + (loop [ids (seq ids) + rch [] + uch []] + (if (nil? ids) + (rx/of (let [has-rch? (not (empty? rch)) + has-uch? (not (empty? uch)) + rch (cond-> rch (and has-rch? reg-objects?) (conj reg-objects)) + uch (cond-> uch (and has-rch? reg-objects?) (conj reg-objects))] + (when (and has-rch? has-uch?) + (commit-changes rch uch {:commit-local? true})))) + + (let [id (first ids) + obj1 (get objects id) + obj2 (f obj1) + rch-operations (generate-operations obj1 obj2) + uch-operations (generate-operations obj2 obj1 true) + rchg {:type :mod-obj + :page-id page-id + :operations rch-operations + :id id} + uchg {:type :mod-obj + :page-id page-id + :operations uch-operations + :id id}] + (recur (next ids) + (if (empty? rch-operations) rch (conj rch rchg)) + (if (empty? uch-operations) uch (conj uch uchg))))))))))) + +(defn update-shapes-recursive + [ids f] + (us/assert ::coll-of-uuid ids) + (us/assert fn? f) + (letfn [(impl-get-children [objects id] + (cons id (cp/get-children id objects))) + + (impl-gen-changes [objects page-id ids] + (loop [sids (seq ids) + cids (seq (impl-get-children objects (first sids))) + rchanges [] + uchanges []] + (cond + (nil? sids) + [rchanges uchanges] + + (nil? cids) + (recur (next sids) + (seq (impl-get-children objects (first (next sids)))) + rchanges + uchanges) + + :else + (let [id (first cids) + obj1 (get objects id) + obj2 (f obj1) + rops (generate-operations obj1 obj2) + uops (generate-operations obj2 obj1 true) + rchg {:type :mod-obj + :page-id page-id + :operations rops + :id id} + uchg {:type :mod-obj + :page-id page-id + :operations uops + :id id}] + (recur sids + (next cids) + (conj rchanges rchg) + (conj uchanges uchg))))))] + (ptk/reify ::update-shapes-recursive + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + objects (get-in state [:workspace-data :pages-index page-id :objects]) + [rchanges uchanges] (impl-gen-changes objects page-id (seq ids))] + (rx/of (commit-changes rchanges uchanges {:commit-local? true}))))))) + +(defn update-indices + [page-id changes] + (ptk/reify ::update-indices + ptk/EffectEvent + (effect [_ state stream] + (uw/ask! {:cmd :update-page-indices + :page-id page-id + :changes changes})))) + +(defn commit-changes + ([changes undo-changes] + (commit-changes changes undo-changes {})) + ([changes undo-changes {:keys [save-undo? + commit-local? + file-id] + :or {save-undo? true + commit-local? false} + :as opts}] + (us/assert ::cp/changes changes) + (us/assert ::cp/changes undo-changes) + (log/debug :msg "commit-changes" + :js/changes changes + :js/undo-changes undo-changes) + + (let [error (volatile! nil)] + (ptk/reify ::commit-changes + cljs.core/IDeref + (-deref [_] {:file-id file-id :changes changes}) + + ptk/UpdateEvent + (update [_ state] + (let [current-file-id (get state :current-file-id) + file-id (or file-id current-file-id) + path1 (if (= file-id current-file-id) + [:workspace-file :data] + [:workspace-libraries file-id :data]) + path2 (if (= file-id current-file-id) + [:workspace-data] + [:workspace-libraries file-id :data])] + (try + (us/assert ::spec/changes changes) + (let [state (update-in state path1 cp/process-changes changes false)] + (cond-> state + commit-local? (update-in path2 cp/process-changes changes false))) + (catch :default e + (vreset! error e) + state)))) + + ptk/WatchEvent + (watch [_ state stream] + (when-not @error + (let [;; adds page-id to page changes (that have the `id` field instead) + add-page-id + (fn [{:keys [id type page] :as change}] + (cond-> change + (page-change? type) + (assoc :page-id (or id (:id page))))) + + changes-by-pages + (->> changes + (map add-page-id) + (remove #(nil? (:page-id %))) + (group-by :page-id)) + + process-page-changes + (fn [[page-id changes]] + (update-indices page-id changes))] + (rx/concat + (rx/from (map process-page-changes changes-by-pages)) + + (when (and save-undo? (seq undo-changes)) + (let [entry {:undo-changes undo-changes + :redo-changes changes}] + (rx/of (dwu/append-undo entry)))))))))))) diff --git a/frontend/src/app/main/data/workspace/colors.cljs b/frontend/src/app/main/data/workspace/colors.cljs index 6a27b8f92..d43d36905 100644 --- a/frontend/src/app/main/data/workspace/colors.cljs +++ b/frontend/src/app/main/data/workspace/colors.cljs @@ -13,6 +13,7 @@ [app.main.data.modal :as md] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.texts :as dwt] + [app.main.data.workspace.changes :as dch] [app.main.repo :as rp] [app.main.store :as st] [app.main.streams :as ms] @@ -150,7 +151,7 @@ (rx/concat (rx/from (map #(dwt/update-text-attrs {:id % :attrs attrs}) text-ids)) - (rx/of (dwc/update-shapes shape-ids (fn [shape] (d/merge shape attrs))))))))) + (rx/of (dch/update-shapes shape-ids (fn [shape] (d/merge shape attrs))))))))) (defn change-stroke [ids color] @@ -176,7 +177,7 @@ (contains? color :opacity) (assoc :stroke-opacity (:opacity color)))] - (rx/of (dwc/update-shapes ids (fn [shape] + (rx/of (dch/update-shapes ids (fn [shape] (cond-> (d/merge shape attrs) (= (:stroke-style shape) :none) (assoc :stroke-style :solid diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index c8f4c3393..b5d8ced0d 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -10,17 +10,15 @@ [app.common.geom.proportions :as gpr] [app.common.geom.shapes :as gsh] [app.common.pages :as cp] - [app.common.pages.spec :as spec] [app.common.spec :as us] [app.common.uuid :as uuid] - [app.main.worker :as uw] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.undo :as dwu] [app.main.streams :as ms] + [app.main.worker :as uw] [app.util.logging :as log] - [app.util.timers :as ts] [beicon.core :as rx] [cljs.spec.alpha :as s] - [clojure.set :as set] - [cuerdas.core :as str] [potok.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module @@ -58,121 +56,6 @@ (get-in state [:workspace-data :components component-id :objects]))) -;; --- Changes Handling - -(defonce page-change? #{:add-page :mod-page :del-page :mov-page}) - -(defn commit-changes - ([changes undo-changes] - (commit-changes changes undo-changes {})) - ([changes undo-changes {:keys [save-undo? - commit-local? - file-id] - :or {save-undo? true - commit-local? false} - :as opts}] - (us/verify ::cp/changes changes) - (us/verify ::cp/changes undo-changes) - (log/debug :msg "commit-changes" - :js/changes changes - :js/undo-changes undo-changes) - - (let [error (volatile! nil)] - (ptk/reify ::commit-changes - cljs.core/IDeref - (-deref [_] {:file-id file-id :changes changes}) - - ptk/UpdateEvent - (update [_ state] - (let [current-file-id (get state :current-file-id) - file-id (or file-id current-file-id) - path1 (if (= file-id current-file-id) - [:workspace-file :data] - [:workspace-libraries file-id :data]) - path2 (if (= file-id current-file-id) - [:workspace-data] - [:workspace-libraries file-id :data])] - (try - (us/verify ::spec/changes changes) - (let [state (update-in state path1 cp/process-changes changes false)] - (cond-> state - commit-local? (update-in path2 cp/process-changes changes false))) - (catch :default e - (vreset! error e) - state)))) - - ptk/WatchEvent - (watch [_ state stream] - (when-not @error - (let [;; adds page-id to page changes (that have the `id` field instead) - add-page-id - (fn [{:keys [id type page] :as change}] - (cond-> change - (page-change? type) - (assoc :page-id (or id (:id page))))) - - changes-by-pages - (->> changes - (map add-page-id) - (remove #(nil? (:page-id %))) - (group-by :page-id)) - - process-page-changes - (fn [[page-id changes]] - (update-indices page-id changes))] - (rx/concat - (rx/from (map process-page-changes changes-by-pages)) - - (when (and save-undo? (seq undo-changes)) - (let [entry {:undo-changes undo-changes - :redo-changes changes}] - (rx/of (append-undo entry)))))))))))) - -(defn generate-operations - ([ma mb] (generate-operations ma mb false)) - ([ma mb undo?] - (let [ops (let [ma-keys (set (keys ma)) - mb-keys (set (keys mb)) - added (set/difference mb-keys ma-keys) - removed (set/difference ma-keys mb-keys) - both (set/intersection ma-keys mb-keys)] - (d/concat - (mapv #(array-map :type :set :attr % :val (get mb %)) added) - (mapv #(array-map :type :set :attr % :val nil) removed) - (loop [items (seq both) - result []] - (if items - (let [k (first items) - vma (get ma k) - vmb (get mb k)] - (if (= vma vmb) - (recur (next items) result) - (recur (next items) - (conj result {:type :set - :attr k - :val vmb - :ignore-touched undo?})))) - result))))] - (if undo? - (conj ops {:type :set-touched :touched (:touched mb)}) - ops)))) - -(defn generate-changes - [page-id objects1 objects2] - (letfn [(impl-diff [res id] - (let [obj1 (get objects1 id) - obj2 (get objects2 id) - ops (generate-operations (dissoc obj1 :shapes :frame-id) - (dissoc obj2 :shapes :frame-id))] - (if (empty? ops) - res - (conj res {:type :mod-obj - :page-id page-id - :operations ops - :id id}))))] - (reduce impl-diff [] (set/union (set (keys objects1)) - (set (keys objects2)))))) - ;; --- Selection Index Handling (defn initialize-indices @@ -186,14 +69,7 @@ (->> (uw/ask! msg) (rx/map (constantly ::index-initialized))))))) -(defn update-indices - [page-id changes] - (ptk/reify ::update-indices - ptk/EffectEvent - (effect [_ state stream] - (uw/ask! {:cmd :update-page-indices - :page-id page-id - :changes changes})))) + ;; --- Common Helpers & Events @@ -253,110 +129,8 @@ (update state :workspace-local dissoc :expanded)))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Undo / Redo -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; - -(s/def ::undo-changes ::cp/changes) -(s/def ::redo-changes ::cp/changes) -(s/def ::undo-entry - (s/keys :req-un [::undo-changes ::redo-changes])) - -(def MAX-UNDO-SIZE 50) - -(defn- conj-undo-entry - [undo data] - (let [undo (conj undo data) - cnt (count undo)] - (if (> cnt MAX-UNDO-SIZE) - (subvec undo (- cnt MAX-UNDO-SIZE)) - undo))) - -(defn- materialize-undo - [changes index] - (ptk/reify ::materialize-undo - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-data cp/process-changes changes) - (assoc-in [:workspace-undo :index] index))))) - -(defn- reset-undo - [index] - (ptk/reify ::reset-undo - ptk/UpdateEvent - (update [_ state] - (-> state - (update :workspace-undo dissoc :undo-index) - (update-in [:workspace-undo :items] (fn [queue] (into [] (take (inc index) queue)))))))) - -(defn- add-undo-entry - [state entry] - (if (and entry - (not-empty (:undo-changes entry)) - (not-empty (:redo-changes entry))) - (let [index (get-in state [:workspace-undo :index] -1) - items (get-in state [:workspace-undo :items] []) - items (->> items (take (inc index)) (into [])) - items (conj-undo-entry items entry)] - (-> state - (update :workspace-undo assoc :items items - :index (min (inc index) - (dec MAX-UNDO-SIZE))))) - state)) - -(defn- accumulate-undo-entry - [state {:keys [undo-changes redo-changes]}] - (-> state - (update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %)) - (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)))) - -(defn- append-undo - [entry] - (us/verify ::undo-entry entry) - (ptk/reify ::append-undo - ptk/UpdateEvent - (update [_ state] - (if (get-in state [:workspace-undo :transaction]) - (accumulate-undo-entry state entry) - (add-undo-entry state entry))))) - -(defonce empty-tx {:undo-changes [] :redo-changes []}) - -(defn start-undo-transaction [] - (ptk/reify ::start-undo-transaction - ptk/UpdateEvent - (update [_ state] - ;; We commit the old transaction before starting the new one - (let [current-tx (get-in state [:workspace-undo :transaction])] - (cond-> state - (nil? current-tx) (assoc-in [:workspace-undo :transaction] empty-tx)))))) - -(defn discard-undo-transaction [] - (ptk/reify ::discard-undo-transaction - ptk/UpdateEvent - (update [_ state] - (update state :workspace-undo dissoc :transaction)))) - -(defn commit-undo-transaction [] - (ptk/reify ::commit-undo-transaction - ptk/UpdateEvent - (update [_ state] - (-> state - (add-undo-entry (get-in state [:workspace-undo :transaction])) - (update :workspace-undo dissoc :transaction))))) - -(def pop-undo-into-transaction - (ptk/reify ::last-undo-into-transaction - ptk/UpdateEvent - (update [_ state] - (let [index (get-in state [:workspace-undo :index] -1)] - - (cond-> state - (>= 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 +;; These functions should've been in `src/app/main/data/workspace/undo.cljs` but doing that causes +;; a circular dependency with `src/app/main/data/workspace/changes.cljs` (def undo (ptk/reify ::undo ptk/WatchEvent @@ -370,8 +144,8 @@ index (or (:index undo) (dec (count items)))] (when-not (or (empty? items) (= index -1)) (let [changes (get-in items [index :undo-changes])] - (rx/of (materialize-undo changes (dec index)) - (commit-changes changes [] {:save-undo? false})))))))))) + (rx/of (dwu/materialize-undo changes (dec index)) + (dch/commit-changes changes [] {:save-undo? false})))))))))) (def redo (ptk/reify ::redo @@ -385,16 +159,8 @@ index (or (:index undo) (dec (count items)))] (when-not (or (empty? items) (= index (dec (count items)))) (let [changes (get-in items [(inc index) :redo-changes])] - (rx/of (materialize-undo changes (inc index)) - (commit-changes changes [] {:save-undo? false})))))))))) - -(def reinitialize-undo - (ptk/reify ::reset-undo - ptk/UpdateEvent - (update [_ state] - (assoc state :workspace-undo {})))) - - + (rx/of (dwu/materialize-undo changes (inc index)) + (dch/commit-changes changes [] {:save-undo? false})))))))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shapes @@ -420,93 +186,6 @@ ;; NOTE: This is a generic implementation for update multiple shapes ;; in one single commit/undo entry. -(s/def ::coll-of-uuid - (s/every ::us/uuid)) - -(defn update-shapes - ([ids f] (update-shapes ids f nil)) - ([ids f {:keys [reg-objects?] :or {reg-objects? false}}] - (us/assert ::coll-of-uuid ids) - (us/assert fn? f) - (ptk/reify ::update-shapes - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (lookup-page-objects state page-id)] - (loop [ids (seq ids) - rch [] - uch []] - (if (nil? ids) - (rx/of (commit-changes - (cond-> rch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)})) - (cond-> uch reg-objects? (conj {:type :reg-objects :page-id page-id :shapes (vec ids)})) - {:commit-local? true})) - - (let [id (first ids) - obj1 (get objects id) - obj2 (f obj1) - rch-operations (generate-operations obj1 obj2) - uch-operations (generate-operations obj2 obj1 true) - rchg {:type :mod-obj - :page-id page-id - :operations rch-operations - :id id} - uchg {:type :mod-obj - :page-id page-id - :operations uch-operations - :id id}] - (recur (next ids) - (if (empty? rch-operations) rch (conj rch rchg)) - (if (empty? uch-operations) uch (conj uch uchg))))))))))) - -(defn update-shapes-recursive - [ids f] - (us/assert ::coll-of-uuid ids) - (us/assert fn? f) - (letfn [(impl-get-children [objects id] - (cons id (cp/get-children id objects))) - - (impl-gen-changes [objects page-id ids] - (loop [sids (seq ids) - cids (seq (impl-get-children objects (first sids))) - rchanges [] - uchanges []] - (cond - (nil? sids) - [rchanges uchanges] - - (nil? cids) - (recur (next sids) - (seq (impl-get-children objects (first (next sids)))) - rchanges - uchanges) - - :else - (let [id (first cids) - obj1 (get objects id) - obj2 (f obj1) - rops (generate-operations obj1 obj2) - uops (generate-operations obj2 obj1 true) - rchg {:type :mod-obj - :page-id page-id - :operations rops - :id id} - uchg {:type :mod-obj - :page-id page-id - :operations uops - :id id}] - (recur sids - (next cids) - (conj rchanges rchg) - (conj uchanges uchg))))))] - (ptk/reify ::update-shapes-recursive - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (lookup-page-objects state page-id) - [rchanges uchanges] (impl-gen-changes objects page-id (seq ids))] - (rx/of (commit-changes rchanges uchanges {:commit-local? true}))))))) - (defn select-shapes [ids] @@ -639,7 +318,7 @@ (assoc :name name)))] (rx/concat - (rx/of (commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (select-shapes (d/ordered-set id))) (when (= :text (:type attrs)) (->> (rx/of (start-edition-mode id)) @@ -672,7 +351,7 @@ :page-id page-id :index index :shapes [shape-id]})))] - (rx/of (commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) (defn delete-shapes @@ -779,7 +458,7 @@ ;; (cljs.pprint/pprint rchanges) ;; (println "================ uchanges") ;; (cljs.pprint/pprint uchanges) - (rx/of (commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) ;; --- Add shape to Workspace diff --git a/frontend/src/app/main/data/workspace/drawing/common.cljs b/frontend/src/app/main/data/workspace/drawing/common.cljs index 922c796af..f31d5be14 100644 --- a/frontend/src/app/main/data/workspace/drawing/common.cljs +++ b/frontend/src/app/main/data/workspace/drawing/common.cljs @@ -12,6 +12,7 @@ [app.common.geom.shapes :as gsh] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.undo :as dwu] [app.main.streams :as ms] [app.main.worker :as uw])) @@ -54,7 +55,7 @@ ;; Add & select the created shape to the workspace (rx/concat (if (= :text (:type shape)) - (rx/of (dwc/start-undo-transaction)) + (rx/of (dwu/start-undo-transaction)) (rx/empty)) (rx/of (dwc/add-shape shape)) diff --git a/frontend/src/app/main/data/workspace/grid.cljs b/frontend/src/app/main/data/workspace/grid.cljs index fe4e439e2..13a327103 100644 --- a/frontend/src/app/main/data/workspace/grid.cljs +++ b/frontend/src/app/main/data/workspace/grid.cljs @@ -10,7 +10,7 @@ [potok.core :as ptk] [app.common.data :as d] [app.common.spec :as us] - [app.main.data.workspace.common :as dwc])) + [app.main.data.workspace.changes :as dch])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Grid @@ -48,7 +48,7 @@ grid {:type :square :params params :display true}] - (rx/of (dwc/update-shapes [frame-id] + (rx/of (dch/update-shapes [frame-id] (fn [obj] (update obj :grids (fnil #(conj % grid) []))))))))) @@ -57,14 +57,14 @@ (ptk/reify ::set-frame-grid ptk/WatchEvent (watch [_ state stream] - (rx/of (dwc/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) [])))))))) + (rx/of (dch/update-shapes [frame-id] (fn [o] (update o :grids (fnil #(d/remove-at-index % index) [])))))))) (defn set-frame-grid [frame-id index data] (ptk/reify ::set-frame-grid ptk/WatchEvent (watch [_ state stream] - (rx/of (dwc/update-shapes [frame-id] #(assoc-in % [:grids index] data)))))) + (rx/of (dch/update-shapes [frame-id] #(assoc-in % [:grids index] data)))))) (defn set-default-grid [type params] @@ -73,7 +73,7 @@ (watch [_ state stream] (let [pid (:current-page-id state) prev-value (get-in state [:workspace-data :pages-index pid :options :saved-grids type])] - (rx/of (dwc/commit-changes [{:type :set-option + (rx/of (dch/commit-changes [{:type :set-option :page-id pid :option [:saved-grids type] :value params}] diff --git a/frontend/src/app/main/data/workspace/groups.cljs b/frontend/src/app/main/data/workspace/groups.cljs index 4bb6736a0..886ff2fbd 100644 --- a/frontend/src/app/main/data/workspace/groups.cljs +++ b/frontend/src/app/main/data/workspace/groups.cljs @@ -4,6 +4,7 @@ [app.common.geom.shapes :as gsh] [app.common.pages :as cp] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] [beicon.core :as rx] [potok.core :as ptk])) @@ -106,7 +107,7 @@ shapes (shapes-for-grouping objects selected)] (when-not (empty? shapes) (let [[group rchanges uchanges] (prepare-create-group page-id shapes "Group-" false)] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) (def ungroup-selected @@ -122,7 +123,7 @@ (= (:type group) :group)) (let [[rchanges uchanges] (prepare-remove-group page-id group objects)] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))))) (def mask-group (ptk/reify ::mask-group @@ -176,7 +177,7 @@ :page-id page-id :shapes [(:id group)]})] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) (def unmask-group @@ -209,7 +210,7 @@ :page-id page-id :shapes [(:id group)]}]] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) diff --git a/frontend/src/app/main/data/workspace/libraries.cljs b/frontend/src/app/main/data/workspace/libraries.cljs index fa9d3b279..1d120c9a9 100644 --- a/frontend/src/app/main/data/workspace/libraries.cljs +++ b/frontend/src/app/main/data/workspace/libraries.cljs @@ -14,6 +14,7 @@ [app.common.geom.shapes :as geom] [app.main.data.messages :as dm] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries-helpers :as dwlh] [app.common.pages :as cp] [app.main.repo :as rp] @@ -90,7 +91,7 @@ uchg {:type :del-color :id id}] (rx/of #(assoc-in % [:workspace-local :color-for-rename] id) - (dwc/commit-changes [rchg] [uchg] {:commit-local? true}))))))) + (dch/commit-changes [rchg] [uchg] {:commit-local? true}))))))) (defn add-recent-color [color] @@ -100,7 +101,7 @@ (watch [_ state s] (let [rchg {:type :add-recent-color :color color}] - (rx/of (dwc/commit-changes [rchg] [] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchg] [] {:commit-local? true})))))) (def clear-color-for-rename (ptk/reify ::clear-color-for-rename @@ -120,7 +121,7 @@ :color color} uchg {:type :mod-color :color prev}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) (sync-file (:current-file-id state) file-id)))))) (defn delete-color @@ -134,7 +135,7 @@ :id id} uchg {:type :add-color :color prev}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) (defn add-media [{:keys [id] :as media}] @@ -147,7 +148,7 @@ :object obj} uchg {:type :del-media :id id}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) (defn rename-media [id new-name] @@ -169,7 +170,7 @@ :name (:name object) :path (:path object)}}]] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) (defn delete-media [{:keys [id] :as params}] @@ -182,7 +183,7 @@ :id id} uchg {:type :add-media :object prev}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) (defn add-typography ([typography] (add-typography typography true)) @@ -196,7 +197,7 @@ :typography typography} uchg {:type :del-typography :id (:id typography)}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) #(cond-> % edit? (assoc-in [:workspace-local :rename-typography] (:id typography)))))))))) @@ -213,7 +214,7 @@ :typography typography} uchg {:type :mod-typography :typography prev}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true}) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true}) (sync-file (:current-file-id state) file-id)))))) (defn delete-typography @@ -227,7 +228,7 @@ :id id} uchg {:type :add-typography :typography prev}] - (rx/of (dwc/commit-changes [rchg] [uchg] {:commit-local? true})))))) + (rx/of (dch/commit-changes [rchg] [uchg] {:commit-local? true})))))) (def add-component "Add a new component to current file library, from the currently selected shapes." @@ -242,7 +243,7 @@ (let [[group rchanges uchanges] (dwlh/generate-add-component selected objects page-id file-id)] (when-not (empty? rchanges) - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes (d/ordered-set (:id group)))))))))) (defn rename-component @@ -273,7 +274,7 @@ :path (:path component) :objects objects}]] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) (defn duplicate-component "Create a new component copied from the one with the given id." @@ -301,7 +302,7 @@ uchanges [{:type :del-component :id (:id new-shape)}]] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) (defn delete-component "Delete the component with the given id, from the current file library." @@ -321,7 +322,7 @@ :path (:path component) :shapes (vals (:objects component))}]] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) (defn instantiate-component "Create a new shape in the current page, from the component with the given id @@ -398,7 +399,7 @@ :ignore-touched true}) new-shapes)] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes (d/ordered-set (:id new-shape)))))))) (defn detach-component @@ -461,7 +462,7 @@ :val (:touched obj)}]}) shapes)] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) (defn nav-to-component-file [file-id] @@ -514,7 +515,7 @@ rchanges local-library)) - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true})))))) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true})))))) (defn update-component "Modify the component linked to the shape with the given id, in the @@ -569,11 +570,11 @@ file)) (rx/of (when (seq local-rchanges) - (dwc/commit-changes local-rchanges local-uchanges + (dch/commit-changes local-rchanges local-uchanges {:commit-local? true :file-id (:id local-library)})) (when (seq rchanges) - (dwc/commit-changes rchanges uchanges + (dch/commit-changes rchanges uchanges {:commit-local? true :file-id file-id}))))))) @@ -623,7 +624,7 @@ (rx/concat (rx/of (dm/hide-tag :sync-dialog)) (when rchanges - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true :file-id file-id}))) (when (not= file-id library-id) ;; When we have just updated the library file, give some time for the @@ -666,7 +667,7 @@ (log/debug :msg "SYNC-FILE (2nd stage) finished" :js/rchanges (log-changes rchanges file)) - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true :file-id file-id}))))))) (def ignore-sync diff --git a/frontend/src/app/main/data/workspace/path.cljs b/frontend/src/app/main/data/workspace/path.cljs index ed3d3c8ee..33a5c2b52 100644 --- a/frontend/src/app/main/data/workspace/path.cljs +++ b/frontend/src/app/main/data/workspace/path.cljs @@ -24,6 +24,7 @@ (d/export edition/start-move-path-point) (d/export edition/start-path-edit) (d/export edition/create-node-at-position) +(d/export edition/move-selected) ;; Selection (d/export selection/handle-selection) diff --git a/frontend/src/app/main/data/workspace/path/changes.cljs b/frontend/src/app/main/data/workspace/path/changes.cljs index 2d046fe6d..0df544a4f 100644 --- a/frontend/src/app/main/data/workspace/path/changes.cljs +++ b/frontend/src/app/main/data/workspace/path/changes.cljs @@ -7,7 +7,7 @@ (ns app.main.data.workspace.path.changes (:require [app.common.spec :as us] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.path.helpers :as helpers] [app.main.data.workspace.path.spec :as spec] [app.main.data.workspace.path.state :as st] @@ -67,7 +67,7 @@ (let [shape (get-in state (st/get-path state)) page-id (:current-page-id state) [rch uch] (generate-path-changes page-id shape old-content (:content shape))] - (rx/of (dwc/commit-changes rch uch {:commit-local? true}))) + (rx/of (dch/commit-changes rch uch {:commit-local? true}))) (rx/empty))))))) diff --git a/frontend/src/app/main/data/workspace/path/edition.cljs b/frontend/src/app/main/data/workspace/path/edition.cljs index 6ccb29a9c..9911a4b98 100644 --- a/frontend/src/app/main/data/workspace/path/edition.cljs +++ b/frontend/src/app/main/data/workspace/path/edition.cljs @@ -9,7 +9,7 @@ [app.common.data :as d] [app.common.geom.point :as gpt] [app.common.math :as mth] - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.helpers :as helpers] @@ -60,49 +60,65 @@ [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] - (rx/of (dwc/commit-changes rch uch {:commit-local? true}) + (rx/of (dch/commit-changes rch uch {:commit-local? true}) (selection/update-selection point-change) (fn [state] (update-in state [:workspace-local :edit-path id] dissoc :content-modifiers :moving-nodes :moving-handler))))))) +(defn modify-content-point + [content {dx :x dy :y} modifiers point] + (let [point-indices (upc/point-indices content point) ;; [indices] + handler-indices (upc/handler-indices content point) ;; [[index prefix]] + + modify-point + (fn [modifiers index] + (-> modifiers + (update index assoc :x dx :y dy))) + + modify-handler + (fn [modifiers [index prefix]] + (let [cx (d/prefix-keyword prefix :x) + cy (d/prefix-keyword prefix :y)] + (-> modifiers + (update index assoc cx dx cy dy))))] + + (as-> modifiers $ + (reduce modify-point $ point-indices) + (reduce modify-handler $ handler-indices)))) + +(defn set-move-modifier + [points move-modifier] + (ptk/reify ::set-modifiers + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + content (get-in state (st/get-path state :content)) + modifiers-reducer (partial modify-content-point content move-modifier) + content-modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) + content-modifiers (->> points + (reduce modifiers-reducer content-modifiers))] + + (-> state + (assoc-in [:workspace-local :edit-path id :content-modifiers] content-modifiers)))))) + (defn move-selected-path-point [from-point to-point] - (letfn [(modify-content-point [content {dx :x dy :y} modifiers point] - (let [point-indices (upc/point-indices content point) ;; [indices] - handler-indices (upc/handler-indices content point) ;; [[index prefix]] + (ptk/reify ::move-point + ptk/UpdateEvent + (update [_ state] + (let [id (st/get-path-id state) + content (get-in state (st/get-path state :content)) + delta (gpt/subtract to-point from-point) - modify-point - (fn [modifiers index] - (-> modifiers - (update index assoc :x dx :y dy))) + modifiers-reducer (partial modify-content-point content delta) - modify-handler - (fn [modifiers [index prefix]] - (let [cx (d/prefix-keyword prefix :x) - cy (d/prefix-keyword prefix :y)] - (-> modifiers - (update index assoc cx dx cy dy))))] + points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - (as-> modifiers $ - (reduce modify-point $ point-indices) - (reduce modify-handler $ handler-indices))))] + modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) + modifiers (->> points + (reduce modifiers-reducer modifiers))] - (ptk/reify ::move-point - ptk/UpdateEvent - (update [_ state] - (let [id (st/get-path-id state) - content (get-in state (st/get-path state :content)) - delta (gpt/subtract to-point from-point) - - modifiers-reducer (partial modify-content-point content delta) - - points (get-in state [:workspace-local :edit-path id :selected-points] #{}) - - modifiers (get-in state [:workspace-local :edit-path id :content-modifiers] {}) - modifiers (->> points - (reduce modifiers-reducer {}))] - - (-> state - (assoc-in [:workspace-local :edit-path id :moving-nodes] true) - (assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers))))))) + (-> state + (assoc-in [:workspace-local :edit-path id :moving-nodes] true) + (assoc-in [:workspace-local :edit-path id :content-modifiers] modifiers)))))) (declare drag-selected-points) @@ -126,7 +142,6 @@ ptk/WatchEvent (watch [_ state stream] (let [stopper (->> stream (rx/filter ms/mouse-up?)) - zoom (get-in state [:workspace-local :zoom]) id (get-in state [:workspace-local :edition]) snap-toggled (get-in state [:workspace-local :edit-path id :snap-toggled]) @@ -143,6 +158,73 @@ (rx/map #(move-selected-path-point start-position %))) (rx/of (apply-content-modifiers))))))) +(defn- get-displacement + "Retrieve the correct displacement delta point for the + provided direction speed and distances thresholds." + [direction] + (case direction + :up (gpt/point 0 (- 1)) + :down (gpt/point 0 1) + :left (gpt/point (- 1) 0) + :right (gpt/point 1 0))) + +(defn finish-move-selected [] + (ptk/reify ::move-selected + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition])] + (-> state + (update-in [:workspace-local :edit-path id] dissoc :current-move)))))) + +(defn move-selected + [direction shift?] + + (let [same-event (js/Symbol "same-event")] + (ptk/reify ::move-selected + IDeref + (-deref [_] direction) + + ptk/UpdateEvent + (update [_ state] + (let [id (get-in state [:workspace-local :edition]) + current-move (get-in state [:workspace-local :edit-path id :current-move])] + (if (nil? current-move) + (-> state + (assoc-in [:workspace-local :edit-path id :moving-nodes] true) + (assoc-in [:workspace-local :edit-path id :current-move] same-event)) + state))) + + ptk/WatchEvent + (watch [_ state stream] + (let [id (get-in state [:workspace-local :edition]) + current-move (get-in state [:workspace-local :edit-path id :current-move])] + (if (= same-event current-move) + (let [points (get-in state [:workspace-local :edit-path id :selected-points] #{}) + + move-events (->> stream + (rx/filter (ptk/type? ::move-selected)) + (rx/filter #(= direction (deref %)))) + + stopper (->> move-events (rx/debounce 100) (rx/take 1)) + + scale (if shift? (gpt/point 10) (gpt/point 1)) + + mov-vec (gpt/multiply (get-displacement direction) scale)] + + (rx/concat + (rx/merge + (->> move-events + (rx/take-until stopper) + (rx/scan #(gpt/add %1 mov-vec) (gpt/point 0 0)) + (rx/map #(set-move-modifier points %))) + + ;; First event is not read by the stream so we need to send it again + (rx/of (move-selected direction shift?))) + + (rx/of (apply-content-modifiers) + (finish-move-selected)))) + (rx/empty))))))) + (defn start-move-handler [index prefix] (ptk/reify ::start-move-handler diff --git a/frontend/src/app/main/data/workspace/path/helpers.cljs b/frontend/src/app/main/data/workspace/path/helpers.cljs index 79c120d4a..9f5fb0621 100644 --- a/frontend/src/app/main/data/workspace/path/helpers.cljs +++ b/frontend/src/app/main/data/workspace/path/helpers.cljs @@ -18,9 +18,6 @@ [app.util.path.subpaths :as ups] [potok.core :as ptk])) -;; CONSTANTS -(defonce enter-keycode 13) - (defn end-path-event? [{:keys [type shift] :as event}] (or (= (ptk/type event) ::common/finish-path) (= (ptk/type event) :esc-pressed) diff --git a/frontend/src/app/main/data/workspace/path/shortcuts.cljs b/frontend/src/app/main/data/workspace/path/shortcuts.cljs index 7c28926f6..2470f2bb3 100644 --- a/frontend/src/app/main/data/workspace/path/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/path/shortcuts.cljs @@ -32,61 +32,95 @@ (rx/empty)))))) (def shortcuts - {:move-nodes {:tooltip "V" - :command "v" - :fn #(st/emit! (drp/change-edit-mode :move))} + {:move-nodes {:tooltip "V" + :command "v" + :fn #(st/emit! (drp/change-edit-mode :move))} - :draw-nodes {:tooltip "P" - :command "p" - :fn #(st/emit! (drp/change-edit-mode :draw))} + :draw-nodes {:tooltip "P" + :command "p" + :fn #(st/emit! (drp/change-edit-mode :draw))} - :add-node {:tooltip "+" - :command "+" - :fn #(st/emit! (drp/add-node))} + :add-node {:tooltip "+" + :command "+" + :fn #(st/emit! (drp/add-node))} - :delete-node {:tooltip (ds/supr) - :command ["del" "backspace"] - :fn #(st/emit! (drp/remove-node))} + :delete-node {:tooltip (ds/supr) + :command ["del" "backspace"] + :fn #(st/emit! (drp/remove-node))} - :merge-nodes {:tooltip (ds/meta "J") - :command (ds/c-mod "j") - :fn #(st/emit! (drp/merge-nodes))} + :merge-nodes {:tooltip (ds/meta "J") + :command (ds/c-mod "j") + :fn #(st/emit! (drp/merge-nodes))} - :join-nodes {:tooltip "J" - :command "j" - :fn #(st/emit! (drp/join-nodes))} + :join-nodes {:tooltip "J" + :command "j" + :fn #(st/emit! (drp/join-nodes))} - :separate-nodes {:tooltip "K" - :command "k" - :fn #(st/emit! (drp/separate-nodes))} + :separate-nodes {:tooltip "K" + :command "k" + :fn #(st/emit! (drp/separate-nodes))} - :make-corner {:tooltip "B" - :command "b" - :fn #(st/emit! (drp/make-corner))} + :make-corner {:tooltip "B" + :command "b" + :fn #(st/emit! (drp/make-corner))} - :make-curve {:tooltip (ds/meta "B") - :command (ds/c-mod "b") - :fn #(st/emit! (drp/make-curve))} + :make-curve {:tooltip (ds/meta "B") + :command (ds/c-mod "b") + :fn #(st/emit! (drp/make-curve))} - :snap-nodes {:tooltip (ds/meta "'") - :command (ds/c-mod "'") - :fn #(st/emit! (drp/toggle-snap))} - - :escape {:tooltip (ds/esc) - :command "escape" - :fn #(st/emit! (esc-pressed))} + :snap-nodes {:tooltip (ds/meta "'") + :command (ds/c-mod "'") + :fn #(st/emit! (drp/toggle-snap))} - :start-editing {:tooltip (ds/enter) - :command "enter" - :fn #(st/emit! (dw/start-editing-selected))} + :escape {:tooltip (ds/esc) + :command "escape" + :fn #(st/emit! (esc-pressed))} - :undo {:tooltip (ds/meta "Z") - :command (ds/c-mod "z") - :fn #(st/emit! (drp/undo-path))} + :start-editing {:tooltip (ds/enter) + :command "enter" + :fn #(st/emit! (dw/start-editing-selected))} + + :undo {:tooltip (ds/meta "Z") + :command (ds/c-mod "z") + :fn #(st/emit! (drp/undo-path))} + + :redo {:tooltip (ds/meta "Y") + :command [(ds/c-mod "shift+z") (ds/c-mod "y")] + :fn #(st/emit! (drp/redo-path))} + + ;; Arrow movement + :move-fast-up {:tooltip (ds/shift ds/up-arrow) + :command "shift+up" + :fn #(st/emit! (drp/move-selected :up true))} + + :move-fast-down {:tooltip (ds/shift ds/down-arrow) + :command "shift+down" + :fn #(st/emit! (drp/move-selected :down true))} + + :move-fast-right {:tooltip (ds/shift ds/right-arrow) + :command "shift+right" + :fn #(st/emit! (drp/move-selected :right true))} + + :move-fast-left {:tooltip (ds/shift ds/left-arrow) + :command "shift+left" + :fn #(st/emit! (drp/move-selected :left true))} + + :move-unit-up {:tooltip ds/up-arrow + :command "up" + :fn #(st/emit! (drp/move-selected :up false))} + + :move-unit-down {:tooltip ds/down-arrow + :command "down" + :fn #(st/emit! (drp/move-selected :down false))} + + :move-unit-left {:tooltip ds/right-arrow + :command "right" + :fn #(st/emit! (drp/move-selected :right false))} + + :move-unit-right {:tooltip ds/left-arrow + :command "left" + :fn #(st/emit! (drp/move-selected :left false))} - :redo {:tooltip (ds/meta "Y") - :command [(ds/c-mod "shift+z") (ds/c-mod "y")] - :fn #(st/emit! (drp/redo-path))} }) (defn get-tooltip [shortcut] diff --git a/frontend/src/app/main/data/workspace/path/tools.cljs b/frontend/src/app/main/data/workspace/path/tools.cljs index 303e50428..c7cb1cd4e 100644 --- a/frontend/src/app/main/data/workspace/path/tools.cljs +++ b/frontend/src/app/main/data/workspace/path/tools.cljs @@ -6,7 +6,7 @@ (ns app.main.data.workspace.path.tools (:require - [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.path.changes :as changes] [app.main.data.workspace.path.common :as common] [app.main.data.workspace.path.state :as st] @@ -32,7 +32,7 @@ new-content (-> (tool-fn (:content shape) points) (ups/close-subpaths)) [rch uch] (changes/generate-path-changes page-id shape (:content shape) new-content)] - (rx/of (dwc/commit-changes rch uch {:commit-local? true}))))))) + (rx/of (dch/commit-changes rch uch {:commit-local? true}))))))) (defn make-corner ([] diff --git a/frontend/src/app/main/data/workspace/persistence.cljs b/frontend/src/app/main/data/workspace/persistence.cljs index 5d4add00b..7b5d4433b 100644 --- a/frontend/src/app/main/data/workspace/persistence.cljs +++ b/frontend/src/app/main/data/workspace/persistence.cljs @@ -17,6 +17,7 @@ [app.main.data.media :as di] [app.main.data.messages :as dm] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.svg-upload :as svg] @@ -51,7 +52,7 @@ (let [stoper (rx/filter #(= ::finalize %) stream) forcer (rx/filter #(= ::force-persist %) stream) notifier (->> stream - (rx/filter (ptk/type? ::dwc/commit-changes)) + (rx/filter dch/commit-changes?) (rx/debounce 2000) (rx/merge stoper forcer)) @@ -79,7 +80,7 @@ (st/emit! (update-persistence-status {:status :saved})))] (->> (rx/merge (->> stream - (rx/filter (ptk/type? ::dwc/commit-changes)) + (rx/filter dch/commit-changes?) (rx/map deref) (rx/filter local-file?) (rx/tap on-dirty) @@ -91,7 +92,7 @@ (rx/tap on-saving) (rx/take-until (rx/delay 100 stoper))) (->> stream - (rx/filter (ptk/type? ::dwc/commit-changes)) + (rx/filter dch/commit-changes?) (rx/map deref) (rx/filter library-file?) (rx/filter (complement #(empty? (:changes %)))) diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index 6812a5a9f..5db6c36e6 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -18,6 +18,7 @@ [app.common.spec :as us] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.modal :as md] [app.main.streams :as ms] [app.main.worker :as uw])) @@ -395,7 +396,7 @@ (map #(get-in % [:obj :id])) (into (d/ordered-set)))] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (select-shapes selected)))))) (defn change-hover-state diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index f10934f16..2ca3c32cc 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -10,6 +10,7 @@ [app.main.data.workspace :as dw] [app.main.data.workspace.colors :as mdc] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.drawing :as dwd] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.texts :as dwtxt] @@ -123,7 +124,7 @@ :clear-undo {:tooltip (ds/meta "Q") :command (ds/c-mod "q") - :fn #(st/emit! dwc/reinitialize-undo)} + :fn #(st/emit! dwu/reinitialize-undo)} :draw-frame {:tooltip "A" :command "a" diff --git a/frontend/src/app/main/data/workspace/svg_upload.cljs b/frontend/src/app/main/data/workspace/svg_upload.cljs index 1b3a479bf..a3950b64b 100644 --- a/frontend/src/app/main/data/workspace/svg_upload.cljs +++ b/frontend/src/app/main/data/workspace/svg_upload.cljs @@ -14,6 +14,7 @@ [app.common.pages :as cp] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.repo :as rp] [app.util.color :as uc] [app.util.path.parser :as upp] @@ -462,7 +463,7 @@ rchanges (conj rchanges reg-objects-action)] - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (rx/of (dch/commit-changes rchanges uchanges {:commit-local? true}) (dwc/select-shapes (d/ordered-set root-id)))) (catch :default e diff --git a/frontend/src/app/main/data/workspace/texts.cljs b/frontend/src/app/main/data/workspace/texts.cljs index 154930a99..c993d431a 100644 --- a/frontend/src/app/main/data/workspace/texts.cljs +++ b/frontend/src/app/main/data/workspace/texts.cljs @@ -15,6 +15,8 @@ [app.main.data.workspace.selection :as dws] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.transforms :as dwt] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.undo :as dwu] [app.main.fonts :as fonts] [app.util.object :as obj] [app.util.text-editor :as ted] @@ -77,8 +79,8 @@ (when (and (not= content (:content shape)) (some? (:current-page-id state))) (rx/of - (dwc/update-shapes [id] #(assoc % :content content)) - (dwc/commit-undo-transaction))))) + (dch/update-shapes [id] #(assoc % :content content)) + (dwu/commit-undo-transaction))))) (rx/of (dws/deselect-shape id) (dwc/delete-shapes [id]))))))) @@ -141,7 +143,7 @@ shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] - (rx/of (dwc/update-shapes shape-ids update-fn)))))) + (rx/of (dch/update-shapes shape-ids update-fn)))))) (defn update-paragraph-attrs [{:keys [id attrs]}] @@ -169,7 +171,7 @@ shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] - (rx/of (dwc/update-shapes shape-ids update-fn)))))))) + (rx/of (dch/update-shapes shape-ids update-fn)))))))) (defn update-text-attrs [{:keys [id attrs]}] @@ -187,7 +189,7 @@ update-fn #(update-shape % txt/is-text-node? attrs/merge attrs) shape-ids (cond (= (:type shape) :text) [id] (= (:type shape) :group) (cp/get-children id objects))] - (rx/of (dwc/update-shapes shape-ids update-fn))))))) + (rx/of (dch/update-shapes shape-ids update-fn))))))) ;; --- RESIZE UTILS @@ -218,58 +220,41 @@ ptk/WatchEvent (watch [_ state stream] (let [page-id (:current-page-id state) - - objects0 (get-in state [:workspace-file :data :pages-index page-id :objects]) - objects1 (get-in state [:workspace-data :pages-index page-id :objects])] - (if-not (every? #(contains? objects1(first %)) changes) + objects (get-in state [:workspace-data :pages-index page-id :objects])] + (if-not (every? #(contains? objects(first %)) changes) (rx/empty) - (let [change-text-shape - (fn [objects [id [new-width new-height]]] - (when (contains? objects id) - (let [shape (get objects id) - {:keys [selrect grow-type overflow-text]} (gsh/transform-shape shape) - {shape-width :width shape-height :height} selrect - modifier-width (gsh/resize-modifiers shape :width new-width) - modifier-height (gsh/resize-modifiers shape :height new-height) + (let [changes-map (->> changes (into {})) + ids (keys changes-map) + update-fn + (fn [shape] + (let [[new-width new-height] (get changes-map (:id shape)) + {:keys [selrect grow-type overflow-text]} (gsh/transform-shape shape) + {shape-width :width shape-height :height} selrect - shape (cond-> shape - (and overflow-text (not= :fixed grow-type)) - (assoc :overflow-text false) + modifier-width (gsh/resize-modifiers shape :width new-width) + modifier-height (gsh/resize-modifiers shape :height new-height)] - (and (= :fixed grow-type) (not overflow-text) (> new-height shape-height)) - (assoc :overflow-text true) + (cond-> shape + (and overflow-text (not= :fixed grow-type)) + (assoc :overflow-text false) - (and (= :fixed grow-type) overflow-text (<= new-height shape-height)) - (assoc :overflow-text false) + (and (= :fixed grow-type) (not overflow-text) (> new-height shape-height)) + (assoc :overflow-text true) - (and (not-changed? shape-width new-width) (= grow-type :auto-width)) - (-> (assoc :modifiers modifier-width) - (gsh/transform-shape)) + (and (= :fixed grow-type) overflow-text (<= new-height shape-height)) + (assoc :overflow-text false) - (and (not-changed? shape-height new-height) - (or (= grow-type :auto-height) (= grow-type :auto-width))) - (-> (assoc :modifiers modifier-height) - (gsh/transform-shape)))] - (assoc objects id shape)))) + (and (not-changed? shape-width new-width) (= grow-type :auto-width)) + (-> (assoc :modifiers modifier-width) + (gsh/transform-shape)) - undo-transaction (get-in state [:workspace-undo :transaction]) - objects2 (->> changes (reduce change-text-shape objects1)) + (and (not-changed? shape-height new-height) + (or (= grow-type :auto-height) (= grow-type :auto-width))) + (-> (assoc :modifiers modifier-height) + (gsh/transform-shape)))))] - regchg {:type :reg-objects - :page-id page-id - :shapes (vec (keys changes))} - - rchanges (dwc/generate-changes page-id objects1 objects2) - uchanges (dwc/generate-changes page-id objects2 objects0)] - - (if (seq rchanges) - (rx/concat - (when-not undo-transaction - (rx/of (dwc/start-undo-transaction))) - (rx/of (dwc/commit-changes (conj rchanges regchg) (conj uchanges regchg) {:commit-local? true})) - (when-not undo-transaction - (rx/of (dwc/discard-undo-transaction))))))))))) + (rx/of (dch/update-shapes ids update-fn {:reg-objects? true})))))))) ;; When a resize-event arrives we start "buffering" for a time ;; after that time we invoke `resize-text-batch` with all the changes diff --git a/frontend/src/app/main/data/workspace/transforms.cljs b/frontend/src/app/main/data/workspace/transforms.cljs index 12143a43c..65756a5d3 100644 --- a/frontend/src/app/main/data/workspace/transforms.cljs +++ b/frontend/src/app/main/data/workspace/transforms.cljs @@ -14,7 +14,9 @@ [app.common.pages :as cp] [app.common.spec :as us] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.selection :as dws] + [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.snap :as snap] [app.main.store :as st] @@ -140,7 +142,7 @@ (filter #(= :text (:type %))) (map :id))] (rx/concat - (rx/of (dwc/update-shapes text-shapes-ids #(assoc % :grow-type :fixed))) + (rx/of (dch/update-shapes text-shapes-ids #(assoc % :grow-type :fixed))) (->> ms/mouse-position (rx/with-latest vector ms/mouse-position-shift) (rx/map normalize-proportion-lock) @@ -259,9 +261,9 @@ :shapes [(:id shape)]})))] (when-not (empty? rch) - (rx/of dwc/pop-undo-into-transaction - (dwc/commit-changes rch uch {:commit-local? true}) - (dwc/commit-undo-transaction) + (rx/of dwu/pop-undo-into-transaction + (dch/commit-changes rch uch {:commit-local? true}) + (dwu/commit-undo-transaction) (dwc/expand-collapse frame-id))))))) (defn start-move @@ -452,35 +454,13 @@ (ptk/reify ::apply-modifiers ptk/WatchEvent (watch [_ state stream] - (let [page-id (:current-page-id state) - - objects0 (get-in state [:workspace-file :data :pages-index page-id :objects]) - objects1 (get-in state [:workspace-data :pages-index page-id :objects]) - - ;; ID's + Children ID's - ids-with-children (d/concat [] (mapcat #(cp/get-children % objects1) ids) ids) - - ;; For each shape applies the modifiers by transforming the objects - update-shape #(update %1 %2 gsh/transform-shape) - objects2 (reduce update-shape objects1 ids-with-children) - - regchg {:type :reg-objects - :page-id page-id - :shapes (vec ids)} - - ;; we need to generate redo chages from current - ;; state (with current temporal values) to new state but - ;; the undo should be calculated from clear current - ;; state (without temporal values in it, for this reason - ;; we have 3 different objects references). - - rchanges (conj (dwc/generate-changes page-id objects1 objects2) regchg) - uchanges (conj (dwc/generate-changes page-id objects2 objects0) regchg)] - - (rx/of (dwc/start-undo-transaction) - (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (let [objects (dwc/lookup-page-objects state) + children-ids (->> ids (mapcat #(cp/get-children % objects))) + ids-with-children (d/concat [] children-ids ids)] + (rx/of (dwu/start-undo-transaction) + (dch/update-shapes ids-with-children gsh/transform-shape {:reg-objects? true}) (clear-local-transform) - (dwc/commit-undo-transaction)))))) + (dwu/commit-undo-transaction)))))) ;; --- Update Dimensions diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs new file mode 100644 index 000000000..f0e752556 --- /dev/null +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -0,0 +1,134 @@ +;; 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.undo + (:require + [app.common.data :as d] + [app.common.geom.proportions :as gpr] + [app.common.geom.shapes :as gsh] + [app.common.pages :as cp] + [app.common.pages.spec :as spec] + [app.common.spec :as us] + [app.common.uuid :as uuid] + [app.main.worker :as uw] + [app.main.streams :as ms] + [app.util.logging :as log] + [app.util.timers :as ts] + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [clojure.set :as set] + [cuerdas.core :as str] + [potok.core :as ptk])) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Undo / Redo +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(s/def ::undo-changes ::cp/changes) +(s/def ::redo-changes ::cp/changes) +(s/def ::undo-entry + (s/keys :req-un [::undo-changes ::redo-changes])) + +(def MAX-UNDO-SIZE 50) + +(defn- conj-undo-entry + [undo data] + (let [undo (conj undo data) + cnt (count undo)] + (if (> cnt MAX-UNDO-SIZE) + (subvec undo (- cnt MAX-UNDO-SIZE)) + undo))) + +(defn- materialize-undo + [changes index] + (ptk/reify ::materialize-undo + ptk/UpdateEvent + (update [_ state] + (-> state + (update :workspace-data cp/process-changes changes) + (assoc-in [:workspace-undo :index] index))))) + +(defn- reset-undo + [index] + (ptk/reify ::reset-undo + ptk/UpdateEvent + (update [_ state] + (-> state + (update :workspace-undo dissoc :undo-index) + (update-in [:workspace-undo :items] (fn [queue] (into [] (take (inc index) queue)))))))) + +(defn- add-undo-entry + [state entry] + (if (and entry + (not-empty (:undo-changes entry)) + (not-empty (:redo-changes entry))) + (let [index (get-in state [:workspace-undo :index] -1) + items (get-in state [:workspace-undo :items] []) + items (->> items (take (inc index)) (into [])) + items (conj-undo-entry items entry)] + (-> state + (update :workspace-undo assoc :items items + :index (min (inc index) + (dec MAX-UNDO-SIZE))))) + state)) + +(defn- accumulate-undo-entry + [state {:keys [undo-changes redo-changes]}] + (-> state + (update-in [:workspace-undo :transaction :undo-changes] #(into undo-changes %)) + (update-in [:workspace-undo :transaction :redo-changes] #(into % redo-changes)))) + +(defn- append-undo + [entry] + (us/assert ::undo-entry entry) + (ptk/reify ::append-undo + ptk/UpdateEvent + (update [_ state] + (if (get-in state [:workspace-undo :transaction]) + (accumulate-undo-entry state entry) + (add-undo-entry state entry))))) + +(defonce empty-tx {:undo-changes [] :redo-changes []}) + +(defn start-undo-transaction [] + (ptk/reify ::start-undo-transaction + ptk/UpdateEvent + (update [_ state] + ;; We commit the old transaction before starting the new one + (let [current-tx (get-in state [:workspace-undo :transaction])] + (cond-> state + (nil? current-tx) (assoc-in [:workspace-undo :transaction] empty-tx)))))) + +(defn discard-undo-transaction [] + (ptk/reify ::discard-undo-transaction + ptk/UpdateEvent + (update [_ state] + (update state :workspace-undo dissoc :transaction)))) + +(defn commit-undo-transaction [] + (ptk/reify ::commit-undo-transaction + ptk/UpdateEvent + (update [_ state] + (-> state + (add-undo-entry (get-in state [:workspace-undo :transaction])) + (update :workspace-undo dissoc :transaction))))) + +(def pop-undo-into-transaction + (ptk/reify ::last-undo-into-transaction + ptk/UpdateEvent + (update [_ state] + (let [index (get-in state [:workspace-undo :index] -1)] + + (cond-> state + (>= index 0) (accumulate-undo-entry (get-in state [:workspace-undo :items index])) + (>= index 0) (update-in [:workspace-undo :index] dec)))))) + +(def reinitialize-undo + (ptk/reify ::reset-undo + ptk/UpdateEvent + (update [_ state] + (assoc state :workspace-undo {})))) + diff --git a/frontend/src/app/main/streams.cljs b/frontend/src/app/main/streams.cljs index c3b41c6b6..e71667fd6 100644 --- a/frontend/src/app/main/streams.cljs +++ b/frontend/src/app/main/streams.cljs @@ -11,8 +11,8 @@ [app.main.store :as st] [app.main.refs :as refs] [app.common.geom.point :as gpt] - [app.util.globals :as globals]) - (:import goog.events.KeyCodes)) + [app.util.globals :as globals] + [app.util.keyboard :as kbd])) ;; --- User Events @@ -113,8 +113,7 @@ ob (->> (rx/merge (->> st/stream (rx/filter keyboard-event?) - (rx/filter #(let [key (:key %)] - (= key KeyCodes.ALT))) + (rx/filter kbd/altKey?) (rx/map #(= :down (:type %)))) ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts, ;; that makes keyboard-alt stream registring the key pressed but @@ -130,10 +129,7 @@ ob (->> (rx/merge (->> st/stream (rx/filter keyboard-event?) - (rx/filter #(let [key (:key %)] - (or - (= key KeyCodes.CTRL) - (= key KeyCodes.META)))) + (rx/filter kbd/ctrlKey?) (rx/map #(= :down (:type %)))) ;; Fix a situation caused by using `ctrl+alt` kind of shortcuts, ;; that makes keyboard-alt stream registring the key pressed but diff --git a/frontend/src/app/main/ui/components/context_menu.cljs b/frontend/src/app/main/ui/components/context_menu.cljs index ef0b15230..50cfbfbde 100644 --- a/frontend/src/app/main/ui/components/context_menu.cljs +++ b/frontend/src/app/main/ui/components/context_menu.cljs @@ -46,7 +46,7 @@ (mf/use-callback (mf/deps top (:offset @local)) (fn [node] - (when (and node (not fixed?)) + (when (some? node) (let [{node-height :height} (dom/get-bounding-rect node) {window-height :height} (dom/get-window-size) target-offset (if (> (+ top node-height) window-height) diff --git a/frontend/src/app/main/ui/components/dropdown.cljs b/frontend/src/app/main/ui/components/dropdown.cljs index b1efae131..82738e985 100644 --- a/frontend/src/app/main/ui/components/dropdown.cljs +++ b/frontend/src/app/main/ui/components/dropdown.cljs @@ -3,10 +3,10 @@ [rumext.alpha :as mf] [app.common.uuid :as uuid] [app.util.dom :as dom] + [app.util.keyboard :as kbd] [goog.events :as events] [goog.object :as gobj]) - (:import goog.events.EventType - goog.events.KeyCodes)) + (:import goog.events.EventType)) (mf/defc dropdown' {::mf/wrap-props false} @@ -27,7 +27,7 @@ on-keyup (fn [event] - (when (= (.-keyCode event) 27) ; ESC + (when (kbd/esc? event) (on-close))) on-mount diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 094784369..d5822551c 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -200,7 +200,7 @@ (when selected? [:& file-menu {:files selected-file-objs :show? (:menu-open @local) - :left (:x (:menu-pos @local)) + :left (+ 24 (:x (:menu-pos @local))) :top (:y (:menu-pos @local)) :navigate? navigate? :on-edit on-edit diff --git a/frontend/src/app/main/ui/dashboard/search.cljs b/frontend/src/app/main/ui/dashboard/search.cljs index cd839cad9..2f9568e14 100644 --- a/frontend/src/app/main/ui/dashboard/search.cljs +++ b/frontend/src/app/main/ui/dashboard/search.cljs @@ -31,9 +31,10 @@ (if (:is-default team) (t locale "dashboard.your-penpot") (:name team)))) - (st/emit! (dd/search-files {:team-id (:id team) - :search-term search-term}) - (dd/clear-selected-files)))) + (when search-term + (st/emit! (dd/search-files {:team-id (:id team) + :search-term search-term}) + (dd/clear-selected-files))))) [:* [:header.dashboard-header diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index 64dd8ede3..f2b331a0e 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -14,6 +14,7 @@ [app.main.data.dashboard :as dd] [app.main.data.messages :as dm] [app.main.data.modal :as modal] + [app.main.data.users :as du] [app.main.refs :as refs] [app.main.repo :as rp] [app.main.store :as st] @@ -241,6 +242,8 @@ ::mf/register-as ::leave-and-reassign} [{:keys [members profile team accept]}] (let [form (fm/use-form :spec ::leave-modal-form :initial {}) + not-current-user? (fn [{:keys [id]}] (not= id (:id profile))) + members (->> members (filterv not-current-user?)) options (into [{:value "" :label (tr "modals.leave-and-reassign.select-memeber-to-promote")}] (map #(hash-map :label (:name %) :value (str (:id %))) members)) @@ -264,11 +267,14 @@ [:div.modal-content.generic-form [:p (tr "modals.leave-and-reassign.hint1" (:name team))] - [:p (tr "modals.leave-and-reassign.hint2")] - [:& fm/form {:form form} - [:& fm/select {:name :member-id - :options options}]]] + (if (empty? members) + [:p (tr "modals.leave-and-reassign.forbiden")] + [:* + [:p (tr "modals.leave-and-reassign.hint2")] + [:& fm/form {:form form} + [:& fm/select {:name :member-id + :options options}]]])] [:div.modal-footer [:div.action-buttons @@ -279,7 +285,7 @@ [:input.accept-button {:type "button" - :class (when-not (:valid @form) "btn-disabled") + :class (if (:valid @form) "primary" "btn-disabled") :disabled (not (:valid @form)) :value (tr "modals.leave-and-reassign.promote-and-leave") :on-click on-accept}]]]]])) @@ -314,7 +320,9 @@ (fn [] (let [team-id (:default-team-id profile)] (da/set-current-team! team-id) - (st/emit! (rt/nav :dashboard-projects {:team-id team-id}))))) + (st/emit! (modal/hide) + (du/fetch-teams) + (rt/nav :dashboard-projects {:team-id team-id}))))) leave-fn (mf/use-callback diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index f97b1a50d..d1126444e 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -303,7 +303,7 @@ (if (:is-default team) (tr "dashboard.your-penpot") (:name team)))) - (st/emitf (dd/fetch-team-members team) + (st/emit! (dd/fetch-team-members team) (dd/fetch-team-stats team)))) [:* diff --git a/frontend/src/app/main/ui/shapes/attrs.cljs b/frontend/src/app/main/ui/shapes/attrs.cljs index cb624ba17..8573682fd 100644 --- a/frontend/src/app/main/ui/shapes/attrs.cljs +++ b/frontend/src/app/main/ui/shapes/attrs.cljs @@ -121,10 +121,7 @@ (defn add-layer-props [attrs shape] (cond-> attrs (:opacity shape) - (obj/set! "opacity" (:opacity shape)) - - (and (:blend-mode shape) (not= (:blend-mode shape) :normal)) - (obj/set! "mixBlendMode" (d/name (:blend-mode shape))))) + (obj/set! "opacity" (:opacity shape)))) (defn extract-svg-attrs [render-id svg-defs svg-attrs] diff --git a/frontend/src/app/main/ui/shapes/shape.cljs b/frontend/src/app/main/ui/shapes/shape.cljs index 95d1007d8..88df3cff8 100644 --- a/frontend/src/app/main/ui/shapes/shape.cljs +++ b/frontend/src/app/main/ui/shapes/shape.cljs @@ -6,6 +6,7 @@ (ns app.main.ui.shapes.shape (:require + [app.common.data :as d] [app.common.uuid :as uuid] [app.main.ui.context :as muc] [app.main.ui.shapes.filters :as filters] @@ -23,7 +24,10 @@ render-id (mf/use-memo #(str (uuid/next))) filter-id (str "filter_" render-id) styles (-> (obj/new) - (obj/set! "pointerEvents" pointer-events)) + (obj/set! "pointerEvents" pointer-events) + + (cond-> (and (:blend-mode shape) (not= (:blend-mode shape) :normal)) + (obj/set! "mixBlendMode" (d/name (:blend-mode shape))))) {:keys [x y width height type]} shape frame? (= :frame type) diff --git a/frontend/src/app/main/ui/viewer/shapes.cljs b/frontend/src/app/main/ui/viewer/shapes.cljs index 05e5d4836..bbcc521ce 100644 --- a/frontend/src/app/main/ui/viewer/shapes.cljs +++ b/frontend/src/app/main/ui/viewer/shapes.cljs @@ -65,15 +65,15 @@ :frame frame :childs childs :is-child-selected? true}] - (when (and (:interactions shape) show-interactions?) + (when (:interactions shape) [:rect {:x (- x 1) :y (- y 1) :width (+ width 2) :height (+ height 2) :fill "#31EFB8" :stroke "#31EFB8" - :stroke-width 1 - :fill-opacity 0.2}])] + :stroke-width (if show-interactions? 1 0) + :fill-opacity (if show-interactions? 0.2 0)}])] ;; Don't wrap svg elements inside a otherwise some can break [:& component {:shape shape diff --git a/frontend/src/app/main/ui/viewer/thumbnails.cljs b/frontend/src/app/main/ui/viewer/thumbnails.cljs index 5079c9976..f2942eae8 100644 --- a/frontend/src/app/main/ui/viewer/thumbnails.cljs +++ b/frontend/src/app/main/ui/viewer/thumbnails.cljs @@ -24,8 +24,7 @@ [app.common.math :as mth] [app.util.router :as rt] [app.main.data.viewer :as vd]) - (:import goog.events.EventType - goog.events.KeyCodes)) + (:import goog.events.EventType)) (mf/defc thumbnails-content [{:keys [children expanded? total] :as props}] diff --git a/frontend/src/app/main/ui/workspace/colorpicker.cljs b/frontend/src/app/main/ui/workspace/colorpicker.cljs index f1ca966e0..993c1cdc8 100644 --- a/frontend/src/app/main/ui/workspace/colorpicker.cljs +++ b/frontend/src/app/main/ui/workspace/colorpicker.cljs @@ -164,8 +164,12 @@ on-select-library-color (fn [color] - (reset! state (data->state color)) - (on-change color)) + (let [editing-stop (:editing-stop @state) + is-gradient? (some? (:gradient color))] + (if (and (some? editing-stop) (not is-gradient?)) + (handle-change-color (color->components (:color color) (:opacity color))) + (do (reset! state (data->state color)) + (on-change color))))) on-add-library-color (fn [color] diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index 3d39ac33e..5720cd50b 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -10,6 +10,7 @@ [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.shortcuts :as sc] [app.main.refs :as refs] @@ -83,10 +84,10 @@ ;; We defer the execution so the mouse event won't close the editor (timers/schedule #(st/emit! (dw/start-editing-selected)))) do-update-component (st/emitf - (dwc/start-undo-transaction) + (dwu/start-undo-transaction) (dwl/update-component id) (dwl/sync-file current-file-id (:component-file shape)) - (dwc/commit-undo-transaction)) + (dwu/commit-undo-transaction)) confirm-update-remote-component (st/emitf (dwl/update-component id) (dwl/sync-file current-file-id diff --git a/frontend/src/app/main/ui/workspace/shapes/common.cljs b/frontend/src/app/main/ui/workspace/shapes/common.cljs index 4d5ad63e7..1c8dff918 100644 --- a/frontend/src/app/main/ui/workspace/shapes/common.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/common.cljs @@ -12,7 +12,8 @@ (defn generic-wrapper-factory [component] (mf/fnc generic-wrapper - {::mf/wrap-props false} + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] + ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape")] [:> shape-container {:shape shape} diff --git a/frontend/src/app/main/ui/workspace/shapes/frame.cljs b/frontend/src/app/main/ui/workspace/shapes/frame.cljs index 2f51adba3..71a38987a 100644 --- a/frontend/src/app/main/ui/workspace/shapes/frame.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/frame.cljs @@ -28,6 +28,28 @@ (contains? (:selected local) id)))] (l/derived check-moving refs/workspace-local))) +(defn check-props + ([props] (check-props props =)) + ([props eqfn?] + (fn [np op] + (every? #(eqfn? (unchecked-get np %) + (unchecked-get op %)) + props)))) + +(defn check-frame-props + "Checks for changes in the props of a frame" + [new-props old-props] + (let [new-shape (unchecked-get new-props "shape") + old-shape (unchecked-get old-props "shape") + + new-objects (unchecked-get new-props "objects") + old-objects (unchecked-get old-props "objects") + + new-children (->> new-shape :shapes (mapv #(get new-objects %))) + old-children (->> old-shape :shapes (mapv #(get old-objects %)))] + (and (= new-shape old-shape) + (= new-children old-children)))) + ;; This custom deffered don't deffer rendering when ghost rendering is ;; used. (defn custom-deferred @@ -48,7 +70,7 @@ [shape-wrapper] (let [frame-shape (frame/frame-shape shape-wrapper)] (mf/fnc frame-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "objects"])) custom-deferred] + {::mf/wrap [#(mf/memo' % check-frame-props) custom-deferred] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") diff --git a/frontend/src/app/main/ui/workspace/shapes/group.cljs b/frontend/src/app/main/ui/workspace/shapes/group.cljs index d7ce59151..d55215d1e 100644 --- a/frontend/src/app/main/ui/workspace/shapes/group.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/group.cljs @@ -30,7 +30,7 @@ [shape-wrapper] (let [group-shape (group/group-shape shape-wrapper)] (mf/fnc group-wrapper - {::mf/wrap [#(mf/memo' % (mf/check-props ["shape" "frame"]))] + {::mf/wrap [#(mf/memo' % (mf/check-props ["shape"]))] ::mf/wrap-props false} [props] (let [shape (unchecked-get props "shape") diff --git a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs index f273d4aa0..036e07a92 100644 --- a/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs +++ b/frontend/src/app/main/ui/workspace/shapes/text/editor.cljs @@ -18,6 +18,7 @@ [app.main.ui.cursors :as cur] [app.main.ui.shapes.text.styles :as sts] [app.util.dom :as dom] + [app.util.keyboard :as kbd] [app.util.object :as obj] [app.util.text-editor :as ted] [cuerdas.core :as str] @@ -25,8 +26,7 @@ [okulary.core :as l] [rumext.alpha :as mf]) (:import - goog.events.EventType - goog.events.KeyCodes)) + goog.events.EventType)) ;; --- Text Editor Rendering @@ -84,7 +84,7 @@ on-key-up (fn [event] (dom/stop-propagation event) - (when (= (.-keyCode event) 27) ; ESC + (when (kbd/esc? event) (do (st/emit! :interrupt) (st/emit! dw/clear-edition-mode)))) @@ -163,10 +163,18 @@ ::mf/wrap-props false ::mf/forward-ref true} [props ref] - (let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape")] - [:foreignObject {:transform (gsh/transform-matrix shape) - :x x :y y - :width (if (#{:auto-width} grow-type) 100000 width) - :height (if (#{:auto-height :auto-width} grow-type) 100000 height)} + (let [{:keys [id x y width height grow-type] :as shape} (obj/get props "shape") + clip-id (str "clip-" id)] + [:g.text-editor {:clip-path (str "url(#" clip-id ")")} + [:defs + ;; This clippath will cut the huge foreign object we use to calculate the automatic resize + [:clipPath {:id clip-id} + [:rect {:x x :y y + :width (+ width 8) :height (+ height 8) + :transform (gsh/transform-matrix shape)}]]] + [:foreignObject {:transform (gsh/transform-matrix shape) + :x x :y y + :width (if (#{:auto-width} grow-type) 100000 width) + :height (if (#{:auto-height :auto-width} grow-type) 100000 height)} - [:& text-shape-edit-html {:shape shape :key (str id)}]])) + [:& text-shape-edit-html {:shape shape :key (str id)}]]])) diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs index 1145deec6..059c816bb 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets.cljs @@ -19,6 +19,7 @@ [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.texts :as dwt] [app.main.exports :as exports] @@ -45,38 +46,6 @@ [okulary.core :as l] [rumext.alpha :as mf])) - -;; ---- Assets selection management - -(def empty-selection #{}) - -(defn toggle-select - [selected asset-id] - (if (contains? selected asset-id) - (disj selected asset-id) - (conj selected asset-id))) - -(defn replace-select - [selected asset-id] - #{asset-id}) - -(defn extend-select - [selected asset-id groups] - (let [assets (->> groups vals flatten) - clicked-idx (d/index-of-pred assets #(= (:id %) asset-id)) - selected-idx (->> selected - (map (fn [id] (d/index-of-pred assets - #(= (:id %) id))))) - min-idx (apply min (conj selected-idx clicked-idx)) - max-idx (apply max (conj selected-idx clicked-idx))] - - (->> assets - d/enumerate - (filter #(<= min-idx (first %) max-idx)) - (map #(-> % second :id)) - set))) - - ;; ---- Group assets management ---- (s/def ::asset-name ::us/not-empty-string) @@ -149,40 +118,42 @@ ;; ---- Components box ---- (mf/defc components-box - [{:keys [file-id local? components listing-thumbs? open? change-selected] :as props}] + [{:keys [file-id local? components listing-thumbs? open? selected-assets + on-asset-click on-assets-delete on-clear-selection] :as props}] (let [state (mf/use-state {:menu-open false :renaming nil :top nil :left nil :component-id nil - :selected empty-selection :folded-groups empty-folded-groups}) + selected-components (:components selected-assets) + multi-components? (> (count selected-components) 1) + multi-assets? (or (not (empty? (:graphics selected-assets))) + (not (empty? (:colors selected-assets))) + (not (empty? (:typographies selected-assets)))) + groups (group-assets components) - selected (:selected @state) folded-groups (:folded-groups @state) on-duplicate (mf/use-callback (mf/deps @state) (fn [] - (if (empty? selected) + (if (empty? selected-components) (st/emit! (dwl/duplicate-component {:id (:component-id @state)})) (do - (st/emit! (dwc/start-undo-transaction)) - (apply st/emit! (map #(dwl/duplicate-component {:id %}) selected)) - (st/emit! (dwc/commit-undo-transaction)))))) + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! (map #(dwl/duplicate-component {:id %}) selected-components)) + (st/emit! (dwu/commit-undo-transaction)))))) on-delete (mf/use-callback - (mf/deps @state) + (mf/deps @state file-id multi-components? multi-assets?) (fn [] - (if (empty? selected) - (st/emit! (dwl/delete-component {:id (:component-id @state)})) - (do - (st/emit! (dwc/start-undo-transaction)) - (apply st/emit! (map #(dwl/delete-component {:id %}) selected)) - (st/emit! (dwc/commit-undo-transaction)))) + (if (or multi-components? multi-assets?) + (on-assets-delete) + (st/emit! (dwl/delete-component {:id (:component-id @state)}))) (st/emit! (dwl/sync-file file-id file-id)))) on-rename @@ -205,51 +176,35 @@ on-context-menu (mf/use-callback - (fn [component-id] - (fn [event] - (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (- (:x pos) 20)] - (dom/prevent-default event) - (swap! state assoc :menu-open true - :top top - :left left - :component-id component-id)))))) - - unselect-all - (mf/use-callback - (fn [event] - (swap! state assoc :selected empty-selection))) - - on-select - (mf/use-callback + (mf/deps selected-components on-clear-selection) (fn [component-id] (fn [event] - (dom/stop-propagation event) - (swap! state update :selected - (fn [selected] - (cond - (kbd/ctrl? event) - (toggle-select selected component-id) - - (kbd/shift? event) - (extend-select selected component-id groups))))))) + (when local? + (let [pos (dom/get-client-position event) + top (:y pos) + left (- (:x pos) 20)] + (dom/prevent-default event) + (when-not (contains? selected-components component-id) + (on-clear-selection)) + (swap! state assoc :menu-open true + :top top + :left left + :component-id component-id)))))) create-group (mf/use-callback - (mf/deps components selected) + (mf/deps components selected-components on-clear-selection) (fn [name] - (swap! state assoc :selected empty-selection) - (st/emit! (dwc/start-undo-transaction)) + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) (apply st/emit! (->> components - (filter #(contains? selected (:id %))) + (filter #(contains? selected-components (:id %))) (map #(dwl/rename-component (:id %) (str name " / " (cp/merge-path-item (:path %) (:name %))))))) - (st/emit! (dwc/commit-undo-transaction)))) + (st/emit! (dwu/commit-undo-transaction)))) on-fold-group (mf/use-callback @@ -262,7 +217,7 @@ on-group (mf/use-callback - (mf/deps components selected) + (mf/deps components selected-components) (fn [event] (dom/stop-propagation event) (modal/show! :create-group-dialog {:create create-group}))) @@ -274,11 +229,7 @@ :component component}) (dnd/set-allowed-effect! event "move")))] - (mf/use-effect - (mf/deps [change-selected selected]) - #(change-selected (count selected))) - - [:div.asset-section {:on-click unselect-all} + [:div.asset-section [:div.asset-title {:class (when (not open?) "closed")} [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :components (not open?)))} i/arrow-slide (tr "workspace.assets.components")] @@ -308,11 +259,11 @@ (let [renaming? (= (:renaming @state)(:id component))] [:div {:key (:id component) :class-name (dom/classnames - :selected (contains? selected (:id component)) + :selected (contains? selected-components (:id component)) :grid-cell @listing-thumbs? :enum-item (not @listing-thumbs?)) :draggable true - :on-click (on-select (:id component)) + :on-click #(on-asset-click % (:id component) groups nil) :on-context-menu (on-context-menu (:id component)) :on-drag-start (partial on-drag-start component)} [:& exports/component-svg {:group (get-in component [:objects (:id component)]) @@ -340,28 +291,35 @@ :on-close #(swap! state assoc :menu-open false) :top (:top @state) :left (:left @state) - :options [(when (<= (count selected) 1) + :options [(when-not (or multi-components? multi-assets?) [(tr "workspace.assets.rename") on-rename]) - [(tr "workspace.assets.duplicate") on-duplicate] + (when-not multi-assets? + [(tr "workspace.assets.duplicate") on-duplicate]) [(tr "workspace.assets.delete") on-delete] - [(tr "workspace.assets.group") on-group]]}])])) + (when-not multi-assets? + [(tr "workspace.assets.group") on-group])]}])])) ;; ---- Graphics box ---- (mf/defc graphics-box - [{:keys [file-id local? objects listing-thumbs? open? change-selected] :as props}] + [{:keys [file-id local? objects listing-thumbs? open? selected-assets + on-asset-click on-assets-delete on-clear-selection] :as props}] (let [input-ref (mf/use-ref nil) state (mf/use-state {:menu-open false :renaming nil :top nil :left nil :object-id nil - :selected empty-selection :folded-groups empty-folded-groups}) + selected-objects (:graphics selected-assets) + multi-objects? (> (count selected-objects) 1) + multi-assets? (or (not (empty? (:components selected-assets))) + (not (empty? (:colors selected-assets))) + (not (empty? (:typographies selected-assets)))) + groups (group-assets objects) - selected (:selected @state) folded-groups (:folded-groups @state) add-graphic @@ -370,7 +328,7 @@ (st/emitf (dwl/set-assets-box-open file-id :graphics true)) (dom/click (mf/ref-val input-ref)))) - on-selected + on-file-selected (mf/use-callback (mf/deps file-id) (fn [blobs] @@ -380,14 +338,11 @@ on-delete (mf/use-callback - (mf/deps @state) + (mf/deps @state multi-objects? multi-assets?) (fn [] - (if (empty? selected) - (st/emit! (dwl/delete-media {:id (:object-id @state)})) - (do - (st/emit! (dwc/start-undo-transaction)) - (apply st/emit! (map #(dwl/delete-media {:id %}) selected)) - (st/emit! (dwc/commit-undo-transaction)))))) + (if (or multi-objects? multi-assets?) + (on-assets-delete) + (st/emit! (dwl/delete-media {:id (:object-id @state)}))))) on-rename (mf/use-callback @@ -409,51 +364,35 @@ on-context-menu (mf/use-callback - (fn [object-id] - (fn [event] - (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (- (:x pos) 20)] - (dom/prevent-default event) - (swap! state assoc :menu-open true - :top top - :left left - :object-id object-id)))))) - - unselect-all - (mf/use-callback - (fn [event] - (swap! state assoc :selected empty-selection))) - - on-select - (mf/use-callback + (mf/deps selected-objects on-clear-selection) (fn [object-id] (fn [event] - (dom/stop-propagation event) - (swap! state update :selected - (fn [selected] - (cond - (kbd/ctrl? event) - (toggle-select selected object-id) - - (kbd/shift? event) - (extend-select selected object-id groups))))))) + (when local? + (let [pos (dom/get-client-position event) + top (:y pos) + left (- (:x pos) 20)] + (dom/prevent-default event) + (when-not (contains? selected-objects object-id) + (on-clear-selection)) + (swap! state assoc :menu-open true + :top top + :left left + :object-id object-id)))))) create-group (mf/use-callback - (mf/deps objects selected) + (mf/deps objects selected-objects on-clear-selection) (fn [name] - (swap! state assoc :selected empty-selection) - (st/emit! (dwc/start-undo-transaction)) + (on-clear-selection) + (st/emit! (dwu/start-undo-transaction)) (apply st/emit! (->> objects - (filter #(contains? selected (:id %))) + (filter #(contains? selected-objects (:id %))) (map #(dwl/rename-media (:id %) (str name " / " (cp/merge-path-item (:path %) (:name %))))))) - (st/emit! (dwc/commit-undo-transaction)))) + (st/emit! (dwu/commit-undo-transaction)))) on-fold-group (mf/use-callback @@ -466,7 +405,7 @@ on-group (mf/use-callback - (mf/deps objects selected) + (mf/deps objects selected-objects) (fn [event] (dom/stop-propagation event) (modal/show! :create-group-dialog {:create create-group}))) @@ -479,11 +418,7 @@ (dnd/set-data! event "text/asset-type" mtype) (dnd/set-allowed-effect! event "move")))] - (mf/use-effect - (mf/deps [change-selected selected]) - #(change-selected (count selected))) - - [:div.asset-section {:on-click unselect-all} + [:div.asset-section [:div.asset-title {:class (when (not open?) "closed")} [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :graphics (not open?)))} i/arrow-slide (tr "workspace.assets.graphics")] @@ -494,7 +429,7 @@ [:& file-uploader {:accept cm/str-media-types :multi true :input-ref input-ref - :on-selected on-selected}]])] + :on-selected on-file-selected}]])] (when open? (for [group groups] (let [path (first group) @@ -518,11 +453,11 @@ (for [object objects] [:div {:key (:id object) :class-name (dom/classnames - :selected (contains? selected (:id object)) + :selected (contains? selected-objects (:id object)) :grid-cell @listing-thumbs? :enum-item (not @listing-thumbs?)) :draggable true - :on-click (on-select (:id object)) + :on-click #(on-asset-click % (:id object) groups nil) :on-context-menu (on-context-menu (:id object)) :on-drag-start (partial on-drag-start object)} [:img {:src (cfg/resolve-file-media object true) @@ -552,16 +487,18 @@ :on-close #(swap! state assoc :menu-open false) :top (:top @state) :left (:left @state) - :options [(when (<= (count selected) 1) + :options [(when-not (or multi-objects? multi-assets?) [(tr "workspace.assets.rename") on-rename]) [(tr "workspace.assets.delete") on-delete] - [(tr "workspace.assets.group") on-group]]}])])) + (when-not multi-assets? + [(tr "workspace.assets.group") on-group])]}])])) ;; ---- Colors box ---- (mf/defc color-item - [{:keys [color local? file-id selected on-select locale] :as props}] + [{:keys [color local? file-id selected-colors multi-colors? multi-assets? + on-asset-click on-assets-delete on-clear-selection colors locale] :as props}] (let [rename? (= (:color-for-rename @refs/workspace-local) (:id color)) id (:id color) input-ref (mf/use-ref) @@ -575,10 +512,8 @@ (:color color) (:color color) :else (:value color)) - click-color - (fn [event] - (when on-select - ((on-select (:id color)) event)) + apply-color + (fn [color-id event] (let [ids (get-in @st/state [:workspace-local :selected])] (if (kbd/shift? event) (st/emit! (dc/change-stroke ids color)) @@ -594,13 +529,12 @@ (st/emit! (dwl/update-color updated-color file-id)))) delete-color - (fn [] - (if (empty? selected) - (st/emit! (dwl/delete-color color)) - (do - (st/emit! (dwc/start-undo-transaction)) - (apply st/emit! (map #(dwl/delete-color {:id %}) selected)) - (st/emit! (dwc/commit-undo-transaction))))) + (mf/use-callback + (mf/deps @state multi-colors? multi-assets?) + (fn [] + (if (or multi-colors? multi-assets?) + (on-assets-delete) + (st/emit! (dwl/delete-color color))))) rename-color-clicked (fn [event] @@ -634,16 +568,20 @@ :position :right})) on-context-menu - (fn [event] - (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (+ 10 (:x pos))] - (dom/prevent-default event) - (swap! state assoc - :menu-open true - :top top - :left left))))] + (mf/use-callback + (mf/deps color selected-colors on-clear-selection) + (fn [event] + (when local? + (let [pos (dom/get-client-position event) + top (:y pos) + left (+ 10 (:x pos))] + (dom/prevent-default event) + (when-not (contains? selected-colors (:id color)) + (on-clear-selection)) + (swap! state assoc + :menu-open true + :top top + :left left)))))] (mf/use-effect (mf/deps (:editing @state)) @@ -653,10 +591,11 @@ nil)) [:div.asset-list-item {:class-name (dom/classnames - :selected (contains? selected (:id color))) + :selected (contains? selected-colors (:id color))) :on-context-menu on-context-menu :on-click (when-not (:editing @state) - click-color)} + #(on-asset-click % (:id color) {"" colors} + (partial apply-color (:id color))))} [:& bc/color-bullet {:color color}] (if (:editing @state) @@ -679,17 +618,20 @@ :on-close #(swap! state assoc :menu-open false) :top (:top @state) :left (:left @state) - :options [(when (<= (count selected) 1) + :options [(when-not (or multi-colors? multi-assets?) [(t locale "workspace.assets.rename") rename-color-clicked]) - (when (<= (count selected) 1) + (when-not (or multi-colors? multi-assets?) [(t locale "workspace.assets.edit") edit-color-clicked]) [(t locale "workspace.assets.delete") delete-color]]}])])) (mf/defc colors-box - [{:keys [file-id local? colors locale open? change-selected] :as props}] - (let [state (mf/use-state {:selected empty-selection}) - - selected (:selected @state) + [{:keys [file-id local? colors locale open? selected-assets + on-asset-click on-assets-delete on-clear-selection] :as props}] + (let [selected-colors (:colors selected-assets) + multi-colors? (> (count selected-colors) 1) + multi-assets? (or (not (empty? (:components selected-assets))) + (not (empty? (:graphics selected-assets))) + (not (empty? (:typographies selected-assets)))) add-color (mf/use-callback @@ -708,32 +650,9 @@ :on-accept add-color :data {:color "#406280" :opacity 1} - :position :right}))) + :position :right})))] - unselect-all - (mf/use-callback - (fn [event] - (swap! state assoc :selected empty-selection))) - - on-select - (mf/use-callback - (fn [color-id] - (fn [event] - (dom/stop-propagation event) - (swap! state update :selected - (fn [selected] - (cond - (kbd/ctrl? event) - (toggle-select selected color-id) - - (kbd/shift? event) - (extend-select selected color-id {"" colors})))))))] - - (mf/use-effect - (mf/deps [change-selected selected]) - #(change-selected (count selected))) - - [:div.asset-section {:on-click unselect-all} + [:div.asset-section [:div.asset-title {:class (when (not open?) "closed")} [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :colors (not open?)))} i/arrow-slide (t locale "workspace.assets.colors")] @@ -751,24 +670,34 @@ :color color :file-id file-id :local? local? - :selected selected - :on-select on-select + :selected-colors selected-colors + :multi-colors? multi-colors? + :multi-assets? multi-assets? + :on-asset-click on-asset-click + :on-assets-delete on-assets-delete + :on-clear-selection on-clear-selection + :colors colors :locale locale}]))])])) ;; ---- Typography box ---- (mf/defc typography-box - [{:keys [file file-id local? typographies locale open? change-selected] :as props}] - + [{:keys [file file-id local? typographies locale open? selected-assets + on-asset-click on-assets-delete on-clear-selection] :as props}] (let [state (mf/use-state {:detail-open? false :menu-open? false :top nil :left nil - :selected empty-selection}) + :id nil}) local (deref refs/workspace-local) - selected (:selected @state) + + selected-typographies (:typographies selected-assets) + multi-typographies? (> (count selected-typographies) 1) + multi-assets? (or (not (empty? (:graphics selected-assets))) + (not (empty? (:colors selected-assets))) + (not (empty? (:typographies selected-assets)))) add-typography (mf/use-callback @@ -782,19 +711,8 @@ (fn [typography changes] (st/emit! (dwl/update-typography (merge typography changes) file-id)))) - handle-typography-selection + apply-typography (fn [typography event] - (dom/stop-propagation event) - - (swap! state update :selected - (fn [selected] - (cond - (kbd/ctrl? event) - (toggle-select selected (:id typography)) - - (kbd/shift? event) - (extend-select selected (:id typography) {"" typographies})))) - (let [ids (get-in @st/state [:workspace-local :selected]) attrs (merge {:typography-ref-file file-id @@ -804,23 +722,21 @@ ids))) on-context-menu - (fn [id event] - - (when local? - (let [pos (dom/get-client-position event) - top (:y pos) - left (- (:x pos) 20)] - (dom/prevent-default event) - (swap! state assoc - :menu-open? true - :top top - :left left - :id id)))) - - unselect-all (mf/use-callback - (fn [event] - (swap! state assoc :selected empty-selection))) + (mf/deps selected-typographies on-clear-selection) + (fn [id event] + (when local? + (let [pos (dom/get-client-position event) + top (:y pos) + left (- (:x pos) 20)] + (dom/prevent-default event) + (when-not (contains? selected-typographies id) + (on-clear-selection)) + (swap! state assoc + :menu-open? true + :top top + :left left + :id id))))) closed-typography-edit (mf/use-callback @@ -836,13 +752,12 @@ (st/emit! #(assoc-in % [:workspace-local :edit-typography] (:id @state)))) handle-delete-typography - (fn [] - (if (empty? selected) - (st/emit! (dwl/delete-typography (:id @state))) - (do - (st/emit! (dwc/start-undo-transaction)) - (apply st/emit! (map #(dwl/delete-typography %) selected)) - (st/emit! (dwc/commit-undo-transaction))))) + (mf/use-callback + (mf/deps @state multi-typographies? multi-assets?) + (fn [] + (if (or multi-typographies? multi-assets?) + (on-assets-delete) + (st/emit! (dwl/delete-typography (:id @state)))))) editting-id (or (:rename-typography local) (:edit-typography local))] @@ -854,11 +769,7 @@ (when (:edit-typography local) (st/emit! #(update % :workspace-local dissoc :edit-typography))))) - (mf/use-effect - (mf/deps [change-selected selected]) - #(change-selected (count selected))) - - [:div.asset-section {:on-click unselect-all} + [:div.asset-section [:div.asset-title {:class (when (not open?) "closed")} [:span {:on-click (st/emitf (dwl/set-assets-box-open file-id :typographies (not open?)))} i/arrow-slide (t locale "workspace.assets.typography")] @@ -872,9 +783,9 @@ :on-close #(swap! state assoc :menu-open? false) :top (:top @state) :left (:left @state) - :options [(when (<= (count selected) 1) + :options [(when-not (or multi-typographies? multi-assets?) [(t locale "workspace.assets.rename") handle-rename-typography-clicked]) - (when (<= (count selected) 1) + (when-not (or multi-typographies? multi-assets?) [(t locale "workspace.assets.edit") handle-edit-typography-clicked]) [(t locale "workspace.assets.delete") handle-delete-typography]]}] (when open? @@ -887,8 +798,9 @@ :read-only? (not local?) :on-context-menu #(on-context-menu (:id typography) %) :on-change #(handle-change typography %) - :selected? (contains? selected (:id typography)) - :on-select #(handle-typography-selection typography %) + :selected? (contains? selected-typographies (:id typography)) + :on-click #(on-asset-click % (:id typography) {"" typographies} + (partial apply-typography typography)) :editting? (= editting-id (:id typography)) :focus-name? (= (:rename-typography local) (:id typography))}])])])) @@ -961,16 +873,19 @@ reverse-sort? (mf/use-state false) listing-thumbs? (mf/use-state true) - selected-count (mf/use-state {:components 0 - :graphics 0 - :colors 0 - :typographies 0}) + + selected-assets (mf/use-state {:components #{} + :graphics #{} + :colors #{} + :typographies #{}}) + + selected-count (+ (count (:components @selected-assets)) + (count (:graphics @selected-assets)) + (count (:colors @selected-assets)) + (count (:typographies @selected-assets))) toggle-open (st/emitf (dwl/set-assets-box-open (:id file) :library (not open?))) - change-selected-count (fn [asset-type cnt] - (swap! selected-count assoc asset-type cnt)) - url (rt/resolve router :workspace {:project-id (:project-id file) :file-id (:id file)} @@ -996,9 +911,84 @@ toggle-listing (mf/use-callback (fn [event] - (swap! listing-thumbs? not)))] + (swap! listing-thumbs? not))) - [:div.tool-window {:on-context-menu #(dom/prevent-default %)} + toggle-selected-asset + (mf/use-callback + (mf/deps @selected-assets) + (fn [asset-type asset-id] + (swap! selected-assets update asset-type + (fn [selected] + (if (contains? selected asset-id) + (disj selected asset-id) + (conj selected asset-id)))))) + + extend-selected-assets + (mf/use-callback + (mf/deps @selected-assets) + (fn [asset-type asset-id asset-groups] + (swap! selected-assets update asset-type + (fn [selected] + (let [all-assets (-> asset-groups vals flatten) + clicked-idx (d/index-of-pred all-assets #(= (:id %) asset-id)) + selected-idx (->> selected + (map (fn [id] + (d/index-of-pred all-assets + #(= (:id %) id))))) + min-idx (apply min (conj selected-idx clicked-idx)) + max-idx (apply max (conj selected-idx clicked-idx))] + + (->> all-assets + d/enumerate + (filter #(<= min-idx (first %) max-idx)) + (map #(-> % second :id)) + set)))))) + + unselect-all + (mf/use-callback + (fn [] + (swap! selected-assets {:components #{} + :graphics #{} + :colors #{} + :typographies #{}}))) + + on-asset-click + (mf/use-callback + (mf/deps toggle-selected-asset extend-selected-assets) + (fn [asset-type event asset-id all-assets default-click] + (cond + (kbd/ctrl? event) + (do + (dom/stop-propagation event) + (toggle-selected-asset asset-type asset-id)) + + (kbd/shift? event) + (do + (dom/stop-propagation event) + (extend-selected-assets asset-type asset-id all-assets)) + + :else + (when default-click + (default-click event))))) + + on-assets-delete + (mf/use-callback + (mf/deps @selected-assets) + (fn [] + (do + (st/emit! (dwu/start-undo-transaction)) + (apply st/emit! (map #(dwl/delete-component {:id %}) + (:components @selected-assets))) + (apply st/emit! (map #(dwl/delete-media {:id %}) + (:graphics @selected-assets))) + (apply st/emit! (map #(dwl/delete-color {:id %}) + (:colors @selected-assets))) + (apply st/emit! (map #(dwl/delete-typography %) + (:typographies @selected-assets))) + (st/emit! (dwu/commit-undo-transaction)))))] + + [:div.tool-window {:on-context-menu #(dom/prevent-default %) + :on-click unselect-all} [:div.tool-window-bar.library-bar {:on-click toggle-open} [:div.collapse-library @@ -1037,14 +1027,9 @@ (str/empty? (:term filters))))] [:div.tool-window-content [:div.listing-options - [:span.selected-count - (let [selected-count @selected-count - total (+ (:components selected-count) - (:graphics selected-count) - (:colors selected-count) - (:typographies selected-count))] - (when (> total 0) - (tr "workspace.assets.selected-count" (i18n/c total))))] + (when (> selected-count 0) + [:span.selected-count + (tr "workspace.assets.selected-count" (i18n/c selected-count))]) [:div.listing-option-btn.first {:on-click toggle-sort} (if @reverse-sort? i/sort-descending @@ -1053,30 +1038,38 @@ (if @listing-thumbs? i/listing-enum i/listing-thumbs)]] + (when show-components? [:& components-box {:file-id (:id file) :local? local? :components components :listing-thumbs? listing-thumbs? - :change-selected (partial change-selected-count - :components) - :open? (open-box? :components)}]) + :open? (open-box? :components) + :selected-assets @selected-assets + :on-asset-click (partial on-asset-click :components) + :on-assets-delete on-assets-delete + :on-clear-selection unselect-all}]) + (when show-graphics? [:& graphics-box {:file-id (:id file) :local? local? :objects media :listing-thumbs? listing-thumbs? - :change-selected (partial change-selected-count - :graphics) - :open? (open-box? :graphics)}]) + :open? (open-box? :graphics) + :selected-assets @selected-assets + :on-asset-click (partial on-asset-click :graphics) + :on-assets-delete on-assets-delete + :on-clear-selection unselect-all}]) (when show-colors? [:& colors-box {:file-id (:id file) :local? local? :locale locale :colors colors :open? (open-box? :colors) - :change-selected (partial change-selected-count - :colors)}]) + :selected-assets @selected-assets + :on-asset-click (partial on-asset-click :colors) + :on-assets-delete on-assets-delete + :on-clear-selection unselect-all}]) (when show-typography? [:& typography-box {:file file @@ -1085,8 +1078,10 @@ :locale locale :typographies typographies :open? (open-box? :typographies) - :change-selected (partial change-selected-count - :typographies)}]) + :selected-assets @selected-assets + :on-asset-click (partial on-asset-click :typographies) + :on-assets-delete on-assets-delete + :on-clear-selection unselect-all}]) (when (and (not show-components?) (not show-graphics?) (not show-colors?)) [:div.asset-section diff --git a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs index 388279d48..a4744b6a4 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/layers.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/layers.cljs @@ -285,27 +285,30 @@ :objects objects :key id}])))]])) +(defn- strip-obj-data [obj] + (select-keys obj [:id + :name + :blocked + :hidden + :shapes + :type + :content + :parent-id + :component-id + :component-file + :shape-ref + :touched + :metadata + :masked-group?])) + (defn- strip-objects [objects] - (let [strip-data #(select-keys % [:id - :name - :blocked - :hidden - :shapes - :type - :content - :parent-id - :component-id - :component-file - :shape-ref - :touched - :metadata - :masked-group?])] - (persistent! - (reduce-kv (fn [res id obj] - (assoc! res id (strip-data obj))) - (transient {}) - objects)))) + (persistent! + (->> objects + (reduce-kv + (fn [res id obj] + (assoc! res id (strip-obj-data obj))) + (transient {}))))) (mf/defc layers-tree-wrapper {::mf/wrap-props false diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs index 10556bff9..f6c8698ca 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/blur.cljs @@ -10,6 +10,7 @@ [app.common.data :as d] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.options.common :refer [advanced-options]] @@ -32,7 +33,7 @@ has-value? (not (nil? blur)) multiple? (= blur :multiple) - change! (fn [update-fn] (st/emit! (dwc/update-shapes ids update-fn))) + change! (fn [update-fn] (st/emit! (dch/update-shapes ids update-fn))) handle-add (fn [] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs index 8044aa721..bc7c39384 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/component.cljs @@ -16,6 +16,7 @@ [app.main.ui.components.context-menu :refer [context-menu]] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.libraries :as dwl] [app.util.i18n :as i18n :refer [t]] [app.util.dom :as dom])) @@ -50,10 +51,10 @@ do-detach-component (st/emitf (dwl/detach-component id)) do-reset-component (st/emitf (dwl/reset-component id)) do-update-component (st/emitf - (dwc/start-undo-transaction) + (dwu/start-undo-transaction) (dwl/update-component id) (dwl/sync-file current-file-id current-file-id) - (dwc/commit-undo-transaction)) + (dwu/commit-undo-transaction)) confirm-update-remote-component (st/emitf (dwl/update-component id) (dwl/sync-file current-file-id diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs index b70447603..47112a3f2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/fill.cljs @@ -9,6 +9,7 @@ [app.common.pages :as cp] [app.main.data.workspace.colors :as dc] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] [app.main.data.workspace.texts :as dwt] [app.main.refs :as refs] [app.main.store :as st] @@ -78,13 +79,13 @@ (mf/use-callback (mf/deps ids) (fn [value opacity id file-id] - (st/emit! (dwc/start-undo-transaction)))) + (st/emit! (dwu/start-undo-transaction)))) on-close-picker (mf/use-callback (mf/deps ids) (fn [value opacity id file-id] - (st/emit! (dwc/commit-undo-transaction))))] + (st/emit! (dwu/commit-undo-transaction))))] (if show? [:div.element-set diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs index 2149367cb..46fa46660 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/layer.cljs @@ -9,6 +9,7 @@ [app.common.data :as d] [app.common.math :as mth] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.store :as st] [app.main.ui.components.numeric-input :refer [numeric-input]] [app.main.ui.icons :as i] @@ -34,7 +35,7 @@ (mf/use-callback (mf/deps ids) (fn [prop value] - (st/emit! (dwc/update-shapes ids #(assoc % prop value))))) + (st/emit! (dch/update-shapes ids #(assoc % prop value))))) handle-change-blend-mode (mf/use-callback diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs index f9fa9df7e..0d02a5419 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/measures.cljs @@ -17,6 +17,7 @@ [app.common.geom.point :as gpt] [app.main.data.workspace :as udw] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.ui.components.numeric-input :refer [numeric-input]] [app.common.math :as math] [app.util.i18n :refer [t] :as i18n])) @@ -106,7 +107,7 @@ (:r1 shape) (-> (assoc :rx 0 :ry 0) (dissoc :r1 :r2 :r3 :r4))))] - (st/emit! (dwc/update-shapes ids-with-children radius-update))))) + (st/emit! (dch/update-shapes ids-with-children radius-update))))) on-switch-to-radius-4 (mf/use-callback @@ -118,7 +119,7 @@ (:rx shape) (-> (assoc :r1 0 :r2 0 :r3 0 :r4 0) (dissoc :rx :ry))))] - (st/emit! (dwc/update-shapes ids-with-children radius-update))))) + (st/emit! (dch/update-shapes ids-with-children radius-update))))) on-radius-1-change (mf/use-callback @@ -134,7 +135,7 @@ (or (:rx shape) (:r1 shape)) (assoc :rx value :ry value)))] - (st/emit! (dwc/update-shapes ids-with-children radius-update))))) + (st/emit! (dch/update-shapes ids-with-children radius-update))))) on-radius-4-change (mf/use-callback @@ -150,7 +151,7 @@ (attr shape) (assoc attr value)))] - (st/emit! (dwc/update-shapes ids-with-children radius-update))))) + (st/emit! (dch/update-shapes ids-with-children radius-update))))) on-width-change #(on-size-change % :width) on-height-change #(on-size-change % :height) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs index 01dda5963..0f84e67c6 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/shadow.cljs @@ -10,6 +10,8 @@ [app.common.data :as d] [app.common.uuid :as uuid] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] + [app.main.data.workspace.undo :as dwu] [app.main.store :as st] [app.main.ui.icons :as i] [app.main.ui.components.numeric-input :refer [numeric-input]] @@ -56,7 +58,7 @@ on-remove-shadow (fn [index] (fn [] - (st/emit! (dwc/update-shapes ids #(update % :shadow remove-shadow-by-index index) )))) + (st/emit! (dch/update-shapes ids #(update % :shadow remove-shadow-by-index index) )))) select-text (fn [ref] (fn [event] (dom/select-text! (mf/ref-val ref)))) @@ -69,14 +71,14 @@ ([index attr valid? update-ref] (fn [value] (when (or (not valid?) (valid? value)) - (do (st/emit! (dwc/update-shapes ids #(assoc-in % [:shadow index attr] value))) + (do (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index attr] value))) (when update-ref (dom/set-value! (mf/ref-val update-ref) value))))))) update-color (fn [index] (fn [color opacity] (let [color (d/without-keys color [:id :file-id :gradient])] - (st/emit! (dwc/update-shapes + (st/emit! (dch/update-shapes ids #(-> % (assoc-in [:shadow index :color] color) @@ -86,7 +88,7 @@ (fn [index] (fn [color opacity] (if-not (string? (:color value)) - (st/emit! (dwc/update-shapes + (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index :color] (dissoc (:color value) :id :file-id))))))) @@ -94,7 +96,7 @@ toggle-visibility (fn [index] (fn [] - (st/emit! (dwc/update-shapes ids #(update-in % [:shadow index :hidden] not)))))] + (st/emit! (dch/update-shapes ids #(update-in % [:shadow index :hidden] not)))))] [:* [:div.element-set-options-group @@ -129,7 +131,7 @@ {:default-value (str (:style value)) :on-change (fn [event] (let [value (-> event dom/get-target dom/get-value d/read-string)] - (st/emit! (dwc/update-shapes ids #(assoc-in % [:shadow index :style] value)))))} + (st/emit! (dch/update-shapes ids #(assoc-in % [:shadow index :style] value)))))} [:option {:value ":drop-shadow"} (t locale "workspace.options.shadow-options.drop-shadow")] [:option {:value ":inner-shadow"} (t locale "workspace.options.shadow-options.inner-shadow")]]] @@ -181,18 +183,18 @@ :disable-gradient true :on-change (update-color index) :on-detach (detach-color index) - :on-open #(st/emit! (dwc/start-undo-transaction)) - :on-close #(st/emit! (dwc/commit-undo-transaction))}]]]])) + :on-open #(st/emit! (dwu/start-undo-transaction)) + :on-close #(st/emit! (dwu/commit-undo-transaction))}]]]])) (mf/defc shadow-menu [{:keys [ids type values] :as props}] (let [locale (i18n/use-locale) on-remove-all-shadows (fn [event] - (st/emit! (dwc/update-shapes ids #(dissoc % :shadow) ))) + (st/emit! (dch/update-shapes ids #(dissoc % :shadow) ))) on-add-shadow (fn [] - (st/emit! (dwc/update-shapes ids #(update % :shadow (fnil conj []) (create-shadow)) )))] + (st/emit! (dch/update-shapes ids #(update % :shadow (fnil conj []) (create-shadow)) )))] [:div.element-set.shadow-options [:div.element-set-title [:span diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs index 002a6a7bc..14ae0a784 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/stroke.cljs @@ -11,6 +11,8 @@ [app.common.data :as d] [app.common.math :as math] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.colors :as dc] [app.main.store :as st] [app.main.ui.icons :as i] @@ -78,7 +80,7 @@ (let [value (-> (dom/get-target event) (dom/get-value) (d/read-string))] - (st/emit! (dwc/update-shapes ids #(assoc % :stroke-style value))))) + (st/emit! (dch/update-shapes ids #(assoc % :stroke-style value))))) on-stroke-alignment-change (fn [event] @@ -86,7 +88,7 @@ (dom/get-value) (d/read-string))] (when-not (str/empty? value) - (st/emit! (dwc/update-shapes ids #(assoc % :stroke-alignment value)))))) + (st/emit! (dch/update-shapes ids #(assoc % :stroke-alignment value)))))) on-stroke-width-change (fn [event] @@ -94,11 +96,11 @@ (dom/get-value) (d/parse-integer 0))] (when-not (str/empty? value) - (st/emit! (dwc/update-shapes ids #(assoc % :stroke-width value)))))) + (st/emit! (dch/update-shapes ids #(assoc % :stroke-width value)))))) on-add-stroke (fn [event] - (st/emit! (dwc/update-shapes ids #(assoc % + (st/emit! (dch/update-shapes ids #(assoc % :stroke-style :solid :stroke-color "#000000" :stroke-opacity 1 @@ -106,19 +108,19 @@ on-del-stroke (fn [event] - (st/emit! (dwc/update-shapes ids #(assoc % :stroke-style :none)))) + (st/emit! (dch/update-shapes ids #(assoc % :stroke-style :none)))) on-open-picker (mf/use-callback (mf/deps ids) (fn [value opacity id file-id] - (st/emit! (dwc/start-undo-transaction)))) + (st/emit! (dwu/start-undo-transaction)))) on-close-picker (mf/use-callback (mf/deps ids) (fn [value opacity id file-id] - (st/emit! (dwc/commit-undo-transaction))))] + (st/emit! (dwu/commit-undo-transaction))))] (if show-options [:div.element-set diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs index 3675d3b86..bd6dc955a 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/svg_attrs.cljs @@ -9,6 +9,7 @@ [cuerdas.core :as str] [app.common.data :as d] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.store :as st] [app.main.ui.workspace.sidebar.options.rows.input-row :refer [input-row]] [app.util.dom :as dom] @@ -61,7 +62,7 @@ (fn [attr value] (let [update-fn (fn [shape] (assoc-in shape (concat [:svg-attrs] attr) value))] - (st/emit! (dwc/update-shapes ids update-fn))))) + (st/emit! (dch/update-shapes ids update-fn))))) handle-delete (mf/use-callback @@ -76,7 +77,7 @@ (empty? (get-in shape [:svg-attrs :style])) (update :svg-attrs dissoc :style))] shape))] - (st/emit! (dwc/update-shapes ids update-fn))))) + (st/emit! (dch/update-shapes ids update-fn))))) ] diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs index 031dc1d3c..5c83e42cc 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/text.cljs @@ -10,6 +10,7 @@ [app.common.uuid :as uuid] [app.common.text :as txt] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.changes :as dch] [app.main.data.workspace.libraries :as dwl] [app.main.data.workspace.texts :as dwt] [app.main.fonts :as fonts] @@ -159,7 +160,7 @@ grow-type (->> values :grow-type) handle-change-grow (fn [event grow-type] - (st/emit! (dwc/update-shapes ids #(assoc % :grow-type grow-type))))] + (st/emit! (dch/update-shapes ids #(assoc % :grow-type grow-type))))] [:div.align-icons [:span.tooltip.tooltip-bottom diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs index f4cbcd5cf..14e715e87 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/typography.cljs @@ -207,7 +207,7 @@ (mf/defc typography-entry - [{:keys [typography read-only? selected? on-select on-change on-detach on-context-menu editting? focus-name? file]}] + [{:keys [typography read-only? selected? on-click on-change on-detach on-context-menu editting? focus-name? file]}] (let [locale (mf/deref i18n/locale) open? (mf/use-state editting?) hover-detach (mf/use-state false) @@ -241,8 +241,8 @@ [:div.element-set-options-group.typography-entry {:class (when selected? "selected")} [:div.typography-selection-wrapper - {:class (when on-select "is-selectable") - :on-click on-select + {:class (when on-click "is-selectable") + :on-click on-click :on-context-menu on-context-menu} [:div.typography-sample {:style {:font-family (:font-family typography) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs index c6cbc40f2..ef96fe0db 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/page.cljs @@ -13,6 +13,7 @@ [app.main.store :as st] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] + [app.main.data.workspace.undo :as dwu] [app.util.i18n :as i18n :refer [t]] [app.main.ui.workspace.sidebar.options.rows.color-row :refer [color-row]])) @@ -31,12 +32,12 @@ on-open (mf/use-callback (mf/deps page-id) - #(st/emit! (dwc/start-undo-transaction))) + #(st/emit! (dwu/start-undo-transaction))) on-close (mf/use-callback (mf/deps page-id) - #(st/emit! (dwc/commit-undo-transaction)))] + #(st/emit! (dwu/commit-undo-transaction)))] [:div.element-set [:div.element-set-title (t locale "workspace.options.canvas-background")] diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index c35ff9530..eb430cbf2 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -102,6 +102,13 @@ :index index :name (:name page)})] + (mf/use-effect + (mf/deps selected?) + (fn [] + (when selected? + (let [node (mf/ref-val dref)] + (.scrollIntoViewIfNeeded ^js node))))) + (mf/use-layout-effect (mf/deps (:edition @local)) (fn [] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 6e73069a5..8c959a0f6 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -95,10 +95,12 @@ ;; Only when we have all the selected shapes in one frame selected-frame (when (= (count selected-frames) 1) (get objects (first selected-frames))) + + create-comment? (= :comments drawing-tool) drawing-path? (or (and edition (= :draw (get-in edit-path [edition :edit-mode]))) (and (some? drawing-obj) (= :path (:type drawing-obj)))) - text-editing? (and edition (= :text (get-in objects [edition :type]))) path-editing? (and edition (= :path (get-in objects [edition :type]))) + text-editing? (and edition (= :text (get-in objects [edition :type]))) on-click (actions/on-click hover selected edition drawing-path? drawing-tool) on-context-menu (actions/on-context-menu hover) @@ -106,7 +108,7 @@ on-drag-enter (actions/on-drag-enter) on-drag-over (actions/on-drag-over) on-drop (actions/on-drop file viewport-ref zoom) - on-mouse-down (actions/on-mouse-down @hover drawing-tool text-editing? edition edit-path selected) + on-mouse-down (actions/on-mouse-down @hover selected edition drawing-tool text-editing? path-editing? drawing-path? create-comment?) on-mouse-up (actions/on-mouse-up disable-paste) on-pointer-down (actions/on-pointer-down) on-pointer-enter (actions/on-pointer-enter in-viewport?) diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 6b886dbe9..f7550428f 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -24,13 +24,12 @@ [beicon.core :as rx] [cuerdas.core :as str] [rumext.alpha :as mf]) - (:import goog.events.WheelEvent - goog.events.KeyCodes)) + (:import goog.events.WheelEvent)) (defn on-mouse-down - [{:keys [id blocked hidden type]} drawing-tool text-editing? edition edit-path selected] + [{:keys [id blocked hidden type]} selected edition drawing-tool text-editing? path-editing? drawing-path? create-comment?] (mf/use-callback - (mf/deps id blocked hidden type drawing-tool text-editing? edition edit-path selected) + (mf/deps id blocked hidden type selected edition drawing-tool text-editing? path-editing? drawing-path? create-comment?) (fn [bevent] (when (or (dom/class? (dom/get-target bevent) "viewport-controls") (dom/class? (dom/get-target bevent) "viewport-selrect")) @@ -45,9 +44,7 @@ middle-click? (= 2 (.-which event)) frame? (= :frame type) - selected? (contains? selected id) - - drawing-path? (= :draw (get-in edit-path [edition :edit-mode]))] + selected? (contains? selected id)] (when middle-click? (dom/prevent-default bevent) @@ -62,13 +59,13 @@ (when (and (not text-editing?) (not blocked) (not hidden) - (not (#{:comments :path} drawing-tool)) + (not create-comment?) (not drawing-path?)) (cond drawing-tool (st/emit! (dd/start-drawing drawing-tool)) - (and edit-path (contains? edit-path edition)) + path-editing? ;; Handle path node area selection (st/emit! (dwdp/handle-selection shift?)) @@ -263,8 +260,7 @@ (mf/use-callback (fn [event] (let [bevent (.getBrowserEvent ^js event) - key (.-keyCode ^js event) - key (.normalizeKeyCode KeyCodes key) + key (.-key ^js event) ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) alt? (kbd/alt? event) @@ -284,8 +280,7 @@ (defn on-key-up [] (mf/use-callback (fn [event] - (let [key (.-keyCode event) - key (.normalizeKeyCode KeyCodes key) + (let [key (.-key event) ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) alt? (kbd/alt? event) diff --git a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs index 2c0a08f8c..1fe1c1685 100644 --- a/frontend/src/app/main/ui/workspace/viewport/hooks.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/hooks.cljs @@ -100,7 +100,8 @@ {:cmd :selection/query :page-id page-id :rect rect - :include-frames? true})))) + :include-frames? true + :reverse? true})))) ;; we want the topmost shape to be selected first ;; We use ref so we don't recreate the stream on a change transform-ref (mf/use-ref nil) diff --git a/frontend/src/app/main/ui/workspace/viewport/selection.cljs b/frontend/src/app/main/ui/workspace/viewport/selection.cljs index 06845cf63..8cad2d8fc 100644 --- a/frontend/src/app/main/ui/workspace/viewport/selection.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/selection.cljs @@ -19,7 +19,6 @@ [app.main.streams :as ms] [app.main.ui.cursors :as cur] [app.main.ui.hooks :as hooks] - [app.main.ui.workspace.viewport.outline :refer [outline]] [app.main.ui.workspace.shapes.path.editor :refer [path-editor]] [app.util.data :as d] [app.util.debug :refer [debug?]] @@ -255,7 +254,6 @@ :zoom zoom :color color :on-move-selected on-move-selected}] - [:& outline {:shape shape :color color}] ;; Handlers (for [{:keys [type position props]} (handlers-for-selection selrect shape zoom)] diff --git a/frontend/src/app/util/keyboard.cljs b/frontend/src/app/util/keyboard.cljs index 35007bda0..2ab1e286f 100644 --- a/frontend/src/app/util/keyboard.cljs +++ b/frontend/src/app/util/keyboard.cljs @@ -6,10 +6,10 @@ (ns app.util.keyboard) -(defn is-keycode? - [keycode] +(defn is-key? + [key] (fn [e] - (= (.-keyCode e) keycode))) + (= (.-key e) key))) (defn ^boolean alt? [event] @@ -27,8 +27,11 @@ [event] (.-shiftKey event)) -(def esc? (is-keycode? 27)) -(def enter? (is-keycode? 13)) -(def space? (is-keycode? 32)) -(def up-arrow? (is-keycode? 38)) -(def down-arrow? (is-keycode? 40)) +(def esc? (is-key? "Escape")) +(def enter? (is-key? "Enter")) +(def space? (is-key? " ")) +(def up-arrow? (is-key? "ArrowUp")) +(def down-arrow? (is-key? "ArrowDown")) +(def altKey? (is-key? "Alt")) +(def ctrlKey? (or (is-key? "Control") + (is-key? "Meta"))) diff --git a/frontend/src/app/worker/selection.cljs b/frontend/src/app/worker/selection.cljs index dd29d20cc..2d0ac3a5d 100644 --- a/frontend/src/app/worker/selection.cljs +++ b/frontend/src/app/worker/selection.cljs @@ -41,8 +41,8 @@ nil)) (defmethod impl/handler :selection/query - [{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks] :or {include-groups? true - disabled-masks #{}} :as message}] + [{:keys [page-id rect frame-id include-frames? include-groups? disabled-masks reverse?] + :or {include-groups? true disabled-masks #{} reverse? false} :as message}] (when-let [index (get @state page-id)] (let [result (-> (qdt/search index (clj->js rect)) (es6-iterator-seq)) @@ -76,11 +76,13 @@ (filter (comp overlaps? :frame)) (filter (comp overlaps-masks? :masks)) (filter overlaps?)) - result)] + result) + + keyfn (if reverse? (comp - :z) :z)] (into (d/ordered-set) (->> matching-shapes - (sort-by (comp - :z)) + (sort-by keyfn) (map :id)))))) (defn create-mask-index diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ff39c7075..eb0eee9d9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -1194,6 +1194,11 @@ msgstr "Send invitation" msgid "modals.invite-member.title" msgstr "Invite to join the team" +msgid "modals.leave-and-reassign.forbiden" +msgstr "" +"You can not leave the team if there is no other member to promote to owner. " +"You might want to delete the team." + #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.hint1" msgstr "You are %s owner." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 93c79ce90..46bacd4eb 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -1184,6 +1184,11 @@ msgstr "Enviar invitacion" msgid "modals.invite-member.title" msgstr "Invitar a unirse al equipo" +msgid "modals.leave-and-reassign.forbiden" +msgstr "" +"No puede abandonar el equipo si no hay otro miembro al que promocionar a " +"dueño. Quizás quiere borrar el equipo." + #: src/app/main/ui/dashboard/sidebar.cljs msgid "modals.leave-and-reassign.hint1" msgstr "Eres %s dueño." @@ -2505,4 +2510,4 @@ msgid "workspace.updates.update" msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" -msgstr "Pulsar para cerrar la ruta" +msgstr "Pulsar para cerrar la ruta" \ No newline at end of file