From 06bc7ef9ec5ecc3793f7df26522544baf1ee1a8b Mon Sep 17 00:00:00 2001 From: Luna Date: Wed, 7 Oct 2026 20:21:49 +0000 Subject: [PATCH] Engine: project.selectAll and project.deselectAll for the Project panel (#168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause: the only Select All was `edit.selectAll`, which fills the timeline clip selection (`state.selection`). Nothing filled the Project panel's selection (`state.project_selection`), so with 161 imported items the user had to click each one. Fix: - `project_panel::commands()` gets `project.selectAll` (selects every item in the project) and `project.deselectAll` (clears the Project panel selection). They are regular journalled commands with no menu entry and no shortcut of their own, so they add no shortcut conflicts. - `handle_shortcuts` (ui-egui) remaps a fired `edit.selectAll` / `edit.deselectAll` to the `project.*` command when the Project panel has focus. Cmd+A / Cmd+Shift+A keep acting on timeline clips everywhere else, and `edit.selectAll` behaves exactly as before. Out of scope: no menu entry. The Edit ▸ Select All menu item still calls `edit.selectAll` directly, because menu clicks don't go through the focus remap. The regression test checks the engine commands but not the UI keyboard remap. Regression test: project_panel_tests::select_all_selects_every_project_item (fails without the fix: unknown command). Co-Authored-By: ninso112 Co-Authored-By: Luna161 --- crates/engine/src/project_panel.rs | 9 +++++++++ crates/engine/src/project_panel_tests.rs | 19 +++++++++++++++++++ crates/ui-egui/src/lib.rs | 6 +++++- 3 files changed, 33 insertions(+), 1 deletion(-) 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"))