Clean-up of Joystick class. Refactor constructor to avoid mandatory 'post-constructor...
[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 [[controller joysticks] addObject:js];
131 [controller->outlineView reloadData];
132 }
133
134 - (Joystick *)findJoystickByRef:(IOHIDDeviceRef)device {
135 for (Joystick *js in joysticks)
136 if (js.device == device)
137 return js;
138 return nil;
139 }
140
141 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
142 JoystickController *controller = (__bridge JoystickController *)ctx;
143 Joystick *match = [controller findJoystickByRef:device];
144 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
145 if (match) {
146 [controller.joysticks removeObject:match];
147 [controller->outlineView reloadData];
148 }
149 }
150
151 - (void)setup {
152 hidManager = IOHIDManagerCreate( kCFAllocatorDefault, kIOHIDOptionsTypeNone);
153 NSArray *criteria = @[
154 create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick),
155 create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad),
156 create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController)];
157
158 IOHIDManagerSetDeviceMatchingMultiple(hidManager, (CFArrayRef)CFBridgingRetain(criteria));
159
160 IOHIDManagerScheduleWithRunLoop( hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode );
161 IOReturn tIOReturn = IOHIDManagerOpen( hidManager, kIOHIDOptionsTypeNone );
162 (void)tIOReturn;
163
164 IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self );
165 IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self);
166
167 // Setup timer for continuous targets
168 CFRunLoopTimerContext ctx = {
169 0, (__bridge void*)self, NULL, NULL, NULL
170 };
171 CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
172 CFAbsoluteTimeGetCurrent(), 1.0/80.0,
173 0, 0, timer_callback, &ctx);
174 CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
175 }
176
177 -(id) determineSelectedAction {
178 id item = [outlineView itemAtRow: [outlineView selectedRow]];
179 if(!item)
180 return NULL;
181 if([item isKindOfClass: [JSAction class]] && [item subActions] != NULL)
182 return NULL;
183 if([item isKindOfClass: [Joystick class]])
184 return NULL;
185 return item;
186 }
187
188 /* outline view */
189
190 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
191 if(item == nil)
192 return [joysticks count];
193 if([item isKindOfClass: [Joystick class]])
194 return [[item children] count];
195 if([item isKindOfClass: [JSAction class]] && [item subActions] != NULL)
196 return [[item subActions] count];
197 return 0;
198 }
199
200 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
201 if(item == nil)
202 return YES;
203 if([item isKindOfClass: [Joystick class]])
204 return YES;
205 if([item isKindOfClass: [JSAction class]])
206 return [item subActions]==NULL ? NO : YES;
207 return NO;
208 }
209
210 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
211 if(item == nil)
212 return joysticks[index];
213
214 if([item isKindOfClass: [Joystick class]])
215 return [item children][index];
216
217 if([item isKindOfClass: [JSAction class]])
218 return [item subActions][index];
219
220 return NULL;
221 }
222 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
223 if(item == nil)
224 return @"root";
225 return [item name];
226 }
227
228 - (void)outlineViewSelectionDidChange: (NSNotification*) notification {
229 [targetController reset];
230 selectedAction = [self determineSelectedAction];
231 [targetController load];
232 if(programmaticallySelecting)
233 [targetController focusKey];
234 programmaticallySelecting = NO;
235 }
236
237 @end