Have device controller setup itself rather than relying on the application delegate...
[enjoyable.git] / Classes / EnjoyableApplicationDelegate.m
index 5c68476..634f362 100644 (file)
 #import "NJOutputController.h"
 #import "NJEvents.h"
 
-@implementation EnjoyableApplicationDelegate
+@implementation EnjoyableApplicationDelegate {
+    NSStatusItem *statusItem;
+}
 
 - (void)didSwitchApplication:(NSNotification *)note {
     NSRunningApplication *activeApp = note.userInfo[NSWorkspaceApplicationKey];
-    NSString *name = activeApp.localizedName;
-    if (!name)
-        name = activeApp.bundleIdentifier;
-    if (name && ![name isEqualToString:NSRunningApplication.currentApplication.localizedName])
-        [self.mappingsController activateMappingForProcess:name];
+    if (activeApp)
+        [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 {
     [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 = 15;
     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) {
+            NSString *msg = [NSString stringWithFormat:@"(and %lu moreā€¦)",
+                             self.mappingsController.mappings.count - MAXIMUM_ITEMS];
+            NSMenuItem *end = [[NSMenuItem alloc] initWithTitle:msg
+                                                         action:@selector(restoreWindowAndShowMappings:)
+                                                  keyEquivalent:@""];
+            // There must be a represented object here so the item gets
+            // removed correctly when the menus are regenerated.
+            end.representedObject = self.mappingsController.mappings;
+            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 {
     [self.mappingsController activateMapping:chosen];
 }
 
-#define OUTPUT_PANE_MIN_WIDTH 390
-#define INPUT_PANE_MIN_WIDTH 160
-
-- (CGFloat)splitView:(NSSplitView *)splitView constrainMinCoordinate:(CGFloat)proposedMinimumPosition ofSubviewAt:(NSInteger)dividerIndex {
-    return INPUT_PANE_MIN_WIDTH;
-}
-
-- (CGFloat)splitView:(NSSplitView *)sender constrainMaxCoordinate:(CGFloat)proposedMax ofSubviewAt:(NSInteger)offset {
-    return proposedMax - OUTPUT_PANE_MIN_WIDTH;
-}
-
-- (void)splitView:(NSSplitView *)splitView resizeSubviewsWithOldSize:(NSSize)oldSize {
-    NSView *inputView = splitView.subviews[0];
-    NSView *outputView = splitView.subviews[1];
-    if (outputView.frame.size.width < OUTPUT_PANE_MIN_WIDTH) {
-        NSSize frameSize = splitView.frame.size;
-        CGFloat inputWidth = frameSize.width - OUTPUT_PANE_MIN_WIDTH - splitView.dividerThickness;
-        inputView.frame = NSMakeRect(inputWidth, frameSize.height,
-                                    inputView.frame.size.width,
-                                    inputView.frame.size.height);
-        outputView.frame = NSMakeRect(inputWidth + splitView.dividerThickness,
-                                     0,
-                                     OUTPUT_PANE_MIN_WIDTH,
-                                     frameSize.height);
-    } else
-        [splitView adjustSubviews];
-}
-
 - (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;