mirror of
https://github.com/penpot/penpot.git
synced 2026-05-10 18:48:23 +00:00
* 🐛 Add missing event add-component-to-variant * 🐛 Fix event apply-tokens, param applied-to-variant * 🐛 Fix missing case on event "add new variant" * 🐛 Fix event combine-as-variants * 🐛 Fix event variant-edit-property-name * 🐛 On variants events, change trigger for origin * 🐛 Split combine-as-variants to not have an optional first parameter
456 lines
18 KiB
Clojure
456 lines
18 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.shapes
|
|
(:require
|
|
[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.shapes-helpers :as cfsh]
|
|
[app.common.logic.shapes :as cls]
|
|
[app.common.schema :as sm]
|
|
[app.common.types.component :as ctc]
|
|
[app.common.types.container :as ctn]
|
|
[app.common.types.shape :as cts]
|
|
[app.common.types.shape-tree :as ctst]
|
|
[app.main.data.changes :as dch]
|
|
[app.main.data.comments :as dc]
|
|
[app.main.data.event :as ev]
|
|
[app.main.data.helpers :as dsh]
|
|
[app.main.data.workspace.collapse :as dwco]
|
|
[app.main.data.workspace.edition :as dwe]
|
|
[app.main.data.workspace.selection :as dws]
|
|
[app.main.data.workspace.undo :as dwu]
|
|
[beicon.v2.core :as rx]
|
|
[potok.v2.core :as ptk]))
|
|
|
|
(def ^:private update-layout-attr? #{:hidden})
|
|
|
|
(defn- add-undo-group
|
|
[changes state]
|
|
(let [undo (:workspace-undo state)
|
|
items (:items undo)
|
|
index (or (:index undo) (dec (count items)))
|
|
prev-item (when-not (or (empty? items) (= index -1))
|
|
(get items index))
|
|
undo-group (:undo-group prev-item)
|
|
add-undo-group? (and
|
|
(not (nil? undo-group))
|
|
(= (get-in changes [:redo-changes 0 :type]) :mod-obj)
|
|
(= (get-in prev-item [:redo-changes 0 :type]) :add-obj)
|
|
(contains? (:tags prev-item) :alt-duplication))] ;; This is a copy-and-move with mouse+alt
|
|
|
|
(cond-> changes add-undo-group? (assoc :undo-group undo-group))))
|
|
|
|
(defn update-shapes
|
|
([ids update-fn] (update-shapes ids update-fn nil))
|
|
([ids update-fn
|
|
{:keys [reg-objects? save-undo? stack-undo? attrs ignore-tree page-id
|
|
ignore-touched undo-group with-objects? changed-sub-attr]
|
|
:or {reg-objects? false
|
|
save-undo? true
|
|
stack-undo? false
|
|
ignore-touched false
|
|
with-objects? false}}]
|
|
|
|
(assert (every? uuid? ids) "expect a coll of uuid for `ids`")
|
|
(assert (fn? update-fn) "the `update-fn` should be a valid function")
|
|
|
|
(ptk/reify ::update-shapes
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (or page-id (get state :current-page-id))
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
ids (into [] (filter some?) ids)
|
|
|
|
xf-update-layout
|
|
(comp
|
|
(map (d/getf objects))
|
|
(filter #(some update-layout-attr? (pcb/changed-attrs % objects update-fn {:attrs attrs :with-objects? with-objects?})))
|
|
(map :id))
|
|
|
|
update-layout-ids
|
|
(->> (into [] xf-update-layout ids)
|
|
(not-empty))
|
|
|
|
changes
|
|
(-> (pcb/empty-changes it page-id)
|
|
(pcb/set-save-undo? save-undo?)
|
|
(pcb/set-stack-undo? stack-undo?)
|
|
(cls/generate-update-shapes ids
|
|
update-fn
|
|
objects
|
|
{:attrs attrs
|
|
:changed-sub-attr changed-sub-attr
|
|
:ignore-tree ignore-tree
|
|
:ignore-touched ignore-touched
|
|
:with-objects? with-objects?})
|
|
(cond-> undo-group
|
|
(pcb/set-undo-group undo-group)))
|
|
|
|
changes
|
|
(add-undo-group changes state)]
|
|
|
|
(rx/concat
|
|
(if (seq (:redo-changes changes))
|
|
(let [changes (cond-> changes reg-objects? (pcb/resize-parents ids))]
|
|
(rx/of (dch/commit-changes changes)))
|
|
(rx/empty))
|
|
|
|
;; Update layouts for properties marked
|
|
(if update-layout-ids
|
|
(rx/of (ptk/data-event :layout/update {:ids update-layout-ids}))
|
|
(rx/empty))))))))
|
|
|
|
(defn add-shape
|
|
([shape]
|
|
(add-shape shape {}))
|
|
([shape {:keys [no-select? no-update-layout?]}]
|
|
|
|
(cts/check-shape shape)
|
|
|
|
(ptk/reify ::add-shape
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
|
|
[shape changes]
|
|
(-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects)
|
|
(cfsh/prepare-add-shape shape objects))
|
|
|
|
changes
|
|
(cond-> changes
|
|
(cfh/text-shape? shape)
|
|
(pcb/set-undo-group (:id shape)))
|
|
|
|
undo-id
|
|
(js/Symbol)
|
|
|
|
parent-type
|
|
(cfh/get-shape-type objects (:parent-id shape))]
|
|
|
|
(rx/concat
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(when-not no-update-layout?
|
|
(ptk/data-event :layout/update {:ids [(:parent-id shape)]}))
|
|
(when-not no-select?
|
|
(dws/select-shapes (d/ordered-set (:id shape))))
|
|
(dwu/commit-undo-transaction undo-id))
|
|
(when (cfh/text-shape? shape)
|
|
(->> (rx/of (dwe/start-edition-mode (:id shape)))
|
|
(rx/observe-on :async)))
|
|
|
|
(rx/of (ev/event {::ev/name "create-shape"
|
|
::ev/origin "workspace:add-shape"
|
|
:type (get shape :type)
|
|
:parent-type parent-type}))
|
|
|
|
(when (cfh/has-layout? objects (:parent-id shape))
|
|
(rx/of (ev/event {::ev/name "layout-add-element"
|
|
::ev/origin "workspace:add-shape"
|
|
:type (get shape :type)
|
|
:parent-type parent-type})))))))))
|
|
|
|
(defn move-shapes-into-frame
|
|
[frame-id shapes]
|
|
(ptk/reify ::move-shapes-into-frame
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
shapes (->> shapes
|
|
(remove #(dm/get-in objects [% :blocked]))
|
|
(cfh/order-by-indexed-shapes objects))
|
|
|
|
changes (-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects))
|
|
|
|
changes (cfsh/prepare-move-shapes-into-frame changes frame-id shapes objects true)]
|
|
|
|
(if (some? changes)
|
|
(rx/of (dch/commit-changes changes))
|
|
(rx/empty))))))
|
|
|
|
(declare update-shape-flags)
|
|
|
|
(defn delete-shapes
|
|
([ids] (delete-shapes nil ids {}))
|
|
([page-id ids] (delete-shapes page-id ids {}))
|
|
([page-id ids options]
|
|
(assert (sm/check-set-of-uuid ids))
|
|
|
|
(ptk/reify ::delete-shapes
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [file-id (:current-file-id state)
|
|
page-id (or page-id (:current-page-id state))
|
|
|
|
fdata (dsh/lookup-file-data state file-id)
|
|
page (dsh/get-page fdata page-id)
|
|
objects (:objects page)
|
|
|
|
undo-id (or (:undo-id options) (js/Symbol))
|
|
[all-parents changes] (-> (pcb/empty-changes it (:id page))
|
|
(cls/generate-delete-shapes fdata page objects ids
|
|
{:ignore-touched (:allow-altering-copies options)
|
|
:undo-group (:undo-group options)
|
|
:undo-id undo-id}))]
|
|
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dc/detach-comment-thread ids)
|
|
(dch/commit-changes changes)
|
|
(ptk/data-event :layout/update {:ids all-parents :undo-group (:undo-group options)})
|
|
(dwu/commit-undo-transaction undo-id)))))))
|
|
|
|
(defn create-and-add-shape
|
|
[type frame-x frame-y {:keys [width height] :as attrs}]
|
|
(ptk/reify ::create-and-add-shape
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [vbc (dsh/get-viewport-center state)
|
|
x (:x attrs (- (:x vbc) (/ width 2)))
|
|
y (:y attrs (- (:y vbc) (/ height 2)))
|
|
page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
frame-id (-> (dsh/lookup-page-objects state page-id)
|
|
(ctst/top-nested-frame {:x frame-x :y frame-y}))
|
|
|
|
selected (dsh/lookup-selected state)
|
|
base (cfh/get-base-shape objects selected)
|
|
|
|
parent-id (if (or (and (= 1 (count selected))
|
|
(cfh/frame-shape? (get objects (first selected))))
|
|
(empty? selected))
|
|
frame-id
|
|
(:parent-id base))
|
|
|
|
;; If the parent-id or the frame-id are component-copies, we need to get the first not copy parent
|
|
parent-id (:id (ctn/get-first-valid-parent objects parent-id)) ;; We don't want to change the structure of component copies
|
|
frame-id (:id (ctn/get-first-valid-parent objects frame-id))
|
|
|
|
shape (cts/setup-shape
|
|
(-> attrs
|
|
(assoc :type type)
|
|
(assoc :x x)
|
|
(assoc :y y)
|
|
(assoc :frame-id frame-id)
|
|
(assoc :parent-id parent-id)))]
|
|
|
|
(rx/of (add-shape shape))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Artboard
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn create-artboard-from-shapes
|
|
([shapes id parent-id index name delta]
|
|
(create-artboard-from-shapes shapes id parent-id index name delta true))
|
|
([shapes id parent-id index name delta layout-update?]
|
|
(ptk/reify ::create-artboard-from-shapes
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
|
|
changes (-> (pcb/empty-changes it page-id)
|
|
(pcb/with-objects objects))
|
|
|
|
[frame-shape changes]
|
|
(cfsh/prepare-create-artboard-from-selection changes
|
|
id
|
|
parent-id
|
|
objects
|
|
shapes
|
|
index
|
|
name
|
|
false
|
|
nil
|
|
delta)
|
|
|
|
undo-id (js/Symbol)]
|
|
|
|
(when changes
|
|
(rx/of
|
|
(dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(dws/select-shapes (d/ordered-set (:id frame-shape)))
|
|
(when layout-update? (ptk/data-event :layout/update {:ids [(:id frame-shape)]}))
|
|
(ev/event {::ev/name "create-board"
|
|
:converted-from (cfh/get-selected-type objects shapes)
|
|
:parent-type (cfh/get-shape-type objects (:parent-id frame-shape))})
|
|
(dwu/commit-undo-transaction undo-id))))))))
|
|
|
|
(defn create-artboard-from-selection
|
|
([]
|
|
(create-artboard-from-selection nil))
|
|
([id]
|
|
(create-artboard-from-selection id nil))
|
|
([id parent-id]
|
|
(create-artboard-from-selection id parent-id nil))
|
|
([id parent-id index]
|
|
(create-artboard-from-selection id parent-id index nil))
|
|
([id parent-id index name]
|
|
(create-artboard-from-selection id parent-id index name nil))
|
|
([id parent-id index name delta]
|
|
(ptk/reify ::create-artboard-from-selection
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
selected (->> (dsh/lookup-selected state)
|
|
(cfh/clean-loops objects)
|
|
(remove #(ctn/has-any-copy-parent? objects (get objects %)))
|
|
(remove #(->> %
|
|
(get objects)
|
|
(ctc/is-variant?))))]
|
|
|
|
(rx/of (create-artboard-from-shapes selected id parent-id index name delta)))))))
|
|
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
;; Shape Flags
|
|
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
|
|
|
|
(defn update-shape-flags
|
|
[ids flags]
|
|
(assert (every? uuid? ids)
|
|
"expected valid coll of uuids")
|
|
|
|
(let [{:keys [blocked hidden undo-group]}
|
|
(cts/check-shape-generic-attrs flags)]
|
|
|
|
(ptk/reify ::update-shape-flags
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [update-fn
|
|
(fn [obj]
|
|
(cond-> obj
|
|
(boolean? blocked) (assoc :blocked blocked)
|
|
(boolean? hidden) (assoc :hidden hidden)))
|
|
objects (dsh/lookup-page-objects state)
|
|
;; We have change only the hidden behaviour, to hide only the
|
|
;; selected shape, block behaviour remains the same.
|
|
ids (if (boolean? blocked)
|
|
(into ids (->> ids (mapcat #(cfh/get-children-ids objects %))))
|
|
ids)]
|
|
(rx/of (update-shapes ids update-fn {:attrs #{:blocked :hidden} :undo-group undo-group})))))))
|
|
|
|
(defn toggle-visibility-selected
|
|
[]
|
|
(ptk/reify ::toggle-visibility-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)]
|
|
(rx/of (update-shapes selected #(update % :hidden not)))))))
|
|
|
|
(defn toggle-lock-selected
|
|
[]
|
|
(ptk/reify ::toggle-lock-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)]
|
|
(rx/of (update-shapes selected #(update % :blocked not)))))))
|
|
|
|
|
|
;; FIXME: this need to be refactored
|
|
|
|
|
|
(defn toggle-file-thumbnail-selected
|
|
[]
|
|
(ptk/reify ::toggle-file-thumbnail-selected
|
|
ptk/WatchEvent
|
|
(watch [_ state _]
|
|
(let [selected (dsh/lookup-selected state)
|
|
fdata (dsh/lookup-file-data state)
|
|
pages (-> fdata :pages-index vals)
|
|
undo-id (js/Symbol)]
|
|
|
|
(rx/concat
|
|
(rx/of (dwu/start-undo-transaction undo-id))
|
|
;; First: clear the `:use-for-thumbnail` flag from all not
|
|
;; selected frames.
|
|
(rx/from
|
|
(->> pages
|
|
(mapcat
|
|
(fn [{:keys [objects id] :as page}]
|
|
(->> (ctst/get-frames objects)
|
|
(sequence
|
|
(comp (filter :use-for-thumbnail)
|
|
(map :id)
|
|
(remove selected)
|
|
(map (partial vector id)))))))
|
|
(d/group-by first second)
|
|
(map (fn [[page-id frame-ids]]
|
|
(update-shapes frame-ids #(dissoc % :use-for-thumbnail) {:page-id page-id})))))
|
|
|
|
;; And finally: toggle the flag value on all the selected shapes
|
|
(rx/of (update-shapes selected #(update % :use-for-thumbnail not))
|
|
(dwu/commit-undo-transaction undo-id)))))))
|
|
|
|
|
|
;; --- Change Shape Order (D&D Ordering)
|
|
|
|
|
|
(defn relocate-shapes
|
|
[ids parent-id to-index & [ignore-parents?]]
|
|
(dm/assert! (every? uuid? ids))
|
|
(dm/assert! (set? ids))
|
|
(dm/assert! (uuid? parent-id))
|
|
(dm/assert! (number? to-index))
|
|
|
|
(ptk/reify ::relocate-shapes
|
|
ptk/WatchEvent
|
|
(watch [it state _]
|
|
(let [page-id (:current-page-id state)
|
|
objects (dsh/lookup-page-objects state page-id)
|
|
data (dsh/lookup-file-data state)
|
|
|
|
;; Ignore any shape whose parent is also intended to be moved
|
|
ids (cfh/clean-loops objects ids)
|
|
|
|
;; If we try to move a parent into a child we remove it
|
|
ids (filter #(not (cfh/is-parent? objects parent-id %)) ids)
|
|
|
|
all-parents (into #{parent-id} (map #(cfh/get-parent-id objects %)) ids)
|
|
|
|
changes (-> (pcb/empty-changes it)
|
|
(pcb/with-page-id page-id)
|
|
(pcb/with-objects objects)
|
|
(pcb/with-library-data data)
|
|
(cls/generate-relocate
|
|
parent-id
|
|
to-index
|
|
ids
|
|
:ignore-parents? ignore-parents?))
|
|
|
|
add-component-to-variant? (and
|
|
;; Any of the shapes is a head
|
|
(some (comp ctc/instance-head? objects) ids)
|
|
;; Any ancestor of the destination parent is a variant
|
|
(->> (cfh/get-parents-with-self objects parent-id)
|
|
(some ctc/is-variant?)))
|
|
|
|
add-new-variant? (and
|
|
;; The parent is a variant container
|
|
(-> parent-id objects ctc/is-variant-container?)
|
|
;; Any of the shapes is a main instance
|
|
(some (comp ctc/main-instance? objects) ids))
|
|
|
|
undo-id (js/Symbol)]
|
|
|
|
(rx/of (dwu/start-undo-transaction undo-id)
|
|
(dch/commit-changes changes)
|
|
(dwco/expand-collapse parent-id)
|
|
(ptk/data-event :layout/update {:ids (concat all-parents ids)})
|
|
(dwu/commit-undo-transaction undo-id)
|
|
(when add-component-to-variant?
|
|
(ev/event {::ev/name "add-component-to-variant"}))
|
|
(when add-new-variant?
|
|
(ev/event {::ev/name "add-new-variant" ::ev/origin "workspace:move-shapes-in-layers-tab"})))))))
|