mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
#878: context menus act on the whole selection
Problem: with several layers selected, right-clicking one of them in the Timeline (name row or layer bar) and choosing Pre-compose, Delete, Duplicate, ... re-selected and acted on only the right-clicked layer. Right-clicking an unselected layer did not select it. In the Project panel, Move to Project Root moved only the right-clicked item. Fix: as in After Effects, a right-click on a selected layer keeps the selection and the menu's commands target every selected layer (in comp order); a right-click on an unselected layer selects only it. Move to Project Root uses the same selection targets as Delete and Label. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
67434e9038
commit
1b7f08198f
3 files changed
+92
-5
No files matched your search
@@ -592,7 +592,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
ui.close();
|
||||
}
|
||||
if it.parent.is_some() && ui.button(crate::i18n::tr("Move to Project Root")).clicked() {
|
||||
actions.push(("project.move".into(), json!({"items": [id.0], "folder": null})));
|
||||
actions.push(("project.move".into(), json!({"items": targets(app, *id, selected), "folder": null})));
|
||||
ui.close();
|
||||
}
|
||||
if !it.is_folder() && ui.button(crate::i18n::tr("Add to Composition")).clicked() {
|
||||
|
||||
@@ -1879,7 +1879,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
_ => start_rename(&ctx, layer.id.0, &layer.name),
|
||||
}
|
||||
}
|
||||
layer_context_menu(&row_resp, layer, &mut actions);
|
||||
layer_context_menu(&row_resp, &comp, layer, &selected, &mut actions);
|
||||
// Switches.
|
||||
let sws: [(Icon, bool, &str, bool); 8] = [
|
||||
(Icon::Shy, sw.shy, "shy", true),
|
||||
@@ -2097,7 +2097,7 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
set_anchor(&ctx, cid, layer.id);
|
||||
}
|
||||
}
|
||||
layer_context_menu(&body, layer, &mut actions);
|
||||
layer_context_menu(&body, &comp, layer, &selected, &mut actions);
|
||||
let drag_key = format!("bar-{}", layer.id.0);
|
||||
for (resp, kind) in [(&body, "move"), (&lin, "in"), (&lout, "out")] {
|
||||
if resp.dragged() {
|
||||
@@ -3442,8 +3442,16 @@ fn prop_menu(
|
||||
item(ui, "essentialGraphics", "Add Property to Essential Graphics", false, "essential.addProperty", json!({"comp": comp.0, "layer": l, "prop": uid}));
|
||||
}
|
||||
|
||||
fn layer_context_menu(resp: &egui::Response, layer: &Layer, actions: &mut Vec<(String, serde_json::Value)>) {
|
||||
resp.context_menu(|ui| layer_menu(ui, &[layer.id.0], actions));
|
||||
/// A layer row's or bar's context menu. As in After Effects, right-clicking a layer that is part
|
||||
/// of the selection keeps the selection and the menu acts on all of it; right-clicking an
|
||||
/// unselected layer selects only that layer.
|
||||
fn layer_context_menu(resp: &egui::Response, comp: &Comp, layer: &Layer, selected: &[LayerId], actions: &mut Vec<(String, serde_json::Value)>) {
|
||||
let in_selection = selected.contains(&layer.id);
|
||||
if resp.secondary_clicked() && !in_selection && !layer.switches.locked {
|
||||
actions.push(("layer.select".into(), json!({"layers": [layer.id.0]})));
|
||||
}
|
||||
let targets: Vec<u64> = if in_selection { comp.layers.iter().filter(|l| selected.contains(&l.id)).map(|l| l.id.0).collect() } else { vec![layer.id.0] };
|
||||
resp.context_menu(|ui| layer_menu(ui, &targets, actions));
|
||||
}
|
||||
|
||||
/// Shared layer actions for Timeline rows and Composition context menus.
|
||||
|
||||
@@ -430,3 +430,82 @@ fn timeline_selected_effect_row_highlights() {
|
||||
assert!(h.state().session.state.selected_props.contains(&(id, fx)), "the click selects the effect");
|
||||
assert!(highlighted(&h), "the selected effect's row is highlighted");
|
||||
}
|
||||
|
||||
/// Right-clicking a layer in the Timeline keeps the selection when the layer is part of it,
|
||||
/// so the context menu's commands act on every selected layer; right-clicking an unselected
|
||||
/// layer selects only it (After Effects). Reported on Discord: Pre-compose and Delete from the
|
||||
/// menu acted on the right-clicked layer only (#878).
|
||||
#[test]
|
||||
fn timeline_context_menu_acts_on_the_whole_selection() {
|
||||
let right_click = |h: &mut Harness<'_, EffectcraftApp>, p: Pos2| {
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(3);
|
||||
};
|
||||
let row = |h: &Harness<'_, EffectcraftApp>, id: LayerId| {
|
||||
let r = rect(h, &format!("timeline.layer.{}.row", id.0));
|
||||
pos2(r.min.x + 210.0, r.center().y)
|
||||
};
|
||||
let bar = |h: &Harness<'_, EffectcraftApp>, id: LayerId| rect(h, &format!("timeline.layer.{}.bar", id.0)).center();
|
||||
let mut h = harness();
|
||||
let ids: Vec<LayerId> = h.state().session.active_comp().unwrap().layers.iter().map(|l| l.id).collect();
|
||||
// Outline row: Delete removes both selected layers.
|
||||
h.state_mut().session.execute("layer.select", json!({"layers":[ids[0].0, ids[1].0]})).unwrap();
|
||||
h.run_steps(3);
|
||||
let p = row(&h, ids[1]);
|
||||
right_click(&mut h, p);
|
||||
assert_eq!(h.state().session.state.selected_layers, vec![ids[0], ids[1]]);
|
||||
h.get_by_label("Delete").click();
|
||||
h.run_steps(3);
|
||||
let left: Vec<LayerId> = h.state().session.active_comp().unwrap().layers.iter().map(|l| l.id).collect();
|
||||
assert_eq!(left, vec![ids[2], ids[3]]);
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
// Layer bar: same, with Duplicate.
|
||||
h.state_mut().session.execute("layer.select", json!({"layers":[ids[2].0, ids[3].0]})).unwrap();
|
||||
h.run_steps(3);
|
||||
let p = bar(&h, ids[3]);
|
||||
right_click(&mut h, p);
|
||||
h.get_by_label("Duplicate").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().session.active_comp().unwrap().layers.len(), 6);
|
||||
h.state_mut().session.execute("edit.undo", json!({})).unwrap();
|
||||
// An unselected row: right-clicking selects only it.
|
||||
h.state_mut().session.execute("layer.select", json!({"layers":[ids[0].0, ids[1].0]})).unwrap();
|
||||
h.run_steps(3);
|
||||
let p = row(&h, ids[3]);
|
||||
right_click(&mut h, p);
|
||||
assert_eq!(h.state().session.state.selected_layers, vec![ids[3]]);
|
||||
}
|
||||
|
||||
/// The Project panel's Move to Project Root acts on every selected item when the right-clicked
|
||||
/// item is part of the selection, as Delete and Label do.
|
||||
#[test]
|
||||
fn project_context_move_to_root_acts_on_the_whole_selection() {
|
||||
let mut h = harness();
|
||||
let solids: Vec<u64> = {
|
||||
let p = &h.state().session.project;
|
||||
p.items.values().filter(|i| matches!(i.kind, effectcraft_engine::project::ItemKind::Solid(_))).map(|i| i.id.0).collect()
|
||||
};
|
||||
assert!(solids.len() >= 2);
|
||||
let folder = h.state_mut().session.execute("project.newFolder", json!({"name":"Bin"})).unwrap()["item"].as_u64().unwrap();
|
||||
h.state_mut().session.execute("project.move", json!({"items": solids, "folder": folder})).unwrap();
|
||||
h.state_mut().ui.project_open_folders.insert(folder);
|
||||
h.state_mut().session.execute("project.select", json!({"items": [solids[0], solids[1]]})).unwrap();
|
||||
h.run_steps(3);
|
||||
let p = rect(&h, &format!("project.item.{}.name", solids[1])).center();
|
||||
for pressed in [true, false] {
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: p, button: PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(3);
|
||||
h.get_by_label("Move to Project Root").click();
|
||||
h.run_steps(3);
|
||||
let project = &h.state().session.project;
|
||||
for id in &solids[..2] {
|
||||
assert_eq!(project.item(effectcraft_engine::project::ItemId(*id)).unwrap().parent, None);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user