Sources/OrgEditorAppKit/Keys.swift
166 lines · 6936 bytes
14 symbols in this file
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 /// The text as shown, for accessibility.
18 var accessibleText: (() -> AccessibleText)?
19 var onMouseDown: (() -> Void)?
20 /// The view gained or lost focus, or its window key status: the selection's color changes.
21 var onFocusChange: (() -> Void)?
22 private var keyObservers: [NSObjectProtocol] = []
23
24 override func becomeFirstResponder() -> Bool {
25 defer { onFocusChange?() }
26 return super.becomeFirstResponder()
27 }
28
29 override func resignFirstResponder() -> Bool {
30 defer { onFocusChange?() }
31 return super.resignFirstResponder()
32 }
33
34 override func viewDidMoveToWindow() {
35 super.viewDidMoveToWindow()
36 keyObservers.forEach(NotificationCenter.default.removeObserver)
37 keyObservers = [NSWindow.didBecomeKeyNotification, NSWindow.didResignKeyNotification].map { name in
38 NotificationCenter.default.addObserver(forName: name, object: window, queue: .main) { [weak self] _ in
39 MainActor.assumeIsolated { self?.onFocusChange?() }
40 }
41 }
42 }
43 /// Each editor keeps its own undo history, not the window's.
44 private let ownUndoManager = UndoManager()
45 override var undoManager: UndoManager? { ownUndoManager }
46 var lineNumberFont = NSFont.monospacedDigitSystemFont(ofSize: 11, weight: .regular)
47 var lineNumberColor = NSColor.tertiaryLabelColor
48 var currentLineNumberColor = NSColor.labelColor
49 /// Line starts, kept by the editor, for the line numbers.
50 var lineIndex: LineIndex?
51 var showsLineNumbers = false {
52 didSet { if showsLineNumbers != oldValue { updateGutter() } }
53 }
54
55 override func drawBackground(in rect: NSRect) {
56 super.drawBackground(in: rect)
57 drawLineNumbers(in: rect)
58 }
59
60 /// Redraws the line number margin, as when the caret changes line.
61 func redrawLineNumbers() {
62 guard showsLineNumbers else { return }
63 setNeedsDisplay(NSRect(x: 0, y: visibleRect.minY, width: textContainerInset.width, height: visibleRect.height))
64 }
65 /// Draw the caret as a block over the next character, for normal and visual states.
66 var blockCursor = false {
67 didSet {
68 guard blockCursor != oldValue else { return }
69 needsDisplay = true
70 }
71 }
72
73 override func drawInsertionPoint(in rect: NSRect, color: NSColor, turnedOn flag: Bool) {
74 guard blockCursor else { return super.drawInsertionPoint(in: rect, color: color, turnedOn: flag) }
75 var block = rect
76 block.size.width = max(rect.width, blockWidth)
77 if flag {
78 color.withAlphaComponent(0.45).setFill()
79 block.fill(using: .sourceOver)
80 } else {
81 setNeedsDisplay(block, avoidAdditionalLayout: true)
82 }
83 }
84
85 override func setNeedsDisplay(_ rect: NSRect, avoidAdditionalLayout flag: Bool) {
86 var widened = rect
87 if blockCursor { widened.size.width += blockWidth }
88 super.setNeedsDisplay(widened, avoidAdditionalLayout: flag)
89 }
90
91 private var blockWidth: CGFloat {
92 let font = self.font ?? NSFont.monospacedSystemFont(ofSize: 13, weight: .regular)
93 return ("x" as NSString).size(withAttributes: [.font: font]).width
94 }
95
96 override func keyDown(with event: NSEvent) {
97 if !hasMarkedText(), keyHandler?(event) == true { return }
98 super.keyDown(with: event)
99 }
100
101 override func performKeyEquivalent(with event: NSEvent) -> Bool {
102 if window?.firstResponder === self, !hasMarkedText(), event.modifierFlags.contains(.command), keyHandler?(event) == true {
103 return true
104 }
105 return super.performKeyEquivalent(with: event)
106 }
107
108 /// ⌘-click: given the clicked offset, true when it followed a link.
109 var onCommandClick: ((Int) -> Bool)?
110
111 /// A plain single click: given the clicked offset, true when it was handled.
112 var onClick: ((Int) -> Bool)?
113
114 override func mouseDown(with event: NSEvent) {
115 if event.modifierFlags.contains(.command), let onCommandClick {
116 let offset = characterIndexForInsertion(at: convert(event.locationInWindow, from: nil))
117 if onCommandClick(offset) { return }
118 }
119 onMouseDown?()
120 if event.modifierFlags.intersection([.command, .shift, .option, .control]).isEmpty, event.clickCount == 1,
121 onClick?(characterIndexForInsertion(at: convert(event.locationInWindow, from: nil))) == true {
122 return
123 }
124 super.mouseDown(with: event)
125 }
126}
127
128enum KeyEvents {
129 /// Device-dependent bits for the left and right Option keys (`NX_DEVICELALTKEYMASK`,
130 /// `NX_DEVICERALTKEYMASK`).
131 static let leftOption: UInt = 0x20
132 static let rightOption: UInt = 0x40
133
134 static let named: [UInt16: String] = [
135 48: "TAB", 36: "RET", 76: "RET", 49: "SPC", 53: "ESC", 51: "DEL", 117: "<delete>",
136 123: "<left>", 124: "<right>", 125: "<down>", 126: "<up>",
137 115: "<home>", 119: "<end>", 116: "<prior>", 121: "<next>",
138 122: "<f1>", 120: "<f2>", 99: "<f3>", 118: "<f4>", 96: "<f5>", 97: "<f6>",
139 98: "<f7>", 100: "<f8>", 101: "<f9>", 109: "<f10>", 103: "<f11>", 111: "<f12>",
140 ]
141
142 /// The chord for a key press, or nil when it types a character with Option.
143 static func chord(for event: NSEvent, optionAsMeta: OptionAsMeta) -> KeyChord? {
144 guard event.type == .keyDown else { return nil }
145 let flags = event.modifierFlags
146 var modifiers: KeyChord.Modifiers = []
147 if flags.contains(.control) { modifiers.insert(.control) }
148 if flags.contains(.command) { modifiers.insert(.command) }
149 if flags.contains(.option) {
150 let raw = flags.rawValue
151 let meta = (raw & leftOption != 0 && optionAsMeta.contains(.left)) || (raw & rightOption != 0 && optionAsMeta.contains(.right))
152 if meta || !modifiers.isEmpty {
153 modifiers.insert(.meta)
154 } else {
155 return nil
156 }
157 }
158 if let name = named[event.keyCode] {
159 if flags.contains(.shift) { modifiers.insert(.shift) }
160 return KeyChord(name, modifiers)
161 }
162 guard let characters = event.charactersIgnoringModifiers, characters.count == 1 else { return nil }
163 return KeyChord(characters, modifiers)
164 }
165}
166#endif