diff --git a/crates/ui-egui/src/app/files.rs b/crates/ui-egui/src/app/files.rs index 3c68dc5619..c24e9d9037 100644 --- a/crates/ui-egui/src/app/files.rs +++ b/crates/ui-egui/src/app/files.rs @@ -1,4 +1,5 @@ use super::*; +use crate::file_filter::AddFilterAnyCase as _; /// Whether `a` and `b` name the same file: equal, or equal once made absolute against the /// current directory (links are not resolved, as in `last_session`). @@ -607,7 +608,9 @@ impl PdfCraftApp { /// The File ▸ Open picker: PDFs, and images and text that become PDFs. #[cfg(not(target_arch = "wasm32"))] pub(super) fn open_picker() -> rfd::AsyncFileDialog { - rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).add_filter(tl!("Images and text (converted to PDF)"), &create_ui::CONVERTIBLE) + rfd::AsyncFileDialog::new() + .add_filter_any_case("PDF", &["pdf"]) + .add_filter_any_case(tl!("Images and text (converted to PDF)"), &create_ui::CONVERTIBLE) } pub fn open_dialog(&mut self) { @@ -615,7 +618,9 @@ impl PdfCraftApp { #[cfg(not(target_arch = "wasm32"))] self.pick( pickers::PickFor::Open, - rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).add_filter(tl!("Images and text (converted to PDF)"), &create_ui::CONVERTIBLE), + rfd::AsyncFileDialog::new() + .add_filter_any_case("PDF", &["pdf"]) + .add_filter_any_case(tl!("Images and text (converted to PDF)"), &create_ui::CONVERTIBLE), true, ); // Browsers pick files asynchronously; the bytes arrive through `inbox`. @@ -625,8 +630,8 @@ impl PdfCraftApp { let ctx = self.ctx.clone(); wasm_bindgen_futures::spawn_local(async move { if let Some(h) = rfd::AsyncFileDialog::new() - .add_filter("PDF", &["pdf"]) - .add_filter(tl!("Images and text"), &create_ui::CONVERTIBLE) + .add_filter_any_case("PDF", &["pdf"]) + .add_filter_any_case(tl!("Images and text"), &create_ui::CONVERTIBLE) .pick_file() .await { diff --git a/crates/ui-egui/src/dialogs/marks_ui.rs b/crates/ui-egui/src/dialogs/marks_ui.rs index 085c581119..46dd1eb3a5 100644 --- a/crates/ui-egui/src/dialogs/marks_ui.rs +++ b/crates/ui-egui/src/dialogs/marks_ui.rs @@ -3,6 +3,8 @@ //! page the range selects, with the marks laid out on it. "Update" opens the same dialog in //! replace mode. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use std::collections::{HashMap, HashSet}; use egui::{Align, Color32, CornerRadius, Layout, Rect, Stroke, pos2, vec2}; @@ -777,7 +779,7 @@ pub(crate) fn body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens, kind: M #[cfg(not(target_arch = "wasm32"))] if browse { let dialog = rfd::AsyncFileDialog::new() - .add_filter(tl!("PDF or image"), &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]); + .add_filter_any_case(tl!("PDF or image"), &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]); let epoch = app.dialog_epoch(); app.ask_one(crate::pickers::Ask::File(dialog), None, move |app, p| { // Only into the dialog that asked: not one closed meanwhile, or opened again since. diff --git a/crates/ui-egui/src/editing/save.rs b/crates/ui-egui/src/editing/save.rs index f685247c9d..b6c04dd9e0 100644 --- a/crates/ui-egui/src/editing/save.rs +++ b/crates/ui-egui/src/editing/save.rs @@ -1,4 +1,6 @@ use super::{PdfCraftApp, StagingName, staging_suffixes}; +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; /// A save held back by [`Dialog::OverwriteChanged`](crate::Dialog::OverwriteChanged): the file /// on disk is another version than the document came from (#2166). @@ -58,7 +60,7 @@ impl PdfCraftApp { (SaveTarget::InPlace, Some(p), _) if !is_mail_temp_copy(&p) => p, _ => { let name = if name.to_ascii_lowercase().ends_with(".pdf") { name } else { format!("{name}.pdf") }; - let dialog = rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).set_file_name(name); + let dialog = rfd::AsyncFileDialog::new().add_filter_any_case("PDF", &["pdf"]).set_file_name(name); let dialog = in_document_folder(dialog, doc_path.as_deref()); // The bytes are taken once the user has chosen, so edits made meanwhile are saved too. self.ask_one(crate::pickers::Ask::Save(dialog), None, move |app, dest| { diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 222bb988e4..ef04581e2d 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -119,6 +119,7 @@ pub use persist::units; #[cfg(not(target_arch = "wasm32"))] pub use persist::window_state; use platform::drag_pointer; +use platform::file_filter; #[cfg(not(target_arch = "wasm32"))] pub use platform::file_watch; #[cfg(target_os = "macos")] diff --git a/crates/ui-egui/src/platform.rs b/crates/ui-egui/src/platform.rs index 129f622f52..47196409f9 100644 --- a/crates/ui-egui/src/platform.rs +++ b/crates/ui-egui/src/platform.rs @@ -1,4 +1,5 @@ pub(super) mod drag_pointer; +pub(super) mod file_filter; #[cfg(not(target_arch = "wasm32"))] pub mod file_watch; #[cfg(target_os = "macos")] diff --git a/crates/ui-egui/src/platform/file_filter.rs b/crates/ui-egui/src/platform/file_filter.rs new file mode 100644 index 0000000000..47e11bae72 --- /dev/null +++ b/crates/ui-egui/src/platform/file_filter.rs @@ -0,0 +1,50 @@ +//! Picker filters that list a file whatever the letter case of its extension. +//! +//! On Linux and the BSDs, rfd gives each extension to the XDG portal, GTK or Zenity as the glob +//! `*.`, and those match it case-sensitively: a "PDF" filter of `pdf` hid `Report.PDF`. +//! Windows, macOS and the browser already ignore case, so there the extension passes unchanged. + +/// Whether the platform's pickers match filter globs case-sensitively. +const CASE_SENSITIVE_GLOBS: bool = cfg!(all(unix, not(target_os = "macos"))); + +/// [`rfd::AsyncFileDialog::add_filter`], with each extension matched in any letter case. +pub(crate) trait AddFilterAnyCase { + #[must_use] + fn add_filter_any_case(self, name: impl Into, extensions: &[impl AsRef]) -> Self; +} + +impl AddFilterAnyCase for rfd::AsyncFileDialog { + fn add_filter_any_case(self, name: impl Into, extensions: &[impl AsRef]) -> Self { + let globs: Vec = extensions.iter().map(|e| any_case(e.as_ref(), CASE_SENSITIVE_GLOBS)).collect(); + self.add_filter(name, &globs) + } +} + +/// `ext` as a glob fragment matching any letter case: `pdf` becomes `[pP][dD][fF]`. +fn any_case(ext: &str, case_sensitive: bool) -> String { + if !case_sensitive { + return ext.to_owned(); + } + ext.chars() + .map(|c| if c.is_ascii_alphabetic() { format!("[{}{}]", c.to_ascii_lowercase(), c.to_ascii_uppercase()) } else { c.to_string() }) + .collect() +} + +#[cfg(test)] +mod tests { + use super::any_case; + + #[test] + fn a_case_sensitive_picker_gets_a_glob_matching_every_case() { + assert_eq!(any_case("pdf", true), "[pP][dD][fF]"); + assert_eq!(any_case("PDF", true), "[pP][dD][fF]"); + assert_eq!(any_case("jp2", true), "[jJ][pP]2"); + assert_eq!(any_case("*", true), "*"); + } + + #[test] + fn a_case_blind_picker_gets_the_extension_unchanged() { + assert_eq!(any_case("pdf", false), "pdf"); + assert_eq!(any_case("*", false), "*"); + } +} diff --git a/crates/ui-egui/src/tools/content_ui.rs b/crates/ui-egui/src/tools/content_ui.rs index e28f20e232..993c6f953b 100644 --- a/crates/ui-egui/src/tools/content_ui.rs +++ b/crates/ui-egui/src/tools/content_ui.rs @@ -3,6 +3,8 @@ //! (images keep their proportions), double-click text to retype it, format it from the panel, //! Delete to remove it. Each change is one undoable engine edit. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use egui::{Color32, CornerRadius, Pos2, Rect, Stroke}; use pdfcraft_engine::{Added, AddedContent, AddedText, Edit}; use pdfcraft_render::DocInfo; @@ -849,7 +851,7 @@ impl crate::PdfCraftApp { Some(_) => {} None => { let dialog = rfd::AsyncFileDialog::new() - .add_filter(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]) + .add_filter_any_case(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]) .set_title(title); let target = self.active_ids().map(|(_, id)| id); self.ask_one(crate::pickers::Ask::File(dialog), target, read); diff --git a/crates/ui-egui/src/tools/fill_sign.rs b/crates/ui-egui/src/tools/fill_sign.rs index 045bb64974..0555655660 100644 --- a/crates/ui-egui/src/tools/fill_sign.rs +++ b/crates/ui-egui/src/tools/fill_sign.rs @@ -3,6 +3,7 @@ //! signature. Everything is an annotation (typewriter text, PdfCraft-drawn stamps, ink), so it //! can be moved, deleted and undone like any comment. +use crate::file_filter::AddFilterAnyCase as _; use egui::{Color32, CornerRadius, Pos2, Sense, Stroke, pos2, vec2}; use pdfcraft_engine::signature_scan::{DEFAULT_SENSITIVITY, PREVIEW_MAX_SIDE, SCAN_MAX_SIDE, ScanError}; use pdfcraft_engine::{Edit, FillMark, NewAnnotation, PhotoRect, Shape, SignatureImage, SignaturePhoto, Style, Turn}; @@ -780,7 +781,7 @@ impl crate::PdfCraftApp { let epoch = self.dialog_epoch(); let initials = self.signature_draft.initials; let scan = self.signature_draft.scan_mode; - let dialog = rfd::AsyncFileDialog::new().add_filter(tl!(if scan { "Photo" } else { "Image" }), &["png", "jpg", "jpeg"]); + let dialog = rfd::AsyncFileDialog::new().add_filter_any_case(tl!(if scan { "Photo" } else { "Image" }), &["png", "jpg", "jpeg"]); #[cfg(not(target_arch = "wasm32"))] self.ask_one(crate::pickers::Ask::File(dialog), None, move |app, path| { if app.dialog_epoch() != epoch diff --git a/crates/ui-egui/src/tools/measure_ui.rs b/crates/ui-egui/src/tools/measure_ui.rs index 5008464ad9..76fa91cdf4 100644 --- a/crates/ui-egui/src/tools/measure_ui.rs +++ b/crates/ui-egui/src/tools/measure_ui.rs @@ -1,4 +1,6 @@ //! Measure objects: page-space clicks, live readings and persistent drawing scales. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use crate::{LeftPanel, PdfCraftApp, QuickTool, canvas::DocView, page_cursor::Layer, theme::Tokens, widgets}; use egui::{Color32, Pos2, Stroke}; use pdfcraft_engine::{ @@ -613,7 +615,7 @@ fn export_csv(app: &mut PdfCraftApp) { match app.export_dir_override.as_ref().map(|d| std::path::PathBuf::from(d).join("measurements.csv")) { Some(path) => write(app, path), None => { - let dialog = rfd::AsyncFileDialog::new().set_file_name("measurements.csv").add_filter("CSV", &["csv"]); + let dialog = rfd::AsyncFileDialog::new().set_file_name("measurements.csv").add_filter_any_case("CSV", &["csv"]); app.ask_one(crate::pickers::Ask::Save(dialog), None, write); } } diff --git a/crates/ui-egui/src/tools/sign_ui.rs b/crates/ui-egui/src/tools/sign_ui.rs index b00d07279a..08ea0b15a3 100644 --- a/crates/ui-egui/src/tools/sign_ui.rs +++ b/crates/ui-egui/src/tools/sign_ui.rs @@ -2,6 +2,8 @@ //! click an empty signature field), the Sign with a Digital ID / Configure New Digital ID / //! Sign as dialogs, the Signatures panel, and the signature message bar. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use std::path::PathBuf; use egui::{Align, Color32, CornerRadius, Layout, Pos2, Rect, Stroke, vec2}; @@ -501,7 +503,7 @@ impl PdfCraftApp { Some(p) => sign_and_save(self, PathBuf::from(p)), #[cfg(not(target_arch = "wasm32"))] None => { - let dialog = rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).set_file_name(format!("{stem}_signed.pdf")); + let dialog = rfd::AsyncFileDialog::new().add_filter_any_case("PDF", &["pdf"]).set_file_name(format!("{stem}_signed.pdf")); let dialog = crate::editing::in_document_folder(dialog, folder.as_deref()); // The signature's page, rectangle and field refer to the document as it is now: // the pick is dropped (with a notice) if it is edited or switched away from @@ -533,7 +535,7 @@ impl PdfCraftApp { #[cfg(not(target_arch = "wasm32"))] pub(crate) fn pick_sign_graphic_image(&mut self) { let epoch = self.dialog_epoch(); - let dialog = rfd::AsyncFileDialog::new().add_filter(tl!("Image"), &["png", "jpg", "jpeg"]); + let dialog = rfd::AsyncFileDialog::new().add_filter_any_case(tl!("Image"), &["png", "jpg", "jpeg"]); self.ask_one(crate::pickers::Ask::File(dialog), None, move |app, path| { if app.dialog_epoch() != epoch || app.dialog != Some(crate::Dialog::Sign) { return; diff --git a/crates/ui-egui/src/tools/sign_ui/dialog.rs b/crates/ui-egui/src/tools/sign_ui/dialog.rs index feb4fdd207..e627f9a9f8 100644 --- a/crates/ui-egui/src/tools/sign_ui/dialog.rs +++ b/crates/ui-egui/src/tools/sign_ui/dialog.rs @@ -2,6 +2,8 @@ use super::{ Align, Appearance, AppearanceLabels, Color32, CornerRadius, KEY_ALGORITHMS, Layout, NewIdDraft, PdfCraftApp, Rect, SavedSig, SignGraphic, SignStep, Stroke, Tokens, icons, sign, theme, vec2, widgets, }; +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; /// Draw the signing dialogs; returns `true` when the dialog should close. pub(crate) fn dialog(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { @@ -233,7 +235,7 @@ fn configure(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool { // The picker answers on a later frame, into the draft if the same dialog is still open. #[cfg(not(target_arch = "wasm32"))] if browse { - let dialog = rfd::AsyncFileDialog::new().add_filter(tl!("Digital ID"), &["p12", "pfx"]); + let dialog = rfd::AsyncFileDialog::new().add_filter_any_case(tl!("Digital ID"), &["p12", "pfx"]); let epoch = app.dialog_epoch(); app.ask_one(crate::pickers::Ask::File(dialog), None, move |app, p| { // Not one closed meanwhile, or opened again since. diff --git a/crates/ui-egui/src/tools/stamps_ui.rs b/crates/ui-egui/src/tools/stamps_ui.rs index 9cddb31d23..3db8d23ad5 100644 --- a/crates/ui-egui/src/tools/stamps_ui.rs +++ b/crates/ui-egui/src/tools/stamps_ui.rs @@ -1,6 +1,8 @@ //! Custom stamps (Add a stamp ▸ Custom stamps ▸ Create): a PDF page or an image saved in the //! user's stamp library under a category and a name, then placed like any stamp. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use std::sync::Arc; use egui::{Align, Layout}; @@ -145,7 +147,7 @@ impl PdfCraftApp { Some(_) => {} None => { let dialog = rfd::AsyncFileDialog::new() - .add_filter(tl!("PDF or image"), &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]) + .add_filter_any_case(tl!("PDF or image"), &["pdf", "png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]) .set_title(tl!("Select a file for the stamp")); self.ask_one(crate::pickers::Ask::File(dialog), None, read); } diff --git a/crates/ui-egui/src/workflows/actions_ui.rs b/crates/ui-egui/src/workflows/actions_ui.rs index 5ef299102f..313d55c5f2 100644 --- a/crates/ui-egui/src/workflows/actions_ui.rs +++ b/crates/ui-egui/src/workflows/actions_ui.rs @@ -1,6 +1,8 @@ //! Action Wizard: the actions (PdfCraft's and your own), running one over files in the //! background, and creating or editing an action's steps. Your actions are remembered. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use std::sync::{Arc, Mutex}; use egui::{Align, Layout}; @@ -186,7 +188,7 @@ impl PdfCraftApp { match self.action_files_override.clone() { Some(p) => run(self, p.into_iter().map(std::path::PathBuf::from).collect()), None => { - let dialog = rfd::AsyncFileDialog::new().set_title(title).add_filter("PDF", &["pdf"]); + let dialog = rfd::AsyncFileDialog::new().set_title(title).add_filter_any_case("PDF", &["pdf"]); self.ask(crate::pickers::Ask::Files(dialog), None, run); } } diff --git a/crates/ui-egui/src/workflows/create_ui.rs b/crates/ui-egui/src/workflows/create_ui.rs index 6dceb50657..177ccf6140 100644 --- a/crates/ui-egui/src/workflows/create_ui.rs +++ b/crates/ui-egui/src/workflows/create_ui.rs @@ -1,6 +1,8 @@ //! Create a PDF (blank, from images, from text) and Reduce File Size (execution plan M10.2, //! M11.1). Opening an image or a text file converts it to a new, unsaved PDF, as Acrobat does. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use std::sync::Arc; use crate::PdfCraftApp; @@ -154,7 +156,7 @@ impl PdfCraftApp { #[cfg(not(target_arch = "wasm32"))] { let dialog = rfd::AsyncFileDialog::new() - .add_filter(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]) + .add_filter_any_case(tl!("Images"), &["png", "jpg", "jpeg", "tif", "tiff", "gif", "bmp", "jp2", "j2k", "jpx"]) .set_title(tl!("Choose images")); self.ask(crate::pickers::Ask::Files(dialog), None, |app, files| { let mut images = Vec::new(); @@ -285,7 +287,7 @@ impl PdfCraftApp { match self.save_override.clone() { Some(p) => write(self, p), None => { - let dialog = rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).set_file_name(&name); + let dialog = rfd::AsyncFileDialog::new().add_filter_any_case("PDF", &["pdf"]).set_file_name(&name); let dialog = crate::editing::in_document_folder(dialog, folder.as_deref()); self.ask_one(crate::pickers::Ask::Save(dialog), None, move |app, p| write(app, p.to_string_lossy().into_owned())); } diff --git a/crates/ui-egui/src/workflows/files.rs b/crates/ui-egui/src/workflows/files.rs index a9313a4714..b33dd32cd2 100644 --- a/crates/ui-egui/src/workflows/files.rs +++ b/crates/ui-egui/src/workflows/files.rs @@ -3,6 +3,7 @@ //! Files are picked asynchronously and used on a later frame: desktop builds through `pickers` //! (a blocking picker crashes the app on macOS), browsers through `requests`. +use crate::file_filter::AddFilterAnyCase as _; use std::sync::Arc; use pdfcraft_engine::{DocId, Edit, SplitBy}; @@ -61,16 +62,16 @@ pub(crate) fn installed_font_files(ctx: &egui::Context) -> Option rfd::AsyncFileDialog { let dialog = rfd::AsyncFileDialog::new(); if matches!(purpose, FilePurpose::TextFont { .. }) { - return dialog.add_filter("TrueType / OpenType font", &["ttf", "otf"]); + return dialog.add_filter_any_case("TrueType / OpenType font", &["ttf", "otf"]); } if purpose == FilePurpose::RedactWords { - return dialog.add_filter(tl!("Text"), &["txt"]); + return dialog.add_filter_any_case(tl!("Text"), &["txt"]); } if !matches!(purpose, FilePurpose::CreateMultiple | FilePurpose::CreateMultipleSeparate | FilePurpose::InsertPages | FilePurpose::Combine) { - return dialog.add_filter("PDF", &["pdf"]); + return dialog.add_filter_any_case("PDF", &["pdf"]); } let all: Vec<&str> = std::iter::once("pdf").chain(pdfcraft_engine::CONVERTIBLE).collect(); - dialog.add_filter(tl!("PDF, images and text"), &all).add_filter("PDF", &["pdf"]) + dialog.add_filter_any_case(tl!("PDF, images and text"), &all).add_filter_any_case("PDF", &["pdf"]) } /// The Replace Pages dialog: the chosen file and the ranges (1-based, inclusive). @@ -898,7 +899,7 @@ impl PdfCraftApp { Some(_) => {} None => { let dialog = rfd::AsyncFileDialog::new() - .add_filter(tl!("Comment and form data").to_string(), &extensions) + .add_filter_any_case(tl!("Comment and form data").to_string(), &extensions) .set_title(tl!("Import data").to_string()); let target = self.active_ids().map(|(_, id)| id); self.ask_one(crate::pickers::Ask::File(dialog), target, import); @@ -939,7 +940,7 @@ impl PdfCraftApp { None => { let dialog = rfd::AsyncFileDialog::new() .set_title(tl!("Export").to_string()) - .add_filter(ext.to_uppercase(), &[ext]) + .add_filter_any_case(ext.to_uppercase(), &[ext]) .set_file_name(format!("{stem}.{ext}")); let dialog = crate::editing::in_document_folder(dialog, folder.as_deref()); self.ask_one(crate::pickers::Ask::Save(dialog), None, write); @@ -959,7 +960,7 @@ impl PdfCraftApp { { let dialog = rfd::AsyncFileDialog::new() .set_title(tl!("Select data files to merge").to_string()) - .add_filter(tl!("Form data and PDF forms").to_string(), &["fdf", "xfdf", "pdf"]); + .add_filter_any_case(tl!("Form data and PDF forms").to_string(), &["fdf", "xfdf", "pdf"]); self.ask(crate::pickers::Ask::Files(dialog), None, |app, paths| { let mut files = Vec::new(); for p in paths { @@ -1004,7 +1005,7 @@ impl PdfCraftApp { None => { let dialog = rfd::AsyncFileDialog::new() .set_title(tl!("Save the spreadsheet").to_string()) - .add_filter("CSV", &["csv"]) + .add_filter_any_case("CSV", &["csv"]) .set_file_name("report.csv"); self.ask_one(crate::pickers::Ask::Save(dialog), None, write); } @@ -1047,22 +1048,22 @@ impl PdfCraftApp { let title = if comments { tl!("Export comments") } else { tl!("Export form data") }; let d = rfd::AsyncFileDialog::new().set_title(title).set_file_name(format!("{stem}.xfdf")); let d = if comments { - d.add_filter("XFDF", &["xfdf"]).add_filter("FDF", &["fdf"]) + d.add_filter_any_case("XFDF", &["xfdf"]).add_filter_any_case("FDF", &["fdf"]) } else if xfa { // An XFA form's data, as Acrobat exports it, first. d.set_file_name(format!("{stem}.xml")) - .add_filter("XML", &["xml"]) - .add_filter("XDP", &["xdp"]) - .add_filter("XFDF", &["xfdf"]) - .add_filter("FDF", &["fdf"]) - .add_filter("CSV", &["csv"]) - .add_filter(tl!("Text").to_string(), &["txt"]) + .add_filter_any_case("XML", &["xml"]) + .add_filter_any_case("XDP", &["xdp"]) + .add_filter_any_case("XFDF", &["xfdf"]) + .add_filter_any_case("FDF", &["fdf"]) + .add_filter_any_case("CSV", &["csv"]) + .add_filter_any_case(tl!("Text").to_string(), &["txt"]) } else { - d.add_filter("XFDF", &["xfdf"]) - .add_filter("FDF", &["fdf"]) - .add_filter("XML", &["xml"]) - .add_filter("CSV", &["csv"]) - .add_filter(tl!("Text").to_string(), &["txt"]) + d.add_filter_any_case("XFDF", &["xfdf"]) + .add_filter_any_case("FDF", &["fdf"]) + .add_filter_any_case("XML", &["xml"]) + .add_filter_any_case("CSV", &["csv"]) + .add_filter_any_case(tl!("Text").to_string(), &["txt"]) }; let d = crate::editing::in_document_folder(d, folder.as_deref()); self.ask_one(crate::pickers::Ask::Save(d), None, write); diff --git a/crates/ui-egui/src/workflows/print_ui.rs b/crates/ui-egui/src/workflows/print_ui.rs index 9145256297..7ea7a18602 100644 --- a/crates/ui-egui/src/workflows/print_ui.rs +++ b/crates/ui-egui/src/workflows/print_ui.rs @@ -3,6 +3,8 @@ //! (Size, Poster, Multiple, Booklet); orientation; comments & forms; and a live preview of the //! sheets. Printing sends the print-ready PDF to the system spooler; "Save as PDF" writes it. +#[cfg(not(target_arch = "wasm32"))] +use crate::file_filter::AddFilterAnyCase as _; use egui::{Color32, Pos2, Rect, Stroke, pos2, vec2}; use pdfcraft_engine::print::{self, Binding, BookletSubset, Content, Layout, Orientation, PAPERS, PageOrder, SizeMode, Subset, spool}; @@ -551,7 +553,7 @@ impl PdfCraftApp { Some(p) => write(self, p.into()), None => { let stem = name.trim_end_matches(".pdf").trim_end_matches(".PDF"); - let dialog = rfd::AsyncFileDialog::new().add_filter("PDF", &["pdf"]).set_file_name(format!("{stem} (print).pdf")); + let dialog = rfd::AsyncFileDialog::new().add_filter_any_case("PDF", &["pdf"]).set_file_name(format!("{stem} (print).pdf")); self.ask_one(crate::pickers::Ask::Save(dialog), None, move |app, path| { write(app, path); })