diff --git a/crates/ui-egui/src/browser.rs b/crates/ui-egui/src/browser.rs index 82300bc0a7..72cc285a97 100644 --- a/crates/ui-egui/src/browser.rs +++ b/crates/ui-egui/src/browser.rs @@ -364,7 +364,7 @@ pub fn group_items(app: &SolveApp, id: u64, scope: &str) -> Vec { v.push(Item::action("ui.newGroup", "New Group", "folder").with(json!({ "component": g.component, "folder": g.folder, "parent": id }))); } v.extend([ - Item::action("ui.rename", "Rename", "rename").with(json!({ "group": id })), + Item::action("ui.rename", "Rename", "rename").key("F2").with(json!({ "group": id })), Item::action("ui.ungroup", "Ungroup", "folder").with(json!({ "group": id })), Item::sep(), // The group itself: its items keep their own state. @@ -2181,7 +2181,7 @@ pub fn canvas_items(app: &SolveApp, id: u64) -> Vec { Item::action("canvas.to_sketch", "Canvas to Sketch", "canvas").with(json!({ "canvas": id })), Item::sep(), Item::action("ui.delete", "Delete", "delete").with(json!({ "canvases": [id] })), - Item::action("ui.rename", "Rename", "rename").with(json!({ "canvas": id })), + Item::action("ui.rename", "Rename", "rename").key("F2").with(json!({ "canvas": id })), Item::sep(), Item::action("ui.canvasVisible", "Show/Hide", "eye").key("V").with(json!({ "canvas": id, "visible": !visible })), ] diff --git a/crates/ui-egui/src/context_menu.rs b/crates/ui-egui/src/context_menu.rs index e67b00fda7..2cffcd97be 100644 --- a/crates/ui-egui/src/context_menu.rs +++ b/crates/ui-egui/src/context_menu.rs @@ -609,7 +609,7 @@ fn feature_items(app: &SolveApp, id: u64) -> Vec { vec![ act("ui.editFeature", "Edit Feature", "edit").with(json!({"feature":id})).on(exists), act("ui.editSketch", "Edit Profile Sketch", "sketch").with(json!({"sketch": sketch.map(|s| s.0)})).on(sketch.is_some()), - act("ui.rename", "Rename", "rename").with(json!({"feature":id})).on(exists), + act("ui.rename", "Rename", "rename").key("F2").with(json!({"feature":id})).on(exists), act("timeline.suppress", if suppressed { "Unsuppress" } else { "Suppress" }, "suppress") .with(json!({"feature":id, "suppressed": !suppressed})) .on(exists), @@ -788,7 +788,7 @@ fn body_items(app: &SolveApp, bodies: &[String], browser: bool) -> Vec { act("ui.paste", "Paste", "paste").key("Ctrl+V").on(crate::edit_clipboard::can_paste(app)), act("ui.delete", "Delete", "delete").key("Del").with(json!({ "items": items })).on(!locked), cmd(app, "solid.remove", "Remove").with(json!({ "bodies": names })).on(!locked), - act("ui.rename", "Rename", "rename").with(json!({ "body": bodies.first() })).on(one), + act("ui.rename", "Rename", "rename").key("F2").with(json!({ "body": bodies.first() })).on(one), Item::sep(), act(if all_hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": names })), act("ui.lock", if locked { "Unlock" } else { "Lock" }, "ground").with(json!({ "bodies": names })), @@ -841,7 +841,7 @@ fn sketch_items(app: &SolveApp, id: u64) -> Vec { act("ui.copy", "Copy", "copy").key("Ctrl+C").on(crate::edit_clipboard::something_to_copy(app)), act("ui.paste", "Paste", "paste").key("Ctrl+V").on(crate::edit_clipboard::can_paste(app)), act("ui.delete", "Delete", "delete").key("Del").with(json!({ "items": [{"type": "feature", "id": id}] })), - act("ui.rename", "Rename", "rename").with(json!({ "feature": id })), + act("ui.rename", "Rename", "rename").key("F2").with(json!({ "feature": id })), crate::browser::sketches_to_parts_item(sketches).on(parts && !editing), Item::sep(), act("ui.lookAt", "Look At", "lookat").with(json!({ "sketch": id })), @@ -868,7 +868,7 @@ fn plane_items(app: &SolveApp, id: u64) -> Vec { items.push(Item::sep()); items.extend([ act("ui.delete", "Delete", "delete").key("Del").with(json!({ "items": [{"type": "feature", "id": id}] })), - act("ui.rename", "Rename", "rename").with(json!({ "feature": id })), + act("ui.rename", "Rename", "rename").key("F2").with(json!({ "feature": id })), Item::sep(), act("ui.originPart", "Show/Hide", "eye").key("V").with(json!({ "keys": [f.name], "show": !visible })), Item::sep(), @@ -1083,7 +1083,7 @@ fn component_items(app: &SolveApp, id: u64) -> Vec { if id != 0 { v.push(act("ui.delete", "Delete", "delete").key("Del").with(json!({ "occurrences": [occ.map(|o| o.id)] })).on(occ.is_some())); } - v.push(act("ui.rename", "Rename", "rename").with(json!({ "component": id }))); + v.push(act("ui.rename", "Rename", "rename").key("F2").with(json!({ "component": id }))); v.push(Item::sep()); v.push(act(if hidden { "ui.show" } else { "ui.hide" }, "Show/Hide", "eye").key("V").with(json!({ "bodies": names })).on(!bodies.is_empty())); v.push(act("ui.showAll", "Show All Bodies", "eye")); diff --git a/crates/ui-egui/src/menu_tests.rs b/crates/ui-egui/src/menu_tests.rs index 50c819cf46..468828919f 100644 --- a/crates/ui-egui/src/menu_tests.rs +++ b/crates/ui-egui/src/menu_tests.rs @@ -1057,6 +1057,88 @@ fn f2_renames_the_selected_item() { assert!(!context_menu::rename_selection(&mut app, pos2(0.0, 0.0)), "nothing selected"); } +/// Rename shows F2 wherever F2 renames (bodies, sketches and features, construction geometry, +/// components and placements, groups, canvases), as Cut, Copy, Delete and Show/Hide show their +/// keys; joints and saved views, which F2 does not rename, show none. F2 pressed with such a +/// menu open renames what the menu is for, as its row says: a group or canvas is not selected by +/// the right-click that opens its menu, so F2 would rename the selection instead. +#[test] +fn rename_shows_f2_and_f2_with_a_menu_open_renames_what_the_menu_is_for() { + use crate::context_menu::RenameWhat; + let mut h = crate::scenario::Harness::new(); + h.app.run("solid.box", json!({"length": 10, "width": 20, "height": 30})).unwrap(); + let b = body(&h.app); + let feature = h.app.session.doc.features[0].id; + h.app.run("construct.plane.offset", json!({"base": "XY", "offset": 5})).unwrap(); + let plane = h.app.session.doc.features.last().unwrap().id; + let part = h.app.run("component.create", json!({"name": "Menu part"})).unwrap()["component"].as_u64().unwrap(); + let occurrence = h.app.session.doc.occurrence_of(part).unwrap().id; + let group = h.app.run("browser.group", json!({"component": 0, "folder": "bodies", "items": [b]})).unwrap()["group"].as_u64().unwrap(); + let mut png = b"\x89PNG\r\n\x1a\n\0\0\0\rIHDR".to_vec(); + png.extend(400u32.to_be_bytes()); + png.extend(200u32.to_be_bytes()); + png.extend([8, 6, 0, 0, 0, 0, 0, 0, 0]); + let data = solidcraft_engine::doc::canvas::base64_encode(&png); + let canvas = h.app.run("canvas.insert", json!({"data": data, "plane": "XY", "width": 80})).unwrap()["canvas"].as_u64().unwrap(); + let rename_key = |app: &SolveApp, target: &Target| { + context_menu::items(app, target) + .into_iter() + .find(|i| i.id == "ui.rename") + .map(|i| i.shortcut) + .unwrap_or_else(|| panic!("no Rename row: {target:?}")) + }; + for target in [ + Target::Body { name: b.clone() }, + Target::Sketch { id: feature }, + Target::Feature { id: feature }, + Target::Plane { id: plane }, + Target::Component { id: part }, + Target::Occurrence { path: vec![occurrence] }, + Target::Group { id: group, scope: "c0".into() }, + Target::Canvas { id: canvas }, + ] { + assert_eq!(rename_key(&h.app, &target), "F2", "Rename shows F2: {target:?}"); + } + assert_eq!(rename_key(&h.app, &Target::NamedView { name: "Top".into() }), "", "F2 does not rename a saved view"); + let sample = solidcraft_engine::sample::samples().into_iter().find(|s| s.name == "Hinge Assembly").unwrap(); + let mut assembly = SolveApp::new(Session::default(), Services::default()); + assembly.session.run_script(&(sample.script)()).unwrap(); + let joint = assembly.session.doc.assembly.joints[0].id; + assert_eq!(rename_key(&assembly, &Target::Joint { id: joint }), "", "F2 does not rename a joint"); + + let press_f2 = |h: &mut crate::scenario::Harness| { + for pressed in [true, false] { + h.app.synthetic.push(egui::Event::Key { key: egui::Key::F2, physical_key: None, pressed, repeat: false, modifiers: Default::default() }); + } + while !h.app.synthetic.is_empty() { + h.frame(); + } + }; + // A body selected elsewhere is not what the group's or the canvas's menu is for. + h.app.run("select.set", json!({"items": [{"type": "body", "name": b}]})).unwrap(); + for (target, what) in + [(Target::Group { id: group, scope: "c0".into() }, RenameWhat::Group(group)), (Target::Canvas { id: canvas }, RenameWhat::Canvas(canvas))] + { + context_menu::open_for(&mut h.app, pos2(300.0, 200.0), target.clone()); + h.frames(2); + press_f2(&mut h); + let renaming = h.app.menu.rename.as_ref().map(|r| r.what.clone()); + assert_eq!(renaming, Some(what), "F2 on the menu of {target:?}"); + assert!(h.app.viewport.context_menu.is_none(), "the menu closes as after a click"); + h.step(&json!({"key": "Escape"})).unwrap(); + h.frames(2); + assert!(h.app.menu.rename.is_none()); + } + // With no menu open, F2 still renames the selection. + h.app.run("select.set", json!({"items": [{"type": "body", "name": b}]})).unwrap(); + h.frames(2); + press_f2(&mut h); + assert_eq!(h.app.menu.rename.as_ref().map(|r| r.what.clone()), Some(RenameWhat::Body(b)), "F2 without a menu renames the selection"); + h.step(&json!({"key": "Escape"})).unwrap(); + h.frames(2); + assert!(h.app.menu.rename.is_none()); +} + /// A double-click on a body's browser row edits its name in the row, like F2 (no floating box); /// Escape cancels, and a second double-click then commits a new name. #[test] diff --git a/crates/ui-egui/src/toolbar.rs b/crates/ui-egui/src/toolbar.rs index 6d6d35c9d5..5808ed5881 100644 --- a/crates/ui-egui/src/toolbar.rs +++ b/crates/ui-egui/src/toolbar.rs @@ -3181,8 +3181,10 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) { } } Key::F2 => { - let at = ctx.input(|i| i.pointer.latest_pos()).unwrap_or(ctx.content_rect().center()); - crate::context_menu::rename_selection(app, at); + if !crate::context_menu::press_shortcut(app, "F2") { + let at = ctx.input(|i| i.pointer.latest_pos()).unwrap_or(ctx.content_rect().center()); + crate::context_menu::rename_selection(app, at); + } } Key::F6 => app.animate_view("fit"), Key::Backspace if crate::tools::undo_spline_point(app) => {}