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];
_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(setup)
- name:NSApplicationDidFinishLaunchingNotification
+ selector:@selector(closeHidIfDisabled:)
+ name:NSApplicationDidResignActiveNotification
+ object:nil];
+ [NSNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(openHid)
+ name:NSApplicationDidBecomeActiveNotification
object:nil];
}
return self;
}
- (void)dealloc {
+ [NSNotificationCenter.defaultCenter removeObserver:self];
[_continuousOutputsTick invalidate];
- IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
- CFRelease(_hidManager);
+ [self closeHid];
}
- (void)expandRecursive:(id <NJInputPathElement>)pathElement {
}
}
+- (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
dev.index = findAvailableIndex(_devices, dev);
[_devices addObject:dev];
[outlineView reloadData];
+ [self reexpandAll];
+ hidSleepingPrompt.hidden = YES;
connectDevicePrompt.hidden = !!_devices.count;
}
[_devices removeObject:match];
[outlineView reloadData];
connectDevicePrompt.hidden = !!_devices.count;
+ hidSleepingPrompt.hidden = YES;
}
if (_devices.count == 1)
[outlineView expandItem:_devices[0]];
#define NSSTR(e) ((NSString *)CFSTR(e))
-- (void)setup {
+- (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),
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"
+ [[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 {
}
- (void)outlineViewSelectionDidChange:(NSNotification *)notification {
+ id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
+ if (item)
+ [NSUserDefaults.standardUserDefaults setObject:item.uid
+ forKey:@"selected input"];
[outputController loadCurrent];
}
return ![self outlineView:outlineView_ isGroupItem:item];
}
+- (void)outlineViewItemDidExpand:(NSNotification *)notification {
+ id <NJInputPathElement> 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 <NJInputPathElement> 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