Output view controller now has a delegate rather than a direct reference to the input...
[enjoyable.git] / Classes / EnjoyableApplicationDelegate.m
1 //
2 // EnjoyableApplicationDelegate.m
3 // Enjoy
4 //
5 // Created by Sam McCall on 4/05/09.
6 //
7
8 #import <Sparkle/Sparkle.h>
9
10 #import "EnjoyableApplicationDelegate.h"
11
12 #import "NJMapping.h"
13 #import "NJInput.h"
14 #import "NJEvents.h"
15
16 @implementation EnjoyableApplicationDelegate {
17 NSStatusItem *statusItem;
18 }
19
20 - (void)didSwitchApplication:(NSNotification *)note {
21 NSRunningApplication *activeApp = note.userInfo[NSWorkspaceApplicationKey];
22 if (activeApp)
23 [self.ic activateMappingForProcess:activeApp];
24 }
25
26 - (void)applicationWillFinishLaunching:(NSNotification *)notification {
27 [NSNotificationCenter.defaultCenter
28 addObserver:self
29 selector:@selector(mappingDidChange:)
30 name:NJEventMappingChanged
31 object:nil];
32 [NSNotificationCenter.defaultCenter
33 addObserver:self
34 selector:@selector(eventSimulationStarted:)
35 name:NJEventSimulationStarted
36 object:nil];
37 [NSNotificationCenter.defaultCenter
38 addObserver:self
39 selector:@selector(eventSimulationStopped:)
40 name:NJEventSimulationStopped
41 object:nil];
42
43 [self.ic load];
44 [self.mvc.mappingList reloadData];
45 [self.mvc changedActiveMappingToIndex:
46 [self.ic indexOfMapping:
47 self.ic.currentMapping]];
48
49 statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:36];
50 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
51 statusItem.highlightMode = YES;
52 statusItem.menu = self.statusItemMenu;
53 statusItem.target = self;
54 }
55
56 - (void)applicationDidFinishLaunching:(NSNotification *)notification {
57 if ([NSUserDefaults.standardUserDefaults boolForKey:@"hidden in status item"]
58 && NSRunningApplication.currentApplication.wasLaunchedAsLoginItemOrResume)
59 [self transformIntoElement:nil];
60 else
61 [self.window makeKeyAndOrderFront:nil];
62 }
63
64 - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication
65 hasVisibleWindows:(BOOL)flag {
66 [self restoreToForeground:theApplication];
67 return NO;
68 }
69
70 - (void)restoreToForeground:(id)sender {
71 ProcessSerialNumber psn = { 0, kCurrentProcess };
72 TransformProcessType(&psn, kProcessTransformToForegroundApplication);
73 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
74 [self.window makeKeyAndOrderFront:sender];
75 [NSObject cancelPreviousPerformRequestsWithTarget:self
76 selector:@selector(transformIntoElement:)
77 object:self];
78 [NSUserDefaults.standardUserDefaults setBool:NO forKey:@"hidden in status item"];
79 }
80
81 - (void)applicationWillBecomeActive:(NSNotification *)notification {
82 if (self.window.isVisible)
83 [self restoreToForeground:notification];
84 }
85
86 - (void)transformIntoElement:(id)sender {
87 ProcessSerialNumber psn = { 0, kCurrentProcess };
88 TransformProcessType(&psn, kProcessTransformToUIElementApplication);
89 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"hidden in status item"];
90 }
91
92 - (void)flashStatusItem {
93 if ([statusItem.image.name isEqualToString:@"Status Menu Icon"]) {
94 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
95 } else {
96 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
97 }
98
99 }
100
101 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
102 [theApplication hide:theApplication];
103 // If we turn into a UIElement right away, the application cancels
104 // the deactivation events. The dock icon disappears, but an
105 // unresponsive menu bar remains until the user clicks somewhere.
106 // So delay just long enough to be past the end handling that.
107 [self performSelector:@selector(transformIntoElement:) withObject:self afterDelay:0.001];
108 return NO;
109 }
110
111 - (void)eventSimulationStarted:(NSNotification *)note {
112 self.simulatingEventsButton.state = NSOnState;
113 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
114 [NSWorkspace.sharedWorkspace.notificationCenter
115 addObserver:self
116 selector:@selector(didSwitchApplication:)
117 name:NSWorkspaceDidActivateApplicationNotification
118 object:nil];
119 }
120
121 - (void)eventSimulationStopped:(NSNotification *)note {
122 self.simulatingEventsButton.state = NSOffState;
123 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
124 [NSWorkspace.sharedWorkspace.notificationCenter
125 removeObserver:self
126 name:NSWorkspaceDidActivateApplicationNotification
127 object:nil];
128 }
129
130 - (void)mappingDidChange:(NSNotification *)note {
131 NSUInteger idx = [note.userInfo[NJMappingIndexKey] intValue];
132 [self.mvc changedActiveMappingToIndex:idx];
133
134 if (!self.window.isVisible)
135 for (int i = 0; i < 4; ++i)
136 [self performSelector:@selector(flashStatusItem)
137 withObject:self
138 afterDelay:0.2 * i];
139 [self loadOutputForInput:self.dvc.selectedHandler];
140 }
141
142 - (NSMenu *)applicationDockMenu:(NSApplication *)sender {
143 return self.dockMenu;
144 }
145
146 - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
147 [self restoreToForeground:sender];
148 NSError *error;
149 NSURL *URL = [NSURL fileURLWithPath:filename];
150 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:URL
151 error:&error];
152 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
153 [self promptForMapping:mapping atIndex:self.ic.mappings.count];
154 } else if ([self.ic mappingForKey:mapping.name]) {
155 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
156 } else if (mapping) {
157 [self.mvc beginUpdates];
158 [self.ic addMapping:mapping];
159 [self.mvc addedMappingAtIndex:self.ic.mappings.count - 1 startEditing:NO];
160 [self.mvc endUpdates];
161 [self.ic activateMapping:mapping];
162 } else {
163 [self.window presentError:error
164 modalForWindow:self.window
165 delegate:nil
166 didPresentSelector:nil
167 contextInfo:nil];
168 }
169 return !!mapping;
170 }
171
172 - (void)mappingWasChosen:(NJMapping *)mapping {
173 [self.ic activateMapping:mapping];
174 }
175
176 - (void)mappingListShouldOpen {
177 [self restoreToForeground:self];
178 [self.mvc mappingTriggerClicked:self];
179 }
180
181 - (void)loginItemPromptDidEnd:(NSWindow *)sheet
182 returnCode:(int)returnCode
183 contextInfo:(void *)contextInfo {
184 if (returnCode == NSAlertDefaultReturn) {
185 [NSRunningApplication.currentApplication addToLoginItems];
186 // If we're going to automatically start, don't bug the user
187 // about automatic updates next boot - they probably want it,
188 // and if they don't they probably want a prompt for it less.
189 SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES;
190 }
191 }
192
193 - (void)loginItemPromptDidDismiss:(NSWindow *)sheet
194 returnCode:(int)returnCode
195 contextInfo:(void *)contextInfo {
196 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"explained login items"];
197 [self.window performClose:sheet];
198 }
199
200 - (BOOL)windowShouldClose:(NSWindow *)sender {
201 if (sender != self.window
202 || NSRunningApplication.currentApplication.isLoginItem
203 || [NSUserDefaults.standardUserDefaults boolForKey:@"explained login items"])
204 return YES;
205 NSBeginAlertSheet(
206 NSLocalizedString(@"login items prompt", @"alert prompt for adding to login items"),
207 NSLocalizedString(@"login items add button", @"button to add to login items"),
208 NSLocalizedString(@"login items don't add button", @"button to not add to login items"),
209 nil, self.window, self,
210 @selector(loginItemPromptDidEnd:returnCode:contextInfo:),
211 @selector(loginItemPromptDidDismiss:returnCode:contextInfo:),
212 NULL,
213 NSLocalizedString(@"login items explanation", @"a brief explanation of login items")
214 );
215 for (int i = 0; i < 10; ++i)
216 [self performSelector:@selector(flashStatusItem)
217 withObject:self
218 afterDelay:0.5 * i];
219 return NO;
220 }
221
222 - (void)importMappingClicked:(id)sender {
223 NSOpenPanel *panel = [NSOpenPanel openPanel];
224 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
225 [panel beginSheetModalForWindow:self.window
226 completionHandler:^(NSInteger result) {
227 if (result != NSFileHandlingPanelOKButton)
228 return;
229 [panel close];
230 NSError *error;
231 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:panel.URL
232 error:&error];
233 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
234 [self promptForMapping:mapping atIndex:self.ic.mappings.count];
235 } else if ([self.ic mappingForKey:mapping.name]) {
236 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
237 } else if (mapping) {
238 [self.ic addMapping:mapping];
239 } else {
240 [self.window presentError:error
241 modalForWindow:self.window
242 delegate:nil
243 didPresentSelector:nil
244 contextInfo:nil];
245 }
246 }];
247
248 }
249
250 - (void)exportMappingClicked:(id)sender {
251 NSSavePanel *panel = [NSSavePanel savePanel];
252 panel.allowedFileTypes = @[ @"enjoyable" ];
253 NJMapping *mapping = self.ic.currentMapping;
254 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
255 [panel beginSheetModalForWindow:self.window
256 completionHandler:^(NSInteger result) {
257 if (result != NSFileHandlingPanelOKButton)
258 return;
259 [panel close];
260 NSError *error;
261 if (![mapping writeToURL:panel.URL error:&error]) {
262 [self.window presentError:error
263 modalForWindow:self.window
264 delegate:nil
265 didPresentSelector:nil
266 contextInfo:nil];
267 }
268 }];
269 }
270
271 - (void)mappingConflictDidResolve:(NSAlert *)alert
272 returnCode:(NSInteger)returnCode
273 contextInfo:(void *)contextInfo {
274 NSDictionary *userInfo = CFBridgingRelease(contextInfo);
275 NJMapping *oldMapping = userInfo[@"old mapping"];
276 NJMapping *newMapping = userInfo[@"new mapping"];
277 NSInteger idx = [userInfo[@"index"] intValue];
278 [alert.window orderOut:nil];
279 switch (returnCode) {
280 case NSAlertFirstButtonReturn: // Merge
281 [self.ic mergeMapping:newMapping intoMapping:oldMapping];
282 [self.ic activateMapping:oldMapping];
283 break;
284 case NSAlertThirdButtonReturn: // New Mapping
285 [self.mvc beginUpdates];
286 [self.ic addMapping:newMapping];
287 [self.mvc addedMappingAtIndex:idx startEditing:YES];
288 [self.mvc endUpdates];
289 [self.ic activateMapping:newMapping];
290 break;
291 default: // Cancel, other.
292 break;
293 }
294 }
295
296 - (void)promptForMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
297 NJMapping *mergeInto = [self.ic mappingForKey:mapping.name];
298 NSAlert *conflictAlert = [[NSAlert alloc] init];
299 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
300 conflictAlert.informativeText =
301 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
302 mapping.name];
303 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
304 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
305 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
306 [conflictAlert beginSheetModalForWindow:self.window
307 modalDelegate:self
308 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
309 contextInfo:(void *)CFBridgingRetain(@{ @"index": @(idx),
310 @"old mapping": mergeInto,
311 @"new mapping": mapping })];
312 }
313
314 - (NSInteger)numberOfMappings:(NJMappingsViewController *)mvc {
315 return self.ic.mappings.count;
316 }
317
318 - (NJMapping *)mappingsViewController:(NJMappingsViewController *)mvc
319 mappingForIndex:(NSUInteger)idx {
320 return self.ic.mappings[idx];
321 }
322
323 - (void)mappingsViewController:(NJMappingsViewController *)mvc
324 renameMappingAtIndex:(NSInteger)index
325 toName:(NSString *)name {
326 [self.ic renameMapping:self.ic.mappings[index]
327 to:name];
328 }
329
330 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
331 canMoveMappingFromIndex:(NSInteger)fromIdx
332 toIndex:(NSInteger)toIdx {
333 return fromIdx != toIdx && fromIdx != 0 && toIdx != 0
334 && toIdx < (NSInteger)self.ic.mappings.count;
335 }
336
337 - (void)mappingsViewController:(NJMappingsViewController *)mvc
338 moveMappingFromIndex:(NSInteger)fromIdx
339 toIndex:(NSInteger)toIdx {
340 [mvc beginUpdates];
341 [mvc.mappingList moveRowAtIndex:fromIdx toIndex:toIdx];
342 [self.ic moveMoveMappingFromIndex:fromIdx toIndex:toIdx];
343 [mvc endUpdates];
344 }
345
346 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
347 canRemoveMappingAtIndex:(NSInteger)idx {
348 return idx != 0;
349 }
350
351 - (void)mappingsViewController:(NJMappingsViewController *)mvc
352 removeMappingAtIndex:(NSInteger)idx {
353 [mvc beginUpdates];
354 [mvc removedMappingAtIndex:idx];
355 [self.ic removeMappingAtIndex:idx];
356 [mvc endUpdates];
357 }
358
359 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
360 importMappingFromURL:(NSURL *)url
361 atIndex:(NSInteger)index
362 error:(NSError **)error {
363 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
364 error:error];
365 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
366 [self promptForMapping:mapping atIndex:index];
367 } else if ([self.ic mappingForKey:mapping.name]) {
368 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
369 } else if (mapping) {
370 [self.mvc beginUpdates];
371 [self.mvc addedMappingAtIndex:index startEditing:NO];
372 [self.ic insertMapping:mapping atIndex:index];
373 [self.mvc endUpdates];
374 }
375 return !!mapping;
376 }
377
378 - (void)mappingsViewController:(NJMappingsViewController *)mvc
379 addMapping:(NJMapping *)mapping {
380 [mvc beginUpdates];
381 [mvc addedMappingAtIndex:self.ic.mappings.count startEditing:YES];
382 [self.ic addMapping:mapping];
383 [mvc endUpdates];
384 [self.ic activateMapping:mapping];
385 }
386
387 - (void)mappingsViewController:(NJMappingsViewController *)mvc
388 choseMappingAtIndex:(NSInteger)idx {
389 [self.ic activateMapping:self.ic.mappings[idx]];
390 }
391
392 - (id)deviceViewController:(NJDeviceViewController *)dvc
393 elementForUID:(NSString *)uid {
394 return [self.ic elementForUID:uid];
395 }
396
397 - (void)loadOutputForInput:(NJInput *)input {
398 NJOutput *output = self.ic.currentMapping[input];
399 [self.oc loadOutput:output forInput:input];
400 }
401
402 - (void)deviceViewControllerDidSelectNothing:(NJDeviceViewController *)dvc {
403 [self loadOutputForInput:nil];
404 }
405
406 - (void)deviceViewController:(NJDeviceViewController *)dvc
407 didSelectBranch:(NJInputPathElement *)handler {
408 [self loadOutputForInput:dvc.selectedHandler];
409 }
410
411 - (void)deviceViewController:(NJDeviceViewController *)dvc
412 didSelectHandler:(NJInputPathElement *)handler {
413 [self loadOutputForInput:dvc.selectedHandler];
414 }
415
416 - (void)deviceViewController:(NJDeviceViewController *)dvc
417 didSelectDevice:(NJInputPathElement *)device {
418 [self loadOutputForInput:dvc.selectedHandler];
419 }
420
421 - (void)inputController:(NJInputController *)ic
422 didAddDevice:(NJDevice *)device {
423 [self.dvc addedDevice:device atIndex:ic.devices.count - 1];
424 }
425
426 - (void)inputController:(NJInputController *)ic
427 didRemoveDeviceAtIndex:(NSInteger)idx {
428 [self.dvc removedDeviceAtIndex:idx];
429 }
430
431 - (void)inputControllerDidStartHID:(NJInputController *)ic {
432 [self.dvc hidStarted];
433 }
434
435 - (void)inputControllerDidStopHID:(NJInputController *)ic {
436 [self.dvc hidStopped];
437 }
438
439 - (void)inputController:(NJInputController *)ic didInput:(NJInput *)input {
440 [self.dvc expandAndSelectItem:input];
441 [self loadOutputForInput:input];
442 [self.oc focusKey];
443 }
444
445 - (void)inputController:(NJInputController *)ic didError:(NSError *)error {
446 // Since the error shows the window, it can trigger another attempt
447 // to re-open the HID manager, which will also probably fail and error,
448 // so don't bother repeating ourselves.
449 if (!self.window.attachedSheet) {
450 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
451 [self.window makeKeyAndOrderFront:nil];
452 [self.window presentError:error
453 modalForWindow:self.window
454 delegate:nil
455 didPresentSelector:nil
456 contextInfo:nil];
457 }
458 }
459
460 - (NSInteger)numberOfDevicesInDeviceList:(NJDeviceViewController *)dvc {
461 return self.ic.devices.count;
462 }
463
464 - (NJDevice *)deviceViewController:(NJDeviceViewController *)dvc
465 deviceForIndex:(NSUInteger)idx {
466 return self.ic.devices[idx];
467 }
468
469 - (IBAction)simulatingEventsChanged:(NSButton *)sender {
470 self.ic.simulatingEvents = sender.state == NSOnState;
471 }
472
473 - (void)outputViewController:(NJOutputViewController *)ovc
474 setOutput:(NJOutput *)output
475 forInput:(NJInput *)input {
476 self.ic.currentMapping[input] = output;
477 [self.ic save];
478 }
479
480 - (NJMapping *)outputViewController:(NJOutputViewController *)ovc
481 mappingForIndex:(NSUInteger)index {
482 return self.ic.mappings[index];
483 }
484
485 @end