Merge pull request #390 from txiki739/fix/linux-file-dialog-extensions
#262: file dialogs list extensions in any capitalisation on Linux
This commit is contained in:
commit
7f81d6b063
|
|
@ -1674,6 +1674,7 @@ dependencies = [
|
|||
"filmcraft-engine",
|
||||
"filmcraft-platform",
|
||||
"filmcraft-ui-egui",
|
||||
"glob",
|
||||
"log",
|
||||
"muda",
|
||||
"objc2 0.6.4",
|
||||
|
|
@ -2826,6 +2827,12 @@ dependencies = [
|
|||
"vello_common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "glob"
|
||||
version = "0.3.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
|
||||
|
||||
[[package]]
|
||||
name = "glow"
|
||||
version = "0.17.0"
|
||||
|
|
|
|||
|
|
@ -28,6 +28,9 @@ cpal = "0.16"
|
|||
chrono = { version = "0.4", default-features = false, features = ["clock"] }
|
||||
sys-locale = "0.3"
|
||||
|
||||
[dev-dependencies]
|
||||
glob = "0.3"
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
muda = "0.21"
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = ["std", "NSProcessInfo", "NSString", "NSArray"] }
|
||||
|
|
|
|||
|
|
@ -0,0 +1,70 @@
|
|||
//! Extension filters for native file dialogs.
|
||||
//!
|
||||
//! Every filtered file dialog (import, relink, open file/preset, open/save project) builds
|
||||
//! its filter with [`extensions`]. On Linux and the BSDs, rfd's XDG portal, GTK and Zenity
|
||||
//! backends turn each extension into a case-sensitive `*.{ext}` glob, so camera files such
|
||||
//! as `shot.MP4` or `shot.Mp4` would be hidden. The helper appends one bracket pattern per
|
||||
//! extension (`mp4` -> `[mM][pP]4`) after the literal extensions, which stay first so save
|
||||
//! dialogs keep their normal default suffix. macOS and Windows dialogs already ignore case
|
||||
//! and receive the plain list.
|
||||
|
||||
pub fn extensions(exts: &[&str]) -> Vec<String> {
|
||||
let extensions: Vec<_> = exts.iter().map(|ext| (*ext).to_owned()).collect();
|
||||
// XDG portal/Zenity globs are case-sensitive. Keep the literal extensions first so
|
||||
// Save dialogs can still infer a normal default suffix, then cover mixed case too.
|
||||
// https://flatpak.github.io/xdg-desktop-portal/docs/doc-org.freedesktop.portal.FileChooser.html
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
{
|
||||
let mut extensions = extensions;
|
||||
for ext in exts {
|
||||
if !ext.bytes().any(|b| b.is_ascii_alphabetic()) {
|
||||
continue;
|
||||
}
|
||||
let mut pattern = String::new();
|
||||
for c in ext.chars() {
|
||||
if c.is_ascii_alphabetic() {
|
||||
pattern.extend(['[', c.to_ascii_lowercase(), c.to_ascii_uppercase(), ']']);
|
||||
} else {
|
||||
pattern.push(c);
|
||||
}
|
||||
}
|
||||
extensions.push(pattern);
|
||||
}
|
||||
extensions
|
||||
}
|
||||
// Native macOS and Windows filters take ordinary extensions.
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
extensions
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::extensions;
|
||||
|
||||
// rfd's XDG portal and Zenity backends turn each extension into `*.{ext}`.
|
||||
// Exercise those case-sensitive patterns against filenames, as the picker does.
|
||||
#[cfg(not(any(target_os = "windows", target_os = "macos")))]
|
||||
#[test]
|
||||
fn camera_files_are_visible_in_every_capitalization() {
|
||||
let filters: Vec<_> = extensions(&["mp4", "mov", "m2ts", "wav", "fcproj"]).iter().map(|ext| glob::Pattern::new(&format!("*.{ext}")).unwrap()).collect();
|
||||
for name in ["camera.mp4", "camera.MP4", "camera.Mp4", "clip.MOV", "clip.moV", "take.M2TS", "sound.WaV", "Edit.FCPROJ", "Edit.FcProj"] {
|
||||
assert!(filters.iter().any(|f| f.matches(name)), "the picker hides {name}");
|
||||
}
|
||||
for name in ["camera.mp4.bak", "camera.mp44", "camera.xmp4", "notes.txt", "mp4"] {
|
||||
assert!(!filters.iter().any(|f| f.matches(name)), "the picker admits {name}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn save_filters_keep_the_literal_default_extension() {
|
||||
assert_eq!(extensions(&["mp4", "mov"]).first().map(String::as_str), Some("mp4"));
|
||||
assert!(extensions(&[]).is_empty());
|
||||
assert_eq!(extensions(&["*", "", "123"]), ["*", "", "123"]);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "windows", target_os = "macos"))]
|
||||
#[test]
|
||||
fn native_filters_receive_only_literal_extensions() {
|
||||
assert_eq!(extensions(&["mp4", "mov", "fcproj"]), ["mp4", "mov", "fcproj"]);
|
||||
}
|
||||
}
|
||||
|
|
@ -28,6 +28,7 @@ mod args;
|
|||
mod audio;
|
||||
mod audio_in;
|
||||
mod control_server;
|
||||
mod file_filters;
|
||||
#[cfg(any(target_os = "windows", test))]
|
||||
mod graphics;
|
||||
mod logging;
|
||||
|
|
@ -199,7 +200,7 @@ fn main() -> eframe::Result {
|
|||
app.audio = Some(Box::new(audio::CpalOut::new()));
|
||||
app.hooks.pick_files = Some(Box::new(|exts: &[&str]| {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(filmcraft_ui_egui::i18n::t("Media"), exts)
|
||||
.add_filter(filmcraft_ui_egui::i18n::t("Media"), &file_filters::extensions(exts))
|
||||
.pick_files()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
|
|
@ -208,18 +209,21 @@ fn main() -> eframe::Result {
|
|||
}));
|
||||
// Link Media ▸ Locate…, Attach Proxies, Reconnect Full Resolution: one path, not imported.
|
||||
app.hooks.pick_file_for_relink = Some(Box::new(|exts: &[&str], _hint| {
|
||||
rfd::FileDialog::new().add_filter(filmcraft_ui_egui::i18n::t("Media"), exts).pick_file().map(|p| p.to_string_lossy().to_string())
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(filmcraft_ui_egui::i18n::t("Media"), &file_filters::extensions(exts))
|
||||
.pick_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
}));
|
||||
app.hooks.pick_save = Some(Box::new(|name: &str| {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(filmcraft_ui_egui::i18n::t("FilmCraft Project"), &["fcproj"])
|
||||
.add_filter(filmcraft_ui_egui::i18n::t("FilmCraft Project"), &file_filters::extensions(&["fcproj"]))
|
||||
.set_file_name(name)
|
||||
.save_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
}));
|
||||
app.hooks.pick_save_as = Some(Box::new(|filter: &str, exts: &[&str], name: &str| {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(filmcraft_ui_egui::i18n::t(filter), exts)
|
||||
.add_filter(filmcraft_ui_egui::i18n::t(filter), &file_filters::extensions(exts))
|
||||
.set_file_name(name)
|
||||
.save_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
|
|
@ -238,11 +242,14 @@ fn main() -> eframe::Result {
|
|||
window_raise::raise_without_focus();
|
||||
}));
|
||||
app.hooks.pick_open_file = Some(Box::new(|filter: &str, exts: &[&str]| {
|
||||
rfd::FileDialog::new().add_filter(filmcraft_ui_egui::i18n::t(filter), exts).pick_file().map(|p| p.to_string_lossy().to_string())
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(filmcraft_ui_egui::i18n::t(filter), &file_filters::extensions(exts))
|
||||
.pick_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
}));
|
||||
app.hooks.pick_open_project = Some(Box::new(|| {
|
||||
rfd::FileDialog::new()
|
||||
.add_filter(filmcraft_ui_egui::i18n::t("FilmCraft Project"), &["fcproj"])
|
||||
.add_filter(filmcraft_ui_egui::i18n::t("FilmCraft Project"), &file_filters::extensions(&["fcproj"]))
|
||||
.pick_file()
|
||||
.map(|p| p.to_string_lossy().to_string())
|
||||
}));
|
||||
|
|
|
|||
Loading…
Reference in New Issue