Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
b1d73b4
feat(mac): added several fields to package, including QR Code
sgschantz Jul 13, 2026
adcfd43
feat(mac): published installed packages as two separate arrays
sgschantz Jul 14, 2026
d228eeb
feat(mac): remove reference to private property
sgschantz Jul 15, 2026
be6cd15
Merge branch 'epic/mac-config' into feat/mac/expand-package
sgschantz Jul 15, 2026
7cbb585
feat(mac): update input method after keyboard config change
sgschantz Jul 16, 2026
88c1232
feat(mac): add remove methods for each package list
sgschantz Jul 16, 2026
9d4bc48
Merge branch 'feat/mac/expand-package' of https://github.com/keymanap…
sgschantz Jul 16, 2026
542e326
feat(mac): fix comment
sgschantz Jul 16, 2026
29ed0ab
feat(mac): added comment
sgschantz Jul 16, 2026
fc34086
feat(mac): change keyboardsChanged notification to deliver immediately
sgschantz Jul 16, 2026
b072356
feat(mac): added author website and default image
sgschantz Jul 21, 2026
9cac155
Merge branch 'epic/mac-config' into feat/mac/expand-package
sgschantz Jul 21, 2026
610b254
Merge branch 'feat/mac/expand-package' into feat/mac/notify-input-method
sgschantz Jul 21, 2026
3e72c9e
feat(mac): add method to delete package by UUID
sgschantz Jul 21, 2026
777e3b3
Merge branch 'feat/mac/expand-package' of https://github.com/keymanap…
sgschantz Jul 21, 2026
2d5f66f
Merge branch 'feat/mac/expand-package' into feat/mac/notify-input-method
sgschantz Jul 21, 2026
e733df1
Merge pull request #16250 from keymanapp/feat/mac/notify-input-method
sgschantz Jul 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 26 additions & 3 deletions mac/Config/Config/ConfigView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ struct ConfigView: View {
Image(systemName: "keyboard")
.imageScale(.large)
.foregroundColor(.accentColor)
Text("keyboard count = \(settings.installedPackages.count)")
Text("multiple keyboard package count = \(settings.multiKeyboardPackages.count)")
Text("single keyboard package count = \(settings.singleKeyboardPackages.count)")
Comment thread
sgschantz marked this conversation as resolved.
Button("debug") {
settings.debug()
}
Expand All @@ -46,17 +47,39 @@ struct ConfigView: View {

ScrollView {
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(settings.installedPackages.enumerated()), id: \.offset) { index, package in
ForEach(Array(settings.singleKeyboardPackages.enumerated()), id: \.offset) { index, package in
VStack {
HStack(alignment: .center, spacing: 10) {
VStack(spacing: 16) {
Text("Scan to visit website:")
Comment thread
sgschantz marked this conversation as resolved.
.font(.headline)

if let qrImage = package.generateSharePackageQRCode(size: 200) {
Image(nsImage: qrImage)
.interpolation(.none) // important: keeps the QR edges sharp
.resizable()
.frame(width: 200, height: 200)
.background(Color.white) // ensures good scanning contrast
} else {
Text("Failed to generate QR Code")
.foregroundColor(.red)
}
}
.padding()
Text(package.packageName)
.font(.headline)
Text(package.packageVersion)
.font(.subheadline)
// Example of Icon-Only Button
Spacer()
if let nsImage = package.graphicImage {
Image(nsImage: nsImage)
.resizable() // Allows resizing
.scaledToFit() // Maintains original aspect ratio
.frame(maxWidth: 140, maxHeight: 250) // Controls the bounds
}
Button(action: {
settings.removePackage(at: index)
settings.removeInstalledPackage(with: package.id)
}) {
Label("remove", systemImage: "trash.fill")
}
Expand Down
20 changes: 10 additions & 10 deletions mac/Config/Installation/InstallationCheck.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public class InstallationCheck {
self.inputMethodVersion = "unknown"
}

self.configurationVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
self.configurationVersion = ConfigAppUtil.configAppVersion()
self.isInputMethodCurrent = InstallationCheck.isVersionCurrent(inputMethodVersion: self.inputMethodVersion, configurationVersion: self.configurationVersion)

self.installationState = self.loadState()
Expand All @@ -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
Expand Down
34 changes: 34 additions & 0 deletions mac/Keyman4MacIM/Keyman4MacIM/KMInputMethodAppDelegate.m
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
NSString *processorType = @"Unknown";
#endif

// distributed notifications
NSString *const kKeyboardsChanged = @"com.keyman.keyboards.changed";

// in-app notifications
NSString *const kKeymanKeyboardDownloadCompletedNotification = @"kKeymanKeyboardDownloadCompletedNotification";

@implementation NSString (VersionNumbers)
Expand Down Expand Up @@ -125,10 +129,22 @@ - (void)initCompletion {
[[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
*/
Expand Down Expand Up @@ -677,6 +693,24 @@ - (NSMutableArray *)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 {
os_log_debug([KMLogs dataLog], "saveEnabledKeyboards");
[KMSettingsRepository.shared writeEnabledKeyboards:_enabledKeyboards];
Expand Down
9 changes: 6 additions & 3 deletions mac/KeymanSettings/Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,19 @@ let package = Package(
.target(
name: "KeymanSettings",
dependencies: [
.product(name: "ZIPFoundation", package: "ZIPFoundation")
.product(name: "ZIPFoundation", package: "ZIPFoundation")
],
path: "Sources"
path: "Sources",
resources: [
.process("Resources")
]
),
.testTarget(
name: "KeymanSettingsTests",
dependencies: ["KeymanSettings"],
path: "Tests",
resources: [
.copy("Resources/amharic.kmp.json") // The test data files, copy files without modifying them
.copy("Resources/amharic.kmp.json") // The test data files, copy files without modifying them
]
),
]
Expand Down
20 changes: 20 additions & 0 deletions mac/KeymanSettings/Sources/KeymanSettings/ConfigAppUtil.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Shawn Schantz on 2026-07-13
*
* Used for getting application metadata from the Config application
*/

import Foundation

public struct ConfigAppUtil {
/**
* returns the short version string from the bundle of the Config app
*/
static public func configAppVersion() -> String {
return Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "unknown"
}


}
Loading