Browse / github.com/storytold/cadcraft / patch
Ctrl/Cmd+W closes the drawing, as the File menu shows
Ctrl/Cmd+W closes the drawing, as the File menu shows ## Problem File ▸ Close shows Ctrl+W (⌘W on macOS), but pressing it did nothing on Windows and Linux: the keyboard table in `menus.rs` had no entry for it. Only the macOS native menu, which reads the menu's shortcut itself, closed the drawing. ## Change - Ctrl/Cmd+W runs `close`, the same path as File ▸ Close, so unsaved changes still ask first. - The keyboard table moves into `builtin_shortcuts()`, unchanged otherwise. #1703 (@Alex-BB44) makes the same move under the same name for its storey shortcuts, so the two should merge without trouble. ## Tests - New `every_shown_shortcut_runs_its_co
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- ATARYS-LAB (imported from pull request #2057, open)
- Base
b9f96d3dd3630a8de2cb7885d66ded334682918b- Size
- +55 −5 in 1 file
- Review
- pending
- Shared
- 2026-10-28
- Id
0d7eef00f4f3d98b70d86399
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/menus.rs
@@ -657,18 +657,20 @@ pub fn run_transparent(app: &mut CadApp, id: &str) { } } -/// Keyboard shortcuts and function keys.-pub fn shortcuts(app: &mut CadApp, ctx: &egui::Context) {+/// CADCraft's keyboard shortcuts and function keys, each with the command it runs. Every shortcut a+/// command shows (its registry `key`, or a UI command's) must be here, or pressing it does nothing.+pub(crate) fn builtin_shortcuts() -> Vec<(egui::KeyboardShortcut, &'static str)> { use egui::{Key, KeyboardShortcut, Modifiers}; let sc = |m: Modifiers, k: Key| KeyboardShortcut::new(m, k); let cmd = Modifiers::COMMAND; let cmd_shift = Modifiers::COMMAND | Modifiers::SHIFT;- let pairs: &[(KeyboardShortcut, &str)] = &[+ vec![ (sc(cmd_shift, Key::A), "pickstyle"), (sc(cmd_shift, Key::Z), "redo"), (sc(cmd, Key::Z), "u"), (sc(cmd, Key::Y), "redo"), (sc(cmd, Key::N), "new"),+ (sc(cmd, Key::W), "close"), (sc(cmd, Key::O), "ui.open"), (sc(cmd_shift, Key::S), "ui.saveas"), (sc(cmd, Key::S), "qsave"),@@ -693,7 +695,15 @@ pub fn shortcuts(app: &mut CadApp, ctx: &egui::Context) { (sc(Modifiers::NONE, Key::F10), "polar"), (sc(Modifiers::NONE, Key::F11), "otrack"), (sc(Modifiers::NONE, Key::F12), "dynmode"),- ];+ ]+}++/// Keyboard shortcuts and function keys.+pub fn shortcuts(app: &mut CadApp, ctx: &egui::Context) {+ use egui::{Key, KeyboardShortcut, Modifiers};+ let sc = |m: Modifiers, k: Key| KeyboardShortcut::new(m, k);+ let cmd = Modifiers::COMMAND;+ let pairs = builtin_shortcuts(); crate::clipboard::mirror(app, ctx); if app.closing.is_some() { return;@@ -708,7 +718,7 @@ pub fn shortcuts(app: &mut CadApp, ctx: &egui::Context) { // Cmd/Ctrl+C, X and V arrive as clipboard events, not key presses. let mut fire = crate::clipboard::shortcut_commands(app, ctx); ctx.input_mut(|i| {- for (s, id) in pairs {+ for (s, id) in &pairs { if i.consume_shortcut(s) && !fire.contains(id) { fire.push(*id); }@@ -762,6 +772,46 @@ mod tests { (CadApp::new(Session::new(), services), asked) } + /// A registry shortcut ("Cmd+Shift+Z", "F8") as egui reads it.+ fn parse_shortcut(s: &str) -> Option<egui::KeyboardShortcut> {+ let mut m = egui::Modifiers::NONE;+ let (mods, key) = s.rsplit_once('+').unwrap_or(("", s));+ for part in mods.split('+').filter(|p| !p.is_empty()) {+ match part {+ "Cmd" => m |= egui::Modifiers::COMMAND,+ "Shift" => m |= egui::Modifiers::SHIFT,+ "Alt" => m |= egui::Modifiers::ALT,+ _ => return None,+ }+ }+ Some(egui::KeyboardShortcut::new(m, egui::Key::from_name(key)?))+ }++ #[test]+ fn every_shown_shortcut_runs_its_command() {+ // Handled outside the table: Delete by the command line (it erases the selection only when+ // the line is empty), Cmd+8 before it (QuickCalc's own field keeps focus), Cmd+Q by the+ // macOS app menu, the only place that shows it.+ let elsewhere = [("Delete", "erase.selection"), ("Cmd+8", "ui.toggle.quickcalc"), ("Cmd+Q", "ui.quit")];+ let table = super::builtin_shortcuts();+ let shown = cadcraft_engine::command_specs()+ .iter()+ .filter_map(|c| Some((c.shortcut?, c.id)))+ .chain(super::UI_COMMANDS.iter().filter_map(|(id, _, _, sc)| Some(((*sc)?, *id))));+ let mut missing = Vec::new();+ for (key, id) in shown {+ if elsewhere.contains(&(key, id)) {+ continue;+ }+ let sc = parse_shortcut(key).unwrap_or_else(|| panic!("{id}: unreadable shortcut {key}"));+ // The table may run a command's UI form (`ui.open` for `open`), which does the same.+ if !table.iter().any(|(s, i)| *s == sc && (*i == id || i.strip_prefix("ui.") == Some(id))) {+ missing.push(format!("{key} {id}"));+ }+ }+ assert!(missing.is_empty(), "shown but not run: {missing:?}");+ }+ #[test] fn plot_and_print_open_the_plot_dialog_and_exportpdf_asks_for_a_file() { set_io(IoHooks { read: |_, _| Err("no reader in tests".into()), write: |_, _| Err("no writer in tests".into()), plot: Some(fake_plot) });