From 18e7a1e363a0fe6692a6e9880f4ee11ce05b168f Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Tue, 3 Sep 2024 20:24:08 +0700 Subject: [PATCH 1/9] change(mac): remove Always show OSK option add simple flag to indicate whether OSK is shown --- .../Base.lproj/preferences.xib | 34 ++++++------------- .../KMConfigurationWindowController.m | 7 ---- .../Keyman4MacIM/KMInputController.m | 3 +- .../Keyman4MacIM/KMInputMethodAppDelegate.h | 1 - .../Keyman4MacIM/KMInputMethodAppDelegate.m | 23 +++++-------- .../Keyman4MacIM/KMSettingsRepository.h | 4 +-- .../Keyman4MacIM/KMSettingsRepository.m | 15 +++++--- 7 files changed, 34 insertions(+), 53 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib index c327ce4ad2..d5afcd00d5 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib @@ -1,15 +1,14 @@ - + - - + + - @@ -79,7 +78,7 @@ - + - - - + - + YnBsaXN0MDDUAQIDBAUGBwpYJHZlcnNpb25ZJGFyY2hpdmVyVCR0b3BYJG9iamVjdHMSAAGGoF8QD05T diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m index c99d632665..f3a0c3501e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/KMConfigurationWindowController.m @@ -14,7 +14,6 @@ @interface KMConfigurationWindowController () @property (nonatomic, weak) IBOutlet NSTableView *tableView; @property (nonatomic, weak) IBOutlet WebView *webView; -@property (nonatomic, weak) IBOutlet NSButton *alwaysShowOSKCheckBox; @property (nonatomic, weak) IBOutlet NSButton *useVerboseLoggingCheckBox; @property (nonatomic, weak) IBOutlet NSTextField *verboseLoggingInfo; @property (nonatomic, weak) IBOutlet NSButton *supportBack; @@ -70,7 +69,6 @@ NSURL *homeUrl = [[NSBundle mainBundle] URLForResource:@"index" withExtension:@"html" subdirectory:@"Help"]; [self.webView.mainFrame loadRequest:[NSURLRequest requestWithURL:homeUrl]]; - [self.alwaysShowOSKCheckBox setState:(self.AppDelegate.alwaysShowOSK ? NSOnState : NSOffState)]; [self.useVerboseLoggingCheckBox setState:(self.AppDelegate.useVerboseLogging ? NSOnState : NSOffState)]; } @@ -439,11 +437,6 @@ [self.AppDelegate.downloadKBWindow.window makeKeyAndOrderFront:nil]; } -- (IBAction)alwaysShowOSKCheckBoxAction:(id)sender { - NSButton *checkBox = (NSButton *)sender; - [self.AppDelegate setAlwaysShowOSK:(checkBox.state == NSOnState)]; -} - - (IBAction)useVerboseLoggingCheckBoxAction:(id)sender { NSButton *checkBox = (NSButton *)sender; BOOL verboseLoggingOn = checkBox.state == NSOnState; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index f832abf27f..2132a0da89 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -10,6 +10,7 @@ #import "KMInputMethodEventHandler.h" #import "KMOSVersion.h" #include /* For kVK_ constants. */ +#import "KMSettingsRepository.h" #import "KMLogs.h" @implementation KMInputController @@ -29,7 +30,7 @@ NSMutableArray *servers; if (self) { servers = [[NSMutableArray alloc] initWithCapacity:2]; self.AppDelegate.inputController = self; - if (self.AppDelegate.kvk != nil && self.AppDelegate.alwaysShowOSK) { + if ((self.AppDelegate.kvk != nil) && ([KMSettingsRepository.shared readShowOsk])) { [self.AppDelegate showOSK]; } } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index ce70724c3a..ec07f142cb 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -90,7 +90,6 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; @property (nonatomic, strong) NSString *downloadFilename; @property (nonatomic, strong) NSMutableData *receivedData; @property (nonatomic, assign) NSUInteger expectedBytes; -@property (nonatomic, assign) BOOL alwaysShowOSK; @property (nonatomic, assign) BOOL useVerboseLogging; @property (nonatomic, assign) BOOL useNullChar; @property (nonatomic, assign) BOOL debugMode; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 78c6362de9..a064ab06cb 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -58,7 +58,6 @@ NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboard @synthesize selectedKeyboard = _selectedKeyboard; @synthesize activeKeyboards = _activeKeyboards; @synthesize contextBuffer = _contextBuffer; -@synthesize alwaysShowOSK = _alwaysShowOSK; id _lastServerWithOSKShowing = nil; @@ -245,7 +244,7 @@ id _lastServerWithOSKShowing = nil; CGEventTapEnable(self.lowLevelEventTap, YES); } // See note in sleepFollowingDeactivationOfServer. - if (_kvk != nil && (_alwaysShowOSK || _lastServerWithOSKShowing == newServer)) { + if (_kvk != nil && (_lastServerWithOSKShowing == newServer) && (self.showOskOnActivation)) { [self showOSK]; } @@ -433,11 +432,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef return [NSString stringWithFormat:@"%@ - Keyman", _keyboardName]; } -- (void)setAlwaysShowOSK:(BOOL)alwaysShowOSK { - _alwaysShowOSK = alwaysShowOSK; - [[KMSettingsRepository shared] writeAlwaysShowOsk:alwaysShowOSK]; -} - - (void)setUseVerboseLogging:(BOOL)useVerboseLogging { os_log_debug([KMLogs configLog], "Turning verbose logging %{public}@", useVerboseLogging ? @"on." : @"off."); _debugMode = useVerboseLogging; @@ -447,11 +441,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [[KMSettingsRepository shared] writeUseVerboseLogging:useVerboseLogging]; } -- (BOOL)alwaysShowOSK { - _alwaysShowOSK = [[KMSettingsRepository shared] readAlwaysShowOsk]; - return _alwaysShowOSK; -} - - (BOOL)useVerboseLogging { return [[KMSettingsRepository shared] readUseVerboseLogging]; } @@ -887,8 +876,6 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef [self setContextBuffer:nil]; [self setSelectedKeyboard:path]; [self applyPersistedOptions]; - if (kvk != nil && self.alwaysShowOSK) - [self showOSK]; } - (NSArray *)getKmxFilesInKeyboardsDirectory { @@ -955,6 +942,14 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef _configWindow = window; } +- (BOOL)showOskOnActivation { + return [KMSettingsRepository.shared readShowOsk]; +} + +- (void)saveShowOskOnActivation: showOsk { + [KMSettingsRepository.shared writeShowOsk:showOsk]; +} + - (void)showOSK { [[self.oskWindow window] makeKeyAndOrderFront:nil]; [[self.oskWindow window] setLevel:NSStatusWindowLevel]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h index 8d7b114f2b..0b6fe3afc5 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h @@ -24,8 +24,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearActiveKeyboards; - (NSDictionary *)readOptionsForSelectedKeyboard; - (void)writeOptionForSelectedKeyboard:(NSString *)key withValue:(NSString*)value; -- (BOOL)readAlwaysShowOsk; -- (void)writeAlwaysShowOsk:(BOOL)alwaysShowOsk; +- (BOOL)readShowOsk; +- (void)writeShowOsk:(BOOL)show; - (BOOL)readUseVerboseLogging; - (void)writeUseVerboseLogging:(BOOL)verboseLogging; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m index bbd87a40ec..f01aed4baf 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m @@ -14,7 +14,7 @@ NSString *const kActiveKeyboardsKey = @"KMActiveKeyboardsKey"; NSString *const kSelectedKeyboardKey = @"KMSelectedKeyboardKey"; NSString *const kPersistedOptionsKey = @"KMPersistedOptionsKey"; -NSString *const kAlwaysShowOSKKey = @"KMAlwaysShowOSKKey"; +NSString *const kShowOsk = @"KMShowOsk"; NSString *const kUseVerboseLogging = @"KMUseVerboseLogging"; /** @@ -25,6 +25,11 @@ NSString *const kUseVerboseLogging = @"KMUseVerboseLogging"; * represents what it is saving. */ NSString *const kKMDeprecatedPersistedOptionsKey = @"KMSavedStoresKey"; +/** + * The following constant "KMAlwaysShowOSKKey" is left here for documentation + * but the related UI has been removed according to issue #12342 + */ +NSString *const kAlwaysShowOSKKey = @"KMAlwaysShowOSKKey"; NSString *const kObsoletePathComponent = @"/Documents/Keyman-Keyboards"; NSString *const kNewPathComponent = @"/Library/Application Support/keyman.inputmethod.Keyman/"; @@ -306,14 +311,14 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec } } -- (BOOL)readAlwaysShowOsk { +- (BOOL)readShowOsk { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData boolForKey:kAlwaysShowOSKKey]; + return [userData boolForKey:kShowOsk]; } -- (void)writeAlwaysShowOsk:(BOOL)alwaysShowOsk { +- (void)writeShowOsk:(BOOL)show { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setBool:alwaysShowOsk forKey:kAlwaysShowOSKKey]; + [userData setBool:show forKey:kShowOsk]; } - (BOOL)readUseVerboseLogging { From a032f802309406eb0166d0bdb61340fefacec34b Mon Sep 17 00:00:00 2001 From: sgschantz Date: Wed, 4 Sep 2024 16:04:59 +0700 Subject: [PATCH 2/9] change(mac): read and write new osk window state persists state whenever user opens or closes OSK also records location and size Fixes: #12342 --- mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m | 9 +++++++-- .../Keyman4MacIM/KMInputMethodAppDelegate.m | 8 ++++++-- mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h | 4 ++-- mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m | 10 +++++----- .../OnScreenKeyboard/OSKWindowController.h | 2 +- .../OnScreenKeyboard/OSKWindowController.m | 10 ++++++++-- .../OnScreenKeyboard/OSKWindowController.xib | 4 ++-- 7 files changed, 31 insertions(+), 16 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 2132a0da89..07bbd6691a 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -30,7 +30,8 @@ NSMutableArray *servers; if (self) { servers = [[NSMutableArray alloc] initWithCapacity:2]; self.AppDelegate.inputController = self; - if ((self.AppDelegate.kvk != nil) && ([KMSettingsRepository.shared readShowOsk])) { + if ((self.AppDelegate.kvk != nil) && ([KMSettingsRepository.shared readShowOskOnActivate])) { + os_log_debug([KMLogs oskLog], "initWithServer, readShowOskOnActivate= YES, showing OSK"); [self.AppDelegate showOSK]; } } @@ -66,7 +67,8 @@ NSMutableArray *servers; [self.AppDelegate wakeUpWith:sender]; [servers addObject:sender]; - + os_log_debug([KMLogs lifecycleLog], "activateServer, adding sender to servers array, sender: %{public}@", sender); + if (_eventHandler != nil) { [_eventHandler deactivate]; } @@ -87,6 +89,7 @@ NSMutableArray *servers; @synchronized(servers) { for (int i = 0; i < servers.count; i++) { if (servers[i] == sender) { + os_log_debug([KMLogs lifecycleLog], "deactivateServer, removing sender from servers array, sender: %{public}@", sender); [servers removeObjectAtIndex:i]; break; } @@ -166,6 +169,8 @@ NSMutableArray *servers; [self showConfigurationWindow:sender]; } else if (itag == OSK_MENUITEM_TAG) { + [KMSettingsRepository.shared writeShowOskOnActivate:YES]; + os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, updating settings writeShowOsk to YES"); [self.AppDelegate showOSK]; } else if (itag == ABOUT_MENUITEM_TAG) { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index a064ab06cb..6a36a1da09 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -244,7 +244,9 @@ id _lastServerWithOSKShowing = nil; CGEventTapEnable(self.lowLevelEventTap, YES); } // See note in sleepFollowingDeactivationOfServer. - if (_kvk != nil && (_lastServerWithOSKShowing == newServer) && (self.showOskOnActivation)) { + if (_kvk != nil && (_lastServerWithOSKShowing == newServer) && + ([KMSettingsRepository.shared readShowOskOnActivate])) { + os_log_debug([KMLogs oskLog], "wakeUpWith, readShowOskOnActivate= YES, showing OSK"); [self showOSK]; } @@ -942,13 +944,15 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef _configWindow = window; } +/* - (BOOL)showOskOnActivation { return [KMSettingsRepository.shared readShowOsk]; } -- (void)saveShowOskOnActivation: showOsk { +- (void)saveShowOskOnActivation: (BOOL)showOsk { [KMSettingsRepository.shared writeShowOsk:showOsk]; } +*/ - (void)showOSK { [[self.oskWindow window] makeKeyAndOrderFront:nil]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h index 0b6fe3afc5..7fdf3de034 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h @@ -24,8 +24,8 @@ NS_ASSUME_NONNULL_BEGIN - (void)clearActiveKeyboards; - (NSDictionary *)readOptionsForSelectedKeyboard; - (void)writeOptionForSelectedKeyboard:(NSString *)key withValue:(NSString*)value; -- (BOOL)readShowOsk; -- (void)writeShowOsk:(BOOL)show; +- (BOOL)readShowOskOnActivate; +- (void)writeShowOskOnActivate:(BOOL)show; - (BOOL)readUseVerboseLogging; - (void)writeUseVerboseLogging:(BOOL)verboseLogging; @end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m index f01aed4baf..db59929eae 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m @@ -14,7 +14,7 @@ NSString *const kActiveKeyboardsKey = @"KMActiveKeyboardsKey"; NSString *const kSelectedKeyboardKey = @"KMSelectedKeyboardKey"; NSString *const kPersistedOptionsKey = @"KMPersistedOptionsKey"; -NSString *const kShowOsk = @"KMShowOsk"; +NSString *const kShowOskOnActivate = @"KMShowOskOnActivate"; NSString *const kUseVerboseLogging = @"KMUseVerboseLogging"; /** @@ -311,14 +311,14 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec } } -- (BOOL)readShowOsk { +- (BOOL)readShowOskOnActivate { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData boolForKey:kShowOsk]; + return [userData boolForKey:kShowOskOnActivate]; } -- (void)writeShowOsk:(BOOL)show { +- (void)writeShowOskOnActivate:(BOOL)show { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setBool:show forKey:kShowOsk]; + [userData setBool:show forKey:kShowOskOnActivate]; } - (BOOL)readUseVerboseLogging { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h index 4b98f633b8..fd3a953a85 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h @@ -9,7 +9,7 @@ #import #import -@interface OSKWindowController : NSWindowController +@interface OSKWindowController : NSWindowController @property (nonatomic, weak) IBOutlet OSKView *oskView; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m index 30d30004c4..3dad2baf63 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 "KMSettingsRepository.h" #import "KMLogs.h" @interface OSKWindowController () @@ -25,7 +26,7 @@ } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; + //[[NSNotificationCenter defaultCenter] removeObserver:self]; [self stopTimer]; } @@ -55,7 +56,7 @@ - (void)windowDidLoad { os_log_debug([KMLogs oskLog], "OSKWindowController windowDidLoad"); [super windowDidLoad]; - [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(windowDidResize:) name:NSWindowDidResizeNotification object:self.window]; + //[[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. @@ -66,6 +67,11 @@ [self.oskView resizeOSKLayout]; } +- (void)windowWillClose:(NSNotification *)notification { + [KMSettingsRepository.shared writeShowOskOnActivate:NO]; + os_log_debug([KMLogs oskLog], "OSKWindowController windowWillClose, updating settings writeShowOsk to NO"); +} + - (void)helpAction:(id)sender { NSString *kvkPath = [self AppDelegate].kvk.filePath; if (!kvkPath) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib index 753a59365a..b753e43802 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib @@ -2,7 +2,7 @@ - + @@ -14,7 +14,7 @@ - + From 78fd4ee19b12f3329f0f5dffae08ae6209ca5ed8 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Fri, 6 Sep 2024 16:41:07 +0700 Subject: [PATCH 3/9] change(mac): renaming, use client map instead of servers array --- .../Keyman4MacIM/KMInputController.m | 141 ++++++++---------- .../Keyman4MacIM/KMInputMethodAppDelegate.h | 2 +- .../Keyman4MacIM/KMInputMethodAppDelegate.m | 5 +- 3 files changed, 63 insertions(+), 85 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 07bbd6691a..252dcab21f 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -12,27 +12,32 @@ #include /* For kVK_ constants. */ #import "KMSettingsRepository.h" #import "KMLogs.h" +#import "InputMethodKit/InputMethodKit.h" + @implementation KMInputController +const double inactivityTimeout = 0.7; KMInputMethodEventHandler* _eventHandler; -NSMutableArray *servers; +NSMutableDictionary *textInputClients; -- (KMInputMethodAppDelegate *)AppDelegate { +- (KMInputMethodAppDelegate *)appDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; } - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { - os_log_debug([KMLogs lifecycleLog], "Initializing Keyman Input Method for server with bundleID: %{public}@", server.bundle.bundleIdentifier); - + NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *clientAppId = [currApp bundleIdentifier]; + os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", clientAppId); + self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { - servers = [[NSMutableArray 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]; + 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]; } } @@ -62,103 +67,75 @@ NSMutableArray *servers; } - (void)activateServer:(id)sender { - @synchronized(servers) { + @synchronized(textInputClients) { + os_log_debug([KMLogs lifecycleLog], "KMInputController activateServer, sender %{public}@", sender); [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; - - [self.AppDelegate wakeUpWith:sender]; - [servers addObject:sender]; - os_log_debug([KMLogs lifecycleLog], "activateServer, adding sender to servers array, sender: %{public}@", sender); + 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]; } - NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *clientAppId = [currApp bundleIdentifier]; - os_log_debug([KMLogs lifecycleLog], "activateServer, new active app: '%{public}@'", clientAppId); - _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:clientAppId client:sender]; - } } - (void)deactivateServer:(id)sender { - if ([self.AppDelegate debugMode]) { - os_log_debug([KMLogs lifecycleLog], "deactivateServer, sender %{public}@", sender); - } - @synchronized(servers) { - for (int i = 0; i < servers.count; i++) { - if (servers[i] == sender) { - os_log_debug([KMLogs lifecycleLog], "deactivateServer, removing sender from servers array, sender: %{public}@", sender); - [servers removeObjectAtIndex:i]; - break; - } + 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); } - if (servers.count == 0) { - os_log_debug([KMLogs lifecycleLog], "No known active server for Keyman IM. Starting countdown to sleep..."); - [self performSelector:@selector(timerAction:) withObject:sender afterDelay:0.7]; + 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)timerAction:(id)lastServer { - @synchronized(servers) { - if (servers.count == 0) { +- (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 sleepFollowingDeactivationOfServer:lastServer]; + [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); } } } - -/* - - (NSDictionary *)modes:(id)sender { - if ([self.AppDelegate debugMode]) - os_log_debug([KMLogs lifecycleLog], "*** Modes ***"); - if (_kmModes == nil) { - NSDictionary *amhMode = [[NSDictionary alloc] initWithObjectsAndKeys:@"keyman.png", kTSInputModeAlternateMenuIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - [NSNumber numberWithBool:YES], kTSInputModeIsVisibleKey, - @"A", kTSInputModeKeyEquivalentKey, - [NSNumber numberWithInteger:4608], kTSInputModeKeyEquivalentModifiersKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - @"keyman.png", kTSInputModeMenuIconFileKey, - @"keyman.png", kTSInputModePaletteIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModePrimaryInScriptKey, - @"smUnicodeScript", kTSInputModeScriptKey, - @"amh", @"TISIntendedLanguage", nil]; - - NSDictionary *hinMode = [[NSDictionary alloc] initWithObjectsAndKeys:@"keyman.png", kTSInputModeAlternateMenuIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - [NSNumber numberWithBool:YES], kTSInputModeIsVisibleKey, - @"H", kTSInputModeKeyEquivalentKey, - [NSNumber numberWithInteger:4608], kTSInputModeKeyEquivalentModifiersKey, - [NSNumber numberWithBool:YES], kTSInputModeDefaultStateKey, - @"keyman.png", kTSInputModeMenuIconFileKey, - @"keyman.png", kTSInputModePaletteIconFileKey, - [NSNumber numberWithBool:YES], kTSInputModePrimaryInScriptKey, - @"smUnicodeScript", kTSInputModeScriptKey, - @"hin", @"TISIntendedLanguage", nil]; - - NSDictionary *modeList = [[NSDictionary alloc] initWithObjectsAndKeys:amhMode, @"com.apple.inputmethod.amh", hinMode, @"com.apple.inputmethod.hin", nil]; - NSArray *modeOrder = [[NSArray alloc] initWithObjects:@"com.apple.inputmethod.amh", @"com.apple.inputmethod.hin", nil]; - _kmModes = [[NSDictionary alloc] initWithObjectsAndKeys:modeList, kTSInputModeListKey, - modeOrder, kTSVisibleInputModeOrderedArrayKey, nil]; - } - - return _kmModes; - } - */ - - (NSMenu *)menu { - return self.AppDelegate.menu; + return self.appDelegate.menu; } - (KMXFile *)kmx { - return self.AppDelegate.kmx; + return self.appDelegate.kmx; } - (void)menuAction:(id)sender { @@ -171,13 +148,13 @@ NSMutableArray *servers; else if (itag == OSK_MENUITEM_TAG) { [KMSettingsRepository.shared writeShowOskOnActivate:YES]; os_log_debug([KMLogs oskLog], "menuAction OSK_MENUITEM_TAG, updating settings writeShowOsk to YES"); - [self.AppDelegate showOSK]; + [self.appDelegate showOSK]; } else if (itag == ABOUT_MENUITEM_TAG) { - [self.AppDelegate showAboutWindow]; + [self.appDelegate showAboutWindow]; } else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { - [self.AppDelegate selectKeyboardFromMenu:itag]; + [self.appDelegate selectKeyboardFromMenu:itag]; } } @@ -189,7 +166,7 @@ NSMutableArray *servers; if ([KMOSVersion Version_10_13_1] <= systemVersion && systemVersion <= [KMOSVersion Version_10_13_3]) // between 10.13.1 and 10.13.3 inclusive { os_log_info([KMLogs uiLog], "Input Menu: calling workaround instead of showPreferences (sys ver %x)", systemVersion); - [self.AppDelegate showConfigurationWindow]; // call our workaround + [self.appDelegate showConfigurationWindow]; // call our workaround } else { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h index ec07f142cb..aed9be9375 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.h @@ -101,7 +101,7 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; - (void)showOSK; - (void)showConfigurationWindow; - (void)selectKeyboardFromMenu:(NSInteger)tag; -- (void)sleepFollowingDeactivationOfServer:(id)lastServer; +- (void)sleepFollowingInactivityTimeout:(id)lastServer; - (void)wakeUpWith:(id)newServer; - (void)handleKeyEvent:(NSEvent *)event; - (BOOL)unzipFile:(NSString *)filePath; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 6a36a1da09..ca3387a9ca 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -221,7 +221,7 @@ id _lastServerWithOSKShowing = nil; return (KMInputMethodAppDelegate *)[NSApp delegate]; } --(void) sleepFollowingDeactivationOfServer:(id)lastServer { +-(void) sleepFollowingInactivityTimeout:(id)lastServer { os_log_debug([KMLogs lifecycleLog], "Keyman no longer active IM."); self.sleeping = YES; if ([self.oskWindow.window isVisible]) { @@ -238,12 +238,13 @@ id _lastServerWithOSKShowing = nil; } -(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 sleepFollowingDeactivationOfServer. + // See note in sleepFollowingInactivityTimeout. if (_kvk != nil && (_lastServerWithOSKShowing == newServer) && ([KMSettingsRepository.shared readShowOskOnActivate])) { os_log_debug([KMLogs oskLog], "wakeUpWith, readShowOskOnActivate= YES, showing OSK"); From b8cbf04ff7a030d4ca8c73af20854b2d6552578e Mon Sep 17 00:00:00 2001 From: sgschantz Date: Tue, 10 Sep 2024 16:06:33 +0700 Subject: [PATCH 4/9] change(mac): added KMInputMethodLifecycle clearly manages the state of the input method and notifies any observers of changes Fixes: #12342 --- .../Keyman4MacIM.xcodeproj/project.pbxproj | 6 + .../Keyman4MacIM/KMInputController.m | 92 ++---- .../Keyman4MacIM/KMInputMethodAppDelegate.h | 3 - .../Keyman4MacIM/KMInputMethodAppDelegate.m | 90 +++--- .../Keyman4MacIM/KMInputMethodLifecycle.h | 26 ++ .../Keyman4MacIM/KMInputMethodLifecycle.m | 262 ++++++++++++++++++ .../OnScreenKeyboard/OSKWindowController.m | 3 +- 7 files changed, 365 insertions(+), 117 deletions(-) create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h create mode 100644 mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m 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. From b3651f0dfa832e973a4616521b589b124d6ff6bb Mon Sep 17 00:00:00 2001 From: sgschantz Date: Wed, 11 Sep 2024 11:03:47 +0700 Subject: [PATCH 5/9] change(mac): renamed property and cleaned up comments Fixes: #12342 --- .../Keyman4MacIM/KMInputMethodLifecycle.m | 72 ++++++++++--------- 1 file changed, 39 insertions(+), 33 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m index 3c07c6e2cd..0c7cfa4005 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -15,6 +15,10 @@ * in the background as an application but only receives events from text input clients * when it is the active input method. * + * Without any simple way to determine whether the input method is active, it is considered + * to be in an Active state if it has an active text input client that does not immediately get + * a deactivate method after activating. + * * 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. * @@ -32,8 +36,8 @@ * 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. + * Additional complexity is encountered when receiving activate messages. After the Keyman input method + * becomes inactive, 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. @@ -52,7 +56,7 @@ NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedN NSString *const kInputMethodClientChangeNotification = @"kInputMethodClientChangeNotification"; typedef enum { - Initialized, + Started, Active, Inactive } LifecycleState; @@ -60,7 +64,7 @@ typedef enum { @interface KMInputMethodLifecycle() @property LifecycleState state; -@property NSMutableDictionary *textInputClients; +@property NSMutableDictionary *activeTextInputClients; @end @implementation KMInputMethodLifecycle @@ -80,8 +84,8 @@ const double minimumActiveDuration = 0.5; - (instancetype)init { self = [super init]; if (self) { - _state = Initialized; - _textInputClients = [[NSMutableDictionary alloc] initWithCapacity:2]; + _state = Started; + _activeTextInputClients = [[NSMutableDictionary alloc] initWithCapacity:2]; } return self; } @@ -90,7 +94,7 @@ const double minimumActiveDuration = 0.5; * called from Application Delgate during init */ - (void)startLifecycle { - _state = Initialized; + _state = Started; } /** @@ -98,20 +102,21 @@ const double minimumActiveDuration = 0.5; */ - (void)activateClient:(id)client { os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle activateClient, client: %{public}@", client); - @synchronized(_textInputClients) { + @synchronized(_activeTextInputClients) { [self addActiveClient:client]; } - if (self.state == Initialized) { + if (self.state == Started) { /** - * 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. + * If state is Started, then change clients and activate immediately. + * When Keyman is just starting, there does not appear to be any chance of receiving + * a pair of false activate/deactivate messages. */ [self changeClient]; [self activateInputMethod]; } else if (self.state == Inactive) { /** - * Before activating the input method, wait and see if we receive an immediate deactivation. + * Before activating the input method, wait and see if we receive an immediate deactivate call. * 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); @@ -128,20 +133,21 @@ const double minimumActiveDuration = 0.5; * called when IMKInputController receives a deactivateServer message */ - (void)deactivateClient:(id)client { - @synchronized(_textInputClients) { + @synchronized(_activeTextInputClients) { 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 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 the input method is currently Inactive, then we may be here during the delay before invoking + * activateInputMethodIfNoDeactivation. In that case there is nothing special to do here, simply removing the + * client */ - 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); + if ((self.state == Active) && (self.activeTextInputClients.count == 0)) { + os_log_debug([KMLogs lifecycleLog], "no text input clients found in activeTextInputClients map; delay for %1.1f seconds and call deactivateInputMethodIfNoClients", inactivityTimeout); [self performSelector:@selector(deactivateInputMethodIfNoClients:) withObject:nil afterDelay:inactivityTimeout]; } } @@ -151,9 +157,9 @@ const double minimumActiveDuration = 0.5; * Executed after a delay: if no deactivation was received, then the input method is now Active */ - (void)activateInputMethodIfNoDeactivation:(id)lastClient { - @synchronized(_textInputClients) { + @synchronized(_activeTextInputClients) { if (self.state == Inactive) { - if (self.textInputClients.count > 0) { + if (self.activeTextInputClients.count > 0) { [self logClients:@"activateInputMethodIfNoDeactivation found an active client, time to activate the input method"]; [self activateInputMethod]; } else { @@ -171,12 +177,12 @@ const double minimumActiveDuration = 0.5; 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]; + os_log_debug([KMLogs lifecycleLog], " ++adding client application '%{public}@' to activeTextInputClients map, derived key: %{public}@", clientAppId, keyString); + [self.activeTextInputClients setObject:clientAppId forKey:keyString]; } /** - * Create a key string from the hash of the client object to be used for storing in the textInputClients map + * Create a key string from the hash of the client object to be used for storing in the activeTextInputClients map */ - (NSString*) keyForClient:(id)client { NSUInteger key = ((NSObject*)client).hash; @@ -188,8 +194,8 @@ const double minimumActiveDuration = 0.5; * 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) { + @synchronized(_activeTextInputClients) { + if (self.activeTextInputClients.count == 0) { if (self.state == Active) { os_log_debug([KMLogs lifecycleLog], "deactivateInputMethodIfNoClients found no clients, time to deactivate"); [self deactivateInputMethod]; @@ -205,12 +211,12 @@ const double minimumActiveDuration = 0.5; */ - (void)removeActiveClient:(id)client { NSString *keyString = [self keyForClient:client]; - NSString *clientAppId = [self.textInputClients objectForKey:keyString]; + NSString *clientAppId = [self.activeTextInputClients objectForKey:keyString]; if (clientAppId) { - os_log_debug([KMLogs lifecycleLog], " --removing client application '%{public}@' from textInputClients map, key: %{public}@", clientAppId, keyString); - [self.textInputClients removeObjectForKey:keyString]; + os_log_debug([KMLogs lifecycleLog], " --removing client application '%{public}@' from activeTextInputClients map, key: %{public}@", clientAppId, keyString); + [self.activeTextInputClients removeObjectForKey:keyString]; } else { - os_log_debug([KMLogs lifecycleLog], " key %{public}@ not found in textInputClients map", keyString); + os_log_debug([KMLogs lifecycleLog], " key %{public}@ not found in activeTextInputClients map", keyString); } } @@ -218,7 +224,7 @@ const double minimumActiveDuration = 0.5; * Write the client list to the log. */ - (void) logClients: (NSString*)message { - os_log_debug([KMLogs lifecycleLog], "%{public}@ textInputClients map: %{public}@", message, self.textInputClients.description); + os_log_debug([KMLogs lifecycleLog], "%{public}@ activeTextInputClients map: %{public}@", message, self.activeTextInputClients.description); } /** @@ -245,14 +251,14 @@ const double minimumActiveDuration = 0.5; } /** - * returns true if Initialized or Active + * returns true if Started or Active */ - (BOOL)shouldEnableEventTap { - return ((self.state == Initialized) || (self.state == Active)); + return ((self.state == Started) || (self.state == Active)); } /** - * returns true if state is Initialized or Active and the Settings require us to show the OSK + * returns true if state is Active and the Settings require us to show the OSK */ - (BOOL)shouldShowOskOnActivate { return [KMSettingsRepository.shared readShowOskOnActivate] From a99ca4f120cb157f4f6c09302d2a4adf5e3e89a4 Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Thu, 12 Sep 2024 19:57:10 +0700 Subject: [PATCH 6/9] change(mac): overhauled logic for determining input method state uses Carbon APIs to determine current input source This, combined with text input client is used to determine whether Keyman is truly active or not. Removed all code executing on delays and immediately checks state and shows/hides OSK and enables/disables eventTap. --- .../Keyman4MacIM/KMInputController.m | 29 +- .../Keyman4MacIM/KMInputMethodLifecycle.h | 1 + .../Keyman4MacIM/KMInputMethodLifecycle.m | 278 ++++++++---------- 3 files changed, 141 insertions(+), 167 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 92495fd00e..0e227de8bd 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -1,10 +1,9 @@ -// -// KMInputController.m -// Keyman4MacIM -// -// Created by Serkan Kurt on 29/01/2015. -// Copyright (c) 2017 SIL International. All rights reserved. -// +/* + * Keyman is copyright (C) SIL International. MIT License. + * + * Created by Serkan Kurt on 2015-01-29. + * + */ #import "KMInputController.h" #import "KMInputMethodEventHandler.h" @@ -26,9 +25,7 @@ KMInputMethodEventHandler* _eventHandler; - (id)initWithServer:(IMKServer *)server delegate:(id)delegate client:(id)inputClient { - NSRunningApplication *currApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; - NSString *clientAppId = [currApp bundleIdentifier]; - os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", clientAppId); + os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getClientApplicationId]); self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { @@ -64,6 +61,9 @@ KMInputMethodEventHandler* _eventHandler; } } +/** + * The Keyman input method is deactivating because the user chose a different input method: notification from KMInputMethodLifecycle + */ - (void)inputMethodDeactivated:(NSNotification *)notification { os_log_debug([KMLogs lifecycleLog], "***KMInputController inputMethodDeactivated, deactivating eventHandler"); if (_eventHandler != nil) { @@ -71,15 +71,16 @@ KMInputMethodEventHandler* _eventHandler; } } +/** + * The user has switched to a different text input client: notification from KMInputMethodLifecycle + */ - (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]; + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getClientApplicationId] client:self.client]; + } - (void)activateServer:(id)sender { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h index c8a06ddba9..62a3aac35d 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h @@ -15,6 +15,7 @@ extern NSString *const kInputMethodClientChangeNotification; @interface KMInputMethodLifecycle : NSObject + (KMInputMethodLifecycle *)shared; ++ (NSString*)getClientApplicationId; - (void)startLifecycle; - (void)activateClient:(id)client; - (void)deactivateClient:(id)client; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m index 0c7cfa4005..bd5bd84270 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -10,50 +10,37 @@ */ /** - * 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. + * This class is needed because many activateServer and deactivateServer messages sent from macOS + * to KMInputController, but they are not particularly reliable. Keyman receives some messages when it + * is not active and should not become active. It also receives messages when it is active, but there is no + * need to change state. For example, when a menu is clicked with Keyman active, macOS will send a + * deactivateServer message followed by an activateServer message when the menu is released. + * The messages may also arrive in an unexpected order. * - * Without any simple way to determine whether the input method is active, it is considered - * to be in an Active state if it has an active text input client that does not immediately get - * a deactivate method after activating. + * Instead of relying on the information conveyed in these messages, this class interprets them as a notification + * that the input method state may have changed. For the actual state of the input method, it gets the current + * input source using the Carbon APIs TISCopyCurrentKeyboardInputSource and TISGetInputSourceProperty. + * If the result is equal to "keyman.inputmethod.Keyman", then Keyman is the active input method. If, for + * example, the U.S. keyboard were selected, then the result would be "com.apple.keylayout.US". * - * 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 text input client is discovered using the NSRunningApplication frontmostApplication API. + * Knowing the current input method and the current text input client enables us to determine whether the + * state has actually changed and how to adjust to the new state. * - * 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 the Keyman input method - * becomes inactive, 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. + * It is important for state 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. */ #import "KMInputMethodLifecycle.h" #import "KMLogs.h" #import #import "KMSettingsRepository.h" +#import NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotification"; NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification"; NSString *const kInputMethodClientChangeNotification = @"kInputMethodClientChangeNotification"; +NSString *const keymanInputMethodName = @"keyman.inputmethod.Keyman"; typedef enum { Started, @@ -61,15 +48,22 @@ typedef enum { Inactive } LifecycleState; +typedef enum { + None, + Activate, + Deactivate, + ChangeClients, +} TransitionType; + + @interface KMInputMethodLifecycle() -@property LifecycleState state; -@property NSMutableDictionary *activeTextInputClients; +@property LifecycleState lifecycleState; +@property NSString *inputSourceId; +@property NSString *clientApplicationId; @end @implementation KMInputMethodLifecycle -const double inactivityTimeout = 0.5; -const double minimumActiveDuration = 0.5; + (KMInputMethodLifecycle *)shared { static KMInputMethodLifecycle *shared = nil; @@ -84,8 +78,9 @@ const double minimumActiveDuration = 0.5; - (instancetype)init { self = [super init]; if (self) { - _state = Started; - _activeTextInputClients = [[NSMutableDictionary alloc] initWithCapacity:2]; + _lifecycleState = Started; + _inputSourceId = @""; + _clientApplicationId = @""; } return self; } @@ -94,175 +89,152 @@ const double minimumActiveDuration = 0.5; * called from Application Delgate during init */ - (void)startLifecycle { - _state = Started; + _lifecycleState = Started; } /** - * called when IMKInputController receives an activateServer message + * Use Carbon APIs to get the current input source or input method. Even though many Carbon APIs were deprecated and removed + * from the OS years ago, these and other low-level APIs are still supported (but apparently completely undocumented). */ -- (void)activateClient:(id)client { - os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle activateClient, client: %{public}@", client); - @synchronized(_activeTextInputClients) { - [self addActiveClient:client]; - } - - if (self.state == Started) { - /** - * If state is Started, then change clients and activate immediately. - * When Keyman is just starting, there does not appear to be any chance of receiving - * a pair of false activate/deactivate messages. - */ - [self changeClient]; - [self activateInputMethod]; - } else if (self.state == Inactive) { - /** - * Before activating the input method, wait and see if we receive an immediate deactivate call. - * 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]; - } ++ (NSString*)getCurrentInputSourceId { + TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource(); + NSString *inputSourceId = (__bridge NSString *)(TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)); + return inputSourceId; } /** - * called when IMKInputController receives a deactivateServer message + * Get the bundle ID of the currently active text input client.. */ -- (void)deactivateClient:(id)client { - @synchronized(_activeTextInputClients) { - 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. - * - * If the input method is currently Inactive, then we may be here during the delay before invoking - * activateInputMethodIfNoDeactivation. In that case there is nothing special to do here, simply removing the - * client - */ - - if ((self.state == Active) && (self.activeTextInputClients.count == 0)) { - os_log_debug([KMLogs lifecycleLog], "no text input clients found in activeTextInputClients 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(_activeTextInputClients) { - if (self.state == Inactive) { - if (self.activeTextInputClients.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 { ++ (NSString*)getClientApplicationId { NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; NSString *clientAppId = [currentApp bundleIdentifier]; - NSString *keyString = [self keyForClient:client]; - os_log_debug([KMLogs lifecycleLog], " ++adding client application '%{public}@' to activeTextInputClients map, derived key: %{public}@", clientAppId, keyString); - [self.activeTextInputClients setObject:clientAppId forKey:keyString]; + return clientAppId; } /** - * Create a key string from the hash of the client object to be used for storing in the activeTextInputClients map + * Based on the current lifecycleState and the input method state from the OS, determine how the state must transition. */ -- (NSString*) keyForClient:(id)client { - NSUInteger key = ((NSObject*)client).hash; - NSString *keyString = [@(key) stringValue]; - return keyString; -} +- (TransitionType)determineTransition:(NSString*)newInputSourceId withAppId:(NSString*)newClientAppId { + TransitionType transition = None; + BOOL inputSourceIsKeyman = [newInputSourceId isEqualTo:keymanInputMethodName]; + BOOL clientHasChanged = [self.clientApplicationId isNotEqualTo:newClientAppId]; + os_log_debug([KMLogs lifecycleLog], "determineTransition, current InputSourceId: %{public}@, new InputSourceId: %{public}@, current ClientAppId: %{public}@, new ClientAppId: %{public}@, inputSourceIsKeyman: %d, clientHasChanged: %d", self.inputSourceId, newInputSourceId, self.clientApplicationId, newClientAppId, inputSourceIsKeyman, clientHasChanged); -/** - * Executed after a delay: if there are still no clients, then deactivate the input method - */ -- (void)deactivateInputMethodIfNoClients:(id)lastClient { - @synchronized(_activeTextInputClients) { - if (self.activeTextInputClients.count == 0) { - if (self.state == Active) { - os_log_debug([KMLogs lifecycleLog], "deactivateInputMethodIfNoClients found no clients, time to deactivate"); - [self deactivateInputMethod]; + switch (self.lifecycleState) { + case Started: + transition = Activate; + break; + case Active: + if (inputSourceIsKeyman) { + if (clientHasChanged) { + transition = ChangeClients; + } + } else { + transition = Deactivate; } - } else { - [self logClients:@"deactivateInputMethodIfNoClients found a newly activated client, "]; - } + break; + case Inactive: + if (inputSourceIsKeyman) { + transition = Activate; + } + break; + } + return transition; +} + +/** + * Update the input method state, consisting of the input source ID and the client application ID. + */ +- (void)saveNewInputMethodState:(NSString*)newInputSourceId withAppId:(NSString*)newClientAppId { + self.inputSourceId = newInputSourceId; + self.clientApplicationId = newClientAppId; +} + +/** + * Called when IMKInputController receives an activateServer message + */ +- (void)performTransition:(id)client { + NSString *currentInputSource = [KMInputMethodLifecycle getCurrentInputSourceId]; + NSString *currentClientAppId = [KMInputMethodLifecycle getClientApplicationId]; + + TransitionType transition = [self determineTransition:currentInputSource withAppId:currentClientAppId]; + [self saveNewInputMethodState:currentInputSource withAppId:currentClientAppId]; + + switch(transition) { + case None: + os_log_info([KMLogs lifecycleLog], "performTransition: None, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + break; + case Activate: + os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [self changeClient]; + [self activateInputMethod]; + break; + case Deactivate: + os_log_info([KMLogs lifecycleLog], "performTransition: Deactivate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [self deactivateInputMethod]; + break; + case ChangeClients: + os_log_info([KMLogs lifecycleLog], "performTransition: ChangeClients, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + [self changeClient]; + break; } } - + /** - * Remove from the list of active clients. + * Called when IMKInputController receives an activateServer message */ -- (void)removeActiveClient:(id)client { - NSString *keyString = [self keyForClient:client]; - NSString *clientAppId = [self.activeTextInputClients objectForKey:keyString]; - if (clientAppId) { - os_log_debug([KMLogs lifecycleLog], " --removing client application '%{public}@' from activeTextInputClients map, key: %{public}@", clientAppId, keyString); - [self.activeTextInputClients removeObjectForKey:keyString]; - } else { - os_log_debug([KMLogs lifecycleLog], " key %{public}@ not found in activeTextInputClients map", keyString); - } +- (void)activateClient:(id)client { + os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle activateClient"); + [self performTransition:client]; } /** - * Write the client list to the log. + * Called when IMKInputController receives an deactivateServer message */ -- (void) logClients: (NSString*)message { - os_log_debug([KMLogs lifecycleLog], "%{public}@ activeTextInputClients map: %{public}@", message, self.activeTextInputClients.description); +- (void)deactivateClient:(id)client { + os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle deactivateClient"); + [self performTransition:client]; } /** - * Change state to Active and send notification. + * Change lifecycleState to Active and send notification. */ - (void)activateInputMethod { - _state = Active; + os_log_debug([KMLogs lifecycleLog], "activateInputMethod"); + _lifecycleState = Active; [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodActivatedNotification object:self]; } /** - * Change state to Inactive and send notification. + * Change lifecycleState to Inactive and send notification. */ - (void)deactivateInputMethod { - _state = Inactive; + os_log_debug([KMLogs lifecycleLog], "deactivateInputMethod"); + _lifecycleState = Inactive; [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self]; } /** - * does not change state, just fires notification so that InputController knows to change the event handler + * Does not change lifecycleState, just fires notification so that InputController knows to change the event handler */ - (void)changeClient { + os_log_debug([KMLogs lifecycleLog], "changeClient"); [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodClientChangeNotification object:self]; } /** - * returns true if Started or Active + * Returns true if Started or Active */ - (BOOL)shouldEnableEventTap { - return ((self.state == Started) || (self.state == Active)); + return ((self.lifecycleState == Started) || (self.lifecycleState == Active)); } /** - * returns true if state is Active and the Settings require us to show the OSK + * Returns true if lifecycleState is Active and the Settings require us to show the OSK */ - (BOOL)shouldShowOskOnActivate { return [KMSettingsRepository.shared readShowOskOnActivate] - && (self.state == Active); + && (self.lifecycleState == Active); } @end From d664797359c38f737fe4c5351713d40730c22304 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Fri, 13 Sep 2024 13:53:08 +0700 Subject: [PATCH 7/9] change(mac): attempt to fix sequoia failure to deactivate --- .../Keyman4MacIM/KMInputMethodLifecycle.m | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m index bd5bd84270..129765c197 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -41,6 +41,7 @@ NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotif NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification"; NSString *const kInputMethodClientChangeNotification = @"kInputMethodClientChangeNotification"; NSString *const keymanInputMethodName = @"keyman.inputmethod.Keyman"; +const double postDeactivateDelay = 0.5; typedef enum { Started, @@ -151,7 +152,7 @@ typedef enum { } /** - * Called when IMKInputController receives an activateServer message + * Called when IMKInputController receives an activateServer or a deactivateServer message */ - (void)performTransition:(id)client { NSString *currentInputSource = [KMInputMethodLifecycle getCurrentInputSourceId]; @@ -194,6 +195,21 @@ typedef enum { - (void)deactivateClient:(id)client { os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle deactivateClient"); [self performTransition:client]; + + /** + * Called when IMKInputController receives an deactivateServer message + */ + if (@available(macOS 10.14, *)) { + os_log_debug([KMLogs lifecycleLog], "performing additional version check for Sonoma+"); + [self performSelector:@selector(checkForDelayedDeactivation:) withObject:client afterDelay:postDeactivateDelay]; + } + + +} + +- (void)checkForDeactivation:(id)client { + os_log_debug([KMLogs lifecycleLog], "checkForDelayedDeactivation"); + [self performTransition:client]; } /** From 4ec6144c647a360e94e3e1cd90d5043fcc9e750d Mon Sep 17 00:00:00 2001 From: sgschantz Date: Fri, 13 Sep 2024 14:53:16 +0700 Subject: [PATCH 8/9] change(mac): added delays before inputmethod state transition --- .../Keyman4MacIM/KMInputMethodLifecycle.m | 20 ++++++------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m index 129765c197..194ced26e6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -41,7 +41,7 @@ NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotif NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification"; NSString *const kInputMethodClientChangeNotification = @"kInputMethodClientChangeNotification"; NSString *const keymanInputMethodName = @"keyman.inputmethod.Keyman"; -const double postDeactivateDelay = 0.5; +const double transitionDelay = 0.2; typedef enum { Started, @@ -186,7 +186,8 @@ typedef enum { */ - (void)activateClient:(id)client { os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle activateClient"); - [self performTransition:client]; + + [self performSelector:@selector(performTransitionAfterDelay:) withObject:client afterDelay:transitionDelay]; } /** @@ -194,21 +195,12 @@ typedef enum { */ - (void)deactivateClient:(id)client { os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle deactivateClient"); - [self performTransition:client]; - - /** - * Called when IMKInputController receives an deactivateServer message - */ - if (@available(macOS 10.14, *)) { - os_log_debug([KMLogs lifecycleLog], "performing additional version check for Sonoma+"); - [self performSelector:@selector(checkForDelayedDeactivation:) withObject:client afterDelay:postDeactivateDelay]; - } - + [self performSelector:@selector(performTransitionAfterDelay:) withObject:client afterDelay:transitionDelay]; } -- (void)checkForDeactivation:(id)client { - os_log_debug([KMLogs lifecycleLog], "checkForDelayedDeactivation"); +- (void)performTransitionAfterDelay:(id)client { + os_log_debug([KMLogs lifecycleLog], "performTransitionAfterDelay: calling performTransition"); [self performTransition:client]; } From 24830f4bc6f0719a8254969b2dc3e8b44e52a151 Mon Sep 17 00:00:00 2001 From: sgschantz Date: Mon, 16 Sep 2024 10:55:21 +0700 Subject: [PATCH 9/9] change(mac): improved comments and increased delay to 0.25 --- mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m | 7 ++++++- mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m | 7 ++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m index 0e227de8bd..e97be3e389 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -32,7 +32,12 @@ KMInputMethodEventHandler* _eventHandler; self.appDelegate.inputController = self; } - // register to receive notifications generated from KMInputMethodLifecycle + /** + * 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]; diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m index 194ced26e6..9b877a530e 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -41,7 +41,7 @@ NSString *const kInputMethodActivatedNotification = @"kInputMethodActivatedNotif NSString *const kInputMethodDeactivatedNotification = @"kInputMethodDeactivatedNotification"; NSString *const kInputMethodClientChangeNotification = @"kInputMethodClientChangeNotification"; NSString *const keymanInputMethodName = @"keyman.inputmethod.Keyman"; -const double transitionDelay = 0.2; +const double transitionDelay = 0.25; typedef enum { Started, @@ -167,6 +167,11 @@ typedef enum { break; case Activate: os_log_info([KMLogs lifecycleLog], "performTransition: Activate, new InputSourceId: %{public}@, new application ID: %{public}@", currentInputSource, currentClientAppId); + /** + * Perform two actions when activating the input method. + * Change the client first which prepares the event handler. + * Then do the activate which starts the event loop and opens the OSK. + */ [self changeClient]; [self activateInputMethod]; break;