diff --git a/mac/Config/Config/ConfigView.swift b/mac/Config/Config/ConfigView.swift index e992026e93..17db5daf12 100644 --- a/mac/Config/Config/ConfigView.swift +++ b/mac/Config/Config/ConfigView.swift @@ -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") } diff --git a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m index 566b90db5d..da0de90fce 100644 --- a/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m +++ b/mac/Keyman4MacIM/Keyman4MacIM/KMSettingsRepository.m @@ -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) { diff --git a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift index 93b4e417d1..301afc5f1d 100644 --- a/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift +++ b/mac/KeymanSettings/Sources/KeymanSettings/SettingsContainer.swift @@ -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)} } } } diff --git a/mac/KeymanSettings/Sources/Model/Keyboard.swift b/mac/KeymanSettings/Sources/Model/Keyboard.swift index 73709b6824..b54a425d7e 100644 --- a/mac/KeymanSettings/Sources/Model/Keyboard.swift +++ b/mac/KeymanSettings/Sources/Model/Keyboard.swift @@ -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 diff --git a/mac/KeymanSettings/Sources/Model/KeymanPackage.swift b/mac/KeymanSettings/Sources/Model/KeymanPackage.swift index afaf586ac1..bf7e22d4e3 100644 --- a/mac/KeymanSettings/Sources/Model/KeymanPackage.swift +++ b/mac/KeymanSettings/Sources/Model/KeymanPackage.swift @@ -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 diff --git a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift index e9b3141f93..be9a2c20a3 100644 --- a/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/PackageRepository.swift @@ -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 */ diff --git a/mac/KeymanSettings/Sources/Persistence/SettingsRepository.swift b/mac/KeymanSettings/Sources/Persistence/SettingsRepository.swift index 13d8bc8568..8c17c65f3f 100644 --- a/mac/KeymanSettings/Sources/Persistence/SettingsRepository.swift +++ b/mac/KeymanSettings/Sources/Persistence/SettingsRepository.swift @@ -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 { - 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 { + 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 { if let options: Dictionary = 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)