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(applicationDidFinishLaunching:)
32 name:NSApplicationDidFinishLaunchingNotification
35 // The HID manager uses 5-10ms per second doing basically
36 // nothing if a noisy device is plugged in (the % of that
37 // spent in input_callback is negligible, so it's not
38 // something we can make faster). I don't really think that's
39 // acceptable, CPU/power wise. So if translation is disabled
40 // and the window is closed, just switch off the HID manager
41 // entirely. This probably also has some marginal benefits for
42 // compatibility with other applications that want exclusive
44 [NSNotificationCenter.defaultCenter
46 selector:@selector(closeHidIfDisabled:)
47 name:NSApplicationDidResignActiveNotification
49 [NSNotificationCenter.defaultCenter
51 selector:@selector(openHid)
52 name:NSApplicationDidBecomeActiveNotification
59 [NSNotificationCenter.defaultCenter removeObserver:self];
60 [_continuousOutputsTick invalidate];
64 - (void)expandRecursive:(id <NJInputPathElement>)pathElement {
66 [self expandRecursive:pathElement.base];
67 [outlineView expandItem:pathElement];
71 - (void)addRunningOutput:(NJOutput *)output {
72 // Axis events will trigger every small movement, don't keep
73 // re-adding them or they trigger multiple times each time.
74 if (![_continousOutputs containsObject:output])
75 [_continousOutputs addObject:output];
76 if (!_continuousOutputsTick) {
77 _continuousOutputsTick = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
79 selector:@selector(updateContinuousOutputs:)
85 - (void)runOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
86 NJDevice *dev = [self findDeviceByRef:device];
87 NJInput *mainInput = [dev inputForEvent:value];
88 [mainInput notifyEvent:value];
89 NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[];
90 for (NJInput *subInput in children) {
91 NJOutput *output = mappingsController.currentMapping[subInput];
92 output.magnitude = subInput.magnitude;
93 output.running = subInput.active;
94 if ((output.running || output.magnitude) && output.isContinuous)
95 [self addRunningOutput:output];
99 - (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
100 NJDevice *dev = [self findDeviceByRef:device];
101 NJInput *handler = [dev handlerForEvent:value];
105 [self expandRecursive:handler];
106 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
107 byExtendingSelection: NO];
108 [outputController focusKey];
111 static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
112 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
113 IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
115 if (controller.translatingEvents) {
116 [controller runOutputForDevice:device value:value];
117 } else if ([NSApplication sharedApplication].mainWindow.isVisible) {
118 [controller showOutputForDevice:device value:value];
122 static int findAvailableIndex(NSArray *list, NJDevice *dev) {
123 for (int index = 1; ; index++) {
124 BOOL available = YES;
125 for (NJDevice *used in list) {
126 if ([used.productName isEqualToString:dev.productName] && used.index == index) {
136 - (void)addDeviceForDevice:(IOHIDDeviceRef)device {
137 IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void *)self);
138 NJDevice *dev = [[NJDevice alloc] initWithDevice:device];
139 dev.index = findAvailableIndex(_devices, dev);
140 [_devices addObject:dev];
141 [outlineView reloadData];
142 connectDevicePrompt.hidden = !!_devices.count;
145 static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
146 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
147 [controller addDeviceForDevice:device];
150 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
151 for (NJDevice *dev in _devices)
152 if (dev.device == device)
157 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
158 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
159 [controller removeDeviceForDevice:device];
162 - (void)removeDeviceForDevice:(IOHIDDeviceRef)device {
163 NJDevice *match = [self findDeviceByRef:device];
164 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
166 [_devices removeObject:match];
167 [outlineView reloadData];
168 connectDevicePrompt.hidden = !!_devices.count;
170 if (_devices.count == 1)
171 [outlineView expandItem:_devices[0]];
174 - (void)updateContinuousOutputs:(NSTimer *)timer {
175 self.mouseLoc = [NSEvent mouseLocation];
176 for (NJOutput *output in [_continousOutputs copy]) {
177 if (![output update:self]) {
178 [_continousOutputs removeObject:output];
181 if (!_continousOutputs.count) {
182 [_continuousOutputsTick invalidate];
183 _continuousOutputsTick = nil;
187 #define NSSTR(e) ((NSString *)CFSTR(e))
192 NSLog(@"Opening HID manager.");
193 _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
194 IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
195 NSArray *criteria = @[ @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
196 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
197 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
198 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
199 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
200 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
202 IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)criteria);
203 IOReturn ret = IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone);
204 if (ret != kIOReturnSuccess) {
205 [[NSAlert alertWithMessageText:@"Input devices are unavailable"
209 informativeTextWithFormat:@"Error 0x%08x occured trying to access your devices. "
210 @"Input may not be correctly detected or mapped.",
212 beginSheetModalForWindow:outlineView.window
218 IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, add_callback, (__bridge void *)self);
219 IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, remove_callback, (__bridge void *)self);
225 NSLog(@"Closing HID manager.");
226 IOHIDManagerUnscheduleFromRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
227 IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
228 CFRelease(_hidManager);
231 [_devices removeAllObjects];
232 [outlineView reloadData];
233 connectDevicePrompt.hidden = !!_devices.count;
236 - (NJInput *)selectedInput {
237 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
238 return (!item.children && item.base) ? item : nil;
241 - (NSInteger)outlineView:(NSOutlineView *)outlineView
242 numberOfChildrenOfItem:(id <NJInputPathElement>)item {
243 return item ? item.children.count : _devices.count;
246 - (BOOL)outlineView:(NSOutlineView *)outlineView
247 isItemExpandable:(id <NJInputPathElement>)item {
248 return item ? [[item children] count] > 0: YES;
251 - (id)outlineView:(NSOutlineView *)outlineView
252 child:(NSInteger)index
253 ofItem:(id <NJInputPathElement>)item {
254 return item ? item.children[index] : _devices[index];
257 - (id)outlineView:(NSOutlineView *)outlineView
258 objectValueForTableColumn:(NSTableColumn *)tableColumn
259 byItem:(id <NJInputPathElement>)item {
260 return item ? item.name : @"root";
263 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
264 [outputController loadCurrent];
267 - (BOOL)outlineView:(NSOutlineView *)outlineView
268 isGroupItem:(id <NJInputPathElement>)item {
269 return [item isKindOfClass:NJDevice.class];
272 - (BOOL)outlineView:(NSOutlineView *)outlineView_
273 shouldSelectItem:(id <NJInputPathElement>)item {
274 return ![self outlineView:outlineView_ isGroupItem:item];
277 - (void)setTranslatingEvents:(BOOL)translatingEvents {
278 if (translatingEvents != _translatingEvents) {
279 _translatingEvents = translatingEvents;
280 NSInteger state = translatingEvents ? NSOnState : NSOffState;
281 translatingEventsButton.state = state;
282 NSString *name = translatingEvents
283 ? NJEventTranslationActivated
284 : NJEventTranslationDeactivated;
285 [NSNotificationCenter.defaultCenter postNotificationName:name
288 if (!translatingEvents && !NSApplication.sharedApplication.isActive)
290 else if (translatingEvents || NSApplication.sharedApplication.isActive)
295 - (void)closeHidIfDisabled:(NSNotification *)application {
296 if (!self.translatingEvents)
300 - (void)applicationDidFinishLaunching:(NSNotification *)application {
301 // NSApplicationWillBecomeActiveNotification occurs just slightly
302 // too late - there's one tick where the UI is showing "No
303 // devices" even with a device plugged in.
307 - (IBAction)translatingEventsChanged:(NSButton *)sender {
308 self.translatingEvents = sender.state == NSOnState;