2 /* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
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/. */
8 #import "mozTableAccessible.h"
9 #import "nsCocoaUtils.h"
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"
20 #include "nsAccUtils.h"
23 using namespace mozilla;
24 using namespace mozilla::a11y;
26 @implementation mozColumnContainer
28 - (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent {
35 - (NSString*)moxRole {
36 return NSAccessibilityColumnRole;
39 - (NSString*)moxRoleDescription {
40 return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil);
43 - (mozAccessible*)moxParent {
47 - (NSArray*)moxUnignoredChildren {
48 if (mChildren) return mChildren;
50 mChildren = [[NSMutableArray alloc] init];
52 TableAccessible* table = [mParent geckoAccessible]->AsTable();
53 MOZ_ASSERT(table, "Got null table when fetching column children!");
54 uint32_t numRows = table->RowCount();
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];
68 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
70 [self invalidateChildren];
73 NS_OBJC_END_TRY_IGNORE_BLOCK;
77 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
79 [self invalidateChildren];
85 NS_OBJC_END_TRY_IGNORE_BLOCK;
89 MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired);
91 return [super isExpired];
94 - (void)invalidateChildren {
95 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
97 // make room for new children
103 NS_OBJC_END_TRY_IGNORE_BLOCK;
108 @implementation mozTablePartAccessible
110 - (NSString*)moxTitle {
114 - (NSString*)moxRole {
115 return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole];
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];
130 @implementation mozTableAccessible
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
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();
148 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
149 return proxy->TableIsProbablyForLayout();
152 - (void)handleAccessibleEvent:(uint32_t)eventType {
153 if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
154 eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) {
155 [self invalidateColumns];
158 [super handleAccessibleEvent:eventType];
162 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
164 [self invalidateColumns];
167 NS_OBJC_END_TRY_IGNORE_BLOCK;
171 [self invalidateColumns];
175 - (NSNumber*)moxRowCount {
176 MOZ_ASSERT(mGeckoAccessible);
178 return @(mGeckoAccessible->AsTable()->RowCount());
181 - (NSNumber*)moxColumnCount {
182 MOZ_ASSERT(mGeckoAccessible);
184 return @(mGeckoAccessible->AsTable()->ColCount());
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.
200 addObjectsFromArray:[[curr moxChildren]
201 filteredArrayUsingPredicate:
202 [NSPredicate predicateWithBlock:^BOOL(
203 mozAccessible* child,
204 NSDictionary* bindings) {
206 isKindOfClass:[mozTableRowAccessible
215 - (NSArray*)moxColumns {
216 MOZ_ASSERT(mGeckoAccessible);
218 if (mColContainers) {
219 return mColContainers;
222 mColContainers = [[NSMutableArray alloc] init];
223 uint32_t numCols = 0;
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];
232 return mColContainers;
235 - (NSArray*)moxUnignoredChildren {
236 if (![self isLayoutTablePart]) {
237 return [[super moxUnignoredChildren]
238 arrayByAddingObjectsFromArray:[self moxColumns]];
241 return [super moxUnignoredChildren];
244 - (NSArray*)moxColumnHeaderUIElements {
245 MOZ_ASSERT(mGeckoAccessible);
247 uint32_t numCols = 0;
248 TableAccessible* table = nullptr;
250 table = mGeckoAccessible->AsTable();
251 numCols = table->ColCount();
252 NSMutableArray* colHeaders =
253 [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease];
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];
266 - (id)moxCellForColumnAndRow:(NSArray*)columnAndRow {
267 if (columnAndRow == nil || [columnAndRow count] != 2) {
271 uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue];
272 uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue];
274 MOZ_ASSERT(mGeckoAccessible);
276 Accessible* cell = mGeckoAccessible->AsTable()->CellAt(row, col);
281 return GetNativeFromGeckoAccessible(cell);
284 - (void)invalidateColumns {
285 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
286 if (mColContainers) {
287 for (mozColumnContainer* col in mColContainers) {
290 [mColContainers release];
291 mColContainers = nil;
293 NS_OBJC_END_TRY_IGNORE_BLOCK;
298 @interface mozTableRowAccessible ()
299 - (mozTableAccessible*)getTableParent;
302 @implementation mozTableRowAccessible
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.
313 return [curr isKindOfClass:[mozTableAccessible class]];
316 return [tableParent isKindOfClass:[mozTableAccessible class]] ? tableParent
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];
328 [super handleAccessibleEvent:eventType];
331 - (NSNumber*)moxIndex {
332 return @([[[self getTableParent] moxRows] indexOfObjectIdenticalTo:self]);
337 @implementation mozTableCellAccessible
339 - (NSValue*)moxRowIndexRange {
340 MOZ_ASSERT(mGeckoAccessible);
342 TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
344 [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())];
347 - (NSValue*)moxColumnIndexRange {
348 MOZ_ASSERT(mGeckoAccessible);
350 TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
352 [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())];
355 - (NSArray*)moxRowHeaderUIElements {
356 MOZ_ASSERT(mGeckoAccessible);
358 TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
359 AutoTArray<Accessible*, 10> headerCells;
360 cell->RowHeaderCells(&headerCells);
361 return utils::ConvertToNSArray(headerCells);
364 - (NSArray*)moxColumnHeaderUIElements {
365 MOZ_ASSERT(mGeckoAccessible);
367 TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
368 AutoTArray<Accessible*, 10> headerCells;
369 cell->ColHeaderCells(&headerCells);
370 return utils::ConvertToNSArray(headerCells);
376 * This rule matches all accessibles with roles::OUTLINEITEM. If
377 * outlines are nested, it ignores the nested subtree and returns
378 * only items which are descendants of the primary outline.
380 class OutlineRule : public PivotRule {
382 uint16_t Match(Accessible* aAcc) override {
383 uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE;
385 if (nsAccUtils::MustPrune(aAcc)) {
386 result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
389 if (![GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) {
393 if (aAcc->Role() == roles::OUTLINE) {
394 // if the accessible is an outline, we ignore all children
395 result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
396 } else if (aAcc->Role() == roles::OUTLINEITEM) {
397 // if the accessible is not an outline item, we match here
398 result |= nsIAccessibleTraversalRule::FILTER_MATCH;
405 @implementation mozOutlineAccessible
407 - (BOOL)isLayoutTablePart {
411 - (NSArray*)moxRows {
412 // Create a new array with the list of outline rows. We
413 // use pivot here to do a deep traversal of all rows nested
414 // in this outline, not just those which are direct
415 // children, since that's what VO expects.
416 NSMutableArray* allRows = [[[NSMutableArray alloc] init] autorelease];
417 Pivot p = Pivot(mGeckoAccessible);
418 OutlineRule rule = OutlineRule();
419 Accessible* firstChild = mGeckoAccessible->FirstChild();
420 Accessible* match = p.Next(firstChild, rule, true);
422 [allRows addObject:GetNativeFromGeckoAccessible(match)];
423 match = p.Next(match, rule);
428 - (NSArray*)moxColumns {
429 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
430 if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) {
431 XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc;
432 NSMutableArray* cols = [[[NSMutableArray alloc] init] autorelease];
433 // XUL trees store their columns in a group at the tree's first
434 // child. Here, we iterate over that group to get each column's
435 // native accessible and add it to our col array.
436 LocalAccessible* treeColumns = treeAcc->LocalChildAt(0);
438 uint32_t colCount = treeColumns->ChildCount();
439 for (uint32_t i = 0; i < colCount; i++) {
440 LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(i);
441 [cols addObject:GetNativeFromGeckoAccessible(treeColumnItem)];
447 // Webkit says we shouldn't expose any cols for aria-tree
448 // so we return an empty array here
452 - (NSArray*)moxSelectedRows {
453 NSMutableArray* selectedRows = [[[NSMutableArray alloc] init] autorelease];
454 NSArray* allRows = [self moxRows];
455 for (mozAccessible* row in allRows) {
456 if ([row stateWithMask:states::SELECTED] != 0) {
457 [selectedRows addObject:row];
464 - (NSString*)moxOrientation {
465 return NSAccessibilityVerticalOrientationValue;
470 @implementation mozOutlineRowAccessible
472 - (BOOL)isLayoutTablePart {
476 - (NSNumber*)moxDisclosing {
477 return @([self stateWithMask:states::EXPANDED] != 0);
480 - (void)moxSetDisclosing:(NSNumber*)disclosing {
481 // VoiceOver requires this to be settable, but doesn't
482 // require it actually affect our disclosing state.
483 // We expose the attr as settable with this method
484 // but do nothing to actually set it.
488 - (NSNumber*)moxExpanded {
489 return @([self stateWithMask:states::EXPANDED] != 0);
492 - (id)moxDisclosedByRow {
493 // According to webkit: this attr corresponds to the row
494 // that contains this row. It should be the same as the
495 // first parent that is a treeitem. If the parent is the tree
496 // itself, this should be nil. This is tricky for xul trees because
497 // all rows are direct children of the outline; they use
498 // relations to expose their heirarchy structure.
500 // first we check the relations to see if we're in a xul tree
501 // with weird row semantics
502 NSArray<mozAccessible*>* disclosingRows =
503 [self getRelationsByType:RelationType::NODE_CHILD_OF];
504 mozAccessible* disclosingRow = [disclosingRows firstObject];
507 // if we find a row from our relation check,
508 // verify it isn't the outline itself and return
510 if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) {
514 return disclosingRow;
517 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
518 // otherwise, its likely we're in an aria tree, so we can use
519 // these role and subrole checks
520 if ([[parent moxRole] isEqualToString:@"AXOutline"]) {
524 if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) {
525 disclosingRow = parent;
531 - (NSNumber*)moxDisclosureLevel {
532 GroupPos groupPos = mGeckoAccessible->GroupPosition();
534 // mac expects 0-indexed levels, but groupPos.level is 1-indexed
535 // so we subtract 1 here for levels above 0
536 return groupPos.level > 0 ? @(groupPos.level - 1) : @(groupPos.level);
539 - (NSArray*)moxDisclosedRows {
540 // According to webkit: this attr corresponds to the rows
541 // that are considered inside this row. Again, this is weird for
542 // xul trees so we have to use relations first and then fall-back
543 // to the children filter for non-xul outlines.
545 // first we check the relations to see if we're in a xul tree
546 // with weird row semantics
547 if (NSArray* disclosedRows =
548 [self getRelationsByType:RelationType::NODE_PARENT_OF]) {
549 // if we find rows from our relation check, return them here
550 return disclosedRows;
553 // otherwise, filter our children for outline rows
554 return [[self moxChildren]
555 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
556 mozAccessible* child,
557 NSDictionary* bindings) {
558 return [child isKindOfClass:[mozOutlineRowAccessible class]];
562 - (NSNumber*)moxIndex {
563 id<MOXAccessible> outline =
564 [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
565 return [[moxAcc moxRole] isEqualToString:@"AXOutline"];
568 NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self];
569 return index == NSNotFound ? nil : @(index);
572 - (NSString*)moxLabel {
574 mGeckoAccessible->Name(title);
576 // XXX: When parsing outlines built with ul/lu's, we
577 // include the bullet in this description even
578 // though webkit doesn't. Not all outlines are built with
579 // ul/lu's so we can't strip the first character here.
581 return nsCocoaUtils::ToNSString(title);
584 - (int)checkedValue {
585 uint64_t state = [self
586 stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)];
588 if (state & states::CHECKABLE) {
589 if (state & states::CHECKED) {
593 if (state & states::MIXED) {
604 int checkedValue = [self checkedValue];
605 return checkedValue >= 0 ? @(checkedValue) : nil;
608 - (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
609 [super stateChanged:state isEnabled:enabled];
611 if (state & states::EXPANDED) {
612 // If the EXPANDED state is updated, fire appropriate events on the
614 [self moxPostNotification:(enabled
615 ? NSAccessibilityRowExpandedNotification
616 : NSAccessibilityRowCollapsedNotification)];
619 if (state & (states::CHECKED | states::CHECKABLE | states::MIXED)) {
620 // If the MIXED, CHECKED or CHECKABLE state changes, update the value we
621 // expose for the row, which communicates checked status.
622 [self moxPostNotification:NSAccessibilityValueChangedNotification];