From 18fe6ad6fd33eb55f024a30f076ab24339873864 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Thu, 12 Nov 2026 00:37:35 -0800 Subject: [PATCH] Automation: ui.dialog.set cannot open Save Preset in Save PDF or Print `__savePresetAs` makes OK save a preset instead of writing the PDF or printing. ui.dialog.set now accepts it only while Save Preset... has added the field, so a request cannot add it and confirm in one step. --- crates/ui-egui/src/dialogs.rs | 17 +++++++++++--- crates/ui-egui/src/tests/automation.rs | 31 ++++++++++++++++++++++++++ docs/control-protocol.md | 2 +- 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 9274bbd40c..838fc8161d 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -528,10 +528,21 @@ fn hidden_controls(d: &Dialog) -> &'static [&'static str] { } } +/// The controls of [`hidden_controls`] that a button adds to the dialog, which `ui.dialog.set` can +/// set only while the dialog holds them: Save PDF's and Print's `__savePresetAs`, whose presence +/// makes OK save a preset instead of writing the PDF or printing. +const ADDED_CONTROLS: &[&str] = &["__savePresetAs"]; + +/// Whether `field` is a control of `d` that `ui.dialog.set` can set: one of the controls of `d`'s +/// kind ([`hidden_controls`]), and for one that a button adds ([`ADDED_CONTROLS`]), one `d` holds. +fn settable_control(d: &Dialog, field: &str) -> bool { + hidden_controls(d).contains(&field) && (!ADDED_CONTROLS.contains(&field) || d.fields.contains_key(field)) +} + /// Whether `field` is a `__` field of `d` that `ui.dialog.set` refuses: any `__` field, held or -/// not, except the controls of `d`'s kind ([`hidden_controls`]). +/// not, except the controls of `d`'s kind ([`settable_control`]). pub fn protected_field(d: &Dialog, field: &str) -> bool { - field.starts_with("__") && !hidden_controls(d).contains(&field) + field.starts_with("__") && !settable_control(d, field) } /// Whether `ui.dialog.set` can set `field` on `d`: a `__` field only when it is one of the controls @@ -581,7 +592,7 @@ pub fn set_field(d: &mut Dialog, field: String, value: Value) { pub fn field_names(d: &Dialog) -> Vec { let spec = spec(&d.kind); let mut names: Vec = d.fields.keys().filter(|k| !k.starts_with("__")).cloned().collect(); - names.extend(hidden_controls(d).iter().map(|f| f.to_string())); + names.extend(hidden_controls(d).iter().filter(|f| settable_control(d, f)).map(|f| f.to_string())); names.extend(spec.discard.map(|_| "discard".to_string())); names.extend(spec.preview.then(|| "preview".to_string())); names.extend((spec.inputs)(&d.kind).iter().map(|i| i.to_string())); diff --git a/crates/ui-egui/src/tests/automation.rs b/crates/ui-egui/src/tests/automation.rs index d2d0a8c4b6..36908ec3e6 100644 --- a/crates/ui-egui/src/tests/automation.rs +++ b/crates/ui-egui/src/tests/automation.rs @@ -886,6 +886,37 @@ fn dialog_set_allows_hidden_controls() { assert_eq!(app.ui.dialog.as_ref().map(|d| d.str("__search")), Some("grid".to_string())); } +/// Save PDF's and Print's OK save a preset while Save Preset… asks for its name: `ui.dialog.set` +/// can't add `__savePresetAs` to turn OK into saving a preset, alone or with `action: "confirm"`, +/// and can type the name once Save Preset… has added the field. +#[test] +fn dialog_set_cannot_open_save_preset() { + let (mut app, tx) = controlled(); + let saved = |app: &VectorcraftApp| (app.session.prefs.pdf_presets.len(), app.session.prefs.print_presets.len()); + for (open, kind) in [("ui.savePdfDialog", "savePdf"), ("file.print", "print")] { + app.run(open, json!({})).unwrap(); + let presets = saved(&app); + for set in [ + json!({"field": "__savePresetAs", "value": "Proofs"}), + json!({"fields": [{"field": "__savePresetAs", "value": "Proofs"}], "action": "confirm"}), + ] { + let before = dialog_fields(&app); + let r = request(&mut app, &tx, "ui.dialog.set", set.clone()); + assert_eq!(r["error"], format!("the `{kind}` dialog cannot set protected field `__savePresetAs`"), "{set}: {r}"); + assert_eq!(dialog_fields(&app), before, "{set}: no field changes and the dialog stays open"); + } + let r = request(&mut app, &tx, "ui.dialog.set", json!({"field": "nosuch", "value": true})); + assert!(!r["error"].as_str().unwrap_or_default().contains("__savePresetAs"), "{kind}: {r}"); + assert_eq!(saved(&app), presets, "{kind}: no preset saved"); + // Save Preset… adds the field; the name can then be typed and OK saves the preset. + app.ui.dialog.as_mut().unwrap().fields.insert("__savePresetAs".into(), json!("")); + let r = request(&mut app, &tx, "ui.dialog.set", json!({"field": "__savePresetAs", "value": "Proofs", "action": "confirm"})); + assert_eq!(r["result"]["result"]["name"], "Proofs", "{kind}: {r}"); + app.ui.dialog = None; + } + assert_eq!(saved(&app), (1, 1)); +} + /// The refusal of an OK on an install question automation asked. const ASKED: &str = "Automation asked to install this plug-in. To install it, choose Object › Plug-ins › Install Plug-in… yourself."; diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 0fdfbc8231..53ce5d35f9 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -102,7 +102,7 @@ the two can still win that race. | `ui.click` / `ui.drag` | `{x, y, button?, count?, shift?, alt?, ctrl?, cmd?}` / `{x, y, toX, toY, steps?, count?, button?, shift?, alt?, ctrl?, cmd?}` | egui pointer input; see synthetic input budgets above | | `ui.wheel` | `{x, y, dy?, dx?, unit?: "line"\|"point", shift?, alt?, cmd?}` | a mouse wheel turn over screen point (x, y): `dy` notches up (+) or down, `dx` sideways. Over the canvas the wheel scrolls, Cmd-wheel (Ctrl on Windows and Linux) scrolls sideways and Alt-wheel (Option on the Mac) zooms about the pointer (with the `ctrlWheelScrollsSideways` preference off, Cmd-wheel zooms too); with the `zoomWithMouseWheel` preference the wheel and Alt-wheel zoom about the pointer, Shift-wheel scrolls up and down and Cmd-wheel (Ctrl on Windows and Linux) sideways. Over a focused numeric field (click it first) each notch steps its value as Up/Down do (Shift: ten, Cmd/Ctrl: a tenth) and the panel stays put; over anything else in a panel the wheel scrolls it. Over the canvas, the wheel preferences in `docs/mcp.md` change what these keys do | | `ui.set` | `{brightness?, panel?, rulers?, outline?, grid?, smartGuides?, boundingBox?, controlBar?, imageTracePreview?}` | `outline`: View › Outline for the active document only (each document has its own, a document opens in Preview, and `ui.inspect` shows it as `view.outline`; an error without a document). `imageTracePreview`: the Image Trace panel's Preview (`ui.inspect` shows it as `ui.image_trace_preview`) | -| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value, action?}` or `{fields: [{field, value}…], action?}` | fill and submit the open dialog. `ui.dialog.set` takes the fields the dialog has (`ui.inspect`) other than its `__` fields, the `__` fields that are controls of the dialog (such as `__section` and `__allArtboards` in Save PDF, `__constrain` in Transform Each and the shape dialogs, and `__search` and `__category` in Preferences), `discard` on a dialog with a discard button, `preview` on one with a live preview, the fields that the dialog reads but adds only when a control changes (such as `searchFolder`, or `copy` in the Transform dialogs), and the fields it passes to the command it runs: every parameter of the `command` dialog's command (`ui.paramDialog`), and the `textureImage` that Load Texture… sets in the Glass and Texturizer effects; any other field is an error that lists the dialog's fields, and the dialog stays as it was. Any other `__` field, held or not, holds what the dialog's OK acts on (such as the `__command` and `__params` of a `confirm` or `command` dialog) or the dialog's own state: setting it is the error "the `` dialog cannot set protected field ``", and the dialog stays as it was. `fields` sets the list (at most 256 items) in order (a missing `value` is `null`) and checks every item first: when one is refused, the call is an error and no field changes. `action: "confirm"` or `"cancel"` on `ui.dialog.set` then confirms or cancels the same dialog in the same request, as `ui.dialog.confirm` and `ui.dialog.cancel` do, and the reply is `{dialog, result}`: the dialog after the fields were set and the reply to confirm (`null` for cancel); any other `action` is an error and no field changes | +| `ui.dialog.set` / `.confirm` / `.cancel` | `{field, value, action?}` or `{fields: [{field, value}…], action?}` | fill and submit the open dialog. `ui.dialog.set` takes the fields the dialog has (`ui.inspect`) other than its `__` fields, the `__` fields that are controls of the dialog (such as `__section` and `__allArtboards` in Save PDF, `__constrain` in Transform Each and the shape dialogs, and `__search` and `__category` in Preferences; Save PDF's and Print's `__savePresetAs`, the preset name, only while Save Preset… asks for it, since OK then saves a preset), `discard` on a dialog with a discard button, `preview` on one with a live preview, the fields that the dialog reads but adds only when a control changes (such as `searchFolder`, or `copy` in the Transform dialogs), and the fields it passes to the command it runs: every parameter of the `command` dialog's command (`ui.paramDialog`), and the `textureImage` that Load Texture… sets in the Glass and Texturizer effects; any other field is an error that lists the dialog's fields, and the dialog stays as it was. Any other `__` field, held or not, holds what the dialog's OK acts on (such as the `__command` and `__params` of a `confirm` or `command` dialog) or the dialog's own state: setting it is the error "the `` dialog cannot set protected field ``", and the dialog stays as it was. `fields` sets the list (at most 256 items) in order (a missing `value` is `null`) and checks every item first: when one is refused, the call is an error and no field changes. `action: "confirm"` or `"cancel"` on `ui.dialog.set` then confirms or cancels the same dialog in the same request, as `ui.dialog.confirm` and `ui.dialog.cancel` do, and the reply is `{dialog, result}`: the dialog after the fields were set and the reply to confirm (`null` for cancel); any other `action` is an error and no field changes | | `ui.screenshot` | `{path?, data?}` | capture the window (PNG), written to `path`; `data: true` sends it back as `pngBase64` too. The inline PNG is capped at 64 MiB: with a `path` the file is still written and `pngBase64` is omitted when the image is larger, while a path-less `data: true` request fails if either the raw RGBA image or the encoded PNG exceeds the cap. Needs a presented frame: with the screen locked or the window minimized/covered it fails after ~8 s with an explanatory error | | `ui.render` | `{path?, scale?, artboard?, data?, traceViews?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first; `traceViews: true`: Image Trace objects draw in their View, as on the canvas, instead of their tracing result): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` | | `ui.render` | `{path?, scale?, artboard?, data?}` | render an artboard headlessly (PNG with the pixels of `app.export {format: "png", background: "white"}` at the same scale; `artboard`: its 0-based index, default the first): written to `path`, else (or with `data: true` as well) sent back as `pngBase64` |