X-Git-Url: https://git.yukkurigames.com/?p=enjoyable.git;a=blobdiff_plain;f=Classes%2FNJInputController.m;fp=Classes%2FNJInputController.m;h=4ca33e1a1bc06dcfd1a940b8197bf152bda11802;hp=0000000000000000000000000000000000000000;hb=93d9951d6bd16b37e88b8fb6c38bc4afd6c9401e;hpb=6a8c6539cc3f7a4b32a9f226da6ae06074e4f0cb diff --git a/Classes/NJInputController.m b/Classes/NJInputController.m new file mode 100644 index 0000000..4ca33e1 --- /dev/null +++ b/Classes/NJInputController.m @@ -0,0 +1,363 @@ +// +// NJDeviceController.m +// Enjoy +// +// Created by Sam McCall on 4/05/09. +// + +#import "NJInputController.h" + +#import "NJMapping.h" +#import "NJDevice.h" +#import "NJInput.h" +#import "NJOutput.h" +#import "NJEvents.h" + +@implementation NJInputController { + NJHIDManager *_hidManager; + NSTimer *_continuousOutputsTick; + NSMutableArray *_continousOutputs; + NSMutableArray *_devices; + NSMutableArray *_mappings; + NJMapping *_manualMapping; + +} + +#define NSSTR(e) ((NSString *)CFSTR(e)) + +- (id)init { + if ((self = [super init])) { + _devices = [[NSMutableArray alloc] initWithCapacity:16]; + _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32]; + + _hidManager = [[NJHIDManager alloc] initWithCriteria:@[ + @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop), + NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) }, + @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop), + NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) }, + @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop), + NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) } + ] + delegate:self]; + + _mappings = [[NSMutableArray alloc] init]; + _currentMapping = [[NJMapping alloc] initWithName: + NSLocalizedString(@"(default)", @"default name for first the mapping")]; + _manualMapping = _currentMapping; + [_mappings addObject:_currentMapping]; + + // The HID manager uses 5-10ms per second doing basically + // nothing if a noisy device is plugged in (the % of that + // spent in input_callback is negligible, so it's not + // something we can make faster). I don't really think that's + // acceptable, CPU/power wise. So if simulation is disabled + // and the window is closed, just switch off the HID manager + // entirely. This probably also has some marginal benefits for + // compatibility with other applications that want exclusive + // grabs. + [NSNotificationCenter.defaultCenter + addObserver:self + selector:@selector(stopHidIfDisabled:) + name:NSApplicationDidResignActiveNotification + object:nil]; + [NSNotificationCenter.defaultCenter + addObserver:self + selector:@selector(startHid) + name:NSApplicationDidBecomeActiveNotification + object:nil]; + } + return self; +} + +- (void)dealloc { + [NSNotificationCenter.defaultCenter removeObserver:self]; + [_continuousOutputsTick invalidate]; +} + +- (void)addRunningOutput:(NJOutput *)output { + // Axis events will trigger every small movement, don't keep + // re-adding them or they trigger multiple times each time. + if (![_continousOutputs containsObject:output]) + [_continousOutputs addObject:output]; + if (!_continuousOutputsTick) { + _continuousOutputsTick = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0 + target:self + selector:@selector(updateContinuousOutputs:) + userInfo:nil + repeats:YES]; + } +} + +- (void)runOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value { + NJDevice *dev = [self findDeviceByRef:device]; + NJInput *mainInput = [dev inputForEvent:value]; + [mainInput notifyEvent:value]; + NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[]; + for (NJInput *subInput in children) { + NJOutput *output = self.currentMapping[subInput]; + output.magnitude = subInput.magnitude; + output.running = subInput.active; + if ((output.running || output.magnitude) && output.isContinuous) + [self addRunningOutput:output]; + } +} + +- (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value { + NJDevice *dev = [self findDeviceByRef:device]; + NJInput *handler = [dev handlerForEvent:value]; + if (!handler) + return; + + [self.delegate deviceController:self didInput:handler]; +} + +- (void)hidManager:(NJHIDManager *)manager + valueChanged:(IOHIDValueRef)value + fromDevice:(IOHIDDeviceRef)device { + if (self.simulatingEvents + && !NSApplication.sharedApplication.isActive) { + [self runOutputForDevice:device value:value]; + } else { + [self showOutputForDevice:device value:value]; + } +} + +- (void)addDevice:(NJDevice *)device { + BOOL available; + do { + available = YES; + for (NJDevice *used in _devices) { + if ([used isEqual:device]) { + device.index += 1; + available = NO; + } + } + } while (!available); + + [_devices addObject:device]; +} + +- (void)hidManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device { + NJDevice *match = [[NJDevice alloc] initWithDevice:device]; + [self addDevice:match]; + [self.delegate deviceController:self didAddDevice:match]; +} + +- (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device { + for (NJDevice *dev in _devices) + if (dev.device == device) + return dev; + return nil; +} + +- (void)hidManager:(NJHIDManager *)manager deviceRemoved:(IOHIDDeviceRef)device { + NJDevice *match = [self findDeviceByRef:device]; + if (match) { + NSInteger idx = [_devices indexOfObjectIdenticalTo:match]; + [_devices removeObjectAtIndex:idx]; + [self.delegate deviceController:self didRemoveDeviceAtIndex:idx]; + } +} + +- (void)updateContinuousOutputs:(NSTimer *)timer { + self.mouseLoc = [NSEvent mouseLocation]; + for (NJOutput *output in [_continousOutputs copy]) { + if (![output update:self]) { + [_continousOutputs removeObject:output]; + } + } + if (!_continousOutputs.count) { + [_continuousOutputsTick invalidate]; + _continuousOutputsTick = nil; + } +} + +- (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error { + [self.delegate deviceController:self didError:error]; + self.simulatingEvents = NO; +} + +- (void)hidManagerDidStart:(NJHIDManager *)manager { + [self.delegate deviceControllerDidStartHID:self]; +} + +- (void)hidManagerDidStop:(NJHIDManager *)manager { + [_devices removeAllObjects]; + [self.delegate deviceControllerDidStopHID:self]; +} + +- (void)startHid { + [_hidManager start]; +} + +- (void)stopHid { + [_hidManager stop]; +} + +- (void)setSimulatingEvents:(BOOL)simulatingEvents { + if (simulatingEvents != _simulatingEvents) { + _simulatingEvents = simulatingEvents; + NSString *name = simulatingEvents + ? NJEventSimulationStarted + : NJEventSimulationStopped; + [NSNotificationCenter.defaultCenter postNotificationName:name + object:self]; + + if (!simulatingEvents && !NSApplication.sharedApplication.isActive) + [self stopHid]; + else + [self startHid]; + } +} + +- (void)stopHidIfDisabled:(NSNotification *)application { + if (!self.simulatingEvents && !NSProcessInfo.processInfo.isBeingDebugged) + [self stopHid]; +} + +- (NJInputPathElement *)elementForUID:(NSString *)uid { + for (NJDevice *dev in _devices) { + id item = [dev elementForUID:uid]; + if (item) + return item; + } + return nil; +} + +- (NJMapping *)mappingForKey:(NSString *)name { + for (NJMapping *mapping in _mappings) + if ([name isEqualToString:mapping.name]) + return mapping; + return nil; +} + +- (void)mappingsSet { + [self postLoadProcess]; + [NSNotificationCenter.defaultCenter + postNotificationName:NJEventMappingListChanged + object:self + userInfo:@{ NJMappingListKey: _mappings, + NJMappingKey: _currentMapping }]; +} + +- (void)mappingsChanged { + [self save]; + [self mappingsSet]; +} + +- (void)activateMappingForProcess:(NSRunningApplication *)app { + NJMapping *oldMapping = _manualMapping; + NSArray *names = app.possibleMappingNames; + BOOL found = NO; + for (NSString *name in names) { + NJMapping *mapping = [self mappingForKey:name]; + if (mapping) { + [self activateMapping:mapping]; + found = YES; + break; + } + } + + if (!found) { + [self activateMapping:oldMapping]; + if ([oldMapping.name.lowercaseString isEqualToString:@"@application"] + || [oldMapping.name.lowercaseString isEqualToString: + NSLocalizedString(@"@Application", nil).lowercaseString]) { + oldMapping.name = app.bestMappingName; + [self mappingsChanged]; + } + } + _manualMapping = oldMapping; +} + +- (void)activateMappingForcibly:(NJMapping *)mapping { + NSLog(@"Switching to mapping %@.", mapping.name); + _currentMapping = mapping; + NSUInteger idx = [self indexOfMapping:_currentMapping]; + [NSNotificationCenter.defaultCenter + postNotificationName:NJEventMappingChanged + object:self + userInfo:@{ NJMappingKey : _currentMapping, + NJMappingIndexKey: @(idx) }]; +} + +- (void)activateMapping:(NJMapping *)mapping { + if (!mapping) + mapping = _manualMapping; + if (mapping == _currentMapping) + return; + _manualMapping = mapping; + [self activateMappingForcibly:mapping]; +} + +- (void)save { + NSLog(@"Saving mappings to defaults."); + NSMutableArray *ary = [[NSMutableArray alloc] initWithCapacity:_mappings.count]; + for (NJMapping *mapping in _mappings) + [ary addObject:[mapping serialize]]; + [NSUserDefaults.standardUserDefaults setObject:ary forKey:@"mappings"]; +} + +- (void)postLoadProcess { + for (NJMapping *mapping in self.mappings) + [mapping postLoadProcess:self.mappings]; +} + +- (void)load { + NSUInteger selected = [NSUserDefaults.standardUserDefaults integerForKey:@"selected"]; + NSArray *storedMappings = [NSUserDefaults.standardUserDefaults arrayForKey:@"mappings"]; + NSMutableArray* newMappings = [[NSMutableArray alloc] initWithCapacity:storedMappings.count]; + + for (NSDictionary *serialization in storedMappings) + [newMappings addObject: + [[NJMapping alloc] initWithSerialization:serialization]]; + + if (newMappings.count) { + _mappings = newMappings; + if (selected >= newMappings.count) + selected = 0; + [self activateMapping:_mappings[selected]]; + [self mappingsSet]; + } +} + +- (NSInteger)indexOfMapping:(NJMapping *)mapping { + return [_mappings indexOfObjectIdenticalTo:mapping]; +} + +- (void)mergeMapping:(NJMapping *)mapping intoMapping:(NJMapping *)existing { + [existing mergeEntriesFrom:mapping]; + [self mappingsChanged]; + if (existing == _currentMapping) + [self activateMappingForcibly:mapping]; +} + +- (void)renameMapping:(NJMapping *)mapping to:(NSString *)name { + mapping.name = name; + [self mappingsChanged]; + if (mapping == _currentMapping) + [self activateMappingForcibly:mapping]; +} + +- (void)addMapping:(NJMapping *)mapping { + [self insertMapping:mapping atIndex:_mappings.count]; +} + +- (void)insertMapping:(NJMapping *)mapping atIndex:(NSInteger)idx { + [_mappings insertObject:mapping atIndex:idx]; + [self mappingsChanged]; +} + +- (void)removeMappingAtIndex:(NSInteger)idx { + NSInteger currentIdx = [self indexOfMapping:_currentMapping]; + [_mappings removeObjectAtIndex:idx]; + [self activateMapping:self.mappings[MIN(currentIdx, _mappings.count - 1)]]; + [self mappingsChanged]; +} + +- (void)moveMoveMappingFromIndex:(NSInteger)fromIdx toIndex:(NSInteger)toIdx { + [_mappings moveObjectAtIndex:fromIdx toIndex:toIdx]; + [self mappingsChanged]; +} + +@end