Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(mac): properly manage Input Method lifecycle #13006

Draft
wants to merge 1 commit into
base: fix/mac/12928-open-osk-on-activation
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
74 changes: 52 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,74 @@ - (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 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 {
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 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"];
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, 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 {
Expand Down
Loading