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