#import "NJOutput.h"
#import "NJOutputController.h"
#import "NJEvents.h"
+#import "NJDeviceViewController.h"
@implementation NJDeviceController {
- IOHIDManagerRef _hidManager;
+ NJHIDManager *_hidManager;
NSTimer *_continuousOutputsTick;
NSMutableArray *_continousOutputs;
NSMutableArray *_devices;
}
+#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];
+
+ // 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(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 <NJInputPathElement>)pathElement {
- if (pathElement) {
- [self expandRecursive:pathElement.base];
- [outlineView expandItem:pathElement];
+- (id)elementForUID:(NSString *)uid {
+ for (NJDevice *dev in _devices) {
+ id item = [dev elementForUID:uid];
+ if (item)
+ return item;
}
+ return nil;
}
- (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
if (!handler)
return;
- [self expandRecursive:handler];
- [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
- byExtendingSelection: NO];
+ [devicesViewController expandAndSelectItem:handler];
[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.simulatingEvents
+ && !NSApplication.sharedApplication.isActive) {
+ [self runOutputForDevice:device value:value];
+ } else {
+ [self showOutputForDevice:device value:value];
}
}
-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) {
+- (void)hidManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device {
+ NJDevice *match = [[NJDevice alloc] initWithDevice:device];
+ match.index = 1;
+ BOOL available;
+ do {
+ available = YES;
+ for (NJDevice *used in _devices) {
+ if ([used isEqual:match]) {
+ match.index += 1;
available = NO;
- break;
}
}
- if (available)
- return index;
- }
-}
-
-- (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];
- [outlineView reloadData];
- connectDevicePrompt.hidden = !!_devices.count;
-}
+ } while (!available);
-static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
- NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
- [controller addDeviceForDevice:device];
+ [_devices addObject:match];
+ [devicesViewController addedDevice:match atIndex:_devices.count - 1];
}
- (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.hidden = !!_devices.count;
+ NSInteger idx = [_devices indexOfObjectIdenticalTo:match];
+ [_devices removeObjectAtIndex:idx];
+ [devicesViewController removedDevice:match atIndex:idx];
}
- if (_devices.count == 1)
- [outlineView expandItem:_devices[0]];
}
- (void)updateContinuousOutputs:(NSTimer *)timer {
}
}
-#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 (!simulatingEventsButton.window.attachedSheet) {
+ [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
+ [simulatingEventsButton.window makeKeyAndOrderFront:nil];
+ [simulatingEventsButton.window presentError:error
+ modalForWindow:simulatingEventsButton.window
+ delegate:nil
+ didPresentSelector:nil
+ contextInfo:nil];
}
-
- IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, add_callback, (__bridge void *)self);
- IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, remove_callback, (__bridge void *)self);
+ self.simulatingEvents = NO;
}
-- (NJInput *)selectedInput {
- id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
- return (!item.children && item.base) ? item : nil;
+- (void)hidManagerDidStart:(NJHIDManager *)manager {
+ [devicesViewController hidStarted];
}
-- (NSInteger)outlineView:(NSOutlineView *)outlineView
- numberOfChildrenOfItem:(id <NJInputPathElement>)item {
- return item ? item.children.count : _devices.count;
+- (void)hidManagerDidStop:(NJHIDManager *)manager {
+ [_devices removeAllObjects];
+ [devicesViewController hidStopped];
}
-- (BOOL)outlineView:(NSOutlineView *)outlineView
- isItemExpandable:(id <NJInputPathElement>)item {
- return item ? [[item children] count] > 0: YES;
+- (void)startHid {
+ [_hidManager start];
}
-- (id)outlineView:(NSOutlineView *)outlineView
- child:(NSInteger)index
- ofItem:(id <NJInputPathElement>)item {
- return item ? item.children[index] : _devices[index];
+- (void)stopHid {
+ [_hidManager stop];
}
-- (id)outlineView:(NSOutlineView *)outlineView
-objectValueForTableColumn:(NSTableColumn *)tableColumn
- byItem:(id <NJInputPathElement>)item {
- return item ? item.name : @"root";
+- (NJInput *)selectedInput {
+ return (NJInput *)devicesViewController.selectedHandler;
}
-- (void)outlineViewSelectionDidChange:(NSNotification *)notification {
- [outputController loadCurrent];
+- (void)setSimulatingEvents:(BOOL)simulatingEvents {
+ if (simulatingEvents != _simulatingEvents) {
+ _simulatingEvents = simulatingEvents;
+ NSInteger state = simulatingEvents ? NSOnState : NSOffState;
+ simulatingEventsButton.state = state;
+ NSString *name = simulatingEvents
+ ? NJEventSimulationStarted
+ : NJEventSimulationStopped;
+ [NSNotificationCenter.defaultCenter postNotificationName:name
+ object:self];
+
+ if (!simulatingEvents && !NSApplication.sharedApplication.isActive)
+ [self stopHid];
+ else
+ [self startHid];
+ }
}
-- (BOOL)outlineView:(NSOutlineView *)outlineView
- isGroupItem:(id <NJInputPathElement>)item {
- return [item isKindOfClass:NJDevice.class];
+- (void)stopHidIfDisabled:(NSNotification *)application {
+ if (!self.simulatingEvents)
+ [self stopHid];
}
-- (BOOL)outlineView:(NSOutlineView *)outlineView_
- shouldSelectItem:(id <NJInputPathElement>)item {
- return ![self outlineView:outlineView_ isGroupItem:item];
+- (IBAction)simulatingEventsChanged:(NSButton *)sender {
+ self.simulatingEvents = sender.state == NSOnState;
}
-- (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];
- }
+- (void)deviceViewControllerDidSelectNothing:(NJDeviceViewController *)devices {
+ [outputController loadCurrent];
+}
+
+- (void)deviceViewController:(NJDeviceViewController *)devices
+ didSelectBranch:(NJInputPathElement *)handler {
+ [outputController loadCurrent];
}
-- (IBAction)translatingEventsChanged:(NSButton *)sender {
- self.translatingEvents = sender.state == NSOnState;
+- (void)deviceViewController:(NJDeviceViewController *)devices
+ didSelectHandler:(NJInputPathElement *)handler {
+ [outputController loadCurrent];
}
+- (void)deviceViewController:(NJDeviceViewController *)devices
+ didSelectDevice:(NJInputPathElement *)device {
+ [outputController loadCurrent];
+}
@end