b0e8de37e701f8c88aeba0b0c5e8baa19525ee6d
[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 #import "NJDeviceViewController.h"
18
19 @implementation NJDeviceController {
20 NJHIDManager *_hidManager;
21 NSTimer *_continuousOutputsTick;
22 NSMutableArray *_continousOutputs;
23 NSMutableArray *_devices;
24 }
25
26 #define NSSTR(e) ((NSString *)CFSTR(e))
27
28 - (id)init {
29 if ((self = [super init])) {
30 _devices = [[NSMutableArray alloc] initWithCapacity:16];
31 _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
32
33 _hidManager = [[NJHIDManager alloc] initWithCriteria:@[
34 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
35 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
36 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
37 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
38 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
39 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
40 ]
41 delegate:self];
42
43 // The HID manager uses 5-10ms per second doing basically
44 // nothing if a noisy device is plugged in (the % of that
45 // spent in input_callback is negligible, so it's not
46 // something we can make faster). I don't really think that's
47 // acceptable, CPU/power wise. So if simulation is disabled
48 // and the window is closed, just switch off the HID manager
49 // entirely. This probably also has some marginal benefits for
50 // compatibility with other applications that want exclusive
51 // grabs.
52 [NSNotificationCenter.defaultCenter
53 addObserver:self
54 selector:@selector(stopHidIfDisabled:)
55 name:NSApplicationDidResignActiveNotification
56 object:nil];
57 [NSNotificationCenter.defaultCenter
58 addObserver:self
59 selector:@selector(startHid)
60 name:NSApplicationDidBecomeActiveNotification
61 object:nil];
62 }
63 return self;
64 }
65
66 - (void)dealloc {
67 [NSNotificationCenter.defaultCenter removeObserver:self];
68 [_continuousOutputsTick invalidate];
69 }
70
71 - (id)elementForUID:(NSString *)uid {
72 for (NJDevice *dev in _devices) {
73 id item = [dev elementForUID:uid];
74 if (item)
75 return item;
76 }
77 return nil;
78 }
79
80 - (void)addRunningOutput:(NJOutput *)output {
81 // Axis events will trigger every small movement, don't keep
82 // re-adding them or they trigger multiple times each time.
83 if (![_continousOutputs containsObject:output])
84 [_continousOutputs addObject:output];
85 if (!_continuousOutputsTick) {
86 _continuousOutputsTick = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
87 target:self
88 selector:@selector(updateContinuousOutputs:)
89 userInfo:nil
90 repeats:YES];
91 }
92 }
93
94 - (void)runOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
95 NJDevice *dev = [self findDeviceByRef:device];
96 NJInput *mainInput = [dev inputForEvent:value];
97 [mainInput notifyEvent:value];
98 NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[];
99 for (NJInput *subInput in children) {
100 NJOutput *output = mappingsController.currentMapping[subInput];
101 output.magnitude = subInput.magnitude;
102 output.running = subInput.active;
103 if ((output.running || output.magnitude) && output.isContinuous)
104 [self addRunningOutput:output];
105 }
106 }
107
108 - (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
109 NJDevice *dev = [self findDeviceByRef:device];
110 NJInput *handler = [dev handlerForEvent:value];
111 if (!handler)
112 return;
113
114 [devicesViewController expandAndSelectItem:handler];
115 [outputController focusKey];
116 }
117
118 - (void)hidManager:(NJHIDManager *)manager
119 valueChanged:(IOHIDValueRef)value
120 fromDevice:(IOHIDDeviceRef)device {
121 if (self.simulatingEvents
122 && !NSApplication.sharedApplication.isActive) {
123 [self runOutputForDevice:device value:value];
124 } else {
125 [self showOutputForDevice:device value:value];
126 }
127 }
128
129 - (void)hidManager:(NJHIDManager *)manager deviceAdded:(IOHIDDeviceRef)device {
130 NJDevice *match = [[NJDevice alloc] initWithDevice:device];
131 match.index = 1;
132 BOOL available;
133 do {
134 available = YES;
135 for (NJDevice *used in _devices) {
136 if ([used isEqual:match]) {
137 match.index += 1;
138 available = NO;
139 }
140 }
141 } while (!available);
142
143 [_devices addObject:match];
144 [devicesViewController addedDevice:match atIndex:_devices.count - 1];
145 }
146
147 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
148 for (NJDevice *dev in _devices)
149 if (dev.device == device)
150 return dev;
151 return nil;
152 }
153
154 - (void)hidManager:(NJHIDManager *)manager deviceRemoved:(IOHIDDeviceRef)device {
155 NJDevice *match = [self findDeviceByRef:device];
156 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
157 if (match) {
158 NSInteger idx = [_devices indexOfObjectIdenticalTo:match];
159 [_devices removeObjectAtIndex:idx];
160 [devicesViewController removedDevice:match atIndex:idx];
161 }
162 }
163
164 - (void)updateContinuousOutputs:(NSTimer *)timer {
165 self.mouseLoc = [NSEvent mouseLocation];
166 for (NJOutput *output in [_continousOutputs copy]) {
167 if (![output update:self]) {
168 [_continousOutputs removeObject:output];
169 }
170 }
171 if (!_continousOutputs.count) {
172 [_continuousOutputsTick invalidate];
173 _continuousOutputsTick = nil;
174 }
175 }
176
177 - (void)hidManager:(NJHIDManager *)manager didError:(NSError *)error {
178 // Since the error shows the window, it can trigger another attempt
179 // to re-open the HID manager, which will also probably fail and error,
180 // so don't bother repeating ourselves.
181 if (!simulatingEventsButton.window.attachedSheet) {
182 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
183 [simulatingEventsButton.window makeKeyAndOrderFront:nil];
184 [simulatingEventsButton.window presentError:error
185 modalForWindow:simulatingEventsButton.window
186 delegate:nil
187 didPresentSelector:nil
188 contextInfo:nil];
189 }
190 self.simulatingEvents = NO;
191 }
192
193 - (void)hidManagerDidStart:(NJHIDManager *)manager {
194 [devicesViewController hidStarted];
195 }
196
197 - (void)hidManagerDidStop:(NJHIDManager *)manager {
198 [_devices removeAllObjects];
199 [devicesViewController hidStopped];
200 }
201
202 - (void)startHid {
203 [_hidManager start];
204 }
205
206 - (void)stopHid {
207 [_hidManager stop];
208 }
209
210 - (NJInput *)selectedInput {
211 return (NJInput *)devicesViewController.selectedHandler;
212 }
213
214 - (void)setSimulatingEvents:(BOOL)simulatingEvents {
215 if (simulatingEvents != _simulatingEvents) {
216 _simulatingEvents = simulatingEvents;
217 NSInteger state = simulatingEvents ? NSOnState : NSOffState;
218 simulatingEventsButton.state = state;
219 NSString *name = simulatingEvents
220 ? NJEventSimulationStarted
221 : NJEventSimulationStopped;
222 [NSNotificationCenter.defaultCenter postNotificationName:name
223 object:self];
224
225 if (!simulatingEvents && !NSApplication.sharedApplication.isActive)
226 [self stopHid];
227 else
228 [self startHid];
229 }
230 }
231
232 - (void)stopHidIfDisabled:(NSNotification *)application {
233 if (!self.simulatingEvents)
234 [self stopHid];
235 }
236
237 - (IBAction)simulatingEventsChanged:(NSButton *)sender {
238 self.simulatingEvents = sender.state == NSOnState;
239 }
240
241 - (void)deviceViewControllerDidSelectNothing:(NJDeviceViewController *)devices {
242 [outputController loadCurrent];
243 }
244
245 - (void)deviceViewController:(NJDeviceViewController *)devices
246 didSelectBranch:(NJInputPathElement *)handler {
247 [outputController loadCurrent];
248 }
249
250 - (void)deviceViewController:(NJDeviceViewController *)devices
251 didSelectHandler:(NJInputPathElement *)handler {
252 [outputController loadCurrent];
253 }
254
255 - (void)deviceViewController:(NJDeviceViewController *)devices
256 didSelectDevice:(NJInputPathElement *)device {
257 [outputController loadCurrent];
258 }
259
260 @end