summaryrefslogtreecommitdiffstats
path: root/accessible/generic/HyperTextAccessible.cpp
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /accessible/generic/HyperTextAccessible.cpp
parentInitial commit. (diff)
downloadfirefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.tar.xz
firefox-e51783d008170d9ab27d25da98ca3a38b0a41b67.zip
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/generic/HyperTextAccessible.cpp')
-rw-r--r--accessible/generic/HyperTextAccessible.cpp1141
1 files changed, 1141 insertions, 0 deletions
diff --git a/accessible/generic/HyperTextAccessible.cpp b/accessible/generic/HyperTextAccessible.cpp
new file mode 100644
index 0000000000..eaeba0ccf9
--- /dev/null
+++ b/accessible/generic/HyperTextAccessible.cpp
@@ -0,0 +1,1141 @@
+/* -*- 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 "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 "nsIScrollableFrame.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/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;
+ }
+ }
+ }
+
+ 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 ======================
+ */
+
+nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
+ int32_t aEndPos) {
+ // Before setting the selection range, we need to ensure that the editor
+ // is initialized. (See bug 804927.)
+ // Otherwise, it's possible that lazy editor initialization will override
+ // the selection we set here and leave the caret at the end of the text.
+ // By calling GetEditor here, we ensure that editor initialization is
+ // completed before we set the selection.
+ RefPtr<EditorBase> editorBase = GetEditor();
+
+ bool isFocusable = InteractiveState() & states::FOCUSABLE;
+
+ // If accessible is focusable then focus it before setting the selection to
+ // neglect control's selection changes on focus if any (for example, inputs
+ // that do select all on focus).
+ // some input controls
+ if (isFocusable) TakeFocus();
+
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ NS_ENSURE_STATE(domSel);
+
+ // Set up the selection.
+ domSel->RemoveAllRanges(IgnoreErrors());
+ SetSelectionBoundsAt(0, aStartPos, aEndPos);
+
+ // Make sure it is visible
+ domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
+ ScrollAxis(), ScrollAxis(),
+ dom::Selection::SCROLL_FOR_CARET_MOVE |
+ dom::Selection::SCROLL_OVERFLOW_HIDDEN);
+
+ // When selection is done, move the focus to the selection if accessible is
+ // not focusable. That happens when selection is set within hypertext
+ // accessible.
+ if (isFocusable) return NS_OK;
+
+ nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
+ if (DOMFocusManager) {
+ NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
+ dom::Document* docNode = mDoc->DocumentNode();
+ NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
+ nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
+ RefPtr<dom::Element> result;
+ DOMFocusManager->MoveFocus(
+ window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
+ nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
+ }
+
+ return NS_OK;
+}
+
+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);
+}
+
+int32_t HyperTextAccessible::CaretLineNumber() {
+ // Provide the line number for the caret, relative to the
+ // currently focused node. Use a 1-based index
+ RefPtr<nsFrameSelection> frameSelection = FrameSelection();
+ if (!frameSelection) return -1;
+
+ dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
+ if (!domSel) return -1;
+
+ nsINode* caretNode = domSel->GetFocusNode();
+ if (!caretNode || !caretNode->IsContent()) return -1;
+
+ nsIContent* caretContent = caretNode->AsContent();
+ if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
+
+ uint32_t caretOffset = domSel->FocusOffset();
+ CaretAssociationHint hint = frameSelection->GetHint();
+ nsIFrame* caretFrame = SelectionMovementUtils::GetFrameForNodeOffset(
+ caretContent, caretOffset, hint);
+ NS_ENSURE_TRUE(caretFrame, -1);
+
+ AutoAssertNoDomMutations guard; // The nsILineIterators below will break if
+ // the DOM is modified while they're in use!
+ int32_t lineNumber = 1;
+ nsILineIterator* lineIterForCaret = nullptr;
+ nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
+ while (caretFrame) {
+ if (hyperTextContent == caretFrame->GetContent()) {
+ return lineNumber; // Must be in a single line hyper text, there is no
+ // line iterator
+ }
+ nsContainerFrame* parentFrame = caretFrame->GetParent();
+ if (!parentFrame) break;
+
+ // Add lines for the sibling frames before the caret
+ nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
+ while (sibling && sibling != caretFrame) {
+ nsILineIterator* lineIterForSibling = sibling->GetLineIterator();
+ if (lineIterForSibling) {
+ // For the frames before that grab all the lines
+ int32_t addLines = lineIterForSibling->GetNumLines();
+ lineNumber += addLines;
+ }
+ sibling = sibling->GetNextSibling();
+ }
+
+ // Get the line number relative to the container with lines
+ if (!lineIterForCaret) { // Add the caret line just once
+ lineIterForCaret = parentFrame->GetLineIterator();
+ if (lineIterForCaret) {
+ // Ancestor of caret
+ int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
+ lineNumber += addLines;
+ }
+ }
+
+ caretFrame = parentFrame;
+ }
+
+ MOZ_ASSERT_UNREACHABLE(
+ "DOM ancestry had this hypertext but frame ancestry didn't");
+ return lineNumber;
+}
+
+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];
+
+ // Get start and end points.
+ nsINode* startNode = range->GetStartContainer();
+ nsINode* endNode = range->GetEndContainer();
+ uint32_t startOffset = range->StartOffset();
+ uint32_t endOffset = range->EndOffset();
+
+ // 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(endNode, endOffset, startNode, startOffset);
+
+ if (!order) {
+ MOZ_ASSERT_UNREACHABLE();
+ return false;
+ }
+
+ if (*order < 0) {
+ std::swap(startNode, endNode);
+ std::swap(startOffset, endOffset);
+ }
+
+ if (!startNode->IsInclusiveDescendantOf(mContent)) {
+ *aStartOffset = 0;
+ } else {
+ *aStartOffset =
+ DOMPointToOffset(startNode, AssertedCast<int32_t>(startOffset));
+ }
+
+ if (!endNode->IsInclusiveDescendantOf(mContent)) {
+ *aEndOffset = CharacterCount();
+ } else {
+ *aEndOffset =
+ DOMPointToOffset(endNode, AssertedCast<int32_t>(endOffset), true);
+ }
+ return true;
+}
+
+bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
+ RefPtr<dom::Selection> domSel = DOMSelection();
+ if (!domSel) return false;
+
+ 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())) {
+ nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
+ if (scrollableFrame) {
+ 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;
+ }
+
+ SetSelectionRange(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) {
+ SetSelectionRange(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) {
+ SetSelectionRange(aStartPos, aEndPos);
+ editorBase->Copy();
+ }
+}
+
+void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (editorBase) {
+ SetSelectionRange(aStartPos, aEndPos);
+ editorBase->Cut();
+ }
+}
+
+void HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) {
+ RefPtr<EditorBase> editorBase = GetEditor();
+ if (!editorBase) {
+ return;
+ }
+ SetSelectionRange(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) {
+ SetSelectionRange(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;
+}