HyperTextAccessible.cpp (35860B)
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 "nsAccessibilityService.h" 10 #include "nsIAccessibleTypes.h" 11 #include "AccAttributes.h" 12 #include "HTMLListAccessible.h" 13 #include "LocalAccessible-inl.h" 14 #include "Relation.h" 15 #include "mozilla/a11y/Role.h" 16 #include "States.h" 17 #include "TextAttrs.h" 18 #include "TextLeafRange.h" 19 #include "TextRange.h" 20 #include "TreeWalker.h" 21 22 #include "nsCaret.h" 23 #include "nsContentUtils.h" 24 #include "nsDebug.h" 25 #include "nsFocusManager.h" 26 #include "nsIEditingSession.h" 27 #include "nsContainerFrame.h" 28 #include "nsFrameSelection.h" 29 #include "nsILineIterator.h" 30 #include "nsIMathMLFrame.h" 31 #include "nsLayoutUtils.h" 32 #include "nsRange.h" 33 #include "mozilla/Assertions.h" 34 #include "mozilla/EditorBase.h" 35 #include "mozilla/HTMLEditor.h" 36 #include "mozilla/PresShell.h" 37 #include "mozilla/ScrollContainerFrame.h" 38 #include "mozilla/SelectionMovementUtils.h" 39 #include "mozilla/dom/Element.h" 40 #include "mozilla/dom/HTMLBRElement.h" 41 #include "mozilla/dom/Selection.h" 42 #include "gfxSkipChars.h" 43 44 using namespace mozilla; 45 using namespace mozilla::a11y; 46 47 //////////////////////////////////////////////////////////////////////////////// 48 // HyperTextAccessible 49 //////////////////////////////////////////////////////////////////////////////// 50 51 HyperTextAccessible::HyperTextAccessible(nsIContent* aNode, DocAccessible* aDoc) 52 : AccessibleWrap(aNode, aDoc) { 53 mType = eHyperTextType; 54 mGenericTypes |= eHyperText; 55 } 56 57 role HyperTextAccessible::NativeRole() const { 58 a11y::role r = GetAccService()->MarkupRole(mContent); 59 if (r != roles::NOTHING) return r; 60 61 nsIFrame* frame = GetFrame(); 62 if (frame && frame->IsInlineFrame()) return roles::TEXT; 63 64 return roles::TEXT_CONTAINER; 65 } 66 67 uint64_t HyperTextAccessible::NativeState() const { 68 uint64_t states = AccessibleWrap::NativeState(); 69 70 if (IsEditable()) { 71 states |= states::EDITABLE; 72 73 } else if (mContent->IsHTMLElement(nsGkAtoms::article)) { 74 // We want <article> to behave like a document in terms of readonly state. 75 states |= states::READONLY; 76 } 77 78 nsIFrame* frame = GetFrame(); 79 if ((states & states::EDITABLE) || (frame && frame->IsSelectable())) { 80 // If the accessible is editable the layout selectable state only disables 81 // mouse selection, but keyboard (shift+arrow) selection is still possible. 82 states |= states::SELECTABLE_TEXT; 83 } 84 85 return states; 86 } 87 88 uint32_t HyperTextAccessible::DOMPointToOffset(nsINode* aNode, 89 int32_t aNodeOffset, 90 bool aIsEndOffset) const { 91 if (!aNode) return 0; 92 93 uint32_t offset = 0; 94 nsINode* findNode = nullptr; 95 96 if (aNodeOffset == -1) { 97 findNode = aNode; 98 99 } else if (aNode->IsText()) { 100 // For text nodes, aNodeOffset comes in as a character offset 101 // Text offset will be added at the end, if we find the offset in this 102 // hypertext We want the "skipped" offset into the text (rendered text 103 // without the extra whitespace) 104 nsIFrame* frame = aNode->AsContent()->GetPrimaryFrame(); 105 NS_ENSURE_TRUE(frame, 0); 106 107 nsresult rv = ContentToRenderedOffset(frame, aNodeOffset, &offset); 108 NS_ENSURE_SUCCESS(rv, 0); 109 110 findNode = aNode; 111 112 } else { 113 // findNode could be null if aNodeOffset == # of child nodes, which means 114 // one of two things: 115 // 1) there are no children, and the passed-in node is not mContent -- use 116 // parentContent for the node to find 117 // 2) there are no children and the passed-in node is mContent, which means 118 // we're an empty nsIAccessibleText 119 // 3) there are children and we're at the end of the children 120 121 findNode = aNode->GetChildAt_Deprecated(aNodeOffset); 122 if (!findNode) { 123 if (aNodeOffset == 0) { 124 if (aNode == GetNode()) { 125 // Case #1: this accessible has no children and thus has empty text, 126 // we can only be at hypertext offset 0. 127 return 0; 128 } 129 130 // Case #2: there are no children, we're at this node. 131 findNode = aNode; 132 } else if (aNodeOffset == static_cast<int32_t>(aNode->GetChildCount())) { 133 // Case #3: we're after the last child, get next node to this one. 134 for (nsINode* tmpNode = aNode; 135 !findNode && tmpNode && tmpNode != mContent; 136 tmpNode = tmpNode->GetParent()) { 137 findNode = tmpNode->GetNextSibling(); 138 } 139 } 140 } 141 } 142 143 // Get accessible for this findNode, or if that node isn't accessible, use the 144 // accessible for the next DOM node which has one (based on forward depth 145 // first search) 146 LocalAccessible* descendant = nullptr; 147 if (findNode) { 148 dom::HTMLBRElement* brElement = dom::HTMLBRElement::FromNode(findNode); 149 if (brElement && brElement->IsPaddingForEmptyEditor()) { 150 // This <br> is the hacky "padding <br> element" used when there is no 151 // text in the editor. 152 return 0; 153 } 154 155 descendant = mDoc->GetAccessible(findNode); 156 if (!descendant && findNode->IsContent()) { 157 LocalAccessible* container = mDoc->GetContainerAccessible(findNode); 158 if (container) { 159 TreeWalker walker(container, findNode->AsContent(), 160 TreeWalker::eWalkContextTree); 161 descendant = walker.Next(); 162 if (!descendant) descendant = container; 163 } 164 } 165 } 166 167 if (!descendant) { 168 // This DOM point can't be mapped to an offset in this HyperTextAccessible. 169 // Return the length as a fallback. 170 return CharacterCount(); 171 } 172 173 if (aNode->IsText() && descendant->GetContent() != aNode) { 174 // `offset` is relative to aNode, but aNode doesn't have an Accessible, so 175 // we used the next Accessible. This means that `offset` is no longer valid. 176 NS_WARNING("No Accessible for DOM text node"); 177 offset = 0; 178 } else if (descendant->IsTextLeaf()) { 179 uint32_t length = nsAccUtils::TextLength(descendant); 180 if (offset > length) { 181 // This can happen if text in the accessibility tree is out of date with 182 // DOM, since the accessibility engine updates text asynchronously. This 183 // should only be the case for a very short time, so it shouldn't be a 184 // real problem. 185 NS_WARNING("Offset too large for text leaf"); 186 offset = length; 187 } 188 } 189 190 return TransformOffset(descendant, offset, aIsEndOffset); 191 } 192 193 uint32_t HyperTextAccessible::TransformOffset(LocalAccessible* aDescendant, 194 uint32_t aOffset, 195 bool aIsEndOffset) const { 196 // From the descendant, go up and get the immediate child of this hypertext. 197 uint32_t offset = aOffset; 198 LocalAccessible* descendant = aDescendant; 199 while (descendant) { 200 LocalAccessible* parent = descendant->LocalParent(); 201 if (parent == this) return GetChildOffset(descendant) + offset; 202 203 // This offset no longer applies because the passed-in text object is not 204 // a child of the hypertext. This happens when there are nested hypertexts, 205 // e.g. <div>abc<h1>def</h1>ghi</div>. Thus we need to adjust the offset 206 // to make it relative the hypertext. 207 // If the end offset is not supposed to be inclusive and the original point 208 // is not at 0 offset then the returned offset should be after an embedded 209 // character the original point belongs to. 210 if (aIsEndOffset) { 211 // Similar to our special casing in FindOffset, we add handling for 212 // bulleted lists here because PeekOffset returns the inner text node 213 // for a list when it should return the list bullet. 214 // We manually set the offset so the error doesn't propagate up. 215 if (offset == 0 && parent && parent->IsHTMLListItem() && 216 descendant->LocalPrevSibling() && 217 descendant->LocalPrevSibling() == 218 parent->AsHTMLListItem()->Bullet()) { 219 offset = 0; 220 } else { 221 offset = (offset > 0 || descendant->IndexInParent() > 0) ? 1 : 0; 222 } 223 } else { 224 offset = 0; 225 } 226 227 descendant = parent; 228 } 229 230 // If the given a11y point cannot be mapped into offset relative this 231 // hypertext offset then return length as fallback value. 232 return CharacterCount(); 233 } 234 235 DOMPoint HyperTextAccessible::OffsetToDOMPoint(int32_t aOffset) const { 236 // 0 offset is valid even if no children. In this case the associated editor 237 // is empty so return a DOM point for editor root element. 238 if (aOffset == 0) { 239 RefPtr<EditorBase> editorBase = GetEditor(); 240 if (editorBase) { 241 if (editorBase->IsEmpty()) { 242 return DOMPoint(editorBase->GetRoot(), 0); 243 } 244 } 245 } 246 247 int32_t childIdx = GetChildIndexAtOffset(aOffset); 248 if (childIdx == -1) return DOMPoint(); 249 250 LocalAccessible* child = LocalChildAt(childIdx); 251 int32_t innerOffset = aOffset - GetChildOffset(childIdx); 252 253 // A text leaf case. 254 if (child->IsTextLeaf()) { 255 // The point is inside the text node. This is always true for any text leaf 256 // except a last child one. See assertion below. 257 if (aOffset < GetChildOffset(childIdx + 1)) { 258 nsIContent* content = child->GetContent(); 259 int32_t idx = 0; 260 if (NS_FAILED(RenderedToContentOffset(content->GetPrimaryFrame(), 261 innerOffset, &idx))) { 262 return DOMPoint(); 263 } 264 265 return DOMPoint(content, idx); 266 } 267 268 // Set the DOM point right after the text node. 269 MOZ_ASSERT(static_cast<uint32_t>(aOffset) == CharacterCount()); 270 innerOffset = 1; 271 } 272 273 // Case of embedded object. The point is either before or after the element. 274 NS_ASSERTION(innerOffset == 0 || innerOffset == 1, "A wrong inner offset!"); 275 nsINode* node = child->GetNode(); 276 nsINode* parentNode = node->GetParentNode(); 277 return parentNode ? DOMPoint(parentNode, 278 parentNode->ComputeIndexOf_Deprecated(node) + 279 innerOffset) 280 : DOMPoint(); 281 } 282 283 already_AddRefed<AccAttributes> HyperTextAccessible::DefaultTextAttributes() { 284 RefPtr<AccAttributes> attributes = new AccAttributes(); 285 286 TextAttrsMgr textAttrsMgr(this); 287 textAttrsMgr.GetAttributes(attributes); 288 return attributes.forget(); 289 } 290 291 void HyperTextAccessible::SetMathMLXMLRoles(AccAttributes* aAttributes) { 292 // Add MathML xmlroles based on the position inside the parent. 293 LocalAccessible* parent = LocalParent(); 294 if (parent) { 295 switch (parent->Role()) { 296 case roles::MATHML_CELL: 297 case roles::MATHML_ENCLOSED: 298 case roles::MATHML_ERROR: 299 case roles::MATHML_MATH: 300 case roles::MATHML_ROW: 301 case roles::MATHML_SQUARE_ROOT: 302 case roles::MATHML_STYLE: 303 if (Role() == roles::MATHML_OPERATOR) { 304 // This is an operator inside an <mrow> (or an inferred <mrow>). 305 // See http://www.w3.org/TR/MathML3/chapter3.html#presm.inferredmrow 306 // XXX We should probably do something similar for MATHML_FENCED, but 307 // operators do not appear in the accessible tree. See bug 1175747. 308 nsIMathMLFrame* mathMLFrame = do_QueryFrame(GetFrame()); 309 if (mathMLFrame) { 310 nsEmbellishData embellishData; 311 mathMLFrame->GetEmbellishData(embellishData); 312 if (embellishData.flags.contains(MathMLEmbellishFlag::Fence)) { 313 if (!LocalPrevSibling()) { 314 aAttributes->SetAttribute(nsGkAtoms::xmlroles, 315 nsGkAtoms::open_fence); 316 } else if (!LocalNextSibling()) { 317 aAttributes->SetAttribute(nsGkAtoms::xmlroles, 318 nsGkAtoms::close_fence); 319 } 320 } 321 if (embellishData.flags.contains(MathMLEmbellishFlag::Separator)) { 322 aAttributes->SetAttribute(nsGkAtoms::xmlroles, 323 nsGkAtoms::separator); 324 } 325 } 326 } 327 break; 328 case roles::MATHML_FRACTION: 329 aAttributes->SetAttribute( 330 nsGkAtoms::xmlroles, IndexInParent() == 0 ? nsGkAtoms::numerator 331 : nsGkAtoms::denominator); 332 break; 333 case roles::MATHML_ROOT: 334 aAttributes->SetAttribute( 335 nsGkAtoms::xmlroles, 336 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::root_index); 337 break; 338 case roles::MATHML_SUB: 339 aAttributes->SetAttribute( 340 nsGkAtoms::xmlroles, 341 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::subscript); 342 break; 343 case roles::MATHML_SUP: 344 aAttributes->SetAttribute( 345 nsGkAtoms::xmlroles, 346 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::superscript); 347 break; 348 case roles::MATHML_SUB_SUP: { 349 int32_t index = IndexInParent(); 350 aAttributes->SetAttribute( 351 nsGkAtoms::xmlroles, 352 index == 0 353 ? nsGkAtoms::base 354 : (index == 1 ? nsGkAtoms::subscript : nsGkAtoms::superscript)); 355 } break; 356 case roles::MATHML_UNDER: 357 aAttributes->SetAttribute( 358 nsGkAtoms::xmlroles, 359 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::underscript); 360 break; 361 case roles::MATHML_OVER: 362 aAttributes->SetAttribute( 363 nsGkAtoms::xmlroles, 364 IndexInParent() == 0 ? nsGkAtoms::base : nsGkAtoms::overscript); 365 break; 366 case roles::MATHML_UNDER_OVER: { 367 int32_t index = IndexInParent(); 368 aAttributes->SetAttribute(nsGkAtoms::xmlroles, 369 index == 0 370 ? nsGkAtoms::base 371 : (index == 1 ? nsGkAtoms::underscript 372 : nsGkAtoms::overscript)); 373 } break; 374 case roles::MATHML_MULTISCRIPTS: { 375 // Get the <multiscripts> base. 376 nsIContent* child; 377 bool baseFound = false; 378 for (child = parent->GetContent()->GetFirstChild(); child; 379 child = child->GetNextSibling()) { 380 if (child->IsMathMLElement()) { 381 baseFound = true; 382 break; 383 } 384 } 385 if (baseFound) { 386 nsIContent* content = GetContent(); 387 if (child == content) { 388 // We are the base. 389 aAttributes->SetAttribute(nsGkAtoms::xmlroles, nsGkAtoms::base); 390 } else { 391 // Browse the list of scripts to find us and determine our type. 392 bool postscript = true; 393 bool subscript = true; 394 for (child = child->GetNextSibling(); child; 395 child = child->GetNextSibling()) { 396 if (!child->IsMathMLElement()) continue; 397 if (child->IsMathMLElement(nsGkAtoms::mprescripts)) { 398 postscript = false; 399 subscript = true; 400 continue; 401 } 402 if (child == content) { 403 if (postscript) { 404 aAttributes->SetAttribute(nsGkAtoms::xmlroles, 405 subscript ? nsGkAtoms::subscript 406 : nsGkAtoms::superscript); 407 } else { 408 aAttributes->SetAttribute(nsGkAtoms::xmlroles, 409 subscript 410 ? nsGkAtoms::presubscript 411 : nsGkAtoms::presuperscript); 412 } 413 break; 414 } 415 subscript = !subscript; 416 } 417 } 418 } 419 } break; 420 default: 421 break; 422 } 423 } 424 } 425 426 already_AddRefed<AccAttributes> HyperTextAccessible::NativeAttributes() { 427 RefPtr<AccAttributes> attributes = AccessibleWrap::NativeAttributes(); 428 429 // 'formatting' attribute is deprecated, 'display' attribute should be 430 // instead. 431 nsIFrame* frame = GetFrame(); 432 if (frame && frame->IsBlockFrame()) { 433 attributes->SetAttribute(nsGkAtoms::formatting, nsGkAtoms::block); 434 } 435 436 if (FocusMgr()->IsFocused(this)) { 437 int32_t lineNumber = CaretLineNumber(); 438 if (lineNumber >= 1) { 439 attributes->SetAttribute(nsGkAtoms::lineNumber, lineNumber); 440 } 441 } 442 443 if (HasOwnContent()) { 444 GetAccService()->MarkupAttributes(this, attributes); 445 if (mContent->IsMathMLElement()) SetMathMLXMLRoles(attributes); 446 } 447 448 return attributes.forget(); 449 } 450 451 int32_t HyperTextAccessible::OffsetAtPoint(int32_t aX, int32_t aY, 452 uint32_t aCoordType) { 453 nsIFrame* hyperFrame = GetFrame(); 454 if (!hyperFrame) return -1; 455 456 LayoutDeviceIntPoint coords = 457 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordType, this); 458 459 nsPresContext* presContext = mDoc->PresContext(); 460 nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits( 461 coords, presContext->AppUnitsPerDevPixel()); 462 463 nsRect frameScreenRect = hyperFrame->GetScreenRectInAppUnits(); 464 if (!frameScreenRect.Contains(coordsInAppUnits.x, coordsInAppUnits.y)) { 465 return -1; // Not found 466 } 467 468 nsPoint pointInHyperText(coordsInAppUnits.x - frameScreenRect.X(), 469 coordsInAppUnits.y - frameScreenRect.Y()); 470 471 // Go through the frames to check if each one has the point. 472 // When one does, add up the character offsets until we have a match 473 474 // We have an point in an accessible child of this, now we need to add up the 475 // offsets before it to what we already have 476 int32_t offset = 0; 477 uint32_t childCount = ChildCount(); 478 for (uint32_t childIdx = 0; childIdx < childCount; childIdx++) { 479 LocalAccessible* childAcc = mChildren[childIdx]; 480 481 nsIFrame* primaryFrame = childAcc->GetFrame(); 482 NS_ENSURE_TRUE(primaryFrame, -1); 483 484 nsIFrame* frame = primaryFrame; 485 while (frame) { 486 nsIContent* content = frame->GetContent(); 487 NS_ENSURE_TRUE(content, -1); 488 nsPoint pointInFrame = pointInHyperText - frame->GetOffsetTo(hyperFrame); 489 nsSize frameSize = frame->GetSize(); 490 if (pointInFrame.x < frameSize.width && 491 pointInFrame.y < frameSize.height) { 492 // Finished 493 if (frame->IsTextFrame()) { 494 nsIFrame::ContentOffsets contentOffsets = 495 frame->GetContentOffsetsFromPointExternal( 496 pointInFrame, nsIFrame::IGNORE_SELECTION_STYLE); 497 if (contentOffsets.IsNull() || contentOffsets.content != content) { 498 return -1; // Not found 499 } 500 uint32_t addToOffset; 501 nsresult rv = ContentToRenderedOffset( 502 primaryFrame, contentOffsets.offset, &addToOffset); 503 NS_ENSURE_SUCCESS(rv, -1); 504 offset += addToOffset; 505 } 506 return offset; 507 } 508 frame = frame->GetNextContinuation(); 509 } 510 511 offset += nsAccUtils::TextLength(childAcc); 512 } 513 514 return -1; // Not found 515 } 516 517 already_AddRefed<EditorBase> HyperTextAccessible::GetEditor() const { 518 if (!mContent->HasFlag(NODE_IS_EDITABLE)) { 519 // If we're inside an editable container, then return that container's 520 // editor 521 LocalAccessible* ancestor = LocalParent(); 522 while (ancestor) { 523 HyperTextAccessible* hyperText = ancestor->AsHyperText(); 524 if (hyperText) { 525 // Recursion will stop at container doc because it has its own impl 526 // of GetEditor() 527 return hyperText->GetEditor(); 528 } 529 530 ancestor = ancestor->LocalParent(); 531 } 532 533 return nullptr; 534 } 535 536 nsCOMPtr<nsIDocShell> docShell = nsCoreUtils::GetDocShellFor(mContent); 537 nsCOMPtr<nsIEditingSession> editingSession; 538 docShell->GetEditingSession(getter_AddRefs(editingSession)); 539 if (!editingSession) return nullptr; // No editing session interface 540 541 dom::Document* docNode = mDoc->DocumentNode(); 542 RefPtr<HTMLEditor> htmlEditor = 543 editingSession->GetHTMLEditorForWindow(docNode->GetWindow()); 544 return htmlEditor.forget(); 545 } 546 547 /** 548 * =================== Caret & Selection ====================== 549 */ 550 551 int32_t HyperTextAccessible::CaretOffset() const { 552 // Not focused focusable accessible except document accessible doesn't have 553 // a caret. 554 if (!IsDoc() && !FocusMgr()->IsFocused(this) && 555 (InteractiveState() & states::FOCUSABLE)) { 556 return -1; 557 } 558 559 // Check cached value. 560 int32_t caretOffset = -1; 561 HyperTextAccessible* text = SelectionMgr()->AccessibleWithCaret(&caretOffset); 562 563 // Use cached value if it corresponds to this accessible. 564 if (caretOffset != -1) { 565 if (text == this) return caretOffset; 566 567 nsINode* textNode = text->GetNode(); 568 // Ignore offset if cached accessible isn't a text leaf. 569 if (nsCoreUtils::IsAncestorOf(GetNode(), textNode)) { 570 return TransformOffset(text, textNode->IsText() ? caretOffset : 0, false); 571 } 572 } 573 574 // No caret if the focused node is not inside this DOM node and this DOM node 575 // is not inside of focused node. 576 FocusManager::FocusDisposition focusDisp = 577 FocusMgr()->IsInOrContainsFocus(this); 578 if (focusDisp == FocusManager::eNone) return -1; 579 580 // Turn the focus node and offset of the selection into caret hypretext 581 // offset. 582 dom::Selection* domSel = DOMSelection(); 583 NS_ENSURE_TRUE(domSel, -1); 584 585 nsINode* focusNode = domSel->GetFocusNode(); 586 uint32_t focusOffset = domSel->FocusOffset(); 587 588 // No caret if this DOM node is inside of focused node but the selection's 589 // focus point is not inside of this DOM node. 590 if (focusDisp == FocusManager::eContainedByFocus) { 591 nsINode* resultNode = 592 nsCoreUtils::GetDOMNodeFromDOMPoint(focusNode, focusOffset); 593 594 nsINode* thisNode = GetNode(); 595 if (resultNode != thisNode && 596 !nsCoreUtils::IsAncestorOf(thisNode, resultNode)) { 597 return -1; 598 } 599 } 600 601 return DOMPointToOffset(focusNode, focusOffset); 602 } 603 604 std::pair<LayoutDeviceIntRect, nsIWidget*> HyperTextAccessible::GetCaretRect() { 605 RefPtr<nsCaret> caret = mDoc->PresShellPtr()->GetCaret(); 606 NS_ENSURE_TRUE(caret, {}); 607 608 bool isVisible = caret->IsVisible(); 609 if (!isVisible) { 610 return {}; 611 } 612 613 nsRect rect; 614 nsIFrame* frame = caret->GetGeometry(&rect); 615 if (!frame || rect.IsEmpty()) { 616 return {}; 617 } 618 619 PresShell* presShell = mDoc->PresShellPtr(); 620 // Transform rect to be relative to the root frame. 621 nsIFrame* rootFrame = presShell->GetRootFrame(); 622 rect = nsLayoutUtils::TransformFrameRectToAncestor(frame, rect, rootFrame); 623 // We need to inverse translate with the offset of the edge of the visual 624 // viewport from top edge of the layout viewport. 625 nsPoint viewportOffset = presShell->GetVisualViewportOffset() - 626 presShell->GetLayoutViewportOffset(); 627 rect.MoveBy(-viewportOffset); 628 // We need to take into account a non-1 resolution set on the presshell. 629 // This happens with async pinch zooming. Here we scale the bounds before 630 // adding the screen-relative offset. 631 rect.ScaleRoundOut(presShell->GetResolution()); 632 // Now we need to put the rect in absolute screen coords. 633 nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits(); 634 rect.MoveBy(rootScreenRect.TopLeft()); 635 // Finally, convert from app units. 636 auto caretRect = LayoutDeviceIntRect::FromAppUnitsToNearest( 637 rect, presShell->GetPresContext()->AppUnitsPerDevPixel()); 638 639 // Correct for character size, so that caret always matches the size of 640 // the character. This is important for font size transitions, and is 641 // necessary because the Gecko caret uses the previous character's size as 642 // the user moves forward in the text by character. 643 int32_t caretOffset = CaretOffset(); 644 if (NS_WARN_IF(caretOffset == -1)) { 645 // The caret offset will be -1 if this Accessible isn't focused. Note that 646 // the DOM node contaning the caret might be focused, but the Accessible 647 // might not be; e.g. due to an autocomplete popup suggestion having a11y 648 // focus. 649 return {}; 650 } 651 652 // Vertically align the caret to the top of the line. 653 TextLeafPoint caretPoint = TextLeafPoint::GetCaret(this); 654 if (caretPoint.mIsEndOfLineInsertionPoint) { 655 caretPoint = 656 caretPoint.FindBoundary(nsIAccessibleText::BOUNDARY_CHAR, eDirPrevious); 657 } 658 659 LayoutDeviceIntRect charRect = caretPoint.CharBounds(); 660 if (!charRect.IsEmpty()) { 661 caretRect.SetTopEdge(charRect.Y()); 662 } 663 664 return {caretRect, frame->GetNearestWidget()}; 665 } 666 667 bool HyperTextAccessible::RemoveFromSelection(int32_t aSelectionNum) { 668 RefPtr<dom::Selection> domSel = DOMSelection(); 669 if (!domSel) return false; 670 671 if (aSelectionNum == TextLeafRange::kRemoveAllExistingSelectedRanges) { 672 domSel->RemoveAllRanges(IgnoreErrors()); 673 return true; 674 } 675 676 if (aSelectionNum < 0 || 677 aSelectionNum >= static_cast<int32_t>(domSel->RangeCount())) { 678 return false; 679 } 680 681 const RefPtr<nsRange> range{ 682 domSel->GetRangeAt(static_cast<uint32_t>(aSelectionNum))}; 683 domSel->RemoveRangeAndUnselectFramesAndNotifyListeners(*range, 684 IgnoreErrors()); 685 return true; 686 } 687 688 void HyperTextAccessible::ScrollSubstringToPoint(int32_t aStartOffset, 689 int32_t aEndOffset, 690 uint32_t aCoordinateType, 691 int32_t aX, int32_t aY) { 692 nsIFrame* frame = GetFrame(); 693 if (!frame) return; 694 695 LayoutDeviceIntPoint coords = 696 nsAccUtils::ConvertToScreenCoords(aX, aY, aCoordinateType, this); 697 698 RefPtr<nsRange> domRange = nsRange::Create(mContent); 699 TextRange range(this, this, aStartOffset, this, aEndOffset); 700 if (!range.AssignDOMRange(domRange)) { 701 return; 702 } 703 704 nsPresContext* presContext = frame->PresContext(); 705 nsPoint coordsInAppUnits = LayoutDeviceIntPoint::ToAppUnits( 706 coords, presContext->AppUnitsPerDevPixel()); 707 708 bool initialScrolled = false; 709 nsIFrame* parentFrame = frame; 710 while ((parentFrame = parentFrame->GetParent())) { 711 if (parentFrame->IsScrollContainerOrSubclass()) { 712 if (!initialScrolled) { 713 // Scroll substring to the given point. Turn the point into percents 714 // relative scrollable area to use nsCoreUtils::ScrollSubstringTo. 715 nsRect frameRect = parentFrame->GetScreenRectInAppUnits(); 716 nscoord offsetPointX = coordsInAppUnits.x - frameRect.X(); 717 nscoord offsetPointY = coordsInAppUnits.y - frameRect.Y(); 718 719 nsSize size(parentFrame->GetSize()); 720 721 // avoid divide by zero 722 size.width = size.width ? size.width : 1; 723 size.height = size.height ? size.height : 1; 724 725 int16_t hPercent = offsetPointX * 100 / size.width; 726 int16_t vPercent = offsetPointY * 100 / size.height; 727 728 nsresult rv = nsCoreUtils::ScrollSubstringTo( 729 frame, domRange, 730 ScrollAxis(WhereToScroll(vPercent), WhenToScroll::Always), 731 ScrollAxis(WhereToScroll(hPercent), WhenToScroll::Always)); 732 if (NS_FAILED(rv)) return; 733 734 initialScrolled = true; 735 } else { 736 // Substring was scrolled to the given point already inside its closest 737 // scrollable area. If there are nested scrollable areas then make 738 // sure we scroll lower areas to the given point inside currently 739 // traversed scrollable area. 740 nsCoreUtils::ScrollFrameToPoint(parentFrame, frame, coords); 741 } 742 } 743 frame = parentFrame; 744 } 745 } 746 747 void HyperTextAccessible::SelectionRanges( 748 nsTArray<a11y::TextRange>* aRanges) const { 749 if (IsDoc() && !AsDoc()->HasLoadState(DocAccessible::eTreeConstructed)) { 750 // Rarely, a client query can be handled after a DocAccessible is created 751 // but before the initial tree is constructed, since DoInitialUpdate happens 752 // during a refresh tick. In that case, there might be a DOM selection, but 753 // we can't use it. We will crash if we try due to mContent being null, etc. 754 // This should only happen in the parent process because we should never 755 // try to push the cache in a content process before the initial tree is 756 // constructed. 757 MOZ_ASSERT(XRE_IsParentProcess(), "Query before DoInitialUpdate"); 758 return; 759 } 760 // Ignore selection if it is not visible. 761 RefPtr<nsFrameSelection> frameSelection = FrameSelection(); 762 if (!frameSelection || frameSelection->GetDisplaySelection() <= 763 nsISelectionController::SELECTION_HIDDEN) { 764 return; 765 } 766 dom::Selection* sel = &frameSelection->NormalSelection(); 767 TextRange::TextRangesFromSelection(sel, aRanges); 768 } 769 770 void HyperTextAccessible::ReplaceText(const nsAString& aText) { 771 if (aText.Length() == 0) { 772 DeleteText(0, CharacterCount()); 773 return; 774 } 775 776 SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges, 0, 777 CharacterCount()); 778 779 RefPtr<EditorBase> editorBase = GetEditor(); 780 if (!editorBase) { 781 return; 782 } 783 784 DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText); 785 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text"); 786 } 787 788 void HyperTextAccessible::InsertText(const nsAString& aText, 789 int32_t aPosition) { 790 RefPtr<EditorBase> editorBase = GetEditor(); 791 if (editorBase) { 792 SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges, 793 aPosition, aPosition); 794 DebugOnly<nsresult> rv = editorBase->InsertTextAsAction(aText); 795 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the text"); 796 } 797 } 798 799 void HyperTextAccessible::CopyText(int32_t aStartPos, int32_t aEndPos) { 800 RefPtr<EditorBase> editorBase = GetEditor(); 801 if (editorBase) { 802 SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges, 803 aStartPos, aEndPos); 804 editorBase->Copy(); 805 } 806 } 807 808 void HyperTextAccessible::CutText(int32_t aStartPos, int32_t aEndPos) { 809 RefPtr<EditorBase> editorBase = GetEditor(); 810 if (editorBase) { 811 SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges, 812 aStartPos, aEndPos); 813 editorBase->Cut(); 814 } 815 } 816 817 void HyperTextAccessible::DeleteText(int32_t aStartPos, int32_t aEndPos) { 818 RefPtr<EditorBase> editorBase = GetEditor(); 819 if (!editorBase) { 820 return; 821 } 822 SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges, 823 aStartPos, aEndPos); 824 DebugOnly<nsresult> rv = 825 editorBase->DeleteSelectionAsAction(nsIEditor::eNone, nsIEditor::eStrip); 826 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to delete text"); 827 } 828 829 void HyperTextAccessible::PasteText(int32_t aPosition) { 830 RefPtr<EditorBase> editorBase = GetEditor(); 831 if (editorBase) { 832 // If the caller wants to paste at the caret, we don't need to set the 833 // selection. If there is text already selected, this also allows the caller 834 // to replace it, just as would happen when pasting using the keyboard or 835 // GUI. 836 if (aPosition != nsIAccessibleText::TEXT_OFFSET_CARET) { 837 SetSelectionBoundsAt(TextLeafRange::kRemoveAllExistingSelectedRanges, 838 aPosition, aPosition); 839 } 840 editorBase->PasteAsAction(nsIClipboard::kGlobalClipboard, 841 EditorBase::DispatchPasteEvent::Yes); 842 } 843 } 844 845 //////////////////////////////////////////////////////////////////////////////// 846 // LocalAccessible public 847 848 // LocalAccessible protected 849 ENameValueFlag HyperTextAccessible::NativeName(nsString& aName) const { 850 // Check @alt attribute for invalid img elements. 851 if (mContent->IsHTMLElement(nsGkAtoms::img)) { 852 mContent->AsElement()->GetAttr(nsGkAtoms::alt, aName); 853 if (!aName.IsEmpty()) return eNameOK; 854 } 855 856 return AccessibleWrap::NativeName(aName); 857 } 858 859 void HyperTextAccessible::Shutdown() { 860 mOffsets.Clear(); 861 AccessibleWrap::Shutdown(); 862 } 863 864 bool HyperTextAccessible::RemoveChild(LocalAccessible* aAccessible) { 865 const int32_t childIndex = aAccessible->IndexInParent(); 866 if (childIndex < static_cast<int32_t>(mOffsets.Length())) { 867 mOffsets.RemoveLastElements(mOffsets.Length() - childIndex); 868 } 869 870 return AccessibleWrap::RemoveChild(aAccessible); 871 } 872 873 bool HyperTextAccessible::InsertChildAt(uint32_t aIndex, 874 LocalAccessible* aChild) { 875 if (aIndex < mOffsets.Length()) { 876 mOffsets.RemoveLastElements(mOffsets.Length() - aIndex); 877 } 878 879 return AccessibleWrap::InsertChildAt(aIndex, aChild); 880 } 881 882 void HyperTextAccessible::RelocateChild(uint32_t aNewIndex, 883 LocalAccessible* aChild) { 884 const int32_t smallestChildIndex = 885 std::min(aChild->IndexInParent(), static_cast<int32_t>(aNewIndex)); 886 if (smallestChildIndex < static_cast<int32_t>(mOffsets.Length())) { 887 mOffsets.RemoveLastElements(mOffsets.Length() - smallestChildIndex); 888 } 889 AccessibleWrap::RelocateChild(aNewIndex, aChild); 890 } 891 892 Relation HyperTextAccessible::RelationByType(RelationType aType) const { 893 Relation rel = LocalAccessible::RelationByType(aType); 894 895 switch (aType) { 896 case RelationType::NODE_CHILD_OF: 897 if (HasOwnContent() && mContent->IsMathMLElement()) { 898 LocalAccessible* parent = LocalParent(); 899 if (parent) { 900 nsIContent* parentContent = parent->GetContent(); 901 if (parentContent && 902 parentContent->IsMathMLElement(nsGkAtoms::mroot)) { 903 // Add a relation pointing to the parent <mroot>. 904 rel.AppendTarget(parent); 905 } 906 } 907 } 908 break; 909 case RelationType::NODE_PARENT_OF: 910 if (HasOwnContent() && mContent->IsMathMLElement(nsGkAtoms::mroot)) { 911 LocalAccessible* base = LocalChildAt(0); 912 LocalAccessible* index = LocalChildAt(1); 913 if (base && index) { 914 // Append the <mroot> children in the order index, base. 915 rel.AppendTarget(index); 916 rel.AppendTarget(base); 917 } 918 } 919 break; 920 default: 921 break; 922 } 923 924 return rel; 925 } 926 927 //////////////////////////////////////////////////////////////////////////////// 928 // HyperTextAccessible public static 929 930 nsresult HyperTextAccessible::ContentToRenderedOffset( 931 nsIFrame* aFrame, int32_t aContentOffset, uint32_t* aRenderedOffset) const { 932 if (!aFrame) { 933 // Current frame not rendered -- this can happen if text is set on 934 // something with display: none 935 *aRenderedOffset = 0; 936 return NS_OK; 937 } 938 939 if (IsTextField()) { 940 *aRenderedOffset = aContentOffset; 941 return NS_OK; 942 } 943 944 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion"); 945 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr, 946 "Call on primary frame only"); 947 948 nsIFrame::RenderedText text = 949 aFrame->GetRenderedText(aContentOffset, aContentOffset + 1, 950 nsIFrame::TextOffsetType::OffsetsInContentText, 951 nsIFrame::TrailingWhitespace::DontTrim); 952 *aRenderedOffset = text.mOffsetWithinNodeRenderedText; 953 954 return NS_OK; 955 } 956 957 nsresult HyperTextAccessible::RenderedToContentOffset( 958 nsIFrame* aFrame, uint32_t aRenderedOffset, int32_t* aContentOffset) const { 959 if (IsTextField()) { 960 *aContentOffset = aRenderedOffset; 961 return NS_OK; 962 } 963 964 *aContentOffset = 0; 965 NS_ENSURE_TRUE(aFrame, NS_ERROR_FAILURE); 966 967 NS_ASSERTION(aFrame->IsTextFrame(), "Need text frame for offset conversion"); 968 NS_ASSERTION(aFrame->GetPrevContinuation() == nullptr, 969 "Call on primary frame only"); 970 971 nsIFrame::RenderedText text = 972 aFrame->GetRenderedText(aRenderedOffset, aRenderedOffset + 1, 973 nsIFrame::TextOffsetType::OffsetsInRenderedText, 974 nsIFrame::TrailingWhitespace::DontTrim); 975 *aContentOffset = text.mOffsetWithinNodeText; 976 977 return NS_OK; 978 }