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();