Fix for dropping input events in OS X 10.10.
[enjoyable.git] / Classes / NJInputController.m
1 //
2 // NJInputController.m
3 // Enjoyable
4 //
5
6 #import "NJInputController.h"
7
8 #import "NJMapping.h"
9 #import "NJDevice.h"
10 #import "NJInput.h"
11 #import "NJOutput.h"
12 #import "NJEvents.h"
13
14 #import <CoreVideo/CoreVideo.h>
15
16 @interface NJInputController ()
17
18 - (void)updateContinuousOutputs;
19
20 @end
21
22 static CVReturn _updateDL(CVDisplayLinkRef displayLink,
23 const CVTimeStamp *inNow,
24 const CVTimeStamp *inOutputTime,
25 CVOptionFlags flagsIn,
26 CVOptionFlags *flagsOut,
27 void *ctxManager) {
28 NJInputController *manager = (__bridge NJInputController *)ctxManager;
29 [manager performSelectorOnMainThread:@selector(updateContinuousOutputs)
30 withObject:nil
31 waitUntilDone:NO];
32 return kCVReturnSuccess;
33 }
34
35 @implementation NJInputController {
36 NJHIDManager *_HIDManager;
37 NSMutableArray *_continousOutputs;
38 NSMutableArray *_devices;
39 NSMutableArray *_mappings;
40 NJMapping *_manualMapping;
41 CVDisplayLinkRef _displayLink;
42 }
43
44 #define NSSTR(e) ((NSString *)CFSTR(e))
45
46 - (id)init {
47 if ((self = [super init])) {
48 _devices = [[NSMutableArray alloc] initWithCapacity:16];
49 _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
50
51 CVReturn cvErr = CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
52 if (cvErr) {
53 NSError *error = [NSError errorWithDomain:NSCocoaErrorDomain
54 code:cvErr
55 userInfo:nil];
56 [self.delegate inputController:self didError:error];
57 NSLog(@"DisplayLink failed creation with error: %@", error);
58 _displayLink = NULL;
59 }
60 CVDisplayLinkSetOutputCallback(_displayLink, _updateDL, (__bridge void *)self);
61
62 _HIDManager = [[NJHIDManager alloc] initWithCriteria:@[
63 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
64 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
65 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
66 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
67 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
68 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
69 ]
70 delegate:self];
71
72 _mappings = [[NSMutableArray alloc] init];
73 _currentMapping = [[NJMapping alloc] initWithName:
74 NSLocalizedString(@"(default)", @"default name for first the mapping")];
75 _manualMapping = _currentMapping;
76 [_mappings addObject:_currentMapping];
77
78 // The HID manager uses 5-10ms per second doing basically
79 // nothing if a noisy device is plugged in (the % of that
80 // spent in input_callback is negligible, so it's not
81 // something we can make faster). I don't really think that's
82 // acceptable, CPU/power wise. So if simulation is disabled
83 // and the window is closed, just switch off the HID manager
84 // entirely. This probably also has some marginal benefits for
85 // compatibility with other applications that want exclusive
86 // grabs.
87 [NSNotificationCenter.defaultCenter
88 addObserver:self
89 selector:@selector(stopHidIfDisabled:)
90 name:NSApplicationDidResignActiveNotification
91 object:nil];
92 [NSNotificationCenter.defaultCenter
93 addObserver:self
94 selector:@selector(startHid)
95 name:NSApplicationDidBecomeActiveNotification
96 object:nil];
97 }
98 return self;
99 }
100
101 - (void)dealloc {
102 [NSNotificationCenter.defaultCenter removeObserver:self];
103 if (_displayLink) {
104 CVDisplayLinkStop(_displayLink);
105 CVDisplayLinkRelease(_displayLink);
106 }
107 }
108
109 - (void)addRunningOutput:(NJOutput *)output {
110 // Axis events will trigger every small movement, don't keep
111 // re-adding them or they trigger multiple times each time.
112 if (![_continousOutputs containsObject:output])
113 [_continousOutputs addObject:output];
114 if (_displayLink && !CVDisplayLinkIsRunning(_displayLink))
115 CVDisplayLinkStart(_displayLink);
116 }
117
118 - (void)runOutputForValue:(IOHIDValueRef)value {
119 IOHIDElementRef elt = value ? IOHIDValueGetElement(value) : NULL;
120 IOHIDDeviceRef device = elt ? IOHIDElementGetDevice(elt) : NULL;
121 NJDevice *dev = [self findDeviceByRef:device];
122 NJInput *mainInput = [dev inputForEvent:value];
123 [mainInput notifyEvent:value];
124 NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[];
125 for (NJInput *subInput in children) {
126 NJOutput *output = self.currentMapping[subInput];
127 output.magnitude = subInput.magnitude;
128 output.running = subInput.active;
129 if ((output.running || output.magnitude) && output.isContinuous)
130 [self addRunningOutput:output];
131 }
132 }
133
134 - (void)showOutputForValue:(IOHIDValueRef)value {
135 IOHIDElementRef elt = value ? IOHIDValueGetElement(value) : NULL;
136 IOHIDDeviceRef device = elt ? IOHIDElementGetDevice(elt) : NULL;
137 NJDevice *dev = [self findDeviceByRef:device];
138 NJInput *handler = [dev handlerForEvent:value];
139 if (!handler)
140 return;
141
142 [self.delegate inputController:self didInput:handler];
143 }
144
145 - (void)HIDManager:(NJHIDManager *)manager valueChanged:(IOHIDValueRef)value {
146 if (self.simulatingEvents && !NSApplication.sharedApplication.isActive) {
147 [self runOutputForValue:value];
148 } else {
149 [self showOutputForValue:value];
150 }
151 }
152
153 - (void)addDevice:(NJDevice *)device {
154 BOOL available;
155 do {
156 available = YES;
157 for (NJDevice *used in _devices) {
158 if ([used isEqual:device]) {
159 device.index += 1;
160 available = NO;
161 }
162 }
163 } while (!available);
164
165 [_devices addObject:device];
166 }
167
168 - (void)HIDManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device {
169 NJDevice *match = [[NJDevice alloc] initWithDevice:device];
170 [self addDevice:match];
171 [self.delegate inputController:self didAddDevice:match];
172 }
173
174 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
175 for (NJDevice *dev in _devices)
176 if (dev.device == device)
177 return dev;
178 return nil;
179 }
180
181 - (void)HIDManager:(NJHIDManager *)manager deviceRemoved:(IOHIDDeviceRef)device {
182 NJDevice *match = [self findDeviceByRef:device];
183 if (match) {
184 NSInteger idx = [_devices indexOfObjectIdenticalTo:match];
185 [_devices removeObjectAtIndex:idx];
186 [self.delegate inputController:self didRemoveDeviceAtIndex:idx];
187 }
188 }
189
190 - (void)updateContinuousOutputs {
191 self.mouseLoc = [NSEvent mouseLocation];
192 for (NJOutput *output in [_continousOutputs copy]) {
193 if (![output update:self]) {
194 [_continousOutputs removeObject:output];
195 }
196 }
197 if (!_continousOutputs.count && _displayLink) {
198 CVDisplayLinkStop(_displayLink);
199 }
200 }
201
202 - (void)HIDManager:(NJHIDManager *)manager didError:(NSError *)error {
203 [self.delegate inputController:self didError:error];
204 self.simulatingEvents = NO;
205 if (_displayLink)
206 CVDisplayLinkStop(_displayLink);
207 }
208
209 - (void)HIDManagerDidStart:(NJHIDManager *)manager {
210 [self.delegate inputControllerDidStartHID:self];
211 }
212
213 - (void)HIDManagerDidStop:(NJHIDManager *)manager {
214 [_devices removeAllObjects];
215 if (_displayLink)
216 CVDisplayLinkStop(_displayLink);
217 [self.delegate inputControllerDidStopHID:self];
218 }
219
220 - (void)startHid {
221 [_HIDManager start];
222 }
223
224 - (void)stopHid {
225 [_HIDManager stop];
226 }
227
228 - (void)setSimulatingEvents:(BOOL)simulatingEvents {
229 if (simulatingEvents != _simulatingEvents) {
230 _simulatingEvents = simulatingEvents;
231 NSString *name = simulatingEvents
232 ? NJEventSimulationStarted
233 : NJEventSimulationStopped;
234 [NSNotificationCenter.defaultCenter postNotificationName:name
235 object:self];
236
237 if (!simulatingEvents && !NSApplication.sharedApplication.isActive)
238 [self stopHid];
239 else
240 [self startHid];
241 }
242 }
243
244 - (void)stopHidIfDisabled:(NSNotification *)application {
245 if (!self.simulatingEvents && !NSProcessInfo.processInfo.isBeingDebugged)
246 [self stopHid];
247 }
248
249 - (NJInputPathElement *)elementForUID:(NSString *)uid {
250 for (NJDevice *dev in _devices) {
251 id item = [dev elementForUID:uid];
252 if (item)
253 return item;
254 }
255 return nil;
256 }
257
258 - (NJMapping *)mappingForKey:(NSString *)name {
259 for (NJMapping *mapping in _mappings)
260 if ([name isEqualToString:mapping.name])
261 return mapping;
262 return nil;
263 }
264
265 - (void)mappingsSet {
266 [self postLoadProcess];
267 [NSNotificationCenter.defaultCenter
268 postNotificationName:NJEventMappingListChanged
269 object:self
270 userInfo:@{ NJMappingListKey: _mappings,
271 NJMappingKey: _currentMapping }];
272 }
273
274 - (void)mappingsChanged {
275 [self save];
276 [self mappingsSet];
277 }
278
279 - (void)activateMappingForProcess:(NSRunningApplication *)app {
280 NJMapping *oldMapping = _manualMapping;
281 NSArray *names = app.possibleMappingNames;
282 BOOL found = NO;
283 for (NSString *name in names) {
284 NJMapping *mapping = [self mappingForKey:name];
285 if (mapping) {
286 [self activateMapping:mapping];
287 found = YES;
288 break;
289 }
290 }
291
292 if (!found) {
293 [self activateMapping:oldMapping];
294 if ([oldMapping.name.lowercaseString isEqualToString:@"@application"]
295 || [oldMapping.name.lowercaseString isEqualToString:
296 NSLocalizedString(@"@Application", nil).lowercaseString]) {
297 [self renameMapping:oldMapping to:app.bestMappingName];
298 }
299 }
300 _manualMapping = oldMapping;
301 }
302
303 - (void)activateMappingForcibly:(NJMapping *)mapping {
304 NSLog(@"Switching to mapping %@.", mapping.name);
305 _currentMapping = mapping;
306 NSUInteger idx = [self indexOfMapping:_currentMapping];
307 [NSNotificationCenter.defaultCenter
308 postNotificationName:NJEventMappingChanged
309 object:self
310 userInfo:@{ NJMappingKey : _currentMapping,
311 NJMappingIndexKey: @(idx) }];
312 }
313
314 - (void)activateMapping:(NJMapping *)mapping {
315 if (!mapping)
316 mapping = _manualMapping;
317 if (mapping == _currentMapping)
318 return;
319 _manualMapping = mapping;
320 [self activateMappingForcibly:mapping];
321 }
322
323 - (void)save {
324 NSLog(@"Saving mappings to defaults.");
325 NSMutableArray *ary = [[NSMutableArray alloc] initWithCapacity:_mappings.count];
326 for (NJMapping *mapping in _mappings)
327 [ary addObject:[mapping serialize]];
328 [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"];
329 }
330
331 - (void)postLoadProcess {
332 for (NJMapping *mapping in self.mappings)
333 [mapping postLoadProcess:self.mappings];
334 }
335
336 - (void)load {
337 NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"];
338 NSArray *storedMappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"];
339 NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count];
340
341 for (NSDictionary *serialization in storedMappings)
342 [newMappings addObject:
343 [[NJMapping alloc] initWithSerialization:serialization]];
344
345 if (newMappings.count) {
346 _mappings = newMappings;
347 if (selected >= newMappings.count)
348 selected = 0;
349 [self activateMapping:_mappings[selected]];
350 [self mappingsSet];
351 }
352 }
353
354 - (NSInteger)indexOfMapping:(NJMapping *)mapping {
355 return [_mappings indexOfObjectIdenticalTo:mapping];
356 }
357
358 - (void)mergeMapping:(NJMapping *)mapping intoMapping:(NJMapping *)existing {
359 [existing mergeEntriesFrom:mapping];
360 [self mappingsChanged];
361 if (existing == _currentMapping)
362 [self activateMappingForcibly:mapping];
363 }
364
365 - (void)renameMapping:(NJMapping *)mapping to:(NSString *)name {
366 mapping.name = name;
367 [self mappingsChanged];
368 if (mapping == _currentMapping)
369 [self activateMappingForcibly:mapping];
370 }
371
372 - (void)addMapping:(NJMapping *)mapping {
373 [self insertMapping:mapping atIndex:_mappings.count];
374 }
375
376 - (void)insertMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
377 [_mappings insertObject:mapping atIndex:idx];
378 [self mappingsChanged];
379 }
380
381 - (void)removeMappingAtIndex:(NSInteger)idx {
382 NSInteger currentIdx = [self indexOfMapping:_currentMapping];
383 [_mappings removeObjectAtIndex:idx];
384 NSInteger activeIdx = MIN(currentIdx, (NSInteger)_mappings.count - 1);
385 [self activateMapping:self.mappings[activeIdx]];
386 [self mappingsChanged];
387 }
388
389 - (void)moveMoveMappingFromIndex:(NSInteger)fromIdx toIndex:(NSInteger)toIdx {
390 [_mappings moveObjectAtIndex:fromIdx toIndex:toIdx];
391 [self mappingsChanged];
392 }
393
394 @end