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"
11 #import "RotorRules.h"
13 #include "AccIterator.h"
14 #include "LocalAccessible.h"
15 #include "TableAccessible.h"
16 #include "TableCellAccessible.h"
17 #include "XULTreeAccessible.h"
21 using namespace mozilla;
22 using namespace mozilla::a11y;
24 enum CachedBool { eCachedBoolMiss, eCachedTrue, eCachedFalse };
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 if (LocalAccessible* acc = [mParent geckoAccessible]->AsLocal()) {
53 TableAccessible* table = acc->AsTable();
54 MOZ_ASSERT(table, "Got null table when fetching column children!");
55 uint32_t numRows = table->RowCount();
57 for (uint32_t j = 0; j < numRows; j++) {
58 LocalAccessible* cell = table->CellAt(j, mIndex);
59 mozAccessible* nativeCell =
60 cell ? GetNativeFromGeckoAccessible(cell) : nil;
61 if ([nativeCell isAccessibilityElement]) {
62 [mChildren addObject:nativeCell];
66 } else if (RemoteAccessible* proxy = [mParent geckoAccessible]->AsRemote()) {
67 uint32_t numRows = proxy->TableRowCount();
69 for (uint32_t j = 0; j < numRows; j++) {
70 RemoteAccessible* cell = proxy->TableCellAt(j, mIndex);
71 mozAccessible* nativeCell =
72 cell ? GetNativeFromGeckoAccessible(cell) : nil;
73 if ([nativeCell isAccessibilityElement]) {
74 [mChildren addObject:nativeCell];
83 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
85 [self invalidateChildren];
88 NS_OBJC_END_TRY_IGNORE_BLOCK;
92 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
94 [self invalidateChildren];
100 NS_OBJC_END_TRY_IGNORE_BLOCK;
104 MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired);
106 return [super isExpired];
109 - (void)invalidateChildren {
110 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
112 // make room for new children
118 NS_OBJC_END_TRY_IGNORE_BLOCK;
123 @implementation mozTablePartAccessible
125 - (NSString*)moxTitle {
129 - (NSString*)moxRole {
130 return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole];
133 - (void)handleAccessibleEvent:(uint32_t)eventType {
134 if (![self isKindOfClass:[mozTableAccessible class]]) {
135 // If we are not a table, we are a cell or a row.
136 // Check to see if the event we're handling should
137 // invalidate the mIsLayoutTable cache on our parent
139 if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
140 eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED ||
141 eventType == nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED) {
142 // Invalidate the cache on our parent table
143 [self invalidateLayoutTableCache];
147 [super handleAccessibleEvent:eventType];
150 - (BOOL)isLayoutTablePart {
151 // mIsLayoutTable is a cache on each mozTableAccessible that stores
152 // the previous result of calling IsProbablyLayoutTable in core. To see
153 // how/when the cache is invalidated, view handleAccessibleEvent.
154 // The cache contains one of three values from the CachedBool enum
155 // defined in mozTableAccessible.h
156 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
157 if ([parent isKindOfClass:[mozTablePartAccessible class]]) {
158 return [(mozTablePartAccessible*)parent isLayoutTablePart];
159 } else if ([parent isKindOfClass:[mozOutlineAccessible class]]) {
160 return [(mozOutlineAccessible*)parent isLayoutTablePart];
166 - (void)invalidateLayoutTableCache {
167 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
168 if ([parent isKindOfClass:[mozTablePartAccessible class]]) {
169 // We do this to prevent dispatching invalidateLayoutTableCache
170 // on outlines or outline parts. This is possible here because
171 // outline rows subclass table rows, which are a table part.
172 // This means `parent` could be an outline, and there is no
173 // cache on outlines to invalidate.
174 [(mozTablePartAccessible*)parent invalidateLayoutTableCache];
179 @implementation mozTableAccessible
181 - (void)invalidateLayoutTableCache {
182 mIsLayoutTable = eCachedBoolMiss;
185 - (BOOL)isLayoutTablePart {
186 if (mIsLayoutTable != eCachedBoolMiss) {
187 return mIsLayoutTable == eCachedTrue;
190 if (mGeckoAccessible->Role() == roles::TREE_TABLE) {
191 // tree tables are never layout tables, and we shouldn't
192 // query IsProbablyLayoutTable() on them, so we short
194 mIsLayoutTable = eCachedFalse;
199 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
200 tableGuess = acc->AsTable()->IsProbablyLayoutTable();
202 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
203 tableGuess = proxy->TableIsProbablyForLayout();
206 mIsLayoutTable = tableGuess ? eCachedTrue : eCachedFalse;
210 - (void)handleAccessibleEvent:(uint32_t)eventType {
211 if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
212 eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED ||
213 eventType == nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED) {
214 [self invalidateLayoutTableCache];
215 [self invalidateColumns];
218 [super handleAccessibleEvent:eventType];
222 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
224 [self invalidateColumns];
227 NS_OBJC_END_TRY_IGNORE_BLOCK;
230 - (NSNumber*)moxRowCount {
231 MOZ_ASSERT(mGeckoAccessible);
233 return mGeckoAccessible->IsLocal()
234 ? @(mGeckoAccessible->AsLocal()->AsTable()->RowCount())
235 : @(mGeckoAccessible->AsRemote()->TableRowCount());
238 - (NSNumber*)moxColumnCount {
239 MOZ_ASSERT(mGeckoAccessible);
241 return mGeckoAccessible->IsLocal()
242 ? @(mGeckoAccessible->AsLocal()->AsTable()->ColCount())
243 : @(mGeckoAccessible->AsRemote()->TableColumnCount());
246 - (NSArray*)moxRows {
247 // Create a new array with the list of table rows.
248 return [[self moxChildren]
249 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
250 mozAccessible* child,
251 NSDictionary* bindings) {
252 return [child isKindOfClass:[mozTableRowAccessible class]];
256 - (NSArray*)moxColumns {
257 MOZ_ASSERT(mGeckoAccessible);
259 if (mColContainers) {
260 return mColContainers;
263 mColContainers = [[NSMutableArray alloc] init];
264 uint32_t numCols = 0;
266 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
267 numCols = acc->AsTable()->ColCount();
269 numCols = mGeckoAccessible->AsRemote()->TableColumnCount();
272 for (uint32_t i = 0; i < numCols; i++) {
273 mozColumnContainer* container =
274 [[mozColumnContainer alloc] initWithIndex:i andParent:self];
275 [mColContainers addObject:container];
278 return mColContainers;
281 - (NSArray*)moxUnignoredChildren {
282 if (![self isLayoutTablePart]) {
283 return [[super moxUnignoredChildren]
284 arrayByAddingObjectsFromArray:[self moxColumns]];
287 return [super moxUnignoredChildren];
290 - (NSArray*)moxColumnHeaderUIElements {
291 MOZ_ASSERT(mGeckoAccessible);
293 uint32_t numCols = 0;
294 TableAccessible* table = nullptr;
296 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
297 table = mGeckoAccessible->AsLocal()->AsTable();
298 numCols = table->ColCount();
300 numCols = mGeckoAccessible->AsRemote()->TableColumnCount();
303 NSMutableArray* colHeaders =
304 [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease];
306 for (uint32_t i = 0; i < numCols; i++) {
309 cell = table->CellAt(0, i);
311 cell = mGeckoAccessible->AsRemote()->TableCellAt(0, i);
314 if (cell && cell->Role() == roles::COLUMNHEADER) {
315 mozAccessible* colHeader = GetNativeFromGeckoAccessible(cell);
316 [colHeaders addObject:colHeader];
323 - (id)moxCellForColumnAndRow:(NSArray*)columnAndRow {
324 if (columnAndRow == nil || [columnAndRow count] != 2) {
328 uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue];
329 uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue];
331 MOZ_ASSERT(mGeckoAccessible);
334 if (mGeckoAccessible->IsLocal()) {
335 cell = mGeckoAccessible->AsLocal()->AsTable()->CellAt(row, col);
337 cell = mGeckoAccessible->AsRemote()->TableCellAt(row, col);
344 return GetNativeFromGeckoAccessible(cell);
347 - (void)invalidateColumns {
348 NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
349 if (mColContainers) {
350 [mColContainers release];
351 mColContainers = nil;
353 NS_OBJC_END_TRY_IGNORE_BLOCK;
358 @implementation mozTableRowAccessible
360 - (void)handleAccessibleEvent:(uint32_t)eventType {
361 if (eventType == nsIAccessibleEvent::EVENT_REORDER) {
362 id parent = [self moxParent];
363 if ([parent isKindOfClass:[mozTableAccessible class]]) {
364 [parent invalidateColumns];
368 [super handleAccessibleEvent:eventType];
371 - (NSNumber*)moxIndex {
372 mozTableAccessible* parent = (mozTableAccessible*)[self moxParent];
373 return @([[parent moxRows] indexOfObjectIdenticalTo:self]);
378 @implementation mozTableCellAccessible
380 - (NSValue*)moxRowIndexRange {
381 MOZ_ASSERT(mGeckoAccessible);
383 if (mGeckoAccessible->IsLocal()) {
384 TableCellAccessible* cell = mGeckoAccessible->AsLocal()->AsTableCell();
386 [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())];
388 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
390 valueWithRange:NSMakeRange(proxy->RowIdx(), proxy->RowExtent())];
394 - (NSValue*)moxColumnIndexRange {
395 MOZ_ASSERT(mGeckoAccessible);
397 if (mGeckoAccessible->IsLocal()) {
398 TableCellAccessible* cell = mGeckoAccessible->AsLocal()->AsTableCell();
400 [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())];
402 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
404 valueWithRange:NSMakeRange(proxy->ColIdx(), proxy->ColExtent())];
408 - (NSArray*)moxRowHeaderUIElements {
409 MOZ_ASSERT(mGeckoAccessible);
411 if (mGeckoAccessible->IsLocal()) {
412 TableCellAccessible* cell = mGeckoAccessible->AsLocal()->AsTableCell();
413 AutoTArray<LocalAccessible*, 10> headerCells;
414 cell->RowHeaderCells(&headerCells);
415 return utils::ConvertToNSArray(headerCells);
417 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
418 nsTArray<RemoteAccessible*> headerCells;
419 proxy->RowHeaderCells(&headerCells);
420 return utils::ConvertToNSArray(headerCells);
424 - (NSArray*)moxColumnHeaderUIElements {
425 MOZ_ASSERT(mGeckoAccessible);
427 if (mGeckoAccessible->IsLocal()) {
428 TableCellAccessible* cell = mGeckoAccessible->AsLocal()->AsTableCell();
429 AutoTArray<LocalAccessible*, 10> headerCells;
430 cell->ColHeaderCells(&headerCells);
431 return utils::ConvertToNSArray(headerCells);
433 RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
434 nsTArray<RemoteAccessible*> headerCells;
435 proxy->ColHeaderCells(&headerCells);
436 return utils::ConvertToNSArray(headerCells);
442 @implementation mozOutlineAccessible
444 - (BOOL)isLayoutTablePart {
448 - (NSArray*)moxRows {
449 // Create a new array with the list of outline rows. We
450 // use pivot here to do a deep traversal of all rows nested
451 // in this outline, not just those which are direct
452 // children, since that's what VO expects.
453 NSMutableArray* allRows = [[[NSMutableArray alloc] init] autorelease];
454 Pivot p = Pivot(mGeckoAccessible);
455 OutlineRule rule = OutlineRule();
456 Accessible* firstChild = mGeckoAccessible->FirstChild();
457 Accessible* match = p.Next(firstChild, rule, true);
459 [allRows addObject:GetNativeFromGeckoAccessible(match)];
460 match = p.Next(match, rule);
465 - (NSArray*)moxColumns {
466 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
467 if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) {
468 XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc;
469 NSMutableArray* cols = [[[NSMutableArray alloc] init] autorelease];
470 // XUL trees store their columns in a group at the tree's first
471 // child. Here, we iterate over that group to get each column's
472 // native accessible and add it to our col array.
473 LocalAccessible* treeColumns = treeAcc->LocalChildAt(0);
475 uint32_t colCount = treeColumns->ChildCount();
476 for (uint32_t i = 0; i < colCount; i++) {
477 LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(i);
478 [cols addObject:GetNativeFromGeckoAccessible(treeColumnItem)];
484 // Webkit says we shouldn't expose any cols for aria-tree
485 // so we return an empty array here
489 - (NSArray*)moxSelectedRows {
490 NSMutableArray* selectedRows = [[[NSMutableArray alloc] init] autorelease];
491 NSArray* allRows = [self moxRows];
492 for (mozAccessible* row in allRows) {
493 if ([row stateWithMask:states::SELECTED] != 0) {
494 [selectedRows addObject:row];
501 - (NSString*)moxOrientation {
502 return NSAccessibilityVerticalOrientationValue;
507 @implementation mozOutlineRowAccessible
509 - (BOOL)isLayoutTablePart {
513 - (NSNumber*)moxDisclosing {
514 return @([self stateWithMask:states::EXPANDED] != 0);
517 - (void)moxSetDisclosing:(NSNumber*)disclosing {
518 // VoiceOver requires this to be settable, but doesn't
519 // require it actually affect our disclosing state.
520 // We expose the attr as settable with this method
521 // but do nothing to actually set it.
525 - (NSNumber*)moxExpanded {
526 return @([self stateWithMask:states::EXPANDED] != 0);
529 - (id)moxDisclosedByRow {
530 // According to webkit: this attr corresponds to the row
531 // that contains this row. It should be the same as the
532 // first parent that is a treeitem. If the parent is the tree
533 // itself, this should be nil. This is tricky for xul trees because
534 // all rows are direct children of the outline; they use
535 // relations to expose their heirarchy structure.
537 // first we check the relations to see if we're in a xul tree
538 // with weird row semantics
539 NSArray<mozAccessible*>* disclosingRows =
540 [self getRelationsByType:RelationType::NODE_CHILD_OF];
541 mozAccessible* disclosingRow = [disclosingRows firstObject];
544 // if we find a row from our relation check,
545 // verify it isn't the outline itself and return
547 if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) {
551 return disclosingRow;
554 mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent];
555 // otherwise, its likely we're in an aria tree, so we can use
556 // these role and subrole checks
557 if ([[parent moxRole] isEqualToString:@"AXOutline"]) {
561 if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) {
562 disclosingRow = parent;
568 - (NSNumber*)moxDisclosureLevel {
570 if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
571 groupPos = acc->GroupPosition();
572 } else if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) {
573 groupPos = proxy->GroupPosition();
575 // mac expects 0-indexed levels, but groupPos.level is 1-indexed
576 // so we subtract 1 here for levels above 0
577 return groupPos.level > 0 ? @(groupPos.level - 1) : @(groupPos.level);
580 - (NSArray*)moxDisclosedRows {
581 // According to webkit: this attr corresponds to the rows
582 // that are considered inside this row. Again, this is weird for
583 // xul trees so we have to use relations first and then fall-back
584 // to the children filter for non-xul outlines.
586 // first we check the relations to see if we're in a xul tree
587 // with weird row semantics
588 if (NSArray* disclosedRows =
589 [self getRelationsByType:RelationType::NODE_PARENT_OF]) {
590 // if we find rows from our relation check, return them here
591 return disclosedRows;
594 // otherwise, filter our children for outline rows
595 return [[self moxChildren]
596 filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL(
597 mozAccessible* child,
598 NSDictionary* bindings) {
599 return [child isKindOfClass:[mozOutlineRowAccessible class]];
603 - (NSNumber*)moxIndex {
604 id<MOXAccessible> outline =
605 [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
606 return [[moxAcc moxRole] isEqualToString:@"AXOutline"];
609 NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self];
610 return index == NSNotFound ? nil : @(index);
613 - (NSString*)moxLabel {
615 mGeckoAccessible->Name(title);
617 // XXX: When parsing outlines built with ul/lu's, we
618 // include the bullet in this description even
619 // though webkit doesn't. Not all outlines are built with
620 // ul/lu's so we can't strip the first character here.
622 return nsCocoaUtils::ToNSString(title);
632 - (int)checkedValue {
633 uint64_t state = [self
634 stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)];
636 if (state & states::CHECKABLE) {
637 if (state & states::CHECKED) {
641 if (state & states::MIXED) {
652 int checkedValue = [self checkedValue];
653 return checkedValue >= 0 ? @(checkedValue) : nil;
656 - (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
657 [super stateChanged:state isEnabled:enabled];
659 if (state & states::EXPANDED) {
660 // If the EXPANDED state is updated, fire appropriate events on the
662 [self moxPostNotification:(enabled
663 ? NSAccessibilityRowExpandedNotification
664 : NSAccessibilityRowCollapsedNotification)];
667 if (state & (states::CHECKED | states::CHECKABLE | states::MIXED)) {
668 // If the MIXED, CHECKED or CHECKABLE state changes, update the value we
669 // expose for the row, which communicates checked status.
670 [self moxPostNotification:NSAccessibilityValueChangedNotification];