/* 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" #include "AccIterator.h" #include "LocalAccessible.h" #include "mozilla/a11y/TableAccessible.h" #include "mozilla/a11y/TableCellAccessible.h" #include "nsAccessibilityService.h" #include "nsIAccessiblePivot.h" #include "XULTreeAccessible.h" #include "Pivot.h" #include "nsAccUtils.h" #include "Relation.h" using namespace mozilla; using namespace mozilla::a11y; @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]; TableAccessible* table = [mParent geckoAccessible]->AsTable(); 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]; } } 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]; } - (BOOL)isLayoutTablePart { 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; } @end @implementation mozTableAccessible - (BOOL)isLayoutTablePart { 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 return false; } // For LocalAccessible and cached RemoteAccessible, we could use // AsTable()->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()) { return acc->AsTable()->IsProbablyLayoutTable(); } RemoteAccessible* proxy = mGeckoAccessible->AsRemote(); return proxy->TableIsProbablyForLayout(); } - (void)handleAccessibleEvent:(uint32_t)eventType { if (eventType == nsIAccessibleEvent::EVENT_REORDER || eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED) { [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 @(mGeckoAccessible->AsTable()->RowCount()); } - (NSNumber*)moxColumnCount { MOZ_ASSERT(mGeckoAccessible); return @(mGeckoAccessible->AsTable()->ColCount()); } - (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; numCols = mGeckoAccessible->AsTable()->ColCount(); 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; TableAccessible* table = nullptr; table = mGeckoAccessible->AsTable(); numCols = table->ColCount(); NSMutableArray* colHeaders = [[[NSMutableArray alloc] initWithCapacity:numCols] autorelease]; for (uint32_t i = 0; i < numCols; i++) { Accessible* cell = table->CellAt(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 = mGeckoAccessible->AsTable()->CellAt(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( [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); TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); return [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())]; } - (NSValue*)moxColumnIndexRange { MOZ_ASSERT(mGeckoAccessible); TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); return [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())]; } - (NSArray*)moxRowHeaderUIElements { MOZ_ASSERT(mGeckoAccessible); TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); AutoTArray headerCells; if (cell) { cell->RowHeaderCells(&headerCells); } return utils::ConvertToNSArray(headerCells); } - (NSArray*)moxColumnHeaderUIElements { MOZ_ASSERT(mGeckoAccessible); TableCellAccessible* cell = mGeckoAccessible->AsTableCell(); AutoTArray headerCells; if (cell) { cell->ColHeaderCells(&headerCells); } return utils::ConvertToNSArray(headerCells); } @end /** * This rule matches all accessibles with roles::OUTLINEITEM. If * outlines are nested, it ignores the nested subtree and returns * only items which are descendants of the primary outline. */ class OutlineRule : public PivotRule { public: uint16_t Match(Accessible* aAcc) override { uint16_t result = nsIAccessibleTraversalRule::FILTER_IGNORE; if (nsAccUtils::MustPrune(aAcc)) { result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } if (![GetNativeFromGeckoAccessible(aAcc) isAccessibilityElement]) { return result; } if (aAcc->Role() == roles::OUTLINE) { // if the accessible is an outline, we ignore all children result |= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE; } else if (aAcc->Role() == roles::OUTLINEITEM) { // if the accessible is not an outline item, we match here result |= nsIAccessibleTraversalRule::FILTER_MATCH; } return result; } }; @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* 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 outline = [self moxFindAncestor:^BOOL(id 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); } - (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