@@ -9,11 +9,16 @@ import AppKit
9
9
10
10
/// # Notes
11
11
///
12
- /// This implementation considers the entire document as one element, ignoring all subviews and lines.
12
+ /// ~~ This implementation considers the entire document as one element, ignoring all subviews and lines.
13
13
/// Another idea would be to make each line fragment an accessibility element, with options for navigating through
14
14
/// lines from there. The text view would then only handle text input, and lines would handle reading out useful data
15
15
/// to the user.
16
- /// More research needs to be done for the best option here.
16
+ /// More research needs to be done for the best option here.~~
17
+ ///
18
+ /// Consider that the system has access to the ``TextView/accessibilityVisibleCharacterRange`` and
19
+ /// ``TextView/accessibilityString(for:)`` methods. These can combine to allow an accessibility system to efficiently
20
+ /// query the text view's contents. Adding accessibility elements to line fragments would require hit testing them,
21
+ /// which will cause performance degradation.
17
22
extension TextView {
18
23
override open func isAccessibilityElement( ) -> Bool {
19
24
true
@@ -27,6 +32,11 @@ extension TextView {
27
32
isFirstResponder
28
33
}
29
34
35
+ override open func setAccessibilityFocused( _ accessibilityFocused: Bool ) {
36
+ guard !isFirstResponder else { return }
37
+ window? . makeFirstResponder ( self )
38
+ }
39
+
30
40
override open func accessibilityLabel( ) -> String ? {
31
41
" Text Editor "
32
42
}
@@ -48,21 +58,26 @@ extension TextView {
48
58
}
49
59
50
60
override open func accessibilityString( for range: NSRange ) -> String ? {
51
- textStorage. substring (
61
+ guard documentRange. intersection ( range) == range else {
62
+ return nil
63
+ }
64
+
65
+ return textStorage. substring (
52
66
from: textStorage. mutableString. rangeOfComposedCharacterSequences ( for: range)
53
67
)
54
68
}
55
69
56
70
// MARK: Selections
57
71
58
72
override open func accessibilitySelectedText( ) -> String ? {
59
- guard let selection = selectionManager
60
- . textSelections
61
- . sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } )
62
- . first else {
73
+ let selectedRange = accessibilitySelectedTextRange ( )
74
+ guard selectedRange != . notFound else {
63
75
return nil
64
76
}
65
- let range = ( textStorage. string as NSString ) . rangeOfComposedCharacterSequences ( for: selection. range)
77
+ if selectedRange. isEmpty {
78
+ return " "
79
+ }
80
+ let range = ( textStorage. string as NSString ) . rangeOfComposedCharacterSequences ( for: selectedRange)
66
81
return textStorage. substring ( from: range)
67
82
}
68
83
@@ -71,7 +86,10 @@ extension TextView {
71
86
. textSelections
72
87
. sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } )
73
88
. first else {
74
- return . zero
89
+ return . notFound
90
+ }
91
+ if selection. range. isEmpty {
92
+ return selection. range
75
93
}
76
94
return textStorage. mutableString. rangeOfComposedCharacterSequences ( for: selection. range)
77
95
}
@@ -83,12 +101,10 @@ extension TextView {
83
101
}
84
102
85
103
override open func accessibilityInsertionPointLineNumber( ) -> Int {
86
- guard let selection = selectionManager
87
- . textSelections
88
- . sorted ( by: { $0. range. lowerBound < $1. range. lowerBound } )
89
- . first,
90
- let linePosition = layoutManager. textLineForOffset ( selection. range. location) else {
91
- return 0
104
+ let selectedRange = accessibilitySelectedTextRange ( )
105
+ guard selectedRange != . notFound,
106
+ let linePosition = layoutManager. textLineForOffset ( selectedRange. location) else {
107
+ return - 1
92
108
}
93
109
return linePosition. index
94
110
}
@@ -122,6 +138,31 @@ extension TextView {
122
138
}
123
139
124
140
override open func accessibilityRange( for index: Int ) -> NSRange {
125
- textStorage. mutableString. rangeOfComposedCharacterSequence ( at: index)
141
+ guard index < documentRange. length else { return . notFound }
142
+ return textStorage. mutableString. rangeOfComposedCharacterSequence ( at: index)
143
+ }
144
+
145
+ override open func accessibilityVisibleCharacterRange( ) -> NSRange {
146
+ visibleTextRange ?? . notFound
147
+ }
148
+
149
+ /// The line index for a given character offset.
150
+ override open func accessibilityLine( for index: Int ) -> Int {
151
+ guard index <= textStorage. length,
152
+ let textLine = layoutManager. textLineForOffset ( index) else {
153
+ return - 1
154
+ }
155
+ return textLine. index
156
+ }
157
+
158
+ override open func accessibilityFrame( for range: NSRange ) -> NSRect {
159
+ guard documentRange. intersection ( range) == range else {
160
+ return . zero
161
+ }
162
+ if range. isEmpty {
163
+ return . zero
164
+ }
165
+ let rects = layoutManager. rectsFor ( range: range)
166
+ return rects. boundingRect ( )
126
167
}
127
168
}
0 commit comments