Move simulation toggle out of device controller.
[enjoyable.git] / Classes / NJDeviceController.m
index d553d7d..1b64aaf 100644 (file)
 #import "NJDevice.h"
 #import "NJInput.h"
 #import "NJOutput.h"
-#import "NJOutputController.h"
 #import "NJEvents.h"
 
 @implementation NJDeviceController {
-    IOHIDManagerRef hidManager;
-    NSTimer *continuousTimer;
-    NSMutableArray *runningOutputs;
+    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];
-        runningOutputs = [[NSMutableArray alloc] initWithCapacity:32];
+        _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(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 {
-    if (pathElement) {
-        [self expandRecursive:pathElement.base];
-        [outlineView expandItem:pathElement];
-    }
+- (NJDevice *)objectAtIndexedSubscript:(NSUInteger)idx {
+    return idx < _devices.count ? _devices[idx] : nil;
+}
+
+- (NSUInteger)count {
+    return _devices.count;
 }
 
 - (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];
-        NSLog(@"Scheduled continuous output timer.");
     }
 }
 
     if (!handler)
         return;
     
-    [self expandRecursive:handler];
-    [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]] byExtendingSelection: NO];
-    [outputController focusKey];
+    [self.delegate deviceController:self didInput:handler];
 }
 
-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)addDevice:(NJDevice *)device {
+    BOOL available;
+    do {
+        available = YES;
+        for (NJDevice *used in _devices) {
+            if ([used isEqual:device]) {
+                device.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];
+    } while (!available);
+    
+    [_devices addObject:device];
 }
 
-static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
-    NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
-    [controller addDeviceForDevice:device];
+- (void)hidManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device {
+    NJDevice *match = [[NJDevice alloc] initWithDevice:device];
+    [self addDevice:match];
+    [self.delegate deviceController:self didAddDevice:match];
 }
 
 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
@@ -127,117 +150,78 @@ 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) {
-        [_devices removeObject:match];
-        [outlineView reloadData];
+        NSInteger idx = [_devices indexOfObjectIdenticalTo:match];
+        [_devices removeObjectAtIndex:idx];
+        [self.delegate deviceController:self didRemoveDeviceAtIndex:idx];
     }
-    
 }
 
-- (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];
-    }
-    
-    IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self);
-    IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self);
+- (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error {
+    [self.delegate deviceController:self didError:error];
+    self.simulatingEvents = NO;
 }
 
-- (NJInput *)selectedInput {
-    id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
-    return (!item.children && item.base) ? item : nil;
+- (void)hidManagerDidStart:(NJHIDManager *)manager {
+    [self.delegate deviceControllerDidStartHID:self];
 }
 
-- (NSInteger)outlineView:(NSOutlineView *)outlineView
-  numberOfChildrenOfItem:(id <NJInputPathElement>)item {
-    return item ? item.children.count : _devices.count;
+- (void)hidManagerDidStop:(NJHIDManager *)manager {
+    [_devices removeAllObjects];
+    [self.delegate deviceControllerDidStopHID:self];
 }
 
-- (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";
-}
-
-- (void)outlineViewSelectionDidChange:(NSNotification *)notification {
-    
-    [outputController loadCurrent];
-}
-
-- (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;
+- (void)setSimulatingEvents:(BOOL)simulatingEvents {
+    if (simulatingEvents != _simulatingEvents) {
+        _simulatingEvents = simulatingEvents;
+        NSString *name = simulatingEvents
+            ? NJEventSimulationStarted
+            : NJEventSimulationStopped;
         [NSNotificationCenter.defaultCenter postNotificationName:name
                                                           object:self];
+
+        if (!simulatingEvents && !NSApplication.sharedApplication.isActive)
+            [self stopHid];
+        else
+            [self startHid];
     }
 }
 
-- (IBAction)translatingEventsChanged:(NSButton *)sender {
-    self.translatingEvents = sender.state == NSOnState;
+- (void)stopHidIfDisabled:(NSNotification *)application {
+    if (!self.simulatingEvents && !NSProcessInfo.processInfo.isBeingDebugged)
+        [self stopHid];
 }
 
+- (NJInputPathElement *)objectForKeyedSubscript:(NSString *)uid {
+    for (NJDevice *dev in _devices) {
+        id item = [dev elementForUID:uid];
+        if (item)
+            return item;
+    }
+    return nil;
+}
 
 @end