From 0ce3147aabb269e6887747ead2e6657f9c2c8fdd Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sat, 7 Nov 2026 00:07:48 -0800 Subject: [PATCH] History: the New Document button makes a document from the selected state The History panel gets a New Document from State button at its bottom, which runs the new edit.newDocumentFromState command. The command makes a new document holding the art of a History panel row ({index?}, the current state when omitted) and makes it the active document. An index that names no state is an error and changes nothing. The new document takes the next Untitled-N name, is marked unsaved, has nothing selected, no steps to undo or redo and no modified date, and leaves symbol, opacity-mask and pattern editing; the art drawn in a pattern's tile is stored in the pattern, as Done stores it. Its links point at the files the source finds, including files found in the source's folder away from the links' paths. Those files are looked for only within the folders the asking caller may read; for a caller without limits, within the folders the caller that opened the source may read. The source document, its steps and its saved state stay as they are; typing or a drag in progress is first kept as a step, as Undo keeps it. While an IME composes, the command waits, as Undo does. Docs: docs/mcp.md, docs/target-app-parity.md. --- crates/engine/src/cmd/edit.rs | 62 +++- crates/engine/src/cmd/links.rs | 22 ++ crates/engine/src/cmd/patterncmds.rs | 24 +- crates/engine/src/command_sets.rs | 3 +- crates/engine/src/history.rs | 11 + crates/engine/src/tests.rs | 1 + crates/engine/src/tests/history_document.rs | 361 ++++++++++++++++++++ crates/engine/tests/command_sweep.rs | 8 + crates/ui-egui/src/canvas/tests.rs | 26 ++ crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/bs.tsv | 2 + crates/ui-egui/src/i18n/ca.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/da.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fi.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hi.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/id.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/ka.tsv | 2 + crates/ui-egui/src/i18n/ko.tsv | 2 + crates/ui-egui/src/i18n/nl.tsv | 2 + crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sv.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/menus/state.rs | 4 +- crates/ui-egui/src/panels/history.rs | 42 ++- docs/mcp.md | 2 +- docs/target-app-parity.md | 3 +- 40 files changed, 604 insertions(+), 19 deletions(-) create mode 100644 crates/engine/src/tests/history_document.rs diff --git a/crates/engine/src/cmd/edit.rs b/crates/engine/src/cmd/edit.rs index 529d9513c..dbe3d4509 100644 --- a/crates/engine/src/cmd/edit.rs +++ b/crates/engine/src/cmd/edit.rs @@ -9,9 +9,9 @@ use vectorcraft_geom::{Affine, Point, Vec2}; use super::clipboard::SwatchChoices; use super::{ - CommandSpec, Result, Session, always, bad, bool_or, can_clear_history, can_redo, can_undo, checked_ids_param, cmd, f64_or, f64_req, - finite_numbers, has_clipboard, has_doc, has_selection, has_selection_guides_or_artboard_tool, has_selection_or_guides, id_param, index, ok, - point_param, str_param, switch_or, + CommandSpec, Result, Session, always, bad, bool_or, can_clear_history, can_redo, can_undo, checked_ids_param, clock_date, cmd, date_param, + f64_or, f64_req, finite_numbers, has_clipboard, has_doc, has_selection, has_selection_guides_or_artboard_tool, has_selection_or_guides, id_param, + index, ok, point_param, str_param, switch_or, }; use crate::{Clipboard, EngineError, inspect}; @@ -116,6 +116,15 @@ pub fn specs() -> Vec { can_clear_history, clear_history ), + cmd!( + "edit.newDocumentFromState", + "New Document from State", + [], + None, + "{index?: a row of the History panel (0: the oldest state kept; default: the current state), created?: Unix seconds|null (File Info's created date; default now, recorded in the journal so a replay matches)} make a new document holding the art as it was at that state, and make it the active document (the History panel's New Document from State button). The new document is named with the next Untitled-N, has nothing selected and no steps to undo or redo, is marked unsaved and links to the same files. It is out of symbol, opacity-mask and pattern editing, with the art drawn in a pattern's tile stored in the pattern as object.pattern.done stores it. The source document and its steps stay as they are, except that typing or a drag in progress is first kept as a step, as edit.undo keeps it; `index` names the row as it was listed before that. An index that names no state is an error and changes nothing → {index: the new document's, title}", + has_doc, + new_document_from_state + ), cmd!( "edit.undoGroup.begin", "Begin Undo Group", @@ -606,6 +615,53 @@ fn clear_history(s: &mut Session, _: &Value) -> Result { Ok(json!({ "cleared": cleared })) } +/// History panel › New Document from State: a new document with the art of a state, and without +/// the editing state that state may hold (opacity-mask or symbol editing, Puppet Warp pins, new art). +/// Its links point at the files the source finds, which may be in the source's folder away from +/// the links' paths. Later artboards in the two documents take their ids separately. A later save of the new document +/// over the file its art was read from asks first, as a save of the source does. +fn new_document_from_state(s: &mut Session, p: &Value) -> Result { + const C: &str = "edit.newDocumentFromState"; + let created = date_param(p, "created", C)?; + let st = s.doc()?; + // The row as the panel lists it now: keeping an edit in progress below changes the rows. + let chosen = match p.get("index") { + None | Some(Value::Null) => None, + Some(v) => { + let state = v.as_u64().and_then(|row| st.history.state(&st.doc, index(row))); + Some(state.ok_or_else(|| bad(C, format!("no history state {v}")))?.clone()) + } + }; + settle_interaction(s)?; + let st = s.doc()?; + let mut doc = Document::clone(chosen.as_ref().unwrap_or(&st.doc)); + let (imported_from, import_losses, opened_by) = (st.imported_from.clone(), st.import_losses.clone(), st.opened_by.clone()); + if let Some(path) = st.path.as_deref() { + // Files are looked for within the asking caller's roots, else within those of the caller that opened the source. + let roots = crate::file_access::current().or_else(|| opened_by.clone()); + crate::file_access::confine(roots.as_ref(), || super::links::with_found_paths(&mut doc, path)); + } + doc.drop_edit_modes(); + if doc.pattern_edit.is_some() { + super::patterncmds::finish_edit(&mut doc, &mut vectorcraft_doc::Selection::default())?; + } + doc.puppet = None; + doc.new_art.set(Arc::default()); + let retired = vectorcraft_doc::RetiredArtboardId::default(); + retired.raise(doc.retired_artboard_id.get()); + doc.retired_artboard_id = retired; + doc.title = s.next_untitled(); + doc.metadata.created = clock_date(s, "created", created); + doc.metadata.modified = None; + let i = s.add_document(doc, None); + let st = s.doc_mut()?; + st.imported_from = imported_from; + st.import_losses = import_losses; + st.opened_by = st.opened_by.take().or(opened_by); + st.mark_unsaved(); + Ok(json!({ "index": i, "title": st.title() })) +} + /// `selection` for an undo (redo) step, sharing that of `like`, the step beside it, when the same. fn shared_selection(selection: vectorcraft_doc::Selection, like: Option<&crate::HistoryEntry>) -> Arc { match like { diff --git a/crates/engine/src/cmd/links.rs b/crates/engine/src/cmd/links.rs index bcda11c2b..6b1d45f7f 100644 --- a/crates/engine/src/cmd/links.rs +++ b/crates/engine/src/cmd/links.rs @@ -851,6 +851,28 @@ pub fn with_relative_paths(d: &Document, dest: &str) -> Option { Some(d) } +/// Sets each link of `d`, a copy of the document saved at `doc_path` that has no file of its own, +/// to the path where that document finds its file, so the copy finds the same files. +/// Links in the layers, symbols, graph designs, patterns and opacity masks are all set. +pub(crate) fn with_found_paths(d: &mut Document, doc_path: &str) { + // The new path of each link by what `locate` reads of it: its path, relative path and name. + let mut found: BTreeMap<(String, Option), Option> = BTreeMap::new(); + let mut look = |l: &LinkInfo| { + found.entry((l.path.clone(), l.relative.clone())).or_insert_with(|| locate(l, Some(doc_path)).map(|f| f.path)); + }; + d.visit_images(|_, im| im.link.iter().for_each(&mut look)); + d.visit_placed(|_, p| look(&p.link)); + found.retain(|(path, _), f| f.as_ref().is_some_and(|f| f != path)); + if found.is_empty() { + return; + } + d.update_links(|l| { + if let Some(Some(f)) = found.get(&(l.path.clone(), l.relative.clone())) { + l.path = f.clone(); + } + }); +} + // ---------- commands ---------- fn check(s: &mut Session, p: &Value) -> Result { diff --git a/crates/engine/src/cmd/patterncmds.rs b/crates/engine/src/cmd/patterncmds.rs index 8f1e99320..614a37d21 100644 --- a/crates/engine/src/cmd/patterncmds.rs +++ b/crates/engine/src/cmd/patterncmds.rs @@ -377,20 +377,24 @@ pub(crate) fn pattern_done(s: &mut Session, _: &Value) -> Result { if editing(s).is_some() { s.end_text_edit(); } - let name = s.edit("Done Editing Pattern", |d, sel| { - let (pe, art) = leave_edit(d, sel)?; - if let Some(def) = d.pattern_mut(&pe.pattern) { - def.art = art; - if def.size_tile_to_art { - def.fit_tile_to_art(); - } - } - Ok(pe.pattern) - })?; + let name = s.edit("Done Editing Pattern", finish_edit)?; after_edit_mode(s, None)?; Ok(json!({ "name": name })) } +/// Leave pattern editing mode in `d`, storing the editing layer's art as the pattern's tile art +/// (resizing the tile to the art when the pattern does so) → the pattern's name. +pub(crate) fn finish_edit(d: &mut Document, sel: &mut Selection) -> Result { + let (pe, art) = leave_edit(d, sel)?; + if let Some(def) = d.pattern_mut(&pe.pattern) { + def.art = art; + if def.size_tile_to_art { + def.fit_tile_to_art(); + } + } + Ok(pe.pattern) +} + fn pattern_cancel(s: &mut Session, _: &Value) -> Result { s.edit("Cancel Pattern Editing", |d, sel| { let (pe, _) = leave_edit(d, sel)?; diff --git a/crates/engine/src/command_sets.rs b/crates/engine/src/command_sets.rs index 4752b0a96..a115498e1 100644 --- a/crates/engine/src/command_sets.rs +++ b/crates/engine/src/command_sets.rs @@ -27,8 +27,9 @@ pub const FIT_ACTIVE_ARTBOARD: [&str; 2] = ["artboard.fitToArt", "artboard.fitTo /// [`ON_ACTIVE_ARTBOARD`] and [`FIT_ACTIVE_ARTBOARD`] commands and to a batch; the artboards the /// Artboard tool had selected for its commands; the view's center the app passes to Edit Symbol); /// an action leaves them out ([`Session::journal_for_action`]). -const REPLAY_ONLY: [(&str, &str); 21] = [ +const REPLAY_ONLY: [(&str, &str); 22] = [ ("file.new", "created"), + ("edit.newDocumentFromState", "created"), ("document.save", "modified"), ("file.saveAs", "modified"), ("edit.pasteInPlace", "artboard"), diff --git a/crates/engine/src/history.rs b/crates/engine/src/history.rs index cb84e108e..86c46fcf2 100644 --- a/crates/engine/src/history.rs +++ b/crates/engine/src/history.rs @@ -155,6 +155,17 @@ impl History { self.undo.iter().chain(&self.redo).map(|e| e.bytes).sum() } + /// The document at row `row` of the History panel, `now` being the document now: the rows before + /// it are the undo steps, oldest first, and the rows after it the redo steps, nearest first. + pub(crate) fn state<'a>(&'a self, now: &'a Arc, row: usize) -> Option<&'a Arc> { + let current = self.undo.len(); + match row.cmp(¤t) { + std::cmp::Ordering::Less => self.undo.get(row).map(|e| &e.doc), + std::cmp::Ordering::Equal => Some(now), + std::cmp::Ordering::Greater => self.redo.len().checked_sub(row - current).and_then(|i| self.redo.get(i)).map(|e| &e.doc), + } + } + /// Let the oldest steps go while over the step limit or the memory budget: undo steps first /// (the oldest), then redo steps (the furthest ahead). pub fn trim(&mut self) { diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 862bc4b77..e2fe63c17 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -70,6 +70,7 @@ mod gradpresets; mod gradspan; mod guide_clipboard; mod halftone; +mod history_document; mod history_mem; mod ids_without_selection; mod inline; diff --git a/crates/engine/src/tests/history_document.rs b/crates/engine/src/tests/history_document.rs new file mode 100644 index 000000000..0c78b1109 --- /dev/null +++ b/crates/engine/src/tests/history_document.rs @@ -0,0 +1,361 @@ +//! History panel › New Document from State (`edit.newDocumentFromState`). + +use std::sync::Arc; + +use serde_json::{Value, json}; + +use super::{rect, session}; +use crate::tests_links::{Folder, RED, place, png, write}; +use crate::*; + +const CMD: &str = "edit.newDocumentFromState"; + +fn objects(s: &Session) -> usize { + s.doc().unwrap().doc.layers[0].children().unwrap().len() +} + +fn x(s: &Session, id: NodeId) -> f64 { + s.doc().unwrap().doc.node(id).unwrap().geometric_bounds().unwrap().x0 +} + +fn steps(s: &Session) -> (usize, usize) { + let h = &s.doc().unwrap().history; + (h.undo.len(), h.redo.len()) +} + +/// Three rectangles, the last one undone: rows 0 to 3 hold 0 to 3 of them, the current one row 2. +fn three_rectangles_one_undone() -> Session { + let mut s = session(); + for x in [10.0, 200.0, 400.0] { + rect(&mut s, x, 10.0, 100.0, 50.0); + } + s.execute("edit.undo", &json!({})).unwrap(); + s +} + +/// Each row gives the art of its state, the steps to redo included. An omitted or null index gives +/// the current state. Each new document is added and made active. +#[test] +fn a_new_document_holds_the_art_of_the_state() { + let params = [ + (json!({"index": 0}), 0), + (json!({"index": 1}), 1), + (json!({"index": 2}), 2), + (json!({"index": 3}), 3), + (json!({}), 2), + (json!({"index": null}), 2), + ]; + for (p, art) in params { + let mut s = three_rectangles_one_undone(); + let r = s.execute(CMD, &p).unwrap(); + assert_eq!((r["index"].as_u64(), s.active_index()), (Some(1), Some(1)), "{p}"); + assert_eq!(objects(&s), art, "{p}"); + assert_eq!(s.documents().len(), 2); + } +} + +/// The source's document, steps, selection and saved state stay as they are, for a modified source +/// and for one at its saved state. +#[test] +fn the_source_stays_as_it_is() { + let mut s = three_rectangles_one_undone(); + let source = s.doc().unwrap().doc.clone(); + let selection = s.doc().unwrap().selection.clone(); + assert!(s.doc().unwrap().is_dirty()); + s.execute(CMD, &json!({"index": 0})).unwrap(); + let st = &s.documents()[0]; + assert!(Arc::ptr_eq(&st.doc, &source)); + assert_eq!((st.history.undo.len(), st.history.redo.len()), (2, 1)); + assert_eq!(st.selection, selection); + assert!(st.is_dirty()); + + let mut s = session(); + rect(&mut s, 10.0, 10.0, 100.0, 50.0); + s.execute("edit.undo", &json!({})).unwrap(); + assert!(!s.doc().unwrap().is_dirty()); + s.execute(CMD, &json!({"index": 1})).unwrap(); + assert!(!s.documents()[0].is_dirty(), "the source stays at its saved state"); + assert!(s.doc().unwrap().is_dirty(), "the new document is unsaved"); +} + +/// An index that names no state, or is not a whole number, is an error and changes nothing: no +/// document, no step and no Untitled number is used up. +#[test] +fn a_bad_index_is_an_error_and_changes_nothing() { + let mut s = three_rectangles_one_undone(); + let source = s.doc().unwrap().doc.clone(); + let untitled = s.peek_untitled(); + let bad: [Value; 8] = [json!(4), json!(99), json!(-1), json!(1.5), json!("1"), json!(true), json!([1]), json!(u64::MAX)]; + for index in bad { + let e = s.execute(CMD, &json!({ "index": index })).unwrap_err(); + assert!(matches!(e, EngineError::BadParams { .. }), "{index}: {e}"); + assert!(e.to_string().contains(&format!("no history state {index}")), "{e}"); + assert_eq!(s.documents().len(), 1, "{index}"); + assert_eq!(steps(&s), (2, 1), "{index}"); + assert!(Arc::ptr_eq(&s.doc().unwrap().doc, &source), "{index}"); + assert_eq!(s.peek_untitled(), untitled, "{index}"); + } + let e = s.execute(CMD, &json!({"created": "today"})).unwrap_err(); + assert!(matches!(e, EngineError::BadParams { .. }), "{e}"); + assert_eq!(s.documents().len(), 1); +} + +/// With no document the command is disabled. +#[test] +fn it_needs_a_document() { + let mut s = Session::new(); + assert!(matches!(s.execute(CMD, &json!({})), Err(EngineError::Disabled(..)))); +} + +/// An edit in either document leaves the other as it is, and so does taking an artboard id. +#[test] +fn the_two_documents_are_independent() { + let mut s = session(); + let a = rect(&mut s, 10.0, 10.0, 100.0, 50.0); + rect(&mut s, 200.0, 10.0, 100.0, 50.0); + s.execute(CMD, &json!({"index": 1})).unwrap(); + s.execute("object.move", &json!({"ids": [a.0], "dx": 30, "dy": 0})).unwrap(); + rect(&mut s, 400.0, 10.0, 100.0, 50.0); + assert_eq!((x(&s, a), objects(&s)), (40.0, 2)); + s.execute("document.activate", &json!({"index": 0})).unwrap(); + assert_eq!((x(&s, a), objects(&s)), (10.0, 2), "the source"); + s.execute("object.move", &json!({"ids": [a.0], "dx": 5, "dy": 0})).unwrap(); + assert_eq!(x(&s, a), 15.0); + let source_board = s.doc().unwrap().doc.next_artboard_id(); + s.execute("document.activate", &json!({"index": 1})).unwrap(); + assert_eq!((x(&s, a), objects(&s)), (40.0, 2), "the new document"); + assert!(s.doc().unwrap().doc.retired_artboard_id.get() < source_board, "an artboard id the source took"); +} + +/// The new document starts with no steps, no selection, no path and the next untitled name, and is +/// unsaved. Undo and Redo then work in it alone, and undoing back to the start leaves it unsaved. +#[test] +fn undo_in_the_new_document() { + let mut s = three_rectangles_one_undone(); + s.execute("select.all", &json!({})).unwrap(); + let r = s.execute(CMD, &json!({"index": 1})).unwrap(); + assert_eq!(r["title"], "Untitled-2"); + let st = s.doc().unwrap(); + assert_eq!(st.title(), "Untitled-2"); + assert_eq!(st.path, None); + assert!(st.selection.objects.is_empty()); + assert!(st.is_dirty()); + let start = st.doc.clone(); + assert_eq!(steps(&s), (0, 0)); + assert!(matches!(s.execute("edit.undo", &json!({})), Err(EngineError::Disabled(..))), "the source's steps are not its own"); + rect(&mut s, 600.0, 10.0, 100.0, 50.0); + assert_eq!(objects(&s), 2); + s.execute("edit.undo", &json!({})).unwrap(); + assert_eq!(objects(&s), 1); + assert!(Arc::ptr_eq(&s.doc().unwrap().doc, &start)); + assert!(s.doc().unwrap().is_dirty(), "still unsaved at the state it started from"); + s.execute("edit.redo", &json!({})).unwrap(); + assert_eq!(objects(&s), 2); + assert_eq!(s.documents()[0].history.undo.len(), 2, "the source's steps"); +} + +/// A drag in progress is first kept as a step and its change goes into the new document. An index +/// names the state as listed before that, and a bad one leaves the drag going on. +#[test] +fn a_drag_in_progress_is_kept_first() { + let mut s = session(); + let a = rect(&mut s, 10.0, 10.0, 100.0, 50.0); + s.begin_interaction("Move").unwrap(); + s.preview("object.move", &json!({"dx": 30, "dy": 0})).unwrap(); + assert!(s.execute(CMD, &json!({"index": 2})).is_err()); + assert!(s.in_interaction(), "a bad index ends nothing"); + assert_eq!(steps(&s), (1, 0)); + s.execute(CMD, &json!({"index": 0})).unwrap(); + assert_eq!(objects(&s), 0, "the row listed as 0"); + assert_eq!(x_of_source(&s, a), 40.0); + assert_eq!(s.documents()[0].history.undo.len(), 2, "the drag is a step in the source"); + + let mut s = session(); + let a = rect(&mut s, 10.0, 10.0, 100.0, 50.0); + s.begin_interaction("Move").unwrap(); + s.preview("object.move", &json!({"dx": 30, "dy": 0})).unwrap(); + s.execute(CMD, &json!({})).unwrap(); + assert_eq!(x(&s, a), 40.0, "the current state holds the drag"); + assert_eq!(x_of_source(&s, a), 40.0); + assert!(s.documents()[0].interaction.is_none()); +} + +fn x_of_source(s: &Session, id: NodeId) -> f64 { + s.documents()[0].doc.node(id).unwrap().geometric_bounds().unwrap().x0 +} + +/// A state saved while editing a symbol gives a document out of that mode and without the +/// temporary editing layer. The source stays in the mode. +#[test] +fn a_new_document_leaves_the_editing_modes() { + let mut s = session(); + rect(&mut s, 10.0, 10.0, 100.0, 50.0); + s.execute("symbol.new", &json!({"name": "Box"})).unwrap(); + let layers = s.doc().unwrap().doc.layers.len(); + s.execute("symbol.edit", &json!({})).unwrap(); + assert!(s.doc().unwrap().doc.symbol_edit.is_some()); + assert_eq!(s.doc().unwrap().doc.layers.len(), layers + 1); + s.execute(CMD, &json!({})).unwrap(); + let st = s.doc().unwrap(); + assert!(st.doc.symbol_edit.is_none()); + assert_eq!(st.doc.layers.len(), layers); + assert!(s.documents()[0].doc.symbol_edit.is_some()); +} + +/// A state saved while editing a pattern gives a document out of that mode, without the editing +/// layer, and with the art drawn in the tile stored in the pattern, as Done stores it. The source +/// stays in the mode. +#[test] +fn a_new_document_finishes_pattern_editing() { + let mut s = session(); + let r = rect(&mut s, 0.0, 0.0, 10.0, 10.0); + s.execute("select.set", &json!({"ids": [r.0]})).unwrap(); + let layers = s.doc().unwrap().doc.layers.len(); + s.execute("object.pattern.make", &json!({"name": "P"})).unwrap(); + rect(&mut s, 2.0, 2.0, 3.0, 3.0); + assert_eq!(s.doc().unwrap().doc.layers.len(), layers + 1); + s.execute(CMD, &json!({})).unwrap(); + assert_eq!(s.active_index(), Some(1), "the new document"); + let st = s.doc().unwrap(); + assert!(st.doc.pattern_edit.is_none()); + assert_eq!(st.doc.layers.len(), layers); + assert_eq!(st.doc.pattern("P").map(|p| p.art.len()), Some(2)); + assert_eq!(st.isolation, None); + assert!(s.documents()[0].doc.pattern_edit.is_some()); +} + +/// A linked image still finds its file from the new document, which has no folder of its own, and +/// its pixels come along. +#[test] +fn links_resolve_in_the_new_document() { + let dir = Folder::new("history-document-links"); + let pic = dir.file("photo.png"); + write(&pic, &png(60, 30, RED)); + let mut s = session(); + let id = place(&mut s, &pic); + let list = |s: &mut Session| s.execute("links.list", &json!({})).unwrap(); + assert_eq!(list(&mut s)["links"][0]["status"], "ok"); + s.execute(CMD, &json!({})).unwrap(); + assert_eq!(s.active_index(), Some(1), "the new document"); + let links = list(&mut s); + assert_eq!((&links["links"][0]["status"], &links["links"][0]["path"]), (&json!("ok"), &json!(pic)), "{links}"); + let st = s.doc().unwrap(); + assert!(st.doc.node(id).is_some()); + assert_eq!(st.doc.images.len(), s.documents()[0].doc.images.len()); +} + +/// A linked file the source finds in its own folder, away from the link's path, is found from the +/// new document too: its link takes the path where the source finds the file. +#[test] +fn a_file_the_source_finds_in_its_folder_is_found_from_the_new_document() { + let a = Folder::new("history-document-moved-a"); + let b = Folder::new("history-document-moved-b"); + let pic = a.file("photo.png"); + write(&pic, &png(60, 30, RED)); + let mut s = session(); + place(&mut s, &pic); + s.execute("document.save", &json!({"path": b.file("poster.vectorcraft")})).unwrap(); + let moved = b.file("photo.png"); + std::fs::rename(&pic, &moved).unwrap(); + let check = |s: &mut Session| s.execute("links.check", &json!({})).unwrap(); + assert_eq!(check(&mut s)["links"][0]["status"], "ok", "the source finds it by name in its folder"); + s.execute(CMD, &json!({})).unwrap(); + assert_eq!(s.active_index(), Some(1), "the new document"); + let c = check(&mut s); + assert_eq!((&c["links"][0]["status"], &c["links"][0]["path"]), (&json!("ok"), &json!(moved)), "{c}"); +} + +/// The same holds for a linked image only a symbol shows. +#[test] +fn a_file_a_symbol_shows_is_found_from_the_new_document() { + let a = Folder::new("history-document-symbol-a"); + let b = Folder::new("history-document-symbol-b"); + let pic = a.file("photo.png"); + write(&pic, &png(60, 30, RED)); + let mut s = session(); + place(&mut s, &pic); + s.execute("symbol.new", &json!({"name": "Photo"})).unwrap(); + s.execute("document.save", &json!({"path": b.file("poster.vectorcraft")})).unwrap(); + let moved = b.file("photo.png"); + std::fs::rename(&pic, &moved).unwrap(); + s.execute(CMD, &json!({})).unwrap(); + assert_eq!(s.active_index(), Some(1), "the new document"); + let d = &s.doc().unwrap().doc; + assert!(!d.symbols.is_empty()); + let mut paths = vec![]; + d.visit_images(|_, im| paths.extend(im.link.as_ref().map(|l| l.path.clone()))); + assert!(!paths.is_empty() && paths.iter().all(|p| *p == moved), "{paths:?}"); +} + +/// Files are looked for only within the roots of the caller that opened the source, or of a +/// confined caller asking now: a file outside them is not linked from the new document. +#[test] +fn a_file_outside_the_callers_roots_is_not_looked_for() { + let a = Folder::new("history-document-confined-a"); + let b = Folder::new("history-document-confined-b"); + let other = Folder::new("history-document-confined-other"); + let pic = a.file("photo.png"); + write(&pic, &png(60, 30, RED)); + let mut s = session(); + place(&mut s, &pic); + s.execute("document.save", &json!({"path": b.file("poster.vectorcraft")})).unwrap(); + let moved = b.file("photo.png"); + std::fs::rename(&pic, &moved).unwrap(); + let narrow = crate::file_access::AutomationRoots::new(&[&other.0], &[]).unwrap().unwrap(); + let link = |s: &Session| { + let mut paths = vec![]; + s.doc().unwrap().doc.visit_images(|_, im| paths.extend(im.link.as_ref().map(|l| l.path.clone()))); + paths + }; + crate::file_access::confine(Some(&narrow), || s.execute(CMD, &json!({}))).unwrap(); + assert_eq!(s.active_index(), Some(1), "the new document"); + assert!(link(&s).iter().all(|p| *p != moved), "a confined caller: {:?}", link(&s)); + s.execute("document.activate", &json!({"index": 0})).unwrap(); + s.active_mut().unwrap().opened_by = Some(narrow); + s.execute(CMD, &json!({})).unwrap(); + assert_eq!(s.active_index(), Some(2), "the second new document"); + let paths = link(&s); + assert!(!paths.is_empty() && paths.iter().all(|p| *p != moved), "opened by a confined caller: {paths:?}"); +} + +/// What reading the source's file left out still guards that file from the new document, and the +/// new document is read within what the caller that opened the source may read. +#[test] +fn the_new_document_keeps_what_guards_the_source() { + let dir = Folder::new("history-document-roots"); + let roots = crate::file_access::AutomationRoots::new(&[&dir.0], &[]).unwrap().unwrap(); + let mut s = session(); + let st = s.active_mut().unwrap(); + st.imported_from = Some("art.ai".into()); + st.import_losses = vec!["hidden text".into()]; + st.opened_by = Some(roots.clone()); + s.execute(CMD, &json!({})).unwrap(); + assert_eq!(s.active_index(), Some(1), "the new document"); + let st = s.doc().unwrap(); + assert_eq!((st.imported_from.as_deref(), st.import_losses.as_slice()), (Some("art.ai"), &["hidden text".to_string()][..])); + assert!(st.opened_by.as_ref().is_some_and(|r| Arc::ptr_eq(r, &roots))); +} + +/// The created date is the new document's own and joins the journal entry, so a replay gives the +/// same date. An action leaves it out. The new document has no modified date until it is saved. +#[test] +fn the_created_date_is_the_new_documents_own() { + let mut s = session(); + Arc::make_mut(&mut s.active_mut().unwrap().doc).metadata.modified = Some(500); + let source = s.doc().unwrap().doc.metadata.created; + s.execute(CMD, &json!({"created": 1000})).unwrap(); + assert_eq!(s.doc().unwrap().doc.metadata.created, Some(1000)); + assert_eq!(s.doc().unwrap().doc.metadata.modified, None, "the new document has not been saved"); + assert_eq!(s.documents()[0].doc.metadata.modified, Some(500)); + assert_eq!(s.documents()[0].doc.metadata.created, source); + s.execute("document.activate", &json!({"index": 0})).unwrap(); + s.execute(CMD, &json!({"created": null})).unwrap(); + assert_eq!(s.doc().unwrap().doc.metadata.created, None); + s.execute("document.activate", &json!({"index": 0})).unwrap(); + s.execute(CMD, &json!({})).unwrap(); + let noted = s.journal.last().unwrap().1["created"].as_i64(); + assert!(noted.is_some() && noted == s.doc().unwrap().doc.metadata.created, "{noted:?}"); + let mut action = s.journal.last().unwrap().1.clone(); + crate::command_sets::strip_replay_only(CMD, &mut action); + assert_eq!(action, json!({}), "an action leaves the date out"); +} diff --git a/crates/engine/tests/command_sweep.rs b/crates/engine/tests/command_sweep.rs index 42970aa16..6a92ec0d9 100644 --- a/crates/engine/tests/command_sweep.rs +++ b/crates/engine/tests/command_sweep.rs @@ -911,7 +911,15 @@ fn every_command_undoes_exactly() { let mut s = Fixture::Multi.session(); let before = doc_json(&s.doc().unwrap().doc); let depth = s.doc().unwrap().history.undo.len(); + let source = s.active_index().unwrap(); let Ok(Ok(_)) = catch_quiet(|| s.execute(c.id, &json!({}))) else { continue }; + // A command that makes a new document and makes it active leaves the source document as it is. + if s.active_index() != Some(source) { + if s.documents().get(source).map(|d| doc_json(&d.doc)).as_ref() != Some(&before) { + failures.push(format!("{}: changed the source document while making another active", c.id)); + } + continue; + } if s.doc().unwrap().history.undo.len() > depth { let _ = no_panic(s.execute("edit.undo", &json!({}))); let after = doc_json(&s.doc().unwrap().doc); diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index b52c4087a..f00836a0a 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -6159,6 +6159,32 @@ fn clear_history_waits_while_the_ime_composes() { assert!(crate::menus::enabled(&app, "edit.clearHistory")); } +/// New Document from State waits while an IME composes, as Undo does: no document is made and the +/// composition goes on. +#[test] +fn new_document_from_state_waits_while_the_ime_composes() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.prefs.placeholder_text = false; + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let ctx = egui::Context::default(); + typing_frame(&mut app, &ctx, vec![]); + app.select_tool("type"); + let xf = Xf::new(app.canvas_rect.unwrap(), app.view().unwrap()); + let at = xf.to_screen(Point::new(100.0, 100.0)); + let click = + |p: Pos2, pressed| egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + typing_frame(&mut app, &ctx, vec![egui::Event::PointerMoved(at), click(at, true)]); + typing_frame(&mut app, &ctx, vec![click(at, false)]); + typing_frame(&mut app, &ctx, vec![preedit("が", 1)]); + assert!(app.session.tool_composing()); + assert!(!crate::menus::enabled(&app, "edit.newDocumentFromState")); + crate::menus::invoke(&mut app, "edit.newDocumentFromState", json!({})); + assert!(app.session.tool_composing(), "the composition goes on"); + assert_eq!(app.session.documents().len(), 1); + typing_frame(&mut app, &ctx, vec![preedit("", 0), egui::Event::Ime(egui::ImeEvent::Commit("雅".into()))]); + assert!(crate::menus::enabled(&app, "edit.newDocumentFromState")); +} + #[test] fn clicking_away_mid_composition_keeps_the_text_and_interrupts_the_ime() { let mut app = VectorcraftApp::new(Session::new(), Default::default()); diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index aed900147..976dce077 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -1401,6 +1401,7 @@ font-technology Variable Променливи Step Backward Стъпка назад Step Forward Стъпка напред Clear History Изчистване на историята + New Document from State Нов документ от състоянието Set History Limit… Задаване на ограничение на историята… History States Състояния на историята # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4241,6 +4242,7 @@ axis Both И двете @msg nothing to save: the document has no visible art няма нищо за запазване: документът няма видима графика @msg nothing to undo няма какво да се отмени @msg no history to clear няма история за изчистване +@msg no history state {v} няма състояние в историята {v} @msg nothing to warp няма нищо за извиване @msg nothing to wrap няма нищо за обвиване @msg object {0} cannot have children обектът {0} не може да има дъщерни елементи diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index eef02974f..a3d05a026 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -1612,6 +1612,7 @@ digits Malayalam Malajalam Step Backward Korak unazad Step Forward Korak naprijed Clear History Očisti historiju + New Document from State Novi dokument iz stanja Set History Limit… Postavi ograničenje historije… Open a document to see its history. Otvorite dokument da vidite njegovu historiju. Apply Data Set Primijeni skup podataka @@ -4637,6 +4638,7 @@ imageTrace Outline Obris @msg nothing to save: the document has no visible art nema ničeg za čuvanje: dokument nema vidljiv crtež @msg nothing to undo nema ničeg za poništavanje @msg no history to clear nema historije za brisanje +@msg no history state {v} nema stanja historije {v} @msg nothing to warp nema ničeg za iskrivljenje @msg nothing to wrap nema ničeg za omotavanje @msg object {0} cannot have children objekat {0} ne može imati djecu diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index dbf6d691f..6eb76fbdd 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -652,6 +652,7 @@ Clear Events Esborra els esdeveniments Clear Guides Esborra les guies Clear History Esborra l’historial + New Document from State Document nou a partir de l’estat Clear Overrides Esborra les modificacions Clear Recent Files Esborra els fitxers recents Click Fes clic @@ -5286,6 +5287,7 @@ digits Malayalam Malaiàlam @msg no graphic style `{name}` no hi ha l’estil gràfic `{name}` @msg no graphic style `{n}` no hi ha l’estil gràfic `{n}` @msg no history to clear no hi ha historial per esborrar +@msg no history state {v} no hi ha cap estat de l’historial {v} @msg no interaction in progress no hi ha cap interacció en curs @msg no key object no hi ha objecte clau @msg no library `{id}` no hi ha la biblioteca `{id}` diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 6711c58ef..390e35eb4 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1636,6 +1636,7 @@ digits Malayalam Malajálamské Step Backward Krok zpět Step Forward Krok vpřed Clear History Vymazat historii + New Document from State Nový dokument ze stavu Set History Limit… Nastavit limit historie… Open a document to see its history. Otevřete dokument, abyste viděli jeho historii. # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4684,6 +4685,7 @@ imageTrace Outline Obrys @msg nothing to save: the document has no visible art není co uložit: dokument nemá žádnou viditelnou kresbu @msg nothing to undo není co vrátit @msg no history to clear není žádná historie k vymazání +@msg no history state {v} žádný stav historie {v} @msg nothing to warp není co pokřivit @msg nothing to wrap není co obtékat @msg object {0} cannot have children objekt {0} nemůže mít potomky diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index de8fa2ed6..b5a2946d2 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1546,6 +1546,7 @@ digits Malayalam Malayalam Step Backward Et trin tilbage Step Forward Et trin frem Clear History Ryd historik + New Document from State Nyt dokument fra tilstand Set History Limit… Angiv historikgrænse… Open a document to see its history. Åbn et dokument for at se dets historik. Abutting Stødende @@ -4339,6 +4340,7 @@ imageTrace Outline Kontur @msg nothing to save: the document has no visible art intet at gemme: dokumentet har ingen synlig grafik @msg nothing to undo intet at fortryde @msg no history to clear ingen historik at rydde +@msg no history state {v} ingen historiktilstand {v} @msg nothing to warp intet at forvride @msg nothing to wrap intet at ombryde @msg object {0} cannot have children objektet {0} kan ikke have underobjekter diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 67c5e629f..3bdbfabbe 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -1432,6 +1432,7 @@ file-filter Templates Vorlagen Step Backward Schritt zurück Step Forward Schritt vorwärts Clear History Protokoll löschen + New Document from State Neues Dokument aus Protokollschritt Set History Limit… Protokollgrenze festlegen… History States Protokollschritte # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4294,6 +4295,7 @@ theme Appearance Erscheinungsbild @msg nothing to save: the document has no visible art nichts zu speichern: Das Dokument enthält keine sichtbare Grafik @msg nothing to undo nichts rückgängig zu machen @msg no history to clear kein Protokoll zu löschen +@msg no history state {v} kein Protokollschritt {v} @msg nothing to warp nichts zu verkrümmen @msg nothing to wrap nichts zu umfließen @msg object {0} cannot have children Objekt {0} kann keine untergeordneten Objekte haben diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index d9d5614a2..4e5c347c0 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -1371,6 +1371,7 @@ font-technology Variable Μεταβλητές Step Backward Ένα βήμα πίσω Step Forward Ένα βήμα εμπρός Clear History Εκκαθάριση ιστορικού + New Document from State Νέο έγγραφο από κατάσταση Set History Limit… Ορισμός ορίου ιστορικού… History States Καταστάσεις ιστορικού # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4333,6 +4334,7 @@ digits Malayalam Μαλαγιάλαμ @msg nothing to save: the document has no visible art δεν υπάρχει τίποτα για αποθήκευση: το έγγραφο δεν έχει ορατό σχέδιο @msg nothing to undo δεν υπάρχει τίποτα για αναίρεση @msg no history to clear δεν υπάρχει ιστορικό για εκκαθάριση +@msg no history state {v} δεν υπάρχει κατάσταση ιστορικού {v} @msg nothing to warp δεν υπάρχει τίποτα για στρέβλωση @msg nothing to wrap δεν υπάρχει τίποτα για αναδίπλωση @msg object {0} cannot have children το αντικείμενο {0} δεν μπορεί να έχει θυγατρικά στοιχεία diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 678f97bb5..a9a940eee 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -1451,6 +1451,7 @@ font-technology Variable Variables Step Backward Paso atrás Step Forward Paso adelante Clear History Borrar historial + New Document from State Nuevo documento desde estado Set History Limit… Definir límite de historial… History States Estados de historial # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4378,6 +4379,7 @@ axis Both Ambos @msg nothing to save: the document has no visible art no hay nada que guardar: el documento no tiene ilustración visible @msg nothing to undo no hay nada que deshacer @msg no history to clear no hay historial que borrar +@msg no history state {v} no hay ningún estado del historial {v} @msg nothing to warp no hay nada que deformar @msg nothing to wrap no hay nada que envolver @msg object {0} cannot have children el objeto {0} no puede tener hijos diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index ef68bedd1..625a93eb7 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1609,6 +1609,7 @@ digits Malayalam Malajalam Step Backward Askel taaksepäin Step Forward Askel eteenpäin Clear History Tyhjennä historia + New Document from State Uusi asiakirja tilasta Set History Limit… Aseta historian raja… Open a document to see its history. Avaa asiakirja, niin näet sen historian. # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4592,6 +4593,7 @@ imageTrace Outline Ääriviiva @msg nothing to save: the document has no visible art ei tallennettavaa: asiakirjassa ei ole näkyvää kuvitusta @msg nothing to undo ei kumottavaa @msg no history to clear ei tyhjennettävää historiaa +@msg no history state {v} ei historiatilaa {v} @msg nothing to warp ei taivutettavaa @msg nothing to wrap ei rivitettävää @msg object {0} cannot have children objektilla {0} ei voi olla alikohteita diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 80344c35d..761c44bc9 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -1445,6 +1445,7 @@ font-technology Variable Variables Step Backward Pas en arrière Step Forward Pas en avant Clear History Effacer l’historique + New Document from State Nouveau document à partir de l’état Set History Limit… Définir la limite de l’historique… History States États de l’historique # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4368,6 +4369,7 @@ axis Both Les deux @msg nothing to save: the document has no visible art rien à enregistrer : le document n’a aucune illustration visible @msg nothing to undo rien à annuler @msg no history to clear aucun historique à effacer +@msg no history state {v} aucun état d’historique {v} @msg nothing to warp rien à déformer @msg nothing to wrap rien à envelopper @msg object {0} cannot have children l’objet {0} ne peut pas avoir d’enfants diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index d181ae8dd..cad5a6781 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -745,6 +745,7 @@ Clear Events घटनाओं को साफ़ करें Clear Guides गाइड्स हटाएँ Clear History हिस्ट्री साफ़ करें + New Document from State स्थिति से नया दस्तावेज़ Clear Overrides साफ़ ओवरराइड्स Clear Recent Files हाल की फ़ाइलें साफ़ करें Clear Selection सिलेक्शन साफ़ करें @@ -6615,6 +6616,7 @@ @msg no guide selected कोई गाइड सिलेक्टेड नहीं @msg no guide selected (or give an index) कोई गाइड नहीं चुना गया (या एक सूचकांक देना) @msg no history to clear स्पष्ट करने के लिए कोई इतिहास नहीं +@msg no history state {v} कोई हिस्ट्री स्थिति {v} नहीं @msg no images in the folder फ़ोल्डर में कोई इमेज नहीं @msg no interaction in progress प्रगति में कोई बातचीत नहीं @msg no item at index {_1} इंडेक्स में कोई आइटम नहीं {_1} diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index dea4dff15..f53038da8 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1846,6 +1846,7 @@ file-filter Templates Sablonok Step Backward Lépés vissza Step Forward Lépés előre Clear History Előzmények törlése + New Document from State Új dokumentum az állapotból Set History Limit… Előzménykorlát beállítása… History States Előzmények lépésszáma # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -5058,6 +5059,7 @@ imageTrace Outline Körvonal @msg nothing to save: the document has no visible art nincs mit menteni: a dokumentumban nincs látható grafika @msg nothing to undo nincs mit visszavonni @msg no history to clear nincs törölhető előzmény +@msg no history state {v} nincs {v} előzményállapot @msg nothing to warp nincs mit torzítani @msg nothing to wrap nincs mit körbefuttatni @msg object ids are non-negative integers, not {v} az objektumazonosító nemnegatív egész szám, ez nem az: {v} diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index 837d84f32..b882af832 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1546,6 +1546,7 @@ digits Malayalam Malayalam Step Backward Mundur satu langkah Step Forward Maju satu langkah Clear History Hapus riwayat + New Document from State Dokumen baru dari status Set History Limit… Atur batas riwayat… Open a document to see its history. Buka dokumen untuk melihat riwayatnya. Abutting Bersinggungan @@ -4339,6 +4340,7 @@ imageTrace Outline Kontur @msg nothing to save: the document has no visible art tidak ada yang dapat disimpan: dokumen tidak memiliki karya seni yang terlihat @msg nothing to undo tidak ada yang dapat diurungkan @msg no history to clear tidak ada riwayat yang dapat dihapus +@msg no history state {v} tidak ada status riwayat {v} @msg nothing to warp tidak ada yang dapat diubah bentuknya @msg nothing to wrap tidak ada yang dapat dibungkus @msg object {0} cannot have children objek {0} tidak dapat memiliki anak diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index dfa7856d4..474df13a5 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -1463,6 +1463,7 @@ file-filter Templates Modelli Step Backward Passo indietro Step Forward Passo avanti Clear History Cancella cronologia + New Document from State Nuovo documento dallo stato Set History Limit… Imposta limite cronologia… History States Stati cronologia # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4420,6 +4421,7 @@ axis Both Entrambi @msg nothing to save: the document has no visible art niente da salvare: il documento non ha grafica visibile @msg nothing to undo niente da annullare @msg no history to clear nessuna cronologia da cancellare +@msg no history state {v} nessuno stato della cronologia {v} @msg nothing to warp niente da alterare @msg nothing to wrap niente da avvolgere @msg object {0} cannot have children l'oggetto {0} non può avere elementi figli diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index a2bbe2630..3fcdc449e 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1774,6 +1774,7 @@ digits Malayalam マラヤーラム数字 Step Backward 一段階戻る Step Forward 一段階進む Clear History ヒストリーを消去 + New Document from State 状態から新規ドキュメント Set History Limit… ヒストリーの上限を設定… History States ヒストリー数 # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4168,6 +4169,7 @@ swatch Bright Violet ビビッドバイオレット @msg nothing to redo やり直す操作がありません @msg nothing to undo 取り消す操作がありません @msg no history to clear 消去するヒストリーがありません +@msg no history state {v} ヒストリーの状態 {v} はありません @msg Expand Appearance: the selection has a basic appearance アピアランスを分割: 選択範囲は基本アピアランスです @msg select (or target) objects whose appearance isn't basic 基本アピアランスではないオブジェクトを選択するか、ターゲットに指定してください @msg {C}: no effect at index {from} {C}: インデックス {from} に効果がありません diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index 66d7e438a..9c45ce547 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -5564,6 +5564,7 @@ imageTrace Outline კონტური @plural {n} more export waiting|{n} more exports waiting რიგში მომლოდინე სხვა ექსპორტირება: {n} @msg Export cancelled ექსპორტირება გაუქმდა Clear History ისტორიის გასუფთავება + New Document from State ახალი დოკუმენტი მდგომარეობიდან Continue in Background ფონურ რეჟიმში გაგრძელება Artboard {n} of {total} სამუშაო დაფა {n} / {total} File {n} of {total} ფაილი {n} / {total} @@ -5582,6 +5583,7 @@ imageTrace Outline კონტური @msg its editing data is for a {w} × {h} pt page, not the file's {pw} × {ph} pt page მისი რედაქტირების მონაცემები {w} × {h} pt გვერდს ეკუთვნის და არა ფაილის {pw} × {ph} pt გვერდს @msg its editing data is for a {w} × {h} pt page, the file's {pw} × {ph} pt page at {scale} მისი რედაქტირების მონაცემები {w} × {h} pt გვერდს ეკუთვნის: ეს ფაილის {pw} × {ph} pt გვერდია მასშტაბით {scale} @msg no history to clear გასასუფთავებელი ისტორია არ არის +@msg no history state {v} ისტორიის მდგომარეობა {v} არ არსებობს @msg {_1} files are written by file.saveAsTemplate {_1} ფაილებს წერს file.saveAsTemplate @msg aiVersion 7 writes legacy .ai files, not .ait templates aiVersion 7 წერს ძველი ფორმატის .ai ფაილებს და არა .ait შაბლონებს @msg the file's variable bindings weren't all read (bindings skipped: {_1}): those objects aren't bound to their variables ფაილის ცვლადების ყველა კავშირი არ იქნა წაკითხული (გამოტოვებული კავშირები: {_1}): ეს ობიექტები არ არის დაკავშირებული თავიანთ ცვლადებთან diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index 2f8a0d7c4..6f76d1297 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1764,6 +1764,7 @@ digits Malayalam 말라얄람어 Step Backward 한 단계 뒤로 Step Forward 한 단계 앞으로 Clear History 히스토리 지우기 + New Document from State 상태에서 새 문서 만들기 Set History Limit… 히스토리 한도 설정… History States 히스토리 상태 # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4095,6 +4096,7 @@ theme Appearance 외관 @msg nothing to redo 다시 실행할 항목이 없습니다 @msg nothing to undo 실행 취소할 항목이 없습니다 @msg no history to clear 지울 히스토리가 없습니다 +@msg no history state {v} 히스토리 상태 {v}이(가) 없습니다 @msg Expand Appearance: the selection has a basic appearance 모양새 확장: 선택 항목은 기본 모양새입니다 @msg select (or target) objects whose appearance isn't basic 모양새가 기본이 아닌 개체를 선택(또는 대상 지정)하세요 @msg {C}: no effect at index {from} {C}: 인덱스 {from}에 효과가 없습니다 diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index cb4562bb5..323d05a9d 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -1560,6 +1560,7 @@ digits Malayalam Malayalam Step Backward Stap terug Step Forward Stap vooruit Clear History Historie wissen + New Document from State Nieuw document uit status Set History Limit… Historielimiet instellen… Open a document to see its history. Open een document om de historie te zien. Abutting Aaneengesloten @@ -4351,6 +4352,7 @@ imageTrace Outline Contour @msg nothing to save: the document has no visible art niets om op te slaan: het document heeft geen zichtbare illustratie @msg nothing to undo niets om ongedaan te maken @msg no history to clear geen historie om te wissen +@msg no history state {v} geen historiestatus {v} @msg nothing to warp niets om te buigen @msg nothing to wrap niets om te laten omlopen @msg object {0} cannot have children object {0} kan geen onderliggende objecten hebben diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index eb5a3aebd..f16d0160c 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1549,6 +1549,7 @@ digits Malayalam Malajalam Step Backward Krok wstecz Step Forward Krok naprzód Clear History Wyczyść historię + New Document from State Nowy dokument ze stanu Set History Limit… Ustaw limit historii… Open a document to see its history. Otwórz dokument, aby zobaczyć jego historię. # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4509,6 +4510,7 @@ imageTrace Outline Kontur @msg nothing to save: the document has no visible art nie ma czego zapisać: dokument nie ma widocznej kompozycji @msg nothing to undo nie ma czego cofnąć @msg no history to clear brak historii do wyczyszczenia +@msg no history state {v} brak stanu historii {v} @msg nothing to warp nie ma czego wypaczyć @msg nothing to wrap nie ma czego oblewać @msg object {0} cannot have children obiekt {0} nie może mieć elementów podrzędnych diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index d3e4a7c6d..25e8a8fcd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -1270,6 +1270,7 @@ Step Backward Voltar um Passo Step Forward Avançar um Passo Clear History Limpar Histórico + New Document from State Novo Documento do Estado Set History Limit… Definir Limite do Histórico… History States Estados do Histórico # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -3957,6 +3958,7 @@ axis Both Ambos @msg nothing to save: the document has no visible art nada para salvar: o documento não tem arte visível @msg nothing to undo nada para desfazer @msg no history to clear nenhum histórico para limpar +@msg no history state {v} nenhum estado do histórico {v} @msg nothing to warp nada para deformar @msg nothing to wrap nada para envolver @msg object {0} cannot have children o objeto {0} não pode ter filhos diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index b3fa57493..05030934a 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -1460,6 +1460,7 @@ file-filter Templates Шаблоны Step Backward Шаг назад Step Forward Шаг вперёд Clear History Очистить историю + New Document from State Новый документ из состояния Set History Limit… Задать предел истории… History States Состояния истории # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4415,6 +4416,7 @@ axis Both Оба @msg nothing to save: the document has no visible art нечего сохранять: в документе нет видимого рисунка @msg nothing to undo нечего отменять @msg no history to clear нет истории для очистки +@msg no history state {v} нет состояния истории {v} @msg nothing to warp нечего деформировать @msg nothing to wrap нечего обтекать @msg object {0} cannot have children объект {0} не может иметь потомков diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 5e48f7f81..fec29cca5 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -1409,6 +1409,7 @@ font-technology Variable Variabla Step Backward Stega bakåt Step Forward Stega framåt Clear History Rensa historik + New Document from State Nytt dokument från tillstånd Set History Limit… Ange historikgräns… History States Historiktillstånd # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4283,6 +4284,7 @@ axis Both Båda @msg nothing to save: the document has no visible art inget att spara: dokumentet har ingen synlig grafik @msg nothing to undo inget att ångra @msg no history to clear ingen historik att rensa +@msg no history state {v} inget historiktillstånd {v} @msg nothing to warp inget att böja @msg nothing to wrap inget att radbryta @msg object {0} cannot have children objekt {0} kan inte ha underordnade objekt diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index c4c515e56..e622e6cd7 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -2794,6 +2794,7 @@ digits Malayalam Malayalam Auto Leading: Otomatik Satır Aralığı: Cancel Export Dışa Aktarmayı İptal Et Clear History Geçmişi Temizle + New Document from State Durumdan Yeni Belge Continue in Background Arka Planda Devam Et Delete Light Işığı Sil Desired İstenen @@ -5460,6 +5461,7 @@ theme Appearance Görünüm @msg its editing data is for a {w} × {h} pt page, not the file's {pw} × {ph} pt page düzenleme verileri {w} × {h} pt boyutunda bir sayfa içindir, dosyanın {pw} × {ph} pt sayfası için değil @msg its editing data is for a {w} × {h} pt page, the file's {pw} × {ph} pt page at {scale} düzenleme verileri {w} × {h} pt boyutunda bir sayfa içindir, dosyanın {scale} ölçeğindeki {pw} × {ph} pt sayfası @msg no history to clear temizlenecek geçmiş yok +@msg no history state {v} {v} geçmiş durumu yok @msg only http, https and mailto URLs open in the browser, not “{url}” tarayıcıda yalnızca http, https ve mailto URL'leri açılır, “{url}” açılmaz @msg {_1} files are written by file.saveAsTemplate {_1} dosyaları file.saveAsTemplate tarafından yazılır @msg {id} opens another app: only the person at the keyboard can run it {id} başka bir uygulamayı açar: yalnızca klavye başındaki kişi çalıştırabilir diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index c6f793067..54249c059 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -1457,6 +1457,7 @@ file-filter Templates Шаблони Step Backward Крок назад Step Forward Крок вперед Clear History Очистити історію + New Document from State Новий документ зі стану Set History Limit… Задати межу історії… History States Стан історії # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4404,6 +4405,7 @@ axis Both Обидві @msg nothing to save: the document has no visible art нічого зберігати: у документі немає видимого малюнка @msg nothing to undo нічого скасовувати @msg no history to clear немає історії для очищення +@msg no history state {v} немає стану історії {v} @msg nothing to warp нічого деформувати @msg nothing to wrap нічого обтікати @msg object {0} cannot have children об’єкт {0} не може мати нащадків diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 29f222849..d98cc3519 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1676,6 +1676,7 @@ digits Hindi Hindi Step Backward Lùi một bước Step Forward Tiến một bước Clear History Xóa lịch sử + New Document from State Tài liệu mới từ trạng thái Set History Limit… Đặt giới hạn lịch sử… History States Trạng thái lịch sử # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -4712,6 +4713,7 @@ theme Appearance Giao diện @msg nothing to save: the document has no visible art không có gì để lưu: tài liệu không có tác phẩm hiển thị @msg nothing to undo không có gì để hoàn tác @msg no history to clear không có lịch sử để xóa +@msg no history state {v} không có trạng thái lịch sử {v} @msg nothing to warp không có gì để biến dạng @msg nothing to wrap không có gì để bọc @msg object ids are non-negative integers, not {v} id đối tượng là số nguyên không âm, không phải {v} diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 4f3d2dcd9..b7a65b421 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -1396,6 +1396,7 @@ file-filter Templates 模板 Step Backward 后退一步 Step Forward 前进一步 Clear History 清除历史记录 + New Document from State 从状态新建文档 Set History Limit… 设置历史记录上限… History States 历史记录步数 # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -3902,6 +3903,7 @@ imageTrace Outline 轮廓 @msg nothing to redo 没有可重做的操作 @msg nothing to undo 没有可撤销的操作 @msg no history to clear 没有可清除的历史记录 +@msg no history state {v} 没有历史记录状态 {v} @msg Expand Appearance: the selection has a basic appearance 扩展外观:所选对象只有基本外观 @msg select (or target) objects whose appearance isn't basic 请选择 (或定位) 外观不是基本外观的对象 @msg {C}: no effect at index {from} {C}:索引 {from} 处没有效果 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index ba3a80ef0..cbbd740bc 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -1428,6 +1428,7 @@ file-filter Templates 範本 Step Backward 後退一步 Step Forward 前進一步 Clear History 清除步驟記錄 + New Document from State 從步驟新增文件 Set History Limit… 設定步驟記錄上限… History States 步驟記錄狀態 # History panel: the names of undo steps (`untranslated_report` lists any that are missing). @@ -1779,6 +1780,7 @@ file-filter Templates 範本 @msg nothing to redo 沒有可重做的動作 @msg nothing to undo 沒有可復原的動作 @msg no history to clear 沒有可清除的步驟記錄 +@msg no history state {v} 沒有步驟記錄 {v} @msg Expand Appearance: the selection has a basic appearance 展開外觀:選取範圍只有基本外觀 @msg select (or target) objects whose appearance isn't basic 請選取 (或指定目標) 外觀不是基本外觀的物件 @msg {C}: no effect at index {from} {C}:索引 {from} 處沒有效果 diff --git a/crates/ui-egui/src/menus/state.rs b/crates/ui-egui/src/menus/state.rs index 963045722..214919939 100644 --- a/crates/ui-egui/src/menus/state.rs +++ b/crates/ui-egui/src/menus/state.rs @@ -157,13 +157,13 @@ pub fn shortcut_of(id: &str) -> Option<&'static str> { /// Commands that would act on the text being typed, held back while an IME composes in the Type /// tool (its marked text isn't committed yet): Undo/Redo — the native menu takes ⌘Z ahead of the /// IME —, the clipboard and the selection. -/// Clear History also waits: it ends the typing session as Undo does. +/// Clear History and New Document from State also wait: they end the typing session as Undo does. pub(crate) fn waits_for_ime(app: &VectorcraftApp, id: &str) -> bool { app.session.tool_composing() && (matches!( id, "edit.undo" | "edit.redo" | "edit.cut" | "edit.copy" | "edit.clear" | "edit.duplicate" | "select.all" | "select.none" | "select.inverse" - ) || id == "edit.clearHistory" + ) || matches!(id, "edit.clearHistory" | "edit.newDocumentFromState") || id.starts_with("edit.paste")) } diff --git a/crates/ui-egui/src/panels/history.rs b/crates/ui-egui/src/panels/history.rs index 7c7acbae7..cc0c60986 100644 --- a/crates/ui-egui/src/panels/history.rs +++ b/crates/ui-egui/src/panels/history.rs @@ -4,7 +4,7 @@ use egui::{Sense, Ui, vec2}; use serde_json::json; use crate::theme::Tokens; -use crate::widgets::menu_item; +use crate::widgets::{self, menu_item}; use crate::{VectorcraftApp, icons}; /// Undo (negative) / redo (positive) steps to reach row `target` when `current` is the current @@ -87,6 +87,15 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } } } + widgets::divider(ui); + // A click on a row moves to its state at once, so the selected state is the current one. + const NEW_DOCUMENT: &str = "edit.newDocumentFromState"; + let new_document = crate::menus::enabled(app, NEW_DOCUMENT); + ui.horizontal(|ui| { + if widgets::icon_button_enabled(ui, "file-plus", tl!("New Document from State"), false, new_document, 24.0).clicked() { + crate::menus::invoke(app, NEW_DOCUMENT, json!({})); + } + }); } pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) { @@ -303,6 +312,37 @@ mod tests { assert_eq!(app.session.active().unwrap().revision, revision, "a click on the disabled item changes nothing"); } + /// The bottom of the panel's New Document from State button makes a new document, the active + /// one, from the state a click on a row moved to. The source document stays open at that state. + #[test] + fn the_new_document_button_makes_a_document_from_the_selected_state() { + let mut app = app_with_two_rectangles(); + app.session.execute("edit.undo", &json!({})).unwrap(); + let source = app.session.active().unwrap().doc.clone(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + egui_extras::install_image_loaders(&ctx); + let screen = Some(egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(240.0, 400.0))); + let frame = |app: &mut VectorcraftApp, events: Vec| { + let mut out = ctx.run_ui(egui::RawInput { events, screen_rect: screen, ..Default::default() }, |ui| show(app, ui)); + out.textures_delta.clear(); + out.platform_output.accesskit_update.map(|u| u.nodes).unwrap_or_default() + }; + frame(&mut app, vec![]); + let nodes = frame(&mut app, vec![]); + let button = nodes.iter().map(|(_, n)| n).find(|n| n.label() == Some("New Document from State")).expect("the button"); + let at = button.bounds().expect("its bounds"); + let at = egui::pos2((at.x0 + at.x1) as f32 / 2.0, (at.y0 + at.y1) as f32 / 2.0); + let press = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(&mut app, vec![egui::Event::PointerMoved(at), press(true), press(false)]); + assert_eq!(app.session.documents().len(), 2); + assert_eq!(app.session.active_index(), Some(1), "the new document is the active one"); + let st = app.session.active().unwrap(); + assert_eq!(st.doc.layers[0].children().unwrap().len(), 1, "the art of the selected state"); + assert!(st.is_dirty(), "the new document is unsaved"); + assert!(std::sync::Arc::ptr_eq(&app.session.documents()[0].doc, &source), "the source stays at that state"); + } + /// Set History Limit… opens Preferences at Performance, the page with History States. #[test] fn set_history_limit_opens_the_history_states_preference() { diff --git a/docs/mcp.md b/docs/mcp.md index e0e7c9746..82bdb808c 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -446,7 +446,7 @@ opens the picker through `run_command` with the remote backend. | `transform` | `{ids?, dx?, dy?, rotate?, scale?, scaleX?, scaleY?, reflect?, shear?, origin?, copy?}` | Runs move, rotate, scale, reflect, shear in that order. With `copy`, the first step duplicates. | | `create_graph` | `{type?, x, y, width, height, csv? \| series?, categories?, rows?}` | The nine Illustrator graph types: column, stacked column, bar, stacked bar, line, area, scatter, pie and radar. An empty CSV cell or a `null` in `rows` is a blank value; a number in straight quotes is a label. Edit later with `graph.setData` / `graph.setType` via `run_command`. `graph.column {design, columnType: vertical\|uniform\|repeating\|sliding, each, fractions: chop\|scale}` draws the columns or bars of the series picked with Group Selection (or `seriesIndexes`, or all of them) with a graph design saved by `graph.design`; with no `design` it returns the series' current design and options. Type in the design that holds a `%` followed by two digits shows each column's value, with at least the first digit's places before the decimal point and the second digit's places after it. | | `text_wrap` | `{ids?, offset?, invert?, release?}` | Area type below the objects (same layer) flows around them. | -| `undo` / `redo` | `{}` | Many calls that change nothing record no undo step, for example `object.align` with nothing to move, `object.move` by 0 without `copy` and `object.showAll` with nothing hidden. `undo` (`edit.undo`) after such a call undoes the step before it. `inspect_document` lists the undo steps as `history`. `run_command` with `edit.clearHistory` removes every undo and redo step of the active document and returns `{cleared}`, the number of steps removed; the document stays as it is, and the command records no undo step. While the Type tool edits type, `undo` and `redo` put its caret, with no characters selected, at the end of the text the step changed: after the text it puts back or retypes, or where the text it takes out was. A step that changes no characters, such as a formatting change, leaves the caret and the selection where they are. | +| `undo` / `redo` | `{}` | Many calls that change nothing record no undo step, for example `object.align` with nothing to move, `object.move` by 0 without `copy` and `object.showAll` with nothing hidden. `undo` (`edit.undo`) after such a call undoes the step before it. `inspect_document` lists the undo steps as `history`. `run_command` with `edit.clearHistory` removes every undo and redo step of the active document and returns `{cleared}`, the number of steps removed; the document stays as it is, and the command records no undo step. `run_command` with `edit.newDocumentFromState` and `{index?, created?}` makes a new document holding the art as it was at that state and makes it the active document, and returns `{index, title}`. `index` is a row of the History panel, with 0 the oldest state kept, and an omitted `index` uses the current state. An `index` that names no state is an error and changes nothing. `created` is File Info's created date in Unix seconds or null, and defaults to now. The new document takes the next `Untitled-N` name, is marked unsaved, has nothing selected and has no steps to undo or redo. It is out of symbol, opacity-mask and pattern editing, and the art drawn in a pattern's tile is stored in the pattern, as `object.pattern.done` stores it. The source document and its steps stay as they are, except that typing or a drag in progress is first kept as a step. While the Type tool edits type, `undo` and `redo` put its caret, with no characters selected, at the end of the text the step changed: after the text it puts back or retypes, or where the text it takes out was. A step that changes no characters, such as a formatting change, leaves the caret and the selection where they are. | For example, create editable Japanese area type with the engine's vertical writing mode: diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 4c65ec5f8..59ff808d4 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -693,8 +693,9 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The status bar's messages for documents opened or saved with notes and for discarded or restored recovered documents, and the Global Edit options' Results line, do the same (`open_save_and_recovery_messages_take_the_plural_form` in `crates/ui-egui/src/i18n/tests.rs`); the warnings for linked images and placed documents output as their previews and the errors for a page past a placed file's last page or artboard name their counts in the singular or the plural in English. - Edit › Undo and Edit › Redo, in the in-window and native menus, name the step in the interface language as the History panel lists it (`panels::history::step_name`), so when the catalog has no plain row for a step named after an effect, such as Feather, the menu shows the translation of the effect's menu row without its ellipsis (`undo_and_redo_name_the_step_as_the_history_panel_does`). - The History panel translates the undo step names the engine, the tools and the shell write as literals: the untranslated report (`untranslated_report`) lists any a complete catalog lacks, and a step whose catalog row exists only with an ellipsis (such as an effect's menu name) shows that row without the ellipsis. - - The History panel menu has Step Backward and Step Forward, which run Edit › Undo and Edit › Redo and are disabled when there is no step to undo or redo, and Set History Limit…, which opens Preferences at Performance, where History States is set. Not done: a new document from the selected state. + - The History panel menu has Step Backward and Step Forward, which run Edit › Undo and Edit › Redo and are disabled when there is no step to undo or redo, and Set History Limit…, which opens Preferences at Performance, where History States is set. - The History panel menu's Clear History (`edit.clearHistory`) removes every step to undo and to redo and leaves the document as it is; it is not an undo step itself, and it is disabled when there is no step to clear. + - The New Document from State button at the bottom of the History panel (`edit.newDocumentFromState`) makes a new document, which becomes active, from the state the panel has selected ([public source](https://helpx.adobe.com/illustrator/desktop/get-started/learn-the-basics/undo-edits-and-manage-design-history.html)). VectorCraft's own rules: the new document takes the next Untitled-N name, is marked unsaved, has nothing selected and has no steps to undo or redo, and the source document and its steps stay as they are. - The Transform panel menu's Transform Object Only, Transform Pattern Only and Transform Both (`transformPanelPatterns`) choose whether the fields, flips and menu flips of the Transform panel and the Properties panel's Transform section move the object and leave its pattern tiles, move the tiles alone, or both (until a choice they follow General › Transform Pattern Tiles, as before); Use Registration Point for Symbol (`useRegistrationPointForSymbol`) measures and transforms one selected symbol instance from its symbol's registration point, where the instance's flips, rotations and shears put it ([Transform panel overview](https://helpx.adobe.com/illustrator/desktop/manage-objects/reshape-transform-objects/transform-panel-overview.html); `panels::transform::tests::the_menu_chooses_whether_the_object_its_pattern_or_both_transform`, `use_registration_point_for_symbol_measures_an_instance_from_its_registration_point`, `use_registration_point_for_symbol_follows_a_transformed_instance`, `tests_prefs::transform_panel_patterns_defaults_to_following_the_general_preference`). - Native macOS menu bar, vector tool cursors, a ⌘K command palette, middle-button panning with any tool. Native menus explicitly end tracking on command selection, outside window presses and Escape; menu updates wait until tracking ends, and a dismissing press never starts a canvas gesture (M14.8). - The errors of the blend spine commands (Move Spine Point, Reshape Spine Segment, Add Spine Point and Delete Spine Point) show in the status bar in the interface language: each has a row in every catalog that translates all status and error messages and in Bulgarian (`blend_spine_param_errors_are_translated`).