#import "NJEvents.h"
@implementation NJDeviceController {
- IOHIDManagerRef hidManager;
- NSTimer *continuousTimer;
- NSMutableArray *runningOutputs;
+ 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];
- 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];
+
+ _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(stopHidIfDisabled:)
+ name:NSApplicationDidResignActiveNotification
+ object:nil];
+ [NSNotificationCenter.defaultCenter
+ addObserver:self
+ selector:@selector(startHid)
+ name:NSApplicationDidBecomeActiveNotification
+ object:nil];
}
return self;
}
- (void)dealloc {
- [continuousTimer invalidate];
- IOHIDManagerClose(hidManager, kIOHIDOptionsTypeNone);
- CFRelease(hidManager);
+ [NSNotificationCenter.defaultCenter removeObserver:self];
+ [_continuousOutputsTick invalidate];
}
-- (void)expandRecursive:(id <NJInputPathElement>)pathElement {
+- (void)expandRecursive:(NJInputPathElement *)pathElement {
if (pathElement) {
[self expandRecursive:pathElement.base];
[outlineView expandItem:pathElement];
}
}
-- (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.");
}
}
return;
[self expandRecursive:handler];
- [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]] byExtendingSelection: NO];
+ [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
+ byExtendingSelection: NO];
[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];
}
}
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;
}
}
}
-- (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];
- [connectDevicePrompt setHidden:!!_devices.count];
-}
-
-static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
- NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
- [controller addDeviceForDevice:device];
+ [self reexpandAll];
+ hidSleepingPrompt.hidden = YES;
+ connectDevicePrompt.hidden = !!_devices.count;
}
- (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
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 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);
- 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];
+- (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error {
+ // Since the error shows the window, it can trigger another attempt
+ // to re-open the HID manager, which will also probably fail and error,
+ // so don't bother repeating ourselves.
+ if (!outlineView.window.attachedSheet) {
+ [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
+ [outlineView.window makeKeyAndOrderFront:nil];
+ [outlineView.window presentError:error
+ modalForWindow:outlineView.window
+ delegate:nil
+ didPresentSelector:nil
+ contextInfo:nil];
}
-
- IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self);
- IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self);
+ self.translatingEvents = NO;
+ if (manager.running)
+ [self hidManagerDidStart:manager];
+ else
+ [self hidManagerDidStop:manager];
+}
+
+- (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 {
- id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
- return (!item.children && item.base) ? item : nil;
+ NJInputPathElement *item = [outlineView itemAtRow:outlineView.selectedRow];
+ return (NJInput *)((!item.children && item.base) ? item : nil);
}
- (NSInteger)outlineView:(NSOutlineView *)outlineView
- numberOfChildrenOfItem:(id <NJInputPathElement>)item {
+ numberOfChildrenOfItem:(NJInputPathElement *)item {
return item ? item.children.count : _devices.count;
}
- (BOOL)outlineView:(NSOutlineView *)outlineView
- isItemExpandable:(id <NJInputPathElement>)item {
+ isItemExpandable:(NJInputPathElement *)item {
return item ? [[item children] count] > 0: YES;
}
- (id)outlineView:(NSOutlineView *)outlineView
child:(NSInteger)index
- ofItem:(id <NJInputPathElement>)item {
+ ofItem:(NJInputPathElement *)item {
return item ? item.children[index] : _devices[index];
}
- (id)outlineView:(NSOutlineView *)outlineView
objectValueForTableColumn:(NSTableColumn *)tableColumn
- byItem:(id <NJInputPathElement>)item {
+ byItem:(NJInputPathElement *)item {
return item ? item.name : @"root";
}
- (void)outlineViewSelectionDidChange:(NSNotification *)notification {
-
+ NJInputPathElement *item = [outlineView itemAtRow:outlineView.selectedRow];
+ if (item)
+ [NSUserDefaults.standardUserDefaults setObject:item.uid
+ forKey:@"selected input"];
[outputController loadCurrent];
}
+- (BOOL)outlineView:(NSOutlineView *)outlineView
+ isGroupItem:(NJInputPathElement *)item {
+ return [item isKindOfClass:NJDevice.class];
+}
+
+- (BOOL)outlineView:(NSOutlineView *)outlineView_
+ shouldSelectItem:(NJInputPathElement *)item {
+ return ![self outlineView:outlineView_ isGroupItem:item];
+}
+
+- (void)outlineViewItemDidExpand:(NSNotification *)notification {
+ 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 {
+ 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 stopHid];
+ else
+ [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