From 8d5e4ae0ef33b65cfc03e1b390bf012bcff8a6c0 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Thu, 8 Oct 2026 16:19:53 +0700 Subject: [PATCH] Timeline: a selected key selects its property and layer (#252) Clicking (or marquee-selecting) keyframes in the Timeline selected only the keys. Their property and layer stayed unselected, so the Graph Editor, which shows the selected properties or else the animated properties of the selected layers, came up empty until the property was selected separately. As in After Effects, the Timeline's key selection now selects the keys' properties and layers too (Shift adds to them). keys.select takes this as `selectProperties`; the Graph Editor's own key clicks leave what it shows alone. Test: a_key_clicked_in_the_timeline_shows_in_the_graph_editor (ui_keyframes). Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/commands/prop.rs | 20 +++++++++++++++++++- crates/ui-egui/src/panels/timeline.rs | 4 ++-- crates/ui-egui/tests/ui_keyframes.rs | 17 +++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/engine/src/commands/prop.rs b/crates/engine/src/commands/prop.rs index 6e82fc2a..7babf85d 100644 --- a/crates/engine/src/commands/prop.rs +++ b/crates/engine/src/commands/prop.rs @@ -387,6 +387,24 @@ fn select_keys(s: &mut Session, p: &Value) -> Result { } else { s.state.selected_keys = sel; } + // From the Timeline, selected keys select their properties and layers too, as in After + // Effects, so the Graph Editor shows them (#252). + if b_p(p, "selectProperties").unwrap_or(false) && !s.state.selected_keys.is_empty() { + let extend = b_p(p, "add").unwrap_or(false) || b_p(p, "toggle").unwrap_or(false); + let st = &mut s.state; + if !extend { + st.selected_props.clear(); + st.selected_layers.clear(); + } + for k in &st.selected_keys { + if !st.selected_props.contains(&(k.layer, k.prop)) { + st.selected_props.push((k.layer, k.prop)); + } + if !st.selected_layers.contains(&k.layer) { + st.selected_layers.push(k.layer); + } + } + } Ok(json!(s.state.selected_keys.len())) } @@ -780,7 +798,7 @@ pub fn specs() -> Vec { "Select Keyframes", [], None, - "{keys: [{layer, prop: uid | path (or `path`), time (layer s)}], add?, toggle?: bool (Shift+click: in or out of the selection)}", + "{keys: [{layer, prop: uid | path (or `path`), time (layer s)}], add?, toggle?: bool (Shift+click: in or out of the selection), selectProperties?: bool (their properties and layers too, as a Timeline click does)}", has_comp, select_keys ), diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 341decdd..9cc575b4 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -2094,7 +2094,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { let kresp = ui.interact(kr, egui::Id::new(("key", uid, k.time.0)), Sense::click_and_drag()); app.auto.add(&format!("timeline.key.{uid}.{}", fr.frame_at(ct)), kr, &format!("{} key", prop.name)); let mods = ui.input(|i| i.modifiers); - let this_key = json!({"keys": [{"layer": layer.id.0, "prop": uid, "time": k.time.seconds()}]}); + let this_key = json!({"keys": [{"layer": layer.id.0, "prop": uid, "time": k.time.seconds()}], "selectProperties": true}); if kresp.clicked() && mods.command { // Ctrl+click: Linear ↔ Auto Bezier; Ctrl+Alt+click: Hold on / off. actions.push(("keys.select".into(), this_key.clone())); @@ -2375,7 +2375,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } } } - actions.push(("keys.select".into(), json!({"keys": keys, "add": ui.input(|i| i.modifiers.shift)}))); + actions.push(("keys.select".into(), json!({"keys": keys, "add": ui.input(|i| i.modifiers.shift), "selectProperties": true}))); } // CTI. diff --git a/crates/ui-egui/tests/ui_keyframes.rs b/crates/ui-egui/tests/ui_keyframes.rs index 55b2ab75..7c51820c 100644 --- a/crates/ui-egui/tests/ui_keyframes.rs +++ b/crates/ui-egui/tests/ui_keyframes.rs @@ -148,6 +148,23 @@ fn shift_click_toggles_and_ctrl_click_switches_interpolation() { assert_eq!(key(&h).out_interp, effectcraft_engine::keyframe::Interp::Hold); } +/// Clicking a key in the Timeline selects its property and layer too, as in After Effects, so +/// the Graph Editor shows the key's curve (#252). +#[test] +fn a_key_clicked_in_the_timeline_shows_in_the_graph_editor() { + let (mut h, id, uid) = harness(); + h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap(); + h.run_steps(2); + let ks = keys(&h, uid); + click_with(&mut h, ks[1], Default::default()); + let st = &h.state().session.state; + assert_eq!(st.selected_keys.len(), 1); + assert_eq!((st.selected_props.clone(), st.selected_layers.clone()), (vec![(id, uid)], vec![id]), "the key's property and layer"); + h.state_mut().ui.timeline.graph_editor = true; + h.run_steps(4); + assert!(h.state().auto.find(&format!("timeline.graph.key.{uid}.0.1")).is_some(), "the Graph Editor shows the key"); +} + #[test] fn graph_editor_drags_move_every_selected_key_and_shift_keeps_an_axis() { let (mut h, id, uid) = harness();