M4.15: Revert as an engine command with a confirmation
file.revert reads and decodes the saved file first: if that fails the
document is left as it is, still modified. Otherwise Session::replace_document
swaps the content in the same tab (same place, path, format and uid; history
and selection cleared; saved state), so every tab keeps its zoom. It runs only
for saved, modified documents. In the app, Revert asks first with the shared
confirm dialog, whose OK runs file.revert {confirmed: true} on the engine.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
515ce2ac94
commit
9fae8c56ec
|
|
@ -5,7 +5,7 @@
|
|||
//! - `encode`: one encoder per writable format, with typed options parsed from the params.
|
||||
//! - `svg`: the SVG Options (styling, fonts, images, object ids, artboards…).
|
||||
//! - `export`: `document.export` / `serialize` / `exportSelection` / `exportForScreens`.
|
||||
//! - `save`: `document.save`, Save As / a Copy / as Template and the per-format options
|
||||
//! - `save`: `document.save`, Save As / a Copy / as Template, Revert and the per-format options
|
||||
//! (`file.formatOptions`); [`save_with`] is the one save path of every frontend.
|
||||
//! - `pdf`: PDF settings and presets for every PDF export, `document.exportPdf`.
|
||||
//! - `pdfimport`: the PDF pages, box and password `document.open` and Place read, `document.pdfInfo`.
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
//! Saving: `document.save`, `file.saveAs`, `file.saveCopy`, `file.saveAsTemplate` and
|
||||
//! `file.formatOptions`.
|
||||
//! Saving: `document.save`, `file.saveAs`, `file.saveCopy`, `file.saveAsTemplate`, `file.revert`
|
||||
//! and `file.formatOptions`.
|
||||
//!
|
||||
//! Every frontend saves the same way: [`save_plan`] resolves where and how (path, format, options,
|
||||
//! a suggested name and folder when there is no path), then [`save_with`] encodes, writes through
|
||||
|
|
@ -10,7 +10,7 @@ use serde_json::{Map, Value, json};
|
|||
use vectorcraft_doc::Document;
|
||||
|
||||
use super::super::*;
|
||||
use super::{Encoded, Format, encode_all, file_stem, format, format_for_name, write_encoded, write_file};
|
||||
use super::{Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, write_encoded, write_file};
|
||||
use crate::DocState;
|
||||
|
||||
/// The formats Save As offers, in menu order (append-only). The other writable formats are exports:
|
||||
|
|
@ -266,6 +266,26 @@ fn save(s: &mut Session, mode: SaveMode, p: &Value) -> Result<Value> {
|
|||
save_with(s, plan, write_file)
|
||||
}
|
||||
|
||||
fn can_revert(s: &Session) -> std::result::Result<(), String> {
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
match (&st.path, st.is_dirty()) {
|
||||
(None, _) => Err("the document has never been saved".into()),
|
||||
(_, false) => Err("no changes since the last save".into()),
|
||||
_ => Ok(()),
|
||||
}
|
||||
}
|
||||
|
||||
/// File → Revert: read and decode the saved file first (on any failure the document is left as it
|
||||
/// is), then replace the document in its tab.
|
||||
fn revert(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let index = s.active_index().ok_or(crate::EngineError::NoDocument)?;
|
||||
let path = s.doc()?.path.clone().ok_or_else(|| bad("file.revert", "the document has never been saved"))?;
|
||||
let Loaded { mut doc, .. } = load(&path, &read_file(&path)?)?;
|
||||
doc.template = false;
|
||||
s.replace_document(index, doc);
|
||||
Ok(json!({ "path": path }))
|
||||
}
|
||||
|
||||
/// `file.formatOptions`: a writable format's options with the values Save would use.
|
||||
fn format_options(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "file.formatOptions";
|
||||
|
|
@ -322,6 +342,15 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
|||
has_doc,
|
||||
|s, p| save(s, SaveMode::Template, p)
|
||||
),
|
||||
cmd!(
|
||||
"file.revert",
|
||||
"Revert",
|
||||
["File"],
|
||||
Some("F12"),
|
||||
"{} discard the changes: reload the saved file into the same tab (history cleared; the tab keeps its place and view). Saved, modified documents only; if the file can't be read the document is left as it is → {path}. The app asks first (a confirm dialog) unless confirmed: true",
|
||||
can_revert,
|
||||
revert
|
||||
),
|
||||
cmd!(
|
||||
query "file.formatOptions",
|
||||
"Format Options",
|
||||
|
|
|
|||
|
|
@ -643,10 +643,9 @@ impl Session {
|
|||
self.untitled_counter += 1;
|
||||
format!("Untitled-{}", self.untitled_counter)
|
||||
}
|
||||
/// Add a document and make it active.
|
||||
pub fn add_document(&mut self, mut doc: Document, path: Option<String>) -> usize {
|
||||
// Text layout bounds are a cache (not saved): compute them now, or selection boxes and
|
||||
// hit testing would use the rough estimate until each text object is edited.
|
||||
/// Text layout bounds are a cache (not saved): compute them for a document just read, or
|
||||
/// selection boxes and hit testing would use the rough estimate until each text is edited.
|
||||
fn refresh_text_bounds(doc: &mut Document) {
|
||||
let mut texts = vec![];
|
||||
doc.walk(|n| {
|
||||
if matches!(n.kind, NodeKind::Text(_)) {
|
||||
|
|
@ -660,6 +659,10 @@ impl Session {
|
|||
cmd::typecmd::refresh_bounds(t);
|
||||
}
|
||||
}
|
||||
}
|
||||
/// Add a document and make it active.
|
||||
pub fn add_document(&mut self, mut doc: Document, path: Option<String>) -> usize {
|
||||
Self::refresh_text_bounds(&mut doc);
|
||||
self.reset_tool_for_doc_switch();
|
||||
let mut st = DocState::new(doc, path);
|
||||
st.history.limit = self.prefs.history_states as usize;
|
||||
|
|
@ -668,6 +671,28 @@ impl Session {
|
|||
self.active = Some(i);
|
||||
i
|
||||
}
|
||||
/// Replace the document in tab `index` (File → Revert): new content, cleared history and
|
||||
/// selection, saved state. The tab keeps its place, path and format.
|
||||
pub fn replace_document(&mut self, index: usize, mut doc: Document) -> bool {
|
||||
if index >= self.docs.len() {
|
||||
return false;
|
||||
}
|
||||
Self::refresh_text_bounds(&mut doc);
|
||||
if self.active == Some(index) {
|
||||
// Pending tool work (typing, a drag) belongs to the content being thrown away.
|
||||
self.reset_tool_for_doc_switch();
|
||||
}
|
||||
let old = &self.docs[index];
|
||||
let mut st = DocState::new(doc, old.path.clone());
|
||||
st.history.limit = old.history.limit;
|
||||
// Same open document (caches keyed by uid stay valid); a new revision redraws it.
|
||||
st.uid = old.uid;
|
||||
st.revision = old.revision + 1;
|
||||
st.format = old.format;
|
||||
st.save_options = old.save_options.clone();
|
||||
self.docs[index] = st;
|
||||
true
|
||||
}
|
||||
pub fn close_document(&mut self, index: usize) -> bool {
|
||||
if index >= self.docs.len() {
|
||||
return false;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
//! The save pipeline: Save writes a document's own format, Save As, a Copy and as Template.
|
||||
//! The save pipeline: Save writes a document's own format, Save As / a Copy / as Template and
|
||||
//! Revert.
|
||||
|
||||
use std::path::PathBuf;
|
||||
|
||||
|
|
@ -33,6 +34,10 @@ fn b64(v: &Value) -> Vec<u8> {
|
|||
vectorcraft_format::base64_decode(v["dataBase64"].as_str().expect("dataBase64")).unwrap()
|
||||
}
|
||||
|
||||
fn objects(s: &Session) -> usize {
|
||||
s.doc().unwrap().doc.node_count()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_svg_path_writes_svg_and_save_writes_it_back() {
|
||||
let d = dir("svg");
|
||||
|
|
@ -123,6 +128,39 @@ fn save_a_copy_keeps_the_path_title_and_modified_state() {
|
|||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revert_reloads_the_saved_file_in_the_same_tab() {
|
||||
let d = dir("revert");
|
||||
let mut s = session();
|
||||
rect(&mut s);
|
||||
let native = path(&d, "a.vectorcraft");
|
||||
s.execute("document.save", &json!({"path": native})).unwrap();
|
||||
assert!(s.execute("file.revert", &json!({})).is_err(), "nothing to revert");
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
s.set_active(0);
|
||||
let uid = s.doc().unwrap().uid;
|
||||
rect(&mut s);
|
||||
rect(&mut s);
|
||||
assert_eq!(objects(&s), 4);
|
||||
s.execute("file.revert", &json!({})).unwrap();
|
||||
let st = s.doc().unwrap();
|
||||
assert_eq!((s.active_index(), s.documents().len(), objects(&s)), (Some(0), 2, 2));
|
||||
assert!(!st.is_dirty() && st.history.undo.is_empty() && st.selection.is_empty());
|
||||
assert_eq!((st.uid, st.path.as_deref()), (uid, Some(native.as_str())));
|
||||
// With the file gone the document is left as it is, still modified.
|
||||
rect(&mut s);
|
||||
std::fs::remove_file(&native).unwrap();
|
||||
assert!(s.execute("file.revert", &json!({})).is_err());
|
||||
let st = s.doc().unwrap();
|
||||
assert!(st.is_dirty());
|
||||
assert_eq!((objects(&s), st.history.undo.len()), (3, 1));
|
||||
// Never saved: nothing to revert to.
|
||||
s.set_active(1);
|
||||
rect(&mut s);
|
||||
assert!(s.execute("file.revert", &json!({})).is_err());
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_plans_suggest_names_and_folders() {
|
||||
let mut s = session();
|
||||
|
|
|
|||
|
|
@ -375,6 +375,16 @@ fn file_url(path: &str) -> String {
|
|||
url
|
||||
}
|
||||
|
||||
/// File → Revert: ask first; OK runs `file.revert` with `confirmed`, which goes to the engine.
|
||||
pub fn ask_revert(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let c = vectorcraft_engine::find_command("file.revert").ok_or("no revert command")?;
|
||||
(c.enabled)(&app.session)?;
|
||||
let name = app.session.active().map(|d| d.title()).unwrap_or_default();
|
||||
let message = format!("Revert to the saved version of “{name}”?");
|
||||
dialogs::confirm::ask(app, &message, "Changes made since it was last saved will be lost.", "file.revert", json!({ "confirmed": true }));
|
||||
Ok(json!({ "pending": dialogs::confirm::KIND }))
|
||||
}
|
||||
|
||||
/// Place a file's bytes (no path, so embedded) centred in the view: `file.place`.
|
||||
pub fn place_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8]) -> Result<(), String> {
|
||||
crate::place::run(app, &serde_json::json!({ "name": name, "dataBase64": vectorcraft_format::base64_encode(bytes) })).map(|_| ())
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ mod tests_place;
|
|||
#[cfg(test)]
|
||||
mod tests_recolor;
|
||||
#[cfg(test)]
|
||||
mod tests_save;
|
||||
#[cfg(test)]
|
||||
mod tests_svg;
|
||||
#[cfg(test)]
|
||||
mod tests_svgsave;
|
||||
|
|
|
|||
|
|
@ -57,7 +57,6 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
|||
"{path?, format?, options?, svg?: {…SVG options}} = document.save, written through the app (a document saved as SVG, PDF or .ai, or opened from one Save can write back, saves as that again, with the same options); with no path known (never saved) the Save As panel asks first",
|
||||
),
|
||||
("file.newFromTemplate", "New from Template…", "Cmd+Shift+N", "{path?} open a template as a new untitled document"),
|
||||
("file.revert", "Revert", "F12", "{}"),
|
||||
(
|
||||
"file.place",
|
||||
"Place…",
|
||||
|
|
@ -443,17 +442,7 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
|||
Some(path) => io::open_path(app, &path).map(|_| Value::Null),
|
||||
None => io::open_dialog(app).map(|_| Value::Null),
|
||||
},
|
||||
"file.revert" => {
|
||||
let path = app.session.active().and_then(|d| d.path.clone());
|
||||
match path {
|
||||
Some(path) => {
|
||||
let i = app.session.active_index().unwrap_or(0);
|
||||
app.session.close_document(i);
|
||||
io::open_path(app, &path).map(|_| Value::Null)
|
||||
}
|
||||
None => Err("document has never been saved".into()),
|
||||
}
|
||||
}
|
||||
"file.revert" if p.get("confirmed").and_then(Value::as_bool) != Some(true) => io::ask_revert(app),
|
||||
id if id.starts_with("file.openRecent") => {
|
||||
let n: usize = id["file.openRecent".len()..].parse().unwrap_or(0);
|
||||
match n.checked_sub(1).and_then(|i| app.ui.recent_files.get(i)).cloned() {
|
||||
|
|
@ -948,7 +937,6 @@ pub fn enabled(app: &VectorcraftApp, id: &str) -> bool {
|
|||
| "view.fitArtboard"
|
||||
| "view.fitAll"
|
||||
| "view.actualSize" => app.session.active().is_some(),
|
||||
"file.revert" => app.session.active().is_some_and(|d| d.path.is_some() && d.is_dirty()),
|
||||
id if id.starts_with("file.openRecent") => {
|
||||
id["file.openRecent".len()..].parse::<usize>().is_ok_and(|n| n >= 1 && n <= app.ui.recent_files.len())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,76 @@
|
|||
//! The File menu around saving, in the app: Revert's confirmation.
|
||||
|
||||
use serde_json::json;
|
||||
use vectorcraft_engine::Session;
|
||||
|
||||
use crate::{Services, VectorcraftApp, dialogs, menus, theme};
|
||||
|
||||
/// An app that reads and writes real files.
|
||||
fn app() -> VectorcraftApp {
|
||||
let services = Services {
|
||||
read: Some(Box::new(|p: &str| std::fs::read(p).map_err(|e| e.to_string()))),
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
|
||||
..Default::default()
|
||||
};
|
||||
VectorcraftApp::new(Session::new(), services)
|
||||
}
|
||||
|
||||
/// A fresh folder for one test.
|
||||
fn dir(tag: &str) -> std::path::PathBuf {
|
||||
let d = std::env::temp_dir().join(format!("vc-ui-save-{tag}-{}", std::process::id()));
|
||||
let _ = std::fs::remove_dir_all(&d);
|
||||
std::fs::create_dir_all(&d).unwrap();
|
||||
d
|
||||
}
|
||||
|
||||
fn rect(app: &mut VectorcraftApp) {
|
||||
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap();
|
||||
}
|
||||
|
||||
fn objects(app: &VectorcraftApp) -> usize {
|
||||
app.session.doc().unwrap().doc.node_count()
|
||||
}
|
||||
|
||||
/// One headless frame of the dialog layer with Enter pressed (OK).
|
||||
fn press_ok(app: &mut VectorcraftApp) {
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
let key = egui::Event::Key { key: egui::Key::Enter, physical_key: None, pressed: true, repeat: false, modifiers: Default::default() };
|
||||
let mut out = ctx.run_ui(egui::RawInput { events: vec![key], ..Default::default() }, |ui| dialogs::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn revert_asks_then_reloads_in_the_same_tab_keeping_every_zoom() {
|
||||
let d = dir("revert");
|
||||
let path = d.join("a.vectorcraft").to_string_lossy().to_string();
|
||||
let mut app = app();
|
||||
app.run("file.new", json!({"width": 100, "height": 100})).unwrap();
|
||||
rect(&mut app);
|
||||
app.run("file.saveAs", json!({"path": path})).unwrap();
|
||||
assert!(!menus::enabled(&app, "file.revert"), "nothing changed since the save");
|
||||
app.run("file.new", json!({"width": 50, "height": 50})).unwrap();
|
||||
app.views[0].zoom = 2.0;
|
||||
app.views[1].zoom = 4.0;
|
||||
app.session.set_active(0);
|
||||
rect(&mut app);
|
||||
assert!(menus::enabled(&app, "file.revert"));
|
||||
let r = app.run("file.revert", json!({})).unwrap();
|
||||
assert_eq!(r["pending"], dialogs::confirm::KIND);
|
||||
assert_eq!(objects(&app), 3, "nothing happens before OK");
|
||||
press_ok(&mut app);
|
||||
assert!(app.ui.dialog.is_none());
|
||||
assert_eq!((objects(&app), app.session.active_index(), app.session.documents().len()), (2, Some(0), 2));
|
||||
assert!(!app.session.active().unwrap().is_dirty());
|
||||
assert_eq!((app.views[0].zoom, app.views[1].zoom), (2.0, 4.0));
|
||||
// Cancel leaves the changes.
|
||||
rect(&mut app);
|
||||
app.run("file.revert", json!({})).unwrap();
|
||||
app.ui.dialog = None;
|
||||
assert_eq!(objects(&app), 3);
|
||||
// An untitled document can't revert.
|
||||
app.session.set_active(1);
|
||||
rect(&mut app);
|
||||
assert!(app.run("file.revert", json!({})).is_err() && app.ui.dialog.is_none());
|
||||
let _ = std::fs::remove_dir_all(d);
|
||||
}
|
||||
|
|
@ -22,7 +22,7 @@
|
|||
| `app.export` with `useArtboards` | `{path, useArtboards: true, range? \| artboards?, …}` | Export As: PNG, JPEG and WebP write one file per chosen artboard (default all; SVG does so for a `range`), `<path stem>-<artboard>.<ext>`, and return `{path, files: [path…]}`; a PDF keeps them as pages of `path`. `useArtboards: false` covers the bounds of the visible art. The menu's Export As… is `file.exportAs`: the `exportAs` dialog (`format`, `useArtboards`, `all`, `range`), then a save dialog and the format's options: `pngOptions` / `jpgOptions` / `webpOptions`, `svgOptions`, or `savePdf` for a PDF with Use Artboards |
|
||||
| `ui.dialog.*` on `gradientStop` | `{field: "color" \| "opacity" \| "location", value}` | double-clicking a stop on the Gradient tool's annotator opens its popover (fields `index`, `x`, `y`, `tab`); set `color` (hex), `opacity` or `location` (percentages) and `ui.dialog.confirm` to apply them to the selected stop. `gradient.selectStop {index}` picks the stop the annotator, the panels and Delete/←/→ (`ui.key`) act on |
|
||||
| `engine.execute` `ui.savePdfDialog` | `{path?, preset?, range?, …document.exportPdf options}` | opens the Save PDF dialog (`file.export.pdf` with no params opens it too). Its fields are the `document.exportPdf` options: set `compatibility` with `ui.dialog.set {field: "compatibility", value: "1.5"}`, a whole section with `{field: "compression", value: {…}}`, the artboards with `__allArtboards: false` and `range`, the visible section with `__section` (General, Compression, Marks and Bleeds, Output, Advanced, Security, Summary). `ui.dialog.confirm` writes `path` (else asks for one), returns `{path, bytes, warnings}` and opens the file when `viewAfterSaving` is set |
|
||||
| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use |
|
||||
| `file.save` / `file.saveAs` / `file.saveCopy` / `file.saveAsTemplate` (via `engine.execute`) | `{path?, format?, options?, svg?}` | Save writes the document's own file in its own format (a document opened from or saved as SVG/PDF saves as that again; the result's `warnings` say what the format loses). With `path` the file is written at once (Save As or a Copy to an SVG path without SVG options first opens SVG Options, below). Without one (never saved, Save As, a Copy, a Template) the app shows a save panel with one file type per save format (`vectorcraft`, `template`, `pdf`, `svg`, `svgz`), then the format's options: SVG opens the `svgOptions` dialog (below; returns `{"pending": "svgOptions", "path"}`), PDF the `savePdf` dialog (its `__save` field names the save command); `ui.dialog.confirm` finishes the save. On the web a `saveOptions` dialog (fields `path`, `format`) first names the file and picks the format. `file.formatOptions {format?}` reads the options a save would use. `file.revert` opens a `confirm` dialog (`{"pending": "confirm"}`); `ui.dialog.confirm` reloads the saved file in the same tab |
|
||||
| SVG Options (`engine.execute` of `file.export.svg`, `file.saveAs`, `file.saveCopy`) | `{}` / `{path?, svg?: {…}}` | `file.export.svg` without params, and Save As or Save a Copy to an `.svg`/`.svgz` path without SVG options, open the `svgOptions` dialog (`ui.dialog.set` its fields: `styling`, `outlineText`, `images`, `objectIds`, `decimals`, `minify`, `responsive`, `fewerTspans`, `metadata`, `preserveEditing`, `useArtboards`, `allArtboards`, `range`, `showCode`; `ui.dialog.confirm` exports or saves and remembers the choices). With `svg: {…}` (the `document.formats` SVG options) they run directly. `app.save {path?, svg?}` takes the same options; a document saved as SVG saves as SVG again with the same options |
|
||||
| `engine.execute` `file.place` | `{paths?}` / `{path \| name+dataBase64, …file.place params}` | with no file, picks files and opens the `place` dialog; with `paths`, opens it for them (fields `files`, `link`, `template`, `replace`; `__replace` says whether Replace applies, `__info` describes each file). `ui.dialog.confirm` places one file centred in the view (or in place of the selected object) and loads the place cursor with several (`file.place.queue`). With a file, it runs `file.place` centred in the view. Files dropped on the canvas are placed at the drop point (linked; Shift embeds), elsewhere or with no document open they open. With the cursor loaded (`ui.inspect` → `tool: "place"`, `toolOptions: {count, current, name, names}`), `ui.pointer` clicks and drags place files and `ui.key` ←/→/↑/↓ and Escape cycle and discard them |
|
||||
| `app.export` raster options | `{path?, format: png \| jpg \| webp, ppi?, background?, antiAlias?, interlaced?, quality?}` | `ppi` sets the pixel size (72 = one pixel per point) and is stored in the file; `background` is `transparent`, `white`, `black` or `"#rrggbb"`; `antiAlias` is `none`, `art` or `type` (text snapped to pixels); `interlaced` writes an Adam7 PNG. The PNG Options dialog (`pngOptions`; `jpgOptions` / `webpOptions` for the other raster formats) has the same fields: fill them with `ui.dialog.set` and press OK with `ui.dialog.confirm` |
|
||||
|
|
|
|||
|
|
@ -141,7 +141,7 @@ markup around it: if another app changed the SVG since, `document.open` reads it
|
|||
|
||||
Saving beyond `save_file` goes through `run_command`: `file.saveAs {path?, format?, options?, svg?}` (the document takes
|
||||
on the new file and format), `file.saveCopy` (the document keeps its path, title and modified state),
|
||||
`file.saveAsTemplate` (a `.vctemplate` that opens untitled) and `file.formatOptions {format?}` (a format's options
|
||||
`file.saveAsTemplate` (a `.vctemplate` that opens untitled), `file.revert` and `file.formatOptions {format?}` (a format's options
|
||||
with the values a save would use). Without a path the save commands return `{dataBase64, name, folder?, warnings}`. A document saved as SVG or PDF
|
||||
remembers its options, so the next save reuses them. A PDF opened with `pages`, `cropTo` or `password` is only part
|
||||
of the file, so Save asks for a new name instead of writing it back.
|
||||
|
|
|
|||
Loading…
Reference in New Issue