Bug 1856666 - run snap tests as cron r=releng-reviewers,ahal
[gecko.git] / accessible / generic / HyperTextAccessible.cpp
blob7ebeee2dddeb0c0c1b0521681d4261c6cd59f273
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=2 sw=2 et tw=78: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "HyperTextAccessible-inl.h"
9 #include "nsAccessibilityService.h"
10 #include "nsIAccessibleTypes.h"
11 #include "AccAttributes.h"
12 #include "HTMLListAccessible.h"
13 #include "LocalAccessible-inl.h"
14 #include "Relation.h"
15 #include "mozilla/a11y/Role.h"
16 #include "States.h"
17 #include "TextAttrs.h"
18 #include "TextRange.h"
19 #include "TreeWalker.h"
21 #include "nsCaret.h"
22 #include "nsContentUtils.h"
23 #include "nsDebug.h"
24 #include "nsFocusManager.h"
25 #include "nsIEditingSession.h"
26 #include "nsContainerFrame.h"
27 #include "nsFrameSelection.h"
28 #include "nsILineIterator.h"
29 #include "nsIScrollableFrame.h"
30 #include "nsIMathMLFrame.h"
31 #include "nsLayoutUtils.h"
32 #include "nsRange.h"
33 #include "mozilla/Assertions.h"
34 #include "mozilla/EditorBase.h"
35 #include "mozilla/HTMLEditor.h"
36 #include "mozilla/IntegerRange.h"
37 #include "mozilla/PresShell.h"
38 #include "mozilla/dom/Element.h"
39 #include "mozilla/dom/HTMLBRElement.h"
40 #include "mozilla/dom/Selection.h"
41 #include "gfxSkipChars.h"
43 using namespace mozilla;
44 using namespace mozilla::a11y;
46 ////////////////////////////////////////////////////////////////////////////////
47 // HyperTextAccessible
48 ////////////////////////////////////////////////////////////////////////////////
50 HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
51 : AccessibleWrap(aNode, aDoc) {
52 mType = eHyperTextType;
53 mGenericTypes |= eHyperText;
56 role HyperTextAccessible::NativeRole() const {
57 a11y::role r = GetAccService()->MarkupRole(mContent);
58 if (r != roles::NOTHING) return r;
60 nsIFrame* frame = GetFrame();
61 if (frame && frame->IsInlineFrame()) return roles::TEXT;
63 return roles::TEXT_CONTAINER;
66 uint64_t HyperTextAccessible::NativeState() const {
67 uint64_t states = AccessibleWrap::NativeState();
69 if (IsEditable()) {
70 states |= states::EDITABLE;
72 } else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
73 // We want <article> to behave like a document in terms of readonly state.
74 states |= states::READONLY;
77 nsIFrame* frame = GetFrame();
78 if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) {
79 // If the accessible is editable the layout selectable state only disables
80 // mouse selection, but keyboard (shift+arrow) selection is still possible.
81 states |= states::SELECTABLE_TEXT;
84 return states;
87 bool HyperTextAccessible::IsEditable() const {
88 if (!mContent) {
89 return false;
91 return mContent->AsElement()->State().HasState(dom::ElementState::READWRITE);
94 uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
95 int32_t aNodeOffset,
96 bool aIsEndOffset) const {
97 if (!aNode) return 0;
99 uint32_t offset = 0;
100 nsINode* findNode = nullptr;
102 if (aNodeOffset == -1) {
103 findNode = aNode;
105 } else if (aNode->IsText()) {
106 // For text nodes, aNodeOffset comes in as a character offset
107 // Text offset will be added at the end, if we find the offset in this
108 // hypertext We want the "skipped" offset into the text (rendered text
109 // without the extra whitespace)
110 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
111 NS_ENSURE_TRUE(frame, 0);
113 nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
114 NS_ENSURE_SUCCESS(rv, 0);
116 findNode = aNode;
118 } else {
119 // findNode could be null if aNodeOffset == # of child nodes, which means
120 // one of two things:
121 // 1) there are no children, and the passed-in node is not mContent -- use
122 // parentContent for the node to find
123 // 2) there are no children and the passed-in node is mContent, which means
124 // we're an empty nsIAccessibleText
125 // 3) there are children and we're at the end of the children
127 findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
128 if (!findNode) {
129 if (aNodeOffset == 0) {
130 if (aNode == GetNode()) {
131 // Case #1: this accessible has no children and thus has empty text,
132 // we can only be at hypertext offset 0.
133 return 0;
136 // Case #2: there are no children, we're at this node.
137 findNode = aNode;
138 } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
139 // Case #3: we're after the last child, get next node to this one.
140 for (nsINode* tmpNode = aNode;
141 !findNode && tmpNode && tmpNode != mContent;
142 tmpNode = tmpNode->GetParent()) {
143 findNode = tmpNode->GetNextSibling();
149 // Get accessible for this findNode, or if that node isn't accessible, use the
150 // accessible for the next DOM node which has one (based on forward depth
151 // first search)
152 LocalAccessible* descendant = nullptr;
153 if (findNode) {
154 dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
155 if (brElement && brElement->IsPaddingForEmptyEditor()) {
156 // This <br> is the hacky "padding <br> element" used when there is no
157 // text in the editor.
158 return 0;
161 descendant = mDoc->GetAccessible(findNode);
162 if (!descendant && findNode->IsContent()) {
163 LocalAccessible* container = mDoc->GetContainerAccessible(findNode);
164 if (container) {
165 TreeWalker walker(container, findNode->AsContent(),
166 TreeWalker::eWalkContextTree);
167 descendant = walker.Next();
168 if (!descendant) descendant = container;
173 return TransformOffset(descendant, offset, aIsEndOffset);
176 uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant,
177 uint32_t aOffset,
178 bool aIsEndOffset) const {
179 // From the descendant, go up and get the immediate child of this hypertext.
180 uint32_t offset = aOffset;
181 LocalAccessible* descendant = aDescendant;
182 while (descendant) {
183 LocalAccessible* parent = descendant->LocalParent();
184 if (parent == this) return GetChildOffset(descendant) + offset;
186 // This offset no longer applies because the passed-in text object is not
187 // a child of the hypertext. This happens when there are nested hypertexts,
188 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
189 // to make it relative the hypertext.
190 // If the end offset is not supposed to be inclusive and the original point
191 // is not at 0 offset then the returned offset should be after an embedded
192 // character the original point belongs to.
193 if (aIsEndOffset) {
194 // Similar to our special casing in FindOffset, we add handling for
195 // bulleted lists here because PeekOffset returns the inner text node
196 // for a list when it should return the list bullet.
197 // We manually set the offset so the error doesn't propagate up.
198 if (offset == 0 && parent && parent->IsHTMLListItem() &&
199 descendant->LocalPrevSibling() &&
200 descendant->LocalPrevSibling() ==
201 parent->AsHTMLListItem()->Bullet()) {
202 offset = 0;
203 } else {
204 offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
206 } else {
207 offset = 0;
210 descendant = parent;
213 // If the given a11y point cannot be mapped into offset relative this
214 // hypertext offset then return length as fallback value.
215 return CharacterCount();
218 DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
219 // 0 offset is valid even if no children. In this case the associated editor
220 // is empty so return a DOM point for editor root element.
221 if (aOffset == 0) {
222 RefPtr<EditorBase> editorBase = GetEditor();
223 if (editorBase) {
224 if (editorBase->IsEmpty()) {
225 return DOMPoint(editorBase->GetRoot(), 0);
230 int32_t childIdx = GetChildIndexAtOffset(aOffset);
231 if (childIdx == -1) return DOMPoint();
233 LocalAccessible* child = LocalChildAt(childIdx);
234 int32_t innerOffset = aOffset - GetChildOffset(childIdx);
236 // A text leaf case.
237 if (child->IsTextLeaf()) {
238 // The point is inside the text node. This is always true for any text leaf
239 // except a last child one. See assertion below.
240 if (aOffset < GetChildOffset(childIdx + 1)) {
241 nsIContent* content = child->GetContent();
242 int32_t idx = 0;
243 if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
244 innerOffset, &idx))) {
245 return DOMPoint();
248 return DOMPoint(content, idx);
251 // Set the DOM point right after the text node.
252 MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
253 innerOffset = 1;
256 // Case of embedded object. The point is either before or after the element.
257 NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
258 nsINode* node = child->GetNode();
259 nsINode* parentNode = node->GetParentNode();
260 return parentNode ? DOMPoint(parentNode,
261 parentNode->ComputeIndexOf_Deprecated(node) +
262 innerOffset)
263 : DOMPoint();
266 already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() {
267 RefPtr<AccAttributes> attributes = new AccAttributes();
269 TextAttrsMgr textAttrsMgr(this);
270 textAttrsMgr.GetAttributes(attributes);
271 return attributes.forget();
274 void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) {
275 // Add MathML xmlroles based on the position inside the parent.
276 LocalAccessible* parent = LocalParent();
277 if (parent) {
278 switch (parent->Role()) {
279 case roles::MATHML_CELL:
280 case roles::MATHML_ENCLOSED:
281 case roles::MATHML_ERROR:
282 case roles::MATHML_MATH:
283 case roles::MATHML_ROW:
284 case roles::MATHML_SQUARE_ROOT:
285 case roles::MATHML_STYLE:
286 if (Role() == roles::MATHML_OPERATOR) {
287 // This is an operator inside an <mrow> (or an inferred <mrow>).
288 // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
289 // XXX We should probably do something similar for MATHML_FENCED, but
290 // operators do not appear in the accessible tree. See bug 1175747.
291 nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
292 if (mathMLFrame) {
293 nsEmbellishData embellishData;
294 mathMLFrame->GetEmbellishData(embellishData);
295 if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
296 if (!LocalPrevSibling()) {
297 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
298 nsGkAtoms::open_fence);
299 } else if (!LocalNextSibling()) {
300 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
301 nsGkAtoms::close_fence);
304 if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
305 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
306 nsGkAtoms::separator_);
310 break;
311 case roles::MATHML_FRACTION:
312 aAttributes->SetAttribute(
313 nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator
314 : nsGkAtoms::denominator);
315 break;
316 case roles::MATHML_ROOT:
317 aAttributes->SetAttribute(
318 nsGkAtoms::xmlroles,
319 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
320 break;
321 case roles::MATHML_SUB:
322 aAttributes->SetAttribute(
323 nsGkAtoms::xmlroles,
324 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
325 break;
326 case roles::MATHML_SUP:
327 aAttributes->SetAttribute(
328 nsGkAtoms::xmlroles,
329 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
330 break;
331 case roles::MATHML_SUB_SUP: {
332 int32_t index = IndexInParent();
333 aAttributes->SetAttribute(
334 nsGkAtoms::xmlroles,
335 index == 0
336 ? nsGkAtoms::base
337 : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
338 } break;
339 case roles::MATHML_UNDER:
340 aAttributes->SetAttribute(
341 nsGkAtoms::xmlroles,
342 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
343 break;
344 case roles::MATHML_OVER:
345 aAttributes->SetAttribute(
346 nsGkAtoms::xmlroles,
347 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
348 break;
349 case roles::MATHML_UNDER_OVER: {
350 int32_t index = IndexInParent();
351 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
352 index == 0
353 ? nsGkAtoms::base
354 : (index == 1 ? nsGkAtoms::underscript
355 : nsGkAtoms::overscript));
356 } break;
357 case roles::MATHML_MULTISCRIPTS: {
358 // Get the <multiscripts> base.
359 nsIContent* child;
360 bool baseFound = false;
361 for (child = parent->GetContent()->GetFirstChild(); child;
362 child = child->GetNextSibling()) {
363 if (child->IsMathMLElement()) {
364 baseFound = true;
365 break;
368 if (baseFound) {
369 nsIContent* content = GetContent();
370 if (child == content) {
371 // We are the base.
372 aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base);
373 } else {
374 // Browse the list of scripts to find us and determine our type.
375 bool postscript = true;
376 bool subscript = true;
377 for (child = child->GetNextSibling(); child;
378 child = child->GetNextSibling()) {
379 if (!child->IsMathMLElement()) continue;
380 if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) {
381 postscript = false;
382 subscript = true;
383 continue;
385 if (child == content) {
386 if (postscript) {
387 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
388 subscript ? nsGkAtoms::subscript
389 : nsGkAtoms::superscript);
390 } else {
391 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
392 subscript
393 ? nsGkAtoms::presubscript
394 : nsGkAtoms::presuperscript);
396 break;
398 subscript = !subscript;
402 } break;
403 default:
404 break;
409 already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() {
410 RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
412 // 'formatting' attribute is deprecated, 'display' attribute should be
413 // instead.
414 nsIFrame* frame = GetFrame();
415 if (frame && frame->IsBlockFrame()) {
416 attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block);
419 if (FocusMgr()->IsFocused(this)) {
420 int32_t lineNumber = CaretLineNumber();
421 if (lineNumber >= 1) {
422 attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber);
426 if (HasOwnContent()) {
427 GetAccService()->MarkupAttributes(this, attributes);
428 if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
431 return attributes.forget();
434 int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
435 uint32_t aCoordType) {
436 nsIFrame* hyperFrame = GetFrame();
437 if (!hyperFrame) return -1;
439 LayoutDeviceIntPoint coords =
440 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
442 nsPresContext* presContext = mDoc->PresContext();
443 nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
444 coords, presContext->AppUnitsPerDevPixel());
446 nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
447 if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) {
448 return -1; // Not found
451 nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
452 coordsInAppUnits.y - frameScreenRect.Y());
454 // Go through the frames to check if each one has the point.
455 // When one does, add up the character offsets until we have a match
457 // We have an point in an accessible child of this, now we need to add up the
458 // offsets before it to what we already have
459 int32_t offset = 0;
460 uint32_t childCount = ChildCount();
461 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
462 LocalAccessible* childAcc = mChildren[childIdx];
464 nsIFrame* primaryFrame = childAcc->GetFrame();
465 NS_ENSURE_TRUE(primaryFrame, -1);
467 nsIFrame* frame = primaryFrame;
468 while (frame) {
469 nsIContent* content = frame->GetContent();
470 NS_ENSURE_TRUE(content, -1);
471 nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
472 nsSize frameSize = frame->GetSize();
473 if (pointInFrame.x < frameSize.width &&
474 pointInFrame.y < frameSize.height) {
475 // Finished
476 if (frame->IsTextFrame()) {
477 nsIFrame::ContentOffsets contentOffsets =
478 frame->GetContentOffsetsFromPointExternal(
479 pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
480 if (contentOffsets.IsNull() || contentOffsets.content != content) {
481 return -1; // Not found
483 uint32_t addToOffset;
484 nsresult rv = ContentToRenderedOffset(
485 primaryFrame, contentOffsets.offset, &addToOffset);
486 NS_ENSURE_SUCCESS(rv, -1);
487 offset += addToOffset;
489 return offset;
491 frame = frame->GetNextContinuation();
494 offset += nsAccUtils::TextLength(childAcc);
497 return -1; // Not found
500 already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const {
501 if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
502 // If we're inside an editable container, then return that container's
503 // editor
504 LocalAccessible* ancestor = LocalParent();
505 while (ancestor) {
506 HyperTextAccessible* hyperText = ancestor->AsHyperText();
507 if (hyperText) {
508 // Recursion will stop at container doc because it has its own impl
509 // of GetEditor()
510 return hyperText->GetEditor();
513 ancestor = ancestor->LocalParent();
516 return nullptr;
519 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
520 nsCOMPtr<nsIEditingSession> editingSession;
521 docShell->GetEditingSession(getter_AddRefs(editingSession));
522 if (!editingSession) return nullptr; // No editing session interface
524 dom::Document* docNode = mDoc->DocumentNode();
525 RefPtr<HTMLEditor> htmlEditor =
526 editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
527 return htmlEditor.forget();
531 * =================== Caret & Selection ======================
534 nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
535 int32_t aEndPos) {
536 // Before setting the selection range, we need to ensure that the editor
537 // is initialized. (See bug 804927.)
538 // Otherwise, it's possible that lazy editor initialization will override
539 // the selection we set here and leave the caret at the end of the text.
540 // By calling GetEditor here, we ensure that editor initialization is
541 // completed before we set the selection.
542 RefPtr<EditorBase> editorBase = GetEditor();
544 bool isFocusable = InteractiveState() & states::FOCUSABLE;
546 // If accessible is focusable then focus it before setting the selection to
547 // neglect control's selection changes on focus if any (for example, inputs
548 // that do select all on focus).
549 // some input controls
550 if (isFocusable) TakeFocus();
552 RefPtr<dom::Selection> domSel = DOMSelection();
553 NS_ENSURE_STATE(domSel);
555 // Set up the selection.
556 for (const uint32_t idx : Reversed(IntegerRange(1u, domSel->RangeCount()))) {
557 MOZ_ASSERT(domSel->RangeCount() == idx + 1);
558 RefPtr<nsRange> range{domSel->GetRangeAt(idx)};
559 if (!range) {
560 break; // The range count has been changed by somebody else.
562 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
563 IgnoreErrors());
565 SetSelectionBoundsAt(0, aStartPos, aEndPos);
567 // Make sure it is visible
568 domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
569 ScrollAxis(), ScrollAxis(),
570 dom::Selection::SCROLL_FOR_CARET_MOVE |
571 dom::Selection::SCROLL_OVERFLOW_HIDDEN);
573 // When selection is done, move the focus to the selection if accessible is
574 // not focusable. That happens when selection is set within hypertext
575 // accessible.
576 if (isFocusable) return NS_OK;
578 nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
579 if (DOMFocusManager) {
580 NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
581 dom::Document* docNode = mDoc->DocumentNode();
582 NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
583 nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
584 RefPtr<dom::Element> result;
585 DOMFocusManager->MoveFocus(
586 window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
587 nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
590 return NS_OK;
593 int32_t HyperTextAccessible::CaretOffset() const {
594 // Not focused focusable accessible except document accessible doesn't have
595 // a caret.
596 if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
597 (InteractiveState() & states::FOCUSABLE)) {
598 return -1;
601 // Check cached value.
602 int32_t caretOffset = -1;
603 HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
605 // Use cached value if it corresponds to this accessible.
606 if (caretOffset != -1) {
607 if (text == this) return caretOffset;
609 nsINode* textNode = text->GetNode();
610 // Ignore offset if cached accessible isn't a text leaf.
611 if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) {
612 return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
616 // No caret if the focused node is not inside this DOM node and this DOM node
617 // is not inside of focused node.
618 FocusManager::FocusDisposition focusDisp =
619 FocusMgr()->IsInOrContainsFocus(this);
620 if (focusDisp == FocusManager::eNone) return -1;
622 // Turn the focus node and offset of the selection into caret hypretext
623 // offset.
624 dom::Selection* domSel = DOMSelection();
625 NS_ENSURE_TRUE(domSel, -1);
627 nsINode* focusNode = domSel->GetFocusNode();
628 uint32_t focusOffset = domSel->FocusOffset();
630 // No caret if this DOM node is inside of focused node but the selection's
631 // focus point is not inside of this DOM node.
632 if (focusDisp == FocusManager::eContainedByFocus) {
633 nsINode* resultNode =
634 nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
636 nsINode* thisNode = GetNode();
637 if (resultNode != thisNode &&
638 !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) {
639 return -1;
643 return DOMPointToOffset(focusNode, focusOffset);
646 int32_t HyperTextAccessible::CaretLineNumber() {
647 // Provide the line number for the caret, relative to the
648 // currently focused node. Use a 1-based index
649 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
650 if (!frameSelection) return -1;
652 dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
653 if (!domSel) return -1;
655 nsINode* caretNode = domSel->GetFocusNode();
656 if (!caretNode || !caretNode->IsContent()) return -1;
658 nsIContent* caretContent = caretNode->AsContent();
659 if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
661 int32_t returnOffsetUnused;
662 uint32_t caretOffset = domSel->FocusOffset();
663 CaretAssociationHint hint = frameSelection->GetHint();
664 nsIFrame* caretFrame = frameSelection->GetFrameForNodeOffset(
665 caretContent, caretOffset, hint, &returnOffsetUnused);
666 NS_ENSURE_TRUE(caretFrame, -1);
668 AutoAssertNoDomMutations guard; // The nsILineIterators below will break if
669 // the DOM is modified while they're in use!
670 int32_t lineNumber = 1;
671 nsILineIterator* lineIterForCaret = nullptr;
672 nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
673 while (caretFrame) {
674 if (hyperTextContent == caretFrame->GetContent()) {
675 return lineNumber; // Must be in a single line hyper text, there is no
676 // line iterator
678 nsContainerFrame* parentFrame = caretFrame->GetParent();
679 if (!parentFrame) break;
681 // Add lines for the sibling frames before the caret
682 nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
683 while (sibling && sibling != caretFrame) {
684 nsILineIterator* lineIterForSibling = sibling->GetLineIterator();
685 if (lineIterForSibling) {
686 // For the frames before that grab all the lines
687 int32_t addLines = lineIterForSibling->GetNumLines();
688 lineNumber += addLines;
690 sibling = sibling->GetNextSibling();
693 // Get the line number relative to the container with lines
694 if (!lineIterForCaret) { // Add the caret line just once
695 lineIterForCaret = parentFrame->GetLineIterator();
696 if (lineIterForCaret) {
697 // Ancestor of caret
698 int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
699 lineNumber += addLines;
703 caretFrame = parentFrame;
706 MOZ_ASSERT_UNREACHABLE(
707 "DOM ancestry had this hypertext but frame ancestry didn't");
708 return lineNumber;
711 LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
712 *aWidget = nullptr;
714 RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
715 NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
717 bool isVisible = caret->IsVisible();
718 if (!isVisible) return LayoutDeviceIntRect();
720 nsRect rect;
721 nsIFrame* frame = caret->GetGeometry(&rect);
722 if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
724 PresShell* presShell = mDoc->PresShellPtr();
725 // Transform rect to be relative to the root frame.
726 nsIFrame* rootFrame = presShell->GetRootFrame();
727 rect = nsLayoutUtils::TransformFrameRectToAncestor(frame, rect, rootFrame);
728 // We need to inverse translate with the offset of the edge of the visual
729 // viewport from top edge of the layout viewport.
730 nsPoint viewportOffset = presShell->GetVisualViewportOffset() -
731 presShell->GetLayoutViewportOffset();
732 rect.MoveBy(-viewportOffset);
733 // We need to take into account a non-1 resolution set on the presshell.
734 // This happens with async pinch zooming. Here we scale the bounds before
735 // adding the screen-relative offset.
736 rect.ScaleRoundOut(presShell->GetResolution());
737 // Now we need to put the rect in absolute screen coords.
738 nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
739 rect.MoveBy(rootScreenRect.TopLeft());
740 // Finally, convert from app units.
741 auto caretRect = LayoutDeviceIntRect::FromAppUnitsToNearest(
742 rect, presShell->GetPresContext()->AppUnitsPerDevPixel());
744 // Correct for character size, so that caret always matches the size of
745 // the character. This is important for font size transitions, and is
746 // necessary because the Gecko caret uses the previous character's size as
747 // the user moves forward in the text by character.
748 int32_t caretOffset = CaretOffset();
749 if (NS_WARN_IF(caretOffset == -1)) {
750 // The caret offset will be -1 if this Accessible isn't focused. Note that
751 // the DOM node contaning the caret might be focused, but the Accessible
752 // might not be; e.g. due to an autocomplete popup suggestion having a11y
753 // focus.
754 return LayoutDeviceIntRect();
756 LayoutDeviceIntRect charRect = CharBounds(
757 caretOffset, nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
758 if (!charRect.IsEmpty()) {
759 caretRect.SetTopEdge(charRect.Y());
762 *aWidget = frame->GetNearestWidget();
763 return caretRect;
766 void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
767 nsTArray<nsRange*>* aRanges) {
768 // Ignore selection if it is not visible.
769 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
770 if (!frameSelection || frameSelection->GetDisplaySelection() <=
771 nsISelectionController::SELECTION_HIDDEN) {
772 return;
775 dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
776 if (!domSel) return;
778 nsINode* startNode = GetNode();
780 RefPtr<EditorBase> editorBase = GetEditor();
781 if (editorBase) {
782 startNode = editorBase->GetRoot();
785 if (!startNode) return;
787 uint32_t childCount = startNode->GetChildCount();
788 nsresult rv = domSel->GetDynamicRangesForIntervalArray(
789 startNode, 0, startNode, childCount, true, aRanges);
790 NS_ENSURE_SUCCESS_VOID(rv);
792 // Remove collapsed ranges
793 aRanges->RemoveElementsBy(
794 [](const auto& range) { return range->Collapsed(); });
797 int32_t HyperTextAccessible::SelectionCount() {
798 nsTArray<nsRange*> ranges;
799 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
800 return ranges.Length();
803 bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
804 int32_t* aStartOffset,
805 int32_t* aEndOffset) {
806 *aStartOffset = *aEndOffset = 0;
808 nsTArray<nsRange*> ranges;
809 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
811 uint32_t rangeCount = ranges.Length();
812 if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) {
813 return false;
816 nsRange* range = ranges[aSelectionNum];
818 // Get start and end points.
819 nsINode* startNode = range->GetStartContainer();
820 nsINode* endNode = range->GetEndContainer();
821 uint32_t startOffset = range->StartOffset();
822 uint32_t endOffset = range->EndOffset();
824 // Make sure start is before end, by swapping DOM points. This occurs when
825 // the user selects backwards in the text.
826 const Maybe<int32_t> order =
827 nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset);
829 if (!order) {
830 MOZ_ASSERT_UNREACHABLE();
831 return false;
834 if (*order < 0) {
835 std::swap(startNode, endNode);
836 std::swap(startOffset, endOffset);
839 if (!startNode->IsInclusiveDescendantOf(mContent)) {
840 *aStartOffset = 0;
841 } else {
842 *aStartOffset =
843 DOMPointToOffset(startNode, AssertedCast<int32_t>(startOffset));
846 if (!endNode->IsInclusiveDescendantOf(mContent)) {
847 *aEndOffset = CharacterCount();
848 } else {
849 *aEndOffset =
850 DOMPointToOffset(endNode, AssertedCast<int32_t>(endOffset), true);
852 return true;
855 bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
856 RefPtr<dom::Selection> domSel = DOMSelection();
857 if (!domSel) return false;
859 if (aSelectionNum < 0 ||
860 aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) {
861 return false;
864 const RefPtr<nsRange> range{
865 domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))};
866 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
867 IgnoreErrors());
868 return true;
871 void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
872 int32_t aEndOffset,
873 uint32_t aCoordinateType,
874 int32_t aX, int32_t aY) {
875 nsIFrame* frame = GetFrame();
876 if (!frame) return;
878 LayoutDeviceIntPoint coords =
879 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
881 RefPtr<nsRange> domRange = nsRange::Create(mContent);
882 TextRange range(this, this, aStartOffset, this, aEndOffset);
883 if (!range.AssignDOMRange(domRange)) {
884 return;
887 nsPresContext* presContext = frame->PresContext();
888 nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
889 coords, presContext->AppUnitsPerDevPixel());
891 bool initialScrolled = false;
892 nsIFrame* parentFrame = frame;
893 while ((parentFrame = parentFrame->GetParent())) {
894 nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
895 if (scrollableFrame) {
896 if (!initialScrolled) {
897 // Scroll substring to the given point. Turn the point into percents
898 // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
899 nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
900 nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
901 nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
903 nsSize size(parentFrame->GetSize());
905 // avoid divide by zero
906 size.width = size.width ? size.width : 1;
907 size.height = size.height ? size.height : 1;
909 int16_t hPercent = offsetPointX * 100 / size.width;
910 int16_t vPercent = offsetPointY * 100 / size.height;
912 nsresult rv = nsCoreUtils::ScrollSubstringTo(
913 frame, domRange,
914 ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always),
915 ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always));
916 if (NS_FAILED(rv)) return;
918 initialScrolled = true;
919 } else {
920 // Substring was scrolled to the given point already inside its closest
921 // scrollable area. If there are nested scrollable areas then make
922 // sure we scroll lower areas to the given point inside currently
923 // traversed scrollable area.
924 nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
927 frame = parentFrame;
931 void HyperTextAccessible::SelectionRanges(
932 nsTArray<a11y::TextRange>* aRanges) const {
933 dom::Selection* sel = DOMSelection();
934 if (!sel) {
935 return;
938 TextRange::TextRangesFromSelection(sel, aRanges);
941 void HyperTextAccessible::ReplaceText(const nsAString& aText) {
942 if (aText.Length() == 0) {
943 DeleteText(0, CharacterCount());
944 return;
947 SetSelectionRange(0, CharacterCount());
949 RefPtr<EditorBase> editorBase = GetEditor();
950 if (!editorBase) {
951 return;
954 DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
955 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text");
958 void HyperTextAccessible::InsertText(const nsAString& aText,
959 int32_t aPosition) {
960 RefPtr<EditorBase> editorBase = GetEditor();
961 if (editorBase) {
962 SetSelectionRange(aPosition, aPosition);
963 DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
964 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the text");
968 void HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) {
969 RefPtr<EditorBase> editorBase = GetEditor();
970 if (editorBase) {
971 SetSelectionRange(aStartPos, aEndPos);
972 editorBase->Copy();
976 void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) {
977 RefPtr<EditorBase> editorBase = GetEditor();
978 if (editorBase) {
979 SetSelectionRange(aStartPos, aEndPos);
980 editorBase->Cut();
984 void HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) {
985 RefPtr<EditorBase> editorBase = GetEditor();
986 if (!editorBase) {
987 return;
989 SetSelectionRange(aStartPos, aEndPos);
990 DebugOnly<nsresult> rv =
991 editorBase->DeleteSelectionAsAction(nsIEditor::eNone, nsIEditor::eStrip);
992 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to delete text");
995 void HyperTextAccessible::PasteText(int32_t aPosition) {
996 RefPtr<EditorBase> editorBase = GetEditor();
997 if (editorBase) {
998 SetSelectionRange(aPosition, aPosition);
999 editorBase->PasteAsAction(nsIClipboard::kGlobalClipboard,
1000 EditorBase::DispatchPasteEvent::Yes);
1004 ////////////////////////////////////////////////////////////////////////////////
1005 // LocalAccessible public
1007 // LocalAccessible protected
1008 ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
1009 // Check @alt attribute for invalid img elements.
1010 if (mContent->IsHTMLElement(nsGkAtoms::img)) {
1011 mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName);
1012 if (!aName.IsEmpty()) return eNameOK;
1015 ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
1016 if (!aName.IsEmpty()) return nameFlag;
1018 // Get name from title attribute for HTML abbr and acronym elements making it
1019 // a valid name from markup. Otherwise their name isn't picked up by recursive
1020 // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
1021 if (IsAbbreviation() && mContent->AsElement()->GetAttr(
1022 kNameSpaceID_None, nsGkAtoms::title, aName)) {
1023 aName.CompressWhitespace();
1026 return eNameOK;
1029 void HyperTextAccessible::Shutdown() {
1030 mOffsets.Clear();
1031 AccessibleWrap::Shutdown();
1034 bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) {
1035 const int32_t childIndex = aAccessible->IndexInParent();
1036 if (childIndex < static_cast<int32_t>(mOffsets.Length())) {
1037 mOffsets.RemoveLastElements(mOffsets.Length() - childIndex);
1040 return AccessibleWrap::RemoveChild(aAccessible);
1043 bool HyperTextAccessible::InsertChildAt(uint32_t aIndex,
1044 LocalAccessible* aChild) {
1045 if (aIndex < mOffsets.Length()) {
1046 mOffsets.RemoveLastElements(mOffsets.Length() - aIndex);
1049 return AccessibleWrap::InsertChildAt(aIndex, aChild);
1052 Relation HyperTextAccessible::RelationByType(RelationType aType) const {
1053 Relation rel = LocalAccessible::RelationByType(aType);
1055 switch (aType) {
1056 case RelationType::NODE_CHILD_OF:
1057 if (HasOwnContent() && mContent->IsMathMLElement()) {
1058 LocalAccessible* parent = LocalParent();
1059 if (parent) {
1060 nsIContent* parentContent = parent->GetContent();
1061 if (parentContent &&
1062 parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
1063 // Add a relation pointing to the parent <mroot>.
1064 rel.AppendTarget(parent);
1068 break;
1069 case RelationType::NODE_PARENT_OF:
1070 if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
1071 LocalAccessible* base = LocalChildAt(0);
1072 LocalAccessible* index = LocalChildAt(1);
1073 if (base && index) {
1074 // Append the <mroot> children in the order index, base.
1075 rel.AppendTarget(index);
1076 rel.AppendTarget(base);
1079 break;
1080 default:
1081 break;
1084 return rel;
1087 ////////////////////////////////////////////////////////////////////////////////
1088 // HyperTextAccessible public static
1090 nsresult HyperTextAccessible::ContentToRenderedOffset(
1091 nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
1092 if (!aFrame) {
1093 // Current frame not rendered -- this can happen if text is set on
1094 // something with display: none
1095 *aRenderedOffset = 0;
1096 return NS_OK;
1099 if (IsTextField()) {
1100 *aRenderedOffset = aContentOffset;
1101 return NS_OK;
1104 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
1105 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
1106 "Call on primary frame only");
1108 nsIFrame::RenderedText text =
1109 aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
1110 nsIFrame::TextOffsetType::OffsetsInContentText,
1111 nsIFrame::TrailingWhitespace::DontTrim);
1112 *aRenderedOffset = text.mOffsetWithinNodeRenderedText;
1114 return NS_OK;
1117 nsresult HyperTextAccessible::RenderedToContentOffset(
1118 nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
1119 if (IsTextField()) {
1120 *aContentOffset = aRenderedOffset;
1121 return NS_OK;
1124 *aContentOffset = 0;
1125 NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
1127 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
1128 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
1129 "Call on primary frame only");
1131 nsIFrame::RenderedText text =
1132 aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
1133 nsIFrame::TextOffsetType::OffsetsInRenderedText,
1134 nsIFrame::TrailingWhitespace::DontTrim);
1135 *aContentOffset = text.mOffsetWithinNodeText;
1137 return NS_OK;