feat(mac): add functions from removing keyboard
Some checks failed
Keyman Build Summary / Summarize build status checks (push) Has been cancelled

also added comments for Settings
renamed activeKeyboards to enabledKeyboards via
settings migration for Keyman 19
This commit is contained in:
Shawn Schantz 2026-04-27 15:47:16 -04:00
parent dd0f2043bf
commit af09f89bc9
7 changed files with 127 additions and 46 deletions

View file

@ -34,20 +34,6 @@ struct ConfigView: View {
}
.padding()
// ScrollView(showsIndicators: false) {
// List {
// // Use $ to get binding to the published array
// ForEach($settings.installedKeyboardPackages) { $package in
// Section(header: Text("Section")) {
// // Use $ to get binding to the nested array
// ForEach($package.keyboards) { $keyboard in
// Toggle(keyboard.keyboardId, isOn: $keyboard.enabled) // Binding here
// }
// }
// }
// }
// .padding()
// }
ScrollView {
VStack(alignment: .leading, spacing: 6) {
ForEach(Array(settings.installedPackages.enumerated()), id: \.offset) { index, package in
@ -60,7 +46,7 @@ struct ConfigView: View {
// Example of Icon-Only Button
Spacer()
Button(action: {
settings.installedPackages.remove(at: index)
settings.removePackage(at: index)
}) {
Label("remove", systemImage: "trash.fill")
}

View file

@ -11,7 +11,18 @@
#import "KMLogs.h"
#import "KMDataRepository.h"
/**
* The UserDefaults key `KMActiveKeyboardsKey` identifies the list of installed keyboard which
* the user has enabled to be included in the Keyman keyboard menu. This key name is a little confusing
* because the only truly active keyboard is the one that Keyman is applying while typing.
* So, with the migration of data for Keyman version 19, this key is renamed to `KMEnabledKeyboardsKey`
* Every enabled keyboard will appear in the Keyman keyboards menu.
* Only one of these keyboards can be selected at a time, and the selected keyboard is the one that
* is actively being used by Keyman for each keystroke.
*/
NSString *const kActiveKeyboardsKey = @"KMActiveKeyboardsKey";
NSString *const kEnabledKeyboardsKey = @"KMEnabledKeyboardsKey";
NSString *const kSelectedKeyboardKey = @"KMSelectedKeyboardKey";
NSString *const kPersistedOptionsKey = @"KMPersistedOptionsKey";
NSString *const kShowOskOnActivate = @"KMShowOskOnActivate";
@ -87,7 +98,7 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec
os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" );
BOOL migrationNeeded = keymanSettingsExist && !keyboardsStoredInLibrary;
os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
os_log([KMLogs dataLog], "keyman18DataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
return migrationNeeded;
}
@ -104,7 +115,7 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec
os_log([KMLogs dataLog], "settings indicate that keyboards are stored in ~/Library: %{public}@", keyboardsStoredInLibrary ? @"YES" : @"NO" );
BOOL migrationNeeded = keymanSettingsExistForInputMethod;
os_log([KMLogs dataLog], "dataMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
os_log([KMLogs dataLog], "keyman19SettingsMigrationNeeded: %{public}@", migrationNeeded ? @"YES" : @"NO" );
return migrationNeeded;
}
@ -132,7 +143,7 @@ NSInteger const kCurrentDataModelVersionNumber = kVersionStoreDataInLibraryDirec
NSArray * activeKeyboards = [self.appDefaults arrayForKey:kActiveKeyboardsKey];
if (activeKeyboards != nil) {
[self.groupDefaults setObject:activeKeyboards forKey:kActiveKeyboardsKey];
[self.groupDefaults setObject:activeKeyboards forKey:kEnabledKeyboardsKey];
}
if ([self.appDefaults objectForKey:kShowOskOnActivate] != nil) {

View file

@ -3,8 +3,8 @@
*
* Created by Shawn Schantz on 2026-04-02
*
* Class that exposes all settings information to the Config app
* Provides a place for the Config app can bind directly to the settings
* Class that exposes all settings information to the Keyman Configuration app
* Provides a place for the config app can bind directly to the settings
* and update when changes are made
*
*/
@ -63,6 +63,9 @@ public class SettingsContainer : ObservableObject {
}
}
/**
* for debugging: prints UserDefaults values
*/
public func logSettings() {
self.settingsRepository.logSettings()
}
@ -80,6 +83,23 @@ public class SettingsContainer : ObservableObject {
return package
}
public func removePackage(at index: Int) {
let package = self.installedPackages[index]
// will removing this package cause the removal of any enabled keyboards?
let removingEnabledKeyboards = !package.getEnabledKeyboardsSettingsKeys().isEmpty
// delete package from disk
self.packageRepository.deletePackage(package: package)
// remove package from installed packages list
_ = self.installedPackages.remove(at: index)
// if we removed any enabled keyboards, then update settings
if removingEnabledKeyboards {
self.persistKeyboardState()
}
}
/**
* returns true if the keyboard is enabled
* when enabled, the keyboard appears in the Keyman sub menu in the mac
@ -109,7 +129,7 @@ public class SettingsContainer : ObservableObject {
print ("Could not read keyboard state for package: \(packageId) and keyboard: \(keyboardId)")
}
// update persisted state in UserDefaults activeKeyboards array
// update persisted state in UserDefaults enabledKeyboards array
self.persistKeyboardState()
}
@ -118,7 +138,7 @@ public class SettingsContainer : ObservableObject {
*/
func persistKeyboardState() {
let enabledKeyboards = self.getAllEnabledKeyboardSettingsKeys()
self.settingsRepository.writeActiveKeyboards(activeKeyboardsArray: Array(enabledKeyboards))
self.settingsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(enabledKeyboards))
}
/**
@ -175,14 +195,14 @@ public class SettingsContainer : ObservableObject {
*/
func validateSettings() {
let installedKeyboardKeys = self.getAllKeyboardSettingsKeys()
let activeKeyboardKeys = self.settingsRepository.readActiveKeyboards()
let enabledKeyboardKeys = self.settingsRepository.readEnabledKeyboards()
if (activeKeyboardKeys.isSubset(of: installedKeyboardKeys)) {
print("only installed keyboards are listed as active: no need to synchronize")
if (enabledKeyboardKeys.isSubset(of: installedKeyboardKeys)) {
print("only installed keyboards are listed as enabled: no need to synchronize")
} else {
print("active keyboards list contains uninstalled keyboards: synchronize active keyboards list")
let installedActiveKeyboardKeys = activeKeyboardKeys.intersection(installedKeyboardKeys)
self.settingsRepository.writeActiveKeyboards(activeKeyboardsArray: Array(installedActiveKeyboardKeys))
print("enabled keyboards list contains uninstalled keyboards: synchronize enabled keyboards list")
let installedEnabledKeyboardKeys = enabledKeyboardKeys.intersection(installedKeyboardKeys)
self.settingsRepository.writeEnabledKeyboards(enabledKeyboardsArray: Array(installedEnabledKeyboardKeys))
}
}
@ -192,11 +212,11 @@ public class SettingsContainer : ObservableObject {
func applySettingsToInstalledPackages() {
self.validateSettings()
let activeKeyboards = self.settingsRepository.readActiveKeyboards()
let enabledKeyboards = self.settingsRepository.readEnabledKeyboards()
// set enabled flag if the keyboard is contained in the set of activeKeyboards
// set enabled flag if the keyboard is contained in the set of enabledKeyboards
self.installedPackages.forEach { $0.keyboards.forEach
{$0.enabled = activeKeyboards.contains($0.keyboardSettingsKey)}
{$0.enabled = enabledKeyboards.contains($0.keyboardSettingsKey)}
}
}
}

View file

@ -22,7 +22,7 @@ public class Keyboard: Identifiable, Hashable, Equatable {
public var keyboardDirectoryUrl: URL
// the URL of the .kmx file for the package
public let kmxFileUrl: URL
// the UserDefaults key for this package, used for the selected Keyboard and active keyboards
// the UserDefaults key for this package, used for the selected Keyboard and enabled keyboards
// the key is in the form "/[package directory]/[package name].kmx"
// for example, "/khmer_angkor/khmer_angkor.kmx"
public let keyboardSettingsKey: String

View file

@ -100,6 +100,18 @@ public class KeymanPackage: Identifiable, Hashable, Equatable {
return keyboard.keyboardSettingsKey
}
public func getEnabledKeyboardsSettingsKeys() -> [String] {
var settingsKeyArray = [String]()
self.keyboards.forEach { keyboard in
if (keyboard.enabled) {
settingsKeyArray.append(keyboard.keyboardSettingsKey)
}
}
return settingsKeyArray
}
public func validate() -> Bool {
var validKeyboards = self.keyboards.isEmpty == false

View file

@ -35,6 +35,19 @@ public struct PackageRepository {
return installedPackages
}
/**
* delete the package from disk
*/
func deletePackage(package: KeymanPackage) {
print("deleting package: \(package.sourceDirectoryUrl)")
do {
try FileManager.default.removeItem(at: package.sourceDirectoryUrl)
print("deleted package: \(package.sourceDirectoryUrl)")
} catch {
print("could not delete directory: \(error.localizedDescription)")
}
}
/**
* Creates the directory tree where keyboards are stored under the standard 'Group Containers' directory
*/

View file

@ -19,7 +19,7 @@ public struct SettingsRepository {
let defaultsSuiteName: String
let defaults: UserDefaults
let kActiveKeyboardsKey = "KMActiveKeyboardsKey"
let kEnabledKeyboardsKey = "KMEnabledKeyboardsKey"
let kSelectedKeyboardKey = "KMSelectedKeyboardKey"
let kPersistedOptionsKey = "KMPersistedOptionsKey"
let kDataModelVersionKey = "KMDataModelVersion"
@ -38,38 +38,69 @@ public struct SettingsRepository {
self.defaults = userDefaults
}
public func readActiveKeyboards() -> Set<String> {
let activeKeyboardsArray = self.defaults.stringArray(forKey: kActiveKeyboardsKey) ?? []
let activeKeyboardsSet = Set(activeKeyboardsArray)
/**
* get the list of enabled keyboards from the UserDefaults
* The enabled keyboards are those that are shown in the Keyman menu
* when it is selected from the System Input Source menu.
*/
public func readEnabledKeyboards() -> Set<String> {
let enabledKeyboardsArray = self.defaults.stringArray(forKey: kEnabledKeyboardsKey) ?? []
let enabledKeyboardsSet = Set(enabledKeyboardsArray)
return activeKeyboardsSet
return enabledKeyboardsSet
}
public func writeActiveKeyboards(activeKeyboardsArray: [String]) {
self.defaults.set(activeKeyboardsArray, forKey: kActiveKeyboardsKey)
/**
* update the list of enabled keyboards in the UserDefaults
*/
public func writeEnabledKeyboards(enabledKeyboardsArray: [String]) {
self.defaults.set(enabledKeyboardsArray, forKey: kEnabledKeyboardsKey)
}
/**
* return the selected keyboard from the UserDefaults
* The selected keyboard is the one that Keyman is applying for each keydown event.
*/
public func readSelectedKeyboard() -> String {
return self.defaults.string(forKey: kSelectedKeyboardKey) ?? ""
}
/**
* update the selected keyboard keyboards in the UserDefaults
*/
public func writeSelectedKeyboard(keyboardName: String) {
self.defaults.set(keyboardName, forKey: kSelectedKeyboardKey)
}
/**
* read the boolean setting which allows a Sentry error to be generated for testing
* This value is set in the **standard** application UserDefaults -- not the shared app group defaults.
* This is necessary because it must be set to true from the command line.
*/
public func readForceSentryError() -> Bool {
return self.defaults.bool(forKey: kShowOskOnActivateKey)
}
public func readShowOskOnActivate() -> Bool {
return self.defaults.bool(forKey: kShowOskOnActivateKey)
return UserDefaults.standard.bool(forKey: kShowOskOnActivateKey)
}
/**
* read the data model version for the settings to know what values and format to expect
*/
public func readDataModelVersion() -> Int {
// note that zero is returned if key is not found in UserDefaults,
return self.defaults.integer(forKey: kDataModelVersionKey)
}
/**
* read the boolean setting which indicates whether the OSK is opened when the input method is activated
* There may be no need to read this from with the config app.
*/
public func readShowOskOnActivate() -> Bool {
return self.defaults.bool(forKey: kShowOskOnActivateKey)
}
/**
* read the list of persisted options that are recorded dynamically from the input method
* There may be no need to read this from with the config app.
*/
public func readPersistedOptions() -> Dictionary<String, Any> {
if let options: Dictionary<String, Any> = self.defaults.dictionary(forKey: kPersistedOptionsKey) {
return options
@ -78,17 +109,25 @@ public struct SettingsRepository {
}
}
/**
* for debugging: prints UserDefaults values to the console
* with app group UserDefaults, there is no way to view from the command line
* (unlike standard application-level UserDefaults)
*/
public func logSettings() {
print("UserDefaults:")
print("\(kSelectedKeyboardKey): \(self.readSelectedKeyboard())")
print("\(kDataModelVersionKey): \(self.readDataModelVersion())")
print("\(kForceSentryErrorKey): \(self.readForceSentryError())")
print("\(kShowOskOnActivateKey): \(self.readShowOskOnActivate())")
print("\(kActiveKeyboardsKey): \(self.readActiveKeyboards())")
print("\(kEnabledKeyboardsKey): \(self.readEnabledKeyboards())")
print("\(kPersistedOptionsKey): \(self.readPersistedOptions())")
}
/**
* for debugging: clear all the entries for the app group UserDefaults
* unlike standard application-level UserDefaults, there is no way to view from the command line
*/
public func clearSettings() {
self.defaults.dictionaryRepresentation().keys.forEach { key in
self.defaults.removeObject(forKey: key)