Shared // Implementing KBEvent.
This commit is contained in:
parent
5e1208bc5e
commit
23ef3124d4
|
@ -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"),
|
||||
]
|
||||
),
|
||||
|
|
|
@ -7,7 +7,6 @@
|
|||
// requirements defined in MIT License.
|
||||
|
||||
import AppKit
|
||||
import InputMethodKit
|
||||
|
||||
public extension NSWindowController {
|
||||
func orderFront() {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
// 準備修飾鍵,用來判定要新增的詞彙是否需要賦以非常低的權重。
|
||||
|
|
|
@ -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"),
|
||||
]
|
||||
),
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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])
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue