Backed out changeset 496886cb30a5 (bug 1867152) for bc failures on browser_user_input...
[gecko.git] / layout / generic / nsFrameSelection.cpp
blob84acbfcd28f81db06e0f80fe81c7aa2fb56b1a95
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/. */
7 /*
8 * Implementation of nsFrameSelection
9 */
11 #include "nsFrameSelection.h"
13 #include "mozilla/intl/BidiEmbeddingLevel.h"
14 #include "mozilla/Attributes.h"
15 #include "mozilla/AutoRestore.h"
16 #include "mozilla/BasePrincipal.h"
17 #include "mozilla/HTMLEditor.h"
18 #include "mozilla/IntegerRange.h"
19 #include "mozilla/Logging.h"
20 #include "mozilla/PresShell.h"
21 #include "mozilla/ScrollTypes.h"
22 #include "mozilla/StaticAnalysisFunctions.h"
23 #include "mozilla/StaticPrefs_bidi.h"
24 #include "mozilla/StaticPrefs_dom.h"
25 #include "mozilla/StaticPrefs_layout.h"
26 #include "mozilla/Unused.h"
28 #include "nsCOMPtr.h"
29 #include "nsDebug.h"
30 #include "nsString.h"
31 #include "nsISelectionListener.h"
32 #include "nsContentCID.h"
33 #include "nsDeviceContext.h"
34 #include "nsIContent.h"
35 #include "nsRange.h"
36 #include "nsITableCellLayout.h"
37 #include "nsTArray.h"
38 #include "nsTableWrapperFrame.h"
39 #include "nsTableCellFrame.h"
40 #include "nsIScrollableFrame.h"
41 #include "nsCCUncollectableMarker.h"
42 #include "nsTextFragment.h"
43 #include <algorithm>
44 #include "nsContentUtils.h"
45 #include "nsCSSFrameConstructor.h"
47 #include "nsGkAtoms.h"
48 #include "nsIFrameTraversal.h"
49 #include "nsLayoutUtils.h"
50 #include "nsLayoutCID.h"
51 #include "nsBidiPresUtils.h"
52 static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID);
53 #include "nsTextFrame.h"
55 #include "nsThreadUtils.h"
56 #include "mozilla/Preferences.h"
58 #include "mozilla/PresShell.h"
59 #include "nsPresContext.h"
60 #include "nsCaret.h"
62 #include "mozilla/MouseEvents.h"
63 #include "mozilla/TextEvents.h"
65 // notifications
66 #include "mozilla/dom/Document.h"
68 #include "nsISelectionController.h" //for the enums
69 #include "nsCopySupport.h"
70 #include "nsIClipboard.h"
71 #include "nsIFrameInlines.h"
73 #include "nsError.h"
74 #include "mozilla/AutoCopyListener.h"
75 #include "mozilla/dom/Element.h"
76 #include "mozilla/dom/Highlight.h"
77 #include "mozilla/dom/Selection.h"
78 #include "mozilla/dom/ShadowRoot.h"
79 #include "mozilla/dom/StaticRange.h"
80 #include "mozilla/dom/Text.h"
81 #include "mozilla/ErrorResult.h"
82 #include "mozilla/dom/SelectionBinding.h"
83 #include "mozilla/AsyncEventDispatcher.h"
84 #include "mozilla/Telemetry.h"
86 #include "nsFocusManager.h"
87 #include "nsPIDOMWindow.h"
89 using namespace mozilla;
90 using namespace mozilla::dom;
92 static LazyLogModule sFrameSelectionLog("FrameSelection");
94 // #define DEBUG_TABLE 1
96 /**
97 * Add cells to the selection inside of the given cells range.
99 * @param aTable [in] HTML table element
100 * @param aStartRowIndex [in] row index where the cells range starts
101 * @param aStartColumnIndex [in] column index where the cells range starts
102 * @param aEndRowIndex [in] row index where the cells range ends
103 * @param aEndColumnIndex [in] column index where the cells range ends
105 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
106 int32_t aStartRowIndex,
107 int32_t aStartColumnIndex,
108 int32_t aEndRowIndex,
109 int32_t aEndColumnIndex,
110 Selection& aNormalSelection);
112 static nsAtom* GetTag(nsINode* aNode);
114 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode);
115 MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult CreateAndAddRange(
116 nsINode* aContainer, int32_t aOffset, Selection& aNormalSelection);
117 static nsresult SelectCellElement(nsIContent* aCellElement,
118 Selection& aNormalSelection);
120 #ifdef XP_MACOSX
121 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel);
122 #endif // XP_MACOSX
124 #ifdef PRINT_RANGE
125 static void printRange(nsRange* aDomRange);
126 # define DEBUG_OUT_RANGE(x) printRange(x)
127 #else
128 # define DEBUG_OUT_RANGE(x)
129 #endif // PRINT_RANGE
131 /******************************************************************************
132 * mozilla::PeekOffsetStruct
133 ******************************************************************************/
135 // #define DEBUG_SELECTION // uncomment for printf describing every collapse and
136 // extend. #define DEBUG_NAVIGATION
138 // #define DEBUG_TABLE_SELECTION 1
140 namespace mozilla {
142 PeekOffsetStruct::PeekOffsetStruct(nsSelectionAmount aAmount,
143 nsDirection aDirection, int32_t aStartOffset,
144 nsPoint aDesiredCaretPos,
145 const PeekOffsetOptions aOptions,
146 EWordMovementType aWordMovementType)
147 : mAmount(aAmount),
148 mDirection(aDirection),
149 mStartOffset(aStartOffset),
150 mDesiredCaretPos(aDesiredCaretPos),
151 mWordMovementType(aWordMovementType),
152 mOptions(aOptions),
153 mResultFrame(nullptr),
154 mContentOffset(0),
155 mAttach(CARET_ASSOCIATE_BEFORE) {}
157 } // namespace mozilla
159 // Array which contains index of each SelecionType in Selection::mDOMSelections.
160 // For avoiding using if nor switch to retrieve the index, this needs to have
161 // -1 for SelectionTypes which won't be created its Selection instance.
162 static const int8_t kIndexOfSelections[] = {
163 -1, // SelectionType::eInvalid
164 -1, // SelectionType::eNone
165 0, // SelectionType::eNormal
166 1, // SelectionType::eSpellCheck
167 2, // SelectionType::eIMERawClause
168 3, // SelectionType::eIMESelectedRawClause
169 4, // SelectionType::eIMEConvertedClause
170 5, // SelectionType::eIMESelectedClause
171 6, // SelectionType::eAccessibility
172 7, // SelectionType::eFind
173 8, // SelectionType::eURLSecondary
174 9, // SelectionType::eURLStrikeout
175 -1, // SelectionType::eHighlight
178 inline int8_t GetIndexFromSelectionType(SelectionType aSelectionType) {
179 // The enum value of eInvalid is -1 and the others are sequential value
180 // starting from 0. Therefore, |SelectionType + 1| is the index of
181 // kIndexOfSelections.
182 return kIndexOfSelections[static_cast<int8_t>(aSelectionType) + 1];
186 The limiter is used specifically for the text areas and textfields
187 In that case it is the DIV tag that is anonymously created for the text
188 areas/fields. Text nodes and BR nodes fall beneath it. In the case of a
189 BR node the limiter will be the parent and the offset will point before or
190 after the BR node. In the case of the text node the parent content is
191 the text node itself and the offset will be the exact character position.
192 The offset is not important to check for validity. Simply look at the
193 passed in content. If it equals the limiter then the selection point is valid.
194 If its parent it the limiter then the point is also valid. In the case of
195 NO limiter all points are valid since you are in a topmost iframe. (browser
196 or composer)
198 bool nsFrameSelection::IsValidSelectionPoint(nsINode* aNode) const {
199 if (!aNode) {
200 return false;
203 nsIContent* limiter = GetLimiter();
204 if (limiter && limiter != aNode && limiter != aNode->GetParent()) {
205 // if newfocus == the limiter. that's ok. but if not there and not parent
206 // bad
207 return false; // not in the right content. tLimiter said so
210 limiter = GetAncestorLimiter();
211 return !limiter || aNode->IsInclusiveDescendantOf(limiter);
214 namespace mozilla {
215 struct MOZ_RAII AutoPrepareFocusRange {
216 AutoPrepareFocusRange(Selection* aSelection,
217 const bool aMultiRangeSelection) {
218 MOZ_ASSERT(aSelection);
219 MOZ_ASSERT(aSelection->GetType() == SelectionType::eNormal);
221 if (aSelection->mStyledRanges.mRanges.Length() <= 1) {
222 return;
225 if (aSelection->mFrameSelection->IsUserSelectionReason()) {
226 mUserSelect.emplace(aSelection);
229 nsTArray<StyledRange>& ranges = aSelection->mStyledRanges.mRanges;
230 if (!aSelection->mUserInitiated || aMultiRangeSelection) {
231 // Scripted command or the user is starting a new explicit multi-range
232 // selection.
233 for (StyledRange& entry : ranges) {
234 MOZ_ASSERT(entry.mRange->IsDynamicRange());
235 entry.mRange->AsDynamicRange()->SetIsGenerated(false);
237 return;
240 if (!IsAnchorRelativeOperation(
241 aSelection->mFrameSelection->mSelectionChangeReasons)) {
242 return;
245 // This operation is against the anchor but our current mAnchorFocusRange
246 // represents the focus in a multi-range selection. The anchor from a user
247 // perspective is the most distant generated range on the opposite side.
248 // Find that range and make it the mAnchorFocusRange.
249 nsRange* const newAnchorFocusRange =
250 FindGeneratedRangeMostDistantFromAnchor(*aSelection);
252 if (!newAnchorFocusRange) {
253 // There are no generated ranges - that's fine.
254 return;
257 // Setup the new mAnchorFocusRange and mark the old one as generated.
258 if (aSelection->mAnchorFocusRange) {
259 aSelection->mAnchorFocusRange->SetIsGenerated(true);
262 newAnchorFocusRange->SetIsGenerated(false);
263 aSelection->mAnchorFocusRange = newAnchorFocusRange;
265 RemoveGeneratedRanges(*aSelection);
267 if (aSelection->mFrameSelection) {
268 aSelection->mFrameSelection->InvalidateDesiredCaretPos();
272 private:
273 static nsRange* FindGeneratedRangeMostDistantFromAnchor(
274 const Selection& aSelection) {
275 const nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
276 const size_t len = ranges.Length();
277 nsRange* result{nullptr};
278 if (aSelection.GetDirection() == eDirNext) {
279 for (size_t i = 0; i < len; ++i) {
280 // This function is only called for selections with type == eNormal.
281 // (see MOZ_ASSERT in constructor).
282 // Therefore, all ranges must be dynamic.
283 if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
284 result = ranges[i].mRange->AsDynamicRange();
285 break;
288 } else {
289 size_t i = len;
290 while (i--) {
291 if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
292 result = ranges[i].mRange->AsDynamicRange();
293 break;
298 return result;
301 static void RemoveGeneratedRanges(Selection& aSelection) {
302 RefPtr<nsPresContext> presContext = aSelection.GetPresContext();
303 nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
304 size_t i = ranges.Length();
305 while (i--) {
306 // This function is only called for selections with type == eNormal.
307 // (see MOZ_ASSERT in constructor).
308 // Therefore, all ranges must be dynamic.
309 if (!ranges[i].mRange->IsDynamicRange()) {
310 continue;
312 nsRange* range = ranges[i].mRange->AsDynamicRange();
313 if (range->IsGenerated()) {
314 range->UnregisterSelection(aSelection);
315 aSelection.SelectFrames(presContext, *range, false);
316 ranges.RemoveElementAt(i);
322 * @aParam aSelectionChangeReasons can be multiple of the reasons defined in
323 nsISelectionListener.idl.
325 static bool IsAnchorRelativeOperation(const int16_t aSelectionChangeReasons) {
326 return aSelectionChangeReasons &
327 (nsISelectionListener::DRAG_REASON |
328 nsISelectionListener::MOUSEDOWN_REASON |
329 nsISelectionListener::MOUSEUP_REASON |
330 nsISelectionListener::COLLAPSETOSTART_REASON);
333 Maybe<Selection::AutoUserInitiated> mUserSelect;
336 } // namespace mozilla
338 ////////////BEGIN nsFrameSelection methods
340 template Result<RefPtr<nsRange>, nsresult>
341 nsFrameSelection::CreateRangeExtendedToSomewhere(
342 nsDirection aDirection, const nsSelectionAmount aAmount,
343 CaretMovementStyle aMovementStyle);
344 template Result<RefPtr<StaticRange>, nsresult>
345 nsFrameSelection::CreateRangeExtendedToSomewhere(
346 nsDirection aDirection, const nsSelectionAmount aAmount,
347 CaretMovementStyle aMovementStyle);
349 nsFrameSelection::nsFrameSelection(PresShell* aPresShell, nsIContent* aLimiter,
350 const bool aAccessibleCaretEnabled) {
351 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
352 mDomSelections[i] = new Selection(kPresentSelectionTypes[i], this);
355 #ifdef XP_MACOSX
356 // On macOS, cache the current selection to send to service menu of macOS.
357 bool enableAutoCopy = true;
358 AutoCopyListener::Init(nsIClipboard::kSelectionCache);
359 #else // #ifdef XP_MACOSX
360 // Check to see if the auto-copy pref is enabled and make the normal
361 // Selection notifies auto-copy listener of its changes.
362 bool enableAutoCopy = AutoCopyListener::IsPrefEnabled();
363 if (enableAutoCopy) {
364 AutoCopyListener::Init(nsIClipboard::kSelectionClipboard);
366 #endif // #ifdef XP_MACOSX #else
368 if (enableAutoCopy) {
369 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
370 if (mDomSelections[index]) {
371 mDomSelections[index]->NotifyAutoCopy();
375 mPresShell = aPresShell;
376 mDragState = false;
377 mLimiters.mLimiter = aLimiter;
379 // This should only ever be initialized on the main thread, so we are OK here.
380 MOZ_ASSERT(NS_IsMainThread());
382 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
384 mAccessibleCaretEnabled = aAccessibleCaretEnabled;
385 if (mAccessibleCaretEnabled) {
386 mDomSelections[index]->MaybeNotifyAccessibleCaretEventHub(aPresShell);
389 if (mDomSelections[index]) {
390 mDomSelections[index]->EnableSelectionChangeEvent();
394 nsFrameSelection::~nsFrameSelection() = default;
396 NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection)
398 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection)
399 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
400 tmp->mDomSelections[i] = nullptr;
402 tmp->mHighlightSelections.Clear();
404 NS_IMPL_CYCLE_COLLECTION_UNLINK(
405 mTableSelection.mClosestInclusiveTableCellAncestor)
406 tmp->mTableSelection.mMode = TableSelectionMode::None;
407 tmp->mTableSelection.mDragSelectingCells = false;
408 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mStartSelectedCell)
409 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mEndSelectedCell)
410 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mAppendStartSelectedCell)
411 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mUnselectCellOnMouseUp)
412 NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainedRange.mRange)
413 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mLimiter)
414 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mAncestorLimiter)
415 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
416 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection)
417 if (tmp->mPresShell && tmp->mPresShell->GetDocument() &&
418 nsCCUncollectableMarker::InGeneration(
419 cb, tmp->mPresShell->GetDocument()->GetMarkedCCGeneration())) {
420 return NS_SUCCESS_INTERRUPTED_TRAVERSE;
422 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
423 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i])
426 for (const auto& value : tmp->mHighlightSelections) {
427 CycleCollectionNoteChild(cb, value.second().get(),
428 "mHighlightSelections[]");
431 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
432 mTableSelection.mClosestInclusiveTableCellAncestor)
433 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mStartSelectedCell)
434 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mEndSelectedCell)
435 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mAppendStartSelectedCell)
436 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mUnselectCellOnMouseUp)
437 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainedRange.mRange)
438 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mLimiter)
439 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mAncestorLimiter)
440 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
442 bool nsFrameSelection::Caret::IsVisualMovement(
443 bool aContinueSelection, CaretMovementStyle aMovementStyle) const {
444 int32_t movementFlag = StaticPrefs::bidi_edit_caret_movement_style();
445 return aMovementStyle == eVisual ||
446 (aMovementStyle == eUsePrefStyle &&
447 (movementFlag == 1 || (movementFlag == 2 && !aContinueSelection)));
450 // Get the x (or y, in vertical writing mode) position requested
451 // by the Key Handling for line-up/down
452 nsresult nsFrameSelection::DesiredCaretPos::FetchPos(
453 nsPoint& aDesiredCaretPos, const PresShell& aPresShell,
454 Selection& aNormalSelection) const {
455 MOZ_ASSERT(aNormalSelection.GetType() == SelectionType::eNormal);
457 if (mIsSet) {
458 aDesiredCaretPos = mValue;
459 return NS_OK;
462 RefPtr<nsCaret> caret = aPresShell.GetCaret();
463 if (!caret) {
464 return NS_ERROR_NULL_POINTER;
467 caret->SetSelection(&aNormalSelection);
469 nsRect coord;
470 nsIFrame* caretFrame = caret->GetGeometry(&coord);
471 if (!caretFrame) {
472 return NS_ERROR_FAILURE;
474 nsPoint viewOffset(0, 0);
475 nsView* view = nullptr;
476 caretFrame->GetOffsetFromView(viewOffset, &view);
477 if (view) {
478 coord += viewOffset;
480 aDesiredCaretPos = coord.TopLeft();
481 return NS_OK;
484 void nsFrameSelection::InvalidateDesiredCaretPos() // do not listen to
485 // mDesiredCaretPos.mValue;
486 // you must get another.
488 mDesiredCaretPos.Invalidate();
491 void nsFrameSelection::DesiredCaretPos::Invalidate() { mIsSet = false; }
493 void nsFrameSelection::DesiredCaretPos::Set(const nsPoint& aPos) {
494 mValue = aPos;
495 mIsSet = true;
498 nsresult nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree(
499 nsIFrame* aFrame, const nsPoint& aPoint, nsIFrame** aRetFrame,
500 nsPoint& aRetPoint) const {
502 // The whole point of this method is to return a frame and point that
503 // that lie within the same valid subtree as the anchor node's frame,
504 // for use with the method GetContentAndOffsetsFromPoint().
506 // A valid subtree is defined to be one where all the content nodes in
507 // the tree have a valid parent-child relationship.
509 // If the anchor frame and aFrame are in the same subtree, aFrame will
510 // be returned in aRetFrame. If they are in different subtrees, we
511 // return the frame for the root of the subtree.
514 if (!aFrame || !aRetFrame) {
515 return NS_ERROR_NULL_POINTER;
518 *aRetFrame = aFrame;
519 aRetPoint = aPoint;
522 // Get the frame and content for the selection's anchor point!
525 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
526 if (!mDomSelections[index]) {
527 return NS_ERROR_NULL_POINTER;
530 nsCOMPtr<nsIContent> anchorContent =
531 do_QueryInterface(mDomSelections[index]->GetAnchorNode());
532 if (!anchorContent) {
533 return NS_ERROR_FAILURE;
537 // Now find the root of the subtree containing the anchor's content.
540 NS_ENSURE_STATE(mPresShell);
541 RefPtr<PresShell> presShell = mPresShell;
542 nsIContent* anchorRoot = anchorContent->GetSelectionRootContent(presShell);
543 NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED);
546 // Now find the root of the subtree containing aFrame's content.
549 nsCOMPtr<nsIContent> content = aFrame->GetContent();
551 if (content) {
552 nsIContent* contentRoot = content->GetSelectionRootContent(presShell);
553 NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED);
555 if (anchorRoot == contentRoot) {
556 // If the aFrame's content isn't the capturing content, it should be
557 // a descendant. At this time, we can return simply.
558 nsIContent* capturedContent = PresShell::GetCapturingContent();
559 if (capturedContent != content) {
560 return NS_OK;
563 // Find the frame under the mouse cursor with the root frame.
564 // At this time, don't use the anchor's frame because it may not have
565 // fixed positioned frames.
566 nsIFrame* rootFrame = presShell->GetRootFrame();
567 nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame);
568 nsIFrame* cursorFrame =
569 nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot);
571 // If the mouse cursor in on a frame which is descendant of same
572 // selection root, we can expand the selection to the frame.
573 if (cursorFrame && cursorFrame->PresShell() == presShell) {
574 nsCOMPtr<nsIContent> cursorContent = cursorFrame->GetContent();
575 NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE);
576 nsIContent* cursorContentRoot =
577 cursorContent->GetSelectionRootContent(presShell);
578 NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED);
579 if (cursorContentRoot == anchorRoot) {
580 *aRetFrame = cursorFrame;
581 aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame);
582 return NS_OK;
585 // Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse
586 // cursor is out of the window), we should use the frame of the anchor
587 // root.
592 // When we can't find a frame which is under the mouse cursor and has a same
593 // selection root as the anchor node's, we should return the selection root
594 // frame.
597 *aRetFrame = anchorRoot->GetPrimaryFrame();
599 if (!*aRetFrame) {
600 return NS_ERROR_FAILURE;
604 // Now make sure that aRetPoint is converted to the same coordinate
605 // system used by aRetFrame.
608 aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame);
610 return NS_OK;
613 void nsFrameSelection::SetCaretBidiLevelAndMaybeSchedulePaint(
614 mozilla::intl::BidiEmbeddingLevel aLevel) {
615 // If the current level is undefined, we have just inserted new text.
616 // In this case, we don't want to reset the keyboard language
617 mCaret.mBidiLevel = aLevel;
619 RefPtr<nsCaret> caret;
620 if (mPresShell && (caret = mPresShell->GetCaret())) {
621 caret->SchedulePaint();
625 mozilla::intl::BidiEmbeddingLevel nsFrameSelection::GetCaretBidiLevel() const {
626 return mCaret.mBidiLevel;
629 void nsFrameSelection::UndefineCaretBidiLevel() {
630 mCaret.mBidiLevel = mozilla::intl::BidiEmbeddingLevel(mCaret.mBidiLevel |
631 BIDI_LEVEL_UNDEFINED);
634 #ifdef PRINT_RANGE
635 void printRange(nsRange* aDomRange) {
636 if (!aDomRange) {
637 printf("NULL Range\n");
639 nsINode* startNode = aDomRange->GetStartContainer();
640 nsINode* endNode = aDomRange->GetEndContainer();
641 int32_t startOffset = aDomRange->StartOffset();
642 int32_t endOffset = aDomRange->EndOffset();
644 printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n",
645 (unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset,
646 (unsigned long)endNode, (long)endOffset);
648 #endif /* PRINT_RANGE */
650 static nsAtom* GetTag(nsINode* aNode) {
651 nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
652 if (!content) {
653 MOZ_ASSERT_UNREACHABLE("bad node passed to GetTag()");
654 return nullptr;
657 return content->NodeInfo()->NameAtom();
661 * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor.
663 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode) {
664 if (!aDomNode) return nullptr;
665 nsINode* current = aDomNode;
666 // Start with current node and look for a table cell
667 while (current) {
668 nsAtom* tag = GetTag(current);
669 if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) return current;
670 current = current->GetParent();
672 return nullptr;
675 static nsDirection GetCaretDirection(const nsIFrame& aFrame,
676 nsDirection aDirection,
677 bool aVisualMovement) {
678 const mozilla::intl::BidiDirection paragraphDirection =
679 nsBidiPresUtils::ParagraphDirection(&aFrame);
680 return (aVisualMovement &&
681 paragraphDirection == mozilla::intl::BidiDirection::RTL)
682 ? nsDirection(1 - aDirection)
683 : aDirection;
686 nsresult nsFrameSelection::MoveCaret(nsDirection aDirection,
687 bool aContinueSelection,
688 const nsSelectionAmount aAmount,
689 CaretMovementStyle aMovementStyle) {
690 NS_ENSURE_STATE(mPresShell);
691 // Flush out layout, since we need it to be up to date to do caret
692 // positioning.
693 OwningNonNull<PresShell> presShell(*mPresShell);
694 presShell->FlushPendingNotifications(FlushType::Layout);
696 if (!mPresShell) {
697 return NS_OK;
700 nsPresContext* context = mPresShell->GetPresContext();
701 if (!context) {
702 return NS_ERROR_FAILURE;
705 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
706 const RefPtr<Selection> sel = mDomSelections[index];
707 if (!sel) {
708 return NS_ERROR_NULL_POINTER;
711 int32_t scrollFlags = Selection::SCROLL_FOR_CARET_MOVE;
712 if (sel->IsEditorSelection()) {
713 // If caret moves in editor, it should cause scrolling even if it's in
714 // overflow: hidden;.
715 scrollFlags |= Selection::SCROLL_OVERFLOW_HIDDEN;
718 const bool doCollapse = [&] {
719 if (sel->IsCollapsed() || aContinueSelection) {
720 return false;
722 if (aAmount > eSelectLine) {
723 return false;
725 int32_t caretStyle = StaticPrefs::layout_selection_caret_style();
726 return caretStyle == 2 || (caretStyle == 0 && aAmount != eSelectLine);
727 }();
729 if (doCollapse) {
730 if (aDirection == eDirPrevious) {
731 SetChangeReasons(nsISelectionListener::COLLAPSETOSTART_REASON);
732 mCaret.mHint = CARET_ASSOCIATE_AFTER;
733 } else {
734 SetChangeReasons(nsISelectionListener::COLLAPSETOEND_REASON);
735 mCaret.mHint = CARET_ASSOCIATE_BEFORE;
737 } else {
738 SetChangeReasons(nsISelectionListener::KEYPRESS_REASON);
741 mCaretMoveAmount = aAmount;
743 AutoPrepareFocusRange prep(sel, false);
745 // we must keep this around and revalidate it when its just UP/DOWN
746 nsPoint desiredPos(0, 0);
748 if (aAmount == eSelectLine) {
749 nsresult result = mDesiredCaretPos.FetchPos(desiredPos, *mPresShell, *sel);
750 if (NS_FAILED(result)) {
751 return result;
753 mDesiredCaretPos.Set(desiredPos);
756 bool visualMovement =
757 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
758 nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(visualMovement);
759 if (!frame) {
760 return NS_ERROR_FAILURE;
763 Result<bool, nsresult> isIntraLineCaretMove = IsIntraLineCaretMove(aAmount);
764 nsDirection direction{aDirection};
765 if (isIntraLineCaretMove.isErr()) {
766 return isIntraLineCaretMove.unwrapErr();
768 if (isIntraLineCaretMove.inspect()) {
769 // Forget old caret position for moving caret to different line since
770 // caret position may be changed.
771 mDesiredCaretPos.Invalidate();
772 direction = GetCaretDirection(*frame, aDirection, visualMovement);
775 if (doCollapse) {
776 const nsRange* anchorFocusRange = sel->GetAnchorFocusRange();
777 if (anchorFocusRange) {
778 RefPtr<nsINode> node;
779 uint32_t offset;
780 if (visualMovement && nsBidiPresUtils::IsReversedDirectionFrame(frame)) {
781 direction = nsDirection(1 - direction);
783 if (direction == eDirPrevious) {
784 node = anchorFocusRange->GetStartContainer();
785 offset = anchorFocusRange->StartOffset();
786 } else {
787 node = anchorFocusRange->GetEndContainer();
788 offset = anchorFocusRange->EndOffset();
790 sel->CollapseInLimiter(node, offset);
792 sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
793 ScrollAxis(), ScrollAxis(), scrollFlags);
794 return NS_OK;
797 CaretAssociateHint tHint(mCaret.mHint); // temporary variable so we dont set
798 // mCaret.mHint until it is necessary
800 Result<PeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
801 direction, aContinueSelection, aAmount, aMovementStyle, desiredPos);
802 nsresult rv;
803 if (result.isOk() && result.inspect().mResultContent) {
804 const PeekOffsetStruct& pos = result.inspect();
805 nsIFrame* theFrame;
806 int32_t currentOffset, frameStart, frameEnd;
808 if (aAmount <= eSelectWordNoSpace) {
809 // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does
810 // not set pos.mAttachForward, so determine the hint here based on the
811 // result frame and offset: If we're at the end of a text frame, set the
812 // hint to ASSOCIATE_BEFORE to indicate that we want the caret displayed
813 // at the end of this frame, not at the beginning of the next one.
814 theFrame = pos.mResultFrame;
815 std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
816 currentOffset = pos.mContentOffset;
817 if (frameEnd == currentOffset && !(frameStart == 0 && frameEnd == 0))
818 tHint = CARET_ASSOCIATE_BEFORE;
819 else
820 tHint = CARET_ASSOCIATE_AFTER;
821 } else {
822 // For up/down and home/end, pos.mResultFrame might not be set correctly,
823 // or not at all. In these cases, get the frame based on the content and
824 // hint returned by PeekOffset().
825 tHint = pos.mAttach;
826 theFrame = GetFrameForNodeOffset(pos.mResultContent, pos.mContentOffset,
827 tHint, &currentOffset);
828 if (!theFrame) return NS_ERROR_FAILURE;
830 std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
833 if (context->BidiEnabled()) {
834 switch (aAmount) {
835 case eSelectBeginLine:
836 case eSelectEndLine: {
837 // In Bidi contexts, PeekOffset calculates pos.mContentOffset
838 // differently depending on whether the movement is visual or logical.
839 // For visual movement, pos.mContentOffset depends on the direction-
840 // ality of the first/last frame on the line (theFrame), and the caret
841 // directionality must correspond.
842 FrameBidiData bidiData = theFrame->GetBidiData();
843 SetCaretBidiLevelAndMaybeSchedulePaint(
844 visualMovement ? bidiData.embeddingLevel : bidiData.baseLevel);
845 break;
847 default:
848 // If the current position is not a frame boundary, it's enough just
849 // to take the Bidi level of the current frame
850 if ((pos.mContentOffset != frameStart &&
851 pos.mContentOffset != frameEnd) ||
852 eSelectLine == aAmount) {
853 SetCaretBidiLevelAndMaybeSchedulePaint(
854 theFrame->GetEmbeddingLevel());
855 } else {
856 BidiLevelFromMove(mPresShell, pos.mResultContent,
857 pos.mContentOffset, aAmount, tHint);
861 // "pos" is on the stack, so pos.mResultContent has stack lifetime, so using
862 // MOZ_KnownLive is ok.
863 const FocusMode focusMode = aContinueSelection
864 ? FocusMode::kExtendSelection
865 : FocusMode::kCollapseToNewPoint;
866 rv = TakeFocus(MOZ_KnownLive(*pos.mResultContent), pos.mContentOffset,
867 pos.mContentOffset, tHint, focusMode);
868 } else if (aAmount <= eSelectWordNoSpace && direction == eDirNext &&
869 !aContinueSelection) {
870 // Collapse selection if PeekOffset failed, we either
871 // 1. bumped into the BRFrame, bug 207623
872 // 2. had select-all in a text input (DIV range), bug 352759.
873 bool isBRFrame = frame->IsBrFrame();
874 RefPtr<nsINode> node = sel->GetFocusNode();
875 sel->CollapseInLimiter(node, sel->FocusOffset());
876 // Note: 'frame' might be dead here.
877 if (!isBRFrame) {
878 mCaret.mHint = CARET_ASSOCIATE_BEFORE; // We're now at the end of the
879 // frame to the left.
881 rv = NS_OK;
882 } else {
883 rv = result.isErr() ? result.unwrapErr() : NS_OK;
885 if (NS_SUCCEEDED(rv)) {
886 rv = sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
887 ScrollAxis(), ScrollAxis(), scrollFlags);
890 return rv;
893 Result<PeekOffsetStruct, nsresult> nsFrameSelection::PeekOffsetForCaretMove(
894 nsDirection aDirection, bool aContinueSelection,
895 const nsSelectionAmount aAmount, CaretMovementStyle aMovementStyle,
896 const nsPoint& aDesiredCaretPos) const {
897 Selection* selection =
898 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
899 if (!selection) {
900 return Err(NS_ERROR_NULL_POINTER);
903 const bool visualMovement =
904 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
906 int32_t offsetused = 0;
907 nsIFrame* frame =
908 selection->GetPrimaryFrameForFocusNode(visualMovement, &offsetused);
909 if (!frame) {
910 return Err(NS_ERROR_FAILURE);
913 // set data using mLimiters.mLimiter to stop on scroll views. If we have a
914 // limiter then we stop peeking when we hit scrollable views. If no limiter
915 // then just let it go ahead
916 PeekOffsetOptions options{PeekOffsetOption::JumpLines,
917 PeekOffsetOption::IsKeyboardSelect};
918 if (mLimiters.mLimiter) {
919 options += PeekOffsetOption::StopAtScroller;
921 if (visualMovement) {
922 options += PeekOffsetOption::Visual;
924 if (aContinueSelection) {
925 options += PeekOffsetOption::Extend;
927 if (selection->IsEditorSelection()) {
928 options += PeekOffsetOption::ForceEditableRegion;
930 PeekOffsetStruct pos(aAmount, aDirection, offsetused, aDesiredCaretPos,
931 options);
932 nsresult rv = frame->PeekOffset(&pos);
933 if (NS_FAILED(rv)) {
934 return Err(rv);
936 return pos;
939 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
940 nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const {
941 return GetPrevNextBidiLevels(aNode, aContentOffset, mCaret.mHint, aJumpLines);
944 // static
945 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
946 nsIContent* aNode, uint32_t aContentOffset, CaretAssociateHint aHint,
947 bool aJumpLines) {
948 // Get the level of the frames on each side
949 nsIFrame* currentFrame;
950 int32_t currentOffset;
951 nsDirection direction;
953 nsPrevNextBidiLevels levels{};
954 levels.SetData(nullptr, nullptr, mozilla::intl::BidiEmbeddingLevel::LTR(),
955 mozilla::intl::BidiEmbeddingLevel::LTR());
957 currentFrame = GetFrameForNodeOffset(
958 aNode, static_cast<int32_t>(aContentOffset), aHint, &currentOffset);
959 if (!currentFrame) {
960 return levels;
963 auto [frameStart, frameEnd] = currentFrame->GetOffsets();
965 if (0 == frameStart && 0 == frameEnd) {
966 direction = eDirPrevious;
967 } else if (frameStart == currentOffset) {
968 direction = eDirPrevious;
969 } else if (frameEnd == currentOffset) {
970 direction = eDirNext;
971 } else {
972 // we are neither at the beginning nor at the end of the frame, so we have
973 // no worries
974 mozilla::intl::BidiEmbeddingLevel currentLevel =
975 currentFrame->GetEmbeddingLevel();
976 levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel);
977 return levels;
980 PeekOffsetOptions peekOffsetOptions{PeekOffsetOption::StopAtScroller};
981 if (aJumpLines) {
982 peekOffsetOptions += PeekOffsetOption::JumpLines;
984 nsIFrame* newFrame =
985 currentFrame->GetFrameFromDirection(direction, peekOffsetOptions).mFrame;
987 FrameBidiData currentBidi = currentFrame->GetBidiData();
988 mozilla::intl::BidiEmbeddingLevel currentLevel = currentBidi.embeddingLevel;
989 mozilla::intl::BidiEmbeddingLevel newLevel =
990 newFrame ? newFrame->GetEmbeddingLevel() : currentBidi.baseLevel;
992 // If not jumping lines, disregard br frames, since they might be positioned
993 // incorrectly.
994 // XXX This could be removed once bug 339786 is fixed.
995 if (!aJumpLines) {
996 if (currentFrame->IsBrFrame()) {
997 currentFrame = nullptr;
998 currentLevel = currentBidi.baseLevel;
1000 if (newFrame && newFrame->IsBrFrame()) {
1001 newFrame = nullptr;
1002 newLevel = currentBidi.baseLevel;
1006 if (direction == eDirNext)
1007 levels.SetData(currentFrame, newFrame, currentLevel, newLevel);
1008 else
1009 levels.SetData(newFrame, currentFrame, newLevel, currentLevel);
1011 return levels;
1014 nsresult nsFrameSelection::GetFrameFromLevel(
1015 nsIFrame* aFrameIn, nsDirection aDirection,
1016 mozilla::intl::BidiEmbeddingLevel aBidiLevel, nsIFrame** aFrameOut) const {
1017 NS_ENSURE_STATE(mPresShell);
1018 mozilla::intl::BidiEmbeddingLevel foundLevel =
1019 mozilla::intl::BidiEmbeddingLevel::LTR();
1020 nsIFrame* foundFrame = aFrameIn;
1022 nsCOMPtr<nsIFrameEnumerator> frameTraversal;
1023 nsresult result;
1024 nsCOMPtr<nsIFrameTraversal> trav(
1025 do_CreateInstance(kFrameTraversalCID, &result));
1026 if (NS_FAILED(result)) return result;
1028 result =
1029 trav->NewFrameTraversal(getter_AddRefs(frameTraversal),
1030 mPresShell->GetPresContext(), aFrameIn, eLeaf,
1031 false, // aVisual
1032 false, // aLockInScrollView
1033 false, // aFollowOOFs
1034 false // aSkipPopupChecks
1036 if (NS_FAILED(result)) return result;
1038 do {
1039 *aFrameOut = foundFrame;
1040 foundFrame = frameTraversal->Traverse(aDirection == eDirNext);
1041 if (!foundFrame) return NS_ERROR_FAILURE;
1042 foundLevel = foundFrame->GetEmbeddingLevel();
1044 } while (foundLevel > aBidiLevel);
1046 return NS_OK;
1049 nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) {
1050 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1051 if (!mDomSelections[index]) {
1052 return NS_ERROR_NULL_POINTER;
1055 mMaintainedRange.MaintainAnchorFocusRange(*mDomSelections[index], aAmount);
1057 return NS_OK;
1060 void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell,
1061 nsIContent* aNode,
1062 uint32_t aContentOffset,
1063 nsSelectionAmount aAmount,
1064 CaretAssociateHint aHint) {
1065 switch (aAmount) {
1066 // Movement within the line: the new cursor Bidi level is the level of the
1067 // last character moved over
1068 case eSelectCharacter:
1069 case eSelectCluster:
1070 case eSelectWord:
1071 case eSelectWordNoSpace:
1072 case eSelectBeginLine:
1073 case eSelectEndLine:
1074 case eSelectNoAmount: {
1075 nsPrevNextBidiLevels levels =
1076 GetPrevNextBidiLevels(aNode, aContentOffset, aHint, false);
1078 SetCaretBidiLevelAndMaybeSchedulePaint(aHint == CARET_ASSOCIATE_BEFORE
1079 ? levels.mLevelBefore
1080 : levels.mLevelAfter);
1081 break;
1084 // Up and Down: the new cursor Bidi level is the smaller of the two
1085 surrounding characters case eSelectLine: case eSelectParagraph:
1086 GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame,
1087 &secondFrame, &firstLevel, &secondLevel);
1088 aPresShell->SetCaretBidiLevelAndMaybeSchedulePaint(std::min(firstLevel,
1089 secondLevel)); break;
1092 default:
1093 UndefineCaretBidiLevel();
1097 void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode,
1098 uint32_t aContentOffset) {
1099 nsIFrame* clickInFrame = nullptr;
1100 int32_t OffsetNotUsed;
1102 clickInFrame = GetFrameForNodeOffset(aNode, aContentOffset, mCaret.mHint,
1103 &OffsetNotUsed);
1104 if (!clickInFrame) return;
1106 SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel());
1109 void nsFrameSelection::MaintainedRange::AdjustNormalSelection(
1110 const nsIContent* aContent, const int32_t aOffset,
1111 Selection& aNormalSelection) const {
1112 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1114 if (!mRange || !aContent) {
1115 return;
1118 nsINode* rangeStartNode = mRange->GetStartContainer();
1119 nsINode* rangeEndNode = mRange->GetEndContainer();
1120 const uint32_t rangeStartOffset = mRange->StartOffset();
1121 const uint32_t rangeEndOffset = mRange->EndOffset();
1123 NS_ASSERTION(aOffset >= 0, "aOffset should not be negative");
1124 const Maybe<int32_t> relToStart =
1125 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1126 rangeStartNode, rangeStartOffset, aContent, aOffset);
1127 if (NS_WARN_IF(!relToStart)) {
1128 // Potentially handle this properly when Selection across Shadow DOM
1129 // boundary is implemented
1130 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1131 return;
1134 const Maybe<int32_t> relToEnd =
1135 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1136 rangeEndNode, rangeEndOffset, aContent, aOffset);
1137 if (NS_WARN_IF(!relToEnd)) {
1138 // Potentially handle this properly when Selection across Shadow DOM
1139 // boundary is implemented
1140 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1141 return;
1144 // If aContent/aOffset is inside (or at the edge of) the maintained
1145 // selection, or if it is on the "anchor" side of the maintained selection,
1146 // we need to do something.
1147 if ((*relToStart <= 0 && *relToEnd >= 0) ||
1148 (*relToStart > 0 && aNormalSelection.GetDirection() == eDirNext) ||
1149 (*relToEnd < 0 && aNormalSelection.GetDirection() == eDirPrevious)) {
1150 // Set the current range to the maintained range.
1151 aNormalSelection.ReplaceAnchorFocusRange(mRange);
1152 // Set the direction of the selection so that the anchor will be on the
1153 // far side of the maintained selection, relative to aContent/aOffset.
1154 aNormalSelection.SetDirection(*relToStart > 0 ? eDirPrevious : eDirNext);
1158 void nsFrameSelection::MaintainedRange::AdjustContentOffsets(
1159 nsIFrame::ContentOffsets& aOffsets, StopAtScroller aStopAtScroller) const {
1160 // Adjust offsets according to maintained amount
1161 if (mRange && mAmount != eSelectNoAmount) {
1162 nsINode* rangenode = mRange->GetStartContainer();
1163 int32_t rangeOffset = mRange->StartOffset();
1164 const Maybe<int32_t> relativePosition = nsContentUtils::ComparePoints(
1165 rangenode, rangeOffset, aOffsets.content, aOffsets.offset);
1166 if (NS_WARN_IF(!relativePosition)) {
1167 // Potentially handle this properly when Selection across Shadow DOM
1168 // boundary is implemented
1169 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1170 return;
1173 nsDirection direction = *relativePosition > 0 ? eDirPrevious : eDirNext;
1174 nsSelectionAmount amount = mAmount;
1175 if (amount == eSelectBeginLine && direction == eDirNext) {
1176 amount = eSelectEndLine;
1179 int32_t offset;
1180 nsIFrame* frame = GetFrameForNodeOffset(aOffsets.content, aOffsets.offset,
1181 CARET_ASSOCIATE_AFTER, &offset);
1183 PeekOffsetOptions peekOffsetOptions{};
1184 if (aStopAtScroller == StopAtScroller::Yes) {
1185 peekOffsetOptions += PeekOffsetOption::StopAtScroller;
1187 if (frame && amount == eSelectWord && direction == eDirPrevious) {
1188 // To avoid selecting the previous word when at start of word,
1189 // first move one character forward.
1190 PeekOffsetStruct charPos(eSelectCharacter, eDirNext, offset,
1191 nsPoint(0, 0), peekOffsetOptions);
1192 if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) {
1193 frame = charPos.mResultFrame;
1194 offset = charPos.mContentOffset;
1198 PeekOffsetStruct pos(amount, direction, offset, nsPoint(0, 0),
1199 peekOffsetOptions);
1200 if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) {
1201 aOffsets.content = pos.mResultContent;
1202 aOffsets.offset = pos.mContentOffset;
1207 void nsFrameSelection::MaintainedRange::MaintainAnchorFocusRange(
1208 const Selection& aNormalSelection, const nsSelectionAmount aAmount) {
1209 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1211 mAmount = aAmount;
1213 const nsRange* anchorFocusRange = aNormalSelection.GetAnchorFocusRange();
1214 if (anchorFocusRange && aAmount != eSelectNoAmount) {
1215 mRange = anchorFocusRange->CloneRange();
1216 return;
1219 mRange = nullptr;
1222 nsresult nsFrameSelection::HandleClick(nsIContent* aNewFocus,
1223 uint32_t aContentOffset,
1224 uint32_t aContentEndOffset,
1225 const FocusMode aFocusMode,
1226 CaretAssociateHint aHint) {
1227 if (!aNewFocus) return NS_ERROR_INVALID_ARG;
1229 if (MOZ_LOG_TEST(sFrameSelectionLog, LogLevel::Debug)) {
1230 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1231 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1232 ("%s: selection=%p, new focus=%p, offsets=(%u,%u), focus mode=%i",
1233 __FUNCTION__,
1234 mDomSelections[index] ? mDomSelections[index].get() : nullptr,
1235 aNewFocus, aContentOffset, aContentEndOffset,
1236 static_cast<int>(aFocusMode)));
1239 mDesiredCaretPos.Invalidate();
1241 if (aFocusMode != FocusMode::kExtendSelection) {
1242 mMaintainedRange.mRange = nullptr;
1243 if (!IsValidSelectionPoint(aNewFocus)) {
1244 mLimiters.mAncestorLimiter = nullptr;
1248 // Don't take focus when dragging off of a table
1249 if (!mTableSelection.mDragSelectingCells) {
1250 BidiLevelFromClick(aNewFocus, aContentOffset);
1251 SetChangeReasons(nsISelectionListener::MOUSEDOWN_REASON +
1252 nsISelectionListener::DRAG_REASON);
1254 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1255 RefPtr<Selection> selection = mDomSelections[index];
1256 MOZ_ASSERT(selection);
1258 if (aFocusMode == FocusMode::kExtendSelection) {
1259 mMaintainedRange.AdjustNormalSelection(aNewFocus, aContentOffset,
1260 *selection);
1263 AutoPrepareFocusRange prep(selection,
1264 aFocusMode == FocusMode::kMultiRangeSelection);
1265 return TakeFocus(*aNewFocus, aContentOffset, aContentEndOffset, aHint,
1266 aFocusMode);
1269 return NS_OK;
1272 void nsFrameSelection::HandleDrag(nsIFrame* aFrame, const nsPoint& aPoint) {
1273 if (!aFrame || !mPresShell) {
1274 return;
1277 nsresult result;
1278 nsIFrame* newFrame = 0;
1279 nsPoint newPoint;
1281 result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame,
1282 newPoint);
1283 if (NS_FAILED(result)) return;
1284 if (!newFrame) return;
1286 nsIFrame::ContentOffsets offsets =
1287 newFrame->GetContentOffsetsFromPoint(newPoint);
1288 if (!offsets.content) return;
1290 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1291 RefPtr<Selection> selection = mDomSelections[index];
1292 if (newFrame->IsSelected() && selection) {
1293 // `MOZ_KnownLive` required because of
1294 // https://bugzilla.mozilla.org/show_bug.cgi?id=1636889.
1295 mMaintainedRange.AdjustNormalSelection(MOZ_KnownLive(offsets.content),
1296 offsets.offset, *selection);
1299 mMaintainedRange.AdjustContentOffsets(
1300 offsets, mLimiters.mLimiter ? MaintainedRange::StopAtScroller::Yes
1301 : MaintainedRange::StopAtScroller::No);
1303 // TODO: no click has happened, rename `HandleClick`.
1304 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.offset,
1305 offsets.offset, FocusMode::kExtendSelection, offsets.associate);
1308 nsresult nsFrameSelection::StartAutoScrollTimer(nsIFrame* aFrame,
1309 const nsPoint& aPoint,
1310 uint32_t aDelay) {
1311 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1312 if (!mDomSelections[index]) {
1313 return NS_ERROR_NULL_POINTER;
1316 RefPtr<Selection> selection = mDomSelections[index];
1317 return selection->StartAutoScrollTimer(aFrame, aPoint, aDelay);
1320 void nsFrameSelection::StopAutoScrollTimer() {
1321 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1322 if (!mDomSelections[index]) {
1323 return;
1326 mDomSelections[index]->StopAutoScrollTimer();
1329 // static
1330 nsINode* nsFrameSelection::TableSelection::IsContentInActivelyEditableTableCell(
1331 nsPresContext* aContext, nsIContent* aContent) {
1332 if (!aContext) {
1333 return nullptr;
1336 RefPtr<HTMLEditor> htmlEditor = nsContentUtils::GetHTMLEditor(aContext);
1337 if (!htmlEditor) {
1338 return nullptr;
1341 nsINode* inclusiveTableCellAncestor =
1342 GetClosestInclusiveTableCellAncestor(aContent);
1343 if (!inclusiveTableCellAncestor) {
1344 return nullptr;
1347 const Element* editingHost = htmlEditor->ComputeEditingHost();
1348 if (!editingHost) {
1349 return nullptr;
1352 const bool editableCell =
1353 inclusiveTableCellAncestor->IsInclusiveDescendantOf(editingHost);
1354 return editableCell ? inclusiveTableCellAncestor : nullptr;
1357 namespace {
1358 struct ParentAndOffset {
1359 explicit ParentAndOffset(const nsINode& aNode)
1360 : mParent{aNode.GetParent()},
1361 mOffset{mParent ? mParent->ComputeIndexOf_Deprecated(&aNode) : 0} {}
1363 nsINode* mParent;
1365 // 0, if there's no parent.
1366 int32_t mOffset;
1369 } // namespace
1371 hard to go from nodes to frames, easy the other way!
1373 nsresult nsFrameSelection::TakeFocus(nsIContent& aNewFocus,
1374 uint32_t aContentOffset,
1375 uint32_t aContentEndOffset,
1376 CaretAssociateHint aHint,
1377 const FocusMode aFocusMode) {
1378 NS_ENSURE_STATE(mPresShell);
1380 if (!IsValidSelectionPoint(&aNewFocus)) {
1381 return NS_ERROR_FAILURE;
1384 MOZ_LOG(sFrameSelectionLog, LogLevel::Verbose,
1385 ("%s: new focus=%p, offsets=(%u, %u), hint=%i, focusMode=%i",
1386 __FUNCTION__, &aNewFocus, aContentOffset, aContentEndOffset,
1387 static_cast<int>(aHint), static_cast<int>(aFocusMode)));
1389 mPresShell->FrameSelectionWillTakeFocus(*this);
1391 // Clear all table selection data
1392 mTableSelection.mMode = TableSelectionMode::None;
1393 mTableSelection.mDragSelectingCells = false;
1394 mTableSelection.mStartSelectedCell = nullptr;
1395 mTableSelection.mEndSelectedCell = nullptr;
1396 mTableSelection.mAppendStartSelectedCell = nullptr;
1397 mCaret.mHint = aHint;
1399 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1400 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1402 Maybe<Selection::AutoUserInitiated> userSelect;
1403 if (IsUserSelectionReason()) {
1404 userSelect.emplace(mDomSelections[index]);
1407 // traverse through document and unselect crap here
1408 switch (aFocusMode) {
1409 case FocusMode::kCollapseToNewPoint:
1410 [[fallthrough]];
1411 case FocusMode::kMultiRangeSelection: {
1412 // single click? setting cursor down
1413 const Batching saveBatching =
1414 mBatching; // hack to use the collapse code.
1415 mBatching.mCounter = 1;
1417 RefPtr<Selection> selection = mDomSelections[index];
1419 if (aFocusMode == FocusMode::kMultiRangeSelection) {
1420 // Remove existing collapsed ranges as there's no point in having
1421 // non-anchor/focus collapsed ranges.
1422 selection->RemoveCollapsedRanges();
1424 ErrorResult error;
1425 RefPtr<nsRange> newRange = nsRange::Create(
1426 &aNewFocus, aContentOffset, &aNewFocus, aContentOffset, error);
1427 if (NS_WARN_IF(error.Failed())) {
1428 return error.StealNSResult();
1430 MOZ_ASSERT(newRange);
1431 selection->AddRangeAndSelectFramesAndNotifyListeners(*newRange,
1432 IgnoreErrors());
1433 } else {
1434 bool oldDesiredPosSet =
1435 mDesiredCaretPos.mIsSet; // need to keep old desired
1436 // position if it was set.
1437 selection->CollapseInLimiter(&aNewFocus, aContentOffset);
1438 mDesiredCaretPos.mIsSet =
1439 oldDesiredPosSet; // now reset desired pos back.
1442 mBatching = saveBatching;
1444 if (aContentEndOffset != aContentOffset) {
1445 selection->Extend(&aNewFocus, aContentEndOffset);
1448 // find out if we are inside a table. if so, find out which one and which
1449 // cell once we do that, the next time we get a takefocus, check the
1450 // parent tree. if we are no longer inside same table ,cell then switch to
1451 // table selection mode. BUT only do this in an editor
1453 NS_ENSURE_STATE(mPresShell);
1454 RefPtr<nsPresContext> context = mPresShell->GetPresContext();
1455 mTableSelection.mClosestInclusiveTableCellAncestor = nullptr;
1456 if (nsINode* inclusiveTableCellAncestor =
1457 TableSelection::IsContentInActivelyEditableTableCell(
1458 context, &aNewFocus)) {
1459 mTableSelection.mClosestInclusiveTableCellAncestor =
1460 inclusiveTableCellAncestor;
1461 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1462 ("%s: Collapsing into new cell", __FUNCTION__));
1465 break;
1467 case FocusMode::kExtendSelection: {
1468 // Now update the range list:
1469 nsINode* inclusiveTableCellAncestor =
1470 GetClosestInclusiveTableCellAncestor(&aNewFocus);
1471 if (mTableSelection.mClosestInclusiveTableCellAncestor &&
1472 inclusiveTableCellAncestor &&
1473 inclusiveTableCellAncestor !=
1474 mTableSelection
1475 .mClosestInclusiveTableCellAncestor) // switch to cell
1476 // selection mode
1478 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1479 ("%s: moving into new cell", __FUNCTION__));
1481 WidgetMouseEvent event(false, eVoidEvent, nullptr,
1482 WidgetMouseEvent::eReal);
1484 // Start selecting in the cell we were in before
1485 ParentAndOffset parentAndOffset{
1486 *mTableSelection.mClosestInclusiveTableCellAncestor};
1487 if (parentAndOffset.mParent) {
1488 const nsresult result = HandleTableSelection(
1489 parentAndOffset.mParent, parentAndOffset.mOffset,
1490 TableSelectionMode::Cell, &event);
1491 if (NS_WARN_IF(NS_FAILED(result))) {
1492 return result;
1496 // Find the parent of this new cell and extend selection to it
1497 parentAndOffset = ParentAndOffset{*inclusiveTableCellAncestor};
1499 // XXXX We need to REALLY get the current key shift state
1500 // (we'd need to add event listener -- let's not bother for now)
1501 event.mModifiers &= ~MODIFIER_SHIFT; // aContinueSelection;
1502 if (parentAndOffset.mParent) {
1503 mTableSelection.mClosestInclusiveTableCellAncestor =
1504 inclusiveTableCellAncestor;
1505 // Continue selection into next cell
1506 const nsresult result = HandleTableSelection(
1507 parentAndOffset.mParent, parentAndOffset.mOffset,
1508 TableSelectionMode::Cell, &event);
1509 if (NS_WARN_IF(NS_FAILED(result))) {
1510 return result;
1513 } else {
1514 RefPtr<Selection> selection = mDomSelections[index];
1515 // XXXX Problem: Shift+click in browser is appending text selection to
1516 // selected table!!!
1517 // is this the place to erase selected cells ?????
1518 uint32_t offset =
1519 (selection->GetDirection() == eDirNext &&
1520 aContentEndOffset > aContentOffset) // didn't go far enough
1521 ? aContentEndOffset // this will only redraw the diff
1522 : aContentOffset;
1523 selection->Extend(&aNewFocus, offset);
1525 break;
1529 // Don't notify selection listeners if batching is on:
1530 if (IsBatching()) {
1531 return NS_OK;
1534 // Be aware, the Selection instance may be destroyed after this call.
1535 return NotifySelectionListeners(SelectionType::eNormal);
1538 UniquePtr<SelectionDetails> nsFrameSelection::LookUpSelection(
1539 nsIContent* aContent, int32_t aContentOffset, int32_t aContentLength,
1540 bool aSlowCheck) const {
1541 if (!aContent || !mPresShell) {
1542 return nullptr;
1545 // TODO: Layout should use `uint32_t` for handling offset in DOM nodes
1546 // (for example: bug 1735262)
1547 MOZ_ASSERT(aContentOffset >= 0);
1548 MOZ_ASSERT(aContentLength >= 0);
1549 if (MOZ_UNLIKELY(aContentOffset < 0) || MOZ_UNLIKELY(aContentLength < 0)) {
1550 return nullptr;
1553 UniquePtr<SelectionDetails> details;
1555 for (size_t j = 0; j < ArrayLength(mDomSelections); j++) {
1556 if (mDomSelections[j]) {
1557 details = mDomSelections[j]->LookUpSelection(
1558 aContent, static_cast<uint32_t>(aContentOffset),
1559 static_cast<uint32_t>(aContentLength), std::move(details),
1560 kPresentSelectionTypes[j], aSlowCheck);
1564 // This may seem counter intuitive at first. Highlight selections need to be
1565 // iterated from back to front:
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, aSlowCheck);
1578 return details;
1581 void nsFrameSelection::SetDragState(bool aState) {
1582 if (mDragState == aState) return;
1584 mDragState = aState;
1586 if (!mDragState) {
1587 mTableSelection.mDragSelectingCells = false;
1588 // Notify that reason is mouse up.
1589 SetChangeReasons(nsISelectionListener::MOUSEUP_REASON);
1591 // flag is set to false in `NotifySelectionListeners`.
1592 // since this function call is part of click event, this would immediately
1593 // reset the flag, rendering it useless.
1594 AutoRestore<bool> restoreIsDoubleClickSelectionFlag(
1595 mIsDoubleClickSelection);
1596 // Be aware, the Selection instance may be destroyed after this call.
1597 NotifySelectionListeners(SelectionType::eNormal);
1601 Selection* nsFrameSelection::GetSelection(SelectionType aSelectionType) const {
1602 int8_t index = GetIndexFromSelectionType(aSelectionType);
1603 if (index < 0) return nullptr;
1605 return mDomSelections[index];
1608 void nsFrameSelection::AddHighlightSelection(
1609 nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight) {
1610 RefPtr<Selection> selection =
1611 aHighlight.CreateHighlightSelection(aHighlightName, this);
1612 if (auto iter =
1613 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1614 [&aHighlightName](auto const& aElm) {
1615 return aElm.first() == aHighlightName;
1617 iter != mHighlightSelections.end()) {
1618 iter->second() = std::move(selection);
1619 } else {
1620 mHighlightSelections.AppendElement(
1621 CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName,
1622 std::move(selection)));
1626 void nsFrameSelection::RemoveHighlightSelection(nsAtom* aHighlightName) {
1627 if (auto iter =
1628 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1629 [&aHighlightName](auto const& aElm) {
1630 return aElm.first() == aHighlightName;
1632 iter != mHighlightSelections.end()) {
1633 RefPtr<Selection> selection = iter->second();
1634 selection->RemoveAllRanges(IgnoreErrors());
1635 mHighlightSelections.RemoveElementAt(iter);
1639 void nsFrameSelection::AddHighlightSelectionRange(
1640 nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight,
1641 mozilla::dom::AbstractRange& aRange) {
1642 if (auto iter =
1643 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1644 [&aHighlightName](auto const& aElm) {
1645 return aElm.first() == aHighlightName;
1647 iter != mHighlightSelections.end()) {
1648 RefPtr<Selection> selection = iter->second();
1649 selection->AddHighlightRangeAndSelectFramesAndNotifyListeners(aRange);
1650 } else {
1651 // if the selection does not exist yet, add all of its ranges and exit.
1652 RefPtr<Selection> selection =
1653 aHighlight.CreateHighlightSelection(aHighlightName, this);
1654 mHighlightSelections.AppendElement(
1655 CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName,
1656 std::move(selection)));
1660 void nsFrameSelection::RemoveHighlightSelectionRange(
1661 nsAtom* aHighlightName, mozilla::dom::AbstractRange& aRange) {
1662 if (auto iter =
1663 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1664 [&aHighlightName](auto const& aElm) {
1665 return aElm.first() == aHighlightName;
1667 iter != mHighlightSelections.end()) {
1668 // NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
1669 RefPtr<Selection> selection = iter->second();
1670 selection->RemoveRangeAndUnselectFramesAndNotifyListeners(aRange,
1671 IgnoreErrors());
1675 nsresult nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType,
1676 SelectionRegion aRegion,
1677 int16_t aFlags) const {
1678 int8_t index = GetIndexFromSelectionType(aSelectionType);
1679 if (index < 0) return NS_ERROR_INVALID_ARG;
1681 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1683 ScrollAxis verticalScroll = ScrollAxis();
1684 int32_t flags = Selection::SCROLL_DO_FLUSH;
1685 if (aFlags & nsISelectionController::SCROLL_SYNCHRONOUS) {
1686 flags |= Selection::SCROLL_SYNCHRONOUS;
1687 } else if (aFlags & nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY) {
1688 flags |= Selection::SCROLL_FIRST_ANCESTOR_ONLY;
1690 if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) {
1691 flags |= Selection::SCROLL_OVERFLOW_HIDDEN;
1693 if (aFlags & nsISelectionController::SCROLL_CENTER_VERTICALLY) {
1694 verticalScroll =
1695 ScrollAxis(WhereToScroll::Center, WhenToScroll::IfNotFullyVisible);
1697 if (aFlags & nsISelectionController::SCROLL_FOR_CARET_MOVE) {
1698 flags |= Selection::SCROLL_FOR_CARET_MOVE;
1701 // After ScrollSelectionIntoView(), the pending notifications might be
1702 // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
1703 RefPtr<Selection> sel = mDomSelections[index];
1704 return sel->ScrollIntoView(aRegion, verticalScroll, ScrollAxis(), flags);
1707 nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) {
1708 int8_t index = GetIndexFromSelectionType(aSelectionType);
1709 if (index < 0) return NS_ERROR_INVALID_ARG;
1710 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1711 NS_ENSURE_STATE(mPresShell);
1713 // On macOS, update the selection cache to the new active selection
1714 // aka the current selection.
1715 #ifdef XP_MACOSX
1716 // Check that we're in the an active window and, if this is Web content,
1717 // in the frontmost tab.
1718 Document* doc = mPresShell->GetDocument();
1719 if (doc && IsInActiveTab(doc) && aSelectionType == SelectionType::eNormal) {
1720 UpdateSelectionCacheOnRepaintSelection(mDomSelections[index]);
1722 #endif
1723 return mDomSelections[index]->Repaint(mPresShell->GetPresContext());
1726 static bool IsDisplayContents(const nsIContent* aContent) {
1727 return aContent->IsElement() && aContent->AsElement()->IsDisplayContents();
1730 bool nsFrameSelection::AdjustFrameForLineStart(nsIFrame*& aFrame,
1731 int32_t& aFrameOffset) {
1732 if (!aFrame->HasSignificantTerminalNewline()) {
1733 return false;
1736 auto [start, end] = aFrame->GetOffsets();
1737 if (aFrameOffset != end) {
1738 return false;
1741 nsIFrame* nextSibling = aFrame->GetNextSibling();
1742 if (!nextSibling) {
1743 return false;
1746 aFrame = nextSibling;
1747 std::tie(start, end) = aFrame->GetOffsets();
1748 aFrameOffset = start;
1749 return true;
1752 // static
1753 nsIFrame* nsFrameSelection::GetFrameForNodeOffset(nsIContent* aNode,
1754 int32_t aOffset,
1755 CaretAssociateHint aHint,
1756 int32_t* aReturnOffset) {
1757 if (!aNode || !aReturnOffset) return nullptr;
1759 if (aOffset < 0) return nullptr;
1761 if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) {
1762 return nullptr;
1765 nsIFrame* returnFrame = nullptr;
1766 nsCOMPtr<nsIContent> theNode;
1768 while (true) {
1769 *aReturnOffset = aOffset;
1771 theNode = aNode;
1773 if (aNode->IsElement()) {
1774 int32_t childIndex = 0;
1775 int32_t numChildren = theNode->GetChildCount();
1777 if (aHint == CARET_ASSOCIATE_BEFORE) {
1778 if (aOffset > 0) {
1779 childIndex = aOffset - 1;
1780 } else {
1781 childIndex = aOffset;
1783 } else {
1784 NS_ASSERTION(aHint == CARET_ASSOCIATE_AFTER, "unknown direction");
1785 if (aOffset >= numChildren) {
1786 if (numChildren > 0) {
1787 childIndex = numChildren - 1;
1788 } else {
1789 childIndex = 0;
1791 } else {
1792 childIndex = aOffset;
1796 if (childIndex > 0 || numChildren > 0) {
1797 nsCOMPtr<nsIContent> childNode =
1798 theNode->GetChildAt_Deprecated(childIndex);
1800 if (!childNode) {
1801 break;
1804 theNode = childNode;
1807 // Now that we have the child node, check if it too
1808 // can contain children. If so, descend into child.
1809 if (theNode->IsElement() && theNode->GetChildCount() &&
1810 !theNode->HasIndependentSelection()) {
1811 aNode = theNode;
1812 aOffset = aOffset > childIndex ? theNode->GetChildCount() : 0;
1813 continue;
1814 } else {
1815 // Check to see if theNode is a text node. If it is, translate
1816 // aOffset into an offset into the text node.
1818 RefPtr<Text> textNode = theNode->GetAsText();
1819 if (textNode) {
1820 if (theNode->GetPrimaryFrame()) {
1821 if (aOffset > childIndex) {
1822 uint32_t textLength = textNode->Length();
1824 *aReturnOffset = (int32_t)textLength;
1825 } else {
1826 *aReturnOffset = 0;
1828 } else {
1829 int32_t numChildren = aNode->GetChildCount();
1830 int32_t newChildIndex = aHint == CARET_ASSOCIATE_BEFORE
1831 ? childIndex - 1
1832 : childIndex + 1;
1834 if (newChildIndex >= 0 && newChildIndex < numChildren) {
1835 nsCOMPtr<nsIContent> newChildNode =
1836 aNode->GetChildAt_Deprecated(newChildIndex);
1837 if (!newChildNode) {
1838 return nullptr;
1841 aNode = newChildNode;
1842 aOffset =
1843 aHint == CARET_ASSOCIATE_BEFORE ? aNode->GetChildCount() : 0;
1844 continue;
1845 } else {
1846 // newChildIndex is illegal which means we're at first or last
1847 // child. Just use original node to get the frame.
1848 theNode = aNode;
1855 // If the node is a ShadowRoot, the frame needs to be adjusted,
1856 // because a ShadowRoot does not get a frame. Its children are rendered
1857 // as children of the host.
1858 if (ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) {
1859 theNode = shadow->GetHost();
1862 returnFrame = theNode->GetPrimaryFrame();
1863 if (!returnFrame) {
1864 if (aHint == CARET_ASSOCIATE_BEFORE) {
1865 if (aOffset > 0) {
1866 --aOffset;
1867 continue;
1868 } else {
1869 break;
1871 } else {
1872 int32_t end = theNode->GetChildCount();
1873 if (aOffset < end) {
1874 ++aOffset;
1875 continue;
1876 } else {
1877 break;
1882 break;
1883 } // end while
1885 if (!returnFrame) return nullptr;
1887 // If we ended up here and were asked to position the caret after a visible
1888 // break, let's return the frame on the next line instead if it exists.
1889 if (aOffset > 0 && (uint32_t)aOffset >= aNode->Length() &&
1890 theNode == aNode->GetLastChild()) {
1891 nsIFrame* newFrame;
1892 nsLayoutUtils::IsInvisibleBreak(theNode, &newFrame);
1893 if (newFrame) {
1894 returnFrame = newFrame;
1895 *aReturnOffset = 0;
1899 // find the child frame containing the offset we want
1900 returnFrame->GetChildFrameContainingOffset(
1901 *aReturnOffset, aHint == CARET_ASSOCIATE_AFTER, &aOffset, &returnFrame);
1902 return returnFrame;
1905 nsIFrame* nsFrameSelection::GetFrameToPageSelect() const {
1906 if (NS_WARN_IF(!mPresShell)) {
1907 return nullptr;
1910 nsIFrame* rootFrameToSelect;
1911 if (mLimiters.mLimiter) {
1912 rootFrameToSelect = mLimiters.mLimiter->GetPrimaryFrame();
1913 if (NS_WARN_IF(!rootFrameToSelect)) {
1914 return nullptr;
1916 } else if (mLimiters.mAncestorLimiter) {
1917 rootFrameToSelect = mLimiters.mAncestorLimiter->GetPrimaryFrame();
1918 if (NS_WARN_IF(!rootFrameToSelect)) {
1919 return nullptr;
1921 } else {
1922 rootFrameToSelect = mPresShell->GetRootScrollFrame();
1923 if (NS_WARN_IF(!rootFrameToSelect)) {
1924 return nullptr;
1928 nsCOMPtr<nsIContent> contentToSelect = mPresShell->GetContentForScrolling();
1929 if (contentToSelect) {
1930 // If there is selected content, look for nearest and vertical scrollable
1931 // parent under the root frame.
1932 for (nsIFrame* frame = contentToSelect->GetPrimaryFrame();
1933 frame && frame != rootFrameToSelect; frame = frame->GetParent()) {
1934 nsIScrollableFrame* scrollableFrame = do_QueryFrame(frame);
1935 if (!scrollableFrame) {
1936 continue;
1938 ScrollStyles scrollStyles = scrollableFrame->GetScrollStyles();
1939 if (scrollStyles.mVertical == StyleOverflow::Hidden) {
1940 continue;
1942 layers::ScrollDirections directions =
1943 scrollableFrame->GetAvailableScrollingDirections();
1944 if (directions.contains(layers::ScrollDirection::eVertical)) {
1945 // If there is sub scrollable frame, let's use its page size to select.
1946 return frame;
1950 // Otherwise, i.e., there is no scrollable frame or only the root frame is
1951 // scrollable, let's return the root frame because Shift + PageUp/PageDown
1952 // should expand the selection in the root content even if it's not
1953 // scrollable.
1954 return rootFrameToSelect;
1957 nsresult nsFrameSelection::PageMove(bool aForward, bool aExtend,
1958 nsIFrame* aFrame,
1959 SelectionIntoView aSelectionIntoView) {
1960 MOZ_ASSERT(aFrame);
1962 // expected behavior for PageMove is to scroll AND move the caret
1963 // and remain relative position of the caret in view. see Bug 4302.
1965 // Get the scrollable frame. If aFrame is not scrollable, this is nullptr.
1966 nsIScrollableFrame* scrollableFrame = aFrame->GetScrollTargetFrame();
1967 // Get the scrolled frame. If aFrame is not scrollable, this is aFrame
1968 // itself.
1969 nsIFrame* scrolledFrame =
1970 scrollableFrame ? scrollableFrame->GetScrolledFrame() : aFrame;
1971 if (!scrolledFrame) {
1972 return NS_OK;
1975 // find out where the caret is.
1976 // we should know mDesiredCaretPos.mValue value of nsFrameSelection, but I
1977 // havent seen that behavior in other windows applications yet.
1978 RefPtr<Selection> selection = GetSelection(SelectionType::eNormal);
1979 if (!selection) {
1980 return NS_OK;
1983 nsRect caretPos;
1984 nsIFrame* caretFrame = nsCaret::GetGeometry(selection, &caretPos);
1985 if (!caretFrame) {
1986 return NS_OK;
1989 // If the scrolled frame is outside of current selection limiter,
1990 // we need to scroll the frame but keep moving selection in the limiter.
1991 nsIFrame* frameToClick = scrolledFrame;
1992 if (!IsValidSelectionPoint(scrolledFrame->GetContent())) {
1993 frameToClick = GetFrameToPageSelect();
1994 if (NS_WARN_IF(!frameToClick)) {
1995 return NS_OK;
1999 if (scrollableFrame) {
2000 // If there is a scrollable frame, adjust pseudo-click position with page
2001 // scroll amount.
2002 // XXX This may scroll more than one page if ScrollSelectionIntoView is
2003 // called later because caret may not fully visible. E.g., if
2004 // clicking line will be visible only half height with scrolling
2005 // the frame, ScrollSelectionIntoView additionally scrolls to show
2006 // the caret entirely.
2007 if (aForward) {
2008 caretPos.y += scrollableFrame->GetPageScrollAmount().height;
2009 } else {
2010 caretPos.y -= scrollableFrame->GetPageScrollAmount().height;
2012 } else {
2013 // Otherwise, adjust pseudo-click position with the frame size.
2014 if (aForward) {
2015 caretPos.y += frameToClick->GetSize().height;
2016 } else {
2017 caretPos.y -= frameToClick->GetSize().height;
2021 caretPos += caretFrame->GetOffsetTo(frameToClick);
2023 // get a content at desired location
2024 nsPoint desiredPoint;
2025 desiredPoint.x = caretPos.x;
2026 desiredPoint.y = caretPos.y + caretPos.height / 2;
2027 nsIFrame::ContentOffsets offsets =
2028 frameToClick->GetContentOffsetsFromPoint(desiredPoint);
2030 if (!offsets.content) {
2031 // XXX Do we need to handle ScrollSelectionIntoView in this case?
2032 return NS_OK;
2035 // First, place the caret.
2036 bool selectionChanged;
2038 // We don't want any script to run until we check whether selection is
2039 // modified by HandleClick.
2040 SelectionBatcher ensureNoSelectionChangeNotifications(selection,
2041 __FUNCTION__);
2043 RangeBoundary oldAnchor = selection->AnchorRef();
2044 RangeBoundary oldFocus = selection->FocusRef();
2046 const FocusMode focusMode =
2047 aExtend ? FocusMode::kExtendSelection : FocusMode::kCollapseToNewPoint;
2048 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
2049 offsets.offset, offsets.offset, focusMode,
2050 CARET_ASSOCIATE_AFTER);
2052 selectionChanged = selection->AnchorRef() != oldAnchor ||
2053 selection->FocusRef() != oldFocus;
2056 bool doScrollSelectionIntoView = !(
2057 aSelectionIntoView == SelectionIntoView::IfChanged && !selectionChanged);
2059 // Then, scroll the given frame one page.
2060 if (scrollableFrame) {
2061 // If we'll call ScrollSelectionIntoView later and selection wasn't
2062 // changed and we scroll outside of selection limiter, we shouldn't use
2063 // smooth scroll here because nsIScrollableFrame uses normal runnable,
2064 // but ScrollSelectionIntoView uses early runner and it cancels the
2065 // pending smooth scroll. Therefore, if we used smooth scroll in such
2066 // case, ScrollSelectionIntoView would scroll to show caret instead of
2067 // page scroll of an element outside selection limiter.
2068 ScrollMode scrollMode = doScrollSelectionIntoView && !selectionChanged &&
2069 scrolledFrame != frameToClick
2070 ? ScrollMode::Instant
2071 : ScrollMode::Smooth;
2072 scrollableFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
2073 ScrollUnit::PAGES, scrollMode);
2076 // Finally, scroll selection into view if requested.
2077 if (!doScrollSelectionIntoView) {
2078 return NS_OK;
2080 return ScrollSelectionIntoView(
2081 SelectionType::eNormal, nsISelectionController::SELECTION_FOCUS_REGION,
2082 nsISelectionController::SCROLL_SYNCHRONOUS |
2083 nsISelectionController::SCROLL_FOR_CARET_MOVE);
2086 nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount,
2087 bool aExtend) {
2088 NS_ENSURE_STATE(mPresShell);
2089 // Flush out layout, since we need it to be up to date to do caret
2090 // positioning.
2091 OwningNonNull<PresShell> presShell(*mPresShell);
2092 presShell->FlushPendingNotifications(FlushType::Layout);
2094 if (!mPresShell) {
2095 return NS_OK;
2098 // Check that parameters are safe
2099 if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) {
2100 return NS_ERROR_FAILURE;
2103 nsPresContext* context = mPresShell->GetPresContext();
2104 if (!context) {
2105 return NS_ERROR_FAILURE;
2108 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2109 RefPtr<Selection> sel = mDomSelections[index];
2110 if (!sel) {
2111 return NS_ERROR_NULL_POINTER;
2114 // Map the abstract movement amounts (0-1) to direction-specific
2115 // selection units.
2116 static const nsSelectionAmount inlineAmount[] = {eSelectCluster, eSelectWord};
2117 static const nsSelectionAmount blockPrevAmount[] = {eSelectLine,
2118 eSelectBeginLine};
2119 static const nsSelectionAmount blockNextAmount[] = {eSelectLine,
2120 eSelectEndLine};
2122 struct PhysicalToLogicalMapping {
2123 nsDirection direction;
2124 const nsSelectionAmount* amounts;
2126 static const PhysicalToLogicalMapping verticalLR[4] = {
2127 {eDirPrevious, blockPrevAmount}, // left
2128 {eDirNext, blockNextAmount}, // right
2129 {eDirPrevious, inlineAmount}, // up
2130 {eDirNext, inlineAmount} // down
2132 static const PhysicalToLogicalMapping verticalRL[4] = {
2133 {eDirNext, blockNextAmount},
2134 {eDirPrevious, blockPrevAmount},
2135 {eDirPrevious, inlineAmount},
2136 {eDirNext, inlineAmount}};
2137 static const PhysicalToLogicalMapping horizontal[4] = {
2138 {eDirPrevious, inlineAmount},
2139 {eDirNext, inlineAmount},
2140 {eDirPrevious, blockPrevAmount},
2141 {eDirNext, blockNextAmount}};
2143 WritingMode wm;
2144 nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(true);
2145 if (frame) {
2146 if (!frame->Style()->IsTextCombined()) {
2147 wm = frame->GetWritingMode();
2148 } else {
2149 // Using different direction for horizontal-in-vertical would
2150 // make it hard to navigate via keyboard. Inherit the moving
2151 // direction from its parent.
2152 MOZ_ASSERT(frame->IsTextFrame());
2153 wm = frame->GetParent()->GetWritingMode();
2154 MOZ_ASSERT(wm.IsVertical(),
2155 "Text combined "
2156 "can only appear in vertical text");
2160 const PhysicalToLogicalMapping& mapping =
2161 wm.IsVertical()
2162 ? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection]
2163 : horizontal[aDirection];
2165 nsresult rv =
2166 MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount], eVisual);
2167 if (NS_FAILED(rv)) {
2168 // If we tried to do a line move, but couldn't move in the given direction,
2169 // then we'll "promote" this to a line-edge move instead.
2170 if (mapping.amounts[aAmount] == eSelectLine) {
2171 rv = MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount + 1],
2172 eVisual);
2174 // And if it was a next-word move that failed (which can happen when
2175 // eat_space_to_next_word is true, see bug 1153237), then just move forward
2176 // to the line-edge.
2177 else if (mapping.amounts[aAmount] == eSelectWord &&
2178 mapping.direction == eDirNext) {
2179 rv = MoveCaret(eDirNext, aExtend, eSelectEndLine, eVisual);
2183 return rv;
2186 nsresult nsFrameSelection::CharacterMove(bool aForward, bool aExtend) {
2187 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectCluster,
2188 eUsePrefStyle);
2191 nsresult nsFrameSelection::WordMove(bool aForward, bool aExtend) {
2192 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectWord,
2193 eUsePrefStyle);
2196 nsresult nsFrameSelection::LineMove(bool aForward, bool aExtend) {
2197 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectLine,
2198 eUsePrefStyle);
2201 nsresult nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) {
2202 if (aForward) {
2203 return MoveCaret(eDirNext, aExtend, eSelectEndLine, eLogical);
2204 } else {
2205 return MoveCaret(eDirPrevious, aExtend, eSelectBeginLine, eLogical);
2209 template <typename RangeType>
2210 Result<RefPtr<RangeType>, nsresult>
2211 nsFrameSelection::CreateRangeExtendedToSomewhere(
2212 nsDirection aDirection, const nsSelectionAmount aAmount,
2213 CaretMovementStyle aMovementStyle) {
2214 MOZ_ASSERT(aDirection == eDirNext || aDirection == eDirPrevious);
2215 MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster ||
2216 aAmount == eSelectWord || aAmount == eSelectBeginLine ||
2217 aAmount == eSelectEndLine);
2218 MOZ_ASSERT(aMovementStyle == eLogical || aMovementStyle == eVisual ||
2219 aMovementStyle == eUsePrefStyle);
2221 if (!mPresShell) {
2222 return Err(NS_ERROR_UNEXPECTED);
2224 OwningNonNull<PresShell> presShell(*mPresShell);
2225 presShell->FlushPendingNotifications(FlushType::Layout);
2226 if (!mPresShell) {
2227 return Err(NS_ERROR_FAILURE);
2229 Selection* selection =
2230 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
2231 if (!selection || selection->RangeCount() != 1) {
2232 return Err(NS_ERROR_FAILURE);
2234 RefPtr<const nsRange> firstRange = selection->GetRangeAt(0);
2235 if (!firstRange || !firstRange->IsPositioned()) {
2236 return Err(NS_ERROR_FAILURE);
2238 Result<PeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
2239 aDirection, true, aAmount, aMovementStyle, nsPoint(0, 0));
2240 if (result.isErr()) {
2241 return Err(NS_ERROR_FAILURE);
2243 const PeekOffsetStruct& pos = result.inspect();
2244 RefPtr<RangeType> range;
2245 if (NS_WARN_IF(!pos.mResultContent)) {
2246 return range;
2248 if (aDirection == eDirPrevious) {
2249 range = RangeType::Create(
2250 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
2251 firstRange->EndRef(), IgnoreErrors());
2252 } else {
2253 range = RangeType::Create(
2254 firstRange->StartRef(),
2255 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
2256 IgnoreErrors());
2258 return range;
2261 //////////END FRAMESELECTION
2263 LazyLogModule gBatchLog("SelectionBatch");
2265 void nsFrameSelection::StartBatchChanges(const char* aRequesterFuncName) {
2266 MOZ_LOG(gBatchLog, LogLevel::Info,
2267 ("%p%snsFrameSelection::StartBatchChanges(%s)", this,
2268 std::string((mBatching.mCounter + 1) * 2, ' ').c_str(),
2269 aRequesterFuncName));
2270 mBatching.mCounter++;
2273 void nsFrameSelection::EndBatchChanges(const char* aRequesterFuncName,
2274 int16_t aReasons) {
2275 MOZ_LOG(gBatchLog, LogLevel::Info,
2276 ("%p%snsFrameSelection::EndBatchChanges (%s, %s)", this,
2277 std::string(mBatching.mCounter * 2, ' ').c_str(), aRequesterFuncName,
2278 SelectionChangeReasonsToCString(aReasons).get()));
2279 MOZ_ASSERT(mBatching.mCounter > 0, "Bad mBatching.mCounter");
2280 mBatching.mCounter--;
2282 if (mBatching.mCounter == 0 && mBatching.mChangesDuringBatching) {
2283 AddChangeReasons(aReasons);
2284 mCaretMoveAmount = eSelectNoAmount;
2285 mBatching.mChangesDuringBatching = false;
2286 // Be aware, the Selection instance may be destroyed after this call.
2287 NotifySelectionListeners(SelectionType::eNormal);
2291 nsresult nsFrameSelection::NotifySelectionListeners(
2292 SelectionType aSelectionType) {
2293 int8_t index = GetIndexFromSelectionType(aSelectionType);
2294 if (index >= 0 && mDomSelections[index]) {
2295 RefPtr<Selection> selection = mDomSelections[index];
2296 selection->NotifySelectionListeners();
2297 mCaretMoveAmount = eSelectNoAmount;
2298 return NS_OK;
2300 return NS_ERROR_FAILURE;
2303 // Start of Table Selection methods
2305 static bool IsCell(nsIContent* aContent) {
2306 return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
2309 // static
2310 nsITableCellLayout* nsFrameSelection::GetCellLayout(
2311 const nsIContent* aCellContent) {
2312 nsITableCellLayout* cellLayoutObject =
2313 do_QueryFrame(aCellContent->GetPrimaryFrame());
2314 return cellLayoutObject;
2317 nsresult nsFrameSelection::ClearNormalSelection() {
2318 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2319 RefPtr<Selection> selection = mDomSelections[index];
2320 if (!selection) {
2321 return NS_ERROR_NULL_POINTER;
2324 ErrorResult err;
2325 selection->RemoveAllRanges(err);
2326 return err.StealNSResult();
2329 static nsIContent* GetFirstSelectedContent(const nsRange* aRange) {
2330 if (!aRange) {
2331 return nullptr;
2334 MOZ_ASSERT(aRange->GetStartContainer(), "Must have start parent!");
2335 MOZ_ASSERT(aRange->GetStartContainer()->IsElement(), "Unexpected parent");
2337 return aRange->GetChildAtStartOffset();
2340 // Table selection support.
2341 // TODO: Separate table methods into a separate nsITableSelection interface
2342 nsresult nsFrameSelection::HandleTableSelection(nsINode* aParentContent,
2343 int32_t aContentOffset,
2344 TableSelectionMode aTarget,
2345 WidgetMouseEvent* aMouseEvent) {
2346 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2347 RefPtr<Selection> selection = mDomSelections[index];
2348 if (!selection) {
2349 return NS_ERROR_NULL_POINTER;
2352 return mTableSelection.HandleSelection(aParentContent, aContentOffset,
2353 aTarget, aMouseEvent, mDragState,
2354 *selection);
2357 nsresult nsFrameSelection::TableSelection::HandleSelection(
2358 nsINode* aParentContent, int32_t aContentOffset, TableSelectionMode aTarget,
2359 WidgetMouseEvent* aMouseEvent, bool aDragState,
2360 Selection& aNormalSelection) {
2361 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2363 NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER);
2364 NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER);
2366 if (aDragState && mDragSelectingCells &&
2367 aTarget == TableSelectionMode::Table) {
2368 // We were selecting cells and user drags mouse in table border or inbetween
2369 // cells,
2370 // just do nothing
2371 return NS_OK;
2374 RefPtr<nsIContent> childContent =
2375 aParentContent->GetChildAt_Deprecated(aContentOffset);
2377 // When doing table selection, always set the direction to next so
2378 // we can be sure that anchorNode's offset always points to the
2379 // selected cell
2380 aNormalSelection.SetDirection(eDirNext);
2382 // Stack-class to wrap all table selection changes in
2383 // BeginBatchChanges() / EndBatchChanges()
2384 SelectionBatcher selectionBatcher(&aNormalSelection, __FUNCTION__);
2386 if (aDragState && mDragSelectingCells) {
2387 return HandleDragSelecting(aTarget, childContent, aMouseEvent,
2388 aNormalSelection);
2391 return HandleMouseUpOrDown(aTarget, aDragState, childContent, aParentContent,
2392 aContentOffset, aMouseEvent, aNormalSelection);
2395 class nsFrameSelection::TableSelection::RowAndColumnRelation {
2396 public:
2397 static Result<RowAndColumnRelation, nsresult> Create(
2398 const nsIContent* aFirst, const nsIContent* aSecond) {
2399 RowAndColumnRelation result;
2401 nsresult errorResult =
2402 GetCellIndexes(aFirst, result.mFirst.mRow, result.mFirst.mColumn);
2403 if (NS_FAILED(errorResult)) {
2404 return Err(errorResult);
2407 errorResult =
2408 GetCellIndexes(aSecond, result.mSecond.mRow, result.mSecond.mColumn);
2409 if (NS_FAILED(errorResult)) {
2410 return Err(errorResult);
2413 return result;
2416 bool IsSameColumn() const { return mFirst.mColumn == mSecond.mColumn; }
2418 bool IsSameRow() const { return mFirst.mRow == mSecond.mRow; }
2420 private:
2421 RowAndColumnRelation() = default;
2423 struct RowAndColumn {
2424 int32_t mRow = 0;
2425 int32_t mColumn = 0;
2428 RowAndColumn mFirst;
2429 RowAndColumn mSecond;
2432 nsresult nsFrameSelection::TableSelection::HandleDragSelecting(
2433 TableSelectionMode aTarget, nsIContent* aChildContent,
2434 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2435 // We are drag-selecting
2436 if (aTarget != TableSelectionMode::Table) {
2437 // If dragging in the same cell as last event, do nothing
2438 if (mEndSelectedCell == aChildContent) {
2439 return NS_OK;
2442 #ifdef DEBUG_TABLE_SELECTION
2443 printf(
2444 " mStartSelectedCell = %p, "
2445 "mEndSelectedCell = %p, aChildContent = %p "
2446 "\n",
2447 mStartSelectedCell.get(), mEndSelectedCell.get(), aChildContent);
2448 #endif
2449 // aTarget can be any "cell mode",
2450 // so we can easily drag-select rows and columns
2451 // Once we are in row or column mode,
2452 // we can drift into any cell to stay in that mode
2453 // even if aTarget = TableSelectionMode::Cell
2455 if (mMode == TableSelectionMode::Row ||
2456 mMode == TableSelectionMode::Column) {
2457 if (mEndSelectedCell) {
2458 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2459 RowAndColumnRelation::Create(mEndSelectedCell, aChildContent);
2461 if (rowAndColumnRelation.isErr()) {
2462 return rowAndColumnRelation.unwrapErr();
2465 if ((mMode == TableSelectionMode::Row &&
2466 rowAndColumnRelation.inspect().IsSameRow()) ||
2467 (mMode == TableSelectionMode::Column &&
2468 rowAndColumnRelation.inspect().IsSameColumn())) {
2469 return NS_OK;
2472 #ifdef DEBUG_TABLE_SELECTION
2473 printf(" Dragged into a new column or row\n");
2474 #endif
2475 // Continue dragging row or column selection
2477 return SelectRowOrColumn(aChildContent, aNormalSelection);
2479 if (mMode == TableSelectionMode::Cell) {
2480 #ifdef DEBUG_TABLE_SELECTION
2481 printf("HandleTableSelection: Dragged into a new cell\n");
2482 #endif
2483 // Trick for quick selection of rows and columns
2484 // Hold down shift, then start selecting in one direction
2485 // If next cell dragged into is in same row, select entire row,
2486 // if next cell is in same column, select entire column
2487 if (mStartSelectedCell && aMouseEvent->IsShift()) {
2488 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2489 RowAndColumnRelation::Create(mStartSelectedCell, aChildContent);
2490 if (rowAndColumnRelation.isErr()) {
2491 return rowAndColumnRelation.unwrapErr();
2494 if (rowAndColumnRelation.inspect().IsSameRow() ||
2495 rowAndColumnRelation.inspect().IsSameColumn()) {
2496 // Force new selection block
2497 mStartSelectedCell = nullptr;
2498 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2500 if (rowAndColumnRelation.inspect().IsSameRow()) {
2501 mMode = TableSelectionMode::Row;
2502 } else {
2503 mMode = TableSelectionMode::Column;
2506 return SelectRowOrColumn(aChildContent, aNormalSelection);
2510 // Reselect block of cells to new end location
2511 return SelectBlockOfCells(mStartSelectedCell, aChildContent,
2512 aNormalSelection);
2515 // Do nothing if dragging in table, but outside a cell
2516 return NS_OK;
2519 nsresult nsFrameSelection::TableSelection::HandleMouseUpOrDown(
2520 TableSelectionMode aTarget, bool aDragState, nsIContent* aChildContent,
2521 nsINode* aParentContent, int32_t aContentOffset,
2522 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2523 nsresult result = NS_OK;
2524 // Not dragging -- mouse event is down or up
2525 if (aDragState) {
2526 #ifdef DEBUG_TABLE_SELECTION
2527 printf("HandleTableSelection: Mouse down event\n");
2528 #endif
2529 // Clear cell we stored in mouse-down
2530 mUnselectCellOnMouseUp = nullptr;
2532 if (aTarget == TableSelectionMode::Cell) {
2533 bool isSelected = false;
2535 // Check if we have other selected cells
2536 nsIContent* previousCellNode =
2537 GetFirstSelectedContent(GetFirstCellRange(aNormalSelection));
2538 if (previousCellNode) {
2539 // We have at least 1 other selected cell
2541 // Check if new cell is already selected
2542 nsIFrame* cellFrame = aChildContent->GetPrimaryFrame();
2543 if (!cellFrame) {
2544 return NS_ERROR_NULL_POINTER;
2546 isSelected = cellFrame->IsSelected();
2547 } else {
2548 // No cells selected -- remove non-cell selection
2549 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2551 mDragSelectingCells = true; // Signal to start drag-cell-selection
2552 mMode = aTarget;
2553 // Set start for new drag-selection block (not appended)
2554 mStartSelectedCell = aChildContent;
2555 // The initial block end is same as the start
2556 mEndSelectedCell = aChildContent;
2558 if (isSelected) {
2559 // Remember this cell to (possibly) unselect it on mouseup
2560 mUnselectCellOnMouseUp = aChildContent;
2561 #ifdef DEBUG_TABLE_SELECTION
2562 printf(
2563 "HandleTableSelection: Saving "
2564 "mUnselectCellOnMouseUp\n");
2565 #endif
2566 } else {
2567 // Select an unselected cell
2568 // but first remove existing selection if not in same table
2569 if (previousCellNode &&
2570 !IsInSameTable(previousCellNode, aChildContent)) {
2571 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2572 // Reset selection mode that is cleared in RemoveAllRanges
2573 mMode = aTarget;
2576 return ::SelectCellElement(aChildContent, aNormalSelection);
2579 return NS_OK;
2581 if (aTarget == TableSelectionMode::Table) {
2582 // TODO: We currently select entire table when clicked between cells,
2583 // should we restrict to only around border?
2584 // *** How do we get location data for cell and click?
2585 mDragSelectingCells = false;
2586 mStartSelectedCell = nullptr;
2587 mEndSelectedCell = nullptr;
2589 // Remove existing selection and select the table
2590 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2591 return CreateAndAddRange(aParentContent, aContentOffset,
2592 aNormalSelection);
2594 if (aTarget == TableSelectionMode::Row ||
2595 aTarget == TableSelectionMode::Column) {
2596 #ifdef DEBUG_TABLE_SELECTION
2597 printf("aTarget == %d\n", aTarget);
2598 #endif
2600 // Start drag-selecting mode so multiple rows/cols can be selected
2601 // Note: Currently, nsIFrame::GetDataForTableSelection
2602 // will never call us for row or column selection on mouse down
2603 mDragSelectingCells = true;
2605 // Force new selection block
2606 mStartSelectedCell = nullptr;
2607 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2608 // Always do this AFTER RemoveAllRanges
2609 mMode = aTarget;
2611 return SelectRowOrColumn(aChildContent, aNormalSelection);
2613 } else {
2614 #ifdef DEBUG_TABLE_SELECTION
2615 printf(
2616 "HandleTableSelection: Mouse UP event. "
2617 "mDragSelectingCells=%d, "
2618 "mStartSelectedCell=%p\n",
2619 mDragSelectingCells, mStartSelectedCell.get());
2620 #endif
2621 // First check if we are extending a block selection
2622 const uint32_t rangeCount = aNormalSelection.RangeCount();
2624 if (rangeCount > 0 && aMouseEvent->IsShift() && mAppendStartSelectedCell &&
2625 mAppendStartSelectedCell != aChildContent) {
2626 // Shift key is down: append a block selection
2627 mDragSelectingCells = false;
2629 return SelectBlockOfCells(mAppendStartSelectedCell, aChildContent,
2630 aNormalSelection);
2633 if (mDragSelectingCells) {
2634 mAppendStartSelectedCell = mStartSelectedCell;
2637 mDragSelectingCells = false;
2638 mStartSelectedCell = nullptr;
2639 mEndSelectedCell = nullptr;
2641 // Any other mouseup actions require that Ctrl or Cmd key is pressed
2642 // else stop table selection mode
2643 bool doMouseUpAction = false;
2644 #ifdef XP_MACOSX
2645 doMouseUpAction = aMouseEvent->IsMeta();
2646 #else
2647 doMouseUpAction = aMouseEvent->IsControl();
2648 #endif
2649 if (!doMouseUpAction) {
2650 #ifdef DEBUG_TABLE_SELECTION
2651 printf(
2652 "HandleTableSelection: Ending cell selection on mouseup: "
2653 "mAppendStartSelectedCell=%p\n",
2654 mAppendStartSelectedCell.get());
2655 #endif
2656 return NS_OK;
2658 // Unselect a cell only if it wasn't
2659 // just selected on mousedown
2660 if (aChildContent == mUnselectCellOnMouseUp) {
2661 // Scan ranges to find the cell to unselect (the selection range to
2662 // remove)
2663 // XXXbz it's really weird that this lives outside the loop, so once we
2664 // find one we keep looking at it even if we find no more cells...
2665 nsINode* previousCellParent = nullptr;
2666 #ifdef DEBUG_TABLE_SELECTION
2667 printf(
2668 "HandleTableSelection: Unselecting "
2669 "mUnselectCellOnMouseUp; "
2670 "rangeCount=%d\n",
2671 rangeCount);
2672 #endif
2673 for (const uint32_t i : IntegerRange(rangeCount)) {
2674 MOZ_ASSERT(aNormalSelection.RangeCount() == rangeCount);
2675 // Strong reference, because sometimes we want to remove
2676 // this range, and then we might be the only owner.
2677 RefPtr<nsRange> range = aNormalSelection.GetRangeAt(i);
2678 if (MOZ_UNLIKELY(!range)) {
2679 return NS_ERROR_NULL_POINTER;
2682 nsINode* container = range->GetStartContainer();
2683 if (!container) {
2684 return NS_ERROR_NULL_POINTER;
2687 int32_t offset = range->StartOffset();
2688 // Be sure previous selection is a table cell
2689 nsIContent* child = range->GetChildAtStartOffset();
2690 if (child && IsCell(child)) {
2691 previousCellParent = container;
2694 // We're done if we didn't find parent of a previously-selected cell
2695 if (!previousCellParent) {
2696 break;
2699 if (previousCellParent == aParentContent && offset == aContentOffset) {
2700 // Cell is already selected
2701 if (rangeCount == 1) {
2702 #ifdef DEBUG_TABLE_SELECTION
2703 printf("HandleTableSelection: Unselecting single selected cell\n");
2704 #endif
2705 // This was the only cell selected.
2706 // Collapse to "normal" selection inside the cell
2707 mStartSelectedCell = nullptr;
2708 mEndSelectedCell = nullptr;
2709 mAppendStartSelectedCell = nullptr;
2710 // TODO: We need a "Collapse to just before deepest child" routine
2711 // Even better, should we collapse to just after the LAST deepest
2712 // child
2713 // (i.e., at the end of the cell's contents)?
2714 return aNormalSelection.CollapseInLimiter(aChildContent, 0);
2716 #ifdef DEBUG_TABLE_SELECTION
2717 printf(
2718 "HandleTableSelection: Removing cell from multi-cell "
2719 "selection\n");
2720 #endif
2721 // Unselecting the start of previous block
2722 // XXX What do we use now!
2723 if (aChildContent == mAppendStartSelectedCell) {
2724 mAppendStartSelectedCell = nullptr;
2727 // Deselect cell by removing its range from selection
2728 ErrorResult err;
2729 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2730 *range, err);
2731 return err.StealNSResult();
2734 mUnselectCellOnMouseUp = nullptr;
2737 return result;
2740 nsresult nsFrameSelection::TableSelection::SelectBlockOfCells(
2741 nsIContent* aStartCell, nsIContent* aEndCell, Selection& aNormalSelection) {
2742 NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER);
2743 NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER);
2744 mEndSelectedCell = aEndCell;
2746 nsresult result = NS_OK;
2748 // If new end cell is in a different table, do nothing
2749 const RefPtr<const nsIContent> table = IsInSameTable(aStartCell, aEndCell);
2750 if (!table) {
2751 return NS_OK;
2754 // Get starting and ending cells' location in the cellmap
2755 int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
2756 result = GetCellIndexes(aStartCell, startRowIndex, startColIndex);
2757 if (NS_FAILED(result)) return result;
2758 result = GetCellIndexes(aEndCell, endRowIndex, endColIndex);
2759 if (NS_FAILED(result)) return result;
2761 if (mDragSelectingCells) {
2762 // Drag selecting: remove selected cells outside of new block limits
2763 // TODO: `UnselectCells`'s return value shouldn't be ignored.
2764 UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex,
2765 true, aNormalSelection);
2768 // Note that we select block in the direction of user's mouse dragging,
2769 // which means start cell may be after the end cell in either row or column
2770 return AddCellsToSelection(table, startRowIndex, startColIndex, endRowIndex,
2771 endColIndex, aNormalSelection);
2774 nsresult nsFrameSelection::TableSelection::UnselectCells(
2775 const nsIContent* aTableContent, int32_t aStartRowIndex,
2776 int32_t aStartColumnIndex, int32_t aEndRowIndex, int32_t aEndColumnIndex,
2777 bool aRemoveOutsideOfCellRange, mozilla::dom::Selection& aNormalSelection) {
2778 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2780 nsTableWrapperFrame* tableFrame =
2781 do_QueryFrame(aTableContent->GetPrimaryFrame());
2782 if (!tableFrame) return NS_ERROR_FAILURE;
2784 int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex);
2785 int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex);
2786 int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex);
2787 int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex);
2789 // Strong reference because we sometimes remove the range
2790 RefPtr<nsRange> range = GetFirstCellRange(aNormalSelection);
2791 nsIContent* cellNode = GetFirstSelectedContent(range);
2792 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2794 int32_t curRowIndex, curColIndex;
2795 while (cellNode) {
2796 nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex);
2797 if (NS_FAILED(result)) return result;
2799 #ifdef DEBUG_TABLE_SELECTION
2800 if (!range) printf("RemoveCellsToSelection -- range is null\n");
2801 #endif
2803 if (range) {
2804 if (aRemoveOutsideOfCellRange) {
2805 if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex ||
2806 curColIndex < minColIndex || curColIndex > maxColIndex) {
2807 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2808 *range, IgnoreErrors());
2809 // Since we've removed the range, decrement pointer to next range
2810 mSelectedCellIndex--;
2813 } else {
2814 // Remove cell from selection if it belongs to the given cells range or
2815 // it is spanned onto the cells range.
2816 nsTableCellFrame* cellFrame =
2817 tableFrame->GetCellFrameAt(curRowIndex, curColIndex);
2819 uint32_t origRowIndex = cellFrame->RowIndex();
2820 uint32_t origColIndex = cellFrame->ColIndex();
2821 uint32_t actualRowSpan =
2822 tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex);
2823 uint32_t actualColSpan =
2824 tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex);
2825 if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) &&
2826 maxRowIndex >= 0 &&
2827 origRowIndex + actualRowSpan - 1 >=
2828 static_cast<uint32_t>(minRowIndex) &&
2829 origColIndex <= static_cast<uint32_t>(maxColIndex) &&
2830 maxColIndex >= 0 &&
2831 origColIndex + actualColSpan - 1 >=
2832 static_cast<uint32_t>(minColIndex)) {
2833 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2834 *range, IgnoreErrors());
2835 // Since we've removed the range, decrement pointer to next range
2836 mSelectedCellIndex--;
2841 range = GetNextCellRange(aNormalSelection);
2842 cellNode = GetFirstSelectedContent(range);
2843 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2846 return NS_OK;
2849 nsresult SelectCellElement(nsIContent* aCellElement,
2850 Selection& aNormalSelection) {
2851 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2853 nsIContent* parent = aCellElement->GetParent();
2855 // Get child offset
2856 const int32_t offset = parent->ComputeIndexOf_Deprecated(aCellElement);
2858 return CreateAndAddRange(parent, offset, aNormalSelection);
2861 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
2862 int32_t aStartRowIndex,
2863 int32_t aStartColumnIndex,
2864 int32_t aEndRowIndex,
2865 int32_t aEndColumnIndex,
2866 Selection& aNormalSelection) {
2867 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2869 nsTableWrapperFrame* tableFrame =
2870 do_QueryFrame(aTableContent->GetPrimaryFrame());
2871 if (!tableFrame) { // Check that |table| is a table.
2872 return NS_ERROR_FAILURE;
2875 nsresult result = NS_OK;
2876 uint32_t row = aStartRowIndex;
2877 while (true) {
2878 uint32_t col = aStartColumnIndex;
2879 while (true) {
2880 nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col);
2882 // Skip cells that are spanned from previous locations or are already
2883 // selected
2884 if (cellFrame) {
2885 uint32_t origRow = cellFrame->RowIndex();
2886 uint32_t origCol = cellFrame->ColIndex();
2887 if (origRow == row && origCol == col && !cellFrame->IsSelected()) {
2888 result = SelectCellElement(cellFrame->GetContent(), aNormalSelection);
2889 if (NS_FAILED(result)) {
2890 return result;
2894 // Done when we reach end column
2895 if (col == static_cast<uint32_t>(aEndColumnIndex)) {
2896 break;
2899 if (aStartColumnIndex < aEndColumnIndex) {
2900 col++;
2901 } else {
2902 col--;
2905 if (row == static_cast<uint32_t>(aEndRowIndex)) {
2906 break;
2909 if (aStartRowIndex < aEndRowIndex) {
2910 row++;
2911 } else {
2912 row--;
2915 return result;
2918 nsresult nsFrameSelection::RemoveCellsFromSelection(nsIContent* aTable,
2919 int32_t aStartRowIndex,
2920 int32_t aStartColumnIndex,
2921 int32_t aEndRowIndex,
2922 int32_t aEndColumnIndex) {
2923 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2924 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2925 if (!selection) {
2926 return NS_ERROR_NULL_POINTER;
2929 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2930 aStartColumnIndex, aEndRowIndex,
2931 aEndColumnIndex, false, *selection);
2934 nsresult nsFrameSelection::RestrictCellsToSelection(nsIContent* aTable,
2935 int32_t aStartRowIndex,
2936 int32_t aStartColumnIndex,
2937 int32_t aEndRowIndex,
2938 int32_t aEndColumnIndex) {
2939 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2940 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2941 if (!selection) {
2942 return NS_ERROR_NULL_POINTER;
2945 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2946 aStartColumnIndex, aEndRowIndex,
2947 aEndColumnIndex, true, *selection);
2950 Result<nsFrameSelection::TableSelection::FirstAndLastCell, nsresult>
2951 nsFrameSelection::TableSelection::FindFirstAndLastCellOfRowOrColumn(
2952 const nsIContent& aCellContent) const {
2953 const nsIContent* table = GetParentTable(&aCellContent);
2954 if (!table) {
2955 return Err(NS_ERROR_NULL_POINTER);
2958 // Get table and cell layout interfaces to access
2959 // cell data based on cellmap location
2960 // Frames are not ref counted, so don't use an nsCOMPtr
2961 nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame());
2962 if (!tableFrame) {
2963 return Err(NS_ERROR_FAILURE);
2965 nsITableCellLayout* cellLayout = GetCellLayout(&aCellContent);
2966 if (!cellLayout) {
2967 return Err(NS_ERROR_FAILURE);
2970 // Get location of target cell:
2971 int32_t rowIndex, colIndex;
2972 nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex);
2973 if (NS_FAILED(result)) {
2974 return Err(result);
2977 // Be sure we start at proper beginning
2978 // (This allows us to select row or col given ANY cell!)
2979 if (mMode == TableSelectionMode::Row) {
2980 colIndex = 0;
2982 if (mMode == TableSelectionMode::Column) {
2983 rowIndex = 0;
2986 FirstAndLastCell firstAndLastCell;
2987 while (true) {
2988 // Loop through all cells in column or row to find first and last
2989 nsCOMPtr<nsIContent> curCellContent =
2990 tableFrame->GetCellAt(rowIndex, colIndex);
2991 if (!curCellContent) {
2992 break;
2995 if (!firstAndLastCell.mFirst) {
2996 firstAndLastCell.mFirst = curCellContent;
2999 firstAndLastCell.mLast = std::move(curCellContent);
3001 // Move to next cell in cellmap, skipping spanned locations
3002 if (mMode == TableSelectionMode::Row) {
3003 colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
3004 } else {
3005 rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
3008 return firstAndLastCell;
3011 nsresult nsFrameSelection::TableSelection::SelectRowOrColumn(
3012 nsIContent* aCellContent, Selection& aNormalSelection) {
3013 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
3015 if (!aCellContent) {
3016 return NS_ERROR_NULL_POINTER;
3019 Result<FirstAndLastCell, nsresult> firstAndLastCell =
3020 FindFirstAndLastCellOfRowOrColumn(*aCellContent);
3021 if (firstAndLastCell.isErr()) {
3022 return firstAndLastCell.unwrapErr();
3025 // Use SelectBlockOfCells:
3026 // This will replace existing selection,
3027 // but allow unselecting by dragging out of selected region
3028 if (firstAndLastCell.inspect().mFirst && firstAndLastCell.inspect().mLast) {
3029 nsresult rv{NS_OK};
3031 if (!mStartSelectedCell) {
3032 // We are starting a new block, so select the first cell
3033 rv = ::SelectCellElement(firstAndLastCell.inspect().mFirst,
3034 aNormalSelection);
3035 if (NS_FAILED(rv)) {
3036 return rv;
3038 mStartSelectedCell = firstAndLastCell.inspect().mFirst;
3041 rv = SelectBlockOfCells(mStartSelectedCell,
3042 firstAndLastCell.inspect().mLast, aNormalSelection);
3044 // This gets set to the cell at end of row/col,
3045 // but we need it to be the cell under cursor
3046 mEndSelectedCell = aCellContent;
3047 return rv;
3050 #if 0
3051 // This is a more efficient strategy that appends row to current selection,
3052 // but doesn't allow dragging OFF of an existing selection to unselect!
3053 do {
3054 // Loop through all cells in column or row
3055 result = tableLayout->GetCellDataAt(rowIndex, colIndex,
3056 getter_AddRefs(cellElement),
3057 curRowIndex, curColIndex,
3058 rowSpan, colSpan,
3059 actualRowSpan, actualColSpan,
3060 isSelected);
3061 if (NS_FAILED(result)) return result;
3062 // We're done when cell is not found
3063 if (!cellElement) break;
3066 // Check spans else we infinitely loop
3067 NS_ASSERTION(actualColSpan, "actualColSpan is 0!");
3068 NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!");
3070 // Skip cells that are already selected or span from outside our region
3071 if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex)
3073 result = SelectCellElement(cellElement);
3074 if (NS_FAILED(result)) return result;
3076 // Move to next row or column in cellmap, skipping spanned locations
3077 if (mMode == TableSelectionMode::Row)
3078 colIndex += actualColSpan;
3079 else
3080 rowIndex += actualRowSpan;
3082 while (cellElement);
3083 #endif
3085 return NS_OK;
3088 // static
3089 nsIContent* nsFrameSelection::GetFirstCellNodeInRange(const nsRange* aRange) {
3090 if (!aRange) return nullptr;
3092 nsIContent* childContent = aRange->GetChildAtStartOffset();
3093 if (!childContent) return nullptr;
3094 // Don't return node if not a cell
3095 if (!IsCell(childContent)) return nullptr;
3097 return childContent;
3100 nsRange* nsFrameSelection::TableSelection::GetFirstCellRange(
3101 const mozilla::dom::Selection& aNormalSelection) {
3102 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
3104 nsRange* firstRange = aNormalSelection.GetRangeAt(0);
3105 if (!GetFirstCellNodeInRange(firstRange)) {
3106 return nullptr;
3109 // Setup for next cell
3110 mSelectedCellIndex = 1;
3112 return firstRange;
3115 nsRange* nsFrameSelection::TableSelection::GetNextCellRange(
3116 const mozilla::dom::Selection& aNormalSelection) {
3117 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
3119 nsRange* range =
3120 aNormalSelection.GetRangeAt(AssertedCast<uint32_t>(mSelectedCellIndex));
3122 // Get first node in next range of selection - test if it's a cell
3123 if (!GetFirstCellNodeInRange(range)) {
3124 return nullptr;
3127 // Setup for next cell
3128 mSelectedCellIndex++;
3130 return range;
3133 // static
3134 nsresult nsFrameSelection::GetCellIndexes(const nsIContent* aCell,
3135 int32_t& aRowIndex,
3136 int32_t& aColIndex) {
3137 if (!aCell) return NS_ERROR_NULL_POINTER;
3139 aColIndex = 0; // initialize out params
3140 aRowIndex = 0;
3142 nsITableCellLayout* cellLayoutObject = GetCellLayout(aCell);
3143 if (!cellLayoutObject) return NS_ERROR_FAILURE;
3144 return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex);
3147 // static
3148 nsIContent* nsFrameSelection::IsInSameTable(const nsIContent* aContent1,
3149 const nsIContent* aContent2) {
3150 if (!aContent1 || !aContent2) return nullptr;
3152 nsIContent* tableNode1 = GetParentTable(aContent1);
3153 nsIContent* tableNode2 = GetParentTable(aContent2);
3155 // Must be in the same table. Note that we want to return false for
3156 // the test if both tables are null.
3157 return (tableNode1 == tableNode2) ? tableNode1 : nullptr;
3160 // static
3161 nsIContent* nsFrameSelection::GetParentTable(const nsIContent* aCell) {
3162 if (!aCell) {
3163 return nullptr;
3166 for (nsIContent* parent = aCell->GetParent(); parent;
3167 parent = parent->GetParent()) {
3168 if (parent->IsHTMLElement(nsGkAtoms::table)) {
3169 return parent;
3173 return nullptr;
3176 nsresult nsFrameSelection::SelectCellElement(nsIContent* aCellElement) {
3177 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3178 const RefPtr<Selection> selection = mDomSelections[index];
3179 if (!selection) {
3180 return NS_ERROR_NULL_POINTER;
3183 return ::SelectCellElement(aCellElement, *selection);
3186 nsresult CreateAndAddRange(nsINode* aContainer, int32_t aOffset,
3187 Selection& aNormalSelection) {
3188 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
3190 if (!aContainer) {
3191 return NS_ERROR_NULL_POINTER;
3194 // Set range around child at given offset
3195 ErrorResult error;
3196 RefPtr<nsRange> range =
3197 nsRange::Create(aContainer, aOffset, aContainer, aOffset + 1, error);
3198 if (NS_WARN_IF(error.Failed())) {
3199 return error.StealNSResult();
3201 MOZ_ASSERT(range);
3203 ErrorResult err;
3204 aNormalSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, err);
3205 return err.StealNSResult();
3208 // End of Table Selection
3210 void nsFrameSelection::SetAncestorLimiter(nsIContent* aLimiter) {
3211 if (mLimiters.mAncestorLimiter != aLimiter) {
3212 mLimiters.mAncestorLimiter = aLimiter;
3213 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3214 if (!mDomSelections[index]) return;
3216 if (!IsValidSelectionPoint(mDomSelections[index]->GetFocusNode())) {
3217 ClearNormalSelection();
3218 if (mLimiters.mAncestorLimiter) {
3219 SetChangeReasons(nsISelectionListener::NO_REASON);
3220 nsCOMPtr<nsIContent> limiter(mLimiters.mAncestorLimiter);
3221 const nsresult rv = TakeFocus(*limiter, 0, 0, CARET_ASSOCIATE_BEFORE,
3222 FocusMode::kCollapseToNewPoint);
3223 Unused << NS_WARN_IF(NS_FAILED(rv));
3224 // TODO: in case of failure, propagate it to the callers.
3230 void nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) {
3231 if (aMouseEvent) {
3232 mDelayedMouseEvent.mIsValid = true;
3233 mDelayedMouseEvent.mIsShift = aMouseEvent->IsShift();
3234 mDelayedMouseEvent.mClickCount = aMouseEvent->mClickCount;
3235 } else {
3236 mDelayedMouseEvent.mIsValid = false;
3240 void nsFrameSelection::DisconnectFromPresShell() {
3241 if (mAccessibleCaretEnabled) {
3242 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3243 mDomSelections[index]->StopNotifyingAccessibleCaretEventHub();
3246 StopAutoScrollTimer();
3247 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
3248 mDomSelections[i]->Clear(nullptr);
3250 mPresShell = nullptr;
3253 #ifdef XP_MACOSX
3255 * See Bug 1288453.
3257 * Update the selection cache on repaint to handle when a pre-existing
3258 * selection becomes active aka the current selection.
3260 * 1. Change the current selection by click n dragging another selection.
3261 * - Make a selection on content page. Make a selection in a text editor.
3262 * - You can click n drag the content selection to make it active again.
3263 * 2. Change the current selection when switching to a tab with a selection.
3264 * - Make selection in tab.
3265 * - Switching tabs will make its respective selection active.
3267 * Therefore, we only update the selection cache on a repaint
3268 * if the current selection being repainted is not an empty selection.
3270 * If the current selection is empty. The current selection cache
3271 * would be cleared by AutoCopyListener::OnSelectionChange().
3273 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel) {
3274 PresShell* presShell = aSel->GetPresShell();
3275 if (!presShell) {
3276 return NS_OK;
3278 nsCOMPtr<Document> aDoc = presShell->GetDocument();
3280 if (aDoc && aSel && !aSel->IsCollapsed()) {
3281 return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3282 aSel, aDoc, nsIClipboard::kSelectionCache, false);
3285 return NS_OK;
3287 #endif // XP_MACOSX
3289 // mozilla::AutoCopyListener
3291 int16_t AutoCopyListener::sClipboardID = -1;
3294 * What we do now:
3295 * On every selection change, we copy to the clipboard anew, creating a
3296 * HTML buffer, a transferable, an nsISupportsString and
3297 * a huge mess every time. This is basically what
3298 * nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the
3299 * selection into the clipboard for Edit->Copy.
3301 * What we should do, to make our end of the deal faster:
3302 * Create a singleton transferable with our own magic converter. When selection
3303 * changes (use a quick cache to detect ``real'' changes), we put the new
3304 * Selection in the transferable. Our magic converter will take care of
3305 * transferable->whatever-other-format when the time comes to actually
3306 * hand over the clipboard contents.
3308 * Other issues:
3309 * - which X clipboard should we populate?
3310 * - should we use a different one than Edit->Copy, so that inadvertant
3311 * selections (or simple clicks, which currently cause a selection
3312 * notification, regardless of if they're in the document which currently has
3313 * selection!) don't lose the contents of the ``application''? Or should we
3314 * just put some intelligence in the ``is this a real selection?'' code to
3315 * protect our selection against clicks in other documents that don't create
3316 * selections?
3317 * - maybe we should just never clear the X clipboard? That would make this
3318 * problem just go away, which is very tempting.
3320 * On macOS,
3321 * nsIClipboard::kSelectionCache is the flag for current selection cache.
3322 * Set the current selection cache on the parent process in
3323 * widget cocoa nsClipboard whenever selection changes.
3326 // static
3327 void AutoCopyListener::OnSelectionChange(Document* aDocument,
3328 Selection& aSelection,
3329 int16_t aReason) {
3330 MOZ_ASSERT(IsValidClipboardID(sClipboardID));
3332 // For now, we should prevent any updates caused by a call of Selection API.
3333 // We should allow this in some cases later, though. See the valid usage in
3334 // bug 1567160.
3335 if (aReason & nsISelectionListener::JS_REASON) {
3336 return;
3339 if (sClipboardID == nsIClipboard::kSelectionCache) {
3340 // Do nothing if this isn't in the active window and,
3341 // in the case of Web content, in the frontmost tab.
3342 if (!aDocument || !IsInActiveTab(aDocument)) {
3343 return;
3347 static const int16_t kResasonsToHandle =
3348 nsISelectionListener::MOUSEUP_REASON |
3349 nsISelectionListener::SELECTALL_REASON |
3350 nsISelectionListener::KEYPRESS_REASON;
3351 if (!(aReason & kResasonsToHandle)) {
3352 return; // Don't care if we are still dragging.
3355 if (!aDocument || aSelection.IsCollapsed()) {
3356 #ifdef DEBUG_CLIPBOARD
3357 fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n");
3358 #endif
3359 if (sClipboardID != nsIClipboard::kSelectionCache) {
3360 // XXX Should we clear X clipboard?
3361 return;
3364 // If on macOS, clear the current selection transferable cached
3365 // on the parent process (nsClipboard) when the selection is empty.
3366 DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache();
3367 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3368 "nsCopySupport::ClearSelectionCache() failed");
3369 return;
3372 DebugOnly<nsresult> rv =
3373 nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3374 &aSelection, aDocument, sClipboardID, false);
3375 NS_WARNING_ASSERTION(
3376 NS_SUCCEEDED(rv),
3377 "nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed");