From 6de71c4c66f341e81fa7a0623a9e85c1337d1834 Mon Sep 17 00:00:00 2001 From: Olive Vaughn Date: Sun, 4 Oct 2026 02:12:06 -0400 Subject: [PATCH] Align stage multi-selection modifiers with the timeline --- frontend/src/arthur/domain/pick.cljs | 2 +- frontend/src/arthur/ui/stage.cljs | 27 ++++++++++++++++++---- frontend/test/arthur/events/lane_test.cljs | 15 ++++++++++++ 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/frontend/src/arthur/domain/pick.cljs b/frontend/src/arthur/domain/pick.cljs index 326112a..c7a91af 100644 --- a/frontend/src/arthur/domain/pick.cljs +++ b/frontend/src/arthur/domain/pick.cljs @@ -7,7 +7,7 @@ WHICH LEVEL a click selects is Figma's and Illustrator's rule, and Flash's without its edit mode: a click selects the thing in the open symbol, a - double-click goes one level into what is selected, ⌘-click goes straight to the + double-click goes one level into what is selected, ⌥-click goes straight to the shape itself. A click inside what is selected keeps it, so a deep selection can be dragged; one elsewhere selects at the same depth, beside it." (:require [arthur.domain.channel :as ch] diff --git a/frontend/src/arthur/ui/stage.cljs b/frontend/src/arthur/ui/stage.cljs index 5227841..d08a324 100644 --- a/frontend/src/arthur/ui/stage.cljs +++ b/frontend/src/arthur/ui/stage.cljs @@ -29,6 +29,7 @@ [arthur.ui.player :as player] [arthur.ui.tracing :as tracing] [arthur.ui.tools :as tools] + [arthur.ui.timeline :as timeline] [re-frame.core :as rf] [reagent.core :as r])) @@ -55,6 +56,7 @@ ;; produces go straight out as `::set-vertex`, which is where the document ;; changes and where re-frame belongs. (defonce ^:private dragging (atom nil)) +(defonce ^:private selection-anchor (atom nil)) (defn- editing "The selected node when it is a polygon on screen, however deep it is nested, @@ -550,20 +552,35 @@ (do (.setPointerCapture svg (.-pointerId event)) (stroke-begin! ctx tool p size tone event)) - (let [path (pick/choose selected (player/at p) - (or (.-metaKey event) (.-ctrlKey event)))] + (let [path (pick/choose selected (player/at p) (.-altKey event))] + (when (and path (not (.-shiftKey event))) + (reset! selection-anchor {:clip-id clip-id :open (:open ctx) :path path})) (cond - (and (.-shiftKey event) path) + (and (or (.-metaKey event) (.-ctrlKey event)) path) (when-let [address (address-for ctx path)] (rf/dispatch [::ui/toggle-selection address])) + (and (.-shiftKey event) path) + (when-let [address (address-for ctx path)] + (let [document (:clip (loaded ctx)) + expanded @(rf/subscribe [::sub/expanded]) + xs (vec (keep :select (timeline/rows document (:open ctx) expanded selected))) + origin (if (and (= clip-id (:clip-id @selection-anchor)) + (= (:open ctx) (:open @selection-anchor))) + (:path @selection-anchor) selected) + anchor (when origin (address-for ctx origin)) + a (.indexOf xs anchor) b (.indexOf xs address)] + (rf/dispatch (if (and (<= 0 a) (<= 0 b)) + [::ui/select-many (subvec xs (min a b) (inc (max a b)))] + [::ui/select address])))) + path (when-not (contains? selected-paths path) (select! ctx path)) :else (do (.setPointerCapture svg (.-pointerId event)) - (reset! marquee {:p0 p :p p :more? (.-shiftKey event)}))) - (when (and path (not (.-shiftKey event))) + (reset! marquee {:p0 p :p p :more? (or (.-metaKey event) (.-ctrlKey event) (.-shiftKey event))}))) + (when (and path (not (or (.-shiftKey event) (.-metaKey event) (.-ctrlKey event)))) (.setPointerCapture svg (.-pointerId event)) (if (and (> (count placements) 1) (contains? selected-paths path)) (begin-many! ctx :move placements p) diff --git a/frontend/test/arthur/events/lane_test.cljs b/frontend/test/arthur/events/lane_test.cljs index c4ca294..782a1c0 100644 --- a/frontend/test/arthur/events/lane_test.cljs +++ b/frontend/test/arthur/events/lane_test.cljs @@ -484,3 +484,18 @@ (is (= [:girl clip-id] path)) (is (some? (get-in saved [:symbols :main :nodes clip-id])) "and that is where the node actually is"))) + +(deftest restacking-a-selection-keeps-all-members-selected + (let [doc (update-in (fixture/document) [:symbols :main] dissoc :display) + id (store/install! {:clip doc :store {}} "restack-many-test") + selections [[:node :main :a [:a]] [:node :main :b [:b]]]] + (reset! rf-db/app-db {:clip/current id :paint/revision 0 + :ui {:open :main :selection (peek selections) :selections selections} + :playback {:frame 6}}) + (rf/dispatch-sync [::ui/restack-nodes selections [:insert] true]) + (let [after (:clip (store/entry (:clip/current @rf-db/app-db))) + z #(get-in after [:symbols :main :nodes % :z])] + (is (pos? (compare (z :a) (z :insert)))) + (is (pos? (compare (z :b) (z :a)))) + (is (= selections (get-in @rf-db/app-db [:ui :selections]))) + (is (= [0 4] (mapv #(get-in after [:symbols :main :nodes % :time :at]) [:a :b]))))))