diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 08e00b6c07..57ce828e4a 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj +++ b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj @@ -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 = ""; }; 293EA3F02714158600545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/MainMenu.strings; sourceTree = ""; }; 293EA3F527181FDA00545EED /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = ""; }; + 296105212C8E91C7007BF6B7 /* KMInputMethodLifecycle.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMInputMethodLifecycle.h; sourceTree = ""; }; + 296105222C8E91C7007BF6B7 /* KMInputMethodLifecycle.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMInputMethodLifecycle.m; sourceTree = ""; }; 296FE2FA275DD21600F46898 /* KMPackageReader.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KMPackageReader.h; sourceTree = ""; }; 296FE2FB275DD21600F46898 /* KMPackageReader.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = KMPackageReader.m; sourceTree = ""; }; 29781101297FB262007C886D /* kn */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = kn; path = kn.lproj/KMAboutWindowController.strings; sourceTree = ""; }; @@ -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 */, diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 252dcab21f..92495fd00e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -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 { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index aed9be9375..7f6b2f3655 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -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_; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index ca3387a9ca..7392c3dc2f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -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]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h new file mode 100644 index 0000000000..c8a06ddba9 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h @@ -0,0 +1,26 @@ +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Shawn Schantz on 2024-09-09. + * + */ + +#import + +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 diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m new file mode 100644 index 0000000000..3c07c6e2cd --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -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 +#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 diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m index 3dad2baf63..33d7eda406 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m @@ -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.