krz/orgstar

A native macOS editor for org-mode files. editor org-mode swift

Sources/OrgEditorAppKit/Keys.swift

c1295824f55c7bba681fca8338bf08167e110105
orgstar/Sources/OrgEditorAppKit/Keys.swift history · blame · raw

166 lines · 6936 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    /// 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