Switch controller/HID bridging to non-retained since the controller lives as long...
[enjoyable.git] / JoystickController.m
1 //
2 // JoystickController.m
3 // Enjoy
4 //
5 // Created by Sam McCall on 4/05/09.
6 //
7
8 #import "CoreFoundation/CoreFoundation.h"
9
10 @implementation JoystickController
11
12 @synthesize joysticks, runningTargets, selectedAction, frontWindowOnly;
13
14 -(id) init {
15 if(self=[super init]) {
16 joysticks = [[NSMutableArray alloc]init];
17 runningTargets = [[NSMutableArray alloc]init];
18 programmaticallySelecting = NO;
19 mouseLoc.x = mouseLoc.y = 0;
20 }
21 return self;
22 }
23
24 -(void) dealloc {
25 for(int i=0; i<[joysticks count]; i++) {
26 [joysticks[i] invalidate];
27 }
28 IOHIDManagerClose(hidManager, kIOHIDOptionsTypeNone);
29 CFRelease(hidManager);
30 }
31
32 static NSMutableDictionary* create_criterion( UInt32 inUsagePage, UInt32 inUsage )
33 {
34 NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
35 dict[(NSString*)CFSTR(kIOHIDDeviceUsagePageKey)] = @(inUsagePage);
36 dict[(NSString*)CFSTR(kIOHIDDeviceUsageKey)] = @(inUsage);
37 return dict;
38 }
39
40 -(void) expandRecursive: (id) handler {
41 if([handler base])
42 [self expandRecursive: [handler base]];
43 [outlineView expandItem: handler];
44 }
45
46 static void timer_callback(CFRunLoopTimerRef timer, void *ctx) {
47 JoystickController *jc = (__bridge JoystickController *)ctx;
48 jc->mouseLoc = [NSEvent mouseLocation];
49 for (Target *target in [jc runningTargets]) {
50 [target update: jc];
51 }
52 }
53
54 static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
55 JoystickController *controller = (__bridge JoystickController *)ctx;
56 IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
57
58 Joystick *js = [controller findJoystickByRef:device];
59 if([(ApplicationController *)[[NSApplication sharedApplication] delegate] active]) {
60 JSAction* mainAction = [js actionForEvent: value];
61 if(!mainAction)
62 return;
63
64 [mainAction notifyEvent: value];
65 NSArray* subactions = [mainAction subActions];
66 if(!subactions)
67 subactions = @[mainAction];
68 for(id subaction in subactions) {
69 Target* target = [[controller->configsController currentConfig] getTargetForAction:subaction];
70 if(!target)
71 continue;
72 /* target application? doesn't seem to be any need since we are only active when it's in front */
73 /* might be required for some strange actions */
74 if ([target running] != [subaction active]) {
75 if ([subaction active]) {
76 [target trigger: controller];
77 }
78 else {
79 [target untrigger: controller];
80 }
81 [target setRunning: [subaction active]];
82 }
83
84 if ([mainAction isKindOfClass: [JSActionAnalog class]]) {
85 double realValue = [(JSActionAnalog*)mainAction getRealValue: IOHIDValueGetIntegerValue(value)];
86 [target setInputValue: realValue];
87
88 // Add to list of running targets
89 if ([target isContinuous] && [target running]) {
90 if (![controller.runningTargets containsObject:target]) {
91 [[controller runningTargets] addObject: target];
92 }
93 }
94 }
95 }
96 } else if([[NSApplication sharedApplication] isActive] && [[[NSApplication sharedApplication]mainWindow]isVisible]) {
97 // joysticks not active, use it to select stuff
98 id handler = [js handlerForEvent: value];
99 if(!handler)
100 return;
101
102 [controller expandRecursive: handler];
103 controller->programmaticallySelecting = YES;
104 [controller->outlineView selectRowIndexes: [NSIndexSet indexSetWithIndex: [controller->outlineView rowForItem: handler]] byExtendingSelection: NO];
105 }
106 }
107
108 static int findAvailableIndex(id list, Joystick* js) {
109 BOOL available;
110 Joystick* js2;
111 for(int index=0;;index++) {
112 available = YES;
113 for(int i=0; i<[list count]; i++) {
114 js2 = list[i];
115 if([js2 vendorId] == [js vendorId] && [js2 productId] == [js productId] && [js index] == index) {
116 available = NO;
117 break;
118 }
119 }
120 if(available)
121 return index;
122 }
123 }
124
125 static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
126 JoystickController *controller = (__bridge JoystickController *)ctx;
127 IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void*)controller);
128 Joystick *js = [[Joystick alloc] initWithDevice:device];
129 js.index = findAvailableIndex(controller.joysticks, js);
130 [js populateActions];
131 [[controller joysticks] addObject:js];
132 [controller->outlineView reloadData];
133 }
134
135 - (Joystick *)findJoystickByRef:(IOHIDDeviceRef)device {
136 for (Joystick *js in joysticks)
137 if (js.device == device)
138 return js;
139 return nil;
140 }
141
142 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
143 JoystickController *controller = (__bridge JoystickController *)ctx;
144 Joystick *match = [controller findJoystickByRef:device];
145 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
146 if (match) {
147 [controller.joysticks removeObject:match];
148 [controller->outlineView reloadData];
149 }
150 }
151
152 - (void)setup {
153 hidManager = IOHIDManagerCreate( kCFAllocatorDefault, kIOHIDOptionsTypeNone);
154 NSArray *criteria = @[
155 create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick),
156 create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad),
157 create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController)];
158
159 IOHIDManagerSetDeviceMatchingMultiple(hidManager, (CFArrayRef)CFBridgingRetain(criteria));
160
161 IOHIDManagerScheduleWithRunLoop( hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode );
162 IOReturn tIOReturn = IOHIDManagerOpen( hidManager, kIOHIDOptionsTypeNone );
163 (void)tIOReturn;
164
165 IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self );
166 IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self);
167
168 // Setup timer for continuous targets
169 CFRunLoopTimerContext ctx = {
170 0, (__bridge void*)self, NULL, NULL, NULL
171 };
172 CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
173 CFAbsoluteTimeGetCurrent(), 1.0/80.0,
174 0, 0, timer_callback, &ctx);
175 CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
176 }
177
178 -(id) determineSelectedAction {
179 id item = [outlineView itemAtRow: [outlineView selectedRow]];
180 if(!item)
181 return NULL;
182 if([item isKindOfClass: [JSAction class]] && [item subActions] != NULL)
183 return NULL;
184 if([item isKindOfClass: [Joystick class]])
185 return NULL;
186 return item;
187 }
188
189 /* outline view */
190
191 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
192 if(item == nil)
193 return [joysticks count];
194 if([item isKindOfClass: [Joystick class]])
195 return [[item children] count];
196 if([item isKindOfClass: [JSAction class]] && [item subActions] != NULL)
197 return [[item subActions] count];
198 return 0;
199 }
200
201 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
202 if(item == nil)
203 return YES;
204 if([item isKindOfClass: [Joystick class]])
205 return YES;
206 if([item isKindOfClass: [JSAction class]])
207 return [item subActions]==NULL ? NO : YES;
208 return NO;
209 }
210
211 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
212 if(item == nil)
213 return joysticks[index];
214
215 if([item isKindOfClass: [Joystick class]])
216 return [item children][index];
217
218 if([item isKindOfClass: [JSAction class]])
219 return [item subActions][index];
220
221 return NULL;
222 }
223 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
224 if(item == nil)
225 return @"root";
226 return [item name];
227 }
228
229 - (void)outlineViewSelectionDidChange: (NSNotification*) notification {
230 [targetController reset];
231 selectedAction = [self determineSelectedAction];
232 [targetController load];
233 if(programmaticallySelecting)
234 [targetController focusKey];
235 programmaticallySelecting = NO;
236 }
237
238 @end