Sources/OrgEditorAppKit/Keys.swift
105 lines · 4213 bytes
1#if os(macOS)
2import AppKit
3import OrgCore
4
5/// Which Option keys act as Meta. The others type characters, as usual on a Mac.
6public struct OptionAsMeta: OptionSet, Sendable {
7 public let rawValue: Int
8 public init(rawValue: Int) { self.rawValue = rawValue }
9 public static let left = OptionAsMeta(rawValue: 1)
10 public static let right = OptionAsMeta(rawValue: 2)
11}
12
13/// A text view that offers key presses to the editor's keymap before the text system.
14final class OrgTextView: NSTextView {
15 /// Returns true when the key was handled.
16 var keyHandler: ((NSEvent) -> Bool)?
17 var onMouseDown: (() -> Void)?
18 /// Draw the caret as a block over the next character, for normal and visual states.
19 var blockCursor = false {
20 didSet {
21 guard blockCursor != oldValue else { return }
22 needsDisplay = true
23 }
24 }
25
26 override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
27 guard blockCursor else { return super.drawInsertionPoint(in: rect, color: color, turnedOn: flag) }
28 var block = rect
29 block.size.width = max(rect.width, blockWidth)
30 if flag {
31 color.withAlphaComponent(0.45).setFill()
32 block.fill(using: .sourceOver)
33 } else {
34 setNeedsDisplay(block, avoidAdditionalLayout: true)
35 }
36 }
37
38 override func setNeedsDisplay(_ rect: NSRect, avoidAdditionalLayout flag: Bool) {
39 var widened = rect
40 if blockCursor { widened.size.width += blockWidth }
41 super.setNeedsDisplay(widened, avoidAdditionalLayout: flag)
42 }
43
44 private var blockWidth: CGFloat {
45 let font = self.font ?? NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
46 return ("x" as NSString).size(withAttributes: [.font: font]).width
47 }
48
49 override func keyDown(with event: NSEvent) {
50 if !hasMarkedText(), keyHandler?(event) == true { return }
51 super.keyDown(with: event)
52 }
53
54 override func performKeyEquivalent(with event: NSEvent) -> Bool {
55 if window?.firstResponder === self, !hasMarkedText(), event.modifierFlags.contains(.command), keyHandler?(event) == true {
56 return true
57 }
58 return super.performKeyEquivalent(with: event)
59 }
60
61 override func mouseDown(with event: NSEvent) {
62 onMouseDown?()
63 super.mouseDown(with: event)
64 }
65}
66
67enum KeyEvents {
68 /// Device-dependent bits for the left and right Option keys (`NX_DEVICELALTKEYMASK`,
69 /// `NX_DEVICERALTKEYMASK`).
70 static let leftOption: UInt = 0x20
71 static let rightOption: UInt = 0x40
72
73 static let named: [UInt16: String] = [
74 48: "TAB", 36: "RET", 76: "RET", 49: "SPC", 53: "ESC", 51: "DEL", 117: "<delete>",
75 123: "<left>", 124: "<right>", 125: "<down>", 126: "<up>",
76 115: "<home>", 119: "<end>", 116: "<prior>", 121: "<next>",
77 122: "<f1>", 120: "<f2>", 99: "<f3>", 118: "<f4>", 96: "<f5>", 97: "<f6>",
78 98: "<f7>", 100: "<f8>", 101: "<f9>", 109: "<f10>", 103: "<f11>", 111: "<f12>",
79 ]
80
81 /// The chord for a key press, or nil when it types a character with Option.
82 static func chord(for event: NSEvent, optionAsMeta: OptionAsMeta) -> KeyChord? {
83 guard event.type == .keyDown else { return nil }
84 let flags = event.modifierFlags
85 var modifiers: KeyChord.Modifiers = []
86 if flags.contains(.control) { modifiers.insert(.control) }
87 if flags.contains(.command) { modifiers.insert(.command) }
88 if flags.contains(.option) {
89 let raw = flags.rawValue
90 let meta = (raw & leftOption != 0 && optionAsMeta.contains(.left)) || (raw & rightOption != 0 && optionAsMeta.contains(.right))
91 if meta || !modifiers.isEmpty {
92 modifiers.insert(.meta)
93 } else {
94 return nil
95 }
96 }
97 if let name = named[event.keyCode] {
98 if flags.contains(.shift) { modifiers.insert(.shift) }
99 return KeyChord(name, modifiers)
100 }
101 guard let characters = event.charactersIgnoringModifiers, characters.count == 1 else { return nil }
102 return KeyChord(characters, modifiers)
103 }
104}
105#endif