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:
sgschantz 2024-09-10 16:06:33 +07:00
parent 78fd4ee19b
commit b8cbf04ff7
7 changed files with 365 additions and 117 deletions

View file

@ -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 */,

View file

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

View file

@ -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_;

View file

@ -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];

View 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

View 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

View file

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