Skip to content

Commit d85adb6

Browse files
Implement ITextProvider and ITextRangeProvider for UIA (flutter#38538)
* Import files from Chromium as-is * Modify AX to compile * Disable new untitests for now * Formatting * License * License * License * License * Excl files * Update third_party/accessibility/ax/ax_node.h Co-authored-by: Loïc Sharma <[email protected]> * Update third_party/accessibility/ax/ax_node.h Co-authored-by: Loïc Sharma <[email protected]> * Typo * Update third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h Co-authored-by: Loïc Sharma <[email protected]> * Add clipping_behavior, tree_id * Formatting * Fix bad patch * TODOs Co-authored-by: Loïc Sharma <[email protected]>
1 parent a4ce69f commit d85adb6

21 files changed

+11203
-14
lines changed

ci/licenses_golden/excluded_files

+2
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,8 @@
358358
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_base_unittest.cc
359359
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_mac_unittest.h
360360
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_mac_unittest.mm
361+
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win_unittest.cc
362+
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc
361363
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_unittest.cc
362364
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_unittest.h
363365
../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_win_unittest.cc

ci/licenses_golden/licenses_flutter

+10
Original file line numberDiff line numberDiff line change
@@ -6072,6 +6072,11 @@ ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_
60726072
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_win.h + ../../../flutter/third_party/accessibility/LICENSE
60736073
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.cc + ../../../flutter/third_party/accessibility/LICENSE
60746074
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.h + ../../../flutter/third_party/accessibility/LICENSE
6075+
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc + ../../../flutter/third_party/accessibility/LICENSE
6076+
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h + ../../../flutter/third_party/accessibility/LICENSE
6077+
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc + ../../../flutter/third_party/accessibility/LICENSE
6078+
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h + ../../../flutter/third_party/accessibility/LICENSE
6079+
ORIGIN: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h + ../../../flutter/third_party/accessibility/LICENSE
60756080
ORIGIN: ../../../flutter/third_party/accessibility/base/win/scoped_safearray.h + ../../../flutter/third_party/accessibility/LICENSE
60766081
TYPE: LicenseType.bsd
60776082
FILE: ../../../flutter/third_party/accessibility/ax/ax_active_popup.cc
@@ -6091,6 +6096,11 @@ FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_wi
60916096
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_fragment_root_win.h
60926097
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.cc
60936098
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_delegate_utils_win.h
6099+
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.cc
6100+
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textprovider_win.h
6101+
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.cc
6102+
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_node_textrangeprovider_win.h
6103+
FILE: ../../../flutter/third_party/accessibility/ax/platform/ax_platform_tree_manager.h
60946104
FILE: ../../../flutter/third_party/accessibility/base/win/scoped_safearray.h
60956105
----------------------------------------------------------------------------------------------------
60966106
Copyright 2019 The Chromium Authors. All rights reserved.

third_party/accessibility/BUILD.gn

+2
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,8 @@ if (enable_unittests) {
9191
if (is_win) {
9292
sources += [
9393
"ax/platform/ax_fragment_root_win_unittest.cc",
94+
"ax/platform/ax_platform_node_textprovider_win_unittest.cc",
95+
"ax/platform/ax_platform_node_textrangeprovider_win_unittest.cc",
9496
"ax/platform/ax_platform_node_win_unittest.cc",
9597
"base/win/dispatch_stub.cc",
9698
"base/win/dispatch_stub.h",

third_party/accessibility/ax/BUILD.gn

+5
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ source_set("ax") {
1717
"platform/ax_platform_node_delegate.h",
1818
"platform/ax_platform_node_delegate_base.cc",
1919
"platform/ax_platform_node_delegate_base.h",
20+
"platform/ax_platform_tree_manager.h",
2021
"platform/ax_unique_id.cc",
2122
"platform/ax_unique_id.h",
2223
"platform/compute_attributes.cc",
@@ -92,6 +93,10 @@ source_set("ax") {
9293
"platform/ax_fragment_root_win.h",
9394
"platform/ax_platform_node_delegate_utils_win.cc",
9495
"platform/ax_platform_node_delegate_utils_win.h",
96+
"platform/ax_platform_node_textprovider_win.cc",
97+
"platform/ax_platform_node_textprovider_win.h",
98+
"platform/ax_platform_node_textrangeprovider_win.cc",
99+
"platform/ax_platform_node_textrangeprovider_win.h",
95100
"platform/ax_platform_node_win.cc",
96101
"platform/ax_platform_node_win.h",
97102
"platform/uia_registrar_win.cc",

third_party/accessibility/ax/ax_node.cc

+91
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
#include "ax_role_properties.h"
1212
#include "ax_table_info.h"
1313
#include "ax_tree.h"
14+
#include "ax_tree_manager.h"
15+
#include "ax_tree_manager_map.h"
1416
#include "base/color_utils.h"
1517
#include "base/string_utils.h"
1618

@@ -1197,6 +1199,30 @@ bool AXNode::IsEmbeddedGroup() const {
11971199
return ui::IsSetLike(parent()->data().role);
11981200
}
11991201

1202+
AXNode* AXNode::GetLowestPlatformAncestor() const {
1203+
AXNode* current_node = const_cast<AXNode*>(this);
1204+
AXNode* lowest_unignored_node = current_node;
1205+
for (; lowest_unignored_node && lowest_unignored_node->IsIgnored();
1206+
lowest_unignored_node = lowest_unignored_node->parent()) {
1207+
}
1208+
1209+
// `highest_leaf_node` could be nullptr.
1210+
AXNode* highest_leaf_node = lowest_unignored_node;
1211+
// For the purposes of this method, a leaf node does not include leaves in the
1212+
// internal accessibility tree, only in the platform exposed tree.
1213+
for (AXNode* ancestor_node = lowest_unignored_node; ancestor_node;
1214+
ancestor_node = ancestor_node->GetUnignoredParent()) {
1215+
if (ancestor_node->IsLeaf())
1216+
highest_leaf_node = ancestor_node;
1217+
}
1218+
if (highest_leaf_node)
1219+
return highest_leaf_node;
1220+
1221+
if (lowest_unignored_node)
1222+
return lowest_unignored_node;
1223+
return current_node;
1224+
}
1225+
12001226
AXNode* AXNode::GetTextFieldAncestor() const {
12011227
AXNode* parent = GetUnignoredParent();
12021228

@@ -1210,4 +1236,69 @@ AXNode* AXNode::GetTextFieldAncestor() const {
12101236
return nullptr;
12111237
}
12121238

1239+
bool AXNode::IsDescendantOfCrossingTreeBoundary(const AXNode* ancestor) const {
1240+
if (!ancestor)
1241+
return false;
1242+
if (this == ancestor)
1243+
return true;
1244+
if (const AXNode* parent = GetParentCrossingTreeBoundary())
1245+
return parent->IsDescendantOfCrossingTreeBoundary(ancestor);
1246+
return false;
1247+
}
1248+
1249+
AXNode* AXNode::GetParentCrossingTreeBoundary() const {
1250+
BASE_DCHECK(!tree_->GetTreeUpdateInProgressState());
1251+
if (parent_)
1252+
return parent_;
1253+
const AXTreeManager* manager =
1254+
AXTreeManagerMap::GetInstance().GetManager(tree_->GetAXTreeID());
1255+
if (manager)
1256+
return manager->GetParentNodeFromParentTreeAsAXNode();
1257+
return nullptr;
1258+
}
1259+
1260+
AXTree::Selection AXNode::GetUnignoredSelection() const {
1261+
BASE_DCHECK(tree())
1262+
<< "Cannot retrieve the current selection if the node is not "
1263+
"attached to an accessibility tree.\n"
1264+
<< *this;
1265+
AXTree::Selection selection = tree()->GetUnignoredSelection();
1266+
1267+
// "selection.anchor_offset" and "selection.focus_ofset" might need to be
1268+
// adjusted if the anchor or the focus nodes include ignored children.
1269+
//
1270+
// TODO(nektar): Move this logic into its own "AXSelection" class and cache
1271+
// the result for faster reuse.
1272+
const AXNode* anchor = tree()->GetFromId(selection.anchor_object_id);
1273+
if (anchor && !anchor->IsLeaf()) {
1274+
BASE_DCHECK(selection.anchor_offset >= 0);
1275+
if (static_cast<size_t>(selection.anchor_offset) <
1276+
anchor->children().size()) {
1277+
const AXNode* anchor_child = anchor->children()[selection.anchor_offset];
1278+
BASE_DCHECK(anchor_child);
1279+
selection.anchor_offset =
1280+
static_cast<int>(anchor_child->GetUnignoredIndexInParent());
1281+
} else {
1282+
selection.anchor_offset =
1283+
static_cast<int>(anchor->GetUnignoredChildCount());
1284+
}
1285+
}
1286+
1287+
const AXNode* focus = tree()->GetFromId(selection.focus_object_id);
1288+
if (focus && !focus->IsLeaf()) {
1289+
BASE_DCHECK(selection.focus_offset >= 0);
1290+
if (static_cast<size_t>(selection.focus_offset) <
1291+
focus->children().size()) {
1292+
const AXNode* focus_child = focus->children()[selection.focus_offset];
1293+
BASE_DCHECK(focus_child);
1294+
selection.focus_offset =
1295+
static_cast<int>(focus_child->GetUnignoredIndexInParent());
1296+
} else {
1297+
selection.focus_offset =
1298+
static_cast<int>(focus->GetUnignoredChildCount());
1299+
}
1300+
}
1301+
return selection;
1302+
}
1303+
12131304
} // namespace ui

third_party/accessibility/ax/ax_node.h

+13
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,10 @@ class AX_EXPORT AXNode final {
120120
size_t GetUnignoredChildCount() const;
121121
AXNode* GetUnignoredChildAtIndex(size_t index) const;
122122
AXNode* GetUnignoredParent() const;
123+
// Gets the unignored selection from the accessibility tree, meaning the
124+
// selection whose endpoints are on unignored nodes. (An "ignored" node is a
125+
// node that is not exposed to platform APIs: See `IsIgnored`.)
126+
OwnerTree::Selection GetUnignoredSelection() const;
123127
size_t GetUnignoredIndexInParent() const;
124128
size_t GetIndexInParent() const;
125129
AXNode* GetFirstUnignoredChild() const;
@@ -191,6 +195,9 @@ class AX_EXPORT AXNode final {
191195
// Return true if this object is equal to or a descendant of |ancestor|.
192196
bool IsDescendantOf(const AXNode* ancestor) const;
193197

198+
bool IsDescendantOfCrossingTreeBoundary(const AXNode* ancestor) const;
199+
AXNode* GetParentCrossingTreeBoundary() const;
200+
194201
// Gets the text offsets where new lines start either from the node's data or
195202
// by computing them and caching the result.
196203
std::vector<int> GetOrComputeLineStartOffsets();
@@ -436,6 +443,12 @@ class AX_EXPORT AXNode final {
436443
// Finds and returns a pointer to ordered set containing node.
437444
AXNode* GetOrderedSet() const;
438445

446+
// If this node is exposed to the platform's accessibility layer, returns this
447+
// node. Otherwise, returns the lowest ancestor that is exposed to the
448+
// platform. (See `IsLeaf` and `IsIgnored` for information on what is
449+
// exposed to platform APIs.)
450+
AXNode* GetLowestPlatformAncestor() const;
451+
439452
private:
440453
// Computes the text offset where each line starts by traversing all child
441454
// leaf nodes.

third_party/accessibility/ax/ax_node_position.cc

+18-5
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,16 @@ AXEmbeddedObjectBehavior g_ax_embedded_object_behavior =
2020
AXEmbeddedObjectBehavior::kSuppressCharacter;
2121
#endif // defined(OS_WIN)
2222

23+
ScopedAXEmbeddedObjectBehaviorSetter::ScopedAXEmbeddedObjectBehaviorSetter(
24+
AXEmbeddedObjectBehavior behavior) {
25+
prev_behavior_ = g_ax_embedded_object_behavior;
26+
g_ax_embedded_object_behavior = behavior;
27+
}
28+
29+
ScopedAXEmbeddedObjectBehaviorSetter::~ScopedAXEmbeddedObjectBehaviorSetter() {
30+
g_ax_embedded_object_behavior = prev_behavior_;
31+
}
32+
2333
// static
2434
AXNodePosition::AXPositionInstance AXNodePosition::CreatePosition(
2535
const AXNode& node,
@@ -205,8 +215,9 @@ std::u16string AXNodePosition::GetText() const {
205215
ax::mojom::StringAttribute::kName);
206216
}
207217

208-
for (int i = 0; i < AnchorChildCount(); ++i)
218+
for (int i = 0; i < AnchorChildCount(); ++i) {
209219
text += CreateChildPositionAt(i)->GetText();
220+
}
210221

211222
return text;
212223
}
@@ -263,8 +274,9 @@ int AXNodePosition::MaxTextOffset() const {
263274
}
264275

265276
int text_length = 0;
266-
for (int i = 0; i < AnchorChildCount(); ++i)
277+
for (int i = 0; i < AnchorChildCount(); ++i) {
267278
text_length += CreateChildPositionAt(i)->MaxTextOffset();
279+
}
268280

269281
return text_length;
270282
}
@@ -286,9 +298,10 @@ bool AXNodePosition::IsInLineBreakingObject() const {
286298
if (IsNullPosition())
287299
return false;
288300
BASE_DCHECK(GetAnchor());
289-
return GetAnchor()->data().GetBoolAttribute(
290-
ax::mojom::BoolAttribute::kIsLineBreakingObject) &&
291-
!GetAnchor()->IsInListMarker();
301+
return (GetAnchor()->data().GetBoolAttribute(
302+
ax::mojom::BoolAttribute::kIsLineBreakingObject) &&
303+
!GetAnchor()->IsInListMarker()) ||
304+
GetAnchor()->data().role == ax::mojom::Role::kLineBreak;
292305
}
293306

294307
ax::mojom::Role AXNodePosition::GetAnchorRole() const {

third_party/accessibility/ax/ax_position.h

+31-7
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,16 @@ enum class AXEmbeddedObjectBehavior {
105105
// overridden for testing.
106106
AX_EXPORT extern AXEmbeddedObjectBehavior g_ax_embedded_object_behavior;
107107

108+
class AX_EXPORT ScopedAXEmbeddedObjectBehaviorSetter {
109+
public:
110+
explicit ScopedAXEmbeddedObjectBehaviorSetter(
111+
AXEmbeddedObjectBehavior behavior);
112+
~ScopedAXEmbeddedObjectBehaviorSetter();
113+
114+
private:
115+
AXEmbeddedObjectBehavior prev_behavior_;
116+
};
117+
108118
// Forward declarations.
109119
template <class AXPositionType, class AXNodeType>
110120
class AXPosition;
@@ -324,8 +334,9 @@ class AXPosition {
324334
BASE_DCHECK(GetAnchor());
325335
// If this position is anchored to an ignored node, then consider this
326336
// position to be ignored.
327-
if (GetAnchor()->IsIgnored())
337+
if (GetAnchor()->IsIgnored()) {
328338
return true;
339+
}
329340

330341
switch (kind_) {
331342
case AXPositionKind::NULL_POSITION:
@@ -372,8 +383,9 @@ class AXPosition {
372383
// If the corresponding leaf position is ignored, the current text
373384
// offset will point to ignored text. Therefore, consider this position
374385
// to be ignored.
375-
if (!IsLeaf())
386+
if (!IsLeaf()) {
376387
return AsLeafTreePosition()->IsIgnored();
388+
}
377389
return false;
378390
}
379391
}
@@ -417,8 +429,9 @@ class AXPosition {
417429
(child_index_ >= 0 && child_index_ <= AnchorChildCount())) &&
418430
!IsInDescendantOfEmptyObject();
419431
case AXPositionKind::TEXT_POSITION:
420-
if (!GetAnchor() || IsInDescendantOfEmptyObject())
432+
if (!GetAnchor() || IsInDescendantOfEmptyObject()) {
421433
return false;
434+
}
422435

423436
// For performance reasons we skip any validation of the text offset
424437
// that involves retrieving the anchor's text, if the offset is set to
@@ -1029,8 +1042,9 @@ class AXPosition {
10291042
const AXNodeType* ancestor_anchor,
10301043
ax::mojom::MoveDirection move_direction =
10311044
ax::mojom::MoveDirection::kForward) const {
1032-
if (!ancestor_anchor)
1045+
if (!ancestor_anchor) {
10331046
return CreateNullPosition();
1047+
}
10341048

10351049
AXPositionInstance ancestor_position = Clone();
10361050
while (!ancestor_position->IsNullPosition() &&
@@ -1285,8 +1299,9 @@ class AXPosition {
12851299
}
12861300

12871301
AXPositionInstance AsLeafTextPosition() const {
1288-
if (IsNullPosition() || IsLeaf())
1302+
if (IsNullPosition() || IsLeaf()) {
12891303
return AsTextPosition();
1304+
}
12901305

12911306
// Adjust the text offset.
12921307
// No need to check for "before text" positions here because they are only
@@ -1316,7 +1331,7 @@ class AXPosition {
13161331
child_position->affinity_ = ax::mojom::TextAffinity::kUpstream;
13171332
break;
13181333
}
1319-
child_position = text_position->CreateChildPositionAt(i);
1334+
child_position = std::move(text_position->CreateChildPositionAt(i));
13201335
adjusted_offset -= max_text_offset_in_parent;
13211336
}
13221337

@@ -1902,7 +1917,7 @@ class AXPosition {
19021917
// the same as the one that would have been computed if the original
19031918
// position were at the start of the inline text box for "Line two".
19041919
const int max_text_offset = MaxTextOffset();
1905-
const int max_text_offset_in_parent =
1920+
int max_text_offset_in_parent =
19061921
IsEmbeddedObjectInParent() ? 1 : max_text_offset;
19071922
int parent_offset = AnchorTextOffsetInParent();
19081923
ax::mojom::TextAffinity parent_affinity = affinity_;
@@ -1935,6 +1950,14 @@ class AXPosition {
19351950
parent_affinity = ax::mojom::TextAffinity::kDownstream;
19361951
}
19371952

1953+
// This dummy position serves to retrieve the max text offset of the
1954+
// anchor-node in which we want to create the parent position.
1955+
AXPositionInstance dummy_position =
1956+
CreateTextPosition(tree_id, parent_id, 0, parent_affinity);
1957+
max_text_offset_in_parent = dummy_position->MaxTextOffset();
1958+
if (parent_offset > max_text_offset_in_parent) {
1959+
parent_offset = max_text_offset_in_parent;
1960+
}
19381961
AXPositionInstance parent_position = CreateTextPosition(
19391962
tree_id, parent_id, parent_offset, parent_affinity);
19401963

@@ -2061,6 +2084,7 @@ class AXPosition {
20612084
BASE_DCHECK(text_position->text_offset_ >= 0);
20622085
return text_position;
20632086
}
2087+
20642088
text_position = text_position->CreateNextLeafTextPosition();
20652089
while (!text_position->IsNullPosition() &&
20662090
(text_position->IsIgnored() || !text_position->MaxTextOffset())) {

third_party/accessibility/ax/ax_range.h

+4-1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <utility>
1212
#include <vector>
1313

14+
#include "ax_clipping_behavior.h"
1415
#include "ax_enums.h"
1516
#include "ax_offscreen_result.h"
1617
#include "ax_role_properties.h"
@@ -35,6 +36,7 @@ class AXRangeRectDelegate {
3536
AXNode::AXID node_id,
3637
int start_offset,
3738
int end_offset,
39+
ui::AXClippingBehavior clipping_behavior,
3840
AXOffscreenResult* offscreen_result) = 0;
3941
virtual gfx::Rect GetBoundsRect(AXTreeID tree_id,
4042
AXNode::AXID node_id,
@@ -392,7 +394,8 @@ class AXRange {
392394
current_line_start->tree_id(),
393395
current_line_start->anchor_id(),
394396
current_line_start->text_offset(),
395-
current_line_end->text_offset(), &offscreen_result)
397+
current_line_end->text_offset(),
398+
ui::AXClippingBehavior::kUnclipped, &offscreen_result)
396399
: delegate->GetBoundsRect(current_line_start->tree_id(),
397400
current_line_start->anchor_id(),
398401
&offscreen_result);

0 commit comments

Comments
 (0)