mirror of
https://github.com/penpot/penpot.git
synced 2026-05-19 23:13:39 +00:00
* ✨ Capture selection state before changes are applied Save current selection IDs in commit-changes so undo entries can track what was selected before each action. * ✨ Save and restore selection state in undo/redo Extend undo entry with selected-before and selected-after fields. On undo, restore selection to what it was before the action. On redo, restore selection to what it was after the action. Handles single entries, stacked entries, accumulated transactions, and undo groups. Fixes #6007 * ♻️ Wire selected-before through workspace undo stream Pass the captured selection state from commit data into the undo entry so it is stored alongside changes. * 🐛 Fix unmatched delimiter in changes.cljs * 🐛 Pass selected-before through commit event to undo entry selected-before was captured in commit-changes but dropped by the commit function since it was missing from the destructuring and the commit map. This caused restore-selection to receive nil on undo. --------- Signed-off-by: eureka928 <meobius123@gmail.com> Co-authored-by: Mihai <noreply@github.com>
1549 lines
55 KiB
Clojure
1549 lines
55 KiB
Clojure
;; 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) KALEIDOS INC
|
|
|
|
(ns app.main.data.workspace
|
|
(:require
|
|
[app.common.attrs :as attrs]
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.files.changes-builder :as pcb]
|
|
[app.common.files.helpers :as cfh]
|
|
[app.common.files.variant :as cfv]
|
|
[app.common.geom.align :as gal]
|
|
[app.common.geom.point :as gpt]
|
|
[app.common.geom.proportions :as gpp]
|
|
[app.common.geom.shapes :as gsh]
|
|
[app.common.logging :as log]
|
|
[app.common.path-names :as cpn]
|
|
[app.common.transit :as t]
|
|
[app.common.types.component :as ctc]
|
|
[app.common.types.components-list :as ctkl]
|
|
[app.common.types.shape :as cts]
|
|
[app.common.types.variant :as ctv]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.main.broadcast :as mbc]
|
|
[app.main.data.changes :as dch]
|
|
[app.main.data.comments :as dcmt]
|
|
[app.main.data.common :as dcm]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.fonts :as df]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.notifications :as ntf]
|
|
[app.main.data.persistence :as dps]
|
|
[app.main.data.plugins :as dp]
|
|
[app.main.data.profile :as du]
|
|
[app.main.data.project :as dpj]
|
|
[app.main.data.workspace.bool :as dwb]
|
|
[app.main.data.workspace.clipboard :as dwcp]
|
|
[app.main.data.workspace.colors :as dwcl]
|
|
[app.main.data.workspace.comments :as dwcm]
|
|
[app.main.data.workspace.common :as dwc]
|
|
[app.main.data.workspace.drawing :as dwd]
|
|
[app.main.data.workspace.edition :as dwe]
|
|
[app.main.data.workspace.fix-deleted-fonts :as fdf]
|
|
[app.main.data.workspace.groups :as dwg]
|
|
[app.main.data.workspace.guides :as dwgu]
|
|
[app.main.data.workspace.highlight :as dwh]
|
|
[app.main.data.workspace.interactions :as dwi]
|
|
[app.main.data.workspace.layers :as dwly]
|
|
[app.main.data.workspace.layout :as layout]
|
|
[app.main.data.workspace.libraries :as dwl]
|
|
[app.main.data.workspace.mcp :as mcp]
|
|
[app.main.data.workspace.notifications :as dwn]
|
|
[app.main.data.workspace.pages :as dwpg]
|
|
[app.main.data.workspace.path :as dwdp]
|
|
[app.main.data.workspace.path.shapes-to-path :as dwps]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.shape-layout :as dwsl]
|
|
[app.main.data.workspace.shapes :as dwsh]
|
|
[app.main.data.workspace.thumbnails :as dwth]
|
|
[app.main.data.workspace.transforms :as dwt]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[app.main.data.workspace.variants :as dwva]
|
|
[app.main.data.workspace.viewport :as dwv]
|
|
[app.main.data.workspace.zoom :as dwz]
|
|
[app.main.errors]
|
|
[app.main.features :as features]
|
|
[app.main.features.pointer-map :as fpmap]
|
|
[app.main.refs :as refs]
|
|
[app.main.repo :as rp]
|
|
[app.main.router :as rt]
|
|
[app.render-wasm :as wasm]
|
|
[app.render-wasm.api :as wasm.api]
|
|
[app.util.dom :as dom]
|
|
[app.util.globals :as ug]
|
|
[app.util.http :as http]
|
|
[app.util.perf :as perf]
|
|
[app.util.storage :as storage]
|
|
[app.util.timers :as tm]
|
|
[app.util.webapi :as wapi]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(log/set-level! :info)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Workspace Initialization
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(declare ^:private workspace-initialized)
|
|
(declare ^:private fetch-libraries)
|
|
|
|
;; --- Initialize Workspace
|
|
|
|
(defn initialize-workspace-layout
|
|
[lname]
|
|
(ptk/reify ::initialize-layout
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(update :workspace-layout #(or % layout/default-layout))
|
|
(update :workspace-global #(or % layout/default-global))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(if (and lname (contains? layout/presets lname))
|
|
(rx/of (layout/ensure-layout lname))
|
|
(rx/of (layout/ensure-layout :layers))))))
|
|
|
|
(defn- datauri->blob-uri
|
|
[uri]
|
|
(->> (http/send! {:uri uri
|
|
:response-type :blob
|
|
:method :get})
|
|
(rx/map :body)
|
|
(rx/map (fn [blob] (wapi/create-uri blob)))))
|
|
|
|
(defn- get-file-object-thumbnails
|
|
[file-id]
|
|
(->> (rp/cmd! :get-file-object-thumbnails {:file-id file-id})
|
|
(rx/mapcat (fn [thumbnails]
|
|
(->> (rx/from thumbnails)
|
|
(rx/mapcat (fn [[k v]]
|
|
;; we only need to fetch the thumbnail if
|
|
;; it is a data:uri, otherwise we can just
|
|
;; use the value as is.
|
|
(if (str/starts-with? v "data:")
|
|
(->> (datauri->blob-uri v)
|
|
(rx/map (fn [uri] [k uri])))
|
|
(rx/of [k v])))))))
|
|
(rx/reduce conj {})))
|
|
|
|
(defn- resolve-file
|
|
[file]
|
|
(log/inf :hint "resolve file"
|
|
:file-id (str (:id file))
|
|
:features (str/join " " (:features file)))
|
|
(->> (fpmap/resolve-file file)
|
|
(rx/map :data)
|
|
(rx/map
|
|
(fn [data]
|
|
(assoc file :data (d/removem (comp t/pointer? val) data))))))
|
|
|
|
(defn- check-libraries-synchronization
|
|
[file-id libraries]
|
|
(ptk/reify ::check-libraries-synchronization
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [file (dsh/lookup-file state file-id)
|
|
ignore-until (get file :ignore-sync-until)
|
|
|
|
needs-check?
|
|
(some #(and (> (:modified-at %) (:synced-at %))
|
|
(or (not ignore-until)
|
|
(> (:modified-at %) ignore-until)))
|
|
libraries)]
|
|
|
|
(when needs-check?
|
|
(->> (rx/of (dwl/notify-sync-file))
|
|
(rx/delay 1000)))))))
|
|
|
|
(defn- library-resolved
|
|
[library]
|
|
(ptk/reify ::library-resolved
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update state :files assoc (:id library) library))))
|
|
|
|
(defn- fetch-libraries
|
|
[file-id features]
|
|
(ptk/reify ::fetch-libries
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
|
|
(->> (rx/concat
|
|
(->> (rp/cmd! :get-file-libraries {:file-id file-id})
|
|
(rx/mapcat
|
|
(fn [libraries]
|
|
(rx/concat
|
|
(rx/of (dwl/libraries-fetched file-id libraries))
|
|
(rx/merge
|
|
(->> (rx/from libraries)
|
|
(rx/merge-map
|
|
(fn [{:keys [id synced-at]}]
|
|
(->> (rp/cmd! :get-file {:id id :features features})
|
|
(rx/map #(assoc % :synced-at synced-at :library-of file-id)))))
|
|
(rx/mapcat resolve-file)
|
|
(rx/map library-resolved))
|
|
(->> (rx/from libraries)
|
|
(rx/map :id)
|
|
(rx/mapcat (fn [file-id]
|
|
(rp/cmd! :get-file-object-thumbnails {:file-id file-id :tag "component"})))
|
|
(rx/map dwl/library-thumbnails-fetched)))
|
|
(rx/of (check-libraries-synchronization file-id libraries))))))
|
|
|
|
;; This events marks that all the libraries have been resolved
|
|
(rx/of (ptk/data-event ::all-libraries-resolved {:file-id file-id})))
|
|
(rx/take-until stopper-s))))))
|
|
|
|
(defn- workspace-initialized
|
|
[file-id]
|
|
(ptk/reify ::workspace-initialized
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(assoc :workspace-undo {})
|
|
(assoc :workspace-ready file-id)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (dp/check-open-plugin)
|
|
(fdf/fix-deleted-fonts-for-local-library file-id)
|
|
(mcp/init-mcp-connection)))))
|
|
|
|
(defn- bundle-fetched
|
|
[{:keys [file file-id thumbnails] :as bundle}]
|
|
(ptk/reify ::bundle-fetched
|
|
IDeref
|
|
(-deref [_] bundle)
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [pending-version-id (:workspace-pending-file-version-id state)
|
|
state (-> state
|
|
(assoc :thumbnails thumbnails)
|
|
(update :files assoc file-id file)
|
|
(dissoc :workspace-pending-file-version-id))]
|
|
(cond-> state
|
|
(some? pending-version-id)
|
|
(assoc :workspace-file-version-id pending-version-id)
|
|
(nil? pending-version-id)
|
|
(dissoc :workspace-file-version-id))))))
|
|
|
|
(defn zoom-to-frame
|
|
[]
|
|
(ptk/reify ::zoom-to-frame
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [params (rt/get-params state)
|
|
board-id (get params :board-id)
|
|
board-id (cond
|
|
(vector? board-id) board-id
|
|
(string? board-id) [board-id])
|
|
frames-id (->> board-id
|
|
(map uuid/uuid)
|
|
(into (d/ordered-set)))]
|
|
(rx/of (dws/select-shapes frames-id)
|
|
dwz/zoom-to-selected-shape)))))
|
|
|
|
(defn- fetch-bundle
|
|
"Multi-stage file bundle fetch coordinator"
|
|
[file-id features]
|
|
(ptk/reify ::fetch-bundle
|
|
ptk/WatchEvent
|
|
(watch [_ _ stream]
|
|
(log/debug :hint "fetch bundle" :file-id (dm/str file-id))
|
|
|
|
(let [stopper-s (rx/filter (ptk/type? ::finalize-workspace) stream)]
|
|
(->> (rx/zip (rp/cmd! :get-file {:id file-id :features features})
|
|
(get-file-object-thumbnails file-id))
|
|
(rx/take 1)
|
|
(rx/mapcat
|
|
(fn [[file thumbnails]]
|
|
(->> (resolve-file file)
|
|
(rx/map (fn [file]
|
|
(log/trace :hint "file resolved" :file-id file-id)
|
|
{:file file
|
|
:file-id file-id
|
|
:features features
|
|
:thumbnails thumbnails})))))
|
|
(rx/map bundle-fetched)
|
|
(rx/take-until stopper-s))))))
|
|
|
|
;; FIXME: this need docstring
|
|
(defn- process-wasm-object
|
|
[id]
|
|
(ptk/reify ::process-wasm-object
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
shape (get objects id)]
|
|
;; Only process objects that exist in the current page
|
|
;; This prevents errors when processing changes from other pages
|
|
(when shape
|
|
(wasm.api/process-object shape))))))
|
|
|
|
(defn initialize-workspace
|
|
([team-id file-id]
|
|
(initialize-workspace team-id file-id nil))
|
|
([team-id file-id version-id]
|
|
(assert (uuid? team-id) "expected valud uuid for `team-id`")
|
|
(assert (uuid? file-id) "expected valud uuid for `file-id`")
|
|
|
|
(ptk/reify ::initialize-workspace
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
(assoc :recent-colors (:recent-colors storage/user))
|
|
(assoc :recent-fonts (:recent-fonts storage/user))
|
|
(assoc :current-file-id file-id)
|
|
(assoc :workspace-presence {})
|
|
;; Store pending version-id; bundle-fetched will set workspace-file-version-id
|
|
;; when the new bundle is applied so the viewport re-inits with new data
|
|
(assoc :workspace-pending-file-version-id version-id)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state stream]
|
|
(let [stoper-s (rx/filter (ptk/type? ::finalize-workspace) stream)
|
|
rparams (rt/get-params state)
|
|
features (features/get-enabled-features state team-id)
|
|
render-wasm? (contains? features "render-wasm/v1")]
|
|
|
|
(log/debug :hint "initialize-workspace"
|
|
:team-id (dm/str team-id)
|
|
:file-id (dm/str file-id))
|
|
|
|
(->> (rx/merge
|
|
(rx/concat
|
|
;; Fetch all essential data that should be loaded before the file
|
|
(rx/merge
|
|
(if ^boolean render-wasm?
|
|
(->> (rx/from @wasm/module)
|
|
(rx/filter true?)
|
|
(rx/tap (fn [_]
|
|
(let [event (ug/event "penpot:wasm:loaded")]
|
|
(ug/dispatch! event))))
|
|
(rx/ignore))
|
|
(rx/empty))
|
|
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::df/fonts-loaded))
|
|
(rx/take 1)
|
|
(rx/ignore))
|
|
|
|
(rx/of (ntf/hide)
|
|
(dcmt/retrieve-comment-threads file-id)
|
|
(dcmt/fetch-profiles)
|
|
(df/fetch-fonts team-id))
|
|
|
|
(when (contains? cf/flags :mcp)
|
|
(rx/of (du/fetch-access-tokens))))
|
|
|
|
;; Once the essential data is fetched, lets proceed to
|
|
;; fetch teh file bunldle
|
|
(rx/of (fetch-bundle file-id features)))
|
|
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::bundle-fetched))
|
|
(rx/take 1)
|
|
(rx/map deref)
|
|
(rx/mapcat
|
|
(fn [{:keys [file]}]
|
|
(log/debug :hint "bundle fetched"
|
|
:team-id (dm/str team-id)
|
|
:file-id (dm/str file-id))
|
|
|
|
(rx/of (dpj/initialize-project (:project-id file))
|
|
(dwn/initialize team-id file-id)
|
|
(dwsl/initialize-shape-layout)
|
|
(fetch-libraries file-id features)
|
|
(-> (workspace-initialized file-id)
|
|
(with-meta {:team-id team-id
|
|
:file-id file-id}))))))
|
|
|
|
;; Install dev perf observers once the workspace is ready
|
|
(when (contains? cf/flags :perf-logs)
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::workspace-initialized))
|
|
(rx/take 1)
|
|
(rx/tap (fn [_] (perf/setup)))))
|
|
|
|
(when (contains? cf/flags :mcp)
|
|
(->> mbc/stream
|
|
(rx/filter (mbc/type? :mcp-enabled-change-connection))
|
|
(rx/map deref)
|
|
(rx/mapcat (fn [value]
|
|
(rx/of (mcp/update-mcp-connection value)
|
|
(mcp/user-disconnect-mcp))))))
|
|
|
|
(when (contains? cf/flags :mcp)
|
|
(->> mbc/stream
|
|
(rx/filter (mbc/type? :mcp-enabled-change-status))
|
|
(rx/map deref)
|
|
(rx/map mcp/update-mcp-status)))
|
|
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dps/persistence-notification))
|
|
(rx/take 1)
|
|
(rx/map dwc/set-workspace-visited))
|
|
|
|
(when-let [component-id (some-> rparams :component-id uuid/parse)]
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::workspace-initialized))
|
|
(rx/observe-on :async)
|
|
(rx/take 1)
|
|
(rx/map #(dwl/go-to-local-component :id component-id :update-layout? (:update-layout rparams)))))
|
|
|
|
(when (:board-id rparams)
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::dwv/initialize-viewport))
|
|
(rx/take 1)
|
|
(rx/map zoom-to-frame)))
|
|
|
|
(when-let [comment-id (some-> rparams :comment-id uuid/parse)]
|
|
(->> stream
|
|
(rx/filter (ptk/type? ::workspace-initialized))
|
|
(rx/observe-on :async)
|
|
(rx/take 1)
|
|
(rx/map #(dwcm/navigate-to-comment-id comment-id))))
|
|
|
|
(when render-wasm?
|
|
(->> stream
|
|
(rx/filter dch/commit?)
|
|
(rx/map deref)
|
|
(rx/mapcat
|
|
(fn [{:keys [redo-changes]}]
|
|
(let [added (->> redo-changes
|
|
(filter #(= (:type %) :add-obj))
|
|
(map :id))]
|
|
(->> (rx/from added)
|
|
(rx/map process-wasm-object)))))))
|
|
|
|
(when render-wasm?
|
|
(let [local-commits-s
|
|
(->> stream
|
|
(rx/filter dch/commit?)
|
|
(rx/map deref)
|
|
(rx/filter #(and (= :local (:source %))
|
|
(not (contains? (:tags %) :position-data))))
|
|
(rx/filter (complement empty?)))
|
|
|
|
notifier-s
|
|
(rx/merge
|
|
(->> local-commits-s (rx/debounce 1000))
|
|
(->> stream (rx/filter dps/force-persist?)))
|
|
|
|
objects-s
|
|
(rx/from-atom refs/workspace-page-objects {:emit-current-value? true})
|
|
|
|
current-page-id-s
|
|
(rx/from-atom refs/current-page-id {:emit-current-value? true})]
|
|
|
|
(->> local-commits-s
|
|
(rx/buffer-until notifier-s)
|
|
(rx/with-latest-from objects-s)
|
|
(rx/map
|
|
(fn [[commits objects]]
|
|
(->> commits
|
|
(mapcat :redo-changes)
|
|
(filter #(contains? #{:mod-obj :add-obj} (:type %)))
|
|
(filter #(cfh/text-shape? objects (:id %)))
|
|
(map #(vector
|
|
(:id %)
|
|
(wasm.api/calculate-position-data (get objects (:id %))))))))
|
|
|
|
(rx/with-latest-from current-page-id-s)
|
|
(rx/map
|
|
(fn [[text-position-data page-id]]
|
|
(let [changes
|
|
(->> text-position-data
|
|
(mapv (fn [[id position-data]]
|
|
{:type :mod-obj
|
|
:id id
|
|
:page-id page-id
|
|
:operations
|
|
[{:type :set
|
|
:attr :position-data
|
|
:val position-data
|
|
:ignore-touched true
|
|
:ignore-geometry true}]})))]
|
|
(when (d/not-empty? changes)
|
|
(dch/commit-changes
|
|
{:redo-changes changes :undo-changes []
|
|
:save-undo? false
|
|
:tags #{:position-data}})))))
|
|
(rx/take-until stoper-s))))
|
|
|
|
(->> stream
|
|
(rx/filter dch/commit?)
|
|
(rx/map deref)
|
|
(rx/mapcat
|
|
(fn [{:keys [save-undo? undo-changes redo-changes undo-group tags stack-undo? selected-before]}]
|
|
(if (and save-undo? (seq undo-changes))
|
|
(let [entry {:undo-changes undo-changes
|
|
:redo-changes redo-changes
|
|
:undo-group undo-group
|
|
:tags tags
|
|
:selected-before selected-before}]
|
|
(rx/of (dwu/append-undo entry stack-undo?)))
|
|
(rx/empty))))))
|
|
(rx/take-until stoper-s))))
|
|
|
|
ptk/EffectEvent
|
|
(effect [_ _ _]
|
|
(let [name (dm/str "workspace-" file-id)]
|
|
(unchecked-set ug/global "name" name))))))
|
|
|
|
(defn finalize-workspace
|
|
[_team-id file-id]
|
|
(ptk/reify ::finalize-workspace
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(-> state
|
|
;; FIXME: revisit
|
|
(dissoc
|
|
:current-file-id
|
|
:workspace-editor-state
|
|
:workspace-media-objects
|
|
:workspace-persistence
|
|
:workspace-presence
|
|
:workspace-tokens
|
|
:workspace-undo)
|
|
(update :workspace-global dissoc :read-only?)
|
|
(assoc-in [:workspace-global :options-mode] :design)
|
|
(update :files d/update-vals #(dissoc % :data))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [project-id (:current-project-id state)]
|
|
(rx/of (dwn/finalize file-id)
|
|
(dpj/finalize-project project-id)
|
|
(dwsl/finalize-shape-layout)
|
|
(dwcl/stop-picker)
|
|
(dwc/set-workspace-visited)
|
|
(modal/hide)
|
|
(ntf/hide))))))
|
|
|
|
(defn- reload-current-file
|
|
[]
|
|
(ptk/reify ::reload-current-file
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [file-id (:current-file-id state)
|
|
team-id (:current-team-id state)]
|
|
(rx/of (initialize-workspace team-id file-id))))))
|
|
|
|
;; Make this event callable through dynamic resolution
|
|
(defmethod ptk/resolve ::reload-current-file [_ _] (reload-current-file))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; WORKSPACE File Actions
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; FIXME: move to common
|
|
(defn rename-file
|
|
[id name]
|
|
{:pre [(uuid? id) (string? name)]}
|
|
(let [name (dm/truncate name 200)]
|
|
(ptk/reify ::rename-file
|
|
IDeref
|
|
(-deref [_]
|
|
{::ev/origin "workspace" :id id :name name})
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [file-id (:current-file-id state)]
|
|
(assoc-in state [:files file-id :name] name)))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(let [params {:id id :name name}]
|
|
(->> (rp/cmd! :rename-file params)
|
|
(rx/ignore)))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Workspace State Manipulation
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; --- Layout Flags
|
|
|
|
(dm/export layout/toggle-layout-flag)
|
|
(dm/export layout/remove-layout-flag)
|
|
|
|
;; --- Profile
|
|
|
|
(defn update-nudge
|
|
[{:keys [big small] :as params}]
|
|
(ptk/reify ::update-nudge
|
|
IDeref
|
|
(-deref [_] (d/without-nils params))
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-in state [:profile :props :nudge]
|
|
(fn [nudge]
|
|
(cond-> nudge
|
|
(number? big) (assoc :big big)
|
|
(number? small) (assoc :small small)))))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [nudge (get-in state [:profile :props :nudge])]
|
|
(rx/of (du/update-profile-props {:nudge nudge}))))))
|
|
|
|
;; --- Set element options mode
|
|
|
|
(dm/export layout/set-options-mode)
|
|
|
|
;; --- Tooltip
|
|
|
|
(defn assign-cursor-tooltip
|
|
[content]
|
|
(ptk/reify ::assign-cursor-tooltip
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if (string? content)
|
|
(assoc-in state [:workspace-global :tooltip] content)
|
|
(assoc-in state [:workspace-global :tooltip] nil)))))
|
|
|
|
;; --- Update Shape Attrs
|
|
|
|
;; FIXME: rename to update-shape-generic-attrs because on the end we
|
|
;; only allow here to update generic attrs
|
|
(defn update-shape
|
|
[id attrs]
|
|
(assert (uuid? id) "expected valid uuid for `id`")
|
|
(let [attrs (cts/check-shape-generic-attrs attrs)]
|
|
(ptk/reify ::update-shape
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (dwsh/update-shapes [id] #(merge % attrs)))))))
|
|
|
|
(defn start-rename-shape
|
|
"Start shape renaming process"
|
|
[id]
|
|
(dm/assert! (uuid? id))
|
|
(ptk/reify ::start-rename-shape
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-local :shape-for-rename] id))))
|
|
|
|
(defn end-rename-shape
|
|
"End the ongoing shape rename process"
|
|
([] (end-rename-shape nil nil))
|
|
([shape-id name]
|
|
(ptk/reify ::end-rename-shape
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
;; Remove rename state from workspace local state
|
|
(update state :workspace-local dissoc :shape-for-rename))
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(when-let [shape-id (d/nilv shape-id (dm/get-in state [:workspace-local :shape-for-rename]))]
|
|
(let [shape (dsh/lookup-shape state shape-id)
|
|
name (str/trim name)
|
|
clean-name (cpn/clean-path name)
|
|
valid? (and (not (str/ends-with? name "/"))
|
|
(string? clean-name)
|
|
(not (str/blank? clean-name)))
|
|
component-id (:component-id shape)
|
|
undo-id (js/Symbol)]
|
|
|
|
(when valid?
|
|
(if (ctc/is-variant-container? shape)
|
|
;; Rename the full variant when it is a variant container
|
|
(rx/of (dwva/rename-variant shape-id clean-name))
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
;; Rename the shape if string is not empty/blank
|
|
(update-shape shape-id {:name clean-name})
|
|
|
|
;; Update the component in case shape is a main instance
|
|
(when (and (some? component-id) (ctc/main-instance? shape))
|
|
(dwl/rename-component component-id clean-name))
|
|
(dwu/commit-undo-transaction undo-id))))))))))
|
|
|
|
(defn rename-shape-or-variant
|
|
([id name]
|
|
(rename-shape-or-variant nil nil id name))
|
|
([file-id page-id id name]
|
|
(ptk/reify ::rename-shape-or-variant
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [file-id (d/nilv file-id (:current-file-id state))
|
|
page-id (d/nilv page-id (:current-page-id state))
|
|
|
|
file-data (dsh/lookup-file-data state file-id)
|
|
shape
|
|
(-> (dsh/lookup-page-objects state file-id page-id)
|
|
(get id))
|
|
|
|
is-variant? (ctc/is-variant? shape)
|
|
variant-id (when is-variant? (:variant-id shape))
|
|
variant-name (when is-variant? (:variant-name shape))
|
|
component-id (:component-id shape)
|
|
component (ctkl/get-component file-data (:component-id shape))
|
|
variant-properties (:variant-properties component)]
|
|
(cond
|
|
(and variant-name (ctv/valid-properties-formula? name))
|
|
(rx/of (dwva/update-properties-names-and-values
|
|
component-id variant-id variant-properties (ctv/properties-formula->map name))
|
|
(dwva/remove-empty-properties variant-id)
|
|
(dwva/update-error component-id))
|
|
|
|
variant-name
|
|
(rx/of (dwva/update-properties-names-and-values
|
|
component-id variant-id variant-properties {})
|
|
(dwva/remove-empty-properties variant-id)
|
|
(dwva/update-error component-id name))
|
|
|
|
:else
|
|
(rx/of (end-rename-shape id name))))))))
|
|
|
|
;; --- Update Selected Shapes attrs
|
|
|
|
(defn update-selected-shapes
|
|
[attrs]
|
|
(ptk/reify ::update-selected-shapes
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)]
|
|
(rx/from (map #(update-shape % attrs) selected))))))
|
|
|
|
;; --- Delete Selected
|
|
|
|
(defn delete-selected
|
|
"Deselect all and remove all selected shapes."
|
|
[]
|
|
(ptk/reify ::delete-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)
|
|
hover-guides (get-in state [:workspace-guides :hover])]
|
|
(cond
|
|
(d/not-empty? selected)
|
|
(rx/of (dwsh/delete-shapes selected)
|
|
(dws/deselect-all))
|
|
|
|
(d/not-empty? hover-guides)
|
|
(rx/of (dwgu/remove-guides hover-guides)))))))
|
|
|
|
|
|
;; --- Start renaming selected shape
|
|
|
|
(defn start-rename-selected
|
|
"Rename selected shape."
|
|
[]
|
|
(ptk/reify ::start-rename-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)
|
|
id (first selected)]
|
|
(when (= (count selected) 1)
|
|
(rx/of (dcm/go-to-workspace :layout :layers)
|
|
(start-rename-shape id)))))))
|
|
|
|
;; --- Shape Vertical Ordering
|
|
|
|
(def valid-vertical-locations
|
|
#{:up :down :bottom :top})
|
|
|
|
(defn vertical-order-selected
|
|
[loc]
|
|
(dm/assert!
|
|
"expected valid location"
|
|
(contains? valid-vertical-locations loc))
|
|
(ptk/reify ::vertical-order-selected
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
selected-ids (dsh/lookup-selected state)
|
|
selected-shapes (map (d/getf objects) selected-ids)
|
|
undo-id (js/Symbol)
|
|
|
|
move-shape
|
|
(fn [changes shape]
|
|
(let [parent (get objects (:parent-id shape))
|
|
sibling-ids (:shapes parent)
|
|
current-index (d/index-of sibling-ids (:id shape))
|
|
index-in-selection (d/index-of selected-ids (:id shape))
|
|
new-index (case loc
|
|
:top (count sibling-ids)
|
|
:down (max 0 (- current-index 1))
|
|
:up (min (count sibling-ids) (+ (inc current-index) 1))
|
|
:bottom index-in-selection)]
|
|
(pcb/change-parent changes
|
|
(:id parent)
|
|
[shape]
|
|
new-index)))
|
|
|
|
changes (reduce move-shape
|
|
(-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects))
|
|
selected-shapes)]
|
|
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(ptk/data-event :layout/update {:ids selected-ids})
|
|
(dwu/commit-undo-transaction undo-id))))))
|
|
|
|
(defn set-shape-index
|
|
[file-id page-id id new-index]
|
|
(ptk/reify ::set-shape-index
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [file-id (or file-id (:current-file-id state))
|
|
page-id (or page-id (:current-page-id state))
|
|
|
|
objects (dsh/lookup-page-objects state file-id page-id)
|
|
|
|
undo-id (js/Symbol)
|
|
|
|
shape (get objects id)
|
|
parent (get objects (:parent-id shape))
|
|
|
|
current-index (d/index-of (:shapes parent) id)
|
|
|
|
new-index
|
|
(if (> new-index current-index)
|
|
(inc new-index)
|
|
new-index)
|
|
|
|
changes
|
|
(-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects)
|
|
(pcb/change-parent (:id parent) [shape] new-index))]
|
|
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(ptk/data-event :layout/update {:ids [id]})
|
|
(dwu/commit-undo-transaction undo-id))))))
|
|
|
|
(defn reorder-children
|
|
[file-id page-id parent-id children]
|
|
|
|
(ptk/reify ::reorder-children
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [file-id (or file-id (:current-file-id state))
|
|
page-id (or page-id (:current-page-id state))
|
|
objects (dsh/lookup-page-objects state file-id page-id)
|
|
undo-id (js/Symbol)
|
|
|
|
changes
|
|
(-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects)
|
|
(pcb/reorder-children parent-id children))]
|
|
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(ptk/data-event :layout/update {:ids [parent-id]})
|
|
(dwu/commit-undo-transaction undo-id))))))
|
|
|
|
;; --- Change Shape Order (D&D Ordering)
|
|
|
|
(defn relocate-selected-shapes
|
|
[parent-id to-index]
|
|
(ptk/reify ::relocate-selected-shapes
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)]
|
|
(rx/of (dwsh/relocate-shapes selected parent-id to-index))))))
|
|
|
|
(defn start-editing-selected
|
|
[]
|
|
(ptk/reify ::start-editing-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)
|
|
objects (dsh/lookup-page-objects state)]
|
|
|
|
(condp = (count selected)
|
|
0 (rx/empty)
|
|
1 (let [{:keys [id type] :as shape} (get objects (first selected))]
|
|
(case type
|
|
:text
|
|
(rx/of (dwe/start-edition-mode id))
|
|
|
|
(:group :bool :frame)
|
|
(let [shapes-ids (into (d/ordered-set) (get shape :shapes))]
|
|
(rx/of (dws/select-shapes shapes-ids)))
|
|
|
|
:svg-raw
|
|
nil
|
|
|
|
(rx/of (dwe/start-edition-mode id)
|
|
(dwdp/start-path-edit id))))
|
|
|
|
;; When we have multiple shapes selected, instead of enter
|
|
;; on the edition mode, we proceed to select all children of
|
|
;; the selected shapes. Because we can't enter on edition
|
|
;; mode on multiple shapes and this is a fallback operation.
|
|
(let [shapes-to-select
|
|
(->> selected
|
|
(reduce
|
|
(fn [result shape-id]
|
|
(let [children (dm/get-in objects [shape-id :shapes])]
|
|
(if (empty? children)
|
|
(conj result shape-id)
|
|
(into result children))))
|
|
(d/ordered-set)))]
|
|
(rx/of (dws/select-shapes shapes-to-select))))))))
|
|
|
|
(defn select-parent-layer
|
|
[]
|
|
(ptk/reify ::select-parent-layer
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)
|
|
objects (dsh/lookup-page-objects state)
|
|
shapes-to-select
|
|
(->> selected
|
|
(reduce
|
|
(fn [result shape-id]
|
|
(let [parent-id (dm/get-in objects [shape-id :parent-id])]
|
|
(if (and (some? parent-id) (not= parent-id uuid/zero))
|
|
(conj result parent-id)
|
|
(conj result shape-id))))
|
|
(d/ordered-set)))]
|
|
(rx/of (dws/select-shapes shapes-to-select))))))
|
|
|
|
;; --- Change Page Order (D&D Ordering)
|
|
|
|
(defn relocate-page
|
|
[id index]
|
|
(ptk/reify ::relocate-page
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [prev-index (-> (dsh/lookup-file-data state)
|
|
(get :pages)
|
|
(d/index-of id))
|
|
changes (-> (pcb/empty-changes it)
|
|
(pcb/move-page id index prev-index))]
|
|
(rx/of (dch/commit-changes changes))))))
|
|
|
|
;; --- Shape / Selection Alignment and Distribution
|
|
|
|
(defn can-align? [selected objects]
|
|
(cond
|
|
(empty? selected) false
|
|
(> (count selected) 1) true
|
|
:else
|
|
(not= uuid/zero (:parent-id (get objects (:id (first selected)))))))
|
|
|
|
(defn align-object-to-parent
|
|
[objects object-id axis]
|
|
(let [object (get objects object-id)
|
|
parent-id (:parent-id (get objects object-id))
|
|
parent (get objects parent-id)]
|
|
[(gal/align-to-parent object parent axis)]))
|
|
|
|
(defn align-objects-list
|
|
[objects selected axis]
|
|
(let [selected-objs (map #(get objects %) selected)
|
|
rect (gsh/shapes->rect selected-objs)]
|
|
(map #(gal/align-to-rect % rect axis) selected-objs)))
|
|
|
|
(defn align-objects
|
|
([axis]
|
|
(align-objects axis nil))
|
|
([axis selected]
|
|
(dm/assert!
|
|
"expected valid align axis value"
|
|
(contains? gal/valid-align-axis axis))
|
|
|
|
(ptk/reify ::align-objects
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
selected (or selected (dsh/lookup-selected state))
|
|
moved (if (= 1 (count selected))
|
|
(align-object-to-parent objects (first selected) axis)
|
|
(align-objects-list objects selected axis))
|
|
undo-id (js/Symbol)]
|
|
(when (can-align? selected objects)
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dwt/position-shapes moved)
|
|
(ptk/data-event :layout/update {:ids selected})
|
|
(dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
(defn can-distribute? [selected]
|
|
(cond
|
|
(empty? selected) false
|
|
(< (count selected) 3) false
|
|
:else true))
|
|
|
|
(defn distribute-objects
|
|
([axis]
|
|
(distribute-objects axis nil))
|
|
([axis ids]
|
|
(dm/assert!
|
|
"expected valid distribute axis value"
|
|
(contains? gal/valid-dist-axis axis))
|
|
|
|
(ptk/reify ::distribute-objects
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
selected (or ids (dsh/lookup-selected state))
|
|
moved (-> (map #(get objects %) selected)
|
|
(gal/distribute-space axis))
|
|
undo-id (js/Symbol)]
|
|
(when (can-distribute? selected)
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dwt/position-shapes moved)
|
|
(ptk/data-event :layout/update {:ids selected})
|
|
(dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
;; --- Shape Proportions
|
|
|
|
(defn set-shape-proportion-lock
|
|
[id lock]
|
|
(ptk/reify ::set-shape-proportion-lock
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(letfn [(assign-proportions [shape]
|
|
(if-not lock
|
|
(assoc shape :proportion-lock false)
|
|
(-> (assoc shape :proportion-lock true)
|
|
(gpp/assign-proportions))))]
|
|
(rx/of (dwsh/update-shapes [id] assign-proportions))))))
|
|
|
|
(defn toggle-proportion-lock
|
|
[]
|
|
(ptk/reify ::toggle-proportion-lock
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
selected (dsh/lookup-selected state)
|
|
selected-obj (-> (map #(get objects %) selected))
|
|
multi (attrs/get-attrs-multi selected-obj [:proportion-lock])
|
|
multi? (= :multiple (:proportion-lock multi))]
|
|
(if multi?
|
|
(rx/of (dwsh/update-shapes selected #(assoc % :proportion-lock true)))
|
|
(rx/of (dwsh/update-shapes selected #(update % :proportion-lock not))))))))
|
|
|
|
(defn workspace-focus-lost
|
|
[]
|
|
(ptk/reify ::workspace-focus-lost
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
;; FIXME: remove the `?` from show-distances?
|
|
(assoc-in state [:workspace-global :show-distances?] false))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Navigation
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn set-assets-section-open
|
|
[file-id section open?]
|
|
(ptk/reify ::set-assets-section-open
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-assets :open-status file-id section] open?))))
|
|
|
|
(defn clear-assets-section-open
|
|
[]
|
|
(ptk/reify ::clear-assets-section-open
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-assets :open-status] {}))))
|
|
|
|
|
|
(defn set-assets-group-open
|
|
[file-id section path open?]
|
|
(ptk/reify ::set-assets-group-open
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-assets :open-status file-id :groups section path] open?))))
|
|
|
|
(defn- check-in-asset
|
|
[items element]
|
|
(let [items (or items #{})]
|
|
(if (contains? items element)
|
|
(disj items element)
|
|
(conj items element))))
|
|
|
|
(defn toggle-selected-assets
|
|
[file-id asset-id type]
|
|
(ptk/reify ::toggle-selected-assets
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(update-in state [:workspace-assets :selected file-id type] check-in-asset asset-id))))
|
|
|
|
(defn select-single-asset
|
|
[file-id asset-id type]
|
|
(ptk/reify ::select-single-asset
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-assets :selected file-id type] #{asset-id}))))
|
|
|
|
(defn select-assets
|
|
[file-id assets-ids type]
|
|
(ptk/reify ::select-assets
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-assets :selected file-id type] (into #{} assets-ids)))))
|
|
|
|
(defn unselect-all-assets
|
|
([] (unselect-all-assets nil))
|
|
([file-id]
|
|
(ptk/reify ::unselect-all-assets
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if file-id
|
|
(update-in state [:workspace-assets :selected] dissoc file-id)
|
|
(update state :workspace-assets dissoc :selected))))))
|
|
|
|
(defn show-component-in-assets
|
|
[component-id]
|
|
|
|
(ptk/reify ::show-component-in-assets
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [file-id (:current-file-id state)
|
|
fdata (dsh/lookup-file-data state file-id)
|
|
component (cfv/get-primary-component fdata component-id)
|
|
cpath (:path component)
|
|
cpath (cpn/split-path cpath)
|
|
paths (map (fn [i] (cpn/join-path (take (inc i) cpath)))
|
|
(range (count cpath)))]
|
|
(rx/concat
|
|
(rx/from (map #(set-assets-group-open file-id :components % true) paths))
|
|
(rx/of (dcm/go-to-workspace :layout :assets)
|
|
(set-assets-section-open file-id :library true)
|
|
(set-assets-section-open file-id :components true)
|
|
(select-single-asset file-id (:id component) :components)))))
|
|
|
|
ptk/EffectEvent
|
|
(effect [_ state _]
|
|
(let [file-id (:current-file-id state)
|
|
fdata (dsh/lookup-file-data state file-id)
|
|
component (cfv/get-primary-component fdata component-id)
|
|
wrapper-id (str "component-shape-id-" (:id component))]
|
|
(tm/schedule-on-idle #(dom/scroll-into-view-if-needed! (dom/get-element wrapper-id)))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Context Menu
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn show-context-menu
|
|
[{:keys [position] :as params}]
|
|
(dm/assert! (gpt/point? position))
|
|
(ptk/reify ::show-context-menu
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-local :context-menu] params))))
|
|
|
|
(defn show-shape-context-menu
|
|
[{:keys [shape] :as params}]
|
|
(ptk/reify ::show-shape-context-menu
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)
|
|
objects (dsh/lookup-page-objects state)
|
|
all-selected (into [] (mapcat #(cfh/get-children-with-self objects %)) selected)
|
|
head (get objects (first selected))
|
|
|
|
not-group-like? (and (= (count selected) 1)
|
|
(not (contains? #{:group :bool} (:type head))))
|
|
|
|
no-bool-shapes? (->> all-selected (some (comp #{:frame :text} :type)))]
|
|
|
|
(if (and (some? shape) (not (contains? selected (:id shape))))
|
|
(rx/concat
|
|
(rx/of (dws/select-shape (:id shape)))
|
|
(rx/of (show-shape-context-menu params)))
|
|
(rx/of (show-context-menu
|
|
(-> params
|
|
(assoc
|
|
:kind :shape
|
|
:disable-booleans? (or no-bool-shapes? not-group-like?)
|
|
:disable-flatten? no-bool-shapes?
|
|
:selected (conj selected (:id shape)))))))))))
|
|
|
|
(defn show-page-item-context-menu
|
|
[{:keys [position page] :as params}]
|
|
(dm/assert! (gpt/point? position))
|
|
(ptk/reify ::show-page-item-context-menu
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (show-context-menu
|
|
(-> params (assoc :kind :page :selected (:id page))))))))
|
|
|
|
(defn show-track-context-menu
|
|
[{:keys [grid-id type index] :as params}]
|
|
(ptk/reify ::show-track-context-menu
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(rx/of (show-context-menu
|
|
(-> params (assoc :kind :grid-track
|
|
:grid-id grid-id
|
|
:type type
|
|
:index index)))))))
|
|
|
|
(defn show-grid-cell-context-menu
|
|
[{:keys [grid-id] :as params}]
|
|
(ptk/reify ::show-grid-cell-context-menu
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
grid (get objects grid-id)
|
|
cells (->> (get-in state [:workspace-grid-edition grid-id :selected])
|
|
(map #(get-in grid [:layout-grid-cells %])))]
|
|
(rx/of (show-context-menu
|
|
(-> params (assoc :kind :grid-cells
|
|
:grid grid
|
|
:cells cells))))))))
|
|
(def hide-context-menu
|
|
(ptk/reify ::hide-context-menu
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-local :context-menu] nil))))
|
|
|
|
(defn toggle-distances-display [value]
|
|
(ptk/reify ::toggle-distances-display
|
|
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-global :show-distances?] value))))
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Interactions
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(dm/export dwi/start-edit-interaction)
|
|
(dm/export dwi/move-edit-interaction)
|
|
(dm/export dwi/finish-edit-interaction)
|
|
(dm/export dwi/start-move-overlay-pos)
|
|
(dm/export dwi/move-overlay-pos)
|
|
(dm/export dwi/finish-move-overlay-pos)
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; CANVAS OPTIONS
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn change-canvas-color
|
|
([color]
|
|
(change-canvas-color nil color))
|
|
([page-id color]
|
|
(ptk/reify ::change-canvas-color
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (or page-id (:current-page-id state))
|
|
page (dsh/lookup-page state page-id)
|
|
changes (-> (pcb/empty-changes it)
|
|
(pcb/with-page page)
|
|
(pcb/mod-page {:background (:color color)}))]
|
|
(rx/of (dch/commit-changes changes)))))))
|
|
|
|
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Measurements
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn set-paddings-selected
|
|
[paddings-selected]
|
|
(ptk/reify ::set-paddings-selected
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-global :paddings-selected] paddings-selected))))
|
|
|
|
(defn set-gap-selected
|
|
[gap-selected]
|
|
(ptk/reify ::set-gap-selected
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-global :gap-selected] gap-selected))))
|
|
|
|
(defn set-margins-selected
|
|
[margins-selected]
|
|
(ptk/reify ::set-margins-selected
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-global :margins-selected] margins-selected))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Orphan Shapes
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn- find-orphan-shapes
|
|
([state]
|
|
(find-orphan-shapes state (:current-page-id state)))
|
|
([state page-id]
|
|
(let [objects (dsh/lookup-page-objects state page-id)
|
|
objects (filter (fn [item]
|
|
(and
|
|
(not= (key item) uuid/zero)
|
|
(not (contains? objects (:parent-id (val item))))))
|
|
objects)]
|
|
objects)))
|
|
|
|
(defn fix-orphan-shapes
|
|
[]
|
|
(ptk/reify ::fix-orphan-shapes
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [orphans (set (into [] (keys (find-orphan-shapes state))))]
|
|
(rx/of (dwsh/relocate-shapes orphans uuid/zero 0 true))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Sitemap
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn start-rename-page-item
|
|
[id]
|
|
(ptk/reify ::start-rename-page-item
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-local :page-item] id))))
|
|
|
|
(defn stop-rename-page-item
|
|
[]
|
|
(ptk/reify ::stop-rename-page-item
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(let [local (-> (:workspace-local state)
|
|
(dissoc :page-item))]
|
|
(assoc state :workspace-local local)))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Components annotations
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn update-component-annotation
|
|
"Update the component with the given annotation"
|
|
[id annotation]
|
|
(dm/assert! (uuid? id))
|
|
(dm/assert! (or (nil? annotation) (string? annotation)))
|
|
(ptk/reify ::update-component-annotation
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [data
|
|
(dsh/lookup-file-data state)
|
|
|
|
update-fn
|
|
(fn [component]
|
|
;; NOTE: we need to ensure the component exists,
|
|
;; because there are small possibilities of race
|
|
;; conditions with component deletion.
|
|
(when component
|
|
(if (nil? annotation)
|
|
(dissoc component :annotation)
|
|
(assoc component :annotation annotation))))
|
|
|
|
changes
|
|
(-> (pcb/empty-changes it)
|
|
(pcb/with-library-data data)
|
|
(pcb/update-component id update-fn))]
|
|
|
|
(rx/concat
|
|
(rx/of (dch/commit-changes changes))
|
|
(when (nil? annotation)
|
|
(rx/of (ptk/data-event ::ev/event {::ev/name "delete-component-annotation"}))))))))
|
|
|
|
(defn set-annotations-expanded
|
|
[expanded]
|
|
(ptk/reify ::set-annotations-expanded
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-annotations :expanded] expanded))))
|
|
|
|
(defn set-annotations-id-for-create
|
|
[id]
|
|
(ptk/reify ::set-annotations-id-for-create
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(if id
|
|
(-> (assoc-in state [:workspace-annotations :id-for-create] id)
|
|
(assoc-in [:workspace-annotations :expanded] true))
|
|
(d/dissoc-in state [:workspace-annotations :id-for-create])))
|
|
|
|
ptk/WatchEvent
|
|
(watch [_ _ _]
|
|
(when (some? id)
|
|
(rx/of (ptk/data-event ::ev/event {::ev/name "create-component-annotation"}))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Preview blend modes
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn set-preview-blend-mode
|
|
[ids blend-mode]
|
|
(ptk/reify ::set-preview-blend-mode
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(reduce #(assoc-in %1 [:workspace-preview-blend %2] blend-mode) state ids))))
|
|
|
|
(defn unset-preview-blend-mode
|
|
[ids]
|
|
(ptk/reify ::unset-preview-blend-mode
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(reduce #(update %1 :workspace-preview-blend dissoc %2) state ids))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Components
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn find-components-norefs
|
|
[]
|
|
(ptk/reify ::find-components-norefs
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [objects (dsh/lookup-page-objects state)
|
|
copies (->> objects
|
|
vals
|
|
(filter #(and (ctc/instance-head? %) (not (ctc/main-instance? %)))))
|
|
|
|
copies-no-ref (filter #(not (:shape-ref %)) copies)
|
|
find-childs-no-ref (fn [acc-map item]
|
|
(let [id (:id item)
|
|
childs (->> (cfh/get-children objects id)
|
|
(filter #(not (:shape-ref %))))]
|
|
(if (seq childs)
|
|
(assoc acc-map id childs)
|
|
acc-map)))
|
|
childs-no-ref (reduce
|
|
find-childs-no-ref
|
|
{}
|
|
copies)]
|
|
(js/console.log "Copies no ref" (count copies-no-ref) (clj->js copies-no-ref))
|
|
(js/console.log "Childs no ref" (count childs-no-ref) (clj->js childs-no-ref))))))
|
|
|
|
(defn set-clipboard-style
|
|
[style]
|
|
(ptk/reify ::set-clipboard-style
|
|
ptk/UpdateEvent
|
|
(update [_ state]
|
|
(assoc-in state [:workspace-global :clipboard-style] style))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Exports
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
;; Transform
|
|
(dm/export dwt/trigger-bounding-box-cloaking)
|
|
(dm/export dwt/start-resize)
|
|
(dm/export dwt/update-dimensions)
|
|
(dm/export dwt/change-orientation)
|
|
(dm/export dwt/start-rotate)
|
|
(dm/export dwt/increase-rotation)
|
|
(dm/export dwt/start-move-selected)
|
|
(dm/export dwt/move-selected)
|
|
(dm/export dwt/update-position)
|
|
(dm/export dwt/update-positions)
|
|
(dm/export dwt/flip-horizontal-selected)
|
|
(dm/export dwt/flip-vertical-selected)
|
|
(dm/export dwly/set-opacity)
|
|
|
|
;; Common
|
|
(dm/export dwsh/add-shape)
|
|
(dm/export dwe/clear-edition-mode)
|
|
(dm/export dws/select-shapes)
|
|
(dm/export dwe/start-edition-mode)
|
|
|
|
;; Clipboard
|
|
(dm/export dwcp/copy-selected)
|
|
(dm/export dwcp/paste-from-clipboard)
|
|
(dm/export dwcp/paste-from-event)
|
|
(dm/export dwcp/copy-selected-css)
|
|
(dm/export dwcp/copy-selected-css-nested)
|
|
(dm/export dwcp/copy-selected-props)
|
|
(dm/export dwcp/copy-selected-svg)
|
|
(dm/export dwcp/copy-selected-text)
|
|
(dm/export dwcp/paste-selected-props)
|
|
(dm/export dwcp/paste-shapes)
|
|
(dm/export dwcp/paste-data-valid?)
|
|
(dm/export dwcp/copy-link-to-clipboard)
|
|
(dm/export dwcp/copy-as-image)
|
|
|
|
;; Drawing
|
|
(dm/export dwd/select-for-drawing)
|
|
|
|
;; Selection
|
|
(dm/export dws/toggle-focus-mode)
|
|
(dm/export dws/deselect-all)
|
|
(dm/export dws/deselect-shape)
|
|
(dm/export dws/duplicate-selected)
|
|
(dm/export dws/handle-area-selection)
|
|
(dm/export dws/select-all)
|
|
(dm/export dws/select-inside-group)
|
|
(dm/export dws/select-shape)
|
|
(dm/export dws/select-prev-shape)
|
|
(dm/export dws/select-next-shape)
|
|
(dm/export dws/shift-select-shapes)
|
|
|
|
;; Highlight
|
|
(dm/export dwh/highlight-shape)
|
|
(dm/export dwh/dehighlight-shape)
|
|
|
|
;; Shape flags
|
|
(dm/export dwsh/update-shape-flags)
|
|
(dm/export dwsh/toggle-visibility-selected)
|
|
(dm/export dwsh/toggle-lock-selected)
|
|
(dm/export dwsh/toggle-file-thumbnail-selected)
|
|
|
|
;; Groups
|
|
(dm/export dwg/mask-group)
|
|
(dm/export dwg/unmask-group)
|
|
(dm/export dwg/group-selected)
|
|
(dm/export dwg/ungroup-selected)
|
|
|
|
;; Boolean
|
|
(dm/export dwb/create-bool)
|
|
(dm/export dwb/group-to-bool)
|
|
(dm/export dwb/bool-to-group)
|
|
(dm/export dwb/change-bool-type)
|
|
|
|
;; Shapes to path
|
|
(dm/export dwps/convert-selected-to-path)
|
|
(dm/export dwps/convert-selected-strokes-to-path)
|
|
|
|
;; Guides
|
|
(dm/export dwgu/update-guides)
|
|
(dm/export dwgu/remove-guide)
|
|
(dm/export dwgu/set-hover-guide)
|
|
|
|
;; Zoom
|
|
(dm/export dwz/reset-zoom)
|
|
(dm/export dwz/zoom-to-selected-shape)
|
|
(dm/export dwz/start-zooming)
|
|
(dm/export dwz/finish-zooming)
|
|
(dm/export dwz/zoom-to-fit-all)
|
|
(dm/export dwz/decrease-zoom)
|
|
(dm/export dwz/increase-zoom)
|
|
(dm/export dwz/set-zoom)
|
|
|
|
;; Thumbnails
|
|
(dm/export dwth/update-thumbnail)
|
|
|
|
;; Viewport
|
|
(dm/export dwv/initialize-viewport)
|
|
(dm/export dwv/update-viewport-position)
|
|
(dm/export dwv/update-viewport-size)
|
|
(dm/export dwv/start-panning)
|
|
(dm/export dwv/finish-panning)
|
|
|
|
;; Undo
|
|
(dm/export dwu/reinitialize-undo)
|
|
|
|
;; Pages
|
|
(dm/export dwpg/initialize-page)
|
|
(dm/export dwpg/finalize-page)
|
|
(dm/export dwpg/create-page)
|
|
(dm/export dwpg/duplicate-page)
|
|
(dm/export dwpg/rename-page)
|
|
(dm/export dwpg/delete-page)
|