diff --git a/Input Source Pro/Models/IndicatorVM+Triggers.swift b/Input Source Pro/Models/IndicatorVM+Triggers.swift index 1d04eed..12d4090 100644 --- a/Input Source Pro/Models/IndicatorVM+Triggers.swift +++ b/Input Source Pro/Models/IndicatorVM+Triggers.swift @@ -87,7 +87,7 @@ extension IndicatorVM { } } - if previous?.inputSource.id != current.inputSource.id { + if previous?.inputSource.persistentIdentifier != current.inputSource.persistentIdentifier { switch current.inputSourceChangeReason { case .noChanges: return .justHide diff --git a/Input Source Pro/Models/IndicatorVM.swift b/Input Source Pro/Models/IndicatorVM.swift index 84e6e99..7631eeb 100644 --- a/Input Source Pro/Models/IndicatorVM.swift +++ b/Input Source Pro/Models/IndicatorVM.swift @@ -172,7 +172,7 @@ extension IndicatorVM { guard appKind1.isSameAppOrWebsite(with: appKind2, detectAddressBar: true) else { return false } - guard lhs.inputSource.id == rhs.inputSource.id + guard lhs.inputSource.persistentIdentifier == rhs.inputSource.persistentIdentifier else { return false } return true @@ -249,7 +249,7 @@ extension IndicatorVM { ) } case let .inputSourceChanged(inputSource): - guard inputSource.id != state.inputSource.id else { return state } + guard inputSource.persistentIdentifier != state.inputSource.persistentIdentifier else { return state } return updateState(appKind: state.appKind, inputSource: inputSource, inputSourceChangeReason: .system) case let .switchInputSourceByShortcut(inputSource): diff --git a/Input Source Pro/Models/PreferencesVM+AppKeyboardCache.swift b/Input Source Pro/Models/PreferencesVM+AppKeyboardCache.swift index 05e30e0..596f569 100644 --- a/Input Source Pro/Models/PreferencesVM+AppKeyboardCache.swift +++ b/Input Source Pro/Models/PreferencesVM+AppKeyboardCache.swift @@ -34,7 +34,7 @@ extension PreferencesVM { let defaultKeyboard = getAppDefaultKeyboard(appKind) if appNeedCacheKeyboard(appKind), - defaultKeyboard?.id != keyboard.id + defaultKeyboard?.persistentIdentifier != keyboard.persistentIdentifier { appKeyboardCache.save(appKind, keyboard: keyboard) } else { diff --git a/Input Source Pro/Models/PreferencesVM.swift b/Input Source Pro/Models/PreferencesVM.swift index e58c7ab..23c1dee 100644 --- a/Input Source Pro/Models/PreferencesVM.swift +++ b/Input Source Pro/Models/PreferencesVM.swift @@ -537,11 +537,11 @@ extension Preferences { extension PreferencesVM { var systemWideDefaultKeyboard: InputSource? { - return InputSource.sources.first { $0.id == preferences.systemWideDefaultKeyboardId } + return InputSource.resolvePersistedIdentifier(preferences.systemWideDefaultKeyboardId) } var browserAddressDefaultKeyboard: InputSource? { - return InputSource.sources.first { $0.id == preferences.browserAddressDefaultKeyboardId } + return InputSource.resolvePersistedIdentifier(preferences.browserAddressDefaultKeyboardId) } } diff --git a/Input Source Pro/Persistence/AppRule.swift b/Input Source Pro/Persistence/AppRule.swift index f2b622b..ac979cf 100644 --- a/Input Source Pro/Persistence/AppRule.swift +++ b/Input Source Pro/Persistence/AppRule.swift @@ -11,9 +11,7 @@ extension AppRule { extension AppRule { @MainActor var forcedKeyboard: InputSource? { - guard let inputSourceId = inputSourceId else { return nil } - - return InputSource.sources.first { $0.id == inputSourceId } + return InputSource.resolvePersistedIdentifier(inputSourceId) } var functionKeyMode: FKeyMode? { diff --git a/Input Source Pro/Persistence/BrowserRule.swift b/Input Source Pro/Persistence/BrowserRule.swift index d6579bc..e5d16ec 100644 --- a/Input Source Pro/Persistence/BrowserRule.swift +++ b/Input Source Pro/Persistence/BrowserRule.swift @@ -24,9 +24,7 @@ enum BrowserRuleType: Int32, CaseIterable { extension BrowserRule { @MainActor var forcedKeyboard: InputSource? { - guard let inputSourceId = inputSourceId else { return nil } - - return InputSource.sources.first { $0.id == inputSourceId } + return InputSource.resolvePersistedIdentifier(inputSourceId) } var type: BrowserRuleType { diff --git a/Input Source Pro/UI/Components/BrowserRuleEditView.swift b/Input Source Pro/UI/Components/BrowserRuleEditView.swift index fad3f4c..224d9c0 100644 --- a/Input Source Pro/UI/Components/BrowserRuleEditView.swift +++ b/Input Source Pro/UI/Components/BrowserRuleEditView.swift @@ -24,7 +24,9 @@ struct BrowserRuleEditView: View { var inputSourceItems: [PickerItem] { [PickerItem.empty] - + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) } + + InputSource.sources.map { + PickerItem(id: $0.persistentIdentifier, title: $0.name, toolTip: $0.persistentIdentifier) + } } var restoreStrategyItems: [PickerItem] { @@ -199,7 +201,11 @@ struct BrowserRuleEditView: View { hideIndicator = rule?.hideIndicator ?? false if let inputSource = rule?.forcedKeyboard { - inputSourceItem = PickerItem(id: inputSource.id, title: inputSource.name, toolTip: inputSource.id) + inputSourceItem = PickerItem( + id: inputSource.persistentIdentifier, + title: inputSource.name, + toolTip: inputSource.persistentIdentifier + ) } if let keyboardRestoreStrategy = rule?.keyboardRestoreStrategy { diff --git a/Input Source Pro/UI/Components/RulesApplicationDetail.swift b/Input Source Pro/UI/Components/RulesApplicationDetail.swift index d97e77e..d105a42 100644 --- a/Input Source Pro/UI/Components/RulesApplicationDetail.swift +++ b/Input Source Pro/UI/Components/RulesApplicationDetail.swift @@ -27,7 +27,7 @@ struct ApplicationDetail: View { @State var functionKeyModeItem: PickerItem? var mixed: Bool { - Set(selectedApp.map { $0.forcedKeyboard?.id }).count > 1 + Set(selectedApp.map { $0.forcedKeyboard?.persistentIdentifier }).count > 1 } var isFunctionKeyModeMixed: Bool { @@ -36,7 +36,9 @@ struct ApplicationDetail: View { var items: [PickerItem] { [mixed ? PickerItem.mixed : nil, PickerItem.empty].compactMap { $0 } - + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) } + + InputSource.sources.map { + PickerItem(id: $0.persistentIdentifier, title: $0.name, toolTip: $0.persistentIdentifier) + } } var functionKeyItems: [PickerItem] { @@ -208,7 +210,11 @@ struct ApplicationDetail: View { if mixed { forceKeyboard = PickerItem.mixed } else if let keyboard = selectedApp.first?.forcedKeyboard { - forceKeyboard = PickerItem(id: keyboard.id, title: keyboard.name, toolTip: keyboard.id) + forceKeyboard = PickerItem( + id: keyboard.persistentIdentifier, + title: keyboard.name, + toolTip: keyboard.persistentIdentifier + ) } else { forceKeyboard = PickerItem.empty } diff --git a/Input Source Pro/UI/Screens/BrowserRulesSettingsView.swift b/Input Source Pro/UI/Screens/BrowserRulesSettingsView.swift index e782765..9aec8c7 100644 --- a/Input Source Pro/UI/Screens/BrowserRulesSettingsView.swift +++ b/Input Source Pro/UI/Screens/BrowserRulesSettingsView.swift @@ -18,7 +18,9 @@ struct BrowserRulesSettingsView: View { var inputSourceItems: [PickerItem] { [PickerItem.empty] - + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) } + + InputSource.sources.map { + PickerItem(id: $0.persistentIdentifier, title: $0.name, toolTip: $0.persistentIdentifier) + } } var body: some View { @@ -39,7 +41,9 @@ struct BrowserRulesSettingsView: View { PopUpButtonPicker( items: inputSourceItems, width: 150, - isItemSelected: { $0?.id == preferencesVM.preferences.browserAddressDefaultKeyboardId }, + isItemSelected: { + $0?.id == (preferencesVM.browserAddressDefaultKeyboard?.persistentIdentifier ?? PickerItem.empty.id) + }, getTitle: { $0?.title ?? "" }, getToolTip: { $0?.toolTip }, onSelect: handleBrowserAddressDefaultKeyboardSelect diff --git a/Input Source Pro/UI/Screens/GeneralSettingsView.swift b/Input Source Pro/UI/Screens/GeneralSettingsView.swift index 4fdc59e..cbd0ba5 100644 --- a/Input Source Pro/UI/Screens/GeneralSettingsView.swift +++ b/Input Source Pro/UI/Screens/GeneralSettingsView.swift @@ -8,7 +8,9 @@ struct GeneralSettingsView: View { var items: [PickerItem] { [PickerItem.empty] - + InputSource.sources.map { PickerItem(id: $0.id, title: $0.name, toolTip: $0.id) } + + InputSource.sources.map { + PickerItem(id: $0.persistentIdentifier, title: $0.name, toolTip: $0.persistentIdentifier) + } } var body: some View { @@ -41,7 +43,9 @@ struct GeneralSettingsView: View { PopUpButtonPicker( items: items, - isItemSelected: { $0?.id == preferencesVM.preferences.systemWideDefaultKeyboardId }, + isItemSelected: { + $0?.id == (preferencesVM.systemWideDefaultKeyboard?.persistentIdentifier ?? PickerItem.empty.id) + }, getTitle: { $0?.title ?? "" }, getToolTip: { $0?.toolTip }, onSelect: handleSystemWideDefaultKeyboardSelect diff --git a/Input Source Pro/Utilities/AppKeyboardCache.swift b/Input Source Pro/Utilities/AppKeyboardCache.swift index d2dc46b..b511ba4 100644 --- a/Input Source Pro/Utilities/AppKeyboardCache.swift +++ b/Input Source Pro/Utilities/AppKeyboardCache.swift @@ -18,7 +18,7 @@ class AppKeyboardCache { func save(_ kind: AppKind, keyboard: InputSource?) { guard let id = kind.getId() else { return } - if let keyboardId = keyboard?.id { + if let keyboardId = keyboard?.persistentIdentifier { logger.debug { "Save \(id)#\(keyboardId)" } cache[id] = keyboardId } @@ -31,7 +31,7 @@ class AppKeyboardCache { logger.debug { "Retrieve \(id)#\(keyboardId)" } - return InputSource.sources.first { $0.id == keyboardId } + return InputSource.resolvePersistedIdentifier(keyboardId) } func clear() { diff --git a/Input Source Pro/Utilities/AppKit/AppRuleMenuItem.swift b/Input Source Pro/Utilities/AppKit/AppRuleMenuItem.swift index e6b6b02..78e9f59 100644 --- a/Input Source Pro/Utilities/AppKit/AppRuleMenuItem.swift +++ b/Input Source Pro/Utilities/AppKit/AppRuleMenuItem.swift @@ -31,7 +31,7 @@ class AppRuleMenuItem: NSMenuItem { } @objc func forceKeyboard(_: Any) { - let inputSourceId = inputSource?.id ?? "" + let inputSourceId = inputSource?.persistentIdentifier ?? "" if let appCustomization = appCustomization { preferencesVM.setForceKeyboard(appCustomization, inputSourceId) @@ -56,6 +56,6 @@ class AppRuleMenuItem: NSMenuItem { } func updateState() { - state = appCustomization?.inputSourceId == inputSource?.id ? .on : .off + state = appCustomization?.forcedKeyboard?.persistentIdentifier == inputSource?.persistentIdentifier ? .on : .off } } diff --git a/Input Source Pro/Utilities/AppKit/BrowserRuleMenuItem.swift b/Input Source Pro/Utilities/AppKit/BrowserRuleMenuItem.swift index 64b26c6..014680e 100644 --- a/Input Source Pro/Utilities/AppKit/BrowserRuleMenuItem.swift +++ b/Input Source Pro/Utilities/AppKit/BrowserRuleMenuItem.swift @@ -31,7 +31,7 @@ class BrowserRuleMenuItem: NSMenuItem { } @objc func forceKeyboard(_: Any) { - let inputSourceId = inputSource?.id ?? "" + let inputSourceId = inputSource?.persistentIdentifier ?? "" let host = url.host ?? "" if let browserRule = browserRule { @@ -69,6 +69,6 @@ class BrowserRuleMenuItem: NSMenuItem { } func updateState() { - state = browserRule?.inputSourceId == inputSource?.id ? .on : .off + state = browserRule?.forcedKeyboard?.persistentIdentifier == inputSource?.persistentIdentifier ? .on : .off } } diff --git a/Input Source Pro/Utilities/InputSource/InputSource.swift b/Input Source Pro/Utilities/InputSource/InputSource.swift index ff43d66..d1ca6ae 100644 --- a/Input Source Pro/Utilities/InputSource/InputSource.swift +++ b/Input Source Pro/Utilities/InputSource/InputSource.swift @@ -4,6 +4,8 @@ import CryptoKit @MainActor class InputSource { + private static let persistentIdentifierSeparator = "::" + static let logger = ISPLogger( category: "🤖 " + String(describing: InputSource.self), disabled: true @@ -16,6 +18,13 @@ class InputSource { var id: String { tisInputSource.id } var name: String { tisInputSource.name } var inputModeID: String? { tisInputSource.inputModeID } + var persistentIdentifier: String { + if let inputModeID = normalizedInputModeID { + return "\(id)\(Self.persistentIdentifierSeparator)\(inputModeID)" + } + + return id + } var isCJKVR: Bool { guard let lang = tisInputSource.sourceLanguages.first else { return false } @@ -64,11 +73,16 @@ class InputSource { func select(useCJKVFix: Bool) { InputSourceSwitcher.switchToInputSource(self, useCJKVFix: useCJKVFix) } + + private var normalizedInputModeID: String? { + guard let inputModeID, !inputModeID.isEmpty else { return nil } + return inputModeID + } } extension InputSource: @preconcurrency Equatable { static func == (lhs: InputSource, rhs: InputSource) -> Bool { - return lhs.id == rhs.id + return lhs.persistentIdentifier == rhs.persistentIdentifier } } @@ -79,6 +93,47 @@ extension InputSource { static func getCurrentInputSource() -> InputSource { return InputSource(tisInputSource: TISCopyCurrentKeyboardInputSource().takeRetainedValue()) } + + static func resolvePersistedIdentifier(_ persistedIdentifier: String?) -> InputSource? { + guard let persistedIdentifier, !persistedIdentifier.isEmpty else { return nil } + + let sources = Self.sources + let (sourceID, inputModeID) = splitPersistedIdentifier(persistedIdentifier) + + if let inputModeID { + if let exactMatch = sources.first(where: { $0.persistentIdentifier == persistedIdentifier }) { + return exactMatch + } + + if let modeMatch = sources.first(where: { $0.inputModeID == inputModeID }) { + return modeMatch + } + } + + if let modeMatch = sources.first(where: { $0.inputModeID == persistedIdentifier }) { + return modeMatch + } + + let matches = sources.filter { $0.id == sourceID } + + guard !matches.isEmpty else { return nil } + + if let inputModeID, + let modeMatch = matches.first(where: { $0.inputModeID == inputModeID }) + { + return modeMatch + } + + if matches.count > 1 { + logger.debug { "Ambiguous persisted input source identifier \(persistedIdentifier); preferring an input-mode match." } + } + + if let preferredModeMatch = matches.first(where: { $0.inputModeID != nil }) { + return preferredModeMatch + } + + return matches.first + } } extension InputSource { @@ -98,6 +153,18 @@ extension InputSource { static func anotherCJKVSource(current: InputSource) -> InputSource? { return sources.first(where: { $0 != current && $0.isCJKVR }) } + + private static func splitPersistedIdentifier(_ persistedIdentifier: String) -> (sourceID: String, inputModeID: String?) { + let components = persistedIdentifier.components(separatedBy: persistentIdentifierSeparator) + + guard components.count >= 2 else { + return (persistedIdentifier, nil) + } + + let sourceID = components[0] + let inputModeID = components.dropFirst().joined(separator: persistentIdentifierSeparator) + return (sourceID, inputModeID.isEmpty ? nil : inputModeID) + } } private extension URL { @@ -116,6 +183,6 @@ private extension URL { extension InputSource: @preconcurrency CustomStringConvertible { var description: String { - id + persistentIdentifier } }