diff --git a/assets/locales/pending/browser-components-start-closed.json b/assets/locales/pending/browser-components-start-closed.json new file mode 100644 index 0000000000..cc0448afb3 --- /dev/null +++ b/assets/locales/pending/browser-components-start-closed.json @@ -0,0 +1,62 @@ +{ + "en": { + "prefs.browser_components_open": "Components start open", + "prefs.browser_components_open_note": "Every component in the browser starts open, as before, instead of closed until you open it." + }, + "ar": { + "prefs.browser_components_open": "المكوّنات مفتوحة من البداية", + "prefs.browser_components_open_note": "يبدأ كل مكوّن في المستعرض مفتوحًا كما كان من قبل، بدلاً من أن يبقى مغلقًا حتى تفتحه." + }, + "de": { + "prefs.browser_components_open": "Komponenten zu Beginn aufgeklappt", + "prefs.browser_components_open_note": "Jede Komponente im Browser ist wie bisher von Anfang an aufgeklappt, statt zugeklappt zu bleiben, bis du sie aufklappst." + }, + "es": { + "prefs.browser_components_open": "Componentes abiertos al inicio", + "prefs.browser_components_open_note": "Cada componente del explorador empieza abierto, como antes, en lugar de cerrado hasta que lo abras." + }, + "fr": { + "prefs.browser_components_open": "Composants dépliés au départ", + "prefs.browser_components_open_note": "Chaque composant du navigateur commence déplié, comme avant, au lieu de rester replié jusqu'à ce que vous le dépliiez." + }, + "he": { + "prefs.browser_components_open": "רכיבים פתוחים מההתחלה", + "prefs.browser_components_open_note": "כל רכיב בעץ התכן מתחיל פתוח, כמו קודם, במקום סגור עד שתפתח אותו." + }, + "it": { + "prefs.browser_components_open": "Componenti aperti all'inizio", + "prefs.browser_components_open_note": "Ogni componente nel browser inizia aperto, come prima, invece che chiuso finché non lo apri." + }, + "ja": { + "prefs.browser_components_open": "コンポーネントを最初から展開", + "prefs.browser_components_open_note": "ブラウザーのすべてのコンポーネントを以前と同じく最初から展開して表示します。オフの場合は展開するまで折りたたまれています。" + }, + "ko": { + "prefs.browser_components_open": "구성요소를 펼친 상태로 시작", + "prefs.browser_components_open_note": "브라우저의 모든 구성요소가 이전처럼 펼쳐진 상태로 시작합니다. 끄면 직접 펼칠 때까지 접혀 있습니다." + }, + "pt-BR": { + "prefs.browser_components_open": "Componentes começam abertos", + "prefs.browser_components_open_note": "Cada componente no navegador começa aberto, como antes, em vez de fechado até que você o abra." + }, + "ru": { + "prefs.browser_components_open": "Компоненты развёрнуты с начала", + "prefs.browser_components_open_note": "Каждый компонент в браузере сразу развёрнут, как раньше, а не свёрнут, пока вы его не развернёте." + }, + "uk": { + "prefs.browser_components_open": "Компоненти розгорнуті з початку", + "prefs.browser_components_open_note": "Кожен компонент у браузері одразу розгорнутий, як раніше, а не згорнутий, доки ви його не розгорнете." + }, + "vi": { + "prefs.browser_components_open": "Thành phần mở sẵn", + "prefs.browser_components_open_note": "Mọi thành phần trong trình duyệt đều mở sẵn như trước, thay vì đóng cho đến khi bạn mở." + }, + "zh": { + "prefs.browser_components_open": "组件初始展开", + "prefs.browser_components_open_note": "浏览器中的每个组件都像以前一样初始展开,而不是在您展开前保持折叠。" + }, + "zh-TW": { + "prefs.browser_components_open": "元件初始展開", + "prefs.browser_components_open_note": "瀏覽器中的每個元件都像以前一樣初始展開,而不是在您展開前保持摺疊。" + } +} diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index cb7c09c989..cdf7700633 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -53,10 +53,10 @@ pub(crate) fn reveal_body(app: &mut SolveApp, body: &str) { }; app.tree.collapsed.remove("c0"); for depth in 1..=path.len() { - app.tree.collapsed.remove(&occurrence_key(&path[..depth])); + app.tree.open_row(&occurrence_key(&path[..depth])); } let scope = if path.is_empty() { format!("c{component}") } else { occurrence_key(&path) }; - app.tree.collapsed.remove(&scope); + app.tree.open_row(&scope); app.tree.collapsed.remove(&format!("{scope}/bodies")); open_group_chain(app, &scope, component, "bodies", &source); } @@ -200,6 +200,25 @@ impl TreeState { self.picked_folder = None; } + /// Open a row. A placement's row is noted open in `expanded` as well, because it starts + /// closed unless the "Components start open" preference is on (#9274). + pub(crate) fn open_row(&mut self, key: &str) { + self.collapsed.remove(key); + if is_placement_key(key) && !self.expanded.contains(key) { + self.expanded.insert(key.to_owned()); + } + } + + /// Close a row, whichever way its placement starts. + pub(crate) fn close_row(&mut self, key: &str) { + if !self.collapsed.contains(key) { + self.collapsed.insert(key.to_owned()); + } + if is_placement_key(key) { + self.expanded.remove(key); + } + } + fn remember(&mut self, key: &str) { if let Some(slot) = self.order.get_mut(self.order_len) { if slot != key { @@ -694,12 +713,32 @@ fn reveal_path(app: &mut SolveApp) -> Option> { app.tree.model.as_mut()?.body_path(&app.session, &body) } +/// A placement's own row (`o:1/2`), not a folder under it (`o:1/2/bodies`). +pub(crate) fn is_placement_key(key: &str) -> bool { + key.strip_prefix("o:").is_some_and(|p| !p.is_empty() && p.bytes().all(|b| b.is_ascii_digit() || b == b'/')) +} + +/// Whether a placement's row is open. Placements start closed, so a large assembly opens as a +/// short list (#9274); the "Components start open" preference keeps them open as before. +pub(crate) fn placement_open(app: &SolveApp, key: &str) -> bool { + !app.tree.collapsed.contains(key) && (app.preferences.browser_components_open || app.tree.expanded.contains(key)) +} + pub(crate) fn is_open(app: &SolveApp, key: &str, default: bool) -> bool { + if is_placement_key(key) { + return placement_open(app, key); + } if default { !app.tree.collapsed.contains(key) } else { app.tree.expanded.contains(key) } } pub(crate) fn toggle(app: &mut SolveApp, key: &str, default: bool) { - if default { + if is_placement_key(key) { + if placement_open(app, key) { + app.tree.close_row(key); + } else { + app.tree.open_row(key); + } + } else if default { if !app.tree.collapsed.remove(key) { app.tree.collapsed.insert(key.to_string()); } @@ -709,18 +748,19 @@ pub(crate) fn toggle(app: &mut SolveApp, key: &str, default: bool) { } /// Apply Expand All / Collapse All: open-by-default keys live in `collapsed` when closed; -/// closed-by-default keys live in `expanded` when open. +/// closed-by-default keys live in `expanded` when open. Placements, whichever way they start, +/// are noted in both. pub fn apply_fold(app: &mut SolveApp, open: bool, open_by_default: &[String], closed_by_default: &[String]) { if open { for key in open_by_default { - app.tree.collapsed.remove(key); + app.tree.open_row(key); } for key in closed_by_default { app.tree.expanded.insert(key.clone()); } } else { for key in open_by_default { - app.tree.collapsed.insert(key.clone()); + app.tree.close_row(key); } for key in closed_by_default { app.tree.expanded.remove(key); @@ -1595,7 +1635,7 @@ fn occurrence_key(path: &[u64]) -> String { /// Show a placement's row closed: a pasted (or moved) component arrives folded, whatever a row /// at that path showed before. pub(crate) fn close_placement(app: &mut SolveApp, path: &[u64]) { - app.tree.collapsed.insert(occurrence_key(path)); + app.tree.close_row(&occurrence_key(path)); } /// A placement, rather than a component definition: repeated child IDs remain distinct @@ -1618,7 +1658,7 @@ fn occurrence_rows_inner( ) { let key = &placement.key; if reveal_path(app).is_some_and(|rp| rp.starts_with(path)) { - app.tree.collapsed.remove(key); + app.tree.open_row(key); } let open = is_open(app, key, true); let on_screen = ui.is_rect_visible(row_rect(ui)); diff --git a/crates/ui-egui/src/browser/features.rs b/crates/ui-egui/src/browser/features.rs index 59aeeee4f4..f4448ede6c 100644 --- a/crates/ui-egui/src/browser/features.rs +++ b/crates/ui-egui/src/browser/features.rs @@ -41,10 +41,10 @@ pub(crate) fn reveal(app: &mut SolveApp, id: u64, body: &str) { app.tree.reveal = None; app.tree.collapsed.remove("c0"); for depth in 1..=path.len() { - app.tree.collapsed.remove(&occurrence_key(&path[..depth])); + app.tree.open_row(&occurrence_key(&path[..depth])); } let scope = if path.is_empty() { format!("c{component}") } else { occurrence_key(&path) }; - app.tree.collapsed.remove(&scope); + app.tree.open_row(&scope); app.tree.expanded.insert(format!("{scope}/features")); app.tree.reveal_feature = Some(Reveal { id, path }); } @@ -731,6 +731,8 @@ mod tests { #[test] fn only_folded_features_folders_on_screen_work_out_their_mark() { let mut h = Harness::new(); + // The folders sit inside placements, which start closed (#9274). + h.app.preferences.browser_components_open = true; let component = h.app.run("component.create", json!({"name":"Part"})).unwrap(); let feature = h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Plate"})).unwrap()["feature"].as_u64().unwrap(); h.app.run("component.activate", json!({"component":0})).unwrap(); diff --git a/crates/ui-egui/src/browser_card_tests.rs b/crates/ui-egui/src/browser_card_tests.rs index e0465f149e..62f1e2fd03 100644 --- a/crates/ui-egui/src/browser_card_tests.rs +++ b/crates/ui-egui/src/browser_card_tests.rs @@ -78,6 +78,8 @@ fn f2_on_a_clicked_browser_group_renames_the_group() { h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Body1"})).unwrap(); (h, 0) }; + // The group's row sits inside a placement, which starts closed (#9274). + h.app.preferences.browser_components_open = true; let body = h.app.session.model.state().bodies[0].name.clone(); let group = h.app.run("browser.group", json!({"component":component,"folder":"bodies","items":[body],"name":"Group"})).unwrap()["group"] .as_u64() @@ -790,6 +792,8 @@ fn browser_occurrence_clicks_fill_align_components_input() { #[test] fn row_eyes_line_up_at_the_right_end_unless_the_preference_keeps_them_left() { let mut h = Harness::new(); + // These rows are read with every placement open, as placements started before #9274. + h.app.preferences.browser_components_open = true; h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Top"})).unwrap(); let part = h.app.run("component.create", json!({"name":"Part"})).unwrap()["occurrence"].as_u64().unwrap(); h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"corner":[20,0,0],"body_name":"Inner"})).unwrap(); diff --git a/crates/ui-egui/src/browser_model_tests.rs b/crates/ui-egui/src/browser_model_tests.rs index 167255ba5c..c58e932535 100644 --- a/crates/ui-egui/src/browser_model_tests.rs +++ b/crates/ui-egui/src/browser_model_tests.rs @@ -500,3 +500,58 @@ fn a_design_saved_before_nested_groups_keeps_its_browser_order() { assert_eq!(drawn(&rows), expected, "groups {groups:?}, saved order {saved:?}"); } } + +/// #9274: placements start closed, so a large assembly opens as a short list. Opening one shows +/// its own placements still closed, and closing and reopening it leaves them as they were. +/// Expand All opens them, and the "Components start open" preference opens every one as before. +#[test] +fn placements_start_closed_and_the_preference_opens_them_as_before() { + fn drawn(h: &mut crate::scenario::Harness) -> Vec<(Vec, bool)> { + h.frames(2); + super::drawn_occurrences().into_iter().map(|(path, _, _, open)| (path, open)).collect() + } + let mut h = crate::scenario::Harness::new(); + h.app.ui.show_browser = true; + let sub = h.app.run("component.create", json!({"name":"Sub"})).unwrap(); + let outer = vec![sub["occurrence"].as_u64().unwrap()]; + let leaf = h.app.run("component.create", json!({"name":"Leaf"})).unwrap()["occurrence"].as_u64().unwrap(); + h.app.run("solid.box", json!({"length":10,"width":10,"height":10})).unwrap(); + h.app.run("component.activate", json!({"component":0})).unwrap(); + let inner = vec![outer[0], leaf]; + let fold = format!("fold:occurrence:{}", outer[0]); + + assert_eq!(drawn(&mut h), [(outer.clone(), false)], "only the top placement is listed, closed"); + click(&mut h, &fold); + assert_eq!(drawn(&mut h), [(outer.clone(), true), (inner.clone(), false)], "the placement inside stays closed"); + click(&mut h, &fold); + assert_eq!(drawn(&mut h), [(outer.clone(), false)]); + click(&mut h, &fold); + assert_eq!(drawn(&mut h), [(outer.clone(), true), (inner.clone(), false)], "reopening shows it as it was"); + + h.app.run("browser.expand_all", json!({})).unwrap(); + assert_eq!(drawn(&mut h), [(outer.clone(), true), (inner.clone(), true)], "Expand All opens every placement"); + h.app.run("browser.collapse_all", json!({})).unwrap(); + assert_eq!(drawn(&mut h), [(outer.clone(), false)], "Collapse All closes them again"); + + h.app.tree.collapsed.clear(); + h.app.tree.expanded.clear(); + h.call("ui.prefs", json!({"browser_components_open": true})); + assert!(h.app.preferences.browser_components_open); + assert_eq!(drawn(&mut h), [(outer.clone(), true), (inner, true)], "the preference opens every placement, as before #9274"); + click(&mut h, &fold); + assert_eq!(drawn(&mut h), [(outer, false)], "and its arrow still closes one"); +} + +/// #9274: Find in Browser opens the placements, closed from the start, down to the body's row. +#[test] +fn find_in_browser_opens_placements_that_start_closed() { + let (mut h, _, occurrence) = fixture(); + h.app.ui.show_browser = true; + let body = super::model_placement(&mut h.app, &[occurrence]).unwrap().bodies[0].world.clone(); + h.frames(2); + assert!(!super::is_open(&h.app, &super::occurrence_key(&[occurrence]), true), "the placement starts closed"); + super::reveal_body(&mut h.app, &body); + h.frames(3); + assert!(super::is_open(&h.app, &super::occurrence_key(&[occurrence]), true)); + assert!(h.app.tree.order.contains(&format!("b:{body}")), "the body's row is drawn"); +} diff --git a/crates/ui-egui/src/joint_browser_tests.rs b/crates/ui-egui/src/joint_browser_tests.rs index d88f00a9ae..e20bf1fbf5 100644 --- a/crates/ui-egui/src/joint_browser_tests.rs +++ b/crates/ui-egui/src/joint_browser_tests.rs @@ -15,6 +15,8 @@ fn harness() -> Harness { let mut h = Harness::new(); h.app.home.open = false; h.app.ui.show_browser = true; + // The tests read the Joints folders inside placements, which start closed (#9274). + h.app.preferences.browser_components_open = true; crate::test_support::grounded_boxes(&[("A", 0), ("B", 30), ("C", 60)], |id, p| h.app.run(id, p).unwrap()); h.app .run("joint.create", json!({"type": "revolute", "name": "Hinge", "a": {"occurrence": "A:1", "face": [10, 10, 10]}, "b": {"occurrence": "B:1", "face": [40, 10, 0]}})) diff --git a/crates/ui-egui/src/occurrence_ui_tests.rs b/crates/ui-egui/src/occurrence_ui_tests.rs index 593be1132f..e1bfac4d9b 100644 --- a/crates/ui-egui/src/occurrence_ui_tests.rs +++ b/crates/ui-egui/src/occurrence_ui_tests.rs @@ -6,6 +6,8 @@ use solidcraft_engine::Sel; #[test] fn replaced_body_name_uses_evaluated_owner_in_browser_and_component_actions() { let mut h = Harness::new(); + // These rows are read with every placement open, as placements started before #9274. + h.app.preferences.browser_components_open = true; h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Block"})).unwrap(); let transferred = h.app.run("component.from_bodies", json!({"bodies":["Block"]})).unwrap(); let owner = transferred["components"][0]["component"].as_u64().unwrap(); @@ -30,6 +32,8 @@ fn replaced_body_name_uses_evaluated_owner_in_browser_and_component_actions() { fn fixture() -> (Harness, u64, u64, u64) { let (mut h, component, first) = crate::test_support::component_box(); let second = h.app.run("occurrence.copy", json!({"occurrence":first,"translate":[50,0,0]})).unwrap()["occurrence"].as_u64().unwrap(); + // These rows are read with every placement open, as placements started before #9274. + h.app.preferences.browser_components_open = true; h.frames(3); (h, component, first, second) } @@ -305,6 +309,8 @@ fn occurrence_body_appearance_editor_inherits_definition_precision() { #[test] fn occurrence_nested_shared_child_has_distinct_rows_and_scope() { let mut h = Harness::new(); + // These rows are read with every placement open, as placements started before #9274. + h.app.preferences.browser_components_open = true; let parent = h.app.run("component.create", json!({"name":"Assembly"})).unwrap(); let parent_component = parent["component"].as_u64().unwrap(); let parent_occurrence = parent["occurrence"].as_u64().unwrap(); @@ -362,6 +368,8 @@ fn occurrence_distance_editor_reads_inches_and_keeps_four_digits_in_model_space( #[test] fn a_body_dropped_on_a_bodies_folder_moves_into_that_component() { let mut h = Harness::new(); + // These rows are read with every placement open, as placements started before #9274. + h.app.preferences.browser_components_open = true; h.app.home.open = false; h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Plate"})).unwrap(); h.app.run("solid.box", json!({"corner":[20,0,0],"length":5,"width":5,"height":5,"body_name":"Lid"})).unwrap(); @@ -459,6 +467,8 @@ fn bodies_folders_fold_by_the_preference_but_never_the_designs_or_a_hidden_one() fn a_folded_body_sits_one_level_under_its_nested_placement() { use crate::prefs::BodiesFolders; let mut h = Harness::new(); + // These rows are read with every placement open, as placements started before #9274. + h.app.preferences.browser_components_open = true; let parent = h.app.run("component.create", json!({"name":"Assembly"})).unwrap()["occurrence"].as_u64().unwrap(); let child = h.app.run("component.create", json!({"name":"Child"})).unwrap()["occurrence"].as_u64().unwrap(); h.app.run("solid.box", json!({"length":10,"width":10,"height":10,"body_name":"Pin"})).unwrap(); @@ -536,6 +546,8 @@ fn a_folded_folders_menu_items_move_to_the_placements_menu() { #[test] fn dragging_an_occurrence_row_onto_another_moves_it_into_that_component() { let mut h = Harness::new(); + // These rows are read with every placement open, as placements started before #9274. + h.app.preferences.browser_components_open = true; // Component A with a body, at the design root. let a = h.app.run("component.create", json!({"name":"A"})).unwrap(); let (_a_comp, a_occ) = (a["component"].as_u64().unwrap(), a["occurrence"].as_u64().unwrap()); diff --git a/crates/ui-egui/src/prefs.rs b/crates/ui-egui/src/prefs.rs index 97bcface5b..7b958aa011 100644 --- a/crates/ui-egui/src/prefs.rs +++ b/crates/ui-egui/src/prefs.rs @@ -1303,6 +1303,9 @@ preference_settings! { /// Each tree row's eye between its fold arrow and its radio, as before #8718, instead of in a /// column at the rows' right end. Off by default. browser_eye_left: bool = false, PAGE_LAYOUT => check_id("prefs.browser_eye_left"); + /// Every component placement in the browser starts open, as before #9274, instead of closed + /// until the user opens it. Off by default. + browser_components_open: bool = false, PAGE_LAYOUT => check_id("prefs.browser_components_open"); /// The wheel zooms toward the point under the cursor (else toward the view's centre). zoom_to_cursor: bool = true, PAGE_NAVIGATION => check("Zoom to the cursor", "The wheel zooms toward the point under the cursor (off: toward the view's center)"); /// Speed multiplier for wheel zoom, independent of the 3D mouse. @@ -5783,6 +5786,7 @@ mod tests { ("view_confirm_corner", json!(true), PAGE_LAYOUT), ("status_bar", json!(true), PAGE_LAYOUT), ("browser_eye_left", json!(true), PAGE_LAYOUT), + ("browser_components_open", json!(true), PAGE_LAYOUT), ("quick_measure", json!(true), PAGE_GRAPHICS), ("origin_plane_names", json!("top_front_right"), PAGE_DESIGN), ("direct_interaction", json!(true), PAGE_TOOLS), diff --git a/crates/ui-egui/src/prefs/interface.rs b/crates/ui-egui/src/prefs/interface.rs index 06e7e8e559..92421bac76 100644 --- a/crates/ui-egui/src/prefs/interface.rs +++ b/crates/ui-egui/src/prefs/interface.rs @@ -341,6 +341,11 @@ pub(super) fn layout(app: &mut SolveApp, ui: &mut egui::Ui) { app.preferences.browser_bodies_folders = *mode; } }); + // Placements open in the browser from the start, as before #9274. + ui_kit::row(ui, "", |ui| { + let check = setting::browser_components_open(app, ui).on_hover_text(app.tr("prefs.browser_components_open_note")); + crate::scenario::publish_target("prefs:browser_components_open", check.rect); + }); // The eye beside the fold arrow, where it was before #8718. ui_kit::row(ui, "", |ui| { let check = setting::browser_eye_left(app, ui).on_hover_text(app.tr("prefs.browser_eye_left_note")); diff --git a/crates/ui-egui/tests/icon_audit.rs b/crates/ui-egui/tests/icon_audit.rs index 760c81a579..951a28be5d 100644 --- a/crates/ui-egui/tests/icon_audit.rs +++ b/crates/ui-egui/tests/icon_audit.rs @@ -10,6 +10,7 @@ use solidcraft_ui_egui::{icon_svg, scenario::Harness, workspace}; fn harness() -> Harness { let mut h = Harness::new(); h.app.preferences.auto_operation = false; + h.app.preferences.browser_components_open = true; h } diff --git a/crates/ui-egui/tests/scenarios/browser_bodies_folders.json b/crates/ui-egui/tests/scenarios/browser_bodies_folders.json index b45898baa2..465ecb0393 100644 --- a/crates/ui-egui/tests/scenarios/browser_bodies_folders.json +++ b/crates/ui-egui/tests/scenarios/browser_bodies_folders.json @@ -1,4 +1,5 @@ [ + {"call": "ui.prefs", "params": {"browser_components_open": true}}, {"note": "Bodies folders (#4217, Preferences › Interface): a nested assembly of plain boxes. Frame:1 holds the body Plate and the placement Pin:1, which holds the body Shaft. The design itself has no bodies, so every Bodies row is a component's."}, {"note": "Each row's indent handle is where its depth puts it, so their order shows each row's depth."}, {"call": "engine.execute", "params": {"command": "component.create", "params": {"name": "Frame"}}}, diff --git a/crates/ui-egui/tests/scenarios/browser_chevrons.json b/crates/ui-egui/tests/scenarios/browser_chevrons.json index 5a6b36a8f0..4467dc9973 100644 --- a/crates/ui-egui/tests/scenarios/browser_chevrons.json +++ b/crates/ui-egui/tests/scenarios/browser_chevrons.json @@ -1,4 +1,5 @@ [ + {"call": "ui.prefs", "params": {"browser_components_open": true}}, { "note": "A design with every kind of browser folder: bodies in a group, a component, a sketch, a construction plane, a canvas and named views." }, diff --git a/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json b/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json index 786335986e..764cfd59a4 100644 --- a/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json +++ b/crates/ui-egui/tests/scenarios/s37_browser_chevrons.json @@ -1,4 +1,5 @@ [ + {"call": "ui.prefs", "params": {"browser_components_open": true}}, { "note": "Browser chevrons expand and collapse every kind of node (folders, components, groups, the origin, named views, document settings). Model: two components with a body each, a joint, a sketch, an offset plane, a named view." }, diff --git a/crates/ui-egui/tests/scenarios/s61_occurrence_browser.json b/crates/ui-egui/tests/scenarios/s61_occurrence_browser.json index d5f3aa102e..b344672a97 100644 --- a/crates/ui-egui/tests/scenarios/s61_occurrence_browser.json +++ b/crates/ui-egui/tests/scenarios/s61_occurrence_browser.json @@ -11,6 +11,8 @@ {"expect":{"shown":"(55.00, 5.00, 5.00) mm","query":{"command":"occurrence.properties","params":{"occurrence":4,"path":[4]},"path":["total","center_of_mass",0],"value":55}}}, {"shot":"second_occurrence_properties"}, {"click":{"handle":"eye:occurrence:4"}}, + {"note":"Placements start closed (#9274): the fold arrow opens the row, then folds it again."}, + {"click":{"handle":"fold:occurrence:4"}}, {"click":{"handle":"fold:occurrence:4"}}, {"shot":"second_occurrence_hidden_folded"}, {"click":{"handle":"eye:occurrence:4"}}, diff --git a/docs/book/src/guide/ui-tour.md b/docs/book/src/guide/ui-tour.md index 20f876f23a..2462420015 100644 --- a/docs/book/src/guide/ui-tour.md +++ b/docs/book/src/guide/ui-tour.md @@ -28,7 +28,10 @@ puts it back beside the fold arrow; press an eye and drag up or down over other eyes to show or hide them all at once, as one undo step), a component's radio button makes it the active component, and right-clicking any row gives that item's menu. Right-click the design or a component and choose **Expand All** or **Collapse All** to open or close every folder under that row (the design row stays visible - when the whole tree is collapsed). Rows drag into groups and other components. **Preferences › + when the whole tree is collapsed). Components start closed, so a large assembly opens as a + short list; open one with its fold arrow, or use **Find in Browser** on a body to open the rows + down to it (**Preferences › Interface › Components start open** opens every component from the + start, as before). Rows drag into groups and other components. **Preferences › Interface › Bodies folders** sets when a component's bodies sit in a Bodies folder of their own: **Several bodies only** (the default) lists a component's body straight under it when it has only one and no groups, **Never** lists every component's bodies and groups straight under it diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 834d85d3d9..b3e95e8e0b 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -468,6 +468,7 @@ One setting per line, sorted by name. A change that adds a setting adds its own - `assembly_move_handles` (default true): explicit Assembly Move uses the permitted joint-motion handles; false keeps the previous Move panel. - `auto_operation` (default true: feature windows choose Join, New Body or Cut from what their material touches until one is picked; false: each keeps the operation it opens with, Extrude its own Join or Cut) - `browser_bodies_folders` (`several` (default): a component with one body and no groups lists it straight under the component in the browser, without a Bodies folder; `always`; `never`: components list their bodies and groups straight under them, and the folder's menu items are on the component's menu; the design's own Bodies folder, and one hidden with its eye, always show; #4217) +- `browser_components_open` (default false: each component placement in the browser starts closed until it is opened, by its fold arrow, Expand All or Find in Browser; true: every placement starts open, as before #9274) - `browser_eye_left` (default false: each browser and History tree row's eye, handle `eye:`, sits in a column at the rows' right end, clear of the fold arrow and the radio; true: between them, as before #8718) - `check_updates` (default true; [updates](book/src/guide/updates.md)) - `color_vision` (`off` (default), `protan`, `deutan`, `tritan`: colour vision modes, #778)