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