diff --git a/cmake/macos/helpers.cmake b/cmake/macos/helpers.cmake index eb2b96d75..2f203cd58 100644 --- a/cmake/macos/helpers.cmake +++ b/cmake/macos/helpers.cmake @@ -68,6 +68,7 @@ function(set_target_properties_obs target) INFOPLIST_KEY_NSHumanReadableCopyright "(c) 2012-${CURRENT_YEAR} Lain Bailey" INFOPLIST_KEY_NSCameraUsageDescription "OBS needs to access the camera to enable camera sources to work." INFOPLIST_KEY_NSMicrophoneUsageDescription "OBS needs to access the microphone to enable audio input." + INFOPLIST_KEY_NSAppleEventsUsageDescription "OBS needs to access background events to enable hotkeys while not in focus." ) get_property(obs_dependencies GLOBAL PROPERTY _OBS_DEPENDENCIES) diff --git a/libobs/obs-cocoa.m b/libobs/obs-cocoa.m index af43dac6b..08b64ef1b 100644 --- a/libobs/obs-cocoa.m +++ b/libobs/obs-cocoa.m @@ -421,8 +421,7 @@ static const macOS_glyph_desc_t key_glyphs[(keyCodeMask >> 8)] = { struct obs_hotkeys_platform { volatile long refs; - CFTypeRef monitor; - CFTypeRef local_monitor; + CFMachPortRef eventTap; bool is_key_down[OBS_KEY_LAST_VALUE]; TISInputSourceRef tis; CFDataRef layout_data; @@ -456,14 +455,10 @@ static void hotkeys_release(obs_hotkeys_platform_t *platform) platform->layout_data = NULL; } - if (platform->monitor) { - [NSEvent removeMonitor:(__bridge id _Nonnull)(platform->monitor)]; - platform->monitor = NULL; - } - - if (platform->local_monitor) { - [NSEvent removeMonitor:(__bridge id _Nonnull)(platform->local_monitor)]; - platform->local_monitor = NULL; + if (platform->eventTap) { + CGEventTapEnable(platform->eventTap, false); + CFRelease(platform->eventTap); + platform->eventTap = NULL; } bfree(platform); @@ -521,20 +516,40 @@ static bool log_layout_name(TISInputSourceRef tis) // MARK: macOS Hotkey CoreFoundation Callbacks -static void MonitorEventHandlerProc(obs_hotkeys_platform_t *platform, NSEvent *event) +static CGEventRef KeyboardEventProc(CGEventTapProxy proxy __unused, CGEventType type, CGEventRef event, void *userInfo) { - NSEventModifierFlags flags = event.modifierFlags; - platform->is_key_down[OBS_KEY_CAPSLOCK] = !!(flags & NSEventModifierFlagCapsLock); - platform->is_key_down[OBS_KEY_SHIFT] = !!(flags & NSEventModifierFlagShift); - platform->is_key_down[OBS_KEY_ALT] = !!(flags & NSEventModifierFlagOption); - platform->is_key_down[OBS_KEY_META] = !!(flags & NSEventModifierFlagCommand); - platform->is_key_down[OBS_KEY_CONTROL] = !!(flags & NSEventModifierFlagControl); + obs_hotkeys_platform_t *platform = userInfo; - if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) { - obs_key_t obsKey = obs_key_from_virtual_key(event.keyCode); + const CGEventFlags flags = CGEventGetFlags(event); + platform->is_key_down[OBS_KEY_SHIFT] = !!(flags & kCGEventFlagMaskShift); + platform->is_key_down[OBS_KEY_ALT] = !!(flags & kCGEventFlagMaskAlternate); + platform->is_key_down[OBS_KEY_META] = !!(flags & kCGEventFlagMaskCommand); + platform->is_key_down[OBS_KEY_CONTROL] = !!(flags & kCGEventFlagMaskControl); - platform->is_key_down[obsKey] = (event.type == NSEventTypeKeyDown); + switch (type) { + case kCGEventKeyDown: { + const int64_t keycode = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); + platform->is_key_down[obs_key_from_virtual_key(keycode)] = true; + break; + } + case kCGEventKeyUp: { + const int64_t keycode = CGEventGetIntegerValueField(event, kCGKeyboardEventKeycode); + platform->is_key_down[obs_key_from_virtual_key(keycode)] = false; + break; + } + case kCGEventFlagsChanged: { + break; + } + case kCGEventTapDisabledByTimeout: { + blog(LOG_DEBUG, "[hotkeys-cocoa]: Hotkey event tap disabled by timeout. Reenabling..."); + CGEventTapEnable(platform->eventTap, true); + break; + } + default: { + blog(LOG_WARNING, "[hotkeys-cocoa]: Received unexpected event with code '%d'", type); + } } + return event; } static void InputMethodChangedProc(CFNotificationCenterRef center __unused, void *observer, @@ -577,19 +592,26 @@ bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys) obs_hotkeys_platform_t *platform = bzalloc(sizeof(obs_hotkeys_platform_t)); - platform->monitor = - (__bridge CFTypeRef)([NSEvent addGlobalMonitorForEventsMatchingMask:NSEventMaskKeyUp | NSEventMaskKeyDown - handler:^(NSEvent *_Nonnull event) { - MonitorEventHandlerProc(platform, event); - }]); + const bool has_event_access = CGPreflightListenEventAccess(); + if (has_event_access) { + platform->eventTap = CGEventTapCreate(kCGHIDEventTap, kCGHeadInsertEventTap, kCGEventTapOptionListenOnly, + CGEventMaskBit(kCGEventKeyDown) | CGEventMaskBit(kCGEventKeyUp) | + CGEventMaskBit(kCGEventFlagsChanged), + KeyboardEventProc, platform); + if (!platform->eventTap) { + blog(LOG_WARNING, "[hotkeys-cocoa]: Couldn't create hotkey event tap."); + hotkeys_release(platform); + platform = NULL; + return false; + } + CFRunLoopSourceRef source = CFMachPortCreateRunLoopSource(kCFAllocatorDefault, platform->eventTap, 0); + CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes); + CFRelease(source); - platform->local_monitor = (__bridge CFTypeRef)([NSEvent - addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp | NSEventMaskKeyDown - handler:^NSEvent *_Nullable(NSEvent *_Nonnull event) { - MonitorEventHandlerProc(platform, event); - - return event; - }]); + CGEventTapEnable(platform->eventTap, true); + } else { + blog(LOG_WARNING, "[hotkeys-cocoa]: No event permissions, could not add global hotkeys."); + } platform->tis = TISCopyCurrentKeyboardLayoutInputSource(); platform->layout_data = (CFDataRef) TISGetInputSourceProperty(platform->tis, kTISPropertyUnicodeKeyLayoutData);