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