Repo // Deprecating Zonble's InputState, using IMEState instead.
- This allows adding user phrases with high-unicode chars like emojis. - AUTHORS // Remove attribution of Zonble's InputState.
This commit is contained in:
parent
a5fa288f97
commit
5f89fdf9b2
2
AUTHORS
2
AUTHORS
|
@ -15,7 +15,7 @@ $ 3rd-Party Modules Used:
|
||||||
$ Contributors and volunteers of the upstream repo, having no responsibility in discussing anything in the current repo:
|
$ Contributors and volunteers of the upstream repo, having no responsibility in discussing anything in the current repo:
|
||||||
|
|
||||||
- Zonble Yang:
|
- Zonble Yang:
|
||||||
- McBopomofo for macOS 2.x architect, especially state-based IME behavior management.
|
- McBopomofo for macOS 2.x architect.
|
||||||
- Voltaire candidate window MK2 (massively modified as MK3 in vChewing by Shiki Suen).
|
- Voltaire candidate window MK2 (massively modified as MK3 in vChewing by Shiki Suen).
|
||||||
- Notifier window and Tooltip UI.
|
- Notifier window and Tooltip UI.
|
||||||
- NSStringUtils and FSEventStreamHelper.
|
- NSStringUtils and FSEventStreamHelper.
|
||||||
|
|
|
@ -9,39 +9,83 @@
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
// 用以讓每個狀態自描述的 enum。
|
// 用以讓每個狀態自描述的 enum。
|
||||||
public enum StateType {
|
public enum StateType: String {
|
||||||
case ofDeactivated
|
case ofDeactivated = "Deactivated"
|
||||||
case ofEmpty
|
case ofEmpty = "Empty"
|
||||||
case ofAbortion // 該狀態會自動轉為 Empty
|
case ofAbortion = "Abortion" // 該狀態會自動轉為 Empty
|
||||||
case ofCommitting
|
case ofCommitting = "Committing"
|
||||||
case ofAssociates
|
case ofAssociates = "Associates"
|
||||||
case ofNotEmpty
|
case ofNotEmpty = "NotEmpty"
|
||||||
case ofInputting
|
case ofInputting = "Inputting"
|
||||||
case ofMarking
|
case ofMarking = "Marking"
|
||||||
case ofCandidates
|
case ofCandidates = "Candidates"
|
||||||
case ofSymbolTable
|
case ofSymbolTable = "SymbolTable"
|
||||||
}
|
}
|
||||||
|
|
||||||
// 所有 InputState 均遵守該協定:
|
// 所有 IMEState 均遵守該協定:
|
||||||
public protocol InputStateProtocol {
|
public protocol IMEStateProtocol {
|
||||||
var type: StateType { get }
|
var type: StateType { get }
|
||||||
var data: StateData { get }
|
var data: StateData { get }
|
||||||
var hasBuffer: Bool { get }
|
var candidates: [(String, String)] { get }
|
||||||
|
var hasComposition: Bool { get }
|
||||||
var isCandidateContainer: Bool { get }
|
var isCandidateContainer: Bool { get }
|
||||||
var displayedText: String { get }
|
var displayedText: String { get }
|
||||||
var textToCommit: String { get set }
|
var textToCommit: String { get set }
|
||||||
var tooltip: String { get set }
|
var tooltip: String { get set }
|
||||||
var attributedString: NSAttributedString { get }
|
var attributedString: NSAttributedString { get }
|
||||||
|
var convertedToInputting: IMEState { get }
|
||||||
|
var isFilterable: Bool { get }
|
||||||
var node: SymbolNode { get set }
|
var node: SymbolNode { get set }
|
||||||
}
|
}
|
||||||
|
|
||||||
public struct IMEState {
|
/// 用以呈現輸入法控制器(ctlInputMethod)的各種狀態。
|
||||||
|
///
|
||||||
|
/// 從實際角度來看,輸入法屬於有限態械(Finite State Machine)。其藉由滑鼠/鍵盤
|
||||||
|
/// 等輸入裝置接收輸入訊號,據此切換至對應的狀態,再根據狀態更新使用者介面內容,
|
||||||
|
/// 最終生成文字輸出、遞交給接收文字輸入行為的客體應用。此乃單向資訊流序,且使用
|
||||||
|
/// 者介面內容與文字輸出均無條件地遵循某一個指定的資料來源。
|
||||||
|
///
|
||||||
|
/// IMEState 型別用以呈現輸入法控制器正在做的事情,且分狀態儲存各種狀態限定的
|
||||||
|
/// 常數與變數。對輸入法而言,使用狀態模式(而非策略模式)來做這種常數變數隔離,
|
||||||
|
/// 可能會讓新手覺得會有些牛鼎烹雞,卻實際上變相減少了在程式維護方面的管理難度、
|
||||||
|
/// 不需要再在某個狀態下為了該狀態不需要的變數與常數的處置策略而煩惱。
|
||||||
|
///
|
||||||
|
/// 對 IMEState 型別下的諸多狀態的切換,應以生成新副本來取代舊有副本的形式來完
|
||||||
|
/// 成。唯一例外是 IMEState.Marking、擁有可以將自身轉變為 IMEState.Inputting
|
||||||
|
/// 的成員函式,但也只是生成副本、來交給輸入法控制器來處理而已。每個狀態都有
|
||||||
|
/// 各自的構造器 (Constructor)。
|
||||||
|
///
|
||||||
|
/// 輸入法控制器持下述狀態:
|
||||||
|
///
|
||||||
|
/// - .Deactivated: 使用者沒在使用輸入法。
|
||||||
|
/// - .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。因為逐字選字模式不需要在
|
||||||
|
/// 組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。
|
||||||
|
/// - .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。抑或是剛剛敲字遞交給
|
||||||
|
/// 客體應用、準備新的輸入行為。
|
||||||
|
/// - .Abortion: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些
|
||||||
|
/// 內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .Empty()。
|
||||||
|
/// - .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。
|
||||||
|
/// - .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。
|
||||||
|
/// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。
|
||||||
|
/// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、還是將這個範圍的
|
||||||
|
/// 詞音組合放入語彙濾除清單。
|
||||||
|
/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。
|
||||||
|
/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。
|
||||||
|
public struct IMEState: IMEStateProtocol {
|
||||||
public var type: StateType = .ofEmpty
|
public var type: StateType = .ofEmpty
|
||||||
public var data: StateData = .init()
|
public var data: StateData = .init()
|
||||||
|
public var node: SymbolNode = .init("")
|
||||||
init(_ data: StateData = .init(), type: StateType = .ofEmpty) {
|
init(_ data: StateData = .init(), type: StateType = .ofEmpty) {
|
||||||
self.data = data
|
self.data = data
|
||||||
self.type = type
|
self.type = type
|
||||||
}
|
}
|
||||||
|
|
||||||
|
init(_ data: StateData = .init(), type: StateType = .ofEmpty, node: SymbolNode) {
|
||||||
|
self.data = data
|
||||||
|
self.type = type
|
||||||
|
self.node = node
|
||||||
|
self.data.candidates = { node.children?.map(\.title) ?? [String]() }().map { ("", $0) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 針對不同的狀態,規定不同的構造器
|
// MARK: - 針對不同的狀態,規定不同的構造器
|
||||||
|
@ -63,59 +107,56 @@ extension IMEState {
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func NotEmpty(nodeValues: [String], reading: String = "", cursor: Int) -> IMEState {
|
public static func NotEmpty(displayTextSegments: [String], cursor: Int) -> IMEState {
|
||||||
var result = IMEState(type: .ofNotEmpty)
|
var result = IMEState(type: .ofNotEmpty)
|
||||||
// 注意資料的設定順序:nodeValuesArray 必須比 reading 先設定。
|
// 注意資料的設定順序,一定得先設定 displayTextSegments。
|
||||||
result.data.nodeValuesArray = nodeValues
|
result.data.displayTextSegments = displayTextSegments
|
||||||
if !reading.isEmpty {
|
|
||||||
result.data.reading = reading // 會在被寫入資料值後自動更新 nodeValuesArray
|
|
||||||
}
|
|
||||||
// 此時 nodeValuesArray 已經被塞上讀音,直接使用即可。
|
|
||||||
result.data.displayedText = result.data.nodeValuesArray.joined()
|
|
||||||
result.data.cursor = cursor
|
result.data.cursor = cursor
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func Inputting(nodeValues: [String], reading: String = "", cursor: Int) -> IMEState {
|
public static func Inputting(displayTextSegments: [String], cursor: Int) -> IMEState {
|
||||||
var result = IMEState.NotEmpty(nodeValues: nodeValues, reading: reading, cursor: cursor)
|
var result = IMEState.NotEmpty(displayTextSegments: displayTextSegments, cursor: cursor)
|
||||||
result.type = .ofInputting
|
result.type = .ofInputting
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func Marking(nodeValues: [String], nodeReadings: [String], cursor: Int, marker: Int) -> IMEState {
|
public static func Marking(
|
||||||
var result = IMEState.NotEmpty(nodeValues: nodeValues, cursor: cursor)
|
displayTextSegments: [String], markedReadings: [String], cursor: Int, marker: Int
|
||||||
|
)
|
||||||
|
-> IMEState
|
||||||
|
{
|
||||||
|
var result = IMEState.NotEmpty(displayTextSegments: displayTextSegments, cursor: cursor)
|
||||||
result.type = .ofMarking
|
result.type = .ofMarking
|
||||||
result.data.nodeReadingsArray = nodeReadings
|
|
||||||
result.data.marker = marker
|
result.data.marker = marker
|
||||||
|
result.data.markedReadings = markedReadings
|
||||||
StateData.Marking.updateParameters(&result.data)
|
StateData.Marking.updateParameters(&result.data)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func Candidates(candidates: [(String, String)], nodeValues: [String], cursor: Int) -> IMEState {
|
public static func Candidates(candidates: [(String, String)], displayTextSegments: [String], cursor: Int) -> IMEState
|
||||||
var result = IMEState.NotEmpty(nodeValues: nodeValues, cursor: cursor)
|
{
|
||||||
|
var result = IMEState.NotEmpty(displayTextSegments: displayTextSegments, cursor: cursor)
|
||||||
result.type = .ofCandidates
|
result.type = .ofCandidates
|
||||||
result.data.candidates = candidates
|
result.data.candidates = candidates
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
public static func SymbolTable(node: SymbolNode, previous: SymbolNode? = nil) -> IMEState {
|
public static func SymbolTable(node: SymbolNode) -> IMEState {
|
||||||
let candidates = { node.children?.map(\.title) ?? [String]() }().map { ("", $0) }
|
var result = IMEState(type: .ofNotEmpty, node: node)
|
||||||
var result = IMEState.Candidates(candidates: candidates, nodeValues: [], cursor: 0)
|
|
||||||
result.type = .ofSymbolTable
|
result.type = .ofSymbolTable
|
||||||
result.data.node = node
|
|
||||||
if let previous = previous {
|
|
||||||
result.data.node.previous = previous
|
|
||||||
}
|
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 規定一個狀態該怎樣返回自己的資料值
|
// MARK: - 規定一個狀態該怎樣返回自己的資料值
|
||||||
|
|
||||||
extension IMEState: InputStateProtocol {
|
extension IMEState {
|
||||||
|
public var isFilterable: Bool { data.isFilterable }
|
||||||
|
public var candidates: [(String, String)] { data.candidates }
|
||||||
public var convertedToInputting: IMEState {
|
public var convertedToInputting: IMEState {
|
||||||
if type == .ofInputting { return self }
|
if type == .ofInputting { return self }
|
||||||
var result = IMEState.Inputting(nodeValues: data.nodeValuesArray, reading: data.reading, cursor: data.cursor)
|
var result = IMEState.Inputting(displayTextSegments: data.displayTextSegments, cursor: data.cursor)
|
||||||
result.tooltip = data.tooltipBackupForInputting
|
result.tooltip = data.tooltipBackupForInputting
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
@ -146,25 +187,7 @@ extension IMEState: InputStateProtocol {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public var node: SymbolNode {
|
public var hasComposition: Bool {
|
||||||
get {
|
|
||||||
data.node
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
data.node = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var tooltipBackupForInputting: String {
|
|
||||||
get {
|
|
||||||
data.tooltipBackupForInputting
|
|
||||||
}
|
|
||||||
set {
|
|
||||||
data.tooltipBackupForInputting = newValue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public var hasBuffer: Bool {
|
|
||||||
switch type {
|
switch type {
|
||||||
case .ofNotEmpty, .ofInputting, .ofMarking, .ofCandidates: return true
|
case .ofNotEmpty, .ofInputting, .ofMarking, .ofCandidates: return true
|
||||||
default: return false
|
default: return false
|
||||||
|
|
|
@ -38,6 +38,9 @@ public struct StateData {
|
||||||
|
|
||||||
// MARK: Cursor & Marker & Range for UTF16 (Read-Only)
|
// MARK: Cursor & Marker & Range for UTF16 (Read-Only)
|
||||||
|
|
||||||
|
/// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度,
|
||||||
|
/// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。
|
||||||
|
/// 這樣就可以免除不必要的類型轉換。
|
||||||
var u16Cursor: Int {
|
var u16Cursor: Int {
|
||||||
displayedText.charComponents[0..<cursor].joined().utf16.count
|
displayedText.charComponents[0..<cursor].joined().utf16.count
|
||||||
}
|
}
|
||||||
|
@ -53,32 +56,14 @@ public struct StateData {
|
||||||
// MARK: Other data for non-empty states.
|
// MARK: Other data for non-empty states.
|
||||||
|
|
||||||
var markedTargetExists: Bool = false
|
var markedTargetExists: Bool = false
|
||||||
var nodeReadingsArray = [String]()
|
var displayTextSegments = [String]() {
|
||||||
var nodeValuesArray = [String]()
|
|
||||||
var reading: String = "" {
|
|
||||||
didSet {
|
didSet {
|
||||||
if !reading.isEmpty {
|
displayedText = displayTextSegments.joined()
|
||||||
var newNodeValuesArray = [String]()
|
|
||||||
var temporaryNode = ""
|
|
||||||
var charCounter = 0
|
|
||||||
for node in nodeValuesArray {
|
|
||||||
for char in node {
|
|
||||||
if charCounter == cursor - reading.count {
|
|
||||||
newNodeValuesArray.append(temporaryNode)
|
|
||||||
temporaryNode = ""
|
|
||||||
newNodeValuesArray.append(reading)
|
|
||||||
}
|
|
||||||
temporaryNode += String(char)
|
|
||||||
charCounter += 1
|
|
||||||
}
|
|
||||||
newNodeValuesArray.append(temporaryNode)
|
|
||||||
temporaryNode = ""
|
|
||||||
}
|
|
||||||
nodeValuesArray = newNodeValuesArray.isEmpty ? [reading] : newNodeValuesArray
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reading: String = ""
|
||||||
|
var markedReadings = [String]()
|
||||||
var candidates = [(String, String)]()
|
var candidates = [(String, String)]()
|
||||||
var textToCommit: String = ""
|
var textToCommit: String = ""
|
||||||
var tooltip: String = ""
|
var tooltip: String = ""
|
||||||
|
@ -94,13 +79,12 @@ public struct StateData {
|
||||||
markedTargetExists ? mgrPrefs.allowedMarkRange.contains(markedRange.count) : false
|
markedTargetExists ? mgrPrefs.allowedMarkRange.contains(markedRange.count) : false
|
||||||
}
|
}
|
||||||
|
|
||||||
var readingCountMismatched: Bool { displayedText.count != nodeReadingsArray.count }
|
|
||||||
var attributedStringNormal: NSAttributedString {
|
var attributedStringNormal: NSAttributedString {
|
||||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
||||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
||||||
let attributedString = NSMutableAttributedString(string: displayedText)
|
let attributedString = NSMutableAttributedString(string: displayedText)
|
||||||
var newBegin = 0
|
var newBegin = 0
|
||||||
for (i, neta) in nodeValuesArray.enumerated() {
|
for (i, neta) in displayTextSegments.enumerated() {
|
||||||
attributedString.setAttributes(
|
attributedString.setAttributes(
|
||||||
[
|
[
|
||||||
/// 不能用 .thick,否則會看不到游標。
|
/// 不能用 .thick,否則會看不到游標。
|
||||||
|
@ -147,17 +131,14 @@ public struct StateData {
|
||||||
)
|
)
|
||||||
return attributedString
|
return attributedString
|
||||||
}
|
}
|
||||||
|
|
||||||
var node: SymbolNode = .init("")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - InputState 工具函式
|
// MARK: - IMEState 工具函式
|
||||||
|
|
||||||
extension StateData {
|
extension StateData {
|
||||||
var chkIfUserPhraseExists: Bool {
|
var chkIfUserPhraseExists: Bool {
|
||||||
let text = displayedText.charComponents[markedRange].joined()
|
let text = displayedText.charComponents[markedRange].joined()
|
||||||
let selectedReadings = nodeReadingsArray[markedRange]
|
let joined = markedReadings.joined(separator: "-")
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
return mgrLangModel.checkIfUserPhraseExist(
|
return mgrLangModel.checkIfUserPhraseExist(
|
||||||
userPhrase: text, mode: IME.currentInputMode, key: joined
|
userPhrase: text, mode: IME.currentInputMode, key: joined
|
||||||
)
|
)
|
||||||
|
@ -165,8 +146,7 @@ extension StateData {
|
||||||
|
|
||||||
var userPhrase: String {
|
var userPhrase: String {
|
||||||
let text = displayedText.charComponents[markedRange].joined()
|
let text = displayedText.charComponents[markedRange].joined()
|
||||||
let selectedReadings = nodeReadingsArray[markedRange]
|
let joined = markedReadings.joined(separator: "-")
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||||
return "\(text) \(joined)\(nerfedScore)"
|
return "\(text) \(joined)\(nerfedScore)"
|
||||||
}
|
}
|
||||||
|
@ -174,8 +154,7 @@ extension StateData {
|
||||||
var userPhraseConverted: String {
|
var userPhraseConverted: String {
|
||||||
let text =
|
let text =
|
||||||
ChineseConverter.crossConvert(displayedText.charComponents[markedRange].joined()) ?? ""
|
ChineseConverter.crossConvert(displayedText.charComponents[markedRange].joined()) ?? ""
|
||||||
let selectedReadings = nodeReadingsArray[markedRange]
|
let joined = markedReadings.joined(separator: "-")
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
||||||
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
|
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
|
||||||
return "\(text) \(joined)\(nerfedScore)\t\(convertedMark)"
|
return "\(text) \(joined)\(nerfedScore)\t\(convertedMark)"
|
||||||
|
@ -184,7 +163,7 @@ extension StateData {
|
||||||
enum Marking {
|
enum Marking {
|
||||||
private static func generateReadingThread(_ data: StateData) -> String {
|
private static func generateReadingThread(_ data: StateData) -> String {
|
||||||
var arrOutput = [String]()
|
var arrOutput = [String]()
|
||||||
for neta in data.nodeReadingsArray[data.markedRange] {
|
for neta in data.markedReadings {
|
||||||
var neta = neta
|
var neta = neta
|
||||||
if neta.isEmpty { continue }
|
if neta.isEmpty { continue }
|
||||||
if neta.contains("_") {
|
if neta.contains("_") {
|
||||||
|
@ -207,12 +186,6 @@ extension StateData {
|
||||||
/// - Parameter data: 要處理的狀態資料包。
|
/// - Parameter data: 要處理的狀態資料包。
|
||||||
public static func updateParameters(_ data: inout StateData) {
|
public static func updateParameters(_ data: inout StateData) {
|
||||||
var tooltipGenerated: String {
|
var tooltipGenerated: String {
|
||||||
if data.displayedText.count != data.nodeReadingsArray.count {
|
|
||||||
ctlInputMethod.tooltipController.setColor(state: .redAlert)
|
|
||||||
return NSLocalizedString(
|
|
||||||
"⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if mgrPrefs.phraseReplacementEnabled {
|
if mgrPrefs.phraseReplacementEnabled {
|
||||||
ctlInputMethod.tooltipController.setColor(state: .warning)
|
ctlInputMethod.tooltipController.setColor(state: .warning)
|
||||||
return NSLocalizedString(
|
return NSLocalizedString(
|
||||||
|
@ -240,8 +213,7 @@ extension StateData {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectedReadings = data.nodeReadingsArray[data.markedRange]
|
let joined = data.markedReadings.joined(separator: "-")
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
let exist = mgrLangModel.checkIfUserPhraseExist(
|
let exist = mgrLangModel.checkIfUserPhraseExist(
|
||||||
userPhrase: text, mode: IME.currentInputMode, key: joined
|
userPhrase: text, mode: IME.currentInputMode, key: joined
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,488 +0,0 @@
|
||||||
// (c) 2011 and onwards The OpenVanilla Project (MIT License).
|
|
||||||
// All possible vChewing-specific modifications are of:
|
|
||||||
// (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
|
|
||||||
|
|
||||||
// 註:所有 InputState 型別均不適合使用 Struct,因為 Struct 無法相互繼承派生。
|
|
||||||
|
|
||||||
/// 此型別用以呈現輸入法控制器(ctlInputMethod)的各種狀態。
|
|
||||||
///
|
|
||||||
/// 從實際角度來看,輸入法屬於有限態械(Finite State Machine)。其藉由滑鼠/鍵盤
|
|
||||||
/// 等輸入裝置接收輸入訊號,據此切換至對應的狀態,再根據狀態更新使用者介面內容,
|
|
||||||
/// 最終生成文字輸出、遞交給接收文字輸入行為的客體應用。此乃單向資訊流序,且使用
|
|
||||||
/// 者介面內容與文字輸出均無條件地遵循某一個指定的資料來源。
|
|
||||||
///
|
|
||||||
/// InputState 型別用以呈現輸入法控制器正在做的事情,且分狀態儲存各種狀態限定的
|
|
||||||
/// 常數與變數。對輸入法而言,使用狀態模式(而非策略模式)來做這種常數變數隔離,
|
|
||||||
/// 可能會讓新手覺得會有些牛鼎烹雞,卻實際上變相減少了在程式維護方面的管理難度、
|
|
||||||
/// 不需要再在某個狀態下為了該狀態不需要的變數與常數的處置策略而煩惱。
|
|
||||||
///
|
|
||||||
/// 對 InputState 型別下的諸多狀態的切換,應以生成新副本來取代舊有副本的形式來完
|
|
||||||
/// 成。唯一例外是 InputState.Marking、擁有可以將自身轉變為 InputState.Inputting
|
|
||||||
/// 的成員函式,但也只是生成副本、來交給輸入法控制器來處理而已。
|
|
||||||
///
|
|
||||||
/// 輸入法控制器持下述狀態:
|
|
||||||
///
|
|
||||||
/// - .Deactivated: 使用者沒在使用輸入法。
|
|
||||||
/// - .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。因為逐字選字模式不需要在
|
|
||||||
/// 組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。
|
|
||||||
/// - .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。抑或是剛剛敲字遞交給
|
|
||||||
/// 客體應用、準備新的輸入行為。
|
|
||||||
/// - .Abortion: 與 Empty 類似,但會扔掉上一個狀態的內容、不將這些
|
|
||||||
/// 內容遞交給客體應用。該狀態在處理完畢之後會被立刻切換至 .Empty()。
|
|
||||||
/// - .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。
|
|
||||||
/// - .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。
|
|
||||||
/// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。
|
|
||||||
/// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、還是將這個範圍的
|
|
||||||
/// 詞音組合放入語彙濾除清單。
|
|
||||||
/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。
|
|
||||||
/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。
|
|
||||||
public enum InputState {
|
|
||||||
/// .Deactivated: 使用者沒在使用輸入法。
|
|
||||||
class Deactivated: InputStateProtocol {
|
|
||||||
var node: SymbolNode = .init("")
|
|
||||||
var attributedString: NSAttributedString = .init()
|
|
||||||
var data: StateData = .init()
|
|
||||||
var textToCommit: String = ""
|
|
||||||
var tooltip: String = ""
|
|
||||||
let displayedText: String = ""
|
|
||||||
let hasBuffer: Bool = false
|
|
||||||
let isCandidateContainer: Bool = false
|
|
||||||
public var type: StateType { .ofDeactivated }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .Empty: 使用者剛剛切換至該輸入法、卻還沒有任何輸入行為。
|
|
||||||
/// 抑或是剛剛敲字遞交給客體應用、準備新的輸入行為。
|
|
||||||
class Empty: InputStateProtocol {
|
|
||||||
var node: SymbolNode = .init("")
|
|
||||||
var attributedString: NSAttributedString = .init()
|
|
||||||
var data: StateData = .init()
|
|
||||||
var textToCommit: String = ""
|
|
||||||
var tooltip: String = ""
|
|
||||||
let hasBuffer: Bool = false
|
|
||||||
let isCandidateContainer: Bool = false
|
|
||||||
public var type: StateType { .ofEmpty }
|
|
||||||
let displayedText: String = ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .Abortion: 與 Empty 類似,
|
|
||||||
/// 但會扔掉上一個狀態的內容、不將這些內容遞交給客體應用。
|
|
||||||
/// 該狀態在處理完畢之後會被立刻切換至 .Empty()。
|
|
||||||
class Abortion: Empty {
|
|
||||||
override public var type: StateType { .ofAbortion }
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .Committing: 該狀態會承載要遞交出去的內容,讓輸入法控制器處理時代為遞交。
|
|
||||||
class Committing: InputStateProtocol {
|
|
||||||
var node: SymbolNode = .init("")
|
|
||||||
var attributedString: NSAttributedString = .init()
|
|
||||||
var data: StateData = .init()
|
|
||||||
var tooltip: String = ""
|
|
||||||
var textToCommit: String = ""
|
|
||||||
let displayedText: String = ""
|
|
||||||
let hasBuffer: Bool = false
|
|
||||||
let isCandidateContainer: Bool = false
|
|
||||||
public var type: StateType { .ofCommitting }
|
|
||||||
|
|
||||||
init(textToCommit: String) {
|
|
||||||
self.textToCommit = textToCommit
|
|
||||||
ChineseConverter.ensureCurrencyNumerals(target: &self.textToCommit)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .AssociatedPhrases: 逐字選字模式內的聯想詞輸入狀態。
|
|
||||||
/// 因為逐字選字模式不需要在組字區內存入任何東西,所以該狀態不受 .NotEmpty 的管轄。
|
|
||||||
class Associates: InputStateProtocol {
|
|
||||||
var node: SymbolNode = .init("")
|
|
||||||
var attributedString: NSAttributedString = .init()
|
|
||||||
var data: StateData = .init()
|
|
||||||
var textToCommit: String = ""
|
|
||||||
var tooltip: String = ""
|
|
||||||
let displayedText: String = ""
|
|
||||||
let hasBuffer: Bool = false
|
|
||||||
let isCandidateContainer: Bool = true
|
|
||||||
public var type: StateType { .ofAssociates }
|
|
||||||
var candidates: [(String, String)] { data.candidates }
|
|
||||||
init(candidates: [(String, String)]) {
|
|
||||||
data.candidates = candidates
|
|
||||||
attributedString = {
|
|
||||||
let attributedString = NSMutableAttributedString(
|
|
||||||
string: " ",
|
|
||||||
attributes: [
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 0,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return attributedString
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .NotEmpty: 非空狀態,是一種狀態大類、用以派生且代表下述諸狀態。
|
|
||||||
/// - .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。
|
|
||||||
/// - .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、
|
|
||||||
/// 還是將這個範圍的詞音組合放入語彙濾除清單。
|
|
||||||
/// - .ChoosingCandidate: 叫出選字窗、允許使用者選字。
|
|
||||||
/// - .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。
|
|
||||||
class NotEmpty: InputStateProtocol {
|
|
||||||
var node: SymbolNode = .init("")
|
|
||||||
var attributedString: NSAttributedString = .init()
|
|
||||||
var data: StateData = .init()
|
|
||||||
var tooltip: String = ""
|
|
||||||
var textToCommit: String = ""
|
|
||||||
let hasBuffer: Bool = true
|
|
||||||
var isCandidateContainer: Bool { false }
|
|
||||||
public var type: StateType { .ofNotEmpty }
|
|
||||||
private(set) var displayedText: String
|
|
||||||
private(set) var cursorIndex: Int = 0 { didSet { cursorIndex = max(cursorIndex, 0) } }
|
|
||||||
private(set) var reading: String = ""
|
|
||||||
private(set) var nodeValuesArray = [String]()
|
|
||||||
public var displayedTextConverted: String {
|
|
||||||
let converted = IME.kanjiConversionIfRequired(displayedText)
|
|
||||||
if converted.utf16.count != displayedText.utf16.count
|
|
||||||
|| converted.count != displayedText.count
|
|
||||||
{
|
|
||||||
return displayedText
|
|
||||||
}
|
|
||||||
return converted
|
|
||||||
}
|
|
||||||
|
|
||||||
public var committingBufferConverted: String { displayedTextConverted }
|
|
||||||
|
|
||||||
init(displayedText: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) {
|
|
||||||
self.displayedText = displayedText
|
|
||||||
self.reading = reading
|
|
||||||
// 為了簡化運算,將 reading 本身也變成一個字詞節點。
|
|
||||||
if !reading.isEmpty {
|
|
||||||
var newNodeValuesArray = [String]()
|
|
||||||
var temporaryNode = ""
|
|
||||||
var charCounter = 0
|
|
||||||
for node in nodeValuesArray {
|
|
||||||
for char in node {
|
|
||||||
if charCounter == cursorIndex - reading.utf16.count {
|
|
||||||
newNodeValuesArray.append(temporaryNode)
|
|
||||||
temporaryNode = ""
|
|
||||||
newNodeValuesArray.append(reading)
|
|
||||||
}
|
|
||||||
temporaryNode += String(char)
|
|
||||||
charCounter += 1
|
|
||||||
}
|
|
||||||
newNodeValuesArray.append(temporaryNode)
|
|
||||||
temporaryNode = ""
|
|
||||||
}
|
|
||||||
self.nodeValuesArray = newNodeValuesArray
|
|
||||||
} else {
|
|
||||||
self.nodeValuesArray = nodeValuesArray
|
|
||||||
}
|
|
||||||
defer {
|
|
||||||
self.cursorIndex = cursorIndex
|
|
||||||
self.attributedString = {
|
|
||||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
|
||||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
|
||||||
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
|
|
||||||
var newBegin = 0
|
|
||||||
for (i, neta) in nodeValuesArray.enumerated() {
|
|
||||||
attributedString.setAttributes(
|
|
||||||
[
|
|
||||||
/// 不能用 .thick,否則會看不到游標。
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: i,
|
|
||||||
], range: NSRange(location: newBegin, length: neta.utf16.count)
|
|
||||||
)
|
|
||||||
newBegin += neta.utf16.count
|
|
||||||
}
|
|
||||||
return attributedString
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .Inputting: 使用者輸入了內容。此時會出現組字區(Compositor)。
|
|
||||||
class Inputting: NotEmpty {
|
|
||||||
override public var type: StateType { .ofInputting }
|
|
||||||
override public var committingBufferConverted: String {
|
|
||||||
let committingBuffer = nodeValuesArray.joined()
|
|
||||||
let converted = IME.kanjiConversionIfRequired(committingBuffer)
|
|
||||||
if converted.utf16.count != displayedText.utf16.count
|
|
||||||
|| converted.count != displayedText.count
|
|
||||||
{
|
|
||||||
return displayedText
|
|
||||||
}
|
|
||||||
return converted
|
|
||||||
}
|
|
||||||
|
|
||||||
override init(displayedText: String, cursorIndex: Int, reading: String = "", nodeValuesArray: [String] = []) {
|
|
||||||
super.init(
|
|
||||||
displayedText: displayedText, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .Marking: 使用者在組字區內標記某段範圍,可以決定是添入新詞、
|
|
||||||
/// 還是將這個範圍的詞音組合放入語彙濾除清單。
|
|
||||||
class Marking: NotEmpty {
|
|
||||||
override public var type: StateType { .ofMarking }
|
|
||||||
private var allowedMarkRange: ClosedRange<Int> = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength
|
|
||||||
private(set) var markerIndex: Int = 0 { didSet { markerIndex = max(markerIndex, 0) } }
|
|
||||||
private(set) var markedRange: Range<Int>
|
|
||||||
private var literalMarkedRange: Range<Int> {
|
|
||||||
let lowerBoundLiteral = displayedText.charIndexLiteral(from: markedRange.lowerBound)
|
|
||||||
let upperBoundLiteral = displayedText.charIndexLiteral(from: markedRange.upperBound)
|
|
||||||
return lowerBoundLiteral..<upperBoundLiteral
|
|
||||||
}
|
|
||||||
|
|
||||||
var literalReadingThread: String {
|
|
||||||
var arrOutput = [String]()
|
|
||||||
for neta in readings[literalMarkedRange] {
|
|
||||||
var neta = neta
|
|
||||||
if neta.isEmpty { continue }
|
|
||||||
if neta.contains("_") {
|
|
||||||
arrOutput.append("??")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if mgrPrefs.showHanyuPinyinInCompositionBuffer { // 恢復陰平標記->注音轉拼音->轉教科書式標調
|
|
||||||
neta = Tekkon.restoreToneOneInZhuyinKey(target: neta)
|
|
||||||
neta = Tekkon.cnvPhonaToHanyuPinyin(target: neta)
|
|
||||||
neta = Tekkon.cnvHanyuPinyinToTextbookStyle(target: neta)
|
|
||||||
} else {
|
|
||||||
neta = Tekkon.cnvZhuyinChainToTextbookReading(target: neta)
|
|
||||||
}
|
|
||||||
arrOutput.append(neta)
|
|
||||||
}
|
|
||||||
return arrOutput.joined(separator: " ")
|
|
||||||
}
|
|
||||||
|
|
||||||
private var markedTargetExists = false
|
|
||||||
|
|
||||||
var tooltipForMarking: String {
|
|
||||||
if displayedText.count != readings.count {
|
|
||||||
ctlInputMethod.tooltipController.setColor(state: .redAlert)
|
|
||||||
return NSLocalizedString(
|
|
||||||
"⚠︎ Unhandlable: Chars and Readings in buffer doesn't match.", comment: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if mgrPrefs.phraseReplacementEnabled {
|
|
||||||
ctlInputMethod.tooltipController.setColor(state: .warning)
|
|
||||||
return NSLocalizedString(
|
|
||||||
"⚠︎ Phrase replacement mode enabled, interfering user phrase entry.", comment: ""
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if markedRange.isEmpty {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
let text = displayedText.utf16SubString(with: markedRange)
|
|
||||||
if literalMarkedRange.count < allowedMarkRange.lowerBound {
|
|
||||||
ctlInputMethod.tooltipController.setColor(state: .denialInsufficiency)
|
|
||||||
return String(
|
|
||||||
format: NSLocalizedString(
|
|
||||||
"\"%@\" length must ≥ 2 for a user phrase.", comment: ""
|
|
||||||
) + "\n// " + literalReadingThread, text
|
|
||||||
)
|
|
||||||
} else if literalMarkedRange.count > allowedMarkRange.upperBound {
|
|
||||||
ctlInputMethod.tooltipController.setColor(state: .denialOverflow)
|
|
||||||
return String(
|
|
||||||
format: NSLocalizedString(
|
|
||||||
"\"%@\" length should ≤ %d for a user phrase.", comment: ""
|
|
||||||
) + "\n// " + literalReadingThread, text, allowedMarkRange.upperBound
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
let selectedReadings = readings[literalMarkedRange]
|
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
let exist = mgrLangModel.checkIfUserPhraseExist(
|
|
||||||
userPhrase: text, mode: IME.currentInputMode, key: joined
|
|
||||||
)
|
|
||||||
if exist {
|
|
||||||
markedTargetExists = exist
|
|
||||||
ctlInputMethod.tooltipController.setColor(state: .prompt)
|
|
||||||
return String(
|
|
||||||
format: NSLocalizedString(
|
|
||||||
"\"%@\" already exists: ENTER to boost, SHIFT+COMMAND+ENTER to nerf, \n BackSpace or Delete key to exclude.",
|
|
||||||
comment: ""
|
|
||||||
) + "\n// " + literalReadingThread, text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
ctlInputMethod.tooltipController.resetColor()
|
|
||||||
return String(
|
|
||||||
format: NSLocalizedString("\"%@\" selected. ENTER to add user phrase.", comment: "") + "\n// "
|
|
||||||
+ literalReadingThread,
|
|
||||||
text
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var tooltipBackupForInputting: String = ""
|
|
||||||
private(set) var readings: [String]
|
|
||||||
|
|
||||||
init(
|
|
||||||
displayedText: String, cursorIndex: Int, markerIndex: Int, readings: [String], nodeValuesArray: [String] = []
|
|
||||||
) {
|
|
||||||
let begin = min(cursorIndex, markerIndex)
|
|
||||||
let end = max(cursorIndex, markerIndex)
|
|
||||||
markedRange = begin..<end
|
|
||||||
self.readings = readings
|
|
||||||
super.init(
|
|
||||||
displayedText: displayedText, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray
|
|
||||||
)
|
|
||||||
defer {
|
|
||||||
self.markerIndex = markerIndex
|
|
||||||
tooltip = tooltipForMarking
|
|
||||||
attributedString = {
|
|
||||||
/// 考慮到因為滑鼠點擊等其它行為導致的組字區內容遞交情況,
|
|
||||||
/// 這裡對組字區內容也加上康熙字轉換或者 JIS 漢字轉換處理。
|
|
||||||
let attributedString = NSMutableAttributedString(string: displayedTextConverted)
|
|
||||||
let end = markedRange.upperBound
|
|
||||||
|
|
||||||
attributedString.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 0,
|
|
||||||
], range: NSRange(location: 0, length: markedRange.lowerBound)
|
|
||||||
)
|
|
||||||
attributedString.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.thick.rawValue,
|
|
||||||
.markedClauseSegment: 1,
|
|
||||||
],
|
|
||||||
range: NSRange(
|
|
||||||
location: markedRange.lowerBound,
|
|
||||||
length: markedRange.upperBound - markedRange.lowerBound
|
|
||||||
)
|
|
||||||
)
|
|
||||||
attributedString.setAttributes(
|
|
||||||
[
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 2,
|
|
||||||
],
|
|
||||||
range: NSRange(
|
|
||||||
location: end,
|
|
||||||
length: displayedText.utf16.count - end
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return attributedString
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var convertedToInputting: Inputting {
|
|
||||||
let state = Inputting(
|
|
||||||
displayedText: displayedText, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
|
||||||
)
|
|
||||||
state.tooltip = tooltipBackupForInputting
|
|
||||||
return state
|
|
||||||
}
|
|
||||||
|
|
||||||
var isFilterable: Bool { markedTargetExists ? allowedMarkRange.contains(literalMarkedRange.count) : false }
|
|
||||||
|
|
||||||
var bufferReadingCountMisMatch: Bool { displayedText.count != readings.count }
|
|
||||||
|
|
||||||
var chkIfUserPhraseExists: Bool {
|
|
||||||
let text = displayedText.utf16SubString(with: markedRange)
|
|
||||||
let selectedReadings = readings[literalMarkedRange]
|
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
return mgrLangModel.checkIfUserPhraseExist(
|
|
||||||
userPhrase: text, mode: IME.currentInputMode, key: joined
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
var userPhrase: String {
|
|
||||||
let text = displayedText.utf16SubString(with: markedRange)
|
|
||||||
let selectedReadings = readings[literalMarkedRange]
|
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
|
||||||
return "\(text) \(joined)\(nerfedScore)"
|
|
||||||
}
|
|
||||||
|
|
||||||
var userPhraseConverted: String {
|
|
||||||
let text =
|
|
||||||
ChineseConverter.crossConvert(displayedText.utf16SubString(with: markedRange)) ?? ""
|
|
||||||
let selectedReadings = readings[literalMarkedRange]
|
|
||||||
let joined = selectedReadings.joined(separator: "-")
|
|
||||||
let nerfedScore = ctlInputMethod.areWeNerfing && markedTargetExists ? " -114.514" : ""
|
|
||||||
let convertedMark = "#𝙃𝙪𝙢𝙖𝙣𝘾𝙝𝙚𝙘𝙠𝙍𝙚𝙦𝙪𝙞𝙧𝙚𝙙"
|
|
||||||
return "\(text) \(joined)\(nerfedScore)\t\(convertedMark)"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .ChoosingCandidate: 叫出選字窗、允許使用者選字。
|
|
||||||
class ChoosingCandidate: NotEmpty {
|
|
||||||
override var isCandidateContainer: Bool { true }
|
|
||||||
override public var type: StateType { .ofCandidates }
|
|
||||||
var candidates: [(String, String)]
|
|
||||||
// 該變數改為可以隨時更改的內容,不然的話 ctlInputMethod.candidateSelectionChanged() 會上演俄羅斯套娃(崩潰)。
|
|
||||||
public var chosenCandidateString: String = "" {
|
|
||||||
didSet {
|
|
||||||
// 去掉讀音資訊,且最終留存「執行康熙 / JIS 轉換之前」的結果。
|
|
||||||
if chosenCandidateString.contains("\u{17}") {
|
|
||||||
chosenCandidateString = String(chosenCandidateString.split(separator: "\u{17}")[0])
|
|
||||||
}
|
|
||||||
if !chosenCandidateString.contains("\u{1A}") { return }
|
|
||||||
chosenCandidateString = String(chosenCandidateString.split(separator: "\u{1A}").reversed()[0])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
init(
|
|
||||||
displayedText: String, cursorIndex: Int, candidates: [(String, String)],
|
|
||||||
nodeValuesArray: [String] = []
|
|
||||||
) {
|
|
||||||
self.candidates = candidates
|
|
||||||
super.init(displayedText: displayedText, cursorIndex: cursorIndex, nodeValuesArray: nodeValuesArray)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MARK: -
|
|
||||||
|
|
||||||
/// .SymbolTable: 波浪鍵符號選單專用的狀態,有自身的特殊處理。
|
|
||||||
class SymbolTable: ChoosingCandidate {
|
|
||||||
override public var type: StateType { .ofSymbolTable }
|
|
||||||
|
|
||||||
init(node: SymbolNode, previous: SymbolNode? = nil) {
|
|
||||||
super.init(displayedText: "", cursorIndex: 0, candidates: [])
|
|
||||||
self.node = node
|
|
||||||
if let previous = previous {
|
|
||||||
self.node.previous = previous
|
|
||||||
}
|
|
||||||
let candidates = node.children?.map(\.title) ?? [String]()
|
|
||||||
self.candidates = candidates.map { ("", $0) }
|
|
||||||
|
|
||||||
// InputState.SymbolTable 這個狀態比較特殊,不能把真空組字區交出去。
|
|
||||||
// 不然的話,在絕大多數終端機類應用當中、以及在 MS Word 等軟體當中
|
|
||||||
// 會出現符號選字窗無法響應方向鍵的問題。
|
|
||||||
// 如有誰要修奇摩注音的一點通選單的話,修復原理也是一樣的。
|
|
||||||
// Crediting Qwertyyb: https://github.com/qwertyyb/Fire/issues/55#issuecomment-1133497700
|
|
||||||
attributedString = {
|
|
||||||
let attributedString = NSMutableAttributedString(
|
|
||||||
string: " ",
|
|
||||||
attributes: [
|
|
||||||
.underlineStyle: NSUnderlineStyle.single.rawValue,
|
|
||||||
.markedClauseSegment: 0,
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return attributedString
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -25,7 +25,7 @@ protocol KeyHandlerDelegate {
|
||||||
_: KeyHandler, didSelectCandidateAt index: Int,
|
_: KeyHandler, didSelectCandidateAt index: Int,
|
||||||
ctlCandidate controller: ctlCandidateProtocol
|
ctlCandidate controller: ctlCandidateProtocol
|
||||||
)
|
)
|
||||||
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol, addToFilter: Bool)
|
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool)
|
||||||
-> Bool
|
-> Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,8 +35,6 @@ protocol KeyHandlerDelegate {
|
||||||
public class KeyHandler {
|
public class KeyHandler {
|
||||||
/// 半衰模組的衰減指數
|
/// 半衰模組的衰減指數
|
||||||
let kEpsilon: Double = 0.000001
|
let kEpsilon: Double = 0.000001
|
||||||
/// 檢測是否出現游標切斷組字圈內字符的情況
|
|
||||||
var isCursorCuttingChar = false
|
|
||||||
/// 檢測是否內容為空(注拼槽與組字器都是空的)
|
/// 檢測是否內容為空(注拼槽與組字器都是空的)
|
||||||
var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty }
|
var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty }
|
||||||
|
|
||||||
|
@ -88,6 +86,21 @@ public class KeyHandler {
|
||||||
|
|
||||||
// MARK: - Functions dealing with Megrez.
|
// MARK: - Functions dealing with Megrez.
|
||||||
|
|
||||||
|
/// 獲取當前標記得範圍。這個函式只能是函式、而非只讀變數。
|
||||||
|
/// - Returns: 當前標記範圍。
|
||||||
|
func currentMarkedRange() -> Range<Int> {
|
||||||
|
min(compositor.cursor, compositor.marker)..<max(compositor.cursor, compositor.marker)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 檢測是否出現游標切斷組字圈內字符的情況
|
||||||
|
func isCursorCuttingChar(isMarker: Bool = false) -> Bool {
|
||||||
|
let index = isMarker ? compositor.marker : compositor.cursor
|
||||||
|
var isBound = (index == compositor.walkedNodes.contextRange(ofGivenCursor: index).lowerBound)
|
||||||
|
if index == compositor.width { isBound = true }
|
||||||
|
let rawResult = compositor.walkedNodes.findNode(at: index)?.isReadingMismatched ?? false
|
||||||
|
return !isBound && rawResult
|
||||||
|
}
|
||||||
|
|
||||||
/// 實際上要拿給 Megrez 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。
|
/// 實際上要拿給 Megrez 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。
|
||||||
///
|
///
|
||||||
/// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。
|
/// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。
|
||||||
|
@ -107,7 +120,8 @@ public class KeyHandler {
|
||||||
// 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。
|
// 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。
|
||||||
if mgrPrefs.isDebugModeEnabled {
|
if mgrPrefs.isDebugModeEnabled {
|
||||||
let result = compositor.dumpDOT
|
let result = compositor.dumpDOT
|
||||||
let appSupportPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].path.appending("vChewing-visualization.dot")
|
let appSupportPath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0].path.appending(
|
||||||
|
"vChewing-visualization.dot")
|
||||||
do {
|
do {
|
||||||
try result.write(toFile: appSupportPath, atomically: true, encoding: .utf8)
|
try result.write(toFile: appSupportPath, atomically: true, encoding: .utf8)
|
||||||
} catch {
|
} catch {
|
||||||
|
|
|
@ -23,9 +23,9 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handleCandidate(
|
func handleCandidate(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard var ctlCandidateCurrent = delegate?.ctlCandidate() else {
|
guard var ctlCandidateCurrent = delegate?.ctlCandidate() else {
|
||||||
|
@ -41,7 +41,7 @@ extension KeyHandler {
|
||||||
|| ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold)
|
|| ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold)
|
||||||
|
|
||||||
if cancelCandidateKey {
|
if cancelCandidateKey {
|
||||||
if state is InputState.Associates
|
if state.type == .ofAssociates
|
||||||
|| mgrPrefs.useSCPCTypingMode
|
|| mgrPrefs.useSCPCTypingMode
|
||||||
|| compositor.isEmpty
|
|| compositor.isEmpty
|
||||||
{
|
{
|
||||||
|
@ -49,13 +49,12 @@ extension KeyHandler {
|
||||||
// 就將當前的組字緩衝區析構處理、強制重設輸入狀態。
|
// 就將當前的組字緩衝區析構處理、強制重設輸入狀態。
|
||||||
// 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。
|
// 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。
|
||||||
// 所以這裡需要對 compositor.isEmpty 做判定。
|
// 所以這裡需要對 compositor.isEmpty 做判定。
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
} else {
|
} else {
|
||||||
stateCallback(buildInputtingState)
|
stateCallback(buildInputtingState)
|
||||||
}
|
}
|
||||||
if let state = state as? InputState.SymbolTable, let nodePrevious = state.node.previous {
|
if state.type == .ofSymbolTable, let nodePrevious = state.node.previous, let _ = nodePrevious.children {
|
||||||
stateCallback(InputState.SymbolTable(node: nodePrevious))
|
stateCallback(IMEState.SymbolTable(node: nodePrevious))
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -63,9 +62,8 @@ extension KeyHandler {
|
||||||
// MARK: Enter
|
// MARK: Enter
|
||||||
|
|
||||||
if input.isEnter {
|
if input.isEnter {
|
||||||
if state is InputState.Associates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
if state.type == .ofAssociates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
delegate?.keyHandler(
|
delegate?.keyHandler(
|
||||||
|
@ -243,23 +241,15 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: End Key
|
// MARK: End Key
|
||||||
|
|
||||||
var candidates: [(String, String)]!
|
if state.candidates.isEmpty {
|
||||||
|
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
|
||||||
candidates = state.candidates
|
|
||||||
} else if let state = state as? InputState.Associates {
|
|
||||||
candidates = state.candidates
|
|
||||||
}
|
|
||||||
|
|
||||||
if candidates.isEmpty {
|
|
||||||
return false
|
return false
|
||||||
} else { // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
} else { // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。
|
||||||
if input.isEnd || input.emacsKey == EmacsKey.end {
|
if input.isEnd || input.emacsKey == EmacsKey.end {
|
||||||
if ctlCandidateCurrent.selectedCandidateIndex == candidates.count - 1 {
|
if ctlCandidateCurrent.selectedCandidateIndex == state.candidates.count - 1 {
|
||||||
IME.prtDebugIntel("9B69AAAD")
|
IME.prtDebugIntel("9B69AAAD")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
} else {
|
} else {
|
||||||
ctlCandidateCurrent.selectedCandidateIndex = candidates.count - 1
|
ctlCandidateCurrent.selectedCandidateIndex = state.candidates.count - 1
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -267,13 +257,13 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: 聯想詞處理 (Associated Phrases)
|
// MARK: 聯想詞處理 (Associated Phrases)
|
||||||
|
|
||||||
if state is InputState.Associates {
|
if state.type == .ofAssociates {
|
||||||
if !input.isShiftHold { return false }
|
if !input.isShiftHold { return false }
|
||||||
}
|
}
|
||||||
|
|
||||||
var index: Int = NSNotFound
|
var index: Int = NSNotFound
|
||||||
let match: String =
|
let match: String =
|
||||||
(state is InputState.Associates) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
(state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text
|
||||||
|
|
||||||
for j in 0..<ctlCandidateCurrent.keyLabels.count {
|
for j in 0..<ctlCandidateCurrent.keyLabels.count {
|
||||||
let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j]
|
let label: CandidateKeyLabel = ctlCandidateCurrent.keyLabels[j]
|
||||||
|
@ -293,7 +283,7 @@ extension KeyHandler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if state is InputState.Associates { return false }
|
if state.type == .ofAssociates { return false }
|
||||||
|
|
||||||
// MARK: 逐字選字模式的處理 (SCPC Mode Processing)
|
// MARK: 逐字選字模式的處理 (SCPC Mode Processing)
|
||||||
|
|
||||||
|
@ -333,10 +323,9 @@ extension KeyHandler {
|
||||||
didSelectCandidateAt: candidateIndex,
|
didSelectCandidateAt: candidateIndex,
|
||||||
ctlCandidate: ctlCandidateCurrent
|
ctlCandidate: ctlCandidateCurrent
|
||||||
)
|
)
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return handle(
|
return handle(
|
||||||
input: input, state: InputState.Empty(), stateCallback: stateCallback, errorCallback: errorCallback
|
input: input, state: IMEState.Empty(), stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -19,7 +19,7 @@ extension KeyHandler {
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handleComposition(
|
func handleComposition(
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool? {
|
) -> Bool? {
|
||||||
// MARK: 注音按鍵輸入處理 (Handle BPMF Keys)
|
// MARK: 注音按鍵輸入處理 (Handle BPMF Keys)
|
||||||
|
@ -100,8 +100,7 @@ extension KeyHandler {
|
||||||
switch compositor.isEmpty {
|
switch compositor.isEmpty {
|
||||||
case false: stateCallback(buildInputtingState)
|
case false: stateCallback(buildInputtingState)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。
|
return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。
|
||||||
}
|
}
|
||||||
|
@ -124,31 +123,26 @@ extension KeyHandler {
|
||||||
|
|
||||||
/// 逐字選字模式的處理。
|
/// 逐字選字模式的處理。
|
||||||
if mgrPrefs.useSCPCTypingMode {
|
if mgrPrefs.useSCPCTypingMode {
|
||||||
let choosingCandidates: InputState.ChoosingCandidate = buildCandidate(
|
let candidateState: IMEState = buildCandidate(
|
||||||
state: inputting,
|
state: inputting,
|
||||||
isTypingVertical: input.isTypingVertical
|
isTypingVertical: input.isTypingVertical
|
||||||
)
|
)
|
||||||
if choosingCandidates.candidates.count == 1, let firstCandidate = choosingCandidates.candidates.first {
|
if candidateState.candidates.count == 1, let firstCandidate = candidateState.candidates.first {
|
||||||
let reading: String = firstCandidate.0
|
let reading: String = firstCandidate.0
|
||||||
let text: String = firstCandidate.1
|
let text: String = firstCandidate.1
|
||||||
stateCallback(InputState.Committing(textToCommit: text))
|
stateCallback(IMEState.Committing(textToCommit: text))
|
||||||
|
|
||||||
if !mgrPrefs.associatedPhrasesEnabled {
|
if !mgrPrefs.associatedPhrasesEnabled {
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
} else {
|
} else {
|
||||||
if let associatedPhrases =
|
let associatedPhrases =
|
||||||
buildAssociatePhraseState(
|
buildAssociatePhraseState(
|
||||||
withPair: .init(key: reading, value: text),
|
withPair: .init(key: reading, value: text)
|
||||||
isTypingVertical: input.isTypingVertical
|
)
|
||||||
), !associatedPhrases.candidates.isEmpty
|
stateCallback(associatedPhrases.candidates.isEmpty ? IMEState.Empty() : associatedPhrases)
|
||||||
{
|
|
||||||
stateCallback(associatedPhrases)
|
|
||||||
} else {
|
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
stateCallback(choosingCandidates)
|
stateCallback(candidateState)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 ctlInputMethod 回報給 IMK。
|
// 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 ctlInputMethod 回報給 IMK。
|
||||||
|
|
|
@ -25,8 +25,8 @@ extension KeyHandler {
|
||||||
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
/// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。
|
||||||
func handle(
|
func handle(
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
// 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。
|
// 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。
|
||||||
|
@ -39,7 +39,7 @@ extension KeyHandler {
|
||||||
if input.isInvalid {
|
if input.isInvalid {
|
||||||
// 在「.Empty(IgnoringPreviousState) 與 .Deactivated」狀態下的首次不合規按鍵輸入可以直接放行。
|
// 在「.Empty(IgnoringPreviousState) 與 .Deactivated」狀態下的首次不合規按鍵輸入可以直接放行。
|
||||||
// 因為「.Abortion」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。
|
// 因為「.Abortion」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。
|
||||||
if state is InputState.Empty || state is InputState.Deactivated {
|
if state.type == .ofEmpty || state.type == .ofDeactivated {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
IME.prtDebugIntel("550BCF7B: KeyHandler just refused an invalid input.")
|
IME.prtDebugIntel("550BCF7B: KeyHandler just refused an invalid input.")
|
||||||
|
@ -51,7 +51,7 @@ extension KeyHandler {
|
||||||
// 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。
|
// 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。
|
||||||
let isFunctionKey: Bool =
|
let isFunctionKey: Bool =
|
||||||
input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey)
|
input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey)
|
||||||
if !(state is InputState.NotEmpty) && !(state is InputState.Associates) && isFunctionKey {
|
if state.type != .ofAssociates, !state.hasComposition, !state.isCandidateContainer, isFunctionKey {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,7 +68,7 @@ extension KeyHandler {
|
||||||
// 略過對 BackSpace 的處理。
|
// 略過對 BackSpace 的處理。
|
||||||
} else if input.isCapsLockOn || input.isASCIIModeInput {
|
} else if input.isCapsLockOn || input.isASCIIModeInput {
|
||||||
// 但願能夠處理這種情況下所有可能的按鍵組合。
|
// 但願能夠處理這種情況下所有可能的按鍵組合。
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
|
|
||||||
// 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。
|
// 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。
|
||||||
if input.isUpperCaseASCIILetterKey {
|
if input.isUpperCaseASCIILetterKey {
|
||||||
|
@ -82,8 +82,8 @@ extension KeyHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
// 將整個組字區的內容遞交給客體應用。
|
// 將整個組字區的內容遞交給客體應用。
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.lowercased()))
|
stateCallback(IMEState.Committing(textToCommit: inputText.lowercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -94,19 +94,19 @@ extension KeyHandler {
|
||||||
// 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。
|
// 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。
|
||||||
// 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。
|
// 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。
|
||||||
if input.isNumericPadKey {
|
if input.isNumericPadKey {
|
||||||
if !(state is InputState.ChoosingCandidate || state is InputState.Associates
|
if !(state.type == .ofCandidates || state.type == .ofAssociates
|
||||||
|| state is InputState.SymbolTable)
|
|| state.type == .ofSymbolTable)
|
||||||
{
|
{
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.lowercased()))
|
stateCallback(IMEState.Committing(textToCommit: inputText.lowercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: 處理候選字詞 (Handle Candidates)
|
// MARK: 處理候選字詞 (Handle Candidates)
|
||||||
|
|
||||||
if state is InputState.ChoosingCandidate {
|
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
|
||||||
return handleCandidate(
|
return handleCandidate(
|
||||||
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
)
|
)
|
||||||
|
@ -114,26 +114,26 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: 處理聯想詞 (Handle Associated Phrases)
|
// MARK: 處理聯想詞 (Handle Associated Phrases)
|
||||||
|
|
||||||
if state is InputState.Associates {
|
if state.type == .ofAssociates {
|
||||||
if handleCandidate(
|
if handleCandidate(
|
||||||
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
} else {
|
} else {
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking)
|
// MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking)
|
||||||
|
|
||||||
if let marking = state as? InputState.Marking {
|
if state.type == .ofMarking {
|
||||||
if handleMarkingState(
|
if handleMarkingState(
|
||||||
marking, input: input, stateCallback: stateCallback,
|
state, input: input, stateCallback: stateCallback,
|
||||||
errorCallback: errorCallback
|
errorCallback: errorCallback
|
||||||
) {
|
) {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
state = marking.convertedToInputting
|
state = state.convertedToInputting
|
||||||
stateCallback(state)
|
stateCallback(state)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -147,7 +147,7 @@ extension KeyHandler {
|
||||||
|
|
||||||
// MARK: 用上下左右鍵呼叫選字窗 (Calling candidate window using Up / Down or PageUp / PageDn.)
|
// MARK: 用上下左右鍵呼叫選字窗 (Calling candidate window using Up / Down or PageUp / PageDn.)
|
||||||
|
|
||||||
if let currentState = state as? InputState.NotEmpty, composer.isEmpty, !input.isOptionHold,
|
if state.hasComposition, composer.isEmpty, !input.isOptionHold,
|
||||||
input.isCursorClockLeft || input.isCursorClockRight || input.isSpace
|
input.isCursorClockLeft || input.isCursorClockRight || input.isSpace
|
||||||
|| input.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior)
|
|| input.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior)
|
||||||
{
|
{
|
||||||
|
@ -155,12 +155,12 @@ extension KeyHandler {
|
||||||
/// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話………
|
/// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話………
|
||||||
if !mgrPrefs.chooseCandidateUsingSpace {
|
if !mgrPrefs.chooseCandidateUsingSpace {
|
||||||
if compositor.cursor >= compositor.length {
|
if compositor.cursor >= compositor.length {
|
||||||
let displayedText = currentState.displayedText
|
let displayedText = state.displayedText
|
||||||
if !displayedText.isEmpty {
|
if !displayedText.isEmpty {
|
||||||
stateCallback(InputState.Committing(textToCommit: displayedText))
|
stateCallback(IMEState.Committing(textToCommit: displayedText))
|
||||||
}
|
}
|
||||||
stateCallback(InputState.Committing(textToCommit: " "))
|
stateCallback(IMEState.Committing(textToCommit: " "))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
} else if currentLM.hasUnigramsFor(key: " ") {
|
} else if currentLM.hasUnigramsFor(key: " ") {
|
||||||
compositor.insertKey(" ")
|
compositor.insertKey(" ")
|
||||||
walk()
|
walk()
|
||||||
|
@ -175,7 +175,7 @@ extension KeyHandler {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
stateCallback(buildCandidate(state: currentState, isTypingVertical: input.isTypingVertical))
|
stateCallback(buildCandidate(state: state))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -236,7 +236,7 @@ extension KeyHandler {
|
||||||
// MARK: Clock-Left & Clock-Right
|
// MARK: Clock-Left & Clock-Right
|
||||||
|
|
||||||
if input.isCursorClockLeft || input.isCursorClockRight {
|
if input.isCursorClockLeft || input.isCursorClockRight {
|
||||||
if input.isOptionHold, state is InputState.Inputting {
|
if input.isOptionHold, state.type == .ofInputting {
|
||||||
if input.isCursorClockRight {
|
if input.isCursorClockRight {
|
||||||
return handleInlineCandidateRotation(
|
return handleInlineCandidateRotation(
|
||||||
state: state, reverseModifier: false, stateCallback: stateCallback, errorCallback: errorCallback
|
state: state, reverseModifier: false, stateCallback: stateCallback, errorCallback: errorCallback
|
||||||
|
@ -285,7 +285,7 @@ extension KeyHandler {
|
||||||
walk()
|
walk()
|
||||||
let inputting = buildInputtingState
|
let inputting = buildInputtingState
|
||||||
stateCallback(inputting)
|
stateCallback(inputting)
|
||||||
stateCallback(buildCandidate(state: inputting, isTypingVertical: input.isTypingVertical))
|
stateCallback(buildCandidate(state: inputting))
|
||||||
} else { // 不要在注音沒敲完整的情況下叫出統合符號選單。
|
} else { // 不要在注音沒敲完整的情況下叫出統合符號選單。
|
||||||
IME.prtDebugIntel("17446655")
|
IME.prtDebugIntel("17446655")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -297,14 +297,14 @@ extension KeyHandler {
|
||||||
// 於是這裡用「模擬一次 Enter 鍵的操作」使其代為執行這個 commit buffer 的動作。
|
// 於是這裡用「模擬一次 Enter 鍵的操作」使其代為執行這個 commit buffer 的動作。
|
||||||
// 這裡不需要該函式所傳回的 bool 結果,所以用「_ =」解消掉。
|
// 這裡不需要該函式所傳回的 bool 結果,所以用「_ =」解消掉。
|
||||||
_ = handleEnter(state: state, stateCallback: stateCallback)
|
_ = handleEnter(state: state, stateCallback: stateCallback)
|
||||||
stateCallback(InputState.SymbolTable(node: SymbolNode.root, isTypingVertical: input.isTypingVertical))
|
stateCallback(IMEState.SymbolTable(node: SymbolNode.root))
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: 全形/半形阿拉伯數字輸入 (FW / HW Arabic Numbers Input)
|
// MARK: 全形/半形阿拉伯數字輸入 (FW / HW Arabic Numbers Input)
|
||||||
|
|
||||||
if state is InputState.Empty {
|
if state.type == .ofEmpty {
|
||||||
if input.isMainAreaNumKey, input.isShiftHold, input.isOptionHold, !input.isControlHold, !input.isCommandHold {
|
if input.isMainAreaNumKey, input.isShiftHold, input.isOptionHold, !input.isControlHold, !input.isCommandHold {
|
||||||
// NOTE: 將來棄用 macOS 10.11 El Capitan 支援的時候,把這裡由 CFStringTransform 改為 StringTransform:
|
// NOTE: 將來棄用 macOS 10.11 El Capitan 支援的時候,把這裡由 CFStringTransform 改為 StringTransform:
|
||||||
// https://developer.apple.com/documentation/foundation/stringtransform
|
// https://developer.apple.com/documentation/foundation/stringtransform
|
||||||
|
@ -312,9 +312,9 @@ extension KeyHandler {
|
||||||
let string = NSMutableString(string: stringRAW)
|
let string = NSMutableString(string: stringRAW)
|
||||||
CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true)
|
CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true)
|
||||||
stateCallback(
|
stateCallback(
|
||||||
InputState.Committing(textToCommit: mgrPrefs.halfWidthPunctuationEnabled ? stringRAW : string as String)
|
IMEState.Committing(textToCommit: mgrPrefs.halfWidthPunctuationEnabled ? stringRAW : string as String)
|
||||||
)
|
)
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -357,10 +357,10 @@ extension KeyHandler {
|
||||||
// MARK: 全形/半形空白 (Full-Width / Half-Width Space)
|
// MARK: 全形/半形空白 (Full-Width / Half-Width Space)
|
||||||
|
|
||||||
/// 該功能僅可在當前組字區沒有任何內容的時候使用。
|
/// 該功能僅可在當前組字區沒有任何內容的時候使用。
|
||||||
if state is InputState.Empty {
|
if state.type == .ofEmpty {
|
||||||
if input.isSpace, !input.isOptionHold, !input.isControlHold, !input.isCommandHold {
|
if input.isSpace, !input.isOptionHold, !input.isControlHold, !input.isCommandHold {
|
||||||
stateCallback(InputState.Committing(textToCommit: input.isShiftHold ? " " : " "))
|
stateCallback(IMEState.Committing(textToCommit: input.isShiftHold ? " " : " "))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -371,14 +371,14 @@ extension KeyHandler {
|
||||||
if input.isShiftHold { // 這裡先不要判斷 isOptionHold。
|
if input.isShiftHold { // 這裡先不要判斷 isOptionHold。
|
||||||
switch mgrPrefs.upperCaseLetterKeyBehavior {
|
switch mgrPrefs.upperCaseLetterKeyBehavior {
|
||||||
case 1:
|
case 1:
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.lowercased()))
|
stateCallback(IMEState.Committing(textToCommit: inputText.lowercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
case 2:
|
case 2:
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
stateCallback(InputState.Committing(textToCommit: inputText.uppercased()))
|
stateCallback(IMEState.Committing(textToCommit: inputText.uppercased()))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
default: // 包括 case 0,直接塞給組字區。
|
default: // 包括 case 0,直接塞給組字區。
|
||||||
let letter = "_letter_\(inputText)"
|
let letter = "_letter_\(inputText)"
|
||||||
|
@ -401,7 +401,7 @@ extension KeyHandler {
|
||||||
/// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。
|
/// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。
|
||||||
/// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。
|
/// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。
|
||||||
/// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。
|
/// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。
|
||||||
if (state is InputState.NotEmpty) || !composer.isEmpty {
|
if state.hasComposition || !composer.isEmpty {
|
||||||
IME.prtDebugIntel(
|
IME.prtDebugIntel(
|
||||||
"Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)")
|
"Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)")
|
||||||
IME.prtDebugIntel("A9BFF20E")
|
IME.prtDebugIntel("A9BFF20E")
|
||||||
|
|
|
@ -17,101 +17,73 @@ import Foundation
|
||||||
extension KeyHandler {
|
extension KeyHandler {
|
||||||
// MARK: - 構築狀態(State Building)
|
// MARK: - 構築狀態(State Building)
|
||||||
|
|
||||||
/// 生成「正在輸入」狀態。
|
/// 生成「正在輸入」狀態。相關的內容會被拿給狀態機械用來處理在電腦螢幕上顯示的內容。
|
||||||
var buildInputtingState: InputState.Inputting {
|
var buildInputtingState: IMEState {
|
||||||
/// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容
|
/// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容
|
||||||
/// 換成由此處重新生成的組字字串(NSAttributeString,否則會不顯示)。
|
/// 換成由此處重新生成的組字字串(NSAttributeString,否則會不顯示)。
|
||||||
var tooltipParameterRef: [String] = ["", ""]
|
var displayTextSegments: [String] = compositor.walkedNodes.values.map {
|
||||||
let nodeValuesArray: [String] = compositor.walkedNodes.values.map {
|
|
||||||
guard let delegate = delegate, delegate.isVerticalTyping else { return $0 }
|
guard let delegate = delegate, delegate.isVerticalTyping else { return $0 }
|
||||||
guard mgrPrefs.hardenVerticalPunctuations else { return $0 }
|
guard mgrPrefs.hardenVerticalPunctuations else { return $0 }
|
||||||
var neta = $0
|
var neta = $0
|
||||||
ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping)
|
ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping)
|
||||||
return neta
|
return neta
|
||||||
}
|
}
|
||||||
|
var cursor = convertCursorForDisplay(compositor.cursor)
|
||||||
|
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: mgrPrefs.showHanyuPinyinInCompositionBuffer)
|
||||||
|
if !reading.isEmpty {
|
||||||
|
var newDisplayTextSegments = [String]()
|
||||||
|
var temporaryNode = ""
|
||||||
|
var charCounter = 0
|
||||||
|
for node in displayTextSegments {
|
||||||
|
for char in node {
|
||||||
|
if charCounter == cursor {
|
||||||
|
newDisplayTextSegments.append(temporaryNode)
|
||||||
|
temporaryNode = ""
|
||||||
|
newDisplayTextSegments.append(reading)
|
||||||
|
}
|
||||||
|
temporaryNode += String(char)
|
||||||
|
charCounter += 1
|
||||||
|
}
|
||||||
|
newDisplayTextSegments.append(temporaryNode)
|
||||||
|
temporaryNode = ""
|
||||||
|
}
|
||||||
|
if newDisplayTextSegments == displayTextSegments { newDisplayTextSegments.append(reading) }
|
||||||
|
displayTextSegments = newDisplayTextSegments
|
||||||
|
cursor += reading.count
|
||||||
|
}
|
||||||
|
/// 這裡生成準備要拿來回呼的「正在輸入」狀態,但還不能立即使用,因為工具提示仍未完成。
|
||||||
|
return IMEState.Inputting(displayTextSegments: displayTextSegments, cursor: cursor)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 生成「正在輸入」狀態。
|
||||||
|
func convertCursorForDisplay(_ rawCursor: Int) -> Int {
|
||||||
var composedStringCursorIndex = 0
|
var composedStringCursorIndex = 0
|
||||||
var readingCursorIndex = 0
|
var readingCursorIndex = 0
|
||||||
/// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度,
|
|
||||||
/// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。
|
|
||||||
/// 這樣就可以免除不必要的類型轉換。
|
|
||||||
for theNode in compositor.walkedNodes {
|
for theNode in compositor.walkedNodes {
|
||||||
let strNodeValue = theNode.value
|
let strNodeValue = theNode.value
|
||||||
let arrSplit: [String] = Array(strNodeValue).charComponents
|
|
||||||
let codepointCount = arrSplit.count
|
|
||||||
/// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。
|
/// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。
|
||||||
/// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來
|
/// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來
|
||||||
/// 累加、以此為依據,來校正「可見游標位置」。
|
/// 累加、以此為依據,來校正「可見游標位置」。
|
||||||
let spanningLength: Int = theNode.spanLength
|
let spanningLength: Int = theNode.keyArray.count
|
||||||
if readingCursorIndex + spanningLength <= compositor.cursor {
|
if readingCursorIndex + spanningLength <= rawCursor {
|
||||||
composedStringCursorIndex += strNodeValue.utf16.count
|
composedStringCursorIndex += strNodeValue.count
|
||||||
readingCursorIndex += spanningLength
|
readingCursorIndex += spanningLength
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if codepointCount == spanningLength {
|
if !theNode.isReadingMismatched {
|
||||||
for i in 0..<codepointCount {
|
for _ in 0..<strNodeValue.count {
|
||||||
guard readingCursorIndex < compositor.cursor else { continue }
|
guard readingCursorIndex < rawCursor else { continue }
|
||||||
composedStringCursorIndex += arrSplit[i].utf16.count
|
composedStringCursorIndex += 1
|
||||||
readingCursorIndex += 1
|
readingCursorIndex += 1
|
||||||
}
|
}
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
guard readingCursorIndex < compositor.cursor else { continue }
|
guard readingCursorIndex < rawCursor else { continue }
|
||||||
composedStringCursorIndex += strNodeValue.utf16.count
|
composedStringCursorIndex += strNodeValue.count
|
||||||
readingCursorIndex += spanningLength
|
readingCursorIndex += spanningLength
|
||||||
readingCursorIndex = min(readingCursorIndex, compositor.cursor)
|
readingCursorIndex = min(readingCursorIndex, rawCursor)
|
||||||
/// 接下來再處理這麼一種情況:
|
|
||||||
/// 某些錨點內的當前候選字詞長度與讀音長度不相等。
|
|
||||||
/// 但此時游標還是按照每個讀音單位來移動的,
|
|
||||||
/// 所以需要上下文工具提示來顯示游標的相對位置。
|
|
||||||
/// 這裡先計算一下要用在工具提示當中的顯示參數的內容。
|
|
||||||
switch compositor.cursor {
|
|
||||||
case compositor.keys.count...:
|
|
||||||
// 這裡的 compositor.cursor 數值不可能大於 readings.count,因為會被 Megrez 自動糾正。
|
|
||||||
tooltipParameterRef[0] = compositor.keys[compositor.cursor - 1]
|
|
||||||
case 0:
|
|
||||||
tooltipParameterRef[1] = compositor.keys[compositor.cursor]
|
|
||||||
default:
|
|
||||||
tooltipParameterRef[0] = compositor.keys[compositor.cursor - 1]
|
|
||||||
tooltipParameterRef[1] = compositor.keys[compositor.cursor]
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return composedStringCursorIndex
|
||||||
isCursorCuttingChar = !tooltipParameterRef[0].isEmpty || !tooltipParameterRef[1].isEmpty
|
|
||||||
|
|
||||||
/// 再接下來,藉由已經計算成功的「可見游標位置」,咱們計算一下在這個游標之前與之後的
|
|
||||||
/// 組字區內容,以便之後在這之間插入正在輸入的漢字讀音(藉由鐵恨 composer 注拼槽取得)。
|
|
||||||
var arrHead = [String.UTF16View.Element]()
|
|
||||||
var arrTail = [String.UTF16View.Element]()
|
|
||||||
|
|
||||||
for (i, n) in nodeValuesArray.joined().utf16.enumerated() {
|
|
||||||
if i < composedStringCursorIndex {
|
|
||||||
arrHead.append(n)
|
|
||||||
} else {
|
|
||||||
arrTail.append(n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 現在呢,咱們拿到了游標前後的 stringview 資料,準備著手生成要在組字區內顯示用的內容。
|
|
||||||
/// 在這對前後資料當中插入目前正在輸入的讀音資料即可。
|
|
||||||
let head = String(utf16CodeUnits: arrHead, count: arrHead.count)
|
|
||||||
let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: mgrPrefs.showHanyuPinyinInCompositionBuffer)
|
|
||||||
let tail = String(utf16CodeUnits: arrTail, count: arrTail.count)
|
|
||||||
let composedText = head + reading + tail
|
|
||||||
let cursorIndex = composedStringCursorIndex + reading.utf16.count
|
|
||||||
|
|
||||||
// 防止組字區內出現不可列印的字元。
|
|
||||||
var cleanedComposition = ""
|
|
||||||
for theChar in composedText {
|
|
||||||
guard let charCode = theChar.utf16.first else { continue }
|
|
||||||
if !(theChar.isASCII && !(charCode.isPrintable)) {
|
|
||||||
cleanedComposition += String(theChar)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 這裡生成準備要拿來回呼的「正在輸入」狀態,但還不能立即使用,因為工具提示仍未完成。
|
|
||||||
return InputState.Inputting(
|
|
||||||
displayedText: cleanedComposition, cursorIndex: cursorIndex, reading: reading, nodeValuesArray: nodeValuesArray
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - 用以生成候選詞陣列及狀態
|
// MARK: - 用以生成候選詞陣列及狀態
|
||||||
|
@ -122,14 +94,13 @@ extension KeyHandler {
|
||||||
/// - isTypingVertical: 是否縱排輸入?
|
/// - isTypingVertical: 是否縱排輸入?
|
||||||
/// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。
|
/// - Returns: 回呼一個新的選詞狀態,來就給定的候選字詞陣列資料內容顯示選字窗。
|
||||||
func buildCandidate(
|
func buildCandidate(
|
||||||
state currentState: InputState.NotEmpty,
|
state currentState: IMEStateProtocol,
|
||||||
isTypingVertical _: Bool = false
|
isTypingVertical _: Bool = false
|
||||||
) -> InputState.ChoosingCandidate {
|
) -> IMEState {
|
||||||
InputState.ChoosingCandidate(
|
IMEState.Candidates(
|
||||||
displayedText: currentState.displayedText,
|
|
||||||
cursorIndex: currentState.cursorIndex,
|
|
||||||
candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection),
|
candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection),
|
||||||
nodeValuesArray: compositor.walkedNodes.values
|
displayTextSegments: compositor.walkedNodes.values,
|
||||||
|
cursor: currentState.data.cursor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,9 +120,9 @@ extension KeyHandler {
|
||||||
/// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。
|
/// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。
|
||||||
func buildAssociatePhraseState(
|
func buildAssociatePhraseState(
|
||||||
withPair pair: Megrez.Compositor.KeyValuePaired
|
withPair pair: Megrez.Compositor.KeyValuePaired
|
||||||
) -> InputState.Associates! {
|
) -> IMEState {
|
||||||
// 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。
|
// 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。
|
||||||
InputState.Associates(
|
IMEState.Associates(
|
||||||
candidates: buildAssociatePhraseArray(withPair: pair))
|
candidates: buildAssociatePhraseArray(withPair: pair))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -165,9 +136,9 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleMarkingState(
|
func handleMarkingState(
|
||||||
_ state: InputState.Marking,
|
_ state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if input.isEsc {
|
if input.isEsc {
|
||||||
|
@ -220,18 +191,19 @@ extension KeyHandler {
|
||||||
|
|
||||||
// Shift + Left
|
// Shift + Left
|
||||||
if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold {
|
if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold {
|
||||||
var index = state.markerIndex
|
if compositor.marker > 0 {
|
||||||
if index > 0 {
|
compositor.marker -= 1
|
||||||
index = state.displayedText.utf16PreviousPosition(for: index)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
|
||||||
displayedText: state.displayedText,
|
}
|
||||||
cursorIndex: state.cursorIndex,
|
var marking = IMEState.Marking(
|
||||||
markerIndex: index,
|
displayTextSegments: state.data.displayTextSegments,
|
||||||
readings: state.readings,
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
nodeValuesArray: compositor.walkedNodes.values
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipBackupForInputting = state.tooltipBackupForInputting
|
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
|
||||||
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("1149908D")
|
IME.prtDebugIntel("1149908D")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -242,18 +214,19 @@ extension KeyHandler {
|
||||||
|
|
||||||
// Shift + Right
|
// Shift + Right
|
||||||
if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold {
|
if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold {
|
||||||
var index = state.markerIndex
|
if compositor.marker < compositor.width {
|
||||||
if index < (state.displayedText.utf16.count) {
|
compositor.marker += 1
|
||||||
index = state.displayedText.utf16NextPosition(for: index)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .front, isMarker: true)
|
||||||
displayedText: state.displayedText,
|
}
|
||||||
cursorIndex: state.cursorIndex,
|
var marking = IMEState.Marking(
|
||||||
markerIndex: index,
|
displayTextSegments: state.data.displayTextSegments,
|
||||||
readings: state.readings,
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
nodeValuesArray: compositor.walkedNodes.values
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipBackupForInputting = state.tooltipBackupForInputting
|
marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting
|
||||||
stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("9B51408D")
|
IME.prtDebugIntel("9B51408D")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -276,9 +249,9 @@ extension KeyHandler {
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handlePunctuation(
|
func handlePunctuation(
|
||||||
_ customPunctuation: String,
|
_ customPunctuation: String,
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
usingVerticalTyping isTypingVertical: Bool,
|
usingVerticalTyping isTypingVertical: Bool,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if !currentLM.hasUnigramsFor(key: customPunctuation) {
|
if !currentLM.hasUnigramsFor(key: customPunctuation) {
|
||||||
|
@ -308,8 +281,8 @@ extension KeyHandler {
|
||||||
if candidateState.candidates.count == 1 {
|
if candidateState.candidates.count == 1 {
|
||||||
clear() // 這句不要砍,因為下文可能會回呼 candidateState。
|
clear() // 這句不要砍,因為下文可能會回呼 candidateState。
|
||||||
if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty {
|
if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty {
|
||||||
stateCallback(InputState.Committing(textToCommit: candidateToCommit.1))
|
stateCallback(IMEState.Committing(textToCommit: candidateToCommit.1))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
} else {
|
} else {
|
||||||
stateCallback(candidateState)
|
stateCallback(candidateState)
|
||||||
}
|
}
|
||||||
|
@ -327,13 +300,13 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleEnter(
|
func handleEnter(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard let currentState = state as? InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
stateCallback(InputState.Committing(textToCommit: currentState.displayedText))
|
stateCallback(IMEState.Committing(textToCommit: state.displayedText))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -345,10 +318,10 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleCtrlCommandEnter(
|
func handleCtrlCommandEnter(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
var displayedText = compositor.keys.joined(separator: "-")
|
var displayedText = compositor.keys.joined(separator: "-")
|
||||||
if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin {
|
if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin {
|
||||||
|
@ -360,8 +333,8 @@ extension KeyHandler {
|
||||||
displayedText = displayedText.replacingOccurrences(of: "-", with: " ")
|
displayedText = displayedText.replacingOccurrences(of: "-", with: " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCallback(InputState.Committing(textToCommit: displayedText))
|
stateCallback(IMEState.Committing(textToCommit: displayedText))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -373,10 +346,10 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleCtrlOptionCommandEnter(
|
func handleCtrlOptionCommandEnter(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
var composed = ""
|
var composed = ""
|
||||||
|
|
||||||
|
@ -396,8 +369,8 @@ extension KeyHandler {
|
||||||
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
|
composed += key.contains("_") ? value : "<ruby>\(value)<rp>(</rp><rt>\(key)</rt><rp>)</rp></ruby>"
|
||||||
}
|
}
|
||||||
|
|
||||||
stateCallback(InputState.Committing(textToCommit: composed))
|
stateCallback(IMEState.Committing(textToCommit: composed))
|
||||||
stateCallback(InputState.Empty())
|
stateCallback(IMEState.Empty())
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -411,12 +384,12 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleBackSpace(
|
func handleBackSpace(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
// 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。
|
// 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。
|
||||||
switch mgrPrefs.specifyShiftBackSpaceKeyBehavior {
|
switch mgrPrefs.specifyShiftBackSpaceKeyBehavior {
|
||||||
|
@ -430,15 +403,13 @@ extension KeyHandler {
|
||||||
stateCallback(buildInputtingState)
|
stateCallback(buildInputtingState)
|
||||||
return true
|
return true
|
||||||
case 1:
|
case 1:
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
|
|
||||||
if input.isShiftHold, input.isOptionHold {
|
if input.isShiftHold, input.isOptionHold {
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,8 +432,7 @@ extension KeyHandler {
|
||||||
switch composer.isEmpty && compositor.isEmpty {
|
switch composer.isEmpty && compositor.isEmpty {
|
||||||
case false: stateCallback(buildInputtingState)
|
case false: stateCallback(buildInputtingState)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -477,16 +447,15 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleDelete(
|
func handleDelete(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if input.isShiftHold {
|
if input.isShiftHold {
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -509,8 +478,7 @@ extension KeyHandler {
|
||||||
switch inputting.displayedText.isEmpty {
|
switch inputting.displayedText.isEmpty {
|
||||||
case false: stateCallback(inputting)
|
case false: stateCallback(inputting)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
@ -524,11 +492,11 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleClockKey(
|
func handleClockKey(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("9B6F908D")
|
IME.prtDebugIntel("9B6F908D")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -546,11 +514,11 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleHome(
|
func handleHome(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("ABC44080")
|
IME.prtDebugIntel("ABC44080")
|
||||||
|
@ -580,11 +548,11 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleEnd(
|
func handleEnd(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("9B69908D")
|
IME.prtDebugIntel("9B69908D")
|
||||||
|
@ -613,16 +581,15 @@ extension KeyHandler {
|
||||||
/// - stateCallback: 狀態回呼。
|
/// - stateCallback: 狀態回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleEsc(
|
func handleEsc(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void
|
stateCallback: @escaping (IMEStateProtocol) -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard state is InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if mgrPrefs.escToCleanInputBuffer {
|
if mgrPrefs.escToCleanInputBuffer {
|
||||||
/// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。
|
/// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。
|
||||||
/// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。
|
/// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
} else {
|
} else {
|
||||||
if composer.isEmpty { return true }
|
if composer.isEmpty { return true }
|
||||||
/// 如果注拼槽不是空的話,則清空之。
|
/// 如果注拼槽不是空的話,則清空之。
|
||||||
|
@ -630,8 +597,7 @@ extension KeyHandler {
|
||||||
switch compositor.isEmpty {
|
switch compositor.isEmpty {
|
||||||
case false: stateCallback(buildInputtingState)
|
case false: stateCallback(buildInputtingState)
|
||||||
case true:
|
case true:
|
||||||
stateCallback(InputState.Abortion())
|
stateCallback(IMEState.Abortion())
|
||||||
stateCallback(InputState.Empty())
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return true
|
return true
|
||||||
|
@ -647,12 +613,12 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleForward(
|
func handleForward(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard let currentState = state as? InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("B3BA5257")
|
IME.prtDebugIntel("B3BA5257")
|
||||||
|
@ -663,16 +629,18 @@ extension KeyHandler {
|
||||||
|
|
||||||
if input.isShiftHold {
|
if input.isShiftHold {
|
||||||
// Shift + Right
|
// Shift + Right
|
||||||
if currentState.cursorIndex < currentState.displayedText.utf16.count {
|
if compositor.cursor < compositor.width {
|
||||||
let nextPosition = currentState.displayedText.utf16NextPosition(
|
compositor.marker = compositor.cursor + 1
|
||||||
for: currentState.cursorIndex)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking: InputState.Marking! = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .front, isMarker: true)
|
||||||
displayedText: currentState.displayedText,
|
}
|
||||||
cursorIndex: currentState.cursorIndex,
|
var marking = IMEState.Marking(
|
||||||
markerIndex: nextPosition,
|
displayTextSegments: compositor.walkedNodes.values,
|
||||||
readings: compositor.keys
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipBackupForInputting = currentState.tooltip
|
marking.data.tooltipBackupForInputting = state.tooltip
|
||||||
stateCallback(marking)
|
stateCallback(marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("BB7F6DB9")
|
IME.prtDebugIntel("BB7F6DB9")
|
||||||
|
@ -680,7 +648,6 @@ extension KeyHandler {
|
||||||
stateCallback(state)
|
stateCallback(state)
|
||||||
}
|
}
|
||||||
} else if input.isOptionHold {
|
} else if input.isOptionHold {
|
||||||
isCursorCuttingChar = false
|
|
||||||
if input.isControlHold {
|
if input.isControlHold {
|
||||||
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
||||||
}
|
}
|
||||||
|
@ -695,12 +662,10 @@ extension KeyHandler {
|
||||||
} else {
|
} else {
|
||||||
if compositor.cursor < compositor.length {
|
if compositor.cursor < compositor.length {
|
||||||
compositor.cursor += 1
|
compositor.cursor += 1
|
||||||
var inputtingState = buildInputtingState
|
if isCursorCuttingChar() {
|
||||||
if isCursorCuttingChar == true {
|
|
||||||
compositor.jumpCursorBySpan(to: .front)
|
compositor.jumpCursorBySpan(to: .front)
|
||||||
inputtingState = buildInputtingState
|
|
||||||
}
|
}
|
||||||
stateCallback(inputtingState)
|
stateCallback(buildInputtingState)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("A96AAD58")
|
IME.prtDebugIntel("A96AAD58")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -721,12 +686,12 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleBackward(
|
func handleBackward(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
input: InputSignalProtocol,
|
input: InputSignalProtocol,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
guard let currentState = state as? InputState.Inputting else { return false }
|
guard state.type == .ofInputting else { return false }
|
||||||
|
|
||||||
if !composer.isEmpty {
|
if !composer.isEmpty {
|
||||||
IME.prtDebugIntel("6ED95318")
|
IME.prtDebugIntel("6ED95318")
|
||||||
|
@ -737,16 +702,18 @@ extension KeyHandler {
|
||||||
|
|
||||||
if input.isShiftHold {
|
if input.isShiftHold {
|
||||||
// Shift + left
|
// Shift + left
|
||||||
if currentState.cursorIndex > 0 {
|
if compositor.cursor > 0 {
|
||||||
let previousPosition = currentState.displayedText.utf16PreviousPosition(
|
compositor.marker = compositor.cursor - 1
|
||||||
for: currentState.cursorIndex)
|
if isCursorCuttingChar(isMarker: true) {
|
||||||
let marking: InputState.Marking! = InputState.Marking(
|
compositor.jumpCursorBySpan(to: .rear, isMarker: true)
|
||||||
displayedText: currentState.displayedText,
|
}
|
||||||
cursorIndex: currentState.cursorIndex,
|
var marking = IMEState.Marking(
|
||||||
markerIndex: previousPosition,
|
displayTextSegments: compositor.walkedNodes.values,
|
||||||
readings: compositor.keys
|
markedReadings: Array(compositor.keys[currentMarkedRange()]),
|
||||||
|
cursor: convertCursorForDisplay(compositor.cursor),
|
||||||
|
marker: convertCursorForDisplay(compositor.marker)
|
||||||
)
|
)
|
||||||
marking.tooltipBackupForInputting = currentState.tooltip
|
marking.data.tooltipBackupForInputting = state.tooltip
|
||||||
stateCallback(marking)
|
stateCallback(marking)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("D326DEA3")
|
IME.prtDebugIntel("D326DEA3")
|
||||||
|
@ -754,7 +721,6 @@ extension KeyHandler {
|
||||||
stateCallback(state)
|
stateCallback(state)
|
||||||
}
|
}
|
||||||
} else if input.isOptionHold {
|
} else if input.isOptionHold {
|
||||||
isCursorCuttingChar = false
|
|
||||||
if input.isControlHold {
|
if input.isControlHold {
|
||||||
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback)
|
||||||
}
|
}
|
||||||
|
@ -769,12 +735,10 @@ extension KeyHandler {
|
||||||
} else {
|
} else {
|
||||||
if compositor.cursor > 0 {
|
if compositor.cursor > 0 {
|
||||||
compositor.cursor -= 1
|
compositor.cursor -= 1
|
||||||
var inputtingState = buildInputtingState
|
if isCursorCuttingChar() {
|
||||||
if isCursorCuttingChar == true {
|
|
||||||
compositor.jumpCursorBySpan(to: .rear)
|
compositor.jumpCursorBySpan(to: .rear)
|
||||||
inputtingState = buildInputtingState
|
|
||||||
}
|
}
|
||||||
stateCallback(inputtingState)
|
stateCallback(buildInputtingState)
|
||||||
} else {
|
} else {
|
||||||
IME.prtDebugIntel("7045E6F3")
|
IME.prtDebugIntel("7045E6F3")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
|
@ -795,14 +759,14 @@ extension KeyHandler {
|
||||||
/// - errorCallback: 錯誤回呼。
|
/// - errorCallback: 錯誤回呼。
|
||||||
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
/// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。
|
||||||
func handleInlineCandidateRotation(
|
func handleInlineCandidateRotation(
|
||||||
state: InputStateProtocol,
|
state: IMEStateProtocol,
|
||||||
reverseModifier: Bool,
|
reverseModifier: Bool,
|
||||||
stateCallback: @escaping (InputStateProtocol) -> Void,
|
stateCallback: @escaping (IMEStateProtocol) -> Void,
|
||||||
errorCallback: @escaping () -> Void
|
errorCallback: @escaping () -> Void
|
||||||
) -> Bool {
|
) -> Bool {
|
||||||
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
|
if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false }
|
||||||
guard state is InputState.Inputting else {
|
guard state.type == .ofInputting else {
|
||||||
guard state is InputState.Empty else {
|
guard state.type == .ofEmpty else {
|
||||||
IME.prtDebugIntel("6044F081")
|
IME.prtDebugIntel("6044F081")
|
||||||
errorCallback()
|
errorCallback()
|
||||||
return true
|
return true
|
||||||
|
|
|
@ -31,9 +31,9 @@ extension ctlInputMethod {
|
||||||
if !shouldUseHandle || (!rencentKeyHandledByKeyHandler && shouldUseHandle) {
|
if !shouldUseHandle || (!rencentKeyHandledByKeyHandler && shouldUseHandle) {
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
||||||
+ toggleASCIIMode()
|
+ (toggleASCIIMode()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
if shouldUseHandle {
|
if shouldUseHandle {
|
||||||
|
|
|
@ -32,16 +32,20 @@ class ctlInputMethod: IMKInputController {
|
||||||
|
|
||||||
// MARK: -
|
// MARK: -
|
||||||
|
|
||||||
/// 按鍵調度模組的副本。
|
|
||||||
var keyHandler: KeyHandler = .init()
|
|
||||||
/// 用以記錄當前輸入法狀態的變數。
|
|
||||||
var state: InputStateProtocol = InputState.Empty()
|
|
||||||
/// 當前這個 ctlInputMethod 副本是否處於英數輸入模式。
|
|
||||||
var isASCIIMode: Bool = false
|
|
||||||
/// 當前這個 ctlInputMethod 副本是否處於英數輸入模式(滯後項)。
|
/// 當前這個 ctlInputMethod 副本是否處於英數輸入模式(滯後項)。
|
||||||
static var isASCIIModeSituation: Bool = false
|
static var isASCIIModeSituation: Bool = false
|
||||||
/// 當前這個 ctlInputMethod 副本是否處於縱排輸入模式(滯後項)。
|
/// 當前這個 ctlInputMethod 副本是否處於縱排輸入模式(滯後項)。
|
||||||
static var isVerticalTypingSituation: Bool = false
|
static var isVerticalTypingSituation: Bool = false
|
||||||
|
/// 當前這個 ctlInputMethod 副本是否處於英數輸入模式。
|
||||||
|
var isASCIIMode: Bool = false
|
||||||
|
/// 按鍵調度模組的副本。
|
||||||
|
var keyHandler: KeyHandler = .init()
|
||||||
|
/// 用以記錄當前輸入法狀態的變數。
|
||||||
|
var state: IMEStateProtocol = IMEState.Empty() {
|
||||||
|
didSet {
|
||||||
|
IME.prtDebugIntel("Current State: \(state.type.rawValue)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// 切換當前 ctlInputMethod 副本的英數輸入模式開關。
|
/// 切換當前 ctlInputMethod 副本的英數輸入模式開關。
|
||||||
func toggleASCIIMode() -> Bool {
|
func toggleASCIIMode() -> Bool {
|
||||||
|
@ -65,15 +69,15 @@ class ctlInputMethod: IMKInputController {
|
||||||
/// 重設按鍵調度模組,會將當前尚未遞交的內容遞交出去。
|
/// 重設按鍵調度模組,會將當前尚未遞交的內容遞交出去。
|
||||||
func resetKeyHandler() {
|
func resetKeyHandler() {
|
||||||
// 過濾掉尚未完成拼寫的注音。
|
// 過濾掉尚未完成拼寫的注音。
|
||||||
if state is InputState.Inputting, mgrPrefs.trimUnfinishedReadingsOnCommit {
|
if state.type == .ofInputting, mgrPrefs.trimUnfinishedReadingsOnCommit {
|
||||||
keyHandler.composer.clear()
|
keyHandler.composer.clear()
|
||||||
handle(state: keyHandler.buildInputtingState)
|
handle(state: keyHandler.buildInputtingState)
|
||||||
}
|
}
|
||||||
if let state = state as? InputState.NotEmpty {
|
if state.hasComposition {
|
||||||
/// 將傳回的新狀態交給調度函式。
|
/// 將傳回的新狀態交給調度函式。
|
||||||
handle(state: InputState.Committing(textToCommit: state.displayedTextConverted))
|
handle(state: IMEState.Committing(textToCommit: state.displayedText))
|
||||||
}
|
}
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.Empty())
|
||||||
}
|
}
|
||||||
|
|
||||||
// MARK: - IMKInputController 方法
|
// MARK: - IMKInputController 方法
|
||||||
|
@ -115,9 +119,9 @@ class ctlInputMethod: IMKInputController {
|
||||||
} else {
|
} else {
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n"
|
||||||
+ isASCIIMode
|
+ (isASCIIMode
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,7 +131,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
|
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
|
||||||
// 強制重設當前鍵盤佈局、使其與偏好設定同步。
|
// 強制重設當前鍵盤佈局、使其與偏好設定同步。
|
||||||
setKeyLayout()
|
setKeyLayout()
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.Empty())
|
||||||
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
||||||
(NSApp.delegate as? AppDelegate)?.checkForUpdate()
|
(NSApp.delegate as? AppDelegate)?.checkForUpdate()
|
||||||
}
|
}
|
||||||
|
@ -137,7 +141,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
override func deactivateServer(_ sender: Any!) {
|
override func deactivateServer(_ sender: Any!) {
|
||||||
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
||||||
resetKeyHandler() // 這條會自動搞定 Empty 狀態。
|
resetKeyHandler() // 這條會自動搞定 Empty 狀態。
|
||||||
handle(state: InputState.Deactivated())
|
handle(state: IMEState.Deactivated())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 切換至某一個輸入法的某個副本時(比如威注音的簡體輸入法副本與繁體輸入法副本),會觸發該函式。
|
/// 切換至某一個輸入法的某個副本時(比如威注音的簡體輸入法副本與繁體輸入法副本),會觸發該函式。
|
||||||
|
@ -168,7 +172,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
|
if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier {
|
||||||
// 強制重設當前鍵盤佈局、使其與偏好設定同步。這裡的這一步也不能省略。
|
// 強制重設當前鍵盤佈局、使其與偏好設定同步。這裡的這一步也不能省略。
|
||||||
setKeyLayout()
|
setKeyLayout()
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.Empty())
|
||||||
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
} // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,8 +290,8 @@ class ctlInputMethod: IMKInputController {
|
||||||
/// - Returns: 字串內容,或者 nil。
|
/// - Returns: 字串內容,或者 nil。
|
||||||
override func composedString(_ sender: Any!) -> Any! {
|
override func composedString(_ sender: Any!) -> Any! {
|
||||||
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
||||||
guard let state = state as? InputState.NotEmpty else { return "" }
|
guard state.hasComposition else { return "" }
|
||||||
return state.committingBufferConverted
|
return state.displayedText
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 輸入法要被換掉或關掉的時候,要做的事情。
|
/// 輸入法要被換掉或關掉的時候,要做的事情。
|
||||||
|
@ -307,7 +311,7 @@ class ctlInputMethod: IMKInputController {
|
||||||
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
_ = sender // 防止格式整理工具毀掉與此對應的參數。
|
||||||
var arrResult = [String]()
|
var arrResult = [String]()
|
||||||
|
|
||||||
// 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。
|
// 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。
|
||||||
func handleCandidatesPrepared(_ candidates: [(String, String)], prefix: String = "") {
|
func handleCandidatesPrepared(_ candidates: [(String, String)], prefix: String = "") {
|
||||||
for theCandidate in candidates {
|
for theCandidate in candidates {
|
||||||
let theConverted = IME.kanjiConversionIfRequired(theCandidate.1)
|
let theConverted = IME.kanjiConversionIfRequired(theCandidate.1)
|
||||||
|
@ -323,12 +327,12 @@ class ctlInputMethod: IMKInputController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let state = state as? InputState.Associates {
|
if state.type == .ofAssociates {
|
||||||
handleCandidatesPrepared(state.candidates, prefix: "⇧")
|
handleCandidatesPrepared(state.candidates, prefix: "⇧")
|
||||||
} else if let state = state as? InputState.SymbolTable {
|
} else if state.type == .ofSymbolTable {
|
||||||
// 分類符號選單不會出現同符異音項、不需要康熙 / JIS 轉換,所以使用簡化過的處理方式。
|
// 分類符號選單不會出現同符異音項、不需要康熙 / JIS 轉換,所以使用簡化過的處理方式。
|
||||||
arrResult = state.candidates.map(\.1)
|
arrResult = state.candidates.map(\.1)
|
||||||
} else if let state = state as? InputState.ChoosingCandidate {
|
} else if state.type == .ofCandidates {
|
||||||
guard !state.candidates.isEmpty else { return .init() }
|
guard !state.candidates.isEmpty else { return .init() }
|
||||||
if state.candidates[0].0.contains("_punctuation") {
|
if state.candidates[0].0.contains("_punctuation") {
|
||||||
arrResult = state.candidates.map(\.1) // 標點符號選單處理。
|
arrResult = state.candidates.map(\.1) // 標點符號選單處理。
|
||||||
|
@ -361,17 +365,16 @@ class ctlInputMethod: IMKInputController {
|
||||||
/// - Parameter candidateString: 已經確認的候選字詞內容。
|
/// - Parameter candidateString: 已經確認的候選字詞內容。
|
||||||
override open func candidateSelected(_ candidateString: NSAttributedString!) {
|
override open func candidateSelected(_ candidateString: NSAttributedString!) {
|
||||||
let candidateString: NSAttributedString = candidateString ?? .init(string: "")
|
let candidateString: NSAttributedString = candidateString ?? .init(string: "")
|
||||||
if state is InputState.Associates {
|
if state.type == .ofAssociates {
|
||||||
if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter {
|
||||||
handle(state: InputState.Abortion())
|
handle(state: IMEState.Abortion())
|
||||||
handle(state: InputState.Empty())
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var indexDeducted = 0
|
var indexDeducted = 0
|
||||||
|
|
||||||
// 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。
|
// 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。
|
||||||
func handleCandidatesSelected(_ candidates: [(String, String)], prefix: String = "") {
|
func handleCandidatesSelected(_ candidates: [(String, String)], prefix: String = "") {
|
||||||
for (i, neta) in candidates.enumerated() {
|
for (i, neta) in candidates.enumerated() {
|
||||||
let theConverted = IME.kanjiConversionIfRequired(neta.1)
|
let theConverted = IME.kanjiConversionIfRequired(neta.1)
|
||||||
|
@ -401,11 +404,11 @@ class ctlInputMethod: IMKInputController {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let state = state as? InputState.Associates {
|
if state.type == .ofAssociates {
|
||||||
handleCandidatesSelected(state.candidates, prefix: "⇧")
|
handleCandidatesSelected(state.candidates, prefix: "⇧")
|
||||||
} else if let state = state as? InputState.SymbolTable {
|
} else if state.type == .ofSymbolTable {
|
||||||
handleSymbolCandidatesSelected(state.candidates)
|
handleSymbolCandidatesSelected(state.candidates)
|
||||||
} else if let state = state as? InputState.ChoosingCandidate {
|
} else if state.type == .ofCandidates {
|
||||||
guard !state.candidates.isEmpty else { return }
|
guard !state.candidates.isEmpty else { return }
|
||||||
if state.candidates[0].0.contains("_punctuation") {
|
if state.candidates[0].0.contains("_punctuation") {
|
||||||
handleSymbolCandidatesSelected(state.candidates) // 標點符號選單處理。
|
handleSymbolCandidatesSelected(state.candidates) // 標點符號選單處理。
|
||||||
|
|
|
@ -37,21 +37,20 @@ extension ctlInputMethod: KeyHandlerDelegate {
|
||||||
ctlCandidate(controller, didSelectCandidateAtIndex: index)
|
ctlCandidate(controller, didSelectCandidateAtIndex: index)
|
||||||
}
|
}
|
||||||
|
|
||||||
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol, addToFilter: Bool)
|
func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool)
|
||||||
-> Bool
|
-> Bool
|
||||||
{
|
{
|
||||||
guard let state = state as? InputState.Marking else { return false }
|
guard state.type == .ofMarking else { return false }
|
||||||
if state.bufferReadingCountMisMatch { return false }
|
|
||||||
let refInputModeReversed: InputMode =
|
let refInputModeReversed: InputMode =
|
||||||
(keyHandler.inputMode == InputMode.imeModeCHT)
|
(keyHandler.inputMode == InputMode.imeModeCHT)
|
||||||
? InputMode.imeModeCHS : InputMode.imeModeCHT
|
? InputMode.imeModeCHS : InputMode.imeModeCHT
|
||||||
if !mgrLangModel.writeUserPhrase(
|
if !mgrLangModel.writeUserPhrase(
|
||||||
state.userPhrase, inputMode: keyHandler.inputMode,
|
state.data.userPhrase, inputMode: keyHandler.inputMode,
|
||||||
areWeDuplicating: state.chkIfUserPhraseExists,
|
areWeDuplicating: state.data.chkIfUserPhraseExists,
|
||||||
areWeDeleting: addToFilter
|
areWeDeleting: addToFilter
|
||||||
)
|
)
|
||||||
|| !mgrLangModel.writeUserPhrase(
|
|| !mgrLangModel.writeUserPhrase(
|
||||||
state.userPhraseConverted, inputMode: refInputModeReversed,
|
state.data.userPhraseConverted, inputMode: refInputModeReversed,
|
||||||
areWeDuplicating: false,
|
areWeDuplicating: false,
|
||||||
areWeDeleting: addToFilter
|
areWeDeleting: addToFilter
|
||||||
)
|
)
|
||||||
|
@ -65,7 +64,7 @@ extension ctlInputMethod: KeyHandlerDelegate {
|
||||||
// MARK: - Candidate Controller Delegate
|
// MARK: - Candidate Controller Delegate
|
||||||
|
|
||||||
extension ctlInputMethod: ctlCandidateDelegate {
|
extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
var isAssociatedPhrasesState: Bool { state is InputState.Associates }
|
var isAssociatedPhrasesState: Bool { state.type == .ofAssociates }
|
||||||
|
|
||||||
/// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。
|
/// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。
|
||||||
/// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。
|
/// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。
|
||||||
|
@ -78,9 +77,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
|
|
||||||
func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int {
|
func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int {
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
return state.candidates.count
|
|
||||||
} else if let state = state as? InputState.Associates {
|
|
||||||
return state.candidates.count
|
return state.candidates.count
|
||||||
}
|
}
|
||||||
return 0
|
return 0
|
||||||
|
@ -91,9 +88,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
/// - Returns: 候選字詞陣列(字音配對)。
|
/// - Returns: 候選字詞陣列(字音配對)。
|
||||||
func candidatesForController(_ controller: ctlCandidateProtocol) -> [(String, String)] {
|
func candidatesForController(_ controller: ctlCandidateProtocol) -> [(String, String)] {
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
return state.candidates
|
|
||||||
} else if let state = state as? InputState.Associates {
|
|
||||||
return state.candidates
|
return state.candidates
|
||||||
}
|
}
|
||||||
return .init()
|
return .init()
|
||||||
|
@ -103,9 +98,7 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
-> (String, String)
|
-> (String, String)
|
||||||
{
|
{
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
return state.candidates[index]
|
|
||||||
} else if let state = state as? InputState.Associates {
|
|
||||||
return state.candidates[index]
|
return state.candidates[index]
|
||||||
}
|
}
|
||||||
return ("", "")
|
return ("", "")
|
||||||
|
@ -114,20 +107,20 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
func ctlCandidate(_ controller: ctlCandidateProtocol, didSelectCandidateAtIndex index: Int) {
|
func ctlCandidate(_ controller: ctlCandidateProtocol, didSelectCandidateAtIndex index: Int) {
|
||||||
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
_ = controller // 防止格式整理工具毀掉與此對應的參數。
|
||||||
|
|
||||||
if let state = state as? InputState.SymbolTable,
|
if state.type == .ofSymbolTable,
|
||||||
let node = state.node.children?[index]
|
let node = state.node.children?[index]
|
||||||
{
|
{
|
||||||
if let children = node.children, !children.isEmpty {
|
if let children = node.children, !children.isEmpty {
|
||||||
handle(state: InputState.Empty()) // 防止縱橫排選字窗同時出現
|
handle(state: IMEState.Empty()) // 防止縱橫排選字窗同時出現
|
||||||
handle(state: InputState.SymbolTable(node: node, previous: state.node))
|
handle(state: IMEState.SymbolTable(node: node))
|
||||||
} else {
|
} else {
|
||||||
handle(state: InputState.Committing(textToCommit: node.title))
|
handle(state: IMEState.Committing(textToCommit: node.title))
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.Empty())
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
|
||||||
let selectedValue = state.candidates[index]
|
let selectedValue = state.candidates[index]
|
||||||
keyHandler.fixNode(
|
keyHandler.fixNode(
|
||||||
candidate: selectedValue, respectCursorPushing: true,
|
candidate: selectedValue, respectCursorPushing: true,
|
||||||
|
@ -137,16 +130,15 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
let inputting = keyHandler.buildInputtingState
|
let inputting = keyHandler.buildInputtingState
|
||||||
|
|
||||||
if mgrPrefs.useSCPCTypingMode {
|
if mgrPrefs.useSCPCTypingMode {
|
||||||
handle(state: InputState.Committing(textToCommit: inputting.displayedTextConverted))
|
handle(state: IMEState.Committing(textToCommit: inputting.displayedText))
|
||||||
// 此時是逐字選字模式,所以「selectedValue.1」是單個字、不用追加處理。
|
// 此時是逐字選字模式,所以「selectedValue.1」是單個字、不用追加處理。
|
||||||
if mgrPrefs.associatedPhrasesEnabled,
|
if mgrPrefs.associatedPhrasesEnabled {
|
||||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
let associates = keyHandler.buildAssociatePhraseState(
|
||||||
withPair: .init(key: selectedValue.0, value: selectedValue.1)
|
withPair: .init(key: selectedValue.0, value: selectedValue.1)
|
||||||
), !associatePhrases.candidates.isEmpty
|
)
|
||||||
{
|
handle(state: associates.candidates.isEmpty ? IMEState.Empty() : associates)
|
||||||
handle(state: associatePhrases)
|
|
||||||
} else {
|
} else {
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.Empty())
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
handle(state: inputting)
|
handle(state: inputting)
|
||||||
|
@ -154,24 +146,25 @@ extension ctlInputMethod: ctlCandidateDelegate {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if let state = state as? InputState.Associates {
|
if state.type == .ofAssociates {
|
||||||
let selectedValue = state.candidates[index]
|
let selectedValue = state.candidates[index]
|
||||||
handle(state: InputState.Committing(textToCommit: selectedValue.1))
|
handle(state: IMEState.Committing(textToCommit: selectedValue.1))
|
||||||
// 此時是聯想詞選字模式,所以「selectedValue.1」必須只保留最後一個字。
|
// 此時是聯想詞選字模式,所以「selectedValue.1」必須只保留最後一個字。
|
||||||
// 不然的話,一旦你選中了由多個字組成的聯想候選詞,則連續聯想會被打斷。
|
// 不然的話,一旦你選中了由多個字組成的聯想候選詞,則連續聯想會被打斷。
|
||||||
guard let valueKept = selectedValue.1.last else {
|
guard let valueKept = selectedValue.1.last else {
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.Empty())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if mgrPrefs.associatedPhrasesEnabled,
|
if mgrPrefs.associatedPhrasesEnabled {
|
||||||
let associatePhrases = keyHandler.buildAssociatePhraseState(
|
let associates = keyHandler.buildAssociatePhraseState(
|
||||||
withPair: .init(key: selectedValue.0, value: String(valueKept))
|
withPair: .init(key: selectedValue.0, value: String(valueKept))
|
||||||
), !associatePhrases.candidates.isEmpty
|
)
|
||||||
{
|
if !associates.candidates.isEmpty {
|
||||||
handle(state: associatePhrases)
|
handle(state: associates)
|
||||||
return
|
return
|
||||||
|
}
|
||||||
}
|
}
|
||||||
handle(state: InputState.Empty())
|
handle(state: IMEState.Empty())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,10 +13,10 @@ import Cocoa
|
||||||
// MARK: - Tooltip Display and Candidate Display Methods
|
// MARK: - Tooltip Display and Candidate Display Methods
|
||||||
|
|
||||||
extension ctlInputMethod {
|
extension ctlInputMethod {
|
||||||
func show(tooltip: String, displayedText: String, cursorIndex: Int) {
|
func show(tooltip: String, displayedText: String, u16Cursor: Int) {
|
||||||
guard let client = client() else { return }
|
guard let client = client() else { return }
|
||||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||||
var cursor = cursorIndex
|
var cursor = u16Cursor
|
||||||
if cursor == displayedText.count, cursor != 0 {
|
if cursor == displayedText.count, cursor != 0 {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
|
@ -38,24 +38,14 @@ extension ctlInputMethod {
|
||||||
ctlInputMethod.tooltipController.show(tooltip: tooltip, at: finalOrigin)
|
ctlInputMethod.tooltipController.show(tooltip: tooltip, at: finalOrigin)
|
||||||
}
|
}
|
||||||
|
|
||||||
func show(candidateWindowWith state: InputStateProtocol) {
|
func show(candidateWindowWith state: IMEStateProtocol) {
|
||||||
guard let client = client() else { return }
|
guard let client = client() else { return }
|
||||||
var isTypingVertical: Bool {
|
|
||||||
if state.type == .ofCandidates {
|
|
||||||
return ctlInputMethod.isVerticalTypingSituation
|
|
||||||
} else if state.type == ..ofAssociates {
|
|
||||||
return ctlInputMethod.isVerticalTypingSituation
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
var isCandidateWindowVertical: Bool {
|
var isCandidateWindowVertical: Bool {
|
||||||
var candidates: [(String, String)] = .init()
|
var candidates: [(String, String)] = .init()
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if state.isCandidateContainer {
|
||||||
candidates = state.candidates
|
|
||||||
} else if let state = state as? InputState.Associates {
|
|
||||||
candidates = state.candidates
|
candidates = state.candidates
|
||||||
}
|
}
|
||||||
if isTypingVertical { return true }
|
if isVerticalTyping { return true }
|
||||||
// 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。
|
// 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。
|
||||||
guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false }
|
guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false }
|
||||||
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
// 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。
|
||||||
|
@ -106,7 +96,7 @@ extension ctlInputMethod {
|
||||||
let candidateKeys = mgrPrefs.candidateKeys
|
let candidateKeys = mgrPrefs.candidateKeys
|
||||||
let keyLabels =
|
let keyLabels =
|
||||||
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
|
candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys)
|
||||||
let keyLabelSuffix = state is InputState.Associates ? "^" : ""
|
let keyLabelSuffix = state.type == .ofAssociates ? "^" : ""
|
||||||
ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map {
|
ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map {
|
||||||
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
CandidateKeyLabel(key: String($0), displayedText: String($0) + keyLabelSuffix)
|
||||||
}
|
}
|
||||||
|
@ -126,8 +116,8 @@ extension ctlInputMethod {
|
||||||
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
var lineHeightRect = NSRect(x: 0.0, y: 0.0, width: 16.0, height: 16.0)
|
||||||
var cursor = 0
|
var cursor = 0
|
||||||
|
|
||||||
if let state = state as? InputState.ChoosingCandidate {
|
if [.ofCandidates, .ofSymbolTable].contains(state.type) {
|
||||||
cursor = state.cursorIndex
|
cursor = state.data.cursor
|
||||||
if cursor == state.displayedText.count, cursor != 0 {
|
if cursor == state.displayedText.count, cursor != 0 {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
|
@ -140,7 +130,7 @@ extension ctlInputMethod {
|
||||||
cursor -= 1
|
cursor -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if isTypingVertical {
|
if isVerticalTyping {
|
||||||
ctlInputMethod.ctlCandidateCurrent.set(
|
ctlInputMethod.ctlCandidateCurrent.set(
|
||||||
windowTopLeftPoint: NSPoint(
|
windowTopLeftPoint: NSPoint(
|
||||||
x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0
|
x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0
|
||||||
|
|
|
@ -18,29 +18,77 @@ extension ctlInputMethod {
|
||||||
/// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數,
|
/// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數,
|
||||||
/// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。
|
/// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。
|
||||||
/// - Parameter newState: 新狀態。
|
/// - Parameter newState: 新狀態。
|
||||||
func handle(state newState: InputStateProtocol) {
|
func handle(state newState: IMEStateProtocol) {
|
||||||
let prevState = state
|
let previous = state
|
||||||
state = newState
|
state = newState
|
||||||
|
switch state.type {
|
||||||
switch newState {
|
case .ofDeactivated:
|
||||||
case let newState as InputState.Deactivated:
|
ctlInputMethod.ctlCandidateCurrent.delegate = nil
|
||||||
handle(state: newState, previous: prevState)
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
case let newState as InputState.Empty:
|
ctlInputMethod.tooltipController.hide()
|
||||||
handle(state: newState, previous: prevState)
|
if previous.hasComposition {
|
||||||
case let newState as InputState.Abortion:
|
commit(text: previous.displayedText)
|
||||||
handle(state: newState, previous: prevState)
|
}
|
||||||
case let newState as InputState.Committing:
|
clearInlineDisplay()
|
||||||
handle(state: newState, previous: prevState)
|
// 最後一道保險
|
||||||
case let newState as InputState.Inputting:
|
keyHandler.clear()
|
||||||
handle(state: newState, previous: prevState)
|
case .ofEmpty, .ofAbortion:
|
||||||
case let newState as InputState.Marking:
|
var previous = previous
|
||||||
handle(state: newState, previous: prevState)
|
if state.type == .ofAbortion {
|
||||||
case let newState as InputState.ChoosingCandidate:
|
state = IMEState.Empty()
|
||||||
handle(state: newState, previous: prevState)
|
previous = state
|
||||||
case let newState as InputState.Associates:
|
}
|
||||||
handle(state: newState, previous: prevState)
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
case let newState as InputState.SymbolTable:
|
ctlInputMethod.tooltipController.hide()
|
||||||
handle(state: newState, previous: prevState)
|
// 全專案用以判斷「.Abortion」的地方僅此一處。
|
||||||
|
if previous.hasComposition, state.type != .ofAbortion {
|
||||||
|
commit(text: previous.displayedText)
|
||||||
|
}
|
||||||
|
// 在這裡手動再取消一次選字窗與工具提示的顯示,可謂雙重保險。
|
||||||
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
|
ctlInputMethod.tooltipController.hide()
|
||||||
|
clearInlineDisplay()
|
||||||
|
// 最後一道保險
|
||||||
|
keyHandler.clear()
|
||||||
|
case .ofCommitting:
|
||||||
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
|
ctlInputMethod.tooltipController.hide()
|
||||||
|
let textToCommit = state.textToCommit
|
||||||
|
if !textToCommit.isEmpty { commit(text: textToCommit) }
|
||||||
|
clearInlineDisplay()
|
||||||
|
// 最後一道保險
|
||||||
|
keyHandler.clear()
|
||||||
|
case .ofInputting:
|
||||||
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
|
ctlInputMethod.tooltipController.hide()
|
||||||
|
let textToCommit = state.textToCommit
|
||||||
|
if !textToCommit.isEmpty { commit(text: textToCommit) }
|
||||||
|
setInlineDisplayWithCursor()
|
||||||
|
if !state.tooltip.isEmpty {
|
||||||
|
show(
|
||||||
|
tooltip: state.tooltip, displayedText: state.displayedText,
|
||||||
|
u16Cursor: state.data.u16Cursor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case .ofMarking:
|
||||||
|
ctlInputMethod.ctlCandidateCurrent.visible = false
|
||||||
|
setInlineDisplayWithCursor()
|
||||||
|
if state.tooltip.isEmpty {
|
||||||
|
ctlInputMethod.tooltipController.hide()
|
||||||
|
} else {
|
||||||
|
let cursorReference: Int = {
|
||||||
|
if state.data.marker >= state.data.cursor { return state.data.u16Cursor }
|
||||||
|
return state.data.u16Marker // 這樣可以讓工具提示視窗始終盡量往書寫方向的後方顯示。
|
||||||
|
}()
|
||||||
|
show(
|
||||||
|
tooltip: state.tooltip, displayedText: state.displayedText,
|
||||||
|
u16Cursor: cursorReference
|
||||||
|
)
|
||||||
|
}
|
||||||
|
case .ofCandidates, .ofAssociates, .ofSymbolTable:
|
||||||
|
ctlInputMethod.tooltipController.hide()
|
||||||
|
setInlineDisplayWithCursor()
|
||||||
|
show(candidateWindowWith: state)
|
||||||
default: break
|
default: break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -48,7 +96,7 @@ extension ctlInputMethod {
|
||||||
/// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。
|
/// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。
|
||||||
func setInlineDisplayWithCursor() {
|
func setInlineDisplayWithCursor() {
|
||||||
guard let client = client() else { return }
|
guard let client = client() else { return }
|
||||||
if let state = state as? InputState.Associates {
|
if state.type == .ofAssociates {
|
||||||
client.setMarkedText(
|
client.setMarkedText(
|
||||||
state.attributedString, selectionRange: NSRange(location: 0, length: 0),
|
state.attributedString, selectionRange: NSRange(location: 0, length: 0),
|
||||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||||
|
@ -56,49 +104,19 @@ extension ctlInputMethod {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let state = state as? InputState.NotEmpty else {
|
if state.hasComposition || state.isCandidateContainer {
|
||||||
clearInlineDisplay()
|
/// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度
|
||||||
|
/// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。
|
||||||
|
/// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。
|
||||||
|
client.setMarkedText(
|
||||||
|
state.attributedString, selectionRange: NSRange(location: state.data.u16Cursor, length: 0),
|
||||||
|
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||||
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var identifier: AnyObject {
|
// 其它情形。
|
||||||
switch IME.currentInputMode {
|
clearInlineDisplay()
|
||||||
case InputMode.imeModeCHS:
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
return "zh-Hans" as AnyObject
|
|
||||||
}
|
|
||||||
case InputMode.imeModeCHT:
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
return (mgrPrefs.shiftJISShinjitaiOutputEnabled || mgrPrefs.chineseConversionEnabled)
|
|
||||||
? "ja" as AnyObject : "zh-Hant" as AnyObject
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
break
|
|
||||||
}
|
|
||||||
return "" as AnyObject
|
|
||||||
}
|
|
||||||
|
|
||||||
// [Shiki's Note] This might needs to be bug-reported to Apple:
|
|
||||||
// The LanguageIdentifier attribute of an NSAttributeString designated to
|
|
||||||
// IMK Client().SetMarkedText won't let the actual font respect your languageIdentifier
|
|
||||||
// settings. Still, this might behaves as Apple's current expectation, I'm afraid.
|
|
||||||
if #available(macOS 12.0, *) {
|
|
||||||
state.attributedString.setAttributes(
|
|
||||||
[.languageIdentifier: identifier],
|
|
||||||
range: NSRange(
|
|
||||||
location: 0,
|
|
||||||
length: state.displayedText.utf16.count
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度
|
|
||||||
/// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。
|
|
||||||
/// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。
|
|
||||||
client.setMarkedText(
|
|
||||||
state.attributedString, selectionRange: NSRange(location: state.cursorIndex, length: 0),
|
|
||||||
replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。
|
/// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。
|
||||||
|
@ -123,109 +141,4 @@ extension ctlInputMethod {
|
||||||
buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
buffer, replacementRange: NSRange(location: NSNotFound, length: NSNotFound)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private func handle(state: InputState.Deactivated, previous: InputStateProtocol) {
|
|
||||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.ctlCandidateCurrent.delegate = nil
|
|
||||||
ctlInputMethod.ctlCandidateCurrent.visible = false
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
if let previous = previous as? InputState.NotEmpty {
|
|
||||||
commit(text: previous.committingBufferConverted)
|
|
||||||
}
|
|
||||||
clearInlineDisplay()
|
|
||||||
// 最後一道保險
|
|
||||||
keyHandler.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(state: InputState.Empty, previous: InputStateProtocol) {
|
|
||||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.ctlCandidateCurrent.visible = false
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
// 全專案用以判斷「.Abortion」的地方僅此一處。
|
|
||||||
if let previous = previous as? InputState.NotEmpty,
|
|
||||||
!(state is InputState.Abortion)
|
|
||||||
{
|
|
||||||
commit(text: previous.committingBufferConverted)
|
|
||||||
}
|
|
||||||
// 在這裡手動再取消一次選字窗與工具提示的顯示,可謂雙重保險。
|
|
||||||
ctlInputMethod.ctlCandidateCurrent.visible = false
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
clearInlineDisplay()
|
|
||||||
// 最後一道保險
|
|
||||||
keyHandler.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(
|
|
||||||
state: InputState.Abortion, previous: InputStateProtocol
|
|
||||||
) {
|
|
||||||
_ = state // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
// 這個函式就是去掉 previous state 使得沒有任何東西可以 commit。
|
|
||||||
handle(state: InputState.Empty())
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(state: InputState.Committing, previous: InputStateProtocol) {
|
|
||||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.ctlCandidateCurrent.visible = false
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
let textToCommit = state.textToCommit
|
|
||||||
if !textToCommit.isEmpty {
|
|
||||||
commit(text: textToCommit)
|
|
||||||
}
|
|
||||||
clearInlineDisplay()
|
|
||||||
// 最後一道保險
|
|
||||||
keyHandler.clear()
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(state: InputState.Inputting, previous: InputStateProtocol) {
|
|
||||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.ctlCandidateCurrent.visible = false
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
let textToCommit = state.textToCommit
|
|
||||||
if !textToCommit.isEmpty {
|
|
||||||
commit(text: textToCommit)
|
|
||||||
}
|
|
||||||
setInlineDisplayWithCursor()
|
|
||||||
if !state.tooltip.isEmpty {
|
|
||||||
show(
|
|
||||||
tooltip: state.tooltip, displayedText: state.displayedText,
|
|
||||||
cursorIndex: state.cursorIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(state: InputState.Marking, previous: InputStateProtocol) {
|
|
||||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.ctlCandidateCurrent.visible = false
|
|
||||||
setInlineDisplayWithCursor()
|
|
||||||
if state.tooltip.isEmpty {
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
} else {
|
|
||||||
show(
|
|
||||||
tooltip: state.tooltip, displayedText: state.displayedText,
|
|
||||||
cursorIndex: state.markerIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(state: InputState.ChoosingCandidate, previous: InputStateProtocol) {
|
|
||||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
setInlineDisplayWithCursor()
|
|
||||||
show(candidateWindowWith: state)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(state: InputState.SymbolTable, previous: InputStateProtocol) {
|
|
||||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
setInlineDisplayWithCursor()
|
|
||||||
show(candidateWindowWith: state)
|
|
||||||
}
|
|
||||||
|
|
||||||
private func handle(state: InputState.Associates, previous: InputStateProtocol) {
|
|
||||||
_ = previous // 防止格式整理工具毀掉與此對應的參數。
|
|
||||||
ctlInputMethod.tooltipController.hide()
|
|
||||||
setInlineDisplayWithCursor()
|
|
||||||
show(candidateWindowWith: state)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -212,9 +212,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n"
|
message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleSCPCTypingModeEnabled()
|
+ (mgrPrefs.toggleSCPCTypingModeEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -222,9 +222,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n"
|
message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleChineseConversionEnabled()
|
+ (mgrPrefs.toggleChineseConversionEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,9 +232,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n"
|
message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
|
+ (mgrPrefs.toggleShiftJISShinjitaiOutputEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -242,9 +242,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n"
|
message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleCurrencyNumeralsEnabled()
|
+ (mgrPrefs.toggleCurrencyNumeralsEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -252,9 +252,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n"
|
message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleHalfWidthPunctuationEnabled()
|
+ (mgrPrefs.toggleHalfWidthPunctuationEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -262,9 +262,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n"
|
message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleCNS11643Enabled()
|
+ (mgrPrefs.toggleCNS11643Enabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -272,9 +272,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n"
|
message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleSymbolInputEnabled()
|
+ (mgrPrefs.toggleSymbolInputEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -282,9 +282,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n"
|
message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n"
|
||||||
+ mgrPrefs.toggleAssociatedPhrasesEnabled()
|
+ (mgrPrefs.toggleAssociatedPhrasesEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,9 +292,9 @@ extension ctlInputMethod {
|
||||||
resetKeyHandler()
|
resetKeyHandler()
|
||||||
NotifierController.notify(
|
NotifierController.notify(
|
||||||
message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n"
|
message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n"
|
||||||
+ mgrPrefs.togglePhraseReplacementEnabled()
|
+ (mgrPrefs.togglePhraseReplacementEnabled()
|
||||||
? NSLocalizedString("NotificationSwitchON", comment: "")
|
? NSLocalizedString("NotificationSwitchON", comment: "")
|
||||||
: NSLocalizedString("NotificationSwitchOFF", comment: "")
|
: NSLocalizedString("NotificationSwitchOFF", comment: ""))
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -18,17 +18,26 @@ public class SymbolNode {
|
||||||
init(_ title: String, _ children: [SymbolNode]? = nil, previous: SymbolNode? = nil) {
|
init(_ title: String, _ children: [SymbolNode]? = nil, previous: SymbolNode? = nil) {
|
||||||
self.title = title
|
self.title = title
|
||||||
self.children = children
|
self.children = children
|
||||||
|
self.children?.forEach {
|
||||||
|
$0.previous = self
|
||||||
|
}
|
||||||
self.previous = previous
|
self.previous = previous
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ title: String, symbols: String) {
|
init(_ title: String, symbols: String) {
|
||||||
self.title = title
|
self.title = title
|
||||||
children = Array(symbols).map { SymbolNode(String($0), nil) }
|
children = Array(symbols).map { SymbolNode(String($0), nil) }
|
||||||
|
children?.forEach {
|
||||||
|
$0.previous = self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
init(_ title: String, symbols: [String]) {
|
init(_ title: String, symbols: [String]) {
|
||||||
self.title = title
|
self.title = title
|
||||||
children = symbols.map { SymbolNode($0, nil) }
|
children = symbols.map { SymbolNode($0, nil) }
|
||||||
|
children?.forEach {
|
||||||
|
$0.previous = self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static func parseUserSymbolNodeData() {
|
static func parseUserSymbolNodeData() {
|
||||||
|
|
|
@ -136,7 +136,6 @@
|
||||||
6ACA41FD15FC1D9000935EF6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41F015FC1D9000935EF6 /* MainMenu.xib */; };
|
6ACA41FD15FC1D9000935EF6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41F015FC1D9000935EF6 /* MainMenu.xib */; };
|
||||||
6ACA420215FC1E5200935EF6 /* vChewing.app in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EA215FC0D2D00ABF4B3 /* vChewing.app */; };
|
6ACA420215FC1E5200935EF6 /* vChewing.app in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EA215FC0D2D00ABF4B3 /* vChewing.app */; };
|
||||||
D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427F76B278CA1BA004A2160 /* AppDelegate.swift */; };
|
D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = D427F76B278CA1BA004A2160 /* AppDelegate.swift */; };
|
||||||
D461B792279DAC010070E734 /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D461B791279DAC010070E734 /* InputState.swift */; };
|
|
||||||
D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; };
|
D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; };
|
||||||
D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */; };
|
D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */; };
|
||||||
D47F7DD0278C0897002F9DD7 /* ctlNonModalAlertWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */; };
|
D47F7DD0278C0897002F9DD7 /* ctlNonModalAlertWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */; };
|
||||||
|
@ -370,7 +369,6 @@
|
||||||
6ACA41EF15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
6ACA41EF15FC1D9000935EF6 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/Localizable.strings; sourceTree = "<group>"; };
|
||||||
6ACA41F215FC1D9000935EF6 /* Installer-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Installer-Info.plist"; path = "Installer/Installer-Info.plist"; sourceTree = SOURCE_ROOT; };
|
6ACA41F215FC1D9000935EF6 /* Installer-Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = "Installer-Info.plist"; path = "Installer/Installer-Info.plist"; sourceTree = SOURCE_ROOT; };
|
||||||
D427F76B278CA1BA004A2160 /* AppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = AppDelegate.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D427F76B278CA1BA004A2160 /* AppDelegate.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = AppDelegate.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputState.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
|
||||||
D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = main.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = main.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlPrefWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlPrefWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlNonModalAlertWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlNonModalAlertWindow.swift; sourceTree = "<group>"; tabWidth = 2; usesTabs = 0; };
|
||||||
|
@ -504,7 +502,6 @@
|
||||||
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
|
5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */,
|
||||||
5BF56F9728C39A2700DD6839 /* IMEState.swift */,
|
5BF56F9728C39A2700DD6839 /* IMEState.swift */,
|
||||||
5BF56F9928C39D1800DD6839 /* IMEStateData.swift */,
|
5BF56F9928C39D1800DD6839 /* IMEStateData.swift */,
|
||||||
D461B791279DAC010070E734 /* InputState.swift */,
|
|
||||||
5BD0113C2818543900609769 /* KeyHandler_Core.swift */,
|
5BD0113C2818543900609769 /* KeyHandler_Core.swift */,
|
||||||
5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */,
|
5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */,
|
||||||
5BE3779F288FED8D0037365B /* KeyHandler_HandleComposition.swift */,
|
5BE3779F288FED8D0037365B /* KeyHandler_HandleComposition.swift */,
|
||||||
|
@ -1211,7 +1208,6 @@
|
||||||
5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */,
|
5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */,
|
||||||
5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */,
|
5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */,
|
||||||
5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */,
|
5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */,
|
||||||
D461B792279DAC010070E734 /* InputState.swift in Sources */,
|
|
||||||
5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */,
|
5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */,
|
||||||
5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */,
|
5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */,
|
||||||
D47B92C027972AD100458394 /* main.swift in Sources */,
|
D47B92C027972AD100458394 /* main.swift in Sources */,
|
||||||
|
|
Loading…
Reference in New Issue