Merge pull request #16250 from keymanapp/feat/mac/notify-input-method
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled

feat(mac): update input method on keyboard configuration change 🍎
This commit is contained in:
Shawn Schantz 2026-07-30 21:06:15 -04:00 committed by GitHub
commit e733df16ea
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 66 additions and 13 deletions

View file

@ -63,37 +63,37 @@ public class InstallationCheck {
print("InstallationCheck registerObservers") print("InstallationCheck registerObservers")
DistributedNotificationCenter.default().addObserver( DistributedNotificationCenter.default().addObserver(
self, self,
selector: #selector(self.handlePermissionNotification(_:)), selector: #selector(self.handleAccessibilityResponse(_:)),
name: NSNotification.Name.accessCheck, name: NSNotification.Name.accessibilityQueryResponse,
object: nil // Observe notifications from any sender object: nil // Observe notifications from any sender
) )
// MAC-CONFIG_TODO: add timeout? // MAC-CONFIG_TODO: add timeout?
} }
/** /**
* called when `NSNotification.Name.accessCheck` is received * called when `NSNotification.Name.accessibilityQueryResponse` is received
*/ */
@objc func handlePermissionNotification(_ notification: Notification) { @objc func handleAccessibilityResponse(_ notification: Notification) {
print("handlePermissionNotification") print("handleAccessibilityResponse")
// Extract message from the notification if available // Extract message from the notification if available
if let message = notification.object as? String { if let message = notification.object as? String {
let permissionGranted = self.processInputMethodResponse(with: message) let permissionGranted = self.processAccessibilityResponse(with: message)
self.completeValidation(accessibilityPermissionGranted: permissionGranted) self.completeValidation(accessibilityPermissionGranted: permissionGranted)
} else { } 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. * 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() let timeStyle = Date.FormatStyle()
.hour(.twoDigits(amPM: Date.FormatStyle.Symbol.Hour.AMPMStyle.abbreviated)) .hour(.twoDigits(amPM: Date.FormatStyle.Symbol.Hour.AMPMStyle.abbreviated))
.minute(.twoDigits) .minute(.twoDigits)
.second(.twoDigits) .second(.twoDigits)
.secondFraction(.fractional(3)) .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 // if the message indicates that access was granted, then return true
return !message.isEmpty && message == kAccessibilityPermissionGrantedMessage return !message.isEmpty && message == kAccessibilityPermissionGrantedMessage

View file

@ -28,6 +28,10 @@ NSString *processorType = @"Intel";
NSString *processorType = @"Unknown"; NSString *processorType = @"Unknown";
#endif #endif
// distributed notifications
NSString *const kKeyboardsChanged = @"com.keyman.keyboards.changed";
// in-app notifications
NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification"; NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification";
@implementation NSString (VersionNumbers) @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(inputMethodDeactivated:) name:kInputMethodDeactivatedNotification object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(inputMethodChangedClient:) name:kInputMethodClientChangeNotification 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 // start Input Method lifecycle
[KMInputMethodLifecycle.shared startLifecycle]; [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 * When the input method is activated -- notification from KMInputMethodLifecycle
*/ */
@ -677,6 +693,24 @@ CGEventRef eventTapFunction(CGEventTapProxy proxy, CGEventType type, CGEventRef
return _enabledKeyboards; 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 { - (void)saveEnabledKeyboards {
os_log_debug([KMLogs dataLog], "saveEnabledKeyboards"); os_log_debug([KMLogs dataLog], "saveEnabledKeyboards");
[KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards]; [KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards];

View file

@ -25,13 +25,17 @@ import Foundation
import Combine import Combine
import ZIPFoundation 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 { public extension Notification.Name {
static let accessCheck = Notification.Name("com.keyman.accessibility.state")
static let newPackageInstalled = Notification.Name("com.keyman.package.installed") static let newPackageInstalled = Notification.Name("com.keyman.package.installed")
static let packageReplaced = Notification.Name("com.keyman.package.replaced") static let packageReplaced = Notification.Name("com.keyman.package.replaced")
static let packageDowngradeRequested = Notification.Name("com.keyman.package.downgrade.requested") 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 { 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 // installed packages are loaded from disk, each package may contain one or more keyboard
fileprivate var installedPackages: [KeymanPackage] { fileprivate var installedPackages: [KeymanPackage] {
didSet { didSet {
// whenever this array is modified, update the arrays that are derived from it
self.updatePackageArrays() self.updatePackageArrays()
} }
} }
@ -135,7 +140,7 @@ public class SettingsContainer : ObservableObject {
name: .packageReplaced, object: nil name: .packageReplaced, object: nil
) )
} }
/** /**
* called for `newPackageInstalled` notification * called for `newPackageInstalled` notification
*/ */

View file

@ -81,8 +81,22 @@ public class DefaultsRepository: DefaultsRepo {
*/ */
public func writeEnabledKeyboards(enabledKeyboardsArray: [String]) { public func writeEnabledKeyboards(enabledKeyboardsArray: [String]) {
self.groupDefaults.set(enabledKeyboardsArray, forKey: kEnabledKeyboardsKey) 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().postNotificationName (
.keyboardsChanged,
object: nil,
userInfo: nil,
deliverImmediately: true
)
}
/** /**
* return the selected keyboard from the UserDefaults * return the selected keyboard from the UserDefaults
* The selected keyboard is the one that Keyman is applying for each keydown event. * The selected keyboard is the one that Keyman is applying for each keydown event.