diff --git a/Packages/vChewing_CocoaExtension/Package.swift b/Packages/vChewing_CocoaExtension/Package.swift index 6a746eb0..64bf2fa8 100644 --- a/Packages/vChewing_CocoaExtension/Package.swift +++ b/Packages/vChewing_CocoaExtension/Package.swift @@ -13,14 +13,12 @@ let package = Package( ), ], dependencies: [ - .package(path: "../vChewing_IMKUtils"), .package(path: "../vChewing_SwiftExtension"), ], targets: [ .target( name: "CocoaExtension", dependencies: [ - .product(name: "IMKUtils", package: "vChewing_IMKUtils"), .product(name: "SwiftExtension", package: "vChewing_SwiftExtension"), ] ), diff --git a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift index 446ef6fe..8e10fa81 100644 --- a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift +++ b/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSWindowController.swift @@ -7,7 +7,6 @@ // requirements defined in MIT License. import AppKit -import InputMethodKit public extension NSWindowController { func orderFront() { diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/InputHandler_HandleStates.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/InputHandler_HandleStates.swift index 7703d85a..05b8bc2c 100644 --- a/Packages/vChewing_MainAssembly/Sources/MainAssembly/InputHandler_HandleStates.swift +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/InputHandler_HandleStates.swift @@ -1076,7 +1076,7 @@ extension InputHandler { // 另外,這裡不要用「!input.isFunctionKeyHold」, // 否則會導致對上下左右鍵與翻頁鍵的判斷失效。 let notEmpty = state.hasComposition && !compositor.isEmpty && isComposerOrCalligrapherEmpty - let bannedModifiers: NSEvent.ModifierFlags = [.option, .shift, .command, .control] + let bannedModifiers: KBEvent.ModifierFlags = [.option, .shift, .command, .control] let noBannedModifiers = bannedModifiers.intersection(input.keyModifierFlags).isEmpty var triggered = input.isCursorClockLeft || input.isCursorClockRight triggered = triggered || (input.isSpace && prefs.chooseCandidateUsingSpace) diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift index a324a07c..e7a97646 100644 --- a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_Core.swift @@ -32,7 +32,7 @@ public class SessionCtl: IMKInputController { public static var areWeNerfing = false /// 上一個被處理過的鍵盤事件。 - public var previouslyHandledEvents: [NSEvent] = .init() + public var previouslyHandledEvents = [KBEvent]() /// 目前在用的的選字窗副本。 public var candidateUI: CtlCandidateProtocol? diff --git a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleEvent.swift b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleEvent.swift index cf7ee494..5e9222c7 100644 --- a/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleEvent.swift +++ b/Packages/vChewing_MainAssembly/Sources/MainAssembly/SessionCtl_HandleEvent.swift @@ -40,20 +40,19 @@ public extension SessionCtl { return true } - var result = false - if [.keyDown, .flagsChanged].contains(event.type) { - result = handleKeyDown(event: event) - if result, event.type == .keyDown { - previouslyHandledEvents.append(event) - } - } else if event.type == .keyUp { - result = handleKeyUp(event: event) - } + guard let newEvent = event.copyAsKBEvent else { return false } - return result + switch newEvent.type { + case .flagsChanged: return handleKeyDown(event: newEvent) + case .keyDown: + let result = handleKeyDown(event: newEvent) + if result { previouslyHandledEvents.append(newEvent) } + return result + case .keyUp: return handleKeyUp(event: newEvent) + } } - private func handleKeyUp(event: NSEvent) -> Bool { + private func handleKeyUp(event: KBEvent) -> Bool { guard ![.ofEmpty, .ofAbortion].contains(state.type) else { return false } let codes = previouslyHandledEvents.map(\.keyCode) if codes.contains(event.keyCode) { @@ -65,7 +64,7 @@ public extension SessionCtl { return false } - private func handleKeyDown(event: NSEvent) -> Bool { + private func handleKeyDown(event: KBEvent) -> Bool { // MARK: 前置處理 // 先放過一些以 .command 觸發的熱鍵(包括剪貼簿熱鍵)。 @@ -131,7 +130,7 @@ public extension SessionCtl { // 如果是方向鍵輸入的話,就想辦法帶上標記資訊、來說明當前是縱排還是橫排。 if event.isUp || event.isDown || event.isLeft || event.isRight { updateVerticalTypingStatus() // 檢查當前環境是否是縱排輸入。 - eventToDeal = event.reinitiate(charactersIgnoringModifiers: isVerticalTyping ? "Vertical" : "Horizontal") ?? event + eventToDeal = event.reinitiate(charactersIgnoringModifiers: isVerticalTyping ? "Vertical" : "Horizontal") } // 使 NSEvent 自翻譯,這樣可以讓 Emacs NSEvent 變成標準 NSEvent。 @@ -158,12 +157,12 @@ public extension SessionCtl { if eventToDeal.isNumericPadKey, let eventCharConverted = eventToDeal.characters?.applyingTransformFW2HW(reverse: false) { - eventToDeal = eventToDeal.reinitiate(characters: eventCharConverted) ?? eventToDeal + eventToDeal = eventToDeal.reinitiate(characters: eventCharConverted) } else if [.ofEmpty, .ofInputting].contains(state.type), eventToDeal.isMainAreaNumKey, !eventToDeal.isCommandHold, !eventToDeal.isControlHold, eventToDeal.isOptionHold { // Alt(+Shift)+主鍵盤區數字鍵 預先處理 - eventToDeal = eventToDeal.reinitiate(characters: eventToDeal.mainAreaNumKeyChar) ?? eventToDeal + eventToDeal = eventToDeal.reinitiate(characters: eventToDeal.mainAreaNumKeyChar) } // 準備修飾鍵,用來判定要新增的詞彙是否需要賦以非常低的權重。 diff --git a/Packages/vChewing_Shared/Package.swift b/Packages/vChewing_Shared/Package.swift index e48fceb6..bef58572 100644 --- a/Packages/vChewing_Shared/Package.swift +++ b/Packages/vChewing_Shared/Package.swift @@ -14,6 +14,7 @@ let package = Package( ], dependencies: [ .package(path: "../vChewing_CocoaExtension"), + .package(path: "../vChewing_IMKUtils"), .package(path: "../vChewing_SwiftExtension"), ], targets: [ @@ -21,6 +22,7 @@ let package = Package( name: "Shared", dependencies: [ .product(name: "CocoaExtension", package: "vChewing_CocoaExtension"), + .product(name: "IMKUtils", package: "vChewing_IMKUtils"), .product(name: "SwiftExtension", package: "vChewing_SwiftExtension"), ] ), diff --git a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSEvent.swift b/Packages/vChewing_Shared/Sources/Shared/KBEvent.swift similarity index 58% rename from Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSEvent.swift rename to Packages/vChewing_Shared/Sources/Shared/KBEvent.swift index ee523a2e..7503b673 100644 --- a/Packages/vChewing_CocoaExtension/Sources/CocoaExtension/CocoaExtension_NSEvent.swift +++ b/Packages/vChewing_Shared/Sources/Shared/KBEvent.swift @@ -6,42 +6,103 @@ // marks, or product names of Contributor, except as required to fulfill notice // requirements defined in MIT License. -import AppKit +import Foundation import IMKUtils -// MARK: - NSEvent Extension - Reconstructors +public struct KBEvent: InputSignalProtocol, Hashable { + public private(set) var type: EventType + public private(set) var modifierFlags: ModifierFlags + public private(set) var timestamp: TimeInterval + public private(set) var windowNumber: Int + public private(set) var characters: String? + public private(set) var charactersIgnoringModifiers: String? + public private(set) var isARepeat: Bool + public private(set) var keyCode: UInt16 -public extension NSEvent { - func reinitiate( - with type: NSEvent.EventType? = nil, - location: NSPoint? = nil, - modifierFlags: NSEvent.ModifierFlags? = nil, + public init( + with type: KBEvent.EventType? = nil, + modifierFlags: KBEvent.ModifierFlags? = nil, timestamp: TimeInterval? = nil, windowNumber: Int? = nil, characters: String? = nil, charactersIgnoringModifiers: String? = nil, isARepeat: Bool? = nil, keyCode: UInt16? = nil - ) -> NSEvent? { + ) { + var characters = characters + checkSpecialKey: if let matchedKey = KeyCode(rawValue: keyCode ?? 0), let flags = modifierFlags { + let scalar = matchedKey.correspondedSpecialKeyScalar(flags: flags) + guard let scalar = scalar else { break checkSpecialKey } + characters = .init(scalar) + } + self.type = type ?? .keyDown + self.modifierFlags = modifierFlags ?? [] + self.timestamp = timestamp ?? Date().timeIntervalSince1970 + self.windowNumber = windowNumber ?? 0 + self.characters = characters ?? "" + self.charactersIgnoringModifiers = charactersIgnoringModifiers ?? characters ?? "" + self.isARepeat = isARepeat ?? false + self.keyCode = keyCode ?? KeyCode.kNone.rawValue + } + + public func reinitiate( + with type: KBEvent.EventType? = nil, + modifierFlags: KBEvent.ModifierFlags? = nil, + timestamp: TimeInterval? = nil, + windowNumber: Int? = nil, + characters: String? = nil, + charactersIgnoringModifiers: String? = nil, + isARepeat: Bool? = nil, + keyCode: UInt16? = nil + ) -> KBEvent { let oldChars: String = text - return NSEvent.keyEvent( - with: type ?? self.type, - location: location ?? locationInWindow, + return KBEvent( + with: type ?? .keyDown, modifierFlags: modifierFlags ?? self.modifierFlags, timestamp: timestamp ?? self.timestamp, windowNumber: windowNumber ?? self.windowNumber, - context: nil, characters: characters ?? oldChars, charactersIgnoringModifiers: charactersIgnoringModifiers ?? characters ?? oldChars, isARepeat: isARepeat ?? self.isARepeat, keyCode: keyCode ?? self.keyCode ) } +} - /// 自 Emacs 熱鍵的 NSEvent 翻譯回標準 NSEvent。失敗的話則會返回原始 NSEvent 自身。 +// MARK: - KBEvent Extension - SubTypes + +public extension KBEvent { + struct ModifierFlags: OptionSet, Hashable { + public init(rawValue: UInt) { + self.rawValue = rawValue + } + + public let rawValue: UInt + public static let capsLock = ModifierFlags(rawValue: 1 << 16) // Set if Caps Lock key is pressed. + public static let shift = ModifierFlags(rawValue: 1 << 17) // Set if Shift key is pressed. + public static let control = ModifierFlags(rawValue: 1 << 18) // Set if Control key is pressed. + public static let option = ModifierFlags(rawValue: 1 << 19) // Set if Option or Alternate key is pressed. + public static let command = ModifierFlags(rawValue: 1 << 20) // Set if Command key is pressed. + public static let numericPad = ModifierFlags(rawValue: 1 << 21) // Set if any key in the numeric keypad is pressed. + public static let help = ModifierFlags(rawValue: 1 << 22) // Set if the Help key is pressed. + public static let function = ModifierFlags(rawValue: 1 << 23) // Set if any function key is pressed. + public static let deviceIndependentFlagsMask = ModifierFlags(rawValue: 0xFFFF_0000) + } + + enum EventType: UInt8 { + case keyDown = 10 + case keyUp = 11 + case flagsChanged = 12 + } +} + +// MARK: - KBEvent Extension - Emacs Key Conversions + +public extension KBEvent { + /// 自 Emacs 熱鍵的 KBEvent 翻譯回標準 KBEvent。失敗的話則會返回原始 KBEvent 自身。 /// - Parameter isVerticalTyping: 是否按照縱排來操作。 /// - Returns: 翻譯結果。失敗的話則返回翻譯原文。 - func convertFromEmacsKeyEvent(isVerticalContext: Bool) -> NSEvent { + func convertFromEmacsKeyEvent(isVerticalContext: Bool) -> KBEvent { guard isEmacsKey else { return self } let newKeyCode: UInt16 = { switch isVerticalContext { @@ -50,32 +111,15 @@ public extension NSEvent { } }() 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 + return reinitiate(modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil, keyCode: newKeyCode) } } -// MARK: - NSEvent Extension - InputSignalProtocol +// MARK: - KBEvent Extension - InputSignalProtocol -public extension NSEvent { +public extension KBEvent { var isTypingVertical: Bool { charactersIgnoringModifiers == "Vertical" } - /// NSEvent.characters 的類型安全版。 + /// KBEvent.characters 的類型安全版。 /// - Remark: 注意:必須針對 event.type == .flagsChanged 提前返回結果, /// 否則,每次處理這種判斷時都會因為讀取 event.characters? 而觸發 NSInternalInconsistencyException。 var text: String { isFlagChanged ? "" : characters ?? "" } @@ -98,10 +142,6 @@ public extension NSEvent { modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock) } - static var keyModifierFlags: ModifierFlags { - Self.modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock) - } - var isFlagChanged: Bool { type == .flagsChanged } var isEmacsKey: Bool { @@ -204,6 +244,94 @@ public extension NSEvent { // MARK: - Enums of Constants +public extension KBEvent { + enum SpecialKey: UInt16 { + var unicodeScalar: Unicode.Scalar { .init(rawValue) ?? .init(0) } + case upArrow = 0xF700 + case downArrow = 0xF701 + case leftArrow = 0xF702 + case rightArrow = 0xF703 + case f1 = 0xF704 + case f2 = 0xF705 + case f3 = 0xF706 + case f4 = 0xF707 + case f5 = 0xF708 + case f6 = 0xF709 + case f7 = 0xF70A + case f8 = 0xF70B + case f9 = 0xF70C + case f10 = 0xF70D + case f11 = 0xF70E + case f12 = 0xF70F + case f13 = 0xF710 + case f14 = 0xF711 + case f15 = 0xF712 + case f16 = 0xF713 + case f17 = 0xF714 + case f18 = 0xF715 + case f19 = 0xF716 + case f20 = 0xF717 + case f21 = 0xF718 + case f22 = 0xF719 + case f23 = 0xF71A + case f24 = 0xF71B + case f25 = 0xF71C + case f26 = 0xF71D + case f27 = 0xF71E + case f28 = 0xF71F + case f29 = 0xF720 + case f30 = 0xF721 + case f31 = 0xF722 + case f32 = 0xF723 + case f33 = 0xF724 + case f34 = 0xF725 + case f35 = 0xF726 + case insert = 0xF727 + case deleteForward = 0xF728 + case home = 0xF729 + case begin = 0xF72A + case end = 0xF72B + case pageUp = 0xF72C + case pageDown = 0xF72D + case printScreen = 0xF72E + case scrollLock = 0xF72F + case pause = 0xF730 + case sysReq = 0xF731 + case `break` = 0xF732 + case reset = 0xF733 + case stop = 0xF734 + case menu = 0xF735 + case user = 0xF736 + case system = 0xF737 + case print = 0xF738 + case clearLine = 0xF739 + case clearDisplay = 0xF73A + case insertLine = 0xF73B + case deleteLine = 0xF73C + case insertCharacter = 0xF73D + case deleteCharacter = 0xF73E + case prev = 0xF73F + case next = 0xF740 + case select = 0xF741 + case execute = 0xF742 + case undo = 0xF743 + case redo = 0xF744 + case find = 0xF745 + case help = 0xF746 + case modeSwitch = 0xF747 + case enter = 0x03 + case backspace = 0x08 + case tab = 0x09 + case newline = 0x0A + case formFeed = 0x0C + case carriageReturn = 0x0D + case backTab = 0x19 + case delete = 0x7F + case lineSeparator = 0x2028 + case paragraphSeparator = 0x2029 + } +} + // Use KeyCodes as much as possible since its recognition won't be affected by macOS Base Keyboard Layouts. // KeyCodes: https://eastmanreference.com/complete-list-of-applescript-key-codes // Also: HIToolbox.framework/Versions/A/Headers/Events.h @@ -265,14 +393,79 @@ public enum KeyCode: UInt16 { case kDownArrow = 125 case kUpArrow = 126 - public func toEvent() -> NSEvent? { - NSEvent.keyEvent( - with: .keyDown, location: .zero, modifierFlags: [], - timestamp: TimeInterval(), windowNumber: 0, context: nil, + public func toKBEvent() -> KBEvent { + .init( + modifierFlags: [], + timestamp: TimeInterval(), windowNumber: 0, characters: "", charactersIgnoringModifiers: "", isARepeat: false, keyCode: rawValue ) } + + public func correspondedSpecialKeyScalar(flags: KBEvent.ModifierFlags) -> Unicode.Scalar? { + var rawData: KBEvent.SpecialKey? { + switch self { + case .kNone: return nil + case .kCarriageReturn: return .carriageReturn + case .kTab: + return flags.contains(.shift) ? .backTab : .tab + case .kSpace: return nil + case .kSymbolMenuPhysicalKeyIntl: return nil + case .kBackSpace: return .backspace + case .kEscape: return nil + case .kCommand: return nil + case .kShift: return nil + case .kCapsLock: return nil + case .kOption: return nil + case .kControl: return nil + case .kRightShift: return nil + case .kRightOption: return nil + case .kRightControl: return nil + case .kFunction: return nil + case .kF17: return .f17 + case .kVolumeUp: return nil + case .kVolumeDown: return nil + case .kMute: return nil + case .kLineFeed: return nil // TODO: return 待釐清 + case .kF18: return .f18 + case .kF19: return .f19 + case .kF20: return .f20 + case .kYen: return nil + case .kSymbolMenuPhysicalKeyJIS: return nil + case .kJISNumPadComma: return nil + case .kF5: return .f5 + case .kF6: return .f6 + case .kF7: return .f7 + case .kF3: return .f7 + case .kF8: return .f8 + case .kF9: return .f9 + case .kJISAlphanumericalKey: return nil + case .kF11: return .f11 + case .kJISKanaSwappingKey: return nil + case .kF13: return .f13 + case .kF16: return .f16 + case .kF14: return .f14 + case .kF10: return .f10 + case .kContextMenu: return .menu + case .kF12: return .f12 + case .kF15: return .f15 + case .kHelp: return .help + case .kHome: return .home + case .kPageUp: return .pageUp + case .kWindowsDelete: return .deleteForward + case .kF4: return .f4 + case .kEnd: return .end + case .kF2: return .f2 + case .kPageDown: return .pageDown + case .kF1: return .f1 + case .kLeftArrow: return .leftArrow + case .kRightArrow: return .rightArrow + case .kDownArrow: return .downArrow + case .kUpArrow: return .upArrow + } + } + return rawData?.unicodeScalar + } } enum KeyCodeBlackListed: UInt16 { @@ -313,17 +506,6 @@ let mapMainAreaNumKey: [UInt16: String] = [ /// 注意:第 95 號 Key Code(逗號)為 JIS 佈局特有的數字小鍵盤按鍵。 let arrNumpadKeyCodes: [UInt16] = [65, 67, 69, 71, 75, 78, 81, 82, 83, 84, 85, 86, 87, 88, 89, 91, 92, 95] -// CharCodes: https://theasciicode.com.ar/ascii-control-characters/horizontal-tab-ascii-code-9.html -enum CharCode: UInt16 { - case yajuusenpaiA = 114 - case yajuusenpaiB = 514 - case yajuusenpaiC = 1919 - case yajuusenpaiD = 810 - // CharCode is not reliable at all. KeyCode is the most appropriate choice due to its accuracy. - // KeyCode doesn't give a phuque about the character sent through macOS keyboard layouts ... - // ... but only focuses on which physical key is pressed. -} - // MARK: - Emacs CharCode-KeyCode translation tables. public enum EmacsKey { @@ -333,17 +515,17 @@ public enum EmacsKey { // MARK: - Apple ABC Keyboard Mapping -public extension NSEvent { - func layoutTranslated(to layout: LatinKeyboardMappings = .qwerty) -> NSEvent { +public extension KBEvent { + func layoutTranslated(to layout: LatinKeyboardMappings = .qwerty) -> KBEvent { let mapTable = layout.mapTable if isFlagChanged { return self } guard keyModifierFlags == .shift || keyModifierFlags.isEmpty else { return self } if !mapTable.keys.contains(keyCode) { return self } guard let dataTuplet = mapTable[keyCode] else { return self } - let result: NSEvent? = reinitiate( + let result: KBEvent = reinitiate( characters: isShiftHold ? dataTuplet.1 : dataTuplet.0, charactersIgnoringModifiers: dataTuplet.0 ) - return result ?? self + return result } } diff --git a/Packages/vChewing_Shared/Sources/Shared/NSEventImpl.swift b/Packages/vChewing_Shared/Sources/Shared/NSEventImpl.swift new file mode 100644 index 00000000..2ca98a2d --- /dev/null +++ b/Packages/vChewing_Shared/Sources/Shared/NSEventImpl.swift @@ -0,0 +1,211 @@ +// (c) 2022 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. + +import AppKit +import IMKUtils + +// MARK: - NSEvent Extension - Reconstructors + +public extension NSEvent { + func reinitiate( + with type: NSEvent.EventType? = nil, + location: NSPoint? = nil, + modifierFlags: NSEvent.ModifierFlags? = nil, + timestamp: TimeInterval? = nil, + windowNumber: Int? = nil, + characters: String? = nil, + charactersIgnoringModifiers: String? = nil, + isARepeat: Bool? = nil, + keyCode: UInt16? = nil + ) -> NSEvent? { + let oldChars: String = text + var characters = characters + checkSpecialKey: if let matchedKey = KeyCode(rawValue: keyCode ?? self.keyCode) { + let scalar = matchedKey.correspondedSpecialKeyScalar(flags: (modifierFlags ?? self.modifierFlags).toKB) + guard let scalar = scalar else { break checkSpecialKey } + characters = .init(scalar) + } + + return NSEvent.keyEvent( + with: type ?? self.type, + location: location ?? locationInWindow, + modifierFlags: modifierFlags ?? self.modifierFlags, + timestamp: timestamp ?? self.timestamp, + windowNumber: windowNumber ?? self.windowNumber, + context: nil, + characters: characters ?? oldChars, + charactersIgnoringModifiers: charactersIgnoringModifiers ?? characters ?? oldChars, + isARepeat: isARepeat ?? self.isARepeat, + keyCode: keyCode ?? self.keyCode + ) + } + + /// 自 Emacs 熱鍵的 NSEvent 翻譯回標準 NSEvent。失敗的話則會返回原始 NSEvent 自身。 + /// - Parameter isVerticalTyping: 是否按照縱排來操作。 + /// - Returns: 翻譯結果。失敗的話則返回翻譯原文。 + func convertFromEmacsKeyEvent(isVerticalContext: Bool) -> NSEvent { + guard isEmacsKey else { return self } + let newKeyCode: UInt16 = { + switch isVerticalContext { + case false: return EmacsKey.charKeyMapHorizontal[charCode] ?? 0 + case true: return EmacsKey.charKeyMapVertical[charCode] ?? 0 + } + }() + guard newKeyCode != 0 else { return self } + return reinitiate(modifierFlags: [], characters: nil, charactersIgnoringModifiers: nil, keyCode: newKeyCode) + ?? self + } +} + +// MARK: - NSEvent Extension - InputSignalProtocol + +public extension NSEvent { + var isTypingVertical: Bool { charactersIgnoringModifiers == "Vertical" } + /// NSEvent.characters 的類型安全版。 + /// - Remark: 注意:必須針對 event.type == .flagsChanged 提前返回結果, + /// 否則,每次處理這種判斷時都會因為讀取 event.characters? 而觸發 NSInternalInconsistencyException。 + var text: String { isFlagChanged ? "" : characters ?? "" } + var inputTextIgnoringModifiers: String? { + guard charactersIgnoringModifiers != nil else { return nil } + return charactersIgnoringModifiers ?? characters ?? "" + } + + var charCode: UInt16 { + guard type != .flagsChanged else { return 0 } + guard characters != nil else { return 0 } + // 這裡不用「count > 0」,因為該整數變數只要「!isEmpty」那就必定滿足這個條件。 + guard !text.isEmpty else { return 0 } + let scalars = text.unicodeScalars + let result = scalars[scalars.startIndex].value + return result <= UInt16.max ? UInt16(result) : UInt16.max + } + + internal var keyModifierFlagsNS: ModifierFlags { + modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock) + } + + static var keyModifierFlags: ModifierFlags { + Self.modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock) + } + + var isFlagChanged: Bool { type == .flagsChanged } + + var isEmacsKey: Bool { + // 這裡不能只用 isControlHold,因為這裡對修飾鍵的要求有排他性。 + [6, 2, 1, 5, 4, 22].contains(charCode) && keyModifierFlagsNS == .control + } + + // 摁 Alt+Shift+主鍵盤區域數字鍵 的話,根據不同的 macOS 鍵盤佈局種類,會出現不同的符號結果。 + // 然而呢,KeyCode 卻是一致的。於是這裡直接準備一個換算表來用。 + // 這句用來返回換算結果。 + var mainAreaNumKeyChar: String? { mapMainAreaNumKey[keyCode] } + + // 除了 ANSI charCode 以外,其餘一律過濾掉,免得 InputHandler 被餵屎。 + var isInvalid: Bool { + (0x20 ... 0xFF).contains(charCode) ? false : !(isReservedKey && !isKeyCodeBlacklisted) + } + + var isKeyCodeBlacklisted: Bool { + guard let code = KeyCodeBlackListed(rawValue: keyCode) else { return false } + return code.rawValue != KeyCode.kNone.rawValue + } + + var isReservedKey: Bool { + guard let code = KeyCode(rawValue: keyCode) else { return false } + return code.rawValue != KeyCode.kNone.rawValue + } + + /// 單獨用 flags 來判定數字小鍵盤輸入的方法已經失效了,所以必須再增補用 KeyCode 判定的方法。 + var isJISAlphanumericalKey: Bool { KeyCode(rawValue: keyCode) == KeyCode.kJISAlphanumericalKey } + var isJISKanaSwappingKey: Bool { KeyCode(rawValue: keyCode) == KeyCode.kJISKanaSwappingKey } + var isNumericPadKey: Bool { arrNumpadKeyCodes.contains(keyCode) } + var isMainAreaNumKey: Bool { mapMainAreaNumKey.keys.contains(keyCode) } + var isShiftHold: Bool { keyModifierFlagsNS.contains(.shift) } + var isCommandHold: Bool { keyModifierFlagsNS.contains(.command) } + var isControlHold: Bool { keyModifierFlagsNS.contains(.control) } + var beganWithLetter: Bool { text.first?.isLetter ?? false } + var isOptionHold: Bool { keyModifierFlagsNS.contains(.option) } + var isOptionHotKey: Bool { keyModifierFlagsNS.contains(.option) && text.first?.isLetter ?? false } + var isCapsLockOn: Bool { modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.capsLock) } + var isFunctionKeyHold: Bool { keyModifierFlagsNS.contains(.function) } + var isNonLaptopFunctionKey: Bool { keyModifierFlagsNS.contains(.numericPad) && !isNumericPadKey } + var isEnter: Bool { [KeyCode.kCarriageReturn, KeyCode.kLineFeed].contains(KeyCode(rawValue: keyCode)) } + var isTab: Bool { KeyCode(rawValue: keyCode) == KeyCode.kTab } + var isUp: Bool { KeyCode(rawValue: keyCode) == KeyCode.kUpArrow } + var isDown: Bool { KeyCode(rawValue: keyCode) == KeyCode.kDownArrow } + var isLeft: Bool { KeyCode(rawValue: keyCode) == KeyCode.kLeftArrow } + var isRight: Bool { KeyCode(rawValue: keyCode) == KeyCode.kRightArrow } + var isPageUp: Bool { KeyCode(rawValue: keyCode) == KeyCode.kPageUp } + var isPageDown: Bool { KeyCode(rawValue: keyCode) == KeyCode.kPageDown } + var isSpace: Bool { KeyCode(rawValue: keyCode) == KeyCode.kSpace } + var isBackSpace: Bool { KeyCode(rawValue: keyCode) == KeyCode.kBackSpace } + var isEsc: Bool { KeyCode(rawValue: keyCode) == KeyCode.kEscape } + var isHome: Bool { KeyCode(rawValue: keyCode) == KeyCode.kHome } + var isEnd: Bool { KeyCode(rawValue: keyCode) == KeyCode.kEnd } + var isDelete: Bool { KeyCode(rawValue: keyCode) == KeyCode.kWindowsDelete } + + var isCursorBackward: Bool { + isTypingVertical + ? KeyCode(rawValue: keyCode) == .kUpArrow + : KeyCode(rawValue: keyCode) == .kLeftArrow + } + + var isCursorForward: Bool { + isTypingVertical + ? KeyCode(rawValue: keyCode) == .kDownArrow + : KeyCode(rawValue: keyCode) == .kRightArrow + } + + var isCursorClockRight: Bool { + isTypingVertical + ? KeyCode(rawValue: keyCode) == .kRightArrow + : KeyCode(rawValue: keyCode) == .kUpArrow + } + + var isCursorClockLeft: Bool { + isTypingVertical + ? KeyCode(rawValue: keyCode) == .kLeftArrow + : KeyCode(rawValue: keyCode) == .kDownArrow + } + + var isASCII: Bool { charCode < 0x80 } + + // 這裡必須加上「flags == .shift」,否則會出現某些情況下輸入法「誤判當前鍵入的非 Shift 字符為大寫」的問題 + var isUpperCaseASCIILetterKey: Bool { + (65 ... 90).contains(charCode) && keyModifierFlagsNS == .shift + } + + // 以 .command 觸發的熱鍵(包括剪貼簿熱鍵)。 + var isSingleCommandBasedLetterHotKey: Bool { + ((65 ... 90).contains(charCode) && keyModifierFlagsNS == [.shift, .command]) + || ((97 ... 122).contains(charCode) && keyModifierFlagsNS == .command) + } + + // 這裡必須用 KeyCode,這樣才不會受隨 macOS 版本更動的 Apple 動態注音鍵盤排列內容的影響。 + // 只是必須得與 ![input isShiftHold] 搭配使用才可以(也就是僅判定 Shift 沒被摁下的情形)。 + var isSymbolMenuPhysicalKey: Bool { + [KeyCode.kSymbolMenuPhysicalKeyIntl, KeyCode.kSymbolMenuPhysicalKeyJIS].contains(KeyCode(rawValue: keyCode)) + } +} + +// MARK: - Apple ABC Keyboard Mapping + +public extension NSEvent { + func layoutTranslated(to layout: LatinKeyboardMappings = .qwerty) -> NSEvent { + let mapTable = layout.mapTable + if isFlagChanged { return self } + guard keyModifierFlagsNS == .shift || keyModifierFlagsNS.isEmpty else { return self } + if !mapTable.keys.contains(keyCode) { return self } + guard let dataTuplet = mapTable[keyCode] else { return self } + let result: NSEvent? = reinitiate( + characters: isShiftHold ? dataTuplet.1 : dataTuplet.0, + charactersIgnoringModifiers: dataTuplet.0 + ) + return result ?? self + } +} diff --git a/Packages/vChewing_Shared/Sources/Shared/NSEventImpl_Shared.swift b/Packages/vChewing_Shared/Sources/Shared/NSEventImpl_Shared.swift new file mode 100644 index 00000000..86000217 --- /dev/null +++ b/Packages/vChewing_Shared/Sources/Shared/NSEventImpl_Shared.swift @@ -0,0 +1,99 @@ +// (c) 2022 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. + +import AppKit + +// MARK: - NSEvent - Conforming to InputSignalProtocol + +extension NSEvent: InputSignalProtocol { + public var keyModifierFlags: KBEvent.ModifierFlags { + .init(rawValue: keyModifierFlagsNS.rawValue) + } +} + +// MARK: - NSEvent - Translating to KBEvent + +public extension NSEvent? { + var copyAsKBEvent: KBEvent? { + self?.copyAsKBEvent ?? nil + } +} + +public extension NSEvent { + var copyAsKBEvent: KBEvent? { + guard let typeKB = type.toKB else { return nil } + return .init( + with: typeKB, + modifierFlags: modifierFlags.toKB, + timestamp: timestamp, + windowNumber: windowNumber, + characters: characters, + charactersIgnoringModifiers: charactersIgnoringModifiers, + isARepeat: isARepeat, + keyCode: keyCode + ) + } +} + +public extension NSEvent.EventType { + var toKB: KBEvent.EventType? { + switch self { + case .flagsChanged: return .flagsChanged + case .keyDown: return .keyDown + case .keyUp: return .keyUp + default: return nil + } + } +} + +public extension NSEvent.ModifierFlags { + var toKB: KBEvent.ModifierFlags { + .init(rawValue: rawValue) + } +} + +// MARK: - KBEvent - Translating to NSEvent + +public extension KBEvent? { + var copyAsNSEvent: NSEvent? { + self?.copyAsNSEvent ?? nil + } +} + +public extension KBEvent { + var copyAsNSEvent: NSEvent? { + NSEvent.keyEvent( + with: type.toNS, + location: .zero, + modifierFlags: modifierFlags.toNS, + timestamp: timestamp, + windowNumber: windowNumber, + context: nil, + characters: characters ?? "", + charactersIgnoringModifiers: charactersIgnoringModifiers ?? "", + isARepeat: isARepeat, + keyCode: keyCode + ) + } +} + +public extension KBEvent.EventType { + var toNS: NSEvent.EventType { + switch self { + case .flagsChanged: return .flagsChanged + case .keyDown: return .keyDown + case .keyUp: return .keyUp + } + } +} + +public extension KBEvent.ModifierFlags { + var toNS: NSEvent.ModifierFlags { + .init(rawValue: rawValue) + } +} diff --git a/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift b/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift index 2404b413..3bc5e81f 100644 --- a/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift +++ b/Packages/vChewing_Shared/Sources/Shared/Protocols/InputSignalProtocol.swift @@ -9,13 +9,10 @@ import AppKit import CocoaExtension -extension NSEvent: InputSignalProtocol {} - // MARK: - InputSignalProtocol public protocol InputSignalProtocol { - var modifierFlags: NSEvent.ModifierFlags { get } - var keyModifierFlags: NSEvent.ModifierFlags { get } + var keyModifierFlags: KBEvent.ModifierFlags { get } var isTypingVertical: Bool { get } var text: String { get } var inputTextIgnoringModifiers: String? { get } @@ -62,3 +59,9 @@ public protocol InputSignalProtocol { var isSingleCommandBasedLetterHotKey: Bool { get } var isSymbolMenuPhysicalKey: Bool { get } } + +public extension InputSignalProtocol { + var commonKeyModifierFlags: KBEvent.ModifierFlags { + keyModifierFlags.subtracting([.function, .numericPad, .help]) + } +}