diff --git a/crates/engine/src/commands/app_more.rs b/crates/engine/src/commands/app_more.rs index 9f6b7c99..2a005690 100644 --- a/crates/engine/src/commands/app_more.rs +++ b/crates/engine/src/commands/app_more.rs @@ -582,7 +582,15 @@ pub fn specs() -> Vec { cmd!("edit.history", "History", [], None, "{steps?: n (undo n steps), redo?: n}", has_history, history), cmd!("file.importRecent", "Import Recent Footage", [], None, "{index? | path?}", has_recent_footage, import_recent), cmd!("file.clearRecentFootage", "Clear Recent Footage", ["File", "Import Recent Footage"], None, "{}", has_recent_footage, clear_recent_footage), - cmd!("anim.applyRecentPreset", "Recent Animation Presets", [], None, "{index? | path?, layers?}", has_recent_presets, apply_recent_preset), + cmd!( + "anim.applyRecentPreset", + "Recent Animation Presets", + [], + Some("Cmd+Alt+Shift+F"), + "{index? | path?, layers?}", + has_recent_presets, + apply_recent_preset + ), cmd!( "anim.clearRecentPresets", "Clear Recent Presets", diff --git a/crates/engine/src/commands/edit.rs b/crates/engine/src/commands/edit.rs index 7f134c6b..06ba43b1 100644 --- a/crates/engine/src/commands/edit.rs +++ b/crates/engine/src/commands/edit.rs @@ -863,14 +863,22 @@ pub fn specs() -> Vec { has_comp, paste_svg ), - cmd!("edit.pasteReversedKeyframes", "Paste Reversed Keyframes", ["Edit"], None, "{layers?, prop?|path?, time?}", has_key_clip, paste_reversed), + cmd!( + "edit.pasteReversedKeyframes", + "Paste Reversed Keyframes", + ["Edit"], + Some("Cmd+Shift+V"), + "{layers?, prop?|path?, time?}", + has_key_clip, + paste_reversed + ), cmd!("edit.clear", "Clear", ["Edit"], Some("Delete"), "{layers?}", layers_or_keys, delete), cmd!("edit.duplicate", "Duplicate", ["Edit"], Some("Cmd+D"), "{layers?} (effects or shape items when they are selected)", has_layers, duplicate), cmd!("edit.splitLayer", "Split Layer", ["Edit"], Some("Cmd+Shift+D"), "{layers?}", has_layers, split), cmd!("edit.liftWorkArea", "Lift Work Area", ["Edit"], None, "{layers?}", has_comp, lift), cmd!("edit.extractWorkArea", "Extract Work Area", ["Edit"], None, "{layers?}", has_comp, extract), cmd!("edit.selectAll", "Select All", ["Edit"], Some("Cmd+A"), "{}", has_comp, select_all), - cmd!("edit.deselectAll", "Deselect All", ["Edit"], Some("Cmd+Shift+A"), "{}", always_ok, deselect_all), + cmd!("edit.deselectAll", "Deselect All", ["Edit"], ["Cmd+Shift+A", "F2"], "{}", always_ok, deselect_all), cmd!("edit.label", "Label", ["Edit", "Label"], None, "{label: Red|Yellow|Aqua|…, layers?, keys?, target?: layers}", always_ok, label), cmd!("edit.selectLabelGroup", "Select Label Group", ["Edit", "Label"], None, "{}", has_props_or_layers_or_items, select_label_group), cmd!("edit.purgeUndo", "Undo", ["Edit", "Purge"], None, "{}", always_ok, purge), diff --git a/crates/engine/src/commands/proxy.rs b/crates/engine/src/commands/proxy.rs index f05e9765..b7d644b9 100644 --- a/crates/engine/src/commands/proxy.rs +++ b/crates/engine/src/commands/proxy.rs @@ -221,7 +221,7 @@ pub fn specs() -> Vec { has_comp_selection, create_proxy ), - cmd!("file.setProxy", "File...", ["File", "Set Proxy"], None, "{path, items?|item?}", has_targets, set_proxy), + cmd!("file.setProxy", "File...", ["File", "Set Proxy"], Some("Cmd+Alt+P"), "{path, items?|item?}", has_targets, set_proxy), cmd!("file.setProxyNone", "None", ["File", "Set Proxy"], None, "{items?|item?}", has_proxy, set_proxy_none), cmd!("file.useProxy", "Use Proxy", [], None, "{items?|item?, on?}", has_proxy, use_proxy), cmd!( diff --git a/crates/engine/src/commands/render_queue.rs b/crates/engine/src/commands/render_queue.rs index 93a56ced..09a7c7d2 100644 --- a/crates/engine/src/commands/render_queue.rs +++ b/crates/engine/src/commands/render_queue.rs @@ -998,7 +998,7 @@ pub fn specs() -> Vec { "renderQueue.add", "Add to Render Queue", ["Composition"], - Some("Cmd+M"), + ["Cmd+M", "Cmd+Shift+/"], "{comp?: id|name, template?: Render Settings template, format?: h264|hevc|av1|prores|uncompressed|webm|png|jpeg|tiff|exr|gif|wav|aiff, output?: path|template, log?: errorsOnly|plusSettings|plusPerFrameInfo, quality?: best|draft|1-100 (jpeg, webm), resolution?: full|half|third|quarter|scale, proxyUse?, effects?, solo?, guideLayers?, colorDepth?, frameBlending?, fieldRender?, pulldown?, motionBlur?, timeSpan?: workArea|comp|custom, start?: s, end?: s, duration?: s, frameRate?: fps|null, skipExisting?: bool, storageOverflow?: bool, channels?: rgb|rgba|alpha, color?, bitrate?: kbps, webmBitrate?, keyframeInterval?, proresProfile?, profile?, level?, rateControl?, webmCodec?, audioBitrate?, opusApplication?, crop?, resize?, includeProjectLink?, audio?: auto|on|off, sampleRate?, audioChannels?, audioFormat?, loop?: bool (values as in setRenderSettings / setOutputModule)}", can_add, add diff --git a/crates/engine/src/commands/settings.rs b/crates/engine/src/commands/settings.rs index 1a5789e2..a6621c3f 100644 --- a/crates/engine/src/commands/settings.rs +++ b/crates/engine/src/commands/settings.rs @@ -283,7 +283,7 @@ pub fn specs() -> Vec { cmd!("shortcuts.export", "Export Keyboard Shortcuts", [], None, "{preset?, path?}", always, sc_export), cmd!("shortcuts.import", "Import Keyboard Shortcuts", [], None, "{path? | preset?: exported document, name?}", always, sc_import), query!("shortcuts.conflicts", "Keyboard Shortcut Conflicts", "{}", sc_conflicts), - cmd!("file.openRecent", "Open Recent", [], None, "{index? | path?}", has_recent, open_recent), + cmd!("file.openRecent", "Open Recent", [], Some("Cmd+Alt+Shift+P"), "{index? | path?}", has_recent, open_recent), cmd!("file.clearRecent", "Clear Recent Projects", ["File", "Open Recent"], None, "{}", has_recent, clear_recent), cmd!("file.autoSave", "Auto-Save Now", [], None, "{}", always, auto_save), query!("file.recoveryInfo", "Auto-Save Status", "{}", recovery_info), diff --git a/crates/engine/src/commands/time.rs b/crates/engine/src/commands/time.rs index 1bc4638d..a0060ad9 100644 --- a/crates/engine/src/commands/time.rs +++ b/crates/engine/src/commands/time.rs @@ -178,8 +178,8 @@ pub fn specs() -> Vec { cmd!("time.forward10", "Forward 10 Frames", [], ["Shift+PageDown", "Cmd+Shift+ArrowRight"], "{}", has_comp, |s, _| step(s, &json!({"frames": 10}))), cmd!("time.back10", "Back 10 Frames", [], ["Shift+PageUp", "Cmd+Shift+ArrowLeft"], "{}", has_comp, |s, _| step(s, &json!({"frames": -10}))), cmd!("time.step", "Step Frames", [], None, "{frames}", has_comp, step), - cmd!("time.start", "Go to Start", [], Some("Home"), "{}", has_comp, |s, _| go(s, &json!({"to": "start"}))), - cmd!("time.end", "Go to End", [], Some("End"), "{}", has_comp, |s, _| go(s, &json!({"to": "end"}))), + cmd!("time.start", "Go to Start", [], ["Home", "Cmd+Alt+ArrowLeft"], "{}", has_comp, |s, _| go(s, &json!({"to": "start"}))), + cmd!("time.end", "Go to End", [], ["End", "Cmd+Alt+ArrowRight"], "{}", has_comp, |s, _| go(s, &json!({"to": "end"}))), cmd!("time.layerIn", "Go to Layer In Point", [], Some("I"), "{}", has_comp, |s, _| go(s, &json!({"to": "layerIn"}))), cmd!("time.layerOut", "Go to Layer Out Point", [], Some("O"), "{}", has_comp, |s, _| go(s, &json!({"to": "layerOut"}))), cmd!( diff --git a/crates/engine/src/commands/view.rs b/crates/engine/src/commands/view.rs index abf92416..a23bf618 100644 --- a/crates/engine/src/commands/view.rs +++ b/crates/engine/src/commands/view.rs @@ -136,7 +136,15 @@ pub fn specs() -> Vec { res!("view.res.half", "Half", ["View", "Resolution"], Some("Cmd+Shift+J"), Half), res!("view.res.third", "Third", ["View", "Resolution"], None, Third), res!("view.res.quarter", "Quarter", ["View", "Resolution"], Some("Cmd+Alt+Shift+J"), Quarter), - cmd!("view.res.custom", "Custom...", ["View", "Resolution"], None, "{factor?: 1..40 (render every n-th pixel), comp?}", has_comp, custom_resolution), + cmd!( + "view.res.custom", + "Custom...", + ["View", "Resolution"], + Some("Cmd+Alt+J"), + "{factor?: 1..40 (render every n-th pixel), comp?}", + has_comp, + custom_resolution + ), // The Composition panel's Resolution popup (not in the View menu). res!("view.res.auto", "Resolution: Auto", [], None, Auto), cmd!("view.layout", "Switch View Layout", [], None, "{views: 1|2|4}", has_comp, view_layout), diff --git a/crates/engine/src/commands/viewer_cmds.rs b/crates/engine/src/commands/viewer_cmds.rs index 263f7191..aac24267 100644 --- a/crates/engine/src/commands/viewer_cmds.rs +++ b/crates/engine/src/commands/viewer_cmds.rs @@ -173,6 +173,20 @@ fn show_snapshot(s: &mut Session, p: &Value) -> Result { Ok(json!(s.state.viewer.show_snapshot)) } +/// A `view.fastPreviews.*` command choosing one Fast Previews mode. +macro_rules! fast { + ($id:literal, $label:literal, $sc:literal, $mode:literal) => { + cmd!($id, $label, [], Some($sc), "{}", always, |s, _| fast_previews(s, &json!({"mode": $mode}))) + }; +} + +/// A `view.channel.*` command showing one channel (again: back to RGB), in grayscale or colorized. +macro_rules! chan { + ($id:literal, $label:literal, $sc:literal, $channel:literal, $colorized:literal) => { + cmd!($id, $label, [], Some($sc), "{}", always, |s, _| channel(s, &json!({"channel": $channel, "colorized": $colorized, "toggle": true}))) + }; +} + fn fast_previews(s: &mut Session, p: &Value) -> Result { let name = str_p(p, "mode").unwrap_or("off"); let m = @@ -504,6 +518,22 @@ pub fn specs() -> Vec { cmd!("view.takeSnapshot", "Take Snapshot", [], Some("Shift+F5"), "{comp?, scale?: 0.05..1}", has_comp, take_snapshot), cmd!("view.showSnapshot", "Show Snapshot", [], None, "{value?}", has_snapshot, show_snapshot), cmd!("view.fastPreviewMode", "Fast Previews", [], None, "{mode: off|adaptive|draft|fastDraft|wireframe}", always, fast_previews), + fast!("view.fastPreviews.off", "Fast Previews: Off (Final Quality)", "Cmd+Alt+1", "off"), + fast!("view.fastPreviews.adaptive", "Fast Previews: Adaptive Resolution", "Cmd+Alt+2", "adaptive"), + fast!("view.fastPreviews.draft", "Fast Previews: Draft", "Cmd+Alt+3", "draft"), + fast!("view.fastPreviews.fastDraft", "Fast Previews: Fast Draft", "Cmd+Alt+4", "fastDraft"), + fast!("view.fastPreviews.wireframe", "Fast Previews: Wireframe", "Cmd+Alt+5", "wireframe"), + chan!("view.channel.red", "Show Channel: Red", "Alt+1", "red", false), + chan!("view.channel.green", "Show Channel: Green", "Alt+2", "green", false), + chan!("view.channel.blue", "Show Channel: Blue", "Alt+3", "blue", false), + chan!("view.channel.alpha", "Show Channel: Alpha", "Alt+4", "alpha", false), + chan!("view.channel.redColorized", "Show Channel: Red, Colorized", "Alt+Shift+1", "red", true), + chan!("view.channel.greenColorized", "Show Channel: Green, Colorized", "Alt+Shift+2", "green", true), + chan!("view.channel.blueColorized", "Show Channel: Blue, Colorized", "Alt+Shift+3", "blue", true), + cmd!("view.channel.rgbStraight", "Show Channel: RGB Straight", [], Some("Alt+Shift+4"), "{}", always, |s, _| channel( + s, + &json!({"channel": "rgbStraight", "toggle": true}) + )), cmd!("view.moveGuide", "Move Guide", [], None, "{comp?, index, position (comp px), merge?}", has_comp, move_guide), cmd!("view.removeGuide", "Remove Guide", [], None, "{comp?, index}", has_comp, remove_guide), cmd!( diff --git a/crates/engine/src/menus.rs b/crates/engine/src/menus.rs index bd1cffb0..1fb215a3 100644 --- a/crates/engine/src/menus.rs +++ b/crates/engine/src/menus.rs @@ -18,10 +18,11 @@ //! ``` //! //! An entry's shortcut is the command's default shortcut unless the line gives one (needed for -//! entries with bound parameters). Adobe-service entries (Team Projects, Libraries, Bridge, Media -//! Encoder, Behance, Creative Cloud…) are intentionally absent; the Essential Graphics workspace -//! uses the Properties panel and the Essential Graphics panel, whose templates use EffectCraft's -//! own open `.ectemplate` format. +//! entries with bound parameters). A line can give further default keys after a comma +//! (`| F3, Cmd+Shift+T`); the menus show the first. Adobe-service entries (Team Projects, +//! Libraries, Bridge, Media Encoder, Behance, Creative Cloud…) are intentionally absent; the +//! Essential Graphics workspace uses the Properties panel and the Essential Graphics panel, whose +//! templates use EffectCraft's own open `.ectemplate` format. use std::sync::OnceLock; @@ -61,7 +62,7 @@ fn file_name(p: &str) -> String { } fn dyn_entry(label: String, command: &str, params: Value) -> MenuEntry { - MenuEntry { label, command: command.into(), params, shortcut: None } + MenuEntry { label, command: command.into(), params, shortcut: None, alt_shortcuts: vec![] } } /// The bindable holding shortcut `keys` in the active preset (its label). @@ -175,6 +176,9 @@ pub struct MenuEntry { #[serde(skip_serializing_if = "Value::is_null")] pub params: Value, pub shortcut: Option, + /// Further default shortcuts (the menus show `shortcut`). + #[serde(skip_serializing_if = "Vec::is_empty")] + pub alt_shortcuts: Vec, } impl MenuEntry { @@ -434,8 +438,9 @@ pub fn parse(text: &str, mac: bool) -> Result, String> { }; // Entries with bound params state their own shortcut; plain entries use the command's. let inherited = if params.is_null() { commands::find(&command).and_then(|c| c.shortcut).map(str::to_string) } else { None }; - let shortcut = parts.next().map(|s| s.trim().to_string()).or(inherited); - out.push(MenuNode::Item(MenuEntry { label, command, params, shortcut })); + let mut keys = parts.next().into_iter().flat_map(|s| s.split(", ")).map(|s| s.trim().to_string()); + let shortcut = keys.next().or(inherited); + out.push(MenuNode::Item(MenuEntry { label, command, params, shortcut, alt_shortcuts: keys.collect() })); } None => { let children = build(lines, i, depth + 1, mac)?; @@ -456,8 +461,13 @@ pub fn parse(text: &str, mac: bool) -> Result, String> { fn effect_categories() -> Vec { let mut cats: Vec<(&str, Vec)> = vec![]; for e in crate::effects::all() { - let entry = - MenuNode::Item(MenuEntry { label: e.name.into(), command: "effect.apply".into(), params: serde_json::json!({"effect": e.id}), shortcut: None }); + let entry = MenuNode::Item(MenuEntry { + label: e.name.into(), + command: "effect.apply".into(), + params: serde_json::json!({"effect": e.id}), + shortcut: None, + alt_shortcuts: vec![], + }); match cats.iter_mut().find(|(c, _)| *c == e.category) { Some((_, v)) => v.push(entry), None => cats.push((e.category, vec![entry])), @@ -650,7 +660,7 @@ Edit All 3D Cache... | edit.purge {"what":"3d"} Undo | edit.purgeUndo Image Cache Memory | edit.purge {"what":"image"} - Snapshot | edit.purge {"what":"snapshot"} + Snapshot | edit.purge {"what":"snapshot"} | Cmd+Shift+F5 --- Edit Original... | edit.editOriginal --- @@ -775,8 +785,8 @@ Layer Draft | layer.quality {"quality":"draft"} | Cmd+Shift+U Wireframe | layer.quality {"quality":"wireframe"} | Cmd+Alt+Shift+U --- - Bilinear | layer.sampling {"sampling":"bilinear"} - Bicubic | layer.sampling {"sampling":"bicubic"} + Bilinear | layer.sampling {"sampling":"bilinear"} | Alt+B + Bicubic | layer.sampling {"sampling":"bicubic"} | Alt+Shift+B Switches Hide Other Video | layer.hideOtherVideo Show All Video | layer.showAllVideo @@ -954,7 +964,7 @@ Layer Pre-compose... | layer.precompose Scene Edit Detection... | layer.sceneEditDetection Effect - Effect Controls | window.panel {"panel":"effectControls"} | F3 + Effect Controls | window.panel {"panel":"effectControls"} | F3, Cmd+Shift+T Last Effect | effect.applyLast Remove All | effect.removeAll --- @@ -1203,7 +1213,7 @@ Window Wiggler | window.panel {"panel":"wiggler"} --- Composition | window.panel {"panel":"composition"} - Effect Controls | window.panel {"panel":"effectControls"} | F3 + Effect Controls | window.panel {"panel":"effectControls"} | F3, Cmd+Shift+T Flowchart | window.panel {"panel":"flowchart"} | Cmd+F11 Footage | window.panel {"panel":"footage"} Layer | window.panel {"panel":"layer"} diff --git a/crates/engine/src/shortcuts.rs b/crates/engine/src/shortcuts.rs index 942184e3..630bfc2d 100644 --- a/crates/engine/src/shortcuts.rs +++ b/crates/engine/src/shortcuts.rs @@ -90,7 +90,8 @@ pub fn bindables(ui: &[UiCommand]) -> Vec { let key = binding_key(&e.command, &e.params); // A plain entry also gets its command's further default keys. let alt = if e.params.is_null() { crate::commands::find(&e.command).map_or(&[][..], |c| c.alt_shortcuts) } else { &[] }; - let defaults = e.shortcut.as_deref().into_iter().chain(alt.iter().copied()).filter_map(normalize).collect(); + let defaults = + e.shortcut.as_deref().into_iter().chain(e.alt_shortcuts.iter().map(String::as_str)).chain(alt.iter().copied()).filter_map(normalize).collect(); push( Bindable { key, diff --git a/crates/engine/src/tests_m145.rs b/crates/engine/src/tests_m145.rs index 183ae65f..64bd9ebb 100644 --- a/crates/engine/src/tests_m145.rs +++ b/crates/engine/src/tests_m145.rs @@ -661,7 +661,13 @@ fn dynamic_menus_and_window_viewer_labels() { assert_eq!(crate::menus::dynamic(&s, "history", &Default::default()).0.len(), s.history.undo.len()); let a = solid(&mut s, "#ff0000"); s.state.selected_layers = vec![LayerId(a)]; - let entry = |cmd: &str, panel: &str| crate::menus::MenuEntry { label: "x".into(), command: cmd.into(), params: json!({"panel": panel}), shortcut: None }; + let entry = |cmd: &str, panel: &str| crate::menus::MenuEntry { + label: "x".into(), + command: cmd.into(), + params: json!({"panel": panel}), + shortcut: None, + alt_shortcuts: vec![], + }; assert_eq!(crate::menus::entry_label(&s, &entry("window.panel", "composition")), "Composition: Main"); assert_eq!(crate::menus::entry_label(&s, &entry("window.panel", "timeline")), "Timeline: Main"); assert!(crate::menus::entry_label(&s, &entry("window.panel", "layer")).starts_with("Layer: ")); diff --git a/crates/engine/src/tests_settings.rs b/crates/engine/src/tests_settings.rs index 02c2a00b..fea7709e 100644 --- a/crates/engine/src/tests_settings.rs +++ b/crates/engine/src/tests_settings.rs @@ -375,6 +375,81 @@ fn defaults_have_no_conflicts_and_match_the_menus() { assert!(t.bindables.len() > 500); } +/// Each Fast Previews and Show Channel mode is a command with a default key. +#[test] +fn fast_previews_and_show_channel_modes_have_default_keys() { + let s = Session::default(); + let t = s.shortcuts(); + for (id, key) in [ + ("view.fastPreviews.off", "Cmd+Alt+1"), + ("view.fastPreviews.adaptive", "Cmd+Alt+2"), + ("view.fastPreviews.draft", "Cmd+Alt+3"), + ("view.fastPreviews.fastDraft", "Cmd+Alt+4"), + ("view.fastPreviews.wireframe", "Cmd+Alt+5"), + ("view.channel.red", "Alt+1"), + ("view.channel.green", "Alt+2"), + ("view.channel.blue", "Alt+3"), + ("view.channel.alpha", "Alt+4"), + ("view.channel.redColorized", "Alt+Shift+1"), + ("view.channel.greenColorized", "Alt+Shift+2"), + ("view.channel.blueColorized", "Alt+Shift+3"), + ("view.channel.rgbStraight", "Alt+Shift+4"), + ] { + assert_eq!(t.shortcut_of(id, &json!(null)), Some(key), "{id}"); + } + assert!(t.all_conflicts().is_empty(), "{:?}", t.all_conflicts()); + // The commands set their mode; a channel's key again is RGB. + let mut s = Session::default(); + s.execute("view.fastPreviews.fastDraft", json!({})).unwrap(); + assert_eq!(s.state.viewer.fast_previews.id(), "fastDraft"); + s.execute("view.channel.redColorized", json!({})).unwrap(); + assert_eq!((s.state.viewer.channel.id(), s.state.viewer.colorized), ("red", true)); + s.execute("view.channel.green", json!({})).unwrap(); + assert_eq!((s.state.viewer.channel.id(), s.state.viewer.colorized), ("green", false)); + s.execute("view.channel.green", json!({})).unwrap(); + assert_eq!(s.state.viewer.channel.id(), "rgb"); +} + +/// Default keys for commands and menu entries that had none. +#[test] +fn default_keys_for_commands_that_had_none() { + let s = Session::default(); + let t = s.shortcuts(); + for (id, params, key) in [ + ("edit.pasteReversedKeyframes", json!(null), "Cmd+Shift+V"), + ("edit.purge", json!({"what": "snapshot"}), "Cmd+Shift+F5"), + ("layer.sampling", json!({"sampling": "bilinear"}), "Alt+B"), + ("layer.sampling", json!({"sampling": "bicubic"}), "Alt+Shift+B"), + ("view.res.custom", json!(null), "Cmd+Alt+J"), + ("file.setProxy", json!(null), "Cmd+Alt+P"), + ("file.openRecent", json!(null), "Cmd+Alt+Shift+P"), + ("anim.applyRecentPreset", json!(null), "Cmd+Alt+Shift+F"), + ] { + assert_eq!(t.shortcut_of(id, ¶ms), Some(key), "{id} {params}"); + } + assert!(t.all_conflicts().is_empty(), "{:?}", t.all_conflicts()); +} + +/// Second default keys: on the command, or after a comma in a menu line with bound parameters. +#[test] +fn second_default_keys() { + let s = Session::default(); + let t = s.shortcuts(); + for (key, want) in [ + ("edit.deselectAll", ["Cmd+Shift+A", "F2"]), + ("time.start", ["Home", "Cmd+Alt+ArrowLeft"]), + ("time.end", ["End", "Cmd+Alt+ArrowRight"]), + ("renderQueue.add", ["Cmd+M", "Cmd+Shift+/"]), + (r#"window.panel {"panel":"effectControls"}"#, ["F3", "Cmd+Shift+T"]), + ] { + assert_eq!(t.keys.get(key).map(Vec::as_slice), Some(want.map(String::from).as_slice()), "{key}"); + } + // The menus keep showing the first key. + assert_eq!(t.shortcut_of("window.panel", &json!({"panel": "effectControls"})), Some("F3")); + assert_eq!(t.shortcut_of("edit.deselectAll", &json!(null)), Some("Cmd+Shift+A")); + assert!(t.all_conflicts().is_empty(), "{:?}", t.all_conflicts()); +} + /// Cmd+Left / Cmd+Right Arrow step a frame like Page Up / Page Down (Shift: 10 frames). #[test] fn cmd_arrows_step_frames_like_page_keys() { diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 4e814ea2..9c10a4e9 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -6591,7 +6591,8 @@ mod tests { assert_eq!(label(&app, "", source), translated); } assert_eq!(label(&app, "unknown.command", "Text"), "Text"); - let panel = MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel": "layer"}), shortcut: None }; + let panel = + MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel": "layer"}), shortcut: None, alt_shortcuts: vec![] }; assert_eq!(entry(&app, &panel, "Layer: File Čeština".into()), "Vrstva: File Čeština"); assert_eq!( submenu(&app, "Assign Shortcut to Workspace", "Assign Shortcut to “File Čeština” Workspace".into()), @@ -6607,8 +6608,8 @@ mod tests { fn czech_undo_redo_keeps_history_labels_verbatim() { let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); app.session.execute("prefs.set", json!({"key": "general.language", "value": "cs"})).unwrap(); - let undo = MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None }; - let redo = MenuEntry { label: "Redo".into(), command: "edit.redo".into(), params: json!({}), shortcut: None }; + let undo = MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }; + let redo = MenuEntry { label: "Redo".into(), command: "edit.redo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }; assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Není co vrátit"); assert_eq!(entry(&app, &redo, "Can't Redo".into()), "Není co zopakovat"); app.session.history.undo.push(("File Čeština".into(), app.session.project.clone())); @@ -6627,7 +6628,7 @@ mod tests { ("Reveal in Explorer", "Zobrazit v Průzkumníku"), ("Reveal in File Manager", "Zobrazit ve správci souborů"), ] { - let e = MenuEntry { label: source.into(), command: command.into(), params: json!({}), shortcut: None }; + let e = MenuEntry { label: source.into(), command: command.into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }; assert_eq!(label(&app, command, source), translated); assert_eq!(entry(&app, &e, source.into()), translated); assert_eq!(label(&app, "file.openRecent", source), source); @@ -6722,7 +6723,8 @@ mod tests { ("Reveal in Explorer", "Показати у Провіднику"), ("Reveal in File Manager", "Показати у файловому менеджері"), ] { - let e = MenuEntry { label: source.into(), command: command.into(), params: json!({"item": "My file.mp4"}), shortcut: None }; + let e = + MenuEntry { label: source.into(), command: command.into(), params: json!({"item": "My file.mp4"}), shortcut: None, alt_shortcuts: vec![] }; assert_eq!(label(&app, command, source), translated); assert_eq!(entry(&app, &e, source.into()), translated); assert_eq!(label(&app, "file.openRecent", source), source); @@ -6751,7 +6753,7 @@ mod tests { assert_eq!( entry( &app, - &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None }, + &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None, alt_shortcuts: vec![] }, "Layer: File".into() ), "レイヤー: File" @@ -6854,13 +6856,17 @@ mod tests { // Untranslated keys and dynamic labels stay verbatim. assert_eq!(label(&app, "unknown.command", "Text"), "Text"); assert_eq!( - entry(&app, &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None }, "Undo Comp1".into()), + entry( + &app, + &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }, + "Undo Comp1".into() + ), "无可撤销的操作" ); assert_eq!( entry( &app, - &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None }, + &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None, alt_shortcuts: vec![] }, "Layer: File".into() ), "图层: File" @@ -6894,13 +6900,17 @@ mod tests { // Untranslated keys and dynamic labels stay verbatim. assert_eq!(label(&app, "unknown.command", "Text"), "Text"); assert_eq!( - entry(&app, &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None }, "Undo Comp1".into()), + entry( + &app, + &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }, + "Undo Comp1".into() + ), "沒有可復原的操作" ); assert_eq!( entry( &app, - &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None }, + &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None, alt_shortcuts: vec![] }, "Layer: File".into() ), "圖層: File" @@ -6943,7 +6953,8 @@ mod tests { assert_eq!(label(&app, "layer.addTextAnimator", "Tracking"), "Міжлітерний інтервал"); assert_eq!(label(&app, "edit.purgeUndo", "Undo"), "Історія скасувань"); assert_eq!(label(&app, "unknown.command", "Text"), "Text"); - let panel = MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None }; + let panel = + MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None, alt_shortcuts: vec![] }; assert_eq!(entry(&app, &panel, "Layer: File".into()), "Шар: File"); assert_eq!( submenu(&app, "Assign Shortcut to Workspace", "Assign Shortcut to “File Україна” Workspace".into()), @@ -6958,8 +6969,8 @@ mod tests { fn ukrainian_undo_redo_keeps_history_labels_verbatim() { let mut app = EffectcraftApp::new(effectcraft_engine::Session::default()); app.session.execute("prefs.set", json!({"key":"general.language", "value":"uk"})).unwrap(); - let undo = MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None }; - let redo = MenuEntry { label: "Redo".into(), command: "edit.redo".into(), params: json!({}), shortcut: None }; + let undo = MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }; + let redo = MenuEntry { label: "Redo".into(), command: "edit.redo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }; assert_eq!(entry(&app, &undo, "Can't Undo".into()), "Немає дій для скасування"); assert_eq!(entry(&app, &redo, "Can't Redo".into()), "Немає дій для повторення"); app.session.history.undo.push(("File Україна".into(), app.session.project.clone())); @@ -6995,13 +7006,17 @@ mod tests { // Untranslated keys and dynamic labels stay verbatim. assert_eq!(label(&app, "unknown.command", "Text"), "Text"); assert_eq!( - entry(&app, &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None }, "Undo Comp1".into()), + entry( + &app, + &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }, + "Undo Comp1".into() + ), "No se puede deshacer" ); assert_eq!( entry( &app, - &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None }, + &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None, alt_shortcuts: vec![] }, "Layer: File".into() ), "Capa: File" @@ -7036,13 +7051,17 @@ mod tests { // Untranslated keys and dynamic labels stay verbatim. assert_eq!(label(&app, "unknown.command", "Text"), "Text"); assert_eq!( - entry(&app, &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None }, "Undo Comp1".into()), + entry( + &app, + &MenuEntry { label: "Undo".into(), command: "edit.undo".into(), params: json!({}), shortcut: None, alt_shortcuts: vec![] }, + "Undo Comp1".into() + ), "Não há nada para desfazer" ); assert_eq!( entry( &app, - &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None }, + &MenuEntry { label: "Layer".into(), command: "window.panel".into(), params: json!({"panel":"layer"}), shortcut: None, alt_shortcuts: vec![] }, "Layer: File".into() ), "Camada: File" diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index bf158cc2..73a0d68f 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -43,7 +43,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ 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", [], None), + 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), @@ -52,7 +52,7 @@ pub const UI_COMMANDS: &[UiCommand] = &[ 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", [], None), + uic!("timeline.zoomFit", "Zoom to Fit Comp", [], Some("Shift+;")), 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")), diff --git a/docs/control-protocol.md b/docs/control-protocol.md index f5487981..75ef0e76 100644 --- a/docs/control-protocol.md +++ b/docs/control-protocol.md @@ -134,7 +134,9 @@ Viewer state that agents drive headless too: `view.snapping`, `view.snappingOpti `header.snappingOptions`: After Effects' Snap Edges Extended and Snap to Features in Collapsed Compositions and Text Layers), `view.channel {channel, colorized?}`, `view.exposure {stops | delta}`, `view.resetExposure`, `view.takeSnapshot`, `view.showSnapshot`, -`view.fastPreviewMode {mode}`, `view.res.full|half|third|quarter|auto {comp?}` and +`view.fastPreviewMode {mode}` (one mode each, for shortcuts: +`view.fastPreviews.off|adaptive|draft|fastDraft|wireframe`, and for the channels +`view.channel.red|green|blue|alpha|redColorized|greenColorized|blueColorized|rgbStraight`), `view.res.full|half|third|quarter|auto {comp?}` and `view.res.custom {factor, comp?}` (the comp's Resolution, saved with the project but no undo step; `ui.set {viewer: {res}}` sets the active comp's, and `ui.inspect` reports it as `ui.viewer.res`), `view.setRegionOfInterest {rect}`, `view.addGuide`,