diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index cbf3bc5e3..6e67578a8 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -286,14 +286,16 @@ pub(crate) fn is_web(app: &VectorcraftApp) -> bool { app.services.download.is_some() } -/// Write `bytes` to `path`, else to a file picked with `name` suggested next to the document (the -/// web downloads them as `name`) → where they went. +/// Write `bytes` to `path`, else to a file picked in a save panel that suggests `name` next to the +/// document and shows its file type, [`file_types`] (the web downloads them as `name`) → where +/// they went. pub(crate) fn write_named(app: &mut VectorcraftApp, path: Option, name: &str, bytes: &[u8]) -> Result { let path = match path { Some(p) => p, None => { let (_, folder) = suggested(app, ""); - pick_path(app, &FilePick { name: name.to_string(), folder, ..Default::default() })? + let filters = file_types(&fileio::extension(name)); + pick_path(app, &FilePick { name: name.to_string(), folder, filters })? } }; write_reporting(app, &path, bytes)?; @@ -344,14 +346,40 @@ fn suggested(app: &VectorcraftApp, ext: &str) -> (String, Option) { (format!("{}.{ext}", fileio::file_stem(&t)), folder.filter(|f| !f.is_empty())) } +/// The save panel's file type, `(label, extensions)`, for the files `write_named` and +/// `target_path` write that are not a document format (`fileio::format` has those). +const FILE_TYPES: &[(&str, &[&str])] = &[ + ("Swatch libraries", &["vcswatches"]), + ("Swatch libraries", &["gpl"]), + ("Swatch libraries", &["ase"]), + ("Graphic Style Libraries", &[vectorcraft_doc::style_libs::STYLES_EXT]), + ("Libraries", vectorcraft_engine::cmd::library::LIBRARY_EXTS), + ("Flattener presets", vectorcraft_engine::cmd::flatten::PRESET_EXTS), + ("PDF presets", vectorcraft_engine::cmd::pdfcmds::PRESET_EXTS), + ("Perspective grid presets", vectorcraft_engine::cmd::perspgrid::PRESET_EXTS), + ("Print presets", vectorcraft_engine::cmd::printpresets::PRESET_EXTS), + ("CSS", &["css"]), + ("PostScript", &["ps"]), +]; + +/// The file type a save panel shows for a file with extension `ext`: its format's, else its +/// [`FILE_TYPES`] row; none for any other extension. +fn file_types(ext: &str) -> Vec<(&'static str, &'static [&'static str])> { + if let Some(f) = fileio::format(ext) { + return vec![(f.label, f.extensions)]; + } + let ext = ext.trim_start_matches('.').to_ascii_lowercase(); + FILE_TYPES.iter().find(|(_, exts)| exts.contains(&ext.as_str())).copied().into_iter().collect() +} + /// `path`, else one picked in a save panel (the web: the suggested name) for the active document -/// with extension `ext`, filtered to that format when it is one. +/// with extension `ext`, filtered to its file type when it has one ([`file_types`]). pub(crate) fn target_path(app: &mut VectorcraftApp, path: Option, ext: &str) -> Result { if let Some(p) = path { return Ok(p); } let (name, folder) = suggested(app, ext); - let filters = fileio::format(ext).map(|f| vec![(f.label, f.extensions)]).unwrap_or_default(); + let filters = file_types(ext); pick_path(app, &FilePick { name, folder, filters }) } @@ -684,7 +712,8 @@ pub fn export(app: &mut VectorcraftApp, format: Option<&str>, path: Option Result { let (path, _) = run_to_file(app, id, ext, params)?; app.status(format!("Saved {path}")); diff --git a/crates/ui-egui/src/shortcut_editor.rs b/crates/ui-egui/src/shortcut_editor.rs index 9871d2702..9d0b3aea6 100644 --- a/crates/ui-egui/src/shortcut_editor.rs +++ b/crates/ui-egui/src/shortcut_editor.rs @@ -476,7 +476,8 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option { let v = export_json(&app.ui.shortcut_set, &app.ui.shortcut_overrides); let bytes = serde_json::to_vec_pretty(&v).unwrap_or_default(); - let path = s("path").or_else(|| crate::picks::save(app, &crate::FilePick::named("VectorCraft Shortcuts.json"))); + let pick = crate::FilePick { filters: vec![("Shortcut sets", &["json"])], ..crate::FilePick::named("VectorCraft Shortcuts.json") }; + let path = s("path").or_else(|| crate::picks::save(app, &pick)); match path { Some(path) => match app.services.write.as_mut() { Some(w) => w(&path, &bytes).map(|_| json!({"path": path})), diff --git a/crates/ui-egui/src/tests_saveext.rs b/crates/ui-egui/src/tests_saveext.rs index 949da5c39..c989f06fa 100644 --- a/crates/ui-egui/src/tests_saveext.rs +++ b/crates/ui-egui/src/tests_saveext.rs @@ -34,6 +34,73 @@ fn last(w: &Written) -> String { w.borrow().last().cloned().unwrap_or_default() } +/// The file types each save panel was asked to show. +type Shown = Rc>>>; + +/// An app with a document whose save panel records the file types it shows and answers the +/// suggested name in `/tmp`. +fn panel_app() -> (VectorcraftApp, Shown) { + let shown = Shown::default(); + let s = shown.clone(); + let services = Services { + write: Some(Box::new(|_: &str, _: &[u8]| Ok(()))), + pick_save: Some(Box::new(move |pick: &FilePick| { + s.borrow_mut().push(pick.filters.clone()); + Some(format!("/tmp/{}", pick.name)) + })), + ..Default::default() + }; + let mut app = VectorcraftApp::new(Session::new(), services); + app.run("file.new", json!({"width": 100, "height": 100})).unwrap(); + app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 30, "height": 20})).unwrap(); + (app, shown) +} + +/// The file types the last save panel showed. +fn last_shown(s: &Shown) -> Vec<(&'static str, &'static [&'static str])> { + s.borrow().last().cloned().unwrap_or_default() +} + +/// Document Info's, the libraries', the presets', the CSS, the PostScript and the shortcut saves +/// show the one file type they write in the save panel, as Save and Export show theirs. +#[test] +fn save_panels_show_the_file_type_they_write() { + use vectorcraft_engine::cmd::{flatten, pdfcmds, perspgrid, printpresets}; + let (mut app, shown) = panel_app(); + app.run("docInfo.save", json!({})).unwrap(); + assert_eq!(last_shown(&shown), [("Text", &["txt"][..])], "Document Info › Save"); + for format in ["vcswatches", "gpl", "ase", "css"] { + app.run("ui.saveSwatchLibrary", json!({})).unwrap(); + app.ui.dialog.as_mut().unwrap().fields.insert("format".into(), json!(format)); + crate::dialogs::confirm(&mut app).unwrap(); + let label = if format == "css" { "CSS" } else { "Swatch libraries" }; + assert_eq!(last_shown(&shown), [(label, &[format][..])], "Save Swatch Library as .{format}"); + } + app.run("ui.saveGraphicStyleLibrary", json!({})).unwrap(); + crate::dialogs::confirm(&mut app).unwrap(); + assert_eq!(last_shown(&shown), [("Graphic Style Libraries", &["vcstyles"][..])], "Save Graphic Style Library"); + let id = app.run("library.create", json!({"name": "Brand"})).unwrap()["id"].as_str().unwrap().to_string(); + io::save_command_output_named(&mut app, "library.export", "Brand.vclibrary", json!({"library": id})).unwrap(); + assert_eq!(last_shown(&shown), [("Libraries", &["vclibrary"][..])], "Libraries panel › Export Library…"); + app.run("css.exportFile", json!({"scope": "all"})).unwrap(); + assert_eq!(last_shown(&shown), [("CSS", &["css"][..])], "CSS Properties › Export All…"); + app.run("file.print", json!({"format": "postscript"})).unwrap(); + assert_eq!(last_shown(&shown), [("PostScript", &["ps"][..])], "Print to a PostScript file"); + app.run("shortcuts.export", json!({})).unwrap(); + assert_eq!(last_shown(&shown), [("Shortcut sets", &["json"][..])], "Keyboard Shortcuts › Export…"); + let presets = [ + ("pdf.preset.save", json!({"name": "Mine"}), "pdf.preset.export", "PDF presets", pdfcmds::PRESET_EXTS), + ("print.presets.save", json!({"name": "Mine"}), "print.presets.export", "Print presets", printpresets::PRESET_EXTS), + ("flattener.presets.save", json!({"name": "Mine"}), "flattener.presets.export", "Flattener presets", flatten::PRESET_EXTS), + ("perspective.presets.save", json!({"name": "Mine"}), "perspective.presets.export", "Perspective grid presets", perspgrid::PRESET_EXTS), + ]; + for (save, params, export, label, exts) in presets { + app.run(save, params).unwrap(); + io::save_command_output(&mut app, export, exts[0], json!({"names": ["Mine"]})).unwrap(); + assert_eq!(last_shown(&shown), [(label, exts)], "{export}: the presets dialog's Export…"); + } +} + #[test] fn saves_add_the_format_extension_to_a_bare_name() { let (mut app, w) = saving_app("/tmp/Untitled-1");