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 <[email protected]>
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

+1 -1
View File
@@ -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))
})),
+9 -3
View File
@@ -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::<Vec<_>>().join(", ");
let both: Vec<String> = 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",
&[
+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).