Write proper constructors for NJInput and subclasses. Rename base to parent to match...
[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:(NJInputPathElement *)pathElement {
85 if (pathElement) {
86 [self expandRecursive:pathElement.parent];
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 // Since the error shows the window, it can trigger another attempt
214 // to re-open the HID manager, which will also probably fail and error,
215 // so don't bother repeating ourselves.
216 if (!outlineView.window.attachedSheet) {
217 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
218 [outlineView.window makeKeyAndOrderFront:nil];
219 [outlineView.window presentError:error
220 modalForWindow:outlineView.window
221 delegate:nil
222 didPresentSelector:nil
223 contextInfo:nil];
224 }
225 self.translatingEvents = NO;
226 if (manager.running)
227 [self hidManagerDidStart:manager];
228 else
229 [self hidManagerDidStop:manager];
230 }
231
232 - (void)hidManagerDidStart:(NJHIDManager *)manager {
233 hidSleepingPrompt.hidden = YES;
234 connectDevicePrompt.hidden = !!_devices.count;
235 }
236
237 - (void)hidManagerDidStop:(NJHIDManager *)manager {
238 [_devices removeAllObjects];
239 [outlineView reloadData];
240 hidSleepingPrompt.hidden = NO;
241 connectDevicePrompt.hidden = YES;
242 }
243
244 - (void)startHid {
245 [_hidManager start];
246 }
247
248 - (void)stopHid {
249 [_hidManager stop];
250 }
251
252 - (NJInput *)selectedInput {
253 NJInputPathElement *item = [outlineView itemAtRow:outlineView.selectedRow];
254 return (NJInput *)((!item.children && item.parent) ? item : nil);
255 }
256
257 - (NSInteger)outlineView:(NSOutlineView *)outlineView
258 numberOfChildrenOfItem:(NJInputPathElement *)item {
259 return item ? item.children.count : _devices.count;
260 }
261
262 - (BOOL)outlineView:(NSOutlineView *)outlineView
263 isItemExpandable:(NJInputPathElement *)item {
264 return item ? [[item children] count] > 0: YES;
265 }
266
267 - (id)outlineView:(NSOutlineView *)outlineView
268 child:(NSInteger)index
269 ofItem:(NJInputPathElement *)item {
270 return item ? item.children[index] : _devices[index];
271 }
272
273 - (id)outlineView:(NSOutlineView *)outlineView
274 objectValueForTableColumn:(NSTableColumn *)tableColumn
275 byItem:(NJInputPathElement *)item {
276 return item ? item.name : @"root";
277 }
278
279 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
280 NJInputPathElement *item = [outlineView itemAtRow:outlineView.selectedRow];
281 if (item)
282 [NSUserDefaults.standardUserDefaults setObject:item.uid
283 forKey:@"selected input"];
284 [outputController loadCurrent];
285 }
286
287 - (BOOL)outlineView:(NSOutlineView *)outlineView
288 isGroupItem:(NJInputPathElement *)item {
289 return [item isKindOfClass:NJDevice.class];
290 }
291
292 - (BOOL)outlineView:(NSOutlineView *)outlineView_
293 shouldSelectItem:(NJInputPathElement *)item {
294 return ![self outlineView:outlineView_ isGroupItem:item];
295 }
296
297 - (void)outlineViewItemDidExpand:(NSNotification *)notification {
298 NJInputPathElement *item = notification.userInfo[@"NSObject"];
299 NSString *uid = item.uid;
300 if (![_expanded containsObject:uid])
301 [_expanded addObject:uid];
302 while (_expanded.count > EXPANDED_MEMORY_MAX_SIZE)
303 [_expanded removeObjectAtIndex:0];
304 [NSUserDefaults.standardUserDefaults setObject:_expanded
305 forKey:@"expanded rows"];
306 }
307
308 - (void)outlineViewItemDidCollapse:(NSNotification *)notification {
309 NJInputPathElement *item = notification.userInfo[@"NSObject"];
310 [_expanded removeObject:item.uid];
311 [NSUserDefaults.standardUserDefaults setObject:_expanded
312 forKey:@"expanded rows"];
313 }
314
315 - (void)setTranslatingEvents:(BOOL)translatingEvents {
316 if (translatingEvents != _translatingEvents) {
317 _translatingEvents = translatingEvents;
318 NSInteger state = translatingEvents ? NSOnState : NSOffState;
319 translatingEventsButton.state = state;
320 NSString *name = translatingEvents
321 ? NJEventTranslationActivated
322 : NJEventTranslationDeactivated;
323 [NSNotificationCenter.defaultCenter postNotificationName:name
324 object:self];
325
326 if (!translatingEvents && !NSApplication.sharedApplication.isActive)
327 [self stopHid];
328 else
329 [self startHid];
330 }
331 }
332
333 - (void)reexpandAll {
334 for (NSString *uid in [_expanded copy])
335 [self expandRecursiveByUID:uid];
336 if (outlineView.selectedRow == -1) {
337 NSString *selectedUid = [NSUserDefaults.standardUserDefaults objectForKey:@"selected input"];
338 id item = [self elementForUID:selectedUid];
339 NSInteger row = [outlineView rowForItem:item];
340 if (row >= 0)
341 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
342 }
343 }
344
345 - (void)stopHidIfDisabled:(NSNotification *)application {
346 if (!self.translatingEvents)
347 [self stopHid];
348 }
349
350 - (IBAction)translatingEventsChanged:(NSButton *)sender {
351 self.translatingEvents = sender.state == NSOnState;
352 }
353
354 @end