//! Frontend-only commands: viewer zoom/overlays, panels, workspaces, dialogs and app //! chrome. They live in the registry so every menu entry is a command (menus, shortcuts, the //! control channel and MCP all see them); running one emits [`crate::Event::Frontend`] and the UI //! performs it. Headless sessions accept and ignore them. use serde_json::{Value, json}; use super::{CommandSpec, always, bad, frontend, has_comp, has_layers}; use crate::{Result, Session, cmd}; macro_rules! fe { ($id:literal, $label:literal, [$($m:literal),*], $sc:expr, $params:literal, $en:expr) => { cmd!($id, $label, [$($m),*], $sc, $params, $en, |s, p| frontend(s, $id, p)) }; } pub fn specs() -> Vec { vec![ // Application. fe!("app.about", "About EffectCraft...", [], None, "{}", always), fe!("layer.style.options", "Layer Style Options...", ["Layer", "Layer Styles"], None, "{layer?, style?: blendingOptions|dropShadow|…}", has_layers), fe!( "app.settings", "Settings...", [], None, "{page?: general|startup|project|composition|previews|appearance|grids|labels|type|import|export|audio|disk|memory|video|3d|scripting}", always ), fe!("app.gpuInfo", "GPU Information...", [], None, "{}", always), fe!("app.hide", "Hide EffectCraft", [], None, "{}", always), // macOS: the native menu's Hide Others / Show All (the desktop app hides the other apps). fe!("app.hideOthers", "Hide Others", [], None, "{}", always), fe!("app.showAll", "Show All", [], None, "{}", always), fe!("app.quit", "Quit EffectCraft", [], Some("Cmd+Q"), "{}", always), fe!("app.commandPalette", "Quick Apply...", ["Edit"], Some("Cmd+Shift+Space"), "{query?}", always), fe!("app.keyboardShortcuts", "Keyboard Shortcuts", ["Edit"], Some("Cmd+Alt+'"), "{}", always), fe!("app.templates", "Templates", [], None, "{kind: renderSettings|outputModule}", always), fe!("app.find", "Find", ["File"], Some("Cmd+F"), "{query?}", always), fe!("effects.copyPresetsFolderPath", "Copy Presets Folder Path", [], None, "{}", always), // Preview. fe!( "playback.toggle", "Play Current Preview", ["Composition", "Preview"], Some("Space"), "{shortcut?: spacebar|shiftSpacebar|numpad0|shiftNumpad0|altNumpad0 (whose Preview panel options to use; default spacebar)}", has_comp ), fe!("playback.cacheWhenIdle", "Cache Frames When Idle", ["Composition", "Preview"], None, "{value?}", always), fe!("playback.audio", "Audio", ["Composition", "Preview"], None, "{value?: include audio in previews}", always), fe!( "playback.scrubAudio", "Scrub Audio", [], None, "{time? (s, default the current time)} — plays one frame of the comp's audio there (Ctrl/Cmd-drag the current time)", has_comp ), // Viewer. fe!("view.zoomIn", "Zoom In", ["View"], Some("."), "{}", has_comp), fe!("view.zoomOut", "Zoom Out", ["View"], Some(","), "{}", has_comp), fe!("view.rulers", "Show Rulers", ["View"], Some("Cmd+R"), "{value?}", always), fe!("view.panelBackground", "Panel Background Color", [], None, "{color?: black|darkGray|mediumGray|lightGray|white|custom|#hex, pick?: true}", always), fe!("view.guides", "Show Guides", ["View"], Some("Cmd+;"), "{value?}", always), fe!("view.snapToGuides", "Snap to Guides", ["View"], Some("Cmd+Shift+;"), "{value?}", always), fe!("view.lockGuides", "Lock Guides", ["View"], Some("Cmd+Alt+Shift+;"), "{value?}", always), fe!("view.grid", "Show Grid", ["View"], Some("Cmd+'"), "{value?}", always), fe!("view.snapToGrid", "Snap to Grid", ["View"], Some("Cmd+Shift+'"), "{value?}", always), fe!("view.options", "View Options...", ["View"], Some("Cmd+Alt+U"), "{}", has_comp), fe!("view.layerControls", "Show Layer Controls", ["View"], Some("Cmd+Shift+H"), "{value?}", always), // macOS uses Control+Command+F for native full screen. On Windows and Linux, // Cmd maps to Ctrl; the After Effects-style Ctrl+\ is window.maximizeApp. fe!("view.fullScreen", "Enter Full Screen", ["View"], cfg!(target_os = "macos").then_some("Ctrl+Cmd+F"), "{}", always), fe!( "view.newViewer", "New Viewer", ["View"], Some("Alt+Shift+N"), "{} — another Composition viewer of the active comp; the one in use is locked", has_comp ), // Panels and workspaces. fe!( "window.panel", "Show Panel", [], None, "{panel: project|effectControls|composition|layer|timeline|info|audio|preview|effectsPresets|properties|character|paragraph|align|tracker|wiggler|smoother|motionSketch|paint|brushes|renderQueue|flowchart|history|markers|tools|lumetriScopes|footage|mediaBrowser|metadata|progress|contentAwareFill|createNullsFromPaths|vrCompEditor}", always ), fe!( "window.workspace", "Workspace", [], None, "{name: Default|Standard|Small Screen|Animation|Effects|Motion Tracking|Paint|Text|Minimal|All Panels}", always ), fe!("window.saveWorkspace", "Save Changes to this Workspace", ["Window", "Workspace"], None, "{}", always), fe!("window.saveWorkspaceAs", "Save as New Workspace...", ["Window", "Workspace"], None, "{name}", always), fe!("window.editWorkspaces", "Edit Workspaces...", ["Window", "Workspace"], None, "{name, rename?: new name, delete?: bool}", always), fe!("window.resetWorkspace", "Reset to Saved Layout", ["Window", "Workspace"], None, "{}", always), fe!("comp.flowchart", "Composition Flowchart", ["Composition"], Some("Cmd+Shift+F11"), "{}", always), fe!("comp.miniFlowchart", "Composition Mini-Flowchart", ["Composition"], Some("Tab"), "{}", has_comp), fe!("layer.openLayer", "Open Layer", ["Layer"], None, "{layer?}", has_layers), fe!("effect.manage", "Manage Effects...", ["Effect"], None, "{}", always), fe!("anim.browsePresets", "Browse Presets...", ["Animation"], None, "{}", always), // Project panel: delete the selected items without the prompt that Delete shows when // compositions use them. The UI runs `project.delete` (agents can run it directly). fe!( "project.deleteWithoutConfirmation", "Delete Project Items Without Confirmation", [], Some("Shift+Delete"), "{} — the Project panel's selection, with the layers that use it; one undo step", always ), // A gradient property's "Edit Gradient…" (the Timeline's Colors value): opens the // Gradient Editor on it at the current time. OK sets it with prop.set (one undo step). cmd!( "prop.editGradient", "Edit Gradient...", [], None, "{layer?, path|prop, comp?} — a gradient property; opens the Gradient Editor at the current time", has_layers, edit_gradient ), ] } /// `prop.editGradient`: check the target is a gradient property before the UI opens the editor, /// so agents get an error (not success) for a wrong one, and hand the UI the resolved ids. fn edit_gradient(s: &mut Session, p: &Value) -> Result { let c = "prop.editGradient"; let (cid, lid, uid) = super::prop::prop_ref(s, p, c)?; let pr = s.project.comp(cid).and_then(|comp| comp.layer(lid)).and_then(|l| l.props.find(uid)).ok_or_else(|| bad(c, format!("no property @{uid}")))?; if !matches!(pr.value, effectcraft_keyframe::Value::Gradient(_)) { return Err(bad(c, format!("{} is not a gradient property", pr.name))); } frontend(s, c, &json!({"comp": cid.0, "layer": lid.0, "prop": uid})) }