mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-24 09:46:59 -06:00
FilePick filters own their text, so a dialog's file types can be made at run time: FileFilter is (String, Vec<String>), FilePick::filter makes one from literals and FilePick::filters converts the engine's lists; the macOS save panel, the Linux portal and the rfd dialogs take the owned lists. No behaviour changes.
This commit is contained in:
@@ -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}");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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")?
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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>>;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)));
|
||||
|
||||
@@ -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 }));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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)) {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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…");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()]);
|
||||
|
||||
Reference in New Issue
Block a user