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