Flutter Linux Embedder
accessibility_bridge.cc
Go to the documentation of this file.
1 // Copyright 2013 The Flutter 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 "accessibility_bridge.h"
6 
7 #include <functional>
8 #include <utility>
9 
10 #include "flutter/third_party/accessibility/ax/ax_tree_manager_map.h"
11 #include "flutter/third_party/accessibility/ax/ax_tree_update.h"
12 #include "flutter/third_party/accessibility/base/logging.h"
13 
14 namespace flutter { // namespace
15 
16 constexpr int kHasScrollingAction =
17  FlutterSemanticsAction::kFlutterSemanticsActionScrollLeft |
18  FlutterSemanticsAction::kFlutterSemanticsActionScrollRight |
19  FlutterSemanticsAction::kFlutterSemanticsActionScrollUp |
20  FlutterSemanticsAction::kFlutterSemanticsActionScrollDown;
21 
22 // AccessibilityBridge
24  : tree_(std::make_unique<ui::AXTree>()) {
25  event_generator_.SetTree(tree_.get());
26  tree_->AddObserver(static_cast<ui::AXTreeObserver*>(this));
27  ui::AXTreeData data = tree_->data();
28  data.tree_id = ui::AXTreeID::CreateNewAXTreeID();
29  tree_->UpdateData(data);
30  ui::AXTreeManagerMap::GetInstance().AddTreeManager(tree_->GetAXTreeID(),
31  this);
32 }
33 
35  event_generator_.ReleaseTree();
36  tree_->RemoveObserver(static_cast<ui::AXTreeObserver*>(this));
37 }
38 
40  const FlutterSemanticsNode2& node) {
41  pending_semantics_node_updates_[node.id] = FromFlutterSemanticsNode(node);
42 }
43 
45  const FlutterSemanticsCustomAction2& action) {
46  pending_semantics_custom_action_updates_[action.id] =
47  FromFlutterSemanticsCustomAction(action);
48 }
49 
51  // AXTree cannot move a node in a single update.
52  // This must be split across two updates:
53  //
54  // * Update 1: remove nodes from their old parents.
55  // * Update 2: re-add nodes (including their children) to their new parents.
56  //
57  // First, start by removing nodes if necessary.
58  std::optional<ui::AXTreeUpdate> remove_reparented =
59  CreateRemoveReparentedNodesUpdate();
60  if (remove_reparented.has_value()) {
61  tree_->Unserialize(remove_reparented.value());
62 
63  std::string error = tree_->error();
64  if (!error.empty()) {
65  FML_LOG(ERROR) << "Failed to update ui::AXTree, error: " << error;
66  assert(false);
67  return;
68  }
69  }
70 
71  // Second, apply the pending node updates. This also moves reparented nodes to
72  // their new parents if needed.
73  ui::AXTreeUpdate update{.tree_data = tree_->data()};
74 
75  // Figure out update order, ui::AXTree only accepts update in tree order,
76  // where parent node must come before the child node in
77  // ui::AXTreeUpdate.nodes. We start with picking a random node and turn the
78  // entire subtree into a list. We pick another node from the remaining update,
79  // and keep doing so until the update map is empty. We then concatenate the
80  // lists in the reversed order, this guarantees parent updates always come
81  // before child updates. If the root is in the update, it is guaranteed to
82  // be the first node of the last list.
83  std::vector<std::vector<SemanticsNode>> results;
84  while (!pending_semantics_node_updates_.empty()) {
85  auto begin = pending_semantics_node_updates_.begin();
86  SemanticsNode target = begin->second;
87  std::vector<SemanticsNode> sub_tree_list;
88  GetSubTreeList(target, sub_tree_list);
89  results.push_back(sub_tree_list);
90  pending_semantics_node_updates_.erase(begin);
91  }
92 
93  for (size_t i = results.size(); i > 0; i--) {
94  for (const SemanticsNode& node : results[i - 1]) {
95  ConvertFlutterUpdate(node, update);
96  }
97  }
98 
99  // The first update must set the tree's root, which is guaranteed to be the
100  // last list's first node. A tree's root node never changes, though it can be
101  // modified.
102  if (!results.empty() && GetRootAsAXNode()->id() == ui::AXNode::kInvalidAXID) {
103  FML_DCHECK(!results.back().empty());
104 
105  update.root_id = results.back().front().id;
106  }
107 
108  tree_->Unserialize(update);
109  pending_semantics_node_updates_.clear();
110  pending_semantics_custom_action_updates_.clear();
111 
112  std::string error = tree_->error();
113  if (!error.empty()) {
114  FML_LOG(ERROR) << "Failed to update ui::AXTree, error: " << error;
115  return;
116  }
117  // Handles accessibility events as the result of the semantics update.
118  for (const auto& targeted_event : event_generator_) {
119  auto event_target =
120  GetFlutterPlatformNodeDelegateFromID(targeted_event.node->id());
121  if (event_target.expired()) {
122  continue;
123  }
124 
125  OnAccessibilityEvent(targeted_event);
126  }
127  event_generator_.ClearEvents();
128 }
129 
130 std::weak_ptr<FlutterPlatformNodeDelegate>
132  AccessibilityNodeId id) const {
133  const auto iter = id_wrapper_map_.find(id);
134  if (iter != id_wrapper_map_.end()) {
135  return iter->second;
136  }
137 
138  return std::weak_ptr<FlutterPlatformNodeDelegate>();
139 }
140 
141 const ui::AXTreeData& AccessibilityBridge::GetAXTreeData() const {
142  return tree_->data();
143 }
144 
145 const std::vector<ui::AXEventGenerator::TargetedEvent>
147  std::vector<ui::AXEventGenerator::TargetedEvent> result(
148  event_generator_.begin(), event_generator_.end());
149  return result;
150 }
151 
152 void AccessibilityBridge::OnNodeWillBeDeleted(ui::AXTree* tree,
153  ui::AXNode* node) {}
154 
155 void AccessibilityBridge::OnSubtreeWillBeDeleted(ui::AXTree* tree,
156  ui::AXNode* node) {}
157 
158 void AccessibilityBridge::OnNodeReparented(ui::AXTree* tree, ui::AXNode* node) {
159 }
160 
161 void AccessibilityBridge::OnRoleChanged(ui::AXTree* tree,
162  ui::AXNode* node,
163  ax::mojom::Role old_role,
164  ax::mojom::Role new_role) {}
165 
166 void AccessibilityBridge::OnNodeDataChanged(
167  ui::AXTree* tree,
168  const ui::AXNodeData& old_node_data,
169  const ui::AXNodeData& new_node_data) {
170  auto platform_view =
171  GetFlutterPlatformNodeDelegateFromID(new_node_data.id).lock();
172  if (platform_view) {
173  platform_view->NodeDataChanged(old_node_data, new_node_data);
174  }
175 }
176 
177 void AccessibilityBridge::OnNodeCreated(ui::AXTree* tree, ui::AXNode* node) {
178  BASE_DCHECK(node);
179  id_wrapper_map_[node->id()] = CreateFlutterPlatformNodeDelegate();
180  id_wrapper_map_[node->id()]->Init(
181  std::static_pointer_cast<FlutterPlatformNodeDelegate::OwnerBridge>(
182  shared_from_this()),
183  node);
184 }
185 
186 void AccessibilityBridge::OnNodeDeleted(ui::AXTree* tree,
187  AccessibilityNodeId node_id) {
188  BASE_DCHECK(node_id != ui::AXNode::kInvalidAXID);
189  if (id_wrapper_map_.find(node_id) != id_wrapper_map_.end()) {
190  id_wrapper_map_.erase(node_id);
191  }
192 }
193 
194 void AccessibilityBridge::OnAtomicUpdateFinished(
195  ui::AXTree* tree,
196  bool root_changed,
197  const std::vector<ui::AXTreeObserver::Change>& changes) {
198  // The Flutter semantics update does not include child->parent relationship
199  // We have to update the relative bound offset container id here in order
200  // to calculate the screen bound correctly.
201  for (const auto& change : changes) {
202  ui::AXNode* node = change.node;
203  const ui::AXNodeData& data = node->data();
204  AccessibilityNodeId offset_container_id = -1;
205  if (node->parent()) {
206  offset_container_id = node->parent()->id();
207  }
208  node->SetLocation(offset_container_id, data.relative_bounds.bounds,
209  data.relative_bounds.transform.get());
210  }
211 }
212 
213 std::optional<ui::AXTreeUpdate>
214 AccessibilityBridge::CreateRemoveReparentedNodesUpdate() {
215  std::unordered_map<int32_t, ui::AXNodeData> updates;
216 
217  for (const auto& node_update : pending_semantics_node_updates_) {
218  for (int32_t child_id : node_update.second.children_in_traversal_order) {
219  // Skip nodes that don't exist or have a parent in the current tree.
220  ui::AXNode* child = tree_->GetFromId(child_id);
221  if (!child) {
222  continue;
223  }
224 
225  // Flutter's root node should never be reparented.
226  assert(child->parent());
227 
228  // Skip nodes whose parents are unchanged.
229  if (child->parent()->id() == node_update.second.id) {
230  continue;
231  }
232 
233  // This pending update moves the current child node.
234  // That new child must have a corresponding pending update.
235  assert(pending_semantics_node_updates_.find(child_id) !=
236  pending_semantics_node_updates_.end());
237 
238  // Create an update to remove the child from its previous parent.
239  int32_t parent_id = child->parent()->id();
240  if (updates.find(parent_id) == updates.end()) {
241  updates[parent_id] = tree_->GetFromId(parent_id)->data();
242  }
243 
244  ui::AXNodeData* parent = &updates[parent_id];
245  auto iter = std::find(parent->child_ids.begin(), parent->child_ids.end(),
246  child_id);
247 
248  assert(iter != parent->child_ids.end());
249  parent->child_ids.erase(iter);
250  }
251  }
252 
253  if (updates.empty()) {
254  return std::nullopt;
255  }
256 
257  ui::AXTreeUpdate update{
258  .tree_data = tree_->data(),
259  .nodes = std::vector<ui::AXNodeData>(),
260  };
261 
262  for (std::pair<int32_t, ui::AXNodeData> data : updates) {
263  update.nodes.push_back(std::move(data.second));
264  }
265 
266  return update;
267 }
268 
269 // Private method.
270 void AccessibilityBridge::GetSubTreeList(const SemanticsNode& target,
271  std::vector<SemanticsNode>& result) {
272  result.push_back(target);
273  for (int32_t child : target.children_in_traversal_order) {
274  auto iter = pending_semantics_node_updates_.find(child);
275  if (iter != pending_semantics_node_updates_.end()) {
276  SemanticsNode node = iter->second;
277  GetSubTreeList(node, result);
278  pending_semantics_node_updates_.erase(iter);
279  }
280  }
281 }
282 
283 void AccessibilityBridge::ConvertFlutterUpdate(const SemanticsNode& node,
284  ui::AXTreeUpdate& tree_update) {
285  ui::AXNodeData node_data;
286  node_data.id = node.id;
287  SetRoleFromFlutterUpdate(node_data, node);
288  SetStateFromFlutterUpdate(node_data, node);
289  SetActionsFromFlutterUpdate(node_data, node);
290  SetBooleanAttributesFromFlutterUpdate(node_data, node);
291  SetIntAttributesFromFlutterUpdate(node_data, node);
292  SetIntListAttributesFromFlutterUpdate(node_data, node);
293  SetStringListAttributesFromFlutterUpdate(node_data, node);
294  SetIdentifierFromFlutterUpdate(node_data, node);
295  SetNameFromFlutterUpdate(node_data, node);
296  SetValueFromFlutterUpdate(node_data, node);
297  SetTooltipFromFlutterUpdate(node_data, node);
298  node_data.relative_bounds.bounds.SetRect(node.rect.left, node.rect.top,
299  node.rect.right - node.rect.left,
300  node.rect.bottom - node.rect.top);
301  node_data.relative_bounds.transform = std::make_unique<gfx::Transform>(
302  node.transform.scaleX, node.transform.skewX, node.transform.transX, 0,
303  node.transform.skewY, node.transform.scaleY, node.transform.transY, 0,
304  node.transform.pers0, node.transform.pers1, node.transform.pers2, 0, 0, 0,
305  0, 0);
306  for (auto child : node.children_in_traversal_order) {
307  node_data.child_ids.push_back(child);
308  }
309  SetTreeData(node, tree_update);
310  tree_update.nodes.push_back(node_data);
311 }
312 
313 void AccessibilityBridge::SetRoleFromFlutterUpdate(ui::AXNodeData& node_data,
314  const SemanticsNode& node) {
315  const FlutterSemanticsFlags* flags = node.flags;
316  FML_DCHECK(flags) << "SemanticsNode::flags must not be null";
317  if (flags->is_button) {
318  node_data.role = ax::mojom::Role::kButton;
319  return;
320  }
321  if (flags->is_text_field && !flags->is_read_only) {
322  node_data.role = ax::mojom::Role::kTextField;
323  return;
324  }
325  if (flags->is_header) {
326  node_data.role = ax::mojom::Role::kHeader;
327  return;
328  }
329  if (flags->is_image) {
330  node_data.role = ax::mojom::Role::kImage;
331  return;
332  }
333  if (flags->is_link) {
334  node_data.role = ax::mojom::Role::kLink;
335  return;
336  }
337 
338  if (flags->is_in_mutually_exclusive_group &&
339  flags->is_checked != FlutterCheckState::kFlutterCheckStateNone) {
340  node_data.role = ax::mojom::Role::kRadioButton;
341  return;
342  }
343  if (flags->is_checked != FlutterCheckState::kFlutterCheckStateNone) {
344  node_data.role = ax::mojom::Role::kCheckBox;
345  return;
346  }
347  if (flags->is_toggled != FlutterTristate::kFlutterTristateNone) {
348  node_data.role = ax::mojom::Role::kSwitch;
349  return;
350  }
351  if (flags->is_slider) {
352  node_data.role = ax::mojom::Role::kSlider;
353  return;
354  }
355  // If the state cannot be derived from the flutter flags, we fallback to group
356  // or static text.
357  if (node.children_in_traversal_order.empty()) {
358  node_data.role = ax::mojom::Role::kStaticText;
359  } else {
360  node_data.role = ax::mojom::Role::kGroup;
361  }
362 }
363 
364 void AccessibilityBridge::SetStateFromFlutterUpdate(ui::AXNodeData& node_data,
365  const SemanticsNode& node) {
366  const FlutterSemanticsFlags* flags = node.flags;
367  FlutterSemanticsAction actions = node.actions;
368  if (flags->is_expanded == FlutterTristate::kFlutterTristateTrue) {
369  node_data.AddState(ax::mojom::State::kExpanded);
370  } else if (flags->is_expanded == FlutterTristate::kFlutterTristateFalse) {
371  node_data.AddState(ax::mojom::State::kCollapsed);
372  }
373  if (flags->is_text_field && !flags->is_read_only) {
374  node_data.AddState(ax::mojom::State::kEditable);
375  }
376  if (node_data.role == ax::mojom::Role::kStaticText &&
377  (actions & kHasScrollingAction) == 0 && node.value.empty() &&
378  node.label.empty() && node.hint.empty()) {
379  node_data.AddState(ax::mojom::State::kIgnored);
380  } else {
381  // kFlutterSemanticsFlagIsFocusable means a keyboard focusable, it is
382  // different from semantics focusable.
383  // TODO(chunhtai): figure out whether something is not semantics focusable.
384  node_data.AddState(ax::mojom::State::kFocusable);
385  }
386 }
387 
388 void AccessibilityBridge::SetActionsFromFlutterUpdate(
389  ui::AXNodeData& node_data,
390  const SemanticsNode& node) {
391  FlutterSemanticsAction actions = node.actions;
392  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionTap) {
393  node_data.AddAction(ax::mojom::Action::kDoDefault);
394  }
395  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionScrollLeft) {
396  node_data.AddAction(ax::mojom::Action::kScrollLeft);
397  }
398  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionScrollRight) {
399  node_data.AddAction(ax::mojom::Action::kScrollRight);
400  }
401  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionScrollUp) {
402  node_data.AddAction(ax::mojom::Action::kScrollUp);
403  }
404  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionScrollDown) {
405  node_data.AddAction(ax::mojom::Action::kScrollDown);
406  }
407  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionIncrease) {
408  node_data.AddAction(ax::mojom::Action::kIncrement);
409  }
410  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionDecrease) {
411  node_data.AddAction(ax::mojom::Action::kDecrement);
412  }
413  // Every node has show on screen action.
414  node_data.AddAction(ax::mojom::Action::kScrollToMakeVisible);
415 
416  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionSetSelection) {
417  node_data.AddAction(ax::mojom::Action::kSetSelection);
418  }
419  if (actions & FlutterSemanticsAction::
420  kFlutterSemanticsActionDidGainAccessibilityFocus) {
421  node_data.AddAction(ax::mojom::Action::kSetAccessibilityFocus);
422  }
423  if (actions & FlutterSemanticsAction::
424  kFlutterSemanticsActionDidLoseAccessibilityFocus) {
425  node_data.AddAction(ax::mojom::Action::kClearAccessibilityFocus);
426  }
427  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionCustomAction) {
428  node_data.AddAction(ax::mojom::Action::kCustomAction);
429  }
430 }
431 
432 void AccessibilityBridge::SetBooleanAttributesFromFlutterUpdate(
433  ui::AXNodeData& node_data,
434  const SemanticsNode& node) {
435  FlutterSemanticsAction actions = node.actions;
436  const FlutterSemanticsFlags* flags = node.flags;
437  node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kScrollable,
438  actions & kHasScrollingAction);
439  node_data.AddBoolAttribute(
440  ax::mojom::BoolAttribute::kClickable,
441  actions & FlutterSemanticsAction::kFlutterSemanticsActionTap);
442  // TODO(chunhtai): figure out if there is a node that does not clip overflow.
443  node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kClipsChildren,
444  !node.children_in_traversal_order.empty());
445  node_data.AddBoolAttribute(
446  ax::mojom::BoolAttribute::kSelected,
447  flags->is_selected == FlutterTristate::kFlutterTristateTrue);
448  node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot,
449  flags->is_text_field && !flags->is_read_only);
450  // Mark nodes as line breaking so that screen readers don't
451  // merge all consecutive objects into one.
452  // TODO(schectman): When should a node have this attribute set?
453  // https://github.com/flutter/flutter/issues/118184
454  node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
455  true);
456 }
457 
458 void AccessibilityBridge::SetIntAttributesFromFlutterUpdate(
459  ui::AXNodeData& node_data,
460  const SemanticsNode& node) {
461  const FlutterSemanticsFlags* flags = node.flags;
462  node_data.AddIntAttribute(ax::mojom::IntAttribute::kTextDirection,
463  node.text_direction);
464 
465  int sel_start = node.text_selection_base;
466  int sel_end = node.text_selection_extent;
467  if (flags->is_text_field && !flags->is_read_only && !node.value.empty()) {
468  // By default the text field selection should be at the end.
469  sel_start = sel_start == -1 ? node.value.length() : sel_start;
470  sel_end = sel_end == -1 ? node.value.length() : sel_end;
471  }
472  node_data.AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, sel_start);
473  node_data.AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, sel_end);
474 
475  if (node_data.role == ax::mojom::Role::kRadioButton ||
476  node_data.role == ax::mojom::Role::kCheckBox) {
477  node_data.AddIntAttribute(
478  ax::mojom::IntAttribute::kCheckedState,
479  static_cast<int32_t>(
480  (flags->is_checked == FlutterCheckState::kFlutterCheckStateMixed)
481  ? ax::mojom::CheckedState::kMixed
482  : (flags->is_checked == FlutterCheckState::kFlutterCheckStateTrue)
483  ? ax::mojom::CheckedState::kTrue
484  : ax::mojom::CheckedState::kFalse));
485  } else if (node_data.role == ax::mojom::Role::kSwitch) {
486  node_data.AddIntAttribute(
487  ax::mojom::IntAttribute::kCheckedState,
488  static_cast<int32_t>(
489  (flags->is_toggled == FlutterTristate::kFlutterTristateTrue)
490  ? ax::mojom::CheckedState::kTrue
491  : ax::mojom::CheckedState::kFalse));
492  }
493 }
494 
495 void AccessibilityBridge::SetIntListAttributesFromFlutterUpdate(
496  ui::AXNodeData& node_data,
497  const SemanticsNode& node) {
498  FlutterSemanticsAction actions = node.actions;
499  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionCustomAction) {
500  std::vector<int32_t> custom_action_ids;
501  custom_action_ids.reserve(node.custom_accessibility_actions.size());
502  for (size_t i = 0; i < node.custom_accessibility_actions.size(); i++) {
503  custom_action_ids.push_back(node.custom_accessibility_actions[i]);
504  }
505  node_data.AddIntListAttribute(ax::mojom::IntListAttribute::kCustomActionIds,
506  custom_action_ids);
507  }
508 }
509 
510 void AccessibilityBridge::SetStringListAttributesFromFlutterUpdate(
511  ui::AXNodeData& node_data,
512  const SemanticsNode& node) {
513  FlutterSemanticsAction actions = node.actions;
514  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionCustomAction) {
515  std::vector<std::string> custom_action_description;
516  for (size_t i = 0; i < node.custom_accessibility_actions.size(); i++) {
517  auto iter = pending_semantics_custom_action_updates_.find(
518  node.custom_accessibility_actions[i]);
519  BASE_DCHECK(iter != pending_semantics_custom_action_updates_.end());
520  custom_action_description.push_back(iter->second.label);
521  }
522  node_data.AddStringListAttribute(
523  ax::mojom::StringListAttribute::kCustomActionDescriptions,
524  custom_action_description);
525  }
526 }
527 
528 void AccessibilityBridge::SetIdentifierFromFlutterUpdate(
529  ui::AXNodeData& node_data,
530  const SemanticsNode& node) {
531  node_data.AddStringAttribute(ax::mojom::StringAttribute::kIdentifier,
532  node.identifier);
533 }
534 
535 void AccessibilityBridge::SetNameFromFlutterUpdate(ui::AXNodeData& node_data,
536  const SemanticsNode& node) {
537  node_data.SetName(node.label);
538 }
539 
540 void AccessibilityBridge::SetValueFromFlutterUpdate(ui::AXNodeData& node_data,
541  const SemanticsNode& node) {
542  node_data.SetValue(node.value);
543 }
544 
545 void AccessibilityBridge::SetTooltipFromFlutterUpdate(
546  ui::AXNodeData& node_data,
547  const SemanticsNode& node) {
548  node_data.SetTooltip(node.tooltip);
549 }
550 
551 void AccessibilityBridge::SetTreeData(const SemanticsNode& node,
552  ui::AXTreeUpdate& tree_update) {
553  const FlutterSemanticsFlags* flags = node.flags;
554  // Set selection of the focused node if:
555  // 1. this text field has a valid selection
556  // 2. this text field doesn't have a valid selection but had selection stored
557  // in the tree.
558  if (flags->is_text_field &&
559  flags->is_focused == FlutterTristate::kFlutterTristateTrue) {
560  if (node.text_selection_base != -1) {
561  tree_update.tree_data.sel_anchor_object_id = node.id;
562  tree_update.tree_data.sel_anchor_offset = node.text_selection_base;
563  tree_update.tree_data.sel_focus_object_id = node.id;
564  tree_update.tree_data.sel_focus_offset = node.text_selection_extent;
565  tree_update.has_tree_data = true;
566  } else if (tree_update.tree_data.sel_anchor_object_id == node.id) {
567  tree_update.tree_data.sel_anchor_object_id = ui::AXNode::kInvalidAXID;
568  tree_update.tree_data.sel_anchor_offset = -1;
569  tree_update.tree_data.sel_focus_object_id = ui::AXNode::kInvalidAXID;
570  tree_update.tree_data.sel_focus_offset = -1;
571  tree_update.has_tree_data = true;
572  }
573  }
574 
575  if (flags->is_focused == FlutterTristate::kFlutterTristateTrue &&
576  tree_update.tree_data.focus_id != node.id) {
577  tree_update.tree_data.focus_id = node.id;
578  tree_update.has_tree_data = true;
579  } else if (flags->is_focused != FlutterTristate::kFlutterTristateTrue &&
580  tree_update.tree_data.focus_id == node.id) {
581  tree_update.tree_data.focus_id = ui::AXNode::kInvalidAXID;
582  tree_update.has_tree_data = true;
583  }
584 }
585 
586 AccessibilityBridge::SemanticsNode
587 AccessibilityBridge::FromFlutterSemanticsNode(
588  const FlutterSemanticsNode2& flutter_node) {
589  SemanticsNode result;
590  result.id = flutter_node.id;
591  FML_DCHECK(flutter_node.flags2)
592  << "FlutterSemanticsNode2::flags2 must not be null";
593 
594  result.flags = flutter_node.flags2;
595  result.actions = flutter_node.actions;
596  result.heading_level = flutter_node.heading_level;
597  result.text_selection_base = flutter_node.text_selection_base;
598  result.text_selection_extent = flutter_node.text_selection_extent;
599  result.scroll_child_count = flutter_node.scroll_child_count;
600  result.scroll_index = flutter_node.scroll_index;
601  result.scroll_position = flutter_node.scroll_position;
602  result.scroll_extent_max = flutter_node.scroll_extent_max;
603  result.scroll_extent_min = flutter_node.scroll_extent_min;
604  if (flutter_node.label) {
605  result.label = std::string(flutter_node.label);
606  }
607  if (flutter_node.hint) {
608  result.hint = std::string(flutter_node.hint);
609  }
610  if (flutter_node.value) {
611  result.value = std::string(flutter_node.value);
612  }
613  if (flutter_node.increased_value) {
614  result.increased_value = std::string(flutter_node.increased_value);
615  }
616  if (flutter_node.decreased_value) {
617  result.decreased_value = std::string(flutter_node.decreased_value);
618  }
619  if (flutter_node.tooltip) {
620  result.tooltip = std::string(flutter_node.tooltip);
621  }
622  result.text_direction = flutter_node.text_direction;
623  result.rect = flutter_node.rect;
624  result.transform = flutter_node.transform;
625  if (flutter_node.child_count > 0) {
626  result.children_in_traversal_order = std::vector<int32_t>(
627  flutter_node.children_in_traversal_order,
628  flutter_node.children_in_traversal_order + flutter_node.child_count);
629  }
630  if (flutter_node.custom_accessibility_actions_count > 0) {
631  result.custom_accessibility_actions = std::vector<int32_t>(
632  flutter_node.custom_accessibility_actions,
633  flutter_node.custom_accessibility_actions +
634  flutter_node.custom_accessibility_actions_count);
635  }
636  if (flutter_node.identifier) {
637  result.identifier = std::string(flutter_node.identifier);
638  }
639  return result;
640 }
641 
642 AccessibilityBridge::SemanticsCustomAction
643 AccessibilityBridge::FromFlutterSemanticsCustomAction(
644  const FlutterSemanticsCustomAction2& flutter_custom_action) {
645  SemanticsCustomAction result;
646  result.id = flutter_custom_action.id;
647  result.override_action = flutter_custom_action.override_action;
648  if (flutter_custom_action.label) {
649  result.label = std::string(flutter_custom_action.label);
650  }
651  if (flutter_custom_action.hint) {
652  result.hint = std::string(flutter_custom_action.hint);
653  }
654  return result;
655 }
656 
657 void AccessibilityBridge::SetLastFocusedId(AccessibilityNodeId node_id) {
658  if (last_focused_id_ != node_id) {
659  auto last_focused_child =
660  GetFlutterPlatformNodeDelegateFromID(last_focused_id_);
661  if (!last_focused_child.expired()) {
663  last_focused_id_,
664  FlutterSemanticsAction::
665  kFlutterSemanticsActionDidLoseAccessibilityFocus,
666  {});
667  }
668  last_focused_id_ = node_id;
669  }
670 }
671 
672 AccessibilityNodeId AccessibilityBridge::GetLastFocusedId() {
673  return last_focused_id_;
674 }
675 
676 gfx::NativeViewAccessible AccessibilityBridge::GetNativeAccessibleFromId(
677  AccessibilityNodeId id) {
678  auto platform_node_delegate = GetFlutterPlatformNodeDelegateFromID(id).lock();
679  if (!platform_node_delegate) {
680  return nullptr;
681  }
682  return platform_node_delegate->GetNativeViewAccessible();
683 }
684 
685 gfx::RectF AccessibilityBridge::RelativeToGlobalBounds(const ui::AXNode* node,
686  bool& offscreen,
687  bool clip_bounds) {
688  return tree_->RelativeToTreeBounds(node, gfx::RectF(), &offscreen,
689  clip_bounds);
690 }
691 
693  ui::AXTreeID tree_id,
694  ui::AXNode::AXID node_id) const {
695  return GetNodeFromTree(node_id);
696 }
697 
699  ui::AXNode::AXID node_id) const {
700  return tree_->GetFromId(node_id);
701 }
702 
703 ui::AXTreeID AccessibilityBridge::GetTreeID() const {
704  return tree_->GetAXTreeID();
705 }
706 
708  return ui::AXTreeIDUnknown();
709 }
710 
712  return tree_->root();
713 }
714 
716  return nullptr;
717 }
718 
719 ui::AXTree* AccessibilityBridge::GetTree() const {
720  return tree_.get();
721 }
722 
724  const ui::AXNode::AXID node_id) const {
725  auto platform_delegate_weak = GetFlutterPlatformNodeDelegateFromID(node_id);
726  auto platform_delegate = platform_delegate_weak.lock();
727  if (!platform_delegate) {
728  return nullptr;
729  }
730  return platform_delegate->GetPlatformNode();
731 }
732 
734  const ui::AXNode& node) const {
735  return GetPlatformNodeFromTree(node.id());
736 }
737 
738 ui::AXPlatformNodeDelegate* AccessibilityBridge::RootDelegate() const {
740  .lock()
741  .get();
742 }
743 
744 } // namespace flutter
ui::AXTree * GetTree() const override
ui::AXPlatformNodeDelegate * RootDelegate() const override
std::weak_ptr< FlutterPlatformNodeDelegate > GetFlutterPlatformNodeDelegateFromID(AccessibilityNodeId id) const
Get the flutter platform node delegate with the given id from this accessibility bridge....
virtual std::shared_ptr< FlutterPlatformNodeDelegate > CreateFlutterPlatformNodeDelegate()=0
Creates a platform specific FlutterPlatformNodeDelegate. Ownership passes to the caller....
void AddFlutterSemanticsNodeUpdate(const FlutterSemanticsNode2 &node)
Adds a semantics node update to the pending semantics update. Calling this method alone will NOT upda...
void AddFlutterSemanticsCustomActionUpdate(const FlutterSemanticsCustomAction2 &action)
Adds a custom semantics action update to the pending semantics update. Calling this method alone will...
ui::AXPlatformNode * GetPlatformNodeFromTree(const ui::AXNode::AXID node_id) const override
const ui::AXTreeData & GetAXTreeData() const
Get the ax tree data from this accessibility bridge. The tree data contains information such as the i...
ui::AXTreeID GetParentTreeID() const override
ui::AXNode * GetRootAsAXNode() const override
ui::AXNode * GetParentNodeFromParentTreeAsAXNode() const override
AccessibilityBridge()
Creates a new instance of a accessibility bridge.
virtual void OnAccessibilityEvent(ui::AXEventGenerator::TargetedEvent targeted_event)=0
Handle accessibility events generated due to accessibility tree changes. These events are needed to b...
ui::AXTreeID GetTreeID() const override
const std::vector< ui::AXEventGenerator::TargetedEvent > GetPendingEvents() const
Gets all pending accessibility events generated during semantics updates. This is useful when decidin...
void CommitUpdates()
Flushes the pending updates and applies them to this accessibility bridge. Calling this with no pendi...
ui::AXNode * GetNodeFromTree(const ui::AXTreeID tree_id, const ui::AXNode::AXID node_id) const override
virtual void DispatchAccessibilityAction(AccessibilityNodeId target, FlutterSemanticsAction action, fml::MallocMapping data)=0
Dispatch accessibility action back to the Flutter framework. These actions are generated in the nativ...
const uint8_t uint32_t uint32_t GError ** error
uint32_t * target
constexpr int kHasScrollingAction
ui::AXNode::AXID AccessibilityNodeId