AccGroupInfo.cpp (12861B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 #include "AccGroupInfo.h" 6 #include "mozilla/a11y/Accessible.h" 7 #include "mozilla/a11y/TableAccessible.h" 8 9 #include "nsAccUtils.h" 10 #include "nsIAccessiblePivot.h" 11 12 #include "Pivot.h" 13 #include "States.h" 14 15 using namespace mozilla::a11y; 16 17 static role BaseRole(role aRole); 18 19 // This rule finds candidate siblings for compound widget children. 20 class CompoundWidgetSiblingRule : public PivotRule { 21 public: 22 CompoundWidgetSiblingRule() = delete; 23 explicit CompoundWidgetSiblingRule(role aRole) : mRole(aRole) {} 24 25 uint16_t Match(Accessible* aAcc) override { 26 // If the acc has a matching role, that's a valid sibling. If the acc is 27 // separator then the group is ended. Return a match for separators with 28 // the assumption that the caller will check for the role of the returned 29 // accessible. 30 const role accRole = aAcc->Role(); 31 if (BaseRole(accRole) == mRole || accRole == role::SEPARATOR) { 32 return nsIAccessibleTraversalRule::FILTER_MATCH | 33 nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; 34 } 35 36 // Ignore generic accessibles, but keep searching through the subtree for 37 // siblings. 38 if (aAcc->IsGeneric()) { 39 return nsIAccessibleTraversalRule::FILTER_IGNORE; 40 } 41 42 return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; 43 } 44 45 private: 46 role mRole = role::NOTHING; 47 }; 48 49 AccGroupInfo::AccGroupInfo(const Accessible* aItem, role aRole) 50 : mPosInSet(0), mSetSize(0), mParentId(0), mItem(aItem), mRole(aRole) { 51 MOZ_COUNT_CTOR(AccGroupInfo); 52 Update(); 53 } 54 55 void AccGroupInfo::Update() { 56 mParentId = 0; 57 58 Accessible* parent = mItem->GetNonGenericParent(); 59 if (!parent) { 60 return; 61 } 62 63 const int32_t level = GetARIAOrDefaultLevel(mItem); 64 65 // Compute position in set. 66 mPosInSet = 1; 67 68 // Search backwards through the tree for candidate siblings. 69 Accessible* candidateSibling = const_cast<Accessible*>(mItem); 70 Pivot pivot{parent}; 71 CompoundWidgetSiblingRule widgetSiblingRule{mRole}; 72 while ((candidateSibling = pivot.Prev(candidateSibling, widgetSiblingRule)) && 73 candidateSibling != parent) { 74 // If the sibling is separator then the group is ended. 75 if (candidateSibling->Role() == roles::SEPARATOR) { 76 break; 77 } 78 79 const AccGroupInfo* siblingGroupInfo = candidateSibling->GetGroupInfo(); 80 // Skip invisible siblings. 81 // If the sibling has calculated group info, that means it's visible. 82 if (!siblingGroupInfo && candidateSibling->State() & states::INVISIBLE) { 83 continue; 84 } 85 86 // Check if it's hierarchical flatten structure, i.e. if the sibling 87 // level is lesser than this one then group is ended, if the sibling level 88 // is greater than this one then the group is split by some child elements 89 // (group will be continued). 90 const int32_t siblingLevel = GetARIAOrDefaultLevel(candidateSibling); 91 if (siblingLevel < level) { 92 mParentId = candidateSibling->ID(); 93 break; 94 } 95 96 // Skip subset. 97 if (siblingLevel > level) { 98 continue; 99 } 100 101 // If the previous item in the group has calculated group information then 102 // build group information for this item based on found one. 103 if (siblingGroupInfo) { 104 mPosInSet += siblingGroupInfo->mPosInSet; 105 mParentId = siblingGroupInfo->mParentId; 106 mSetSize = siblingGroupInfo->mSetSize; 107 return; 108 } 109 110 mPosInSet++; 111 } 112 113 // Compute set size. 114 mSetSize = mPosInSet; 115 116 candidateSibling = const_cast<Accessible*>(mItem); 117 while ((candidateSibling = pivot.Next(candidateSibling, widgetSiblingRule)) && 118 candidateSibling != parent) { 119 // If the sibling is separator then the group is ended. 120 if (candidateSibling->Role() == roles::SEPARATOR) { 121 break; 122 } 123 124 const AccGroupInfo* siblingGroupInfo = candidateSibling->GetGroupInfo(); 125 // Skip invisible siblings. 126 // If the sibling has calculated group info, that means it's visible. 127 if (!siblingGroupInfo && candidateSibling->State() & states::INVISIBLE) { 128 continue; 129 } 130 131 // and check if it's hierarchical flatten structure. 132 const int32_t siblingLevel = GetARIAOrDefaultLevel(candidateSibling); 133 if (siblingLevel < level) { 134 break; 135 } 136 137 // Skip subset. 138 if (siblingLevel > level) { 139 continue; 140 } 141 142 // If the next item in the group has calculated group information then 143 // build group information for this item based on found one. 144 if (siblingGroupInfo) { 145 mParentId = siblingGroupInfo->mParentId; 146 mSetSize = siblingGroupInfo->mSetSize; 147 return; 148 } 149 150 mSetSize++; 151 } 152 153 if (mParentId) { 154 return; 155 } 156 157 roles::Role parentRole = parent->Role(); 158 if (ShouldReportRelations(mRole, parentRole)) { 159 mParentId = parent->ID(); 160 } 161 162 // ARIA tree and list can be arranged by using ARIA groups to organize levels. 163 if (parentRole != roles::GROUPING) { 164 return; 165 } 166 167 // Way #1 for ARIA tree (not ARIA treegrid): previous sibling of a group is a 168 // parent. In other words the parent of the tree item will be a group and 169 // the previous tree item of the group is a conceptual parent of the tree 170 // item. 171 if (mRole == roles::OUTLINEITEM) { 172 // Find the relevant grandparent of the item. Use that parent as the root 173 // and find the previous outline item sibling within that root. 174 Accessible* grandParent = parent->GetNonGenericParent(); 175 MOZ_ASSERT(grandParent); 176 Pivot pivot{grandParent}; 177 CompoundWidgetSiblingRule parentSiblingRule{mRole}; 178 Accessible* parentPrevSibling = pivot.Prev(parent, widgetSiblingRule); 179 if (parentPrevSibling && parentPrevSibling->Role() == mRole) { 180 mParentId = parentPrevSibling->ID(); 181 return; 182 } 183 } 184 185 // Way #2 for ARIA list and tree: group is a child of an item. In other words 186 // the parent of the item will be a group and containing item of the group is 187 // a conceptual parent of the item. 188 if (mRole == roles::LISTITEM || mRole == roles::OUTLINEITEM) { 189 Accessible* grandParent = parent->GetNonGenericParent(); 190 if (grandParent && grandParent->Role() == mRole) { 191 mParentId = grandParent->ID(); 192 } 193 } 194 } 195 196 AccGroupInfo* AccGroupInfo::CreateGroupInfo(const Accessible* aAccessible) { 197 mozilla::a11y::role role = aAccessible->Role(); 198 if (role != mozilla::a11y::roles::ROW && 199 role != mozilla::a11y::roles::OUTLINEITEM && 200 role != mozilla::a11y::roles::OPTION && 201 role != mozilla::a11y::roles::LISTITEM && 202 role != mozilla::a11y::roles::MENUITEM && 203 role != mozilla::a11y::roles::COMBOBOX_OPTION && 204 role != mozilla::a11y::roles::RICH_OPTION && 205 role != mozilla::a11y::roles::CHECK_RICH_OPTION && 206 role != mozilla::a11y::roles::PARENT_MENUITEM && 207 role != mozilla::a11y::roles::CHECK_MENU_ITEM && 208 role != mozilla::a11y::roles::RADIO_MENU_ITEM && 209 role != mozilla::a11y::roles::RADIOBUTTON && 210 role != mozilla::a11y::roles::PAGETAB && 211 role != mozilla::a11y::roles::COMMENT) { 212 return nullptr; 213 } 214 215 AccGroupInfo* info = new AccGroupInfo(aAccessible, BaseRole(role)); 216 return info; 217 } 218 219 Accessible* AccGroupInfo::FirstItemOf(const Accessible* aContainer) { 220 // ARIA tree can be arranged by ARIA groups case #1 (previous sibling of a 221 // group is a parent) or by aria-level. 222 a11y::role containerRole = aContainer->Role(); 223 Accessible* item = aContainer->NextSibling(); 224 if (item) { 225 if (containerRole == roles::OUTLINEITEM && 226 item->Role() == roles::GROUPING) { 227 item = item->FirstChild(); 228 } 229 230 if (item) { 231 AccGroupInfo* itemGroupInfo = item->GetOrCreateGroupInfo(); 232 if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer) { 233 return item; 234 } 235 } 236 } 237 238 // ARIA list and tree can be arranged by ARIA groups case #2 (group is 239 // a child of an item). 240 item = aContainer->LastChild(); 241 if (!item) return nullptr; 242 243 if (item->Role() == roles::GROUPING && 244 (containerRole == roles::LISTITEM || 245 containerRole == roles::OUTLINEITEM)) { 246 item = item->FirstChild(); 247 if (item) { 248 AccGroupInfo* itemGroupInfo = item->GetOrCreateGroupInfo(); 249 if (itemGroupInfo && itemGroupInfo->ConceptualParent() == aContainer) { 250 return item; 251 } 252 } 253 } 254 255 // Otherwise, it can be a direct child if the container is a list or tree. 256 item = aContainer->FirstChild(); 257 if (ShouldReportRelations(item->Role(), containerRole)) return item; 258 259 return nullptr; 260 } 261 262 uint32_t AccGroupInfo::TotalItemCount(Accessible* aContainer, 263 bool* aIsHierarchical) { 264 uint32_t itemCount = 0; 265 switch (aContainer->Role()) { 266 case roles::GRID: 267 case roles::TABLE: 268 if (auto val = aContainer->GetIntARIAAttr(nsGkAtoms::aria_rowcount)) { 269 if (*val >= 0) { 270 return *val; 271 } 272 } 273 if (TableAccessible* tableAcc = aContainer->AsTable()) { 274 return tableAcc->RowCount(); 275 } 276 break; 277 case roles::ROW: 278 if (Accessible* table = nsAccUtils::TableFor(aContainer)) { 279 if (auto val = table->GetIntARIAAttr(nsGkAtoms::aria_colcount)) { 280 if (*val >= 0) { 281 return *val; 282 } 283 } 284 if (TableAccessible* tableAcc = table->AsTable()) { 285 return tableAcc->ColCount(); 286 } 287 } 288 break; 289 case roles::OUTLINE: 290 case roles::LIST: 291 case roles::MENUBAR: 292 case roles::MENUPOPUP: 293 case roles::COMBOBOX: 294 case roles::GROUPING: 295 case roles::ROWGROUP: 296 case roles::TREE_TABLE: 297 case roles::COMBOBOX_LIST: 298 case roles::LISTBOX: 299 case roles::DEFINITION_LIST: 300 case roles::EDITCOMBOBOX: 301 case roles::RADIO_GROUP: 302 case roles::PAGETABLIST: { 303 Accessible* childItem = AccGroupInfo::FirstItemOf(aContainer); 304 if (!childItem) { 305 childItem = aContainer->FirstChild(); 306 if (childItem && childItem->IsTextLeaf()) { 307 // First child can be a text leaf, check its sibling for an item. 308 childItem = childItem->NextSibling(); 309 } 310 } 311 312 if (childItem) { 313 GroupPos groupPos = childItem->GroupPosition(); 314 itemCount = groupPos.setSize; 315 if (groupPos.level && aIsHierarchical) { 316 *aIsHierarchical = true; 317 } 318 } 319 break; 320 } 321 default: 322 break; 323 } 324 325 return itemCount; 326 } 327 328 Accessible* AccGroupInfo::NextItemTo(Accessible* aItem) { 329 AccGroupInfo* groupInfo = aItem->GetOrCreateGroupInfo(); 330 if (!groupInfo) return nullptr; 331 332 // If the item in middle of the group then search next item in siblings. 333 if (groupInfo->PosInSet() >= groupInfo->SetSize()) return nullptr; 334 335 Accessible* parent = aItem->Parent(); 336 uint32_t childCount = parent->ChildCount(); 337 for (uint32_t idx = aItem->IndexInParent() + 1; idx < childCount; idx++) { 338 Accessible* nextItem = parent->ChildAt(idx); 339 AccGroupInfo* nextGroupInfo = nextItem->GetOrCreateGroupInfo(); 340 if (nextGroupInfo && 341 nextGroupInfo->ConceptualParent() == groupInfo->ConceptualParent()) { 342 return nextItem; 343 } 344 } 345 346 MOZ_ASSERT_UNREACHABLE( 347 "Item in the middle of the group but there's no next item!"); 348 return nullptr; 349 } 350 351 size_t AccGroupInfo::SizeOfIncludingThis(MallocSizeOf aMallocSizeOf) { 352 // We don't count mParentId or mItem since they (should be) counted 353 // as part of the document. 354 return aMallocSizeOf(this); 355 } 356 357 bool AccGroupInfo::ShouldReportRelations(role aRole, role aParentRole) { 358 // We only want to report hierarchy-based node relations for items in tree or 359 // list form. ARIA level/owns relations are always reported. 360 if (aParentRole == roles::OUTLINE && aRole == roles::OUTLINEITEM) return true; 361 if (aParentRole == roles::TREE_TABLE && aRole == roles::ROW) return true; 362 if (aParentRole == roles::LIST && aRole == roles::LISTITEM) return true; 363 364 return false; 365 } 366 367 int32_t AccGroupInfo::GetARIAOrDefaultLevel(const Accessible* aAccessible) { 368 int32_t level = 0; 369 aAccessible->ARIAGroupPosition(&level, nullptr, nullptr); 370 371 if (level != 0) return level; 372 373 return aAccessible->GetLevel(true); 374 } 375 376 Accessible* AccGroupInfo::ConceptualParent() const { 377 if (!mParentId) { 378 // The conceptual parent can never be the document, so id 0 means none. 379 return nullptr; 380 } 381 if (Accessible* doc = 382 nsAccUtils::DocumentFor(const_cast<Accessible*>(mItem))) { 383 return nsAccUtils::GetAccessibleByID(doc, mParentId); 384 } 385 return nullptr; 386 } 387 388 static role BaseRole(role aRole) { 389 if (aRole == roles::CHECK_MENU_ITEM || aRole == roles::PARENT_MENUITEM || 390 aRole == roles::RADIO_MENU_ITEM) { 391 return roles::MENUITEM; 392 } 393 394 if (aRole == roles::CHECK_RICH_OPTION) { 395 return roles::RICH_OPTION; 396 } 397 398 return aRole; 399 }