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