mod environment;
mod recording_command;
mod redap_server_command;
mod table_command;
mod ui_command;
pub use self::environment::CommandEnvironment;
pub use self::recording_command::{
RecordingCommand, RecordingCommandKind, RecordingCommandSender, SetPlaybackSpeed,
};
pub use self::redap_server_command::{
RedapServerCommand, RedapServerCommandKind, RedapServerCommandSender,
};
pub use self::table_command::{TableCommand, TableCommandKind, TableCommandSender};
pub use self::ui_command::{UICommand, UICommandSender};
use egui::KeyboardShortcut;
use smallvec::{SmallVec, smallvec};
pub fn refresh_shortcuts(os: egui::os::OperatingSystem) -> SmallVec<[KeyboardShortcut; 2]> {
use egui::{Key, KeyboardShortcut, Modifiers};
if os == egui::os::OperatingSystem::Mac {
smallvec![
KeyboardShortcut::new(Modifiers::COMMAND, Key::R),
KeyboardShortcut::new(Modifiers::NONE, Key::F5),
]
} else {
smallvec![KeyboardShortcut::new(Modifiers::NONE, Key::F5)]
}
}
#[derive(Clone, Debug)]
pub enum ResolvedCommand {
Ui(UICommand),
Recording(RecordingCommand),
RedapServer(RedapServerCommand),
Table(TableCommand),
}
pub fn consume_timeline_shortcut(egui_ctx: &egui::Context) -> Option<RecordingCommandKind> {
use strum::IntoEnumIterator as _;
let os = egui_ctx.os();
let commands = RecordingCommandKind::iter()
.filter(|kind| kind.is_timeline())
.flat_map(|kind| {
kind.kb_shortcuts(os)
.into_iter()
.map(move |kb_shortcut| (kb_shortcut, kind))
})
.collect();
consume_best_shortcut(egui_ctx, commands)
.or_else(|| RecordingCommandKind::handle_playback_chord(egui_ctx))
}
pub fn listen_for_kb_shortcuts(
egui_ctx: &egui::Context,
env: &CommandEnvironment,
) -> Option<ResolvedCommand> {
use strum::IntoEnumIterator as _;
#[derive(Clone, Copy)]
enum Matched {
Ui(UICommand),
Recording(RecordingCommandKind),
RedapServer(RedapServerCommandKind),
Table(TableCommandKind),
}
let os = egui_ctx.os();
let commands = itertools::chain!(
UICommand::iter().flat_map(|cmd| {
cmd.kb_shortcuts(os)
.into_iter()
.map(move |kb_shortcut| (kb_shortcut, Matched::Ui(cmd)))
}),
RecordingCommandKind::iter()
.filter(|kind| !kind.is_timeline())
.flat_map(|kind| {
kind.kb_shortcuts(os)
.into_iter()
.map(move |kb_shortcut| (kb_shortcut, Matched::Recording(kind)))
}),
TableCommandKind::iter()
.filter(|kind| kind.for_environment(env).is_some())
.flat_map(|kind| {
kind.kb_shortcuts(os)
.into_iter()
.map(move |kb_shortcut| (kb_shortcut, Matched::Table(kind)))
}),
RedapServerCommandKind::iter()
.filter(|kind| kind.for_environment(env).is_some())
.flat_map(|kind| {
kind.kb_shortcuts(os)
.into_iter()
.map(move |kb_shortcut| (kb_shortcut, Matched::RedapServer(kind)))
}),
)
.collect();
match consume_best_shortcut(egui_ctx, commands)? {
Matched::Ui(cmd) => Some(ResolvedCommand::Ui(cmd)),
Matched::Recording(kind) => kind.for_environment(env).map(ResolvedCommand::Recording),
Matched::RedapServer(kind) => kind.for_environment(env).map(ResolvedCommand::RedapServer),
Matched::Table(kind) => kind.for_environment(env).map(ResolvedCommand::Table),
}
}
fn consume_best_shortcut<Cmd: Copy>(
egui_ctx: &egui::Context,
mut commands: Vec<(KeyboardShortcut, Cmd)>,
) -> Option<Cmd> {
use crate::egui_ext::KeyboardShortcutExt as _;
let text_edit_has_focus = egui_ctx.text_edit_focused();
commands.sort_by_key(|(kb_shortcut, _cmd)| {
let num_shift_alts = kb_shortcut.modifiers.shift as i32 + kb_shortcut.modifiers.alt as i32;
-num_shift_alts });
egui_ctx.input_mut(|input| {
for (kb_shortcut, command) in commands {
if text_edit_has_focus && kb_shortcut.conflicts_with_text_editing() {
continue; }
if input.consume_shortcut(&kb_shortcut) {
input.keys_down.remove(&kb_shortcut.logical_key);
return Some(command);
}
}
None
})
}
#[test]
fn check_for_clashing_command_shortcuts() {
use egui::os::OperatingSystem;
fn clashes(a: KeyboardShortcut, b: KeyboardShortcut) -> bool {
if a.logical_key != b.logical_key {
return false;
}
if a.modifiers.alt != b.modifiers.alt {
return false;
}
if a.modifiers.shift != b.modifiers.shift {
return false;
}
(a.modifiers.command || a.modifiers.ctrl) == (b.modifiers.command || b.modifiers.ctrl)
}
use strum::IntoEnumIterator as _;
for os in [
OperatingSystem::Mac,
OperatingSystem::Windows,
OperatingSystem::Nix,
] {
let all_commands: Vec<(String, KeyboardShortcut)> = itertools::chain!(
UICommand::iter().flat_map(|cmd| {
cmd.kb_shortcuts(os)
.into_iter()
.map(move |shortcut| (format!("Ui::{cmd:?}"), shortcut))
}),
RecordingCommandKind::iter().flat_map(|kind| {
kind.kb_shortcuts(os)
.into_iter()
.map(move |shortcut| (format!("Recording::{kind:?}"), shortcut))
}),
RedapServerCommandKind::iter().flat_map(|kind| {
kind.kb_shortcuts(os)
.into_iter()
.map(move |shortcut| (format!("RedapServer::{kind:?}"), shortcut))
}),
TableCommandKind::iter().flat_map(|kind| {
kind.kb_shortcuts(os)
.into_iter()
.map(move |shortcut| (format!("Table::{kind:?}"), shortcut))
}),
)
.collect();
let intentional_clash = |a: &str, b: &str| {
let pair = ("RedapServer::Refresh", "Table::Refresh");
(a, b) == pair || (b, a) == pair
};
for (a_name, a_shortcut) in &all_commands {
for (b_name, b_shortcut) in &all_commands {
if a_name == b_name || intentional_clash(a_name, b_name) {
continue;
}
assert!(
!clashes(*a_shortcut, *b_shortcut),
"Command '{a_name}' and '{b_name}' have overlapping keyboard shortcuts: {:?} vs {:?}",
a_shortcut.format(&egui::ModifierNames::NAMES, true),
b_shortcut.format(&egui::ModifierNames::NAMES, true),
);
}
}
}
}