#import "NJOutputController.h"
#import "NJEvents.h"
-@implementation EnjoyableApplicationDelegate
+@implementation EnjoyableApplicationDelegate {
+ NSStatusItem *statusItem;
+}
- (void)didSwitchApplication:(NSNotification *)note {
NSRunningApplication *activeApp = note.userInfo[NSWorkspaceApplicationKey];
[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 {
}
- (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;