From 7389cc22d6af82de9311193eb8cb6e30cb423d14 Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 1 Nov 2026 18:18:19 -0800 Subject: [PATCH] Document Info: Save writes the category the panel shows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Save… in the Document Info panel menu now writes only the category the panel shows, with Selection Only applied. With All Categories shown it writes every category, as before. docInfo.save takes an optional category (a document.info category id) and passes it to document.info. --- crates/ui-egui/src/menus/ui_commands.rs | 2 +- crates/ui-egui/src/panels/doc_info.rs | 71 +++++++++++++++++++++++-- docs/control-protocol.md | 3 +- docs/mcp.md | 4 +- docs/target-app-parity.md | 1 + 5 files changed, 73 insertions(+), 8 deletions(-) diff --git a/crates/ui-egui/src/menus/ui_commands.rs b/crates/ui-egui/src/menus/ui_commands.rs index 11f342420..48a1d2ecc 100644 --- a/crates/ui-egui/src/menus/ui_commands.rs +++ b/crates/ui-egui/src/menus/ui_commands.rs @@ -1130,7 +1130,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[ "docInfo.save", "Save Document Info…", "", - "{path?, selectionOnly?} write Document Info's text report (document.info {format: \"text\"}) to path, else a picked file (the web downloads it) → {path}", + "{path?, selectionOnly?, category?} write Document Info's text report (document.info {format: \"text\"}) to path, else a picked file (the web downloads it); with category (a document.info category id) only that category, else every category. The panel menu's Save… passes the category the panel shows and its Selection Only → {path}", ), ( "ui.epsOptionsDialog", diff --git a/crates/ui-egui/src/panels/doc_info.rs b/crates/ui-egui/src/panels/doc_info.rs index 87f08f7f3..f6becb391 100644 --- a/crates/ui-egui/src/panels/doc_info.rs +++ b/crates/ui-egui/src/panels/doc_info.rs @@ -101,11 +101,15 @@ fn named_rows(id: &str, rows: &[Value]) -> std::ops::Range { } } -/// `docInfo.save {path?, selectionOnly?}`: write the text report to `path`, else a picked file -/// (the web downloads it) → `{path}`. +/// `docInfo.save {path?, selectionOnly?, category?}`: write the text report (only `category` when given, +/// else every category) to `path`, else a picked file (the web downloads it) → `{path}`. pub(crate) fn save_report(app: &mut VectorcraftApp, p: &Value) -> Result { let sel_only = p.get("selectionOnly").and_then(Value::as_bool).unwrap_or(false); - let r = app.session.execute("document.info", &json!({ "format": "text", "selectionOnly": sel_only })).map_err(|e| e.to_string())?; + let mut q = json!({ "format": "text", "selectionOnly": sel_only }); + if let Some(c) = p.get("category").and_then(Value::as_str).filter(|c| !c.is_empty()) { + q["category"] = c.into(); + } + let r = app.session.execute("document.info", &q).map_err(|e| e.to_string())?; let text = r["text"].as_str().unwrap_or_default(); let title = app.session.active().map(|d| d.title()).unwrap_or_default(); let stem = std::path::Path::new(&title).file_stem().map_or_else(|| "Untitled".into(), |s| s.to_string_lossy().into_owned()); @@ -134,7 +138,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { } ui.separator(); if menu_item(ui, tl!("Save…"), app.session.active().is_some(), false) { - crate::menus::invoke(app, "docInfo.save", json!({ "selectionOnly": sel })); + crate::menus::invoke(app, "docInfo.save", json!({ "selectionOnly": sel, "category": category })); } } @@ -207,4 +211,63 @@ mod tests { let report = String::from_utf8(written.borrow().clone()).unwrap(); assert!(report.starts_with("Document Info: ") && report.contains("\nFONT DETAILS\n") && report.contains("\nREPEAT OBJECTS\n"), "{report}"); } + + /// The report the panel menu's Save… writes with category `category` shown ("" for All + /// Categories) and Selection Only set to `sel_only`. + fn saved_from_menu(app: &mut VectorcraftApp, category: &str, sel_only: bool) -> String { + let written: std::rc::Rc>> = Default::default(); + let w = written.clone(); + app.services.write = Some(Box::new(move |_: &str, b: &[u8]| { + w.borrow_mut().extend_from_slice(b); + Ok(()) + })); + app.services.pick_save = Some(Box::new(|pick: &crate::FilePick| Some(format!("/tmp/{}", pick.name)))); + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + set_pstate(&ctx, "docinfo-category", category.to_string()); + set_pstate(&ctx, "docinfo-sel", sel_only); + let frame = |app: &mut VectorcraftApp, events: Vec| { + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(236.0, 800.0)); + let mut out = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), events, ..Default::default() }, |ui| menu(app, ui)); + out.textures_delta.clear(); + crate::tests_removeanchors::shapes_text(&out.shapes.iter().map(|c| c.shape.clone()).collect::>()) + }; + let labels = frame(app, vec![]); + let at = labels.iter().find(|(t, _)| t.trim() == "Save…").map(|(_, r)| r.center()).unwrap_or_else(|| panic!("no Save… in {labels:?}")); + let button = + |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE }; + frame(app, vec![egui::Event::PointerMoved(at), button(true)]); + frame(app, vec![button(false)]); + let report = String::from_utf8(written.borrow().clone()).unwrap(); + assert!(report.starts_with("Document Info: "), "nothing saved: {report:?}"); + report + } + + /// Save… writes the category the panel shows, with Selection Only applied; All Categories + /// writes every category. + #[test] + fn save_writes_the_category_the_panel_shows() { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 100, "height": 100})).unwrap(); + app.session.execute("shape.rectangle", &json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); + let text = app.session.execute("text.create", &json!({"x": 10, "y": 40, "text": "Hi"})).unwrap()["id"].clone(); + let objects = saved_from_menu(&mut app, "objects", false); + assert!(objects.contains("\nOBJECTS\n") && objects.contains("Paths: 1") && objects.contains("Text Objects: 1"), "{objects}"); + assert!(!objects.contains("\nDOCUMENT\n") && !objects.contains("\nFONTS\n"), "only Objects: {objects}"); + let fonts = saved_from_menu(&mut app, "fonts", false); + assert!(fonts.contains("\nFONTS\n") && fonts.contains("Source Sans 3"), "{fonts}"); + assert!(!fonts.contains("\nOBJECTS\n") && !fonts.contains("\nFONT DETAILS\n"), "only Fonts: {fonts}"); + let all = saved_from_menu(&mut app, "", false); + assert!( + all.contains("\nDOCUMENT\n") && all.contains("\nOBJECTS\n") && all.contains("\nFONTS\n") && all.contains("\nREPEAT OBJECTS\n"), + "{all}" + ); + app.session.execute("select.set", &json!({"ids": [text]})).unwrap(); + let selected = saved_from_menu(&mut app, "objects", true); + assert!(selected.contains("(the selection only)") && selected.contains("Text Objects: 1"), "{selected}"); + assert!(!selected.contains("Paths: 1") && !selected.contains("\nFONTS\n"), "the selection's objects only: {selected}"); + // The command takes the category too; an unknown one is an error and writes nothing. + let err = app.run("docInfo.save", json!({"path": "/tmp/info.txt", "category": "colors"})).unwrap_err(); + assert!(err.contains("category `colors`"), "{err}"); + } } diff --git a/docs/control-protocol.md b/docs/control-protocol.md index 8f0943230..379c4b37b 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -646,7 +646,8 @@ Package: File → Package… (`ui.packageDialog`) opens the `package` dialog for gets a `confirm` dialog whose OK runs `file.saveAs`). Fields: `folder` (the location), `name` (the package folder), `copyLinks`, `linksFolder`, `relink`, `copyFonts`, `report`. `ui.dialog.confirm` saves unsaved changes, runs `file.package` and then opens a `confirm` dialog whose OK runs `file.showPackage {folder}`; the web downloads the zip -instead. Document Info's flyout picks a category and Save… runs `docInfo.save {path?, selectionOnly?}`. +instead. Document Info's flyout picks a category and Save… runs `docInfo.save {path?, selectionOnly?, category?}` with the +category the panel shows (none for All Categories, which writes every category). Slices: through the app, `object.slice.options` and `object.slice.divide` with no params open their dialogs on the selected slices. `sliceOptions` (fields `kind`: `image`/`noImage`/`htmlText`, `name`, `url`, `target`, `message`, diff --git a/docs/mcp.md b/docs/mcp.md index f1e55b9fd..ac1eef851 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -4052,8 +4052,8 @@ groups and in other repeats included, with its type `radial`, `grid` or `mirror` and a grid's `rows` and `cols`; with `selectionOnly`, the Repeat objects in the selection) and `pathLength` (pt: the total length of the paths, compound paths' pieces included and guides left out, which objects lists as Path Length in the document's units, say for a cut line or an LED strip along the letters); -`format: "text"` returns the plain-text report Document Info › Save… (`docInfo.save {path?}`) and the package report -write. +`format: "text"` returns the plain-text report Document Info › Save… (`docInfo.save {path?, selectionOnly?, category?}`: +only `category` when given, else every category) and the package report write. ```json {"name":"run_command","arguments":{"command":"file.package","params":{"folder":"/tmp/handoff","copyFonts":false}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 40e1d1267..e0eacde1e 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -986,6 +986,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The Brushes panel's Delete Brush button and panel menu item ask "Delete the selected brushes?" with Yes and No when none of the brushes is in use. Alt-click (Option-click on macOS) on the button deletes them without asking, as the [Default keyboard shortcuts](https://web.archive.org/web/20260726060015/https://helpx.adobe.com/illustrator/using/default-keyboard-shortcuts.html) page (archived copy) lists ("Delete without confirmation (except for Layers panel)"). When a stroke uses one of the brushes, the button, Alt-click and the menu item all ask with Remove Strokes and Cancel; Remove Strokes deletes the brushes and the strokes that used them become plain strokes. Tests: `panels::brushes::tests`, `dialogs::brush_delete::tests`. Document Info has a Brushes category after Graphic Styles. It lists the Brushes panel's brushes, or with Selection Only it lists the brushes that the selected objects' strokes use. The Document Info report and Package's report list them too (`document.info {category: "brushes"}`, `brushNames`). Document Info has a Repeat Objects category after Embedded Images. It lists each radial, grid and mirror Repeat object by name with its kind and the number of instances it makes. For a grid it gives the rows and columns instead, followed by the number of instances it draws when that is fewer. With Selection Only it lists the Repeat objects in the selection. The Document Info report and Package's report list them too (`document.info {category: "repeats"}`, `repeatObjects`). + Document Info's Save… writes the category the panel shows, with Selection Only applied, as the [Document Info panel](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/document-info-panel.html) page describes ("Exports the selected category's visible information to a text file"). With All Categories shown it writes every category, which is VectorCraft's own rule (`docInfo.save {category}`). Test: `panels::doc_info::tests::save_writes_the_category_the_panel_shows`. The Brushes panel menu's Select All Unused selects the brushes that are not used in the document, as the [Brushes](https://web.archive.org/web/20241230205225/https://helpx.adobe.com/illustrator/using/brushes.html) page (archived copy) describes. A brush counts as used when a stroke paints with it in the art (opacity masks included), in symbols, graph designs, pattern tiles or graphic styles, or in the art of a used brush (`brush.unused`). A rename in Brush Options and Delete Brush change every stroke that paints with the brush: in the layers and opacity masks, symbols, graph designs, pattern tiles, graphic styles and the art of other brushes. A rename gives those strokes the new name, and Delete Brush makes them plain strokes, in one undo step. In Pattern Brush Options, a menu below the Outer Corner Tile and the Inner Corner Tile sets each to the brush's own corner art (Original), None, or a tile made from the side tile: Auto-Centered stretches one side tile around the corner, Auto-Between bends a side tile on each side into the corner's diagonal, Auto-Sliced cuts a side tile on each side along the diagonal, and Auto-Overlap runs a side tile on each side past the corner. Auto-Sliced masks the tiles at the diagonal, so their outlines stay as drawn. Brushes saved without these choices draw their corners as before (`brush.options` `outer_source`, `inner_source`). Evidence: `tests::pattern_auto_corners_fill_each_corner_of_a_square`, `tests::pattern_corner_source_none_leaves_that_kind_of_corner_empty` (vectorcraft-brush), `dialogs::brush_options::tests::pattern_options_set_each_corner_tile_to_original_none_or_an_automatic_tile` (vectorcraft-ui-egui).