mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-14 19:26:56 -04:00
M4.79: print presets in the Print dialog and Edit > Print Presets
The Print dialog's Print Preset list loads [Default] or a saved preset (reading [Custom] once the settings differ) and Save Preset... names the settings as a preset. Edit > Print Presets... lists [Default] (generated, protected) and the saved presets with how they differ from it; New... and Edit... open the preset editor (the Print dialog's sections), Delete, Import... and Export... use .vcprintpresets files, which File > Open also imports. Commands print.presets.list/save/delete/import/export; the presets live in Prefs.print_presets and persist with the preferences. The open-dialog filter count test counts the new Print presets filter. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
ffce1cf686
commit
7c748a5e3c
16 files changed
+961
-33
No files matched your search
@@ -613,13 +613,14 @@ fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static st
|
||||
}
|
||||
|
||||
/// Open-dialog filters: "All readable files" first, then one per readable format, then swatch
|
||||
/// libraries (which open in the library panel), flattener presets and PDF presets (imported).
|
||||
/// libraries (which open in the library panel), flattener, PDF and print presets (imported).
|
||||
pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
|
||||
std::iter::once(("All readable files", OPEN_EXTS))
|
||||
.chain(format_filters())
|
||||
.chain(std::iter::once(("Swatch libraries", super::swatchlib::LIBRARY_EXTS)))
|
||||
.chain(std::iter::once(("Flattener presets", super::flatten::PRESET_EXTS)))
|
||||
.chain(std::iter::once(("PDF presets", super::pdfcmds::PRESET_EXTS)))
|
||||
.chain(std::iter::once(("Print presets", super::printpresets::PRESET_EXTS)))
|
||||
}
|
||||
|
||||
/// File → Place dialog filters: "All placeable files", then one per readable format, then text.
|
||||
|
||||
@@ -146,7 +146,7 @@ fn open_exts_cover_every_readable_format() {
|
||||
}
|
||||
let filters: Vec<_> = open_filters().collect();
|
||||
assert_eq!(filters[0], ("All readable files", OPEN_EXTS));
|
||||
assert_eq!(filters.len(), 4 + FORMATS.iter().filter(|f| f.read).count(), "and swatch libraries, flattener presets and PDF presets");
|
||||
assert_eq!(filters.len(), 5 + FORMATS.iter().filter(|f| f.read).count(), "and swatch libraries, flattener, PDF and print presets");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -47,6 +47,7 @@ pub mod pdfcmds;
|
||||
mod place;
|
||||
pub mod prefscmds;
|
||||
pub mod print;
|
||||
pub mod printpresets;
|
||||
pub mod rasterfx;
|
||||
mod recolor;
|
||||
pub mod recovery;
|
||||
@@ -239,6 +240,7 @@ pub fn command_specs() -> &'static [CommandSpec] {
|
||||
v.extend(webexport::specs());
|
||||
v.extend(assets::specs());
|
||||
v.extend(css::specs());
|
||||
v.extend(printpresets::specs());
|
||||
v
|
||||
})
|
||||
}
|
||||
|
||||
@@ -51,7 +51,11 @@ fn saved(doc: &Document) -> Value {
|
||||
|
||||
/// The settings of `doc` with `p.settings` over them, checked.
|
||||
fn settings(cmd: &str, doc: &Document, p: &Value) -> Result<PrintSettings> {
|
||||
let mut v = saved(doc);
|
||||
settings_over(cmd, saved(doc), p)
|
||||
}
|
||||
|
||||
/// The settings `v` (JSON) with `p.settings` over them, checked.
|
||||
pub(crate) fn settings_over(cmd: &str, mut v: Value, p: &Value) -> Result<PrintSettings> {
|
||||
match p.get("settings") {
|
||||
None | Some(Value::Null) => {}
|
||||
Some(over @ Value::Object(_)) => merge(&mut v, over),
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
//! Edit → Print Presets: `print.presets.list`, `save`, `delete`, `export` and `import`. The
|
||||
//! built-in [Default] (the default print settings) is generated in code and protected; the user's
|
||||
//! presets live in [`crate::Prefs::print_presets`]. The Print dialog's preset list loads them.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_pdf::PrintSettings;
|
||||
|
||||
use super::fileio::pdf::changed;
|
||||
use super::print::settings_over;
|
||||
use super::*;
|
||||
|
||||
/// The built-in preset's name.
|
||||
pub const DEFAULT_PRESET: &str = "[Default]";
|
||||
|
||||
/// The `format` of a print presets file, also its extension.
|
||||
pub const PRESET_FORMAT: &str = "vcprintpresets";
|
||||
|
||||
/// The extensions of print presets files (opening one imports it).
|
||||
pub const PRESET_EXTS: &[&str] = &[PRESET_FORMAT];
|
||||
|
||||
/// A named set of print settings: [Default] or a saved one ([`crate::Prefs::print_presets`]).
|
||||
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct PrintPreset {
|
||||
pub name: String,
|
||||
#[serde(default)]
|
||||
pub settings: PrintSettings,
|
||||
}
|
||||
|
||||
/// What `print.presets.export` writes and `print.presets.import` reads.
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct PresetFile {
|
||||
format: String,
|
||||
#[serde(default)]
|
||||
presets: Vec<PrintPreset>,
|
||||
}
|
||||
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!(
|
||||
query "print.presets.list",
|
||||
"Print Presets",
|
||||
[],
|
||||
None,
|
||||
"{} → {presets: [{name, builtIn, settings (as print.setup's), changed: [{option, value}] (how it differs from [Default])}]} [Default] (the default print settings, protected) first, then the saved presets",
|
||||
always,
|
||||
list
|
||||
),
|
||||
cmd!(
|
||||
query "print.presets.save",
|
||||
"Save Print Preset",
|
||||
[],
|
||||
None,
|
||||
"{name?: (default: a new \"Print Preset N\"), newName?: rename it, preset?: the preset to start from (default: the saved preset `name`, else [Default]), settings?: {…print.setup settings} (over it)} create or change a saved preset ([Default] is protected) → {name, created, settings}",
|
||||
always,
|
||||
save
|
||||
),
|
||||
cmd!(query "print.presets.delete", "Delete Print Preset", [], None, "{name} delete a saved preset ([Default] stays) → {deleted: name}", always, delete),
|
||||
cmd!(
|
||||
query "print.presets.export",
|
||||
"Export Print Presets",
|
||||
[],
|
||||
None,
|
||||
"{names?: [preset names, [Default] too] (default: every saved preset), path?} write the presets as a .vcprintpresets file (JSON) → {path, count}; without path → {data: the file's text, count}",
|
||||
always,
|
||||
export
|
||||
),
|
||||
cmd!(
|
||||
query "print.presets.import",
|
||||
"Import Print Presets",
|
||||
[],
|
||||
None,
|
||||
"{path? | data?: file text | dataBase64?, replace?: false (replace saved presets of the same names; else imported ones whose name is taken get a number)} add the presets of a .vcprintpresets file (as print.presets.export writes) to the saved ones → {imported: [names]}",
|
||||
always,
|
||||
import
|
||||
),
|
||||
]
|
||||
}
|
||||
|
||||
/// Is `name` the built-in [Default] (also as "Default")?
|
||||
pub fn is_default(name: &str) -> bool {
|
||||
let n = name.trim();
|
||||
n.eq_ignore_ascii_case(DEFAULT_PRESET) || n.eq_ignore_ascii_case("Default")
|
||||
}
|
||||
|
||||
/// `[{option, value}]` for every setting of `set` that differs from `base` (`marks.trim`).
|
||||
pub fn changes(set: &PrintSettings, base: &PrintSettings) -> Vec<Value> {
|
||||
let (Ok(v), Ok(base)) = (serde_json::to_value(set), serde_json::to_value(base)) else { return vec![] };
|
||||
let mut diff = vec![];
|
||||
changed("", &v, &base, &mut diff);
|
||||
diff
|
||||
}
|
||||
|
||||
impl Session {
|
||||
/// Every print preset: [Default], then the saved ones.
|
||||
pub fn print_presets(&self) -> Vec<PrintPreset> {
|
||||
let mut v = vec![PrintPreset { name: DEFAULT_PRESET.into(), settings: PrintSettings::default() }];
|
||||
v.extend(self.prefs.print_presets.iter().cloned());
|
||||
v
|
||||
}
|
||||
|
||||
/// The settings of the print preset `name` ([Default] or a saved one, any case).
|
||||
pub fn print_preset_settings(&self, name: &str) -> Option<PrintSettings> {
|
||||
if is_default(name) {
|
||||
return Some(PrintSettings::default());
|
||||
}
|
||||
self.print_saved_index(name).and_then(|i| self.prefs.print_presets.get(i)).map(|p| p.settings.clone())
|
||||
}
|
||||
|
||||
/// The first free "Print Preset N": the name a new preset gets.
|
||||
pub fn new_print_preset_name(&self) -> String {
|
||||
(1..).map(|i| format!("Print Preset {i}")).find(|n| !self.print_preset_taken(n, None)).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// The index of the saved print preset `name` (any case).
|
||||
fn print_saved_index(&self, name: &str) -> Option<usize> {
|
||||
self.prefs.print_presets.iter().position(|q| q.name.eq_ignore_ascii_case(name.trim()))
|
||||
}
|
||||
|
||||
/// Whether `name` is [Default]'s or a saved preset's other than the one at `except`.
|
||||
fn print_preset_taken(&self, name: &str, except: Option<usize>) -> bool {
|
||||
is_default(name) || self.print_saved_index(name).is_some_and(|i| Some(i) != except)
|
||||
}
|
||||
}
|
||||
|
||||
fn row(p: PrintPreset, built_in: bool) -> Value {
|
||||
let changed = changes(&p.settings, &PrintSettings::default());
|
||||
json!({ "name": p.name, "builtIn": built_in, "settings": p.settings, "changed": changed })
|
||||
}
|
||||
|
||||
fn list(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let rows: Vec<Value> = s.print_presets().into_iter().enumerate().map(|(i, p)| row(p, i == 0)).collect();
|
||||
Ok(json!({ "presets": rows }))
|
||||
}
|
||||
|
||||
fn save(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "print.presets.save";
|
||||
let name = match str_param(p, "name").map(str::trim) {
|
||||
Some("") => return Err(bad(C, "`name` is empty")),
|
||||
Some(n) => n.to_string(),
|
||||
None => s.new_print_preset_name(),
|
||||
};
|
||||
if is_default(&name) {
|
||||
return Err(bad(C, format!("{DEFAULT_PRESET} is protected: save the settings under another name")));
|
||||
}
|
||||
let at = s.print_saved_index(&name);
|
||||
let saved = at.and_then(|i| s.prefs.print_presets.get(i)).cloned();
|
||||
// A saved preset changes from its own settings unless `preset` says where to start.
|
||||
let base = match str_param(p, "preset") {
|
||||
Some(b) => s.print_preset_settings(b).ok_or_else(|| bad(C, format!("no print preset named `{b}` (see print.presets.list)")))?,
|
||||
None => saved.as_ref().map(|old| old.settings.clone()).unwrap_or_default(),
|
||||
};
|
||||
let base = serde_json::to_value(base).map_err(|e| EngineError::Other(e.to_string()))?;
|
||||
let settings = settings_over(C, base, p)?;
|
||||
let name = match str_param(p, "newName").map(str::trim) {
|
||||
Some("") => return Err(bad(C, "`newName` is empty")),
|
||||
Some(n) if s.print_preset_taken(n, at) => return Err(bad(C, format!("a preset named `{n}` exists"))),
|
||||
Some(n) => n.to_string(),
|
||||
None => saved.map_or(name, |old| old.name),
|
||||
};
|
||||
let preset = PrintPreset { name: name.clone(), settings: settings.clone() };
|
||||
match at.and_then(|i| s.prefs.print_presets.get_mut(i)) {
|
||||
Some(slot) => *slot = preset,
|
||||
None => s.prefs.print_presets.push(preset),
|
||||
}
|
||||
Ok(json!({ "name": name, "created": at.is_none(), "settings": settings }))
|
||||
}
|
||||
|
||||
fn delete(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "print.presets.delete";
|
||||
let name = str_param(p, "name").ok_or_else(|| bad(C, "missing `name`"))?;
|
||||
if is_default(name) {
|
||||
return Err(bad(C, format!("{DEFAULT_PRESET} is protected and stays")));
|
||||
}
|
||||
let i = s.print_saved_index(name).ok_or_else(|| bad(C, format!("no saved print preset named `{name}`")))?;
|
||||
Ok(json!({ "deleted": s.prefs.print_presets.remove(i).name }))
|
||||
}
|
||||
|
||||
fn export(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "print.presets.export";
|
||||
let presets = match p.get("names").and_then(Value::as_array) {
|
||||
Some(names) => {
|
||||
let find = |n: &Value| {
|
||||
let n = n.as_str().unwrap_or_default();
|
||||
let settings = s.print_preset_settings(n).ok_or_else(|| bad(C, format!("no print preset named `{n}`")))?;
|
||||
let name = s.print_saved_index(n).and_then(|i| s.prefs.print_presets.get(i)).map_or(DEFAULT_PRESET.to_string(), |q| q.name.clone());
|
||||
Ok(PrintPreset { name, settings })
|
||||
};
|
||||
names.iter().map(find).collect::<Result<Vec<_>>>()?
|
||||
}
|
||||
None => s.prefs.print_presets.clone(),
|
||||
};
|
||||
if presets.is_empty() {
|
||||
return Err(bad(C, "no saved presets to export (name [Default] in `names` to export it)"));
|
||||
}
|
||||
let count = presets.len();
|
||||
let text = serde_json::to_string_pretty(&PresetFile { format: PRESET_FORMAT.into(), presets }).map_err(|e| bad(C, e.to_string()))?;
|
||||
match str_param(p, "path") {
|
||||
Some(path) => {
|
||||
super::fileio::write_file(path, text.as_bytes())?;
|
||||
Ok(json!({"path": path, "count": count}))
|
||||
}
|
||||
None => Ok(json!({"data": text, "count": count})),
|
||||
}
|
||||
}
|
||||
|
||||
fn import(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "print.presets.import";
|
||||
let bytes = match (str_param(p, "path"), str_param(p, "data"), str_param(p, "dataBase64")) {
|
||||
(Some(path), ..) => super::fileio::read_file(path)?,
|
||||
(None, Some(text), _) => text.as_bytes().to_vec(),
|
||||
(None, None, Some(b64)) => vectorcraft_format::base64_decode(b64).ok_or_else(|| bad(C, "bad dataBase64"))?,
|
||||
_ => return Err(bad(C, "give `path`, `data` or `dataBase64`")),
|
||||
};
|
||||
let file: PresetFile = serde_json::from_slice(&bytes).map_err(|e| bad(C, format!("not a print presets file: {e}")))?;
|
||||
if file.format != PRESET_FORMAT {
|
||||
return Err(bad(C, format!("not a print presets file (format `{}`)", file.format)));
|
||||
}
|
||||
// Values a hand-edited file may carry are refused as the print commands refuse them.
|
||||
for q in &file.presets {
|
||||
q.settings.check().map_err(|e| bad(C, format!("`{}`: {e}", q.name)))?;
|
||||
}
|
||||
let replace = bool_or(p, "replace", false);
|
||||
let mut imported = vec![];
|
||||
for mut preset in file.presets {
|
||||
// [Default] comes in as a saved copy ("Default 2"…): the built-in one stays.
|
||||
let name = preset.name.trim().trim_start_matches('[').trim_end_matches(']').trim();
|
||||
let base = Some(name).filter(|n| !n.is_empty()).unwrap_or("Print Preset").to_string();
|
||||
match s.print_saved_index(&base).filter(|_| replace) {
|
||||
Some(i) => {
|
||||
if let Some(slot) = s.prefs.print_presets.get_mut(i) {
|
||||
preset.name = slot.name.clone();
|
||||
imported.push(preset.name.clone());
|
||||
*slot = preset;
|
||||
}
|
||||
}
|
||||
None => {
|
||||
preset.name = unique_name(&base, |n| s.print_preset_taken(n, None));
|
||||
imported.push(preset.name.clone());
|
||||
s.prefs.print_presets.push(preset);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(json!({ "imported": imported }))
|
||||
}
|
||||
@@ -465,6 +465,10 @@ pub struct Prefs {
|
||||
/// Save for Web: the settings the dialog opens on (`webExport.settings`); none until set.
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub web_export_settings: Option<cmd::webexport::WebSettings>,
|
||||
/// Edit → Print Presets: the user's presets ([Default] isn't stored). A local library, not a
|
||||
/// Preferences dialog field: resetting the preferences keeps it; `print.presets.*` edit it.
|
||||
#[serde(skip_serializing_if = "Vec::is_empty")]
|
||||
pub print_presets: Vec<cmd::printpresets::PrintPreset>,
|
||||
}
|
||||
|
||||
impl Default for Prefs {
|
||||
@@ -596,6 +600,7 @@ impl Default for Prefs {
|
||||
use_compression: false,
|
||||
web_export_presets: vec![],
|
||||
web_export_settings: None,
|
||||
print_presets: vec![],
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1210,6 +1215,8 @@ mod tests_previewbounds;
|
||||
#[cfg(test)]
|
||||
mod tests_print;
|
||||
#[cfg(test)]
|
||||
mod tests_printpresets;
|
||||
#[cfg(test)]
|
||||
mod tests_printpreview;
|
||||
#[cfg(test)]
|
||||
mod tests_proxyitems;
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
//! Edit → Print Presets: [Default] is protected, saved presets round-trip through files and
|
||||
//! persist with the preferences.
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
use crate::cmd::printpresets::DEFAULT_PRESET;
|
||||
|
||||
fn is_bad_params(r: Result<serde_json::Value>) -> bool {
|
||||
matches!(r, Err(EngineError::BadParams { .. }))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn default_is_listed_first_and_protected() {
|
||||
let mut s = Session::new();
|
||||
let v = s.execute("print.presets.list", &json!({})).unwrap();
|
||||
assert_eq!((v["presets"][0]["name"].clone(), v["presets"][0]["builtIn"].clone()), (json!(DEFAULT_PRESET), json!(true)));
|
||||
assert_eq!(v["presets"][0]["settings"]["copies"], 1);
|
||||
assert_eq!(v["presets"][0]["changed"], json!([]));
|
||||
for name in [DEFAULT_PRESET, "default", " [default] "] {
|
||||
assert!(is_bad_params(s.execute("print.presets.save", &json!({"name": name, "settings": {"copies": 2}}))), "{name}");
|
||||
assert!(is_bad_params(s.execute("print.presets.delete", &json!({"name": name}))), "{name}");
|
||||
}
|
||||
// Nor can a saved preset take its name.
|
||||
s.execute("print.presets.save", &json!({"name": "Proofs"})).unwrap();
|
||||
assert!(is_bad_params(s.execute("print.presets.save", &json!({"name": "Proofs", "newName": "Default"}))));
|
||||
assert_eq!(s.prefs.print_presets.len(), 1);
|
||||
assert!(is_bad_params(s.execute("print.presets.delete", &json!({"name": "Nope"}))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_save_change_rename_and_delete() {
|
||||
let mut s = Session::new();
|
||||
let v = s.execute("print.presets.save", &json!({"settings": {"media": "a4", "marks": {"trim": true}}})).unwrap();
|
||||
assert_eq!((v["name"].clone(), v["created"].clone()), (json!("Print Preset 1"), json!(true)));
|
||||
// A saved preset changes from its own settings; `preset` starts from another one.
|
||||
let v = s.execute("print.presets.save", &json!({"name": "print preset 1", "settings": {"copies": 3}})).unwrap();
|
||||
assert_eq!((v["created"].clone(), v["settings"]["media"].clone(), v["settings"]["copies"].clone()), (json!(false), json!("a4"), json!(3)));
|
||||
let v = s.execute("print.presets.save", &json!({"name": "Plain", "preset": "Print Preset 1", "settings": {"marks": {"trim": false}}})).unwrap();
|
||||
assert_eq!((v["settings"]["copies"].clone(), v["settings"]["marks"]["trim"].clone()), (json!(3), json!(false)));
|
||||
let v = s.execute("print.presets.list", &json!({})).unwrap();
|
||||
let changed: Vec<&str> = v["presets"][1]["changed"].as_array().unwrap().iter().map(|c| c["option"].as_str().unwrap()).collect();
|
||||
assert_eq!(changed, ["copies", "marks.trim", "media"]);
|
||||
// Renames keep the settings; bad settings and unknown bases are refused.
|
||||
s.execute("print.presets.save", &json!({"name": "Plain", "newName": "Draft"})).unwrap();
|
||||
assert_eq!(s.prefs.print_presets[1].name, "Draft");
|
||||
assert!(is_bad_params(s.execute("print.presets.save", &json!({"name": "Draft", "newName": "print preset 1"}))));
|
||||
assert!(is_bad_params(s.execute("print.presets.save", &json!({"name": "X", "settings": {"copies": 0}}))));
|
||||
assert!(is_bad_params(s.execute("print.presets.save", &json!({"name": "X", "preset": "Nope"}))));
|
||||
assert!(is_bad_params(s.execute("print.presets.save", &json!({"name": " "}))));
|
||||
assert_eq!(s.execute("print.presets.delete", &json!({"name": "draft"})).unwrap()["deleted"], "Draft");
|
||||
assert_eq!(s.prefs.print_presets.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_export_and_import_round_trip() {
|
||||
let mut s = Session::new();
|
||||
s.execute("print.presets.save", &json!({"name": "Tiles", "settings": {"scaling": "tileFull", "overlap": 18}})).unwrap();
|
||||
let saved = s.prefs.print_presets.clone();
|
||||
let data = s.execute("print.presets.export", &json!({"names": ["Tiles", DEFAULT_PRESET]})).unwrap();
|
||||
assert_eq!(data["count"], 2);
|
||||
let text = data["data"].as_str().unwrap().to_string();
|
||||
s.execute("print.presets.delete", &json!({"name": "Tiles"})).unwrap();
|
||||
let r = s.execute("print.presets.import", &json!({"data": text})).unwrap();
|
||||
assert_eq!(r["imported"], json!(["Tiles", "Default 2"]), "[Default] comes in as a saved copy");
|
||||
assert_eq!(s.prefs.print_presets[0], saved[0], "the same preset comes back");
|
||||
assert_eq!(s.prefs.print_presets[1].settings, vectorcraft_pdf::PrintSettings::default());
|
||||
// replace: saved presets of the same names are overwritten.
|
||||
let r = s.execute("print.presets.import", &json!({"data": text, "replace": true})).unwrap();
|
||||
assert_eq!(r["imported"], json!(["Tiles", "Default 3"]));
|
||||
assert_eq!(s.prefs.print_presets.len(), 3);
|
||||
// Through a file, and every saved preset by default.
|
||||
let path = std::env::temp_dir().join(format!("vc-printpresets-{}.vcprintpresets", std::process::id())).to_string_lossy().to_string();
|
||||
assert_eq!(s.execute("print.presets.export", &json!({"path": path})).unwrap()["count"], 3);
|
||||
let mut t = Session::new();
|
||||
assert_eq!(t.execute("print.presets.import", &json!({"path": path})).unwrap()["imported"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(t.prefs.print_presets, s.prefs.print_presets);
|
||||
let _ = std::fs::remove_file(&path);
|
||||
// Not a presets file, or values out of range: refused, nothing added.
|
||||
for bad in [
|
||||
json!({"data": "{}"}),
|
||||
json!({"data": "{\"format\": \"vcpdfpresets\", \"presets\": []}"}),
|
||||
json!({"data": "{\"format\": \"vcprintpresets\", \"presets\": [{\"name\": \"X\", \"settings\": {\"copies\": 5000}}]}"}),
|
||||
json!({"data": "{\"format\": \"vcprintpresets\", \"presets\": [{\"name\": \"X\", \"settings\": {\"media\": \"quarto\"}}]}"}),
|
||||
json!({"dataBase64": "!!"}),
|
||||
json!({}),
|
||||
] {
|
||||
assert!(is_bad_params(t.execute("print.presets.import", &bad)), "{bad}");
|
||||
}
|
||||
assert_eq!(t.prefs.print_presets.len(), 3);
|
||||
assert!(is_bad_params(Session::new().execute("print.presets.export", &json!({}))), "nothing saved to export");
|
||||
assert!(is_bad_params(Session::new().execute("print.presets.export", &json!({"names": ["Nope"]}))));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn saved_presets_persist_with_the_preferences() {
|
||||
let mut s = Session::new();
|
||||
s.execute("print.presets.save", &json!({"name": "Kept", "settings": {"copies": 2}})).unwrap();
|
||||
let back: crate::Prefs = serde_json::from_value(s.prefs.to_json()).unwrap();
|
||||
assert_eq!(back.print_presets, s.prefs.print_presets);
|
||||
// Resetting the preferences keeps them (a library, not a preference).
|
||||
s.execute("prefs.reset", &json!({})).unwrap();
|
||||
assert_eq!(s.prefs.print_presets.len(), 1);
|
||||
// Preferences saved before print presets existed still load, and none are written while empty.
|
||||
let old: crate::Prefs = serde_json::from_value(json!({"keyboardIncrement": 2.0})).unwrap();
|
||||
assert!(old.print_presets.is_empty());
|
||||
assert!(crate::Prefs::default().to_json().get("printPresets").is_none());
|
||||
}
|
||||
@@ -1105,3 +1105,40 @@ proptest! {
|
||||
paste_from_elsewhere("clipboard.importEmf", json!({"dataBase64": mutated_b64(metafile_samples()[which].clone(), cut, &edits)}))?;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- print presets files ----------
|
||||
|
||||
/// Import print presets from `data`, then lay out (and print, when short) the rich document with
|
||||
/// each one imported.
|
||||
fn print_presets(what: &str, data: &str) -> Result<(), TestCaseError> {
|
||||
survive_library(what, "print.presets.import", data, |s, r| {
|
||||
let list = s.execute("print.presets.list", &json!({})).unwrap_or_default();
|
||||
for name in r["imported"].as_array().into_iter().flatten() {
|
||||
let Some(p) = list["presets"].as_array().into_iter().flatten().find(|p| &p["name"] == name) else { continue };
|
||||
let settings = json!({ "settings": p["settings"] });
|
||||
let pages = s.execute("print.preview", &settings).ok().and_then(|v| v["pages"].as_u64());
|
||||
let _ = s.execute("print.setup", &settings);
|
||||
if pages.is_some_and(|n| n <= 8) {
|
||||
let _ = s.execute("file.print", &settings);
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(config())]
|
||||
|
||||
#[test]
|
||||
fn print_presets_garbage_never_panics(s in ".{0,300}", head in prop::sample::select(vec!["", "{\"format\": \"vcprintpresets\", ", "{\"format\": \"vcprintpresets\", \"presets\": [{\"name\": \"x\", \"settings\": "])) {
|
||||
print_presets("print presets garbage", &format!("{head}{s}"))?;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mutated_print_presets_never_panic(cut in 0usize..20_000, edits in prop::collection::vec(arb_edit(), 0..10)) {
|
||||
let mut s = rich_session();
|
||||
s.execute("print.presets.save", &json!({"name": "Tiles", "settings": {"scaling": "tileFull", "scale": {"width": 300, "height": 300}, "marks": {"trim": true}}})).unwrap();
|
||||
s.execute("print.presets.save", &json!({"name": "Seps", "settings": {"output": {"mode": "separations", "inks": [{"name": "Cyan", "print": false}]}}})).unwrap();
|
||||
let text = s.execute("print.presets.export", &json!({"names": ["Tiles", "Seps", "[Default]"]})).unwrap()["data"].as_str().unwrap().to_string();
|
||||
print_presets("mutated print presets", &mutate_text(&text, cut, &edits))?;
|
||||
}
|
||||
}
|
||||
@@ -42,6 +42,7 @@ pub mod place;
|
||||
pub mod placement_options;
|
||||
mod png_options;
|
||||
pub mod print;
|
||||
pub mod print_presets;
|
||||
mod psd_options;
|
||||
pub mod raster_effects;
|
||||
pub mod recolor;
|
||||
@@ -238,6 +239,8 @@ registry! {
|
||||
SaveForWeb: [save_for_web::KIND] => save_for_web::SPEC,
|
||||
PsdOptions: ["psdOptions"] => png_options::SPEC,
|
||||
Print: [print::KIND] => print::SPEC,
|
||||
PrintPreset: [print::PRESET_KIND] => print::PRESET_SPEC,
|
||||
PrintPresets: [print_presets::KIND] => print_presets::SPEC,
|
||||
}
|
||||
|
||||
/// The spec for a `Dialog::kind` ([`DialogSpec::FORM`] when unregistered).
|
||||
@@ -372,3 +375,6 @@ mod tests_psd;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_print;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests_print_presets;
|
||||
@@ -1,4 +1,5 @@
|
||||
//! File → Print: the Print dialog. The printer (and Setup…) on top, then the sections General,
|
||||
//! File → Print: the Print dialog. The print preset (and Save Preset…) and the printer (and
|
||||
//! Setup…) on top, then the sections General,
|
||||
//! Marks and Bleed, Output, Graphics, Color Management, Advanced and Summary, with a preview of the
|
||||
//! printed page under the section list: the arrows step through the pages, dragging the art moves
|
||||
//! its placement on the paper.
|
||||
@@ -9,6 +10,11 @@
|
||||
//! keeps the settings with the document (`print.setup`) and prints them (`file.print`); Done
|
||||
//! (`discard: true`, then confirm) only keeps them. The preview, the inks and the Summary come
|
||||
//! from `print.preview`: the dialog lays nothing out itself.
|
||||
//!
|
||||
//! `preset` names the print preset whose settings were loaded last: setting it (the list, or
|
||||
//! `ui.dialog.set`) loads that preset's settings. Save Preset… names the settings as a preset
|
||||
//! (`print.presets.save`). The same dialog edits a preset (kind `printPreset`, from Edit → Print
|
||||
//! Presets): a `name` instead of the preset and printer, and OK saves the preset.
|
||||
|
||||
use std::sync::{Arc, LazyLock};
|
||||
|
||||
@@ -24,12 +30,31 @@ use super::{DialogSpec, form};
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::{self, Tokens};
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
use vectorcraft_engine::cmd::printpresets::DEFAULT_PRESET;
|
||||
|
||||
pub(super) const KIND: &str = "print";
|
||||
|
||||
pub(super) const SPEC: DialogSpec =
|
||||
DialogSpec { heading: |_| "Print".into(), body, confirm, ok: Some("Print"), discard: Some("Done"), ..DialogSpec::FORM };
|
||||
|
||||
/// The dialog kind of the preset editor (New / Edit in Edit → Print Presets).
|
||||
pub(super) const PRESET_KIND: &str = "printPreset";
|
||||
|
||||
pub(super) const PRESET_SPEC: DialogSpec = DialogSpec {
|
||||
heading: |d| if d.str(EDITING).is_empty() { "New Print Preset" } else { "Edit Print Preset" }.into(),
|
||||
body,
|
||||
confirm: confirm_preset,
|
||||
ok: Some("Save Preset"),
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
/// The preset editor's field holding the name of the saved preset it edits (empty: a new one).
|
||||
const EDITING: &str = "__editing";
|
||||
/// The field holding the name typed for Save Preset… (present while that row shows).
|
||||
const SAVE_AS: &str = "__savePresetAs";
|
||||
/// The preset whose settings were loaded last (`preset` loads its settings when it differs).
|
||||
const LOADED: &str = "__loaded";
|
||||
|
||||
pub(super) const SECTIONS: [&str; 7] = ["General", "Marks and Bleed", "Output", "Graphics", "Color Management", "Advanced", "Summary"];
|
||||
|
||||
/// The default print settings as JSON: what a field an agent left out reads as.
|
||||
@@ -37,11 +62,11 @@ static DEFAULTS: LazyLock<Value> = LazyLock::new(|| serde_json::to_value(PrintSe
|
||||
|
||||
/// The defaults of the settings a dialog of `kind` edits, when it is a Print dialog.
|
||||
pub(super) fn defaults(kind: &str) -> Option<&'static Value> {
|
||||
(kind == KIND).then(|| &*DEFAULTS)
|
||||
(kind == KIND || kind == PRESET_KIND).then(|| &*DEFAULTS)
|
||||
}
|
||||
|
||||
/// The fields that aren't print settings.
|
||||
const NOT_SETTINGS: [&str; 3] = ["printer", "toFile", "discard"];
|
||||
const NOT_SETTINGS: [&str; 5] = ["printer", "toFile", "discard", "preset", "name"];
|
||||
|
||||
/// The Printer list's entries besides the system's printers.
|
||||
const DEFAULT_PRINTER: &str = "Default Printer";
|
||||
@@ -55,9 +80,7 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let st = app.session.active().ok_or("no document")?;
|
||||
// The settings saved with the document, every field filled in (defaults if never set up).
|
||||
let saved: PrintSettings = st.doc.print_setup.clone().and_then(|v| serde_json::from_value(v).ok()).unwrap_or_default();
|
||||
let Value::Object(mut fields) = serde_json::to_value(saved).map_err(|e| e.to_string())? else {
|
||||
return Err("print settings are not an object".into());
|
||||
};
|
||||
let mut fields = fields_of(&saved)?;
|
||||
let printers = crate::print::printers(app);
|
||||
let list = printers["printers"].as_array().map(Vec::as_slice).unwrap_or_default();
|
||||
let default = list.iter().find(|p| p["default"] == true).or(list.first()).and_then(|p| p["name"].as_str()).unwrap_or_default();
|
||||
@@ -66,10 +89,82 @@ pub fn open(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
fields.insert("__printers".into(), printers);
|
||||
fields.insert("__section".into(), json!(SECTIONS[0]));
|
||||
fields.insert("__sheet".into(), json!(0));
|
||||
// The saved preset these settings are, if any (else [Default], shown as [Custom] if they differ).
|
||||
let preset = app.session.prefs.print_presets.iter().find(|p| p.settings == saved).map_or(DEFAULT_PRESET, |p| p.name.as_str());
|
||||
fields.insert("preset".into(), json!(preset));
|
||||
fields.insert(LOADED.into(), json!(preset));
|
||||
app.ui.dialog = Some(Dialog { kind: KIND.into(), fields });
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// The settings as dialog fields.
|
||||
fn fields_of(set: &PrintSettings) -> Result<serde_json::Map<String, Value>, String> {
|
||||
match serde_json::to_value(set).map_err(|e| e.to_string())? {
|
||||
Value::Object(fields) => Ok(fields),
|
||||
_ => Err("print settings are not an object".into()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Load the settings of the preset `preset` names once it changed (the list, or `ui.dialog.set`).
|
||||
fn sync_preset(app: &VectorcraftApp, d: &mut Dialog) -> Result<(), String> {
|
||||
let name = d.str("preset");
|
||||
if d.kind != KIND || name == d.str(LOADED) {
|
||||
return Ok(());
|
||||
}
|
||||
d.fields.insert(LOADED.into(), json!(name));
|
||||
let set = app.session.print_preset_settings(&name).ok_or_else(|| format!("no print preset named `{name}`"))?;
|
||||
d.fields.extend(fields_of(&set)?);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save Preset…: save the settings under the typed name and pick that preset.
|
||||
fn save_preset(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<Value, String> {
|
||||
let r = app.run("print.presets.save", json!({ "name": d.str(SAVE_AS), "preset": DEFAULT_PRESET, "settings": settings(d) }))?;
|
||||
d.fields.remove(SAVE_AS);
|
||||
d.fields.insert("preset".into(), r["name"].clone());
|
||||
d.fields.insert(LOADED.into(), r["name"].clone());
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// Open the preset editor on the saved preset `name`, or on a new preset starting from `preset`
|
||||
/// (default: [Default]).
|
||||
pub fn open_preset(app: &mut VectorcraftApp, params: &Value) -> Result<Value, String> {
|
||||
let s = |k: &str| params.get(k).and_then(Value::as_str);
|
||||
let (base, name, editing) = match s("name") {
|
||||
Some(name) => {
|
||||
let p = app.session.prefs.print_presets.iter().find(|p| p.name.eq_ignore_ascii_case(name.trim()));
|
||||
let p = p.ok_or_else(|| format!("no saved print preset named `{name}` ({DEFAULT_PRESET} is protected: start a new one from it)"))?;
|
||||
(p.name.clone(), p.name.clone(), p.name.clone())
|
||||
}
|
||||
None => (s("preset").unwrap_or(DEFAULT_PRESET).to_string(), app.session.new_print_preset_name(), String::new()),
|
||||
};
|
||||
let set = app.session.print_preset_settings(&base).ok_or_else(|| format!("no print preset named `{base}`"))?;
|
||||
let mut fields = fields_of(&set)?;
|
||||
fields.insert("name".into(), json!(name));
|
||||
fields.insert(EDITING.into(), json!(editing));
|
||||
fields.insert("__section".into(), json!(SECTIONS[0]));
|
||||
fields.insert("__sheet".into(), json!(0));
|
||||
app.ui.dialog = Some(Dialog { kind: PRESET_KIND.into(), fields });
|
||||
Ok(Value::Null)
|
||||
}
|
||||
|
||||
/// Save Preset (the preset editor's OK): `print.presets.save`, then back to Edit → Print Presets on
|
||||
/// it. A new preset may not take the name of another one.
|
||||
fn confirm_preset(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let (name, editing) = (d.str("name"), d.str(EDITING));
|
||||
let p = if editing.is_empty() {
|
||||
if app.session.print_preset_settings(&name).is_some() {
|
||||
return Err(format!("a preset named `{}` exists", name.trim()));
|
||||
}
|
||||
json!({ "name": name, "preset": DEFAULT_PRESET, "settings": settings(d) })
|
||||
} else {
|
||||
json!({ "name": editing, "newName": name, "settings": settings(d) })
|
||||
};
|
||||
let r = app.run("print.presets.save", p)?;
|
||||
super::print_presets::open(app, r["name"].as_str());
|
||||
Ok(r)
|
||||
}
|
||||
|
||||
/// The print settings the dialog stands for (`print.setup`'s `settings`).
|
||||
pub(super) fn settings(d: &Dialog) -> Value {
|
||||
let mut p = form::params(d);
|
||||
@@ -82,18 +177,27 @@ pub(super) fn settings(d: &Dialog) -> Value {
|
||||
}
|
||||
|
||||
/// Print, or Done (`discard`): keep the settings with the document (one undo step when they
|
||||
/// changed), then print them; settings that can't print keep the dialog open, unchanged.
|
||||
/// changed), then print them; settings that can't print keep the dialog open, unchanged. While
|
||||
/// Save Preset… asks for a name, it saves the preset instead.
|
||||
fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
let mut d = d.clone();
|
||||
// Done's flag holds for this press only: the dialog may stay open.
|
||||
if let Some(open) = app.ui.dialog.as_mut() {
|
||||
open.fields.remove("discard");
|
||||
let done = d.fields.remove("discard").is_some_and(|v| v == true);
|
||||
let synced = sync_preset(app, &mut d);
|
||||
app.ui.dialog = Some(d.clone());
|
||||
synced?;
|
||||
// While Save Preset… asks for a name, OK (and Enter) saves the preset instead.
|
||||
if d.fields.contains_key(SAVE_AS) {
|
||||
let r = save_preset(app, &mut d);
|
||||
app.ui.dialog = Some(d);
|
||||
return r;
|
||||
}
|
||||
let settings = settings(d);
|
||||
let settings = settings(&d);
|
||||
let p = json!({ "settings": settings });
|
||||
// Laid out first, so a bad range or overlap is refused before anything is kept.
|
||||
app.session.execute("print.preview", &p).map_err(|e| e.to_string())?;
|
||||
app.run("print.setup", p)?;
|
||||
if d.bool("discard") {
|
||||
if done {
|
||||
app.ui.dialog = None;
|
||||
return Ok(json!({ "settings": settings }));
|
||||
}
|
||||
@@ -106,7 +210,17 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
printer_row(app, ui, d);
|
||||
if let Err(e) = sync_preset(app, d) {
|
||||
app.status(e);
|
||||
}
|
||||
if d.kind == PRESET_KIND {
|
||||
row_with(ui, "Preset name:", TOP_LABEL_WIDTH, |ui| {
|
||||
form::text_edit(ui, d, "name", 288.0);
|
||||
});
|
||||
} else {
|
||||
preset_rows(app, ui, d);
|
||||
printer_row(app, ui, d);
|
||||
}
|
||||
ui.add_space(10.0);
|
||||
let section = current_section(d, &SECTIONS);
|
||||
let preview = preview_of(app, ui.ctx(), &settings(d));
|
||||
@@ -126,7 +240,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
"Graphics" => graphics(ui, d),
|
||||
"Color Management" => color(ui, d),
|
||||
"Advanced" => advanced(app, ui, d),
|
||||
"Summary" => summary(ui, d, &preview),
|
||||
"Summary" => summary(ui, d, &preview, app.session.active().is_some()),
|
||||
_ => general(app, ui, d),
|
||||
});
|
||||
});
|
||||
@@ -134,6 +248,49 @@ fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
/// The Print Preset row ([Default], the saved presets; [Custom] once the settings differ from the
|
||||
/// preset's) with Save Preset…, and while Save Preset… asks for a name, the row taking it.
|
||||
fn preset_rows(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
const CUSTOM: &str = "[Custom]";
|
||||
let name = d.str("preset");
|
||||
let current = serde_json::from_value::<PrintSettings>(settings(d)).ok();
|
||||
let same = current.is_some() && current == app.session.print_preset_settings(&name);
|
||||
let names: Vec<&str> = std::iter::once(DEFAULT_PRESET).chain(app.session.prefs.print_presets.iter().map(|p| p.name.as_str())).collect();
|
||||
let mut picked = None;
|
||||
let asking = d.fields.contains_key(SAVE_AS);
|
||||
let mut ask = false;
|
||||
row_with(ui, "Print Preset:", TOP_LABEL_WIDTH, |ui| {
|
||||
picked =
|
||||
widgets::dropdown(ui, "print-preset", if same { &name } else { CUSTOM }, &names, 300.0).and_then(|i| names.get(i)).map(|n| n.to_string());
|
||||
let save = ui.add_enabled_ui(!asking, |ui| widgets::flat_button(ui, "Save Preset…", 96.0)).inner;
|
||||
ask = save.on_hover_text("Save these settings as a print preset").clicked();
|
||||
});
|
||||
if let Some(n) = picked {
|
||||
// Picking the preset shown again brings its settings back.
|
||||
d.fields.insert(LOADED.into(), json!(""));
|
||||
d.fields.insert("preset".into(), json!(n));
|
||||
if let Err(e) = sync_preset(app, d) {
|
||||
app.status(e);
|
||||
}
|
||||
}
|
||||
if ask {
|
||||
d.fields.insert(SAVE_AS.into(), json!(app.session.new_print_preset_name()));
|
||||
}
|
||||
if d.fields.contains_key(SAVE_AS) {
|
||||
row_with(ui, "Save as preset:", TOP_LABEL_WIDTH, |ui| {
|
||||
form::text_edit(ui, d, SAVE_AS, 200.0);
|
||||
if widgets::flat_button(ui, "Save", 52.0).clicked()
|
||||
&& let Err(e) = save_preset(app, d)
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
if widgets::flat_button(ui, "Cancel", 60.0).clicked() {
|
||||
d.fields.remove(SAVE_AS);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// What the Printer list offers.
|
||||
enum Destination {
|
||||
Printer(String),
|
||||
@@ -233,7 +390,11 @@ fn page_preview(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog, pv:
|
||||
ui.painter().rect_filled(rect, 3.0, t.pasteboard);
|
||||
let sheet = pv.as_ref().ok().and_then(|v| current_sheet(d, v));
|
||||
let Some((index, count, sheet)) = sheet else {
|
||||
let text = pv.as_ref().err().map_or("Nothing to print", |_| "Can't print these settings: see the Summary");
|
||||
let text = match pv {
|
||||
_ if app.session.active().is_none() => "Open a document to preview its pages",
|
||||
Ok(_) => "Nothing to print",
|
||||
Err(_) => "Can't print these settings: see the Summary",
|
||||
};
|
||||
let font = egui::FontId::proportional(11.0);
|
||||
let galley = ui.painter().layout(text.into(), font, t.text_dim, rect.width() - 16.0);
|
||||
ui.painter().galley(rect.center() - galley.size() / 2.0, galley, t.text_dim);
|
||||
@@ -568,7 +729,7 @@ fn advanced(app: &VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
|
||||
/// The section an option is set in, and whether its first key is the section's own object
|
||||
/// (`output.mode` → Output › Mode).
|
||||
fn section_of(option: &str) -> (usize, bool) {
|
||||
pub(super) fn section_of(option: &str) -> (usize, bool) {
|
||||
match option.split('.').next().unwrap_or_default() {
|
||||
"marks" | "bleed" => (1, false),
|
||||
"output" => (2, true),
|
||||
@@ -580,7 +741,7 @@ fn section_of(option: &str) -> (usize, bool) {
|
||||
}
|
||||
|
||||
/// `output.mode` → "Output › Mode", `placement.x` → "General › Placement › X".
|
||||
fn option_label(option: &str) -> String {
|
||||
pub(super) fn option_label(option: &str) -> String {
|
||||
let (section, own) = section_of(option);
|
||||
let keys = option.split('.').skip(usize::from(own)).map(|k| form::humanize(k).trim_end_matches(':').to_string());
|
||||
std::iter::once(SECTIONS.get(section).copied().unwrap_or_default().to_string()).chain(keys).collect::<Vec<_>>().join(" › ")
|
||||
@@ -615,19 +776,21 @@ pub(super) fn labelled(changed: &mut [Value]) {
|
||||
}
|
||||
}
|
||||
|
||||
fn summary(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result<Value, String>) {
|
||||
fn summary(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result<Value, String>, has_doc: bool) {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let v = match pv {
|
||||
Ok(v) => v,
|
||||
match pv {
|
||||
Ok(v) => {
|
||||
heading(ui, "Pages");
|
||||
let per_copy = v["sheets"].as_array().map_or(0, Vec::len);
|
||||
ui.label(egui::RichText::new(format!("{} in all, {per_copy} per copy", v["pages"])).color(t.text));
|
||||
}
|
||||
// A preset edited without a document has no pages to count.
|
||||
Err(_) if !has_doc => {}
|
||||
Err(e) => {
|
||||
heading(ui, "Error");
|
||||
ui.label(egui::RichText::new(format!("⚠ {e}")).color(t.text));
|
||||
return;
|
||||
}
|
||||
};
|
||||
heading(ui, "Pages");
|
||||
let per_copy = v["sheets"].as_array().map_or(0, Vec::len);
|
||||
ui.label(egui::RichText::new(format!("{} in all, {per_copy} per copy", v["pages"])).color(t.text));
|
||||
}
|
||||
heading(ui, "Options");
|
||||
let mut changed = vec![];
|
||||
vectorcraft_engine::cmd::fileio::pdf::changed("", &settings(d), &DEFAULTS, &mut changed);
|
||||
@@ -637,6 +800,8 @@ fn summary(ui: &mut egui::Ui, d: &mut Dialog, pv: &Result<Value, String>) {
|
||||
note(ui, "Every option is at its default.");
|
||||
}
|
||||
super::save_pdf::option_rows(ui, &changed, option_label);
|
||||
heading(ui, "Warnings");
|
||||
super::save_pdf::warning_rows(ui, v["warnings"].as_array().map(Vec::as_slice).unwrap_or_default());
|
||||
if let Ok(v) = pv {
|
||||
heading(ui, "Warnings");
|
||||
super::save_pdf::warning_rows(ui, v["warnings"].as_array().map(Vec::as_slice).unwrap_or_default());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,163 @@
|
||||
//! Edit → Print Presets: every preset in a list ([Default], protected, then the saved ones) with
|
||||
//! how the selected one differs from [Default] beside it; New… (a preset starting from the
|
||||
//! selected one), Edit… (saved presets), Delete, Import… and Export… (the selected preset) under
|
||||
//! the list. New… and Edit… open the preset editor (the Print dialog, kind `printPreset`), whose OK
|
||||
//! comes back here.
|
||||
//!
|
||||
//! Field: `selected` (a preset's name).
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::cmd::printpresets::{DEFAULT_PRESET, PRESET_EXTS, PRESET_FORMAT, changes};
|
||||
|
||||
use super::DialogSpec;
|
||||
use super::print::{open_preset, option_label, section_of};
|
||||
use super::save_pdf::option_rows;
|
||||
use crate::state::Dialog;
|
||||
use crate::theme::Tokens;
|
||||
use crate::{VectorcraftApp, widgets};
|
||||
|
||||
/// The dialog kind of Print Presets.
|
||||
pub const KIND: &str = "printPresets";
|
||||
|
||||
pub(super) const SPEC: DialogSpec = DialogSpec {
|
||||
heading: |_| "Print Presets".into(),
|
||||
body,
|
||||
confirm: |app, _| {
|
||||
app.ui.dialog = None;
|
||||
Ok(Value::Null)
|
||||
},
|
||||
ok: None,
|
||||
min_width: 620.0,
|
||||
..DialogSpec::FORM
|
||||
};
|
||||
|
||||
/// Open the presets manager on `selected` (default: [Default]).
|
||||
pub fn open(app: &mut VectorcraftApp, selected: Option<&str>) {
|
||||
app.ui.dialog = Some(Dialog::new(KIND, json!({ "selected": selected.unwrap_or(DEFAULT_PRESET) })));
|
||||
}
|
||||
|
||||
/// What a button asked for, done once the list and the details are drawn.
|
||||
pub(super) enum Action {
|
||||
Select(String),
|
||||
New,
|
||||
Edit,
|
||||
Delete,
|
||||
Import,
|
||||
Export,
|
||||
}
|
||||
|
||||
fn body(app: &mut VectorcraftApp, ui: &mut egui::Ui, d: &mut Dialog) -> bool {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let saved = &app.session.prefs.print_presets;
|
||||
let selected = d.str("selected");
|
||||
// 0 is [Default]; saved presets follow.
|
||||
let i = saved.iter().position(|p| p.name.eq_ignore_ascii_case(selected.trim())).map_or(0, |i| i + 1);
|
||||
let name = i.checked_sub(1).and_then(|k| saved.get(k)).map_or(DEFAULT_PRESET, |p| p.name.as_str()).to_string();
|
||||
let builtin = i == 0;
|
||||
let mut act = None;
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(280.0);
|
||||
widgets::dim_label(ui, "Presets:");
|
||||
widgets::list_box(ui, |ui| {
|
||||
egui::ScrollArea::vertical().id_salt("print-presets").min_scrolled_height(240.0).max_height(240.0).show(ui, |ui| {
|
||||
ui.set_width(ui.available_width());
|
||||
ui.set_min_height(240.0);
|
||||
for (k, n) in std::iter::once(DEFAULT_PRESET).chain(saved.iter().map(|p| p.name.as_str())).enumerate() {
|
||||
if ui.selectable_label(k == i, n).clicked() {
|
||||
act = Some(Action::Select(n.to_string()));
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
ui.horizontal(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
if widgets::flat_button(ui, "New…", 48.0).on_hover_text("A new preset starting from the selected one").clicked() {
|
||||
act = Some(Action::New);
|
||||
}
|
||||
if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Edit…", 48.0)).inner.clicked() {
|
||||
act = Some(Action::Edit);
|
||||
}
|
||||
if ui.add_enabled_ui(!builtin, |ui| widgets::flat_button(ui, "Delete", 52.0)).inner.clicked() {
|
||||
act = Some(Action::Delete);
|
||||
}
|
||||
if widgets::flat_button(ui, "Import…", 60.0).clicked() {
|
||||
act = Some(Action::Import);
|
||||
}
|
||||
if widgets::flat_button(ui, "Export…", 60.0).on_hover_text("Save the selected preset to a file").clicked() {
|
||||
act = Some(Action::Export);
|
||||
}
|
||||
});
|
||||
});
|
||||
ui.add_space(18.0);
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(300.0);
|
||||
ui.label(egui::RichText::new(&name).color(t.text_strong).strong());
|
||||
ui.add_space(8.0);
|
||||
widgets::dim_label(ui, &format!("Differences from {DEFAULT_PRESET}:"));
|
||||
let settings = i.checked_sub(1).and_then(|k| saved.get(k)).map(|p| &p.settings);
|
||||
let mut changed = settings.map(|s| changes(s, &Default::default())).unwrap_or_default();
|
||||
changed.sort_by_key(|c| section_of(c["option"].as_str().unwrap_or_default()).0);
|
||||
egui::ScrollArea::vertical().id_salt("print-preset-details").max_height(200.0).show(ui, |ui| {
|
||||
if changed.is_empty() {
|
||||
ui.label(egui::RichText::new("None.").color(t.text_dim).size(11.5));
|
||||
}
|
||||
option_rows(ui, &changed, option_label);
|
||||
});
|
||||
if builtin {
|
||||
ui.add_space(6.0);
|
||||
ui.label(egui::RichText::new("[Default] is protected: New… starts an editable copy.").color(t.text_dim).size(11.5));
|
||||
}
|
||||
});
|
||||
});
|
||||
if let Some(a) = act
|
||||
&& let Err(e) = run(app, d, a, &name)
|
||||
{
|
||||
app.status(e);
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn select(d: &mut Dialog, name: &str) {
|
||||
d.fields.insert("selected".into(), json!(name));
|
||||
}
|
||||
|
||||
pub(super) fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action, current: &str) -> Result<(), String> {
|
||||
match act {
|
||||
Action::Select(name) => select(d, &name),
|
||||
// The editor replaces this dialog; its OK comes back here.
|
||||
Action::New => {
|
||||
open_preset(app, &json!({ "preset": current }))?;
|
||||
*d = app.ui.dialog.take().ok_or("no preset editor")?;
|
||||
}
|
||||
Action::Edit => {
|
||||
open_preset(app, &json!({ "name": current }))?;
|
||||
*d = app.ui.dialog.take().ok_or("no preset editor")?;
|
||||
}
|
||||
Action::Delete => {
|
||||
let i = app.session.prefs.print_presets.iter().position(|p| p.name == current);
|
||||
app.run("print.presets.delete", json!({ "name": current }))?;
|
||||
// The one above it takes its place ([Default] above the first saved one).
|
||||
let next = i.and_then(|i| i.checked_sub(1)).and_then(|i| app.session.prefs.print_presets.get(i)).map(|p| p.name.clone());
|
||||
select(d, next.as_deref().unwrap_or(DEFAULT_PRESET));
|
||||
}
|
||||
Action::Import => {
|
||||
// On the web the picked file arrives later and imports through `io::open_bytes`.
|
||||
if let Some(f) = app.services.open_async.as_mut() {
|
||||
f();
|
||||
return Ok(());
|
||||
}
|
||||
let pick = crate::FilePick { filters: vec![("Print presets", PRESET_EXTS)], ..Default::default() };
|
||||
let path = app.services.pick_open.as_mut().and_then(|f| f(&pick)).ok_or("cancelled")?;
|
||||
let r = app.run("print.presets.import", json!({ "path": path }))?;
|
||||
if let Some(first) = r["imported"].get(0).and_then(Value::as_str) {
|
||||
select(d, first);
|
||||
}
|
||||
}
|
||||
Action::Export => {
|
||||
crate::io::save_command_output(app, "print.presets.export", PRESET_FORMAT, json!({ "names": [current] }))?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
//! Print presets in the UI: the Print dialog's preset list and Save Preset…, and Edit → Print
|
||||
//! Presets (New…, Edit…, Delete, Import…, Export…) with its preset editor.
|
||||
|
||||
use serde_json::{Value, json};
|
||||
use vectorcraft_engine::Session;
|
||||
use vectorcraft_engine::cmd::printpresets::DEFAULT_PRESET;
|
||||
|
||||
use super::print::{KIND, PRESET_KIND, SECTIONS};
|
||||
use super::print_presets::{self, Action};
|
||||
use crate::{VectorcraftApp, theme};
|
||||
|
||||
fn frame(app: &mut VectorcraftApp) {
|
||||
let ctx = egui::Context::default();
|
||||
theme::install_fonts(&ctx);
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| super::show(app, ui.ctx()));
|
||||
out.textures_delta.clear();
|
||||
}
|
||||
|
||||
/// Press a manager button on the preset `current`, as the body does.
|
||||
fn press(app: &mut VectorcraftApp, act: Action, current: &str) {
|
||||
let mut d = app.ui.dialog.take().unwrap();
|
||||
print_presets::run(app, &mut d, act, current).unwrap();
|
||||
app.ui.dialog = Some(d);
|
||||
frame(app);
|
||||
}
|
||||
|
||||
fn kind(app: &VectorcraftApp) -> Option<&str> {
|
||||
app.ui.dialog.as_ref().map(|d| d.kind.as_str())
|
||||
}
|
||||
|
||||
fn field(app: &VectorcraftApp, k: &str) -> Value {
|
||||
app.ui.dialog.as_ref().and_then(|d| d.fields.get(k).cloned()).unwrap_or_default()
|
||||
}
|
||||
|
||||
fn set(app: &mut VectorcraftApp, k: &str, v: Value) {
|
||||
app.ui.dialog.as_mut().expect("a dialog is open").fields.insert(k.into(), v);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_print_dialog_saves_and_loads_presets() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 200, "height": 100})).unwrap();
|
||||
app.run("file.print", json!({})).unwrap();
|
||||
assert_eq!(field(&app, "preset"), DEFAULT_PRESET, "never set up: [Default]");
|
||||
// Save Preset… asks for a name; OK (and Enter) then saves the preset, not printing.
|
||||
set(&mut app, "copies", json!(4));
|
||||
set(&mut app, "media", json!("a4"));
|
||||
set(&mut app, "__savePresetAs", json!("Proofs"));
|
||||
frame(&mut app);
|
||||
let r = super::confirm(&mut app).unwrap();
|
||||
assert_eq!(r["name"], "Proofs");
|
||||
assert_eq!(kind(&app), Some(KIND), "the Print dialog stays open");
|
||||
assert_eq!(field(&app, "preset"), "Proofs");
|
||||
assert!(field(&app, "__savePresetAs").is_null());
|
||||
let saved = &app.session.prefs.print_presets[0].settings;
|
||||
assert_eq!((saved.copies, saved.media), (4, vectorcraft_pdf::Media::A4));
|
||||
// Setting `preset` (the list, or an agent) loads that preset's settings.
|
||||
set(&mut app, "preset", json!(DEFAULT_PRESET));
|
||||
frame(&mut app);
|
||||
assert_eq!((field(&app, "copies"), field(&app, "media")), (json!(1), json!("letter")));
|
||||
set(&mut app, "preset", json!("proofs"));
|
||||
set(&mut app, "discard", json!(true));
|
||||
super::confirm(&mut app).unwrap();
|
||||
let setup = app.session.active().unwrap().doc.print_setup.clone().unwrap();
|
||||
assert_eq!((setup["copies"].clone(), setup["media"].clone()), (json!(4), json!("a4")), "Done kept the preset's settings");
|
||||
// The next Print knows the preset its settings are.
|
||||
app.run("file.print", json!({})).unwrap();
|
||||
assert_eq!(field(&app, "preset"), "Proofs");
|
||||
frame(&mut app);
|
||||
// An unknown preset is refused; [Default]'s name can't be taken.
|
||||
set(&mut app, "preset", json!("Nope"));
|
||||
set(&mut app, "discard", json!(true));
|
||||
assert!(super::confirm(&mut app).is_err());
|
||||
assert!(field(&app, "discard").is_null(), "Done's press doesn't stick to the dialog");
|
||||
set(&mut app, "__savePresetAs", json!("[Default]"));
|
||||
assert!(super::confirm(&mut app).is_err());
|
||||
assert_eq!(app.session.prefs.print_presets.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_manager_adds_edits_and_deletes_presets_through_the_editor() {
|
||||
// Edit → Print Presets works without a document too.
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
assert!(crate::menus::menu_entries(&app).iter().any(|e| e.command.as_deref() == Some("ui.printPresetsDialog") && e.enabled));
|
||||
app.run("ui.printPresetsDialog", json!({})).unwrap();
|
||||
frame(&mut app);
|
||||
assert_eq!((kind(&app), field(&app, "selected")), (Some(print_presets::KIND), json!(DEFAULT_PRESET)));
|
||||
// [Default] can't be edited: New… starts a copy.
|
||||
assert!(app.run("ui.printPresetDialog", json!({"name": DEFAULT_PRESET})).is_err());
|
||||
app.run("ui.printPresetDialog", json!({"preset": DEFAULT_PRESET})).unwrap();
|
||||
assert_eq!((kind(&app), field(&app, "name")), (Some(PRESET_KIND), json!("Print Preset 1")));
|
||||
for s in SECTIONS {
|
||||
set(&mut app, "__section", json!(s));
|
||||
frame(&mut app);
|
||||
assert_eq!(kind(&app), Some(PRESET_KIND), "{s}");
|
||||
}
|
||||
set(&mut app, "name", json!("Tiled"));
|
||||
set(&mut app, "scaling", json!("tileFull"));
|
||||
super::confirm(&mut app).unwrap();
|
||||
assert_eq!((kind(&app), field(&app, "selected")), (Some(print_presets::KIND), json!("Tiled")), "back to the manager");
|
||||
frame(&mut app);
|
||||
// A new preset can't take a name in use; Edit… renames.
|
||||
app.run("ui.printPresetDialog", json!({"preset": "Tiled"})).unwrap();
|
||||
assert_eq!(field(&app, "scaling"), "tileFull", "New… starts from the selected preset");
|
||||
set(&mut app, "name", json!("tiled"));
|
||||
assert!(super::confirm(&mut app).is_err());
|
||||
assert_eq!(kind(&app), Some(PRESET_KIND), "stays open on a taken name");
|
||||
app.run("ui.printPresetDialog", json!({"name": "Tiled"})).unwrap();
|
||||
set(&mut app, "name", json!("Posters"));
|
||||
set(&mut app, "overlap", json!(9));
|
||||
super::confirm(&mut app).unwrap();
|
||||
let p = &app.session.prefs.print_presets;
|
||||
assert_eq!((p.len(), p[0].name.as_str(), p[0].settings.overlap), (1, "Posters", 9.0));
|
||||
// Delete selects the one above ([Default]).
|
||||
press(&mut app, Action::Delete, "Posters");
|
||||
assert!(app.session.prefs.print_presets.is_empty());
|
||||
assert_eq!(field(&app, "selected"), DEFAULT_PRESET);
|
||||
super::confirm(&mut app).unwrap();
|
||||
assert!(app.ui.dialog.is_none(), "Close");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn presets_files_import_and_export() {
|
||||
let dir = std::env::temp_dir().join(format!("vc-printpresets-ui-{}", std::process::id()));
|
||||
std::fs::create_dir_all(&dir).unwrap();
|
||||
let file = dir.join("mine.vcprintpresets").to_string_lossy().to_string();
|
||||
let picked = file.clone();
|
||||
let services = crate::Services {
|
||||
pick_save: Some(Box::new(move |_: &crate::FilePick| Some(picked.clone()))),
|
||||
write: Some(Box::new(|p: &str, b: &[u8]| std::fs::write(p, b).map_err(|e| e.to_string()))),
|
||||
..Default::default()
|
||||
};
|
||||
let mut app = VectorcraftApp::new(Session::new(), services);
|
||||
app.run("print.presets.save", json!({"name": "Mine", "settings": {"copies": 2}})).unwrap();
|
||||
app.run("ui.printPresetsDialog", json!({"selected": "Mine"})).unwrap();
|
||||
press(&mut app, Action::Export, "Mine");
|
||||
// Opening the file imports it (also how the web's picked file arrives).
|
||||
let bytes = std::fs::read(&file).unwrap();
|
||||
crate::io::open_bytes(&mut app, &file, &bytes, Some(file.clone())).unwrap();
|
||||
let names: Vec<&str> = app.session.prefs.print_presets.iter().map(|p| p.name.as_str()).collect();
|
||||
assert_eq!(names, ["Mine", "Mine 2"]);
|
||||
assert_eq!(app.session.prefs.print_presets[1].settings.copies, 2);
|
||||
assert!(app.ui.status.contains("print presets"), "{}", app.ui.status);
|
||||
let _ = std::fs::remove_dir_all(dir);
|
||||
}
|
||||
@@ -15,13 +15,14 @@ use crate::{FilePick, Services, VectorcraftApp, dialogs};
|
||||
const TEMPLATE_EXTS: &[&str] = &["vctemplate", "ait", "vectorcraft", "drawcraft"];
|
||||
|
||||
/// Open bytes of any readable format as a new document (templates open untitled); swatch and
|
||||
/// graphic style library files open in the library panel and flattener and PDF presets files are
|
||||
/// imported.
|
||||
/// graphic style library files open in the library panel and flattener, PDF and print presets
|
||||
/// files are imported.
|
||||
pub fn open_bytes(app: &mut VectorcraftApp, name: &str, bytes: &[u8], path: Option<String>) -> Result<(), String> {
|
||||
let ext = fileio::extension(name);
|
||||
let presets = [
|
||||
(vectorcraft_engine::cmd::flatten::PRESET_EXTS, "flattener.presets.import", "flattener presets"),
|
||||
(vectorcraft_engine::cmd::pdfcmds::PRESET_EXTS, "pdf.preset.import", "PDF presets"),
|
||||
(vectorcraft_engine::cmd::printpresets::PRESET_EXTS, "print.presets.import", "print presets"),
|
||||
];
|
||||
if let Some((_, import, what)) = presets.iter().find(|(exts, ..)| exts.contains(&ext.as_str())) {
|
||||
let r = app.run(import, serde_json::json!({"data": String::from_utf8_lossy(bytes)}))?;
|
||||
|
||||
@@ -507,7 +507,7 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"file.print",
|
||||
"Print…",
|
||||
"Cmd+P",
|
||||
"{} open the Print dialog (dialog `print`: the print.setup settings, printer, toFile; Print keeps the settings with the document and prints, Done (discard: true) only keeps them); with params {settings? (over the document's), printer? (print.printers name; \"\" the default), toFile?, path?} print without it: the print-ready PDF (engine file.print) goes to the printer → {pages, printer, printed: true, warnings}; with toFile, a path or no printing here (no print service) it is saved as a PDF at path (else a picked file, the web downloads it) → {path, pages, printed: false, warnings}",
|
||||
"{} open the Print dialog (dialog `print`: the print.setup settings, preset (setting it loads that print preset; Save Preset… saves them as one), printer, toFile; Print keeps the settings with the document and prints, Done (discard: true) only keeps them); with params {settings? (over the document's), printer? (print.printers name; \"\" the default), toFile?, path?} print without it: the print-ready PDF (engine file.print) goes to the printer → {pages, printer, printed: true, warnings}; with toFile, a path or no printing here (no print service) it is saved as a PDF at path (else a picked file, the web downloads it) → {path, pages, printed: false, warnings}",
|
||||
),
|
||||
(
|
||||
"print.printers",
|
||||
@@ -516,6 +516,18 @@ pub const UI_COMMANDS: &[(&str, &str, &str, &str)] = &[
|
||||
"{} → {printers: [{name, default}] (the system's printers), service (printing is available; else Print saves a PDF), setup (print.printerSetup can open the printer settings)}",
|
||||
),
|
||||
("print.printerSetup", "Printer Setup…", "", "{printer?} open the system's settings of the printer (the Print dialog's Setup…; desktop)"),
|
||||
(
|
||||
"ui.printPresetsDialog",
|
||||
"Print Presets…",
|
||||
"",
|
||||
"{selected?: preset name} open Edit → Print Presets (dialog `printPresets`, field `selected`): [Default] (protected) and the saved presets, with how the selected one differs from [Default]. New… and Edit… open the preset editor (ui.printPresetDialog); Delete, Import… and Export… run print.presets.delete / import / export",
|
||||
),
|
||||
(
|
||||
"ui.printPresetDialog",
|
||||
"Print Preset",
|
||||
"",
|
||||
"{name?: a saved preset to edit | preset?: the preset a new one starts from (default [Default])} open the preset editor (dialog `printPreset`: the Print dialog's settings fields plus `name`); OK runs print.presets.save and returns to Print Presets",
|
||||
),
|
||||
];
|
||||
|
||||
/// Handle a UI command. `None` = not a UI command (the engine handles it).
|
||||
@@ -922,6 +934,11 @@ pub fn run_ui_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<R
|
||||
None => Err("printing isn't available here".into()),
|
||||
}
|
||||
}
|
||||
"ui.printPresetsDialog" => {
|
||||
crate::dialogs::print_presets::open(app, s("selected").as_deref());
|
||||
Ok(Value::Null)
|
||||
}
|
||||
"ui.printPresetDialog" => crate::dialogs::print::open_preset(app, p),
|
||||
_ => return None,
|
||||
};
|
||||
Some(r)
|
||||
@@ -1293,7 +1310,7 @@ pub fn menu_tree() -> Vec<(&'static str, Vec<Item>)> {
|
||||
c("Edit Original", "links.editOriginal"),
|
||||
Sep,
|
||||
c("Transparency Flattener Presets…", "ui.flattenerPresetsDialog"),
|
||||
todo("Print Presets…"),
|
||||
c("Print Presets…", "ui.printPresetsDialog"),
|
||||
c("PDF Presets…", "ui.pdfPresetsDialog"),
|
||||
cp("Perspective Grid Presets…", "perspective.grid.preset", json!({"kind": 2})),
|
||||
Sep,
|
||||
|
||||
@@ -405,3 +405,14 @@ PDF. With `discard: true` it is Done: it only keeps the settings. Settings that
|
||||
range of no tiles) answer with an error and keep the dialog open. `file.print {settings?, printer?, toFile?, path?}`
|
||||
prints without the dialog; `print.printers` lists the printers and `print.printerSetup {printer?}` opens their system
|
||||
settings (Setup…).
|
||||
|
||||
Print presets: in the `print` dialog, `preset` names the print preset whose settings were loaded last; setting it
|
||||
(`ui.dialog.set {field: "preset", value: "Posters"}`) loads that preset's settings, and the list reads `[Custom]` once
|
||||
they differ. Save Preset… shows a name field (`__savePresetAs`): while it shows, `ui.dialog.confirm` saves the dialog's
|
||||
settings as that preset (`print.presets.save`) and selects it instead of printing. `ui.printPresetsDialog {selected?}`
|
||||
opens Edit → Print Presets (dialog `printPresets`, field `selected`); `[Default]` is protected. New… and Edit… open the
|
||||
preset editor, which `ui.printPresetDialog {name? (a saved preset to edit) | preset? (the preset a new one starts
|
||||
from)}` opens too: dialog `printPreset`, the Print dialog's settings fields plus `name`; `ui.dialog.confirm` runs
|
||||
`print.presets.save` and returns to Print Presets (a new preset can't take a name in use). Delete, Import… and
|
||||
Export… are `print.presets.delete`, `print.presets.import` and `print.presets.export`; `ui.dialog.confirm` closes.
|
||||
Opening a `.vcprintpresets` file with `app.open` imports its presets.
|
||||
+15
@@ -1506,3 +1506,18 @@ default printer) or, with `toFile`, a `path` or no printing available, saves the
|
||||
{"name":"run_command","arguments":{"command":"print.preview","params":{"settings":{"copies":2,"output":{"mode":"separations"}}}}}
|
||||
{"name":"run_command","arguments":{"command":"file.print","params":{"path":"/tmp/job.pdf"}}}
|
||||
```
|
||||
|
||||
## Print presets
|
||||
|
||||
`print.presets.list` lists `[Default]` (the default print settings, `builtIn: true`, protected) and the saved presets,
|
||||
each with its `settings` and how they differ from `[Default]` (`changed`). `print.presets.save {name?, newName?,
|
||||
preset?, settings?}` creates or changes a saved preset (starting from its own settings, or from `preset`, with
|
||||
`settings` over them) and `print.presets.delete {name}` deletes one; `[Default]` can be neither changed nor deleted.
|
||||
`print.presets.export {names?, path?}` writes them as a `.vcprintpresets` JSON file (without `path` it returns `data`),
|
||||
and `print.presets.import {path? | data? | dataBase64?, replace?}` adds a file's presets (a name in use gets a number
|
||||
unless `replace`). Saved presets live with the preferences. To print with a preset, pass its `settings` to
|
||||
`print.setup`, `print.preview` or `file.print`.
|
||||
|
||||
```json
|
||||
{"name":"run_command","arguments":{"command":"print.presets.save","params":{"name":"Posters","settings":{"scaling":"tileImageable","overlap":18,"marks":{"trim":true}}}}}
|
||||
```
|
||||
Reference in new issue
Block a user