2849838b22e2c576de28e45d06add5f278075776
[enjoyable.git] / Classes / NJDeviceController.m
1 //
2 // NJDeviceController.m
3 // Enjoy
4 //
5 // Created by Sam McCall on 4/05/09.
6 //
7
8 #import "NJDeviceController.h"
9
10 #import "NJMapping.h"
11 #import "NJMappingsController.h"
12 #import "NJDevice.h"
13 #import "NJInput.h"
14 #import "NJOutput.h"
15 #import "NJOutputController.h"
16 #import "NJEvents.h"
17
18 @implementation NJDeviceController {
19 IOHIDManagerRef _hidManager;
20 NSTimer *_continuousOutputsTick;
21 NSMutableArray *_continousOutputs;
22 NSMutableArray *_devices;
23 }
24
25 - (id)init {
26 if ((self = [super init])) {
27 _devices = [[NSMutableArray alloc] initWithCapacity:16];
28 _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
29 [NSNotificationCenter.defaultCenter
30 addObserver:self
31 selector:@selector(applicationDidFinishLaunching:)
32 name:NSApplicationDidFinishLaunchingNotification
33 object:nil];
34
35 // The HID manager uses 5-10ms per second doing basically
36 // nothing if a noisy device is plugged in (the % of that
37 // spent in input_callback is negligible, so it's not
38 // something we can make faster). I don't really think that's
39 // acceptable, CPU/power wise. So if translation is disabled
40 // and the window is closed, just switch off the HID manager
41 // entirely. This probably also has some marginal benefits for
42 // compatibility with other applications that want exclusive
43 // grabs.
44 [NSNotificationCenter.defaultCenter
45 addObserver:self
46 selector:@selector(closeHidIfDisabled:)
47 name:NSApplicationDidResignActiveNotification
48 object:nil];
49 [NSNotificationCenter.defaultCenter
50 addObserver:self
51 selector:@selector(openHid)
52 name:NSApplicationDidBecomeActiveNotification
53 object:nil];
54 }
55 return self;
56 }
57
58 - (void)dealloc {
59 [NSNotificationCenter.defaultCenter removeObserver:self];
60 [_continuousOutputsTick invalidate];
61 [self closeHid];
62 }
63
64 - (void)expandRecursive:(id <NJInputPathElement>)pathElement {
65 if (pathElement) {
66 [self expandRecursive:pathElement.base];
67 [outlineView expandItem:pathElement];
68 }
69 }
70
71 - (void)addRunningOutput:(NJOutput *)output {
72 // Axis events will trigger every small movement, don't keep
73 // re-adding them or they trigger multiple times each time.
74 if (![_continousOutputs containsObject:output])
75 [_continousOutputs addObject:output];
76 if (!_continuousOutputsTick) {
77 _continuousOutputsTick = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
78 target:self
79 selector:@selector(updateContinuousOutputs:)
80 userInfo:nil
81 repeats:YES];
82 }
83 }
84
85 - (void)runOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
86 NJDevice *dev = [self findDeviceByRef:device];
87 NJInput *mainInput = [dev inputForEvent:value];
88 [mainInput notifyEvent:value];
89 NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[];
90 for (NJInput *subInput in children) {
91 NJOutput *output = mappingsController.currentMapping[subInput];
92 output.magnitude = subInput.magnitude;
93 output.running = subInput.active;
94 if ((output.running || output.magnitude) && output.isContinuous)
95 [self addRunningOutput:output];
96 }
97 }
98
99 - (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
100 NJDevice *dev = [self findDeviceByRef:device];
101 NJInput *handler = [dev handlerForEvent:value];
102 if (!handler)
103 return;
104
105 [self expandRecursive:handler];
106 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
107 byExtendingSelection: NO];
108 [outputController focusKey];
109 }
110
111 static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
112 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
113 IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
114
115 if (controller.translatingEvents) {
116 [controller runOutputForDevice:device value:value];
117 } else if ([NSApplication sharedApplication].mainWindow.isVisible) {
118 [controller showOutputForDevice:device value:value];
119 }
120 }
121
122 static int findAvailableIndex(NSArray *list, NJDevice *dev) {
123 for (int index = 1; ; index++) {
124 BOOL available = YES;
125 for (NJDevice *used in list) {
126 if ([used.productName isEqualToString:dev.productName] && used.index == index) {
127 available = NO;
128 break;
129 }
130 }
131 if (available)
132 return index;
133 }
134 }
135
136 - (void)addDeviceForDevice:(IOHIDDeviceRef)device {
137 IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void *)self);
138 NJDevice *dev = [[NJDevice alloc] initWithDevice:device];
139 dev.index = findAvailableIndex(_devices, dev);
140 [_devices addObject:dev];
141 [outlineView reloadData];
142 connectDevicePrompt.hidden = !!_devices.count;
143 }
144
145 static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
146 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
147 [controller addDeviceForDevice:device];
148 }
149
150 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
151 for (NJDevice *dev in _devices)
152 if (dev.device == device)
153 return dev;
154 return nil;
155 }
156
157 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
158 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
159 [controller removeDeviceForDevice:device];
160 }
161
162 - (void)removeDeviceForDevice:(IOHIDDeviceRef)device {
163 NJDevice *match = [self findDeviceByRef:device];
164 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
165 if (match) {
166 [_devices removeObject:match];
167 [outlineView reloadData];
168 connectDevicePrompt.hidden = !!_devices.count;
169 }
170 if (_devices.count == 1)
171 [outlineView expandItem:_devices[0]];
172 }
173
174 - (void)updateContinuousOutputs:(NSTimer *)timer {
175 self.mouseLoc = [NSEvent mouseLocation];
176 for (NJOutput *output in [_continousOutputs copy]) {
177 if (![output update:self]) {
178 [_continousOutputs removeObject:output];
179 }
180 }
181 if (!_continousOutputs.count) {
182 [_continuousOutputsTick invalidate];
183 _continuousOutputsTick = nil;
184 }
185 }
186
187 #define NSSTR(e) ((NSString *)CFSTR(e))
188
189 - (void)openHid {
190 if (_hidManager)
191 return;
192 NSLog(@"Opening HID manager.");
193 _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
194 IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
195 NSArray *criteria = @[ @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
196 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
197 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
198 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
199 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
200 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
201 ];
202 IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)criteria);
203 IOReturn ret = IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone);
204 if (ret != kIOReturnSuccess) {
205 [[NSAlert alertWithMessageText:@"Input devices are unavailable"
206 defaultButton:nil
207 alternateButton:nil
208 otherButton:nil
209 informativeTextWithFormat:@"Error 0x%08x occured trying to access your devices. "
210 @"Input may not be correctly detected or mapped.",
211 ret]
212 beginSheetModalForWindow:outlineView.window
213 modalDelegate:nil
214 didEndSelector:nil
215 contextInfo:nil];
216 [self closeHid];
217 } else {
218 IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, add_callback, (__bridge void *)self);
219 IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, remove_callback, (__bridge void *)self);
220 }
221 }
222
223 - (void)closeHid {
224 if (_hidManager) {
225 NSLog(@"Closing HID manager.");
226 IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
227 IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
228 CFRelease(_hidManager);
229 _hidManager = NULL;
230 }
231 [_devices removeAllObjects];
232 [outlineView reloadData];
233 connectDevicePrompt.hidden = !!_devices.count;
234 }
235
236 - (NJInput *)selectedInput {
237 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
238 return (!item.children && item.base) ? item : nil;
239 }
240
241 - (NSInteger)outlineView:(NSOutlineView *)outlineView
242 numberOfChildrenOfItem:(id <NJInputPathElement>)item {
243 return item ? item.children.count : _devices.count;
244 }
245
246 - (BOOL)outlineView:(NSOutlineView *)outlineView
247 isItemExpandable:(id <NJInputPathElement>)item {
248 return item ? [[item children] count] > 0: YES;
249 }
250
251 - (id)outlineView:(NSOutlineView *)outlineView
252 child:(NSInteger)index
253 ofItem:(id <NJInputPathElement>)item {
254 return item ? item.children[index] : _devices[index];
255 }
256
257 - (id)outlineView:(NSOutlineView *)outlineView
258 objectValueForTableColumn:(NSTableColumn *)tableColumn
259 byItem:(id <NJInputPathElement>)item {
260 return item ? item.name : @"root";
261 }
262
263 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
264 [outputController loadCurrent];
265 }
266
267 - (BOOL)outlineView:(NSOutlineView *)outlineView
268 isGroupItem:(id <NJInputPathElement>)item {
269 return [item isKindOfClass:NJDevice.class];
270 }
271
272 - (BOOL)outlineView:(NSOutlineView *)outlineView_
273 shouldSelectItem:(id <NJInputPathElement>)item {
274 return ![self outlineView:outlineView_ isGroupItem:item];
275 }
276
277 - (void)setTranslatingEvents:(BOOL)translatingEvents {
278 if (translatingEvents != _translatingEvents) {
279 _translatingEvents = translatingEvents;
280 NSInteger state = translatingEvents ? NSOnState : NSOffState;
281 translatingEventsButton.state = state;
282 NSString *name = translatingEvents
283 ? NJEventTranslationActivated
284 : NJEventTranslationDeactivated;
285 [NSNotificationCenter.defaultCenter postNotificationName:name
286 object:self];
287
288 if (!translatingEvents && !NSApplication.sharedApplication.isActive)
289 [self closeHid];
290 else if (translatingEvents || NSApplication.sharedApplication.isActive)
291 [self openHid];
292 }
293 }
294
295 - (void)closeHidIfDisabled:(NSNotification *)application {
296 if (!self.translatingEvents)
297 [self closeHid];
298 }
299
300 - (void)applicationDidFinishLaunching:(NSNotification *)application {
301 // NSApplicationWillBecomeActiveNotification occurs just slightly
302 // too late - there's one tick where the UI is showing "No
303 // devices" even with a device plugged in.
304 [self openHid];
305 }
306
307 - (IBAction)translatingEventsChanged:(NSButton *)sender {
308 self.translatingEvents = sender.state == NSOnState;
309 }
310
311 @end