Merge pull request #2628 from storytold/fix/2552-timeline-group-multiselect

Timeline: Shift-click adds effects and other groups to the selection (#2552)
This commit is contained in:
BFlat
2026-10-30 07:45:22 +07:00
committed by GitHub
2 changed files with 48 additions and 1 deletions
+9 -1
View File
@@ -3591,7 +3591,15 @@ fn group_row_click(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &mut Rw, rc:
}
app.auto.add(&format!("timeline.group.{uid}.name"), gr_rect, name);
if gr.clicked() && uid & (WAVE_BIT | GEOM_BIT) == 0 {
actions.push(("prop.select".into(), json!({"layer": layer.id.0, "prop": uid, "selectKeys": false})));
// Shift or Cmd/Ctrl adds the group to the selection, or takes a selected one out, as
// property rows and Effect Controls headers do (#2552: several effects and styles for
// one animation preset).
let add = ui.input(|i| i.modifiers.shift || i.modifiers.command);
if add && app.session.state.selected_props.contains(&(layer.id, *uid)) {
actions.push(("prop.deselect".into(), json!({"layer": layer.id.0, "prop": uid})));
} else {
actions.push(("prop.select".into(), json!({"layer": layer.id.0, "prop": uid, "selectKeys": false, "add": add})));
}
}
if gr.double_clicked() {
if fx.is_some() {
@@ -448,3 +448,42 @@ fn effects_drag_to_a_new_place_in_the_timeline() {
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
assert_eq!(names(&h), ["Tint", "Gaussian Blur", "Invert"], "one undo step per drop");
}
/// #2552: Shift-clicking effect rows in the Timeline adds them to the selection (and a second
/// Shift-click takes one out), so one animation preset can hold several effects.
#[test]
fn shift_click_selects_several_effects_for_a_preset() {
let mut s = session();
let l = s.execute("layer.newSolid", json!({"name": "Plate", "color": "#406080"})).unwrap()["layer"].as_u64().unwrap();
for fx in ["Gaussian Blur", "Invert"] {
s.execute("effect.apply", json!({"layers": [l], "effect": fx})).unwrap();
}
let effects = s.active_comp().unwrap().layer(LayerId(l)).unwrap().props.sub("effects").unwrap().clone();
let uids: Vec<u64> = effects.groups().map(|g| g.uid).collect();
let mut h = harness(s);
h.state_mut().ui.timeline.open_groups.insert(effects.uid);
open_layer(&mut h, l);
let shift_click = |h: &mut Harness<'_, EffectcraftApp>, uid: u64| {
let p = center(h, &format!("timeline.group.{uid}.name"));
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::SHIFT));
h.input_mut().events.push(Event::PointerMoved(p));
h.step();
for pressed in [true, false] {
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Primary, pressed, modifiers: Modifiers::SHIFT });
h.step();
}
h.input_mut().events.push(Event::ModifiersChanged(Modifiers::NONE));
h.run_steps(2);
};
let selected = |h: &Harness<'_, EffectcraftApp>| h.state().session.state.selected_props.iter().map(|(_, u)| *u).collect::<Vec<_>>();
click(&mut h, &format!("timeline.group.{}.name", uids[0]));
assert_eq!(selected(&h), [uids[0]]);
shift_click(&mut h, uids[1]);
assert_eq!(selected(&h), [uids[0], uids[1]], "Shift adds the second effect");
let path = std::env::temp_dir().join(format!("ec-2552-{}.ffx", std::process::id()));
let r = h.state_mut().session.execute("anim.savePreset", json!({"path": path.to_string_lossy()})).unwrap();
assert_eq!(r["effects"], 2, "{r}");
let _ = std::fs::remove_file(&path);
shift_click(&mut h, uids[1]);
assert_eq!(selected(&h), [uids[0]], "a second Shift-click takes it out");
}