Analog mouse scrolling.
[enjoyable.git] / JoystickController.m
index 51c2a37..33201f5 100644 (file)
 //  Created by Sam McCall on 4/05/09.
 //
 
-#import "CoreFoundation/CoreFoundation.h"
+#import "JoystickController.h"
+
+#import "Config.h"
+#import "ConfigsController.h"
+#import "Joystick.h"
+#import "JSAction.h"
+#import "Target.h"
+#import "TargetController.h"
+
+@implementation JoystickController {
+    IOHIDManagerRef hidManager;
+    NSTimer *continuousTimer;
+    NSMutableArray *runningTargets;
+    NSMutableArray *_joysticks;
+}
 
-@implementation JoystickController
+- (id)init {
+    if ((self = [super init])) {
+        _joysticks = [[NSMutableArray alloc] initWithCapacity:16];
+        runningTargets = [[NSMutableArray alloc] initWithCapacity:32];
+    }
+    return self;
+}
 
-@synthesize joysticks, runningTargets, selectedAction, frontWindowOnly;
+- (void)dealloc {
+    [continuousTimer invalidate];
+    IOHIDManagerClose(hidManager, kIOHIDOptionsTypeNone);
+    CFRelease(hidManager);
+}
 
--(id) init {
-       if(self=[super init]) {
-               joysticks = [[NSMutableArray alloc]init];
-        runningTargets = [[NSMutableArray alloc]init];
-               programmaticallySelecting = NO;
-        mouseLoc.x = mouseLoc.y = 0;
-       }
-       return self;
+- (void)expandRecursive:(id)handler {
+    if ([handler base])
+        [self expandRecursive:[handler base]];
+    [outlineView expandItem:handler];
 }
 
--(void) dealloc {
-       for(int i=0; i<[joysticks count]; i++) {
-               [joysticks[i] invalidate];
-       }
-       IOHIDManagerClose(hidManager, kIOHIDOptionsTypeNone);
-       CFRelease(hidManager);
+- (void)addRunningTarget:(Target *)target {
+    if (![runningTargets containsObject:target]) {
+        [runningTargets addObject:target];
+    }
+    if (!continuousTimer) {
+        continuousTimer = [NSTimer scheduledTimerWithTimeInterval:1.f/60.f
+                                                           target:self
+                                                         selector:@selector(updateContinuousActions:)
+                                                         userInfo:nil
+                                                          repeats:YES];
+        NSLog(@"Scheduled continuous target timer.");
+    }
 }
 
-static NSMutableDictionary* create_criterion( UInt32 inUsagePage, UInt32 inUsage )
-{
-       NSMutableDictionary* dict = [[NSMutableDictionary alloc] init];
-       dict[(NSString*)CFSTR(kIOHIDDeviceUsagePageKey)] = @(inUsagePage);
-       dict[(NSString*)CFSTR(kIOHIDDeviceUsageKey)] = @(inUsage);
-       return dict;
-} 
+- (void)runTargetForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
+    Joystick *js = [self findJoystickByRef:device];
+    JSAction *mainAction = [js actionForEvent:value];
+    [mainAction notifyEvent:value];
+    NSArray *children = mainAction.children ? mainAction.children : mainAction ? @[mainAction] : @[];
+    for (JSAction *subaction in children) {
+        Target *target = configsController.currentConfig[subaction];
+        target.magnitude = mainAction.magnitude;
+        target.running = subaction.active;
+        if (target.running && target.isContinuous)
+            [self addRunningTarget:target];
+    }
+}
 
--(void) expandRecursive: (id) handler {
-       if([handler base])
-               [self expandRecursive: [handler base]];
-       [outlineView expandItem: handler];
+- (void)showTargetForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
+    Joystick *js = [self findJoystickByRef:device];
+    JSAction *handler = [js handlerForEvent:value];
+    if (!handler)
+        return;
+    
+    [self expandRecursive:handler];
+    [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]] byExtendingSelection: NO];
+    [targetController focusKey];
 }
 
-static void timer_callback(CFRunLoopTimerRef timer, void *ctx) {
-    JoystickController *jc = (__bridge JoystickController *)ctx;
-    jc->mouseLoc = [NSEvent mouseLocation];
-    for (Target *target in [jc runningTargets]) {
-        [target update: jc];
+static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
+    JoystickController *controller = (__bridge JoystickController *)ctx;
+    IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
+    
+    if (controller.sendingRealEvents) {
+        [controller runTargetForDevice:device value:value];
+    } else if ([NSApplication sharedApplication].mainWindow.isVisible) {
+        [controller showTargetForDevice:device value:value];
     }
 }
 
-static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
-       JoystickController *controller = (__bridge JoystickController *)ctx;
-       IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
-       
-       Joystick *js = [controller findJoystickByRef:device];
-       if([(ApplicationController *)[[NSApplication sharedApplication] delegate] active]) {
-               JSAction* mainAction = [js actionForEvent: value];
-               if(!mainAction)
-                       return;
-               
-               [mainAction notifyEvent: value];
-               NSArray* subactions = [mainAction subActions];
-               if(!subactions)
-                       subactions = @[mainAction];
-               for(id subaction in subactions) {
-                       Target* target = [[controller->configsController currentConfig] getTargetForAction:subaction];
-                       if(!target)
-                               continue;
-                       /* target application? doesn't seem to be any need since we are only active when it's in front */
-                       /* might be required for some strange actions */
-            if ([target running] != [subaction active]) {
-                if ([subaction active]) {
-                    [target trigger: controller];
-                }
-                else {
-                    [target untrigger: controller];
-                }
-                [target setRunning: [subaction active]];
+static int findAvailableIndex(NSArray *list, Joystick *js) {
+    for (int index = 1; ; index++) {
+        BOOL available = YES;
+        for (Joystick *used in list) {
+            if ([used.productName isEqualToString:js.productName] && used.index == index) {
+                available = NO;
+                break;
             }
-            
-            if ([mainAction isKindOfClass: [JSActionAnalog class]]) {
-                double realValue = [(JSActionAnalog*)mainAction getRealValue: IOHIDValueGetIntegerValue(value)];
-                [target setInputValue: realValue];
-            
-                // Add to list of running targets
-                if ([target isContinuous] && [target running]) {
-                    if (![controller.runningTargets containsObject:target]) {
-                        [[controller runningTargets] addObject: target];
-                    }
-                }
-            }
-               }
-       } else if([[NSApplication sharedApplication] isActive] && [[[NSApplication sharedApplication]mainWindow]isVisible]) {
-               // joysticks not active, use it to select stuff
-               id handler = [js handlerForEvent: value];
-               if(!handler)
-                       return;
-       
-               [controller expandRecursive: handler];
-               controller->programmaticallySelecting = YES;
-               [controller->outlineView selectRowIndexes: [NSIndexSet indexSetWithIndex: [controller->outlineView rowForItem: handler]] byExtendingSelection: NO];
-       }
-}
-
-static int findAvailableIndex(id list, Joystick* js) {
-       BOOL available;
-       Joystick* js2;
-       for(int index=0;;index++) {
-               available = YES;
-               for(int i=0; i<[list count]; i++) {
-                       js2 = list[i];
-                       if([js2 vendorId] == [js vendorId] && [js2 productId] == [js productId] && [js index] == index) {
-                               available = NO;
-                               break;
-                       }
-               }
-               if(available)
-                       return index;
-       }
+        }
+        if (available)
+            return index;
+    }
+}
+
+- (void)addJoystickForDevice:(IOHIDDeviceRef)device {
+    IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void*)self);
+    Joystick *js = [[Joystick alloc] initWithDevice:device];
+    js.index = findAvailableIndex(_joysticks, js);
+    [_joysticks addObject:js];
+    [outlineView reloadData];
 }
 
 static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
-       JoystickController *controller = (__bridge JoystickController *)ctx;
-    IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void*)controller);
-       Joystick *js = [[Joystick alloc] initWithDevice:device];
-    js.index = findAvailableIndex(controller.joysticks, js);
-       [js populateActions];
-       [[controller joysticks] addObject:js];
-       [controller->outlineView reloadData];
-}
-       
+    JoystickController *controller = (__bridge JoystickController *)ctx;
+    [controller addJoystickForDevice:device];
+}
+
 - (Joystick *)findJoystickByRef:(IOHIDDeviceRef)device {
-    for (Joystick *js in joysticks)
+    for (Joystick *js in _joysticks)
         if (js.device == device)
             return js;
-       return nil;
-}      
+    return nil;
+}
 
 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
-       JoystickController *controller = (__bridge JoystickController *)ctx;
-       Joystick *match = [controller findJoystickByRef:device];
+    JoystickController *controller = (__bridge JoystickController *)ctx;
+    [controller removeJoystickForDevice:device];
+}
+
+- (void)removeJoystickForDevice:(IOHIDDeviceRef)device {
+    Joystick *match = [self findJoystickByRef:device];
     IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
-       if (match) {
-        [controller.joysticks removeObject:match];
-        [controller->outlineView reloadData];
+    if (match) {
+        [_joysticks removeObject:match];
+        [outlineView reloadData];
     }
+    
 }
 
+- (void)updateContinuousActions:(NSTimer *)timer {
+    self.mouseLoc = [NSEvent mouseLocation];
+    for (Target *target in [runningTargets copy]) {
+        if (![target update:self]) {
+            [runningTargets removeObject:target];
+        }
+    }
+    if (!runningTargets.count) {
+        [continuousTimer invalidate];
+        continuousTimer = nil;
+        NSLog(@"Unscheduled continuous target timer.");
+    }
+}
+
+#define NSSTR(e) ((NSString *)CFSTR(e))
+
 - (void)setup {
-    hidManager = IOHIDManagerCreate( kCFAllocatorDefault, kIOHIDOptionsTypeNone);
-       NSArray *criteria = @[
-        create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_Joystick),
-        create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_GamePad),
-        create_criterion(kHIDPage_GenericDesktop, kHIDUsage_GD_MultiAxisController)];
-       
-       IOHIDManagerSetDeviceMatchingMultiple(hidManager, (CFArrayRef)CFBridgingRetain(criteria));
+    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 tIOReturn = IOHIDManagerOpen( hidManager, kIOHIDOptionsTypeNone );
-       (void)tIOReturn;
-       
-       IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self );
-       IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self);
-
-    // Setup timer for continuous targets
-    CFRunLoopTimerContext ctx = {
-        0, (__bridge void*)self, NULL, NULL, NULL
-    };
-    CFRunLoopTimerRef timer = CFRunLoopTimerCreate(kCFAllocatorDefault,
-                                                   CFAbsoluteTimeGetCurrent(), 1.0/80.0,
-                                                   0, 0, timer_callback, &ctx);
-    CFRunLoopAddTimer(CFRunLoopGetCurrent(), timer, kCFRunLoopDefaultMode);
-}
-
--(id) determineSelectedAction {
-       id item = [outlineView itemAtRow: [outlineView selectedRow]];
-       if(!item)
-               return NULL;
-       if([item isKindOfClass: [JSAction class]] && [item subActions] != NULL)
-               return NULL;
-       if([item isKindOfClass: [Joystick class]])
-               return NULL;
-       return item;
-}
-
-/* outline view */
-
-- (int)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
-       if(item == nil)
-               return [joysticks count];
-       if([item isKindOfClass: [Joystick class]])
-               return [[item children] count];
-       if([item isKindOfClass: [JSAction class]] && [item subActions] != NULL)
-               return [[item subActions] count];
-       return 0;
+    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]
+         runModal];
+    }
+    
+    IOHIDManagerRegisterDeviceMatchingCallback(hidManager, add_callback, (__bridge void *)self);
+    IOHIDManagerRegisterDeviceRemovalCallback(hidManager, remove_callback, (__bridge void *)self);
 }
 
-- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
-       if(item == nil)
-               return YES;
-       if([item isKindOfClass: [Joystick class]])
-               return YES;
-       if([item isKindOfClass: [JSAction class]]) 
-               return [item subActions]==NULL ? NO : YES;
-       return NO;
+- (JSAction *)selectedAction {
+    id item = [outlineView itemAtRow:outlineView.selectedRow];
+    return [item children] ? nil : item;
 }
 
-- (id)outlineView:(NSOutlineView *)outlineView child:(int)index ofItem:(id)item {
-       if(item == nil) 
-               return joysticks[index];
+- (NSInteger)outlineView:(NSOutlineView *)outlineView numberOfChildrenOfItem:(id)item {
+    return item ? [[item children] count] : _joysticks.count;
+}
 
-       if([item isKindOfClass: [Joystick class]])
-               return [item children][index];
-       
-       if([item isKindOfClass: [JSAction class]]) 
-               return [item subActions][index];
+- (BOOL)outlineView:(NSOutlineView *)outlineView isItemExpandable:(id)item {
+    return item ? [[item children] count] > 0: YES;
+}
 
-       return NULL;
+- (id)outlineView:(NSOutlineView *)outlineView child:(NSInteger)index ofItem:(id)item {
+    return item ? [item children][index] : _joysticks[index];
 }
+
 - (id)outlineView:(NSOutlineView *)outlineView objectValueForTableColumn:(NSTableColumn *)tableColumn byItem:(id)item  {
-       if(item == nil)
-               return @"root";
-       return [item name];
+    if(item == nil)
+        return @"root";
+    return [item name];
 }
 
-- (void)outlineViewSelectionDidChange: (NSNotification*) notification {
-       [targetController reset];
-       selectedAction = [self determineSelectedAction];
-       [targetController load];
-       if(programmaticallySelecting)
-               [targetController focusKey];
-       programmaticallySelecting = NO;
+- (void)outlineViewSelectionDidChange:(NSNotification *)notification {
+    [targetController loadCurrent];
 }
-       
+
 @end