mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-19 06:07:40 +00:00
change(mac): added KMInputMethodLifecycle
clearly manages the state of the input method and notifies any observers of changes Fixes: #12342
This commit is contained in:
parent
78fd4ee19b
commit
b8cbf04ff7
7 changed files with 365 additions and 117 deletions
|
|
@ -14,6 +14,7 @@
|
|||
293EA3E627140D8100545EED /* KMAboutWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3E827140D8100545EED /* KMAboutWindowController.xib */; };
|
||||
293EA3EB27140DEC00545EED /* preferences.xib in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3ED27140DEC00545EED /* preferences.xib */; };
|
||||
293EA3F427181FDA00545EED /* Localizable.strings in Resources */ = {isa = PBXBuildFile; fileRef = 293EA3F627181FDA00545EED /* Localizable.strings */; };
|
||||
296105232C8E91C7007BF6B7 /* KMInputMethodLifecycle.m in Sources */ = {isa = PBXBuildFile; fileRef = 296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */; };
|
||||
296FE2FC275DD21600F46898 /* KMPackageReader.m in Sources */ = {isa = PBXBuildFile; fileRef = 296FE2FB275DD21600F46898 /* KMPackageReader.m */; };
|
||||
297A501728DF4D360074EB1B /* PrivacyWindowController.m in Sources */ = {isa = PBXBuildFile; fileRef = 297A501228DF4D360074EB1B /* PrivacyWindowController.m */; };
|
||||
297A501828DF4D360074EB1B /* PrivacyWindowController.xib in Resources */ = {isa = PBXBuildFile; fileRef = 297A501328DF4D360074EB1B /* PrivacyWindowController.xib */; };
|
||||
|
|
@ -158,6 +159,8 @@
|
|||
293EA3EF27140DFA00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/preferences.strings; sourceTree = "<group>"; };
|
||||
293EA3F02714158600545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = "<group>"; };
|
||||
293EA3F527181FDA00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||
296105212C8E91C7007BF6B7 /* KMInputMethodLifecycle.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodLifecycle.h; sourceTree = "<group>"; };
|
||||
296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodLifecycle.m; sourceTree = "<group>"; };
|
||||
296FE2FA275DD21600F46898 /* KMPackageReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMPackageReader.h; sourceTree = "<group>"; };
|
||||
296FE2FB275DD21600F46898 /* KMPackageReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackageReader.m; sourceTree = "<group>"; };
|
||||
29781101297FB262007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/KMAboutWindowController.strings; sourceTree = "<group>"; };
|
||||
|
|
@ -579,6 +582,8 @@
|
|||
98D6DA7D1A799FF400B09822 /* KMInputController.m */,
|
||||
98A778C21A8C53BF00CF809D /* KMInputMethodAppDelegate.h */,
|
||||
98A778C31A8C53BF00CF809D /* KMInputMethodAppDelegate.m */,
|
||||
296105212C8E91C7007BF6B7 /* KMInputMethodLifecycle.h */,
|
||||
296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */,
|
||||
E21799031FC5B74D00F2D66A /* KMInputMethodEventHandler.h */,
|
||||
E21799041FC5B7BC00F2D66A /* KMInputMethodEventHandler.m */,
|
||||
298D09F62A1F4533006B9DFE /* TextApiCompliance.h */,
|
||||
|
|
@ -993,6 +998,7 @@
|
|||
29B4A0D52BF7675A00682049 /* KMLogs.m in Sources */,
|
||||
98BF924F1BF02DC20002126A /* KMBarView.m in Sources */,
|
||||
E240F599202DED740000067D /* KMPackage.m in Sources */,
|
||||
296105232C8E91C7007BF6B7 /* KMInputMethodLifecycle.m in Sources */,
|
||||
D861B03F2C5747F70003675E /* KMSettingsRepository.m in Sources */,
|
||||
984B8F441AF1C3D900E096A8 /* OSKWindowController.m in Sources */,
|
||||
9836B3711AE5F11D00780482 /* mztools.c in Sources */,
|
||||
|
|
|
|||
|
|
@ -13,13 +13,12 @@
|
|||
#import "KMSettingsRepository.h"
|
||||
#import "KMLogs.h"
|
||||
#import "InputMethodKit/InputMethodKit.h"
|
||||
#import "KMInputMethodLifecycle.h"
|
||||
|
||||
|
||||
@implementation KMInputController
|
||||
const double inactivityTimeout = 0.7;
|
||||
|
||||
KMInputMethodEventHandler* _eventHandler;
|
||||
NSMutableDictionary *textInputClients;
|
||||
|
||||
- (KMInputMethodAppDelegate *)appDelegate {
|
||||
return (KMInputMethodAppDelegate *)[NSApp delegate];
|
||||
|
|
@ -33,14 +32,13 @@ NSMutableDictionary *textInputClients;
|
|||
|
||||
self = [super initWithServer:server delegate:delegate client:inputClient];
|
||||
if (self) {
|
||||
textInputClients = [[NSMutableDictionary alloc] initWithCapacity:2];
|
||||
self.appDelegate.inputController = self;
|
||||
if ((self.appDelegate.kvk != nil) && ([KMSettingsRepository.shared readShowOskOnActivate])) {
|
||||
os_log_debug([KMLogs oskLog], " initWithServer, readShowOskOnActivate= YES, showing OSK");
|
||||
[self.appDelegate showOSK];
|
||||
}
|
||||
}
|
||||
|
||||
// register to receive notifications generated from KMInputMethodLifecycle
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil];
|
||||
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil];
|
||||
|
||||
return self;
|
||||
}
|
||||
|
||||
|
|
@ -66,68 +64,32 @@ NSMutableDictionary *textInputClients;
|
|||
}
|
||||
}
|
||||
|
||||
- (void)activateServer:(id)sender {
|
||||
@synchronized(textInputClients) {
|
||||
os_log_debug([KMLogs lifecycleLog], "KMInputController activateServer, sender %{public}@", sender);
|
||||
[sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"];
|
||||
NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
NSString *clientAppId = [currentApp bundleIdentifier];
|
||||
NSUInteger key = ((NSObject*)sender).hash;
|
||||
NSString *keyString = [@(key) stringValue];
|
||||
//NSValue *key = [NSValue valueWithNonretainedObject:sender];
|
||||
os_log_debug([KMLogs lifecycleLog], " +++adding client application '%{public}@' to textInputClients map, derived key: %{public}@", clientAppId, keyString);
|
||||
|
||||
[textInputClients setObject:clientAppId forKey:keyString];
|
||||
os_log_debug([KMLogs lifecycleLog], " textInputClients map: %{public}@", textInputClients.description);
|
||||
|
||||
[self.appDelegate wakeUpWith:sender];
|
||||
|
||||
if (_eventHandler != nil) {
|
||||
[_eventHandler deactivate];
|
||||
}
|
||||
|
||||
_eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:sender];
|
||||
- (void)inputMethodDeactivated:(NSNotification *)notification {
|
||||
os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodDeactivated, deactivating eventHandler");
|
||||
if (_eventHandler != nil) {
|
||||
[_eventHandler deactivate];
|
||||
}
|
||||
}
|
||||
|
||||
- (void)inputMethodChangedClient:(NSNotification *)notification {
|
||||
os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodChangedClient, deactivating old eventHandler and activating new one");
|
||||
if (_eventHandler != nil) {
|
||||
[_eventHandler deactivate];
|
||||
}
|
||||
NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
NSString *clientAppId = [currentApp bundleIdentifier];
|
||||
// TODO: remove client argument
|
||||
_eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:nil];
|
||||
}
|
||||
|
||||
- (void)activateServer:(id)sender {
|
||||
[sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"];
|
||||
[KMInputMethodLifecycle.shared activateClient:sender];
|
||||
}
|
||||
|
||||
- (void)deactivateServer:(id)sender {
|
||||
os_log_debug([KMLogs lifecycleLog], "KMInputController deactivateServer, sender %{public}@", sender);
|
||||
@synchronized(textInputClients) {
|
||||
NSUInteger key = ((NSObject*)sender).hash;
|
||||
NSString *keyString = [@(key) stringValue];
|
||||
//NSValue *key = [NSValue valueWithNonretainedObject:sender];
|
||||
NSString *clientAppId = [textInputClients objectForKey:keyString];
|
||||
|
||||
if (clientAppId) {
|
||||
os_log_debug([KMLogs lifecycleLog], " ---removing client application '%{public}@' from textInputClients map, key: %{public}@", clientAppId, keyString);
|
||||
[textInputClients removeObjectForKey:keyString];
|
||||
} else {
|
||||
os_log_debug([KMLogs lifecycleLog], " key %{public}@ not found in textInputClients map", keyString);
|
||||
}
|
||||
os_log_debug([KMLogs lifecycleLog], " textInputClients map: %{public}@", textInputClients.description);
|
||||
if (textInputClients.count == 0) {
|
||||
os_log_debug([KMLogs lifecycleLog], "no text input clients found in textInputClients map; delay for %f seconds and call sleepIfNoClients", inactivityTimeout);
|
||||
[self performSelector:@selector(sleepIfNoClients:) withObject:sender afterDelay:inactivityTimeout];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
- (void)sleepIfNoClients:(id)lastClient {
|
||||
@synchronized(textInputClients) {
|
||||
if (textInputClients.count == 0) {
|
||||
os_log_debug([KMLogs lifecycleLog], "sleepIfNoClients found no clients, time to sleep");
|
||||
if (_eventHandler != nil) {
|
||||
[_eventHandler deactivate];
|
||||
_eventHandler = nil;
|
||||
}
|
||||
[self.appDelegate sleepFollowingInactivityTimeout:lastClient];
|
||||
} else {
|
||||
NSArray*keys=[textInputClients allKeys];
|
||||
NSObject *key = keys[0];
|
||||
NSString *clientAppId = [textInputClients objectForKey:key];
|
||||
os_log_debug([KMLogs lifecycleLog], "sleepIfNoClients found a newly activated client, clientAppId '%{public}@', key: %{public}@", clientAppId, key);
|
||||
}
|
||||
}
|
||||
[KMInputMethodLifecycle.shared deactivateClient:sender];
|
||||
[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
}
|
||||
|
||||
- (NSMenu *)menu {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,6 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0;
|
|||
@property (nonatomic, assign) NSEventModifierFlags currentModifierFlags;
|
||||
@property (nonatomic, assign) CFMachPortRef lowLevelEventTap;
|
||||
@property (nonatomic, assign) CFRunLoopSourceRef runLoopEventSrc;
|
||||
@property (nonatomic, assign) BOOL sleeping;
|
||||
@property (nonatomic, assign) BOOL contextChangedByLowLevelEvent;
|
||||
@property (nonatomic, strong) OSKWindowController *oskWindow;
|
||||
@property (nonatomic, strong) NSString *keyboardName;
|
||||
|
|
@ -101,8 +100,6 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0;
|
|||
- (void)showOSK;
|
||||
- (void)showConfigurationWindow;
|
||||
- (void)selectKeyboardFromMenu:(NSInteger)tag;
|
||||
- (void)sleepFollowingInactivityTimeout:(id)lastServer;
|
||||
- (void)wakeUpWith:(id)newServer;
|
||||
- (void)handleKeyEvent:(NSEvent *)event;
|
||||
- (BOOL)unzipFile:(NSString *)filePath;
|
||||
- (NSWindowController *)downloadKBWindow_;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
//
|
||||
|
||||
#import "KMInputMethodAppDelegate.h"
|
||||
#import "KMInputMethodLifecycle.h"
|
||||
#import "KMSettingsRepository.h"
|
||||
#import "KMDataRepository.h"
|
||||
#import "KMConfigurationWindowController.h"
|
||||
|
|
@ -115,6 +116,45 @@ id _lastServerWithOSKShowing = nil;
|
|||
if (self.runLoopEventSrc && runLoop) {
|
||||
CFRunLoopAddSource(runLoop, self.runLoopEventSrc, kCFRunLoopDefaultMode);
|
||||
}
|
||||
|
||||
// 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];
|
||||
|
||||
// start Input Method lifecycle
|
||||
[KMInputMethodLifecycle.shared startLifecycle];
|
||||
}
|
||||
|
||||
/**
|
||||
* When the input method is deactivated, hide the OSK and disable the low-level event tap
|
||||
*/
|
||||
- (void)inputMethodDeactivated:(NSNotification *)notification {
|
||||
if ([self.oskWindow.window isVisible]) {
|
||||
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, hiding OSK");
|
||||
[self.oskWindow.window setIsVisible:NO];
|
||||
} else {
|
||||
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodDeactivated, OSK already hidden");
|
||||
}
|
||||
|
||||
if (self.lowLevelEventTap) {
|
||||
os_log_debug([KMLogs lifecycleLog], "***inputMethodDeactivated, disabling event tap");
|
||||
CGEventTapEnable(self.lowLevelEventTap, NO);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* When the input method is activated, show the OSK and enable the low-level event tap
|
||||
*/
|
||||
- (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);
|
||||
}
|
||||
|
||||
if (_kvk != nil && ([KMInputMethodLifecycle.shared shouldShowOskOnActivate])) {
|
||||
os_log_debug([KMLogs oskLog], "***KMInputMethodAppDelegate inputMethodActivated, showing OSK");
|
||||
[self showOSK];
|
||||
}
|
||||
}
|
||||
|
||||
- (KeymanVersionInfo)versionInfo {
|
||||
|
|
@ -221,46 +261,12 @@ id _lastServerWithOSKShowing = nil;
|
|||
return (KMInputMethodAppDelegate *)[NSApp delegate];
|
||||
}
|
||||
|
||||
-(void) sleepFollowingInactivityTimeout:(id)lastServer {
|
||||
os_log_debug([KMLogs lifecycleLog], "Keyman no longer active IM.");
|
||||
self.sleeping = YES;
|
||||
if ([self.oskWindow.window isVisible]) {
|
||||
os_log_debug([KMLogs oskLog], "sleepFollowingDeactivationOfServer, Hiding OSK.");
|
||||
// Storing this ensures that if the deactivation is temporary, resulting from dropping down a menu,
|
||||
// the OSK will re-display when that client application re-activates.
|
||||
_lastServerWithOSKShowing = lastServer;
|
||||
[self.oskWindow.window setIsVisible:NO];
|
||||
}
|
||||
if (self.lowLevelEventTap) {
|
||||
os_log_debug([KMLogs lifecycleLog], "sleepFollowingDeactivationOfServer, disabling event tap...");
|
||||
CGEventTapEnable(self.lowLevelEventTap, NO);
|
||||
}
|
||||
}
|
||||
|
||||
-(void) wakeUpWith:(id)newServer {
|
||||
os_log_debug([KMLogs oskLog], "wakeUpWith, newServer:%{public}@", newServer);
|
||||
self.sleeping = NO;
|
||||
if (self.lowLevelEventTap && !CGEventTapIsEnabled(self.lowLevelEventTap)) {
|
||||
os_log_debug([KMLogs lifecycleLog], "wakeUpWith, Keyman is now the active IM. Re-enabling event tap...");
|
||||
CGEventTapEnable(self.lowLevelEventTap, YES);
|
||||
}
|
||||
// See note in sleepFollowingInactivityTimeout.
|
||||
if (_kvk != nil && (_lastServerWithOSKShowing == newServer) &&
|
||||
([KMSettingsRepository.shared readShowOskOnActivate])) {
|
||||
os_log_debug([KMLogs oskLog], "wakeUpWith, readShowOskOnActivate= YES, showing OSK");
|
||||
[self showOSK];
|
||||
}
|
||||
|
||||
_lastServerWithOSKShowing = nil;
|
||||
}
|
||||
|
||||
CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef event, void *refcon) {
|
||||
KMInputMethodAppDelegate *appDelegate = [KMInputMethodAppDelegate AppDelegate];
|
||||
if (appDelegate != nil) {
|
||||
if (type == kCGEventTapDisabledByTimeout || type == kCGEventTapDisabledByUserInput) {
|
||||
// kCGEventTapDisabledByUserInput most likely means we're "sleeping", in which case we want it to stay
|
||||
// disabled until we get the wake-up call.
|
||||
if (!appDelegate.sleeping) {
|
||||
// kCGEventTapDisabledByUserInput most likely means we're "sleeping", in which case we want it to stay disabled until we get the wake-up call.
|
||||
if ([KMInputMethodLifecycle.shared shouldEnableEventTap]) {
|
||||
// REVIEW: We might need to consider putting in some kind of counter/flag to ensure that the very next
|
||||
// event is not another disable so we don't end up in an endless cycle.
|
||||
os_log([KMLogs eventsLog], "Event tap disabled by %{public}@! Attempting to restart...", (type == kCGEventTapDisabledByTimeout ? @"timeout" : @"user"));
|
||||
|
|
@ -703,7 +709,7 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
|
|||
[KMDataRepository.shared createDataDirectoryIfNecessary];
|
||||
|
||||
if ([KMSettingsRepository.shared dataMigrationNeeded]) {
|
||||
BOOL movedData = [KMDataRepository.shared migrateData];
|
||||
[KMDataRepository.shared migrateData];
|
||||
[KMSettingsRepository.shared convertSettingsForMigration];
|
||||
}
|
||||
|
||||
|
|
@ -945,16 +951,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
|
|||
_configWindow = window;
|
||||
}
|
||||
|
||||
/*
|
||||
- (BOOL)showOskOnActivation {
|
||||
return [KMSettingsRepository.shared readShowOsk];
|
||||
}
|
||||
|
||||
- (void)saveShowOskOnActivation: (BOOL)showOsk {
|
||||
[KMSettingsRepository.shared writeShowOsk:showOsk];
|
||||
}
|
||||
*/
|
||||
|
||||
- (void)showOSK {
|
||||
[[self.oskWindow window] makeKeyAndOrderFront:nil];
|
||||
[[self.oskWindow window] setLevel:NSStatusWindowLevel];
|
||||
|
|
|
|||
26
mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h
Normal file
26
mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL International. MIT License.
|
||||
*
|
||||
* Created by Shawn Schantz on 2024-09-09.
|
||||
*
|
||||
*/
|
||||
|
||||
#import <Foundation/Foundation.h>
|
||||
|
||||
NS_ASSUME_NONNULL_BEGIN
|
||||
|
||||
extern NSString *const kInputMethodActivatedNotification;
|
||||
extern NSString *const kInputMethodDeactivatedNotification;
|
||||
extern NSString *const kInputMethodClientChangeNotification;
|
||||
|
||||
@interface KMInputMethodLifecycle : NSObject
|
||||
+ (KMInputMethodLifecycle *)shared;
|
||||
- (void)startLifecycle;
|
||||
- (void)activateClient:(id)client;
|
||||
- (void)deactivateClient:(id)client;
|
||||
- (BOOL)shouldEnableEventTap;
|
||||
- (BOOL)shouldShowOskOnActivate;
|
||||
|
||||
@end
|
||||
|
||||
NS_ASSUME_NONNULL_END
|
||||
262
mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
Normal file
262
mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m
Normal file
|
|
@ -0,0 +1,262 @@
|
|||
/*
|
||||
* Keyman is copyright (C) SIL International. MIT License.
|
||||
*
|
||||
* Created by Shawn Schantz on 2024-09-09.
|
||||
*
|
||||
* This class is responsible for determining the state of the Keyman input method.
|
||||
* It is called from the KMInputController (a subclass of IMKInputController), and
|
||||
* shares changes in the state of the input method by synchronously posting
|
||||
* notifications to NSNotificationCenter.
|
||||
*/
|
||||
|
||||
/**
|
||||
* This class is needed because there are no available APIs or notifications in macOS that
|
||||
* an input method can use to determine whether it is currently active. An input method runs
|
||||
* in the background as an application but only receives events from text input clients
|
||||
* when it is the active input method.
|
||||
*
|
||||
* It is important for state of the input method to be known so that the On-screen keyboard can
|
||||
* be appropriately shown or hidden and the low-level event tap can be stopped or started.
|
||||
*
|
||||
* The state of the input method is mostly determined by the calls that the OS makes to KMInputController.
|
||||
* initWithServer is called when a new text input client is being used with Keyman
|
||||
* activateServer is called to inform Keyman that the text input client is ready to send events
|
||||
* deactivateServer is called when a client stops being used with Keyman
|
||||
*
|
||||
* Unfortunately, there is no way to distinguish between deactivate messages that result from
|
||||
* 1) changing the text input client which is in use or
|
||||
* 2) changing from Keyman to a different input method such as a system keyboard
|
||||
* Because of this, we must set a timer after a deactivate to see if an activate follows it.
|
||||
* If it does not, then we assume that the user has changed input methods and Keyman is inactive.
|
||||
*
|
||||
* Note that Keyman does not necessarily receive the deactivate before the activate, so
|
||||
* that complicates the handling of these events.
|
||||
*
|
||||
* Additional complexity is encountered when receiving activate messages. After Keyman
|
||||
* is deactivated, it can receive a pair of activate and deactivate messages in rapid succession.
|
||||
* These are unexpected and may be generated by clicking on windows or selecting menus and
|
||||
* may only be a few milliseconds apart. We want to ignore these false activate messages
|
||||
* rather than showing the OSK and enabling the event tap and then immediately hiding and disabling.
|
||||
* The false activate messages are detected by using a timer to ensure that when the
|
||||
* state is Inactive, we only change to Active if we receive an activate message that is not followed
|
||||
* immediately by a deactivate message.
|
||||
*/
|
||||
|
||||
#import "KMInputMethodLifecycle.h"
|
||||
#import "KMLogs.h"
|
||||
#import <AppKit/AppKit.h>
|
||||
#import "KMSettingsRepository.h"
|
||||
|
||||
NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotification";
|
||||
NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification";
|
||||
NSString *const kInputMethodClientChangeNotification = @"kInputMethodClientChangeNotification";
|
||||
|
||||
typedef enum {
|
||||
Initialized,
|
||||
Active,
|
||||
Inactive
|
||||
} LifecycleState;
|
||||
|
||||
@interface KMInputMethodLifecycle()
|
||||
|
||||
@property LifecycleState state;
|
||||
@property NSMutableDictionary *textInputClients;
|
||||
@end
|
||||
|
||||
@implementation KMInputMethodLifecycle
|
||||
const double inactivityTimeout = 0.5;
|
||||
const double minimumActiveDuration = 0.5;
|
||||
|
||||
+ (KMInputMethodLifecycle *)shared {
|
||||
static KMInputMethodLifecycle *shared = nil;
|
||||
static dispatch_once_t onceToken;
|
||||
dispatch_once(&onceToken, ^{
|
||||
shared = [[KMInputMethodLifecycle alloc] init];
|
||||
});
|
||||
|
||||
return shared;
|
||||
}
|
||||
|
||||
- (instancetype)init {
|
||||
self = [super init];
|
||||
if (self) {
|
||||
_state = Initialized;
|
||||
_textInputClients = [[NSMutableDictionary alloc] initWithCapacity:2];
|
||||
}
|
||||
return self;
|
||||
}
|
||||
|
||||
/**
|
||||
* called from Application Delgate during init
|
||||
*/
|
||||
- (void)startLifecycle {
|
||||
_state = Initialized;
|
||||
}
|
||||
|
||||
/**
|
||||
* called when IMKInputController receives an activateServer message
|
||||
*/
|
||||
- (void)activateClient:(id)client {
|
||||
os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle activateClient, client: %{public}@", client);
|
||||
@synchronized(_textInputClients) {
|
||||
[self addActiveClient:client];
|
||||
}
|
||||
|
||||
if (self.state == Initialized) {
|
||||
/**
|
||||
* If the input method is only Initialized, then change clients and activate immediately.
|
||||
* No timer delay is needed because we can assume that we just started Keyman.
|
||||
*/
|
||||
[self changeClient];
|
||||
[self activateInputMethod];
|
||||
} else if (self.state == Inactive) {
|
||||
/**
|
||||
* Before activating the input method, wait and see if we receive an immediate deactivation.
|
||||
* If not, then it is safe to assume that we should change the state to Active.
|
||||
*/
|
||||
os_log_debug([KMLogs lifecycleLog], "before activating the inactive input method, wait for %1.1f seconds and call activateInputMethodIfNoDeactivation", minimumActiveDuration);
|
||||
[self performSelector:@selector(activateInputMethodIfNoDeactivation:) withObject:nil afterDelay:minimumActiveDuration];
|
||||
} else if (self.state == Active) {
|
||||
/**
|
||||
* If the input method was already active, then just send a notification to change clients
|
||||
*/
|
||||
[self changeClient];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* called when IMKInputController receives a deactivateServer message
|
||||
*/
|
||||
- (void)deactivateClient:(id)client {
|
||||
@synchronized(_textInputClients) {
|
||||
os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle deactivateClient, client: %{public}@", client);
|
||||
[self removeActiveClient: client];
|
||||
|
||||
/**
|
||||
* If the input method is currently active, and we removed the last client, wait and see if another client is activated.
|
||||
* If not, then it is time to deactivate the input method.
|
||||
*
|
||||
* No need to do this if we are not in Active state, as this may be a spurious deactivate call that just followed a spurious
|
||||
* activate call. If that's the case, then it is likely happening during the delay before activateInputMethodIfNoDeactivation.
|
||||
*/
|
||||
|
||||
if ((self.state == Active) && (self.textInputClients.count == 0)) {
|
||||
os_log_debug([KMLogs lifecycleLog], "no text input clients found in textInputClients map; delay for %1.1f seconds and call deactivateInputMethodIfNoClients", inactivityTimeout);
|
||||
[self performSelector:@selector(deactivateInputMethodIfNoClients:) withObject:nil afterDelay:inactivityTimeout];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed after a delay: if no deactivation was received, then the input method is now Active
|
||||
*/
|
||||
- (void)activateInputMethodIfNoDeactivation:(id)lastClient {
|
||||
@synchronized(_textInputClients) {
|
||||
if (self.state == Inactive) {
|
||||
if (self.textInputClients.count > 0) {
|
||||
[self logClients:@"activateInputMethodIfNoDeactivation found an active client, time to activate the input method"];
|
||||
[self activateInputMethod];
|
||||
} else {
|
||||
os_log_debug([KMLogs lifecycleLog], "activateInputMethodIfNoDeactivation found that the active client was removed, do not activate input method");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add to the list of active clients. There are usually zero or one clients in the list, but could
|
||||
* bet two when we are late receiving a deactivate message.
|
||||
*/
|
||||
- (void)addActiveClient:(id)client {
|
||||
NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication];
|
||||
NSString *clientAppId = [currentApp bundleIdentifier];
|
||||
NSString *keyString = [self keyForClient:client];
|
||||
os_log_debug([KMLogs lifecycleLog], " ++adding client application '%{public}@' to textInputClients map, derived key: %{public}@", clientAppId, keyString);
|
||||
[self.textInputClients setObject:clientAppId forKey:keyString];
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a key string from the hash of the client object to be used for storing in the textInputClients map
|
||||
*/
|
||||
- (NSString*) keyForClient:(id)client {
|
||||
NSUInteger key = ((NSObject*)client).hash;
|
||||
NSString *keyString = [@(key) stringValue];
|
||||
return keyString;
|
||||
}
|
||||
|
||||
/**
|
||||
* Executed after a delay: if there are still no clients, then deactivate the input method
|
||||
*/
|
||||
- (void)deactivateInputMethodIfNoClients:(id)lastClient {
|
||||
@synchronized(_textInputClients) {
|
||||
if (self.textInputClients.count == 0) {
|
||||
if (self.state == Active) {
|
||||
os_log_debug([KMLogs lifecycleLog], "deactivateInputMethodIfNoClients found no clients, time to deactivate");
|
||||
[self deactivateInputMethod];
|
||||
}
|
||||
} else {
|
||||
[self logClients:@"deactivateInputMethodIfNoClients found a newly activated client, "];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove from the list of active clients.
|
||||
*/
|
||||
- (void)removeActiveClient:(id)client {
|
||||
NSString *keyString = [self keyForClient:client];
|
||||
NSString *clientAppId = [self.textInputClients objectForKey:keyString];
|
||||
if (clientAppId) {
|
||||
os_log_debug([KMLogs lifecycleLog], " --removing client application '%{public}@' from textInputClients map, key: %{public}@", clientAppId, keyString);
|
||||
[self.textInputClients removeObjectForKey:keyString];
|
||||
} else {
|
||||
os_log_debug([KMLogs lifecycleLog], " key %{public}@ not found in textInputClients map", keyString);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write the client list to the log.
|
||||
*/
|
||||
- (void) logClients: (NSString*)message {
|
||||
os_log_debug([KMLogs lifecycleLog], "%{public}@ textInputClients map: %{public}@", message, self.textInputClients.description);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change state to Active and send notification.
|
||||
*/
|
||||
- (void)activateInputMethod {
|
||||
_state = Active;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodActivatedNotification object:self];
|
||||
}
|
||||
|
||||
/**
|
||||
* Change state to Inactive and send notification.
|
||||
*/
|
||||
- (void)deactivateInputMethod {
|
||||
_state = Inactive;
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self];
|
||||
}
|
||||
|
||||
/**
|
||||
* does not change state, just fires notification so that InputController knows to change the event handler
|
||||
*/
|
||||
- (void)changeClient {
|
||||
[[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodClientChangeNotification object:self];
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if Initialized or Active
|
||||
*/
|
||||
- (BOOL)shouldEnableEventTap {
|
||||
return ((self.state == Initialized) || (self.state == Active));
|
||||
}
|
||||
|
||||
/**
|
||||
* returns true if state is Initialized or Active and the Settings require us to show the OSK
|
||||
*/
|
||||
- (BOOL)shouldShowOskOnActivate {
|
||||
return [KMSettingsRepository.shared readShowOskOnActivate]
|
||||
&& (self.state == Active);
|
||||
}
|
||||
|
||||
@end
|
||||
|
|
@ -8,6 +8,7 @@
|
|||
|
||||
#import "OSKWindowController.h"
|
||||
#import "KMInputMethodAppDelegate.h"
|
||||
#import "KMInputMethodLifecycle.h"
|
||||
#import "KMSettingsRepository.h"
|
||||
#import "KMLogs.h"
|
||||
|
||||
|
|
@ -26,7 +27,6 @@
|
|||
}
|
||||
|
||||
- (void)dealloc {
|
||||
//[[NSNotificationCenter defaultCenter] removeObserver:self];
|
||||
[self stopTimer];
|
||||
}
|
||||
|
||||
|
|
@ -56,7 +56,6 @@
|
|||
- (void)windowDidLoad {
|
||||
os_log_debug([KMLogs oskLog], "OSKWindowController windowDidLoad");
|
||||
[super windowDidLoad];
|
||||
//[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidResize:) name:NSWindowDidResizeNotification object:self.window];
|
||||
[self.oskView setKvk:[self.AppDelegate kvk]];
|
||||
[self startTimerWithTimeInterval:0.1];
|
||||
// Implement this method to handle any initialization after your window controller's window has been loaded from its nib file.
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue