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>
This commit is contained in:
Mateusz SychandMateuszSychXpl authored and GitHub committed 2026-10-17 00:27:11 +00:00
1 parent a6ca2e8ca1
commit c599238653
7 files changed
+46 -14

No files matched your search

+28 -4
View File
@@ -34,10 +34,15 @@ impl SolveApp {
}
pub(crate) fn pick_open(&mut self, complete: impl FnOnce(&mut SolveApp, Option<String>) + '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<String>) + '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<RefCell<Vec<Vec<String>>>> = 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::<Vec<_>>()), "{command}");
}
}
#[test]
fn picker_holds_engine_requests_but_answers_window_resize_requests() {
let mut h = crate::scenario::Harness::new();
+5 -3
View File
@@ -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<Box<dyn Fn() -> 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<Box<dyn Fn(&[&str]) -> FilePickerFuture>>,
pub pick_save: Option<Box<dyn Fn(&str, &[&str]) -> FilePickerFuture>>,
/// Open a dialog asking for a program (a slicer); `None` when the host has no such dialog.
pub pick_program: Option<Box<dyn Fn() -> 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" {
+1 -1
View File
@@ -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() {
+1 -1
View File
@@ -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));
}
+1 -1
View File
@@ -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).