Skip to content

Commit

Permalink
Merge pull request #13006 from keymanapp/fix/mac/12997-restore-notifi…
Browse files Browse the repository at this point in the history
…cations

fix(mac): properly manage Input Method lifecycle
  • Loading branch information
sgschantz authored Jan 31, 2025
2 parents 17885a3 + ca2fec7 commit 0da0fb0
Show file tree
Hide file tree
Showing 3 changed files with 94 additions and 38 deletions.
3 changes: 2 additions & 1 deletion mac/Keyman4MacIM/Keyman4MacIM/KMInputController.h
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@

- (void)menuAction:(id)sender;
- (void)handleBackspace:(NSEvent *)event;

- (void)changeClients:(NSString *)clientAppId;
- (void)deactivateClient;
@end
81 changes: 59 additions & 22 deletions mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m
Original file line number Diff line number Diff line change
Expand Up @@ -25,22 +25,13 @@ - (KMInputMethodAppDelegate *)appDelegate {

- (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;
}

Expand All @@ -67,35 +58,81 @@ - (void)handleBackspace:(NSEvent *)event {
}

/**
* 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");
- (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];
}
_eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getRunningApplicationId] client:self.client];
}

- (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 {
Expand Down
48 changes: 33 additions & 15 deletions mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,39 @@ - (void)initCompletion {
// 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"];
Expand All @@ -141,28 +165,22 @@ - (void)inputMethodDeactivated:(NSNotification *)notification {
}
[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 {
Expand Down

0 comments on commit 0da0fb0

Please sign in to comment.