// Copyright 2018 The Chromium Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include "ui/accessibility/ax_table_info.h" #include "ui/accessibility/ax_node.h" #include "ui/accessibility/ax_role_properties.h" #include "ui/accessibility/ax_tree.h" #include "ui/gfx/geometry/rect_f.h" namespace ui { namespace { void FindCells(AXNode* node, std::vector* cells) { for (AXNode* child : node->children()) { if (child->data().HasState(ax::mojom::State::kIgnored) || child->data().role == ax::mojom::Role::kGenericContainer) FindCells(child, cells); else if (IsCellOrTableHeaderRole(child->data().role)) cells->push_back(child); } } void FindRowsAndThenCells(AXNode* node, std::vector* cells) { for (AXNode* child : node->children()) { if (child->data().HasState(ax::mojom::State::kIgnored) || child->data().role == ax::mojom::Role::kGenericContainer) FindRowsAndThenCells(child, cells); else if (child->data().role == ax::mojom::Role::kRow) FindCells(child, cells); } } } // namespace // static AXTableInfo* AXTableInfo::Create(AXTree* tree, AXNode* table_node) { DCHECK(tree); DCHECK(table_node); #if DCHECK_IS_ON() // Sanity check, make sure the node is in the tree. AXNode* node = table_node; while (node && node != tree->root()) node = node->parent(); DCHECK(node == tree->root()); #endif if (!IsTableLikeRole(table_node->data().role)) return nullptr; AXTableInfo* info = new AXTableInfo(tree, table_node); bool success = info->Update(); DCHECK(success); return info; } bool AXTableInfo::Update() { if (!IsTableLikeRole(table_node_->data().role)) return false; col_headers.clear(); row_headers.clear(); all_headers.clear(); cell_ids.clear(); unique_cell_ids.clear(); std::vector cells; FindRowsAndThenCells(table_node_, &cells); // Compute the actual row and column count, and the set of all unique cell ids // in the table. row_count = table_node_->data().GetIntAttribute( ax::mojom::IntAttribute::kTableRowCount); col_count = table_node_->data().GetIntAttribute( ax::mojom::IntAttribute::kTableColumnCount); for (AXNode* cell : cells) { int row_index = cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellRowIndex); int row_span = std::max(1, cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellRowSpan)); row_count = std::max(row_count, row_index + row_span); int col_index = cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellColumnIndex); int col_span = std::max(1, cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellColumnSpan)); col_count = std::max(col_count, col_index + col_span); } // Allocate space for the 2-D array of cell IDs and 1-D // arrays of row headers and column headers. row_headers.resize(row_count); col_headers.resize(col_count); cell_ids.resize(row_count); for (auto& row : cell_ids) row.resize(col_count); // Now iterate over the cells and fill in the cell IDs, row headers, // and column headers based on the index and span of each cell. int32_t cell_index = 0; for (AXNode* cell : cells) { unique_cell_ids.push_back(cell->id()); cell_id_to_index[cell->id()] = cell_index++; int row_index = cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellRowIndex); int row_span = std::max(1, cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellRowSpan)); int col_index = cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellColumnIndex); int col_span = std::max(1, cell->data().GetIntAttribute( ax::mojom::IntAttribute::kTableCellColumnSpan)); // Cells must contain a 0-based row index and col index. if (row_index < 0 || col_index < 0) continue; for (int r = row_index; r < row_index + row_span; r++) { DCHECK_LT(r, row_count); for (int c = col_index; c < col_index + col_span; c++) { DCHECK_LT(c, col_count); cell_ids[r][c] = cell->id(); if (cell->data().role == ax::mojom::Role::kColumnHeader) { col_headers[c].push_back(cell->id()); all_headers.push_back(cell->id()); } else if (cell->data().role == ax::mojom::Role::kRowHeader) { row_headers[r].push_back(cell->id()); all_headers.push_back(cell->id()); } } } } if (tree_->enable_extra_mac_nodes()) UpdateExtraMacNodes(); valid_ = true; return true; } void AXTableInfo::Invalidate() { valid_ = false; } void AXTableInfo::UpdateExtraMacNodes() { // On macOS, maintain additional AXNodes: one column node for each // column of the table, and one table header container. // // The nodes all set the table as the parent node, that way the Mac-specific // platform code can treat these nodes as additional children of the table // node. // // The columns have id -1, -2, -3, ... - this won't conflict with ids from // Blink, which are all positive. // // Each column has the kColumnIndex attribute set, and then each of the cells // in that column gets added as an indirect ID. That exposes them as children // via Mac APIs but ensures we don't explore those nodes multiple times when // walking the tree. The column also has the ID of the first column header // set. // // The table header container is just a node with all of the headers in the // table as indirect children. // One node for each column, and one more for the table header container. size_t extra_node_count = static_cast(col_count + 1); if (extra_mac_nodes.size() != extra_node_count) { // Delete old extra nodes. ClearExtraMacNodes(); // Resize. extra_mac_nodes.resize(col_count + 1); // Create column nodes. for (int i = 0; i < col_count; i++) extra_mac_nodes[i] = CreateExtraMacColumnNode(i); // Create table header container node. extra_mac_nodes[col_count] = CreateExtraMacTableHeaderNode(); } // Update the columns to reflect current state of the table. for (int i = 0; i < col_count; i++) UpdateExtraMacColumnNodeAttributes(i); // Update the table header container to contain all headers. ui::AXNodeData data = extra_mac_nodes[col_count]->data(); data.intlist_attributes.clear(); data.AddIntListAttribute(ax::mojom::IntListAttribute::kIndirectChildIds, all_headers); extra_mac_nodes[col_count]->SetData(data); } AXNode* AXTableInfo::CreateExtraMacColumnNode(int col_index) { int32_t id = tree_->GetNextNegativeInternalNodeId(); int32_t index_in_parent = col_index + table_node_->child_count(); AXNode* node = new AXNode(table_node_, id, index_in_parent); AXNodeData data; data.id = id; data.role = ax::mojom::Role::kColumn; node->SetData(data); if (tree_->delegate()) tree_->delegate()->OnNodeCreated(tree_, node); return node; } AXNode* AXTableInfo::CreateExtraMacTableHeaderNode() { int32_t id = tree_->GetNextNegativeInternalNodeId(); int32_t index_in_parent = col_count + table_node_->child_count(); AXNode* node = new AXNode(table_node_, id, index_in_parent); AXNodeData data; data.id = id; data.role = ax::mojom::Role::kTableHeaderContainer; node->SetData(data); if (tree_->delegate()) tree_->delegate()->OnNodeCreated(tree_, node); return node; } void AXTableInfo::UpdateExtraMacColumnNodeAttributes(int col_index) { ui::AXNodeData data = extra_mac_nodes[col_index]->data(); data.int_attributes.clear(); // Update the column index. data.AddIntAttribute(ax::mojom::IntAttribute::kTableColumnIndex, col_index); // Update the column header. if (!col_headers[col_index].empty()) { data.AddIntAttribute(ax::mojom::IntAttribute::kTableColumnHeaderId, col_headers[col_index][0]); } // Update the list of cells in the column. data.intlist_attributes.clear(); std::vector col_nodes; int32_t last = 0; for (int row_index = 0; row_index < row_count; row_index++) { int32_t cell_id = cell_ids[row_index][col_index]; if (cell_id != 0 && cell_id != last) col_nodes.push_back(cell_id); last = cell_id; } data.AddIntListAttribute(ax::mojom::IntListAttribute::kIndirectChildIds, col_nodes); extra_mac_nodes[col_index]->SetData(data); } void AXTableInfo::ClearExtraMacNodes() { for (size_t i = 0; i < extra_mac_nodes.size(); i++) { if (tree_->delegate()) tree_->delegate()->OnNodeWillBeDeleted(tree_, extra_mac_nodes[i]); delete extra_mac_nodes[i]; } } AXTableInfo::AXTableInfo(AXTree* tree, AXNode* table_node) : tree_(tree), table_node_(table_node) {} AXTableInfo::~AXTableInfo() { ClearExtraMacNodes(); } } // namespace ui