/* 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 "Accessible.h" #include "TableAccessible.h" #include "TableCellAccessible.h" #include "XULTreeAccessible.h" #include "Pivot.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]; if (Accessible* acc = [mParent geckoAccessible].AsAccessible()) { TableAccessible* table = acc->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]; } } } else if (ProxyAccessible* proxy = [mParent geckoAccessible].AsProxy()) { uint32_t numRows = proxy->TableRowCount(); for (uint32_t j = 0; j < numRows; j++) { ProxyAccessible* 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_ABORT_BLOCK; [self invalidateChildren]; [super dealloc]; NS_OBJC_END_TRY_ABORT_BLOCK; } - (void)expire { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [self invalidateChildren]; mParent = nil; [super expire]; NS_OBJC_END_TRY_ABORT_BLOCK; } - (BOOL)isExpired { MOZ_ASSERT((mChildren == nil && mParent == nil) == mIsExpired); return [super isExpired]; } - (void)invalidateChildren { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; // make room for new children if (mChildren) { [mChildren release]; mChildren = nil; } NS_OBJC_END_TRY_ABORT_BLOCK; } @end @implementation mozTablePartAccessible - (NSString*)moxTitle { return @""; } - (NSString*)moxRole { return [self isLayoutTablePart] ? NSAccessibilityGroupRole : [super moxRole]; } - (BOOL)isLayoutTablePart { if (Accessible* acc = mGeckoAccessible.AsAccessible()) { while (acc) { if (acc->Role() == roles::TREE_TABLE) { return false; } if (acc->IsTable()) { return acc->AsTable()->IsProbablyLayoutTable(); } acc = acc->Parent(); } return false; } if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { while (proxy) { if (proxy->Role() == roles::TREE_TABLE) { return false; } if (proxy->IsTable()) { return proxy->TableIsProbablyForLayout(); } proxy = proxy->Parent(); } } return false; } @end @implementation mozTableAccessible - (void)handleAccessibleEvent:(uint32_t)eventType { if (eventType == nsIAccessibleEvent::EVENT_REORDER) { [self invalidateColumns]; } [super handleAccessibleEvent:eventType]; } - (void)dealloc { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; [self invalidateColumns]; [super dealloc]; NS_OBJC_END_TRY_ABORT_BLOCK; } - (NSNumber*)moxRowCount { MOZ_ASSERT(!mGeckoAccessible.IsNull()); return mGeckoAccessible.IsAccessible() ? @(mGeckoAccessible.AsAccessible()->AsTable()->RowCount()) : @(mGeckoAccessible.AsProxy()->TableRowCount()); } - (NSNumber*)moxColumnCount { MOZ_ASSERT(!mGeckoAccessible.IsNull()); return mGeckoAccessible.IsAccessible() ? @(mGeckoAccessible.AsAccessible()->AsTable()->ColCount()) : @(mGeckoAccessible.AsProxy()->TableColumnCount()); } - (NSArray*)moxRows { // Create a new array with the list of table rows. return [[self moxChildren] filteredArrayUsingPredicate:[NSPredicate predicateWithBlock:^BOOL( mozAccessible* child, NSDictionary* bindings) { return [child isKindOfClass:[mozTableRowAccessible class]]; }]]; } - (NSArray*)moxColumns { MOZ_ASSERT(!mGeckoAccessible.IsNull()); if (mColContainers) { return mColContainers; } mColContainers = [[NSMutableArray alloc] init]; uint32_t numCols = 0; if (Accessible* acc = mGeckoAccessible.AsAccessible()) { numCols = acc->AsTable()->ColCount(); } else { numCols = mGeckoAccessible.AsProxy()->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.IsNull()); uint32_t numCols = 0; TableAccessible* table = nullptr; if (Accessible* acc = mGeckoAccessible.AsAccessible()) { table = mGeckoAccessible.AsAccessible()->AsTable(); numCols = table->ColCount(); } else { numCols = mGeckoAccessible.AsProxy()->TableColumnCount(); } NSMutableArray* colHeaders = [[NSMutableArray alloc] initWithCapacity:numCols]; for (uint32_t i = 0; i < numCols; i++) { AccessibleOrProxy cell; if (table) { cell = table->CellAt(0, i); } else { cell = mGeckoAccessible.AsProxy()->TableCellAt(0, i); } if (!cell.IsNull() && 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.IsNull()); AccessibleOrProxy cell; if (mGeckoAccessible.IsAccessible()) { cell = mGeckoAccessible.AsAccessible()->AsTable()->CellAt(row, col); } else { cell = mGeckoAccessible.AsProxy()->TableCellAt(row, col); } if (cell.IsNull()) { return nil; } return GetNativeFromGeckoAccessible(cell); } - (void)invalidateColumns { NS_OBJC_BEGIN_TRY_ABORT_BLOCK; if (mColContainers) { [mColContainers release]; mColContainers = nil; } NS_OBJC_END_TRY_ABORT_BLOCK; } @end @implementation mozTableRowAccessible - (void)handleAccessibleEvent:(uint32_t)eventType { if (eventType == nsIAccessibleEvent::EVENT_REORDER) { id parent = [self moxParent]; if ([parent isKindOfClass:[mozTableAccessible class]]) { [parent invalidateColumns]; } } [super handleAccessibleEvent:eventType]; } - (NSNumber*)moxIndex { mozTableAccessible* parent = (mozTableAccessible*)[self moxParent]; return @([[parent moxRows] indexOfObjectIdenticalTo:self]); } @end @implementation mozTableCellAccessible - (NSValue*)moxRowIndexRange { MOZ_ASSERT(!mGeckoAccessible.IsNull()); if (mGeckoAccessible.IsAccessible()) { TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); return [NSValue valueWithRange:NSMakeRange(cell->RowIdx(), cell->RowExtent())]; } else { ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); return [NSValue valueWithRange:NSMakeRange(proxy->RowIdx(), proxy->RowExtent())]; } } - (NSValue*)moxColumnIndexRange { MOZ_ASSERT(!mGeckoAccessible.IsNull()); if (mGeckoAccessible.IsAccessible()) { TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); return [NSValue valueWithRange:NSMakeRange(cell->ColIdx(), cell->ColExtent())]; } else { ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); return [NSValue valueWithRange:NSMakeRange(proxy->ColIdx(), proxy->ColExtent())]; } } - (NSArray*)moxRowHeaderUIElements { MOZ_ASSERT(!mGeckoAccessible.IsNull()); if (mGeckoAccessible.IsAccessible()) { TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); AutoTArray headerCells; cell->RowHeaderCells(&headerCells); return utils::ConvertToNSArray(headerCells); } else { ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); nsTArray headerCells; proxy->RowHeaderCells(&headerCells); return utils::ConvertToNSArray(headerCells); } } - (NSArray*)moxColumnHeaderUIElements { MOZ_ASSERT(!mGeckoAccessible.IsNull()); if (mGeckoAccessible.IsAccessible()) { TableCellAccessible* cell = mGeckoAccessible.AsAccessible()->AsTableCell(); AutoTArray headerCells; cell->ColHeaderCells(&headerCells); return utils::ConvertToNSArray(headerCells); } else { ProxyAccessible* proxy = mGeckoAccessible.AsProxy(); nsTArray headerCells; proxy->ColHeaderCells(&headerCells); return utils::ConvertToNSArray(headerCells); } } @end @implementation mozOutlineAccessible - (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]; Pivot p = Pivot(mGeckoAccessible); OutlineRule rule = OutlineRule(); AccessibleOrProxy firstChild = mGeckoAccessible.FirstChild(); AccessibleOrProxy match = p.Next(firstChild, rule, true); while (!match.IsNull()) { [allRows addObject:GetNativeFromGeckoAccessible(match)]; match = p.Next(match, rule); } return allRows; } - (NSArray*)moxColumns { if (Accessible* acc = mGeckoAccessible.AsAccessible()) { if (acc->IsContent() && acc->GetContent()->IsXULElement(nsGkAtoms::tree)) { XULTreeAccessible* treeAcc = (XULTreeAccessible*)acc; NSMutableArray* cols = [[NSMutableArray alloc] init]; // 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. Accessible* treeColumns = treeAcc->GetChildAt(0); if (treeColumns) { uint32_t colCount = treeColumns->ChildCount(); for (uint32_t i = 0; i < colCount; i++) { Accessible* treeColumnItem = treeColumns->GetChildAt(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]; 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; if (Accessible* acc = mGeckoAccessible.AsAccessible()) { groupPos = acc->GroupPosition(); } else if (ProxyAccessible* proxy = mGeckoAccessible.AsProxy()) { groupPos = proxy->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 { mozAccessible* parent = (mozAccessible*)[self moxUnignoredParent]; while (parent) { if ([[parent moxRole] isEqualToString:@"AXOutline"]) { break; } parent = (mozAccessible*)[parent moxUnignoredParent]; } NSUInteger index = [[(mozOutlineAccessible*)parent moxRows] indexOfObjectIdenticalTo:self]; return index == NSNotFound ? nil : @(index); } - (NSString*)moxLabel { nsAutoString title; if (Accessible* acc = mGeckoAccessible.AsAccessible()) { acc->Name(title); } else { mGeckoAccessible.AsProxy()->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); } - (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)]; } } @end