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()21TraversalRule::TraversalRule() 22 : TraversalRule(java::SessionAccessibility::HTML_GRANULARITY_DEFAULT) {} 23 TraversalRule(int32_t aGranularity)24TraversalRule::TraversalRule(int32_t aGranularity) 25 : mGranularity(aGranularity) {} 26 Match(Accessible * aAcc)27uint16_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)74bool 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)98bool TraversalRule::IsListItemBullet(const Accessible* aAccessible) { 99 return aAccessible->Role() == roles::LISTITEM_MARKER; 100 } 101 IsFlatSubtree(const Accessible * aAccessible)102bool 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)118bool TraversalRule::HasName(const Accessible* aAccessible) { 119 nsAutoString name; 120 aAccessible->Name(name); 121 name.CompressWhitespace(); 122 return !name.IsEmpty(); 123 } 124 LinkMatch(Accessible * aAccessible)125uint16_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)135uint16_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)143uint16_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)153uint16_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)161uint16_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)200uint16_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)288uint16_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