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/. */
6
7 #include "HyperTextAccessible-inl.h"
8
9 #include "Accessible-inl.h"
10 #include "nsAccessibilityService.h"
11 #include "nsIAccessibleTypes.h"
12 #include "DocAccessible.h"
13 #include "HTMLListAccessible.h"
14 #include "Relation.h"
15 #include "Role.h"
16 #include "States.h"
17 #include "TextAttrs.h"
18 #include "TextRange.h"
19 #include "TreeWalker.h"
20
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 "nsIInterfaceRequestorUtils.h"
30 #include "nsPersistentProperties.h"
31 #include "nsIScrollableFrame.h"
32 #include "nsIMathMLFrame.h"
33 #include "nsRange.h"
34 #include "nsTextFragment.h"
35 #include "mozilla/Assertions.h"
36 #include "mozilla/BinarySearch.h"
37 #include "mozilla/EventStates.h"
38 #include "mozilla/HTMLEditor.h"
39 #include "mozilla/MathAlgorithms.h"
40 #include "mozilla/PresShell.h"
41 #include "mozilla/TextEditor.h"
42 #include "mozilla/dom/Element.h"
43 #include "mozilla/dom/HTMLBRElement.h"
44 #include "mozilla/dom/HTMLHeadingElement.h"
45 #include "mozilla/dom/Selection.h"
46 #include "gfxSkipChars.h"
47 #include <algorithm>
48
49 using namespace mozilla;
50 using namespace mozilla::a11y;
51
52 ////////////////////////////////////////////////////////////////////////////////
53 // HyperTextAccessible
54 ////////////////////////////////////////////////////////////////////////////////
55
HyperTextAccessible(nsIContent * aNode,DocAccessible * aDoc)56 HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc)
57 : AccessibleWrap(aNode, aDoc) {
58 mType = eHyperTextType;
59 mGenericTypes |= eHyperText;
60 }
61
NativeRole() const62 role HyperTextAccessible::NativeRole() const {
63 a11y::role r = GetAccService()->MarkupRole(mContent);
64 if (r != roles::NOTHING) return r;
65
66 nsIFrame* frame = GetFrame();
67 if (frame && frame->IsInlineFrame()) return roles::TEXT;
68
69 return roles::TEXT_CONTAINER;
70 }
71
NativeState() const72 uint64_t HyperTextAccessible::NativeState() const {
73 uint64_t states = AccessibleWrap::NativeState();
74
75 if (mContent->AsElement()->State().HasState(NS_EVENT_STATE_READWRITE)) {
76 states |= states::EDITABLE;
77
78 } else if (mContent->IsHTMLElement(nsGkAtoms::article)) {
79 // We want <article> to behave like a document in terms of readonly state.
80 states |= states::READONLY;
81 }
82
83 if (HasChildren()) states |= states::SELECTABLE_TEXT;
84
85 return states;
86 }
87
GetBoundsInFrame(nsIFrame * aFrame,uint32_t aStartRenderedOffset,uint32_t aEndRenderedOffset)88 nsIntRect HyperTextAccessible::GetBoundsInFrame(nsIFrame* aFrame,
89 uint32_t aStartRenderedOffset,
90 uint32_t aEndRenderedOffset) {
91 nsPresContext* presContext = mDoc->PresContext();
92 if (!aFrame->IsTextFrame()) {
93 return aFrame->GetScreenRectInAppUnits().ToNearestPixels(
94 presContext->AppUnitsPerDevPixel());
95 }
96
97 // Substring must be entirely within the same text node.
98 int32_t startContentOffset, endContentOffset;
99 nsresult rv = RenderedToContentOffset(aFrame, aStartRenderedOffset,
100 &startContentOffset);
101 NS_ENSURE_SUCCESS(rv, nsIntRect());
102 rv = RenderedToContentOffset(aFrame, aEndRenderedOffset, &endContentOffset);
103 NS_ENSURE_SUCCESS(rv, nsIntRect());
104
105 nsIFrame* frame;
106 int32_t startContentOffsetInFrame;
107 // Get the right frame continuation -- not really a child, but a sibling of
108 // the primary frame passed in
109 rv = aFrame->GetChildFrameContainingOffset(
110 startContentOffset, false, &startContentOffsetInFrame, &frame);
111 NS_ENSURE_SUCCESS(rv, nsIntRect());
112
113 nsRect screenRect;
114 while (frame && startContentOffset < endContentOffset) {
115 // Start with this frame's screen rect, which we will shrink based on
116 // the substring we care about within it. We will then add that frame to
117 // the total screenRect we are returning.
118 nsRect frameScreenRect = frame->GetScreenRectInAppUnits();
119
120 // Get the length of the substring in this frame that we want the bounds for
121 int32_t startFrameTextOffset, endFrameTextOffset;
122 frame->GetOffsets(startFrameTextOffset, endFrameTextOffset);
123 int32_t frameTotalTextLength = endFrameTextOffset - startFrameTextOffset;
124 int32_t seekLength = endContentOffset - startContentOffset;
125 int32_t frameSubStringLength =
126 std::min(frameTotalTextLength - startContentOffsetInFrame, seekLength);
127
128 // Add the point where the string starts to the frameScreenRect
129 nsPoint frameTextStartPoint;
130 rv = frame->GetPointFromOffset(startContentOffset, &frameTextStartPoint);
131 NS_ENSURE_SUCCESS(rv, nsIntRect());
132
133 // Use the point for the end offset to calculate the width
134 nsPoint frameTextEndPoint;
135 rv = frame->GetPointFromOffset(startContentOffset + frameSubStringLength,
136 &frameTextEndPoint);
137 NS_ENSURE_SUCCESS(rv, nsIntRect());
138
139 frameScreenRect.SetRectX(
140 frameScreenRect.X() +
141 std::min(frameTextStartPoint.x, frameTextEndPoint.x),
142 mozilla::Abs(frameTextStartPoint.x - frameTextEndPoint.x));
143
144 screenRect.UnionRect(frameScreenRect, screenRect);
145
146 // Get ready to loop back for next frame continuation
147 startContentOffset += frameSubStringLength;
148 startContentOffsetInFrame = 0;
149 frame = frame->GetNextContinuation();
150 }
151
152 return screenRect.ToNearestPixels(presContext->AppUnitsPerDevPixel());
153 }
154
TextSubstring(int32_t aStartOffset,int32_t aEndOffset,nsAString & aText)155 void HyperTextAccessible::TextSubstring(int32_t aStartOffset,
156 int32_t aEndOffset, nsAString& aText) {
157 aText.Truncate();
158
159 index_t startOffset = ConvertMagicOffset(aStartOffset);
160 index_t endOffset = ConvertMagicOffset(aEndOffset);
161 if (!startOffset.IsValid() || !endOffset.IsValid() ||
162 startOffset > endOffset || endOffset > CharacterCount()) {
163 NS_ERROR("Wrong in offset");
164 return;
165 }
166
167 int32_t startChildIdx = GetChildIndexAtOffset(startOffset);
168 if (startChildIdx == -1) return;
169
170 int32_t endChildIdx = GetChildIndexAtOffset(endOffset);
171 if (endChildIdx == -1) return;
172
173 if (startChildIdx == endChildIdx) {
174 int32_t childOffset = GetChildOffset(startChildIdx);
175 if (childOffset == -1) return;
176
177 Accessible* child = GetChildAt(startChildIdx);
178 child->AppendTextTo(aText, startOffset - childOffset,
179 endOffset - startOffset);
180 return;
181 }
182
183 int32_t startChildOffset = GetChildOffset(startChildIdx);
184 if (startChildOffset == -1) return;
185
186 Accessible* startChild = GetChildAt(startChildIdx);
187 startChild->AppendTextTo(aText, startOffset - startChildOffset);
188
189 for (int32_t childIdx = startChildIdx + 1; childIdx < endChildIdx;
190 childIdx++) {
191 Accessible* child = GetChildAt(childIdx);
192 child->AppendTextTo(aText);
193 }
194
195 int32_t endChildOffset = GetChildOffset(endChildIdx);
196 if (endChildOffset == -1) return;
197
198 Accessible* endChild = GetChildAt(endChildIdx);
199 endChild->AppendTextTo(aText, 0, endOffset - endChildOffset);
200 }
201
DOMPointToOffset(nsINode * aNode,int32_t aNodeOffset,bool aIsEndOffset) const202 uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode,
203 int32_t aNodeOffset,
204 bool aIsEndOffset) const {
205 if (!aNode) return 0;
206
207 uint32_t offset = 0;
208 nsINode* findNode = nullptr;
209
210 if (aNodeOffset == -1) {
211 findNode = aNode;
212
213 } else if (aNode->IsText()) {
214 // For text nodes, aNodeOffset comes in as a character offset
215 // Text offset will be added at the end, if we find the offset in this
216 // hypertext We want the "skipped" offset into the text (rendered text
217 // without the extra whitespace)
218 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame();
219 NS_ENSURE_TRUE(frame, 0);
220
221 nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset);
222 NS_ENSURE_SUCCESS(rv, 0);
223
224 findNode = aNode;
225
226 } else {
227 // findNode could be null if aNodeOffset == # of child nodes, which means
228 // one of two things:
229 // 1) there are no children, and the passed-in node is not mContent -- use
230 // parentContent for the node to find
231 // 2) there are no children and the passed-in node is mContent, which means
232 // we're an empty nsIAccessibleText
233 // 3) there are children and we're at the end of the children
234
235 findNode = aNode->GetChildAt_Deprecated(aNodeOffset);
236 if (!findNode) {
237 if (aNodeOffset == 0) {
238 if (aNode == GetNode()) {
239 // Case #1: this accessible has no children and thus has empty text,
240 // we can only be at hypertext offset 0.
241 return 0;
242 }
243
244 // Case #2: there are no children, we're at this node.
245 findNode = aNode;
246 } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) {
247 // Case #3: we're after the last child, get next node to this one.
248 for (nsINode* tmpNode = aNode;
249 !findNode && tmpNode && tmpNode != mContent;
250 tmpNode = tmpNode->GetParent()) {
251 findNode = tmpNode->GetNextSibling();
252 }
253 }
254 }
255 }
256
257 // Get accessible for this findNode, or if that node isn't accessible, use the
258 // accessible for the next DOM node which has one (based on forward depth
259 // first search)
260 Accessible* descendant = nullptr;
261 if (findNode) {
262 dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode);
263 if (brElement && brElement->IsPaddingForEmptyEditor()) {
264 // This <br> is the hacky "padding <br> element" used when there is no
265 // text in the editor.
266 return 0;
267 }
268
269 descendant = mDoc->GetAccessible(findNode);
270 if (!descendant && findNode->IsContent()) {
271 Accessible* container = mDoc->GetContainerAccessible(findNode);
272 if (container) {
273 TreeWalker walker(container, findNode->AsContent(),
274 TreeWalker::eWalkContextTree);
275 descendant = walker.Next();
276 if (!descendant) descendant = container;
277 }
278 }
279 }
280
281 return TransformOffset(descendant, offset, aIsEndOffset);
282 }
283
TransformOffset(Accessible * aDescendant,uint32_t aOffset,bool aIsEndOffset) const284 uint32_t HyperTextAccessible::TransformOffset(Accessible* aDescendant,
285 uint32_t aOffset,
286 bool aIsEndOffset) const {
287 // From the descendant, go up and get the immediate child of this hypertext.
288 uint32_t offset = aOffset;
289 Accessible* descendant = aDescendant;
290 while (descendant) {
291 Accessible* parent = descendant->Parent();
292 if (parent == this) return GetChildOffset(descendant) + offset;
293
294 // This offset no longer applies because the passed-in text object is not
295 // a child of the hypertext. This happens when there are nested hypertexts,
296 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset
297 // to make it relative the hypertext.
298 // If the end offset is not supposed to be inclusive and the original point
299 // is not at 0 offset then the returned offset should be after an embedded
300 // character the original point belongs to.
301 if (aIsEndOffset) {
302 // Similar to our special casing in FindOffset, we add handling for
303 // bulleted lists here because PeekOffset returns the inner text node
304 // for a list when it should return the list bullet.
305 // We manually set the offset so the error doesn't propagate up.
306 if (offset == 0 && parent && parent->IsHTMLListItem() &&
307 descendant->PrevSibling() && descendant->PrevSibling()->GetFrame() &&
308 descendant->PrevSibling()->GetFrame()->IsBulletFrame()) {
309 offset = 0;
310 } else {
311 offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0;
312 }
313 } else {
314 offset = 0;
315 }
316
317 descendant = parent;
318 }
319
320 // If the given a11y point cannot be mapped into offset relative this
321 // hypertext offset then return length as fallback value.
322 return CharacterCount();
323 }
324
325 /**
326 * GetElementAsContentOf() returns a content representing an element which is
327 * or includes aNode.
328 *
329 * XXX This method is enough to retrieve ::before or ::after pseudo element.
330 * So, if you want to use this for other purpose, you might need to check
331 * ancestors too.
332 */
GetElementAsContentOf(nsINode * aNode)333 static nsIContent* GetElementAsContentOf(nsINode* aNode) {
334 if (auto* element = dom::Element::FromNode(aNode)) {
335 return element;
336 }
337 return aNode->GetParentElement();
338 }
339
OffsetsToDOMRange(int32_t aStartOffset,int32_t aEndOffset,nsRange * aRange) const340 bool HyperTextAccessible::OffsetsToDOMRange(int32_t aStartOffset,
341 int32_t aEndOffset,
342 nsRange* aRange) const {
343 DOMPoint startPoint = OffsetToDOMPoint(aStartOffset);
344 if (!startPoint.node) return false;
345
346 // HyperTextAccessible manages pseudo elements generated by ::before or
347 // ::after. However, contents of them are not in the DOM tree normally.
348 // Therefore, they are not selectable and editable. So, when this creates
349 // a DOM range, it should not start from nor end in any pseudo contents.
350
351 nsIContent* container = GetElementAsContentOf(startPoint.node);
352 DOMPoint startPointForDOMRange =
353 ClosestNotGeneratedDOMPoint(startPoint, container);
354 aRange->SetStart(startPointForDOMRange.node, startPointForDOMRange.idx);
355
356 // If the caller wants collapsed range, let's collapse the range to its start.
357 if (aStartOffset == aEndOffset) {
358 aRange->Collapse(true);
359 return true;
360 }
361
362 DOMPoint endPoint = OffsetToDOMPoint(aEndOffset);
363 if (!endPoint.node) return false;
364
365 if (startPoint.node != endPoint.node) {
366 container = GetElementAsContentOf(endPoint.node);
367 }
368
369 DOMPoint endPointForDOMRange =
370 ClosestNotGeneratedDOMPoint(endPoint, container);
371 aRange->SetEnd(endPointForDOMRange.node, endPointForDOMRange.idx);
372 return true;
373 }
374
OffsetToDOMPoint(int32_t aOffset) const375 DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const {
376 // 0 offset is valid even if no children. In this case the associated editor
377 // is empty so return a DOM point for editor root element.
378 if (aOffset == 0) {
379 RefPtr<TextEditor> textEditor = GetEditor();
380 if (textEditor) {
381 if (textEditor->IsEmpty()) {
382 return DOMPoint(textEditor->GetRoot(), 0);
383 }
384 }
385 }
386
387 int32_t childIdx = GetChildIndexAtOffset(aOffset);
388 if (childIdx == -1) return DOMPoint();
389
390 Accessible* child = GetChildAt(childIdx);
391 int32_t innerOffset = aOffset - GetChildOffset(childIdx);
392
393 // A text leaf case.
394 if (child->IsTextLeaf()) {
395 // The point is inside the text node. This is always true for any text leaf
396 // except a last child one. See assertion below.
397 if (aOffset < GetChildOffset(childIdx + 1)) {
398 nsIContent* content = child->GetContent();
399 int32_t idx = 0;
400 if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(),
401 innerOffset, &idx)))
402 return DOMPoint();
403
404 return DOMPoint(content, idx);
405 }
406
407 // Set the DOM point right after the text node.
408 MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount());
409 innerOffset = 1;
410 }
411
412 // Case of embedded object. The point is either before or after the element.
413 NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!");
414 nsINode* node = child->GetNode();
415 nsINode* parentNode = node->GetParentNode();
416 return parentNode ? DOMPoint(parentNode,
417 parentNode->ComputeIndexOf(node) + innerOffset)
418 : DOMPoint();
419 }
420
ClosestNotGeneratedDOMPoint(const DOMPoint & aDOMPoint,nsIContent * aElementContent) const421 DOMPoint HyperTextAccessible::ClosestNotGeneratedDOMPoint(
422 const DOMPoint& aDOMPoint, nsIContent* aElementContent) const {
423 MOZ_ASSERT(aDOMPoint.node, "The node must not be null");
424
425 // ::before pseudo element
426 if (aElementContent &&
427 aElementContent->IsGeneratedContentContainerForBefore()) {
428 MOZ_ASSERT(aElementContent->GetParent(),
429 "::before must have parent element");
430 // The first child of its parent (i.e., immediately after the ::before) is
431 // good point for a DOM range.
432 return DOMPoint(aElementContent->GetParent(), 0);
433 }
434
435 // ::after pseudo element
436 if (aElementContent &&
437 aElementContent->IsGeneratedContentContainerForAfter()) {
438 MOZ_ASSERT(aElementContent->GetParent(),
439 "::after must have parent element");
440 // The end of its parent (i.e., immediately before the ::after) is good
441 // point for a DOM range.
442 return DOMPoint(aElementContent->GetParent(),
443 aElementContent->GetParent()->GetChildCount());
444 }
445
446 return aDOMPoint;
447 }
448
FindOffset(uint32_t aOffset,nsDirection aDirection,nsSelectionAmount aAmount,EWordMovementType aWordMovementType)449 uint32_t HyperTextAccessible::FindOffset(uint32_t aOffset,
450 nsDirection aDirection,
451 nsSelectionAmount aAmount,
452 EWordMovementType aWordMovementType) {
453 NS_ASSERTION(aDirection == eDirPrevious || aAmount != eSelectBeginLine,
454 "eSelectBeginLine should only be used with eDirPrevious");
455
456 // Find a leaf accessible frame to start with. PeekOffset wants this.
457 HyperTextAccessible* text = this;
458 Accessible* child = nullptr;
459 int32_t innerOffset = aOffset;
460
461 do {
462 int32_t childIdx = text->GetChildIndexAtOffset(innerOffset);
463
464 // We can have an empty text leaf as our only child. Since empty text
465 // leaves are not accessible we then have no children, but 0 is a valid
466 // innerOffset.
467 if (childIdx == -1) {
468 NS_ASSERTION(innerOffset == 0 && !text->ChildCount(), "No childIdx?");
469 return DOMPointToOffset(text->GetNode(), 0, aDirection == eDirNext);
470 }
471
472 child = text->GetChildAt(childIdx);
473
474 // HTML list items may need special processing because PeekOffset doesn't
475 // work with list bullets.
476 if (text->IsHTMLListItem()) {
477 HTMLLIAccessible* li = text->AsHTMLListItem();
478 if (child == li->Bullet()) {
479 // XXX: the logic is broken for multichar bullets in moving by
480 // char/cluster/word cases.
481 if (text != this) {
482 return aDirection == eDirPrevious ? TransformOffset(text, 0, false)
483 : TransformOffset(text, 1, true);
484 }
485 if (aDirection == eDirPrevious) return 0;
486
487 uint32_t nextOffset = GetChildOffset(1);
488 if (nextOffset == 0) return 0;
489
490 switch (aAmount) {
491 case eSelectLine:
492 case eSelectEndLine:
493 // Ask a text leaf next (if not empty) to the bullet for an offset
494 // since list item may be multiline.
495 return nextOffset < CharacterCount()
496 ? FindOffset(nextOffset, aDirection, aAmount,
497 aWordMovementType)
498 : nextOffset;
499
500 default:
501 return nextOffset;
502 }
503 }
504 }
505
506 innerOffset -= text->GetChildOffset(childIdx);
507
508 text = child->AsHyperText();
509 } while (text);
510
511 nsIFrame* childFrame = child->GetFrame();
512 if (!childFrame) {
513 NS_ERROR("No child frame");
514 return 0;
515 }
516
517 int32_t innerContentOffset = innerOffset;
518 if (child->IsTextLeaf()) {
519 NS_ASSERTION(childFrame->IsTextFrame(), "Wrong frame!");
520 RenderedToContentOffset(childFrame, innerOffset, &innerContentOffset);
521 }
522
523 nsIFrame* frameAtOffset = childFrame;
524 int32_t unusedOffsetInFrame = 0;
525 childFrame->GetChildFrameContainingOffset(
526 innerContentOffset, true, &unusedOffsetInFrame, &frameAtOffset);
527
528 const bool kIsJumpLinesOk = true; // okay to jump lines
529 const bool kIsScrollViewAStop = false; // do not stop at scroll views
530 const bool kIsKeyboardSelect = true; // is keyboard selection
531 const bool kIsVisualBidi = false; // use visual order for bidi text
532 nsPeekOffsetStruct pos(
533 aAmount, aDirection, innerContentOffset, nsPoint(0, 0), kIsJumpLinesOk,
534 kIsScrollViewAStop, kIsKeyboardSelect, kIsVisualBidi, false,
535 nsPeekOffsetStruct::ForceEditableRegion::No, aWordMovementType, false);
536 nsresult rv = frameAtOffset->PeekOffset(&pos);
537
538 // PeekOffset fails on last/first lines of the text in certain cases.
539 bool fallBackToSelectEndLine = false;
540 if (NS_FAILED(rv) && aAmount == eSelectLine) {
541 fallBackToSelectEndLine = aDirection == eDirNext;
542 pos.mAmount = fallBackToSelectEndLine ? eSelectEndLine : eSelectBeginLine;
543 frameAtOffset->PeekOffset(&pos);
544 }
545 if (!pos.mResultContent) {
546 NS_ERROR("No result content!");
547 return 0;
548 }
549
550 // Turn the resulting DOM point into an offset.
551 uint32_t hyperTextOffset = DOMPointToOffset(
552 pos.mResultContent, pos.mContentOffset, aDirection == eDirNext);
553
554 if (fallBackToSelectEndLine && IsLineEndCharAt(hyperTextOffset)) {
555 // We used eSelectEndLine, but the caller requested eSelectLine.
556 // If there's a '\n' at the end of the line, eSelectEndLine will stop
557 // on it rather than after it. This is not what we want, since the caller
558 // wants the next line, not the same line.
559 ++hyperTextOffset;
560 }
561
562 if (aDirection == eDirPrevious) {
563 // If we reached the end during search, this means we didn't find the DOM
564 // point and we're actually at the start of the paragraph
565 if (hyperTextOffset == CharacterCount()) return 0;
566
567 // PeekOffset stops right before bullet so return 0 to workaround it.
568 if (IsHTMLListItem() && aAmount == eSelectBeginLine &&
569 hyperTextOffset > 0) {
570 Accessible* prevOffsetChild = GetChildAtOffset(hyperTextOffset - 1);
571 if (prevOffsetChild == AsHTMLListItem()->Bullet()) return 0;
572 }
573 }
574
575 return hyperTextOffset;
576 }
577
FindLineBoundary(uint32_t aOffset,EWhichLineBoundary aWhichLineBoundary)578 uint32_t HyperTextAccessible::FindLineBoundary(
579 uint32_t aOffset, EWhichLineBoundary aWhichLineBoundary) {
580 // Note: empty last line doesn't have own frame (a previous line contains '\n'
581 // character instead) thus when it makes a difference we need to process this
582 // case separately (otherwise operations are performed on previous line).
583 switch (aWhichLineBoundary) {
584 case ePrevLineBegin: {
585 // Fetch a previous line and move to its start (as arrow up and home keys
586 // were pressed).
587 if (IsEmptyLastLineOffset(aOffset))
588 return FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
589
590 uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
591 return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
592 }
593
594 case ePrevLineEnd: {
595 if (IsEmptyLastLineOffset(aOffset)) return aOffset - 1;
596
597 // If offset is at first line then return 0 (first line start).
598 uint32_t tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
599 if (tmpOffset == 0) return 0;
600
601 // Otherwise move to end of previous line (as arrow up and end keys were
602 // pressed).
603 tmpOffset = FindOffset(aOffset, eDirPrevious, eSelectLine);
604 return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
605 }
606
607 case eThisLineBegin:
608 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
609
610 // Move to begin of the current line (as home key was pressed).
611 return FindOffset(aOffset, eDirPrevious, eSelectBeginLine);
612
613 case eThisLineEnd:
614 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
615
616 // Move to end of the current line (as end key was pressed).
617 return FindOffset(aOffset, eDirNext, eSelectEndLine);
618
619 case eNextLineBegin: {
620 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
621
622 // Move to begin of the next line if any (arrow down and home keys),
623 // otherwise end of the current line (arrow down only).
624 uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
625 if (tmpOffset == CharacterCount()) return tmpOffset;
626
627 return FindOffset(tmpOffset, eDirPrevious, eSelectBeginLine);
628 }
629
630 case eNextLineEnd: {
631 if (IsEmptyLastLineOffset(aOffset)) return aOffset;
632
633 // Move to next line end (as down arrow and end key were pressed).
634 uint32_t tmpOffset = FindOffset(aOffset, eDirNext, eSelectLine);
635 if (tmpOffset == CharacterCount()) return tmpOffset;
636
637 return FindOffset(tmpOffset, eDirNext, eSelectEndLine);
638 }
639 }
640
641 return 0;
642 }
643
TextBeforeOffset(int32_t aOffset,AccessibleTextBoundary aBoundaryType,int32_t * aStartOffset,int32_t * aEndOffset,nsAString & aText)644 void HyperTextAccessible::TextBeforeOffset(int32_t aOffset,
645 AccessibleTextBoundary aBoundaryType,
646 int32_t* aStartOffset,
647 int32_t* aEndOffset,
648 nsAString& aText) {
649 *aStartOffset = *aEndOffset = 0;
650 aText.Truncate();
651
652 index_t convertedOffset = ConvertMagicOffset(aOffset);
653 if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
654 NS_ERROR("Wrong in offset!");
655 return;
656 }
657
658 uint32_t adjustedOffset = convertedOffset;
659 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
660 adjustedOffset = AdjustCaretOffset(adjustedOffset);
661
662 switch (aBoundaryType) {
663 case nsIAccessibleText::BOUNDARY_CHAR:
664 if (convertedOffset != 0)
665 CharAt(convertedOffset - 1, aText, aStartOffset, aEndOffset);
666 break;
667
668 case nsIAccessibleText::BOUNDARY_WORD_START: {
669 // If the offset is a word start (except text length offset) then move
670 // backward to find a start offset (end offset is the given offset).
671 // Otherwise move backward twice to find both start and end offsets.
672 if (adjustedOffset == CharacterCount()) {
673 *aEndOffset =
674 FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
675 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
676 } else {
677 *aStartOffset =
678 FindWordBoundary(adjustedOffset, eDirPrevious, eStartWord);
679 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
680 if (*aEndOffset != static_cast<int32_t>(adjustedOffset)) {
681 *aEndOffset = *aStartOffset;
682 *aStartOffset =
683 FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
684 }
685 }
686 TextSubstring(*aStartOffset, *aEndOffset, aText);
687 break;
688 }
689
690 case nsIAccessibleText::BOUNDARY_WORD_END: {
691 // Move word backward twice to find start and end offsets.
692 *aEndOffset = FindWordBoundary(convertedOffset, eDirPrevious, eEndWord);
693 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
694 TextSubstring(*aStartOffset, *aEndOffset, aText);
695 break;
696 }
697
698 case nsIAccessibleText::BOUNDARY_LINE_START:
699 *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineBegin);
700 *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
701 TextSubstring(*aStartOffset, *aEndOffset, aText);
702 break;
703
704 case nsIAccessibleText::BOUNDARY_LINE_END: {
705 *aEndOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
706 int32_t tmpOffset = *aEndOffset;
707 // Adjust offset if line is wrapped.
708 if (*aEndOffset != 0 && !IsLineEndCharAt(*aEndOffset)) tmpOffset--;
709
710 *aStartOffset = FindLineBoundary(tmpOffset, ePrevLineEnd);
711 TextSubstring(*aStartOffset, *aEndOffset, aText);
712 break;
713 }
714 }
715 }
716
TextAtOffset(int32_t aOffset,AccessibleTextBoundary aBoundaryType,int32_t * aStartOffset,int32_t * aEndOffset,nsAString & aText)717 void HyperTextAccessible::TextAtOffset(int32_t aOffset,
718 AccessibleTextBoundary aBoundaryType,
719 int32_t* aStartOffset,
720 int32_t* aEndOffset, nsAString& aText) {
721 *aStartOffset = *aEndOffset = 0;
722 aText.Truncate();
723
724 uint32_t adjustedOffset = ConvertMagicOffset(aOffset);
725 if (adjustedOffset == std::numeric_limits<uint32_t>::max()) {
726 NS_ERROR("Wrong given offset!");
727 return;
728 }
729
730 switch (aBoundaryType) {
731 case nsIAccessibleText::BOUNDARY_CHAR:
732 // Return no char if caret is at the end of wrapped line (case of no line
733 // end character). Returning a next line char is confusing for AT.
734 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET &&
735 IsCaretAtEndOfLine())
736 *aStartOffset = *aEndOffset = adjustedOffset;
737 else
738 CharAt(adjustedOffset, aText, aStartOffset, aEndOffset);
739 break;
740
741 case nsIAccessibleText::BOUNDARY_WORD_START:
742 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
743 adjustedOffset = AdjustCaretOffset(adjustedOffset);
744
745 *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
746 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eStartWord);
747 TextSubstring(*aStartOffset, *aEndOffset, aText);
748 break;
749
750 case nsIAccessibleText::BOUNDARY_WORD_END:
751 // Ignore the spec and follow what WebKitGtk does because Orca expects it,
752 // i.e. return a next word at word end offset of the current word
753 // (WebKitGtk behavior) instead the current word (AKT spec).
754 *aEndOffset = FindWordBoundary(adjustedOffset, eDirNext, eEndWord);
755 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
756 TextSubstring(*aStartOffset, *aEndOffset, aText);
757 break;
758
759 case nsIAccessibleText::BOUNDARY_LINE_START:
760 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
761 adjustedOffset = AdjustCaretOffset(adjustedOffset);
762
763 *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineBegin);
764 *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
765 TextSubstring(*aStartOffset, *aEndOffset, aText);
766 break;
767
768 case nsIAccessibleText::BOUNDARY_LINE_END:
769 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
770 adjustedOffset = AdjustCaretOffset(adjustedOffset);
771
772 // In contrast to word end boundary we follow the spec here.
773 *aStartOffset = FindLineBoundary(adjustedOffset, ePrevLineEnd);
774 *aEndOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
775 TextSubstring(*aStartOffset, *aEndOffset, aText);
776 break;
777 }
778 }
779
TextAfterOffset(int32_t aOffset,AccessibleTextBoundary aBoundaryType,int32_t * aStartOffset,int32_t * aEndOffset,nsAString & aText)780 void HyperTextAccessible::TextAfterOffset(int32_t aOffset,
781 AccessibleTextBoundary aBoundaryType,
782 int32_t* aStartOffset,
783 int32_t* aEndOffset,
784 nsAString& aText) {
785 *aStartOffset = *aEndOffset = 0;
786 aText.Truncate();
787
788 index_t convertedOffset = ConvertMagicOffset(aOffset);
789 if (!convertedOffset.IsValid() || convertedOffset > CharacterCount()) {
790 NS_ERROR("Wrong in offset!");
791 return;
792 }
793
794 uint32_t adjustedOffset = convertedOffset;
795 if (aOffset == nsIAccessibleText::TEXT_OFFSET_CARET)
796 adjustedOffset = AdjustCaretOffset(adjustedOffset);
797
798 switch (aBoundaryType) {
799 case nsIAccessibleText::BOUNDARY_CHAR:
800 // If caret is at the end of wrapped line (case of no line end character)
801 // then char after the offset is a first char at next line.
802 if (adjustedOffset >= CharacterCount())
803 *aStartOffset = *aEndOffset = CharacterCount();
804 else
805 CharAt(adjustedOffset + 1, aText, aStartOffset, aEndOffset);
806 break;
807
808 case nsIAccessibleText::BOUNDARY_WORD_START:
809 // Move word forward twice to find start and end offsets.
810 *aStartOffset = FindWordBoundary(adjustedOffset, eDirNext, eStartWord);
811 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eStartWord);
812 TextSubstring(*aStartOffset, *aEndOffset, aText);
813 break;
814
815 case nsIAccessibleText::BOUNDARY_WORD_END:
816 // If the offset is a word end (except 0 offset) then move forward to find
817 // end offset (start offset is the given offset). Otherwise move forward
818 // twice to find both start and end offsets.
819 if (convertedOffset == 0) {
820 *aStartOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
821 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
822 } else {
823 *aEndOffset = FindWordBoundary(convertedOffset, eDirNext, eEndWord);
824 *aStartOffset = FindWordBoundary(*aEndOffset, eDirPrevious, eEndWord);
825 if (*aStartOffset != static_cast<int32_t>(convertedOffset)) {
826 *aStartOffset = *aEndOffset;
827 *aEndOffset = FindWordBoundary(*aStartOffset, eDirNext, eEndWord);
828 }
829 }
830 TextSubstring(*aStartOffset, *aEndOffset, aText);
831 break;
832
833 case nsIAccessibleText::BOUNDARY_LINE_START:
834 *aStartOffset = FindLineBoundary(adjustedOffset, eNextLineBegin);
835 *aEndOffset = FindLineBoundary(*aStartOffset, eNextLineBegin);
836 TextSubstring(*aStartOffset, *aEndOffset, aText);
837 break;
838
839 case nsIAccessibleText::BOUNDARY_LINE_END:
840 *aStartOffset = FindLineBoundary(adjustedOffset, eThisLineEnd);
841 *aEndOffset = FindLineBoundary(adjustedOffset, eNextLineEnd);
842 TextSubstring(*aStartOffset, *aEndOffset, aText);
843 break;
844 }
845 }
846
TextAttributes(bool aIncludeDefAttrs,int32_t aOffset,int32_t * aStartOffset,int32_t * aEndOffset)847 already_AddRefed<nsIPersistentProperties> HyperTextAccessible::TextAttributes(
848 bool aIncludeDefAttrs, int32_t aOffset, int32_t* aStartOffset,
849 int32_t* aEndOffset) {
850 // 1. Get each attribute and its ranges one after another.
851 // 2. As we get each new attribute, we pass the current start and end offsets
852 // as in/out parameters. In other words, as attributes are collected,
853 // the attribute range itself can only stay the same or get smaller.
854
855 *aStartOffset = *aEndOffset = 0;
856 index_t offset = ConvertMagicOffset(aOffset);
857 if (!offset.IsValid() || offset > CharacterCount()) {
858 NS_ERROR("Wrong in offset!");
859 return nullptr;
860 }
861
862 RefPtr<nsPersistentProperties> attributes = new nsPersistentProperties();
863
864 Accessible* accAtOffset = GetChildAtOffset(offset);
865 if (!accAtOffset) {
866 // Offset 0 is correct offset when accessible has empty text. Include
867 // default attributes if they were requested, otherwise return empty set.
868 if (offset == 0) {
869 if (aIncludeDefAttrs) {
870 TextAttrsMgr textAttrsMgr(this);
871 textAttrsMgr.GetAttributes(attributes);
872 }
873 return attributes.forget();
874 }
875 return nullptr;
876 }
877
878 int32_t accAtOffsetIdx = accAtOffset->IndexInParent();
879 uint32_t startOffset = GetChildOffset(accAtOffsetIdx);
880 uint32_t endOffset = GetChildOffset(accAtOffsetIdx + 1);
881 int32_t offsetInAcc = offset - startOffset;
882
883 TextAttrsMgr textAttrsMgr(this, aIncludeDefAttrs, accAtOffset,
884 accAtOffsetIdx);
885 textAttrsMgr.GetAttributes(attributes, &startOffset, &endOffset);
886
887 // Compute spelling attributes on text accessible only.
888 nsIFrame* offsetFrame = accAtOffset->GetFrame();
889 if (offsetFrame && offsetFrame->IsTextFrame()) {
890 int32_t nodeOffset = 0;
891 RenderedToContentOffset(offsetFrame, offsetInAcc, &nodeOffset);
892
893 // Set 'misspelled' text attribute.
894 GetSpellTextAttr(accAtOffset->GetNode(), nodeOffset, &startOffset,
895 &endOffset, attributes);
896 }
897
898 *aStartOffset = startOffset;
899 *aEndOffset = endOffset;
900 return attributes.forget();
901 }
902
903 already_AddRefed<nsIPersistentProperties>
DefaultTextAttributes()904 HyperTextAccessible::DefaultTextAttributes() {
905 RefPtr<nsPersistentProperties> attributes = new nsPersistentProperties();
906
907 TextAttrsMgr textAttrsMgr(this);
908 textAttrsMgr.GetAttributes(attributes);
909 return attributes.forget();
910 }
911
GetLevelInternal()912 int32_t HyperTextAccessible::GetLevelInternal() {
913 if (auto* heading = dom::HTMLHeadingElement::FromNode(mContent)) {
914 return heading->AccessibilityLevel();
915 }
916 return AccessibleWrap::GetLevelInternal();
917 }
918
SetMathMLXMLRoles(nsIPersistentProperties * aAttributes)919 void HyperTextAccessible::SetMathMLXMLRoles(
920 nsIPersistentProperties* aAttributes) {
921 // Add MathML xmlroles based on the position inside the parent.
922 Accessible* parent = Parent();
923 if (parent) {
924 switch (parent->Role()) {
925 case roles::MATHML_CELL:
926 case roles::MATHML_ENCLOSED:
927 case roles::MATHML_ERROR:
928 case roles::MATHML_MATH:
929 case roles::MATHML_ROW:
930 case roles::MATHML_SQUARE_ROOT:
931 case roles::MATHML_STYLE:
932 if (Role() == roles::MATHML_OPERATOR) {
933 // This is an operator inside an <mrow> (or an inferred <mrow>).
934 // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow
935 // XXX We should probably do something similar for MATHML_FENCED, but
936 // operators do not appear in the accessible tree. See bug 1175747.
937 nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame());
938 if (mathMLFrame) {
939 nsEmbellishData embellishData;
940 mathMLFrame->GetEmbellishData(embellishData);
941 if (NS_MATHML_EMBELLISH_IS_FENCE(embellishData.flags)) {
942 if (!PrevSibling()) {
943 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
944 nsGkAtoms::open_fence);
945 } else if (!NextSibling()) {
946 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
947 nsGkAtoms::close_fence);
948 }
949 }
950 if (NS_MATHML_EMBELLISH_IS_SEPARATOR(embellishData.flags)) {
951 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
952 nsGkAtoms::separator_);
953 }
954 }
955 }
956 break;
957 case roles::MATHML_FRACTION:
958 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
959 IndexInParent() == 0 ? nsGkAtoms::numerator
960 : nsGkAtoms::denominator);
961 break;
962 case roles::MATHML_ROOT:
963 nsAccUtils::SetAccAttr(
964 aAttributes, nsGkAtoms::xmlroles,
965 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index);
966 break;
967 case roles::MATHML_SUB:
968 nsAccUtils::SetAccAttr(
969 aAttributes, nsGkAtoms::xmlroles,
970 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript);
971 break;
972 case roles::MATHML_SUP:
973 nsAccUtils::SetAccAttr(
974 aAttributes, nsGkAtoms::xmlroles,
975 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript);
976 break;
977 case roles::MATHML_SUB_SUP: {
978 int32_t index = IndexInParent();
979 nsAccUtils::SetAccAttr(
980 aAttributes, nsGkAtoms::xmlroles,
981 index == 0
982 ? nsGkAtoms::base
983 : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript));
984 } break;
985 case roles::MATHML_UNDER:
986 nsAccUtils::SetAccAttr(
987 aAttributes, nsGkAtoms::xmlroles,
988 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript);
989 break;
990 case roles::MATHML_OVER:
991 nsAccUtils::SetAccAttr(
992 aAttributes, nsGkAtoms::xmlroles,
993 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript);
994 break;
995 case roles::MATHML_UNDER_OVER: {
996 int32_t index = IndexInParent();
997 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
998 index == 0
999 ? nsGkAtoms::base
1000 : (index == 1 ? nsGkAtoms::underscript
1001 : nsGkAtoms::overscript));
1002 } break;
1003 case roles::MATHML_MULTISCRIPTS: {
1004 // Get the <multiscripts> base.
1005 nsIContent* child;
1006 bool baseFound = false;
1007 for (child = parent->GetContent()->GetFirstChild(); child;
1008 child = child->GetNextSibling()) {
1009 if (child->IsMathMLElement()) {
1010 baseFound = true;
1011 break;
1012 }
1013 }
1014 if (baseFound) {
1015 nsIContent* content = GetContent();
1016 if (child == content) {
1017 // We are the base.
1018 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1019 nsGkAtoms::base);
1020 } else {
1021 // Browse the list of scripts to find us and determine our type.
1022 bool postscript = true;
1023 bool subscript = true;
1024 for (child = child->GetNextSibling(); child;
1025 child = child->GetNextSibling()) {
1026 if (!child->IsMathMLElement()) continue;
1027 if (child->IsMathMLElement(nsGkAtoms::mprescripts_)) {
1028 postscript = false;
1029 subscript = true;
1030 continue;
1031 }
1032 if (child == content) {
1033 if (postscript) {
1034 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1035 subscript ? nsGkAtoms::subscript
1036 : nsGkAtoms::superscript);
1037 } else {
1038 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::xmlroles,
1039 subscript ? nsGkAtoms::presubscript
1040 : nsGkAtoms::presuperscript);
1041 }
1042 break;
1043 }
1044 subscript = !subscript;
1045 }
1046 }
1047 }
1048 } break;
1049 default:
1050 break;
1051 }
1052 }
1053 }
1054
1055 already_AddRefed<nsIPersistentProperties>
NativeAttributes()1056 HyperTextAccessible::NativeAttributes() {
1057 nsCOMPtr<nsIPersistentProperties> attributes =
1058 AccessibleWrap::NativeAttributes();
1059
1060 // 'formatting' attribute is deprecated, 'display' attribute should be
1061 // instead.
1062 nsIFrame* frame = GetFrame();
1063 if (frame && frame->IsBlockFrame()) {
1064 nsAutoString unused;
1065 attributes->SetStringProperty(NS_LITERAL_CSTRING("formatting"),
1066 NS_LITERAL_STRING("block"), unused);
1067 }
1068
1069 if (FocusMgr()->IsFocused(this)) {
1070 int32_t lineNumber = CaretLineNumber();
1071 if (lineNumber >= 1) {
1072 nsAutoString strLineNumber;
1073 strLineNumber.AppendInt(lineNumber);
1074 nsAccUtils::SetAccAttr(attributes, nsGkAtoms::lineNumber, strLineNumber);
1075 }
1076 }
1077
1078 if (HasOwnContent()) {
1079 GetAccService()->MarkupAttributes(mContent, attributes);
1080 if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes);
1081 }
1082
1083 return attributes.forget();
1084 }
1085
LandmarkRole() const1086 nsAtom* HyperTextAccessible::LandmarkRole() const {
1087 if (!HasOwnContent()) return nullptr;
1088
1089 // For the html landmark elements we expose them like we do ARIA landmarks to
1090 // make AT navigation schemes "just work".
1091 if (mContent->IsHTMLElement(nsGkAtoms::nav)) {
1092 return nsGkAtoms::navigation;
1093 }
1094
1095 if (mContent->IsHTMLElement(nsGkAtoms::aside)) {
1096 return nsGkAtoms::complementary;
1097 }
1098
1099 if (mContent->IsHTMLElement(nsGkAtoms::main)) {
1100 return nsGkAtoms::main;
1101 }
1102
1103 return AccessibleWrap::LandmarkRole();
1104 }
1105
OffsetAtPoint(int32_t aX,int32_t aY,uint32_t aCoordType)1106 int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY,
1107 uint32_t aCoordType) {
1108 nsIFrame* hyperFrame = GetFrame();
1109 if (!hyperFrame) return -1;
1110
1111 nsIntPoint coords =
1112 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this);
1113
1114 nsPresContext* presContext = mDoc->PresContext();
1115 nsPoint coordsInAppUnits =
1116 ToAppUnits(coords, presContext->AppUnitsPerDevPixel());
1117
1118 nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits();
1119 if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y))
1120 return -1; // Not found
1121
1122 nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(),
1123 coordsInAppUnits.y - frameScreenRect.Y());
1124
1125 // Go through the frames to check if each one has the point.
1126 // When one does, add up the character offsets until we have a match
1127
1128 // We have an point in an accessible child of this, now we need to add up the
1129 // offsets before it to what we already have
1130 int32_t offset = 0;
1131 uint32_t childCount = ChildCount();
1132 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) {
1133 Accessible* childAcc = mChildren[childIdx];
1134
1135 nsIFrame* primaryFrame = childAcc->GetFrame();
1136 NS_ENSURE_TRUE(primaryFrame, -1);
1137
1138 nsIFrame* frame = primaryFrame;
1139 while (frame) {
1140 nsIContent* content = frame->GetContent();
1141 NS_ENSURE_TRUE(content, -1);
1142 nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame);
1143 nsSize frameSize = frame->GetSize();
1144 if (pointInFrame.x < frameSize.width &&
1145 pointInFrame.y < frameSize.height) {
1146 // Finished
1147 if (frame->IsTextFrame()) {
1148 nsIFrame::ContentOffsets contentOffsets =
1149 frame->GetContentOffsetsFromPointExternal(
1150 pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE);
1151 if (contentOffsets.IsNull() || contentOffsets.content != content) {
1152 return -1; // Not found
1153 }
1154 uint32_t addToOffset;
1155 nsresult rv = ContentToRenderedOffset(
1156 primaryFrame, contentOffsets.offset, &addToOffset);
1157 NS_ENSURE_SUCCESS(rv, -1);
1158 offset += addToOffset;
1159 }
1160 return offset;
1161 }
1162 frame = frame->GetNextContinuation();
1163 }
1164
1165 offset += nsAccUtils::TextLength(childAcc);
1166 }
1167
1168 return -1; // Not found
1169 }
1170
TextBounds(int32_t aStartOffset,int32_t aEndOffset,uint32_t aCoordType)1171 nsIntRect HyperTextAccessible::TextBounds(int32_t aStartOffset,
1172 int32_t aEndOffset,
1173 uint32_t aCoordType) {
1174 index_t startOffset = ConvertMagicOffset(aStartOffset);
1175 index_t endOffset = ConvertMagicOffset(aEndOffset);
1176 if (!startOffset.IsValid() || !endOffset.IsValid() ||
1177 startOffset > endOffset || endOffset > CharacterCount()) {
1178 NS_ERROR("Wrong in offset");
1179 return nsIntRect();
1180 }
1181
1182 if (CharacterCount() == 0) {
1183 nsPresContext* presContext = mDoc->PresContext();
1184 // Empty content, use our own bound to at least get x,y coordinates
1185 nsIFrame* frame = GetFrame();
1186 if (!frame) {
1187 return nsIntRect();
1188 }
1189 return frame->GetScreenRectInAppUnits().ToNearestPixels(
1190 presContext->AppUnitsPerDevPixel());
1191 }
1192
1193 int32_t childIdx = GetChildIndexAtOffset(startOffset);
1194 if (childIdx == -1) return nsIntRect();
1195
1196 nsIntRect bounds;
1197 int32_t prevOffset = GetChildOffset(childIdx);
1198 int32_t offset1 = startOffset - prevOffset;
1199
1200 while (childIdx < static_cast<int32_t>(ChildCount())) {
1201 nsIFrame* frame = GetChildAt(childIdx++)->GetFrame();
1202 if (!frame) {
1203 MOZ_ASSERT_UNREACHABLE("No frame for a child!");
1204 continue;
1205 }
1206
1207 int32_t nextOffset = GetChildOffset(childIdx);
1208 if (nextOffset >= static_cast<int32_t>(endOffset)) {
1209 bounds.UnionRect(
1210 bounds, GetBoundsInFrame(frame, offset1, endOffset - prevOffset));
1211 break;
1212 }
1213
1214 bounds.UnionRect(bounds,
1215 GetBoundsInFrame(frame, offset1, nextOffset - prevOffset));
1216
1217 prevOffset = nextOffset;
1218 offset1 = 0;
1219 }
1220
1221 // This document may have a resolution set, we will need to multiply
1222 // the document-relative coordinates by that value and re-apply the doc's
1223 // screen coordinates.
1224 nsPresContext* presContext = mDoc->PresContext();
1225 nsIFrame* rootFrame = presContext->PresShell()->GetRootFrame();
1226 nsIntRect orgRectPixels =
1227 rootFrame->GetScreenRectInAppUnits().ToNearestPixels(
1228 presContext->AppUnitsPerDevPixel());
1229 bounds.MoveBy(-orgRectPixels.X(), -orgRectPixels.Y());
1230 bounds.ScaleRoundOut(presContext->PresShell()->GetResolution());
1231 bounds.MoveBy(orgRectPixels.X(), orgRectPixels.Y());
1232
1233 auto boundsX = bounds.X();
1234 auto boundsY = bounds.Y();
1235 nsAccUtils::ConvertScreenCoordsTo(&boundsX, &boundsY, aCoordType, this);
1236 bounds.MoveTo(boundsX, boundsY);
1237 return bounds;
1238 }
1239
GetEditor() const1240 already_AddRefed<TextEditor> HyperTextAccessible::GetEditor() const {
1241 if (!mContent->HasFlag(NODE_IS_EDITABLE)) {
1242 // If we're inside an editable container, then return that container's
1243 // editor
1244 Accessible* ancestor = Parent();
1245 while (ancestor) {
1246 HyperTextAccessible* hyperText = ancestor->AsHyperText();
1247 if (hyperText) {
1248 // Recursion will stop at container doc because it has its own impl
1249 // of GetEditor()
1250 return hyperText->GetEditor();
1251 }
1252
1253 ancestor = ancestor->Parent();
1254 }
1255
1256 return nullptr;
1257 }
1258
1259 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent);
1260 nsCOMPtr<nsIEditingSession> editingSession;
1261 docShell->GetEditingSession(getter_AddRefs(editingSession));
1262 if (!editingSession) return nullptr; // No editing session interface
1263
1264 dom::Document* docNode = mDoc->DocumentNode();
1265 RefPtr<HTMLEditor> htmlEditor =
1266 editingSession->GetHTMLEditorForWindow(docNode->GetWindow());
1267 return htmlEditor.forget();
1268 }
1269
1270 /**
1271 * =================== Caret & Selection ======================
1272 */
1273
SetSelectionRange(int32_t aStartPos,int32_t aEndPos)1274 nsresult HyperTextAccessible::SetSelectionRange(int32_t aStartPos,
1275 int32_t aEndPos) {
1276 // Before setting the selection range, we need to ensure that the editor
1277 // is initialized. (See bug 804927.)
1278 // Otherwise, it's possible that lazy editor initialization will override
1279 // the selection we set here and leave the caret at the end of the text.
1280 // By calling GetEditor here, we ensure that editor initialization is
1281 // completed before we set the selection.
1282 RefPtr<TextEditor> textEditor = GetEditor();
1283
1284 bool isFocusable = InteractiveState() & states::FOCUSABLE;
1285
1286 // If accessible is focusable then focus it before setting the selection to
1287 // neglect control's selection changes on focus if any (for example, inputs
1288 // that do select all on focus).
1289 // some input controls
1290 if (isFocusable) TakeFocus();
1291
1292 RefPtr<dom::Selection> domSel = DOMSelection();
1293 NS_ENSURE_STATE(domSel);
1294
1295 // Set up the selection.
1296 for (int32_t idx = domSel->RangeCount() - 1; idx > 0; idx--) {
1297 RefPtr<nsRange> range{domSel->GetRangeAt(idx)};
1298 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
1299 IgnoreErrors());
1300 }
1301 SetSelectionBoundsAt(0, aStartPos, aEndPos);
1302
1303 // Make sure it is visible
1304 domSel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
1305 ScrollAxis(), ScrollAxis(),
1306 dom::Selection::SCROLL_FOR_CARET_MOVE |
1307 dom::Selection::SCROLL_OVERFLOW_HIDDEN);
1308
1309 // When selection is done, move the focus to the selection if accessible is
1310 // not focusable. That happens when selection is set within hypertext
1311 // accessible.
1312 if (isFocusable) return NS_OK;
1313
1314 nsFocusManager* DOMFocusManager = nsFocusManager::GetFocusManager();
1315 if (DOMFocusManager) {
1316 NS_ENSURE_TRUE(mDoc, NS_ERROR_FAILURE);
1317 dom::Document* docNode = mDoc->DocumentNode();
1318 NS_ENSURE_TRUE(docNode, NS_ERROR_FAILURE);
1319 nsCOMPtr<nsPIDOMWindowOuter> window = docNode->GetWindow();
1320 RefPtr<dom::Element> result;
1321 DOMFocusManager->MoveFocus(
1322 window, nullptr, nsIFocusManager::MOVEFOCUS_CARET,
1323 nsIFocusManager::FLAG_BYMOVEFOCUS, getter_AddRefs(result));
1324 }
1325
1326 return NS_OK;
1327 }
1328
CaretOffset() const1329 int32_t HyperTextAccessible::CaretOffset() const {
1330 // Not focused focusable accessible except document accessible doesn't have
1331 // a caret.
1332 if (!IsDoc() && !FocusMgr()->IsFocused(this) &&
1333 (InteractiveState() & states::FOCUSABLE)) {
1334 return -1;
1335 }
1336
1337 // Check cached value.
1338 int32_t caretOffset = -1;
1339 HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset);
1340
1341 // Use cached value if it corresponds to this accessible.
1342 if (caretOffset != -1) {
1343 if (text == this) return caretOffset;
1344
1345 nsINode* textNode = text->GetNode();
1346 // Ignore offset if cached accessible isn't a text leaf.
1347 if (nsCoreUtils::IsAncestorOf(GetNode(), textNode))
1348 return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false);
1349 }
1350
1351 // No caret if the focused node is not inside this DOM node and this DOM node
1352 // is not inside of focused node.
1353 FocusManager::FocusDisposition focusDisp =
1354 FocusMgr()->IsInOrContainsFocus(this);
1355 if (focusDisp == FocusManager::eNone) return -1;
1356
1357 // Turn the focus node and offset of the selection into caret hypretext
1358 // offset.
1359 dom::Selection* domSel = DOMSelection();
1360 NS_ENSURE_TRUE(domSel, -1);
1361
1362 nsINode* focusNode = domSel->GetFocusNode();
1363 uint32_t focusOffset = domSel->FocusOffset();
1364
1365 // No caret if this DOM node is inside of focused node but the selection's
1366 // focus point is not inside of this DOM node.
1367 if (focusDisp == FocusManager::eContainedByFocus) {
1368 nsINode* resultNode =
1369 nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset);
1370
1371 nsINode* thisNode = GetNode();
1372 if (resultNode != thisNode &&
1373 !nsCoreUtils::IsAncestorOf(thisNode, resultNode))
1374 return -1;
1375 }
1376
1377 return DOMPointToOffset(focusNode, focusOffset);
1378 }
1379
CaretLineNumber()1380 int32_t HyperTextAccessible::CaretLineNumber() {
1381 // Provide the line number for the caret, relative to the
1382 // currently focused node. Use a 1-based index
1383 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
1384 if (!frameSelection) return -1;
1385
1386 dom::Selection* domSel = frameSelection->GetSelection(SelectionType::eNormal);
1387 if (!domSel) return -1;
1388
1389 nsINode* caretNode = domSel->GetFocusNode();
1390 if (!caretNode || !caretNode->IsContent()) return -1;
1391
1392 nsIContent* caretContent = caretNode->AsContent();
1393 if (!nsCoreUtils::IsAncestorOf(GetNode(), caretContent)) return -1;
1394
1395 int32_t returnOffsetUnused;
1396 uint32_t caretOffset = domSel->FocusOffset();
1397 CaretAssociationHint hint = frameSelection->GetHint();
1398 nsIFrame* caretFrame = frameSelection->GetFrameForNodeOffset(
1399 caretContent, caretOffset, hint, &returnOffsetUnused);
1400 NS_ENSURE_TRUE(caretFrame, -1);
1401
1402 int32_t lineNumber = 1;
1403 nsAutoLineIterator lineIterForCaret;
1404 nsIContent* hyperTextContent = IsContent() ? mContent.get() : nullptr;
1405 while (caretFrame) {
1406 if (hyperTextContent == caretFrame->GetContent()) {
1407 return lineNumber; // Must be in a single line hyper text, there is no
1408 // line iterator
1409 }
1410 nsContainerFrame* parentFrame = caretFrame->GetParent();
1411 if (!parentFrame) break;
1412
1413 // Add lines for the sibling frames before the caret
1414 nsIFrame* sibling = parentFrame->PrincipalChildList().FirstChild();
1415 while (sibling && sibling != caretFrame) {
1416 nsAutoLineIterator lineIterForSibling = sibling->GetLineIterator();
1417 if (lineIterForSibling) {
1418 // For the frames before that grab all the lines
1419 int32_t addLines = lineIterForSibling->GetNumLines();
1420 lineNumber += addLines;
1421 }
1422 sibling = sibling->GetNextSibling();
1423 }
1424
1425 // Get the line number relative to the container with lines
1426 if (!lineIterForCaret) { // Add the caret line just once
1427 lineIterForCaret = parentFrame->GetLineIterator();
1428 if (lineIterForCaret) {
1429 // Ancestor of caret
1430 int32_t addLines = lineIterForCaret->FindLineContaining(caretFrame);
1431 lineNumber += addLines;
1432 }
1433 }
1434
1435 caretFrame = parentFrame;
1436 }
1437
1438 MOZ_ASSERT_UNREACHABLE(
1439 "DOM ancestry had this hypertext but frame ancestry didn't");
1440 return lineNumber;
1441 }
1442
GetCaretRect(nsIWidget ** aWidget)1443 LayoutDeviceIntRect HyperTextAccessible::GetCaretRect(nsIWidget** aWidget) {
1444 *aWidget = nullptr;
1445
1446 RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret();
1447 NS_ENSURE_TRUE(caret, LayoutDeviceIntRect());
1448
1449 bool isVisible = caret->IsVisible();
1450 if (!isVisible) return LayoutDeviceIntRect();
1451
1452 nsRect rect;
1453 nsIFrame* frame = caret->GetGeometry(&rect);
1454 if (!frame || rect.IsEmpty()) return LayoutDeviceIntRect();
1455
1456 nsPoint offset;
1457 // Offset from widget origin to the frame origin, which includes chrome
1458 // on the widget.
1459 *aWidget = frame->GetNearestWidget(offset);
1460 NS_ENSURE_TRUE(*aWidget, LayoutDeviceIntRect());
1461 rect.MoveBy(offset);
1462
1463 LayoutDeviceIntRect caretRect = LayoutDeviceIntRect::FromUnknownRect(
1464 rect.ToOutsidePixels(frame->PresContext()->AppUnitsPerDevPixel()));
1465 // clang-format off
1466 // ((content screen origin) - (content offset in the widget)) = widget origin on the screen
1467 // clang-format on
1468 caretRect.MoveBy((*aWidget)->WidgetToScreenOffset() -
1469 (*aWidget)->GetClientOffset());
1470
1471 // Correct for character size, so that caret always matches the size of
1472 // the character. This is important for font size transitions, and is
1473 // necessary because the Gecko caret uses the previous character's size as
1474 // the user moves forward in the text by character.
1475 nsIntRect charRect = CharBounds(
1476 CaretOffset(), nsIAccessibleCoordinateType::COORDTYPE_SCREEN_RELATIVE);
1477 if (!charRect.IsEmpty()) {
1478 caretRect.SetTopEdge(charRect.Y());
1479 }
1480 return caretRect;
1481 }
1482
GetSelectionDOMRanges(SelectionType aSelectionType,nsTArray<nsRange * > * aRanges)1483 void HyperTextAccessible::GetSelectionDOMRanges(SelectionType aSelectionType,
1484 nsTArray<nsRange*>* aRanges) {
1485 // Ignore selection if it is not visible.
1486 RefPtr<nsFrameSelection> frameSelection = FrameSelection();
1487 if (!frameSelection || frameSelection->GetDisplaySelection() <=
1488 nsISelectionController::SELECTION_HIDDEN)
1489 return;
1490
1491 dom::Selection* domSel = frameSelection->GetSelection(aSelectionType);
1492 if (!domSel) return;
1493
1494 nsINode* startNode = GetNode();
1495
1496 RefPtr<TextEditor> textEditor = GetEditor();
1497 if (textEditor) {
1498 startNode = textEditor->GetRoot();
1499 }
1500
1501 if (!startNode) return;
1502
1503 uint32_t childCount = startNode->GetChildCount();
1504 nsresult rv = domSel->GetRangesForIntervalArray(startNode, 0, startNode,
1505 childCount, true, aRanges);
1506 NS_ENSURE_SUCCESS_VOID(rv);
1507
1508 // Remove collapsed ranges
1509 uint32_t numRanges = aRanges->Length();
1510 for (uint32_t idx = 0; idx < numRanges; idx++) {
1511 if ((*aRanges)[idx]->Collapsed()) {
1512 aRanges->RemoveElementAt(idx);
1513 --numRanges;
1514 --idx;
1515 }
1516 }
1517 }
1518
SelectionCount()1519 int32_t HyperTextAccessible::SelectionCount() {
1520 nsTArray<nsRange*> ranges;
1521 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
1522 return ranges.Length();
1523 }
1524
SelectionBoundsAt(int32_t aSelectionNum,int32_t * aStartOffset,int32_t * aEndOffset)1525 bool HyperTextAccessible::SelectionBoundsAt(int32_t aSelectionNum,
1526 int32_t* aStartOffset,
1527 int32_t* aEndOffset) {
1528 *aStartOffset = *aEndOffset = 0;
1529
1530 nsTArray<nsRange*> ranges;
1531 GetSelectionDOMRanges(SelectionType::eNormal, &ranges);
1532
1533 uint32_t rangeCount = ranges.Length();
1534 if (aSelectionNum < 0 || aSelectionNum >= static_cast<int32_t>(rangeCount))
1535 return false;
1536
1537 nsRange* range = ranges[aSelectionNum];
1538
1539 // Get start and end points.
1540 nsINode* startNode = range->GetStartContainer();
1541 nsINode* endNode = range->GetEndContainer();
1542 int32_t startOffset = range->StartOffset(), endOffset = range->EndOffset();
1543
1544 // Make sure start is before end, by swapping DOM points. This occurs when
1545 // the user selects backwards in the text.
1546 const Maybe<int32_t> order =
1547 nsContentUtils::ComparePoints(endNode, endOffset, startNode, startOffset);
1548
1549 if (!order) {
1550 MOZ_ASSERT_UNREACHABLE();
1551 return false;
1552 }
1553
1554 if (*order < 0) {
1555 nsINode* tempNode = startNode;
1556 startNode = endNode;
1557 endNode = tempNode;
1558 int32_t tempOffset = startOffset;
1559 startOffset = endOffset;
1560 endOffset = tempOffset;
1561 }
1562
1563 if (!startNode->IsInclusiveDescendantOf(mContent))
1564 *aStartOffset = 0;
1565 else
1566 *aStartOffset = DOMPointToOffset(startNode, startOffset);
1567
1568 if (!endNode->IsInclusiveDescendantOf(mContent))
1569 *aEndOffset = CharacterCount();
1570 else
1571 *aEndOffset = DOMPointToOffset(endNode, endOffset, true);
1572 return true;
1573 }
1574
SetSelectionBoundsAt(int32_t aSelectionNum,int32_t aStartOffset,int32_t aEndOffset)1575 bool HyperTextAccessible::SetSelectionBoundsAt(int32_t aSelectionNum,
1576 int32_t aStartOffset,
1577 int32_t aEndOffset) {
1578 index_t startOffset = ConvertMagicOffset(aStartOffset);
1579 index_t endOffset = ConvertMagicOffset(aEndOffset);
1580 if (!startOffset.IsValid() || !endOffset.IsValid() ||
1581 std::max(startOffset, endOffset) > CharacterCount()) {
1582 NS_ERROR("Wrong in offset");
1583 return false;
1584 }
1585
1586 RefPtr<dom::Selection> domSel = DOMSelection();
1587 if (!domSel) return false;
1588
1589 RefPtr<nsRange> range;
1590 uint32_t rangeCount = domSel->RangeCount();
1591 if (aSelectionNum == static_cast<int32_t>(rangeCount)) {
1592 range = nsRange::Create(mContent);
1593 } else {
1594 range = domSel->GetRangeAt(aSelectionNum);
1595 }
1596
1597 if (!range) return false;
1598
1599 if (!OffsetsToDOMRange(std::min(startOffset, endOffset),
1600 std::max(startOffset, endOffset), range))
1601 return false;
1602
1603 // If this is not a new range, notify selection listeners that the existing
1604 // selection range has changed. Otherwise, just add the new range.
1605 if (aSelectionNum != static_cast<int32_t>(rangeCount)) {
1606 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
1607 IgnoreErrors());
1608 }
1609
1610 IgnoredErrorResult err;
1611 domSel->AddRangeAndSelectFramesAndNotifyListeners(*range, err);
1612
1613 if (!err.Failed()) {
1614 // Changing the direction of the selection assures that the caret
1615 // will be at the logical end of the selection.
1616 domSel->SetDirection(startOffset < endOffset ? eDirNext : eDirPrevious);
1617 return true;
1618 }
1619
1620 return false;
1621 }
1622
RemoveFromSelection(int32_t aSelectionNum)1623 bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) {
1624 RefPtr<dom::Selection> domSel = DOMSelection();
1625 if (!domSel) return false;
1626
1627 if (aSelectionNum < 0 ||
1628 aSelectionNum >= static_cast<int32_t>(domSel->RangeCount()))
1629 return false;
1630
1631 const RefPtr<nsRange> range{domSel->GetRangeAt(aSelectionNum)};
1632 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range,
1633 IgnoreErrors());
1634 return true;
1635 }
1636
ScrollSubstringTo(int32_t aStartOffset,int32_t aEndOffset,uint32_t aScrollType)1637 void HyperTextAccessible::ScrollSubstringTo(int32_t aStartOffset,
1638 int32_t aEndOffset,
1639 uint32_t aScrollType) {
1640 RefPtr<nsRange> range = nsRange::Create(mContent);
1641 if (OffsetsToDOMRange(aStartOffset, aEndOffset, range))
1642 nsCoreUtils::ScrollSubstringTo(GetFrame(), range, aScrollType);
1643 }
1644
ScrollSubstringToPoint(int32_t aStartOffset,int32_t aEndOffset,uint32_t aCoordinateType,int32_t aX,int32_t aY)1645 void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset,
1646 int32_t aEndOffset,
1647 uint32_t aCoordinateType,
1648 int32_t aX, int32_t aY) {
1649 nsIFrame* frame = GetFrame();
1650 if (!frame) return;
1651
1652 nsIntPoint coords =
1653 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this);
1654
1655 RefPtr<nsRange> range = nsRange::Create(mContent);
1656 if (!OffsetsToDOMRange(aStartOffset, aEndOffset, range)) return;
1657
1658 nsPresContext* presContext = frame->PresContext();
1659 nsPoint coordsInAppUnits =
1660 ToAppUnits(coords, presContext->AppUnitsPerDevPixel());
1661
1662 bool initialScrolled = false;
1663 nsIFrame* parentFrame = frame;
1664 while ((parentFrame = parentFrame->GetParent())) {
1665 nsIScrollableFrame* scrollableFrame = do_QueryFrame(parentFrame);
1666 if (scrollableFrame) {
1667 if (!initialScrolled) {
1668 // Scroll substring to the given point. Turn the point into percents
1669 // relative scrollable area to use nsCoreUtils::ScrollSubstringTo.
1670 nsRect frameRect = parentFrame->GetScreenRectInAppUnits();
1671 nscoord offsetPointX = coordsInAppUnits.x - frameRect.X();
1672 nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y();
1673
1674 nsSize size(parentFrame->GetSize());
1675
1676 // avoid divide by zero
1677 size.width = size.width ? size.width : 1;
1678 size.height = size.height ? size.height : 1;
1679
1680 int16_t hPercent = offsetPointX * 100 / size.width;
1681 int16_t vPercent = offsetPointY * 100 / size.height;
1682
1683 nsresult rv = nsCoreUtils::ScrollSubstringTo(
1684 frame, range, ScrollAxis(vPercent, WhenToScroll::Always),
1685 ScrollAxis(hPercent, WhenToScroll::Always));
1686 if (NS_FAILED(rv)) return;
1687
1688 initialScrolled = true;
1689 } else {
1690 // Substring was scrolled to the given point already inside its closest
1691 // scrollable area. If there are nested scrollable areas then make
1692 // sure we scroll lower areas to the given point inside currently
1693 // traversed scrollable area.
1694 nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords);
1695 }
1696 }
1697 frame = parentFrame;
1698 }
1699 }
1700
EnclosingRange(a11y::TextRange & aRange) const1701 void HyperTextAccessible::EnclosingRange(a11y::TextRange& aRange) const {
1702 if (IsTextField()) {
1703 aRange.Set(mDoc, const_cast<HyperTextAccessible*>(this), 0,
1704 const_cast<HyperTextAccessible*>(this), CharacterCount());
1705 } else {
1706 aRange.Set(mDoc, mDoc, 0, mDoc, mDoc->CharacterCount());
1707 }
1708 }
1709
SelectionRanges(nsTArray<a11y::TextRange> * aRanges) const1710 void HyperTextAccessible::SelectionRanges(
1711 nsTArray<a11y::TextRange>* aRanges) const {
1712 MOZ_ASSERT(aRanges->Length() == 0, "TextRange array supposed to be empty");
1713
1714 dom::Selection* sel = DOMSelection();
1715 if (!sel) return;
1716
1717 aRanges->SetCapacity(sel->RangeCount());
1718
1719 for (uint32_t idx = 0; idx < sel->RangeCount(); idx++) {
1720 const nsRange* DOMRange = sel->GetRangeAt(idx);
1721 HyperTextAccessible* startContainer =
1722 nsAccUtils::GetTextContainer(DOMRange->GetStartContainer());
1723 HyperTextAccessible* endContainer =
1724 nsAccUtils::GetTextContainer(DOMRange->GetEndContainer());
1725 if (!startContainer || !endContainer) {
1726 continue;
1727 }
1728
1729 int32_t startOffset = startContainer->DOMPointToOffset(
1730 DOMRange->GetStartContainer(), DOMRange->StartOffset(), false);
1731 int32_t endOffset = endContainer->DOMPointToOffset(
1732 DOMRange->GetEndContainer(), DOMRange->EndOffset(), true);
1733
1734 TextRange tr(IsTextField() ? const_cast<HyperTextAccessible*>(this) : mDoc,
1735 startContainer, startOffset, endContainer, endOffset);
1736 *(aRanges->AppendElement()) = std::move(tr);
1737 }
1738 }
1739
VisibleRanges(nsTArray<a11y::TextRange> * aRanges) const1740 void HyperTextAccessible::VisibleRanges(
1741 nsTArray<a11y::TextRange>* aRanges) const {}
1742
RangeByChild(Accessible * aChild,a11y::TextRange & aRange) const1743 void HyperTextAccessible::RangeByChild(Accessible* aChild,
1744 a11y::TextRange& aRange) const {
1745 HyperTextAccessible* ht = aChild->AsHyperText();
1746 if (ht) {
1747 aRange.Set(mDoc, ht, 0, ht, ht->CharacterCount());
1748 return;
1749 }
1750
1751 Accessible* child = aChild;
1752 Accessible* parent = nullptr;
1753 while ((parent = child->Parent()) && !(ht = parent->AsHyperText()))
1754 child = parent;
1755
1756 // If no text then return collapsed text range, otherwise return a range
1757 // containing the text enclosed by the given child.
1758 if (ht) {
1759 int32_t childIdx = child->IndexInParent();
1760 int32_t startOffset = ht->GetChildOffset(childIdx);
1761 int32_t endOffset =
1762 child->IsTextLeaf() ? ht->GetChildOffset(childIdx + 1) : startOffset;
1763 aRange.Set(mDoc, ht, startOffset, ht, endOffset);
1764 }
1765 }
1766
RangeAtPoint(int32_t aX,int32_t aY,a11y::TextRange & aRange) const1767 void HyperTextAccessible::RangeAtPoint(int32_t aX, int32_t aY,
1768 a11y::TextRange& aRange) const {
1769 Accessible* child = mDoc->ChildAtPoint(aX, aY, eDeepestChild);
1770 if (!child) return;
1771
1772 Accessible* parent = nullptr;
1773 while ((parent = child->Parent()) && !parent->IsHyperText()) child = parent;
1774
1775 // Return collapsed text range for the point.
1776 if (parent) {
1777 HyperTextAccessible* ht = parent->AsHyperText();
1778 int32_t offset = ht->GetChildOffset(child);
1779 aRange.Set(mDoc, ht, offset, ht, offset);
1780 }
1781 }
1782
1783 ////////////////////////////////////////////////////////////////////////////////
1784 // Accessible public
1785
1786 // Accessible protected
NativeName(nsString & aName) const1787 ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const {
1788 // Check @alt attribute for invalid img elements.
1789 bool hasImgAlt = false;
1790 if (mContent->IsHTMLElement(nsGkAtoms::img)) {
1791 hasImgAlt = mContent->AsElement()->GetAttr(kNameSpaceID_None,
1792 nsGkAtoms::alt, aName);
1793 if (!aName.IsEmpty()) return eNameOK;
1794 }
1795
1796 ENameValueFlag nameFlag = AccessibleWrap::NativeName(aName);
1797 if (!aName.IsEmpty()) return nameFlag;
1798
1799 // Get name from title attribute for HTML abbr and acronym elements making it
1800 // a valid name from markup. Otherwise their name isn't picked up by recursive
1801 // name computation algorithm. See NS_OK_NAME_FROM_TOOLTIP.
1802 if (IsAbbreviation() && mContent->AsElement()->GetAttr(
1803 kNameSpaceID_None, nsGkAtoms::title, aName))
1804 aName.CompressWhitespace();
1805
1806 return hasImgAlt ? eNoNameOnPurpose : eNameOK;
1807 }
1808
Shutdown()1809 void HyperTextAccessible::Shutdown() {
1810 mOffsets.Clear();
1811 AccessibleWrap::Shutdown();
1812 }
1813
RemoveChild(Accessible * aAccessible)1814 bool HyperTextAccessible::RemoveChild(Accessible* aAccessible) {
1815 int32_t childIndex = aAccessible->IndexInParent();
1816 int32_t count = mOffsets.Length() - childIndex;
1817 if (count > 0) mOffsets.RemoveElementsAt(childIndex, count);
1818
1819 return AccessibleWrap::RemoveChild(aAccessible);
1820 }
1821
InsertChildAt(uint32_t aIndex,Accessible * aChild)1822 bool HyperTextAccessible::InsertChildAt(uint32_t aIndex, Accessible* aChild) {
1823 int32_t count = mOffsets.Length() - aIndex;
1824 if (count > 0) {
1825 mOffsets.RemoveElementsAt(aIndex, count);
1826 }
1827 return AccessibleWrap::InsertChildAt(aIndex, aChild);
1828 }
1829
RelationByType(RelationType aType) const1830 Relation HyperTextAccessible::RelationByType(RelationType aType) const {
1831 Relation rel = Accessible::RelationByType(aType);
1832
1833 switch (aType) {
1834 case RelationType::NODE_CHILD_OF:
1835 if (HasOwnContent() && mContent->IsMathMLElement()) {
1836 Accessible* parent = Parent();
1837 if (parent) {
1838 nsIContent* parentContent = parent->GetContent();
1839 if (parentContent &&
1840 parentContent->IsMathMLElement(nsGkAtoms::mroot_)) {
1841 // Add a relation pointing to the parent <mroot>.
1842 rel.AppendTarget(parent);
1843 }
1844 }
1845 }
1846 break;
1847 case RelationType::NODE_PARENT_OF:
1848 if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot_)) {
1849 Accessible* base = GetChildAt(0);
1850 Accessible* index = GetChildAt(1);
1851 if (base && index) {
1852 // Append the <mroot> children in the order index, base.
1853 rel.AppendTarget(index);
1854 rel.AppendTarget(base);
1855 }
1856 }
1857 break;
1858 default:
1859 break;
1860 }
1861
1862 return rel;
1863 }
1864
1865 ////////////////////////////////////////////////////////////////////////////////
1866 // HyperTextAccessible public static
1867
ContentToRenderedOffset(nsIFrame * aFrame,int32_t aContentOffset,uint32_t * aRenderedOffset) const1868 nsresult HyperTextAccessible::ContentToRenderedOffset(
1869 nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const {
1870 if (!aFrame) {
1871 // Current frame not rendered -- this can happen if text is set on
1872 // something with display: none
1873 *aRenderedOffset = 0;
1874 return NS_OK;
1875 }
1876
1877 if (IsTextField()) {
1878 *aRenderedOffset = aContentOffset;
1879 return NS_OK;
1880 }
1881
1882 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
1883 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
1884 "Call on primary frame only");
1885
1886 nsIFrame::RenderedText text =
1887 aFrame->GetRenderedText(aContentOffset, aContentOffset + 1,
1888 nsIFrame::TextOffsetType::OffsetsInContentText,
1889 nsIFrame::TrailingWhitespace::DontTrim);
1890 *aRenderedOffset = text.mOffsetWithinNodeRenderedText;
1891
1892 return NS_OK;
1893 }
1894
RenderedToContentOffset(nsIFrame * aFrame,uint32_t aRenderedOffset,int32_t * aContentOffset) const1895 nsresult HyperTextAccessible::RenderedToContentOffset(
1896 nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const {
1897 if (IsTextField()) {
1898 *aContentOffset = aRenderedOffset;
1899 return NS_OK;
1900 }
1901
1902 *aContentOffset = 0;
1903 NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE);
1904
1905 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion");
1906 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr,
1907 "Call on primary frame only");
1908
1909 nsIFrame::RenderedText text =
1910 aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1,
1911 nsIFrame::TextOffsetType::OffsetsInRenderedText,
1912 nsIFrame::TrailingWhitespace::DontTrim);
1913 *aContentOffset = text.mOffsetWithinNodeText;
1914
1915 return NS_OK;
1916 }
1917
1918 ////////////////////////////////////////////////////////////////////////////////
1919 // HyperTextAccessible public
1920
GetChildOffset(uint32_t aChildIndex,bool aInvalidateAfter) const1921 int32_t HyperTextAccessible::GetChildOffset(uint32_t aChildIndex,
1922 bool aInvalidateAfter) const {
1923 if (aChildIndex == 0) {
1924 if (aInvalidateAfter) mOffsets.Clear();
1925
1926 return aChildIndex;
1927 }
1928
1929 int32_t count = mOffsets.Length() - aChildIndex;
1930 if (count > 0) {
1931 if (aInvalidateAfter) mOffsets.RemoveElementsAt(aChildIndex, count);
1932
1933 return mOffsets[aChildIndex - 1];
1934 }
1935
1936 uint32_t lastOffset =
1937 mOffsets.IsEmpty() ? 0 : mOffsets[mOffsets.Length() - 1];
1938
1939 while (mOffsets.Length() < aChildIndex) {
1940 Accessible* child = mChildren[mOffsets.Length()];
1941 lastOffset += nsAccUtils::TextLength(child);
1942 mOffsets.AppendElement(lastOffset);
1943 }
1944
1945 return mOffsets[aChildIndex - 1];
1946 }
1947
GetChildIndexAtOffset(uint32_t aOffset) const1948 int32_t HyperTextAccessible::GetChildIndexAtOffset(uint32_t aOffset) const {
1949 uint32_t lastOffset = 0;
1950 const uint32_t offsetCount = mOffsets.Length();
1951
1952 if (offsetCount > 0) {
1953 lastOffset = mOffsets[offsetCount - 1];
1954 if (aOffset < lastOffset) {
1955 size_t index;
1956 if (BinarySearch(mOffsets, 0, offsetCount, aOffset, &index)) {
1957 return (index < (offsetCount - 1)) ? index + 1 : index;
1958 }
1959
1960 return (index == offsetCount) ? -1 : index;
1961 }
1962 }
1963
1964 uint32_t childCount = ChildCount();
1965 while (mOffsets.Length() < childCount) {
1966 Accessible* child = GetChildAt(mOffsets.Length());
1967 lastOffset += nsAccUtils::TextLength(child);
1968 mOffsets.AppendElement(lastOffset);
1969 if (aOffset < lastOffset) return mOffsets.Length() - 1;
1970 }
1971
1972 if (aOffset == lastOffset) return mOffsets.Length() - 1;
1973
1974 return -1;
1975 }
1976
1977 ////////////////////////////////////////////////////////////////////////////////
1978 // HyperTextAccessible protected
1979
GetDOMPointByFrameOffset(nsIFrame * aFrame,int32_t aOffset,Accessible * aAccessible,DOMPoint * aPoint)1980 nsresult HyperTextAccessible::GetDOMPointByFrameOffset(nsIFrame* aFrame,
1981 int32_t aOffset,
1982 Accessible* aAccessible,
1983 DOMPoint* aPoint) {
1984 NS_ENSURE_ARG(aAccessible);
1985
1986 if (!aFrame) {
1987 // If the given frame is null then set offset after the DOM node of the
1988 // given accessible.
1989 NS_ASSERTION(!aAccessible->IsDoc(),
1990 "Shouldn't be called on document accessible!");
1991
1992 nsIContent* content = aAccessible->GetContent();
1993 NS_ASSERTION(content, "Shouldn't operate on defunct accessible!");
1994
1995 nsIContent* parent = content->GetParent();
1996
1997 aPoint->idx = parent->ComputeIndexOf(content) + 1;
1998 aPoint->node = parent;
1999
2000 } else if (aFrame->IsTextFrame()) {
2001 nsIContent* content = aFrame->GetContent();
2002 NS_ENSURE_STATE(content);
2003
2004 nsIFrame* primaryFrame = content->GetPrimaryFrame();
2005 nsresult rv =
2006 RenderedToContentOffset(primaryFrame, aOffset, &(aPoint->idx));
2007 NS_ENSURE_SUCCESS(rv, rv);
2008
2009 aPoint->node = content;
2010
2011 } else {
2012 nsIContent* content = aFrame->GetContent();
2013 NS_ENSURE_STATE(content);
2014
2015 nsIContent* parent = content->GetParent();
2016 NS_ENSURE_STATE(parent);
2017
2018 aPoint->idx = parent->ComputeIndexOf(content);
2019 aPoint->node = parent;
2020 }
2021
2022 return NS_OK;
2023 }
2024
2025 // HyperTextAccessible
GetSpellTextAttr(nsINode * aNode,int32_t aNodeOffset,uint32_t * aStartOffset,uint32_t * aEndOffset,nsIPersistentProperties * aAttributes)2026 void HyperTextAccessible::GetSpellTextAttr(
2027 nsINode* aNode, int32_t aNodeOffset, uint32_t* aStartOffset,
2028 uint32_t* aEndOffset, nsIPersistentProperties* aAttributes) {
2029 RefPtr<nsFrameSelection> fs = FrameSelection();
2030 if (!fs) return;
2031
2032 dom::Selection* domSel = fs->GetSelection(SelectionType::eSpellCheck);
2033 if (!domSel) return;
2034
2035 int32_t rangeCount = domSel->RangeCount();
2036 if (rangeCount <= 0) return;
2037
2038 uint32_t startOffset = 0, endOffset = 0;
2039 for (int32_t idx = 0; idx < rangeCount; idx++) {
2040 const nsRange* range = domSel->GetRangeAt(idx);
2041 if (range->Collapsed()) continue;
2042
2043 // See if the point comes after the range in which case we must continue in
2044 // case there is another range after this one.
2045 nsINode* endNode = range->GetEndContainer();
2046 int32_t endNodeOffset = range->EndOffset();
2047 Maybe<int32_t> order = nsContentUtils::ComparePoints(
2048 aNode, aNodeOffset, endNode, endNodeOffset);
2049 if (NS_WARN_IF(!order)) {
2050 continue;
2051 }
2052
2053 if (*order >= 0) {
2054 continue;
2055 }
2056
2057 // At this point our point is either in this range or before it but after
2058 // the previous range. So we check to see if the range starts before the
2059 // point in which case the point is in the missspelled range, otherwise it
2060 // must be before the range and after the previous one if any.
2061 nsINode* startNode = range->GetStartContainer();
2062 int32_t startNodeOffset = range->StartOffset();
2063 order = nsContentUtils::ComparePoints(startNode, startNodeOffset, aNode,
2064 aNodeOffset);
2065 if (!order) {
2066 // As (`aNode`, `aNodeOffset`) is comparable to the end of the range, it
2067 // should also be comparable to the range's start. Returning here
2068 // prevents crashes in release builds.
2069 MOZ_ASSERT_UNREACHABLE();
2070 return;
2071 }
2072
2073 if (*order <= 0) {
2074 startOffset = DOMPointToOffset(startNode, startNodeOffset);
2075
2076 endOffset = DOMPointToOffset(endNode, endNodeOffset);
2077
2078 if (startOffset > *aStartOffset) *aStartOffset = startOffset;
2079
2080 if (endOffset < *aEndOffset) *aEndOffset = endOffset;
2081
2082 if (aAttributes) {
2083 nsAccUtils::SetAccAttr(aAttributes, nsGkAtoms::invalid,
2084 NS_LITERAL_STRING("spelling"));
2085 }
2086
2087 return;
2088 }
2089
2090 // This range came after the point.
2091 endOffset = DOMPointToOffset(startNode, startNodeOffset);
2092
2093 if (idx > 0) {
2094 const nsRange* prevRange = domSel->GetRangeAt(idx - 1);
2095 startOffset = DOMPointToOffset(prevRange->GetEndContainer(),
2096 prevRange->EndOffset());
2097 }
2098
2099 // The previous range might not be within this accessible. In that case,
2100 // DOMPointToOffset returns length as a fallback. We don't want to use
2101 // that offset if so, hence the startOffset < *aEndOffset check.
2102 if (startOffset > *aStartOffset && startOffset < *aEndOffset)
2103 *aStartOffset = startOffset;
2104
2105 if (endOffset < *aEndOffset) *aEndOffset = endOffset;
2106
2107 return;
2108 }
2109
2110 // We never found a range that ended after the point, therefore we know that
2111 // the point is not in a range, that we do not need to compute an end offset,
2112 // and that we should use the end offset of the last range to compute the
2113 // start offset of the text attribute range.
2114 const nsRange* prevRange = domSel->GetRangeAt(rangeCount - 1);
2115 startOffset =
2116 DOMPointToOffset(prevRange->GetEndContainer(), prevRange->EndOffset());
2117
2118 // The previous range might not be within this accessible. In that case,
2119 // DOMPointToOffset returns length as a fallback. We don't want to use
2120 // that offset if so, hence the startOffset < *aEndOffset check.
2121 if (startOffset > *aStartOffset && startOffset < *aEndOffset)
2122 *aStartOffset = startOffset;
2123 }
2124
IsTextRole()2125 bool HyperTextAccessible::IsTextRole() {
2126 const nsRoleMapEntry* roleMapEntry = ARIARoleMap();
2127 if (roleMapEntry && (roleMapEntry->role == roles::GRAPHIC ||
2128 roleMapEntry->role == roles::IMAGE_MAP ||
2129 roleMapEntry->role == roles::SLIDER ||
2130 roleMapEntry->role == roles::PROGRESSBAR ||
2131 roleMapEntry->role == roles::SEPARATOR))
2132 return false;
2133
2134 return true;
2135 }
2136