Add the Shift+ctrl+drag to deselect (#6494)

*  Allow shape deselection using Ctrl+Shift+Drag

*  Allow point deselection using Ctrl+Shift+Drag

*  Properly remember previous selection during addition/removal of shapes

*  Preload point selection in path handle-area-selection

Also: prefer dm/get-in over get-in

*  Highlight path nodes in selection rectangle incrementally
This commit is contained in:
Miguel de Benito Delgado 2025-05-20 15:23:05 +02:00 committed by GitHub
parent 31f642ed25
commit 48a3d38d82
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 63 additions and 47 deletions

View file

@ -14,6 +14,7 @@
[app.main.streams :as ms]
[app.util.mouse :as mse]
[beicon.v2.core :as rx]
[beicon.v2.operators :as rxo]
[potok.v2.core :as ptk]))
(defn path-pointer-enter [position]
@ -45,7 +46,7 @@
(update-in state [:workspace-local :edit-path id :hover-handlers] disj [index prefix])))))
(defn select-node-area
[shift?]
[initial-set remove?]
(ptk/reify ::select-node-area
ptk/UpdateEvent
(update [_ state]
@ -57,13 +58,12 @@
(partial gsh/has-point-rect? selrect)
(constantly false))
selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points])
selected-points (or selected-points #{})
xform (comp (filter #(not (= (:command %) :close-path)))
(map (comp gpt/point :params))
(filter selected-point?))
positions (into (if shift? selected-points #{}) xform content)]
positions (if remove?
(apply disj initial-set (into #{} xform content))
(into initial-set xform content))]
(cond-> state
(some? id)
@ -73,8 +73,8 @@
(ptk/reify ::select-node
ptk/UpdateEvent
(update [_ state]
(let [id (get-in state [:workspace-local :edition])
selected-points (or (get-in state [:workspace-local :edit-path id :selected-points]) #{})
(let [id (dm/get-in state [:workspace-local :edition])
selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
selected-points (cond
(and shift? (contains? selected-points position))
(disj selected-points position)
@ -111,25 +111,42 @@
(update state :workspace-local dissoc :selrect))))
(defn handle-area-selection
[shift?]
[append? remove?]
(letfn [(valid-rect? [zoom {width :width height :height}]
(or (> width (/ 10 zoom)) (> height (/ 10 zoom))))]
(ptk/reify ::handle-area-selection
ptk/WatchEvent
(watch [_ state stream]
(let [zoom (get-in state [:workspace-local :zoom] 1)
(let [id (dm/get-in state [:workspace-local :edition])
zoom (dm/get-in state [:workspace-local :zoom] 1)
stopper (mse/drag-stopper stream)
from-p @ms/mouse-position]
(rx/concat
from-p @ms/mouse-position
initial-set
(if (or append? remove?)
(dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
#{})
selrect-stream
(->> ms/mouse-position
(rx/map #(grc/points->rect [from-p %]))
(rx/filter (partial valid-rect? zoom))
(rx/map update-area-selection)
(rx/take-until stopper))
(rx/take-until stopper))]
(rx/of (select-node-area shift?)
(clear-area-selection))))))))
(rx/concat
(if (or append? remove?)
(rx/empty)
(rx/of (deselect-all)))
(rx/merge
(->> selrect-stream
(rx/map update-area-selection))
(->> selrect-stream
(rx/buffer-time 100)
(rx/map last)
(rx/pipe (rxo/distinct-contiguous))
(rx/map #(select-node-area initial-set remove?))))
(rx/of (clear-area-selection))))))))
(defn update-selection
[point-change]
@ -137,7 +154,7 @@
ptk/UpdateEvent
(update [_ state]
(let [id (st/get-path-id state)
selected-points (get-in state [:workspace-local :edit-path id :selected-points] #{})
selected-points (dm/get-in state [:workspace-local :edit-path id :selected-points] #{})
selected-points (into #{} (map point-change) selected-points)]
(-> state
(assoc-in [:workspace-local :edit-path id :selected-points] selected-points))))))

View file

@ -54,7 +54,7 @@
(assoc-in state [:workspace-local :selrect] selrect))))
(defn handle-area-selection
[preserve?]
[append? remove? ignore-groups?]
(ptk/reify ::handle-area-selection
ptk/WatchEvent
(watch [_ state stream]
@ -62,6 +62,10 @@
stopper (mse/drag-stopper stream)
init-position @ms/mouse-position
initial-set (if (or append? remove?)
(dsh/lookup-selected state)
lks/empty-linked-set)
init-selrect (grc/make-rect
(dm/get-prop init-position :x)
(dm/get-prop init-position :y)
@ -91,7 +95,7 @@
(rx/take-until stopper))]
(rx/concat
(if preserve?
(if (or append? remove?)
(rx/empty)
(rx/of (deselect-all)))
@ -103,20 +107,14 @@
(rx/buffer-time 100)
(rx/map last)
(rx/pipe (rxo/distinct-contiguous))
(rx/with-latest-from ms/keyboard-mod ms/keyboard-shift)
(rx/map
(fn [[_ mod? shift?]]
(select-shapes-by-current-selrect shift? mod?))))
(rx/map #(select-shapes-by-current-selrect initial-set remove? ignore-groups?)))
;; The last "tick" from the mouse cannot be buffered so we are sure
;; a selection is returned. Without this we can have empty selections on
;; very fast movement
(->> selrect-stream
(rx/last)
(rx/with-latest-from ms/keyboard-mod ms/keyboard-shift)
(rx/map
(fn [[_ mod? shift?]]
(select-shapes-by-current-selrect shift? mod? false)))))
(rx/map #(select-shapes-by-current-selrect initial-set remove? ignore-groups? false))))
(->> (rx/of (update-selrect nil))
;; We need the async so the current event finishes before updating the selrect
@ -325,22 +323,23 @@
;; --- Select Shapes (By selrect)
(defn select-shapes-by-current-selrect
([preserve? ignore-groups?]
(select-shapes-by-current-selrect preserve? ignore-groups? true))
([preserve? ignore-groups? buffered?]
"Sends the current selection rectangle to the worker to compute the selection,
and sends its result to select-shapes for storage in the state."
([initial-set remove? ignore-groups?]
(select-shapes-by-current-selrect initial-set remove? ignore-groups? true))
([initial-set remove? ignore-groups? buffered?]
(ptk/reify ::select-shapes-by-current-selrect
ptk/WatchEvent
(watch [_ state _]
(let [page-id (:current-page-id state)
objects (dsh/lookup-page-objects state)
selected (dsh/lookup-selected state)
initial-set (if preserve?
selected
lks/empty-linked-set)
selrect (dm/get-in state [:workspace-local :selrect])
blocked? (fn [id] (dm/get-in objects [id :blocked] false))
ask-worker (if buffered? mw/ask-buffered! mw/ask!)]
ask-worker (if buffered? mw/ask-buffered! mw/ask!)
filter-objs (comp
(filter (complement blocked?))
(remove (partial cfh/hidden-parent? objects)))]
(if (some? selrect)
(->> (ask-worker
@ -353,9 +352,9 @@
:using-selrect? true})
(rx/filter some?)
(rx/map #(cfh/clean-loops objects %))
(rx/map #(into initial-set (comp
(filter (complement blocked?))
(remove (partial cfh/hidden-parent? objects))) %))
(rx/map (if remove?
#(apply disj initial-set %)
#(into initial-set filter-objs %)))
(rx/map select-shapes))
(rx/empty)))))))

View file

@ -102,14 +102,14 @@
node-editing?
;; Handle path node area selection
(when-not read-only?
(st/emit! (dwdp/handle-area-selection shift?)))
(st/emit! (dwdp/handle-area-selection shift? (and shift? mod?))))
drawing-tool
(when-not read-only?
(st/emit! (dd/start-drawing drawing-tool)))
(or (not id) mod?)
(st/emit! (dw/handle-area-selection shift?))
(st/emit! (dw/handle-area-selection shift? (and shift? mod?) mod?))
(not drawing-tool)
(when-not read-only?