From 542300e91175e0ff3c12f8fc952c1e1628ecc569 Mon Sep 17 00:00:00 2001 From: MarekSpodBiedry <73122968+MarekSpodBiedry@users.noreply.github.com> Date: Mon, 12 Oct 2026 22:26:58 +0200 Subject: [PATCH] fix(media-browser): drag every selected file to the Timeline, not just one (#1360) --- crates/ui-egui/src/panels/media_browser.rs | 47 +++++++++++++++++----- crates/ui-egui/tests/project_panel_ui.rs | 34 ++++++++++++++++ 2 files changed, 70 insertions(+), 11 deletions(-) diff --git a/crates/ui-egui/src/panels/media_browser.rs b/crates/ui-egui/src/panels/media_browser.rs index 14529a29..180f44a3 100644 --- a/crates/ui-egui/src/panels/media_browser.rs +++ b/crates/ui-egui/src/panels/media_browser.rs @@ -447,26 +447,38 @@ fn entry_interactions(app: &mut FilmcraftApp, ui: &egui::Ui, resp: &egui::Respon }); } -/// Dragging a file imports it (the selection when the file is in it) and carries the new item, so -/// the Project panel, Timeline and monitors accept it; a drop elsewhere undoes the import. +/// Dragging a file imports it (the selection when the file is in it) and carries its items, so +/// the Project panel, Timeline and monitors accept them; a drop elsewhere undoes the import. fn start_file_drag(app: &mut FilmcraftApp, ui: &egui::Ui, e: &Entry) { let paths: Vec = if app.session.browser.selection.contains(&e.path) { app.session.browser.selection.clone() } else { vec![e.path.clone()] }; - // files already in the project are dragged as they are - if let Some(i) = mb::item_for_path(&app.session, &e.path) { - crate::panels::start_drag_item(ui, i); - return; - } + // files already in the project are dragged as they are; only the others are imported + let known: Vec = paths.iter().filter_map(|p| mb::item_for_path(&app.session, p)).collect(); + let fresh: Vec = paths.iter().filter(|p| mb::item_for_path(&app.session, p).is_none()).cloned().collect(); let before = app.session.history.undo.len(); - let Ok(r) = app.session.execute("mediaBrowser.import", json!({"paths": paths})) else { return }; - let items: Vec = r["items"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default(); - if let Some(first) = items.first() { - crate::panels::start_drag_item(ui, ItemId(*first)); + let mut items: Vec = Vec::new(); + if !fresh.is_empty() { + let Ok(r) = app.session.execute("mediaBrowser.import", json!({"paths": fresh})) else { return }; + items = r["items"].as_array().map(|a| a.iter().filter_map(Value::as_u64).collect()).unwrap_or_default(); + } + let all = file_drag_items(&known, &items); + let Some(first) = all.first().copied() else { return }; + // the drop targets place the whole Project selection when the dragged item is one of it + if all.len() > 1 { + app.session.state.project_selection = all; + } + crate::panels::start_drag_item(ui, first); + if !items.is_empty() { let pos = ui.ctx().pointer_latest_pos().unwrap_or_default(); let after = app.session.history.undo.len(); with_cache(ui.ctx(), |c| c.drag = Some((items, after.max(before), pos))); } } +/// Every item a Media Browser drag carries: the selected files already in the project, then the newly imported ones. +fn file_drag_items(known: &[ItemId], imported: &[u64]) -> Vec { + known.iter().copied().chain(imported.iter().map(|i| ItemId(*i))).collect() +} + /// After a file drag ends: keep the import when it landed on a panel that takes items. fn finish_drag(app: &mut FilmcraftApp, ui: &egui::Ui) { let Some((items, hist, _)) = with_cache(ui.ctx(), |c| c.drag.clone()) else { return }; @@ -854,3 +866,16 @@ pub fn dialogs(app: &mut FilmcraftApp, ctx: &egui::Context) { } app.ui.media_browser.edit_columns = if close || crate::widgets::escape_closes(ctx) { None } else { Some(cols) }; } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dragging_several_files_carries_all_of_them() { + let all = file_drag_items(&[ItemId(7)], &[11, 12]); + assert_eq!(all, vec![ItemId(7), ItemId(11), ItemId(12)]); + assert_eq!(file_drag_items(&[], &[3]), vec![ItemId(3)]); + assert!(file_drag_items(&[], &[]).is_empty()); + } +} diff --git a/crates/ui-egui/tests/project_panel_ui.rs b/crates/ui-egui/tests/project_panel_ui.rs index 8a76e0a4..eb6bb3ec 100644 --- a/crates/ui-egui/tests/project_panel_ui.rs +++ b/crates/ui-egui/tests/project_panel_ui.rs @@ -760,6 +760,40 @@ fn media_browser_drag_imports_into_the_project_panel() { let _ = std::fs::remove_dir_all(&dir); } +/// Dragging several selected Media Browser files onto the Timeline (#1360) places every one of +/// them, whether it was already in the project or is imported by the drag. +#[test] +fn media_browser_drag_places_every_selected_file() { + let dir = media_dir("drag-many"); + let mut session = Session::default(); + session.execute("file.openDemoProject", json!({})).unwrap(); + let mut d = Driver::with(session); + d.ok("ui.set", json!({"workspace": "Editing"})); + d.ok("ui.panel.show", json!({"panel": "MediaBrowser"})); + d.exec("mediaBrowser.navigate", json!({"path": dir.to_string_lossy()})); + d.frames(3); + d.wait_for("mediaBrowser.entry.b-roll.wav"); + let listed = d.exec("mediaBrowser.list", json!({})); + let path = |name: &str| listed["entries"].as_array().unwrap().iter().find(|e| e["name"] == json!(name)).unwrap()["path"].clone(); + let (a, b) = (path("a-interview.wav"), path("b-roll.wav")); + // one file is already in the project, the other is imported by the drag + d.exec("mediaBrowser.import", json!({"paths": [a]})); + d.exec("mediaBrowser.select", json!({"paths": [a, b]})); + d.frames(2); + let n0 = d.app().session.project.items.len(); + let clips = |d: &mut Driver| { + d.app().session.active_sequence().map(|q| q.video_tracks.iter().chain(&q.audio_tracks).map(|t| t.items.len()).sum::()).unwrap_or(0) + }; + let c0 = clips(&mut d); + let e = d.rect("mediaBrowser.entry.b-roll.wav"); + let tl = d.rect("panel.Timeline"); + d.ok("ui.drag", json!({"from": {"x": e[0] + 40.0, "y": e[1] + e[3] / 2.0}, "to": {"x": tl[0] + tl[2] * 0.6, "y": tl[1] + tl[3] * 0.75}, "steps": 20})); + d.frames(6); + assert_eq!(d.app().session.project.items.len(), n0 + 1, "only the new file is imported"); + assert_eq!(clips(&mut d), c0 + 2, "both files are placed; status: {}", d.app().ui.status); + let _ = std::fs::remove_dir_all(&dir); +} + /// Dragging one of several selected Project items onto the Timeline places all of them, back to /// back, as one undo step; an item outside the selection goes alone. #[test]