vChewing-macOS/Packages/vChewing_Shared/Sources/Shared/KBEvent.swift

532 lines
19 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (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 Foundation
import IMKUtils
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 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
) {
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 KBEvent(
with: type ?? .keyDown,
modifierFlags: modifierFlags ?? self.modifierFlags,
timestamp: timestamp ?? self.timestamp,
windowNumber: windowNumber ?? self.windowNumber,
characters: characters ?? oldChars,
charactersIgnoringModifiers: charactersIgnoringModifiers ?? characters ?? oldChars,
isARepeat: isARepeat ?? self.isARepeat,
keyCode: keyCode ?? self.keyCode
)
}
}
// 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) -> KBEvent {
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)
}
}
// MARK: - KBEvent Extension - InputSignalProtocol
public extension KBEvent {
var isTypingVertical: Bool { charactersIgnoringModifiers == "Vertical" }
/// KBEvent.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
}
var keyModifierFlags: ModifierFlags {
modifierFlags.intersection(.deviceIndependentFlagsMask).subtracting(.capsLock)
}
var isFlagChanged: Bool { type == .flagsChanged }
var isEmacsKey: Bool {
// isControlHold
[6, 2, 1, 5, 4, 22].contains(charCode) && keyModifierFlags == .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 { keyModifierFlags.contains(.shift) }
var isCommandHold: Bool { keyModifierFlags.contains(.command) }
var isControlHold: Bool { keyModifierFlags.contains(.control) }
var beganWithLetter: Bool { text.first?.isLetter ?? false }
var isOptionHold: Bool { keyModifierFlags.contains(.option) }
var isOptionHotKey: Bool { keyModifierFlags.contains(.option) && text.first?.isLetter ?? false }
var isCapsLockOn: Bool { modifierFlags.intersection(.deviceIndependentFlagsMask).contains(.capsLock) }
var isFunctionKeyHold: Bool { keyModifierFlags.contains(.function) }
var isNonLaptopFunctionKey: Bool { keyModifierFlags.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) && keyModifierFlags == .shift
}
// .command 簿
var isSingleCommandBasedLetterHotKey: Bool {
((65 ... 90).contains(charCode) && keyModifierFlags == [.shift, .command])
|| ((97 ... 122).contains(charCode) && keyModifierFlags == .command)
}
// KeyCode macOS Apple
// ![input isShiftHold] 使 Shift
var isSymbolMenuPhysicalKey: Bool {
[KeyCode.kSymbolMenuPhysicalKeyIntl, KeyCode.kSymbolMenuPhysicalKeyJIS].contains(KeyCode(rawValue: keyCode))
}
}
// 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
public enum KeyCode: UInt16 {
case kNone = 0
case kCarriageReturn = 36 // Renamed from "kReturn" to avoid nomenclatural confusions.
case kTab = 48
case kSpace = 49
case kSymbolMenuPhysicalKeyIntl = 50 // vChewing Specific (Non-JIS)
case kBackSpace = 51 // Renamed from "kDelete" to avoid nomenclatural confusions.
case kEscape = 53
case kCommand = 55
case kShift = 56
case kCapsLock = 57
case kOption = 58
case kControl = 59
case kRightShift = 60
case kRightOption = 61
case kRightControl = 62
case kFunction = 63
case kF17 = 64
case kVolumeUp = 72
case kVolumeDown = 73
case kMute = 74
case kLineFeed = 76 // Another keyCode to identify the Enter Key, typable by Fn+Enter.
case kF18 = 79
case kF19 = 80
case kF20 = 90
case kYen = 93
case kSymbolMenuPhysicalKeyJIS = 94 // vChewing Specific (JIS)
case kJISNumPadComma = 95
case kF5 = 96
case kF6 = 97
case kF7 = 98
case kF3 = 99
case kF8 = 100
case kF9 = 101
case kJISAlphanumericalKey = 102
case kF11 = 103
case kJISKanaSwappingKey = 104
case kF13 = 105 // PrtSc
case kF16 = 106
case kF14 = 107
case kF10 = 109
case kContextMenu = 110
case kF12 = 111
case kF15 = 113
case kHelp = 114 // Insert
case kHome = 115
case kPageUp = 116
case kWindowsDelete = 117 // Renamed from "kForwardDelete" to avoid nomenclatural confusions.
case kF4 = 118
case kEnd = 119
case kF2 = 120
case kPageDown = 121
case kF1 = 122
case kLeftArrow = 123
case kRightArrow = 124
case kDownArrow = 125
case kUpArrow = 126
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 {
case kF17 = 64
case kVolumeUp = 72
case kVolumeDown = 73
case kMute = 74
case kF18 = 79
case kF19 = 80
case kF20 = 90
case kF5 = 96
case kF6 = 97
case kF7 = 98
case kF3 = 99
case kF8 = 100
case kF9 = 101
case kF11 = 103
case kF13 = 105 // PrtSc
case kF16 = 106
case kF14 = 107
case kF10 = 109
case kF12 = 111
case kF15 = 113
case kHelp = 114 // Insert
case kF4 = 118
case kF2 = 120
case kF1 = 122
}
// Alt+Shift+ macOS
// KeyCode
let mapMainAreaNumKey: [UInt16: String] = [
18: "1", 19: "2", 20: "3", 21: "4", 23: "5", 22: "6", 26: "7", 28: "8", 25: "9", 29: "0",
]
/// KeyCode
///
/// 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]
// MARK: - Emacs CharCode-KeyCode translation tables.
public enum EmacsKey {
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: - Apple ABC Keyboard Mapping
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: KBEvent = reinitiate(
characters: isShiftHold ? dataTuplet.1 : dataTuplet.0,
charactersIgnoringModifiers: dataTuplet.0
)
return result
}
}