nsFrameSelection.cpp (118983B)
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */ 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 /* 8 * Implementation of nsFrameSelection 9 */ 10 11 #include "nsFrameSelection.h" 12 13 #include <algorithm> 14 15 #include "ErrorList.h" 16 #include "mozilla/Attributes.h" 17 #include "mozilla/AutoRestore.h" 18 #include "mozilla/BasePrincipal.h" 19 #include "mozilla/HTMLEditor.h" 20 #include "mozilla/IntegerRange.h" 21 #include "mozilla/Logging.h" 22 #include "mozilla/MouseEvents.h" 23 #include "mozilla/Preferences.h" 24 #include "mozilla/PresShell.h" 25 #include "mozilla/PseudoStyleType.h" 26 #include "mozilla/ScrollContainerFrame.h" 27 #include "mozilla/ScrollTypes.h" 28 #include "mozilla/StaticAnalysisFunctions.h" 29 #include "mozilla/StaticPrefs_bidi.h" 30 #include "mozilla/StaticPrefs_dom.h" 31 #include "mozilla/StaticPrefs_layout.h" 32 #include "mozilla/TextEvents.h" 33 #include "mozilla/intl/BidiEmbeddingLevel.h" 34 #include "nsBidiPresUtils.h" 35 #include "nsCCUncollectableMarker.h" 36 #include "nsCOMPtr.h" 37 #include "nsCSSFrameConstructor.h" 38 #include "nsCaret.h" 39 #include "nsContentUtils.h" 40 #include "nsDebug.h" 41 #include "nsDeviceContext.h" 42 #include "nsFrameTraversal.h" 43 #include "nsGkAtoms.h" 44 #include "nsIContent.h" 45 #include "nsISelectionListener.h" 46 #include "nsITableCellLayout.h" 47 #include "nsLayoutUtils.h" 48 #include "nsPresContext.h" 49 #include "nsRange.h" 50 #include "nsString.h" 51 #include "nsTArray.h" 52 #include "nsTableCellFrame.h" 53 #include "nsTableWrapperFrame.h" 54 #include "nsTextFrame.h" 55 #include "nsThreadUtils.h" 56 57 // notifications 58 #include "SelectionMovementUtils.h" 59 #include "mozilla/AsyncEventDispatcher.h" 60 #include "mozilla/AutoCopyListener.h" 61 #include "mozilla/ErrorResult.h" 62 #include "mozilla/dom/AncestorIterator.h" 63 #include "mozilla/dom/Document.h" 64 #include "mozilla/dom/Element.h" 65 #include "mozilla/dom/Highlight.h" 66 #include "mozilla/dom/Selection.h" 67 #include "mozilla/dom/SelectionBinding.h" 68 #include "mozilla/dom/ShadowRoot.h" 69 #include "mozilla/dom/StaticRange.h" 70 #include "mozilla/dom/Text.h" 71 #include "nsCopySupport.h" 72 #include "nsError.h" 73 #include "nsFocusManager.h" 74 #include "nsIClipboard.h" 75 #include "nsIFrameInlines.h" 76 #include "nsISelectionController.h" //for the enums 77 #include "nsPIDOMWindow.h" 78 79 using namespace mozilla; 80 using namespace mozilla::dom; 81 82 static LazyLogModule sFrameSelectionLog("FrameSelection"); 83 84 std::ostream& operator<<(std::ostream& aStream, 85 const nsFrameSelection& aFrameSelection) { 86 return aStream << "{ mPresShell=" << aFrameSelection.mPresShell 87 << ", mLimiters={ mIndependentSelectionRootElement=" 88 << aFrameSelection.mLimiters.mIndependentSelectionRootElement 89 << ", mAncestorLimiter=" 90 << aFrameSelection.mLimiters.mAncestorLimiter 91 << "}, IsBatching()=" << std::boolalpha 92 << aFrameSelection.IsBatching() 93 << ", IsInTableSelectionMode()=" << std::boolalpha 94 << aFrameSelection.IsInTableSelectionMode() 95 << ", GetDragState()=" << std::boolalpha 96 << aFrameSelection.GetDragState() 97 << ", HighlightSelectionCount()=" 98 << aFrameSelection.HighlightSelectionCount() << " }"; 99 } 100 101 namespace mozilla { 102 extern LazyLogModule sSelectionAPILog; 103 extern void LogStackForSelectionAPI(); 104 105 static void LogSelectionAPI(const dom::Selection* aSelection, 106 const char* aFuncName, const char* aArgName, 107 const nsIContent* aContent) { 108 MOZ_LOG(sSelectionAPILog, LogLevel::Info, 109 ("%p nsFrameSelection::%s(%s=%s)", aSelection, aFuncName, aArgName, 110 aContent ? ToString(*aContent).c_str() : "<nullptr>")); 111 } 112 } // namespace mozilla 113 114 // #define DEBUG_TABLE 1 115 116 /** 117 * Add cells to the selection inside of the given cells range. 118 * 119 * @param aTable [in] HTML table element 120 * @param aStartRowIndex [in] row index where the cells range starts 121 * @param aStartColumnIndex [in] column index where the cells range starts 122 * @param aEndRowIndex [in] row index where the cells range ends 123 * @param aEndColumnIndex [in] column index where the cells range ends 124 */ 125 static nsresult AddCellsToSelection(const nsIContent* aTableContent, 126 int32_t aStartRowIndex, 127 int32_t aStartColumnIndex, 128 int32_t aEndRowIndex, 129 int32_t aEndColumnIndex, 130 Selection& aNormalSelection); 131 132 static nsAtom* GetTag(nsINode* aNode); 133 134 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode); 135 MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult CreateAndAddRange( 136 nsINode* aContainer, int32_t aOffset, Selection& aNormalSelection); 137 static nsresult SelectCellElement(nsIContent* aCellElement, 138 Selection& aNormalSelection); 139 140 #ifdef XP_MACOSX 141 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel); 142 #endif // XP_MACOSX 143 144 #ifdef PRINT_RANGE 145 static void printRange(nsRange* aDomRange); 146 # define DEBUG_OUT_RANGE(x) printRange(x) 147 #else 148 # define DEBUG_OUT_RANGE(x) 149 #endif // PRINT_RANGE 150 151 /****************************************************************************** 152 * mozilla::PeekOffsetStruct 153 ******************************************************************************/ 154 155 // #define DEBUG_SELECTION // uncomment for printf describing every collapse and 156 // extend. #define DEBUG_NAVIGATION 157 158 // #define DEBUG_TABLE_SELECTION 1 159 160 namespace mozilla { 161 162 PeekOffsetStruct::PeekOffsetStruct( 163 nsSelectionAmount aAmount, nsDirection aDirection, int32_t aStartOffset, 164 nsPoint aDesiredCaretPos, const PeekOffsetOptions aOptions, 165 EWordMovementType aWordMovementType /* = eDefaultBehavior */, 166 const Element* aAncestorLimiter /* = nullptr */) 167 : mAmount(aAmount), 168 mDirection(aDirection), 169 mStartOffset(aStartOffset), 170 mDesiredCaretPos(aDesiredCaretPos), 171 mWordMovementType(aWordMovementType), 172 mOptions(aOptions), 173 mAncestorLimiter(aAncestorLimiter), 174 mResultFrame(nullptr), 175 mContentOffset(0), 176 mAttach(CaretAssociationHint::Before) {} 177 178 } // namespace mozilla 179 180 // Array which contains index of each SelectionType in 181 // Selection::mDOMSelections. For avoiding using if nor switch to retrieve the 182 // index, this needs to have -1 for SelectionTypes which won't be created its 183 // Selection instance. 184 static const int8_t kIndexOfSelections[] = { 185 -1, // SelectionType::eInvalid 186 -1, // SelectionType::eNone 187 0, // SelectionType::eNormal 188 1, // SelectionType::eSpellCheck 189 2, // SelectionType::eIMERawClause 190 3, // SelectionType::eIMESelectedRawClause 191 4, // SelectionType::eIMEConvertedClause 192 5, // SelectionType::eIMESelectedClause 193 6, // SelectionType::eAccessibility 194 7, // SelectionType::eFind 195 8, // SelectionType::eURLSecondary 196 9, // SelectionType::eURLStrikeout 197 10, // SelectionType::eTargetText 198 -1, // SelectionType::eHighlight 199 }; 200 201 inline int8_t GetIndexFromSelectionType(SelectionType aSelectionType) { 202 // The enum value of eInvalid is -1 and the others are sequential value 203 // starting from 0. Therefore, |SelectionType + 1| is the index of 204 // kIndexOfSelections. 205 return kIndexOfSelections[static_cast<int8_t>(aSelectionType) + 1]; 206 } 207 208 /* 209 The limiter is used specifically for the text areas and textfields 210 In that case it is the DIV tag that is anonymously created for the text 211 areas/fields. Text nodes and BR nodes fall beneath it. In the case of a 212 BR node the limiter will be the parent and the offset will point before or 213 after the BR node. In the case of the text node the parent content is 214 the text node itself and the offset will be the exact character position. 215 The offset is not important to check for validity. Simply look at the 216 passed in content. If it equals the limiter then the selection point is valid. 217 If its parent it the limiter then the point is also valid. In the case of 218 NO limiter all points are valid since you are in a topmost iframe. (browser 219 or composer) 220 */ 221 bool nsFrameSelection::NodeIsInLimiters(const nsINode* aContainerNode) const { 222 return NodeIsInLimiters(aContainerNode, GetIndependentSelectionRootElement(), 223 GetAncestorLimiter()); 224 } 225 226 // static 227 bool nsFrameSelection::NodeIsInLimiters( 228 const nsINode* aContainerNode, 229 const Element* aIndependentSelectionLimiterElement, 230 const Element* aSelectionAncestorLimiter) { 231 if (!aContainerNode) { 232 return false; 233 } 234 235 // If there is a selection limiter, it must be the anonymous <div> of a text 236 // control. The <div> should have only one Text and/or a <br>. Therefore, 237 // when it's non-nullptr, selection range containers must be the container or 238 // the Text in it. 239 if (aIndependentSelectionLimiterElement) { 240 MOZ_ASSERT(aIndependentSelectionLimiterElement->GetPseudoElementType() == 241 PseudoStyleType::mozTextControlEditingRoot); 242 MOZ_ASSERT( 243 aIndependentSelectionLimiterElement->IsHTMLElement(nsGkAtoms::div)); 244 if (aIndependentSelectionLimiterElement == aContainerNode) { 245 return true; 246 } 247 if (aIndependentSelectionLimiterElement == aContainerNode->GetParent()) { 248 NS_WARNING_ASSERTION(aContainerNode->IsText(), 249 ToString(*aContainerNode).c_str()); 250 MOZ_ASSERT(aContainerNode->IsText()); 251 return true; 252 } 253 return false; 254 } 255 256 // XXX We might need to return `false` if aContainerNode is in a native 257 // anonymous subtree, but doing it will make it impossible to select the 258 // anonymous subtree text in <details>. 259 return !aSelectionAncestorLimiter || 260 aContainerNode->IsInclusiveDescendantOf(aSelectionAncestorLimiter); 261 } 262 263 namespace mozilla { 264 struct MOZ_RAII AutoPrepareFocusRange { 265 AutoPrepareFocusRange(Selection* aSelection, 266 const bool aMultiRangeSelection) { 267 MOZ_ASSERT(aSelection); 268 MOZ_ASSERT(aSelection->GetType() == SelectionType::eNormal); 269 270 if (aSelection->mStyledRanges.mRanges.Length() <= 1) { 271 return; 272 } 273 274 if (aSelection->mFrameSelection->IsUserSelectionReason()) { 275 mUserSelect.emplace(aSelection); 276 } 277 278 nsTArray<StyledRange>& ranges = aSelection->mStyledRanges.mRanges; 279 if (!aSelection->mUserInitiated || aMultiRangeSelection) { 280 // Scripted command or the user is starting a new explicit multi-range 281 // selection. 282 for (StyledRange& entry : ranges) { 283 MOZ_ASSERT(entry.mRange->IsDynamicRange()); 284 entry.mRange->AsDynamicRange()->SetIsGenerated(false); 285 } 286 return; 287 } 288 289 if (!IsAnchorRelativeOperation( 290 aSelection->mFrameSelection->mSelectionChangeReasons)) { 291 return; 292 } 293 294 // This operation is against the anchor but our current mAnchorFocusRange 295 // represents the focus in a multi-range selection. The anchor from a user 296 // perspective is the most distant generated range on the opposite side. 297 // Find that range and make it the mAnchorFocusRange. 298 nsRange* const newAnchorFocusRange = 299 FindGeneratedRangeMostDistantFromAnchor(*aSelection); 300 301 if (!newAnchorFocusRange) { 302 // There are no generated ranges - that's fine. 303 return; 304 } 305 306 // Setup the new mAnchorFocusRange and mark the old one as generated. 307 if (aSelection->mAnchorFocusRange) { 308 aSelection->mAnchorFocusRange->SetIsGenerated(true); 309 } 310 311 newAnchorFocusRange->SetIsGenerated(false); 312 aSelection->mAnchorFocusRange = newAnchorFocusRange; 313 314 RemoveGeneratedRanges(*aSelection); 315 316 if (aSelection->mFrameSelection) { 317 aSelection->mFrameSelection->InvalidateDesiredCaretPos(); 318 } 319 } 320 321 private: 322 static nsRange* FindGeneratedRangeMostDistantFromAnchor( 323 const Selection& aSelection) { 324 const nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges; 325 const size_t len = ranges.Length(); 326 nsRange* result{nullptr}; 327 if (aSelection.GetDirection() == eDirNext) { 328 for (size_t i = 0; i < len; ++i) { 329 // This function is only called for selections with type == eNormal. 330 // (see MOZ_ASSERT in constructor). 331 // Therefore, all ranges must be dynamic. 332 if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) { 333 result = ranges[i].mRange->AsDynamicRange(); 334 break; 335 } 336 } 337 } else { 338 size_t i = len; 339 while (i--) { 340 if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) { 341 result = ranges[i].mRange->AsDynamicRange(); 342 break; 343 } 344 } 345 } 346 347 return result; 348 } 349 350 static void RemoveGeneratedRanges(Selection& aSelection) { 351 RefPtr<nsPresContext> presContext = aSelection.GetPresContext(); 352 nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges; 353 size_t i = ranges.Length(); 354 while (i--) { 355 // This function is only called for selections with type == eNormal. 356 // (see MOZ_ASSERT in constructor). 357 // Therefore, all ranges must be dynamic. 358 if (!ranges[i].mRange->IsDynamicRange()) { 359 continue; 360 } 361 nsRange* range = ranges[i].mRange->AsDynamicRange(); 362 if (range->IsGenerated()) { 363 range->UnregisterSelection(aSelection); 364 aSelection.SelectFrames(presContext, *range, false); 365 ranges.RemoveElementAt(i); 366 } 367 } 368 } 369 370 /** 371 * @aParam aSelectionChangeReasons can be multiple of the reasons defined in 372 nsISelectionListener.idl. 373 */ 374 static bool IsAnchorRelativeOperation(const int16_t aSelectionChangeReasons) { 375 return aSelectionChangeReasons & 376 (nsISelectionListener::DRAG_REASON | 377 nsISelectionListener::MOUSEDOWN_REASON | 378 nsISelectionListener::MOUSEUP_REASON | 379 nsISelectionListener::COLLAPSETOSTART_REASON); 380 } 381 382 Maybe<Selection::AutoUserInitiated> mUserSelect; 383 }; 384 385 } // namespace mozilla 386 387 ////////////BEGIN nsFrameSelection methods 388 389 template Result<RefPtr<nsRange>, nsresult> 390 nsFrameSelection::CreateRangeExtendedToSomewhere( 391 PresShell& aPresShell, 392 const mozilla::LimitersAndCaretData& aLimitersAndCaretData, 393 const AbstractRange& aRange, nsDirection aRangeDirection, 394 nsDirection aExtendDirection, nsSelectionAmount aAmount, 395 CaretMovementStyle aMovementStyle); 396 template Result<RefPtr<StaticRange>, nsresult> 397 nsFrameSelection::CreateRangeExtendedToSomewhere( 398 PresShell& aPresShell, 399 const mozilla::LimitersAndCaretData& aLimitersAndCaretData, 400 const AbstractRange& aRange, nsDirection aRangeDirection, 401 nsDirection aExtendDirection, nsSelectionAmount aAmount, 402 CaretMovementStyle aMovementStyle); 403 404 nsFrameSelection::nsFrameSelection( 405 PresShell* aPresShell, const bool aAccessibleCaretEnabled, 406 Element* aEditorRootAnonymousDiv /* = nullptr */) { 407 for (size_t i = 0; i < std::size(mDomSelections); i++) { 408 mDomSelections[i] = new Selection(kPresentSelectionTypes[i], this); 409 } 410 411 Selection& sel = NormalSelection(); 412 if (AutoCopyListener::IsEnabled()) { 413 sel.NotifyAutoCopy(); 414 } 415 416 mPresShell = aPresShell; 417 mDragState = false; 418 419 MOZ_ASSERT_IF(aEditorRootAnonymousDiv, 420 aEditorRootAnonymousDiv->GetPseudoElementType() == 421 PseudoStyleType::mozTextControlEditingRoot); 422 MOZ_ASSERT_IF(aEditorRootAnonymousDiv, 423 aEditorRootAnonymousDiv->IsHTMLElement(nsGkAtoms::div)); 424 mLimiters.mIndependentSelectionRootElement = aEditorRootAnonymousDiv; 425 426 // This should only ever be initialized on the main thread, so we are OK here. 427 MOZ_ASSERT(NS_IsMainThread()); 428 429 mAccessibleCaretEnabled = aAccessibleCaretEnabled; 430 if (mAccessibleCaretEnabled) { 431 sel.MaybeNotifyAccessibleCaretEventHub(aPresShell); 432 } 433 434 sel.EnableSelectionChangeEvent(); 435 } 436 437 nsFrameSelection::~nsFrameSelection() = default; 438 439 NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection) 440 441 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection) 442 for (size_t i = 0; i < std::size(tmp->mDomSelections); ++i) { 443 tmp->mDomSelections[i] = nullptr; 444 } 445 tmp->mHighlightSelections.Clear(); 446 447 NS_IMPL_CYCLE_COLLECTION_UNLINK( 448 mTableSelection.mClosestInclusiveTableCellAncestor) 449 tmp->mTableSelection.mMode = TableSelectionMode::None; 450 tmp->mTableSelection.mDragSelectingCells = false; 451 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mStartSelectedCell) 452 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mEndSelectedCell) 453 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mAppendStartSelectedCell) 454 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mUnselectCellOnMouseUp) 455 NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainedRange.mRange) 456 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mIndependentSelectionRootElement) 457 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mAncestorLimiter) 458 NS_IMPL_CYCLE_COLLECTION_UNLINK_END 459 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection) 460 if (tmp->mPresShell && tmp->mPresShell->GetDocument() && 461 nsCCUncollectableMarker::InGeneration( 462 cb, tmp->mPresShell->GetDocument()->GetMarkedCCGeneration())) { 463 return NS_SUCCESS_INTERRUPTED_TRAVERSE; 464 } 465 for (size_t i = 0; i < std::size(tmp->mDomSelections); ++i) { 466 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i]) 467 } 468 469 for (const auto& value : tmp->mHighlightSelections) { 470 CycleCollectionNoteChild(cb, value.second().get(), 471 "mHighlightSelections[]"); 472 } 473 474 NS_IMPL_CYCLE_COLLECTION_TRAVERSE( 475 mTableSelection.mClosestInclusiveTableCellAncestor) 476 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mStartSelectedCell) 477 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mEndSelectedCell) 478 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mAppendStartSelectedCell) 479 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mUnselectCellOnMouseUp) 480 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainedRange.mRange) 481 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mIndependentSelectionRootElement) 482 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mAncestorLimiter) 483 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END 484 485 // static 486 bool nsFrameSelection::Caret::IsVisualMovement( 487 ExtendSelection aExtendSelection, CaretMovementStyle aMovementStyle) { 488 int32_t movementFlag = StaticPrefs::bidi_edit_caret_movement_style(); 489 return aMovementStyle == eVisual || 490 (aMovementStyle == eUsePrefStyle && 491 (movementFlag == 1 || 492 (movementFlag == 2 && aExtendSelection == ExtendSelection::No))); 493 } 494 495 // Get the x (or y, in vertical writing mode) position requested 496 // by the Key Handling for line-up/down 497 nsresult nsFrameSelection::DesiredCaretPos::FetchPos( 498 nsPoint& aDesiredCaretPos, const PresShell& aPresShell, 499 Selection& aNormalSelection) const { 500 MOZ_ASSERT(aNormalSelection.GetType() == SelectionType::eNormal); 501 502 if (mIsSet) { 503 aDesiredCaretPos = mValue; 504 return NS_OK; 505 } 506 507 RefPtr<nsCaret> caret = aPresShell.GetCaret(); 508 if (!caret) { 509 return NS_ERROR_NULL_POINTER; 510 } 511 512 caret->SetSelection(&aNormalSelection); 513 514 nsRect coord; 515 nsIFrame* caretFrame = caret->GetGeometry(&coord); 516 if (!caretFrame) { 517 return NS_ERROR_FAILURE; 518 } 519 coord += caretFrame->GetOffsetToRootFrame(); 520 aDesiredCaretPos = coord.TopLeft(); 521 return NS_OK; 522 } 523 524 void nsFrameSelection::InvalidateDesiredCaretPos() // do not listen to 525 // mDesiredCaretPos.mValue; 526 // you must get another. 527 { 528 mDesiredCaretPos.Invalidate(); 529 } 530 531 void nsFrameSelection::DesiredCaretPos::Invalidate() { mIsSet = false; } 532 533 void nsFrameSelection::DesiredCaretPos::Set(const nsPoint& aPos) { 534 mValue = aPos; 535 mIsSet = true; 536 } 537 538 nsresult nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree( 539 nsIFrame* aFrame, const nsPoint& aPoint, nsIFrame** aRetFrame, 540 nsPoint& aRetPoint) const { 541 // 542 // The whole point of this method is to return a frame and point that 543 // that lie within the same valid subtree as the anchor node's frame, 544 // for use with the method GetContentAndOffsetsFromPoint(). 545 // 546 // A valid subtree is defined to be one where all the content nodes in 547 // the tree have a valid parent-child relationship. 548 // 549 // If the anchor frame and aFrame are in the same subtree, aFrame will 550 // be returned in aRetFrame. If they are in different subtrees, we 551 // return the frame for the root of the subtree. 552 // 553 554 if (!aFrame || !aRetFrame) { 555 return NS_ERROR_NULL_POINTER; 556 } 557 558 *aRetFrame = aFrame; 559 aRetPoint = aPoint; 560 561 // 562 // Get the frame and content for the selection's anchor point! 563 // 564 565 const Selection& sel = NormalSelection(); 566 567 nsCOMPtr<nsIContent> anchorContent = 568 nsIContent::FromNodeOrNull(sel.GetMayCrossShadowBoundaryAnchorNode()); 569 if (!anchorContent) { 570 return NS_ERROR_FAILURE; 571 } 572 573 // 574 // Now find the root of the subtree containing the anchor's content. 575 // 576 577 NS_ENSURE_STATE(mPresShell); 578 RefPtr<PresShell> presShell = mPresShell; 579 nsIContent* anchorRoot = anchorContent->GetSelectionRootContent( 580 presShell, nsINode::IgnoreOwnIndependentSelection::Yes, 581 static_cast<nsINode::AllowCrossShadowBoundary>( 582 StaticPrefs::dom_shadowdom_selection_across_boundary_enabled())); 583 NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED); 584 585 // 586 // Now find the root of the subtree containing aFrame's content. 587 // 588 589 nsCOMPtr<nsIContent> content = aFrame->GetContent(); 590 591 if (content) { 592 nsIContent* contentRoot = content->GetSelectionRootContent( 593 presShell, nsINode::IgnoreOwnIndependentSelection::Yes, 594 static_cast<nsINode::AllowCrossShadowBoundary>( 595 StaticPrefs::dom_shadowdom_selection_across_boundary_enabled())); 596 NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED); 597 598 if (anchorRoot == contentRoot) { 599 // If the aFrame's content isn't the capturing content, it should be 600 // a descendant. At this time, we can return simply. 601 nsIContent* capturedContent = PresShell::GetCapturingContent(); 602 if (capturedContent != content) { 603 return NS_OK; 604 } 605 606 // Find the frame under the mouse cursor with the root frame. 607 // At this time, don't use the anchor's frame because it may not have 608 // fixed positioned frames. 609 nsIFrame* rootFrame = presShell->GetRootFrame(); 610 nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame); 611 nsIFrame* cursorFrame = 612 nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot); 613 614 // If the mouse cursor in on a frame which is descendant of same 615 // selection root, we can expand the selection to the frame. 616 if (cursorFrame && cursorFrame->PresShell() == presShell) { 617 nsCOMPtr<nsIContent> cursorContent = cursorFrame->GetContent(); 618 NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE); 619 nsIContent* cursorContentRoot = cursorContent->GetSelectionRootContent( 620 presShell, nsINode::IgnoreOwnIndependentSelection::Yes, 621 static_cast<nsINode::AllowCrossShadowBoundary>( 622 StaticPrefs:: 623 dom_shadowdom_selection_across_boundary_enabled())); 624 NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED); 625 if (cursorContentRoot == anchorRoot) { 626 *aRetFrame = cursorFrame; 627 aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame); 628 return NS_OK; 629 } 630 } 631 // Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse 632 // cursor is out of the window), we should use the frame of the anchor 633 // root. 634 } 635 } 636 637 // 638 // When we can't find a frame which is under the mouse cursor and has a same 639 // selection root as the anchor node's, we should return the selection root 640 // frame. 641 // 642 643 *aRetFrame = anchorRoot->GetPrimaryFrame(); 644 645 if (!*aRetFrame) { 646 return NS_ERROR_FAILURE; 647 } 648 649 // 650 // Now make sure that aRetPoint is converted to the same coordinate 651 // system used by aRetFrame. 652 // 653 654 aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame); 655 656 return NS_OK; 657 } 658 659 void nsFrameSelection::SetCaretBidiLevelAndMaybeSchedulePaint( 660 mozilla::intl::BidiEmbeddingLevel aLevel) { 661 // If the current level is undefined, we have just inserted new text. 662 // In this case, we don't want to reset the keyboard language 663 mCaret.mBidiLevel = aLevel; 664 665 RefPtr<nsCaret> caret; 666 if (mPresShell && (caret = mPresShell->GetCaret())) { 667 caret->SchedulePaint(); 668 } 669 } 670 671 mozilla::intl::BidiEmbeddingLevel nsFrameSelection::GetCaretBidiLevel() const { 672 return mCaret.mBidiLevel; 673 } 674 675 void nsFrameSelection::UndefineCaretBidiLevel() { 676 mCaret.mBidiLevel = mozilla::intl::BidiEmbeddingLevel(mCaret.mBidiLevel | 677 BIDI_LEVEL_UNDEFINED); 678 } 679 680 #ifdef PRINT_RANGE 681 void printRange(nsRange* aDomRange) { 682 if (!aDomRange) { 683 printf("NULL Range\n"); 684 } 685 nsINode* startNode = aDomRange->GetStartContainer(); 686 nsINode* endNode = aDomRange->GetEndContainer(); 687 int32_t startOffset = aDomRange->StartOffset(); 688 int32_t endOffset = aDomRange->EndOffset(); 689 690 printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n", 691 (unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset, 692 (unsigned long)endNode, (long)endOffset); 693 } 694 #endif /* PRINT_RANGE */ 695 696 static nsAtom* GetTag(nsINode* aNode) { 697 nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); 698 if (!content) { 699 MOZ_ASSERT_UNREACHABLE("bad node passed to GetTag()"); 700 return nullptr; 701 } 702 703 return content->NodeInfo()->NameAtom(); 704 } 705 706 /** 707 * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor. 708 */ 709 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode) { 710 if (!aDomNode) { 711 return nullptr; 712 } 713 nsINode* current = aDomNode; 714 // Start with current node and look for a table cell 715 while (current) { 716 nsAtom* tag = GetTag(current); 717 if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) { 718 return current; 719 } 720 current = current->GetParent(); 721 } 722 return nullptr; 723 } 724 725 static nsDirection GetCaretDirection(const nsIFrame& aFrame, 726 nsDirection aDirection, 727 bool aVisualMovement) { 728 const mozilla::intl::BidiDirection paragraphDirection = 729 nsBidiPresUtils::ParagraphDirection(&aFrame); 730 return (aVisualMovement && 731 paragraphDirection == mozilla::intl::BidiDirection::RTL) 732 ? nsDirection(1 - aDirection) 733 : aDirection; 734 } 735 736 nsresult nsFrameSelection::MoveCaret(nsDirection aDirection, 737 ExtendSelection aExtendSelection, 738 const nsSelectionAmount aAmount, 739 CaretMovementStyle aMovementStyle) { 740 NS_ENSURE_STATE(mPresShell); 741 // Flush out layout, since we need it to be up to date to do caret 742 // positioning. 743 OwningNonNull<PresShell> presShell(*mPresShell); 744 presShell->FlushPendingNotifications(FlushType::Layout); 745 746 if (!mPresShell) { 747 return NS_OK; 748 } 749 750 nsPresContext* context = mPresShell->GetPresContext(); 751 if (!context) { 752 return NS_ERROR_FAILURE; 753 } 754 755 const RefPtr<Selection> sel = &NormalSelection(); 756 757 auto scrollFlags = ScrollFlags::None; 758 if (sel->IsEditorSelection()) { 759 // If caret moves in editor, it should cause scrolling even if it's in 760 // overflow: hidden;. 761 scrollFlags |= ScrollFlags::ScrollOverflowHidden; 762 } 763 764 const bool doCollapse = [&] { 765 if (sel->IsCollapsed() || aExtendSelection == ExtendSelection::Yes) { 766 return false; 767 } 768 if (aAmount > eSelectLine) { 769 return false; 770 } 771 int32_t caretStyle = StaticPrefs::layout_selection_caret_style(); 772 return caretStyle == 2 || (caretStyle == 0 && aAmount != eSelectLine); 773 }(); 774 775 if (doCollapse) { 776 if (aDirection == eDirPrevious) { 777 SetChangeReasons(nsISelectionListener::COLLAPSETOSTART_REASON); 778 mCaret.mHint = CaretAssociationHint::After; 779 } else { 780 SetChangeReasons(nsISelectionListener::COLLAPSETOEND_REASON); 781 mCaret.mHint = CaretAssociationHint::Before; 782 } 783 } else { 784 SetChangeReasons(nsISelectionListener::KEYPRESS_REASON); 785 } 786 787 mCaretMoveAmount = aAmount; 788 789 AutoPrepareFocusRange prep(sel, false); 790 791 // we must keep this around and revalidate it when its just UP/DOWN 792 nsPoint desiredPos(0, 0); 793 794 if (aAmount == eSelectLine) { 795 nsresult result = mDesiredCaretPos.FetchPos(desiredPos, *mPresShell, *sel); 796 if (NS_FAILED(result)) { 797 return result; 798 } 799 mDesiredCaretPos.Set(desiredPos); 800 } 801 802 bool visualMovement = 803 Caret::IsVisualMovement(aExtendSelection, aMovementStyle); 804 const PrimaryFrameData frameForFocus = 805 sel->GetPrimaryFrameForCaretAtFocusNode(visualMovement); 806 if (!frameForFocus) { 807 return NS_ERROR_FAILURE; 808 } 809 if (visualMovement) { 810 // FYI: This was done during a call of GetPrimaryFrameForCaretAtFocusNode. 811 // Therefore, this may not be intended by the original author. 812 SetHint(frameForFocus.mHint); 813 } 814 815 Result<bool, nsresult> isIntraLineCaretMove = 816 SelectionMovementUtils::IsIntraLineCaretMove(aAmount); 817 nsDirection direction{aDirection}; 818 if (isIntraLineCaretMove.isErr()) { 819 return isIntraLineCaretMove.unwrapErr(); 820 } 821 if (isIntraLineCaretMove.inspect()) { 822 // Forget old caret position for moving caret to different line since 823 // caret position may be changed. 824 mDesiredCaretPos.Invalidate(); 825 direction = 826 GetCaretDirection(*frameForFocus.mFrame, aDirection, visualMovement); 827 } 828 829 if (doCollapse) { 830 const nsRange* anchorFocusRange = sel->GetAnchorFocusRange(); 831 if (anchorFocusRange) { 832 RefPtr<nsINode> node; 833 uint32_t offset; 834 if (visualMovement && 835 nsBidiPresUtils::IsReversedDirectionFrame(frameForFocus.mFrame)) { 836 direction = nsDirection(1 - direction); 837 } 838 if (direction == eDirPrevious) { 839 node = anchorFocusRange->GetStartContainer(); 840 offset = anchorFocusRange->StartOffset(); 841 } else { 842 node = anchorFocusRange->GetEndContainer(); 843 offset = anchorFocusRange->EndOffset(); 844 } 845 sel->CollapseInLimiter(node, offset); 846 } 847 sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, 848 ScrollAxis(), ScrollAxis(), scrollFlags); 849 return NS_OK; 850 } 851 852 CaretAssociationHint tHint( 853 mCaret.mHint); // temporary variable so we dont set 854 // mCaret.mHint until it is necessary 855 856 Result<PeekOffsetOptions, nsresult> options = 857 CreatePeekOffsetOptionsForCaretMove(sel, aExtendSelection, 858 aMovementStyle); 859 if (options.isErr()) { 860 return options.propagateErr(); 861 } 862 Result<const dom::Element*, nsresult> ancestorLimiter = 863 GetAncestorLimiterForCaretMove(sel); 864 if (ancestorLimiter.isErr()) { 865 return ancestorLimiter.propagateErr(); 866 } 867 nsIContent* content = nsIContent::FromNodeOrNull(sel->GetFocusNode()); 868 869 Result<PeekOffsetStruct, nsresult> result = 870 SelectionMovementUtils::PeekOffsetForCaretMove( 871 content, sel->FocusOffset(), direction, GetHint(), 872 GetCaretBidiLevel(), aAmount, desiredPos, options.unwrap(), 873 ancestorLimiter.unwrap()); 874 nsresult rv; 875 if (result.isOk() && result.inspect().mResultContent) { 876 const PeekOffsetStruct& pos = result.inspect(); 877 nsIFrame* theFrame; 878 int32_t frameStart, frameEnd; 879 880 if (aAmount <= eSelectWordNoSpace) { 881 // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does 882 // not set pos.mAttachForward, so determine the hint here based on the 883 // result frame and offset: If we're at the end of a text frame, set the 884 // hint to ASSOCIATE_BEFORE to indicate that we want the caret displayed 885 // at the end of this frame, not at the beginning of the next one. 886 theFrame = pos.mResultFrame; 887 std::tie(frameStart, frameEnd) = theFrame->GetOffsets(); 888 if (frameEnd == pos.mContentOffset && 889 !(frameStart == 0 && frameEnd == 0)) { 890 tHint = CaretAssociationHint::Before; 891 } else { 892 tHint = CaretAssociationHint::After; 893 } 894 } else { 895 // For up/down and home/end, pos.mResultFrame might not be set correctly, 896 // or not at all. In these cases, get the frame based on the content and 897 // hint returned by PeekOffset(). 898 tHint = pos.mAttach; 899 theFrame = SelectionMovementUtils::GetFrameForNodeOffset( 900 pos.mResultContent, pos.mContentOffset, tHint); 901 if (!theFrame) { 902 return NS_ERROR_FAILURE; 903 } 904 905 std::tie(frameStart, frameEnd) = theFrame->GetOffsets(); 906 } 907 908 if (context->BidiEnabled()) { 909 switch (aAmount) { 910 case eSelectBeginLine: 911 case eSelectEndLine: { 912 // In Bidi contexts, PeekOffset calculates pos.mContentOffset 913 // differently depending on whether the movement is visual or logical. 914 // For visual movement, pos.mContentOffset depends on the direction- 915 // ality of the first/last frame on the line (theFrame), and the caret 916 // directionality must correspond. 917 FrameBidiData bidiData = theFrame->GetBidiData(); 918 SetCaretBidiLevelAndMaybeSchedulePaint( 919 visualMovement ? bidiData.embeddingLevel : bidiData.baseLevel); 920 break; 921 } 922 default: 923 // If the current position is not a frame boundary, it's enough just 924 // to take the Bidi level of the current frame 925 if ((pos.mContentOffset != frameStart && 926 pos.mContentOffset != frameEnd) || 927 eSelectLine == aAmount) { 928 SetCaretBidiLevelAndMaybeSchedulePaint( 929 theFrame->GetEmbeddingLevel()); 930 } else { 931 BidiLevelFromMove(mPresShell, pos.mResultContent, 932 pos.mContentOffset, aAmount, tHint); 933 } 934 } 935 } 936 // "pos" is on the stack, so pos.mResultContent has stack lifetime, so using 937 // MOZ_KnownLive is ok. 938 const FocusMode focusMode = aExtendSelection == ExtendSelection::Yes 939 ? FocusMode::kExtendSelection 940 : FocusMode::kCollapseToNewPoint; 941 rv = TakeFocus(MOZ_KnownLive(*pos.mResultContent), pos.mContentOffset, 942 pos.mContentOffset, tHint, focusMode); 943 } else if (aAmount <= eSelectWordNoSpace && direction == eDirNext && 944 aExtendSelection == ExtendSelection::No) { 945 // Collapse selection if PeekOffset failed, we either 946 // 1. bumped into the BRFrame, bug 207623 947 // 2. had select-all in a text input (DIV range), bug 352759. 948 bool isBRFrame = frameForFocus.mFrame->IsBrFrame(); 949 RefPtr<nsINode> node = sel->GetFocusNode(); 950 sel->CollapseInLimiter(node, sel->FocusOffset()); 951 // Note: 'frameForFocus.mFrame' might be dead here. 952 if (!isBRFrame) { 953 mCaret.mHint = CaretAssociationHint::Before; // We're now at the end of 954 // the frame to the left. 955 } 956 rv = NS_OK; 957 } else { 958 rv = result.isErr() ? result.unwrapErr() : NS_OK; 959 } 960 if (NS_SUCCEEDED(rv)) { 961 rv = sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, 962 ScrollAxis(), ScrollAxis(), scrollFlags); 963 } 964 965 return rv; 966 } 967 968 // static 969 Result<PeekOffsetOptions, nsresult> 970 nsFrameSelection::CreatePeekOffsetOptionsForCaretMove( 971 const Element* aSelectionLimiter, ForceEditableRegion aForceEditableRegion, 972 ExtendSelection aExtendSelection, CaretMovementStyle aMovementStyle) { 973 PeekOffsetOptions options; 974 // set data using aSelectionLimiter to stop on scroll views. If we have a 975 // limiter then we stop peeking when we hit scrollable views. If no limiter 976 // then just let it go ahead 977 if (aSelectionLimiter) { 978 options += PeekOffsetOption::StopAtScroller; 979 } 980 const bool visualMovement = 981 Caret::IsVisualMovement(aExtendSelection, aMovementStyle); 982 if (visualMovement) { 983 options += PeekOffsetOption::Visual; 984 } 985 if (aExtendSelection == ExtendSelection::Yes) { 986 options += PeekOffsetOption::Extend; 987 } 988 if (static_cast<bool>(aForceEditableRegion)) { 989 options += PeekOffsetOption::ForceEditableRegion; 990 } 991 return options; 992 } 993 994 Result<Element*, nsresult> nsFrameSelection::GetAncestorLimiterForCaretMove( 995 dom::Selection* aSelection) const { 996 if (!mPresShell) { 997 return Err(NS_ERROR_NULL_POINTER); 998 } 999 1000 MOZ_ASSERT(aSelection); 1001 nsIContent* content = nsIContent::FromNodeOrNull(aSelection->GetFocusNode()); 1002 if (!content) { 1003 return Err(NS_ERROR_FAILURE); 1004 } 1005 1006 MOZ_ASSERT(mPresShell->GetDocument() == content->GetComposedDoc()); 1007 1008 Element* ancestorLimiter = GetAncestorLimiter(); 1009 if (aSelection->IsEditorSelection()) { 1010 // If the editor has not receive `focus` event, it may have not set ancestor 1011 // limiter. Then, we need to compute it here for the caret move. 1012 if (!ancestorLimiter) { 1013 // Editing hosts can be nested. Therefore, computing selection root from 1014 // selection range may be different from the focused editing host. 1015 // Therefore, we may need to use a non-closest inclusive ancestor editing 1016 // host of selection range container. On the other hand, selection ranges 1017 // may be outside of focused editing host. In such case, we should use 1018 // the closest editing host as the ancestor limiter instead. 1019 PresShell* const presShell = aSelection->GetPresShell(); 1020 const Document* const doc = 1021 presShell ? presShell->GetDocument() : nullptr; 1022 if (const nsPIDOMWindowInner* const win = 1023 doc ? doc->GetInnerWindow() : nullptr) { 1024 Element* const focusedElement = win->GetFocusedElement(); 1025 Element* closestEditingHost = nullptr; 1026 for (Element* element : content->InclusiveAncestorsOfType<Element>()) { 1027 if (element->IsEditingHost()) { 1028 if (!closestEditingHost) { 1029 closestEditingHost = element; 1030 } 1031 if (focusedElement == element) { 1032 ancestorLimiter = focusedElement; 1033 break; 1034 } 1035 } 1036 } 1037 if (!ancestorLimiter) { 1038 ancestorLimiter = closestEditingHost; 1039 } 1040 } 1041 // If it's the root element, we don't need to limit the new caret 1042 // position. 1043 if (ancestorLimiter && !ancestorLimiter->GetParent()) { 1044 ancestorLimiter = nullptr; 1045 } 1046 } 1047 } 1048 return ancestorLimiter; 1049 } 1050 1051 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels( 1052 nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const { 1053 return SelectionMovementUtils::GetPrevNextBidiLevels( 1054 aNode, aContentOffset, mCaret.mHint, aJumpLines, 1055 GetAncestorLimiterOrIndependentSelectionRootElement()); 1056 } 1057 1058 nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) { 1059 const Selection& sel = NormalSelection(); 1060 1061 mMaintainedRange.MaintainAnchorFocusRange(sel, aAmount); 1062 1063 return NS_OK; 1064 } 1065 1066 void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell, 1067 nsIContent* aNode, 1068 uint32_t aContentOffset, 1069 nsSelectionAmount aAmount, 1070 CaretAssociationHint aHint) { 1071 switch (aAmount) { 1072 // Movement within the line: the new cursor Bidi level is the level of the 1073 // last character moved over 1074 case eSelectCharacter: 1075 case eSelectCluster: 1076 case eSelectWord: 1077 case eSelectWordNoSpace: 1078 case eSelectBeginLine: 1079 case eSelectEndLine: 1080 case eSelectNoAmount: { 1081 nsPrevNextBidiLevels levels = 1082 SelectionMovementUtils::GetPrevNextBidiLevels( 1083 aNode, aContentOffset, aHint, false, 1084 GetAncestorLimiterOrIndependentSelectionRootElement()); 1085 1086 SetCaretBidiLevelAndMaybeSchedulePaint( 1087 aHint == CaretAssociationHint::Before ? levels.mLevelBefore 1088 : levels.mLevelAfter); 1089 break; 1090 } 1091 /* 1092 // Up and Down: the new cursor Bidi level is the smaller of the two 1093 surrounding characters case eSelectLine: case eSelectParagraph: 1094 GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame, 1095 &secondFrame, &firstLevel, &secondLevel); 1096 aPresShell->SetCaretBidiLevelAndMaybeSchedulePaint(std::min(firstLevel, 1097 secondLevel)); break; 1098 */ 1099 1100 default: 1101 UndefineCaretBidiLevel(); 1102 } 1103 } 1104 1105 void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode, 1106 uint32_t aContentOffset) { 1107 nsIFrame* clickInFrame = SelectionMovementUtils::GetFrameForNodeOffset( 1108 aNode, aContentOffset, mCaret.mHint); 1109 if (!clickInFrame) { 1110 return; 1111 } 1112 1113 SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel()); 1114 } 1115 1116 void nsFrameSelection::MaintainedRange::AdjustNormalSelection( 1117 const nsIContent* aContent, const int32_t aOffset, 1118 Selection& aNormalSelection) const { 1119 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 1120 1121 if (!mRange || !aContent) { 1122 return; 1123 } 1124 1125 nsINode* rangeStartNode = mRange->GetStartContainer(); 1126 nsINode* rangeEndNode = mRange->GetEndContainer(); 1127 const uint32_t rangeStartOffset = mRange->StartOffset(); 1128 const uint32_t rangeEndOffset = mRange->EndOffset(); 1129 1130 NS_ASSERTION(aOffset >= 0, "aOffset should not be negative"); 1131 const Maybe<int32_t> relToStart = 1132 nsContentUtils::ComparePoints_AllowNegativeOffsets( 1133 rangeStartNode, rangeStartOffset, aContent, aOffset); 1134 if (NS_WARN_IF(!relToStart)) { 1135 // Potentially handle this properly when Selection across Shadow DOM 1136 // boundary is implemented 1137 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497). 1138 return; 1139 } 1140 1141 const Maybe<int32_t> relToEnd = 1142 nsContentUtils::ComparePoints_AllowNegativeOffsets( 1143 rangeEndNode, rangeEndOffset, aContent, aOffset); 1144 if (NS_WARN_IF(!relToEnd)) { 1145 // Potentially handle this properly when Selection across Shadow DOM 1146 // boundary is implemented 1147 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497). 1148 return; 1149 } 1150 1151 // If aContent/aOffset is inside (or at the edge of) the maintained 1152 // selection, or if it is on the "anchor" side of the maintained selection, 1153 // we need to do something. 1154 if ((*relToStart <= 0 && *relToEnd >= 0) || 1155 (*relToStart > 0 && aNormalSelection.GetDirection() == eDirNext) || 1156 (*relToEnd < 0 && aNormalSelection.GetDirection() == eDirPrevious)) { 1157 // Set the current range to the maintained range. 1158 aNormalSelection.ReplaceAnchorFocusRange(mRange); 1159 // Set the direction of the selection so that the anchor will be on the 1160 // far side of the maintained selection, relative to aContent/aOffset. 1161 aNormalSelection.SetDirection(*relToStart > 0 ? eDirPrevious : eDirNext); 1162 } 1163 } 1164 1165 void nsFrameSelection::MaintainedRange::AdjustContentOffsets( 1166 nsIFrame::ContentOffsets& aOffsets, StopAtScroller aStopAtScroller) const { 1167 // Adjust offsets according to maintained amount 1168 if (mRange && mAmount != eSelectNoAmount) { 1169 const Maybe<int32_t> relativePosition = nsContentUtils::ComparePoints( 1170 mRange->StartRef(), 1171 RawRangeBoundary(aOffsets.content, aOffsets.offset, 1172 RangeBoundaryIsMutationObserved::No)); 1173 if (NS_WARN_IF(!relativePosition)) { 1174 // Potentially handle this properly when Selection across Shadow DOM 1175 // boundary is implemented 1176 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497). 1177 return; 1178 } 1179 1180 nsDirection direction = *relativePosition > 0 ? eDirPrevious : eDirNext; 1181 nsSelectionAmount amount = mAmount; 1182 if (amount == eSelectBeginLine && direction == eDirNext) { 1183 amount = eSelectEndLine; 1184 } 1185 1186 FrameAndOffset frameAndOffset = 1187 SelectionMovementUtils::GetFrameForNodeOffset( 1188 aOffsets.content, aOffsets.offset, CaretAssociationHint::After); 1189 1190 PeekOffsetOptions peekOffsetOptions{}; 1191 if (aStopAtScroller == StopAtScroller::Yes) { 1192 peekOffsetOptions += PeekOffsetOption::StopAtScroller; 1193 } 1194 if (frameAndOffset && amount == eSelectWord && direction == eDirPrevious) { 1195 // To avoid selecting the previous word when at start of word, 1196 // first move one character forward. 1197 PeekOffsetStruct charPos( 1198 eSelectCharacter, eDirNext, 1199 static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent), 1200 nsPoint(0, 0), peekOffsetOptions); 1201 if (NS_SUCCEEDED(frameAndOffset->PeekOffset(&charPos))) { 1202 frameAndOffset = {charPos.mResultFrame, 1203 static_cast<uint32_t>(charPos.mContentOffset)}; 1204 } 1205 } 1206 1207 PeekOffsetStruct pos( 1208 amount, direction, 1209 static_cast<int32_t>(frameAndOffset.mOffsetInFrameContent), 1210 nsPoint(0, 0), peekOffsetOptions); 1211 if (frameAndOffset && NS_SUCCEEDED(frameAndOffset->PeekOffset(&pos)) && 1212 pos.mResultContent) { 1213 aOffsets.content = pos.mResultContent; 1214 aOffsets.offset = pos.mContentOffset; 1215 } 1216 } 1217 } 1218 1219 void nsFrameSelection::MaintainedRange::MaintainAnchorFocusRange( 1220 const Selection& aNormalSelection, const nsSelectionAmount aAmount) { 1221 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 1222 1223 mAmount = aAmount; 1224 1225 const nsRange* anchorFocusRange = aNormalSelection.GetAnchorFocusRange(); 1226 if (anchorFocusRange && aAmount != eSelectNoAmount) { 1227 mRange = anchorFocusRange->CloneRange(); 1228 return; 1229 } 1230 1231 mRange = nullptr; 1232 } 1233 1234 nsresult nsFrameSelection::HandleClick(nsIContent* aNewFocus, 1235 uint32_t aContentOffset, 1236 uint32_t aContentEndOffset, 1237 const FocusMode aFocusMode, 1238 CaretAssociationHint aHint) { 1239 if (!aNewFocus) { 1240 return NS_ERROR_INVALID_ARG; 1241 } 1242 1243 if (MOZ_LOG_TEST(sFrameSelectionLog, LogLevel::Debug)) { 1244 const Selection& sel = NormalSelection(); 1245 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug, 1246 ("%s: selection=%p, new focus=%p, offsets=(%u,%u), focus mode=%i", 1247 __FUNCTION__, &sel, aNewFocus, aContentOffset, aContentEndOffset, 1248 static_cast<int>(aFocusMode))); 1249 } 1250 1251 mDesiredCaretPos.Invalidate(); 1252 1253 if (aFocusMode != FocusMode::kExtendSelection) { 1254 mMaintainedRange.mRange = nullptr; 1255 if (!NodeIsInLimiters(aNewFocus)) { 1256 mLimiters.mAncestorLimiter = nullptr; 1257 } 1258 } 1259 1260 // Don't take focus when dragging off of a table 1261 if (!mTableSelection.mDragSelectingCells) { 1262 BidiLevelFromClick(aNewFocus, aContentOffset); 1263 SetChangeReasons(nsISelectionListener::MOUSEDOWN_REASON + 1264 nsISelectionListener::DRAG_REASON); 1265 1266 RefPtr<Selection> selection = &NormalSelection(); 1267 1268 if (aFocusMode == FocusMode::kExtendSelection) { 1269 mMaintainedRange.AdjustNormalSelection(aNewFocus, aContentOffset, 1270 *selection); 1271 } 1272 1273 AutoPrepareFocusRange prep(selection, 1274 aFocusMode == FocusMode::kMultiRangeSelection); 1275 return TakeFocus(*aNewFocus, aContentOffset, aContentEndOffset, aHint, 1276 aFocusMode); 1277 } 1278 1279 return NS_OK; 1280 } 1281 1282 void nsFrameSelection::HandleDrag(nsIFrame* aFrame, const nsPoint& aPoint) { 1283 if (!aFrame || !mPresShell) { 1284 return; 1285 } 1286 1287 nsresult result; 1288 nsIFrame* newFrame = 0; 1289 nsPoint newPoint; 1290 1291 result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame, 1292 newPoint); 1293 if (NS_FAILED(result)) { 1294 return; 1295 } 1296 if (!newFrame) { 1297 return; 1298 } 1299 1300 nsIFrame::ContentOffsets offsets = 1301 newFrame->GetContentOffsetsFromPoint(newPoint); 1302 if (!offsets.content) { 1303 return; 1304 } 1305 1306 RefPtr<Selection> selection = &NormalSelection(); 1307 if (newFrame->IsSelected()) { 1308 // `MOZ_KnownLive` required because of 1309 // https://bugzilla.mozilla.org/show_bug.cgi?id=1636889. 1310 mMaintainedRange.AdjustNormalSelection(MOZ_KnownLive(offsets.content), 1311 offsets.offset, *selection); 1312 } 1313 1314 mMaintainedRange.AdjustContentOffsets( 1315 offsets, mLimiters.mIndependentSelectionRootElement 1316 ? MaintainedRange::StopAtScroller::Yes 1317 : MaintainedRange::StopAtScroller::No); 1318 1319 // TODO: no click has happened, rename `HandleClick`. 1320 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.offset, 1321 offsets.offset, FocusMode::kExtendSelection, offsets.associate); 1322 } 1323 1324 nsresult nsFrameSelection::StartAutoScrollTimer(nsIFrame* aFrame, 1325 const nsPoint& aPoint, 1326 uint32_t aDelay) { 1327 RefPtr<Selection> selection = &NormalSelection(); 1328 return selection->StartAutoScrollTimer(aFrame, aPoint, aDelay); 1329 } 1330 1331 void nsFrameSelection::StopAutoScrollTimer() { 1332 Selection& sel = NormalSelection(); 1333 sel.StopAutoScrollTimer(); 1334 } 1335 1336 // static 1337 nsINode* nsFrameSelection::TableSelection::IsContentInActivelyEditableTableCell( 1338 nsPresContext* aContext, nsIContent* aContent) { 1339 if (!aContext) { 1340 return nullptr; 1341 } 1342 1343 RefPtr<HTMLEditor> htmlEditor = nsContentUtils::GetHTMLEditor(aContext); 1344 if (!htmlEditor) { 1345 return nullptr; 1346 } 1347 1348 nsINode* inclusiveTableCellAncestor = 1349 GetClosestInclusiveTableCellAncestor(aContent); 1350 if (!inclusiveTableCellAncestor) { 1351 return nullptr; 1352 } 1353 1354 const Element* editingHost = htmlEditor->ComputeEditingHost(); 1355 if (!editingHost) { 1356 return nullptr; 1357 } 1358 1359 const bool editableCell = 1360 inclusiveTableCellAncestor->IsInclusiveDescendantOf(editingHost); 1361 return editableCell ? inclusiveTableCellAncestor : nullptr; 1362 } 1363 1364 namespace { 1365 struct ParentAndOffset { 1366 explicit ParentAndOffset(const nsINode& aNode) 1367 : mParent{aNode.GetParent()}, 1368 mOffset{mParent ? mParent->ComputeIndexOf_Deprecated(&aNode) : 0} {} 1369 1370 nsINode* mParent; 1371 1372 // 0, if there's no parent. 1373 int32_t mOffset; 1374 }; 1375 1376 } // namespace 1377 /** 1378 hard to go from nodes to frames, easy the other way! 1379 */ 1380 nsresult nsFrameSelection::TakeFocus(nsIContent& aNewFocus, 1381 uint32_t aContentOffset, 1382 uint32_t aContentEndOffset, 1383 CaretAssociationHint aHint, 1384 const FocusMode aFocusMode) { 1385 NS_ENSURE_STATE(mPresShell); 1386 1387 if (!NodeIsInLimiters(&aNewFocus)) { 1388 return NS_ERROR_FAILURE; 1389 } 1390 1391 MOZ_LOG(sFrameSelectionLog, LogLevel::Verbose, 1392 ("%s: new focus=%p, offsets=(%u, %u), hint=%i, focusMode=%i", 1393 __FUNCTION__, &aNewFocus, aContentOffset, aContentEndOffset, 1394 static_cast<int>(aHint), static_cast<int>(aFocusMode))); 1395 1396 mPresShell->FrameSelectionWillTakeFocus( 1397 *this, aNewFocus.CanStartSelectionAsWebCompatHack() 1398 ? PresShell::CanMoveLastSelectionForToString::Yes 1399 : PresShell::CanMoveLastSelectionForToString::No); 1400 1401 // Clear all table selection data 1402 mTableSelection.mMode = TableSelectionMode::None; 1403 mTableSelection.mDragSelectingCells = false; 1404 mTableSelection.mStartSelectedCell = nullptr; 1405 mTableSelection.mEndSelectedCell = nullptr; 1406 mTableSelection.mAppendStartSelectedCell = nullptr; 1407 mCaret.mHint = aHint; 1408 1409 RefPtr<Selection> selection = &NormalSelection(); 1410 1411 Maybe<Selection::AutoUserInitiated> userSelect; 1412 if (IsUserSelectionReason()) { 1413 userSelect.emplace(selection); 1414 } 1415 1416 // traverse through document and unselect crap here 1417 switch (aFocusMode) { 1418 case FocusMode::kCollapseToNewPoint: 1419 [[fallthrough]]; 1420 case FocusMode::kMultiRangeSelection: { 1421 // single click? setting cursor down 1422 const Batching saveBatching = 1423 mBatching; // hack to use the collapse code. 1424 mBatching.mCounter = 1; 1425 1426 if (aFocusMode == FocusMode::kMultiRangeSelection) { 1427 // Remove existing collapsed ranges as there's no point in having 1428 // non-anchor/focus collapsed ranges. 1429 selection->RemoveCollapsedRanges(); 1430 1431 ErrorResult error; 1432 RefPtr<nsRange> newRange = nsRange::Create( 1433 &aNewFocus, aContentOffset, &aNewFocus, aContentOffset, error); 1434 if (NS_WARN_IF(error.Failed())) { 1435 return error.StealNSResult(); 1436 } 1437 MOZ_ASSERT(newRange); 1438 selection->AddRangeAndSelectFramesAndNotifyListeners(*newRange, 1439 IgnoreErrors()); 1440 } else { 1441 bool oldDesiredPosSet = 1442 mDesiredCaretPos.mIsSet; // need to keep old desired 1443 // position if it was set. 1444 selection->CollapseInLimiter(&aNewFocus, aContentOffset); 1445 mDesiredCaretPos.mIsSet = 1446 oldDesiredPosSet; // now reset desired pos back. 1447 } 1448 1449 mBatching = saveBatching; 1450 1451 if (aContentEndOffset != aContentOffset) { 1452 selection->Extend(&aNewFocus, aContentEndOffset); 1453 } 1454 1455 // find out if we are inside a table. if so, find out which one and which 1456 // cell once we do that, the next time we get a takefocus, check the 1457 // parent tree. if we are no longer inside same table ,cell then switch to 1458 // table selection mode. BUT only do this in an editor 1459 1460 NS_ENSURE_STATE(mPresShell); 1461 RefPtr<nsPresContext> context = mPresShell->GetPresContext(); 1462 mTableSelection.mClosestInclusiveTableCellAncestor = nullptr; 1463 if (nsINode* inclusiveTableCellAncestor = 1464 TableSelection::IsContentInActivelyEditableTableCell( 1465 context, &aNewFocus)) { 1466 mTableSelection.mClosestInclusiveTableCellAncestor = 1467 inclusiveTableCellAncestor; 1468 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug, 1469 ("%s: Collapsing into new cell", __FUNCTION__)); 1470 } 1471 1472 break; 1473 } 1474 case FocusMode::kExtendSelection: { 1475 // Now update the range list: 1476 nsINode* inclusiveTableCellAncestor = 1477 GetClosestInclusiveTableCellAncestor(&aNewFocus); 1478 if (mTableSelection.mClosestInclusiveTableCellAncestor && 1479 inclusiveTableCellAncestor && 1480 inclusiveTableCellAncestor != 1481 mTableSelection 1482 .mClosestInclusiveTableCellAncestor) // switch to cell 1483 // selection mode 1484 { 1485 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug, 1486 ("%s: moving into new cell", __FUNCTION__)); 1487 1488 WidgetMouseEvent event(false, eVoidEvent, nullptr, 1489 WidgetMouseEvent::eReal); 1490 1491 // Start selecting in the cell we were in before 1492 ParentAndOffset parentAndOffset{ 1493 *mTableSelection.mClosestInclusiveTableCellAncestor}; 1494 if (const nsCOMPtr<nsINode> previousParent = parentAndOffset.mParent) { 1495 const nsresult result = 1496 HandleTableSelection(previousParent, parentAndOffset.mOffset, 1497 TableSelectionMode::Cell, &event); 1498 if (NS_WARN_IF(NS_FAILED(result))) { 1499 return result; 1500 } 1501 } 1502 1503 // Find the parent of this new cell and extend selection to it 1504 parentAndOffset = ParentAndOffset{*inclusiveTableCellAncestor}; 1505 1506 // XXXX We need to REALLY get the current key shift state 1507 // (we'd need to add event listener -- let's not bother for now) 1508 event.mModifiers &= ~MODIFIER_SHIFT; // aExtendSelection; 1509 if (const nsCOMPtr<nsINode> newParent = parentAndOffset.mParent) { 1510 mTableSelection.mClosestInclusiveTableCellAncestor = 1511 inclusiveTableCellAncestor; 1512 // Continue selection into next cell 1513 const nsresult result = 1514 HandleTableSelection(newParent, parentAndOffset.mOffset, 1515 TableSelectionMode::Cell, &event); 1516 if (NS_WARN_IF(NS_FAILED(result))) { 1517 return result; 1518 } 1519 } 1520 } else { 1521 // XXXX Problem: Shift+click in browser is appending text selection to 1522 // selected table!!! 1523 // is this the place to erase selected cells ????? 1524 uint32_t offset = 1525 (selection->GetDirection() == eDirNext && 1526 aContentEndOffset > aContentOffset) // didn't go far enough 1527 ? aContentEndOffset // this will only redraw the diff 1528 : aContentOffset; 1529 selection->Extend(&aNewFocus, offset); 1530 } 1531 break; 1532 } 1533 } 1534 1535 // Be aware, the Selection instance may be destroyed after this call. 1536 return NotifySelectionListeners(SelectionType::eNormal); 1537 } 1538 1539 UniquePtr<SelectionDetails> nsFrameSelection::LookUpSelection( 1540 nsIContent* aContent, int32_t aContentOffset, int32_t aContentLength, 1541 IgnoreNormalSelection aIgnoreNormalSelection) const { 1542 if (!aContent || !mPresShell) { 1543 return nullptr; 1544 } 1545 1546 // TODO: Layout should use `uint32_t` for handling offset in DOM nodes 1547 // (for example: bug 1735262) 1548 MOZ_ASSERT(aContentOffset >= 0); 1549 MOZ_ASSERT(aContentLength >= 0); 1550 if (MOZ_UNLIKELY(aContentOffset < 0) || MOZ_UNLIKELY(aContentLength < 0)) { 1551 return nullptr; 1552 } 1553 1554 UniquePtr<SelectionDetails> details; 1555 for (size_t j = aIgnoreNormalSelection == IgnoreNormalSelection::Yes ? 1 : 0; 1556 j < std::size(mDomSelections); j++) { 1557 MOZ_ASSERT(mDomSelections[j]); 1558 details = mDomSelections[j]->LookUpSelection( 1559 aContent, static_cast<uint32_t>(aContentOffset), 1560 static_cast<uint32_t>(aContentLength), std::move(details), 1561 kPresentSelectionTypes[j]); 1562 } 1563 1564 // This may seem counter intuitive at first. Highlight selections need to be 1565 // iterated from back to front: 1566 // 1567 // - `mHighlightSelections` is ordered by insertion, i.e. if two or more 1568 // highlights overlap, the latest must take precedence. 1569 // - however, the `LookupSelection()` algorithm reverses the order by setting 1570 // the current `details` as `mNext`. 1571 for (const auto& iter : Reversed(mHighlightSelections)) { 1572 details = iter.second()->LookUpSelection( 1573 aContent, static_cast<uint32_t>(aContentOffset), 1574 static_cast<uint32_t>(aContentLength), std::move(details), 1575 SelectionType::eHighlight); 1576 } 1577 1578 return details; 1579 } 1580 1581 void nsFrameSelection::SetDragState(bool aState) { 1582 if (mDragState == aState) { 1583 return; 1584 } 1585 1586 mDragState = aState; 1587 1588 if (!mDragState) { 1589 mTableSelection.mDragSelectingCells = false; 1590 // Notify that reason is mouse up. 1591 SetChangeReasons(nsISelectionListener::MOUSEUP_REASON); 1592 1593 // flag is set to NotApplicable in `Selection::NotifySelectionListeners`. 1594 // since this function call is part of click event, this would immediately 1595 // reset the flag, rendering it useless. 1596 AutoRestore<ClickSelectionType> restoreClickSelectionType( 1597 mClickSelectionType); 1598 // Be aware, the Selection instance may be destroyed after this call. 1599 NotifySelectionListeners(SelectionType::eNormal); 1600 } 1601 } 1602 1603 Selection* nsFrameSelection::GetSelection(SelectionType aSelectionType) const { 1604 int8_t index = GetIndexFromSelectionType(aSelectionType); 1605 if (index < 0) { 1606 return nullptr; 1607 } 1608 MOZ_ASSERT(mDomSelections[index]); 1609 return mDomSelections[index]; 1610 } 1611 1612 void nsFrameSelection::AddHighlightSelection( 1613 nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight) { 1614 RefPtr<Selection> selection = 1615 aHighlight.CreateHighlightSelection(aHighlightName, this); 1616 if (auto iter = 1617 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(), 1618 [&aHighlightName](auto const& aElm) { 1619 return aElm.first() == aHighlightName; 1620 }); 1621 iter != mHighlightSelections.end()) { 1622 iter->second() = std::move(selection); 1623 } else { 1624 mHighlightSelections.AppendElement( 1625 CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName, 1626 std::move(selection))); 1627 } 1628 } 1629 1630 void nsFrameSelection::RepaintHighlightSelection(nsAtom* aHighlightName) { 1631 if (auto iter = 1632 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(), 1633 [&aHighlightName](auto const& aElm) { 1634 return aElm.first() == aHighlightName; 1635 }); 1636 iter != mHighlightSelections.end()) { 1637 RefPtr selection = iter->second(); 1638 selection->Repaint(mPresShell->GetPresContext()); 1639 } 1640 } 1641 1642 void nsFrameSelection::RemoveHighlightSelection(nsAtom* aHighlightName) { 1643 if (auto iter = 1644 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(), 1645 [&aHighlightName](auto const& aElm) { 1646 return aElm.first() == aHighlightName; 1647 }); 1648 iter != mHighlightSelections.end()) { 1649 RefPtr<Selection> selection = iter->second(); 1650 selection->RemoveAllRanges(IgnoreErrors()); 1651 mHighlightSelections.RemoveElementAt(iter); 1652 } 1653 } 1654 1655 void nsFrameSelection::AddHighlightSelectionRange( 1656 nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight, 1657 mozilla::dom::AbstractRange& aRange) { 1658 if (auto iter = 1659 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(), 1660 [&aHighlightName](auto const& aElm) { 1661 return aElm.first() == aHighlightName; 1662 }); 1663 iter != mHighlightSelections.end()) { 1664 RefPtr<Selection> selection = iter->second(); 1665 selection->AddHighlightRangeAndSelectFramesAndNotifyListeners(aRange); 1666 } else { 1667 // if the selection does not exist yet, add all of its ranges and exit. 1668 RefPtr<Selection> selection = 1669 aHighlight.CreateHighlightSelection(aHighlightName, this); 1670 mHighlightSelections.AppendElement( 1671 CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName, 1672 std::move(selection))); 1673 } 1674 } 1675 1676 void nsFrameSelection::RemoveHighlightSelectionRange( 1677 nsAtom* aHighlightName, mozilla::dom::AbstractRange& aRange) { 1678 if (auto iter = 1679 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(), 1680 [&aHighlightName](auto const& aElm) { 1681 return aElm.first() == aHighlightName; 1682 }); 1683 iter != mHighlightSelections.end()) { 1684 // NOLINTNEXTLINE(performance-unnecessary-copy-initialization) 1685 RefPtr<Selection> selection = iter->second(); 1686 selection->RemoveRangeAndUnselectFramesAndNotifyListeners(aRange, 1687 IgnoreErrors()); 1688 } 1689 } 1690 1691 nsresult nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType, 1692 SelectionRegion aRegion, 1693 int16_t aFlags) const { 1694 RefPtr<Selection> sel = GetSelection(aSelectionType); 1695 if (!sel) { 1696 return NS_ERROR_INVALID_ARG; 1697 } 1698 1699 const auto vScroll = [&]() -> WhereToScroll { 1700 if (aFlags & nsISelectionController::SCROLL_VERTICAL_START) { 1701 return WhereToScroll::Start; 1702 } 1703 if (aFlags & nsISelectionController::SCROLL_VERTICAL_END) { 1704 return WhereToScroll::End; 1705 } 1706 if (aFlags & nsISelectionController::SCROLL_VERTICAL_CENTER) { 1707 return WhereToScroll::Center; 1708 } 1709 return WhereToScroll::Nearest; 1710 }(); 1711 1712 auto mode = aFlags & nsISelectionController::SCROLL_SYNCHRONOUS 1713 ? SelectionScrollMode::SyncFlush 1714 : SelectionScrollMode::Async; 1715 1716 auto scrollFlags = ScrollFlags::None; 1717 if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) { 1718 scrollFlags |= ScrollFlags::ScrollOverflowHidden; 1719 } 1720 1721 // After ScrollSelectionIntoView(), the pending notifications might be 1722 // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. 1723 return sel->ScrollIntoView(aRegion, ScrollAxis(vScroll), ScrollAxis(), 1724 scrollFlags, mode); 1725 } 1726 1727 nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) { 1728 RefPtr<Selection> sel = GetSelection(aSelectionType); 1729 if (!sel) { 1730 return NS_ERROR_INVALID_ARG; 1731 } 1732 if (!mPresShell) { 1733 return NS_ERROR_UNEXPECTED; 1734 } 1735 1736 // On macOS, update the selection cache to the new active selection 1737 // aka the current selection. 1738 #ifdef XP_MACOSX 1739 // Check that we're in the an active window and, if this is Web content, 1740 // in the frontmost tab. 1741 Document* doc = mPresShell->GetDocument(); 1742 if (doc && IsInActiveTab(doc) && aSelectionType == SelectionType::eNormal) { 1743 UpdateSelectionCacheOnRepaintSelection(sel); 1744 } 1745 #endif 1746 return sel->Repaint(mPresShell->GetPresContext()); 1747 } 1748 1749 nsIFrame* nsFrameSelection::GetFrameToPageSelect() const { 1750 if (NS_WARN_IF(!mPresShell)) { 1751 return nullptr; 1752 } 1753 1754 nsIFrame* rootFrameToSelect; 1755 if (mLimiters.mIndependentSelectionRootElement) { 1756 rootFrameToSelect = 1757 mLimiters.mIndependentSelectionRootElement->GetPrimaryFrame(); 1758 if (NS_WARN_IF(!rootFrameToSelect)) { 1759 return nullptr; 1760 } 1761 } else if (mLimiters.mAncestorLimiter) { 1762 rootFrameToSelect = mLimiters.mAncestorLimiter->GetPrimaryFrame(); 1763 if (NS_WARN_IF(!rootFrameToSelect)) { 1764 return nullptr; 1765 } 1766 } else { 1767 rootFrameToSelect = mPresShell->GetRootScrollContainerFrame(); 1768 if (NS_WARN_IF(!rootFrameToSelect)) { 1769 return nullptr; 1770 } 1771 } 1772 1773 nsCOMPtr<nsIContent> contentToSelect = mPresShell->GetContentForScrolling(); 1774 if (contentToSelect) { 1775 // If there is selected content, look for nearest and vertical scrollable 1776 // parent under the root frame. 1777 for (nsIFrame* frame = contentToSelect->GetPrimaryFrame(); 1778 frame && frame != rootFrameToSelect; frame = frame->GetParent()) { 1779 ScrollContainerFrame* scrollContainerFrame = do_QueryFrame(frame); 1780 if (!scrollContainerFrame) { 1781 continue; 1782 } 1783 ScrollStyles scrollStyles = scrollContainerFrame->GetScrollStyles(); 1784 if (scrollStyles.mVertical == StyleOverflow::Hidden) { 1785 continue; 1786 } 1787 layers::ScrollDirections directions = 1788 scrollContainerFrame->GetAvailableScrollingDirections(); 1789 if (directions.contains(layers::ScrollDirection::eVertical)) { 1790 // If there is sub scrollable frame, let's use its page size to select. 1791 return frame; 1792 } 1793 } 1794 } 1795 // Otherwise, i.e., there is no scrollable frame or only the root frame is 1796 // scrollable, let's return the root frame because Shift + PageUp/PageDown 1797 // should expand the selection in the root content even if it's not 1798 // scrollable. 1799 return rootFrameToSelect; 1800 } 1801 1802 nsresult nsFrameSelection::PageMove(bool aForward, bool aExtend, 1803 nsIFrame* aFrame, 1804 SelectionIntoView aSelectionIntoView) { 1805 MOZ_ASSERT(aFrame); 1806 1807 // expected behavior for PageMove is to scroll AND move the caret 1808 // and remain relative position of the caret in view. see Bug 4302. 1809 1810 // Get the scroll container frame. If aFrame is not scrollable, this is 1811 // nullptr. 1812 ScrollContainerFrame* scrollContainerFrame = aFrame->GetScrollTargetFrame(); 1813 // Get the scrolled frame. If aFrame is not scrollable, this is aFrame 1814 // itself. 1815 nsIFrame* scrolledFrame = 1816 scrollContainerFrame ? scrollContainerFrame->GetScrolledFrame() : aFrame; 1817 if (!scrolledFrame) { 1818 return NS_OK; 1819 } 1820 1821 // find out where the caret is. 1822 // we should know mDesiredCaretPos.mValue value of nsFrameSelection, but I 1823 // havent seen that behavior in other windows applications yet. 1824 RefPtr<Selection> selection = &NormalSelection(); 1825 if (!selection) { 1826 return NS_OK; 1827 } 1828 1829 nsRect caretPos; 1830 nsIFrame* caretFrame = nsCaret::GetGeometry(selection, &caretPos); 1831 if (!caretFrame) { 1832 return NS_OK; 1833 } 1834 1835 // If the scrolled frame is outside of current selection limiter, 1836 // we need to scroll the frame but keep moving selection in the limiter. 1837 nsIFrame* frameToClick = scrolledFrame; 1838 if (!NodeIsInLimiters(scrolledFrame->GetContent())) { 1839 frameToClick = GetFrameToPageSelect(); 1840 if (NS_WARN_IF(!frameToClick)) { 1841 return NS_OK; 1842 } 1843 } 1844 1845 if (scrollContainerFrame) { 1846 // If there is a scrollable frame, adjust pseudo-click position with page 1847 // scroll amount. 1848 // XXX This may scroll more than one page if ScrollSelectionIntoView is 1849 // called later because caret may not fully visible. E.g., if 1850 // clicking line will be visible only half height with scrolling 1851 // the frame, ScrollSelectionIntoView additionally scrolls to show 1852 // the caret entirely. 1853 if (aForward) { 1854 caretPos.y += scrollContainerFrame->GetPageScrollAmount().height; 1855 } else { 1856 caretPos.y -= scrollContainerFrame->GetPageScrollAmount().height; 1857 } 1858 } else { 1859 // Otherwise, adjust pseudo-click position with the frame size. 1860 if (aForward) { 1861 caretPos.y += frameToClick->GetSize().height; 1862 } else { 1863 caretPos.y -= frameToClick->GetSize().height; 1864 } 1865 } 1866 1867 caretPos += caretFrame->GetOffsetTo(frameToClick); 1868 1869 // get a content at desired location 1870 nsPoint desiredPoint; 1871 desiredPoint.x = caretPos.x; 1872 desiredPoint.y = caretPos.y + caretPos.height / 2; 1873 nsIFrame::ContentOffsets offsets = 1874 frameToClick->GetContentOffsetsFromPoint(desiredPoint); 1875 1876 if (!offsets.content) { 1877 // XXX Do we need to handle ScrollSelectionIntoView in this case? 1878 return NS_OK; 1879 } 1880 1881 // First, place the caret. 1882 bool selectionChanged; 1883 { 1884 // We don't want any script to run until we check whether selection is 1885 // modified by HandleClick. 1886 SelectionBatcher ensureNoSelectionChangeNotifications(selection, 1887 __FUNCTION__); 1888 1889 RangeBoundary oldAnchor = selection->AnchorRef(); 1890 RangeBoundary oldFocus = selection->FocusRef(); 1891 1892 const FocusMode focusMode = 1893 aExtend ? FocusMode::kExtendSelection : FocusMode::kCollapseToNewPoint; 1894 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, 1895 offsets.offset, offsets.offset, focusMode, 1896 CaretAssociationHint::After); 1897 1898 selectionChanged = selection->AnchorRef() != oldAnchor || 1899 selection->FocusRef() != oldFocus; 1900 } 1901 1902 bool doScrollSelectionIntoView = !( 1903 aSelectionIntoView == SelectionIntoView::IfChanged && !selectionChanged); 1904 1905 // Then, scroll the given frame one page. 1906 if (scrollContainerFrame) { 1907 // If we'll call ScrollSelectionIntoView later and selection wasn't 1908 // changed and we scroll outside of selection limiter, we shouldn't use 1909 // smooth scroll here because ScrollContainerFrame uses normal runnable, 1910 // but ScrollSelectionIntoView uses early runner and it cancels the 1911 // pending smooth scroll. Therefore, if we used smooth scroll in such 1912 // case, ScrollSelectionIntoView would scroll to show caret instead of 1913 // page scroll of an element outside selection limiter. 1914 ScrollMode scrollMode = doScrollSelectionIntoView && !selectionChanged && 1915 scrolledFrame != frameToClick 1916 ? ScrollMode::Instant 1917 : ScrollMode::Smooth; 1918 scrollContainerFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), 1919 ScrollUnit::PAGES, scrollMode); 1920 } 1921 1922 // Finally, scroll selection into view if requested. 1923 if (!doScrollSelectionIntoView) { 1924 return NS_OK; 1925 } 1926 return ScrollSelectionIntoView(SelectionType::eNormal, 1927 nsISelectionController::SELECTION_FOCUS_REGION, 1928 nsISelectionController::SCROLL_SYNCHRONOUS); 1929 } 1930 1931 nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount, 1932 bool aExtend) { 1933 NS_ENSURE_STATE(mPresShell); 1934 // Flush out layout, since we need it to be up to date to do caret 1935 // positioning. 1936 OwningNonNull<PresShell> presShell(*mPresShell); 1937 presShell->FlushPendingNotifications(FlushType::Layout); 1938 1939 if (!mPresShell) { 1940 return NS_OK; 1941 } 1942 1943 // Check that parameters are safe 1944 if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) { 1945 return NS_ERROR_FAILURE; 1946 } 1947 1948 nsPresContext* context = mPresShell->GetPresContext(); 1949 if (!context) { 1950 return NS_ERROR_FAILURE; 1951 } 1952 1953 RefPtr<Selection> sel = &NormalSelection(); 1954 1955 // Map the abstract movement amounts (0-1) to direction-specific 1956 // selection units. 1957 static const nsSelectionAmount inlineAmount[] = {eSelectCluster, eSelectWord}; 1958 static const nsSelectionAmount blockPrevAmount[] = {eSelectLine, 1959 eSelectBeginLine}; 1960 static const nsSelectionAmount blockNextAmount[] = {eSelectLine, 1961 eSelectEndLine}; 1962 1963 struct PhysicalToLogicalMapping { 1964 nsDirection direction; 1965 const nsSelectionAmount* amounts; 1966 }; 1967 static const PhysicalToLogicalMapping verticalLR[4] = { 1968 {eDirPrevious, blockPrevAmount}, // left 1969 {eDirNext, blockNextAmount}, // right 1970 {eDirPrevious, inlineAmount}, // up 1971 {eDirNext, inlineAmount} // down 1972 }; 1973 static const PhysicalToLogicalMapping verticalRL[4] = { 1974 {eDirNext, blockNextAmount}, 1975 {eDirPrevious, blockPrevAmount}, 1976 {eDirPrevious, inlineAmount}, 1977 {eDirNext, inlineAmount}}; 1978 static const PhysicalToLogicalMapping horizontal[4] = { 1979 {eDirPrevious, inlineAmount}, 1980 {eDirNext, inlineAmount}, 1981 {eDirPrevious, blockPrevAmount}, 1982 {eDirNext, blockNextAmount}}; 1983 1984 WritingMode wm; 1985 const PrimaryFrameData frameForFocus = 1986 sel->GetPrimaryFrameForCaretAtFocusNode(true); 1987 if (frameForFocus) { 1988 // FYI: Setting the caret association hint was done during a call of 1989 // GetPrimaryFrameForCaretAtFocusNode. Therefore, this may not be intended 1990 // by the original author. 1991 sel->GetFrameSelection()->SetHint(frameForFocus.mHint); 1992 1993 if (!frameForFocus->Style()->IsTextCombined()) { 1994 wm = frameForFocus->GetWritingMode(); 1995 } else { 1996 // Using different direction for horizontal-in-vertical would 1997 // make it hard to navigate via keyboard. Inherit the moving 1998 // direction from its parent. 1999 MOZ_ASSERT(frameForFocus->IsTextFrame()); 2000 wm = frameForFocus->GetParent()->GetWritingMode(); 2001 MOZ_ASSERT(wm.IsVertical(), 2002 "Text combined " 2003 "can only appear in vertical text"); 2004 } 2005 } 2006 2007 const PhysicalToLogicalMapping& mapping = 2008 wm.IsVertical() 2009 ? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection] 2010 : horizontal[aDirection]; 2011 2012 nsresult rv = MoveCaret(mapping.direction, ExtendSelection(aExtend), 2013 mapping.amounts[aAmount], eVisual); 2014 if (NS_FAILED(rv)) { 2015 // If we tried to do a line move, but couldn't move in the given direction, 2016 // then we'll "promote" this to a line-edge move instead. 2017 if (mapping.amounts[aAmount] == eSelectLine) { 2018 rv = MoveCaret(mapping.direction, ExtendSelection(aExtend), 2019 mapping.amounts[aAmount + 1], eVisual); 2020 } 2021 // And if it was a next-word move that failed (which can happen when 2022 // eat_space_to_next_word is true, see bug 1153237), then just move forward 2023 // to the line-edge. 2024 else if (mapping.amounts[aAmount] == eSelectWord && 2025 mapping.direction == eDirNext) { 2026 rv = MoveCaret(eDirNext, ExtendSelection(aExtend), eSelectEndLine, 2027 eVisual); 2028 } 2029 } 2030 2031 return rv; 2032 } 2033 2034 nsresult nsFrameSelection::CharacterMove(bool aForward, bool aExtend) { 2035 return MoveCaret(aForward ? eDirNext : eDirPrevious, ExtendSelection(aExtend), 2036 eSelectCluster, eUsePrefStyle); 2037 } 2038 2039 nsresult nsFrameSelection::WordMove(bool aForward, bool aExtend) { 2040 return MoveCaret(aForward ? eDirNext : eDirPrevious, ExtendSelection(aExtend), 2041 eSelectWord, eUsePrefStyle); 2042 } 2043 2044 nsresult nsFrameSelection::LineMove(bool aForward, bool aExtend) { 2045 return MoveCaret(aForward ? eDirNext : eDirPrevious, ExtendSelection(aExtend), 2046 eSelectLine, eUsePrefStyle); 2047 } 2048 2049 nsresult nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) { 2050 if (aForward) { 2051 return MoveCaret(eDirNext, ExtendSelection(aExtend), eSelectEndLine, 2052 eLogical); 2053 } 2054 return MoveCaret(eDirPrevious, ExtendSelection(aExtend), eSelectBeginLine, 2055 eLogical); 2056 } 2057 2058 // static 2059 template <typename RangeType> 2060 Result<RefPtr<RangeType>, nsresult> 2061 nsFrameSelection::CreateRangeExtendedToSomewhere( 2062 PresShell& aPresShell, 2063 const mozilla::LimitersAndCaretData& aLimitersAndCaretData, 2064 const AbstractRange& aRange, nsDirection aRangeDirection, 2065 nsDirection aExtendDirection, nsSelectionAmount aAmount, 2066 CaretMovementStyle aMovementStyle) { 2067 MOZ_ASSERT(aRangeDirection == eDirNext || aRangeDirection == eDirPrevious); 2068 MOZ_ASSERT(aExtendDirection == eDirNext || aExtendDirection == eDirPrevious); 2069 MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster || 2070 aAmount == eSelectWord || aAmount == eSelectBeginLine || 2071 aAmount == eSelectEndLine); 2072 MOZ_ASSERT(aMovementStyle == eLogical || aMovementStyle == eVisual || 2073 aMovementStyle == eUsePrefStyle); 2074 2075 aPresShell.FlushPendingNotifications(FlushType::Layout); 2076 if (aPresShell.IsDestroying()) { 2077 return Err(NS_ERROR_FAILURE); 2078 } 2079 if (!aRange.IsPositioned()) { 2080 return Err(NS_ERROR_FAILURE); 2081 } 2082 const ForceEditableRegion forceEditableRegion = [&]() { 2083 if (aRange.GetStartContainer()->IsEditable()) { 2084 return ForceEditableRegion::Yes; 2085 } 2086 const auto* const element = Element::FromNode(aRange.GetStartContainer()); 2087 return element && element->State().HasState(ElementState::READWRITE) 2088 ? ForceEditableRegion::Yes 2089 : ForceEditableRegion::No; 2090 }(); 2091 Result<PeekOffsetOptions, nsresult> options = 2092 CreatePeekOffsetOptionsForCaretMove( 2093 aLimitersAndCaretData.mIndependentSelectionRootElement, 2094 forceEditableRegion, ExtendSelection::Yes, aMovementStyle); 2095 if (MOZ_UNLIKELY(options.isErr())) { 2096 return options.propagateErr(); 2097 } 2098 Result<RawRangeBoundary, nsresult> result = 2099 SelectionMovementUtils::MoveRangeBoundaryToSomewhere( 2100 aRangeDirection == eDirNext ? aRange.StartRef().AsRaw() 2101 : aRange.EndRef().AsRaw(), 2102 aExtendDirection, aLimitersAndCaretData.mCaretAssociationHint, 2103 aLimitersAndCaretData.mCaretBidiLevel, aAmount, options.unwrap(), 2104 aLimitersAndCaretData.mAncestorLimiter); 2105 if (result.isErr()) { 2106 return result.propagateErr(); 2107 } 2108 RefPtr<RangeType> range; 2109 RawRangeBoundary rangeBoundary = result.unwrap(); 2110 if (!rangeBoundary.IsSetAndValid()) { 2111 return range; 2112 } 2113 if (aExtendDirection == eDirPrevious) { 2114 range = RangeType::Create(rangeBoundary, aRange.EndRef(), IgnoreErrors()); 2115 } else { 2116 range = RangeType::Create(aRange.StartRef(), rangeBoundary, IgnoreErrors()); 2117 } 2118 return range; 2119 } 2120 2121 //////////END FRAMESELECTION 2122 2123 LazyLogModule gBatchLog("SelectionBatch"); 2124 2125 void nsFrameSelection::StartBatchChanges(const char* aRequesterFuncName) { 2126 MOZ_LOG(gBatchLog, LogLevel::Info, 2127 ("%p%snsFrameSelection::StartBatchChanges(%s)", this, 2128 std::string((mBatching.mCounter + 1) * 2, ' ').c_str(), 2129 aRequesterFuncName)); 2130 mBatching.mCounter++; 2131 } 2132 2133 void nsFrameSelection::EndBatchChanges(const char* aRequesterFuncName, 2134 int16_t aReasons) { 2135 MOZ_LOG(gBatchLog, LogLevel::Info, 2136 ("%p%snsFrameSelection::EndBatchChanges (%s, %s)", this, 2137 std::string(mBatching.mCounter * 2, ' ').c_str(), aRequesterFuncName, 2138 SelectionChangeReasonsToCString(aReasons).get())); 2139 MOZ_ASSERT(mBatching.mCounter > 0, "Bad mBatching.mCounter"); 2140 mBatching.mCounter--; 2141 2142 if (mBatching.mCounter == 0) { 2143 AddChangeReasons(aReasons); 2144 mCaretMoveAmount = eSelectNoAmount; 2145 // Be aware, the Selection instance may be destroyed after this call, 2146 // hence make sure that this instance remains until the end of this call. 2147 RefPtr frameSelection = this; 2148 for (auto selectionType : kPresentSelectionTypes) { 2149 // This returns NS_ERROR_FAILURE if being called for a selection that is 2150 // not present. We don't care about that here, so we silently ignore it 2151 // and continue. 2152 (void)NotifySelectionListeners(selectionType, IsBatchingEnd::Yes); 2153 } 2154 } 2155 } 2156 2157 nsresult nsFrameSelection::NotifySelectionListeners( 2158 SelectionType aSelectionType, IsBatchingEnd aEndBatching) { 2159 if (RefPtr<Selection> selection = GetSelection(aSelectionType)) { 2160 if (aEndBatching == IsBatchingEnd::Yes && 2161 !selection->ChangesDuringBatching()) { 2162 return NS_OK; 2163 } 2164 selection->NotifySelectionListeners(); 2165 mCaretMoveAmount = eSelectNoAmount; 2166 return NS_OK; 2167 } 2168 return NS_ERROR_FAILURE; 2169 } 2170 2171 // Start of Table Selection methods 2172 2173 static bool IsCell(nsIContent* aContent) { 2174 return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th); 2175 } 2176 2177 // static 2178 nsITableCellLayout* nsFrameSelection::GetCellLayout( 2179 const nsIContent* aCellContent) { 2180 nsITableCellLayout* cellLayoutObject = 2181 do_QueryFrame(aCellContent->GetPrimaryFrame()); 2182 return cellLayoutObject; 2183 } 2184 2185 nsresult nsFrameSelection::ClearNormalSelection() { 2186 RefPtr<Selection> selection = &NormalSelection(); 2187 ErrorResult err; 2188 selection->RemoveAllRanges(err); 2189 return err.StealNSResult(); 2190 } 2191 2192 static nsIContent* GetFirstSelectedContent(const nsRange* aRange) { 2193 if (!aRange) { 2194 return nullptr; 2195 } 2196 2197 MOZ_ASSERT(aRange->GetStartContainer(), "Must have start parent!"); 2198 MOZ_ASSERT(aRange->GetStartContainer()->IsElement(), "Unexpected parent"); 2199 2200 return aRange->GetChildAtStartOffset(); 2201 } 2202 2203 // Table selection support. 2204 // TODO: Separate table methods into a separate nsITableSelection interface 2205 nsresult nsFrameSelection::HandleTableSelection(nsINode* aParentContent, 2206 int32_t aContentOffset, 2207 TableSelectionMode aTarget, 2208 WidgetMouseEvent* aMouseEvent) { 2209 RefPtr<Selection> selection = &NormalSelection(); 2210 return mTableSelection.HandleSelection(aParentContent, aContentOffset, 2211 aTarget, aMouseEvent, mDragState, 2212 *selection); 2213 } 2214 2215 nsresult nsFrameSelection::TableSelection::HandleSelection( 2216 nsINode* aParentContent, int32_t aContentOffset, TableSelectionMode aTarget, 2217 WidgetMouseEvent* aMouseEvent, bool aDragState, 2218 Selection& aNormalSelection) { 2219 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 2220 2221 NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER); 2222 NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER); 2223 2224 if (aDragState && mDragSelectingCells && 2225 aTarget == TableSelectionMode::Table) { 2226 // We were selecting cells and user drags mouse in table border or inbetween 2227 // cells, 2228 // just do nothing 2229 return NS_OK; 2230 } 2231 2232 RefPtr<nsIContent> childContent = 2233 aParentContent->GetChildAt_Deprecated(aContentOffset); 2234 2235 // When doing table selection, always set the direction to next so 2236 // we can be sure that anchorNode's offset always points to the 2237 // selected cell 2238 aNormalSelection.SetDirection(eDirNext); 2239 2240 // Stack-class to wrap all table selection changes in 2241 // BeginBatchChanges() / EndBatchChanges() 2242 SelectionBatcher selectionBatcher(&aNormalSelection, __FUNCTION__); 2243 2244 if (aDragState && mDragSelectingCells) { 2245 return HandleDragSelecting(aTarget, childContent, aMouseEvent, 2246 aNormalSelection); 2247 } 2248 2249 return HandleMouseUpOrDown(aTarget, aDragState, childContent, aParentContent, 2250 aContentOffset, aMouseEvent, aNormalSelection); 2251 } 2252 2253 class nsFrameSelection::TableSelection::RowAndColumnRelation { 2254 public: 2255 static Result<RowAndColumnRelation, nsresult> Create( 2256 const nsIContent* aFirst, const nsIContent* aSecond) { 2257 RowAndColumnRelation result; 2258 2259 nsresult errorResult = 2260 GetCellIndexes(aFirst, result.mFirst.mRow, result.mFirst.mColumn); 2261 if (NS_FAILED(errorResult)) { 2262 return Err(errorResult); 2263 } 2264 2265 errorResult = 2266 GetCellIndexes(aSecond, result.mSecond.mRow, result.mSecond.mColumn); 2267 if (NS_FAILED(errorResult)) { 2268 return Err(errorResult); 2269 } 2270 2271 return result; 2272 } 2273 2274 bool IsSameColumn() const { return mFirst.mColumn == mSecond.mColumn; } 2275 2276 bool IsSameRow() const { return mFirst.mRow == mSecond.mRow; } 2277 2278 private: 2279 RowAndColumnRelation() = default; 2280 2281 struct RowAndColumn { 2282 int32_t mRow = 0; 2283 int32_t mColumn = 0; 2284 }; 2285 2286 RowAndColumn mFirst; 2287 RowAndColumn mSecond; 2288 }; 2289 2290 nsresult nsFrameSelection::TableSelection::HandleDragSelecting( 2291 TableSelectionMode aTarget, nsIContent* aChildContent, 2292 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) { 2293 // We are drag-selecting 2294 if (aTarget != TableSelectionMode::Table) { 2295 // If dragging in the same cell as last event, do nothing 2296 if (mEndSelectedCell == aChildContent) { 2297 return NS_OK; 2298 } 2299 2300 #ifdef DEBUG_TABLE_SELECTION 2301 printf( 2302 " mStartSelectedCell = %p, " 2303 "mEndSelectedCell = %p, aChildContent = %p " 2304 "\n", 2305 mStartSelectedCell.get(), mEndSelectedCell.get(), aChildContent); 2306 #endif 2307 // aTarget can be any "cell mode", 2308 // so we can easily drag-select rows and columns 2309 // Once we are in row or column mode, 2310 // we can drift into any cell to stay in that mode 2311 // even if aTarget = TableSelectionMode::Cell 2312 2313 if (mMode == TableSelectionMode::Row || 2314 mMode == TableSelectionMode::Column) { 2315 if (mEndSelectedCell) { 2316 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation = 2317 RowAndColumnRelation::Create(mEndSelectedCell, aChildContent); 2318 2319 if (rowAndColumnRelation.isErr()) { 2320 return rowAndColumnRelation.unwrapErr(); 2321 } 2322 2323 if ((mMode == TableSelectionMode::Row && 2324 rowAndColumnRelation.inspect().IsSameRow()) || 2325 (mMode == TableSelectionMode::Column && 2326 rowAndColumnRelation.inspect().IsSameColumn())) { 2327 return NS_OK; 2328 } 2329 } 2330 #ifdef DEBUG_TABLE_SELECTION 2331 printf(" Dragged into a new column or row\n"); 2332 #endif 2333 // Continue dragging row or column selection 2334 2335 return SelectRowOrColumn(aChildContent, aNormalSelection); 2336 } 2337 if (mMode == TableSelectionMode::Cell) { 2338 #ifdef DEBUG_TABLE_SELECTION 2339 printf("HandleTableSelection: Dragged into a new cell\n"); 2340 #endif 2341 // Trick for quick selection of rows and columns 2342 // Hold down shift, then start selecting in one direction 2343 // If next cell dragged into is in same row, select entire row, 2344 // if next cell is in same column, select entire column 2345 if (mStartSelectedCell && aMouseEvent->IsShift()) { 2346 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation = 2347 RowAndColumnRelation::Create(mStartSelectedCell, aChildContent); 2348 if (rowAndColumnRelation.isErr()) { 2349 return rowAndColumnRelation.unwrapErr(); 2350 } 2351 2352 if (rowAndColumnRelation.inspect().IsSameRow() || 2353 rowAndColumnRelation.inspect().IsSameColumn()) { 2354 // Force new selection block 2355 mStartSelectedCell = nullptr; 2356 aNormalSelection.RemoveAllRanges(IgnoreErrors()); 2357 2358 if (rowAndColumnRelation.inspect().IsSameRow()) { 2359 mMode = TableSelectionMode::Row; 2360 } else { 2361 mMode = TableSelectionMode::Column; 2362 } 2363 2364 return SelectRowOrColumn(aChildContent, aNormalSelection); 2365 } 2366 } 2367 2368 // Reselect block of cells to new end location 2369 const nsCOMPtr<nsIContent> startSelectedCell = mStartSelectedCell; 2370 return SelectBlockOfCells(startSelectedCell, aChildContent, 2371 aNormalSelection); 2372 } 2373 } 2374 // Do nothing if dragging in table, but outside a cell 2375 return NS_OK; 2376 } 2377 2378 nsresult nsFrameSelection::TableSelection::HandleMouseUpOrDown( 2379 TableSelectionMode aTarget, bool aDragState, nsIContent* aChildContent, 2380 nsINode* aParentContent, int32_t aContentOffset, 2381 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) { 2382 nsresult result = NS_OK; 2383 // Not dragging -- mouse event is down or up 2384 if (aDragState) { 2385 #ifdef DEBUG_TABLE_SELECTION 2386 printf("HandleTableSelection: Mouse down event\n"); 2387 #endif 2388 // Clear cell we stored in mouse-down 2389 mUnselectCellOnMouseUp = nullptr; 2390 2391 if (aTarget == TableSelectionMode::Cell) { 2392 bool isSelected = false; 2393 2394 // Check if we have other selected cells 2395 nsIContent* previousCellNode = 2396 GetFirstSelectedContent(GetFirstCellRange(aNormalSelection)); 2397 if (previousCellNode) { 2398 // We have at least 1 other selected cell 2399 2400 // Check if new cell is already selected 2401 nsIFrame* cellFrame = aChildContent->GetPrimaryFrame(); 2402 if (!cellFrame) { 2403 return NS_ERROR_NULL_POINTER; 2404 } 2405 isSelected = cellFrame->IsSelected(); 2406 } else { 2407 // No cells selected -- remove non-cell selection 2408 aNormalSelection.RemoveAllRanges(IgnoreErrors()); 2409 } 2410 mDragSelectingCells = true; // Signal to start drag-cell-selection 2411 mMode = aTarget; 2412 // Set start for new drag-selection block (not appended) 2413 mStartSelectedCell = aChildContent; 2414 // The initial block end is same as the start 2415 mEndSelectedCell = aChildContent; 2416 2417 if (isSelected) { 2418 // Remember this cell to (possibly) unselect it on mouseup 2419 mUnselectCellOnMouseUp = aChildContent; 2420 #ifdef DEBUG_TABLE_SELECTION 2421 printf( 2422 "HandleTableSelection: Saving " 2423 "mUnselectCellOnMouseUp\n"); 2424 #endif 2425 } else { 2426 // Select an unselected cell 2427 // but first remove existing selection if not in same table 2428 if (previousCellNode && 2429 !IsInSameTable(previousCellNode, aChildContent)) { 2430 aNormalSelection.RemoveAllRanges(IgnoreErrors()); 2431 // Reset selection mode that is cleared in RemoveAllRanges 2432 mMode = aTarget; 2433 } 2434 2435 return ::SelectCellElement(aChildContent, aNormalSelection); 2436 } 2437 2438 return NS_OK; 2439 } 2440 if (aTarget == TableSelectionMode::Table) { 2441 // TODO: We currently select entire table when clicked between cells, 2442 // should we restrict to only around border? 2443 // *** How do we get location data for cell and click? 2444 mDragSelectingCells = false; 2445 mStartSelectedCell = nullptr; 2446 mEndSelectedCell = nullptr; 2447 2448 // Remove existing selection and select the table 2449 aNormalSelection.RemoveAllRanges(IgnoreErrors()); 2450 return CreateAndAddRange(aParentContent, aContentOffset, 2451 aNormalSelection); 2452 } 2453 if (aTarget == TableSelectionMode::Row || 2454 aTarget == TableSelectionMode::Column) { 2455 #ifdef DEBUG_TABLE_SELECTION 2456 printf("aTarget == %d\n", aTarget); 2457 #endif 2458 2459 // Start drag-selecting mode so multiple rows/cols can be selected 2460 // Note: Currently, nsIFrame::GetDataForTableSelection 2461 // will never call us for row or column selection on mouse down 2462 mDragSelectingCells = true; 2463 2464 // Force new selection block 2465 mStartSelectedCell = nullptr; 2466 aNormalSelection.RemoveAllRanges(IgnoreErrors()); 2467 // Always do this AFTER RemoveAllRanges 2468 mMode = aTarget; 2469 2470 return SelectRowOrColumn(aChildContent, aNormalSelection); 2471 } 2472 } else { 2473 #ifdef DEBUG_TABLE_SELECTION 2474 printf( 2475 "HandleTableSelection: Mouse UP event. " 2476 "mDragSelectingCells=%d, " 2477 "mStartSelectedCell=%p\n", 2478 mDragSelectingCells, mStartSelectedCell.get()); 2479 #endif 2480 // First check if we are extending a block selection 2481 const uint32_t rangeCount = aNormalSelection.RangeCount(); 2482 2483 if (rangeCount > 0 && aMouseEvent->IsShift() && mAppendStartSelectedCell && 2484 mAppendStartSelectedCell != aChildContent) { 2485 // Shift key is down: append a block selection 2486 mDragSelectingCells = false; 2487 2488 const OwningNonNull<nsIContent> appendStartSelectedCell = 2489 *mAppendStartSelectedCell; 2490 return SelectBlockOfCells(appendStartSelectedCell, aChildContent, 2491 aNormalSelection); 2492 } 2493 2494 if (mDragSelectingCells) { 2495 mAppendStartSelectedCell = mStartSelectedCell; 2496 } 2497 2498 mDragSelectingCells = false; 2499 mStartSelectedCell = nullptr; 2500 mEndSelectedCell = nullptr; 2501 2502 // Any other mouseup actions require that Ctrl or Cmd key is pressed 2503 // else stop table selection mode 2504 bool doMouseUpAction = false; 2505 #ifdef XP_MACOSX 2506 doMouseUpAction = aMouseEvent->IsMeta(); 2507 #else 2508 doMouseUpAction = aMouseEvent->IsControl(); 2509 #endif 2510 if (!doMouseUpAction) { 2511 #ifdef DEBUG_TABLE_SELECTION 2512 printf( 2513 "HandleTableSelection: Ending cell selection on mouseup: " 2514 "mAppendStartSelectedCell=%p\n", 2515 mAppendStartSelectedCell.get()); 2516 #endif 2517 return NS_OK; 2518 } 2519 // Unselect a cell only if it wasn't 2520 // just selected on mousedown 2521 if (aChildContent == mUnselectCellOnMouseUp) { 2522 // Scan ranges to find the cell to unselect (the selection range to 2523 // remove) 2524 // XXXbz it's really weird that this lives outside the loop, so once we 2525 // find one we keep looking at it even if we find no more cells... 2526 nsINode* previousCellParent = nullptr; 2527 #ifdef DEBUG_TABLE_SELECTION 2528 printf( 2529 "HandleTableSelection: Unselecting " 2530 "mUnselectCellOnMouseUp; " 2531 "rangeCount=%d\n", 2532 rangeCount); 2533 #endif 2534 for (const uint32_t i : IntegerRange(rangeCount)) { 2535 MOZ_ASSERT(aNormalSelection.RangeCount() == rangeCount); 2536 // Strong reference, because sometimes we want to remove 2537 // this range, and then we might be the only owner. 2538 RefPtr<nsRange> range = aNormalSelection.GetRangeAt(i); 2539 if (MOZ_UNLIKELY(!range)) { 2540 return NS_ERROR_NULL_POINTER; 2541 } 2542 2543 nsINode* container = range->GetStartContainer(); 2544 if (!container) { 2545 return NS_ERROR_NULL_POINTER; 2546 } 2547 2548 int32_t offset = range->StartOffset(); 2549 // Be sure previous selection is a table cell 2550 nsIContent* child = range->GetChildAtStartOffset(); 2551 if (child && IsCell(child)) { 2552 previousCellParent = container; 2553 } 2554 2555 // We're done if we didn't find parent of a previously-selected cell 2556 if (!previousCellParent) { 2557 break; 2558 } 2559 2560 if (previousCellParent == aParentContent && offset == aContentOffset) { 2561 // Cell is already selected 2562 if (rangeCount == 1) { 2563 #ifdef DEBUG_TABLE_SELECTION 2564 printf("HandleTableSelection: Unselecting single selected cell\n"); 2565 #endif 2566 // This was the only cell selected. 2567 // Collapse to "normal" selection inside the cell 2568 mStartSelectedCell = nullptr; 2569 mEndSelectedCell = nullptr; 2570 mAppendStartSelectedCell = nullptr; 2571 // TODO: We need a "Collapse to just before deepest child" routine 2572 // Even better, should we collapse to just after the LAST deepest 2573 // child 2574 // (i.e., at the end of the cell's contents)? 2575 return aNormalSelection.CollapseInLimiter(aChildContent, 0); 2576 } 2577 #ifdef DEBUG_TABLE_SELECTION 2578 printf( 2579 "HandleTableSelection: Removing cell from multi-cell " 2580 "selection\n"); 2581 #endif 2582 // Unselecting the start of previous block 2583 // XXX What do we use now! 2584 if (aChildContent == mAppendStartSelectedCell) { 2585 mAppendStartSelectedCell = nullptr; 2586 } 2587 2588 // Deselect cell by removing its range from selection 2589 ErrorResult err; 2590 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners( 2591 *range, err); 2592 return err.StealNSResult(); 2593 } 2594 } 2595 mUnselectCellOnMouseUp = nullptr; 2596 } 2597 } 2598 return result; 2599 } 2600 2601 nsresult nsFrameSelection::TableSelection::SelectBlockOfCells( 2602 nsIContent* aStartCell, nsIContent* aEndCell, Selection& aNormalSelection) { 2603 NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER); 2604 NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER); 2605 mEndSelectedCell = aEndCell; 2606 2607 nsresult result = NS_OK; 2608 2609 // If new end cell is in a different table, do nothing 2610 const RefPtr<const nsIContent> table = IsInSameTable(aStartCell, aEndCell); 2611 if (!table) { 2612 return NS_OK; 2613 } 2614 2615 // Get starting and ending cells' location in the cellmap 2616 int32_t startRowIndex, startColIndex, endRowIndex, endColIndex; 2617 result = GetCellIndexes(aStartCell, startRowIndex, startColIndex); 2618 if (NS_FAILED(result)) { 2619 return result; 2620 } 2621 result = GetCellIndexes(aEndCell, endRowIndex, endColIndex); 2622 if (NS_FAILED(result)) { 2623 return result; 2624 } 2625 2626 if (mDragSelectingCells) { 2627 // Drag selecting: remove selected cells outside of new block limits 2628 // TODO: `UnselectCells`'s return value shouldn't be ignored. 2629 UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex, 2630 true, aNormalSelection); 2631 } 2632 2633 // Note that we select block in the direction of user's mouse dragging, 2634 // which means start cell may be after the end cell in either row or column 2635 return AddCellsToSelection(table, startRowIndex, startColIndex, endRowIndex, 2636 endColIndex, aNormalSelection); 2637 } 2638 2639 nsresult nsFrameSelection::TableSelection::UnselectCells( 2640 const nsIContent* aTableContent, int32_t aStartRowIndex, 2641 int32_t aStartColumnIndex, int32_t aEndRowIndex, int32_t aEndColumnIndex, 2642 bool aRemoveOutsideOfCellRange, mozilla::dom::Selection& aNormalSelection) { 2643 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 2644 2645 nsTableWrapperFrame* tableFrame = 2646 do_QueryFrame(aTableContent->GetPrimaryFrame()); 2647 if (!tableFrame) { 2648 return NS_ERROR_FAILURE; 2649 } 2650 2651 int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex); 2652 int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex); 2653 int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex); 2654 int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex); 2655 2656 // Strong reference because we sometimes remove the range 2657 RefPtr<nsRange> range = GetFirstCellRange(aNormalSelection); 2658 nsIContent* cellNode = GetFirstSelectedContent(range); 2659 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range"); 2660 2661 int32_t curRowIndex, curColIndex; 2662 while (cellNode) { 2663 nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex); 2664 if (NS_FAILED(result)) { 2665 return result; 2666 } 2667 2668 #ifdef DEBUG_TABLE_SELECTION 2669 if (!range) printf("RemoveCellsToSelection -- range is null\n"); 2670 #endif 2671 2672 if (range) { 2673 if (aRemoveOutsideOfCellRange) { 2674 if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex || 2675 curColIndex < minColIndex || curColIndex > maxColIndex) { 2676 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners( 2677 *range, IgnoreErrors()); 2678 // Since we've removed the range, decrement pointer to next range 2679 mSelectedCellIndex--; 2680 } 2681 2682 } else { 2683 // Remove cell from selection if it belongs to the given cells range or 2684 // it is spanned onto the cells range. 2685 nsTableCellFrame* cellFrame = 2686 tableFrame->GetCellFrameAt(curRowIndex, curColIndex); 2687 2688 uint32_t origRowIndex = cellFrame->RowIndex(); 2689 uint32_t origColIndex = cellFrame->ColIndex(); 2690 uint32_t actualRowSpan = 2691 tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex); 2692 uint32_t actualColSpan = 2693 tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex); 2694 if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) && 2695 maxRowIndex >= 0 && 2696 origRowIndex + actualRowSpan - 1 >= 2697 static_cast<uint32_t>(minRowIndex) && 2698 origColIndex <= static_cast<uint32_t>(maxColIndex) && 2699 maxColIndex >= 0 && 2700 origColIndex + actualColSpan - 1 >= 2701 static_cast<uint32_t>(minColIndex)) { 2702 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners( 2703 *range, IgnoreErrors()); 2704 // Since we've removed the range, decrement pointer to next range 2705 mSelectedCellIndex--; 2706 } 2707 } 2708 } 2709 2710 range = GetNextCellRange(aNormalSelection); 2711 cellNode = GetFirstSelectedContent(range); 2712 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range"); 2713 } 2714 2715 return NS_OK; 2716 } 2717 2718 nsresult SelectCellElement(nsIContent* aCellElement, 2719 Selection& aNormalSelection) { 2720 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 2721 2722 nsIContent* parent = aCellElement->GetParent(); 2723 2724 // Get child offset 2725 const int32_t offset = parent->ComputeIndexOf_Deprecated(aCellElement); 2726 2727 return CreateAndAddRange(parent, offset, aNormalSelection); 2728 } 2729 2730 static nsresult AddCellsToSelection(const nsIContent* aTableContent, 2731 int32_t aStartRowIndex, 2732 int32_t aStartColumnIndex, 2733 int32_t aEndRowIndex, 2734 int32_t aEndColumnIndex, 2735 Selection& aNormalSelection) { 2736 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 2737 2738 nsTableWrapperFrame* tableFrame = 2739 do_QueryFrame(aTableContent->GetPrimaryFrame()); 2740 if (!tableFrame) { // Check that |table| is a table. 2741 return NS_ERROR_FAILURE; 2742 } 2743 2744 nsresult result = NS_OK; 2745 uint32_t row = aStartRowIndex; 2746 while (true) { 2747 uint32_t col = aStartColumnIndex; 2748 while (true) { 2749 nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col); 2750 2751 // Skip cells that are spanned from previous locations or are already 2752 // selected 2753 if (cellFrame) { 2754 uint32_t origRow = cellFrame->RowIndex(); 2755 uint32_t origCol = cellFrame->ColIndex(); 2756 if (origRow == row && origCol == col && !cellFrame->IsSelected()) { 2757 result = SelectCellElement(cellFrame->GetContent(), aNormalSelection); 2758 if (NS_FAILED(result)) { 2759 return result; 2760 } 2761 } 2762 } 2763 // Done when we reach end column 2764 if (col == static_cast<uint32_t>(aEndColumnIndex)) { 2765 break; 2766 } 2767 2768 if (aStartColumnIndex < aEndColumnIndex) { 2769 col++; 2770 } else { 2771 col--; 2772 } 2773 } 2774 if (row == static_cast<uint32_t>(aEndRowIndex)) { 2775 break; 2776 } 2777 2778 if (aStartRowIndex < aEndRowIndex) { 2779 row++; 2780 } else { 2781 row--; 2782 } 2783 } 2784 return result; 2785 } 2786 2787 nsresult nsFrameSelection::RemoveCellsFromSelection(nsIContent* aTable, 2788 int32_t aStartRowIndex, 2789 int32_t aStartColumnIndex, 2790 int32_t aEndRowIndex, 2791 int32_t aEndColumnIndex) { 2792 const RefPtr<Selection> selection = &NormalSelection(); 2793 return mTableSelection.UnselectCells(aTable, aStartRowIndex, 2794 aStartColumnIndex, aEndRowIndex, 2795 aEndColumnIndex, false, *selection); 2796 } 2797 2798 nsresult nsFrameSelection::RestrictCellsToSelection(nsIContent* aTable, 2799 int32_t aStartRowIndex, 2800 int32_t aStartColumnIndex, 2801 int32_t aEndRowIndex, 2802 int32_t aEndColumnIndex) { 2803 const RefPtr<Selection> selection = &NormalSelection(); 2804 return mTableSelection.UnselectCells(aTable, aStartRowIndex, 2805 aStartColumnIndex, aEndRowIndex, 2806 aEndColumnIndex, true, *selection); 2807 } 2808 2809 Result<nsFrameSelection::TableSelection::FirstAndLastCell, nsresult> 2810 nsFrameSelection::TableSelection::FindFirstAndLastCellOfRowOrColumn( 2811 const nsIContent& aCellContent) const { 2812 const nsIContent* table = GetParentTable(&aCellContent); 2813 if (!table) { 2814 return Err(NS_ERROR_NULL_POINTER); 2815 } 2816 2817 // Get table and cell layout interfaces to access 2818 // cell data based on cellmap location 2819 // Frames are not ref counted, so don't use an nsCOMPtr 2820 nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame()); 2821 if (!tableFrame) { 2822 return Err(NS_ERROR_FAILURE); 2823 } 2824 nsITableCellLayout* cellLayout = GetCellLayout(&aCellContent); 2825 if (!cellLayout) { 2826 return Err(NS_ERROR_FAILURE); 2827 } 2828 2829 // Get location of target cell: 2830 int32_t rowIndex, colIndex; 2831 nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex); 2832 if (NS_FAILED(result)) { 2833 return Err(result); 2834 } 2835 2836 // Be sure we start at proper beginning 2837 // (This allows us to select row or col given ANY cell!) 2838 if (mMode == TableSelectionMode::Row) { 2839 colIndex = 0; 2840 } 2841 if (mMode == TableSelectionMode::Column) { 2842 rowIndex = 0; 2843 } 2844 2845 FirstAndLastCell firstAndLastCell; 2846 while (true) { 2847 // Loop through all cells in column or row to find first and last 2848 nsCOMPtr<nsIContent> curCellContent = 2849 tableFrame->GetCellAt(rowIndex, colIndex); 2850 if (!curCellContent) { 2851 break; 2852 } 2853 2854 if (!firstAndLastCell.mFirst) { 2855 firstAndLastCell.mFirst = curCellContent; 2856 } 2857 2858 firstAndLastCell.mLast = std::move(curCellContent); 2859 2860 // Move to next cell in cellmap, skipping spanned locations 2861 if (mMode == TableSelectionMode::Row) { 2862 colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex); 2863 } else { 2864 rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex); 2865 } 2866 } 2867 return firstAndLastCell; 2868 } 2869 2870 nsresult nsFrameSelection::TableSelection::SelectRowOrColumn( 2871 nsIContent* aCellContent, Selection& aNormalSelection) { 2872 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 2873 2874 if (!aCellContent) { 2875 return NS_ERROR_NULL_POINTER; 2876 } 2877 2878 Result<FirstAndLastCell, nsresult> firstAndLastCell = 2879 FindFirstAndLastCellOfRowOrColumn(*aCellContent); 2880 if (firstAndLastCell.isErr()) { 2881 return firstAndLastCell.unwrapErr(); 2882 } 2883 2884 // Use SelectBlockOfCells: 2885 // This will replace existing selection, 2886 // but allow unselecting by dragging out of selected region 2887 if (firstAndLastCell.inspect().mFirst && firstAndLastCell.inspect().mLast) { 2888 nsresult rv{NS_OK}; 2889 2890 if (!mStartSelectedCell) { 2891 // We are starting a new block, so select the first cell 2892 rv = ::SelectCellElement(firstAndLastCell.inspect().mFirst, 2893 aNormalSelection); 2894 if (NS_FAILED(rv)) { 2895 return rv; 2896 } 2897 mStartSelectedCell = firstAndLastCell.inspect().mFirst; 2898 } 2899 2900 const nsCOMPtr<nsIContent> startSelectedCell = mStartSelectedCell; 2901 rv = SelectBlockOfCells(startSelectedCell, 2902 MOZ_KnownLive(firstAndLastCell.inspect().mLast), 2903 aNormalSelection); 2904 2905 // This gets set to the cell at end of row/col, 2906 // but we need it to be the cell under cursor 2907 mEndSelectedCell = aCellContent; 2908 return rv; 2909 } 2910 2911 #if 0 2912 // This is a more efficient strategy that appends row to current selection, 2913 // but doesn't allow dragging OFF of an existing selection to unselect! 2914 do { 2915 // Loop through all cells in column or row 2916 result = tableLayout->GetCellDataAt(rowIndex, colIndex, 2917 getter_AddRefs(cellElement), 2918 curRowIndex, curColIndex, 2919 rowSpan, colSpan, 2920 actualRowSpan, actualColSpan, 2921 isSelected); 2922 if (NS_FAILED(result)) return result; 2923 // We're done when cell is not found 2924 if (!cellElement) break; 2925 2926 2927 // Check spans else we infinitely loop 2928 NS_ASSERTION(actualColSpan, "actualColSpan is 0!"); 2929 NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!"); 2930 2931 // Skip cells that are already selected or span from outside our region 2932 if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex) 2933 { 2934 result = SelectCellElement(cellElement); 2935 if (NS_FAILED(result)) return result; 2936 } 2937 // Move to next row or column in cellmap, skipping spanned locations 2938 if (mMode == TableSelectionMode::Row) 2939 colIndex += actualColSpan; 2940 else 2941 rowIndex += actualRowSpan; 2942 } 2943 while (cellElement); 2944 #endif 2945 2946 return NS_OK; 2947 } 2948 2949 // static 2950 nsIContent* nsFrameSelection::GetFirstCellNodeInRange(const nsRange* aRange) { 2951 if (!aRange) { 2952 return nullptr; 2953 } 2954 2955 nsIContent* childContent = aRange->GetChildAtStartOffset(); 2956 if (!childContent) { 2957 return nullptr; 2958 } 2959 // Don't return node if not a cell 2960 if (!IsCell(childContent)) { 2961 return nullptr; 2962 } 2963 2964 return childContent; 2965 } 2966 2967 nsRange* nsFrameSelection::TableSelection::GetFirstCellRange( 2968 const mozilla::dom::Selection& aNormalSelection) { 2969 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 2970 2971 nsRange* firstRange = aNormalSelection.GetRangeAt(0); 2972 if (!GetFirstCellNodeInRange(firstRange)) { 2973 return nullptr; 2974 } 2975 2976 // Setup for next cell 2977 mSelectedCellIndex = 1; 2978 2979 return firstRange; 2980 } 2981 2982 nsRange* nsFrameSelection::TableSelection::GetNextCellRange( 2983 const mozilla::dom::Selection& aNormalSelection) { 2984 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 2985 2986 nsRange* range = 2987 aNormalSelection.GetRangeAt(AssertedCast<uint32_t>(mSelectedCellIndex)); 2988 2989 // Get first node in next range of selection - test if it's a cell 2990 if (!GetFirstCellNodeInRange(range)) { 2991 return nullptr; 2992 } 2993 2994 // Setup for next cell 2995 mSelectedCellIndex++; 2996 2997 return range; 2998 } 2999 3000 // static 3001 nsresult nsFrameSelection::GetCellIndexes(const nsIContent* aCell, 3002 int32_t& aRowIndex, 3003 int32_t& aColIndex) { 3004 if (!aCell) { 3005 return NS_ERROR_NULL_POINTER; 3006 } 3007 3008 aColIndex = 0; // initialize out params 3009 aRowIndex = 0; 3010 3011 nsITableCellLayout* cellLayoutObject = GetCellLayout(aCell); 3012 if (!cellLayoutObject) { 3013 return NS_ERROR_FAILURE; 3014 } 3015 return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex); 3016 } 3017 3018 // static 3019 nsIContent* nsFrameSelection::IsInSameTable(const nsIContent* aContent1, 3020 const nsIContent* aContent2) { 3021 if (!aContent1 || !aContent2) { 3022 return nullptr; 3023 } 3024 3025 nsIContent* tableNode1 = GetParentTable(aContent1); 3026 nsIContent* tableNode2 = GetParentTable(aContent2); 3027 3028 // Must be in the same table. Note that we want to return false for 3029 // the test if both tables are null. 3030 return (tableNode1 == tableNode2) ? tableNode1 : nullptr; 3031 } 3032 3033 // static 3034 nsIContent* nsFrameSelection::GetParentTable(const nsIContent* aCell) { 3035 if (!aCell) { 3036 return nullptr; 3037 } 3038 3039 for (nsIContent* parent = aCell->GetParent(); parent; 3040 parent = parent->GetParent()) { 3041 if (parent->IsHTMLElement(nsGkAtoms::table)) { 3042 return parent; 3043 } 3044 } 3045 3046 return nullptr; 3047 } 3048 3049 nsresult nsFrameSelection::SelectCellElement(nsIContent* aCellElement) { 3050 const RefPtr<Selection> selection = &NormalSelection(); 3051 return ::SelectCellElement(aCellElement, *selection); 3052 } 3053 3054 nsresult CreateAndAddRange(nsINode* aContainer, int32_t aOffset, 3055 Selection& aNormalSelection) { 3056 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal); 3057 3058 if (!aContainer) { 3059 return NS_ERROR_NULL_POINTER; 3060 } 3061 3062 // Set range around child at given offset 3063 ErrorResult error; 3064 RefPtr<nsRange> range = 3065 nsRange::Create(aContainer, aOffset, aContainer, aOffset + 1, error); 3066 if (NS_WARN_IF(error.Failed())) { 3067 return error.StealNSResult(); 3068 } 3069 MOZ_ASSERT(range); 3070 3071 ErrorResult err; 3072 aNormalSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, err); 3073 return err.StealNSResult(); 3074 } 3075 3076 // End of Table Selection 3077 3078 void nsFrameSelection::SetAncestorLimiter(Element* aLimiter) { 3079 if (mLimiters.mAncestorLimiter != aLimiter) { 3080 mLimiters.mAncestorLimiter = aLimiter; 3081 const Selection& sel = NormalSelection(); 3082 LogSelectionAPI(&sel, __FUNCTION__, "aLimiter", aLimiter); 3083 3084 if (!NodeIsInLimiters(sel.GetFocusNode())) { 3085 ClearNormalSelection(); 3086 if (mLimiters.mAncestorLimiter) { 3087 SetChangeReasons(nsISelectionListener::NO_REASON); 3088 nsCOMPtr<nsIContent> limiter(mLimiters.mAncestorLimiter); 3089 const nsresult rv = 3090 TakeFocus(*limiter, 0, 0, CaretAssociationHint::Before, 3091 FocusMode::kCollapseToNewPoint); 3092 (void)NS_WARN_IF(NS_FAILED(rv)); 3093 // TODO: in case of failure, propagate it to the callers. 3094 } 3095 } 3096 } 3097 } 3098 3099 void nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) { 3100 if (aMouseEvent) { 3101 mDelayedMouseEvent.mIsValid = true; 3102 mDelayedMouseEvent.mIsShift = aMouseEvent->IsShift(); 3103 mDelayedMouseEvent.mClickCount = aMouseEvent->mClickCount; 3104 } else { 3105 mDelayedMouseEvent.mIsValid = false; 3106 } 3107 } 3108 3109 void nsFrameSelection::DisconnectFromPresShell() { 3110 if (mAccessibleCaretEnabled) { 3111 Selection& sel = NormalSelection(); 3112 sel.StopNotifyingAccessibleCaretEventHub(); 3113 } 3114 3115 StopAutoScrollTimer(); 3116 for (size_t i = 0; i < std::size(mDomSelections); i++) { 3117 MOZ_ASSERT(mDomSelections[i]); 3118 mDomSelections[i]->Clear(nullptr); 3119 } 3120 3121 if (auto* presshell = mPresShell) { 3122 if (const nsFrameSelection* sel = presshell->GetLastSelectionForToString(); 3123 sel == this) { 3124 presshell->UpdateLastSelectionForToString(nullptr); 3125 } 3126 mPresShell = nullptr; 3127 } 3128 } 3129 3130 #ifdef XP_MACOSX 3131 /** 3132 * See Bug 1288453. 3133 * 3134 * Update the selection cache on repaint to handle when a pre-existing 3135 * selection becomes active aka the current selection. 3136 * 3137 * 1. Change the current selection by click n dragging another selection. 3138 * - Make a selection on content page. Make a selection in a text editor. 3139 * - You can click n drag the content selection to make it active again. 3140 * 2. Change the current selection when switching to a tab with a selection. 3141 * - Make selection in tab. 3142 * - Switching tabs will make its respective selection active. 3143 * 3144 * Therefore, we only update the selection cache on a repaint 3145 * if the current selection being repainted is not an empty selection. 3146 * 3147 * If the current selection is empty. The current selection cache 3148 * would be cleared by AutoCopyListener::OnSelectionChange(). 3149 */ 3150 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel) { 3151 PresShell* presShell = aSel->GetPresShell(); 3152 if (!presShell) { 3153 return NS_OK; 3154 } 3155 nsCOMPtr<Document> aDoc = presShell->GetDocument(); 3156 3157 if (aDoc && aSel && !aSel->IsCollapsed()) { 3158 return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard( 3159 aSel, aDoc, nsIClipboard::kSelectionCache, false); 3160 } 3161 3162 return NS_OK; 3163 } 3164 #endif // XP_MACOSX 3165 3166 // mozilla::AutoCopyListener 3167 3168 /* 3169 * What we do now: 3170 * On every selection change, we copy to the clipboard anew, creating a 3171 * HTML buffer, a transferable, an nsISupportsString and 3172 * a huge mess every time. This is basically what 3173 * nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the 3174 * selection into the clipboard for Edit->Copy. 3175 * 3176 * What we should do, to make our end of the deal faster: 3177 * Create a singleton transferable with our own magic converter. When selection 3178 * changes (use a quick cache to detect ``real'' changes), we put the new 3179 * Selection in the transferable. Our magic converter will take care of 3180 * transferable->whatever-other-format when the time comes to actually 3181 * hand over the clipboard contents. 3182 * 3183 * Other issues: 3184 * - which X clipboard should we populate? 3185 * - should we use a different one than Edit->Copy, so that inadvertant 3186 * selections (or simple clicks, which currently cause a selection 3187 * notification, regardless of if they're in the document which currently has 3188 * selection!) don't lose the contents of the ``application''? Or should we 3189 * just put some intelligence in the ``is this a real selection?'' code to 3190 * protect our selection against clicks in other documents that don't create 3191 * selections? 3192 * - maybe we should just never clear the X clipboard? That would make this 3193 * problem just go away, which is very tempting. 3194 * 3195 * On macOS, 3196 * nsIClipboard::kSelectionCache is the flag for current selection cache. 3197 * Set the current selection cache on the parent process in 3198 * widget cocoa nsClipboard whenever selection changes. 3199 */ 3200 3201 // static 3202 void AutoCopyListener::OnSelectionChange(Document* aDocument, 3203 Selection& aSelection, 3204 int16_t aReason) { 3205 MOZ_ASSERT(IsEnabled()); 3206 3207 // For now, we should prevent any updates caused by a call of Selection API. 3208 // We should allow this in some cases later, though. See the valid usage in 3209 // bug 1567160. 3210 if (aReason & nsISelectionListener::JS_REASON) { 3211 return; 3212 } 3213 3214 if (sClipboardID == nsIClipboard::kSelectionCache) { 3215 // Do nothing if this isn't in the active window and, 3216 // in the case of Web content, in the frontmost tab. 3217 if (!aDocument || !IsInActiveTab(aDocument)) { 3218 return; 3219 } 3220 } 3221 3222 static const int16_t kResasonsToHandle = 3223 nsISelectionListener::MOUSEUP_REASON | 3224 nsISelectionListener::SELECTALL_REASON | 3225 nsISelectionListener::KEYPRESS_REASON; 3226 if (!(aReason & kResasonsToHandle)) { 3227 return; // Don't care if we are still dragging. 3228 } 3229 3230 if (!aDocument || 3231 aSelection.AreNormalAndCrossShadowBoundaryRangesCollapsed()) { 3232 #ifdef DEBUG_CLIPBOARD 3233 fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n"); 3234 #endif 3235 if (sClipboardID != nsIClipboard::kSelectionCache) { 3236 // XXX Should we clear X clipboard? 3237 return; 3238 } 3239 3240 // If on macOS, clear the current selection transferable cached 3241 // on the parent process (nsClipboard) when the selection is empty. 3242 DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache(); 3243 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 3244 "nsCopySupport::ClearSelectionCache() failed"); 3245 return; 3246 } 3247 3248 DebugOnly<nsresult> rv = 3249 nsCopySupport::EncodeDocumentWithContextAndPutToClipboard( 3250 &aSelection, aDocument, sClipboardID, false); 3251 NS_WARNING_ASSERTION( 3252 NS_SUCCEEDED(rv), 3253 "nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed"); 3254 }