mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-30 04:37:29 -06:00
M4.16: Template safety: Templates folder, "<name> template", New from Template
Save as Template suggests "<name> template.vctemplate" in the Templates folder
(the new templatesFolder preference; default Documents/VectorCraft Templates,
created by the desktop save panel) and never the source path. New from
Template is an engine command (file.newFromTemplate {path} or {name,
dataBase64}) that opens any readable file as a new Untitled-N document; in the
app, bytes from an agent go straight to the engine, otherwise the open dialog
starts in the Templates folder with template types first. .ait and template
files keep opening untitled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
9fae8c56ec
commit
70e2726321
@@ -151,11 +151,22 @@ pub fn open_bytes(s: &mut Session, name: &str, bytes: &[u8], path: Option<String
|
||||
/// [`open_bytes`] with the `document.open` options in `p` ([`LoadOptions::from_params`]).
|
||||
pub fn open_bytes_with(s: &mut Session, name: &str, bytes: &[u8], path: Option<String>, p: &Value) -> Result<Value> {
|
||||
let opts = LoadOptions::from_params("document.open", p)?;
|
||||
let Loaded { mut doc, format, warnings, restored } = load_with(name, bytes, &opts)?;
|
||||
open_loaded(s, load_with(name, bytes, &opts)?, path, &opts, false)
|
||||
}
|
||||
|
||||
/// File → New from Template: any readable file (at `path`, if it is one: its links are looked for
|
||||
/// from there) as a new untitled document.
|
||||
pub fn open_template(s: &mut Session, name: &str, bytes: &[u8], path: Option<&str>) -> Result<Value> {
|
||||
open_loaded(s, load(name, bytes)?, path.map(str::to_string), &LoadOptions::default(), true)
|
||||
}
|
||||
|
||||
/// Make a loaded file the new active document; `opts` are the options it was read with.
|
||||
fn open_loaded(s: &mut Session, loaded: Loaded, path: Option<String>, opts: &LoadOptions, as_template: bool) -> Result<Value> {
|
||||
let Loaded { mut doc, format, warnings, restored } = loaded;
|
||||
let links = crate::cmd::links::resolve(&mut doc, path.as_deref(), s.prefs.update_links == "automatically");
|
||||
// A template (saved by Save as Template, or an .ait/.vctemplate file) opens as a new untitled
|
||||
// document.
|
||||
let template = doc.template || matches!(format.id, "ait" | "template");
|
||||
let template = as_template || doc.template || matches!(format.id, "ait" | "template");
|
||||
if template {
|
||||
doc.template = false;
|
||||
doc.title = s.next_untitled();
|
||||
@@ -200,6 +211,11 @@ pub(super) fn open(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
open_bytes_with(s, src.name, &src.bytes, src.path.map(str::to_string), p)
|
||||
}
|
||||
|
||||
pub(super) fn new_from_template(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let src = source(p, "file.newFromTemplate")?;
|
||||
open_template(s, src.name, &src.bytes, src.path)
|
||||
}
|
||||
|
||||
/// Decode an image's header (and, for formats stored as PNG, its pixels).
|
||||
pub fn raster_image(bytes: &[u8]) -> Result<RasterImage> {
|
||||
let reader = image::ImageReader::new(Cursor::new(bytes)).with_guessed_format().map_err(err)?;
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
//! - `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, Revert and the per-format options
|
||||
//! (`file.formatOptions`); [`save_with`] is the one save path of every frontend.
|
||||
//! - `save`: `document.save`, Save As / a Copy / as Template, New from 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`.
|
||||
//!
|
||||
@@ -30,9 +30,9 @@ pub use encode::{ARTBOARD_PARAMS, ArtboardPick, Encoded, encode, encode_all, enc
|
||||
pub(crate) use encode::{anti_alias, background, with_single_artboard};
|
||||
use load::err;
|
||||
pub(crate) use load::source;
|
||||
pub use load::{Loaded, RasterImage, detect, file_name, load, load_with, open_bytes, open_bytes_with, raster_image};
|
||||
pub use load::{Loaded, RasterImage, detect, file_name, load, load_with, open_bytes, open_bytes_with, open_template, raster_image};
|
||||
pub use pdfimport::{LoadOptions, page_document};
|
||||
pub use save::{SAVE_FORMATS, SaveMode, SavePlan, save_filters, save_format, save_plan, save_with, stamp_save_dates};
|
||||
pub use save::{SAVE_FORMATS, SaveMode, SavePlan, save_filters, save_format, save_plan, save_with, stamp_save_dates, templates_folder};
|
||||
pub use svg::options_map as svg_options;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
//! Saving: `document.save`, `file.saveAs`, `file.saveCopy`, `file.saveAsTemplate`, `file.revert`
|
||||
//! and `file.formatOptions`.
|
||||
//! Saving: `document.save`, `file.saveAs`, `file.saveCopy`, `file.saveAsTemplate`,
|
||||
//! `file.newFromTemplate`, `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
|
||||
@@ -11,7 +11,7 @@ use vectorcraft_doc::Document;
|
||||
|
||||
use super::super::*;
|
||||
use super::{Encoded, Format, Loaded, encode_all, file_stem, format, format_for_name, load, read_file, write_encoded, write_file};
|
||||
use crate::DocState;
|
||||
use crate::{DocState, Prefs};
|
||||
|
||||
/// The formats Save As offers, in menu order (append-only). The other writable formats are exports:
|
||||
/// they never become the document's own file.
|
||||
@@ -68,9 +68,9 @@ pub struct SavePlan {
|
||||
pub format: &'static Format,
|
||||
/// The format's options (only those its encoder reads).
|
||||
pub options: Map<String, Value>,
|
||||
/// Suggested file name: `<name>.<ext>` or `<name> copy.<ext>`.
|
||||
/// Suggested file name: `<name>.<ext>`, `<name> copy.<ext>` or `<name> template.vctemplate`.
|
||||
pub name: String,
|
||||
/// Suggested folder: the document's own.
|
||||
/// Suggested folder: the document's own, or the Templates folder for a template.
|
||||
pub folder: Option<String>,
|
||||
}
|
||||
|
||||
@@ -91,6 +91,16 @@ pub fn stamp_save_dates(st: &mut DocState) {
|
||||
d.metadata.modified = Some(now);
|
||||
}
|
||||
|
||||
/// The Templates folder: the `templatesFolder` preference, else `Documents/VectorCraft Templates`
|
||||
/// in the user's home (none where there is no home folder, as on the web).
|
||||
pub fn templates_folder(prefs: &Prefs) -> Option<String> {
|
||||
if !prefs.templates_folder.is_empty() {
|
||||
return Some(prefs.templates_folder.clone());
|
||||
}
|
||||
let home = std::env::var("HOME").or_else(|_| std::env::var("USERPROFILE")).ok().filter(|h| !h.is_empty())?;
|
||||
Some(std::path::Path::new(&home).join("Documents").join("VectorCraft Templates").to_string_lossy().to_string())
|
||||
}
|
||||
|
||||
/// The folder of a path (`None` for a bare file name).
|
||||
fn parent_folder(path: &str) -> Option<String> {
|
||||
std::path::Path::new(path).parent().map(|p| p.to_string_lossy().to_string()).filter(|p| !p.is_empty())
|
||||
@@ -154,9 +164,13 @@ pub fn save_plan(s: &Session, mode: SaveMode, p: &Value) -> Result<SavePlan> {
|
||||
let ext = format.extensions[0];
|
||||
let name = match mode {
|
||||
SaveMode::Copy => format!("{stem} copy.{ext}"),
|
||||
SaveMode::Save | SaveMode::SaveAs | SaveMode::Template => format!("{stem}.{ext}"),
|
||||
SaveMode::Template => format!("{stem} template.{ext}"),
|
||||
SaveMode::Save | SaveMode::SaveAs => format!("{stem}.{ext}"),
|
||||
};
|
||||
let folder = match mode {
|
||||
SaveMode::Template => templates_folder(&s.prefs),
|
||||
_ => st.path.as_deref().and_then(parent_folder),
|
||||
};
|
||||
let folder = st.path.as_deref().and_then(parent_folder);
|
||||
Ok(SavePlan { mode, path, format, options, name, folder })
|
||||
}
|
||||
|
||||
@@ -338,10 +352,19 @@ pub(super) fn specs() -> Vec<CommandSpec> {
|
||||
"Save as Template…",
|
||||
["File"],
|
||||
None,
|
||||
"{path?} a native template (.vctemplate) that opens as a new untitled document; the document is unchanged → {path, format, bytes, warnings}; no path → {dataBase64, format, name: \"<name>.vctemplate\", folder?, warnings}",
|
||||
"{path?} a native template (.vctemplate) that opens as a new untitled document; the document is unchanged → {path, format, bytes, warnings}; no path → {dataBase64, format, name: \"<name> template.vctemplate\", folder: the Templates folder (preference templatesFolder), warnings}",
|
||||
has_doc,
|
||||
|s, p| save(s, SaveMode::Template, p)
|
||||
),
|
||||
cmd!(
|
||||
"file.newFromTemplate",
|
||||
"New from Template…",
|
||||
["File"],
|
||||
Some("Cmd+Shift+N"),
|
||||
"{path} or {name, dataBase64}: open a template (or any readable file) as a new untitled document → {index, title, format, warnings}",
|
||||
always,
|
||||
super::load::new_from_template
|
||||
),
|
||||
cmd!(
|
||||
"file.revert",
|
||||
"Revert",
|
||||
|
||||
@@ -315,6 +315,8 @@ pub const PREF_SPECS: &[PrefSpec] = &[
|
||||
p!("japaneseCropMarks", "General", "Options", "Use Japanese Crop Marks", bool),
|
||||
// Appearance panel menu
|
||||
p!("newArtBasic", "General", "Appearance Panel", "New Art Has Basic Appearance", bool),
|
||||
// File Handling (continued)
|
||||
p!("templatesFolder", "File Handling", "Files", "Templates Folder", text),
|
||||
];
|
||||
|
||||
pub fn spec(key: &str) -> Option<&'static PrefSpec> {
|
||||
|
||||
Reference in New Issue
Block a user