зеркало из https://github.com/mozilla/gecko-dev.git
475 строки
16 KiB
C++
475 строки
16 KiB
C++
/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
|
|
/* vim: set ts=2 et sw=2 tw=80: */
|
|
/* 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/. */
|
|
|
|
#include "CachedTableAccessible.h"
|
|
|
|
#include "AccIterator.h"
|
|
#include "DocAccessibleParent.h"
|
|
#include "mozilla/ClearOnShutdown.h"
|
|
#include "mozilla/StaticPtr.h"
|
|
#include "mozilla/UniquePtr.h"
|
|
#include "nsAccUtils.h"
|
|
#include "nsIAccessiblePivot.h"
|
|
#include "Pivot.h"
|
|
#include "RemoteAccessible.h"
|
|
#include "TableAccessible.h"
|
|
#include "TableCellAccessible.h"
|
|
|
|
namespace mozilla::a11y {
|
|
|
|
// Used to search for table descendants relevant to table structure.
|
|
class TablePartRule : public PivotRule {
|
|
public:
|
|
virtual uint16_t Match(Accessible* aAcc) override {
|
|
role accRole = aAcc->Role();
|
|
if (accRole == roles::CAPTION || aAcc->IsTableCell()) {
|
|
return nsIAccessibleTraversalRule::FILTER_MATCH |
|
|
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
|
|
}
|
|
if (aAcc->IsTableRow()) {
|
|
return nsIAccessibleTraversalRule::FILTER_MATCH;
|
|
}
|
|
if (aAcc->IsTable() ||
|
|
// Generic containers.
|
|
accRole == roles::TEXT || accRole == roles::TEXT_CONTAINER ||
|
|
accRole == roles::SECTION ||
|
|
// Row groups.
|
|
accRole == roles::GROUPING) {
|
|
// Walk inside these, but don't match them.
|
|
return nsIAccessibleTraversalRule::FILTER_IGNORE;
|
|
}
|
|
return nsIAccessibleTraversalRule::FILTER_IGNORE |
|
|
nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE;
|
|
}
|
|
};
|
|
|
|
// Iterates through headers explicitly associated with a remote table cell via
|
|
// the headers DOM attribute. These are cached as Accessible ids.
|
|
class RemoteExplicitHeadersIterator : public AccIterable {
|
|
public:
|
|
RemoteExplicitHeadersIterator(const nsTArray<uint64_t>& aHeaders,
|
|
Accessible* aDoc)
|
|
: mHeaders(aHeaders), mDoc(aDoc), mIndex(0) {}
|
|
|
|
virtual Accessible* Next() override {
|
|
while (mIndex < mHeaders.Length()) {
|
|
uint64_t id = mHeaders[mIndex++];
|
|
Accessible* acc = nsAccUtils::GetAccessibleByID(mDoc, id);
|
|
if (acc) {
|
|
return acc;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
private:
|
|
const nsTArray<uint64_t>& mHeaders;
|
|
Accessible* mDoc;
|
|
uint32_t mIndex;
|
|
};
|
|
|
|
// The Accessible* keys should only be used for lookup. They should not be
|
|
// dereferenced.
|
|
using CachedTablesMap = nsTHashMap<Accessible*, CachedTableAccessible>;
|
|
// We use a global map rather than a map in each document for three reasons:
|
|
// 1. We don't have a common base class for local and remote documents.
|
|
// 2. It avoids wasting memory in a document that doesn't have any tables.
|
|
// 3. It allows the cache management to be encapsulated here in
|
|
// CachedTableAccessible.
|
|
static StaticAutoPtr<CachedTablesMap> sCachedTables;
|
|
|
|
/* static */
|
|
CachedTableAccessible* CachedTableAccessible::GetFrom(Accessible* aAcc) {
|
|
if (!sCachedTables) {
|
|
sCachedTables = new CachedTablesMap();
|
|
ClearOnShutdown(&sCachedTables);
|
|
}
|
|
return &sCachedTables->LookupOrInsertWith(
|
|
aAcc, [&] { return CachedTableAccessible(aAcc); });
|
|
}
|
|
|
|
/* static */
|
|
void CachedTableAccessible::Invalidate(Accessible* aAcc) {
|
|
if (!sCachedTables) {
|
|
return;
|
|
}
|
|
Accessible* table = nullptr;
|
|
if (aAcc->IsTable()) {
|
|
table = aAcc;
|
|
} else if (aAcc->IsTableCell()) {
|
|
for (table = aAcc->Parent(); table; table = table->Parent()) {
|
|
if (table->IsTable()) {
|
|
break;
|
|
}
|
|
}
|
|
} else {
|
|
MOZ_ASSERT_UNREACHABLE("Should only be called on a table or a cell");
|
|
}
|
|
if (table) {
|
|
// Destroy the instance (if any). We'll create a new one the next time it
|
|
// is requested.
|
|
sCachedTables->Remove(table);
|
|
}
|
|
}
|
|
|
|
CachedTableAccessible::CachedTableAccessible(Accessible* aAcc) : mAcc(aAcc) {
|
|
MOZ_ASSERT(mAcc);
|
|
// Build the cache. The cache can only be built once per instance. When it's
|
|
// invalidated, we just throw away the instance and create a new one when
|
|
// the cache is next needed.
|
|
int32_t rowIdx = -1;
|
|
uint32_t colIdx = 0;
|
|
// Maps a column index to the cell index of its previous implicit column
|
|
// header.
|
|
nsTHashMap<uint32_t, uint32_t> prevColHeaders;
|
|
Pivot pivot(mAcc);
|
|
TablePartRule rule;
|
|
for (Accessible* part = pivot.Next(mAcc, rule); part;
|
|
part = pivot.Next(part, rule)) {
|
|
role partRole = part->Role();
|
|
if (partRole == roles::CAPTION) {
|
|
// If there are multiple captions, use the first.
|
|
if (!mCaptionAccID) {
|
|
mCaptionAccID = part->ID();
|
|
}
|
|
continue;
|
|
}
|
|
if (part->IsTableRow()) {
|
|
++rowIdx;
|
|
colIdx = 0;
|
|
// This might be an empty row, so ensure a row here, as our row count is
|
|
// based on the length of mRowColToCellIdx.
|
|
EnsureRow(rowIdx);
|
|
continue;
|
|
}
|
|
MOZ_ASSERT(part->IsTableCell());
|
|
if (rowIdx == -1) {
|
|
// We haven't created a row yet, so this cell must be outside a row.
|
|
continue;
|
|
}
|
|
// Check for a cell spanning multiple rows which already occupies this
|
|
// position. Keep incrementing until we find a vacant position.
|
|
for (;;) {
|
|
EnsureRowCol(rowIdx, colIdx);
|
|
if (mRowColToCellIdx[rowIdx][colIdx] == kNoCellIdx) {
|
|
// This position is not occupied.
|
|
break;
|
|
}
|
|
// This position is occupied.
|
|
++colIdx;
|
|
}
|
|
// Create the cell.
|
|
uint32_t cellIdx = mCells.Length();
|
|
auto prevColHeader = prevColHeaders.MaybeGet(colIdx);
|
|
auto cell = mCells.AppendElement(
|
|
CachedTableCellAccessible(part->ID(), part, rowIdx, colIdx,
|
|
prevColHeader ? *prevColHeader : kNoCellIdx));
|
|
mAccToCellIdx.InsertOrUpdate(part, cellIdx);
|
|
// Update our row/col map.
|
|
// This cell might span multiple rows and/or columns. In that case, we need
|
|
// to occupy multiple coordinates in the row/col map.
|
|
uint32_t lastRowForCell =
|
|
static_cast<uint32_t>(rowIdx) + cell->RowExtent() - 1;
|
|
MOZ_ASSERT(lastRowForCell >= static_cast<uint32_t>(rowIdx));
|
|
uint32_t lastColForCell = colIdx + cell->ColExtent() - 1;
|
|
MOZ_ASSERT(lastColForCell >= colIdx);
|
|
for (uint32_t spannedRow = static_cast<uint32_t>(rowIdx);
|
|
spannedRow <= lastRowForCell; ++spannedRow) {
|
|
for (uint32_t spannedCol = colIdx; spannedCol <= lastColForCell;
|
|
++spannedCol) {
|
|
EnsureRowCol(spannedRow, spannedCol);
|
|
MOZ_ASSERT(mRowColToCellIdx[spannedRow][spannedCol] == kNoCellIdx);
|
|
auto& rowCol = mRowColToCellIdx[spannedRow][spannedCol];
|
|
// If a cell already occupies this position, it overlaps with this one;
|
|
// e.g. r1..2c2 and r2c1..2. In that case, we want to prefer the first
|
|
// cell.
|
|
if (rowCol == kNoCellIdx) {
|
|
rowCol = cellIdx;
|
|
}
|
|
}
|
|
}
|
|
if (partRole == roles::COLUMNHEADER) {
|
|
for (uint32_t spannedCol = colIdx; spannedCol <= lastColForCell;
|
|
++spannedCol) {
|
|
prevColHeaders.InsertOrUpdate(spannedCol, cellIdx);
|
|
}
|
|
}
|
|
// Increment for the next cell.
|
|
colIdx = lastColForCell + 1;
|
|
}
|
|
}
|
|
|
|
void CachedTableAccessible::EnsureRow(uint32_t aRowIdx) {
|
|
for (uint32_t newRow = mRowColToCellIdx.Length(); newRow <= aRowIdx;
|
|
++newRow) {
|
|
// The next row doesn't exist yet. Create it.
|
|
mRowColToCellIdx.AppendElement();
|
|
}
|
|
MOZ_ASSERT(mRowColToCellIdx.Length() > aRowIdx);
|
|
}
|
|
|
|
void CachedTableAccessible::EnsureRowCol(uint32_t aRowIdx, uint32_t aColIdx) {
|
|
EnsureRow(aRowIdx);
|
|
auto& row = mRowColToCellIdx[aRowIdx];
|
|
for (uint32_t newCol = row.Length(); newCol <= aColIdx; ++newCol) {
|
|
// An entry doesn't yet exist for this column in this row.
|
|
row.AppendElement(kNoCellIdx);
|
|
}
|
|
MOZ_ASSERT(row.Length() > aColIdx);
|
|
if (mColCount <= aColIdx) {
|
|
++mColCount;
|
|
}
|
|
}
|
|
|
|
Accessible* CachedTableAccessible::Caption() const {
|
|
if (mCaptionAccID) {
|
|
Accessible* caption = nsAccUtils::GetAccessibleByID(
|
|
nsAccUtils::DocumentFor(mAcc), mCaptionAccID);
|
|
MOZ_ASSERT(caption, "Dead caption Accessible!");
|
|
MOZ_ASSERT(caption->Role() != roles::CAPTION, "Caption has wrong role");
|
|
return caption;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void CachedTableAccessible::Summary(nsString& aSummary) {
|
|
if (Caption()) {
|
|
// If there's a caption, we map caption to Name and summary to Description.
|
|
mAcc->Description(aSummary);
|
|
} else {
|
|
// If there's no caption, we map summary to Name.
|
|
mAcc->Name(aSummary);
|
|
}
|
|
}
|
|
|
|
Accessible* CachedTableAccessible::CellAt(uint32_t aRowIdx, uint32_t aColIdx) {
|
|
int32_t cellIdx = CellIndexAt(aRowIdx, aColIdx);
|
|
if (cellIdx == -1) {
|
|
return nullptr;
|
|
}
|
|
return mCells[cellIdx].Acc(mAcc);
|
|
}
|
|
|
|
void CachedTableAccessible::SelectCol(uint32_t aColIdx) {
|
|
if (LocalAccessible* localAcc = mAcc->AsLocal()) {
|
|
TableAccessible* table = localAcc->AsTable();
|
|
table->SelectCol(aColIdx);
|
|
}
|
|
// XXX Implement support for RemoteAccessible.
|
|
}
|
|
|
|
void CachedTableAccessible::UnselectCol(uint32_t aColIdx) {
|
|
if (LocalAccessible* localAcc = mAcc->AsLocal()) {
|
|
TableAccessible* table = localAcc->AsTable();
|
|
table->UnselectCol(aColIdx);
|
|
}
|
|
// XXX Implement support for RemoteAccessible.
|
|
}
|
|
|
|
void CachedTableAccessible::SelectRow(uint32_t aRowIdx) {
|
|
if (LocalAccessible* localAcc = mAcc->AsLocal()) {
|
|
TableAccessible* table = localAcc->AsTable();
|
|
table->SelectRow(aRowIdx);
|
|
}
|
|
// XXX Implement support for RemoteAccessible.
|
|
}
|
|
|
|
void CachedTableAccessible::UnselectRow(uint32_t aRowIdx) {
|
|
if (LocalAccessible* localAcc = mAcc->AsLocal()) {
|
|
TableAccessible* table = localAcc->AsTable();
|
|
table->UnselectRow(aRowIdx);
|
|
}
|
|
// XXX Implement support for RemoteAccessible.
|
|
}
|
|
|
|
bool CachedTableAccessible::IsProbablyLayoutTable() {
|
|
if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
|
|
return remoteAcc->TableIsProbablyForLayout();
|
|
}
|
|
TableAccessible* localTable = mAcc->AsLocal()->AsTable();
|
|
return localTable->IsProbablyLayoutTable();
|
|
}
|
|
|
|
/* static */
|
|
CachedTableCellAccessible* CachedTableCellAccessible::GetFrom(
|
|
Accessible* aAcc) {
|
|
MOZ_ASSERT(aAcc->IsTableCell());
|
|
for (Accessible* parent = aAcc; parent; parent = parent->Parent()) {
|
|
if (auto* table =
|
|
static_cast<CachedTableAccessible*>(parent->AsTableBase())) {
|
|
if (auto cellIdx = table->mAccToCellIdx.Lookup(aAcc)) {
|
|
return &table->mCells[*cellIdx];
|
|
}
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
Accessible* CachedTableCellAccessible::Acc(Accessible* aTableAcc) const {
|
|
Accessible* acc =
|
|
nsAccUtils::GetAccessibleByID(nsAccUtils::DocumentFor(aTableAcc), mAccID);
|
|
MOZ_DIAGNOSTIC_ASSERT(acc == mAcc, "Cell's cached mAcc is dead!");
|
|
return acc;
|
|
}
|
|
|
|
TableAccessibleBase* CachedTableCellAccessible::Table() const {
|
|
for (const Accessible* acc = mAcc; acc; acc = acc->Parent()) {
|
|
// Since the caller has this cell, the table is already created, so it's
|
|
// okay to ignore the const restriction here.
|
|
if (TableAccessibleBase* table =
|
|
const_cast<Accessible*>(acc)->AsTableBase()) {
|
|
return table;
|
|
}
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
uint32_t CachedTableCellAccessible::ColExtent() const {
|
|
if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
|
|
if (remoteAcc->mCachedFields) {
|
|
if (auto colSpan = remoteAcc->mCachedFields->GetAttribute<int32_t>(
|
|
nsGkAtoms::colspan)) {
|
|
return *colSpan;
|
|
}
|
|
}
|
|
} else if (LocalAccessible* localAcc = mAcc->AsLocal()) {
|
|
// For HTML table cells, we must use the HTMLTableCellAccessible
|
|
// GetColExtent method rather than using the DOM attributes directly.
|
|
// This is because of things like rowspan="0" which depend on knowing
|
|
// about thead, tbody, etc., which is info we don't have in the a11y tree.
|
|
TableCellAccessible* cell = localAcc->AsTableCell();
|
|
MOZ_ASSERT(cell);
|
|
return cell->ColExtent();
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
uint32_t CachedTableCellAccessible::RowExtent() const {
|
|
if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
|
|
if (remoteAcc->mCachedFields) {
|
|
if (auto rowSpan = remoteAcc->mCachedFields->GetAttribute<int32_t>(
|
|
nsGkAtoms::rowspan)) {
|
|
return *rowSpan;
|
|
}
|
|
}
|
|
} else if (LocalAccessible* localAcc = mAcc->AsLocal()) {
|
|
// For HTML table cells, we must use the HTMLTableCellAccessible
|
|
// GetRowExtent method rather than using the DOM attributes directly.
|
|
// This is because of things like rowspan="0" which depend on knowing
|
|
// about thead, tbody, etc., which is info we don't have in the a11y tree.
|
|
TableCellAccessible* cell = localAcc->AsTableCell();
|
|
MOZ_ASSERT(cell);
|
|
return cell->RowExtent();
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
UniquePtr<AccIterable> CachedTableCellAccessible::GetExplicitHeadersIterator() {
|
|
if (RemoteAccessible* remoteAcc = mAcc->AsRemote()) {
|
|
if (remoteAcc->mCachedFields) {
|
|
if (auto headers =
|
|
remoteAcc->mCachedFields->GetAttribute<nsTArray<uint64_t>>(
|
|
nsGkAtoms::headers)) {
|
|
return MakeUnique<RemoteExplicitHeadersIterator>(*headers,
|
|
remoteAcc->Document());
|
|
}
|
|
}
|
|
} else if (LocalAccessible* localAcc = mAcc->AsLocal()) {
|
|
return MakeUnique<IDRefsIterator>(
|
|
localAcc->Document(), localAcc->GetContent(), nsGkAtoms::headers);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void CachedTableCellAccessible::ColHeaderCells(nsTArray<Accessible*>* aCells) {
|
|
auto* table = static_cast<CachedTableAccessible*>(Table());
|
|
if (!table) {
|
|
return;
|
|
}
|
|
if (auto iter = GetExplicitHeadersIterator()) {
|
|
while (Accessible* header = iter->Next()) {
|
|
role headerRole = header->Role();
|
|
if (headerRole == roles::COLUMNHEADER) {
|
|
aCells->AppendElement(header);
|
|
} else if (headerRole != roles::ROWHEADER) {
|
|
// Treat this cell as a column header only if it's in the same column.
|
|
if (auto cellIdx = table->mAccToCellIdx.Lookup(header)) {
|
|
CachedTableCellAccessible& cell = table->mCells[*cellIdx];
|
|
if (cell.ColIdx() == ColIdx()) {
|
|
aCells->AppendElement(header);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!aCells->IsEmpty()) {
|
|
return;
|
|
}
|
|
}
|
|
Accessible* doc = nsAccUtils::DocumentFor(table->AsAccessible());
|
|
// Each cell stores its previous implicit column header, effectively forming a
|
|
// linked list. We traverse that to get all the headers.
|
|
CachedTableCellAccessible* cell = this;
|
|
for (;;) {
|
|
if (cell->mPrevColHeaderCellIdx == kNoCellIdx) {
|
|
break; // No more headers.
|
|
}
|
|
cell = &table->mCells[cell->mPrevColHeaderCellIdx];
|
|
Accessible* cellAcc = nsAccUtils::GetAccessibleByID(doc, cell->mAccID);
|
|
aCells->AppendElement(cellAcc);
|
|
}
|
|
}
|
|
|
|
void CachedTableCellAccessible::RowHeaderCells(nsTArray<Accessible*>* aCells) {
|
|
auto* table = static_cast<CachedTableAccessible*>(Table());
|
|
if (!table) {
|
|
return;
|
|
}
|
|
if (auto iter = GetExplicitHeadersIterator()) {
|
|
while (Accessible* header = iter->Next()) {
|
|
role headerRole = header->Role();
|
|
if (headerRole == roles::ROWHEADER) {
|
|
aCells->AppendElement(header);
|
|
} else if (headerRole != roles::COLUMNHEADER) {
|
|
// Treat this cell as a row header only if it's in the same row.
|
|
if (auto cellIdx = table->mAccToCellIdx.Lookup(header)) {
|
|
CachedTableCellAccessible& cell = table->mCells[*cellIdx];
|
|
if (cell.RowIdx() == RowIdx()) {
|
|
aCells->AppendElement(header);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (!aCells->IsEmpty()) {
|
|
return;
|
|
}
|
|
}
|
|
// We don't cache implicit row headers because there are usually not that many
|
|
// cells per row. Get all the row headers on the row before this cell.
|
|
uint32_t row = RowIdx();
|
|
uint32_t thisCol = ColIdx();
|
|
for (uint32_t col = thisCol - 1; col < thisCol; --col) {
|
|
Accessible* cellAcc = table->CellAt(row, col);
|
|
if (!cellAcc) {
|
|
continue;
|
|
}
|
|
TableCellAccessibleBase* cell = cellAcc->AsTableCellBase();
|
|
MOZ_ASSERT(cell);
|
|
// cell might span multiple columns. We don't want to visit it multiple
|
|
// times, so ensure col is set to cell's starting column.
|
|
col = cell->ColIdx();
|
|
if (cellAcc->Role() != roles::ROWHEADER) {
|
|
continue;
|
|
}
|
|
aCells->AppendElement(cellAcc);
|
|
}
|
|
}
|
|
|
|
bool CachedTableCellAccessible::Selected() {
|
|
return mAcc->State() & states::SELECTED;
|
|
}
|
|
|
|
} // namespace mozilla::a11y
|