diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h index 684db6fb38..c1264cb210 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h @@ -14,5 +14,6 @@ - (void)menuAction:(id)sender; - (void)handleBackspace:(NSEvent *)event; - +- (void)changeClients:(NSString *)clientAppId; +- (void)deactivateClient; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index dd53b49622..c628311b9f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -25,22 +25,13 @@ KMInputMethodEventHandler* _eventHandler; - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { - os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]); + os_log_debug([KMLogs lifecycleLog], "+++KMInputController initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getRunningApplicationId]); self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { - self.appDelegate.inputController = self; + os_log_debug([KMLogs lifecycleLog], " +++initWithServer, self: %p", self); } - /** - * Register to receive the Deactivated and ChangedClient notification generated from KMInputMethodLifecycle so - * that the eventHandler can be changed. There is no need to receive the Activated notification because - * the InputController does it all it needs to when it receives the ChangedClient. - */ - - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; - return self; } @@ -67,35 +58,81 @@ KMInputMethodEventHandler* _eventHandler; } /** - * The Keyman input method is deactivating because the user chose a different input method: notification from KMInputMethodLifecycle + * Called by the app delegate when KMInputMethodLifecycle determines the user has switched clients */ -- (void)inputMethodDeactivated:(NSNotification *)notification { - os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodDeactivated, deactivating eventHandler"); - if (_eventHandler != nil) { - [_eventHandler deactivate]; - } +- (void)changeClients:(NSString *)clientAppId { + os_log_debug([KMLogs lifecycleLog], "***KMInputController changeClients, deactivate old eventHandler and activate new one"); + + [self deactivateEventHandler]; + [self activateEventHandler:clientAppId]; } /** - * The user has switched to a different text input client: notification from KMInputMethodLifecycle + * Called by the app delegate when KMInputMethodLifecycle determines the user has deactivated Keyman */ -- (void)inputMethodChangedClient:(NSNotification *)notification { - os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodChangedClient, deactivating old eventHandler and activating new one"); - if (_eventHandler != nil) { - [_eventHandler deactivate]; - } - _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getRunningApplicationId] client:self.client]; - +- (void)deactivateClient { + os_log_debug([KMLogs lifecycleLog], "***KMInputController deactivateClient, deactivate old eventHandler"); + + [self deactivateEventHandler]; } +- (void)deactivateEventHandler { + if (_eventHandler != nil) { + os_log_debug([KMLogs lifecycleLog], " * KMInputController deactivate old eventHandler"); + [_eventHandler deactivate]; + } +} + +- (void)activateEventHandler:(NSString *)clientAppId { + os_log_debug([KMLogs lifecycleLog], " * KMInputController activate new eventHandler"); + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:self.client]; +} + +/** + * Called by the OS to activate the input method, but some calls are not useful and are followed milliseconds later + * by a deactivateServer. These short-lived activations may be generated by clicking and releasing menu items without changing applications. + * Rather than treat every message received as a true activation, we send a message to KMInputMethodLifeCycle to evaluate. + */ - (void)activateServer:(id)sender { + os_log_info([KMLogs lifecycleLog], " +++KMInputController, activateServer, self=%p", self); [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; + + /* + When this KMInputController becomes the active server for the input method, + then immediately update the AppDelegate. The duration that this controller is the + active server may be extremely short, but, if so, we will receive another + call to activateServer moments later and can update the AppDelegate again. + */ + [self attachToAppDelegate]; + + /* + Call the shared lifecycle object so it can evaluate the current state + and determine whether this is + 1) a real activation of the input method or + 2) a change in clients or + 3) a false alarm + */ [KMInputMethodLifecycle.shared activateClient:sender]; } +- (void)attachToAppDelegate { + self.appDelegate.inputController = self; +} + +/** + * Called by the OS to deactivate the input method + */ - (void)deactivateServer:(id)sender { + os_log_info([KMLogs lifecycleLog], " +++KMInputController, deactivateServer, self=%p", self); + + /* + Call the shared lifecycle object so it can evaluate the current state + and determine whether this is + 1) a real deactivation of the input method or + 2) a change in clients or + 3) a false alarm + */ [KMInputMethodLifecycle.shared deactivateClient:sender]; - [[NSNotificationCenter defaultCenter] removeObserver:self]; } - (NSMenu *)menu { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 0c4199959e..5e88d2b59b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -123,15 +123,39 @@ id _lastServerWithOSKShowing = nil; // register to receive notifications generated from KMInputMethodLifecycle [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodActivated:) name:kInputMethodActivatedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; // start Input Method lifecycle [KMInputMethodLifecycle.shared startLifecycle]; } /** - * When the input method is deactivated, hide the OSK and disable the low-level event tap + * When the input method is activated -- notification from KMInputMethodLifecycle + */ +- (void)inputMethodActivated:(NSNotification *)notification { + + // enable event tap + if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) { + os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap..."); + CGEventTapEnable(self.lowLevelEventTap, YES); + } + + // show OSK if necessary + os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false"); + + if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) { + os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); + [KMSentryHelper addDebugBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; + [self showOSK]; + } +} + +/** + * When the input method is deactivated -- notification from KMInputMethodLifecycle */ - (void)inputMethodDeactivated:(NSNotification *)notification { + + // if the OSK is visible, hide it if ([self.oskWindow.window isVisible]) { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK"); [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"hiding OSK on input method deactivation"]; @@ -141,28 +165,22 @@ id _lastServerWithOSKShowing = nil; } [KMSentryHelper addOskVisibleTag:[self.oskWindow.window isVisible]]; + // disable the event tap if (self.lowLevelEventTap) { os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap"); CGEventTapEnable(self.lowLevelEventTap, NO); } + + // deactive the text input client + [self.inputController deactivateClient]; } /** - * When the input method is activated, enable the low-level event tap and show the OSK + * When the user switches to a different text input client -- notification from KMInputMethodLifecycle */ -- (void)inputMethodActivated:(NSNotification *)notification { - if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) { - os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodActivated, re-enabling event tap..."); - CGEventTapEnable(self.lowLevelEventTap, YES); - } - - os_log_debug([KMLogs lifecycleLog], "--- inputMethodActivated, kvk is non-nil: %{public}@ showOskOnActivate: %{public}@", (_kvk!=nil)?@"true":@"false", [KMInputMethodLifecycle.shared shouldShowOskOnActivate]?@"true":@"false"); - - if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) { - os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); - [KMSentryHelper addInfoBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; - [self showOSK]; - } +- (void)inputMethodChangedClient:(NSNotification *)notification { + os_log_debug([KMLogs lifecycleLog], "***KMInputMethodAppDelegate inputMethodChangedClient"); + [self.inputController changeClients:[KMInputMethodLifecycle getRunningApplicationId]]; } - (KeymanVersionInfo)versionInfo {