diff options
Diffstat (limited to '')
-rw-r--r-- | accessible/mac/mozTableAccessible.mm | 739 |
1 files changed, 739 insertions, 0 deletions
diff --git a/accessible/mac/mozTableAccessible.mm b/accessible/mac/mozTableAccessible.mm new file mode 100644 index 0000000000..48cd8ba11f --- /dev/null +++ b/accessible/mac/mozTableAccessible.mm @@ -0,0 +1,739 @@ +/* clang-format off */ +/* -*- Mode: Objective-C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* clang-format on */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +#import "mozTableAccessible.h" +#import "nsCocoaUtils.h" +#import "MacUtils.h" +#import "RotorRules.h" + +#include "AccIterator.h" +#include "LocalAccessible.h" +#include "mozilla/a11y/TableAccessibleBase.h" +#include "mozilla/a11y/TableCellAccessibleBase.h" +#include "mozilla/StaticPrefs_accessibility.h" +#include "XULTreeAccessible.h" +#include "Pivot.h" +#include "Relation.h" + +using namespace mozilla; +using namespace mozilla::a11y; + +enum CachedBool { eCachedBoolMiss, eCachedTrue, eCachedFalse }; + +@implementation mozColumnContainer + +- (id)initWithIndex:(uint32_t)aIndex andParent:(mozAccessible*)aParent { + self = [super init]; + mIndex = aIndex; + mParent = aParent; + return self; +} + +- (NSString*)moxRole { + return NSAccessibilityColumnRole; +} + +- (NSString*)moxRoleDescription { + return NSAccessibilityRoleDescription(NSAccessibilityColumnRole, nil); +} + +- (mozAccessible*)moxParent { + return mParent; +} + +- (NSArray*)moxUnignoredChildren { + if (mChildren) return mChildren; + + mChildren = [[NSMutableArray alloc] init]; + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + [mParent geckoAccessible]->IsLocal()) { + TableAccessibleBase* table = [mParent geckoAccessible]->AsTableBase(); + MOZ_ASSERT(table, "Got null table when fetching column children!"); + uint32_t numRows = table->RowCount(); + + for (uint32_t j = 0; j < numRows; j++) { + Accessible* cell = table->CellAt(j, mIndex); + mozAccessible* nativeCell = + cell ? GetNativeFromGeckoAccessible(cell) : nil; + if ([nativeCell isAccessibilityElement]) { + [mChildren addObject:nativeCell]; + } + } + + } else if (RemoteAccessible* proxy = [mParent geckoAccessible]->AsRemote()) { + uint32_t numRows = proxy->TableRowCount(); + + for (uint32_t j = 0; j < numRows; j++) { + RemoteAccessible* cell = proxy->TableCellAt(j, mIndex); + mozAccessible* nativeCell = + cell ? GetNativeFromGeckoAccessible(cell) : nil; + if ([nativeCell isAccessibilityElement]) { + [mChildren addObject:nativeCell]; + } + } + } + + return mChildren; +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [self invalidateChildren]; + [super dealloc]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +- (void)expire { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [self invalidateChildren]; + + mParent = nil; + + [super expire]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +- (BOOL)isExpired { + MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired); + + return [super isExpired]; +} + +- (void)invalidateChildren { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + // make room for new children + if (mChildren) { + [mChildren release]; + mChildren = nil; + } + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +@end + +@implementation mozTablePartAccessible + +- (NSString*)moxTitle { + return @""; +} + +- (NSString*)moxRole { + return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole]; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (![self isKindOfClass:[mozTableAccessible class]] && + !StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // If we are not a table, we are a cell or a row. + // Check to see if the event we're handling should + // invalidate the mIsLayoutTable cache on our parent + // table. Only do this when the core cache is off, because + // we don't use the platform cache when its on. + if (eventType == nsIAccessibleEvent::EVENT_REORDER || + eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) { + // Invalidate the cache on our parent table + [self invalidateLayoutTableCache]; + } + } + + [super handleAccessibleEvent:eventType]; +} + +- (BOOL)isLayoutTablePart { + // mIsLayoutTable is a cache on each mozTableAccessible that stores + // the previous result of calling IsProbablyLayoutTable in core. To see + // how/when the cache is invalidated, view handleAccessibleEvent. + // The cache contains one of three values from the CachedBool enum + // defined in mozTableAccessible.h + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + if ([parent isKindOfClass:[mozTablePartAccessible class]]) { + return [(mozTablePartAccessible*)parent isLayoutTablePart]; + } else if ([parent isKindOfClass:[mozOutlineAccessible class]]) { + return [(mozOutlineAccessible*)parent isLayoutTablePart]; + } + + return NO; +} + +- (void)invalidateLayoutTableCache { + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + if ([parent isKindOfClass:[mozTablePartAccessible class]]) { + // We do this to prevent dispatching invalidateLayoutTableCache + // on outlines or outline parts. This is possible here because + // outline rows subclass table rows, which are a table part. + // This means `parent` could be an outline, and there is no + // cache on outlines to invalidate. + [(mozTablePartAccessible*)parent invalidateLayoutTableCache]; + } +} +@end + +@implementation mozTableAccessible + +- (void)invalidateLayoutTableCache { + MOZ_ASSERT(!StaticPrefs::accessibility_cache_enabled_AtStartup(), + "If the core cache is enabled we shouldn't be maintaining the " + "platform table cache!"); + mIsLayoutTable = eCachedBoolMiss; +} + +- (BOOL)isLayoutTablePart { + if (mIsLayoutTable != eCachedBoolMiss && + !StaticPrefs::accessibility_cache_enabled_AtStartup()) { + // Only use the platform cache if the core cache is not on + return mIsLayoutTable == eCachedTrue; + } + + if (mGeckoAccessible->Role() == roles::TREE_TABLE) { + // tree tables are never layout tables, and we shouldn't + // query IsProbablyLayoutTable() on them, so we short + // circuit here + mIsLayoutTable = eCachedFalse; + return false; + } + + bool tableGuess; + // For LocalAccessible and cached RemoteAccessible, we could use + // AsTableBase()->IsProbablyLayoutTable(). However, if the cache is enabled, + // that would build the table cache, which is pointless for layout tables on + // Mac because layout tables are AXGroups and do not expose table properties + // like AXRows, AXColumns, etc. + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + tableGuess = acc->AsTable()->IsProbablyLayoutTable(); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + tableGuess = proxy->TableIsProbablyForLayout(); + } + + mIsLayoutTable = tableGuess ? eCachedTrue : eCachedFalse; + return tableGuess; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (eventType == nsIAccessibleEvent::EVENT_REORDER || + eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED || + eventType == nsIAccessibleEvent::EVENT_TABLE_STYLING_CHANGED) { + if (!StaticPrefs::accessibility_cache_enabled_AtStartup()) { + [self invalidateLayoutTableCache]; + } + [self invalidateColumns]; + } + + [super handleAccessibleEvent:eventType]; +} + +- (void)dealloc { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + + [self invalidateColumns]; + [super dealloc]; + + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +- (void)expire { + [self invalidateColumns]; + [super expire]; +} + +- (NSNumber*)moxRowCount { + MOZ_ASSERT(mGeckoAccessible); + + return (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) + ? @(mGeckoAccessible->AsTableBase()->RowCount()) + : @(mGeckoAccessible->AsRemote()->TableRowCount()); +} + +- (NSNumber*)moxColumnCount { + MOZ_ASSERT(mGeckoAccessible); + + return (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) + ? @(mGeckoAccessible->AsTableBase()->ColCount()) + : @(mGeckoAccessible->AsRemote()->TableColumnCount()); +} + +- (NSArray*)moxRows { + // Create a new array with the list of table rows. + NSArray* children = [self moxChildren]; + NSMutableArray* rows = [[[NSMutableArray alloc] init] autorelease]; + for (mozAccessible* curr : children) { + if ([curr isKindOfClass:[mozTableRowAccessible class]]) { + [rows addObject:curr]; + } else if ([[curr moxRole] isEqualToString:@"AXGroup"]) { + // Plain thead/tbody elements are removed from the core a11y tree and + // replaced with their subtree, but thead/tbody elements with click + // handlers are not -- they remain as groups. We need to expose any + // rows they contain as rows of the parent table. + [rows + addObjectsFromArray:[[curr moxChildren] + filteredArrayUsingPredicate: + [NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + return [child + isKindOfClass:[mozTableRowAccessible + class]]; + }]]]; + } + } + + return rows; +} + +- (NSArray*)moxColumns { + MOZ_ASSERT(mGeckoAccessible); + + if (mColContainers) { + return mColContainers; + } + + mColContainers = [[NSMutableArray alloc] init]; + uint32_t numCols = 0; + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + numCols = mGeckoAccessible->AsTableBase()->ColCount(); + } else { + numCols = mGeckoAccessible->AsRemote()->TableColumnCount(); + } + + for (uint32_t i = 0; i < numCols; i++) { + mozColumnContainer* container = + [[mozColumnContainer alloc] initWithIndex:i andParent:self]; + [mColContainers addObject:container]; + } + + return mColContainers; +} + +- (NSArray*)moxUnignoredChildren { + if (![self isLayoutTablePart]) { + return [[super moxUnignoredChildren] + arrayByAddingObjectsFromArray:[self moxColumns]]; + } + + return [super moxUnignoredChildren]; +} + +- (NSArray*)moxColumnHeaderUIElements { + MOZ_ASSERT(mGeckoAccessible); + + uint32_t numCols = 0; + TableAccessibleBase* table = nullptr; + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + table = mGeckoAccessible->AsTableBase(); + numCols = table->ColCount(); + } else { + numCols = mGeckoAccessible->AsRemote()->TableColumnCount(); + } + + NSMutableArray* colHeaders = + [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease]; + + for (uint32_t i = 0; i < numCols; i++) { + Accessible* cell; + if (table) { + cell = table->CellAt(0, i); + } else { + cell = mGeckoAccessible->AsRemote()->TableCellAt(0, i); + } + + if (cell && cell->Role() == roles::COLUMNHEADER) { + mozAccessible* colHeader = GetNativeFromGeckoAccessible(cell); + [colHeaders addObject:colHeader]; + } + } + + return colHeaders; +} + +- (id)moxCellForColumnAndRow:(NSArray*)columnAndRow { + if (columnAndRow == nil || [columnAndRow count] != 2) { + return nil; + } + + uint32_t col = [[columnAndRow objectAtIndex:0] unsignedIntValue]; + uint32_t row = [[columnAndRow objectAtIndex:1] unsignedIntValue]; + + MOZ_ASSERT(mGeckoAccessible); + + Accessible* cell; + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + cell = mGeckoAccessible->AsTableBase()->CellAt(row, col); + } else { + cell = mGeckoAccessible->AsRemote()->TableCellAt(row, col); + } + + if (!cell) { + return nil; + } + + return GetNativeFromGeckoAccessible(cell); +} + +- (void)invalidateColumns { + NS_OBJC_BEGIN_TRY_IGNORE_BLOCK; + if (mColContainers) { + for (mozColumnContainer* col in mColContainers) { + [col expire]; + } + [mColContainers release]; + mColContainers = nil; + } + NS_OBJC_END_TRY_IGNORE_BLOCK; +} + +@end + +@interface mozTableRowAccessible () +- (mozTableAccessible*)getTableParent; +@end + +@implementation mozTableRowAccessible + +- (mozTableAccessible*)getTableParent { + id tableParent = static_cast<mozTableAccessible*>( + [self moxFindAncestor:^BOOL(id curr, BOOL* stop) { + if ([curr isKindOfClass:[mozOutlineAccessible class]]) { + // Outline rows are a kind of table row, so it's possible + // we're trying to call getTableParent on an outline row here. + // Stop searching. + *stop = YES; + } + return [curr isKindOfClass:[mozTableAccessible class]]; + }]); + + return [tableParent isKindOfClass:[mozTableAccessible class]] ? tableParent + : nil; +} + +- (void)handleAccessibleEvent:(uint32_t)eventType { + if (eventType == nsIAccessibleEvent::EVENT_REORDER) { + // It is possible for getTableParent to return nil if we're + // handling a reorder on an outilne row. Outlines don't have + // columns, so there's nothing to do here and this will no-op. + [[self getTableParent] invalidateColumns]; + } + + [super handleAccessibleEvent:eventType]; +} + +- (NSNumber*)moxIndex { + return @([[[self getTableParent] moxRows] indexOfObjectIdenticalTo:self]); +} + +@end + +@implementation mozTableCellAccessible + +- (NSValue*)moxRowIndexRange { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + return + [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())]; + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + return [NSValue + valueWithRange:NSMakeRange(proxy->RowIdx(), proxy->RowExtent())]; + } +} + +- (NSValue*)moxColumnIndexRange { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + return + [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())]; + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + return [NSValue + valueWithRange:NSMakeRange(proxy->ColIdx(), proxy->ColExtent())]; + } +} + +- (NSArray*)moxRowHeaderUIElements { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + AutoTArray<Accessible*, 10> headerCells; + cell->RowHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + nsTArray<RemoteAccessible*> headerCells; + proxy->RowHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } +} + +- (NSArray*)moxColumnHeaderUIElements { + MOZ_ASSERT(mGeckoAccessible); + + if (StaticPrefs::accessibility_cache_enabled_AtStartup() || + mGeckoAccessible->IsLocal()) { + TableCellAccessibleBase* cell = mGeckoAccessible->AsTableCellBase(); + AutoTArray<Accessible*, 10> headerCells; + cell->ColHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } else { + RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); + nsTArray<RemoteAccessible*> headerCells; + proxy->ColHeaderCells(&headerCells); + return utils::ConvertToNSArray(headerCells); + } +} + +@end + +@implementation mozOutlineAccessible + +- (BOOL)isLayoutTablePart { + return NO; +} + +- (NSArray*)moxRows { + // Create a new array with the list of outline rows. We + // use pivot here to do a deep traversal of all rows nested + // in this outline, not just those which are direct + // children, since that's what VO expects. + NSMutableArray* allRows = [[[NSMutableArray alloc] init] autorelease]; + Pivot p = Pivot(mGeckoAccessible); + OutlineRule rule = OutlineRule(); + Accessible* firstChild = mGeckoAccessible->FirstChild(); + Accessible* match = p.Next(firstChild, rule, true); + while (match) { + [allRows addObject:GetNativeFromGeckoAccessible(match)]; + match = p.Next(match, rule); + } + return allRows; +} + +- (NSArray*)moxColumns { + if (LocalAccessible* acc = mGeckoAccessible->AsLocal()) { + if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) { + XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc; + NSMutableArray* cols = [[[NSMutableArray alloc] init] autorelease]; + // XUL trees store their columns in a group at the tree's first + // child. Here, we iterate over that group to get each column's + // native accessible and add it to our col array. + LocalAccessible* treeColumns = treeAcc->LocalChildAt(0); + if (treeColumns) { + uint32_t colCount = treeColumns->ChildCount(); + for (uint32_t i = 0; i < colCount; i++) { + LocalAccessible* treeColumnItem = treeColumns->LocalChildAt(i); + [cols addObject:GetNativeFromGeckoAccessible(treeColumnItem)]; + } + return cols; + } + } + } + // Webkit says we shouldn't expose any cols for aria-tree + // so we return an empty array here + return @[]; +} + +- (NSArray*)moxSelectedRows { + NSMutableArray* selectedRows = [[[NSMutableArray alloc] init] autorelease]; + NSArray* allRows = [self moxRows]; + for (mozAccessible* row in allRows) { + if ([row stateWithMask:states::SELECTED] != 0) { + [selectedRows addObject:row]; + } + } + + return selectedRows; +} + +- (NSString*)moxOrientation { + return NSAccessibilityVerticalOrientationValue; +} + +@end + +@implementation mozOutlineRowAccessible + +- (BOOL)isLayoutTablePart { + return NO; +} + +- (NSNumber*)moxDisclosing { + return @([self stateWithMask:states::EXPANDED] != 0); +} + +- (void)moxSetDisclosing:(NSNumber*)disclosing { + // VoiceOver requires this to be settable, but doesn't + // require it actually affect our disclosing state. + // We expose the attr as settable with this method + // but do nothing to actually set it. + return; +} + +- (NSNumber*)moxExpanded { + return @([self stateWithMask:states::EXPANDED] != 0); +} + +- (id)moxDisclosedByRow { + // According to webkit: this attr corresponds to the row + // that contains this row. It should be the same as the + // first parent that is a treeitem. If the parent is the tree + // itself, this should be nil. This is tricky for xul trees because + // all rows are direct children of the outline; they use + // relations to expose their heirarchy structure. + + // first we check the relations to see if we're in a xul tree + // with weird row semantics + NSArray<mozAccessible*>* disclosingRows = + [self getRelationsByType:RelationType::NODE_CHILD_OF]; + mozAccessible* disclosingRow = [disclosingRows firstObject]; + + if (disclosingRow) { + // if we find a row from our relation check, + // verify it isn't the outline itself and return + // appropriately + if ([[disclosingRow moxRole] isEqualToString:@"AXOutline"]) { + return nil; + } + + return disclosingRow; + } + + mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; + // otherwise, its likely we're in an aria tree, so we can use + // these role and subrole checks + if ([[parent moxRole] isEqualToString:@"AXOutline"]) { + return nil; + } + + if ([[parent moxSubrole] isEqualToString:@"AXOutlineRow"]) { + disclosingRow = parent; + } + + return nil; +} + +- (NSNumber*)moxDisclosureLevel { + GroupPos groupPos = mGeckoAccessible->GroupPosition(); + + // mac expects 0-indexed levels, but groupPos.level is 1-indexed + // so we subtract 1 here for levels above 0 + return groupPos.level > 0 ? @(groupPos.level - 1) : @(groupPos.level); +} + +- (NSArray*)moxDisclosedRows { + // According to webkit: this attr corresponds to the rows + // that are considered inside this row. Again, this is weird for + // xul trees so we have to use relations first and then fall-back + // to the children filter for non-xul outlines. + + // first we check the relations to see if we're in a xul tree + // with weird row semantics + if (NSArray* disclosedRows = + [self getRelationsByType:RelationType::NODE_PARENT_OF]) { + // if we find rows from our relation check, return them here + return disclosedRows; + } + + // otherwise, filter our children for outline rows + return [[self moxChildren] + filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( + mozAccessible* child, + NSDictionary* bindings) { + return [child isKindOfClass:[mozOutlineRowAccessible class]]; + }]]; +} + +- (NSNumber*)moxIndex { + id<MOXAccessible> outline = + [self moxFindAncestor:^BOOL(id<MOXAccessible> moxAcc, BOOL* stop) { + return [[moxAcc moxRole] isEqualToString:@"AXOutline"]; + }]; + + NSUInteger index = [[outline moxRows] indexOfObjectIdenticalTo:self]; + return index == NSNotFound ? nil : @(index); +} + +- (NSString*)moxLabel { + nsAutoString title; + mGeckoAccessible->Name(title); + + // XXX: When parsing outlines built with ul/lu's, we + // include the bullet in this description even + // though webkit doesn't. Not all outlines are built with + // ul/lu's so we can't strip the first character here. + + return nsCocoaUtils::ToNSString(title); +} + +enum CheckedState { + kUncheckable = -1, + kUnchecked = 0, + kChecked = 1, + kMixed = 2 +}; + +- (int)checkedValue { + uint64_t state = [self + stateWithMask:(states::CHECKABLE | states::CHECKED | states::MIXED)]; + + if (state & states::CHECKABLE) { + if (state & states::CHECKED) { + return kChecked; + } + + if (state & states::MIXED) { + return kMixed; + } + + return kUnchecked; + } + + return kUncheckable; +} + +- (id)moxValue { + int checkedValue = [self checkedValue]; + return checkedValue >= 0 ? @(checkedValue) : nil; +} + +- (void)stateChanged:(uint64_t)state isEnabled:(BOOL)enabled { + [super stateChanged:state isEnabled:enabled]; + + if (state & states::EXPANDED) { + // If the EXPANDED state is updated, fire appropriate events on the + // outline row. + [self moxPostNotification:(enabled + ? NSAccessibilityRowExpandedNotification + : NSAccessibilityRowCollapsedNotification)]; + } + + if (state & (states::CHECKED | states::CHECKABLE | states::MIXED)) { + // If the MIXED, CHECKED or CHECKABLE state changes, update the value we + // expose for the row, which communicates checked status. + [self moxPostNotification:NSAccessibilityValueChangedNotification]; + } +} + +@end |