|
| 1 | +// Copyright 2019 The Chromium Authors. All rights reserved. |
| 2 | +// Use of this source code is governed by a BSD-style license that can be |
| 3 | +// found in the LICENSE file. |
| 4 | + |
| 5 | +#ifndef UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_ |
| 6 | +#define UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_ |
| 7 | + |
| 8 | +#include <atlbase.h> |
| 9 | +#include <atlcom.h> |
| 10 | + |
| 11 | +#include <UIAutomationCore.h> |
| 12 | + |
| 13 | +#include "ax/ax_node_position.h" |
| 14 | +#include "ax/ax_tree_observer.h" |
| 15 | +#include "ax/platform/ax_platform_node_delegate.h" |
| 16 | +#include "ax/platform/ax_platform_node_win.h" |
| 17 | + |
| 18 | +namespace ui { |
| 19 | + |
| 20 | +class AX_EXPORT __declspec(uuid("3071e40d-a10d-45ff-a59f-6e8e1138e2c1")) |
| 21 | + AXPlatformNodeTextRangeProviderWin |
| 22 | + : public CComObjectRootEx<CComMultiThreadModel>, |
| 23 | + public ITextRangeProvider { |
| 24 | + public: |
| 25 | + BEGIN_COM_MAP(AXPlatformNodeTextRangeProviderWin) |
| 26 | + COM_INTERFACE_ENTRY(ITextRangeProvider) |
| 27 | + COM_INTERFACE_ENTRY(AXPlatformNodeTextRangeProviderWin) |
| 28 | + END_COM_MAP() |
| 29 | + |
| 30 | + AXPlatformNodeTextRangeProviderWin(); |
| 31 | + ~AXPlatformNodeTextRangeProviderWin(); |
| 32 | + |
| 33 | + static ITextRangeProvider* CreateTextRangeProvider( |
| 34 | + AXNodePosition::AXPositionInstance start, |
| 35 | + AXNodePosition::AXPositionInstance end); |
| 36 | + |
| 37 | + // Creates an instance of the class for unit tests, where AXPlatformNodes |
| 38 | + // cannot be queried automatically from endpoints. |
| 39 | + static ITextRangeProvider* CreateTextRangeProviderForTesting( |
| 40 | + AXPlatformNodeWin* owner, |
| 41 | + AXNodePosition::AXPositionInstance start, |
| 42 | + AXNodePosition::AXPositionInstance end); |
| 43 | + |
| 44 | + // |
| 45 | + // ITextRangeProvider methods. |
| 46 | + // |
| 47 | + |
| 48 | + IFACEMETHODIMP Clone(ITextRangeProvider** clone) override; |
| 49 | + IFACEMETHODIMP Compare(ITextRangeProvider* other, BOOL* result) override; |
| 50 | + IFACEMETHODIMP |
| 51 | + CompareEndpoints(TextPatternRangeEndpoint this_endpoint, |
| 52 | + ITextRangeProvider* other, |
| 53 | + TextPatternRangeEndpoint other_endpoint, |
| 54 | + int* result) override; |
| 55 | + IFACEMETHODIMP ExpandToEnclosingUnit(TextUnit unit) override; |
| 56 | + IFACEMETHODIMP |
| 57 | + FindAttribute(TEXTATTRIBUTEID attribute_id, |
| 58 | + VARIANT attribute_val, |
| 59 | + BOOL is_backward, |
| 60 | + ITextRangeProvider** result) override; |
| 61 | + IFACEMETHODIMP |
| 62 | + FindText(BSTR string, |
| 63 | + BOOL backwards, |
| 64 | + BOOL ignore_case, |
| 65 | + ITextRangeProvider** result) override; |
| 66 | + IFACEMETHODIMP GetAttributeValue(TEXTATTRIBUTEID attribute_id, |
| 67 | + VARIANT* value) override; |
| 68 | + IFACEMETHODIMP |
| 69 | + GetBoundingRectangles(SAFEARRAY** screen_physical_pixel_rectangles) override; |
| 70 | + IFACEMETHODIMP |
| 71 | + GetEnclosingElement(IRawElementProviderSimple** element) override; |
| 72 | + IFACEMETHODIMP GetText(int max_count, BSTR* text) override; |
| 73 | + IFACEMETHODIMP Move(TextUnit unit, int count, int* units_moved) override; |
| 74 | + IFACEMETHODIMP |
| 75 | + MoveEndpointByUnit(TextPatternRangeEndpoint endpoint, |
| 76 | + TextUnit unit, |
| 77 | + int count, |
| 78 | + int* units_moved) override; |
| 79 | + IFACEMETHODIMP |
| 80 | + MoveEndpointByRange(TextPatternRangeEndpoint this_endpoint, |
| 81 | + ITextRangeProvider* other, |
| 82 | + TextPatternRangeEndpoint other_endpoint) override; |
| 83 | + IFACEMETHODIMP Select() override; |
| 84 | + IFACEMETHODIMP AddToSelection() override; |
| 85 | + IFACEMETHODIMP RemoveFromSelection() override; |
| 86 | + IFACEMETHODIMP ScrollIntoView(BOOL align_to_top) override; |
| 87 | + IFACEMETHODIMP GetChildren(SAFEARRAY** children) override; |
| 88 | + |
| 89 | + AXPlatformNodeWin* GetOwner() const; |
| 90 | + void SetOwnerForTesting(AXPlatformNodeWin* owner); |
| 91 | + |
| 92 | + private: |
| 93 | + using AXPositionInstance = AXNodePosition::AXPositionInstance; |
| 94 | + using AXPositionInstanceType = typename AXPositionInstance::element_type; |
| 95 | + using AXNodeRange = AXRange<AXPositionInstanceType>; |
| 96 | + |
| 97 | + friend class AXPlatformNodeTextRangeProviderTest; |
| 98 | + friend class AXPlatformNodeTextProviderTest; |
| 99 | + friend class AXRangePhysicalPixelRectDelegate; |
| 100 | + |
| 101 | + static bool AtStartOfLinePredicate(const AXPositionInstance& position); |
| 102 | + static bool AtEndOfLinePredicate(const AXPositionInstance& position); |
| 103 | + |
| 104 | + static AXPositionInstance GetNextTextBoundaryPosition( |
| 105 | + const AXPositionInstance& position, |
| 106 | + ax::mojom::TextBoundary boundary_type, |
| 107 | + AXBoundaryBehavior options, |
| 108 | + ax::mojom::MoveDirection boundary_direction); |
| 109 | + |
| 110 | + // Prefer these *Impl methods when functionality is needed internally. We |
| 111 | + // should avoid calling external APIs internally as it will cause the |
| 112 | + // histograms to become innaccurate. |
| 113 | + HRESULT MoveEndpointByUnitImpl(TextPatternRangeEndpoint endpoint, |
| 114 | + TextUnit unit, |
| 115 | + int count, |
| 116 | + int* units_moved); |
| 117 | + |
| 118 | + IFACEMETHODIMP ExpandToEnclosingUnitImpl(TextUnit unit); |
| 119 | + |
| 120 | + std::u16string GetString(int max_count, |
| 121 | + size_t* appended_newlines_count = nullptr); |
| 122 | + const AXPositionInstance& start() const { return endpoints_.GetStart(); } |
| 123 | + const AXPositionInstance& end() const { return endpoints_.GetEnd(); } |
| 124 | + AXPlatformNodeDelegate* GetDelegate( |
| 125 | + const AXPositionInstanceType* position) const; |
| 126 | + AXPlatformNodeDelegate* GetDelegate(const AXTreeID tree_id, |
| 127 | + const AXNode::AXID node_id) const; |
| 128 | + |
| 129 | + template <typename AnchorIterator, typename ExpandMatchLambda> |
| 130 | + HRESULT FindAttributeRange(const TEXTATTRIBUTEID text_attribute_id, |
| 131 | + VARIANT attribute_val, |
| 132 | + const AnchorIterator first, |
| 133 | + const AnchorIterator last, |
| 134 | + ExpandMatchLambda expand_match); |
| 135 | + |
| 136 | + AXPositionInstance MoveEndpointByCharacter(const AXPositionInstance& endpoint, |
| 137 | + const int count, |
| 138 | + int* units_moved); |
| 139 | + AXPositionInstance MoveEndpointByWord(const AXPositionInstance& endpoint, |
| 140 | + const int count, |
| 141 | + int* units_moved); |
| 142 | + AXPositionInstance MoveEndpointByLine(const AXPositionInstance& endpoint, |
| 143 | + bool is_start_endpoint, |
| 144 | + const int count, |
| 145 | + int* units_moved); |
| 146 | + AXPositionInstance MoveEndpointByParagraph(const AXPositionInstance& endpoint, |
| 147 | + const bool is_start_endpoint, |
| 148 | + const int count, |
| 149 | + int* units_moved); |
| 150 | + AXPositionInstance MoveEndpointByPage(const AXPositionInstance& endpoint, |
| 151 | + const bool is_start_endpoint, |
| 152 | + const int count, |
| 153 | + int* units_moved); |
| 154 | + AXPositionInstance MoveEndpointByDocument(const AXPositionInstance& endpoint, |
| 155 | + const int count, |
| 156 | + int* units_moved); |
| 157 | + |
| 158 | + AXPositionInstance MoveEndpointByUnitHelper( |
| 159 | + const AXPositionInstance& endpoint, |
| 160 | + const ax::mojom::TextBoundary boundary_type, |
| 161 | + const int count, |
| 162 | + int* units_moved); |
| 163 | + |
| 164 | + // A text range normalization is necessary to prevent a |start_| endpoint to |
| 165 | + // be positioned at the end of an anchor when it can be at the start of the |
| 166 | + // next anchor. After normalization, it is guaranteed that: |
| 167 | + // * both endpoints passed by parameter are always positioned on unignored |
| 168 | + // anchors; |
| 169 | + // * both endpoints passed by parameter are never between a grapheme cluster; |
| 170 | + // * if the endpoints passed by parameter create a degenerate range, both |
| 171 | + // endpoints are on the same anchor. |
| 172 | + // Normalization never updates the internal endpoints directly. Instead, it |
| 173 | + // normalizes the endpoints passed by parameter. |
| 174 | + void NormalizeTextRange(AXPositionInstance& start, AXPositionInstance& end); |
| 175 | + static void NormalizeAsUnignoredPosition(AXPositionInstance& position); |
| 176 | + static void NormalizeAsUnignoredTextRange(AXPositionInstance& start, |
| 177 | + AXPositionInstance& end); |
| 178 | + |
| 179 | + AXPlatformNodeDelegate* GetRootDelegate(const ui::AXTreeID tree_id); |
| 180 | + AXNode* GetSelectionCommonAnchor(); |
| 181 | + void RemoveFocusFromPreviousSelectionIfNeeded( |
| 182 | + const AXNodeRange& new_selection); |
| 183 | + AXPlatformNodeWin* GetPlatformNodeFromAXNode(const AXNode* node) const; |
| 184 | + AXPlatformNodeWin* GetLowestAccessibleCommonPlatformNode() const; |
| 185 | + bool HasTextRangeOrSelectionInAtomicTextField( |
| 186 | + const AXPositionInstance& start_position, |
| 187 | + const AXPositionInstance& end_position) const; |
| 188 | + |
| 189 | + void SetStart(AXPositionInstance start); |
| 190 | + void SetEnd(AXPositionInstance end); |
| 191 | + |
| 192 | + static bool TextAttributeIsArrayType(TEXTATTRIBUTEID attribute_id); |
| 193 | + static bool TextAttributeIsUiaReservedValue( |
| 194 | + const base::win::VariantVector& vector); |
| 195 | + static bool ShouldReleaseTextAttributeAsSafearray( |
| 196 | + TEXTATTRIBUTEID attribute_id, |
| 197 | + const base::win::VariantVector& vector); |
| 198 | + |
| 199 | + Microsoft::WRL::ComPtr<AXPlatformNodeWin> owner_for_test_; |
| 200 | + |
| 201 | + // The TextRangeEndpoints class has the responsibility of keeping the |
| 202 | + // endpoints of the range valid or nullify them when it can't find a valid |
| 203 | + // alternative. |
| 204 | + // |
| 205 | + // An endpoint can become invalid when |
| 206 | + // A. the node it's on gets deleted, |
| 207 | + // B. when an ancestor node gets deleted, deleting the subtree our endpoint |
| 208 | + // is on, or |
| 209 | + // C. when a descendant node gets deleted, potentially rendering the |
| 210 | + // position invalid due to a smaller MaxTextOffset value (for a text |
| 211 | + // position) or fewer child nodes (for a tree position). |
| 212 | + // |
| 213 | + // In all cases, our approach to resolve the endpoints to valid positions |
| 214 | + // takes two steps: |
| 215 | + // 1. Move the endpoint to an equivalent ancestor position before the node |
| 216 | + // gets deleted - we can't move the position once the node it's on is |
| 217 | + // deleted since this position would already be considered invalid. |
| 218 | + // 2. Call AsValidPosition on that new position once the node is deleted - |
| 219 | + // calling this function before the node gets deleted wouldn't do much |
| 220 | + // since our position would still be considered valid at this point. |
| 221 | + // |
| 222 | + // Because AsValidPosition can potentially be expensive, we only want to run |
| 223 | + // it when necessary. For this reason, we store the node ID and tree ID that |
| 224 | + // causes the first step to happen and only run the second step in |
| 225 | + // OnNodeDeleted for the corresponding node deletion. When OnNodeDeleted is |
| 226 | + // called, the |start_| and |end_| endpoints have already been moved up to an |
| 227 | + // ancestor that is still part of the tree. This is to ensure that we don't |
| 228 | + // have to read the node/tree structure of the deleted node in that function - |
| 229 | + // which would likely result in a crash. |
| 230 | + // |
| 231 | + // Both scenarios A and B are fixed by this approach (by the implementation of |
| 232 | + // OnSubtreeWillBeDeleted), but we still have work to do to fix scenario C. |
| 233 | + // This case, in theory, would only require the second step to ensure that the |
| 234 | + // position is always valid but computing whether node is part of the subtree |
| 235 | + // of the endpoint we're on would be very expensive. Furthermore, because the |
| 236 | + // endpoints are generally on leaf nodes, the scenario is unlikely - we |
| 237 | + // haven't heard of issues caused by this scenario yet. Eventually, we might |
| 238 | + // be able to scope the fix to specific use cases, like when the range is on |
| 239 | + // UIA embedded object (e.g. button, select, etc.) |
| 240 | + // |
| 241 | + // *** |
| 242 | + // |
| 243 | + // Why we can't use a ScopedObserver here: |
| 244 | + // We tried using a ScopedObserver instead of a simple observer in this case, |
| 245 | + // but there appears to be a problem with the lifetime of the referenced |
| 246 | + // AXTreeManager in the ScopedObserver. The AXTreeManager can get deleted |
| 247 | + // before the TextRangeEndpoints does, so when the destructor of the |
| 248 | + // ScopedObserver calls ScopedObserver::RemoveAll on an already deleted |
| 249 | + // AXTreeManager, it crashes. |
| 250 | + class TextRangeEndpoints : public AXTreeObserver { |
| 251 | + public: |
| 252 | + TextRangeEndpoints(); |
| 253 | + ~TextRangeEndpoints() override; |
| 254 | + const AXPositionInstance& GetStart() const { return start_; } |
| 255 | + const AXPositionInstance& GetEnd() const { return end_; } |
| 256 | + void SetStart(AXPositionInstance new_start); |
| 257 | + void SetEnd(AXPositionInstance new_end); |
| 258 | + |
| 259 | + void AddObserver(const AXTreeID tree_id); |
| 260 | + void RemoveObserver(const AXTreeID tree_id); |
| 261 | + void OnSubtreeWillBeDeleted(AXTree* tree, AXNode* node) override; |
| 262 | + void OnNodeDeleted(AXTree* tree, AXNode::AXID node_id) override; |
| 263 | + |
| 264 | + private: |
| 265 | + struct DeletionOfInterest { |
| 266 | + AXTreeID tree_id; |
| 267 | + AXNode::AXID node_id; |
| 268 | + }; |
| 269 | + |
| 270 | + void AdjustEndpointForSubtreeDeletion(AXTree* tree, |
| 271 | + const AXNode* const node, |
| 272 | + bool is_start_endpoint); |
| 273 | + |
| 274 | + AXPositionInstance start_; |
| 275 | + AXPositionInstance end_; |
| 276 | + |
| 277 | + std::optional<DeletionOfInterest> validation_necessary_for_start_; |
| 278 | + std::optional<DeletionOfInterest> validation_necessary_for_end_; |
| 279 | + }; |
| 280 | + TextRangeEndpoints endpoints_; |
| 281 | +}; |
| 282 | + |
| 283 | +} // namespace ui |
| 284 | + |
| 285 | +#endif // UI_ACCESSIBILITY_PLATFORM_AX_PLATFORM_NODE_TEXTRANGEPROVIDER_WIN_H_ |
0 commit comments