Bug 1732219 - Add API for fetching the preview image. r=geckoview-reviewers,agi,mconley
[gecko.git] / accessible / mac / mozTableAccessible.mm
blob745054f7777963a1d9b0f5d7a12124e8807920b7
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"
11 #import "RotorRules.h"
13 #include "AccIterator.h"
14 #include "LocalAccessible.h"
15 #include "TableAccessible.h"
16 #include "TableCellAccessible.h"
17 #include "XULTreeAccessible.h"
18 #include "Pivot.h"
19 #include "Relation.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 {
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   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];
63       }
64     }
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];
75       }
76     }
77   }
79   return mChildren;
82 - (void)dealloc {
83   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
85   [self invalidateChildren];
86   [super dealloc];
88   NS_OBJC_END_TRY_IGNORE_BLOCK;
91 - (void)expire {
92   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
94   [self invalidateChildren];
96   mParent = nil;
98   [super expire];
100   NS_OBJC_END_TRY_IGNORE_BLOCK;
103 - (BOOL)isExpired {
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
113   if (mChildren) {
114     [mChildren release];
115     mChildren = nil;
116   }
118   NS_OBJC_END_TRY_IGNORE_BLOCK;
121 @end
123 @implementation mozTablePartAccessible
125 - (NSString*)moxTitle {
126   return @"";
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
138     // table.
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];
144     }
145   }
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];
161   }
163   return NO;
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];
175   }
177 @end
179 @implementation mozTableAccessible
181 - (void)invalidateLayoutTableCache {
182   mIsLayoutTable = eCachedBoolMiss;
185 - (BOOL)isLayoutTablePart {
186   if (mIsLayoutTable != eCachedBoolMiss) {
187     return mIsLayoutTable == eCachedTrue;
188   }
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
193     // circuit here
194     mIsLayoutTable = eCachedFalse;
195     return false;
196   }
198   bool tableGuess;
199   if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
200     tableGuess = acc->AsTable()->IsProbablyLayoutTable();
201   } else {
202     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
203     tableGuess = proxy->TableIsProbablyForLayout();
204   }
206   mIsLayoutTable = tableGuess ? eCachedTrue : eCachedFalse;
207   return tableGuess;
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];
216   }
218   [super handleAccessibleEvent:eventType];
221 - (void)dealloc {
222   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
224   [self invalidateColumns];
225   [super dealloc];
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]];
253       }]];
256 - (NSArray*)moxColumns {
257   MOZ_ASSERT(mGeckoAccessible);
259   if (mColContainers) {
260     return mColContainers;
261   }
263   mColContainers = [[NSMutableArray alloc] init];
264   uint32_t numCols = 0;
266   if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
267     numCols = acc->AsTable()->ColCount();
268   } else {
269     numCols = mGeckoAccessible->AsRemote()->TableColumnCount();
270   }
272   for (uint32_t i = 0; i < numCols; i++) {
273     mozColumnContainer* container =
274         [[mozColumnContainer alloc] initWithIndex:i andParent:self];
275     [mColContainers addObject:container];
276   }
278   return mColContainers;
281 - (NSArray*)moxUnignoredChildren {
282   if (![self isLayoutTablePart]) {
283     return [[super moxUnignoredChildren]
284         arrayByAddingObjectsFromArray:[self moxColumns]];
285   }
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();
299   } else {
300     numCols = mGeckoAccessible->AsRemote()->TableColumnCount();
301   }
303   NSMutableArray* colHeaders =
304       [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease];
306   for (uint32_t i = 0; i < numCols; i++) {
307     Accessible* cell;
308     if (table) {
309       cell = table->CellAt(0, i);
310     } else {
311       cell = mGeckoAccessible->AsRemote()->TableCellAt(0, i);
312     }
314     if (cell && cell->Role() == roles::COLUMNHEADER) {
315       mozAccessible* colHeader = GetNativeFromGeckoAccessible(cell);
316       [colHeaders addObject:colHeader];
317     }
318   }
320   return colHeaders;
323 - (id)moxCellForColumnAndRow:(NSArray*)columnAndRow {
324   if (columnAndRow == nil || [columnAndRow count] != 2) {
325     return nil;
326   }
328   uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue];
329   uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue];
331   MOZ_ASSERT(mGeckoAccessible);
333   Accessible* cell;
334   if (mGeckoAccessible->IsLocal()) {
335     cell = mGeckoAccessible->AsLocal()->AsTable()->CellAt(row, col);
336   } else {
337     cell = mGeckoAccessible->AsRemote()->TableCellAt(row, col);
338   }
340   if (!cell) {
341     return nil;
342   }
344   return GetNativeFromGeckoAccessible(cell);
347 - (void)invalidateColumns {
348   NS_OBJC_BEGIN_TRY_IGNORE_BLOCK;
349   if (mColContainers) {
350     [mColContainers release];
351     mColContainers = nil;
352   }
353   NS_OBJC_END_TRY_IGNORE_BLOCK;
356 @end
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];
365     }
366   }
368   [super handleAccessibleEvent:eventType];
371 - (NSNumber*)moxIndex {
372   mozTableAccessible* parent = (mozTableAccessible*)[self moxParent];
373   return @([[parent moxRows] indexOfObjectIdenticalTo:self]);
376 @end
378 @implementation mozTableCellAccessible
380 - (NSValue*)moxRowIndexRange {
381   MOZ_ASSERT(mGeckoAccessible);
383   if (mGeckoAccessible->IsLocal()) {
384     TableCellAccessible* cell = mGeckoAccessible->AsLocal()->AsTableCell();
385     return
386         [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())];
387   } else {
388     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
389     return [NSValue
390         valueWithRange:NSMakeRange(proxy->RowIdx(), proxy->RowExtent())];
391   }
394 - (NSValue*)moxColumnIndexRange {
395   MOZ_ASSERT(mGeckoAccessible);
397   if (mGeckoAccessible->IsLocal()) {
398     TableCellAccessible* cell = mGeckoAccessible->AsLocal()->AsTableCell();
399     return
400         [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())];
401   } else {
402     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
403     return [NSValue
404         valueWithRange:NSMakeRange(proxy->ColIdx(), proxy->ColExtent())];
405   }
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);
416   } else {
417     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
418     nsTArray<RemoteAccessible*> headerCells;
419     proxy->RowHeaderCells(&headerCells);
420     return utils::ConvertToNSArray(headerCells);
421   }
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);
432   } else {
433     RemoteAccessible* proxy = mGeckoAccessible->AsRemote();
434     nsTArray<RemoteAccessible*> headerCells;
435     proxy->ColHeaderCells(&headerCells);
436     return utils::ConvertToNSArray(headerCells);
437   }
440 @end
442 @implementation mozOutlineAccessible
444 - (BOOL)isLayoutTablePart {
445   return NO;
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);
458   while (match) {
459     [allRows addObject:GetNativeFromGeckoAccessible(match)];
460     match = p.Next(match, rule);
461   }
462   return allRows;
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);
474       if (treeColumns) {
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)];
479         }
480         return cols;
481       }
482     }
483   }
484   // Webkit says we shouldn't expose any cols for aria-tree
485   // so we return an empty array here
486   return @[];
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];
495     }
496   }
498   return selectedRows;
501 - (NSString*)moxOrientation {
502   return NSAccessibilityVerticalOrientationValue;
505 @end
507 @implementation mozOutlineRowAccessible
509 - (BOOL)isLayoutTablePart {
510   return NO;
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.
522   return;
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];
543   if (disclosingRow) {
544     // if we find a row from our relation check,
545     // verify it isn't the outline itself and return
546     // appropriately
547     if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) {
548       return nil;
549     }
551     return disclosingRow;
552   }
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"]) {
558     return nil;
559   }
561   if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) {
562     disclosingRow = parent;
563   }
565   return nil;
568 - (NSNumber*)moxDisclosureLevel {
569   GroupPos groupPos;
570   if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) {
571     groupPos = acc->GroupPosition();
572   } else if (RemoteAccessible* proxy = mGeckoAccessible->AsRemote()) {
573     groupPos = proxy->GroupPosition();
574   }
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;
592   }
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]];
600       }]];
603 - (NSNumber*)moxIndex {
604   id<MOXAccessible> outline =
605       [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) {
606         return [[moxAcc moxRole] isEqualToString:@"AXOutline"];
607       }];
609   NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self];
610   return index == NSNotFound ? nil : @(index);
613 - (NSString*)moxLabel {
614   nsAutoString title;
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);
625 enum CheckedState {
626   kUncheckable = -1,
627   kUnchecked = 0,
628   kChecked = 1,
629   kMixed = 2
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) {
638       return kChecked;
639     }
641     if (state & states::MIXED) {
642       return kMixed;
643     }
645     return kUnchecked;
646   }
648   return kUncheckable;
651 - (id)moxValue {
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
661     // outline row.
662     [self moxPostNotification:(enabled
663                                    ? NSAccessibilityRowExpandedNotification
664                                    : NSAccessibilityRowCollapsedNotification)];
665   }
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];
671   }
674 @end