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.
This commit is contained in:
dotCooCoo
2026-10-31 02:52:26 +07:00
committed by bflatastic
parent 78b01044fc
commit 7a16e302d2
5 changed files with 72 additions and 24 deletions
+19 -23
View File
@@ -353,38 +353,30 @@ pub(crate) fn hide_group(app: &mut VectorcraftApp, panel: &str) -> Result<Value,
let mut txn = Transaction::begin(app)?;
let panel = panel.to_string();
let closed = txn.apply(Action::CloseGroup { panel })?;
Ok(txn.commit(app, closed, None))
let group = closed.clone();
let result = txn.commit(app, closed, None);
if !group.is_empty() {
app.ui.docking_hidden_groups.push(group);
}
Ok(result)
}
/// Reopens the hidden tabs connected to `panel` and brings it to the front.
/// Reopens the tabs hidden together with `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);
}
let members =
app.ui.docking_hidden_groups.iter().find(|group| group.iter().any(|id| id == panel)).cloned().unwrap_or_else(|| vec![panel.to_string()]);
for member in members {
if !txn.workspace.hidden.iter().any(|(id, _)| id == &member) {
continue;
}
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())))
let result = txn.commit(app, Vec::new(), Some(panel.to_string()));
app.ui.docking_hidden_groups.retain(|group| !group.iter().any(|id| id == panel));
Ok(result)
}
/// Show `panel` as `window.panel` does, but leave it be when it already shows ([`showing`]): a
@@ -969,6 +961,10 @@ impl Transaction {
}
}
let reveal = reveal.filter(|panel| layout.contains(panel));
for group in &mut app.ui.docking_hidden_groups {
group.retain(|panel| hidden.contains_key(panel) && !layout.contains(panel));
}
app.ui.docking_hidden_groups.retain(|group| !group.is_empty());
let flyout = reveal.as_ref().filter(|panel| in_iconic_column(&layout, panel)).cloned();
match (app.ui.docking.is_none() && hidden.is_empty()).then(|| default_flags(&app.ui, &layout)).flatten() {
Some((tab, collapsed, width)) => {
+37 -1
View File
@@ -532,6 +532,7 @@ 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();
let before = group_of(&h.layout(), &"layers".into());
assert!(!showing(&h.app.ui, "layers"));
h.press(egui::Key::F7, egui::Modifiers::NONE);
assert!(showing(&h.app.ui, "layers"));
@@ -539,7 +540,7 @@ fn f7_activates_a_background_tab_before_hiding_it() {
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(), "layers".into(), "libraries".into()]);
assert_eq!(group_of(&h.layout(), &"layers".into()), before);
}
#[test]
@@ -557,6 +558,41 @@ fn f7_hides_and_restores_the_whole_expanded_group() {
assert!(showing(&h.app.ui, "properties"));
}
#[test]
fn f7_restores_only_tabs_hidden_with_the_group() {
let mut h = Harness::new();
h.app.run("window.panel.close", json!({"panel": "libraries"})).unwrap();
assert!(h.app.ui.docking_hidden.contains_key("libraries"));
h.press(egui::Key::F7, egui::Modifiers::NONE);
h.press(egui::Key::F7, egui::Modifiers::NONE);
assert!(["properties", "layers"].iter().all(|panel| h.app.ui.docking_hidden.contains_key(*panel)));
h.press(egui::Key::F7, egui::Modifiers::NONE);
assert_eq!(group_of(&h.layout(), &"layers".into()), ["properties", "layers"]);
assert!(!h.layout().contains(&"libraries".to_string()));
assert!(showing(&h.app.ui, "layers"));
}
#[test]
fn independently_reopened_tab_is_not_restored_by_the_group_key() {
let mut h = Harness::new();
h.press(egui::Key::F7, egui::Modifiers::NONE);
assert!(showing(&h.app.ui, "layers"));
h.press(egui::Key::F7, egui::Modifiers::NONE);
assert!(["properties", "layers", "libraries"].iter().all(|panel| h.app.ui.docking_hidden.contains_key(*panel)));
assert!(["properties", "layers", "libraries"].iter().all(|panel| !h.layout().contains(&panel.to_string())));
h.app.run("window.panel.activate", json!({"panel": "libraries"})).unwrap();
assert!(h.layout().contains(&"libraries".to_string()));
assert!(!h.app.ui.docking_hidden.contains_key("libraries"));
assert!(["properties", "layers"].iter().all(|panel| h.app.ui.docking_hidden.contains_key(*panel)));
h.app.run("window.panel.close", json!({"panel": "libraries"})).unwrap();
assert!(h.app.ui.docking_hidden.contains_key("libraries"));
assert!(!h.layout().contains(&"libraries".to_string()));
h.press(egui::Key::F7, egui::Modifiers::NONE);
assert_eq!(group_of(&h.layout(), &"layers".into()), ["properties", "layers"]);
assert!(!h.layout().contains(&"libraries".to_string()));
assert!(showing(&h.app.ui, "layers"));
}
#[test]
fn f7_hides_and_restores_a_floating_tab_group() {
let mut h = Harness::new();
@@ -381,6 +381,13 @@ fn rename(ui: &mut UiState, from: &str, to: &str) -> Result<(), String> {
}
ui.docking = docking;
ui.docking_hidden = hidden;
for group in &mut ui.docking_hidden_groups {
for panel in group {
if panel == from {
*panel = to.to_string();
}
}
}
if ui.open_panel.as_deref() == Some(from) {
ui.open_panel = Some(to.into());
}
+4
View File
@@ -382,6 +382,9 @@ pub struct UiState {
pub docking: Option<craft_ui::docking::Layout<String>>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub docking_hidden: std::collections::BTreeMap<String, craft_ui::docking::Location<String>>,
/// Tabs closed together by panel shortcuts, kept with their workspace return locations.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docking_hidden_groups: Vec<Vec<String>>,
/// Panels dropped on the icon column, in older files (read only, like [`Self::floating_panels`]).
#[serde(default, skip_serializing)]
pub docking_icons: std::collections::BTreeSet<String>,
@@ -838,6 +841,7 @@ impl Default for UiState {
floating_panels: vec![],
docking: None,
docking_hidden: Default::default(),
docking_hidden_groups: Vec::new(),
docking_icons: Default::default(),
docking_collapsed: Default::default(),
docking_generation: 0,
+5
View File
@@ -43,6 +43,9 @@ pub struct Workspace {
pub docking: Option<craft_ui::docking::Layout<String>>,
#[serde(default, skip_serializing_if = "std::collections::BTreeMap::is_empty")]
pub docking_hidden: std::collections::BTreeMap<String, craft_ui::docking::Location<String>>,
/// Tabs closed together by a panel shortcut.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub docking_hidden_groups: Vec<Vec<String>>,
/// Panels dropped on the icon column, in older workspaces (read only).
#[serde(default, skip_serializing)]
pub docking_icons: std::collections::BTreeSet<String>,
@@ -184,6 +187,7 @@ pub fn capture(ui: &UiState, name: &str) -> Workspace {
floating_panels: Vec::new(),
docking: ui.docking.clone(),
docking_hidden: ui.docking_hidden.clone(),
docking_hidden_groups: ui.docking_hidden_groups.clone(),
docking_icons: Default::default(),
docking_collapsed: Default::default(),
toolbar_pos: ui.toolbar_pos,
@@ -209,6 +213,7 @@ pub fn apply(ui: &mut UiState, w: &Workspace) {
ui.docking_generation = ui.docking_generation.wrapping_add(1);
ui.docking = w.docking.clone().filter(crate::panel_docking::valid);
ui.docking_hidden = w.docking_hidden.clone();
ui.docking_hidden_groups = w.docking_hidden_groups.clone();
ui.toolbar_pos = w.toolbar_pos;
ui.dock_width = w.dock_width;
crate::state::sanitize_toolbar_pos(&mut ui.toolbar_pos);