84366ef028e40e218d1448091c3667ac88b4b8b3
[enjoyable.git] / JoystickController.m
1 //
2 // JoystickController.m
3 // Enjoy
4 //
5 // Created by Sam McCall on 4/05/09.
6 //
7
8 #import "JoystickController.h"
9
10 #import "Config.h"
11 #import "ConfigsController.h"
12 #import "Joystick.h"
13 #import "JSAction.h"
14 #import "Target.h"
15 #import "TargetController.h"
16
17 @implementation JoystickController {
18 IOHIDManagerRef hidManager;
19 NSTimer *continuousTimer;
20 NSMutableArray *runningTargets;
21 }
22
23 - (id)init {
24 if ((self = [super init])) {
25 _joysticks = [[NSMutableArray alloc] initWithCapacity:16];
26 runningTargets = [[NSMutableArray alloc] initWithCapacity:32];
27 }
28 return self;
29 }
30
31 - (void)dealloc {
32 [continuousTimer invalidate];
33 IOHIDManagerClose(hidManager, kIOHIDOptionsTypeNone);
34 CFRelease(hidManager);
35 }
36
37 - (void)expandRecursive:(id)handler {
38 if ([handler base])
39 [self expandRecursive:[handler base]];
40 [outlineView expandItem:handler];
41 }
42
43 - (void)addRunningTarget:(Target *)target {
44 if (![runningTargets containsObject:target]) {
45 [runningTargets addObject:target];
46 }
47 if (!continuousTimer) {
48 continuousTimer = [NSTimer scheduledTimerWithTimeInterval:1.f/60.f
49 target:self
50 selector:@selector(updateContinuousActions:)
51 userInfo:nil
52 repeats:YES];
53 NSLog(@"Scheduled continuous target timer.");
54 }
55 }
56
57 static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
58 JoystickController *controller = (__bridge JoystickController *)ctx;
59 IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
60
61 Joystick *js = [controller findJoystickByRef:device];
62 if (controller.sendingRealEvents) {
63 JSAction *mainAction = [js actionForEvent:value];
64 [mainAction notifyEvent:value];
65 NSArray *children = mainAction.children ? mainAction.children : mainAction ? @[mainAction] : @[];
66 for (JSAction *subaction in children) {
67 Target *target = controller.currentConfig[subaction];
68 target.magnitude = mainAction.magnitude;
69 target.running = subaction.active;
70 if (target.running && target.isContinuous)
71 [controller addRunningTarget:target];
72 }
73 } else if ([NSApplication sharedApplication].isActive
74 && [NSApplication sharedApplication].mainWindow.isVisible) {
75 JSAction *handler = [js handlerForEvent:value];
76 if (!handler)
77 return;
78
79 [controller expandRecursive:handler];
80 [controller->outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[controller->outlineView rowForItem:handler]] byExtendingSelection: NO];
81 [controller->targetController focusKey];
82 }
83 }
84
85 static int findAvailableIndex(NSArray *list, Joystick *js) {
86 for (int index = 1; ; index++) {
87 BOOL available = YES;
88 for (Joystick *used in list) {
89 if ([used.productName isEqualToString:js.productName] && used.index == index) {
90 available = NO;
91 break;
92 }
93 }
94 if (available)
95 return index;
96 }
97 }
98
99 static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
100 JoystickController *controller = (__bridge JoystickController *)ctx;
101 IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void*)controller);
102 Joystick *js = [[Joystick alloc] initWithDevice:device];
103 js.index = findAvailableIndex(controller.joysticks, js);
104 [[controller joysticks] addObject:js];
105 [controller->outlineView reloadData];
106 }
107
108 - (Joystick *)findJoystickByRef:(IOHIDDeviceRef)device {
109 for (Joystick *js in _joysticks)
110 if (js.device == device)
111 return js;
112 return nil;
113 }
114
115 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
116 JoystickController *controller = (__bridge JoystickController *)ctx;
117 Joystick *match = [controller findJoystickByRef:device];
118 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
119 if (match) {
120 [controller.joysticks removeObject:match];
121 [controller->outlineView reloadData];
122 }
123 }
124
125 - (void)updateContinuousActions:(NSTimer *)timer {
126 self.mouseLoc = [NSEvent mouseLocation];
127 for (Target *target in [runningTargets copy]) {
128 if (![target update:self]) {
129 [runningTargets removeObject:target];
130 NSLog(@"Removing action, now running %lu.", runningTargets.count);
131 }
132 }
133 if (!runningTargets.count) {
134 [continuousTimer invalidate];
135 continuousTimer = nil;
136 NSLog(@"Unscheduled continuous target timer.");
137 }
138 }
139
140 #define NSSTR(e) ((NSString *)CFSTR(e))
141
142 - (void)setup {
143 hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
144 NSArray *criteria = @[ @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
145 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
146 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
147 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
148 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
149 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
150 ];
151 IOHIDManagerSetDeviceMatchingMultiple(hidManager, (__bridge CFArrayRef)criteria);
152
153 IOHIDManagerScheduleWithRunLoop(hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
154 IOReturn ret = IOHIDManagerOpen(hidManager, kIOHIDOptionsTypeNone);
155 if (ret != kIOReturnSuccess) {
156 [[NSAlert alertWithMessageText:@"Input devices are unavailable"
157 defaultButton:nil
158 alternateButton:nil
159 otherButton:nil
160 informativeTextWithFormat:@"Error 0x%08x occured trying to access your devices. "
161 @"Input may not be correctly detected or mapped.",
162 ret]
163 runModal];
164 }
165
166 IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self);
167 IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self);
168 }
169
170 - (Config *)currentConfig {
171 return configsController.currentConfig;
172 }
173
174 - (JSAction *)selectedAction {
175 id item = [outlineView itemAtRow:outlineView.selectedRow];
176 return [item children] ? nil : item;
177 }
178
179 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
180 return item ? [[item children] count] : _joysticks.count;
181 }
182
183 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
184 return item ? [[item children] count] > 0: YES;
185 }
186
187 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
188 return item ? [item children][index] : _joysticks[index];
189 }
190
191 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
192 if(item == nil)
193 return @"root";
194 return [item name];
195 }
196
197 - (void)outlineViewSelectionDidChange: (NSNotification*) notification {
198 [targetController reset];
199 [targetController load];
200 }
201
202 @end