mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-24 10:56:51 -06:00
fix(ui): toggle QuickCalc with Ctrl/Cmd+8 (#2001)
Co-authored-by: cloudj <jonbahnes@gmail.com>
This commit is contained in:
@@ -27,7 +27,8 @@ pub const UI_COMMANDS: &[(&str, &str, &[&str], Option<&str>)] = &[
|
||||
("ui.toggle.toolsets", "Tool Sets", &["Window", "Tool Sets"], Some("Cmd+3")),
|
||||
("ui.panel.properties", "Properties Inspector", &["Window", "Properties Inspector"], Some("Cmd+1")),
|
||||
("ui.panel.layers", "Layers Palette", &["Window", "Layers Palette"], None),
|
||||
("ui.panel.quickcalc", "QuickCalc", &["Window", "QuickCalc"], Some("Cmd+8")),
|
||||
("ui.toggle.quickcalc", "QuickCalc", &["Window", "QuickCalc"], Some("Cmd+8")),
|
||||
("ui.panel.quickcalc", "QuickCalc", &[], None),
|
||||
("ui.toggle.systemcursor", "Show System Cursor", &["View", "Accessibility", "Show System Cursor"], None),
|
||||
("ui.animations.system", "Use System Setting", &["View", "Accessibility", "Animation Effects", "Use System Setting"], None),
|
||||
("ui.animations.on", "On", &["View", "Accessibility", "Animation Effects", "On"], None),
|
||||
@@ -102,7 +103,10 @@ fn run_ui_command_inner(app: &mut CadApp, id: &str, params: &Value) -> Option<Re
|
||||
let panel = if id == "ui.panel.properties" { "properties" } else { "layers" };
|
||||
return Some(crate::docking::command(app, false, &json!({"operation":"open", "panel":panel})));
|
||||
}
|
||||
// QUICKCALC typed or from a menu opens the palette; with parameters it is CAL.
|
||||
if id == "ui.toggle.quickcalc" {
|
||||
return Some(crate::palettes::quickcalc::toggle(app));
|
||||
}
|
||||
// Explicit opens remain idempotent; typed QUICKCALC with JSON parameters uses the engine's CAL alias.
|
||||
if id == "ui.panel.quickcalc" || (matches!(id, "quickcalc" | "qc") && params.is_null()) {
|
||||
return Some(crate::palettes::quickcalc::open(app));
|
||||
}
|
||||
@@ -604,8 +608,13 @@ fn switch_on_hover(ctx: &egui::Context, titles: &[egui::Response]) {
|
||||
}
|
||||
}
|
||||
|
||||
/// Like choosing a menu item: starts the command interactively.
|
||||
/// Activate a menu command. QuickCalc toggles transparently; other items clear pending
|
||||
/// command-line text and start their command interactively.
|
||||
pub fn activate(app: &mut CadApp, id: &str) {
|
||||
if id == "ui.toggle.quickcalc" {
|
||||
run_transparent(app, id);
|
||||
return;
|
||||
}
|
||||
app.cmd.buffer.clear();
|
||||
app.start(id);
|
||||
}
|
||||
@@ -663,7 +672,14 @@ pub fn shortcuts(app: &mut CadApp, ctx: &egui::Context) {
|
||||
(sc(Modifiers::NONE, Key::F12), "dynmode"),
|
||||
];
|
||||
crate::clipboard::mirror(app, ctx);
|
||||
if ctx.egui_wants_keyboard_input() || app.closing.is_some() {
|
||||
if app.closing.is_some() {
|
||||
return;
|
||||
}
|
||||
// A palette toggle is global even while its expression field owns keyboard focus.
|
||||
if ctx.input_mut(|i| i.consume_shortcut(&sc(cmd, Key::Num8))) {
|
||||
run_transparent(app, "ui.toggle.quickcalc");
|
||||
}
|
||||
if ctx.egui_wants_keyboard_input() {
|
||||
return;
|
||||
}
|
||||
// Cmd/Ctrl+C, X and V arrive as clipboard events, not key presses.
|
||||
|
||||
@@ -182,6 +182,17 @@ impl State {
|
||||
}
|
||||
}
|
||||
|
||||
/// Close only QuickCalc when present; otherwise restore it through the normal open path.
|
||||
/// Hiding the panel leaves its expression, history and variables intact.
|
||||
pub(crate) fn toggle(app: &mut CadApp) -> Result<Value, String> {
|
||||
let panel = crate::docking::Panel::QuickCalc;
|
||||
if app.ui.docking.layout.contains(&panel) {
|
||||
crate::docking::command(app, false, &json!({ "operation": "close", "panel": panel.id() }))
|
||||
} else {
|
||||
open(app)
|
||||
}
|
||||
}
|
||||
|
||||
/// Open the palette. The first time it floats by the right edge of the drawing; afterwards it
|
||||
/// returns where it was closed.
|
||||
pub fn open(app: &mut CadApp) -> Result<Value, String> {
|
||||
|
||||
@@ -0,0 +1,148 @@
|
||||
//! QuickCalc's menu and platform shortcut toggle only its panel, preserving drafting and calculator
|
||||
//! state. Explicit opens remain idempotent and programmatic calculations do not open the palette.
|
||||
|
||||
use cadcraft_engine::Session;
|
||||
use cadcraft_ui_egui::{CadApp, Services, docking::Panel, menus};
|
||||
use egui::{Key, Modifiers, accesskit::Role, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::{NodeT, Queryable, by};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
fn harness() -> Harness<'static, CadApp> {
|
||||
let mut app = CadApp::new(Session::default(), Services::default());
|
||||
app.ui.start_tab = false;
|
||||
app.ui.in_window_menu = false;
|
||||
let mut h = Harness::builder().with_size(vec2(1280.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut CadApp| {
|
||||
app.logic(&ui.ctx().clone());
|
||||
app.ui(ui);
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(5);
|
||||
h
|
||||
}
|
||||
|
||||
/// In these tests QuickCalc floats alone; scope to its body to avoid the Properties text fields.
|
||||
/// Its first text input is the expression, before conversion values and variable definitions.
|
||||
fn expression<'a>(h: &'a Harness<'_, CadApp>) -> egui_kittest::Node<'a> {
|
||||
h.get_by_role_and_label(Role::Tab, "QuickCalc").parent().unwrap().get_all_by_role(Role::TextInput).next().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn platform_shortcut_toggles_with_expression_focus_and_preserves_drafting() {
|
||||
for (os, modifiers) in [
|
||||
(egui::os::OperatingSystem::Windows, Modifiers { ctrl: true, command: true, ..Modifiers::NONE }),
|
||||
(egui::os::OperatingSystem::Mac, Modifiers { mac_cmd: true, command: true, ..Modifiers::NONE }),
|
||||
] {
|
||||
let mut h = harness();
|
||||
h.ctx.set_os(os);
|
||||
let drawing = h.state().session.doc().unwrap().clone();
|
||||
h.state_mut().start("line");
|
||||
h.state_mut().cmd.buffer = "12,34".into();
|
||||
h.key_press_modifiers(modifiers, Key::Num8);
|
||||
h.run_steps(5);
|
||||
assert!(h.state().ui.docking.layout.contains(&Panel::QuickCalc));
|
||||
let floating = h.state().ui.docking.layout.floating.iter().find(|group| group.panels.contains(&Panel::QuickCalc)).unwrap().rect;
|
||||
expression(&h).click();
|
||||
h.run_steps(3);
|
||||
expression(&h).type_text("2+3");
|
||||
h.run_steps(3);
|
||||
assert!(h.ctx.egui_wants_keyboard_input());
|
||||
h.key_press_modifiers(modifiers, Key::Num8);
|
||||
h.run_steps(5);
|
||||
assert!(!h.state().ui.docking.layout.contains(&Panel::QuickCalc), "text focus must not swallow the toggle");
|
||||
h.key_press_modifiers(modifiers, Key::Num8);
|
||||
h.run_steps(5);
|
||||
assert_eq!(h.get(by().role(Role::TextInput).value("2+3")).value().as_deref(), Some("2+3"));
|
||||
assert_eq!(h.state().ui.docking.layout.floating.iter().find(|group| group.panels.contains(&Panel::QuickCalc)).unwrap().rect, floating);
|
||||
assert_eq!(h.state().session.running.as_ref().unwrap().id, "line");
|
||||
assert_eq!(h.state().cmd.buffer, "12,34");
|
||||
assert_eq!(h.state().session.doc().unwrap(), &drawing);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn window_menu_toggle_preserves_calculator_history_and_command_line_input() {
|
||||
let mut h = harness();
|
||||
let (id, _, _, shortcut) = menus::UI_COMMANDS.iter().find(|(_, label, path, _)| *label == "QuickCalc" && !path.is_empty()).unwrap();
|
||||
assert_eq!(*id, "ui.toggle.quickcalc");
|
||||
assert_eq!(*shortcut, Some("Cmd+8"));
|
||||
// Both the native menu and egui menu dispatch through activate().
|
||||
menus::activate(h.state_mut(), id);
|
||||
h.run_steps(5);
|
||||
expression(&h).click();
|
||||
h.run_steps(3);
|
||||
expression(&h).type_text("2+3");
|
||||
h.run_steps(3);
|
||||
h.get_by_role_and_label(Role::Button, "=").click();
|
||||
h.run_steps(3);
|
||||
assert!(h.query_all(by().value("2+3")).next().is_some());
|
||||
h.state_mut().start("line");
|
||||
h.state_mut().cmd.buffer = "12,34".into();
|
||||
menus::activate(h.state_mut(), id);
|
||||
h.run_steps(5);
|
||||
assert!(!h.state().ui.docking.layout.contains(&Panel::QuickCalc));
|
||||
menus::activate(h.state_mut(), id);
|
||||
h.run_steps(5);
|
||||
assert!(h.query_all(by().value("2+3")).next().is_some(), "closing must not discard calculation history");
|
||||
assert_eq!(h.get(by().role(Role::TextInput).value("5")).value().as_deref(), Some("5"));
|
||||
assert_eq!(h.state().session.running.as_ref().unwrap().id, "line");
|
||||
assert_eq!(h.state().cmd.buffer, "12,34");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn toggle_restores_floating_or_docked_location_without_hiding_other_panels() {
|
||||
for floating in [false, true] {
|
||||
let mut app = CadApp::new(Session::default(), Services::default());
|
||||
app.run("ui.panel.quickcalc", Value::Null).unwrap();
|
||||
let params = if floating {
|
||||
json!({"operation":"float", "panel":"quickCalc", "rect":[80,120,360,480]})
|
||||
} else {
|
||||
json!({"operation":"move", "panel":"quickCalc", "target":"toolSets", "zone":"center"})
|
||||
};
|
||||
app.run("ui.dock", params).unwrap();
|
||||
let layout = app.ui.docking.layout.clone();
|
||||
app.run("ui.toggle.quickcalc", json!({})).unwrap();
|
||||
assert!(!app.ui.docking.layout.contains(&Panel::QuickCalc));
|
||||
for panel in [Panel::Canvas, Panel::Layers, Panel::Properties, Panel::ToolSets] {
|
||||
assert!(app.ui.docking.layout.contains(&panel));
|
||||
}
|
||||
app.run("ui.toggle.quickcalc", json!({})).unwrap();
|
||||
assert_eq!(app.ui.docking.layout, layout);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn explicit_opens_still_reveal_and_calculations_do_not_open_the_palette() {
|
||||
let mut app = CadApp::new(Session::default(), Services::default());
|
||||
let value = app.run("cal", json!({"expr":"2+3"})).unwrap();
|
||||
assert_eq!(value["value"], 5.0);
|
||||
assert!(app.cmdline(r#"quickcalc {"expr":"2+3"}"#).is_none());
|
||||
assert!(!app.ui.docking.layout.contains(&Panel::QuickCalc));
|
||||
assert!(menus::is_ui_command("ui.panel.quickcalc"));
|
||||
assert!(app.cmdline("QUICKCALC").is_none());
|
||||
let layout = app.ui.docking.layout.clone();
|
||||
for command in ["QC", "QUICKCALC", "ui.panel.quickcalc"] {
|
||||
assert!(app.cmdline(command).is_none());
|
||||
assert_eq!(app.ui.docking.layout, layout, "{command} must not toggle an open palette");
|
||||
}
|
||||
app.run("ui.dock", json!({"operation":"move", "panel":"quickCalc", "target":"toolSets", "zone":"center"})).unwrap();
|
||||
app.run("ui.dock", json!({"operation":"activate", "panel":"toolSets"})).unwrap();
|
||||
app.run("ui.panel.quickcalc", Value::Null).unwrap();
|
||||
let mut workspace = app.ui.docking.clone();
|
||||
workspace.reveal_panel(Panel::QuickCalc).unwrap();
|
||||
assert_eq!(app.ui.docking.layout, workspace.layout, "explicit open must activate an inactive tab");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quickcalc_shortcut_does_not_bypass_the_unsaved_drawing_prompt() {
|
||||
let mut h = harness();
|
||||
h.state_mut().run("circle", json!({"center":[0,0], "radius":1})).unwrap();
|
||||
assert!(h.state_mut().request_quit());
|
||||
assert!(h.state().closing.is_some());
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::Num8);
|
||||
h.run_steps(4);
|
||||
assert!(!h.state().ui.docking.layout.contains(&Panel::QuickCalc));
|
||||
assert!(h.state().closing.is_some());
|
||||
}
|
||||
Reference in New Issue
Block a user