Files
tubestation/accessible/generic/HyperTextAccessible.cpp
James Teh 7932ffe32b Bug 1963996: Clear the selection if IAccessibleTextSelectionContainer::setSelections is called with nSelections set to 0. r=eeejay
I removed this behaviour in bug 1951573.
Unfortunately, it turns out that contrary to the spec (which says "1 or more selections"), NVDA depends on this behaviour.
Reinstate this by supporting kRemoveAllExistingSelectedRanges in HyperTextAccessibleBase::RemoveFromSelection and using that.

Differential Revision: https://phabricator.services.mozilla.com/D247875
2025-05-08 06:52:14 +00:00

1066 lines
38 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=2 sw=2 et tw=78: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
#include "HyperTextAccessible-inl.h"
#include "nsAccessibilityService.h"
#include "nsIAccessibleTypes.h"
#include "AccAttributes.h"
#include "HTMLListAccessible.h"
#include "LocalAccessible-inl.h"
#include "Relation.h"
#include "mozilla/a11y/Role.h"
#include "States.h"
#include "TextAttrs.h"
#include "TextLeafRange.h"
#include "TextRange.h"
#include "TreeWalker.h"
#include "nsCaret.h"
#include "nsContentUtils.h"
#include "nsDebug.h"
#include "nsFocusManager.h"
#include "nsIEditingSession.h"
#include "nsContainerFrame.h"
#include "nsFrameSelection.h"
#include "nsILineIterator.h"
#include "nsIMathMLFrame.h"
#include "nsLayoutUtils.h"
#include "nsRange.h"
#include "mozilla/Assertions.h"
#include "mozilla/EditorBase.h"
#include "mozilla/HTMLEditor.h"
#include "mozilla/IntegerRange.h"
#include "mozilla/PresShell.h"
#include "mozilla/ScrollContainerFrame.h"
#include "mozilla/SelectionMovementUtils.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/HTMLBRElement.h"
#include "mozilla/dom/Selection.h"
#include "gfxSkipChars.h"
using namespace mozilla;
using namespace mozilla::a11y;
////////////////////////////////////////////////////////////////////////////////
// HyperTextAccessible
////////////////////////////////////////////////////////////////////////////////
HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
: AccessibleWrap(aNode, aDoc) {
mType = eHyperTextType;
mGenericTypes |= eHyperText;
}
role HyperTextAccessible::NativeRole() const {
a11y::role r = GetAccService()->MarkupRole(mContent);
if (r != roles::NOTHING) return r;
nsIFrame* frame = GetFrame();
if (frame && frame->IsInlineFrame()) return roles::TEXT;
return roles::TEXT_CONTAINER;
}
uint64_t HyperTextAccessible::NativeState() const {
uint64_t states = AccessibleWrap::NativeState();
if (IsEditable()) {
states |= states::EDITABLE;
} else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
// We want <article> to behave like a document in terms of readonly state.
states |= states::READONLY;
}
nsIFrame* frame = GetFrame();
if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) {
// If the accessible is editable the layout selectable state only disables
// mouse selection, but keyboard (shift+arrow) selection is still possible.
states |= states::SELECTABLE_TEXT;
}
return states;
}
bool HyperTextAccessible::IsEditable() const {
if (!mContent) {
return false;
}
return mContent->AsElement()->State().HasState(dom::ElementState::READWRITE);
}
uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
int32_t aNodeOffset,
bool aIsEndOffset) const {
if (!aNode) return 0;
uint32_t offset = 0;
nsINode* findNode = nullptr;
if (aNodeOffset == -1) {
findNode = aNode;
} else if (aNode->IsText()) {
// For text nodes, aNodeOffset comes in as a character offset
// Text offset will be added at the end, if we find the offset in this
// hypertext We want the "skipped" offset into the text (rendered text
// without the extra whitespace)
nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
NS_ENSURE_TRUE(frame, 0);
nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
NS_ENSURE_SUCCESS(rv, 0);
findNode = aNode;
} else {
// findNode could be null if aNodeOffset == # of child nodes, which means
// one of two things:
// 1) there are no children, and the passed-in node is not mContent -- use
// parentContent for the node to find
// 2) there are no children and the passed-in node is mContent, which means
// we're an empty nsIAccessibleText
// 3) there are children and we're at the end of the children
findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
if (!findNode) {
if (aNodeOffset == 0) {
if (aNode == GetNode()) {
// Case #1: this accessible has no children and thus has empty text,
// we can only be at hypertext offset 0.
return 0;
}
// Case #2: there are no children, we're at this node.
findNode = aNode;
} else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
// Case #3: we're after the last child, get next node to this one.
for (nsINode* tmpNode = aNode;
!findNode && tmpNode && tmpNode != mContent;
tmpNode = tmpNode->GetParent()) {
findNode = tmpNode->GetNextSibling();
}
}
}
}
// Get accessible for this findNode, or if that node isn't accessible, use the
// accessible for the next DOM node which has one (based on forward depth
// first search)
LocalAccessible* descendant = nullptr;
if (findNode) {
dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
if (brElement && brElement->IsPaddingForEmptyEditor()) {
// This <br> is the hacky "padding <br> element" used when there is no
// text in the editor.
return 0;
}
descendant = mDoc->GetAccessible(findNode);
if (!descendant && findNode->IsContent()) {
LocalAccessible* container = mDoc->GetContainerAccessible(findNode);
if (container) {
TreeWalker walker(container, findNode->AsContent(),
TreeWalker::eWalkContextTree);
descendant = walker.Next();
if (!descendant) descendant = container;
}
}
}
if (!descendant) {
// This DOM point can't be mapped to an offset in this HyperTextAccessible.
// Return the length as a fallback.
return CharacterCount();
}
if (aNode->IsText() && descendant->GetContent() != aNode) {
// `offset` is relative to aNode, but aNode doesn't have an Accessible, so
// we used the next Accessible. This means that `offset` is no longer valid.
NS_WARNING("No Accessible for DOM text node");
offset = 0;
} else if (descendant->IsTextLeaf()) {
uint32_t length = nsAccUtils::TextLength(descendant);
if (offset > length) {
// This can happen if text in the accessibility tree is out of date with
// DOM, since the accessibility engine updates text asynchronously. This
// should only be the case for a very short time, so it shouldn't be a
// real problem.
NS_WARNING("Offset too large for text leaf");
offset = length;
}
}
return TransformOffset(descendant, offset, aIsEndOffset);
}
uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant,
uint32_t aOffset,
bool aIsEndOffset) const {
// From the descendant, go up and get the immediate child of this hypertext.
uint32_t offset = aOffset;
LocalAccessible* descendant = aDescendant;
while (descendant) {
LocalAccessible* parent = descendant->LocalParent();
if (parent == this) return GetChildOffset(descendant) + offset;
// This offset no longer applies because the passed-in text object is not
// a child of the hypertext. This happens when there are nested hypertexts,
// e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
// to make it relative the hypertext.
// If the end offset is not supposed to be inclusive and the original point
// is not at 0 offset then the returned offset should be after an embedded
// character the original point belongs to.
if (aIsEndOffset) {
// Similar to our special casing in FindOffset, we add handling for
// bulleted lists here because PeekOffset returns the inner text node
// for a list when it should return the list bullet.
// We manually set the offset so the error doesn't propagate up.
if (offset == 0 && parent && parent->IsHTMLListItem() &&
descendant->LocalPrevSibling() &&
descendant->LocalPrevSibling() ==
parent->AsHTMLListItem()->Bullet()) {
offset = 0;
} else {
offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
}
} else {
offset = 0;
}
descendant = parent;
}
// If the given a11y point cannot be mapped into offset relative this
// hypertext offset then return length as fallback value.
return CharacterCount();
}
DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
// 0 offset is valid even if no children. In this case the associated editor
// is empty so return a DOM point for editor root element.
if (aOffset == 0) {
RefPtr<EditorBase> editorBase = GetEditor();
if (editorBase) {
if (editorBase->IsEmpty()) {
return DOMPoint(editorBase->GetRoot(), 0);
}
}
}
int32_t childIdx = GetChildIndexAtOffset(aOffset);
if (childIdx == -1) return DOMPoint();
LocalAccessible* child = LocalChildAt(childIdx);
int32_t innerOffset = aOffset - GetChildOffset(childIdx);
// A text leaf case.
if (child->IsTextLeaf()) {
// The point is inside the text node. This is always true for any text leaf
// except a last child one. See assertion below.
if (aOffset < GetChildOffset(childIdx + 1)) {
nsIContent* content = child->GetContent();
int32_t idx = 0;
if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
innerOffset, &idx))) {
return DOMPoint();
}
return DOMPoint(content, idx);
}
// Set the DOM point right after the text node.
MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
innerOffset = 1;
}
// Case of embedded object. The point is either before or after the element.
NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
nsINode* node = child->GetNode();
nsINode* parentNode = node->GetParentNode();
return parentNode ? DOMPoint(parentNode,
parentNode->ComputeIndexOf_Deprecated(node) +
innerOffset)
: DOMPoint();
}
already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() {
RefPtr<AccAttributes> attributes = new AccAttributes();
TextAttrsMgr textAttrsMgr(this);
textAttrsMgr.GetAttributes(attributes);
return attributes.forget();
}
void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) {
// Add MathML xmlroles based on the position inside the parent.
LocalAccessible* parent = LocalParent();
if (parent) {
switch (parent->Role()) {
case roles::MATHML_CELL:
case roles::MATHML_ENCLOSED:
case roles::MATHML_ERROR:
case roles::MATHML_MATH:
case roles::MATHML_ROW:
case roles::MATHML_SQUARE_ROOT:
case roles::MATHML_STYLE:
if (Role() == roles::MATHML_OPERATOR) {
// This is an operator inside an <mrow> (or an inferred <mrow>).
// See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
// XXX We should probably do something similar for MATHML_FENCED, but
// operators do not appear in the accessible tree. See bug 1175747.
nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
if (mathMLFrame) {
nsEmbellishData embellishData;
mathMLFrame->GetEmbellishData(embellishData);
if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
if (!LocalPrevSibling()) {
aAttributes->SetAttribute(nsGkAtoms::xmlroles,
nsGkAtoms::open_fence);
} else if (!LocalNextSibling()) {
aAttributes->SetAttribute(nsGkAtoms::xmlroles,
nsGkAtoms::close_fence);
}
}
if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
aAttributes->SetAttribute(nsGkAtoms::xmlroles,
nsGkAtoms::separator);
}
}
}
break;
case roles::MATHML_FRACTION:
aAttributes->SetAttribute(
nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator
: nsGkAtoms::denominator);
break;
case roles::MATHML_ROOT:
aAttributes->SetAttribute(
nsGkAtoms::xmlroles,
IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
break;
case roles::MATHML_SUB:
aAttributes->SetAttribute(
nsGkAtoms::xmlroles,
IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
break;
case roles::MATHML_SUP:
aAttributes->SetAttribute(
nsGkAtoms::xmlroles,
IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
break;
case roles::MATHML_SUB_SUP: {
int32_t index = IndexInParent();
aAttributes->SetAttribute(
nsGkAtoms::xmlroles,
index == 0
? nsGkAtoms::base
: (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
} break;
case roles::MATHML_UNDER:
aAttributes->SetAttribute(
nsGkAtoms::xmlroles,
IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
break;
case roles::MATHML_OVER:
aAttributes->SetAttribute(
nsGkAtoms::xmlroles,
IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
break;
case roles::MATHML_UNDER_OVER: {
int32_t index = IndexInParent();
aAttributes->SetAttribute(nsGkAtoms::xmlroles,
index == 0
? nsGkAtoms::base
: (index == 1 ? nsGkAtoms::underscript
: nsGkAtoms::overscript));
} break;
case roles::MATHML_MULTISCRIPTS: {
// Get the <multiscripts> base.
nsIContent* child;
bool baseFound = false;
for (child = parent->GetContent()->GetFirstChild(); child;
child = child->GetNextSibling()) {
if (child->IsMathMLElement()) {
baseFound = true;
break;
}
}
if (baseFound) {
nsIContent* content = GetContent();
if (child == content) {
// We are the base.
aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base);
} else {
// Browse the list of scripts to find us and determine our type.
bool postscript = true;
bool subscript = true;
for (child = child->GetNextSibling(); child;
child = child->GetNextSibling()) {
if (!child->IsMathMLElement()) continue;
if (child->IsMathMLElement(nsGkAtoms::mprescripts)) {
postscript = false;
subscript = true;
continue;
}
if (child == content) {
if (postscript) {
aAttributes->SetAttribute(nsGkAtoms::xmlroles,
subscript ? nsGkAtoms::subscript
: nsGkAtoms::superscript);
} else {
aAttributes->SetAttribute(nsGkAtoms::xmlroles,
subscript
? nsGkAtoms::presubscript
: nsGkAtoms::presuperscript);
}
break;
}
subscript = !subscript;
}
}
}
} break;
default:
break;
}
}
}
already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() {
RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
// 'formatting' attribute is deprecated, 'display' attribute should be
// instead.
nsIFrame* frame = GetFrame();
if (frame && frame->IsBlockFrame()) {
attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block);
}
if (FocusMgr()->IsFocused(this)) {
int32_t lineNumber = CaretLineNumber();
if (lineNumber >= 1) {
attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber);
}
}
if (HasOwnContent()) {
GetAccService()->MarkupAttributes(this, attributes);
if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
}
return attributes.forget();
}
int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
uint32_t aCoordType) {
nsIFrame* hyperFrame = GetFrame();
if (!hyperFrame) return -1;
LayoutDeviceIntPoint coords =
nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
nsPresContext* presContext = mDoc->PresContext();
nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
coords, presContext->AppUnitsPerDevPixel());
nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) {
return -1; // Not found
}
nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
coordsInAppUnits.y - frameScreenRect.Y());
// Go through the frames to check if each one has the point.
// When one does, add up the character offsets until we have a match
// We have an point in an accessible child of this, now we need to add up the
// offsets before it to what we already have
int32_t offset = 0;
uint32_t childCount = ChildCount();
for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
LocalAccessible* childAcc = mChildren[childIdx];
nsIFrame* primaryFrame = childAcc->GetFrame();
NS_ENSURE_TRUE(primaryFrame, -1);
nsIFrame* frame = primaryFrame;
while (frame) {
nsIContent* content = frame->GetContent();
NS_ENSURE_TRUE(content, -1);
nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
nsSize frameSize = frame->GetSize();
if (pointInFrame.x < frameSize.width &&
pointInFrame.y < frameSize.height) {
// Finished
if (frame->IsTextFrame()) {
nsIFrame::ContentOffsets contentOffsets =
frame->GetContentOffsetsFromPointExternal(
pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
if (contentOffsets.IsNull() || contentOffsets.content != content) {
return -1; // Not found
}
uint32_t addToOffset;
nsresult rv = ContentToRenderedOffset(
primaryFrame, contentOffsets.offset, &addToOffset);
NS_ENSURE_SUCCESS(rv, -1);
offset += addToOffset;
}
return offset;
}
frame = frame->GetNextContinuation();
}
offset += nsAccUtils::TextLength(childAcc);
}
return -1; // Not found
}
already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const {
if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
// If we're inside an editable container, then return that container's
// editor
LocalAccessible* ancestor = LocalParent();
while (ancestor) {
HyperTextAccessible* hyperText = ancestor->AsHyperText();
if (hyperText) {
// Recursion will stop at container doc because it has its own impl
// of GetEditor()
return hyperText->GetEditor();
}
ancestor = ancestor->LocalParent();
}
return nullptr;
}
nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
nsCOMPtr<nsIEditingSession> editingSession;
docShell->GetEditingSession(getter_AddRefs(editingSession));
if (!editingSession) return nullptr; // No editing session interface
dom::Document* docNode = mDoc->DocumentNode();
RefPtr<HTMLEditor> htmlEditor =
editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
return htmlEditor.forget();
}
/**
* =================== Caret & Selection ======================
*/
int32_t HyperTextAccessible::CaretOffset() const {
// Not focused focusable accessible except document accessible doesn't have
// a caret.
if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
(InteractiveState() & states::FOCUSABLE)) {
return -1;
}
// Check cached value.
int32_t caretOffset = -1;
HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
// Use cached value if it corresponds to this accessible.
if (caretOffset != -1) {
if (text == this) return caretOffset;
nsINode* textNode = text->GetNode();
// Ignore offset if cached accessible isn't a text leaf.
if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) {
return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
}
}
// No caret if the focused node is not inside this DOM node and this DOM node
// is not inside of focused node.
FocusManager::FocusDisposition focusDisp =
FocusMgr()->IsInOrContainsFocus(this);
if (focusDisp == FocusManager::eNone) return -1;
// Turn the focus node and offset of the selection into caret hypretext
// offset.
dom::Selection* domSel = DOMSelection();
NS_ENSURE_TRUE(domSel, -1);
nsINode* focusNode = domSel->GetFocusNode();
uint32_t focusOffset = domSel->FocusOffset();
// No caret if this DOM node is inside of focused node but the selection's
// focus point is not inside of this DOM node.
if (focusDisp == FocusManager::eContainedByFocus) {
nsINode* resultNode =
nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
nsINode* thisNode = GetNode();
if (resultNode != thisNode &&
!nsCoreUtils::IsAncestorOf(thisNode, resultNode)) {
return -1;
}
}
return DOMPointToOffset(focusNode, focusOffset);
}
LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
*aWidget = nullptr;
RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
bool isVisible = caret->IsVisible();
if (!isVisible) return LayoutDeviceIntRect();
nsRect rect;
nsIFrame* frame = caret->GetGeometry(&rect);
if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
PresShell* presShell = mDoc->PresShellPtr();
// Transform rect to be relative to the root frame.
nsIFrame* rootFrame = presShell->GetRootFrame();
rect = nsLayoutUtils::TransformFrameRectToAncestor(frame, rect, rootFrame);
// We need to inverse translate with the offset of the edge of the visual
// viewport from top edge of the layout viewport.
nsPoint viewportOffset = presShell->GetVisualViewportOffset() -
presShell->GetLayoutViewportOffset();
rect.MoveBy(-viewportOffset);
// We need to take into account a non-1 resolution set on the presshell.
// This happens with async pinch zooming. Here we scale the bounds before
// adding the screen-relative offset.
rect.ScaleRoundOut(presShell->GetResolution());
// Now we need to put the rect in absolute screen coords.
nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
rect.MoveBy(rootScreenRect.TopLeft());
// Finally, convert from app units.
auto caretRect = LayoutDeviceIntRect::FromAppUnitsToNearest(
rect, presShell->GetPresContext()->AppUnitsPerDevPixel());
// Correct for character size, so that caret always matches the size of
// the character. This is important for font size transitions, and is
// necessary because the Gecko caret uses the previous character's size as
// the user moves forward in the text by character.
int32_t caretOffset = CaretOffset();
if (NS_WARN_IF(caretOffset == -1)) {
// The caret offset will be -1 if this Accessible isn't focused. Note that
// the DOM node contaning the caret might be focused, but the Accessible
// might not be; e.g. due to an autocomplete popup suggestion having a11y
// focus.
return LayoutDeviceIntRect();
}
LayoutDeviceIntRect charRect = CharBounds(
caretOffset, nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
if (!charRect.IsEmpty()) {
caretRect.SetTopEdge(charRect.Y());
}
*aWidget = frame->GetNearestWidget();
return caretRect;
}
void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
nsTArray<nsRange*>* aRanges) {
if (IsDoc() && !AsDoc()->HasLoadState(DocAccessible::eTreeConstructed)) {
// Rarely, a client query can be handled after a DocAccessible is created
// but before the initial tree is constructed, since DoInitialUpdate happens
// during a refresh tick. In that case, there might be a DOM selection, but
// we can't use it. We will crash if we try due to mContent being null, etc.
// This should only happen in the parent process because we should never
// try to push the cache in a content process before the initial tree is
// constructed.
MOZ_ASSERT(XRE_IsParentProcess(), "Query before DoInitialUpdate");
return;
}
// Ignore selection if it is not visible.
RefPtr<nsFrameSelection> frameSelection = FrameSelection();
if (!frameSelection || frameSelection->GetDisplaySelection() <=
nsISelectionController::SELECTION_HIDDEN) {
return;
}
dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
if (!domSel) return;
nsINode* startNode = GetNode();
RefPtr<EditorBase> editorBase = GetEditor();
if (editorBase) {
startNode = editorBase->GetRoot();
}
if (!startNode) return;
uint32_t childCount = startNode->GetChildCount();
nsresult rv = domSel->GetDynamicRangesForIntervalArray(
startNode, 0, startNode, childCount, true, aRanges);
NS_ENSURE_SUCCESS_VOID(rv);
// Remove collapsed ranges
aRanges->RemoveElementsBy(
[](const auto& range) { return range->Collapsed(); });
}
int32_t HyperTextAccessible::SelectionCount() {
nsTArray<nsRange*> ranges;
GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
return ranges.Length();
}
bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
int32_t* aStartOffset,
int32_t* aEndOffset) {
*aStartOffset = *aEndOffset = 0;
nsTArray<nsRange*> ranges;
GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
uint32_t rangeCount = ranges.Length();
if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) {
return false;
}
nsRange* range = ranges[aSelectionNum];
// Make sure start is before end, by swapping DOM points. This occurs when
// the user selects backwards in the text.
const Maybe<int32_t> order =
nsContentUtils::ComparePoints(range->EndRef(), range->StartRef());
if (!order) {
MOZ_ASSERT_UNREACHABLE();
return false;
}
const RangeBoundary& precedingBoundary =
*order < 0 ? range->EndRef() : range->StartRef();
const RangeBoundary& followingBoundary =
*order < 0 ? range->StartRef() : range->EndRef();
if (!precedingBoundary.GetContainer()->IsInclusiveDescendantOf(mContent)) {
*aStartOffset = 0;
} else {
*aStartOffset = DOMPointToOffset(
precedingBoundary.GetContainer(),
AssertedCast<int32_t>(*precedingBoundary.Offset(
RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)));
}
if (!followingBoundary.GetContainer()->IsInclusiveDescendantOf(mContent)) {
*aEndOffset = CharacterCount();
} else {
*aEndOffset = DOMPointToOffset(
followingBoundary.GetContainer(),
AssertedCast<int32_t>(*followingBoundary.Offset(
RangeBoundary::OffsetFilter::kValidOrInvalidOffsets)),
true);
}
return true;
}
bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
RefPtr<dom::Selection> domSel = DOMSelection();
if (!domSel) return false;
if (aSelectionNum == TextLeafRange::kRemoveAllExistingSelectedRanges) {
domSel->RemoveAllRanges(IgnoreErrors());
return true;
}
if (aSelectionNum < 0 ||
aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) {
return false;
}
const RefPtr<nsRange> range{
domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))};
domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
IgnoreErrors());
return true;
}
void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
int32_t aEndOffset,
uint32_t aCoordinateType,
int32_t aX, int32_t aY) {
nsIFrame* frame = GetFrame();
if (!frame) return;
LayoutDeviceIntPoint coords =
nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
RefPtr<nsRange> domRange = nsRange::Create(mContent);
TextRange range(this, this, aStartOffset, this, aEndOffset);
if (!range.AssignDOMRange(domRange)) {
return;
}
nsPresContext* presContext = frame->PresContext();
nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
coords, presContext->AppUnitsPerDevPixel());
bool initialScrolled = false;
nsIFrame* parentFrame = frame;
while ((parentFrame = parentFrame->GetParent())) {
if (parentFrame->IsScrollContainerOrSubclass()) {
if (!initialScrolled) {
// Scroll substring to the given point. Turn the point into percents
// relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
nsSize size(parentFrame->GetSize());
// avoid divide by zero
size.width = size.width ? size.width : 1;
size.height = size.height ? size.height : 1;
int16_t hPercent = offsetPointX * 100 / size.width;
int16_t vPercent = offsetPointY * 100 / size.height;
nsresult rv = nsCoreUtils::ScrollSubstringTo(
frame, domRange,
ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always),
ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always));
if (NS_FAILED(rv)) return;
initialScrolled = true;
} else {
// Substring was scrolled to the given point already inside its closest
// scrollable area. If there are nested scrollable areas then make
// sure we scroll lower areas to the given point inside currently
// traversed scrollable area.
nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
}
}
frame = parentFrame;
}
}
void HyperTextAccessible::SelectionRanges(
nsTArray<a11y::TextRange>* aRanges) const {
dom::Selection* sel = DOMSelection();
if (!sel) {
return;
}
TextRange::TextRangesFromSelection(sel, aRanges);
}
void HyperTextAccessible::ReplaceText(const nsAString& aText) {
if (aText.Length() == 0) {
DeleteText(0, CharacterCount());
return;
}
SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges, 0,
CharacterCount());
RefPtr<EditorBase> editorBase = GetEditor();
if (!editorBase) {
return;
}
DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text");
}
void HyperTextAccessible::InsertText(const nsAString& aText,
int32_t aPosition) {
RefPtr<EditorBase> editorBase = GetEditor();
if (editorBase) {
SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges,
aPosition, aPosition);
DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the text");
}
}
void HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) {
RefPtr<EditorBase> editorBase = GetEditor();
if (editorBase) {
SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges,
aStartPos, aEndPos);
editorBase->Copy();
}
}
void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) {
RefPtr<EditorBase> editorBase = GetEditor();
if (editorBase) {
SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges,
aStartPos, aEndPos);
editorBase->Cut();
}
}
void HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) {
RefPtr<EditorBase> editorBase = GetEditor();
if (!editorBase) {
return;
}
SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges,
aStartPos, aEndPos);
DebugOnly<nsresult> rv =
editorBase->DeleteSelectionAsAction(nsIEditor::eNone, nsIEditor::eStrip);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to delete text");
}
void HyperTextAccessible::PasteText(int32_t aPosition) {
RefPtr<EditorBase> editorBase = GetEditor();
if (editorBase) {
// If the caller wants to paste at the caret, we don't need to set the
// selection. If there is text already selected, this also allows the caller
// to replace it, just as would happen when pasting using the keyboard or
// GUI.
if (aPosition != nsIAccessibleText::TEXT_OFFSET_CARET) {
SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges,
aPosition, aPosition);
}
editorBase->PasteAsAction(nsIClipboard::kGlobalClipboard,
EditorBase::DispatchPasteEvent::Yes);
}
}
////////////////////////////////////////////////////////////////////////////////
// LocalAccessible public
// LocalAccessible protected
ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
// Check @alt attribute for invalid img elements.
if (mContent->IsHTMLElement(nsGkAtoms::img)) {
mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName);
if (!aName.IsEmpty()) return eNameOK;
}
ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
if (!aName.IsEmpty()) return nameFlag;
// Get name from title attribute for HTML abbr and acronym elements making it
// a valid name from markup. Otherwise their name isn't picked up by recursive
// name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
if (IsAbbreviation() && mContent->AsElement()->GetAttr(
kNameSpaceID_None, nsGkAtoms::title, aName)) {
aName.CompressWhitespace();
}
return eNameOK;
}
void HyperTextAccessible::Shutdown() {
mOffsets.Clear();
AccessibleWrap::Shutdown();
}
bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) {
const int32_t childIndex = aAccessible->IndexInParent();
if (childIndex < static_cast<int32_t>(mOffsets.Length())) {
mOffsets.RemoveLastElements(mOffsets.Length() - childIndex);
}
return AccessibleWrap::RemoveChild(aAccessible);
}
bool HyperTextAccessible::InsertChildAt(uint32_t aIndex,
LocalAccessible* aChild) {
if (aIndex < mOffsets.Length()) {
mOffsets.RemoveLastElements(mOffsets.Length() - aIndex);
}
return AccessibleWrap::InsertChildAt(aIndex, aChild);
}
Relation HyperTextAccessible::RelationByType(RelationType aType) const {
Relation rel = LocalAccessible::RelationByType(aType);
switch (aType) {
case RelationType::NODE_CHILD_OF:
if (HasOwnContent() && mContent->IsMathMLElement()) {
LocalAccessible* parent = LocalParent();
if (parent) {
nsIContent* parentContent = parent->GetContent();
if (parentContent &&
parentContent->IsMathMLElement(nsGkAtoms::mroot)) {
// Add a relation pointing to the parent <mroot>.
rel.AppendTarget(parent);
}
}
}
break;
case RelationType::NODE_PARENT_OF:
if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot)) {
LocalAccessible* base = LocalChildAt(0);
LocalAccessible* index = LocalChildAt(1);
if (base && index) {
// Append the <mroot> children in the order index, base.
rel.AppendTarget(index);
rel.AppendTarget(base);
}
}
break;
default:
break;
}
return rel;
}
////////////////////////////////////////////////////////////////////////////////
// HyperTextAccessible public static
nsresult HyperTextAccessible::ContentToRenderedOffset(
nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
if (!aFrame) {
// Current frame not rendered -- this can happen if text is set on
// something with display: none
*aRenderedOffset = 0;
return NS_OK;
}
if (IsTextField()) {
*aRenderedOffset = aContentOffset;
return NS_OK;
}
NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
"Call on primary frame only");
nsIFrame::RenderedText text =
aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
nsIFrame::TextOffsetType::OffsetsInContentText,
nsIFrame::TrailingWhitespace::DontTrim);
*aRenderedOffset = text.mOffsetWithinNodeRenderedText;
return NS_OK;
}
nsresult HyperTextAccessible::RenderedToContentOffset(
nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
if (IsTextField()) {
*aContentOffset = aRenderedOffset;
return NS_OK;
}
*aContentOffset = 0;
NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
"Call on primary frame only");
nsIFrame::RenderedText text =
aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
nsIFrame::TextOffsetType::OffsetsInRenderedText,
nsIFrame::TrailingWhitespace::DontTrim);
*aContentOffset = text.mOffsetWithinNodeText;
return NS_OK;
}