diff --git a/crates/ui-egui/src/io.rs b/crates/ui-egui/src/io.rs index 4c02b48fa..972b214ad 100644 --- a/crates/ui-egui/src/io.rs +++ b/crates/ui-egui/src/io.rs @@ -410,16 +410,17 @@ fn with_save_extension(path: &str, f: &Format, mode: SaveMode) -> String { /// [`with_save_extension`] for a save panel that reports the file type picked in it (`picked`, /// that type's extension; the XDG portal's panel on Linux and the BSDs), which then takes `f`'s -/// place. A path that still ends in the extension of the name the panel suggested (`suggested`) -/// gets the picked type's extension instead. Without a picked type, or with one the panel did not +/// place. A path that still ends in the extension of the name the panel suggested (`suggested`), +/// in any letter case, gets the picked type's extension instead, unless that extension is one of +/// the picked type's; such a path stays as typed. Without a picked type, or with one the panel did not /// offer, [`with_save_extension`] with `f`. fn with_picked_type(path: &str, suggested: &str, picked: Option<&str>, f: &Format, mode: SaveMode) -> String { let formats = if mode == SaveMode::Template { TEMPLATE_FORMATS } else { SAVE_FORMATS }; let Some(g) = picked.and_then(fileio::format).filter(|g| formats.contains(&g.id)) else { return with_save_extension(path, f, mode); }; - let has = std::path::Path::new(path).extension(); - if has.is_some() && has == std::path::Path::new(suggested).extension() { + let has = fileio::extension(path); + if !has.is_empty() && has == fileio::extension(suggested) && !g.extensions.contains(&has.as_str()) { return std::path::Path::new(path).with_extension(g.extensions[0]).to_string_lossy().into_owned(); } with_save_extension(path, g, mode) diff --git a/crates/ui-egui/src/tests_saveext.rs b/crates/ui-egui/src/tests_saveext.rs index 5272af5cc..01e9a1d9f 100644 --- a/crates/ui-egui/src/tests_saveext.rs +++ b/crates/ui-egui/src/tests_saveext.rs @@ -161,6 +161,25 @@ fn the_file_type_picked_in_the_save_dialog_decides_the_format() { } } +/// The suggested name's extension typed in another case still gets the picked type's +/// (`Untitled-1.VECTORCRAFT` with .ai picked is saved as `Untitled-1.ai`), and the picked type's +/// own extension typed in another case stays as typed (`a.AI` with .ai picked). +#[test] +fn the_suggested_names_extension_in_another_case_gets_the_picked_types() { + for (cmd, answer, expected) in [ + ("file.save", &["/art/Untitled-1.VECTORCRAFT", "ai"][..], "/art/Untitled-1.ai"), + ("file.save", &["/art/Untitled-1.VectorCraft", "svg"], "/art/Untitled-1.svg"), + ("file.saveAsTemplate", &["/art/Untitled-1.VCTemplate", "ait"], "/art/Untitled-1.ait"), + ] { + let (mut app, shown, written) = portal_app(); + assert_eq!(saved_from(&mut app, &shown, &written, cmd, answer), expected, "{cmd} {answer:?}"); + } + // A document saved as .ai suggests `a.ai`. + let (mut app, shown, written) = portal_app(); + app.run("file.saveAs", json!({"path": "/art/a.ai", "options": {}})).unwrap(); + assert_eq!(saved_from(&mut app, &shown, &written, "file.saveAs", &["/art/a.AI", "ai"]), "/art/a.AI"); +} + /// A name typed with the extension of a format the save does not write has it replaced, with or /// without a picked type: `card.ait` and `card.png` are saved as `card.vectorcraft`, not /// `card.ait.vectorcraft`. An extension that names no format stays part of the name. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 81b8cc2af..ea3002742 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -783,7 +783,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur A name typed in the desktop save panel without the file type's extension gets it, and when a file of that name exists, which the panel did not ask about, Save, Save As, Save a Copy, Save as Template, the exports and every other write through a save panel ask before replacing it (dialog `replaceFile`; Cancel writes nothing). A name returned by the desktop save panel with the extension of a format the save does not write has it replaced with the saved format's: Save As of a new document saves `card.ait` and `card.png` as `card.vectorcraft`, and Save as Template saves `card.pdf` as `card.vctemplate`. An extension that names no format stays part of the name (`notes.v2` is saved as `notes.v2.vectorcraft`). - macOS: the save sheet of Save As, Save a Copy, Save as Template and the first Save of a new document has a Format menu that lists each format the save can write, with the suggested file's format selected. Choosing a format changes the extension in the name field, and a name typed without an extension gets the chosen format's. A name typed with another listed extension is saved in that format. The menu is set up before the sheet opens; a save panel with one file type is shown by rfd as before (#866, `apps/vectorcraft/src/mac_save_format.rs`). - - Linux and the BSDs: the save dialog of Save As, Save a Copy, Save as Template and the first Save of a new document is asked of the desktop portal, starting on the suggested file's type, and the file type picked in it decides the format. A name typed without an extension, or with the suggested file's, gets the picked type's extension; a name typed with another save format's extension (for Save as Template, a template format's) is saved in that format, another format's extension is replaced with the picked type's, and any other extension gets the picked type's added. rfd shows the dialog when the portal can't (#2679, `apps/vectorcraft/src/portal_save.rs`). + - Linux and the BSDs: the save dialog of Save As, Save a Copy, Save as Template and the first Save of a new document is asked of the desktop portal, starting on the suggested file's type, and the file type picked in it decides the format. A name typed without an extension, or with the suggested file's in any letter case, gets the picked type's extension; a name typed with another save format's extension (for Save as Template, a template format's) is saved in that format, another format's extension is replaced with the picked type's, and any other extension gets the picked type's added. rfd shows the dialog when the portal can't (#2679, `apps/vectorcraft/src/portal_save.rs`). - Save as Template writes a PDF-compatible `.ait` as well as a `.vctemplate`, which stays the default: the save panel and the web build's Save as Template dialog list both, and New from Template opens the `.ait` as an untitled document with its layers and art (`file.saveAsTemplate {format: "ait"}`, or a path ending in `.ait`). Save as Template always writes a file named with its template type's extension: `file.saveAsTemplate` saves a path ending in another format's extension (`card.png`) as `card.vctemplate` (or `card.ait`), the desktop save panel does the same with a Save As format's extension typed in it (`card.pdf` is saved as `card.vctemplate`), and `file.saveAs` and `document.save` refuse `ait` with an error that names `file.saveAsTemplate`. A `.ait` is PDF-compatible only: `aiVersion: "7"` is an error for it, and the web build's dialog shows no Version row for it. Exports refuse a `.ait` with an error that names `file.saveAsTemplate`, and `vectorcraft-cli convert in.pdf out.ait` and `run --export out.ait` write the template as `file.saveAsTemplate` does.