Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Implement ITextProvider and ITextRangeProvider for UIA #38284

Closed
wants to merge 25 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ci/licenses_golden/excluded_files
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_base_unittest.cc
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_mac_unittest.h
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_mac_unittest.mm
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_unittest.cc
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_unittest.h
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_win_unittest.cc
Expand Down
11 changes: 11 additions & 0 deletions ci/licenses_golden/licenses_flutter
Original file line number Diff line number Diff line change
Expand Up @@ -5589,6 +5589,7 @@ FILE: ../../../flutter/shell/profiling/sampling_profiler.h
FILE: ../../../flutter/shell/version/version.cc
FILE: ../../../flutter/shell/version/version.h
FILE: ../../../flutter/shell/vmservice/empty.dart
FILE: ../../../flutter/sky/tools/roll/patches/chromium/android_build.patch
FILE: ../../../flutter/third_party/accessibility/base/color_utils.h
FILE: ../../../flutter/third_party/accessibility/base/compiler_specific.h
FILE: ../../../flutter/third_party/accessibility/base/container_utils.h
Expand Down Expand Up @@ -6050,6 +6051,11 @@ ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_win.h + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.cc + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.h + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h + ../../../flutter/third_party/accessibility/LICENSE
ORIGIN: ../../../flutter/third_party/accessibility/base/win/scoped_safearray.h + ../../../flutter/third_party/accessibility/LICENSE
TYPE: LicenseType.bsd
FILE: ../../../flutter/third_party/accessibility/ax/ax_active_popup.cc
Expand All @@ -6069,6 +6075,11 @@ FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_wi
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_win.h
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.cc
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.h
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h
FILE: ../../../flutter/third_party/accessibility/base/win/scoped_safearray.h
----------------------------------------------------------------------------------------------------
Copyright 2019 The Chromium Authors. All rights reserved.
Expand Down
101 changes: 85 additions & 16 deletions shell/platform/common/accessibility_bridge.cc
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
#include <functional>
#include <utility>

#include "flutter/third_party/accessibility/ax/ax_tree_manager.h"
#include "flutter/third_party/accessibility/ax/ax_tree_manager_map.h"
#include "flutter/third_party/accessibility/ax/ax_tree_update.h"
#include "flutter/third_party/accessibility/base/logging.h"

Expand All @@ -19,14 +21,20 @@ constexpr int kHasScrollingAction =
FlutterSemanticsAction::kFlutterSemanticsActionScrollDown;

// AccessibilityBridge
AccessibilityBridge::AccessibilityBridge() {
event_generator_.SetTree(&tree_);
tree_.AddObserver(static_cast<ui::AXTreeObserver*>(this));
AccessibilityBridge::AccessibilityBridge()
: tree_(std::make_unique<ui::AXTree>()) {
event_generator_.SetTree(tree_.get());
tree_->AddObserver(static_cast<ui::AXTreeObserver*>(this));
ui::AXTreeData data = tree_->data();
data.tree_id = ui::AXTreeID::FromString("tree_token");
tree_->UpdateData(data);
ui::AXTreeManagerMap::GetInstance().AddTreeManager(tree_->GetAXTreeID(),
this);
}

AccessibilityBridge::~AccessibilityBridge() {
event_generator_.ReleaseTree();
tree_.RemoveObserver(static_cast<ui::AXTreeObserver*>(this));
tree_->RemoveObserver(static_cast<ui::AXTreeObserver*>(this));
}

void AccessibilityBridge::AddFlutterSemanticsNodeUpdate(
Expand All @@ -51,9 +59,9 @@ void AccessibilityBridge::CommitUpdates() {
std::optional<ui::AXTreeUpdate> remove_reparented =
CreateRemoveReparentedNodesUpdate();
if (remove_reparented.has_value()) {
tree_.Unserialize(remove_reparented.value());
tree_->Unserialize(remove_reparented.value());

std::string error = tree_.error();
std::string error = tree_->error();
if (!error.empty()) {
FML_LOG(ERROR) << "Failed to update ui::AXTree, error: " << error;
assert(false);
Expand All @@ -63,7 +71,7 @@ void AccessibilityBridge::CommitUpdates() {

// Second, apply the pending node updates. This also moves reparented nodes to
// their new parents if needed.
ui::AXTreeUpdate update{.tree_data = tree_.data()};
ui::AXTreeUpdate update{.tree_data = tree_->data()};

// Figure out update order, ui::AXTree only accepts update in tree order,
// where parent node must come before the child node in
Expand All @@ -88,11 +96,11 @@ void AccessibilityBridge::CommitUpdates() {
}
}

tree_.Unserialize(update);
tree_->Unserialize(update);
pending_semantics_node_updates_.clear();
pending_semantics_custom_action_updates_.clear();

std::string error = tree_.error();
std::string error = tree_->error();
if (!error.empty()) {
FML_LOG(ERROR) << "Failed to update ui::AXTree, error: " << error;
return;
Expand Down Expand Up @@ -122,7 +130,7 @@ AccessibilityBridge::GetFlutterPlatformNodeDelegateFromID(
}

const ui::AXTreeData& AccessibilityBridge::GetAXTreeData() const {
return tree_.data();
return tree_->data();
}

const std::vector<ui::AXEventGenerator::TargetedEvent>
Expand Down Expand Up @@ -201,7 +209,7 @@ AccessibilityBridge::CreateRemoveReparentedNodesUpdate() {
for (auto node_update : pending_semantics_node_updates_) {
for (int32_t child_id : node_update.second.children_in_traversal_order) {
// Skip nodes that don't exist or have a parent in the current tree.
ui::AXNode* child = tree_.GetFromId(child_id);
ui::AXNode* child = tree_->GetFromId(child_id);
if (!child) {
continue;
}
Expand All @@ -222,7 +230,7 @@ AccessibilityBridge::CreateRemoveReparentedNodesUpdate() {
// Create an update to remove the child from its previous parent.
int32_t parent_id = child->parent()->id();
if (updates.find(parent_id) == updates.end()) {
updates[parent_id] = tree_.GetFromId(parent_id)->data();
updates[parent_id] = tree_->GetFromId(parent_id)->data();
}

ui::AXNodeData* parent = &updates[parent_id];
Expand All @@ -239,7 +247,7 @@ AccessibilityBridge::CreateRemoveReparentedNodesUpdate() {
}

ui::AXTreeUpdate update{
.tree_data = tree_.data(),
.tree_data = tree_->data(),
.nodes = std::vector<ui::AXNodeData>(),
};

Expand Down Expand Up @@ -428,6 +436,11 @@ void AccessibilityBridge::SetBooleanAttributesFromFlutterUpdate(
ax::mojom::BoolAttribute::kEditableRoot,
flags & FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField &&
(flags & FlutterSemanticsFlag::kFlutterSemanticsFlagIsReadOnly) == 0);

// TODO(schectman): figure out when we actually want this attribute set or
// not.
node_data.AddBoolAttribute(ax::mojom::BoolAttribute::kIsLineBreakingObject,
true);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to double-check, should this be kept?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I probably will need to set up a new issue for this if there isn't one yet. I don't think we currently have a good way to keep track of this attribute that is used by the AX code, so this is a placeholder as I think commonly used elements usually behave as their own line wrt a11y.

}

void AccessibilityBridge::SetIntAttributesFromFlutterUpdate(
Expand Down Expand Up @@ -524,7 +537,8 @@ void AccessibilityBridge::SetTreeData(const SemanticsNode& node,
// 1. this text field has a valid selection
// 2. this text field doesn't have a valid selection but had selection stored
// in the tree.
if (flags & FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField) {
if (flags & FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField &&
flags & FlutterSemanticsFlag::kFlutterSemanticsFlagIsFocused) {
if (node.text_selection_base != -1) {
tree_update.tree_data.sel_anchor_object_id = node.id;
tree_update.tree_data.sel_anchor_offset = node.text_selection_base;
Expand Down Expand Up @@ -649,8 +663,63 @@ gfx::NativeViewAccessible AccessibilityBridge::GetNativeAccessibleFromId(
gfx::RectF AccessibilityBridge::RelativeToGlobalBounds(const ui::AXNode* node,
bool& offscreen,
bool clip_bounds) {
return tree_.RelativeToTreeBounds(node, gfx::RectF(), &offscreen,
clip_bounds);
return tree_->RelativeToTreeBounds(node, gfx::RectF(), &offscreen,
clip_bounds);
}

ui::AXNode* AccessibilityBridge::GetNodeFromTree(
ui::AXTreeID tree_id,
ui::AXNode::AXID node_id) const {
return GetNodeFromTree(node_id);
}

ui::AXNode* AccessibilityBridge::GetNodeFromTree(
ui::AXNode::AXID node_id) const {
return tree_->GetFromId(node_id);
}

ui::AXTreeID AccessibilityBridge::GetTreeID() const {
return tree_->GetAXTreeID();
}

ui::AXTreeID AccessibilityBridge::GetParentTreeID() const {
return ui::AXTreeIDUnknown();
}

ui::AXNode* AccessibilityBridge::GetRootAsAXNode() const {
return tree_->root();
}

ui::AXNode* AccessibilityBridge::GetParentNodeFromParentTreeAsAXNode() const {
return nullptr;
}

ui::AXTree* AccessibilityBridge::GetTree() const {
return tree_.get();
}

ui::AXPlatformNode* AccessibilityBridge::GetPlatformNodeFromTree(
const ui::AXNode::AXID node_id) const {
auto platform_delegate_weak = GetFlutterPlatformNodeDelegateFromID(node_id);
if (platform_delegate_weak.expired()) {
return nullptr;
}
auto platform_delegate = platform_delegate_weak.lock();
if (!platform_delegate) {
return nullptr;
}
return platform_delegate->GetPlatformNode();
}

ui::AXPlatformNode* AccessibilityBridge::GetPlatformNodeFromTree(
const ui::AXNode& node) const {
return GetPlatformNodeFromTree(node.id());
}

ui::AXPlatformNodeDelegate* AccessibilityBridge::RootDelegate() const {
return GetFlutterPlatformNodeDelegateFromID(GetRootAsAXNode()->id())
.lock()
.get();
}

} // namespace flutter
38 changes: 37 additions & 1 deletion shell/platform/common/accessibility_bridge.h
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@

#include "flutter/third_party/accessibility/ax/ax_event_generator.h"
#include "flutter/third_party/accessibility/ax/ax_tree.h"
#include "flutter/third_party/accessibility/ax/ax_tree_manager.h"
#include "flutter/third_party/accessibility/ax/ax_tree_observer.h"
#include "flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate.h"
#include "flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h"

#include "flutter_platform_node_delegate.h"

Expand All @@ -39,6 +41,7 @@ namespace flutter {
class AccessibilityBridge
: public std::enable_shared_from_this<AccessibilityBridge>,
public FlutterPlatformNodeDelegate::OwnerBridge,
public ui::AXPlatformTreeManager,
private ui::AXTreeObserver {
public:
//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -106,6 +109,39 @@ class AccessibilityBridge
const std::vector<ui::AXEventGenerator::TargetedEvent> GetPendingEvents()
const;

// |AXTreeManager|
ui::AXNode* GetNodeFromTree(const ui::AXTreeID tree_id,
const ui::AXNode::AXID node_id) const override;

// |AXTreeManager|
ui::AXNode* GetNodeFromTree(const ui::AXNode::AXID node_id) const override;

// |AXTreeManager|
ui::AXTreeID GetTreeID() const override;

// |AXTreeManager|
ui::AXTreeID GetParentTreeID() const override;

// |AXTreeManager|
ui::AXNode* GetRootAsAXNode() const override;

// |AXTreeManager|
ui::AXNode* GetParentNodeFromParentTreeAsAXNode() const override;

// |AXTreeManager|
ui::AXTree* GetTree() const override;

// |AXPlatformTreeManager|
ui::AXPlatformNode* GetPlatformNodeFromTree(
const ui::AXNode::AXID node_id) const override;

// |AXPlatformTreeManager|
ui::AXPlatformNode* GetPlatformNodeFromTree(
const ui::AXNode& node) const override;

// |AXPlatformTreeManager|
ui::AXPlatformNodeDelegate* RootDelegate() const override;

protected:
//---------------------------------------------------------------------------
/// @brief Handle accessibility events generated due to accessibility
Expand Down Expand Up @@ -176,7 +212,7 @@ class AccessibilityBridge
std::unordered_map<AccessibilityNodeId,
std::shared_ptr<FlutterPlatformNodeDelegate>>
id_wrapper_map_;
ui::AXTree tree_;
std::unique_ptr<ui::AXTree> tree_;
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed this to a pointer so we can return a pointer in the implementation for AX Tree Manager.

ui::AXEventGenerator event_generator_;
std::unordered_map<int32_t, SemanticsNode> pending_semantics_node_updates_;
std::unordered_map<int32_t, SemanticsCustomAction>
Expand Down
4 changes: 3 additions & 1 deletion shell/platform/common/accessibility_bridge_unittests.cc
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,9 @@ TEST(AccessibilityBridgeTest, canHandleSelectionChangeCorrectly) {
std::shared_ptr<TestAccessibilityBridge> bridge =
std::make_shared<TestAccessibilityBridge>();
FlutterSemanticsNode root = CreateSemanticsNode(0, "root");
root.flags = FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField;
root.flags = static_cast<FlutterSemanticsFlag>(
FlutterSemanticsFlag::kFlutterSemanticsFlagIsTextField |
FlutterSemanticsFlag::kFlutterSemanticsFlagIsFocused);
bridge->AddFlutterSemanticsNodeUpdate(&root);
bridge->CommitUpdates();

Expand Down
49 changes: 49 additions & 0 deletions shell/platform/common/flutter_platform_node_delegate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,10 @@

#include <utility>

#include "flutter/shell/platform/common/accessibility_bridge.h"
#include "flutter/third_party/accessibility/ax/ax_action_data.h"
#include "flutter/third_party/accessibility/ax/ax_tree_manager_map.h"
#include "flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h"
#include "flutter/third_party/accessibility/gfx/geometry/rect_conversions.h"

namespace flutter {
Expand Down Expand Up @@ -114,4 +117,50 @@ FlutterPlatformNodeDelegate::GetOwnerBridge() const {
return bridge_;
}

gfx::NativeViewAccessible
FlutterPlatformNodeDelegate::GetLowestPlatformAncestor() const {
auto bridge_ptr = bridge_.lock();
BASE_DCHECK(bridge_ptr);
auto lowest_platform_ancestor = ax_node_->GetLowestPlatformAncestor();
if (lowest_platform_ancestor) {
return bridge_ptr->GetNativeAccessibleFromId(
ax_node_->GetLowestPlatformAncestor()->id());
}
return nullptr;
}

ui::AXNodePosition::AXPositionInstance
FlutterPlatformNodeDelegate::CreateTextPositionAt(int offset) const {
return ui::AXNodePosition::CreatePosition(*ax_node_, offset);
}

ui::AXPlatformNode* FlutterPlatformNodeDelegate::GetPlatformNode() const {
return nullptr;
}

ui::AXPlatformNode* FlutterPlatformNodeDelegate::GetFromNodeID(
int32_t node_id) {
ui::AXTreeManager* tree_manager =
ui::AXTreeManagerMap::GetInstance().GetManager(
ax_node_->tree()->GetAXTreeID());
AccessibilityBridge* platform_manager =
static_cast<AccessibilityBridge*>(tree_manager);
return platform_manager->GetPlatformNodeFromTree(node_id);
}

ui::AXPlatformNode* FlutterPlatformNodeDelegate::GetFromTreeIDAndNodeID(
const ui::AXTreeID& tree_id,
int32_t node_id) {
ui::AXTreeManager* tree_manager =
ui::AXTreeManagerMap::GetInstance().GetManager(tree_id);
AccessibilityBridge* platform_manager =
static_cast<AccessibilityBridge*>(tree_manager);
return platform_manager->GetPlatformNodeFromTree(node_id);
}

const ui::AXTree::Selection FlutterPlatformNodeDelegate::GetUnignoredSelection()
const {
return ax_node_->GetUnignoredSelection();
}

} // namespace flutter
Loading