Commit 19d3bcf5 authored by Nektarios Paisios's avatar Nektarios Paisios Committed by Chromium LUCI CQ

Adds AXNode::IsEmptyLeaf and improves AXNode::IsIgnored to exclude ignored nodes with children

This is required before merging BrowserAccessibilityPosition and AXNodePosition.

In UI Automation, we expose an "embedded object replacement character" \xFFFC in the text representation
of all empty controls, so that they will be treated by AXPosition as word, character and line boundaries.

This patch moves some of the logic for determining which node is an empty
control to AXNode in the form of the AXNode::IsEmptyLeaf method.
Note that the definition of a control is quite loose
and it encompasses all leaf nodes that expose no inner text or hypertext.
This is in order to cover all possible future additions to our rendering engine
and in order to extend this behavior to possibly other elements which meet the same criteria.
This behavior is not new, but it has been moved from AXPosition::IsEmptyLeaf.

This patch also fixes AXNode::IsLeaf to properly mark ignored nodes that have children as not leaves.
A leaf node should be, either a node with no children (including ignored and unignored),
or any unignored node whose entire subtree is not exposed to platform APIs.
Ignored nodes that are already in an ignored subtree, should not be marked as leaves too, otherwise AXPosition::AsLeafTextPosition()
might stop unexpectedly while traversing the AX tree to find a leaf node:
++kTextField "Some Text" (leaf node)
++++kGenericContainer ignored (should not be marked as a leaf node)
++++++kStaticText "Some text"
++++++++kInlineTextBox "Some text" (leaf node)

For optimal performance AXNode::GetInnerTextLength() is introduced and
is used by AXNode::IsEmptyLeaf(). Concadenating strings is much slower than adding up their lengths.

Finally, AXNode::GetHypertext() is fixed so that descendants of leaf nodes have the correct
hypertext per the existing IA2 and ATK behavior.

R=dmazzoni@chromium.org, aleventhal@chromium.org

AX-Relnotes: n/a.
Bug: 1049261
Change-Id: I4ea5b578dded79920f07bf6a66df208dd4a88fae
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2637515Reviewed-by: default avatarKurt Catti-Schmidt <kschmi@microsoft.com>
Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Commit-Queue: Nektarios Paisios <nektar@chromium.org>
Cr-Commit-Position: refs/heads/master@{#845782}
parent 5649cc37
...@@ -23,7 +23,10 @@ ...@@ -23,7 +23,10 @@
namespace ui { namespace ui {
// Definition of static class members.
constexpr AXNode::AXID AXNode::kInvalidAXID; constexpr AXNode::AXID AXNode::kInvalidAXID;
constexpr base::char16 AXNode::kEmbeddedCharacter[];
constexpr int AXNode::kEmbeddedCharacterLength;
AXNode::AXNode(AXNode::OwnerTree* tree, AXNode::AXNode(AXNode::OwnerTree* tree,
AXNode* parent, AXNode* parent,
...@@ -492,25 +495,45 @@ void AXNode::ClearLanguageInfo() { ...@@ -492,25 +495,45 @@ void AXNode::ClearLanguageInfo() {
language_info_.reset(); language_info_.reset();
} }
std::string AXNode::GetHypertext() const { base::string16 AXNode::GetHypertext() const {
DCHECK(!tree_->GetTreeUpdateInProgressState()); DCHECK(!tree_->GetTreeUpdateInProgressState());
if (IsIgnoredForTextNavigation()) if (IsIgnoredForTextNavigation())
return std::string(); return base::string16();
if (IsLeaf()) // Hypertext is not exposed for descendants of leaf nodes. For such nodes,
return GetInnerText(); // their inner text is equivalent to their hypertext. Otherwise, we would
// never be able to compute equivalent ancestor positions in text fields given
// an AXPosition on an inline text box descendant, because there is often an
// ignored generic container between the text descendants and the text field
// node.
//
// For example, look at the following accessibility tree and the text
// positions indicated using "<>" symbols in the inner text of every node, and
// then imagine what would happen if the generic container was represented by
// an "embedded object replacement character" in the text of its text field
// parent.
// ++kTextField "Hell<o>" IsLeaf=true
// ++++kGenericContainer "Hell<o>" ignored IsChildOfLeaf=true
// ++++++kStaticText "Hell<o>" IsChildOfLeaf=true
// ++++++++kInlineTextBox "Hell<o>" IsChildOfLeaf=true
if (IsLeaf() || IsChildOfLeaf())
return base::UTF8ToUTF16(GetInnerText());
// Construct the hypertext for this node, which contains the concatenation of // Construct the hypertext for this node, which contains the concatenation of
// the inner text of this node's textual children, and an embedded object // the inner text of this node's textual children, and an "object replacement
// character for all the other children. // character" for all the other children.
const std::string embedded_character_str("\xEF\xBF\xBC"); //
std::string hypertext; // Note that the word "hypertext" comes from the IAccessible2 Standard and has
// nothing to do with HTML.
const base::string16 embedded_character_str(kEmbeddedCharacter);
DCHECK_EQ(int{embedded_character_str.length()}, kEmbeddedCharacterLength);
base::string16 hypertext;
for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) { for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it) {
// Similar to Firefox, we don't expose text nodes in IAccessible2 and ATK // Similar to Firefox, we don't expose text nodes in IAccessible2 and ATK
// hypertext with the embedded object character. We copy all of their text // hypertext with the embedded object character. We copy all of their text
// instead. // instead.
if (it->IsText()) { if (it->IsText()) {
hypertext += it->GetInnerText(); hypertext += base::UTF8ToUTF16(it->GetInnerText());
} else { } else {
hypertext += embedded_character_str; hypertext += embedded_character_str;
} }
...@@ -584,6 +607,27 @@ std::string AXNode::GetInnerText() const { ...@@ -584,6 +607,27 @@ std::string AXNode::GetInnerText() const {
return inner_text; return inner_text;
} }
int AXNode::GetInnerTextLength() const {
// This is an optimized version of `AXNode::GetInnerText()`.length(). Instead
// of concatenating the strings in GetInnerText() to then get their length, we
// sum the lengths of the individual strings. This is faster than
// concatenating the strings first and then taking their length, especially
// when the process is recursive.
const bool is_plain_text_field_with_descendants =
(data().IsTextField() && GetUnignoredChildCount());
// Plain text fields are always leaves so we need to exclude them when
// computing the length of their inner text if that text should be derived
// from their descendant nodes.
if (IsLeaf() && !is_plain_text_field_with_descendants)
return int{GetInnerText().length()};
int inner_text_length = 0;
for (auto it = UnignoredChildrenBegin(); it != UnignoredChildrenEnd(); ++it)
inner_text_length += it->GetInnerTextLength();
return inner_text_length;
}
std::string AXNode::GetLanguage() const { std::string AXNode::GetLanguage() const {
DCHECK(!tree_->GetTreeUpdateInProgressState()); DCHECK(!tree_->GetTreeUpdateInProgressState());
// Walk up tree considering both detected and author declared languages. // Walk up tree considering both detected and author declared languages.
...@@ -1230,9 +1274,29 @@ bool AXNode::IsChildOfLeaf() const { ...@@ -1230,9 +1274,29 @@ bool AXNode::IsChildOfLeaf() const {
return false; return false;
} }
bool AXNode::IsEmptyLeaf() const {
if (!IsLeaf())
return false;
if (GetUnignoredChildCount())
return !GetInnerTextLength();
// Text exposed by ignored leaf (text) nodes is not exposed to the platforms'
// accessibility layer, hence such leaf nodes are in effect empty.
return IsIgnored() || !GetInnerTextLength();
}
bool AXNode::IsLeaf() const { bool AXNode::IsLeaf() const {
// A node is also a leaf if all of it's descendants are ignored. // A node is a leaf if it has no descendants, regardless whether it is ignored
if (children().empty() || !GetUnignoredChildCount()) // or not.
if (children().empty())
return true;
// Leaf nodes with descendants should always be exposed to the platforms'
// accessibility layer.
if (IsIgnored())
return false;
// An unignored node is a leaf if all of its descendants are ignored.
if (!GetUnignoredChildCount())
return true; return true;
#if defined(OS_WIN) #if defined(OS_WIN)
......
...@@ -34,6 +34,24 @@ class AX_EXPORT AXNode final { ...@@ -34,6 +34,24 @@ class AX_EXPORT AXNode final {
// kInvalidAXID. // kInvalidAXID.
static constexpr AXID kInvalidAXID = 0; static constexpr AXID kInvalidAXID = 0;
// Replacement character used to represent an embedded (or, additionally for
// text navigation, an empty) object. Encoded in UTF16 format. Part of the
// Unicode Standard.
//
// On some platforms, most objects are represented in the text of their
// parents with a special "embedded object character" and not with their
// actual text contents. Also on the same platforms, if a node has only
// ignored descendants, i.e., it appears to be empty to assistive software, we
// need to treat it as a character and a word boundary.
//
// Note that we cannot use L"..." because it works correctly only on Windows.
// TODO(nektar): Consider using UTF8 encoding instead, "\xEF\xBF\xBC".
static constexpr base::char16 kEmbeddedCharacter[] = {0xFFFC, 0x0000};
// We compute the embedded character's length instead of manually typing it in
// order to avoid the two variables getting out of sync in a future update.
static constexpr int kEmbeddedCharacterLength =
int{sizeof(kEmbeddedCharacter) / sizeof(base::char16) - 1};
// Interface to the tree class that owns an AXNode. We use this instead // Interface to the tree class that owns an AXNode. We use this instead
// of letting AXNode have a pointer to its AXTree directly so that we're // of letting AXNode have a pointer to its AXTree directly so that we're
// forced to think twice before calling an AXTree interface that might not // forced to think twice before calling an AXTree interface that might not
...@@ -308,16 +326,29 @@ class AX_EXPORT AXNode final { ...@@ -308,16 +326,29 @@ class AX_EXPORT AXNode final {
// //
// This is how displayed text and embedded objects are represented in // This is how displayed text and embedded objects are represented in
// ATK and IAccessible2 APIs. // ATK and IAccessible2 APIs.
std::string GetHypertext() const; //
// TODO(nektar): Consider changing the return value to std::string.
base::string16 GetHypertext() const;
// Returns the text of this node and all descendant nodes; including text // Returns the text that is found inside this node and all its descendants;
// found in embedded objects. // including text found in embedded objects.
// //
// Only text displayed on screen is included. Text from ARIA and HTML // Only text displayed on screen is included. Text from ARIA and HTML
// attributes that is either not displayed on screen, or outside this node, is // attributes that is either not displayed on screen, or outside this node, is
// not returned. // not returned.
std::string GetInnerText() const; std::string GetInnerText() const;
// Returns the length of the text (in UTF16 code units) that is found inside
// this node and all its descendants; including text found in embedded
// objects.
//
// Only text displayed on screen is counted. Text from ARIA and HTML
// attributes that is either not displayed on screen, or outside this node, is
// not included.
//
// The length of the text is in UTF8 code units, not in grapheme clusters.
int GetInnerTextLength() const;
// Returns a string representing the language code. // Returns a string representing the language code.
// //
// This will consider the language declared in the DOM, and may eventually // This will consider the language declared in the DOM, and may eventually
...@@ -444,8 +475,16 @@ class AX_EXPORT AXNode final { ...@@ -444,8 +475,16 @@ class AX_EXPORT AXNode final {
// platform's accessibility layer. // platform's accessibility layer.
bool IsChildOfLeaf() const; bool IsChildOfLeaf() const;
// Returns true if this is a leaf node that has no inner text. Note that all
// descendants of a leaf node are not exposed to any platform's accessibility
// layer, but they may be used to compute the node's inner text. Note also
// that, ignored nodes (leaf or otherwise) do not expose their inner text or
// hypertext to the platforms' accessibility layer, but they expose the inner
// text or hypertext of their unignored descendants.
bool IsEmptyLeaf() const;
// Returns true if this is a leaf node, meaning all its // Returns true if this is a leaf node, meaning all its
// children should not be exposed to any platform's native accessibility // descendants should not be exposed to any platform's accessibility
// layer. // layer.
// //
// The definition of a leaf includes nodes with children that are exclusively // The definition of a leaf includes nodes with children that are exclusively
......
...@@ -197,7 +197,7 @@ base::string16 AXNodePosition::GetText() const { ...@@ -197,7 +197,7 @@ base::string16 AXNodePosition::GetText() const {
case AXEmbeddedObjectBehavior::kSuppressCharacter: case AXEmbeddedObjectBehavior::kSuppressCharacter:
return base::UTF8ToUTF16(anchor->GetInnerText()); return base::UTF8ToUTF16(anchor->GetInnerText());
case AXEmbeddedObjectBehavior::kExposeCharacter: case AXEmbeddedObjectBehavior::kExposeCharacter:
return base::UTF8ToUTF16(anchor->GetHypertext()); return anchor->GetHypertext();
} }
} }
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment