diff --git a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSEvent.swift b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSEvent.swift index 74bed8b4..06432e1f 100644 --- a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSEvent.swift +++ b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSEvent.swift @@ -129,7 +129,7 @@ public extension NSEvent { var isShiftHold: Bool { modifierFlags.contains([.shift]) } var isCommandHold: Bool { modifierFlags.contains([.command]) } var isControlHold: Bool { modifierFlags.contains([.control]) } - var isControlHotKey: Bool { modifierFlags.contains([.control]) && text.first?.isLetter ?? false } + var beganWithLetter: Bool { text.first?.isLetter ?? false } var isOptionHold: Bool { modifierFlags.contains([.option]) } var isOptionHotKey: Bool { modifierFlags.contains([.option]) && text.first?.isLetter ?? false } var isCapsLockOn: Bool { modifierFlags.contains([.capsLock]) } diff --git a/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift b/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift index ce61cf87..5da0f3f0 100644 --- a/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift +++ b/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift @@ -33,7 +33,7 @@ public protocol InputSignalProtocol { var isShiftHold: Bool { get } var isCommandHold: Bool { get } var isControlHold: Bool { get } - var isControlHotKey: Bool { get } + var beganWithLetter: Bool { get } var isOptionHold: Bool { get } var isOptionHotKey: Bool { get } var isCapsLockOn: Bool { get } diff --git a/Source/Modules/InputHandler_Core.swift b/Source/Modules/InputHandler_Core.swift index 7e757c70..ea033267 100644 --- a/Source/Modules/InputHandler_Core.swift +++ b/Source/Modules/InputHandler_Core.swift @@ -542,9 +542,9 @@ public class InputHandler: InputHandlerProtocol { return result > 0 } - /// 生成標點符號索引鍵。 + /// 生成標點符號索引鍵頭。 /// - Parameter input: 輸入的按鍵訊號。 - /// - Returns: 生成的標點符號索引鍵。 + /// - Returns: 生成的標點符號索引鍵頭。 func generatePunctuationNamePrefix(withKeyCondition input: InputSignalProtocol) -> String { if prefs.halfWidthPunctuationEnabled { return "_half_punctuation_" } // 注意:這一行為 SHIFT+ALT+主鍵盤數字鍵專用,強制無視不同地區的鍵盤在這個按鍵組合下的符號輸入差異。 @@ -559,6 +559,27 @@ public class InputHandler: InputHandlerProtocol { } return result } + + /// 生成用以在詞庫內檢索標點符號按鍵資料的檢索字串陣列。 + /// - Parameter input: 輸入的按鍵訊號。 + /// - Returns: 生成的標點符號索引字串。 + func punctuationQueryStrings(input: InputSignalProtocol) -> [String] { + /// 如果仍無匹配結果的話,先看一下: + /// - 是否是針對當前注音排列/拼音輸入種類專門提供的標點符號。 + /// - 是否是需要摁修飾鍵才可以輸入的那種標點符號。 + var result: [String] = [] + let inputText = input.text + let punctuationNamePrefix: String = generatePunctuationNamePrefix(withKeyCondition: input) + let parser = currentKeyboardParser + let arrCustomPunctuations: [String] = [punctuationNamePrefix, parser, inputText] + let customPunctuation: String = arrCustomPunctuations.joined() + result.append(customPunctuation) + /// 如果仍無匹配結果的話,看看這個輸入是否是不需要修飾鍵的那種標點鍵輸入。 + let arrPunctuations: [String] = [punctuationNamePrefix, inputText] + let punctuation: String = arrPunctuations.joined() + result.append(punctuation) + return result + } } // MARK: - Components for Popup Composition Buffer (PCB) Window. diff --git a/Source/Modules/InputHandler_HandleCandidate.swift b/Source/Modules/InputHandler_HandleCandidate.swift index 33fb23f6..6d831ea7 100644 --- a/Source/Modules/InputHandler_HandleCandidate.swift +++ b/Source/Modules/InputHandler_HandleCandidate.swift @@ -16,8 +16,7 @@ import Shared extension InputHandler { /// 當且僅當選字窗出現時,對於經過初次篩選處理的輸入訊號的處理均藉由此函式來進行。 - /// - Parameters: - /// - input: 輸入訊號。 + /// - Parameter input: 輸入訊號。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 func handleCandidate(input: InputSignalProtocol) -> Bool { guard let delegate = delegate else { return false } @@ -69,7 +68,7 @@ extension InputHandler { } else { delegate.switchState(generateStateOfInputting()) if input.isCursorBackward || input.isCursorForward, input.isShiftHold { - return handleInput(event: input) + return triageInput(event: input) } } if state.type == .ofSymbolTable, let nodePrevious = state.node.previous, !nodePrevious.members.isEmpty { @@ -212,7 +211,7 @@ extension InputHandler { guard let candidateIndex = ctlCandidate.candidateIndexAtKeyLabelIndex(0) else { return true } delegate.candidateSelectionConfirmedByInputHandler(at: candidateIndex) delegate.switchState(IMEState.ofAbortion()) - return handleInput(event: input) + return triageInput(event: input) } } diff --git a/Source/Modules/InputHandler_HandleComposition.swift b/Source/Modules/InputHandler_HandleComposition.swift index 3a139c27..46fa7fb4 100644 --- a/Source/Modules/InputHandler_HandleComposition.swift +++ b/Source/Modules/InputHandler_HandleComposition.swift @@ -13,8 +13,7 @@ import Tekkon extension InputHandler { /// 用來處理 InputHandler.HandleInput() 當中的與組字有關的行為。 - /// - Parameters: - /// - input: 輸入訊號。 + /// - Parameter input: 輸入訊號。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 func handleComposition(input: InputSignalProtocol) -> Bool? { // 不處理任何包含不可列印字元的訊號。 @@ -27,8 +26,7 @@ extension InputHandler { // MARK: 注音按鍵輸入處理 (Handle BPMF Keys) /// 用來處理 InputHandler.HandleInput() 當中的與注音输入有關的組字行為。 - /// - Parameters: - /// - input: 輸入訊號。 + /// - Parameter input: 輸入訊號。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 private func handlePhonabetComposition(input: InputSignalProtocol) -> Bool? { guard let delegate = delegate else { return nil } @@ -208,8 +206,7 @@ extension InputHandler { extension InputHandler { /// 用來處理 InputHandler.HandleInput() 當中的與磁帶模組有關的組字行為。 - /// - Parameters: - /// - input: 輸入訊號。 + /// - Parameter input: 輸入訊號。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 private func handleCassetteComposition(input: InputSignalProtocol) -> Bool? { guard let delegate = delegate else { return nil } @@ -339,8 +336,7 @@ extension InputHandler { // MARK: 區位輸入處理 (Handle Code Point Input) /// 用來處理 InputHandler.HandleInput() 當中的與區位輸入有關的組字行為。 - /// - Parameters: - /// - input: 輸入訊號。 + /// - Parameter input: 輸入訊號。 /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 private func handleCodePointComposition(input: InputSignalProtocol) -> Bool? { guard !input.isReservedKey else { return nil } diff --git a/Source/Modules/InputHandler_HandleEvent.swift b/Source/Modules/InputHandler_HandleEvent.swift index b3597a1f..f74db611 100644 --- a/Source/Modules/InputHandler_HandleEvent.swift +++ b/Source/Modules/InputHandler_HandleEvent.swift @@ -18,7 +18,7 @@ extension InputHandler { /// - Parameter event: 由 IMK 選字窗接收的裝置操作輸入事件。 /// - Returns: 回「`true`」以將該按鍵已攔截處理的訊息傳遞給 IMK;回「`false`」則放行、不作處理。 public func handleEvent(_ event: NSEvent) -> Bool { - imkCandidatesEventPreHandler(event: event) ?? handleInput(event: event) + imkCandidatesEventPreHandler(event: event) ?? triageInput(event: event) } /// 專門處理與 IMK 選字窗有關的判斷語句。 @@ -71,7 +71,7 @@ extension InputHandler { let eventArray = [event] guard let imkC = delegate.candidateController() as? CtlCandidateIMK else { return false } if event.isEsc || event.isBackSpace || event.isDelete || (event.isShiftHold && !event.isSpace) { - return handleInput(event: event) + return triageInput(event: event) } else if event.isSymbolMenuPhysicalKey { // 符號鍵的行為是固定的,不受偏好設定影響。 switch imkC.currentLayout { @@ -118,7 +118,7 @@ extension InputHandler { if let newEvent = newEvent { if prefs.useSCPCTypingMode, delegate.state.type == .ofAssociates { // 註:input.isShiftHold 已經在 delegate.handle() 內處理,因為在那邊處理才有效。 - return event.isShiftHold ? true : handleInput(event: event) + return event.isShiftHold ? true : triageInput(event: event) } else { if #available(macOS 10.14, *) { PrefMgr.shared.failureFlagForIMKCandidates = true @@ -149,7 +149,7 @@ extension InputHandler { } if prefs.useSCPCTypingMode, !event.isReservedKey { - return handleInput(event: event) + return triageInput(event: event) } if delegate.state.type == .ofAssociates, @@ -157,7 +157,7 @@ extension InputHandler { !event.isCursorClockLeft, !event.isCursorClockRight, !event.isSpace, !event.isEnter || !prefs.alsoConfirmAssociatedCandidatesByEnter { - return handleInput(event: event) + return triageInput(event: event) } imkC.interpretKeyEvents(eventArray) return true diff --git a/Source/Modules/InputHandler_HandleInput.swift b/Source/Modules/InputHandler_HandleInput.swift deleted file mode 100644 index e0c88c74..00000000 --- a/Source/Modules/InputHandler_HandleInput.swift +++ /dev/null @@ -1,297 +0,0 @@ -// (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. - -/// 該檔案乃輸入調度模組當中「用來規定當 IMK 接受按鍵訊號時且首次交給輸入調度模組處理時、 -/// 輸入調度模組要率先處理」的部分。據此判斷是否需要將按鍵處理委派給其它成員函式。 - -import CocoaExtension -import IMKUtils -import LangModelAssembly -import Shared - -// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) - -extension InputHandler { - /// 對於輸入訊號的第一關處理均藉由此函式來進行。 - /// - Remark: 送入該函式處理之前,先用 inputHandler?.handleEvent() 分診、來判斷是否需要交給 IMKCandidates 處理。 - /// - Parameters: - /// - input: 輸入訊號。 - /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 - func handleInput(event input: InputSignalProtocol) -> Bool { - // delegate 必須存在,否則不處理。 - guard let delegate = delegate else { return false } - - let inputText: String = input.text - var state: IMEStateProtocol { delegate.state } // 常數轉變數。 - - // 提前放行一些用不到的特殊按鍵輸入情形。 - if input.isInvalid, state.type == .ofEmpty || state.type == .ofDeactivated { return false } - - // 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。 - let isFunctionKey: Bool = - input.isControlHotKey || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey) - if state.type != .ofAssociates, !state.hasComposition, !state.isCandidateContainer, isFunctionKey { - return false - } - - // MARK: Caps Lock 處理 - - /// 若 Caps Lock 被啟用的話,則暫停對注音輸入的處理。 - /// 這裡的處理仍舊有用,不然 Caps Lock 英文模式無法直接鍵入小寫字母。 - if input.isCapsLockOn || delegate.isASCIIMode { - // 低於 macOS 12 的系統無法偵測 CapsLock 的啟用狀態,所以這裡一律強制重置狀態為 .ofEmpty()。 - delegate.switchState(IMEState.ofEmpty()) - - // 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。 - if (input.isUpperCaseASCIILetterKey && delegate.isASCIIMode) - || (input.isCapsLockOn && input.isShiftHold) - { - return false - } - - /// 如果是 ASCII 當中的不可列印的字元的話,不使用「insertText:replacementRange:」。 - /// 某些應用無法正常處理非 ASCII 字符的輸入。 - if input.isASCII, !input.charCode.isPrintableASCII { return false } - - // 將整個組字區的內容遞交給客體應用。 - delegate.switchState(IMEState.ofCommitting(textToCommit: inputText.lowercased())) - - return true - } - - // MARK: 處理數字小鍵盤 (Numeric Pad Processing) - - // 這裡的「isNumericPadKey」處理邏輯已經改成用 KeyCode 判定數字鍵區輸入、以鎖定按鍵範圍。 - // 不然、使用 Cocoa 內建的 flags 的話,會誤傷到在主鍵盤區域的功能鍵。 - // 我們先規定允許小鍵盤區域操縱選字窗,其餘場合一律直接放行。 - if input.isNumericPadKey { - if ![.ofCandidates, .ofAssociates, .ofSymbolTable].contains(state.type) { - delegate.switchState(IMEState.ofEmpty()) - delegate.switchState(IMEState.ofCommitting(textToCommit: inputText.lowercased())) - return true - } - } - - // MARK: 處理候選字詞 (Handle Candidates) - - if [.ofCandidates, .ofSymbolTable].contains(state.type) { return handleCandidate(input: input) } - - // MARK: 攔截部分無效按鍵。 - - // 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。 - // 不處理任何包含不可列印字元的訊號。 - guard !input.text.isEmpty, input.charCode.isPrintable else { return false } - - // MARK: 處理聯想詞 (Handle Associated Phrases) - - if state.type == .ofAssociates { - if handleCandidate(input: input) { - return true - } else { - delegate.switchState(IMEState.ofEmpty()) - } - } - - // MARK: 處理標記範圍、以便決定要把哪個範圍拿來新增使用者(濾除)語彙 (Handle Marking) - - if state.type == .ofMarking { - if handleMarkingState(input: input) { return true } - delegate.switchState(state.convertedToInputting) - } - - // MARK: 判斷是否響應傳統的漢音鍵盤符號模式熱鍵。 - - if let x = input.inputTextIgnoringModifiers, - "¥\\".contains(x), input.modifierFlags.isEmpty, - prefs.classicHaninKeyboardSymbolModeShortcutEnabled - { - return handleHaninKeyboardSymbolModeToggle() - } - - // MARK: 注音按鍵輸入與漢音鍵盤符號輸入處理 (Handle BPMF Keys & Hanin Keyboard Symbol Input) - - if isHaninKeyboardSymbolMode, [[], .shift].contains(input.modifierFlags) { - return handleHaninKeyboardSymbolModeInput(input: input) - } else if let compositionHandled = handleComposition(input: input) { - return compositionHandled - } - - // MARK: 用上下左右鍵呼叫選字窗 (Calling candidate window using Up / Down or PageUp / PageDn.) - - // 僅憑藉 state.hasComposition 的話,並不能真實把握組字器的狀況。 - // 另外,這裡不要用「!input.isFunctionKeyHold」,否則會導致對上下左右鍵與翻頁鍵的判斷失效。 - if state.hasComposition, !compositor.isEmpty, isComposerOrCalligrapherEmpty, - !input.isOptionHold, !input.isShiftHold, !input.isCommandHold, !input.isControlHold, - input.isCursorClockLeft || input.isCursorClockRight || (input.isSpace && prefs.chooseCandidateUsingSpace) - || input.isPageDown || input.isPageUp || (input.isTab && prefs.specifyShiftTabKeyBehavior) - { - // 開始決定是否切換至選字狀態。 - let candidateState: IMEStateProtocol = generateStateOfCandidates() - _ = candidateState.candidates.isEmpty ? delegate.callError("3572F238") : delegate.switchState(candidateState) - return true - } - - // MARK: Ctrl+Command+[] 輪替候選字 - - // Shift+Command+[] 被 Chrome 系瀏覽器佔用,所以改用 Ctrl。 - revolveCandidateWithBrackets: if input.modifierFlags == [.control, .command] { - if state.type != .ofInputting { break revolveCandidateWithBrackets } - // 此處 JIS 鍵盤判定無法用於螢幕鍵盤。所以,螢幕鍵盤的場合,系統會依照 US 鍵盤的判定方案。 - let isJIS: Bool = KBGetLayoutType(Int16(LMGetKbdType())) == kKeyboardJIS - switch (input.keyCode, isJIS) { - case (30, true), (33, false): return revolveCandidate(reverseOrder: true) - case (42, true), (30, false): return revolveCandidate(reverseOrder: false) - default: break - } - } - - // MARK: 批次集中處理某些常用功能鍵 - - if let keyCodeType = KeyCode(rawValue: input.keyCode) { - switch keyCodeType { - case .kEscape: return handleEsc() - case .kTab, .kContextMenu: return revolveCandidate(reverseOrder: input.isShiftHold) - case .kUpArrow, .kDownArrow, .kLeftArrow, .kRightArrow: - let rotation: Bool = (input.isOptionHold || input.isShiftHold) && state.type == .ofInputting - handleArrowKey: switch (keyCodeType, delegate.isVerticalTyping) { - case (.kLeftArrow, false), (.kUpArrow, true): return handleBackward(input: input) - case (.kRightArrow, false), (.kDownArrow, true): return handleForward(input: input) - case (.kUpArrow, false), (.kLeftArrow, true): - return rotation ? revolveCandidate(reverseOrder: true) : handleClockKey() - case (.kDownArrow, false), (.kRightArrow, true): - return rotation ? revolveCandidate(reverseOrder: false) : handleClockKey() - default: break handleArrowKey // 該情況應該不會發生,因為上面都有處理過。 - } - case .kHome: return handleHome() - case .kEnd: return handleEnd() - case .kBackSpace: return handleBackSpace(input: input) - case .kWindowsDelete: return handleDelete(input: input) - case .kCarriageReturn, .kLineFeed: return handleEnter(input: input) - case .kSymbolMenuPhysicalKeyJIS, .kSymbolMenuPhysicalKeyIntl: - let isJIS = keyCodeType == .kSymbolMenuPhysicalKeyJIS - switch input.modifierFlags { - case []: - return handlePunctuationList(alternative: false, isJIS: isJIS) - case [.option, .shift]: - return handlePunctuationList(alternative: true, isJIS: isJIS) - case .option: - switch (isCodePointInputMode, isHaninKeyboardSymbolMode) { - case (false, false): return handleCodePointInputToggle() - case (true, false), (false, true): - return handleHaninKeyboardSymbolModeToggle() - default: break - } - return true - default: break - } - case .kSpace: // 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話……… - // 空格字符輸入行為處理。 - switch state.type { - case .ofEmpty: - if !input.isOptionHold, !input.isControlHold, !input.isCommandHold { - delegate.switchState(IMEState.ofCommitting(textToCommit: input.isShiftHold ? " " : " ")) - return true - } - case .ofInputting: - // 臉書等網站會攔截 Tab 鍵,所以用 Shift+Command+Space 對候選字詞做正向/反向輪替。 - if input.isShiftHold, !input.isControlHold, !input.isOptionHold { - return revolveCandidate(reverseOrder: input.isCommandHold) - } - if compositor.cursor < compositor.length, compositor.insertKey(" ") { - walk() - // 一邊吃一邊屙(僅對位列黑名單的 App 用這招限制組字區長度)。 - let textToCommit = commitOverflownComposition - var inputting = generateStateOfInputting() - inputting.textToCommit = textToCommit - delegate.switchState(inputting) - } else { - let displayedText = state.displayedText - if !displayedText.isEmpty, !isConsideredEmptyForNow { - delegate.switchState(IMEState.ofCommitting(textToCommit: displayedText)) - } - delegate.switchState(IMEState.ofCommitting(textToCommit: " ")) - } - return true - default: break - } - default: break - } - } - - // MARK: 全形/半形阿拉伯數字輸入 (FW / HW Arabic Numbers Input) - - if state.type == .ofEmpty { - if input.isMainAreaNumKey, input.isOptionHold, !input.isCommandHold, !input.isControlHold { - guard let strRAW = input.mainAreaNumKeyChar else { return false } - let newString: String = { - if input.isShiftHold { - return strRAW.applyingTransformFW2HW(reverse: !prefs.halfWidthPunctuationEnabled) - } - return strRAW.applyingTransformFW2HW(reverse: false) - }() - delegate.switchState(IMEState.ofCommitting(textToCommit: newString)) - return true - } - } - - // MARK: Punctuation - - /// 如果仍無匹配結果的話,先看一下: - /// - 是否是針對當前注音排列/拼音輸入種類專門提供的標點符號。 - /// - 是否是需要摁修飾鍵才可以輸入的那種標點符號。 - let punctuationNamePrefix: String = generatePunctuationNamePrefix(withKeyCondition: input) - let parser = currentKeyboardParser - let arrCustomPunctuations: [String] = [punctuationNamePrefix, parser, input.text] - let customPunctuation: String = arrCustomPunctuations.joined() - if handlePunctuation(customPunctuation) { return true } - /// 如果仍無匹配結果的話,看看這個輸入是否是不需要修飾鍵的那種標點鍵輸入。 - let arrPunctuations: [String] = [punctuationNamePrefix, input.text] - let punctuation: String = arrPunctuations.joined() - if handlePunctuation(punctuation) { return true } - - // MARK: 摁住 Shift+字母鍵 的處理 (Shift+Letter Processing) - - if input.isUpperCaseASCIILetterKey, !input.isCommandHold, !input.isControlHold { - if input.isShiftHold { // 這裡先不要判斷 isOptionHold。 - switch prefs.upperCaseLetterKeyBehavior { - case 1, 3: - if prefs.upperCaseLetterKeyBehavior == 3, !isConsideredEmptyForNow { break } - let commitText = generateStateOfInputting(sansReading: true).displayedText - delegate.switchState(IMEState.ofCommitting(textToCommit: commitText + inputText.lowercased())) - return true - case 2, 4: - if prefs.upperCaseLetterKeyBehavior == 4, !isConsideredEmptyForNow { break } - let commitText = generateStateOfInputting(sansReading: true).displayedText - delegate.switchState(IMEState.ofCommitting(textToCommit: commitText + inputText.uppercased())) - return true - default: // 包括 case 0。 - break - } - // 直接塞給組字區。 - let letter = "_letter_\(inputText)" - if handlePunctuation(letter) { - return true - } - } - } - - // MARK: - 終末處理 (Still Nothing) - - /// 對剩下的漏網之魚做攔截處理、直接將當前狀態繼續回呼給 SessionCtl。 - /// 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。 - /// 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 - /// 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 - if state.hasComposition || !isComposerOrCalligrapherEmpty { - delegate.callError("Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode), text: \(input.text)") - delegate.callError("A9BFF20E") - return true - } - - return false - } -} diff --git a/Source/Modules/InputHandler_HandleStates.swift b/Source/Modules/InputHandler_HandleStates.swift index 9e5672e9..eb3a1cae 100644 --- a/Source/Modules/InputHandler_HandleStates.swift +++ b/Source/Modules/InputHandler_HandleStates.swift @@ -864,7 +864,7 @@ extension InputHandler { return true } - // MARK: - 處理區位輸入狀態的啟動過程 + // MARK: - 處理區位輸入狀態的啟動過程(CodePoint Input Toggle) @discardableResult func handleCodePointInputToggle() -> Bool { guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false } @@ -883,7 +883,7 @@ extension InputHandler { return true } - // MARK: - 處理漢音鍵盤符號輸入狀態的啟動過程 + // MARK: - 處理漢音鍵盤符號輸入狀態的啟動過程(Hanin Pallete) @discardableResult func handleHaninKeyboardSymbolModeToggle() -> Bool { guard let delegate = delegate, delegate.state.type != .ofDeactivated else { return false } @@ -931,7 +931,7 @@ extension InputHandler { return true } - // MARK: - 處理符號選單 + // MARK: - 處理符號選單(Symbol Menu Input) /// 處理符號選單。 /// - Parameters: @@ -976,4 +976,117 @@ extension InputHandler { return true } } + + // MARK: - 處理 Caps Lock 與英數輸入模式(Caps Lock and Alphanumerical mode) + + /// 處理 CapsLock 與英數輸入模式。 + /// - Remark: 若 Caps Lock 被啟用的話,則暫停對注音輸入的處理。 + /// 這裡的處理仍舊有用,不然 Caps Lock 英文模式無法直接鍵入小寫字母。 + /// - Parameter input: 輸入訊號。 + /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 + func handleCapsLockAndAlphanumericalMode(input: InputSignalProtocol) -> Bool? { + guard let delegate = delegate else { return nil } + guard input.isCapsLockOn || delegate.isASCIIMode else { return nil } + + // 低於 macOS 12 的系統無法偵測 CapsLock 的啟用狀態, + // 所以這裡一律強制重置狀態為 .ofEmpty()。 + delegate.switchState(IMEState.ofEmpty()) + + // 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。 + if (input.isUpperCaseASCIILetterKey && delegate.isASCIIMode) + || (input.isCapsLockOn && input.isShiftHold) + { + return false + } + + /// 如果是 ASCII 當中的不可列印的字元的話, + /// 不使用「insertText:replacementRange:」。 + /// 某些應用無法正常處理非 ASCII 字符的輸入。 + if input.isASCII, !input.charCode.isPrintableASCII { return false } + + // 將整個組字區的內容遞交給客體應用。 + delegate.switchState(IMEState.ofCommitting(textToCommit: input.text.lowercased())) + + return true + } + + // MARK: - 呼叫選字窗(Intentionally Call Candidate Window) + + /// 手動呼叫選字窗。 + /// - Parameter input: 輸入訊號。 + /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 + func callCandidateState(input: InputSignalProtocol) -> Bool { + guard let delegate = delegate else { return false } + var state: IMEStateProtocol { delegate.state } + // 用上下左右鍵呼叫選字窗。 + // 僅憑藉 state.hasComposition 的話,並不能真實把握組字器的狀況。 + // 另外,這裡不要用「!input.isFunctionKeyHold」, + // 否則會導致對上下左右鍵與翻頁鍵的判斷失效。 + let notEmpty = state.hasComposition && !compositor.isEmpty && isComposerOrCalligrapherEmpty + let bannedModifiers: NSEvent.ModifierFlags = [.option, .shift, .command, .control] + let noBannedModifiers = bannedModifiers.intersection(input.modifierFlags).isEmpty + var triggered = input.isCursorClockLeft || input.isCursorClockRight + triggered = triggered || (input.isSpace && prefs.chooseCandidateUsingSpace) + triggered = triggered || input.isPageDown || input.isPageUp + triggered = triggered || (input.isTab && prefs.specifyShiftTabKeyBehavior) + guard notEmpty, noBannedModifiers, triggered else { return false } + // 開始決定是否切換至選字狀態。 + let candidateState: IMEStateProtocol = generateStateOfCandidates() + _ = candidateState.candidates.isEmpty ? delegate.callError("3572F238") : delegate.switchState(candidateState) + return true + } + + // MARK: - 處理全形/半形阿拉伯數字輸入(FW/HW Arabic Numerals) + + /// 處理全形/半形阿拉伯數字輸入。 + /// - Parameter input: 輸入訊號。 + /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 + func handleArabicNumeralInputs(input: InputSignalProtocol) -> Bool { + guard let delegate = delegate else { return false } + guard delegate.state.type == .ofEmpty, input.isMainAreaNumKey else { return false } + guard input.isOptionHold, !input.isCommandHold, !input.isControlHold else { return false } + guard let strRAW = input.mainAreaNumKeyChar else { return false } + let newString: String = { + if input.isShiftHold { + return strRAW.applyingTransformFW2HW(reverse: !prefs.halfWidthPunctuationEnabled) + } + return strRAW.applyingTransformFW2HW(reverse: false) + }() + delegate.switchState(IMEState.ofCommitting(textToCommit: newString)) + return true + } + + // MARK: - 處理「摁住 SHIFT 敲字母鍵」的輸入行為(Shift + Letter keys) + + /// 處理「摁住 SHIFT 敲字母鍵」的輸入行為。 + /// - Parameter input: 輸入訊號。 + /// - Returns: 告知 IMK「該按鍵是否已經被輸入法攔截處理」。 + func handleLettersWithShiftHold(input: InputSignalProtocol) -> Bool { + guard let delegate = delegate else { return false } + let inputText = input.text + if input.isUpperCaseASCIILetterKey, !input.isCommandHold, !input.isControlHold { + if input.isShiftHold { // 這裡先不要判斷 isOptionHold。 + switch prefs.upperCaseLetterKeyBehavior { + case 1, 3: + if prefs.upperCaseLetterKeyBehavior == 3, !isConsideredEmptyForNow { break } + let commitText = generateStateOfInputting(sansReading: true).displayedText + delegate.switchState(IMEState.ofCommitting(textToCommit: commitText + inputText.lowercased())) + return true + case 2, 4: + if prefs.upperCaseLetterKeyBehavior == 4, !isConsideredEmptyForNow { break } + let commitText = generateStateOfInputting(sansReading: true).displayedText + delegate.switchState(IMEState.ofCommitting(textToCommit: commitText + inputText.uppercased())) + return true + default: // 包括 case 0。 + break + } + // 直接塞給組字區。 + let letter = "_letter_\(inputText)" + if handlePunctuation(letter) { + return true + } + } + } + return false + } } diff --git a/Source/Modules/InputHandler_TriageInput.swift b/Source/Modules/InputHandler_TriageInput.swift new file mode 100644 index 00000000..30bc242c --- /dev/null +++ b/Source/Modules/InputHandler_TriageInput.swift @@ -0,0 +1,196 @@ +// (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. + +/// 該檔案乃輸入調度模組當中「用來規定當 IMK 接受按鍵訊號時且首次交給輸入調度模組處理時、 +/// 輸入調度模組要率先處理」的部分。據此判斷是否需要將按鍵處理委派給其它成員函式。 + +import CocoaExtension +import IMKUtils +import LangModelAssembly +import Shared + +// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) * Triage + +extension InputHandler { + func triageInput(event input: InputSignalProtocol) -> Bool { + guard let delegate = delegate else { return false } + var state: IMEStateProtocol { delegate.state } + let inputText = input.text + + // MARK: - 按鍵碼分診(Triage by KeyCode) + + func triageByKeyCode() -> Bool? { + guard let keyCodeType = KeyCode(rawValue: input.keyCode) else { return nil } + switch keyCodeType { + case .kEscape: return handleEsc() + case .kTab, .kContextMenu: return revolveCandidate(reverseOrder: input.isShiftHold) + case .kUpArrow, .kDownArrow, .kLeftArrow, .kRightArrow: + let rotation: Bool = (input.isOptionHold || input.isShiftHold) && state.type == .ofInputting + handleArrowKey: switch (keyCodeType, delegate.isVerticalTyping) { + case (.kLeftArrow, false), (.kUpArrow, true): return handleBackward(input: input) + case (.kRightArrow, false), (.kDownArrow, true): return handleForward(input: input) + case (.kUpArrow, false), (.kLeftArrow, true): + return rotation ? revolveCandidate(reverseOrder: true) : handleClockKey() + case (.kDownArrow, false), (.kRightArrow, true): + return rotation ? revolveCandidate(reverseOrder: false) : handleClockKey() + default: break handleArrowKey // 該情況應該不會發生,因為上面都有處理過。 + } + case .kHome: return handleHome() + case .kEnd: return handleEnd() + case .kBackSpace: return handleBackSpace(input: input) + case .kWindowsDelete: return handleDelete(input: input) + case .kCarriageReturn, .kLineFeed: return handleEnter(input: input) + case .kSymbolMenuPhysicalKeyJIS, .kSymbolMenuPhysicalKeyIntl: + let isJIS = keyCodeType == .kSymbolMenuPhysicalKeyJIS + switch input.modifierFlags { + case []: + return handlePunctuationList(alternative: false, isJIS: isJIS) + case [.option, .shift]: + return handlePunctuationList(alternative: true, isJIS: isJIS) + case .option: + switch (isCodePointInputMode, isHaninKeyboardSymbolMode) { + case (false, false): return handleCodePointInputToggle() + case (true, false), (false, true): + return handleHaninKeyboardSymbolModeToggle() + default: break + } + return true + default: break + } + case .kSpace: + // 倘若沒有在偏好設定內將 Space 空格鍵設為選字窗呼叫用鍵的話……… + // 空格字符輸入行為處理。 + switch state.type { + case .ofEmpty: + if !input.isOptionHold, !input.isControlHold, !input.isCommandHold { + delegate.switchState(IMEState.ofCommitting(textToCommit: input.isShiftHold ? " " : " ")) + return true + } + case .ofInputting: + // 臉書等網站會攔截 Tab 鍵,所以用 Shift+Command+Space 對候選字詞做正向/反向輪替。 + if input.isShiftHold, !input.isControlHold, !input.isOptionHold { + return revolveCandidate(reverseOrder: input.isCommandHold) + } + if compositor.cursor < compositor.length, compositor.insertKey(" ") { + walk() + // 一邊吃一邊屙(僅對位列黑名單的 App 用這招限制組字區長度)。 + let textToCommit = commitOverflownComposition + var inputting = generateStateOfInputting() + inputting.textToCommit = textToCommit + delegate.switchState(inputting) + } else { + let displayedText = state.displayedText + if !displayedText.isEmpty, !isConsideredEmptyForNow { + delegate.switchState(IMEState.ofCommitting(textToCommit: displayedText)) + } + delegate.switchState(IMEState.ofCommitting(textToCommit: " ")) + } + return true + default: break + } + default: break + } + return nil + } + + // MARK: - 按狀態分診(Triage by States) + + switch state.type { + case .ofDeactivated, .ofAbortion, .ofCommitting: return false + case .ofAssociates, .ofCandidates, .ofSymbolTable: + let result = handleCandidate(input: input) + guard !result, state.type == .ofAssociates else { return true } + delegate.switchState(IMEState.ofEmpty()) + return triageInput(event: input) + case .ofMarking: + if handleMarkingState(input: input) { return true } + delegate.switchState(state.convertedToInputting) + return triageInput(event: input) + case .ofEmpty, .ofInputting: + // 提前放行一些用不到的特殊按鍵輸入情形。 + guard !(input.isInvalid && state.type == .ofEmpty) else { return false } + + // 如果當前組字器為空的話,就不再攔截某些修飾鍵,畢竟這些鍵可能會會用來觸發某些功能。 + let isFunctional: Bool = (input.isControlHold && input.beganWithLetter) + || (input.isCommandHold || input.isOptionHotKey || input.isNonLaptopFunctionKey) + if !state.hasComposition, isFunctional { return false } + + // 若 Caps Lock 被啟用的話,則暫停對注音輸入的處理。 + // 這裡的處理仍舊有用,不然 Caps Lock 英文模式無法直接鍵入小寫字母。 + if let capsHandleResult = handleCapsLockAndAlphanumericalMode(input: input) { + return capsHandleResult + } + + // 處理九宮格數字鍵盤區域。 + if input.isNumericPadKey { + delegate.switchState(IMEState.ofEmpty()) + delegate.switchState(IMEState.ofCommitting(textToCommit: inputText.lowercased())) + return true + } + + // 判斷是否響應傳統的漢音鍵盤符號模式熱鍵。 + haninSymbolInput: if prefs.classicHaninKeyboardSymbolModeShortcutEnabled { + guard let x = input.inputTextIgnoringModifiers, + "¥\\".contains(x), input.modifierFlags.isEmpty + else { break haninSymbolInput } + return handleHaninKeyboardSymbolModeToggle() + } + + // 注音按鍵輸入與漢音鍵盤符號輸入處理。 + if isHaninKeyboardSymbolMode, [[], .shift].contains(input.modifierFlags) { + return handleHaninKeyboardSymbolModeInput(input: input) + } else if let compositionHandled = handleComposition(input: input) { + return compositionHandled + } + + // 手動呼叫選字窗。 + if callCandidateState(input: input) { return true } + + // Ctrl+Command+[] 輪替候選字。 + // Shift+Command+[] 被 Chrome 系瀏覽器佔用,所以改用 Ctrl。 + revolveCandidateWithBrackets: if input.modifierFlags == [.control, .command] { + if state.type != .ofInputting { break revolveCandidateWithBrackets } + // 此處 JIS 鍵盤判定無法用於螢幕鍵盤。所以,螢幕鍵盤的場合,系統會依照 US 鍵盤的判定方案。 + let isJIS: Bool = KBGetLayoutType(Int16(LMGetKbdType())) == kKeyboardJIS + switch (input.keyCode, isJIS) { + case (30, true), (33, false): return revolveCandidate(reverseOrder: true) + case (42, true), (30, false): return revolveCandidate(reverseOrder: false) + default: break + } + } + + // 根據 keyCode 進行分診處理。 + if let keyCodeTriaged = triageByKeyCode() { return keyCodeTriaged } + + // 全形/半形阿拉伯數字輸入。 + if handleArabicNumeralInputs(input: input) { return true } + + // 標點符號。 + let queryStrings: [String] = punctuationQueryStrings(input: input) + for queryString in queryStrings { + guard !handlePunctuation(queryString) else { return true } + } + + // 摁住 Shift+字母鍵 的處理 + if handleLettersWithShiftHold(input: input) { return true } + } + + // 終末處理(Still Nothing): + // 對剩下的漏網之魚做攔截處理、直接將當前狀態繼續回呼給 SessionCtl。 + // 否則的話,可能會導致輸入法行為異常:部分應用會阻止輸入法完全攔截某些按鍵訊號。 + // 砍掉這一段會導致「F1-F12 按鍵干擾組字區」的問題。 + // 暫時只能先恢復這段,且補上偵錯彙報機制,方便今後排查故障。 + if state.hasComposition || !isComposerOrCalligrapherEmpty { + delegate.callError("Blocked data: charCode: \(input.charCode), keyCode: \(input.keyCode), text: \(input.text)") + delegate.callError("A9BFF20E") + return true + } + + return false + } +} diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index 74f0ef0c..d8c8ccc3 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -49,7 +49,7 @@ 5B78EE0D28A562B4009456C1 /* VwrPrefPaneDevZone.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B78EE0C28A562B4009456C1 /* VwrPrefPaneDevZone.swift */; }; 5B7BC4B027AFFBE800F66C24 /* frmPrefWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5B7BC4AE27AFFBE800F66C24 /* frmPrefWindow.xib */; }; 5B7DA80328BF6BC600D7B2AD /* fixinstall.sh in Resources */ = {isa = PBXBuildFile; fileRef = 5B7DA80228BF6BBA00D7B2AD /* fixinstall.sh */; }; - 5B7F225D2808501000DDD3CB /* InputHandler_HandleInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B7F225C2808501000DDD3CB /* InputHandler_HandleInput.swift */; }; + 5B7F225D2808501000DDD3CB /* InputHandler_TriageInput.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B7F225C2808501000DDD3CB /* InputHandler_TriageInput.swift */; }; 5B84579E2871AD2200C93B01 /* convdict.json in Resources */ = {isa = PBXBuildFile; fileRef = 5B84579C2871AD2200C93B01 /* convdict.json */; }; 5B8457A12871ADBE00C93B01 /* ChineseConverterBridge.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5B8457A02871ADBE00C93B01 /* ChineseConverterBridge.swift */; }; 5B963C9D28D5BFB800DCEE88 /* CocoaExtension in Frameworks */ = {isa = PBXBuildFile; productRef = 5B963C9C28D5BFB800DCEE88 /* CocoaExtension */; }; @@ -250,7 +250,7 @@ 5B7BC4AF27AFFBE800F66C24 /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmPrefWindow.xib; sourceTree = ""; }; 5B7BC4B227AFFC0B00F66C24 /* en */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = en; path = en.lproj/frmPrefWindow.strings; sourceTree = ""; }; 5B7DA80228BF6BBA00D7B2AD /* fixinstall.sh */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.script.sh; lineEnding = 0; path = fixinstall.sh; sourceTree = ""; }; - 5B7F225C2808501000DDD3CB /* InputHandler_HandleInput.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputHandler_HandleInput.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; + 5B7F225C2808501000DDD3CB /* InputHandler_TriageInput.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = InputHandler_TriageInput.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5B84579C2871AD2200C93B01 /* convdict.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = convdict.json; sourceTree = ""; }; 5B8457A02871ADBE00C93B01 /* ChineseConverterBridge.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ChineseConverterBridge.swift; sourceTree = ""; }; 5B963C9B28D5BE4100DCEE88 /* vChewing_CocoaExtension */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = vChewing_CocoaExtension; path = Packages/vChewing_CocoaExtension; sourceTree = ""; }; @@ -694,8 +694,8 @@ 5B782EC3280C243C007276DE /* InputHandler_HandleCandidate.swift */, 5BE3779F288FED8D0037365B /* InputHandler_HandleComposition.swift */, 5BE1F8A828F86AB5006C7FF5 /* InputHandler_HandleEvent.swift */, - 5B7F225C2808501000DDD3CB /* InputHandler_HandleInput.swift */, 5B3133BE280B229700A4A505 /* InputHandler_HandleStates.swift */, + 5B7F225C2808501000DDD3CB /* InputHandler_TriageInput.swift */, 5BAEFACF28012565001F42C9 /* LMMgr_Core.swift */, 5B33844E29B8B4C200FCB497 /* LMMgr_PhraseEditorDelegate.swift */, 5B33844C29B8980100FCB497 /* LMMgr_UserPhraseStructure.swift */, @@ -1106,7 +1106,7 @@ 5BF018FD299923C200248CDD /* VwrPrefPaneCandidates.swift in Sources */, 5B963CA828D5DB1400DCEE88 /* PrefMgr_Core.swift in Sources */, D427F76C278CA2B0004A2160 /* AppDelegate.swift in Sources */, - 5B7F225D2808501000DDD3CB /* InputHandler_HandleInput.swift in Sources */, + 5B7F225D2808501000DDD3CB /* InputHandler_TriageInput.swift in Sources */, 5B660A8628F64A8800E5E4F6 /* SymbolMenuDefaultData.swift in Sources */, 5BF56F9828C39A2700DD6839 /* IMEState.swift in Sources */, 5B62A33D27AE7CC100A19448 /* CtlAboutWindow.swift in Sources */,