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