mozTableAccessible.mm (18102B)
1 /* clang-format off */ 2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 3 /* clang-format on */ 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 #import "mozTableAccessible.h" 9 #import "nsCocoaUtils.h" 10 #import "MacUtils.h" 11 12 #include "AccIterator.h" 13 #include "LocalAccessible.h" 14 #include "mozilla/a11y/TableAccessible.h" 15 #include "mozilla/a11y/TableCellAccessible.h" 16 #include "nsAccessibilityService.h" 17 #include "nsIAccessiblePivot.h" 18 #include "XULTreeAccessible.h" 19 #include "Pivot.h" 20 #include "nsAccUtils.h" 21 #include "Relation.h" 22 23 using namespace mozilla; 24 using namespace mozilla::a11y; 25 26 @implementation mozColumnContainer 27 28 - (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent { 29 self = [super init]; 30 mIndex = aIndex; 31 mParent = aParent; 32 return self; 33 } 34 35 - (NSString*)moxRole { 36 return NSAccessibilityColumnRole; 37 } 38 39 - (NSString*)moxRoleDescription { 40 return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil); 41 } 42 43 - (mozAccessible*)moxParent { 44 return mParent; 45 } 46 47 - (NSArray*)moxUnignoredChildren { 48 if (mChildren) return mChildren; 49 50 mChildren = [[NSMutableArray alloc] init]; 51 52 TableAccessible* table = [mParent geckoAccessible]->AsTable(); 53 MOZ_ASSERT(table, "Got null table when fetching column children!"); 54 uint32_t numRows = table->RowCount(); 55 56 for (uint32_t j = 0; j < numRows; j++) { 57 Accessible* cell = table->CellAt(j, mIndex); 58 mozAccessible* nativeCell = cell ? GetNativeFromGeckoAccessible(cell) : nil; 59 if ([nativeCell isAccessibilityElement]) { 60 [mChildren addObject:nativeCell]; 61 } 62 } 63 64 return mChildren; 65 } 66 67 - (void)dealloc { 68 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 69 70 [self invalidateChildren]; 71 [super dealloc]; 72 73 NS_OBJC_END_TRY_IGNORE_BLOCK; 74 } 75 76 - (void)expire { 77 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 78 79 [self invalidateChildren]; 80 81 mParent = nil; 82 83 [super expire]; 84 85 NS_OBJC_END_TRY_IGNORE_BLOCK; 86 } 87 88 - (BOOL)isExpired { 89 MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired); 90 91 return [super isExpired]; 92 } 93 94 - (void)invalidateChildren { 95 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 96 97 // make room for new children 98 if (mChildren) { 99 [mChildren release]; 100 mChildren = nil; 101 } 102 103 NS_OBJC_END_TRY_IGNORE_BLOCK; 104 } 105 106 @end 107 108 @implementation mozTablePartAccessible 109 110 - (NSString*)moxTitle { 111 return @""; 112 } 113 114 - (NSString*)moxRole { 115 return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole]; 116 } 117 118 - (BOOL)isLayoutTablePart { 119 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; 120 if ([parent isKindOfClass:[mozTablePartAccessible class]]) { 121 return [(mozTablePartAccessible*)parent isLayoutTablePart]; 122 } else if ([parent isKindOfClass:[mozOutlineAccessible class]]) { 123 return [(mozOutlineAccessible*)parent isLayoutTablePart]; 124 } 125 126 return NO; 127 } 128 @end 129 130 @implementation mozTableAccessible 131 132 - (BOOL)isLayoutTablePart { 133 if (mGeckoAccessible->Role() == roles::TREE_TABLE) { 134 // tree tables are never layout tables, and we shouldn't 135 // query IsProbablyLayoutTable() on them, so we short 136 // circuit here 137 return false; 138 } 139 140 // For LocalAccessible and cached RemoteAccessible, we could use 141 // AsTable()->IsProbablyLayoutTable(). However, if the cache is enabled, 142 // that would build the table cache, which is pointless for layout tables on 143 // Mac because layout tables are AXGroups and do not expose table properties 144 // like AXRows, AXColumns, etc. 145 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { 146 return acc->AsTable()->IsProbablyLayoutTable(); 147 } 148 RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); 149 return proxy->TableIsProbablyForLayout(); 150 } 151 152 - (void)handleAccessibleEvent:(uint32_t)eventType { 153 if (eventType == nsIAccessibleEvent::EVENT_REORDER || 154 eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) { 155 [self invalidateColumns]; 156 } 157 158 [super handleAccessibleEvent:eventType]; 159 } 160 161 - (void)dealloc { 162 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 163 164 [self invalidateColumns]; 165 [super dealloc]; 166 167 NS_OBJC_END_TRY_IGNORE_BLOCK; 168 } 169 170 - (void)expire { 171 [self invalidateColumns]; 172 [super expire]; 173 } 174 175 - (NSNumber*)moxRowCount { 176 MOZ_ASSERT(mGeckoAccessible); 177 178 return @(mGeckoAccessible->AsTable()->RowCount()); 179 } 180 181 - (NSNumber*)moxColumnCount { 182 MOZ_ASSERT(mGeckoAccessible); 183 184 return @(mGeckoAccessible->AsTable()->ColCount()); 185 } 186 187 - (NSArray*)moxRows { 188 // Create a new array with the list of table rows. 189 NSArray* children = [self moxChildren]; 190 NSMutableArray* rows = [[[NSMutableArray alloc] init] autorelease]; 191 for (mozAccessible* curr : children) { 192 if ([curr isKindOfClass:[mozTableRowAccessible class]]) { 193 [rows addObject:curr]; 194 } else if ([[curr moxRole] isEqualToString:@"AXGroup"]) { 195 // Plain thead/tbody elements are removed from the core a11y tree and 196 // replaced with their subtree, but thead/tbody elements with click 197 // handlers are not -- they remain as groups. We need to expose any 198 // rows they contain as rows of the parent table. 199 [rows 200 addObjectsFromArray:[[curr moxChildren] 201 filteredArrayUsingPredicate: 202 [NSPredicate predicateWithBlock:^BOOL( 203 mozAccessible* child, 204 NSDictionary* bindings) { 205 return [child 206 isKindOfClass:[mozTableRowAccessible 207 class]]; 208 }]]]; 209 } 210 } 211 212 return rows; 213 } 214 215 - (NSArray*)moxColumns { 216 MOZ_ASSERT(mGeckoAccessible); 217 218 if (mColContainers) { 219 return mColContainers; 220 } 221 222 mColContainers = [[NSMutableArray alloc] init]; 223 uint32_t numCols = 0; 224 225 numCols = mGeckoAccessible->AsTable()->ColCount(); 226 for (uint32_t i = 0; i < numCols; i++) { 227 mozColumnContainer* container = 228 [[mozColumnContainer alloc] initWithIndex:i andParent:self]; 229 [mColContainers addObject:container]; 230 } 231 232 return mColContainers; 233 } 234 235 - (NSArray*)moxUnignoredChildren { 236 if (![self isLayoutTablePart]) { 237 return [[super moxUnignoredChildren] 238 arrayByAddingObjectsFromArray:[self moxColumns]]; 239 } 240 241 return [super moxUnignoredChildren]; 242 } 243 244 - (NSArray*)moxColumnHeaderUIElements { 245 MOZ_ASSERT(mGeckoAccessible); 246 247 uint32_t numCols = 0; 248 TableAccessible* table = nullptr; 249 250 table = mGeckoAccessible->AsTable(); 251 numCols = table->ColCount(); 252 NSMutableArray* colHeaders = 253 [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease]; 254 255 for (uint32_t i = 0; i < numCols; i++) { 256 Accessible* cell = table->CellAt(0, i); 257 if (cell && cell->Role() == roles::COLUMNHEADER) { 258 mozAccessible* colHeader = GetNativeFromGeckoAccessible(cell); 259 [colHeaders addObject:colHeader]; 260 } 261 } 262 263 return colHeaders; 264 } 265 266 - (id)moxCellForColumnAndRow:(NSArray*)columnAndRow { 267 if (columnAndRow == nil || [columnAndRow count] != 2) { 268 return nil; 269 } 270 271 uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue]; 272 uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue]; 273 274 MOZ_ASSERT(mGeckoAccessible); 275 276 Accessible* cell = mGeckoAccessible->AsTable()->CellAt(row, col); 277 if (!cell) { 278 return nil; 279 } 280 281 return GetNativeFromGeckoAccessible(cell); 282 } 283 284 - (void)invalidateColumns { 285 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; 286 if (mColContainers) { 287 for (mozColumnContainer* col in mColContainers) { 288 [col expire]; 289 } 290 [mColContainers release]; 291 mColContainers = nil; 292 } 293 NS_OBJC_END_TRY_IGNORE_BLOCK; 294 } 295 296 @end 297 298 @interface mozTableRowAccessible () 299 - (mozTableAccessible*)getTableParent; 300 @end 301 302 @implementation mozTableRowAccessible 303 304 - (mozTableAccessible*)getTableParent { 305 id tableParent = static_cast<mozTableAccessible*>( 306 [self moxFindAncestor:^BOOL(id curr, BOOL* stop) { 307 if ([curr isKindOfClass:[mozOutlineAccessible class]]) { 308 // Outline rows are a kind of table row, so it's possible 309 // we're trying to call getTableParent on an outline row here. 310 // Stop searching. 311 *stop = YES; 312 } 313 return [curr isKindOfClass:[mozTableAccessible class]]; 314 }]); 315 316 return [tableParent isKindOfClass:[mozTableAccessible class]] ? tableParent 317 : nil; 318 } 319 320 - (void)handleAccessibleEvent:(uint32_t)eventType { 321 if (eventType == nsIAccessibleEvent::EVENT_REORDER) { 322 // It is possible for getTableParent to return nil if we're 323 // handling a reorder on an outilne row. Outlines don't have 324 // columns, so there's nothing to do here and this will no-op. 325 [[self getTableParent] invalidateColumns]; 326 } 327 328 [super handleAccessibleEvent:eventType]; 329 } 330 331 - (NSNumber*)moxIndex { 332 return @([[[self getTableParent] moxRows] indexOfObjectIdenticalTo:self]); 333 } 334 335 @end 336 337 @implementation mozTableCellAccessible 338 339 - (NSValue*)moxRowIndexRange { 340 MOZ_ASSERT(mGeckoAccessible); 341 342 TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); 343 return 344 [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())]; 345 } 346 347 - (NSValue*)moxColumnIndexRange { 348 MOZ_ASSERT(mGeckoAccessible); 349 350 TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); 351 return 352 [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())]; 353 } 354 355 - (NSArray*)moxRowHeaderUIElements { 356 MOZ_ASSERT(mGeckoAccessible); 357 358 TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); 359 AutoTArray<Accessible*, 10> headerCells; 360 if (cell) { 361 cell->RowHeaderCells(&headerCells); 362 } 363 return utils::ConvertToNSArray(headerCells); 364 } 365 366 - (NSArray*)moxColumnHeaderUIElements { 367 MOZ_ASSERT(mGeckoAccessible); 368 369 TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); 370 AutoTArray<Accessible*, 10> headerCells; 371 if (cell) { 372 cell->ColHeaderCells(&headerCells); 373 } 374 return utils::ConvertToNSArray(headerCells); 375 } 376 377 @end 378 379 /** 380 * This rule matches all accessibles with roles::OUTLINEITEM. If 381 * outlines are nested, it ignores the nested subtree and returns 382 * only items which are descendants of the primary outline. 383 */ 384 class OutlineRule : public PivotRule { 385 public: 386 uint16_t Match(Accessible* aAcc) override { 387 uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; 388 389 if (nsAccUtils::MustPrune(aAcc)) { 390 result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; 391 } 392 393 if (![GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) { 394 return result; 395 } 396 397 if (aAcc->Role() == roles::OUTLINE) { 398 // if the accessible is an outline, we ignore all children 399 result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; 400 } else if (aAcc->Role() == roles::OUTLINEITEM) { 401 // if the accessible is not an outline item, we match here 402 result |= nsIAccessibleTraversalRule::FILTER_MATCH; 403 } 404 405 return result; 406 } 407 }; 408 409 @implementation mozOutlineAccessible 410 411 - (BOOL)isLayoutTablePart { 412 return NO; 413 } 414 415 - (NSArray*)moxRows { 416 // Create a new array with the list of outline rows. We 417 // use pivot here to do a deep traversal of all rows nested 418 // in this outline, not just those which are direct 419 // children, since that's what VO expects. 420 NSMutableArray* allRows = [[[NSMutableArray alloc] init] autorelease]; 421 Pivot p = Pivot(mGeckoAccessible); 422 OutlineRule rule = OutlineRule(); 423 Accessible* firstChild = mGeckoAccessible->FirstChild(); 424 Accessible* match = p.Next(firstChild, rule, true); 425 while (match) { 426 [allRows addObject:GetNativeFromGeckoAccessible(match)]; 427 match = p.Next(match, rule); 428 } 429 return allRows; 430 } 431 432 - (NSArray*)moxColumns { 433 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { 434 if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) { 435 XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc; 436 NSMutableArray* cols = [[[NSMutableArray alloc] init] autorelease]; 437 // XUL trees store their columns in a group at the tree's first 438 // child. Here, we iterate over that group to get each column's 439 // native accessible and add it to our col array. 440 LocalAccessible* treeColumns = treeAcc->LocalChildAt(0); 441 if (treeColumns) { 442 uint32_t colCount = treeColumns->ChildCount(); 443 for (uint32_t i = 0; i < colCount; i++) { 444 LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(i); 445 [cols addObject:GetNativeFromGeckoAccessible(treeColumnItem)]; 446 } 447 return cols; 448 } 449 } 450 } 451 // Webkit says we shouldn't expose any cols for aria-tree 452 // so we return an empty array here 453 return @[]; 454 } 455 456 - (NSArray*)moxSelectedRows { 457 NSMutableArray* selectedRows = [[[NSMutableArray alloc] init] autorelease]; 458 NSArray* allRows = [self moxRows]; 459 for (mozAccessible* row in allRows) { 460 if ([row stateWithMask:states::SELECTED] != 0) { 461 [selectedRows addObject:row]; 462 } 463 } 464 465 return selectedRows; 466 } 467 468 - (NSString*)moxOrientation { 469 return NSAccessibilityVerticalOrientationValue; 470 } 471 472 @end 473 474 @implementation mozOutlineRowAccessible 475 476 - (BOOL)isLayoutTablePart { 477 return NO; 478 } 479 480 - (NSNumber*)moxDisclosing { 481 return @([self stateWithMask:states::EXPANDED] != 0); 482 } 483 484 - (void)moxSetDisclosing:(NSNumber*)disclosing { 485 // VoiceOver requires this to be settable, but doesn't 486 // require it actually affect our disclosing state. 487 // We expose the attr as settable with this method 488 // but do nothing to actually set it. 489 return; 490 } 491 492 - (NSNumber*)moxExpanded { 493 return @([self stateWithMask:states::EXPANDED] != 0); 494 } 495 496 - (id)moxDisclosedByRow { 497 // According to webkit: this attr corresponds to the row 498 // that contains this row. It should be the same as the 499 // first parent that is a treeitem. If the parent is the tree 500 // itself, this should be nil. This is tricky for xul trees because 501 // all rows are direct children of the outline; they use 502 // relations to expose their heirarchy structure. 503 504 // first we check the relations to see if we're in a xul tree 505 // with weird row semantics 506 NSArray<mozAccessible*>* disclosingRows = 507 [self getRelationsByType:RelationType::NODE_CHILD_OF]; 508 mozAccessible* disclosingRow = [disclosingRows firstObject]; 509 510 if (disclosingRow) { 511 // if we find a row from our relation check, 512 // verify it isn't the outline itself and return 513 // appropriately 514 if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) { 515 return nil; 516 } 517 518 return disclosingRow; 519 } 520 521 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; 522 // otherwise, its likely we're in an aria tree, so we can use 523 // these role and subrole checks 524 if ([[parent moxRole] isEqualToString:@"AXOutline"]) { 525 return nil; 526 } 527 528 if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) { 529 disclosingRow = parent; 530 } 531 532 return nil; 533 } 534 535 - (NSNumber*)moxDisclosureLevel { 536 GroupPos groupPos = mGeckoAccessible->GroupPosition(); 537 538 // mac expects 0-indexed levels, but groupPos.level is 1-indexed 539 // so we subtract 1 here for levels above 0 540 return groupPos.level > 0 ? @(groupPos.level - 1) : @(groupPos.level); 541 } 542 543 - (NSArray*)moxDisclosedRows { 544 // According to webkit: this attr corresponds to the rows 545 // that are considered inside this row. Again, this is weird for 546 // xul trees so we have to use relations first and then fall-back 547 // to the children filter for non-xul outlines. 548 549 // first we check the relations to see if we're in a xul tree 550 // with weird row semantics 551 if (NSArray* disclosedRows = 552 [self getRelationsByType:RelationType::NODE_PARENT_OF]) { 553 // if we find rows from our relation check, return them here 554 return disclosedRows; 555 } 556 557 // otherwise, filter our children for outline rows 558 return [[self moxChildren] 559 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( 560 mozAccessible* child, 561 NSDictionary* bindings) { 562 return [child isKindOfClass:[mozOutlineRowAccessible class]]; 563 }]]; 564 } 565 566 - (NSNumber*)moxIndex { 567 id<MOXAccessible> outline = 568 [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { 569 return [[moxAcc moxRole] isEqualToString:@"AXOutline"]; 570 }]; 571 572 NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self]; 573 return index == NSNotFound ? nil : @(index); 574 } 575 576 - (NSString*)moxLabel { 577 nsAutoString title; 578 mGeckoAccessible->Name(title); 579 580 // XXX: When parsing outlines built with ul/lu's, we 581 // include the bullet in this description even 582 // though webkit doesn't. Not all outlines are built with 583 // ul/lu's so we can't strip the first character here. 584 585 return nsCocoaUtils::ToNSString(title); 586 } 587 588 - (int)checkedValue { 589 uint64_t state = [self 590 stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)]; 591 592 if (state & states::CHECKABLE) { 593 if (state & states::CHECKED) { 594 return kChecked; 595 } 596 597 if (state & states::MIXED) { 598 return kMixed; 599 } 600 601 return kUnchecked; 602 } 603 604 return kUncheckable; 605 } 606 607 - (id)moxValue { 608 int checkedValue = [self checkedValue]; 609 return checkedValue >= 0 ? @(checkedValue) : nil; 610 } 611 612 - (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { 613 [super stateChanged:state isEnabled:enabled]; 614 615 if (state & states::EXPANDED) { 616 // If the EXPANDED state is updated, fire appropriate events on the 617 // outline row. 618 [self moxPostNotification:(enabled 619 ? NSAccessibilityRowExpandedNotification 620 : NSAccessibilityRowCollapsedNotification)]; 621 } 622 623 if (state & (states::CHECKED | states::CHECKABLE | states::MIXED)) { 624 // If the MIXED, CHECKED or CHECKABLE state changes, update the value we 625 // expose for the row, which communicates checked status. 626 [self moxPostNotification:NSAccessibilityValueChangedNotification]; 627 } 628 } 629 630 @end