diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 1bb536f76..47ecead54 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -707,6 +707,7 @@ fn run_ui_part8(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option crate::dialogs::tile_edge_color::open(app), "ui.layerOptions" => crate::dialogs::layer_options::open(app, p), + "ui.rename" => crate::panels::layers::rename(app, p), "ui.newLayer" => crate::dialogs::layer_options::open_new(app, p), "ui.newArtboard" => crate::dialogs::artboard_options::open_new(app), "ui.layersPanelOptions" => crate::dialogs::layers_panel_options::open(app), diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 44627725d..52f8f8473 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -855,6 +855,12 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "", "{ids?|id?} open Layer Options for Layers panel rows (default: the highlighted rows, else the current layer), filled in from the first: dialog `layerOptions`, fields name, color (a preset name such as \"Light Blue\" or #rrggbb), template, locked, visible, printable, preview, dimImages (bool), dimPercent (0–100); an object's row has name, visible and locked. OK runs layer.setProps (one undo step); for several rows it sends only the fields set in the dialog", ), + ( + "ui.rename", + "Rename", + "", + "{id?} open the Layers panel's name field on the row of layer, group or object id, as double-clicking the row's name does (the panel is shown and the layers around the row open): Enter renames it with layer.setProps (one undo step), Escape keeps the name. Without id: the highlighted row when a Layers panel row was clicked last, else the one selected object or group, else the one highlighted row, else the current layer. Several rows or objects, or a row the panel's search, filter or Show Layers Only hides, is an error and nothing opens → {id, name}", + ), ( "ui.newLayer", "New Layer Options…", diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 98c082fc7..65f7c6bc3 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -250,7 +250,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { if let Some(id) = located { open_around(app, &doc, &[id]); } - let reveal = located.or(reveal); + // A row Rename (`ui.rename`) opened the name field of comes into view first. + let renamed = ui.data_mut(|d| d.remove_temp::(key("rename-reveal"))); + let reveal = renamed.or(located).or(reveal); let Some(st) = app.session.active() else { return }; let list = rows_of_doc(ui, st, &app.ui.layers_panel, &query, &app.ui.layers_filter); let view = View { @@ -377,6 +379,58 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { see_selection(app, ui, selected); } +/// The search typed into the Layers panel's Search All field, as the panel matches it. +fn search_query(ctx: &egui::Context) -> String { + ctx.data(|d| d.get_temp::(egui::Id::unique("layers-search"))).unwrap_or_default().trim().to_lowercase() +} + +/// `ui.rename {id?}`: open the Layers panel's name field on the row of layer, group or object +/// `id`, as double-clicking the row's name does: Enter renames it (`layer.setProps`, one undo +/// step), Escape leaves the name as it was. Without `id`, the row Rename names is the Layers +/// panel's highlighted row when a row was clicked last, else the one selected object or group, +/// else the one highlighted row, else the current layer. Several of them, or a row the panel's +/// search, filter or Show Layers Only leaves out, is an error that opens nothing. +pub fn rename(app: &mut VectorcraftApp, p: &Value) -> Result { + let ctx = app.ui_ctx.clone().ok_or("the Layers panel is not shown")?; + let st = app.session.active().ok_or("no document open")?; + let one = |ids: &[NodeId]| match ids { + [id] => Ok(*id), + _ => Err("select one layer, group or object to rename".to_string()), + }; + let id = match p.get("id") { + Some(v) => { + let id = NodeId(v.as_u64().ok_or_else(|| format!("`id` is an object id, not {v}"))?); + st.doc.node(id).map(|_| id).ok_or("no such layer or object")? + } + None => { + let rows = st.highlighted_rows(); + let selected = &st.selection.objects; + let rows_first = super::has_row_keys(&ctx, ROW_KEYS) || selected.is_empty(); + if rows_first && !rows.is_empty() { + one(&rows)? + } else if !selected.is_empty() { + one(selected)? + } else { + st.current_layer().ok_or("no layer to rename")? + } + } + }; + let doc = st.doc.clone(); + let node = doc.node(id).ok_or("no such layer or object")?; + if !listed_when_opened(&doc, &st.layers_open, &app.ui.layers_panel, &search_query(&ctx), &app.ui.layers_filter, id) { + return Err("the Layers panel's search, filter or Show Layers Only hides its row".into()); + } + let name = node.identify_label(app.session.prefs.identify_objects_by == "xmlId"); + crate::panel_docking::reveal_panel(app, "layers")?; + open_around(app, &doc, &[id]); + ctx.data_mut(|d| { + d.insert_temp(rename_id(), (id.0, name.clone())); + d.insert_temp(key("rename-reveal"), id); + }); + ctx.request_repaint(); + Ok(json!({ "id": id.0, "name": name })) +} + /// Open the layers and groups around rows `ids`. fn open_around(app: &mut VectorcraftApp, doc: &Document, ids: &[NodeId]) { let around: Vec = diff --git a/crates/ui-egui/src/panels/layers/tests.rs b/crates/ui-egui/src/panels/layers/tests.rs index f138b6cbf..0bd0efdde 100644 --- a/crates/ui-egui/src/panels/layers/tests.rs +++ b/crates/ui-egui/src/panels/layers/tests.rs @@ -469,6 +469,66 @@ fn shift_enter_renames_a_layer_and_keeps_the_field_open() { assert!(ctx.data(|d| d.get_temp::<(u64, String)>(rename_id())).is_none(), "and closes the field"); } +/// Rename (`ui.rename`, F2) opens the name field on the selected object's row, as a +/// double-click on the name does: Enter renames it in one undo step. +#[test] +fn rename_opens_the_name_field_of_the_selected_object() { + let (mut app, _, [a, _]) = two_rects(); + let ctx = egui::Context::default(); + app.ui_ctx = Some(ctx.clone()); + let name = |app: &VectorcraftApp| app.session.doc().unwrap().doc.node(a).unwrap().display_name(); + let before = name(&app); + app.session.execute("select.set", &json!({"ids": [a.0]})).unwrap(); + let opened = app.run("ui.rename", json!({})).unwrap(); + assert_eq!(opened, json!({"id": a.0, "name": before})); + assert_eq!(ctx.data(|d| d.get_temp::<(u64, String)>(rename_id())), Some((a.0, before.clone())), "the field opens on its row"); + let key = |key, modifiers| egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }; + frame(&mut app, &ctx, vec![], false); + frame(&mut app, &ctx, vec![], false); + assert!(ctx.memory(|m| m.has_focus(rename_id().with("field"))), "with the keyboard"); + frame(&mut app, &ctx, vec![key(egui::Key::A, egui::Modifiers::COMMAND), egui::Event::Text("Sun".into())], false); + frame(&mut app, &ctx, vec![key(egui::Key::Enter, egui::Modifiers::NONE)], false); + frame(&mut app, &ctx, vec![], false); + assert_eq!(name(&app), "Sun"); + app.session.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(name(&app), before, "one undo step"); +} + +/// Rename (`ui.rename`) has no default key, F2 being free: it is listed in Keyboard Shortcuts, +/// where a key can be assigned to it. +#[test] +fn rename_has_no_default_key() { + use crate::shortcut_editor::{command_for_key, default_command_shortcut}; + crate::shortcut_editor::sync(&crate::state::UiState::default()); + assert_eq!(default_command_shortcut("ui.rename"), None); + assert_eq!(command_for_key("F2"), None); + let (mut app, _, _) = two_rects(); + let listed = app.run("shortcuts.list", json!({"query": "ui.rename"})).unwrap(); + let rows: Vec<_> = listed.as_array().expect("rows").iter().map(|r| r["label"].clone()).collect(); + assert_eq!(rows, [json!("Rename")], "Keyboard Shortcuts lists it"); +} + +/// Without a selection Rename names the current layer; it refuses several objects and a row the +/// panel doesn't list, and opens nothing then. +#[test] +fn rename_names_the_current_layer_and_refuses_several_or_hidden_rows() { + let (mut app, layer, [a, b]) = two_rects(); + let ctx = egui::Context::default(); + app.ui_ctx = Some(ctx.clone()); + let open = |ctx: &egui::Context| ctx.data(|d| d.get_temp::<(u64, String)>(rename_id())).map(|r| r.0); + assert_eq!(app.run("ui.rename", json!({})).unwrap()["id"], json!(layer.0), "nothing selected: the current layer"); + ctx.data_mut(|d| d.remove::<(u64, String)>(rename_id())); + app.session.execute("select.set", &json!({"ids": [a.0, b.0]})).unwrap(); + assert!(app.run("ui.rename", json!({})).is_err(), "two objects"); + assert_eq!(open(&ctx), None); + app.ui.layers_panel.layers_only = true; + assert!(app.run("ui.rename", json!({"id": a.0})).is_err(), "Show Layers Only lists no object rows"); + assert_eq!(open(&ctx), None); + assert!(app.run("ui.rename", json!({"id": 999_999})).is_err(), "no such object"); + assert_eq!(app.run("ui.rename", json!({"id": layer.0})).unwrap()["id"], json!(layer.0)); + assert_eq!(open(&ctx), Some(layer.0)); +} + #[test] fn an_open_rename_does_not_follow_into_another_document() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 50026209e..ee2b6ea83 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -370,7 +370,9 @@ pub fn entries() -> &'static [Entry] { is_tool: false, })); for (id, label, sc, params) in menus::UI_COMMANDS { - if !seen.insert(id) || (sc.is_empty() && !params.starts_with("{}") && !params.starts_with("{path")) { + // Rename has no default key, and a key press names the selection: it needs no `id`. + let key_runs_it = !sc.is_empty() || params.starts_with("{}") || params.starts_with("{path") || *id == "ui.rename"; + if !seen.insert(id) || !key_runs_it { continue; } v.push(Entry { key: id.to_string(), label: label.to_string(), group: id_group(id), is_tool: false }); diff --git a/docs/ui-parity.md b/docs/ui-parity.md index abe967692..bfa231843 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-06 · **Change:** minor (Layers: Rename (`ui.rename`) opens the name field of the selected layer, group or object, #5324; Properties in Edit Artboards: Transform section above the Artboards section, #5219; scrubby labels: the Control bar's Stroke: and Opacity: links and the Properties panel's Stroke link scrub their fields, #4088; Properties: Quick Actions has Repeat Options for one selected repeat, #5181; Direct Selection: Shift-click picks several blend spine points, which drag together, #5285; Direct Selection: Cmd/Ctrl held lends the Selection tool and shows its bounding box, #3901; Join tool trims two crossing open paths and joins them at the crossing, #5844; Control bar: Paragraph: link and Align left/center/right while type is selected, #5372; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) ## Feature submission evidence @@ -38,6 +38,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-06 | minor | Rename (`ui.rename`, no default key) opens the Layers panel's name field on the selected layer, group or object's row, as double-clicking its name does; a key can be assigned in Keyboard Shortcuts (#5324, a VectorCraft addition); scores and estimates unchanged | | 2026-11-06 | minor | Properties panel in Edit Artboards (Shift+O): a Transform section with the shared reference point, X, Y, W and H, the preset and orientation, and Move and Scale Artwork with Artboard, above the Artboards section with the name, Artboard Options and Fill (#5219); scores and estimates unchanged | | 2026-11-06 | minor | Scrubby labels: the Control bar's Stroke: and Opacity: and the Properties panel's Stroke, links to their panels, scrub the field after them as other labels do; a click still opens the panel (#4088); scores and estimates unchanged | | 2026-11-05 | minor | Direct Selection: Shift-click picks several points of a blend's spine and dragging one moves them all, with the key objects on them, in one undo step (#5285); scores and estimates unchanged | @@ -305,7 +306,7 @@ Cmd/Ctrl+/ (`appearance.addFill`) and Cmd/Ctrl+Alt+/ (`appearance.addStroke`) ad selected objects, as the Appearance panel's Add New Fill and Add New Stroke buttons do. ~110 distinct modified shortcuts on menu commands plus single-key tool shortcuts for the default toolbar -(`crates/tools/src/catalog.rs`), a shortcut editor with chords, and saved sets. Cmd/Ctrl+Alt+L (`ui.newLayer`) opens Layer Options for a new layer, as an Alt-click on the Layers panel's New Layer button does, and Cmd/Ctrl+L still adds a layer without the dialog. F8 (`ui.symbolOptions`) opens Symbol Options for a new symbol made from the selection, as the Symbols panel's New Symbol button does. Not yet diffed item by item +(`crates/tools/src/catalog.rs`), a shortcut editor with chords, and saved sets. Cmd/Ctrl+Alt+L (`ui.newLayer`) opens Layer Options for a new layer, as an Alt-click on the Layers panel's New Layer button does, and Cmd/Ctrl+L still adds a layer without the dialog. F8 (`ui.symbolOptions`) opens Symbol Options for a new symbol made from the selection, as the Symbols panel's New Symbol button does. Rename (`ui.rename`, in the shortcut editor with no default key; a VectorCraft addition, #5324) opens the Layers panel's name field on the selected layer, group or object's row, as double-clicking its name does: Enter renames it in one undo step and Escape keeps the name; several selected objects, or a row the panel's search, filter or Show Layers Only hides, is refused with a message. Not yet diffed item by item against Illustrator's published default shortcut list; estimated ~70% of it. Remaining: other panel shortcuts, the type shortcuts (tracking/kerning/leading/baseline steps with Alt/Shift), and selection shortcuts in the Layers panel. The shortcut editor also lists the commands that are in no menu but run without parameters (Edit › Duplicate,