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