Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement Resolving Hotkeys on Windows #459

Merged
merged 1 commit into from
Oct 24, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions crates/livesplit-hotkey/src/key_code.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use alloc::borrow::Cow;
use core::str::FromStr;

// Based on
Expand Down Expand Up @@ -261,6 +262,20 @@ pub enum KeyCode {
ZoomToggle,
}

#[derive(Debug, Eq, PartialEq, Hash, Copy, Clone, serde::Serialize, serde::Deserialize)]
pub enum KeyCodeClass {
WritingSystem,
Functional,
ControlPad,
ArrowPad,
Numpad,
Function,
Media,
Legacy,
Gamepad,
NonStandard,
}

impl KeyCode {
/// Resolve the KeyCode according to the standard US layout.
pub fn as_str(self) -> &'static str {
Expand Down Expand Up @@ -482,6 +497,88 @@ impl KeyCode {
ZoomToggle => "Zoom Toggle",
}
}

pub fn classify(self) -> KeyCodeClass {
use self::KeyCode::*;
match self {
// Writing System Keys
Backquote | Backslash | Backspace | BracketLeft | BracketRight | Comma | Digit0
| Digit1 | Digit2 | Digit3 | Digit4 | Digit5 | Digit6 | Digit7 | Digit8 | Digit9
| Equal | IntlBackslash | IntlRo | IntlYen | KeyA | KeyB | KeyC | KeyD | KeyE
| KeyF | KeyG | KeyH | KeyI | KeyJ | KeyK | KeyL | KeyM | KeyN | KeyO | KeyP | KeyQ
| KeyR | KeyS | KeyT | KeyU | KeyV | KeyW | KeyX | KeyY | KeyZ | Minus | Period
| Quote | Semicolon | Slash => KeyCodeClass::WritingSystem,

// Functional Keys
AltLeft | AltRight | CapsLock | ContextMenu | ControlLeft | ControlRight | Enter
| MetaLeft | MetaRight | ShiftLeft | ShiftRight | Space | Tab | Convert | KanaMode
| Lang1 | Lang2 | Lang3 | Lang4 | Lang5 | NonConvert => KeyCodeClass::Functional,

// Control Pad Section
Delete | End | Help | Home | Insert | PageDown | PageUp => KeyCodeClass::ControlPad,

// Arrow Pad Section
ArrowDown | ArrowLeft | ArrowRight | ArrowUp => KeyCodeClass::ArrowPad,

// Numpad Section
NumLock | Numpad0 | Numpad1 | Numpad2 | Numpad3 | Numpad4 | Numpad5 | Numpad6
| Numpad7 | Numpad8 | Numpad9 | NumpadAdd | NumpadBackspace | NumpadClear
| NumpadClearEntry | NumpadComma | NumpadDecimal | NumpadDivide | NumpadEnter
| NumpadEqual | NumpadHash | NumpadMemoryAdd | NumpadMemoryClear
| NumpadMemoryRecall | NumpadMemoryStore | NumpadMemorySubtract | NumpadMultiply
| NumpadParenLeft | NumpadParenRight | NumpadStar | NumpadSubtract => {
KeyCodeClass::Numpad
}

// Function Section
Escape | F1 | F2 | F3 | F4 | F5 | F6 | F7 | F8 | F9 | F10 | F11 | F12 | F13 | F14
| F15 | F16 | F17 | F18 | F19 | F20 | F21 | F22 | F23 | F24 | Fn | FnLock
| PrintScreen | ScrollLock | Pause => KeyCodeClass::Function,

// Media Keys
BrowserBack | BrowserFavorites | BrowserForward | BrowserHome | BrowserRefresh
| BrowserSearch | BrowserStop | Eject | LaunchApp1 | LaunchApp2 | LaunchMail
| MediaPlayPause | MediaSelect | MediaStop | MediaTrackNext | MediaTrackPrevious
| Power | Sleep | AudioVolumeDown | AudioVolumeMute | AudioVolumeUp | WakeUp => {
KeyCodeClass::Media
}

// Legacy, Non-Standard and Special Keys
Again | Copy | Cut | Find | Open | Paste | Props | Select | Undo => {
KeyCodeClass::Legacy
}

// Gamepad Keys
Gamepad0 | Gamepad1 | Gamepad2 | Gamepad3 | Gamepad4 | Gamepad5 | Gamepad6
| Gamepad7 | Gamepad8 | Gamepad9 | Gamepad10 | Gamepad11 | Gamepad12 | Gamepad13
| Gamepad14 | Gamepad15 | Gamepad16 | Gamepad17 | Gamepad18 | Gamepad19 => {
KeyCodeClass::Gamepad
}

// Browser specific Keys
BrightnessDown | BrightnessUp | DisplayToggleIntExt | KeyboardLayoutSelect
| LaunchAssistant | LaunchControlPanel | LaunchScreenSaver | MailForward
| MailReply | MailSend | MediaFastForward | MediaPause | MediaPlay | MediaRecord
| MediaRewind | PrivacyScreenToggle | SelectTask | ShowAllWindows | ZoomToggle => {
KeyCodeClass::NonStandard
}
}
}

pub fn resolve(self) -> Cow<'static, str> {
let class = self.classify();
if class == KeyCodeClass::WritingSystem {
if let Some(resolved) = crate::platform::try_resolve(self) {
let uppercase = if resolved != "ß" {
resolved.to_uppercase()
} else {
resolved
};
return uppercase.into();
}
}
self.as_str().into()
}
}

impl FromStr for KeyCode {
Expand Down
35 changes: 27 additions & 8 deletions crates/livesplit-hotkey/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,37 +2,39 @@
#![recursion_limit = "1024"]
#![cfg_attr(not(feature = "std"), no_std)]

extern crate alloc;

cfg_if::cfg_if! {
if #[cfg(not(feature = "std"))] {
mod other;
pub use self::other::*;
use self::other as platform;
} else if #[cfg(windows)] {
mod windows;
pub use self::windows::*;
use self::windows as platform;
} else if #[cfg(target_os = "linux")] {
mod linux;
pub use self::linux::*;
use self::linux as platform;
} else if #[cfg(target_os = "macos")] {
mod macos;
pub use self::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;
pub use self::wasm_web::*;
use self::wasm_web as platform;
} else {
mod wasm_unknown;
pub use self::wasm_unknown::*;
use self::wasm_unknown as platform;
}
}
} else {
mod other;
pub use self::other::*;
use self::other as platform;
}
}

mod key_code;
pub use self::key_code::*;
pub use self::{key_code::*, platform::*};

#[cfg(test)]
mod tests {
Expand All @@ -56,4 +58,21 @@ mod tests {
thread::sleep(Duration::from_secs(5));
hook.unregister(KeyCode::Numpad1).unwrap();
}

#[test]
fn resolve() {
// Based on German keyboard layout.
println!("ß: {}", KeyCode::Minus.resolve());
println!("ü: {}", KeyCode::BracketLeft.resolve());
println!("#: {}", KeyCode::Backslash.resolve());
println!("+: {}", KeyCode::BracketRight.resolve());
println!("z: {}", KeyCode::KeyY.resolve());
println!("^: {}", KeyCode::Backquote.resolve());
println!("<: {}", KeyCode::IntlBackslash.resolve());
println!("Yen: {}", KeyCode::IntlYen.resolve());
println!("Enter: {}", KeyCode::Enter.resolve());
println!("Space: {}", KeyCode::Space.resolve());
println!("Tab: {}", KeyCode::Tab.resolve());
println!("Numpad0: {}", KeyCode::Numpad0.resolve());
}
}
4 changes: 4 additions & 0 deletions crates/livesplit-hotkey/src/linux/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,3 +421,7 @@ impl Hook {
future.value().ok_or(Error::ThreadStopped)?
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
4 changes: 4 additions & 0 deletions crates/livesplit-hotkey/src/macos/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,3 +292,7 @@ unsafe extern "C" fn callback(
}
event
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
5 changes: 5 additions & 0 deletions crates/livesplit-hotkey/src/other/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::KeyCode;
use alloc::string::String;

#[derive(Debug, snafu::Snafu)]
pub enum Error {}
Expand All @@ -23,3 +24,7 @@ impl Hook {
Ok(())
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
4 changes: 4 additions & 0 deletions crates/livesplit-hotkey/src/wasm_unknown/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,7 @@ impl Hook {
}
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
10 changes: 5 additions & 5 deletions crates/livesplit-hotkey/src/wasm_web/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ use wasm_bindgen::{prelude::*, JsCast};
use web_sys::{window, Gamepad, GamepadButton, KeyboardEvent};

use std::{
array,
cell::Cell,
collections::hash_map::{Entry, HashMap},
sync::{Arc, Mutex},
Expand Down Expand Up @@ -82,10 +81,7 @@ impl Hook {
}) as Box<dyn FnMut(KeyboardEvent)>);

window
.add_event_listener_with_callback(
"keydown",
keyboard_callback.as_ref().unchecked_ref(),
)
.add_event_listener_with_callback("keydown", keyboard_callback.as_ref().unchecked_ref())
.map_err(|_| Error::FailedToCreateHook)?;

let hotkey_map = hotkeys.clone();
Expand Down Expand Up @@ -162,3 +158,7 @@ impl Hook {
}
}
}

pub(crate) fn try_resolve(_key_code: KeyCode) -> Option<String> {
None
}
80 changes: 78 additions & 2 deletions crates/livesplit-hotkey/src/windows/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ use winapi::{
libloaderapi::GetModuleHandleW,
processthreadsapi::GetCurrentThreadId,
winuser::{
CallNextHookEx, GetMessageW, PostThreadMessageW, SetWindowsHookExW,
UnhookWindowsHookEx, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, WH_KEYBOARD_LL, WM_KEYDOWN,
CallNextHookEx, GetMessageW, MapVirtualKeyW, PostThreadMessageW, SetWindowsHookExW,
UnhookWindowsHookEx, KBDLLHOOKSTRUCT, LLKHF_EXTENDED, MAPVK_VK_TO_CHAR,
MAPVK_VSC_TO_VK_EX, WH_KEYBOARD_LL, WM_KEYDOWN,
},
},
};
Expand Down Expand Up @@ -346,3 +347,78 @@ impl Hook {
}
}
}

pub(crate) fn try_resolve(key_code: KeyCode) -> Option<String> {
use self::KeyCode::*;
let scan_code = match key_code {
Backquote => 0x0029,
Backslash => 0x002B,
Backspace => 0x000E,
BracketLeft => 0x001A,
BracketRight => 0x001B,
Comma => 0x0033,
Digit1 => 0x0002,
Digit2 => 0x0003,
Digit3 => 0x0004,
Digit4 => 0x0005,
Digit5 => 0x0006,
Digit6 => 0x0007,
Digit7 => 0x0008,
Digit8 => 0x0009,
Digit9 => 0x000A,
Digit0 => 0x000B,
Equal => 0x000D,
IntlBackslash => 0x0056,
IntlRo => 0x0073,
IntlYen => 0x007D,
KeyA => 0x001E,
KeyB => 0x0030,
KeyC => 0x002E,
KeyD => 0x0020,
KeyE => 0x0012,
KeyF => 0x0021,
KeyG => 0x0022,
KeyH => 0x0023,
KeyI => 0x0017,
KeyJ => 0x0024,
KeyK => 0x0025,
KeyL => 0x0026,
KeyM => 0x0032,
KeyN => 0x0031,
KeyO => 0x0018,
KeyP => 0x0019,
KeyQ => 0x0010,
KeyR => 0x0013,
KeyS => 0x001F,
KeyT => 0x0014,
KeyU => 0x0016,
KeyV => 0x002F,
KeyW => 0x0011,
KeyX => 0x002D,
KeyY => 0x0015,
KeyZ => 0x002C,
Minus => 0x000C,
Period => 0x0034,
Quote => 0x0028,
Semicolon => 0x0027,
Slash => 0x0035,
_ => return None,
};

let virtual_key_code = unsafe { MapVirtualKeyW(scan_code, MAPVK_VSC_TO_VK_EX) };
if virtual_key_code == 0 {
return None;
}

let mapped_char = unsafe { MapVirtualKeyW(virtual_key_code, MAPVK_VK_TO_CHAR) };
if mapped_char == 0 {
return None;
}

// Dead keys (diacritics) are indicated by setting the top bit of the return
// value.
const TOP_BIT_MASK: u32 = u32::MAX >> 1;
let char = mapped_char & TOP_BIT_MASK;

Some(char::from_u32(char)?.to_string())
}
5 changes: 4 additions & 1 deletion src/settings/image/mod.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
use crate::platform::prelude::*;
use base64::{display::Base64Display, STANDARD};
use core::{ops::Deref, sync::atomic::{AtomicUsize, Ordering}};
use core::{
ops::Deref,
sync::atomic::{AtomicUsize, Ordering},
};
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};

#[cfg(test)]
Expand Down