1use crate::settings::Settings;
7use eframe::egui::{self, Key, KeyboardShortcut, Modifiers};
8use std::collections::HashMap;
9
10macro_rules! actions {
11 ($($v:ident => $id:literal, $label:literal, $sc:expr;)*) => {
12 #[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
13 pub enum Action { $($v),* }
14 impl Action {
15 pub const ALL: &'static [Action] = &[$(Action::$v),*];
16 pub fn id(self) -> &'static str { match self { $(Action::$v => $id),* } }
18 pub fn label(self) -> &'static str { match self { $(Action::$v => $label),* } }
19 pub fn default_shortcut(self) -> Option<KeyboardShortcut> { match self { $(Action::$v => $sc),* } }
20 pub fn from_id(s: &str) -> Option<Action> { Self::ALL.iter().copied().find(|a| a.id() == s) }
21 }
22 };
23}
24
25const fn sc(m: Modifiers, k: Key) -> Option<KeyboardShortcut> {
26 Some(KeyboardShortcut::new(m, k))
27}
28const NONE: Modifiers = Modifiers::NONE;
29const CTRL: Modifiers = Modifiers::CTRL;
30const SHIFT: Modifiers = Modifiers::SHIFT;
31const ALT: Modifiers = Modifiers::ALT;
32const CTRL_SHIFT: Modifiers = Modifiers { alt: false, ctrl: true, shift: true, mac_cmd: false, command: false };
33const CTRL_ALT: Modifiers = Modifiers { alt: true, ctrl: true, shift: false, mac_cmd: false, command: false };
34
35fn canon(ks: &KeyboardShortcut) -> (bool, bool, bool, Key) {
37 (ks.modifiers.ctrl || ks.modifiers.command, ks.modifiers.shift, ks.modifiers.alt, ks.logical_key)
38}
39fn same(a: Option<KeyboardShortcut>, b: Option<KeyboardShortcut>) -> bool {
40 match (a, b) {
41 (None, None) => true,
42 (Some(a), Some(b)) => canon(&a) == canon(&b),
43 _ => false,
44 }
45}
46
47actions! {
48 NewProject => "new_project", "New Project", sc(CTRL, Key::N);
49 OpenFile => "open", "Open Video / Media…", sc(CTRL, Key::O);
50 OpenProject => "open_project", "Open Project…", sc(CTRL_SHIFT, Key::O);
51 Save => "save", "Save (project, or overwrite opened video)", sc(CTRL, Key::S);
52 SaveProjectAs => "save_project_as", "Save Project As…", sc(CTRL_SHIFT, Key::S);
53 ExportVideo => "export", "Export Video As…", sc(CTRL, Key::E);
54 ExportLossless => "export_lossless", "Fast Lossless Cut…", sc(CTRL_ALT, Key::E);
55 ExportXml => "export_xml", "Export Premiere / Resolve XML…", sc(CTRL_SHIFT, Key::E);
56 ImportMedia => "import", "Import Media…", sc(CTRL, Key::I);
57 Settings => "settings", "Settings…", sc(CTRL, Key::Comma);
58 Undo => "undo", "Undo", sc(CTRL, Key::Z);
59 Redo => "redo", "Redo", sc(CTRL_SHIFT, Key::Z);
60 PlayPause => "play_pause", "Play / Pause", sc(NONE, Key::Space);
61 Stop => "stop", "Stop", sc(NONE, Key::K);
62 StepBack => "step_back", "Step Back 1 Frame", sc(NONE, Key::ArrowLeft);
63 StepForward => "step_fwd", "Step Forward 1 Frame", sc(NONE, Key::ArrowRight);
64 GoStart => "go_start", "Go to Start", sc(NONE, Key::Home);
65 GoEnd => "go_end", "Go to End", sc(NONE, Key::End);
66 PrevCut => "prev_cut", "Previous Cut", sc(NONE, Key::ArrowUp);
67 NextCut => "next_cut", "Next Cut", sc(NONE, Key::ArrowDown);
68 Split => "split", "Split at Playhead", sc(CTRL, Key::B);
69 Delete => "delete", "Delete", sc(NONE, Key::Delete);
70 RippleDelete => "ripple_delete", "Ripple Delete", sc(SHIFT, Key::Delete);
71 SelectAll => "select_all", "Select All", sc(CTRL, Key::A);
72 Deselect => "deselect", "Deselect All", sc(CTRL_SHIFT, Key::A);
73 MarkIn => "mark_in", "Mark In", sc(NONE, Key::I);
74 MarkOut => "mark_out", "Mark Out", sc(NONE, Key::O);
75 ClearInOut => "clear_in_out", "Clear In / Out", sc(ALT, Key::X);
76 TrimToInOut => "trim_in_out", "Trim to In / Out", sc(CTRL_SHIFT, Key::I);
77 RippleDeleteInOut => "ripple_in_out", "Ripple Delete In / Out", sc(CTRL_SHIFT, Key::Delete);
78 AddText => "add_text", "Add Text Clip", sc(SHIFT, Key::T);
79 ZoomIn => "zoom_in", "Zoom In", sc(CTRL, Key::Equals);
80 ZoomOut => "zoom_out", "Zoom Out", sc(CTRL, Key::Minus);
81 ZoomFit => "zoom_fit", "Zoom to Fit", sc(SHIFT, Key::Z);
82 LinkToggle => "link", "Link / Unlink", sc(CTRL, Key::L);
83 ToggleEnabled => "toggle_enabled", "Enable / Disable Clip", sc(SHIFT, Key::D);
84 NudgeLeft => "nudge_left", "Nudge Left 1 Frame", sc(NONE, Key::Comma);
85 NudgeRight => "nudge_right", "Nudge Right 1 Frame", sc(NONE, Key::Period);
86 ToggleSnap => "snap", "Toggle Snapping", sc(NONE, Key::N);
87 AddVideoTrack => "add_video_track", "Add Video Track", None;
88 AddAudioTrack => "add_audio_track", "Add Audio Track", None;
89 ToggleLibrary => "toggle_library", "Show / Hide Library", sc(CTRL, Key::Num1);
90 ToggleInspector => "toggle_inspector", "Show / Hide Inspector", sc(CTRL, Key::Num2);
91 ToggleEffects => "toggle_effects", "Show / Hide Effects", sc(CTRL, Key::Num3);
92 ToggleTransitions => "toggle_transitions", "Show / Hide Transitions", sc(CTRL, Key::Num4);
93 ToggleCurves => "toggle_curves", "Show / Hide Curve Editor", sc(CTRL, Key::Num5);
94 ToggleSubtitles => "toggle_subtitles", "Show / Hide Subtitles", sc(CTRL, Key::Num6);
95 Retime => "retime", "Speed / Retime…", sc(CTRL, Key::R);
96 FreezeFrame => "freeze", "Freeze Frame at Playhead", sc(SHIFT, Key::R);
97 Fullscreen => "fullscreen", "Fullscreen Playback", sc(NONE, Key::F11);
98 AddTransition => "add_transition", "Add Transition at Selected Cut", sc(CTRL_SHIFT, Key::T);
99 AddSubtitle => "add_subtitle", "Add Subtitle at Playhead", sc(ALT, Key::S);
100 AddTransitionEnd => "add_transition_end", "Add Transition at Clip End", None;
101 TogglePlanner => "toggle_planner", "Show / Hide Planner", sc(CTRL, Key::Num7);
102 AutoCut => "auto_cut", "Auto-cut (silence) Panel", sc(CTRL_ALT, Key::A);
103 NestSequence => "nest", "Nest Selection into a New Sequence", sc(ALT, Key::N);
104 OpenParentSequence => "parent_sequence", "Back to Parent Timeline", sc(ALT, Key::ArrowUp);
105 SaveTemplate => "save_template", "Save Selection as Template…", None;
106 ApplyFlow => "flow", "Flow Motion Between Selected Clips", None;
107 AddLastTransition => "add_last_transition", "Add Last Used Transition", sc(CTRL, Key::T);
109 CopyAttributes => "copy_attrs", "Copy Attributes", sc(CTRL_ALT, Key::C);
110 PasteAttributes => "paste_attrs", "Paste Attributes…", sc(CTRL_ALT, Key::V);
111 AddMarker => "add_marker", "Add Marker at Playhead", sc(SHIFT, Key::M);
112 ToggleMarkers => "toggle_markers", "Show / Hide Markers", sc(CTRL, Key::Num8);
113 ToggleNodes => "toggle_nodes", "Show / Hide Node Editor", sc(CTRL, Key::Num9);
114 ToggleMixer => "toggle_mixer", "Show / Hide Mixer", sc(CTRL, Key::Num0);
115 ToggleTools => "toggle_tools", "Show / Hide Tools", None;
116 AddShape => "add_shape", "Add Shape", sc(SHIFT, Key::S);
117 AddAdjustment => "add_adjustment", "Add Adjustment Layer", sc(CTRL_ALT, Key::L);
118 AddMask => "add_mask", "Add Mask to Selection", sc(CTRL_SHIFT, Key::M);
119 ExportFrame => "export_frame", "Export Frame…", sc(CTRL_SHIFT, Key::F);
120 ScreenCapture => "screen_capture", "Screen Recording…", None;
121 Voiceover => "voiceover", "Record Voiceover…", sc(CTRL_ALT, Key::R);
122 ImportTimeline => "import_timeline", "Import Timeline (Premiere / Resolve XML, EDL)…", None;
123 MovieMode => "movie_mode", "Movie Mode (pre-render)", None;
124 CopyClips => "copy_clips", "Copy Clips", sc(CTRL, Key::C);
126 CutClips => "cut_clips", "Cut Clips", sc(CTRL, Key::X);
127 PasteClips => "paste_clips", "Paste Clips at Playhead", sc(CTRL, Key::V);
128 PasteInPlace => "paste_in_place", "Paste Clips on the First Free Track", sc(CTRL_SHIFT, Key::V);
129 PasteInsert => "paste_insert", "Paste Insert (ripple the rest right)", sc(NONE, Key::F10);
130 PasteAtTop => "paste_at_top", "Paste on a New Track at the Top", sc(NONE, Key::F9);
131 AddContainer => "add_container", "Add Container Clip at Playhead", None;
133 ReplaceContainerMedia => "replace_container", "Replace Container Media…", None;
134 MakeContainer => "make_container", "Convert to Container", None;
135 UnmakeContainer => "unmake_container", "Remove Container", None;
136}
137
138pub struct Hotkeys {
139 map: HashMap<Action, Option<KeyboardShortcut>>,
140}
141
142impl Hotkeys {
143 pub fn defaults() -> Self {
144 Self { map: Action::ALL.iter().map(|&a| (a, a.default_shortcut())).collect() }
145 }
146 pub fn from_settings(s: &Settings) -> Self {
147 let mut h = Self::defaults();
148 for (id, text) in &s.hotkeys {
149 if let Some(a) = Action::from_id(id) {
150 h.map.insert(a, Self::parse(text));
151 }
152 }
153 h
154 }
155 pub fn to_settings(&self, s: &mut Settings) {
157 s.hotkeys.clear();
158 for &a in Action::ALL {
159 let cur = self.get(a);
160 if !same(cur, a.default_shortcut()) {
161 s.hotkeys.insert(a.id().to_string(), cur.map(|k| Self::format(&k)).unwrap_or_default());
162 }
163 }
164 }
165 pub fn get(&self, a: Action) -> Option<KeyboardShortcut> {
166 self.map.get(&a).copied().flatten()
167 }
168 pub fn set(&mut self, a: Action, ks: Option<KeyboardShortcut>) {
170 if let Some(k) = ks {
171 for (_, v) in self.map.iter_mut() {
172 if same(*v, Some(k)) {
173 *v = None;
174 }
175 }
176 }
177 self.map.insert(a, ks);
178 }
179 pub fn reset(&mut self, a: Action) {
180 self.set(a, a.default_shortcut());
181 }
182 pub fn reset_all(&mut self) {
183 *self = Self::defaults();
184 }
185 pub fn conflict(&self, ks: KeyboardShortcut) -> Option<Action> {
187 Action::ALL.iter().copied().find(|&a| same(self.get(a), Some(ks)))
188 }
189 pub fn text(&self, a: Action) -> String {
191 self.get(a).map(|k| Self::format(&k)).unwrap_or_default()
192 }
193 pub fn format(ks: &KeyboardShortcut) -> String {
194 let mut s = String::new();
195 if ks.modifiers.ctrl || ks.modifiers.command {
196 s.push_str("Ctrl+");
197 }
198 if ks.modifiers.shift {
199 s.push_str("Shift+");
200 }
201 if ks.modifiers.alt {
202 s.push_str("Alt+");
203 }
204 s.push_str(ks.logical_key.name());
205 s
206 }
207 pub fn parse(s: &str) -> Option<KeyboardShortcut> {
208 let s = s.trim();
209 if s.is_empty() {
210 return None;
211 }
212 let mut m = Modifiers::NONE;
213 let mut key = None;
214 for part in s.split('+') {
215 match part.trim().to_ascii_lowercase().as_str() {
216 "ctrl" | "control" | "cmd" | "command" => m = m.plus(Modifiers::CTRL),
217 "shift" => m = m.plus(Modifiers::SHIFT),
218 "alt" => m = m.plus(Modifiers::ALT),
219 other => key = Key::from_name(other).or_else(|| Key::from_name(part.trim())),
220 }
221 }
222 key.map(|k| KeyboardShortcut::new(m, k))
223 }
224 pub fn poll(&self, ctx: &egui::Context) -> Vec<Action> {
226 self.poll_pass(ctx, false)
227 }
228 pub fn poll_late(&self, ctx: &egui::Context) -> Vec<Action> {
232 self.poll_pass(ctx, true)
233 }
234 fn poll_pass(&self, ctx: &egui::Context, late: bool) -> Vec<Action> {
235 if ctx.wants_keyboard_input() {
236 return Vec::new();
237 }
238 if !late {
239 restore_clipboard_keys(ctx);
240 }
241 let mut order: Vec<Action> = Action::ALL.iter().copied().filter(|&a| is_late(a) == late).collect();
244 order.sort_by_key(|&a| {
245 std::cmp::Reverse(self.get(a).map_or(0u8, |k| k.modifiers.shift as u8 + k.modifiers.alt as u8))
246 });
247 let mut out = Vec::new();
248 ctx.input_mut(|i| {
249 for a in order {
250 if let Some(ks) = self.get(a) {
251 if i.consume_shortcut(&ks) {
252 out.push(a);
253 }
254 }
255 }
256 });
257 out
258 }
259}
260
261fn is_late(a: Action) -> bool {
266 matches!(a, Action::CopyClips | Action::CutClips | Action::PasteClips | Action::PasteInPlace | Action::Delete)
267}
268
269fn restore_clipboard_keys(ctx: &egui::Context) {
275 ctx.input_mut(|i| {
276 let m = i.modifiers;
277 let cmd = m.ctrl || m.command;
278 let mut keys: Vec<Key> = Vec::new();
279 for e in &i.events {
280 match e {
281 egui::Event::Cut if m.shift => keys.push(Key::Delete),
282 egui::Event::Cut if cmd => keys.push(Key::X),
283 egui::Event::Copy if cmd => keys.push(Key::C),
284 egui::Event::Paste(_) if cmd => keys.push(Key::V),
285 _ => {}
286 }
287 }
288 for key in keys {
289 i.events.push(egui::Event::Key { key, physical_key: None, pressed: true, repeat: false, modifiers: m });
290 }
291 });
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297 #[test]
298 fn parse_format_roundtrip() {
299 for &a in Action::ALL {
300 if let Some(k) = a.default_shortcut() {
301 let txt = Hotkeys::format(&k);
302 assert!(same(Hotkeys::parse(&txt), Some(k)), "{txt}");
303 }
304 }
305 }
306 #[test]
310 fn clipboard_chords_survive_winit_translation() {
311 let h = Hotkeys::defaults();
312 let run = |ev: egui::Event, m: Modifiers| {
313 let ctx = egui::Context::default();
314 let raw = egui::RawInput { modifiers: m, events: vec![ev], ..Default::default() };
315 let (mut early, mut late) = (Vec::new(), Vec::new());
316 ctx.run(raw, |ctx| {
317 early = h.poll(ctx);
318 late = h.poll_late(ctx);
319 });
320 (early, late)
321 };
322 let ctrl = Modifiers { ctrl: true, command: true, ..Modifiers::NONE };
323 let ctrl_shift = Modifiers { shift: true, ..ctrl };
324 for (ev, m, want) in [
325 (egui::Event::Copy, ctrl, Action::CopyClips),
326 (egui::Event::Cut, ctrl, Action::CutClips),
327 (egui::Event::Paste("x".into()), ctrl, Action::PasteClips),
328 (egui::Event::Paste("x".into()), ctrl_shift, Action::PasteInPlace),
329 ] {
330 let (early, late) = run(ev, m);
331 assert!(early.is_empty(), "{want:?} must wait for the late pass, got {early:?}");
332 assert_eq!(late, vec![want]);
333 }
334 let (early, _) = run(egui::Event::Cut, Modifiers::SHIFT);
336 assert_eq!(early, vec![Action::RippleDelete]);
337 }
338
339 #[test]
340 fn no_duplicate_defaults() {
341 let mut seen = std::collections::HashSet::new();
342 for &a in Action::ALL {
343 if let Some(k) = a.default_shortcut() {
344 assert!(seen.insert(Hotkeys::format(&k)), "duplicate default {}", Hotkeys::format(&k));
345 }
346 }
347 }
348}