diff --git a/crates/engine/src/project_panel.rs b/crates/engine/src/project_panel.rs index 2588a7550..ea18581dd 100644 --- a/crates/engine/src/project_panel.rs +++ b/crates/engine/src/project_panel.rs @@ -17,6 +17,7 @@ //! | `project.freeform.saveArrangement` / `.restoreArrangement` / `.arrangements` / `.deleteArrangement` | saved arrangements | //! | `project.freeform.options` | Freeform View Options… (grid, snap, names, durations) | //! | `project.renameBin` | rename a bin (inline rename, undoable) | +//! | `project.selectAll` / `project.deselectAll` | select every item in the project / clear the Project panel selection (Cmd+A / Cmd+Shift+A with the panel focused) | use std::collections::BTreeMap; @@ -1222,5 +1223,13 @@ pub(crate) fn commands() -> Vec { freeform_options, ), c("project.renameBin", "Rename Bin", r#"{"bin":binId,"name":str}"#, always, rename_bin), + c("project.selectAll", "Select All Project Items", "{}", always, |s, _| { + s.state.project_selection = s.project.items.keys().copied().collect(); + Ok(json!({"selected": s.state.project_selection.len()})) + }), + c("project.deselectAll", "Deselect All Project Items", "{}", always, |s, _| { + s.state.project_selection.clear(); + Ok(Value::Null) + }), ] } diff --git a/crates/engine/src/project_panel_tests.rs b/crates/engine/src/project_panel_tests.rs index f84cf4448..42d3c79e9 100644 --- a/crates/engine/src/project_panel_tests.rs +++ b/crates/engine/src/project_panel_tests.rs @@ -286,3 +286,22 @@ fn usage_counts_sequence_uses() { assert_eq!(text, (if v > 0 { v } else { a }).to_string()); assert!(!matches!(it.kind, ItemKind::Graphic { .. })); } + +#[test] +fn select_all_selects_every_project_item() { + let mut s = demo(); + assert!(s.project.items.len() >= 3); + s.state.project_selection.clear(); + let r = s.execute("project.selectAll", json!({})).unwrap(); + assert_eq!(r["selected"].as_u64(), Some(s.project.items.len() as u64)); + assert_eq!(s.state.project_selection.len(), s.project.items.len()); + assert!(s.project.items.keys().all(|i| s.state.project_selection.contains(i))); + s.execute("project.deselectAll", json!({})).unwrap(); + assert!(s.state.project_selection.is_empty()); + // the timeline Select All still selects timeline clips and leaves the Project panel alone + s.execute("project.selectAll", json!({})).unwrap(); + let before = s.state.project_selection.clone(); + s.execute("edit.selectAll", json!({})).unwrap(); + assert!(!s.state.selection.is_empty()); + assert_eq!(s.state.project_selection, before); +} diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e7779a907..2d03d3d77 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -1001,7 +1001,11 @@ impl FilmcraftApp { } } }); - for id in fire { + for mut id in fire { + // Select All / Deselect All act on the Project panel's items when it has focus (#168). + if self.ui.focused == PanelKind::Project && matches!(id.as_str(), "edit.selectAll" | "edit.deselectAll") { + id = id.replacen("edit.", "project.", 1); + } // Mark In/Out in the Source monitor when it has focus. let params = if self.ui.focused == PanelKind::Source && (matches!(id.as_str(), "markers.markIn" | "markers.markOut") || id.starts_with("markers.markSplit") || id.starts_with("markers.goToSplit"))