diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 85d1840ca..d6a4e0889 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -3359,11 +3359,13 @@ fn field_event(app: &mut VectorcraftApp, id: &str) -> Option { /// Invoke an item clicked in the system menu bar (macOS; its key equivalents come back to egui as /// key presses, see [`crate::native_menu`]). While a text field has the keyboard, Select All, Cut, -/// Copy and Paste act on the field's text, as their keys do; everything else (and those commands -/// with no field focused) goes to [`invoke`]. +/// Copy and Paste act on the field's text, as their keys do. While its keys select every artboard +/// ([`crate::panels::artboards::selects_all`]), Select All does too. Everything else (and those +/// commands with no field focused) goes to [`invoke`]. pub fn invoke_from_system_menu(app: &mut VectorcraftApp, ctx: &egui::Context, id: &str, p: Value) { match focused_text_field(ctx).and_then(|f| Some((f, field_event(app, id)?))) { Some((field, e)) => to_field(ctx, field, e), + None if id == "select.all" && crate::panels::artboards::selects_all(app, ctx) => crate::panels::artboards::select_all(app), None => invoke(app, id, p), } } diff --git a/crates/ui-egui/src/panels/artboards.rs b/crates/ui-egui/src/panels/artboards.rs index a5a81aa8f..d1de35d56 100644 --- a/crates/ui-egui/src/panels/artboards.rs +++ b/crates/ui-egui/src/panels/artboards.rs @@ -203,6 +203,32 @@ fn toggle(app: &mut VectorcraftApp, i: usize, n: usize) { app.session.set_tool_option("selected", &json!(all)); } +/// Whether the panel has the keyboard: one of its rows was clicked, and in every frame since then +/// the panel was drawn and the pointer was not pressed outside its list. +fn has_keyboard(app: &VectorcraftApp, ctx: &egui::Context) -> bool { + pstate::>(ctx, "ab-keys").is_some_and(|f| f + 1 >= app.frame) +} + +/// Whether Select All's shortcut selects every artboard ([`select_all`]) instead of the art: the +/// panel has the keyboard and the Artboard tool is in use. [`crate::shortcuts::handle`] and +/// [`crate::menus::invoke_from_system_menu`] read it before they run `select.all`, which they reach +/// only while no text field has the keyboard. +pub(crate) fn selects_all(app: &VectorcraftApp, ctx: &egui::Context) -> bool { + app.session.tool_id() == "artboard" && has_keyboard(app, ctx) +} + +/// Ctrl/Cmd+A while [`selects_all`]: the Artboard tool selects every artboard, and the active +/// artboard stays active. The art's selection stays as it is. +pub(crate) fn select_all(app: &mut VectorcraftApp) { + let n = app.session.active().map_or(0, |d| d.doc.artboards.len()); + if n == 0 { + return; + } + let active = selected(app, n); + let all: Vec = std::iter::once(active).chain((0..n).filter(|i| *i != active)).collect(); + app.session.set_tool_option("selected", &json!(all)); +} + /// Make artboard `i` the active one, leaving the view where it is: the navigator's and, while it /// is the tool, the Artboard tool's. pub(crate) fn select(app: &mut VectorcraftApp, i: usize) { @@ -254,10 +280,11 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let sel = selected(app, abs.len()); let all = picked(app, abs.len()); let editing: Option = pstate(ui.ctx(), "ab-edit"); - widgets::list_box(ui, |ui| { + let mut row_clicked = false; + let list_rect = widgets::list_box(ui, |ui| { ui.set_min_height(110.0); ui.spacing_mut().item_spacing.y = 0.0; - egui::ScrollArea::vertical().id_salt("ab-scroll").max_height(220.0).show(ui, |ui| { + let scrolled = egui::ScrollArea::vertical().id_salt("ab-scroll").max_height(220.0).show(ui, |ui| { for (i, name) in abs.iter().enumerate() { let (r, resp) = ui.allocate_exact_size(vec2(ui.available_width(), 24.0), Sense::click_and_drag()); if resp.drag_started() { @@ -308,6 +335,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } let m = ui.input(|inp| inp.modifiers); let tool = app.session.tool_id() == "artboard"; + row_clicked |= resp.clicked(); if resp.clicked() && tool && m.shift { let from = select_range(app, pstate(ui.ctx(), "ab-anchor"), i, abs.len()); set_pstate(ui.ctx(), "ab-anchor", Some(from)); @@ -329,7 +357,13 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } }); + scrolled.inner_rect }); + // Keyboard: the panel takes it with a click on a row and gives it up when the pointer is + // pressed outside the list or the panel is not drawn (`ab-keys`: the frame it last had it in). + let pressed_outside = ui.input(|i| i.pointer.any_pressed() && i.pointer.interact_pos().is_some_and(|p| !list_rect.contains(p))); + let keys = row_clicked || (has_keyboard(app, ui.ctx()) && !pressed_outside); + set_pstate(ui.ctx(), "ab-keys", keys.then_some(app.frame)); let n = abs.len(); widgets::bottom_bar(ui, |ui| { if widgets::icon_button_enabled(ui, "dc-rearrange", tl!("Rearrange All Artboards"), false, n > 1, 24.0).clicked() { diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 8b852939f..52213c3e5 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -520,7 +520,13 @@ pub fn handle(app: &mut VectorcraftApp, ctx: &egui::Context) { } } if let Some((id, p)) = fire { - crate::menus::invoke(app, id, p); + // With the Artboard tool, Select All's keys after a click on an Artboards panel row select + // every artboard instead of the art. + if id == "select.all" && crate::panels::artboards::selects_all(app, ctx) { + crate::panels::artboards::select_all(app); + } else { + crate::menus::invoke(app, id, p); + } return; } // Arrow nudges (Shift = ×10, Alt = copy). diff --git a/crates/ui-egui/src/tests_selectall.rs b/crates/ui-egui/src/tests_selectall.rs index 51629a2f2..e5efd535c 100644 --- a/crates/ui-egui/src/tests_selectall.rs +++ b/crates/ui-egui/src/tests_selectall.rs @@ -1,6 +1,7 @@ //! Select All (Cmd+A) follows the keyboard, from the window and from the system menu bar (macOS): //! a focused text field selects its own text, the Type tool editing selects the text being edited, -//! and only otherwise are all the objects selected. +//! the Artboards panel selects every artboard after a click on one of its rows with the Artboard +//! tool, and only otherwise are all the objects selected. use egui::{Event, Key, Modifiers, PointerButton, Pos2, Rect, vec2}; use serde_json::json; @@ -172,6 +173,61 @@ fn cmd_a_on_the_canvas_selects_all_objects() { assert_eq!(a.selected(), 2, "so does the menu's Select All"); } +/// With the Artboard tool, Cmd+A after a click on an Artboards panel row selects every artboard +/// and leaves the active artboard and the art as they are; so does the macOS menu bar's Select All. +/// After a press on the canvas, with the panel closed, or with the Selection tool, Cmd+A selects +/// every object. +#[test] +fn cmd_a_after_a_click_in_the_artboards_panel_selects_every_artboard() { + let mut a = App::new(); + for x in [500, 1000] { + a.app.run("artboard.new", json!({"x": x, "y": 0, "width": 400, "height": 300})).unwrap(); + } + a.app.run("window.panel", json!({"panel": "artboards"})).unwrap(); + a.app.select_tool("artboard"); + let picked = |a: &App| (crate::panels::artboards::picked(&a.app, 3), crate::panels::artboards::selected(&a.app, 3)); + // Artboard 2's row, while Artboard 1 is active (the Control bar and the navigator show its name). + let row = |a: &mut App| { + crate::panels::artboards::select(&mut a.app, 0); + a.frame(vec![]); + text_at(&a.frame(vec![]), "Artboard 2", false) + }; + let at = row(&mut a); + click_at(&mut a, at); + assert_eq!(picked(&a), (vec![1], 1)); + let art = a.selected(); + a.frame(vec![press(Key::A, Modifiers::COMMAND)]); + assert_eq!(picked(&a), (vec![0, 1, 2], 1), "Cmd+A selects every artboard, and Artboard 2 stays active"); + assert_eq!(a.selected(), art, "and not the art"); + // So does Select All in the macOS menu bar. + click_at(&mut a, at); + assert_eq!(picked(&a), (vec![1], 1)); + a.menu("select.all"); + assert_eq!(picked(&a), (vec![0, 1, 2], 1), "the menu's Select All selects every artboard"); + assert_eq!(a.selected(), art, "and not the art"); + // A press on the canvas gives the keys back to the art. + let canvas = a.app.canvas_rect.unwrap(); + click_at(&mut a, canvas.left_top() + vec2(20.0, 20.0)); + a.frame(vec![press(Key::A, Modifiers::COMMAND)]); + assert_eq!(a.selected(), 2, "after a press on the canvas, Cmd+A selects every object"); + // So does closing the panel after the click on a row. + a.app.run("select.none", json!({})).unwrap(); + let at = row(&mut a); + click_at(&mut a, at); + a.app.run("window.panel", json!({"panel": "artboards", "toggle": true})).unwrap(); + a.frame(vec![]); + a.frame(vec![press(Key::A, Modifiers::COMMAND)]); + assert_eq!(a.selected(), 2, "with the panel closed, Cmd+A selects every object"); + // With the Selection tool, Cmd+A after a click on a row selects every object. + a.app.run("select.none", json!({})).unwrap(); + a.app.run("window.panel", json!({"panel": "artboards"})).unwrap(); + a.app.select_tool("selection"); + let at = row(&mut a); + click_at(&mut a, at); + a.frame(vec![press(Key::A, Modifiers::COMMAND)]); + assert_eq!(a.selected(), 2, "with the Selection tool, Cmd+A selects every object"); +} + /// The copied texts of a frame's output. fn copied(out: &egui::FullOutput) -> Vec { out.platform_output.commands.iter().filter_map(|c| if let egui::OutputCommand::CopyText(t) = c { Some(t.clone()) } else { None }).collect() diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index decb3e017..c93d6180c 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -483,6 +483,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Shift+Tab (`window.panels`) shows and hides the dock and the floating panels and leaves the toolbar and the Control bar as they are; hiding the panels with Tab or Shift+Tab also closes a panel popped out of the icon column. - The second Tab (`window.dock`) shows again only the bars the first Tab hid, after a restart too: a Control bar that was off stays off, a toolbar hidden with Window › Tools stays hidden, and panels shown with Shift+Tab in between stay shown. - Cmd/Ctrl+A follows the keyboard: in a panel, dialog or search field it selects the field's text, while the Type tool edits it selects all of that text (also `select.all` over MCP), and only otherwise every object. On macOS the menu bar's Select All, Cut, Copy and Paste (keys or clicks) act on a focused field too, and elsewhere the in-window menus' do: the field that had the keyboard when the menu opened gets them, with its selection, and the keyboard back (#516). Not yet: Select All across threaded frames (one frame's text is selected). + - With the Artboard tool, Cmd/Ctrl+A after a click on a row of the Artboards panel selects every artboard, as the macOS menu bar's Select All does then; the active artboard stays active, and the art's selection does not change. After a press outside the panel's list, Cmd/Ctrl+A selects every object again. It used to select every object after a click in the panel too. Public reference: [Select artboards](https://helpx.adobe.com/illustrator/desktop/create-manage-artboards/add-edit-artboards/select-artboards.html). - Cmd/Ctrl+Alt+Shift+. and , (`type.size.increaseByFive`, `type.size.decreaseByFive`) step the font size of the selected type, or of the text the Type tool has selected, by five times the Size/Leading increment (Preferences › Type); Cmd/Ctrl+Shift+. and , still step it by one increment. - Cmd/Ctrl+Alt+Q (`type.resetTracking`) sets the tracking of the selected type, or of the text the Type tool has selected, to 0 and its kerning to Auto (0 in the Character panel), in one undo step; at a caret it resets the kerning of the character before it. - Cmd/Ctrl+Shift+X (`type.resetHorizontalScale`) sets the horizontal scale of the selected type, or of the text the Type tool has selected, to 100% as the Character panel shows it (the object's own stretch included), in one undo step; at a caret it leaves the text as it is.