Make SolveApp::start honour a command's enabled flag, and disable solid.move inside a sketch (#740)

`SolveApp::start` now honours a command's enabled flag, so shortcuts, the command palette and the control channel can't start a disabled command, and Move is disabled while a sketch is open.
This commit is contained in:
William Dunn authored and GitHub committed 2026-10-13 11:43:45 -04:00
1 parent 62a7f903f2
commit ff609bc320
8 files changed
+132 -4

No files matched your search

+1
View File
@@ -137,6 +137,7 @@ pub fn handle(app: &mut SolveApp, ctx: &egui::Context, req: &ControlRequest) ->
"camera": app.cam,
"tool": app.tool.as_ref().map(|t| t.cmd),
"dialog": app.dialog.as_ref().map(|d| format!("{d:?}")),
"status": app.status.as_ref().map(|(text, _, error)| json!({"text": text, "error": error})),
"hover": app.viewport.hover.as_ref().map(|h| format!("{h:?}")),
"renderer": if app.viewport.gpu.is_some() { "gpu" } else { "cpu" },
"frame_ms": app.frame_ms,
+10
View File
@@ -530,6 +530,16 @@ impl SolveApp {
if self.file_load.busy() {
return;
}
// A disabled command starts nothing, however it was reached. The toolbar greys its
// button, but a shortcut, the command palette and the control channel all land here.
// Its reason goes to the status line, so the refusal is never silent.
if let Some(info) = solidcraft_engine::find_command(id).map(|c| c.info(&self.session))
&& let Some(why) = info.disabled_reason
{
let label = self.tr_label(info.label).to_string();
self.set_status(format!("{label}: {why}"), true);
return;
}
// Another command ends Edit Text (its dialog is not a feature edit to keep).
sketch_text_move::cancel_drag(self);
if sketch_text::editing(self) || self.dialog.as_ref().is_some_and(|d| matches!(d.kind, dialogs::Kind::TextMove(_))) {
+15 -1
View File
@@ -34,6 +34,7 @@
//! - `{"note": "…"}`: a comment; `{"pending": "why"}`: the scenario waits for a fix (its test
//! is ignored)
//! - `{"expect": {…}}`: checks, see [`check`]; `{"until": {…}}` waits (frames) until they pass
//! (`"status"` takes a substring of the status line, or null for none)
//!
//! The same files drive the live app over the control channel for screenshots.
@@ -562,7 +563,8 @@ fn approx(got: f64, want: &Value, what: &str) -> Result<(), String> {
/// Checks: `bodies` (count), `volume` / `area` (total, number or [value, rel tol]), `faces`,
/// `features` (count), `errors` (timeline features in error, default 0), `dialog` (null: none
/// open, or a word its kind must contain), `tool` (null or command id), `sketching` (bool),
/// open, or a word its kind must contain), `tool` (null or command id), `status` (null, or a
/// substring of the status line), `sketching` (bool),
/// `selection` (count), `glyphs` ({constraint id: count}, none overlapping one another or a dimension value), `param` ({name: value}), `sketch_status` (solved|…), `dof`, `query`
/// ({command, params?, path, value}: a command's result).
pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
@@ -760,6 +762,18 @@ pub fn check(h: &mut Harness, e: &Value) -> Result<(), String> {
return Err(format!("tool: got {}, want {v}", ui["tool"]));
}
}
"status" => {
// A substring of the status line's text, or null for no status at all.
let got = &ui["status"]["text"];
let ok = match v {
Value::Null => ui["status"].is_null(),
Value::String(w) => got.as_str().is_some_and(|g| g.contains(w.as_str())),
_ => false,
};
if !ok {
return Err(format!("status: got {}, want {v}", ui["status"]));
}
}
"glyphs" => {
// {constraint id: glyphs drawn for it}; glyphs never overlap one another.
let g = crate::sketch_tools::glyph_positions();