no bug - Bumping Firefox l10n changesets r=release a=l10n-bump DONTBUILD CLOSED TREE
[gecko.git] / accessible / generic / HyperTextAccessible.cpp
blobeaeba0ccf9d0479c55f176eacd3c73b3db5fe726
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/SelectionMovementUtils.h"
39 #include "mozilla/dom/Element.h"
40 #include "mozilla/dom/HTMLBRElement.h"
41 #include "mozilla/dom/Selection.h"
42 #include "gfxSkipChars.h"
44 using namespace mozilla;
45 using namespace mozilla::a11y;
47 ////////////////////////////////////////////////////////////////////////////////
48 // HyperTextAccessible
49 ////////////////////////////////////////////////////////////////////////////////
51 HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
52 : AccessibleWrap(aNode, aDoc) {
53 mType = eHyperTextType;
54 mGenericTypes |= eHyperText;
57 role HyperTextAccessible::NativeRole() const {
58 a11y::role r = GetAccService()->MarkupRole(mContent);
59 if (r != roles::NOTHING) return r;
61 nsIFrame* frame = GetFrame();
62 if (frame && frame->IsInlineFrame()) return roles::TEXT;
64 return roles::TEXT_CONTAINER;
67 uint64_t HyperTextAccessible::NativeState() const {
68 uint64_t states = AccessibleWrap::NativeState();
70 if (IsEditable()) {
71 states |= states::EDITABLE;
73 } else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
74 // We want <article> to behave like a document in terms of readonly state.
75 states |= states::READONLY;
78 nsIFrame* frame = GetFrame();
79 if ((states & states::EDITABLE) || (frame && frame->IsSelectable(nullptr))) {
80 // If the accessible is editable the layout selectable state only disables
81 // mouse selection, but keyboard (shift+arrow) selection is still possible.
82 states |= states::SELECTABLE_TEXT;
85 return states;
88 bool HyperTextAccessible::IsEditable() const {
89 if (!mContent) {
90 return false;
92 return mContent->AsElement()->State().HasState(dom::ElementState::READWRITE);
95 uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
96 int32_t aNodeOffset,
97 bool aIsEndOffset) const {
98 if (!aNode) return 0;
100 uint32_t offset = 0;
101 nsINode* findNode = nullptr;
103 if (aNodeOffset == -1) {
104 findNode = aNode;
106 } else if (aNode->IsText()) {
107 // For text nodes, aNodeOffset comes in as a character offset
108 // Text offset will be added at the end, if we find the offset in this
109 // hypertext We want the "skipped" offset into the text (rendered text
110 // without the extra whitespace)
111 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
112 NS_ENSURE_TRUE(frame, 0);
114 nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
115 NS_ENSURE_SUCCESS(rv, 0);
117 findNode = aNode;
119 } else {
120 // findNode could be null if aNodeOffset == # of child nodes, which means
121 // one of two things:
122 // 1) there are no children, and the passed-in node is not mContent -- use
123 // parentContent for the node to find
124 // 2) there are no children and the passed-in node is mContent, which means
125 // we're an empty nsIAccessibleText
126 // 3) there are children and we're at the end of the children
128 findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
129 if (!findNode) {
130 if (aNodeOffset == 0) {
131 if (aNode == GetNode()) {
132 // Case #1: this accessible has no children and thus has empty text,
133 // we can only be at hypertext offset 0.
134 return 0;
137 // Case #2: there are no children, we're at this node.
138 findNode = aNode;
139 } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
140 // Case #3: we're after the last child, get next node to this one.
141 for (nsINode* tmpNode = aNode;
142 !findNode && tmpNode && tmpNode != mContent;
143 tmpNode = tmpNode->GetParent()) {
144 findNode = tmpNode->GetNextSibling();
150 // Get accessible for this findNode, or if that node isn't accessible, use the
151 // accessible for the next DOM node which has one (based on forward depth
152 // first search)
153 LocalAccessible* descendant = nullptr;
154 if (findNode) {
155 dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
156 if (brElement && brElement->IsPaddingForEmptyEditor()) {
157 // This <br> is the hacky "padding <br> element" used when there is no
158 // text in the editor.
159 return 0;
162 descendant = mDoc->GetAccessible(findNode);
163 if (!descendant && findNode->IsContent()) {
164 LocalAccessible* container = mDoc->GetContainerAccessible(findNode);
165 if (container) {
166 TreeWalker walker(container, findNode->AsContent(),
167 TreeWalker::eWalkContextTree);
168 descendant = walker.Next();
169 if (!descendant) descendant = container;
174 return TransformOffset(descendant, offset, aIsEndOffset);
177 uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant,
178 uint32_t aOffset,
179 bool aIsEndOffset) const {
180 // From the descendant, go up and get the immediate child of this hypertext.
181 uint32_t offset = aOffset;
182 LocalAccessible* descendant = aDescendant;
183 while (descendant) {
184 LocalAccessible* parent = descendant->LocalParent();
185 if (parent == this) return GetChildOffset(descendant) + offset;
187 // This offset no longer applies because the passed-in text object is not
188 // a child of the hypertext. This happens when there are nested hypertexts,
189 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
190 // to make it relative the hypertext.
191 // If the end offset is not supposed to be inclusive and the original point
192 // is not at 0 offset then the returned offset should be after an embedded
193 // character the original point belongs to.
194 if (aIsEndOffset) {
195 // Similar to our special casing in FindOffset, we add handling for
196 // bulleted lists here because PeekOffset returns the inner text node
197 // for a list when it should return the list bullet.
198 // We manually set the offset so the error doesn't propagate up.
199 if (offset == 0 && parent && parent->IsHTMLListItem() &&
200 descendant->LocalPrevSibling() &&
201 descendant->LocalPrevSibling() ==
202 parent->AsHTMLListItem()->Bullet()) {
203 offset = 0;
204 } else {
205 offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
207 } else {
208 offset = 0;
211 descendant = parent;
214 // If the given a11y point cannot be mapped into offset relative this
215 // hypertext offset then return length as fallback value.
216 return CharacterCount();
219 DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
220 // 0 offset is valid even if no children. In this case the associated editor
221 // is empty so return a DOM point for editor root element.
222 if (aOffset == 0) {
223 RefPtr<EditorBase> editorBase = GetEditor();
224 if (editorBase) {
225 if (editorBase->IsEmpty()) {
226 return DOMPoint(editorBase->GetRoot(), 0);
231 int32_t childIdx = GetChildIndexAtOffset(aOffset);
232 if (childIdx == -1) return DOMPoint();
234 LocalAccessible* child = LocalChildAt(childIdx);
235 int32_t innerOffset = aOffset - GetChildOffset(childIdx);
237 // A text leaf case.
238 if (child->IsTextLeaf()) {
239 // The point is inside the text node. This is always true for any text leaf
240 // except a last child one. See assertion below.
241 if (aOffset < GetChildOffset(childIdx + 1)) {
242 nsIContent* content = child->GetContent();
243 int32_t idx = 0;
244 if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
245 innerOffset, &idx))) {
246 return DOMPoint();
249 return DOMPoint(content, idx);
252 // Set the DOM point right after the text node.
253 MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
254 innerOffset = 1;
257 // Case of embedded object. The point is either before or after the element.
258 NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
259 nsINode* node = child->GetNode();
260 nsINode* parentNode = node->GetParentNode();
261 return parentNode ? DOMPoint(parentNode,
262 parentNode->ComputeIndexOf_Deprecated(node) +
263 innerOffset)
264 : DOMPoint();
267 already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() {
268 RefPtr<AccAttributes> attributes = new AccAttributes();
270 TextAttrsMgr textAttrsMgr(this);
271 textAttrsMgr.GetAttributes(attributes);
272 return attributes.forget();
275 void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) {
276 // Add MathML xmlroles based on the position inside the parent.
277 LocalAccessible* parent = LocalParent();
278 if (parent) {
279 switch (parent->Role()) {
280 case roles::MATHML_CELL:
281 case roles::MATHML_ENCLOSED:
282 case roles::MATHML_ERROR:
283 case roles::MATHML_MATH:
284 case roles::MATHML_ROW:
285 case roles::MATHML_SQUARE_ROOT:
286 case roles::MATHML_STYLE:
287 if (Role() == roles::MATHML_OPERATOR) {
288 // This is an operator inside an <mrow> (or an inferred <mrow>).
289 // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
290 // XXX We should probably do something similar for MATHML_FENCED, but
291 // operators do not appear in the accessible tree. See bug 1175747.
292 nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
293 if (mathMLFrame) {
294 nsEmbellishData embellishData;
295 mathMLFrame->GetEmbellishData(embellishData);
296 if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
297 if (!LocalPrevSibling()) {
298 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
299 nsGkAtoms::open_fence);
300 } else if (!LocalNextSibling()) {
301 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
302 nsGkAtoms::close_fence);
305 if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
306 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
307 nsGkAtoms::separator_);
311 break;
312 case roles::MATHML_FRACTION:
313 aAttributes->SetAttribute(
314 nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator
315 : nsGkAtoms::denominator);
316 break;
317 case roles::MATHML_ROOT:
318 aAttributes->SetAttribute(
319 nsGkAtoms::xmlroles,
320 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
321 break;
322 case roles::MATHML_SUB:
323 aAttributes->SetAttribute(
324 nsGkAtoms::xmlroles,
325 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
326 break;
327 case roles::MATHML_SUP:
328 aAttributes->SetAttribute(
329 nsGkAtoms::xmlroles,
330 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
331 break;
332 case roles::MATHML_SUB_SUP: {
333 int32_t index = IndexInParent();
334 aAttributes->SetAttribute(
335 nsGkAtoms::xmlroles,
336 index == 0
337 ? nsGkAtoms::base
338 : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
339 } break;
340 case roles::MATHML_UNDER:
341 aAttributes->SetAttribute(
342 nsGkAtoms::xmlroles,
343 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
344 break;
345 case roles::MATHML_OVER:
346 aAttributes->SetAttribute(
347 nsGkAtoms::xmlroles,
348 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
349 break;
350 case roles::MATHML_UNDER_OVER: {
351 int32_t index = IndexInParent();
352 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
353 index == 0
354 ? nsGkAtoms::base
355 : (index == 1 ? nsGkAtoms::underscript
356 : nsGkAtoms::overscript));
357 } break;
358 case roles::MATHML_MULTISCRIPTS: {
359 // Get the <multiscripts> base.
360 nsIContent* child;
361 bool baseFound = false;
362 for (child = parent->GetContent()->GetFirstChild(); child;
363 child = child->GetNextSibling()) {
364 if (child->IsMathMLElement()) {
365 baseFound = true;
366 break;
369 if (baseFound) {
370 nsIContent* content = GetContent();
371 if (child == content) {
372 // We are the base.
373 aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base);
374 } else {
375 // Browse the list of scripts to find us and determine our type.
376 bool postscript = true;
377 bool subscript = true;
378 for (child = child->GetNextSibling(); child;
379 child = child->GetNextSibling()) {
380 if (!child->IsMathMLElement()) continue;
381 if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) {
382 postscript = false;
383 subscript = true;
384 continue;
386 if (child == content) {
387 if (postscript) {
388 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
389 subscript ? nsGkAtoms::subscript
390 : nsGkAtoms::superscript);
391 } else {
392 aAttributes->SetAttribute(nsGkAtoms::xmlroles,
393 subscript
394 ? nsGkAtoms::presubscript
395 : nsGkAtoms::presuperscript);
397 break;
399 subscript = !subscript;
403 } break;
404 default:
405 break;
410 already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() {
411 RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes();
413 // 'formatting' attribute is deprecated, 'display' attribute should be
414 // instead.
415 nsIFrame* frame = GetFrame();
416 if (frame && frame->IsBlockFrame()) {
417 attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block);
420 if (FocusMgr()->IsFocused(this)) {
421 int32_t lineNumber = CaretLineNumber();
422 if (lineNumber >= 1) {
423 attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber);
427 if (HasOwnContent()) {
428 GetAccService()->MarkupAttributes(this, attributes);
429 if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
432 return attributes.forget();
435 int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
436 uint32_t aCoordType) {
437 nsIFrame* hyperFrame = GetFrame();
438 if (!hyperFrame) return -1;
440 LayoutDeviceIntPoint coords =
441 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
443 nsPresContext* presContext = mDoc->PresContext();
444 nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
445 coords, presContext->AppUnitsPerDevPixel());
447 nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
448 if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) {
449 return -1; // Not found
452 nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
453 coordsInAppUnits.y - frameScreenRect.Y());
455 // Go through the frames to check if each one has the point.
456 // When one does, add up the character offsets until we have a match
458 // We have an point in an accessible child of this, now we need to add up the
459 // offsets before it to what we already have
460 int32_t offset = 0;
461 uint32_t childCount = ChildCount();
462 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
463 LocalAccessible* childAcc = mChildren[childIdx];
465 nsIFrame* primaryFrame = childAcc->GetFrame();
466 NS_ENSURE_TRUE(primaryFrame, -1);
468 nsIFrame* frame = primaryFrame;
469 while (frame) {
470 nsIContent* content = frame->GetContent();
471 NS_ENSURE_TRUE(content, -1);
472 nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
473 nsSize frameSize = frame->GetSize();
474 if (pointInFrame.x < frameSize.width &&
475 pointInFrame.y < frameSize.height) {
476 // Finished
477 if (frame->IsTextFrame()) {
478 nsIFrame::ContentOffsets contentOffsets =
479 frame->GetContentOffsetsFromPointExternal(
480 pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
481 if (contentOffsets.IsNull() || contentOffsets.content != content) {
482 return -1; // Not found
484 uint32_t addToOffset;
485 nsresult rv = ContentToRenderedOffset(
486 primaryFrame, contentOffsets.offset, &addToOffset);
487 NS_ENSURE_SUCCESS(rv, -1);
488 offset += addToOffset;
490 return offset;
492 frame = frame->GetNextContinuation();
495 offset += nsAccUtils::TextLength(childAcc);
498 return -1; // Not found
501 already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const {
502 if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
503 // If we're inside an editable container, then return that container's
504 // editor
505 LocalAccessible* ancestor = LocalParent();
506 while (ancestor) {
507 HyperTextAccessible* hyperText = ancestor->AsHyperText();
508 if (hyperText) {
509 // Recursion will stop at container doc because it has its own impl
510 // of GetEditor()
511 return hyperText->GetEditor();
514 ancestor = ancestor->LocalParent();
517 return nullptr;
520 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
521 nsCOMPtr<nsIEditingSession> editingSession;
522 docShell->GetEditingSession(getter_AddRefs(editingSession));
523 if (!editingSession) return nullptr; // No editing session interface
525 dom::Document* docNode = mDoc->DocumentNode();
526 RefPtr<HTMLEditor> htmlEditor =
527 editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
528 return htmlEditor.forget();
532 * =================== Caret & Selection ======================
535 nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
536 int32_t aEndPos) {
537 // Before setting the selection range, we need to ensure that the editor
538 // is initialized. (See bug 804927.)
539 // Otherwise, it's possible that lazy editor initialization will override
540 // the selection we set here and leave the caret at the end of the text.
541 // By calling GetEditor here, we ensure that editor initialization is
542 // completed before we set the selection.
543 RefPtr<EditorBase> editorBase = GetEditor();
545 bool isFocusable = InteractiveState() & states::FOCUSABLE;
547 // If accessible is focusable then focus it before setting the selection to
548 // neglect control's selection changes on focus if any (for example, inputs
549 // that do select all on focus).
550 // some input controls
551 if (isFocusable) TakeFocus();
553 RefPtr<dom::Selection> domSel = DOMSelection();
554 NS_ENSURE_STATE(domSel);
556 // Set up the selection.
557 domSel->RemoveAllRanges(IgnoreErrors());
558 SetSelectionBoundsAt(0, aStartPos, aEndPos);
560 // Make sure it is visible
561 domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
562 ScrollAxis(), ScrollAxis(),
563 dom::Selection::SCROLL_FOR_CARET_MOVE |
564 dom::Selection::SCROLL_OVERFLOW_HIDDEN);
566 // When selection is done, move the focus to the selection if accessible is
567 // not focusable. That happens when selection is set within hypertext
568 // accessible.
569 if (isFocusable) return NS_OK;
571 nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
572 if (DOMFocusManager) {
573 NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
574 dom::Document* docNode = mDoc->DocumentNode();
575 NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
576 nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
577 RefPtr<dom::Element> result;
578 DOMFocusManager->MoveFocus(
579 window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
580 nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
583 return NS_OK;
586 int32_t HyperTextAccessible::CaretOffset() const {
587 // Not focused focusable accessible except document accessible doesn't have
588 // a caret.
589 if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
590 (InteractiveState() & states::FOCUSABLE)) {
591 return -1;
594 // Check cached value.
595 int32_t caretOffset = -1;
596 HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
598 // Use cached value if it corresponds to this accessible.
599 if (caretOffset != -1) {
600 if (text == this) return caretOffset;
602 nsINode* textNode = text->GetNode();
603 // Ignore offset if cached accessible isn't a text leaf.
604 if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) {
605 return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
609 // No caret if the focused node is not inside this DOM node and this DOM node
610 // is not inside of focused node.
611 FocusManager::FocusDisposition focusDisp =
612 FocusMgr()->IsInOrContainsFocus(this);
613 if (focusDisp == FocusManager::eNone) return -1;
615 // Turn the focus node and offset of the selection into caret hypretext
616 // offset.
617 dom::Selection* domSel = DOMSelection();
618 NS_ENSURE_TRUE(domSel, -1);
620 nsINode* focusNode = domSel->GetFocusNode();
621 uint32_t focusOffset = domSel->FocusOffset();
623 // No caret if this DOM node is inside of focused node but the selection's
624 // focus point is not inside of this DOM node.
625 if (focusDisp == FocusManager::eContainedByFocus) {
626 nsINode* resultNode =
627 nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
629 nsINode* thisNode = GetNode();
630 if (resultNode != thisNode &&
631 !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) {
632 return -1;
636 return DOMPointToOffset(focusNode, focusOffset);
639 int32_t HyperTextAccessible::CaretLineNumber() {
640 // Provide the line number for the caret, relative to the
641 // currently focused node. Use a 1-based index
642 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
643 if (!frameSelection) return -1;
645 dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
646 if (!domSel) return -1;
648 nsINode* caretNode = domSel->GetFocusNode();
649 if (!caretNode || !caretNode->IsContent()) return -1;
651 nsIContent* caretContent = caretNode->AsContent();
652 if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
654 uint32_t caretOffset = domSel->FocusOffset();
655 CaretAssociationHint hint = frameSelection->GetHint();
656 nsIFrame* caretFrame = SelectionMovementUtils::GetFrameForNodeOffset(
657 caretContent, caretOffset, hint);
658 NS_ENSURE_TRUE(caretFrame, -1);
660 AutoAssertNoDomMutations guard; // The nsILineIterators below will break if
661 // the DOM is modified while they're in use!
662 int32_t lineNumber = 1;
663 nsILineIterator* lineIterForCaret = nullptr;
664 nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
665 while (caretFrame) {
666 if (hyperTextContent == caretFrame->GetContent()) {
667 return lineNumber; // Must be in a single line hyper text, there is no
668 // line iterator
670 nsContainerFrame* parentFrame = caretFrame->GetParent();
671 if (!parentFrame) break;
673 // Add lines for the sibling frames before the caret
674 nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
675 while (sibling && sibling != caretFrame) {
676 nsILineIterator* lineIterForSibling = sibling->GetLineIterator();
677 if (lineIterForSibling) {
678 // For the frames before that grab all the lines
679 int32_t addLines = lineIterForSibling->GetNumLines();
680 lineNumber += addLines;
682 sibling = sibling->GetNextSibling();
685 // Get the line number relative to the container with lines
686 if (!lineIterForCaret) { // Add the caret line just once
687 lineIterForCaret = parentFrame->GetLineIterator();
688 if (lineIterForCaret) {
689 // Ancestor of caret
690 int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
691 lineNumber += addLines;
695 caretFrame = parentFrame;
698 MOZ_ASSERT_UNREACHABLE(
699 "DOM ancestry had this hypertext but frame ancestry didn't");
700 return lineNumber;
703 LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
704 *aWidget = nullptr;
706 RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
707 NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
709 bool isVisible = caret->IsVisible();
710 if (!isVisible) return LayoutDeviceIntRect();
712 nsRect rect;
713 nsIFrame* frame = caret->GetGeometry(&rect);
714 if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
716 PresShell* presShell = mDoc->PresShellPtr();
717 // Transform rect to be relative to the root frame.
718 nsIFrame* rootFrame = presShell->GetRootFrame();
719 rect = nsLayoutUtils::TransformFrameRectToAncestor(frame, rect, rootFrame);
720 // We need to inverse translate with the offset of the edge of the visual
721 // viewport from top edge of the layout viewport.
722 nsPoint viewportOffset = presShell->GetVisualViewportOffset() -
723 presShell->GetLayoutViewportOffset();
724 rect.MoveBy(-viewportOffset);
725 // We need to take into account a non-1 resolution set on the presshell.
726 // This happens with async pinch zooming. Here we scale the bounds before
727 // adding the screen-relative offset.
728 rect.ScaleRoundOut(presShell->GetResolution());
729 // Now we need to put the rect in absolute screen coords.
730 nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
731 rect.MoveBy(rootScreenRect.TopLeft());
732 // Finally, convert from app units.
733 auto caretRect = LayoutDeviceIntRect::FromAppUnitsToNearest(
734 rect, presShell->GetPresContext()->AppUnitsPerDevPixel());
736 // Correct for character size, so that caret always matches the size of
737 // the character. This is important for font size transitions, and is
738 // necessary because the Gecko caret uses the previous character's size as
739 // the user moves forward in the text by character.
740 int32_t caretOffset = CaretOffset();
741 if (NS_WARN_IF(caretOffset == -1)) {
742 // The caret offset will be -1 if this Accessible isn't focused. Note that
743 // the DOM node contaning the caret might be focused, but the Accessible
744 // might not be; e.g. due to an autocomplete popup suggestion having a11y
745 // focus.
746 return LayoutDeviceIntRect();
748 LayoutDeviceIntRect charRect = CharBounds(
749 caretOffset, nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
750 if (!charRect.IsEmpty()) {
751 caretRect.SetTopEdge(charRect.Y());
754 *aWidget = frame->GetNearestWidget();
755 return caretRect;
758 void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
759 nsTArray<nsRange*>* aRanges) {
760 if (IsDoc() && !AsDoc()->HasLoadState(DocAccessible::eTreeConstructed)) {
761 // Rarely, a client query can be handled after a DocAccessible is created
762 // but before the initial tree is constructed, since DoInitialUpdate happens
763 // during a refresh tick. In that case, there might be a DOM selection, but
764 // we can't use it. We will crash if we try due to mContent being null, etc.
765 // This should only happen in the parent process because we should never
766 // try to push the cache in a content process before the initial tree is
767 // constructed.
768 MOZ_ASSERT(XRE_IsParentProcess(), "Query before DoInitialUpdate");
769 return;
771 // Ignore selection if it is not visible.
772 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
773 if (!frameSelection || frameSelection->GetDisplaySelection() <=
774 nsISelectionController::SELECTION_HIDDEN) {
775 return;
778 dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
779 if (!domSel) return;
781 nsINode* startNode = GetNode();
783 RefPtr<EditorBase> editorBase = GetEditor();
784 if (editorBase) {
785 startNode = editorBase->GetRoot();
788 if (!startNode) return;
790 uint32_t childCount = startNode->GetChildCount();
791 nsresult rv = domSel->GetDynamicRangesForIntervalArray(
792 startNode, 0, startNode, childCount, true, aRanges);
793 NS_ENSURE_SUCCESS_VOID(rv);
795 // Remove collapsed ranges
796 aRanges->RemoveElementsBy(
797 [](const auto& range) { return range->Collapsed(); });
800 int32_t HyperTextAccessible::SelectionCount() {
801 nsTArray<nsRange*> ranges;
802 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
803 return ranges.Length();
806 bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
807 int32_t* aStartOffset,
808 int32_t* aEndOffset) {
809 *aStartOffset = *aEndOffset = 0;
811 nsTArray<nsRange*> ranges;
812 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
814 uint32_t rangeCount = ranges.Length();
815 if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount)) {
816 return false;
819 nsRange* range = ranges[aSelectionNum];
821 // Get start and end points.
822 nsINode* startNode = range->GetStartContainer();
823 nsINode* endNode = range->GetEndContainer();
824 uint32_t startOffset = range->StartOffset();
825 uint32_t endOffset = range->EndOffset();
827 // Make sure start is before end, by swapping DOM points. This occurs when
828 // the user selects backwards in the text.
829 const Maybe<int32_t> order =
830 nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset);
832 if (!order) {
833 MOZ_ASSERT_UNREACHABLE();
834 return false;
837 if (*order < 0) {
838 std::swap(startNode, endNode);
839 std::swap(startOffset, endOffset);
842 if (!startNode->IsInclusiveDescendantOf(mContent)) {
843 *aStartOffset = 0;
844 } else {
845 *aStartOffset =
846 DOMPointToOffset(startNode, AssertedCast<int32_t>(startOffset));
849 if (!endNode->IsInclusiveDescendantOf(mContent)) {
850 *aEndOffset = CharacterCount();
851 } else {
852 *aEndOffset =
853 DOMPointToOffset(endNode, AssertedCast<int32_t>(endOffset), true);
855 return true;
858 bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
859 RefPtr<dom::Selection> domSel = DOMSelection();
860 if (!domSel) return false;
862 if (aSelectionNum < 0 ||
863 aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) {
864 return false;
867 const RefPtr<nsRange> range{
868 domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))};
869 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
870 IgnoreErrors());
871 return true;
874 void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
875 int32_t aEndOffset,
876 uint32_t aCoordinateType,
877 int32_t aX, int32_t aY) {
878 nsIFrame* frame = GetFrame();
879 if (!frame) return;
881 LayoutDeviceIntPoint coords =
882 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
884 RefPtr<nsRange> domRange = nsRange::Create(mContent);
885 TextRange range(this, this, aStartOffset, this, aEndOffset);
886 if (!range.AssignDOMRange(domRange)) {
887 return;
890 nsPresContext* presContext = frame->PresContext();
891 nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits(
892 coords, presContext->AppUnitsPerDevPixel());
894 bool initialScrolled = false;
895 nsIFrame* parentFrame = frame;
896 while ((parentFrame = parentFrame->GetParent())) {
897 nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
898 if (scrollableFrame) {
899 if (!initialScrolled) {
900 // Scroll substring to the given point. Turn the point into percents
901 // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
902 nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
903 nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
904 nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
906 nsSize size(parentFrame->GetSize());
908 // avoid divide by zero
909 size.width = size.width ? size.width : 1;
910 size.height = size.height ? size.height : 1;
912 int16_t hPercent = offsetPointX * 100 / size.width;
913 int16_t vPercent = offsetPointY * 100 / size.height;
915 nsresult rv = nsCoreUtils::ScrollSubstringTo(
916 frame, domRange,
917 ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always),
918 ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always));
919 if (NS_FAILED(rv)) return;
921 initialScrolled = true;
922 } else {
923 // Substring was scrolled to the given point already inside its closest
924 // scrollable area. If there are nested scrollable areas then make
925 // sure we scroll lower areas to the given point inside currently
926 // traversed scrollable area.
927 nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
930 frame = parentFrame;
934 void HyperTextAccessible::SelectionRanges(
935 nsTArray<a11y::TextRange>* aRanges) const {
936 dom::Selection* sel = DOMSelection();
937 if (!sel) {
938 return;
941 TextRange::TextRangesFromSelection(sel, aRanges);
944 void HyperTextAccessible::ReplaceText(const nsAString& aText) {
945 if (aText.Length() == 0) {
946 DeleteText(0, CharacterCount());
947 return;
950 SetSelectionRange(0, CharacterCount());
952 RefPtr<EditorBase> editorBase = GetEditor();
953 if (!editorBase) {
954 return;
957 DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
958 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text");
961 void HyperTextAccessible::InsertText(const nsAString& aText,
962 int32_t aPosition) {
963 RefPtr<EditorBase> editorBase = GetEditor();
964 if (editorBase) {
965 SetSelectionRange(aPosition, aPosition);
966 DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText);
967 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the text");
971 void HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) {
972 RefPtr<EditorBase> editorBase = GetEditor();
973 if (editorBase) {
974 SetSelectionRange(aStartPos, aEndPos);
975 editorBase->Copy();
979 void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) {
980 RefPtr<EditorBase> editorBase = GetEditor();
981 if (editorBase) {
982 SetSelectionRange(aStartPos, aEndPos);
983 editorBase->Cut();
987 void HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) {
988 RefPtr<EditorBase> editorBase = GetEditor();
989 if (!editorBase) {
990 return;
992 SetSelectionRange(aStartPos, aEndPos);
993 DebugOnly<nsresult> rv =
994 editorBase->DeleteSelectionAsAction(nsIEditor::eNone, nsIEditor::eStrip);
995 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to delete text");
998 void HyperTextAccessible::PasteText(int32_t aPosition) {
999 RefPtr<EditorBase> editorBase = GetEditor();
1000 if (editorBase) {
1001 SetSelectionRange(aPosition, aPosition);
1002 editorBase->PasteAsAction(nsIClipboard::kGlobalClipboard,
1003 EditorBase::DispatchPasteEvent::Yes);
1007 ////////////////////////////////////////////////////////////////////////////////
1008 // LocalAccessible public
1010 // LocalAccessible protected
1011 ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
1012 // Check @alt attribute for invalid img elements.
1013 if (mContent->IsHTMLElement(nsGkAtoms::img)) {
1014 mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName);
1015 if (!aName.IsEmpty()) return eNameOK;
1018 ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
1019 if (!aName.IsEmpty()) return nameFlag;
1021 // Get name from title attribute for HTML abbr and acronym elements making it
1022 // a valid name from markup. Otherwise their name isn't picked up by recursive
1023 // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
1024 if (IsAbbreviation() && mContent->AsElement()->GetAttr(
1025 kNameSpaceID_None, nsGkAtoms::title, aName)) {
1026 aName.CompressWhitespace();
1029 return eNameOK;
1032 void HyperTextAccessible::Shutdown() {
1033 mOffsets.Clear();
1034 AccessibleWrap::Shutdown();
1037 bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) {
1038 const int32_t childIndex = aAccessible->IndexInParent();
1039 if (childIndex < static_cast<int32_t>(mOffsets.Length())) {
1040 mOffsets.RemoveLastElements(mOffsets.Length() - childIndex);
1043 return AccessibleWrap::RemoveChild(aAccessible);
1046 bool HyperTextAccessible::InsertChildAt(uint32_t aIndex,
1047 LocalAccessible* aChild) {
1048 if (aIndex < mOffsets.Length()) {
1049 mOffsets.RemoveLastElements(mOffsets.Length() - aIndex);
1052 return AccessibleWrap::InsertChildAt(aIndex, aChild);
1055 Relation HyperTextAccessible::RelationByType(RelationType aType) const {
1056 Relation rel = LocalAccessible::RelationByType(aType);
1058 switch (aType) {
1059 case RelationType::NODE_CHILD_OF:
1060 if (HasOwnContent() && mContent->IsMathMLElement()) {
1061 LocalAccessible* parent = LocalParent();
1062 if (parent) {
1063 nsIContent* parentContent = parent->GetContent();
1064 if (parentContent &&
1065 parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
1066 // Add a relation pointing to the parent <mroot>.
1067 rel.AppendTarget(parent);
1071 break;
1072 case RelationType::NODE_PARENT_OF:
1073 if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
1074 LocalAccessible* base = LocalChildAt(0);
1075 LocalAccessible* index = LocalChildAt(1);
1076 if (base && index) {
1077 // Append the <mroot> children in the order index, base.
1078 rel.AppendTarget(index);
1079 rel.AppendTarget(base);
1082 break;
1083 default:
1084 break;
1087 return rel;
1090 ////////////////////////////////////////////////////////////////////////////////
1091 // HyperTextAccessible public static
1093 nsresult HyperTextAccessible::ContentToRenderedOffset(
1094 nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
1095 if (!aFrame) {
1096 // Current frame not rendered -- this can happen if text is set on
1097 // something with display: none
1098 *aRenderedOffset = 0;
1099 return NS_OK;
1102 if (IsTextField()) {
1103 *aRenderedOffset = aContentOffset;
1104 return NS_OK;
1107 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
1108 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
1109 "Call on primary frame only");
1111 nsIFrame::RenderedText text =
1112 aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
1113 nsIFrame::TextOffsetType::OffsetsInContentText,
1114 nsIFrame::TrailingWhitespace::DontTrim);
1115 *aRenderedOffset = text.mOffsetWithinNodeRenderedText;
1117 return NS_OK;
1120 nsresult HyperTextAccessible::RenderedToContentOffset(
1121 nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
1122 if (IsTextField()) {
1123 *aContentOffset = aRenderedOffset;
1124 return NS_OK;
1127 *aContentOffset = 0;
1128 NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
1130 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
1131 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
1132 "Call on primary frame only");
1134 nsIFrame::RenderedText text =
1135 aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
1136 nsIFrame::TextOffsetType::OffsetsInRenderedText,
1137 nsIFrame::TrailingWhitespace::DontTrim);
1138 *aContentOffset = text.mOffsetWithinNodeText;
1140 return NS_OK;