From a69426f2f53364a8912f102806a7288754468dd5 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Thu, 23 Jan 2025 16:28:48 +0700 Subject: [PATCH 1/3] fix(mac): properly manage lifecycle moves handling of all KMInputMethodLifeCycle notifications to KMInputMethodAppDelegate and out of KMInputController because KMInputController instances are being cached by the OS and swapped in when the input method changes state Fixes: #12997 --- .../Keyman4MacIM/KMInputController.h | 3 +- .../Keyman4MacIM/KMInputController.m | 82 +++++++++++++------ .../Keyman4MacIM/KMInputMethodAppDelegate.m | 48 +++++++---- 3 files changed, 91 insertions(+), 42 deletions(-) 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..1885f33091 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,74 @@ 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 figure out whether this is a real activation of + * the input method or a change in clients or 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 figure out whether this is a real deactivation of + * the input method or a change in clients or 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 73afb9271d..144b2e6537 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 addBreadCrumb:@"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 addBreadCrumb:@"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, show the OSK and enable the low-level event tap + * 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 addBreadCrumb:@"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 { From 7ba30224ffcc966f15a3159c50e67b47782416e5 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Thu, 30 Jan 2025 16:00:44 +0700 Subject: [PATCH 2/3] fix(mac): resolve build error --- mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index b7b5830500..5e88d2b59b 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -145,7 +145,7 @@ id _lastServerWithOSKShowing = nil; if ([KMInputMethodLifecycle.shared shouldShowOskOnActivate]) { os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK"); - [KMSentryHelper addBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; + [KMSentryHelper addDebugBreadCrumb:@"lifecycle" message:@"opening OSK on input method activation"]; [self showOSK]; } } From ca2fec729c74ed3b4649bc8a064a82616c33a928 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Fri, 31 Jan 2025 09:44:08 +0700 Subject: [PATCH 3/3] fix(mac): comment changes --- .../Keyman4MacIM/KMInputController.m | 27 ++++++++++++------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 1885f33091..c628311b9f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -97,16 +97,20 @@ KMInputMethodEventHandler* _eventHandler; 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. + /* + 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 figure out whether this is a real activation of - * the input method or a change in clients or a false alarm. + /* + 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]; } @@ -121,9 +125,12 @@ KMInputMethodEventHandler* _eventHandler; - (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 figure out whether this is a real deactivation of - * the input method or a change in clients or a false alarm. + /* + 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]; }