Add status item. Disable automatic termination, but still hide from the dock when...
[enjoyable.git] / Classes / EnjoyableApplicationDelegate.m
index 78b930b..aaa9f00 100644 (file)
@@ -13,7 +13,9 @@
 #import "NJOutputController.h"
 #import "NJEvents.h"
 
-@implementation EnjoyableApplicationDelegate
+@implementation EnjoyableApplicationDelegate {
+    NSStatusItem *statusItem;
+}
 
 - (void)didSwitchApplication:(NSNotification *)note {
     NSRunningApplication *activeApp = note.userInfo[NSWorkspaceApplicationKey];
@@ -21,7 +23,7 @@
         [self.mappingsController activateMappingForProcess:activeApp];
 }
 
-- (void)applicationDidFinishLaunching:(NSNotification *)notification {
+- (void)applicationWillFinishLaunching:(NSNotification *)notification {
     [NSNotificationCenter.defaultCenter
         addObserver:self
         selector:@selector(mappingDidChange:)
         name:NJEventTranslationDeactivated
         object:nil];
 
-    [self.inputController setup];
     [self.mappingsController load];
+
+    statusItem = [NSStatusBar.systemStatusBar statusItemWithLength:36];
+    statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
+    statusItem.highlightMode = YES;
+    statusItem.menu = statusItemMenu;
+    statusItem.target = self;
 }
 
-- (void)applicationDidBecomeActive:(NSNotification *)notification {
+- (void)applicationDidFinishLaunching:(NSNotification *)notification {
+    [self.inputController setup];
     [window makeKeyAndOrderFront:nil];
 }
 
 - (BOOL)applicationShouldHandleReopen:(NSApplication *)theApplication
                     hasVisibleWindows:(BOOL)flag {
-    [window makeKeyAndOrderFront:nil];
+    [self restoreToForeground:theApplication];
+    return NO;
+}
+
+- (void)restoreToForeground:(id)sender {
+    ProcessSerialNumber psn = { 0, kCurrentProcess };
+    TransformProcessType(&psn, kProcessTransformToForegroundApplication);
+    [NSApplication.sharedApplication activateIgnoringOtherApps:YES];
+    [window makeKeyAndOrderFront:sender];
+    [NSObject cancelPreviousPerformRequestsWithTarget:self
+                                             selector:@selector(transformIntoElement:)
+                                               object:self];
+}
+
+- (void)transformIntoElement:(id)sender {
+    ProcessSerialNumber psn = { 0, kCurrentProcess };
+    TransformProcessType(&psn, kProcessTransformToUIElementApplication);
+}
+
+- (void)flashStatusItem {
+    if ([statusItem.image.name isEqualToString:@"Status Menu Icon"]) {
+        statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
+    } else {
+        statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
+    }
+    
+}
+
+- (BOOL)applicationShouldTerminateAfterLastWindowClosed:(NSApplication *)theApplication {
+    [theApplication hide:theApplication];
+    // If we turn into a UIElement right away, the application cancels
+    // the deactivation events. The dock icon disappears, but an
+    // unresponsive menu bar remains until the user clicks somewhere.
+    // So delay just long enough to be past the end handling that.
+    [self performSelector:@selector(transformIntoElement:) withObject:self afterDelay:0.001];
     return NO;
 }
 
 - (void)eventTranslationActivated:(NSNotification *)note {
-    [NSProcessInfo.processInfo disableAutomaticTermination:@"Input translation is active."];
+    [dockMenu itemAtIndex:0].state = NSOnState;
+    [statusItemMenu itemAtIndex:0].state = NSOnState;
+    statusItem.image = [NSImage imageNamed:@"Status Menu Icon"];
     [NSWorkspace.sharedWorkspace.notificationCenter
         addObserver:self
         selector:@selector(didSwitchApplication:)
         name:NSWorkspaceDidActivateApplicationNotification
         object:nil];
-    NSLog(@"Listening for application changes.");
 }
 
 - (void)eventTranslationDeactivated:(NSNotification *)note {
-    [NSProcessInfo.processInfo enableAutomaticTermination:@"Input translation is active."];
+    [dockMenu itemAtIndex:0].state = NSOffState;
+    [statusItemMenu itemAtIndex:0].state = NSOffState;
+    statusItem.image = [NSImage imageNamed:@"Status Menu Icon Disabled"];
     [NSWorkspace.sharedWorkspace.notificationCenter
         removeObserver:self
         name:NSWorkspaceDidActivateApplicationNotification
         object:nil];
-    NSLog(@"Ignoring application changes.");
 }
 
-- (void)mappingListDidChange:(NSNotification *)note {
-    NSArray *mappings = note.object;
-    while (dockMenuBase.lastItem.representedObject)
-        [dockMenuBase removeLastItem];
+- (void)restoreWindowAndShowMappings:(id)sender {
+    [self restoreToForeground:sender];
+    [self.mappingsController mappingPressed:sender];
+}
+
+- (void)addMappingsToMenu:(NSMenu *)menu withKeys:(BOOL)withKeys atIndex:(NSInteger)index {
+    static const NSUInteger MAXIMUM_ITEMS = 5;
     int added = 0;
-    for (NJMapping *mapping in mappings) {
-        NSString *keyEquiv = ++added < 10 ? @(added).stringValue : @"";
+    for (NJMapping *mapping in self.mappingsController) {
+        NSString *keyEquiv = (++added < 10 && withKeys) ? @(added).stringValue : @"";
         NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:mapping.name
                                                       action:@selector(chooseMapping:)
                                                keyEquivalent:keyEquiv];
         item.representedObject = mapping;
         item.state = mapping == self.mappingsController.currentMapping;
-        [dockMenuBase addItem:item];
-    }
+        [menu insertItem:item atIndex:index++];
+        if (added == MAXIMUM_ITEMS && self.mappingsController.mappings.count > MAXIMUM_ITEMS + 1) {
+            NSMenuItem *end = [[NSMenuItem alloc] initWithTitle:@"…"
+                                                         action:@selector(restoreWindowAndShowMappings:)
+                                                  keyEquivalent:@""];
+            end.target = self;
+            [menu insertItem:end atIndex:index++];
+            break;
+        }
+    }    
+}
+
+- (void)mappingListDidChange:(NSNotification *)note {
+    while (mappingsMenu.lastItem.representedObject)
+        [mappingsMenu removeLastItem];
+    [self addMappingsToMenu:mappingsMenu withKeys:YES atIndex:mappingsMenu.numberOfItems];
+    while ([statusItemMenu itemAtIndex:2].representedObject)
+        [statusItemMenu removeItemAtIndex:2];
+    [self addMappingsToMenu:statusItemMenu withKeys:NO atIndex:2];
 }
 
 - (void)mappingDidChange:(NSNotification *)note {
     NJMapping *current = note.object;
-    for (NSMenuItem *item in dockMenuBase.itemArray)
+    for (NSMenuItem *item in mappingsMenu.itemArray)
+        if (item.representedObject)
+            item.state = item.representedObject == current;
+    for (NSMenuItem *item in statusItemMenu.itemArray)
         if (item.representedObject)
             item.state = item.representedObject == current;
+    
+    if (!window.isVisible)
+        for (int i = 0; i < 4; ++i)
+            [self performSelector:@selector(flashStatusItem)
+                       withObject:self
+                       afterDelay:0.2 * i];
 }
 
 - (void)chooseMapping:(NSMenuItem *)sender {
 }
 
 - (NSMenu *)applicationDockMenu:(NSApplication *)sender {
-    NSMenu *menu = [[NSMenu alloc] init];
-    int added = 0;
-    for (NJMapping *mapping in self.mappingsController) {
-        NSString *keyEquiv = ++added < 10 ? @(added).stringValue : @"";
-        NSMenuItem *item = [[NSMenuItem alloc] initWithTitle:mapping.name
-                                                      action:@selector(chooseMapping:)
-                                               keyEquivalent:keyEquiv];
-        item.representedObject = mapping;
-        item.state = mapping == self.mappingsController.currentMapping;
-        [menu addItem:item];
-    }
-    return menu;
+    while (dockMenu.lastItem.representedObject)
+        [dockMenu removeLastItem];
+    [self addMappingsToMenu:dockMenu withKeys:NO atIndex:dockMenu.numberOfItems];
+    return dockMenu;
 }
 
 - (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
+    [self restoreToForeground:sender];
     NSURL *url = [NSURL fileURLWithPath:filename];
     [self.mappingsController addMappingWithContentsOfURL:url];
     return YES;