X-Git-Url: https://git.yukkurigames.com/?p=enjoyable.git;a=blobdiff_plain;f=Classes%2FNJDeviceController.m;h=2a1546c974c8ce7a64b352fe5eec2ecdc8a35d17;hp=0ed8bc697ad3efdbfb602d54aa68f80003415bf4;hb=8fa589c4e6be7272402952c4f929f81763700212;hpb=18160be57e656a3733fc29878caddcda5081a2c2 diff --git a/Classes/NJDeviceController.m b/Classes/NJDeviceController.m index 0ed8bc6..2a1546c 100644 --- a/Classes/NJDeviceController.m +++ b/Classes/NJDeviceController.m @@ -16,24 +16,59 @@ #import "NJEvents.h" @implementation NJDeviceController { - IOHIDManagerRef hidManager; - NSTimer *continuousTimer; - NSMutableArray *runningOutputs; + IOHIDManagerRef _hidManager; + NSTimer *_continuousOutputsTick; + NSMutableArray *_continousOutputs; NSMutableArray *_devices; + NSMutableArray *_expanded; } +#define EXPANDED_MEMORY_MAX_SIZE 100 + - (id)init { if ((self = [super init])) { _devices = [[NSMutableArray alloc] initWithCapacity:16]; - runningOutputs = [[NSMutableArray alloc] initWithCapacity:32]; + _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]; + + [NSNotificationCenter.defaultCenter + addObserver:self + selector:@selector(applicationDidFinishLaunching:) + 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(closeHidIfDisabled:) + name:NSApplicationDidResignActiveNotification + object:nil]; + [NSNotificationCenter.defaultCenter + addObserver:self + selector:@selector(openHid) + name:NSApplicationDidBecomeActiveNotification + object:nil]; } return self; } - (void)dealloc { - [continuousTimer invalidate]; - IOHIDManagerClose(hidManager, kIOHIDOptionsTypeNone); - CFRelease(hidManager); + [NSNotificationCenter.defaultCenter removeObserver:self]; + [_continuousOutputsTick invalidate]; + [self closeHid]; } - (void)expandRecursive:(id )pathElement { @@ -43,17 +78,30 @@ } } -- (void)addRunningOutput:(NJOutput *)output { - if (![runningOutputs containsObject:output]) { - [runningOutputs addObject:output]; +- (id)elementForUID:(NSString *)uid { + for (NJDevice *dev in _devices) { + id item = [dev elementForUID:uid]; + if (item) + return item; } - if (!continuousTimer) { - continuousTimer = [NSTimer scheduledTimerWithTimeInterval:1.f/60.f + return nil; +} + +- (void)expandRecursiveByUID:(NSString *)uid { + [self expandRecursive:[self elementForUID:uid]]; +} + +- (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(updateContinuousInputs:) + selector:@selector(updateContinuousOutputs:) userInfo:nil repeats:YES]; - NSLog(@"Scheduled continuous output timer."); } } @@ -78,7 +126,8 @@ return; [self expandRecursive:handler]; - [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]] byExtendingSelection: NO]; + [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]] + byExtendingSelection: NO]; [outputController focusKey]; } @@ -108,12 +157,14 @@ static int findAvailableIndex(NSArray *list, NJDevice *dev) { } - (void)addDeviceForDevice:(IOHIDDeviceRef)device { - IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void*)self); + IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void *)self); NJDevice *dev = [[NJDevice alloc] initWithDevice:device]; dev.index = findAvailableIndex(_devices, dev); [_devices addObject:dev]; [outlineView reloadData]; - [connectDevicePrompt setHidden:!!_devices.count]; + [self reexpandAll]; + hidSleepingPrompt.hidden = YES; + connectDevicePrompt.hidden = !!_devices.count; } static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) { @@ -139,29 +190,34 @@ static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDD if (match) { [_devices removeObject:match]; [outlineView reloadData]; - [connectDevicePrompt setHidden:!!_devices.count]; + connectDevicePrompt.hidden = !!_devices.count; + hidSleepingPrompt.hidden = YES; } - + if (_devices.count == 1) + [outlineView expandItem:_devices[0]]; } -- (void)updateContinuousInputs:(NSTimer *)timer { +- (void)updateContinuousOutputs:(NSTimer *)timer { self.mouseLoc = [NSEvent mouseLocation]; - for (NJOutput *output in [runningOutputs copy]) { + for (NJOutput *output in [_continousOutputs copy]) { if (![output update:self]) { - [runningOutputs removeObject:output]; + [_continousOutputs removeObject:output]; } } - if (!runningOutputs.count) { - [continuousTimer invalidate]; - continuousTimer = nil; - NSLog(@"Unscheduled continuous output timer."); + if (!_continousOutputs.count) { + [_continuousOutputsTick invalidate]; + _continuousOutputsTick = nil; } } #define NSSTR(e) ((NSString *)CFSTR(e)) -- (void)setup { - hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); +- (void)openHid { + if (_hidManager) + return; + NSLog(@"Opening HID manager."); + _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone); + IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); NSArray *criteria = @[ @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop), NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) }, @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop), @@ -169,26 +225,41 @@ static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDD @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop), NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) } ]; - IOHIDManagerSetDeviceMatchingMultiple(hidManager, (__bridge CFArrayRef)criteria); - - IOHIDManagerScheduleWithRunLoop(hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); - IOReturn ret = IOHIDManagerOpen(hidManager, kIOHIDOptionsTypeNone); + IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)criteria); + IOReturn ret = IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone); if (ret != kIOReturnSuccess) { - [[NSAlert alertWithMessageText:@"Input devices are unavailable" + [[NSAlert alertWithMessageText:NSLocalizedString(@"input devices unavailable", + @"error title when devices can't be read") 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]; + informativeTextWithFormat:NSLocalizedString(@"input error 0x%08x occurred", + @"message containing IOReturn failure code when devices can't be read"), ret] + beginSheetModalForWindow:outlineView.window + modalDelegate:nil + didEndSelector:nil + contextInfo:nil]; + [self closeHid]; + } else { + IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, add_callback, (__bridge void *)self); + IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, remove_callback, (__bridge void *)self); + hidSleepingPrompt.hidden = YES; + connectDevicePrompt.hidden = !!_devices.count; } - - IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self); - IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self); +} + +- (void)closeHid { + if (_hidManager) { + NSLog(@"Closing HID manager."); + IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode); + IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone); + CFRelease(_hidManager); + _hidManager = NULL; + } + [_devices removeAllObjects]; + [outlineView reloadData]; + hidSleepingPrompt.hidden = NO; + connectDevicePrompt.hidden = YES; } - (NJInput *)selectedInput { @@ -219,27 +290,85 @@ 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]; } +- (BOOL)outlineView:(NSOutlineView *)outlineView + isGroupItem:(id )item { + return [item isKindOfClass:NJDevice.class]; +} + +- (BOOL)outlineView:(NSOutlineView *)outlineView_ + shouldSelectItem:(id )item { + 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 closeHid]; + else if (translatingEvents || NSApplication.sharedApplication.isActive) + [self openHid]; } } +- (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)closeHidIfDisabled:(NSNotification *)application { + if (!self.translatingEvents) + [self closeHid]; +} + +- (void)applicationDidFinishLaunching:(NSNotification *)application { + // NSApplicationWillBecomeActiveNotification occurs just slightly + // too late - there's one tick where the UI is showing "No + // devices" even with a device plugged in. + [self openHid]; +} + - (IBAction)translatingEventsChanged:(NSButton *)sender { self.translatingEvents = sender.state == NSOnState; } - @end