diff --git a/apps/soundcraft/src/main.rs b/apps/soundcraft/src/main.rs index ad91aa00..8121ae8b 100644 --- a/apps/soundcraft/src/main.rs +++ b/apps/soundcraft/src/main.rs @@ -156,6 +156,9 @@ fn services() -> Services { }; d.pick_file() })), + pick_open_many: Some(Box::new(|_purpose: &str| { + rfd::FileDialog::new().add_filter("Audio", &FileFormat::readable_extensions()).pick_files().unwrap_or_default() + })), pick_save: Some(Box::new(|purpose: &str, path: &std::path::Path| { let mut d = match purpose { "file.bounce_mix" => rfd::FileDialog::new().add_filter("Audio", craft_audio::ENCODE_EXTENSIONS), diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index eafe687f..fd803165 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -331,7 +331,25 @@ fn bounce_file_row(app: &mut SoundApp, ui: &mut egui::Ui, path: &mut String, for fn path_prompt_form(app: &mut SoundApp, ui: &mut egui::Ui, cmd: &str, path: &mut String, key: &str, enter: bool) -> Option<(String, Value)> { crate::widgets::named_text_field(ui.add(egui::TextEdit::singleline(path).desired_width(420.0)), "Path"); let mut picked = None; + // Import Audio picks several files at once; every other open dialog takes one. + let multi = cmd == "file.import_audio" && app.services.pick_open_many.is_some(); + if multi + && let Some(pick) = &app.services.pick_open_many + && ui.button("Browse…").clicked() + { + let mut files = pick(cmd); + if files.len() > 1 { + let texts: Result, _> = files.iter().map(|f| unicode(f)).collect(); + match texts { + Ok(texts) => return Some((cmd.to_string(), json!({"paths": texts}))), + Err(e) => app.ui.status = e.to_string(), + } + } else { + picked = files.pop(); + } + } if let Some(pick) = &app.services.pick_open + && !multi && cmd != "session.save_as" && cmd != "session.save_copy" && key != "dir" diff --git a/crates/ui-egui/src/import.rs b/crates/ui-egui/src/import.rs index acd736af..f17a041b 100644 --- a/crates/ui-egui/src/import.rs +++ b/crates/ui-egui/src/import.rs @@ -46,8 +46,30 @@ impl Imports { } /// Imports the audio file `params["path"]` names (the `file.import_audio` params) in the -/// background. Anything else (several `paths`, no path, the browser) runs the command now. +/// background; several `paths` queue one import each. Anything else (no path, the browser) runs the command now. pub fn start(app: &mut SoundApp, params: Value) { + // Several picked files: one queue entry each, so they decode one at a time and the app stays responsive. + if !cfg!(target_arch = "wasm32") + && params.get("path").is_none() + && let Some(paths) = params.get("paths").and_then(Value::as_array) + { + let singles: Vec = paths + .iter() + .filter_map(Value::as_str) + .map(|p| { + let mut one = params.clone(); + if let Some(o) = one.as_object_mut() { + o.remove("paths"); + o.insert("path".into(), Value::from(p)); + } + one + }) + .collect(); + if !singles.is_empty() { + app.imports.queue.extend(singles); + return; + } + } if cfg!(target_arch = "wasm32") || params.get("path").and_then(Value::as_str).is_none() { run_now(app, params); return; @@ -213,6 +235,20 @@ mod tests { assert_eq!(app.engine.session().tracks.len(), 1, "the good file still imports"); } + /// Issue #1089: several files picked in one Browse go to the queue, one entry each. + #[test] + fn several_paths_import_one_after_another() { + let ctx = egui::Context::default(); + let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services::default()); + let (a, b, c) = (tone_wav("many-a.wav"), tone_wav("many-b.wav"), tone_wav("many-c.wav")); + start(&mut app, json!({"paths": [a, b, c]})); + assert_eq!(app.imports.queue.len(), 3); + step(&mut app, &ctx); + assert_eq!(app.imports.label().map(|l| l.1), Some(2), "the other two wait for the first"); + settle(&mut app, &ctx); + assert_eq!(app.engine.session().tracks.len(), 3); + } + #[test] fn a_failed_import_stays_on_the_status_line_while_the_next_one_starts() { let ctx = egui::Context::default(); diff --git a/crates/ui-egui/src/services.rs b/crates/ui-egui/src/services.rs index e128241f..1b5469e4 100644 --- a/crates/ui-egui/src/services.rs +++ b/crates/ui-egui/src/services.rs @@ -5,6 +5,8 @@ use std::path::{Path, PathBuf}; #[derive(Default)] pub struct Services { pub pick_open: Option Option>>, + /// Like `pick_open`, but the user may select several files (Import Audio). Empty when cancelled. + pub pick_open_many: Option Vec>>, /// The purpose and the file to start from. pub pick_save: Option Option>>, /// The folder to start in. diff --git a/crates/ui-egui/tests/it/import_browse.rs b/crates/ui-egui/tests/it/import_browse.rs new file mode 100644 index 00000000..03299b7c --- /dev/null +++ b/crates/ui-egui/tests/it/import_browse.rs @@ -0,0 +1,41 @@ +//! Import Audio's Browse button lets the user select several files at once (#1089). +use egui::vec2; +use egui_kittest::{Harness, kittest::Queryable}; +use soundcraft_ui_egui::{Services, SoundApp, dialogs::Dialog}; +use std::path::PathBuf; + +fn browse(picked: Vec) -> Harness<'static, SoundApp> { + let pick_open_many: Box Vec> = Box::new(move |purpose| { + assert_eq!(purpose, "file.import_audio"); + picked.clone() + }); + let mut app = SoundApp::new(soundcraft_engine::Engine::default(), None, Services { pick_open_many: Some(pick_open_many), ..Default::default() }); + assert!(app.dialogs.open_for_command(&app.engine, "file.import_audio")); + let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_ui_state( + |ui, app: &mut SoundApp| { + app.logic(&ui.ctx().clone()); + app.ui(ui); + }, + app, + ); + h.run_steps(3); + h.get_by_label("Browse…").click(); + h.run_steps(1); + h +} + +#[test] +fn browse_with_several_files_imports_them_and_closes_the_dialog() { + let h = browse(vec!["/nope/a.wav".into(), "/nope/b.wav".into()]); + assert!(h.state().dialogs.open.is_none()); + assert!(h.state().imports.busy() || !h.state().ui.status.is_empty(), "the files were queued"); +} + +#[test] +fn browse_with_one_file_fills_the_path_field() { + let h = browse(vec!["/nope/a.wav".into()]); + match &h.state().dialogs.open { + Some(Dialog::PathPrompt { path, .. }) => assert_eq!(path, "/nope/a.wav"), + other => panic!("{other:?}"), + } +} diff --git a/crates/ui-egui/tests/it/main.rs b/crates/ui-egui/tests/it/main.rs index f07c99ad..72ca315c 100644 --- a/crates/ui-egui/tests/it/main.rs +++ b/crates/ui-egui/tests/it/main.rs @@ -22,6 +22,7 @@ mod focused_dialog_buttons; mod group_dialog; mod hide_floating; mod idle; +mod import_browse; mod key_families; mod long_menu_scroll; mod marker_rename;