Bug 1800044 [wpt PR 36907] - Fix same URL navigation test, a=testonly
[gecko.git] / layout / generic / nsFrameSelection.cpp
blobcca57621c9b2920a473ada933ccdd67c231c77ce
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/Selection.h"
77 #include "mozilla/dom/ShadowRoot.h"
78 #include "mozilla/dom/StaticRange.h"
79 #include "mozilla/dom/Text.h"
80 #include "mozilla/ErrorResult.h"
81 #include "mozilla/dom/SelectionBinding.h"
82 #include "mozilla/AsyncEventDispatcher.h"
83 #include "mozilla/Telemetry.h"
85 #include "nsFocusManager.h"
86 #include "nsPIDOMWindow.h"
88 using namespace mozilla;
89 using namespace mozilla::dom;
91 static LazyLogModule sFrameSelectionLog("FrameSelection");
93 //#define DEBUG_TABLE 1
95 /**
96 * Add cells to the selection inside of the given cells range.
98 * @param aTable [in] HTML table element
99 * @param aStartRowIndex [in] row index where the cells range starts
100 * @param aStartColumnIndex [in] column index where the cells range starts
101 * @param aEndRowIndex [in] row index where the cells range ends
102 * @param aEndColumnIndex [in] column index where the cells range ends
104 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
105 int32_t aStartRowIndex,
106 int32_t aStartColumnIndex,
107 int32_t aEndRowIndex,
108 int32_t aEndColumnIndex,
109 Selection& aNormalSelection);
111 static nsAtom* GetTag(nsINode* aNode);
113 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode);
114 MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult CreateAndAddRange(
115 nsINode* aContainer, int32_t aOffset, Selection& aNormalSelection);
116 static nsresult SelectCellElement(nsIContent* aCellElement,
117 Selection& aNormalSelection);
119 #ifdef XP_MACOSX
120 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel);
121 #endif // XP_MACOSX
123 #ifdef PRINT_RANGE
124 static void printRange(nsRange* aDomRange);
125 # define DEBUG_OUT_RANGE(x) printRange(x)
126 #else
127 # define DEBUG_OUT_RANGE(x)
128 #endif // PRINT_RANGE
130 /******************************************************************************
131 * nsPeekOffsetStruct
132 ******************************************************************************/
134 //#define DEBUG_SELECTION // uncomment for printf describing every collapse and
135 // extend. #define DEBUG_NAVIGATION
137 //#define DEBUG_TABLE_SELECTION 1
139 nsPeekOffsetStruct::nsPeekOffsetStruct(
140 nsSelectionAmount aAmount, nsDirection aDirection, int32_t aStartOffset,
141 nsPoint aDesiredCaretPos, bool aJumpLines, bool aScrollViewStop,
142 bool aIsKeyboardSelect, bool aVisual, bool aExtend,
143 ForceEditableRegion aForceEditableRegion,
144 EWordMovementType aWordMovementType, bool aTrimSpaces)
145 : mAmount(aAmount),
146 mDirection(aDirection),
147 mStartOffset(aStartOffset),
148 mDesiredCaretPos(aDesiredCaretPos),
149 mWordMovementType(aWordMovementType),
150 mJumpLines(aJumpLines),
151 mTrimSpaces(aTrimSpaces),
152 mScrollViewStop(aScrollViewStop),
153 mIsKeyboardSelect(aIsKeyboardSelect),
154 mVisual(aVisual),
155 mExtend(aExtend),
156 mForceEditableRegion(aForceEditableRegion == ForceEditableRegion::Yes),
157 mResultContent(),
158 mResultFrame(nullptr),
159 mContentOffset(0),
160 mAttach(CARET_ASSOCIATE_BEFORE) {}
162 // Array which contains index of each SelecionType in Selection::mDOMSelections.
163 // For avoiding using if nor switch to retrieve the index, this needs to have
164 // -1 for SelectionTypes which won't be created its Selection instance.
165 static const int8_t kIndexOfSelections[] = {
166 -1, // SelectionType::eInvalid
167 -1, // SelectionType::eNone
168 0, // SelectionType::eNormal
169 1, // SelectionType::eSpellCheck
170 2, // SelectionType::eIMERawClause
171 3, // SelectionType::eIMESelectedRawClause
172 4, // SelectionType::eIMEConvertedClause
173 5, // SelectionType::eIMESelectedClause
174 6, // SelectionType::eAccessibility
175 7, // SelectionType::eFind
176 8, // SelectionType::eURLSecondary
177 9, // SelectionType::eURLStrikeout
180 inline int8_t GetIndexFromSelectionType(SelectionType aSelectionType) {
181 // The enum value of eInvalid is -1 and the others are sequential value
182 // starting from 0. Therefore, |SelectionType + 1| is the index of
183 // kIndexOfSelections.
184 return kIndexOfSelections[static_cast<int8_t>(aSelectionType) + 1];
188 The limiter is used specifically for the text areas and textfields
189 In that case it is the DIV tag that is anonymously created for the text
190 areas/fields. Text nodes and BR nodes fall beneath it. In the case of a
191 BR node the limiter will be the parent and the offset will point before or
192 after the BR node. In the case of the text node the parent content is
193 the text node itself and the offset will be the exact character position.
194 The offset is not important to check for validity. Simply look at the
195 passed in content. If it equals the limiter then the selection point is valid.
196 If its parent it the limiter then the point is also valid. In the case of
197 NO limiter all points are valid since you are in a topmost iframe. (browser
198 or composer)
200 bool nsFrameSelection::IsValidSelectionPoint(nsINode* aNode) const {
201 if (!aNode) {
202 return false;
205 nsIContent* limiter = GetLimiter();
206 if (limiter && limiter != aNode && limiter != aNode->GetParent()) {
207 // if newfocus == the limiter. that's ok. but if not there and not parent
208 // bad
209 return false; // not in the right content. tLimiter said so
212 limiter = GetAncestorLimiter();
213 return !limiter || aNode->IsInclusiveDescendantOf(limiter);
216 namespace mozilla {
217 struct MOZ_RAII AutoPrepareFocusRange {
218 AutoPrepareFocusRange(Selection* aSelection,
219 const bool aMultiRangeSelection) {
220 MOZ_ASSERT(aSelection);
221 MOZ_ASSERT(aSelection->GetType() == SelectionType::eNormal);
223 if (aSelection->mStyledRanges.mRanges.Length() <= 1) {
224 return;
227 if (aSelection->mFrameSelection->IsUserSelectionReason()) {
228 mUserSelect.emplace(aSelection);
231 nsTArray<StyledRange>& ranges = aSelection->mStyledRanges.mRanges;
232 if (!aSelection->mUserInitiated || aMultiRangeSelection) {
233 // Scripted command or the user is starting a new explicit multi-range
234 // selection.
235 for (StyledRange& entry : ranges) {
236 entry.mRange->SetIsGenerated(false);
238 return;
241 if (!IsAnchorRelativeOperation(
242 aSelection->mFrameSelection->mSelectionChangeReasons)) {
243 return;
246 // This operation is against the anchor but our current mAnchorFocusRange
247 // represents the focus in a multi-range selection. The anchor from a user
248 // perspective is the most distant generated range on the opposite side.
249 // Find that range and make it the mAnchorFocusRange.
250 nsRange* const newAnchorFocusRange =
251 FindGeneratedRangeMostDistantFromAnchor(*aSelection);
253 if (!newAnchorFocusRange) {
254 // There are no generated ranges - that's fine.
255 return;
258 // Setup the new mAnchorFocusRange and mark the old one as generated.
259 if (aSelection->mAnchorFocusRange) {
260 aSelection->mAnchorFocusRange->SetIsGenerated(true);
263 newAnchorFocusRange->SetIsGenerated(false);
264 aSelection->mAnchorFocusRange = newAnchorFocusRange;
266 RemoveGeneratedRanges(*aSelection);
268 if (aSelection->mFrameSelection) {
269 aSelection->mFrameSelection->InvalidateDesiredCaretPos();
273 private:
274 static nsRange* FindGeneratedRangeMostDistantFromAnchor(
275 const Selection& aSelection) {
276 const nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
277 const size_t len = ranges.Length();
278 nsRange* result{nullptr};
279 if (aSelection.GetDirection() == eDirNext) {
280 for (size_t i = 0; i < len; ++i) {
281 if (ranges[i].mRange->IsGenerated()) {
282 result = ranges[i].mRange;
283 break;
286 } else {
287 size_t i = len;
288 while (i--) {
289 if (ranges[i].mRange->IsGenerated()) {
290 result = ranges[i].mRange;
291 break;
296 return result;
299 static void RemoveGeneratedRanges(Selection& aSelection) {
300 RefPtr<nsPresContext> presContext = aSelection.GetPresContext();
301 nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
302 size_t i = ranges.Length();
303 while (i--) {
304 nsRange* range = ranges[i].mRange;
305 if (range->IsGenerated()) {
306 range->UnregisterSelection();
307 aSelection.SelectFrames(presContext, range, false);
308 ranges.RemoveElementAt(i);
314 * @aParam aSelectionChangeReasons can be multiple of the reasons defined in
315 nsISelectionListener.idl.
317 static bool IsAnchorRelativeOperation(const int16_t aSelectionChangeReasons) {
318 return aSelectionChangeReasons &
319 (nsISelectionListener::DRAG_REASON |
320 nsISelectionListener::MOUSEDOWN_REASON |
321 nsISelectionListener::MOUSEUP_REASON |
322 nsISelectionListener::COLLAPSETOSTART_REASON);
325 Maybe<Selection::AutoUserInitiated> mUserSelect;
328 } // namespace mozilla
330 ////////////BEGIN nsFrameSelection methods
332 template Result<RefPtr<nsRange>, nsresult>
333 nsFrameSelection::CreateRangeExtendedToSomewhere(
334 nsDirection aDirection, const nsSelectionAmount aAmount,
335 CaretMovementStyle aMovementStyle);
336 template Result<RefPtr<StaticRange>, nsresult>
337 nsFrameSelection::CreateRangeExtendedToSomewhere(
338 nsDirection aDirection, const nsSelectionAmount aAmount,
339 CaretMovementStyle aMovementStyle);
341 nsFrameSelection::nsFrameSelection(PresShell* aPresShell, nsIContent* aLimiter,
342 const bool aAccessibleCaretEnabled) {
343 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
344 mDomSelections[i] = new Selection(kPresentSelectionTypes[i], this);
347 #ifdef XP_MACOSX
348 // On macOS, cache the current selection to send to service menu of macOS.
349 bool enableAutoCopy = true;
350 AutoCopyListener::Init(nsIClipboard::kSelectionCache);
351 #else // #ifdef XP_MACOSX
352 // Check to see if the auto-copy pref is enabled and make the normal
353 // Selection notifies auto-copy listener of its changes.
354 bool enableAutoCopy = AutoCopyListener::IsPrefEnabled();
355 if (enableAutoCopy) {
356 AutoCopyListener::Init(nsIClipboard::kSelectionClipboard);
358 #endif // #ifdef XP_MACOSX #else
360 if (enableAutoCopy) {
361 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
362 if (mDomSelections[index]) {
363 mDomSelections[index]->NotifyAutoCopy();
367 mPresShell = aPresShell;
368 mDragState = false;
369 mLimiters.mLimiter = aLimiter;
371 // This should only ever be initialized on the main thread, so we are OK here.
372 MOZ_ASSERT(NS_IsMainThread());
374 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
376 mAccessibleCaretEnabled = aAccessibleCaretEnabled;
377 if (mAccessibleCaretEnabled) {
378 mDomSelections[index]->MaybeNotifyAccessibleCaretEventHub(aPresShell);
381 if (mDomSelections[index]) {
382 mDomSelections[index]->EnableSelectionChangeEvent();
386 nsFrameSelection::~nsFrameSelection() = default;
388 NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection)
390 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection)
391 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
392 tmp->mDomSelections[i] = nullptr;
395 NS_IMPL_CYCLE_COLLECTION_UNLINK(
396 mTableSelection.mClosestInclusiveTableCellAncestor)
397 tmp->mTableSelection.mMode = TableSelectionMode::None;
398 tmp->mTableSelection.mDragSelectingCells = false;
399 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mStartSelectedCell)
400 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mEndSelectedCell)
401 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mAppendStartSelectedCell)
402 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mUnselectCellOnMouseUp)
403 NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainedRange.mRange)
404 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mLimiter)
405 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mAncestorLimiter)
406 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
407 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection)
408 if (tmp->mPresShell && tmp->mPresShell->GetDocument() &&
409 nsCCUncollectableMarker::InGeneration(
410 cb, tmp->mPresShell->GetDocument()->GetMarkedCCGeneration())) {
411 return NS_SUCCESS_INTERRUPTED_TRAVERSE;
413 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
414 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i])
417 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
418 mTableSelection.mClosestInclusiveTableCellAncestor)
419 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mStartSelectedCell)
420 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mEndSelectedCell)
421 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mAppendStartSelectedCell)
422 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mUnselectCellOnMouseUp)
423 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainedRange.mRange)
424 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mLimiter)
425 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mAncestorLimiter)
426 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
428 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsFrameSelection, AddRef)
429 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsFrameSelection, Release)
431 bool nsFrameSelection::Caret::IsVisualMovement(
432 bool aContinueSelection, CaretMovementStyle aMovementStyle) const {
433 int32_t movementFlag = StaticPrefs::bidi_edit_caret_movement_style();
434 return aMovementStyle == eVisual ||
435 (aMovementStyle == eUsePrefStyle &&
436 (movementFlag == 1 || (movementFlag == 2 && !aContinueSelection)));
439 // Get the x (or y, in vertical writing mode) position requested
440 // by the Key Handling for line-up/down
441 nsresult nsFrameSelection::DesiredCaretPos::FetchPos(
442 nsPoint& aDesiredCaretPos, const PresShell& aPresShell,
443 Selection& aNormalSelection) const {
444 MOZ_ASSERT(aNormalSelection.GetType() == SelectionType::eNormal);
446 if (mIsSet) {
447 aDesiredCaretPos = mValue;
448 return NS_OK;
451 RefPtr<nsCaret> caret = aPresShell.GetCaret();
452 if (!caret) {
453 return NS_ERROR_NULL_POINTER;
456 caret->SetSelection(&aNormalSelection);
458 nsRect coord;
459 nsIFrame* caretFrame = caret->GetGeometry(&coord);
460 if (!caretFrame) {
461 return NS_ERROR_FAILURE;
463 nsPoint viewOffset(0, 0);
464 nsView* view = nullptr;
465 caretFrame->GetOffsetFromView(viewOffset, &view);
466 if (view) {
467 coord += viewOffset;
469 aDesiredCaretPos = coord.TopLeft();
470 return NS_OK;
473 void nsFrameSelection::InvalidateDesiredCaretPos() // do not listen to
474 // mDesiredCaretPos.mValue;
475 // you must get another.
477 mDesiredCaretPos.Invalidate();
480 void nsFrameSelection::DesiredCaretPos::Invalidate() { mIsSet = false; }
482 void nsFrameSelection::DesiredCaretPos::Set(const nsPoint& aPos) {
483 mValue = aPos;
484 mIsSet = true;
487 nsresult nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree(
488 nsIFrame* aFrame, const nsPoint& aPoint, nsIFrame** aRetFrame,
489 nsPoint& aRetPoint) const {
491 // The whole point of this method is to return a frame and point that
492 // that lie within the same valid subtree as the anchor node's frame,
493 // for use with the method GetContentAndOffsetsFromPoint().
495 // A valid subtree is defined to be one where all the content nodes in
496 // the tree have a valid parent-child relationship.
498 // If the anchor frame and aFrame are in the same subtree, aFrame will
499 // be returned in aRetFrame. If they are in different subtrees, we
500 // return the frame for the root of the subtree.
503 if (!aFrame || !aRetFrame) {
504 return NS_ERROR_NULL_POINTER;
507 *aRetFrame = aFrame;
508 aRetPoint = aPoint;
511 // Get the frame and content for the selection's anchor point!
514 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
515 if (!mDomSelections[index]) {
516 return NS_ERROR_NULL_POINTER;
519 nsCOMPtr<nsIContent> anchorContent =
520 do_QueryInterface(mDomSelections[index]->GetAnchorNode());
521 if (!anchorContent) {
522 return NS_ERROR_FAILURE;
526 // Now find the root of the subtree containing the anchor's content.
529 NS_ENSURE_STATE(mPresShell);
530 RefPtr<PresShell> presShell = mPresShell;
531 nsIContent* anchorRoot = anchorContent->GetSelectionRootContent(presShell);
532 NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED);
535 // Now find the root of the subtree containing aFrame's content.
538 nsCOMPtr<nsIContent> content = aFrame->GetContent();
540 if (content) {
541 nsIContent* contentRoot = content->GetSelectionRootContent(presShell);
542 NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED);
544 if (anchorRoot == contentRoot) {
545 // If the aFrame's content isn't the capturing content, it should be
546 // a descendant. At this time, we can return simply.
547 nsIContent* capturedContent = PresShell::GetCapturingContent();
548 if (capturedContent != content) {
549 return NS_OK;
552 // Find the frame under the mouse cursor with the root frame.
553 // At this time, don't use the anchor's frame because it may not have
554 // fixed positioned frames.
555 nsIFrame* rootFrame = presShell->GetRootFrame();
556 nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame);
557 nsIFrame* cursorFrame =
558 nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot);
560 // If the mouse cursor in on a frame which is descendant of same
561 // selection root, we can expand the selection to the frame.
562 if (cursorFrame && cursorFrame->PresShell() == presShell) {
563 nsCOMPtr<nsIContent> cursorContent = cursorFrame->GetContent();
564 NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE);
565 nsIContent* cursorContentRoot =
566 cursorContent->GetSelectionRootContent(presShell);
567 NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED);
568 if (cursorContentRoot == anchorRoot) {
569 *aRetFrame = cursorFrame;
570 aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame);
571 return NS_OK;
574 // Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse
575 // cursor is out of the window), we should use the frame of the anchor
576 // root.
581 // When we can't find a frame which is under the mouse cursor and has a same
582 // selection root as the anchor node's, we should return the selection root
583 // frame.
586 *aRetFrame = anchorRoot->GetPrimaryFrame();
588 if (!*aRetFrame) {
589 return NS_ERROR_FAILURE;
593 // Now make sure that aRetPoint is converted to the same coordinate
594 // system used by aRetFrame.
597 aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame);
599 return NS_OK;
602 void nsFrameSelection::SetCaretBidiLevelAndMaybeSchedulePaint(
603 mozilla::intl::BidiEmbeddingLevel aLevel) {
604 // If the current level is undefined, we have just inserted new text.
605 // In this case, we don't want to reset the keyboard language
606 mCaret.mBidiLevel = aLevel;
608 RefPtr<nsCaret> caret;
609 if (mPresShell && (caret = mPresShell->GetCaret())) {
610 caret->SchedulePaint();
614 mozilla::intl::BidiEmbeddingLevel nsFrameSelection::GetCaretBidiLevel() const {
615 return mCaret.mBidiLevel;
618 void nsFrameSelection::UndefineCaretBidiLevel() {
619 mCaret.mBidiLevel = mozilla::intl::BidiEmbeddingLevel(mCaret.mBidiLevel |
620 BIDI_LEVEL_UNDEFINED);
623 #ifdef PRINT_RANGE
624 void printRange(nsRange* aDomRange) {
625 if (!aDomRange) {
626 printf("NULL Range\n");
628 nsINode* startNode = aDomRange->GetStartContainer();
629 nsINode* endNode = aDomRange->GetEndContainer();
630 int32_t startOffset = aDomRange->StartOffset();
631 int32_t endOffset = aDomRange->EndOffset();
633 printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n",
634 (unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset,
635 (unsigned long)endNode, (long)endOffset);
637 #endif /* PRINT_RANGE */
639 static nsAtom* GetTag(nsINode* aNode) {
640 nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
641 if (!content) {
642 MOZ_ASSERT_UNREACHABLE("bad node passed to GetTag()");
643 return nullptr;
646 return content->NodeInfo()->NameAtom();
650 * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor.
652 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode) {
653 if (!aDomNode) return nullptr;
654 nsINode* current = aDomNode;
655 // Start with current node and look for a table cell
656 while (current) {
657 nsAtom* tag = GetTag(current);
658 if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) return current;
659 current = current->GetParent();
661 return nullptr;
664 static nsDirection GetCaretDirection(const nsIFrame& aFrame,
665 nsDirection aDirection,
666 bool aVisualMovement) {
667 const mozilla::intl::BidiDirection paragraphDirection =
668 nsBidiPresUtils::ParagraphDirection(&aFrame);
669 return (aVisualMovement &&
670 paragraphDirection == mozilla::intl::BidiDirection::RTL)
671 ? nsDirection(1 - aDirection)
672 : aDirection;
675 nsresult nsFrameSelection::MoveCaret(nsDirection aDirection,
676 bool aContinueSelection,
677 const nsSelectionAmount aAmount,
678 CaretMovementStyle aMovementStyle) {
679 NS_ENSURE_STATE(mPresShell);
680 // Flush out layout, since we need it to be up to date to do caret
681 // positioning.
682 OwningNonNull<PresShell> presShell(*mPresShell);
683 presShell->FlushPendingNotifications(FlushType::Layout);
685 if (!mPresShell) {
686 return NS_OK;
689 nsPresContext* context = mPresShell->GetPresContext();
690 if (!context) {
691 return NS_ERROR_FAILURE;
694 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
695 const RefPtr<Selection> sel = mDomSelections[index];
696 if (!sel) {
697 return NS_ERROR_NULL_POINTER;
700 int32_t scrollFlags = Selection::SCROLL_FOR_CARET_MOVE;
701 if (sel->IsEditorSelection()) {
702 // If caret moves in editor, it should cause scrolling even if it's in
703 // overflow: hidden;.
704 scrollFlags |= Selection::SCROLL_OVERFLOW_HIDDEN;
707 const bool doCollapse = [&] {
708 if (sel->IsCollapsed() || aContinueSelection) {
709 return false;
711 if (aAmount > eSelectLine) {
712 return false;
714 int32_t caretStyle = StaticPrefs::layout_selection_caret_style();
715 return caretStyle == 2 || (caretStyle == 0 && aAmount != eSelectLine);
716 }();
718 if (doCollapse) {
719 if (aDirection == eDirPrevious) {
720 SetChangeReasons(nsISelectionListener::COLLAPSETOSTART_REASON);
721 mCaret.mHint = CARET_ASSOCIATE_AFTER;
722 } else {
723 SetChangeReasons(nsISelectionListener::COLLAPSETOEND_REASON);
724 mCaret.mHint = CARET_ASSOCIATE_BEFORE;
726 } else {
727 SetChangeReasons(nsISelectionListener::KEYPRESS_REASON);
730 mCaretMoveAmount = aAmount;
732 AutoPrepareFocusRange prep(sel, false);
734 // we must keep this around and revalidate it when its just UP/DOWN
735 nsPoint desiredPos(0, 0);
737 if (aAmount == eSelectLine) {
738 nsresult result = mDesiredCaretPos.FetchPos(desiredPos, *mPresShell, *sel);
739 if (NS_FAILED(result)) {
740 return result;
742 mDesiredCaretPos.Set(desiredPos);
745 bool visualMovement =
746 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
747 nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(visualMovement);
748 if (!frame) {
749 return NS_ERROR_FAILURE;
752 Result<bool, nsresult> isIntraLineCaretMove = IsIntraLineCaretMove(aAmount);
753 nsDirection direction{aDirection};
754 if (isIntraLineCaretMove.isErr()) {
755 return isIntraLineCaretMove.unwrapErr();
757 if (isIntraLineCaretMove.inspect()) {
758 // Forget old caret position for moving caret to different line since
759 // caret position may be changed.
760 mDesiredCaretPos.Invalidate();
761 direction = GetCaretDirection(*frame, aDirection, visualMovement);
764 if (doCollapse) {
765 const nsRange* anchorFocusRange = sel->GetAnchorFocusRange();
766 if (anchorFocusRange) {
767 RefPtr<nsINode> node;
768 uint32_t offset;
769 if (visualMovement && nsBidiPresUtils::IsReversedDirectionFrame(frame)) {
770 direction = nsDirection(1 - direction);
772 if (direction == eDirPrevious) {
773 node = anchorFocusRange->GetStartContainer();
774 offset = anchorFocusRange->StartOffset();
775 } else {
776 node = anchorFocusRange->GetEndContainer();
777 offset = anchorFocusRange->EndOffset();
779 sel->CollapseInLimiter(node, offset);
781 sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
782 ScrollAxis(), ScrollAxis(), scrollFlags);
783 return NS_OK;
786 CaretAssociateHint tHint(mCaret.mHint); // temporary variable so we dont set
787 // mCaret.mHint until it is necessary
789 Result<nsPeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
790 direction, aContinueSelection, aAmount, aMovementStyle, desiredPos);
791 nsresult rv;
792 if (result.isOk() && result.inspect().mResultContent) {
793 const nsPeekOffsetStruct& pos = result.inspect();
794 nsIFrame* theFrame;
795 int32_t currentOffset, frameStart, frameEnd;
797 if (aAmount <= eSelectWordNoSpace) {
798 // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does
799 // not set pos.mAttachForward, so determine the hint here based on the
800 // result frame and offset: If we're at the end of a text frame, set the
801 // hint to ASSOCIATE_BEFORE to indicate that we want the caret displayed
802 // at the end of this frame, not at the beginning of the next one.
803 theFrame = pos.mResultFrame;
804 std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
805 currentOffset = pos.mContentOffset;
806 if (frameEnd == currentOffset && !(frameStart == 0 && frameEnd == 0))
807 tHint = CARET_ASSOCIATE_BEFORE;
808 else
809 tHint = CARET_ASSOCIATE_AFTER;
810 } else {
811 // For up/down and home/end, pos.mResultFrame might not be set correctly,
812 // or not at all. In these cases, get the frame based on the content and
813 // hint returned by PeekOffset().
814 tHint = pos.mAttach;
815 theFrame = GetFrameForNodeOffset(pos.mResultContent, pos.mContentOffset,
816 tHint, &currentOffset);
817 if (!theFrame) return NS_ERROR_FAILURE;
819 std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
822 if (context->BidiEnabled()) {
823 switch (aAmount) {
824 case eSelectBeginLine:
825 case eSelectEndLine: {
826 // In Bidi contexts, PeekOffset calculates pos.mContentOffset
827 // differently depending on whether the movement is visual or logical.
828 // For visual movement, pos.mContentOffset depends on the direction-
829 // ality of the first/last frame on the line (theFrame), and the caret
830 // directionality must correspond.
831 FrameBidiData bidiData = theFrame->GetBidiData();
832 SetCaretBidiLevelAndMaybeSchedulePaint(
833 visualMovement ? bidiData.embeddingLevel : bidiData.baseLevel);
834 break;
836 default:
837 // If the current position is not a frame boundary, it's enough just
838 // to take the Bidi level of the current frame
839 if ((pos.mContentOffset != frameStart &&
840 pos.mContentOffset != frameEnd) ||
841 eSelectLine == aAmount) {
842 SetCaretBidiLevelAndMaybeSchedulePaint(
843 theFrame->GetEmbeddingLevel());
844 } else {
845 BidiLevelFromMove(mPresShell, pos.mResultContent,
846 pos.mContentOffset, aAmount, tHint);
850 // "pos" is on the stack, so pos.mResultContent has stack lifetime, so using
851 // MOZ_KnownLive is ok.
852 const FocusMode focusMode = aContinueSelection
853 ? FocusMode::kExtendSelection
854 : FocusMode::kCollapseToNewPoint;
855 rv = TakeFocus(MOZ_KnownLive(*pos.mResultContent), pos.mContentOffset,
856 pos.mContentOffset, tHint, focusMode);
857 } else if (aAmount <= eSelectWordNoSpace && direction == eDirNext &&
858 !aContinueSelection) {
859 // Collapse selection if PeekOffset failed, we either
860 // 1. bumped into the BRFrame, bug 207623
861 // 2. had select-all in a text input (DIV range), bug 352759.
862 bool isBRFrame = frame->IsBrFrame();
863 RefPtr<nsINode> node = sel->GetFocusNode();
864 sel->CollapseInLimiter(node, sel->FocusOffset());
865 // Note: 'frame' might be dead here.
866 if (!isBRFrame) {
867 mCaret.mHint = CARET_ASSOCIATE_BEFORE; // We're now at the end of the
868 // frame to the left.
870 rv = NS_OK;
871 } else {
872 rv = result.isErr() ? result.unwrapErr() : NS_OK;
874 if (NS_SUCCEEDED(rv)) {
875 rv = sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
876 ScrollAxis(), ScrollAxis(), scrollFlags);
879 return rv;
882 Result<nsPeekOffsetStruct, nsresult> nsFrameSelection::PeekOffsetForCaretMove(
883 nsDirection aDirection, bool aContinueSelection,
884 const nsSelectionAmount aAmount, CaretMovementStyle aMovementStyle,
885 const nsPoint& aDesiredCaretPos) const {
886 Selection* selection =
887 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
888 if (!selection) {
889 return Err(NS_ERROR_NULL_POINTER);
892 const bool visualMovement =
893 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
895 int32_t offsetused = 0;
896 nsIFrame* frame =
897 selection->GetPrimaryFrameForFocusNode(visualMovement, &offsetused);
898 if (!frame) {
899 return Err(NS_ERROR_FAILURE);
902 const auto kForceEditableRegion =
903 selection->IsEditorSelection()
904 ? nsPeekOffsetStruct::ForceEditableRegion::Yes
905 : nsPeekOffsetStruct::ForceEditableRegion::No;
907 // set data using mLimiters.mLimiter to stop on scroll views. If we have a
908 // limiter then we stop peeking when we hit scrollable views. If no limiter
909 // then just let it go ahead
910 nsPeekOffsetStruct pos(aAmount, aDirection, offsetused, aDesiredCaretPos,
911 true, !!mLimiters.mLimiter, true, visualMovement,
912 aContinueSelection, kForceEditableRegion);
913 nsresult rv = frame->PeekOffset(&pos);
914 if (NS_FAILED(rv)) {
915 return Err(rv);
917 return pos;
920 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
921 nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const {
922 return GetPrevNextBidiLevels(aNode, aContentOffset, mCaret.mHint, aJumpLines);
925 // static
926 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
927 nsIContent* aNode, uint32_t aContentOffset, CaretAssociateHint aHint,
928 bool aJumpLines) {
929 // Get the level of the frames on each side
930 nsIFrame* currentFrame;
931 int32_t currentOffset;
932 nsDirection direction;
934 nsPrevNextBidiLevels levels{};
935 levels.SetData(nullptr, nullptr, mozilla::intl::BidiEmbeddingLevel::LTR(),
936 mozilla::intl::BidiEmbeddingLevel::LTR());
938 currentFrame = GetFrameForNodeOffset(
939 aNode, static_cast<int32_t>(aContentOffset), aHint, &currentOffset);
940 if (!currentFrame) {
941 return levels;
944 auto [frameStart, frameEnd] = currentFrame->GetOffsets();
946 if (0 == frameStart && 0 == frameEnd) {
947 direction = eDirPrevious;
948 } else if (frameStart == currentOffset) {
949 direction = eDirPrevious;
950 } else if (frameEnd == currentOffset) {
951 direction = eDirNext;
952 } else {
953 // we are neither at the beginning nor at the end of the frame, so we have
954 // no worries
955 mozilla::intl::BidiEmbeddingLevel currentLevel =
956 currentFrame->GetEmbeddingLevel();
957 levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel);
958 return levels;
961 nsIFrame* newFrame =
962 currentFrame
963 ->GetFrameFromDirection(direction, false, aJumpLines, true, false)
964 .mFrame;
966 FrameBidiData currentBidi = currentFrame->GetBidiData();
967 mozilla::intl::BidiEmbeddingLevel currentLevel = currentBidi.embeddingLevel;
968 mozilla::intl::BidiEmbeddingLevel newLevel =
969 newFrame ? newFrame->GetEmbeddingLevel() : currentBidi.baseLevel;
971 // If not jumping lines, disregard br frames, since they might be positioned
972 // incorrectly.
973 // XXX This could be removed once bug 339786 is fixed.
974 if (!aJumpLines) {
975 if (currentFrame->IsBrFrame()) {
976 currentFrame = nullptr;
977 currentLevel = currentBidi.baseLevel;
979 if (newFrame && newFrame->IsBrFrame()) {
980 newFrame = nullptr;
981 newLevel = currentBidi.baseLevel;
985 if (direction == eDirNext)
986 levels.SetData(currentFrame, newFrame, currentLevel, newLevel);
987 else
988 levels.SetData(newFrame, currentFrame, newLevel, currentLevel);
990 return levels;
993 nsresult nsFrameSelection::GetFrameFromLevel(
994 nsIFrame* aFrameIn, nsDirection aDirection,
995 mozilla::intl::BidiEmbeddingLevel aBidiLevel, nsIFrame** aFrameOut) const {
996 NS_ENSURE_STATE(mPresShell);
997 mozilla::intl::BidiEmbeddingLevel foundLevel =
998 mozilla::intl::BidiEmbeddingLevel::LTR();
999 nsIFrame* foundFrame = aFrameIn;
1001 nsCOMPtr<nsIFrameEnumerator> frameTraversal;
1002 nsresult result;
1003 nsCOMPtr<nsIFrameTraversal> trav(
1004 do_CreateInstance(kFrameTraversalCID, &result));
1005 if (NS_FAILED(result)) return result;
1007 result =
1008 trav->NewFrameTraversal(getter_AddRefs(frameTraversal),
1009 mPresShell->GetPresContext(), aFrameIn, eLeaf,
1010 false, // aVisual
1011 false, // aLockInScrollView
1012 false, // aFollowOOFs
1013 false // aSkipPopupChecks
1015 if (NS_FAILED(result)) return result;
1017 do {
1018 *aFrameOut = foundFrame;
1019 foundFrame = frameTraversal->Traverse(aDirection == eDirNext);
1020 if (!foundFrame) return NS_ERROR_FAILURE;
1021 foundLevel = foundFrame->GetEmbeddingLevel();
1023 } while (foundLevel > aBidiLevel);
1025 return NS_OK;
1028 nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) {
1029 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1030 if (!mDomSelections[index]) {
1031 return NS_ERROR_NULL_POINTER;
1034 mMaintainedRange.MaintainAnchorFocusRange(*mDomSelections[index], aAmount);
1036 return NS_OK;
1039 void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell,
1040 nsIContent* aNode,
1041 uint32_t aContentOffset,
1042 nsSelectionAmount aAmount,
1043 CaretAssociateHint aHint) {
1044 switch (aAmount) {
1045 // Movement within the line: the new cursor Bidi level is the level of the
1046 // last character moved over
1047 case eSelectCharacter:
1048 case eSelectCluster:
1049 case eSelectWord:
1050 case eSelectWordNoSpace:
1051 case eSelectBeginLine:
1052 case eSelectEndLine:
1053 case eSelectNoAmount: {
1054 nsPrevNextBidiLevels levels =
1055 GetPrevNextBidiLevels(aNode, aContentOffset, aHint, false);
1057 SetCaretBidiLevelAndMaybeSchedulePaint(aHint == CARET_ASSOCIATE_BEFORE
1058 ? levels.mLevelBefore
1059 : levels.mLevelAfter);
1060 break;
1063 // Up and Down: the new cursor Bidi level is the smaller of the two
1064 surrounding characters case eSelectLine: case eSelectParagraph:
1065 GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame,
1066 &secondFrame, &firstLevel, &secondLevel);
1067 aPresShell->SetCaretBidiLevelAndMaybeSchedulePaint(std::min(firstLevel,
1068 secondLevel)); break;
1071 default:
1072 UndefineCaretBidiLevel();
1076 void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode,
1077 uint32_t aContentOffset) {
1078 nsIFrame* clickInFrame = nullptr;
1079 int32_t OffsetNotUsed;
1081 clickInFrame = GetFrameForNodeOffset(aNode, aContentOffset, mCaret.mHint,
1082 &OffsetNotUsed);
1083 if (!clickInFrame) return;
1085 SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel());
1088 void nsFrameSelection::MaintainedRange::AdjustNormalSelection(
1089 const nsIContent* aContent, const int32_t aOffset,
1090 Selection& aNormalSelection) const {
1091 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1093 if (!mRange || !aContent) {
1094 return;
1097 nsINode* rangeStartNode = mRange->GetStartContainer();
1098 nsINode* rangeEndNode = mRange->GetEndContainer();
1099 const uint32_t rangeStartOffset = mRange->StartOffset();
1100 const uint32_t rangeEndOffset = mRange->EndOffset();
1102 NS_ASSERTION(aOffset >= 0, "aOffset should not be negative");
1103 const Maybe<int32_t> relToStart =
1104 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1105 rangeStartNode, rangeStartOffset, aContent, aOffset);
1106 if (NS_WARN_IF(!relToStart)) {
1107 // Potentially handle this properly when Selection across Shadow DOM
1108 // boundary is implemented
1109 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1110 return;
1113 const Maybe<int32_t> relToEnd =
1114 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1115 rangeEndNode, rangeEndOffset, aContent, aOffset);
1116 if (NS_WARN_IF(!relToEnd)) {
1117 // Potentially handle this properly when Selection across Shadow DOM
1118 // boundary is implemented
1119 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1120 return;
1123 // If aContent/aOffset is inside (or at the edge of) the maintained
1124 // selection, or if it is on the "anchor" side of the maintained selection,
1125 // we need to do something.
1126 if ((*relToStart <= 0 && *relToEnd >= 0) ||
1127 (*relToStart > 0 && aNormalSelection.GetDirection() == eDirNext) ||
1128 (*relToEnd < 0 && aNormalSelection.GetDirection() == eDirPrevious)) {
1129 // Set the current range to the maintained range.
1130 aNormalSelection.ReplaceAnchorFocusRange(mRange);
1131 // Set the direction of the selection so that the anchor will be on the
1132 // far side of the maintained selection, relative to aContent/aOffset.
1133 aNormalSelection.SetDirection(*relToStart > 0 ? eDirPrevious : eDirNext);
1137 void nsFrameSelection::MaintainedRange::AdjustContentOffsets(
1138 nsIFrame::ContentOffsets& aOffsets, const bool aScrollViewStop) const {
1139 // Adjust offsets according to maintained amount
1140 if (mRange && mAmount != eSelectNoAmount) {
1141 nsINode* rangenode = mRange->GetStartContainer();
1142 int32_t rangeOffset = mRange->StartOffset();
1143 const Maybe<int32_t> relativePosition = nsContentUtils::ComparePoints(
1144 rangenode, rangeOffset, aOffsets.content, aOffsets.offset);
1145 if (NS_WARN_IF(!relativePosition)) {
1146 // Potentially handle this properly when Selection across Shadow DOM
1147 // boundary is implemented
1148 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1149 return;
1152 nsDirection direction = *relativePosition > 0 ? eDirPrevious : eDirNext;
1153 nsSelectionAmount amount = mAmount;
1154 if (amount == eSelectBeginLine && direction == eDirNext) {
1155 amount = eSelectEndLine;
1158 int32_t offset;
1159 nsIFrame* frame = GetFrameForNodeOffset(aOffsets.content, aOffsets.offset,
1160 CARET_ASSOCIATE_AFTER, &offset);
1162 if (frame && amount == eSelectWord && direction == eDirPrevious) {
1163 // To avoid selecting the previous word when at start of word,
1164 // first move one character forward.
1165 nsPeekOffsetStruct charPos(eSelectCharacter, eDirNext, offset,
1166 nsPoint(0, 0), false, aScrollViewStop, false,
1167 false, false);
1168 if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) {
1169 frame = charPos.mResultFrame;
1170 offset = charPos.mContentOffset;
1174 nsPeekOffsetStruct pos(amount, direction, offset, nsPoint(0, 0), false,
1175 aScrollViewStop, false, false, false);
1177 if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) {
1178 aOffsets.content = pos.mResultContent;
1179 aOffsets.offset = pos.mContentOffset;
1184 void nsFrameSelection::MaintainedRange::MaintainAnchorFocusRange(
1185 const Selection& aNormalSelection, const nsSelectionAmount aAmount) {
1186 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1188 mAmount = aAmount;
1190 const nsRange* anchorFocusRange = aNormalSelection.GetAnchorFocusRange();
1191 if (anchorFocusRange && aAmount != eSelectNoAmount) {
1192 mRange = anchorFocusRange->CloneRange();
1193 return;
1196 mRange = nullptr;
1199 nsresult nsFrameSelection::HandleClick(nsIContent* aNewFocus,
1200 uint32_t aContentOffset,
1201 uint32_t aContentEndOffset,
1202 const FocusMode aFocusMode,
1203 CaretAssociateHint aHint) {
1204 if (!aNewFocus) return NS_ERROR_INVALID_ARG;
1206 if (MOZ_LOG_TEST(sFrameSelectionLog, LogLevel::Debug)) {
1207 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1208 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1209 ("%s: selection=%p, new focus=%p, offsets=(%u,%u), focus mode=%i",
1210 __FUNCTION__,
1211 mDomSelections[index] ? mDomSelections[index].get() : nullptr,
1212 aNewFocus, aContentOffset, aContentEndOffset,
1213 static_cast<int>(aFocusMode)));
1216 mDesiredCaretPos.Invalidate();
1218 if (aFocusMode != FocusMode::kExtendSelection) {
1219 mMaintainedRange.mRange = nullptr;
1220 if (!IsValidSelectionPoint(aNewFocus)) {
1221 mLimiters.mAncestorLimiter = nullptr;
1225 // Don't take focus when dragging off of a table
1226 if (!mTableSelection.mDragSelectingCells) {
1227 BidiLevelFromClick(aNewFocus, aContentOffset);
1228 SetChangeReasons(nsISelectionListener::MOUSEDOWN_REASON +
1229 nsISelectionListener::DRAG_REASON);
1231 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1232 RefPtr<Selection> selection = mDomSelections[index];
1233 MOZ_ASSERT(selection);
1235 if (aFocusMode == FocusMode::kExtendSelection) {
1236 mMaintainedRange.AdjustNormalSelection(aNewFocus, aContentOffset,
1237 *selection);
1240 AutoPrepareFocusRange prep(selection,
1241 aFocusMode == FocusMode::kMultiRangeSelection);
1242 return TakeFocus(*aNewFocus, aContentOffset, aContentEndOffset, aHint,
1243 aFocusMode);
1246 return NS_OK;
1249 void nsFrameSelection::HandleDrag(nsIFrame* aFrame, const nsPoint& aPoint) {
1250 if (!aFrame || !mPresShell) {
1251 return;
1254 nsresult result;
1255 nsIFrame* newFrame = 0;
1256 nsPoint newPoint;
1258 result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame,
1259 newPoint);
1260 if (NS_FAILED(result)) return;
1261 if (!newFrame) return;
1263 nsIFrame::ContentOffsets offsets =
1264 newFrame->GetContentOffsetsFromPoint(newPoint);
1265 if (!offsets.content) return;
1267 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1268 RefPtr<Selection> selection = mDomSelections[index];
1269 if (newFrame->IsSelected() && selection) {
1270 // `MOZ_KnownLive` required because of
1271 // https://bugzilla.mozilla.org/show_bug.cgi?id=1636889.
1272 mMaintainedRange.AdjustNormalSelection(MOZ_KnownLive(offsets.content),
1273 offsets.offset, *selection);
1276 const bool scrollViewStop = mLimiters.mLimiter != nullptr;
1277 mMaintainedRange.AdjustContentOffsets(offsets, scrollViewStop);
1279 // TODO: no click has happened, rename `HandleClick`.
1280 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.offset,
1281 offsets.offset, FocusMode::kExtendSelection, offsets.associate);
1284 nsresult nsFrameSelection::StartAutoScrollTimer(nsIFrame* aFrame,
1285 const nsPoint& aPoint,
1286 uint32_t aDelay) {
1287 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1288 if (!mDomSelections[index]) {
1289 return NS_ERROR_NULL_POINTER;
1292 RefPtr<Selection> selection = mDomSelections[index];
1293 return selection->StartAutoScrollTimer(aFrame, aPoint, aDelay);
1296 void nsFrameSelection::StopAutoScrollTimer() {
1297 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1298 if (!mDomSelections[index]) {
1299 return;
1302 mDomSelections[index]->StopAutoScrollTimer();
1305 // static
1306 nsINode* nsFrameSelection::TableSelection::IsContentInActivelyEditableTableCell(
1307 nsPresContext* aContext, nsIContent* aContent) {
1308 if (!aContext) {
1309 return nullptr;
1312 RefPtr<HTMLEditor> htmlEditor = nsContentUtils::GetHTMLEditor(aContext);
1313 if (!htmlEditor) {
1314 return nullptr;
1317 nsINode* inclusiveTableCellAncestor =
1318 GetClosestInclusiveTableCellAncestor(aContent);
1319 if (!inclusiveTableCellAncestor) {
1320 return nullptr;
1323 const Element* editingHost = htmlEditor->ComputeEditingHost();
1324 if (!editingHost) {
1325 return nullptr;
1328 const bool editableCell =
1329 inclusiveTableCellAncestor->IsInclusiveDescendantOf(editingHost);
1330 return editableCell ? inclusiveTableCellAncestor : nullptr;
1333 namespace {
1334 struct ParentAndOffset {
1335 explicit ParentAndOffset(const nsINode& aNode)
1336 : mParent{aNode.GetParent()},
1337 mOffset{mParent ? mParent->ComputeIndexOf_Deprecated(&aNode) : 0} {}
1339 nsINode* mParent;
1341 // 0, if there's no parent.
1342 int32_t mOffset;
1345 } // namespace
1347 hard to go from nodes to frames, easy the other way!
1349 nsresult nsFrameSelection::TakeFocus(nsIContent& aNewFocus,
1350 uint32_t aContentOffset,
1351 uint32_t aContentEndOffset,
1352 CaretAssociateHint aHint,
1353 const FocusMode aFocusMode) {
1354 NS_ENSURE_STATE(mPresShell);
1356 if (!IsValidSelectionPoint(&aNewFocus)) {
1357 return NS_ERROR_FAILURE;
1360 MOZ_LOG(sFrameSelectionLog, LogLevel::Verbose,
1361 ("%s: new focus=%p, offsets=(%u, %u), hint=%i, focusMode=%i",
1362 __FUNCTION__, &aNewFocus, aContentOffset, aContentEndOffset,
1363 static_cast<int>(aHint), static_cast<int>(aFocusMode)));
1365 mPresShell->FrameSelectionWillTakeFocus(*this);
1367 // Clear all table selection data
1368 mTableSelection.mMode = TableSelectionMode::None;
1369 mTableSelection.mDragSelectingCells = false;
1370 mTableSelection.mStartSelectedCell = nullptr;
1371 mTableSelection.mEndSelectedCell = nullptr;
1372 mTableSelection.mAppendStartSelectedCell = nullptr;
1373 mCaret.mHint = aHint;
1375 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1376 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1378 Maybe<Selection::AutoUserInitiated> userSelect;
1379 if (IsUserSelectionReason()) {
1380 userSelect.emplace(mDomSelections[index]);
1383 // traverse through document and unselect crap here
1384 switch (aFocusMode) {
1385 case FocusMode::kCollapseToNewPoint:
1386 [[fallthrough]];
1387 case FocusMode::kMultiRangeSelection: {
1388 // single click? setting cursor down
1389 const Batching saveBatching =
1390 mBatching; // hack to use the collapse code.
1391 mBatching.mCounter = 1;
1393 RefPtr<Selection> selection = mDomSelections[index];
1395 if (aFocusMode == FocusMode::kMultiRangeSelection) {
1396 // Remove existing collapsed ranges as there's no point in having
1397 // non-anchor/focus collapsed ranges.
1398 selection->RemoveCollapsedRanges();
1400 ErrorResult error;
1401 RefPtr<nsRange> newRange = nsRange::Create(
1402 &aNewFocus, aContentOffset, &aNewFocus, aContentOffset, error);
1403 if (NS_WARN_IF(error.Failed())) {
1404 return error.StealNSResult();
1406 MOZ_ASSERT(newRange);
1407 selection->AddRangeAndSelectFramesAndNotifyListeners(*newRange,
1408 IgnoreErrors());
1409 } else {
1410 bool oldDesiredPosSet =
1411 mDesiredCaretPos.mIsSet; // need to keep old desired
1412 // position if it was set.
1413 selection->CollapseInLimiter(&aNewFocus, aContentOffset);
1414 mDesiredCaretPos.mIsSet =
1415 oldDesiredPosSet; // now reset desired pos back.
1418 mBatching = saveBatching;
1420 if (aContentEndOffset != aContentOffset) {
1421 selection->Extend(&aNewFocus, aContentEndOffset);
1424 // find out if we are inside a table. if so, find out which one and which
1425 // cell once we do that, the next time we get a takefocus, check the
1426 // parent tree. if we are no longer inside same table ,cell then switch to
1427 // table selection mode. BUT only do this in an editor
1429 NS_ENSURE_STATE(mPresShell);
1430 RefPtr<nsPresContext> context = mPresShell->GetPresContext();
1431 mTableSelection.mClosestInclusiveTableCellAncestor = nullptr;
1432 if (nsINode* inclusiveTableCellAncestor =
1433 TableSelection::IsContentInActivelyEditableTableCell(
1434 context, &aNewFocus)) {
1435 mTableSelection.mClosestInclusiveTableCellAncestor =
1436 inclusiveTableCellAncestor;
1437 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1438 ("%s: Collapsing into new cell", __FUNCTION__));
1441 break;
1443 case FocusMode::kExtendSelection: {
1444 // Now update the range list:
1445 nsINode* inclusiveTableCellAncestor =
1446 GetClosestInclusiveTableCellAncestor(&aNewFocus);
1447 if (mTableSelection.mClosestInclusiveTableCellAncestor &&
1448 inclusiveTableCellAncestor &&
1449 inclusiveTableCellAncestor !=
1450 mTableSelection
1451 .mClosestInclusiveTableCellAncestor) // switch to cell
1452 // selection mode
1454 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1455 ("%s: moving into new cell", __FUNCTION__));
1457 WidgetMouseEvent event(false, eVoidEvent, nullptr,
1458 WidgetMouseEvent::eReal);
1460 // Start selecting in the cell we were in before
1461 ParentAndOffset parentAndOffset{
1462 *mTableSelection.mClosestInclusiveTableCellAncestor};
1463 if (parentAndOffset.mParent) {
1464 const nsresult result = HandleTableSelection(
1465 parentAndOffset.mParent, parentAndOffset.mOffset,
1466 TableSelectionMode::Cell, &event);
1467 if (NS_WARN_IF(NS_FAILED(result))) {
1468 return result;
1472 // Find the parent of this new cell and extend selection to it
1473 parentAndOffset = ParentAndOffset{*inclusiveTableCellAncestor};
1475 // XXXX We need to REALLY get the current key shift state
1476 // (we'd need to add event listener -- let's not bother for now)
1477 event.mModifiers &= ~MODIFIER_SHIFT; // aContinueSelection;
1478 if (parentAndOffset.mParent) {
1479 mTableSelection.mClosestInclusiveTableCellAncestor =
1480 inclusiveTableCellAncestor;
1481 // Continue selection into next cell
1482 const nsresult result = HandleTableSelection(
1483 parentAndOffset.mParent, parentAndOffset.mOffset,
1484 TableSelectionMode::Cell, &event);
1485 if (NS_WARN_IF(NS_FAILED(result))) {
1486 return result;
1489 } else {
1490 RefPtr<Selection> selection = mDomSelections[index];
1491 // XXXX Problem: Shift+click in browser is appending text selection to
1492 // selected table!!!
1493 // is this the place to erase selected cells ?????
1494 uint32_t offset =
1495 (selection->GetDirection() == eDirNext &&
1496 aContentEndOffset > aContentOffset) // didn't go far enough
1497 ? aContentEndOffset // this will only redraw the diff
1498 : aContentOffset;
1499 selection->Extend(&aNewFocus, offset);
1501 break;
1505 // Don't notify selection listeners if batching is on:
1506 if (IsBatching()) {
1507 return NS_OK;
1510 // Be aware, the Selection instance may be destroyed after this call.
1511 return NotifySelectionListeners(SelectionType::eNormal);
1514 UniquePtr<SelectionDetails> nsFrameSelection::LookUpSelection(
1515 nsIContent* aContent, int32_t aContentOffset, int32_t aContentLength,
1516 bool aSlowCheck) const {
1517 if (!aContent || !mPresShell) {
1518 return nullptr;
1521 // TODO: Layout should use `uint32_t` for handling offset in DOM nodes
1522 // (for example: bug 1735262)
1523 MOZ_ASSERT(aContentOffset >= 0);
1524 MOZ_ASSERT(aContentLength >= 0);
1525 if (MOZ_UNLIKELY(aContentOffset < 0) || MOZ_UNLIKELY(aContentLength < 0)) {
1526 return nullptr;
1529 UniquePtr<SelectionDetails> details;
1531 for (size_t j = 0; j < ArrayLength(mDomSelections); j++) {
1532 if (mDomSelections[j]) {
1533 details = mDomSelections[j]->LookUpSelection(
1534 aContent, static_cast<uint32_t>(aContentOffset),
1535 static_cast<uint32_t>(aContentLength), std::move(details),
1536 kPresentSelectionTypes[j], aSlowCheck);
1540 return details;
1543 void nsFrameSelection::SetDragState(bool aState) {
1544 if (mDragState == aState) return;
1546 mDragState = aState;
1548 if (!mDragState) {
1549 mTableSelection.mDragSelectingCells = false;
1550 // Notify that reason is mouse up.
1551 SetChangeReasons(nsISelectionListener::MOUSEUP_REASON);
1553 // flag is set to false in `NotifySelectionListeners`.
1554 // since this function call is part of click event, this would immediately
1555 // reset the flag, rendering it useless.
1556 AutoRestore<bool> restoreIsDoubleClickSelectionFlag(
1557 mIsDoubleClickSelection);
1558 // Be aware, the Selection instance may be destroyed after this call.
1559 NotifySelectionListeners(SelectionType::eNormal);
1563 Selection* nsFrameSelection::GetSelection(SelectionType aSelectionType) const {
1564 int8_t index = GetIndexFromSelectionType(aSelectionType);
1565 if (index < 0) return nullptr;
1567 return mDomSelections[index];
1570 nsresult nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType,
1571 SelectionRegion aRegion,
1572 int16_t aFlags) const {
1573 int8_t index = GetIndexFromSelectionType(aSelectionType);
1574 if (index < 0) return NS_ERROR_INVALID_ARG;
1576 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1578 ScrollAxis verticalScroll = ScrollAxis();
1579 int32_t flags = Selection::SCROLL_DO_FLUSH;
1580 if (aFlags & nsISelectionController::SCROLL_SYNCHRONOUS) {
1581 flags |= Selection::SCROLL_SYNCHRONOUS;
1582 } else if (aFlags & nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY) {
1583 flags |= Selection::SCROLL_FIRST_ANCESTOR_ONLY;
1585 if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) {
1586 flags |= Selection::SCROLL_OVERFLOW_HIDDEN;
1588 if (aFlags & nsISelectionController::SCROLL_CENTER_VERTICALLY) {
1589 verticalScroll =
1590 ScrollAxis(kScrollToCenter, WhenToScroll::IfNotFullyVisible);
1592 if (aFlags & nsISelectionController::SCROLL_FOR_CARET_MOVE) {
1593 flags |= Selection::SCROLL_FOR_CARET_MOVE;
1596 // After ScrollSelectionIntoView(), the pending notifications might be
1597 // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
1598 RefPtr<Selection> sel = mDomSelections[index];
1599 return sel->ScrollIntoView(aRegion, verticalScroll, ScrollAxis(), flags);
1602 nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) {
1603 int8_t index = GetIndexFromSelectionType(aSelectionType);
1604 if (index < 0) return NS_ERROR_INVALID_ARG;
1605 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1606 NS_ENSURE_STATE(mPresShell);
1608 // On macOS, update the selection cache to the new active selection
1609 // aka the current selection.
1610 #ifdef XP_MACOSX
1611 // Check that we're in the an active window and, if this is Web content,
1612 // in the frontmost tab.
1613 Document* doc = mPresShell->GetDocument();
1614 if (doc && IsInActiveTab(doc) && aSelectionType == SelectionType::eNormal) {
1615 UpdateSelectionCacheOnRepaintSelection(mDomSelections[index]);
1617 #endif
1618 return mDomSelections[index]->Repaint(mPresShell->GetPresContext());
1621 static bool IsDisplayContents(const nsIContent* aContent) {
1622 return aContent->IsElement() && aContent->AsElement()->IsDisplayContents();
1625 bool nsFrameSelection::AdjustFrameForLineStart(nsIFrame*& aFrame,
1626 int32_t& aFrameOffset) {
1627 if (!aFrame->HasSignificantTerminalNewline()) {
1628 return false;
1631 auto [start, end] = aFrame->GetOffsets();
1632 if (aFrameOffset != end) {
1633 return false;
1636 nsIFrame* nextSibling = aFrame->GetNextSibling();
1637 if (!nextSibling) {
1638 return false;
1641 aFrame = nextSibling;
1642 std::tie(start, end) = aFrame->GetOffsets();
1643 aFrameOffset = start;
1644 return true;
1647 // static
1648 nsIFrame* nsFrameSelection::GetFrameForNodeOffset(nsIContent* aNode,
1649 int32_t aOffset,
1650 CaretAssociateHint aHint,
1651 int32_t* aReturnOffset) {
1652 if (!aNode || !aReturnOffset) return nullptr;
1654 if (aOffset < 0) return nullptr;
1656 if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) {
1657 return nullptr;
1660 nsIFrame* returnFrame = nullptr;
1661 nsCOMPtr<nsIContent> theNode;
1663 while (true) {
1664 *aReturnOffset = aOffset;
1666 theNode = aNode;
1668 if (aNode->IsElement()) {
1669 int32_t childIndex = 0;
1670 int32_t numChildren = theNode->GetChildCount();
1672 if (aHint == CARET_ASSOCIATE_BEFORE) {
1673 if (aOffset > 0) {
1674 childIndex = aOffset - 1;
1675 } else {
1676 childIndex = aOffset;
1678 } else {
1679 NS_ASSERTION(aHint == CARET_ASSOCIATE_AFTER, "unknown direction");
1680 if (aOffset >= numChildren) {
1681 if (numChildren > 0) {
1682 childIndex = numChildren - 1;
1683 } else {
1684 childIndex = 0;
1686 } else {
1687 childIndex = aOffset;
1691 if (childIndex > 0 || numChildren > 0) {
1692 nsCOMPtr<nsIContent> childNode =
1693 theNode->GetChildAt_Deprecated(childIndex);
1695 if (!childNode) {
1696 break;
1699 theNode = childNode;
1702 // Now that we have the child node, check if it too
1703 // can contain children. If so, descend into child.
1704 if (theNode->IsElement() && theNode->GetChildCount() &&
1705 !theNode->HasIndependentSelection()) {
1706 aNode = theNode;
1707 aOffset = aOffset > childIndex ? theNode->GetChildCount() : 0;
1708 continue;
1709 } else {
1710 // Check to see if theNode is a text node. If it is, translate
1711 // aOffset into an offset into the text node.
1713 RefPtr<Text> textNode = theNode->GetAsText();
1714 if (textNode) {
1715 if (theNode->GetPrimaryFrame()) {
1716 if (aOffset > childIndex) {
1717 uint32_t textLength = textNode->Length();
1719 *aReturnOffset = (int32_t)textLength;
1720 } else {
1721 *aReturnOffset = 0;
1723 } else {
1724 int32_t numChildren = aNode->GetChildCount();
1725 int32_t newChildIndex = aHint == CARET_ASSOCIATE_BEFORE
1726 ? childIndex - 1
1727 : childIndex + 1;
1729 if (newChildIndex >= 0 && newChildIndex < numChildren) {
1730 nsCOMPtr<nsIContent> newChildNode =
1731 aNode->GetChildAt_Deprecated(newChildIndex);
1732 if (!newChildNode) {
1733 return nullptr;
1736 aNode = newChildNode;
1737 aOffset =
1738 aHint == CARET_ASSOCIATE_BEFORE ? aNode->GetChildCount() : 0;
1739 continue;
1740 } else {
1741 // newChildIndex is illegal which means we're at first or last
1742 // child. Just use original node to get the frame.
1743 theNode = aNode;
1750 // If the node is a ShadowRoot, the frame needs to be adjusted,
1751 // because a ShadowRoot does not get a frame. Its children are rendered
1752 // as children of the host.
1753 if (ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) {
1754 theNode = shadow->GetHost();
1757 returnFrame = theNode->GetPrimaryFrame();
1758 if (!returnFrame) {
1759 if (aHint == CARET_ASSOCIATE_BEFORE) {
1760 if (aOffset > 0) {
1761 --aOffset;
1762 continue;
1763 } else {
1764 break;
1766 } else {
1767 int32_t end = theNode->GetChildCount();
1768 if (aOffset < end) {
1769 ++aOffset;
1770 continue;
1771 } else {
1772 break;
1777 break;
1778 } // end while
1780 if (!returnFrame) return nullptr;
1782 // If we ended up here and were asked to position the caret after a visible
1783 // break, let's return the frame on the next line instead if it exists.
1784 if (aOffset > 0 && (uint32_t)aOffset >= aNode->Length() &&
1785 theNode == aNode->GetLastChild()) {
1786 nsIFrame* newFrame;
1787 nsLayoutUtils::IsInvisibleBreak(theNode, &newFrame);
1788 if (newFrame) {
1789 returnFrame = newFrame;
1790 *aReturnOffset = 0;
1794 // find the child frame containing the offset we want
1795 returnFrame->GetChildFrameContainingOffset(
1796 *aReturnOffset, aHint == CARET_ASSOCIATE_AFTER, &aOffset, &returnFrame);
1797 return returnFrame;
1800 nsIFrame* nsFrameSelection::GetFrameToPageSelect() const {
1801 if (NS_WARN_IF(!mPresShell)) {
1802 return nullptr;
1805 nsIFrame* rootFrameToSelect;
1806 if (mLimiters.mLimiter) {
1807 rootFrameToSelect = mLimiters.mLimiter->GetPrimaryFrame();
1808 if (NS_WARN_IF(!rootFrameToSelect)) {
1809 return nullptr;
1811 } else if (mLimiters.mAncestorLimiter) {
1812 rootFrameToSelect = mLimiters.mAncestorLimiter->GetPrimaryFrame();
1813 if (NS_WARN_IF(!rootFrameToSelect)) {
1814 return nullptr;
1816 } else {
1817 rootFrameToSelect = mPresShell->GetRootScrollFrame();
1818 if (NS_WARN_IF(!rootFrameToSelect)) {
1819 return nullptr;
1823 nsCOMPtr<nsIContent> contentToSelect = mPresShell->GetContentForScrolling();
1824 if (contentToSelect) {
1825 // If there is selected content, look for nearest and vertical scrollable
1826 // parent under the root frame.
1827 for (nsIFrame* frame = contentToSelect->GetPrimaryFrame();
1828 frame && frame != rootFrameToSelect; frame = frame->GetParent()) {
1829 nsIScrollableFrame* scrollableFrame = do_QueryFrame(frame);
1830 if (!scrollableFrame) {
1831 continue;
1833 ScrollStyles scrollStyles = scrollableFrame->GetScrollStyles();
1834 if (scrollStyles.mVertical == StyleOverflow::Hidden) {
1835 continue;
1837 layers::ScrollDirections directions =
1838 scrollableFrame->GetAvailableScrollingDirections();
1839 if (directions.contains(layers::ScrollDirection::eVertical)) {
1840 // If there is sub scrollable frame, let's use its page size to select.
1841 return frame;
1845 // Otherwise, i.e., there is no scrollable frame or only the root frame is
1846 // scrollable, let's return the root frame because Shift + PageUp/PageDown
1847 // should expand the selection in the root content even if it's not
1848 // scrollable.
1849 return rootFrameToSelect;
1852 nsresult nsFrameSelection::PageMove(bool aForward, bool aExtend,
1853 nsIFrame* aFrame,
1854 SelectionIntoView aSelectionIntoView) {
1855 MOZ_ASSERT(aFrame);
1857 // expected behavior for PageMove is to scroll AND move the caret
1858 // and remain relative position of the caret in view. see Bug 4302.
1860 // Get the scrollable frame. If aFrame is not scrollable, this is nullptr.
1861 nsIScrollableFrame* scrollableFrame = aFrame->GetScrollTargetFrame();
1862 // Get the scrolled frame. If aFrame is not scrollable, this is aFrame
1863 // itself.
1864 nsIFrame* scrolledFrame =
1865 scrollableFrame ? scrollableFrame->GetScrolledFrame() : aFrame;
1866 if (!scrolledFrame) {
1867 return NS_OK;
1870 // find out where the caret is.
1871 // we should know mDesiredCaretPos.mValue value of nsFrameSelection, but I
1872 // havent seen that behavior in other windows applications yet.
1873 RefPtr<Selection> selection = GetSelection(SelectionType::eNormal);
1874 if (!selection) {
1875 return NS_OK;
1878 nsRect caretPos;
1879 nsIFrame* caretFrame = nsCaret::GetGeometry(selection, &caretPos);
1880 if (!caretFrame) {
1881 return NS_OK;
1884 // If the scrolled frame is outside of current selection limiter,
1885 // we need to scroll the frame but keep moving selection in the limiter.
1886 nsIFrame* frameToClick = scrolledFrame;
1887 if (!IsValidSelectionPoint(scrolledFrame->GetContent())) {
1888 frameToClick = GetFrameToPageSelect();
1889 if (NS_WARN_IF(!frameToClick)) {
1890 return NS_OK;
1894 if (scrollableFrame) {
1895 // If there is a scrollable frame, adjust pseudo-click position with page
1896 // scroll amount.
1897 // XXX This may scroll more than one page if ScrollSelectionIntoView is
1898 // called later because caret may not fully visible. E.g., if
1899 // clicking line will be visible only half height with scrolling
1900 // the frame, ScrollSelectionIntoView additionally scrolls to show
1901 // the caret entirely.
1902 if (aForward) {
1903 caretPos.y += scrollableFrame->GetPageScrollAmount().height;
1904 } else {
1905 caretPos.y -= scrollableFrame->GetPageScrollAmount().height;
1907 } else {
1908 // Otherwise, adjust pseudo-click position with the frame size.
1909 if (aForward) {
1910 caretPos.y += frameToClick->GetSize().height;
1911 } else {
1912 caretPos.y -= frameToClick->GetSize().height;
1916 caretPos += caretFrame->GetOffsetTo(frameToClick);
1918 // get a content at desired location
1919 nsPoint desiredPoint;
1920 desiredPoint.x = caretPos.x;
1921 desiredPoint.y = caretPos.y + caretPos.height / 2;
1922 nsIFrame::ContentOffsets offsets =
1923 frameToClick->GetContentOffsetsFromPoint(desiredPoint);
1925 if (!offsets.content) {
1926 // XXX Do we need to handle ScrollSelectionIntoView in this case?
1927 return NS_OK;
1930 // First, place the caret.
1931 bool selectionChanged;
1933 // We don't want any script to run until we check whether selection is
1934 // modified by HandleClick.
1935 SelectionBatcher ensureNoSelectionChangeNotifications(selection,
1936 __FUNCTION__);
1938 RangeBoundary oldAnchor = selection->AnchorRef();
1939 RangeBoundary oldFocus = selection->FocusRef();
1941 const FocusMode focusMode =
1942 aExtend ? FocusMode::kExtendSelection : FocusMode::kCollapseToNewPoint;
1943 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
1944 offsets.offset, offsets.offset, focusMode,
1945 CARET_ASSOCIATE_AFTER);
1947 selectionChanged = selection->AnchorRef() != oldAnchor ||
1948 selection->FocusRef() != oldFocus;
1951 bool doScrollSelectionIntoView = !(
1952 aSelectionIntoView == SelectionIntoView::IfChanged && !selectionChanged);
1954 // Then, scroll the given frame one page.
1955 if (scrollableFrame) {
1956 // If we'll call ScrollSelectionIntoView later and selection wasn't
1957 // changed and we scroll outside of selection limiter, we shouldn't use
1958 // smooth scroll here because nsIScrollableFrame uses normal runnable,
1959 // but ScrollSelectionIntoView uses early runner and it cancels the
1960 // pending smooth scroll. Therefore, if we used smooth scroll in such
1961 // case, ScrollSelectionIntoView would scroll to show caret instead of
1962 // page scroll of an element outside selection limiter.
1963 ScrollMode scrollMode = doScrollSelectionIntoView && !selectionChanged &&
1964 scrolledFrame != frameToClick
1965 ? ScrollMode::Instant
1966 : ScrollMode::Smooth;
1967 scrollableFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
1968 ScrollUnit::PAGES, scrollMode);
1971 // Finally, scroll selection into view if requested.
1972 if (!doScrollSelectionIntoView) {
1973 return NS_OK;
1975 return ScrollSelectionIntoView(
1976 SelectionType::eNormal, nsISelectionController::SELECTION_FOCUS_REGION,
1977 nsISelectionController::SCROLL_SYNCHRONOUS |
1978 nsISelectionController::SCROLL_FOR_CARET_MOVE);
1981 nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount,
1982 bool aExtend) {
1983 NS_ENSURE_STATE(mPresShell);
1984 // Flush out layout, since we need it to be up to date to do caret
1985 // positioning.
1986 OwningNonNull<PresShell> presShell(*mPresShell);
1987 presShell->FlushPendingNotifications(FlushType::Layout);
1989 if (!mPresShell) {
1990 return NS_OK;
1993 // Check that parameters are safe
1994 if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) {
1995 return NS_ERROR_FAILURE;
1998 nsPresContext* context = mPresShell->GetPresContext();
1999 if (!context) {
2000 return NS_ERROR_FAILURE;
2003 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2004 RefPtr<Selection> sel = mDomSelections[index];
2005 if (!sel) {
2006 return NS_ERROR_NULL_POINTER;
2009 // Map the abstract movement amounts (0-1) to direction-specific
2010 // selection units.
2011 static const nsSelectionAmount inlineAmount[] = {eSelectCluster, eSelectWord};
2012 static const nsSelectionAmount blockPrevAmount[] = {eSelectLine,
2013 eSelectBeginLine};
2014 static const nsSelectionAmount blockNextAmount[] = {eSelectLine,
2015 eSelectEndLine};
2017 struct PhysicalToLogicalMapping {
2018 nsDirection direction;
2019 const nsSelectionAmount* amounts;
2021 static const PhysicalToLogicalMapping verticalLR[4] = {
2022 {eDirPrevious, blockPrevAmount}, // left
2023 {eDirNext, blockNextAmount}, // right
2024 {eDirPrevious, inlineAmount}, // up
2025 {eDirNext, inlineAmount} // down
2027 static const PhysicalToLogicalMapping verticalRL[4] = {
2028 {eDirNext, blockNextAmount},
2029 {eDirPrevious, blockPrevAmount},
2030 {eDirPrevious, inlineAmount},
2031 {eDirNext, inlineAmount}};
2032 static const PhysicalToLogicalMapping horizontal[4] = {
2033 {eDirPrevious, inlineAmount},
2034 {eDirNext, inlineAmount},
2035 {eDirPrevious, blockPrevAmount},
2036 {eDirNext, blockNextAmount}};
2038 WritingMode wm;
2039 nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(true);
2040 if (frame) {
2041 if (!frame->Style()->IsTextCombined()) {
2042 wm = frame->GetWritingMode();
2043 } else {
2044 // Using different direction for horizontal-in-vertical would
2045 // make it hard to navigate via keyboard. Inherit the moving
2046 // direction from its parent.
2047 MOZ_ASSERT(frame->IsTextFrame());
2048 wm = frame->GetParent()->GetWritingMode();
2049 MOZ_ASSERT(wm.IsVertical(),
2050 "Text combined "
2051 "can only appear in vertical text");
2055 const PhysicalToLogicalMapping& mapping =
2056 wm.IsVertical()
2057 ? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection]
2058 : horizontal[aDirection];
2060 nsresult rv =
2061 MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount], eVisual);
2062 if (NS_FAILED(rv)) {
2063 // If we tried to do a line move, but couldn't move in the given direction,
2064 // then we'll "promote" this to a line-edge move instead.
2065 if (mapping.amounts[aAmount] == eSelectLine) {
2066 rv = MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount + 1],
2067 eVisual);
2069 // And if it was a next-word move that failed (which can happen when
2070 // eat_space_to_next_word is true, see bug 1153237), then just move forward
2071 // to the line-edge.
2072 else if (mapping.amounts[aAmount] == eSelectWord &&
2073 mapping.direction == eDirNext) {
2074 rv = MoveCaret(eDirNext, aExtend, eSelectEndLine, eVisual);
2078 return rv;
2081 nsresult nsFrameSelection::CharacterMove(bool aForward, bool aExtend) {
2082 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectCluster,
2083 eUsePrefStyle);
2086 nsresult nsFrameSelection::WordMove(bool aForward, bool aExtend) {
2087 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectWord,
2088 eUsePrefStyle);
2091 nsresult nsFrameSelection::LineMove(bool aForward, bool aExtend) {
2092 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectLine,
2093 eUsePrefStyle);
2096 nsresult nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) {
2097 if (aForward) {
2098 return MoveCaret(eDirNext, aExtend, eSelectEndLine, eLogical);
2099 } else {
2100 return MoveCaret(eDirPrevious, aExtend, eSelectBeginLine, eLogical);
2104 template <typename RangeType>
2105 Result<RefPtr<RangeType>, nsresult>
2106 nsFrameSelection::CreateRangeExtendedToSomewhere(
2107 nsDirection aDirection, const nsSelectionAmount aAmount,
2108 CaretMovementStyle aMovementStyle) {
2109 MOZ_ASSERT(aDirection == eDirNext || aDirection == eDirPrevious);
2110 MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster ||
2111 aAmount == eSelectWord || aAmount == eSelectBeginLine ||
2112 aAmount == eSelectEndLine);
2113 MOZ_ASSERT(aMovementStyle == eLogical || aMovementStyle == eVisual ||
2114 aMovementStyle == eUsePrefStyle);
2116 if (!mPresShell) {
2117 return Err(NS_ERROR_UNEXPECTED);
2119 OwningNonNull<PresShell> presShell(*mPresShell);
2120 presShell->FlushPendingNotifications(FlushType::Layout);
2121 if (!mPresShell) {
2122 return Err(NS_ERROR_FAILURE);
2124 Selection* selection =
2125 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
2126 if (!selection || selection->RangeCount() != 1) {
2127 return Err(NS_ERROR_FAILURE);
2129 RefPtr<const nsRange> firstRange = selection->GetRangeAt(0);
2130 if (!firstRange || !firstRange->IsPositioned()) {
2131 return Err(NS_ERROR_FAILURE);
2133 Result<nsPeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
2134 aDirection, true, aAmount, aMovementStyle, nsPoint(0, 0));
2135 if (result.isErr()) {
2136 return Err(NS_ERROR_FAILURE);
2138 const nsPeekOffsetStruct& pos = result.inspect();
2139 RefPtr<RangeType> range;
2140 if (NS_WARN_IF(!pos.mResultContent)) {
2141 return range;
2143 if (aDirection == eDirPrevious) {
2144 range = RangeType::Create(
2145 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
2146 firstRange->EndRef(), IgnoreErrors());
2147 } else {
2148 range = RangeType::Create(
2149 firstRange->StartRef(),
2150 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
2151 IgnoreErrors());
2153 return range;
2156 //////////END FRAMESELECTION
2158 LazyLogModule gBatchLog("SelectionBatch");
2160 void nsFrameSelection::StartBatchChanges(const char* aRequesterFuncName) {
2161 MOZ_LOG(gBatchLog, LogLevel::Info,
2162 ("%p%snsFrameSelection::StartBatchChanges(%s)", this,
2163 std::string((mBatching.mCounter + 1) * 2, ' ').c_str(),
2164 aRequesterFuncName));
2165 mBatching.mCounter++;
2168 void nsFrameSelection::EndBatchChanges(const char* aRequesterFuncName,
2169 int16_t aReasons) {
2170 MOZ_LOG(gBatchLog, LogLevel::Info,
2171 ("%p%snsFrameSelection::EndBatchChanges (%s, %s)", this,
2172 std::string(mBatching.mCounter * 2, ' ').c_str(), aRequesterFuncName,
2173 SelectionChangeReasonsToCString(aReasons).get()));
2174 MOZ_ASSERT(mBatching.mCounter > 0, "Bad mBatching.mCounter");
2175 mBatching.mCounter--;
2177 if (mBatching.mCounter == 0 && mBatching.mChangesDuringBatching) {
2178 AddChangeReasons(aReasons);
2179 mCaretMoveAmount = eSelectNoAmount;
2180 mBatching.mChangesDuringBatching = false;
2181 // Be aware, the Selection instance may be destroyed after this call.
2182 NotifySelectionListeners(SelectionType::eNormal);
2186 nsresult nsFrameSelection::NotifySelectionListeners(
2187 SelectionType aSelectionType) {
2188 int8_t index = GetIndexFromSelectionType(aSelectionType);
2189 if (index >= 0 && mDomSelections[index]) {
2190 RefPtr<Selection> selection = mDomSelections[index];
2191 selection->NotifySelectionListeners();
2192 mCaretMoveAmount = eSelectNoAmount;
2193 return NS_OK;
2195 return NS_ERROR_FAILURE;
2198 // Start of Table Selection methods
2200 static bool IsCell(nsIContent* aContent) {
2201 return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
2204 // static
2205 nsITableCellLayout* nsFrameSelection::GetCellLayout(
2206 const nsIContent* aCellContent) {
2207 nsITableCellLayout* cellLayoutObject =
2208 do_QueryFrame(aCellContent->GetPrimaryFrame());
2209 return cellLayoutObject;
2212 nsresult nsFrameSelection::ClearNormalSelection() {
2213 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2214 RefPtr<Selection> selection = mDomSelections[index];
2215 if (!selection) {
2216 return NS_ERROR_NULL_POINTER;
2219 ErrorResult err;
2220 selection->RemoveAllRanges(err);
2221 return err.StealNSResult();
2224 static nsIContent* GetFirstSelectedContent(const nsRange* aRange) {
2225 if (!aRange) {
2226 return nullptr;
2229 MOZ_ASSERT(aRange->GetStartContainer(), "Must have start parent!");
2230 MOZ_ASSERT(aRange->GetStartContainer()->IsElement(), "Unexpected parent");
2232 return aRange->GetChildAtStartOffset();
2235 // Table selection support.
2236 // TODO: Separate table methods into a separate nsITableSelection interface
2237 nsresult nsFrameSelection::HandleTableSelection(nsINode* aParentContent,
2238 int32_t aContentOffset,
2239 TableSelectionMode aTarget,
2240 WidgetMouseEvent* aMouseEvent) {
2241 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2242 RefPtr<Selection> selection = mDomSelections[index];
2243 if (!selection) {
2244 return NS_ERROR_NULL_POINTER;
2247 return mTableSelection.HandleSelection(aParentContent, aContentOffset,
2248 aTarget, aMouseEvent, mDragState,
2249 *selection);
2252 nsresult nsFrameSelection::TableSelection::HandleSelection(
2253 nsINode* aParentContent, int32_t aContentOffset, TableSelectionMode aTarget,
2254 WidgetMouseEvent* aMouseEvent, bool aDragState,
2255 Selection& aNormalSelection) {
2256 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2258 NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER);
2259 NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER);
2261 if (aDragState && mDragSelectingCells &&
2262 aTarget == TableSelectionMode::Table) {
2263 // We were selecting cells and user drags mouse in table border or inbetween
2264 // cells,
2265 // just do nothing
2266 return NS_OK;
2269 RefPtr<nsIContent> childContent =
2270 aParentContent->GetChildAt_Deprecated(aContentOffset);
2272 // When doing table selection, always set the direction to next so
2273 // we can be sure that anchorNode's offset always points to the
2274 // selected cell
2275 aNormalSelection.SetDirection(eDirNext);
2277 // Stack-class to wrap all table selection changes in
2278 // BeginBatchChanges() / EndBatchChanges()
2279 SelectionBatcher selectionBatcher(&aNormalSelection, __FUNCTION__);
2281 if (aDragState && mDragSelectingCells) {
2282 return HandleDragSelecting(aTarget, childContent, aMouseEvent,
2283 aNormalSelection);
2286 return HandleMouseUpOrDown(aTarget, aDragState, childContent, aParentContent,
2287 aContentOffset, aMouseEvent, aNormalSelection);
2290 class nsFrameSelection::TableSelection::RowAndColumnRelation {
2291 public:
2292 static Result<RowAndColumnRelation, nsresult> Create(
2293 const nsIContent* aFirst, const nsIContent* aSecond) {
2294 RowAndColumnRelation result;
2296 nsresult errorResult =
2297 GetCellIndexes(aFirst, result.mFirst.mRow, result.mFirst.mColumn);
2298 if (NS_FAILED(errorResult)) {
2299 return Err(errorResult);
2302 errorResult =
2303 GetCellIndexes(aSecond, result.mSecond.mRow, result.mSecond.mColumn);
2304 if (NS_FAILED(errorResult)) {
2305 return Err(errorResult);
2308 return result;
2311 bool IsSameColumn() const { return mFirst.mColumn == mSecond.mColumn; }
2313 bool IsSameRow() const { return mFirst.mRow == mSecond.mRow; }
2315 private:
2316 RowAndColumnRelation() = default;
2318 struct RowAndColumn {
2319 int32_t mRow = 0;
2320 int32_t mColumn = 0;
2323 RowAndColumn mFirst;
2324 RowAndColumn mSecond;
2327 nsresult nsFrameSelection::TableSelection::HandleDragSelecting(
2328 TableSelectionMode aTarget, nsIContent* aChildContent,
2329 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2330 // We are drag-selecting
2331 if (aTarget != TableSelectionMode::Table) {
2332 // If dragging in the same cell as last event, do nothing
2333 if (mEndSelectedCell == aChildContent) {
2334 return NS_OK;
2337 #ifdef DEBUG_TABLE_SELECTION
2338 printf(
2339 " mStartSelectedCell = %p, "
2340 "mEndSelectedCell = %p, aChildContent = %p "
2341 "\n",
2342 mStartSelectedCell.get(), mEndSelectedCell.get(), aChildContent);
2343 #endif
2344 // aTarget can be any "cell mode",
2345 // so we can easily drag-select rows and columns
2346 // Once we are in row or column mode,
2347 // we can drift into any cell to stay in that mode
2348 // even if aTarget = TableSelectionMode::Cell
2350 if (mMode == TableSelectionMode::Row ||
2351 mMode == TableSelectionMode::Column) {
2352 if (mEndSelectedCell) {
2353 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2354 RowAndColumnRelation::Create(mEndSelectedCell, aChildContent);
2356 if (rowAndColumnRelation.isErr()) {
2357 return rowAndColumnRelation.unwrapErr();
2360 if ((mMode == TableSelectionMode::Row &&
2361 rowAndColumnRelation.inspect().IsSameRow()) ||
2362 (mMode == TableSelectionMode::Column &&
2363 rowAndColumnRelation.inspect().IsSameColumn())) {
2364 return NS_OK;
2367 #ifdef DEBUG_TABLE_SELECTION
2368 printf(" Dragged into a new column or row\n");
2369 #endif
2370 // Continue dragging row or column selection
2372 return SelectRowOrColumn(aChildContent, aNormalSelection);
2374 if (mMode == TableSelectionMode::Cell) {
2375 #ifdef DEBUG_TABLE_SELECTION
2376 printf("HandleTableSelection: Dragged into a new cell\n");
2377 #endif
2378 // Trick for quick selection of rows and columns
2379 // Hold down shift, then start selecting in one direction
2380 // If next cell dragged into is in same row, select entire row,
2381 // if next cell is in same column, select entire column
2382 if (mStartSelectedCell && aMouseEvent->IsShift()) {
2383 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2384 RowAndColumnRelation::Create(mStartSelectedCell, aChildContent);
2385 if (rowAndColumnRelation.isErr()) {
2386 return rowAndColumnRelation.unwrapErr();
2389 if (rowAndColumnRelation.inspect().IsSameRow() ||
2390 rowAndColumnRelation.inspect().IsSameColumn()) {
2391 // Force new selection block
2392 mStartSelectedCell = nullptr;
2393 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2395 if (rowAndColumnRelation.inspect().IsSameRow()) {
2396 mMode = TableSelectionMode::Row;
2397 } else {
2398 mMode = TableSelectionMode::Column;
2401 return SelectRowOrColumn(aChildContent, aNormalSelection);
2405 // Reselect block of cells to new end location
2406 return SelectBlockOfCells(mStartSelectedCell, aChildContent,
2407 aNormalSelection);
2410 // Do nothing if dragging in table, but outside a cell
2411 return NS_OK;
2414 nsresult nsFrameSelection::TableSelection::HandleMouseUpOrDown(
2415 TableSelectionMode aTarget, bool aDragState, nsIContent* aChildContent,
2416 nsINode* aParentContent, int32_t aContentOffset,
2417 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2418 nsresult result = NS_OK;
2419 // Not dragging -- mouse event is down or up
2420 if (aDragState) {
2421 #ifdef DEBUG_TABLE_SELECTION
2422 printf("HandleTableSelection: Mouse down event\n");
2423 #endif
2424 // Clear cell we stored in mouse-down
2425 mUnselectCellOnMouseUp = nullptr;
2427 if (aTarget == TableSelectionMode::Cell) {
2428 bool isSelected = false;
2430 // Check if we have other selected cells
2431 nsIContent* previousCellNode =
2432 GetFirstSelectedContent(GetFirstCellRange(aNormalSelection));
2433 if (previousCellNode) {
2434 // We have at least 1 other selected cell
2436 // Check if new cell is already selected
2437 nsIFrame* cellFrame = aChildContent->GetPrimaryFrame();
2438 if (!cellFrame) {
2439 return NS_ERROR_NULL_POINTER;
2441 isSelected = cellFrame->IsSelected();
2442 } else {
2443 // No cells selected -- remove non-cell selection
2444 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2446 mDragSelectingCells = true; // Signal to start drag-cell-selection
2447 mMode = aTarget;
2448 // Set start for new drag-selection block (not appended)
2449 mStartSelectedCell = aChildContent;
2450 // The initial block end is same as the start
2451 mEndSelectedCell = aChildContent;
2453 if (isSelected) {
2454 // Remember this cell to (possibly) unselect it on mouseup
2455 mUnselectCellOnMouseUp = aChildContent;
2456 #ifdef DEBUG_TABLE_SELECTION
2457 printf(
2458 "HandleTableSelection: Saving "
2459 "mUnselectCellOnMouseUp\n");
2460 #endif
2461 } else {
2462 // Select an unselected cell
2463 // but first remove existing selection if not in same table
2464 if (previousCellNode &&
2465 !IsInSameTable(previousCellNode, aChildContent)) {
2466 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2467 // Reset selection mode that is cleared in RemoveAllRanges
2468 mMode = aTarget;
2471 return ::SelectCellElement(aChildContent, aNormalSelection);
2474 return NS_OK;
2476 if (aTarget == TableSelectionMode::Table) {
2477 // TODO: We currently select entire table when clicked between cells,
2478 // should we restrict to only around border?
2479 // *** How do we get location data for cell and click?
2480 mDragSelectingCells = false;
2481 mStartSelectedCell = nullptr;
2482 mEndSelectedCell = nullptr;
2484 // Remove existing selection and select the table
2485 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2486 return CreateAndAddRange(aParentContent, aContentOffset,
2487 aNormalSelection);
2489 if (aTarget == TableSelectionMode::Row ||
2490 aTarget == TableSelectionMode::Column) {
2491 #ifdef DEBUG_TABLE_SELECTION
2492 printf("aTarget == %d\n", aTarget);
2493 #endif
2495 // Start drag-selecting mode so multiple rows/cols can be selected
2496 // Note: Currently, nsIFrame::GetDataForTableSelection
2497 // will never call us for row or column selection on mouse down
2498 mDragSelectingCells = true;
2500 // Force new selection block
2501 mStartSelectedCell = nullptr;
2502 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2503 // Always do this AFTER RemoveAllRanges
2504 mMode = aTarget;
2506 return SelectRowOrColumn(aChildContent, aNormalSelection);
2508 } else {
2509 #ifdef DEBUG_TABLE_SELECTION
2510 printf(
2511 "HandleTableSelection: Mouse UP event. "
2512 "mDragSelectingCells=%d, "
2513 "mStartSelectedCell=%p\n",
2514 mDragSelectingCells, mStartSelectedCell.get());
2515 #endif
2516 // First check if we are extending a block selection
2517 const uint32_t rangeCount = aNormalSelection.RangeCount();
2519 if (rangeCount > 0 && aMouseEvent->IsShift() && mAppendStartSelectedCell &&
2520 mAppendStartSelectedCell != aChildContent) {
2521 // Shift key is down: append a block selection
2522 mDragSelectingCells = false;
2524 return SelectBlockOfCells(mAppendStartSelectedCell, aChildContent,
2525 aNormalSelection);
2528 if (mDragSelectingCells) {
2529 mAppendStartSelectedCell = mStartSelectedCell;
2532 mDragSelectingCells = false;
2533 mStartSelectedCell = nullptr;
2534 mEndSelectedCell = nullptr;
2536 // Any other mouseup actions require that Ctrl or Cmd key is pressed
2537 // else stop table selection mode
2538 bool doMouseUpAction = false;
2539 #ifdef XP_MACOSX
2540 doMouseUpAction = aMouseEvent->IsMeta();
2541 #else
2542 doMouseUpAction = aMouseEvent->IsControl();
2543 #endif
2544 if (!doMouseUpAction) {
2545 #ifdef DEBUG_TABLE_SELECTION
2546 printf(
2547 "HandleTableSelection: Ending cell selection on mouseup: "
2548 "mAppendStartSelectedCell=%p\n",
2549 mAppendStartSelectedCell.get());
2550 #endif
2551 return NS_OK;
2553 // Unselect a cell only if it wasn't
2554 // just selected on mousedown
2555 if (aChildContent == mUnselectCellOnMouseUp) {
2556 // Scan ranges to find the cell to unselect (the selection range to
2557 // remove)
2558 // XXXbz it's really weird that this lives outside the loop, so once we
2559 // find one we keep looking at it even if we find no more cells...
2560 nsINode* previousCellParent = nullptr;
2561 #ifdef DEBUG_TABLE_SELECTION
2562 printf(
2563 "HandleTableSelection: Unselecting "
2564 "mUnselectCellOnMouseUp; "
2565 "rangeCount=%d\n",
2566 rangeCount);
2567 #endif
2568 for (const uint32_t i : IntegerRange(rangeCount)) {
2569 MOZ_ASSERT(aNormalSelection.RangeCount() == rangeCount);
2570 // Strong reference, because sometimes we want to remove
2571 // this range, and then we might be the only owner.
2572 RefPtr<nsRange> range = aNormalSelection.GetRangeAt(i);
2573 if (MOZ_UNLIKELY(!range)) {
2574 return NS_ERROR_NULL_POINTER;
2577 nsINode* container = range->GetStartContainer();
2578 if (!container) {
2579 return NS_ERROR_NULL_POINTER;
2582 int32_t offset = range->StartOffset();
2583 // Be sure previous selection is a table cell
2584 nsIContent* child = range->GetChildAtStartOffset();
2585 if (child && IsCell(child)) {
2586 previousCellParent = container;
2589 // We're done if we didn't find parent of a previously-selected cell
2590 if (!previousCellParent) {
2591 break;
2594 if (previousCellParent == aParentContent && offset == aContentOffset) {
2595 // Cell is already selected
2596 if (rangeCount == 1) {
2597 #ifdef DEBUG_TABLE_SELECTION
2598 printf("HandleTableSelection: Unselecting single selected cell\n");
2599 #endif
2600 // This was the only cell selected.
2601 // Collapse to "normal" selection inside the cell
2602 mStartSelectedCell = nullptr;
2603 mEndSelectedCell = nullptr;
2604 mAppendStartSelectedCell = nullptr;
2605 // TODO: We need a "Collapse to just before deepest child" routine
2606 // Even better, should we collapse to just after the LAST deepest
2607 // child
2608 // (i.e., at the end of the cell's contents)?
2609 return aNormalSelection.CollapseInLimiter(aChildContent, 0);
2611 #ifdef DEBUG_TABLE_SELECTION
2612 printf(
2613 "HandleTableSelection: Removing cell from multi-cell "
2614 "selection\n");
2615 #endif
2616 // Unselecting the start of previous block
2617 // XXX What do we use now!
2618 if (aChildContent == mAppendStartSelectedCell) {
2619 mAppendStartSelectedCell = nullptr;
2622 // Deselect cell by removing its range from selection
2623 ErrorResult err;
2624 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2625 *range, err);
2626 return err.StealNSResult();
2629 mUnselectCellOnMouseUp = nullptr;
2632 return result;
2635 nsresult nsFrameSelection::TableSelection::SelectBlockOfCells(
2636 nsIContent* aStartCell, nsIContent* aEndCell, Selection& aNormalSelection) {
2637 NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER);
2638 NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER);
2639 mEndSelectedCell = aEndCell;
2641 nsresult result = NS_OK;
2643 // If new end cell is in a different table, do nothing
2644 const RefPtr<const nsIContent> table = IsInSameTable(aStartCell, aEndCell);
2645 if (!table) {
2646 return NS_OK;
2649 // Get starting and ending cells' location in the cellmap
2650 int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
2651 result = GetCellIndexes(aStartCell, startRowIndex, startColIndex);
2652 if (NS_FAILED(result)) return result;
2653 result = GetCellIndexes(aEndCell, endRowIndex, endColIndex);
2654 if (NS_FAILED(result)) return result;
2656 if (mDragSelectingCells) {
2657 // Drag selecting: remove selected cells outside of new block limits
2658 // TODO: `UnselectCells`'s return value shouldn't be ignored.
2659 UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex,
2660 true, aNormalSelection);
2663 // Note that we select block in the direction of user's mouse dragging,
2664 // which means start cell may be after the end cell in either row or column
2665 return AddCellsToSelection(table, startRowIndex, startColIndex, endRowIndex,
2666 endColIndex, aNormalSelection);
2669 nsresult nsFrameSelection::TableSelection::UnselectCells(
2670 const nsIContent* aTableContent, int32_t aStartRowIndex,
2671 int32_t aStartColumnIndex, int32_t aEndRowIndex, int32_t aEndColumnIndex,
2672 bool aRemoveOutsideOfCellRange, mozilla::dom::Selection& aNormalSelection) {
2673 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2675 nsTableWrapperFrame* tableFrame =
2676 do_QueryFrame(aTableContent->GetPrimaryFrame());
2677 if (!tableFrame) return NS_ERROR_FAILURE;
2679 int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex);
2680 int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex);
2681 int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex);
2682 int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex);
2684 // Strong reference because we sometimes remove the range
2685 RefPtr<nsRange> range = GetFirstCellRange(aNormalSelection);
2686 nsIContent* cellNode = GetFirstSelectedContent(range);
2687 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2689 int32_t curRowIndex, curColIndex;
2690 while (cellNode) {
2691 nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex);
2692 if (NS_FAILED(result)) return result;
2694 #ifdef DEBUG_TABLE_SELECTION
2695 if (!range) printf("RemoveCellsToSelection -- range is null\n");
2696 #endif
2698 if (range) {
2699 if (aRemoveOutsideOfCellRange) {
2700 if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex ||
2701 curColIndex < minColIndex || curColIndex > maxColIndex) {
2702 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2703 *range, IgnoreErrors());
2704 // Since we've removed the range, decrement pointer to next range
2705 mSelectedCellIndex--;
2708 } else {
2709 // Remove cell from selection if it belongs to the given cells range or
2710 // it is spanned onto the cells range.
2711 nsTableCellFrame* cellFrame =
2712 tableFrame->GetCellFrameAt(curRowIndex, curColIndex);
2714 uint32_t origRowIndex = cellFrame->RowIndex();
2715 uint32_t origColIndex = cellFrame->ColIndex();
2716 uint32_t actualRowSpan =
2717 tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex);
2718 uint32_t actualColSpan =
2719 tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex);
2720 if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) &&
2721 maxRowIndex >= 0 &&
2722 origRowIndex + actualRowSpan - 1 >=
2723 static_cast<uint32_t>(minRowIndex) &&
2724 origColIndex <= static_cast<uint32_t>(maxColIndex) &&
2725 maxColIndex >= 0 &&
2726 origColIndex + actualColSpan - 1 >=
2727 static_cast<uint32_t>(minColIndex)) {
2728 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2729 *range, IgnoreErrors());
2730 // Since we've removed the range, decrement pointer to next range
2731 mSelectedCellIndex--;
2736 range = GetNextCellRange(aNormalSelection);
2737 cellNode = GetFirstSelectedContent(range);
2738 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2741 return NS_OK;
2744 nsresult SelectCellElement(nsIContent* aCellElement,
2745 Selection& aNormalSelection) {
2746 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2748 nsIContent* parent = aCellElement->GetParent();
2750 // Get child offset
2751 const int32_t offset = parent->ComputeIndexOf_Deprecated(aCellElement);
2753 return CreateAndAddRange(parent, offset, aNormalSelection);
2756 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
2757 int32_t aStartRowIndex,
2758 int32_t aStartColumnIndex,
2759 int32_t aEndRowIndex,
2760 int32_t aEndColumnIndex,
2761 Selection& aNormalSelection) {
2762 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2764 nsTableWrapperFrame* tableFrame =
2765 do_QueryFrame(aTableContent->GetPrimaryFrame());
2766 if (!tableFrame) { // Check that |table| is a table.
2767 return NS_ERROR_FAILURE;
2770 nsresult result = NS_OK;
2771 uint32_t row = aStartRowIndex;
2772 while (true) {
2773 uint32_t col = aStartColumnIndex;
2774 while (true) {
2775 nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col);
2777 // Skip cells that are spanned from previous locations or are already
2778 // selected
2779 if (cellFrame) {
2780 uint32_t origRow = cellFrame->RowIndex();
2781 uint32_t origCol = cellFrame->ColIndex();
2782 if (origRow == row && origCol == col && !cellFrame->IsSelected()) {
2783 result = SelectCellElement(cellFrame->GetContent(), aNormalSelection);
2784 if (NS_FAILED(result)) {
2785 return result;
2789 // Done when we reach end column
2790 if (col == static_cast<uint32_t>(aEndColumnIndex)) {
2791 break;
2794 if (aStartColumnIndex < aEndColumnIndex) {
2795 col++;
2796 } else {
2797 col--;
2800 if (row == static_cast<uint32_t>(aEndRowIndex)) {
2801 break;
2804 if (aStartRowIndex < aEndRowIndex) {
2805 row++;
2806 } else {
2807 row--;
2810 return result;
2813 nsresult nsFrameSelection::RemoveCellsFromSelection(nsIContent* aTable,
2814 int32_t aStartRowIndex,
2815 int32_t aStartColumnIndex,
2816 int32_t aEndRowIndex,
2817 int32_t aEndColumnIndex) {
2818 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2819 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2820 if (!selection) {
2821 return NS_ERROR_NULL_POINTER;
2824 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2825 aStartColumnIndex, aEndRowIndex,
2826 aEndColumnIndex, false, *selection);
2829 nsresult nsFrameSelection::RestrictCellsToSelection(nsIContent* aTable,
2830 int32_t aStartRowIndex,
2831 int32_t aStartColumnIndex,
2832 int32_t aEndRowIndex,
2833 int32_t aEndColumnIndex) {
2834 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2835 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2836 if (!selection) {
2837 return NS_ERROR_NULL_POINTER;
2840 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2841 aStartColumnIndex, aEndRowIndex,
2842 aEndColumnIndex, true, *selection);
2845 Result<nsFrameSelection::TableSelection::FirstAndLastCell, nsresult>
2846 nsFrameSelection::TableSelection::FindFirstAndLastCellOfRowOrColumn(
2847 const nsIContent& aCellContent) const {
2848 const nsIContent* table = GetParentTable(&aCellContent);
2849 if (!table) {
2850 return Err(NS_ERROR_NULL_POINTER);
2853 // Get table and cell layout interfaces to access
2854 // cell data based on cellmap location
2855 // Frames are not ref counted, so don't use an nsCOMPtr
2856 nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame());
2857 if (!tableFrame) {
2858 return Err(NS_ERROR_FAILURE);
2860 nsITableCellLayout* cellLayout = GetCellLayout(&aCellContent);
2861 if (!cellLayout) {
2862 return Err(NS_ERROR_FAILURE);
2865 // Get location of target cell:
2866 int32_t rowIndex, colIndex;
2867 nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex);
2868 if (NS_FAILED(result)) {
2869 return Err(result);
2872 // Be sure we start at proper beginning
2873 // (This allows us to select row or col given ANY cell!)
2874 if (mMode == TableSelectionMode::Row) {
2875 colIndex = 0;
2877 if (mMode == TableSelectionMode::Column) {
2878 rowIndex = 0;
2881 FirstAndLastCell firstAndLastCell;
2882 while (true) {
2883 // Loop through all cells in column or row to find first and last
2884 nsCOMPtr<nsIContent> curCellContent =
2885 tableFrame->GetCellAt(rowIndex, colIndex);
2886 if (!curCellContent) {
2887 break;
2890 if (!firstAndLastCell.mFirst) {
2891 firstAndLastCell.mFirst = curCellContent;
2894 firstAndLastCell.mLast = std::move(curCellContent);
2896 // Move to next cell in cellmap, skipping spanned locations
2897 if (mMode == TableSelectionMode::Row) {
2898 colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
2899 } else {
2900 rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
2903 return firstAndLastCell;
2906 nsresult nsFrameSelection::TableSelection::SelectRowOrColumn(
2907 nsIContent* aCellContent, Selection& aNormalSelection) {
2908 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2910 if (!aCellContent) {
2911 return NS_ERROR_NULL_POINTER;
2914 Result<FirstAndLastCell, nsresult> firstAndLastCell =
2915 FindFirstAndLastCellOfRowOrColumn(*aCellContent);
2916 if (firstAndLastCell.isErr()) {
2917 return firstAndLastCell.unwrapErr();
2920 // Use SelectBlockOfCells:
2921 // This will replace existing selection,
2922 // but allow unselecting by dragging out of selected region
2923 if (firstAndLastCell.inspect().mFirst && firstAndLastCell.inspect().mLast) {
2924 nsresult rv{NS_OK};
2926 if (!mStartSelectedCell) {
2927 // We are starting a new block, so select the first cell
2928 rv = ::SelectCellElement(firstAndLastCell.inspect().mFirst,
2929 aNormalSelection);
2930 if (NS_FAILED(rv)) {
2931 return rv;
2933 mStartSelectedCell = firstAndLastCell.inspect().mFirst;
2936 rv = SelectBlockOfCells(mStartSelectedCell,
2937 firstAndLastCell.inspect().mLast, aNormalSelection);
2939 // This gets set to the cell at end of row/col,
2940 // but we need it to be the cell under cursor
2941 mEndSelectedCell = aCellContent;
2942 return rv;
2945 #if 0
2946 // This is a more efficient strategy that appends row to current selection,
2947 // but doesn't allow dragging OFF of an existing selection to unselect!
2948 do {
2949 // Loop through all cells in column or row
2950 result = tableLayout->GetCellDataAt(rowIndex, colIndex,
2951 getter_AddRefs(cellElement),
2952 curRowIndex, curColIndex,
2953 rowSpan, colSpan,
2954 actualRowSpan, actualColSpan,
2955 isSelected);
2956 if (NS_FAILED(result)) return result;
2957 // We're done when cell is not found
2958 if (!cellElement) break;
2961 // Check spans else we infinitely loop
2962 NS_ASSERTION(actualColSpan, "actualColSpan is 0!");
2963 NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!");
2965 // Skip cells that are already selected or span from outside our region
2966 if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex)
2968 result = SelectCellElement(cellElement);
2969 if (NS_FAILED(result)) return result;
2971 // Move to next row or column in cellmap, skipping spanned locations
2972 if (mMode == TableSelectionMode::Row)
2973 colIndex += actualColSpan;
2974 else
2975 rowIndex += actualRowSpan;
2977 while (cellElement);
2978 #endif
2980 return NS_OK;
2983 // static
2984 nsIContent* nsFrameSelection::GetFirstCellNodeInRange(const nsRange* aRange) {
2985 if (!aRange) return nullptr;
2987 nsIContent* childContent = aRange->GetChildAtStartOffset();
2988 if (!childContent) return nullptr;
2989 // Don't return node if not a cell
2990 if (!IsCell(childContent)) return nullptr;
2992 return childContent;
2995 nsRange* nsFrameSelection::TableSelection::GetFirstCellRange(
2996 const mozilla::dom::Selection& aNormalSelection) {
2997 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2999 nsRange* firstRange = aNormalSelection.GetRangeAt(0);
3000 if (!GetFirstCellNodeInRange(firstRange)) {
3001 return nullptr;
3004 // Setup for next cell
3005 mSelectedCellIndex = 1;
3007 return firstRange;
3010 nsRange* nsFrameSelection::TableSelection::GetNextCellRange(
3011 const mozilla::dom::Selection& aNormalSelection) {
3012 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
3014 nsRange* range =
3015 aNormalSelection.GetRangeAt(AssertedCast<uint32_t>(mSelectedCellIndex));
3017 // Get first node in next range of selection - test if it's a cell
3018 if (!GetFirstCellNodeInRange(range)) {
3019 return nullptr;
3022 // Setup for next cell
3023 mSelectedCellIndex++;
3025 return range;
3028 // static
3029 nsresult nsFrameSelection::GetCellIndexes(const nsIContent* aCell,
3030 int32_t& aRowIndex,
3031 int32_t& aColIndex) {
3032 if (!aCell) return NS_ERROR_NULL_POINTER;
3034 aColIndex = 0; // initialize out params
3035 aRowIndex = 0;
3037 nsITableCellLayout* cellLayoutObject = GetCellLayout(aCell);
3038 if (!cellLayoutObject) return NS_ERROR_FAILURE;
3039 return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex);
3042 // static
3043 nsIContent* nsFrameSelection::IsInSameTable(const nsIContent* aContent1,
3044 const nsIContent* aContent2) {
3045 if (!aContent1 || !aContent2) return nullptr;
3047 nsIContent* tableNode1 = GetParentTable(aContent1);
3048 nsIContent* tableNode2 = GetParentTable(aContent2);
3050 // Must be in the same table. Note that we want to return false for
3051 // the test if both tables are null.
3052 return (tableNode1 == tableNode2) ? tableNode1 : nullptr;
3055 // static
3056 nsIContent* nsFrameSelection::GetParentTable(const nsIContent* aCell) {
3057 if (!aCell) {
3058 return nullptr;
3061 for (nsIContent* parent = aCell->GetParent(); parent;
3062 parent = parent->GetParent()) {
3063 if (parent->IsHTMLElement(nsGkAtoms::table)) {
3064 return parent;
3068 return nullptr;
3071 nsresult nsFrameSelection::SelectCellElement(nsIContent* aCellElement) {
3072 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3073 const RefPtr<Selection> selection = mDomSelections[index];
3074 if (!selection) {
3075 return NS_ERROR_NULL_POINTER;
3078 return ::SelectCellElement(aCellElement, *selection);
3081 nsresult CreateAndAddRange(nsINode* aContainer, int32_t aOffset,
3082 Selection& aNormalSelection) {
3083 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
3085 if (!aContainer) {
3086 return NS_ERROR_NULL_POINTER;
3089 // Set range around child at given offset
3090 ErrorResult error;
3091 RefPtr<nsRange> range =
3092 nsRange::Create(aContainer, aOffset, aContainer, aOffset + 1, error);
3093 if (NS_WARN_IF(error.Failed())) {
3094 return error.StealNSResult();
3096 MOZ_ASSERT(range);
3098 ErrorResult err;
3099 aNormalSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, err);
3100 return err.StealNSResult();
3103 // End of Table Selection
3105 void nsFrameSelection::SetAncestorLimiter(nsIContent* aLimiter) {
3106 if (mLimiters.mAncestorLimiter != aLimiter) {
3107 mLimiters.mAncestorLimiter = aLimiter;
3108 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3109 if (!mDomSelections[index]) return;
3111 if (!IsValidSelectionPoint(mDomSelections[index]->GetFocusNode())) {
3112 ClearNormalSelection();
3113 if (mLimiters.mAncestorLimiter) {
3114 SetChangeReasons(nsISelectionListener::NO_REASON);
3115 nsCOMPtr<nsIContent> limiter(mLimiters.mAncestorLimiter);
3116 const nsresult rv = TakeFocus(*limiter, 0, 0, CARET_ASSOCIATE_BEFORE,
3117 FocusMode::kCollapseToNewPoint);
3118 Unused << NS_WARN_IF(NS_FAILED(rv));
3119 // TODO: in case of failure, propagate it to the callers.
3125 void nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) {
3126 if (aMouseEvent) {
3127 mDelayedMouseEvent.mIsValid = true;
3128 mDelayedMouseEvent.mIsShift = aMouseEvent->IsShift();
3129 mDelayedMouseEvent.mClickCount = aMouseEvent->mClickCount;
3130 } else {
3131 mDelayedMouseEvent.mIsValid = false;
3135 void nsFrameSelection::DisconnectFromPresShell() {
3136 if (mAccessibleCaretEnabled) {
3137 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3138 mDomSelections[index]->StopNotifyingAccessibleCaretEventHub();
3141 StopAutoScrollTimer();
3142 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
3143 mDomSelections[i]->Clear(nullptr);
3145 mPresShell = nullptr;
3148 #ifdef XP_MACOSX
3150 * See Bug 1288453.
3152 * Update the selection cache on repaint to handle when a pre-existing
3153 * selection becomes active aka the current selection.
3155 * 1. Change the current selection by click n dragging another selection.
3156 * - Make a selection on content page. Make a selection in a text editor.
3157 * - You can click n drag the content selection to make it active again.
3158 * 2. Change the current selection when switching to a tab with a selection.
3159 * - Make selection in tab.
3160 * - Switching tabs will make its respective selection active.
3162 * Therefore, we only update the selection cache on a repaint
3163 * if the current selection being repainted is not an empty selection.
3165 * If the current selection is empty. The current selection cache
3166 * would be cleared by AutoCopyListener::OnSelectionChange().
3168 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel) {
3169 PresShell* presShell = aSel->GetPresShell();
3170 if (!presShell) {
3171 return NS_OK;
3173 nsCOMPtr<Document> aDoc = presShell->GetDocument();
3175 if (aDoc && aSel && !aSel->IsCollapsed()) {
3176 return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3177 aSel, aDoc, nsIClipboard::kSelectionCache, false);
3180 return NS_OK;
3182 #endif // XP_MACOSX
3184 // mozilla::AutoCopyListener
3186 int16_t AutoCopyListener::sClipboardID = -1;
3189 * What we do now:
3190 * On every selection change, we copy to the clipboard anew, creating a
3191 * HTML buffer, a transferable, an nsISupportsString and
3192 * a huge mess every time. This is basically what
3193 * nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the
3194 * selection into the clipboard for Edit->Copy.
3196 * What we should do, to make our end of the deal faster:
3197 * Create a singleton transferable with our own magic converter. When selection
3198 * changes (use a quick cache to detect ``real'' changes), we put the new
3199 * Selection in the transferable. Our magic converter will take care of
3200 * transferable->whatever-other-format when the time comes to actually
3201 * hand over the clipboard contents.
3203 * Other issues:
3204 * - which X clipboard should we populate?
3205 * - should we use a different one than Edit->Copy, so that inadvertant
3206 * selections (or simple clicks, which currently cause a selection
3207 * notification, regardless of if they're in the document which currently has
3208 * selection!) don't lose the contents of the ``application''? Or should we
3209 * just put some intelligence in the ``is this a real selection?'' code to
3210 * protect our selection against clicks in other documents that don't create
3211 * selections?
3212 * - maybe we should just never clear the X clipboard? That would make this
3213 * problem just go away, which is very tempting.
3215 * On macOS,
3216 * nsIClipboard::kSelectionCache is the flag for current selection cache.
3217 * Set the current selection cache on the parent process in
3218 * widget cocoa nsClipboard whenever selection changes.
3221 // static
3222 void AutoCopyListener::OnSelectionChange(Document* aDocument,
3223 Selection& aSelection,
3224 int16_t aReason) {
3225 MOZ_ASSERT(IsValidClipboardID(sClipboardID));
3227 if (sClipboardID == nsIClipboard::kSelectionCache) {
3228 // Do nothing if this isn't in the active window and,
3229 // in the case of Web content, in the frontmost tab.
3230 if (!aDocument || !IsInActiveTab(aDocument)) {
3231 return;
3235 static const int16_t kResasonsToHandle =
3236 nsISelectionListener::MOUSEUP_REASON |
3237 nsISelectionListener::SELECTALL_REASON |
3238 nsISelectionListener::KEYPRESS_REASON;
3239 if (!(aReason & kResasonsToHandle)) {
3240 return; // Don't care if we are still dragging.
3243 if (!aDocument || aSelection.IsCollapsed()) {
3244 #ifdef DEBUG_CLIPBOARD
3245 fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n");
3246 #endif
3247 if (sClipboardID != nsIClipboard::kSelectionCache) {
3248 // XXX Should we clear X clipboard?
3249 return;
3252 // If on macOS, clear the current selection transferable cached
3253 // on the parent process (nsClipboard) when the selection is empty.
3254 DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache();
3255 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3256 "nsCopySupport::ClearSelectionCache() failed");
3257 return;
3260 DebugOnly<nsresult> rv =
3261 nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3262 &aSelection, aDocument, sClipboardID, false);
3263 NS_WARNING_ASSERTION(
3264 NS_SUCCEEDED(rv),
3265 "nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed");