diff --git a/Packages/vChewing_LangModelAssembly/Sources/LangModelAssembly/SubLMs/lmCassette.swift b/Packages/vChewing_LangModelAssembly/Sources/LangModelAssembly/SubLMs/lmCassette.swift index 5a064e13..35194dff 100644 --- a/Packages/vChewing_LangModelAssembly/Sources/LangModelAssembly/SubLMs/lmCassette.swift +++ b/Packages/vChewing_LangModelAssembly/Sources/LangModelAssembly/SubLMs/lmCassette.swift @@ -29,7 +29,7 @@ extension vChewingLM { /// 是否已有資料載入。 public var isLoaded: Bool { !charDefMap.isEmpty } /// 返回「允許使用的敲字鍵」的 - public var allowedKeys: [String] { Array(keyNameMap.keys) } + public var allowedKeys: [String] { Array(keyNameMap.keys + [" "]).deduplicated } /// 將給定的按鍵字母轉換成要顯示的形態。 public func convertKeyToDisplay(char: String) -> String { keyNameMap[char] ?? char diff --git a/Source/Modules/InputHandler_Core.swift b/Source/Modules/InputHandler_Core.swift index 62cef7ce..fd894794 100644 --- a/Source/Modules/InputHandler_Core.swift +++ b/Source/Modules/InputHandler_Core.swift @@ -57,6 +57,7 @@ public class InputHandler: InputHandlerProtocol { /// 半衰模組的衰減指數 let kEpsilon: Double = 0.000_001 + public var calligrapher = "" // 磁帶專用組筆區 public var composer: Tekkon.Composer = .init() // 注拼槽 public var compositor: Megrez.Compositor // 組字器 public var currentUOM: vChewingLM.LMUserOverride @@ -83,6 +84,7 @@ public class InputHandler: InputHandlerProtocol { public func clear() { composer.clear() compositor.clear() + calligrapher.removeAll() } // MARK: - Functions dealing with Megrez. @@ -336,6 +338,8 @@ public class InputHandler: InputHandlerProtocol { // MARK: - Extracted methods and functions (Tekkon). + var isComposerOrCalligrapherEmpty: Bool { prefs.cassetteEnabled ? calligrapher.isEmpty : composer.isEmpty } + /// 獲取與當前注音排列或拼音輸入種類有關的標點索引鍵,以英數下畫線「_」結尾。 var currentKeyboardParser: String { currentKeyboardParserType.name + "_" } var currentKeyboardParserType: KeyboardParser { .init(rawValue: prefs.keyboardParser) ?? .ofStandard } @@ -363,6 +367,22 @@ public class InputHandler: InputHandlerProtocol { composer.phonabetCombinationCorrectionEnabled = prefs.autoCorrectReadingCombination } + func clearComposerAndCalligrapher() { + _ = prefs.cassetteEnabled ? calligrapher.removeAll() : composer.clear() + } + + func letComposerAndCalligrapherDoBackSpace() { + _ = prefs.cassetteEnabled ? calligrapher = String(calligrapher.dropLast(1)) : composer.doBackSpace() + } + + /// 返回前一個游標位置的可解析的漢字筆畫。 + /// 返回的內容分別是:「完整讀音」「去掉聲調的讀音」「是否有聲調」。 + var previousParsableCalligraph: String? { + if compositor.cursor == 0 { return nil } + let cursorPrevious = max(compositor.cursor - 1, 0) + return compositor.keys[cursorPrevious] + } + /// 返回前一個游標位置的可解析的漢字讀音。 /// 返回的內容分別是:「完整讀音」「去掉聲調的讀音」「是否有聲調」。 var previousParsableReading: (String, String, Bool)? { @@ -390,6 +410,18 @@ public class InputHandler: InputHandlerProtocol { return theComposer.hasIntonation(withNothingElse: true) } + var readingForDisplay: String { + if !prefs.cassetteEnabled { + return composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer) + } + if !prefs.showTranslatedStrokesInCompositionBuffer { return calligrapher } + var result = calligrapher.charComponents + for idx in 0.. Bool? { - handlePhonabetComposition(input: input) + prefs.cassetteEnabled ? handleCassetteComposition(input: input) : handlePhonabetComposition(input: input) } + // MARK: 注音按鍵輸入處理 (Handle BPMF Keys) + /// 用來處理 InputHandler.HandleInput() 當中的與注音输入有關的組字行為。 /// - Parameters: /// - input: 輸入訊號。 @@ -27,8 +29,6 @@ extension InputHandler { private func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? { guard let delegate = delegate else { return nil } - // MARK: 注音按鍵輸入處理 (Handle BPMF Keys) - var keyConsumedByReading = false let skipPhoneticHandling = input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey @@ -76,7 +76,7 @@ extension InputHandler { var composeReading = composer.hasIntonation() && composer.inputValidityCheck(key: input.charCode) // 這裡不需要做排他性判斷。 - // 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 _composer 內的注音來做檢查了。 + // 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 composer 內的注音來做檢查了。 // 來看看詞庫內到底有沒有對應的讀音索引。這裡用了類似「|=」的判斷處理方式。 composeReading = composeReading || (!composer.isEmpty && (input.isSpace || input.isEnter)) if composeReading { @@ -160,3 +160,106 @@ extension InputHandler { return nil } } + +// MARK: - 磁帶模式的組字支援。 + +extension InputHandler { + /// 用來處理 InputHandler.HandleInput() 當中的與磁帶模組有關的組字行為。 + /// - Parameters: + /// - input: 輸入訊號。 + /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 + private func handleCassetteComposition(input: InputSignalProtocol) -> Bool? { + guard let delegate = delegate else { return nil } + + var keyConsumedByStrokes = false + let skipStrokeHandling = + input.isReservedKey || input.isNumericPadKey || input.isNonLaptopFunctionKey + || input.isControlHold || input.isOptionHold || input.isShiftHold || input.isCommandHold + + var isStrokesFull: Bool { calligrapher.count >= currentLM.currentCassette.maxKeyLength } + + if !skipStrokeHandling && currentLM.currentCassette.allowedKeys.contains(input.text) { + if isStrokesFull { + calligrapher = String(calligrapher.dropLast(1)) + } + calligrapher.append(input.text) + keyConsumedByStrokes = true + + if !isStrokesFull { + delegate.switchState(generateStateOfInputting()) + return true + } + } + + var compoundStrokes = isStrokesFull // 這裡不需要做排他性判斷。 + + // 如果當前的按鍵是 Enter 或 Space 的話,這時就可以取出 calligrapher 內的筆畫來做檢查了。 + // 來看看詞庫內到底有沒有對應的讀音索引。這裡用了類似「|=」的判斷處理方式。 + compoundStrokes = compoundStrokes || (!calligrapher.isEmpty && (input.isSpace || input.isEnter)) + if compoundStrokes { + // 向語言模型詢問是否有對應的記錄。 + if !currentLM.hasUnigramsFor(key: calligrapher) { + delegate.callError("B49C0979_Cassette:語彙庫內無「\(calligrapher)」的匹配記錄。") + + calligrapher.removeAll() + // 根據「組字器是否為空」來判定回呼哪一種狀態。 + switch compositor.isEmpty { + case false: delegate.switchState(generateStateOfInputting()) + case true: delegate.switchState(IMEState.ofAbortion()) + } + return true // 向 IMK 報告說這個按鍵訊號已經被輸入法攔截處理了。 + } + + // 將該讀音插入至組字器內的軌格當中。 + compositor.insertKey(calligrapher) + + // 讓組字器反爬軌格。 + walk() + + // 一邊吃一邊屙(僅對位列黑名單的 App 用這招限制組字區長度)。 + let textToCommit = commitOverflownComposition + + // 看看半衰記憶模組是否會對目前的狀態給出自動選字建議。 + retrieveUOMSuggestions(apply: true) + + // 之後就是更新組字區了。先清空注拼槽的內容。 + calligrapher.removeAll() + + // 再以回呼組字狀態的方式來執行 setInlineDisplayWithCursor()。 + var inputting = generateStateOfInputting() + inputting.textToCommit = textToCommit + delegate.switchState(inputting) + + /// 逐字選字模式的處理,與注音輸入的部分完全雷同。 + if prefs.useSCPCTypingMode { + let candidateState: IMEStateProtocol = generateStateOfCandidates() + switch candidateState.candidates.count { + case 2...: delegate.switchState(candidateState) + case 1: + let firstCandidate = candidateState.candidates.first! // 一定會有,所以強制拆包也無妨。 + let reading: String = firstCandidate.0 + let text: String = firstCandidate.1 + delegate.switchState(IMEState.ofCommitting(textToCommit: text)) + + if !prefs.associatedPhrasesEnabled { + delegate.switchState(IMEState.ofEmpty()) + } else { + let associatedPhrases = generateStateOfAssociates(withPair: .init(key: reading, value: text)) + delegate.switchState(associatedPhrases.candidates.isEmpty ? IMEState.ofEmpty() : associatedPhrases) + } + default: break + } + } + // 將「這個按鍵訊號已經被輸入法攔截處理了」的結果藉由 SessionCtl 回報給 IMK。 + return true + } + + /// 是說此時注拼槽並非為空、卻還沒組音。這種情況下只可能是「注拼槽內只有聲調」。 + if keyConsumedByStrokes { + // 以回呼組字狀態的方式來執行 setInlineDisplayWithCursor()。 + delegate.switchState(generateStateOfInputting()) + return true + } + return nil + } +} diff --git a/Source/Modules/InputHandler_HandleInput.swift b/Source/Modules/InputHandler_HandleInput.swift index f591d7ca..79e25042 100644 --- a/Source/Modules/InputHandler_HandleInput.swift +++ b/Source/Modules/InputHandler_HandleInput.swift @@ -112,7 +112,7 @@ extension InputHandler { // MARK: 用上下左右鍵呼叫選字窗 (Calling candidate window using Up / Down or PageUp / PageDn.) - if state.hasComposition, composer.isEmpty, !input.isOptionHold, + if state.hasComposition, isComposerOrCalligrapherEmpty, !input.isOptionHold, input.isCursorClockLeft || input.isCursorClockRight || input.isSpace || input.isPageDown || input.isPageUp || (input.isTab && prefs.specifyShiftTabKeyBehavior) { @@ -181,7 +181,7 @@ extension InputHandler { if input.isSymbolMenuPhysicalKey, !input.isShiftHold, !input.isControlHold, state.type != .ofDeactivated { if input.isOptionHold { if currentLM.hasUnigramsFor(key: "_punctuation_list") { - if composer.isEmpty { + if isComposerOrCalligrapherEmpty { compositor.insertKey("_punctuation_list") walk() // 一邊吃一邊屙(僅對位列黑名單的 App 用這招限制組字區長度)。 @@ -268,7 +268,7 @@ extension InputHandler { /// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。 /// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 /// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 - if state.hasComposition || !composer.isEmpty { + if state.hasComposition || !isComposerOrCalligrapherEmpty { delegate.callError("Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode)") delegate.callError("A9BFF20E") delegate.switchState(state) diff --git a/Source/Modules/InputHandler_States.swift b/Source/Modules/InputHandler_States.swift index ff76fa62..9610ed4c 100644 --- a/Source/Modules/InputHandler_States.swift +++ b/Source/Modules/InputHandler_States.swift @@ -23,7 +23,7 @@ extension InputHandler { /// 換成由此處重新生成的原始資料在 IMEStateData 當中生成的 NSAttributeString。 var displayTextSegments: [String] = compositor.walkedNodes.values var cursor = convertCursorForDisplay(compositor.cursor) - let reading = composer.getInlineCompositionForDisplay(isHanyuPinyin: prefs.showHanyuPinyinInCompositionBuffer) + let reading: String = readingForDisplay // 先提出來,減輕運算負擔。 if !reading.isEmpty { var newDisplayTextSegments = [String]() var temporaryNode = "" @@ -67,10 +67,11 @@ extension InputHandler { continue } if !theNode.isReadingMismatched { - for _ in 0.. 0 { compositor.dropKey(direction: .rear) walk() @@ -372,10 +383,10 @@ extension InputHandler { return true } } else { - composer.doBackSpace() + letComposerAndCalligrapherDoBackSpace() } - switch composer.isEmpty && compositor.isEmpty { + switch isComposerOrCalligrapherEmpty && compositor.isEmpty { case false: delegate.switchState(generateStateOfInputting()) case true: delegate.switchState(IMEState.ofAbortion()) } @@ -398,17 +409,17 @@ extension InputHandler { return true } - if compositor.cursor == compositor.length, composer.isEmpty { + if compositor.cursor == compositor.length, isComposerOrCalligrapherEmpty { delegate.callError("9B69938D") delegate.switchState(state) return true } - if composer.isEmpty { + if isComposerOrCalligrapherEmpty { compositor.dropKey(direction: .front) walk() } else { - composer.clear() + clearComposerAndCalligrapher() } let inputting = generateStateOfInputting() @@ -428,7 +439,7 @@ extension InputHandler { guard let delegate = delegate else { return false } let state = delegate.state guard state.type == .ofInputting else { return false } - if !composer.isEmpty { delegate.callError("9B6F908D") } + if !isComposerOrCalligrapherEmpty { delegate.callError("9B6F908D") } delegate.switchState(state) return true } @@ -442,7 +453,7 @@ extension InputHandler { let state = delegate.state guard state.type == .ofInputting else { return false } - if !composer.isEmpty { + if !isComposerOrCalligrapherEmpty { delegate.callError("ABC44080") delegate.switchState(state) return true @@ -468,7 +479,7 @@ extension InputHandler { let state = delegate.state guard state.type == .ofInputting else { return false } - if !composer.isEmpty { + if !isComposerOrCalligrapherEmpty { delegate.callError("9B69908D") delegate.switchState(state) return true @@ -499,9 +510,9 @@ extension InputHandler { /// 此乃 macOS 內建注音輸入法預設之行為,但不太受 Windows 使用者群體之待見。 delegate.switchState(IMEState.ofAbortion()) } else { - if composer.isEmpty { return true } - /// 如果注拼槽不是空的話,則清空之。 - composer.clear() + if isComposerOrCalligrapherEmpty { return true } + /// 如果注拼槽或組筆區不是空的話,則清空之。 + clearComposerAndCalligrapher() switch compositor.isEmpty { case false: delegate.switchState(generateStateOfInputting()) case true: delegate.switchState(IMEState.ofAbortion()) @@ -521,7 +532,7 @@ extension InputHandler { let state = delegate.state guard state.type == .ofInputting else { return false } - if !composer.isEmpty { + if !isComposerOrCalligrapherEmpty { delegate.callError("B3BA5257") delegate.switchState(state) return true @@ -584,7 +595,7 @@ extension InputHandler { let state = delegate.state guard state.type == .ofInputting else { return false } - if !composer.isEmpty { + if !isComposerOrCalligrapherEmpty { delegate.callError("6ED95318") delegate.switchState(state) return true @@ -643,7 +654,7 @@ extension InputHandler { func rotateCandidate(reverseOrder: Bool) -> Bool { guard let delegate = delegate else { return false } let state = delegate.state - if composer.isEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false } + if isComposerOrCalligrapherEmpty, compositor.isEmpty || compositor.walkedNodes.isEmpty { return false } guard state.type == .ofInputting else { guard state.type == .ofEmpty else { delegate.callError("6044F081") @@ -653,7 +664,7 @@ extension InputHandler { return false } - guard composer.isEmpty else { + guard isComposerOrCalligrapherEmpty else { delegate.callError("A2DAF7BC") return true }