Commit 5eea952e authored by Victor Fei's avatar Victor Fei Committed by Commit Bot

Refactor PosInSet & SetSize algorithm

Refactored the PosInSet & SetSize algorithm, more specifically:
  - AXTree::ComputeSetSizePosInSetAndCache
  - AXTree::PopulateOrderedSetItemsMap
To compute PosInSet & SetSize for ordered set items, we first need to
parse ordered set items from DOM structure and store/populate it into
a data structure, and from which we can perform PosInSet & SetSize
computations. Previously, we store these ordered set items into
std::vector<AXNode*>. Using std::vector works well when set items
from various hierarchical levels are nested in DOM structure. However,
for flattened structure where levels are distinguished by aria-level
attributes, the previous algorithm did not work well. Although
various changes (e.g. CL:1461539) have been made, the algorithm is
still not robust enough for handling flattened structures, Bug:952904.

The primary change in the current CL is to replace
|items_to_be_populated| from std::vector to std::map (and renaming it
to |items_map_to_be_populated|), where hierarchical levels are mapped
to lists of ordered sets:

  std::map<base::Optional<int32_t>, // Hierarchical level
        std::vector<OrderedSetContent>> // Ordered sets on that level

  struct AXTree::OrderedSetContent {
    std::vector<AXNode*> set_items_; // Ordered set items
    AXNode* set_container_; // Ordered set container
  };

This new data structure enables AXTree::PopulateOrderedSetItemsMap to
parse, sort, and store various ordered set items according to its
ordered set and hierarchical level, which greatly cleans up PosInSet
and SetSize algorithm, and more robust handlings for flattened ordered
set structures.

Bug: 952904
Change-Id: Ia856290b143afcf911d9083c735dd7bf9bb3ddf9
Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2036908Reviewed-by: default avatarDominic Mazzoni <dmazzoni@chromium.org>
Reviewed-by: default avatarIan Prest <iapres@microsoft.com>
Reviewed-by: default avatarAkihiro Ota <akihiroota@chromium.org>
Commit-Queue: Victor Fei <vicfei@microsoft.com>
Cr-Commit-Position: refs/heads/master@{#745315}
parent 8fe57796
...@@ -1006,9 +1006,8 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, ...@@ -1006,9 +1006,8 @@ IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
RunAriaTest(FILE_PATH_LITERAL("aria-sort-aria-grid.html")); RunAriaTest(FILE_PATH_LITERAL("aria-sort-aria-grid.html"));
} }
// Flaky. http://crbug.com/952904
IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest, IN_PROC_BROWSER_TEST_P(DumpAccessibilityTreeTest,
DISABLED_AccessibilityAriaSetCountsWithTreeLevels) { AccessibilityAriaSetCountsWithTreeLevels) {
RunAriaTest(FILE_PATH_LITERAL("aria-set-counts-with-tree-levels.html")); RunAriaTest(FILE_PATH_LITERAL("aria-set-counts-with-tree-levels.html"));
} }
......
rootWebArea rootWebArea
++tree setSize=2 ++genericContainer ignored
++++treeItem name='Item A' hierarchicalLevel=1 setSize=2 posInSet=1 selected=false ++++tree setSize=2
++++++staticText name='Item A' ++++++treeItem name='Item A' hierarchicalLevel=1 setSize=2 posInSet=1
++++++++inlineTextBox name='Item A' ++++++++staticText name='Item A'
++++treeItem name='Item A1' hierarchicalLevel=2 setSize=2 posInSet=1 selected=false ++++++++++inlineTextBox name='Item A'
++++++staticText name='Item A1' ++++++treeItem name='Item A1' hierarchicalLevel=2 setSize=2 posInSet=1
++++++++inlineTextBox name='Item A1' ++++++++staticText name='Item A1'
++++treeItem name='Item A1x' hierarchicalLevel=3 setSize=3 posInSet=1 selected=false ++++++++++inlineTextBox name='Item A1'
++++++staticText name='Item A1x' ++++++treeItem name='Item A1x' hierarchicalLevel=3 setSize=3 posInSet=1
++++++++inlineTextBox name='Item A1x' ++++++++staticText name='Item A1x'
++++treeItem name='Item A1y' hierarchicalLevel=3 setSize=3 posInSet=2 selected=false ++++++++++inlineTextBox name='Item A1x'
++++++staticText name='Item A1y' ++++++treeItem name='Item A1y' hierarchicalLevel=3 setSize=3 posInSet=2
++++++++inlineTextBox name='Item A1y' ++++++++staticText name='Item A1y'
++++treeItem name='Item A1z' hierarchicalLevel=3 setSize=3 posInSet=3 selected=false ++++++++++inlineTextBox name='Item A1y'
++++++staticText name='Item A1z' ++++++treeItem name='Item A1z' hierarchicalLevel=3 setSize=3 posInSet=3
++++++++inlineTextBox name='Item A1z' ++++++++staticText name='Item A1z'
++++treeItem name='Item A2' hierarchicalLevel=2 setSize=2 posInSet=2 selected=false ++++++++++inlineTextBox name='Item A1z'
++++++staticText name='Item A2' ++++++treeItem name='Item A2' hierarchicalLevel=2 setSize=2 posInSet=2
++++++++inlineTextBox name='Item A2' ++++++++staticText name='Item A2'
++++treeItem name='Item B' hierarchicalLevel=1 setSize=2 posInSet=2 selected=false ++++++++++inlineTextBox name='Item A2'
++++++staticText name='Item B' ++++++treeItem name='Item B' hierarchicalLevel=1 setSize=2 posInSet=2
++++++++inlineTextBox name='Item B' ++++++++staticText name='Item B'
++++treeItem name='Item B1' hierarchicalLevel=2 setSize=1 posInSet=1 selected=false ++++++++++inlineTextBox name='Item B'
++++++staticText name='Item B1' ++++++treeItem name='Item B1' hierarchicalLevel=2 setSize=1 posInSet=1
++++++++inlineTextBox name='Item B1' ++++++++staticText name='Item B1'
++++++++++inlineTextBox name='Item B1'
<!-- <!--
@BLINK-ALLOW:hierarchicalLevel*
@BLINK-ALLOW:setSize* @BLINK-ALLOW:setSize*
@BLINK-ALLOW:posInSet* @BLINK-ALLOW:posInSet*
@BLINK-DENY:setSize=0 @BLINK-DENY:setSize=0
@BLINK-DENY:posInSet=0 @BLINK-DENY:posInSet=0
--> -->
<!-- According to CORE-AAM:
For role="treeitem", walk the tree backward and forward until the explicit or
computed level becomes less than the current item's level. Count items only
if they are at the same level as the current item.
-->
<html> <html>
<body> <body>
<div role="tree"> <div role="tree">
......
...@@ -11,4 +11,3 @@ rootWebArea ...@@ -11,4 +11,3 @@ rootWebArea
++++++++++tabList horizontal hierarchicalLevel=3 setSize=1 ++++++++++tabList horizontal hierarchicalLevel=3 setSize=1
++++++++++++tab name='Tab 1, level 3' setSize=1 posInSet=1 selected=false ++++++++++++tab name='Tab 1, level 3' setSize=1 posInSet=1 selected=false
++++++tab name='Tab 3, level 1' setSize=3 posInSet=3 selected=false ++++++tab name='Tab 3, level 1' setSize=3 posInSet=3 selected=false
<-- End-of-file -->
...@@ -734,6 +734,19 @@ void AXNode::IdVectorToNodeVector(std::vector<int32_t>& ids, ...@@ -734,6 +734,19 @@ void AXNode::IdVectorToNodeVector(std::vector<int32_t>& ids,
} }
} }
base::Optional<int> AXNode::GetHierarchicalLevel() const {
int hierarchical_level =
GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
// According to the WAI_ARIA spec, a defined hierarchical level value is
// greater than 0.
// https://www.w3.org/TR/wai-aria-1.1/#aria-level
if (hierarchical_level > 0)
return base::Optional<int>(hierarchical_level);
return base::nullopt;
}
bool AXNode::IsOrderedSetItem() const { bool AXNode::IsOrderedSetItem() const {
return ui::IsItemLike(data().role); return ui::IsItemLike(data().role);
} }
...@@ -806,7 +819,8 @@ bool AXNode::SetRoleMatchesItemRole(const AXNode* ordered_set) const { ...@@ -806,7 +819,8 @@ bool AXNode::SetRoleMatchesItemRole(const AXNode* ordered_set) const {
case ax::mojom::Role::kList: case ax::mojom::Role::kList:
return item_role == ax::mojom::Role::kListItem; return item_role == ax::mojom::Role::kListItem;
case ax::mojom::Role::kGroup: case ax::mojom::Role::kGroup:
return item_role == ax::mojom::Role::kListItem || return item_role == ax::mojom::Role::kComment ||
item_role == ax::mojom::Role::kListItem ||
item_role == ax::mojom::Role::kMenuItem || item_role == ax::mojom::Role::kMenuItem ||
item_role == ax::mojom::Role::kMenuItemRadio || item_role == ax::mojom::Role::kMenuItemRadio ||
item_role == ax::mojom::Role::kTreeItem; item_role == ax::mojom::Role::kTreeItem;
......
...@@ -264,6 +264,9 @@ class AX_EXPORT AXNode final { ...@@ -264,6 +264,9 @@ class AX_EXPORT AXNode final {
return data().GetHtmlAttribute(attribute, value); return data().GetHtmlAttribute(attribute, value);
} }
// Return the hierarchical level if supported.
base::Optional<int> GetHierarchicalLevel() const;
// PosInSet and SetSize public methods. // PosInSet and SetSize public methods.
bool IsOrderedSetItem() const; bool IsOrderedSetItem() const;
bool IsOrderedSet() const; bool IsOrderedSet() const;
......
...@@ -565,6 +565,84 @@ struct AXTreeUpdateState { ...@@ -565,6 +565,84 @@ struct AXTreeUpdateState {
const AXTree& tree; const AXTree& tree;
}; };
struct AXTree::NodeSetSizePosInSetInfo {
NodeSetSizePosInSetInfo() = default;
~NodeSetSizePosInSetInfo() = default;
int32_t pos_in_set = 0;
int32_t set_size = 0;
base::Optional<int> lowest_hierarchical_level;
};
struct AXTree::OrderedSetContent {
explicit OrderedSetContent(const AXNode* ordered_set = nullptr)
: ordered_set_(ordered_set) {}
~OrderedSetContent() = default;
std::vector<const AXNode*> set_items_;
// Some ordered set items may not be associated with an ordered set.
const AXNode* ordered_set_;
};
struct AXTree::OrderedSetItemsMap {
OrderedSetItemsMap() = default;
~OrderedSetItemsMap() = default;
// Check if a particular hierarchical level exists in this map.
bool HierarchicalLevelExists(base::Optional<int> level) {
if (items_map_.find(level) == items_map_.end())
return false;
return true;
}
// Add the OrderedSetContent to the corresponding hierarchical level in the
// map.
void Add(base::Optional<int> level,
const OrderedSetContent& ordered_set_content) {
if (!HierarchicalLevelExists(level))
items_map_[level] = std::vector<OrderedSetContent>();
items_map_[level].push_back(ordered_set_content);
}
// Add an ordered set item to the OrderedSetItemsMap given its hierarchical
// level. We always want to append the item to the last OrderedSetContent of
// that hierarchical level, due to the following:
// - The last OrderedSetContent on any level of the items map is in progress
// of being populated.
// - All other OrderedSetContent other than the last one on a level
// represents a complete ordered set and should not be modified.
void AddItemToBack(base::Optional<int> level, const AXNode* item) {
if (!HierarchicalLevelExists(level))
return;
std::vector<OrderedSetContent>& sets_list = items_map_[level];
if (!sets_list.empty()) {
OrderedSetContent& ordered_set_content = sets_list.back();
ordered_set_content.set_items_.push_back(item);
}
}
// Retrieve the first OrderedSetContent of the OrderedSetItemsMap.
OrderedSetContent* GetFirstOrderedSetContent() {
if (items_map_.empty())
return nullptr;
std::vector<OrderedSetContent>& sets_list = items_map_.begin()->second;
if (sets_list.empty())
return nullptr;
return &(sets_list.front());
}
// Clears all the content in the map.
void Clear() { items_map_.clear(); }
// Maps a hierarchical level to a list of OrderedSetContent.
std::map<base::Optional<int32_t>, std::vector<OrderedSetContent>> items_map_;
};
AXTree::AXTree() { AXTree::AXTree() {
AXNodeData root; AXNodeData root;
root.id = AXNode::kInvalidAXID; root.id = AXNode::kInvalidAXID;
...@@ -979,8 +1057,8 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) { ...@@ -979,8 +1057,8 @@ bool AXTree::Unserialize(const AXTreeUpdate& update) {
} }
} }
// Clear list_info_map_ // Clears |node_set_size_pos_in_set_info_map_|
ordered_set_info_map_.clear(); node_set_size_pos_in_set_info_map_.clear();
std::vector<AXTreeObserver::Change> changes; std::vector<AXTreeObserver::Change> changes;
changes.reserve(update.nodes.size()); changes.reserve(update.nodes.size());
...@@ -1831,50 +1909,71 @@ int32_t AXTree::GetNextNegativeInternalNodeId() { ...@@ -1831,50 +1909,71 @@ int32_t AXTree::GetNextNegativeInternalNodeId() {
return return_value; return return_value;
} }
void AXTree::PopulateOrderedSetItems( void AXTree::PopulateOrderedSetItemsMap(
const AXNode& original_node, const AXNode& original_node,
const AXNode* ordered_set, const AXNode* ordered_set,
std::vector<const AXNode*>& items_to_be_populated) const { OrderedSetItemsMap& items_map_to_be_populated) const {
// Ignored nodes are not a part of ordered sets. // Ignored nodes are not a part of ordered sets.
if (original_node.IsIgnored()) if (original_node.IsIgnored())
return; return;
// Default hierarchical_level is 0, which represents that no hierarchical // Not all ordered set containers support hierarchical level, but their set
// level was detected on |original_node|. // items may support hierarchical level. For example, container <tree> does
int original_node_min_level = original_node.GetIntAttribute( // not support level, but <treeitem> supports level. For ordered sets like
ax::mojom::IntAttribute::kHierarchicalLevel); // this, the set container (e.g. <tree>) will take on the min of the levels
// of its direct children(e.g. <treeitem>), if the children's levels are
// If we are calling this function on the ordered set container itself, that // defined.
// is |original_node| is ordered set, then set |original_node|'s hierarchical base::Optional<int> ordered_set_min_level =
// level to be the min level of |original_node|'s direct children, if the ordered_set->GetHierarchicalLevel();
// child's level is defined.
if (&original_node == ordered_set) { for (AXNode::UnignoredChildIterator child =
for (AXNode::UnignoredChildIterator itr = ordered_set->UnignoredChildrenBegin();
original_node.UnignoredChildrenBegin(); child != ordered_set->UnignoredChildrenEnd(); ++child) {
itr != original_node.UnignoredChildrenEnd(); ++itr) { base::Optional<int> child_level = child->GetHierarchicalLevel();
int32_t child_level = if (child_level) {
itr->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel); ordered_set_min_level = ordered_set_min_level
if (child_level > 0) ? std::min(child_level, ordered_set_min_level)
original_node_min_level = : child_level;
original_node_min_level > 0
? std::min(child_level, original_node_min_level)
: child_level;
} }
} }
RecursivelyPopulateOrderedSetItems(original_node, ordered_set, ordered_set, RecursivelyPopulateOrderedSetItemsMap(original_node, ordered_set, ordered_set,
original_node_min_level, ordered_set_min_level, base::nullopt,
items_to_be_populated); items_map_to_be_populated);
// If after |RecursivelyPopulateOrderedSetItemsMap| call, the corresponding
// level (i.e. ordered_set_min_level) does not exist in
// |items_map_to_be_populated|, and |original_node| equals |ordered_set|, we
// know |original_node| is an empty ordered set and contains no set items.
// However, |original_node| may still have set size attribute, so we still
// want to add this empty set (i.e. original_node/ordered_set) to
// |items_map_to_be_populated|.
if (&original_node == ordered_set &&
!items_map_to_be_populated.HierarchicalLevelExists(ordered_set_min_level))
items_map_to_be_populated.Add(ordered_set_min_level,
OrderedSetContent(&original_node));
} }
void AXTree::RecursivelyPopulateOrderedSetItems( void AXTree::RecursivelyPopulateOrderedSetItemsMap(
const AXNode& original_node, const AXNode& original_node,
const AXNode* ordered_set, const AXNode* ordered_set,
const AXNode* local_parent, const AXNode* local_parent,
int32_t original_node_min_level, base::Optional<int> ordered_set_min_level,
std::vector<const AXNode*>& items_to_be_populated) const { base::Optional<int> prev_level,
// Stop searching recursively on node |local_parent| if it turns out to be an OrderedSetItemsMap& items_map_to_be_populated) const {
// ordered set whose role matches that of the top level ordered set. // For optimization purpose, we want to only populate set items that are
// direct descendants of |ordered_set|, since we will only be calculating
// PosInSet & SetSize of items of that level. So we skip items on deeper
// levels by stop searching recursively on node |local_parent| that turns out
// to be an ordered set whose role matches that of |ordered_set|. However,
// when we encounter a flattened structure such as the following:
// <div role="tree">
// <div role="treeitem" aria-level="1"></div>
// <div role="treeitem" aria-level="2"></div>
// <div role="treeitem" aria-level="3"></div>
// </div>
// This optimization won't apply, we will end up populating items from all
// levels.
if (ordered_set->data().role == local_parent->data().role && if (ordered_set->data().role == local_parent->data().role &&
ordered_set != local_parent) ordered_set != local_parent)
return; return;
...@@ -1895,7 +1994,9 @@ void AXTree::RecursivelyPopulateOrderedSetItems( ...@@ -1895,7 +1994,9 @@ void AXTree::RecursivelyPopulateOrderedSetItems(
continue; continue;
} }
// Add child to |items_to_be_populated| if role matches with the role of base::Optional<int> curr_level = child->GetHierarchicalLevel();
// Add child to |items_map_to_be_populated| if role matches with the role of
// |ordered_set|. If role of node is kRadioButton, don't add items of other // |ordered_set|. If role of node is kRadioButton, don't add items of other
// roles, even if item role matches the role of |ordered_set|. // roles, even if item role matches the role of |ordered_set|.
if (child->data().role == ax::mojom::Role::kComment || if (child->data().role == ax::mojom::Role::kComment ||
...@@ -1903,56 +2004,66 @@ void AXTree::RecursivelyPopulateOrderedSetItems( ...@@ -1903,56 +2004,66 @@ void AXTree::RecursivelyPopulateOrderedSetItems(
child->data().role == ax::mojom::Role::kRadioButton) || child->data().role == ax::mojom::Role::kRadioButton) ||
(original_node.data().role != ax::mojom::Role::kRadioButton && (original_node.data().role != ax::mojom::Role::kRadioButton &&
child->SetRoleMatchesItemRole(ordered_set))) { child->SetRoleMatchesItemRole(ordered_set))) {
int child_level = // According to WAI-ARIA spec, some ordered set items do not support
child->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel); // hierarchical level while its ordered set container does. For example,
// <tab> does not support level, while <tablist> supports level.
// If the hierarchical level of |child| and the level of |original_node|
// differ, we do not add child to |items_to_be_populated| and we do not
// recurse into |child| and populate its order set item descendants.
// Additionally, as an exception, we always add tab items to the set,
// because according to WAI-ARIA spec, tab does not support hierarchical
// level, while tab's set container tablist supports hierarchical level.
// Due to this, we always assume sibling tabs are always on the same
// level, and always add tab child item to |items_to_be_populated|.
// https://www.w3.org/WAI/PF/aria/roles#tab // https://www.w3.org/WAI/PF/aria/roles#tab
// https://www.w3.org/WAI/PF/aria/roles#tablist // https://www.w3.org/WAI/PF/aria/roles#tablist
if (child_level != original_node_min_level && // For this special case, when we add set items (e.g. tab) to
child->data().role != ax::mojom::Role::kTab) { // |items_map_to_be_populated|, set item is placed at the same level as
if (child_level < original_node_min_level && // its container (e.g. tablist) in |items_map_to_be_populated|.
original_node.GetUnignoredParent() == child->GetUnignoredParent()) { if (!curr_level && child->GetUnignoredParent() == ordered_set)
// For a flattened structure, where |original_node| and |child| share curr_level = ordered_set_min_level;
// the same parent, if a decrease in level occurs after
// |original_node| has been examined (i.e. |original_node|'s index // We only add child to |items_map_to_be_populated| if the child set item
// comes before that of |child|), we stop adding to this set, and stop // is at the same hierarchical level as |ordered_set|'s level.
// from populating |child|'s other siblings to |items_to_be_populated| if (!items_map_to_be_populated.HierarchicalLevelExists(curr_level)) {
// as well. const AXNode* child_ordered_set =
if (original_node.GetUnignoredIndexInParent() < (child->SetRoleMatchesItemRole(ordered_set) &&
child->GetUnignoredIndexInParent()) ordered_set_min_level == curr_level)
break; ? ordered_set
: nullptr;
// For a flattened structure, where |original_node| and |child| share items_map_to_be_populated.Add(curr_level,
// the same parent, if a decrease in level has been detected before OrderedSetContent(child_ordered_set));
// |original_node| has been examined (i.e. |original_node|'s index
// comes after that of |child|), then everything previously added to
// items actually belongs to a different set. Clear the items set.
items_to_be_populated.clear();
}
continue;
} }
// We only add child to |items_to_be_populated| if the child set item is items_map_to_be_populated.AddItemToBack(curr_level, child);
// at the same hierarchical level as |original_node|'s level.
items_to_be_populated.push_back(child);
} }
// Recurse if there is a generic container, ignored, or unknown. // Recurse if there is a generic container, ignored, or unknown.
if (child->IsIgnored() || if (child->IsIgnored() ||
child->data().role == ax::mojom::Role::kGenericContainer || child->data().role == ax::mojom::Role::kGenericContainer ||
child->data().role == ax::mojom::Role::kUnknown) { child->data().role == ax::mojom::Role::kUnknown) {
RecursivelyPopulateOrderedSetItems(original_node, ordered_set, child, RecursivelyPopulateOrderedSetItemsMap(original_node, ordered_set, child,
original_node_min_level, ordered_set_min_level, curr_level,
items_to_be_populated); items_map_to_be_populated);
} }
// If |curr_level| goes up one level from |prev_level|, which indicates
// the ordered set of |prev_level| is closed, we add a new OrderedSetContent
// on the previous level of |items_map_to_be_populated| to signify this.
// Consider the example below:
// <div role="tree">
// <div role="treeitem" aria-level="1"></div>
// <!--- set1-level2 -->
// <div role="treeitem" aria-level="2"></div>
// <div role="treeitem" aria-level="2"></div> <--|prev_level|
// <div role="treeitem" aria-level="1" id="item2-level1"> <--|curr_level|
// </div>
// <!--- set2-level2 -->
// <div role="treeitem" aria-level="2"></div>
// <div role="treeitem" aria-level="2"></div>
// </div>
// |prev_level| is on the last item of "set1-level2" and |curr_level| is on
// "item2-level1". Since |curr_level| is up one level from |prev_level|, we
// already completed adding all items from "set1-level2" to
// |items_map_to_be_populated|. So we close up "set1-level2" by adding a new
// OrderedSetContent to level 2. When |curr_level| ends up on the items of
// "set2-level2" next, it has a fresh new set to be populated.
if (child->SetRoleMatchesItemRole(ordered_set) && curr_level < prev_level)
items_map_to_be_populated.Add(prev_level, OrderedSetContent());
prev_level = curr_level;
} }
} }
...@@ -1962,143 +2073,162 @@ void AXTree::RecursivelyPopulateOrderedSetItems( ...@@ -1962,143 +2073,162 @@ void AXTree::RecursivelyPopulateOrderedSetItems(
void AXTree::ComputeSetSizePosInSetAndCache(const AXNode& node, void AXTree::ComputeSetSizePosInSetAndCache(const AXNode& node,
const AXNode* ordered_set) { const AXNode* ordered_set) {
DCHECK(ordered_set); DCHECK(ordered_set);
std::vector<const AXNode*> items;
// Find all items within ordered_set and add to vector. // Set items role::kComment and role::kRadioButton are special cases and do
PopulateOrderedSetItems(node, ordered_set, items); // not necessarily need to be contained in an ordered set.
if (node.data().role != ax::mojom::Role::kComment &&
node.data().role != ax::mojom::Role::kRadioButton &&
!node.SetRoleMatchesItemRole(ordered_set) && !IsSetLike(node.data().role))
return;
OrderedSetItemsMap items_map_to_be_populated;
// Find all items within ordered_set and add to |items_map_to_be_populated|.
PopulateOrderedSetItemsMap(node, ordered_set, items_map_to_be_populated);
// If ordered_set role is kPopUpButton and it wraps a kMenuListPopUp, then we // If ordered_set role is kPopUpButton and it wraps a kMenuListPopUp, then we
// would like it to inherit the SetSize from the kMenuListPopUp it wraps. To // would like it to inherit the SetSize from the kMenuListPopUp it wraps. To
// do this, we treat the kMenuListPopUp as the ordered_set and eventually // do this, we treat the kMenuListPopUp as the ordered_set and eventually
// assign its SetSize value to the kPopUpButton. // assign its SetSize value to the kPopUpButton.
if ((node.data().role == ax::mojom::Role::kPopUpButton) && if (node.data().role == ax::mojom::Role::kPopUpButton) {
(items.size() != 0)) {
// kPopUpButtons are only allowed to contain one kMenuListPopUp. // kPopUpButtons are only allowed to contain one kMenuListPopUp.
// The single element is guaranteed to be a kMenuListPopUp because that is // The single element is guaranteed to be a kMenuListPopUp because that is
// the only item role that matches the ordered set role of kPopUpButton. // the only item role that matches the ordered set role of kPopUpButton.
// Please see AXNode::SetRoleMatchesItemRole for more details. // Please see AXNode::SetRoleMatchesItemRole for more details.
DCHECK(items.size() == 1); OrderedSetContent* set_content =
const AXNode* menu_list_popup = items[0]; items_map_to_be_populated.GetFirstOrderedSetContent();
items.clear(); if (set_content && set_content->set_items_.size() == 1) {
PopulateOrderedSetItems(node, menu_list_popup, items); const AXNode* menu_list_popup = set_content->set_items_.front();
if (menu_list_popup->data().role == ax::mojom::Role::kMenuListPopup) {
items_map_to_be_populated.Clear();
PopulateOrderedSetItemsMap(node, menu_list_popup,
items_map_to_be_populated);
set_content = items_map_to_be_populated.GetFirstOrderedSetContent();
// Replace |set_content|'s ordered set container with |node|
// (Role::kPopUpButton), which acts as the set container for nodes with
// Role::kMenuListOptions (children of |menu_list_popup|).
if (set_content)
set_content->ordered_set_ = &node;
}
}
}
// Iterate over all items from OrderedSetItemsMap to compute and cache each
// ordered set item's PosInSet and SetSize and corresponding ordered set
// container's SetSize.
for (auto element : items_map_to_be_populated.items_map_) {
for (const OrderedSetContent& ordered_set_content : element.second) {
ComputeSetSizePosInSetAndCacheHelper(ordered_set_content);
}
} }
}
// Keep track of the number of elements ordered_set has. void AXTree::ComputeSetSizePosInSetAndCacheHelper(
const OrderedSetContent& ordered_set_content) {
// Keep track of number of items in the set.
int32_t num_elements = 0; int32_t num_elements = 0;
// Necessary for calculating set_size. // Keep track of largest ordered set item's |aria-setsize| attribute value.
int32_t largest_assigned_set_size = 0; int32_t max_item_set_size_from_attribute = 0;
// Compute pos_in_set_values. for (const AXNode* item : ordered_set_content.set_items_) {
for (size_t i = 0; i < items.size(); ++i) { // |item|'s PosInSet value is the maximum of accumulated number of
const AXNode* item = items[i]; // elements count and the value from its |aria-posinset| attribute.
ordered_set_info_map_[item->id()] = OrderedSetInfo(); int32_t pos_in_set_value =
int32_t pos_in_set_value = 0; std::max(num_elements + 1,
int hierarchical_level =
item->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel);
pos_in_set_value = num_elements + 1;
// Check if item has a valid kPosInSet assignment, which takes precedence
// over previous assignment. Invalid assignments are decreasing or
// duplicates, and should be ignored.
pos_in_set_value =
std::max(pos_in_set_value,
item->GetIntAttribute(ax::mojom::IntAttribute::kPosInSet)); item->GetIntAttribute(ax::mojom::IntAttribute::kPosInSet));
// If level is specified, use author-provided value, if present. // For |item| that has defined hierarchical level and |aria-posinset|
if (hierarchical_level != 0 && // attribute, the attribute value takes precedence.
item->HasIntAttribute(ax::mojom::IntAttribute::kPosInSet)) { // Note: According to WAI-ARIA spec, items that support
// |aria-posinset| do not necessarily support hierarchical level.
if (item->GetHierarchicalLevel() &&
item->HasIntAttribute(ax::mojom::IntAttribute::kPosInSet))
pos_in_set_value = pos_in_set_value =
item->GetIntAttribute(ax::mojom::IntAttribute::kPosInSet); item->GetIntAttribute(ax::mojom::IntAttribute::kPosInSet);
}
// Assign pos_in_set and update role counts.
ordered_set_info_map_[item->id()].pos_in_set = pos_in_set_value;
num_elements = pos_in_set_value; num_elements = pos_in_set_value;
// Check if kSetSize is assigned and update if it's the largest assigned // Cache computed PosInSet value for |item|.
// kSetSize. node_set_size_pos_in_set_info_map_[item->id()] = NodeSetSizePosInSetInfo();
if (item->HasIntAttribute(ax::mojom::IntAttribute::kSetSize)) node_set_size_pos_in_set_info_map_[item->id()].pos_in_set =
largest_assigned_set_size = pos_in_set_value;
std::max(largest_assigned_set_size,
item->GetIntAttribute(ax::mojom::IntAttribute::kSetSize)); // Track the largest set size for this OrderedSetContent.
} max_item_set_size_from_attribute =
std::max(max_item_set_size_from_attribute,
item->GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
} // End of iterating over each item in |ordered_set_content|.
// Compute set_size value. // The SetSize of an ordered set (and all of its items) is the maximum of
// The SetSize of an ordered set (and all of its items) is the maximum of the // the following values:
// following candidate values:
// 1. The number of elements in the ordered set. // 1. The number of elements in the ordered set.
// 2. The Largest assigned SetSize in the ordered set. // 2. The largest item set size from |aria-setsize| attribute.
// 3. The SetSize assigned within the ordered set. // 3. The ordered set container's |aria-setsize| attribute value.
int32_t set_size_value =
// Set to 0 if ordered_set has no kSetSize attribute. std::max(num_elements, max_item_set_size_from_attribute);
int32_t ordered_set_candidate =
ordered_set->GetIntAttribute(ax::mojom::IntAttribute::kSetSize); // Cache the hierarchical level and set size of |ordered_set_content|'s set
// container, if the container exists.
int32_t set_size_value = std::max( if (const AXNode* ordered_set = ordered_set_content.ordered_set_) {
std::max(num_elements, largest_assigned_set_size), ordered_set_candidate); set_size_value = std::max(
set_size_value,
// Assign set_size to ordered_set. ordered_set->GetIntAttribute(ax::mojom::IntAttribute::kSetSize));
// Must meet one of two conditions:
// 1. Node role matches ordered set role. // Cache |ordered_set|'s hierarchical level.
// 2. The node that calculations were called on is the ordered_set. base::Optional<int> ordered_set_level = ordered_set->GetHierarchicalLevel();
if (node.SetRoleMatchesItemRole(ordered_set) || ordered_set == &node) { if (node_set_size_pos_in_set_info_map_.find(ordered_set->id()) ==
auto ordered_set_info_result = node_set_size_pos_in_set_info_map_.end()) {
ordered_set_info_map_.find(ordered_set->id()); node_set_size_pos_in_set_info_map_[ordered_set->id()] =
int hierarchical_level = NodeSetSizePosInSetInfo();
node.GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel); node_set_size_pos_in_set_info_map_[ordered_set->id()]
// If ordered_set is not in the cache, assign it a new set_size. .lowest_hierarchical_level = ordered_set_level;
if (ordered_set_info_result == ordered_set_info_map_.end()) { } else if (node_set_size_pos_in_set_info_map_[ordered_set->id()]
ordered_set_info_map_[ordered_set->id()] = OrderedSetInfo(); .lowest_hierarchical_level > ordered_set_level) {
ordered_set_info_map_[ordered_set->id()].set_size = set_size_value; node_set_size_pos_in_set_info_map_[ordered_set->id()]
ordered_set_info_map_[ordered_set->id()].lowest_hierarchical_level = .lowest_hierarchical_level = ordered_set_level;
hierarchical_level;
} else {
OrderedSetInfo ordered_set_info = ordered_set_info_result->second;
if (ordered_set_info.lowest_hierarchical_level > hierarchical_level) {
ordered_set_info.set_size = set_size_value;
ordered_set_info.lowest_hierarchical_level = hierarchical_level;
}
} }
// Cache |ordered_set|'s set size.
node_set_size_pos_in_set_info_map_[ordered_set->id()].set_size =
set_size_value;
} }
// Assign set_size to items. // Cache the set size of |ordered_set_content|'s set items.
for (size_t j = 0; j < items.size(); ++j) { for (const AXNode* item : ordered_set_content.set_items_) {
const AXNode* item = items[j]; // If item's hierarchical level and |aria-setsize| attribute are specified,
int hierarchical_level = // the item's |aria-setsize| value takes precedence.
item->GetIntAttribute(ax::mojom::IntAttribute::kHierarchicalLevel); if (item->GetHierarchicalLevel() &&
// If level is specified, use author-provided value, if present.
if (hierarchical_level != 0 &&
item->HasIntAttribute(ax::mojom::IntAttribute::kSetSize)) item->HasIntAttribute(ax::mojom::IntAttribute::kSetSize))
ordered_set_info_map_[item->id()].set_size = node_set_size_pos_in_set_info_map_[item->id()].set_size =
item->GetIntAttribute(ax::mojom::IntAttribute::kSetSize); item->GetIntAttribute(ax::mojom::IntAttribute::kSetSize);
else else
ordered_set_info_map_[item->id()].set_size = set_size_value; node_set_size_pos_in_set_info_map_[item->id()].set_size = set_size_value;
} } // End of iterating over each item in |ordered_set_content|.
} }
// Returns the pos_in_set of item. Looks in ordered_set_info_map_ for cached // Returns the pos_in_set of item. Looks in |node_set_size_pos_in_set_info_map_|
// value. Calculates pos_in_set and set_size for item (and all other items in // for cached value. Calculates pos_in_set and set_size for item (and all other
// the same ordered set) if no value is present in the cache. // items in the same ordered set) if no value is present in the cache. This
// This function is guaranteed to be only called on nodes that can hold // function is guaranteed to be only called on nodes that can hold pos_in_set
// pos_in_set values, minimizing the size of the cache. // values, minimizing the size of the cache.
int32_t AXTree::GetPosInSet(const AXNode& node, const AXNode* ordered_set) { int32_t AXTree::GetPosInSet(const AXNode& node, const AXNode* ordered_set) {
// If item's id is not in the cache, compute it. // If item's id is not in the cache, compute it.
if (ordered_set_info_map_.find(node.id()) == ordered_set_info_map_.end()) if (node_set_size_pos_in_set_info_map_.find(node.id()) ==
node_set_size_pos_in_set_info_map_.end())
ComputeSetSizePosInSetAndCache(node, ordered_set); ComputeSetSizePosInSetAndCache(node, ordered_set);
return ordered_set_info_map_[node.id()].pos_in_set; return node_set_size_pos_in_set_info_map_[node.id()].pos_in_set;
} }
// Returns the set_size of node. node could be an ordered set or an item. // Returns the set_size of node. node could be an ordered set or an item.
// Looks in ordered_set_info_map_ for cached value. Calculates pos_inset_set // Looks in |node_set_size_pos_in_set_info_map_| for cached value. Calculates
// and set_size for all nodes in same ordered set if no value is present in the // pos_in_set and set_size for all nodes in same ordered set if no value is
// cache. // present in the cache. This function is guaranteed to be only called on nodes
// This function is guaranteed to be only called on nodes that can hold // that can hold set_size values, minimizing the size of the cache.
// set_size values, minimizing the size of the cache.
int32_t AXTree::GetSetSize(const AXNode& node, const AXNode* ordered_set) { int32_t AXTree::GetSetSize(const AXNode& node, const AXNode* ordered_set) {
// If node's id is not in the cache, compute it. // If node's id is not in the cache, compute it.
if (ordered_set_info_map_.find(node.id()) == ordered_set_info_map_.end()) if (node_set_size_pos_in_set_info_map_.find(node.id()) ==
node_set_size_pos_in_set_info_map_.end())
ComputeSetSizePosInSetAndCache(node, ordered_set); ComputeSetSizePosInSetAndCache(node, ordered_set);
return ordered_set_info_map_[node.id()].set_size; return node_set_size_pos_in_set_info_map_[node.id()].set_size;
} }
AXTree::Selection AXTree::GetUnignoredSelection() const { AXTree::Selection AXTree::GetUnignoredSelection() const {
......
...@@ -142,15 +142,15 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { ...@@ -142,15 +142,15 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree {
// conflict with positive-numbered node IDs from tree sources. // conflict with positive-numbered node IDs from tree sources.
int32_t GetNextNegativeInternalNodeId(); int32_t GetNextNegativeInternalNodeId();
// Returns the pos_in_set of node. Looks in ordered_set_info_map_ for cached // Returns the pos_in_set of node. Looks in node_set_size_pos_in_set_info_map_
// value. Calculates pos_in_set and set_size for node (and all other nodes in // for cached value. Calculates pos_in_set and set_size for node (and all
// the same ordered set) if no value is present in the cache. // other nodes in the same ordered set) if no value is present in the cache.
// This function is guaranteed to be only called on nodes that can hold // This function is guaranteed to be only called on nodes that can hold
// pos_in_set values, minimizing the size of the cache. // pos_in_set values, minimizing the size of the cache.
int32_t GetPosInSet(const AXNode& node, const AXNode* ordered_set) override; int32_t GetPosInSet(const AXNode& node, const AXNode* ordered_set) override;
// Returns the set_size of node. Looks in ordered_set_info_map_ for cached // Returns the set_size of node. Looks in node_set_size_pos_in_set_info_map_
// value. Calculates pos_inset_set and set_size for node (and all other nodes // for cached value. Calculates pos_inset_set and set_size for node (and all
// in the same ordered set) if no value is present in the cache. // other nodes in the same ordered set) if no value is present in the cache.
// This function is guaranteed to be only called on nodes that can hold // This function is guaranteed to be only called on nodes that can hold
// set_size values, minimizing the size of the cache. // set_size values, minimizing the size of the cache.
int32_t GetSetSize(const AXNode& node, const AXNode* ordered_set) override; int32_t GetSetSize(const AXNode& node, const AXNode* ordered_set) override;
...@@ -323,43 +323,53 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree { ...@@ -323,43 +323,53 @@ class AX_EXPORT AXTree : public AXNode::OwnerTree {
bool enable_extra_mac_nodes_ = false; bool enable_extra_mac_nodes_ = false;
// Contains pos_in_set and set_size data for an AXNode. // Contains pos_in_set and set_size data for an AXNode.
struct OrderedSetInfo { struct NodeSetSizePosInSetInfo;
int32_t pos_in_set;
int32_t set_size; // Represents the content of an ordered set which includes the ordered set
int32_t lowest_hierarchical_level; // items and the ordered set container if it exists.
OrderedSetInfo() : pos_in_set(0), set_size(0) {} struct OrderedSetContent;
~OrderedSetInfo() {}
}; // Maps a particular hierarchical level to a list of OrderedSetContents.
// Represents all ordered set items/container on a particular hierarchical
// Populates ordered set items vector with all items associated with // level.
struct OrderedSetItemsMap;
// Populates |items_map_to_be_populated| with all items associated with
// |original_node| and within |ordered_set|. Only items whose roles match the // |original_node| and within |ordered_set|. Only items whose roles match the
// role of the |ordered_set| will be added. // role of the |ordered_set| will be added.
void PopulateOrderedSetItems( void PopulateOrderedSetItemsMap(
const AXNode& original_node, const AXNode& original_node,
const AXNode* ordered_set, const AXNode* ordered_set,
std::vector<const AXNode*>& items_to_be_populated) const; OrderedSetItemsMap& items_map_to_be_populated) const;
// Helper function for recursively populating ordered sets items vector with // Helper function for recursively populating ordered sets items map with
// all items associated with |original_node| and |ordered_set|. |local_parent| // all items associated with |original_node| and |ordered_set|. |local_parent|
// tracks the recursively passed in child nodes of |ordered_set|. // tracks the recursively passed in child nodes of |ordered_set|.
void RecursivelyPopulateOrderedSetItems( void RecursivelyPopulateOrderedSetItemsMap(
const AXNode& original_node, const AXNode& original_node,
const AXNode* ordered_set, const AXNode* ordered_set,
const AXNode* local_parent, const AXNode* local_parent,
int32_t original_node_min_level, base::Optional<int> ordered_set_min_level,
std::vector<const AXNode*>& items_to_be_populated) const; base::Optional<int> prev_level,
OrderedSetItemsMap& items_map_to_be_populated) const;
// Helper for GetPosInSet and GetSetSize. Computes the pos_in_set and set_size // Computes the pos_in_set and set_size values of all items in ordered_set and
// values of all items in ordered_set and caches those values. // caches those values. Called by GetPosInSet and GetSetSize.
void ComputeSetSizePosInSetAndCache(const AXNode& node, void ComputeSetSizePosInSetAndCache(const AXNode& node,
const AXNode* ordered_set); const AXNode* ordered_set);
// Helper for ComputeSetSizePosInSetAndCache. Computes and caches the
// pos_in_set and set_size values for a given OrderedSetContent.
void ComputeSetSizePosInSetAndCacheHelper(
const OrderedSetContent& ordered_set_content);
// Map from node ID to OrderedSetInfo. // Map from node ID to OrderedSetInfo.
// Item-like and ordered-set-like objects will map to populated OrderedSetInfo // Item-like and ordered-set-like objects will map to populated OrderedSetInfo
// objects. // objects.
// All other objects will map to default-constructed OrderedSetInfo objects. // All other objects will map to default-constructed OrderedSetInfo objects.
// Invalidated every time the tree is updated. // Invalidated every time the tree is updated.
mutable std::unordered_map<int32_t, OrderedSetInfo> ordered_set_info_map_; mutable std::unordered_map<int32_t, NodeSetSizePosInSetInfo>
node_set_size_pos_in_set_info_map_;
// AXTree owns pointers so copying is non-trivial. // AXTree owns pointers so copying is non-trivial.
DISALLOW_COPY_AND_ASSIGN(AXTree); DISALLOW_COPY_AND_ASSIGN(AXTree);
......
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