#import "NJEvents.h"
@implementation NJDeviceController {
- IOHIDManagerRef hidManager;
- NSTimer *continuousTimer;
- NSMutableArray *runningOutputs;
+ IOHIDManagerRef _hidManager;
+ NSTimer *_continuousOutputsTick;
+ NSMutableArray *_continousOutputs;
NSMutableArray *_devices;
}
- (id)init {
if ((self = [super init])) {
_devices = [[NSMutableArray alloc] initWithCapacity:16];
- runningOutputs = [[NSMutableArray alloc] initWithCapacity:32];
+ _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
+ [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 <NJInputPathElement>)pathElement {
}
- (void)addRunningOutput:(NJOutput *)output {
- if (![runningOutputs containsObject:output]) {
- [runningOutputs addObject:output];
- }
- if (!continuousTimer) {
- continuousTimer = [NSTimer scheduledTimerWithTimeInterval:1.f/60.f
+ // 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];
}
return;
[self expandRecursive:handler];
- [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]] byExtendingSelection: NO];
+ [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
+ byExtendingSelection: NO];
[outputController focusKey];
}
}
- (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];
+ connectDevicePrompt.hidden = !!_devices.count;
}
static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
if (match) {
[_devices removeObject:match];
[outlineView reloadData];
- [connectDevicePrompt setHidden:!!_devices.count];
+ connectDevicePrompt.hidden = !!_devices.count;
}
-
+ 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;
+ 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),
@{ 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);
}
-
- 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];
+ connectDevicePrompt.hidden = !!_devices.count;
}
- (NJInput *)selectedInput {
}
- (void)outlineViewSelectionDidChange:(NSNotification *)notification {
-
[outputController loadCurrent];
}
+- (BOOL)outlineView:(NSOutlineView *)outlineView
+ isGroupItem:(id <NJInputPathElement>)item {
+ return [item isKindOfClass:NJDevice.class];
+}
+
+- (BOOL)outlineView:(NSOutlineView *)outlineView_
+ shouldSelectItem:(id <NJInputPathElement>)item {
+ return ![self outlineView:outlineView_ isGroupItem:item];
+}
+
- (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)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