Rename methods uniquely between mapping/device controllers.
[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 "NJMappingsController.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.mappingsController 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.mappingsController load];
44 [self.mvc.mappingList reloadData];
45 [self.mvc changedActiveMappingToIndex:
46 [self.mappingsController indexOfMapping:
47 self.mappingsController.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 }
140
141 - (NSMenu *)applicationDockMenu:(NSApplication *)sender {
142 return self.dockMenu;
143 }
144
145 - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
146 [self restoreToForeground:sender];
147 NSError *error;
148 NSURL *URL = [NSURL fileURLWithPath:filename];
149 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:URL
150 error:&error];
151 if ([[self.mappingsController mappingForKey:mapping.name] hasConflictWith:mapping]) {
152 [self promptForMapping:mapping atIndex:self.mappingsController.mappings.count];
153 } else if ([self.mappingsController mappingForKey:mapping.name]) {
154 [[self.mappingsController mappingForKey:mapping.name] mergeEntriesFrom:mapping];
155 } else if (mapping) {
156 [self.mvc beginUpdates];
157 [self.mappingsController addMapping:mapping];
158 [self.mvc addedMappingAtIndex:self.mappingsController.mappings.count - 1 startEditing:NO];
159 [self.mvc endUpdates];
160 [self.mappingsController activateMapping:mapping];
161 } else {
162 [self.window presentError:error
163 modalForWindow:self.window
164 delegate:nil
165 didPresentSelector:nil
166 contextInfo:nil];
167 }
168 return !!mapping;
169 }
170
171 - (void)mappingWasChosen:(NJMapping *)mapping {
172 [self.mappingsController activateMapping:mapping];
173 }
174
175 - (void)mappingListShouldOpen {
176 [self restoreToForeground:self];
177 [self.mvc mappingTriggerClicked:self];
178 }
179
180 - (void)loginItemPromptDidEnd:(NSWindow *)sheet
181 returnCode:(int)returnCode
182 contextInfo:(void *)contextInfo {
183 if (returnCode == NSAlertDefaultReturn) {
184 [NSRunningApplication.currentApplication addToLoginItems];
185 // If we're going to automatically start, don't bug the user
186 // about automatic updates next boot - they probably want it,
187 // and if they don't they probably want a prompt for it less.
188 SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES;
189 }
190 }
191
192 - (void)loginItemPromptDidDismiss:(NSWindow *)sheet
193 returnCode:(int)returnCode
194 contextInfo:(void *)contextInfo {
195 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"explained login items"];
196 [self.window performClose:sheet];
197 }
198
199 - (BOOL)windowShouldClose:(NSWindow *)sender {
200 if (sender != self.window
201 || NSRunningApplication.currentApplication.isLoginItem
202 || [NSUserDefaults.standardUserDefaults boolForKey:@"explained login items"])
203 return YES;
204 NSBeginAlertSheet(
205 NSLocalizedString(@"login items prompt", @"alert prompt for adding to login items"),
206 NSLocalizedString(@"login items add button", @"button to add to login items"),
207 NSLocalizedString(@"login items don't add button", @"button to not add to login items"),
208 nil, self.window, self,
209 @selector(loginItemPromptDidEnd:returnCode:contextInfo:),
210 @selector(loginItemPromptDidDismiss:returnCode:contextInfo:),
211 NULL,
212 NSLocalizedString(@"login items explanation", @"a brief explanation of login items")
213 );
214 for (int i = 0; i < 10; ++i)
215 [self performSelector:@selector(flashStatusItem)
216 withObject:self
217 afterDelay:0.5 * i];
218 return NO;
219 }
220
221 - (void)importMappingClicked:(id)sender {
222 NSOpenPanel *panel = [NSOpenPanel openPanel];
223 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
224 [panel beginSheetModalForWindow:self.window
225 completionHandler:^(NSInteger result) {
226 if (result != NSFileHandlingPanelOKButton)
227 return;
228 [panel close];
229 NSError *error;
230 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:panel.URL
231 error:&error];
232 if ([[self.mappingsController mappingForKey:mapping.name] hasConflictWith:mapping]) {
233 [self promptForMapping:mapping atIndex:self.mappingsController.mappings.count];
234 } else if ([self.mappingsController mappingForKey:mapping.name]) {
235 [[self.mappingsController mappingForKey:mapping.name] mergeEntriesFrom:mapping];
236 } else if (mapping) {
237 [self.mappingsController addMapping:mapping];
238 } else {
239 [self.window presentError:error
240 modalForWindow:self.window
241 delegate:nil
242 didPresentSelector:nil
243 contextInfo:nil];
244 }
245 }];
246
247 }
248
249 - (void)exportMappingClicked:(id)sender {
250 NSSavePanel *panel = [NSSavePanel savePanel];
251 panel.allowedFileTypes = @[ @"enjoyable" ];
252 NJMapping *mapping = self.mappingsController.currentMapping;
253 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
254 [panel beginSheetModalForWindow:self.window
255 completionHandler:^(NSInteger result) {
256 if (result != NSFileHandlingPanelOKButton)
257 return;
258 [panel close];
259 NSError *error;
260 if (![mapping writeToURL:panel.URL error:&error]) {
261 [self.window presentError:error
262 modalForWindow:self.window
263 delegate:nil
264 didPresentSelector:nil
265 contextInfo:nil];
266 }
267 }];
268 }
269
270 - (void)mappingConflictDidResolve:(NSAlert *)alert
271 returnCode:(NSInteger)returnCode
272 contextInfo:(void *)contextInfo {
273 NSDictionary *userInfo = CFBridgingRelease(contextInfo);
274 NJMapping *oldMapping = userInfo[@"old mapping"];
275 NJMapping *newMapping = userInfo[@"new mapping"];
276 NSInteger idx = [userInfo[@"index"] intValue];
277 [alert.window orderOut:nil];
278 switch (returnCode) {
279 case NSAlertFirstButtonReturn: // Merge
280 [self.mappingsController mergeMapping:newMapping intoMapping:oldMapping];
281 [self.mappingsController activateMapping:oldMapping];
282 break;
283 case NSAlertThirdButtonReturn: // New Mapping
284 [self.mvc beginUpdates];
285 [self.mappingsController addMapping:newMapping];
286 [self.mvc addedMappingAtIndex:idx startEditing:YES];
287 [self.mvc endUpdates];
288 [self.mappingsController activateMapping:newMapping];
289 break;
290 default: // Cancel, other.
291 break;
292 }
293 }
294
295 - (void)promptForMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
296 NJMapping *mergeInto = [self.mappingsController mappingForKey:mapping.name];
297 NSAlert *conflictAlert = [[NSAlert alloc] init];
298 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
299 conflictAlert.informativeText =
300 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
301 mapping.name];
302 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
303 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
304 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
305 [conflictAlert beginSheetModalForWindow:self.window
306 modalDelegate:self
307 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
308 contextInfo:(void *)CFBridgingRetain(@{ @"index": @(idx),
309 @"old mapping": mergeInto,
310 @"new mapping": mapping })];
311 }
312
313 - (NSInteger)numberOfMappings:(NJMappingsViewController *)mvc {
314 return self.mappingsController.mappings.count;
315 }
316
317 - (NJMapping *)mappingsViewController:(NJMappingsViewController *)mvc
318 mappingForIndex:(NSUInteger)idx {
319 return self.mappingsController.mappings[idx];
320 }
321
322 - (void)mappingsViewController:(NJMappingsViewController *)mvc
323 renameMappingAtIndex:(NSInteger)index
324 toName:(NSString *)name {
325 [self.mappingsController renameMapping:self.mappingsController.mappings[index]
326 to:name];
327 }
328
329 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
330 canMoveMappingFromIndex:(NSInteger)fromIdx
331 toIndex:(NSInteger)toIdx {
332 return fromIdx != toIdx && fromIdx != 0 && toIdx != 0
333 && toIdx < (NSInteger)self.mappingsController.mappings.count;
334 }
335
336 - (void)mappingsViewController:(NJMappingsViewController *)mvc
337 moveMappingFromIndex:(NSInteger)fromIdx
338 toIndex:(NSInteger)toIdx {
339 [mvc beginUpdates];
340 [mvc.mappingList moveRowAtIndex:fromIdx toIndex:toIdx];
341 [self.mappingsController moveMoveMappingFromIndex:fromIdx toIndex:toIdx];
342 [mvc endUpdates];
343 }
344
345 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
346 canRemoveMappingAtIndex:(NSInteger)idx {
347 return idx != 0;
348 }
349
350 - (void)mappingsViewController:(NJMappingsViewController *)mvc
351 removeMappingAtIndex:(NSInteger)idx {
352 [mvc beginUpdates];
353 [mvc removedMappingAtIndex:idx];
354 [self.mappingsController removeMappingAtIndex:idx];
355 [mvc endUpdates];
356 }
357
358 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
359 importMappingFromURL:(NSURL *)url
360 atIndex:(NSInteger)index
361 error:(NSError **)error {
362 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
363 error:error];
364 if ([[self.mappingsController mappingForKey:mapping.name] hasConflictWith:mapping]) {
365 [self promptForMapping:mapping atIndex:index];
366 } else if ([self.mappingsController mappingForKey:mapping.name]) {
367 [[self.mappingsController mappingForKey:mapping.name] mergeEntriesFrom:mapping];
368 } else if (mapping) {
369 [self.mvc beginUpdates];
370 [self.mvc addedMappingAtIndex:index startEditing:NO];
371 [self.mappingsController insertMapping:mapping atIndex:index];
372 [self.mvc endUpdates];
373 }
374 return !!mapping;
375 }
376
377 - (void)mappingsViewController:(NJMappingsViewController *)mvc
378 addMapping:(NJMapping *)mapping {
379 [mvc beginUpdates];
380 [mvc addedMappingAtIndex:self.mappingsController.mappings.count startEditing:YES];
381 [self.mappingsController addMapping:mapping];
382 [mvc endUpdates];
383 [self.mappingsController activateMapping:mapping];
384 }
385
386 - (void)mappingsViewController:(NJMappingsViewController *)mvc
387 choseMappingAtIndex:(NSInteger)idx {
388 [self.mappingsController activateMapping:self.mappingsController.mappings[idx]];
389 }
390
391 - (id)deviceViewController:(NJDeviceViewController *)dvc
392 elementForUID:(NSString *)uid {
393 return [self.deviceController elementForUID:uid];
394 }
395
396 - (void)deviceViewControllerDidSelectNothing:(NJDeviceViewController *)dvc {
397 [self.outputController loadInput:dvc.selectedHandler];
398 }
399
400 - (void)deviceViewController:(NJDeviceViewController *)dvc
401 didSelectBranch:(NJInputPathElement *)handler {
402 [self.outputController loadInput:dvc.selectedHandler];
403 }
404
405 - (void)deviceViewController:(NJDeviceViewController *)dvc
406 didSelectHandler:(NJInputPathElement *)handler {
407 [self.outputController loadInput:dvc.selectedHandler];
408 }
409
410 - (void)deviceViewController:(NJDeviceViewController *)dvc
411 didSelectDevice:(NJInputPathElement *)device {
412 [self.outputController loadInput:dvc.selectedHandler];
413 }
414
415 - (void)deviceController:(NJDeviceController *)dc
416 didAddDevice:(NJDevice *)device {
417 [self.dvc addedDevice:device atIndex:dc.devices.count - 1];
418 }
419
420 - (void)deviceController:(NJDeviceController *)dc
421 didRemoveDeviceAtIndex:(NSInteger)idx {
422 [self.dvc removedDeviceAtIndex:idx];
423 }
424
425 - (void)deviceControllerDidStartHID:(NJDeviceController *)dc {
426 [self.dvc hidStarted];
427 }
428
429 - (void)deviceControllerDidStopHID:(NJDeviceController *)dc {
430 [self.dvc hidStopped];
431 }
432
433 - (void)deviceController:(NJDeviceController *)dc didInput:(NJInput *)input {
434 [self.outputController loadInput:input];
435 [self.outputController focusKey];
436 }
437
438 - (void)deviceController:(NJDeviceController *)dc didError:(NSError *)error {
439 // Since the error shows the window, it can trigger another attempt
440 // to re-open the HID manager, which will also probably fail and error,
441 // so don't bother repeating ourselves.
442 if (!self.window.attachedSheet) {
443 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
444 [self.window makeKeyAndOrderFront:nil];
445 [self.window presentError:error
446 modalForWindow:self.window
447 delegate:nil
448 didPresentSelector:nil
449 contextInfo:nil];
450 }
451 }
452
453 - (NSInteger)numberOfDevicesInDeviceList:(NJDeviceViewController *)dvc {
454 return self.deviceController.devices.count;
455 }
456
457 - (NJDevice *)deviceViewController:(NJDeviceViewController *)dvc
458 deviceForIndex:(NSUInteger)idx {
459 return self.deviceController.devices[idx];
460 }
461
462 - (IBAction)simulatingEventsChanged:(NSButton *)sender {
463 self.deviceController.simulatingEvents = sender.state == NSOnState;
464 }
465
466 @end