From df5075972a5368b28ce788c3c5bff2ad7cd8f8e7 Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Sat, 10 Feb 2024 13:44:43 +0800 Subject: [PATCH] IMKHelper // Fix a TISInputSource installation crash in macOS 10.9. --- Installer/InstallerShared.swift | 2 +- .../Sources/IMKUtils/IMKHelper.swift | 42 +++++++------ .../IMKUtils/TISInputSourceExtension.swift | 59 +++++++++++++++---- .../MainAssembly/PrefMgr_Extension.swift | 5 +- .../SettingsUI/UserDefRenderableImpl.swift | 4 +- .../UserDef/UserDefRenderableCocoa.swift | 4 +- 6 files changed, 76 insertions(+), 40 deletions(-) diff --git a/Installer/InstallerShared.swift b/Installer/InstallerShared.swift index 5411745a..e3e52478 100644 --- a/Installer/InstallerShared.swift +++ b/Installer/InstallerShared.swift @@ -50,7 +50,7 @@ var allRegisteredInstancesOfThisInputMethod: [TISInputSource] { else { return [] } - return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) } + return TISInputSource.match(modeIDs: tsInputModeListKey.keys.map(\.description)) } // MARK: - NSApp Activation Helper diff --git a/Packages/vChewing_IMKUtils/Sources/IMKUtils/IMKHelper.swift b/Packages/vChewing_IMKUtils/Sources/IMKUtils/IMKHelper.swift index 19358781..2443e081 100644 --- a/Packages/vChewing_IMKUtils/Sources/IMKUtils/IMKHelper.swift +++ b/Packages/vChewing_IMKUtils/Sources/IMKUtils/IMKHelper.swift @@ -27,7 +27,7 @@ public enum IMKHelper { "com.apple.keylayout.Dvorak-Right", ] if #unavailable(macOS 10.13) { - result.append("com.apple.keylayout.US") + result.insert("com.apple.keylayout.US", at: result.startIndex) result.append("com.apple.keylayout.German") result.append("com.apple.keylayout.French") } @@ -49,31 +49,29 @@ public enum IMKHelper { "org.unknown.keylayout.vChewingMiTAC", ] - public static var allowedAlphanumericalTISInputSources: [TISInputSource] { - arrWhitelistedKeyLayoutsASCII.compactMap { TISInputSource.generate(from: $0) } + public static var allowedAlphanumericalTISInputSources: [TISInputSource.KeyboardLayout] { + let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap() + return arrWhitelistedKeyLayoutsASCII.compactMap { allTISKeyboardLayouts[$0] } } - public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource?] { - // 為了保證清單順序,先弄兩個容器。 - var containerA: [TISInputSource?] = [] - var containerB: [TISInputSource?] = [] - var containerC: [TISInputSource?] = [] + public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource.KeyboardLayout?] { + let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap() + // 為了保證清單順序,先弄幾個容器。 + var containerA: [TISInputSource.KeyboardLayout?] = [] + var containerB: [TISInputSource.KeyboardLayout?] = [] + var containerC: [TISInputSource.KeyboardLayout] = [] - let rawDictionary = TISInputSource.rawTISInputSources(onlyASCII: false) - - Self.arrWhitelistedKeyLayoutsASCII.forEach { - if let neta = rawDictionary[$0], !arrDynamicBasicKeyLayouts.contains(neta.identifier) { - containerC.append(neta) - } + let filterSet = Array(Set(arrWhitelistedKeyLayoutsASCII).subtracting(Set(arrDynamicBasicKeyLayouts))) + let matchedGroupBasic = (arrWhitelistedKeyLayoutsASCII + arrDynamicBasicKeyLayouts).compactMap { + allTISKeyboardLayouts[$0] } - - Self.arrDynamicBasicKeyLayouts.forEach { - if let neta = rawDictionary[$0] { - if neta.identifier.contains("com.apple") { - containerA.append(neta) - } else { - containerB.append(neta) - } + matchedGroupBasic.forEach { neta in + if filterSet.contains(neta.id) { + containerC.append(neta) + } else if neta.id.hasPrefix("com.apple") { + containerA.append(neta) + } else { + containerB.append(neta) } } diff --git a/Packages/vChewing_IMKUtils/Sources/IMKUtils/TISInputSourceExtension.swift b/Packages/vChewing_IMKUtils/Sources/IMKUtils/TISInputSourceExtension.swift index c6112b3c..6fe5984a 100644 --- a/Packages/vChewing_IMKUtils/Sources/IMKUtils/TISInputSourceExtension.swift +++ b/Packages/vChewing_IMKUtils/Sources/IMKUtils/TISInputSourceExtension.swift @@ -12,8 +12,13 @@ import InputMethodKit // MARK: - TISInputSource Extension by The vChewing Project (MIT-NTL License). public extension TISInputSource { + struct KeyboardLayout: Identifiable { + public var id: String + public var titleLocalized: String + } + static var allRegisteredInstancesOfThisInputMethod: [TISInputSource] { - TISInputSource.modes.compactMap { TISInputSource.generate(from: $0) } + TISInputSource.match(modeIDs: TISInputSource.modes) } static var modes: [String] { @@ -22,7 +27,7 @@ public extension TISInputSource { else { return [] } - return tsInputModeListKey.keys.map { $0 } + return tsInputModeListKey.keys.map(\.description) } @discardableResult static func registerInputMethod() -> Bool { @@ -80,10 +85,6 @@ public extension TISInputSource { == kCFBooleanTrue } - static func generate(from identifier: String) -> TISInputSource? { - TISInputSource.rawTISInputSources(onlyASCII: false)[identifier] - } - var inputModeID: String { unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputModeID), to: NSString.self) as String? ?? "" } @@ -120,9 +121,34 @@ public extension TISInputSource { return unsafeBitCast(r, to: NSString.self).integerValue as Int? ?? 0 } - static func rawTISInputSources(onlyASCII: Bool = false) -> [String: TISInputSource] { + // Refactored by Shiki Suen. + static func match(identifiers: [String] = [], modeIDs: [String] = [], onlyASCII: Bool = false) -> [TISInputSource] { + let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [ + kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString, + kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean, + ] + let cfDict = !onlyASCII ? nil : dicConditions as CFDictionary + var resultStack: [TISInputSource] = [] + let unionedIDs = NSOrderedSet(array: modeIDs + identifiers).compactMap { $0 as? String } + let retrieved = (TISCreateInputSourceList(cfDict, true)?.takeRetainedValue() as? [TISInputSource]) ?? [] + retrieved.forEach { tis in + unionedIDs.forEach { id in + guard tis.identifier == id || tis.inputModeID == id else { return } + if onlyASCII { + guard tis.scriptCode == 0 else { return } + } + resultStack.append(tis) + } + } + // 為了保持指定排序,才在最後做這種處理。效能略有打折,但至少比起直接迭代容量破百的 retrieved 要好多了。 + return unionedIDs.compactMap { currentIdentifier in + retrieved.first { $0.identifier == currentIdentifier || $0.inputModeID == currentIdentifier } + } + } + + /// 備註:這是 Mzp 的原版函式,留在這裡當範本參考。上述的 .match() 函式都衍生自此。 + static func rawTISInputSources(onlyASCII: Bool = false) -> [TISInputSource] { // 為了指定檢索條件,先構築 CFDictionary 辭典。 - // 第二項代指辭典容量。 let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [ kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString, kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean, @@ -132,10 +158,21 @@ public extension TISInputSource { if onlyASCII { result = result.filter { $0.scriptCode == 0 } } - var resultDictionary: [String: TISInputSource] = [:] + return result + } + + /// Derived from rawTISInputSources(). + static func getAllTISInputKeyboardLayoutMap() -> [String: TISInputSource.KeyboardLayout] { + // 為了指定檢索條件,先構築 CFDictionary 辭典。 + let dicConditions: [CFString: Any] = [kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString] + // 返回鍵盤配列清單。 + let result = TISCreateInputSourceList(dicConditions as CFDictionary, true)?.takeRetainedValue() as? [TISInputSource] ?? .init() + var resultDictionary: [String: TISInputSource.KeyboardLayout] = [:] result.forEach { - resultDictionary[$0.inputModeID] = $0 - resultDictionary[$0.identifier] = $0 + let newNeta1 = TISInputSource.KeyboardLayout(id: $0.inputModeID, titleLocalized: $0.vChewingLocalizedName) + let newNeta2 = TISInputSource.KeyboardLayout(id: $0.identifier, titleLocalized: $0.vChewingLocalizedName) + resultDictionary[$0.inputModeID] = newNeta1 + resultDictionary[$0.identifier] = newNeta2 } return resultDictionary } diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/PrefMgr_Extension.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/PrefMgr_Extension.swift index fbf4dc3d..d2f8282f 100644 --- a/Packages/vChewing_MainAssembly/Sources/MainAssembly/PrefMgr_Extension.swift +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/PrefMgr_Extension.swift @@ -51,10 +51,11 @@ public extension PrefMgr { keyboardParser = 0 } // 基礎鍵盤排列選項糾錯。 - if TISInputSource.generate(from: basicKeyboardLayout) == nil { + let matchedResults = TISInputSource.match(identifiers: [basicKeyboardLayout, alphanumericalKeyboardLayout]) + if !matchedResults.contains(where: { $0.identifier == basicKeyboardLayout }) { basicKeyboardLayout = Self.kDefaultBasicKeyboardLayout } - if TISInputSource.generate(from: alphanumericalKeyboardLayout) == nil { + if !matchedResults.contains(where: { $0.identifier == alphanumericalKeyboardLayout }) { alphanumericalKeyboardLayout = Self.kDefaultAlphanumericalKeyboardLayout } // 其它多元選項參數自動糾錯。 diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/Settings/SettingsUI/UserDefRenderableImpl.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/Settings/SettingsUI/UserDefRenderableImpl.swift index 47d73299..3ff0970b 100644 --- a/Packages/vChewing_MainAssembly/Sources/MainAssembly/Settings/SettingsUI/UserDefRenderableImpl.swift +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/Settings/SettingsUI/UserDefRenderableImpl.swift @@ -41,7 +41,7 @@ public extension UserDefRenderable { Picker(LocalizedStringKey(metaData.shortTitle ?? ""), selection: binding) { ForEach(0 ... (IMKHelper.allowedAlphanumericalTISInputSources.count - 1), id: \.self) { id in let theEntry = IMKHelper.allowedAlphanumericalTISInputSources[id] - Text(theEntry.vChewingLocalizedName).tag(theEntry.identifier) + Text(theEntry.titleLocalized).tag(theEntry.id) }.id(UUID()) } case (.string, .kBasicKeyboardLayout): @@ -49,7 +49,7 @@ public extension UserDefRenderable { ForEach(0 ... (IMKHelper.allowedBasicLayoutsAsTISInputSources.count - 1), id: \.self) { id in let theEntry = IMKHelper.allowedBasicLayoutsAsTISInputSources[id] if let theEntry = theEntry { - Text(theEntry.vChewingLocalizedName).tag(theEntry.identifier) + Text(theEntry.titleLocalized).tag(theEntry.id) } else { Divider() } diff --git a/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift b/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift index f1d60a2c..fe396361 100644 --- a/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift +++ b/Packages/vChewing_Shared/Sources/Shared/UserDef/UserDefRenderableCocoa.swift @@ -41,7 +41,7 @@ public class UserDefRenderableCocoa: NSObject, Identifiable { checkDef: switch def { case .kAlphanumericalKeyboardLayout: IMKHelper.allowedAlphanumericalTISInputSources.forEach { currentTIS in - objOptions.append((currentTIS.identifier, currentTIS.vChewingLocalizedName)) + objOptions.append((currentTIS.id, currentTIS.titleLocalized)) } optionsLocalizedAsIdentifiables = objOptions case .kBasicKeyboardLayout: @@ -50,7 +50,7 @@ public class UserDefRenderableCocoa: NSObject, Identifiable { objOptions.append(nil) return } - objOptions.append((currentTIS.identifier, currentTIS.vChewingLocalizedName)) + objOptions.append((currentTIS.id, currentTIS.titleLocalized)) } optionsLocalizedAsIdentifiables = objOptions case .kKeyboardParser: