Missed a translating -> simulating.
[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 simulation 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 if (!self.simulatingEvents)
142 [outputController focusKey];
143 }
144
145 - (void)hidManager:(NJHIDManager *)manager
146 valueChanged:(IOHIDValueRef)value
147 fromDevice:(IOHIDDeviceRef)device {
148 if (self.simulatingEvents
149 && !NSApplication.sharedApplication.isActive) {
150 [self runOutputForDevice:device value:value];
151 } else {
152 [self showOutputForDevice:device value:value];
153 }
154 }
155
156 static int findAvailableIndex(NSArray *list, NJDevice *dev) {
157 for (int index = 1; ; index++) {
158 BOOL available = YES;
159 for (NJDevice *used in list) {
160 if ([used.productName isEqualToString:dev.productName]
161 && used.index == index) {
162 available = NO;
163 break;
164 }
165 }
166 if (available)
167 return index;
168 }
169 }
170
171 - (void)hidManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device {
172 NJDevice *match = [[NJDevice alloc] initWithDevice:device];
173 match.index = findAvailableIndex(_devices, match);
174 [_devices addObject:match];
175 [outlineView reloadData];
176 [self reexpandAll];
177 hidSleepingPrompt.hidden = YES;
178 connectDevicePrompt.hidden = !!_devices.count;
179 }
180
181 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
182 for (NJDevice *dev in _devices)
183 if (dev.device == device)
184 return dev;
185 return nil;
186 }
187
188 - (void)hidManager:(NJHIDManager *)manager deviceRemoved:(IOHIDDeviceRef)device {
189 NJDevice *match = [self findDeviceByRef:device];
190 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
191 if (match) {
192 [_devices removeObject:match];
193 [outlineView reloadData];
194 connectDevicePrompt.hidden = !!_devices.count;
195 hidSleepingPrompt.hidden = YES;
196 }
197 if (_devices.count == 1)
198 [outlineView expandItem:_devices[0]];
199 }
200
201 - (void)updateContinuousOutputs:(NSTimer *)timer {
202 self.mouseLoc = [NSEvent mouseLocation];
203 for (NJOutput *output in [_continousOutputs copy]) {
204 if (![output update:self]) {
205 [_continousOutputs removeObject:output];
206 }
207 }
208 if (!_continousOutputs.count) {
209 [_continuousOutputsTick invalidate];
210 _continuousOutputsTick = nil;
211 }
212 }
213
214 - (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error {
215 // Since the error shows the window, it can trigger another attempt
216 // to re-open the HID manager, which will also probably fail and error,
217 // so don't bother repeating ourselves.
218 if (!outlineView.window.attachedSheet) {
219 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
220 [outlineView.window makeKeyAndOrderFront:nil];
221 [outlineView.window presentError:error
222 modalForWindow:outlineView.window
223 delegate:nil
224 didPresentSelector:nil
225 contextInfo:nil];
226 }
227 self.simulatingEvents = NO;
228 if (manager.running)
229 [self hidManagerDidStart:manager];
230 else
231 [self hidManagerDidStop:manager];
232 }
233
234 - (void)hidManagerDidStart:(NJHIDManager *)manager {
235 hidSleepingPrompt.hidden = YES;
236 connectDevicePrompt.hidden = !!_devices.count;
237 }
238
239 - (void)hidManagerDidStop:(NJHIDManager *)manager {
240 [_devices removeAllObjects];
241 [outlineView reloadData];
242 hidSleepingPrompt.hidden = NO;
243 connectDevicePrompt.hidden = YES;
244 }
245
246 - (void)startHid {
247 [_hidManager start];
248 }
249
250 - (void)stopHid {
251 [_hidManager stop];
252 }
253
254 - (NJInput *)selectedInput {
255 NJInputPathElement *item = [outlineView itemAtRow:outlineView.selectedRow];
256 return (NJInput *)((!item.children && item.parent) ? item : nil);
257 }
258
259 - (NSInteger)outlineView:(NSOutlineView *)outlineView
260 numberOfChildrenOfItem:(NJInputPathElement *)item {
261 return item ? item.children.count : _devices.count;
262 }
263
264 - (BOOL)outlineView:(NSOutlineView *)outlineView
265 isItemExpandable:(NJInputPathElement *)item {
266 return item ? [[item children] count] > 0: YES;
267 }
268
269 - (id)outlineView:(NSOutlineView *)outlineView
270 child:(NSInteger)index
271 ofItem:(NJInputPathElement *)item {
272 return item ? item.children[index] : _devices[index];
273 }
274
275 - (id)outlineView:(NSOutlineView *)outlineView
276 objectValueForTableColumn:(NSTableColumn *)tableColumn
277 byItem:(NJInputPathElement *)item {
278 return item ? item.name : @"root";
279 }
280
281 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
282 NJInputPathElement *item = [outlineView itemAtRow:outlineView.selectedRow];
283 if (item)
284 [NSUserDefaults.standardUserDefaults setObject:item.uid
285 forKey:@"selected input"];
286 [outputController loadCurrent];
287 }
288
289 - (BOOL)outlineView:(NSOutlineView *)outlineView
290 isGroupItem:(NJInputPathElement *)item {
291 return [item isKindOfClass:NJDevice.class];
292 }
293
294 - (BOOL)outlineView:(NSOutlineView *)outlineView_
295 shouldSelectItem:(NJInputPathElement *)item {
296 return ![self outlineView:outlineView_ isGroupItem:item];
297 }
298
299 - (void)outlineViewItemDidExpand:(NSNotification *)notification {
300 NJInputPathElement *item = notification.userInfo[@"NSObject"];
301 NSString *uid = item.uid;
302 if (![_expanded containsObject:uid])
303 [_expanded addObject:uid];
304 while (_expanded.count > EXPANDED_MEMORY_MAX_SIZE)
305 [_expanded removeObjectAtIndex:0];
306 [NSUserDefaults.standardUserDefaults setObject:_expanded
307 forKey:@"expanded rows"];
308 }
309
310 - (void)outlineViewItemDidCollapse:(NSNotification *)notification {
311 NJInputPathElement *item = notification.userInfo[@"NSObject"];
312 [_expanded removeObject:item.uid];
313 [NSUserDefaults.standardUserDefaults setObject:_expanded
314 forKey:@"expanded rows"];
315 }
316
317 - (void)setSimulatingEvents:(BOOL)simulatingEvents {
318 if (simulatingEvents != _simulatingEvents) {
319 _simulatingEvents = simulatingEvents;
320 NSInteger state = simulatingEvents ? NSOnState : NSOffState;
321 simulatingEventsButton.state = state;
322 NSString *name = simulatingEvents
323 ? NJEventSimulationStarted
324 : NJEventSimulationStopped;
325 [NSNotificationCenter.defaultCenter postNotificationName:name
326 object:self];
327
328 if (!simulatingEvents && !NSApplication.sharedApplication.isActive)
329 [self stopHid];
330 else
331 [self startHid];
332 }
333 }
334
335 - (void)reexpandAll {
336 for (NSString *uid in [_expanded copy])
337 [self expandRecursiveByUID:uid];
338 if (outlineView.selectedRow == -1) {
339 NSString *selectedUid = [NSUserDefaults.standardUserDefaults objectForKey:@"selected input"];
340 id item = [self elementForUID:selectedUid];
341 NSInteger row = [outlineView rowForItem:item];
342 if (row >= 0)
343 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:row] byExtendingSelection:NO];
344 }
345 }
346
347 - (void)stopHidIfDisabled:(NSNotification *)application {
348 if (!self.simulatingEvents)
349 [self stopHid];
350 }
351
352 - (IBAction)simulatingEventsChanged:(NSButton *)sender {
353 self.simulatingEvents = sender.state == NSOnState;
354 }
355
356 @end