Merge pull request #255 from storytold/fix/252-key-selection-props

Timeline: a selected key selects its property and layer (#252)
This commit is contained in:
BFlat authored and GitHub committed 2026-10-08 16:20:15 +07:00
commit c486130a9b
3 files changed
+38 -3

No files matched your search

+19 -1
View File
@@ -387,6 +387,24 @@ fn select_keys(s: &mut Session, p: &Value) -> Result<Value> {
} 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<CommandSpec> {
"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
),
+2 -2
View File
@@ -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.
+17
View File
@@ -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();