chromium/ui/accessibility/platform/ax_platform_node_textprovider_win.cc

// Copyright 2019 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/accessibility/platform/ax_platform_node_textprovider_win.h"

#include <utility>

#include "base/win/scoped_safearray.h"
#include "ui/accessibility/ax_node_position.h"
#include "ui/accessibility/ax_selection.h"
#include "ui/accessibility/platform/ax_platform_node_base.h"
#include "ui/accessibility/platform/ax_platform_node_delegate.h"
#include "ui/accessibility/platform/ax_platform_node_textrangeprovider_win.h"

#define UIA_VALIDATE_TEXTPROVIDER_CALL() \
  if (!owner()->GetDelegate())           \
    return UIA_E_ELEMENTNOTAVAILABLE;
#define UIA_VALIDATE_TEXTPROVIDER_CALL_1_ARG(arg) \
  if (!owner()->GetDelegate())                    \
    return UIA_E_ELEMENTNOTAVAILABLE;             \
  if (!arg)                                       \
    return E_INVALIDARG;

namespace ui {

AXPlatformNodeTextProviderWin::AXPlatformNodeTextProviderWin() {
  DVLOG(1) << __func__;
}

AXPlatformNodeTextProviderWin::~AXPlatformNodeTextProviderWin() {}

// static
AXPlatformNodeTextProviderWin* AXPlatformNodeTextProviderWin::Create(
    AXPlatformNodeWin* owner) {
  CComObject<AXPlatformNodeTextProviderWin>* text_provider = nullptr;
  if (SUCCEEDED(CComObject<AXPlatformNodeTextProviderWin>::CreateInstance(
          &text_provider))) {
    DCHECK(text_provider);
    text_provider->owner_ = owner;
    text_provider->AddRef();
    return text_provider;
  }

  return nullptr;
}

// static
void AXPlatformNodeTextProviderWin::CreateIUnknown(AXPlatformNodeWin* owner,
                                                   IUnknown** unknown) {
  Microsoft::WRL::ComPtr<AXPlatformNodeTextProviderWin> text_provider(
      Create(owner));
  if (text_provider)
    *unknown = text_provider.Detach();
}

//
// ITextProvider methods.
//

HRESULT AXPlatformNodeTextProviderWin::GetSelection(SAFEARRAY** selection) {
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GETSELECTION);
  UIA_VALIDATE_TEXTPROVIDER_CALL();

  *selection = nullptr;

  AXPlatformNodeDelegate* delegate = owner()->GetDelegate();
  AXSelection unignored_selection = delegate->GetUnignoredSelection();

  AXPlatformNode* anchor_object =
      delegate->GetFromNodeID(unignored_selection.anchor_object_id);
  AXPlatformNode* focus_object =
      delegate->GetFromNodeID(unignored_selection.focus_object_id);

  // anchor_offset corresponds to the selection start index
  // and focus_offset is where the selection ends.
  auto start_offset = unignored_selection.anchor_offset;
  auto end_offset = unignored_selection.focus_offset;

  // If there's no selected object, return success and don't fill the SAFEARRAY.
  if (!anchor_object || !focus_object)
    return S_OK;

  AXNodePosition::AXPositionInstance start =
      anchor_object->GetDelegate()->CreatePositionAt(start_offset);
  AXNodePosition::AXPositionInstance end =
      focus_object->GetDelegate()->CreatePositionAt(end_offset);

  DCHECK(!start->IsNullPosition());
  DCHECK(!end->IsNullPosition());

  start->SnapToMaxTextOffsetIfBeyond();
  end->SnapToMaxTextOffsetIfBeyond();
  if (*start > *end) {
    std::swap(start, end);
  }

  Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider;
  AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
      std::move(start), std::move(end), &text_range_provider);
  if (&text_range_provider == nullptr)
    return E_OUTOFMEMORY;

  // Since we don't support disjoint text ranges, the SAFEARRAY returned
  // will always have one element
  base::win::ScopedSafearray selections_to_return(
      SafeArrayCreateVector(/* element type */ VT_UNKNOWN, /* lower bound */ 0,
                            /* number of elements */ 1));

  if (!selections_to_return.Get())
    return E_OUTOFMEMORY;

  LONG index = 0;
  HRESULT hr = SafeArrayPutElement(selections_to_return.Get(), &index,
                                   text_range_provider.Get());
  DCHECK(SUCCEEDED(hr));

  // Since DCHECK only happens in debug builds, return immediately to ensure
  // that we're not leaking the SAFEARRAY on release builds.
  if (FAILED(hr))
    return E_FAIL;

  *selection = selections_to_return.Release();

  return S_OK;
}

HRESULT AXPlatformNodeTextProviderWin::GetVisibleRanges(
    SAFEARRAY** visible_ranges) {
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GETVISIBLERANGES);
  UIA_VALIDATE_TEXTPROVIDER_CALL();

  // Whether we expose embedded object characters for nodes is managed by the
  // |g_ax_embedded_object_behavior| global variable set in ax_node_position.cc.
  // When on Windows, this variable is always set to
  // kExposeCharacterForHypertext... which is incorrect if we run UIA-specific
  // code relating to computing text content of nodes that themselves do not
  // have text, such as `<p>` elements. To avoid problems caused by that, we use
  // the following ScopedAXEmbeddedObjectBehaviorSetter to modify the value of
  // the global variable to what is really expected on UIA.

  ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior(
      AXEmbeddedObjectBehavior::kSuppressCharacter);
  const AXPlatformNodeDelegate* delegate = owner()->GetDelegate();

  // Get the Clipped Frame Bounds of the current node, not from the root,
  // so if this node is wrapped with overflow styles it will have the
  // correct bounds
  const gfx::Rect frame_rect = delegate->GetBoundsRect(
      AXCoordinateSystem::kFrame, AXClippingBehavior::kClipped);

  const auto start = delegate->CreateTextPositionAt(0);
  const auto end = start->CreatePositionAtEndOfAnchor();
  DCHECK(start->GetAnchor() == end->GetAnchor());

  // SAFEARRAYs are not dynamic, so fill the visible ranges in a vector
  // and then transfer to an appropriately-sized SAFEARRAY
  std::vector<Microsoft::WRL::ComPtr<ITextRangeProvider>> ranges;

  auto current_line_start = start->Clone();
  while (!current_line_start->IsNullPosition() && *current_line_start < *end) {
    auto current_line_end = current_line_start->CreateNextLineEndPosition(
        {AXBoundaryBehavior::kCrossBoundary,
         AXBoundaryDetection::kDontCheckInitialPosition});
    if (current_line_end->IsNullPosition() || *current_line_end > *end)
      current_line_end = end->Clone();

    gfx::Rect current_rect = delegate->GetInnerTextRangeBoundsRect(
        current_line_start->text_offset(), current_line_end->text_offset(),
        AXCoordinateSystem::kFrame, AXClippingBehavior::kUnclipped);

    // There are scenarios where the text range bounds might be slightly outside
    // the container bounds, so we check if the bounding rects intersect rather
    // than if it is only contained within.
    if (frame_rect.Intersects(current_rect)) {
      Microsoft::WRL::ComPtr<ITextRangeProvider> text_range_provider;
      AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
          current_line_start->AsLeafTextPosition(),
          current_line_end->AsLeafTextPosition(),
          &text_range_provider);

      ranges.emplace_back(text_range_provider);
    }

    current_line_start = current_line_start->CreateNextLineStartPosition(
        {AXBoundaryBehavior::kCrossBoundary,
         AXBoundaryDetection::kDontCheckInitialPosition});
  }

  base::win::ScopedSafearray scoped_visible_ranges(
      SafeArrayCreateVector(/* element type */ VT_UNKNOWN, /* lower bound */ 0,
                            /* number of elements */ ranges.size()));

  if (!scoped_visible_ranges.Get())
    return E_OUTOFMEMORY;

  LONG index = 0;
  for (Microsoft::WRL::ComPtr<ITextRangeProvider>& current_provider : ranges) {
    HRESULT hr = SafeArrayPutElement(scoped_visible_ranges.Get(), &index,
                                     current_provider.Get());
    DCHECK(SUCCEEDED(hr));

    // Since DCHECK only happens in debug builds, return immediately to ensure
    // that we're not leaking the SAFEARRAY on release builds.
    if (FAILED(hr))
      return E_FAIL;

    ++index;
  }

  *visible_ranges = scoped_visible_ranges.Release();

  return S_OK;
}

HRESULT AXPlatformNodeTextProviderWin::RangeFromChild(
    IRawElementProviderSimple* child,
    ITextRangeProvider** range) {
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_RANGEFROMCHILD);
  UIA_VALIDATE_TEXTPROVIDER_CALL_1_ARG(child);

  *range = nullptr;

  Microsoft::WRL::ComPtr<AXPlatformNodeWin> child_platform_node;
  if (!SUCCEEDED(child->QueryInterface(IID_PPV_ARGS(&child_platform_node))))
    return UIA_E_INVALIDOPERATION;

  if (!owner()->IsDescendant(child_platform_node.Get()))
    return E_INVALIDARG;

  GetRangeFromChild(owner(), child_platform_node.Get(), range);

  return S_OK;
}

HRESULT AXPlatformNodeTextProviderWin::RangeFromPoint(
    UiaPoint uia_point,
    ITextRangeProvider** range) {
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_RANGEFROMPOINT);
  WIN_ACCESSIBILITY_API_PERF_HISTOGRAM(UMA_API_TEXT_RANGEFROMPOINT);
  UIA_VALIDATE_TEXTPROVIDER_CALL();
  *range = nullptr;

  gfx::Point point(uia_point.x, uia_point.y);
  // Retrieve the closest accessibility node. No coordinate unit conversion is
  // needed, hit testing input is also in screen coordinates.

  AXPlatformNodeWin* nearest_node =
      static_cast<AXPlatformNodeWin*>(owner()->NearestLeafToPoint(point));
  DCHECK(nearest_node);
  DCHECK(nearest_node->IsLeaf());

  AXNodePosition::AXPositionInstance start, end;
  start = nearest_node->GetDelegate()->CreateTextPositionAt(
      nearest_node->NearestTextIndexToPoint(point));
  DCHECK(!start->IsNullPosition());
  end = start->Clone();

  AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
      std::move(start), std::move(end), range);

  return S_OK;
}

HRESULT AXPlatformNodeTextProviderWin::get_DocumentRange(
    ITextRangeProvider** range) {
  ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior(
      AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent);
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GET_DOCUMENTRANGE);
  UIA_VALIDATE_TEXTPROVIDER_CALL();

  // Get range from child, where child is the current node. In other words,
  // getting the text range of the current owner AxPlatformNodeWin node.
  GetRangeFromChild(owner(), owner(), range);
  return S_OK;
}

HRESULT AXPlatformNodeTextProviderWin::get_SupportedTextSelection(
    enum SupportedTextSelection* text_selection) {
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXT_GET_SUPPORTEDTEXTSELECTION);
  UIA_VALIDATE_TEXTPROVIDER_CALL();

  *text_selection = SupportedTextSelection_Single;
  return S_OK;
}

//
// ITextEditProvider methods.
//

HRESULT AXPlatformNodeTextProviderWin::GetActiveComposition(
    ITextRangeProvider** range) {
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTEDIT_GETACTIVECOMPOSITION);
  UIA_VALIDATE_TEXTPROVIDER_CALL();

  *range = nullptr;
  return GetTextRangeProviderFromActiveComposition(range);
}

HRESULT AXPlatformNodeTextProviderWin::GetConversionTarget(
    ITextRangeProvider** range) {
  WIN_ACCESSIBILITY_API_HISTOGRAM(UMA_API_TEXTEDIT_GETCONVERSIONTARGET);
  UIA_VALIDATE_TEXTPROVIDER_CALL();

  *range = nullptr;
  return GetTextRangeProviderFromActiveComposition(range);
}

void AXPlatformNodeTextProviderWin::GetRangeFromChild(
    AXPlatformNodeWin* ancestor,
    AXPlatformNodeWin* descendant,
    ITextRangeProvider** range) {
  DCHECK(ancestor);
  DCHECK(descendant);
  DCHECK(descendant->GetDelegate());
  DCHECK(ancestor->IsDescendant(descendant));
  ScopedAXEmbeddedObjectBehaviorSetter ax_embedded_object_behavior(
      AXEmbeddedObjectBehavior::kUIAExposeCharacterForTextContent);

  // Start and end should be leaf text positions that span the beginning and end
  // of text content within a node. The start position should be the directly
  // first child and the end position should be the deepest last child node.
  AXNodePosition::AXPositionInstance start =
      descendant->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition();

  AXNodePosition::AXPositionInstance end;
  if (descendant->IsPlatformDocument()) {
    // Fast path for getting the range of the web or PDF root.
    // If the last position is ignored, we need to get an unignored position
    // otherwise future comparisons can end up with null positions (which in
    // turn might collapse the range). Note that we move backwards, since there
    // is no position after the end-of-content position (i.e. moving forward
    // results in a null position).
    end = start->CreatePositionAtEndOfContent()->AsUnignoredPosition(
        AXPositionAdjustmentBehavior::kMoveBackward);
  } else if (descendant->GetChildCount() == 0) {
    end = descendant->GetDelegate()
              ->CreateTextPositionAt(0)
              ->CreatePositionAtEndOfAnchor()
              ->AsLeafTextPosition();
  } else {
    AXPlatformNodeBase* deepest_last_child = descendant->GetLastChild();
    while (deepest_last_child && deepest_last_child->GetChildCount() > 0)
      deepest_last_child = deepest_last_child->GetLastChild();

    end = deepest_last_child->GetDelegate()
              ->CreateTextPositionAt(0)
              ->CreatePositionAtEndOfAnchor()
              ->AsLeafTextPosition();
  }

  AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
      std::move(start), std::move(end), range);
}

void AXPlatformNodeTextProviderWin::CreateDegenerateRangeAtStart(
    AXPlatformNodeWin* node,
    ITextRangeProvider** text_range_provider) {
  DCHECK(node);
  DCHECK(node->GetDelegate());

  // Create a degenerate range positioned at the node's start.
  AXNodePosition::AXPositionInstance start, end;
  start = node->GetDelegate()->CreateTextPositionAt(0)->AsLeafTextPosition();
  end = start->Clone();
  AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
      std::move(start), std::move(end), text_range_provider);
}

AXPlatformNodeWin* AXPlatformNodeTextProviderWin::owner() const {
  return owner_.Get();
}

HRESULT
AXPlatformNodeTextProviderWin::GetTextRangeProviderFromActiveComposition(
    ITextRangeProvider** range) {
  *range = nullptr;
  // We fetch the start and end offset of an active composition only if
  // this object has focus and TSF is in composition mode.
  // The offsets here refer to the character positions in a plain text
  // view of the DOM tree. Ex: if the active composition in an element
  // has "abc" then the range will be (0,3) in both TSF and accessibility
  if ((AXPlatformNode::FromNativeViewAccessible(
           owner()->GetDelegate()->GetFocus()) ==
       static_cast<AXPlatformNode*>(owner())) &&
      owner()->HasActiveComposition()) {
    gfx::Range active_composition_offset =
        owner()->GetActiveCompositionOffsets();
    AXNodePosition::AXPositionInstance start =
        owner()->GetDelegate()->CreateTextPositionAt(
            /*offset*/ active_composition_offset.start());
    AXNodePosition::AXPositionInstance end =
        owner()->GetDelegate()->CreateTextPositionAt(
            /*offset*/ active_composition_offset.end());

    AXPlatformNodeTextRangeProviderWin::CreateTextRangeProvider(
        std::move(start), std::move(end), range);
  }

  return S_OK;
}

}  // namespace ui