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.
This commit is contained in:
dotCooCoo
2026-10-31 02:52:26 +07:00
committed by bflatastic
parent a535d7b890
commit 78b01044fc
7 changed files with 137 additions and 4 deletions
+3
View File
@@ -48,6 +48,9 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
app.ui.open_panel = None;
return Some(Ok(json!({"open": null})));
}
if p.get("toggleGroup").and_then(Value::as_bool) == Some(true) {
return Some(crate::panel_docking::hide_group(app, panel));
}
return Some(app.run("window.panel.close", json!({"panel": panel})));
}
if let Some(result) = crate::panel_docking::command(app, id, p) {
+42
View File
@@ -337,6 +337,9 @@ fn show_panel(app: &mut VectorcraftApp, params: &Value) -> Result<Value, String>
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<Value, String>
})
}
/// 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<Value, String> {
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<Value, String> {
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<Value, String> {
+51 -2
View File
@@ -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]
+1
View File
@@ -755,6 +755,7 @@ fn shortcut_params(c: &CommandRef) -> serde_json::Value {
)
{
params["toggle"] = json!(true);
params["toggleGroup"] = json!(true);
}
params
}
+38
View File
@@ -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();
+1 -1
View File
@@ -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).
+1 -1
View File
@@ -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