Refactor Actions. SubAction is a (mostly) proper JSAction. JSActions have (more)...
[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 @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 - (JSAction *)selectedAction {
178 id item = [outlineView itemAtRow:outlineView.selectedRow];
179 if ([item isKindOfClass: [JSAction class]] && ![item subActions])
180 return item;
181 else
182 return nil;
183 }
184
185 /* outline view */
186
187 - (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
188 if(item == nil)
189 return [joysticks count];
190 if([item isKindOfClass: [Joystick class]])
191 return [[item children] count];
192 if([item isKindOfClass: [JSAction class]] && [item subActions] != NULL)
193 return [[item subActions] count];
194 return 0;
195 }
196
197 - (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
198 if(item == nil)
199 return YES;
200 if([item isKindOfClass: [Joystick class]])
201 return YES;
202 if([item isKindOfClass: [JSAction class]])
203 return [item subActions]==NULL ? NO : YES;
204 return NO;
205 }
206
207 - (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
208 if(item == nil)
209 return joysticks[index];
210
211 if([item isKindOfClass: [Joystick class]])
212 return [item children][index];
213
214 if([item isKindOfClass: [JSAction class]])
215 return [item subActions][index];
216
217 return NULL;
218 }
219 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item {
220 if(item == nil)
221 return @"root";
222 return [item name];
223 }
224
225 - (void)outlineViewSelectionDidChange: (NSNotification*) notification {
226 [targetController reset];
227 [targetController load];
228 if (programmaticallySelecting)
229 [targetController focusKey];
230 programmaticallySelecting = NO;
231 }
232
233 @end