mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
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:
co-authored by
Claude Opus 5.5
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();
|
||||
|
||||
Reference in New Issue
Block a user