Merge pull request #13006 from keymanapp/fix/mac/12997-restore-notifications

fix(mac): properly manage Input Method lifecycle
This commit is contained in:
Shawn Schantz 2025-01-31 11:18:23 +07:00 • committed by GitHub
commit 0da0fb0d37
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 98 additions and 42 deletions

View file

@ -14,5 +14,6 @@
- (void)menuAction:(id)sender;
- (void)handleBackspace:(NSEvent *)event;
- (void)changeClients:(NSString *)clientAppId;
- (void)deactivateClient;
@end

View file

@ -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 {

View file

@ -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 {