Tweak some animations, clean up for preparation to move to app delegate.
[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
45 statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:36];
46 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
47 statusItem.highlightMode = YES;
48 statusItem.menu = statusItemMenu;
49 statusItem.target = self;
50 }
51
52 - (void)applicationDidFinishLaunching:(NSNotification *)notification {
53 if ([NSUserDefaults.standardUserDefaults boolForKey:@"hidden in status item"]
54 && NSRunningApplication.currentApplication.wasLaunchedAsLoginItemOrResume)
55 [self transformIntoElement:nil];
56 else
57 [window makeKeyAndOrderFront:nil];
58 }
59
60 - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication
61 hasVisibleWindows:(BOOL)flag {
62 [self restoreToForeground:theApplication];
63 return NO;
64 }
65
66 - (void)restoreToForeground:(id)sender {
67 ProcessSerialNumber psn = { 0, kCurrentProcess };
68 TransformProcessType(&psn, kProcessTransformToForegroundApplication);
69 [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
70 [window makeKeyAndOrderFront:sender];
71 [NSObject cancelPreviousPerformRequestsWithTarget:self
72 selector:@selector(transformIntoElement:)
73 object:self];
74 [NSUserDefaults.standardUserDefaults setBool:NO forKey:@"hidden in status item"];
75 }
76
77 - (void)applicationWillBecomeActive:(NSNotification *)notification {
78 if (window.isVisible)
79 [self restoreToForeground:notification];
80 }
81
82 - (void)transformIntoElement:(id)sender {
83 ProcessSerialNumber psn = { 0, kCurrentProcess };
84 TransformProcessType(&psn, kProcessTransformToUIElementApplication);
85 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"hidden in status item"];
86 }
87
88 - (void)flashStatusItem {
89 if ([statusItem.image.name isEqualToString:@"Status Menu Icon"]) {
90 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
91 } else {
92 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
93 }
94
95 }
96
97 - (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
98 [theApplication hide:theApplication];
99 // If we turn into a UIElement right away, the application cancels
100 // the deactivation events. The dock icon disappears, but an
101 // unresponsive menu bar remains until the user clicks somewhere.
102 // So delay just long enough to be past the end handling that.
103 [self performSelector:@selector(transformIntoElement:) withObject:self afterDelay:0.001];
104 return NO;
105 }
106
107 - (void)eventSimulationStarted:(NSNotification *)note {
108 statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
109 [NSWorkspace.sharedWorkspace.notificationCenter
110 addObserver:self
111 selector:@selector(didSwitchApplication:)
112 name:NSWorkspaceDidActivateApplicationNotification
113 object:nil];
114 }
115
116 - (void)eventSimulationStopped:(NSNotification *)note {
117 statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
118 [NSWorkspace.sharedWorkspace.notificationCenter
119 removeObserver:self
120 name:NSWorkspaceDidActivateApplicationNotification
121 object:nil];
122 }
123
124 - (void)mappingDidChange:(NSNotification *)note {
125 if (!window.isVisible)
126 for (int i = 0; i < 4; ++i)
127 [self performSelector:@selector(flashStatusItem)
128 withObject:self
129 afterDelay:0.2 * i];
130 }
131
132 - (NSMenu *)applicationDockMenu:(NSApplication *)sender {
133 return dockMenu;
134 }
135
136 - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
137 [self restoreToForeground:sender];
138 NSError *error;
139 NSURL *URL = [NSURL fileURLWithPath:filename];
140 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:URL
141 error:&error];
142 if ([self.mappingsController[mapping.name] hasConflictWith:mapping]) {
143 [self.mappingsController promptForMapping:mapping atIndex:self.mappingsController.count];
144 } else if (self.mappingsController[mapping.name]) {
145 [self.mappingsController[mapping.name] mergeEntriesFrom:mapping];
146 } else if (mapping) {
147 [self.mappingsController addMapping:mapping];
148 } else {
149 [window presentError:error
150 modalForWindow:window
151 delegate:nil
152 didPresentSelector:nil
153 contextInfo:nil];
154 }
155 return !!mapping;
156 }
157
158 - (void)mappingWasChosen:(NJMapping *)mapping {
159 [self.mappingsController activateMapping:mapping];
160 }
161
162 - (void)mappingListShouldOpen {
163 [self restoreToForeground:self];
164 [self.mappingsController.mvc mappingTriggerClicked:self];
165 }
166
167 - (void)loginItemPromptDidEnd:(NSWindow *)sheet
168 returnCode:(int)returnCode
169 contextInfo:(void *)contextInfo {
170 if (returnCode == NSAlertDefaultReturn) {
171 [NSRunningApplication.currentApplication addToLoginItems];
172 // If we're going to automatically start, don't bug the user
173 // about automatic updates next boot - they probably want it,
174 // and if they don't they probably want a prompt for it less.
175 SUUpdater.sharedUpdater.automaticallyChecksForUpdates = YES;
176 }
177 }
178
179 - (void)loginItemPromptDidDismiss:(NSWindow *)sheet
180 returnCode:(int)returnCode
181 contextInfo:(void *)contextInfo {
182 [NSUserDefaults.standardUserDefaults setBool:YES forKey:@"explained login items"];
183 [window performClose:sheet];
184 }
185
186 - (BOOL)windowShouldClose:(NSWindow *)sender {
187 if (sender != window
188 || NSRunningApplication.currentApplication.isLoginItem
189 || [NSUserDefaults.standardUserDefaults boolForKey:@"explained login items"])
190 return YES;
191 NSBeginAlertSheet(
192 NSLocalizedString(@"login items prompt", @"alert prompt for adding to login items"),
193 NSLocalizedString(@"login items add button", @"button to add to login items"),
194 NSLocalizedString(@"login items don't add button", @"button to not add to login items"),
195 nil, window, self,
196 @selector(loginItemPromptDidEnd:returnCode:contextInfo:),
197 @selector(loginItemPromptDidDismiss:returnCode:contextInfo:),
198 NULL,
199 NSLocalizedString(@"login items explanation", @"a brief explanation of login items")
200 );
201 for (int i = 0; i < 10; ++i)
202 [self performSelector:@selector(flashStatusItem)
203 withObject:self
204 afterDelay:0.5 * i];
205 return NO;
206 }
207
208 - (void)importMappingClicked:(id)sender {
209 NSOpenPanel *panel = [NSOpenPanel openPanel];
210 panel.allowedFileTypes = @[ @"enjoyable", @"json", @"txt" ];
211 [panel beginSheetModalForWindow:window
212 completionHandler:^(NSInteger result) {
213 if (result != NSFileHandlingPanelOKButton)
214 return;
215 [panel close];
216 NSError *error;
217 NJMapping *mapping = [NJMapping mappingWithContentsOfURL:panel.URL
218 error:&error];
219 if ([self.mappingsController[mapping.name] hasConflictWith:mapping]) {
220 [self.mappingsController promptForMapping:mapping atIndex:self.mappingsController.count];
221 } else if (self.mappingsController[mapping.name]) {
222 [self.mappingsController[mapping.name] mergeEntriesFrom:mapping];
223 } else if (mapping) {
224 [self.mappingsController addMapping:mapping];
225 } else {
226 [window presentError:error
227 modalForWindow:window
228 delegate:nil
229 didPresentSelector:nil
230 contextInfo:nil];
231 }
232 }];
233
234 }
235
236 - (void)exportMappingClicked:(id)sender {
237 NSSavePanel *panel = [NSSavePanel savePanel];
238 panel.allowedFileTypes = @[ @"enjoyable" ];
239 NJMapping *mapping = self.mappingsController.currentMapping;
240 panel.nameFieldStringValue = [mapping.name stringByFixingPathComponent];
241 [panel beginSheetModalForWindow:window
242 completionHandler:^(NSInteger result) {
243 if (result != NSFileHandlingPanelOKButton)
244 return;
245 [panel close];
246 NSError *error;
247 if (![mapping writeToURL:panel.URL error:&error]) {
248 [window presentError:error
249 modalForWindow:window
250 delegate:nil
251 didPresentSelector:nil
252 contextInfo:nil];
253 }
254 }];
255 }
256
257
258
259 @end