diff --git a/Cargo.toml b/Cargo.toml index 07d7cf0..ce45f43 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,11 @@ keyboard-types = "0.7" once_cell = "1" thiserror = "1" +[target."cfg(target_os = \"macos\")".dependencies] +bitflags = "2" +cocoa = "0.25" +objc = "0.2" + [target."cfg(target_os = \"windows\")".dependencies.windows-sys] version = "0.52" features = [ diff --git a/examples/egui.rs b/examples/egui.rs index 9582840..e02c0d9 100644 --- a/examples/egui.rs +++ b/examples/egui.rs @@ -7,8 +7,9 @@ use global_hotkey::{hotkey::HotKey, GlobalHotKeyEvent, GlobalHotKeyManager}; use keyboard_types::{Code, Modifiers}; fn main() -> Result<(), eframe::Error> { - let manager = GlobalHotKeyManager::new().unwrap(); + let mut manager = GlobalHotKeyManager::new().unwrap(); let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); + manager.register(hotkey).unwrap(); let receiver = GlobalHotKeyEvent::receiver(); std::thread::spawn(|| loop { diff --git a/examples/iced.rs b/examples/iced.rs index 952b8f9..b90ab49 100644 --- a/examples/iced.rs +++ b/examples/iced.rs @@ -28,11 +28,12 @@ impl Application for Example { type Flags = (); fn new(_flags: Self::Flags) -> (Example, iced::Command) { - let manager = GlobalHotKeyManager::new().unwrap(); + let mut manager = GlobalHotKeyManager::new().unwrap(); let hotkey_1 = HotKey::new(Some(Modifiers::CONTROL), Code::ArrowRight); let hotkey_2 = HotKey::new(None, Code::ArrowUp); - manager.register(hotkey_2).unwrap(); + manager.register(hotkey_1).unwrap(); + manager.register(hotkey_2).unwrap(); ( Example { last_pressed: "".to_string(), diff --git a/examples/tao.rs b/examples/tao.rs index f57ad55..66b0576 100644 --- a/examples/tao.rs +++ b/examples/tao.rs @@ -11,15 +11,29 @@ use tao::event_loop::{ControlFlow, EventLoopBuilder}; fn main() { let event_loop = EventLoopBuilder::new().build(); - let hotkeys_manager = GlobalHotKeyManager::new().unwrap(); + let mut hotkeys_manager = GlobalHotKeyManager::new().unwrap(); let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); let hotkey2 = HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::KeyD); let hotkey3 = HotKey::new(None, Code::KeyF); + let hotkey4 = { + #[cfg(target_os = "macos")] + { + HotKey::new( + Some(Modifiers::SHIFT | Modifiers::ALT), + Code::MediaPlayPause, + ) + } + #[cfg(not(target_os = "macos"))] + { + HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::MediaPlay) + } + }; hotkeys_manager.register(hotkey).unwrap(); hotkeys_manager.register(hotkey2).unwrap(); hotkeys_manager.register(hotkey3).unwrap(); + hotkeys_manager.register(hotkey4).unwrap(); let global_hotkey_channel = GlobalHotKeyEvent::receiver(); diff --git a/examples/winit.rs b/examples/winit.rs index 598e71b..faf9cc0 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -11,7 +11,7 @@ use winit::event_loop::{ControlFlow, EventLoopBuilder}; fn main() { let event_loop = EventLoopBuilder::new().build().unwrap(); - let hotkeys_manager = GlobalHotKeyManager::new().unwrap(); + let mut hotkeys_manager = GlobalHotKeyManager::new().unwrap(); let hotkey = HotKey::new(Some(Modifiers::SHIFT), Code::KeyD); let hotkey2 = HotKey::new(Some(Modifiers::SHIFT | Modifiers::ALT), Code::KeyD); diff --git a/src/error.rs b/src/error.rs index f83f735..ee54d5f 100644 --- a/src/error.rs +++ b/src/error.rs @@ -26,6 +26,8 @@ pub enum Error { FailedToUnRegister(HotKey), #[error("HotKey already registerd: {0:?}")] AlreadyRegistered(HotKey), + #[error("Failed to watch media key event")] + FailedToWatchMediaKeyEvent, } /// Convenient type alias of Result type for tray-icon. diff --git a/src/lib.rs b/src/lib.rs index 3a9a319..c7b2616 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -141,24 +141,25 @@ impl GlobalHotKeyManager { }) } - pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { + pub fn register(&mut self, hotkey: HotKey) -> crate::Result<()> { self.platform_impl.register(hotkey) } - pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { + pub fn unregister(&mut self, hotkey: HotKey) -> crate::Result<()> { self.platform_impl.unregister(hotkey) } - pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { + pub fn register_all(&mut self, hotkeys: &[HotKey]) -> crate::Result<()> { self.platform_impl.register_all(hotkeys)?; Ok(()) } - pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { + pub fn unregister_all(&mut self, hotkeys: &[HotKey]) -> crate::Result<()> { self.platform_impl.unregister_all(hotkeys)?; Ok(()) } } + #[cfg(test)] mod tests { fn assert_send() {} diff --git a/src/platform_impl/macos/ffi.rs b/src/platform_impl/macos/ffi.rs index 53f0f92..baeeb4c 100644 --- a/src/platform_impl/macos/ffi.rs +++ b/src/platform_impl/macos/ffi.rs @@ -5,6 +5,8 @@ /* taken from https://github.com/wusyong/carbon-bindgen/blob/467fca5d71047050b632fbdfb41b1f14575a8499/bindings.rs */ +use std::ffi::{c_long, c_void}; + pub type UInt32 = ::std::os::raw::c_uint; pub type SInt32 = ::std::os::raw::c_int; pub type OSStatus = SInt32; @@ -115,3 +117,144 @@ extern "C" { ) -> OSStatus; pub fn UnregisterEventHotKey(inHotKey: EventHotKeyRef) -> OSStatus; } + +/* Core Graphics */ + +/// Possible tapping points for events. +#[repr(C)] +#[derive(Clone, Copy, Debug)] +pub enum CGEventTapLocation { + Hid, + Session, + AnnotatedSession, +} + +// The next three enums are taken from: +// [Ref](https://github.com/phracker/MacOSX-SDKs/blob/ef9fe35d5691b6dd383c8c46d867a499817a01b6/MacOSX10.15.sdk/System/Library/Frameworks/CoreGraphics.framework/Versions/A/Headers/CGEventTypes.h) +/* Constants that specify where a new event tap is inserted into the list of active event taps. */ +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum CGEventTapPlacement { + HeadInsertEventTap = 0, + TailAppendEventTap, +} + +/* Constants that specify whether a new event tap is an active filter or a passive listener. */ +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum CGEventTapOptions { + Default = 0x00000000, + ListenOnly = 0x00000001, +} + +/// Constants that specify the different types of input events. +/// +/// [Ref](http://opensource.apple.com/source/IOHIDFamily/IOHIDFamily-700/IOHIDSystem/IOKit/hidsystem/IOLLEvent.h) +#[repr(u32)] +#[derive(Clone, Copy, Debug)] +pub enum CGEventType { + Null = 0, + + // Mouse events. + LeftMouseDown = 1, + LeftMouseUp = 2, + RightMouseDown = 3, + RightMouseUp = 4, + MouseMoved = 5, + LeftMouseDragged = 6, + RightMouseDragged = 7, + + // Keyboard events. + KeyDown = 10, + KeyUp = 11, + FlagsChanged = 12, + + // Composite events. + AppKitDefined = 13, + SystemDefined = 14, + ApplicationDefined = 15, + + // Specialized control devices. + ScrollWheel = 22, + TabletPointer = 23, + TabletProximity = 24, + OtherMouseDown = 25, + OtherMouseUp = 26, + OtherMouseDragged = 27, + + // Out of band event types. These are delivered to the event tap callback + // to notify it of unusual conditions that disable the event tap. + TapDisabledByTimeout = 0xFFFFFFFE, + TapDisabledByUserInput = 0xFFFFFFFF, +} + +pub type CGEventMask = u64; +#[macro_export] +macro_rules! CGEventMaskBit { + ($eventType:expr) => { + 1 << $eventType as CGEventMask + }; +} + +pub enum CGEvent {} +pub type CGEventRef = *const CGEvent; + +pub type CGEventTapProxy = *const c_void; +type CGEventTapCallBack = unsafe extern "C" fn( + proxy: CGEventTapProxy, + etype: CGEventType, + event: CGEventRef, + user_info: *const c_void, +) -> CGEventRef; + +#[link(name = "CoreGraphics", kind = "framework")] +extern "C" { + pub fn CGEventTapCreate( + tap: CGEventTapLocation, + place: CGEventTapPlacement, + options: CGEventTapOptions, + events_of_interest: CGEventMask, + callback: CGEventTapCallBack, + user_info: *const c_void, + ) -> CFMachPortRef; + pub fn CGEventTapEnable(tap: CFMachPortRef, enable: bool); +} + +/* Core Foundation */ + +pub enum CFAllocator {} +pub type CFAllocatorRef = *mut CFAllocator; +pub enum CFRunLoop {} +pub type CFRunLoopRef = *mut CFRunLoop; +pub type CFRunLoopMode = CFStringRef; +pub enum CFRunLoopObserver {} +pub type CFRunLoopObserverRef = *mut CFRunLoopObserver; +pub enum CFRunLoopTimer {} +pub type CFRunLoopTimerRef = *mut CFRunLoopTimer; +pub enum CFRunLoopSource {} +pub type CFRunLoopSourceRef = *mut CFRunLoopSource; +pub enum CFString {} +pub type CFStringRef = *const CFString; + +pub enum CFMachPort {} +pub type CFMachPortRef = *mut CFMachPort; + +pub type CFIndex = c_long; + +#[link(name = "CoreFoundation", kind = "framework")] +extern "C" { + pub static kCFRunLoopCommonModes: CFRunLoopMode; + pub static kCFAllocatorDefault: CFAllocatorRef; + + pub fn CFRunLoopGetMain() -> CFRunLoopRef; + + pub fn CFMachPortCreateRunLoopSource( + allocator: CFAllocatorRef, + port: CFMachPortRef, + order: CFIndex, + ) -> CFRunLoopSourceRef; + pub fn CFMachPortInvalidate(port: CFMachPortRef); + pub fn CFRunLoopAddSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); + pub fn CFRunLoopRemoveSource(rl: CFRunLoopRef, source: CFRunLoopSourceRef, mode: CFRunLoopMode); + pub fn CFRelease(cftype: *const c_void); +} diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 737febc..71acc4e 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -1,12 +1,33 @@ -use std::{collections::BTreeMap, ffi::c_void, sync::Mutex}; - +use bitflags::bitflags; +use cocoa::{ + appkit::NSEventType, + base::id, + foundation::{NSInteger, NSUInteger}, +}; use keyboard_types::{Code, Modifiers}; +use objc::{class, msg_send, sel, sel_impl}; +use std::{ + collections::{BTreeMap, HashSet}, + ffi::c_void, + ptr, + sync::{Arc, Mutex}, +}; -use crate::{hotkey::HotKey, GlobalHotKeyEvent}; +use crate::{ + hotkey::HotKey, + platform_impl::platform::ffi::{ + kCFAllocatorDefault, kCFRunLoopCommonModes, CFMachPortCreateRunLoopSource, + CFRunLoopAddSource, CFRunLoopGetMain, CGEventMask, CGEventRef, CGEventTapCreate, + CGEventTapEnable, CGEventTapLocation, CGEventTapOptions, CGEventTapPlacement, + CGEventTapProxy, CGEventType, + }, + CGEventMaskBit, GlobalHotKeyEvent, +}; use self::ffi::{ kEventClassKeyboard, kEventHotKeyPressed, kEventHotKeyReleased, kEventParamDirectObject, noErr, - typeEventHotKeyID, EventHandlerCallRef, EventHandlerRef, EventHotKeyID, EventHotKeyRef, + typeEventHotKeyID, CFMachPortInvalidate, CFMachPortRef, CFRelease, CFRunLoopRemoveSource, + CFRunLoopSourceRef, EventHandlerCallRef, EventHandlerRef, EventHotKeyID, EventHotKeyRef, EventRef, EventTypeSpec, GetApplicationEventTarget, GetEventKind, GetEventParameter, InstallEventHandler, OSStatus, RegisterEventHotKey, RemoveEventHandler, UnregisterEventHotKey, }; @@ -16,6 +37,9 @@ mod ffi; pub struct GlobalHotKeyManager { event_handler_ptr: EventHandlerRef, hotkeys: Mutex>, + event_tap: Option, + event_tap_source: Option, + media_hotkeys: Arc>>, } unsafe impl Send for GlobalHotKeyManager {} @@ -55,10 +79,13 @@ impl GlobalHotKeyManager { Ok(Self { event_handler_ptr: ptr, hotkeys: Mutex::new(BTreeMap::new()), + event_tap: None, + event_tap_source: None, + media_hotkeys: Arc::new(Mutex::new(HashSet::new())), }) } - pub fn register(&self, hotkey: HotKey) -> crate::Result<()> { + pub fn register(&mut self, hotkey: HotKey) -> crate::Result<()> { let mut mods: u32 = 0; if hotkey.mods.contains(Modifiers::SHIFT) { mods |= 512; @@ -115,6 +142,14 @@ impl GlobalHotKeyManager { .unwrap() .insert(hotkey.id(), HotKeyWrapper { ptr, hotkey }); Ok(()) + } else if is_media_key(hotkey.key) { + { + let mut media_hotkeys = self.media_hotkeys.lock().unwrap(); + if !media_hotkeys.insert(hotkey) { + return Err(crate::Error::AlreadyRegistered(hotkey)); + } + } + self.start_watching_media_keys() } else { Err(crate::Error::FailedToRegister(format!( "Unable to register accelerator (unknown scancode for this key: {}).", @@ -123,22 +158,27 @@ impl GlobalHotKeyManager { } } - pub fn unregister(&self, hotkey: HotKey) -> crate::Result<()> { - if let Some(hotkeywrapper) = self.hotkeys.lock().unwrap().remove(&hotkey.id()) { + pub fn unregister(&mut self, hotkey: HotKey) -> crate::Result<()> { + if is_media_key(hotkey.key) { + self.media_hotkeys.lock().unwrap().remove(&hotkey); + if self.media_hotkeys.lock().unwrap().is_empty() { + self.stop_watching_media_keys(); + } + } else if let Some(hotkeywrapper) = self.hotkeys.lock().unwrap().remove(&hotkey.id()) { unsafe { self.unregister_hotkey_ptr(hotkeywrapper.ptr, hotkey) }?; } Ok(()) } - pub fn register_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { + pub fn register_all(&mut self, hotkeys: &[HotKey]) -> crate::Result<()> { for hotkey in hotkeys { self.register(*hotkey)?; } Ok(()) } - pub fn unregister_all(&self, hotkeys: &[HotKey]) -> crate::Result<()> { + pub fn unregister_all(&mut self, hotkeys: &[HotKey]) -> crate::Result<()> { for hotkey in hotkeys { self.unregister(*hotkey)?; } @@ -156,6 +196,121 @@ impl GlobalHotKeyManager { Ok(()) } + + fn start_watching_media_keys(&mut self) -> crate::Result<()> { + if self.event_tap.is_some() || self.event_tap_source.is_some() { + return Ok(()); + } + + unsafe { + let event_mask: CGEventMask = CGEventMaskBit!(CGEventType::SystemDefined); + let event_tap = CGEventTapCreate( + CGEventTapLocation::Session, + CGEventTapPlacement::HeadInsertEventTap, + CGEventTapOptions::Default, + event_mask, + media_key_event_callback, + Arc::into_raw(self.media_hotkeys.clone()) as *const c_void, + ); + if event_tap.is_null() { + return Err(crate::Error::FailedToWatchMediaKeyEvent); + } + self.event_tap = Some(event_tap); + + let loop_source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, event_tap, 0); + if loop_source.is_null() { + return Err(crate::Error::FailedToWatchMediaKeyEvent); + } + self.event_tap_source = Some(loop_source); + + let run_loop = CFRunLoopGetMain(); + CFRunLoopAddSource(run_loop, loop_source, kCFRunLoopCommonModes); + CGEventTapEnable(event_tap, true); + + Ok(()) + } + } + + fn stop_watching_media_keys(&mut self) { + if let (Some(event_tap), Some(event_tap_source)) = (self.event_tap, self.event_tap_source) { + unsafe { + let run_loop = CFRunLoopGetMain(); + CFRunLoopRemoveSource(run_loop, event_tap_source, kCFRunLoopCommonModes); + + CFMachPortInvalidate(event_tap); + CFRelease(event_tap as *const c_void); + self.event_tap = None; + + CFRelease(event_tap_source as *const c_void); + self.event_tap_source = None; + } + } + } +} + +bitflags! { + #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] + struct NSEventModifierFlags: NSUInteger { + const Shift = 1 << 17; + const Control = 1 << 18; + const Option = 1 << 19; + const Command = 1 << 20; + } +} + +impl From for Modifiers { + fn from(mod_flags: NSEventModifierFlags) -> Self { + let mut mods = Modifiers::empty(); + if mod_flags.contains(NSEventModifierFlags::Shift) { + mods |= Modifiers::SHIFT; + } + if mod_flags.contains(NSEventModifierFlags::Control) { + mods |= Modifiers::CONTROL; + } + if mod_flags.contains(NSEventModifierFlags::Option) { + mods |= Modifiers::ALT; + } + if mod_flags.contains(NSEventModifierFlags::Command) { + mods |= Modifiers::META; + } + mods + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] +enum MediaKeyCode { + PlayPause = 16, + Next = 17, + Previous = 18, + Fast = 19, + Rewind = 20, +} + +impl TryFrom for MediaKeyCode { + type Error = String; + + fn try_from(value: i64) -> Result { + match value { + 16 => Ok(MediaKeyCode::PlayPause), + 17 => Ok(MediaKeyCode::Next), + 18 => Ok(MediaKeyCode::Previous), + 19 => Ok(MediaKeyCode::Fast), + 20 => Ok(MediaKeyCode::Rewind), + _ => Err(String::from("Not defined media key")), + } + } +} + +impl From for Code { + fn from(media_key: MediaKeyCode) -> Self { + match media_key { + MediaKeyCode::PlayPause => Code::MediaPlayPause, + MediaKeyCode::Next => Code::MediaTrackNext, + MediaKeyCode::Previous => Code::MediaTrackPrevious, + MediaKeyCode::Fast => Code::MediaFastForward, + MediaKeyCode::Rewind => Code::MediaRewind, + } + } } impl Drop for GlobalHotKeyManager { @@ -207,6 +362,55 @@ unsafe extern "C" fn hotkey_handler( noErr as _ } +unsafe extern "C" fn media_key_event_callback( + _proxy: CGEventTapProxy, + ev_type: CGEventType, + event: CGEventRef, + user_info: *const c_void, +) -> CGEventRef { + let ns_event: id = msg_send![class!(NSEvent), eventWithCGEvent:event]; + let event_type: NSEventType = msg_send![ns_event, type]; + + if let CGEventType::SystemDefined = ev_type { + let event_subtype: u64 = msg_send![ns_event, subtype]; + if event_type == NSEventType::NSSystemDefined && event_subtype == 8 { + // Key + let data_1: NSInteger = msg_send![ns_event, data1]; + let media_key = MediaKeyCode::try_from((data_1 & 0xFFFF0000) >> 16); + if media_key.is_err() { + return event; + } + let media_key = media_key.unwrap(); + + // Modifiers + let mod_flags: NSUInteger = msg_send![ns_event, modifierFlags]; + let mod_flags = NSEventModifierFlags::from_bits_truncate(mod_flags); + + // Generate hotkey for matching + let hotkey = HotKey::new(Some(mod_flags.into()), media_key.into()); + + // Prevent Arc been releaded after callback returned + let media_hotkeys = &*(user_info as *const Mutex>); + + if let Some(media_hotkey) = media_hotkeys.lock().unwrap().get(&hotkey) { + let key_flags = data_1 & 0x0000FFFF; + let is_pressed: bool = ((key_flags & 0xFF00) >> 8) == 0xA; + GlobalHotKeyEvent::send(GlobalHotKeyEvent { + id: media_hotkey.id(), + state: match is_pressed { + true => crate::HotKeyState::Pressed, + false => crate::HotKeyState::Released, + }, + }); + + // Hotkey was found, return null to stop propagate event + return ptr::null(); + } + } + } + event +} + #[derive(Clone, Copy, Debug)] struct HotKeyWrapper { ptr: EventHotKeyRef, @@ -325,3 +529,14 @@ pub fn key_to_scancode(code: Code) -> Option { _ => None, } } + +fn is_media_key(code: Code) -> bool { + matches!( + code, + Code::MediaPlayPause + | Code::MediaTrackNext + | Code::MediaTrackPrevious + | Code::MediaFastForward + | Code::MediaRewind + ) +}