Skip to content

Commit

Permalink
ax_ui: move selection based AXTree methods under AXSelection
Browse files Browse the repository at this point in the history
Extract selection logic out of AXTree and move it under AXSelection
umbrella. Make AXSelection keeping a AXTreeID instance
as it allows to retrieve AXNode instance by node id. It is needed to
compute unignored selection, as well it will be helpful to enclose
anchor/focus AXNode object getting into AXSelection.

Later, we may switch to an architecture of a single AXSelection instance
per an AXTree object, which will allow to implement lazily computed and
live unignored selection.

Change-Id: Ib3446b12e7ebb40c70520499ab3a59e200f26d7d
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/3863567
Commit-Queue: Alexander Surkkov <asurkov@igalia.com>
Reviewed-by: Nektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/main@{#1054178}
  • Loading branch information
asurkov authored and Chromium LUCI CQ committed Oct 3, 2022
1 parent 1b6486c commit e0ea0ef
Show file tree
Hide file tree
Showing 7 changed files with 152 additions and 113 deletions.
1 change: 1 addition & 0 deletions ui/accessibility/BUILD.gn
Expand Up @@ -147,6 +147,7 @@ component("accessibility") {
"ax_offscreen_result.h",
"ax_position.h",
"ax_range.h",
"ax_selection.cc",
"ax_selection.h",
"ax_serializable_tree.cc",
"ax_serializable_tree.h",
Expand Down
125 changes: 125 additions & 0 deletions ui/accessibility/ax_selection.cc
@@ -0,0 +1,125 @@
// Copyright 2022 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/ax_selection.h"

#include "ui/accessibility/ax_node_position.h"

namespace ui {

namespace {

// Helper for GetUnignoredSelection. Creates a position using |node_id|,
// |offset| and |affinity|, and if it's ignored, updates these arguments so
// that they represent a non-null non-ignored position, according to
// |adjustment_behavior|. Returns true on success, false on failure. Note that
// if the position is initially null, it's not ignored and it's a success.
bool ComputeUnignoredSelectionEndpoint(
const AXTree* tree,
AXPositionAdjustmentBehavior adjustment_behavior,
AXNodeID& node_id,
int32_t& offset,
ax::mojom::TextAffinity& affinity) {
AXNode* node = tree ? tree->GetFromId(node_id) : nullptr;
if (!node) {
node_id = kInvalidAXNodeID;
offset = AXNodePosition::INVALID_OFFSET;
affinity = ax::mojom::TextAffinity::kDownstream;
return false;
}

AXNodePosition::AXPositionInstance position =
AXNodePosition::CreatePosition(*node, offset, affinity);

// Null positions are never ignored, but must be considered successful, or
// these Android tests would fail:
// org.chromium.content.browser.accessibility.AssistViewStructureTest#*
// The reason is that |position| becomes null because no AXTreeManager is
// registered for that |tree|'s AXTreeID.
// TODO(accessibility): investigate and fix this if needed.
if (!position->IsIgnored())
return true; // We assume that unignored positions are already valid.

position =
position->AsValidPosition()->AsUnignoredPosition(adjustment_behavior);

// Moving to an unignored position might have placed the position on a leaf
// node. Any selection endpoint that is inside a leaf node is expressed as a
// text position in AXTreeData. (Note that in this context "leaf node" means
// a node with no children or with only ignored children. This does not
// refer to a platform leaf.)
if (position->IsLeafTreePosition())
position = position->AsTextPosition();

// We do not expect the selection to have an endpoint on an inline text
// box as this will create issues with parts of the code that don't use
// inline text boxes.
if (position->IsTextPosition() &&
position->GetRole() == ax::mojom::Role::kInlineTextBox) {
position = position->CreateParentPosition();
}

switch (position->kind()) {
case AXPositionKind::NULL_POSITION:
node_id = kInvalidAXNodeID;
offset = AXNodePosition::INVALID_OFFSET;
affinity = ax::mojom::TextAffinity::kDownstream;
return false;
case AXPositionKind::TREE_POSITION:
node_id = position->anchor_id();
offset = position->child_index();
affinity = ax::mojom::TextAffinity::kDownstream;
return true;
case AXPositionKind::TEXT_POSITION:
node_id = position->anchor_id();
offset = position->text_offset();
affinity = position->affinity();
return true;
}
}

} // namespace

AXSelection::AXSelection() = default;
AXSelection::AXSelection(const AXSelection&) = default;

AXSelection::~AXSelection() = default;

AXSelection::AXSelection(const AXTree& tree)
: is_backward(tree.data().sel_is_backward),
anchor_object_id(tree.data().sel_anchor_object_id),
anchor_offset(tree.data().sel_anchor_offset),
anchor_affinity(tree.data().sel_anchor_affinity),
focus_object_id(tree.data().sel_focus_object_id),
focus_offset(tree.data().sel_focus_offset),
focus_affinity(tree.data().sel_focus_affinity),
tree_id_(tree.GetAXTreeID()) {}

AXSelection& AXSelection::ToUnignoredSelection() {
const AXTreeManager* manager = AXTreeManager::FromID(tree_id_);
DCHECK(manager);

// If one of the selection endpoints is invalid, then the other endpoint
// should also be unset.
if (!ComputeUnignoredSelectionEndpoint(
manager->ax_tree(),
is_backward ? AXPositionAdjustmentBehavior::kMoveForward
: AXPositionAdjustmentBehavior::kMoveBackward,
anchor_object_id, anchor_offset, anchor_affinity)) {
focus_object_id = kInvalidAXNodeID;
focus_offset = AXNodePosition::INVALID_OFFSET;
focus_affinity = ax::mojom::TextAffinity::kDownstream;
} else if (!ComputeUnignoredSelectionEndpoint(
manager->ax_tree(),
is_backward ? AXPositionAdjustmentBehavior::kMoveBackward
: AXPositionAdjustmentBehavior::kMoveForward,
focus_object_id, focus_offset, focus_affinity)) {
anchor_object_id = kInvalidAXNodeID;
anchor_offset = AXNodePosition::INVALID_OFFSET;
anchor_affinity = ax::mojom::TextAffinity::kDownstream;
}
return *this;
}

} // namespace ui
15 changes: 15 additions & 0 deletions ui/accessibility/ax_selection.h
Expand Up @@ -5,15 +5,25 @@
#ifndef UI_ACCESSIBILITY_AX_SELECTION_H_
#define UI_ACCESSIBILITY_AX_SELECTION_H_

// #include "ui/accessibility/ax_enums.mojom-forward.h"
#include "ui/accessibility/ax_export.h"
#include "ui/accessibility/ax_node_id_forward.h"
#include "ui/accessibility/ax_tree_id.h"

namespace ui {

class AXTree;

// A data structure that can store either the selected range of nodes in the
// accessibility tree, or the location of the caret in the case of a
// "collapsed" selection.
class AX_EXPORT AXSelection final {
public:
AXSelection();
explicit AXSelection(const AXTree&);
AXSelection(const AXSelection&);
~AXSelection();

// Returns true if this instance represents the position of the caret.
constexpr bool IsCollapsed() const {
return focus_object_id != kInvalidAXNodeID &&
Expand All @@ -27,6 +37,11 @@ class AX_EXPORT AXSelection final {
AXNodeID focus_object_id = kInvalidAXNodeID;
int focus_offset = -1;
ax::mojom::TextAffinity focus_affinity;

AXSelection& ToUnignoredSelection();

private:
AXTreeID tree_id_;
};

} // namespace ui
Expand Down
113 changes: 5 additions & 108 deletions ui/accessibility/ax_tree.cc
Expand Up @@ -2631,118 +2631,15 @@ absl::optional<int> AXTree::GetSetSize(const AXNode& node) {
return set_size;
}

namespace {

// Helper for GetUnignoredSelection. Creates a position using |node_id|,
// |offset| and |affinity|, and if it's ignored, updates these arguments so
// that they represent a non-null non-ignored position, according to
// |adjustment_behavior|. Returns true on success, false on failure. Note that
// if the position is initially null, it's not ignored and it's a success.
bool ComputeUnignoredSelectionEndpoint(
const AXTree& tree,
AXPositionAdjustmentBehavior adjustment_behavior,
AXNodeID& node_id,
int32_t& offset,
ax::mojom::TextAffinity& affinity) {
AXNode* node = nullptr;
if (node_id != kInvalidAXNodeID)
node = tree.GetFromId(node_id);
if (!node) {
node_id = kInvalidAXNodeID;
offset = AXNodePosition::INVALID_OFFSET;
affinity = ax::mojom::TextAffinity::kDownstream;
return false;
}

AXNodePosition::AXPositionInstance position =
AXNodePosition::CreatePosition(*node, offset, affinity);

// Null positions are never ignored, but must be considered successful, or
// these Android tests would fail:
// org.chromium.content.browser.accessibility.AssistViewStructureTest#*
// The reason is that |position| becomes null because no AXTreeManager is
// registered for that |tree|'s AXTreeID.
// TODO(accessibility): investigate and fix this if needed.
if (!position->IsIgnored())
return true; // We assume that unignored positions are already valid.

position =
position->AsValidPosition()->AsUnignoredPosition(adjustment_behavior);

// Moving to an unignored position might have placed the position on a leaf
// node. Any selection endpoint that is inside a leaf node is expressed as a
// text position in AXTreeData. (Note that in this context "leaf node" means
// a node with no children or with only ignored children. This does not
// refer to a platform leaf.)
if (position->IsLeafTreePosition())
position = position->AsTextPosition();

// We do not expect the selection to have an endpoint on an inline text
// box as this will create issues with parts of the code that don't use
// inline text boxes.
if (position->IsTextPosition() &&
position->GetRole() == ax::mojom::Role::kInlineTextBox) {
position = position->CreateParentPosition();
}

switch (position->kind()) {
case AXPositionKind::NULL_POSITION:
node_id = kInvalidAXNodeID;
offset = -1;
affinity = ax::mojom::TextAffinity::kDownstream;
return false;
case AXPositionKind::TREE_POSITION:
node_id = position->anchor_id();
offset = position->child_index();
affinity = ax::mojom::TextAffinity::kDownstream;
return true;
case AXPositionKind::TEXT_POSITION:
node_id = position->anchor_id();
offset = position->text_offset();
affinity = position->affinity();
return true;
}
}

} // namespace

AXSelection AXTree::GetSelection() const {
return {data().sel_is_backward, data().sel_anchor_object_id,
data().sel_anchor_offset, data().sel_anchor_affinity,
data().sel_focus_object_id, data().sel_focus_offset,
data().sel_focus_affinity};
// TODO(accessibility): do not create a selection object every time it's
// requested. Either switch AXSelection to getters that computes selection
// data upon request or provide an invalidation mechanism.
return AXSelection(*this);
}

AXSelection AXTree::GetUnignoredSelection() const {
AXSelection unignored_selection = GetSelection();

// If one of the selection endpoints is invalid, then the other endpoint
// should also be unset.
if (!ComputeUnignoredSelectionEndpoint(
*this,
unignored_selection.is_backward
? AXPositionAdjustmentBehavior::kMoveForward
: AXPositionAdjustmentBehavior::kMoveBackward,
unignored_selection.anchor_object_id,
unignored_selection.anchor_offset,
unignored_selection.anchor_affinity)) {
unignored_selection.focus_object_id = kInvalidAXNodeID;
unignored_selection.focus_offset = -1;
unignored_selection.focus_affinity = ax::mojom::TextAffinity::kDownstream;
} else if (!ComputeUnignoredSelectionEndpoint(
*this,
unignored_selection.is_backward
? AXPositionAdjustmentBehavior::kMoveBackward
: AXPositionAdjustmentBehavior::kMoveForward,
unignored_selection.focus_object_id,
unignored_selection.focus_offset,
unignored_selection.focus_affinity)) {
unignored_selection.anchor_object_id = kInvalidAXNodeID;
unignored_selection.anchor_offset = -1;
unignored_selection.anchor_affinity = ax::mojom::TextAffinity::kDownstream;
}

return unignored_selection;
return GetSelection().ToUnignoredSelection();
}

bool AXTree::GetTreeUpdateInProgressState() const {
Expand Down
Expand Up @@ -268,7 +268,7 @@ std::u16string AXPlatformNodeDelegateBase::GetValueForControl() const {

const AXSelection AXPlatformNodeDelegateBase::GetUnignoredSelection() const {
NOTIMPLEMENTED();
return AXSelection{false, -1, -1, ax::mojom::TextAffinity::kDownstream};
return AXSelection();
}

AXNodePosition::AXPositionInstance AXPlatformNodeDelegateBase::CreatePositionAt(
Expand Down
7 changes: 4 additions & 3 deletions ui/accessibility/test_ax_tree_manager.cc
Expand Up @@ -60,7 +60,10 @@ void TestAXTreeManager::SetTree(std::unique_ptr<AXTree> tree) {
GetMap().AddTreeManager(GetTreeID(), this);
}

AXTree* TestAXTreeManager::Init(const AXTreeUpdate& tree_update) {
AXTree* TestAXTreeManager::Init(AXTreeUpdate tree_update) {
tree_update.has_tree_data = true;
if (tree_update.tree_data.tree_id == AXTreeIDUnknown())
tree_update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
SetTree(std::make_unique<AXTree>(tree_update));
return ax_tree_.get();
}
Expand All @@ -84,8 +87,6 @@ AXTree* TestAXTreeManager::Init(
const ui::AXNodeData& node12 /* = AXNodeData() */) {
AXTreeUpdate update;
update.root_id = node1.id;
update.has_tree_data = true;
update.tree_data.tree_id = AXTreeID::CreateNewAXTreeID();
update.tree_data.title = "Dialog title";
update.nodes.push_back(node1);
if (node2.id != kInvalidAXNodeID)
Expand Down
2 changes: 1 addition & 1 deletion ui/accessibility/test_ax_tree_manager.h
Expand Up @@ -46,7 +46,7 @@ class TestAXTreeManager : public AXTreeManager {
void SetTree(std::unique_ptr<AXTree> tree);

// Creates and set the tree by a given AXTreeUpdate instance.
AXTree* Init(const AXTreeUpdate& tree_update);
AXTree* Init(AXTreeUpdate tree_update);

// Set the tree by a given TestAXTreeUpdateNode instance.
AXTree* Init(const TestAXTreeUpdateNode& tree_update_root);
Expand Down

0 comments on commit e0ea0ef

Please sign in to comment.