Flutter iOS 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(ax::mojom::BoolAttribute::kSelected,
446  flags->is_selected);
447  node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kEditableRoot,
448  flags->is_text_field && !flags->is_read_only);
449  // Mark nodes as line breaking so that screen readers don't
450  // merge all consecutive objects into one.
451  // TODO(schectman): When should a node have this attribute set?
452  // https://github.com/flutter/flutter/issues/118184
453  node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
454  true);
455 }
456 
457 void AccessibilityBridge::SetIntAttributesFromFlutterUpdate(
458  ui::AXNodeData& node_data,
459  const SemanticsNode& node) {
460  const FlutterSemanticsFlags* flags = node.flags;
461  node_data.AddIntAttribute(ax::mojom::IntAttribute::kTextDirection,
462  node.text_direction);
463 
464  int sel_start = node.text_selection_base;
465  int sel_end = node.text_selection_extent;
466  if (flags->is_text_field && !flags->is_read_only && !node.value.empty()) {
467  // By default the text field selection should be at the end.
468  sel_start = sel_start == -1 ? node.value.length() : sel_start;
469  sel_end = sel_end == -1 ? node.value.length() : sel_end;
470  }
471  node_data.AddIntAttribute(ax::mojom::IntAttribute::kTextSelStart, sel_start);
472  node_data.AddIntAttribute(ax::mojom::IntAttribute::kTextSelEnd, sel_end);
473 
474  if (node_data.role == ax::mojom::Role::kRadioButton ||
475  node_data.role == ax::mojom::Role::kCheckBox) {
476  node_data.AddIntAttribute(
477  ax::mojom::IntAttribute::kCheckedState,
478  static_cast<int32_t>(
479  (flags->is_checked == FlutterCheckState::kFlutterCheckStateMixed)
480  ? ax::mojom::CheckedState::kMixed
481  : (flags->is_checked == FlutterCheckState::kFlutterCheckStateTrue)
482  ? ax::mojom::CheckedState::kTrue
483  : ax::mojom::CheckedState::kFalse));
484  } else if (node_data.role == ax::mojom::Role::kSwitch) {
485  node_data.AddIntAttribute(
486  ax::mojom::IntAttribute::kCheckedState,
487  static_cast<int32_t>(
488  (flags->is_toggled == FlutterTristate::kFlutterTristateTrue)
489  ? ax::mojom::CheckedState::kTrue
490  : ax::mojom::CheckedState::kFalse));
491  }
492 }
493 
494 void AccessibilityBridge::SetIntListAttributesFromFlutterUpdate(
495  ui::AXNodeData& node_data,
496  const SemanticsNode& node) {
497  FlutterSemanticsAction actions = node.actions;
498  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionCustomAction) {
499  std::vector<int32_t> custom_action_ids;
500  custom_action_ids.reserve(node.custom_accessibility_actions.size());
501  for (size_t i = 0; i < node.custom_accessibility_actions.size(); i++) {
502  custom_action_ids.push_back(node.custom_accessibility_actions[i]);
503  }
504  node_data.AddIntListAttribute(ax::mojom::IntListAttribute::kCustomActionIds,
505  custom_action_ids);
506  }
507 }
508 
509 void AccessibilityBridge::SetStringListAttributesFromFlutterUpdate(
510  ui::AXNodeData& node_data,
511  const SemanticsNode& node) {
512  FlutterSemanticsAction actions = node.actions;
513  if (actions & FlutterSemanticsAction::kFlutterSemanticsActionCustomAction) {
514  std::vector<std::string> custom_action_description;
515  for (size_t i = 0; i < node.custom_accessibility_actions.size(); i++) {
516  auto iter = pending_semantics_custom_action_updates_.find(
517  node.custom_accessibility_actions[i]);
518  BASE_DCHECK(iter != pending_semantics_custom_action_updates_.end());
519  custom_action_description.push_back(iter->second.label);
520  }
521  node_data.AddStringListAttribute(
522  ax::mojom::StringListAttribute::kCustomActionDescriptions,
523  custom_action_description);
524  }
525 }
526 
527 void AccessibilityBridge::SetIdentifierFromFlutterUpdate(
528  ui::AXNodeData& node_data,
529  const SemanticsNode& node) {
530  node_data.AddStringAttribute(ax::mojom::StringAttribute::kIdentifier,
531  node.identifier);
532 }
533 
534 void AccessibilityBridge::SetNameFromFlutterUpdate(ui::AXNodeData& node_data,
535  const SemanticsNode& node) {
536  node_data.SetName(node.label);
537 }
538 
539 void AccessibilityBridge::SetValueFromFlutterUpdate(ui::AXNodeData& node_data,
540  const SemanticsNode& node) {
541  node_data.SetValue(node.value);
542 }
543 
544 void AccessibilityBridge::SetTooltipFromFlutterUpdate(
545  ui::AXNodeData& node_data,
546  const SemanticsNode& node) {
547  node_data.SetTooltip(node.tooltip);
548 }
549 
550 void AccessibilityBridge::SetTreeData(const SemanticsNode& node,
551  ui::AXTreeUpdate& tree_update) {
552  const FlutterSemanticsFlags* flags = node.flags;
553  // Set selection of the focused node if:
554  // 1. this text field has a valid selection
555  // 2. this text field doesn't have a valid selection but had selection stored
556  // in the tree.
557  if (flags->is_text_field &&
558  flags->is_focused == FlutterTristate::kFlutterTristateTrue) {
559  if (node.text_selection_base != -1) {
560  tree_update.tree_data.sel_anchor_object_id = node.id;
561  tree_update.tree_data.sel_anchor_offset = node.text_selection_base;
562  tree_update.tree_data.sel_focus_object_id = node.id;
563  tree_update.tree_data.sel_focus_offset = node.text_selection_extent;
564  tree_update.has_tree_data = true;
565  } else if (tree_update.tree_data.sel_anchor_object_id == node.id) {
566  tree_update.tree_data.sel_anchor_object_id = ui::AXNode::kInvalidAXID;
567  tree_update.tree_data.sel_anchor_offset = -1;
568  tree_update.tree_data.sel_focus_object_id = ui::AXNode::kInvalidAXID;
569  tree_update.tree_data.sel_focus_offset = -1;
570  tree_update.has_tree_data = true;
571  }
572  }
573 
574  if (flags->is_focused == FlutterTristate::kFlutterTristateTrue &&
575  tree_update.tree_data.focus_id != node.id) {
576  tree_update.tree_data.focus_id = node.id;
577  tree_update.has_tree_data = true;
578  } else if (flags->is_focused != FlutterTristate::kFlutterTristateTrue &&
579  tree_update.tree_data.focus_id == node.id) {
580  tree_update.tree_data.focus_id = ui::AXNode::kInvalidAXID;
581  tree_update.has_tree_data = true;
582  }
583 }
584 
585 AccessibilityBridge::SemanticsNode
586 AccessibilityBridge::FromFlutterSemanticsNode(
587  const FlutterSemanticsNode2& flutter_node) {
588  SemanticsNode result;
589  result.id = flutter_node.id;
590  FML_DCHECK(flutter_node.flags2)
591  << "FlutterSemanticsNode2::flags2 must not be null";
592 
593  result.flags = flutter_node.flags2;
594  result.actions = flutter_node.actions;
595  result.heading_level = flutter_node.heading_level;
596  result.text_selection_base = flutter_node.text_selection_base;
597  result.text_selection_extent = flutter_node.text_selection_extent;
598  result.scroll_child_count = flutter_node.scroll_child_count;
599  result.scroll_index = flutter_node.scroll_index;
600  result.scroll_position = flutter_node.scroll_position;
601  result.scroll_extent_max = flutter_node.scroll_extent_max;
602  result.scroll_extent_min = flutter_node.scroll_extent_min;
603  if (flutter_node.label) {
604  result.label = std::string(flutter_node.label);
605  }
606  if (flutter_node.hint) {
607  result.hint = std::string(flutter_node.hint);
608  }
609  if (flutter_node.value) {
610  result.value = std::string(flutter_node.value);
611  }
612  if (flutter_node.increased_value) {
613  result.increased_value = std::string(flutter_node.increased_value);
614  }
615  if (flutter_node.decreased_value) {
616  result.decreased_value = std::string(flutter_node.decreased_value);
617  }
618  if (flutter_node.tooltip) {
619  result.tooltip = std::string(flutter_node.tooltip);
620  }
621  result.text_direction = flutter_node.text_direction;
622  result.rect = flutter_node.rect;
623  result.transform = flutter_node.transform;
624  if (flutter_node.child_count > 0) {
625  result.children_in_traversal_order = std::vector<int32_t>(
626  flutter_node.children_in_traversal_order,
627  flutter_node.children_in_traversal_order + flutter_node.child_count);
628  }
629  if (flutter_node.custom_accessibility_actions_count > 0) {
630  result.custom_accessibility_actions = std::vector<int32_t>(
631  flutter_node.custom_accessibility_actions,
632  flutter_node.custom_accessibility_actions +
633  flutter_node.custom_accessibility_actions_count);
634  }
635  if (flutter_node.identifier) {
636  result.identifier = std::string(flutter_node.identifier);
637  }
638  return result;
639 }
640 
641 AccessibilityBridge::SemanticsCustomAction
642 AccessibilityBridge::FromFlutterSemanticsCustomAction(
643  const FlutterSemanticsCustomAction2& flutter_custom_action) {
644  SemanticsCustomAction result;
645  result.id = flutter_custom_action.id;
646  result.override_action = flutter_custom_action.override_action;
647  if (flutter_custom_action.label) {
648  result.label = std::string(flutter_custom_action.label);
649  }
650  if (flutter_custom_action.hint) {
651  result.hint = std::string(flutter_custom_action.hint);
652  }
653  return result;
654 }
655 
656 void AccessibilityBridge::SetLastFocusedId(AccessibilityNodeId node_id) {
657  if (last_focused_id_ != node_id) {
658  auto last_focused_child =
659  GetFlutterPlatformNodeDelegateFromID(last_focused_id_);
660  if (!last_focused_child.expired()) {
662  last_focused_id_,
663  FlutterSemanticsAction::
664  kFlutterSemanticsActionDidLoseAccessibilityFocus,
665  {});
666  }
667  last_focused_id_ = node_id;
668  }
669 }
670 
671 AccessibilityNodeId AccessibilityBridge::GetLastFocusedId() {
672  return last_focused_id_;
673 }
674 
675 gfx::NativeViewAccessible AccessibilityBridge::GetNativeAccessibleFromId(
676  AccessibilityNodeId id) {
677  auto platform_node_delegate = GetFlutterPlatformNodeDelegateFromID(id).lock();
678  if (!platform_node_delegate) {
679  return nullptr;
680  }
681  return platform_node_delegate->GetNativeViewAccessible();
682 }
683 
684 gfx::RectF AccessibilityBridge::RelativeToGlobalBounds(const ui::AXNode* node,
685  bool& offscreen,
686  bool clip_bounds) {
687  return tree_->RelativeToTreeBounds(node, gfx::RectF(), &offscreen,
688  clip_bounds);
689 }
690 
692  ui::AXTreeID tree_id,
693  ui::AXNode::AXID node_id) const {
694  return GetNodeFromTree(node_id);
695 }
696 
698  ui::AXNode::AXID node_id) const {
699  return tree_->GetFromId(node_id);
700 }
701 
702 ui::AXTreeID AccessibilityBridge::GetTreeID() const {
703  return tree_->GetAXTreeID();
704 }
705 
707  return ui::AXTreeIDUnknown();
708 }
709 
711  return tree_->root();
712 }
713 
715  return nullptr;
716 }
717 
718 ui::AXTree* AccessibilityBridge::GetTree() const {
719  return tree_.get();
720 }
721 
723  const ui::AXNode::AXID node_id) const {
724  auto platform_delegate_weak = GetFlutterPlatformNodeDelegateFromID(node_id);
725  auto platform_delegate = platform_delegate_weak.lock();
726  if (!platform_delegate) {
727  return nullptr;
728  }
729  return platform_delegate->GetPlatformNode();
730 }
731 
733  const ui::AXNode& node) const {
734  return GetPlatformNodeFromTree(node.id());
735 }
736 
737 ui::AXPlatformNodeDelegate* AccessibilityBridge::RootDelegate() const {
739  .lock()
740  .get();
741 }
742 
743 } // namespace flutter
std::unique_ptr< flutter::PlatformViewIOS > platform_view
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...
constexpr int kHasScrollingAction
ui::AXNode::AXID AccessibilityNodeId