Split actual IOKit HID interfacing off from NJDeviceController.
[enjoyable.git] / Classes / NJDeviceController.m
1 //
2 // NJDeviceController.m
3 // Enjoy
4 //
5 // Created by Sam McCall on 4/05/09.
6 //
7
8 #import "NJDeviceController.h"
9
10 #import "NJMapping.h"
11 #import "NJMappingsController.h"
12 #import "NJDevice.h"
13 #import "NJInput.h"
14 #import "NJOutput.h"
15 #import "NJOutputController.h"
16 #import "NJEvents.h"
17
18 @implementation NJDeviceController {
19 NJHIDManager *_hidManager;
20 NSTimer *_continuousOutputsTick;
21 NSMutableArray *_continousOutputs;
22 NSMutableArray *_devices;
23 NSMutableArray *_expanded;
24 }
25
26 #define EXPANDED_MEMORY_MAX_SIZE 100
27 #define NSSTR(e) ((NSString *)CFSTR(e))
28
29 - (id)init {
30 if ((self = [super init])) {
31 _devices = [[NSMutableArray alloc] initWithCapacity:16];
32 _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
33
34 NSArray *expanded = [NSUserDefaults.standardUserDefaults objectForKey:@"expanded rows"];
35 if (![expanded isKindOfClass:NSArray.class])
36 expanded = @[];
37 _expanded = [[NSMutableArray alloc] initWithCapacity:MAX(16, _expanded.count)];
38 [_expanded addObjectsFromArray:expanded];
39
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) }
47 ]
48 delegate:self];
49
50 [NSNotificationCenter.defaultCenter
51 addObserver:self
52 selector:@selector(startHid)
53 name:NSApplicationDidFinishLaunchingNotification
54 object:nil];
55
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
64 // grabs.
65 [NSNotificationCenter.defaultCenter
66 addObserver:self
67 selector:@selector(stopHidIfDisabled:)
68 name:NSApplicationDidResignActiveNotification
69 object:nil];
70 [NSNotificationCenter.defaultCenter
71 addObserver:self
72 selector:@selector(startHid)
73 name:NSApplicationDidBecomeActiveNotification
74 object:nil];
75 }
76 return self;
77 }
78
79 - (void)dealloc {
80 [NSNotificationCenter.defaultCenter removeObserver:self];
81 [_continuousOutputsTick invalidate];
82 }
83
84 - (void)expandRecursive:(id <NJInputPathElement>)pathElement {
85 if (pathElement) {
86 [self expandRecursive:pathElement.base];
87 [outlineView expandItem:pathElement];
88 }
89 }
90
91 - (id)elementForUID:(NSString *)uid {
92 for (NJDevice *dev in _devices) {
93 id item = [dev elementForUID:uid];
94 if (item)
95 return item;
96 }
97 return nil;
98 }
99
100 - (void)expandRecursiveByUID:(NSString *)uid {
101 [self expandRecursive:[self elementForUID:uid]];
102 }
103
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
111 target:self
112 selector:@selector(updateContinuousOutputs:)
113 userInfo:nil
114 repeats:YES];
115 }
116 }
117
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];
129 }
130 }
131
132 - (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
133 NJDevice *dev = [self findDeviceByRef:device];
134 NJInput *handler = [dev handlerForEvent:value];
135 if (!handler)
136 return;
137
138 [self expandRecursive:handler];
139 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
140 byExtendingSelection: NO];
141 [outputController focusKey];
142 }
143
144 - (void)hidManager:(NJHIDManager *)manager
145 valueChanged:(IOHIDValueRef)value
146 fromDevice:(IOHIDDeviceRef)device {
147 if (self.translatingEvents) {
148 [self runOutputForDevice:device value:value];
149 } else {
150 [self showOutputForDevice:device value:value];
151 }
152 }
153
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) {
160 available = NO;
161 break;
162 }
163 }
164 if (available)
165 return index;
166 }
167 }
168
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];
174 [self reexpandAll];
175 hidSleepingPrompt.hidden = YES;
176 connectDevicePrompt.hidden = !!_devices.count;
177 }
178
179 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
180 for (NJDevice *dev in _devices)
181 if (dev.device == device)
182 return dev;
183 return nil;
184 }
185
186 - (void)hidManager:(NJHIDManager *)manager deviceRemoved:(IOHIDDeviceRef)device {
187 NJDevice *match = [self findDeviceByRef:device];
188 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
189 if (match) {
190 [_devices removeObject:match];
191 [outlineView reloadData];
192 connectDevicePrompt.hidden = !!_devices.count;
193 hidSleepingPrompt.hidden = YES;
194 }
195 if (_devices.count == 1)
196 [outlineView expandItem:_devices[0]];
197 }
198
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];
204 }
205 }
206 if (!_continousOutputs.count) {
207 [_continuousOutputsTick invalidate];
208 _continuousOutputsTick = nil;
209 }
210 }
211
212 - (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error {
213 [outlineView.window presentError:error
214 modalForWindow:outlineView.window
215 delegate:nil
216 didPresentSelector:nil
217 contextInfo:nil];
218 }
219
220 - (void)hidManagerDidStart:(NJHIDManager *)manager {
221 hidSleepingPrompt.hidden = YES;
222 connectDevicePrompt.hidden = !!_devices.count;
223 }
224
225 - (void)hidManagerDidStop:(NJHIDManager *)manager {
226 [_devices removeAllObjects];
227 [outlineView reloadData];
228 hidSleepingPrompt.hidden = NO;
229 connectDevicePrompt.hidden = YES;
230 }
231
232 - (void)startHid {
233 [_hidManager start];
234 }
235
236 - (void)stopHid {
237 [_hidManager stop];
238 }
239
240 - (NJInput *)selectedInput {
241 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
242 return (!item.children && item.base) ? item : nil;
243 }
244
245 - (NSInteger)outlineView:(NSOutlineView *)outlineView
246 numberOfChildrenOfItem:(id <NJInputPathElement>)item {
247 return item ? item.children.count : _devices.count;
248 }
249
250 - (BOOL)outlineView:(NSOutlineView *)outlineView
251 isItemExpandable:(id <NJInputPathElement>)item {
252 return item ? [[item children] count] > 0: YES;
253 }
254
255 - (id)outlineView:(NSOutlineView *)outlineView
256 child:(NSInteger)index
257 ofItem:(id <NJInputPathElement>)item {
258 return item ? item.children[index] : _devices[index];
259 }
260
261 - (id)outlineView:(NSOutlineView *)outlineView
262 objectValueForTableColumn:(NSTableColumn *)tableColumn
263 byItem:(id <NJInputPathElement>)item {
264 return item ? item.name : @"root";
265 }
266
267 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
268 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
269 if (item)
270 [NSUserDefaults.standardUserDefaults setObject:item.uid
271 forKey:@"selected input"];
272 [outputController loadCurrent];
273 }
274
275 - (BOOL)outlineView:(NSOutlineView *)outlineView
276 isGroupItem:(id <NJInputPathElement>)item {
277 return [item isKindOfClass:NJDevice.class];
278 }
279
280 - (BOOL)outlineView:(NSOutlineView *)outlineView_
281 shouldSelectItem:(id <NJInputPathElement>)item {
282 return ![self outlineView:outlineView_ isGroupItem:item];
283 }
284
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"];
294 }
295
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"];
301 }
302
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
312 object:self];
313
314 if (!translatingEvents && !NSApplication.sharedApplication.isActive)
315 [self stopHid];
316 else if (translatingEvents || NSApplication.sharedApplication.isActive)
317 [self startHid];
318 }
319 }
320
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];
328 if (row >= 0)
329 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
330 }
331 }
332
333 - (void)stopHidIfDisabled:(NSNotification *)application {
334 if (!self.translatingEvents)
335 [self stopHid];
336 }
337
338 - (IBAction)translatingEventsChanged:(NSButton *)sender {
339 self.translatingEvents = sender.state == NSOnState;
340 }
341
342 @end