From 319d2cba3b435a2111cf82174ffd763e26b6fb3e Mon Sep 17 00:00:00 2001 From: aiongg Date: Mon, 26 Oct 2026 21:20:36 +0000 Subject: [PATCH] refactor(ui-egui): move menu commands, dispatch, entries, shortcuts and the menu bar to menus/*.rs --- crates/ui-egui/src/i18n/ui.rs | 7 +- crates/ui-egui/src/menus.rs | 2339 +------------------------ crates/ui-egui/src/menus/bar.rs | 289 +++ crates/ui-egui/src/menus/commands.rs | 122 ++ crates/ui-egui/src/menus/entries.rs | 147 ++ crates/ui-egui/src/menus/frontend.rs | 684 ++++++++ crates/ui-egui/src/menus/invoke.rs | 692 ++++++++ crates/ui-egui/src/menus/shortcuts.rs | 397 +++++ 8 files changed, 2358 insertions(+), 2319 deletions(-) create mode 100644 crates/ui-egui/src/menus/bar.rs create mode 100644 crates/ui-egui/src/menus/commands.rs create mode 100644 crates/ui-egui/src/menus/entries.rs create mode 100644 crates/ui-egui/src/menus/frontend.rs create mode 100644 crates/ui-egui/src/menus/invoke.rs create mode 100644 crates/ui-egui/src/menus/shortcuts.rs diff --git a/crates/ui-egui/src/i18n/ui.rs b/crates/ui-egui/src/i18n/ui.rs index 63459f1da..40e78dd02 100644 --- a/crates/ui-egui/src/i18n/ui.rs +++ b/crates/ui-egui/src/i18n/ui.rs @@ -5433,7 +5433,12 @@ mod tests { ("dock.rs", include_str!("../dock.rs")), ("dock_ui.rs", include_str!("../dock_ui.rs")), ("header.rs", include_str!("../header.rs")), - ("menus.rs", include_str!("../menus.rs")), + ("menus/bar.rs", include_str!("../menus/bar.rs")), + ("menus/commands.rs", include_str!("../menus/commands.rs")), + ("menus/entries.rs", include_str!("../menus/entries.rs")), + ("menus/frontend.rs", include_str!("../menus/frontend.rs")), + ("menus/invoke.rs", include_str!("../menus/invoke.rs")), + ("menus/shortcuts.rs", include_str!("../menus/shortcuts.rs")), ("prefs_live.rs", include_str!("../prefs_live.rs")), ("widgets.rs", include_str!("../widgets.rs")), ("panels/anim_tools.rs", include_str!("../panels/anim_tools.rs")), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index d8eabcad5..cb28b0bcd 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -12,6 +12,27 @@ use crate::dock::{PanelKind, WorkspaceExt as _}; use crate::state::{Resolution, Tool}; use effectcraft_engine::menus::{MenuEntry, MenuNode}; +mod bar; +mod commands; +mod entries; +mod frontend; +mod invoke; +mod shortcuts; + +pub use bar::menu_bar; +pub(crate) use bar::{dyn_ctx, entry_for, menu_contents, submenu_at}; +pub use commands::UI_COMMANDS; +pub use entries::{MenuItem, menu_items, menus}; +pub(crate) use entries::{entry_checked, entry_enabled, entry_label, entry_shortcut, menu_choice, panel_shown}; +pub use frontend::frontend; +#[cfg(test)] +use frontend::zoom_step; +pub(crate) use frontend::{dialog_answered, run_engine}; +pub use invoke::{invoke, panel_command_id, reveal}; +#[cfg(test)] +use shortcuts::modifiers_at_start; +pub use shortcuts::{Binding, bindings, chord_matches, forwardable_during_text_edit, handle_shortcuts, handle_tab_shortcut, parse_shortcut, shortcut_text}; + pub struct UiCommand { pub id: &'static str, pub label: &'static str, @@ -19,2323 +40,5 @@ pub struct UiCommand { pub shortcut: Option<&'static str>, } -macro_rules! uic { - ($id:literal, $label:literal, [$($m:literal),*], $sc:expr) => { - UiCommand { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc } - }; -} - -/// UI-only commands (not in the AE menus; shortcuts and the command palette). -pub const UI_COMMANDS: &[UiCommand] = &[ - uic!("window.dockAction", "Docking Action", [], None), - uic!("playback.ramPreview", "Preview (Numpad 0)", [], Some("Num0")), - uic!("playback.preview.shiftSpacebar", "Preview (Shift+Spacebar)", [], Some("Shift+Space")), - uic!("playback.preview.shiftNumpad0", "Preview (Shift+Numpad 0)", [], Some("Shift+Num0")), - uic!("playback.preview.altNumpad0", "Preview (Alt+Numpad 0)", [], Some("Alt+Num0")), - uic!("playback.stop", "Stop", [], None), - uic!("view.fit", "Fit", [], Some("Shift+/")), - uic!("view.actualSize", "100%", [], Some("/")), - uic!("view.fitUpTo100", "Fit up to 100%", [], Some("Alt+/")), - uic!("view.nextItem", "Next Composition in Viewer", [], Some("Shift+.")), - uic!("view.previousItem", "Previous Composition in Viewer", [], Some("Shift+,")), - uic!("viewer.nudge.up", "Move Selected Layers Up 1 Pixel at Current Magnification", [], Some("ArrowUp")), - uic!("viewer.nudge.down", "Move Selected Layers Down 1 Pixel at Current Magnification", [], Some("ArrowDown")), - uic!("viewer.nudge.left", "Move Selected Layers Left 1 Pixel at Current Magnification", [], Some("ArrowLeft")), - uic!("viewer.nudge.right", "Move Selected Layers Right 1 Pixel at Current Magnification", [], Some("ArrowRight")), - uic!("viewer.nudge.up10", "Move Selected Layers Up 10 Pixels at Current Magnification", [], Some("Shift+ArrowUp")), - uic!("viewer.nudge.down10", "Move Selected Layers Down 10 Pixels at Current Magnification", [], Some("Shift+ArrowDown")), - uic!("viewer.nudge.left10", "Move Selected Layers Left 10 Pixels at Current Magnification", [], Some("Shift+ArrowLeft")), - uic!("viewer.nudge.right10", "Move Selected Layers Right 10 Pixels at Current Magnification", [], Some("Shift+ArrowRight")), - uic!("view.safeMargins", "Title/Action Safe", [], Some("'")), - uic!("view.transparencyGrid", "Transparency Grid", [], None), - uic!("view.fastPreviews", "Fast Previews", [], None), - uic!("view.theme.toggle", "Next Appearance Mode", [], None), - uic!("view.theme.dark", "Theme: Dark", [], None), - uic!("view.theme.darker", "Theme: Darker", [], None), - uic!("view.theme.light", "Theme: Light", [], None), - uic!("timeline.zoomIn", "Zoom In Time", [], Some("=")), - uic!("timeline.zoomOut", "Zoom Out Time", [], Some("-")), - uic!("timeline.zoomFit", "Zoom to Fit Comp", [], Some("Shift+;")), - uic!("timeline.scrollToTime", "Scroll to Current Time", [], Some("D")), - uic!("timeline.scrollToLayer", "Scroll Topmost Selected Layer to Top", [], Some("X")), - uic!("timeline.parentColumn", "Show/Hide Parent Column", [], Some("Shift+F4")), - uic!("timeline.zoomFrameToggle", "Zoom In to Frame Level / Out to the Whole Comp", [], Some(";")), - uic!("timeline.graphEditor", "Graph Editor", [], Some("Shift+F3")), - uic!("timeline.switchesModes", "Toggle Switches / Modes", [], Some("F4")), - uic!("timeline.workAreaBegin", "Set Work Area Begin", [], Some("B")), - uic!("timeline.workAreaEnd", "Set Work Area End", [], Some("N")), - uic!("timeline.moveInToTime", "Move Layer In Point to Current Time", [], Some("[")), - uic!("timeline.moveOutToTime", "Move Layer Out Point to Current Time", [], Some("]")), - uic!("timeline.trimInToTime", "Trim Layer In Point to Current Time", [], Some("Alt+[")), - uic!("timeline.trimOutToTime", "Trim Layer Out Point to Current Time", [], Some("Alt+]")), - uic!("timeline.reveal.position", "Reveal Position", [], Some("P")), - uic!("timeline.reveal.scale", "Reveal Scale", [], Some("S")), - uic!("timeline.reveal.rotation", "Reveal Rotation", [], Some("R")), - uic!("timeline.reveal.opacity", "Reveal Opacity", [], Some("T")), - uic!("timeline.reveal.anchor", "Reveal Anchor Point", [], Some("A")), - uic!("timeline.reveal.effects", "Reveal Effects", [], Some("E")), - uic!("timeline.reveal.masks", "Reveal Masks", [], Some("M")), - uic!("timeline.reveal.feather", "Reveal Mask Feather", [], Some("F")), - uic!("timeline.reveal.levels", "Reveal Audio Levels (press twice: Waveform)", [], Some("L")), - uic!("timeline.reveal.timeRemap", "Reveal Time Remap", [], None), - uic!("timeline.reveal.expressions", "Reveal Expressions", [], None), - uic!("timeline.reveal.maskOpacity", "Reveal Mask Opacity", [], None), - uic!("timeline.reveal.maskPath", "Reveal Mask Path", [], None), - uic!("timeline.reveal.material", "Reveal Material Options", [], None), - uic!("timeline.reveal.paint", "Reveal Paint, Roto Brush and Puppet", [], None), - uic!("timeline.reveal.selected", "Reveal Selected Properties", [], None), - uic!("timeline.reveal.missingEffects", "Reveal Missing Effects", [], None), - uic!("timeline.reveal.waveform", "Reveal Audio Waveform", [], None), - uic!("timeline.revealAdd.position", "Add Position to Revealed Properties", [], Some("Shift+P")), - uic!("timeline.revealAdd.scale", "Add Scale to Revealed Properties", [], Some("Shift+S")), - uic!("timeline.revealAdd.rotation", "Add Rotation to Revealed Properties", [], Some("Shift+R")), - uic!("timeline.revealAdd.opacity", "Add Opacity to Revealed Properties", [], Some("Shift+T")), - uic!("timeline.revealAdd.anchor", "Add Anchor Point to Revealed Properties", [], Some("Shift+A")), - uic!("timeline.revealAdd.effects", "Add Effects to Revealed Properties", [], Some("Shift+E")), - uic!("timeline.revealAdd.masks", "Add Masks to Revealed Properties", [], Some("Shift+M")), - uic!("timeline.revealAdd.feather", "Add Mask Feather to Revealed Properties", [], Some("Shift+F")), - uic!("timeline.revealAdd.levels", "Add Audio Levels to Revealed Properties", [], Some("Shift+L")), - uic!("timeline.revealAdd.animated", "Add Animated Properties to Revealed Properties", [], Some("Shift+U")), - uic!("timeline.column", "Show/Hide Timeline Column", [], None), - uic!("timeline.sourceName", "Source Name / Layer Name", [], None), - uic!("timeline.search", "Search Timeline", [], None), - uic!("flowchart.options", "Flowchart Options", [], None), - uic!("flowchart.graph", "Flowchart Graph", [], None), - uic!("timeline.collapseAll", "Collapse All", [], None), - uic!("timeline.twirlSelected", "Twirl Selected Layers Open / Closed", [], Some("Cmd+`")), - uic!("timeline.keyAt.anchor", "Add or Remove Anchor Point Keyframe", [], Some("Alt+Shift+A")), - uic!("timeline.keyAt.position", "Add or Remove Position Keyframe", [], Some("Alt+Shift+P")), - uic!("timeline.keyAt.scale", "Add or Remove Scale Keyframe", [], Some("Alt+Shift+S")), - uic!("timeline.keyAt.rotation", "Add or Remove Rotation Keyframe", [], Some("Alt+Shift+R")), - uic!("timeline.keyAt.opacity", "Add or Remove Opacity Keyframe", [], Some("Alt+Shift+T")), - uic!("tool.selection", "Selection Tool", [], Some("V")), - uic!("tool.hand", "Hand Tool", [], Some("H")), - uic!("tool.zoom", "Zoom Tool", [], Some("Z")), - uic!("tool.orbit", "Orbit Tool", [], Some("1")), - uic!("tool.panCamera", "Pan Camera Tool", [], Some("2")), - uic!("tool.dolly", "Dolly Tool", [], Some("3")), - uic!("tool.camera", "Camera Tools", [], Some("C")), - uic!("tool.rotate", "Rotation Tool", [], Some("W")), - uic!("tool.panBehind", "Pan Behind Tool", [], Some("Y")), - uic!("tool.shape", "Shape Tools", [], Some("Q")), - uic!("tool.pen", "Pen Tool", [], Some("G")), - uic!("tool.type", "Type Tools", [], Some("Cmd+T")), - uic!("tool.brush", "Brush Tools", [], Some("Cmd+B")), - uic!("tool.rotoBrush", "Roto Brush & Refine Edge Tools", [], Some("Alt+W")), - uic!("tool.puppet", "Puppet Tools", [], Some("Cmd+P")), - uic!("app.newComp", "New Composition...", [], None), - uic!("app.compSettings", "Composition Settings...", [], None), - uic!("app.solidSettings", "New Solid...", [], None), - uic!("app.home", "Home", [], None), - uic!("markers.dialog", "Marker Settings...", [], None), - uic!("window.maximizePanel", "Maximize Panel Under Pointer", [], Some("`")), - uic!("window.maximizeApp", "Maximize or Restore Application Window", [], Some("Cmd+\\")), - uic!("window.toggleCompTimeline", "Switch Between Composition and Timeline Panel", [], Some("\\")), - uic!("window.nextPanel", "Next Panel in Frame", [], Some("Alt+Shift+.")), - uic!("window.previousPanel", "Previous Panel in Frame", [], Some("Alt+Shift+,")), - uic!("window.closeAll", "Close Panel or All Viewers", [], Some("Cmd+Shift+W")), - uic!("window.dockPanel", "Dock Panel", [], None), - uic!("window.floatPanel", "Undock Panel", [], None), - uic!("window.closePanel", "Close Panel", [], None), - uic!("window.panelWindow", "Move Panel to New Window", [], None), -]; - -pub fn panel_command_id(p: PanelKind) -> String { - format!("window.panel.{}", p.id()) -} - -/// The reveal shortcuts (P, S, R, T, A, E, M, F, L…). `add` (Shift+key) adds the property to -/// (or removes it from) those already revealed instead of replacing them. -pub fn reveal(app: &mut EffectcraftApp, kind: &str, now: f64, add: bool) { - // The double-press shortcuts: the same key again within 0.6 s reveals its second set (LL - // Waveform, TT Mask Opacity, MM all mask properties, EE Expressions, RR Time Remap, AA - // Material Options, PP paint / Roto Brush / Puppet, SS selected properties, FF missing - // effects). A single M shows Mask Path. - let double = |k: &str| -> Option<&'static str> { - Some(match k { - "levels" => "waveform", - "opacity" => "maskOpacity", - "masks" => "masks", - "effects" => "expressions", - "rotation" => "timeRemap", - "anchor" => "material", - "position" => "paint", - "scale" => "selected", - "feather" => "missingEffects", - _ => return None, - }) - }; - let pressed = kind; - // (With Shift a second press removes the property again instead.) - let again = !add && matches!(app.last_reveal.take(), Some((k, t)) if k == pressed && now - t < 0.6); - app.last_reveal = Some((if again { String::new() } else { pressed.to_string() }, now)); - let kind = match (again, double(pressed)) { - (true, Some(d)) => { - // The second press replaces what the first one revealed. - let first = if pressed == "masks" { "maskPath" } else { pressed }; - app.ui.timeline.reveal.retain(|k| k != first); - d - } - _ if pressed == "masks" => "maskPath", - _ => pressed, - }; - let targets = reveal_targets(app); - let tl = &mut app.ui.timeline; - let present = tl.reveal.iter().any(|k| k == kind); - let mut kinds = tl.reveal.clone(); - if add { - if present { - kinds.retain(|k| k != kind); - } else { - kinds.retain(|k| k != "props" || !tl.reveal_props.is_empty()); - kinds.push(kind.to_string()); - } - } else if present && kinds.len() == 1 { - kinds.clear(); - } else { - kinds = vec![kind.to_string()]; - } - tl.apply_reveal(&targets, kinds.clone()); - crate::panels::timeline::open_revealed(app, &targets, &kinds); -} - -/// Show Time Remap on the layers of a `layer.enableTimeRemap` (`layers` by id, else the -/// selected ones) that have it now. -fn reveal_time_remap(app: &mut EffectcraftApp, params: &Value) { - let asked: Vec = match params.get("layers").and_then(Value::as_array) { - Some(a) => a.iter().filter_map(Value::as_u64).collect(), - None => app.session.state.selected_layers.iter().map(|l| l.0).collect(), - }; - let Some(comp) = app.session.active_comp() else { return }; - let layers: Vec = comp.layers.iter().filter(|l| asked.contains(&l.id.0) && l.props.get("timeRemap").is_some()).map(|l| l.id.0).collect(); - if !layers.is_empty() { - app.ui.timeline.apply_reveal(&layers, vec!["timeRemap".into()]); - } -} - -/// After Alt+Shift+P (A, S, R, T) the Timeline shows the property on each layer it keyed, as in -/// After Effects: added to what a reveal shortcut shows, else with the layer's Transform twirled -/// open, else alone like P. -fn reveal_keyed(app: &mut EffectcraftApp, keyed: &Value, kind: &str) { - let layers: std::collections::BTreeSet = keyed.as_array().into_iter().flatten().filter_map(|k| k.get("layer").and_then(Value::as_u64)).collect(); - let Some(comp) = app.session.active_comp() else { return }; - let tl = &mut app.ui.timeline; - for id in layers { - let shown = tl.layer_reveal.get(&id).filter(|k| !k.is_empty()).cloned(); - match shown { - Some(mut kinds) => { - if !kinds.iter().any(|k| k == kind) { - kinds.push(kind.to_string()); - tl.layer_reveal.insert(id, kinds); - } - } - None if tl.open_layers.contains(&id) => { - if let Some(tr) = comp.layer(effectcraft_engine::project::LayerId(id)).and_then(|l| l.transform()) { - tl.open_groups.insert(tr.uid); - } - } - None => { - tl.open_layers.insert(id); - tl.layer_reveal.insert(id, vec![kind.to_string()]); - } - } - } -} - -/// The layers a reveal shortcut acts on: the selected ones, else every layer of the active comp. -fn reveal_targets(app: &EffectcraftApp) -> Vec { - if app.session.state.selected_layers.is_empty() { - app.session.active_comp().map(|c| c.layers.iter().map(|l| l.id.0).collect()).unwrap_or_default() - } else { - app.session.state.selected_layers.iter().map(|l| l.0).collect() - } -} - -/// The arrow keys nudge the selected layers 1 pixel at the viewer's magnification (`up`, -/// `down`, `left`, `right`), 10 with Shift (`up10`…), as After Effects does, so zoomed in -/// they move by fractions of a comp pixel. From any panel except Project (which owns the -/// arrow keys for its own keyboard navigation, #961); not while typing, with a dialog or -/// menu open. With mask points selected the arrows move the points instead (#1288). -fn nudge(app: &mut EffectcraftApp, ctx: &egui::Context, dir: &str) -> Result { - let (side, n) = match dir.strip_suffix("10") { - Some(side) => (side, 10.0), - None => (dir, 1.0), - }; - let (x, y) = match side { - "up" => (0.0, -n), - "down" => (0.0, n), - "left" => (-n, 0.0), - "right" => (n, 0.0), - _ => return Err(format!("unknown nudge `{dir}`")), - }; - let st = &app.session.state; - if app.ui.focused == PanelKind::Project || (st.selected_layers.is_empty() && st.selected_vertices.is_empty()) || st.text_edit.is_some() { - return Ok(Value::Null); - } - // Screen pixels per comp pixel. - let zoom = (app.ui.viewer.zoom.unwrap_or_else(|| crate::panels::viewer::last_fit(ctx)) * ctx.pixels_per_point()) as f64; - let k = if zoom.is_finite() && zoom > 1e-6 { 1.0 / zoom } else { 1.0 }; - if let Some(layer) = st.selected_vertices.first().map(|v| v.layer) { - let d = vertex_delta(app, layer, [x * k, y * k]); - return run_engine(app, ctx, "mask.moveVertices", json!({"delta": d})); - } - run_engine(app, ctx, "layer.nudge", json!({"x": x * k, "y": y * k})) -} - -/// A comp-space step `d` in the layer space of `layer`, where mask points live. Like a drag of -/// the points in the viewer, every selected point takes the first one's layer space. -fn vertex_delta(app: &EffectcraftApp, layer: effectcraft_engine::project::LayerId, d: [f64; 2]) -> [f64; 2] { - use effectcraft_engine::geom::vec2; - let s = &app.session; - let (Some(cid), Some(comp)) = (s.active_comp_id(), s.active_comp()) else { return d }; - let Some(l) = comp.layer(layer) else { return d }; - let ectx = effectcraft_engine::render::EvalCtx { project: &s.project, comp_id: cid, comp, time: s.time(), expr: s.expr.as_deref(), footage: None }; - let (l2c, _) = crate::panels::viewer::l2c(&ectx, l); - l2c.inverse().map_or(d, |inv| { - let v = inv.apply_vec(vec2(d[0], d[1])); - [v.x, v.y] - }) -} - -fn no_params(p: &Value) -> bool { - p.as_object().is_none_or(|m| m.is_empty()) -} - -/// `{"comp": id}` and nothing else: Composition Settings opens on that comp instead of -/// applying no settings to it. -fn only_comp(p: &Value) -> Option { - let m = p.as_object().filter(|m| m.len() == 1)?; - m.get("comp")?.as_u64().map(effectcraft_engine::project::ItemId) -} - -/// Execute a UI or engine command by id. -pub fn invoke(app: &mut EffectcraftApp, ctx: &egui::Context, id: &str, params: Value) -> Result { - if matches!(id, "effects.copyPresetsFolderPath" | "mediaBrowser.copySourcePath") { - return frontend(app, ctx, id, params); - } - // Layer ▸ New ▸ Text (menu, shortcut, context menus) puts an insertion point in the centre of - // the comp, as After Effects does, so typing goes into Source Text rather than running - // shortcuts (#866). `engine.execute` keeps the headless default. - if id == "layer.newText" && no_params(¶ms) { - return run_engine(app, ctx, id, json!({"text": "", "edit": true})); - } - // A new folder (File ▸ New, its shortcut, the Project panel's button and context menu) opens - // with its name ready to type, as in After Effects (#1658). The command selects it. - // `engine.execute` keeps the headless default. - if id == "project.newFolder" && no_params(¶ms) { - let r = run_engine(app, ctx, id, params); - if r.is_ok() { - app.show_panel(PanelKind::Project); - crate::panels::project::begin_rename(app, ctx); - } - return r; - } - // The Project-panel focus redirects apply only while no text edit is open: creating a text - // layer leaves focus on Project while the insertion point lives in the comp, so during an - // edit these commands address the text context — redirected, Deselect All would clear a - // project selection the user is not looking at and silently swallow the edit's own end. - if no_params(¶ms) && app.ui.focused == PanelKind::Project && app.session.state.text_edit.is_none() { - match id { - "edit.duplicate" => return run_engine(app, ctx, "project.duplicate", json!({})), - "edit.selectAll" => { - let items: Vec = crate::panels::project::visible_rows(app) - .into_iter() - .filter(|(id, _)| app.ui.project_search.is_empty() || app.session.project.item(*id).is_some_and(|it| !it.is_folder())) - .map(|(id, _)| id.0) - .collect(); - return run_engine(app, ctx, "project.select", json!({"items":items})); - } - "edit.deselectAll" => return run_engine(app, ctx, "project.select", json!({"items":[]})), - "edit.cut" | "edit.copy" | "edit.paste" | "edit.copyWithPropertyLinks" | "edit.copyWithRelativePropertyLinks" | "edit.copyExpressionOnly" => { - return Err("Project clipboard actions are not supported yet".into()); - } - _ => {} - } - } - // Edit ▸ Paste with SVG markup on the system clipboard pastes it as a shape layer (After - // Effects 26.3). Copying in the app replaces the system clipboard with a note naming what - // was copied, so SVG there was copied after anything copied in the app. - if id == "edit.paste" && no_params(¶ms) && app.session.state.text_edit.is_none() { - let text = ctx.data_mut(|d| d.remove_temp::(system_paste_id())).or_else(|| app.hooks.clipboard_text.as_ref().and_then(|f| f())); - if let Some(svg) = text.filter(|t| effectcraft_engine::vector::is_svg_text(t)) { - return run_engine(app, ctx, "edit.pasteSvg", json!({"svg": svg})); - } - } - // Clear follows panel focus, including the Edit menu and keyboard shortcut. - if id == "edit.clear" && params.as_object().is_some_and(|p| p.is_empty()) && app.ui.focused == PanelKind::Project { - // Through `invoke`, so deleting items that compositions use still asks first (M3.16). - return invoke(app, ctx, "project.delete", json!({})); - } - if id == "edit.clear" && params.as_object().is_some_and(|p| p.is_empty()) && app.ui.focused == PanelKind::RenderQueue { - let selected = ctx.data(|d| d.get_temp::<(Vec, Option)>(egui::Id::unique("rq-ui"))).and_then(|(_, id)| id); - return match selected { - Some(item) => run_engine(app, ctx, "renderQueue.remove", json!({"item": item})), - None => Err("select a Render Queue item first".into()), - }; - } - // Shift+Delete deletes the Project panel's selection without asking; it does nothing in the - // other panels. - if id == "project.deleteWithoutConfirmation" { - if app.ui.focused != PanelKind::Project { - return Ok(Value::Null); - } - return crate::panels::delete_items::delete_confirmed(app, ctx, json!({})); - } - let now = app.now(); - // Closing a modified project asks to save it first; the command runs once answered. - if crate::panels::unsaved::guard(app, id, ¶ms) { - return Ok(json!({"dialog": "unsavedChanges"})); - } - // Deleting Project items that compositions use asks first, as in After Effects. - if crate::panels::delete_items::guard(app, id, ¶ms) { - return Ok(json!({"dialog": "deleteItems"})); - } - // J / K and Select All Keyframes act on what the Timeline shows (its revealed properties). - if matches!(id, "time.nextKey" | "time.previousKey" | "keys.selectAll") - && ["visible", "layers", "layer", "prop"].iter().all(|k| params.get(*k).is_none()) - && let Some(c) = app.session.active_comp_arc() - { - let mut p = if params.is_object() { params.clone() } else { json!({}) }; - p["visible"] = json!(crate::panels::timeline::visible_props(app, &c)); - return run_engine(app, ctx, id, p); - } - // New Camera/Light and Camera/Light Settings without parameters open their dialogs. - if crate::panels::dialogs_3d::route(app, id, ¶ms)? { - return Ok(Value::Null); - } - // New 3D Primitive without parameters opens its dialog. - if crate::panels::primitive_dialog::route(app, id, ¶ms)? { - return Ok(Value::Null); - } - // Layer Settings on a solid, adjustment layer or null opens its settings dialog. - if crate::panels::dialogs::route_layer_settings(app, id, ¶ms)? { - return Ok(Value::Null); - } - // Layer ▸ Layer Styles ▸