Merge pull request #2633 from dotCooCoo/fix/save-panels-offer-their-file-type

Save: save panels for files other than documents offer the file type they write
This commit is contained in:
BFlat
2026-10-17 17:16:01 +07:00
committed by GitHub
3 changed files with 104 additions and 7 deletions
+35 -6
View File
@@ -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<String>, name: &str, bytes: &[u8]) -> Result<String, String> {
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<String>) {
(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<String>, ext: &str) -> Result<String, String> {
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<Strin
}
/// Run an engine command that returns `{dataBase64}` and write the bytes to a picked path
/// (Export Selection, Save as Template).
/// (Export Selection, Save for Office Documents, Text Export Options and the presets dialogs'
/// Export…).
pub fn save_command_output(app: &mut VectorcraftApp, id: &str, ext: &str, params: Value) -> Result<String, String> {
let (path, _) = run_to_file(app, id, ext, params)?;
app.status(format!("Saved {path}"));
+2 -1
View File
@@ -476,7 +476,8 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Resu
"shortcuts.export" => {
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})),
+67
View File
@@ -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<RefCell<Vec<Vec<(&'static str, &'static [&'static str])>>>>;
/// 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");