diff --git a/crates/livesplit-hotkey/Cargo.toml b/crates/livesplit-hotkey/Cargo.toml index 0b2d9022b..b42714f30 100644 --- a/crates/livesplit-hotkey/Cargo.toml +++ b/crates/livesplit-hotkey/Cargo.toml @@ -59,4 +59,5 @@ std = [ "windows-sys", "x11-dl", ] +press_and_release = [] wasm-web = ["wasm-bindgen", "web-sys", "js-sys"] diff --git a/crates/livesplit-hotkey/src/lib.rs b/crates/livesplit-hotkey/src/lib.rs index 74a3432a1..936e53f1e 100644 --- a/crates/livesplit-hotkey/src/lib.rs +++ b/crates/livesplit-hotkey/src/lib.rs @@ -89,6 +89,18 @@ impl Hook { self.0.register(hotkey, callback) } + /// Registers a hotkey to listen to, but with specific handling for + /// press and release events. + /// + /// Requires the `press_and_release` feature to be enabled. + #[cfg(feature = "press_and_release")] + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + self.0.register_specific(hotkey, callback) + } + /// Unregisters a previously registered hotkey. pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { self.0.unregister(hotkey) diff --git a/crates/livesplit-hotkey/src/linux/evdev_impl.rs b/crates/livesplit-hotkey/src/linux/evdev_impl.rs index d9b26eb8e..e455aa530 100644 --- a/crates/livesplit-hotkey/src/linux/evdev_impl.rs +++ b/crates/livesplit-hotkey/src/linux/evdev_impl.rs @@ -1,10 +1,10 @@ use std::{collections::hash_map::HashMap, os::unix::prelude::AsRawFd, ptr, thread}; use evdev::{Device, EventType, InputEventKind, Key}; -use mio::{unix::SourceFd, Events, Interest, Poll, Token, Waker}; -use x11_dl::xlib::{Xlib, _XDisplay}; +use mio::{Events, Interest, Poll, Token, Waker, unix::SourceFd}; +use x11_dl::xlib::{_XDisplay, Xlib}; -use super::{x11_impl, Error, Hook, Message}; +use super::{Error, Hook, Message, x11_impl}; use crate::{KeyCode, Modifiers, Result}; // Low numbered tokens are allocated to devices. @@ -255,6 +255,9 @@ pub fn new() -> Result { let mut result = Ok(()); let mut events = Events::with_capacity(1024); let mut hotkeys: HashMap<(Key, Modifiers), Box> = HashMap::new(); + #[cfg(feature = "press_and_release")] + let mut specific_hotkeys: HashMap<(Key, Modifiers), Box> = + HashMap::new(); let mut modifiers = Modifiers::empty(); let (mut xlib, mut display) = (None, None); @@ -274,6 +277,12 @@ pub fn new() -> Result { const PRESSED: i32 = 1; match ev.value() { PRESSED => { + #[cfg(feature = "press_and_release")] + if let Some(callback) = + specific_hotkeys.get_mut(&(k, modifiers)) + { + callback(true); + } if let Some(callback) = hotkeys.get_mut(&(k, modifiers)) { callback(); } @@ -293,21 +302,29 @@ pub fn new() -> Result { _ => {} } } - 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); + RELEASED => { + #[cfg(feature = "press_and_release")] + if let Some(callback) = + specific_hotkeys.get_mut(&(k, modifiers)) + { + callback(false); } - Key::KEY_LEFTSHIFT | Key::KEY_RIGHTSHIFT => { - modifiers.remove(Modifiers::SHIFT); + 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 } } @@ -327,6 +344,21 @@ pub fn new() -> Result { }, ); } + #[cfg(feature = "press_and_release")] + Message::RegisterSpecific(key, callback, promise) => { + promise.set( + if code_for(key.key_code) + .and_then(|k| { + specific_hotkeys.insert((k, key.modifiers), callback) + }) + .is_some() + { + Err(crate::Error::AlreadyRegistered) + } else { + Ok(()) + }, + ); + } Message::Unregister(key, promise) => promise.set( code_for(key.key_code) .and_then(|k| hotkeys.remove(&(k, key.modifiers)).map(drop)) diff --git a/crates/livesplit-hotkey/src/linux/mod.rs b/crates/livesplit-hotkey/src/linux/mod.rs index b9179fc26..4f0d53e5a 100644 --- a/crates/livesplit-hotkey/src/linux/mod.rs +++ b/crates/livesplit-hotkey/src/linux/mod.rs @@ -3,8 +3,8 @@ use std::{fmt, thread::JoinHandle}; use crate::{ConsumePreference, Hotkey, KeyCode, Result}; use crossbeam_channel::Sender; use mio::Waker; -use nix::unistd::{getgroups, Group}; -use promising_future::{future_promise, Promise}; +use nix::unistd::{Group, getgroups}; +use promising_future::{Promise, future_promise}; mod evdev_impl; mod x11_impl; @@ -43,6 +43,12 @@ enum Message { Box, Promise>, ), + #[cfg(feature = "press_and_release")] + RegisterSpecific( + Hotkey, + Box, + Promise>, + ), Unregister(Hotkey, Promise>), Resolve(KeyCode, Promise>), End, @@ -105,6 +111,26 @@ impl Hook { future.value().ok_or(Error::ThreadStopped)? } + #[cfg(feature = "press_and_release")] + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + let (future, promise) = future_promise(); + + self.sender + .send(Message::RegisterSpecific( + hotkey, + Box::new(callback), + promise, + )) + .map_err(|_| Error::ThreadStopped)?; + + self.waker.wake().map_err(|_| Error::ThreadStopped)?; + + future.value().ok_or(Error::ThreadStopped)? + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { let (future, promise) = future_promise(); diff --git a/crates/livesplit-hotkey/src/linux/x11_impl.rs b/crates/livesplit-hotkey/src/linux/x11_impl.rs index 10bb5b01d..504d91a17 100644 --- a/crates/livesplit-hotkey/src/linux/x11_impl.rs +++ b/crates/livesplit-hotkey/src/linux/x11_impl.rs @@ -7,8 +7,8 @@ use std::{ use mio::{Events, Interest, Poll, Token, Waker, unix::SourceFd}; use x11_dl::xlib::{ - _XDisplay, AnyKey, AnyModifier, ControlMask, Display, GrabModeAsync, KeyPress, LockMask, - Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, ShiftMask, XErrorEvent, XKeyEvent, Xlib, + _XDisplay, AnyKey, AnyModifier, ControlMask, Display, GrabModeAsync, KeyPress, KeyRelease, + LockMask, Mod1Mask, Mod2Mask, Mod3Mask, Mod4Mask, ShiftMask, XErrorEvent, XKeyEvent, Xlib, }; use super::{Error, Hook, Message}; @@ -151,6 +151,8 @@ pub fn new() -> Result { let mut result = Ok(()); let mut events = Events::with_capacity(1024); let mut hotkeys = HashMap::new(); + #[cfg(feature = "press_and_release")] + let mut specific_hotkeys = HashMap::new(); // For some reason we need to call this once for any KeyGrabs to // actually do anything. @@ -179,6 +181,22 @@ pub fn new() -> Result { Ok(()) }); } + #[cfg(feature = "press_and_release")] + Message::RegisterSpecific(key, callback, promise) => { + promise.set(if let Some(code) = code_for(key.key_code) { + if specific_hotkeys + .insert((code, key.modifiers), callback) + .is_some() + { + Err(crate::Error::AlreadyRegistered) + } else { + grab_key(&xlib, display, code, key.modifiers, false); + Ok(()) + } + } else { + Ok(()) + }); + } Message::Unregister(key, promise) => { let res = if let Some(code) = code_for(key.key_code) { let res = hotkeys @@ -208,7 +226,8 @@ pub fn new() -> Result { let err_code = (xlib.XNextEvent)(display, event.as_mut_ptr()); if err_code == 0 { let event = event.assume_init(); - if event.get_type() == KeyPress { + let event_type = event.get_type(); + if event_type == KeyPress || event_type == KeyRelease { let event: &XKeyEvent = event.as_ref(); let mut modifiers = Modifiers::empty(); @@ -225,10 +244,22 @@ pub fn new() -> Result { modifiers.insert(Modifiers::META); } + let is_press = event_type == KeyPress; + + #[cfg(feature = "press_and_release")] if let Some(callback) = - hotkeys.get_mut(&(event.keycode, modifiers)) + specific_hotkeys.get_mut(&(event.keycode, modifiers)) { - callback(); + callback(is_press); + } + + // Existing hotkeys (press only) + if is_press { + if let Some(callback) = + hotkeys.get_mut(&(event.keycode, modifiers)) + { + callback(); + } } } } diff --git a/crates/livesplit-hotkey/src/macos/cg.rs b/crates/livesplit-hotkey/src/macos/cg.rs index a8d61d234..9d279e2fa 100644 --- a/crates/livesplit-hotkey/src/macos/cg.rs +++ b/crates/livesplit-hotkey/src/macos/cg.rs @@ -223,4 +223,6 @@ unsafe extern "C" { pub fn CGEventTapEnable(tap: MachPortRef, enable: bool); pub fn CGEventGetIntegerValueField(event: EventRef, field: EventField) -> i64; + + pub fn CGEventGetType(event: EventRef) -> EventType; } diff --git a/crates/livesplit-hotkey/src/macos/mod.rs b/crates/livesplit-hotkey/src/macos/mod.rs index 6a4eea481..8e6bb16b1 100644 --- a/crates/livesplit-hotkey/src/macos/mod.rs +++ b/crates/livesplit-hotkey/src/macos/mod.rs @@ -76,6 +76,8 @@ unsafe impl Sync for RunLoop {} struct State { hotkeys: Mutex>>, + #[cfg(feature = "press_and_release")] + specific_hotkeys: Mutex>>, } /// A hook allows you to listen to hotkeys. @@ -105,6 +107,8 @@ impl Hook { let state = Arc::new(State { hotkeys: Mutex::new(HashMap::new()), + #[cfg(feature = "press_and_release")] + specific_hotkeys: Mutex::new(HashMap::new()), }); let thread_state = state.clone(); @@ -131,7 +135,7 @@ impl Hook { } else { EventTapOptions::LISTEN_ONLY }, - EventMask::KEY_DOWN, + EventMask::KEY_DOWN | EventMask::KEY_UP, Some(callback), state_ptr as *mut c_void, ); @@ -185,6 +189,19 @@ impl Hook { } } + #[cfg(feature = "press_and_release")] + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + if let Entry::Vacant(vacant) = self.state.specific_hotkeys.lock().unwrap().entry(hotkey) { + vacant.insert(Box::new(callback)); + Ok(()) + } else { + Err(crate::Error::AlreadyRegistered) + } + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { let _ = self .state @@ -491,8 +508,22 @@ unsafe extern "C" fn callback( // If we handled the event and the hook is consuming, we should return // null so the system deletes the event. If the hook is not consuming // the return value will be ignored, so return null anyway. - null_mut() - } else { - event + return null_mut(); } + + #[cfg(feature = "press_and_release")] + if let Some(callback) = state + .specific_hotkeys + .lock() + .unwrap() + .get_mut(&key_code.with_modifiers(modifiers)) + { + // Determine if this is a key down or key up event + let is_key_up = unsafe { cg::CGEventGetType(event) } == cg::EventType::KEY_UP; + + callback(!is_key_up); + return null_mut(); + } + + event } diff --git a/crates/livesplit-hotkey/src/macos/permission.rs b/crates/livesplit-hotkey/src/macos/permission.rs index dac2e57be..e804d530d 100644 --- a/crates/livesplit-hotkey/src/macos/permission.rs +++ b/crates/livesplit-hotkey/src/macos/permission.rs @@ -1,10 +1,10 @@ use super::{ - ax::{kAXTrustedCheckOptionPrompt, AXIsProcessTrustedWithOptions}, + Owned, + ax::{AXIsProcessTrustedWithOptions, kAXTrustedCheckOptionPrompt}, cf::{ - kCFAllocatorDefault, kCFBooleanTrue, kCFTypeDictionaryKeyCallBacks, - kCFTypeDictionaryValueCallBacks, CFDictionaryCreate, + CFDictionaryCreate, kCFAllocatorDefault, kCFBooleanTrue, kCFTypeDictionaryKeyCallBacks, + kCFTypeDictionaryValueCallBacks, }, - Owned, }; use std::ffi::c_void; diff --git a/crates/livesplit-hotkey/src/other/mod.rs b/crates/livesplit-hotkey/src/other/mod.rs index 3e2cb7406..26af8d375 100644 --- a/crates/livesplit-hotkey/src/other/mod.rs +++ b/crates/livesplit-hotkey/src/other/mod.rs @@ -28,6 +28,15 @@ impl Hook { Ok(()) } + #[inline] + #[cfg(feature = "press_and_release")] + pub fn register_specific(&self, _: Hotkey, _: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + Ok(()) + } + #[inline] pub fn unregister(&self, _: Hotkey) -> Result<()> { Ok(()) diff --git a/crates/livesplit-hotkey/src/wasm_web/mod.rs b/crates/livesplit-hotkey/src/wasm_web/mod.rs index d08f0ed51..49fd5e9c0 100644 --- a/crates/livesplit-hotkey/src/wasm_web/mod.rs +++ b/crates/livesplit-hotkey/src/wasm_web/mod.rs @@ -27,6 +27,9 @@ impl fmt::Display for Error { pub struct Hook { hotkeys: Arc>>>, + #[cfg(feature = "press_and_release")] + specific_hotkeys: + Arc>>>, keyboard_callback: Closure, gamepad_callback: Closure, interval_id: Cell>, @@ -41,6 +44,11 @@ impl Drop for Hook { "keydown", self.keyboard_callback.as_ref().unchecked_ref(), ); + #[cfg(feature = "press_and_release")] + let _ = window.remove_event_listener_with_callback( + "keyup", + self.keyboard_callback.as_ref().unchecked_ref(), + ); if let Some(interval_id) = self.interval_id.get() { window.clear_interval_with_handle(interval_id); } @@ -83,10 +91,17 @@ impl Hook { Hotkey, Box, >::new())); + #[cfg(feature = "press_and_release")] + let specific_hotkeys = Arc::new(Mutex::new(HashMap::< + (KeyCode, Modifiers), + Box, + >::new())); let window = window().ok_or(crate::Error::Platform(Error::FailedToCreateHook))?; let hotkey_map = hotkeys.clone(); + #[cfg(feature = "press_and_release")] + let specific_hotkey_map = specific_hotkeys.clone(); let keyboard_callback = Closure::wrap(Box::new(move |event: MaybeKeyboardEvent| { // Despite all sorts of documentation claiming that `keydown` events // pass you a `KeyboardEvent`, this is not actually always the case @@ -131,6 +146,18 @@ impl Hook { event.prevent_default(); } } + + #[cfg(feature = "press_and_release")] + if let Some(callback) = specific_hotkey_map + .lock() + .unwrap() + .get_mut(&(code, modifiers)) + { + callback(event.type_() == "keydown"); + if prevent_default { + event.prevent_default(); + } + } } } }) as Box); @@ -139,7 +166,14 @@ impl Hook { .add_event_listener_with_callback("keydown", keyboard_callback.as_ref().unchecked_ref()) .map_err(|_| crate::Error::Platform(Error::FailedToCreateHook))?; + #[cfg(feature = "press_and_release")] + window + .add_event_listener_with_callback("keyup", keyboard_callback.as_ref().unchecked_ref()) + .map_err(|_| crate::Error::Platform(Error::FailedToCreateHook))?; + let hotkey_map = hotkeys.clone(); + #[cfg(feature = "press_and_release")] + let specific_hotkey_map = specific_hotkeys.clone(); let mut states = Vec::new(); let navigator = window.navigator(); @@ -196,6 +230,15 @@ impl Hook { { callback(); } + } else if !pressed && *state { + #[cfg(feature = "press_and_release")] + if let Some(callback) = specific_hotkey_map + .lock() + .unwrap() + .get_mut(&(code, Modifiers::empty())) + { + callback(false); + } } *state = pressed; } @@ -237,6 +280,24 @@ impl Hook { } } + #[cfg(feature = "press_and_release")] + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + if let Entry::Vacant(vacant) = self + .specific_hotkeys + .lock() + .unwrap() + .entry((hotkey.key_code, hotkey.modifiers)) + { + vacant.insert(Box::new(callback)); + Ok(()) + } else { + Err(crate::Error::AlreadyRegistered) + } + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { Ok(()) diff --git a/crates/livesplit-hotkey/src/windows/mod.rs b/crates/livesplit-hotkey/src/windows/mod.rs index acadd05e0..cef625c0c 100644 --- a/crates/livesplit-hotkey/src/windows/mod.rs +++ b/crates/livesplit-hotkey/src/windows/mod.rs @@ -46,10 +46,14 @@ impl fmt::Display for Error { } type Callback = Box; +#[cfg(feature = "press_and_release")] +type PressReleaseCallback = Box; pub struct Hook { thread_id: u32, hotkeys: Arc>>, + #[cfg(feature = "press_and_release")] + specific_hotkeys: Arc>>, } impl Drop for Hook { @@ -62,7 +66,7 @@ impl Drop for Hook { struct State { hook: HHOOK, - events: Sender, + events: Sender<(Hotkey, bool)>, modifiers: Modifiers, // FIXME: Use variant count when it's stable. // https://github.com/rust-lang/rust/issues/73662 @@ -294,10 +298,13 @@ unsafe extern "system" fn callback_proc(code: i32, wparam: WPARAM, lparam: LPARA state .events - .send(Hotkey { - key_code, - modifiers: state.modifiers, - }) + .send(( + Hotkey { + key_code, + modifiers: state.modifiers, + }, + true, + )) .expect("Callback Thread disconnected"); match key_code { @@ -347,6 +354,17 @@ unsafe extern "system" fn callback_proc(code: i32, wparam: WPARAM, lparam: LPARA let (idx, bit) = key_idx(key_code); state.key_state[idx as usize] &= !bit; + state + .events + .send(( + Hotkey { + key_code, + modifiers: state.modifiers, + }, + false, + )) + .expect("Callback Thread disconnected"); + match key_code { KeyCode::AltLeft | KeyCode::AltRight => { state.modifiers.remove(Modifiers::ALT); @@ -390,6 +408,11 @@ impl Hook { Hotkey, Box, >::new())); + #[cfg(feature = "press_and_release")] + let specific_hotkeys = Arc::new(Mutex::new(HashMap::< + (KeyCode, Modifiers), + Box, + >::new())); let (initialized_tx, initialized_rx) = channel(); let (events_tx, events_rx) = channel(); @@ -446,11 +469,24 @@ impl Hook { }); let hotkey_map = hotkeys.clone(); + #[cfg(feature = "press_and_release")] + let specific_hotkey_map = specific_hotkeys.clone(); thread::spawn(move || { - while let Ok(key) = events_rx.recv() { - if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) { - callback(); + while let Ok((key, is_press)) = events_rx.recv() { + // TODO make single expression once this is stable + if is_press { + if let Some(callback) = hotkey_map.lock().unwrap().get_mut(&key) { + callback(); + } + } + #[cfg(feature = "press_and_release")] + if let Some(callback) = specific_hotkey_map + .lock() + .unwrap() + .get_mut(&(key.key_code, key.modifiers)) + { + callback(is_press); } } }); @@ -459,7 +495,12 @@ impl Hook { .recv() .map_err(|_| crate::Error::Platform(Error::ThreadStopped))??; - Ok(Hook { thread_id, hotkeys }) + Ok(Hook { + thread_id, + hotkeys, + #[cfg(feature = "press_and_release")] + specific_hotkeys, + }) } pub fn register(&self, hotkey: Hotkey, callback: F) -> Result<()> @@ -474,6 +515,24 @@ impl Hook { } } + #[cfg(feature = "press_and_release")] + pub fn register_specific(&self, hotkey: Hotkey, callback: F) -> Result<()> + where + F: FnMut(bool) + Send + 'static, + { + if let Entry::Vacant(vacant) = self + .specific_hotkeys + .lock() + .unwrap() + .entry((hotkey.key_code, hotkey.modifiers)) + { + vacant.insert(Box::new(callback)); + Ok(()) + } else { + Err(crate::Error::AlreadyRegistered) + } + } + pub fn unregister(&self, hotkey: Hotkey) -> Result<()> { if self.hotkeys.lock().unwrap().remove(&hotkey).is_some() { Ok(())