mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-09 04:54:45 +00:00
Plug-ins: File › Open lists only the importer extensions it opens with an importer
An importer may declare an extension that File › Open handles another way: a plug-in, shortcut set, script, preset, library or swatch library such as .ase, or one a built-in format reads. "All readable files" and the importer's own filter no longer list those extensions, and an importer with none left gets no filter. Open With still opens them.
This commit is contained in:
@@ -132,7 +132,8 @@ use formats::{ARTBOARD, ARTBOARDS, RANGE, USE_ARTBOARDS, import_name, writable};
|
||||
pub use formats::{
|
||||
DOC_EXTS, DOCX_EXTS, FORMATS, Format, FormatOption, OPEN_EXTS, PLACE_EXTS, RTF_EXTS, TEXT_EXTS, UNASSOCIATED_EXTS, builtin_open_filters,
|
||||
export_extensions, extension, file_stem, format, format_for_name, has_own_title, importer_formats, library_open_filters, open_extensions,
|
||||
open_filters, place_filters, plugin_formats, read_extensions, readable_format, readable_format_for_name, titled_for_export, writable_format,
|
||||
open_filters, opens_as_other, place_filters, plugin_formats, read_extensions, readable_format, readable_format_for_name, titled_for_export,
|
||||
writable_format,
|
||||
};
|
||||
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -669,39 +669,83 @@ fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static st
|
||||
FORMATS.iter().filter(|f| f.read).map(|f| (f.label, read_extensions(f)))
|
||||
}
|
||||
|
||||
/// Every extension File › Open reads as a document: [`OPEN_EXTS`], then the extensions the
|
||||
/// installed importer plug-ins list that no built-in format reads, in plug-in id order. Made once
|
||||
/// for each set of installed plug-ins, so it lives as long as the dialogs that hold it.
|
||||
pub fn open_extensions() -> &'static [&'static str] {
|
||||
/// Whether File › Open opens a file of extension `ext` (lower case) as something other than a
|
||||
/// document: a plug-in (installed), a shortcut set, a script (checked), flattener, PDF, print,
|
||||
/// perspective grid or export presets (imported), a Libraries panel library, or a swatch or graphic
|
||||
/// style library.
|
||||
pub fn opens_as_other(ext: &str) -> bool {
|
||||
[
|
||||
crate::cmd::plugin::EXTS,
|
||||
crate::cmd::shortcutset::EXTS,
|
||||
crate::cmd::scripts::EXTS,
|
||||
crate::cmd::flatten::PRESET_EXTS,
|
||||
crate::cmd::pdfcmds::PRESET_EXTS,
|
||||
crate::cmd::printpresets::PRESET_EXTS,
|
||||
crate::cmd::perspgrid::PRESET_EXTS,
|
||||
screens::PRESET_FILE_EXTS,
|
||||
crate::cmd::library::LIBRARY_EXTS,
|
||||
crate::cmd::swatchlib::LIBRARY_EXTS,
|
||||
std::slice::from_ref(&vectorcraft_doc::style_libs::STYLES_EXT),
|
||||
]
|
||||
.iter()
|
||||
.any(|exts| exts.contains(&ext))
|
||||
}
|
||||
|
||||
/// File › Open's extensions and filters for the installed importer plug-ins.
|
||||
type ImporterPart = (&'static [&'static str], Vec<(&'static str, &'static [&'static str])>);
|
||||
|
||||
/// File › Open's part for the installed importer plug-ins: the extensions of "All readable files"
|
||||
/// ([`OPEN_EXTS`], then the importers' extensions in plug-in id order) and one filter per importer
|
||||
/// that has extensions left, under its name. An importer's extensions leave out those a built-in
|
||||
/// format reads, which File › Open reads with that format, and those File › Open opens as something
|
||||
/// else ([`opens_as_other`]). Made once for each set of installed plug-ins, so it lives as long as
|
||||
/// the dialogs that hold it.
|
||||
fn importer_open_part() -> ImporterPart {
|
||||
use vectorcraft_plugins::registry;
|
||||
static MADE: std::sync::Mutex<Option<(u64, &'static [&'static str])>> = std::sync::Mutex::new(None);
|
||||
static MADE: std::sync::Mutex<Option<(u64, ImporterPart)>> = std::sync::Mutex::new(None);
|
||||
let mut made = MADE.lock().unwrap_or_else(|e| e.into_inner());
|
||||
let revision = registry::revision();
|
||||
if let Some((r, exts)) = *made
|
||||
&& r == revision
|
||||
if let Some((r, part)) = made.as_ref()
|
||||
&& *r == revision
|
||||
{
|
||||
return exts;
|
||||
return part.clone();
|
||||
}
|
||||
let mut exts: Vec<&'static str> = OPEN_EXTS.to_vec();
|
||||
for e in importer_formats().iter().flat_map(|f| f.extensions.iter().copied()) {
|
||||
if !exts.contains(&e) && readable_format(e).is_none() {
|
||||
exts.push(e);
|
||||
let mut filters = Vec::new();
|
||||
for f in importer_formats() {
|
||||
let own: Vec<&'static str> = f.extensions.iter().copied().filter(|e| readable_format(e).is_none() && !opens_as_other(e)).collect();
|
||||
if own.is_empty() {
|
||||
continue;
|
||||
}
|
||||
for &e in &own {
|
||||
if !exts.contains(&e) {
|
||||
exts.push(e);
|
||||
}
|
||||
}
|
||||
let own: &'static [&'static str] = if own.len() == f.extensions.len() { f.extensions } else { Box::leak(own.into_boxed_slice()) };
|
||||
filters.push((f.label, own));
|
||||
}
|
||||
let exts: &'static [&'static str] = if exts.len() == OPEN_EXTS.len() { OPEN_EXTS } else { Box::leak(exts.into_boxed_slice()) };
|
||||
*made = Some((revision, exts));
|
||||
exts
|
||||
let part = (exts, filters);
|
||||
*made = Some((revision, part.clone()));
|
||||
part
|
||||
}
|
||||
|
||||
/// Every extension File › Open reads as a document: [`OPEN_EXTS`], then the extensions the
|
||||
/// installed importer plug-ins list that no built-in format reads and that File › Open doesn't open
|
||||
/// as something else ([`opens_as_other`]), in plug-in id order.
|
||||
pub fn open_extensions() -> &'static [&'static str] {
|
||||
importer_open_part().0
|
||||
}
|
||||
|
||||
/// File › Open's filters: "All readable files" first, then one per readable format, then one per
|
||||
/// importer plug-in (under its name), then swatch libraries (which open in the library panel),
|
||||
/// Libraries panel libraries, flattener, PDF, print and export presets (imported), scripts (checked
|
||||
/// without running them) and plug-ins (installed).
|
||||
/// importer plug-in (under its name, with the extensions [`open_extensions`] takes from it), then
|
||||
/// swatch libraries (which open in the library panel), Libraries panel libraries, flattener, PDF,
|
||||
/// print and export presets (imported), scripts (checked without running them) and plug-ins
|
||||
/// (installed).
|
||||
pub fn open_filters() -> impl Iterator<Item = (&'static str, &'static [&'static str])> {
|
||||
std::iter::once(("All readable files", open_extensions()))
|
||||
.chain(format_filters())
|
||||
.chain(importer_formats().into_iter().map(|f| (f.label, f.extensions)))
|
||||
.chain(other_open_filters())
|
||||
let (all, importers) = importer_open_part();
|
||||
std::iter::once(("All readable files", all)).chain(format_filters()).chain(importers).chain(other_open_filters())
|
||||
}
|
||||
|
||||
/// [`open_filters`] without the importer plug-ins: "All readable files" lists [`OPEN_EXTS`]. For
|
||||
|
||||
@@ -157,6 +157,27 @@ fn built_in_formats_keep_their_files() {
|
||||
assert_eq!(fileio::open_extensions().iter().filter(|e| **e == "svg").count(), 1);
|
||||
}
|
||||
|
||||
/// File › Open lists an importer's extensions only where it would open the file with the importer:
|
||||
/// not those a built-in format reads, and not those it opens as a plug-in, a shortcut set, a
|
||||
/// script, presets or a library. An importer with none of its own gets no filter.
|
||||
#[test]
|
||||
fn file_open_lists_only_the_extensions_it_opens_with_an_importer() {
|
||||
let mut s = Session::new();
|
||||
install(&mut s, &tpl::returning(&tpl::importer("test.claimed", r#"["tcl","ase","wasm","kys","jsx","vcstyles","svg"]"#, "{}"), RECT));
|
||||
install(&mut s, &tpl::returning(&tpl::importer("test.claimedonly", r#"["ase","gpl"]"#, "{}"), RECT));
|
||||
for ext in ["ase", "gpl", "wasm", "kys", "jsx", "vcstyles"] {
|
||||
assert!(fileio::opens_as_other(ext), "{ext}");
|
||||
assert!(!fileio::open_extensions().contains(&ext), "All readable files: {ext}");
|
||||
}
|
||||
assert!(!fileio::opens_as_other("tcl") && !fileio::opens_as_other("svg"));
|
||||
assert!(fileio::open_extensions().contains(&"tcl"));
|
||||
let filters: Vec<_> = fileio::open_filters().collect();
|
||||
assert!(filters.contains(&("Test test.claimed", &["tcl"][..])), "{filters:?}");
|
||||
assert!(!filters.iter().any(|(label, _)| *label == "Test test.claimedonly"), "{filters:?}");
|
||||
s.execute("plugin.remove", &json!({"id": "test.claimed"})).unwrap();
|
||||
s.execute("plugin.remove", &json!({"id": "test.claimedonly"})).unwrap();
|
||||
}
|
||||
|
||||
/// Several importers list one extension: the one whose id sorts first opens the file, and
|
||||
/// `importer` picks another (by its format id or its id).
|
||||
#[test]
|
||||
|
||||
@@ -164,6 +164,47 @@ fn only_file_open_s_dialog_lists_an_importer_s_files() {
|
||||
app.run("plugin.remove", json!({"id": "test.ui.filters"})).unwrap();
|
||||
}
|
||||
|
||||
/// The extensions File › Open's filters leave out of importers' (`fileio::opens_as_other`) are the
|
||||
/// ones File › Open installs, imports, checks or loads as something other than a document.
|
||||
#[test]
|
||||
fn importer_filters_leave_out_what_file_open_opens_as_something_else() {
|
||||
use vectorcraft_engine::cmd::{self, fileio};
|
||||
let lists: [&[&str]; 10] = [
|
||||
cmd::plugin::EXTS,
|
||||
cmd::shortcutset::EXTS,
|
||||
cmd::scripts::EXTS,
|
||||
cmd::flatten::PRESET_EXTS,
|
||||
cmd::pdfcmds::PRESET_EXTS,
|
||||
cmd::printpresets::PRESET_EXTS,
|
||||
cmd::perspgrid::PRESET_EXTS,
|
||||
fileio::SCREEN_EXPORT_PRESET_EXTS,
|
||||
cmd::library::LIBRARY_EXTS,
|
||||
cmd::swatchlib::LIBRARY_EXTS,
|
||||
];
|
||||
for ext in lists.iter().flat_map(|l| l.iter().copied()).chain([vectorcraft_doc::style_libs::STYLES_EXT]) {
|
||||
assert!(crate::io::opens_as(&format!("a.{ext}")).is_some(), "{ext}");
|
||||
assert!(fileio::opens_as_other(ext), "{ext}");
|
||||
}
|
||||
for ext in ["svg", "tcl", "pdf"] {
|
||||
assert!(crate::io::opens_as(&format!("a.{ext}")).is_none() && !fileio::opens_as_other(ext), "{ext}");
|
||||
}
|
||||
// File › Open's dialog lists an importer's `.tuo` but not its `.ase` (a swatch library there).
|
||||
let mut app = app();
|
||||
install(&mut app, &tpl::module(&tpl::importer("test.ui.claimed", r#"["tuo","ase"]"#, "{}"), tpl::ECHO, "", 1));
|
||||
let asked: std::rc::Rc<std::cell::RefCell<Option<crate::FilePick>>> = Default::default();
|
||||
let seen = asked.clone();
|
||||
app.services.pick_open = Some(Box::new(move |pick: &crate::FilePick| {
|
||||
*seen.borrow_mut() = Some(pick.clone());
|
||||
None
|
||||
}));
|
||||
let _ = app.run("file.open", json!({}));
|
||||
let pick = asked.borrow_mut().take().expect("File › Open showed its dialog");
|
||||
let own = pick.filters.iter().find(|(label, _)| label == "Test test.ui.claimed").map(|(_, exts)| exts.clone());
|
||||
assert_eq!(own, Some(vec!["tuo".to_string()]), "{:?}", pick.filters);
|
||||
assert!(!pick.filters[0].1.iter().any(|e| e == "ase"), "All readable files: {:?}", pick.filters[0]);
|
||||
app.run("plugin.remove", json!({"id": "test.ui.claimed"})).unwrap();
|
||||
}
|
||||
|
||||
/// File › Open reads a file of an extension only an importer lists, and refuses one past the
|
||||
/// importer's input limit before reading it, as Open With does.
|
||||
#[test]
|
||||
|
||||
+1
-1
@@ -6091,7 +6091,7 @@ id `plugin.<id>`: `effect.apply`, `effect.setParams` and `effect.expandAppearanc
|
||||
`effect.list` lists it. An exporter adds a write-only file format `plugin.<id>` (listed by `document.formats`):
|
||||
`document.export {format: "plugin.<id>", params?, path?}` writes it, as does a `path` ending in one of its extensions
|
||||
that no built-in format has. An importer adds a read-only file format `plugin.<id>` (listed by `document.formats`
|
||||
with `read: true`, its extensions in `openExtensions`): `document.open {path | name, dataBase64, importer?:
|
||||
with `read: true`, and `openExtensions` holds the extensions File › Open opens with it, without those a built-in format reads or that File › Open installs, imports or loads as something else, such as `.wasm` or `.ase`): `document.open {path | name, dataBase64, importer?:
|
||||
"plugin.<id>", params?}` (and `open_file {path, importer?, params?}`) opens a file whose extension no built-in format reads, by its content or its
|
||||
extension, with the installed importer that lists the extension (the one whose id sorts first, when several do).
|
||||
`importer` names the importer to use, which must list the file's extension, also one a built-in format reads, such
|
||||
|
||||
+6
-3
@@ -41,8 +41,8 @@ An exporter adds a write-only format, `plugin.<id>`, which `document.formats` li
|
||||
`document.export {"format": "plugin.<id>", "params"?: {…}, "path"?}` writes it, and so does a `path` ending in one of
|
||||
its extensions that no built-in format has; `selectedOnly` hands it the selection, as for the built-in formats.
|
||||
|
||||
An importer adds a read-only format, `plugin.<id>`, which `document.formats` lists with its `plugin` id, its extensions
|
||||
in `openExtensions`. `document.open {"path"}` (or `{"name", "dataBase64"}`) opens a file with it when no built-in format
|
||||
An importer adds a read-only format, `plugin.<id>`, which `document.formats` lists with its `plugin` id. `openExtensions`
|
||||
holds the extensions File › Open opens with the importer (the lists below describe which are left out). `document.open {"path"}` (or `{"name", "dataBase64"}`) opens a file with it when no built-in format
|
||||
reads the file, by its content or by its extension, and the importer lists the file's extension; when several do, the
|
||||
one whose id sorts first opens it. `document.open {"importer": "plugin.<id>", "params"?: {…}}` names the importer,
|
||||
which must list the file's extension, also one a built-in format reads (an importer that lists `svg` opens an `.svg`
|
||||
@@ -60,7 +60,10 @@ heading and buttons in view. Exporters are formats of **File › Export › Expo
|
||||
Artboards is greyed out (the plug-in writes the whole document); one with parameters asks for them in a dialog of the
|
||||
same kind once the file is picked, and its Export button writes the file. Importers' files are listed under File ›
|
||||
Open's "All readable files" and under a filter of their own, named after the importer (other open dialogs, such as New
|
||||
from Template and Load Styles, list only the files the built-in formats read); **File › Open With** lists the
|
||||
from Template and Load Styles, list only the files the built-in formats read). These lists leave out an importer's
|
||||
extensions that a built-in format reads, and those File › Open installs, imports or loads as something else (`.wasm`,
|
||||
`.kys`, `.jsx`, presets, libraries and swatch or graphic style libraries such as `.ase`). File › Open does not give
|
||||
those files to the importer, and Open With does. **File › Open With** lists the
|
||||
installed importers by name (`file.openWith {"importer", "path"?, "params"?}`), and each opens a file picked among its
|
||||
extensions with that importer (on the web, File › Open's picker opens instead) and adds it to Open Recent. An importer
|
||||
with parameters, given no `params`, asks for them in a dialog of the same kind once the file is picked, and its Open
|
||||
|
||||
Reference in New Issue
Block a user