|
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim:expandtab:shiftwidth=2:tabstop=2: |
|
3 */ |
|
4 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
5 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
6 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
7 |
|
8 #include "nsTextEquivUtils.h" |
|
9 |
|
10 #include "Accessible-inl.h" |
|
11 #include "AccIterator.h" |
|
12 #include "nsCoreUtils.h" |
|
13 #include "nsIDOMXULLabeledControlEl.h" |
|
14 |
|
15 using namespace mozilla::a11y; |
|
16 |
|
17 /** |
|
18 * The accessible for which we are computing a text equivalent. It is useful |
|
19 * for bailing out during recursive text computation, or for special cases |
|
20 * like step f. of the ARIA implementation guide. |
|
21 */ |
|
22 static Accessible* sInitiatorAcc = nullptr; |
|
23 |
|
24 //////////////////////////////////////////////////////////////////////////////// |
|
25 // nsTextEquivUtils. Public. |
|
26 |
|
27 nsresult |
|
28 nsTextEquivUtils::GetNameFromSubtree(Accessible* aAccessible, |
|
29 nsAString& aName) |
|
30 { |
|
31 aName.Truncate(); |
|
32 |
|
33 if (sInitiatorAcc) |
|
34 return NS_OK; |
|
35 |
|
36 sInitiatorAcc = aAccessible; |
|
37 if (GetRoleRule(aAccessible->Role()) == eNameFromSubtreeRule) { |
|
38 //XXX: is it necessary to care the accessible is not a document? |
|
39 if (aAccessible->IsContent()) { |
|
40 nsAutoString name; |
|
41 AppendFromAccessibleChildren(aAccessible, &name); |
|
42 name.CompressWhitespace(); |
|
43 if (!nsCoreUtils::IsWhitespaceString(name)) |
|
44 aName = name; |
|
45 } |
|
46 } |
|
47 |
|
48 sInitiatorAcc = nullptr; |
|
49 |
|
50 return NS_OK; |
|
51 } |
|
52 |
|
53 nsresult |
|
54 nsTextEquivUtils::GetTextEquivFromIDRefs(Accessible* aAccessible, |
|
55 nsIAtom *aIDRefsAttr, |
|
56 nsAString& aTextEquiv) |
|
57 { |
|
58 aTextEquiv.Truncate(); |
|
59 |
|
60 nsIContent* content = aAccessible->GetContent(); |
|
61 if (!content) |
|
62 return NS_OK; |
|
63 |
|
64 nsIContent* refContent = nullptr; |
|
65 IDRefsIterator iter(aAccessible->Document(), content, aIDRefsAttr); |
|
66 while ((refContent = iter.NextElem())) { |
|
67 if (!aTextEquiv.IsEmpty()) |
|
68 aTextEquiv += ' '; |
|
69 |
|
70 nsresult rv = AppendTextEquivFromContent(aAccessible, refContent, |
|
71 &aTextEquiv); |
|
72 NS_ENSURE_SUCCESS(rv, rv); |
|
73 } |
|
74 |
|
75 return NS_OK; |
|
76 } |
|
77 |
|
78 nsresult |
|
79 nsTextEquivUtils::AppendTextEquivFromContent(Accessible* aInitiatorAcc, |
|
80 nsIContent *aContent, |
|
81 nsAString *aString) |
|
82 { |
|
83 // Prevent recursion which can cause infinite loops. |
|
84 if (sInitiatorAcc) |
|
85 return NS_OK; |
|
86 |
|
87 sInitiatorAcc = aInitiatorAcc; |
|
88 |
|
89 // If the given content is not visible or isn't accessible then go down |
|
90 // through the DOM subtree otherwise go down through accessible subtree and |
|
91 // calculate the flat string. |
|
92 nsIFrame *frame = aContent->GetPrimaryFrame(); |
|
93 bool isVisible = frame && frame->StyleVisibility()->IsVisible(); |
|
94 |
|
95 nsresult rv = NS_ERROR_FAILURE; |
|
96 bool goThroughDOMSubtree = true; |
|
97 |
|
98 if (isVisible) { |
|
99 Accessible* accessible = |
|
100 sInitiatorAcc->Document()->GetAccessible(aContent); |
|
101 if (accessible) { |
|
102 rv = AppendFromAccessible(accessible, aString); |
|
103 goThroughDOMSubtree = false; |
|
104 } |
|
105 } |
|
106 |
|
107 if (goThroughDOMSubtree) |
|
108 rv = AppendFromDOMNode(aContent, aString); |
|
109 |
|
110 sInitiatorAcc = nullptr; |
|
111 return rv; |
|
112 } |
|
113 |
|
114 nsresult |
|
115 nsTextEquivUtils::AppendTextEquivFromTextContent(nsIContent *aContent, |
|
116 nsAString *aString) |
|
117 { |
|
118 if (aContent->IsNodeOfType(nsINode::eTEXT)) { |
|
119 bool isHTMLBlock = false; |
|
120 |
|
121 nsIContent *parentContent = aContent->GetFlattenedTreeParent(); |
|
122 if (parentContent) { |
|
123 nsIFrame *frame = parentContent->GetPrimaryFrame(); |
|
124 if (frame) { |
|
125 // If this text is inside a block level frame (as opposed to span |
|
126 // level), we need to add spaces around that block's text, so we don't |
|
127 // get words jammed together in final name. |
|
128 const nsStyleDisplay* display = frame->StyleDisplay(); |
|
129 if (display->IsBlockOutsideStyle() || |
|
130 display->mDisplay == NS_STYLE_DISPLAY_TABLE_CELL) { |
|
131 isHTMLBlock = true; |
|
132 if (!aString->IsEmpty()) { |
|
133 aString->Append(char16_t(' ')); |
|
134 } |
|
135 } |
|
136 } |
|
137 } |
|
138 |
|
139 if (aContent->TextLength() > 0) { |
|
140 nsIFrame *frame = aContent->GetPrimaryFrame(); |
|
141 if (frame) { |
|
142 nsresult rv = frame->GetRenderedText(aString); |
|
143 NS_ENSURE_SUCCESS(rv, rv); |
|
144 } else { |
|
145 // If aContent is an object that is display: none, we have no a frame. |
|
146 aContent->AppendTextTo(*aString); |
|
147 } |
|
148 if (isHTMLBlock && !aString->IsEmpty()) { |
|
149 aString->Append(char16_t(' ')); |
|
150 } |
|
151 } |
|
152 |
|
153 return NS_OK; |
|
154 } |
|
155 |
|
156 if (aContent->IsHTML() && |
|
157 aContent->NodeInfo()->Equals(nsGkAtoms::br)) { |
|
158 aString->AppendLiteral("\r\n"); |
|
159 return NS_OK; |
|
160 } |
|
161 |
|
162 return NS_OK_NO_NAME_CLAUSE_HANDLED; |
|
163 } |
|
164 |
|
165 //////////////////////////////////////////////////////////////////////////////// |
|
166 // nsTextEquivUtils. Private. |
|
167 |
|
168 nsresult |
|
169 nsTextEquivUtils::AppendFromAccessibleChildren(Accessible* aAccessible, |
|
170 nsAString *aString) |
|
171 { |
|
172 nsresult rv = NS_OK_NO_NAME_CLAUSE_HANDLED; |
|
173 |
|
174 uint32_t childCount = aAccessible->ChildCount(); |
|
175 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { |
|
176 Accessible* child = aAccessible->GetChildAt(childIdx); |
|
177 rv = AppendFromAccessible(child, aString); |
|
178 NS_ENSURE_SUCCESS(rv, rv); |
|
179 } |
|
180 |
|
181 return rv; |
|
182 } |
|
183 |
|
184 nsresult |
|
185 nsTextEquivUtils::AppendFromAccessible(Accessible* aAccessible, |
|
186 nsAString *aString) |
|
187 { |
|
188 //XXX: is it necessary to care the accessible is not a document? |
|
189 if (aAccessible->IsContent()) { |
|
190 nsresult rv = AppendTextEquivFromTextContent(aAccessible->GetContent(), |
|
191 aString); |
|
192 if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) |
|
193 return rv; |
|
194 } |
|
195 |
|
196 bool isEmptyTextEquiv = true; |
|
197 |
|
198 // If the name is from tooltip then append it to result string in the end |
|
199 // (see h. step of name computation guide). |
|
200 nsAutoString text; |
|
201 if (aAccessible->Name(text) != eNameFromTooltip) |
|
202 isEmptyTextEquiv = !AppendString(aString, text); |
|
203 |
|
204 // Implementation of f. step. |
|
205 nsresult rv = AppendFromValue(aAccessible, aString); |
|
206 NS_ENSURE_SUCCESS(rv, rv); |
|
207 |
|
208 if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) |
|
209 isEmptyTextEquiv = false; |
|
210 |
|
211 // Implementation of g) step of text equivalent computation guide. Go down |
|
212 // into subtree if accessible allows "text equivalent from subtree rule" or |
|
213 // it's not root and not control. |
|
214 if (isEmptyTextEquiv) { |
|
215 uint32_t nameRule = GetRoleRule(aAccessible->Role()); |
|
216 if (nameRule & eNameFromSubtreeIfReqRule) { |
|
217 rv = AppendFromAccessibleChildren(aAccessible, aString); |
|
218 NS_ENSURE_SUCCESS(rv, rv); |
|
219 |
|
220 if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) |
|
221 isEmptyTextEquiv = false; |
|
222 } |
|
223 } |
|
224 |
|
225 // Implementation of h. step |
|
226 if (isEmptyTextEquiv && !text.IsEmpty()) { |
|
227 AppendString(aString, text); |
|
228 return NS_OK; |
|
229 } |
|
230 |
|
231 return rv; |
|
232 } |
|
233 |
|
234 nsresult |
|
235 nsTextEquivUtils::AppendFromValue(Accessible* aAccessible, |
|
236 nsAString *aString) |
|
237 { |
|
238 if (GetRoleRule(aAccessible->Role()) != eNameFromValueRule) |
|
239 return NS_OK_NO_NAME_CLAUSE_HANDLED; |
|
240 |
|
241 // Implementation of step f. of text equivalent computation. If the given |
|
242 // accessible is not root accessible (the accessible the text equivalent is |
|
243 // computed for in the end) then append accessible value. Otherwise append |
|
244 // value if and only if the given accessible is in the middle of its parent. |
|
245 |
|
246 nsAutoString text; |
|
247 if (aAccessible != sInitiatorAcc) { |
|
248 aAccessible->Value(text); |
|
249 |
|
250 return AppendString(aString, text) ? |
|
251 NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED; |
|
252 } |
|
253 |
|
254 //XXX: is it necessary to care the accessible is not a document? |
|
255 if (aAccessible->IsDoc()) |
|
256 return NS_ERROR_UNEXPECTED; |
|
257 |
|
258 nsIContent *content = aAccessible->GetContent(); |
|
259 |
|
260 for (nsIContent* childContent = content->GetPreviousSibling(); childContent; |
|
261 childContent = childContent->GetPreviousSibling()) { |
|
262 // check for preceding text... |
|
263 if (!childContent->TextIsOnlyWhitespace()) { |
|
264 for (nsIContent* siblingContent = content->GetNextSibling(); siblingContent; |
|
265 siblingContent = siblingContent->GetNextSibling()) { |
|
266 // .. and subsequent text |
|
267 if (!siblingContent->TextIsOnlyWhitespace()) { |
|
268 aAccessible->Value(text); |
|
269 |
|
270 return AppendString(aString, text) ? |
|
271 NS_OK : NS_OK_NO_NAME_CLAUSE_HANDLED; |
|
272 break; |
|
273 } |
|
274 } |
|
275 break; |
|
276 } |
|
277 } |
|
278 |
|
279 return NS_OK_NO_NAME_CLAUSE_HANDLED; |
|
280 } |
|
281 |
|
282 nsresult |
|
283 nsTextEquivUtils::AppendFromDOMChildren(nsIContent *aContent, |
|
284 nsAString *aString) |
|
285 { |
|
286 for (nsIContent* childContent = aContent->GetFirstChild(); childContent; |
|
287 childContent = childContent->GetNextSibling()) { |
|
288 nsresult rv = AppendFromDOMNode(childContent, aString); |
|
289 NS_ENSURE_SUCCESS(rv, rv); |
|
290 } |
|
291 |
|
292 return NS_OK; |
|
293 } |
|
294 |
|
295 nsresult |
|
296 nsTextEquivUtils::AppendFromDOMNode(nsIContent *aContent, nsAString *aString) |
|
297 { |
|
298 nsresult rv = AppendTextEquivFromTextContent(aContent, aString); |
|
299 NS_ENSURE_SUCCESS(rv, rv); |
|
300 |
|
301 if (rv != NS_OK_NO_NAME_CLAUSE_HANDLED) |
|
302 return NS_OK; |
|
303 |
|
304 if (aContent->IsXUL()) { |
|
305 nsAutoString textEquivalent; |
|
306 nsCOMPtr<nsIDOMXULLabeledControlElement> labeledEl = |
|
307 do_QueryInterface(aContent); |
|
308 |
|
309 if (labeledEl) { |
|
310 labeledEl->GetLabel(textEquivalent); |
|
311 } else { |
|
312 if (aContent->NodeInfo()->Equals(nsGkAtoms::label, |
|
313 kNameSpaceID_XUL)) |
|
314 aContent->GetAttr(kNameSpaceID_None, nsGkAtoms::value, |
|
315 textEquivalent); |
|
316 |
|
317 if (textEquivalent.IsEmpty()) |
|
318 aContent->GetAttr(kNameSpaceID_None, |
|
319 nsGkAtoms::tooltiptext, textEquivalent); |
|
320 } |
|
321 |
|
322 AppendString(aString, textEquivalent); |
|
323 } |
|
324 |
|
325 return AppendFromDOMChildren(aContent, aString); |
|
326 } |
|
327 |
|
328 bool |
|
329 nsTextEquivUtils::AppendString(nsAString *aString, |
|
330 const nsAString& aTextEquivalent) |
|
331 { |
|
332 if (aTextEquivalent.IsEmpty()) |
|
333 return false; |
|
334 |
|
335 // Insert spaces to insure that words from controls aren't jammed together. |
|
336 if (!aString->IsEmpty() && !nsCoreUtils::IsWhitespace(aString->Last())) |
|
337 aString->Append(char16_t(' ')); |
|
338 |
|
339 aString->Append(aTextEquivalent); |
|
340 |
|
341 if (!nsCoreUtils::IsWhitespace(aString->Last())) |
|
342 aString->Append(char16_t(' ')); |
|
343 |
|
344 return true; |
|
345 } |
|
346 |
|
347 uint32_t |
|
348 nsTextEquivUtils::GetRoleRule(role aRole) |
|
349 { |
|
350 #define ROLE(geckoRole, stringRole, atkRole, \ |
|
351 macRole, msaaRole, ia2Role, nameRule) \ |
|
352 case roles::geckoRole: \ |
|
353 return nameRule; |
|
354 |
|
355 switch (aRole) { |
|
356 #include "RoleMap.h" |
|
357 default: |
|
358 MOZ_CRASH("Unknown role."); |
|
359 } |
|
360 |
|
361 #undef ROLE |
|
362 } |