mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-07 08:29:41 -06:00
Merge pull request #5212 from dotCooCoo/feat/document-info-save-category
Document Info: Save writes the category the panel shows
This commit is contained in:
@@ -1136,7 +1136,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",
|
||||
|
||||
@@ -101,11 +101,15 @@ fn named_rows(id: &str, rows: &[Value]) -> std::ops::Range<usize> {
|
||||
}
|
||||
}
|
||||
|
||||
/// `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<Value, String> {
|
||||
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<std::cell::RefCell<Vec<u8>>> = 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<egui::Event>| {
|
||||
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::<Vec<_>>())
|
||||
};
|
||||
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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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`,
|
||||
|
||||
+2
-2
@@ -4089,8 +4089,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}}}
|
||||
|
||||
@@ -993,6 +993,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).
|
||||
|
||||
Reference in New Issue
Block a user