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