From 7cbb5851d7bba17f74e7654146a42d05402d64a4 Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Thu, 16 Jul 2026 12:38:33 -0400 Subject: [PATCH 1/2] feat(mac): update input method after keyboard config change Sendsend a distributed notification from the config app whenever a change to the enabled keyboards occurs. In response, the input method loads the list of enabled keyboards and updates the Keyman menu. --- .../Installation/InstallationCheck.swift | 18 +++++----- .../Keyman4MacIM/KMInputMethodAppDelegate.m | 34 +++++++++++++++++++ .../KeymanSettings/SettingsContainer.swift | 13 ++++--- .../Persistence/DefaultsRepository.swift | 13 +++++++ 4 files changed, 65 insertions(+), 13 deletions(-) diff --git a/mac/Config/Installation/InstallationCheck.swift b/mac/Config/Installation/InstallationCheck.swift index 04d118b312..1eaf159913 100644 --- a/mac/Config/Installation/InstallationCheck.swift +++ b/mac/Config/Installation/InstallationCheck.swift @@ -63,37 +63,37 @@ public class InstallationCheck { print("InstallationCheck registerObservers") DistributedNotificationCenter.default().addObserver( self, - selector: #selector(self.handlePermissionNotification(_:)), - name: NSNotification.Name.accessCheck, + selector: #selector(self.handleAccessibilityResponse(_:)), + name: NSNotification.Name.accessibilityQueryResponse, object: nil // Observe notifications from any sender ) // MAC-CONFIG_TODO: add timeout? } /** - * called when `NSNotification.Name.accessCheck` is received + * called when `NSNotification.Name.accessibilityQueryResponse` is received */ - @objc func handlePermissionNotification(_ notification: Notification) { - print("handlePermissionNotification") + @objc func handleAccessibilityResponse(_ notification: Notification) { + print("handleAccessibilityResponse") // Extract message from the notification if available if let message = notification.object as? String { - let permissionGranted = self.processInputMethodResponse(with: message) + let permissionGranted = self.processAccessibilityResponse(with: message) self.completeValidation(accessibilityPermissionGranted: permissionGranted) } else { - print("accessCheckResponse received but did not include message") + print("accessibilityQueryResponse received but did not include message") } } /** * Process the distributed notification message that we received from the Keyman input method. */ - func processInputMethodResponse(with message: String) -> Bool { + func processAccessibilityResponse(with message: String) -> Bool { let timeStyle = Date.FormatStyle() .hour(.twoDigits(amPM: Date.FormatStyle.Symbol.Hour.AMPMStyle.abbreviated)) .minute(.twoDigits) .second(.twoDigits) .secondFraction(.fractional(3)) - print("processAccessibilityCheckResponse received message: \(message), time: \(Date().formatted(timeStyle))") + print("processAccessibilityResponse received message: \(message), time: \(Date().formatted(timeStyle))") // if the message indicates that access was granted, then return true return !message.isEmpty && message == kAccessibilityPermissionGrantedMessage diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m index 089665c058..d6f2014bb6 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m @@ -28,6 +28,10 @@ NSString *processorType = @"Intel"; NSString *processorType = @"Unknown"; #endif +// distributed notifications +NSString *const kKeyboardsChanged = @"com.keyman.keyboards.changed"; + +// in-app notifications NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification"; @implementation NSString (VersionNumbers) @@ -125,10 +129,22 @@ id _lastServerWithOSKShowing = nil; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification object:nil]; + // register to receive notifications generated from Keyman Configuration App + [[NSDistributedNotificationCenter defaultCenter] addObserver:self selector:@selector(handleKeyboardsChanged:) name:kKeyboardsChanged object:nil]; + // start Input Method lifecycle [KMInputMethodLifecycle.shared startLifecycle]; } + +/** + * When packages have been installed, removed, enabled or disabled -- notification from the Keyman Configuration app + */ +- (void)handleKeyboardsChanged:(NSNotification *)notification { + os_log_debug([KMLogs configLog], "***KMInputMethodAppDelegate handleKeyboardsChanged"); + [self reloadEnabledKeyboards]; +} + /** * When the input method is activated -- notification from KMInputMethodLifecycle */ @@ -677,6 +693,24 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef return _enabledKeyboards; } +/** + * Called when the enabled keyboards have been changed by the Keyman Config app. + * Reload the enabled keyboards from the UserDefaults + * Then update the Keyman menu + */ +- (void)reloadEnabledKeyboards { + os_log_debug([KMLogs configLog], "reloadEnabledKeyboards"); + + // read the enabled keyboards from UserDefaults + _enabledKeyboards = [[KMSettingsRepository.shared readEnabledKeyboards] mutableCopy]; + + // set Sentry tag for how many keyboards are enabled + [KMSentryHelper addEnabledKeyboardCountTag:_enabledKeyboards.count]; + + // update the keyboard menu to reflect the updated list of enabled keyboards + [self updateKeyboardMenuItems]; +} + - (void)saveEnabledKeyboards { os_log_debug([KMLogs dataLog], "saveEnabledKeyboards"); [KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards]; diff --git a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift index 3cf08fce75..4a5a1f92a7 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift +++ b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift @@ -25,13 +25,17 @@ import Foundation import Combine import ZIPFoundation +// distributed notifications +public extension Notification.Name { + static let accessibilityQueryResponse = Notification.Name("com.keyman.accessibility.state") + static let keyboardsChanged = Notification.Name("com.keyman.keyboards.changed") +} + +// in-app notifications public extension Notification.Name { - static let accessCheck = Notification.Name("com.keyman.accessibility.state") static let newPackageInstalled = Notification.Name("com.keyman.package.installed") static let packageReplaced = Notification.Name("com.keyman.package.replaced") static let packageDowngradeRequested = Notification.Name("com.keyman.package.downgrade.requested") - static let packageLoadRequired = Notification.Name("com.keyman.package.load.required") - static let packageUpdateRequired = Notification.Name("com.keyman.package.update.required") } public enum SettingsError: Error { @@ -43,6 +47,7 @@ public class SettingsContainer : ObservableObject { // installed packages are loaded from disk, each package may contain one or more keyboard fileprivate var installedPackages: [KeymanPackage] { didSet { + // whenever this array is modified, update the arrays that are derived from it self.updatePackageArrays() } } @@ -135,7 +140,7 @@ public class SettingsContainer : ObservableObject { name: .packageReplaced, object: nil ) } - + /** * called for `newPackageInstalled` notification */ diff --git a/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift index c7c2fa2196..9794dee521 100644 --- a/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift @@ -81,8 +81,21 @@ public class DefaultsRepository: DefaultsRepo { */ public func writeEnabledKeyboards(enabledKeyboardsArray: [String]) { self.groupDefaults.set(enabledKeyboardsArray, forKey: kEnabledKeyboardsKey) + self.sendKeyboardsChangedNotification() } + /** + * Send a distributed notification that the keyboards have changed. + * The input method will receive this and reload the enabled keyboards. + */ + func sendKeyboardsChangedNotification() { + DistributedNotificationCenter.default.post( + name: .keyboardsChanged, + object: nil, + userInfo: nil + ) + } + /** * return the selected keyboard from the UserDefaults * The selected keyboard is the one that Keyman is applying for each keydown event. From fc340866ad07917a190b6d674e7a9ccebd8dcee9 Mon Sep 17 00:00:00 2001 From: Shawn Schantz Date: Thu, 16 Jul 2026 17:14:20 -0400 Subject: [PATCH 2/2] feat(mac): change keyboardsChanged notification to deliver immediately also explict call default() to be sure that we get back a DistributedNotificationCenter --- .../Sources/Persistence/DefaultsRepository.swift | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift index 9794dee521..e7e8af18ab 100644 --- a/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/DefaultsRepository.swift @@ -89,10 +89,11 @@ public class DefaultsRepository: DefaultsRepo { * The input method will receive this and reload the enabled keyboards. */ func sendKeyboardsChangedNotification() { - DistributedNotificationCenter.default.post( - name: .keyboardsChanged, + DistributedNotificationCenter.default().postNotificationName ( + .keyboardsChanged, object: nil, - userInfo: nil + userInfo: nil, + deliverImmediately: true ) }