1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* This Source Code Form is subject to the terms of the Mozilla Public
3 * License, v. 2.0. If a copy of the MPL was not distributed with this
4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 #include "AccIterator.h"
9 #include "LocalAccessible.h"
10 #include "RemoteAccessible.h"
11 #include "DocAccessible.h"
12 #include "nsAccessibilityService.h"
13 #include "nsAccUtils.h"
15 #include "mozilla/a11y/Accessible.h"
16 #include "mozilla/a11y/HyperTextAccessibleBase.h"
17 #include "mozilla/dom/ChildIterator.h"
18 #include "mozilla/dom/Element.h"
19 #include "mozilla/StaticPrefs_accessibility.h"
21 using namespace mozilla
;
22 using namespace mozilla::a11y
;
24 ////////////////////////////////////////////////////////////////////////////////
26 ////////////////////////////////////////////////////////////////////////////////
28 Pivot::Pivot(Accessible
* aRoot
) : mRoot(aRoot
) { MOZ_COUNT_CTOR(Pivot
); }
30 Pivot::~Pivot() { MOZ_COUNT_DTOR(Pivot
); }
32 Accessible
* Pivot::AdjustStartPosition(Accessible
* aAnchor
, PivotRule
& aRule
,
33 uint16_t* aFilterResult
) {
34 Accessible
* matched
= aAnchor
;
35 *aFilterResult
= aRule
.Match(aAnchor
);
37 if (aAnchor
&& aAnchor
!= mRoot
) {
38 for (Accessible
* temp
= aAnchor
->Parent(); temp
&& temp
!= mRoot
;
39 temp
= temp
->Parent()) {
40 uint16_t filtered
= aRule
.Match(temp
);
41 if (filtered
& nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
) {
42 *aFilterResult
= filtered
;
51 Accessible
* Pivot::SearchBackward(Accessible
* aAnchor
, PivotRule
& aRule
,
52 bool aSearchCurrent
) {
53 // Initial position could be unset, in that case return null.
58 uint16_t filtered
= nsIAccessibleTraversalRule::FILTER_IGNORE
;
60 Accessible
* acc
= AdjustStartPosition(aAnchor
, aRule
, &filtered
);
62 if (aSearchCurrent
&& (filtered
& nsIAccessibleTraversalRule::FILTER_MATCH
)) {
66 while (acc
&& acc
!= mRoot
) {
67 Accessible
* parent
= acc
->Parent();
68 int32_t idxInParent
= acc
->IndexInParent();
69 while (idxInParent
> 0 && parent
) {
70 acc
= parent
->ChildAt(--idxInParent
);
75 filtered
= aRule
.Match(acc
);
77 Accessible
* lastChild
= acc
->LastChild();
78 while (!(filtered
& nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
) &&
82 idxInParent
= acc
->IndexInParent();
83 filtered
= aRule
.Match(acc
);
84 lastChild
= acc
->LastChild();
87 if (filtered
& nsIAccessibleTraversalRule::FILTER_MATCH
) {
97 filtered
= aRule
.Match(acc
);
99 if (filtered
& nsIAccessibleTraversalRule::FILTER_MATCH
) {
107 Accessible
* Pivot::SearchForward(Accessible
* aAnchor
, PivotRule
& aRule
,
108 bool aSearchCurrent
) {
109 // Initial position could be not set, in that case begin search from root.
110 Accessible
* acc
= aAnchor
? aAnchor
: mRoot
;
112 uint16_t filtered
= nsIAccessibleTraversalRule::FILTER_IGNORE
;
113 acc
= AdjustStartPosition(acc
, aRule
, &filtered
);
114 if (aSearchCurrent
&& (filtered
& nsIAccessibleTraversalRule::FILTER_MATCH
)) {
119 Accessible
* firstChild
= acc
->FirstChild();
120 while (!(filtered
& nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
) &&
123 filtered
= aRule
.Match(acc
);
125 if (filtered
& nsIAccessibleTraversalRule::FILTER_MATCH
) {
128 firstChild
= acc
->FirstChild();
131 Accessible
* sibling
= nullptr;
132 Accessible
* temp
= acc
;
138 sibling
= temp
->NextSibling();
143 temp
= temp
->Parent();
151 filtered
= aRule
.Match(acc
);
152 if (filtered
& nsIAccessibleTraversalRule::FILTER_MATCH
) {
160 Accessible
* Pivot::SearchForText(Accessible
* aAnchor
, bool aBackward
) {
161 if (mRoot
->IsRemote() &&
162 !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
163 // Not supported for RemoteAccessible when the cache is disabled.
166 Accessible
* accessible
= aAnchor
;
168 Accessible
* child
= nullptr;
170 while ((child
= (aBackward
? accessible
->LastChild()
171 : accessible
->FirstChild()))) {
173 if (child
->IsHyperText()) {
178 Accessible
* sibling
= nullptr;
179 Accessible
* temp
= accessible
;
185 // Unlike traditional pre-order traversal we revisit the parent
186 // nodes when we go up the tree. This is because our starting point
187 // may be a subtree or a leaf. If it's parent matches, it should
188 // take precedent over a sibling.
189 if (temp
!= aAnchor
&& temp
->IsHyperText()) {
197 sibling
= aBackward
? temp
->PrevSibling() : temp
->NextSibling();
198 } while ((temp
= temp
->Parent()));
204 accessible
= sibling
;
205 if (accessible
->IsHyperText()) {
213 Accessible
* Pivot::Next(Accessible
* aAnchor
, PivotRule
& aRule
,
214 bool aIncludeStart
) {
215 return SearchForward(aAnchor
, aRule
, aIncludeStart
);
218 Accessible
* Pivot::Prev(Accessible
* aAnchor
, PivotRule
& aRule
,
219 bool aIncludeStart
) {
220 return SearchBackward(aAnchor
, aRule
, aIncludeStart
);
223 Accessible
* Pivot::First(PivotRule
& aRule
) {
224 return SearchForward(mRoot
, aRule
, true);
227 Accessible
* Pivot::Last(PivotRule
& aRule
) {
228 Accessible
* lastAcc
= mRoot
;
230 // First go to the last accessible in pre-order
231 while (lastAcc
&& lastAcc
->HasChildren()) {
232 lastAcc
= lastAcc
->LastChild();
235 // Search backwards from last accessible and find the last occurrence in the
237 return SearchBackward(lastAcc
, aRule
, true);
240 Accessible
* Pivot::NextText(Accessible
* aAnchor
, int32_t* aStartOffset
,
241 int32_t* aEndOffset
, int32_t aBoundaryType
) {
242 if (mRoot
->IsRemote() &&
243 !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
244 // Not supported for RemoteAccessible when the cache is disabled.
248 int32_t tempStart
= *aStartOffset
, tempEnd
= *aEndOffset
;
249 Accessible
* tempPosition
= aAnchor
;
251 // if we're starting on a text leaf, translate the offsets to the
252 // HyperTextAccessible parent and start from there.
253 if (aAnchor
->IsTextLeaf() && aAnchor
->Parent() &&
254 aAnchor
->Parent()->IsHyperText()) {
255 tempPosition
= aAnchor
->Parent();
256 HyperTextAccessibleBase
* text
= tempPosition
->AsHyperTextBase();
257 int32_t childOffset
= text
->GetChildOffset(aAnchor
);
262 tempStart
+= childOffset
;
263 tempEnd
+= childOffset
;
267 MOZ_ASSERT(tempPosition
);
268 Accessible
* curPosition
= tempPosition
;
269 HyperTextAccessibleBase
* text
= nullptr;
270 // Find the nearest text node using a preorder traversal starting from
272 if (!(text
= tempPosition
->AsHyperTextBase())) {
273 tempPosition
= SearchForText(tempPosition
, false);
278 if (tempPosition
!= curPosition
) {
279 tempStart
= tempEnd
= -1;
281 text
= tempPosition
->AsHyperTextBase();
284 // If the search led to the parent of the node we started on (e.g. when
285 // starting on a text leaf), start the text movement from the end of that
286 // node, otherwise we just default to 0.
288 tempEnd
= tempPosition
== curPosition
->Parent()
289 ? text
->GetChildOffset(curPosition
)
293 // If there's no more text on the current node, try to find the next text
294 // node; if there isn't one, bail out.
295 if (tempEnd
== static_cast<int32_t>(text
->CharacterCount())) {
296 if (tempPosition
== mRoot
) {
300 // If we're currently sitting on a link, try move to either the next
301 // sibling or the parent, whichever is closer to the current end
302 // offset. Otherwise, do a forward search for the next node to land on
303 // (we don't do this in the first case because we don't want to go to the
305 Accessible
* sibling
= tempPosition
->NextSibling();
306 if (tempPosition
->IsLink()) {
307 if (sibling
&& sibling
->IsLink()) {
308 tempStart
= tempEnd
= -1;
309 tempPosition
= sibling
;
311 tempStart
= tempPosition
->StartOffset();
312 tempEnd
= tempPosition
->EndOffset();
313 tempPosition
= tempPosition
->Parent();
316 tempPosition
= SearchForText(tempPosition
, false);
321 tempStart
= tempEnd
= -1;
326 AccessibleTextBoundary startBoundary
, endBoundary
;
327 switch (aBoundaryType
) {
328 case nsIAccessiblePivot::CHAR_BOUNDARY
:
329 startBoundary
= nsIAccessibleText::BOUNDARY_CHAR
;
330 endBoundary
= nsIAccessibleText::BOUNDARY_CHAR
;
332 case nsIAccessiblePivot::WORD_BOUNDARY
:
333 startBoundary
= nsIAccessibleText::BOUNDARY_WORD_START
;
334 endBoundary
= nsIAccessibleText::BOUNDARY_WORD_END
;
336 case nsIAccessiblePivot::LINE_BOUNDARY
:
337 startBoundary
= nsIAccessibleText::BOUNDARY_LINE_START
;
338 endBoundary
= nsIAccessibleText::BOUNDARY_LINE_END
;
344 nsAutoString unusedText
;
345 int32_t newStart
= 0, newEnd
= 0, currentEnd
= tempEnd
;
346 text
->TextAtOffset(tempEnd
, endBoundary
, &newStart
, &tempEnd
, unusedText
);
347 text
->TextBeforeOffset(tempEnd
, startBoundary
, &newStart
, &newEnd
,
349 int32_t potentialStart
= newEnd
== tempEnd
? newStart
: newEnd
;
350 tempStart
= potentialStart
> tempStart
? potentialStart
: currentEnd
;
352 // The offset range we've obtained might have embedded characters in it,
353 // limit the range to the start of the first occurrence of an embedded
355 Accessible
* childAtOffset
= nullptr;
356 for (int32_t i
= tempStart
; i
< tempEnd
; i
++) {
357 childAtOffset
= text
->GetChildAtOffset(i
);
358 if (childAtOffset
&& childAtOffset
->IsHyperText()) {
363 // If there's an embedded character at the very start of the range, we
364 // instead want to traverse into it. So restart the movement with
365 // the child as the starting point.
366 if (childAtOffset
&& childAtOffset
->IsHyperText() &&
367 tempStart
== static_cast<int32_t>(childAtOffset
->StartOffset())) {
368 tempPosition
= childAtOffset
;
369 tempStart
= tempEnd
= -1;
373 *aStartOffset
= tempStart
;
374 *aEndOffset
= tempEnd
;
376 MOZ_ASSERT(tempPosition
);
381 Accessible
* Pivot::PrevText(Accessible
* aAnchor
, int32_t* aStartOffset
,
382 int32_t* aEndOffset
, int32_t aBoundaryType
) {
383 if (mRoot
->IsRemote() &&
384 !StaticPrefs::accessibility_cache_enabled_AtStartup()) {
385 // Not supported for RemoteAccessible when the cache is disabled.
389 int32_t tempStart
= *aStartOffset
, tempEnd
= *aEndOffset
;
390 Accessible
* tempPosition
= aAnchor
;
392 // if we're starting on a text leaf, translate the offsets to the
393 // HyperTextAccessible parent and start from there.
394 if (aAnchor
->IsTextLeaf() && aAnchor
->Parent() &&
395 aAnchor
->Parent()->IsHyperText()) {
396 tempPosition
= aAnchor
->Parent();
397 HyperTextAccessibleBase
* text
= tempPosition
->AsHyperTextBase();
398 int32_t childOffset
= text
->GetChildOffset(aAnchor
);
399 if (tempStart
== -1) {
400 tempStart
= nsAccUtils::TextLength(aAnchor
);
403 tempStart
+= childOffset
;
404 tempEnd
+= childOffset
;
408 MOZ_ASSERT(tempPosition
);
410 Accessible
* curPosition
= tempPosition
;
411 HyperTextAccessibleBase
* text
;
412 // Find the nearest text node using a reverse preorder traversal starting
413 // from the current node.
414 if (!(text
= tempPosition
->AsHyperTextBase())) {
415 tempPosition
= SearchForText(tempPosition
, true);
420 if (tempPosition
!= curPosition
) {
421 tempStart
= tempEnd
= -1;
423 text
= tempPosition
->AsHyperTextBase();
426 // If the search led to the parent of the node we started on (e.g. when
427 // starting on a text leaf), start the text movement from the end offset
428 // of that node. Otherwise we just default to the last offset in the parent.
429 if (tempStart
== -1) {
430 if (tempPosition
!= curPosition
&&
431 tempPosition
== curPosition
->Parent()) {
432 tempStart
= text
->GetChildOffset(curPosition
) +
433 nsAccUtils::TextLength(curPosition
);
435 tempStart
= text
->CharacterCount();
439 // If there's no more text on the current node, try to find the previous
440 // text node; if there isn't one, bail out.
441 if (tempStart
== 0) {
442 if (tempPosition
== mRoot
) {
446 // If we're currently sitting on a link, try move to either the previous
447 // sibling or the parent, whichever is closer to the current end
448 // offset. Otherwise, do a forward search for the next node to land on
449 // (we don't do this in the first case because we don't want to go to the
451 Accessible
* sibling
= tempPosition
->PrevSibling();
452 if (tempPosition
->IsLink()) {
453 if (sibling
&& sibling
->IsLink()) {
454 HyperTextAccessibleBase
* siblingText
= sibling
->AsHyperTextBase();
455 tempStart
= tempEnd
=
456 siblingText
? siblingText
->CharacterCount() : -1;
457 tempPosition
= sibling
;
459 tempStart
= tempPosition
->StartOffset();
460 tempEnd
= tempPosition
->EndOffset();
461 tempPosition
= tempPosition
->Parent();
464 tempPosition
= SearchForText(tempPosition
, true);
469 HyperTextAccessibleBase
* tempText
= tempPosition
->AsHyperTextBase();
470 tempStart
= tempEnd
= tempText
->CharacterCount();
475 AccessibleTextBoundary startBoundary
, endBoundary
;
476 switch (aBoundaryType
) {
477 case nsIAccessiblePivot::CHAR_BOUNDARY
:
478 startBoundary
= nsIAccessibleText::BOUNDARY_CHAR
;
479 endBoundary
= nsIAccessibleText::BOUNDARY_CHAR
;
481 case nsIAccessiblePivot::WORD_BOUNDARY
:
482 startBoundary
= nsIAccessibleText::BOUNDARY_WORD_START
;
483 endBoundary
= nsIAccessibleText::BOUNDARY_WORD_END
;
485 case nsIAccessiblePivot::LINE_BOUNDARY
:
486 startBoundary
= nsIAccessibleText::BOUNDARY_LINE_START
;
487 endBoundary
= nsIAccessibleText::BOUNDARY_LINE_END
;
493 nsAutoString unusedText
;
494 int32_t newStart
= 0, newEnd
= 0, currentStart
= tempStart
,
496 text
->TextBeforeOffset(tempStart
, startBoundary
, &newStart
, &newEnd
,
498 if (newStart
< tempStart
) {
499 tempStart
= newEnd
>= currentStart
? newStart
: newEnd
;
501 // XXX: In certain odd cases newStart is equal to tempStart
502 text
->TextBeforeOffset(tempStart
- 1, startBoundary
, &newStart
,
503 &tempStart
, unusedText
);
505 text
->TextAtOffset(tempStart
, endBoundary
, &newStart
, &potentialEnd
,
507 tempEnd
= potentialEnd
< tempEnd
? potentialEnd
: currentStart
;
509 // The offset range we've obtained might have embedded characters in it,
510 // limit the range to the start of the last occurrence of an embedded
512 Accessible
* childAtOffset
= nullptr;
513 for (int32_t i
= tempEnd
- 1; i
>= tempStart
; i
--) {
514 childAtOffset
= text
->GetChildAtOffset(i
);
515 if (childAtOffset
&& !childAtOffset
->IsText()) {
516 tempStart
= childAtOffset
->EndOffset();
520 // If there's an embedded character at the very end of the range, we
521 // instead want to traverse into it. So restart the movement with
522 // the child as the starting point.
523 if (childAtOffset
&& !childAtOffset
->IsText() &&
524 tempEnd
== static_cast<int32_t>(childAtOffset
->EndOffset())) {
525 tempPosition
= childAtOffset
;
526 tempStart
= tempEnd
= static_cast<int32_t>(
527 childAtOffset
->AsHyperTextBase()->CharacterCount());
531 *aStartOffset
= tempStart
;
532 *aEndOffset
= tempEnd
;
534 MOZ_ASSERT(tempPosition
);
539 Accessible
* Pivot::AtPoint(int32_t aX
, int32_t aY
, PivotRule
& aRule
) {
540 Accessible
* match
= nullptr;
542 mRoot
? mRoot
->ChildAtPoint(aX
, aY
,
543 Accessible::EWhichChildAtPoint::DeepestChild
)
545 while (child
&& (mRoot
!= child
)) {
546 uint16_t filtered
= aRule
.Match(child
);
548 // Ignore any matching nodes that were below this one
549 if (filtered
& nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
) {
553 // Match if no node below this is a match
554 if ((filtered
& nsIAccessibleTraversalRule::FILTER_MATCH
) && !match
) {
555 LayoutDeviceIntRect childRect
= child
->IsLocal()
556 ? child
->AsLocal()->Bounds()
557 : child
->AsRemote()->Bounds();
558 // Double-check child's bounds since the deepest child may have been out
559 // of bounds. This assures we don't return a false positive.
560 if (childRect
.Contains(aX
, aY
)) {
565 child
= child
->Parent();
573 PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole
)
574 : mRole(aRole
), mDirectDescendantsFrom(nullptr) {}
576 PivotRoleRule::PivotRoleRule(mozilla::a11y::role aRole
,
577 Accessible
* aDirectDescendantsFrom
)
578 : mRole(aRole
), mDirectDescendantsFrom(aDirectDescendantsFrom
) {}
580 uint16_t PivotRoleRule::Match(Accessible
* aAcc
) {
581 uint16_t result
= nsIAccessibleTraversalRule::FILTER_IGNORE
;
583 if (nsAccUtils::MustPrune(aAcc
)) {
584 result
|= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
587 if (mDirectDescendantsFrom
&& (aAcc
!= mDirectDescendantsFrom
)) {
588 // If we've specified mDirectDescendantsFrom, we should ignore
589 // non-direct descendants of from the specified AoP. Because
590 // pivot performs a preorder traversal, the first aAcc
591 // object(s) that don't equal mDirectDescendantsFrom will be
592 // mDirectDescendantsFrom's children. We'll process them, but ignore
593 // their subtrees thereby processing direct descendants of
594 // mDirectDescendantsFrom only.
595 result
|= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
598 if (aAcc
&& aAcc
->Role() == mRole
) {
599 result
|= nsIAccessibleTraversalRule::FILTER_MATCH
;
607 PivotStateRule::PivotStateRule(uint64_t aState
) : mState(aState
) {}
609 uint16_t PivotStateRule::Match(Accessible
* aAcc
) {
610 uint16_t result
= nsIAccessibleTraversalRule::FILTER_IGNORE
;
612 if (nsAccUtils::MustPrune(aAcc
)) {
613 result
|= nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
616 if (aAcc
&& (aAcc
->State() & mState
)) {
617 result
= nsIAccessibleTraversalRule::FILTER_MATCH
|
618 nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
624 // LocalAccInSameDocRule
626 uint16_t LocalAccInSameDocRule::Match(Accessible
* aAcc
) {
627 LocalAccessible
* acc
= aAcc
? aAcc
->AsLocal() : nullptr;
629 return nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
631 if (acc
->IsOuterDoc()) {
632 return nsIAccessibleTraversalRule::FILTER_MATCH
|
633 nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE
;
635 return nsIAccessibleTraversalRule::FILTER_MATCH
;