From 82a916c433e3d2af57a0696a9226fdaa2f76d80c Mon Sep 17 00:00:00 2001 From: zonble Date: Mon, 24 Jan 2022 17:46:34 +0800 Subject: [PATCH] Adds a new file KeyHandler.swift to start to break input controller to testable modules. --- McBopomofo.xcodeproj/project.pbxproj | 4 + Source/InputMethodController.mm | 5 +- Source/KeyHandler.swift | 200 +++++++++++++++++++++++++++ 3 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 Source/KeyHandler.swift diff --git a/McBopomofo.xcodeproj/project.pbxproj b/McBopomofo.xcodeproj/project.pbxproj index fa5ebe69..62ebcd81 100644 --- a/McBopomofo.xcodeproj/project.pbxproj +++ b/McBopomofo.xcodeproj/project.pbxproj @@ -52,6 +52,7 @@ D44FB74727919D35003C80A6 /* EmacsKeyHelper.swift in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74627919C83003C80A6 /* EmacsKeyHelper.swift */; }; D44FB74A2791B829003C80A6 /* VXHanConvert in Frameworks */ = {isa = PBXBuildFile; productRef = D44FB7492791B829003C80A6 /* VXHanConvert */; }; D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */ = {isa = PBXBuildFile; fileRef = D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */; }; + D456576E279E4F7B00DF6BC9 /* KeyHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = D456576D279E4F7B00DF6BC9 /* KeyHandler.swift */; }; D461B792279DAC010070E734 /* InputState.swift in Sources */ = {isa = PBXBuildFile; fileRef = D461B791279DAC010070E734 /* InputState.swift */; }; D47B92C027972AD100458394 /* main.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47B92BF27972AC800458394 /* main.swift */; }; D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */ = {isa = PBXBuildFile; fileRef = D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */; }; @@ -205,6 +206,7 @@ D44FB7482791B346003C80A6 /* VXHanConvert */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = VXHanConvert; path = Packages/VXHanConvert; sourceTree = ""; }; D44FB74B2792189A003C80A6 /* PhraseReplacementMap.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = PhraseReplacementMap.cpp; sourceTree = ""; }; D44FB74C2792189A003C80A6 /* PhraseReplacementMap.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhraseReplacementMap.h; sourceTree = ""; }; + D456576D279E4F7B00DF6BC9 /* KeyHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeyHandler.swift; sourceTree = ""; }; D461B791279DAC010070E734 /* InputState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InputState.swift; sourceTree = ""; }; D47B92BF27972AC800458394 /* main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = main.swift; sourceTree = ""; }; D47F7DCD278BFB57002F9DD7 /* PreferencesWindowController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PreferencesWindowController.swift; sourceTree = ""; }; @@ -302,6 +304,7 @@ D41355D6278D7409005E5CBD /* LanguageModelManager.h */, D41355D7278D7409005E5CBD /* LanguageModelManager.mm */, D461B791279DAC010070E734 /* InputState.swift */, + D456576D279E4F7B00DF6BC9 /* KeyHandler.swift */, D47B92BF27972AC800458394 /* main.swift */, D427F76B278CA1BA004A2160 /* AppDelegate.swift */, D44FB74427915555003C80A6 /* Preferences.swift */, @@ -705,6 +708,7 @@ D44FB74D2792189A003C80A6 /* PhraseReplacementMap.cpp in Sources */, D44FB74527915565003C80A6 /* Preferences.swift in Sources */, D47F7DD0278C0897002F9DD7 /* NonModalAlertWindowController.swift in Sources */, + D456576E279E4F7B00DF6BC9 /* KeyHandler.swift in Sources */, D47F7DCE278BFB57002F9DD7 /* PreferencesWindowController.swift in Sources */, 6A0D4ED215FC0D6400ABF4B3 /* InputMethodController.mm in Sources */, D41355DB278E6D17005E5CBD /* McBopomofoLM.cpp in Sources */, diff --git a/Source/InputMethodController.mm b/Source/InputMethodController.mm index 21cdb15b..ee830be2 100644 --- a/Source/InputMethodController.mm +++ b/Source/InputMethodController.mm @@ -631,7 +631,7 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } && (flags & NSEventModifierFlagShift)) { NSUInteger index = currentState.markerIndex; if (index < currentState.composingBuffer.length) { - index -= 1; + index += 1; InputStateMarking *state = [[InputStateMarking alloc] initWithComposingBuffer:currentState.composingBuffer cursorIndex:currentState.cursorIndex markerIndex:index]; [self handleState:state client:client]; } @@ -653,7 +653,8 @@ NS_INLINE size_t max(size_t a, size_t b) { return a > b ? a : b; } // update the composing buffer composeReading = _bpmfReadingBuffer->hasToneMarker(); if (!composeReading) { - [self handleState:_state client:client]; + InputStateInputting *inputting = [self buildInputingState]; + [self handleState:inputting client:client]; return YES; } } diff --git a/Source/KeyHandler.swift b/Source/KeyHandler.swift new file mode 100644 index 00000000..b15818ed --- /dev/null +++ b/Source/KeyHandler.swift @@ -0,0 +1,200 @@ +import Cocoa + +@objc enum KeyCode: UInt16 { + case none = 0 + case enter = 76 + case up = 126 + case down = 125 + case left = 123 + case right = 124 + case pageUp = 116 + case pageDown = 121 + case home = 115 + case end = 119 + case delete = 117 +} + +class KeyHandlerInput: NSObject { + private (set) var event: NSEvent + private (set) var useVerticalMode: Bool + + var inputText: String? { + event.characters + } + + var charCode: UInt16 { + guard let inputText = inputText, inputText.count > 0 else { + return 0 + } + let first = inputText[inputText.startIndex].utf16.first! + return first + } + + var keyCode: UInt16 { + event.keyCode + } + + var flags: NSEvent.ModifierFlags { + event.modifierFlags + } + + var cursorForwardKey: KeyCode { + useVerticalMode ? .down : .right + } + + var cursorBackwardKey: KeyCode { + useVerticalMode ? .up : .left + } + + var extraChooseCandidateKey: KeyCode { + useVerticalMode ? .left : .down + } + + var absorbedArrowKey: KeyCode { + useVerticalMode ? .right : .up + } + + var verticalModeOnlyChooseCandidateKey: KeyCode { + useVerticalMode ? absorbedArrowKey : .none + } + + init(event: NSEvent, isVerticalMode: Bool) { + self.event = event + self.useVerticalMode = isVerticalMode + } +} + +typealias KeyHandlerStateCallback = (InputState) -> () +typealias KeyHandlerErrorCallback = () -> () + +@objc protocol KeyHandlerDelegate: AnyObject { + func keyHandlerRequestCurrentInputtingState(_ handler: KeyHandler) -> InputStateInputting + func keyHandler(_ handler: KeyHandler, requestWriteUserPhrase state: InputStateMarking ) -> Bool + + func keyHandler(_ handler: KeyHandler, isCharCodeValidBmpfReading charCode:UInt16 ) -> Bool + func keyHandler(_ handler: KeyHandler, insertCharCodeToBmpfReading charCode:UInt16 ) -> Void + +} + + +class KeyHandler: NSObject { + var delegate: KeyHandlerDelegate? + + @objc func handle(_ input: KeyHandlerInput, + currentState: InputState, + stateCallback: @escaping KeyHandlerStateCallback, + errorCallback: @escaping KeyHandlerErrorCallback + ) -> Bool { + guard let delegate = delegate else { + return false + } + + // if the inputText is empty, it's a function key combination, we ignore it + guard let inputText = input.inputText else { + return false + } + if inputText.isEmpty { + return false + } + + let flags = input.flags + let charCode = input.charCode + let keyCode = input.keyCode + let emacsKey = EmacsKeyHelper.detect(charCode: charCode, flags: flags) + + // if the composing buffer is empty and there's no reading, and there is some function key combination, we ignore it + let isFunctionKey = flags.contains(.command) || flags.contains(.control) || flags.contains(.option) || flags.contains(.numericPad) + if currentState is InputStateInputting == false && isFunctionKey { + return false + } + + // Caps Lock processing : if Caps Lock is on, temporarily disable bopomofo. + if charCode == 8 || + charCode == 13 || + keyCode == input.absorbedArrowKey.rawValue || + keyCode == input.cursorForwardKey.rawValue || + keyCode == input.cursorBackwardKey.rawValue { + // do nothing if backspace is pressed -- we ignore the key + } else if flags.contains(.capsLock) { + // process all possible combination, we hope. + stateCallback(InputStateEmpty()) + + // first commit everything in the buffer. + if flags.contains(.shift) { + return false + } + + // if ASCII but not printable, don't use insertText:replacementRange: as many apps don't handle non-ASCII char insertions. + if charCode < 0x80 && isprint(Int32(charCode)) == 0 { + return false + } + stateCallback(InputStateCommitting(poppedText: inputText.lowercased())) + stateCallback(InputStateEmpty()) + return true + } + + if flags.contains(.numericPad) { + if keyCode != KeyCode.left.rawValue && + keyCode != KeyCode.right.rawValue && + keyCode != KeyCode.down.rawValue && + keyCode != KeyCode.up.rawValue && + charCode != 32 && + isprint(Int32(charCode)) != 0 { + stateCallback(InputStateEmpty()) + stateCallback(InputStateCommitting(poppedText: inputText.lowercased())) + stateCallback(InputStateEmpty()) + return true + } + } + + // candidates? + + if let state = currentState as? InputStateMarking { + // ESC + if charCode == 27 { + let inputting = InputStateInputting(composingBuffer: state.composingBuffer, cursorIndex: state.cursorIndex) + stateCallback(inputting) + return true + } + // Enter + if charCode == 13 { + if delegate.keyHandler(self, requestWriteUserPhrase: state) == false { + errorCallback() + return true + } + let inputting = InputStateInputting(composingBuffer: state.composingBuffer, cursorIndex: state.cursorIndex) + stateCallback(inputting) + return true + } + // Shift + left + if (keyCode == input.cursorBackwardKey.rawValue || emacsKey == .backward) && flags.contains(.shift) { + var index = state.markerIndex + if index > 0 { + index -= 1 + let marking = InputStateMarking(composingBuffer: state.composingBuffer, cursorIndex: state.cursorIndex, markerIndex: state.markerIndex) + stateCallback(marking) + } else { + errorCallback() + } + return true + } + if (keyCode == input.cursorForwardKey.rawValue || emacsKey == .forward) && flags.contains(.shift) { + var index = state.markerIndex + if index < state.composingBuffer.count { + index += 1 + let marking = InputStateMarking(composingBuffer: state.composingBuffer, cursorIndex: state.cursorIndex, markerIndex: state.markerIndex) + stateCallback(marking) + } else { + errorCallback() + } + return true + } + } + + + + + return false + } + +}