gecko-dev/accessible/mac/mozTableAccessible.mm

723 строки
21 KiB
Plaintext

/* 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]]) {
// 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.
if (eventType == nsIAccessibleEvent::EVENT_REORDER ||
eventType == nsIAccessibleEvent::EVENT_OBJECT_ATTRIBUTE_CHANGED ||
eventType == nsIAccessibleEvent::EVENT_TABLE_STYLING_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 {
mIsLayoutTable = eCachedBoolMiss;
}
- (BOOL)isLayoutTablePart {
if (mIsLayoutTable != eCachedBoolMiss) {
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) {
[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 {
mozTableAccessible* tableParent = static_cast<mozTableAccessible*>(
[self moxFindAncestor:^BOOL(id curr, BOOL* stop) {
return [curr isKindOfClass:[mozTableAccessible class]];
}]);
MOZ_ASSERT(tableParent, "Table row not contained in table?");
return tableParent;
}
- (void)handleAccessibleEvent:(uint32_t)eventType {
if (eventType == nsIAccessibleEvent::EVENT_REORDER) {
[[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