Plug-ins: a file only an importer reads opens when dropped on the canvas

File › Place reads only built-in formats, so a dropped file that only an
importer reads failed with a place error. It now opens as File › Open
opens it, with or without a drop position, also when an exporter lists
the same extension.
This commit is contained in:
dotCooCoo
2026-11-08 09:38:30 -08:00
parent 9f528da663
commit 054c14a819
5 changed files with 39 additions and 6 deletions
+2 -2
View File
@@ -132,8 +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, opens_as_other, place_filters, plugin_formats, read_extensions, readable_format, readable_format_for_name, titled_for_export,
writable_format,
open_filters, opens_as_other, opens_with_importer, place_filters, plugin_formats, read_extensions, readable_format, readable_format_for_name,
titled_for_export, writable_format,
};
#[cfg(test)]
+7
View File
@@ -669,6 +669,13 @@ fn format_filters() -> impl Iterator<Item = (&'static str, &'static [&'static st
FORMATS.iter().filter(|f| f.read).map(|f| (f.label, read_extensions(f)))
}
/// Whether a file named `name` can only be read by an importer plug-in: an installed importer lists
/// its extension and File › Place does not read that extension.
pub fn opens_with_importer(name: &str) -> bool {
let ext = extension(name);
!ext.is_empty() && !PLACE_EXTS.contains(&ext.as_str()) && !vectorcraft_plugins::import::for_extension(&ext).is_empty()
}
/// 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
@@ -191,6 +191,32 @@ fn export_as_lists_exporters_and_not_importers() {
app.run("plugin.remove", json!({"id": "test.ui.writes"})).unwrap();
}
/// A file only an importer reads opens wherever it is dropped, also when an exporter lists its
/// extension; a file File › Place reads is still placed on the canvas.
#[test]
fn a_dropped_file_only_an_importer_reads_opens() {
use crate::place::DropTarget;
let mut app = app();
app.run("file.new", json!({"width": 400, "height": 300})).unwrap();
app.sync_views();
app.canvas_rect = Some(egui::Rect::from_min_size(egui::pos2(0.0, 0.0), egui::vec2(800.0, 600.0)));
let on_canvas = Some(egui::pos2(400.0, 300.0));
let placed = |app: &VectorcraftApp, name, pos| matches!(app.drop_target(name, pos, false), DropTarget::Place(_));
assert!(placed(&app, "a.tud", on_canvas), "no importer lists the extension yet");
install(&mut app, &tpl::module(&tpl::importer("test.ui.dropped", r#"["tud","tus","png"]"#, "{}"), tpl::ECHO, "", 1));
install(&mut app, &tpl::module(&tpl::exporter("test.ui.dropshare", r#"["tus"]"#, "{}"), tpl::ECHO, "", 1));
for name in ["a.tud", "A.TUD", "a.tus"] {
assert_eq!(app.drop_target(name, on_canvas, false), DropTarget::Open, "{name} on the canvas");
assert_eq!(app.drop_target(name, None, false), DropTarget::Open, "{name} without a position");
}
assert!(placed(&app, "a.png", on_canvas), "File › Place reads the extension");
assert!(placed(&app, "a.png", None));
app.run("plugin.remove", json!({"id": "test.ui.dropped"})).unwrap();
app.run("plugin.remove", json!({"id": "test.ui.dropshare"})).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]
+3 -3
View File
@@ -444,12 +444,12 @@ impl VectorcraftApp {
/// onto the canvas of the open document (Shift embeds it), else opened. Where the platform
/// doesn't say (`None`: desktop drags carry no position), a document opens as a tab of its
/// own and a picture or text is placed in the middle of the view. A library, a presets file, a
/// plug-in, a shortcut set or a script opens wherever it is dropped, as File → Open opens it
/// ([`io::opens_as`]).
/// plug-in, a shortcut set, a script or a file only an importer plug-in reads opens wherever it
/// is dropped, as File → Open opens it ([`io::opens_as`], [`fileio::opens_with_importer`]).
pub fn drop_target(&self, name: &str, pos: Option<egui::Pos2>, shift: bool) -> DropTarget {
let (Some(rect), Some(st), Some(v)) = (self.canvas_rect, self.session.active(), self.view()) else { return DropTarget::Open };
let at = match pos {
_ if io::opens_as(name).is_some() => return DropTarget::Open,
_ if io::opens_as(name).is_some() || fileio::opens_with_importer(name) => return DropTarget::Open,
Some(p) if !rect.contains(p) => return DropTarget::Open,
Some(p) => crate::canvas::Xf::new(rect, v).to_doc(p),
None if is_document(name) => return DropTarget::Open,
+1 -1
View File
@@ -48,7 +48,7 @@ one whose id sorts first opens it. `document.open {"importer": "plugin.<id>", "p
which must list the file's extension, also one a built-in format reads (an importer that lists `svg` opens an `.svg`
only this way). A damaged file of a built-in format reports that format's error, and the file is not handed to an
importer. The importer's objects become a new document whose artboard is their bounds (at least 1 pt wide and high,
and within ±4,000,000 pt); the document keeps no path, so Save asks for a name. File › Place reads only the built-in formats. These rules are
and within ±4,000,000 pt); the document keeps no path, so Save asks for a name. File › Place reads only the built-in formats, and a file only an importer reads opens as a new document when it is dropped on the canvas. These rules are
VectorCraft's own.
In the app, installed filters appear under **Object › Plug-ins**, next to **Install Plug-in…** and **Reload