2 // EnjoyableApplicationDelegate.m
5 // Created by Sam McCall on 4/05/09.
8 #import <Sparkle/Sparkle.h>
10 #import "EnjoyableApplicationDelegate.h"
16 @implementation EnjoyableApplicationDelegate {
17 NSStatusItem *statusItem;
18 NSMutableArray *_errors;
21 - (void)didSwitchApplication:(NSNotification *)note {
22 NSRunningApplication *activeApp = note.userInfo[NSWorkspaceApplicationKey];
24 [self.ic activateMappingForProcess:activeApp];
27 - (void)applicationWillFinishLaunching:(NSNotification *)notification {
28 [NSNotificationCenter.defaultCenter
30 selector:@selector(mappingDidChange:)
31 name:NJEventMappingChanged
33 [NSNotificationCenter.defaultCenter
35 selector:@selector(eventSimulationStarted:)
36 name:NJEventSimulationStarted
38 [NSNotificationCenter.defaultCenter
40 selector:@selector(eventSimulationStopped:)
41 name:NJEventSimulationStopped
45 [self.mvc.mappingList reloadData];
46 [self.mvc changedActiveMappingToIndex:
47 [self.ic indexOfMapping:
48 self.ic.currentMapping]];
50 statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:36];
51 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
52 statusItem.highlightMode = YES;
53 statusItem.menu = self.statusItemMenu;
54 statusItem.target = self;
57 - (void)applicationDidFinishLaunching:(NSNotification *)notification {
58 if ([NSUserDefaults.standardUserDefaults boolForKey:@"hidden in status item"]
59 && NSRunningApplication.currentApplication.wasLaunchedAsLoginItemOrResume)
60 [self transformIntoElement:nil];
62 [self.window makeKeyAndOrderFront:nil];
65 - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication
66 hasVisibleWindows:(BOOL)flag {
67 [self restoreToForeground:theApplication];
71 - (void)restoreToForeground:(id)sender {
72 ProcessSerialNumber psn = { 0, kCurrentProcess };
73 TransformProcessType(&psn, kProcessTransformToForegroundApplication);
74 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
75 [self.window makeKeyAndOrderFront:sender];
76 [NSObject cancelPreviousPerformRequestsWithTarget:self
77 selector:@selector(transformIntoElement:)
79 [NSUserDefaults.standardUserDefaults setBool:NO forKey:@"hidden in status item"];
82 - (void)applicationWillBecomeActive:(NSNotification *)notification {
83 if (self.window.isVisible)
84 [self restoreToForeground:notification];
87 - (void)transformIntoElement:(id)sender {
88 ProcessSerialNumber psn = { 0, kCurrentProcess };
89 TransformProcessType(&psn, kProcessTransformToUIElementApplication);
90 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"hidden in status item"];
93 - (void)flashStatusItem {
94 if ([statusItem.image.name isEqualToString:@"Status Menu Icon"]) {
95 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
97 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
102 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
103 [theApplication hide:theApplication];
104 // If we turn into a UIElement right away, the application cancels
105 // the deactivation events. The dock icon disappears, but an
106 // unresponsive menu bar remains until the user clicks somewhere.
107 // So delay just long enough to be past the end handling that.
108 [self performSelector:@selector(transformIntoElement:) withObject:self afterDelay:0.001];
112 - (void)eventSimulationStarted:(NSNotification *)note {
113 self.simulatingEventsButton.state = NSOnState;
114 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
115 [NSWorkspace.sharedWorkspace.notificationCenter
117 selector:@selector(didSwitchApplication:)
118 name:NSWorkspaceDidActivateApplicationNotification
122 - (void)eventSimulationStopped:(NSNotification *)note {
123 self.simulatingEventsButton.state = NSOffState;
124 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
125 [NSWorkspace.sharedWorkspace.notificationCenter
127 name:NSWorkspaceDidActivateApplicationNotification
131 - (void)mappingDidChange:(NSNotification *)note {
132 NSUInteger idx = [note.userInfo[NJMappingIndexKey] intValue];
133 [self.mvc changedActiveMappingToIndex:idx];
135 if (!self.window.isVisible)
136 for (int i = 0; i < 4; ++i)
137 [self performSelector:@selector(flashStatusItem)
140 [self loadOutputForInput:self.dvc.selectedHandler];
143 - (NSMenu *)applicationDockMenu:(NSApplication *)sender {
144 return self.dockMenu;
147 - (void)showNextError {
148 if (!self.window.attachedSheet && _errors.count) {
149 NSError *error = _errors.lastObject;
150 [_errors removeLastObject];
151 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
152 [self.window makeKeyAndOrderFront:nil];
153 [self.window presentError:error
154 modalForWindow:self.window
156 didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:)
161 - (void)didPresentErrorWithRecovery:(BOOL)didRecover
162 contextInfo:(void *)contextInfo {
163 [self showNextError];
166 - (void)presentErrorSheet:(NSError *)error {
168 _errors = [[NSMutableArray alloc] initWithCapacity:1];
169 [_errors insertObject:error atIndex:0];
170 [self showNextError];
173 - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
174 [self restoreToForeground:sender];
176 NSURL *URL = [NSURL fileURLWithPath:filename];
177 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:URL
179 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
180 [self promptForMapping:mapping atIndex:self.ic.mappings.count];
181 } else if ([self.ic mappingForKey:mapping.name]) {
182 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
183 } else if (mapping) {
184 [self.mvc beginUpdates];
185 [self.ic addMapping:mapping];
186 [self.mvc addedMappingAtIndex:self.ic.mappings.count - 1 startEditing:NO];
187 [self.mvc endUpdates];
188 [self.ic activateMapping:mapping];
190 [self presentErrorSheet:error];
195 - (void)mappingWasChosen:(NJMapping *)mapping {
196 [self.ic activateMapping:mapping];
199 - (void)mappingListShouldOpen {
200 [self restoreToForeground:self];
201 [self.mvc mappingTriggerClicked:self];
204 - (void)loginItemPromptDidEnd:(NSWindow *)sheet
205 returnCode:(int)returnCode
206 contextInfo:(void *)contextInfo {
207 if (returnCode == NSAlertDefaultReturn) {
208 [NSRunningApplication.currentApplication addToLoginItems];
209 // If we're going to automatically start, don't bug the user
210 // about automatic updates next boot - they probably want it,
211 // and if they don't they probably want a prompt for it less.
212 SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES;
216 - (void)loginItemPromptDidDismiss:(NSWindow *)sheet
217 returnCode:(int)returnCode
218 contextInfo:(void *)contextInfo {
219 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"explained login items"];
220 [self.window performClose:sheet];
223 - (BOOL)windowShouldClose:(NSWindow *)sender {
224 if (sender != self.window
225 || NSRunningApplication.currentApplication.isLoginItem
226 || [NSUserDefaults.standardUserDefaults boolForKey:@"explained login items"])
229 NSLocalizedString(@"login items prompt", @"alert prompt for adding to login items"),
230 NSLocalizedString(@"login items add button", @"button to add to login items"),
231 NSLocalizedString(@"login items don't add button", @"button to not add to login items"),
232 nil, self.window, self,
233 @selector(loginItemPromptDidEnd:returnCode:contextInfo:),
234 @selector(loginItemPromptDidDismiss:returnCode:contextInfo:),
236 NSLocalizedString(@"login items explanation", @"a brief explanation of login items")
238 for (int i = 0; i < 10; ++i)
239 [self performSelector:@selector(flashStatusItem)
245 - (void)importMappingClicked:(id)sender {
246 NSOpenPanel *panel = [NSOpenPanel openPanel];
247 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
248 [panel beginSheetModalForWindow:self.window
249 completionHandler:^(NSInteger result) {
250 if (result != NSFileHandlingPanelOKButton)
254 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:panel.URL
256 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
257 [self promptForMapping:mapping atIndex:self.ic.mappings.count];
258 } else if ([self.ic mappingForKey:mapping.name]) {
259 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
260 } else if (mapping) {
261 [self.ic addMapping:mapping];
263 [self presentErrorSheet:error];
269 - (void)exportMappingClicked:(id)sender {
270 NSSavePanel *panel = [NSSavePanel savePanel];
271 panel.allowedFileTypes = @[ @"enjoyable" ];
272 NJMapping *mapping = self.ic.currentMapping;
273 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
274 [panel beginSheetModalForWindow:self.window
275 completionHandler:^(NSInteger result) {
276 if (result != NSFileHandlingPanelOKButton)
280 if (![mapping writeToURL:panel.URL error:&error]) {
281 [self presentErrorSheet:error];
286 - (void)mappingConflictDidResolve:(NSAlert *)alert
287 returnCode:(NSInteger)returnCode
288 contextInfo:(void *)contextInfo {
289 NSDictionary *userInfo = CFBridgingRelease(contextInfo);
290 NJMapping *oldMapping = userInfo[@"old mapping"];
291 NJMapping *newMapping = userInfo[@"new mapping"];
292 NSInteger idx = [userInfo[@"index"] intValue];
293 [alert.window orderOut:nil];
294 switch (returnCode) {
295 case NSAlertFirstButtonReturn: // Merge
296 [self.ic mergeMapping:newMapping intoMapping:oldMapping];
297 [self.ic activateMapping:oldMapping];
299 case NSAlertThirdButtonReturn: // New Mapping
300 [self.mvc beginUpdates];
301 [self.ic addMapping:newMapping];
302 [self.mvc addedMappingAtIndex:idx startEditing:YES];
303 [self.mvc endUpdates];
304 [self.ic activateMapping:newMapping];
306 default: // Cancel, other.
311 - (void)promptForMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
312 NJMapping *mergeInto = [self.ic mappingForKey:mapping.name];
313 NSAlert *conflictAlert = [[NSAlert alloc] init];
314 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
315 conflictAlert.informativeText =
316 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
318 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
319 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
320 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
321 [conflictAlert beginSheetModalForWindow:self.window
323 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
324 contextInfo:(void *)CFBridgingRetain(@{ @"index": @(idx),
325 @"old mapping": mergeInto,
326 @"new mapping": mapping })];
329 - (NSInteger)numberOfMappings:(NJMappingsViewController *)mvc {
330 return self.ic.mappings.count;
333 - (NJMapping *)mappingsViewController:(NJMappingsViewController *)mvc
334 mappingForIndex:(NSUInteger)idx {
335 return self.ic.mappings[idx];
338 - (void)mappingsViewController:(NJMappingsViewController *)mvc
339 renameMappingAtIndex:(NSInteger)index
340 toName:(NSString *)name {
341 [self.ic renameMapping:self.ic.mappings[index]
345 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
346 canMoveMappingFromIndex:(NSInteger)fromIdx
347 toIndex:(NSInteger)toIdx {
348 return fromIdx != toIdx && fromIdx != 0 && toIdx != 0
349 && toIdx < (NSInteger)self.ic.mappings.count;
352 - (void)mappingsViewController:(NJMappingsViewController *)mvc
353 moveMappingFromIndex:(NSInteger)fromIdx
354 toIndex:(NSInteger)toIdx {
356 [mvc.mappingList moveRowAtIndex:fromIdx toIndex:toIdx];
357 [self.ic moveMoveMappingFromIndex:fromIdx toIndex:toIdx];
361 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
362 canRemoveMappingAtIndex:(NSInteger)idx {
366 - (void)mappingsViewController:(NJMappingsViewController *)mvc
367 removeMappingAtIndex:(NSInteger)idx {
369 [mvc removedMappingAtIndex:idx];
370 [self.ic removeMappingAtIndex:idx];
374 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
375 importMappingFromURL:(NSURL *)url
376 atIndex:(NSInteger)index
377 error:(NSError **)error {
378 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
380 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
381 [self promptForMapping:mapping atIndex:index];
382 } else if ([self.ic mappingForKey:mapping.name]) {
383 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
384 } else if (mapping) {
385 [self.mvc beginUpdates];
386 [self.mvc addedMappingAtIndex:index startEditing:NO];
387 [self.ic insertMapping:mapping atIndex:index];
388 [self.mvc endUpdates];
393 - (void)mappingsViewController:(NJMappingsViewController *)mvc
394 addMapping:(NJMapping *)mapping {
396 [mvc addedMappingAtIndex:self.ic.mappings.count startEditing:YES];
397 [self.ic addMapping:mapping];
399 [self.ic activateMapping:mapping];
402 - (void)mappingsViewController:(NJMappingsViewController *)mvc
403 choseMappingAtIndex:(NSInteger)idx {
404 [self.ic activateMapping:self.ic.mappings[idx]];
407 - (id)deviceViewController:(NJDeviceViewController *)dvc
408 elementForUID:(NSString *)uid {
409 return [self.ic elementForUID:uid];
412 - (void)loadOutputForInput:(NJInput *)input {
413 NJOutput *output = self.ic.currentMapping[input];
414 [self.oc loadOutput:output forInput:input];
417 - (void)deviceViewControllerDidSelectNothing:(NJDeviceViewController *)dvc {
418 [self loadOutputForInput:nil];
421 - (void)deviceViewController:(NJDeviceViewController *)dvc
422 didSelectBranch:(NJInputPathElement *)handler {
423 [self loadOutputForInput:dvc.selectedHandler];
426 - (void)deviceViewController:(NJDeviceViewController *)dvc
427 didSelectHandler:(NJInputPathElement *)handler {
428 [self loadOutputForInput:dvc.selectedHandler];
431 - (void)deviceViewController:(NJDeviceViewController *)dvc
432 didSelectDevice:(NJInputPathElement *)device {
433 [self loadOutputForInput:dvc.selectedHandler];
436 - (void)inputController:(NJInputController *)ic
437 didAddDevice:(NJDevice *)device {
438 [self.dvc addedDevice:device atIndex:ic.devices.count - 1];
441 - (void)inputController:(NJInputController *)ic
442 didRemoveDeviceAtIndex:(NSInteger)idx {
443 [self.dvc removedDeviceAtIndex:idx];
446 - (void)inputControllerDidStartHID:(NJInputController *)ic {
447 [self.dvc hidStarted];
450 - (void)inputControllerDidStopHID:(NJInputController *)ic {
451 [self.dvc hidStopped];
454 - (void)inputController:(NJInputController *)ic didInput:(NJInput *)input {
455 [self.dvc expandAndSelectItem:input];
456 [self loadOutputForInput:input];
460 - (void)inputController:(NJInputController *)ic didError:(NSError *)error {
461 [self presentErrorSheet:error];
464 - (NSInteger)numberOfDevicesInDeviceList:(NJDeviceViewController *)dvc {
465 return self.ic.devices.count;
468 - (NJDevice *)deviceViewController:(NJDeviceViewController *)dvc
469 deviceForIndex:(NSUInteger)idx {
470 return self.ic.devices[idx];
473 - (IBAction)simulatingEventsChanged:(NSButton *)sender {
474 self.ic.simulatingEvents = sender.state == NSOnState;
477 - (void)outputViewController:(NJOutputViewController *)ovc
478 setOutput:(NJOutput *)output
479 forInput:(NJInput *)input {
480 self.ic.currentMapping[input] = output;
484 - (NJMapping *)outputViewController:(NJOutputViewController *)ovc
485 mappingForIndex:(NSUInteger)index {
486 return self.ic.mappings[index];