| //======================================================================== |
| // GLFW 3.4 Cocoa - www.glfw.org |
| //------------------------------------------------------------------------ |
| // Copyright (c) 2009-2019 Camilla Löwy <elmindreda@glfw.org> |
| // Copyright (c) 2012 Torsten Walluhn <tw@mad-cad.net> |
| // |
| // This software is provided 'as-is', without any express or implied |
| // warranty. In no event will the authors be held liable for any damages |
| // arising from the use of this software. |
| // |
| // Permission is granted to anyone to use this software for any purpose, |
| // including commercial applications, and to alter it and redistribute it |
| // freely, subject to the following restrictions: |
| // |
| // 1. The origin of this software must not be misrepresented; you must not |
| // claim that you wrote the original software. If you use this software |
| // in a product, an acknowledgment in the product documentation would |
| // be appreciated but is not required. |
| // |
| // 2. Altered source versions must be plainly marked as such, and must not |
| // be misrepresented as being the original software. |
| // |
| // 3. This notice may not be removed or altered from any source |
| // distribution. |
| // |
| //======================================================================== |
| |
| #include "internal.h" |
| |
| #if defined(_GLFW_COCOA) |
| |
| #include <unistd.h> |
| #include <ctype.h> |
| #include <string.h> |
| |
| #include <mach/mach.h> |
| #include <mach/mach_error.h> |
| |
| #include <CoreFoundation/CoreFoundation.h> |
| #include <Kernel/IOKit/hidsystem/IOHIDUsageTables.h> |
| |
| |
| // Joystick element information |
| // |
| typedef struct _GLFWjoyelementNS |
| { |
| IOHIDElementRef native; |
| uint32_t usage; |
| int index; |
| long minimum; |
| long maximum; |
| |
| } _GLFWjoyelementNS; |
| |
| |
| // Returns the value of the specified element of the specified joystick |
| // |
| static long getElementValue(_GLFWjoystick* js, _GLFWjoyelementNS* element) |
| { |
| IOHIDValueRef valueRef; |
| long value = 0; |
| |
| if (js->ns.device) |
| { |
| if (IOHIDDeviceGetValue(js->ns.device, |
| element->native, |
| &valueRef) == kIOReturnSuccess) |
| { |
| value = IOHIDValueGetIntegerValue(valueRef); |
| } |
| } |
| |
| return value; |
| } |
| |
| // Comparison function for matching the SDL element order |
| // |
| static CFComparisonResult compareElements(const void* fp, |
| const void* sp, |
| void* user) |
| { |
| const _GLFWjoyelementNS* fe = fp; |
| const _GLFWjoyelementNS* se = sp; |
| if (fe->usage < se->usage) |
| return kCFCompareLessThan; |
| if (fe->usage > se->usage) |
| return kCFCompareGreaterThan; |
| if (fe->index < se->index) |
| return kCFCompareLessThan; |
| if (fe->index > se->index) |
| return kCFCompareGreaterThan; |
| return kCFCompareEqualTo; |
| } |
| |
| // Removes the specified joystick |
| // |
| static void closeJoystick(_GLFWjoystick* js) |
| { |
| _glfwInputJoystick(js, GLFW_DISCONNECTED); |
| |
| for (int i = 0; i < CFArrayGetCount(js->ns.axes); i++) |
| _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.axes, i)); |
| CFRelease(js->ns.axes); |
| |
| for (int i = 0; i < CFArrayGetCount(js->ns.buttons); i++) |
| _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.buttons, i)); |
| CFRelease(js->ns.buttons); |
| |
| for (int i = 0; i < CFArrayGetCount(js->ns.hats); i++) |
| _glfw_free((void*) CFArrayGetValueAtIndex(js->ns.hats, i)); |
| CFRelease(js->ns.hats); |
| |
| _glfwFreeJoystick(js); |
| } |
| |
| // Callback for user-initiated joystick addition |
| // |
| static void matchCallback(void* context, |
| IOReturn result, |
| void* sender, |
| IOHIDDeviceRef device) |
| { |
| int jid; |
| char name[256]; |
| char guid[33]; |
| CFTypeRef property; |
| uint32_t vendor = 0, product = 0, version = 0; |
| _GLFWjoystick* js; |
| CFMutableArrayRef axes, buttons, hats; |
| |
| for (jid = 0; jid <= GLFW_JOYSTICK_LAST; jid++) |
| { |
| if (_glfw.joysticks[jid].ns.device == device) |
| return; |
| } |
| |
| CFArrayRef elements = |
| IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone); |
| |
| // It is reportedly possible for this to fail on macOS 13 Ventura |
| // if the application does not have input monitoring permissions |
| if (!elements) |
| return; |
| |
| axes = CFArrayCreateMutable(NULL, 0, NULL); |
| buttons = CFArrayCreateMutable(NULL, 0, NULL); |
| hats = CFArrayCreateMutable(NULL, 0, NULL); |
| |
| property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductKey)); |
| if (property) |
| { |
| CFStringGetCString(property, |
| name, |
| sizeof(name), |
| kCFStringEncodingUTF8); |
| } |
| else |
| strncpy(name, "Unknown", sizeof(name)); |
| |
| property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVendorIDKey)); |
| if (property) |
| CFNumberGetValue(property, kCFNumberSInt32Type, &vendor); |
| |
| property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDProductIDKey)); |
| if (property) |
| CFNumberGetValue(property, kCFNumberSInt32Type, &product); |
| |
| property = IOHIDDeviceGetProperty(device, CFSTR(kIOHIDVersionNumberKey)); |
| if (property) |
| CFNumberGetValue(property, kCFNumberSInt32Type, &version); |
| |
| // Generate a joystick GUID that matches the SDL 2.0.5+ one |
| if (vendor && product) |
| { |
| sprintf(guid, "03000000%02x%02x0000%02x%02x0000%02x%02x0000", |
| (uint8_t) vendor, (uint8_t) (vendor >> 8), |
| (uint8_t) product, (uint8_t) (product >> 8), |
| (uint8_t) version, (uint8_t) (version >> 8)); |
| } |
| else |
| { |
| sprintf(guid, "05000000%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x%02x00", |
| name[0], name[1], name[2], name[3], |
| name[4], name[5], name[6], name[7], |
| name[8], name[9], name[10]); |
| } |
| |
| for (CFIndex i = 0; i < CFArrayGetCount(elements); i++) |
| { |
| IOHIDElementRef native = (IOHIDElementRef) |
| CFArrayGetValueAtIndex(elements, i); |
| if (CFGetTypeID(native) != IOHIDElementGetTypeID()) |
| continue; |
| |
| const IOHIDElementType type = IOHIDElementGetType(native); |
| if ((type != kIOHIDElementTypeInput_Axis) && |
| (type != kIOHIDElementTypeInput_Button) && |
| (type != kIOHIDElementTypeInput_Misc)) |
| { |
| continue; |
| } |
| |
| CFMutableArrayRef target = NULL; |
| |
| const uint32_t usage = IOHIDElementGetUsage(native); |
| const uint32_t page = IOHIDElementGetUsagePage(native); |
| if (page == kHIDPage_GenericDesktop) |
| { |
| switch (usage) |
| { |
| case kHIDUsage_GD_X: |
| case kHIDUsage_GD_Y: |
| case kHIDUsage_GD_Z: |
| case kHIDUsage_GD_Rx: |
| case kHIDUsage_GD_Ry: |
| case kHIDUsage_GD_Rz: |
| case kHIDUsage_GD_Slider: |
| case kHIDUsage_GD_Dial: |
| case kHIDUsage_GD_Wheel: |
| target = axes; |
| break; |
| case kHIDUsage_GD_Hatswitch: |
| target = hats; |
| break; |
| case kHIDUsage_GD_DPadUp: |
| case kHIDUsage_GD_DPadRight: |
| case kHIDUsage_GD_DPadDown: |
| case kHIDUsage_GD_DPadLeft: |
| case kHIDUsage_GD_SystemMainMenu: |
| case kHIDUsage_GD_Select: |
| case kHIDUsage_GD_Start: |
| target = buttons; |
| break; |
| } |
| } |
| else if (page == kHIDPage_Simulation) |
| { |
| switch (usage) |
| { |
| case kHIDUsage_Sim_Accelerator: |
| case kHIDUsage_Sim_Brake: |
| case kHIDUsage_Sim_Throttle: |
| case kHIDUsage_Sim_Rudder: |
| case kHIDUsage_Sim_Steering: |
| target = axes; |
| break; |
| } |
| } |
| else if (page == kHIDPage_Button || page == kHIDPage_Consumer) |
| target = buttons; |
| |
| if (target) |
| { |
| _GLFWjoyelementNS* element = _glfw_calloc(1, sizeof(_GLFWjoyelementNS)); |
| element->native = native; |
| element->usage = usage; |
| element->index = (int) CFArrayGetCount(target); |
| element->minimum = IOHIDElementGetLogicalMin(native); |
| element->maximum = IOHIDElementGetLogicalMax(native); |
| CFArrayAppendValue(target, element); |
| } |
| } |
| |
| CFRelease(elements); |
| |
| CFArraySortValues(axes, CFRangeMake(0, CFArrayGetCount(axes)), |
| compareElements, NULL); |
| CFArraySortValues(buttons, CFRangeMake(0, CFArrayGetCount(buttons)), |
| compareElements, NULL); |
| CFArraySortValues(hats, CFRangeMake(0, CFArrayGetCount(hats)), |
| compareElements, NULL); |
| |
| js = _glfwAllocJoystick(name, guid, |
| (int) CFArrayGetCount(axes), |
| (int) CFArrayGetCount(buttons), |
| (int) CFArrayGetCount(hats)); |
| |
| js->ns.device = device; |
| js->ns.axes = axes; |
| js->ns.buttons = buttons; |
| js->ns.hats = hats; |
| |
| _glfwInputJoystick(js, GLFW_CONNECTED); |
| } |
| |
| // Callback for user-initiated joystick removal |
| // |
| static void removeCallback(void* context, |
| IOReturn result, |
| void* sender, |
| IOHIDDeviceRef device) |
| { |
| for (int jid = 0; jid <= GLFW_JOYSTICK_LAST; jid++) |
| { |
| if (_glfw.joysticks[jid].connected && _glfw.joysticks[jid].ns.device == device) |
| { |
| closeJoystick(&_glfw.joysticks[jid]); |
| break; |
| } |
| } |
| } |
| |
| |
| ////////////////////////////////////////////////////////////////////////// |
| ////// GLFW platform API ////// |
| ////////////////////////////////////////////////////////////////////////// |
| |
| GLFWbool _glfwInitJoysticksCocoa(void) |
| { |
| CFMutableArrayRef matching; |
| const long usages[] = |
| { |
| kHIDUsage_GD_Joystick, |
| kHIDUsage_GD_GamePad, |
| kHIDUsage_GD_MultiAxisController |
| }; |
| |
| _glfw.ns.hidManager = IOHIDManagerCreate(kCFAllocatorDefault, |
| kIOHIDOptionsTypeNone); |
| |
| matching = CFArrayCreateMutable(kCFAllocatorDefault, |
| 0, |
| &kCFTypeArrayCallBacks); |
| if (!matching) |
| { |
| _glfwInputError(GLFW_PLATFORM_ERROR, "Cocoa: Failed to create array"); |
| return GLFW_FALSE; |
| } |
| |
| for (size_t i = 0; i < sizeof(usages) / sizeof(long); i++) |
| { |
| const long page = kHIDPage_GenericDesktop; |
| |
| CFMutableDictionaryRef dict = |
| CFDictionaryCreateMutable(kCFAllocatorDefault, |
| 0, |
| &kCFTypeDictionaryKeyCallBacks, |
| &kCFTypeDictionaryValueCallBacks); |
| if (!dict) |
| continue; |
| |
| CFNumberRef pageRef = CFNumberCreate(kCFAllocatorDefault, |
| kCFNumberLongType, |
| &page); |
| CFNumberRef usageRef = CFNumberCreate(kCFAllocatorDefault, |
| kCFNumberLongType, |
| &usages[i]); |
| if (pageRef && usageRef) |
| { |
| CFDictionarySetValue(dict, |
| CFSTR(kIOHIDDeviceUsagePageKey), |
| pageRef); |
| CFDictionarySetValue(dict, |
| CFSTR(kIOHIDDeviceUsageKey), |
| usageRef); |
| CFArrayAppendValue(matching, dict); |
| } |
| |
| if (pageRef) |
| CFRelease(pageRef); |
| if (usageRef) |
| CFRelease(usageRef); |
| |
| CFRelease(dict); |
| } |
| |
| IOHIDManagerSetDeviceMatchingMultiple(_glfw.ns.hidManager, matching); |
| CFRelease(matching); |
| |
| IOHIDManagerRegisterDeviceMatchingCallback(_glfw.ns.hidManager, |
| &matchCallback, NULL); |
| IOHIDManagerRegisterDeviceRemovalCallback(_glfw.ns.hidManager, |
| &removeCallback, NULL); |
| IOHIDManagerScheduleWithRunLoop(_glfw.ns.hidManager, |
| CFRunLoopGetMain(), |
| kCFRunLoopDefaultMode); |
| IOHIDManagerOpen(_glfw.ns.hidManager, kIOHIDOptionsTypeNone); |
| |
| // Execute the run loop once in order to register any initially-attached |
| // joysticks |
| CFRunLoopRunInMode(kCFRunLoopDefaultMode, 0, false); |
| return GLFW_TRUE; |
| } |
| |
| void _glfwTerminateJoysticksCocoa(void) |
| { |
| for (int jid = 0; jid <= GLFW_JOYSTICK_LAST; jid++) |
| { |
| if (_glfw.joysticks[jid].connected) |
| closeJoystick(&_glfw.joysticks[jid]); |
| } |
| |
| if (_glfw.ns.hidManager) |
| { |
| CFRelease(_glfw.ns.hidManager); |
| _glfw.ns.hidManager = NULL; |
| } |
| } |
| |
| |
| GLFWbool _glfwPollJoystickCocoa(_GLFWjoystick* js, int mode) |
| { |
| if (mode & _GLFW_POLL_AXES) |
| { |
| for (CFIndex i = 0; i < CFArrayGetCount(js->ns.axes); i++) |
| { |
| _GLFWjoyelementNS* axis = (_GLFWjoyelementNS*) |
| CFArrayGetValueAtIndex(js->ns.axes, i); |
| |
| const long raw = getElementValue(js, axis); |
| // Perform auto calibration |
| if (raw < axis->minimum) |
| axis->minimum = raw; |
| if (raw > axis->maximum) |
| axis->maximum = raw; |
| |
| const long size = axis->maximum - axis->minimum; |
| if (size == 0) |
| _glfwInputJoystickAxis(js, (int) i, 0.f); |
| else |
| { |
| const float value = (2.f * (raw - axis->minimum) / size) - 1.f; |
| _glfwInputJoystickAxis(js, (int) i, value); |
| } |
| } |
| } |
| |
| if (mode & _GLFW_POLL_BUTTONS) |
| { |
| for (CFIndex i = 0; i < CFArrayGetCount(js->ns.buttons); i++) |
| { |
| _GLFWjoyelementNS* button = (_GLFWjoyelementNS*) |
| CFArrayGetValueAtIndex(js->ns.buttons, i); |
| const char value = getElementValue(js, button) - button->minimum; |
| const int state = (value > 0) ? GLFW_PRESS : GLFW_RELEASE; |
| _glfwInputJoystickButton(js, (int) i, state); |
| } |
| |
| for (CFIndex i = 0; i < CFArrayGetCount(js->ns.hats); i++) |
| { |
| const int states[9] = |
| { |
| GLFW_HAT_UP, |
| GLFW_HAT_RIGHT_UP, |
| GLFW_HAT_RIGHT, |
| GLFW_HAT_RIGHT_DOWN, |
| GLFW_HAT_DOWN, |
| GLFW_HAT_LEFT_DOWN, |
| GLFW_HAT_LEFT, |
| GLFW_HAT_LEFT_UP, |
| GLFW_HAT_CENTERED |
| }; |
| |
| _GLFWjoyelementNS* hat = (_GLFWjoyelementNS*) |
| CFArrayGetValueAtIndex(js->ns.hats, i); |
| long state = getElementValue(js, hat) - hat->minimum; |
| if (state < 0 || state > 8) |
| state = 8; |
| |
| _glfwInputJoystickHat(js, (int) i, states[state]); |
| } |
| } |
| |
| return js->connected; |
| } |
| |
| const char* _glfwGetMappingNameCocoa(void) |
| { |
| return "Mac OS X"; |
| } |
| |
| void _glfwUpdateGamepadGUIDCocoa(char* guid) |
| { |
| if ((strncmp(guid + 4, "000000000000", 12) == 0) && |
| (strncmp(guid + 20, "000000000000", 12) == 0)) |
| { |
| char original[33]; |
| strncpy(original, guid, sizeof(original) - 1); |
| sprintf(guid, "03000000%.4s0000%.4s000000000000", |
| original, original + 16); |
| } |
| } |
| |
| #endif // _GLFW_COCOA |
| |