vChewing-macOS/Packages/vChewing_IMKUtils/Sources/IMKUtils/TISInputSourceExtension.swift

180 lines
7.0 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (c) 2021 and onwards The vChewing Project (MIT-NTL License).
// ====================
// This code is released under the MIT license (SPDX-License-Identifier: MIT)
// ... with NTL restriction stating that:
// No trademark license is granted to use the trade names, trademarks, service
// marks, or product names of Contributor, except as required to fulfill notice
// requirements defined in MIT License.
import Foundation
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.match(modeIDs: TISInputSource.modes)
}
static var modes: [String] {
guard let components = Bundle.main.infoDictionary?["ComponentInputModeDict"] as? [String: Any],
let tsInputModeListKey = components["tsInputModeListKey"] as? [String: Any]
else {
return []
}
return tsInputModeListKey.keys.map(\.description)
}
@discardableResult static func registerInputMethod() -> Bool {
let instances = TISInputSource.allRegisteredInstancesOfThisInputMethod
if instances.isEmpty {
//
NSLog("Registering input source.")
if !TISInputSource.registerInputSource() {
NSLog("Input source registration failed.")
return false
}
}
var succeeded = true
instances.forEach {
NSLog("Enabling input source: \($0.identifier)")
if !$0.activate() {
NSLog("Failed from enabling input source: \($0.identifier)")
succeeded = false
}
}
return succeeded
}
@discardableResult static func registerInputSource() -> Bool {
TISRegisterInputSource(Bundle.main.bundleURL as CFURL) == noErr
}
@discardableResult func activate() -> Bool {
TISEnableInputSource(self) == noErr
}
@discardableResult func select() -> Bool {
if !isSelectable {
NSLog("Non-selectable: \(identifier)")
return false
}
if TISSelectInputSource(self) != noErr {
NSLog("Failed from switching to \(identifier)")
return false
}
return true
}
@discardableResult func deactivate() -> Bool {
TISDisableInputSource(self) == noErr
}
var isActivated: Bool {
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputSourceIsEnabled), to: CFBoolean.self)
== kCFBooleanTrue
}
var isSelectable: Bool {
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputSourceIsSelectCapable), to: CFBoolean.self)
== kCFBooleanTrue
}
var inputModeID: String {
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputModeID), to: NSString.self) as String? ?? ""
}
var vChewingLocalizedName: String {
switch identifier {
case "com.apple.keylayout.ZhuyinBopomofo":
return NSLocalizedString("Apple Zhuyin Bopomofo (Dachen)", comment: "")
case "com.apple.keylayout.ZhuyinEten":
return NSLocalizedString("Apple Zhuyin Eten (Traditional)", comment: "")
default: return localizedName
}
}
}
// MARK: - TISInputSource Extension by Mizuno Hiroki (a.k.a. "Mzp") (MIT License)
// Ref: Original source codes are written in Swift 4 from Mzp's InputMethodKit textbook.
// Note: Slightly modified by vChewing Project: Using Dictionaries when necessary.
public extension TISInputSource {
var localizedName: String {
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyLocalizedName), to: NSString.self) as String? ?? ""
}
var identifier: String {
unsafeBitCast(TISGetInputSourceProperty(self, kTISPropertyInputSourceID), to: NSString.self) as String? ?? ""
}
var scriptCode: Int {
// Shiki's note: There is no "kTISPropertyScriptCode" in TextInputSources.h file.
// Using Mzp's latest solution in his blog: https://mzp.hatenablog.com/entry/2018/07/16/212026
let r = TISGetInputSourceProperty(self, "TSMInputSourcePropertyScriptCode" as CFString)
return unsafeBitCast(r, to: NSString.self).integerValue as Int? ?? 0
}
// 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,
]
//
var result = TISCreateInputSourceList(dicConditions as CFDictionary, true)?.takeRetainedValue() as? [TISInputSource] ?? .init()
if onlyASCII {
result = result.filter { $0.scriptCode == 0 }
}
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 {
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
}
}