Merge pull request #1050 from pravdomil/claude/bind-existing-shortcuts

This commit is contained in:
Brandon Thomas committed 2026-10-13 04:28:25 -04:00
commit 0e424b45f1
15 files changed
+212 -45

No files matched your search

+9 -1
View File
@@ -582,7 +582,15 @@ pub fn specs() -> Vec<CommandSpec> {
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",
+10 -2
View File
@@ -863,14 +863,22 @@ pub fn specs() -> Vec<CommandSpec> {
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),
+1 -1
View File
@@ -221,7 +221,7 @@ pub fn specs() -> Vec<CommandSpec> {
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!(
+1 -1
View File
@@ -998,7 +998,7 @@ pub fn specs() -> Vec<CommandSpec> {
"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
+1 -1
View File
@@ -283,7 +283,7 @@ pub fn specs() -> Vec<CommandSpec> {
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),
+2 -2
View File
@@ -178,8 +178,8 @@ pub fn specs() -> Vec<CommandSpec> {
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!(
+9 -1
View File
@@ -136,7 +136,15 @@ pub fn specs() -> Vec<CommandSpec> {
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),
+30
View File
@@ -173,6 +173,20 @@ fn show_snapshot(s: &mut Session, p: &Value) -> Result<Value> {
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<Value> {
let name = str_p(p, "mode").unwrap_or("off");
let m =
@@ -504,6 +518,22 @@ pub fn specs() -> Vec<CommandSpec> {
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!(
+24 -14
View File
@@ -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<String>,
/// Further default shortcuts (the menus show `shortcut`).
#[serde(skip_serializing_if = "Vec::is_empty")]
pub alt_shortcuts: Vec<String>,
}
impl MenuEntry {
@@ -434,8 +438,9 @@ pub fn parse(text: &str, mac: bool) -> Result<Vec<MenuNode>, 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<Vec<MenuNode>, String> {
fn effect_categories() -> Vec<MenuNode> {
let mut cats: Vec<(&str, Vec<MenuNode>)> = 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"}
+2 -1
View File
@@ -90,7 +90,8 @@ pub fn bindables(ui: &[UiCommand]) -> Vec<Bindable> {
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,
+7 -1
View File
@@ -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: "));
+75
View File
@@ -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, &params), 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() {
+36 -17
View File
@@ -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"
+2 -2
View File
@@ -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")),
+3 -1
View File
@@ -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`,