1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4  * License, v. 2.0. If a copy of the MPL was not distributed with this
5  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 
7 #include "TraversalRule.h"
8 
9 #include "mozilla/ArrayUtils.h"
10 #include "mozilla/a11y/Accessible.h"
11 
12 #include "Role.h"
13 #include "HTMLListAccessible.h"
14 #include "SessionAccessibility.h"
15 #include "nsAccUtils.h"
16 #include "nsIAccessiblePivot.h"
17 
18 using namespace mozilla;
19 using namespace mozilla::a11y;
20 
TraversalRule()21 TraversalRule::TraversalRule()
22     : TraversalRule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT) {}
23 
TraversalRule(int32_t aGranularity)24 TraversalRule::TraversalRule(int32_t aGranularity)
25     : mGranularity(aGranularity) {}
26 
Match(Accessible * aAcc)27 uint16_t TraversalRule::Match(Accessible* aAcc) {
28   MOZ_ASSERT(aAcc);
29 
30   uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
31 
32   if (nsAccUtils::MustPrune(aAcc)) {
33     result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
34   }
35 
36   uint64_t state = aAcc->State();
37 
38   if ((state & states::INVISIBLE) != 0) {
39     return result;
40   }
41 
42   // Bug 1733268: Support OPAQUE1/opacity remotely
43   if (aAcc->IsLocal() && (state & states::OPAQUE1) == 0) {
44     nsIFrame* frame = aAcc->AsLocal()->GetFrame();
45     if (frame && frame->StyleEffects()->mOpacity == 0.0f) {
46       return result | nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
47     }
48   }
49 
50   switch (mGranularity) {
51     case java::SessionAccessibility::HTML_GRANULARITY_LINK:
52       result |= LinkMatch(aAcc);
53       break;
54     case java::SessionAccessibility::HTML_GRANULARITY_CONTROL:
55       result |= ControlMatch(aAcc);
56       break;
57     case java::SessionAccessibility::HTML_GRANULARITY_SECTION:
58       result |= SectionMatch(aAcc);
59       break;
60     case java::SessionAccessibility::HTML_GRANULARITY_HEADING:
61       result |= HeadingMatch(aAcc);
62       break;
63     case java::SessionAccessibility::HTML_GRANULARITY_LANDMARK:
64       result |= LandmarkMatch(aAcc);
65       break;
66     default:
67       result |= DefaultMatch(aAcc);
68       break;
69   }
70 
71   return result;
72 }
73 
IsSingleLineage(Accessible * aAccessible)74 bool TraversalRule::IsSingleLineage(Accessible* aAccessible) {
75   Accessible* child = aAccessible;
76   while (child) {
77     switch (child->ChildCount()) {
78       case 0:
79         return true;
80       case 1:
81         child = child->FirstChild();
82         break;
83       case 2:
84         if (IsListItemBullet(child->FirstChild())) {
85           child = child->LastChild();
86         } else {
87           return false;
88         }
89         break;
90       default:
91         return false;
92     }
93   }
94 
95   return true;
96 }
97 
IsListItemBullet(const Accessible * aAccessible)98 bool TraversalRule::IsListItemBullet(const Accessible* aAccessible) {
99   return aAccessible->Role() == roles::LISTITEM_MARKER;
100 }
101 
IsFlatSubtree(const Accessible * aAccessible)102 bool TraversalRule::IsFlatSubtree(const Accessible* aAccessible) {
103   for (auto child = aAccessible->FirstChild(); child;
104        child = child->NextSibling()) {
105     roles::Role role = child->Role();
106     if (role == roles::TEXT_LEAF || role == roles::STATICTEXT) {
107       continue;
108     }
109 
110     if (child->ChildCount() > 0 || child->ActionCount() > 0) {
111       return false;
112     }
113   }
114 
115   return true;
116 }
117 
HasName(const Accessible * aAccessible)118 bool TraversalRule::HasName(const Accessible* aAccessible) {
119   nsAutoString name;
120   aAccessible->Name(name);
121   name.CompressWhitespace();
122   return !name.IsEmpty();
123 }
124 
LinkMatch(Accessible * aAccessible)125 uint16_t TraversalRule::LinkMatch(Accessible* aAccessible) {
126   if (aAccessible->Role() == roles::LINK &&
127       (aAccessible->State() & states::LINKED) != 0) {
128     return nsIAccessibleTraversalRule::FILTER_MATCH |
129            nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
130   }
131 
132   return nsIAccessibleTraversalRule::FILTER_IGNORE;
133 }
134 
HeadingMatch(Accessible * aAccessible)135 uint16_t TraversalRule::HeadingMatch(Accessible* aAccessible) {
136   if (aAccessible->Role() == roles::HEADING && aAccessible->ChildCount()) {
137     return nsIAccessibleTraversalRule::FILTER_MATCH;
138   }
139 
140   return nsIAccessibleTraversalRule::FILTER_IGNORE;
141 }
142 
SectionMatch(Accessible * aAccessible)143 uint16_t TraversalRule::SectionMatch(Accessible* aAccessible) {
144   roles::Role role = aAccessible->Role();
145   if (role == roles::HEADING || role == roles::LANDMARK ||
146       aAccessible->LandmarkRole()) {
147     return nsIAccessibleTraversalRule::FILTER_MATCH;
148   }
149 
150   return nsIAccessibleTraversalRule::FILTER_IGNORE;
151 }
152 
LandmarkMatch(Accessible * aAccessible)153 uint16_t TraversalRule::LandmarkMatch(Accessible* aAccessible) {
154   if (aAccessible->LandmarkRole()) {
155     return nsIAccessibleTraversalRule::FILTER_MATCH;
156   }
157 
158   return nsIAccessibleTraversalRule::FILTER_IGNORE;
159 }
160 
ControlMatch(Accessible * aAccessible)161 uint16_t TraversalRule::ControlMatch(Accessible* aAccessible) {
162   switch (aAccessible->Role()) {
163     case roles::PUSHBUTTON:
164     case roles::SPINBUTTON:
165     case roles::TOGGLE_BUTTON:
166     case roles::BUTTONDROPDOWN:
167     case roles::BUTTONDROPDOWNGRID:
168     case roles::COMBOBOX:
169     case roles::LISTBOX:
170     case roles::ENTRY:
171     case roles::PASSWORD_TEXT:
172     case roles::PAGETAB:
173     case roles::RADIOBUTTON:
174     case roles::RADIO_MENU_ITEM:
175     case roles::SLIDER:
176     case roles::CHECKBUTTON:
177     case roles::CHECK_MENU_ITEM:
178     case roles::SWITCH:
179     case roles::MENUITEM:
180       return nsIAccessibleTraversalRule::FILTER_MATCH |
181              nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
182     case roles::LINK:
183       return LinkMatch(aAccessible);
184     case roles::EDITCOMBOBOX:
185       if (aAccessible->State() & states::EDITABLE) {
186         // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is
187         // editable. If it's a 1.1 combobox, the combobox is just a container;
188         // we want to stop on the textbox inside it, not the container.
189         return nsIAccessibleTraversalRule::FILTER_MATCH |
190                nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
191       }
192       break;
193     default:
194       break;
195   }
196 
197   return nsIAccessibleTraversalRule::FILTER_IGNORE;
198 }
199 
DefaultMatch(Accessible * aAccessible)200 uint16_t TraversalRule::DefaultMatch(Accessible* aAccessible) {
201   switch (aAccessible->Role()) {
202     case roles::COMBOBOX:
203       // We don't want to ignore the subtree because this is often
204       // where the list box hangs out.
205       return nsIAccessibleTraversalRule::FILTER_MATCH;
206     case roles::EDITCOMBOBOX:
207       if (aAccessible->State() & states::EDITABLE) {
208         // Only match ARIA 1.0 comboboxes; i.e. where the combobox itself is
209         // editable. If it's a 1.1 combobox, the combobox is just a container;
210         // we want to stop on the textbox inside it.
211         return nsIAccessibleTraversalRule::FILTER_MATCH;
212       }
213       break;
214     case roles::TEXT_LEAF:
215     case roles::GRAPHIC:
216       // Nameless text leaves are boring, skip them.
217       if (HasName(aAccessible)) {
218         return nsIAccessibleTraversalRule::FILTER_MATCH;
219       }
220       break;
221     case roles::STATICTEXT:
222       // Ignore list bullets
223       if (!IsListItemBullet(aAccessible)) {
224         return nsIAccessibleTraversalRule::FILTER_MATCH;
225       }
226       break;
227     case roles::HEADER:
228     case roles::HEADING:
229     case roles::COLUMNHEADER:
230     case roles::ROWHEADER:
231     case roles::STATUSBAR:
232       if ((aAccessible->ChildCount() > 0 || HasName(aAccessible)) &&
233           (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible))) {
234         return nsIAccessibleTraversalRule::FILTER_MATCH |
235                nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
236       }
237       break;
238     case roles::GRID_CELL:
239       if (IsSingleLineage(aAccessible) || IsFlatSubtree(aAccessible)) {
240         return nsIAccessibleTraversalRule::FILTER_MATCH |
241                nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
242       }
243       break;
244     case roles::LISTITEM:
245       if (IsFlatSubtree(aAccessible) || IsSingleLineage(aAccessible)) {
246         return nsIAccessibleTraversalRule::FILTER_MATCH |
247                nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
248       }
249       break;
250     case roles::LABEL:
251       if (IsFlatSubtree(aAccessible)) {
252         // Match if this is a label with text but no nested controls.
253         return nsIAccessibleTraversalRule::FILTER_MATCH |
254                nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
255       }
256       break;
257     case roles::MENUITEM:
258     case roles::LINK:
259     case roles::PAGETAB:
260     case roles::PUSHBUTTON:
261     case roles::CHECKBUTTON:
262     case roles::RADIOBUTTON:
263     case roles::PROGRESSBAR:
264     case roles::BUTTONDROPDOWN:
265     case roles::BUTTONMENU:
266     case roles::CHECK_MENU_ITEM:
267     case roles::PASSWORD_TEXT:
268     case roles::RADIO_MENU_ITEM:
269     case roles::TOGGLE_BUTTON:
270     case roles::ENTRY:
271     case roles::KEY:
272     case roles::SLIDER:
273     case roles::SPINBUTTON:
274     case roles::OPTION:
275     case roles::SWITCH:
276     case roles::MATHML_MATH:
277       // Ignore the subtree, if there is one. So that we don't land on
278       // the same content that was already presented by its parent.
279       return nsIAccessibleTraversalRule::FILTER_MATCH |
280              nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
281     default:
282       break;
283   }
284 
285   return nsIAccessibleTraversalRule::FILTER_IGNORE;
286 }
287 
Match(Accessible * aAcc)288 uint16_t ExploreByTouchRule::Match(Accessible* aAcc) {
289   if (aAcc->IsRemote()) {
290     // Explore by touch happens in the local process and should
291     // not drill down into remote frames.
292     return nsIAccessibleTraversalRule::FILTER_IGNORE |
293            nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
294   }
295 
296   return TraversalRule::Match(aAcc);
297 }
298