From 054c14a8191de3b8b012705e81612a065430ab9d Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Sun, 8 Nov 2026 09:38:30 -0800 Subject: [PATCH] Plug-ins: a file only an importer reads opens when dropped on the canvas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- crates/engine/src/cmd/fileio.rs | 4 +-- crates/engine/src/cmd/fileio/formats.rs | 7 +++++ .../src/dialogs/tests_plugin_import.rs | 26 +++++++++++++++++++ crates/ui-egui/src/place.rs | 6 ++--- docs/plugins.md | 2 +- 5 files changed, 39 insertions(+), 6 deletions(-) diff --git a/crates/engine/src/cmd/fileio.rs b/crates/engine/src/cmd/fileio.rs index 4fda8e1ca..4e3cec17a 100644 --- a/crates/engine/src/cmd/fileio.rs +++ b/crates/engine/src/cmd/fileio.rs @@ -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)] diff --git a/crates/engine/src/cmd/fileio/formats.rs b/crates/engine/src/cmd/fileio/formats.rs index 3ecb456f6..7d646afd6 100644 --- a/crates/engine/src/cmd/fileio/formats.rs +++ b/crates/engine/src/cmd/fileio/formats.rs @@ -669,6 +669,13 @@ fn format_filters() -> impl Iterator 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 diff --git a/crates/ui-egui/src/dialogs/tests_plugin_import.rs b/crates/ui-egui/src/dialogs/tests_plugin_import.rs index 481d1eebf..a6adadb63 100644 --- a/crates/ui-egui/src/dialogs/tests_plugin_import.rs +++ b/crates/ui-egui/src/dialogs/tests_plugin_import.rs @@ -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] diff --git a/crates/ui-egui/src/place.rs b/crates/ui-egui/src/place.rs index d4f4412ec..ebb42852a 100644 --- a/crates/ui-egui/src/place.rs +++ b/crates/ui-egui/src/place.rs @@ -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, 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, diff --git a/docs/plugins.md b/docs/plugins.md index 9bae546a8..6aa9816f0 100644 --- a/docs/plugins.md +++ b/docs/plugins.md @@ -48,7 +48,7 @@ one whose id sorts first opens it. `document.open {"importer": "plugin.", "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