Project panel: footage items' menu has Interpret Footage, Replace and Reload Footage (#290)
Right-clicking imported footage offered no way to change its interpretation. As in After Effects, right-clicking an item now selects it, and footage items' menu has the File menu's footage commands: Interpret Footage (Main, Proxy, Remember / Apply Interpretation), Replace Footage, Reload Footage and Reveal in Explorer / Finder, with the menu bar's labels, shortcuts and enabled state (menus::submenu_at / entry_for). The panel's Interpret Footage button, which only ever said to select footage, opens the dialog for selected footage. Test: footage_context_menu_interprets_the_footage (ui_project_rows). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
2f3a507f8e
commit
0f44588bbd
|
|
@ -1668,6 +1668,38 @@ fn menu_nodes(app: &mut EffectcraftApp, ui: &mut egui::Ui, nodes: &[MenuNode], c
|
|||
}
|
||||
}
|
||||
|
||||
/// The menu bar's submenu at `path` (`["File", "Interpret Footage"]`) in a panel's context menu,
|
||||
/// with the menu bar's labels, shortcuts and enabled state; a chosen entry goes to `clicked`.
|
||||
pub(crate) fn submenu_at(app: &mut EffectcraftApp, ui: &mut egui::Ui, path: &[&str], clicked: &mut Option<(String, Value)>) {
|
||||
let mut nodes = effectcraft_engine::menus::menu_bar();
|
||||
let mut found = None;
|
||||
for label in path {
|
||||
let Some(n) = nodes.iter().find(|n| matches!(n, MenuNode::Submenu { label: l, .. } if l == label)) else { return };
|
||||
if let MenuNode::Submenu { children, .. } = n {
|
||||
nodes = children;
|
||||
}
|
||||
found = Some(n);
|
||||
}
|
||||
if let Some(n) = found {
|
||||
menu_nodes(app, ui, std::slice::from_ref(n), clicked);
|
||||
}
|
||||
if let Some((_, p)) = clicked.as_mut()
|
||||
&& p.is_null()
|
||||
{
|
||||
*p = json!({});
|
||||
}
|
||||
}
|
||||
|
||||
/// The menu bar's entry for `command` in a panel's context menu (see [`submenu_at`]).
|
||||
pub(crate) fn entry_for(app: &EffectcraftApp, ui: &mut egui::Ui, command: &str, clicked: &mut Option<(String, Value)>) {
|
||||
if let Some((_, e)) = effectcraft_engine::menus::entries().into_iter().find(|(_, e)| e.command == command)
|
||||
&& menu_entry(app, ui, e)
|
||||
{
|
||||
*clicked = Some((e.command.clone(), e.params_or_empty()));
|
||||
ui.close();
|
||||
}
|
||||
}
|
||||
|
||||
/// Frontend state for dynamic menus (the current workspace and the saved ones).
|
||||
pub(crate) fn dyn_ctx<'a>(workspace: &'a str, saved_workspaces: &'a [String]) -> effectcraft_engine::menus::DynCtx<'a> {
|
||||
effectcraft_engine::menus::DynCtx { workspace: Some(workspace), saved_workspaces }
|
||||
|
|
|
|||
|
|
@ -572,6 +572,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
if resp.drag_started() {
|
||||
egui::DragAndDrop::set_payload(&ctx, DragPayload::Item(id.0));
|
||||
}
|
||||
// Right-clicking an item selects it (After Effects): the menu's File commands act on the
|
||||
// selection.
|
||||
if resp.secondary_clicked() && !selected {
|
||||
app.session.state.project_selection = vec![*id];
|
||||
}
|
||||
let footage = matches!(it.kind, ItemKind::Footage(_));
|
||||
resp.context_menu(|ui| {
|
||||
if ui.button("Delete").clicked() {
|
||||
actions.push(("project.delete".into(), json!({"items": targets(app, *id, selected)})));
|
||||
|
|
@ -610,6 +616,15 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
}
|
||||
}
|
||||
});
|
||||
// Footage: the File menu's footage commands (Interpret Footage, Replace Footage…).
|
||||
if footage {
|
||||
let mut chosen = None;
|
||||
crate::menus::submenu_at(app, ui, &["File", "Interpret Footage"], &mut chosen);
|
||||
crate::menus::submenu_at(app, ui, &["File", "Replace Footage"], &mut chosen);
|
||||
crate::menus::entry_for(app, ui, "file.reloadFootage", &mut chosen);
|
||||
crate::menus::entry_for(app, ui, "file.revealInFinder", &mut chosen);
|
||||
actions.extend(chosen);
|
||||
}
|
||||
});
|
||||
}
|
||||
// The scroll bars, over the rows (which would take their presses otherwise).
|
||||
|
|
@ -680,9 +695,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
|||
let resp = widgets::icon_button(ui, r, icon, false, &t, egui::Id::new(("pfoot", id))).on_hover_text(tip);
|
||||
app.auto.add(&format!("project.{id}"), r, tip);
|
||||
if resp.clicked() {
|
||||
let footage =
|
||||
app.session.state.project_selection.first().and_then(|i| app.session.project.item(*i)).is_some_and(|i| matches!(i.kind, ItemKind::Footage(_)));
|
||||
match id {
|
||||
"newComp" => crate::panels::dialogs::open_new_comp(app),
|
||||
"newFolder" => actions.push(("project.newFolder".into(), json!({}))),
|
||||
_ if footage => actions.push(("file.interpretFootage".into(), json!({}))),
|
||||
_ => app.ui.status = "Interpret Footage: select a footage item".into(),
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@
|
|||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_engine::color::Label;
|
||||
use effectcraft_engine::project::{ItemId, ItemKind};
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use effectcraft_engine::project::{Footage, FootageKind, ItemId, ItemKind};
|
||||
use effectcraft_ui_egui::{Dialog, EffectcraftApp};
|
||||
use egui::{Color32, Event, Modifiers, Pos2, Rect, pos2};
|
||||
use egui_kittest::Harness;
|
||||
use egui_kittest::kittest::Queryable;
|
||||
|
|
@ -110,6 +110,44 @@ fn label_swatch_opens_the_label_menu() {
|
|||
assert!(h.query_by_label(&green).is_none(), "the menu closed after the choice");
|
||||
}
|
||||
|
||||
/// Right-clicking a footage item selects it and offers the File menu's footage commands:
|
||||
/// Interpret Footage ▸ Main... opens the Interpret Footage dialog on it (#290). A solid has none.
|
||||
#[test]
|
||||
fn footage_context_menu_interprets_the_footage() {
|
||||
let (mut h, _, _, solid) = harness();
|
||||
let footage = Footage { kind: FootageKind::Video, path: "clip.mp4".into(), width: 64, height: 36, has_video: true, ..Default::default() };
|
||||
let clip = std::sync::Arc::make_mut(&mut h.state_mut().session.project).add_item("Clip", Default::default(), None, ItemKind::Footage(footage));
|
||||
h.run_steps(2);
|
||||
let right_click = |h: &mut Harness<'_, EffectcraftApp>, p: Pos2| {
|
||||
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: egui::PointerButton::Secondary, pressed, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
}
|
||||
h.run_steps(2);
|
||||
};
|
||||
let at = center(&h, &format!("project.item.{}.name", solid.0));
|
||||
right_click(&mut h, at);
|
||||
assert_eq!(h.state().session.state.project_selection, vec![solid]);
|
||||
assert!(h.query_by_label_contains("Interpret Footage").is_none(), "a solid isn't footage");
|
||||
h.input_mut().events.push(Event::Key { key: egui::Key::Escape, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(2);
|
||||
let at = center(&h, &format!("project.item.{}.name", clip.0));
|
||||
right_click(&mut h, at);
|
||||
assert_eq!(h.state().session.state.project_selection, vec![clip], "right-click selects the item");
|
||||
let sub = h.query_by_label_contains("Interpret Footage").expect("Interpret Footage in the menu").rect();
|
||||
h.input_mut().events.push(Event::PointerMoved(sub.center()));
|
||||
h.run_steps(3);
|
||||
let main = h.query_by_label_contains("Main...").expect("Interpret Footage ▸ Main...").rect();
|
||||
for k in 0..=6 {
|
||||
h.input_mut().events.push(Event::PointerMoved(pos2(main.center().x, sub.center().y + (main.center().y - sub.center().y) * k as f32 / 6.0)));
|
||||
h.step();
|
||||
}
|
||||
click_at(&mut h, main.center());
|
||||
assert_eq!(h.state().dialog, Some(Dialog::Form), "the Interpret Footage dialog opened");
|
||||
}
|
||||
|
||||
/// Dragging from the Project panel's empty area draws a selection box that selects the rows it
|
||||
/// touches; a click there deselects (#203).
|
||||
#[test]
|
||||
|
|
|
|||
Loading…
Reference in New Issue