0
0
mirror of https://github.com/obsproject/obs-studio.git synced 2024-09-20 04:42:18 +02:00
obs-studio/libobs/obs-cocoa.m
PatTheMav 4dac84999d libobs: Update macOS platform implementation
Cleans up the source code by sorting functions by topic and also
removes macros for hotkey implementation, removing the need to disable
compiler warnings.

Key codes and descriptions are now contained in arrays which either
use the OBS key code or the macOS key code as lookup keys with all
necessary context information directly associated with the key.

The implementation also uses the zero-initialization rule for arrays
of structs in such a way that the struct's "is_valid" boolean is
"false" by default so that only values explicitly initialized with a
key are ever "valid".
2024-09-12 16:44:39 -04:00

826 lines
33 KiB
Objective-C

/******************************************************************************
Copyright (C) 2013 by Ruwen Hahn <palana@stunned.de>
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
******************************************************************************/
#include "util/dstr.h"
#include "obs.h"
#include "obs-internal.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/sysctl.h>
#include <Carbon/Carbon.h>
#include <IOKit/hid/IOHIDDevice.h>
#include <IOKit/hid/IOHIDManager.h>
#import <AppKit/AppKit.h>
// MARK: macOS Bundle Management
bool is_in_bundle()
{
NSRunningApplication *app = [NSRunningApplication currentApplication];
return [app bundleIdentifier] != nil;
}
const char *get_module_extension(void)
{
return "";
}
void add_default_module_paths(void)
{
NSURL *pluginURL = [[NSBundle mainBundle] builtInPlugInsURL];
NSString *pluginModulePath = [[pluginURL path] stringByAppendingString:@"/%module%.plugin/Contents/MacOS/"];
NSString *pluginDataPath = [[pluginURL path] stringByAppendingString:@"/%module%.plugin/Contents/Resources/"];
obs_add_module_path(pluginModulePath.UTF8String, pluginDataPath.UTF8String);
}
char *find_libobs_data_file(const char *file)
{
NSBundle *frameworkBundle = [NSBundle bundleWithIdentifier:@"com.obsproject.libobs"];
NSString *libobsDataPath =
[[[frameworkBundle bundleURL] path] stringByAppendingFormat:@"/%@/%s", @"Resources", file];
size_t path_length = strlen(libobsDataPath.UTF8String);
char *path = bmalloc(path_length + 1);
snprintf(path, (path_length + 1), "%s", libobsDataPath.UTF8String);
return path;
}
// MARK: - macOS Hardware Info Helpers
static void log_processor_name(void)
{
char *name = NULL;
size_t size;
int ret;
ret = sysctlbyname("machdep.cpu.brand_string", NULL, &size, NULL, 0);
if (ret != 0)
return;
name = bmalloc(size);
ret = sysctlbyname("machdep.cpu.brand_string", name, &size, NULL, 0);
if (ret == 0)
blog(LOG_INFO, "CPU Name: %s", name);
bfree(name);
}
static void log_processor_speed(void)
{
size_t size;
long long freq;
int ret;
size = sizeof(freq);
ret = sysctlbyname("hw.cpufrequency", &freq, &size, NULL, 0);
if (ret == 0)
blog(LOG_INFO, "CPU Speed: %lldMHz", freq / 1000000);
}
static void log_model_name(void)
{
char *name = NULL;
size_t size;
int ret;
ret = sysctlbyname("hw.model", NULL, &size, NULL, 0);
if (ret != 0)
return;
name = bmalloc(size);
ret = sysctlbyname("hw.model", name, &size, NULL, 0);
if (ret == 0)
blog(LOG_INFO, "Model Identifier: %s", name);
bfree(name);
}
static void log_processor_cores(void)
{
blog(LOG_INFO, "Physical Cores: %d, Logical Cores: %d", os_get_physical_cores(), os_get_logical_cores());
}
static void log_emulation_status(void)
{
blog(LOG_INFO, "Rosetta translation used: %s", os_get_emulation_status() ? "true" : "false");
}
static void log_available_memory(void)
{
size_t size;
long long memory_available;
int ret;
size = sizeof(memory_available);
ret = sysctlbyname("hw.memsize", &memory_available, &size, NULL, 0);
if (ret == 0)
blog(LOG_INFO, "Physical Memory: %lldMB Total", memory_available / 1024 / 1024);
}
static void log_os(void)
{
NSProcessInfo *pi = [NSProcessInfo processInfo];
blog(LOG_INFO, "OS Name: macOS");
blog(LOG_INFO, "OS Version: %s", [[pi operatingSystemVersionString] UTF8String]);
}
static void log_kernel_version(void)
{
char kernel_version[1024];
size_t size = sizeof(kernel_version);
int ret;
ret = sysctlbyname("kern.osrelease", kernel_version, &size, NULL, 0);
if (ret == 0)
blog(LOG_INFO, "Kernel Version: %s", kernel_version);
}
void log_system_info(void)
{
log_processor_name();
log_processor_speed();
log_processor_cores();
log_available_memory();
log_model_name();
log_os();
log_emulation_status();
log_kernel_version();
}
// MARK: - Type Conversion Utilities
static bool dstr_from_cfstring(struct dstr *str, CFStringRef ref)
{
CFIndex length = CFStringGetLength(ref);
CFIndex max_size = CFStringGetMaximumSizeForEncoding(length, kCFStringEncodingUTF8);
dstr_reserve(str, max_size);
if (!CFStringGetCString(ref, str->array, max_size, kCFStringEncodingUTF8))
return false;
str->len = strlen(str->array);
return true;
}
// MARK: - Graphics Thread Wrappers
void *obs_graphics_thread_autorelease(void *param)
{
@autoreleasepool {
return obs_graphics_thread(param);
}
}
bool obs_graphics_thread_loop_autorelease(struct obs_graphics_context *context)
{
@autoreleasepool {
return obs_graphics_thread_loop(context);
}
}
// MARK: - macOS Hotkey Management
typedef struct obs_key_code {
int code;
bool is_valid;
} obs_key_code_t;
typedef struct macOS_glyph_desc {
UniChar glyph;
char *desc;
bool is_glyph;
bool is_valid;
} macOS_glyph_desc_t;
typedef struct obs_key_desc {
char *desc;
bool is_valid;
} obs_key_desc_t;
static int INVALID_KEY = 0xFF;
/* clang-format off */
static const obs_key_code_t virtual_keys[OBS_KEY_LAST_VALUE] = {
[OBS_KEY_A] = {.code = kVK_ANSI_A, .is_valid = true},
[OBS_KEY_B] = {.code = kVK_ANSI_B, .is_valid = true},
[OBS_KEY_C] = {.code = kVK_ANSI_C, .is_valid = true},
[OBS_KEY_D] = {.code = kVK_ANSI_D, .is_valid = true},
[OBS_KEY_E] = {.code = kVK_ANSI_E, .is_valid = true},
[OBS_KEY_F] = {.code = kVK_ANSI_F, .is_valid = true},
[OBS_KEY_G] = {.code = kVK_ANSI_G, .is_valid = true},
[OBS_KEY_H] = {.code = kVK_ANSI_H, .is_valid = true},
[OBS_KEY_I] = {.code = kVK_ANSI_I, .is_valid = true},
[OBS_KEY_J] = {.code = kVK_ANSI_J, .is_valid = true},
[OBS_KEY_K] = {.code = kVK_ANSI_K, .is_valid = true},
[OBS_KEY_L] = {.code = kVK_ANSI_L, .is_valid = true},
[OBS_KEY_M] = {.code = kVK_ANSI_M, .is_valid = true},
[OBS_KEY_N] = {.code = kVK_ANSI_N, .is_valid = true},
[OBS_KEY_O] = {.code = kVK_ANSI_O, .is_valid = true},
[OBS_KEY_P] = {.code = kVK_ANSI_P, .is_valid = true},
[OBS_KEY_Q] = {.code = kVK_ANSI_Q, .is_valid = true},
[OBS_KEY_R] = {.code = kVK_ANSI_R, .is_valid = true},
[OBS_KEY_S] = {.code = kVK_ANSI_S, .is_valid = true},
[OBS_KEY_T] = {.code = kVK_ANSI_T, .is_valid = true},
[OBS_KEY_U] = {.code = kVK_ANSI_U, .is_valid = true},
[OBS_KEY_V] = {.code = kVK_ANSI_V, .is_valid = true},
[OBS_KEY_W] = {.code = kVK_ANSI_W, .is_valid = true},
[OBS_KEY_X] = {.code = kVK_ANSI_X, .is_valid = true},
[OBS_KEY_Y] = {.code = kVK_ANSI_Y, .is_valid = true},
[OBS_KEY_Z] = {.code = kVK_ANSI_Z, .is_valid = true},
[OBS_KEY_1] = {.code = kVK_ANSI_1, .is_valid = true},
[OBS_KEY_2] = {.code = kVK_ANSI_2, .is_valid = true},
[OBS_KEY_3] = {.code = kVK_ANSI_3, .is_valid = true},
[OBS_KEY_4] = {.code = kVK_ANSI_4, .is_valid = true},
[OBS_KEY_5] = {.code = kVK_ANSI_5, .is_valid = true},
[OBS_KEY_6] = {.code = kVK_ANSI_6, .is_valid = true},
[OBS_KEY_7] = {.code = kVK_ANSI_7, .is_valid = true},
[OBS_KEY_8] = {.code = kVK_ANSI_8, .is_valid = true},
[OBS_KEY_9] = {.code = kVK_ANSI_9, .is_valid = true},
[OBS_KEY_0] = {.code = kVK_ANSI_0, .is_valid = true},
[OBS_KEY_RETURN] = {.code = kVK_Return, .is_valid = true},
[OBS_KEY_ESCAPE] = {.code = kVK_Escape, .is_valid = true},
[OBS_KEY_BACKSPACE] = {.code = kVK_Delete, .is_valid = true},
[OBS_KEY_TAB] = {.code = kVK_Tab, .is_valid = true},
[OBS_KEY_SPACE] = {.code = kVK_Space, .is_valid = true},
[OBS_KEY_MINUS] = {.code = kVK_ANSI_Minus, .is_valid = true},
[OBS_KEY_EQUAL] = {.code = kVK_ANSI_Equal, .is_valid = true},
[OBS_KEY_BRACKETLEFT] = {.code = kVK_ANSI_LeftBracket, .is_valid = true},
[OBS_KEY_BRACKETRIGHT] = {.code = kVK_ANSI_RightBracket, .is_valid = true},
[OBS_KEY_BACKSLASH] = {.code = kVK_ANSI_Backslash, .is_valid = true},
[OBS_KEY_SEMICOLON] = {.code = kVK_ANSI_Semicolon, .is_valid = true},
[OBS_KEY_QUOTE] = {.code = kVK_ANSI_Quote, .is_valid = true},
[OBS_KEY_DEAD_GRAVE] = {.code = kVK_ANSI_Grave, .is_valid = true},
[OBS_KEY_COMMA] = {.code = kVK_ANSI_Comma, .is_valid = true},
[OBS_KEY_PERIOD] = {.code = kVK_ANSI_Period, .is_valid = true},
[OBS_KEY_SLASH] = {.code = kVK_ANSI_Slash, .is_valid = true},
[OBS_KEY_CAPSLOCK] = {.code = kVK_CapsLock, .is_valid = true},
[OBS_KEY_SECTION] = {.code = kVK_ISO_Section, .is_valid = true},
[OBS_KEY_F1] = {.code = kVK_F1, .is_valid = true},
[OBS_KEY_F2] = {.code = kVK_F2, .is_valid = true},
[OBS_KEY_F3] = {.code = kVK_F3, .is_valid = true},
[OBS_KEY_F4] = {.code = kVK_F4, .is_valid = true},
[OBS_KEY_F5] = {.code = kVK_F5, .is_valid = true},
[OBS_KEY_F6] = {.code = kVK_F6, .is_valid = true},
[OBS_KEY_F7] = {.code = kVK_F7, .is_valid = true},
[OBS_KEY_F8] = {.code = kVK_F8, .is_valid = true},
[OBS_KEY_F9] = {.code = kVK_F9, .is_valid = true},
[OBS_KEY_F10] = {.code = kVK_F10, .is_valid = true},
[OBS_KEY_F11] = {.code = kVK_F11, .is_valid = true},
[OBS_KEY_F12] = {.code = kVK_F12, .is_valid = true},
[OBS_KEY_HELP] = {.code = kVK_Help, .is_valid = true},
[OBS_KEY_HOME] = {.code = kVK_Home, .is_valid = true},
[OBS_KEY_PAGEUP] = {.code = kVK_PageUp, .is_valid = true},
[OBS_KEY_DELETE] = {.code = kVK_ForwardDelete, .is_valid = true},
[OBS_KEY_END] = {.code = kVK_End, .is_valid = true},
[OBS_KEY_PAGEDOWN] = {.code = kVK_PageDown, .is_valid = true},
[OBS_KEY_RIGHT] = {.code = kVK_RightArrow, .is_valid = true},
[OBS_KEY_LEFT] = {.code = kVK_LeftArrow, .is_valid = true},
[OBS_KEY_DOWN] = {.code = kVK_DownArrow, .is_valid = true},
[OBS_KEY_UP] = {.code = kVK_UpArrow, .is_valid = true},
[OBS_KEY_CLEAR] = {.code = kVK_ANSI_KeypadClear, .is_valid = true},
[OBS_KEY_NUMSLASH] = {.code = kVK_ANSI_KeypadDivide, .is_valid = true},
[OBS_KEY_NUMASTERISK] = {.code = kVK_ANSI_KeypadMultiply, .is_valid = true},
[OBS_KEY_NUMMINUS] = {.code = kVK_ANSI_KeypadMinus, .is_valid = true},
[OBS_KEY_NUMPLUS] = {.code = kVK_ANSI_KeypadPlus, .is_valid = true},
[OBS_KEY_ENTER] = {.code = kVK_ANSI_KeypadEnter, .is_valid = true},
[OBS_KEY_NUM1] = {.code = kVK_ANSI_Keypad1, .is_valid = true},
[OBS_KEY_NUM2] = {.code = kVK_ANSI_Keypad2, .is_valid = true},
[OBS_KEY_NUM3] = {.code = kVK_ANSI_Keypad3, .is_valid = true},
[OBS_KEY_NUM4] = {.code = kVK_ANSI_Keypad4, .is_valid = true},
[OBS_KEY_NUM5] = {.code = kVK_ANSI_Keypad5, .is_valid = true},
[OBS_KEY_NUM6] = {.code = kVK_ANSI_Keypad6, .is_valid = true},
[OBS_KEY_NUM7] = {.code = kVK_ANSI_Keypad7, .is_valid = true},
[OBS_KEY_NUM8] = {.code = kVK_ANSI_Keypad8, .is_valid = true},
[OBS_KEY_NUM9] = {.code = kVK_ANSI_Keypad9, .is_valid = true},
[OBS_KEY_NUM0] = {.code = kVK_ANSI_Keypad0, .is_valid = true},
[OBS_KEY_NUMPERIOD] = {.code = kVK_ANSI_KeypadDecimal, .is_valid = true},
[OBS_KEY_NUMEQUAL] = {.code = kVK_ANSI_KeypadEquals, .is_valid = true},
[OBS_KEY_F13] = {.code = kVK_F13, .is_valid = true},
[OBS_KEY_F14] = {.code = kVK_F14, .is_valid = true},
[OBS_KEY_F15] = {.code = kVK_F15, .is_valid = true},
[OBS_KEY_F16] = {.code = kVK_F16, .is_valid = true},
[OBS_KEY_F17] = {.code = kVK_F17, .is_valid = true},
[OBS_KEY_F18] = {.code = kVK_F18, .is_valid = true},
[OBS_KEY_F19] = {.code = kVK_F19, .is_valid = true},
[OBS_KEY_F20] = {.code = kVK_F20, .is_valid = true},
[OBS_KEY_CONTROL] = {.code = kVK_Control, .is_valid = true},
[OBS_KEY_SHIFT] = {.code = kVK_Shift, .is_valid = true},
[OBS_KEY_ALT] = {.code = kVK_Option, .is_valid = true},
[OBS_KEY_META] = {.code = kVK_Command, .is_valid = true},
};
static const obs_key_desc_t key_descriptions[OBS_KEY_LAST_VALUE] = {
[OBS_KEY_SPACE] = {.desc = "Space", .is_valid = true},
[OBS_KEY_NUMEQUAL] = {.desc = "= (Keypad)", .is_valid = true},
[OBS_KEY_NUMASTERISK] = {.desc = "* (Keypad)", .is_valid = true},
[OBS_KEY_NUMPLUS] = {.desc = "+ (Keypad)", .is_valid = true},
[OBS_KEY_NUMMINUS] = {.desc = "- (Keypad)", .is_valid = true},
[OBS_KEY_NUMPERIOD] = {.desc = ". (Keypad)", .is_valid = true},
[OBS_KEY_NUMSLASH] = {.desc = "/ (Keypad)", .is_valid = true},
[OBS_KEY_NUM0] = {.desc = "0 (Keypad)", .is_valid = true},
[OBS_KEY_NUM1] = {.desc = "1 (Keypad)", .is_valid = true},
[OBS_KEY_NUM2] = {.desc = "2 (Keypad)", .is_valid = true},
[OBS_KEY_NUM3] = {.desc = "3 (Keypad)", .is_valid = true},
[OBS_KEY_NUM4] = {.desc = "4 (Keypad)", .is_valid = true},
[OBS_KEY_NUM5] = {.desc = "5 (Keypad)", .is_valid = true},
[OBS_KEY_NUM6] = {.desc = "6 (Keypad)", .is_valid = true},
[OBS_KEY_NUM7] = {.desc = "7 (Keypad)", .is_valid = true},
[OBS_KEY_NUM8] = {.desc = "8 (Keypad)", .is_valid = true},
[OBS_KEY_NUM9] = {.desc = "9 (Keypad)", .is_valid = true},
[OBS_KEY_MOUSE1] = {.desc = "Mouse 1", .is_valid = true},
[OBS_KEY_MOUSE2] = {.desc = "Mouse 2", .is_valid = true},
[OBS_KEY_MOUSE3] = {.desc = "Mouse 3", .is_valid = true},
[OBS_KEY_MOUSE4] = {.desc = "Mouse 4", .is_valid = true},
[OBS_KEY_MOUSE5] = {.desc = "Mouse 5", .is_valid = true},
[OBS_KEY_MOUSE6] = {.desc = "Mouse 6", .is_valid = true},
[OBS_KEY_MOUSE7] = {.desc = "Mouse 7", .is_valid = true},
[OBS_KEY_MOUSE8] = {.desc = "Mouse 8", .is_valid = true},
[OBS_KEY_MOUSE9] = {.desc = "Mouse 9", .is_valid = true},
[OBS_KEY_MOUSE10] = {.desc = "Mouse 10", .is_valid = true},
[OBS_KEY_MOUSE11] = {.desc = "Mouse 11", .is_valid = true},
[OBS_KEY_MOUSE12] = {.desc = "Mouse 12", .is_valid = true},
[OBS_KEY_MOUSE13] = {.desc = "Mouse 13", .is_valid = true},
[OBS_KEY_MOUSE14] = {.desc = "Mouse 14", .is_valid = true},
[OBS_KEY_MOUSE15] = {.desc = "Mouse 15", .is_valid = true},
[OBS_KEY_MOUSE16] = {.desc = "Mouse 16", .is_valid = true},
[OBS_KEY_MOUSE17] = {.desc = "Mouse 17", .is_valid = true},
[OBS_KEY_MOUSE18] = {.desc = "Mouse 18", .is_valid = true},
[OBS_KEY_MOUSE19] = {.desc = "Mouse 19", .is_valid = true},
[OBS_KEY_MOUSE20] = {.desc = "Mouse 20", .is_valid = true},
[OBS_KEY_MOUSE21] = {.desc = "Mouse 21", .is_valid = true},
[OBS_KEY_MOUSE22] = {.desc = "Mouse 22", .is_valid = true},
[OBS_KEY_MOUSE23] = {.desc = "Mouse 23", .is_valid = true},
[OBS_KEY_MOUSE24] = {.desc = "Mouse 24", .is_valid = true},
[OBS_KEY_MOUSE25] = {.desc = "Mouse 25", .is_valid = true},
[OBS_KEY_MOUSE26] = {.desc = "Mouse 26", .is_valid = true},
[OBS_KEY_MOUSE27] = {.desc = "Mouse 27", .is_valid = true},
[OBS_KEY_MOUSE28] = {.desc = "Mouse 28", .is_valid = true},
[OBS_KEY_MOUSE29] = {.desc = "Mouse 29", .is_valid = true},
};
static const macOS_glyph_desc_t key_glyphs[(keyCodeMask >> 8)] = {
[kVK_Return] = {.glyph = 0x21A9, .is_glyph = true, .is_valid = true},
[kVK_Escape] = {.glyph = 0x238B, .is_glyph = true, .is_valid = true},
[kVK_Delete] = {.glyph = 0x232B, .is_glyph = true, .is_valid = true},
[kVK_Tab] = {.glyph = 0x21e5, .is_glyph = true, .is_valid = true},
[kVK_CapsLock] = {.glyph = 0x21EA, .is_glyph = true, .is_valid = true},
[kVK_ANSI_KeypadClear] = {.glyph = 0x2327, .is_glyph = true, .is_valid = true},
[kVK_ANSI_KeypadEnter] = {.glyph = 0x2305, .is_glyph = true, .is_valid = true},
[kVK_Help] = {.glyph = 0x003F, .is_glyph = true, .is_valid = true},
[kVK_Home] = {.glyph = 0x2196, .is_glyph = true, .is_valid = true},
[kVK_PageUp] = {.glyph = 0x21de, .is_glyph = true, .is_valid = true},
[kVK_ForwardDelete] = {.glyph = 0x2326, .is_glyph = true, .is_valid = true},
[kVK_End] = {.glyph = 0x2198, .is_glyph = true, .is_valid = true},
[kVK_PageDown] = {.glyph = 0x21df, .is_glyph = true, .is_valid = true},
[kVK_Control] = {.glyph = kControlUnicode, .is_glyph = true, .is_valid = true},
[kVK_Shift] = {.glyph = kShiftUnicode, .is_glyph = true, .is_valid = true},
[kVK_Option] = {.glyph = kOptionUnicode, .is_glyph = true, .is_valid = true},
[kVK_Command] = {.glyph = kCommandUnicode, .is_glyph = true, .is_valid = true},
[kVK_RightControl] = {.glyph = kControlUnicode, .is_glyph = true, .is_valid = true},
[kVK_RightShift] = {.glyph = kShiftUnicode, .is_glyph = true, .is_valid = true},
[kVK_RightOption] = {.glyph = kOptionUnicode, .is_glyph = true, .is_valid = true},
[kVK_F1] = {.desc = "F1", .is_valid = true},
[kVK_F2] = {.desc = "F2", .is_valid = true},
[kVK_F3] = {.desc = "F3", .is_valid = true},
[kVK_F4] = {.desc = "F4", .is_valid = true},
[kVK_F5] = {.desc = "F5", .is_valid = true},
[kVK_F6] = {.desc = "F6", .is_valid = true},
[kVK_F7] = {.desc = "F7", .is_valid = true},
[kVK_F8] = {.desc = "F8", .is_valid = true},
[kVK_F9] = {.desc = "F9", .is_valid = true},
[kVK_F10] = {.desc = "F10", .is_valid = true},
[kVK_F11] = {.desc = "F11", .is_valid = true},
[kVK_F12] = {.desc = "F12", .is_valid = true},
[kVK_F13] = {.desc = "F13", .is_valid = true},
[kVK_F14] = {.desc = "F14", .is_valid = true},
[kVK_F15] = {.desc = "F15", .is_valid = true},
[kVK_F16] = {.desc = "F16", .is_valid = true},
[kVK_F17] = {.desc = "F17", .is_valid = true},
[kVK_F18] = {.desc = "F18", .is_valid = true},
[kVK_F19] = {.desc = "F19", .is_valid = true},
[kVK_F20] = {.desc = "F20", .is_valid = true}
};
/* clang-format on */
struct obs_hotkeys_platform {
volatile long refs;
CFTypeRef monitor;
CFTypeRef local_monitor;
bool is_key_down[OBS_KEY_LAST_VALUE];
TISInputSourceRef tis;
CFDataRef layout_data;
UCKeyboardLayout *layout;
};
// MARK: macOS Hotkey Implementation
#define OBS_COCOA_MODIFIER_SIZE (int) 7
static char string_control[OBS_COCOA_MODIFIER_SIZE];
static char string_option[OBS_COCOA_MODIFIER_SIZE];
static char string_shift[OBS_COCOA_MODIFIER_SIZE];
static char string_command[OBS_COCOA_MODIFIER_SIZE];
static dispatch_once_t onceToken;
static void hotkeys_retain(obs_hotkeys_platform_t *platform)
{
os_atomic_inc_long(&platform->refs);
}
static void hotkeys_release(obs_hotkeys_platform_t *platform)
{
if (os_atomic_dec_long(&platform->refs) == -1) {
if (platform->tis) {
CFRelease(platform->tis);
platform->tis = NULL;
}
if (platform->layout_data) {
CFRelease(platform->layout_data);
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;
}
bfree(platform);
}
}
static bool obs_key_to_localized_string(obs_key_t key, struct dstr *str)
{
if (key < OBS_KEY_LAST_VALUE && !key_descriptions[key].is_valid) {
return false;
}
dstr_copy(str, obs_get_hotkey_translation(key, key_descriptions[key].desc));
return true;
}
static bool key_code_to_string(int code, struct dstr *str)
{
if (code < INVALID_KEY) {
macOS_glyph_desc_t glyph = key_glyphs[code];
if (glyph.is_valid && glyph.is_glyph && glyph.glyph > 0) {
dstr_from_wcs(str, (wchar_t[]) {glyph.glyph, 0});
} else if (glyph.is_valid && glyph.desc) {
dstr_copy(str, glyph.desc);
} else {
return false;
}
}
return true;
}
static bool log_layout_name(TISInputSourceRef tis)
{
struct dstr layout_name = {0};
CFStringRef sid = (CFStringRef) TISGetInputSourceProperty(tis, kTISPropertyInputSourceID);
if (!sid) {
blog(LOG_ERROR, "hotkeys-cocoa: Unable to get input source ID");
return false;
}
if (!dstr_from_cfstring(&layout_name, sid)) {
blog(LOG_ERROR, "hotkeys-cocoa: Unable to convert input source ID");
dstr_free(&layout_name);
return false;
}
blog(LOG_INFO, "hotkeys-cocoa: Using keyboard layout '%s'", layout_name.array);
dstr_free(&layout_name);
return true;
}
// MARK: macOS Hotkey CoreFoundation Callbacks
static void MonitorEventHandlerProc(obs_hotkeys_platform_t *platform, NSEvent *event)
{
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);
if (event.type == NSEventTypeKeyDown || event.type == NSEventTypeKeyUp) {
obs_key_t obsKey = obs_key_from_virtual_key(event.keyCode);
platform->is_key_down[obsKey] = (event.type == NSEventTypeKeyDown);
}
}
static void InputMethodChangedProc(CFNotificationCenterRef center __unused, void *observer,
CFNotificationName name __unused, const void *object __unused,
CFDictionaryRef userInfo __unused)
{
struct obs_core_hotkeys *hotkeys = observer;
obs_hotkeys_platform_t *platform = hotkeys->platform_context;
pthread_mutex_lock(&hotkeys->mutex);
if (platform->layout_data) {
CFRelease(platform->layout_data);
}
platform->tis = TISCopyCurrentKeyboardLayoutInputSource();
platform->layout_data = (CFDataRef) TISGetInputSourceProperty(platform->tis, kTISPropertyUnicodeKeyLayoutData);
if (!platform->layout_data) {
blog(LOG_ERROR, "hotkeys-cocoa: Failed to retrieve keyboard layout data");
hotkeys->platform_context = NULL;
pthread_mutex_unlock(&hotkeys->mutex);
hotkeys_release(platform);
return;
}
CFRetain(platform->layout_data);
platform->layout = (UCKeyboardLayout *) CFDataGetBytePtr(platform->layout_data);
}
// MARK: macOS Hotkey API Implementation
bool obs_hotkeys_platform_init(struct obs_core_hotkeys *hotkeys)
{
CFNotificationCenterAddObserver(CFNotificationCenterGetDistributedCenter(), hotkeys, InputMethodChangedProc,
kTISNotifySelectedKeyboardInputSourceChanged, NULL,
CFNotificationSuspensionBehaviorDeliverImmediately);
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);
}]);
platform->local_monitor = (__bridge CFTypeRef)([NSEvent
addLocalMonitorForEventsMatchingMask:NSEventMaskKeyUp | NSEventMaskKeyDown
handler:^NSEvent *_Nullable(NSEvent *_Nonnull event) {
MonitorEventHandlerProc(platform, event);
return event;
}]);
platform->tis = TISCopyCurrentKeyboardLayoutInputSource();
platform->layout_data = (CFDataRef) TISGetInputSourceProperty(platform->tis, kTISPropertyUnicodeKeyLayoutData);
if (!platform->layout_data) {
blog(LOG_ERROR, "hotkeys-cocoa: Failed to retrieve keyboard layout data");
hotkeys_release(platform);
platform = NULL;
return false;
}
CFRetain(platform->layout_data);
platform->layout = (UCKeyboardLayout *) CFDataGetBytePtr(platform->layout_data);
obs_hotkeys_platform_t *currentPlatform;
pthread_mutex_lock(&hotkeys->mutex);
currentPlatform = hotkeys->platform_context;
if (platform && currentPlatform && platform->layout_data == currentPlatform->layout_data) {
pthread_mutex_unlock(&hotkeys->mutex);
hotkeys_release(platform);
return true;
}
hotkeys->platform_context = platform;
log_layout_name(platform->tis);
pthread_mutex_unlock(&hotkeys->mutex);
calldata_t parameters = {0};
signal_handler_signal(hotkeys->signals, "hotkey_layout_change", &parameters);
if (currentPlatform) {
hotkeys_release(currentPlatform);
}
bool hasPlatformContext = hotkeys->platform_context != NULL;
return hasPlatformContext;
}
void obs_hotkeys_platform_free(struct obs_core_hotkeys *hotkeys)
{
CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDistributedCenter(), hotkeys);
hotkeys_release(hotkeys->platform_context);
}
int obs_key_to_virtual_key(obs_key_t key)
{
if (virtual_keys[key].is_valid) {
return virtual_keys[key].code;
} else {
return INVALID_KEY;
}
}
obs_key_t obs_key_from_virtual_key(int keyCode)
{
for (size_t i = 0; i < OBS_KEY_LAST_VALUE; i++) {
if (virtual_keys[i].is_valid && virtual_keys[i].code == keyCode) {
return (obs_key_t) i;
}
}
return OBS_KEY_NONE;
}
bool obs_hotkeys_platform_is_pressed(obs_hotkeys_platform_t *platform, obs_key_t key)
{
if (key >= OBS_KEY_MOUSE1 && key <= OBS_KEY_MOUSE29) {
int button = key - 1;
NSUInteger buttons = [NSEvent pressedMouseButtons];
if ((buttons & (1 << button)) != 0) {
return true;
} else {
return false;
}
}
if (!platform) {
return false;
}
if (key >= OBS_KEY_LAST_VALUE) {
return false;
}
return platform->is_key_down[key];
}
static void unichar_to_utf8(const UniChar *character, char *buffer)
{
CFStringRef string = CFStringCreateWithCharactersNoCopy(NULL, character, 2, kCFAllocatorNull);
if (!string) {
blog(LOG_ERROR, "hotkey-cocoa: Unable to create CFStringRef with UniChar character");
return;
}
if (!CFStringGetCString(string, buffer, OBS_COCOA_MODIFIER_SIZE, kCFStringEncodingUTF8)) {
blog(LOG_ERROR, "hotkey-cocoa: Error while populating buffer with glyph %d (0x%x)", character[0], character[0]);
}
CFRelease(string);
}
void obs_key_combination_to_str(obs_key_combination_t key, struct dstr *str)
{
struct dstr keyString = {0};
if (key.key != OBS_KEY_NONE) {
obs_key_to_str(key.key, &keyString);
}
dispatch_once(&onceToken, ^{
const UniChar controlCharacter[] = {kControlUnicode, 0};
const UniChar optionCharacter[] = {kOptionUnicode, 0};
const UniChar shiftCharacter[] = {kShiftUnicode, 0};
const UniChar commandCharacter[] = {kCommandUnicode, 0};
unichar_to_utf8(controlCharacter, string_control);
unichar_to_utf8(optionCharacter, string_option);
unichar_to_utf8(shiftCharacter, string_shift);
unichar_to_utf8(commandCharacter, string_command);
});
const char *modifier_control = (key.modifiers & INTERACT_CONTROL_KEY) ? string_control : "";
const char *modifier_option = (key.modifiers & INTERACT_ALT_KEY) ? string_option : "";
const char *modifier_shift = (key.modifiers & INTERACT_SHIFT_KEY) ? string_shift : "";
const char *modifier_command = (key.modifiers & INTERACT_COMMAND_KEY) ? string_command : "";
const char *modifier_key = keyString.len ? keyString.array : "";
dstr_printf(str, "%s%s%s%s%s", modifier_control, modifier_option, modifier_shift, modifier_command, modifier_key);
dstr_free(&keyString);
}
void obs_key_to_str(obs_key_t key, struct dstr *str)
{
const UniCharCount maxLength = 16;
UniChar buffer[16];
if (obs_key_to_localized_string(key, str)) {
return;
}
int code = obs_key_to_virtual_key(key);
if (key_code_to_string(code, str)) {
return;
}
if (code == INVALID_KEY) {
const char *keyName = obs_key_to_name(key);
blog(LOG_ERROR, "hotkey-cocoa: Got invalid key while translating '%d' (%s)", key, keyName);
dstr_copy(str, keyName);
return;
}
struct obs_hotkeys_platform *platform = NULL;
if (obs) {
pthread_mutex_lock(&obs->hotkeys.mutex);
platform = obs->hotkeys.platform_context;
hotkeys_retain(platform);
pthread_mutex_unlock(&obs->hotkeys.mutex);
}
if (!platform) {
const char *keyName = obs_key_to_name(key);
blog(LOG_ERROR, "hotkey-cocoa: Could not get hotkey platform while translating '%d' (%s)", key, keyName);
dstr_copy(str, keyName);
return;
}
UInt32 deadKeyState = 0;
UniCharCount length = 0;
OSStatus err = UCKeyTranslate(platform->layout, code, kUCKeyActionDown, ((alphaLock >> 8) & 0xFF), LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit, &deadKeyState, maxLength, &length, buffer);
if (err == noErr && length <= 0 && deadKeyState) {
err = UCKeyTranslate(platform->layout, kVK_Space, kUCKeyActionDown, ((alphaLock >> 8) & 0xFF), LMGetKbdType(),
kUCKeyTranslateNoDeadKeysBit, &deadKeyState, maxLength, &length, buffer);
}
hotkeys_release(platform);
if (err != noErr) {
const char *keyName = obs_key_to_name(key);
blog(LOG_ERROR, "hotkey-cocoa: Error while translating '%d' (0x%x, %s) to string: %d", key, code, keyName, err);
dstr_copy(str, keyName);
return;
}
if (length == 0) {
const char *keyName = obs_key_to_name(key);
blog(LOG_ERROR, "hotkey-cocoa: Got zero length string while translating '%d' (0x%x, %s) to string", key, code,
keyName);
dstr_copy(str, keyName);
return;
}
CFStringRef string = CFStringCreateWithCharactersNoCopy(NULL, buffer, length, kCFAllocatorNull);
if (!string) {
const char *keyName = obs_key_to_name(key);
blog(LOG_ERROR, "hotkey-cocoa: Could not create CFStringRef while translating '%d' (0x%x, %s) to string", key,
code, keyName);
dstr_copy(str, keyName);
return;
}
if (!dstr_from_cfstring(str, string)) {
const char *keyName = obs_key_to_name(key);
blog(LOG_ERROR, "hotkey-cocoa: Could not translate CFStringRef to CString while translating '%d' (0x%x, %s)",
key, code, keyName);
CFRelease(string);
dstr_copy(str, keyName);
return;
}
CFRelease(string);
return;
}