Don't let the app control itself, it only leads to accidents or mouse bugs. Don't...
[enjoyable.git] / Classes / NJDeviceController.m
index 2a1546c..0b0fe92 100644 (file)
@@ -16,7 +16,7 @@
 #import "NJEvents.h"
 
 @implementation NJDeviceController {
-    IOHIDManagerRef _hidManager;
+    NJHIDManager *_hidManager;
     NSTimer *_continuousOutputsTick;
     NSMutableArray *_continousOutputs;
     NSMutableArray *_devices;
@@ -24,6 +24,7 @@
 }
 
 #define EXPANDED_MEMORY_MAX_SIZE 100
+#define NSSTR(e) ((NSString *)CFSTR(e))
 
 - (id)init {
     if ((self = [super init])) {
             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(applicationDidFinishLaunching:)
+             selector:@selector(startHid)
              name:NSApplicationDidFinishLaunchingNotification
              object:nil];
         
         // grabs.
         [NSNotificationCenter.defaultCenter
             addObserver:self
-            selector:@selector(closeHidIfDisabled:)
+            selector:@selector(stopHidIfDisabled:)
             name:NSApplicationDidResignActiveNotification
             object:nil];
         [NSNotificationCenter.defaultCenter
             addObserver:self
-            selector:@selector(openHid)
+            selector:@selector(startHid)
             name:NSApplicationDidBecomeActiveNotification
             object:nil];
     }
 - (void)dealloc {
     [NSNotificationCenter.defaultCenter removeObserver:self];
     [_continuousOutputsTick invalidate];
-    [self closeHid];
 }
 
-- (void)expandRecursive:(id <NJInputPathElement>)pathElement {
+- (void)expandRecursive:(NJInputPathElement *)pathElement {
     if (pathElement) {
-        [self expandRecursive:pathElement.base];
+        [self expandRecursive:pathElement.parent];
         [outlineView expandItem:pathElement];
     }
 }
     [self expandRecursive:handler];
     [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
              byExtendingSelection: NO];
-    [outputController focusKey];
+    if (!self.simulatingEvents)
+        [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];
     }
 }
 
@@ -146,7 +157,8 @@ 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) {
+            if ([used.productName isEqualToString:dev.productName]
+                && used.index == index) {
                 available = NO;
                 break;
             }
@@ -156,22 +168,16 @@ static int findAvailableIndex(NSArray *list, NJDevice *dev) {
     }
 }
 
-- (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];
     [self reexpandAll];
     hidSleepingPrompt.hidden = YES;
     connectDevicePrompt.hidden = !!_devices.count;
 }
 
-static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
-    NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
-    [controller addDeviceForDevice:device];
-}
-
 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
     for (NJDevice *dev in _devices)
         if (dev.device == device)
@@ -179,12 +185,7 @@ static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDevi
     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) {
@@ -210,87 +211,75 @@ static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDD
     }
 }
 
-#define NSSTR(e) ((NSString *)CFSTR(e))
-
-- (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_GamePad) },
-                           @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
-                              NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
-                           ];
-    IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)criteria);
-    IOReturn ret = IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone);
-    if (ret != kIOReturnSuccess) {
-        [[NSAlert alertWithMessageText:NSLocalizedString(@"input devices unavailable",
-                                                         @"error title when devices can't be read")
-                         defaultButton:nil
-                       alternateButton:nil
-                           otherButton: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;
+- (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];
     }
+    self.simulatingEvents = NO;
+    if (manager.running)
+        [self hidManagerDidStart:manager];
+    else
+        [self hidManagerDidStop:manager];
 }
 
-- (void)closeHid {
-    if (_hidManager) {
-        NSLog(@"Closing HID manager.");
-        IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
-        IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
-        CFRelease(_hidManager);
-        _hidManager = NULL;
-    }
+- (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.parent) ? 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 {
-    id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
+    NJInputPathElement *item = [outlineView itemAtRow:outlineView.selectedRow];
     if (item)
         [NSUserDefaults.standardUserDefaults setObject:item.uid
                                                 forKey:@"selected input"];
@@ -298,17 +287,17 @@ objectValueForTableColumn:(NSTableColumn *)tableColumn
 }
 
 - (BOOL)outlineView:(NSOutlineView *)outlineView
-        isGroupItem:(id <NJInputPathElement>)item {
+        isGroupItem:(NJInputPathElement *)item {
     return [item isKindOfClass:NJDevice.class];
 }
 
 - (BOOL)outlineView:(NSOutlineView *)outlineView_
-   shouldSelectItem:(id <NJInputPathElement>)item {
+   shouldSelectItem:(NJInputPathElement *)item {
     return ![self outlineView:outlineView_ isGroupItem:item];
 }
 
 - (void)outlineViewItemDidExpand:(NSNotification *)notification {
-    id <NJInputPathElement> item = notification.userInfo[@"NSObject"];
+    NJInputPathElement *item = notification.userInfo[@"NSObject"];
     NSString *uid = item.uid;
     if (![_expanded containsObject:uid])
         [_expanded addObject:uid];
@@ -319,27 +308,27 @@ objectValueForTableColumn:(NSTableColumn *)tableColumn
 }
 
 - (void)outlineViewItemDidCollapse:(NSNotification *)notification {
-    id <NJInputPathElement> item = notification.userInfo[@"NSObject"];
+    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;
+- (void)setSimulatingEvents:(BOOL)simulatingEvents {
+    if (simulatingEvents != _simulatingEvents) {
+        _simulatingEvents = simulatingEvents;
+        NSInteger state = simulatingEvents ? NSOnState : NSOffState;
         translatingEventsButton.state = state;
-        NSString *name = translatingEvents
-            ? NJEventTranslationActivated
-            : NJEventTranslationDeactivated;
+        NSString *name = simulatingEvents
+            ? NJEventSimulationStarted
+            : NJEventSimulationStopped;
         [NSNotificationCenter.defaultCenter postNotificationName:name
                                                           object:self];
 
-        if (!translatingEvents && !NSApplication.sharedApplication.isActive)
-            [self closeHid];
-        else if (translatingEvents || NSApplication.sharedApplication.isActive)
-            [self openHid];
+        if (!simulatingEvents && !NSApplication.sharedApplication.isActive)
+            [self stopHid];
+        else
+            [self startHid];
     }
 }
 
@@ -355,20 +344,13 @@ objectValueForTableColumn:(NSTableColumn *)tableColumn
     }
 }
 
-- (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];
+- (void)stopHidIfDisabled:(NSNotification *)application {
+    if (!self.simulatingEvents)
+        [self stopHid];
 }
 
-- (IBAction)translatingEventsChanged:(NSButton *)sender {
-    self.translatingEvents = sender.state == NSOnState;
+- (IBAction)simulatingEventsChanged:(NSButton *)sender {
+    self.simulatingEvents = sender.state == NSOnState;
 }
 
 @end