From 174b9db1d25c07b7d03d3a2df88b34b383e4576e Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 2 Jun 2020 15:49:18 +0200 Subject: [PATCH] :tada: Duplicate move --- frontend/src/uxbox/main/data/workspace.cljs | 280 +--------------- .../src/uxbox/main/data/workspace/common.cljs | 14 + .../uxbox/main/data/workspace/selection.cljs | 299 ++++++++++++++++++ .../uxbox/main/data/workspace/transforms.cljs | 67 ++-- frontend/src/uxbox/main/streams.cljs | 25 +- frontend/src/uxbox/main/ui/cursors.clj | 2 +- frontend/src/uxbox/main/ui/cursors.cljs | 1 - frontend/src/uxbox/main/ui/hooks.cljs | 7 + .../src/uxbox/main/ui/workspace/shapes.cljs | 12 +- .../src/uxbox/main/ui/workspace/viewport.cljs | 41 +-- 10 files changed, 428 insertions(+), 320 deletions(-) create mode 100644 frontend/src/uxbox/main/data/workspace/selection.cljs diff --git a/frontend/src/uxbox/main/data/workspace.cljs b/frontend/src/uxbox/main/data/workspace.cljs index 20f5556e5..8de92b734 100644 --- a/frontend/src/uxbox/main/data/workspace.cljs +++ b/frontend/src/uxbox/main/data/workspace.cljs @@ -25,6 +25,7 @@ [uxbox.main.data.workspace.persistence :as dwp] [uxbox.main.data.workspace.texts :as dwtxt] [uxbox.main.data.workspace.transforms :as dwt] + [uxbox.main.data.workspace.selection :as dws] [uxbox.main.repo :as rp] [uxbox.main.store :as st] [uxbox.main.streams :as ms] @@ -438,101 +439,6 @@ (assoc :zoom zoom) (update :vbox merge srect))))))))))) -;; --- Selection Rect - -(declare select-shapes-by-current-selrect) -(declare deselect-all) - -(defn update-selrect - [selrect] - (ptk/reify ::update-selrect - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :selrect] selrect)))) - -(def handle-selection - (letfn [(data->selrect [data] - (let [start (:start data) - stop (:stop data) - start-x (min (:x start) (:x stop)) - start-y (min (:y start) (:y stop)) - end-x (max (:x start) (:x stop)) - end-y (max (:y start) (:y stop))] - {:type :rect - :x start-x - :y start-y - :width (mth/abs (- end-x start-x)) - :height (mth/abs (- end-y start-y))}))] - (ptk/reify ::handle-selection - ptk/WatchEvent - (watch [_ state stream] - (let [stoper (rx/filter #(or (interrupt? %) - (ms/mouse-up? %)) - stream)] - (rx/concat - (rx/of deselect-all) - (->> ms/mouse-position - (rx/scan (fn [data pos] - (if data - (assoc data :stop pos) - {:start pos :stop pos})) - nil) - (rx/map data->selrect) - (rx/filter #(or (> (:width %) 10) - (> (:height %) 10))) - (rx/map update-selrect) - (rx/take-until stoper)) - (rx/of select-shapes-by-current-selrect))))))) - -;; --- Toggle shape's selection status (selected or deselected) - -(declare expand-all-parents) - -(defn select-shape - ([id] (select-shape id false)) - ([id toggle?] - (us/verify ::us/uuid id) - (ptk/reify ::select-shape - ptk/UpdateEvent - (update [_ state] - (update-in state [:workspace-local :selected] - (fn [selected] - (if-not toggle? - (conj selected id) - (if (contains? selected id) - (disj selected id) - (conj selected id)))))) - - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (get-in state [:workspace-page :id]) - objects (get-in state [:workspace-data page-id :objects])] - (rx/of (expand-all-parents [id] objects))))))) - -(defn select-shapes - [ids] - (us/verify ::set-of-uuid ids) - (ptk/reify ::select-shapes - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-local :selected] ids)) - - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (get-in state [:workspace-page :id]) - objects (get-in state [:workspace-data page-id :objects])] - (rx/of (expand-all-parents ids objects)))))) - -(def deselect-all - "Clear all possible state of drawing, edition - or any similar action taken by the user." - (ptk/reify ::deselect-all - ptk/UpdateEvent - (update [_ state] - (update state :workspace-local #(-> % - (assoc :selected #{}) - (dissoc :selected-frame)))))) - ;; --- Add shape to Workspace @@ -598,161 +504,12 @@ :id id}] (rx/of (dwc/commit-changes [rchange] [uchange] {:commit-local? true}) - (select-shapes #{id}) + (dws/select-shapes #{id}) (when (= :text (:type attrs)) (start-edition-mode id))))))) -;; --- Duplicate Shapes -(declare prepare-duplicate-changes) -(declare prepare-duplicate-change) -(declare prepare-duplicate-frame-change) -(declare prepare-duplicate-shape-change) - -(def ^:private change->name #(get-in % [:obj :name])) - -(defn- prepare-duplicate-changes - "Prepare objects to paste: generate new id, give them unique names, - move to the position of mouse pointer, and find in what frame they - fit." - [objects names ids delta] - (loop [names names - chgs [] - id (first ids) - ids (rest ids)] - (if (nil? id) - chgs - (let [result (prepare-duplicate-change objects names id delta) - result (if (vector? result) result [result])] - (recur - (into names (map change->name) result) - (into chgs result) - (first ids) - (rest ids)))))) - -(defn- prepare-duplicate-change - [objects names id delta] - (let [obj (get objects id)] - (if (= :frame (:type obj)) - (prepare-duplicate-frame-change objects names obj delta) - (prepare-duplicate-shape-change objects names obj delta nil nil)))) - -(defn- prepare-duplicate-shape-change - [objects names obj delta frame-id parent-id] - (let [id (uuid/next) - name (generate-unique-name names (:name obj)) - renamed-obj (assoc obj :id id :name name) - moved-obj (geom/move renamed-obj delta) - frames (cp/select-frames objects) - frame-id (if frame-id - frame-id - (dwc/calculate-frame-overlap frames moved-obj)) - - parent-id (or parent-id frame-id) - - children-changes - (loop [names names - result [] - cid (first (:shapes obj)) - cids (rest (:shapes obj))] - (if (nil? cid) - result - (let [obj (get objects cid) - changes (prepare-duplicate-shape-change objects names obj delta frame-id id)] - (recur - (into names (map change->name changes)) - (into result changes) - (first cids) - (rest cids))))) - - reframed-obj (-> moved-obj - (assoc :frame-id frame-id) - (dissoc :shapes))] - (into [{:type :add-obj - :id id - :old-id (:id obj) - :frame-id frame-id - :parent-id parent-id - :obj (dissoc reframed-obj :shapes)}] - children-changes))) - -(defn- prepare-duplicate-frame-change - [objects names obj delta] - (let [frame-id (uuid/next) - frame-name (generate-unique-name names (:name obj)) - sch (->> (map #(get objects %) (:shapes obj)) - (mapcat #(prepare-duplicate-shape-change objects names % delta frame-id frame-id))) - - renamed-frame (-> obj - (assoc :id frame-id) - (assoc :name frame-name) - (assoc :frame-id uuid/zero) - (dissoc :shapes)) - - moved-frame (geom/move renamed-frame delta) - - fch {:type :add-obj - :old-id (:id obj) - :id frame-id - :frame-id uuid/zero - :obj moved-frame}] - - (into [fch] sch))) - -(declare select-shapes) - -(def duplicate-selected - (ptk/reify ::duplicate-selected - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - selected (get-in state [:workspace-local :selected]) - objects (get-in state [:workspace-data page-id :objects]) - delta (gpt/point 0 0) - unames (retrieve-used-names objects) - - rchanges (prepare-duplicate-changes objects unames selected delta) - uchanges (mapv #(array-map :type :del-obj :id (:id %)) - (reverse rchanges)) - - selected (->> rchanges - (filter #(selected (:old-id %))) - (map #(get-in % [:obj :id])) - (into #{}))] - - (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) - (select-shapes selected)))))) - - -;; --- Select Shapes (By selrect) - -(def select-shapes-by-current-selrect - (ptk/reify ::select-shapes-by-current-selrect - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (get-in state [:workspace-page :id]) - selrect (get-in state [:workspace-local :selrect])] - (rx/merge - (rx/of (update-selrect nil)) - (when selrect - (->> (uw/ask! {:cmd :selection/query - :page-id page-id - :rect selrect}) - (rx/map select-shapes)))))))) - -(defn select-inside-group - [group-id position] - (ptk/reify ::select-inside-group - ptk/WatchEvent - (watch [_ state stream] - (let [page-id (:current-page-id state) - objects (get-in state [:workspace-data page-id :objects]) - group (get objects group-id) - children (map #(get objects %) (:shapes group)) - selected (->> children (filter #(geom/has-point? % position)) first)] - (when selected - (rx/of deselect-all (select-shape (:id selected)))))))) ;; --- Update Shape Attrs @@ -906,7 +663,7 @@ shapes (map lookup selected) shape? #(not= (:type %) :frame)] (rx/of (delete-shapes selected) - deselect-all))))) + dws/deselect-all))))) ;; --- Rename Shape @@ -1186,21 +943,6 @@ (update [_ state] (update state :workspace-local dissoc :expanded)))) -(defn expand-all-parents - [ids objects] - (ptk/reify ::expand-all-parents - ptk/UpdateEvent - (update [_ state] - (let [expand-fn (fn [expanded] - (merge expanded - (->> ids - (map #(cp/get-all-parents % objects)) - flatten - (filter #(not= % uuid/zero)) - (map (fn [id] {id true})) - (into {}))))] - (update-in state [:workspace-local :expanded] expand-fn))))) - (defn recursive-assign "A helper for assign recursively a shape attr." [id attr value] @@ -1284,7 +1026,7 @@ ptk/WatchEvent (watch [_ state stream] - (rx/of (select-shape (:id shape)))))) + (rx/of (dws/select-shape (:id shape)))))) (def hide-context-menu (ptk/reify ::hide-context-menu @@ -1342,7 +1084,7 @@ unames (-> (get-in state [:workspace-data page-id :objects]) (retrieve-used-names)) - rchanges (prepare-duplicate-changes objects unames selected delta) + rchanges (dws/prepare-duplicate-changes objects unames selected delta) uchanges (mapv #(array-map :type :del-obj :id (:id %)) (reverse rchanges)) @@ -1351,7 +1093,7 @@ (map #(get-in % [:obj :id])) (into #{}))] (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) - (select-shapes selected)))))) + (dws/select-shapes selected)))))) (def paste (ptk/reify ::paste @@ -1432,7 +1174,7 @@ {:type :del-obj :id id}]] (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) - (select-shapes #{id})))))))) + (dws/select-shapes #{id})))))))) (def remove-group (ptk/reify ::remove-group @@ -1551,6 +1293,14 @@ (def delete-page dwp/delete-page) (def create-empty-page dwp/create-empty-page) +;; Selection +(def select-shape dws/select-shape) +(def deselect-all dws/deselect-all) +(def select-shapes dws/select-shapes) +(def duplicate-selected dws/duplicate-selected) +(def handle-selection dws/handle-selection) +(def select-inside-group dws/select-inside-group) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Shortcuts ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/uxbox/main/data/workspace/common.cljs b/frontend/src/uxbox/main/data/workspace/common.cljs index 66e2843f2..fea80b434 100644 --- a/frontend/src/uxbox/main/data/workspace/common.cljs +++ b/frontend/src/uxbox/main/data/workspace/common.cljs @@ -298,3 +298,17 @@ (update state :workspace-local dissoc :undo-index :undo)))) +(defn expand-all-parents + [ids objects] + (ptk/reify ::expand-all-parents + ptk/UpdateEvent + (update [_ state] + (let [expand-fn (fn [expanded] + (merge expanded + (->> ids + (map #(cp/get-all-parents % objects)) + flatten + (filter #(not= % uuid/zero)) + (map (fn [id] {id true})) + (into {}))))] + (update-in state [:workspace-local :expanded] expand-fn))))) diff --git a/frontend/src/uxbox/main/data/workspace/selection.cljs b/frontend/src/uxbox/main/data/workspace/selection.cljs new file mode 100644 index 000000000..034f64474 --- /dev/null +++ b/frontend/src/uxbox/main/data/workspace/selection.cljs @@ -0,0 +1,299 @@ +;; 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/. +;; +;; This Source Code Form is "Incompatible With Secondary Licenses", as +;; defined by the Mozilla Public License, v. 2.0. +;; +;; Copyright (c) 2020 UXBOX Labs SL + +(ns uxbox.main.data.workspace.selection + (:require + [beicon.core :as rx] + [cljs.spec.alpha :as s] + [potok.core :as ptk] + [uxbox.main.data.workspace.common :as dwc] + [uxbox.main.worker :as uw] + [uxbox.main.streams :as ms] + [uxbox.common.pages :as cp] + [uxbox.common.spec :as us] + [uxbox.common.uuid :as uuid] + [uxbox.common.data :as d] + [uxbox.common.geom.shapes :as geom] + [uxbox.common.geom.point :as gpt] + [uxbox.common.math :as mth])) + +(s/def ::set-of-uuid + (s/every uuid? :kind set?)) + +(s/def ::set-of-string + (s/every string? :kind set?)) + +;; Duplicate from workspace. +;; FIXME: Move these functions to a common place + +(defn interrupt? [e] (= e :interrupt)) + +(defn- retrieve-used-names + [objects] + (into #{} (map :name) (vals objects))) + +(defn- extract-numeric-suffix + [basename] + (if-let [[match p1 p2] (re-find #"(.*)-([0-9]+)$" basename)] + [p1 (+ 1 (d/parse-integer p2))] + [basename 1])) + +(defn- generate-unique-name + "A unique name generator" + [used basename] + (s/assert ::set-of-string used) + (s/assert ::us/string basename) + (let [[prefix initial] (extract-numeric-suffix basename)] + (loop [counter initial] + (let [candidate (str prefix "-" counter)] + (if (contains? used candidate) + (recur (inc counter)) + candidate))))) + +;; --- Selection Rect + +(declare select-shapes-by-current-selrect) +(declare deselect-all) + +(defn update-selrect + [selrect] + (ptk/reify ::update-selrect + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :selrect] selrect)))) + +(def handle-selection + (letfn [(data->selrect [data] + (let [start (:start data) + stop (:stop data) + start-x (min (:x start) (:x stop)) + start-y (min (:y start) (:y stop)) + end-x (max (:x start) (:x stop)) + end-y (max (:y start) (:y stop))] + {:type :rect + :x start-x + :y start-y + :width (mth/abs (- end-x start-x)) + :height (mth/abs (- end-y start-y))}))] + (ptk/reify ::handle-selection + ptk/WatchEvent + (watch [_ state stream] + (let [stoper (rx/filter #(or (interrupt? %) + (ms/mouse-up? %)) + stream)] + (rx/concat + (rx/of deselect-all) + (->> ms/mouse-position + (rx/scan (fn [data pos] + (if data + (assoc data :stop pos) + {:start pos :stop pos})) + nil) + (rx/map data->selrect) + (rx/filter #(or (> (:width %) 10) + (> (:height %) 10))) + (rx/map update-selrect) + (rx/take-until stoper)) + (rx/of select-shapes-by-current-selrect))))))) + +;; --- Toggle shape's selection status (selected or deselected) + +(defn select-shape + ([id] (select-shape id false)) + ([id toggle?] + (us/verify ::us/uuid id) + (ptk/reify ::select-shape + ptk/UpdateEvent + (update [_ state] + (update-in state [:workspace-local :selected] + (fn [selected] + (if-not toggle? + (conj selected id) + (if (contains? selected id) + (disj selected id) + (conj selected id)))))) + + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (get-in state [:workspace-page :id]) + objects (get-in state [:workspace-data page-id :objects])] + (rx/of (dwc/expand-all-parents [id] objects))))))) + +(defn select-shapes + [ids] + (us/verify ::set-of-uuid ids) + (ptk/reify ::select-shapes + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-local :selected] ids)) + + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (get-in state [:workspace-page :id]) + objects (get-in state [:workspace-data page-id :objects])] + (rx/of (dwc/expand-all-parents ids objects)))))) + +(def deselect-all + "Clear all possible state of drawing, edition + or any similar action taken by the user." + (ptk/reify ::deselect-all + ptk/UpdateEvent + (update [_ state] + (update state :workspace-local #(-> % + (assoc :selected #{}) + (dissoc :selected-frame)))))) + + +;; --- Select Shapes (By selrect) + +(def select-shapes-by-current-selrect + (ptk/reify ::select-shapes-by-current-selrect + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (get-in state [:workspace-page :id]) + selrect (get-in state [:workspace-local :selrect])] + (rx/merge + (rx/of (update-selrect nil)) + (when selrect + (->> (uw/ask! {:cmd :selection/query + :page-id page-id + :rect selrect}) + (rx/map select-shapes)))))))) + +(defn select-inside-group + [group-id position] + (ptk/reify ::select-inside-group + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + objects (get-in state [:workspace-data page-id :objects]) + group (get objects group-id) + children (map #(get objects %) (:shapes group)) + selected (->> children (filter #(geom/has-point? % position)) first)] + (when selected + (rx/of deselect-all (select-shape (:id selected)))))))) + + +;; --- Duplicate Shapes +(declare prepare-duplicate-changes) +(declare prepare-duplicate-change) +(declare prepare-duplicate-frame-change) +(declare prepare-duplicate-shape-change) + +(def ^:private change->name #(get-in % [:obj :name])) + +(defn- prepare-duplicate-changes + "Prepare objects to paste: generate new id, give them unique names, + move to the position of mouse pointer, and find in what frame they + fit." + [objects names ids delta] + (loop [names names + chgs [] + id (first ids) + ids (rest ids)] + (if (nil? id) + chgs + (let [result (prepare-duplicate-change objects names id delta) + result (if (vector? result) result [result])] + (recur + (into names (map change->name) result) + (into chgs result) + (first ids) + (rest ids)))))) + +(defn- prepare-duplicate-change + [objects names id delta] + (let [obj (get objects id)] + (if (= :frame (:type obj)) + (prepare-duplicate-frame-change objects names obj delta) + (prepare-duplicate-shape-change objects names obj delta nil nil)))) + +(defn- prepare-duplicate-shape-change + [objects names obj delta frame-id parent-id] + (let [id (uuid/next) + name (generate-unique-name names (:name obj)) + renamed-obj (assoc obj :id id :name name) + moved-obj (geom/move renamed-obj delta) + frames (cp/select-frames objects) + frame-id (if frame-id + frame-id + (dwc/calculate-frame-overlap frames moved-obj)) + + parent-id (or parent-id frame-id) + + children-changes + (loop [names names + result [] + cid (first (:shapes obj)) + cids (rest (:shapes obj))] + (if (nil? cid) + result + (let [obj (get objects cid) + changes (prepare-duplicate-shape-change objects names obj delta frame-id id)] + (recur + (into names (map change->name changes)) + (into result changes) + (first cids) + (rest cids))))) + + reframed-obj (-> moved-obj + (assoc :frame-id frame-id) + (dissoc :shapes))] + (into [{:type :add-obj + :id id + :old-id (:id obj) + :frame-id frame-id + :parent-id parent-id + :obj (dissoc reframed-obj :shapes)}] + children-changes))) + +(defn- prepare-duplicate-frame-change + [objects names obj delta] + (let [frame-id (uuid/next) + frame-name (generate-unique-name names (:name obj)) + sch (->> (map #(get objects %) (:shapes obj)) + (mapcat #(prepare-duplicate-shape-change objects names % delta frame-id frame-id))) + + renamed-frame (-> obj + (assoc :id frame-id) + (assoc :name frame-name) + (assoc :frame-id uuid/zero) + (dissoc :shapes)) + + moved-frame (geom/move renamed-frame delta) + + fch {:type :add-obj + :old-id (:id obj) + :id frame-id + :frame-id uuid/zero + :obj moved-frame}] + + (into [fch] sch))) + +(def duplicate-selected + (ptk/reify ::duplicate-selected + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (:current-page-id state) + selected (get-in state [:workspace-local :selected]) + objects (get-in state [:workspace-data page-id :objects]) + delta (gpt/point 0 0) + unames (retrieve-used-names objects) + + rchanges (prepare-duplicate-changes objects unames selected delta) + uchanges (mapv #(array-map :type :del-obj :id (:id %)) + (reverse rchanges)) + + selected (->> rchanges + (filter #(selected (:old-id %))) + (map #(get-in % [:obj :id])) + (into #{}))] + + (rx/of (dwc/commit-changes rchanges uchanges {:commit-local? true}) + (select-shapes selected)))))) diff --git a/frontend/src/uxbox/main/data/workspace/transforms.cljs b/frontend/src/uxbox/main/data/workspace/transforms.cljs index 42d093602..74a27a63a 100644 --- a/frontend/src/uxbox/main/data/workspace/transforms.cljs +++ b/frontend/src/uxbox/main/data/workspace/transforms.cljs @@ -18,6 +18,7 @@ [uxbox.common.spec :as us] [uxbox.common.pages :as cp] [uxbox.main.data.workspace.common :as dwc] + [uxbox.main.data.workspace.selection :as dws] [uxbox.main.refs :as refs] [uxbox.main.store :as st] [uxbox.main.streams :as ms] @@ -191,6 +192,7 @@ ;; -- MOVE (declare start-move) +(declare start-move-duplicate) (defn start-move-selected [] @@ -206,33 +208,52 @@ (rx/map #(gpt/length %)) (rx/filter #(> % 1)) (rx/take 1) - (rx/map #(start-move initial selected))))))) - -(defn start-move - [from-position ids] - (ptk/reify ::start-move - ptk/UpdateEvent - (update [_ state] - (-> state - (assoc-in [:workspace-local :transform] :move))) + (rx/with-latest vector ms/mouse-position-alt) + (rx/flat-map + (fn [[_ alt?]] + (if alt? + ;; When alt is down we start a duplicate+move + (rx/of (start-move-duplicate initial) + dws/duplicate-selected) + ;; Otherwise just plain old move + (rx/of (start-move initial selected)))))))))) +(defn start-move-duplicate [from-position] + (ptk/reify ::start-move-selected ptk/WatchEvent (watch [_ state stream] - (let [page-id (get state :current-page-id) - objects (get-in state [:workspace-data page-id :objects]) - shapes (mapv #(get-in state [:workspace-data page-id :objects %]) ids) - stopper (rx/filter ms/mouse-up? stream) - layout (get state :workspace-layout)] - (rx/concat - (->> ms/mouse-position - (rx/take-until stopper) - (rx/map #(gpt/to-vec from-position %)) - (rx/switch-map #(snap/closest-snap-move page-id shapes objects layout %)) - (rx/map gmt/translate-matrix) - (rx/map #(set-modifiers ids {:displacement %}))) + (->> stream + (rx/filter (ptk/type? ::dws/duplicate-selected)) + (rx/first) + (rx/map #(start-move from-position)))))) - (rx/of (apply-modifiers ids) - finish-transform)))))) +(defn start-move + ([from-position] (start-move from-position nil)) + ([from-position ids] + (ptk/reify ::start-move + ptk/UpdateEvent + (update [_ state] + (-> state + (assoc-in [:workspace-local :transform] :move))) + + ptk/WatchEvent + (watch [_ state stream] + (let [page-id (get state :current-page-id) + objects (get-in state [:workspace-data page-id :objects]) + ids (if (nil? ids) (get-in state [:workspace-local :selected]) ids) + shapes (mapv #(get-in state [:workspace-data page-id :objects %]) ids) + stopper (rx/filter ms/mouse-up? stream) + layout (get state :workspace-layout)] + (rx/concat + (->> ms/mouse-position + (rx/take-until stopper) + (rx/map #(gpt/to-vec from-position %)) + (rx/switch-map #(snap/closest-snap-move page-id shapes objects layout %)) + (rx/map gmt/translate-matrix) + (rx/map #(set-modifiers ids {:displacement %}))) + + (rx/of (apply-modifiers ids) + finish-transform))))))) (defn- get-displacement-with-grid "Retrieve the correct displacement delta point for the diff --git a/frontend/src/uxbox/main/streams.cljs b/frontend/src/uxbox/main/streams.cljs index be9f95697..8f5b639b8 100644 --- a/frontend/src/uxbox/main/streams.cljs +++ b/frontend/src/uxbox/main/streams.cljs @@ -14,13 +14,13 @@ ;; --- User Events -(defrecord KeyboardEvent [type key shift ctrl]) +(defrecord KeyboardEvent [type key shift ctrl alt]) (defn keyboard-event? [v] (instance? KeyboardEvent v)) -(defrecord MouseEvent [type ctrl shift]) +(defrecord MouseEvent [type ctrl shift alt]) (defn mouse-event? [v] @@ -36,7 +36,7 @@ (and (mouse-event? v) (= :click (:type v)))) -(defrecord PointerEvent [source pt ctrl shift]) +(defrecord PointerEvent [source pt ctrl shift alt]) (defn pointer-event? [v] @@ -73,6 +73,24 @@ (rx/subscribe-with ob sub) sub)) +(defonce mouse-position-alt + (let [sub (rx/behavior-subject nil) + ob (->> st/stream + (rx/filter pointer-event?) + (rx/map :alt) + (rx/dedupe))] + (rx/subscribe-with ob sub) + sub)) + +(defonce keyboard-alt + (let [sub (rx/behavior-subject nil) + ob (->> st/stream + (rx/filter keyboard-event?) + (rx/map :alt) + (rx/dedupe))] + (rx/subscribe-with ob sub) + sub)) + (defn mouse-position-deltas [current] (->> (rx/concat (rx/of current) @@ -97,4 +115,3 @@ (rx/map :point))] (rx/subscribe-with sob sub) sub)) - diff --git a/frontend/src/uxbox/main/ui/cursors.clj b/frontend/src/uxbox/main/ui/cursors.clj index 46270d37f..1b10738c1 100644 --- a/frontend/src/uxbox/main/ui/cursors.clj +++ b/frontend/src/uxbox/main/ui/cursors.clj @@ -59,7 +59,7 @@ transform (if rotation (str " transform='rotate(" rotation ")'") "") data (clojure.pprint/cl-format nil - "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='24px' height='24px'~A%3E~A%3C/svg%3E\") ~A ~A, auto" + "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' width='20px' height='20px'~A%3E~A%3C/svg%3E\") ~A ~A, auto" transform data x y)] data)) diff --git a/frontend/src/uxbox/main/ui/cursors.cljs b/frontend/src/uxbox/main/ui/cursors.cljs index f02d31951..c78b38846 100644 --- a/frontend/src/uxbox/main/ui/cursors.cljs +++ b/frontend/src/uxbox/main/ui/cursors.cljs @@ -53,4 +53,3 @@ [:span {:style {:white-space "nowrap" :margin-right "1rem"}} (pr-str key)]])))])) - diff --git a/frontend/src/uxbox/main/ui/hooks.cljs b/frontend/src/uxbox/main/ui/hooks.cljs index fdbcefa4b..8534c7e2b 100644 --- a/frontend/src/uxbox/main/ui/hooks.cljs +++ b/frontend/src/uxbox/main/ui/hooks.cljs @@ -227,3 +227,10 @@ [(deref state) ref])) + +(defn use-stream + "Wraps the subscription to a strem into a `use-effect` call" + [stream on-subscribe] + (mf/use-effect (fn [] + (let [sub (->> stream (rx/subs on-subscribe))] + #(rx/dispose! sub))))) diff --git a/frontend/src/uxbox/main/ui/workspace/shapes.cljs b/frontend/src/uxbox/main/ui/workspace/shapes.cljs index 022cc4e39..296be3bd8 100644 --- a/frontend/src/uxbox/main/ui/workspace/shapes.cljs +++ b/frontend/src/uxbox/main/ui/workspace/shapes.cljs @@ -11,6 +11,10 @@ "A workspace specific shapes wrappers." (:require [rumext.alpha :as mf] + [beicon.core :as rx] + [uxbox.main.streams :as ms] + [uxbox.main.ui.hooks :as hooks] + [uxbox.main.ui.cursors :as cur] [uxbox.main.ui.shapes.rect :as rect] [uxbox.main.ui.shapes.circle :as circle] [uxbox.main.ui.shapes.icon :as icon] @@ -55,9 +59,13 @@ (let [shape (unchecked-get props "shape") frame (unchecked-get props "frame") opts #js {:shape (->> shape (geom/transform-shape frame)) - :frame frame}] + :frame frame} + alt? (mf/use-state false)] + + (hooks/use-stream ms/keyboard-alt #(reset! alt? %)) + (when (and shape (not (:hidden shape))) - [:* + [:g.shape {:style {:cursor (if @alt? cur/duplicate nil)}} (case (:type shape) :curve [:> path/path-wrapper opts] :path [:> path/path-wrapper opts] diff --git a/frontend/src/uxbox/main/ui/workspace/viewport.cljs b/frontend/src/uxbox/main/ui/workspace/viewport.cljs index ae5b8f162..dae03277b 100644 --- a/frontend/src/uxbox/main/ui/workspace/viewport.cljs +++ b/frontend/src/uxbox/main/ui/workspace/viewport.cljs @@ -154,9 +154,8 @@ (let [event (.-nativeEvent event) ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (ms/->MouseEvent :down ctrl? shift?)) + alt? (kbd/alt? event)] + (st/emit! (ms/->MouseEvent :down ctrl? shift? alt?)) (cond (and (not edition) (= 1 (.-which event))) @@ -183,9 +182,8 @@ (let [event (.-nativeEvent event) ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (ms/->MouseEvent :up ctrl? shift?)) + alt? (kbd/alt? event)] + (st/emit! (ms/->MouseEvent :up ctrl? shift? alt?)) (when (= 2 (.-which event)) (st/emit! dw/finish-pan @@ -213,9 +211,8 @@ (dom/stop-propagation event) (let [ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (ms/->MouseEvent :click ctrl? shift?))))) + alt? (kbd/alt? event)] + (st/emit! (ms/->MouseEvent :click ctrl? shift? alt?))))) on-double-click (mf/use-callback @@ -223,9 +220,8 @@ (dom/stop-propagation event) (let [ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) - opts {:shift? shift? - :ctrl? ctrl?}] - (st/emit! (ms/->MouseEvent :double-click ctrl? shift?))))) + alt? (kbd/alt? event)] + (st/emit! (ms/->MouseEvent :double-click ctrl? shift? alt?))))) on-key-down (mf/use-callback @@ -234,13 +230,11 @@ key (.-keyCode event) ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) - opts {:key key - :shift? shift? - :ctrl? ctrl?} + alt? (kbd/alt? event) target (dom/get-target event)] (when-not (.-repeat bevent) - (st/emit! (ms/->KeyboardEvent :down key ctrl? shift?)) + (st/emit! (ms/->KeyboardEvent :down key ctrl? shift? alt?)) (when (and (kbd/space? event) (not= "rich-text" (obj/get target "className"))) (handle-viewport-positioning viewport-ref)))))) @@ -251,13 +245,10 @@ (let [key (.-keyCode event) ctrl? (kbd/ctrl? event) shift? (kbd/shift? event) - opts {:key key - :shift? shift? - :ctrl? ctrl?}] + alt? (kbd/alt? event)] (when (kbd/space? event) - (st/emit! dw/finish-pan - ::finish-positioning)) - (st/emit! (ms/->KeyboardEvent :up key ctrl? shift?))))) + (st/emit! dw/finish-pan ::finish-positioning)) + (st/emit! (ms/->KeyboardEvent :up key ctrl? shift? alt?))))) translate-point-to-viewport (fn [pt] @@ -282,10 +273,12 @@ (.-movementY event))] (st/emit! (ms/->PointerEvent :delta delta (kbd/ctrl? event) - (kbd/shift? event))) + (kbd/shift? event) + (kbd/alt? event))) (st/emit! (ms/->PointerEvent :viewport pt (kbd/ctrl? event) - (kbd/shift? event))))) + (kbd/shift? event) + (kbd/alt? event))))) on-mouse-wheel (mf/use-callback