Bug 1685303: part 16) Slightly simplify code in `nsFrameSelection::TakeFocus` and...
[gecko.git] / layout / generic / nsFrameSelection.cpp
blob7adedc935535a3346c6dbb66c6cddc2ccda233de
1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 /*
8 * Implementation of nsFrameSelection
9 */
11 #include "nsFrameSelection.h"
13 #include "mozilla/Attributes.h"
14 #include "mozilla/AutoRestore.h"
15 #include "mozilla/BasePrincipal.h"
16 #include "mozilla/EventStates.h"
17 #include "mozilla/HTMLEditor.h"
18 #include "mozilla/Logging.h"
19 #include "mozilla/PresShell.h"
20 #include "mozilla/ScrollTypes.h"
21 #include "mozilla/StaticAnalysisFunctions.h"
22 #include "mozilla/StaticPrefs_bidi.h"
23 #include "mozilla/StaticPrefs_dom.h"
24 #include "mozilla/StaticPrefs_layout.h"
26 #include "nsCOMPtr.h"
27 #include "nsDebug.h"
28 #include "nsString.h"
29 #include "nsISelectionListener.h"
30 #include "nsContentCID.h"
31 #include "nsDeviceContext.h"
32 #include "nsIContent.h"
33 #include "nsRange.h"
34 #include "nsITableCellLayout.h"
35 #include "nsTArray.h"
36 #include "nsTableWrapperFrame.h"
37 #include "nsTableCellFrame.h"
38 #include "nsIScrollableFrame.h"
39 #include "nsCCUncollectableMarker.h"
40 #include "nsTextFragment.h"
41 #include <algorithm>
42 #include "nsContentUtils.h"
43 #include "nsCSSFrameConstructor.h"
45 #include "nsGkAtoms.h"
46 #include "nsIFrameTraversal.h"
47 #include "nsLayoutUtils.h"
48 #include "nsLayoutCID.h"
49 #include "nsBidiPresUtils.h"
50 static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID);
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/Selection.h"
75 #include "mozilla/dom/ShadowRoot.h"
76 #include "mozilla/dom/StaticRange.h"
77 #include "mozilla/dom/Text.h"
78 #include "mozilla/ErrorResult.h"
79 #include "mozilla/dom/SelectionBinding.h"
80 #include "mozilla/AsyncEventDispatcher.h"
81 #include "mozilla/Telemetry.h"
82 #include "mozilla/layers/ScrollInputMethods.h"
84 #include "nsFocusManager.h"
85 #include "nsPIDOMWindow.h"
87 using namespace mozilla;
88 using namespace mozilla::dom;
89 using mozilla::layers::ScrollInputMethod;
91 static LazyLogModule sFrameSelectionLog("FrameSelection");
93 //#define DEBUG_TABLE 1
95 /**
96 * Add cells to the selection inside of the given cells range.
98 * @param aTable [in] HTML table element
99 * @param aStartRowIndex [in] row index where the cells range starts
100 * @param aStartColumnIndex [in] column index where the cells range starts
101 * @param aEndRowIndex [in] row index where the cells range ends
102 * @param aEndColumnIndex [in] column index where the cells range ends
104 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
105 int32_t aStartRowIndex,
106 int32_t aStartColumnIndex,
107 int32_t aEndRowIndex,
108 int32_t aEndColumnIndex,
109 Selection& aNormalSelection);
111 static nsAtom* GetTag(nsINode* aNode);
113 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode);
114 MOZ_CAN_RUN_SCRIPT_BOUNDARY static nsresult CreateAndAddRange(
115 nsINode* aContainer, int32_t aOffset, Selection& aNormalSelection);
116 static nsresult SelectCellElement(nsIContent* aCellElement,
117 Selection& aNormalSelection);
119 #ifdef XP_MACOSX
120 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel);
121 #endif // XP_MACOSX
123 #ifdef PRINT_RANGE
124 static void printRange(nsRange* aDomRange);
125 # define DEBUG_OUT_RANGE(x) printRange(x)
126 #else
127 # define DEBUG_OUT_RANGE(x)
128 #endif // PRINT_RANGE
130 /******************************************************************************
131 * nsPeekOffsetStruct
132 ******************************************************************************/
134 //#define DEBUG_SELECTION // uncomment for printf describing every collapse and
135 // extend. #define DEBUG_NAVIGATION
137 //#define DEBUG_TABLE_SELECTION 1
139 nsPeekOffsetStruct::nsPeekOffsetStruct(
140 nsSelectionAmount aAmount, nsDirection aDirection, int32_t aStartOffset,
141 nsPoint aDesiredCaretPos, bool aJumpLines, bool aScrollViewStop,
142 bool aIsKeyboardSelect, bool aVisual, bool aExtend,
143 ForceEditableRegion aForceEditableRegion,
144 EWordMovementType aWordMovementType, bool aTrimSpaces)
145 : mAmount(aAmount),
146 mDirection(aDirection),
147 mStartOffset(aStartOffset),
148 mDesiredCaretPos(aDesiredCaretPos),
149 mWordMovementType(aWordMovementType),
150 mJumpLines(aJumpLines),
151 mTrimSpaces(aTrimSpaces),
152 mScrollViewStop(aScrollViewStop),
153 mIsKeyboardSelect(aIsKeyboardSelect),
154 mVisual(aVisual),
155 mExtend(aExtend),
156 mForceEditableRegion(aForceEditableRegion == ForceEditableRegion::Yes),
157 mResultContent(),
158 mResultFrame(nullptr),
159 mContentOffset(0),
160 mAttach(CARET_ASSOCIATE_BEFORE) {}
162 // Array which contains index of each SelecionType in Selection::mDOMSelections.
163 // For avoiding using if nor switch to retrieve the index, this needs to have
164 // -1 for SelectionTypes which won't be created its Selection instance.
165 static const int8_t kIndexOfSelections[] = {
166 -1, // SelectionType::eInvalid
167 -1, // SelectionType::eNone
168 0, // SelectionType::eNormal
169 1, // SelectionType::eSpellCheck
170 2, // SelectionType::eIMERawClause
171 3, // SelectionType::eIMESelectedRawClause
172 4, // SelectionType::eIMEConvertedClause
173 5, // SelectionType::eIMESelectedClause
174 6, // SelectionType::eAccessibility
175 7, // SelectionType::eFind
176 8, // SelectionType::eURLSecondary
177 9, // SelectionType::eURLStrikeout
180 inline int8_t GetIndexFromSelectionType(SelectionType aSelectionType) {
181 // The enum value of eInvalid is -1 and the others are sequential value
182 // starting from 0. Therefore, |SelectionType + 1| is the index of
183 // kIndexOfSelections.
184 return kIndexOfSelections[static_cast<int8_t>(aSelectionType) + 1];
188 The limiter is used specifically for the text areas and textfields
189 In that case it is the DIV tag that is anonymously created for the text
190 areas/fields. Text nodes and BR nodes fall beneath it. In the case of a
191 BR node the limiter will be the parent and the offset will point before or
192 after the BR node. In the case of the text node the parent content is
193 the text node itself and the offset will be the exact character position.
194 The offset is not important to check for validity. Simply look at the
195 passed in content. If it equals the limiter then the selection point is valid.
196 If its parent it the limiter then the point is also valid. In the case of
197 NO limiter all points are valid since you are in a topmost iframe. (browser
198 or composer)
200 bool nsFrameSelection::IsValidSelectionPoint(nsINode* aNode) const {
201 if (!aNode) {
202 return false;
205 nsIContent* limiter = GetLimiter();
206 if (limiter && limiter != aNode && limiter != aNode->GetParent()) {
207 // if newfocus == the limiter. that's ok. but if not there and not parent
208 // bad
209 return false; // not in the right content. tLimiter said so
212 limiter = GetAncestorLimiter();
213 return !limiter || aNode->IsInclusiveDescendantOf(limiter);
216 namespace mozilla {
217 struct MOZ_RAII AutoPrepareFocusRange {
218 AutoPrepareFocusRange(Selection* aSelection,
219 const bool aMultiRangeSelection) {
220 MOZ_ASSERT(aSelection);
221 MOZ_ASSERT(aSelection->GetType() == SelectionType::eNormal);
223 if (aSelection->mStyledRanges.mRanges.Length() <= 1) {
224 return;
227 if (aSelection->mFrameSelection->IsUserSelectionReason()) {
228 mUserSelect.emplace(aSelection);
231 nsTArray<StyledRange>& ranges = aSelection->mStyledRanges.mRanges;
232 if (!aSelection->mUserInitiated || aMultiRangeSelection) {
233 // Scripted command or the user is starting a new explicit multi-range
234 // selection.
235 for (StyledRange& entry : ranges) {
236 entry.mRange->SetIsGenerated(false);
238 return;
241 if (!IsAnchorRelativeOperation(
242 aSelection->mFrameSelection->mSelectionChangeReasons)) {
243 return;
246 // This operation is against the anchor but our current mAnchorFocusRange
247 // represents the focus in a multi-range selection. The anchor from a user
248 // perspective is the most distant generated range on the opposite side.
249 // Find that range and make it the mAnchorFocusRange.
250 nsRange* const newAnchorFocusRange =
251 FindGeneratedRangeMostDistantFromAnchor(*aSelection);
253 if (!newAnchorFocusRange) {
254 // There are no generated ranges - that's fine.
255 return;
258 // Setup the new mAnchorFocusRange and mark the old one as generated.
259 if (aSelection->mAnchorFocusRange) {
260 aSelection->mAnchorFocusRange->SetIsGenerated(true);
263 newAnchorFocusRange->SetIsGenerated(false);
264 aSelection->mAnchorFocusRange = newAnchorFocusRange;
266 RemoveGeneratedRanges(*aSelection);
268 if (aSelection->mFrameSelection) {
269 aSelection->mFrameSelection->InvalidateDesiredCaretPos();
273 private:
274 static nsRange* FindGeneratedRangeMostDistantFromAnchor(
275 const Selection& aSelection) {
276 const nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
277 const size_t len = ranges.Length();
278 nsRange* result{nullptr};
279 if (aSelection.GetDirection() == eDirNext) {
280 for (size_t i = 0; i < len; ++i) {
281 if (ranges[i].mRange->IsGenerated()) {
282 result = ranges[i].mRange;
283 break;
286 } else {
287 size_t i = len;
288 while (i--) {
289 if (ranges[i].mRange->IsGenerated()) {
290 result = ranges[i].mRange;
291 break;
296 return result;
299 static void RemoveGeneratedRanges(Selection& aSelection) {
300 RefPtr<nsPresContext> presContext = aSelection.GetPresContext();
301 nsTArray<StyledRange>& ranges = aSelection.mStyledRanges.mRanges;
302 size_t i = ranges.Length();
303 while (i--) {
304 nsRange* range = ranges[i].mRange;
305 if (range->IsGenerated()) {
306 range->UnregisterSelection();
307 aSelection.SelectFrames(presContext, range, false);
308 ranges.RemoveElementAt(i);
314 * @aParam aSelectionChangeReasons can be multiple of the reasons defined in
315 nsISelectionListener.idl.
317 static bool IsAnchorRelativeOperation(const int16_t aSelectionChangeReasons) {
318 return aSelectionChangeReasons &
319 (nsISelectionListener::DRAG_REASON |
320 nsISelectionListener::MOUSEDOWN_REASON |
321 nsISelectionListener::MOUSEUP_REASON |
322 nsISelectionListener::COLLAPSETOSTART_REASON);
325 Maybe<Selection::AutoUserInitiated> mUserSelect;
328 } // namespace mozilla
330 ////////////BEGIN nsFrameSelection methods
332 template Result<RefPtr<nsRange>, nsresult>
333 nsFrameSelection::CreateRangeExtendedToSomewhere(
334 nsDirection aDirection, const nsSelectionAmount aAmount,
335 CaretMovementStyle aMovementStyle);
336 template Result<RefPtr<StaticRange>, nsresult>
337 nsFrameSelection::CreateRangeExtendedToSomewhere(
338 nsDirection aDirection, const nsSelectionAmount aAmount,
339 CaretMovementStyle aMovementStyle);
341 nsFrameSelection::nsFrameSelection(PresShell* aPresShell, nsIContent* aLimiter,
342 const bool aAccessibleCaretEnabled) {
343 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
344 mDomSelections[i] = new Selection(kPresentSelectionTypes[i], this);
347 #ifdef XP_MACOSX
348 // On macOS, cache the current selection to send to service menu of macOS.
349 bool enableAutoCopy = true;
350 AutoCopyListener::Init(nsIClipboard::kSelectionCache);
351 #else // #ifdef XP_MACOSX
352 // Check to see if the auto-copy pref is enabled and make the normal
353 // Selection notifies auto-copy listener of its changes.
354 bool enableAutoCopy = AutoCopyListener::IsPrefEnabled();
355 if (enableAutoCopy) {
356 AutoCopyListener::Init(nsIClipboard::kSelectionClipboard);
358 #endif // #ifdef XP_MACOSX #else
360 if (enableAutoCopy) {
361 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
362 if (mDomSelections[index]) {
363 mDomSelections[index]->NotifyAutoCopy();
367 mPresShell = aPresShell;
368 mDragState = false;
369 mLimiters.mLimiter = aLimiter;
371 // This should only ever be initialized on the main thread, so we are OK here.
372 MOZ_ASSERT(NS_IsMainThread());
374 mAccessibleCaretEnabled = aAccessibleCaretEnabled;
375 if (mAccessibleCaretEnabled) {
376 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
377 mDomSelections[index]->MaybeNotifyAccessibleCaretEventHub(aPresShell);
380 bool plaintextControl = (aLimiter != nullptr);
381 bool initSelectEvents =
382 plaintextControl ? StaticPrefs::dom_select_events_textcontrols_enabled()
383 : StaticPrefs::dom_select_events_enabled();
385 Document* doc = aPresShell->GetDocument();
386 if (initSelectEvents || (doc && doc->NodePrincipal()->IsSystemPrincipal())) {
387 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
388 if (mDomSelections[index]) {
389 mDomSelections[index]->EnableSelectionChangeEvent();
394 nsFrameSelection::~nsFrameSelection() = default;
396 NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection)
398 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection)
399 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
400 tmp->mDomSelections[i] = nullptr;
403 NS_IMPL_CYCLE_COLLECTION_UNLINK(
404 mTableSelection.mClosestInclusiveTableCellAncestor)
405 tmp->mTableSelection.mMode = TableSelectionMode::None;
406 tmp->mTableSelection.mDragSelectingCells = false;
407 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mStartSelectedCell)
408 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mEndSelectedCell)
409 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mAppendStartSelectedCell)
410 NS_IMPL_CYCLE_COLLECTION_UNLINK(mTableSelection.mUnselectCellOnMouseUp)
411 NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainedRange.mRange)
412 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mLimiter)
413 NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiters.mAncestorLimiter)
414 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
415 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection)
416 if (tmp->mPresShell && tmp->mPresShell->GetDocument() &&
417 nsCCUncollectableMarker::InGeneration(
418 cb, tmp->mPresShell->GetDocument()->GetMarkedCCGeneration())) {
419 return NS_SUCCESS_INTERRUPTED_TRAVERSE;
421 for (size_t i = 0; i < ArrayLength(tmp->mDomSelections); ++i) {
422 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i])
425 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
426 mTableSelection.mClosestInclusiveTableCellAncestor)
427 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mStartSelectedCell)
428 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mEndSelectedCell)
429 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mAppendStartSelectedCell)
430 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mTableSelection.mUnselectCellOnMouseUp)
431 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainedRange.mRange)
432 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mLimiter)
433 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiters.mAncestorLimiter)
434 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
436 NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsFrameSelection, AddRef)
437 NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsFrameSelection, Release)
439 bool nsFrameSelection::Caret::IsVisualMovement(
440 bool aContinueSelection, CaretMovementStyle aMovementStyle) const {
441 int32_t movementFlag = StaticPrefs::bidi_edit_caret_movement_style();
442 return aMovementStyle == eVisual ||
443 (aMovementStyle == eUsePrefStyle &&
444 (movementFlag == 1 || (movementFlag == 2 && !aContinueSelection)));
447 // Get the x (or y, in vertical writing mode) position requested
448 // by the Key Handling for line-up/down
449 nsresult nsFrameSelection::DesiredCaretPos::FetchPos(
450 nsPoint& aDesiredCaretPos, const PresShell& aPresShell,
451 Selection& aNormalSelection) const {
452 MOZ_ASSERT(aNormalSelection.GetType() == SelectionType::eNormal);
454 if (mIsSet) {
455 aDesiredCaretPos = mValue;
456 return NS_OK;
459 RefPtr<nsCaret> caret = aPresShell.GetCaret();
460 if (!caret) {
461 return NS_ERROR_NULL_POINTER;
464 caret->SetSelection(&aNormalSelection);
466 nsRect coord;
467 nsIFrame* caretFrame = caret->GetGeometry(&coord);
468 if (!caretFrame) {
469 return NS_ERROR_FAILURE;
471 nsPoint viewOffset(0, 0);
472 nsView* view = nullptr;
473 caretFrame->GetOffsetFromView(viewOffset, &view);
474 if (view) {
475 coord += viewOffset;
477 aDesiredCaretPos = coord.TopLeft();
478 return NS_OK;
481 void nsFrameSelection::InvalidateDesiredCaretPos() // do not listen to
482 // mDesiredCaretPos.mValue;
483 // you must get another.
485 mDesiredCaretPos.Invalidate();
488 void nsFrameSelection::DesiredCaretPos::Invalidate() { mIsSet = false; }
490 void nsFrameSelection::DesiredCaretPos::Set(const nsPoint& aPos) {
491 mValue = aPos;
492 mIsSet = true;
495 nsresult nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree(
496 nsIFrame* aFrame, const nsPoint& aPoint, nsIFrame** aRetFrame,
497 nsPoint& aRetPoint) const {
499 // The whole point of this method is to return a frame and point that
500 // that lie within the same valid subtree as the anchor node's frame,
501 // for use with the method GetContentAndOffsetsFromPoint().
503 // A valid subtree is defined to be one where all the content nodes in
504 // the tree have a valid parent-child relationship.
506 // If the anchor frame and aFrame are in the same subtree, aFrame will
507 // be returned in aRetFrame. If they are in different subtrees, we
508 // return the frame for the root of the subtree.
511 if (!aFrame || !aRetFrame) {
512 return NS_ERROR_NULL_POINTER;
515 *aRetFrame = aFrame;
516 aRetPoint = aPoint;
519 // Get the frame and content for the selection's anchor point!
522 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
523 if (!mDomSelections[index]) {
524 return NS_ERROR_NULL_POINTER;
527 nsCOMPtr<nsIContent> anchorContent =
528 do_QueryInterface(mDomSelections[index]->GetAnchorNode());
529 if (!anchorContent) {
530 return NS_ERROR_FAILURE;
534 // Now find the root of the subtree containing the anchor's content.
537 NS_ENSURE_STATE(mPresShell);
538 RefPtr<PresShell> presShell = mPresShell;
539 nsIContent* anchorRoot = anchorContent->GetSelectionRootContent(presShell);
540 NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED);
543 // Now find the root of the subtree containing aFrame's content.
546 nsCOMPtr<nsIContent> content = aFrame->GetContent();
548 if (content) {
549 nsIContent* contentRoot = content->GetSelectionRootContent(presShell);
550 NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED);
552 if (anchorRoot == contentRoot) {
553 // If the aFrame's content isn't the capturing content, it should be
554 // a descendant. At this time, we can return simply.
555 nsIContent* capturedContent = PresShell::GetCapturingContent();
556 if (capturedContent != content) {
557 return NS_OK;
560 // Find the frame under the mouse cursor with the root frame.
561 // At this time, don't use the anchor's frame because it may not have
562 // fixed positioned frames.
563 nsIFrame* rootFrame = presShell->GetRootFrame();
564 nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame);
565 nsIFrame* cursorFrame =
566 nsLayoutUtils::GetFrameForPoint(RelativeTo{rootFrame}, ptInRoot);
568 // If the mouse cursor in on a frame which is descendant of same
569 // selection root, we can expand the selection to the frame.
570 if (cursorFrame && cursorFrame->PresShell() == presShell) {
571 nsCOMPtr<nsIContent> cursorContent = cursorFrame->GetContent();
572 NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE);
573 nsIContent* cursorContentRoot =
574 cursorContent->GetSelectionRootContent(presShell);
575 NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED);
576 if (cursorContentRoot == anchorRoot) {
577 *aRetFrame = cursorFrame;
578 aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame);
579 return NS_OK;
582 // Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse
583 // cursor is out of the window), we should use the frame of the anchor
584 // root.
589 // When we can't find a frame which is under the mouse cursor and has a same
590 // selection root as the anchor node's, we should return the selection root
591 // frame.
594 *aRetFrame = anchorRoot->GetPrimaryFrame();
596 if (!*aRetFrame) {
597 return NS_ERROR_FAILURE;
601 // Now make sure that aRetPoint is converted to the same coordinate
602 // system used by aRetFrame.
605 aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame);
607 return NS_OK;
610 void nsFrameSelection::SetCaretBidiLevelAndMaybeSchedulePaint(
611 nsBidiLevel aLevel) {
612 // If the current level is undefined, we have just inserted new text.
613 // In this case, we don't want to reset the keyboard language
614 mCaret.mBidiLevel = aLevel;
616 RefPtr<nsCaret> caret;
617 if (mPresShell && (caret = mPresShell->GetCaret())) {
618 caret->SchedulePaint();
622 nsBidiLevel nsFrameSelection::GetCaretBidiLevel() const {
623 return mCaret.mBidiLevel;
626 void nsFrameSelection::UndefineCaretBidiLevel() {
627 mCaret.mBidiLevel |= BIDI_LEVEL_UNDEFINED;
630 #ifdef PRINT_RANGE
631 void printRange(nsRange* aDomRange) {
632 if (!aDomRange) {
633 printf("NULL Range\n");
635 nsINode* startNode = aDomRange->GetStartContainer();
636 nsINode* endNode = aDomRange->GetEndContainer();
637 int32_t startOffset = aDomRange->StartOffset();
638 int32_t endOffset = aDomRange->EndOffset();
640 printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n",
641 (unsigned long)aDomRange, (unsigned long)startNode, (long)startOffset,
642 (unsigned long)endNode, (long)endOffset);
644 #endif /* PRINT_RANGE */
646 static nsAtom* GetTag(nsINode* aNode) {
647 nsCOMPtr<nsIContent> content = do_QueryInterface(aNode);
648 if (!content) {
649 MOZ_ASSERT_UNREACHABLE("bad node passed to GetTag()");
650 return nullptr;
653 return content->NodeInfo()->NameAtom();
657 * https://dom.spec.whatwg.org/#concept-tree-inclusive-ancestor.
659 static nsINode* GetClosestInclusiveTableCellAncestor(nsINode* aDomNode) {
660 if (!aDomNode) return nullptr;
661 nsINode* current = aDomNode;
662 // Start with current node and look for a table cell
663 while (current) {
664 nsAtom* tag = GetTag(current);
665 if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) return current;
666 current = current->GetParent();
668 return nullptr;
671 static nsDirection GetCaretDirection(const nsIFrame& aFrame,
672 nsDirection aDirection,
673 bool aVisualMovement) {
674 const nsBidiDirection paragraphDirection =
675 nsBidiPresUtils::ParagraphDirection(&aFrame);
676 return (aVisualMovement && paragraphDirection == NSBIDI_RTL)
677 ? nsDirection(1 - aDirection)
678 : aDirection;
681 nsresult nsFrameSelection::MoveCaret(nsDirection aDirection,
682 bool aContinueSelection,
683 const nsSelectionAmount aAmount,
684 CaretMovementStyle aMovementStyle) {
685 NS_ENSURE_STATE(mPresShell);
686 // Flush out layout, since we need it to be up to date to do caret
687 // positioning.
688 OwningNonNull<PresShell> presShell(*mPresShell);
689 presShell->FlushPendingNotifications(FlushType::Layout);
691 if (!mPresShell) {
692 return NS_OK;
695 nsPresContext* context = mPresShell->GetPresContext();
696 if (!context) {
697 return NS_ERROR_FAILURE;
700 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
701 const RefPtr<Selection> sel = mDomSelections[index];
702 if (!sel) {
703 return NS_ERROR_NULL_POINTER;
706 int32_t scrollFlags = Selection::SCROLL_FOR_CARET_MOVE;
707 if (sel->IsEditorSelection()) {
708 // If caret moves in editor, it should cause scrolling even if it's in
709 // overflow: hidden;.
710 scrollFlags |= Selection::SCROLL_OVERFLOW_HIDDEN;
713 int32_t caretStyle = StaticPrefs::layout_selection_caret_style();
714 if (caretStyle == 0
715 #ifdef XP_WIN
716 && aAmount != eSelectLine
717 #endif
719 // Put caret at the selection edge in the |aDirection| direction.
720 caretStyle = 2;
723 const bool doCollapse = !sel->IsCollapsed() && !aContinueSelection &&
724 caretStyle == 2 && aAmount <= eSelectLine;
725 if (doCollapse) {
726 if (aDirection == eDirPrevious) {
727 SetChangeReasons(nsISelectionListener::COLLAPSETOSTART_REASON);
728 mCaret.mHint = CARET_ASSOCIATE_AFTER;
729 } else {
730 SetChangeReasons(nsISelectionListener::COLLAPSETOEND_REASON);
731 mCaret.mHint = CARET_ASSOCIATE_BEFORE;
733 } else {
734 SetChangeReasons(nsISelectionListener::KEYPRESS_REASON);
737 AutoPrepareFocusRange prep(sel, false);
739 // we must keep this around and revalidate it when its just UP/DOWN
740 nsPoint desiredPos(0, 0);
742 if (aAmount == eSelectLine) {
743 nsresult result = mDesiredCaretPos.FetchPos(desiredPos, *mPresShell, *sel);
744 if (NS_FAILED(result)) {
745 return result;
747 mDesiredCaretPos.Set(desiredPos);
750 bool visualMovement =
751 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
752 nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(visualMovement);
753 if (!frame) {
754 return NS_ERROR_FAILURE;
757 Result<bool, nsresult> isIntraLineCaretMove = IsIntraLineCaretMove(aAmount);
758 nsDirection direction{aDirection};
759 if (isIntraLineCaretMove.isErr()) {
760 return isIntraLineCaretMove.unwrapErr();
762 if (isIntraLineCaretMove.inspect()) {
763 // Forget old caret position for moving caret to different line since
764 // caret position may be changed.
765 mDesiredCaretPos.Invalidate();
766 direction = GetCaretDirection(*frame, aDirection, visualMovement);
769 if (doCollapse) {
770 const nsRange* anchorFocusRange = sel->GetAnchorFocusRange();
771 if (anchorFocusRange) {
772 RefPtr<nsINode> node;
773 int32_t offset;
774 if (visualMovement && nsBidiPresUtils::IsReversedDirectionFrame(frame)) {
775 direction = nsDirection(1 - direction);
777 if (direction == eDirPrevious) {
778 node = anchorFocusRange->GetStartContainer();
779 offset = anchorFocusRange->StartOffset();
780 } else {
781 node = anchorFocusRange->GetEndContainer();
782 offset = anchorFocusRange->EndOffset();
784 sel->CollapseInLimiter(node, offset);
786 sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
787 ScrollAxis(), ScrollAxis(), scrollFlags);
788 return NS_OK;
791 CaretAssociateHint tHint(mCaret.mHint); // temporary variable so we dont set
792 // mCaret.mHint until it is necessary
794 Result<nsPeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
795 direction, aContinueSelection, aAmount, aMovementStyle, desiredPos);
796 nsresult rv;
797 if (result.isOk() && result.inspect().mResultContent) {
798 const nsPeekOffsetStruct& pos = result.inspect();
799 nsIFrame* theFrame;
800 int32_t currentOffset, frameStart, frameEnd;
802 if (aAmount <= eSelectWordNoSpace) {
803 // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does
804 // not set pos.mAttachForward, so determine the hint here based on the
805 // result frame and offset: If we're at the end of a text frame, set the
806 // hint to ASSOCIATE_BEFORE to indicate that we want the caret displayed
807 // at the end of this frame, not at the beginning of the next one.
808 theFrame = pos.mResultFrame;
809 theFrame->GetOffsets(frameStart, frameEnd);
810 currentOffset = pos.mContentOffset;
811 if (frameEnd == currentOffset && !(frameStart == 0 && frameEnd == 0))
812 tHint = CARET_ASSOCIATE_BEFORE;
813 else
814 tHint = CARET_ASSOCIATE_AFTER;
815 } else {
816 // For up/down and home/end, pos.mResultFrame might not be set correctly,
817 // or not at all. In these cases, get the frame based on the content and
818 // hint returned by PeekOffset().
819 tHint = pos.mAttach;
820 theFrame = GetFrameForNodeOffset(pos.mResultContent, pos.mContentOffset,
821 tHint, &currentOffset);
822 if (!theFrame) return NS_ERROR_FAILURE;
824 theFrame->GetOffsets(frameStart, frameEnd);
827 if (context->BidiEnabled()) {
828 switch (aAmount) {
829 case eSelectBeginLine:
830 case eSelectEndLine: {
831 // In Bidi contexts, PeekOffset calculates pos.mContentOffset
832 // differently depending on whether the movement is visual or logical.
833 // For visual movement, pos.mContentOffset depends on the direction-
834 // ality of the first/last frame on the line (theFrame), and the caret
835 // directionality must correspond.
836 FrameBidiData bidiData = theFrame->GetBidiData();
837 SetCaretBidiLevelAndMaybeSchedulePaint(
838 visualMovement ? bidiData.embeddingLevel : bidiData.baseLevel);
839 break;
841 default:
842 // If the current position is not a frame boundary, it's enough just
843 // to take the Bidi level of the current frame
844 if ((pos.mContentOffset != frameStart &&
845 pos.mContentOffset != frameEnd) ||
846 eSelectLine == aAmount) {
847 SetCaretBidiLevelAndMaybeSchedulePaint(
848 theFrame->GetEmbeddingLevel());
849 } else {
850 BidiLevelFromMove(mPresShell, pos.mResultContent,
851 pos.mContentOffset, aAmount, tHint);
855 // "pos" is on the stack, so pos.mResultContent has stack lifetime, so using
856 // MOZ_KnownLive is ok.
857 const FocusMode focusMode = aContinueSelection
858 ? FocusMode::kExtendSelection
859 : FocusMode::kCollapseToNewPoint;
860 rv = TakeFocus(MOZ_KnownLive(pos.mResultContent), pos.mContentOffset,
861 pos.mContentOffset, tHint, focusMode);
862 } else if (aAmount <= eSelectWordNoSpace && direction == eDirNext &&
863 !aContinueSelection) {
864 // Collapse selection if PeekOffset failed, we either
865 // 1. bumped into the BRFrame, bug 207623
866 // 2. had select-all in a text input (DIV range), bug 352759.
867 bool isBRFrame = frame->IsBrFrame();
868 RefPtr<nsINode> node = sel->GetFocusNode();
869 sel->CollapseInLimiter(node, sel->FocusOffset());
870 // Note: 'frame' might be dead here.
871 if (!isBRFrame) {
872 mCaret.mHint = CARET_ASSOCIATE_BEFORE; // We're now at the end of the
873 // frame to the left.
875 rv = NS_OK;
876 } else {
877 rv = result.isErr() ? result.unwrapErr() : NS_OK;
879 if (NS_SUCCEEDED(rv)) {
880 rv = sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION,
881 ScrollAxis(), ScrollAxis(), scrollFlags);
884 return rv;
887 Result<nsPeekOffsetStruct, nsresult> nsFrameSelection::PeekOffsetForCaretMove(
888 nsDirection aDirection, bool aContinueSelection,
889 const nsSelectionAmount aAmount, CaretMovementStyle aMovementStyle,
890 const nsPoint& aDesiredCaretPos) const {
891 Selection* selection =
892 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
893 if (!selection) {
894 return Err(NS_ERROR_NULL_POINTER);
897 const bool visualMovement =
898 mCaret.IsVisualMovement(aContinueSelection, aMovementStyle);
900 int32_t offsetused = 0;
901 nsIFrame* frame =
902 selection->GetPrimaryFrameForFocusNode(visualMovement, &offsetused);
903 if (!frame) {
904 return Err(NS_ERROR_FAILURE);
907 const auto kForceEditableRegion =
908 selection->IsEditorSelection()
909 ? nsPeekOffsetStruct::ForceEditableRegion::Yes
910 : nsPeekOffsetStruct::ForceEditableRegion::No;
912 // set data using mLimiters.mLimiter to stop on scroll views. If we have a
913 // limiter then we stop peeking when we hit scrollable views. If no limiter
914 // then just let it go ahead
915 nsPeekOffsetStruct pos(aAmount, aDirection, offsetused, aDesiredCaretPos,
916 true, !!mLimiters.mLimiter, true, visualMovement,
917 aContinueSelection, kForceEditableRegion);
918 nsresult rv = frame->PeekOffset(&pos);
919 if (NS_FAILED(rv)) {
920 return Err(rv);
922 return pos;
925 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
926 nsIContent* aNode, uint32_t aContentOffset, bool aJumpLines) const {
927 return GetPrevNextBidiLevels(aNode, aContentOffset, mCaret.mHint, aJumpLines);
930 // static
931 nsPrevNextBidiLevels nsFrameSelection::GetPrevNextBidiLevels(
932 nsIContent* aNode, uint32_t aContentOffset, CaretAssociateHint aHint,
933 bool aJumpLines) {
934 // Get the level of the frames on each side
935 nsIFrame* currentFrame;
936 int32_t currentOffset;
937 int32_t frameStart, frameEnd;
938 nsDirection direction;
940 nsPrevNextBidiLevels levels;
941 levels.SetData(nullptr, nullptr, 0, 0);
943 currentFrame =
944 GetFrameForNodeOffset(aNode, aContentOffset, aHint, &currentOffset);
945 if (!currentFrame) return levels;
947 currentFrame->GetOffsets(frameStart, frameEnd);
949 if (0 == frameStart && 0 == frameEnd)
950 direction = eDirPrevious;
951 else if (frameStart == currentOffset)
952 direction = eDirPrevious;
953 else if (frameEnd == currentOffset)
954 direction = eDirNext;
955 else {
956 // we are neither at the beginning nor at the end of the frame, so we have
957 // no worries
958 nsBidiLevel currentLevel = currentFrame->GetEmbeddingLevel();
959 levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel);
960 return levels;
963 nsIFrame* newFrame =
964 currentFrame
965 ->GetFrameFromDirection(direction, false, aJumpLines, true, false)
966 .mFrame;
968 FrameBidiData currentBidi = currentFrame->GetBidiData();
969 nsBidiLevel currentLevel = currentBidi.embeddingLevel;
970 nsBidiLevel newLevel =
971 newFrame ? newFrame->GetEmbeddingLevel() : currentBidi.baseLevel;
973 // If not jumping lines, disregard br frames, since they might be positioned
974 // incorrectly.
975 // XXX This could be removed once bug 339786 is fixed.
976 if (!aJumpLines) {
977 if (currentFrame->IsBrFrame()) {
978 currentFrame = nullptr;
979 currentLevel = currentBidi.baseLevel;
981 if (newFrame && newFrame->IsBrFrame()) {
982 newFrame = nullptr;
983 newLevel = currentBidi.baseLevel;
987 if (direction == eDirNext)
988 levels.SetData(currentFrame, newFrame, currentLevel, newLevel);
989 else
990 levels.SetData(newFrame, currentFrame, newLevel, currentLevel);
992 return levels;
995 nsresult nsFrameSelection::GetFrameFromLevel(nsIFrame* aFrameIn,
996 nsDirection aDirection,
997 nsBidiLevel aBidiLevel,
998 nsIFrame** aFrameOut) const {
999 NS_ENSURE_STATE(mPresShell);
1000 nsBidiLevel foundLevel = 0;
1001 nsIFrame* foundFrame = aFrameIn;
1003 nsCOMPtr<nsIFrameEnumerator> frameTraversal;
1004 nsresult result;
1005 nsCOMPtr<nsIFrameTraversal> trav(
1006 do_CreateInstance(kFrameTraversalCID, &result));
1007 if (NS_FAILED(result)) return result;
1009 result =
1010 trav->NewFrameTraversal(getter_AddRefs(frameTraversal),
1011 mPresShell->GetPresContext(), aFrameIn, eLeaf,
1012 false, // aVisual
1013 false, // aLockInScrollView
1014 false, // aFollowOOFs
1015 false // aSkipPopupChecks
1017 if (NS_FAILED(result)) return result;
1019 do {
1020 *aFrameOut = foundFrame;
1021 foundFrame = frameTraversal->Traverse(aDirection == eDirNext);
1022 if (!foundFrame) return NS_ERROR_FAILURE;
1023 foundLevel = foundFrame->GetEmbeddingLevel();
1025 } while (foundLevel > aBidiLevel);
1027 return NS_OK;
1030 nsresult nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) {
1031 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1032 if (!mDomSelections[index]) {
1033 return NS_ERROR_NULL_POINTER;
1036 mMaintainedRange.MaintainAnchorFocusRange(*mDomSelections[index], aAmount);
1038 return NS_OK;
1041 void nsFrameSelection::BidiLevelFromMove(PresShell* aPresShell,
1042 nsIContent* aNode,
1043 uint32_t aContentOffset,
1044 nsSelectionAmount aAmount,
1045 CaretAssociateHint aHint) {
1046 switch (aAmount) {
1047 // Movement within the line: the new cursor Bidi level is the level of the
1048 // last character moved over
1049 case eSelectCharacter:
1050 case eSelectCluster:
1051 case eSelectWord:
1052 case eSelectWordNoSpace:
1053 case eSelectBeginLine:
1054 case eSelectEndLine:
1055 case eSelectNoAmount: {
1056 nsPrevNextBidiLevels levels =
1057 GetPrevNextBidiLevels(aNode, aContentOffset, aHint, false);
1059 SetCaretBidiLevelAndMaybeSchedulePaint(aHint == CARET_ASSOCIATE_BEFORE
1060 ? levels.mLevelBefore
1061 : levels.mLevelAfter);
1062 break;
1065 // Up and Down: the new cursor Bidi level is the smaller of the two
1066 surrounding characters case eSelectLine: case eSelectParagraph:
1067 GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame,
1068 &secondFrame, &firstLevel, &secondLevel);
1069 aPresShell->SetCaretBidiLevelAndMaybeSchedulePaint(std::min(firstLevel,
1070 secondLevel)); break;
1073 default:
1074 UndefineCaretBidiLevel();
1078 void nsFrameSelection::BidiLevelFromClick(nsIContent* aNode,
1079 uint32_t aContentOffset) {
1080 nsIFrame* clickInFrame = nullptr;
1081 int32_t OffsetNotUsed;
1083 clickInFrame = GetFrameForNodeOffset(aNode, aContentOffset, mCaret.mHint,
1084 &OffsetNotUsed);
1085 if (!clickInFrame) return;
1087 SetCaretBidiLevelAndMaybeSchedulePaint(clickInFrame->GetEmbeddingLevel());
1090 void nsFrameSelection::MaintainedRange::AdjustNormalSelection(
1091 const nsIContent* aContent, const int32_t aOffset,
1092 Selection& aNormalSelection) const {
1093 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1095 if (!mRange || !aContent) {
1096 return;
1099 nsINode* rangeStartNode = mRange->GetStartContainer();
1100 nsINode* rangeEndNode = mRange->GetEndContainer();
1101 int32_t rangeStartOffset = mRange->StartOffset();
1102 int32_t rangeEndOffset = mRange->EndOffset();
1104 const Maybe<int32_t> relToStart = nsContentUtils::ComparePoints(
1105 rangeStartNode, rangeStartOffset, aContent, aOffset);
1106 if (NS_WARN_IF(!relToStart)) {
1107 // Potentially handle this properly when Selection across Shadow DOM
1108 // boundary is implemented
1109 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1110 return;
1113 const Maybe<int32_t> relToEnd = nsContentUtils::ComparePoints(
1114 rangeEndNode, rangeEndOffset, aContent, aOffset);
1115 if (NS_WARN_IF(!relToEnd)) {
1116 // Potentially handle this properly when Selection across Shadow DOM
1117 // boundary is implemented
1118 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1119 return;
1122 // If aContent/aOffset is inside (or at the edge of) the maintained
1123 // selection, or if it is on the "anchor" side of the maintained selection,
1124 // we need to do something.
1125 if ((*relToStart <= 0 && *relToEnd >= 0) ||
1126 (*relToStart > 0 && aNormalSelection.GetDirection() == eDirNext) ||
1127 (*relToEnd < 0 && aNormalSelection.GetDirection() == eDirPrevious)) {
1128 // Set the current range to the maintained range.
1129 aNormalSelection.ReplaceAnchorFocusRange(mRange);
1130 // Set the direction of the selection so that the anchor will be on the
1131 // far side of the maintained selection, relative to aContent/aOffset.
1132 aNormalSelection.SetDirection(*relToStart > 0 ? eDirPrevious : eDirNext);
1136 void nsFrameSelection::MaintainedRange::AdjustContentOffsets(
1137 nsIFrame::ContentOffsets& aOffsets, const bool aScrollViewStop) const {
1138 // Adjust offsets according to maintained amount
1139 if (mRange && mAmount != eSelectNoAmount) {
1140 nsINode* rangenode = mRange->GetStartContainer();
1141 int32_t rangeOffset = mRange->StartOffset();
1142 const Maybe<int32_t> relativePosition = nsContentUtils::ComparePoints(
1143 rangenode, rangeOffset, aOffsets.content, aOffsets.offset);
1144 if (NS_WARN_IF(!relativePosition)) {
1145 // Potentially handle this properly when Selection across Shadow DOM
1146 // boundary is implemented
1147 // (https://bugzilla.mozilla.org/show_bug.cgi?id=1607497).
1148 return;
1151 nsDirection direction = *relativePosition > 0 ? eDirPrevious : eDirNext;
1152 nsSelectionAmount amount = mAmount;
1153 if (amount == eSelectBeginLine && direction == eDirNext) {
1154 amount = eSelectEndLine;
1157 int32_t offset;
1158 nsIFrame* frame = GetFrameForNodeOffset(aOffsets.content, aOffsets.offset,
1159 CARET_ASSOCIATE_AFTER, &offset);
1161 if (frame && amount == eSelectWord && direction == eDirPrevious) {
1162 // To avoid selecting the previous word when at start of word,
1163 // first move one character forward.
1164 nsPeekOffsetStruct charPos(eSelectCharacter, eDirNext, offset,
1165 nsPoint(0, 0), false, aScrollViewStop, false,
1166 false, false);
1167 if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) {
1168 frame = charPos.mResultFrame;
1169 offset = charPos.mContentOffset;
1173 nsPeekOffsetStruct pos(amount, direction, offset, nsPoint(0, 0), false,
1174 aScrollViewStop, false, false, false);
1176 if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) {
1177 aOffsets.content = pos.mResultContent;
1178 aOffsets.offset = pos.mContentOffset;
1183 void nsFrameSelection::MaintainedRange::MaintainAnchorFocusRange(
1184 const Selection& aNormalSelection, const nsSelectionAmount aAmount) {
1185 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
1187 mAmount = aAmount;
1189 const nsRange* anchorFocusRange = aNormalSelection.GetAnchorFocusRange();
1190 if (anchorFocusRange && aAmount != eSelectNoAmount) {
1191 mRange = anchorFocusRange->CloneRange();
1192 return;
1195 mRange = nullptr;
1198 nsresult nsFrameSelection::HandleClick(nsIContent* aNewFocus,
1199 uint32_t aContentOffset,
1200 uint32_t aContentEndOffset,
1201 const FocusMode aFocusMode,
1202 CaretAssociateHint aHint) {
1203 if (!aNewFocus) return NS_ERROR_INVALID_ARG;
1205 if (MOZ_LOG_TEST(sFrameSelectionLog, LogLevel::Debug)) {
1206 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1207 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1208 ("%s: selection=%p, new focus=%p, offsets=(%u,%u), focus mode=%i",
1209 __FUNCTION__,
1210 mDomSelections[index] ? mDomSelections[index].get() : nullptr,
1211 aNewFocus, aContentOffset, aContentEndOffset,
1212 static_cast<int>(aFocusMode)));
1215 mDesiredCaretPos.Invalidate();
1217 if (aFocusMode != FocusMode::kExtendSelection) {
1218 mMaintainedRange.mRange = nullptr;
1219 if (!IsValidSelectionPoint(aNewFocus)) {
1220 mLimiters.mAncestorLimiter = nullptr;
1224 // Don't take focus when dragging off of a table
1225 if (!mTableSelection.mDragSelectingCells) {
1226 BidiLevelFromClick(aNewFocus, aContentOffset);
1227 SetChangeReasons(nsISelectionListener::MOUSEDOWN_REASON +
1228 nsISelectionListener::DRAG_REASON);
1230 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1231 RefPtr<Selection> selection = mDomSelections[index];
1232 MOZ_ASSERT(selection);
1234 if (aFocusMode == FocusMode::kExtendSelection) {
1235 mMaintainedRange.AdjustNormalSelection(aNewFocus, aContentOffset,
1236 *selection);
1239 AutoPrepareFocusRange prep(selection,
1240 aFocusMode == FocusMode::kMultiRangeSelection);
1241 return TakeFocus(aNewFocus, aContentOffset, aContentEndOffset, aHint,
1242 aFocusMode);
1245 return NS_OK;
1248 void nsFrameSelection::HandleDrag(nsIFrame* aFrame, const nsPoint& aPoint) {
1249 if (!aFrame || !mPresShell) {
1250 return;
1253 nsresult result;
1254 nsIFrame* newFrame = 0;
1255 nsPoint newPoint;
1257 result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame,
1258 newPoint);
1259 if (NS_FAILED(result)) return;
1260 if (!newFrame) return;
1262 nsIFrame::ContentOffsets offsets =
1263 newFrame->GetContentOffsetsFromPoint(newPoint);
1264 if (!offsets.content) return;
1266 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1267 RefPtr<Selection> selection = mDomSelections[index];
1268 if (newFrame->IsSelected() && selection) {
1269 // `MOZ_KnownLive` required because of
1270 // https://bugzilla.mozilla.org/show_bug.cgi?id=1636889.
1271 mMaintainedRange.AdjustNormalSelection(MOZ_KnownLive(offsets.content),
1272 offsets.offset, *selection);
1275 const bool scrollViewStop = mLimiters.mLimiter != nullptr;
1276 mMaintainedRange.AdjustContentOffsets(offsets, scrollViewStop);
1278 // TODO: no click has happened, rename `HandleClick`.
1279 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */, offsets.offset,
1280 offsets.offset, FocusMode::kExtendSelection, offsets.associate);
1283 nsresult nsFrameSelection::StartAutoScrollTimer(nsIFrame* aFrame,
1284 const nsPoint& aPoint,
1285 uint32_t aDelay) {
1286 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1287 if (!mDomSelections[index]) {
1288 return NS_ERROR_NULL_POINTER;
1291 RefPtr<Selection> selection = mDomSelections[index];
1292 return selection->StartAutoScrollTimer(aFrame, aPoint, aDelay);
1295 void nsFrameSelection::StopAutoScrollTimer() {
1296 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1297 if (!mDomSelections[index]) {
1298 return;
1301 mDomSelections[index]->StopAutoScrollTimer();
1304 // static
1305 nsINode* nsFrameSelection::TableSelection::IsContentInActivelyEditableTableCell(
1306 nsPresContext* aContext, nsIContent* aContent) {
1307 if (!aContext) {
1308 return nullptr;
1311 RefPtr<HTMLEditor> htmlEditor = nsContentUtils::GetHTMLEditor(aContext);
1312 if (!htmlEditor) {
1313 return nullptr;
1316 nsINode* inclusiveTableCellAncestor =
1317 GetClosestInclusiveTableCellAncestor(aContent);
1318 if (!inclusiveTableCellAncestor) {
1319 return nullptr;
1322 const Element* editorHostNode = htmlEditor->GetActiveEditingHost();
1323 if (!editorHostNode) {
1324 return nullptr;
1327 const bool editableCell =
1328 inclusiveTableCellAncestor->IsInclusiveDescendantOf(editorHostNode);
1329 return editableCell ? inclusiveTableCellAncestor : nullptr;
1332 namespace {
1333 struct ParentAndOffset {
1334 explicit ParentAndOffset(const nsINode& aNode)
1335 : mParent{aNode.GetParent()},
1336 mOffset{mParent ? mParent->ComputeIndexOf(&aNode) : 0} {}
1338 nsINode* mParent;
1340 // 0, if there's no parent.
1341 int32_t mOffset;
1344 } // namespace
1346 hard to go from nodes to frames, easy the other way!
1348 nsresult nsFrameSelection::TakeFocus(nsIContent* const aNewFocus,
1349 uint32_t aContentOffset,
1350 uint32_t aContentEndOffset,
1351 CaretAssociateHint aHint,
1352 const FocusMode aFocusMode) {
1353 if (!aNewFocus) {
1354 return NS_ERROR_NULL_POINTER;
1357 NS_ENSURE_STATE(mPresShell);
1359 if (!IsValidSelectionPoint(aNewFocus)) {
1360 return NS_ERROR_FAILURE;
1363 MOZ_LOG(sFrameSelectionLog, LogLevel::Verbose,
1364 ("%s: new focus=%p, offsets=(%u, %u), hint=%i, focusMode=%i",
1365 __FUNCTION__, aNewFocus, aContentOffset, aContentEndOffset,
1366 static_cast<int>(aHint), static_cast<int>(aFocusMode)));
1368 mPresShell->FrameSelectionWillTakeFocus(*this);
1370 // Clear all table selection data
1371 mTableSelection.mMode = TableSelectionMode::None;
1372 mTableSelection.mDragSelectingCells = false;
1373 mTableSelection.mStartSelectedCell = nullptr;
1374 mTableSelection.mEndSelectedCell = nullptr;
1375 mTableSelection.mAppendStartSelectedCell = nullptr;
1376 mCaret.mHint = aHint;
1378 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1379 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1381 Maybe<Selection::AutoUserInitiated> userSelect;
1382 if (IsUserSelectionReason()) {
1383 userSelect.emplace(mDomSelections[index]);
1386 // traverse through document and unselect crap here
1387 switch (aFocusMode) {
1388 case FocusMode::kCollapseToNewPoint:
1389 [[fallthrough]];
1390 case FocusMode::kMultiRangeSelection: {
1391 // single click? setting cursor down
1392 const Batching saveBatching =
1393 mBatching; // hack to use the collapse code.
1394 mBatching.mCounter = 1;
1396 if (aFocusMode == FocusMode::kMultiRangeSelection) {
1397 // Remove existing collapsed ranges as there's no point in having
1398 // non-anchor/focus collapsed ranges.
1399 mDomSelections[index]->RemoveCollapsedRanges();
1401 ErrorResult error;
1402 RefPtr<nsRange> newRange = nsRange::Create(
1403 aNewFocus, aContentOffset, aNewFocus, aContentOffset, error);
1404 if (NS_WARN_IF(error.Failed())) {
1405 return error.StealNSResult();
1407 MOZ_ASSERT(newRange);
1408 const RefPtr<Selection> selection{mDomSelections[index]};
1409 selection->AddRangeAndSelectFramesAndNotifyListeners(*newRange,
1410 IgnoreErrors());
1411 } else {
1412 bool oldDesiredPosSet =
1413 mDesiredCaretPos.mIsSet; // need to keep old desired
1414 // position if it was set.
1415 RefPtr<Selection> selection = mDomSelections[index];
1416 selection->CollapseInLimiter(aNewFocus, aContentOffset);
1417 mDesiredCaretPos.mIsSet =
1418 oldDesiredPosSet; // now reset desired pos back.
1421 mBatching = saveBatching;
1423 if (aContentEndOffset != aContentOffset) {
1424 mDomSelections[index]->Extend(aNewFocus, aContentEndOffset);
1427 // find out if we are inside a table. if so, find out which one and which
1428 // cell once we do that, the next time we get a takefocus, check the
1429 // parent tree. if we are no longer inside same table ,cell then switch to
1430 // table selection mode. BUT only do this in an editor
1432 NS_ENSURE_STATE(mPresShell);
1433 RefPtr<nsPresContext> context = mPresShell->GetPresContext();
1434 mTableSelection.mClosestInclusiveTableCellAncestor = nullptr;
1435 if (nsINode* inclusiveTableCellAncestor =
1436 TableSelection::IsContentInActivelyEditableTableCell(context,
1437 aNewFocus)) {
1438 mTableSelection.mClosestInclusiveTableCellAncestor =
1439 inclusiveTableCellAncestor;
1440 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1441 ("%s: Collapsing into new cell", __FUNCTION__));
1444 break;
1446 case FocusMode::kExtendSelection: {
1447 // Now update the range list:
1448 nsINode* inclusiveTableCellAncestor =
1449 GetClosestInclusiveTableCellAncestor(aNewFocus);
1450 if (mTableSelection.mClosestInclusiveTableCellAncestor &&
1451 inclusiveTableCellAncestor &&
1452 inclusiveTableCellAncestor !=
1453 mTableSelection
1454 .mClosestInclusiveTableCellAncestor) // switch to cell
1455 // selection mode
1457 MOZ_LOG(sFrameSelectionLog, LogLevel::Debug,
1458 ("%s: moving into new cell", __FUNCTION__));
1460 WidgetMouseEvent event(false, eVoidEvent, nullptr,
1461 WidgetMouseEvent::eReal);
1463 // Start selecting in the cell we were in before
1464 ParentAndOffset parentAndOffset{
1465 *mTableSelection.mClosestInclusiveTableCellAncestor};
1466 if (parentAndOffset.mParent) {
1467 const nsresult result = HandleTableSelection(
1468 parentAndOffset.mParent, parentAndOffset.mOffset,
1469 TableSelectionMode::Cell, &event);
1470 if (NS_WARN_IF(NS_FAILED(result))) {
1471 return result;
1475 // Find the parent of this new cell and extend selection to it
1476 parentAndOffset = ParentAndOffset{*inclusiveTableCellAncestor};
1478 // XXXX We need to REALLY get the current key shift state
1479 // (we'd need to add event listener -- let's not bother for now)
1480 event.mModifiers &= ~MODIFIER_SHIFT; // aContinueSelection;
1481 if (parentAndOffset.mParent) {
1482 mTableSelection.mClosestInclusiveTableCellAncestor =
1483 inclusiveTableCellAncestor;
1484 // Continue selection into next cell
1485 const nsresult result = HandleTableSelection(
1486 parentAndOffset.mParent, parentAndOffset.mOffset,
1487 TableSelectionMode::Cell, &event);
1488 if (NS_WARN_IF(NS_FAILED(result))) {
1489 return result;
1492 } else {
1493 // XXXX Problem: Shift+click in browser is appending text selection to
1494 // selected table!!!
1495 // is this the place to erase seleced cells ?????
1496 if (mDomSelections[index]->GetDirection() == eDirNext &&
1497 aContentEndOffset > aContentOffset) // didn't go far enough
1499 mDomSelections[index]->Extend(
1500 aNewFocus, aContentEndOffset); // this will only redraw the diff
1501 } else
1502 mDomSelections[index]->Extend(aNewFocus, aContentOffset);
1504 break;
1508 // Don't notify selection listeners if batching is on:
1509 if (IsBatching()) {
1510 return NS_OK;
1513 // Be aware, the Selection instance may be destroyed after this call.
1514 return NotifySelectionListeners(SelectionType::eNormal);
1517 UniquePtr<SelectionDetails> nsFrameSelection::LookUpSelection(
1518 nsIContent* aContent, int32_t aContentOffset, int32_t aContentLength,
1519 bool aSlowCheck) const {
1520 if (!aContent || !mPresShell) {
1521 return nullptr;
1524 UniquePtr<SelectionDetails> details;
1526 for (size_t j = 0; j < ArrayLength(mDomSelections); j++) {
1527 if (mDomSelections[j]) {
1528 details = mDomSelections[j]->LookUpSelection(
1529 aContent, aContentOffset, aContentLength, std::move(details),
1530 kPresentSelectionTypes[j], aSlowCheck);
1534 return details;
1537 void nsFrameSelection::SetDragState(bool aState) {
1538 if (mDragState == aState) return;
1540 mDragState = aState;
1542 if (!mDragState) {
1543 mTableSelection.mDragSelectingCells = false;
1544 // Notify that reason is mouse up.
1545 SetChangeReasons(nsISelectionListener::MOUSEUP_REASON);
1546 // Be aware, the Selection instance may be destroyed after this call.
1547 NotifySelectionListeners(SelectionType::eNormal);
1551 Selection* nsFrameSelection::GetSelection(SelectionType aSelectionType) const {
1552 int8_t index = GetIndexFromSelectionType(aSelectionType);
1553 if (index < 0) return nullptr;
1555 return mDomSelections[index];
1558 nsresult nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType,
1559 SelectionRegion aRegion,
1560 int16_t aFlags) const {
1561 int8_t index = GetIndexFromSelectionType(aSelectionType);
1562 if (index < 0) return NS_ERROR_INVALID_ARG;
1564 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1566 ScrollAxis verticalScroll = ScrollAxis();
1567 int32_t flags = Selection::SCROLL_DO_FLUSH;
1568 if (aFlags & nsISelectionController::SCROLL_SYNCHRONOUS) {
1569 flags |= Selection::SCROLL_SYNCHRONOUS;
1570 } else if (aFlags & nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY) {
1571 flags |= Selection::SCROLL_FIRST_ANCESTOR_ONLY;
1573 if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) {
1574 flags |= Selection::SCROLL_OVERFLOW_HIDDEN;
1576 if (aFlags & nsISelectionController::SCROLL_CENTER_VERTICALLY) {
1577 verticalScroll =
1578 ScrollAxis(kScrollToCenter, WhenToScroll::IfNotFullyVisible);
1580 if (aFlags & nsISelectionController::SCROLL_FOR_CARET_MOVE) {
1581 flags |= Selection::SCROLL_FOR_CARET_MOVE;
1584 // After ScrollSelectionIntoView(), the pending notifications might be
1585 // flushed and PresShell/PresContext/Frames may be dead. See bug 418470.
1586 RefPtr<Selection> sel = mDomSelections[index];
1587 return sel->ScrollIntoView(aRegion, verticalScroll, ScrollAxis(), flags);
1590 nsresult nsFrameSelection::RepaintSelection(SelectionType aSelectionType) {
1591 int8_t index = GetIndexFromSelectionType(aSelectionType);
1592 if (index < 0) return NS_ERROR_INVALID_ARG;
1593 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
1594 NS_ENSURE_STATE(mPresShell);
1596 // On macOS, update the selection cache to the new active selection
1597 // aka the current selection.
1598 #ifdef XP_MACOSX
1599 // Check that we're in the an active window and, if this is Web content,
1600 // in the frontmost tab.
1601 Document* doc = mPresShell->GetDocument();
1602 if (doc && IsInActiveTab(doc) && aSelectionType == SelectionType::eNormal) {
1603 UpdateSelectionCacheOnRepaintSelection(mDomSelections[index]);
1605 #endif
1606 return mDomSelections[index]->Repaint(mPresShell->GetPresContext());
1609 static bool IsDisplayContents(const nsIContent* aContent) {
1610 return aContent->IsElement() && aContent->AsElement()->IsDisplayContents();
1613 // static
1614 nsIFrame* nsFrameSelection::GetFrameForNodeOffset(nsIContent* aNode,
1615 int32_t aOffset,
1616 CaretAssociateHint aHint,
1617 int32_t* aReturnOffset) {
1618 if (!aNode || !aReturnOffset) return nullptr;
1620 if (aOffset < 0) return nullptr;
1622 if (!aNode->GetPrimaryFrame() && !IsDisplayContents(aNode)) {
1623 return nullptr;
1626 nsIFrame* returnFrame = nullptr;
1627 nsCOMPtr<nsIContent> theNode;
1629 while (true) {
1630 *aReturnOffset = aOffset;
1632 theNode = aNode;
1634 if (aNode->IsElement()) {
1635 int32_t childIndex = 0;
1636 int32_t numChildren = theNode->GetChildCount();
1638 if (aHint == CARET_ASSOCIATE_BEFORE) {
1639 if (aOffset > 0) {
1640 childIndex = aOffset - 1;
1641 } else {
1642 childIndex = aOffset;
1644 } else {
1645 NS_ASSERTION(aHint == CARET_ASSOCIATE_AFTER, "unknown direction");
1646 if (aOffset >= numChildren) {
1647 if (numChildren > 0) {
1648 childIndex = numChildren - 1;
1649 } else {
1650 childIndex = 0;
1652 } else {
1653 childIndex = aOffset;
1657 if (childIndex > 0 || numChildren > 0) {
1658 nsCOMPtr<nsIContent> childNode =
1659 theNode->GetChildAt_Deprecated(childIndex);
1661 if (!childNode) {
1662 break;
1665 theNode = childNode;
1668 // Now that we have the child node, check if it too
1669 // can contain children. If so, descend into child.
1670 if (theNode->IsElement() && theNode->GetChildCount() &&
1671 !theNode->HasIndependentSelection()) {
1672 aNode = theNode;
1673 aOffset = aOffset > childIndex ? theNode->GetChildCount() : 0;
1674 continue;
1675 } else {
1676 // Check to see if theNode is a text node. If it is, translate
1677 // aOffset into an offset into the text node.
1679 RefPtr<Text> textNode = theNode->GetAsText();
1680 if (textNode) {
1681 if (theNode->GetPrimaryFrame()) {
1682 if (aOffset > childIndex) {
1683 uint32_t textLength = textNode->Length();
1685 *aReturnOffset = (int32_t)textLength;
1686 } else {
1687 *aReturnOffset = 0;
1689 } else {
1690 int32_t numChildren = aNode->GetChildCount();
1691 int32_t newChildIndex = aHint == CARET_ASSOCIATE_BEFORE
1692 ? childIndex - 1
1693 : childIndex + 1;
1695 if (newChildIndex >= 0 && newChildIndex < numChildren) {
1696 nsCOMPtr<nsIContent> newChildNode =
1697 aNode->GetChildAt_Deprecated(newChildIndex);
1698 if (!newChildNode) {
1699 return nullptr;
1702 aNode = newChildNode;
1703 aOffset =
1704 aHint == CARET_ASSOCIATE_BEFORE ? aNode->GetChildCount() : 0;
1705 continue;
1706 } else {
1707 // newChildIndex is illegal which means we're at first or last
1708 // child. Just use original node to get the frame.
1709 theNode = aNode;
1716 // If the node is a ShadowRoot, the frame needs to be adjusted,
1717 // because a ShadowRoot does not get a frame. Its children are rendered
1718 // as children of the host.
1719 if (ShadowRoot* shadow = ShadowRoot::FromNode(theNode)) {
1720 theNode = shadow->GetHost();
1723 returnFrame = theNode->GetPrimaryFrame();
1724 if (!returnFrame) {
1725 if (aHint == CARET_ASSOCIATE_BEFORE) {
1726 if (aOffset > 0) {
1727 --aOffset;
1728 continue;
1729 } else {
1730 break;
1732 } else {
1733 int32_t end = theNode->GetChildCount();
1734 if (aOffset < end) {
1735 ++aOffset;
1736 continue;
1737 } else {
1738 break;
1743 break;
1744 } // end while
1746 if (!returnFrame) return nullptr;
1748 // If we ended up here and were asked to position the caret after a visible
1749 // break, let's return the frame on the next line instead if it exists.
1750 if (aOffset > 0 && (uint32_t)aOffset >= aNode->Length() &&
1751 theNode == aNode->GetLastChild()) {
1752 nsIFrame* newFrame;
1753 nsLayoutUtils::IsInvisibleBreak(theNode, &newFrame);
1754 if (newFrame) {
1755 returnFrame = newFrame;
1756 *aReturnOffset = 0;
1760 // find the child frame containing the offset we want
1761 returnFrame->GetChildFrameContainingOffset(
1762 *aReturnOffset, aHint == CARET_ASSOCIATE_AFTER, &aOffset, &returnFrame);
1763 return returnFrame;
1766 nsIFrame* nsFrameSelection::GetFrameToPageSelect() const {
1767 if (NS_WARN_IF(!mPresShell)) {
1768 return nullptr;
1771 nsIFrame* rootFrameToSelect;
1772 if (mLimiters.mLimiter) {
1773 rootFrameToSelect = mLimiters.mLimiter->GetPrimaryFrame();
1774 if (NS_WARN_IF(!rootFrameToSelect)) {
1775 return nullptr;
1777 } else if (mLimiters.mAncestorLimiter) {
1778 rootFrameToSelect = mLimiters.mAncestorLimiter->GetPrimaryFrame();
1779 if (NS_WARN_IF(!rootFrameToSelect)) {
1780 return nullptr;
1782 } else {
1783 rootFrameToSelect = mPresShell->GetRootScrollFrame();
1784 if (NS_WARN_IF(!rootFrameToSelect)) {
1785 return nullptr;
1789 nsCOMPtr<nsIContent> contentToSelect = mPresShell->GetContentForScrolling();
1790 if (contentToSelect) {
1791 // If there is selected content, look for nearest and vertical scrollable
1792 // parent under the root frame.
1793 for (nsIFrame* frame = contentToSelect->GetPrimaryFrame();
1794 frame && frame != rootFrameToSelect; frame = frame->GetParent()) {
1795 nsIScrollableFrame* scrollableFrame = do_QueryFrame(frame);
1796 if (!scrollableFrame) {
1797 continue;
1799 ScrollStyles scrollStyles = scrollableFrame->GetScrollStyles();
1800 if (scrollStyles.mVertical == StyleOverflow::Hidden) {
1801 continue;
1803 uint32_t directions = scrollableFrame->GetAvailableScrollingDirections();
1804 if (directions & nsIScrollableFrame::VERTICAL) {
1805 // If there is sub scrollable frame, let's use its page size to select.
1806 return frame;
1810 // Otherwise, i.e., there is no scrollable frame or only the root frame is
1811 // scrollable, let's return the root frame because Shift + PageUp/PageDown
1812 // should expand the selection in the root content even if it's not
1813 // scrollable.
1814 return rootFrameToSelect;
1817 nsresult nsFrameSelection::PageMove(bool aForward, bool aExtend,
1818 nsIFrame* aFrame,
1819 SelectionIntoView aSelectionIntoView) {
1820 MOZ_ASSERT(aFrame);
1822 // expected behavior for PageMove is to scroll AND move the caret
1823 // and remain relative position of the caret in view. see Bug 4302.
1825 // Get the scrollable frame. If aFrame is not scrollable, this is nullptr.
1826 nsIScrollableFrame* scrollableFrame = aFrame->GetScrollTargetFrame();
1827 // Get the scrolled frame. If aFrame is not scrollable, this is aFrame
1828 // itself.
1829 nsIFrame* scrolledFrame =
1830 scrollableFrame ? scrollableFrame->GetScrolledFrame() : aFrame;
1831 if (!scrolledFrame) {
1832 return NS_OK;
1835 // find out where the caret is.
1836 // we should know mDesiredCaretPos.mValue value of nsFrameSelection, but I
1837 // havent seen that behavior in other windows applications yet.
1838 RefPtr<Selection> selection = GetSelection(SelectionType::eNormal);
1839 if (!selection) {
1840 return NS_OK;
1843 nsRect caretPos;
1844 nsIFrame* caretFrame = nsCaret::GetGeometry(selection, &caretPos);
1845 if (!caretFrame) {
1846 return NS_OK;
1849 // If the scrolled frame is outside of current selection limiter,
1850 // we need to scroll the frame but keep moving selection in the limiter.
1851 nsIFrame* frameToClick = scrolledFrame;
1852 if (!IsValidSelectionPoint(scrolledFrame->GetContent())) {
1853 frameToClick = GetFrameToPageSelect();
1854 if (NS_WARN_IF(!frameToClick)) {
1855 return NS_OK;
1859 if (scrollableFrame) {
1860 // If there is a scrollable frame, adjust pseudo-click position with page
1861 // scroll amount.
1862 // XXX This may scroll more than one page if ScrollSelectionIntoView is
1863 // called later because caret may not fully visible. E.g., if
1864 // clicking line will be visible only half height with scrolling
1865 // the frame, ScrollSelectionIntoView additionally scrolls to show
1866 // the caret entirely.
1867 if (aForward) {
1868 caretPos.y += scrollableFrame->GetPageScrollAmount().height;
1869 } else {
1870 caretPos.y -= scrollableFrame->GetPageScrollAmount().height;
1872 } else {
1873 // Otherwise, adjust pseudo-click position with the frame size.
1874 if (aForward) {
1875 caretPos.y += frameToClick->GetSize().height;
1876 } else {
1877 caretPos.y -= frameToClick->GetSize().height;
1881 caretPos += caretFrame->GetOffsetTo(frameToClick);
1883 // get a content at desired location
1884 nsPoint desiredPoint;
1885 desiredPoint.x = caretPos.x;
1886 desiredPoint.y = caretPos.y + caretPos.height / 2;
1887 nsIFrame::ContentOffsets offsets =
1888 frameToClick->GetContentOffsetsFromPoint(desiredPoint);
1890 if (!offsets.content) {
1891 // XXX Do we need to handle ScrollSelectionIntoView in this case?
1892 return NS_OK;
1895 // First, place the caret.
1896 bool selectionChanged;
1898 // We don't want any script to run until we check whether selection is
1899 // modified by HandleClick.
1900 SelectionBatcher ensureNoSelectionChangeNotifications(selection);
1902 RangeBoundary oldAnchor = selection->AnchorRef();
1903 RangeBoundary oldFocus = selection->FocusRef();
1905 const FocusMode focusMode =
1906 aExtend ? FocusMode::kExtendSelection : FocusMode::kCollapseToNewPoint;
1907 HandleClick(MOZ_KnownLive(offsets.content) /* bug 1636889 */,
1908 offsets.offset, offsets.offset, focusMode,
1909 CARET_ASSOCIATE_AFTER);
1911 selectionChanged = selection->AnchorRef() != oldAnchor ||
1912 selection->FocusRef() != oldFocus;
1915 bool doScrollSelectionIntoView = !(
1916 aSelectionIntoView == SelectionIntoView::IfChanged && !selectionChanged);
1918 // Then, scroll the given frame one page.
1919 if (scrollableFrame) {
1920 mozilla::Telemetry::Accumulate(
1921 mozilla::Telemetry::SCROLL_INPUT_METHODS,
1922 (uint32_t)ScrollInputMethod::MainThreadScrollPage);
1924 // If we'll call ScrollSelectionIntoView later and selection wasn't
1925 // changed and we scroll outside of selection limiter, we shouldn't use
1926 // smooth scroll here because nsIScrollableFrame uses normal runnable,
1927 // but ScrollSelectionIntoView uses early runner and it cancels the
1928 // pending smooth scroll. Therefore, if we used smooth scroll in such
1929 // case, ScrollSelectionIntoView would scroll to show caret instead of
1930 // page scroll of an element outside selection limiter.
1931 ScrollMode scrollMode = doScrollSelectionIntoView && !selectionChanged &&
1932 scrolledFrame != frameToClick
1933 ? ScrollMode::Instant
1934 : ScrollMode::Smooth;
1935 scrollableFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1),
1936 ScrollUnit::PAGES, scrollMode);
1939 // Finally, scroll selection into view if requested.
1940 if (!doScrollSelectionIntoView) {
1941 return NS_OK;
1943 return ScrollSelectionIntoView(
1944 SelectionType::eNormal, nsISelectionController::SELECTION_FOCUS_REGION,
1945 nsISelectionController::SCROLL_SYNCHRONOUS |
1946 nsISelectionController::SCROLL_FOR_CARET_MOVE);
1949 nsresult nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount,
1950 bool aExtend) {
1951 NS_ENSURE_STATE(mPresShell);
1952 // Flush out layout, since we need it to be up to date to do caret
1953 // positioning.
1954 OwningNonNull<PresShell> presShell(*mPresShell);
1955 presShell->FlushPendingNotifications(FlushType::Layout);
1957 if (!mPresShell) {
1958 return NS_OK;
1961 // Check that parameters are safe
1962 if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) {
1963 return NS_ERROR_FAILURE;
1966 nsPresContext* context = mPresShell->GetPresContext();
1967 if (!context) {
1968 return NS_ERROR_FAILURE;
1971 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
1972 RefPtr<Selection> sel = mDomSelections[index];
1973 if (!sel) {
1974 return NS_ERROR_NULL_POINTER;
1977 // Map the abstract movement amounts (0-1) to direction-specific
1978 // selection units.
1979 static const nsSelectionAmount inlineAmount[] = {eSelectCluster, eSelectWord};
1980 static const nsSelectionAmount blockPrevAmount[] = {eSelectLine,
1981 eSelectBeginLine};
1982 static const nsSelectionAmount blockNextAmount[] = {eSelectLine,
1983 eSelectEndLine};
1985 struct PhysicalToLogicalMapping {
1986 nsDirection direction;
1987 const nsSelectionAmount* amounts;
1989 static const PhysicalToLogicalMapping verticalLR[4] = {
1990 {eDirPrevious, blockPrevAmount}, // left
1991 {eDirNext, blockNextAmount}, // right
1992 {eDirPrevious, inlineAmount}, // up
1993 {eDirNext, inlineAmount} // down
1995 static const PhysicalToLogicalMapping verticalRL[4] = {
1996 {eDirNext, blockNextAmount},
1997 {eDirPrevious, blockPrevAmount},
1998 {eDirPrevious, inlineAmount},
1999 {eDirNext, inlineAmount}};
2000 static const PhysicalToLogicalMapping horizontal[4] = {
2001 {eDirPrevious, inlineAmount},
2002 {eDirNext, inlineAmount},
2003 {eDirPrevious, blockPrevAmount},
2004 {eDirNext, blockNextAmount}};
2006 WritingMode wm;
2007 nsIFrame* frame = sel->GetPrimaryFrameForFocusNode(true);
2008 if (frame) {
2009 if (!frame->Style()->IsTextCombined()) {
2010 wm = frame->GetWritingMode();
2011 } else {
2012 // Using different direction for horizontal-in-vertical would
2013 // make it hard to navigate via keyboard. Inherit the moving
2014 // direction from its parent.
2015 MOZ_ASSERT(frame->IsTextFrame());
2016 wm = frame->GetParent()->GetWritingMode();
2017 MOZ_ASSERT(wm.IsVertical(),
2018 "Text combined "
2019 "can only appear in vertical text");
2023 const PhysicalToLogicalMapping& mapping =
2024 wm.IsVertical()
2025 ? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection]
2026 : horizontal[aDirection];
2028 nsresult rv =
2029 MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount], eVisual);
2030 if (NS_FAILED(rv)) {
2031 // If we tried to do a line move, but couldn't move in the given direction,
2032 // then we'll "promote" this to a line-edge move instead.
2033 if (mapping.amounts[aAmount] == eSelectLine) {
2034 rv = MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount + 1],
2035 eVisual);
2037 // And if it was a next-word move that failed (which can happen when
2038 // eat_space_to_next_word is true, see bug 1153237), then just move forward
2039 // to the line-edge.
2040 else if (mapping.amounts[aAmount] == eSelectWord &&
2041 mapping.direction == eDirNext) {
2042 rv = MoveCaret(eDirNext, aExtend, eSelectEndLine, eVisual);
2046 return rv;
2049 nsresult nsFrameSelection::CharacterMove(bool aForward, bool aExtend) {
2050 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectCluster,
2051 eUsePrefStyle);
2054 nsresult nsFrameSelection::WordMove(bool aForward, bool aExtend) {
2055 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectWord,
2056 eUsePrefStyle);
2059 nsresult nsFrameSelection::LineMove(bool aForward, bool aExtend) {
2060 return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectLine,
2061 eUsePrefStyle);
2064 nsresult nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) {
2065 if (aForward) {
2066 return MoveCaret(eDirNext, aExtend, eSelectEndLine, eLogical);
2067 } else {
2068 return MoveCaret(eDirPrevious, aExtend, eSelectBeginLine, eLogical);
2072 template <typename RangeType>
2073 Result<RefPtr<RangeType>, nsresult>
2074 nsFrameSelection::CreateRangeExtendedToSomewhere(
2075 nsDirection aDirection, const nsSelectionAmount aAmount,
2076 CaretMovementStyle aMovementStyle) {
2077 MOZ_ASSERT(aDirection == eDirNext || aDirection == eDirPrevious);
2078 MOZ_ASSERT(aAmount == eSelectCharacter || aAmount == eSelectCluster ||
2079 aAmount == eSelectWord || aAmount == eSelectBeginLine ||
2080 aAmount == eSelectEndLine);
2081 MOZ_ASSERT(aMovementStyle == eLogical || aMovementStyle == eVisual ||
2082 aMovementStyle == eUsePrefStyle);
2084 if (!mPresShell) {
2085 return Err(NS_ERROR_UNEXPECTED);
2087 OwningNonNull<PresShell> presShell(*mPresShell);
2088 presShell->FlushPendingNotifications(FlushType::Layout);
2089 if (!mPresShell) {
2090 return Err(NS_ERROR_FAILURE);
2092 Selection* selection =
2093 mDomSelections[GetIndexFromSelectionType(SelectionType::eNormal)];
2094 if (!selection || selection->RangeCount() != 1) {
2095 return Err(NS_ERROR_FAILURE);
2097 RefPtr<const nsRange> firstRange = selection->GetRangeAt(0);
2098 if (!firstRange || !firstRange->IsPositioned()) {
2099 return Err(NS_ERROR_FAILURE);
2101 Result<nsPeekOffsetStruct, nsresult> result = PeekOffsetForCaretMove(
2102 aDirection, true, aAmount, aMovementStyle, nsPoint(0, 0));
2103 if (result.isErr()) {
2104 return Err(NS_ERROR_FAILURE);
2106 const nsPeekOffsetStruct& pos = result.inspect();
2107 RefPtr<RangeType> range;
2108 if (NS_WARN_IF(!pos.mResultContent)) {
2109 return range;
2111 if (aDirection == eDirPrevious) {
2112 range = RangeType::Create(
2113 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
2114 firstRange->EndRef(), IgnoreErrors());
2115 } else {
2116 range = RangeType::Create(
2117 firstRange->StartRef(),
2118 RawRangeBoundary(pos.mResultContent, pos.mContentOffset),
2119 IgnoreErrors());
2121 return range;
2124 //////////END FRAMESELECTION
2126 void nsFrameSelection::StartBatchChanges() { mBatching.mCounter++; }
2128 void nsFrameSelection::EndBatchChanges(int16_t aReasons) {
2129 MOZ_ASSERT(mBatching.mCounter > 0, "Bad mBatching.mCounter");
2130 mBatching.mCounter--;
2132 if (mBatching.mCounter == 0 && mBatching.mChangesDuringBatching) {
2133 AddChangeReasons(aReasons);
2134 mBatching.mChangesDuringBatching = false;
2135 // Be aware, the Selection instance may be destroyed after this call.
2136 NotifySelectionListeners(SelectionType::eNormal);
2140 nsresult nsFrameSelection::NotifySelectionListeners(
2141 SelectionType aSelectionType) {
2142 int8_t index = GetIndexFromSelectionType(aSelectionType);
2143 if (index >= 0 && mDomSelections[index]) {
2144 RefPtr<Selection> selection = mDomSelections[index];
2145 return selection->NotifySelectionListeners();
2147 return NS_ERROR_FAILURE;
2150 // Start of Table Selection methods
2152 static bool IsCell(nsIContent* aContent) {
2153 return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th);
2156 // static
2157 nsITableCellLayout* nsFrameSelection::GetCellLayout(
2158 const nsIContent* aCellContent) {
2159 nsITableCellLayout* cellLayoutObject =
2160 do_QueryFrame(aCellContent->GetPrimaryFrame());
2161 return cellLayoutObject;
2164 nsresult nsFrameSelection::ClearNormalSelection() {
2165 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2166 if (!mDomSelections[index]) return NS_ERROR_NULL_POINTER;
2168 ErrorResult err;
2169 mDomSelections[index]->RemoveAllRanges(err);
2170 return err.StealNSResult();
2173 static nsIContent* GetFirstSelectedContent(const nsRange* aRange) {
2174 if (!aRange) {
2175 return nullptr;
2178 MOZ_ASSERT(aRange->GetStartContainer(), "Must have start parent!");
2179 MOZ_ASSERT(aRange->GetStartContainer()->IsElement(), "Unexpected parent");
2181 return aRange->GetChildAtStartOffset();
2184 // Table selection support.
2185 // TODO: Separate table methods into a separate nsITableSelection interface
2186 nsresult nsFrameSelection::HandleTableSelection(nsINode* aParentContent,
2187 int32_t aContentOffset,
2188 TableSelectionMode aTarget,
2189 WidgetMouseEvent* aMouseEvent) {
2190 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2191 RefPtr<Selection> selection = mDomSelections[index];
2192 if (!selection) {
2193 return NS_ERROR_NULL_POINTER;
2196 return mTableSelection.HandleSelection(aParentContent, aContentOffset,
2197 aTarget, aMouseEvent, mDragState,
2198 *selection);
2201 nsresult nsFrameSelection::TableSelection::HandleSelection(
2202 nsINode* aParentContent, int32_t aContentOffset, TableSelectionMode aTarget,
2203 WidgetMouseEvent* aMouseEvent, bool aDragState,
2204 Selection& aNormalSelection) {
2205 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2207 NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER);
2208 NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER);
2210 if (aDragState && mDragSelectingCells &&
2211 aTarget == TableSelectionMode::Table) {
2212 // We were selecting cells and user drags mouse in table border or inbetween
2213 // cells,
2214 // just do nothing
2215 return NS_OK;
2218 RefPtr<nsIContent> childContent =
2219 aParentContent->GetChildAt_Deprecated(aContentOffset);
2221 // When doing table selection, always set the direction to next so
2222 // we can be sure that anchorNode's offset always points to the
2223 // selected cell
2224 aNormalSelection.SetDirection(eDirNext);
2226 // Stack-class to wrap all table selection changes in
2227 // BeginBatchChanges() / EndBatchChanges()
2228 SelectionBatcher selectionBatcher(&aNormalSelection);
2230 if (aDragState && mDragSelectingCells) {
2231 return HandleDragSelecting(aTarget, childContent, aMouseEvent,
2232 aNormalSelection);
2235 return HandleMouseUpOrDown(aTarget, aDragState, childContent, aParentContent,
2236 aContentOffset, aMouseEvent, aNormalSelection);
2239 class nsFrameSelection::TableSelection::RowAndColumnRelation {
2240 public:
2241 static Result<RowAndColumnRelation, nsresult> Create(
2242 const nsIContent* aFirst, const nsIContent* aSecond) {
2243 RowAndColumnRelation result;
2245 nsresult errorResult =
2246 GetCellIndexes(aFirst, result.mFirst.mRow, result.mFirst.mColumn);
2247 if (NS_FAILED(errorResult)) {
2248 return Err(errorResult);
2251 errorResult =
2252 GetCellIndexes(aSecond, result.mSecond.mRow, result.mSecond.mColumn);
2253 if (NS_FAILED(errorResult)) {
2254 return Err(errorResult);
2257 return result;
2260 bool IsSameColumn() const { return mFirst.mColumn == mSecond.mColumn; }
2262 bool IsSameRow() const { return mFirst.mRow == mSecond.mRow; }
2264 private:
2265 RowAndColumnRelation() = default;
2267 struct RowAndColumn {
2268 int32_t mRow = 0;
2269 int32_t mColumn = 0;
2272 RowAndColumn mFirst;
2273 RowAndColumn mSecond;
2276 nsresult nsFrameSelection::TableSelection::HandleDragSelecting(
2277 TableSelectionMode aTarget, nsIContent* aChildContent,
2278 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2279 // We are drag-selecting
2280 if (aTarget != TableSelectionMode::Table) {
2281 // If dragging in the same cell as last event, do nothing
2282 if (mEndSelectedCell == aChildContent) {
2283 return NS_OK;
2286 #ifdef DEBUG_TABLE_SELECTION
2287 printf(
2288 " mStartSelectedCell = %p, "
2289 "mEndSelectedCell = %p, aChildContent = %p "
2290 "\n",
2291 mStartSelectedCell.get(), mEndSelectedCell.get(), aChildContent);
2292 #endif
2293 // aTarget can be any "cell mode",
2294 // so we can easily drag-select rows and columns
2295 // Once we are in row or column mode,
2296 // we can drift into any cell to stay in that mode
2297 // even if aTarget = TableSelectionMode::Cell
2299 if (mMode == TableSelectionMode::Row ||
2300 mMode == TableSelectionMode::Column) {
2301 if (mEndSelectedCell) {
2302 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2303 RowAndColumnRelation::Create(mEndSelectedCell, aChildContent);
2305 if (rowAndColumnRelation.isErr()) {
2306 return rowAndColumnRelation.unwrapErr();
2309 if ((mMode == TableSelectionMode::Row &&
2310 rowAndColumnRelation.inspect().IsSameRow()) ||
2311 (mMode == TableSelectionMode::Column &&
2312 rowAndColumnRelation.inspect().IsSameColumn())) {
2313 return NS_OK;
2316 #ifdef DEBUG_TABLE_SELECTION
2317 printf(" Dragged into a new column or row\n");
2318 #endif
2319 // Continue dragging row or column selection
2321 return SelectRowOrColumn(aChildContent, aNormalSelection);
2323 if (mMode == TableSelectionMode::Cell) {
2324 #ifdef DEBUG_TABLE_SELECTION
2325 printf("HandleTableSelection: Dragged into a new cell\n");
2326 #endif
2327 // Trick for quick selection of rows and columns
2328 // Hold down shift, then start selecting in one direction
2329 // If next cell dragged into is in same row, select entire row,
2330 // if next cell is in same column, select entire column
2331 if (mStartSelectedCell && aMouseEvent->IsShift()) {
2332 Result<RowAndColumnRelation, nsresult> rowAndColumnRelation =
2333 RowAndColumnRelation::Create(mStartSelectedCell, aChildContent);
2334 if (rowAndColumnRelation.isErr()) {
2335 return rowAndColumnRelation.unwrapErr();
2338 if (rowAndColumnRelation.inspect().IsSameRow() ||
2339 rowAndColumnRelation.inspect().IsSameColumn()) {
2340 // Force new selection block
2341 mStartSelectedCell = nullptr;
2342 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2344 if (rowAndColumnRelation.inspect().IsSameRow()) {
2345 mMode = TableSelectionMode::Row;
2346 } else {
2347 mMode = TableSelectionMode::Column;
2350 return SelectRowOrColumn(aChildContent, aNormalSelection);
2354 // Reselect block of cells to new end location
2355 return SelectBlockOfCells(mStartSelectedCell, aChildContent,
2356 aNormalSelection);
2359 // Do nothing if dragging in table, but outside a cell
2360 return NS_OK;
2363 nsresult nsFrameSelection::TableSelection::HandleMouseUpOrDown(
2364 TableSelectionMode aTarget, bool aDragState, nsIContent* aChildContent,
2365 nsINode* aParentContent, int32_t aContentOffset,
2366 const WidgetMouseEvent* aMouseEvent, Selection& aNormalSelection) {
2367 nsresult result = NS_OK;
2368 // Not dragging -- mouse event is down or up
2369 if (aDragState) {
2370 #ifdef DEBUG_TABLE_SELECTION
2371 printf("HandleTableSelection: Mouse down event\n");
2372 #endif
2373 // Clear cell we stored in mouse-down
2374 mUnselectCellOnMouseUp = nullptr;
2376 if (aTarget == TableSelectionMode::Cell) {
2377 bool isSelected = false;
2379 // Check if we have other selected cells
2380 nsIContent* previousCellNode =
2381 GetFirstSelectedContent(GetFirstCellRange(aNormalSelection));
2382 if (previousCellNode) {
2383 // We have at least 1 other selected cell
2385 // Check if new cell is already selected
2386 nsIFrame* cellFrame = aChildContent->GetPrimaryFrame();
2387 if (!cellFrame) {
2388 return NS_ERROR_NULL_POINTER;
2390 isSelected = cellFrame->IsSelected();
2391 } else {
2392 // No cells selected -- remove non-cell selection
2393 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2395 mDragSelectingCells = true; // Signal to start drag-cell-selection
2396 mMode = aTarget;
2397 // Set start for new drag-selection block (not appended)
2398 mStartSelectedCell = aChildContent;
2399 // The initial block end is same as the start
2400 mEndSelectedCell = aChildContent;
2402 if (isSelected) {
2403 // Remember this cell to (possibly) unselect it on mouseup
2404 mUnselectCellOnMouseUp = aChildContent;
2405 #ifdef DEBUG_TABLE_SELECTION
2406 printf(
2407 "HandleTableSelection: Saving "
2408 "mUnselectCellOnMouseUp\n");
2409 #endif
2410 } else {
2411 // Select an unselected cell
2412 // but first remove existing selection if not in same table
2413 if (previousCellNode &&
2414 !IsInSameTable(previousCellNode, aChildContent)) {
2415 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2416 // Reset selection mode that is cleared in RemoveAllRanges
2417 mMode = aTarget;
2420 return ::SelectCellElement(aChildContent, aNormalSelection);
2423 return NS_OK;
2425 if (aTarget == TableSelectionMode::Table) {
2426 // TODO: We currently select entire table when clicked between cells,
2427 // should we restrict to only around border?
2428 // *** How do we get location data for cell and click?
2429 mDragSelectingCells = false;
2430 mStartSelectedCell = nullptr;
2431 mEndSelectedCell = nullptr;
2433 // Remove existing selection and select the table
2434 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2435 return CreateAndAddRange(aParentContent, aContentOffset,
2436 aNormalSelection);
2438 if (aTarget == TableSelectionMode::Row ||
2439 aTarget == TableSelectionMode::Column) {
2440 #ifdef DEBUG_TABLE_SELECTION
2441 printf("aTarget == %d\n", aTarget);
2442 #endif
2444 // Start drag-selecting mode so multiple rows/cols can be selected
2445 // Note: Currently, nsIFrame::GetDataForTableSelection
2446 // will never call us for row or column selection on mouse down
2447 mDragSelectingCells = true;
2449 // Force new selection block
2450 mStartSelectedCell = nullptr;
2451 aNormalSelection.RemoveAllRanges(IgnoreErrors());
2452 // Always do this AFTER RemoveAllRanges
2453 mMode = aTarget;
2455 return SelectRowOrColumn(aChildContent, aNormalSelection);
2457 } else {
2458 #ifdef DEBUG_TABLE_SELECTION
2459 printf(
2460 "HandleTableSelection: Mouse UP event. "
2461 "mDragSelectingCells=%d, "
2462 "mStartSelectedCell=%p\n",
2463 mDragSelectingCells, mStartSelectedCell.get());
2464 #endif
2465 // First check if we are extending a block selection
2466 uint32_t rangeCount = aNormalSelection.RangeCount();
2468 if (rangeCount > 0 && aMouseEvent->IsShift() && mAppendStartSelectedCell &&
2469 mAppendStartSelectedCell != aChildContent) {
2470 // Shift key is down: append a block selection
2471 mDragSelectingCells = false;
2473 return SelectBlockOfCells(mAppendStartSelectedCell, aChildContent,
2474 aNormalSelection);
2477 if (mDragSelectingCells) {
2478 mAppendStartSelectedCell = mStartSelectedCell;
2481 mDragSelectingCells = false;
2482 mStartSelectedCell = nullptr;
2483 mEndSelectedCell = nullptr;
2485 // Any other mouseup actions require that Ctrl or Cmd key is pressed
2486 // else stop table selection mode
2487 bool doMouseUpAction = false;
2488 #ifdef XP_MACOSX
2489 doMouseUpAction = aMouseEvent->IsMeta();
2490 #else
2491 doMouseUpAction = aMouseEvent->IsControl();
2492 #endif
2493 if (!doMouseUpAction) {
2494 #ifdef DEBUG_TABLE_SELECTION
2495 printf(
2496 "HandleTableSelection: Ending cell selection on mouseup: "
2497 "mAppendStartSelectedCell=%p\n",
2498 mAppendStartSelectedCell.get());
2499 #endif
2500 return NS_OK;
2502 // Unselect a cell only if it wasn't
2503 // just selected on mousedown
2504 if (aChildContent == mUnselectCellOnMouseUp) {
2505 // Scan ranges to find the cell to unselect (the selection range to
2506 // remove)
2507 // XXXbz it's really weird that this lives outside the loop, so once we
2508 // find one we keep looking at it even if we find no more cells...
2509 nsINode* previousCellParent = nullptr;
2510 #ifdef DEBUG_TABLE_SELECTION
2511 printf(
2512 "HandleTableSelection: Unselecting "
2513 "mUnselectCellOnMouseUp; "
2514 "rangeCount=%d\n",
2515 rangeCount);
2516 #endif
2517 for (uint32_t i = 0; i < rangeCount; i++) {
2518 // Strong reference, because sometimes we want to remove
2519 // this range, and then we might be the only owner.
2520 RefPtr<nsRange> range = aNormalSelection.GetRangeAt(i);
2521 if (!range) {
2522 return NS_ERROR_NULL_POINTER;
2525 nsINode* container = range->GetStartContainer();
2526 if (!container) {
2527 return NS_ERROR_NULL_POINTER;
2530 int32_t offset = range->StartOffset();
2531 // Be sure previous selection is a table cell
2532 nsIContent* child = range->GetChildAtStartOffset();
2533 if (child && IsCell(child)) {
2534 previousCellParent = container;
2537 // We're done if we didn't find parent of a previously-selected cell
2538 if (!previousCellParent) {
2539 break;
2542 if (previousCellParent == aParentContent && offset == aContentOffset) {
2543 // Cell is already selected
2544 if (rangeCount == 1) {
2545 #ifdef DEBUG_TABLE_SELECTION
2546 printf("HandleTableSelection: Unselecting single selected cell\n");
2547 #endif
2548 // This was the only cell selected.
2549 // Collapse to "normal" selection inside the cell
2550 mStartSelectedCell = nullptr;
2551 mEndSelectedCell = nullptr;
2552 mAppendStartSelectedCell = nullptr;
2553 // TODO: We need a "Collapse to just before deepest child" routine
2554 // Even better, should we collapse to just after the LAST deepest
2555 // child
2556 // (i.e., at the end of the cell's contents)?
2557 return aNormalSelection.CollapseInLimiter(aChildContent, 0);
2559 #ifdef DEBUG_TABLE_SELECTION
2560 printf(
2561 "HandleTableSelection: Removing cell from multi-cell "
2562 "selection\n");
2563 #endif
2564 // Unselecting the start of previous block
2565 // XXX What do we use now!
2566 if (aChildContent == mAppendStartSelectedCell) {
2567 mAppendStartSelectedCell = nullptr;
2570 // Deselect cell by removing its range from selection
2571 ErrorResult err;
2572 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2573 *range, err);
2574 return err.StealNSResult();
2577 mUnselectCellOnMouseUp = nullptr;
2580 return result;
2583 nsresult nsFrameSelection::TableSelection::SelectBlockOfCells(
2584 nsIContent* aStartCell, nsIContent* aEndCell, Selection& aNormalSelection) {
2585 NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER);
2586 NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER);
2587 mEndSelectedCell = aEndCell;
2589 nsresult result = NS_OK;
2591 // If new end cell is in a different table, do nothing
2592 const RefPtr<const nsIContent> table = IsInSameTable(aStartCell, aEndCell);
2593 if (!table) {
2594 return NS_OK;
2597 // Get starting and ending cells' location in the cellmap
2598 int32_t startRowIndex, startColIndex, endRowIndex, endColIndex;
2599 result = GetCellIndexes(aStartCell, startRowIndex, startColIndex);
2600 if (NS_FAILED(result)) return result;
2601 result = GetCellIndexes(aEndCell, endRowIndex, endColIndex);
2602 if (NS_FAILED(result)) return result;
2604 if (mDragSelectingCells) {
2605 // Drag selecting: remove selected cells outside of new block limits
2606 // TODO: `UnselectCells`'s return value shouldn't be ignored.
2607 UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex,
2608 true, aNormalSelection);
2611 // Note that we select block in the direction of user's mouse dragging,
2612 // which means start cell may be after the end cell in either row or column
2613 return AddCellsToSelection(table, startRowIndex, startColIndex, endRowIndex,
2614 endColIndex, aNormalSelection);
2617 nsresult nsFrameSelection::TableSelection::UnselectCells(
2618 const nsIContent* aTableContent, int32_t aStartRowIndex,
2619 int32_t aStartColumnIndex, int32_t aEndRowIndex, int32_t aEndColumnIndex,
2620 bool aRemoveOutsideOfCellRange, mozilla::dom::Selection& aNormalSelection) {
2621 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2623 nsTableWrapperFrame* tableFrame =
2624 do_QueryFrame(aTableContent->GetPrimaryFrame());
2625 if (!tableFrame) return NS_ERROR_FAILURE;
2627 int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex);
2628 int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex);
2629 int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex);
2630 int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex);
2632 // Strong reference because we sometimes remove the range
2633 RefPtr<nsRange> range = GetFirstCellRange(aNormalSelection);
2634 nsIContent* cellNode = GetFirstSelectedContent(range);
2635 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2637 int32_t curRowIndex, curColIndex;
2638 while (cellNode) {
2639 nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex);
2640 if (NS_FAILED(result)) return result;
2642 #ifdef DEBUG_TABLE_SELECTION
2643 if (!range) printf("RemoveCellsToSelection -- range is null\n");
2644 #endif
2646 if (range) {
2647 if (aRemoveOutsideOfCellRange) {
2648 if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex ||
2649 curColIndex < minColIndex || curColIndex > maxColIndex) {
2650 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2651 *range, IgnoreErrors());
2652 // Since we've removed the range, decrement pointer to next range
2653 mSelectedCellIndex--;
2656 } else {
2657 // Remove cell from selection if it belongs to the given cells range or
2658 // it is spanned onto the cells range.
2659 nsTableCellFrame* cellFrame =
2660 tableFrame->GetCellFrameAt(curRowIndex, curColIndex);
2662 uint32_t origRowIndex = cellFrame->RowIndex();
2663 uint32_t origColIndex = cellFrame->ColIndex();
2664 uint32_t actualRowSpan =
2665 tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex);
2666 uint32_t actualColSpan =
2667 tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex);
2668 if (origRowIndex <= static_cast<uint32_t>(maxRowIndex) &&
2669 maxRowIndex >= 0 &&
2670 origRowIndex + actualRowSpan - 1 >=
2671 static_cast<uint32_t>(minRowIndex) &&
2672 origColIndex <= static_cast<uint32_t>(maxColIndex) &&
2673 maxColIndex >= 0 &&
2674 origColIndex + actualColSpan - 1 >=
2675 static_cast<uint32_t>(minColIndex)) {
2676 aNormalSelection.RemoveRangeAndUnselectFramesAndNotifyListeners(
2677 *range, IgnoreErrors());
2678 // Since we've removed the range, decrement pointer to next range
2679 mSelectedCellIndex--;
2684 range = GetNextCellRange(aNormalSelection);
2685 cellNode = GetFirstSelectedContent(range);
2686 MOZ_ASSERT(!range || cellNode, "Must have cellNode if had a range");
2689 return NS_OK;
2692 nsresult SelectCellElement(nsIContent* aCellElement,
2693 Selection& aNormalSelection) {
2694 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2696 nsIContent* parent = aCellElement->GetParent();
2698 // Get child offset
2699 int32_t offset = parent->ComputeIndexOf(aCellElement);
2701 return CreateAndAddRange(parent, offset, aNormalSelection);
2704 static nsresult AddCellsToSelection(const nsIContent* aTableContent,
2705 int32_t aStartRowIndex,
2706 int32_t aStartColumnIndex,
2707 int32_t aEndRowIndex,
2708 int32_t aEndColumnIndex,
2709 Selection& aNormalSelection) {
2710 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2712 nsTableWrapperFrame* tableFrame =
2713 do_QueryFrame(aTableContent->GetPrimaryFrame());
2714 if (!tableFrame) { // Check that |table| is a table.
2715 return NS_ERROR_FAILURE;
2718 nsresult result = NS_OK;
2719 uint32_t row = aStartRowIndex;
2720 while (true) {
2721 uint32_t col = aStartColumnIndex;
2722 while (true) {
2723 nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col);
2725 // Skip cells that are spanned from previous locations or are already
2726 // selected
2727 if (cellFrame) {
2728 uint32_t origRow = cellFrame->RowIndex();
2729 uint32_t origCol = cellFrame->ColIndex();
2730 if (origRow == row && origCol == col && !cellFrame->IsSelected()) {
2731 result = SelectCellElement(cellFrame->GetContent(), aNormalSelection);
2732 if (NS_FAILED(result)) {
2733 return result;
2737 // Done when we reach end column
2738 if (col == static_cast<uint32_t>(aEndColumnIndex)) {
2739 break;
2742 if (aStartColumnIndex < aEndColumnIndex) {
2743 col++;
2744 } else {
2745 col--;
2748 if (row == static_cast<uint32_t>(aEndRowIndex)) {
2749 break;
2752 if (aStartRowIndex < aEndRowIndex) {
2753 row++;
2754 } else {
2755 row--;
2758 return result;
2761 nsresult nsFrameSelection::RemoveCellsFromSelection(nsIContent* aTable,
2762 int32_t aStartRowIndex,
2763 int32_t aStartColumnIndex,
2764 int32_t aEndRowIndex,
2765 int32_t aEndColumnIndex) {
2766 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2767 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2768 if (!selection) {
2769 return NS_ERROR_NULL_POINTER;
2772 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2773 aStartColumnIndex, aEndRowIndex,
2774 aEndColumnIndex, false, *selection);
2777 nsresult nsFrameSelection::RestrictCellsToSelection(nsIContent* aTable,
2778 int32_t aStartRowIndex,
2779 int32_t aStartColumnIndex,
2780 int32_t aEndRowIndex,
2781 int32_t aEndColumnIndex) {
2782 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
2783 const RefPtr<mozilla::dom::Selection> selection = mDomSelections[index];
2784 if (!selection) {
2785 return NS_ERROR_NULL_POINTER;
2788 return mTableSelection.UnselectCells(aTable, aStartRowIndex,
2789 aStartColumnIndex, aEndRowIndex,
2790 aEndColumnIndex, true, *selection);
2793 Result<nsFrameSelection::TableSelection::FirstAndLastCell, nsresult>
2794 nsFrameSelection::TableSelection::FindFirstAndLastCellOfRowOrColumn(
2795 const nsIContent& aCellContent) const {
2796 const nsIContent* table = GetParentTable(&aCellContent);
2797 if (!table) {
2798 return Err(NS_ERROR_NULL_POINTER);
2801 // Get table and cell layout interfaces to access
2802 // cell data based on cellmap location
2803 // Frames are not ref counted, so don't use an nsCOMPtr
2804 nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame());
2805 if (!tableFrame) {
2806 return Err(NS_ERROR_FAILURE);
2808 nsITableCellLayout* cellLayout = GetCellLayout(&aCellContent);
2809 if (!cellLayout) {
2810 return Err(NS_ERROR_FAILURE);
2813 // Get location of target cell:
2814 int32_t rowIndex, colIndex;
2815 nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex);
2816 if (NS_FAILED(result)) {
2817 return Err(result);
2820 // Be sure we start at proper beginning
2821 // (This allows us to select row or col given ANY cell!)
2822 if (mMode == TableSelectionMode::Row) {
2823 colIndex = 0;
2825 if (mMode == TableSelectionMode::Column) {
2826 rowIndex = 0;
2829 FirstAndLastCell firstAndLastCell;
2830 while (true) {
2831 // Loop through all cells in column or row to find first and last
2832 nsCOMPtr<nsIContent> curCellContent =
2833 tableFrame->GetCellAt(rowIndex, colIndex);
2834 if (!curCellContent) {
2835 break;
2838 if (!firstAndLastCell.mFirst) {
2839 firstAndLastCell.mFirst = curCellContent;
2842 firstAndLastCell.mLast = std::move(curCellContent);
2844 // Move to next cell in cellmap, skipping spanned locations
2845 if (mMode == TableSelectionMode::Row) {
2846 colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
2847 } else {
2848 rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex);
2851 return firstAndLastCell;
2854 nsresult nsFrameSelection::TableSelection::SelectRowOrColumn(
2855 nsIContent* aCellContent, Selection& aNormalSelection) {
2856 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2858 if (!aCellContent) {
2859 return NS_ERROR_NULL_POINTER;
2862 Result<FirstAndLastCell, nsresult> firstAndLastCell =
2863 FindFirstAndLastCellOfRowOrColumn(*aCellContent);
2864 if (firstAndLastCell.isErr()) {
2865 return firstAndLastCell.unwrapErr();
2868 // Use SelectBlockOfCells:
2869 // This will replace existing selection,
2870 // but allow unselecting by dragging out of selected region
2871 if (firstAndLastCell.inspect().mFirst && firstAndLastCell.inspect().mLast) {
2872 nsresult rv{NS_OK};
2874 if (!mStartSelectedCell) {
2875 // We are starting a new block, so select the first cell
2876 rv = ::SelectCellElement(firstAndLastCell.inspect().mFirst,
2877 aNormalSelection);
2878 if (NS_FAILED(rv)) {
2879 return rv;
2881 mStartSelectedCell = firstAndLastCell.inspect().mFirst;
2884 rv = SelectBlockOfCells(mStartSelectedCell,
2885 firstAndLastCell.inspect().mLast, aNormalSelection);
2887 // This gets set to the cell at end of row/col,
2888 // but we need it to be the cell under cursor
2889 mEndSelectedCell = aCellContent;
2890 return rv;
2893 #if 0
2894 // This is a more efficient strategy that appends row to current selection,
2895 // but doesn't allow dragging OFF of an existing selection to unselect!
2896 do {
2897 // Loop through all cells in column or row
2898 result = tableLayout->GetCellDataAt(rowIndex, colIndex,
2899 getter_AddRefs(cellElement),
2900 curRowIndex, curColIndex,
2901 rowSpan, colSpan,
2902 actualRowSpan, actualColSpan,
2903 isSelected);
2904 if (NS_FAILED(result)) return result;
2905 // We're done when cell is not found
2906 if (!cellElement) break;
2909 // Check spans else we infinitely loop
2910 NS_ASSERTION(actualColSpan, "actualColSpan is 0!");
2911 NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!");
2913 // Skip cells that are already selected or span from outside our region
2914 if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex)
2916 result = SelectCellElement(cellElement);
2917 if (NS_FAILED(result)) return result;
2919 // Move to next row or column in cellmap, skipping spanned locations
2920 if (mMode == TableSelectionMode::Row)
2921 colIndex += actualColSpan;
2922 else
2923 rowIndex += actualRowSpan;
2925 while (cellElement);
2926 #endif
2928 return NS_OK;
2931 // static
2932 nsIContent* nsFrameSelection::GetFirstCellNodeInRange(const nsRange* aRange) {
2933 if (!aRange) return nullptr;
2935 nsIContent* childContent = aRange->GetChildAtStartOffset();
2936 if (!childContent) return nullptr;
2937 // Don't return node if not a cell
2938 if (!IsCell(childContent)) return nullptr;
2940 return childContent;
2943 nsRange* nsFrameSelection::TableSelection::GetFirstCellRange(
2944 const mozilla::dom::Selection& aNormalSelection) {
2945 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2947 nsRange* firstRange = aNormalSelection.GetRangeAt(0);
2948 if (!GetFirstCellNodeInRange(firstRange)) {
2949 return nullptr;
2952 // Setup for next cell
2953 mSelectedCellIndex = 1;
2955 return firstRange;
2958 nsRange* nsFrameSelection::TableSelection::GetNextCellRange(
2959 const mozilla::dom::Selection& aNormalSelection) {
2960 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
2962 nsRange* range = aNormalSelection.GetRangeAt(mSelectedCellIndex);
2964 // Get first node in next range of selection - test if it's a cell
2965 if (!GetFirstCellNodeInRange(range)) {
2966 return nullptr;
2969 // Setup for next cell
2970 mSelectedCellIndex++;
2972 return range;
2975 // static
2976 nsresult nsFrameSelection::GetCellIndexes(const nsIContent* aCell,
2977 int32_t& aRowIndex,
2978 int32_t& aColIndex) {
2979 if (!aCell) return NS_ERROR_NULL_POINTER;
2981 aColIndex = 0; // initialize out params
2982 aRowIndex = 0;
2984 nsITableCellLayout* cellLayoutObject = GetCellLayout(aCell);
2985 if (!cellLayoutObject) return NS_ERROR_FAILURE;
2986 return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex);
2989 // static
2990 nsIContent* nsFrameSelection::IsInSameTable(const nsIContent* aContent1,
2991 const nsIContent* aContent2) {
2992 if (!aContent1 || !aContent2) return nullptr;
2994 nsIContent* tableNode1 = GetParentTable(aContent1);
2995 nsIContent* tableNode2 = GetParentTable(aContent2);
2997 // Must be in the same table. Note that we want to return false for
2998 // the test if both tables are null.
2999 return (tableNode1 == tableNode2) ? tableNode1 : nullptr;
3002 // static
3003 nsIContent* nsFrameSelection::GetParentTable(const nsIContent* aCell) {
3004 if (!aCell) {
3005 return nullptr;
3008 for (nsIContent* parent = aCell->GetParent(); parent;
3009 parent = parent->GetParent()) {
3010 if (parent->IsHTMLElement(nsGkAtoms::table)) {
3011 return parent;
3015 return nullptr;
3018 nsresult nsFrameSelection::SelectCellElement(nsIContent* aCellElement) {
3019 const int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3020 const RefPtr<Selection> selection = mDomSelections[index];
3021 if (!selection) {
3022 return NS_ERROR_NULL_POINTER;
3025 return ::SelectCellElement(aCellElement, *selection);
3028 nsresult CreateAndAddRange(nsINode* aContainer, int32_t aOffset,
3029 Selection& aNormalSelection) {
3030 MOZ_ASSERT(aNormalSelection.Type() == SelectionType::eNormal);
3032 if (!aContainer) {
3033 return NS_ERROR_NULL_POINTER;
3036 // Set range around child at given offset
3037 ErrorResult error;
3038 RefPtr<nsRange> range =
3039 nsRange::Create(aContainer, aOffset, aContainer, aOffset + 1, error);
3040 if (NS_WARN_IF(error.Failed())) {
3041 return error.StealNSResult();
3043 MOZ_ASSERT(range);
3045 ErrorResult err;
3046 aNormalSelection.AddRangeAndSelectFramesAndNotifyListeners(*range, err);
3047 return err.StealNSResult();
3050 // End of Table Selection
3052 void nsFrameSelection::SetAncestorLimiter(nsIContent* aLimiter) {
3053 if (mLimiters.mAncestorLimiter != aLimiter) {
3054 mLimiters.mAncestorLimiter = aLimiter;
3055 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3056 if (!mDomSelections[index]) return;
3058 if (!IsValidSelectionPoint(mDomSelections[index]->GetFocusNode())) {
3059 ClearNormalSelection();
3060 if (mLimiters.mAncestorLimiter) {
3061 SetChangeReasons(nsISelectionListener::NO_REASON);
3062 nsCOMPtr<nsIContent> limiter(mLimiters.mAncestorLimiter);
3063 TakeFocus(limiter, 0, 0, CARET_ASSOCIATE_BEFORE,
3064 FocusMode::kCollapseToNewPoint);
3070 void nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) {
3071 if (aMouseEvent) {
3072 mDelayedMouseEvent.mIsValid = true;
3073 mDelayedMouseEvent.mIsShift = aMouseEvent->IsShift();
3074 mDelayedMouseEvent.mClickCount = aMouseEvent->mClickCount;
3075 } else {
3076 mDelayedMouseEvent.mIsValid = false;
3080 void nsFrameSelection::DisconnectFromPresShell() {
3081 if (mAccessibleCaretEnabled) {
3082 int8_t index = GetIndexFromSelectionType(SelectionType::eNormal);
3083 mDomSelections[index]->StopNotifyingAccessibleCaretEventHub();
3086 StopAutoScrollTimer();
3087 for (size_t i = 0; i < ArrayLength(mDomSelections); i++) {
3088 mDomSelections[i]->Clear(nullptr);
3090 mPresShell = nullptr;
3093 #ifdef XP_MACOSX
3095 * See Bug 1288453.
3097 * Update the selection cache on repaint to handle when a pre-existing
3098 * selection becomes active aka the current selection.
3100 * 1. Change the current selection by click n dragging another selection.
3101 * - Make a selection on content page. Make a selection in a text editor.
3102 * - You can click n drag the content selection to make it active again.
3103 * 2. Change the current selection when switching to a tab with a selection.
3104 * - Make selection in tab.
3105 * - Switching tabs will make its respective selection active.
3107 * Therefore, we only update the selection cache on a repaint
3108 * if the current selection being repainted is not an empty selection.
3110 * If the current selection is empty. The current selection cache
3111 * would be cleared by AutoCopyListener::OnSelectionChange().
3113 static nsresult UpdateSelectionCacheOnRepaintSelection(Selection* aSel) {
3114 PresShell* presShell = aSel->GetPresShell();
3115 if (!presShell) {
3116 return NS_OK;
3118 nsCOMPtr<Document> aDoc = presShell->GetDocument();
3120 if (aDoc && aSel && !aSel->IsCollapsed()) {
3121 return nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3122 aSel, aDoc, nsIClipboard::kSelectionCache, false);
3125 return NS_OK;
3127 #endif // XP_MACOSX
3129 // mozilla::AutoCopyListener
3131 int16_t AutoCopyListener::sClipboardID = -1;
3134 * What we do now:
3135 * On every selection change, we copy to the clipboard anew, creating a
3136 * HTML buffer, a transferable, an nsISupportsString and
3137 * a huge mess every time. This is basically what
3138 * nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() does to move the
3139 * selection into the clipboard for Edit->Copy.
3141 * What we should do, to make our end of the deal faster:
3142 * Create a singleton transferable with our own magic converter. When selection
3143 * changes (use a quick cache to detect ``real'' changes), we put the new
3144 * Selection in the transferable. Our magic converter will take care of
3145 * transferable->whatever-other-format when the time comes to actually
3146 * hand over the clipboard contents.
3148 * Other issues:
3149 * - which X clipboard should we populate?
3150 * - should we use a different one than Edit->Copy, so that inadvertant
3151 * selections (or simple clicks, which currently cause a selection
3152 * notification, regardless of if they're in the document which currently has
3153 * selection!) don't lose the contents of the ``application''? Or should we
3154 * just put some intelligence in the ``is this a real selection?'' code to
3155 * protect our selection against clicks in other documents that don't create
3156 * selections?
3157 * - maybe we should just never clear the X clipboard? That would make this
3158 * problem just go away, which is very tempting.
3160 * On macOS,
3161 * nsIClipboard::kSelectionCache is the flag for current selection cache.
3162 * Set the current selection cache on the parent process in
3163 * widget cocoa nsClipboard whenever selection changes.
3166 // static
3167 void AutoCopyListener::OnSelectionChange(Document* aDocument,
3168 Selection& aSelection,
3169 int16_t aReason) {
3170 MOZ_ASSERT(IsValidClipboardID(sClipboardID));
3172 if (sClipboardID == nsIClipboard::kSelectionCache) {
3173 // Do nothing if this isn't in the active window and,
3174 // in the case of Web content, in the frontmost tab.
3175 if (!aDocument || !IsInActiveTab(aDocument)) {
3176 return;
3180 static const int16_t kResasonsToHandle =
3181 nsISelectionListener::MOUSEUP_REASON |
3182 nsISelectionListener::SELECTALL_REASON |
3183 nsISelectionListener::KEYPRESS_REASON;
3184 if (!(aReason & kResasonsToHandle)) {
3185 return; // Don't care if we are still dragging.
3188 if (!aDocument || aSelection.IsCollapsed()) {
3189 #ifdef DEBUG_CLIPBOARD
3190 fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n");
3191 #endif
3192 if (sClipboardID != nsIClipboard::kSelectionCache) {
3193 // XXX Should we clear X clipboard?
3194 return;
3197 // If on macOS, clear the current selection transferable cached
3198 // on the parent process (nsClipboard) when the selection is empty.
3199 DebugOnly<nsresult> rv = nsCopySupport::ClearSelectionCache();
3200 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
3201 "nsCopySupport::ClearSelectionCache() failed");
3202 return;
3205 DebugOnly<nsresult> rv =
3206 nsCopySupport::EncodeDocumentWithContextAndPutToClipboard(
3207 &aSelection, aDocument, sClipboardID, false);
3208 NS_WARNING_ASSERTION(
3209 NS_SUCCEEDED(rv),
3210 "nsCopySupport::EncodeDocumentWithContextAndPutToClipboard() failed");