1use core::f32;
2use egui::{KeyboardShortcut, ModifierNames, Modifiers, Vec2};
3use eyre::Result;
4use serde::{Deserialize, Deserializer, Serialize};
5
6use crate::SystemState;
7use crate::message::{Message, MessageTarget};
8use crate::wave_data::{PER_SCROLL_EVENT, SCROLL_EVENTS_PER_PAGE};
9
10#[derive(Clone, Copy, Debug)]
12pub enum ShortcutAction {
13 OpenFile,
14 SwitchFile,
15 Redo,
16 Undo,
17 ToggleSidePanel,
18 ToggleToolbar,
19 GoToEnd,
20 GoToStart,
21 SaveStateFile,
22 GoToTop,
23 GoToBottom,
24 ItemFocus,
25 GroupNew,
26 SelectAll,
27 SelectToggle,
28 ReloadWaveform,
29 ZoomIn,
30 ZoomOut,
31 UiZoomIn,
32 UiZoomOut,
33 ScrollUp,
34 ScrollDown,
35 DeleteSelected,
36 MarkerAdd,
37 ToggleMenu,
38 ShowCommandPrompt,
39 RenameItem,
40 DividerAdd,
41}
42
43#[derive(Clone, Debug)]
45struct DispatchEntry {
46 action: ShortcutAction,
47 priority: u8,
48}
49
50#[derive(Clone, Debug, Serialize, Deserialize)]
51pub struct SurferShortcuts {
52 #[serde(with = "keyboard_shortcuts_serde")]
53 pub open_file: Vec<KeyboardShortcut>,
54 #[serde(with = "keyboard_shortcuts_serde")]
55 pub switch_file: Vec<KeyboardShortcut>,
56 #[serde(with = "keyboard_shortcuts_serde")]
57 pub undo: Vec<KeyboardShortcut>,
58 #[serde(with = "keyboard_shortcuts_serde")]
59 pub redo: Vec<KeyboardShortcut>,
60 #[serde(with = "keyboard_shortcuts_serde")]
61 pub toggle_side_panel: Vec<KeyboardShortcut>,
62 #[serde(with = "keyboard_shortcuts_serde")]
63 pub toggle_toolbar: Vec<KeyboardShortcut>,
64 #[serde(with = "keyboard_shortcuts_serde")]
65 pub goto_end: Vec<KeyboardShortcut>,
66 #[serde(with = "keyboard_shortcuts_serde")]
67 pub goto_start: Vec<KeyboardShortcut>,
68 #[serde(with = "keyboard_shortcuts_serde")]
69 pub save_state_file: Vec<KeyboardShortcut>,
70 #[serde(with = "keyboard_shortcuts_serde")]
71 pub goto_top: Vec<KeyboardShortcut>,
72 #[serde(with = "keyboard_shortcuts_serde")]
73 pub goto_bottom: Vec<KeyboardShortcut>,
74 #[serde(with = "keyboard_shortcuts_serde")]
75 pub group_new: Vec<KeyboardShortcut>,
76 #[serde(with = "keyboard_shortcuts_serde")]
77 pub item_focus: Vec<KeyboardShortcut>,
78 #[serde(with = "keyboard_shortcuts_serde")]
79 pub select_all: Vec<KeyboardShortcut>,
80 #[serde(with = "keyboard_shortcuts_serde")]
81 pub select_toggle: Vec<KeyboardShortcut>,
82 #[serde(with = "keyboard_shortcuts_serde")]
83 pub reload_waveform: Vec<KeyboardShortcut>,
84 #[serde(with = "keyboard_shortcuts_serde")]
85 pub zoom_in: Vec<KeyboardShortcut>,
86 #[serde(with = "keyboard_shortcuts_serde")]
87 pub zoom_out: Vec<KeyboardShortcut>,
88 #[serde(with = "keyboard_shortcuts_serde")]
89 pub ui_zoom_in: Vec<KeyboardShortcut>,
90 #[serde(with = "keyboard_shortcuts_serde")]
91 pub ui_zoom_out: Vec<KeyboardShortcut>,
92 #[serde(with = "keyboard_shortcuts_serde")]
93 pub scroll_up: Vec<KeyboardShortcut>,
94 #[serde(with = "keyboard_shortcuts_serde")]
95 pub scroll_down: Vec<KeyboardShortcut>,
96 #[serde(with = "keyboard_shortcuts_serde")]
97 pub delete_selected: Vec<KeyboardShortcut>,
98 #[serde(with = "keyboard_shortcuts_serde")]
99 pub marker_add: Vec<KeyboardShortcut>,
100 #[serde(with = "keyboard_shortcuts_serde")]
101 pub toggle_menu: Vec<KeyboardShortcut>,
102 #[serde(with = "keyboard_shortcuts_serde")]
103 pub show_command_prompt: Vec<KeyboardShortcut>,
104 #[serde(with = "keyboard_shortcuts_serde")]
105 pub rename_item: Vec<KeyboardShortcut>,
106 #[serde(with = "keyboard_shortcuts_serde")]
107 pub divider_add: Vec<KeyboardShortcut>,
108
109 #[serde(skip)]
110 cached_dispatch_table: Vec<DispatchEntry>,
111}
112
113pub fn deserialize_shortcuts<'de, D>(deserializer: D) -> Result<SurferShortcuts, D::Error>
114where
115 D: Deserializer<'de>,
116{
117 let mut shortcuts = SurferShortcuts::deserialize(deserializer)?;
118 shortcuts.cached_dispatch_table = shortcuts.build_dispatch_table();
119 Ok(shortcuts)
120}
121
122impl SurferShortcuts {
123 pub fn format_shortcut(&self, action: ShortcutAction) -> String {
124 #[cfg(any(not(target_os = "macos"), test))]
125 let is_mac = false;
126 #[cfg(all(target_os = "macos", not(test)))]
127 let is_mac = true;
128 self.shortcuts_for_action(action)
129 .iter()
130 .map(|kb| kb.format(&ModifierNames::NAMES, is_mac))
131 .collect::<Vec<String>>()
132 .join("/")
133 }
134
135 fn build_dispatch_table(&self) -> Vec<DispatchEntry> {
136 let mut dispatch_table = Vec::with_capacity(10);
138
139 dispatch_table.extend_from_slice(&[
141 DispatchEntry {
142 action: ShortcutAction::OpenFile,
143 priority: modifier_priority(&self.open_file),
144 },
145 DispatchEntry {
146 action: ShortcutAction::SwitchFile,
147 priority: modifier_priority(&self.switch_file),
148 },
149 DispatchEntry {
150 action: ShortcutAction::Redo,
151 priority: modifier_priority(&self.redo),
152 },
153 DispatchEntry {
154 action: ShortcutAction::Undo,
155 priority: modifier_priority(&self.undo),
156 },
157 DispatchEntry {
158 action: ShortcutAction::ToggleSidePanel,
159 priority: modifier_priority(&self.toggle_side_panel),
160 },
161 DispatchEntry {
162 action: ShortcutAction::ToggleToolbar,
163 priority: modifier_priority(&self.toggle_toolbar),
164 },
165 DispatchEntry {
166 action: ShortcutAction::GoToEnd,
167 priority: modifier_priority(&self.goto_end),
168 },
169 DispatchEntry {
170 action: ShortcutAction::GoToStart,
171 priority: modifier_priority(&self.goto_start),
172 },
173 DispatchEntry {
174 action: ShortcutAction::SaveStateFile,
175 priority: modifier_priority(&self.save_state_file),
176 },
177 DispatchEntry {
178 action: ShortcutAction::GoToTop,
179 priority: modifier_priority(&self.goto_top),
180 },
181 DispatchEntry {
182 action: ShortcutAction::GoToBottom,
183 priority: modifier_priority(&self.goto_bottom),
184 },
185 DispatchEntry {
186 action: ShortcutAction::GroupNew,
187 priority: modifier_priority(&self.group_new),
188 },
189 DispatchEntry {
190 action: ShortcutAction::ItemFocus,
191 priority: modifier_priority(&self.item_focus),
192 },
193 DispatchEntry {
194 action: ShortcutAction::SelectAll,
195 priority: modifier_priority(&self.select_all),
196 },
197 DispatchEntry {
198 action: ShortcutAction::SelectToggle,
199 priority: modifier_priority(&self.select_toggle),
200 },
201 DispatchEntry {
202 action: ShortcutAction::ReloadWaveform,
203 priority: modifier_priority(&self.reload_waveform),
204 },
205 DispatchEntry {
206 action: ShortcutAction::ZoomIn,
207 priority: modifier_priority(&self.zoom_in),
208 },
209 DispatchEntry {
210 action: ShortcutAction::ZoomOut,
211 priority: modifier_priority(&self.zoom_out),
212 },
213 DispatchEntry {
214 action: ShortcutAction::UiZoomIn,
215 priority: modifier_priority(&self.ui_zoom_in),
216 },
217 DispatchEntry {
218 action: ShortcutAction::UiZoomOut,
219 priority: modifier_priority(&self.ui_zoom_out),
220 },
221 DispatchEntry {
222 action: ShortcutAction::ScrollUp,
223 priority: modifier_priority(&self.scroll_up),
224 },
225 DispatchEntry {
226 action: ShortcutAction::ScrollDown,
227 priority: modifier_priority(&self.scroll_down),
228 },
229 DispatchEntry {
230 action: ShortcutAction::DeleteSelected,
231 priority: modifier_priority(&self.delete_selected),
232 },
233 DispatchEntry {
234 action: ShortcutAction::MarkerAdd,
235 priority: modifier_priority(&self.marker_add),
236 },
237 DispatchEntry {
238 action: ShortcutAction::ToggleMenu,
239 priority: modifier_priority(&self.toggle_menu),
240 },
241 DispatchEntry {
242 action: ShortcutAction::ShowCommandPrompt,
243 priority: modifier_priority(&self.show_command_prompt),
244 },
245 DispatchEntry {
246 action: ShortcutAction::RenameItem,
247 priority: modifier_priority(&self.rename_item),
248 },
249 DispatchEntry {
250 action: ShortcutAction::DividerAdd,
251 priority: modifier_priority(&self.divider_add),
252 },
253 ]);
254
255 dispatch_table.sort_by_key(|entry| entry.priority);
257 dispatch_table
258 }
259
260 fn shortcuts_for_action(&self, action: ShortcutAction) -> &[KeyboardShortcut] {
261 match action {
262 ShortcutAction::OpenFile => &self.open_file,
263 ShortcutAction::SwitchFile => &self.switch_file,
264 ShortcutAction::Undo => &self.undo,
265 ShortcutAction::Redo => &self.redo,
266 ShortcutAction::ToggleSidePanel => &self.toggle_side_panel,
267 ShortcutAction::ToggleToolbar => &self.toggle_toolbar,
268 ShortcutAction::GoToEnd => &self.goto_end,
269 ShortcutAction::GoToStart => &self.goto_start,
270 ShortcutAction::SaveStateFile => &self.save_state_file,
271 ShortcutAction::GoToTop => &self.goto_top,
272 ShortcutAction::GoToBottom => &self.goto_bottom,
273 ShortcutAction::ItemFocus => &self.item_focus,
274 ShortcutAction::GroupNew => &self.group_new,
275 ShortcutAction::SelectAll => &self.select_all,
276 ShortcutAction::SelectToggle => &self.select_toggle,
277 ShortcutAction::ReloadWaveform => &self.reload_waveform,
278 ShortcutAction::ZoomIn => &self.zoom_in,
279 ShortcutAction::ZoomOut => &self.zoom_out,
280 ShortcutAction::UiZoomIn => &self.ui_zoom_in,
281 ShortcutAction::UiZoomOut => &self.ui_zoom_out,
282 ShortcutAction::ScrollUp => &self.scroll_up,
283 ShortcutAction::ScrollDown => &self.scroll_down,
284 ShortcutAction::DeleteSelected => &self.delete_selected,
285 ShortcutAction::MarkerAdd => &self.marker_add,
286 ShortcutAction::ToggleMenu => &self.toggle_menu,
287 ShortcutAction::ShowCommandPrompt => &self.show_command_prompt,
288 ShortcutAction::RenameItem => &self.rename_item,
289 ShortcutAction::DividerAdd => &self.divider_add,
290 }
291 }
292
293 fn execute_action(&self, action: ShortcutAction, msgs: &mut Vec<Message>, state: &SystemState) {
294 match action {
295 ShortcutAction::OpenFile => {
296 msgs.push(Message::OpenFileDialog(crate::file_dialog::OpenMode::Open));
297 }
298 ShortcutAction::SwitchFile => {
299 msgs.push(Message::OpenFileDialog(
300 crate::file_dialog::OpenMode::Switch,
301 ));
302 }
303 ShortcutAction::Redo => {
304 msgs.push(Message::Redo(state.get_count()));
305 }
306 ShortcutAction::Undo => {
307 msgs.push(Message::Undo(state.get_count()));
308 }
309 ShortcutAction::ToggleSidePanel => {
310 msgs.push(Message::SetSidePanelVisible(!state.show_hierarchy()));
311 }
312 ShortcutAction::ToggleToolbar => {
313 msgs.push(Message::SetToolbarVisible(!state.show_toolbar()));
314 }
315 ShortcutAction::GoToEnd => {
316 msgs.push(Message::GoToEnd { viewport_idx: 0 });
317 }
318 ShortcutAction::GoToStart => {
319 msgs.push(Message::GoToStart { viewport_idx: 0 });
320 }
321 ShortcutAction::SaveStateFile => {
322 msgs.push(Message::SaveStateFile(state.user.state_file.clone()));
323 }
324 ShortcutAction::GoToTop => {
325 msgs.push(Message::ScrollToItem(0));
326 }
327 ShortcutAction::GoToBottom => {
328 if let Some(waves) = &state.user.waves
329 && waves.displayed_items.len() > 1
330 {
331 msgs.push(Message::ScrollToItem(waves.displayed_items.len() - 1));
332 }
333 }
334 ShortcutAction::GroupNew => {
335 msgs.push(Message::GroupNew {
336 name: None,
337 before: None,
338 items: None,
339 });
340 msgs.push(Message::ShowCommandPrompt("item_rename ".to_owned(), None));
341 }
342 ShortcutAction::ItemFocus => {
343 msgs.push(Message::ShowCommandPrompt("item_focus ".to_string(), None));
344 }
345 ShortcutAction::SelectAll => {
346 msgs.push(Message::ItemSelectAll);
347 }
348 ShortcutAction::SelectToggle => {
349 msgs.push(Message::ToggleItemSelected(None));
350 }
351 ShortcutAction::ReloadWaveform => {
352 msgs.push(Message::ReloadWaveform(
353 state.user.config.behavior.keep_during_reload,
354 ));
355 }
356 ShortcutAction::ZoomIn => {
357 msgs.push(Message::CanvasZoom {
358 mouse_ptr: None,
359 delta: 0.5,
360 viewport_idx: 0,
361 });
362 }
363 ShortcutAction::ZoomOut => {
364 msgs.push(Message::CanvasZoom {
365 mouse_ptr: None,
366 delta: 2.0,
367 viewport_idx: 0,
368 });
369 }
370 ShortcutAction::UiZoomIn => {
371 let mut next_factor = 0f32;
372 for factor in &state.user.config.layout.zoom_factors {
373 if *factor < state.ui_zoom_factor() && *factor > next_factor {
374 next_factor = *factor;
375 }
376 }
377 if next_factor > 0f32 {
378 msgs.push(Message::SetUIZoomFactor(next_factor));
379 }
380 }
381 ShortcutAction::UiZoomOut => {
382 let mut next_factor = f32::INFINITY;
383 for factor in &state.user.config.layout.zoom_factors {
384 if *factor > state.ui_zoom_factor() && *factor < next_factor {
385 next_factor = *factor;
386 }
387 }
388 if next_factor != f32::INFINITY {
389 msgs.push(Message::SetUIZoomFactor(next_factor));
390 }
391 }
392 ShortcutAction::ScrollUp => {
393 msgs.push(Message::CanvasScroll {
394 delta: Vec2 {
395 x: 0.,
396 y: -PER_SCROLL_EVENT * SCROLL_EVENTS_PER_PAGE,
397 },
398 viewport_idx: 0,
399 });
400 }
401 ShortcutAction::ScrollDown => {
402 msgs.push(Message::CanvasScroll {
403 delta: Vec2 {
404 x: 0.,
405 y: PER_SCROLL_EVENT * SCROLL_EVENTS_PER_PAGE,
406 },
407 viewport_idx: 0,
408 });
409 }
410 ShortcutAction::DeleteSelected => {
411 msgs.push(Message::RemoveVisibleItems(MessageTarget::CurrentSelection));
412 }
413 ShortcutAction::MarkerAdd => {
414 if let Some(waves) = state.user.waves.as_ref()
415 && let Some(cursor) = waves.cursor.as_ref()
416 {
417 let marker_exists = waves
419 .markers
420 .values()
421 .any(|marker_time| marker_time == cursor);
422 if !marker_exists {
423 msgs.push(Message::AddMarker {
424 time: cursor.clone(),
425 name: None,
426 move_focus: state.user.config.layout.move_focus_on_inserted_marker(),
427 });
428 }
429 }
430 }
431 ShortcutAction::ToggleMenu => {
432 msgs.push(Message::SetMenuVisible(!state.show_menu()));
433 }
434 ShortcutAction::ShowCommandPrompt => {
435 msgs.push(Message::ShowCommandPrompt(String::new(), None));
436 }
437 ShortcutAction::RenameItem => {
438 if let Some(waves) = &state.user.waves
439 && waves.focused_item.is_some()
440 {
441 msgs.push(Message::ShowCommandPrompt("item_rename".to_owned(), None));
442 }
443 }
444 ShortcutAction::DividerAdd => {
445 msgs.push(Message::AddDivider(None, None));
446 }
447 }
448 }
449
450 pub fn process(&self, ctx: &egui::Context, msgs: &mut Vec<Message>, state: &SystemState) {
451 for entry in &self.cached_dispatch_table {
453 if self
454 .shortcuts_for_action(entry.action)
455 .iter()
456 .any(|shortcut| ctx.input_mut(|i| i.consume_shortcut(shortcut)))
457 {
458 self.execute_action(entry.action, msgs, state);
459 }
460 }
461 }
462}
463
464fn modifier_priority(shortcuts: &[KeyboardShortcut]) -> u8 {
465 shortcuts
466 .iter()
467 .find_map(|shortcut| {
468 let has_shift = shortcut.modifiers.contains(Modifiers::SHIFT);
469 let has_alt = shortcut.modifiers.contains(Modifiers::ALT);
470
471 match (has_shift, has_alt) {
472 (true, true) => Some(0), (_, true) => Some(1), (true, _) => Some(2), _ => None,
476 }
477 })
478 .unwrap_or(3) }
480
481mod keyboard_shortcuts_serde {
483 use egui::Key;
484 use serde::{Deserializer, Serializer};
485
486 use super::*;
487
488 pub fn serialize<S>(shortcuts: &[KeyboardShortcut], serializer: S) -> Result<S::Ok, S::Error>
489 where
490 S: Serializer,
491 {
492 let bindings: Vec<String> = shortcuts
493 .iter()
494 .map(|s| format_binding(s.modifiers, s.logical_key))
495 .collect();
496 bindings.serialize(serializer)
497 }
498
499 pub fn deserialize<'de, D>(deserializer: D) -> Result<Vec<KeyboardShortcut>, D::Error>
500 where
501 D: Deserializer<'de>,
502 {
503 let bindings: Vec<String> = Vec::deserialize(deserializer)?;
504 bindings
505 .iter()
506 .map(|s| parse_binding(s).map_err(serde::de::Error::custom))
507 .collect()
508 }
509
510 fn format_binding(modifiers: Modifiers, logical_key: Key) -> String {
511 const MODIFIER_NAMES: &[(Modifiers, &str)] = &[
512 (Modifiers::CTRL, "Ctrl"),
513 (Modifiers::SHIFT, "Shift"),
514 (Modifiers::ALT, "Alt"),
515 (Modifiers::MAC_CMD, "Mac_cmd"),
516 (Modifiers::COMMAND, "Command"),
517 ];
518
519 let mut parts = Vec::with_capacity(6);
521
522 for (modifier, name) in MODIFIER_NAMES {
523 if modifiers.contains(*modifier) {
524 parts.push(*name);
525 }
526 }
527 let key_name = format!("{:?}", logical_key);
528 parts.push(&key_name);
529 parts.join("+")
530 }
531
532 fn parse_binding(binding: &str) -> Result<KeyboardShortcut, String> {
533 const MODIFIER_MAP: &[(&str, Modifiers)] = &[
534 ("ctrl", Modifiers::CTRL),
535 ("shift", Modifiers::SHIFT),
536 ("alt", Modifiers::ALT),
537 ("mac_cmd", Modifiers::MAC_CMD),
538 ("command", Modifiers::COMMAND),
539 ("cmd", Modifiers::COMMAND),
540 ];
541
542 let parts: Vec<&str> = binding.split('+').map(|s| s.trim()).collect();
543
544 let (modifier_parts, key_str) = match parts.as_slice() {
546 [modifiers @ .., key] => (modifiers, *key),
547 [] => return Err("Empty binding".to_string()),
548 };
549
550 let logical_key =
551 Key::from_name(key_str).ok_or_else(|| format!("Unknown key: {}", key_str))?;
552
553 let modifiers = modifier_parts
555 .iter()
556 .try_fold(Modifiers::NONE, |acc, &modifier_str| {
557 let lower = modifier_str.to_lowercase();
558 MODIFIER_MAP
559 .iter()
560 .find(|(name, _)| name == &lower)
561 .map(|(_, mod_bit)| acc | *mod_bit)
562 .ok_or_else(|| format!("Unknown modifier: {}", modifier_str))
563 })?;
564
565 Ok(KeyboardShortcut::new(modifiers, logical_key))
566 }
567}