1
0
mirror of https://github.com/TomHarte/CLK.git synced 2024-11-19 08:31:11 +00:00
CLK/OSBindings/Mac/Clock Signal/Joystick Manager/CSJoystickManager.m
C.W. Betts 5a48e50355 Use isEqual: to compare GCController when connecting/disconnecting.
Only remove observers for GCController notifications.
2020-12-14 15:41:11 -07:00

619 lines
18 KiB
Objective-C

//
// CSJoystickManager.m
// Clock Signal
//
// Created by Thomas Harte on 19/07/2018.
// Copyright © 2018 Thomas Harte. All rights reserved.
//
#import "CSJoystickManager.h"
@import IOKit;
#include <IOKit/hid/IOHIDLib.h>
@import GameController;
#pragma mark - CSJoystickButton
@implementation CSJoystickButton {
@package
bool _isPressed;
}
- (instancetype)initWithIndex:(NSInteger)index {
if (self = [super init]) {
_index = index;
}
return self;
}
@end
@interface CSIOJoystickButton: CSJoystickButton
@end
@implementation CSIOJoystickButton {
IOHIDElementRef _element;
}
- (instancetype)initWithElement:(IOHIDElementRef)element index:(NSInteger)index {
self = [super initWithIndex:index];
if(self) {
_element = (IOHIDElementRef)CFRetain(element);
}
return self;
}
- (void)dealloc {
CFRelease(_element);
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSIOJoystickButton: %p>; button %ld, %@", self, (long)self.index, self.isPressed ? @"pressed" : @"released"];
}
- (IOHIDElementRef)element {
return _element;
}
- (void)setIsPressed:(bool)isPressed {
_isPressed = isPressed;
}
@end
#pragma mark - CSJoystickAxis
@implementation CSJoystickAxis {
@package
float _position;
}
- (instancetype)initWithType:(CSJoystickAxisType)type
{
if (self = [super init]) {
_type = type;
}
return self;
}
@end
@interface CSIOJoystickAxis: CSJoystickAxis
@end
@implementation CSIOJoystickAxis {
IOHIDElementRef _element;
}
- (instancetype)initWithElement:(IOHIDElementRef)element type:(CSJoystickAxisType)type {
self = [super initWithType:type];
if(self) {
_element = (IOHIDElementRef)CFRetain(element);
_position = 0.5f;
}
return self;
}
- (void)dealloc {
CFRelease(_element);
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSIOJoystickAxis: %p>; type %d, value %0.2f", self, (int)self.type, self.position];
}
- (IOHIDElementRef)element {
return _element;
}
- (void)setPosition:(float)position {
_position = position;
}
@end
#pragma mark - CSJoystickHat
@implementation CSJoystickHat {
@package
CSJoystickHatDirection _direction;
}
@end
@interface CSIOJoystickHat: CSJoystickHat
@end
@implementation CSIOJoystickHat {
IOHIDElementRef _element;
}
- (instancetype)initWithElement:(IOHIDElementRef)element {
self = [super init];
if(self) {
_element = (IOHIDElementRef)CFRetain(element);
}
return self;
}
- (void)dealloc {
CFRelease(_element);
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSIOJoystickHat: %p>; direction %ld", self, (long)self.direction];
}
- (IOHIDElementRef)element {
return _element;
}
- (void)setDirection:(CSJoystickHatDirection)direction {
_direction = direction;
}
@end
#pragma mark - CSJoystick
@implementation CSJoystick {
@package
NSArray<CSJoystickButton *> *_buttons;
NSArray<CSJoystickAxis *> *_axes;
NSArray<CSJoystickHat *> *_hats;
}
- (void)update
{
//subclass!
}
@end
@interface CSIOJoystick: CSJoystick
@end
@implementation CSIOJoystick {
IOHIDDeviceRef _device;
}
- (instancetype)initWithButtons:(NSArray<CSJoystickButton *> *)buttons
axes:(NSArray<CSJoystickAxis *> *)axes
hats:(NSArray<CSJoystickHat *> *)hats
device:(IOHIDDeviceRef)device {
self = [super init];
if(self) {
// Sort buttons by index.
_buttons = [buttons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"index" ascending:YES]]];
// Sort axes by enum value.
_axes = [axes sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"type" ascending:YES]]];
// Hats have no guaranteed ordering.
_hats = hats;
// Keep hold of the device.
_device = (IOHIDDeviceRef)CFRetain(device);
}
return self;
}
- (void)dealloc {
CFRelease(_device);
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSIOJoystick: %p>; buttons %@, axes %@, hats %@", self, self.buttons, self.axes, self.hats];
}
- (void)update {
// Update buttons.
for(CSIOJoystickButton *button in _buttons) {
IOHIDValueRef value;
if(IOHIDDeviceGetValue(_device, button.element, &value) == kIOReturnSuccess) {
// Some pressure-sensitive buttons return values greater than 1 for hard presses,
// but this class rationalised everything to Boolean.
button.isPressed = !!IOHIDValueGetIntegerValue(value);
}
}
// Update hats.
for(CSIOJoystickHat *hat in _hats) {
IOHIDValueRef value;
if(IOHIDDeviceGetValue(_device, hat.element, &value) == kIOReturnSuccess) {
// Hats report a direction, which is either one of eight or one of four.
CFIndex integerValue = IOHIDValueGetIntegerValue(value) - IOHIDElementGetLogicalMin(hat.element);
const CFIndex range = 1 + IOHIDElementGetLogicalMax(hat.element) - IOHIDElementGetLogicalMin(hat.element);
integerValue *= 8 / range;
// Map from the HID direction to the bit field.
switch(integerValue) {
default: hat.direction = 0; break;
case 0: hat.direction = CSJoystickHatDirectionUp; break;
case 1: hat.direction = CSJoystickHatDirectionUp | CSJoystickHatDirectionRight; break;
case 2: hat.direction = CSJoystickHatDirectionRight; break;
case 3: hat.direction = CSJoystickHatDirectionRight | CSJoystickHatDirectionDown; break;
case 4: hat.direction = CSJoystickHatDirectionDown; break;
case 5: hat.direction = CSJoystickHatDirectionDown | CSJoystickHatDirectionLeft; break;
case 6: hat.direction = CSJoystickHatDirectionLeft; break;
case 7: hat.direction = CSJoystickHatDirectionLeft | CSJoystickHatDirectionUp; break;
}
}
}
// Update axes.
for(CSIOJoystickAxis *axis in _axes) {
IOHIDValueRef value;
if(IOHIDDeviceGetValue(_device, axis.element, &value) == kIOReturnSuccess) {
const CFIndex integerValue = IOHIDValueGetIntegerValue(value) - IOHIDElementGetLogicalMin(axis.element);
const CFIndex range = 1 + IOHIDElementGetLogicalMax(axis.element) - IOHIDElementGetLogicalMin(axis.element);
axis.position = (float)integerValue / (float)range;
}
}
}
- (IOHIDDeviceRef)device {
return _device;
}
@end
#pragma mark - GameController subclasses
API_AVAILABLE(macos(11.0))
@interface CSGCJoystickHat: CSJoystickHat
@property (readonly, strong) GCDeviceDirectionPad *directionPad;
@end
API_AVAILABLE(macos(11.0))
@interface CSGCJoystickAxis: CSJoystickAxis
@property (readonly, strong) GCDeviceAxisInput *axis;
@end
API_AVAILABLE(macos(11.0))
@interface CSGCJoystickButton: CSJoystickButton
@property (readonly, strong) GCDeviceButtonInput *button;
@end
API_AVAILABLE(macos(11.0))
@interface CSGCJoystick: CSJoystick
@property (readonly, strong) GCController *device;
@end
@implementation CSGCJoystickHat
- (instancetype)initWithDirectionPad:(GCDeviceDirectionPad*)dPad {
if (self = [super init]) {
_directionPad = dPad;
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSGCJoystickHat: %p>; direction %ld", self, (long)self.direction];
}
- (void)setDirection:(CSJoystickHatDirection)direction {
_direction = direction;
}
@end
@implementation CSGCJoystickAxis
- (instancetype)initWithAxis:(GCDeviceAxisInput*)element type:(CSJoystickAxisType)type {
self = [super initWithType:type];
if(self) {
_axis = element;
_position = 0.5f;
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSGCJoystickAxis: %p>; type %d, value %0.2f", self, (int)self.type, self.position];
}
- (void)setPosition:(float)position {
_position = position;
}
@end
@implementation CSGCJoystickButton
- (instancetype)initWithButton:(GCDeviceButtonInput*)element index:(NSInteger)index {
self = [super initWithIndex:index];
if(self) {
_button = element;
}
return self;
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSGCJoystickButton: %p>; button %ld, %@", self, (long)self.index, self.isPressed ? @"pressed" : @"released"];
}
- (void)setIsPressed:(bool)isPressed {
_isPressed = isPressed;
}
@end
@implementation CSGCJoystick
- (instancetype)initWithButtons:(NSArray<CSJoystickButton *> *)buttons
axes:(NSArray<CSJoystickAxis *> *)axes
hats:(NSArray<CSJoystickHat *> *)hats
device:(GCController*)device {
if (self = [super init]) {
// Sort buttons by index.
_buttons = [buttons sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"index" ascending:YES]]];
// Sort axes by enum value.
_axes = [axes sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"type" ascending:YES]]];
// Hats have no guaranteed ordering.
_hats = hats;
// Keep hold of the device.
_device = device;
}
return self;
}
-(void)update {
// Update buttons.
for(CSGCJoystickButton *button in _buttons) {
// This assumes that the values provided by GCDeviceButtonInput are
// digital. This might not always be the case.
button.isPressed = button.button.pressed;
}
for(CSGCJoystickAxis *axis in _axes) {
float val = axis.axis.value;
val += 1;
val /= 2;
axis.position = val;
}
for(CSGCJoystickHat *hat in _hats) {
// This assumes that the values provided by GCDeviceDirectionPad are
// digital. this might not always be the case.
CSJoystickHatDirection hatDir = 0;
if (hat.directionPad.down.pressed) {
hatDir |= CSJoystickHatDirectionDown;
}
if (hat.directionPad.up.pressed) {
hatDir |= CSJoystickHatDirectionUp;
}
if (hat.directionPad.left.pressed) {
hatDir |= CSJoystickHatDirectionLeft;
}
if (hat.directionPad.right.pressed) {
hatDir |= CSJoystickHatDirectionRight;
}
// There shouldn't be any conflicting directions.
hat.direction = hatDir;
}
}
- (NSString *)description {
return [NSString stringWithFormat:@"<CSGCJoystick: %p>; buttons %@, axes %@, hats %@", self, self.buttons, self.axes, self.hats];
}
@end
#pragma mark - CSJoystickManager
@interface CSJoystickManager ()
- (void)deviceMatched:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender;
- (void)deviceRemoved:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender;
- (void)controllerDidConnect:(NSNotification *)note API_AVAILABLE(macos(11.0));
- (void)controllerDidDisconnect:(NSNotification *)note API_AVAILABLE(macos(11.0));
@end
static void DeviceMatched(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) {
[(__bridge CSJoystickManager *)context deviceMatched:device result:result sender:sender];
}
static void DeviceRemoved(void *context, IOReturn result, void *sender, IOHIDDeviceRef device) {
[(__bridge CSJoystickManager *)context deviceRemoved:device result:result sender:sender];
}
@implementation CSJoystickManager {
IOHIDManagerRef _hidManager;
NSMutableArray<CSJoystick *> *_joysticks;
}
- (instancetype)init {
self = [super init];
if(self) {
_joysticks = [[NSMutableArray alloc] init];
_hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
if(!_hidManager) return nil;
NSArray<NSDictionary<NSString *, NSNumber *> *> *const multiple = @[
@{ @kIOHIDDeviceUsagePageKey: @(kHIDPage_GenericDesktop), @kIOHIDDeviceUsageKey: @(kHIDUsage_GD_Joystick) },
@{ @kIOHIDDeviceUsagePageKey: @(kHIDPage_GenericDesktop), @kIOHIDDeviceUsageKey: @(kHIDUsage_GD_GamePad) },
@{ @kIOHIDDeviceUsagePageKey: @(kHIDPage_GenericDesktop), @kIOHIDDeviceUsageKey: @(kHIDUsage_GD_MultiAxisController) },
];
IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)multiple);
IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, DeviceMatched, (__bridge void *)self);
IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, DeviceRemoved, (__bridge void *)self);
IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
if (@available(macOS 11.0, *)) {
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidConnect:) name:GCControllerDidConnectNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(controllerDidDisconnect:) name:GCControllerDidDisconnectNotification object:nil];
}
if(IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone) != kIOReturnSuccess) {
NSLog(@"Failed to open HID manager");
// something
return nil;
}
}
return self;
}
- (void)controllerDidConnect:(NSNotification *)note {
GCController *controller = note.object;
// Double check this joystick isn't already known.
for(CSGCJoystick *joystick in _joysticks) {
if (![joystick isKindOfClass:[CSGCJoystick class]]) {
continue;
}
if([joystick.device isEqual:controller]) return;
}
// Prepare to collate a list of buttons, axes and hats for the new device.
NSMutableArray<CSJoystickButton *> *buttons = [[NSMutableArray alloc] init];
NSMutableArray<CSJoystickAxis *> *axes = [[NSMutableArray alloc] init];
NSMutableArray<CSJoystickHat *> *hats = [[NSMutableArray alloc] init];
if (controller.extendedGamepad) {
GCExtendedGamepad *gp = controller.extendedGamepad;
// Let's go a b x y
// 1 2 3 4
[buttons addObject:[[CSGCJoystickButton alloc] initWithButton:gp.buttonA index:1]];
[buttons addObject:[[CSGCJoystickButton alloc] initWithButton:gp.buttonB index:2]];
[buttons addObject:[[CSGCJoystickButton alloc] initWithButton:gp.buttonX index:3]];
[buttons addObject:[[CSGCJoystickButton alloc] initWithButton:gp.buttonY index:4]];
[hats addObject:[[CSGCJoystickHat alloc] initWithDirectionPad:gp.dpad]];
[axes addObject:[[CSGCJoystickAxis alloc] initWithAxis:gp.leftThumbstick.xAxis type:CSJoystickAxisTypeX]];
[axes addObject:[[CSGCJoystickAxis alloc] initWithAxis:gp.leftThumbstick.yAxis type:CSJoystickAxisTypeY]];
[axes addObject:[[CSGCJoystickAxis alloc] initWithAxis:gp.rightThumbstick.xAxis type:CSJoystickAxisTypeZ]];
} else {
return;
}
// Add this joystick to the list.
[_joysticks addObject:[[CSGCJoystick alloc] initWithButtons:buttons axes:axes hats:hats device:controller]];
}
- (void)controllerDidDisconnect:(NSNotification *)note {
GCController *controller = note.object;
// If this joystick was recorded, remove it.
for(CSGCJoystick *joystick in [_joysticks copy]) {
if (![joystick isKindOfClass:[CSGCJoystick class]]) {
continue;
}
if([joystick.device isEqual:controller]) {
[_joysticks removeObject:joystick];
return;
}
}
}
- (void)dealloc {
IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
CFRelease(_hidManager);
if (@available(macOS 11.0, *)) {
[[NSNotificationCenter defaultCenter] removeObserver:self name:GCControllerDidConnectNotification object:nil];
[[NSNotificationCenter defaultCenter] removeObserver:self name:GCControllerDidDisconnectNotification object:nil];
}
}
- (void)deviceMatched:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender {
if (@available(macOS 11.0, *)) {
if ([GCController supportsHIDDevice:device]) {
return;
}
}
// Double check this joystick isn't already known.
for(CSIOJoystick *joystick in _joysticks) {
if (![joystick isKindOfClass:[CSIOJoystick class]]) {
continue;
}
if(joystick.device == device) return;
}
// Prepare to collate a list of buttons, axes and hats for the new device.
NSMutableArray<CSJoystickButton *> *buttons = [[NSMutableArray alloc] init];
NSMutableArray<CSJoystickAxis *> *axes = [[NSMutableArray alloc] init];
NSMutableArray<CSJoystickHat *> *hats = [[NSMutableArray alloc] init];
// Inspect all elements for those that are comprehensible to this code.
const CFArrayRef elements = IOHIDDeviceCopyMatchingElements(device, NULL, kIOHIDOptionsTypeNone);
for(CFIndex index = 0; index < CFArrayGetCount(elements); ++index) {
const IOHIDElementRef element = (IOHIDElementRef)CFArrayGetValueAtIndex(elements, index);
// Check that this element is either on the generic desktop page or else is a button.
const uint32_t usagePage = IOHIDElementGetUsagePage(element);
if(usagePage != kHIDPage_GenericDesktop && usagePage != kHIDPage_Button) continue;
// Then inspect the type.
switch(IOHIDElementGetType(element)) {
default: break;
case kIOHIDElementTypeInput_Button: {
// Add a button; pretty easy stuff. 'Usage' provides a button index.
const uint32_t usage = IOHIDElementGetUsage(element);
[buttons addObject:[[CSIOJoystickButton alloc] initWithElement:element index:usage]];
} break;
case kIOHIDElementTypeInput_Misc:
case kIOHIDElementTypeInput_Axis: {
CSJoystickAxisType axisType;
switch(IOHIDElementGetUsage(element)) {
default: continue;
// Three analogue axes are implemented here; there are another three sets
// of these that could be parsed in the future if interesting.
case kHIDUsage_GD_X: axisType = CSJoystickAxisTypeX; break;
case kHIDUsage_GD_Y: axisType = CSJoystickAxisTypeY; break;
case kHIDUsage_GD_Z: axisType = CSJoystickAxisTypeZ; break;
// A hatswitch is a multi-directional control all of its own.
case kHIDUsage_GD_Hatswitch:
[hats addObject:[[CSIOJoystickHat alloc] initWithElement:element]];
continue;
}
// Add the axis; if it was a hat switch or unrecognised then the code doesn't
// reach here.
[axes addObject:[[CSIOJoystickAxis alloc] initWithElement:element type:axisType]];
} break;
}
}
CFRelease(elements);
// Add this joystick to the list.
[_joysticks addObject:[[CSIOJoystick alloc] initWithButtons:buttons axes:axes hats:hats device:device]];
}
- (void)deviceRemoved:(IOHIDDeviceRef)device result:(IOReturn)result sender:(void *)sender {
// If this joystick was recorded, remove it.
for(CSIOJoystick *joystick in [_joysticks copy]) {
if (![joystick isKindOfClass:[CSIOJoystick class]]) {
continue;
}
if(joystick.device == device) {
[_joysticks removeObject:joystick];
return;
}
}
}
- (void)update {
[self.joysticks makeObjectsPerformSelector:@selector(update)];
}
- (NSArray<CSJoystick *> *)joysticks {
return [_joysticks copy];
}
@end