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.
This commit is contained in:
dotCooCoo
2026-11-12 00:37:35 -08:00
parent e6f330afd2
commit 18fe6ad6fd
3 changed files with 46 additions and 4 deletions
+14 -3
View File
@@ -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<String> {
let spec = spec(&d.kind);
let mut names: Vec<String> = 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()));
+31
View File
@@ -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.";
+1 -1
View File
@@ -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 `<kind>` dialog cannot set protected field `<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 `<kind>` dialog cannot set protected field `<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` |