diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift index caec6545..b2e3125a 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleCandidate.swift @@ -112,7 +112,7 @@ extension KeyHandler { // MARK: PgDn - if input.isPageDown || input.emacsKey == EmacsKey.nextPage { + if input.isPageDown { let updated: Bool = ctlCandidateCurrent.showNextPage() if !updated { IME.prtDebugIntel("9B691919") @@ -150,17 +150,6 @@ extension KeyHandler { return true } - // MARK: EmacsKey Backward - - if input.emacsKey == EmacsKey.backward { - let updated: Bool = ctlCandidateCurrent.highlightPreviousCandidate() - if !updated { - IME.prtDebugIntel("9B89308D") - errorCallback() - } - return true - } - // MARK: Right Arrow if input.isRight { @@ -179,17 +168,6 @@ extension KeyHandler { return true } - // MARK: EmacsKey Forward - - if input.emacsKey == EmacsKey.forward { - let updated: Bool = ctlCandidateCurrent.highlightNextCandidate() - if !updated { - IME.prtDebugIntel("9B2428D") - errorCallback() - } - return true - } - // MARK: Up Arrow if input.isUp { @@ -228,7 +206,7 @@ extension KeyHandler { // MARK: Home Key - if input.isHome || input.emacsKey == EmacsKey.home { + if input.isHome { if ctlCandidateCurrent.selectedCandidateIndex == 0 { IME.prtDebugIntel("9B6EDE8D") errorCallback() @@ -244,7 +222,7 @@ extension KeyHandler { if state.candidates.isEmpty { return false } else { // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。 - if input.isEnd || input.emacsKey == EmacsKey.end { + if input.isEnd { if ctlCandidateCurrent.selectedCandidateIndex == state.candidates.count - 1 { IME.prtDebugIntel("9B69AAAD") errorCallback() diff --git a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift index d708993c..91d4173f 100644 --- a/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift +++ b/Source/Modules/ControllerModules/KeyHandler_HandleInput.swift @@ -71,7 +71,9 @@ extension KeyHandler { stateCallback(IMEState.ofEmpty()) // 字母鍵摁 Shift 的話,無須額外處理,因為直接就會敲出大寫字母。 - if input.isUpperCaseASCIILetterKey { + if (input.isUpperCaseASCIILetterKey && input.isASCIIModeInput) + || (input.isCapsLockOn && input.isShiftHold) + { return false } @@ -195,7 +197,7 @@ extension KeyHandler { // MARK: Cursor backward - if input.isCursorBackward || input.emacsKey == EmacsKey.backward { + if input.isCursorBackward { return handleBackward( state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) @@ -203,7 +205,7 @@ extension KeyHandler { // MARK: Cursor forward - if input.isCursorForward || input.emacsKey == EmacsKey.forward { + if input.isCursorForward { return handleForward( state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback ) @@ -211,13 +213,13 @@ extension KeyHandler { // MARK: Home - if input.isHome || input.emacsKey == EmacsKey.home { + if input.isHome { return handleHome(state: state, stateCallback: stateCallback, errorCallback: errorCallback) } // MARK: End - if input.isEnd || input.emacsKey == EmacsKey.end { + if input.isEnd { return handleEnd(state: state, stateCallback: stateCallback, errorCallback: errorCallback) } @@ -259,7 +261,7 @@ extension KeyHandler { // MARK: Delete - if input.isDelete || input.emacsKey == EmacsKey.delete { + if input.isDelete { return handleDelete(state: state, input: input, stateCallback: stateCallback, errorCallback: errorCallback) } diff --git a/Source/Modules/ControllerModules/KeyHandler_States.swift b/Source/Modules/ControllerModules/KeyHandler_States.swift index 8ee851db..e5153a9d 100644 --- a/Source/Modules/ControllerModules/KeyHandler_States.swift +++ b/Source/Modules/ControllerModules/KeyHandler_States.swift @@ -190,7 +190,7 @@ extension KeyHandler { } // Shift + Left - if input.isCursorBackward || input.emacsKey == EmacsKey.backward, input.isShiftHold { + if input.isCursorBackward, input.isShiftHold { if compositor.marker > 0 { compositor.marker -= 1 if isCursorCuttingChar(isMarker: true) { @@ -213,7 +213,7 @@ extension KeyHandler { } // Shift + Right - if input.isCursorForward || input.emacsKey == EmacsKey.forward, input.isShiftHold { + if input.isCursorForward, input.isShiftHold { if compositor.marker < compositor.width { compositor.marker += 1 if isCursorCuttingChar(isMarker: true) { diff --git a/Source/Modules/ControllerModules/NSEventExtension.swift b/Source/Modules/ControllerModules/NSEventExtension.swift index 1bd242a9..98e232f9 100644 --- a/Source/Modules/ControllerModules/NSEventExtension.swift +++ b/Source/Modules/ControllerModules/NSEventExtension.swift @@ -35,6 +35,38 @@ extension NSEvent { keyCode: keyCode ?? self.keyCode ) } + + /// 自 Emacs 熱鍵的 NSEvent 翻譯回標準 NSEvent。失敗的話則會返回原始 NSEvent 自身。 + /// - Parameter isVerticalTyping: 是否按照縱排來操作。 + /// - Returns: 翻譯結果。失敗的話則返回翻譯原文。 + public func convertFromEmacKeyEvent(isVerticalContext: Bool) -> NSEvent { + guard isEmacsKey else { return self } + let newKeyCode: UInt16 = { + switch isVerticalContext { + case false: return IME.vChewingEmacsKey.charKeyMapHorizontal[charCode] ?? 0 + case true: return IME.vChewingEmacsKey.charKeyMapVertical[charCode] ?? 0 + } + }() + guard newKeyCode != 0 else { return self } + let newCharScalar: Unicode.Scalar = { + switch charCode { + case 6: + return isVerticalContext + ? NSEvent.SpecialKey.downArrow.unicodeScalar : NSEvent.SpecialKey.rightArrow.unicodeScalar + case 2: + return isVerticalContext + ? NSEvent.SpecialKey.upArrow.unicodeScalar : NSEvent.SpecialKey.leftArrow.unicodeScalar + case 1: return NSEvent.SpecialKey.home.unicodeScalar + case 5: return NSEvent.SpecialKey.end.unicodeScalar + case 4: return NSEvent.SpecialKey.deleteForward.unicodeScalar // Use "deleteForward" for PC delete. + case 22: return NSEvent.SpecialKey.pageDown.unicodeScalar + default: return .init(0) + } + }() + let newChar = String(newCharScalar) + return reinitiate(modifierFlags: [], characters: newChar, charactersIgnoringModifiers: newChar, keyCode: newKeyCode) + ?? self + } } // MARK: - NSEvent Extension - InputSignalProtocol @@ -58,8 +90,9 @@ extension NSEvent: InputSignalProtocol { public var isFlagChanged: Bool { type == .flagsChanged } - public var emacsKey: EmacsKey { - NSEvent.detectEmacsKey(charCode: charCode, flags: modifierFlags) + public var isEmacsKey: Bool { + // 這裡不能只用 isControlHold,因為這裡對修飾鍵的要求有排他性。 + [6, 2, 1, 5, 4, 22].contains(charCode) && modifierFlags == .control } // 摁 Alt+Shift+主鍵盤區域數字鍵 的話,根據不同的 macOS 鍵盤佈局種類,會出現不同的符號結果。 @@ -150,13 +183,6 @@ extension NSEvent: InputSignalProtocol { public var isSymbolMenuPhysicalKey: Bool { [KeyCode.kSymbolMenuPhysicalKeyIntl, KeyCode.kSymbolMenuPhysicalKeyJIS].contains(KeyCode(rawValue: keyCode)) } - - static func detectEmacsKey(charCode: UniChar, flags: NSEvent.ModifierFlags) -> EmacsKey { - if flags.contains(.control) { - return EmacsKey(rawValue: charCode) ?? .none - } - return .none - } } // MARK: - InputSignalProtocol @@ -169,7 +195,6 @@ public protocol InputSignalProtocol { var charCode: UInt16 { get } var keyCode: UInt16 { get } var isFlagChanged: Bool { get } - var emacsKey: EmacsKey { get } var mainAreaNumKeyChar: String? { get } var isASCII: Bool { get } var isInvalid: Bool { get } @@ -319,13 +344,3 @@ enum CharCode: UInt16 { // KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts ... // ... but only focuses on which physical key is pressed. } - -public enum EmacsKey: UInt16 { - case none = 0 - case forward = 6 // F - case backward = 2 // B - case home = 1 // A - case end = 5 // E - case delete = 4 // D - case nextPage = 22 // V -} diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Common.swift b/Source/Modules/ControllerModules/ctlInputMethod_Common.swift index 2ec05c81..85db9525 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Common.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Common.swift @@ -46,6 +46,15 @@ extension ctlInputMethod { /// 沒有文字輸入客體的話,就不要再往下處理了。 guard client() != nil else { return false } + var event = event + // 使 NSEvent 自翻譯,這樣可以讓 Emacs NSEvent 變成標準 NSEvent。 + if event.isEmacsKey { + let verticalProcessing = + (state.isCandidateContainer) + ? ctlInputMethod.isVerticalCandidateSituation : ctlInputMethod.isVerticalTypingSituation + event = event.convertFromEmacKeyEvent(isVerticalContext: verticalProcessing) + } + /// 這裡仍舊需要判斷 flags。之前使輸入法狀態卡住無法敲漢字的問題已在 KeyHandler 內修復。 /// 這裡不判斷 flags 的話,用方向鍵前後定位光標之後,再次試圖觸發組字區時、反而會在首次按鍵時失敗。 /// 同時注意:必須在 event.type == .flagsChanged 結尾插入 return false, diff --git a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift index d8c90ca0..5d47caeb 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_Core.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_Core.swift @@ -36,6 +36,8 @@ class ctlInputMethod: IMKInputController { static var isASCIIModeSituation: Bool = false /// 當前這個 ctlInputMethod 副本是否處於縱排輸入模式(滯後項)。 static var isVerticalTypingSituation: Bool = false + /// 當前這個 ctlInputMethod 副本是否處於縱排選字窗模式(滯後項)。 + static var isVerticalCandidateSituation: Bool = false /// 當前這個 ctlInputMethod 副本是否處於英數輸入模式。 var isASCIIMode: Bool = false /// 按鍵調度模組的副本。 @@ -221,7 +223,11 @@ class ctlInputMethod: IMKInputController { // - 還是藉由 delegate 扔回 ctlInputMethod 給 KeyHandler 處理? proc: if let ctlCandidateCurrent = ctlInputMethod.ctlCandidateCurrent as? ctlCandidateIMK { guard ctlCandidateCurrent.visible else { break proc } - let event: NSEvent = ctlCandidateIMK.replaceNumPadKeyCodes(target: event) ?? event + var event: NSEvent = ctlCandidateIMK.replaceNumPadKeyCodes(target: event) ?? event + // 使 NSEvent 自翻譯,這樣可以讓 Emacs NSEvent 變成標準 NSEvent。 + if event.isEmacsKey { + event = event.convertFromEmacKeyEvent(isVerticalContext: ctlInputMethod.isVerticalCandidateSituation) + } // Shift+Enter 是個特殊情形,不提前攔截處理的話、會有垃圾參數傳給 delegate 的 keyHandler 從而崩潰。 // 所以這裡直接將 Shift Flags 清空。 @@ -247,6 +253,7 @@ class ctlInputMethod: IMKInputController { /// 我們不在這裡處理了,直接交給 commonEventHandler 來處理。 /// 這樣可以與 IMK 選字窗共用按鍵處理資源,維護起來也比較方便。 + /// 警告:這裡的 event 必須是原始 event 且不能被 var,否則會影響 Shift 中英模式判定。 return commonEventHandler(event) } diff --git a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift index 9dd74dac..8e6e9520 100644 --- a/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift +++ b/Source/Modules/ControllerModules/ctlInputMethod_HandleDisplay.swift @@ -58,6 +58,8 @@ extension ctlInputMethod { // 上面這句如果是 true 的話,就會是縱排;反之則為橫排。 } + ctlInputMethod.isVerticalCandidateSituation = (isCandidateWindowVertical || !mgrPrefs.useHorizontalCandidateList) + ctlInputMethod.ctlCandidateCurrent.delegate = nil /// 下面這一段本可直接指定 currentLayout,但這樣的話翻頁按鈕位置無法精準地重新繪製。 diff --git a/Source/Modules/IMEModules/IME.swift b/Source/Modules/IMEModules/IME.swift index 9d800519..900a9456 100644 --- a/Source/Modules/IMEModules/IME.swift +++ b/Source/Modules/IMEModules/IME.swift @@ -26,6 +26,13 @@ public enum IME { fileURLWithFileSystemRepresentation: getpwuid(getuid()).pointee.pw_dir, isDirectory: true, relativeTo: nil ) + // MARK: - vChewing Emacs CharCode-KeyCode translation tables. + + public enum vChewingEmacsKey { + static let charKeyMapHorizontal: [UInt16: UInt16] = [6: 124, 2: 123, 1: 115, 5: 119, 4: 117, 22: 121] + static let charKeyMapVertical: [UInt16: UInt16] = [6: 125, 2: 126, 1: 115, 5: 119, 4: 117, 22: 121] + } + // MARK: - 瀏覽器 Bundle Identifier 關鍵詞匹配黑名單 /// 瀏覽器 Bundle Identifier 關鍵詞匹配黑名單,匹配到的瀏覽器會做出特殊的 Shift 鍵擊劍判定處理。 diff --git a/Update-Info.plist b/Update-Info.plist index ed43c90e..ffc5d819 100644 --- a/Update-Info.plist +++ b/Update-Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 2.4.0 CFBundleVersion - 2400 + 2402 UpdateInfoEndpoint https://gitee.com/vchewing/vChewing-macOS/raw/main/Update-Info.plist UpdateInfoSite diff --git a/vChewing.xcodeproj/project.pbxproj b/vChewing.xcodeproj/project.pbxproj index baf22521..96390b2f 100644 --- a/vChewing.xcodeproj/project.pbxproj +++ b/vChewing.xcodeproj/project.pbxproj @@ -1455,7 +1455,7 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_PREPROCESSOR_DEFINITIONS = ( @@ -1494,7 +1494,7 @@ CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CODE_SIGN_STYLE = Automatic; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; ENABLE_NS_ASSERTIONS = NO; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; @@ -1532,7 +1532,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; DEAD_CODE_STRIPPING = YES; ENABLE_HARDENED_RUNTIME = YES; GCC_C_LANGUAGE_STANDARD = gnu11; @@ -1584,7 +1584,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; DEAD_CODE_STRIPPING = YES; ENABLE_HARDENED_RUNTIME = YES; ENABLE_NS_ASSERTIONS = NO; @@ -1718,7 +1718,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; @@ -1777,7 +1777,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = ""; DEVELOPMENT_TEAM = ""; @@ -1824,7 +1824,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -1868,7 +1868,7 @@ CODE_SIGN_IDENTITY = "-"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 2400; + CURRENT_PROJECT_VERSION = 2402; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES;