From 8734ebd68fdb54915ac35499bfa143a3d5c09e9c Mon Sep 17 00:00:00 2001 From: ShikiSuen Date: Thu, 9 Feb 2023 16:55:33 +0800 Subject: [PATCH] InputHandler // Implementing Code Point Input support. --- Source/Modules/InputHandler_Core.swift | 29 ++++++++- .../InputHandler_HandleComposition.swift | 61 ++++++++++++++++++- Source/Modules/InputHandler_HandleInput.swift | 4 +- .../Modules/InputHandler_HandleStates.swift | 58 ++++++++++++++++-- .../Resources/Base.lproj/Localizable.strings | 2 + Source/Resources/en.lproj/Localizable.strings | 2 + Source/Resources/ja.lproj/Localizable.strings | 2 + .../zh-Hans.lproj/Localizable.strings | 2 + .../zh-Hant.lproj/Localizable.strings | 2 + 9 files changed, 155 insertions(+), 7 deletions(-) diff --git a/Source/Modules/InputHandler_Core.swift b/Source/Modules/InputHandler_Core.swift index da597644..b1c185f3 100644 --- a/Source/Modules/InputHandler_Core.swift +++ b/Source/Modules/InputHandler_Core.swift @@ -108,6 +108,29 @@ public class InputHandler: InputHandlerProtocol { public func clear() { clearComposerAndCalligrapher() compositor.clear() + isCodePointInputMode = false + } + + // MARK: - Codepoint Input Buffer. + + var isCodePointInputMode = false { + willSet { + strCodePointBuffer.removeAll() + } + } + + var strCodePointBuffer = "" + + var tooltipCodePointInputMode: String { + let commonTerm = NSMutableString() + commonTerm.insert("Code Point Input Mode.".localized, at: 0) + switch IMEApp.currentInputMode { + case .imeModeCHS: commonTerm.insert("[GB] ", at: 0) + case .imeModeCHT: commonTerm.insert("[Big5] ", at: 0) + default: break + } + commonTerm.append("  ") + return commonTerm.description } // MARK: - Functions dealing with Megrez. @@ -366,7 +389,10 @@ public class InputHandler: InputHandlerProtocol { // MARK: - Extracted methods and functions (Tekkon). - var isComposerOrCalligrapherEmpty: Bool { prefs.cassetteEnabled ? calligrapher.isEmpty : composer.isEmpty } + var isComposerOrCalligrapherEmpty: Bool { + if !strCodePointBuffer.isEmpty { return false } + return prefs.cassetteEnabled ? calligrapher.isEmpty : composer.isEmpty + } /// 獲取與當前注音排列或拼音輸入種類有關的標點索引鍵,以英數下畫線「_」結尾。 var currentKeyboardParser: String { currentKeyboardParserType.name + "_" } @@ -400,6 +426,7 @@ public class InputHandler: InputHandlerProtocol { public func clearComposerAndCalligrapher() { calligrapher.removeAll() composer.clear() + strCodePointBuffer.removeAll() } func letComposerAndCalligrapherDoBackSpace() { diff --git a/Source/Modules/InputHandler_HandleComposition.swift b/Source/Modules/InputHandler_HandleComposition.swift index 8f645ed7..6ddd1dc1 100644 --- a/Source/Modules/InputHandler_HandleComposition.swift +++ b/Source/Modules/InputHandler_HandleComposition.swift @@ -17,7 +17,9 @@ extension InputHandler { /// - input: 輸入訊號。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 func handleComposition(input: InputSignalProtocol) -> Bool? { - prefs.cassetteEnabled ? handleCassetteComposition(input: input) : handlePhonabetComposition(input: input) + if isCodePointInputMode { return handleCodePointComposition(input: input) } + if prefs.cassetteEnabled { return handleCassetteComposition(input: input) } + return handlePhonabetComposition(input: input) } // MARK: 注音按鍵輸入處理 (Handle BPMF Keys) @@ -305,4 +307,61 @@ extension InputHandler { } return nil } + + // MARK: 區位輸入處理 (Handle Code Point Input) + + /// 用來處理 InputHandler.HandleInput() 當中的與區位輸入有關的組字行為。 + /// - Parameters: + /// - input: 輸入訊號。 + /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 + private func handleCodePointComposition(input: InputSignalProtocol) -> Bool? { + guard !input.isReservedKey else { return nil } + guard let delegate = delegate, input.text.count == 1 else { return nil } + guard !input.text.compactMap(\.hexDigitValue).isEmpty else { + delegate.callError("05DD692C:輸入的字元並非 ASCII 字元。。") + return true + } + switch strCodePointBuffer.count { + case 0 ..< 4: + if strCodePointBuffer.count < 3 { + strCodePointBuffer.append(input.text) + var updatedState = generateStateOfInputting() + updatedState.tooltipDuration = 0 + updatedState.tooltip = tooltipCodePointInputMode + delegate.switchState(updatedState) + return true + } + let encoding: CFStringEncodings? = { + switch IMEApp.currentInputMode { + case .imeModeCHS: return .GB_18030_2000 + case .imeModeCHT: return .big5_HKSCS_1999 + default: return nil + } + }() + guard + var char = "\(strCodePointBuffer)\(input.text)" + .parsedAsHexLiteral(encoding: encoding)?.first?.description + else { + delegate.callError("D220B880:輸入的字碼沒有對應的字元。") + var updatedState = IMEState.ofAbortion() + updatedState.tooltipDuration = 0 + updatedState.tooltip = "Invalid Code Point.".localized + "  " + delegate.switchState(updatedState) + isCodePointInputMode = true + return true + } + // 某些舊版 macOS 會在這裡生成的字元後面插入垃圾字元。這裡只保留起始字元。 + if char.count > 1 { char = char.map(\.description)[0] } + var updatedState = IMEState.ofCommitting(textToCommit: char) + updatedState.tooltipDuration = 0 + updatedState.tooltip = tooltipCodePointInputMode + delegate.switchState(updatedState) + isCodePointInputMode = true + return true + default: + delegate.switchState(generateStateOfInputting()) + isCodePointInputMode = true + return true + } + } } diff --git a/Source/Modules/InputHandler_HandleInput.swift b/Source/Modules/InputHandler_HandleInput.swift index f14c30f1..45456360 100644 --- a/Source/Modules/InputHandler_HandleInput.swift +++ b/Source/Modules/InputHandler_HandleInput.swift @@ -162,8 +162,10 @@ extension InputHandler { switch input.modifierFlags { case []: return handlePunctuationList(alternative: false, isJIS: isJIS) - case [.option]: + case [.option, .shift]: return handlePunctuationList(alternative: true, isJIS: isJIS) + case .option: + return handleCodePointInputToggle() default: break } case .kSpace: // 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話……… diff --git a/Source/Modules/InputHandler_HandleStates.swift b/Source/Modules/InputHandler_HandleStates.swift index 850c0d29..e0dadbb1 100644 --- a/Source/Modules/InputHandler_HandleStates.swift +++ b/Source/Modules/InputHandler_HandleStates.swift @@ -19,11 +19,16 @@ extension InputHandler { /// 生成「正在輸入」狀態。相關的內容會被拿給狀態機械用來處理在電腦螢幕上顯示的內容。 public func generateStateOfInputting(sansReading: Bool = false) -> IMEStateProtocol { + let cpInput = isCodePointInputMode && !sansReading /// 「更新內文組字區 (Update the composing buffer)」是指要求客體軟體將組字緩衝區的內容 /// 換成由此處重新生成的原始資料在 IMEStateData 當中生成的 NSAttributeString。 - var displayTextSegments: [String] = compositor.walkedNodes.values - var cursor = convertCursorForDisplay(compositor.cursor) - let reading: String = sansReading ? "" : readingForDisplay // 先提出來,減輕運算負擔。 + var displayTextSegments: [String] = cpInput + ? [strCodePointBuffer] + : compositor.walkedNodes.values + var cursor = cpInput + ? displayTextSegments.joined().count + : convertCursorForDisplay(compositor.cursor) + let reading: String = (sansReading || isCodePointInputMode) ? "" : readingForDisplay // 先提出來,減輕運算負擔。 if !reading.isEmpty { var newDisplayTextSegments = [String]() var temporaryNode = "" @@ -287,6 +292,9 @@ extension InputHandler { @discardableResult func handleEnter(input: InputSignalProtocol, readingOnly: Bool = false) -> Bool { guard let delegate = delegate else { return false } let state = delegate.state + + if isCodePointInputMode { return handleCodePointInputToggle() } + guard state.type == .ofInputting else { return false } var displayedText = state.displayedText @@ -384,7 +392,24 @@ extension InputHandler { func handleBackSpace(input: InputSignalProtocol) -> Bool { guard let delegate = delegate else { return false } let state = delegate.state - guard state.type == .ofInputting else { return false } + guard state.type == .ofInputting else { + isCodePointInputMode = false + return false + } + + if isCodePointInputMode { + if !strCodePointBuffer.isEmpty { + strCodePointBuffer = strCodePointBuffer.dropLast(1).description + if !strCodePointBuffer.isEmpty { + var updatedState = generateStateOfInputting() + updatedState.tooltipDuration = 0 + updatedState.tooltip = tooltipCodePointInputMode + delegate.switchState(updatedState) + return true + } + } + return handleCodePointInputToggle() + } // 引入 macOS 內建注音輸入法的行為,允許用 Shift+BackSpace 解構前一個漢字的讀音。 shiftBksp: switch prefs.specifyShiftBackSpaceKeyBehavior { @@ -448,6 +473,9 @@ extension InputHandler { func handleDelete(input: InputSignalProtocol) -> Bool { guard let delegate = delegate else { return false } let state = delegate.state + + if isCodePointInputMode { return handleCodePointInputToggle() } + guard state.type == .ofInputting else { return false } if input.isShiftHold { @@ -543,6 +571,9 @@ extension InputHandler { func handleEsc() -> Bool { guard let delegate = delegate else { return false } let state = delegate.state + + if isCodePointInputMode { return handleCodePointInputToggle() } + guard state.type == .ofInputting else { return false } if prefs.escToCleanInputBuffer { @@ -763,6 +794,25 @@ extension InputHandler { return true } + // MARK: - 處理區位輸入狀態的啟動過程 + + @discardableResult func handleCodePointInputToggle() -> Bool { + guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false } + if isCodePointInputMode { + isCodePointInputMode = false + delegate.switchState(IMEState.ofAbortion()) + return true + } + var updatedState = generateStateOfInputting(sansReading: true) + delegate.switchState(IMEState.ofCommitting(textToCommit: updatedState.displayedText)) + updatedState = IMEState.ofEmpty() + updatedState.tooltipDuration = 0 + updatedState.tooltip = tooltipCodePointInputMode + delegate.switchState(updatedState) + isCodePointInputMode = true + return true + } + // MARK: - 處理符號選單 /// 處理符號選單。 diff --git a/Source/Resources/Base.lproj/Localizable.strings b/Source/Resources/Base.lproj/Localizable.strings index 77edc2d0..3e9517b7 100644 --- a/Source/Resources/Base.lproj/Localizable.strings +++ b/Source/Resources/Base.lproj/Localizable.strings @@ -1,4 +1,6 @@ "vChewing" = "vChewing"; +"Invalid Code Point." = "Invalid Code Point."; +"Code Point Input Mode." = "Code Point Input Mode."; "You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas." = "You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas."; "Uncheck" = "Uncheck"; "Leave it checked" = "Leave it checked"; diff --git a/Source/Resources/en.lproj/Localizable.strings b/Source/Resources/en.lproj/Localizable.strings index 77edc2d0..3e9517b7 100644 --- a/Source/Resources/en.lproj/Localizable.strings +++ b/Source/Resources/en.lproj/Localizable.strings @@ -1,4 +1,6 @@ "vChewing" = "vChewing"; +"Invalid Code Point." = "Invalid Code Point."; +"Code Point Input Mode." = "Code Point Input Mode."; "You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas." = "You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas."; "Uncheck" = "Uncheck"; "Leave it checked" = "Leave it checked"; diff --git a/Source/Resources/ja.lproj/Localizable.strings b/Source/Resources/ja.lproj/Localizable.strings index bcc86625..bcfcb694 100644 --- a/Source/Resources/ja.lproj/Localizable.strings +++ b/Source/Resources/ja.lproj/Localizable.strings @@ -1,4 +1,6 @@ "vChewing" = "威注音入力アプリ"; +"Invalid Code Point." = "コードポイントは正しくない。"; +"Code Point Input Mode." = "コードポイント入力。"; "You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas." = "マナーモードのチェックの外すことになります。周りの人は貴殿のパソコンから屁音を聞いてしまい、それについての結果は全部貴殿の自己責任でございます。公的なる場所でこのチェックを外すのはお勧めできません。"; "Uncheck" = "チェックの外す"; "Leave it checked" = "チェックをそのままに"; diff --git a/Source/Resources/zh-Hans.lproj/Localizable.strings b/Source/Resources/zh-Hans.lproj/Localizable.strings index 6bbbeb65..a118c160 100644 --- a/Source/Resources/zh-Hans.lproj/Localizable.strings +++ b/Source/Resources/zh-Hans.lproj/Localizable.strings @@ -1,4 +1,6 @@ "vChewing" = "威注音输入法"; +"Invalid Code Point." = "内码不正确。"; +"Code Point Input Mode." = "内码输入模式。"; "You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas." = "您即将取消对廉耻模式的勾选。周围的人会听到您的电脑传出放屁的声音,且您对此负全部责任。我们强烈建议您不要在任何公共场合取消对该模式的勾选。"; "Uncheck" = "取消勾选"; "Leave it checked" = "保持勾选"; diff --git a/Source/Resources/zh-Hant.lproj/Localizable.strings b/Source/Resources/zh-Hant.lproj/Localizable.strings index d777276b..00891677 100644 --- a/Source/Resources/zh-Hant.lproj/Localizable.strings +++ b/Source/Resources/zh-Hant.lproj/Localizable.strings @@ -1,4 +1,6 @@ "vChewing" = "威注音輸入法"; +"Invalid Code Point." = "內碼不正確。"; +"Code Point Input Mode." = "內碼輸入模式。"; "You are about to uncheck this fart suppressor. You are responsible for all consequences lead by letting people nearby hear the fart sound come from your computer. We strongly advise against unchecking this in any public circumstance that prohibits NSFW netas." = "您即將取消對廉恥模式的勾選。周圍的人會聽到您的電腦傳出放屁的聲音,且您對此負全部責任。我們強烈建議您不要在任何公共場合取消對該模式的勾選。"; "Uncheck" = "取消勾選"; "Leave it checked" = "保持勾選";