0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-01-09 08:20:45 -05:00

Merge pull request #148 from uxbox/us/182/workspace-copy-paste

Copy&Paste preview
This commit is contained in:
Andrey Antukh 2020-03-25 09:38:51 +01:00 committed by GitHub
commit 0c4c50270c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 248 additions and 155 deletions

View file

@ -215,11 +215,14 @@
(defmethod process-change :del-obj
[data {:keys [id] :as change}]
(when-let [{:keys [frame-id] :as obj} (get-in data [:objects id])]
(-> data
(update :objects dissoc id)
(update-in [:objects frame-id :shapes]
(fn [s] (filterv #(not= % id) s))))))
(when-let [{:keys [frame-id shapes] :as obj} (get-in data [:objects id])]
(let [data (update data :objects dissoc id)]
(cond-> data
(contains? (:objects data) frame-id)
(update-in [:objects frame-id :shapes] (fn [s] (filterv #(not= % id) s)))
(seq shapes) ; Recursive delete all dependend objects
(as-> $ (reduce #(process-change %1 {:type :del-obj :id %2}) $ shapes))))))
(defmethod process-operation :set
[shape op]

View file

@ -5,7 +5,7 @@
;; This Source Code Form is "Incompatible With Secondary Licenses", as
;; defined by the Mozilla Public License, v. 2.0.
;;
;; Copyright (c) 2015-2020 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2020 UXBOX Labs SL
(ns ^:figwheel-hooks uxbox.main
(:require

View file

@ -11,6 +11,8 @@
(:require
[clojure.set :as set]
[beicon.core :as rx]
[goog.object :as gobj]
[goog.events :as events]
[cljs.spec.alpha :as s]
[potok.core :as ptk]
[uxbox.common.data :as d]
@ -36,7 +38,11 @@
[uxbox.util.time :as dt]
[uxbox.util.transit :as t]
[uxbox.util.uuid :as uuid]
[vendor.randomcolor]))
[uxbox.util.webapi :as wapi]
[vendor.randomcolor])
(:import goog.events.EventType
goog.events.KeyCodes
goog.ui.KeyboardShortcutHandler))
;; TODO: temporal workaround
(def clear-ruler nil)
@ -358,6 +364,63 @@
(let [local (:workspace-local state)]
(assoc-in state [:workspace-cache page-id] local)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Persistence
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare persist-changes)
(declare diff-and-commit-changes)
(defn initialize-page-persistence
[page-id]
(ptk/reify ::initialize-persistence
ptk/UpdateEvent
(update [_ state]
(assoc state ::page-id page-id))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter #(or (ptk/type? ::finalize %)
(ptk/type? ::initialize-page %))
stream)
notifier (->> stream
(rx/filter (ptk/type? ::commit-changes))
(rx/debounce 2000)
(rx/merge stoper))]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::commit-changes))
(rx/map deref)
(rx/buffer-until notifier)
(rx/map vec)
(rx/filter (complement empty?))
(rx/map #(persist-changes page-id %))
(rx/take-until (rx/delay 100 stoper)))
(->> stream
(rx/filter #(satisfies? IBatchedChange %))
(rx/debounce 200)
(rx/map (fn [_] (diff-and-commit-changes page-id)))
(rx/take-until stoper)))))))
(defn persist-changes
[page-id changes]
(ptk/reify ::persist-changes
ptk/WatchEvent
(watch [_ state stream]
(let [session-id (:session-id state)
page (get-in state [:pages page-id])
changes (->> changes
(mapcat identity)
(map #(assoc % :session-id session-id))
(vec))
params {:id (:id page)
:revn (:revn page)
:changes changes}]
(->> (rp/mutation :update-page params)
(rx/map shapes-changes-commited))))))
(defn- generate-operations
[ma mb]
(let [ma-keys (set (keys ma))
@ -410,60 +473,6 @@
(when-not (empty? changes)
(rx/of (commit-changes changes undo-changes)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Persistence
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(declare persist-changes)
(defn initialize-page-persistence
[page-id]
(ptk/reify ::initialize-persistence
ptk/UpdateEvent
(update [_ state]
(assoc state ::page-id page-id))
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter #(or (ptk/type? ::finalize %)
(ptk/type? ::initialize-page %))
stream)
notifier (->> stream
(rx/filter (ptk/type? ::commit-changes))
(rx/debounce 2000)
(rx/merge stoper))]
(rx/merge
(->> stream
(rx/filter (ptk/type? ::commit-changes))
(rx/map deref)
(rx/buffer-until notifier)
(rx/map vec)
(rx/filter (complement empty?))
(rx/map #(persist-changes page-id %))
(rx/take-until (rx/delay 100 stoper)))
(->> stream
(rx/filter #(satisfies? IBatchedChange %))
(rx/debounce 200)
(rx/map (fn [_] (diff-and-commit-changes page-id)))
(rx/take-until stoper)))))))
(defn persist-changes
[page-id changes]
(ptk/reify ::persist-changes
ptk/WatchEvent
(watch [_ state stream]
(let [session-id (:session-id state)
page (get-in state [:pages page-id])
changes (->> changes
(mapcat identity)
(map #(assoc % :session-id session-id))
(vec))
params {:id (:id page)
:revn (:revn page)
:changes changes}]
(->> (rp/mutation :update-page params)
(rx/map shapes-changes-commited))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Data Fetching & Uploading
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -896,7 +905,7 @@
in the current workspace page."
[state]
(let [page-id (::page-id state)
objects (get-in state [:workspace-page page-id :objects])]
objects (get-in state [:workspace-data page-id :objects])]
(into #{} (map :name) (vals objects))))
(defn impl-generate-unique-name
@ -1043,14 +1052,12 @@
{:type :add-obj
:id (:id obj)
:frame-id frame-id
:obj (assoc obj :frame-id frame-id)
:session-id (:session-id state)}))
:obj (assoc obj :frame-id frame-id)}))
(:shapes frame))
uchanges (mapv (fn [rch]
{:type :del-obj
:id (:id rch)
:session-id (:session-id state)})
:id (:id rch)})
rchanges)
shapes (mapv :id rchanges)
@ -1068,7 +1075,7 @@
:id frame-id
:session-id (:session-id state)}]
(rx/of (commit-changes (d/concat [rchange] rchanges)
(d/concat [uchange] uchanges)
(d/concat [] uchanges [uchange])
{:commit-local? true}))))))
@ -1096,7 +1103,6 @@
(rx/empty))))))
;; --- Toggle shape's selection status (selected or deselected)
(defn select-shape
@ -1979,6 +1985,95 @@
(assoc-in state [:workspace-local :context-menu] nil))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Clipboard
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def copy-selected
(letfn [(prepare-selected [state selected]
(let [data (reduce #(prepare %1 state %2) {} selected)]
{:type :copied-shapes
:data (assoc data :selected selected)}))
(prepare [result state id]
(let [page-id (::page-id state)
objects (get-in state [:workspace-data page-id :objects])
object (get objects id)]
(cond-> (assoc-in result [:objects id] object)
(= :frame (:type object))
(as-> $ (reduce #(prepare %1 state %2) $ (:shapes object))))))
(on-copy-error [error]
(js/console.error "Clipboard blocked:" error)
(rx/empty))]
(ptk/reify ::copy-selected
ptk/WatchEvent
(watch [_ state stream]
(let [selected (get-in state [:workspace-local :selected])
cdata (prepare-selected state selected)]
(->> (rx/from (wapi/write-to-clipboard cdata))
(rx/catch on-copy-error)
(rx/ignore)))))))
(defn- paste-impl
[{:keys [selected objects] :as data}]
(letfn [(prepare-change [id]
(let [obj (get objects id)]
;; (prn "prepare-change" id obj)
(if (= :frame (:type obj))
(prepare-frame-change obj)
(prepare-shape-change obj uuid/zero))))
(prepare-shape-change [obj frame-id]
(let [id (uuid/next)]
{:type :add-obj
:id id
:frame-id frame-id
:obj (assoc obj :id id :frame-id frame-id)}))
(prepare-frame-change [obj]
(let [frame-id (uuid/next)
sch (->> (map #(get objects %) (:shapes obj))
(map #(prepare-shape-change % frame-id)))
fch {:type :add-obj
:id frame-id
:frame-id uuid/zero
:obj (-> obj
(assoc :id frame-id)
(assoc :frame-id uuid/zero)
(assoc :shapes (mapv :id sch)))}]
(d/concat [fch] sch)))]
(ptk/reify ::paste-impl
ptk/WatchEvent
(watch [_ state stream]
(let [rchanges (->> (map prepare-change selected)
(flatten))
uchanges (map (fn [ch]
{:type :del-obj
:id (:id ch)})
rchanges)]
(cljs.pprint/pprint rchanges)
(rx/of (commit-changes (vec rchanges)
(vec (reverse uchanges))
{:commit-local? true})))))))
(def paste
(ptk/reify ::paste
ptk/WatchEvent
(watch [_ state stream]
(->> (rx/from (wapi/read-from-clipboard))
(rx/filter #(= :copied-shapes (:type %)))
(rx/pr-log "pasting:")
(rx/map :data)
(rx/map paste-impl)
(rx/catch (fn [err]
(js/console.error "Clipboard blocked:" err)
(rx/empty)))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Page Changes Reactions
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
@ -1998,3 +2093,63 @@
pages (vec (concat before [id] after))]
(assoc-in state [:projects (:project-id page) :pages] pages)))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Shortcuts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(def shortcuts
{"ctrl+shift+m" #(rx/of (toggle-layout-flag :sitemap))
"ctrl+shift+f" #(rx/of (toggle-layout-flag :drawtools))
"ctrl+shift+i" #(rx/of (toggle-layout-flag :icons))
"ctrl+shift+l" #(rx/of (toggle-layout-flag :layers))
"ctrl+0" #(rx/of (reset-zoom))
"ctrl+d" #(rx/of duplicate-selected)
"ctrl+z" #(rx/of undo)
"ctrl+shift+z" #(rx/of redo)
"ctrl+y" #(rx/of redo)
"ctrl+q" #(rx/of reinitialize-undo)
"ctrl+b" #(rx/of (select-for-drawing :rect))
"ctrl+e" #(rx/of (select-for-drawing :circle))
"ctrl+t" #(rx/of (select-for-drawing :text))
"ctrl+c" #(rx/of copy-selected)
"ctrl+v" #(rx/of paste)
"esc" #(rx/of :interrupt deselect-all)
"delete" #(rx/of delete-selected)
"ctrl+up" #(rx/of (vertical-order-selected :up))
"ctrl+down" #(rx/of (vertical-order-selected :down))
"ctrl+shift+up" #(rx/of (vertical-order-selected :top))
"ctrl+shift+down" #(rx/of (vertical-order-selected :bottom))
"shift+up" #(rx/of (move-selected :up true))
"shift+down" #(rx/of (move-selected :down true))
"shift+right" #(rx/of (move-selected :right true))
"shift+left" #(rx/of (move-selected :left true))
"up" #(rx/of (move-selected :up false))
"down" #(rx/of (move-selected :down false))
"right" #(rx/of (move-selected :right false))
"left" #(rx/of (move-selected :left false))})
(def initialize-shortcuts
(letfn [(initialize [sink]
(let [handler (KeyboardShortcutHandler. js/document)]
;; Register shortcuts.
(run! #(.registerShortcut handler % %) (keys shortcuts))
;; Initialize shortcut listener.
(let [event KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED
callback #(sink (gobj/get % "identifier"))
key (events/listen handler event callback)]
(fn []
(events/unlistenByKey key)
(.clearKeyListener handler)))))]
(ptk/reify ::initialize-shortcuts
ptk/WatchEvent
(watch [_ state stream]
(let [stoper (rx/filter #(= ::finalize-shortcuts %) stream)]
(->> (rx/create initialize)
(rx/pr-log "[debug]: shortcut:")
(rx/map #(get shortcuts %))
(rx/filter fn?)
(rx/merge-map (fn [f] (f)))
(rx/take-until stoper)))))))

View file

@ -133,7 +133,6 @@
translate #(translate-to-frame % ds-modifier (gpt/point (- x) (- y)))
]
[:svg {:x x :y y :width width :height height}
[:& "rect" props]
(for [item (reverse childs)]

View file

@ -26,7 +26,6 @@
[uxbox.main.ui.workspace.header :refer [header]]
[uxbox.main.ui.workspace.rules :refer [horizontal-rule vertical-rule]]
[uxbox.main.ui.workspace.scroll :as scroll]
[uxbox.main.ui.workspace.shortcuts :as shortcuts]
[uxbox.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]]
[uxbox.main.ui.workspace.sidebar.history :refer [history-dialog]]
[uxbox.main.ui.workspace.left-toolbar :refer [left-toolbar]]
@ -119,10 +118,11 @@
{:fn #(st/emit! dw/initialize-layout)})
(mf/use-effect
{:deps (mf/deps file-id page-id)
{:deps (mf/deps file-id)
:fn (fn []
(let [sub (shortcuts/init)]
#(rx/cancel! sub)))})
(st/emit! dw/initialize-shortcuts)
#(st/emit! ::dw/finalize-shortcuts))})
(let [file (mf/deref refs/workspace-file)
page (mf/deref refs/workspace-page)
layout (mf/deref refs/workspace-layout)]

View file

@ -1,81 +0,0 @@
;; 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) 2015-2016 Andrey Antukh <niwi@niwi.nz>
;; Copyright (c) 2015-2016 Juan de la Cruz <delacruzgarciajuan@gmail.com>
(ns uxbox.main.ui.workspace.shortcuts
(:require [goog.events :as events]
[beicon.core :as rx]
[potok.core :as ptk]
[uxbox.main.store :as st]
[uxbox.main.data.lightbox :as dl]
[uxbox.main.data.workspace :as dw])
(:import goog.events.EventType
goog.events.KeyCodes
goog.ui.KeyboardShortcutHandler))
(declare move-selected)
;; --- Shortcuts
(defonce +shortcuts+
{
;; :shift+g #(st/emit! (dw/toggle-flag :grid))
:ctrl+shift+m #(st/emit! (dw/toggle-layout-flag :sitemap))
:ctrl+shift+f #(st/emit! (dw/toggle-layout-flag :drawtools))
:ctrl+shift+i #(st/emit! (dw/toggle-layout-flag :icons))
:ctrl+shift+l #(st/emit! (dw/toggle-layout-flag :layers))
:ctrl+0 #(st/emit! (dw/reset-zoom))
;; :ctrl+r #(st/emit! (dw/toggle-flag :ruler))
:ctrl+d #(st/emit! dw/duplicate-selected)
:ctrl+z #(st/emit! dw/undo)
:ctrl+shift+z #(st/emit! dw/redo)
:ctrl+y #(st/emit! dw/redo)
:ctrl+q #(st/emit! dw/reinitialize-undo)
:ctrl+b #(st/emit! (dw/select-for-drawing :rect))
:ctrl+e #(st/emit! (dw/select-for-drawing :circle))
:ctrl+t #(st/emit! (dw/select-for-drawing :text))
:esc #(st/emit! :interrupt dw/deselect-all)
:delete #(st/emit! dw/delete-selected)
:ctrl+up #(st/emit! (dw/vertical-order-selected :up))
:ctrl+down #(st/emit! (dw/vertical-order-selected :down))
:ctrl+shift+up #(st/emit! (dw/vertical-order-selected :top))
:ctrl+shift+down #(st/emit! (dw/vertical-order-selected :bottom))
:shift+up #(st/emit! (dw/move-selected :up true))
:shift+down #(st/emit! (dw/move-selected :down true))
:shift+right #(st/emit! (dw/move-selected :right true))
:shift+left #(st/emit! (dw/move-selected :left true))
:up #(st/emit! (dw/move-selected :up false))
:down #(st/emit! (dw/move-selected :down false))
:right #(st/emit! (dw/move-selected :right false))
:left #(st/emit! (dw/move-selected :left false))
})
;; --- Shortcuts Setup Functions
(defn- watch-shortcuts
[sink]
(let [handler (KeyboardShortcutHandler. js/document)]
;; Register shortcuts.
(doseq [item (keys +shortcuts+)]
(let [identifier (name item)]
(.registerShortcut handler identifier identifier)))
;; Initialize shortcut listener.
(let [event KeyboardShortcutHandler.EventType.SHORTCUT_TRIGGERED
callback #(sink (keyword (.-identifier %)))
key (events/listen handler event callback)]
(fn []
(events/unlistenByKey key)
(.clearKeyListener handler)))))
(defn init
[]
(let [stream (->> (rx/create watch-shortcuts)
(rx/pr-log "[debug]: shortcut:"))]
(rx/on-value stream (fn [event]
(when-let [handler (get +shortcuts+ event)]
(handler))))))

View file

@ -7,8 +7,10 @@
(ns uxbox.util.webapi
"HTML5 web api helpers."
(:require
[promesa.core :as p]
[beicon.core :as rx]
[cuerdas.core :as str]))
[cuerdas.core :as str]
[uxbox.util.transit :as t]))
(defn read-file-as-text
[file]
@ -65,4 +67,19 @@
;; (rx/create on-subscribe)))
(defn write-to-clipboard
[data]
(let [cboard (unchecked-get js/navigator "clipboard")]
(.writeText cboard (uxbox.util.transit/encode data))))
(defn- read-from-clipboard
[]
(let [cboard (unchecked-get js/navigator "clipboard")]
(-> (.readText cboard)
(p/then (fn [data]
(try
(t/decode data)
(catch :default e
nil)))))))