diff --git a/Source/Modules/InputHandler_Core.swift b/Source/Modules/InputHandler_Core.swift index ca8ae2f5..05650073 100644 --- a/Source/Modules/InputHandler_Core.swift +++ b/Source/Modules/InputHandler_Core.swift @@ -20,7 +20,9 @@ import Tekkon /// InputHandler 委任協定 public protocol InputHandlerDelegate { var selectionKeys: String { get } + var state: IMEStateProtocol { get set } var clientBundleIdentifier: String { get } + func handle(state newState: IMEStateProtocol, replaceCurrent: Bool) func candidateController() -> CtlCandidateProtocol func candidateSelectionCalledByInputHandler(at index: Int) func performUserPhraseOperation(with state: IMEStateProtocol, addToFilter: Bool) diff --git a/Source/Modules/InputHandler_HandleEvent.swift b/Source/Modules/InputHandler_HandleEvent.swift new file mode 100644 index 00000000..96aabbd6 --- /dev/null +++ b/Source/Modules/InputHandler_HandleEvent.swift @@ -0,0 +1,143 @@ +// (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. + +/// 該檔案乃按鍵調度模組當中用來預處理 NSEvent 的模組。 + +import InputMethodKit +import Shared + +// MARK: - § 根據狀態調度按鍵輸入 (Handle Input with States) + +extension InputHandler { + /// 分診函式,會先確認是否是 IMK 選字窗要處理的事件、然後再決定處理步驟。 + /// - Parameter event: 由 IMK 選字窗接收的裝置操作輸入事件。 + /// - Returns: 回「`true`」以將該案件已攔截處理的訊息傳遞給 IMK;回「`false`」則放行、不作處理。 + public func handleEvent(_ event: NSEvent) -> Bool { + imkCandidatesEventPreHandler(event: event) ?? commonEventHandler(event) + } + + /// 將按鍵行為與當前輸入法狀態結合起來、交給按鍵調度模組來處理。 + /// 再根據返回的 result bool 數值來告知 IMK「這個按鍵事件是被處理了還是被放行了」。 + /// 這裡不用 handleCandidate() 是因為需要針對聯想詞輸入狀態做額外處理。 + private func commonEventHandler(_ event: NSEvent) -> Bool { + guard let delegate = delegate else { return false } + + let result = handleInput(event: event, state: delegate.state) { newState in + delegate.handle(state: newState, replaceCurrent: true) + } errorCallback: { errorString in + vCLog(errorString) + IMEApp.buzz() + } + return result + } + + /// 專門處理與 IMK 選字窗有關的判斷語句。 + /// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。 + /// - Parameter event: 由 IMK 選字窗接收的裝置操作輸入事件。 + /// - Returns: 回「`true`」以將該案件已攔截處理的訊息傳遞給 IMK;回「`false`」則放行、不作處理。 + private func imkCandidatesEventPreHandler(event eventToDeal: NSEvent) -> Bool? { + guard let delegate = delegate else { return false } + + // IMK 選字窗處理,當且僅當啟用了 IMK 選字窗的時候才會生效。 + // 這樣可以讓 interpretKeyEvents() 函式自行判斷: + // - 是就地交給 imkCandidates.interpretKeyEvents() 處理? + // - 還是藉由 delegate 扔回 SessionCtl 給 InputHandler 處理? + if let imkCandidates = delegate.candidateController() as? CtlCandidateIMK, imkCandidates.visible { + let event: NSEvent = CtlCandidateIMK.replaceNumPadKeyCodes(target: eventToDeal) ?? eventToDeal + + // Shift+Enter 是個特殊情形,不提前攔截處理的話、會有垃圾參數傳給 delegate 的 inputHandler 從而崩潰。 + // 所以這裡直接將 Shift Flags 清空。 + if event.isShiftHold, event.isEnter { + guard let newEvent = event.reinitiate(modifierFlags: []) else { + IMEApp.buzz() + return true + } + + return imkCandidatesEventSubHandler(event: newEvent) + } + + // 聯想詞選字。 + if let newChar = CtlCandidateIMK.defaultIMKSelectionKey[event.keyCode], + event.isShiftHold, delegate.state.type == .ofAssociates, + let newEvent = event.reinitiate(modifierFlags: [], characters: newChar) + { + if #available(macOS 10.14, *) { + imkCandidates.handleKeyboardEvent(newEvent) + } else { + imkCandidates.interpretKeyEvents([newEvent]) + } + return true + } + + return imkCandidatesEventSubHandler(event: event) + } + return nil + } + + private func imkCandidatesEventSubHandler(event: NSEvent) -> Bool { + guard let delegate = delegate else { return false } + 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 commonEventHandler(event) + } else if event.isSymbolMenuPhysicalKey { + // 符號鍵的行為是固定的,不受偏好設定影響。 + switch imkC.currentLayout { + case .horizontal: _ = event.isShiftHold ? imkC.moveUp(self) : imkC.moveDown(self) + case .vertical: _ = event.isShiftHold ? imkC.moveLeft(self) : imkC.moveRight(self) + @unknown default: break + } + return true + } else if event.isSpace { + switch prefs.specifyShiftSpaceKeyBehavior { + case true: _ = event.isShiftHold ? imkC.highlightNextCandidate() : imkC.showNextPage() + case false: _ = event.isShiftHold ? imkC.showNextPage() : imkC.highlightNextCandidate() + } + return true + } else if event.isTab { + switch prefs.specifyShiftTabKeyBehavior { + case true: _ = event.isShiftHold ? imkC.showPreviousPage() : imkC.showNextPage() + case false: _ = event.isShiftHold ? imkC.highlightPreviousCandidate() : imkC.highlightNextCandidate() + } + return true + } else { + if let newChar = CtlCandidateIMK.defaultIMKSelectionKey[event.keyCode] { + /// 根據 KeyCode 重新換算一下選字鍵的 NSEvent,糾正其 Character 數值。 + /// 反正 IMK 選字窗目前也沒辦法修改選字鍵。 + let newEvent = event.reinitiate(characters: newChar) + if let newEvent = newEvent { + if prefs.useSCPCTypingMode, delegate.state.type == .ofAssociates { + // 註:input.isShiftHold 已經在 Self.handle() 內處理,因為在那邊處理才有效。 + return event.isShiftHold ? true : commonEventHandler(event) + } else { + if #available(macOS 10.14, *) { + imkC.handleKeyboardEvent(newEvent) + } else { + imkC.interpretKeyEvents([newEvent]) + } + return true + } + } + } + + if prefs.useSCPCTypingMode, !event.isReservedKey { + return commonEventHandler(event) + } + + if delegate.state.type == .ofAssociates, + !event.isPageUp, !event.isPageDown, !event.isCursorForward, !event.isCursorBackward, + !event.isCursorClockLeft, !event.isCursorClockRight, !event.isSpace, + !event.isEnter || !prefs.alsoConfirmAssociatedCandidatesByEnter + { + return commonEventHandler(event) + } + imkC.interpretKeyEvents(eventArray) + return true + } + } +} diff --git a/Source/Modules/InputHandler_HandleInput.swift b/Source/Modules/InputHandler_HandleInput.swift index d8fdca92..57871ea1 100644 --- a/Source/Modules/InputHandler_HandleInput.swift +++ b/Source/Modules/InputHandler_HandleInput.swift @@ -16,6 +16,7 @@ import Shared extension InputHandler { /// 對於輸入訊號的第一關處理均藉由此函式來進行。 + /// - Remark: 送入該函式處理之前,先用 inputHandler.handleEvent() 分診、來判斷是否需要交給 IMKCandidates 處理。 /// - Parameters: /// - input: 輸入訊號。 /// - state: 給定狀態(通常為當前狀態)。 @@ -29,7 +30,7 @@ extension InputHandler { errorCallback: @escaping (String) -> Void ) -> Bool { // 如果按鍵訊號內的 inputTest 是空的話,則忽略該按鍵輸入,因為很可能是功能修飾鍵。 - guard !input.text.isEmpty else { return false } + guard !input.text.isEmpty, input.charCode.isPrintable else { return false } let inputText: String = input.text var state = state // 常數轉變數。 diff --git a/Source/Modules/SessionCtl_HandleEvent.swift b/Source/Modules/SessionCtl_HandleEvent.swift index db0219f3..35cd8d94 100644 --- a/Source/Modules/SessionCtl_HandleEvent.swift +++ b/Source/Modules/SessionCtl_HandleEvent.swift @@ -130,145 +130,9 @@ extension SessionCtl { // 準備修飾鍵,用來判定要新增的詞彙是否需要賦以非常低的權重。 Self.areWeNerfing = eventToDeal.modifierFlags.contains([.shift, .command]) - // IMK 選字窗處理,當且僅當啟用了 IMK 選字窗的時候才會生效。 - if let result = imkCandidatesEventPreHandler(event: eventToDeal) { - if shouldUseShiftToggleHandle { rencentKeyHandledByInputHandlerEtc = result } - return result - } - - /// 剩下的 NSEvent 直接交給 commonEventHandler 來處理。 - /// 這樣可以與 IMK 選字窗共用按鍵處理資源,維護起來也比較方便。 - let result = commonEventHandler(eventToDeal) - if shouldUseShiftToggleHandle { - rencentKeyHandledByInputHandlerEtc = result - } + /// 直接交給 commonEventHandler 來處理。 + let result = inputHandler.handleEvent(eventToDeal) + if shouldUseShiftToggleHandle { rencentKeyHandledByInputHandlerEtc = result } return result } } - -// MARK: - Private functions - -extension SessionCtl { - /// 完成 handle() 函式本該完成的內容,但去掉了與 IMK 選字窗有關的判斷語句。 - /// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。 - /// - Parameter event: 由 IMK 選字窗接收的裝置操作輸入事件。 - /// - Returns: 回「`true`」以將該案件已攔截處理的訊息傳遞給 IMK;回「`false`」則放行、不作處理。 - private func commonEventHandler(_ event: NSEvent) -> Bool { - // 無法列印的訊號輸入,一概不作處理。 - // 這個過程不能放在 InputHandler 內,否則不會起作用。 - if !event.charCode.isPrintable { return false } - - /// 將按鍵行為與當前輸入法狀態結合起來、交給按鍵調度模組來處理。 - /// 再根據返回的 result bool 數值來告知 IMK「這個按鍵事件是被處理了還是被放行了」。 - /// 這裡不用 inputHandler.handleCandidate() 是因為需要針對聯想詞輸入狀態做額外處理。 - let result = inputHandler.handleInput(event: event, state: state) { newState in - self.handle(state: newState) - } errorCallback: { errorString in - vCLog(errorString) - IMEApp.buzz() - } - return result - } - - /// 完成 handle() 函式本該完成的內容,但專門處理與 IMK 選字窗有關的判斷語句。 - /// 這樣分開處理很有必要,不然 handle() 函式會陷入無限迴圈。 - /// - Parameter event: 由 IMK 選字窗接收的裝置操作輸入事件。 - /// - Returns: 回「`true`」以將該案件已攔截處理的訊息傳遞給 IMK;回「`false`」則放行、不作處理。 - private func imkCandidatesEventPreHandler(event eventToDeal: NSEvent) -> Bool? { - // IMK 選字窗處理,當且僅當啟用了 IMK 選字窗的時候才會生效。 - // 這樣可以讓 interpretKeyEvents() 函式自行判斷: - // - 是就地交給 imkCandidates.interpretKeyEvents() 處理? - // - 還是藉由 delegate 扔回 SessionCtl 給 InputHandler 處理? - if let imkCandidates = ctlCandidateCurrent as? CtlCandidateIMK, imkCandidates.visible { - let event: NSEvent = CtlCandidateIMK.replaceNumPadKeyCodes(target: eventToDeal) ?? eventToDeal - - // Shift+Enter 是個特殊情形,不提前攔截處理的話、會有垃圾參數傳給 delegate 的 inputHandler 從而崩潰。 - // 所以這裡直接將 Shift Flags 清空。 - if event.isShiftHold, event.isEnter { - guard let newEvent = event.reinitiate(modifierFlags: []) else { - NSSound.beep() - return true - } - - return imkCandidatesEventSubHandler(event: newEvent) - } - - // 聯想詞選字。 - if let newChar = CtlCandidateIMK.defaultIMKSelectionKey[event.keyCode], - event.isShiftHold, state.type == .ofAssociates, - let newEvent = event.reinitiate(modifierFlags: [], characters: newChar) - { - if #available(macOS 10.14, *) { - imkCandidates.handleKeyboardEvent(newEvent) - } else { - imkCandidates.interpretKeyEvents([newEvent]) - } - return true - } - - return imkCandidatesEventSubHandler(event: event) - } - return nil - } - - private func imkCandidatesEventSubHandler(event: NSEvent) -> Bool { - let eventArray = [event] - guard let imkC = ctlCandidateCurrent as? CtlCandidateIMK else { return false } - if event.isEsc || event.isBackSpace || event.isDelete || (event.isShiftHold && !event.isSpace) { - return commonEventHandler(event) - } else if event.isSymbolMenuPhysicalKey { - // 符號鍵的行為是固定的,不受偏好設定影響。 - switch imkC.currentLayout { - case .horizontal: _ = event.isShiftHold ? imkC.moveUp(self) : imkC.moveDown(self) - case .vertical: _ = event.isShiftHold ? imkC.moveLeft(self) : imkC.moveRight(self) - @unknown default: break - } - return true - } else if event.isSpace { - switch PrefMgr.shared.specifyShiftSpaceKeyBehavior { - case true: _ = event.isShiftHold ? imkC.highlightNextCandidate() : imkC.showNextPage() - case false: _ = event.isShiftHold ? imkC.showNextPage() : imkC.highlightNextCandidate() - } - return true - } else if event.isTab { - switch PrefMgr.shared.specifyShiftTabKeyBehavior { - case true: _ = event.isShiftHold ? imkC.showPreviousPage() : imkC.showNextPage() - case false: _ = event.isShiftHold ? imkC.highlightPreviousCandidate() : imkC.highlightNextCandidate() - } - return true - } else { - if let newChar = CtlCandidateIMK.defaultIMKSelectionKey[event.keyCode] { - /// 根據 KeyCode 重新換算一下選字鍵的 NSEvent,糾正其 Character 數值。 - /// 反正 IMK 選字窗目前也沒辦法修改選字鍵。 - let newEvent = event.reinitiate(characters: newChar) - if let newEvent = newEvent { - if PrefMgr.shared.useSCPCTypingMode, state.type == .ofAssociates { - // 註:input.isShiftHold 已經在 Self.handle() 內處理,因為在那邊處理才有效。 - return event.isShiftHold ? true : commonEventHandler(event) - } else { - if #available(macOS 10.14, *) { - imkC.handleKeyboardEvent(newEvent) - } else { - imkC.interpretKeyEvents([newEvent]) - } - return true - } - } - } - - if PrefMgr.shared.useSCPCTypingMode, !event.isReservedKey { - return commonEventHandler(event) - } - - if state.type == .ofAssociates, - !event.isPageUp, !event.isPageDown, !event.isCursorForward, !event.isCursorBackward, - !event.isCursorClockLeft, !event.isCursorClockRight, !event.isSpace, - !event.isEnter || !PrefMgr.shared.alsoConfirmAssociatedCandidatesByEnter - { - return commonEventHandler(event) - } - imkC.interpretKeyEvents(eventArray) - return true - } - } -} diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index a822e552..f5fdf445 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -84,6 +84,7 @@ 5BDB7A4528D4824A001AC277 /* ShiftKeyUpChecker in Frameworks */ = {isa = PBXBuildFile; productRef = 5BDB7A4428D4824A001AC277 /* ShiftKeyUpChecker */; }; 5BDB7A4728D4824A001AC277 /* Tekkon in Frameworks */ = {isa = PBXBuildFile; productRef = 5BDB7A4628D4824A001AC277 /* Tekkon */; }; 5BDCBB2E27B4E67A00D0CC59 /* vChewingPhraseEditor.app in Resources */ = {isa = PBXBuildFile; fileRef = 5BD05BB827B2A429004C4F1D /* vChewingPhraseEditor.app */; }; + 5BE1F8A928F86AB5006C7FF5 /* InputHandler_HandleEvent.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE1F8A828F86AB5006C7FF5 /* InputHandler_HandleEvent.swift */; }; 5BE377A0288FED8D0037365B /* InputHandler_HandleComposition.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE3779F288FED8D0037365B /* InputHandler_HandleComposition.swift */; }; 5BE78BD927B3775B005EA1BE /* CtlAboutWindow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5BE78BD827B37750005EA1BE /* CtlAboutWindow.swift */; }; 5BE78BDD27B3776D005EA1BE /* frmAboutWindow.xib in Resources */ = {isa = PBXBuildFile; fileRef = 5BE78BDA27B37764005EA1BE /* frmAboutWindow.xib */; }; @@ -283,6 +284,7 @@ 5BDCBB4A27B4F6C700D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/Localizable.strings"; sourceTree = ""; }; 5BDCBB4B27B4F6C700D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/frmAboutWindow.strings"; sourceTree = ""; }; 5BDCBB4D27B4F6C700D0CC59 /* zh-Hant */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hant"; path = "zh-Hant.lproj/InfoPlist.strings"; sourceTree = ""; }; + 5BE1F8A828F86AB5006C7FF5 /* InputHandler_HandleEvent.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputHandler_HandleEvent.swift; sourceTree = ""; }; 5BE3779F288FED8D0037365B /* InputHandler_HandleComposition.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputHandler_HandleComposition.swift; sourceTree = ""; }; 5BE78BD827B37750005EA1BE /* CtlAboutWindow.swift */ = {isa = PBXFileReference; explicitFileType = sourcecode.swift; fileEncoding = 4; indentWidth = 2; lineEnding = 0; path = CtlAboutWindow.swift; sourceTree = ""; tabWidth = 2; usesTabs = 0; }; 5BE78BDB27B37764005EA1BE /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/frmAboutWindow.xib; sourceTree = ""; }; @@ -675,6 +677,7 @@ 5BD0113C2818543900609769 /* InputHandler_Core.swift */, 5B782EC3280C243C007276DE /* InputHandler_HandleCandidate.swift */, 5BE3779F288FED8D0037365B /* InputHandler_HandleComposition.swift */, + 5BE1F8A828F86AB5006C7FF5 /* InputHandler_HandleEvent.swift */, 5B7F225C2808501000DDD3CB /* InputHandler_HandleInput.swift */, 5B3133BE280B229700A4A505 /* InputHandler_States.swift */, 5BAEFACF28012565001F42C9 /* LMMgr.swift */, @@ -1081,6 +1084,7 @@ 5BD0113D2818543900609769 /* InputHandler_Core.swift in Sources */, 5BF56F9A28C39D1800DD6839 /* IMEStateData.swift in Sources */, 5B21176C287539BB000443A9 /* SessionCtl_HandleStates.swift in Sources */, + 5BE1F8A928F86AB5006C7FF5 /* InputHandler_HandleEvent.swift in Sources */, 5BAEFAD028012565001F42C9 /* LMMgr.swift in Sources */, 5B782EC4280C243C007276DE /* InputHandler_HandleCandidate.swift in Sources */, 5BA9FD0F27FEDB6B002DE248 /* VwrPrefPaneGeneral.swift in Sources */,