/* -*- 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
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(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
is the hacky "padding
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.
abc

def

ghi
. 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 = 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(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 HyperTextAccessible::DefaultTextAttributes() { RefPtr 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 (or an inferred ). // 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 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 HyperTextAccessible::NativeAttributes() { RefPtr 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 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 docShell = nsCoreUtils::GetDocShellFor(mContent); nsCOMPtr editingSession; docShell->GetEditingSession(getter_AddRefs(editingSession)); if (!editingSession) return nullptr; // No editing session interface dom::Document* docNode = mDoc->DocumentNode(); RefPtr 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 = 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 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 window = docNode->GetWindow(); RefPtr 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 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 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* 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 frameSelection = FrameSelection(); if (!frameSelection || frameSelection->GetDisplaySelection() <= nsISelectionController::SELECTION_HIDDEN) { return; } dom::Selection* domSel = frameSelection->GetSelection(aSelectionType); if (!domSel) return; nsINode* startNode = GetNode(); RefPtr 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 ranges; GetSelectionDOMRanges(SelectionType::eNormal, &ranges); return ranges.Length(); } bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum, int32_t* aStartOffset, int32_t* aEndOffset) { *aStartOffset = *aEndOffset = 0; nsTArray ranges; GetSelectionDOMRanges(SelectionType::eNormal, &ranges); uint32_t rangeCount = ranges.Length(); if (aSelectionNum < 0 || aSelectionNum >= static_cast(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 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(startOffset)); } if (!endNode->IsInclusiveDescendantOf(mContent)) { *aEndOffset = CharacterCount(); } else { *aEndOffset = DOMPointToOffset(endNode, AssertedCast(endOffset), true); } return true; } bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) { RefPtr domSel = DOMSelection(); if (!domSel) return false; if (aSelectionNum < 0 || aSelectionNum >= static_cast(domSel->RangeCount())) { return false; } const RefPtr range{ domSel->GetRangeAt(static_cast(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 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* 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 = GetEditor(); if (!editorBase) { return; } DebugOnly 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 = GetEditor(); if (editorBase) { SetSelectionRange(aPosition, aPosition); DebugOnly 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 = GetEditor(); if (editorBase) { SetSelectionRange(aStartPos, aEndPos); editorBase->Copy(); } } void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) { RefPtr editorBase = GetEditor(); if (editorBase) { SetSelectionRange(aStartPos, aEndPos); editorBase->Cut(); } } void HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) { RefPtr editorBase = GetEditor(); if (!editorBase) { return; } SetSelectionRange(aStartPos, aEndPos); DebugOnly 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 = 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(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 . 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 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; }