Bug 1874684 - Part 20: Tag stack classes with MOZ_STACK_CLASS. r=allstarschh
[gecko.git] / layout / generic / nsFrameSelection.cpp
blobca4e3d647e9c38f4e370c2d3fc050fa62bd2fa1a
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 "ErrorList.h"
14 #include "mozilla/intl/BidiEmbeddingLevel.h"
15 #include "mozilla/Attributes.h"
16 #include "mozilla/AutoRestore.h"
17 #include "mozilla/BasePrincipal.h"
18 #include "mozilla/HTMLEditor.h"
19 #include "mozilla/IntegerRange.h"
20 #include "mozilla/Logging.h"
21 #include "mozilla/PresShell.h"
22 #include "mozilla/ScrollTypes.h"
23 #include "mozilla/StaticAnalysisFunctions.h"
24 #include "mozilla/StaticPrefs_bidi.h"
25 #include "mozilla/StaticPrefs_dom.h"
26 #include "mozilla/StaticPrefs_layout.h"
27 #include "mozilla/Unused.h"
29 #include "nsCOMPtr.h"
30 #include "nsDebug.h"
31 #include "nsFrameTraversal.h"
32 #include "nsString.h"
33 #include "nsISelectionListener.h"
34 #include "nsDeviceContext.h"
35 #include "nsIContent.h"
36 #include "nsRange.h"
37 #include "nsITableCellLayout.h"
38 #include "nsTArray.h"
39 #include "nsTableWrapperFrame.h"
40 #include "nsTableCellFrame.h"
41 #include "nsIScrollableFrame.h"
42 #include "nsCCUncollectableMarker.h"
43 #include "nsTextFragment.h"
44 #include <algorithm>
45 #include "nsContentUtils.h"
46 #include "nsCSSFrameConstructor.h"
48 #include "nsGkAtoms.h"
49 #include "nsLayoutUtils.h"
50 #include "nsBidiPresUtils.h"
51 #include "nsTextFrame.h"
53 #include "nsThreadUtils.h"
54 #include "mozilla/Preferences.h"
56 #include "mozilla/PresShell.h"
57 #include "nsPresContext.h"
58 #include "nsCaret.h"
60 #include "mozilla/MouseEvents.h"
61 #include "mozilla/TextEvents.h"
63 // notifications
64 #include "mozilla/dom/Document.h"
66 #include "nsISelectionController.h" //for the enums
67 #include "nsCopySupport.h"
68 #include "nsIClipboard.h"
69 #include "nsIFrameInlines.h"
71 #include "nsError.h"
72 #include "mozilla/AutoCopyListener.h"
73 #include "mozilla/dom/Element.h"
74 #include "mozilla/dom/Highlight.h"
75 #include "mozilla/dom/Selection.h"
76 #include "mozilla/dom/ShadowRoot.h"
77 #include "mozilla/dom/StaticRange.h"
78 #include "mozilla/dom/Text.h"
79 #include "mozilla/ErrorResult.h"
80 #include "mozilla/dom/SelectionBinding.h"
81 #include "mozilla/AsyncEventDispatcher.h"
82 #include "mozilla/Telemetry.h"
84 #include "nsFocusManager.h"
85 #include "nsPIDOMWindow.h"
87 #include "SelectionMovementUtils.h"
89 using namespace mozilla;
90 using namespace mozilla::dom;
92 static LazyLogModule sFrameSelectionLog("FrameSelection");
94 // #define DEBUG_TABLE 1
96 /**
97 * Add cells to the selection inside of the given cells range.
99 * @param aTable [in] HTML table element
100 * @param aStartRowIndex [in] row index where the cells range starts
101 * @param aStartColumnIndex [in] column index where the cells range starts
102 * @param aEndRowIndex [in] row index where the cells range ends
103 * @param aEndColumnIndex [in] column index where the cells range ends
105 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
106 int32_t aStartRowIndex,
107 int32_t aStartColumnIndex,
108 int32_t aEndRowIndex,
109 int32_t aEndColumnIndex,
110 Selection& aNormalSelection);
112 static nsAtom* GetTag(nsINode* aNode);
114 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode);
115 MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult CreateAndAddRange(
116 nsINode* aContainer, int32_t aOffset, Selection& aNormalSelection);
117 static nsresult SelectCellElement(nsIContent* aCellElement,
118 Selection& aNormalSelection);
120 #ifdef XP_MACOSX
121 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel);
122 #endif // XP_MACOSX
124 #ifdef PRINT_RANGE
125 static void printRange(nsRange* aDomRange);
126 # define DEBUG_OUT_RANGE(x) printRange(x)
127 #else
128 # define DEBUG_OUT_RANGE(x)
129 #endif // PRINT_RANGE
131 /******************************************************************************
132 * mozilla::PeekOffsetStruct
133 ******************************************************************************/
135 // #define DEBUG_SELECTION // uncomment for printf describing every collapse and
136 // extend. #define DEBUG_NAVIGATION
138 // #define DEBUG_TABLE_SELECTION 1
140 namespace mozilla {
142 PeekOffsetStruct::PeekOffsetStruct(nsSelectionAmount aAmount,
143 nsDirection aDirection, int32_t aStartOffset,
144 nsPoint aDesiredCaretPos,
145 const PeekOffsetOptions aOptions,
146 EWordMovementType aWordMovementType)
147 : mAmount(aAmount),
148 mDirection(aDirection),
149 mStartOffset(aStartOffset),
150 mDesiredCaretPos(aDesiredCaretPos),
151 mWordMovementType(aWordMovementType),
152 mOptions(aOptions),
153 mResultFrame(nullptr),
154 mContentOffset(0),
155 mAttach(CaretAssociationHint::Before) {}
157 } // namespace mozilla
159 // Array which contains index of each SelectionType in
160 // Selection::mDOMSelections. For avoiding using if nor switch to retrieve the
161 // index, this needs to have -1 for SelectionTypes which won't be created its
162 // Selection instance.
163 static const int8_t kIndexOfSelections[] = {
164 -1, // SelectionType::eInvalid
165 -1, // SelectionType::eNone
166 0, // SelectionType::eNormal
167 1, // SelectionType::eSpellCheck
168 2, // SelectionType::eIMERawClause
169 3, // SelectionType::eIMESelectedRawClause
170 4, // SelectionType::eIMEConvertedClause
171 5, // SelectionType::eIMESelectedClause
172 6, // SelectionType::eAccessibility
173 7, // SelectionType::eFind
174 8, // SelectionType::eURLSecondary
175 9, // SelectionType::eURLStrikeout
176 10, // SelectionType::eTargetText
177 -1, // SelectionType::eHighlight
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 MOZ_ASSERT(entry.mRange->IsDynamicRange());
237 entry.mRange->AsDynamicRange()->SetIsGenerated(false);
239 return;
242 if (!IsAnchorRelativeOperation(
243 aSelection->mFrameSelection->mSelectionChangeReasons)) {
244 return;
247 // This operation is against the anchor but our current mAnchorFocusRange
248 // represents the focus in a multi-range selection. The anchor from a user
249 // perspective is the most distant generated range on the opposite side.
250 // Find that range and make it the mAnchorFocusRange.
251 nsRange* const newAnchorFocusRange =
252 FindGeneratedRangeMostDistantFromAnchor(*aSelection);
254 if (!newAnchorFocusRange) {
255 // There are no generated ranges - that's fine.
256 return;
259 // Setup the new mAnchorFocusRange and mark the old one as generated.
260 if (aSelection->mAnchorFocusRange) {
261 aSelection->mAnchorFocusRange->SetIsGenerated(true);
264 newAnchorFocusRange->SetIsGenerated(false);
265 aSelection->mAnchorFocusRange = newAnchorFocusRange;
267 RemoveGeneratedRanges(*aSelection);
269 if (aSelection->mFrameSelection) {
270 aSelection->mFrameSelection->InvalidateDesiredCaretPos();
274 private:
275 static nsRange* FindGeneratedRangeMostDistantFromAnchor(
276 const Selection& aSelection) {
277 const nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
278 const size_t len = ranges.Length();
279 nsRange* result{nullptr};
280 if (aSelection.GetDirection() == eDirNext) {
281 for (size_t i = 0; i < len; ++i) {
282 // This function is only called for selections with type == eNormal.
283 // (see MOZ_ASSERT in constructor).
284 // Therefore, all ranges must be dynamic.
285 if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
286 result = ranges[i].mRange->AsDynamicRange();
287 break;
290 } else {
291 size_t i = len;
292 while (i--) {
293 if (ranges[i].mRange->AsDynamicRange()->IsGenerated()) {
294 result = ranges[i].mRange->AsDynamicRange();
295 break;
300 return result;
303 static void RemoveGeneratedRanges(Selection& aSelection) {
304 RefPtr<nsPresContext> presContext = aSelection.GetPresContext();
305 nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
306 size_t i = ranges.Length();
307 while (i--) {
308 // This function is only called for selections with type == eNormal.
309 // (see MOZ_ASSERT in constructor).
310 // Therefore, all ranges must be dynamic.
311 if (!ranges[i].mRange->IsDynamicRange()) {
312 continue;
314 nsRange* range = ranges[i].mRange->AsDynamicRange();
315 if (range->IsGenerated()) {
316 range->UnregisterSelection(aSelection);
317 aSelection.SelectFrames(presContext, *range, false);
318 ranges.RemoveElementAt(i);
324 * @aParam aSelectionChangeReasons can be multiple of the reasons defined in
325 nsISelectionListener.idl.
327 static bool IsAnchorRelativeOperation(const int16_t aSelectionChangeReasons) {
328 return aSelectionChangeReasons &
329 (nsISelectionListener::DRAG_REASON |
330 nsISelectionListener::MOUSEDOWN_REASON |
331 nsISelectionListener::MOUSEUP_REASON |
332 nsISelectionListener::COLLAPSETOSTART_REASON);
335 Maybe<Selection::AutoUserInitiated> mUserSelect;
338 } // namespace mozilla
340 ////////////BEGIN nsFrameSelection methods
342 template Result<RefPtr<nsRange>, nsresult>
343 nsFrameSelection::CreateRangeExtendedToSomewhere(
344 nsDirection aDirection, const nsSelectionAmount aAmount,
345 CaretMovementStyle aMovementStyle);
346 template Result<RefPtr<StaticRange>, nsresult>
347 nsFrameSelection::CreateRangeExtendedToSomewhere(
348 nsDirection aDirection, const nsSelectionAmount aAmount,
349 CaretMovementStyle aMovementStyle);
351 nsFrameSelection::nsFrameSelection(PresShell* aPresShell, nsIContent* aLimiter,
352 const bool aAccessibleCaretEnabled) {
353 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
354 mDomSelections[i] = new Selection(kPresentSelectionTypes[i], this);
357 #ifdef XP_MACOSX
358 // On macOS, cache the current selection to send to service menu of macOS.
359 bool enableAutoCopy = true;
360 AutoCopyListener::Init(nsIClipboard::kSelectionCache);
361 #else // #ifdef XP_MACOSX
362 // Check to see if the auto-copy pref is enabled and make the normal
363 // Selection notifies auto-copy listener of its changes.
364 bool enableAutoCopy = AutoCopyListener::IsPrefEnabled();
365 if (enableAutoCopy) {
366 AutoCopyListener::Init(nsIClipboard::kSelectionClipboard);
368 #endif // #ifdef XP_MACOSX #else
370 if (enableAutoCopy) {
371 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
372 if (mDomSelections[index]) {
373 mDomSelections[index]->NotifyAutoCopy();
377 mPresShell = aPresShell;
378 mDragState = false;
379 mLimiters.mLimiter = aLimiter;
381 // This should only ever be initialized on the main thread, so we are OK here.
382 MOZ_ASSERT(NS_IsMainThread());
384 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
386 mAccessibleCaretEnabled = aAccessibleCaretEnabled;
387 if (mAccessibleCaretEnabled) {
388 mDomSelections[index]->MaybeNotifyAccessibleCaretEventHub(aPresShell);
391 if (mDomSelections[index]) {
392 mDomSelections[index]->EnableSelectionChangeEvent();
396 nsFrameSelection::~nsFrameSelection() = default;
398 NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection)
400 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection)
401 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
402 tmp->mDomSelections[i] = nullptr;
404 tmp->mHighlightSelections.Clear();
406 NS_IMPL_CYCLE_COLLECTION_UNLINK(
407 mTableSelection.mClosestInclusiveTableCellAncestor)
408 tmp->mTableSelection.mMode = TableSelectionMode::None;
409 tmp->mTableSelection.mDragSelectingCells = false;
410 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mStartSelectedCell)
411 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mEndSelectedCell)
412 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mAppendStartSelectedCell)
413 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mUnselectCellOnMouseUp)
414 NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainedRange.mRange)
415 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mLimiter)
416 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mAncestorLimiter)
417 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
418 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection)
419 if (tmp->mPresShell && tmp->mPresShell->GetDocument() &&
420 nsCCUncollectableMarker::InGeneration(
421 cb, tmp->mPresShell->GetDocument()->GetMarkedCCGeneration())) {
422 return NS_SUCCESS_INTERRUPTED_TRAVERSE;
424 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
425 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i])
428 for (const auto& value : tmp->mHighlightSelections) {
429 CycleCollectionNoteChild(cb, value.second().get(),
430 "mHighlightSelections[]");
433 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
434 mTableSelection.mClosestInclusiveTableCellAncestor)
435 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mStartSelectedCell)
436 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mEndSelectedCell)
437 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mAppendStartSelectedCell)
438 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mUnselectCellOnMouseUp)
439 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainedRange.mRange)
440 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mLimiter)
441 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mAncestorLimiter)
442 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
444 bool nsFrameSelection::Caret::IsVisualMovement(
445 bool aContinueSelection, CaretMovementStyle aMovementStyle) const {
446 int32_t movementFlag = StaticPrefs::bidi_edit_caret_movement_style();
447 return aMovementStyle == eVisual ||
448 (aMovementStyle == eUsePrefStyle &&
449 (movementFlag == 1 || (movementFlag == 2 && !aContinueSelection)));
452 // Get the x (or y, in vertical writing mode) position requested
453 // by the Key Handling for line-up/down
454 nsresult nsFrameSelection::DesiredCaretPos::FetchPos(
455 nsPoint& aDesiredCaretPos, const PresShell& aPresShell,
456 Selection& aNormalSelection) const {
457 MOZ_ASSERT(aNormalSelection.GetType() == SelectionType::eNormal);
459 if (mIsSet) {
460 aDesiredCaretPos = mValue;
461 return NS_OK;
464 RefPtr<nsCaret> caret = aPresShell.GetCaret();
465 if (!caret) {
466 return NS_ERROR_NULL_POINTER;
469 caret->SetSelection(&aNormalSelection);
471 nsRect coord;
472 nsIFrame* caretFrame = caret->GetGeometry(&coord);
473 if (!caretFrame) {
474 return NS_ERROR_FAILURE;
476 nsPoint viewOffset(0, 0);
477 nsView* view = nullptr;
478 caretFrame->GetOffsetFromView(viewOffset, &view);
479 if (view) {
480 coord += viewOffset;
482 aDesiredCaretPos = coord.TopLeft();
483 return NS_OK;
486 void nsFrameSelection::InvalidateDesiredCaretPos() // do not listen to
487 // mDesiredCaretPos.mValue;
488 // you must get another.
490 mDesiredCaretPos.Invalidate();
493 void nsFrameSelection::DesiredCaretPos::Invalidate() { mIsSet = false; }
495 void nsFrameSelection::DesiredCaretPos::Set(const nsPoint& aPos) {
496 mValue = aPos;
497 mIsSet = true;
500 nsresult nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree(
501 nsIFrame* aFrame, const nsPoint& aPoint, nsIFrame** aRetFrame,
502 nsPoint& aRetPoint) const {
504 // The whole point of this method is to return a frame and point that
505 // that lie within the same valid subtree as the anchor node's frame,
506 // for use with the method GetContentAndOffsetsFromPoint().
508 // A valid subtree is defined to be one where all the content nodes in
509 // the tree have a valid parent-child relationship.
511 // If the anchor frame and aFrame are in the same subtree, aFrame will
512 // be returned in aRetFrame. If they are in different subtrees, we
513 // return the frame for the root of the subtree.
516 if (!aFrame || !aRetFrame) {
517 return NS_ERROR_NULL_POINTER;
520 *aRetFrame = aFrame;
521 aRetPoint = aPoint;
524 // Get the frame and content for the selection's anchor point!
527 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
528 if (!mDomSelections[index]) {
529 return NS_ERROR_NULL_POINTER;
532 nsCOMPtr<nsIContent> anchorContent =
533 do_QueryInterface(mDomSelections[index]->GetAnchorNode());
534 if (!anchorContent) {
535 return NS_ERROR_FAILURE;
539 // Now find the root of the subtree containing the anchor's content.
542 NS_ENSURE_STATE(mPresShell);
543 RefPtr<PresShell> presShell = mPresShell;
544 nsIContent* anchorRoot =
545 anchorContent
546 ->GetSelectionRootContent(
547 presShell,
548 StaticPrefs::dom_shadowdom_selection_across_boundary_enabled() /* aAllowCrossShadowBoundary */);
549 NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED);
552 // Now find the root of the subtree containing aFrame's content.
555 nsCOMPtr<nsIContent> content = aFrame->GetContent();
557 if (content) {
558 nsIContent* contentRoot =
559 content->GetSelectionRootContent(
560 presShell, StaticPrefs::
561 dom_shadowdom_selection_across_boundary_enabled() /* aAllowCrossShadowBoundary */);
562 NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED);
564 if (anchorRoot == contentRoot) {
565 // If the aFrame's content isn't the capturing content, it should be
566 // a descendant. At this time, we can return simply.
567 nsIContent* capturedContent = PresShell::GetCapturingContent();
568 if (capturedContent != content) {
569 return NS_OK;
572 // Find the frame under the mouse cursor with the root frame.
573 // At this time, don't use the anchor's frame because it may not have
574 // fixed positioned frames.
575 nsIFrame* rootFrame = presShell->GetRootFrame();
576 nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame);
577 nsIFrame* cursorFrame =
578 nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot);
580 // If the mouse cursor in on a frame which is descendant of same
581 // selection root, we can expand the selection to the frame.
582 if (cursorFrame && cursorFrame->PresShell() == presShell) {
583 nsCOMPtr<nsIContent> cursorContent = cursorFrame->GetContent();
584 NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE);
585 nsIContent* cursorContentRoot = cursorContent->GetSelectionRootContent(
586 presShell, StaticPrefs::
587 dom_shadowdom_selection_across_boundary_enabled() /* aAllowCrossShadowBoundary */);
588 NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED);
589 if (cursorContentRoot == anchorRoot) {
590 *aRetFrame = cursorFrame;
591 aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame);
592 return NS_OK;
595 // Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse
596 // cursor is out of the window), we should use the frame of the anchor
597 // root.
602 // When we can't find a frame which is under the mouse cursor and has a same
603 // selection root as the anchor node's, we should return the selection root
604 // frame.
607 *aRetFrame = anchorRoot->GetPrimaryFrame();
609 if (!*aRetFrame) {
610 return NS_ERROR_FAILURE;
614 // Now make sure that aRetPoint is converted to the same coordinate
615 // system used by aRetFrame.
618 aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame);
620 return NS_OK;
623 void nsFrameSelection::SetCaretBidiLevelAndMaybeSchedulePaint(
624 mozilla::intl::BidiEmbeddingLevel aLevel) {
625 // If the current level is undefined, we have just inserted new text.
626 // In this case, we don't want to reset the keyboard language
627 mCaret.mBidiLevel = aLevel;
629 RefPtr<nsCaret> caret;
630 if (mPresShell && (caret = mPresShell->GetCaret())) {
631 caret->SchedulePaint();
635 mozilla::intl::BidiEmbeddingLevel nsFrameSelection::GetCaretBidiLevel() const {
636 return mCaret.mBidiLevel;
639 void nsFrameSelection::UndefineCaretBidiLevel() {
640 mCaret.mBidiLevel = mozilla::intl::BidiEmbeddingLevel(mCaret.mBidiLevel |
641 BIDI_LEVEL_UNDEFINED);
644 #ifdef PRINT_RANGE
645 void printRange(nsRange* aDomRange) {
646 if (!aDomRange) {
647 printf("NULL Range\n");
649 nsINode* startNode = aDomRange->GetStartContainer();
650 nsINode* endNode = aDomRange->GetEndContainer();
651 int32_t startOffset = aDomRange->StartOffset();
652 int32_t endOffset = aDomRange->EndOffset();
654 printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n",
655 (unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset,
656 (unsigned long)endNode, (long)endOffset);
658 #endif /* PRINT_RANGE */
660 static nsAtom* GetTag(nsINode* aNode) {
661 nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
662 if (!content) {
663 MOZ_ASSERT_UNREACHABLE("bad node passed to GetTag()");
664 return nullptr;
667 return content->NodeInfo()->NameAtom();
671 * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor.
673 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode) {
674 if (!aDomNode) return nullptr;
675 nsINode* current = aDomNode;
676 // Start with current node and look for a table cell
677 while (current) {
678 nsAtom* tag = GetTag(current);
679 if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) return current;
680 current = current->GetParent();
682 return nullptr;
685 static nsDirection GetCaretDirection(const nsIFrame& aFrame,
686 nsDirection aDirection,
687 bool aVisualMovement) {
688 const mozilla::intl::BidiDirection paragraphDirection =
689 nsBidiPresUtils::ParagraphDirection(&aFrame);
690 return (aVisualMovement &&
691 paragraphDirection == mozilla::intl::BidiDirection::RTL)
692 ? nsDirection(1 - aDirection)
693 : aDirection;
696 nsresult nsFrameSelection::MoveCaret(nsDirection aDirection,
697 bool aContinueSelection,
698 const nsSelectionAmount aAmount,
699 CaretMovementStyle aMovementStyle) {
700 NS_ENSURE_STATE(mPresShell);
701 // Flush out layout, since we need it to be up to date to do caret
702 // positioning.
703 OwningNonNull<PresShell> presShell(*mPresShell);
704 presShell->FlushPendingNotifications(FlushType::Layout);
706 if (!mPresShell) {
707 return NS_OK;
710 nsPresContext* context = mPresShell->GetPresContext();
711 if (!context) {
712 return NS_ERROR_FAILURE;
715 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
716 const RefPtr<Selection> sel = mDomSelections[index];
717 if (!sel) {
718 return NS_ERROR_NULL_POINTER;
721 int32_t scrollFlags = Selection::SCROLL_FOR_CARET_MOVE;
722 if (sel->IsEditorSelection()) {
723 // If caret moves in editor, it should cause scrolling even if it's in
724 // overflow: hidden;.
725 scrollFlags |= Selection::SCROLL_OVERFLOW_HIDDEN;
728 const bool doCollapse = [&] {
729 if (sel->IsCollapsed() || aContinueSelection) {
730 return false;
732 if (aAmount > eSelectLine) {
733 return false;
735 int32_t caretStyle = StaticPrefs::layout_selection_caret_style();
736 return caretStyle == 2 || (caretStyle == 0 && aAmount != eSelectLine);
737 }();
739 if (doCollapse) {
740 if (aDirection == eDirPrevious) {
741 SetChangeReasons(nsISelectionListener::COLLAPSETOSTART_REASON);
742 mCaret.mHint = CaretAssociationHint::After;
743 } else {
744 SetChangeReasons(nsISelectionListener::COLLAPSETOEND_REASON);
745 mCaret.mHint = CaretAssociationHint::Before;
747 } else {
748 SetChangeReasons(nsISelectionListener::KEYPRESS_REASON);
751 mCaretMoveAmount = aAmount;
753 AutoPrepareFocusRange prep(sel, false);
755 // we must keep this around and revalidate it when its just UP/DOWN
756 nsPoint desiredPos(0, 0);
758 if (aAmount == eSelectLine) {
759 nsresult result = mDesiredCaretPos.FetchPos(desiredPos, *mPresShell, *sel);
760 if (NS_FAILED(result)) {
761 return result;
763 mDesiredCaretPos.Set(desiredPos);
766 bool visualMovement =
767 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
768 const PrimaryFrameData frameForFocus =
769 sel->GetPrimaryFrameForCaretAtFocusNode(visualMovement);
770 if (!frameForFocus.mFrame) {
771 return NS_ERROR_FAILURE;
773 if (visualMovement) {
774 // FYI: This was done during a call of GetPrimaryFrameForCaretAtFocusNode.
775 // Therefore, this may not be intended by the original author.
776 SetHint(frameForFocus.mHint);
779 Result<bool, nsresult> isIntraLineCaretMove =
780 SelectionMovementUtils::IsIntraLineCaretMove(aAmount);
781 nsDirection direction{aDirection};
782 if (isIntraLineCaretMove.isErr()) {
783 return isIntraLineCaretMove.unwrapErr();
785 if (isIntraLineCaretMove.inspect()) {
786 // Forget old caret position for moving caret to different line since
787 // caret position may be changed.
788 mDesiredCaretPos.Invalidate();
789 direction =
790 GetCaretDirection(*frameForFocus.mFrame, aDirection, visualMovement);
793 if (doCollapse) {
794 const nsRange* anchorFocusRange = sel->GetAnchorFocusRange();
795 if (anchorFocusRange) {
796 RefPtr<nsINode> node;
797 uint32_t offset;
798 if (visualMovement &&
799 nsBidiPresUtils::IsReversedDirectionFrame(frameForFocus.mFrame)) {
800 direction = nsDirection(1 - direction);
802 if (direction == eDirPrevious) {
803 node = anchorFocusRange->GetStartContainer();
804 offset = anchorFocusRange->StartOffset();
805 } else {
806 node = anchorFocusRange->GetEndContainer();
807 offset = anchorFocusRange->EndOffset();
809 sel->CollapseInLimiter(node, offset);
811 sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
812 ScrollAxis(), ScrollAxis(), scrollFlags);
813 return NS_OK;
816 CaretAssociationHint tHint(
817 mCaret.mHint); // temporary variable so we dont set
818 // mCaret.mHint until it is necessary
820 Result<PeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
821 direction, aContinueSelection, aAmount, aMovementStyle, desiredPos);
822 nsresult rv;
823 if (result.isOk() && result.inspect().mResultContent) {
824 const PeekOffsetStruct& pos = result.inspect();
825 nsIFrame* theFrame;
826 int32_t frameStart, frameEnd;
828 if (aAmount <= eSelectWordNoSpace) {
829 // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does
830 // not set pos.mAttachForward, so determine the hint here based on the
831 // result frame and offset: If we're at the end of a text frame, set the
832 // hint to ASSOCIATE_BEFORE to indicate that we want the caret displayed
833 // at the end of this frame, not at the beginning of the next one.
834 theFrame = pos.mResultFrame;
835 std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
836 if (frameEnd == pos.mContentOffset && !(frameStart == 0 && frameEnd == 0))
837 tHint = CaretAssociationHint::Before;
838 else
839 tHint = CaretAssociationHint::After;
840 } else {
841 // For up/down and home/end, pos.mResultFrame might not be set correctly,
842 // or not at all. In these cases, get the frame based on the content and
843 // hint returned by PeekOffset().
844 tHint = pos.mAttach;
845 theFrame = SelectionMovementUtils::GetFrameForNodeOffset(
846 pos.mResultContent, pos.mContentOffset, tHint);
847 if (!theFrame) return NS_ERROR_FAILURE;
849 std::tie(frameStart, frameEnd) = theFrame->GetOffsets();
852 if (context->BidiEnabled()) {
853 switch (aAmount) {
854 case eSelectBeginLine:
855 case eSelectEndLine: {
856 // In Bidi contexts, PeekOffset calculates pos.mContentOffset
857 // differently depending on whether the movement is visual or logical.
858 // For visual movement, pos.mContentOffset depends on the direction-
859 // ality of the first/last frame on the line (theFrame), and the caret
860 // directionality must correspond.
861 FrameBidiData bidiData = theFrame->GetBidiData();
862 SetCaretBidiLevelAndMaybeSchedulePaint(
863 visualMovement ? bidiData.embeddingLevel : bidiData.baseLevel);
864 break;
866 default:
867 // If the current position is not a frame boundary, it's enough just
868 // to take the Bidi level of the current frame
869 if ((pos.mContentOffset != frameStart &&
870 pos.mContentOffset != frameEnd) ||
871 eSelectLine == aAmount) {
872 SetCaretBidiLevelAndMaybeSchedulePaint(
873 theFrame->GetEmbeddingLevel());
874 } else {
875 BidiLevelFromMove(mPresShell, pos.mResultContent,
876 pos.mContentOffset, aAmount, tHint);
880 // "pos" is on the stack, so pos.mResultContent has stack lifetime, so using
881 // MOZ_KnownLive is ok.
882 const FocusMode focusMode = aContinueSelection
883 ? FocusMode::kExtendSelection
884 : FocusMode::kCollapseToNewPoint;
885 rv = TakeFocus(MOZ_KnownLive(*pos.mResultContent), pos.mContentOffset,
886 pos.mContentOffset, tHint, focusMode);
887 } else if (aAmount <= eSelectWordNoSpace && direction == eDirNext &&
888 !aContinueSelection) {
889 // Collapse selection if PeekOffset failed, we either
890 // 1. bumped into the BRFrame, bug 207623
891 // 2. had select-all in a text input (DIV range), bug 352759.
892 bool isBRFrame = frameForFocus.mFrame->IsBrFrame();
893 RefPtr<nsINode> node = sel->GetFocusNode();
894 sel->CollapseInLimiter(node, sel->FocusOffset());
895 // Note: 'frameForFocus.mFrame' might be dead here.
896 if (!isBRFrame) {
897 mCaret.mHint = CaretAssociationHint::Before; // We're now at the end of
898 // the frame to the left.
900 rv = NS_OK;
901 } else {
902 rv = result.isErr() ? result.unwrapErr() : NS_OK;
904 if (NS_SUCCEEDED(rv)) {
905 rv = sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
906 ScrollAxis(), ScrollAxis(), scrollFlags);
909 return rv;
912 Result<PeekOffsetStruct, nsresult> nsFrameSelection::PeekOffsetForCaretMove(
913 nsDirection aDirection, bool aContinueSelection,
914 const nsSelectionAmount aAmount, CaretMovementStyle aMovementStyle,
915 const nsPoint& aDesiredCaretPos) const {
916 if (!mPresShell) {
917 return Err(NS_ERROR_NULL_POINTER);
920 Selection* selection =
921 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
922 if (!selection) {
923 return Err(NS_ERROR_NULL_POINTER);
926 nsIContent* content = nsIContent::FromNodeOrNull(selection->GetFocusNode());
927 if (!content) {
928 return Err(NS_ERROR_FAILURE);
930 MOZ_ASSERT(mPresShell->GetDocument() == content->GetComposedDoc());
932 const bool visualMovement =
933 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
935 PeekOffsetOptions options;
936 // set data using mLimiters.mLimiter to stop on scroll views. If we have a
937 // limiter then we stop peeking when we hit scrollable views. If no limiter
938 // then just let it go ahead
939 if (mLimiters.mLimiter) {
940 options += PeekOffsetOption::StopAtScroller;
942 if (visualMovement) {
943 options += PeekOffsetOption::Visual;
945 if (aContinueSelection) {
946 options += PeekOffsetOption::Extend;
948 if (selection->IsEditorSelection()) {
949 options += PeekOffsetOption::ForceEditableRegion;
952 return SelectionMovementUtils::PeekOffsetForCaretMove(
953 content, selection->FocusOffset(), aDirection, GetHint(),
954 GetCaretBidiLevel(), aAmount, aDesiredCaretPos, options);
957 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
958 nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const {
959 return SelectionMovementUtils::GetPrevNextBidiLevels(
960 aNode, aContentOffset, mCaret.mHint, aJumpLines);
963 nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) {
964 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
965 if (!mDomSelections[index]) {
966 return NS_ERROR_NULL_POINTER;
969 mMaintainedRange.MaintainAnchorFocusRange(*mDomSelections[index], aAmount);
971 return NS_OK;
974 void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell,
975 nsIContent* aNode,
976 uint32_t aContentOffset,
977 nsSelectionAmount aAmount,
978 CaretAssociationHint aHint) {
979 switch (aAmount) {
980 // Movement within the line: the new cursor Bidi level is the level of the
981 // last character moved over
982 case eSelectCharacter:
983 case eSelectCluster:
984 case eSelectWord:
985 case eSelectWordNoSpace:
986 case eSelectBeginLine:
987 case eSelectEndLine:
988 case eSelectNoAmount: {
989 nsPrevNextBidiLevels levels =
990 SelectionMovementUtils::GetPrevNextBidiLevels(aNode, aContentOffset,
991 aHint, false);
993 SetCaretBidiLevelAndMaybeSchedulePaint(
994 aHint == CaretAssociationHint::Before ? levels.mLevelBefore
995 : levels.mLevelAfter);
996 break;
999 // Up and Down: the new cursor Bidi level is the smaller of the two
1000 surrounding characters case eSelectLine: case eSelectParagraph:
1001 GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame,
1002 &secondFrame, &firstLevel, &secondLevel);
1003 aPresShell->SetCaretBidiLevelAndMaybeSchedulePaint(std::min(firstLevel,
1004 secondLevel)); break;
1007 default:
1008 UndefineCaretBidiLevel();
1012 void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode,
1013 uint32_t aContentOffset) {
1014 nsIFrame* clickInFrame = nullptr;
1015 clickInFrame = SelectionMovementUtils::GetFrameForNodeOffset(
1016 aNode, aContentOffset, mCaret.mHint);
1017 if (!clickInFrame) return;
1019 SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel());
1022 void nsFrameSelection::MaintainedRange::AdjustNormalSelection(
1023 const nsIContent* aContent, const int32_t aOffset,
1024 Selection& aNormalSelection) const {
1025 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1027 if (!mRange || !aContent) {
1028 return;
1031 nsINode* rangeStartNode = mRange->GetStartContainer();
1032 nsINode* rangeEndNode = mRange->GetEndContainer();
1033 const uint32_t rangeStartOffset = mRange->StartOffset();
1034 const uint32_t rangeEndOffset = mRange->EndOffset();
1036 NS_ASSERTION(aOffset >= 0, "aOffset should not be negative");
1037 const Maybe<int32_t> relToStart =
1038 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1039 rangeStartNode, rangeStartOffset, aContent, aOffset);
1040 if (NS_WARN_IF(!relToStart)) {
1041 // Potentially handle this properly when Selection across Shadow DOM
1042 // boundary is implemented
1043 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1044 return;
1047 const Maybe<int32_t> relToEnd =
1048 nsContentUtils::ComparePoints_AllowNegativeOffsets(
1049 rangeEndNode, rangeEndOffset, aContent, aOffset);
1050 if (NS_WARN_IF(!relToEnd)) {
1051 // Potentially handle this properly when Selection across Shadow DOM
1052 // boundary is implemented
1053 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1054 return;
1057 // If aContent/aOffset is inside (or at the edge of) the maintained
1058 // selection, or if it is on the "anchor" side of the maintained selection,
1059 // we need to do something.
1060 if ((*relToStart <= 0 && *relToEnd >= 0) ||
1061 (*relToStart > 0 && aNormalSelection.GetDirection() == eDirNext) ||
1062 (*relToEnd < 0 && aNormalSelection.GetDirection() == eDirPrevious)) {
1063 // Set the current range to the maintained range.
1064 aNormalSelection.ReplaceAnchorFocusRange(mRange);
1065 // Set the direction of the selection so that the anchor will be on the
1066 // far side of the maintained selection, relative to aContent/aOffset.
1067 aNormalSelection.SetDirection(*relToStart > 0 ? eDirPrevious : eDirNext);
1071 void nsFrameSelection::MaintainedRange::AdjustContentOffsets(
1072 nsIFrame::ContentOffsets& aOffsets, StopAtScroller aStopAtScroller) const {
1073 // Adjust offsets according to maintained amount
1074 if (mRange && mAmount != eSelectNoAmount) {
1075 nsINode* rangenode = mRange->GetStartContainer();
1076 int32_t rangeOffset = mRange->StartOffset();
1077 const Maybe<int32_t> relativePosition = nsContentUtils::ComparePoints(
1078 rangenode, rangeOffset, aOffsets.content, aOffsets.offset);
1079 if (NS_WARN_IF(!relativePosition)) {
1080 // Potentially handle this properly when Selection across Shadow DOM
1081 // boundary is implemented
1082 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1083 return;
1086 nsDirection direction = *relativePosition > 0 ? eDirPrevious : eDirNext;
1087 nsSelectionAmount amount = mAmount;
1088 if (amount == eSelectBeginLine && direction == eDirNext) {
1089 amount = eSelectEndLine;
1092 uint32_t offset;
1093 nsIFrame* frame = SelectionMovementUtils::GetFrameForNodeOffset(
1094 aOffsets.content, aOffsets.offset, CaretAssociationHint::After,
1095 &offset);
1097 PeekOffsetOptions peekOffsetOptions{};
1098 if (aStopAtScroller == StopAtScroller::Yes) {
1099 peekOffsetOptions += PeekOffsetOption::StopAtScroller;
1101 if (frame && amount == eSelectWord && direction == eDirPrevious) {
1102 // To avoid selecting the previous word when at start of word,
1103 // first move one character forward.
1104 PeekOffsetStruct charPos(eSelectCharacter, eDirNext,
1105 static_cast<int32_t>(offset), nsPoint(0, 0),
1106 peekOffsetOptions);
1107 if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) {
1108 frame = charPos.mResultFrame;
1109 offset = charPos.mContentOffset;
1113 PeekOffsetStruct pos(amount, direction, static_cast<int32_t>(offset),
1114 nsPoint(0, 0), peekOffsetOptions);
1115 if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) {
1116 aOffsets.content = pos.mResultContent;
1117 aOffsets.offset = pos.mContentOffset;
1122 void nsFrameSelection::MaintainedRange::MaintainAnchorFocusRange(
1123 const Selection& aNormalSelection, const nsSelectionAmount aAmount) {
1124 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1126 mAmount = aAmount;
1128 const nsRange* anchorFocusRange = aNormalSelection.GetAnchorFocusRange();
1129 if (anchorFocusRange && aAmount != eSelectNoAmount) {
1130 mRange = anchorFocusRange->CloneRange();
1131 return;
1134 mRange = nullptr;
1137 nsresult nsFrameSelection::HandleClick(nsIContent* aNewFocus,
1138 uint32_t aContentOffset,
1139 uint32_t aContentEndOffset,
1140 const FocusMode aFocusMode,
1141 CaretAssociationHint aHint) {
1142 if (!aNewFocus) return NS_ERROR_INVALID_ARG;
1144 if (MOZ_LOG_TEST(sFrameSelectionLog, LogLevel::Debug)) {
1145 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1146 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1147 ("%s: selection=%p, new focus=%p, offsets=(%u,%u), focus mode=%i",
1148 __FUNCTION__,
1149 mDomSelections[index] ? mDomSelections[index].get() : nullptr,
1150 aNewFocus, aContentOffset, aContentEndOffset,
1151 static_cast<int>(aFocusMode)));
1154 mDesiredCaretPos.Invalidate();
1156 if (aFocusMode != FocusMode::kExtendSelection) {
1157 mMaintainedRange.mRange = nullptr;
1158 if (!IsValidSelectionPoint(aNewFocus)) {
1159 mLimiters.mAncestorLimiter = nullptr;
1163 // Don't take focus when dragging off of a table
1164 if (!mTableSelection.mDragSelectingCells) {
1165 BidiLevelFromClick(aNewFocus, aContentOffset);
1166 SetChangeReasons(nsISelectionListener::MOUSEDOWN_REASON +
1167 nsISelectionListener::DRAG_REASON);
1169 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1170 RefPtr<Selection> selection = mDomSelections[index];
1171 MOZ_ASSERT(selection);
1173 if (aFocusMode == FocusMode::kExtendSelection) {
1174 mMaintainedRange.AdjustNormalSelection(aNewFocus, aContentOffset,
1175 *selection);
1178 AutoPrepareFocusRange prep(selection,
1179 aFocusMode == FocusMode::kMultiRangeSelection);
1180 return TakeFocus(*aNewFocus, aContentOffset, aContentEndOffset, aHint,
1181 aFocusMode);
1184 return NS_OK;
1187 void nsFrameSelection::HandleDrag(nsIFrame* aFrame, const nsPoint& aPoint) {
1188 if (!aFrame || !mPresShell) {
1189 return;
1192 nsresult result;
1193 nsIFrame* newFrame = 0;
1194 nsPoint newPoint;
1196 result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame,
1197 newPoint);
1198 if (NS_FAILED(result)) return;
1199 if (!newFrame) return;
1201 nsIFrame::ContentOffsets offsets =
1202 newFrame->GetContentOffsetsFromPoint(newPoint);
1203 if (!offsets.content) return;
1205 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1206 RefPtr<Selection> selection = mDomSelections[index];
1207 if (newFrame->IsSelected() && selection) {
1208 // `MOZ_KnownLive` required because of
1209 // https://bugzilla.mozilla.org/show_bug.cgi?id=1636889.
1210 mMaintainedRange.AdjustNormalSelection(MOZ_KnownLive(offsets.content),
1211 offsets.offset, *selection);
1214 mMaintainedRange.AdjustContentOffsets(
1215 offsets, mLimiters.mLimiter ? MaintainedRange::StopAtScroller::Yes
1216 : MaintainedRange::StopAtScroller::No);
1218 // TODO: no click has happened, rename `HandleClick`.
1219 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.offset,
1220 offsets.offset, FocusMode::kExtendSelection, offsets.associate);
1223 nsresult nsFrameSelection::StartAutoScrollTimer(nsIFrame* aFrame,
1224 const nsPoint& aPoint,
1225 uint32_t aDelay) {
1226 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1227 if (!mDomSelections[index]) {
1228 return NS_ERROR_NULL_POINTER;
1231 RefPtr<Selection> selection = mDomSelections[index];
1232 return selection->StartAutoScrollTimer(aFrame, aPoint, aDelay);
1235 void nsFrameSelection::StopAutoScrollTimer() {
1236 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1237 if (!mDomSelections[index]) {
1238 return;
1241 mDomSelections[index]->StopAutoScrollTimer();
1244 // static
1245 nsINode* nsFrameSelection::TableSelection::IsContentInActivelyEditableTableCell(
1246 nsPresContext* aContext, nsIContent* aContent) {
1247 if (!aContext) {
1248 return nullptr;
1251 RefPtr<HTMLEditor> htmlEditor = nsContentUtils::GetHTMLEditor(aContext);
1252 if (!htmlEditor) {
1253 return nullptr;
1256 nsINode* inclusiveTableCellAncestor =
1257 GetClosestInclusiveTableCellAncestor(aContent);
1258 if (!inclusiveTableCellAncestor) {
1259 return nullptr;
1262 const Element* editingHost = htmlEditor->ComputeEditingHost();
1263 if (!editingHost) {
1264 return nullptr;
1267 const bool editableCell =
1268 inclusiveTableCellAncestor->IsInclusiveDescendantOf(editingHost);
1269 return editableCell ? inclusiveTableCellAncestor : nullptr;
1272 namespace {
1273 struct ParentAndOffset {
1274 explicit ParentAndOffset(const nsINode& aNode)
1275 : mParent{aNode.GetParent()},
1276 mOffset{mParent ? mParent->ComputeIndexOf_Deprecated(&aNode) : 0} {}
1278 nsINode* mParent;
1280 // 0, if there's no parent.
1281 int32_t mOffset;
1284 } // namespace
1286 hard to go from nodes to frames, easy the other way!
1288 nsresult nsFrameSelection::TakeFocus(nsIContent& aNewFocus,
1289 uint32_t aContentOffset,
1290 uint32_t aContentEndOffset,
1291 CaretAssociationHint aHint,
1292 const FocusMode aFocusMode) {
1293 NS_ENSURE_STATE(mPresShell);
1295 if (!IsValidSelectionPoint(&aNewFocus)) {
1296 return NS_ERROR_FAILURE;
1299 MOZ_LOG(sFrameSelectionLog, LogLevel::Verbose,
1300 ("%s: new focus=%p, offsets=(%u, %u), hint=%i, focusMode=%i",
1301 __FUNCTION__, &aNewFocus, aContentOffset, aContentEndOffset,
1302 static_cast<int>(aHint), static_cast<int>(aFocusMode)));
1304 mPresShell->FrameSelectionWillTakeFocus(*this);
1306 // Clear all table selection data
1307 mTableSelection.mMode = TableSelectionMode::None;
1308 mTableSelection.mDragSelectingCells = false;
1309 mTableSelection.mStartSelectedCell = nullptr;
1310 mTableSelection.mEndSelectedCell = nullptr;
1311 mTableSelection.mAppendStartSelectedCell = nullptr;
1312 mCaret.mHint = aHint;
1314 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1315 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1317 Maybe<Selection::AutoUserInitiated> userSelect;
1318 if (IsUserSelectionReason()) {
1319 userSelect.emplace(mDomSelections[index]);
1322 // traverse through document and unselect crap here
1323 switch (aFocusMode) {
1324 case FocusMode::kCollapseToNewPoint:
1325 [[fallthrough]];
1326 case FocusMode::kMultiRangeSelection: {
1327 // single click? setting cursor down
1328 const Batching saveBatching =
1329 mBatching; // hack to use the collapse code.
1330 mBatching.mCounter = 1;
1332 RefPtr<Selection> selection = mDomSelections[index];
1334 if (aFocusMode == FocusMode::kMultiRangeSelection) {
1335 // Remove existing collapsed ranges as there's no point in having
1336 // non-anchor/focus collapsed ranges.
1337 selection->RemoveCollapsedRanges();
1339 ErrorResult error;
1340 RefPtr<nsRange> newRange = nsRange::Create(
1341 &aNewFocus, aContentOffset, &aNewFocus, aContentOffset, error);
1342 if (NS_WARN_IF(error.Failed())) {
1343 return error.StealNSResult();
1345 MOZ_ASSERT(newRange);
1346 selection->AddRangeAndSelectFramesAndNotifyListeners(*newRange,
1347 IgnoreErrors());
1348 } else {
1349 bool oldDesiredPosSet =
1350 mDesiredCaretPos.mIsSet; // need to keep old desired
1351 // position if it was set.
1352 selection->CollapseInLimiter(&aNewFocus, aContentOffset);
1353 mDesiredCaretPos.mIsSet =
1354 oldDesiredPosSet; // now reset desired pos back.
1357 mBatching = saveBatching;
1359 if (aContentEndOffset != aContentOffset) {
1360 selection->Extend(&aNewFocus, aContentEndOffset);
1363 // find out if we are inside a table. if so, find out which one and which
1364 // cell once we do that, the next time we get a takefocus, check the
1365 // parent tree. if we are no longer inside same table ,cell then switch to
1366 // table selection mode. BUT only do this in an editor
1368 NS_ENSURE_STATE(mPresShell);
1369 RefPtr<nsPresContext> context = mPresShell->GetPresContext();
1370 mTableSelection.mClosestInclusiveTableCellAncestor = nullptr;
1371 if (nsINode* inclusiveTableCellAncestor =
1372 TableSelection::IsContentInActivelyEditableTableCell(
1373 context, &aNewFocus)) {
1374 mTableSelection.mClosestInclusiveTableCellAncestor =
1375 inclusiveTableCellAncestor;
1376 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1377 ("%s: Collapsing into new cell", __FUNCTION__));
1380 break;
1382 case FocusMode::kExtendSelection: {
1383 // Now update the range list:
1384 nsINode* inclusiveTableCellAncestor =
1385 GetClosestInclusiveTableCellAncestor(&aNewFocus);
1386 if (mTableSelection.mClosestInclusiveTableCellAncestor &&
1387 inclusiveTableCellAncestor &&
1388 inclusiveTableCellAncestor !=
1389 mTableSelection
1390 .mClosestInclusiveTableCellAncestor) // switch to cell
1391 // selection mode
1393 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1394 ("%s: moving into new cell", __FUNCTION__));
1396 WidgetMouseEvent event(false, eVoidEvent, nullptr,
1397 WidgetMouseEvent::eReal);
1399 // Start selecting in the cell we were in before
1400 ParentAndOffset parentAndOffset{
1401 *mTableSelection.mClosestInclusiveTableCellAncestor};
1402 if (parentAndOffset.mParent) {
1403 const nsresult result = HandleTableSelection(
1404 parentAndOffset.mParent, parentAndOffset.mOffset,
1405 TableSelectionMode::Cell, &event);
1406 if (NS_WARN_IF(NS_FAILED(result))) {
1407 return result;
1411 // Find the parent of this new cell and extend selection to it
1412 parentAndOffset = ParentAndOffset{*inclusiveTableCellAncestor};
1414 // XXXX We need to REALLY get the current key shift state
1415 // (we'd need to add event listener -- let's not bother for now)
1416 event.mModifiers &= ~MODIFIER_SHIFT; // aContinueSelection;
1417 if (parentAndOffset.mParent) {
1418 mTableSelection.mClosestInclusiveTableCellAncestor =
1419 inclusiveTableCellAncestor;
1420 // Continue selection into next cell
1421 const nsresult result = HandleTableSelection(
1422 parentAndOffset.mParent, parentAndOffset.mOffset,
1423 TableSelectionMode::Cell, &event);
1424 if (NS_WARN_IF(NS_FAILED(result))) {
1425 return result;
1428 } else {
1429 RefPtr<Selection> selection = mDomSelections[index];
1430 // XXXX Problem: Shift+click in browser is appending text selection to
1431 // selected table!!!
1432 // is this the place to erase selected cells ?????
1433 uint32_t offset =
1434 (selection->GetDirection() == eDirNext &&
1435 aContentEndOffset > aContentOffset) // didn't go far enough
1436 ? aContentEndOffset // this will only redraw the diff
1437 : aContentOffset;
1438 selection->Extend(&aNewFocus, offset);
1440 break;
1444 // Don't notify selection listeners if batching is on:
1445 if (IsBatching()) {
1446 return NS_OK;
1449 // Be aware, the Selection instance may be destroyed after this call.
1450 return NotifySelectionListeners(SelectionType::eNormal);
1453 UniquePtr<SelectionDetails> nsFrameSelection::LookUpSelection(
1454 nsIContent* aContent, int32_t aContentOffset, int32_t aContentLength,
1455 bool aSlowCheck) const {
1456 if (!aContent || !mPresShell) {
1457 return nullptr;
1460 // TODO: Layout should use `uint32_t` for handling offset in DOM nodes
1461 // (for example: bug 1735262)
1462 MOZ_ASSERT(aContentOffset >= 0);
1463 MOZ_ASSERT(aContentLength >= 0);
1464 if (MOZ_UNLIKELY(aContentOffset < 0) || MOZ_UNLIKELY(aContentLength < 0)) {
1465 return nullptr;
1468 UniquePtr<SelectionDetails> details;
1470 for (size_t j = 0; j < ArrayLength(mDomSelections); j++) {
1471 if (mDomSelections[j]) {
1472 details = mDomSelections[j]->LookUpSelection(
1473 aContent, static_cast<uint32_t>(aContentOffset),
1474 static_cast<uint32_t>(aContentLength), std::move(details),
1475 kPresentSelectionTypes[j], aSlowCheck);
1479 // This may seem counter intuitive at first. Highlight selections need to be
1480 // iterated from back to front:
1482 // - `mHighlightSelections` is ordered by insertion, i.e. if two or more
1483 // highlights overlap, the latest must take precedence.
1484 // - however, the `LookupSelection()` algorithm reverses the order by setting
1485 // the current `details` as `mNext`.
1486 for (const auto& iter : Reversed(mHighlightSelections)) {
1487 details = iter.second()->LookUpSelection(
1488 aContent, static_cast<uint32_t>(aContentOffset),
1489 static_cast<uint32_t>(aContentLength), std::move(details),
1490 SelectionType::eHighlight, aSlowCheck);
1493 return details;
1496 void nsFrameSelection::SetDragState(bool aState) {
1497 if (mDragState == aState) return;
1499 mDragState = aState;
1501 if (!mDragState) {
1502 mTableSelection.mDragSelectingCells = false;
1503 // Notify that reason is mouse up.
1504 SetChangeReasons(nsISelectionListener::MOUSEUP_REASON);
1506 // flag is set to NotApplicable in `Selection::NotifySelectionListeners`.
1507 // since this function call is part of click event, this would immediately
1508 // reset the flag, rendering it useless.
1509 AutoRestore<ClickSelectionType> restoreClickSelectionType(
1510 mClickSelectionType);
1511 // Be aware, the Selection instance may be destroyed after this call.
1512 NotifySelectionListeners(SelectionType::eNormal);
1516 Selection* nsFrameSelection::GetSelection(SelectionType aSelectionType) const {
1517 int8_t index = GetIndexFromSelectionType(aSelectionType);
1518 if (index < 0) return nullptr;
1520 return mDomSelections[index];
1523 void nsFrameSelection::AddHighlightSelection(
1524 nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight) {
1525 RefPtr<Selection> selection =
1526 aHighlight.CreateHighlightSelection(aHighlightName, this);
1527 if (auto iter =
1528 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1529 [&aHighlightName](auto const& aElm) {
1530 return aElm.first() == aHighlightName;
1532 iter != mHighlightSelections.end()) {
1533 iter->second() = std::move(selection);
1534 } else {
1535 mHighlightSelections.AppendElement(
1536 CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName,
1537 std::move(selection)));
1541 void nsFrameSelection::RemoveHighlightSelection(nsAtom* aHighlightName) {
1542 if (auto iter =
1543 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1544 [&aHighlightName](auto const& aElm) {
1545 return aElm.first() == aHighlightName;
1547 iter != mHighlightSelections.end()) {
1548 RefPtr<Selection> selection = iter->second();
1549 selection->RemoveAllRanges(IgnoreErrors());
1550 mHighlightSelections.RemoveElementAt(iter);
1554 void nsFrameSelection::AddHighlightSelectionRange(
1555 nsAtom* aHighlightName, mozilla::dom::Highlight& aHighlight,
1556 mozilla::dom::AbstractRange& aRange) {
1557 if (auto iter =
1558 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1559 [&aHighlightName](auto const& aElm) {
1560 return aElm.first() == aHighlightName;
1562 iter != mHighlightSelections.end()) {
1563 RefPtr<Selection> selection = iter->second();
1564 selection->AddHighlightRangeAndSelectFramesAndNotifyListeners(aRange);
1565 } else {
1566 // if the selection does not exist yet, add all of its ranges and exit.
1567 RefPtr<Selection> selection =
1568 aHighlight.CreateHighlightSelection(aHighlightName, this);
1569 mHighlightSelections.AppendElement(
1570 CompactPair<RefPtr<nsAtom>, RefPtr<Selection>>(aHighlightName,
1571 std::move(selection)));
1575 void nsFrameSelection::RemoveHighlightSelectionRange(
1576 nsAtom* aHighlightName, mozilla::dom::AbstractRange& aRange) {
1577 if (auto iter =
1578 std::find_if(mHighlightSelections.begin(), mHighlightSelections.end(),
1579 [&aHighlightName](auto const& aElm) {
1580 return aElm.first() == aHighlightName;
1582 iter != mHighlightSelections.end()) {
1583 // NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
1584 RefPtr<Selection> selection = iter->second();
1585 selection->RemoveRangeAndUnselectFramesAndNotifyListeners(aRange,
1586 IgnoreErrors());
1590 nsresult nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType,
1591 SelectionRegion aRegion,
1592 int16_t aFlags) const {
1593 int8_t index = GetIndexFromSelectionType(aSelectionType);
1594 if (index < 0) return NS_ERROR_INVALID_ARG;
1596 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1598 ScrollAxis verticalScroll = ScrollAxis();
1599 int32_t flags = Selection::SCROLL_DO_FLUSH;
1600 if (aFlags & nsISelectionController::SCROLL_SYNCHRONOUS) {
1601 flags |= Selection::SCROLL_SYNCHRONOUS;
1602 } else if (aFlags & nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY) {
1603 flags |= Selection::SCROLL_FIRST_ANCESTOR_ONLY;
1605 if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) {
1606 flags |= Selection::SCROLL_OVERFLOW_HIDDEN;
1608 if (aFlags & nsISelectionController::SCROLL_CENTER_VERTICALLY) {
1609 verticalScroll =
1610 ScrollAxis(WhereToScroll::Center, WhenToScroll::IfNotFullyVisible);
1612 if (aFlags & nsISelectionController::SCROLL_FOR_CARET_MOVE) {
1613 flags |= Selection::SCROLL_FOR_CARET_MOVE;
1616 // After ScrollSelectionIntoView(), the pending notifications might be
1617 // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
1618 RefPtr<Selection> sel = mDomSelections[index];
1619 return sel->ScrollIntoView(aRegion, verticalScroll, ScrollAxis(), flags);
1622 nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) {
1623 int8_t index = GetIndexFromSelectionType(aSelectionType);
1624 if (index < 0) return NS_ERROR_INVALID_ARG;
1625 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1626 NS_ENSURE_STATE(mPresShell);
1628 // On macOS, update the selection cache to the new active selection
1629 // aka the current selection.
1630 #ifdef XP_MACOSX
1631 // Check that we're in the an active window and, if this is Web content,
1632 // in the frontmost tab.
1633 Document* doc = mPresShell->GetDocument();
1634 if (doc && IsInActiveTab(doc) && aSelectionType == SelectionType::eNormal) {
1635 UpdateSelectionCacheOnRepaintSelection(mDomSelections[index]);
1637 #endif
1638 return mDomSelections[index]->Repaint(mPresShell->GetPresContext());
1641 nsIFrame* nsFrameSelection::GetFrameToPageSelect() const {
1642 if (NS_WARN_IF(!mPresShell)) {
1643 return nullptr;
1646 nsIFrame* rootFrameToSelect;
1647 if (mLimiters.mLimiter) {
1648 rootFrameToSelect = mLimiters.mLimiter->GetPrimaryFrame();
1649 if (NS_WARN_IF(!rootFrameToSelect)) {
1650 return nullptr;
1652 } else if (mLimiters.mAncestorLimiter) {
1653 rootFrameToSelect = mLimiters.mAncestorLimiter->GetPrimaryFrame();
1654 if (NS_WARN_IF(!rootFrameToSelect)) {
1655 return nullptr;
1657 } else {
1658 rootFrameToSelect = mPresShell->GetRootScrollFrame();
1659 if (NS_WARN_IF(!rootFrameToSelect)) {
1660 return nullptr;
1664 nsCOMPtr<nsIContent> contentToSelect = mPresShell->GetContentForScrolling();
1665 if (contentToSelect) {
1666 // If there is selected content, look for nearest and vertical scrollable
1667 // parent under the root frame.
1668 for (nsIFrame* frame = contentToSelect->GetPrimaryFrame();
1669 frame && frame != rootFrameToSelect; frame = frame->GetParent()) {
1670 nsIScrollableFrame* scrollableFrame = do_QueryFrame(frame);
1671 if (!scrollableFrame) {
1672 continue;
1674 ScrollStyles scrollStyles = scrollableFrame->GetScrollStyles();
1675 if (scrollStyles.mVertical == StyleOverflow::Hidden) {
1676 continue;
1678 layers::ScrollDirections directions =
1679 scrollableFrame->GetAvailableScrollingDirections();
1680 if (directions.contains(layers::ScrollDirection::eVertical)) {
1681 // If there is sub scrollable frame, let's use its page size to select.
1682 return frame;
1686 // Otherwise, i.e., there is no scrollable frame or only the root frame is
1687 // scrollable, let's return the root frame because Shift + PageUp/PageDown
1688 // should expand the selection in the root content even if it's not
1689 // scrollable.
1690 return rootFrameToSelect;
1693 nsresult nsFrameSelection::PageMove(bool aForward, bool aExtend,
1694 nsIFrame* aFrame,
1695 SelectionIntoView aSelectionIntoView) {
1696 MOZ_ASSERT(aFrame);
1698 // expected behavior for PageMove is to scroll AND move the caret
1699 // and remain relative position of the caret in view. see Bug 4302.
1701 // Get the scrollable frame. If aFrame is not scrollable, this is nullptr.
1702 nsIScrollableFrame* scrollableFrame = aFrame->GetScrollTargetFrame();
1703 // Get the scrolled frame. If aFrame is not scrollable, this is aFrame
1704 // itself.
1705 nsIFrame* scrolledFrame =
1706 scrollableFrame ? scrollableFrame->GetScrolledFrame() : aFrame;
1707 if (!scrolledFrame) {
1708 return NS_OK;
1711 // find out where the caret is.
1712 // we should know mDesiredCaretPos.mValue value of nsFrameSelection, but I
1713 // havent seen that behavior in other windows applications yet.
1714 RefPtr<Selection> selection = GetSelection(SelectionType::eNormal);
1715 if (!selection) {
1716 return NS_OK;
1719 nsRect caretPos;
1720 nsIFrame* caretFrame = nsCaret::GetGeometry(selection, &caretPos);
1721 if (!caretFrame) {
1722 return NS_OK;
1725 // If the scrolled frame is outside of current selection limiter,
1726 // we need to scroll the frame but keep moving selection in the limiter.
1727 nsIFrame* frameToClick = scrolledFrame;
1728 if (!IsValidSelectionPoint(scrolledFrame->GetContent())) {
1729 frameToClick = GetFrameToPageSelect();
1730 if (NS_WARN_IF(!frameToClick)) {
1731 return NS_OK;
1735 if (scrollableFrame) {
1736 // If there is a scrollable frame, adjust pseudo-click position with page
1737 // scroll amount.
1738 // XXX This may scroll more than one page if ScrollSelectionIntoView is
1739 // called later because caret may not fully visible. E.g., if
1740 // clicking line will be visible only half height with scrolling
1741 // the frame, ScrollSelectionIntoView additionally scrolls to show
1742 // the caret entirely.
1743 if (aForward) {
1744 caretPos.y += scrollableFrame->GetPageScrollAmount().height;
1745 } else {
1746 caretPos.y -= scrollableFrame->GetPageScrollAmount().height;
1748 } else {
1749 // Otherwise, adjust pseudo-click position with the frame size.
1750 if (aForward) {
1751 caretPos.y += frameToClick->GetSize().height;
1752 } else {
1753 caretPos.y -= frameToClick->GetSize().height;
1757 caretPos += caretFrame->GetOffsetTo(frameToClick);
1759 // get a content at desired location
1760 nsPoint desiredPoint;
1761 desiredPoint.x = caretPos.x;
1762 desiredPoint.y = caretPos.y + caretPos.height / 2;
1763 nsIFrame::ContentOffsets offsets =
1764 frameToClick->GetContentOffsetsFromPoint(desiredPoint);
1766 if (!offsets.content) {
1767 // XXX Do we need to handle ScrollSelectionIntoView in this case?
1768 return NS_OK;
1771 // First, place the caret.
1772 bool selectionChanged;
1774 // We don't want any script to run until we check whether selection is
1775 // modified by HandleClick.
1776 SelectionBatcher ensureNoSelectionChangeNotifications(selection,
1777 __FUNCTION__);
1779 RangeBoundary oldAnchor = selection->AnchorRef();
1780 RangeBoundary oldFocus = selection->FocusRef();
1782 const FocusMode focusMode =
1783 aExtend ? FocusMode::kExtendSelection : FocusMode::kCollapseToNewPoint;
1784 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
1785 offsets.offset, offsets.offset, focusMode,
1786 CaretAssociationHint::After);
1788 selectionChanged = selection->AnchorRef() != oldAnchor ||
1789 selection->FocusRef() != oldFocus;
1792 bool doScrollSelectionIntoView = !(
1793 aSelectionIntoView == SelectionIntoView::IfChanged && !selectionChanged);
1795 // Then, scroll the given frame one page.
1796 if (scrollableFrame) {
1797 // If we'll call ScrollSelectionIntoView later and selection wasn't
1798 // changed and we scroll outside of selection limiter, we shouldn't use
1799 // smooth scroll here because nsIScrollableFrame uses normal runnable,
1800 // but ScrollSelectionIntoView uses early runner and it cancels the
1801 // pending smooth scroll. Therefore, if we used smooth scroll in such
1802 // case, ScrollSelectionIntoView would scroll to show caret instead of
1803 // page scroll of an element outside selection limiter.
1804 ScrollMode scrollMode = doScrollSelectionIntoView && !selectionChanged &&
1805 scrolledFrame != frameToClick
1806 ? ScrollMode::Instant
1807 : ScrollMode::Smooth;
1808 scrollableFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
1809 ScrollUnit::PAGES, scrollMode);
1812 // Finally, scroll selection into view if requested.
1813 if (!doScrollSelectionIntoView) {
1814 return NS_OK;
1816 return ScrollSelectionIntoView(
1817 SelectionType::eNormal, nsISelectionController::SELECTION_FOCUS_REGION,
1818 nsISelectionController::SCROLL_SYNCHRONOUS |
1819 nsISelectionController::SCROLL_FOR_CARET_MOVE);
1822 nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount,
1823 bool aExtend) {
1824 NS_ENSURE_STATE(mPresShell);
1825 // Flush out layout, since we need it to be up to date to do caret
1826 // positioning.
1827 OwningNonNull<PresShell> presShell(*mPresShell);
1828 presShell->FlushPendingNotifications(FlushType::Layout);
1830 if (!mPresShell) {
1831 return NS_OK;
1834 // Check that parameters are safe
1835 if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) {
1836 return NS_ERROR_FAILURE;
1839 nsPresContext* context = mPresShell->GetPresContext();
1840 if (!context) {
1841 return NS_ERROR_FAILURE;
1844 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1845 RefPtr<Selection> sel = mDomSelections[index];
1846 if (!sel) {
1847 return NS_ERROR_NULL_POINTER;
1850 // Map the abstract movement amounts (0-1) to direction-specific
1851 // selection units.
1852 static const nsSelectionAmount inlineAmount[] = {eSelectCluster, eSelectWord};
1853 static const nsSelectionAmount blockPrevAmount[] = {eSelectLine,
1854 eSelectBeginLine};
1855 static const nsSelectionAmount blockNextAmount[] = {eSelectLine,
1856 eSelectEndLine};
1858 struct PhysicalToLogicalMapping {
1859 nsDirection direction;
1860 const nsSelectionAmount* amounts;
1862 static const PhysicalToLogicalMapping verticalLR[4] = {
1863 {eDirPrevious, blockPrevAmount}, // left
1864 {eDirNext, blockNextAmount}, // right
1865 {eDirPrevious, inlineAmount}, // up
1866 {eDirNext, inlineAmount} // down
1868 static const PhysicalToLogicalMapping verticalRL[4] = {
1869 {eDirNext, blockNextAmount},
1870 {eDirPrevious, blockPrevAmount},
1871 {eDirPrevious, inlineAmount},
1872 {eDirNext, inlineAmount}};
1873 static const PhysicalToLogicalMapping horizontal[4] = {
1874 {eDirPrevious, inlineAmount},
1875 {eDirNext, inlineAmount},
1876 {eDirPrevious, blockPrevAmount},
1877 {eDirNext, blockNextAmount}};
1879 WritingMode wm;
1880 const PrimaryFrameData frameForFocus =
1881 sel->GetPrimaryFrameForCaretAtFocusNode(true);
1882 if (frameForFocus.mFrame) {
1883 // FYI: Setting the caret association hint was done during a call of
1884 // GetPrimaryFrameForCaretAtFocusNode. Therefore, this may not be intended
1885 // by the original author.
1886 sel->GetFrameSelection()->SetHint(frameForFocus.mHint);
1888 if (!frameForFocus.mFrame->Style()->IsTextCombined()) {
1889 wm = frameForFocus.mFrame->GetWritingMode();
1890 } else {
1891 // Using different direction for horizontal-in-vertical would
1892 // make it hard to navigate via keyboard. Inherit the moving
1893 // direction from its parent.
1894 MOZ_ASSERT(frameForFocus.mFrame->IsTextFrame());
1895 wm = frameForFocus.mFrame->GetParent()->GetWritingMode();
1896 MOZ_ASSERT(wm.IsVertical(),
1897 "Text combined "
1898 "can only appear in vertical text");
1902 const PhysicalToLogicalMapping& mapping =
1903 wm.IsVertical()
1904 ? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection]
1905 : horizontal[aDirection];
1907 nsresult rv =
1908 MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount], eVisual);
1909 if (NS_FAILED(rv)) {
1910 // If we tried to do a line move, but couldn't move in the given direction,
1911 // then we'll "promote" this to a line-edge move instead.
1912 if (mapping.amounts[aAmount] == eSelectLine) {
1913 rv = MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount + 1],
1914 eVisual);
1916 // And if it was a next-word move that failed (which can happen when
1917 // eat_space_to_next_word is true, see bug 1153237), then just move forward
1918 // to the line-edge.
1919 else if (mapping.amounts[aAmount] == eSelectWord &&
1920 mapping.direction == eDirNext) {
1921 rv = MoveCaret(eDirNext, aExtend, eSelectEndLine, eVisual);
1925 return rv;
1928 nsresult nsFrameSelection::CharacterMove(bool aForward, bool aExtend) {
1929 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectCluster,
1930 eUsePrefStyle);
1933 nsresult nsFrameSelection::WordMove(bool aForward, bool aExtend) {
1934 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectWord,
1935 eUsePrefStyle);
1938 nsresult nsFrameSelection::LineMove(bool aForward, bool aExtend) {
1939 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectLine,
1940 eUsePrefStyle);
1943 nsresult nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) {
1944 if (aForward) {
1945 return MoveCaret(eDirNext, aExtend, eSelectEndLine, eLogical);
1946 } else {
1947 return MoveCaret(eDirPrevious, aExtend, eSelectBeginLine, eLogical);
1951 template <typename RangeType>
1952 Result<RefPtr<RangeType>, nsresult>
1953 nsFrameSelection::CreateRangeExtendedToSomewhere(
1954 nsDirection aDirection, const nsSelectionAmount aAmount,
1955 CaretMovementStyle aMovementStyle) {
1956 MOZ_ASSERT(aDirection == eDirNext || aDirection == eDirPrevious);
1957 MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster ||
1958 aAmount == eSelectWord || aAmount == eSelectBeginLine ||
1959 aAmount == eSelectEndLine);
1960 MOZ_ASSERT(aMovementStyle == eLogical || aMovementStyle == eVisual ||
1961 aMovementStyle == eUsePrefStyle);
1963 if (!mPresShell) {
1964 return Err(NS_ERROR_UNEXPECTED);
1966 OwningNonNull<PresShell> presShell(*mPresShell);
1967 presShell->FlushPendingNotifications(FlushType::Layout);
1968 if (!mPresShell) {
1969 return Err(NS_ERROR_FAILURE);
1971 Selection* selection =
1972 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
1973 if (!selection || selection->RangeCount() != 1) {
1974 return Err(NS_ERROR_FAILURE);
1976 RefPtr<const nsRange> firstRange = selection->GetRangeAt(0);
1977 if (!firstRange || !firstRange->IsPositioned()) {
1978 return Err(NS_ERROR_FAILURE);
1980 Result<PeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
1981 aDirection, true, aAmount, aMovementStyle, nsPoint(0, 0));
1982 if (result.isErr()) {
1983 return Err(NS_ERROR_FAILURE);
1985 const PeekOffsetStruct& pos = result.inspect();
1986 RefPtr<RangeType> range;
1987 if (NS_WARN_IF(!pos.mResultContent)) {
1988 return range;
1990 if (aDirection == eDirPrevious) {
1991 range = RangeType::Create(
1992 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
1993 firstRange->EndRef(), IgnoreErrors());
1994 } else {
1995 range = RangeType::Create(
1996 firstRange->StartRef(),
1997 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
1998 IgnoreErrors());
2000 return range;
2003 //////////END FRAMESELECTION
2005 LazyLogModule gBatchLog("SelectionBatch");
2007 void nsFrameSelection::StartBatchChanges(const char* aRequesterFuncName) {
2008 MOZ_LOG(gBatchLog, LogLevel::Info,
2009 ("%p%snsFrameSelection::StartBatchChanges(%s)", this,
2010 std::string((mBatching.mCounter + 1) * 2, ' ').c_str(),
2011 aRequesterFuncName));
2012 mBatching.mCounter++;
2015 void nsFrameSelection::EndBatchChanges(const char* aRequesterFuncName,
2016 int16_t aReasons) {
2017 MOZ_LOG(gBatchLog, LogLevel::Info,
2018 ("%p%snsFrameSelection::EndBatchChanges (%s, %s)", this,
2019 std::string(mBatching.mCounter * 2, ' ').c_str(), aRequesterFuncName,
2020 SelectionChangeReasonsToCString(aReasons).get()));
2021 MOZ_ASSERT(mBatching.mCounter > 0, "Bad mBatching.mCounter");
2022 mBatching.mCounter--;
2024 if (mBatching.mCounter == 0 && mBatching.mChangesDuringBatching) {
2025 AddChangeReasons(aReasons);
2026 mCaretMoveAmount = eSelectNoAmount;
2027 mBatching.mChangesDuringBatching = false;
2028 // Be aware, the Selection instance may be destroyed after this call.
2029 NotifySelectionListeners(SelectionType::eNormal);
2033 nsresult nsFrameSelection::NotifySelectionListeners(
2034 SelectionType aSelectionType) {
2035 int8_t index = GetIndexFromSelectionType(aSelectionType);
2036 if (index >= 0 && mDomSelections[index]) {
2037 RefPtr<Selection> selection = mDomSelections[index];
2038 selection->NotifySelectionListeners();
2039 mCaretMoveAmount = eSelectNoAmount;
2040 return NS_OK;
2042 return NS_ERROR_FAILURE;
2045 // Start of Table Selection methods
2047 static bool IsCell(nsIContent* aContent) {
2048 return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
2051 // static
2052 nsITableCellLayout* nsFrameSelection::GetCellLayout(
2053 const nsIContent* aCellContent) {
2054 nsITableCellLayout* cellLayoutObject =
2055 do_QueryFrame(aCellContent->GetPrimaryFrame());
2056 return cellLayoutObject;
2059 nsresult nsFrameSelection::ClearNormalSelection() {
2060 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2061 RefPtr<Selection> selection = mDomSelections[index];
2062 if (!selection) {
2063 return NS_ERROR_NULL_POINTER;
2066 ErrorResult err;
2067 selection->RemoveAllRanges(err);
2068 return err.StealNSResult();
2071 static nsIContent* GetFirstSelectedContent(const nsRange* aRange) {
2072 if (!aRange) {
2073 return nullptr;
2076 MOZ_ASSERT(aRange->GetStartContainer(), "Must have start parent!");
2077 MOZ_ASSERT(aRange->GetStartContainer()->IsElement(), "Unexpected parent");
2079 return aRange->GetChildAtStartOffset();
2082 // Table selection support.
2083 // TODO: Separate table methods into a separate nsITableSelection interface
2084 nsresult nsFrameSelection::HandleTableSelection(nsINode* aParentContent,
2085 int32_t aContentOffset,
2086 TableSelectionMode aTarget,
2087 WidgetMouseEvent* aMouseEvent) {
2088 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2089 RefPtr<Selection> selection = mDomSelections[index];
2090 if (!selection) {
2091 return NS_ERROR_NULL_POINTER;
2094 return mTableSelection.HandleSelection(aParentContent, aContentOffset,
2095 aTarget, aMouseEvent, mDragState,
2096 *selection);
2099 nsresult nsFrameSelection::TableSelection::HandleSelection(
2100 nsINode* aParentContent, int32_t aContentOffset, TableSelectionMode aTarget,
2101 WidgetMouseEvent* aMouseEvent, bool aDragState,
2102 Selection& aNormalSelection) {
2103 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2105 NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER);
2106 NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER);
2108 if (aDragState && mDragSelectingCells &&
2109 aTarget == TableSelectionMode::Table) {
2110 // We were selecting cells and user drags mouse in table border or inbetween
2111 // cells,
2112 // just do nothing
2113 return NS_OK;
2116 RefPtr<nsIContent> childContent =
2117 aParentContent->GetChildAt_Deprecated(aContentOffset);
2119 // When doing table selection, always set the direction to next so
2120 // we can be sure that anchorNode's offset always points to the
2121 // selected cell
2122 aNormalSelection.SetDirection(eDirNext);
2124 // Stack-class to wrap all table selection changes in
2125 // BeginBatchChanges() / EndBatchChanges()
2126 SelectionBatcher selectionBatcher(&aNormalSelection, __FUNCTION__);
2128 if (aDragState && mDragSelectingCells) {
2129 return HandleDragSelecting(aTarget, childContent, aMouseEvent,
2130 aNormalSelection);
2133 return HandleMouseUpOrDown(aTarget, aDragState, childContent, aParentContent,
2134 aContentOffset, aMouseEvent, aNormalSelection);
2137 class nsFrameSelection::TableSelection::RowAndColumnRelation {
2138 public:
2139 static Result<RowAndColumnRelation, nsresult> Create(
2140 const nsIContent* aFirst, const nsIContent* aSecond) {
2141 RowAndColumnRelation result;
2143 nsresult errorResult =
2144 GetCellIndexes(aFirst, result.mFirst.mRow, result.mFirst.mColumn);
2145 if (NS_FAILED(errorResult)) {
2146 return Err(errorResult);
2149 errorResult =
2150 GetCellIndexes(aSecond, result.mSecond.mRow, result.mSecond.mColumn);
2151 if (NS_FAILED(errorResult)) {
2152 return Err(errorResult);
2155 return result;
2158 bool IsSameColumn() const { return mFirst.mColumn == mSecond.mColumn; }
2160 bool IsSameRow() const { return mFirst.mRow == mSecond.mRow; }
2162 private:
2163 RowAndColumnRelation() = default;
2165 struct RowAndColumn {
2166 int32_t mRow = 0;
2167 int32_t mColumn = 0;
2170 RowAndColumn mFirst;
2171 RowAndColumn mSecond;
2174 nsresult nsFrameSelection::TableSelection::HandleDragSelecting(
2175 TableSelectionMode aTarget, nsIContent* aChildContent,
2176 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2177 // We are drag-selecting
2178 if (aTarget != TableSelectionMode::Table) {
2179 // If dragging in the same cell as last event, do nothing
2180 if (mEndSelectedCell == aChildContent) {
2181 return NS_OK;
2184 #ifdef DEBUG_TABLE_SELECTION
2185 printf(
2186 " mStartSelectedCell = %p, "
2187 "mEndSelectedCell = %p, aChildContent = %p "
2188 "\n",
2189 mStartSelectedCell.get(), mEndSelectedCell.get(), aChildContent);
2190 #endif
2191 // aTarget can be any "cell mode",
2192 // so we can easily drag-select rows and columns
2193 // Once we are in row or column mode,
2194 // we can drift into any cell to stay in that mode
2195 // even if aTarget = TableSelectionMode::Cell
2197 if (mMode == TableSelectionMode::Row ||
2198 mMode == TableSelectionMode::Column) {
2199 if (mEndSelectedCell) {
2200 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2201 RowAndColumnRelation::Create(mEndSelectedCell, aChildContent);
2203 if (rowAndColumnRelation.isErr()) {
2204 return rowAndColumnRelation.unwrapErr();
2207 if ((mMode == TableSelectionMode::Row &&
2208 rowAndColumnRelation.inspect().IsSameRow()) ||
2209 (mMode == TableSelectionMode::Column &&
2210 rowAndColumnRelation.inspect().IsSameColumn())) {
2211 return NS_OK;
2214 #ifdef DEBUG_TABLE_SELECTION
2215 printf(" Dragged into a new column or row\n");
2216 #endif
2217 // Continue dragging row or column selection
2219 return SelectRowOrColumn(aChildContent, aNormalSelection);
2221 if (mMode == TableSelectionMode::Cell) {
2222 #ifdef DEBUG_TABLE_SELECTION
2223 printf("HandleTableSelection: Dragged into a new cell\n");
2224 #endif
2225 // Trick for quick selection of rows and columns
2226 // Hold down shift, then start selecting in one direction
2227 // If next cell dragged into is in same row, select entire row,
2228 // if next cell is in same column, select entire column
2229 if (mStartSelectedCell && aMouseEvent->IsShift()) {
2230 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2231 RowAndColumnRelation::Create(mStartSelectedCell, aChildContent);
2232 if (rowAndColumnRelation.isErr()) {
2233 return rowAndColumnRelation.unwrapErr();
2236 if (rowAndColumnRelation.inspect().IsSameRow() ||
2237 rowAndColumnRelation.inspect().IsSameColumn()) {
2238 // Force new selection block
2239 mStartSelectedCell = nullptr;
2240 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2242 if (rowAndColumnRelation.inspect().IsSameRow()) {
2243 mMode = TableSelectionMode::Row;
2244 } else {
2245 mMode = TableSelectionMode::Column;
2248 return SelectRowOrColumn(aChildContent, aNormalSelection);
2252 // Reselect block of cells to new end location
2253 return SelectBlockOfCells(mStartSelectedCell, aChildContent,
2254 aNormalSelection);
2257 // Do nothing if dragging in table, but outside a cell
2258 return NS_OK;
2261 nsresult nsFrameSelection::TableSelection::HandleMouseUpOrDown(
2262 TableSelectionMode aTarget, bool aDragState, nsIContent* aChildContent,
2263 nsINode* aParentContent, int32_t aContentOffset,
2264 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2265 nsresult result = NS_OK;
2266 // Not dragging -- mouse event is down or up
2267 if (aDragState) {
2268 #ifdef DEBUG_TABLE_SELECTION
2269 printf("HandleTableSelection: Mouse down event\n");
2270 #endif
2271 // Clear cell we stored in mouse-down
2272 mUnselectCellOnMouseUp = nullptr;
2274 if (aTarget == TableSelectionMode::Cell) {
2275 bool isSelected = false;
2277 // Check if we have other selected cells
2278 nsIContent* previousCellNode =
2279 GetFirstSelectedContent(GetFirstCellRange(aNormalSelection));
2280 if (previousCellNode) {
2281 // We have at least 1 other selected cell
2283 // Check if new cell is already selected
2284 nsIFrame* cellFrame = aChildContent->GetPrimaryFrame();
2285 if (!cellFrame) {
2286 return NS_ERROR_NULL_POINTER;
2288 isSelected = cellFrame->IsSelected();
2289 } else {
2290 // No cells selected -- remove non-cell selection
2291 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2293 mDragSelectingCells = true; // Signal to start drag-cell-selection
2294 mMode = aTarget;
2295 // Set start for new drag-selection block (not appended)
2296 mStartSelectedCell = aChildContent;
2297 // The initial block end is same as the start
2298 mEndSelectedCell = aChildContent;
2300 if (isSelected) {
2301 // Remember this cell to (possibly) unselect it on mouseup
2302 mUnselectCellOnMouseUp = aChildContent;
2303 #ifdef DEBUG_TABLE_SELECTION
2304 printf(
2305 "HandleTableSelection: Saving "
2306 "mUnselectCellOnMouseUp\n");
2307 #endif
2308 } else {
2309 // Select an unselected cell
2310 // but first remove existing selection if not in same table
2311 if (previousCellNode &&
2312 !IsInSameTable(previousCellNode, aChildContent)) {
2313 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2314 // Reset selection mode that is cleared in RemoveAllRanges
2315 mMode = aTarget;
2318 return ::SelectCellElement(aChildContent, aNormalSelection);
2321 return NS_OK;
2323 if (aTarget == TableSelectionMode::Table) {
2324 // TODO: We currently select entire table when clicked between cells,
2325 // should we restrict to only around border?
2326 // *** How do we get location data for cell and click?
2327 mDragSelectingCells = false;
2328 mStartSelectedCell = nullptr;
2329 mEndSelectedCell = nullptr;
2331 // Remove existing selection and select the table
2332 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2333 return CreateAndAddRange(aParentContent, aContentOffset,
2334 aNormalSelection);
2336 if (aTarget == TableSelectionMode::Row ||
2337 aTarget == TableSelectionMode::Column) {
2338 #ifdef DEBUG_TABLE_SELECTION
2339 printf("aTarget == %d\n", aTarget);
2340 #endif
2342 // Start drag-selecting mode so multiple rows/cols can be selected
2343 // Note: Currently, nsIFrame::GetDataForTableSelection
2344 // will never call us for row or column selection on mouse down
2345 mDragSelectingCells = true;
2347 // Force new selection block
2348 mStartSelectedCell = nullptr;
2349 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2350 // Always do this AFTER RemoveAllRanges
2351 mMode = aTarget;
2353 return SelectRowOrColumn(aChildContent, aNormalSelection);
2355 } else {
2356 #ifdef DEBUG_TABLE_SELECTION
2357 printf(
2358 "HandleTableSelection: Mouse UP event. "
2359 "mDragSelectingCells=%d, "
2360 "mStartSelectedCell=%p\n",
2361 mDragSelectingCells, mStartSelectedCell.get());
2362 #endif
2363 // First check if we are extending a block selection
2364 const uint32_t rangeCount = aNormalSelection.RangeCount();
2366 if (rangeCount > 0 && aMouseEvent->IsShift() && mAppendStartSelectedCell &&
2367 mAppendStartSelectedCell != aChildContent) {
2368 // Shift key is down: append a block selection
2369 mDragSelectingCells = false;
2371 return SelectBlockOfCells(mAppendStartSelectedCell, aChildContent,
2372 aNormalSelection);
2375 if (mDragSelectingCells) {
2376 mAppendStartSelectedCell = mStartSelectedCell;
2379 mDragSelectingCells = false;
2380 mStartSelectedCell = nullptr;
2381 mEndSelectedCell = nullptr;
2383 // Any other mouseup actions require that Ctrl or Cmd key is pressed
2384 // else stop table selection mode
2385 bool doMouseUpAction = false;
2386 #ifdef XP_MACOSX
2387 doMouseUpAction = aMouseEvent->IsMeta();
2388 #else
2389 doMouseUpAction = aMouseEvent->IsControl();
2390 #endif
2391 if (!doMouseUpAction) {
2392 #ifdef DEBUG_TABLE_SELECTION
2393 printf(
2394 "HandleTableSelection: Ending cell selection on mouseup: "
2395 "mAppendStartSelectedCell=%p\n",
2396 mAppendStartSelectedCell.get());
2397 #endif
2398 return NS_OK;
2400 // Unselect a cell only if it wasn't
2401 // just selected on mousedown
2402 if (aChildContent == mUnselectCellOnMouseUp) {
2403 // Scan ranges to find the cell to unselect (the selection range to
2404 // remove)
2405 // XXXbz it's really weird that this lives outside the loop, so once we
2406 // find one we keep looking at it even if we find no more cells...
2407 nsINode* previousCellParent = nullptr;
2408 #ifdef DEBUG_TABLE_SELECTION
2409 printf(
2410 "HandleTableSelection: Unselecting "
2411 "mUnselectCellOnMouseUp; "
2412 "rangeCount=%d\n",
2413 rangeCount);
2414 #endif
2415 for (const uint32_t i : IntegerRange(rangeCount)) {
2416 MOZ_ASSERT(aNormalSelection.RangeCount() == rangeCount);
2417 // Strong reference, because sometimes we want to remove
2418 // this range, and then we might be the only owner.
2419 RefPtr<nsRange> range = aNormalSelection.GetRangeAt(i);
2420 if (MOZ_UNLIKELY(!range)) {
2421 return NS_ERROR_NULL_POINTER;
2424 nsINode* container = range->GetStartContainer();
2425 if (!container) {
2426 return NS_ERROR_NULL_POINTER;
2429 int32_t offset = range->StartOffset();
2430 // Be sure previous selection is a table cell
2431 nsIContent* child = range->GetChildAtStartOffset();
2432 if (child && IsCell(child)) {
2433 previousCellParent = container;
2436 // We're done if we didn't find parent of a previously-selected cell
2437 if (!previousCellParent) {
2438 break;
2441 if (previousCellParent == aParentContent && offset == aContentOffset) {
2442 // Cell is already selected
2443 if (rangeCount == 1) {
2444 #ifdef DEBUG_TABLE_SELECTION
2445 printf("HandleTableSelection: Unselecting single selected cell\n");
2446 #endif
2447 // This was the only cell selected.
2448 // Collapse to "normal" selection inside the cell
2449 mStartSelectedCell = nullptr;
2450 mEndSelectedCell = nullptr;
2451 mAppendStartSelectedCell = nullptr;
2452 // TODO: We need a "Collapse to just before deepest child" routine
2453 // Even better, should we collapse to just after the LAST deepest
2454 // child
2455 // (i.e., at the end of the cell's contents)?
2456 return aNormalSelection.CollapseInLimiter(aChildContent, 0);
2458 #ifdef DEBUG_TABLE_SELECTION
2459 printf(
2460 "HandleTableSelection: Removing cell from multi-cell "
2461 "selection\n");
2462 #endif
2463 // Unselecting the start of previous block
2464 // XXX What do we use now!
2465 if (aChildContent == mAppendStartSelectedCell) {
2466 mAppendStartSelectedCell = nullptr;
2469 // Deselect cell by removing its range from selection
2470 ErrorResult err;
2471 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2472 *range, err);
2473 return err.StealNSResult();
2476 mUnselectCellOnMouseUp = nullptr;
2479 return result;
2482 nsresult nsFrameSelection::TableSelection::SelectBlockOfCells(
2483 nsIContent* aStartCell, nsIContent* aEndCell, Selection& aNormalSelection) {
2484 NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER);
2485 NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER);
2486 mEndSelectedCell = aEndCell;
2488 nsresult result = NS_OK;
2490 // If new end cell is in a different table, do nothing
2491 const RefPtr<const nsIContent> table = IsInSameTable(aStartCell, aEndCell);
2492 if (!table) {
2493 return NS_OK;
2496 // Get starting and ending cells' location in the cellmap
2497 int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
2498 result = GetCellIndexes(aStartCell, startRowIndex, startColIndex);
2499 if (NS_FAILED(result)) return result;
2500 result = GetCellIndexes(aEndCell, endRowIndex, endColIndex);
2501 if (NS_FAILED(result)) return result;
2503 if (mDragSelectingCells) {
2504 // Drag selecting: remove selected cells outside of new block limits
2505 // TODO: `UnselectCells`'s return value shouldn't be ignored.
2506 UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex,
2507 true, aNormalSelection);
2510 // Note that we select block in the direction of user's mouse dragging,
2511 // which means start cell may be after the end cell in either row or column
2512 return AddCellsToSelection(table, startRowIndex, startColIndex, endRowIndex,
2513 endColIndex, aNormalSelection);
2516 nsresult nsFrameSelection::TableSelection::UnselectCells(
2517 const nsIContent* aTableContent, int32_t aStartRowIndex,
2518 int32_t aStartColumnIndex, int32_t aEndRowIndex, int32_t aEndColumnIndex,
2519 bool aRemoveOutsideOfCellRange, mozilla::dom::Selection& aNormalSelection) {
2520 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2522 nsTableWrapperFrame* tableFrame =
2523 do_QueryFrame(aTableContent->GetPrimaryFrame());
2524 if (!tableFrame) return NS_ERROR_FAILURE;
2526 int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex);
2527 int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex);
2528 int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex);
2529 int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex);
2531 // Strong reference because we sometimes remove the range
2532 RefPtr<nsRange> range = GetFirstCellRange(aNormalSelection);
2533 nsIContent* cellNode = GetFirstSelectedContent(range);
2534 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2536 int32_t curRowIndex, curColIndex;
2537 while (cellNode) {
2538 nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex);
2539 if (NS_FAILED(result)) return result;
2541 #ifdef DEBUG_TABLE_SELECTION
2542 if (!range) printf("RemoveCellsToSelection -- range is null\n");
2543 #endif
2545 if (range) {
2546 if (aRemoveOutsideOfCellRange) {
2547 if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex ||
2548 curColIndex < minColIndex || curColIndex > maxColIndex) {
2549 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2550 *range, IgnoreErrors());
2551 // Since we've removed the range, decrement pointer to next range
2552 mSelectedCellIndex--;
2555 } else {
2556 // Remove cell from selection if it belongs to the given cells range or
2557 // it is spanned onto the cells range.
2558 nsTableCellFrame* cellFrame =
2559 tableFrame->GetCellFrameAt(curRowIndex, curColIndex);
2561 uint32_t origRowIndex = cellFrame->RowIndex();
2562 uint32_t origColIndex = cellFrame->ColIndex();
2563 uint32_t actualRowSpan =
2564 tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex);
2565 uint32_t actualColSpan =
2566 tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex);
2567 if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) &&
2568 maxRowIndex >= 0 &&
2569 origRowIndex + actualRowSpan - 1 >=
2570 static_cast<uint32_t>(minRowIndex) &&
2571 origColIndex <= static_cast<uint32_t>(maxColIndex) &&
2572 maxColIndex >= 0 &&
2573 origColIndex + actualColSpan - 1 >=
2574 static_cast<uint32_t>(minColIndex)) {
2575 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2576 *range, IgnoreErrors());
2577 // Since we've removed the range, decrement pointer to next range
2578 mSelectedCellIndex--;
2583 range = GetNextCellRange(aNormalSelection);
2584 cellNode = GetFirstSelectedContent(range);
2585 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2588 return NS_OK;
2591 nsresult SelectCellElement(nsIContent* aCellElement,
2592 Selection& aNormalSelection) {
2593 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2595 nsIContent* parent = aCellElement->GetParent();
2597 // Get child offset
2598 const int32_t offset = parent->ComputeIndexOf_Deprecated(aCellElement);
2600 return CreateAndAddRange(parent, offset, aNormalSelection);
2603 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
2604 int32_t aStartRowIndex,
2605 int32_t aStartColumnIndex,
2606 int32_t aEndRowIndex,
2607 int32_t aEndColumnIndex,
2608 Selection& aNormalSelection) {
2609 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2611 nsTableWrapperFrame* tableFrame =
2612 do_QueryFrame(aTableContent->GetPrimaryFrame());
2613 if (!tableFrame) { // Check that |table| is a table.
2614 return NS_ERROR_FAILURE;
2617 nsresult result = NS_OK;
2618 uint32_t row = aStartRowIndex;
2619 while (true) {
2620 uint32_t col = aStartColumnIndex;
2621 while (true) {
2622 nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col);
2624 // Skip cells that are spanned from previous locations or are already
2625 // selected
2626 if (cellFrame) {
2627 uint32_t origRow = cellFrame->RowIndex();
2628 uint32_t origCol = cellFrame->ColIndex();
2629 if (origRow == row && origCol == col && !cellFrame->IsSelected()) {
2630 result = SelectCellElement(cellFrame->GetContent(), aNormalSelection);
2631 if (NS_FAILED(result)) {
2632 return result;
2636 // Done when we reach end column
2637 if (col == static_cast<uint32_t>(aEndColumnIndex)) {
2638 break;
2641 if (aStartColumnIndex < aEndColumnIndex) {
2642 col++;
2643 } else {
2644 col--;
2647 if (row == static_cast<uint32_t>(aEndRowIndex)) {
2648 break;
2651 if (aStartRowIndex < aEndRowIndex) {
2652 row++;
2653 } else {
2654 row--;
2657 return result;
2660 nsresult nsFrameSelection::RemoveCellsFromSelection(nsIContent* aTable,
2661 int32_t aStartRowIndex,
2662 int32_t aStartColumnIndex,
2663 int32_t aEndRowIndex,
2664 int32_t aEndColumnIndex) {
2665 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2666 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2667 if (!selection) {
2668 return NS_ERROR_NULL_POINTER;
2671 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2672 aStartColumnIndex, aEndRowIndex,
2673 aEndColumnIndex, false, *selection);
2676 nsresult nsFrameSelection::RestrictCellsToSelection(nsIContent* aTable,
2677 int32_t aStartRowIndex,
2678 int32_t aStartColumnIndex,
2679 int32_t aEndRowIndex,
2680 int32_t aEndColumnIndex) {
2681 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2682 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2683 if (!selection) {
2684 return NS_ERROR_NULL_POINTER;
2687 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2688 aStartColumnIndex, aEndRowIndex,
2689 aEndColumnIndex, true, *selection);
2692 Result<nsFrameSelection::TableSelection::FirstAndLastCell, nsresult>
2693 nsFrameSelection::TableSelection::FindFirstAndLastCellOfRowOrColumn(
2694 const nsIContent& aCellContent) const {
2695 const nsIContent* table = GetParentTable(&aCellContent);
2696 if (!table) {
2697 return Err(NS_ERROR_NULL_POINTER);
2700 // Get table and cell layout interfaces to access
2701 // cell data based on cellmap location
2702 // Frames are not ref counted, so don't use an nsCOMPtr
2703 nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame());
2704 if (!tableFrame) {
2705 return Err(NS_ERROR_FAILURE);
2707 nsITableCellLayout* cellLayout = GetCellLayout(&aCellContent);
2708 if (!cellLayout) {
2709 return Err(NS_ERROR_FAILURE);
2712 // Get location of target cell:
2713 int32_t rowIndex, colIndex;
2714 nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex);
2715 if (NS_FAILED(result)) {
2716 return Err(result);
2719 // Be sure we start at proper beginning
2720 // (This allows us to select row or col given ANY cell!)
2721 if (mMode == TableSelectionMode::Row) {
2722 colIndex = 0;
2724 if (mMode == TableSelectionMode::Column) {
2725 rowIndex = 0;
2728 FirstAndLastCell firstAndLastCell;
2729 while (true) {
2730 // Loop through all cells in column or row to find first and last
2731 nsCOMPtr<nsIContent> curCellContent =
2732 tableFrame->GetCellAt(rowIndex, colIndex);
2733 if (!curCellContent) {
2734 break;
2737 if (!firstAndLastCell.mFirst) {
2738 firstAndLastCell.mFirst = curCellContent;
2741 firstAndLastCell.mLast = std::move(curCellContent);
2743 // Move to next cell in cellmap, skipping spanned locations
2744 if (mMode == TableSelectionMode::Row) {
2745 colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
2746 } else {
2747 rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
2750 return firstAndLastCell;
2753 nsresult nsFrameSelection::TableSelection::SelectRowOrColumn(
2754 nsIContent* aCellContent, Selection& aNormalSelection) {
2755 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2757 if (!aCellContent) {
2758 return NS_ERROR_NULL_POINTER;
2761 Result<FirstAndLastCell, nsresult> firstAndLastCell =
2762 FindFirstAndLastCellOfRowOrColumn(*aCellContent);
2763 if (firstAndLastCell.isErr()) {
2764 return firstAndLastCell.unwrapErr();
2767 // Use SelectBlockOfCells:
2768 // This will replace existing selection,
2769 // but allow unselecting by dragging out of selected region
2770 if (firstAndLastCell.inspect().mFirst && firstAndLastCell.inspect().mLast) {
2771 nsresult rv{NS_OK};
2773 if (!mStartSelectedCell) {
2774 // We are starting a new block, so select the first cell
2775 rv = ::SelectCellElement(firstAndLastCell.inspect().mFirst,
2776 aNormalSelection);
2777 if (NS_FAILED(rv)) {
2778 return rv;
2780 mStartSelectedCell = firstAndLastCell.inspect().mFirst;
2783 rv = SelectBlockOfCells(mStartSelectedCell,
2784 firstAndLastCell.inspect().mLast, aNormalSelection);
2786 // This gets set to the cell at end of row/col,
2787 // but we need it to be the cell under cursor
2788 mEndSelectedCell = aCellContent;
2789 return rv;
2792 #if 0
2793 // This is a more efficient strategy that appends row to current selection,
2794 // but doesn't allow dragging OFF of an existing selection to unselect!
2795 do {
2796 // Loop through all cells in column or row
2797 result = tableLayout->GetCellDataAt(rowIndex, colIndex,
2798 getter_AddRefs(cellElement),
2799 curRowIndex, curColIndex,
2800 rowSpan, colSpan,
2801 actualRowSpan, actualColSpan,
2802 isSelected);
2803 if (NS_FAILED(result)) return result;
2804 // We're done when cell is not found
2805 if (!cellElement) break;
2808 // Check spans else we infinitely loop
2809 NS_ASSERTION(actualColSpan, "actualColSpan is 0!");
2810 NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!");
2812 // Skip cells that are already selected or span from outside our region
2813 if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex)
2815 result = SelectCellElement(cellElement);
2816 if (NS_FAILED(result)) return result;
2818 // Move to next row or column in cellmap, skipping spanned locations
2819 if (mMode == TableSelectionMode::Row)
2820 colIndex += actualColSpan;
2821 else
2822 rowIndex += actualRowSpan;
2824 while (cellElement);
2825 #endif
2827 return NS_OK;
2830 // static
2831 nsIContent* nsFrameSelection::GetFirstCellNodeInRange(const nsRange* aRange) {
2832 if (!aRange) return nullptr;
2834 nsIContent* childContent = aRange->GetChildAtStartOffset();
2835 if (!childContent) return nullptr;
2836 // Don't return node if not a cell
2837 if (!IsCell(childContent)) return nullptr;
2839 return childContent;
2842 nsRange* nsFrameSelection::TableSelection::GetFirstCellRange(
2843 const mozilla::dom::Selection& aNormalSelection) {
2844 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2846 nsRange* firstRange = aNormalSelection.GetRangeAt(0);
2847 if (!GetFirstCellNodeInRange(firstRange)) {
2848 return nullptr;
2851 // Setup for next cell
2852 mSelectedCellIndex = 1;
2854 return firstRange;
2857 nsRange* nsFrameSelection::TableSelection::GetNextCellRange(
2858 const mozilla::dom::Selection& aNormalSelection) {
2859 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2861 nsRange* range =
2862 aNormalSelection.GetRangeAt(AssertedCast<uint32_t>(mSelectedCellIndex));
2864 // Get first node in next range of selection - test if it's a cell
2865 if (!GetFirstCellNodeInRange(range)) {
2866 return nullptr;
2869 // Setup for next cell
2870 mSelectedCellIndex++;
2872 return range;
2875 // static
2876 nsresult nsFrameSelection::GetCellIndexes(const nsIContent* aCell,
2877 int32_t& aRowIndex,
2878 int32_t& aColIndex) {
2879 if (!aCell) return NS_ERROR_NULL_POINTER;
2881 aColIndex = 0; // initialize out params
2882 aRowIndex = 0;
2884 nsITableCellLayout* cellLayoutObject = GetCellLayout(aCell);
2885 if (!cellLayoutObject) return NS_ERROR_FAILURE;
2886 return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex);
2889 // static
2890 nsIContent* nsFrameSelection::IsInSameTable(const nsIContent* aContent1,
2891 const nsIContent* aContent2) {
2892 if (!aContent1 || !aContent2) return nullptr;
2894 nsIContent* tableNode1 = GetParentTable(aContent1);
2895 nsIContent* tableNode2 = GetParentTable(aContent2);
2897 // Must be in the same table. Note that we want to return false for
2898 // the test if both tables are null.
2899 return (tableNode1 == tableNode2) ? tableNode1 : nullptr;
2902 // static
2903 nsIContent* nsFrameSelection::GetParentTable(const nsIContent* aCell) {
2904 if (!aCell) {
2905 return nullptr;
2908 for (nsIContent* parent = aCell->GetParent(); parent;
2909 parent = parent->GetParent()) {
2910 if (parent->IsHTMLElement(nsGkAtoms::table)) {
2911 return parent;
2915 return nullptr;
2918 nsresult nsFrameSelection::SelectCellElement(nsIContent* aCellElement) {
2919 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2920 const RefPtr<Selection> selection = mDomSelections[index];
2921 if (!selection) {
2922 return NS_ERROR_NULL_POINTER;
2925 return ::SelectCellElement(aCellElement, *selection);
2928 nsresult CreateAndAddRange(nsINode* aContainer, int32_t aOffset,
2929 Selection& aNormalSelection) {
2930 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2932 if (!aContainer) {
2933 return NS_ERROR_NULL_POINTER;
2936 // Set range around child at given offset
2937 ErrorResult error;
2938 RefPtr<nsRange> range =
2939 nsRange::Create(aContainer, aOffset, aContainer, aOffset + 1, error);
2940 if (NS_WARN_IF(error.Failed())) {
2941 return error.StealNSResult();
2943 MOZ_ASSERT(range);
2945 ErrorResult err;
2946 aNormalSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, err);
2947 return err.StealNSResult();
2950 // End of Table Selection
2952 void nsFrameSelection::SetAncestorLimiter(nsIContent* aLimiter) {
2953 if (mLimiters.mAncestorLimiter != aLimiter) {
2954 mLimiters.mAncestorLimiter = aLimiter;
2955 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2956 if (!mDomSelections[index]) return;
2958 if (!IsValidSelectionPoint(mDomSelections[index]->GetFocusNode())) {
2959 ClearNormalSelection();
2960 if (mLimiters.mAncestorLimiter) {
2961 SetChangeReasons(nsISelectionListener::NO_REASON);
2962 nsCOMPtr<nsIContent> limiter(mLimiters.mAncestorLimiter);
2963 const nsresult rv =
2964 TakeFocus(*limiter, 0, 0, CaretAssociationHint::Before,
2965 FocusMode::kCollapseToNewPoint);
2966 Unused << NS_WARN_IF(NS_FAILED(rv));
2967 // TODO: in case of failure, propagate it to the callers.
2973 void nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) {
2974 if (aMouseEvent) {
2975 mDelayedMouseEvent.mIsValid = true;
2976 mDelayedMouseEvent.mIsShift = aMouseEvent->IsShift();
2977 mDelayedMouseEvent.mClickCount = aMouseEvent->mClickCount;
2978 } else {
2979 mDelayedMouseEvent.mIsValid = false;
2983 void nsFrameSelection::DisconnectFromPresShell() {
2984 if (mAccessibleCaretEnabled) {
2985 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2986 mDomSelections[index]->StopNotifyingAccessibleCaretEventHub();
2989 StopAutoScrollTimer();
2990 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
2991 mDomSelections[i]->Clear(nullptr);
2993 mPresShell = nullptr;
2996 #ifdef XP_MACOSX
2998 * See Bug 1288453.
3000 * Update the selection cache on repaint to handle when a pre-existing
3001 * selection becomes active aka the current selection.
3003 * 1. Change the current selection by click n dragging another selection.
3004 * - Make a selection on content page. Make a selection in a text editor.
3005 * - You can click n drag the content selection to make it active again.
3006 * 2. Change the current selection when switching to a tab with a selection.
3007 * - Make selection in tab.
3008 * - Switching tabs will make its respective selection active.
3010 * Therefore, we only update the selection cache on a repaint
3011 * if the current selection being repainted is not an empty selection.
3013 * If the current selection is empty. The current selection cache
3014 * would be cleared by AutoCopyListener::OnSelectionChange().
3016 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel) {
3017 PresShell* presShell = aSel->GetPresShell();
3018 if (!presShell) {
3019 return NS_OK;
3021 nsCOMPtr<Document> aDoc = presShell->GetDocument();
3023 if (aDoc && aSel && !aSel->IsCollapsed()) {
3024 return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3025 aSel, aDoc, nsIClipboard::kSelectionCache, false);
3028 return NS_OK;
3030 #endif // XP_MACOSX
3032 // mozilla::AutoCopyListener
3034 int16_t AutoCopyListener::sClipboardID = -1;
3037 * What we do now:
3038 * On every selection change, we copy to the clipboard anew, creating a
3039 * HTML buffer, a transferable, an nsISupportsString and
3040 * a huge mess every time. This is basically what
3041 * nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the
3042 * selection into the clipboard for Edit->Copy.
3044 * What we should do, to make our end of the deal faster:
3045 * Create a singleton transferable with our own magic converter. When selection
3046 * changes (use a quick cache to detect ``real'' changes), we put the new
3047 * Selection in the transferable. Our magic converter will take care of
3048 * transferable->whatever-other-format when the time comes to actually
3049 * hand over the clipboard contents.
3051 * Other issues:
3052 * - which X clipboard should we populate?
3053 * - should we use a different one than Edit->Copy, so that inadvertant
3054 * selections (or simple clicks, which currently cause a selection
3055 * notification, regardless of if they're in the document which currently has
3056 * selection!) don't lose the contents of the ``application''? Or should we
3057 * just put some intelligence in the ``is this a real selection?'' code to
3058 * protect our selection against clicks in other documents that don't create
3059 * selections?
3060 * - maybe we should just never clear the X clipboard? That would make this
3061 * problem just go away, which is very tempting.
3063 * On macOS,
3064 * nsIClipboard::kSelectionCache is the flag for current selection cache.
3065 * Set the current selection cache on the parent process in
3066 * widget cocoa nsClipboard whenever selection changes.
3069 // static
3070 void AutoCopyListener::OnSelectionChange(Document* aDocument,
3071 Selection& aSelection,
3072 int16_t aReason) {
3073 MOZ_ASSERT(IsValidClipboardID(sClipboardID));
3075 // For now, we should prevent any updates caused by a call of Selection API.
3076 // We should allow this in some cases later, though. See the valid usage in
3077 // bug 1567160.
3078 if (aReason & nsISelectionListener::JS_REASON) {
3079 return;
3082 if (sClipboardID == nsIClipboard::kSelectionCache) {
3083 // Do nothing if this isn't in the active window and,
3084 // in the case of Web content, in the frontmost tab.
3085 if (!aDocument || !IsInActiveTab(aDocument)) {
3086 return;
3090 static const int16_t kResasonsToHandle =
3091 nsISelectionListener::MOUSEUP_REASON |
3092 nsISelectionListener::SELECTALL_REASON |
3093 nsISelectionListener::KEYPRESS_REASON;
3094 if (!(aReason & kResasonsToHandle)) {
3095 return; // Don't care if we are still dragging.
3098 if (!aDocument || aSelection.IsCollapsed()) {
3099 #ifdef DEBUG_CLIPBOARD
3100 fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n");
3101 #endif
3102 if (sClipboardID != nsIClipboard::kSelectionCache) {
3103 // XXX Should we clear X clipboard?
3104 return;
3107 // If on macOS, clear the current selection transferable cached
3108 // on the parent process (nsClipboard) when the selection is empty.
3109 DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache();
3110 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3111 "nsCopySupport::ClearSelectionCache() failed");
3112 return;
3115 DebugOnly<nsresult> rv =
3116 nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3117 &aSelection, aDocument, sClipboardID, false);
3118 NS_WARNING_ASSERTION(
3119 NS_SUCCEEDED(rv),
3120 "nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed");