Merge pull request #3177 from lehni/refactor/file-pick-filters

UI: FilePick filters own their text, so a dialog's file types can be made at run time
This commit is contained in:
BFlat
2026-10-21 17:26:09 +07:00
committed by GitHub
27 changed files with 115 additions and 78 deletions
+25 -24
View File
@@ -26,9 +26,7 @@ use objc2_app_kit::{NSModalResponse, NSModalResponseOK, NSPopUpButton, NSSavePan
use objc2_foundation::{NSArray, NSObjectProtocol, NSPoint, NSRect, NSSize, NSString, NSURL};
use raw_window_handle::{HasWindowHandle, RawWindowHandle};
use vectorcraft_engine::cmd::fileio;
use vectorcraft_ui_egui::{FilePick, i18n};
type Filter = (&'static str, &'static [&'static str]);
use vectorcraft_ui_egui::{FileFilter as Filter, FilePick, i18n};
/// The accessory's height and the room around and between its label and menu, in points.
const HEIGHT: f64 = 38.0;
@@ -70,7 +68,8 @@ define_class!(
/// The file types a Format menu lists: `filters` without "every file" ones. Empty when fewer than
/// two remain, and the panel is then left to rfd.
fn menu_formats(filters: &[Filter]) -> Vec<Filter> {
let named: Vec<Filter> = filters.iter().copied().filter(|(_, extensions)| !extensions.is_empty() && !extensions.contains(&"*")).collect();
let named: Vec<Filter> =
filters.iter().filter(|(_, extensions)| !extensions.is_empty() && !extensions.iter().any(|x| x == "*")).cloned().collect();
if named.len() < 2 { Vec::new() } else { named }
}
@@ -95,10 +94,10 @@ fn filename(name: &str, extension: &str, filters: &[Filter]) -> String {
/// The extensions a panel with menu `filters` allows, those of entry `selected` first. AppKit adds
/// the first one to a name typed without an extension, and a name typed with another of them is
/// saved in that format.
fn allowed_types(filters: &[Filter], selected: usize) -> Vec<&'static str> {
let first = filters.get(selected).map_or(&[][..], |(_, extensions)| *extensions);
fn allowed_types(filters: &[Filter], selected: usize) -> Vec<&str> {
let first = filters.get(selected).map_or(&[][..], |(_, extensions)| extensions.as_slice());
let others = filters.iter().enumerate().filter(|(i, _)| *i != selected).flat_map(|(_, (_, extensions))| extensions.iter());
first.iter().chain(others).copied().collect()
first.iter().chain(others).map(String::as_str).collect()
}
/// Set `panel`'s allowed file types ([`allowed_types`]).
@@ -184,26 +183,28 @@ fn accessory(mtm: MainThreadMarker, panel: &NSSavePanel, filters: Vec<Filter>, s
mod tests {
use super::*;
const FILTERS: &[Filter] = &[("VectorCraft", &["vectorcraft"]), ("SVG", &["svg"]), ("PDF", &["pdf"])];
fn filters() -> Vec<Filter> {
FilePick::filters([("VectorCraft", &["vectorcraft"][..]), ("SVG", &["svg"]), ("PDF", &["pdf"])])
}
/// Save, Save As and Save a Copy list every save format with the default first; a panel with one
/// named type, or one for every file, gets no menu, and rfd shows it.
#[test]
fn save_panels_with_several_formats_get_the_menu() {
let save = menu_formats(&fileio::save_filters("vectorcraft"));
let save = menu_formats(&FilePick::filters(fileio::save_filters("vectorcraft")));
assert!(save.len() >= 2, "{save:?}");
assert_eq!(save.first().map(|(_, e)| *e), Some(&["vectorcraft"][..]));
assert!(menu_formats(&fileio::save_filters("svg")).first().is_some_and(|(_, e)| e.contains(&"svg")));
assert!(menu_formats(&fileio::template_filters("template")).len() >= 2);
assert!(menu_formats(&[("PDF", &["pdf"])]).is_empty());
assert!(menu_formats(&[("PDF", &["pdf"]), ("All files", &["*"]), ("Any", &[])]).is_empty());
assert_eq!(menu_formats(&[("PDF", &["pdf"]), ("All files", &["*"]), ("SVG", &["svg"])]).len(), 2);
assert_eq!(save.first().map(|(_, e)| e.as_slice()), Some(&["vectorcraft".to_owned()][..]));
assert!(menu_formats(&FilePick::filters(fileio::save_filters("svg"))).first().is_some_and(|(_, e)| e.iter().any(|e| e == "svg")));
assert!(menu_formats(&FilePick::filters(fileio::template_filters("template"))).len() >= 2);
assert!(menu_formats(&FilePick::filters([("PDF", &["pdf"][..])])).is_empty());
assert!(menu_formats(&FilePick::filters([("PDF", &["pdf"][..]), ("All files", &["*"]), ("Any", &[])])).is_empty());
assert_eq!(menu_formats(&FilePick::filters([("PDF", &["pdf"][..]), ("All files", &["*"]), ("SVG", &["svg"])])).len(), 2);
}
#[test]
fn the_suggested_name_selects_its_format_and_an_unknown_one_the_default() {
for (name, index) in [("Untitled-1", 0), ("a b.SVG", 1), ("art.pdf", 2), ("version.2", 0), ("", 0)] {
assert_eq!(initial_format(name, FILTERS), index, "{name}");
assert_eq!(initial_format(name, &filters()), index, "{name}");
}
}
@@ -219,7 +220,7 @@ mod tests {
("photo.png", "photo.svg"),
("", ""),
] {
assert_eq!(filename(name, "svg", FILTERS), expected, "{name}");
assert_eq!(filename(name, "svg", &filters()), expected, "{name}");
}
}
@@ -227,19 +228,19 @@ mod tests {
/// type. The others stay allowed.
#[test]
fn the_chosen_format_is_the_first_allowed_type() {
assert_eq!(allowed_types(FILTERS, 0), ["vectorcraft", "svg", "pdf"]);
assert_eq!(allowed_types(FILTERS, 1), ["svg", "vectorcraft", "pdf"]);
assert_eq!(allowed_types(FILTERS, 2), ["pdf", "vectorcraft", "svg"]);
assert_eq!(allowed_types(FILTERS, 9), ["vectorcraft", "svg", "pdf"]);
assert_eq!(allowed_types(&filters(), 0), ["vectorcraft", "svg", "pdf"]);
assert_eq!(allowed_types(&filters(), 1), ["svg", "vectorcraft", "pdf"]);
assert_eq!(allowed_types(&filters(), 2), ["pdf", "vectorcraft", "svg"]);
assert_eq!(allowed_types(&filters(), 9), ["vectorcraft", "svg", "pdf"]);
}
/// Each entry of the Save As menu gives a name the save command reads as that entry's format.
#[test]
fn each_menu_entry_names_a_file_the_save_command_writes_in_that_format() {
let filters = menu_formats(&fileio::save_filters("vectorcraft"));
let filters = menu_formats(&FilePick::filters(fileio::save_filters("vectorcraft")));
for (label, extensions) in &filters {
let name = filename("Untitled-1.vectorcraft", extensions[0], &filters);
assert_eq!(fileio::format_for_name(&name).map(|f| f.label), Some(*label), "{name}");
let name = filename("Untitled-1.vectorcraft", &extensions[0], &filters);
assert_eq!(fileio::format_for_name(&name).map(|f| f.label), Some(label.as_str()), "{name}");
}
}
+5 -6
View File
@@ -293,7 +293,7 @@ type Parent = Option<std::sync::Arc<winit::window::Window>>;
trait DialogSettings: Sized {
fn parent(self, window: &winit::window::Window) -> Self;
fn title(self, title: &str) -> Self;
fn filter(self, name: &str, exts: &[&str]) -> Self;
fn filter(self, name: &str, exts: &[impl ToString]) -> Self;
fn folder(self, folder: &str) -> Self;
fn file_name(self, name: &str) -> Self;
}
@@ -307,7 +307,7 @@ macro_rules! dialog_settings {
fn title(self, title: &str) -> Self {
self.set_title(title)
}
fn filter(self, name: &str, exts: &[&str]) -> Self {
fn filter(self, name: &str, exts: &[impl ToString]) -> Self {
self.add_filter(name, exts)
}
fn folder(self, folder: &str) -> Self {
@@ -333,10 +333,9 @@ fn dialog_for<D: DialogSettings>(d: D, request: &PickRequest) -> D {
}
PickRequest::Folder => return d,
};
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), exts));
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)
});
let d = match &pick.folder {
Some(folder) => d.folder(folder),
None => d,
+5 -1
View File
@@ -326,7 +326,11 @@ mod tests {
let pick = FilePick {
name: "Untitled-1.vectorcraft".into(),
folder: Some("/home/me/Art".into()),
filters: vec![("VectorCraft", &["vectorcraft"]), ("SVG", &["svg"]), ("PDF-compatible .ai", &["ai"])],
filters: vec![
FilePick::filter("VectorCraft", &["vectorcraft"]),
FilePick::filter("SVG", &["svg"]),
FilePick::filter("PDF-compatible .ai", &["ai"]),
],
};
SaveRequest::new(&pick, "x11:3a0000f".into())
}
+1 -1
View File
@@ -263,7 +263,7 @@ fn glass_textures(loaded: bool) -> form::Choices {
/// it and picked in the Texture menu.
fn load_texture(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<(), String> {
use vectorcraft_engine::cmd::fileio;
let filters = fileio::FORMATS.iter().filter(|f| f.read && f.raster).map(|f| (f.label, f.extensions)).collect();
let filters = crate::FilePick::filters(fileio::FORMATS.iter().filter(|f| f.read && f.raster).map(|f| (f.label, f.extensions)));
let path = crate::picks::open(app, &crate::FilePick { filters, ..Default::default() }).ok_or("cancelled")?;
// The button is off where files can't be read.
let Some(read) = app.services.read.as_ref() else { return Ok(()) };
@@ -189,7 +189,7 @@ fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action, current: &str) ->
f();
return Ok(());
}
let pick = crate::FilePick { filters: vec![("Flattener presets", PRESET_EXTS)], ..Default::default() };
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("Flattener presets", PRESET_EXTS)], ..Default::default() };
let path = crate::picks::open(app, &pick).ok_or("cancelled")?;
let r = app.run("flattener.presets.import", json!({ "path": path }))?;
if let Some(first) = r["imported"].get(0).and_then(Value::as_str) {
+1 -1
View File
@@ -327,7 +327,7 @@ fn revert(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<(), String> {
/// Import Data: a tab-separated text file's rows into the sheet.
fn import(app: &mut VectorcraftApp, d: &mut Dialog) -> Result<(), String> {
let pick = crate::FilePick { filters: vec![("Text", &["txt", "tsv", "csv"])], ..Default::default() };
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("Text", &["txt", "tsv", "csv"])], ..Default::default() };
let path = crate::picks::open(app, &pick).ok_or("cancelled")?;
// The button is off where files can't be read.
let Some(read) = app.services.read.as_ref() else { return Ok(()) };
@@ -43,7 +43,7 @@ const JS_EXTS: &[&str] = &["js"];
/// The open dialog for a JavaScript file.
fn pick() -> FilePick {
FilePick { filters: vec![("JavaScript", JS_EXTS)], ..Default::default() }
FilePick { filters: vec![FilePick::filter("JavaScript", JS_EXTS)], ..Default::default() }
}
/// Choose…: the file picked in the open dialog into File or URL, as a `file:///` URL
+2 -1
View File
@@ -172,7 +172,8 @@ fn confirm(app: &mut VectorcraftApp, d: &Dialog) -> Result<Value, String> {
let path = match d.str("path") {
p if !p.is_empty() => p,
_ => {
let pick = crate::FilePick { filters: vectorcraft_engine::cmd::fileio::place_filters().collect(), ..Default::default() };
let pick =
crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::place_filters()), ..Default::default() };
crate::picks::open(app, &pick).ok_or("cancelled")?
}
};
+4 -1
View File
@@ -177,7 +177,10 @@ fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action, current: &str) ->
f();
return Ok(());
}
let pick = crate::FilePick { filters: vec![("PDF presets", vectorcraft_engine::cmd::pdfcmds::PRESET_EXTS)], ..Default::default() };
let pick = crate::FilePick {
filters: vec![crate::FilePick::filter("PDF presets", vectorcraft_engine::cmd::pdfcmds::PRESET_EXTS)],
..Default::default()
};
let path = crate::picks::open(app, &pick).ok_or("cancelled")?;
let r = app.run("pdf.preset.import", json!({ "path": path }))?;
if let Some(first) = r["imported"].get(0).and_then(Value::as_str) {
@@ -195,7 +195,7 @@ pub(super) fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action, current
f();
return Ok(());
}
let pick = crate::FilePick { filters: vec![("Perspective grid presets", PRESET_EXTS)], ..Default::default() };
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("Perspective grid presets", PRESET_EXTS)], ..Default::default() };
let path = crate::picks::open(app, &pick).ok_or("cancelled")?;
let r = app.run("perspective.presets.import", json!({ "path": path }))?;
if let Some(first) = r["imported"].get(0).and_then(Value::as_str) {
+1 -1
View File
@@ -157,7 +157,7 @@ pub(super) fn run(app: &mut VectorcraftApp, d: &mut Dialog, act: Action, current
f();
return Ok(());
}
let pick = crate::FilePick { filters: vec![("Print presets", PRESET_EXTS)], ..Default::default() };
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("Print presets", PRESET_EXTS)], ..Default::default() };
let path = crate::picks::open(app, &pick).ok_or("cancelled")?;
let r = app.run("print.presets.import", json!({ "path": path }))?;
if let Some(first) = r["imported"].get(0).and_then(Value::as_str) {
+2 -1
View File
@@ -452,7 +452,8 @@ fn assign_tab(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog, rows: &mut
// Import a library (.ase, .acb, .gpl, .vcswatches or a document's swatches) and limit to it.
if app.services.pick_open.is_some() && widgets::icon_button(ui, "folder-open", tl!("Other Library…"), false, 24.0).clicked() {
let r = crate::picks::in_dialog(app, d, |app, d| {
let pick = crate::FilePick { filters: vectorcraft_engine::cmd::fileio::open_filters().collect(), ..Default::default() };
let pick =
crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::open_filters()), ..Default::default() };
let Some(path) = crate::picks::open(app, &pick) else { return Ok(()) };
let r = app.run("swatch.library.load", json!({ "path": path }))?;
d.fields.insert("limitTo".into(), r["library"].clone());
+5 -2
View File
@@ -543,7 +543,7 @@ mod tests {
assert_eq!((r["pending"].as_str(), r["path"].as_str()), (Some("svgOptions"), Some("art.svg")));
// The save panel offers every save format, the document's own first.
let pick = &picks.borrow()[0];
let labels: Vec<&str> = pick.filters.iter().map(|f| f.0).collect();
let labels: Vec<&str> = pick.filters.iter().map(|f| f.0.as_str()).collect();
assert_eq!(labels, ["VectorCraft", "VectorCraft Template", "PDF", "SVG", "SVG Compressed", "PDF-compatible .ai"]);
assert_eq!(pick.name, "Untitled-1.vectorcraft");
let d = dialog(&app);
@@ -698,7 +698,10 @@ mod tests {
let pick = &picks.borrow()[1];
assert_eq!(pick.name, "Untitled-1 template.vctemplate");
assert_eq!(pick.folder.as_deref().map(std::path::Path::new), Some(folder.as_path()));
assert_eq!(pick.filters, [("VectorCraft Template", &["vctemplate"][..]), ("PDF-compatible .ait template", &["ait"][..])]);
assert_eq!(
pick.filters,
crate::FilePick::filters([("VectorCraft Template", &["vctemplate"][..]), ("PDF-compatible .ait template", &["ait"][..])])
);
assert_eq!(written.borrow()[1].0, "copy.vectorcraft.vctemplate", "the picked name, with the template's extension");
}
+1 -1
View File
@@ -40,7 +40,7 @@ pub(crate) fn command(app: &mut VectorcraftApp, p: &Value) -> Result<Value, Stri
if app.services.open_async.is_some() {
return io::open_dialog(app).map(|_| Value::Null);
}
let pick = FilePick { filters: vec![("Scripts", vectorcraft_engine::cmd::scripts::EXTS)], ..Default::default() };
let pick = FilePick { filters: vec![FilePick::filter("Scripts", vectorcraft_engine::cmd::scripts::EXTS)], ..Default::default() };
params = json!({ "path": crate::picks::open(app, &pick).ok_or("cancelled")? });
}
if params.get("path").is_some()
@@ -462,7 +462,8 @@ pub fn command(app: &mut VectorcraftApp, p: &Value) -> Result<Value, String> {
Some(path) => path,
None if app.services.open_async.is_some() => return crate::io::open_dialog(app).map(|_| Value::Null),
None => {
let pick = FilePick { filters: vec![("Shortcut sets", vectorcraft_engine::cmd::shortcutset::EXTS)], ..Default::default() };
let pick =
FilePick { filters: vec![FilePick::filter("Shortcut sets", vectorcraft_engine::cmd::shortcutset::EXTS)], ..Default::default() };
crate::picks::open(app, &pick).ok_or("cancelled")?
}
};
+10 -8
View File
@@ -152,7 +152,9 @@ pub fn install_plugin(app: &mut VectorcraftApp, path: Option<String>) -> Result<
let path = match path {
Some(p) => p,
None if app.services.open_async.is_some() => return open_dialog(app).map(|_| Value::Null),
None => pick_open(app, &FilePick { filters: vec![("Plug-ins", vectorcraft_engine::cmd::plugin::EXTS)], ..Default::default() })?,
None => {
pick_open(app, &FilePick { filters: vec![FilePick::filter("Plug-ins", vectorcraft_engine::cmd::plugin::EXTS)], ..Default::default() })?
}
};
let r = app.run("plugin.install", json!({ "path": path }))?;
app.status(format!("Installed plug-in {}", r["name"].as_str().unwrap_or_default()));
@@ -165,7 +167,7 @@ pub fn open_dialog(app: &mut VectorcraftApp) -> Result<(), String> {
f();
return Ok(());
}
let path = pick_open(app, &FilePick { filters: fileio::open_filters().collect(), ..Default::default() })?;
let path = pick_open(app, &FilePick { filters: FilePick::filters(fileio::open_filters()), ..Default::default() })?;
open_reporting(app, &path).map(|_| ())
}
@@ -264,7 +266,7 @@ pub fn new_from_template(app: &mut VectorcraftApp, path: Option<String>) -> Resu
Some(p) => p,
None if app.services.open_async.is_some() => return open_dialog(app).map(|_| Value::Null),
None => {
let filters = std::iter::once(("Templates", TEMPLATE_EXTS)).chain(fileio::open_filters()).collect();
let filters = FilePick::filters(std::iter::once(("Templates", TEMPLATE_EXTS)).chain(fileio::open_filters()));
pick_open(app, &FilePick { folder: fileio::templates_dialog_folder(&app.session.prefs), filters, ..Default::default() })?
}
};
@@ -367,12 +369,12 @@ const FILE_TYPES: &[(&str, &[&str])] = &[
/// 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])> {
fn file_types(ext: &str) -> Vec<crate::FileFilter> {
if let Some(f) = fileio::format(ext) {
return vec![(f.label, f.extensions)];
return vec![FilePick::filter(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()
FilePick::filters(FILE_TYPES.iter().find(|(_, exts)| exts.contains(&ext.as_str())).copied())
}
/// `path`, else one picked in a save panel (the web: the suggested name) for the active document
@@ -461,10 +463,10 @@ pub fn save(app: &mut VectorcraftApp, mode: SaveMode, p: &Value, ask_options: bo
if is_web(app) && ask_options {
return Ok(open_options(app, mode.command(), first.format, &first.name, true));
}
let filters = match mode {
let filters = FilePick::filters(match mode {
SaveMode::Template => fileio::template_filters(first.format.id),
_ => fileio::save_filters(first.format.id),
};
});
let pick = FilePick { name: first.name.clone(), folder: first.folder.clone(), filters };
let picked = pick_finished(app, &pick, |picked, file_type| with_picked_type(picked, &first.name, file_type, first.format, mode))?;
// The picked file type wins over a `format` param.
+16 -2
View File
@@ -175,14 +175,18 @@ pub use control::{ControlRequest, ControlResponse, Ticket as ControlTicket};
pub use state::{UiState, View};
pub use sysclip::{ClipboardProbeFactory, SystemClipboard};
/// A file type a dialog filters by: its label, which shows translated, and its extensions without
/// the dot.
pub type FileFilter = (String, Vec<String>);
/// What a file dialog shows: a suggested file name (save dialogs), the folder to start in and the
/// file-type filters.
#[derive(Clone, Debug, Default, PartialEq)]
pub struct FilePick {
pub name: String,
pub folder: Option<String>,
/// `(label, extensions without the dot)`, the default type first; empty = every file.
pub filters: Vec<(&'static str, &'static [&'static str])>,
/// The default type first; empty = every file.
pub filters: Vec<FileFilter>,
}
impl FilePick {
@@ -190,6 +194,16 @@ impl FilePick {
pub fn named(name: &str) -> Self {
Self { name: name.to_string(), ..Default::default() }
}
/// The file type `label` with `extensions`.
pub fn filter(label: &str, extensions: &[&str]) -> FileFilter {
(label.to_owned(), extensions.iter().map(|e| (*e).to_owned()).collect())
}
/// The file types of `list`, as the engine lists them: `(label, extensions)`.
pub fn filters<'a>(list: impl IntoIterator<Item = (&'a str, &'a [&'a str])>) -> Vec<FileFilter> {
list.into_iter().map(|(label, extensions)| Self::filter(label, extensions)).collect()
}
}
pub type PickOpen = Box<dyn FnMut(&FilePick) -> Option<String>>;
+1 -1
View File
@@ -635,7 +635,7 @@ fn import_picked(app: &mut VectorcraftApp) {
f();
return;
}
let pick = crate::FilePick { filters: vec![("Libraries", LIBRARY_EXTS)], ..Default::default() };
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("Libraries", LIBRARY_EXTS)], ..Default::default() };
if let Some(path) = crate::picks::open(app, &pick) {
let r = import(app, json!({ "path": path }));
report(app, r);
+1 -1
View File
@@ -481,7 +481,7 @@ pub(crate) fn pick_library_file(app: &mut VectorcraftApp, path: Option<String>)
return Ok(None);
}
path.or_else(|| {
let pick = crate::FilePick { filters: vectorcraft_engine::cmd::fileio::open_filters().collect(), ..Default::default() };
let pick = crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::open_filters()), ..Default::default() };
crate::picks::open(app, &pick)
})
.map(Some)
+2 -2
View File
@@ -123,7 +123,7 @@ pub(crate) fn relink(app: &mut VectorcraftApp, ids: Vec<u64>) {
}
fn relink_picked(app: &mut VectorcraftApp, ids: Vec<u64>) {
let pick = crate::FilePick { filters: vectorcraft_engine::cmd::fileio::place_filters().collect(), ..Default::default() };
let pick = crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::place_filters()), ..Default::default() };
let Some(path) = crate::picks::open(app, &pick) else { return };
let r = app.run("links.relink", json!({ "ids": ids, "path": path }));
report(app, r, |v| format!("Relinked {} image(s)", v["relinked"].as_array().map_or(0, Vec::len)));
@@ -134,7 +134,7 @@ fn relink_picked(app: &mut VectorcraftApp, ids: Vec<u64>) {
/// say.
pub(crate) fn replace(app: &mut VectorcraftApp, ids: Vec<u64>) {
crate::picks::button(app, move |app| {
let pick = crate::FilePick { filters: vectorcraft_engine::cmd::fileio::place_filters().collect(), ..Default::default() };
let pick = crate::FilePick { filters: crate::FilePick::filters(vectorcraft_engine::cmd::fileio::place_filters()), ..Default::default() };
let Some(path) = crate::picks::open(app, &pick) else { return };
let r = app.run("links.relink", json!({ "ids": ids.clone(), "path": path, "preserve": "fit" }));
report(app, r, |v| format!("Replaced {} image(s)", v["relinked"].as_array().map_or(0, Vec::len)));
+1 -1
View File
@@ -112,7 +112,7 @@ fn settings_section(app: &mut VectorcraftApp, ui: &mut Ui) {
if widgets::flat_button(ui, tl!("Load Profile…"), 110.0).clicked() {
// Its file dialog, shown off the UI thread, loads the profile when it answers.
crate::picks::button(app, |app| {
let pick = crate::FilePick { filters: vec![("ICC Profiles", &["icc", "icm"])], ..Default::default() };
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("ICC Profiles", &["icc", "icm"])], ..Default::default() };
if let Some(path) = crate::picks::open(app, &pick) {
run(app, "color.loadProfile", json!({ "path": path }));
}
+2 -2
View File
@@ -334,7 +334,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
/// Variables panel: Import Data Sets… opens a file dialog for a comma- or tab-separated file and runs `dataset.import` on the chosen file.
fn import_datasets(app: &mut VectorcraftApp) {
let pick = crate::FilePick { filters: vec![("CSV", &["csv", "tsv", "txt"])], ..Default::default() };
let pick = crate::FilePick { filters: vec![crate::FilePick::filter("CSV", &["csv", "tsv", "txt"])], ..Default::default() };
let Some(path) = crate::picks::open(app, &pick) else { return };
if let Err(e) = app.run("dataset.import", json!({"path": path})) {
app.status(e);
@@ -343,7 +343,7 @@ fn import_datasets(app: &mut VectorcraftApp) {
/// Variables panel: Export Data Sets… — pick a path and run `dataset.export`.
fn export_datasets(app: &mut VectorcraftApp) {
let pick = crate::FilePick { name: "datasets.csv".into(), filters: vec![("CSV", &["csv"])], ..Default::default() };
let pick = crate::FilePick { name: "datasets.csv".into(), filters: vec![crate::FilePick::filter("CSV", &["csv"])], ..Default::default() };
let Some(path) = crate::picks::save(app, &pick) else { return };
if let Err(e) = app.run("dataset.export", json!({"path": path})) {
app.status(e);
+3 -1
View File
@@ -139,7 +139,9 @@ fn pick(app: &mut VectorcraftApp) -> Result<(), String> {
let paths = if app.services.pick_open_multi.is_some() {
crate::picks::open_many(app)
} else {
crate::picks::open(app, &crate::FilePick { filters: fileio::place_filters().collect(), ..Default::default() }).into_iter().collect()
crate::picks::open(app, &crate::FilePick { filters: crate::FilePick::filters(fileio::place_filters()), ..Default::default() })
.into_iter()
.collect()
};
if paths.is_empty() {
return Err("cancelled".into());
+8 -2
View File
@@ -476,7 +476,10 @@ 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 pick = crate::FilePick { filters: vec![("Shortcut sets", &["json"])], ..crate::FilePick::named("VectorCraft Shortcuts.json") };
let pick = crate::FilePick {
filters: vec![crate::FilePick::filter("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() {
@@ -495,7 +498,10 @@ pub fn run_command(app: &mut VectorcraftApp, id: &str, p: &Value) -> Option<Resu
let data = if let Some(v) = p.get("data") {
Ok(v.clone())
} else {
let pick = crate::FilePick { filters: vec![("Shortcut sets", crate::dialogs::shortcut_import::IMPORT_EXTS)], ..Default::default() };
let pick = crate::FilePick {
filters: vec![crate::FilePick::filter("Shortcut sets", crate::dialogs::shortcut_import::IMPORT_EXTS)],
..Default::default()
};
let path = s("path").or_else(|| crate::picks::open(app, &pick));
// A `.kys` file is read by Import Shortcut Set, up to its reader's cap.
if let Some(path) = path.as_deref().filter(|p| crate::dialogs::shortcut_import::is_kys(p)) {
+2 -2
View File
@@ -213,7 +213,7 @@ fn load_profile_asks_off_the_ui_thread() {
let (mut app, shown, _) = app();
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
click(&mut app, "Load Profile…", crate::panels::separations::show);
assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(p)] if p.filters.iter().any(|(_, e)| e.contains(&"icc"))));
assert!(matches!(shown.requests().as_slice(), [PickRequest::Open(p)] if p.filters.iter().any(|(_, e)| e.iter().any(|e| e == "icc"))));
poll(&mut app);
assert!(app.ui.status.is_empty(), "{}", app.ui.status);
shown.answer(&["/no/such/profile.icc"]);
@@ -897,7 +897,7 @@ fn only_picks_shows_the_hosts_dialogs() {
.services
.pick_open
.as_mut()
.and_then(|f| f(&crate::FilePick { filters: vec![("ICC Profiles", &["icc", "icm"])], ..Default::default() }))
.and_then(|f| f(&crate::FilePick { filters: vec![FilePick::filter("ICC Profiles", &["icc", "icm"])], ..Default::default() }))
{"#;
assert_eq!(in_line_dialogs(&code_only(before)).len(), 1);
assert_eq!(in_line_dialogs(&code_only("let s = &mut app.services; let p = (s.pick_save.as_mut()?)(&pick);")).len(), 1);
+10 -10
View File
@@ -35,7 +35,7 @@ fn last(w: &Written) -> String {
}
/// The file types each save panel was asked to show.
type Shown = Rc<RefCell<Vec<Vec<(&'static str, &'static [&'static str])>>>>;
type Shown = Rc<RefCell<Vec<Vec<crate::FileFilter>>>>;
/// An app with a document whose save panel records the file types it shows and answers the
/// suggested name in `/tmp`.
@@ -57,7 +57,7 @@ fn panel_app() -> (VectorcraftApp, Shown) {
}
/// The file types the last save panel showed.
fn last_shown(s: &Shown) -> Vec<(&'static str, &'static [&'static str])> {
fn last_shown(s: &Shown) -> Vec<crate::FileFilter> {
s.borrow().last().cloned().unwrap_or_default()
}
@@ -68,26 +68,26 @@ 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");
assert_eq!(last_shown(&shown), FilePick::filters([("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}");
assert_eq!(last_shown(&shown), FilePick::filters([(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");
assert_eq!(last_shown(&shown), FilePick::filters([("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…");
assert_eq!(last_shown(&shown), FilePick::filters([("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…");
assert_eq!(last_shown(&shown), FilePick::filters([("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");
assert_eq!(last_shown(&shown), FilePick::filters([("PostScript", &["ps"][..])]), "Print to a PostScript file");
app.run("shortcuts.export", json!({})).unwrap();
assert_eq!(last_shown(&shown), [("Shortcut sets", &["json"][..])], "Keyboard Shortcuts › Export…");
assert_eq!(last_shown(&shown), FilePick::filters([("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),
@@ -97,7 +97,7 @@ fn save_panels_show_the_file_type_they_write() {
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…");
assert_eq!(last_shown(&shown), FilePick::filters([(label, exts)]), "{export}: the presets dialog's Export…");
}
}
+2 -2
View File
@@ -129,7 +129,7 @@ fn file_open_of_a_script_checks_it_with_what_it_includes() {
}));
app.ui.dialog = None;
menus::run_ui_command(&mut app, "ui.checkScript", &json!({})).unwrap().unwrap();
assert_eq!(asked.borrow().as_slice(), [vec![("Scripts", &["jsx", "jsxinc", "jsxbin"][..])]]);
assert_eq!(asked.borrow().as_slice(), [crate::FilePick::filters([("Scripts", &["jsx", "jsxinc", "jsxbin"][..])])]);
assert_eq!(report(&app)["name"], "main.jsx");
let _ = std::fs::remove_dir_all(dir);
}
@@ -172,7 +172,7 @@ fn check_script_asks_off_the_ui_thread_and_needs_no_document() {
app.run("file.new", json!({"width": 100, "height": 80})).unwrap();
assert!(app.run("ui.checkScript", json!({})).is_err(), "nothing yet: the dialog is open");
assert!(
matches!(shown.requests().as_slice(), [crate::picks::PickRequest::Open(pick)] if pick.filters == [("Scripts", &["jsx", "jsxinc", "jsxbin"][..])])
matches!(shown.requests().as_slice(), [crate::picks::PickRequest::Open(pick)] if pick.filters == crate::FilePick::filters([("Scripts", &["jsx", "jsxinc", "jsxbin"][..])]))
);
app.run("file.close", json!({})).unwrap();
shown.answer(&[&path.to_string_lossy()]);