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