mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-11 02:19:56 -06:00
Merge pull request #5644 from dotCooCoo/fix/linux-dialog-filters-any-case
File dialogs: Open and Place list files with capitalized extensions on Linux
This commit is contained in:
@@ -44,19 +44,50 @@ macro_rules! dialog_settings {
|
||||
|
||||
dialog_settings!(rfd::FileDialog, rfd::AsyncFileDialog);
|
||||
|
||||
/// On Linux and the BSDs the dialog is GTK's or the desktop portal's, which match a file type's
|
||||
/// `*.ext` patterns case-sensitively.
|
||||
const CASE_SENSITIVE_FILTERS: bool =
|
||||
cfg!(any(target_os = "linux", target_os = "freebsd", target_os = "dragonfly", target_os = "netbsd", target_os = "openbsd"));
|
||||
|
||||
/// The extensions an open dialog's filter is given for `exts`: with `any_case`, each extension
|
||||
/// that has a letter is followed by a pattern with each letter as a `[xX]` pair, so the filter for
|
||||
/// `dib` also lists `image.DIB` and `image.Dib`.
|
||||
fn open_extensions(exts: &[impl AsRef<str>], any_case: bool) -> Vec<String> {
|
||||
let mut out = Vec::with_capacity(exts.len() * 2);
|
||||
for e in exts {
|
||||
let e = e.as_ref();
|
||||
out.push(e.to_string());
|
||||
if any_case && e.chars().any(|c| c.is_ascii_alphabetic()) {
|
||||
out.push(
|
||||
e.chars()
|
||||
.map(|c| if c.is_ascii_alphabetic() { format!("[{}{}]", c.to_ascii_lowercase(), c.to_ascii_uppercase()) } else { c.to_string() })
|
||||
.collect(),
|
||||
);
|
||||
}
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// `d` set up for `request`: its title, file types, folder and suggested name.
|
||||
fn dialog_for<D: DialogSettings>(d: D, request: &PickRequest) -> D {
|
||||
let pick = match request {
|
||||
PickRequest::Open(pick) | PickRequest::Save(pick) => pick,
|
||||
PickRequest::OpenMany => {
|
||||
return fileio::place_filters().fold(d.title(vectorcraft_ui_egui::i18n::t("Place")), |d, (name, exts)| {
|
||||
d.filter(vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name), exts)
|
||||
d.filter(
|
||||
vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name),
|
||||
&open_extensions(exts, CASE_SENSITIVE_FILTERS),
|
||||
)
|
||||
});
|
||||
}
|
||||
PickRequest::Folder => return d,
|
||||
};
|
||||
let any_case = CASE_SENSITIVE_FILTERS && matches!(request, PickRequest::Open(_));
|
||||
let d = pick.filters.iter().fold(d, |d, (name, exts)| {
|
||||
d.filter(vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name.as_str()), exts)
|
||||
d.filter(
|
||||
vectorcraft_ui_egui::i18n::tr_ctx(vectorcraft_ui_egui::i18n::current(), "file-filter", name.as_str()),
|
||||
&open_extensions(exts, any_case),
|
||||
)
|
||||
});
|
||||
let d = match &pick.folder {
|
||||
Some(folder) => d.folder(folder),
|
||||
@@ -663,3 +694,84 @@ mod open_with_tests {
|
||||
assert_eq!(super::registry_default_path(windows::Win32::System::Registry::HKEY_CURRENT_USER, &key), None, "deleted");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod dialog_tests {
|
||||
use super::{CASE_SENSITIVE_FILTERS, DialogSettings, FilePick, PickRequest, dialog_for, fileio, open_extensions};
|
||||
|
||||
/// The file types a dialog was given, by label.
|
||||
#[derive(Default)]
|
||||
struct Filters(Vec<(String, Vec<String>)>);
|
||||
|
||||
impl DialogSettings for Filters {
|
||||
fn parent(self, _window: &winit::window::Window) -> Self {
|
||||
self
|
||||
}
|
||||
fn title(self, _title: &str) -> Self {
|
||||
self
|
||||
}
|
||||
fn filter(mut self, name: &str, exts: &[impl ToString]) -> Self {
|
||||
self.0.push((name.to_string(), exts.iter().map(ToString::to_string).collect()));
|
||||
self
|
||||
}
|
||||
fn folder(self, _folder: &str) -> Self {
|
||||
self
|
||||
}
|
||||
fn file_name(self, _name: &str) -> Self {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// Does the case-sensitive pattern `*.{ext}` match `name`? `ext` may hold literal characters,
|
||||
/// `*` and `[..]` sets.
|
||||
fn matches(ext: &str, name: &str) -> bool {
|
||||
fn here(p: &[char], n: &[char]) -> bool {
|
||||
match p.split_first() {
|
||||
None => n.is_empty(),
|
||||
Some(('*', rest)) => (0..=n.len()).any(|i| here(rest, &n[i..])),
|
||||
Some(('[', rest)) => {
|
||||
let Some(end) = rest.iter().position(|&c| c == ']') else { return false };
|
||||
n.split_first().is_some_and(|(c, n)| rest[..end].contains(c) && here(&rest[end + 1..], n))
|
||||
}
|
||||
Some((c, rest)) => n.split_first().is_some_and(|(d, n)| d == c && here(rest, n)),
|
||||
}
|
||||
}
|
||||
let pattern: Vec<char> = format!("*.{ext}").chars().collect();
|
||||
here(&pattern, &name.chars().collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
/// Where filters match case-sensitively, every extension of File › Open and Place lists files
|
||||
/// whose extension is in capitals or mixed case, `image.DIB` and `image.Pns` included.
|
||||
#[test]
|
||||
fn open_and_place_filters_list_extensions_in_any_case() {
|
||||
let all = fileio::open_filters().chain(fileio::place_filters());
|
||||
for (label, exts) in all {
|
||||
let patterns = open_extensions(exts, true);
|
||||
for ext in exts {
|
||||
let mut mixed = ext.to_ascii_uppercase();
|
||||
mixed.replace_range(..1, &ext[..1]);
|
||||
for name in [format!("a.{ext}"), format!("a.{}", ext.to_ascii_uppercase()), format!("a.{mixed}")] {
|
||||
assert!(patterns.iter().any(|p| matches(p, &name)), "{label}: {patterns:?} don't match {name}");
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(open_extensions(&["dib", "jp2", "*"], true), ["dib", "[dD][iI][bB]", "jp2", "[jJ][pP]2", "*"]);
|
||||
assert_eq!(open_extensions(&["dib", "PNS"], false), ["dib", "PNS"]);
|
||||
}
|
||||
|
||||
/// Open and Place dialogs get the any-case patterns on Linux and the BSDs; a save dialog's file
|
||||
/// types stay as listed.
|
||||
#[test]
|
||||
fn open_dialogs_get_any_case_patterns_where_filters_are_case_sensitive() {
|
||||
let dib = vec![FilePick::filter("BMP", &["bmp", "dib"])];
|
||||
let pick = FilePick { filters: dib.clone(), ..Default::default() };
|
||||
let expected = |any_case: bool| open_extensions(&["bmp", "dib"], any_case);
|
||||
let opened = dialog_for(Filters::default(), &PickRequest::Open(pick.clone())).0;
|
||||
assert_eq!(opened[0].1, expected(CASE_SENSITIVE_FILTERS));
|
||||
let saved = dialog_for(Filters::default(), &PickRequest::Save(pick)).0;
|
||||
assert_eq!(saved, dib);
|
||||
let placed = dialog_for(Filters::default(), &PickRequest::OpenMany).0;
|
||||
let first = fileio::place_filters().next().map(|(_, exts)| open_extensions(exts, CASE_SENSITIVE_FILTERS));
|
||||
assert_eq!(placed.first().map(|(_, exts)| exts.clone()), first);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user