use egui::{Key, KeyboardShortcut, Modifiers};
use std::collections::{BTreeMap, HashMap};
#[derive(
Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, serde::Deserialize, serde::Serialize,
)]
pub enum Action {
Copy,
Paste,
NewGraph,
Undo,
Redo,
#[serde(alias = "ToggleCommandPalette")]
ToggleNodePalette,
SelectAll,
Cut,
Duplicate,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct Keymap {
#[serde(default)]
overrides: BTreeMap<Action, Vec<KeyboardShortcut>>,
}
const CMD: Modifiers = Modifiers {
alt: false,
ctrl: false,
shift: false,
mac_cmd: false,
command: true,
};
const CMD_SHIFT: Modifiers = Modifiers {
alt: false,
ctrl: false,
shift: true,
mac_cmd: false,
command: true,
};
const COPY: &[KeyboardShortcut] = &[KeyboardShortcut::new(CMD, Key::C)];
const PASTE: &[KeyboardShortcut] = &[KeyboardShortcut::new(CMD, Key::V)];
const NEW_GRAPH: &[KeyboardShortcut] = &[KeyboardShortcut::new(CMD, Key::T)];
const UNDO: &[KeyboardShortcut] = &[KeyboardShortcut::new(CMD, Key::Z)];
const REDO: &[KeyboardShortcut] = &[
KeyboardShortcut::new(CMD_SHIFT, Key::Z),
KeyboardShortcut::new(CMD, Key::Y),
];
const TOGGLE_NODE_PALETTE: &[KeyboardShortcut] =
&[KeyboardShortcut::new(Modifiers::NONE, Key::Space)];
const SELECT_ALL: &[KeyboardShortcut] = &[KeyboardShortcut::new(CMD, Key::A)];
const CUT: &[KeyboardShortcut] = &[KeyboardShortcut::new(CMD, Key::X)];
const DUPLICATE: &[KeyboardShortcut] = &[KeyboardShortcut::new(CMD, Key::D)];
impl Action {
pub const ALL: &'static [Action] = &[
Action::Copy,
Action::Paste,
Action::NewGraph,
Action::Undo,
Action::Redo,
Action::ToggleNodePalette,
Action::SelectAll,
Action::Cut,
Action::Duplicate,
];
pub fn label(self) -> &'static str {
match self {
Action::Copy => "Copy",
Action::Paste => "Paste",
Action::NewGraph => "New graph",
Action::Undo => "Undo",
Action::Redo => "Redo",
Action::ToggleNodePalette => "Node palette",
Action::SelectAll => "Select all",
Action::Cut => "Cut",
Action::Duplicate => "Duplicate",
}
}
pub fn description(self) -> &'static str {
match self {
Action::Copy => "Copy the selected nodes to the clipboard.",
Action::Paste => "Paste nodes from the clipboard into the focused graph.",
Action::NewGraph => "Open a new graph in a new tab.",
Action::Undo => "Undo the last change to the focused graph.",
Action::Redo => "Redo the last undone change to the focused graph.",
Action::ToggleNodePalette => "Show or hide the node palette for creating nodes.",
Action::SelectAll => "Select every node in the focused graph.",
Action::Cut => "Copy the selected nodes to the clipboard, then remove them.",
Action::Duplicate => "Duplicate the selected nodes in place.",
}
}
pub fn default_bindings(self) -> &'static [KeyboardShortcut] {
match self {
Action::Copy => COPY,
Action::Paste => PASTE,
Action::NewGraph => NEW_GRAPH,
Action::Undo => UNDO,
Action::Redo => REDO,
Action::ToggleNodePalette => TOGGLE_NODE_PALETTE,
Action::SelectAll => SELECT_ALL,
Action::Cut => CUT,
Action::Duplicate => DUPLICATE,
}
}
}
impl Keymap {
pub fn bindings(&self, action: Action) -> &[KeyboardShortcut] {
match self.overrides.get(&action) {
Some(bindings) => bindings,
None => action.default_bindings(),
}
}
pub fn is_overridden(&self, action: Action) -> bool {
self.overrides.contains_key(&action)
}
pub fn consume(&self, ui: &egui::Ui, action: Action) -> bool {
let bindings = self.bindings(action);
ui.input_mut(|i| {
bindings
.iter()
.fold(false, |fired, s| i.consume_shortcut(s) | fired)
})
}
pub fn set(&mut self, action: Action, bindings: Vec<KeyboardShortcut>) {
if bindings.as_slice() == action.default_bindings() {
self.overrides.remove(&action);
} else {
self.overrides.insert(action, bindings);
}
}
pub fn add(&mut self, action: Action, shortcut: KeyboardShortcut) {
let mut bindings = self.bindings(action).to_vec();
if !bindings.contains(&shortcut) {
bindings.push(shortcut);
self.set(action, bindings);
}
}
pub fn remove(&mut self, action: Action, shortcut: KeyboardShortcut) {
let mut bindings = self.bindings(action).to_vec();
bindings.retain(|&s| s != shortcut);
self.set(action, bindings);
}
pub fn reset(&mut self, action: Action) {
self.overrides.remove(&action);
}
pub fn reset_all(&mut self) {
self.overrides.clear();
}
pub fn conflicts(&self) -> HashMap<KeyboardShortcut, Vec<Action>> {
let mut by_shortcut: HashMap<KeyboardShortcut, Vec<Action>> = HashMap::new();
for &action in Action::ALL {
for &shortcut in self.bindings(action) {
let actions = by_shortcut.entry(shortcut).or_default();
if !actions.contains(&action) {
actions.push(action);
}
}
}
by_shortcut.retain(|_, actions| actions.len() > 1);
by_shortcut
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_for_absent_actions() {
let km = Keymap::default();
assert_eq!(km.bindings(Action::Copy), Action::Copy.default_bindings());
assert_eq!(km.bindings(Action::Copy).len(), 1);
assert_eq!(km.bindings(Action::Redo).len(), 2);
assert!(!km.is_overridden(Action::Copy));
}
#[test]
fn set_reset_and_sparseness() {
let mut km = Keymap::default();
let new = vec![KeyboardShortcut::new(CMD_SHIFT, Key::C)];
km.set(Action::Copy, new.clone());
assert!(km.is_overridden(Action::Copy));
assert_eq!(km.bindings(Action::Copy), new.as_slice());
km.set(Action::Copy, Action::Copy.default_bindings().to_vec());
assert!(!km.is_overridden(Action::Copy));
km.set(Action::Copy, new);
km.reset(Action::Copy);
assert!(!km.is_overridden(Action::Copy));
}
#[test]
fn add_and_remove_bindings() {
let mut km = Keymap::default();
let extra = KeyboardShortcut::new(CMD, Key::Insert);
km.add(Action::Copy, extra);
assert!(km.bindings(Action::Copy).contains(&extra));
km.add(Action::Copy, extra);
assert_eq!(km.bindings(Action::Copy).len(), 2);
km.remove(Action::Copy, extra);
assert!(!km.is_overridden(Action::Copy));
}
#[test]
fn conflicts_detects_shared_binding() {
let mut km = Keymap::default();
assert!(km.conflicts().is_empty());
km.set(Action::Paste, vec![KeyboardShortcut::new(CMD, Key::C)]);
let conflicts = km.conflicts();
let shortcut = KeyboardShortcut::new(CMD, Key::C);
let actions = conflicts.get(&shortcut).expect("expected a conflict");
assert!(actions.contains(&Action::Copy));
assert!(actions.contains(&Action::Paste));
}
#[test]
fn serde_round_trip_is_sparse() {
let mut km = Keymap::default();
km.set(Action::Undo, vec![KeyboardShortcut::new(CMD, Key::U)]);
let encoded = ron::to_string(&km).unwrap();
let back: Keymap = ron::from_str(&encoded).unwrap();
assert_eq!(km, back);
assert!(back.is_overridden(Action::Undo));
assert!(!back.is_overridden(Action::Copy));
}
}