diff --git a/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj b/mac/Keyman4MacIM/Keyman4MacIM.xcodeproj/project.pbxproj index 08e00b6c07a..57ce828e4a5 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/KMConfiguration/Base.lproj/preferences.xib b/mac/Keyman4MacIM/Keyman4MacIM/KMConfiguration/Base.lproj/preferences.xib index c327ce4ad27..d5afcd00d5e 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 c99d6326657..f3a0c3501e5 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 @@ - (void)windowDidLoad { 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 @@ - (IBAction)downloadAction:(id)sender { [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 f832abf27fd..e97be3e389c 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputController.m @@ -1,39 +1,46 @@ -// -// 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" #import "KMOSVersion.h" #include /* For kVK_ constants. */ +#import "KMSettingsRepository.h" #import "KMLogs.h" +#import "InputMethodKit/InputMethodKit.h" +#import "KMInputMethodLifecycle.h" + @implementation KMInputController KMInputMethodEventHandler* _eventHandler; -NSMutableArray *servers; -- (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); - + os_log_debug([KMLogs lifecycleLog], "initWithServer, active app: '%{public}@'", [KMInputMethodLifecycle getClientApplicationId]); + self = [super initWithServer:server delegate:delegate client:inputClient]; if (self) { - servers = [[NSMutableArray alloc] initWithCapacity:2]; - self.AppDelegate.inputController = self; - if (self.AppDelegate.kvk != nil && self.AppDelegate.alwaysShowOSK) { - [self.AppDelegate showOSK]; - } + self.appDelegate.inputController = self; } + /** + * Register to receive the Deactivated and ChangedClient notification generated from KMInputMethodLifecycle so + * that the eventHandler can be changed. There is no need to receive the Activated notification because + * the InputController does it all it needs to when it receives the ChangedClient. + */ + + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; + [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; + return self; } @@ -59,102 +66,44 @@ - (void)handleBackspace:(NSEvent *)event { } } -- (void)activateServer:(id)sender { - @synchronized(servers) { - [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; - - [self.AppDelegate wakeUpWith:sender]; - [servers addObject: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]; - +/** + * 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) { + [_eventHandler deactivate]; } } -- (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) { - [servers removeObjectAtIndex:i]; - break; - } - } - 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]; - } +/** + * 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]; } -} + _eventHandler = [[KMInputMethodEventHandler alloc] initWithClient:[KMInputMethodLifecycle getClientApplicationId] client:self.client]; -- (void)timerAction:(id)lastServer { - @synchronized(servers) { - if (servers.count == 0) { - if (_eventHandler != nil) { - [_eventHandler deactivate]; - _eventHandler = nil; - } - [self.AppDelegate sleepFollowingDeactivationOfServer:lastServer]; - } - } } +- (void)activateServer:(id)sender { + [sender overrideKeyboardWithKeyboardNamed:@"com.apple.keylayout.US"]; + [KMInputMethodLifecycle.shared activateClient:sender]; +} -/* - - (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; - } - */ +- (void)deactivateServer:(id)sender { + [KMInputMethodLifecycle.shared deactivateClient:sender]; + [[NSNotificationCenter defaultCenter] removeObserver:self]; +} - (NSMenu *)menu { - return self.AppDelegate.menu; + return self.appDelegate.menu; } - (KMXFile *)kmx { - return self.AppDelegate.kmx; + return self.appDelegate.kmx; } - (void)menuAction:(id)sender { @@ -165,13 +114,15 @@ - (void)menuAction:(id)sender { [self showConfigurationWindow:sender]; } else if (itag == OSK_MENUITEM_TAG) { - [self.AppDelegate showOSK]; + [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) { - [self.AppDelegate showAboutWindow]; + [self.appDelegate showAboutWindow]; } else if (itag >= KEYMAN_FIRST_KEYBOARD_MENUITEM_TAG) { - [self.AppDelegate selectKeyboardFromMenu:itag]; + [self.appDelegate selectKeyboardFromMenu:itag]; } } @@ -183,7 +134,7 @@ - (void)showConfigurationWindow:(id)sender { 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 ce70724c3a5..7f6b2f3655b 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; @@ -90,7 +89,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; @@ -102,8 +100,6 @@ static const int KEYMAN_FIRST_KEYBOARD_MENUITEM_INDEX = 0; - (void)showOSK; - (void)showConfigurationWindow; - (void)selectKeyboardFromMenu:(NSInteger)tag; -- (void)sleepFollowingDeactivationOfServer:(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 78c6362de93..7392c3dc2ff 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" @@ -58,7 +59,6 @@ @implementation KMInputMethodAppDelegate @synthesize selectedKeyboard = _selectedKeyboard; @synthesize activeKeyboards = _activeKeyboards; @synthesize contextBuffer = _contextBuffer; -@synthesize alwaysShowOSK = _alwaysShowOSK; id _lastServerWithOSKShowing = nil; @@ -116,6 +116,45 @@ - (void)initCompletion { 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 { @@ -222,43 +261,12 @@ + (KMInputMethodAppDelegate *)AppDelegate { return (KMInputMethodAppDelegate *)[NSApp delegate]; } --(void) sleepFollowingDeactivationOfServer:(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 { - 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. - if (_kvk != nil && (_alwaysShowOSK || _lastServerWithOSKShowing == newServer)) { - [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")); @@ -433,11 +441,6 @@ - (NSString *)oskWindowTitle { 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 +450,6 @@ - (void)setUseVerboseLogging:(BOOL)useVerboseLogging { [[KMSettingsRepository shared] writeUseVerboseLogging:useVerboseLogging]; } -- (BOOL)alwaysShowOSK { - _alwaysShowOSK = [[KMSettingsRepository shared] readAlwaysShowOsk]; - return _alwaysShowOSK; -} - - (BOOL)useVerboseLogging { return [[KMSettingsRepository shared] readUseVerboseLogging]; } @@ -711,7 +709,7 @@ - (void)prepareStorage { [KMDataRepository.shared createDataDirectoryIfNecessary]; if ([KMSettingsRepository.shared dataMigrationNeeded]) { - BOOL movedData = [KMDataRepository.shared migrateData]; + [KMDataRepository.shared migrateData]; [KMSettingsRepository.shared convertSettingsForMigration]; } @@ -887,8 +885,6 @@ - (void)selectKeyboardFromMenu:(NSInteger)tag { [self setContextBuffer:nil]; [self setSelectedKeyboard:path]; [self applyPersistedOptions]; - if (kvk != nil && self.alwaysShowOSK) - [self showOSK]; } - (NSArray *)getKmxFilesInKeyboardsDirectory { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h new file mode 100644 index 00000000000..62a3aac35d1 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.h @@ -0,0 +1,27 @@ +/* + * 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; ++ (NSString*)getClientApplicationId; +- (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 00000000000..9b877a530e7 --- /dev/null +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodLifecycle.m @@ -0,0 +1,253 @@ +/* + * 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 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. + * + * 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". + * + * 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. + * + * 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"; +const double transitionDelay = 0.25; + +typedef enum { + Started, + Active, + Inactive +} LifecycleState; + +typedef enum { + None, + Activate, + Deactivate, + ChangeClients, +} TransitionType; + + +@interface KMInputMethodLifecycle() + +@property LifecycleState lifecycleState; +@property NSString *inputSourceId; +@property NSString *clientApplicationId; +@end + +@implementation KMInputMethodLifecycle + ++ (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) { + _lifecycleState = Started; + _inputSourceId = @""; + _clientApplicationId = @""; + } + return self; +} + +/** + * called from Application Delgate during init + */ +- (void)startLifecycle { + _lifecycleState = Started; +} + +/** + * 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). + */ ++ (NSString*)getCurrentInputSourceId { + TISInputSourceRef inputSource = TISCopyCurrentKeyboardInputSource(); + NSString *inputSourceId = (__bridge NSString *)(TISGetInputSourceProperty(inputSource, kTISPropertyInputSourceID)); + return inputSourceId; +} + +/** + * Get the bundle ID of the currently active text input client.. + */ ++ (NSString*)getClientApplicationId { + NSRunningApplication *currentApp = [[NSWorkspace sharedWorkspace] frontmostApplication]; + NSString *clientAppId = [currentApp bundleIdentifier]; + return clientAppId; +} + +/** + * Based on the current lifecycleState and the input method state from the OS, determine how the state must transition. + */ +- (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); + + switch (self.lifecycleState) { + case Started: + transition = Activate; + break; + case Active: + if (inputSourceIsKeyman) { + if (clientHasChanged) { + transition = ChangeClients; + } + } else { + transition = Deactivate; + } + 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 or a deactivateServer 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); + /** + * 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; + 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; + } +} + +/** + * Called when IMKInputController receives an activateServer message + */ +- (void)activateClient:(id)client { + os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle activateClient"); + + [self performSelector:@selector(performTransitionAfterDelay:) withObject:client afterDelay:transitionDelay]; +} + +/** + * Called when IMKInputController receives an deactivateServer message + */ +- (void)deactivateClient:(id)client { + os_log_debug([KMLogs lifecycleLog], "KMInputMethodLifecycle deactivateClient"); + + [self performSelector:@selector(performTransitionAfterDelay:) withObject:client afterDelay:transitionDelay]; +} + +- (void)performTransitionAfterDelay:(id)client { + os_log_debug([KMLogs lifecycleLog], "performTransitionAfterDelay: calling performTransition"); + [self performTransition:client]; +} + +/** + * Change lifecycleState to Active and send notification. + */ +- (void)activateInputMethod { + os_log_debug([KMLogs lifecycleLog], "activateInputMethod"); + _lifecycleState = Active; + [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodActivatedNotification object:self]; +} + +/** + * Change lifecycleState to Inactive and send notification. + */ +- (void)deactivateInputMethod { + os_log_debug([KMLogs lifecycleLog], "deactivateInputMethod"); + _lifecycleState = Inactive; + [[NSNotificationCenter defaultCenter] postNotificationName:kInputMethodDeactivatedNotification object:self]; +} + +/** + * 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 + */ +- (BOOL)shouldEnableEventTap { + return ((self.lifecycleState == Started) || (self.lifecycleState == Active)); +} + +/** + * Returns true if lifecycleState is Active and the Settings require us to show the OSK + */ +- (BOOL)shouldShowOskOnActivate { + return [KMSettingsRepository.shared readShowOskOnActivate] + && (self.lifecycleState == Active); +} + +@end diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.h index 8d7b114f2b9..7fdf3de0346 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)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 bbd87a40ec6..db59929eaec 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 kShowOskOnActivate = @"KMShowOskOnActivate"; NSString *const kUseVerboseLogging = @"KMUseVerboseLogging"; /** @@ -25,6 +25,11 @@ * 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 @@ - (void)convertOptionsPathsForMigration { } } -- (BOOL)readAlwaysShowOsk { +- (BOOL)readShowOskOnActivate { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - return [userData boolForKey:kAlwaysShowOSKKey]; + return [userData boolForKey:kShowOskOnActivate]; } -- (void)writeAlwaysShowOsk:(BOOL)alwaysShowOsk { +- (void)writeShowOskOnActivate:(BOOL)show { NSUserDefaults *userData = [NSUserDefaults standardUserDefaults]; - [userData setBool:alwaysShowOsk forKey:kAlwaysShowOSKKey]; + [userData setBool:show forKey:kShowOskOnActivate]; } - (BOOL)readUseVerboseLogging { diff --git a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.h index 4b98f633b85..fd3a953a858 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 30d30004c45..33d7eda4062 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.m @@ -8,6 +8,8 @@ #import "OSKWindowController.h" #import "KMInputMethodAppDelegate.h" +#import "KMInputMethodLifecycle.h" +#import "KMSettingsRepository.h" #import "KMLogs.h" @interface OSKWindowController () @@ -25,7 +27,6 @@ - (KMInputMethodAppDelegate *)AppDelegate { } - (void)dealloc { - [[NSNotificationCenter defaultCenter] removeObserver:self]; [self stopTimer]; } @@ -55,7 +56,6 @@ - (void)awakeFromNib { - (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. @@ -66,6 +66,11 @@ - (void)windowDidResize:(NSNotification *)notification { [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 753a59365a9..b753e438028 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib +++ b/mac/Keyman4MacIM/Keyman4MacIM/OnScreenKeyboard/OSKWindowController.xib @@ -2,7 +2,7 @@ - + @@ -14,7 +14,7 @@ - +