Remove login item prompts, going back on this idea.
[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 NSMutableArray *_errors;
19 }
20
21 - (void)didSwitchApplication:(NSNotification *)note {
22 NSRunningApplication *activeApp = note.userInfo[NSWorkspaceApplicationKey];
23 if (activeApp)
24 [self.ic activateMappingForProcess:activeApp];
25 }
26
27 - (void)applicationWillFinishLaunching:(NSNotification *)notification {
28 [NSNotificationCenter.defaultCenter
29 addObserver:self
30 selector:@selector(mappingDidChange:)
31 name:NJEventMappingChanged
32 object:nil];
33 [NSNotificationCenter.defaultCenter
34 addObserver:self
35 selector:@selector(eventSimulationStarted:)
36 name:NJEventSimulationStarted
37 object:nil];
38 [NSNotificationCenter.defaultCenter
39 addObserver:self
40 selector:@selector(eventSimulationStopped:)
41 name:NJEventSimulationStopped
42 object:nil];
43
44 [self.ic load];
45 [self.mvc.mappingList reloadData];
46 [self.mvc changedActiveMappingToIndex:
47 [self.ic indexOfMapping:
48 self.ic.currentMapping]];
49
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;
55 }
56
57 - (void)applicationDidFinishLaunching:(NSNotification *)notification {
58 if ([NSUserDefaults.standardUserDefaults boolForKey:@"hidden in status item"]
59 && NSRunningApplication.currentApplication.wasLaunchedAsLoginItemOrResume)
60 [self transformIntoElement:nil];
61 else
62 [self.window makeKeyAndOrderFront:nil];
63 }
64
65 - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication
66 hasVisibleWindows:(BOOL)flag {
67 [self restoreToForeground:theApplication];
68 return NO;
69 }
70
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:)
78 object:self];
79 [NSUserDefaults.standardUserDefaults setBool:NO forKey:@"hidden in status item"];
80 }
81
82 - (void)applicationWillBecomeActive:(NSNotification *)notification {
83 if (self.window.isVisible)
84 [self restoreToForeground:notification];
85 }
86
87 - (void)transformIntoElement:(id)sender {
88 ProcessSerialNumber psn = { 0, kCurrentProcess };
89 TransformProcessType(&psn, kProcessTransformToUIElementApplication);
90 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"hidden in status item"];
91 }
92
93 - (void)flashStatusItem {
94 if ([statusItem.image.name isEqualToString:@"Status Menu Icon"]) {
95 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
96 } else {
97 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
98 }
99
100 }
101
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];
109 return NO;
110 }
111
112 - (void)eventSimulationStarted:(NSNotification *)note {
113 self.simulatingEventsButton.state = NSOnState;
114 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
115 [NSWorkspace.sharedWorkspace.notificationCenter
116 addObserver:self
117 selector:@selector(didSwitchApplication:)
118 name:NSWorkspaceDidActivateApplicationNotification
119 object:nil];
120 }
121
122 - (void)eventSimulationStopped:(NSNotification *)note {
123 self.simulatingEventsButton.state = NSOffState;
124 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
125 [NSWorkspace.sharedWorkspace.notificationCenter
126 removeObserver:self
127 name:NSWorkspaceDidActivateApplicationNotification
128 object:nil];
129 }
130
131 - (void)mappingDidChange:(NSNotification *)note {
132 NSUInteger idx = [note.userInfo[NJMappingIndexKey] intValue];
133 [self.mvc changedActiveMappingToIndex:idx];
134
135 if (!self.window.isVisible)
136 for (int i = 0; i < 4; ++i)
137 [self performSelector:@selector(flashStatusItem)
138 withObject:self
139 afterDelay:0.2 * i];
140 [self loadOutputForInput:self.dvc.selectedHandler];
141 }
142
143 - (NSMenu *)applicationDockMenu:(NSApplication *)sender {
144 return self.dockMenu;
145 }
146
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
155 delegate:self
156 didPresentSelector:@selector(didPresentErrorWithRecovery:contextInfo:)
157 contextInfo:nil];
158 }
159 }
160
161 - (void)didPresentErrorWithRecovery:(BOOL)didRecover
162 contextInfo:(void *)contextInfo {
163 [self showNextError];
164 }
165
166 - (void)presentErrorSheet:(NSError *)error {
167 if (!_errors)
168 _errors = [[NSMutableArray alloc] initWithCapacity:1];
169 [_errors insertObject:error atIndex:0];
170 [self showNextError];
171 }
172
173 - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
174 [self restoreToForeground:sender];
175 NSError *error;
176 NSURL *URL = [NSURL fileURLWithPath:filename];
177 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:URL
178 error:&error];
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];
189 } else {
190 [self presentErrorSheet:error];
191 }
192 return !!mapping;
193 }
194
195 - (void)mappingWasChosen:(NJMapping *)mapping {
196 [self.ic activateMapping:mapping];
197 }
198
199 - (void)mappingListShouldOpen {
200 [self restoreToForeground:self];
201 [self.mvc mappingTriggerClicked:self];
202 }
203
204 - (void)importMappingClicked:(id)sender {
205 NSOpenPanel *panel = [NSOpenPanel openPanel];
206 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
207 [panel beginSheetModalForWindow:self.window
208 completionHandler:^(NSInteger result) {
209 if (result != NSFileHandlingPanelOKButton)
210 return;
211 [panel close];
212 NSError *error;
213 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:panel.URL
214 error:&error];
215 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
216 [self promptForMapping:mapping atIndex:self.ic.mappings.count];
217 } else if ([self.ic mappingForKey:mapping.name]) {
218 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
219 } else if (mapping) {
220 [self.ic addMapping:mapping];
221 } else {
222 [self presentErrorSheet:error];
223 }
224 }];
225
226 }
227
228 - (void)exportMappingClicked:(id)sender {
229 NSSavePanel *panel = [NSSavePanel savePanel];
230 panel.allowedFileTypes = @[ @"enjoyable" ];
231 NJMapping *mapping = self.ic.currentMapping;
232 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
233 [panel beginSheetModalForWindow:self.window
234 completionHandler:^(NSInteger result) {
235 if (result != NSFileHandlingPanelOKButton)
236 return;
237 [panel close];
238 NSError *error;
239 if (![mapping writeToURL:panel.URL error:&error]) {
240 [self presentErrorSheet:error];
241 }
242 }];
243 }
244
245 - (void)mappingConflictDidResolve:(NSAlert *)alert
246 returnCode:(NSInteger)returnCode
247 contextInfo:(void *)contextInfo {
248 NSDictionary *userInfo = CFBridgingRelease(contextInfo);
249 NJMapping *oldMapping = userInfo[@"old mapping"];
250 NJMapping *newMapping = userInfo[@"new mapping"];
251 NSInteger idx = [userInfo[@"index"] intValue];
252 [alert.window orderOut:nil];
253 switch (returnCode) {
254 case NSAlertFirstButtonReturn: // Merge
255 [self.ic mergeMapping:newMapping intoMapping:oldMapping];
256 [self.ic activateMapping:oldMapping];
257 break;
258 case NSAlertThirdButtonReturn: // New Mapping
259 [self.mvc beginUpdates];
260 [self.ic addMapping:newMapping];
261 [self.mvc addedMappingAtIndex:idx startEditing:YES];
262 [self.mvc endUpdates];
263 [self.ic activateMapping:newMapping];
264 break;
265 default: // Cancel, other.
266 break;
267 }
268 }
269
270 - (void)promptForMapping:(NJMapping *)mapping atIndex:(NSInteger)idx {
271 NJMapping *mergeInto = [self.ic mappingForKey:mapping.name];
272 NSAlert *conflictAlert = [[NSAlert alloc] init];
273 conflictAlert.messageText = NSLocalizedString(@"import conflict prompt", @"Title of import conflict alert");
274 conflictAlert.informativeText =
275 [NSString stringWithFormat:NSLocalizedString(@"import conflict in %@", @"Explanation of import conflict"),
276 mapping.name];
277 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import and merge", @"button to merge imported mappings")];
278 [conflictAlert addButtonWithTitle:NSLocalizedString(@"cancel import", @"button to cancel import")];
279 [conflictAlert addButtonWithTitle:NSLocalizedString(@"import new mapping", @"button to import as new mapping")];
280 [conflictAlert beginSheetModalForWindow:self.window
281 modalDelegate:self
282 didEndSelector:@selector(mappingConflictDidResolve:returnCode:contextInfo:)
283 contextInfo:(void *)CFBridgingRetain(@{ @"index": @(idx),
284 @"old mapping": mergeInto,
285 @"new mapping": mapping })];
286 }
287
288 - (NSInteger)numberOfMappings:(NJMappingsViewController *)mvc {
289 return self.ic.mappings.count;
290 }
291
292 - (NJMapping *)mappingsViewController:(NJMappingsViewController *)mvc
293 mappingForIndex:(NSUInteger)idx {
294 return self.ic.mappings[idx];
295 }
296
297 - (void)mappingsViewController:(NJMappingsViewController *)mvc
298 renameMappingAtIndex:(NSInteger)index
299 toName:(NSString *)name {
300 [self.ic renameMapping:self.ic.mappings[index]
301 to:name];
302 }
303
304 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
305 canMoveMappingFromIndex:(NSInteger)fromIdx
306 toIndex:(NSInteger)toIdx {
307 return fromIdx != toIdx && fromIdx != 0 && toIdx != 0
308 && toIdx < (NSInteger)self.ic.mappings.count;
309 }
310
311 - (void)mappingsViewController:(NJMappingsViewController *)mvc
312 moveMappingFromIndex:(NSInteger)fromIdx
313 toIndex:(NSInteger)toIdx {
314 [mvc beginUpdates];
315 [mvc.mappingList moveRowAtIndex:fromIdx toIndex:toIdx];
316 [self.ic moveMoveMappingFromIndex:fromIdx toIndex:toIdx];
317 [mvc endUpdates];
318 }
319
320 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
321 canRemoveMappingAtIndex:(NSInteger)idx {
322 return idx != 0;
323 }
324
325 - (void)mappingsViewController:(NJMappingsViewController *)mvc
326 removeMappingAtIndex:(NSInteger)idx {
327 [mvc beginUpdates];
328 [mvc removedMappingAtIndex:idx];
329 [self.ic removeMappingAtIndex:idx];
330 [mvc endUpdates];
331 }
332
333 - (BOOL)mappingsViewController:(NJMappingsViewController *)mvc
334 importMappingFromURL:(NSURL *)url
335 atIndex:(NSInteger)index
336 error:(NSError **)error {
337 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:url
338 error:error];
339 if ([[self.ic mappingForKey:mapping.name] hasConflictWith:mapping]) {
340 [self promptForMapping:mapping atIndex:index];
341 } else if ([self.ic mappingForKey:mapping.name]) {
342 [[self.ic mappingForKey:mapping.name] mergeEntriesFrom:mapping];
343 } else if (mapping) {
344 [self.mvc beginUpdates];
345 [self.mvc addedMappingAtIndex:index startEditing:NO];
346 [self.ic insertMapping:mapping atIndex:index];
347 [self.mvc endUpdates];
348 }
349 return !!mapping;
350 }
351
352 - (void)mappingsViewController:(NJMappingsViewController *)mvc
353 addMapping:(NJMapping *)mapping {
354 [mvc beginUpdates];
355 [mvc addedMappingAtIndex:self.ic.mappings.count startEditing:YES];
356 [self.ic addMapping:mapping];
357 [mvc endUpdates];
358 [self.ic activateMapping:mapping];
359 }
360
361 - (void)mappingsViewController:(NJMappingsViewController *)mvc
362 choseMappingAtIndex:(NSInteger)idx {
363 [self.ic activateMapping:self.ic.mappings[idx]];
364 }
365
366 - (id)deviceViewController:(NJDeviceViewController *)dvc
367 elementForUID:(NSString *)uid {
368 return [self.ic elementForUID:uid];
369 }
370
371 - (void)loadOutputForInput:(NJInput *)input {
372 NJOutput *output = self.ic.currentMapping[input];
373 [self.oc loadOutput:output forInput:input];
374 }
375
376 - (void)deviceViewControllerDidSelectNothing:(NJDeviceViewController *)dvc {
377 [self loadOutputForInput:nil];
378 }
379
380 - (void)deviceViewController:(NJDeviceViewController *)dvc
381 didSelectBranch:(NJInputPathElement *)handler {
382 [self loadOutputForInput:dvc.selectedHandler];
383 }
384
385 - (void)deviceViewController:(NJDeviceViewController *)dvc
386 didSelectHandler:(NJInputPathElement *)handler {
387 [self loadOutputForInput:dvc.selectedHandler];
388 }
389
390 - (void)deviceViewController:(NJDeviceViewController *)dvc
391 didSelectDevice:(NJInputPathElement *)device {
392 [self loadOutputForInput:dvc.selectedHandler];
393 }
394
395 - (void)inputController:(NJInputController *)ic
396 didAddDevice:(NJDevice *)device {
397 [self.dvc addedDevice:device atIndex:ic.devices.count - 1];
398 }
399
400 - (void)inputController:(NJInputController *)ic
401 didRemoveDeviceAtIndex:(NSInteger)idx {
402 [self.dvc removedDeviceAtIndex:idx];
403 }
404
405 - (void)inputControllerDidStartHID:(NJInputController *)ic {
406 [self.dvc hidStarted];
407 }
408
409 - (void)inputControllerDidStopHID:(NJInputController *)ic {
410 [self.dvc hidStopped];
411 }
412
413 - (void)inputController:(NJInputController *)ic didInput:(NJInput *)input {
414 [self.dvc expandAndSelectItem:input];
415 [self loadOutputForInput:input];
416 [self.oc focusKey];
417 }
418
419 - (void)inputController:(NJInputController *)ic didError:(NSError *)error {
420 [self presentErrorSheet:error];
421 }
422
423 - (NSInteger)numberOfDevicesInDeviceList:(NJDeviceViewController *)dvc {
424 return self.ic.devices.count;
425 }
426
427 - (NJDevice *)deviceViewController:(NJDeviceViewController *)dvc
428 deviceForIndex:(NSUInteger)idx {
429 return self.ic.devices[idx];
430 }
431
432 - (IBAction)simulatingEventsChanged:(NSButton *)sender {
433 self.ic.simulatingEvents = sender.state == NSOnState;
434 }
435
436 - (void)outputViewController:(NJOutputViewController *)ovc
437 setOutput:(NJOutput *)output
438 forInput:(NJInput *)input {
439 self.ic.currentMapping[input] = output;
440 [self.ic save];
441 }
442
443 - (NJMapping *)outputViewController:(NJOutputViewController *)ovc
444 mappingForIndex:(NSUInteger)index {
445 return self.ic.mappings[index];
446 }
447
448 @end