Merge pull request #1017 from marcoarnulfo/feat/layer-context-add-marker

# Conflicts:
#	crates/ui-egui/tests/ui_blank_context_menus.rs
This commit is contained in:
Brandon Thomas 2026-10-13 04:29:11 -04:00
commit c4a162b2dc
5 changed files with 69 additions and 1 deletions

View File

@ -305,6 +305,7 @@ const JAPANESE: &[(&str, &str)] = &[
("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OR Apache-2.0 • フォント:Inter、JetBrains Mono (OFL)"),
("Magnification ratio popup", "拡大率のポップアップ"),
("Make Comp Size", "コンポジションサイズにする"),
("Markers", "マーカー"),
("Masks", "マスク"),
("Maximize Panel", "パネルを最大化"),
("Media Replacement", "メディアの置き換え"),
@ -841,6 +842,7 @@ const SIMPLIFIED_CHINESE: &[(&str, &str)] = &[
("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT 或 Apache-2.0 • 字体:Inter、JetBrains Mono(OFL)"),
("Magnification ratio popup", "放大倍率弹出菜单"),
("Make Comp Size", "设为合成长宽"),
("Markers", "标记"),
("Masks", "蒙版"),
("Maximize Panel", "最大化面板"),
("Media Replacement", "媒体替换"),
@ -1565,6 +1567,7 @@ const TRADITIONAL_CHINESE: &[(&str, &str)] = &[
("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT 或 Apache-2.0 • 字型:Inter、JetBrains Mono(OFL)"),
("Magnification ratio popup", "放大倍率快顯選單"),
("Make Comp Size", "設為合成長寬"),
("Markers", "標記"),
("Masks", "遮色片"),
("Maximize Panel", "最大化面板"),
("Media Replacement", "媒體替換"),
@ -2292,6 +2295,7 @@ const UKRAINIAN: &[(&str, &str)] = &[
("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OR Apache-2.0 • Шрифти: Inter, JetBrains Mono (OFL)"),
("Magnification ratio popup", "Меню коефіцієнта збільшення"),
("Make Comp Size", "За розміром композиції"),
("Markers", "Маркери"),
("Masks", "Маски"),
("Maximize Panel", "Розгорнути панель"),
("Media Replacement", "Заміна матеріалу"),
@ -2831,6 +2835,7 @@ const PORTUGUESE: &[(&str, &str)] = &[
("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OU Apache-2.0 • Fontes: Inter, JetBrains Mono (OFL)"),
("Magnification ratio popup", "Menu de proporção de ampliação"),
("Make Comp Size", "Ajustar ao tamanho da composição"),
("Markers", "Marcadores"),
("Masks", "Máscaras"),
("Maximize Panel", "Maximizar painel"),
("Media Replacement", "Substituição de mídia"),
@ -3277,6 +3282,7 @@ const CZECH: &[(&str, &str)] = &[
("MIT OR Apache-2.0 • Fonts: Inter, JetBrains Mono (OFL)", "MIT OR Apache-2.0 • Písma: Inter, JetBrains Mono (OFL)"),
("Magnification ratio popup", "Nabídka míry přiblížení"),
("Make Comp Size", "Nastavit velikost kompozice"),
("Markers", "Značky"),
("Masks", "Masky"),
("Maximize Panel", "Maximalizovat panel"),
("Media Replacement", "Nahrazení médií"),

View File

@ -1836,6 +1836,11 @@ fn handle_shortcuts_impl(app: &mut EffectcraftApp, ctx: &egui::Context, tab_only
// comes as text.
if !tab_only && clipboard && app.dialog.is_none() && ctx.input(|i| i.events.iter().any(|e| matches!(e, egui::Event::Text(t) if t == "*"))) {
let bound = app.session.shortcuts().bindings().into_iter().find(|(k, _)| *k == "Num*").map(|(_, b)| (b.command.clone(), b.params.clone()));
// As in After Effects, it marks the selected layers; only with none selected does it add
// a composition marker (#976).
let bound = bound.map(|(id, params)| {
if id == "comp.addMarker" && !app.session.state.selected_layers.is_empty() { ("layer.addMarker".to_owned(), params) } else { (id, params) }
});
if let Some((id, params)) = bound
&& let Err(e) = invoke(app, ctx, &id, if params.is_null() { json!({}) } else { params })
{

View File

@ -3798,6 +3798,9 @@ pub(crate) fn layer_menu(ui: &mut egui::Ui, layers: &[u64], vector: bool, action
item(ui, "Freeze Frame", "layer.freezeFrame", json!({"layers": layers}));
item(ui, "Freeze on Last Frame", "layer.freezeOnLastFrame", json!({"layers": layers}));
});
ui.menu_button(crate::i18n::tr("Markers"), |ui| {
item(ui, "Add Marker", "layer.addMarker", json!({"layers": layers}));
});
ui.menu_button(crate::i18n::tr("Blending Mode"), |ui| {
widgets::menu_scroll(ui, |ui| {
for m in BlendMode::ALL {

View File

@ -259,3 +259,32 @@ fn timeline_layer_context_menu_has_the_effects_submenu_and_applies_an_effect() {
.unwrap_or_default();
assert_eq!(names, ["Gaussian Blur"]);
}
/// #976: a layer's context menu has After Effects' Markers ▸ Add Marker, which marks the layer that
/// was right-clicked (not the one selected before, and not the composition) at the current time.
#[test]
fn layer_context_menu_adds_a_layer_marker() {
let mut h = harness();
let solid = |h: &mut Harness<'_, EffectcraftApp>, name: &str| -> u64 {
h.state_mut().session.execute("layer.newSolid", json!({"name": name, "color": "#406080"})).unwrap()["layer"].as_u64().unwrap()
};
let (below, top) = (solid(&mut h, "Below"), solid(&mut h, "Top"));
h.state_mut().session.execute("layer.select", json!({"layers": [below]})).unwrap();
h.state_mut().session.execute("time.set", json!({"time": 1.5})).unwrap();
h.run_steps(3);
let r = rect(&h, &format!("timeline.layer.{top}.row"));
right_click(&mut h, pos2(r.min.x + 210.0, r.center().y));
h.get_by_label("Markers ⏵").click();
h.run_steps(3);
snapshot(&mut h, "layer-markers");
h.get_by_label("Add Marker").click();
h.run_steps(3);
let now = h.state().session.time().seconds();
let comp = h.state().session.active_comp().unwrap();
let markers = |id: u64| -> Vec<f64> { comp.layer(effectcraft_engine::project::LayerId(id)).unwrap().markers.iter().map(|m| m.time.seconds()).collect() };
assert_eq!(markers(top), vec![now]);
assert!((now - 1.5).abs() < 0.01, "at the current time: {now}");
assert!(markers(below).is_empty(), "{:?}", markers(below));
assert!(comp.markers.is_empty(), "no composition marker");
assert_eq!(h.state().session.history.undo.last().map(|u| u.0.as_str()), Some("Add Marker"));
}

View File

@ -356,13 +356,38 @@ fn dragging_out_of_the_marker_bin_adds_a_comp_marker() {
assert_eq!(markers(&h), vec![13.0]);
let history = &h.state().session.history.undo;
assert_eq!((history.len(), history.last().map(|u| u.0.as_str())), (undo + 1, Some("Add Marker")), "one undo step");
// Numpad * (it arrives as text): Add Marker at the current time.
// Numpad * (it arrives as text) with no layer selected: Add Marker at the current time.
h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap();
h.state_mut().session.execute("time.set", json!({"time": 20.0})).unwrap();
h.event(Event::Text("*".into()));
h.run_steps(2);
assert_eq!(markers(&h), vec![13.0, 20.0]);
}
/// #976: as in After Effects, Numpad * marks the selected layers at the current time; only with
/// no layer selected does it add a composition marker.
#[test]
fn numpad_star_marks_the_selected_layer() {
let (mut h, layer) = harness();
let comp_markers =
|h: &Harness<'_, EffectcraftApp>| -> Vec<f64> { h.state().session.active_comp().unwrap().markers.iter().map(|m| m.time.seconds()).collect() };
let layer_markers = |h: &Harness<'_, EffectcraftApp>| -> Vec<f64> {
let comp = h.state().session.active_comp().unwrap();
comp.layer(effectcraft_engine::project::LayerId(layer)).unwrap().markers.iter().map(|m| m.time.seconds()).collect()
};
let star = |h: &mut Harness<'_, EffectcraftApp>, time: f64| {
h.state_mut().session.execute("time.set", json!({"time": time})).unwrap();
h.event(Event::Text("*".into()));
h.run_steps(2);
};
h.state_mut().session.execute("layer.select", json!({"layers": [layer]})).unwrap();
star(&mut h, 2.0);
assert_eq!((layer_markers(&h), comp_markers(&h)), (vec![2.0], vec![]));
h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap();
star(&mut h, 3.0);
assert_eq!((layer_markers(&h), comp_markers(&h)), (vec![2.0], vec![3.0]));
}
/// #468: zoomed in, a CTI scrub held past the right end of the ruler scrolls the timeline (the
/// CTI rides the edge), past the left end scrolls it back; during playback the view pages to
/// keep the CTI in sight.