From a535d7b890ed6854657d7a3e033b7fc95780ecf9 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 30 Oct 2026 07:39:07 -0700 Subject: [PATCH 1/3] Panels: a second press of a panel's function key hides the panel The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency and Symbols now run window.panel with toggle: true, as the Window menu items do. A second press hides an active panel in the expanded dock, and the next press restores it in its group. A chord the user assigns to one of these panels toggles the same way. Plain window.panel calls still only show the panel. --- crates/ui-egui/src/menus/run.rs | 4 +- crates/ui-egui/src/panel_docking/tests.rs | 118 ++++++++++++++++++++++ crates/ui-egui/src/shortcuts.rs | 35 ++++++- crates/ui-egui/src/shortcuts/tests.rs | 24 +++++ docs/target-app-parity.md | 1 + 5 files changed, 177 insertions(+), 5 deletions(-) diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index 339682c94..f31c50c40 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -40,8 +40,8 @@ pub fn check_params(session: &vectorcraft_engine::Session, id: &str, params: &Va /// handles its own way (a Window menu panel item that toggles, …) → `None` to leave it to the /// engine. pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option> { - // The Window menu's panel items toggle like its other checked items (#2361); the panel's - // other ways in (shortcuts, Show Properties Panel, a tool's double-click) only show it. + // Window menu items and the named panel function keys toggle against the checked state; + // other commands that open panels show them. if id == "window.panel" && p.get("toggle").and_then(Value::as_bool) == Some(true) && checked(app, id, p) == Some(true) { let panel = p.get("panel").and_then(Value::as_str).and_then(normalize_panel).unwrap_or_default(); if app.ui.open_panel.as_deref() == Some(panel) { diff --git a/crates/ui-egui/src/panel_docking/tests.rs b/crates/ui-egui/src/panel_docking/tests.rs index 4bb77928b..f72a433cf 100644 --- a/crates/ui-egui/src/panel_docking/tests.rs +++ b/crates/ui-egui/src/panel_docking/tests.rs @@ -63,6 +63,11 @@ impl Harness { self.frame(vec![]); } + fn press(&mut self, key: egui::Key, modifiers: egui::Modifiers) { + self.frame(vec![Event::ModifiersChanged(modifiers), Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers }]); + self.frame(vec![Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers }]); + } + /// Press at `from` and move to `to` in steps, holding the button. fn press_and_move(&mut self, from: Pos2, to: Pos2) { self.frame(vec![Event::PointerMoved(from), button(from, true)]); @@ -497,3 +502,116 @@ fn window_menu_panels_without_a_place_open_in_their_icon_group_or_float() { h.frame(vec![]); assert!(h.tab("openType").is_some()); } + +#[test] +fn f7_hides_and_restores_the_expanded_layers_tab() { + let mut h = Harness::new(); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(showing(&h.app.ui, "layers"), "the first press activates the tab"); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(!showing(&h.app.ui, "layers"), "a second press hides the active tab"); + assert!(h.app.ui.docking_hidden.contains_key("layers")); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(showing(&h.app.ui, "layers"), "the next press restores it"); + assert_eq!(main_panels(&h.layout()), ["properties", "layers", "libraries"]); + + h.app.run("window.panel.collapse", json!({"panel": "layers", "collapsed": true})).unwrap(); + let layout = h.layout(); + let group = dock_columns(&layout).iter().flat_map(|c| &c.groups).find(|g| g.panels.contains(&"layers".to_string())).unwrap(); + assert!(group.minimized); + h.press(egui::Key::F7, egui::Modifiers::NONE); + let layout = h.layout(); + let group = dock_columns(&layout).iter().flat_map(|c| &c.groups).find(|g| g.panels.contains(&"layers".to_string())).unwrap(); + assert!(!group.minimized, "the first press expands a collapsed group"); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(!showing(&h.app.ui, "layers"), "the following press hides the expanded group"); +} + +#[test] +fn f7_activates_a_background_tab_before_hiding_it() { + let mut h = Harness::new(); + h.app.run("window.panel.move", json!({"panel": "layers", "anchor": "properties"})).unwrap(); + h.app.run("window.panel.activate", json!({"panel": "properties"})).unwrap(); + assert!(!showing(&h.app.ui, "layers")); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(showing(&h.app.ui, "layers")); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(!h.layout().contains(&"layers".to_string())); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(showing(&h.app.ui, "layers")); + assert_eq!(group_of(&h.layout(), &"layers".into()), vec!["properties".to_string(), "libraries".into(), "layers".into()]); +} + +#[test] +fn panel_function_keys_toggle_icon_flyouts_and_floating_groups() { + let mut h = Harness::new(); + h.press(egui::Key::F5, egui::Modifiers::NONE); + assert_eq!(h.app.ui.open_panel.as_deref(), Some("brushes")); + h.press(egui::Key::F5, egui::Modifiers::NONE); + assert_eq!(h.app.ui.open_panel, None); + h.press(egui::Key::F5, egui::Modifiers::NONE); + assert_eq!(h.app.ui.open_panel.as_deref(), Some("brushes")); + + h.app.run("window.panel.float", json!({"panel": "layers"})).unwrap(); + h.frame(vec![]); + let rect = floating_window(&h.layout(), "layers").unwrap().rect; + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(floating_window(&h.layout(), "layers").is_none()); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert_eq!(floating_window(&h.layout(), "layers").unwrap().rect, rect); + h.app.run("window.panel.dock", json!({"panel": "layers"})).unwrap(); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(!showing(&h.app.ui, "layers"), "the expanded dock also toggles"); +} + +#[test] +fn other_function_keys_and_keyboard_guards_keep_their_routes() { + let mut h = Harness::new(); + h.app.run("window.panel", json!({"panel": "layers"})).unwrap(); + let keymap = crate::shortcut_editor::current(); + for (chord, command) in [("F8", "ui.symbolOptions"), ("F12", "file.revert")] { + let bound = crate::keymap::vc_chord(chord).and_then(|c| crate::keymap::lookup(&keymap.keymap, &c)).unwrap(); + assert_eq!(bound.id, command); + } + h.app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + h.press(egui::Key::F8, egui::Modifiers::NONE); + assert_eq!(h.app.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some("symbolOptions")); + crate::dialogs::confirm(&mut h.app).unwrap(); + assert_eq!(h.app.session.doc().unwrap().doc.symbols.len(), 1); + crate::menus::invoke(&mut h.app, "view.rotate", json!({})); + assert!(h.app.ui.dialog.is_some()); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(showing(&h.app.ui, "layers"), "F7 does nothing while a dialog is open"); + h.app.ui.dialog = None; + + let field = egui::Id::unique("panel-shortcut-text-focus"); + h.ctx.memory_mut(|m| m.request_focus(field)); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(showing(&h.app.ui, "layers"), "F7 does nothing while a text field has focus"); + h.ctx.memory_mut(|m| m.surrender_focus(field)); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(!showing(&h.app.ui, "layers"), "F7 toggles when the field releases focus"); + + let dir = vectorcraft_testkit::temp_dir("panel-shortcut-revert"); + let path = dir.join(format!("panel-shortcut-revert-{}.vectorcraft", std::process::id())); + let services = crate::Services { + read: Some(Box::new(|p| std::fs::read(p).map_err(|e| e.to_string()))), + write: Some(Box::new(|p, data| std::fs::write(p, data).map_err(|e| e.to_string()))), + ..Default::default() + }; + let mut saved = VectorcraftApp::new(Session::new(), services); + saved.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + saved.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 20, "height": 20})).unwrap(); + saved.run("file.saveAs", json!({"path": path.to_string_lossy()})).unwrap(); + saved.run("shape.rectangle", json!({"x": 40, "y": 40, "width": 20, "height": 20})).unwrap(); + assert_eq!(saved.session.doc().unwrap().doc.node_count(), 3); + let ctx = egui::Context::default(); + let key = Event::Key { key: egui::Key::F12, physical_key: None, pressed: true, repeat: false, modifiers: egui::Modifiers::NONE }; + ctx.run_ui(egui::RawInput { events: vec![key], ..Default::default() }, |ui| crate::shortcuts::handle(&mut saved, ui.ctx())) + .textures_delta + .clear(); + assert_eq!(saved.ui.dialog.as_ref().map(|d| d.kind.as_str()), Some(crate::dialogs::confirm::KIND)); + crate::dialogs::confirm(&mut saved).unwrap(); + assert_eq!(saved.session.doc().unwrap().doc.node_count(), 2, "F12 reloads the saved document"); + std::fs::remove_dir_all(dir).unwrap(); +} diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 9f3a70515..5724f4d41 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -381,7 +381,7 @@ fn single_key_shortcuts(app: &mut VectorcraftApp, ctx: &egui::Context) { None if hit.params.is_null() => { let _ = app.run(&hit.id, json!({})); } - None => crate::menus::invoke(app, &hit.id, hit.params.clone()), + None => crate::menus::invoke(app, &hit.id, shortcut_params(hit)), } } } @@ -729,6 +729,36 @@ fn dispatch(app: &mut VectorcraftApp, ctx: &egui::Context, accept: impl FnMut(&H }) } +/// Params used when a shortcut invokes its command. The named panel function keys retain their +/// toggle action when their bindings are reassigned. +fn shortcut_params(c: &CommandRef) -> serde_json::Value { + let mut params = if c.params.is_null() { json!({}) } else { c.params.clone() }; + if c.id == "window.panel" + && matches!( + params.get("panel").and_then(serde_json::Value::as_str), + Some( + "brushes" + | "color" + | "layers" + | "info" + | "gradient" + | "stroke" + | "attributes" + | "graphicStyles" + | "appearance" + | "align" + | "transform" + | "pathfinder" + | "transparency" + | "symbols" + ) + ) + { + params["toggle"] = json!(true); + } + params +} + /// Runs what a shortcut is bound to: a tool, a panel (`window.panel {panel}`) or a command. fn run_hit(app: &mut VectorcraftApp, ctx: &egui::Context, c: &CommandRef) { if let Some(tool) = c.id.strip_prefix("tool:") { @@ -738,8 +768,7 @@ fn run_hit(app: &mut VectorcraftApp, ctx: &egui::Context, c: &CommandRef) { // every artboard instead of the art. crate::panels::artboards::select_all(app); } else { - let p = if c.params.is_null() { json!({}) } else { c.params.clone() }; - crate::menus::invoke(app, &c.id, p); + crate::menus::invoke(app, &c.id, shortcut_params(c)); } } diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index 62686d34d..703d28f3f 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -1,5 +1,29 @@ use super::*; +#[test] +fn rebound_layers_chord_toggles_and_plain_panel_command_reveals() { + let mut overrides = craft_keymap::Overrides::new(); + crate::keymap::set_override(&mut overrides, "panel:layers", "Cmd+Alt+Shift+F7"); + let keymap = crate::keymap::keymap_with(&overrides); + let old = crate::keymap::vc_chord("F7").unwrap(); + assert!(crate::keymap::lookup(&keymap, &old).is_none(), "the old F7 is free"); + let rebound = crate::keymap::vc_chord("Cmd+Alt+Shift+F7").unwrap(); + let hit = crate::keymap::lookup(&keymap, &rebound).unwrap(); + assert_eq!(hit.id, "window.panel"); + assert_eq!(hit.params["panel"], "layers"); + + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 300})).unwrap(); + app.run("window.panel", json!({"panel": "layers"})).unwrap(); + let ctx = egui::Context::default(); + run_hit(&mut app, &ctx, &hit); + assert_eq!(crate::menus::checked(&app, "window.panel", &json!({"panel": "layers"})), Some(false)); + run_hit(&mut app, &ctx, &hit); + assert_eq!(crate::menus::checked(&app, "window.panel", &json!({"panel": "layers"})), Some(true)); + app.run("window.panel", json!({"panel": "layers"})).unwrap(); + assert_eq!(crate::menus::checked(&app, "window.panel", &json!({"panel": "layers"})), Some(true), "a plain command still reveals"); +} + /// One headless frame delivering `events` to the shortcut handler. fn frame(app: &mut VectorcraftApp, events: Vec) -> egui::FullOutput { let ctx = egui::Context::default(); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index cbf1e4590..4172d15fe 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -611,6 +611,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields. - 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. + - The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency, and Symbols hide an active panel on a second press and restore it on the next press, using the same visibility state as the Window menu. A reassigned chord keeps that behavior. - 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). - While the Type tool edits text, a character typed with AltGr is typed and runs no command. Firefox on Windows reports AltGr as Ctrl and Alt held, so in the web build AltGr+L (ł on a Polish layout) does not also open Layer Options for a new layer, and AltGr+Q (@ on a German layout) does not also run Reset Tracking and Kerning. A Ctrl+Alt chord that types no character runs its command. - 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). From 78b01044fc9dd89978bdb2caf628271d857b19b3 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 30 Oct 2026 10:56:01 -0700 Subject: [PATCH 2/3] Panels: a panel's function key hides and restores its whole group When Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency or Symbols is in front of an expanded dock group or a floating group, its function key now hides the whole group instead of closing only that tab. The next press brings the group back in the same place with every tab and the panel in front, and Window > a hidden sibling also restores the group. A background tab still comes to the front on the first press, and in an icon column the key opens and closes the flyout. Other panel shortcuts and plain window.panel calls keep their behavior. --- crates/ui-egui/src/menus/run.rs | 3 ++ crates/ui-egui/src/panel_docking.rs | 42 ++++++++++++++++++ crates/ui-egui/src/panel_docking/tests.rs | 53 ++++++++++++++++++++++- crates/ui-egui/src/shortcuts.rs | 1 + crates/ui-egui/src/shortcuts/tests.rs | 38 ++++++++++++++++ docs/target-app-parity.md | 2 +- docs/ui-parity.md | 2 +- 7 files changed, 137 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/menus/run.rs b/crates/ui-egui/src/menus/run.rs index f31c50c40..8eff1fc12 100644 --- a/crates/ui-egui/src/menus/run.rs +++ b/crates/ui-egui/src/menus/run.rs @@ -48,6 +48,9 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option Result if !layout.contains(&panel.to_string()) && !app.ui.docking_hidden.contains_key(panel) && crate::panels::library_panel::parse(panel).is_some() { return crate::panels::library_panel::float(app, panel); } + if app.ui.docking_hidden.contains_key(panel) { + return restore_hidden_group(app, panel); + } let value = shared_command(app, "window.panel.activate", &json!({"panel": panel}))?; let layout = layout_of(&app.ui); Ok(match floating_window(&layout, panel) { @@ -345,6 +348,45 @@ fn show_panel(app: &mut VectorcraftApp, params: &Value) -> Result }) } +/// Hides the group holding an active panel and records each tab's return location. +pub(crate) fn hide_group(app: &mut VectorcraftApp, panel: &str) -> Result { + let mut txn = Transaction::begin(app)?; + let panel = panel.to_string(); + let closed = txn.apply(Action::CloseGroup { panel })?; + Ok(txn.commit(app, closed, None)) +} + +/// Reopens the hidden tabs connected to `panel` and brings it to the front. +fn restore_hidden_group(app: &mut VectorcraftApp, panel: &str) -> Result { + let mut txn = Transaction::begin(app)?; + let mut root = panel.to_string(); + for _ in 0..craft_ui::docking::MAX_PANELS { + let Some((_, location)) = txn.workspace.hidden.iter().find(|(id, _)| id == &root) else { break }; + let Some(anchor) = location.anchor.as_ref().filter(|_| matches!(location.placement, Placement::Tab { .. })) else { break }; + if !txn.workspace.hidden.iter().any(|(id, _)| id == anchor) || anchor == &root { + break; + } + root = anchor.clone(); + } + let mut members = vec![root]; + for _ in 0..craft_ui::docking::MAX_PANELS { + let next = txn.workspace.hidden.iter().find_map(|(id, location)| { + (matches!(location.placement, Placement::Tab { .. }) + && location.anchor.as_ref().is_some_and(|anchor| members.contains(anchor)) + && !members.contains(id)) + .then(|| id.clone()) + }); + let Some(next) = next else { break }; + members.push(next); + } + for member in members { + let fallback = first_dock_panel(&txn.workspace.layout, &member); + txn.workspace.set_visible(member, true, fallback, model_permissions).map_err(|e| e.to_string())?; + } + txn.apply(Action::Activate { panel: panel.to_string() })?; + Ok(txn.commit(app, Vec::new(), Some(panel.to_string()))) +} + /// Show `panel` as `window.panel` does, but leave it be when it already shows ([`showing`]): a /// command that opens a panel to work in (Edit › Color Settings…) never puts it away. pub(crate) fn reveal_panel(app: &mut VectorcraftApp, panel: &str) -> Result { diff --git a/crates/ui-egui/src/panel_docking/tests.rs b/crates/ui-egui/src/panel_docking/tests.rs index f72a433cf..fe25133d2 100644 --- a/crates/ui-egui/src/panel_docking/tests.rs +++ b/crates/ui-egui/src/panel_docking/tests.rs @@ -536,10 +536,59 @@ fn f7_activates_a_background_tab_before_hiding_it() { h.press(egui::Key::F7, egui::Modifiers::NONE); assert!(showing(&h.app.ui, "layers")); h.press(egui::Key::F7, egui::Modifiers::NONE); - assert!(!h.layout().contains(&"layers".to_string())); + assert!(["properties", "layers", "libraries"].iter().all(|panel| !h.layout().contains(&panel.to_string()))); h.press(egui::Key::F7, egui::Modifiers::NONE); assert!(showing(&h.app.ui, "layers")); - assert_eq!(group_of(&h.layout(), &"layers".into()), vec!["properties".to_string(), "libraries".into(), "layers".into()]); + assert_eq!(group_of(&h.layout(), &"layers".into()), vec!["properties".to_string(), "layers".into(), "libraries".into()]); +} + +#[test] +fn f7_hides_and_restores_the_whole_expanded_group() { + let mut h = Harness::new(); + h.press(egui::Key::F7, egui::Modifiers::NONE); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(["properties", "layers", "libraries"].iter().all(|panel| !h.layout().contains(&panel.to_string()))); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert_eq!(group_of(&h.layout(), &"layers".into()), ["properties", "layers", "libraries"]); + assert!(showing(&h.app.ui, "layers")); + h.press(egui::Key::F7, egui::Modifiers::NONE); + h.app.run("window.panel", json!({"panel": "properties", "toggle": true})).unwrap(); + assert_eq!(group_of(&h.layout(), &"properties".into()), ["properties", "layers", "libraries"]); + assert!(showing(&h.app.ui, "properties")); +} + +#[test] +fn f7_hides_and_restores_a_floating_tab_group() { + let mut h = Harness::new(); + h.app.run("window.panel.float", json!({"panel": "layers", "group": true})).unwrap(); + h.app.run("window.panel.activate", json!({"panel": "layers"})).unwrap(); + h.frame(vec![]); + let before = h.layout(); + let window = floating_window(&before, "layers").unwrap(); + let rect = window.rect; + assert_eq!(group_of(&before, &"layers".into()), ["properties", "layers", "libraries"]); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert!(floating_window(&h.layout(), "layers").is_none()); + assert!(["properties", "layers", "libraries"].iter().all(|panel| !h.layout().contains(&panel.to_string()))); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert_eq!(floating_window(&h.layout(), "layers").unwrap().rect, rect); + assert_eq!(group_of(&h.layout(), &"layers".into()), ["properties", "layers", "libraries"]); + assert!(showing(&h.app.ui, "layers")); +} + +#[test] +fn f7_toggles_a_multi_tab_icon_flyout() { + let mut h = Harness::new(); + h.app.run("window.collapseDock", json!({"collapsed": true})).unwrap(); + h.frame(vec![]); + assert!(in_iconic_column(&h.layout(), "layers")); + assert_eq!(group_of(&h.layout(), &"layers".into()), ["properties", "layers", "libraries"]); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert_eq!(h.app.ui.open_panel.as_deref(), Some("layers")); + h.press(egui::Key::F7, egui::Modifiers::NONE); + assert_eq!(h.app.ui.open_panel, None); + assert!(in_iconic_column(&h.layout(), "layers")); + assert_eq!(group_of(&h.layout(), &"layers".into()), ["properties", "layers", "libraries"]); } #[test] diff --git a/crates/ui-egui/src/shortcuts.rs b/crates/ui-egui/src/shortcuts.rs index 5724f4d41..00249145d 100644 --- a/crates/ui-egui/src/shortcuts.rs +++ b/crates/ui-egui/src/shortcuts.rs @@ -755,6 +755,7 @@ fn shortcut_params(c: &CommandRef) -> serde_json::Value { ) { params["toggle"] = json!(true); + params["toggleGroup"] = json!(true); } params } diff --git a/crates/ui-egui/src/shortcuts/tests.rs b/crates/ui-egui/src/shortcuts/tests.rs index 703d28f3f..a8f9b08c1 100644 --- a/crates/ui-egui/src/shortcuts/tests.rs +++ b/crates/ui-egui/src/shortcuts/tests.rs @@ -1,5 +1,43 @@ use super::*; +#[test] +fn named_panel_default_chords_toggle_their_panels() { + let keymap = crate::keymap::keymap_with(&craft_keymap::Overrides::new()); + for (panel, chord) in [ + ("brushes", "F5"), + ("color", "F6"), + ("layers", "F7"), + ("graphicStyles", "Shift+F5"), + ("appearance", "Shift+F6"), + ("align", "Shift+F7"), + ("transform", "Shift+F8"), + ("info", "Cmd+F8"), + ("gradient", "Cmd+F9"), + ("pathfinder", "Cmd+Shift+F9"), + ("stroke", "Cmd+F10"), + ("transparency", "Cmd+Shift+F10"), + ("attributes", "Cmd+F11"), + ("symbols", "Cmd+Shift+F11"), + ] { + let chord = crate::keymap::vc_chord(chord).unwrap(); + let hit = crate::keymap::lookup(&keymap, &chord).unwrap(); + assert_eq!(hit.id, "window.panel"); + assert_eq!(hit.params["panel"], panel); + assert_eq!(shortcut_params(&hit)["toggle"], true); + assert_eq!(shortcut_params(&hit)["toggleGroup"], true); + } +} + +#[test] +fn other_panel_shortcuts_keep_plain_panel_params() { + for panel in ["character", "swatches"] { + let hit = CommandRef { id: "window.panel".into(), params: json!({"panel": panel}) }; + let params = shortcut_params(&hit); + assert!(params.get("toggle").is_none(), "{panel} gained toggle"); + assert!(params.get("toggleGroup").is_none(), "{panel} gained toggleGroup"); + } +} + #[test] fn rebound_layers_chord_toggles_and_plain_panel_command_reveals() { let mut overrides = craft_keymap::Overrides::new(); diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4172d15fe..55ad521f9 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -611,7 +611,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Tab shows and hides the panels (in the Type tool it types a tab) without moving the keyboard focus onto a field, so the next shortcut or letter still works; in dialogs and panel fields it moves between fields. - 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. - - The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency, and Symbols hide an active panel on a second press and restore it on the next press, using the same visibility state as the Window menu. A reassigned chord keeps that behavior. + - The function key shortcuts for Brushes, Color, Layers, Info, Gradient, Stroke, Attributes, Graphic Styles, Appearance, Align, Transform, Pathfinder, Transparency, and Symbols activate a background tab on the first press, hide its expanded dock or floating group when that tab is in front, and restore the group with all its tabs together on the next press; in an icon column they open and close the flyout while keeping the icons. A reassigned chord keeps that behavior, and Window > a hidden sibling restores the group. - 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). - While the Type tool edits text, a character typed with AltGr is typed and runs no command. Firefox on Windows reports AltGr as Ctrl and Alt held, so in the web build AltGr+L (ł on a Polish layout) does not also open Layer Options for a new layer, and AltGr+Q (@ on a German layout) does not also run Reset Tracking and Kerning. A Ctrl+Alt chord that types no character runs its command. - 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). diff --git a/docs/ui-parity.md b/docs/ui-parity.md index f56514e57..68f5227a1 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -248,7 +248,7 @@ selected objects, as the Appearance panel's Add New Fill and Add New Stroke butt ~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 -against Illustrator's published default shortcut list; estimated ~70% of it. Remaining: the panel shortcuts, +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, panel-menu commands such as Break Link to Symbol), so they can be given shortcuts (#1539). It lists each button of the From 7a16e302d28593e71002ddb6a3418a93d3b824ec Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Fri, 30 Oct 2026 12:12:31 -0700 Subject: [PATCH 3/3] Panels: a panel's function key restores only the tabs its group hid Hiding a group with a panel's function key records the tabs it closed, and the next press reopens only that set with the panel in front. A tab closed on its own before the group was hidden, or reopened and closed again while the group was hidden, stays closed. The record is saved with the workspace. --- crates/ui-egui/src/panel_docking.rs | 42 ++++++++++------------ crates/ui-egui/src/panel_docking/tests.rs | 38 +++++++++++++++++++- crates/ui-egui/src/panels/library_panel.rs | 7 ++++ crates/ui-egui/src/state.rs | 4 +++ crates/ui-egui/src/workspaces.rs | 5 +++ 5 files changed, 72 insertions(+), 24 deletions(-) diff --git a/crates/ui-egui/src/panel_docking.rs b/crates/ui-egui/src/panel_docking.rs index 54f9666ad..02a95cfd5 100644 --- a/crates/ui-egui/src/panel_docking.rs +++ b/crates/ui-egui/src/panel_docking.rs @@ -353,38 +353,30 @@ pub(crate) fn hide_group(app: &mut VectorcraftApp, panel: &str) -> Result