diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index d4b9f0707..86f74b6b2 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -715,6 +715,37 @@ impl VectorcraftApp { } } + /// The active document's uid and the artboards the Artboard tool has selected (null with another + /// tool), which [`Self::follow_artboard_tool`] compares before and after a command. + fn artboard_tool_state(&self) -> (Option, Value) { + let selected = if self.session.tool_id() == "artboard" { self.session.tool_options()["selected"].clone() } else { Value::Null }; + (self.session.active().map(|d| d.uid), selected) + } + + /// After a command run with the Artboard tool, given [`Self::artboard_tool_state`] from before + /// it: when the command chose another document, or `reverted` the document (File › Revert), + /// the tool takes the document's active artboard, as choosing its tab does. When the command + /// changed the artboards the tool has selected (Paste and Cut with the tool, `tool.setOption`), + /// the tool's active artboard becomes the active one (the view's), as a click with the tool + /// makes it. + fn follow_artboard_tool(&mut self, before: (Option, Value), reverted: bool) { + self.sync_views(); + if self.session.tool_id() != "artboard" { + return; + } + let Some(n) = self.session.active().map(|d| d.doc.artboards.len()).filter(|n| *n > 0) else { return }; + let (doc, selected) = self.artboard_tool_state(); + if reverted || doc != before.0 { + panels::artboards::select(self, panels::artboards::selected(self, n)); + } else if selected != before.1 + && let Some(i) = self.session.tool_options()["active"].as_u64().and_then(|i| usize::try_from(i).ok()) + && i < n + && let Some(v) = self.view_mut() + { + v.artboard = i; + } + } + pub fn view(&self) -> Option<&View> { self.session.active_index().and_then(|i| self.views.get(i)) } @@ -782,7 +813,9 @@ impl VectorcraftApp { if cfg!(debug_assertions) { menus::check_params(&self.session, id, ¶ms)?; } + let artboards = self.artboard_tool_state(); if let Some(r) = menus::run_ui_command(self, id, ¶ms) { + self.follow_artboard_tool(artboards, false); return r; } let mut params = params; @@ -839,6 +872,8 @@ impl VectorcraftApp { self.publish_clipboard(); } self.sync_views(); + // File › Revert replaces the document in its tab, keeps the tab's view and starts the tool afresh. + self.follow_artboard_tool(artboards, r.is_ok() && id == "file.revert"); match &r { Err(e) => self.ui.status = e.clone(), Ok(v) => { diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index fdb03af05..a0411f8d3 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -897,9 +897,10 @@ mod tests { assert_eq!(names(&app), ["Artboard 1", "Solo", "Artboard 3", "Artboard 4"]); } - /// Paste with the Artboard tool selects the pasted artboard and leaves the active artboard - /// where it was. The panel menu's Rename then opens the name field on the selected artboard's - /// row and renames that artboard, not the active one. + /// Paste run on the session alone (`session.execute`, which leaves the view as it is) with the + /// Artboard tool selects the pasted artboard and leaves the active artboard where it was. The + /// panel menu's Rename then opens the name field on the selected artboard's row and renames + /// that artboard, not the active one. #[test] fn the_panel_menus_rename_opens_on_a_selected_artboard() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); @@ -936,6 +937,52 @@ mod tests { assert_eq!(names, ["Artboard 1", "Artboard 2", "Pasted"]); } + /// With the Artboard tool, Paste and Paste in Place make the pasted artboard the active one + /// (the navigator's and the panel's), and Cut makes the artboard before the cut one active, the + /// one the tool then has active. + #[test] + fn paste_and_cut_with_the_artboard_tool_make_its_artboard_active() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 150, "artboards": 3})).unwrap(); + app.select_tool("artboard"); + select(&mut app, 1); + let active = |app: &VectorcraftApp| (app.view().unwrap().artboard, app.session.tool_options()["active"].clone()); + app.run("edit.copy", json!({})).unwrap(); + app.run("edit.paste", json!({})).unwrap(); + assert_eq!(active(&app), (3, json!(3)), "Paste: the pasted Artboard 4"); + app.run("edit.pasteInPlace", json!({})).unwrap(); + assert_eq!(active(&app), (4, json!(4)), "Paste in Place: the pasted Artboard 5"); + select(&mut app, 2); + app.run("edit.cut", json!({})).unwrap(); + assert_eq!(active(&app), (1, json!(1)), "Cut Artboard 3: Artboard 2"); + } + + /// `tool.setOption` with the Artboard tool's `selected` or `active` makes the artboard the tool + /// then has active the active one. + #[test] + fn the_artboard_tools_options_make_its_artboard_active() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 150, "artboards": 3})).unwrap(); + app.select_tool("artboard"); + app.run("tool.setOption", json!({"key": "selected", "value": [2, 0]})).unwrap(); + assert_eq!((app.view().unwrap().artboard, picked(&app, 3)), (2, vec![0, 2])); + app.run("tool.setOption", json!({"tool": "artboard", "values": {"active": 1}})).unwrap(); + assert_eq!((app.view().unwrap().artboard, picked(&app, 3)), (1, vec![1])); + } + + /// With the Artboard tool, `document.activate` makes the active artboard of the document it + /// switches to the tool's, as choosing the document's tab does. + #[test] + fn switching_documents_with_the_artboard_tool_keeps_the_active_artboard() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 200, "height": 150, "artboards": 3})).unwrap(); + app.select_tool("artboard"); + select(&mut app, 2); + app.run("file.new", json!({"width": 200, "height": 150, "artboards": 2})).unwrap(); + app.run("document.activate", json!({"index": 0})).unwrap(); + assert_eq!((app.view().unwrap().artboard, app.session.tool_options()["selected"].clone()), (2, json!([2]))); + } + /// With the Artboard tool, a double-click on a name with Shift or Ctrl/Cmd held that leaves /// several artboards selected opens the name field for all of them. A Ctrl/Cmd double-click on /// an unselected row's name adds its artboard to the selection, and the second click leaves it diff --git a/crates/ui-egui/src/tests_save.rs b/crates/ui-egui/src/tests_save.rs index cf200fe52..ebc88de35 100644 --- a/crates/ui-egui/src/tests_save.rs +++ b/crates/ui-egui/src/tests_save.rs @@ -80,6 +80,22 @@ fn revert_asks_then_reloads_in_the_same_tab_keeping_every_zoom() { let _ = std::fs::remove_dir_all(d); } +/// With the Artboard tool, Revert keeps the active artboard, and the tool starts on it. +#[test] +fn revert_with_the_artboard_tool_keeps_the_active_artboard() { + let d = dir("revert-artboard"); + let path = d.join("a.vectorcraft").to_string_lossy().to_string(); + let mut app = app(); + app.run("file.new", json!({"width": 100, "height": 100, "artboards": 3})).unwrap(); + app.run("file.saveAs", json!({"path": path})).unwrap(); + app.select_tool("artboard"); + crate::panels::artboards::select(&mut app, 2); + rect(&mut app); + app.run("file.revert", json!({"confirmed": true})).unwrap(); + assert_eq!((app.view().unwrap().artboard, app.session.tool_options()["selected"].clone()), (2, json!([2]))); + let _ = std::fs::remove_dir_all(d); +} + #[test] fn new_from_template_takes_bytes_from_agents() { let mut app = app(); diff --git a/docs/mcp.md b/docs/mcp.md index 6fe656f45..750e3e11e 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4470,7 +4470,9 @@ tool keeps last across tool switches (and documents) and are saved with the pref `prefs.get`'s full object), as the reference app keeps them: the Liquify tools' brush and tool options, Mirror & Cut's axis and side, Puppet Warp's mesh, the Symbolism brush, the line, grid, pencil, brush and eraser tools' options, polygon sides and star points, the Flare Tool Options and the Shape Builder Tool Options. The Liquify tools share one set of Global Brush Dimensions (width, height, angle, -intensity), and the Symbolism tools one brush. Interaction state (pins, a reference point) starts afresh. +intensity), and the Symbolism tools one brush. Interaction state (pins, a reference point) starts afresh. In the +desktop app, while the Artboard tool is in use, a change to its `selected` or `active` makes the artboard it then has +active the active artboard (the navigator's and the Artboards panel's). ```json {"name":"run_command","arguments":{"command":"tool.setOption","params":{"tool":"twirl","values":{"width":60,"rate":90}}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index bfcd4bf17..e09976657 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -664,6 +664,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur Object › Artboards › Fit to Selected Art resizes the active artboard (`artboard.fitToSelection {index}`). It used to resize artboard 1 whichever artboard was active. When an action plays, its Fit to Artwork Bounds and Fit to Selected Art steps that name no artboard resize the artboard that is active then. Object › Artboards › Fit to Artwork Bounds and the Artboards panel menu's Fit to Artwork Bounds fit each selected artboard (those the Artboard tool has selected, else the active one) to the art on it, as the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page describes (`artboard.fitToArt {indices}`). An object partly on an artboard counts for each artboard it overlaps, and an artboard with no art on it keeps its size and place. They used to fit the active artboard to all the art in the document. The Artboards panel menu's Duplicate Artboards copies each selected artboard (those the Artboard tool has selected, else the active one) with its art, in one undo step, and the copies are then selected, as the public [Use the artboard context menu](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/use-artboard-context-menu.html) page describes. With the Artboard tool, Copy and Cut take every selected artboard with its art, as the public [Cut, copy, and paste artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/organize-manage-artboards/cut-copy-and-paste-artboards.html) page describes, and Paste brings them all back in one undo step (`artboard.duplicate`, `artboard.copy` and `artboard.cut` with `indices`). The copies keep their places relative to each other, together right of the last artboard. All three used to act on the active artboard only. + With the Artboard tool, Paste and Paste in Place make the first pasted artboard the active one (the navigator's and the Artboards panel's), and Cut makes the artboard before the first cut one active (the new first artboard when artboard 1 was cut). A command that chooses another document (`document.activate`) starts the tool on that document's active artboard, as choosing its tab does, and File › Revert keeps the active artboard and starts the tool on it. Paste used to leave the active artboard where it was while the tool had the pasted artboard active, and Cut left the active artboard on the artboard after the cut one while the tool had the one before it. The Artboards panel menu's Convert to Artboards turns the geometric bounds of each selected object that has an area into a new artboard and removes those objects, in one undo step, as Object › Artboards › Convert to Artboards does (`artboard.convertToArtboards`). It is disabled when nothing is selected. It used to be disabled always. The canvas draws the active artboard's edge in black, as the public [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html) page describes. It draws the other artboards' edges in gray. With several artboards selected by the Artboard tool, the canvas draws the black edge around the active one, which the tool shows with handles and its name. It used to draw artboard 1's edge in black whichever artboard was active. View › Rotate View turns the view to the status bar's preset angles (`view.rotate {angle}`, which takes any angle). View › Rotate View to Selection turns the view to the angle of the selected objects' bounding box, so the box is square to the window (`view.rotateToSelection`).