Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
ax_table_info.cc
Go to the documentation of this file.
1// Copyright 2018 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5#include "ax_table_info.h"
6
7#include <cstddef>
8
9#include "ax_constants.h"
10#include "ax_enums.h"
11#include "ax_node.h"
12#include "ax_role_properties.h"
13#include "ax_tree.h"
14#include "ax_tree_observer.h"
15#include "base/logging.h"
16
18
19namespace ui {
20
21namespace {
22
23// Given a node representing a table row, search its children
24// recursively to find any cells or table headers, and append
25// them to |cells|.
26//
27// We recursively check generic containers like <div> and any
28// nodes that are ignored, but we don't search any other roles
29// in-between a table row and its cells.
30void FindCellsInRow(AXNode* node, std::vector<AXNode*>* cell_nodes) {
31 for (AXNode* child : node->children()) {
32 if (child->IsIgnored() ||
33 child->data().role == ax::mojom::Role::kGenericContainer)
34 FindCellsInRow(child, cell_nodes);
35 else if (IsCellOrTableHeader(child->data().role))
36 cell_nodes->push_back(child);
37 }
38}
39
40// Given a node representing a table/grid, search its children
41// recursively to find any rows and append them to |row_node_list|, then
42// for each row find its cells and add them to |cell_nodes_per_row| as a
43// 2-dimensional array.
44//
45// We only recursively check for the following roles in between a table and
46// its rows: generic containers like <div>, any nodes that are ignored, and
47// table sections (which have Role::kRowGroup).
48void FindRowsAndThenCells(AXNode* node,
49 std::vector<AXNode*>* row_node_list,
50 std::vector<std::vector<AXNode*>>* cell_nodes_per_row,
51 int32_t& caption_node_id) {
52 for (AXNode* child : node->children()) {
53 if (child->IsIgnored() ||
54 child->data().role == ax::mojom::Role::kGenericContainer ||
55 child->data().role == ax::mojom::Role::kGroup ||
56 child->data().role == ax::mojom::Role::kRowGroup) {
57 FindRowsAndThenCells(child, row_node_list, cell_nodes_per_row,
58 caption_node_id);
59 } else if (IsTableRow(child->data().role)) {
60 row_node_list->push_back(child);
61 cell_nodes_per_row->push_back(std::vector<AXNode*>());
62 FindCellsInRow(child, &cell_nodes_per_row->back());
63 } else if (child->data().role == ax::mojom::Role::kCaption)
64 caption_node_id = child->id();
65 }
66}
67
68size_t GetSizeTAttribute(const AXNode& node, IntAttribute attribute) {
69 return base::saturated_cast<size_t>(node.GetIntAttribute(attribute));
70}
71
72} // namespace
73
74// static
76 BASE_DCHECK(tree);
77 BASE_DCHECK(table_node);
78
79#ifndef NDEBUG
80 // Sanity check, make sure the node is in the tree.
81 AXNode* node = table_node;
82 while (node && node != tree->root())
83 node = node->parent();
84 BASE_DCHECK(node == tree->root());
85#endif
86
87 if (!IsTableLike(table_node->data().role))
88 return nullptr;
89
90 AXTableInfo* info = new AXTableInfo(tree, table_node);
91 bool success = info->Update();
92 BASE_DCHECK(success);
93
94 return info;
95}
96
98 if (!table_node_->IsTable())
99 return false;
100
101 ClearVectors();
102
103 std::vector<std::vector<AXNode*>> cell_nodes_per_row;
104 caption_id = 0;
105 FindRowsAndThenCells(table_node_, &row_nodes, &cell_nodes_per_row,
106 caption_id);
107 BASE_DCHECK(cell_nodes_per_row.size() == row_nodes.size());
108
109 // Get the optional row and column count from the table. If we encounter
110 // a cell with an index or span larger than this, we'll update the
111 // table row and column count to be large enough to fit all cells.
112 row_count = GetSizeTAttribute(*table_node_, IntAttribute::kTableRowCount);
113 col_count = GetSizeTAttribute(*table_node_, IntAttribute::kTableColumnCount);
114
115 // Note - GetIntAttribute returns 0 if no value has been specified for the
116 // attribute.
117 aria_row_count = static_cast<int>(
118 table_node_->GetIntAttribute(IntAttribute::kAriaRowCount));
119 aria_col_count = static_cast<int>(
120 table_node_->GetIntAttribute(IntAttribute::kAriaColumnCount));
121
122 // Iterate over the cells and build up an array of CellData
123 // entries, one for each cell. Compute the actual row and column
124 BuildCellDataVectorFromRowAndCellNodes(row_nodes, cell_nodes_per_row);
125
126 // At this point we have computed valid row and column indices for
127 // every cell in the table, and an accurate row and column count for the
128 // whole table that fits every cell and its spans. The final step is to
129 // fill in a 2-dimensional array that lets us look up an individual cell
130 // by its (row, column) coordinates, plus arrays to hold row and column
131 // headers.
132 BuildCellAndHeaderVectorsFromCellData();
133
134 // On Mac, we add a few extra nodes to the table - see comment
135 // at the top of UpdateExtraMacNodes for details.
136 if (tree_->enable_extra_mac_nodes())
137 UpdateExtraMacNodes();
138
139 // The table metadata is now valid, any table queries will now be
140 // fast. Any time a node in the table is updated, we'll have to
141 // recompute all of this.
142 valid_ = true;
143 return true;
144}
145
147 valid_ = false;
148}
149
150void AXTableInfo::ClearVectors() {
151 col_headers.clear();
152 row_headers.clear();
153 all_headers.clear();
154 cell_ids.clear();
155 unique_cell_ids.clear();
156 cell_data_vector.clear();
157 row_nodes.clear();
158 cell_id_to_index.clear();
159 row_id_to_index.clear();
160 incremental_row_col_map_.clear();
161}
162
163void AXTableInfo::BuildCellDataVectorFromRowAndCellNodes(
164 const std::vector<AXNode*>& row_node_list,
165 const std::vector<std::vector<AXNode*>>& cell_nodes_per_row) {
166 // Iterate over the cells and build up an array of CellData
167 // entries, one for each cell. Compute the actual row and column
168 // indices for each cell by taking the specified row and column
169 // index in the accessibility tree if legal, but replacing it with
170 // valid table coordinates otherwise.
171 size_t cell_index = 0;
172 size_t current_aria_row_index = 1;
173 size_t next_row_index = 0;
174 for (size_t i = 0; i < cell_nodes_per_row.size(); i++) {
175 auto& cell_nodes_in_this_row = cell_nodes_per_row[i];
176 AXNode* row_node = row_node_list[i];
177 bool is_first_cell_in_row = true;
178 size_t current_col_index = 0;
179 size_t current_aria_col_index = 1;
180
181 // Make sure the row index is always at least as high as the one reported by
182 // the source tree.
183 row_id_to_index[row_node->id()] =
184 std::max(next_row_index,
185 GetSizeTAttribute(*row_node, IntAttribute::kTableRowIndex));
186 size_t* current_row_index = &row_id_to_index[row_node->id()];
187 size_t spanned_col_index = 0;
188 for (AXNode* cell : cell_nodes_in_this_row) {
189 // Fill in basic info in CellData.
190 CellData cell_data;
191 unique_cell_ids.push_back(cell->id());
192 cell_id_to_index[cell->id()] = cell_index++;
193 cell_data.cell = cell;
194
195 // Get table cell accessibility attributes - note that these may
196 // be missing or invalid, we'll correct them next.
197 cell_data.row_index =
198 GetSizeTAttribute(*cell, IntAttribute::kTableCellRowIndex);
199 cell_data.row_span =
200 GetSizeTAttribute(*cell, IntAttribute::kTableCellRowSpan);
201 cell_data.aria_row_index =
202 GetSizeTAttribute(*cell, IntAttribute::kAriaCellRowIndex);
203 cell_data.col_index =
204 GetSizeTAttribute(*cell, IntAttribute::kTableCellColumnIndex);
205 cell_data.aria_col_index =
206 GetSizeTAttribute(*cell, IntAttribute::kAriaCellColumnIndex);
207 cell_data.col_span =
208 GetSizeTAttribute(*cell, IntAttribute::kTableCellColumnSpan);
209
210 // The col span and row span must be at least 1.
211 cell_data.row_span = std::max(size_t{1}, cell_data.row_span);
212 cell_data.col_span = std::max(size_t{1}, cell_data.col_span);
213
214 // Ensure the column index must always be incrementing.
215 cell_data.col_index = std::max(cell_data.col_index, current_col_index);
216
217 // And update the spanned column index.
218 spanned_col_index = std::max(spanned_col_index, cell_data.col_index);
219
220 if (is_first_cell_in_row) {
221 is_first_cell_in_row = false;
222
223 // If it's the first cell in the row, ensure the row index is
224 // incrementing. The rest of the cells in this row are forced to have
225 // the same row index.
226 if (cell_data.row_index > *current_row_index) {
227 *current_row_index = cell_data.row_index;
228 } else {
229 cell_data.row_index = *current_row_index;
230 }
231
232 // The starting ARIA row and column index might be specified in
233 // the row node, we should check there.
234 if (!cell_data.aria_row_index) {
235 cell_data.aria_row_index =
236 GetSizeTAttribute(*row_node, IntAttribute::kAriaCellRowIndex);
237 }
238 if (!cell_data.aria_col_index) {
239 cell_data.aria_col_index =
240 GetSizeTAttribute(*row_node, IntAttribute::kAriaCellColumnIndex);
241 }
242 cell_data.aria_row_index =
243 std::max(cell_data.aria_row_index, current_aria_row_index);
244 current_aria_row_index = cell_data.aria_row_index;
245 } else {
246 // Don't allow the row index to change after the beginning
247 // of a row.
248 cell_data.row_index = *current_row_index;
249 cell_data.aria_row_index = current_aria_row_index;
250 }
251
252 // Adjust the spanned col index by looking at the incremental row col map.
253 // This map contains already filled in values, accounting for spans, of
254 // all row, col indices. The map should have filled in all values we need
255 // (upper left triangle of cells of the table).
256 while (true) {
257 const auto& row_it = incremental_row_col_map_.find(*current_row_index);
258 if (row_it == incremental_row_col_map_.end()) {
259 break;
260 } else {
261 const auto& col_it = row_it->second.find(spanned_col_index);
262 if (col_it == row_it->second.end()) {
263 break;
264 } else {
265 // A pre-existing cell resides in our desired position. Make a
266 // best-fit to the right of the existing span.
267 const CellData& spanned_cell_data = col_it->second;
268 spanned_col_index =
269 spanned_cell_data.col_index + spanned_cell_data.col_span;
270
271 // Adjust the actual col index to be the best fit with the existing
272 // spanned cell data.
273 cell_data.col_index = spanned_col_index;
274 }
275 }
276 }
277
278 // Memoize the cell data using our incremental row col map.
279 for (size_t r = cell_data.row_index;
280 r < (cell_data.row_index + cell_data.row_span); r++) {
281 for (size_t c = cell_data.col_index;
282 c < (cell_data.col_index + cell_data.col_span); c++) {
283 incremental_row_col_map_[r][c] = cell_data;
284 }
285 }
286
287 // Ensure the ARIA col index is incrementing.
288 cell_data.aria_col_index =
289 std::max(cell_data.aria_col_index, current_aria_col_index);
290 current_aria_col_index = cell_data.aria_col_index;
291
292 // Update the row count and col count for the whole table to make
293 // sure they're large enough to fit this cell, including its spans.
294 // The -1 in the ARIA calculations is because ARIA indices are 1-based,
295 // whereas all other indices are zero-based.
296 row_count = std::max(row_count, cell_data.row_index + cell_data.row_span);
297 col_count = std::max(col_count, cell_data.col_index + cell_data.col_span);
299 aria_row_count = std::max(
301 static_cast<int>(current_aria_row_index + cell_data.row_span - 1));
302 }
304 aria_col_count = std::max(
306 static_cast<int>(current_aria_col_index + cell_data.col_span - 1));
307 }
308 // Update |current_col_index| to reflect the next available index after
309 // this cell including its colspan. The next column index in this row
310 // must be at least this large. Same for the current ARIA col index.
311 current_col_index = cell_data.col_index + cell_data.col_span;
312 current_aria_col_index = cell_data.aria_col_index + cell_data.col_span;
313 spanned_col_index = current_col_index;
314
315 // Add this cell to our vector.
316 cell_data_vector.push_back(cell_data);
317 }
318
319 // At the end of each row, increment |current_aria_row_index| to reflect the
320 // next available index after this row. The next row index must be at least
321 // this large. Also update |next_row_index|.
322 current_aria_row_index++;
323 next_row_index = *current_row_index + 1;
324 }
325}
326
327void AXTableInfo::BuildCellAndHeaderVectorsFromCellData() {
328 // Allocate space for the 2-D array of cell IDs and 1-D
329 // arrays of row headers and column headers.
330 row_headers.resize(row_count);
331 col_headers.resize(col_count);
332 // Fill in the arrays.
333 //
334 // At this point we have computed valid row and column indices for
335 // every cell in the table, and an accurate row and column count for the
336 // whole table that fits every cell and its spans. The final step is to
337 // fill in a 2-dimensional array that lets us look up an individual cell
338 // by its (row, column) coordinates, plus arrays to hold row and column
339 // headers.
340
341 // For cells.
342 cell_ids.resize(row_count);
343 for (size_t r = 0; r < row_count; r++) {
344 cell_ids[r].resize(col_count);
345 for (size_t c = 0; c < col_count; c++) {
346 const auto& row_it = incremental_row_col_map_.find(r);
347 if (row_it != incremental_row_col_map_.end()) {
348 const auto& col_it = row_it->second.find(c);
349 if (col_it != row_it->second.end())
350 cell_ids[r][c] = col_it->second.cell->id();
351 }
352 }
353 }
354
355 // No longer need this.
356 incremental_row_col_map_.clear();
357
358 // For relations.
359 for (auto& cell_data : cell_data_vector) {
360 for (size_t r = cell_data.row_index;
361 r < cell_data.row_index + cell_data.row_span; r++) {
363 for (size_t c = cell_data.col_index;
364 c < cell_data.col_index + cell_data.col_span; c++) {
366 AXNode* cell = cell_data.cell;
367 if (cell->data().role == ax::mojom::Role::kColumnHeader) {
368 col_headers[c].push_back(cell->id());
369 all_headers.push_back(cell->id());
370 } else if (cell->data().role == ax::mojom::Role::kRowHeader) {
371 row_headers[r].push_back(cell->id());
372 all_headers.push_back(cell->id());
373 }
374 }
375 }
376 }
377}
378
379void AXTableInfo::UpdateExtraMacNodes() {
380 // On macOS, maintain additional AXNodes: one column node for each
381 // column of the table, and one table header container.
382 //
383 // The nodes all set the table as the parent node, that way the Mac-specific
384 // platform code can treat these nodes as additional children of the table
385 // node.
386 //
387 // The columns have id -1, -2, -3, ... - this won't conflict with ids from
388 // the source tree, which are all positive.
389 //
390 // Each column has the kColumnIndex attribute set, and then each of the cells
391 // in that column gets added as an indirect ID. That exposes them as children
392 // via Mac APIs but ensures we don't explore those nodes multiple times when
393 // walking the tree. The column also has the ID of the first column header
394 // set.
395 //
396 // The table header container is just a node with all of the headers in the
397 // table as indirect children.
398
399 if (!extra_mac_nodes.empty()) {
400 // Delete old extra nodes.
401 ClearExtraMacNodes();
402 }
403
404 // One node for each column, and one more for the table header container.
405 size_t extra_node_count = col_count + 1;
406 // Resize.
407 extra_mac_nodes.resize(extra_node_count);
408
409 std::vector<AXTreeObserver::Change> changes;
410 changes.reserve(extra_node_count +
411 1); // Room for extra nodes + table itself.
412
413 // Create column nodes.
414 for (size_t i = 0; i < col_count; i++) {
415 extra_mac_nodes[i] = CreateExtraMacColumnNode(i);
416 changes.push_back(AXTreeObserver::Change(
418 }
419
420 // Create table header container node.
421 extra_mac_nodes[col_count] = CreateExtraMacTableHeaderNode();
422 changes.push_back(AXTreeObserver::Change(
424
425 // Update the columns to reflect current state of the table.
426 for (size_t i = 0; i < col_count; i++)
427 UpdateExtraMacColumnNodeAttributes(i);
428
429 // Update the table header container to contain all headers.
431 data.intlist_attributes.clear();
434 extra_mac_nodes[col_count]->SetData(data);
435
436 changes.push_back(AXTreeObserver::Change(
438 for (AXTreeObserver* observer : tree_->observers()) {
439 observer->OnAtomicUpdateFinished(tree_, false, changes);
440 }
441}
442
443AXNode* AXTableInfo::CreateExtraMacColumnNode(size_t col_index) {
444 int32_t id = tree_->GetNextNegativeInternalNodeId();
445 size_t index_in_parent = col_index + table_node_->children().size();
446 int32_t unignored_index_in_parent =
447 col_index + table_node_->GetUnignoredChildCount();
448 AXNode* node = new AXNode(tree_, table_node_, id, index_in_parent,
449 unignored_index_in_parent);
450 AXNodeData data;
451 data.id = id;
453 node->SetData(data);
454 for (AXTreeObserver* observer : tree_->observers())
455 observer->OnNodeCreated(tree_, node);
456 return node;
457}
458
459AXNode* AXTableInfo::CreateExtraMacTableHeaderNode() {
460 int32_t id = tree_->GetNextNegativeInternalNodeId();
461 size_t index_in_parent = col_count + table_node_->children().size();
462 int32_t unignored_index_in_parent =
463 col_count + table_node_->GetUnignoredChildCount();
464 AXNode* node = new AXNode(tree_, table_node_, id, index_in_parent,
465 unignored_index_in_parent);
466 AXNodeData data;
467 data.id = id;
469 node->SetData(data);
470
471 for (AXTreeObserver* observer : tree_->observers())
472 observer->OnNodeCreated(tree_, node);
473
474 return node;
475}
476
477void AXTableInfo::UpdateExtraMacColumnNodeAttributes(size_t col_index) {
478 ui::AXNodeData data = extra_mac_nodes[col_index]->data();
479 data.int_attributes.clear();
480
481 // Update the column index.
482 data.AddIntAttribute(IntAttribute::kTableColumnIndex,
483 static_cast<int32_t>(col_index));
484
485 // Update the column header.
486 if (!col_headers[col_index].empty()) {
487 data.AddIntAttribute(IntAttribute::kTableColumnHeaderId,
488 col_headers[col_index][0]);
489 }
490
491 // Update the list of cells in the column.
492 data.intlist_attributes.clear();
493 std::vector<int32_t> col_nodes;
494 int32_t last = 0;
495 for (size_t row_index = 0; row_index < row_count; row_index++) {
496 int32_t cell_id = cell_ids[row_index][col_index];
497 if (cell_id != 0 && cell_id != last)
498 col_nodes.push_back(cell_id);
499 last = cell_id;
500 }
502 col_nodes);
503 extra_mac_nodes[col_index]->SetData(data);
504}
505
506void AXTableInfo::ClearExtraMacNodes() {
507 for (AXNode* extra_mac_node : extra_mac_nodes) {
508 for (AXTreeObserver* observer : tree_->observers())
509 observer->OnNodeWillBeDeleted(tree_, extra_mac_node);
510 AXNode::AXID deleted_id = extra_mac_node->id();
511 delete extra_mac_node;
512 for (AXTreeObserver* observer : tree_->observers())
513 observer->OnNodeDeleted(tree_, deleted_id);
514 }
515 extra_mac_nodes.clear();
516}
517
518std::string AXTableInfo::ToString() const {
519 // First, scan through to get the length of the largest id.
520 int padding = 0;
521 for (size_t r = 0; r < row_count; r++) {
522 for (size_t c = 0; c < col_count; c++) {
523 // Extract the length of the id for padding purposes.
524 padding = std::max(padding, static_cast<int>(log10(cell_ids[r][c])));
525 }
526 }
527
528 std::string result;
529 for (size_t r = 0; r < row_count; r++) {
530 result += "|";
531 for (size_t c = 0; c < col_count; c++) {
532 int cell_id = cell_ids[r][c];
533 result += base::NumberToString(cell_id);
534 int cell_padding = padding;
535 if (cell_id != 0)
536 cell_padding = padding - static_cast<int>(log10(cell_id));
537 result += std::string(cell_padding, ' ') + '|';
538 }
539 result += "\n";
540 }
541 return result;
542}
543
544AXTableInfo::AXTableInfo(AXTree* tree, AXNode* table_node)
545 : tree_(tree), table_node_(table_node) {}
546
548 if (!extra_mac_nodes.empty()) {
549 ClearExtraMacNodes();
550 for (AXTreeObserver* observer : tree_->observers()) {
551 observer->OnAtomicUpdateFinished(
552 tree_, false,
554 }
555 }
556}
557
558} // namespace ui
static void info(const char *fmt,...) SK_PRINTF_LIKE(1
Definition DM.cpp:213
size_t GetUnignoredChildCount() const
Definition ax_node.cc:37
int32_t AXID
Definition ax_node.h:36
const std::vector< AXNode * > & children() const
Definition ax_node.h:113
AXNode * parent() const
Definition ax_node.h:111
bool IsTable() const
Definition ax_node.cc:540
int GetIntAttribute(ax::mojom::IntAttribute attribute) const
Definition ax_node.h:232
const AXNodeData & data() const
Definition ax_node.h:112
static AXTableInfo * Create(AXTree *tree, AXNode *table_node)
std::vector< std::vector< int32_t > > col_headers
std::vector< CellData > cell_data_vector
std::vector< std::vector< int32_t > > cell_ids
std::unordered_map< int32_t, size_t > row_id_to_index
std::vector< AXNode * > extra_mac_nodes
std::unordered_map< int32_t, size_t > cell_id_to_index
std::vector< AXNode * > row_nodes
std::string ToString() const
std::vector< int32_t > all_headers
std::vector< std::vector< int32_t > > row_headers
std::vector< int32_t > unique_cell_ids
bool enable_extra_mac_nodes() const
Definition ax_tree.h:141
AXNode * root() const
Definition ax_tree.h:57
int32_t GetNextNegativeInternalNodeId()
Definition ax_tree.cc:1947
std::vector< AXTreeObserver * > & observers()
Definition ax_tree.h:55
EMSCRIPTEN_KEEPALIVE void empty()
GAsyncResult * result
const int32_t kUnknownAriaColumnOrRowCount
std::string NumberToString(int32_t number)
DEF_SWITCHES_START aot vmservice shared library Name of the *so containing AOT compiled Dart assets for launching the service isolate vm snapshot data
Definition switches.h:41
bool IsCellOrTableHeader(const ax::mojom::Role role)
bool IsTableRow(ax::mojom::Role role)
bool IsTableLike(const ax::mojom::Role role)
ax::mojom::Role role
const uintptr_t id
#define BASE_DCHECK(condition)
Definition logging.h:63