mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-14 19:27:05 -04:00
Shortcuts: Cmd+Left / Cmd+Right Arrow step a frame
Previous / Next Frame also answer to Cmd+Left / Cmd+Right Arrow, and Back / Forward 10 Frames to Cmd+Shift+Left / Right, next to the Page keys (After Effects' second default keys). A command spec can now list several default shortcuts (`alt_shortcuts`, `cmd!(…, ["PageDown", "Cmd+ArrowRight"], …)`); menus and tooltips keep showing the first. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
9a7d3e7699
commit
88bf89ea5d
8 files changed
+93
-9
No files matched your search
@@ -109,6 +109,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
label: "Run Commands (Batch)",
|
||||
menu: &[],
|
||||
shortcut: None,
|
||||
alt_shortcuts: &[],
|
||||
params: "{steps: [{command, params?}], label? (undo step name, default Batch), atomic?: bool (default true: a failing step rolls the whole batch back)} → {steps, results: [each step's result]}; a string param \"$N\" or \"$N.key.0\" is step N's result (1-based), e.g. {\"layer\": \"$1.layer\"}",
|
||||
enabled: always,
|
||||
run: batch,
|
||||
|
||||
@@ -405,6 +405,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
label: "Generate Fill Layer",
|
||||
menu: &[],
|
||||
shortcut: None,
|
||||
alt_shortcuts: &[],
|
||||
params: P,
|
||||
enabled: has_layers,
|
||||
run: generate,
|
||||
@@ -415,6 +416,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
label: "Content-Aware Fill Layer...",
|
||||
menu: &["Layer", "New"],
|
||||
shortcut: None,
|
||||
alt_shortcuts: &[],
|
||||
params: P,
|
||||
enabled: always,
|
||||
run: open_panel,
|
||||
|
||||
@@ -98,6 +98,8 @@ pub struct CommandSpec {
|
||||
pub menu: &'static [&'static str],
|
||||
/// Default shortcut (egui-style names, `Cmd` = ⌘ on macOS / Ctrl elsewhere).
|
||||
pub shortcut: Option<&'static str>,
|
||||
/// Further default shortcuts (the menus and tooltips show `shortcut`).
|
||||
pub alt_shortcuts: &'static [&'static str],
|
||||
/// Parameter description for agents and the MCP schema.
|
||||
pub params: &'static str,
|
||||
pub enabled: Enabled,
|
||||
@@ -108,8 +110,22 @@ pub struct CommandSpec {
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! cmd {
|
||||
// Several default shortcuts: `["PageDown", "Cmd+ArrowRight"]`.
|
||||
($id:literal, $label:literal, [$($m:literal),*], [$sc:literal, $($alt:literal),+], $params:literal, $en:expr, $run:expr) => {
|
||||
$crate::commands::CommandSpec { alt_shortcuts: &[$($alt),+], ..$crate::cmd!($id, $label, [$($m),*], Some($sc), $params, $en, $run) }
|
||||
};
|
||||
($id:literal, $label:literal, [$($m:literal),*], $sc:expr, $params:literal, $en:expr, $run:expr) => {
|
||||
$crate::commands::CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: $sc, params: $params, enabled: $en, run: $run, journal: true }
|
||||
$crate::commands::CommandSpec {
|
||||
id: $id,
|
||||
label: $label,
|
||||
menu: &[$($m),*],
|
||||
shortcut: $sc,
|
||||
alt_shortcuts: &[],
|
||||
params: $params,
|
||||
enabled: $en,
|
||||
run: $run,
|
||||
journal: true,
|
||||
}
|
||||
};
|
||||
}
|
||||
#[macro_export]
|
||||
@@ -120,6 +136,7 @@ macro_rules! query {
|
||||
label: $label,
|
||||
menu: &[],
|
||||
shortcut: None,
|
||||
alt_shortcuts: &[],
|
||||
params: $params,
|
||||
enabled: $crate::commands::always,
|
||||
run: $run,
|
||||
|
||||
@@ -10,10 +10,10 @@ use super::{CommandSpec, not_yet, not_yet_run};
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! stub {
|
||||
($id:literal, $label:literal, [$($m:literal),*], $params:literal) => {
|
||||
CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: None, params: $params, enabled: not_yet, run: not_yet_run, journal: true }
|
||||
CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: None, alt_shortcuts: &[], params: $params, enabled: not_yet, run: not_yet_run, journal: true }
|
||||
};
|
||||
($id:literal, $label:literal, [$($m:literal),*], $sc:literal, $params:literal) => {
|
||||
CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: Some($sc), params: $params, enabled: not_yet, run: not_yet_run, journal: true }
|
||||
CommandSpec { id: $id, label: $label, menu: &[$($m),*], shortcut: Some($sc), alt_shortcuts: &[], params: $params, enabled: not_yet, run: not_yet_run, journal: true }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -169,10 +169,10 @@ fn with_to(p: &Value, to: &str) -> Value {
|
||||
pub fn specs() -> Vec<CommandSpec> {
|
||||
vec![
|
||||
cmd!("time.set", "Go to Time...", ["View"], Some("Alt+Shift+J"), "{time? (s) | frame? | timecode?}", has_comp, set),
|
||||
cmd!("time.nextFrame", "Next Frame", [], Some("PageDown"), "{}", has_comp, |s, _| step(s, &json!({"frames": 1}))),
|
||||
cmd!("time.previousFrame", "Previous Frame", [], Some("PageUp"), "{}", has_comp, |s, _| step(s, &json!({"frames": -1}))),
|
||||
cmd!("time.forward10", "Forward 10 Frames", [], Some("Shift+PageDown"), "{}", has_comp, |s, _| step(s, &json!({"frames": 10}))),
|
||||
cmd!("time.back10", "Back 10 Frames", [], Some("Shift+PageUp"), "{}", has_comp, |s, _| step(s, &json!({"frames": -10}))),
|
||||
cmd!("time.nextFrame", "Next Frame", [], ["PageDown", "Cmd+ArrowRight"], "{}", has_comp, |s, _| step(s, &json!({"frames": 1}))),
|
||||
cmd!("time.previousFrame", "Previous Frame", [], ["PageUp", "Cmd+ArrowLeft"], "{}", has_comp, |s, _| step(s, &json!({"frames": -1}))),
|
||||
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"}))),
|
||||
|
||||
@@ -88,7 +88,9 @@ pub fn bindables(ui: &[UiCommand]) -> Vec<Bindable> {
|
||||
let entries = crate::menus::entries();
|
||||
for (path, e) in &entries {
|
||||
let key = binding_key(&e.command, &e.params);
|
||||
let defaults = e.shortcut.iter().filter_map(|s| normalize(s)).collect();
|
||||
// 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();
|
||||
push(
|
||||
Bindable {
|
||||
key,
|
||||
@@ -106,7 +108,7 @@ pub fn bindables(ui: &[UiCommand]) -> Vec<Bindable> {
|
||||
if !c.journal || entries.iter().any(|(_, e)| e.command == c.id) {
|
||||
continue;
|
||||
}
|
||||
let defaults = c.shortcut.and_then(normalize).into_iter().collect();
|
||||
let defaults = c.shortcut.into_iter().chain(c.alt_shortcuts.iter().copied()).filter_map(normalize).collect();
|
||||
push(
|
||||
Bindable {
|
||||
key: c.id.to_string(),
|
||||
|
||||
@@ -333,6 +333,24 @@ fn defaults_have_no_conflicts_and_match_the_menus() {
|
||||
assert!(t.bindables.len() > 500);
|
||||
}
|
||||
|
||||
/// 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() {
|
||||
let s = Session::default();
|
||||
let t = s.shortcuts();
|
||||
for (id, keys) in [
|
||||
("time.nextFrame", ["PageDown", "Cmd+ArrowRight"]),
|
||||
("time.previousFrame", ["PageUp", "Cmd+ArrowLeft"]),
|
||||
("time.forward10", ["Shift+PageDown", "Cmd+Shift+ArrowRight"]),
|
||||
("time.back10", ["Shift+PageUp", "Cmd+Shift+ArrowLeft"]),
|
||||
] {
|
||||
assert_eq!(t.keys.get(id).map(Vec::as_slice), Some(keys.map(String::from).as_slice()), "{id}");
|
||||
// The menus and tooltips keep showing the Page key.
|
||||
assert_eq!(t.shortcut_of(id, &json!(null)), Some(keys[0]));
|
||||
}
|
||||
assert!(t.all_conflicts().is_empty(), "{:?}", t.all_conflicts());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shortcut_edits_detect_conflicts_and_create_a_custom_preset() {
|
||||
let store = Arc::new(MemoryConfig::default());
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
//! Stepping the current time from the keyboard: Page Down / Page Up and Cmd+Right / Cmd+Left
|
||||
//! Arrow move one frame, with Shift ten.
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use egui::{Event, Key, Modifiers};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
fn key(h: &mut Harness<'_, EffectcraftApp>, key: Key, modifiers: Modifiers) {
|
||||
h.event(Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers });
|
||||
h.step();
|
||||
h.event(Event::Key { key, physical_key: None, pressed: false, repeat: false, modifiers });
|
||||
h.step();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cmd_arrows_step_the_current_frame() {
|
||||
let mut session = Session::default();
|
||||
session.execute("comp.new", json!({"name": "Main", "width": 320, "height": 180, "frameRate": 30.0, "duration": 4.0})).unwrap();
|
||||
let cid = session.state.active_comp.unwrap();
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_| EffectcraftApp::new(session));
|
||||
h.run_steps(2);
|
||||
let frame = |h: &Harness<'_, EffectcraftApp>| {
|
||||
let app = h.state();
|
||||
app.session.project.comp(cid).unwrap().frame_rate.frame_at(app.session.time())
|
||||
};
|
||||
assert_eq!(frame(&h), 0);
|
||||
let cmd = Modifiers::COMMAND;
|
||||
let cmd_shift = Modifiers::COMMAND | Modifiers::SHIFT;
|
||||
key(&mut h, Key::ArrowRight, cmd);
|
||||
assert_eq!(frame(&h), 1);
|
||||
key(&mut h, Key::PageDown, Modifiers::NONE);
|
||||
assert_eq!(frame(&h), 2);
|
||||
key(&mut h, Key::ArrowRight, cmd_shift);
|
||||
assert_eq!(frame(&h), 12);
|
||||
key(&mut h, Key::ArrowLeft, cmd);
|
||||
assert_eq!(frame(&h), 11);
|
||||
key(&mut h, Key::ArrowLeft, cmd_shift);
|
||||
assert_eq!(frame(&h), 1);
|
||||
// The plain arrows still nudge layers, not the time.
|
||||
key(&mut h, Key::ArrowRight, Modifiers::NONE);
|
||||
assert_eq!(frame(&h), 1);
|
||||
}
|
||||
Reference in new issue
Block a user