use std::{
borrow::Borrow,
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
str::FromStr
};
use crate::keyboard::{KeyCode, ModifiersState, NativeKeyCode};
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Accelerator {
id: Option<AcceleratorId>,
pub(crate) mods: ModifiersState,
pub(crate) key: KeyCode
}
impl Accelerator {
pub fn new(mods: impl Into<Option<ModifiersState>>, key: KeyCode) -> Self {
Self {
id: None,
mods: mods.into().unwrap_or_else(ModifiersState::empty),
key
}
}
pub fn with_id(mut self, id: AcceleratorId) -> Self {
self.id = Some(id);
self
}
pub fn id(self) -> AcceleratorId {
if let Some(id) = self.id {
return id;
}
AcceleratorId(hash_accelerator_to_u16(self))
}
pub fn matches(&self, modifiers: impl Borrow<ModifiersState>, key: impl Borrow<KeyCode>) -> bool {
let base_mods = ModifiersState::SHIFT | ModifiersState::CONTROL | ModifiersState::ALT | ModifiersState::SUPER;
let modifiers = modifiers.borrow();
let key = key.borrow();
self.mods == *modifiers & base_mods && self.key == *key
}
}
impl FromStr for Accelerator {
type Err = AcceleratorParseError;
fn from_str(accelerator_string: &str) -> Result<Self, Self::Err> {
parse_accelerator(accelerator_string)
}
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy)]
pub enum SysMods {
None,
Shift,
Cmd,
AltCmd,
CmdShift,
AltCmdShift
}
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RawMods {
None,
Alt,
Ctrl,
Meta,
Shift,
AltCtrl,
AltMeta,
AltShift,
CtrlShift,
CtrlMeta,
MetaShift,
AltCtrlMeta,
AltCtrlShift,
AltMetaShift,
CtrlMetaShift,
AltCtrlMetaShift
}
impl From<SysMods> for Option<ModifiersState> {
fn from(src: SysMods) -> Option<ModifiersState> {
Some(src.into())
}
}
impl From<RawMods> for Option<ModifiersState> {
fn from(src: RawMods) -> Option<ModifiersState> {
Some(src.into())
}
}
impl From<RawMods> for ModifiersState {
fn from(src: RawMods) -> ModifiersState {
let (alt, ctrl, meta, shift) = match src {
RawMods::None => (false, false, false, false),
RawMods::Alt => (true, false, false, false),
RawMods::Ctrl => (false, true, false, false),
RawMods::Meta => (false, false, true, false),
RawMods::Shift => (false, false, false, true),
RawMods::AltCtrl => (true, true, false, false),
RawMods::AltMeta => (true, false, true, false),
RawMods::AltShift => (true, false, false, true),
RawMods::CtrlMeta => (false, true, true, false),
RawMods::CtrlShift => (false, true, false, true),
RawMods::MetaShift => (false, false, true, true),
RawMods::AltCtrlMeta => (true, true, true, false),
RawMods::AltMetaShift => (true, false, true, true),
RawMods::AltCtrlShift => (true, true, false, true),
RawMods::CtrlMetaShift => (false, true, true, true),
RawMods::AltCtrlMetaShift => (true, true, true, true)
};
let mut mods = ModifiersState::empty();
mods.set(ModifiersState::ALT, alt);
mods.set(ModifiersState::CONTROL, ctrl);
mods.set(ModifiersState::SUPER, meta);
mods.set(ModifiersState::SHIFT, shift);
mods
}
}
impl From<SysMods> for ModifiersState {
fn from(src: SysMods) -> ModifiersState {
let (alt, ctrl, meta, shift) = match src {
SysMods::None => (false, false, false, false),
SysMods::Shift => (false, false, false, true),
#[cfg(target_os = "macos")]
SysMods::AltCmd => (true, false, true, false),
#[cfg(not(target_os = "macos"))]
SysMods::AltCmd => (true, true, false, false),
#[cfg(target_os = "macos")]
SysMods::AltCmdShift => (true, false, true, true),
#[cfg(not(target_os = "macos"))]
SysMods::AltCmdShift => (true, true, false, true),
#[cfg(target_os = "macos")]
SysMods::Cmd => (false, false, true, false),
#[cfg(not(target_os = "macos"))]
SysMods::Cmd => (false, true, false, false),
#[cfg(target_os = "macos")]
SysMods::CmdShift => (false, false, true, true),
#[cfg(not(target_os = "macos"))]
SysMods::CmdShift => (false, true, false, true)
};
let mut mods = ModifiersState::empty();
mods.set(ModifiersState::ALT, alt);
mods.set(ModifiersState::CONTROL, ctrl);
mods.set(ModifiersState::SUPER, meta);
mods.set(ModifiersState::SHIFT, shift);
mods
}
}
impl From<SysMods> for RawMods {
fn from(src: SysMods) -> RawMods {
#[cfg(target_os = "macos")]
match src {
SysMods::None => RawMods::None,
SysMods::Shift => RawMods::Shift,
SysMods::Cmd => RawMods::Meta,
SysMods::AltCmd => RawMods::AltMeta,
SysMods::CmdShift => RawMods::MetaShift,
SysMods::AltCmdShift => RawMods::AltMetaShift
}
#[cfg(not(target_os = "macos"))]
match src {
SysMods::None => RawMods::None,
SysMods::Shift => RawMods::Shift,
SysMods::Cmd => RawMods::Ctrl,
SysMods::AltCmd => RawMods::AltCtrl,
SysMods::CmdShift => RawMods::CtrlShift,
SysMods::AltCmdShift => RawMods::AltCtrlShift
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct AcceleratorId(pub u16);
impl From<AcceleratorId> for u16 {
fn from(s: AcceleratorId) -> u16 {
s.0
}
}
impl From<AcceleratorId> for u32 {
fn from(s: AcceleratorId) -> u32 {
s.0 as u32
}
}
impl From<AcceleratorId> for i32 {
fn from(s: AcceleratorId) -> i32 {
s.0 as i32
}
}
impl AcceleratorId {
pub const EMPTY: AcceleratorId = AcceleratorId(0);
pub fn new(accelerator_string: &str) -> AcceleratorId {
AcceleratorId(hash_string_to_u16(accelerator_string))
}
pub fn is_empty(self) -> bool {
Self::EMPTY == self
}
}
#[derive(Debug, Clone)]
pub struct AcceleratorParseError(String);
impl std::fmt::Display for AcceleratorParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[AcceleratorParseError]: {}", self.0)
}
}
fn parse_accelerator(accelerator_string: &str) -> Result<Accelerator, AcceleratorParseError> {
let mut mods = ModifiersState::empty();
let mut key = KeyCode::Unidentified(NativeKeyCode::Unidentified);
for raw in accelerator_string.to_uppercase().split('+') {
let token = raw.trim().to_string();
if token.is_empty() {
return Err(AcceleratorParseError("Unexpected empty token while parsing accelerator".into()));
}
if key != KeyCode::Unidentified(NativeKeyCode::Unidentified) {
return Err(AcceleratorParseError(format!("Unexpected accelerator string format: \"{}\"", accelerator_string)));
}
match token.as_str() {
"OPTION" | "ALT" => {
mods.set(ModifiersState::ALT, true);
}
"CONTROL" | "CTRL" => {
mods.set(ModifiersState::CONTROL, true);
}
"COMMAND" | "CMD" | "SUPER" => {
mods.set(ModifiersState::SUPER, true);
}
"SHIFT" => {
mods.set(ModifiersState::SHIFT, true);
}
"COMMANDORCONTROL" | "COMMANDORCTRL" | "CMDORCTRL" | "CMDORCONTROL" => {
#[cfg(target_os = "macos")]
mods.set(ModifiersState::SUPER, true);
#[cfg(not(target_os = "macos"))]
mods.set(ModifiersState::CONTROL, true);
}
_ => {
if let Ok(keycode) = KeyCode::from_str(token.to_uppercase().as_str()) {
match keycode {
KeyCode::Unidentified(_) => return Err(AcceleratorParseError(format!("Couldn't identify \"{}\" as a valid `KeyCode`", token))),
_ => key = keycode
}
}
}
}
}
Ok(Accelerator {
id: Some(AcceleratorId(hash_string_to_u16(accelerator_string))),
key,
mods
})
}
fn hash_string_to_u16(title: &str) -> u16 {
let mut s = DefaultHasher::new();
title.to_uppercase().hash(&mut s);
s.finish() as u16
}
fn hash_accelerator_to_u16(hotkey: Accelerator) -> u16 {
let mut s = DefaultHasher::new();
hotkey.hash(&mut s);
s.finish() as u16
}
#[test]
fn test_parse_accelerator() {
assert_eq!(
parse_accelerator("CTRL+X").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("CTRL+X")),
mods: ModifiersState::CONTROL,
key: KeyCode::KeyX
}
);
assert_eq!(
parse_accelerator("SHIFT+C").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("SHIFT+C")),
mods: ModifiersState::SHIFT,
key: KeyCode::KeyC
}
);
assert_eq!(
parse_accelerator("CTRL+Z").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("CTRL+Z")),
mods: ModifiersState::CONTROL,
key: KeyCode::KeyZ
}
);
assert_eq!(
parse_accelerator("super+ctrl+SHIFT+alt+Up").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("super+ctrl+SHIFT+alt+Up")),
mods: ModifiersState::SUPER | ModifiersState::CONTROL | ModifiersState::SHIFT | ModifiersState::ALT,
key: KeyCode::ArrowUp
}
);
assert_eq!(
parse_accelerator("5").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("5")),
mods: ModifiersState::empty(),
key: KeyCode::Digit5
}
);
assert_eq!(
parse_accelerator("G").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("G")),
mods: ModifiersState::empty(),
key: KeyCode::KeyG
}
);
assert_eq!(
parse_accelerator("G").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("g")),
mods: ModifiersState::empty(),
key: KeyCode::KeyG
}
);
let acc = parse_accelerator("+G");
assert!(acc.is_err());
let acc = parse_accelerator("SHGSH+G");
assert!(acc.is_err());
assert_eq!(
parse_accelerator("SHiFT+F12").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("SHIFT+F12")),
mods: ModifiersState::SHIFT,
key: KeyCode::F12
}
);
assert_eq!(
parse_accelerator("CmdOrCtrl+Space").unwrap(),
Accelerator {
id: Some(AcceleratorId::new("CmdOrCtrl+Space")),
#[cfg(target_os = "macos")]
mods: ModifiersState::SUPER,
#[cfg(not(target_os = "macos"))]
mods: ModifiersState::CONTROL,
key: KeyCode::Space
}
);
let acc = parse_accelerator("CTRL+");
assert!(acc.is_err());
}