Menus take the keyboard (Up/Down choose a row, Enter runs it); F2 renames the selected body, sketch, feature, plane, component or canvas

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01AS5Tn3xieewUSg3uxrMVdL
This commit is contained in:
bherbruckandClaude Opus 5.5 committed 2026-10-08 13:08:43 +00:00
1 parent 0ee343b9e6
commit c009bb2c2a
4 files changed
+63 -4

No files matched your search

+46 -3
View File
@@ -136,6 +136,8 @@ fn act(id: &str, label: &str, icon: &str) -> Item {
#[derive(Default)]
pub struct MenuState {
pub target: Target,
/// The list row chosen with the arrow keys (Enter runs it).
pub key_row: Option<usize>,
/// The radial ring is shown (viewport menus).
pub radial: bool,
/// The right button is held through the menu: releasing over a direction picks it.
@@ -927,6 +929,24 @@ pub fn toggle_visibility(app: &mut SolveApp) {
}
}
/// Rename what is selected (F2): one body, sketch or feature, construction plane, component
/// picked in the browser, or canvas.
pub fn rename_selection(app: &mut SolveApp, at: Pos2) -> bool {
let what = match (app.session.selection.as_slice(), app.tree.picked_components.as_slice(), app.tree.picked_canvases.as_slice()) {
([Sel::Body { name }], [], []) => RenameWhat::Body(name.clone()),
([Sel::Feature { id }], [], []) => RenameWhat::Feature(*id),
([Sel::Plane { name }], [], []) => match app.session.doc.find_feature(name) {
Some(f) => RenameWhat::Feature(f.id),
None => return false,
},
([], [c], []) => RenameWhat::Component(*c),
([], [], [c]) => RenameWhat::Canvas(*c),
_ => return false,
};
start_rename(app, what, at);
true
}
/// Add the edges that continue the selected edges smoothly to the selection.
fn tangent_chain(app: &mut SolveApp) {
let st = app.session.model.state();
@@ -1015,6 +1035,7 @@ pub fn open(app: &mut SolveApp, at: Pos2) {
/// Open a menu for a target (the radial ring only for the viewport).
pub fn open_for(app: &mut SolveApp, at: Pos2, target: Target) {
app.menu.key_row = None;
app.menu.radial = target == Target::Viewport;
app.menu.target = target;
app.menu.flick = false;
@@ -1260,10 +1281,27 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) {
at
};
// Up/Down choose a row of the list, Enter runs it.
let (up, down, enter) = ctx.input(|i| (i.key_pressed(egui::Key::ArrowUp), i.key_pressed(egui::Key::ArrowDown), i.key_pressed(egui::Key::Enter)));
let usable: Vec<usize> = list.iter().enumerate().filter(|(_, i)| i.enabled && !i.is_sep() && !i.is_heading()).map(|(k, _)| k).collect();
if up || down {
let at = app.menu.key_row.and_then(|r| usable.iter().position(|u| *u == r));
let next = match (at, down) {
(None, true) => Some(0),
(None, false) => usable.len().checked_sub(1),
(Some(k), true) => Some((k + 1) % usable.len().max(1)),
(Some(k), false) => Some((k + usable.len().max(1) - 1) % usable.len().max(1)),
};
app.menu.key_row = next.and_then(|k| usable.get(k).copied());
}
if enter && let Some(item) = app.menu.key_row.and_then(|r| list.get(r)) {
picked = Some(item.clone());
}
let key_row = app.menu.key_row;
let resp = egui::Area::new(egui::Id::new("sc_context_menu")).fixed_pos(list_at).constrain(true).order(egui::Order::Foreground).show(ctx, |ui| {
menu_frame(ui, |ui| {
for item in &list {
if let Some(p) = list_row(ui, item) {
for (k, item) in list.iter().enumerate() {
if let Some(p) = list_row_lit(ui, item, key_row == Some(k)) {
picked = Some(p);
}
}
@@ -1314,6 +1352,11 @@ pub fn menu_frame<R>(ui: &mut egui::Ui, add: impl FnOnce(&mut egui::Ui) -> R) ->
/// One list row; the item when it was clicked.
pub fn list_row(ui: &mut egui::Ui, item: &Item) -> Option<Item> {
list_row_lit(ui, item, false)
}
/// A list row, drawn highlighted when `lit` (the keyboard's choice).
pub fn list_row_lit(ui: &mut egui::Ui, item: &Item, lit: bool) -> Option<Item> {
let t = Tokens::get();
let w = LIST_W - 10.0;
if item.is_sep() {
@@ -1333,7 +1376,7 @@ pub fn list_row(ui: &mut egui::Ui, item: &Item) -> Option<Item> {
return None;
}
let (r, resp) = ui.allocate_exact_size(vec2(w, 24.0), if item.enabled { Sense::click() } else { Sense::hover() });
if item.enabled && resp.hovered() {
if item.enabled && (resp.hovered() || lit) {
ui.painter().rect_filled(r.shrink2(vec2(2.0, 1.0)), 4.0, t.hover);
}
let ink = if item.enabled { t.icon } else { t.border };
+1 -1
View File
@@ -13,7 +13,7 @@ use crate::SolveApp;
use crate::theme::Tokens;
/// Keys the application handles before any command shortcut.
pub const RESERVED: &[&str] = &["Escape", "S", "V", "F6", "Delete", "Backspace", "Ctrl+Z", "Ctrl+Y", "Ctrl+S", "Ctrl+N", "Ctrl+O"];
pub const RESERVED: &[&str] = &["Escape", "S", "V", "F2", "F6", "Delete", "Backspace", "Ctrl+Z", "Ctrl+Y", "Ctrl+S", "Ctrl+N", "Ctrl+O"];
#[derive(Default)]
pub struct Keymap {
+12
View File
@@ -443,3 +443,15 @@ fn an_uncaptured_move_asks_before_the_next_command() {
assert_eq!(app.session.doc.occurrences[0].transform[3][0], 10.0);
assert!(crate::browser::answer_capture(&mut app, "capture").is_err(), "nothing waits now");
}
#[test]
fn f2_renames_the_selected_item() {
let mut app = sample_app();
let b = body(&app);
app.run("select.set", json!({"items": [{"type": "body", "name": b}]})).unwrap();
assert!(context_menu::rename_selection(&mut app, pos2(0.0, 0.0)));
assert!(context_menu::finish_rename(&mut app, Some("Base"), true));
assert!(app.session.model.state().body("Base").is_some());
app.run("select.clear", json!({})).unwrap();
assert!(!context_menu::rename_selection(&mut app, pos2(0.0, 0.0)), "nothing selected");
}
+4
View File
@@ -463,6 +463,10 @@ pub fn shortcuts(app: &mut SolveApp, ctx: &egui::Context) {
crate::shortcut_box::open(app, at);
}
Key::V => crate::context_menu::toggle_visibility(app),
Key::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::Delete | Key::Backspace => crate::viewport::delete_selection(app),
_ => {