mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-27 19:27:21 -06:00
With View > Guides > Lock Guides on, Session::select drops the paths made into guides from the selection (#1534), but commands that set the selection another way skipped that filter and left a locked guide selected, so the next move moved it. object.exitIsolation selected an isolated guide path. Undo, Redo, a canceled undo group, a canceled drag or dialog preview and a failed command.batch brought back locked guide paths and ruler guides in the selection. Unlock All, Show All and Paste selected the guide paths they unlocked, showed or pasted. Locking the guides deselected them in the active document only. The filter is now deselect_guides, which also clears the selected ruler guides. Session::select and Session::edit run it while Lock Guides is on, and so do object.exitIsolation, Undo, Redo, Session::cancel_interaction, a canceled undo group and a failed batch. Locking the guides deselects them in every open document. With Lock Guides off, the selection is set as before. Docs: docs/mcp.md.
115 lines
5.5 KiB
Rust
115 lines
5.5 KiB
Rust
//! `command.batch`: several commands as one undo step (in each document they edit).
|
|
|
|
use serde_json::{Value, json};
|
|
|
|
use super::super::*;
|
|
use crate::{DocState, EngineError};
|
|
|
|
pub(super) fn batch(s: &mut Session, p: &Value) -> Result<Value> {
|
|
let mut cmds = p.get("commands").and_then(Value::as_array).ok_or_else(|| bad("command.batch", "missing commands"))?.clone();
|
|
let label = str_param(p, "label").unwrap_or("Batch").to_string();
|
|
// The active artboard the app passes, for the steps that act on it.
|
|
let artboard = p.get("artboard").and_then(Value::as_u64);
|
|
// What an error rolls back besides the documents: session-level state a step may change
|
|
// (paint defaults, drawing mode, clipboard, the Untitled-N count).
|
|
let saved = (s.paint.clone(), s.fill_active, s.draw_mode, s.draw_inside, s.clipboard.clone(), s.untitled_counter);
|
|
// A drag or typing still in progress is a step of its own, as before Undo: the batch's steps
|
|
// would otherwise run inside it and take its baseline as theirs (#1697).
|
|
if s.active().is_some() {
|
|
super::super::edit::settle_interaction(s)?;
|
|
}
|
|
// The documents as they are now, whole: what a failed step brings back. Their art, history,
|
|
// file, format and saved state share their data with the open ones until a step changes them.
|
|
let start = s.docs.clone();
|
|
let active = s.active;
|
|
s.batch_stash = Some(vec![]);
|
|
let r = run_steps(s, &label, artboard, &mut cmds);
|
|
let stash = s.batch_stash.take().unwrap_or_default();
|
|
let results = match r {
|
|
Ok(results) => results,
|
|
Err(e) => {
|
|
roll_back(s, start, &stash);
|
|
s.active = active;
|
|
(s.paint, s.fill_active, s.draw_mode, s.draw_inside, s.clipboard, s.untitled_counter) = saved;
|
|
s.pending_paint = None;
|
|
return Err(e);
|
|
}
|
|
};
|
|
// Each document the steps edited keeps them as one undo step.
|
|
s.remember_pending_paint();
|
|
for st in &mut s.docs {
|
|
if let Some(it) = st.interaction.take() {
|
|
st.keep_interaction(it);
|
|
}
|
|
}
|
|
// The batch journals itself, whichever document it ends in.
|
|
let mut entry = p.clone();
|
|
if let Some(o) = entry.as_object_mut() {
|
|
o.insert("commands".into(), Value::Array(cmds));
|
|
}
|
|
s.journal.push(("command.batch".into(), entry));
|
|
Ok(json!({ "results": results }))
|
|
}
|
|
|
|
/// Run the steps → their results. Each runs inside an interaction of the document active when it
|
|
/// starts (one for all the steps in that document), so a step may open, switch or close documents.
|
|
/// A step that acts on the active artboard ([`crate::ON_ACTIVE_ARTBOARD`]) and names none gets
|
|
/// `artboard`, as the Artboards panel shows it in the document the step runs in: past that
|
|
/// document's last artboard, the last one. A step that resizes the active artboard
|
|
/// ([`crate::FIT_ACTIVE_ARTBOARD`]) and names no `index` gets that artboard as its `index`.
|
|
fn run_steps(s: &mut Session, label: &str, artboard: Option<u64>, cmds: &mut [Value]) -> Result<Vec<Value>> {
|
|
let mut results = Vec::with_capacity(cmds.len());
|
|
for c in cmds {
|
|
let id = c.get("command").and_then(Value::as_str).unwrap_or("").to_string();
|
|
if id == "command.batch" {
|
|
return Err(bad("command.batch", "batches cannot nest"));
|
|
}
|
|
if s.active().is_some() {
|
|
s.begin_interaction(label)?;
|
|
}
|
|
let mut params = c.get("params").cloned().unwrap_or(json!({}));
|
|
let key = if crate::ON_ACTIVE_ARTBOARD.contains(&id.as_str()) {
|
|
Some("artboard")
|
|
} else {
|
|
crate::FIT_ACTIVE_ARTBOARD.contains(&id.as_str()).then_some("index")
|
|
};
|
|
if let Some(a) = artboard
|
|
&& let Some(key) = key
|
|
&& params.get(key).is_none()
|
|
&& let Some(last) = s.active().and_then(|d| d.doc.artboards.len().checked_sub(1))
|
|
&& let Some(o) = params.as_object_mut()
|
|
{
|
|
o.insert(key.into(), json!(usize::try_from(a).map_or(last, |a| a.min(last))));
|
|
}
|
|
let (v, noted) =
|
|
s.execute_step(&id, ¶ms).map_err(|e| EngineError::Other(format!("batch step {} (`{id}`) failed: {e}", results.len())))?;
|
|
// The step's journal entry records what it took from the preferences or the clock (a
|
|
// save's date, a new document's), as a command of its own would.
|
|
if noted != params
|
|
&& let Some(c) = c.as_object_mut()
|
|
{
|
|
c.insert("params".into(), noted);
|
|
}
|
|
results.push(v);
|
|
}
|
|
Ok(results)
|
|
}
|
|
|
|
/// The documents as they were before the batch (`start`), whatever the steps did to them: opened
|
|
/// ones close, closed or replaced ones (`stash`) come back, and each has its art, selection,
|
|
/// history, file, format and saved state again (#1697: undoing the batch's interaction alone left
|
|
/// a Save As step's new file and an Undo step's history in place). Files the steps wrote stay
|
|
/// written. Lock Guides stays as the steps left it, and while it is on the guides are deselected.
|
|
fn roll_back(s: &mut Session, start: Vec<DocState>, stash: &[DocState]) {
|
|
let open = std::mem::take(&mut s.docs);
|
|
s.docs = start;
|
|
for st in &mut s.docs {
|
|
// Past the newest revision the document reached, so the restored one redraws.
|
|
let newest = open.iter().chain(stash).filter(|d| d.uid == st.uid).map(|d| d.revision).max().unwrap_or(st.revision);
|
|
st.revision = newest.saturating_add(1);
|
|
if s.menu.guides_locked {
|
|
crate::deselect_guides(&st.doc, &mut st.selection);
|
|
}
|
|
}
|
|
}
|