2 // NJDeviceController.m
5 // Created by Sam McCall on 4/05/09.
8 #import "NJDeviceController.h"
11 #import "NJMappingsController.h"
15 #import "NJOutputController.h"
18 @implementation NJDeviceController {
19 IOHIDManagerRef _hidManager;
20 NSTimer *_continuousOutputsTick;
21 NSMutableArray *_continousOutputs;
22 NSMutableArray *_devices;
26 if ((self = [super init])) {
27 _devices = [[NSMutableArray alloc] initWithCapacity:16];
28 _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
29 [NSNotificationCenter.defaultCenter
31 selector:@selector(setup)
32 name:NSApplicationDidFinishLaunchingNotification
39 [_continuousOutputsTick invalidate];
40 IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
41 CFRelease(_hidManager);
44 - (void)expandRecursive:(id <NJInputPathElement>)pathElement {
46 [self expandRecursive:pathElement.base];
47 [outlineView expandItem:pathElement];
51 - (void)addRunningOutput:(NJOutput *)output {
52 [_continousOutputs addObject:output];
53 if (!_continuousOutputsTick) {
54 _continuousOutputsTick = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
56 selector:@selector(updateContinuousOutputs:)
62 - (void)runOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
63 NJDevice *dev = [self findDeviceByRef:device];
64 NJInput *mainInput = [dev inputForEvent:value];
65 [mainInput notifyEvent:value];
66 NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[];
67 for (NJInput *subInput in children) {
68 NJOutput *output = mappingsController.currentMapping[subInput];
69 output.magnitude = subInput.magnitude;
70 output.running = subInput.active;
71 if ((output.running || output.magnitude) && output.isContinuous)
72 [self addRunningOutput:output];
76 - (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
77 NJDevice *dev = [self findDeviceByRef:device];
78 NJInput *handler = [dev handlerForEvent:value];
82 [self expandRecursive:handler];
83 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
84 byExtendingSelection: NO];
85 [outputController focusKey];
88 static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
89 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
90 IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
92 if (controller.translatingEvents) {
93 [controller runOutputForDevice:device value:value];
94 } else if ([NSApplication sharedApplication].mainWindow.isVisible) {
95 [controller showOutputForDevice:device value:value];
99 static int findAvailableIndex(NSArray *list, NJDevice *dev) {
100 for (int index = 1; ; index++) {
101 BOOL available = YES;
102 for (NJDevice *used in list) {
103 if ([used.productName isEqualToString:dev.productName] && used.index == index) {
113 - (void)addDeviceForDevice:(IOHIDDeviceRef)device {
114 IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void *)self);
115 NJDevice *dev = [[NJDevice alloc] initWithDevice:device];
116 dev.index = findAvailableIndex(_devices, dev);
117 [_devices addObject:dev];
118 [outlineView reloadData];
119 connectDevicePrompt.hidden = !!_devices.count;
122 static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
123 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
124 [controller addDeviceForDevice:device];
127 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
128 for (NJDevice *dev in _devices)
129 if (dev.device == device)
134 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
135 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
136 [controller removeDeviceForDevice:device];
139 - (void)removeDeviceForDevice:(IOHIDDeviceRef)device {
140 NJDevice *match = [self findDeviceByRef:device];
141 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
143 [_devices removeObject:match];
144 [outlineView reloadData];
145 connectDevicePrompt.hidden = !!_devices.count;
147 if (_devices.count == 1)
148 [outlineView expandItem:_devices[0]];
151 - (void)updateContinuousOutputs:(NSTimer *)timer {
152 self.mouseLoc = [NSEvent mouseLocation];
153 for (NJOutput *output in [_continousOutputs copy]) {
154 if (![output update:self]) {
155 [_continousOutputs removeObject:output];
158 if (!_continousOutputs.count) {
159 [_continuousOutputsTick invalidate];
160 _continuousOutputsTick = nil;
164 #define NSSTR(e) ((NSString *)CFSTR(e))
167 _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
168 NSArray *criteria = @[ @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
169 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
170 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
171 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
172 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
173 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
175 IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)criteria);
177 IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
178 IOReturn ret = IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone);
179 if (ret != kIOReturnSuccess) {
180 [[NSAlert alertWithMessageText:@"Input devices are unavailable"
184 informativeTextWithFormat:@"Error 0x%08x occured trying to access your devices. "
185 @"Input may not be correctly detected or mapped.",
187 beginSheetModalForWindow:outlineView.window
193 IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, add_callback, (__bridge void *)self);
194 IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, remove_callback, (__bridge void *)self);
197 - (NJInput *)selectedInput {
198 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
199 return (!item.children && item.base) ? item : nil;
202 - (NSInteger)outlineView:(NSOutlineView *)outlineView
203 numberOfChildrenOfItem:(id <NJInputPathElement>)item {
204 return item ? item.children.count : _devices.count;
207 - (BOOL)outlineView:(NSOutlineView *)outlineView
208 isItemExpandable:(id <NJInputPathElement>)item {
209 return item ? [[item children] count] > 0: YES;
212 - (id)outlineView:(NSOutlineView *)outlineView
213 child:(NSInteger)index
214 ofItem:(id <NJInputPathElement>)item {
215 return item ? item.children[index] : _devices[index];
218 - (id)outlineView:(NSOutlineView *)outlineView
219 objectValueForTableColumn:(NSTableColumn *)tableColumn
220 byItem:(id <NJInputPathElement>)item {
221 return item ? item.name : @"root";
224 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
225 [outputController loadCurrent];
228 - (BOOL)outlineView:(NSOutlineView *)outlineView
229 isGroupItem:(id <NJInputPathElement>)item {
230 return [item isKindOfClass:NJDevice.class];
233 - (BOOL)outlineView:(NSOutlineView *)outlineView_
234 shouldSelectItem:(id <NJInputPathElement>)item {
235 return ![self outlineView:outlineView_ isGroupItem:item];
238 - (void)setTranslatingEvents:(BOOL)translatingEvents {
239 if (translatingEvents != _translatingEvents) {
240 _translatingEvents = translatingEvents;
241 NSInteger state = translatingEvents ? NSOnState : NSOffState;
242 translatingEventsButton.state = state;
243 NSString *name = translatingEvents
244 ? NJEventTranslationActivated
245 : NJEventTranslationDeactivated;
246 [NSNotificationCenter.defaultCenter postNotificationName:name
251 - (IBAction)translatingEventsChanged:(NSButton *)sender {
252 self.translatingEvents = sender.state == NSOnState;