diff --git a/AUTHORS b/AUTHORS index a08f15e5..62722924 100644 --- a/AUTHORS +++ b/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: - 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). - Notifier window and Tooltip UI. - NSStringUtils and FSEventStreamHelper. diff --git a/Source/Modules/ControllerModules/IMEState.swift b/Source/Modules/ControllerModules/IMEState.swift index 55082a7f..bdf37901 100644 --- a/Source/Modules/ControllerModules/IMEState.swift +++ b/Source/Modules/ControllerModules/IMEState.swift @@ -9,39 +9,83 @@ import Foundation // 用以讓每個狀態自描述的 enum。 -public enum StateType { - case ofDeactivated - case ofEmpty - case ofAbortion // 該狀態會自動轉為 Empty - case ofCommitting - case ofAssociates - case ofNotEmpty - case ofInputting - case ofMarking - case ofCandidates - case ofSymbolTable +public enum StateType: String { + case ofDeactivated = "Deactivated" + case ofEmpty = "Empty" + case ofAbortion = "Abortion" // 該狀態會自動轉為 Empty + case ofCommitting = "Committing" + case ofAssociates = "Associates" + case ofNotEmpty = "NotEmpty" + case ofInputting = "Inputting" + case ofMarking = "Marking" + case ofCandidates = "Candidates" + case ofSymbolTable = "SymbolTable" } -// 所有 InputState 均遵守該協定: -public protocol InputStateProtocol { +// 所有 IMEState 均遵守該協定: +public protocol IMEStateProtocol { var type: StateType { get } var data: StateData { get } - var hasBuffer: Bool { get } + var candidates: [(String, String)] { get } + var hasComposition: Bool { get } var isCandidateContainer: Bool { get } var displayedText: String { get } var textToCommit: String { get set } var tooltip: String { get set } var attributedString: NSAttributedString { get } + var convertedToInputting: IMEState { get } + var isFilterable: Bool { get } 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 data: StateData = .init() + public var node: SymbolNode = .init("") init(_ data: StateData = .init(), type: StateType = .ofEmpty) { self.data = data 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: - 針對不同的狀態,規定不同的構造器 @@ -63,59 +107,56 @@ extension IMEState { 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) - // 注意資料的設定順序:nodeValuesArray 必須比 reading 先設定。 - result.data.nodeValuesArray = nodeValues - if !reading.isEmpty { - result.data.reading = reading // 會在被寫入資料值後自動更新 nodeValuesArray - } - // 此時 nodeValuesArray 已經被塞上讀音,直接使用即可。 - result.data.displayedText = result.data.nodeValuesArray.joined() + // 注意資料的設定順序,一定得先設定 displayTextSegments。 + result.data.displayTextSegments = displayTextSegments result.data.cursor = cursor return result } - public static func Inputting(nodeValues: [String], reading: String = "", cursor: Int) -> IMEState { - var result = IMEState.NotEmpty(nodeValues: nodeValues, reading: reading, cursor: cursor) + public static func Inputting(displayTextSegments: [String], cursor: Int) -> IMEState { + var result = IMEState.NotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) result.type = .ofInputting return result } - public static func Marking(nodeValues: [String], nodeReadings: [String], cursor: Int, marker: Int) -> IMEState { - var result = IMEState.NotEmpty(nodeValues: nodeValues, cursor: cursor) + public static func Marking( + displayTextSegments: [String], markedReadings: [String], cursor: Int, marker: Int + ) + -> IMEState + { + var result = IMEState.NotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) result.type = .ofMarking - result.data.nodeReadingsArray = nodeReadings result.data.marker = marker + result.data.markedReadings = markedReadings StateData.Marking.updateParameters(&result.data) return result } - public static func Candidates(candidates: [(String, String)], nodeValues: [String], cursor: Int) -> IMEState { - var result = IMEState.NotEmpty(nodeValues: nodeValues, cursor: cursor) + public static func Candidates(candidates: [(String, String)], displayTextSegments: [String], cursor: Int) -> IMEState + { + var result = IMEState.NotEmpty(displayTextSegments: displayTextSegments, cursor: cursor) result.type = .ofCandidates result.data.candidates = candidates return result } - public static func SymbolTable(node: SymbolNode, previous: SymbolNode? = nil) -> IMEState { - let candidates = { node.children?.map(\.title) ?? [String]() }().map { ("", $0) } - var result = IMEState.Candidates(candidates: candidates, nodeValues: [], cursor: 0) + public static func SymbolTable(node: SymbolNode) -> IMEState { + var result = IMEState(type: .ofNotEmpty, node: node) result.type = .ofSymbolTable - result.data.node = node - if let previous = previous { - result.data.node.previous = previous - } return result } } // MARK: - 規定一個狀態該怎樣返回自己的資料值 -extension IMEState: InputStateProtocol { +extension IMEState { + public var isFilterable: Bool { data.isFilterable } + public var candidates: [(String, String)] { data.candidates } public var convertedToInputting: IMEState { 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 return result } @@ -146,25 +187,7 @@ extension IMEState: InputStateProtocol { } } - public var node: SymbolNode { - get { - data.node - } - set { - data.node = newValue - } - } - - public var tooltipBackupForInputting: String { - get { - data.tooltipBackupForInputting - } - set { - data.tooltipBackupForInputting = newValue - } - } - - public var hasBuffer: Bool { + public var hasComposition: Bool { switch type { case .ofNotEmpty, .ofInputting, .ofMarking, .ofCandidates: return true default: return false diff --git a/Source/Modules/ControllerModules/IMEStateData.swift b/Source/Modules/ControllerModules/IMEStateData.swift index a4276527..d2c0f0d8 100644 --- a/Source/Modules/ControllerModules/IMEStateData.swift +++ b/Source/Modules/ControllerModules/IMEStateData.swift @@ -38,6 +38,9 @@ public struct StateData { // MARK: Cursor & Marker & Range for UTF16 (Read-Only) + /// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度, + /// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。 + /// 這樣就可以免除不必要的類型轉換。 var u16Cursor: Int { displayedText.charComponents[0.. String { var arrOutput = [String]() - for neta in data.nodeReadingsArray[data.markedRange] { + for neta in data.markedReadings { var neta = neta if neta.isEmpty { continue } if neta.contains("_") { @@ -207,12 +186,6 @@ extension StateData { /// - Parameter data: 要處理的狀態資料包。 public static func updateParameters(_ data: inout StateData) { 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 { ctlInputMethod.tooltipController.setColor(state: .warning) return NSLocalizedString( @@ -240,8 +213,7 @@ extension StateData { ) } - let selectedReadings = data.nodeReadingsArray[data.markedRange] - let joined = selectedReadings.joined(separator: "-") + let joined = data.markedReadings.joined(separator: "-") let exist = mgrLangModel.checkIfUserPhraseExist( userPhrase: text, mode: IME.currentInputMode, key: joined ) diff --git a/Source/Modules/ControllerModules/InputState.swift b/Source/Modules/ControllerModules/InputState.swift deleted file mode 100644 index 7300ad05..00000000 --- a/Source/Modules/ControllerModules/InputState.swift +++ /dev/null @@ -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 = mgrPrefs.minCandidateLength...mgrPrefs.maxCandidateLength - private(set) var markerIndex: Int = 0 { didSet { markerIndex = max(markerIndex, 0) } } - private(set) var markedRange: Range - private var literalMarkedRange: Range { - let lowerBoundLiteral = displayedText.charIndexLiteral(from: markedRange.lowerBound) - let upperBoundLiteral = displayedText.charIndexLiteral(from: markedRange.upperBound) - return lowerBoundLiteral..注音轉拼音->轉教科書式標調 - 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.. Bool } @@ -35,8 +35,6 @@ protocol KeyHandlerDelegate { public class KeyHandler { /// 半衰模組的衰減指數 let kEpsilon: Double = 0.000001 - /// 檢測是否出現游標切斷組字圈內字符的情況 - var isCursorCuttingChar = false /// 檢測是否內容為空(注拼槽與組字器都是空的) var isTypingContentEmpty: Bool { composer.isEmpty && compositor.isEmpty } @@ -88,6 +86,21 @@ public class KeyHandler { // MARK: - Functions dealing with Megrez. + /// 獲取當前標記得範圍。這個函式只能是函式、而非只讀變數。 + /// - Returns: 當前標記範圍。 + func currentMarkedRange() -> Range { + min(compositor.cursor, compositor.marker).. 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 使用的的滑鼠游標位址,以方便在組字器最開頭或者最末尾的時候始終能抓取候選字節點陣列。 /// /// 威注音對游標前置與游標後置模式採取的候選字節點陣列抓取方法是分離的,且不使用 Node Crossing。 @@ -107,7 +120,8 @@ public class KeyHandler { // 在偵錯模式開啟時,將 GraphViz 資料寫入至指定位置。 if mgrPrefs.isDebugModeEnabled { 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 { try result.write(toFile: appSupportPath, atomically: true, encoding: .utf8) } catch { diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift index 5aba1570..02a9d5dc 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift @@ -23,9 +23,9 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 func handleCandidate( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { guard var ctlCandidateCurrent = delegate?.ctlCandidate() else { @@ -41,7 +41,7 @@ extension KeyHandler { || ((input.isCursorBackward || input.isCursorForward) && input.isShiftHold) if cancelCandidateKey { - if state is InputState.Associates + if state.type == .ofAssociates || mgrPrefs.useSCPCTypingMode || compositor.isEmpty { @@ -49,13 +49,12 @@ extension KeyHandler { // 就將當前的組字緩衝區析構處理、強制重設輸入狀態。 // 否則,一個本不該出現的真空組字緩衝區會使前後方向鍵與 BackSpace 鍵失靈。 // 所以這裡需要對 compositor.isEmpty 做判定。 - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) } else { stateCallback(buildInputtingState) } - if let state = state as? InputState.SymbolTable, let nodePrevious = state.node.previous { - stateCallback(InputState.SymbolTable(node: nodePrevious)) + if state.type == .ofSymbolTable, let nodePrevious = state.node.previous, let _ = nodePrevious.children { + stateCallback(IMEState.SymbolTable(node: nodePrevious)) } return true } @@ -63,9 +62,8 @@ extension KeyHandler { // MARK: Enter if input.isEnter { - if state is InputState.Associates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter { - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + if state.type == .ofAssociates, !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter { + stateCallback(IMEState.Abortion()) return true } delegate?.keyHandler( @@ -243,23 +241,15 @@ extension KeyHandler { // MARK: End Key - var candidates: [(String, String)]! - - if let state = state as? InputState.ChoosingCandidate { - candidates = state.candidates - } else if let state = state as? InputState.Associates { - candidates = state.candidates - } - - if candidates.isEmpty { + if state.candidates.isEmpty { return false } else { // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。 if input.isEnd || input.emacsKey == EmacsKey.end { - if ctlCandidateCurrent.selectedCandidateIndex == candidates.count - 1 { + if ctlCandidateCurrent.selectedCandidateIndex == state.candidates.count - 1 { IME.prtDebugIntel("9B69AAAD") errorCallback() } else { - ctlCandidateCurrent.selectedCandidateIndex = candidates.count - 1 + ctlCandidateCurrent.selectedCandidateIndex = state.candidates.count - 1 } return true } @@ -267,13 +257,13 @@ extension KeyHandler { // MARK: 聯想詞處理 (Associated Phrases) - if state is InputState.Associates { + if state.type == .ofAssociates { if !input.isShiftHold { return false } } var index: Int = NSNotFound let match: String = - (state is InputState.Associates) ? input.inputTextIgnoringModifiers ?? "" : input.text + (state.type == .ofAssociates) ? input.inputTextIgnoringModifiers ?? "" : input.text for j in 0.. Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool? { // MARK: 注音按鍵輸入處理 (Handle BPMF Keys) @@ -100,8 +100,7 @@ extension KeyHandler { switch compositor.isEmpty { case false: stateCallback(buildInputtingState) case true: - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) } return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。 } @@ -124,31 +123,26 @@ extension KeyHandler { /// 逐字選字模式的處理。 if mgrPrefs.useSCPCTypingMode { - let choosingCandidates: InputState.ChoosingCandidate = buildCandidate( + let candidateState: IMEState = buildCandidate( state: inputting, 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 text: String = firstCandidate.1 - stateCallback(InputState.Committing(textToCommit: text)) + stateCallback(IMEState.Committing(textToCommit: text)) if !mgrPrefs.associatedPhrasesEnabled { - stateCallback(InputState.Empty()) + stateCallback(IMEState.Empty()) } else { - if let associatedPhrases = + let associatedPhrases = buildAssociatePhraseState( - withPair: .init(key: reading, value: text), - isTypingVertical: input.isTypingVertical - ), !associatedPhrases.candidates.isEmpty - { - stateCallback(associatedPhrases) - } else { - stateCallback(InputState.Empty()) - } + withPair: .init(key: reading, value: text) + ) + stateCallback(associatedPhrases.candidates.isEmpty ? IMEState.Empty() : associatedPhrases) } } else { - stateCallback(choosingCandidates) + stateCallback(candidateState) } } // 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 ctlInputMethod 回報給 IMK。 diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift index 0fb32515..ed3b70aa 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift @@ -25,8 +25,8 @@ extension KeyHandler { /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 func handle( input: InputSignalProtocol, - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { // 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。 @@ -39,7 +39,7 @@ extension KeyHandler { if input.isInvalid { // 在「.Empty(IgnoringPreviousState) 與 .Deactivated」狀態下的首次不合規按鍵輸入可以直接放行。 // 因為「.Abortion」會在處理之後被自動轉為「.Empty」,所以不需要單獨判斷。 - if state is InputState.Empty || state is InputState.Deactivated { + if state.type == .ofEmpty || state.type == .ofDeactivated { return false } IME.prtDebugIntel("550BCF7B: KeyHandler just refused an invalid input.") @@ -51,7 +51,7 @@ extension KeyHandler { // 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。 let isFunctionKey: Bool = 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 } @@ -68,7 +68,7 @@ extension KeyHandler { // 略過對 BackSpace 的處理。 } else if input.isCapsLockOn || input.isASCIIModeInput { // 但願能夠處理這種情況下所有可能的按鍵組合。 - stateCallback(InputState.Empty()) + stateCallback(IMEState.Empty()) // 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。 if input.isUpperCaseASCIILetterKey { @@ -82,8 +82,8 @@ extension KeyHandler { } // 將整個組字區的內容遞交給客體應用。 - stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Committing(textToCommit: inputText.lowercased())) + stateCallback(IMEState.Empty()) return true } @@ -94,19 +94,19 @@ extension KeyHandler { // 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。 // 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。 if input.isNumericPadKey { - if !(state is InputState.ChoosingCandidate || state is InputState.Associates - || state is InputState.SymbolTable) + if !(state.type == .ofCandidates || state.type == .ofAssociates + || state.type == .ofSymbolTable) { - stateCallback(InputState.Empty()) - stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Empty()) + stateCallback(IMEState.Committing(textToCommit: inputText.lowercased())) + stateCallback(IMEState.Empty()) return true } } // MARK: 處理候選字詞 (Handle Candidates) - if state is InputState.ChoosingCandidate { + if [.ofCandidates, .ofSymbolTable].contains(state.type) { return handleCandidate( state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) @@ -114,26 +114,26 @@ extension KeyHandler { // MARK: 處理聯想詞 (Handle Associated Phrases) - if state is InputState.Associates { + if state.type == .ofAssociates { if handleCandidate( state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) { return true } else { - stateCallback(InputState.Empty()) + stateCallback(IMEState.Empty()) } } // MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking) - if let marking = state as? InputState.Marking { + if state.type == .ofMarking { if handleMarkingState( - marking, input: input, stateCallback: stateCallback, + state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) { return true } - state = marking.convertedToInputting + state = state.convertedToInputting stateCallback(state) } @@ -147,7 +147,7 @@ extension KeyHandler { // 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.isPageDown || input.isPageUp || (input.isTab && mgrPrefs.specifyShiftTabKeyBehavior) { @@ -155,12 +155,12 @@ extension KeyHandler { /// 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話……… if !mgrPrefs.chooseCandidateUsingSpace { if compositor.cursor >= compositor.length { - let displayedText = currentState.displayedText + let displayedText = state.displayedText if !displayedText.isEmpty { - stateCallback(InputState.Committing(textToCommit: displayedText)) + stateCallback(IMEState.Committing(textToCommit: displayedText)) } - stateCallback(InputState.Committing(textToCommit: " ")) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Committing(textToCommit: " ")) + stateCallback(IMEState.Empty()) } else if currentLM.hasUnigramsFor(key: " ") { compositor.insertKey(" ") walk() @@ -175,7 +175,7 @@ extension KeyHandler { ) } } - stateCallback(buildCandidate(state: currentState, isTypingVertical: input.isTypingVertical)) + stateCallback(buildCandidate(state: state)) return true } @@ -236,7 +236,7 @@ extension KeyHandler { // MARK: Clock-Left & Clock-Right if input.isCursorClockLeft || input.isCursorClockRight { - if input.isOptionHold, state is InputState.Inputting { + if input.isOptionHold, state.type == .ofInputting { if input.isCursorClockRight { return handleInlineCandidateRotation( state: state, reverseModifier: false, stateCallback: stateCallback, errorCallback: errorCallback @@ -285,7 +285,7 @@ extension KeyHandler { walk() let inputting = buildInputtingState stateCallback(inputting) - stateCallback(buildCandidate(state: inputting, isTypingVertical: input.isTypingVertical)) + stateCallback(buildCandidate(state: inputting)) } else { // 不要在注音沒敲完整的情況下叫出統合符號選單。 IME.prtDebugIntel("17446655") errorCallback() @@ -297,14 +297,14 @@ extension KeyHandler { // 於是這裡用「模擬一次 Enter 鍵的操作」使其代為執行這個 commit buffer 的動作。 // 這裡不需要該函式所傳回的 bool 結果,所以用「_ =」解消掉。 _ = handleEnter(state: state, stateCallback: stateCallback) - stateCallback(InputState.SymbolTable(node: SymbolNode.root, isTypingVertical: input.isTypingVertical)) + stateCallback(IMEState.SymbolTable(node: SymbolNode.root)) return true } } // 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 { // NOTE: 將來棄用 macOS 10.11 El Capitan 支援的時候,把這裡由 CFStringTransform 改為 StringTransform: // https://developer.apple.com/documentation/foundation/stringtransform @@ -312,9 +312,9 @@ extension KeyHandler { let string = NSMutableString(string: stringRAW) CFStringTransform(string, nil, kCFStringTransformFullwidthHalfwidth, true) 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 } } @@ -357,10 +357,10 @@ extension KeyHandler { // MARK: 全形/半形空白 (Full-Width / Half-Width Space) /// 該功能僅可在當前組字區沒有任何內容的時候使用。 - if state is InputState.Empty { + if state.type == .ofEmpty { if input.isSpace, !input.isOptionHold, !input.isControlHold, !input.isCommandHold { - stateCallback(InputState.Committing(textToCommit: input.isShiftHold ? " " : " ")) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Committing(textToCommit: input.isShiftHold ? " " : " ")) + stateCallback(IMEState.Empty()) return true } } @@ -371,14 +371,14 @@ extension KeyHandler { if input.isShiftHold { // 這裡先不要判斷 isOptionHold。 switch mgrPrefs.upperCaseLetterKeyBehavior { case 1: - stateCallback(InputState.Empty()) - stateCallback(InputState.Committing(textToCommit: inputText.lowercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Empty()) + stateCallback(IMEState.Committing(textToCommit: inputText.lowercased())) + stateCallback(IMEState.Empty()) return true case 2: - stateCallback(InputState.Empty()) - stateCallback(InputState.Committing(textToCommit: inputText.uppercased())) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Empty()) + stateCallback(IMEState.Committing(textToCommit: inputText.uppercased())) + stateCallback(IMEState.Empty()) return true default: // 包括 case 0,直接塞給組字區。 let letter = "_letter_\(inputText)" @@ -401,7 +401,7 @@ extension KeyHandler { /// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。 /// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 /// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 - if (state is InputState.NotEmpty) || !composer.isEmpty { + if state.hasComposition || !composer.isEmpty { IME.prtDebugIntel( "Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)") IME.prtDebugIntel("A9BFF20E") diff --git a/Source/Modules/ControllerModules/KeyHandler_States.swift b/Source/Modules/ControllerModules/KeyHandler_States.swift index 11f7cd42..c943851c 100644 --- a/Source/Modules/ControllerModules/KeyHandler_States.swift +++ b/Source/Modules/ControllerModules/KeyHandler_States.swift @@ -17,101 +17,73 @@ import Foundation extension KeyHandler { // MARK: - 構築狀態(State Building) - /// 生成「正在輸入」狀態。 - var buildInputtingState: InputState.Inputting { + /// 生成「正在輸入」狀態。相關的內容會被拿給狀態機械用來處理在電腦螢幕上顯示的內容。 + var buildInputtingState: IMEState { /// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容 /// 換成由此處重新生成的組字字串(NSAttributeString,否則會不顯示)。 - var tooltipParameterRef: [String] = ["", ""] - let nodeValuesArray: [String] = compositor.walkedNodes.values.map { + var displayTextSegments: [String] = compositor.walkedNodes.values.map { guard let delegate = delegate, delegate.isVerticalTyping else { return $0 } guard mgrPrefs.hardenVerticalPunctuations else { return $0 } var neta = $0 ChineseConverter.hardenVerticalPunctuations(target: &neta, convert: delegate.isVerticalTyping) 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 readingCursorIndex = 0 - /// IMK 協定的內文組字區的游標長度與游標位置無法正確統計 UTF8 高萬字(比如 emoji)的長度, - /// 所以在這裡必須做糾偏處理。因為在用 Swift,所以可以用「.utf16」取代「NSString.length()」。 - /// 這樣就可以免除不必要的類型轉換。 for theNode in compositor.walkedNodes { let strNodeValue = theNode.value - let arrSplit: [String] = Array(strNodeValue).charComponents - let codepointCount = arrSplit.count /// 藉下述步驟重新將「可見游標位置」對齊至「組字器內的游標所在的讀音位置」。 /// 每個節錨(NodeAnchor)都有自身的幅位長度(spanningLength),可以用來 /// 累加、以此為依據,來校正「可見游標位置」。 - let spanningLength: Int = theNode.spanLength - if readingCursorIndex + spanningLength <= compositor.cursor { - composedStringCursorIndex += strNodeValue.utf16.count + let spanningLength: Int = theNode.keyArray.count + if readingCursorIndex + spanningLength <= rawCursor { + composedStringCursorIndex += strNodeValue.count readingCursorIndex += spanningLength continue } - if codepointCount == spanningLength { - for i in 0.. InputState.ChoosingCandidate { - InputState.ChoosingCandidate( - displayedText: currentState.displayedText, - cursorIndex: currentState.cursorIndex, + ) -> IMEState { + IMEState.Candidates( candidates: getCandidatesArray(fixOrder: mgrPrefs.useFixecCandidateOrderOnSelection), - nodeValuesArray: compositor.walkedNodes.values + displayTextSegments: compositor.walkedNodes.values, + cursor: currentState.data.cursor ) } @@ -149,9 +120,9 @@ extension KeyHandler { /// - Returns: 回呼一個新的聯想詞狀態,來就給定的聯想詞陣列資料內容顯示選字窗。 func buildAssociatePhraseState( withPair pair: Megrez.Compositor.KeyValuePaired - ) -> InputState.Associates! { + ) -> IMEState { // 上一行必須要用驚嘆號,否則 Xcode 會誤導你砍掉某些實際上必需的語句。 - InputState.Associates( + IMEState.Associates( candidates: buildAssociatePhraseArray(withPair: pair)) } @@ -165,9 +136,9 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleMarkingState( - _ state: InputState.Marking, + _ state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { if input.isEsc { @@ -220,18 +191,19 @@ extension KeyHandler { // Shift + Left if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold { - var index = state.markerIndex - if index > 0 { - index = state.displayedText.utf16PreviousPosition(for: index) - let marking = InputState.Marking( - displayedText: state.displayedText, - cursorIndex: state.cursorIndex, - markerIndex: index, - readings: state.readings, - nodeValuesArray: compositor.walkedNodes.values + if compositor.marker > 0 { + compositor.marker -= 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .rear, isMarker: true) + } + var marking = IMEState.Marking( + displayTextSegments: state.data.displayTextSegments, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipBackupForInputting = state.tooltipBackupForInputting - stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking) + marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting + stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking) } else { IME.prtDebugIntel("1149908D") errorCallback() @@ -242,18 +214,19 @@ extension KeyHandler { // Shift + Right if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold { - var index = state.markerIndex - if index < (state.displayedText.utf16.count) { - index = state.displayedText.utf16NextPosition(for: index) - let marking = InputState.Marking( - displayedText: state.displayedText, - cursorIndex: state.cursorIndex, - markerIndex: index, - readings: state.readings, - nodeValuesArray: compositor.walkedNodes.values + if compositor.marker < compositor.width { + compositor.marker += 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .front, isMarker: true) + } + var marking = IMEState.Marking( + displayTextSegments: state.data.displayTextSegments, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipBackupForInputting = state.tooltipBackupForInputting - stateCallback(marking.markedRange.isEmpty ? marking.convertedToInputting : marking) + marking.data.tooltipBackupForInputting = state.data.tooltipBackupForInputting + stateCallback(marking.data.markedRange.isEmpty ? marking.convertedToInputting : marking) } else { IME.prtDebugIntel("9B51408D") errorCallback() @@ -276,9 +249,9 @@ extension KeyHandler { /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handlePunctuation( _ customPunctuation: String, - state: InputStateProtocol, + state: IMEStateProtocol, usingVerticalTyping isTypingVertical: Bool, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { if !currentLM.hasUnigramsFor(key: customPunctuation) { @@ -308,8 +281,8 @@ extension KeyHandler { if candidateState.candidates.count == 1 { clear() // 這句不要砍,因為下文可能會回呼 candidateState。 if let candidateToCommit: (String, String) = candidateState.candidates.first, !candidateToCommit.1.isEmpty { - stateCallback(InputState.Committing(textToCommit: candidateToCommit.1)) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Committing(textToCommit: candidateToCommit.1)) + stateCallback(IMEState.Empty()) } else { stateCallback(candidateState) } @@ -327,13 +300,13 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEnter( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> 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(InputState.Empty()) + stateCallback(IMEState.Committing(textToCommit: state.displayedText)) + stateCallback(IMEState.Empty()) return true } @@ -345,10 +318,10 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleCtrlCommandEnter( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } var displayedText = compositor.keys.joined(separator: "-") if mgrPrefs.inlineDumpPinyinInLieuOfZhuyin { @@ -360,8 +333,8 @@ extension KeyHandler { displayedText = displayedText.replacingOccurrences(of: "-", with: " ") } - stateCallback(InputState.Committing(textToCommit: displayedText)) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Committing(textToCommit: displayedText)) + stateCallback(IMEState.Empty()) return true } @@ -373,10 +346,10 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleCtrlOptionCommandEnter( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } var composed = "" @@ -396,8 +369,8 @@ extension KeyHandler { composed += key.contains("_") ? value : "\(value)(\(key))" } - stateCallback(InputState.Committing(textToCommit: composed)) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Committing(textToCommit: composed)) + stateCallback(IMEState.Empty()) return true } @@ -411,12 +384,12 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleBackSpace( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } // 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。 switch mgrPrefs.specifyShiftBackSpaceKeyBehavior { @@ -430,15 +403,13 @@ extension KeyHandler { stateCallback(buildInputtingState) return true case 1: - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) return true default: break } if input.isShiftHold, input.isOptionHold { - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) return true } @@ -461,8 +432,7 @@ extension KeyHandler { switch composer.isEmpty && compositor.isEmpty { case false: stateCallback(buildInputtingState) case true: - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) } return true } @@ -477,16 +447,15 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleDelete( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if input.isShiftHold { - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) return true } @@ -509,8 +478,7 @@ extension KeyHandler { switch inputting.displayedText.isEmpty { case false: stateCallback(inputting) case true: - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) } return true } @@ -524,11 +492,11 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleClockKey( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("9B6F908D") errorCallback() @@ -546,11 +514,11 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleHome( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("ABC44080") @@ -580,11 +548,11 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEnd( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("9B69908D") @@ -613,16 +581,15 @@ extension KeyHandler { /// - stateCallback: 狀態回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleEsc( - state: InputStateProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void + state: IMEStateProtocol, + stateCallback: @escaping (IMEStateProtocol) -> Void ) -> Bool { - guard state is InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if mgrPrefs.escToCleanInputBuffer { /// 若啟用了該選項,則清空組字器的內容與注拼槽的內容。 /// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。 - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) } else { if composer.isEmpty { return true } /// 如果注拼槽不是空的話,則清空之。 @@ -630,8 +597,7 @@ extension KeyHandler { switch compositor.isEmpty { case false: stateCallback(buildInputtingState) case true: - stateCallback(InputState.Abortion()) - stateCallback(InputState.Empty()) + stateCallback(IMEState.Abortion()) } } return true @@ -647,12 +613,12 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleForward( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard let currentState = state as? InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("B3BA5257") @@ -663,16 +629,18 @@ extension KeyHandler { if input.isShiftHold { // Shift + Right - if currentState.cursorIndex < currentState.displayedText.utf16.count { - let nextPosition = currentState.displayedText.utf16NextPosition( - for: currentState.cursorIndex) - let marking: InputState.Marking! = InputState.Marking( - displayedText: currentState.displayedText, - cursorIndex: currentState.cursorIndex, - markerIndex: nextPosition, - readings: compositor.keys + if compositor.cursor < compositor.width { + compositor.marker = compositor.cursor + 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .front, isMarker: true) + } + var marking = IMEState.Marking( + displayTextSegments: compositor.walkedNodes.values, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipBackupForInputting = currentState.tooltip + marking.data.tooltipBackupForInputting = state.tooltip stateCallback(marking) } else { IME.prtDebugIntel("BB7F6DB9") @@ -680,7 +648,6 @@ extension KeyHandler { stateCallback(state) } } else if input.isOptionHold { - isCursorCuttingChar = false if input.isControlHold { return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback) } @@ -695,12 +662,10 @@ extension KeyHandler { } else { if compositor.cursor < compositor.length { compositor.cursor += 1 - var inputtingState = buildInputtingState - if isCursorCuttingChar == true { + if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .front) - inputtingState = buildInputtingState } - stateCallback(inputtingState) + stateCallback(buildInputtingState) } else { IME.prtDebugIntel("A96AAD58") errorCallback() @@ -721,12 +686,12 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleBackward( - state: InputStateProtocol, + state: IMEStateProtocol, input: InputSignalProtocol, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { - guard let currentState = state as? InputState.Inputting else { return false } + guard state.type == .ofInputting else { return false } if !composer.isEmpty { IME.prtDebugIntel("6ED95318") @@ -737,16 +702,18 @@ extension KeyHandler { if input.isShiftHold { // Shift + left - if currentState.cursorIndex > 0 { - let previousPosition = currentState.displayedText.utf16PreviousPosition( - for: currentState.cursorIndex) - let marking: InputState.Marking! = InputState.Marking( - displayedText: currentState.displayedText, - cursorIndex: currentState.cursorIndex, - markerIndex: previousPosition, - readings: compositor.keys + if compositor.cursor > 0 { + compositor.marker = compositor.cursor - 1 + if isCursorCuttingChar(isMarker: true) { + compositor.jumpCursorBySpan(to: .rear, isMarker: true) + } + var marking = IMEState.Marking( + displayTextSegments: compositor.walkedNodes.values, + markedReadings: Array(compositor.keys[currentMarkedRange()]), + cursor: convertCursorForDisplay(compositor.cursor), + marker: convertCursorForDisplay(compositor.marker) ) - marking.tooltipBackupForInputting = currentState.tooltip + marking.data.tooltipBackupForInputting = state.tooltip stateCallback(marking) } else { IME.prtDebugIntel("D326DEA3") @@ -754,7 +721,6 @@ extension KeyHandler { stateCallback(state) } } else if input.isOptionHold { - isCursorCuttingChar = false if input.isControlHold { return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback) } @@ -769,12 +735,10 @@ extension KeyHandler { } else { if compositor.cursor > 0 { compositor.cursor -= 1 - var inputtingState = buildInputtingState - if isCursorCuttingChar == true { + if isCursorCuttingChar() { compositor.jumpCursorBySpan(to: .rear) - inputtingState = buildInputtingState } - stateCallback(inputtingState) + stateCallback(buildInputtingState) } else { IME.prtDebugIntel("7045E6F3") errorCallback() @@ -795,14 +759,14 @@ extension KeyHandler { /// - errorCallback: 錯誤回呼。 /// - Returns: 將按鍵行為「是否有處理掉」藉由 ctlInputMethod 回報給 IMK。 func handleInlineCandidateRotation( - state: InputStateProtocol, + state: IMEStateProtocol, reverseModifier: Bool, - stateCallback: @escaping (InputStateProtocol) -> Void, + stateCallback: @escaping (IMEStateProtocol) -> Void, errorCallback: @escaping () -> Void ) -> Bool { if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false } - guard state is InputState.Inputting else { - guard state is InputState.Empty else { + guard state.type == .ofInputting else { + guard state.type == .ofEmpty else { IME.prtDebugIntel("6044F081") errorCallback() return true diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Common.swift b/Source/Modules/ControllerModules/ctlInputMethod_Common.swift index 56fbfe74..2ec05c81 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Common.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Common.swift @@ -31,9 +31,9 @@ extension ctlInputMethod { if !shouldUseHandle || (!rencentKeyHandledByKeyHandler && shouldUseHandle) { NotifierController.notify( message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n" - + toggleASCIIMode() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (toggleASCIIMode() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } if shouldUseHandle { diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift index aecd81c6..71f0f620 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift @@ -32,16 +32,20 @@ class ctlInputMethod: IMKInputController { // MARK: - - /// 按鍵調度模組的副本。 - var keyHandler: KeyHandler = .init() - /// 用以記錄當前輸入法狀態的變數。 - var state: InputStateProtocol = InputState.Empty() - /// 當前這個 ctlInputMethod 副本是否處於英數輸入模式。 - var isASCIIMode: Bool = false /// 當前這個 ctlInputMethod 副本是否處於英數輸入模式(滯後項)。 static var isASCIIModeSituation: Bool = false /// 當前這個 ctlInputMethod 副本是否處於縱排輸入模式(滯後項)。 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 副本的英數輸入模式開關。 func toggleASCIIMode() -> Bool { @@ -65,15 +69,15 @@ class ctlInputMethod: IMKInputController { /// 重設按鍵調度模組,會將當前尚未遞交的內容遞交出去。 func resetKeyHandler() { // 過濾掉尚未完成拼寫的注音。 - if state is InputState.Inputting, mgrPrefs.trimUnfinishedReadingsOnCommit { + if state.type == .ofInputting, mgrPrefs.trimUnfinishedReadingsOnCommit { keyHandler.composer.clear() 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 方法 @@ -115,9 +119,9 @@ class ctlInputMethod: IMKInputController { } else { NotifierController.notify( message: NSLocalizedString("Alphanumerical Mode", comment: "") + "\n" - + isASCIIMode - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (isASCIIMode + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } } @@ -127,7 +131,7 @@ class ctlInputMethod: IMKInputController { if let client = client(), client.bundleIdentifier() != Bundle.main.bundleIdentifier { // 強制重設當前鍵盤佈局、使其與偏好設定同步。 setKeyLayout() - handle(state: InputState.Empty()) + handle(state: IMEState.Empty()) } // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。 (NSApp.delegate as? AppDelegate)?.checkForUpdate() } @@ -137,7 +141,7 @@ class ctlInputMethod: IMKInputController { override func deactivateServer(_ sender: Any!) { _ = sender // 防止格式整理工具毀掉與此對應的參數。 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 { // 強制重設當前鍵盤佈局、使其與偏好設定同步。這裡的這一步也不能省略。 setKeyLayout() - handle(state: InputState.Empty()) + handle(state: IMEState.Empty()) } // 除此之外就不要動了,免得在點開輸入法自身的視窗時卡死。 } @@ -286,8 +290,8 @@ class ctlInputMethod: IMKInputController { /// - Returns: 字串內容,或者 nil。 override func composedString(_ sender: Any!) -> Any! { _ = sender // 防止格式整理工具毀掉與此對應的參數。 - guard let state = state as? InputState.NotEmpty else { return "" } - return state.committingBufferConverted + guard state.hasComposition else { return "" } + return state.displayedText } /// 輸入法要被換掉或關掉的時候,要做的事情。 @@ -307,7 +311,7 @@ class ctlInputMethod: IMKInputController { _ = sender // 防止格式整理工具毀掉與此對應的參數。 var arrResult = [String]() - // 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。 + // 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。 func handleCandidatesPrepared(_ candidates: [(String, String)], prefix: String = "") { for theCandidate in candidates { 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: "⇧") - } else if let state = state as? InputState.SymbolTable { + } else if state.type == .ofSymbolTable { // 分類符號選單不會出現同符異音項、不需要康熙 / JIS 轉換,所以使用簡化過的處理方式。 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() } if state.candidates[0].0.contains("_punctuation") { arrResult = state.candidates.map(\.1) // 標點符號選單處理。 @@ -361,17 +365,16 @@ class ctlInputMethod: IMKInputController { /// - Parameter candidateString: 已經確認的候選字詞內容。 override open func candidateSelected(_ candidateString: NSAttributedString!) { let candidateString: NSAttributedString = candidateString ?? .init(string: "") - if state is InputState.Associates { + if state.type == .ofAssociates { if !mgrPrefs.alsoConfirmAssociatedCandidatesByEnter { - handle(state: InputState.Abortion()) - handle(state: InputState.Empty()) + handle(state: IMEState.Abortion()) return } } var indexDeducted = 0 - // 注意:下文中的不可列印字元是用來方便在 InputState 當中用來分割資料的。 + // 注意:下文中的不可列印字元是用來方便在 IMEState 當中用來分割資料的。 func handleCandidatesSelected(_ candidates: [(String, String)], prefix: String = "") { for (i, neta) in candidates.enumerated() { 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: "⇧") - } else if let state = state as? InputState.SymbolTable { + } else if state.type == .ofSymbolTable { handleSymbolCandidatesSelected(state.candidates) - } else if let state = state as? InputState.ChoosingCandidate { + } else if state.type == .ofCandidates { guard !state.candidates.isEmpty else { return } if state.candidates[0].0.contains("_punctuation") { handleSymbolCandidatesSelected(state.candidates) // 標點符號選單處理。 diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift b/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift index b0c63631..b4dc7dea 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Delegates.swift @@ -37,21 +37,20 @@ extension ctlInputMethod: KeyHandlerDelegate { ctlCandidate(controller, didSelectCandidateAtIndex: index) } - func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: InputStateProtocol, addToFilter: Bool) + func keyHandler(_ keyHandler: KeyHandler, didRequestWriteUserPhraseWith state: IMEStateProtocol, addToFilter: Bool) -> Bool { - guard let state = state as? InputState.Marking else { return false } - if state.bufferReadingCountMisMatch { return false } + guard state.type == .ofMarking else { return false } let refInputModeReversed: InputMode = (keyHandler.inputMode == InputMode.imeModeCHT) ? InputMode.imeModeCHS : InputMode.imeModeCHT if !mgrLangModel.writeUserPhrase( - state.userPhrase, inputMode: keyHandler.inputMode, - areWeDuplicating: state.chkIfUserPhraseExists, + state.data.userPhrase, inputMode: keyHandler.inputMode, + areWeDuplicating: state.data.chkIfUserPhraseExists, areWeDeleting: addToFilter ) || !mgrLangModel.writeUserPhrase( - state.userPhraseConverted, inputMode: refInputModeReversed, + state.data.userPhraseConverted, inputMode: refInputModeReversed, areWeDuplicating: false, areWeDeleting: addToFilter ) @@ -65,7 +64,7 @@ extension ctlInputMethod: KeyHandlerDelegate { // MARK: - Candidate Controller Delegate extension ctlInputMethod: ctlCandidateDelegate { - var isAssociatedPhrasesState: Bool { state is InputState.Associates } + var isAssociatedPhrasesState: Bool { state.type == .ofAssociates } /// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。 /// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。 @@ -78,9 +77,7 @@ extension ctlInputMethod: ctlCandidateDelegate { func candidateCountForController(_ controller: ctlCandidateProtocol) -> Int { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.ChoosingCandidate { - return state.candidates.count - } else if let state = state as? InputState.Associates { + if state.isCandidateContainer { return state.candidates.count } return 0 @@ -91,9 +88,7 @@ extension ctlInputMethod: ctlCandidateDelegate { /// - Returns: 候選字詞陣列(字音配對)。 func candidatesForController(_ controller: ctlCandidateProtocol) -> [(String, String)] { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.ChoosingCandidate { - return state.candidates - } else if let state = state as? InputState.Associates { + if state.isCandidateContainer { return state.candidates } return .init() @@ -103,9 +98,7 @@ extension ctlInputMethod: ctlCandidateDelegate { -> (String, String) { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.ChoosingCandidate { - return state.candidates[index] - } else if let state = state as? InputState.Associates { + if state.isCandidateContainer { return state.candidates[index] } return ("", "") @@ -114,20 +107,20 @@ extension ctlInputMethod: ctlCandidateDelegate { func ctlCandidate(_ controller: ctlCandidateProtocol, didSelectCandidateAtIndex index: Int) { _ = controller // 防止格式整理工具毀掉與此對應的參數。 - if let state = state as? InputState.SymbolTable, + if state.type == .ofSymbolTable, let node = state.node.children?[index] { if let children = node.children, !children.isEmpty { - handle(state: InputState.Empty()) // 防止縱橫排選字窗同時出現 - handle(state: InputState.SymbolTable(node: node, previous: state.node)) + handle(state: IMEState.Empty()) // 防止縱橫排選字窗同時出現 + handle(state: IMEState.SymbolTable(node: node)) } else { - handle(state: InputState.Committing(textToCommit: node.title)) - handle(state: InputState.Empty()) + handle(state: IMEState.Committing(textToCommit: node.title)) + handle(state: IMEState.Empty()) } return } - if let state = state as? InputState.ChoosingCandidate { + if [.ofCandidates, .ofSymbolTable].contains(state.type) { let selectedValue = state.candidates[index] keyHandler.fixNode( candidate: selectedValue, respectCursorPushing: true, @@ -137,16 +130,15 @@ extension ctlInputMethod: ctlCandidateDelegate { let inputting = keyHandler.buildInputtingState if mgrPrefs.useSCPCTypingMode { - handle(state: InputState.Committing(textToCommit: inputting.displayedTextConverted)) + handle(state: IMEState.Committing(textToCommit: inputting.displayedText)) // 此時是逐字選字模式,所以「selectedValue.1」是單個字、不用追加處理。 - if mgrPrefs.associatedPhrasesEnabled, - let associatePhrases = keyHandler.buildAssociatePhraseState( + if mgrPrefs.associatedPhrasesEnabled { + let associates = keyHandler.buildAssociatePhraseState( withPair: .init(key: selectedValue.0, value: selectedValue.1) - ), !associatePhrases.candidates.isEmpty - { - handle(state: associatePhrases) + ) + handle(state: associates.candidates.isEmpty ? IMEState.Empty() : associates) } else { - handle(state: InputState.Empty()) + handle(state: IMEState.Empty()) } } else { handle(state: inputting) @@ -154,24 +146,25 @@ extension ctlInputMethod: ctlCandidateDelegate { return } - if let state = state as? InputState.Associates { + if state.type == .ofAssociates { let selectedValue = state.candidates[index] - handle(state: InputState.Committing(textToCommit: selectedValue.1)) + handle(state: IMEState.Committing(textToCommit: selectedValue.1)) // 此時是聯想詞選字模式,所以「selectedValue.1」必須只保留最後一個字。 // 不然的話,一旦你選中了由多個字組成的聯想候選詞,則連續聯想會被打斷。 guard let valueKept = selectedValue.1.last else { - handle(state: InputState.Empty()) + handle(state: IMEState.Empty()) return } - if mgrPrefs.associatedPhrasesEnabled, - let associatePhrases = keyHandler.buildAssociatePhraseState( + if mgrPrefs.associatedPhrasesEnabled { + let associates = keyHandler.buildAssociatePhraseState( withPair: .init(key: selectedValue.0, value: String(valueKept)) - ), !associatePhrases.candidates.isEmpty - { - handle(state: associatePhrases) - return + ) + if !associates.candidates.isEmpty { + handle(state: associates) + return + } } - handle(state: InputState.Empty()) + handle(state: IMEState.Empty()) } } } diff --git a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift index e05d41bf..9dd74dac 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift @@ -13,10 +13,10 @@ import Cocoa // MARK: - Tooltip Display and Candidate Display Methods extension ctlInputMethod { - func show(tooltip: String, displayedText: String, cursorIndex: Int) { + func show(tooltip: String, displayedText: String, u16Cursor: Int) { guard let client = client() else { return } 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 { cursor -= 1 } @@ -38,24 +38,14 @@ extension ctlInputMethod { ctlInputMethod.tooltipController.show(tooltip: tooltip, at: finalOrigin) } - func show(candidateWindowWith state: InputStateProtocol) { + func show(candidateWindowWith state: IMEStateProtocol) { 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 candidates: [(String, String)] = .init() - if let state = state as? InputState.ChoosingCandidate { - candidates = state.candidates - } else if let state = state as? InputState.Associates { + if state.isCandidateContainer { candidates = state.candidates } - if isTypingVertical { return true } + if isVerticalTyping { return true } // 接下來的判斷並非適用於 IMK 選字窗,所以先插入排除語句。 guard ctlInputMethod.ctlCandidateCurrent is ctlCandidateUniversal else { return false } // 以上是通用情形。接下來決定橫排輸入時是否使用縱排選字窗。 @@ -106,7 +96,7 @@ extension ctlInputMethod { let candidateKeys = mgrPrefs.candidateKeys let keyLabels = candidateKeys.count > 4 ? Array(candidateKeys) : Array(mgrPrefs.defaultCandidateKeys) - let keyLabelSuffix = state is InputState.Associates ? "^" : "" + let keyLabelSuffix = state.type == .ofAssociates ? "^" : "" ctlInputMethod.ctlCandidateCurrent.keyLabels = keyLabels.map { 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 cursor = 0 - if let state = state as? InputState.ChoosingCandidate { - cursor = state.cursorIndex + if [.ofCandidates, .ofSymbolTable].contains(state.type) { + cursor = state.data.cursor if cursor == state.displayedText.count, cursor != 0 { cursor -= 1 } @@ -140,7 +130,7 @@ extension ctlInputMethod { cursor -= 1 } - if isTypingVertical { + if isVerticalTyping { ctlInputMethod.ctlCandidateCurrent.set( windowTopLeftPoint: NSPoint( x: lineHeightRect.origin.x + lineHeightRect.size.width + 4.0, y: lineHeightRect.origin.y - 4.0 diff --git a/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift b/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift index 4d534fa5..b04476e9 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_HandleStates.swift @@ -18,29 +18,77 @@ extension ctlInputMethod { /// 先將舊狀態單獨記錄起來,再將新舊狀態作為參數, /// 根據新狀態本身的狀態種類來判斷交給哪一個專門的函式來處理。 /// - Parameter newState: 新狀態。 - func handle(state newState: InputStateProtocol) { - let prevState = state + func handle(state newState: IMEStateProtocol) { + let previous = state state = newState - - switch newState { - case let newState as InputState.Deactivated: - handle(state: newState, previous: prevState) - case let newState as InputState.Empty: - handle(state: newState, previous: prevState) - case let newState as InputState.Abortion: - handle(state: newState, previous: prevState) - case let newState as InputState.Committing: - handle(state: newState, previous: prevState) - case let newState as InputState.Inputting: - handle(state: newState, previous: prevState) - case let newState as InputState.Marking: - handle(state: newState, previous: prevState) - case let newState as InputState.ChoosingCandidate: - handle(state: newState, previous: prevState) - case let newState as InputState.Associates: - handle(state: newState, previous: prevState) - case let newState as InputState.SymbolTable: - handle(state: newState, previous: prevState) + switch state.type { + case .ofDeactivated: + ctlInputMethod.ctlCandidateCurrent.delegate = nil + ctlInputMethod.ctlCandidateCurrent.visible = false + ctlInputMethod.tooltipController.hide() + if previous.hasComposition { + commit(text: previous.displayedText) + } + clearInlineDisplay() + // 最後一道保險 + keyHandler.clear() + case .ofEmpty, .ofAbortion: + var previous = previous + if state.type == .ofAbortion { + state = IMEState.Empty() + previous = state + } + ctlInputMethod.ctlCandidateCurrent.visible = false + ctlInputMethod.tooltipController.hide() + // 全專案用以判斷「.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 } } @@ -48,7 +96,7 @@ extension ctlInputMethod { /// 針對受 .NotEmpty() 管轄的非空狀態,在組字區內顯示游標。 func setInlineDisplayWithCursor() { guard let client = client() else { return } - if let state = state as? InputState.Associates { + if state.type == .ofAssociates { client.setMarkedText( state.attributedString, selectionRange: NSRange(location: 0, length: 0), replacementRange: NSRange(location: NSNotFound, length: NSNotFound) @@ -56,49 +104,19 @@ extension ctlInputMethod { return } - guard let state = state as? InputState.NotEmpty else { - clearInlineDisplay() + if state.hasComposition || state.isCandidateContainer { + /// 所謂選區「selectionRange」,就是「可見游標位置」的位置,只不過長度 + /// 是 0 且取代範圍(replacementRange)為「NSNotFound」罷了。 + /// 也就是說,內文組字區該在哪裡出現,得由客體軟體來作主。 + client.setMarkedText( + state.attributedString, selectionRange: NSRange(location: state.data.u16Cursor, length: 0), + replacementRange: NSRange(location: NSNotFound, length: NSNotFound) + ) return } - var identifier: AnyObject { - switch IME.currentInputMode { - 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) - ) + // 其它情形。 + clearInlineDisplay() } /// 在處理不受 .NotEmpty() 管轄的狀態時可能要用到的函式,會清空螢幕上顯示的內文組字區。 @@ -123,109 +141,4 @@ extension ctlInputMethod { 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) - } } diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift b/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift index b3615e68..d1ac194b 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Menu.swift @@ -212,9 +212,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Per-Char Select Mode", comment: "") + "\n" - + mgrPrefs.toggleSCPCTypingModeEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleSCPCTypingModeEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -222,9 +222,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Force KangXi Writing", comment: "") + "\n" - + mgrPrefs.toggleChineseConversionEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleChineseConversionEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -232,9 +232,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("JIS Shinjitai Output", comment: "") + "\n" - + mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleShiftJISShinjitaiOutputEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -242,9 +242,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Currency Numeral Output", comment: "") + "\n" - + mgrPrefs.toggleCurrencyNumeralsEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleCurrencyNumeralsEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -252,9 +252,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Half-Width Punctuation Mode", comment: "") + "\n" - + mgrPrefs.toggleHalfWidthPunctuationEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleHalfWidthPunctuationEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -262,9 +262,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("CNS11643 Mode", comment: "") + "\n" - + mgrPrefs.toggleCNS11643Enabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleCNS11643Enabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -272,9 +272,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Symbol & Emoji Input", comment: "") + "\n" - + mgrPrefs.toggleSymbolInputEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleSymbolInputEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -282,9 +282,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Per-Char Associated Phrases", comment: "") + "\n" - + mgrPrefs.toggleAssociatedPhrasesEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.toggleAssociatedPhrasesEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } @@ -292,9 +292,9 @@ extension ctlInputMethod { resetKeyHandler() NotifierController.notify( message: NSLocalizedString("Use Phrase Replacement", comment: "") + "\n" - + mgrPrefs.togglePhraseReplacementEnabled() - ? NSLocalizedString("NotificationSwitchON", comment: "") - : NSLocalizedString("NotificationSwitchOFF", comment: "") + + (mgrPrefs.togglePhraseReplacementEnabled() + ? NSLocalizedString("NotificationSwitchON", comment: "") + : NSLocalizedString("NotificationSwitchOFF", comment: "")) ) } diff --git a/Source/Modules/LangModelRelated/LMSymbolNode.swift b/Source/Modules/LangModelRelated/LMSymbolNode.swift index 19091d1b..4e298acf 100644 --- a/Source/Modules/LangModelRelated/LMSymbolNode.swift +++ b/Source/Modules/LangModelRelated/LMSymbolNode.swift @@ -18,17 +18,26 @@ public class SymbolNode { init(_ title: String, _ children: [SymbolNode]? = nil, previous: SymbolNode? = nil) { self.title = title self.children = children + self.children?.forEach { + $0.previous = self + } self.previous = previous } init(_ title: String, symbols: String) { self.title = title children = Array(symbols).map { SymbolNode(String($0), nil) } + children?.forEach { + $0.previous = self + } } init(_ title: String, symbols: [String]) { self.title = title children = symbols.map { SymbolNode($0, nil) } + children?.forEach { + $0.previous = self + } } static func parseUserSymbolNodeData() { diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 4837073e..8eb21e63 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -136,7 +136,6 @@ 6ACA41FD15FC1D9000935EF6 /* MainMenu.xib in Resources */ = {isa = PBXBuildFile; fileRef = 6ACA41F015FC1D9000935EF6 /* MainMenu.xib */; }; 6ACA420215FC1E5200935EF6 /* vChewing.app in Resources */ = {isa = PBXBuildFile; fileRef = 6A0D4EA215FC0D2D00ABF4B3 /* vChewing.app */; }; 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 */; }; D47F7DCE278BFB57002F9DD7 /* ctlPrefWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.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 = ""; }; 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 = ""; tabWidth = 2; usesTabs = 0; }; - D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputState.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = main.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; D47F7DCD278BFB57002F9DD7 /* ctlPrefWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlPrefWindow.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; D47F7DCF278C0897002F9DD7 /* ctlNonModalAlertWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = ctlNonModalAlertWindow.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; @@ -504,7 +502,6 @@ 5BB802D927FABA8300CF1C19 /* ctlInputMethod_Menu.swift */, 5BF56F9728C39A2700DD6839 /* IMEState.swift */, 5BF56F9928C39D1800DD6839 /* IMEStateData.swift */, - D461B791279DAC010070E734 /* InputState.swift */, 5BD0113C2818543900609769 /* KeyHandler_Core.swift */, 5B782EC3280C243C007276DE /* KeyHandler_HandleCandidate.swift */, 5BE3779F288FED8D0037365B /* KeyHandler_HandleComposition.swift */, @@ -1211,7 +1208,6 @@ 5BA9FD4127FEF3C8002DE248 /* PreferencesStyle.swift in Sources */, 5B7F225D2808501000DDD3CB /* KeyHandler_HandleInput.swift in Sources */, 5BA9FD1227FEDB6B002DE248 /* suiPrefPaneExperience.swift in Sources */, - D461B792279DAC010070E734 /* InputState.swift in Sources */, 5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */, 5B62A33D27AE7CC100A19448 /* ctlAboutWindow.swift in Sources */, D47B92C027972AD100458394 /* main.swift in Sources */,