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

View file

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

View file

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