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