Timeline: the label swatch and the Modes column act on the selection (#1849)

The label swatch's menu, the Blending Mode menu and Preserve Transparency
passed the clicked layer alone, so a colour or a mode had to be set one layer
at a time. They now act on the whole selection, with the Modes column edits
going only to the selected layers that have video, as its cells do.

The preserve-transparency checkbox also gets an automation id, like the cells
around it.
This commit is contained in:
Camille Moine
2026-10-19 09:55:24 +02:00
parent bf7aff11f8
commit 16d43346c4
2 changed files with 60 additions and 5 deletions
+10 -3
View File
@@ -1341,6 +1341,12 @@ fn switch_targets(comp: &Comp, project: &Project, selected: &[LayerId], layer: u
edited_layers(LayerId(layer), selected).into_iter().filter(|l| comp.layer(LayerId(*l)).is_some_and(|l| switch_applies(l, name, project))).collect()
}
/// The layers a Modes column edit on `layer`'s row (its blending mode, Preserve Transparency)
/// acts on: [`edited_layers`] that have video.
fn mode_targets(comp: &Comp, layer: LayerId, selected: &[LayerId]) -> Vec<u64> {
edited_layers(layer, selected).into_iter().filter(|l| comp.layer(LayerId(*l)).is_some_and(|l| l.source.is_av())).collect()
}
/// A layer's switch (A/V Features and Switches columns) in `row`, with the
/// [`switch_gesture`].
#[allow(clippy::too_many_arguments)]
@@ -2077,7 +2083,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
widgets::open_popup(ui, pop);
}
if let Some(lab) = widgets::label_popup(ui, pop, lr.left_bottom(), layer.label, |l| app.session.prefs.label_name(l), &t) {
actions.push(("edit.label".into(), json!({"layers": [layer.id.0], "label": lab.name()})));
actions.push(("edit.label".into(), json!({"layers": edited_layers(layer.id, &selected), "label": lab.name()})));
}
}
if vis.num {
@@ -2230,11 +2236,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
.flat_map(|m| if m.ends_group() { vec![m.label().to_string(), "-".to_string()] } else { vec![m.label().to_string()] })
.collect();
if let Some(i) = widgets::popup_menu(ui, pop, mr.left_bottom(), &opts, opts.iter().position(|o| o == layer.blend_mode.label())) {
actions.push(("layer.setBlendMode".into(), json!({"layers": [layer.id.0], "mode": opts[i]})));
actions.push(("layer.setBlendMode".into(), json!({"layers": mode_targets(&comp, layer.id, &selected), "mode": opts[i]})));
}
let tr = Rect::from_center_size(pos2(cw.mode + 76.0 + 6.0, cy), vec2(14.0, 14.0));
app.auto.add(&format!("timeline.layer.{}.preserveTransparency", layer.id.0), tr, "Preserve Transparency");
if widgets::checkbox(ui, tr, layer.preserve_transparency, &t, egui::Id::new(("pt", layer.id.0))).clicked() {
actions.push(("layer.setSwitch".into(), json!({"layers": [layer.id.0], "switch": "preserveTransparency"})));
actions.push(("layer.setSwitch".into(), json!({"layers": mode_targets(&comp, layer.id, &selected), "switch": "preserveTransparency"})));
}
// Track matte: layer picker + kind.
let mr = Rect::from_min_size(pos2(cw.trkmat + 2.0, cy - 9.0), vec2(104.0, 18.0));
+50 -2
View File
@@ -1,12 +1,15 @@
//! A Timeline switch clicked on one of several selected layers applies to the whole selection
//! (#1849), as in After Effects. The engine commands already took several layers; the row
//! A Timeline row control clicked on one of several selected layers applies to the whole
//! selection (#1849), as in After Effects: the A/V Features and Switches columns, the label
//! swatch and the Modes column. The engine commands already took several layers; the row
//! controls used to pass the clicked layer alone.
use effectcraft_engine::Session;
use effectcraft_engine::color::{BlendMode, Label};
use effectcraft_engine::project::{Layer, LayerId, Switches};
use effectcraft_ui_egui::EffectcraftApp;
use egui::{Event, Modifiers, Pos2, Rect, pos2, vec2};
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use serde_json::json;
/// Comp "Main" with solids A (top) to D (bottom); returns their ids top to bottom.
@@ -49,6 +52,11 @@ fn click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
drag(h, p, p, 1);
}
/// A click at `p` (a menu entry, found by its label).
fn click_at(h: &mut Harness<'_, EffectcraftApp>, p: Pos2) {
drag(h, p, p, 1);
}
fn alt_click(h: &mut Harness<'_, EffectcraftApp>, id: &str) {
let p = rect(h, id).center();
let modifiers = Modifiers::ALT;
@@ -102,6 +110,10 @@ fn motion_blur(l: &Layer) -> bool {
l.switches.motion_blur
}
fn preserved(l: &Layer) -> bool {
l.preserve_transparency
}
/// #1849: a click on the Lock, 3D, Solo, Shy or Motion Blur switch of a selected layer gives the
/// switch to every selected layer, in one undo step.
#[test]
@@ -177,3 +189,39 @@ fn alt_click_sets_the_selected_layers_apart() {
alt_click(&mut h, &format!("timeline.layer.{}.video", ids[0]));
assert_eq!(states(&h, &ids, video), [true; 4]);
}
/// The label swatch menu of a selected layer sets the label of every selected layer.
#[test]
fn the_label_menu_sets_the_selections_label() {
let (mut h, ids) = harness();
select(&mut h, &[ids[0], ids[2]]);
let green = h.state().session.prefs.label_name(Label::Green);
click(&mut h, &format!("timeline.layer.{}.label", ids[0]));
let entry = h.query_by_label(&green).expect("the label menu is open").rect();
click_at(&mut h, entry.center());
let label = |id: u64| h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().label;
assert_eq!([label(ids[0]), label(ids[1]), label(ids[2]), label(ids[3])], [Label::Green, Label::Red, Label::Green, Label::Red]);
}
/// The Modes column: the Blending Mode menu and the Preserve Transparency checkbox of a selected
/// layer set both selected layers, one undo step each.
#[test]
fn the_modes_column_sets_the_whole_selection() {
let (mut h, ids) = harness();
h.state_mut().ui.timeline.show_modes = true;
h.run_steps(3);
select(&mut h, &[ids[0], ids[2]]);
click(&mut h, &format!("timeline.layer.{}.mode", ids[0]));
let entry = h.query_by_label("Multiply").expect("the blend mode menu is open").rect();
click_at(&mut h, entry.center());
let mode = |id: u64| h.state().session.active_comp().unwrap().layer(LayerId(id)).unwrap().blend_mode;
assert_eq!([mode(ids[0]), mode(ids[1]), mode(ids[2]), mode(ids[3])], [BlendMode::Multiply, BlendMode::Normal, BlendMode::Multiply, BlendMode::Normal]);
click(&mut h, &format!("timeline.layer.{}.preserveTransparency", ids[0]));
assert_eq!(states(&h, &ids, preserved), [true, false, true, false]);
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
h.run_steps(2);
assert_eq!(states(&h, &ids, preserved), [false; 4], "one undo step put both back");
}