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 NJHIDManager *_hidManager;
20 NSTimer *_continuousOutputsTick;
21 NSMutableArray *_continousOutputs;
22 NSMutableArray *_devices;
23 NSMutableArray *_expanded;
26 #define EXPANDED_MEMORY_MAX_SIZE 100
27 #define NSSTR(e) ((NSString *)CFSTR(e))
30 if ((self = [super init])) {
31 _devices = [[NSMutableArray alloc] initWithCapacity:16];
32 _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
34 NSArray *expanded = [NSUserDefaults.standardUserDefaults objectForKey:@"expanded rows"];
35 if (![expanded isKindOfClass:NSArray.class])
37 _expanded = [[NSMutableArray alloc] initWithCapacity:MAX(16, _expanded.count)];
38 [_expanded addObjectsFromArray:expanded];
40 _hidManager = [[NJHIDManager alloc] initWithCriteria:@[
41 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
42 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
43 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
44 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
45 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
46 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
50 [NSNotificationCenter.defaultCenter
52 selector:@selector(startHid)
53 name:NSApplicationDidFinishLaunchingNotification
56 // The HID manager uses 5-10ms per second doing basically
57 // nothing if a noisy device is plugged in (the % of that
58 // spent in input_callback is negligible, so it's not
59 // something we can make faster). I don't really think that's
60 // acceptable, CPU/power wise. So if translation is disabled
61 // and the window is closed, just switch off the HID manager
62 // entirely. This probably also has some marginal benefits for
63 // compatibility with other applications that want exclusive
65 [NSNotificationCenter.defaultCenter
67 selector:@selector(stopHidIfDisabled:)
68 name:NSApplicationDidResignActiveNotification
70 [NSNotificationCenter.defaultCenter
72 selector:@selector(startHid)
73 name:NSApplicationDidBecomeActiveNotification
80 [NSNotificationCenter.defaultCenter removeObserver:self];
81 [_continuousOutputsTick invalidate];
84 - (void)expandRecursive:(id <NJInputPathElement>)pathElement {
86 [self expandRecursive:pathElement.base];
87 [outlineView expandItem:pathElement];
91 - (id)elementForUID:(NSString *)uid {
92 for (NJDevice *dev in _devices) {
93 id item = [dev elementForUID:uid];
100 - (void)expandRecursiveByUID:(NSString *)uid {
101 [self expandRecursive:[self elementForUID:uid]];
104 - (void)addRunningOutput:(NJOutput *)output {
105 // Axis events will trigger every small movement, don't keep
106 // re-adding them or they trigger multiple times each time.
107 if (![_continousOutputs containsObject:output])
108 [_continousOutputs addObject:output];
109 if (!_continuousOutputsTick) {
110 _continuousOutputsTick = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
112 selector:@selector(updateContinuousOutputs:)
118 - (void)runOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
119 NJDevice *dev = [self findDeviceByRef:device];
120 NJInput *mainInput = [dev inputForEvent:value];
121 [mainInput notifyEvent:value];
122 NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[];
123 for (NJInput *subInput in children) {
124 NJOutput *output = mappingsController.currentMapping[subInput];
125 output.magnitude = subInput.magnitude;
126 output.running = subInput.active;
127 if ((output.running || output.magnitude) && output.isContinuous)
128 [self addRunningOutput:output];
132 - (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
133 NJDevice *dev = [self findDeviceByRef:device];
134 NJInput *handler = [dev handlerForEvent:value];
138 [self expandRecursive:handler];
139 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
140 byExtendingSelection: NO];
141 [outputController focusKey];
144 - (void)hidManager:(NJHIDManager *)manager
145 valueChanged:(IOHIDValueRef)value
146 fromDevice:(IOHIDDeviceRef)device {
147 if (self.translatingEvents) {
148 [self runOutputForDevice:device value:value];
150 [self showOutputForDevice:device value:value];
154 static int findAvailableIndex(NSArray *list, NJDevice *dev) {
155 for (int index = 1; ; index++) {
156 BOOL available = YES;
157 for (NJDevice *used in list) {
158 if ([used.productName isEqualToString:dev.productName]
159 && used.index == index) {
169 - (void)hidManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device {
170 NJDevice *match = [[NJDevice alloc] initWithDevice:device];
171 match.index = findAvailableIndex(_devices, match);
172 [_devices addObject:match];
173 [outlineView reloadData];
175 hidSleepingPrompt.hidden = YES;
176 connectDevicePrompt.hidden = !!_devices.count;
179 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
180 for (NJDevice *dev in _devices)
181 if (dev.device == device)
186 - (void)hidManager:(NJHIDManager *)manager deviceRemoved:(IOHIDDeviceRef)device {
187 NJDevice *match = [self findDeviceByRef:device];
188 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
190 [_devices removeObject:match];
191 [outlineView reloadData];
192 connectDevicePrompt.hidden = !!_devices.count;
193 hidSleepingPrompt.hidden = YES;
195 if (_devices.count == 1)
196 [outlineView expandItem:_devices[0]];
199 - (void)updateContinuousOutputs:(NSTimer *)timer {
200 self.mouseLoc = [NSEvent mouseLocation];
201 for (NJOutput *output in [_continousOutputs copy]) {
202 if (![output update:self]) {
203 [_continousOutputs removeObject:output];
206 if (!_continousOutputs.count) {
207 [_continuousOutputsTick invalidate];
208 _continuousOutputsTick = nil;
212 - (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error {
213 [outlineView.window presentError:error
214 modalForWindow:outlineView.window
216 didPresentSelector:nil
220 - (void)hidManagerDidStart:(NJHIDManager *)manager {
221 hidSleepingPrompt.hidden = YES;
222 connectDevicePrompt.hidden = !!_devices.count;
225 - (void)hidManagerDidStop:(NJHIDManager *)manager {
226 [_devices removeAllObjects];
227 [outlineView reloadData];
228 hidSleepingPrompt.hidden = NO;
229 connectDevicePrompt.hidden = YES;
240 - (NJInput *)selectedInput {
241 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
242 return (!item.children && item.base) ? item : nil;
245 - (NSInteger)outlineView:(NSOutlineView *)outlineView
246 numberOfChildrenOfItem:(id <NJInputPathElement>)item {
247 return item ? item.children.count : _devices.count;
250 - (BOOL)outlineView:(NSOutlineView *)outlineView
251 isItemExpandable:(id <NJInputPathElement>)item {
252 return item ? [[item children] count] > 0: YES;
255 - (id)outlineView:(NSOutlineView *)outlineView
256 child:(NSInteger)index
257 ofItem:(id <NJInputPathElement>)item {
258 return item ? item.children[index] : _devices[index];
261 - (id)outlineView:(NSOutlineView *)outlineView
262 objectValueForTableColumn:(NSTableColumn *)tableColumn
263 byItem:(id <NJInputPathElement>)item {
264 return item ? item.name : @"root";
267 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
268 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
270 [NSUserDefaults.standardUserDefaults setObject:item.uid
271 forKey:@"selected input"];
272 [outputController loadCurrent];
275 - (BOOL)outlineView:(NSOutlineView *)outlineView
276 isGroupItem:(id <NJInputPathElement>)item {
277 return [item isKindOfClass:NJDevice.class];
280 - (BOOL)outlineView:(NSOutlineView *)outlineView_
281 shouldSelectItem:(id <NJInputPathElement>)item {
282 return ![self outlineView:outlineView_ isGroupItem:item];
285 - (void)outlineViewItemDidExpand:(NSNotification *)notification {
286 id <NJInputPathElement> item = notification.userInfo[@"NSObject"];
287 NSString *uid = item.uid;
288 if (![_expanded containsObject:uid])
289 [_expanded addObject:uid];
290 while (_expanded.count > EXPANDED_MEMORY_MAX_SIZE)
291 [_expanded removeObjectAtIndex:0];
292 [NSUserDefaults.standardUserDefaults setObject:_expanded
293 forKey:@"expanded rows"];
296 - (void)outlineViewItemDidCollapse:(NSNotification *)notification {
297 id <NJInputPathElement> item = notification.userInfo[@"NSObject"];
298 [_expanded removeObject:item.uid];
299 [NSUserDefaults.standardUserDefaults setObject:_expanded
300 forKey:@"expanded rows"];
303 - (void)setTranslatingEvents:(BOOL)translatingEvents {
304 if (translatingEvents != _translatingEvents) {
305 _translatingEvents = translatingEvents;
306 NSInteger state = translatingEvents ? NSOnState : NSOffState;
307 translatingEventsButton.state = state;
308 NSString *name = translatingEvents
309 ? NJEventTranslationActivated
310 : NJEventTranslationDeactivated;
311 [NSNotificationCenter.defaultCenter postNotificationName:name
314 if (!translatingEvents && !NSApplication.sharedApplication.isActive)
316 else if (translatingEvents || NSApplication.sharedApplication.isActive)
321 - (void)reexpandAll {
322 for (NSString *uid in [_expanded copy])
323 [self expandRecursiveByUID:uid];
324 if (outlineView.selectedRow == -1) {
325 NSString *selectedUid = [NSUserDefaults.standardUserDefaults objectForKey:@"selected input"];
326 id item = [self elementForUID:selectedUid];
327 NSInteger row = [outlineView rowForItem:item];
329 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
333 - (void)stopHidIfDisabled:(NSNotification *)application {
334 if (!self.translatingEvents)
338 - (IBAction)translatingEventsChanged:(NSButton *)sender {
339 self.translatingEvents = sender.state == NSOnState;