X-Git-Url: https://git.yukkurigames.com/?p=enjoyable.git;a=blobdiff_plain;f=Classes%2FNJDeviceController.m;h=610c07b5a4f29c925572a850209c3b4e3d6bfc9d;hp=5a6603c9bac55f5ffc92572e41971d2232d290c8;hb=402a1b679ced5422e46c7a5caeecc45e5ed878db;hpb=1d9578185de7fb08cf1f4b1e42812e87d8e18040 diff --git a/Classes/NJDeviceController.m b/Classes/NJDeviceController.m index 5a6603c..610c07b 100644 --- a/Classes/NJDeviceController.m +++ b/Classes/NJDeviceController.m @@ -16,29 +16,69 @@ #import "NJEvents.h" @implementation NJDeviceController { - IOHIDManagerRef _hidManager; + NJHIDManager *_hidManager; NSTimer *_continuousOutputsTick; NSMutableArray *_continousOutputs; NSMutableArray *_devices; + NSMutableArray *_expanded; } +#define EXPANDED_MEMORY_MAX_SIZE 100 +#define NSSTR(e) ((NSString *)CFSTR(e)) + - (id)init { if ((self = [super init])) { _devices = [[NSMutableArray alloc] initWithCapacity:16]; _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32]; + + NSArray *expanded = [NSUserDefaults.standardUserDefaults objectForKey:@"expanded rows"]; + if (![expanded isKindOfClass:NSArray.class]) + expanded = @[]; + _expanded = [[NSMutableArray alloc] initWithCapacity:MAX(16, _expanded.count)]; + [_expanded addObjectsFromArray:expanded]; + + _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]; + + [NSNotificationCenter.defaultCenter + addObserver:self + selector:@selector(startHid) + name:NSApplicationDidFinishLaunchingNotification + object:nil]; + + // 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 translation 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(setup) - name:NSApplicationDidFinishLaunchingNotification + 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]; - IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone); - CFRelease(_hidManager); } - (void)expandRecursive:(id )pathElement { @@ -48,8 +88,24 @@ } } +- (id)elementForUID:(NSString *)uid { + for (NJDevice *dev in _devices) { + id item = [dev elementForUID:uid]; + if (item) + return item; + } + return nil; +} + +- (void)expandRecursiveByUID:(NSString *)uid { + [self expandRecursive:[self elementForUID:uid]]; +} + - (void)addRunningOutput:(NJOutput *)output { - [_continousOutputs addObject: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 @@ -85,14 +141,13 @@ [outputController focusKey]; } -static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) { - NJDeviceController *controller = (__bridge NJDeviceController *)ctx; - IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender); - - if (controller.translatingEvents) { - [controller runOutputForDevice:device value:value]; - } else if ([NSApplication sharedApplication].mainWindow.isVisible) { - [controller showOutputForDevice:device value:value]; +- (void)hidManager:(NJHIDManager *)manager + valueChanged:(IOHIDValueRef)value + fromDevice:(IOHIDDeviceRef)device { + if (self.translatingEvents) { + [self runOutputForDevice:device value:value]; + } else { + [self showOutputForDevice:device value:value]; } } @@ -100,7 +155,8 @@ static int findAvailableIndex(NSArray *list, NJDevice *dev) { for (int index = 1; ; index++) { BOOL available = YES; for (NJDevice *used in list) { - if ([used.productName isEqualToString:dev.productName] && used.index == index) { + if ([used.productName isEqualToString:dev.productName] + && used.index == index) { available = NO; break; } @@ -110,20 +166,16 @@ static int findAvailableIndex(NSArray *list, NJDevice *dev) { } } -- (void)addDeviceForDevice:(IOHIDDeviceRef)device { - IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void *)self); - NJDevice *dev = [[NJDevice alloc] initWithDevice:device]; - dev.index = findAvailableIndex(_devices, dev); - [_devices addObject:dev]; +- (void)hidManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device { + NJDevice *match = [[NJDevice alloc] initWithDevice:device]; + match.index = findAvailableIndex(_devices, match); + [_devices addObject:match]; [outlineView reloadData]; + [self reexpandAll]; + hidSleepingPrompt.hidden = YES; connectDevicePrompt.hidden = !!_devices.count; } -static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) { - NJDeviceController *controller = (__bridge NJDeviceController *)ctx; - [controller addDeviceForDevice:device]; -} - - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device { for (NJDevice *dev in _devices) if (dev.device == device) @@ -131,18 +183,14 @@ static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDevi return nil; } -static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) { - NJDeviceController *controller = (__bridge NJDeviceController *)ctx; - [controller removeDeviceForDevice:device]; -} - -- (void)removeDeviceForDevice:(IOHIDDeviceRef)device { +- (void)hidManager:(NJHIDManager *)manager deviceRemoved:(IOHIDDeviceRef)device { NJDevice *match = [self findDeviceByRef:device]; IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL); if (match) { [_devices removeObject:match]; [outlineView reloadData]; connectDevicePrompt.hidden = !!_devices.count; + hidSleepingPrompt.hidden = YES; } if (_devices.count == 1) [outlineView expandItem:_devices[0]]; @@ -161,37 +209,32 @@ static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDD } } -#define NSSTR(e) ((NSString *)CFSTR(e)) +- (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error { + [outlineView.window presentError:error + modalForWindow:outlineView.window + delegate:nil + didPresentSelector:nil + contextInfo:nil]; +} -- (void)setup { - _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); - NSArray *criteria = @[ @{ 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) } - ]; - IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)criteria); - - IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - IOReturn ret = IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone); - if (ret != kIOReturnSuccess) { - [[NSAlert alertWithMessageText:@"Input devices are unavailable" - defaultButton:nil - alternateButton:nil - otherButton:nil - informativeTextWithFormat:@"Error 0x%08x occured trying to access your devices. " - @"Input may not be correctly detected or mapped.", - ret] - beginSheetModalForWindow:outlineView.window - modalDelegate:nil - didEndSelector:nil - contextInfo:nil]; - } - - IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, add_callback, (__bridge void *)self); - IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, remove_callback, (__bridge void *)self); +- (void)hidManagerDidStart:(NJHIDManager *)manager { + hidSleepingPrompt.hidden = YES; + connectDevicePrompt.hidden = !!_devices.count; +} + +- (void)hidManagerDidStop:(NJHIDManager *)manager { + [_devices removeAllObjects]; + [outlineView reloadData]; + hidSleepingPrompt.hidden = NO; + connectDevicePrompt.hidden = YES; +} + +- (void)startHid { + [_hidManager start]; +} + +- (void)stopHid { + [_hidManager stop]; } - (NJInput *)selectedInput { @@ -222,6 +265,10 @@ objectValueForTableColumn:(NSTableColumn *)tableColumn } - (void)outlineViewSelectionDidChange:(NSNotification *)notification { + id item = [outlineView itemAtRow:outlineView.selectedRow]; + if (item) + [NSUserDefaults.standardUserDefaults setObject:item.uid + forKey:@"selected input"]; [outputController loadCurrent]; } @@ -235,23 +282,61 @@ objectValueForTableColumn:(NSTableColumn *)tableColumn return ![self outlineView:outlineView_ isGroupItem:item]; } +- (void)outlineViewItemDidExpand:(NSNotification *)notification { + id item = notification.userInfo[@"NSObject"]; + NSString *uid = item.uid; + if (![_expanded containsObject:uid]) + [_expanded addObject:uid]; + while (_expanded.count > EXPANDED_MEMORY_MAX_SIZE) + [_expanded removeObjectAtIndex:0]; + [NSUserDefaults.standardUserDefaults setObject:_expanded + forKey:@"expanded rows"]; +} + +- (void)outlineViewItemDidCollapse:(NSNotification *)notification { + id item = notification.userInfo[@"NSObject"]; + [_expanded removeObject:item.uid]; + [NSUserDefaults.standardUserDefaults setObject:_expanded + forKey:@"expanded rows"]; +} + - (void)setTranslatingEvents:(BOOL)translatingEvents { if (translatingEvents != _translatingEvents) { _translatingEvents = translatingEvents; NSInteger state = translatingEvents ? NSOnState : NSOffState; translatingEventsButton.state = state; - translatingEventsMenu.title = translatingEvents ? @"Disable" : @"Enable"; NSString *name = translatingEvents ? NJEventTranslationActivated : NJEventTranslationDeactivated; [NSNotificationCenter.defaultCenter postNotificationName:name object:self]; + + if (!translatingEvents && !NSApplication.sharedApplication.isActive) + [self stopHid]; + else if (translatingEvents || NSApplication.sharedApplication.isActive) + [self startHid]; + } +} + +- (void)reexpandAll { + for (NSString *uid in [_expanded copy]) + [self expandRecursiveByUID:uid]; + if (outlineView.selectedRow == -1) { + NSString *selectedUid = [NSUserDefaults.standardUserDefaults objectForKey:@"selected input"]; + id item = [self elementForUID:selectedUid]; + NSInteger row = [outlineView rowForItem:item]; + if (row >= 0) + [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO]; } } +- (void)stopHidIfDisabled:(NSNotification *)application { + if (!self.translatingEvents) + [self stopHid]; +} + - (IBAction)translatingEventsChanged:(NSButton *)sender { self.translatingEvents = sender.state == NSOnState; } - @end