Sources/OrgEditorAppKit/Accessibility.swift
189 lines · 7656 bytes
22 symbols in this file
AccessibleTextcopyvisiblevisiblebufferlinerangeaccessibilityValueaccessibilityNumberOfCharactersaccessibilitySelectedTextRangesetAccessibilitySelectedTextRangeaccessibilitySelectedTextaccessibilityVisibleCharacterRangeaccessibilityStringaccessibilityAttributedStringaccessibilityLineaccessibilityInsertionPointLineNumberaccessibilityRangeaccessibilityRangeaccessibilityStyleRangeaccessibilityFrameaccessibilityRange
1#if os(macOS)
2import AppKit
3
4/// The text as shown, for VoiceOver: without hidden markup (link brackets and targets, hidden
5/// emphasis markers and stars), with pretty entities and shrunk table cells as they're drawn.
6/// Folded text is still read, so the whole file can be. `model` maps each UTF-16 unit of
7/// `string` to the buffer.
8struct AccessibleText {
9 let string: String
10 /// Buffer offset of each unit of `string`, then the buffer's length.
11 let model: [Int]
12
13 init(_ storage: NSAttributedString) {
14 let ns = storage.string as NSString
15 let whole = NSRange(location: 0, length: ns.length)
16 // What isn't read: markup drawn at no size. Entities read as drawn.
17 var skipped: [Range<Int>] = []
18 storage.enumerateAttribute(.font, in: whole) { value, run, _ in
19 if let font = value as? NSFont, font.pointSize < 0.1 { skipped.append(run.location..<NSMaxRange(run)) }
20 }
21 var displays: [(at: Int, text: String)] = []
22 storage.enumerateAttribute(.orgEntityDisplay, in: whole) { value, run, _ in
23 if let text = value as? String { displays.append((run.location, text)) }
24 }
25 skipped.sort { $0.lowerBound < $1.lowerBound }
26 var units = [unichar](repeating: 0, count: ns.length)
27 units.withUnsafeMutableBufferPointer { ns.getCharacters($0.baseAddress!, range: whole) }
28 var outUnits: [unichar] = []
29 var model: [Int] = []
30 outUnits.reserveCapacity(ns.length)
31 model.reserveCapacity(ns.length + 1)
32 var next = 0
33 var display = 0
34 func copy(_ from: Int, _ to: Int) {
35 var p = from
36 while p < to {
37 while display < displays.count, displays[display].at < p { display += 1 }
38 let stop = display < displays.count ? min(to, displays[display].at) : to
39 outUnits.append(contentsOf: units[p..<stop])
40 model.append(contentsOf: p..<stop)
41 p = stop
42 if p < to {
43 for unit in displays[display].text.utf16 {
44 outUnits.append(unit)
45 model.append(p)
46 }
47 p += 1
48 display += 1
49 }
50 }
51 }
52 for skip in skipped where skip.upperBound > next {
53 if skip.lowerBound > next { copy(next, skip.lowerBound) }
54 next = max(next, skip.upperBound)
55 }
56 copy(next, ns.length)
57 model.append(ns.length)
58 string = String(utf16CodeUnits: outUnits, count: outUnits.count)
59 self.model = model
60 }
61
62 var length: Int { model.count - 1 }
63
64 /// The shown offset at or after buffer offset `offset`.
65 func visible(_ offset: Int) -> Int {
66 var low = 0
67 var high = length
68 while low < high {
69 let mid = (low + high) / 2
70 if model[mid] < offset { low = mid + 1 } else { high = mid }
71 }
72 return low
73 }
74
75 func visible(_ range: NSRange) -> NSRange {
76 let start = visible(range.location)
77 return NSRange(location: start, length: max(0, visible(NSMaxRange(range)) - start))
78 }
79
80 /// The buffer range a shown range covers.
81 func buffer(_ range: NSRange) -> NSRange {
82 let start = model[min(max(0, range.location), length)]
83 let endIndex = min(max(0, NSMaxRange(range)), length)
84 let end = endIndex > range.location && endIndex > 0 ? model[endIndex - 1] + 1 : model[endIndex]
85 return NSRange(location: start, length: max(0, end - start))
86 }
87
88 /// Shown lines: line `n` is the text between the n-th and (n+1)-th newline.
89 func line(for index: Int) -> Int {
90 let ns = string as NSString
91 var line = 0
92 var p = 0
93 while p < min(index, ns.length) {
94 let next = ns.range(of: "\n", range: NSRange(location: p, length: min(index, ns.length) - p))
95 guard next.location != NSNotFound else { break }
96 line += 1
97 p = next.location + 1
98 }
99 return line
100 }
101
102 func range(forLine line: Int) -> NSRange {
103 let ns = string as NSString
104 var start = 0
105 for _ in 0..<line {
106 let next = ns.range(of: "\n", range: NSRange(location: start, length: ns.length - start))
107 guard next.location != NSNotFound else { return NSRange(location: ns.length, length: 0) }
108 start = next.location + 1
109 }
110 return ns.lineRange(for: NSRange(location: start, length: 0))
111 }
112}
113
114extension OrgTextView {
115 private var shown: AccessibleText? { accessibleText?() }
116
117 override func accessibilityValue() -> String? {
118 shown?.string ?? super.accessibilityValue()
119 }
120
121 override func accessibilityNumberOfCharacters() -> Int {
122 shown?.length ?? super.accessibilityNumberOfCharacters()
123 }
124
125 override func accessibilitySelectedTextRange() -> NSRange {
126 guard let shown else { return super.accessibilitySelectedTextRange() }
127 return shown.visible(selectedRange())
128 }
129
130 override func setAccessibilitySelectedTextRange(_ range: NSRange) {
131 guard let shown else { return super.setAccessibilitySelectedTextRange(range) }
132 setSelectedRange(shown.buffer(range))
133 }
134
135 override func accessibilitySelectedText() -> String? {
136 guard let shown else { return super.accessibilitySelectedText() }
137 return (shown.string as NSString).substring(with: shown.visible(selectedRange()))
138 }
139
140 override func accessibilityVisibleCharacterRange() -> NSRange {
141 guard let shown else { return super.accessibilityVisibleCharacterRange() }
142 return shown.visible(super.accessibilityVisibleCharacterRange())
143 }
144
145 override func accessibilityString(for range: NSRange) -> String? {
146 guard let shown else { return super.accessibilityString(for: range) }
147 let ns = shown.string as NSString
148 guard range.location >= 0, NSMaxRange(range) <= ns.length else { return nil }
149 return ns.substring(with: range)
150 }
151
152 override func accessibilityAttributedString(for range: NSRange) -> NSAttributedString? {
153 accessibilityString(for: range).map { NSAttributedString(string: $0) }
154 }
155
156 override func accessibilityLine(for index: Int) -> Int {
157 shown?.line(for: index) ?? super.accessibilityLine(for: index)
158 }
159
160 override func accessibilityInsertionPointLineNumber() -> Int {
161 guard let shown else { return super.accessibilityInsertionPointLineNumber() }
162 return shown.line(for: shown.visible(selectedRange().location))
163 }
164
165 override func accessibilityRange(forLine line: Int) -> NSRange {
166 shown?.range(forLine: line) ?? super.accessibilityRange(forLine: line)
167 }
168
169 override func accessibilityRange(for index: Int) -> NSRange {
170 guard let shown else { return super.accessibilityRange(for: index) }
171 return (shown.string as NSString).rangeOfComposedCharacterSequence(at: min(index, max(0, shown.length - 1)))
172 }
173
174 override func accessibilityStyleRange(for index: Int) -> NSRange {
175 accessibilityRange(for: index)
176 }
177
178 override func accessibilityFrame(for range: NSRange) -> NSRect {
179 guard let shown else { return super.accessibilityFrame(for: range) }
180 return super.accessibilityFrame(for: shown.buffer(range))
181 }
182
183 override func accessibilityRange(for point: NSPoint) -> NSRange {
184 guard let shown else { return super.accessibilityRange(for: point) }
185 let range = super.accessibilityRange(for: point)
186 return shown.visible(range)
187 }
188}
189#endif