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 "LocalAccessible-inl.h"
10 #include "nsAccessibilityService.h"
11 #include "nsAccessiblePivot.h"
12 #include "nsIAccessibleTypes.h"
13 #include "DocAccessible.h"
14 #include "HTMLListAccessible.h"
19 #include "TextAttrs.h"
20 #include "TextRange.h"
21 #include "TreeWalker.h"
24 #include "nsContentUtils.h"
26 #include "nsFocusManager.h"
27 #include "nsIEditingSession.h"
28 #include "nsContainerFrame.h"
29 #include "nsFrameSelection.h"
30 #include "nsILineIterator.h"
31 #include "nsIInterfaceRequestorUtils.h"
32 #include "nsPersistentProperties.h"
33 #include "nsIScrollableFrame.h"
34 #include "nsIMathMLFrame.h"
36 #include "nsTextFragment.h"
37 #include "mozilla/Assertions.h"
38 #include "mozilla/BinarySearch.h"
39 #include "mozilla/EventStates.h"
40 #include "mozilla/HTMLEditor.h"
41 #include "mozilla/MathAlgorithms.h"
42 #include "mozilla/PresShell.h"
43 #include "mozilla/StaticPrefs_layout.h"
44 #include "mozilla/TextEditor.h"
45 #include "mozilla/dom/Element.h"
46 #include "mozilla/dom/HTMLBRElement.h"
47 #include "mozilla/dom/HTMLHeadingElement.h"
48 #include "mozilla/dom/Selection.h"
49 #include "gfxSkipChars.h"
52 using namespace mozilla
;
53 using namespace mozilla::a11y
;
56 * This class is used in HyperTextAccessible to search for paragraph
59 class ParagraphBoundaryRule
: public PivotRule
{
61 explicit ParagraphBoundaryRule(LocalAccessible
* aAnchor
,
62 uint32_t aAnchorTextoffset
,
63 nsDirection aDirection
,
64 bool aSkipAnchorSubtree
= false)
66 mAnchorTextOffset(aAnchorTextoffset
),
67 mDirection(aDirection
),
68 mSkipAnchorSubtree(aSkipAnchorSubtree
),
69 mLastMatchTextOffset(0) {}
71 virtual uint16_t Match(const AccessibleOrProxy
& aAccOrProxy
) override
{
72 MOZ_ASSERT(aAccOrProxy
.IsAccessible());
73 LocalAccessible
* acc
= aAccOrProxy
.AsAccessible();
74 if (acc
->IsOuterDoc()) {
75 // The child document might be remote and we can't (and don't want to)
76 // handle remote documents. Also, iframes are inline anyway and thus
77 // can't be paragraph boundaries. Therefore, skip this unconditionally.
78 return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
81 uint16_t result
= nsIAccessibleTraversalRule::FILTER_IGNORE
;
82 if (mSkipAnchorSubtree
&& acc
== mAnchor
) {
83 result
|= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
86 // First, deal with the case that we encountered a line break, for example,
87 // a br in a paragraph.
88 if (acc
->Role() == roles::WHITESPACE
) {
89 result
|= nsIAccessibleTraversalRule::FILTER_MATCH
;
93 // Now, deal with the case that we encounter a new block level accessible.
94 // This also means a new paragraph boundary start.
95 nsIFrame
* frame
= acc
->GetFrame();
96 if (frame
&& frame
->IsBlockFrame()) {
97 result
|= nsIAccessibleTraversalRule::FILTER_MATCH
;
101 // A text leaf can contain a line break if it's pre-formatted text.
102 if (acc
->IsTextLeaf()) {
106 if (mDirection
== eDirPrevious
) {
107 if (acc
== mAnchor
&& mAnchorTextOffset
== 0) {
108 // We're already at the start of this node, so there can be no line
112 // If we began on a line break, we don't want to match it, so search
113 // from 1 before our anchor offset.
115 name
.RFindChar('\n', acc
== mAnchor
? mAnchorTextOffset
- 1 : -1);
117 offset
= name
.FindChar('\n', acc
== mAnchor
? mAnchorTextOffset
: 0);
121 mLastMatchTextOffset
= offset
;
122 result
|= nsIAccessibleTraversalRule::FILTER_MATCH
;
129 // This is only valid if the last match was a text leaf. It returns the
130 // offset of the line break character in that text leaf.
131 uint32_t GetLastMatchTextOffset() { return mLastMatchTextOffset
; }
134 LocalAccessible
* mAnchor
;
135 uint32_t mAnchorTextOffset
;
136 nsDirection mDirection
;
137 bool mSkipAnchorSubtree
;
138 uint32_t mLastMatchTextOffset
;
142 * This class is used in HyperTextAccessible::FindParagraphStartOffset to
143 * search forward exactly one step from a match found by the above.
144 * It should only be initialized with a boundary, and it will skip that
145 * boundary's sub tree if it is a block element boundary.
147 class SkipParagraphBoundaryRule
: public PivotRule
{
149 explicit SkipParagraphBoundaryRule(AccessibleOrProxy
& aBoundary
)
150 : mBoundary(aBoundary
) {}
152 virtual uint16_t Match(const AccessibleOrProxy
& aAccOrProxy
) override
{
153 MOZ_ASSERT(aAccOrProxy
.IsAccessible());
154 // If matching the boundary, skip its sub tree.
155 if (aAccOrProxy
== mBoundary
) {
156 return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
158 return nsIAccessibleTraversalRule::FILTER_MATCH
;
162 AccessibleOrProxy
& mBoundary
;
165 ////////////////////////////////////////////////////////////////////////////////
166 // HyperTextAccessible
167 ////////////////////////////////////////////////////////////////////////////////
169 HyperTextAccessible::HyperTextAccessible(nsIContent
* aNode
, DocAccessible
* aDoc
)
170 : AccessibleWrap(aNode
, aDoc
) {
171 mType
= eHyperTextType
;
172 mGenericTypes
|= eHyperText
;
175 role
HyperTextAccessible::NativeRole() const {
176 a11y::role r
= GetAccService()->MarkupRole(mContent
);
177 if (r
!= roles::NOTHING
) return r
;
179 nsIFrame
* frame
= GetFrame();
180 if (frame
&& frame
->IsInlineFrame()) return roles::TEXT
;
182 return roles::TEXT_CONTAINER
;
185 uint64_t HyperTextAccessible::NativeState() const {
186 uint64_t states
= AccessibleWrap::NativeState();
188 if (mContent
->AsElement()->State().HasState(NS_EVENT_STATE_READWRITE
)) {
189 states
|= states::EDITABLE
;
191 } else if (mContent
->IsHTMLElement(nsGkAtoms::article
)) {
192 // We want <article> to behave like a document in terms of readonly state.
193 states
|= states::READONLY
;
196 nsIFrame
* frame
= GetFrame();
197 if ((states
& states::EDITABLE
) || (frame
&& frame
->IsSelectable(nullptr))) {
198 // If the accessible is editable the layout selectable state only disables
199 // mouse selection, but keyboard (shift+arrow) selection is still possible.
200 states
|= states::SELECTABLE_TEXT
;
206 nsIntRect
HyperTextAccessible::GetBoundsInFrame(nsIFrame
* aFrame
,
207 uint32_t aStartRenderedOffset
,
208 uint32_t aEndRenderedOffset
) {
209 nsPresContext
* presContext
= mDoc
->PresContext();
210 if (!aFrame
->IsTextFrame()) {
211 return aFrame
->GetScreenRectInAppUnits().ToNearestPixels(
212 presContext
->AppUnitsPerDevPixel());
215 // Substring must be entirely within the same text node.
216 int32_t startContentOffset
, endContentOffset
;
217 nsresult rv
= RenderedToContentOffset(aFrame
, aStartRenderedOffset
,
218 &startContentOffset
);
219 NS_ENSURE_SUCCESS(rv
, nsIntRect());
220 rv
= RenderedToContentOffset(aFrame
, aEndRenderedOffset
, &endContentOffset
);
221 NS_ENSURE_SUCCESS(rv
, nsIntRect());
224 int32_t startContentOffsetInFrame
;
225 // Get the right frame continuation -- not really a child, but a sibling of
226 // the primary frame passed in
227 rv
= aFrame
->GetChildFrameContainingOffset(
228 startContentOffset
, false, &startContentOffsetInFrame
, &frame
);
229 NS_ENSURE_SUCCESS(rv
, nsIntRect());
232 while (frame
&& startContentOffset
< endContentOffset
) {
233 // Start with this frame's screen rect, which we will shrink based on
234 // the substring we care about within it. We will then add that frame to
235 // the total screenRect we are returning.
236 nsRect frameScreenRect
= frame
->GetScreenRectInAppUnits();
238 // Get the length of the substring in this frame that we want the bounds for
239 int32_t startFrameTextOffset
, endFrameTextOffset
;
240 frame
->GetOffsets(startFrameTextOffset
, endFrameTextOffset
);
241 int32_t frameTotalTextLength
= endFrameTextOffset
- startFrameTextOffset
;
242 int32_t seekLength
= endContentOffset
- startContentOffset
;
243 int32_t frameSubStringLength
=
244 std::min(frameTotalTextLength
- startContentOffsetInFrame
, seekLength
);
246 // Add the point where the string starts to the frameScreenRect
247 nsPoint frameTextStartPoint
;
248 rv
= frame
->GetPointFromOffset(startContentOffset
, &frameTextStartPoint
);
249 NS_ENSURE_SUCCESS(rv
, nsIntRect());
251 // Use the point for the end offset to calculate the width
252 nsPoint frameTextEndPoint
;
253 rv
= frame
->GetPointFromOffset(startContentOffset
+ frameSubStringLength
,
255 NS_ENSURE_SUCCESS(rv
, nsIntRect());
257 frameScreenRect
.SetRectX(
258 frameScreenRect
.X() +
259 std::min(frameTextStartPoint
.x
, frameTextEndPoint
.x
),
260 mozilla::Abs(frameTextStartPoint
.x
- frameTextEndPoint
.x
));
262 screenRect
.UnionRect(frameScreenRect
, screenRect
);
264 // Get ready to loop back for next frame continuation
265 startContentOffset
+= frameSubStringLength
;
266 startContentOffsetInFrame
= 0;
267 frame
= frame
->GetNextContinuation();
270 return screenRect
.ToNearestPixels(presContext
->AppUnitsPerDevPixel());
273 void HyperTextAccessible::TextSubstring(int32_t aStartOffset
,
274 int32_t aEndOffset
, nsAString
& aText
) {
277 index_t startOffset
= ConvertMagicOffset(aStartOffset
);
278 index_t endOffset
= ConvertMagicOffset(aEndOffset
);
279 if (!startOffset
.IsValid() || !endOffset
.IsValid() ||
280 startOffset
> endOffset
|| endOffset
> CharacterCount()) {
281 NS_ERROR("Wrong in offset");
285 int32_t startChildIdx
= GetChildIndexAtOffset(startOffset
);
286 if (startChildIdx
== -1) return;
288 int32_t endChildIdx
= GetChildIndexAtOffset(endOffset
);
289 if (endChildIdx
== -1) return;
291 if (startChildIdx
== endChildIdx
) {
292 int32_t childOffset
= GetChildOffset(startChildIdx
);
293 if (childOffset
== -1) return;
295 LocalAccessible
* child
= LocalChildAt(startChildIdx
);
296 child
->AppendTextTo(aText
, startOffset
- childOffset
,
297 endOffset
- startOffset
);
301 int32_t startChildOffset
= GetChildOffset(startChildIdx
);
302 if (startChildOffset
== -1) return;
304 LocalAccessible
* startChild
= LocalChildAt(startChildIdx
);
305 startChild
->AppendTextTo(aText
, startOffset
- startChildOffset
);
307 for (int32_t childIdx
= startChildIdx
+ 1; childIdx
< endChildIdx
;
309 LocalAccessible
* child
= LocalChildAt(childIdx
);
310 child
->AppendTextTo(aText
);
313 int32_t endChildOffset
= GetChildOffset(endChildIdx
);
314 if (endChildOffset
== -1) return;
316 LocalAccessible
* endChild
= LocalChildAt(endChildIdx
);
317 endChild
->AppendTextTo(aText
, 0, endOffset
- endChildOffset
);
320 uint32_t HyperTextAccessible::DOMPointToOffset(nsINode
* aNode
,
322 bool aIsEndOffset
) const {
323 if (!aNode
) return 0;
326 nsINode
* findNode
= nullptr;
328 if (aNodeOffset
== -1) {
331 } else if (aNode
->IsText()) {
332 // For text nodes, aNodeOffset comes in as a character offset
333 // Text offset will be added at the end, if we find the offset in this
334 // hypertext We want the "skipped" offset into the text (rendered text
335 // without the extra whitespace)
336 nsIFrame
* frame
= aNode
->AsContent()->GetPrimaryFrame();
337 NS_ENSURE_TRUE(frame
, 0);
339 nsresult rv
= ContentToRenderedOffset(frame
, aNodeOffset
, &offset
);
340 NS_ENSURE_SUCCESS(rv
, 0);
345 // findNode could be null if aNodeOffset == # of child nodes, which means
346 // one of two things:
347 // 1) there are no children, and the passed-in node is not mContent -- use
348 // parentContent for the node to find
349 // 2) there are no children and the passed-in node is mContent, which means
350 // we're an empty nsIAccessibleText
351 // 3) there are children and we're at the end of the children
353 findNode
= aNode
->GetChildAt_Deprecated(aNodeOffset
);
355 if (aNodeOffset
== 0) {
356 if (aNode
== GetNode()) {
357 // Case #1: this accessible has no children and thus has empty text,
358 // we can only be at hypertext offset 0.
362 // Case #2: there are no children, we're at this node.
364 } else if (aNodeOffset
== static_cast<int32_t>(aNode
->GetChildCount())) {
365 // Case #3: we're after the last child, get next node to this one.
366 for (nsINode
* tmpNode
= aNode
;
367 !findNode
&& tmpNode
&& tmpNode
!= mContent
;
368 tmpNode
= tmpNode
->GetParent()) {
369 findNode
= tmpNode
->GetNextSibling();
375 // Get accessible for this findNode, or if that node isn't accessible, use the
376 // accessible for the next DOM node which has one (based on forward depth
378 LocalAccessible
* descendant
= nullptr;
380 dom::HTMLBRElement
* brElement
= dom::HTMLBRElement::FromNode(findNode
);
381 if (brElement
&& brElement
->IsPaddingForEmptyEditor()) {
382 // This <br> is the hacky "padding <br> element" used when there is no
383 // text in the editor.
387 descendant
= mDoc
->GetAccessible(findNode
);
388 if (!descendant
&& findNode
->IsContent()) {
389 LocalAccessible
* container
= mDoc
->GetContainerAccessible(findNode
);
391 TreeWalker
walker(container
, findNode
->AsContent(),
392 TreeWalker::eWalkContextTree
);
393 descendant
= walker
.Next();
394 if (!descendant
) descendant
= container
;
399 return TransformOffset(descendant
, offset
, aIsEndOffset
);
402 uint32_t HyperTextAccessible::TransformOffset(LocalAccessible
* aDescendant
,
404 bool aIsEndOffset
) const {
405 // From the descendant, go up and get the immediate child of this hypertext.
406 uint32_t offset
= aOffset
;
407 LocalAccessible
* descendant
= aDescendant
;
409 LocalAccessible
* parent
= descendant
->LocalParent();
410 if (parent
== this) return GetChildOffset(descendant
) + offset
;
412 // This offset no longer applies because the passed-in text object is not
413 // a child of the hypertext. This happens when there are nested hypertexts,
414 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
415 // to make it relative the hypertext.
416 // If the end offset is not supposed to be inclusive and the original point
417 // is not at 0 offset then the returned offset should be after an embedded
418 // character the original point belongs to.
420 // Similar to our special casing in FindOffset, we add handling for
421 // bulleted lists here because PeekOffset returns the inner text node
422 // for a list when it should return the list bullet.
423 // We manually set the offset so the error doesn't propagate up.
424 if (offset
== 0 && parent
&& parent
->IsHTMLListItem() &&
425 descendant
->LocalPrevSibling() &&
426 descendant
->LocalPrevSibling()->GetFrame() &&
427 descendant
->LocalPrevSibling()->GetFrame()->IsBulletFrame()) {
430 offset
= (offset
> 0 || descendant
->IndexInParent() > 0) ? 1 : 0;
439 // If the given a11y point cannot be mapped into offset relative this
440 // hypertext offset then return length as fallback value.
441 return CharacterCount();
444 DOMPoint
HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset
) const {
445 // 0 offset is valid even if no children. In this case the associated editor
446 // is empty so return a DOM point for editor root element.
448 RefPtr
<TextEditor
> textEditor
= GetEditor();
450 if (textEditor
->IsEmpty()) {
451 return DOMPoint(textEditor
->GetRoot(), 0);
456 int32_t childIdx
= GetChildIndexAtOffset(aOffset
);
457 if (childIdx
== -1) return DOMPoint();
459 LocalAccessible
* child
= LocalChildAt(childIdx
);
460 int32_t innerOffset
= aOffset
- GetChildOffset(childIdx
);
463 if (child
->IsTextLeaf()) {
464 // The point is inside the text node. This is always true for any text leaf
465 // except a last child one. See assertion below.
466 if (aOffset
< GetChildOffset(childIdx
+ 1)) {
467 nsIContent
* content
= child
->GetContent();
469 if (NS_FAILED(RenderedToContentOffset(content
->GetPrimaryFrame(),
470 innerOffset
, &idx
))) {
474 return DOMPoint(content
, idx
);
477 // Set the DOM point right after the text node.
478 MOZ_ASSERT(static_cast<uint32_t>(aOffset
) == CharacterCount());
482 // Case of embedded object. The point is either before or after the element.
483 NS_ASSERTION(innerOffset
== 0 || innerOffset
== 1, "A wrong inner offset!");
484 nsINode
* node
= child
->GetNode();
485 nsINode
* parentNode
= node
->GetParentNode();
486 return parentNode
? DOMPoint(parentNode
,
487 parentNode
->ComputeIndexOf(node
) + innerOffset
)
491 uint32_t HyperTextAccessible::FindOffset(uint32_t aOffset
,
492 nsDirection aDirection
,
493 nsSelectionAmount aAmount
,
494 EWordMovementType aWordMovementType
) {
495 NS_ASSERTION(aDirection
== eDirPrevious
|| aAmount
!= eSelectBeginLine
,
496 "eSelectBeginLine should only be used with eDirPrevious");
498 // Find a leaf accessible frame to start with. PeekOffset wants this.
499 HyperTextAccessible
* text
= this;
500 LocalAccessible
* child
= nullptr;
501 int32_t innerOffset
= aOffset
;
504 int32_t childIdx
= text
->GetChildIndexAtOffset(innerOffset
);
506 // We can have an empty text leaf as our only child. Since empty text
507 // leaves are not accessible we then have no children, but 0 is a valid
509 if (childIdx
== -1) {
510 NS_ASSERTION(innerOffset
== 0 && !text
->ChildCount(), "No childIdx?");
511 return DOMPointToOffset(text
->GetNode(), 0, aDirection
== eDirNext
);
514 child
= text
->LocalChildAt(childIdx
);
516 // HTML list items may need special processing because PeekOffset doesn't
517 // work with list bullets.
518 if (text
->IsHTMLListItem()) {
519 HTMLLIAccessible
* li
= text
->AsHTMLListItem();
520 if (child
== li
->Bullet()) {
521 // XXX: the logic is broken for multichar bullets in moving by
522 // char/cluster/word cases.
524 return aDirection
== eDirPrevious
? TransformOffset(text
, 0, false)
525 : TransformOffset(text
, 1, true);
527 if (aDirection
== eDirPrevious
) return 0;
529 uint32_t nextOffset
= GetChildOffset(1);
530 if (nextOffset
== 0) return 0;
535 // Ask a text leaf next (if not empty) to the bullet for an offset
536 // since list item may be multiline.
537 return nextOffset
< CharacterCount()
538 ? FindOffset(nextOffset
, aDirection
, aAmount
,
548 innerOffset
-= text
->GetChildOffset(childIdx
);
550 text
= child
->AsHyperText();
553 nsIFrame
* childFrame
= child
->GetFrame();
555 NS_ERROR("No child frame");
559 int32_t innerContentOffset
= innerOffset
;
560 if (child
->IsTextLeaf()) {
561 NS_ASSERTION(childFrame
->IsTextFrame(), "Wrong frame!");
562 RenderedToContentOffset(childFrame
, innerOffset
, &innerContentOffset
);
565 nsIFrame
* frameAtOffset
= childFrame
;
566 int32_t unusedOffsetInFrame
= 0;
567 childFrame
->GetChildFrameContainingOffset(
568 innerContentOffset
, true, &unusedOffsetInFrame
, &frameAtOffset
);
570 const bool kIsJumpLinesOk
= true; // okay to jump lines
571 const bool kIsScrollViewAStop
= false; // do not stop at scroll views
572 const bool kIsKeyboardSelect
= true; // is keyboard selection
573 const bool kIsVisualBidi
= false; // use visual order for bidi text
574 nsPeekOffsetStruct
pos(
575 aAmount
, aDirection
, innerContentOffset
, nsPoint(0, 0), kIsJumpLinesOk
,
576 kIsScrollViewAStop
, kIsKeyboardSelect
, kIsVisualBidi
, false,
577 nsPeekOffsetStruct::ForceEditableRegion::No
, aWordMovementType
, false);
578 nsresult rv
= frameAtOffset
->PeekOffset(&pos
);
580 // PeekOffset fails on last/first lines of the text in certain cases.
581 bool fallBackToSelectEndLine
= false;
582 if (NS_FAILED(rv
) && aAmount
== eSelectLine
) {
583 fallBackToSelectEndLine
= aDirection
== eDirNext
;
584 pos
.mAmount
= fallBackToSelectEndLine
? eSelectEndLine
: eSelectBeginLine
;
585 frameAtOffset
->PeekOffset(&pos
);
587 if (!pos
.mResultContent
) {
588 NS_ERROR("No result content!");
592 // Turn the resulting DOM point into an offset.
593 uint32_t hyperTextOffset
= DOMPointToOffset(
594 pos
.mResultContent
, pos
.mContentOffset
, aDirection
== eDirNext
);
596 if (fallBackToSelectEndLine
&& IsLineEndCharAt(hyperTextOffset
)) {
597 // We used eSelectEndLine, but the caller requested eSelectLine.
598 // If there's a '\n' at the end of the line, eSelectEndLine will stop on
599 // it rather than after it. This is not what we want, since the caller
600 // wants the next line, not the same line.
604 if (aDirection
== eDirPrevious
) {
605 // If we reached the end during search, this means we didn't find the DOM
606 // point and we're actually at the start of the paragraph
607 if (hyperTextOffset
== CharacterCount()) return 0;
609 // PeekOffset stops right before bullet so return 0 to workaround it.
610 if (IsHTMLListItem() && aAmount
== eSelectBeginLine
&&
611 hyperTextOffset
> 0) {
612 LocalAccessible
* prevOffsetChild
= GetChildAtOffset(hyperTextOffset
- 1);
613 if (prevOffsetChild
== AsHTMLListItem()->Bullet()) return 0;
617 return hyperTextOffset
;
620 uint32_t HyperTextAccessible::FindWordBoundary(
621 uint32_t aOffset
, nsDirection aDirection
,
622 EWordMovementType aWordMovementType
) {
624 FindOffset(aOffset
, aDirection
, eSelectWord
, aWordMovementType
);
625 if (aWordMovementType
!= eStartWord
) {
628 if (aDirection
== eDirPrevious
) {
629 // When layout.word_select.stop_at_punctuation is true (the default),
630 // for a word beginning with punctuation, layout treats the punctuation
631 // as the start of the word when moving next. However, when moving
632 // previous, layout stops *after* the punctuation. We want to be
633 // consistent regardless of movement direction and always treat punctuation
634 // as the start of a word.
635 if (!StaticPrefs::layout_word_select_stop_at_punctuation()) {
638 // Case 1: Example: "a @"
639 // If aOffset is 2 or 3, orig will be 0, but it should be 2. That is,
640 // previous word moved back too far.
641 LocalAccessible
* child
= GetChildAtOffset(orig
);
642 if (child
&& child
->IsHyperText()) {
643 // For a multi-word embedded object, previous word correctly goes back
644 // to the start of the word (the embedded object). Next word (below)
645 // incorrectly stops after the embedded object in this case, so return
646 // the already correct result.
647 // Example: "a x y b", where "x y" is an embedded link
648 // If aOffset is 4, orig will be 2, which is correct.
649 // If we get the next word (below), we'll end up returning 3 instead.
652 uint32_t next
= FindOffset(orig
, eDirNext
, eSelectWord
, eStartWord
);
653 if (next
< aOffset
) {
654 // Next word stopped on punctuation.
657 // case 2: example: "a @@b"
658 // If aOffset is 2, 3 or 4, orig will be 4, but it should be 2. That is,
659 // previous word didn't go back far enough.
663 // Walk backwards by offset, getting the next word.
664 // In the loop, o is unsigned, so o >= 0 will always be true and won't
665 // prevent us from decrementing at 0. Instead, we check that o doesn't
667 for (uint32_t o
= orig
- 1; o
< orig
; --o
) {
668 next
= FindOffset(o
, eDirNext
, eSelectWord
, eStartWord
);
670 // Next word and previous word were consistent. This
671 // punctuation problem isn't applicable here.
675 // Next word stopped on punctuation.
680 // When layout.word_select.stop_at_punctuation is true (the default),
681 // when positioned on punctuation in the middle of a word, next word skips
682 // the rest of the word. However, when positioned before the punctuation,
683 // next word moves just after the punctuation. We want to be consistent
684 // regardless of starting position and always stop just after the
686 // Next word can move too far when positioned on white space too.
688 // If aOffset is 3, orig will be 5, but it should be 4. That is, next word
693 uint32_t prev
= FindOffset(orig
, eDirPrevious
, eSelectWord
, eStartWord
);
694 if (prev
<= aOffset
) {
695 // orig definitely isn't too far forward.
698 // Walk backwards by offset, getting the next word.
699 // In the loop, o is unsigned, so o >= 0 will always be true and won't
700 // prevent us from decrementing at 0. Instead, we check that o doesn't
702 for (uint32_t o
= aOffset
- 1; o
< aOffset
; --o
) {
703 uint32_t next
= FindOffset(o
, eDirNext
, eSelectWord
, eStartWord
);
704 if (next
> aOffset
&& next
< orig
) {
707 if (next
<= aOffset
) {
715 uint32_t HyperTextAccessible::FindLineBoundary(
716 uint32_t aOffset
, EWhichLineBoundary aWhichLineBoundary
) {
717 // Note: empty last line doesn't have own frame (a previous line contains '\n'
718 // character instead) thus when it makes a difference we need to process this
719 // case separately (otherwise operations are performed on previous line).
720 switch (aWhichLineBoundary
) {
721 case ePrevLineBegin
: {
722 // Fetch a previous line and move to its start (as arrow up and home keys
724 if (IsEmptyLastLineOffset(aOffset
)) {
725 return FindOffset(aOffset
, eDirPrevious
, eSelectBeginLine
);
728 uint32_t tmpOffset
= FindOffset(aOffset
, eDirPrevious
, eSelectLine
);
729 return FindOffset(tmpOffset
, eDirPrevious
, eSelectBeginLine
);
733 if (IsEmptyLastLineOffset(aOffset
)) return aOffset
- 1;
735 // If offset is at first line then return 0 (first line start).
736 uint32_t tmpOffset
= FindOffset(aOffset
, eDirPrevious
, eSelectBeginLine
);
737 if (tmpOffset
== 0) return 0;
739 // Otherwise move to end of previous line (as arrow up and end keys were
741 tmpOffset
= FindOffset(aOffset
, eDirPrevious
, eSelectLine
);
742 return FindOffset(tmpOffset
, eDirNext
, eSelectEndLine
);
745 case eThisLineBegin
: {
746 if (IsEmptyLastLineOffset(aOffset
)) return aOffset
;
748 // Move to begin of the current line (as home key was pressed).
749 uint32_t thisLineBeginOffset
=
750 FindOffset(aOffset
, eDirPrevious
, eSelectBeginLine
);
751 if (IsCharAt(thisLineBeginOffset
, kEmbeddedObjectChar
)) {
752 // We landed on an embedded character, don't mess with possible embedded
753 // line breaks, and assume the offset is correct.
754 return thisLineBeginOffset
;
757 // Sometimes, there is the possibility layout returned an
758 // offset smaller than it should. Sanity-check by moving to the end of the
759 // previous line and see if that has a greater offset.
760 uint32_t tmpOffset
= FindOffset(aOffset
, eDirPrevious
, eSelectLine
);
761 tmpOffset
= FindOffset(tmpOffset
, eDirNext
, eSelectEndLine
);
762 if (tmpOffset
> thisLineBeginOffset
&& tmpOffset
< aOffset
) {
763 // We found a previous line offset. Return the next character after it
764 // as our start offset if it points to a line end char.
765 return IsLineEndCharAt(tmpOffset
) ? tmpOffset
+ 1 : tmpOffset
;
767 return thisLineBeginOffset
;
771 if (IsEmptyLastLineOffset(aOffset
)) return aOffset
;
773 // Move to end of the current line (as end key was pressed).
774 return FindOffset(aOffset
, eDirNext
, eSelectEndLine
);
776 case eNextLineBegin
: {
777 if (IsEmptyLastLineOffset(aOffset
)) return aOffset
;
779 // Move to begin of the next line if any (arrow down and home keys),
780 // otherwise end of the current line (arrow down only).
781 uint32_t tmpOffset
= FindOffset(aOffset
, eDirNext
, eSelectLine
);
782 uint32_t characterCount
= CharacterCount();
783 if (tmpOffset
== characterCount
) {
787 // Now, simulate the Home key on the next line to get its real offset.
788 uint32_t nextLineBeginOffset
=
789 FindOffset(tmpOffset
, eDirPrevious
, eSelectBeginLine
);
790 // Sometimes, there are line breaks inside embedded characters. If this
791 // is the case, the cursor is after the line break, but the offset will
792 // be that of the embedded character, which points to before the line
793 // break. We definitely want the line break included.
794 if (IsCharAt(nextLineBeginOffset
, kEmbeddedObjectChar
)) {
795 // We can determine if there is a line break by pressing End from
796 // the queried offset. If there is a line break, the offset will be 1
797 // greater, since this line ends with the embed. If there is not, the
798 // value will be different even if a line break follows right after the
800 uint32_t thisLineEndOffset
=
801 FindOffset(aOffset
, eDirNext
, eSelectEndLine
);
802 if (thisLineEndOffset
== nextLineBeginOffset
+ 1) {
803 // If we're querying the offset of the embedded character, we want
804 // the end offset of the parent line instead. Press End
805 // once more from the current position, which is after the embed.
806 if (nextLineBeginOffset
== aOffset
) {
807 uint32_t thisLineEndOffset2
=
808 FindOffset(thisLineEndOffset
, eDirNext
, eSelectEndLine
);
809 // The above returns an offset exclusive the final line break, so we
810 // need to add 1 to it to return an inclusive end offset. Make sure
811 // we don't overshoot if we've started from another embedded
812 // character that has a line break, or landed on another embedded
813 // character, or if the result is the very end.
814 return (thisLineEndOffset2
== characterCount
||
815 (IsCharAt(thisLineEndOffset
, kEmbeddedObjectChar
) &&
816 thisLineEndOffset2
== thisLineEndOffset
+ 1) ||
817 IsCharAt(thisLineEndOffset2
, kEmbeddedObjectChar
))
819 : thisLineEndOffset2
+ 1;
822 return thisLineEndOffset
;
824 return nextLineBeginOffset
;
827 // If the resulting offset is not greater than the offset we started from,
828 // layout could not find the offset for us. This can happen with certain
829 // inline-block elements.
830 if (nextLineBeginOffset
<= aOffset
) {
831 // Walk forward from the offset we started from up to tmpOffset,
832 // stopping after a line end character.
833 nextLineBeginOffset
= aOffset
;
834 while (nextLineBeginOffset
< tmpOffset
) {
835 if (IsLineEndCharAt(nextLineBeginOffset
)) {
836 return nextLineBeginOffset
+ 1;
838 nextLineBeginOffset
++;
842 return nextLineBeginOffset
;
846 if (IsEmptyLastLineOffset(aOffset
)) return aOffset
;
848 // Move to next line end (as down arrow and end key were pressed).
849 uint32_t tmpOffset
= FindOffset(aOffset
, eDirNext
, eSelectLine
);
850 if (tmpOffset
== CharacterCount()) return tmpOffset
;
852 return FindOffset(tmpOffset
, eDirNext
, eSelectEndLine
);
859 int32_t HyperTextAccessible::FindParagraphStartOffset(uint32_t aOffset
) {
860 // Because layout often gives us offsets that are incompatible with
861 // accessibility API requirements, for example when a paragraph contains
862 // presentational line breaks as found in Google Docs, use the accessibility
863 // tree to find the start offset instead.
864 LocalAccessible
* child
= GetChildAtOffset(aOffset
);
866 return -1; // Invalid offset
869 // Use the pivot class to search for the start offset.
870 Pivot p
= Pivot(this);
871 ParagraphBoundaryRule boundaryRule
= ParagraphBoundaryRule(
872 child
, child
->IsTextLeaf() ? aOffset
- GetChildOffset(child
) : 0,
874 AccessibleOrProxy wrappedChild
= AccessibleOrProxy(child
);
875 AccessibleOrProxy match
= p
.Prev(wrappedChild
, boundaryRule
, true);
876 if (match
.IsNull() || match
.AsAccessible() == this) {
877 // Found nothing, or pivot found the root of the search, startOffset is 0.
878 // This will include all relevant text nodes.
882 if (match
== wrappedChild
) {
883 // We started out on a boundary.
884 if (match
.Role() == roles::WHITESPACE
) {
885 // We are on a line break boundary, so force pivot to find the previous
886 // boundary. What we want is any text before this, if any.
887 match
= p
.Prev(match
, boundaryRule
);
888 if (match
.IsNull() || match
.AsAccessible() == this) {
889 // Same as before, we landed on the root, so offset is definitely 0.
892 } else if (!match
.AsAccessible()->IsTextLeaf()) {
893 // The match is a block element, which is always a starting point, so
894 // just return its offset.
895 return TransformOffset(match
.AsAccessible(), 0, false);
899 if (match
.AsAccessible()->IsTextLeaf()) {
900 // ParagraphBoundaryRule only returns a text leaf if it contains a line
901 // break. We want to stop after that.
902 return TransformOffset(match
.AsAccessible(),
903 boundaryRule
.GetLastMatchTextOffset() + 1, false);
906 // This is a previous boundary, we don't want to include it itself.
907 // So, walk forward one accessible, excluding the descendants of this
908 // boundary if it is a block element. The below call to Next should always be
909 // initialized with a boundary.
910 SkipParagraphBoundaryRule goForwardOneRule
= SkipParagraphBoundaryRule(match
);
911 match
= p
.Next(match
, goForwardOneRule
);
912 // We already know that the search skipped over at least one accessible,
913 // so match can't be null. Get its transformed offset.
914 MOZ_ASSERT(!match
.IsNull());
915 return TransformOffset(match
.AsAccessible(), 0, false);
918 int32_t HyperTextAccessible::FindParagraphEndOffset(uint32_t aOffset
) {
919 // Because layout often gives us offsets that are incompatible with
920 // accessibility API requirements, for example when a paragraph contains
921 // presentational line breaks as found in Google Docs, use the accessibility
922 // tree to find the end offset instead.
923 LocalAccessible
* child
= GetChildAtOffset(aOffset
);
925 return -1; // invalid offset
928 // Use the pivot class to search for the end offset.
929 Pivot p
= Pivot(this);
930 AccessibleOrProxy wrappedChild
= AccessibleOrProxy(child
);
931 ParagraphBoundaryRule boundaryRule
= ParagraphBoundaryRule(
932 child
, child
->IsTextLeaf() ? aOffset
- GetChildOffset(child
) : 0,
934 // In order to encompass all paragraphs inside embedded objects, not just
935 // the first, we want to skip the anchor's subtree.
936 /* aSkipAnchorSubtree */ true);
937 // Search forward for the end offset, including wrappedChild. We don't want
938 // to go beyond this point if this offset indicates a paragraph boundary.
939 AccessibleOrProxy match
= p
.Next(wrappedChild
, boundaryRule
, true);
940 if (!match
.IsNull()) {
941 // Found something of relevance, adjust end offset.
942 LocalAccessible
* matchAcc
= match
.AsAccessible();
943 uint32_t matchOffset
;
944 if (matchAcc
->IsTextLeaf()) {
945 // ParagraphBoundaryRule only returns a text leaf if it contains a line
947 matchOffset
= boundaryRule
.GetLastMatchTextOffset() + 1;
948 } else if (matchAcc
->Role() != roles::WHITESPACE
&& matchAcc
!= child
) {
949 // We found a block boundary that wasn't our origin. We want to stop
950 // right on it, not after it, since we don't want to include the content
954 matchOffset
= nsAccUtils::TextLength(matchAcc
);
956 return TransformOffset(matchAcc
, matchOffset
, true);
959 // Didn't find anything, end offset is character count.
960 return CharacterCount();
963 void HyperTextAccessible::TextBeforeOffset(int32_t aOffset
,
964 AccessibleTextBoundary aBoundaryType
,
965 int32_t* aStartOffset
,
968 *aStartOffset
= *aEndOffset
= 0;
971 if (aBoundaryType
== nsIAccessibleText::BOUNDARY_PARAGRAPH
) {
972 // Not supported, bail out with empty text.
976 index_t convertedOffset
= ConvertMagicOffset(aOffset
);
977 if (!convertedOffset
.IsValid() || convertedOffset
> CharacterCount()) {
978 NS_ERROR("Wrong in offset!");
982 uint32_t adjustedOffset
= convertedOffset
;
983 if (aOffset
== nsIAccessibleText::TEXT_OFFSET_CARET
) {
984 adjustedOffset
= AdjustCaretOffset(adjustedOffset
);
987 switch (aBoundaryType
) {
988 case nsIAccessibleText::BOUNDARY_CHAR
:
989 if (convertedOffset
!= 0) {
990 CharAt(convertedOffset
- 1, aText
, aStartOffset
, aEndOffset
);
994 case nsIAccessibleText::BOUNDARY_WORD_START
: {
995 // If the offset is a word start (except text length offset) then move
996 // backward to find a start offset (end offset is the given offset).
997 // Otherwise move backward twice to find both start and end offsets.
998 if (adjustedOffset
== CharacterCount()) {
1000 FindWordBoundary(adjustedOffset
, eDirPrevious
, eStartWord
);
1001 *aStartOffset
= FindWordBoundary(*aEndOffset
, eDirPrevious
, eStartWord
);
1004 FindWordBoundary(adjustedOffset
, eDirPrevious
, eStartWord
);
1005 *aEndOffset
= FindWordBoundary(*aStartOffset
, eDirNext
, eStartWord
);
1006 if (*aEndOffset
!= static_cast<int32_t>(adjustedOffset
)) {
1007 *aEndOffset
= *aStartOffset
;
1009 FindWordBoundary(*aEndOffset
, eDirPrevious
, eStartWord
);
1012 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1016 case nsIAccessibleText::BOUNDARY_WORD_END
: {
1017 // Move word backward twice to find start and end offsets.
1018 *aEndOffset
= FindWordBoundary(convertedOffset
, eDirPrevious
, eEndWord
);
1019 *aStartOffset
= FindWordBoundary(*aEndOffset
, eDirPrevious
, eEndWord
);
1020 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1024 case nsIAccessibleText::BOUNDARY_LINE_START
:
1025 *aStartOffset
= FindLineBoundary(adjustedOffset
, ePrevLineBegin
);
1026 *aEndOffset
= FindLineBoundary(adjustedOffset
, eThisLineBegin
);
1027 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1030 case nsIAccessibleText::BOUNDARY_LINE_END
: {
1031 *aEndOffset
= FindLineBoundary(adjustedOffset
, ePrevLineEnd
);
1032 int32_t tmpOffset
= *aEndOffset
;
1033 // Adjust offset if line is wrapped.
1034 if (*aEndOffset
!= 0 && !IsLineEndCharAt(*aEndOffset
)) tmpOffset
--;
1036 *aStartOffset
= FindLineBoundary(tmpOffset
, ePrevLineEnd
);
1037 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1043 void HyperTextAccessible::TextAtOffset(int32_t aOffset
,
1044 AccessibleTextBoundary aBoundaryType
,
1045 int32_t* aStartOffset
,
1046 int32_t* aEndOffset
, nsAString
& aText
) {
1047 *aStartOffset
= *aEndOffset
= 0;
1050 uint32_t adjustedOffset
= ConvertMagicOffset(aOffset
);
1051 if (adjustedOffset
== std::numeric_limits
<uint32_t>::max()) {
1052 NS_ERROR("Wrong given offset!");
1056 switch (aBoundaryType
) {
1057 case nsIAccessibleText::BOUNDARY_CHAR
:
1058 // Return no char if caret is at the end of wrapped line (case of no line
1059 // end character). Returning a next line char is confusing for AT.
1060 if (aOffset
== nsIAccessibleText::TEXT_OFFSET_CARET
&&
1061 IsCaretAtEndOfLine()) {
1062 *aStartOffset
= *aEndOffset
= adjustedOffset
;
1064 CharAt(adjustedOffset
, aText
, aStartOffset
, aEndOffset
);
1068 case nsIAccessibleText::BOUNDARY_WORD_START
:
1069 if (aOffset
== nsIAccessibleText::TEXT_OFFSET_CARET
) {
1070 adjustedOffset
= AdjustCaretOffset(adjustedOffset
);
1073 *aEndOffset
= FindWordBoundary(adjustedOffset
, eDirNext
, eStartWord
);
1074 *aStartOffset
= FindWordBoundary(*aEndOffset
, eDirPrevious
, eStartWord
);
1075 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1078 case nsIAccessibleText::BOUNDARY_WORD_END
:
1079 // Ignore the spec and follow what WebKitGtk does because Orca expects it,
1080 // i.e. return a next word at word end offset of the current word
1081 // (WebKitGtk behavior) instead the current word (AKT spec).
1082 *aEndOffset
= FindWordBoundary(adjustedOffset
, eDirNext
, eEndWord
);
1083 *aStartOffset
= FindWordBoundary(*aEndOffset
, eDirPrevious
, eEndWord
);
1084 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1087 case nsIAccessibleText::BOUNDARY_LINE_START
:
1088 if (aOffset
== nsIAccessibleText::TEXT_OFFSET_CARET
) {
1089 adjustedOffset
= AdjustCaretOffset(adjustedOffset
);
1092 *aStartOffset
= FindLineBoundary(adjustedOffset
, eThisLineBegin
);
1093 *aEndOffset
= FindLineBoundary(adjustedOffset
, eNextLineBegin
);
1094 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1097 case nsIAccessibleText::BOUNDARY_LINE_END
:
1098 if (aOffset
== nsIAccessibleText::TEXT_OFFSET_CARET
) {
1099 adjustedOffset
= AdjustCaretOffset(adjustedOffset
);
1102 // In contrast to word end boundary we follow the spec here.
1103 *aStartOffset
= FindLineBoundary(adjustedOffset
, ePrevLineEnd
);
1104 *aEndOffset
= FindLineBoundary(adjustedOffset
, eThisLineEnd
);
1105 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1108 case nsIAccessibleText::BOUNDARY_PARAGRAPH
: {
1109 if (aOffset
== nsIAccessibleText::TEXT_OFFSET_CARET
) {
1110 adjustedOffset
= AdjustCaretOffset(adjustedOffset
);
1113 if (IsEmptyLastLineOffset(adjustedOffset
)) {
1114 // We are on the last line of a paragraph where there is no text.
1115 // For example, in a textarea where a new line has just been inserted.
1116 // In this case, return offsets for an empty line without text content.
1117 *aStartOffset
= *aEndOffset
= adjustedOffset
;
1121 *aStartOffset
= FindParagraphStartOffset(adjustedOffset
);
1122 *aEndOffset
= FindParagraphEndOffset(adjustedOffset
);
1123 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1129 void HyperTextAccessible::TextAfterOffset(int32_t aOffset
,
1130 AccessibleTextBoundary aBoundaryType
,
1131 int32_t* aStartOffset
,
1132 int32_t* aEndOffset
,
1134 *aStartOffset
= *aEndOffset
= 0;
1137 if (aBoundaryType
== nsIAccessibleText::BOUNDARY_PARAGRAPH
) {
1138 // Not supported, bail out with empty text.
1142 index_t convertedOffset
= ConvertMagicOffset(aOffset
);
1143 if (!convertedOffset
.IsValid() || convertedOffset
> CharacterCount()) {
1144 NS_ERROR("Wrong in offset!");
1148 uint32_t adjustedOffset
= convertedOffset
;
1149 if (aOffset
== nsIAccessibleText::TEXT_OFFSET_CARET
) {
1150 adjustedOffset
= AdjustCaretOffset(adjustedOffset
);
1153 switch (aBoundaryType
) {
1154 case nsIAccessibleText::BOUNDARY_CHAR
:
1155 // If caret is at the end of wrapped line (case of no line end character)
1156 // then char after the offset is a first char at next line.
1157 if (adjustedOffset
>= CharacterCount()) {
1158 *aStartOffset
= *aEndOffset
= CharacterCount();
1160 CharAt(adjustedOffset
+ 1, aText
, aStartOffset
, aEndOffset
);
1164 case nsIAccessibleText::BOUNDARY_WORD_START
:
1165 // Move word forward twice to find start and end offsets.
1166 *aStartOffset
= FindWordBoundary(adjustedOffset
, eDirNext
, eStartWord
);
1167 *aEndOffset
= FindWordBoundary(*aStartOffset
, eDirNext
, eStartWord
);
1168 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1171 case nsIAccessibleText::BOUNDARY_WORD_END
:
1172 // If the offset is a word end (except 0 offset) then move forward to find
1173 // end offset (start offset is the given offset). Otherwise move forward
1174 // twice to find both start and end offsets.
1175 if (convertedOffset
== 0) {
1176 *aStartOffset
= FindWordBoundary(convertedOffset
, eDirNext
, eEndWord
);
1177 *aEndOffset
= FindWordBoundary(*aStartOffset
, eDirNext
, eEndWord
);
1179 *aEndOffset
= FindWordBoundary(convertedOffset
, eDirNext
, eEndWord
);
1180 *aStartOffset
= FindWordBoundary(*aEndOffset
, eDirPrevious
, eEndWord
);
1181 if (*aStartOffset
!= static_cast<int32_t>(convertedOffset
)) {
1182 *aStartOffset
= *aEndOffset
;
1183 *aEndOffset
= FindWordBoundary(*aStartOffset
, eDirNext
, eEndWord
);
1186 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1189 case nsIAccessibleText::BOUNDARY_LINE_START
:
1190 *aStartOffset
= FindLineBoundary(adjustedOffset
, eNextLineBegin
);
1191 *aEndOffset
= FindLineBoundary(*aStartOffset
, eNextLineBegin
);
1192 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1195 case nsIAccessibleText::BOUNDARY_LINE_END
:
1196 *aStartOffset
= FindLineBoundary(adjustedOffset
, eThisLineEnd
);
1197 *aEndOffset
= FindLineBoundary(adjustedOffset
, eNextLineEnd
);
1198 TextSubstring(*aStartOffset
, *aEndOffset
, aText
);
1203 already_AddRefed
<nsIPersistentProperties
> HyperTextAccessible::TextAttributes(
1204 bool aIncludeDefAttrs
, int32_t aOffset
, int32_t* aStartOffset
,
1205 int32_t* aEndOffset
) {
1206 // 1. Get each attribute and its ranges one after another.
1207 // 2. As we get each new attribute, we pass the current start and end offsets
1208 // as in/out parameters. In other words, as attributes are collected,
1209 // the attribute range itself can only stay the same or get smaller.
1211 *aStartOffset
= *aEndOffset
= 0;
1212 index_t offset
= ConvertMagicOffset(aOffset
);
1213 if (!offset
.IsValid() || offset
> CharacterCount()) {
1214 NS_ERROR("Wrong in offset!");
1218 RefPtr
<nsPersistentProperties
> attributes
= new nsPersistentProperties();
1220 LocalAccessible
* accAtOffset
= GetChildAtOffset(offset
);
1222 // Offset 0 is correct offset when accessible has empty text. Include
1223 // default attributes if they were requested, otherwise return empty set.
1225 if (aIncludeDefAttrs
) {
1226 TextAttrsMgr
textAttrsMgr(this);
1227 textAttrsMgr
.GetAttributes(attributes
);
1229 return attributes
.forget();
1234 int32_t accAtOffsetIdx
= accAtOffset
->IndexInParent();
1235 uint32_t startOffset
= GetChildOffset(accAtOffsetIdx
);
1236 uint32_t endOffset
= GetChildOffset(accAtOffsetIdx
+ 1);
1237 int32_t offsetInAcc
= offset
- startOffset
;
1239 TextAttrsMgr
textAttrsMgr(this, aIncludeDefAttrs
, accAtOffset
,
1241 textAttrsMgr
.GetAttributes(attributes
, &startOffset
, &endOffset
);
1243 // Compute spelling attributes on text accessible only.
1244 nsIFrame
* offsetFrame
= accAtOffset
->GetFrame();
1245 if (offsetFrame
&& offsetFrame
->IsTextFrame()) {
1246 int32_t nodeOffset
= 0;
1247 RenderedToContentOffset(offsetFrame
, offsetInAcc
, &nodeOffset
);
1249 // Set 'misspelled' text attribute.
1250 GetSpellTextAttr(accAtOffset
->GetNode(), nodeOffset
, &startOffset
,
1251 &endOffset
, attributes
);
1254 *aStartOffset
= startOffset
;
1255 *aEndOffset
= endOffset
;
1256 return attributes
.forget();
1259 already_AddRefed
<nsIPersistentProperties
>
1260 HyperTextAccessible::DefaultTextAttributes() {
1261 RefPtr
<nsPersistentProperties
> attributes
= new nsPersistentProperties();
1263 TextAttrsMgr
textAttrsMgr(this);
1264 textAttrsMgr
.GetAttributes(attributes
);
1265 return attributes
.forget();
1268 int32_t HyperTextAccessible::GetLevelInternal() {
1269 if (auto* heading
= dom::HTMLHeadingElement::FromNode(mContent
)) {
1270 return heading
->AccessibilityLevel();
1272 return AccessibleWrap::GetLevelInternal();
1275 void HyperTextAccessible::SetMathMLXMLRoles(
1276 nsIPersistentProperties
* aAttributes
) {
1277 // Add MathML xmlroles based on the position inside the parent.
1278 LocalAccessible
* parent
= LocalParent();
1280 switch (parent
->Role()) {
1281 case roles::MATHML_CELL
:
1282 case roles::MATHML_ENCLOSED
:
1283 case roles::MATHML_ERROR
:
1284 case roles::MATHML_MATH
:
1285 case roles::MATHML_ROW
:
1286 case roles::MATHML_SQUARE_ROOT
:
1287 case roles::MATHML_STYLE
:
1288 if (Role() == roles::MATHML_OPERATOR
) {
1289 // This is an operator inside an <mrow> (or an inferred <mrow>).
1290 // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
1291 // XXX We should probably do something similar for MATHML_FENCED, but
1292 // operators do not appear in the accessible tree. See bug 1175747.
1293 nsIMathMLFrame
* mathMLFrame
= do_QueryFrame(GetFrame());
1295 nsEmbellishData embellishData
;
1296 mathMLFrame
->GetEmbellishData(embellishData
);
1297 if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData
.flags
)) {
1298 if (!LocalPrevSibling()) {
1299 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1300 nsGkAtoms::open_fence
);
1301 } else if (!LocalNextSibling()) {
1302 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1303 nsGkAtoms::close_fence
);
1306 if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData
.flags
)) {
1307 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1308 nsGkAtoms::separator_
);
1313 case roles::MATHML_FRACTION
:
1314 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1315 IndexInParent() == 0 ? nsGkAtoms::numerator
1316 : nsGkAtoms::denominator
);
1318 case roles::MATHML_ROOT
:
1319 nsAccUtils::SetAccAttr(
1320 aAttributes
, nsGkAtoms::xmlroles
,
1321 IndexInParent() == 0 ? nsGkAtoms::base
: nsGkAtoms::root_index
);
1323 case roles::MATHML_SUB
:
1324 nsAccUtils::SetAccAttr(
1325 aAttributes
, nsGkAtoms::xmlroles
,
1326 IndexInParent() == 0 ? nsGkAtoms::base
: nsGkAtoms::subscript
);
1328 case roles::MATHML_SUP
:
1329 nsAccUtils::SetAccAttr(
1330 aAttributes
, nsGkAtoms::xmlroles
,
1331 IndexInParent() == 0 ? nsGkAtoms::base
: nsGkAtoms::superscript
);
1333 case roles::MATHML_SUB_SUP
: {
1334 int32_t index
= IndexInParent();
1335 nsAccUtils::SetAccAttr(
1336 aAttributes
, nsGkAtoms::xmlroles
,
1339 : (index
== 1 ? nsGkAtoms::subscript
: nsGkAtoms::superscript
));
1341 case roles::MATHML_UNDER
:
1342 nsAccUtils::SetAccAttr(
1343 aAttributes
, nsGkAtoms::xmlroles
,
1344 IndexInParent() == 0 ? nsGkAtoms::base
: nsGkAtoms::underscript
);
1346 case roles::MATHML_OVER
:
1347 nsAccUtils::SetAccAttr(
1348 aAttributes
, nsGkAtoms::xmlroles
,
1349 IndexInParent() == 0 ? nsGkAtoms::base
: nsGkAtoms::overscript
);
1351 case roles::MATHML_UNDER_OVER
: {
1352 int32_t index
= IndexInParent();
1353 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1356 : (index
== 1 ? nsGkAtoms::underscript
1357 : nsGkAtoms::overscript
));
1359 case roles::MATHML_MULTISCRIPTS
: {
1360 // Get the <multiscripts> base.
1362 bool baseFound
= false;
1363 for (child
= parent
->GetContent()->GetFirstChild(); child
;
1364 child
= child
->GetNextSibling()) {
1365 if (child
->IsMathMLElement()) {
1371 nsIContent
* content
= GetContent();
1372 if (child
== content
) {
1374 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1377 // Browse the list of scripts to find us and determine our type.
1378 bool postscript
= true;
1379 bool subscript
= true;
1380 for (child
= child
->GetNextSibling(); child
;
1381 child
= child
->GetNextSibling()) {
1382 if (!child
->IsMathMLElement()) continue;
1383 if (child
->IsMathMLElement(nsGkAtoms::mprescripts_
)) {
1388 if (child
== content
) {
1390 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1391 subscript
? nsGkAtoms::subscript
1392 : nsGkAtoms::superscript
);
1394 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::xmlroles
,
1395 subscript
? nsGkAtoms::presubscript
1396 : nsGkAtoms::presuperscript
);
1400 subscript
= !subscript
;
1411 already_AddRefed
<nsIPersistentProperties
>
1412 HyperTextAccessible::NativeAttributes() {
1413 nsCOMPtr
<nsIPersistentProperties
> attributes
=
1414 AccessibleWrap::NativeAttributes();
1416 // 'formatting' attribute is deprecated, 'display' attribute should be
1418 nsIFrame
* frame
= GetFrame();
1419 if (frame
&& frame
->IsBlockFrame()) {
1420 nsAutoString unused
;
1421 attributes
->SetStringProperty("formatting"_ns
, u
"block"_ns
, unused
);
1424 if (FocusMgr()->IsFocused(this)) {
1425 int32_t lineNumber
= CaretLineNumber();
1426 if (lineNumber
>= 1) {
1427 nsAutoString strLineNumber
;
1428 strLineNumber
.AppendInt(lineNumber
);
1429 nsAccUtils::SetAccAttr(attributes
, nsGkAtoms::lineNumber
, strLineNumber
);
1433 if (HasOwnContent()) {
1434 GetAccService()->MarkupAttributes(mContent
, attributes
);
1435 if (mContent
->IsMathMLElement()) SetMathMLXMLRoles(attributes
);
1438 return attributes
.forget();
1441 nsAtom
* HyperTextAccessible::LandmarkRole() const {
1442 if (!HasOwnContent()) return nullptr;
1444 // For the html landmark elements we expose them like we do ARIA landmarks to
1445 // make AT navigation schemes "just work".
1446 if (mContent
->IsHTMLElement(nsGkAtoms::nav
)) {
1447 return nsGkAtoms::navigation
;
1450 if (mContent
->IsHTMLElement(nsGkAtoms::aside
)) {
1451 return nsGkAtoms::complementary
;
1454 if (mContent
->IsHTMLElement(nsGkAtoms::main
)) {
1455 return nsGkAtoms::main
;
1458 return AccessibleWrap::LandmarkRole();
1461 int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX
, int32_t aY
,
1462 uint32_t aCoordType
) {
1463 nsIFrame
* hyperFrame
= GetFrame();
1464 if (!hyperFrame
) return -1;
1467 nsAccUtils::ConvertToScreenCoords(aX
, aY
, aCoordType
, this);
1469 nsPresContext
* presContext
= mDoc
->PresContext();
1470 nsPoint coordsInAppUnits
=
1471 ToAppUnits(coords
, presContext
->AppUnitsPerDevPixel());
1473 nsRect frameScreenRect
= hyperFrame
->GetScreenRectInAppUnits();
1474 if (!frameScreenRect
.Contains(coordsInAppUnits
.x
, coordsInAppUnits
.y
)) {
1475 return -1; // Not found
1478 nsPoint
pointInHyperText(coordsInAppUnits
.x
- frameScreenRect
.X(),
1479 coordsInAppUnits
.y
- frameScreenRect
.Y());
1481 // Go through the frames to check if each one has the point.
1482 // When one does, add up the character offsets until we have a match
1484 // We have an point in an accessible child of this, now we need to add up the
1485 // offsets before it to what we already have
1487 uint32_t childCount
= ChildCount();
1488 for (uint32_t childIdx
= 0; childIdx
< childCount
; childIdx
++) {
1489 LocalAccessible
* childAcc
= mChildren
[childIdx
];
1491 nsIFrame
* primaryFrame
= childAcc
->GetFrame();
1492 NS_ENSURE_TRUE(primaryFrame
, -1);
1494 nsIFrame
* frame
= primaryFrame
;
1496 nsIContent
* content
= frame
->GetContent();
1497 NS_ENSURE_TRUE(content
, -1);
1498 nsPoint pointInFrame
= pointInHyperText
- frame
->GetOffsetTo(hyperFrame
);
1499 nsSize frameSize
= frame
->GetSize();
1500 if (pointInFrame
.x
< frameSize
.width
&&
1501 pointInFrame
.y
< frameSize
.height
) {
1503 if (frame
->IsTextFrame()) {
1504 nsIFrame::ContentOffsets contentOffsets
=
1505 frame
->GetContentOffsetsFromPointExternal(
1506 pointInFrame
, nsIFrame::IGNORE_SELECTION_STYLE
);
1507 if (contentOffsets
.IsNull() || contentOffsets
.content
!= content
) {
1508 return -1; // Not found
1510 uint32_t addToOffset
;
1511 nsresult rv
= ContentToRenderedOffset(
1512 primaryFrame
, contentOffsets
.offset
, &addToOffset
);
1513 NS_ENSURE_SUCCESS(rv
, -1);
1514 offset
+= addToOffset
;
1518 frame
= frame
->GetNextContinuation();
1521 offset
+= nsAccUtils::TextLength(childAcc
);
1524 return -1; // Not found
1527 nsIntRect
HyperTextAccessible::TextBounds(int32_t aStartOffset
,
1529 uint32_t aCoordType
) {
1530 index_t startOffset
= ConvertMagicOffset(aStartOffset
);
1531 index_t endOffset
= ConvertMagicOffset(aEndOffset
);
1532 if (!startOffset
.IsValid() || !endOffset
.IsValid() ||
1533 startOffset
> endOffset
|| endOffset
> CharacterCount()) {
1534 NS_ERROR("Wrong in offset");
1538 if (CharacterCount() == 0) {
1539 nsPresContext
* presContext
= mDoc
->PresContext();
1540 // Empty content, use our own bound to at least get x,y coordinates
1541 nsIFrame
* frame
= GetFrame();
1545 return frame
->GetScreenRectInAppUnits().ToNearestPixels(
1546 presContext
->AppUnitsPerDevPixel());
1549 int32_t childIdx
= GetChildIndexAtOffset(startOffset
);
1550 if (childIdx
== -1) return nsIntRect();
1553 int32_t prevOffset
= GetChildOffset(childIdx
);
1554 int32_t offset1
= startOffset
- prevOffset
;
1556 while (childIdx
< static_cast<int32_t>(ChildCount())) {
1557 nsIFrame
* frame
= LocalChildAt(childIdx
++)->GetFrame();
1559 MOZ_ASSERT_UNREACHABLE("No frame for a child!");
1563 int32_t nextOffset
= GetChildOffset(childIdx
);
1564 if (nextOffset
>= static_cast<int32_t>(endOffset
)) {
1566 bounds
, GetBoundsInFrame(frame
, offset1
, endOffset
- prevOffset
));
1570 bounds
.UnionRect(bounds
,
1571 GetBoundsInFrame(frame
, offset1
, nextOffset
- prevOffset
));
1573 prevOffset
= nextOffset
;
1577 // This document may have a resolution set, we will need to multiply
1578 // the document-relative coordinates by that value and re-apply the doc's
1579 // screen coordinates.
1580 nsPresContext
* presContext
= mDoc
->PresContext();
1581 nsIFrame
* rootFrame
= presContext
->PresShell()->GetRootFrame();
1582 nsIntRect orgRectPixels
=
1583 rootFrame
->GetScreenRectInAppUnits().ToNearestPixels(
1584 presContext
->AppUnitsPerDevPixel());
1585 bounds
.MoveBy(-orgRectPixels
.X(), -orgRectPixels
.Y());
1586 bounds
.ScaleRoundOut(presContext
->PresShell()->GetResolution());
1587 bounds
.MoveBy(orgRectPixels
.X(), orgRectPixels
.Y());
1589 auto boundsX
= bounds
.X();
1590 auto boundsY
= bounds
.Y();
1591 nsAccUtils::ConvertScreenCoordsTo(&boundsX
, &boundsY
, aCoordType
, this);
1592 bounds
.MoveTo(boundsX
, boundsY
);
1596 already_AddRefed
<TextEditor
> HyperTextAccessible::GetEditor() const {
1597 if (!mContent
->HasFlag(NODE_IS_EDITABLE
)) {
1598 // If we're inside an editable container, then return that container's
1600 LocalAccessible
* ancestor
= LocalParent();
1602 HyperTextAccessible
* hyperText
= ancestor
->AsHyperText();
1604 // Recursion will stop at container doc because it has its own impl
1606 return hyperText
->GetEditor();
1609 ancestor
= ancestor
->LocalParent();
1615 nsCOMPtr
<nsIDocShell
> docShell
= nsCoreUtils::GetDocShellFor(mContent
);
1616 nsCOMPtr
<nsIEditingSession
> editingSession
;
1617 docShell
->GetEditingSession(getter_AddRefs(editingSession
));
1618 if (!editingSession
) return nullptr; // No editing session interface
1620 dom::Document
* docNode
= mDoc
->DocumentNode();
1621 RefPtr
<HTMLEditor
> htmlEditor
=
1622 editingSession
->GetHTMLEditorForWindow(docNode
->GetWindow());
1623 return htmlEditor
.forget();
1627 * =================== Caret & Selection ======================
1630 nsresult
HyperTextAccessible::SetSelectionRange(int32_t aStartPos
,
1632 // Before setting the selection range, we need to ensure that the editor
1633 // is initialized. (See bug 804927.)
1634 // Otherwise, it's possible that lazy editor initialization will override
1635 // the selection we set here and leave the caret at the end of the text.
1636 // By calling GetEditor here, we ensure that editor initialization is
1637 // completed before we set the selection.
1638 RefPtr
<TextEditor
> textEditor
= GetEditor();
1640 bool isFocusable
= InteractiveState() & states::FOCUSABLE
;
1642 // If accessible is focusable then focus it before setting the selection to
1643 // neglect control's selection changes on focus if any (for example, inputs
1644 // that do select all on focus).
1645 // some input controls
1646 if (isFocusable
) TakeFocus();
1648 RefPtr
<dom::Selection
> domSel
= DOMSelection();
1649 NS_ENSURE_STATE(domSel
);
1651 // Set up the selection.
1652 for (int32_t idx
= domSel
->RangeCount() - 1; idx
> 0; idx
--) {
1653 RefPtr
<nsRange
> range
{domSel
->GetRangeAt(idx
)};
1654 domSel
->RemoveRangeAndUnselectFramesAndNotifyListeners(*range
,
1657 SetSelectionBoundsAt(0, aStartPos
, aEndPos
);
1659 // Make sure it is visible
1660 domSel
->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION
,
1661 ScrollAxis(), ScrollAxis(),
1662 dom::Selection::SCROLL_FOR_CARET_MOVE
|
1663 dom::Selection::SCROLL_OVERFLOW_HIDDEN
);
1665 // When selection is done, move the focus to the selection if accessible is
1666 // not focusable. That happens when selection is set within hypertext
1668 if (isFocusable
) return NS_OK
;
1670 nsFocusManager
* DOMFocusManager
= nsFocusManager::GetFocusManager();
1671 if (DOMFocusManager
) {
1672 NS_ENSURE_TRUE(mDoc
, NS_ERROR_FAILURE
);
1673 dom::Document
* docNode
= mDoc
->DocumentNode();
1674 NS_ENSURE_TRUE(docNode
, NS_ERROR_FAILURE
);
1675 nsCOMPtr
<nsPIDOMWindowOuter
> window
= docNode
->GetWindow();
1676 RefPtr
<dom::Element
> result
;
1677 DOMFocusManager
->MoveFocus(
1678 window
, nullptr, nsIFocusManager::MOVEFOCUS_CARET
,
1679 nsIFocusManager::FLAG_BYMOVEFOCUS
, getter_AddRefs(result
));
1685 int32_t HyperTextAccessible::CaretOffset() const {
1686 // Not focused focusable accessible except document accessible doesn't have
1688 if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
1689 (InteractiveState() & states::FOCUSABLE
)) {
1693 // Check cached value.
1694 int32_t caretOffset
= -1;
1695 HyperTextAccessible
* text
= SelectionMgr()->AccessibleWithCaret(&caretOffset
);
1697 // Use cached value if it corresponds to this accessible.
1698 if (caretOffset
!= -1) {
1699 if (text
== this) return caretOffset
;
1701 nsINode
* textNode
= text
->GetNode();
1702 // Ignore offset if cached accessible isn't a text leaf.
1703 if (nsCoreUtils::IsAncestorOf(GetNode(), textNode
)) {
1704 return TransformOffset(text
, textNode
->IsText() ? caretOffset
: 0, false);
1708 // No caret if the focused node is not inside this DOM node and this DOM node
1709 // is not inside of focused node.
1710 FocusManager::FocusDisposition focusDisp
=
1711 FocusMgr()->IsInOrContainsFocus(this);
1712 if (focusDisp
== FocusManager::eNone
) return -1;
1714 // Turn the focus node and offset of the selection into caret hypretext
1716 dom::Selection
* domSel
= DOMSelection();
1717 NS_ENSURE_TRUE(domSel
, -1);
1719 nsINode
* focusNode
= domSel
->GetFocusNode();
1720 uint32_t focusOffset
= domSel
->FocusOffset();
1722 // No caret if this DOM node is inside of focused node but the selection's
1723 // focus point is not inside of this DOM node.
1724 if (focusDisp
== FocusManager::eContainedByFocus
) {
1725 nsINode
* resultNode
=
1726 nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode
, focusOffset
);
1728 nsINode
* thisNode
= GetNode();
1729 if (resultNode
!= thisNode
&&
1730 !nsCoreUtils::IsAncestorOf(thisNode
, resultNode
)) {
1735 return DOMPointToOffset(focusNode
, focusOffset
);
1738 int32_t HyperTextAccessible::CaretLineNumber() {
1739 // Provide the line number for the caret, relative to the
1740 // currently focused node. Use a 1-based index
1741 RefPtr
<nsFrameSelection
> frameSelection
= FrameSelection();
1742 if (!frameSelection
) return -1;
1744 dom::Selection
* domSel
= frameSelection
->GetSelection(SelectionType::eNormal
);
1745 if (!domSel
) return -1;
1747 nsINode
* caretNode
= domSel
->GetFocusNode();
1748 if (!caretNode
|| !caretNode
->IsContent()) return -1;
1750 nsIContent
* caretContent
= caretNode
->AsContent();
1751 if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent
)) return -1;
1753 int32_t returnOffsetUnused
;
1754 uint32_t caretOffset
= domSel
->FocusOffset();
1755 CaretAssociationHint hint
= frameSelection
->GetHint();
1756 nsIFrame
* caretFrame
= frameSelection
->GetFrameForNodeOffset(
1757 caretContent
, caretOffset
, hint
, &returnOffsetUnused
);
1758 NS_ENSURE_TRUE(caretFrame
, -1);
1760 int32_t lineNumber
= 1;
1761 nsAutoLineIterator lineIterForCaret
;
1762 nsIContent
* hyperTextContent
= IsContent() ? mContent
.get() : nullptr;
1763 while (caretFrame
) {
1764 if (hyperTextContent
== caretFrame
->GetContent()) {
1765 return lineNumber
; // Must be in a single line hyper text, there is no
1768 nsContainerFrame
* parentFrame
= caretFrame
->GetParent();
1769 if (!parentFrame
) break;
1771 // Add lines for the sibling frames before the caret
1772 nsIFrame
* sibling
= parentFrame
->PrincipalChildList().FirstChild();
1773 while (sibling
&& sibling
!= caretFrame
) {
1774 nsAutoLineIterator lineIterForSibling
= sibling
->GetLineIterator();
1775 if (lineIterForSibling
) {
1776 // For the frames before that grab all the lines
1777 int32_t addLines
= lineIterForSibling
->GetNumLines();
1778 lineNumber
+= addLines
;
1780 sibling
= sibling
->GetNextSibling();
1783 // Get the line number relative to the container with lines
1784 if (!lineIterForCaret
) { // Add the caret line just once
1785 lineIterForCaret
= parentFrame
->GetLineIterator();
1786 if (lineIterForCaret
) {
1787 // Ancestor of caret
1788 int32_t addLines
= lineIterForCaret
->FindLineContaining(caretFrame
);
1789 lineNumber
+= addLines
;
1793 caretFrame
= parentFrame
;
1796 MOZ_ASSERT_UNREACHABLE(
1797 "DOM ancestry had this hypertext but frame ancestry didn't");
1801 LayoutDeviceIntRect
HyperTextAccessible::GetCaretRect(nsIWidget
** aWidget
) {
1804 RefPtr
<nsCaret
> caret
= mDoc
->PresShellPtr()->GetCaret();
1805 NS_ENSURE_TRUE(caret
, LayoutDeviceIntRect());
1807 bool isVisible
= caret
->IsVisible();
1808 if (!isVisible
) return LayoutDeviceIntRect();
1811 nsIFrame
* frame
= caret
->GetGeometry(&rect
);
1812 if (!frame
|| rect
.IsEmpty()) return LayoutDeviceIntRect();
1815 // Offset from widget origin to the frame origin, which includes chrome
1817 *aWidget
= frame
->GetNearestWidget(offset
);
1818 NS_ENSURE_TRUE(*aWidget
, LayoutDeviceIntRect());
1819 rect
.MoveBy(offset
);
1821 LayoutDeviceIntRect caretRect
= LayoutDeviceIntRect::FromUnknownRect(
1822 rect
.ToOutsidePixels(frame
->PresContext()->AppUnitsPerDevPixel()));
1824 // ((content screen origin) - (content offset in the widget)) = widget origin on the screen
1826 caretRect
.MoveBy((*aWidget
)->WidgetToScreenOffset() -
1827 (*aWidget
)->GetClientOffset());
1829 // Correct for character size, so that caret always matches the size of
1830 // the character. This is important for font size transitions, and is
1831 // necessary because the Gecko caret uses the previous character's size as
1832 // the user moves forward in the text by character.
1833 nsIntRect charRect
= CharBounds(
1834 CaretOffset(), nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE
);
1835 if (!charRect
.IsEmpty()) {
1836 caretRect
.SetTopEdge(charRect
.Y());
1841 void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType
,
1842 nsTArray
<nsRange
*>* aRanges
) {
1843 // Ignore selection if it is not visible.
1844 RefPtr
<nsFrameSelection
> frameSelection
= FrameSelection();
1845 if (!frameSelection
|| frameSelection
->GetDisplaySelection() <=
1846 nsISelectionController::SELECTION_HIDDEN
) {
1850 dom::Selection
* domSel
= frameSelection
->GetSelection(aSelectionType
);
1851 if (!domSel
) return;
1853 nsINode
* startNode
= GetNode();
1855 RefPtr
<TextEditor
> textEditor
= GetEditor();
1857 startNode
= textEditor
->GetRoot();
1860 if (!startNode
) return;
1862 uint32_t childCount
= startNode
->GetChildCount();
1863 nsresult rv
= domSel
->GetRangesForIntervalArray(startNode
, 0, startNode
,
1864 childCount
, true, aRanges
);
1865 NS_ENSURE_SUCCESS_VOID(rv
);
1867 // Remove collapsed ranges
1868 aRanges
->RemoveElementsBy(
1869 [](const auto& range
) { return range
->Collapsed(); });
1872 int32_t HyperTextAccessible::SelectionCount() {
1873 nsTArray
<nsRange
*> ranges
;
1874 GetSelectionDOMRanges(SelectionType::eNormal
, &ranges
);
1875 return ranges
.Length();
1878 bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum
,
1879 int32_t* aStartOffset
,
1880 int32_t* aEndOffset
) {
1881 *aStartOffset
= *aEndOffset
= 0;
1883 nsTArray
<nsRange
*> ranges
;
1884 GetSelectionDOMRanges(SelectionType::eNormal
, &ranges
);
1886 uint32_t rangeCount
= ranges
.Length();
1887 if (aSelectionNum
< 0 || aSelectionNum
>= static_cast<int32_t>(rangeCount
)) {
1891 nsRange
* range
= ranges
[aSelectionNum
];
1893 // Get start and end points.
1894 nsINode
* startNode
= range
->GetStartContainer();
1895 nsINode
* endNode
= range
->GetEndContainer();
1896 int32_t startOffset
= range
->StartOffset(), endOffset
= range
->EndOffset();
1898 // Make sure start is before end, by swapping DOM points. This occurs when
1899 // the user selects backwards in the text.
1900 const Maybe
<int32_t> order
=
1901 nsContentUtils::ComparePoints(endNode
, endOffset
, startNode
, startOffset
);
1904 MOZ_ASSERT_UNREACHABLE();
1909 nsINode
* tempNode
= startNode
;
1910 startNode
= endNode
;
1912 int32_t tempOffset
= startOffset
;
1913 startOffset
= endOffset
;
1914 endOffset
= tempOffset
;
1917 if (!startNode
->IsInclusiveDescendantOf(mContent
)) {
1920 *aStartOffset
= DOMPointToOffset(startNode
, startOffset
);
1923 if (!endNode
->IsInclusiveDescendantOf(mContent
)) {
1924 *aEndOffset
= CharacterCount();
1926 *aEndOffset
= DOMPointToOffset(endNode
, endOffset
, true);
1931 bool HyperTextAccessible::SetSelectionBoundsAt(int32_t aSelectionNum
,
1932 int32_t aStartOffset
,
1933 int32_t aEndOffset
) {
1934 index_t startOffset
= ConvertMagicOffset(aStartOffset
);
1935 index_t endOffset
= ConvertMagicOffset(aEndOffset
);
1936 if (!startOffset
.IsValid() || !endOffset
.IsValid() ||
1937 std::max(startOffset
, endOffset
) > CharacterCount()) {
1938 NS_ERROR("Wrong in offset");
1942 TextRange
range(this, this, startOffset
, this, endOffset
);
1943 return range
.SetSelectionAt(aSelectionNum
);
1946 bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum
) {
1947 RefPtr
<dom::Selection
> domSel
= DOMSelection();
1948 if (!domSel
) return false;
1950 if (aSelectionNum
< 0 ||
1951 aSelectionNum
>= static_cast<int32_t>(domSel
->RangeCount())) {
1955 const RefPtr
<nsRange
> range
{domSel
->GetRangeAt(aSelectionNum
)};
1956 domSel
->RemoveRangeAndUnselectFramesAndNotifyListeners(*range
,
1961 void HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset
,
1963 uint32_t aScrollType
) {
1964 TextRange
range(this, this, aStartOffset
, this, aEndOffset
);
1965 range
.ScrollIntoView(aScrollType
);
1968 void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset
,
1970 uint32_t aCoordinateType
,
1971 int32_t aX
, int32_t aY
) {
1972 nsIFrame
* frame
= GetFrame();
1976 nsAccUtils::ConvertToScreenCoords(aX
, aY
, aCoordinateType
, this);
1978 RefPtr
<nsRange
> domRange
= nsRange::Create(mContent
);
1979 TextRange
range(this, this, aStartOffset
, this, aEndOffset
);
1980 if (!range
.AssignDOMRange(domRange
)) {
1984 nsPresContext
* presContext
= frame
->PresContext();
1985 nsPoint coordsInAppUnits
=
1986 ToAppUnits(coords
, presContext
->AppUnitsPerDevPixel());
1988 bool initialScrolled
= false;
1989 nsIFrame
* parentFrame
= frame
;
1990 while ((parentFrame
= parentFrame
->GetParent())) {
1991 nsIScrollableFrame
* scrollableFrame
= do_QueryFrame(parentFrame
);
1992 if (scrollableFrame
) {
1993 if (!initialScrolled
) {
1994 // Scroll substring to the given point. Turn the point into percents
1995 // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
1996 nsRect frameRect
= parentFrame
->GetScreenRectInAppUnits();
1997 nscoord offsetPointX
= coordsInAppUnits
.x
- frameRect
.X();
1998 nscoord offsetPointY
= coordsInAppUnits
.y
- frameRect
.Y();
2000 nsSize
size(parentFrame
->GetSize());
2002 // avoid divide by zero
2003 size
.width
= size
.width
? size
.width
: 1;
2004 size
.height
= size
.height
? size
.height
: 1;
2006 int16_t hPercent
= offsetPointX
* 100 / size
.width
;
2007 int16_t vPercent
= offsetPointY
* 100 / size
.height
;
2009 nsresult rv
= nsCoreUtils::ScrollSubstringTo(
2010 frame
, domRange
, ScrollAxis(vPercent
, WhenToScroll::Always
),
2011 ScrollAxis(hPercent
, WhenToScroll::Always
));
2012 if (NS_FAILED(rv
)) return;
2014 initialScrolled
= true;
2016 // Substring was scrolled to the given point already inside its closest
2017 // scrollable area. If there are nested scrollable areas then make
2018 // sure we scroll lower areas to the given point inside currently
2019 // traversed scrollable area.
2020 nsCoreUtils::ScrollFrameToPoint(parentFrame
, frame
, coords
);
2023 frame
= parentFrame
;
2027 void HyperTextAccessible::EnclosingRange(a11y::TextRange
& aRange
) const {
2028 if (IsTextField()) {
2029 aRange
.Set(mDoc
, const_cast<HyperTextAccessible
*>(this), 0,
2030 const_cast<HyperTextAccessible
*>(this), CharacterCount());
2032 aRange
.Set(mDoc
, mDoc
, 0, mDoc
, mDoc
->CharacterCount());
2036 void HyperTextAccessible::SelectionRanges(
2037 nsTArray
<a11y::TextRange
>* aRanges
) const {
2038 dom::Selection
* sel
= DOMSelection();
2043 TextRange::TextRangesFromSelection(sel
, aRanges
);
2046 void HyperTextAccessible::VisibleRanges(
2047 nsTArray
<a11y::TextRange
>* aRanges
) const {}
2049 void HyperTextAccessible::RangeByChild(LocalAccessible
* aChild
,
2050 a11y::TextRange
& aRange
) const {
2051 HyperTextAccessible
* ht
= aChild
->AsHyperText();
2053 aRange
.Set(mDoc
, ht
, 0, ht
, ht
->CharacterCount());
2057 LocalAccessible
* child
= aChild
;
2058 LocalAccessible
* parent
= nullptr;
2059 while ((parent
= child
->LocalParent()) && !(ht
= parent
->AsHyperText())) {
2063 // If no text then return collapsed text range, otherwise return a range
2064 // containing the text enclosed by the given child.
2066 int32_t childIdx
= child
->IndexInParent();
2067 int32_t startOffset
= ht
->GetChildOffset(childIdx
);
2069 child
->IsTextLeaf() ? ht
->GetChildOffset(childIdx
+ 1) : startOffset
;
2070 aRange
.Set(mDoc
, ht
, startOffset
, ht
, endOffset
);
2074 void HyperTextAccessible::RangeAtPoint(int32_t aX
, int32_t aY
,
2075 a11y::TextRange
& aRange
) const {
2076 LocalAccessible
* child
=
2077 mDoc
->LocalChildAtPoint(aX
, aY
, EWhichChildAtPoint::DeepestChild
);
2080 LocalAccessible
* parent
= nullptr;
2081 while ((parent
= child
->LocalParent()) && !parent
->IsHyperText()) {
2085 // Return collapsed text range for the point.
2087 HyperTextAccessible
* ht
= parent
->AsHyperText();
2088 int32_t offset
= ht
->GetChildOffset(child
);
2089 aRange
.Set(mDoc
, ht
, offset
, ht
, offset
);
2093 ////////////////////////////////////////////////////////////////////////////////
2094 // LocalAccessible public
2096 // LocalAccessible protected
2097 ENameValueFlag
HyperTextAccessible::NativeName(nsString
& aName
) const {
2098 // Check @alt attribute for invalid img elements.
2099 bool hasImgAlt
= false;
2100 if (mContent
->IsHTMLElement(nsGkAtoms::img
)) {
2101 hasImgAlt
= mContent
->AsElement()->GetAttr(kNameSpaceID_None
,
2102 nsGkAtoms::alt
, aName
);
2103 if (!aName
.IsEmpty()) return eNameOK
;
2106 ENameValueFlag nameFlag
= AccessibleWrap::NativeName(aName
);
2107 if (!aName
.IsEmpty()) return nameFlag
;
2109 // Get name from title attribute for HTML abbr and acronym elements making it
2110 // a valid name from markup. Otherwise their name isn't picked up by recursive
2111 // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
2112 if (IsAbbreviation() && mContent
->AsElement()->GetAttr(
2113 kNameSpaceID_None
, nsGkAtoms::title
, aName
)) {
2114 aName
.CompressWhitespace();
2117 return hasImgAlt
? eNoNameOnPurpose
: eNameOK
;
2120 void HyperTextAccessible::Shutdown() {
2122 AccessibleWrap::Shutdown();
2125 bool HyperTextAccessible::RemoveChild(LocalAccessible
* aAccessible
) {
2126 const int32_t childIndex
= aAccessible
->IndexInParent();
2127 if (childIndex
< static_cast<int64_t>(mOffsets
.Length())) {
2128 mOffsets
.RemoveLastElements(mOffsets
.Length() -
2129 aAccessible
->IndexInParent());
2132 return AccessibleWrap::RemoveChild(aAccessible
);
2135 bool HyperTextAccessible::InsertChildAt(uint32_t aIndex
,
2136 LocalAccessible
* aChild
) {
2137 if (aIndex
< mOffsets
.Length()) {
2138 mOffsets
.RemoveLastElements(mOffsets
.Length() - aIndex
);
2141 return AccessibleWrap::InsertChildAt(aIndex
, aChild
);
2144 Relation
HyperTextAccessible::RelationByType(RelationType aType
) const {
2145 Relation rel
= LocalAccessible::RelationByType(aType
);
2148 case RelationType::NODE_CHILD_OF
:
2149 if (HasOwnContent() && mContent
->IsMathMLElement()) {
2150 LocalAccessible
* parent
= LocalParent();
2152 nsIContent
* parentContent
= parent
->GetContent();
2153 if (parentContent
&&
2154 parentContent
->IsMathMLElement(nsGkAtoms::mroot_
)) {
2155 // Add a relation pointing to the parent <mroot>.
2156 rel
.AppendTarget(parent
);
2161 case RelationType::NODE_PARENT_OF
:
2162 if (HasOwnContent() && mContent
->IsMathMLElement(nsGkAtoms::mroot_
)) {
2163 LocalAccessible
* base
= LocalChildAt(0);
2164 LocalAccessible
* index
= LocalChildAt(1);
2165 if (base
&& index
) {
2166 // Append the <mroot> children in the order index, base.
2167 rel
.AppendTarget(index
);
2168 rel
.AppendTarget(base
);
2179 ////////////////////////////////////////////////////////////////////////////////
2180 // HyperTextAccessible public static
2182 nsresult
HyperTextAccessible::ContentToRenderedOffset(
2183 nsIFrame
* aFrame
, int32_t aContentOffset
, uint32_t* aRenderedOffset
) const {
2185 // Current frame not rendered -- this can happen if text is set on
2186 // something with display: none
2187 *aRenderedOffset
= 0;
2191 if (IsTextField()) {
2192 *aRenderedOffset
= aContentOffset
;
2196 NS_ASSERTION(aFrame
->IsTextFrame(), "Need text frame for offset conversion");
2197 NS_ASSERTION(aFrame
->GetPrevContinuation() == nullptr,
2198 "Call on primary frame only");
2200 nsIFrame::RenderedText text
=
2201 aFrame
->GetRenderedText(aContentOffset
, aContentOffset
+ 1,
2202 nsIFrame::TextOffsetType::OffsetsInContentText
,
2203 nsIFrame::TrailingWhitespace::DontTrim
);
2204 *aRenderedOffset
= text
.mOffsetWithinNodeRenderedText
;
2209 nsresult
HyperTextAccessible::RenderedToContentOffset(
2210 nsIFrame
* aFrame
, uint32_t aRenderedOffset
, int32_t* aContentOffset
) const {
2211 if (IsTextField()) {
2212 *aContentOffset
= aRenderedOffset
;
2216 *aContentOffset
= 0;
2217 NS_ENSURE_TRUE(aFrame
, NS_ERROR_FAILURE
);
2219 NS_ASSERTION(aFrame
->IsTextFrame(), "Need text frame for offset conversion");
2220 NS_ASSERTION(aFrame
->GetPrevContinuation() == nullptr,
2221 "Call on primary frame only");
2223 nsIFrame::RenderedText text
=
2224 aFrame
->GetRenderedText(aRenderedOffset
, aRenderedOffset
+ 1,
2225 nsIFrame::TextOffsetType::OffsetsInRenderedText
,
2226 nsIFrame::TrailingWhitespace::DontTrim
);
2227 *aContentOffset
= text
.mOffsetWithinNodeText
;
2232 ////////////////////////////////////////////////////////////////////////////////
2233 // HyperTextAccessible public
2235 int32_t HyperTextAccessible::GetChildOffset(uint32_t aChildIndex
,
2236 bool aInvalidateAfter
) const {
2237 if (aChildIndex
== 0) {
2238 if (aInvalidateAfter
) mOffsets
.Clear();
2243 int32_t count
= mOffsets
.Length() - aChildIndex
;
2245 if (aInvalidateAfter
) mOffsets
.RemoveElementsAt(aChildIndex
, count
);
2247 return mOffsets
[aChildIndex
- 1];
2250 uint32_t lastOffset
=
2251 mOffsets
.IsEmpty() ? 0 : mOffsets
[mOffsets
.Length() - 1];
2253 while (mOffsets
.Length() < aChildIndex
) {
2254 LocalAccessible
* child
= mChildren
[mOffsets
.Length()];
2255 lastOffset
+= nsAccUtils::TextLength(child
);
2256 mOffsets
.AppendElement(lastOffset
);
2259 return mOffsets
[aChildIndex
- 1];
2262 int32_t HyperTextAccessible::GetChildIndexAtOffset(uint32_t aOffset
) const {
2263 uint32_t lastOffset
= 0;
2264 const uint32_t offsetCount
= mOffsets
.Length();
2266 if (offsetCount
> 0) {
2267 lastOffset
= mOffsets
[offsetCount
- 1];
2268 if (aOffset
< lastOffset
) {
2270 if (BinarySearch(mOffsets
, 0, offsetCount
, aOffset
, &index
)) {
2271 return (index
< (offsetCount
- 1)) ? index
+ 1 : index
;
2274 return (index
== offsetCount
) ? -1 : index
;
2278 uint32_t childCount
= ChildCount();
2279 while (mOffsets
.Length() < childCount
) {
2280 LocalAccessible
* child
= LocalChildAt(mOffsets
.Length());
2281 lastOffset
+= nsAccUtils::TextLength(child
);
2282 mOffsets
.AppendElement(lastOffset
);
2283 if (aOffset
< lastOffset
) return mOffsets
.Length() - 1;
2286 if (aOffset
== lastOffset
) return mOffsets
.Length() - 1;
2291 ////////////////////////////////////////////////////////////////////////////////
2292 // HyperTextAccessible protected
2294 nsresult
HyperTextAccessible::GetDOMPointByFrameOffset(
2295 nsIFrame
* aFrame
, int32_t aOffset
, LocalAccessible
* aAccessible
,
2297 NS_ENSURE_ARG(aAccessible
);
2300 // If the given frame is null then set offset after the DOM node of the
2301 // given accessible.
2302 NS_ASSERTION(!aAccessible
->IsDoc(),
2303 "Shouldn't be called on document accessible!");
2305 nsIContent
* content
= aAccessible
->GetContent();
2306 NS_ASSERTION(content
, "Shouldn't operate on defunct accessible!");
2308 nsIContent
* parent
= content
->GetParent();
2310 aPoint
->idx
= parent
->ComputeIndexOf(content
) + 1;
2311 aPoint
->node
= parent
;
2313 } else if (aFrame
->IsTextFrame()) {
2314 nsIContent
* content
= aFrame
->GetContent();
2315 NS_ENSURE_STATE(content
);
2317 nsIFrame
* primaryFrame
= content
->GetPrimaryFrame();
2319 RenderedToContentOffset(primaryFrame
, aOffset
, &(aPoint
->idx
));
2320 NS_ENSURE_SUCCESS(rv
, rv
);
2322 aPoint
->node
= content
;
2325 nsIContent
* content
= aFrame
->GetContent();
2326 NS_ENSURE_STATE(content
);
2328 nsIContent
* parent
= content
->GetParent();
2329 NS_ENSURE_STATE(parent
);
2331 aPoint
->idx
= parent
->ComputeIndexOf(content
);
2332 aPoint
->node
= parent
;
2338 // HyperTextAccessible
2339 void HyperTextAccessible::GetSpellTextAttr(
2340 nsINode
* aNode
, int32_t aNodeOffset
, uint32_t* aStartOffset
,
2341 uint32_t* aEndOffset
, nsIPersistentProperties
* aAttributes
) {
2342 RefPtr
<nsFrameSelection
> fs
= FrameSelection();
2345 dom::Selection
* domSel
= fs
->GetSelection(SelectionType::eSpellCheck
);
2346 if (!domSel
) return;
2348 int32_t rangeCount
= domSel
->RangeCount();
2349 if (rangeCount
<= 0) return;
2351 uint32_t startOffset
= 0, endOffset
= 0;
2352 for (int32_t idx
= 0; idx
< rangeCount
; idx
++) {
2353 const nsRange
* range
= domSel
->GetRangeAt(idx
);
2354 if (range
->Collapsed()) continue;
2356 // See if the point comes after the range in which case we must continue in
2357 // case there is another range after this one.
2358 nsINode
* endNode
= range
->GetEndContainer();
2359 int32_t endNodeOffset
= range
->EndOffset();
2360 Maybe
<int32_t> order
= nsContentUtils::ComparePoints(
2361 aNode
, aNodeOffset
, endNode
, endNodeOffset
);
2362 if (NS_WARN_IF(!order
)) {
2370 // At this point our point is either in this range or before it but after
2371 // the previous range. So we check to see if the range starts before the
2372 // point in which case the point is in the missspelled range, otherwise it
2373 // must be before the range and after the previous one if any.
2374 nsINode
* startNode
= range
->GetStartContainer();
2375 int32_t startNodeOffset
= range
->StartOffset();
2376 order
= nsContentUtils::ComparePoints(startNode
, startNodeOffset
, aNode
,
2379 // As (`aNode`, `aNodeOffset`) is comparable to the end of the range, it
2380 // should also be comparable to the range's start. Returning here
2381 // prevents crashes in release builds.
2382 MOZ_ASSERT_UNREACHABLE();
2387 startOffset
= DOMPointToOffset(startNode
, startNodeOffset
);
2389 endOffset
= DOMPointToOffset(endNode
, endNodeOffset
);
2391 if (startOffset
> *aStartOffset
) *aStartOffset
= startOffset
;
2393 if (endOffset
< *aEndOffset
) *aEndOffset
= endOffset
;
2396 nsAccUtils::SetAccAttr(aAttributes
, nsGkAtoms::invalid
, u
"spelling"_ns
);
2402 // This range came after the point.
2403 endOffset
= DOMPointToOffset(startNode
, startNodeOffset
);
2406 const nsRange
* prevRange
= domSel
->GetRangeAt(idx
- 1);
2407 startOffset
= DOMPointToOffset(prevRange
->GetEndContainer(),
2408 prevRange
->EndOffset());
2411 // The previous range might not be within this accessible. In that case,
2412 // DOMPointToOffset returns length as a fallback. We don't want to use
2413 // that offset if so, hence the startOffset < *aEndOffset check.
2414 if (startOffset
> *aStartOffset
&& startOffset
< *aEndOffset
) {
2415 *aStartOffset
= startOffset
;
2418 if (endOffset
< *aEndOffset
) *aEndOffset
= endOffset
;
2423 // We never found a range that ended after the point, therefore we know that
2424 // the point is not in a range, that we do not need to compute an end offset,
2425 // and that we should use the end offset of the last range to compute the
2426 // start offset of the text attribute range.
2427 const nsRange
* prevRange
= domSel
->GetRangeAt(rangeCount
- 1);
2429 DOMPointToOffset(prevRange
->GetEndContainer(), prevRange
->EndOffset());
2431 // The previous range might not be within this accessible. In that case,
2432 // DOMPointToOffset returns length as a fallback. We don't want to use
2433 // that offset if so, hence the startOffset < *aEndOffset check.
2434 if (startOffset
> *aStartOffset
&& startOffset
< *aEndOffset
) {
2435 *aStartOffset
= startOffset
;