IMKHelper // Fix a TISInputSource installation crash in macOS 10.9.

This commit is contained in:
ShikiSuen 2024-02-10 13:44:43 +08:00
parent cfad082b14
commit df5075972a
6 changed files with 76 additions and 40 deletions

View File

@ -50,7 +50,7 @@ var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
else { else {
return [] return []
} }
return tsInputModeListKey.keys.compactMap { TISInputSource.generate(from: $0) } return TISInputSource.match(modeIDs: tsInputModeListKey.keys.map(\.description))
} }
// MARK: - NSApp Activation Helper // MARK: - NSApp Activation Helper

View File

@ -27,7 +27,7 @@ public enum IMKHelper {
"com.apple.keylayout.Dvorak-Right", "com.apple.keylayout.Dvorak-Right",
] ]
if #unavailable(macOS 10.13) { 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.German")
result.append("com.apple.keylayout.French") result.append("com.apple.keylayout.French")
} }
@ -49,31 +49,29 @@ public enum IMKHelper {
"org.unknown.keylayout.vChewingMiTAC", "org.unknown.keylayout.vChewingMiTAC",
] ]
public static var allowedAlphanumericalTISInputSources: [TISInputSource] { public static var allowedAlphanumericalTISInputSources: [TISInputSource.KeyboardLayout] {
arrWhitelistedKeyLayoutsASCII.compactMap { TISInputSource.generate(from: $0) } let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap()
return arrWhitelistedKeyLayoutsASCII.compactMap { allTISKeyboardLayouts[$0] }
} }
public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource?] { public static var allowedBasicLayoutsAsTISInputSources: [TISInputSource.KeyboardLayout?] {
// let allTISKeyboardLayouts = TISInputSource.getAllTISInputKeyboardLayoutMap()
var containerA: [TISInputSource?] = [] //
var containerB: [TISInputSource?] = [] var containerA: [TISInputSource.KeyboardLayout?] = []
var containerC: [TISInputSource?] = [] var containerB: [TISInputSource.KeyboardLayout?] = []
var containerC: [TISInputSource.KeyboardLayout] = []
let rawDictionary = TISInputSource.rawTISInputSources(onlyASCII: false) let filterSet = Array(Set(arrWhitelistedKeyLayoutsASCII).subtracting(Set(arrDynamicBasicKeyLayouts)))
let matchedGroupBasic = (arrWhitelistedKeyLayoutsASCII + arrDynamicBasicKeyLayouts).compactMap {
Self.arrWhitelistedKeyLayoutsASCII.forEach { allTISKeyboardLayouts[$0]
if let neta = rawDictionary[$0], !arrDynamicBasicKeyLayouts.contains(neta.identifier) {
containerC.append(neta)
}
} }
matchedGroupBasic.forEach { neta in
Self.arrDynamicBasicKeyLayouts.forEach { if filterSet.contains(neta.id) {
if let neta = rawDictionary[$0] { containerC.append(neta)
if neta.identifier.contains("com.apple") { } else if neta.id.hasPrefix("com.apple") {
containerA.append(neta) containerA.append(neta)
} else { } else {
containerB.append(neta) containerB.append(neta)
}
} }
} }

View File

@ -12,8 +12,13 @@ import InputMethodKit
// MARK: - TISInputSource Extension by The vChewing Project (MIT-NTL License). // MARK: - TISInputSource Extension by The vChewing Project (MIT-NTL License).
public extension TISInputSource { public extension TISInputSource {
struct KeyboardLayout: Identifiable {
public var id: String
public var titleLocalized: String
}
static var allRegisteredInstancesOfThisInputMethod: [TISInputSource] { static var allRegisteredInstancesOfThisInputMethod: [TISInputSource] {
TISInputSource.modes.compactMap { TISInputSource.generate(from: $0) } TISInputSource.match(modeIDs: TISInputSource.modes)
} }
static var modes: [String] { static var modes: [String] {
@ -22,7 +27,7 @@ public extension TISInputSource {
else { else {
return [] return []
} }
return tsInputModeListKey.keys.map { $0 } return tsInputModeListKey.keys.map(\.description)
} }
@discardableResult static func registerInputMethod() -> Bool { @discardableResult static func registerInputMethod() -> Bool {
@ -80,10 +85,6 @@ public extension TISInputSource {
== kCFBooleanTrue == kCFBooleanTrue
} }
static func generate(from identifier: String) -> TISInputSource? {
TISInputSource.rawTISInputSources(onlyASCII: false)[identifier]
}
var inputModeID: String { var inputModeID: String {
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputModeID), to: NSString.self) as 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 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 // CFDictionary
//
let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [ let dicConditions: [CFString: Any] = !onlyASCII ? [:] : [
kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString, kTISPropertyInputSourceType: kTISTypeKeyboardLayout as CFString,
kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean, kTISPropertyInputSourceIsASCIICapable: kCFBooleanTrue as CFBoolean,
@ -132,10 +158,21 @@ public extension TISInputSource {
if onlyASCII { if onlyASCII {
result = result.filter { $0.scriptCode == 0 } 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 { result.forEach {
resultDictionary[$0.inputModeID] = $0 let newNeta1 = TISInputSource.KeyboardLayout(id: $0.inputModeID, titleLocalized: $0.vChewingLocalizedName)
resultDictionary[$0.identifier] = $0 let newNeta2 = TISInputSource.KeyboardLayout(id: $0.identifier, titleLocalized: $0.vChewingLocalizedName)
resultDictionary[$0.inputModeID] = newNeta1
resultDictionary[$0.identifier] = newNeta2
} }
return resultDictionary return resultDictionary
} }

View File

@ -51,10 +51,11 @@ public extension PrefMgr {
keyboardParser = 0 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 basicKeyboardLayout = Self.kDefaultBasicKeyboardLayout
} }
if TISInputSource.generate(from: alphanumericalKeyboardLayout) == nil { if !matchedResults.contains(where: { $0.identifier == alphanumericalKeyboardLayout }) {
alphanumericalKeyboardLayout = Self.kDefaultAlphanumericalKeyboardLayout alphanumericalKeyboardLayout = Self.kDefaultAlphanumericalKeyboardLayout
} }
// //

View File

@ -41,7 +41,7 @@ public extension UserDefRenderable<String> {
Picker(LocalizedStringKey(metaData.shortTitle ?? ""), selection: binding) { Picker(LocalizedStringKey(metaData.shortTitle ?? ""), selection: binding) {
ForEach(0 ... (IMKHelper.allowedAlphanumericalTISInputSources.count - 1), id: \.self) { id in ForEach(0 ... (IMKHelper.allowedAlphanumericalTISInputSources.count - 1), id: \.self) { id in
let theEntry = IMKHelper.allowedAlphanumericalTISInputSources[id] let theEntry = IMKHelper.allowedAlphanumericalTISInputSources[id]
Text(theEntry.vChewingLocalizedName).tag(theEntry.identifier) Text(theEntry.titleLocalized).tag(theEntry.id)
}.id(UUID()) }.id(UUID())
} }
case (.string, .kBasicKeyboardLayout): case (.string, .kBasicKeyboardLayout):
@ -49,7 +49,7 @@ public extension UserDefRenderable<String> {
ForEach(0 ... (IMKHelper.allowedBasicLayoutsAsTISInputSources.count - 1), id: \.self) { id in ForEach(0 ... (IMKHelper.allowedBasicLayoutsAsTISInputSources.count - 1), id: \.self) { id in
let theEntry = IMKHelper.allowedBasicLayoutsAsTISInputSources[id] let theEntry = IMKHelper.allowedBasicLayoutsAsTISInputSources[id]
if let theEntry = theEntry { if let theEntry = theEntry {
Text(theEntry.vChewingLocalizedName).tag(theEntry.identifier) Text(theEntry.titleLocalized).tag(theEntry.id)
} else { } else {
Divider() Divider()
} }

View File

@ -41,7 +41,7 @@ public class UserDefRenderableCocoa: NSObject, Identifiable {
checkDef: switch def { checkDef: switch def {
case .kAlphanumericalKeyboardLayout: case .kAlphanumericalKeyboardLayout:
IMKHelper.allowedAlphanumericalTISInputSources.forEach { currentTIS in IMKHelper.allowedAlphanumericalTISInputSources.forEach { currentTIS in
objOptions.append((currentTIS.identifier, currentTIS.vChewingLocalizedName)) objOptions.append((currentTIS.id, currentTIS.titleLocalized))
} }
optionsLocalizedAsIdentifiables = objOptions optionsLocalizedAsIdentifiables = objOptions
case .kBasicKeyboardLayout: case .kBasicKeyboardLayout:
@ -50,7 +50,7 @@ public class UserDefRenderableCocoa: NSObject, Identifiable {
objOptions.append(nil) objOptions.append(nil)
return return
} }
objOptions.append((currentTIS.identifier, currentTIS.vChewingLocalizedName)) objOptions.append((currentTIS.id, currentTIS.titleLocalized))
} }
optionsLocalizedAsIdentifiables = objOptions optionsLocalizedAsIdentifiables = objOptions
case .kKeyboardParser: case .kKeyboardParser: