From c59923865324cb487ff90def4361ad9c7d8a2556 Mon Sep 17 00:00:00 2001 From: Mateusz Sych <151755914+SychPL@users.noreply.github.com> Date: Sat, 17 Oct 2026 00:27:11 +0000 Subject: [PATCH] Insert SVG: the file picker opens on SVG files (#1801) (#1827) * claim: #1801 * Insert SVG: the file picker opens on SVG files (#1801) Every Open went through one host picker whose filters start with designs, CAD files and meshes, so Insert SVG (and Insert DXF) in a sketch opened on STEP, IGES and meshes, and the SVG file had to be found under another filter. The host's pick_open now takes the extensions the command reads and offers them as the picker's first, default filter. Insert SVG and Insert DXF pass their type; every other Open passes none and keeps the list it had. The browser host ignores them (it shows its own file panel). --------- Co-authored-by: MateuszSychXpl <151755914+MateuszSychXpl@users.noreply.github.com> --- apps/solidcraft-web/src/main.rs | 2 +- apps/solidcraft/src/main.rs | 12 ++++++--- crates/ui-egui/src/file_picker.rs | 32 ++++++++++++++++++++--- crates/ui-egui/src/lib.rs | 8 +++--- crates/ui-egui/src/params_dialog.rs | 2 +- crates/ui-egui/src/picker_test_support.rs | 2 +- crates/ui-egui/src/scenario.rs | 2 +- 7 files changed, 46 insertions(+), 14 deletions(-) diff --git a/apps/solidcraft-web/src/main.rs b/apps/solidcraft-web/src/main.rs index e50cb9da..83840db2 100644 --- a/apps/solidcraft-web/src/main.rs +++ b/apps/solidcraft-web/src/main.rs @@ -481,7 +481,7 @@ mod web { let panel = files.clone(); let services = Services { // Open shows the browser's files (and the computer's, from there). - pick_open: Some(Box::new(move || { + pick_open: Some(Box::new(move |_: &[&str]| { panel.panel.set(true); Box::pin(std::future::ready(None)) })), diff --git a/apps/solidcraft/src/main.rs b/apps/solidcraft/src/main.rs index 8e6ebea2..ea8b26c9 100644 --- a/apps/solidcraft/src/main.rs +++ b/apps/solidcraft/src/main.rs @@ -117,9 +117,15 @@ fn services(cc: &eframe::CreationContext<'_>) -> Services { let open_parent = parent.clone(); let program_parent = parent.clone(); Services { - pick_open: Some(Box::new(move || { - let future = open_parent - .clone() + pick_open: Some(Box::new(move |first: &[&str]| { + // The file type the command reads comes first, so the picker opens on it (#1801). + let mut dialog = open_parent.clone(); + if !first.is_empty() { + let label = first.iter().map(|e| e.to_uppercase()).collect::>().join(", "); + let both: Vec = first.iter().flat_map(|e| [e.to_lowercase(), e.to_uppercase()]).collect(); + dialog = dialog.add_filter(label, &both); + } + let future = dialog .add_filter( "Designs, CAD files and meshes", &[ diff --git a/crates/ui-egui/src/file_picker.rs b/crates/ui-egui/src/file_picker.rs index f508ed58..e880c07f 100644 --- a/crates/ui-egui/src/file_picker.rs +++ b/crates/ui-egui/src/file_picker.rs @@ -34,10 +34,15 @@ impl SolveApp { } pub(crate) fn pick_open(&mut self, complete: impl FnOnce(&mut SolveApp, Option) + 'static) { + self.pick_open_for(&[], complete); + } + + /// Open a file of this type: the picker offers these extensions first (#1801). + pub(crate) fn pick_open_for(&mut self, extensions: &[&str], complete: impl FnOnce(&mut SolveApp, Option) + 'static) { if self.file_picker_busy() { return; } - let future = self.services.pick_open.as_ref().map(|pick| pick()); + let future = self.services.pick_open.as_ref().map(|pick| pick(extensions)); self.begin_picker(future, Box::new(complete)); } @@ -66,9 +71,9 @@ impl SolveApp { self.file_picker = Some(Pending { future, complete }); } - pub(crate) fn pick_open_command(&mut self, command: &str, params: Value) { + pub(crate) fn pick_open_command(&mut self, command: &str, params: Value, extensions: &[&str]) { let command = command.to_string(); - self.pick_open(move |app, path| { + self.pick_open_for(extensions, move |app, path| { if let Some(path) = path { let mut params = params; params["path"] = json!(path); @@ -144,7 +149,7 @@ mod tests { if save { app.services.pick_save = Some(Box::new(move |_, _| Box::pin(Delayed(captured.clone())))); } else { - app.services.pick_open = Some(Box::new(move || Box::pin(Delayed(captured.clone())))); + app.services.pick_open = Some(Box::new(move |_| Box::pin(Delayed(captured.clone())))); } answer } @@ -275,6 +280,25 @@ mod tests { assert!(!crate::documents::any_dirty(&h.app)); } + /// #1801: Insert SVG and Insert DXF ask the host for their own file type first; other + /// opens keep the default list. + #[test] + fn sketch_inserts_ask_for_their_file_type() { + let mut h = crate::scenario::Harness::new(); + let asked: Rc>>> = Rc::default(); + let log = asked.clone(); + h.app.services.pick_open = Some(Box::new(move |extensions| { + log.borrow_mut().push(extensions.iter().map(|e| e.to_string()).collect()); + Box::pin(std::future::ready(None)) + })); + h.app.run("sketch.create", json!({"plane": "XY"})).unwrap(); + for (command, want) in [("sketch.insert_svg", vec!["svg"]), ("sketch.insert_dxf", vec!["dxf"]), ("file.insert_step", vec![])] { + h.app.start(command); + tick(&mut h.app, &h.ctx); + assert_eq!(asked.borrow().last().cloned(), Some(want.iter().map(|e| e.to_string()).collect::>()), "{command}"); + } + } + #[test] fn picker_holds_engine_requests_but_answers_window_resize_requests() { let mut h = crate::scenario::Harness::new(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 906ef0d9..40e05f7b 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -254,7 +254,9 @@ impl Default for UiState { /// Host services injected by the app so this crate stays portable. #[derive(Default)] pub struct Services { - pub pick_open: Option FilePickerFuture>>, + /// Ask for a file to open. The extensions are the file type the command reads, offered as + /// the picker's first filter (`&[]`: designs, CAD files and meshes, #1801). + pub pick_open: Option FilePickerFuture>>, pub pick_save: Option FilePickerFuture>>, /// Open a dialog asking for a program (a slicer); `None` when the host has no such dialog. pub pick_program: Option FilePickerFuture>>, @@ -736,8 +738,8 @@ impl SolveApp { dim_view::cancel(); self.tool = None; self.home.open = false; - if matches!(id, "sketch.insert_dxf" | "sketch.insert_svg") { - self.pick_open_command(id, json!({})); + if let Some(ext) = id.strip_prefix("sketch.insert_").filter(|e| matches!(*e, "dxf" | "svg")) { + self.pick_open_command(id, json!({}), &[ext]); return; } if id == "component.insert_design" { diff --git a/crates/ui-egui/src/params_dialog.rs b/crates/ui-egui/src/params_dialog.rs index d2cc46e9..8752588d 100644 --- a/crates/ui-egui/src/params_dialog.rs +++ b/crates/ui-egui/src/params_dialog.rs @@ -144,7 +144,7 @@ pub fn show(app: &mut SolveApp, ctx: &egui::Context) { ui.add(egui::TextEdit::singleline(&mut d.filter).hint_text(crate::i18n::tr_label("Filter")).desired_width(180.0)); ui.separator(); if ui.button("Import…").on_hover_text(crate::i18n::tr_label("CSV or JSON: updates existing parameters, adds new ones")).clicked() { - app.pick_open_command("parameters.import", json!({})); + app.pick_open_command("parameters.import", json!({}), &[]); } for (label, ext) in [("Export CSV…", "csv"), ("Export JSON…", "json")] { if ui.button(label).clicked() { diff --git a/crates/ui-egui/src/picker_test_support.rs b/crates/ui-egui/src/picker_test_support.rs index efd53678..c4ffd275 100644 --- a/crates/ui-egui/src/picker_test_support.rs +++ b/crates/ui-egui/src/picker_test_support.rs @@ -21,5 +21,5 @@ pub fn save(app: &mut SolveApp, slot: &Slot) { } pub fn open(app: &mut SolveApp, slot: &Slot) { let slot = slot.clone(); - app.services.pick_open = Some(Box::new(move || Box::pin(Manual(slot.clone())) as FilePickerFuture)); + app.services.pick_open = Some(Box::new(move |_| Box::pin(Manual(slot.clone())) as FilePickerFuture)); } diff --git a/crates/ui-egui/src/scenario.rs b/crates/ui-egui/src/scenario.rs index a2fea596..7a3de61a 100644 --- a/crates/ui-egui/src/scenario.rs +++ b/crates/ui-egui/src/scenario.rs @@ -183,7 +183,7 @@ impl Harness { self.app.services.pick_save = Some(Box::new(move |name, _| Box::pin(std::future::ready(Some(dir.join(name).to_string_lossy().into_owned()))))); let next = self.next_file.clone(); - self.app.services.pick_open = Some(Box::new(move || Box::pin(std::future::ready(next.borrow_mut().take())))); + self.app.services.pick_open = Some(Box::new(move |_| Box::pin(std::future::ready(next.borrow_mut().take())))); } /// Autosave into the scenario's recovery folder (as the desktop app does into the user's).