Bug 1769547 - Do not MOZ_CRASH() on missing process r=nika
[gecko.git] / accessible / base / Pivot.cpp
blob90697af46e2cf140244d0c1a26f543245c997bc9
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/. */
6 #include "Pivot.h"
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 ////////////////////////////////////////////////////////////////////////////////
25 // Pivot
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;
43 matched = temp;
48 return matched;
51 Accessible* Pivot::SearchBackward(Accessible* aAnchor, PivotRule& aRule,
52 bool aSearchCurrent) {
53 // Initial position could be unset, in that case return null.
54 if (!aAnchor) {
55 return nullptr;
58 uint16_t filtered = nsIAccessibleTraversalRule::FILTER_IGNORE;
60 Accessible* acc = AdjustStartPosition(aAnchor, aRule, &filtered);
62 if (aSearchCurrent && (filtered & nsIAccessibleTraversalRule::FILTER_MATCH)) {
63 return acc;
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);
71 if (!acc) {
72 continue;
75 filtered = aRule.Match(acc);
77 Accessible* lastChild = acc->LastChild();
78 while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
79 lastChild) {
80 parent = acc;
81 acc = lastChild;
82 idxInParent = acc->IndexInParent();
83 filtered = aRule.Match(acc);
84 lastChild = acc->LastChild();
87 if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
88 return acc;
92 acc = parent;
93 if (!acc) {
94 break;
97 filtered = aRule.Match(acc);
99 if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
100 return acc;
104 return nullptr;
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)) {
115 return acc;
118 while (acc) {
119 Accessible* firstChild = acc->FirstChild();
120 while (!(filtered & nsIAccessibleTraversalRule::FILTER_IGNORE_SUBTREE) &&
121 firstChild) {
122 acc = firstChild;
123 filtered = aRule.Match(acc);
125 if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
126 return acc;
128 firstChild = acc->FirstChild();
131 Accessible* sibling = nullptr;
132 Accessible* temp = acc;
133 do {
134 if (temp == mRoot) {
135 break;
138 sibling = temp->NextSibling();
140 if (sibling) {
141 break;
143 temp = temp->Parent();
144 } while (temp);
146 if (!sibling) {
147 break;
150 acc = sibling;
151 filtered = aRule.Match(acc);
152 if (filtered & nsIAccessibleTraversalRule::FILTER_MATCH) {
153 return acc;
157 return nullptr;
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.
164 return nullptr;
166 Accessible* accessible = aAnchor;
167 while (true) {
168 Accessible* child = nullptr;
170 while ((child = (aBackward ? accessible->LastChild()
171 : accessible->FirstChild()))) {
172 accessible = child;
173 if (child->IsHyperText()) {
174 return child;
178 Accessible* sibling = nullptr;
179 Accessible* temp = accessible;
180 do {
181 if (temp == mRoot) {
182 break;
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()) {
190 return temp;
193 if (sibling) {
194 break;
197 sibling = aBackward ? temp->PrevSibling() : temp->NextSibling();
198 } while ((temp = temp->Parent()));
200 if (!sibling) {
201 break;
204 accessible = sibling;
205 if (accessible->IsHyperText()) {
206 return accessible;
210 return nullptr;
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
236 // doc
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.
245 return nullptr;
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);
258 if (tempEnd == -1) {
259 tempStart = 0;
260 tempEnd = 0;
262 tempStart += childOffset;
263 tempEnd += childOffset;
266 while (true) {
267 MOZ_ASSERT(tempPosition);
268 Accessible* curPosition = tempPosition;
269 HyperTextAccessibleBase* text = nullptr;
270 // Find the nearest text node using a preorder traversal starting from
271 // the current node.
272 if (!(text = tempPosition->AsHyperTextBase())) {
273 tempPosition = SearchForText(tempPosition, false);
274 if (!tempPosition) {
275 return nullptr;
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.
287 if (tempEnd == -1) {
288 tempEnd = tempPosition == curPosition->Parent()
289 ? text->GetChildOffset(curPosition)
290 : 0;
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) {
297 return nullptr;
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
304 // subtree).
305 Accessible* sibling = tempPosition->NextSibling();
306 if (tempPosition->IsLink()) {
307 if (sibling && sibling->IsLink()) {
308 tempStart = tempEnd = -1;
309 tempPosition = sibling;
310 } else {
311 tempStart = tempPosition->StartOffset();
312 tempEnd = tempPosition->EndOffset();
313 tempPosition = tempPosition->Parent();
315 } else {
316 tempPosition = SearchForText(tempPosition, false);
317 if (!tempPosition) {
318 return nullptr;
321 tempStart = tempEnd = -1;
323 continue;
326 AccessibleTextBoundary startBoundary, endBoundary;
327 switch (aBoundaryType) {
328 case nsIAccessiblePivot::CHAR_BOUNDARY:
329 startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
330 endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
331 break;
332 case nsIAccessiblePivot::WORD_BOUNDARY:
333 startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
334 endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
335 break;
336 case nsIAccessiblePivot::LINE_BOUNDARY:
337 startBoundary = nsIAccessibleText::BOUNDARY_LINE_START;
338 endBoundary = nsIAccessibleText::BOUNDARY_LINE_END;
339 break;
340 default:
341 return nullptr;
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,
348 unusedText);
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
354 // character.
355 Accessible* childAtOffset = nullptr;
356 for (int32_t i = tempStart; i < tempEnd; i++) {
357 childAtOffset = text->GetChildAtOffset(i);
358 if (childAtOffset && childAtOffset->IsHyperText()) {
359 tempEnd = i;
360 break;
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;
370 continue;
373 *aStartOffset = tempStart;
374 *aEndOffset = tempEnd;
376 MOZ_ASSERT(tempPosition);
377 return 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.
386 return nullptr;
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);
401 tempEnd = tempStart;
403 tempStart += childOffset;
404 tempEnd += childOffset;
407 while (true) {
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);
416 if (!tempPosition) {
417 return nullptr;
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);
434 } else {
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) {
443 return nullptr;
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
450 // subtree).
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;
458 } else {
459 tempStart = tempPosition->StartOffset();
460 tempEnd = tempPosition->EndOffset();
461 tempPosition = tempPosition->Parent();
463 } else {
464 tempPosition = SearchForText(tempPosition, true);
465 if (!tempPosition) {
466 return nullptr;
469 HyperTextAccessibleBase* tempText = tempPosition->AsHyperTextBase();
470 tempStart = tempEnd = tempText->CharacterCount();
472 continue;
475 AccessibleTextBoundary startBoundary, endBoundary;
476 switch (aBoundaryType) {
477 case nsIAccessiblePivot::CHAR_BOUNDARY:
478 startBoundary = nsIAccessibleText::BOUNDARY_CHAR;
479 endBoundary = nsIAccessibleText::BOUNDARY_CHAR;
480 break;
481 case nsIAccessiblePivot::WORD_BOUNDARY:
482 startBoundary = nsIAccessibleText::BOUNDARY_WORD_START;
483 endBoundary = nsIAccessibleText::BOUNDARY_WORD_END;
484 break;
485 case nsIAccessiblePivot::LINE_BOUNDARY:
486 startBoundary = nsIAccessibleText::BOUNDARY_LINE_START;
487 endBoundary = nsIAccessibleText::BOUNDARY_LINE_END;
488 break;
489 default:
490 return nullptr;
493 nsAutoString unusedText;
494 int32_t newStart = 0, newEnd = 0, currentStart = tempStart,
495 potentialEnd = 0;
496 text->TextBeforeOffset(tempStart, startBoundary, &newStart, &newEnd,
497 unusedText);
498 if (newStart < tempStart) {
499 tempStart = newEnd >= currentStart ? newStart : newEnd;
500 } else {
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,
506 unusedText);
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
511 // character.
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();
517 break;
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());
528 continue;
531 *aStartOffset = tempStart;
532 *aEndOffset = tempEnd;
534 MOZ_ASSERT(tempPosition);
535 return tempPosition;
539 Accessible* Pivot::AtPoint(int32_t aX, int32_t aY, PivotRule& aRule) {
540 Accessible* match = nullptr;
541 Accessible* child =
542 mRoot ? mRoot->ChildAtPoint(aX, aY,
543 Accessible::EWhichChildAtPoint::DeepestChild)
544 : nullptr;
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) {
550 match = nullptr;
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)) {
561 match = child;
565 child = child->Parent();
568 return match;
571 // Role Rule
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;
602 return result;
605 // State Rule
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;
621 return result;
624 // LocalAccInSameDocRule
626 uint16_t LocalAccInSameDocRule::Match(Accessible* aAcc) {
627 LocalAccessible* acc = aAcc ? aAcc->AsLocal() : nullptr;
628 if (!acc) {
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;