Browse / github.com/storytold/filmcraft / patch
Select All in a text field selects its text, not the timeline clips
Select All in a text field selects its text, not the timeline clips ## Problem On macOS the native menu takes Cmd+A before a focused text field sees the key and runs Edit ▸ Select All. With the cursor in the Project panel's search box, Cmd+A selected every clip on the timeline instead of the typed text. #1102 fixed the same for text edited on the Program monitor. ## Change `crates/ui-egui/src/menus.rs`: `select_all_in_text_field` — Edit ▸ Select All with a text field focused sets that field's selection to all of its text (end clamped by the field on its next layout) and runs nothing else. It sets the field's state rather than re-sending the key, so it also
- Status
- open
- Score
- 0.00
- Adopters
- 0 other operators
- Author
- punkhop (imported from pull request #3328, open)
- Base
fa0502602cbef03bf4a3a98131d72c1e3674c238- Size
- +45 −1 in 2 files
- Review
- pending
- Shared
- 2026-10-28
- Id
9fa76dde3dd1671137fc86d5
How it went
No other operator has reported using it yet.
Diff
raw · crates/ui-egui/src/menus.rs crates/ui-egui/tests/it/project_panel_ui.rs
@@ -180,9 +180,25 @@ pub fn targets_source(app: &FilmcraftApp, params: &Value) -> bool { } } +/// Edit ▸ Select All while a text field has the keyboard (the Project panel's search, a name being+/// typed) selects that field's text, not the timeline clips: on macOS the native menu takes Cmd+A+/// before the field sees the key. The selection is set on the field's state, so it holds however+/// late in the pass the command runs (menu inbox or control channel); the field clamps the end to+/// its text. False when no text field has the keyboard.+fn select_all_in_text_field(ctx: &egui::Context) -> bool {+ use egui::text::{CCursor, CCursorRange};+ let Some(id) = ctx.memory(|m| m.focused()) else { return false };+ let Some(mut state) = egui::TextEdit::load_state(ctx, id) else { return false };+ state.cursor.set_char_range(Some(CCursorRange::two(CCursor::new(0), CCursor::new(usize::MAX))));+ state.store(ctx, id);+ true+}+ /// Execute a UI or engine command by id. pub fn invoke(app: &mut FilmcraftApp, ctx: &egui::Context, id: &str, mut params: Value) -> Result<Value, String> {- if id == "edit.selectAll" && (crate::panels::graphics::select_all_edited_text(app) || crate::panels::effect_controls::select_all_keyframes(app)) {+ if id == "edit.selectAll"+ && (select_all_in_text_field(ctx) || crate::panels::graphics::select_all_edited_text(app) || crate::panels::effect_controls::select_all_keyframes(app))+ { return Ok(Value::Null); } if filmcraft_engine::source_monitor::source_command(id) && targets_source(app, ¶ms) && params.get("target").is_none() {@@ -554,6 +554,34 @@ fn select_all_takes_only_the_shown_bin() { assert_eq!(sorted(&d.app().session.state.project_selection), footage); } +/// Select All while typing in the Project panel's search selects the typed text, not the clips: on+/// macOS the native menu takes Cmd+A before the field sees the key and sent Edit ▸ Select All,+/// which selected every clip on the timeline. Typing after it replaces the search.+#[test]+fn select_all_in_the_search_box_selects_its_text() {+ let mut d = project_driver();+ d.click("project.search");+ d.ok("ui.type", json!({"text": "comedy manager"}));+ d.frames(2);+ let (timeline, items) = (d.app().session.state.selection.clone(), d.app().session.state.project_selection.clone());+ let (tx, rx) = channel();+ d.app().command_inbox = Some(rx);+ tx.send("edit.selectAll".to_string()).unwrap();+ d.frames(2);+ assert_eq!(d.app().session.state.selection, timeline, "the timeline selection is left alone");+ assert_eq!(d.app().session.state.project_selection, items, "and so are the Project panel's items");+ d.ok("ui.type", json!({"text": "club"}));+ d.frames(2);+ assert_eq!(d.app().ui.project_search, "club", "the typed text replaced the selected search");+ // the same from the control channel, which runs after the field has been laid out+ d.ok("ui.menu.invoke", json!({"id": "edit.selectAll"}));+ d.frames(2);+ assert_eq!(d.app().session.state.selection, timeline);+ d.ok("ui.type", json!({"text": "stool"}));+ d.frames(2);+ assert_eq!(d.app().ui.project_search, "stool");+}+ #[test] fn a_dragged_bin_nests_in_another_and_back_out() { // #456: a bin stayed wherever it was created; dragging it onto another bin did nothing