Browser: show F2 shortcut on Rename context menu rows and route key when open (#10388)

Show F2 beside Rename on context menus where F2 already renames the
target (bodies, features and sketches, construction planes, components and
occurrences, browser groups, and canvases). Saved views and joints remain
without a shortcut label because F2 does not rename them without a menu.

When a context menu is open, pressing F2 routes through press_shortcut to
rename the menu's target (such as a clicked browser group or canvas) and
close the menu, rather than renaming the viewport selection.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Gemini 3.8 Flash <noreply@google.com>
This commit is contained in:
MarkusRe
2026-11-01 01:37:25 +00:00
committed by GitHub
parent 653bc00be4
commit 60d79e7ea2
4 changed files with 93 additions and 9 deletions
+2 -2
View File
@@ -364,7 +364,7 @@ pub fn group_items(app: &SolveApp, id: u64, scope: &str) -> Vec<Item> {
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> {
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 })),
]
+5 -5
View File
@@ -609,7 +609,7 @@ fn feature_items(app: &SolveApp, id: u64) -> Vec<Item> {
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<Item> {
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<Item> {
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<Item> {
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<Item> {
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"));
+82
View File
@@ -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]
+4 -2
View File
@@ -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) => {}