From 998fff4e79c22ef645d5bd5eef4e9842cb445aa0 Mon Sep 17 00:00:00 2001 From: Christopher Serr Date: Tue, 11 Oct 2022 00:02:55 +0200 Subject: [PATCH] Add Support for Key Modifiers to the Hotkeys This adds support for key modifiers to the `livesplit-hotkey` crate. Instead of specifying a `KeyCode`, you now specify a `Hotkey` which consists of a `KeyCode` and a set of `Modifiers`. All the implementations support modifiers. However while `wasm-web` and `macOS` natively provide the state of the modifiers to us, the other platforms manually track their state. --- .github/workflows/build.yml | 2 +- crates/livesplit-hotkey/Cargo.toml | 11 +- crates/livesplit-hotkey/src/hotkey.rs | 99 ++++++ crates/livesplit-hotkey/src/key_code.rs | 285 +++++++++++++++++- crates/livesplit-hotkey/src/lib.rs | 42 +-- crates/livesplit-hotkey/src/linux/mod.rs | 69 +++-- crates/livesplit-hotkey/src/macos/cg.rs | 2 +- crates/livesplit-hotkey/src/macos/mod.rs | 125 ++++++-- crates/livesplit-hotkey/src/modifiers.rs | 101 +++++++ crates/livesplit-hotkey/src/other/mod.rs | 6 +- .../livesplit-hotkey/src/wasm_unknown/mod.rs | 106 ------- crates/livesplit-hotkey/src/wasm_web/mod.rs | 43 ++- crates/livesplit-hotkey/src/windows/mod.rs | 80 ++++- src/hotkey_config.rs | 37 ++- src/hotkey_system.rs | 172 +++++------ src/settings/value.rs | 12 +- 16 files changed, 880 insertions(+), 312 deletions(-) create mode 100644 crates/livesplit-hotkey/src/hotkey.rs create mode 100644 crates/livesplit-hotkey/src/modifiers.rs delete mode 100644 crates/livesplit-hotkey/src/wasm_unknown/mod.rs diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1331be8..747c4d74 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -631,7 +631,7 @@ jobs: - name: Install cross if: matrix.cross == '' && matrix.no_std == '' - run: cargo install cross + run: cargo install cross --debug - name: Build Static Library run: sh .github/workflows/build_static.sh diff --git a/crates/livesplit-hotkey/Cargo.toml b/crates/livesplit-hotkey/Cargo.toml index 81687828..383e2c2d 100644 --- a/crates/livesplit-hotkey/Cargo.toml +++ b/crates/livesplit-hotkey/Cargo.toml @@ -7,7 +7,7 @@ repository = "https://github.com/LiveSplit/livesplit-core/tree/master/crates/liv license = "Apache-2.0/MIT" description = "livesplit-hotkey provides cross-platform global hotkey hooks." keywords = ["speedrun", "timer", "livesplit", "hotkey", "keyboard"] -edition = "2018" +edition = "2021" [target.'cfg(windows)'.dependencies] winapi = { version = "0.3.2", features = [ @@ -16,14 +16,14 @@ winapi = { version = "0.3.2", features = [ "winuser" ], optional = true } +[target.'cfg(target_os = "macos")'.dependencies] +objc = "0.2.7" + [target.'cfg(target_os = "linux")'.dependencies] evdev = { version = "=0.11.4", optional = true } mio = { version = "0.8.0", default-features = false, features = ["os-ext", "os-poll"], optional = true } promising-future = { version = "0.2.4", optional = true } -[target.'cfg(target_os = "macos")'.dependencies] -bitflags = { version = "1.2.1", optional = true } - [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies] wasm-bindgen = { version = "0.2.54", optional = true } web-sys = { version = "0.3.28", default-features = false, features = ["Gamepad", "GamepadButton", "EventTarget", "KeyboardEvent", "Navigator", "Window"], optional = true } @@ -32,8 +32,9 @@ web-sys = { version = "0.3.28", default-features = false, features = ["Gamepad", cfg-if = "1.0.0" serde = { version = "1.0.98", default-features = false, features = ["derive", "alloc"] } snafu = { version = "0.7.0", default-features = false } +bitflags = { version = "1.2.1" } [features] default = ["std"] -std = ["snafu/std", "serde/std", "evdev", "mio", "promising-future", "winapi", "bitflags"] +std = ["snafu/std", "serde/std", "evdev", "mio", "promising-future", "winapi"] wasm-web = ["wasm-bindgen", "web-sys"] diff --git a/crates/livesplit-hotkey/src/hotkey.rs b/crates/livesplit-hotkey/src/hotkey.rs new file mode 100644 index 00000000..5f382a69 --- /dev/null +++ b/crates/livesplit-hotkey/src/hotkey.rs @@ -0,0 +1,99 @@ +use core::{fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; + +use crate::{KeyCode, Modifiers}; + +/// A hotkey is a combination of a key code and a set of modifiers. +#[derive(Eq, PartialEq, Hash, Copy, Clone)] +pub struct Hotkey { + /// The key code of the hotkey. + pub key_code: KeyCode, + /// The modifiers of the hotkey. + pub modifiers: Modifiers, +} + +impl fmt::Debug for Hotkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(self, f) + } +} + +impl fmt::Display for Hotkey { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.modifiers.is_empty() { + f.write_str(self.key_code.name()) + } else { + write!(f, "{} + {}", self.modifiers, self.key_code.name()) + } + } +} + +impl FromStr for Hotkey { + type Err = (); + + fn from_str(s: &str) -> Result { + if let Some((modifiers, key_code)) = s.rsplit_once('+') { + let modifiers = modifiers.trim_end().parse()?; + let key_code = key_code.trim_start().parse()?; + Ok(Self { + key_code, + modifiers, + }) + } else { + let key_code = s.parse()?; + Ok(Self { + key_code, + modifiers: Modifiers::empty(), + }) + } + } +} + +impl Serialize for Hotkey { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + if self.modifiers.is_empty() { + self.key_code.serialize(serializer) + } else { + serializer.collect_str(self) + } + } +} + +impl<'de> Deserialize<'de> for Hotkey { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(HotkeyVisitor) + } +} + +struct HotkeyVisitor; + +impl<'de> serde::de::Visitor<'de> for HotkeyVisitor { + type Value = Hotkey; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a valid hotkey") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Hotkey::from_str(v).map_err(|()| serde::de::Error::custom("invalid hotkey")) + } +} + +impl From for Hotkey { + fn from(key_code: KeyCode) -> Self { + Self { + key_code, + modifiers: Modifiers::empty(), + } + } +} diff --git a/crates/livesplit-hotkey/src/key_code.rs b/crates/livesplit-hotkey/src/key_code.rs index f2199411..62f2b6af 100644 --- a/crates/livesplit-hotkey/src/key_code.rs +++ b/crates/livesplit-hotkey/src/key_code.rs @@ -1,5 +1,7 @@ +use crate::{Hotkey, Modifiers}; use alloc::borrow::Cow; -use core::str::FromStr; +use core::{fmt, str::FromStr}; +use serde::{Deserialize, Serialize}; // This is based on the web KeyboardEvent code Values specification and the // individual mappings are based on the following sources: @@ -13,7 +15,8 @@ use core::str::FromStr; // Firefox's sources: // https://github.com/mozilla/gecko-dev/blob/25002b534963ad95ff0c1a3dd0f906ba023ddc8e/widget/NativeKeyToDOMCodeName.h // -// Safari's sources: Windows: +// Safari's sources: +// Windows: // https://github.com/WebKit/WebKit/blob/8afe31a018b11741abdf9b4d5bb973d7c1d9ff05/Source/WebCore/platform/win/WindowsKeyNames.cpp // macOS: // https://github.com/WebKit/WebKit/blob/main/Source/WebCore/platform/mac/PlatformEventFactoryMac.mm @@ -26,7 +29,7 @@ use core::str::FromStr; /// keyboard layout. The values are based on the [`UI Events KeyboardEvent code /// Values`](https://www.w3.org/TR/uievents-code/) specification. There are some /// additional values for Gamepad support and some browser specific values. -#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Eq, PartialEq, Hash, Copy, Clone)] #[non_exhaustive] pub enum KeyCode { /// `Backtick` and `~` on a US keyboard. This is the `半角/全角/漢字` @@ -1153,8 +1156,49 @@ pub enum KeyCode { ZoomToggle, } +impl fmt::Debug for KeyCode { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(self.name()) + } +} + +impl Serialize for KeyCode { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(self.name()) + } +} + +impl<'de> Deserialize<'de> for KeyCode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(KeyCodeVisitor) + } +} + +struct KeyCodeVisitor; + +impl<'de> serde::de::Visitor<'de> for KeyCodeVisitor { + type Value = KeyCode; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("a valid key code") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + KeyCode::from_str(v).map_err(|()| serde::de::Error::custom("invalid key code")) + } +} + /// Every [`KeyCode`] is grouped into one of these classes. -#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, Serialize, Deserialize)] pub enum KeyCodeClass { /// The *writing system keys* are those that change meaning (i.e., they /// produce different key values) based on the current locale and keyboard @@ -1202,8 +1246,237 @@ pub enum KeyCodeClass { } impl KeyCode { + /// Combines the key code with the modifiers to form a [`Hotkey`]. + pub fn with_modifiers(self, modifiers: Modifiers) -> Hotkey { + Hotkey { + key_code: self, + modifiers, + } + } + + /// Returns the name of the key code. + pub const fn name(self) -> &'static str { + match self { + Self::Backquote => "Backquote", + Self::Backslash => "Backslash", + Self::BracketLeft => "BracketLeft", + Self::BracketRight => "BracketRight", + Self::Comma => "Comma", + Self::Digit0 => "Digit0", + Self::Digit1 => "Digit1", + Self::Digit2 => "Digit2", + Self::Digit3 => "Digit3", + Self::Digit4 => "Digit4", + Self::Digit5 => "Digit5", + Self::Digit6 => "Digit6", + Self::Digit7 => "Digit7", + Self::Digit8 => "Digit8", + Self::Digit9 => "Digit9", + Self::Equal => "Equal", + Self::IntlBackslash => "IntlBackslash", + Self::IntlRo => "IntlRo", + Self::IntlYen => "IntlYen", + Self::KeyA => "KeyA", + Self::KeyB => "KeyB", + Self::KeyC => "KeyC", + Self::KeyD => "KeyD", + Self::KeyE => "KeyE", + Self::KeyF => "KeyF", + Self::KeyG => "KeyG", + Self::KeyH => "KeyH", + Self::KeyI => "KeyI", + Self::KeyJ => "KeyJ", + Self::KeyK => "KeyK", + Self::KeyL => "KeyL", + Self::KeyM => "KeyM", + Self::KeyN => "KeyN", + Self::KeyO => "KeyO", + Self::KeyP => "KeyP", + Self::KeyQ => "KeyQ", + Self::KeyR => "KeyR", + Self::KeyS => "KeyS", + Self::KeyT => "KeyT", + Self::KeyU => "KeyU", + Self::KeyV => "KeyV", + Self::KeyW => "KeyW", + Self::KeyX => "KeyX", + Self::KeyY => "KeyY", + Self::KeyZ => "KeyZ", + Self::Minus => "Minus", + Self::Period => "Period", + Self::Quote => "Quote", + Self::Semicolon => "Semicolon", + Self::Slash => "Slash", + Self::AltLeft => "AltLeft", + Self::AltRight => "AltRight", + Self::Backspace => "Backspace", + Self::CapsLock => "CapsLock", + Self::ContextMenu => "ContextMenu", + Self::ControlLeft => "ControlLeft", + Self::ControlRight => "ControlRight", + Self::Enter => "Enter", + Self::MetaLeft => "MetaLeft", + Self::MetaRight => "MetaRight", + Self::ShiftLeft => "ShiftLeft", + Self::ShiftRight => "ShiftRight", + Self::Space => "Space", + Self::Tab => "Tab", + Self::Convert => "Convert", + Self::KanaMode => "KanaMode", + Self::Lang1 => "Lang1", + Self::Lang2 => "Lang2", + Self::Lang3 => "Lang3", + Self::Lang4 => "Lang4", + Self::Lang5 => "Lang5", + Self::NonConvert => "NonConvert", + Self::Delete => "Delete", + Self::End => "End", + Self::Help => "Help", + Self::Home => "Home", + Self::Insert => "Insert", + Self::PageDown => "PageDown", + Self::PageUp => "PageUp", + Self::ArrowDown => "ArrowDown", + Self::ArrowLeft => "ArrowLeft", + Self::ArrowRight => "ArrowRight", + Self::ArrowUp => "ArrowUp", + Self::NumLock => "NumLock", + Self::Numpad0 => "Numpad0", + Self::Numpad1 => "Numpad1", + Self::Numpad2 => "Numpad2", + Self::Numpad3 => "Numpad3", + Self::Numpad4 => "Numpad4", + Self::Numpad5 => "Numpad5", + Self::Numpad6 => "Numpad6", + Self::Numpad7 => "Numpad7", + Self::Numpad8 => "Numpad8", + Self::Numpad9 => "Numpad9", + Self::NumpadAdd => "NumpadAdd", + Self::NumpadBackspace => "NumpadBackspace", + Self::NumpadClear => "NumpadClear", + Self::NumpadClearEntry => "NumpadClearEntry", + Self::NumpadComma => "NumpadComma", + Self::NumpadDecimal => "NumpadDecimal", + Self::NumpadDivide => "NumpadDivide", + Self::NumpadEnter => "NumpadEnter", + Self::NumpadEqual => "NumpadEqual", + Self::NumpadHash => "NumpadHash", + Self::NumpadMemoryAdd => "NumpadMemoryAdd", + Self::NumpadMemoryClear => "NumpadMemoryClear", + Self::NumpadMemoryRecall => "NumpadMemoryRecall", + Self::NumpadMemoryStore => "NumpadMemoryStore", + Self::NumpadMemorySubtract => "NumpadMemorySubtract", + Self::NumpadMultiply => "NumpadMultiply", + Self::NumpadParenLeft => "NumpadParenLeft", + Self::NumpadParenRight => "NumpadParenRight", + Self::NumpadStar => "NumpadStar", + Self::NumpadSubtract => "NumpadSubtract", + Self::Escape => "Escape", + Self::F1 => "F1", + Self::F2 => "F2", + Self::F3 => "F3", + Self::F4 => "F4", + Self::F5 => "F5", + Self::F6 => "F6", + Self::F7 => "F7", + Self::F8 => "F8", + Self::F9 => "F9", + Self::F10 => "F10", + Self::F11 => "F11", + Self::F12 => "F12", + Self::F13 => "F13", + Self::F14 => "F14", + Self::F15 => "F15", + Self::F16 => "F16", + Self::F17 => "F17", + Self::F18 => "F18", + Self::F19 => "F19", + Self::F20 => "F20", + Self::F21 => "F21", + Self::F22 => "F22", + Self::F23 => "F23", + Self::F24 => "F24", + Self::Fn => "Fn", + Self::FnLock => "FnLock", + Self::PrintScreen => "PrintScreen", + Self::ScrollLock => "ScrollLock", + Self::Pause => "Pause", + Self::BrowserBack => "BrowserBack", + Self::BrowserFavorites => "BrowserFavorites", + Self::BrowserForward => "BrowserForward", + Self::BrowserHome => "BrowserHome", + Self::BrowserRefresh => "BrowserRefresh", + Self::BrowserSearch => "BrowserSearch", + Self::BrowserStop => "BrowserStop", + Self::Eject => "Eject", + Self::LaunchApp1 => "LaunchApp1", + Self::LaunchApp2 => "LaunchApp2", + Self::LaunchMail => "LaunchMail", + Self::MediaPlayPause => "MediaPlayPause", + Self::MediaSelect => "MediaSelect", + Self::MediaStop => "MediaStop", + Self::MediaTrackNext => "MediaTrackNext", + Self::MediaTrackPrevious => "MediaTrackPrevious", + Self::Power => "Power", + Self::Sleep => "Sleep", + Self::AudioVolumeDown => "AudioVolumeDown", + Self::AudioVolumeMute => "AudioVolumeMute", + Self::AudioVolumeUp => "AudioVolumeUp", + Self::WakeUp => "WakeUp", + Self::Again => "Again", + Self::Copy => "Copy", + Self::Cut => "Cut", + Self::Find => "Find", + Self::Open => "Open", + Self::Paste => "Paste", + Self::Props => "Props", + Self::Select => "Select", + Self::Undo => "Undo", + Self::Gamepad0 => "Gamepad0", + Self::Gamepad1 => "Gamepad1", + Self::Gamepad2 => "Gamepad2", + Self::Gamepad3 => "Gamepad3", + Self::Gamepad4 => "Gamepad4", + Self::Gamepad5 => "Gamepad5", + Self::Gamepad6 => "Gamepad6", + Self::Gamepad7 => "Gamepad7", + Self::Gamepad8 => "Gamepad8", + Self::Gamepad9 => "Gamepad9", + Self::Gamepad10 => "Gamepad10", + Self::Gamepad11 => "Gamepad11", + Self::Gamepad12 => "Gamepad12", + Self::Gamepad13 => "Gamepad13", + Self::Gamepad14 => "Gamepad14", + Self::Gamepad15 => "Gamepad15", + Self::Gamepad16 => "Gamepad16", + Self::Gamepad17 => "Gamepad17", + Self::Gamepad18 => "Gamepad18", + Self::Gamepad19 => "Gamepad19", + Self::BrightnessDown => "BrightnessDown", + Self::BrightnessUp => "BrightnessUp", + Self::DisplayToggleIntExt => "DisplayToggleIntExt", + Self::KeyboardLayoutSelect => "KeyboardLayoutSelect", + Self::LaunchAssistant => "LaunchAssistant", + Self::LaunchControlPanel => "LaunchControlPanel", + Self::LaunchScreenSaver => "LaunchScreenSaver", + Self::MailForward => "MailForward", + Self::MailReply => "MailReply", + Self::MailSend => "MailSend", + Self::MediaFastForward => "MediaFastForward", + Self::MediaPlay => "MediaPlay", + Self::MediaPause => "MediaPause", + Self::MediaRecord => "MediaRecord", + Self::MediaRewind => "MediaRewind", + Self::MicrophoneMuteToggle => "MicrophoneMuteToggle", + Self::PrivacyScreenToggle => "PrivacyScreenToggle", + Self::SelectTask => "SelectTask", + Self::ShowAllWindows => "ShowAllWindows", + Self::ZoomToggle => "ZoomToggle", + } + } + /// Resolve the KeyCode according to the standard US layout. - pub const fn as_str(self) -> &'static str { + pub const fn resolve_en_us(self) -> &'static str { use self::KeyCode::*; match self { Backquote => "`", @@ -1506,7 +1779,7 @@ impl KeyCode { return uppercase.into(); } } - self.as_str().into() + self.resolve_en_us().into() } } diff --git a/crates/livesplit-hotkey/src/lib.rs b/crates/livesplit-hotkey/src/lib.rs index 2117f40b..cb3e2a1f 100644 --- a/crates/livesplit-hotkey/src/lib.rs +++ b/crates/livesplit-hotkey/src/lib.rs @@ -27,26 +27,23 @@ cfg_if::cfg_if! { mod linux; use self::linux as platform; } else if #[cfg(target_os = "macos")] { + #[macro_use] + extern crate objc; mod macos; use self::macos as platform; - } else if #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] { - cfg_if::cfg_if! { - if #[cfg(feature = "wasm-web")] { - mod wasm_web; - use self::wasm_web as platform; - } else { - mod wasm_unknown; - use self::wasm_unknown as platform; - } - } + } else if #[cfg(all(target_arch = "wasm32", target_os = "unknown", feature = "wasm-web"))] { + mod wasm_web; + use self::wasm_web as platform; } else { mod other; use self::other as platform; } } +mod hotkey; mod key_code; -pub use self::{key_code::*, platform::*}; +mod modifiers; +pub use self::{hotkey::*, key_code::*, modifiers::*, platform::*}; #[cfg(test)] mod tests { @@ -57,18 +54,27 @@ mod tests { #[test] fn test() { let hook = Hook::new().unwrap(); - hook.register(KeyCode::Numpad1, || println!("A")).unwrap(); - println!("Press Numpad1"); + + hook.register(KeyCode::Numpad1.with_modifiers(Modifiers::SHIFT), || { + println!("A") + }) + .unwrap(); + println!("Press Shift + Numpad1"); thread::sleep(Duration::from_secs(5)); - hook.unregister(KeyCode::Numpad1).unwrap(); - hook.register(KeyCode::KeyN, || println!("B")).unwrap(); + hook.unregister(KeyCode::Numpad1.with_modifiers(Modifiers::SHIFT)) + .unwrap(); + + hook.register(KeyCode::KeyN.into(), || println!("B")) + .unwrap(); println!("Press KeyN"); thread::sleep(Duration::from_secs(5)); - hook.unregister(KeyCode::KeyN).unwrap(); - hook.register(KeyCode::Numpad1, || println!("C")).unwrap(); + hook.unregister(KeyCode::KeyN.into()).unwrap(); + + hook.register(KeyCode::Numpad1.into(), || println!("C")) + .unwrap(); println!("Press Numpad1"); thread::sleep(Duration::from_secs(5)); - hook.unregister(KeyCode::Numpad1).unwrap(); + hook.unregister(KeyCode::Numpad1.into()).unwrap(); } #[test] diff --git a/crates/livesplit-hotkey/src/linux/mod.rs b/crates/livesplit-hotkey/src/linux/mod.rs index 638d8b00..bdc22a0e 100644 --- a/crates/livesplit-hotkey/src/linux/mod.rs +++ b/crates/livesplit-hotkey/src/linux/mod.rs @@ -1,4 +1,4 @@ -use crate::KeyCode; +use crate::{Hotkey, KeyCode, Modifiers}; use evdev::{self, Device, EventType, InputEventKind, Key}; use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker}; use promising_future::{future_promise, Promise}; @@ -30,11 +30,11 @@ pub type Result = std::result::Result; enum Message { Register( - KeyCode, + Hotkey, Box, Promise>, ), - Unregister(KeyCode, Promise>), + Unregister(Hotkey, Promise>), End, } @@ -303,7 +303,8 @@ impl Hook { let join_handle = thread::spawn(move || -> Result<()> { let mut result = Ok(()); let mut events = Events::with_capacity(1024); - let mut hotkeys: HashMap> = HashMap::new(); + let mut hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); + let mut modifiers = Modifiers::empty(); 'event_loop: loop { if poll.poll(&mut events, None).is_err() { @@ -316,15 +317,45 @@ impl Hook { let idx = mio_event.token().0; for ev in devices[idx].fetch_events().map_err(|_| Error::EvDev)? { if let InputEventKind::Key(k) = ev.kind() { - // The values are: - // - 0: Released - // - 1: Pressed - // - 2: Repeating - // We don't want it to repeat so we only care about 1. - if ev.value() == 1 { - if let Some(callback) = hotkeys.get_mut(&k) { - callback(); + const RELEASED: i32 = 0; + const PRESSED: i32 = 1; + match ev.value() { + PRESSED => { + if let Some(callback) = hotkeys.get_mut(&(k, modifiers)) { + callback(); + } + match k { + Key::KEY_LEFTALT | Key::KEY_RIGHTALT => { + modifiers.insert(Modifiers::ALT); + } + Key::KEY_LEFTCTRL | Key::KEY_RIGHTCTRL => { + modifiers.insert(Modifiers::CONTROL); + } + Key::KEY_LEFTMETA | Key::KEY_RIGHTMETA => { + modifiers.insert(Modifiers::META); + } + Key::KEY_LEFTSHIFT | Key::KEY_RIGHTSHIFT => { + modifiers.insert(Modifiers::SHIFT); + } + _ => {} + } } + RELEASED => match k { + Key::KEY_LEFTALT | Key::KEY_RIGHTALT => { + modifiers.remove(Modifiers::ALT); + } + Key::KEY_LEFTCTRL | Key::KEY_RIGHTCTRL => { + modifiers.remove(Modifiers::CONTROL); + } + Key::KEY_LEFTMETA | Key::KEY_RIGHTMETA => { + modifiers.remove(Modifiers::META); + } + Key::KEY_LEFTSHIFT | Key::KEY_RIGHTSHIFT => { + modifiers.remove(Modifiers::SHIFT); + } + _ => {} + }, + _ => {} // Ignore repeating } } } @@ -333,8 +364,10 @@ impl Hook { match message { Message::Register(key, callback, promise) => { promise.set( - if code_for(key) - .and_then(|k| hotkeys.insert(k, callback)) + if code_for(key.key_code) + .and_then(|k| { + hotkeys.insert((k, key.modifiers), callback) + }) .is_some() { Err(Error::AlreadyRegistered) @@ -344,8 +377,8 @@ impl Hook { ); } Message::Unregister(key, promise) => promise.set( - code_for(key) - .and_then(|k| hotkeys.remove(&k).map(drop)) + code_for(key.key_code) + .and_then(|k| hotkeys.remove(&(k, key.modifiers)).map(drop)) .ok_or(Error::NotRegistered), ), Message::End => { @@ -367,7 +400,7 @@ impl Hook { } /// Registers a hotkey to listen to. - pub fn register(&self, hotkey: KeyCode, callback: F) -> Result<()> + pub fn register(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut() + Send + 'static, { @@ -383,7 +416,7 @@ impl Hook { } /// Unregisters a previously registered hotkey. - pub fn unregister(&self, hotkey: KeyCode) -> Result<()> { + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { let (future, promise) = future_promise(); self.sender diff --git a/crates/livesplit-hotkey/src/macos/cg.rs b/crates/livesplit-hotkey/src/macos/cg.rs index 01a0c316..c452e39c 100644 --- a/crates/livesplit-hotkey/src/macos/cg.rs +++ b/crates/livesplit-hotkey/src/macos/cg.rs @@ -72,7 +72,7 @@ bitflags::bitflags! { } #[repr(u32)] -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy, Debug, PartialEq, Eq)] #[non_exhaustive] pub enum EventType { Null = 0, diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index 234b5291..5aeee9f7 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -18,8 +18,9 @@ use self::{ EventTapPlacement, EventTapProxy, EventType, }, }; -use crate::KeyCode; +use crate::{Hotkey, KeyCode, Modifiers}; use cg::EventField; +use objc::runtime; use std::{ collections::{hash_map::Entry, HashMap}, ffi::c_void, @@ -63,12 +64,15 @@ struct RunLoop(cf::RunLoopRef); unsafe impl Send for RunLoop {} -type RegisteredKeys = Mutex>>; +struct State { + hotkeys: Mutex>>, + ns_event_class: &'static runtime::Class, +} /// A hook allows you to listen to hotkeys. pub struct Hook { event_loop: RunLoop, - hotkeys: Arc, + state: Arc, } impl Drop for Hook { @@ -86,8 +90,16 @@ impl Drop for Hook { impl Hook { /// Creates a new hook. pub fn new() -> Result { - let hotkeys = Arc::new(Mutex::new(HashMap::new())); - let thread_hotkeys = hotkeys.clone(); + #[link(name = "AppKit", kind = "framework")] + extern "C" { + // NSEvent is in the AppKit framework. + } + + let state = Arc::new(State { + hotkeys: Mutex::new(HashMap::new()), + ns_event_class: class!(NSEvent), + }); + let thread_state = state.clone(); let (sender, receiver) = channel(); @@ -95,15 +107,15 @@ impl Hook { // https://github.com/kwhat/libuiohook/blob/f4bb19be8aee7d7ee5ead89b5a89dbf440e2a71a/src/darwin/input_hook.c#L1086 thread::spawn(move || unsafe { - let hotkeys_ptr: *const Mutex<_> = &*thread_hotkeys; + let state_ptr: *const State = &*thread_state; let port = CGEventTapCreate( EventTapLocation::Session, EventTapPlacement::HeadInsertEventTap, EventTapOptions::DefaultTap, - EventMask::KEY_DOWN, + EventMask::KEY_DOWN | EventMask::FLAGS_CHANGED, Some(callback), - hotkeys_ptr as *mut c_void, + state_ptr as *mut c_void, ); if port.is_null() { let _ = sender.send(Err(Error::CouldntCreateEventTap)); @@ -139,18 +151,15 @@ impl Hook { .recv() .map_err(|_| Error::ThreadStoppedUnexpectedly)??; - Ok(Hook { - event_loop, - hotkeys, - }) + Ok(Hook { event_loop, state }) } /// Registers a hotkey to listen to. - pub fn register(&self, hotkey: KeyCode, callback: F) -> Result<()> + pub fn register(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut() + Send + 'static, { - if let Entry::Vacant(vacant) = self.hotkeys.lock().unwrap().entry(hotkey) { + if let Entry::Vacant(vacant) = self.state.hotkeys.lock().unwrap().entry(hotkey) { vacant.insert(Box::new(callback)); Ok(()) } else { @@ -159,12 +168,15 @@ impl Hook { } /// Unregisters a previously registered hotkey. - pub fn unregister(&self, hotkey: KeyCode) -> Result<()> { - if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { - Ok(()) - } else { - Err(Error::NotRegistered) - } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { + let _ = self + .state + .hotkeys + .lock() + .unwrap() + .remove(&hotkey) + .ok_or(Error::NotRegistered)?; + Ok(()) } } @@ -174,9 +186,12 @@ unsafe extern "C" fn callback( event: EventRef, user_info: *mut c_void, ) -> EventRef { - if !matches!(ty, EventType::KeyDown) { - return event; - } + // If the tap ever gets disabled by a timeout, we may need the following code: + // // Handle the timeout case by re-enabling the tap. + // if (type == kCGEventTapDisabledByTimeout) { + // CGEventTapEnable(shortcut_listener->event_tap_, TRUE); + // return event; + // } let is_repeating = cg::CGEventGetIntegerValueField(event, EventField::KeyboardEventAutorepeat); if is_repeating != 0 { @@ -311,9 +326,67 @@ unsafe extern "C" fn callback( _ => return event, }; - let hotkeys = user_info as *const RegisteredKeys; - let hotkeys = &*hotkeys; - if let Some(callback) = hotkeys.lock().unwrap().get_mut(&key_code) { + let state = user_info as *const State; + let state = &*state; + + let ns_event: *mut runtime::Object = msg_send![state.ns_event_class, eventWithCGEvent: event]; + if ns_event.is_null() { + return event; + } + let modifier_flags: ModifierFlags = msg_send![ns_event, modifierFlags]; + + bitflags::bitflags! { + struct ModifierFlags: u64 { + const CAPS_LOCK = 1 << 16; + const SHIFT = 1 << 17; + const CONTROL = 1 << 18; + const OPTION = 1 << 19; + const COMMAND = 1 << 20; + const NUMERIC_PAD = 1 << 21; + const HELP = 1 << 22; + const FUNCTION = 1 << 23; + } + } + + let mut modifiers = Modifiers::empty(); + if modifier_flags.contains(ModifierFlags::SHIFT) { + modifiers.insert(Modifiers::SHIFT); + } + if modifier_flags.contains(ModifierFlags::CONTROL) { + modifiers.insert(Modifiers::CONTROL); + } + if modifier_flags.contains(ModifierFlags::OPTION) { + modifiers.insert(Modifiers::ALT); + } + if modifier_flags.contains(ModifierFlags::COMMAND) { + modifiers.insert(Modifiers::META); + } + + // The modifier keys don't come in through the key down event, so we use the + // flags changed event. However in order to tell that they have been freshly + // pressed instead of released we need to check if they are part of the + // modifiers and only if they are not do we proceed with the event. The key + // also needs to be removed from the modifiers then to not appear twice. + if ty == EventType::FlagsChanged { + let modifier = match key_code { + KeyCode::AltLeft | KeyCode::AltRight => Modifiers::ALT, + KeyCode::ControlLeft | KeyCode::ControlRight => Modifiers::CONTROL, + KeyCode::MetaLeft | KeyCode::MetaRight => Modifiers::META, + KeyCode::ShiftLeft | KeyCode::ShiftRight => Modifiers::SHIFT, + _ => Modifiers::empty(), + }; + if !modifiers.contains(modifier) { + return event; + } + modifiers.remove(modifier); + } + + if let Some(callback) = state + .hotkeys + .lock() + .unwrap() + .get_mut(&key_code.with_modifiers(modifiers)) + { callback(); } diff --git a/crates/livesplit-hotkey/src/modifiers.rs b/crates/livesplit-hotkey/src/modifiers.rs new file mode 100644 index 00000000..6598273a --- /dev/null +++ b/crates/livesplit-hotkey/src/modifiers.rs @@ -0,0 +1,101 @@ +use core::{fmt, str::FromStr}; + +use serde::{Deserialize, Serialize}; + +bitflags::bitflags! { + /// The modifier keys that are currently pressed. + pub struct Modifiers: u8 { + /// The shift key is pressed. + const SHIFT = 1 << 0; + /// The control key is pressed. + const CONTROL = 1 << 1; + /// The alt key is pressed. + const ALT = 1 << 2; + /// The meta key is pressed. + const META = 1 << 3; + } +} + +impl fmt::Display for Modifiers { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut first = true; + if self.contains(Modifiers::CONTROL) { + first = false; + f.write_str("Ctrl")?; + } + if self.contains(Modifiers::ALT) { + if !first { + f.write_str(" + ")?; + } + first = false; + f.write_str("Alt")?; + } + if self.contains(Modifiers::META) { + if !first { + f.write_str(" + ")?; + } + first = false; + f.write_str("Meta")?; + } + if self.contains(Modifiers::SHIFT) { + if !first { + f.write_str(" + ")?; + } + f.write_str("Shift")?; + } + Ok(()) + } +} + +impl FromStr for Modifiers { + type Err = (); + + fn from_str(s: &str) -> Result { + let mut modifiers = Modifiers::empty(); + for modifier in s.split('+').map(str::trim) { + match modifier { + "Ctrl" => modifiers.insert(Modifiers::CONTROL), + "Alt" => modifiers.insert(Modifiers::ALT), + "Meta" => modifiers.insert(Modifiers::META), + "Shift" => modifiers.insert(Modifiers::SHIFT), + _ => return Err(()), + } + } + Ok(modifiers) + } +} + +impl Serialize for Modifiers { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.collect_str(self) + } +} + +impl<'de> Deserialize<'de> for Modifiers { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_str(ModifiersVisitor) + } +} + +struct ModifiersVisitor; + +impl<'de> serde::de::Visitor<'de> for ModifiersVisitor { + type Value = Modifiers; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("valid modifiers") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + Modifiers::from_str(v).map_err(|()| serde::de::Error::custom("invalid modifiers")) + } +} diff --git a/crates/livesplit-hotkey/src/other/mod.rs b/crates/livesplit-hotkey/src/other/mod.rs index 27740d56..e36c112a 100644 --- a/crates/livesplit-hotkey/src/other/mod.rs +++ b/crates/livesplit-hotkey/src/other/mod.rs @@ -1,4 +1,4 @@ -use crate::KeyCode; +use crate::{Hotkey, KeyCode}; use alloc::string::String; /// The error type for this crate. @@ -19,7 +19,7 @@ impl Hook { } /// Registers a hotkey to listen to. - pub fn register(&self, _: KeyCode, _: F) -> Result<()> + pub fn register(&self, _: Hotkey, _: F) -> Result<()> where F: FnMut() + Send + 'static, { @@ -27,7 +27,7 @@ impl Hook { } /// Unregisters a previously registered hotkey. - pub fn unregister(&self, _: KeyCode) -> Result<()> { + pub fn unregister(&self, _: Hotkey) -> Result<()> { Ok(()) } } diff --git a/crates/livesplit-hotkey/src/wasm_unknown/mod.rs b/crates/livesplit-hotkey/src/wasm_unknown/mod.rs deleted file mode 100644 index de418382..00000000 --- a/crates/livesplit-hotkey/src/wasm_unknown/mod.rs +++ /dev/null @@ -1,106 +0,0 @@ -use crate::KeyCode; -use std::{ - collections::hash_map::{Entry, HashMap}, - slice, str, - sync::{Arc, Mutex}, -}; - -/// The error type for this crate. -#[derive(Debug, snafu::Snafu)] -#[non_exhaustive] -pub enum Error { - /// The hotkey was already registered. - AlreadyRegistered, - /// The hotkey to unregister was not registered. - NotRegistered, -} - -/// The result type for this crate. -pub type Result = std::result::Result; - -pub type EventListenerHandle = Box; - -/// A hook allows you to listen to hotkeys. -pub struct Hook { - hotkeys: Arc>>>, - event: Option>, -} - -#[allow(improper_ctypes)] -extern "C" { - fn HotkeyHook_new(handle: *const EventListenerHandle); - fn HotkeyHook_drop(handle: *const EventListenerHandle); -} - -impl Drop for Hook { - fn drop(&mut self) { - let handle = self.event.take().unwrap(); - unsafe { - HotkeyHook_drop(&*handle); - } - } -} - -#[no_mangle] -pub unsafe extern "C" fn HotkeyHook_callback( - ptr: *const u8, - len: usize, - handle: *const EventListenerHandle, -) { - let t = str::from_utf8(slice::from_raw_parts(ptr, len)).unwrap(); - (*handle)(t); -} - -impl Hook { - /// Creates a new hook. - pub fn new() -> Result { - let hotkeys = Arc::new(Mutex::new(HashMap::< - KeyCode, - Box, - >::new())); - - let hotkey_map = hotkeys.clone(); - let event = Box::new(Box::new(move |code: &str| { - if let Ok(code) = code.parse() { - if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&code) { - callback(); - } - } - }) as EventListenerHandle); - - unsafe { - HotkeyHook_new(&*event); - } - - Ok(Hook { - hotkeys, - event: Some(event), - }) - } - - /// Registers a hotkey to listen to. - pub fn register(&self, hotkey: KeyCode, callback: F) -> Result<()> - where - F: FnMut() + Send + 'static, - { - if let Entry::Vacant(vacant) = self.hotkeys.lock().unwrap().entry(hotkey) { - vacant.insert(Box::new(callback)); - Ok(()) - } else { - Err(Error::AlreadyRegistered) - } - } - - /// Unregisters a previously registered hotkey. - pub fn unregister(&self, hotkey: KeyCode) -> Result<()> { - if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { - Ok(()) - } else { - Err(Error::NotRegistered) - } - } -} - -pub(crate) fn try_resolve(_key_code: KeyCode) -> Option { - None -} diff --git a/crates/livesplit-hotkey/src/wasm_web/mod.rs b/crates/livesplit-hotkey/src/wasm_web/mod.rs index dfe1299b..e9e14971 100644 --- a/crates/livesplit-hotkey/src/wasm_web/mod.rs +++ b/crates/livesplit-hotkey/src/wasm_web/mod.rs @@ -1,4 +1,4 @@ -use crate::KeyCode; +use crate::{Hotkey, KeyCode, Modifiers}; use wasm_bindgen::{prelude::*, JsCast}; use web_sys::{window, Event, Gamepad, GamepadButton, KeyboardEvent}; @@ -25,7 +25,7 @@ pub type Result = std::result::Result; /// A hook allows you to listen to hotkeys. pub struct Hook { - hotkeys: Arc>>>, + hotkeys: Arc>>>, keyboard_callback: Closure, gamepad_callback: Closure, interval_id: Cell>, @@ -73,7 +73,7 @@ impl Hook { /// Creates a new hook. pub fn new() -> Result { let hotkeys = Arc::new(Mutex::new(HashMap::< - KeyCode, + Hotkey, Box, >::new())); @@ -87,8 +87,33 @@ impl Hook { // `input` sends a `keydown` event that is not a `KeyboardEvent`. if let Ok(event) = event.dyn_into::() { if !event.repeat() { - if let Ok(code) = event.code().parse() { - if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&code) { + if let Ok(code) = event.code().parse::() { + let mut modifiers = Modifiers::empty(); + if event.shift_key() + && !matches!(code, KeyCode::ShiftLeft | KeyCode::ShiftRight) + { + modifiers.insert(Modifiers::SHIFT); + } + if event.ctrl_key() + && !matches!(code, KeyCode::ControlLeft | KeyCode::ControlRight) + { + modifiers.insert(Modifiers::CONTROL); + } + if event.alt_key() && !matches!(code, KeyCode::AltLeft | KeyCode::AltRight) + { + modifiers.insert(Modifiers::ALT); + } + if event.meta_key() + && !matches!(code, KeyCode::MetaLeft | KeyCode::MetaRight) + { + modifiers.insert(Modifiers::META); + } + + if let Some(callback) = hotkey_map + .lock() + .unwrap() + .get_mut(&code.with_modifiers(modifiers)) + { callback(); } } @@ -123,7 +148,7 @@ impl Hook { let pressed = button.pressed(); if pressed && !*state { if let Some(callback) = - hotkey_map.lock().unwrap().get_mut(&code) + hotkey_map.lock().unwrap().get_mut(&code.into()) { callback(); } @@ -145,12 +170,12 @@ impl Hook { } /// Registers a hotkey to listen to. - pub fn register(&self, hotkey: KeyCode, callback: F) -> Result<()> + pub fn register(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut() + Send + 'static, { if let Entry::Vacant(vacant) = self.hotkeys.lock().unwrap().entry(hotkey) { - if GAMEPAD_BUTTONS.contains(&hotkey) && self.interval_id.get().is_none() { + if GAMEPAD_BUTTONS.contains(&hotkey.key_code) && self.interval_id.get().is_none() { let interval_id = window() .ok_or(Error::FailedToCreateHook)? .set_interval_with_callback_and_timeout_and_arguments_0( @@ -168,7 +193,7 @@ impl Hook { } /// Unregisters a previously registered hotkey. - pub fn unregister(&self, hotkey: KeyCode) -> Result<()> { + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { Ok(()) } else { diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index 6eb82a8c..a351e488 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -1,4 +1,4 @@ -use crate::KeyCode; +use crate::{Hotkey, KeyCode, Modifiers}; use std::{ cell::RefCell, collections::hash_map::{Entry, HashMap}, @@ -21,7 +21,8 @@ use winapi::{ winuser::{ CallNextHookEx, GetMessageW, MapVirtualKeyW, PostThreadMessageW, SetWindowsHookExW, UnhookWindowsHookEx, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MAPVK_VK_TO_CHAR, - MAPVK_VK_TO_VSC_EX, MAPVK_VSC_TO_VK_EX, WH_KEYBOARD_LL, WM_KEYDOWN, WM_SYSKEYDOWN, + MAPVK_VK_TO_VSC_EX, MAPVK_VSC_TO_VK_EX, WH_KEYBOARD_LL, WM_KEYDOWN, WM_KEYUP, + WM_SYSKEYDOWN, WM_SYSKEYUP, }, }, }; @@ -50,7 +51,7 @@ pub type Result = std::result::Result; /// A hook allows you to listen to hotkeys. pub struct Hook { thread_id: DWORD, - hotkeys: Arc>>>, + hotkeys: Arc>>>, } impl Drop for Hook { @@ -63,7 +64,8 @@ impl Drop for Hook { struct State { hook: HHOOK, - events: Sender, + events: Sender, + modifiers: Modifiers, } thread_local! { @@ -276,8 +278,69 @@ unsafe extern "system" fn callback_proc(code: c_int, wparam: WPARAM, lparam: LPA if let Some(key_code) = parse_scan_code(scan_code) { state .events - .send(key_code) + .send(Hotkey { + key_code, + modifiers: state.modifiers, + }) .expect("Callback Thread disconnected"); + + match key_code { + KeyCode::AltLeft | KeyCode::AltRight => { + state.modifiers.insert(Modifiers::ALT); + } + KeyCode::ControlLeft | KeyCode::ControlRight => { + state.modifiers.insert(Modifiers::CONTROL); + } + KeyCode::MetaLeft | KeyCode::MetaRight => { + state.modifiers.insert(Modifiers::META); + } + KeyCode::ShiftLeft | KeyCode::ShiftRight => { + state.modifiers.insert(Modifiers::SHIFT); + } + _ => {} + } + } + } else if event == WM_KEYUP || event == WM_SYSKEYUP { + // Windows in addition to the scan code has a notion of a + // virtual key code. This however is already dependent on the + // keyboard layout. So we should prefer the scan code over the + // virtual key code. It's hard to come by what these scan codes + // actually mean, but there's a document released by Microsoft + // that contains most (not all sadly) mappings from USB HID to + // the scan code (which matches the PS/2 scan code set 1 make + // column). + // http://download.microsoft.com/download/1/6/1/161ba512-40e2-4cc9-843a-923143f3456c/translate.pdf + // Scan codes can come in an extended form `e0/e1 xx`, so you + // need to check for the extended field in the flags, as the + // scan code provided by itself is not extended. Also not every + // key press somehow even has a scan code. It seems like these + // might be caused by a special keyboard driver that directly + // emits the virtual key codes for those keys rather than any + // physical scan codes ever coming in. Windows has a way to + // translate those back into scan codes though, so this is what + // we do in that case. + let scan_code = if hook_struct.scanCode != 0 { + hook_struct.scanCode + ((hook_struct.flags & LLKHF_EXTENDED) * 0xE000) + } else { + MapVirtualKeyW(hook_struct.vkCode, MAPVK_VK_TO_VSC_EX) + }; + + if let Some(key_code) = parse_scan_code(scan_code) { + match key_code { + KeyCode::AltLeft | KeyCode::AltRight => { + state.modifiers.remove(Modifiers::ALT); + } + KeyCode::ControlLeft | KeyCode::ControlRight => { + state.modifiers.remove(Modifiers::CONTROL); + } + KeyCode::MetaLeft | KeyCode::MetaRight => { + state.modifiers.remove(Modifiers::META); + } + KeyCode::ShiftLeft | KeyCode::ShiftRight => { + state.modifiers.remove(Modifiers::SHIFT); + } + _ => {} + } } } } @@ -290,7 +353,7 @@ impl Hook { /// Creates a new hook. pub fn new() -> Result { let hotkeys = Arc::new(Mutex::new(HashMap::< - KeyCode, + Hotkey, Box, >::new())); @@ -323,6 +386,7 @@ impl Hook { *state.borrow_mut() = Some(State { hook, events: events_tx, + modifiers: Modifiers::empty(), }); Ok(()) @@ -362,7 +426,7 @@ impl Hook { } /// Registers a hotkey to listen to. - pub fn register(&self, hotkey: KeyCode, callback: F) -> Result<()> + pub fn register(&self, hotkey: Hotkey, callback: F) -> Result<()> where F: FnMut() + Send + 'static, { @@ -375,7 +439,7 @@ impl Hook { } /// Unregisters a previously registered hotkey. - pub fn unregister(&self, hotkey: KeyCode) -> Result<()> { + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { Ok(()) } else { diff --git a/src/hotkey_config.rs b/src/hotkey_config.rs index 2b021588..f8f9c974 100644 --- a/src/hotkey_config.rs +++ b/src/hotkey_config.rs @@ -1,7 +1,7 @@ #![allow(clippy::trivially_copy_pass_by_ref)] use crate::{ - hotkey::KeyCode, + hotkey::{Hotkey, KeyCode}, platform::prelude::*, settings::{Field, SettingsDescription, Value}, }; @@ -13,39 +13,38 @@ use serde::{Deserialize, Serialize}; #[serde(default)] pub struct HotkeyConfig { /// The key to use for splitting and starting a new attempt. - pub split: Option, + pub split: Option, /// The key to use for resetting the current attempt. - pub reset: Option, + pub reset: Option, /// The key to use for undoing the last split. - pub undo: Option, + pub undo: Option, /// The key to use for skipping the current split. - pub skip: Option, + pub skip: Option, /// The key to use for pausing the current attempt and starting a new /// attempt. - pub pause: Option, + pub pause: Option, /// The key to use for removing all the pause times from the current time. - pub undo_all_pauses: Option, + pub undo_all_pauses: Option, /// The key to use for switching to the previous comparison. - pub previous_comparison: Option, + pub previous_comparison: Option, /// The key to use for switching to the next comparison. - pub next_comparison: Option, + pub next_comparison: Option, /// The key to use for toggling between the `Real Time` and `Game Time` /// timing methods. - pub toggle_timing_method: Option, + pub toggle_timing_method: Option, } impl Default for HotkeyConfig { fn default() -> Self { - use crate::hotkey::KeyCode::*; Self { - split: Some(Numpad1), - reset: Some(Numpad3), - undo: Some(Numpad8), - skip: Some(Numpad2), - pause: Some(Numpad5), + split: Some(KeyCode::Numpad1.into()), + reset: Some(KeyCode::Numpad3.into()), + undo: Some(KeyCode::Numpad8.into()), + skip: Some(KeyCode::Numpad2.into()), + pause: None, undo_all_pauses: None, - previous_comparison: Some(Numpad4), - next_comparison: Some(Numpad6), + previous_comparison: None, + next_comparison: None, toggle_timing_method: None, } } @@ -87,7 +86,7 @@ impl HotkeyConfig { /// the type of the setting's value. A panic can also occur if the index of /// the setting provided is out of bounds. pub fn set_value(&mut self, index: usize, value: Value) -> Result<(), ()> { - let value: Option = value.into(); + let value: Option = value.into(); if value.is_some() { let any = [ diff --git a/src/hotkey_system.rs b/src/hotkey_system.rs index 068afbce..128727fa 100644 --- a/src/hotkey_system.rs +++ b/src/hotkey_system.rs @@ -1,5 +1,5 @@ use crate::{ - hotkey::{Hook, KeyCode}, + hotkey::{Hook, Hotkey}, HotkeyConfig, SharedTimer, }; @@ -7,7 +7,7 @@ pub use crate::hotkey::{Error, Result}; // This enum might be better situated in hotkey_config, but the last method should stay in this file #[derive(Debug, Copy, Clone, Eq, PartialEq)] -enum Hotkey { +enum Action { Split, /// The key to use for resetting the current attempt. Reset, @@ -29,50 +29,50 @@ enum Hotkey { ToggleTimingMethod, } -impl Hotkey { - fn set_keycode(self, config: &mut HotkeyConfig, keycode: Option) { +impl Action { + fn set_hotkey(self, config: &mut HotkeyConfig, hotkey: Option) { match self { - Hotkey::Split => config.split = keycode, - Hotkey::Reset => config.reset = keycode, - Hotkey::Undo => config.undo = keycode, - Hotkey::Skip => config.skip = keycode, - Hotkey::Pause => config.pause = keycode, - Hotkey::UndoAllPauses => config.undo_all_pauses = keycode, - Hotkey::PreviousComparison => config.previous_comparison = keycode, - Hotkey::NextComparison => config.next_comparison = keycode, - Hotkey::ToggleTimingMethod => config.toggle_timing_method = keycode, + Action::Split => config.split = hotkey, + Action::Reset => config.reset = hotkey, + Action::Undo => config.undo = hotkey, + Action::Skip => config.skip = hotkey, + Action::Pause => config.pause = hotkey, + Action::UndoAllPauses => config.undo_all_pauses = hotkey, + Action::PreviousComparison => config.previous_comparison = hotkey, + Action::NextComparison => config.next_comparison = hotkey, + Action::ToggleTimingMethod => config.toggle_timing_method = hotkey, } } - const fn get_keycode(self, config: &HotkeyConfig) -> Option { + const fn get_hotkey(self, config: &HotkeyConfig) -> Option { match self { - Hotkey::Split => config.split, - Hotkey::Reset => config.reset, - Hotkey::Undo => config.undo, - Hotkey::Skip => config.skip, - Hotkey::Pause => config.pause, - Hotkey::UndoAllPauses => config.undo_all_pauses, - Hotkey::PreviousComparison => config.previous_comparison, - Hotkey::NextComparison => config.next_comparison, - Hotkey::ToggleTimingMethod => config.toggle_timing_method, + Action::Split => config.split, + Action::Reset => config.reset, + Action::Undo => config.undo, + Action::Skip => config.skip, + Action::Pause => config.pause, + Action::UndoAllPauses => config.undo_all_pauses, + Action::PreviousComparison => config.previous_comparison, + Action::NextComparison => config.next_comparison, + Action::ToggleTimingMethod => config.toggle_timing_method, } } fn callback(self, timer: SharedTimer) -> Box { match self { - Hotkey::Split => Box::new(move || timer.write().unwrap().split_or_start()), - Hotkey::Reset => Box::new(move || timer.write().unwrap().reset(true)), - Hotkey::Undo => Box::new(move || timer.write().unwrap().undo_split()), - Hotkey::Skip => Box::new(move || timer.write().unwrap().skip_split()), - Hotkey::Pause => Box::new(move || timer.write().unwrap().toggle_pause_or_start()), - Hotkey::UndoAllPauses => Box::new(move || timer.write().unwrap().undo_all_pauses()), - Hotkey::PreviousComparison => { + Action::Split => Box::new(move || timer.write().unwrap().split_or_start()), + Action::Reset => Box::new(move || timer.write().unwrap().reset(true)), + Action::Undo => Box::new(move || timer.write().unwrap().undo_split()), + Action::Skip => Box::new(move || timer.write().unwrap().skip_split()), + Action::Pause => Box::new(move || timer.write().unwrap().toggle_pause_or_start()), + Action::UndoAllPauses => Box::new(move || timer.write().unwrap().undo_all_pauses()), + Action::PreviousComparison => { Box::new(move || timer.write().unwrap().switch_to_previous_comparison()) } - Hotkey::NextComparison => { + Action::NextComparison => { Box::new(move || timer.write().unwrap().switch_to_next_comparison()) } - Hotkey::ToggleTimingMethod => { + Action::ToggleTimingMethod => { Box::new(move || timer.write().unwrap().toggle_timing_method()) } } @@ -112,109 +112,109 @@ impl HotkeySystem { // This method should never be public, because it might mess up the internal // state and we might leak a registered hotkey - fn register_inner(&mut self, hotkey: Hotkey) -> Result<()> { + fn register_inner(&mut self, action: Action) -> Result<()> { let inner = self.timer.clone(); - if let Some(keycode) = hotkey.get_keycode(&self.config) { - self.hook.register(keycode, hotkey.callback(inner))?; + if let Some(hotkey) = action.get_hotkey(&self.config) { + self.hook.register(hotkey, action.callback(inner))?; } Ok(()) } - fn register(&mut self, hotkey: Hotkey, keycode: Option) -> Result<()> { - hotkey.set_keycode(&mut self.config, keycode); - self.register_inner(hotkey) + fn register(&mut self, action: Action, hotkey: Option) -> Result<()> { + action.set_hotkey(&mut self.config, hotkey); + self.register_inner(action) } // This method should never be public, because it might mess up the internal // state and we might leak a registered hotkey - fn unregister_inner(&mut self, hotkey: Hotkey) -> Result<()> { - if let Some(keycode) = hotkey.get_keycode(&self.config) { - self.hook.unregister(keycode)?; + fn unregister_inner(&mut self, action: Action) -> Result<()> { + if let Some(hotkey) = action.get_hotkey(&self.config) { + self.hook.unregister(hotkey)?; } Ok(()) } - fn unregister(&mut self, hotkey: Hotkey) -> Result<()> { - self.unregister_inner(hotkey)?; - hotkey.set_keycode(&mut self.config, None); + fn unregister(&mut self, action: Action) -> Result<()> { + self.unregister_inner(action)?; + action.set_hotkey(&mut self.config, None); Ok(()) } - fn set_hotkey(&mut self, hotkey: Hotkey, keycode: Option) -> Result<()> { - // FIXME: We do not check whether the keycode is already in use - if hotkey.get_keycode(&self.config) == keycode { + fn set_hotkey(&mut self, action: Action, hotkey: Option) -> Result<()> { + // FIXME: We do not check whether the hotkey is already in use + if action.get_hotkey(&self.config) == hotkey { return Ok(()); } if self.is_active { - self.unregister(hotkey)?; - self.register(hotkey, keycode)?; + self.unregister(action)?; + self.register(action, hotkey)?; } else { - hotkey.set_keycode(&mut self.config, keycode); + action.set_hotkey(&mut self.config, hotkey); } Ok(()) } /// Sets the key to use for splitting and starting a new attempt. - pub fn set_split(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::Split, hotkey) + pub fn set_split(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::Split, hotkey) } /// Sets the key to use for resetting the current attempt. - pub fn set_reset(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::Reset, hotkey) + pub fn set_reset(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::Reset, hotkey) } /// Sets the key to use for pausing the current attempt and starting a new /// attempt. - pub fn set_pause(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::Pause, hotkey) + pub fn set_pause(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::Pause, hotkey) } /// Sets the key to use for skipping the current split. - pub fn set_skip(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::Skip, hotkey) + pub fn set_skip(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::Skip, hotkey) } /// Sets the key to use for undoing the last split. - pub fn set_undo(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::Undo, hotkey) + pub fn set_undo(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::Undo, hotkey) } /// Sets the key to use for switching to the previous comparison. - pub fn set_previous_comparison(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::PreviousComparison, hotkey) + pub fn set_previous_comparison(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::PreviousComparison, hotkey) } /// Sets the key to use for switching to the next comparison. - pub fn set_next_comparison(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::NextComparison, hotkey) + pub fn set_next_comparison(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::NextComparison, hotkey) } /// Sets the key to use for removing all the pause times from the current /// time. - pub fn set_undo_all_pauses(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::UndoAllPauses, hotkey) + pub fn set_undo_all_pauses(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::UndoAllPauses, hotkey) } /// Sets the key to use for toggling between the `Real Time` and `Game Time` /// timing methods. - pub fn set_toggle_timing_method(&mut self, hotkey: Option) -> Result<()> { - self.set_hotkey(Hotkey::ToggleTimingMethod, hotkey) + pub fn set_toggle_timing_method(&mut self, hotkey: Option) -> Result<()> { + self.set_hotkey(Action::ToggleTimingMethod, hotkey) } /// Deactivates the Hotkey System. No hotkeys will go through until it gets /// activated again. If it's already deactivated, nothing happens. pub fn deactivate(&mut self) -> Result<()> { if self.is_active { - self.unregister_inner(Hotkey::Split)?; - self.unregister_inner(Hotkey::Reset)?; - self.unregister_inner(Hotkey::Undo)?; - self.unregister_inner(Hotkey::Skip)?; - self.unregister_inner(Hotkey::Pause)?; - self.unregister_inner(Hotkey::UndoAllPauses)?; - self.unregister_inner(Hotkey::PreviousComparison)?; - self.unregister_inner(Hotkey::NextComparison)?; - self.unregister_inner(Hotkey::ToggleTimingMethod)?; + self.unregister_inner(Action::Split)?; + self.unregister_inner(Action::Reset)?; + self.unregister_inner(Action::Undo)?; + self.unregister_inner(Action::Skip)?; + self.unregister_inner(Action::Pause)?; + self.unregister_inner(Action::UndoAllPauses)?; + self.unregister_inner(Action::PreviousComparison)?; + self.unregister_inner(Action::NextComparison)?; + self.unregister_inner(Action::ToggleTimingMethod)?; } self.is_active = false; Ok(()) @@ -224,15 +224,15 @@ impl HotkeySystem { /// active, nothing happens. pub fn activate(&mut self) -> Result<()> { if !self.is_active { - self.register_inner(Hotkey::Split)?; - self.register_inner(Hotkey::Reset)?; - self.register_inner(Hotkey::Undo)?; - self.register_inner(Hotkey::Skip)?; - self.register_inner(Hotkey::Pause)?; - self.register_inner(Hotkey::UndoAllPauses)?; - self.register_inner(Hotkey::PreviousComparison)?; - self.register_inner(Hotkey::NextComparison)?; - self.register_inner(Hotkey::ToggleTimingMethod)?; + self.register_inner(Action::Split)?; + self.register_inner(Action::Reset)?; + self.register_inner(Action::Undo)?; + self.register_inner(Action::Skip)?; + self.register_inner(Action::Pause)?; + self.register_inner(Action::UndoAllPauses)?; + self.register_inner(Action::PreviousComparison)?; + self.register_inner(Action::NextComparison)?; + self.register_inner(Action::ToggleTimingMethod)?; } self.is_active = true; Ok(()) diff --git a/src/settings/value.rs b/src/settings/value.rs index bdbf9f28..08dc4dfb 100644 --- a/src/settings/value.rs +++ b/src/settings/value.rs @@ -3,7 +3,7 @@ use crate::{ splits::{ColumnStartWith, ColumnUpdateTrigger, ColumnUpdateWith}, timer::DeltaGradient, }, - hotkey::KeyCode, + hotkey::Hotkey, layout::LayoutDirection, platform::prelude::*, settings::{Alignment, Color, Font, Gradient, ListGradient}, @@ -65,7 +65,7 @@ pub enum Value { /// A value describing when to update a column of the Splits Component. ColumnUpdateTrigger(ColumnUpdateTrigger), /// A value describing what hotkey to press to trigger a certain action. - Hotkey(Option), + Hotkey(Option), /// A value describing the direction of a layout. LayoutDirection(LayoutDirection), /// A value describing a font to use. `None` if a default font should be @@ -172,8 +172,8 @@ impl From for Value { } } -impl From> for Value { - fn from(x: Option) -> Self { +impl From> for Value { + fn from(x: Option) -> Self { Value::Hotkey(x) } } @@ -348,7 +348,7 @@ impl Value { } /// Tries to convert the value into a hotkey. - pub fn into_hotkey(self) -> Result> { + pub fn into_hotkey(self) -> Result> { match self { Value::Hotkey(v) => Ok(v), Value::String(v) | Value::OptionalString(Some(v)) => { @@ -490,7 +490,7 @@ impl From for ColumnUpdateTrigger { } } -impl From for Option { +impl From for Option { fn from(value: Value) -> Self { value.into_hotkey().unwrap() }