Still need to prevent duplicates for axes.
[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 IOHIDManagerRef _hidManager;
20 NSTimer *_continuousOutputsTick;
21 NSMutableArray *_continousOutputs;
22 NSMutableArray *_devices;
23 }
24
25 - (id)init {
26 if ((self = [super init])) {
27 _devices = [[NSMutableArray alloc] initWithCapacity:16];
28 _continousOutputs = [[NSMutableArray alloc] initWithCapacity:32];
29 [NSNotificationCenter.defaultCenter
30 addObserver:self
31 selector:@selector(setup)
32 name:NSApplicationDidFinishLaunchingNotification
33 object:nil];
34 }
35 return self;
36 }
37
38 - (void)dealloc {
39 [_continuousOutputsTick invalidate];
40 IOHIDManagerClose(_hidManager, kIOHIDOptionsTypeNone);
41 CFRelease(_hidManager);
42 }
43
44 - (void)expandRecursive:(id <NJInputPathElement>)pathElement {
45 if (pathElement) {
46 [self expandRecursive:pathElement.base];
47 [outlineView expandItem:pathElement];
48 }
49 }
50
51 - (void)addRunningOutput:(NJOutput *)output {
52 // Axis events will trigger every small movement, don't keep
53 // re-adding them or they trigger multiple times each time.
54 if (![_continousOutputs containsObject:output])
55 [_continousOutputs addObject:output];
56 if (!_continuousOutputsTick) {
57 _continuousOutputsTick = [NSTimer scheduledTimerWithTimeInterval:1.0/60.0
58 target:self
59 selector:@selector(updateContinuousOutputs:)
60 userInfo:nil
61 repeats:YES];
62 }
63 }
64
65 - (void)runOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
66 NJDevice *dev = [self findDeviceByRef:device];
67 NJInput *mainInput = [dev inputForEvent:value];
68 [mainInput notifyEvent:value];
69 NSArray *children = mainInput.children ? mainInput.children : mainInput ? @[mainInput] : @[];
70 for (NJInput *subInput in children) {
71 NJOutput *output = mappingsController.currentMapping[subInput];
72 output.magnitude = subInput.magnitude;
73 output.running = subInput.active;
74 if ((output.running || output.magnitude) && output.isContinuous)
75 [self addRunningOutput:output];
76 }
77 }
78
79 - (void)showOutputForDevice:(IOHIDDeviceRef)device value:(IOHIDValueRef)value {
80 NJDevice *dev = [self findDeviceByRef:device];
81 NJInput *handler = [dev handlerForEvent:value];
82 if (!handler)
83 return;
84
85 [self expandRecursive:handler];
86 [outlineView selectRowIndexes:[NSIndexSet indexSetWithIndex:[outlineView rowForItem:handler]]
87 byExtendingSelection: NO];
88 [outputController focusKey];
89 }
90
91 static void input_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDValueRef value) {
92 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
93 IOHIDDeviceRef device = IOHIDQueueGetDevice(inSender);
94
95 if (controller.translatingEvents) {
96 [controller runOutputForDevice:device value:value];
97 } else if ([NSApplication sharedApplication].mainWindow.isVisible) {
98 [controller showOutputForDevice:device value:value];
99 }
100 }
101
102 static int findAvailableIndex(NSArray *list, NJDevice *dev) {
103 for (int index = 1; ; index++) {
104 BOOL available = YES;
105 for (NJDevice *used in list) {
106 if ([used.productName isEqualToString:dev.productName] && used.index == index) {
107 available = NO;
108 break;
109 }
110 }
111 if (available)
112 return index;
113 }
114 }
115
116 - (void)addDeviceForDevice:(IOHIDDeviceRef)device {
117 IOHIDDeviceRegisterInputValueCallback(device, input_callback, (__bridge void *)self);
118 NJDevice *dev = [[NJDevice alloc] initWithDevice:device];
119 dev.index = findAvailableIndex(_devices, dev);
120 [_devices addObject:dev];
121 [outlineView reloadData];
122 connectDevicePrompt.hidden = !!_devices.count;
123 }
124
125 static void add_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
126 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
127 [controller addDeviceForDevice:device];
128 }
129
130 - (NJDevice *)findDeviceByRef:(IOHIDDeviceRef)device {
131 for (NJDevice *dev in _devices)
132 if (dev.device == device)
133 return dev;
134 return nil;
135 }
136
137 static void remove_callback(void *ctx, IOReturn inResult, void *inSender, IOHIDDeviceRef device) {
138 NJDeviceController *controller = (__bridge NJDeviceController *)ctx;
139 [controller removeDeviceForDevice:device];
140 }
141
142 - (void)removeDeviceForDevice:(IOHIDDeviceRef)device {
143 NJDevice *match = [self findDeviceByRef:device];
144 IOHIDDeviceRegisterInputValueCallback(device, NULL, NULL);
145 if (match) {
146 [_devices removeObject:match];
147 [outlineView reloadData];
148 connectDevicePrompt.hidden = !!_devices.count;
149 }
150 if (_devices.count == 1)
151 [outlineView expandItem:_devices[0]];
152 }
153
154 - (void)updateContinuousOutputs:(NSTimer *)timer {
155 self.mouseLoc = [NSEvent mouseLocation];
156 for (NJOutput *output in [_continousOutputs copy]) {
157 if (![output update:self]) {
158 [_continousOutputs removeObject:output];
159 }
160 }
161 if (!_continousOutputs.count) {
162 [_continuousOutputsTick invalidate];
163 _continuousOutputsTick = nil;
164 }
165 }
166
167 #define NSSTR(e) ((NSString *)CFSTR(e))
168
169 - (void)setup {
170 _hidManager = IOHIDManagerCreate(kCFAllocatorDefault, kIOHIDOptionsTypeNone);
171 NSArray *criteria = @[ @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
172 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_Joystick) },
173 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
174 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_GamePad) },
175 @{ NSSTR(kIOHIDDeviceUsagePageKey) : @(kHIDPage_GenericDesktop),
176 NSSTR(kIOHIDDeviceUsageKey) : @(kHIDUsage_GD_MultiAxisController) }
177 ];
178 IOHIDManagerSetDeviceMatchingMultiple(_hidManager, (__bridge CFArrayRef)criteria);
179
180 IOHIDManagerScheduleWithRunLoop(_hidManager, CFRunLoopGetCurrent(), kCFRunLoopDefaultMode);
181 IOReturn ret = IOHIDManagerOpen(_hidManager, kIOHIDOptionsTypeNone);
182 if (ret != kIOReturnSuccess) {
183 [[NSAlert alertWithMessageText:@"Input devices are unavailable"
184 defaultButton:nil
185 alternateButton:nil
186 otherButton:nil
187 informativeTextWithFormat:@"Error 0x%08x occured trying to access your devices. "
188 @"Input may not be correctly detected or mapped.",
189 ret]
190 beginSheetModalForWindow:outlineView.window
191 modalDelegate:nil
192 didEndSelector:nil
193 contextInfo:nil];
194 }
195
196 IOHIDManagerRegisterDeviceMatchingCallback(_hidManager, add_callback, (__bridge void *)self);
197 IOHIDManagerRegisterDeviceRemovalCallback(_hidManager, remove_callback, (__bridge void *)self);
198 }
199
200 - (NJInput *)selectedInput {
201 id <NJInputPathElement> item = [outlineView itemAtRow:outlineView.selectedRow];
202 return (!item.children && item.base) ? item : nil;
203 }
204
205 - (NSInteger)outlineView:(NSOutlineView *)outlineView
206 numberOfChildrenOfItem:(id <NJInputPathElement>)item {
207 return item ? item.children.count : _devices.count;
208 }
209
210 - (BOOL)outlineView:(NSOutlineView *)outlineView
211 isItemExpandable:(id <NJInputPathElement>)item {
212 return item ? [[item children] count] > 0: YES;
213 }
214
215 - (id)outlineView:(NSOutlineView *)outlineView
216 child:(NSInteger)index
217 ofItem:(id <NJInputPathElement>)item {
218 return item ? item.children[index] : _devices[index];
219 }
220
221 - (id)outlineView:(NSOutlineView *)outlineView
222 objectValueForTableColumn:(NSTableColumn *)tableColumn
223 byItem:(id <NJInputPathElement>)item {
224 return item ? item.name : @"root";
225 }
226
227 - (void)outlineViewSelectionDidChange:(NSNotification *)notification {
228 [outputController loadCurrent];
229 }
230
231 - (BOOL)outlineView:(NSOutlineView *)outlineView
232 isGroupItem:(id <NJInputPathElement>)item {
233 return [item isKindOfClass:NJDevice.class];
234 }
235
236 - (BOOL)outlineView:(NSOutlineView *)outlineView_
237 shouldSelectItem:(id <NJInputPathElement>)item {
238 return ![self outlineView:outlineView_ isGroupItem:item];
239 }
240
241 - (void)setTranslatingEvents:(BOOL)translatingEvents {
242 if (translatingEvents != _translatingEvents) {
243 _translatingEvents = translatingEvents;
244 NSInteger state = translatingEvents ? NSOnState : NSOffState;
245 translatingEventsButton.state = state;
246 NSString *name = translatingEvents
247 ? NJEventTranslationActivated
248 : NJEventTranslationDeactivated;
249 [NSNotificationCenter.defaultCenter postNotificationName:name
250 object:self];
251 }
252 }
253
254 - (IBAction)translatingEventsChanged:(NSButton *)sender {
255 self.translatingEvents = sender.state == NSOnState;
256 }
257
258 @end