Import Audio: Browse selects several files at once (#1089) (#1206)

This commit is contained in:
MarekSpodBiedry
2026-11-13 01:50:08 +01:00
committed by GitHub
parent 1c4a8380e5
commit 16110013c3
6 changed files with 102 additions and 1 deletions
+3
View File
@@ -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),
+18
View File
@@ -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<Vec<&str>, _> = 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"
+37 -1
View File
@@ -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<Value> = 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();
+2
View File
@@ -5,6 +5,8 @@ use std::path::{Path, PathBuf};
#[derive(Default)]
pub struct Services {
pub pick_open: Option<Box<dyn Fn(&str, &[&str]) -> Option<PathBuf>>>,
/// Like `pick_open`, but the user may select several files (Import Audio). Empty when cancelled.
pub pick_open_many: Option<Box<dyn Fn(&str) -> Vec<PathBuf>>>,
/// The purpose and the file to start from.
pub pick_save: Option<Box<dyn Fn(&str, &Path) -> Option<PathBuf>>>,
/// The folder to start in.
+41
View File
@@ -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<PathBuf>) -> Harness<'static, SoundApp> {
let pick_open_many: Box<dyn Fn(&str) -> Vec<PathBuf>> = 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:?}"),
}
}
+1
View File
@@ -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;