no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / accessible / mac / mozTableAccessible.mm
bloba179780a8136eec17b5c59b46d67b056e8772a13
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/. */
8 #import "mozTableAccessible.h"
9 #import "nsCocoaUtils.h"
10 #import "MacUtils.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"
19 #include "Pivot.h"
20 #include "nsAccUtils.h"
21 #include "Relation.h"
23 using namespace mozilla;
24 using namespace mozilla::a11y;
26 @implementation mozColumnContainer
28 - (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent {
29   self = [super init];
30   mIndex = aIndex;
31   mParent = aParent;
32   return self;
35 - (NSString*)moxRole {
36   return NSAccessibilityColumnRole;
39 - (NSString*)moxRoleDescription {
40   return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil);
43 - (mozAccessible*)moxParent {
44   return mParent;
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];
61     }
62   }
64   return mChildren;
67 - (void)dealloc {
68   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
70   [self invalidateChildren];
71   [super dealloc];
73   NS_OBJC_END_TRY_IGNORE_BLOCK;
76 - (void)expire {
77   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
79   [self invalidateChildren];
81   mParent = nil;
83   [super expire];
85   NS_OBJC_END_TRY_IGNORE_BLOCK;
88 - (BOOL)isExpired {
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
98   if (mChildren) {
99     [mChildren release];
100     mChildren = nil;
101   }
103   NS_OBJC_END_TRY_IGNORE_BLOCK;
106 @end
108 @implementation mozTablePartAccessible
110 - (NSString*)moxTitle {
111   return @"";
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];
124   }
126   return NO;
128 @end
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
136     // circuit here
137     return false;
138   }
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();
152 - (void)handleAccessibleEvent:(uint32_t)eventType {
153   if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
154       eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) {
155     [self invalidateColumns];
156   }
158   [super handleAccessibleEvent:eventType];
161 - (void)dealloc {
162   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
164   [self invalidateColumns];
165   [super dealloc];
167   NS_OBJC_END_TRY_IGNORE_BLOCK;
170 - (void)expire {
171   [self invalidateColumns];
172   [super expire];
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.
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   }
212   return rows;
215 - (NSArray*)moxColumns {
216   MOZ_ASSERT(mGeckoAccessible);
218   if (mColContainers) {
219     return mColContainers;
220   }
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];
230   }
232   return mColContainers;
235 - (NSArray*)moxUnignoredChildren {
236   if (![self isLayoutTablePart]) {
237     return [[super moxUnignoredChildren]
238         arrayByAddingObjectsFromArray:[self moxColumns]];
239   }
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];
260     }
261   }
263   return colHeaders;
266 - (id)moxCellForColumnAndRow:(NSArray*)columnAndRow {
267   if (columnAndRow == nil || [columnAndRow count] != 2) {
268     return nil;
269   }
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);
277   if (!cell) {
278     return nil;
279   }
281   return GetNativeFromGeckoAccessible(cell);
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;
296 @end
298 @interface mozTableRowAccessible ()
299 - (mozTableAccessible*)getTableParent;
300 @end
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.
310           // Stop searching.
311           *stop = YES;
312         }
313         return [curr isKindOfClass:[mozTableAccessible class]];
314       }]);
316   return [tableParent isKindOfClass:[mozTableAccessible class]] ? tableParent
317                                                                 : nil;
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   }
328   [super handleAccessibleEvent:eventType];
331 - (NSNumber*)moxIndex {
332   return @([[[self getTableParent] moxRows] indexOfObjectIdenticalTo:self]);
335 @end
337 @implementation mozTableCellAccessible
339 - (NSValue*)moxRowIndexRange {
340   MOZ_ASSERT(mGeckoAccessible);
342   TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
343   return
344       [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())];
347 - (NSValue*)moxColumnIndexRange {
348   MOZ_ASSERT(mGeckoAccessible);
350   TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
351   return
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   if (cell) {
361     cell->RowHeaderCells(&headerCells);
362   }
363   return utils::ConvertToNSArray(headerCells);
366 - (NSArray*)moxColumnHeaderUIElements {
367   MOZ_ASSERT(mGeckoAccessible);
369   TableCellAccessible* cell = mGeckoAccessible->AsTableCell();
370   AutoTArray<Accessible*, 10> headerCells;
371   if (cell) {
372     cell->ColHeaderCells(&headerCells);
373   }
374   return utils::ConvertToNSArray(headerCells);
377 @end
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;
389     if (nsAccUtils::MustPrune(aAcc)) {
390       result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
391     }
393     if (![GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) {
394       return result;
395     }
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     }
405     return result;
406   }
409 @implementation mozOutlineAccessible
411 - (BOOL)isLayoutTablePart {
412   return NO;
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;
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 @[];
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   }
465   return selectedRows;
468 - (NSString*)moxOrientation {
469   return NSAccessibilityVerticalOrientationValue;
472 @end
474 @implementation mozOutlineRowAccessible
476 - (BOOL)isLayoutTablePart {
477   return NO;
480 - (NSNumber*)moxDisclosing {
481   return @([self stateWithMask:states::EXPANDED] != 0);
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;
492 - (NSNumber*)moxExpanded {
493   return @([self stateWithMask:states::EXPANDED] != 0);
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.
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];
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     }
518     return disclosingRow;
519   }
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   }
528   if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) {
529     disclosingRow = parent;
530   }
532   return nil;
535 - (NSNumber*)moxDisclosureLevel {
536   GroupPos groupPos = mGeckoAccessible->GroupPosition();
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);
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.
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   }
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       }]];
566 - (NSNumber*)moxIndex {
567   id<MOXAccessible> outline =
568       [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
569         return [[moxAcc moxRole] isEqualToString:@"AXOutline"];
570       }];
572   NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self];
573   return index == NSNotFound ? nil : @(index);
576 - (NSString*)moxLabel {
577   nsAutoString title;
578   mGeckoAccessible->Name(title);
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.
585   return nsCocoaUtils::ToNSString(title);
588 - (int)checkedValue {
589   uint64_t state = [self
590       stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)];
592   if (state & states::CHECKABLE) {
593     if (state & states::CHECKED) {
594       return kChecked;
595     }
597     if (state & states::MIXED) {
598       return kMixed;
599     }
601     return kUnchecked;
602   }
604   return kUncheckable;
607 - (id)moxValue {
608   int checkedValue = [self checkedValue];
609   return checkedValue >= 0 ? @(checkedValue) : nil;
612 - (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled {
613   [super stateChanged:state isEnabled:enabled];
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   }
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   }
630 @end