diff options
Diffstat (limited to 'layout/generic/nsSelection.cpp')
-rw-r--r-- | layout/generic/nsSelection.cpp | 6837 |
1 files changed, 6837 insertions, 0 deletions
diff --git a/layout/generic/nsSelection.cpp b/layout/generic/nsSelection.cpp new file mode 100644 index 000000000..e0d65632e --- /dev/null +++ b/layout/generic/nsSelection.cpp @@ -0,0 +1,6837 @@ +/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ +/* vim: set ts=2 sw=2 et tw=78: */ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* + * Implementation of selection: nsISelection,nsISelectionPrivate and nsFrameSelection + */ + +#include "mozilla/dom/Selection.h" + +#include "mozilla/Attributes.h" +#include "mozilla/EventStates.h" + +#include "nsCOMPtr.h" +#include "nsString.h" +#include "nsFrameSelection.h" +#include "nsISelectionListener.h" +#include "nsContentCID.h" +#include "nsDeviceContext.h" +#include "nsIContent.h" +#include "nsIDOMNode.h" +#include "nsRange.h" +#include "nsCOMArray.h" +#include "nsITableCellLayout.h" +#include "nsTArray.h" +#include "nsTableWrapperFrame.h" +#include "nsTableCellFrame.h" +#include "nsIScrollableFrame.h" +#include "nsCCUncollectableMarker.h" +#include "nsIContentIterator.h" +#include "nsIDocumentEncoder.h" +#include "nsTextFragment.h" +#include <algorithm> +#include "nsContentUtils.h" + +#include "nsGkAtoms.h" +#include "nsIFrameTraversal.h" +#include "nsLayoutUtils.h" +#include "nsLayoutCID.h" +#include "nsBidiPresUtils.h" +static NS_DEFINE_CID(kFrameTraversalCID, NS_FRAMETRAVERSAL_CID); +#include "nsTextFrame.h" + +#include "nsIDOMText.h" + +#include "nsContentUtils.h" +#include "nsThreadUtils.h" +#include "mozilla/Preferences.h" +#include "nsDOMClassInfoID.h" + +#include "nsPresContext.h" +#include "nsIPresShell.h" +#include "nsCaret.h" +#include "AccessibleCaretEventHub.h" + +#include "mozilla/MouseEvents.h" +#include "mozilla/TextEvents.h" + +#include "nsITimer.h" +#include "nsFrameManager.h" +// notifications +#include "nsIDOMDocument.h" +#include "nsIDocument.h" + +#include "nsISelectionController.h"//for the enums +#include "nsAutoCopyListener.h" +#include "SelectionChangeListener.h" +#include "nsCopySupport.h" +#include "nsIClipboard.h" +#include "nsIFrameInlines.h" + +#include "nsIBidiKeyboard.h" + +#include "nsError.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/ShadowRoot.h" +#include "mozilla/ErrorResult.h" +#include "mozilla/dom/SelectionBinding.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/Telemetry.h" +#include "mozilla/layers/ScrollInputMethods.h" +#include "nsViewManager.h" + +#include "nsIEditor.h" +#include "nsIHTMLEditor.h" +#include "nsFocusManager.h" + +using namespace mozilla; +using namespace mozilla::dom; +using mozilla::layers::ScrollInputMethod; + +//#define DEBUG_TABLE 1 + +static bool IsValidSelectionPoint(nsFrameSelection *aFrameSel, nsINode *aNode); + +static nsIAtom *GetTag(nsINode *aNode); +// returns the parent +static nsINode* ParentOffset(nsINode *aNode, int32_t *aChildOffset); +static nsINode* GetCellParent(nsINode *aDomNode); + +#ifdef PRINT_RANGE +static void printRange(nsRange *aDomRange); +#define DEBUG_OUT_RANGE(x) printRange(x) +#else +#define DEBUG_OUT_RANGE(x) +#endif // PRINT_RANGE + +/****************************************************************************** + * Utility methods defined in nsISelectionController.idl + ******************************************************************************/ + +namespace mozilla { + +const char* +ToChar(SelectionType aSelectionType) +{ + switch (aSelectionType) { + case SelectionType::eInvalid: + return "SelectionType::eInvalid"; + case SelectionType::eNone: + return "SelectionType::eNone"; + case SelectionType::eNormal: + return "SelectionType::eNormal"; + case SelectionType::eSpellCheck: + return "SelectionType::eSpellCheck"; + case SelectionType::eIMERawClause: + return "SelectionType::eIMERawClause"; + case SelectionType::eIMESelectedRawClause: + return "SelectionType::eIMESelectedRawClause"; + case SelectionType::eIMEConvertedClause: + return "SelectionType::eIMEConvertedClause"; + case SelectionType::eIMESelectedClause: + return "SelectionType::eIMESelectedClause"; + case SelectionType::eAccessibility: + return "SelectionType::eAccessibility"; + case SelectionType::eFind: + return "SelectionType::eFind"; + case SelectionType::eURLSecondary: + return "SelectionType::eURLSecondary"; + case SelectionType::eURLStrikeout: + return "SelectionType::eURLStrikeout"; + default: + return "Invalid SelectionType"; + } +} + +static bool +IsValidSelectionType(RawSelectionType aRawSelectionType) +{ + switch (static_cast<SelectionType>(aRawSelectionType)) { + case SelectionType::eNone: + case SelectionType::eNormal: + case SelectionType::eSpellCheck: + case SelectionType::eIMERawClause: + case SelectionType::eIMESelectedRawClause: + case SelectionType::eIMEConvertedClause: + case SelectionType::eIMESelectedClause: + case SelectionType::eAccessibility: + case SelectionType::eFind: + case SelectionType::eURLSecondary: + case SelectionType::eURLStrikeout: + return true; + default: + return false; + } +} + +SelectionType +ToSelectionType(RawSelectionType aRawSelectionType) +{ + if (!IsValidSelectionType(aRawSelectionType)) { + return SelectionType::eInvalid; + } + return static_cast<SelectionType>(aRawSelectionType); +} + +RawSelectionType +ToRawSelectionType(SelectionType aSelectionType) +{ + return static_cast<RawSelectionType>(aSelectionType); +} + +bool operator &(SelectionType aSelectionType, + RawSelectionType aRawSelectionTypes) +{ + return (ToRawSelectionType(aSelectionType) & aRawSelectionTypes) != 0; +} + +} // namespace mozilla + +/****************************************************************************** + * nsPeekOffsetStruct + ******************************************************************************/ + +//#define DEBUG_SELECTION // uncomment for printf describing every collapse and extend. +//#define DEBUG_NAVIGATION + + +//#define DEBUG_TABLE_SELECTION 1 + +nsPeekOffsetStruct::nsPeekOffsetStruct(nsSelectionAmount aAmount, + nsDirection aDirection, + int32_t aStartOffset, + nsPoint aDesiredPos, + bool aJumpLines, + bool aScrollViewStop, + bool aIsKeyboardSelect, + bool aVisual, + bool aExtend, + EWordMovementType aWordMovementType) + : mAmount(aAmount) + , mDirection(aDirection) + , mStartOffset(aStartOffset) + , mDesiredPos(aDesiredPos) + , mWordMovementType(aWordMovementType) + , mJumpLines(aJumpLines) + , mScrollViewStop(aScrollViewStop) + , mIsKeyboardSelect(aIsKeyboardSelect) + , mVisual(aVisual) + , mExtend(aExtend) + , mResultContent() + , mResultFrame(nullptr) + , mContentOffset(0) + , mAttach(CARET_ASSOCIATE_BEFORE) +{ +} + +struct CachedOffsetForFrame { + CachedOffsetForFrame() + : mCachedFrameOffset(0, 0) // nsPoint ctor + , mLastCaretFrame(nullptr) + , mLastContentOffset(0) + , mCanCacheFrameOffset(false) + {} + + nsPoint mCachedFrameOffset; // cached frame offset + nsIFrame* mLastCaretFrame; // store the frame the caret was last drawn in. + int32_t mLastContentOffset; // store last content offset + bool mCanCacheFrameOffset; // cached frame offset is valid? +}; + +class nsAutoScrollTimer final : public nsITimerCallback +{ +public: + + NS_DECL_ISUPPORTS + + nsAutoScrollTimer() + : mFrameSelection(0), mSelection(0), mPresContext(0), mPoint(0,0), mDelay(30) + { + } + + // aPoint is relative to aPresContext's root frame + nsresult Start(nsPresContext *aPresContext, nsPoint &aPoint) + { + mPoint = aPoint; + + // Store the presentation context. The timer will be + // stopped by the selection if the prescontext is destroyed. + mPresContext = aPresContext; + + mContent = nsIPresShell::GetCapturingContent(); + + if (!mTimer) + { + nsresult result; + mTimer = do_CreateInstance("@mozilla.org/timer;1", &result); + + if (NS_FAILED(result)) + return result; + } + + return mTimer->InitWithCallback(this, mDelay, nsITimer::TYPE_ONE_SHOT); + } + + nsresult Stop() + { + if (mTimer) + { + mTimer->Cancel(); + mTimer = nullptr; + } + + mContent = nullptr; + return NS_OK; + } + + nsresult Init(nsFrameSelection* aFrameSelection, Selection* aSelection) + { + mFrameSelection = aFrameSelection; + mSelection = aSelection; + return NS_OK; + } + + nsresult SetDelay(uint32_t aDelay) + { + mDelay = aDelay; + return NS_OK; + } + + NS_IMETHOD Notify(nsITimer *timer) override + { + if (mSelection && mPresContext) + { + nsWeakFrame frame = + mContent ? mPresContext->GetPrimaryFrameFor(mContent) : nullptr; + if (!frame) + return NS_OK; + mContent = nullptr; + + nsPoint pt = mPoint - + frame->GetOffsetTo(mPresContext->PresShell()->FrameManager()->GetRootFrame()); + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + frameSelection->HandleDrag(frame, pt); + if (!frame.IsAlive()) + return NS_OK; + + NS_ASSERTION(frame->PresContext() == mPresContext, "document mismatch?"); + mSelection->DoAutoScroll(frame, pt); + } + return NS_OK; + } + +protected: + virtual ~nsAutoScrollTimer() + { + if (mTimer) { + mTimer->Cancel(); + } + } + +private: + nsFrameSelection *mFrameSelection; + Selection* mSelection; + nsPresContext *mPresContext; + // relative to mPresContext's root frame + nsPoint mPoint; + nsCOMPtr<nsITimer> mTimer; + nsCOMPtr<nsIContent> mContent; + uint32_t mDelay; +}; + +NS_IMPL_ISUPPORTS(nsAutoScrollTimer, nsITimerCallback) + +nsresult NS_NewDomSelection(nsISelection **aDomSelection) +{ + Selection* rlist = new Selection; + *aDomSelection = (nsISelection *)rlist; + NS_ADDREF(rlist); + return NS_OK; +} + +static int8_t +GetIndexFromSelectionType(SelectionType aSelectionType) +{ + switch (aSelectionType) { + case SelectionType::eNormal: + return 0; + case SelectionType::eSpellCheck: + return 1; + case SelectionType::eIMERawClause: + return 2; + case SelectionType::eIMESelectedRawClause: + return 3; + case SelectionType::eIMEConvertedClause: + return 4; + case SelectionType::eIMESelectedClause: + return 5; + case SelectionType::eAccessibility: + return 6; + case SelectionType::eFind: + return 7; + case SelectionType::eURLSecondary: + return 8; + case SelectionType::eURLStrikeout: + return 9; + default: + return -1; + } + /* NOTREACHED */ +} + +static SelectionType +GetSelectionTypeFromIndex(int8_t aIndex) +{ + static const SelectionType kSelectionTypes[] = { + SelectionType::eNormal, + SelectionType::eSpellCheck, + SelectionType::eIMERawClause, + SelectionType::eIMESelectedRawClause, + SelectionType::eIMEConvertedClause, + SelectionType::eIMESelectedClause, + SelectionType::eAccessibility, + SelectionType::eFind, + SelectionType::eURLSecondary, + SelectionType::eURLStrikeout + }; + if (NS_WARN_IF(aIndex < 0) || + NS_WARN_IF(static_cast<size_t>(aIndex) >= ArrayLength(kSelectionTypes))) { + return SelectionType::eNormal; + } + return kSelectionTypes[aIndex]; +} + +/* +The limiter is used specifically for the text areas and textfields +In that case it is the DIV tag that is anonymously created for the text +areas/fields. Text nodes and BR nodes fall beneath it. In the case of a +BR node the limiter will be the parent and the offset will point before or +after the BR node. In the case of the text node the parent content is +the text node itself and the offset will be the exact character position. +The offset is not important to check for validity. Simply look at the +passed in content. If it equals the limiter then the selection point is valid. +If its parent it the limiter then the point is also valid. In the case of +NO limiter all points are valid since you are in a topmost iframe. (browser +or composer) +*/ +bool +IsValidSelectionPoint(nsFrameSelection *aFrameSel, nsINode *aNode) +{ + if (!aFrameSel || !aNode) + return false; + + nsIContent *limiter = aFrameSel->GetLimiter(); + if (limiter && limiter != aNode && limiter != aNode->GetParent()) { + //if newfocus == the limiter. that's ok. but if not there and not parent bad + return false; //not in the right content. tLimiter said so + } + + limiter = aFrameSel->GetAncestorLimiter(); + return !limiter || nsContentUtils::ContentIsDescendantOf(aNode, limiter); +} + +namespace mozilla { +struct MOZ_RAII AutoPrepareFocusRange +{ + AutoPrepareFocusRange(Selection* aSelection, + bool aContinueSelection, + bool aMultipleSelection + MOZ_GUARD_OBJECT_NOTIFIER_PARAM) + { + MOZ_GUARD_OBJECT_NOTIFIER_INIT; + + if (aSelection->mRanges.Length() <= 1) { + return; + } + + if (aSelection->mFrameSelection->IsUserSelectionReason()) { + mUserSelect.emplace(aSelection); + } + bool userSelection = aSelection->mUserInitiated; + + nsTArray<RangeData>& ranges = aSelection->mRanges; + if (!userSelection || + (!aContinueSelection && aMultipleSelection)) { + // Scripted command or the user is starting a new explicit multi-range + // selection. + for (RangeData& entry : ranges) { + entry.mRange->SetIsGenerated(false); + } + return; + } + + int16_t reason = aSelection->mFrameSelection->mSelectionChangeReason; + bool isAnchorRelativeOp = (reason & (nsISelectionListener::DRAG_REASON | + nsISelectionListener::MOUSEDOWN_REASON | + nsISelectionListener::MOUSEUP_REASON | + nsISelectionListener::COLLAPSETOSTART_REASON)); + if (!isAnchorRelativeOp) { + return; + } + + // This operation is against the anchor but our current mAnchorFocusRange + // represents the focus in a multi-range selection. The anchor from a user + // perspective is the most distant generated range on the opposite side. + // Find that range and make it the mAnchorFocusRange. + const size_t len = ranges.Length(); + size_t newAnchorFocusIndex = size_t(-1); + if (aSelection->GetDirection() == eDirNext) { + for (size_t i = 0; i < len; ++i) { + if (ranges[i].mRange->IsGenerated()) { + newAnchorFocusIndex = i; + break; + } + } + } else { + size_t i = len; + while (i--) { + if (ranges[i].mRange->IsGenerated()) { + newAnchorFocusIndex = i; + break; + } + } + } + + if (newAnchorFocusIndex == size_t(-1)) { + // There are no generated ranges - that's fine. + return; + } + + // Setup the new mAnchorFocusRange and mark the old one as generated. + if (aSelection->mAnchorFocusRange) { + aSelection->mAnchorFocusRange->SetIsGenerated(true); + } + nsRange* range = ranges[newAnchorFocusIndex].mRange; + range->SetIsGenerated(false); + aSelection->mAnchorFocusRange = range; + + // Remove all generated ranges (including the old mAnchorFocusRange). + RefPtr<nsPresContext> presContext = aSelection->GetPresContext(); + size_t i = len; + while (i--) { + range = aSelection->mRanges[i].mRange; + if (range->IsGenerated()) { + range->SetSelection(nullptr); + aSelection->selectFrames(presContext, range, false); + aSelection->mRanges.RemoveElementAt(i); + } + } + if (aSelection->mFrameSelection) { + aSelection->mFrameSelection->InvalidateDesiredPos(); + } + } + + Maybe<Selection::AutoUserInitiated> mUserSelect; + MOZ_DECL_USE_GUARD_OBJECT_NOTIFIER +}; + +} // namespace mozilla + +////////////BEGIN nsFrameSelection methods + +nsFrameSelection::nsFrameSelection() +{ + for (size_t i = 0; i < kPresentSelectionTypeCount; i++){ + mDomSelections[i] = new Selection(this); + mDomSelections[i]->SetType(GetSelectionTypeFromIndex(i)); + } + mBatching = 0; + mChangesDuringBatching = false; + mNotifyFrames = true; + + mMouseDoubleDownState = false; + + mHint = CARET_ASSOCIATE_BEFORE; + mCaretBidiLevel = BIDI_LEVEL_UNDEFINED; + mKbdBidiLevel = NSBIDI_LTR; + + mDragSelectingCells = false; + mSelectingTableCellMode = 0; + mSelectedCellIndex = 0; + + nsAutoCopyListener *autoCopy = nullptr; + // On macOS, cache the current selection to send to osx service menu. +#ifdef XP_MACOSX + autoCopy = nsAutoCopyListener::GetInstance(nsIClipboard::kSelectionCache); +#endif + + // Check to see if the autocopy pref is enabled + // and add the autocopy listener if it is + if (Preferences::GetBool("clipboard.autocopy")) { + autoCopy = nsAutoCopyListener::GetInstance(nsIClipboard::kSelectionClipboard); + } + + if (autoCopy) { + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (mDomSelections[index]) { + autoCopy->Listen(mDomSelections[index]); + } + } + + mDisplaySelection = nsISelectionController::SELECTION_OFF; + mSelectionChangeReason = nsISelectionListener::NO_REASON; + + mDelayedMouseEventValid = false; + // These values are not used since they are only valid when + // mDelayedMouseEventValid is true, and setting mDelayedMouseEventValid + //alwaysoverrides these values. + mDelayedMouseEventIsShift = false; + mDelayedMouseEventClickCount = 0; +} + +nsFrameSelection::~nsFrameSelection() +{ +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(nsFrameSelection) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(nsFrameSelection) + for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) { + tmp->mDomSelections[i] = nullptr; + } + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mCellParent) + tmp->mSelectingTableCellMode = 0; + tmp->mDragSelectingCells = false; + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartSelectedCell) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndSelectedCell) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAppendStartSelectedCell) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mUnselectCellOnMouseUp) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mMaintainRange) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mLimiter) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mAncestorLimiter) +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(nsFrameSelection) + if (tmp->mShell && tmp->mShell->GetDocument() && + nsCCUncollectableMarker::InGeneration(cb, + tmp->mShell->GetDocument()-> + GetMarkedCCGeneration())) { + return NS_SUCCESS_INTERRUPTED_TRAVERSE; + } + for (size_t i = 0; i < kPresentSelectionTypeCount; ++i) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDomSelections[i]) + } + + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCellParent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStartSelectedCell) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndSelectedCell) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAppendStartSelectedCell) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mUnselectCellOnMouseUp) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mMaintainRange) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mLimiter) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAncestorLimiter) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_CYCLE_COLLECTION_ROOT_NATIVE(nsFrameSelection, AddRef) +NS_IMPL_CYCLE_COLLECTION_UNROOT_NATIVE(nsFrameSelection, Release) + +// Get the x (or y, in vertical writing mode) position requested +// by the Key Handling for line-up/down +nsresult +nsFrameSelection::FetchDesiredPos(nsPoint &aDesiredPos) +{ + if (!mShell) { + NS_ERROR("fetch desired position failed"); + return NS_ERROR_FAILURE; + } + if (mDesiredPosSet) { + aDesiredPos = mDesiredPos; + return NS_OK; + } + + RefPtr<nsCaret> caret = mShell->GetCaret(); + if (!caret) { + return NS_ERROR_NULL_POINTER; + } + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + caret->SetSelection(mDomSelections[index]); + + nsRect coord; + nsIFrame* caretFrame = caret->GetGeometry(&coord); + if (!caretFrame) { + return NS_ERROR_FAILURE; + } + nsPoint viewOffset(0, 0); + nsView* view = nullptr; + caretFrame->GetOffsetFromView(viewOffset, &view); + if (view) { + coord += viewOffset; + } + aDesiredPos = coord.TopLeft(); + return NS_OK; +} + +void +nsFrameSelection::InvalidateDesiredPos() // do not listen to mDesiredPos; + // you must get another. +{ + mDesiredPosSet = false; +} + +void +nsFrameSelection::SetDesiredPos(nsPoint aPos) +{ + mDesiredPos = aPos; + mDesiredPosSet = true; +} + +nsresult +nsFrameSelection::ConstrainFrameAndPointToAnchorSubtree(nsIFrame *aFrame, + nsPoint& aPoint, + nsIFrame **aRetFrame, + nsPoint& aRetPoint) +{ + // + // The whole point of this method is to return a frame and point that + // that lie within the same valid subtree as the anchor node's frame, + // for use with the method GetContentAndOffsetsFromPoint(). + // + // A valid subtree is defined to be one where all the content nodes in + // the tree have a valid parent-child relationship. + // + // If the anchor frame and aFrame are in the same subtree, aFrame will + // be returned in aRetFrame. If they are in different subtrees, we + // return the frame for the root of the subtree. + // + + if (!aFrame || !aRetFrame) + return NS_ERROR_NULL_POINTER; + + *aRetFrame = aFrame; + aRetPoint = aPoint; + + // + // Get the frame and content for the selection's anchor point! + // + + nsresult result; + nsCOMPtr<nsIDOMNode> anchorNode; + int32_t anchorOffset = 0; + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + result = mDomSelections[index]->GetAnchorNode(getter_AddRefs(anchorNode)); + + if (NS_FAILED(result)) + return result; + + if (!anchorNode) + return NS_OK; + + result = mDomSelections[index]->GetAnchorOffset(&anchorOffset); + + if (NS_FAILED(result)) + return result; + + nsCOMPtr<nsIContent> anchorContent = do_QueryInterface(anchorNode); + + if (!anchorContent) + return NS_ERROR_FAILURE; + + // + // Now find the root of the subtree containing the anchor's content. + // + + NS_ENSURE_STATE(mShell); + nsIContent* anchorRoot = anchorContent->GetSelectionRootContent(mShell); + NS_ENSURE_TRUE(anchorRoot, NS_ERROR_UNEXPECTED); + + // + // Now find the root of the subtree containing aFrame's content. + // + + nsIContent* content = aFrame->GetContent(); + + if (content) + { + nsIContent* contentRoot = content->GetSelectionRootContent(mShell); + NS_ENSURE_TRUE(contentRoot, NS_ERROR_UNEXPECTED); + + if (anchorRoot == contentRoot) + { + // If the aFrame's content isn't the capturing content, it should be + // a descendant. At this time, we can return simply. + nsIContent* capturedContent = nsIPresShell::GetCapturingContent(); + if (capturedContent != content) + { + return NS_OK; + } + + // Find the frame under the mouse cursor with the root frame. + // At this time, don't use the anchor's frame because it may not have + // fixed positioned frames. + nsIFrame* rootFrame = mShell->FrameManager()->GetRootFrame(); + nsPoint ptInRoot = aPoint + aFrame->GetOffsetTo(rootFrame); + nsIFrame* cursorFrame = + nsLayoutUtils::GetFrameForPoint(rootFrame, ptInRoot); + + // If the mouse cursor in on a frame which is descendant of same + // selection root, we can expand the selection to the frame. + if (cursorFrame && cursorFrame->PresContext()->PresShell() == mShell) + { + nsIContent* cursorContent = cursorFrame->GetContent(); + NS_ENSURE_TRUE(cursorContent, NS_ERROR_FAILURE); + nsIContent* cursorContentRoot = + cursorContent->GetSelectionRootContent(mShell); + NS_ENSURE_TRUE(cursorContentRoot, NS_ERROR_UNEXPECTED); + if (cursorContentRoot == anchorRoot) + { + *aRetFrame = cursorFrame; + aRetPoint = aPoint + aFrame->GetOffsetTo(cursorFrame); + return NS_OK; + } + } + // Otherwise, e.g., the cursor isn't on any frames (e.g., the mouse + // cursor is out of the window), we should use the frame of the anchor + // root. + } + } + + // + // When we can't find a frame which is under the mouse cursor and has a same + // selection root as the anchor node's, we should return the selection root + // frame. + // + + *aRetFrame = anchorRoot->GetPrimaryFrame(); + + if (!*aRetFrame) + return NS_ERROR_FAILURE; + + // + // Now make sure that aRetPoint is converted to the same coordinate + // system used by aRetFrame. + // + + aRetPoint = aPoint + aFrame->GetOffsetTo(*aRetFrame); + + return NS_OK; +} + +void +nsFrameSelection::SetCaretBidiLevel(nsBidiLevel aLevel) +{ + // If the current level is undefined, we have just inserted new text. + // In this case, we don't want to reset the keyboard language + mCaretBidiLevel = aLevel; + + RefPtr<nsCaret> caret; + if (mShell && (caret = mShell->GetCaret())) { + caret->SchedulePaint(); + } + + return; +} + +nsBidiLevel +nsFrameSelection::GetCaretBidiLevel() const +{ + return mCaretBidiLevel; +} + +void +nsFrameSelection::UndefineCaretBidiLevel() +{ + mCaretBidiLevel |= BIDI_LEVEL_UNDEFINED; +} + +#ifdef PRINT_RANGE +void printRange(nsRange *aDomRange) +{ + if (!aDomRange) + { + printf("NULL nsIDOMRange\n"); + } + nsINode* startNode = aDomRange->GetStartParent(); + nsINode* endNode = aDomRange->GetEndParent(); + int32_t startOffset = aDomRange->StartOffset(); + int32_t endOffset = aDomRange->EndOffset(); + + printf("range: 0x%lx\t start: 0x%lx %ld, \t end: 0x%lx,%ld\n", + (unsigned long)aDomRange, + (unsigned long)startNode, (long)startOffset, + (unsigned long)endNode, (long)endOffset); + +} +#endif /* PRINT_RANGE */ + +static +nsIAtom *GetTag(nsINode *aNode) +{ + nsCOMPtr<nsIContent> content = do_QueryInterface(aNode); + if (!content) + { + NS_NOTREACHED("bad node passed to GetTag()"); + return nullptr; + } + + return content->NodeInfo()->NameAtom(); +} + +// Returns the parent +nsINode* +ParentOffset(nsINode *aNode, int32_t *aChildOffset) +{ + if (!aNode || !aChildOffset) + return nullptr; + + nsIContent* parent = aNode->GetParent(); + if (parent) + { + *aChildOffset = parent->IndexOf(aNode); + + return parent; + } + + return nullptr; +} + +static nsINode* +GetCellParent(nsINode *aDomNode) +{ + if (!aDomNode) + return nullptr; + nsINode* current = aDomNode; + // Start with current node and look for a table cell + while (current) + { + nsIAtom* tag = GetTag(current); + if (tag == nsGkAtoms::td || tag == nsGkAtoms::th) + return current; + current = current->GetParent(); + } + return nullptr; +} + +void +nsFrameSelection::Init(nsIPresShell *aShell, nsIContent *aLimiter) +{ + mShell = aShell; + mDragState = false; + mDesiredPosSet = false; + mLimiter = aLimiter; + mCaretMovementStyle = + Preferences::GetInt("bidi.edit.caret_movement_style", 2); + + // This should only ever be initialized on the main thread, so we are OK here. + static bool prefCachesInitialized = false; + if (!prefCachesInitialized) { + prefCachesInitialized = true; + + Preferences::AddBoolVarCache(&sSelectionEventsEnabled, + "dom.select_events.enabled", false); + Preferences::AddBoolVarCache(&sSelectionEventsOnTextControlsEnabled, + "dom.select_events.textcontrols.enabled", false); + } + + RefPtr<AccessibleCaretEventHub> eventHub = mShell->GetAccessibleCaretEventHub(); + if (eventHub) { + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (mDomSelections[index]) { + mDomSelections[index]->AddSelectionListener(eventHub); + } + } + + nsIDocument* doc = aShell->GetDocument(); + if (sSelectionEventsEnabled || + (doc && nsContentUtils::IsSystemPrincipal(doc->NodePrincipal()))) { + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (mDomSelections[index]) { + // The Selection instance will hold a strong reference to its selectionchangelistener + // so we don't have to worry about that! + RefPtr<SelectionChangeListener> listener = new SelectionChangeListener; + mDomSelections[index]->AddSelectionListener(listener); + } + } +} + +bool nsFrameSelection::sSelectionEventsEnabled = false; +bool nsFrameSelection::sSelectionEventsOnTextControlsEnabled = false; + +nsresult +nsFrameSelection::MoveCaret(nsDirection aDirection, + bool aContinueSelection, + nsSelectionAmount aAmount, + CaretMovementStyle aMovementStyle) +{ + bool visualMovement = aMovementStyle == eVisual || + (aMovementStyle == eUsePrefStyle && + (mCaretMovementStyle == 1 || + (mCaretMovementStyle == 2 && !aContinueSelection))); + + NS_ENSURE_STATE(mShell); + // Flush out layout, since we need it to be up to date to do caret + // positioning. + mShell->FlushPendingNotifications(Flush_Layout); + + if (!mShell) { + return NS_OK; + } + + nsPresContext *context = mShell->GetPresContext(); + if (!context) + return NS_ERROR_FAILURE; + + bool isCollapsed; + nsPoint desiredPos(0, 0); //we must keep this around and revalidate it when its just UP/DOWN + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + RefPtr<Selection> sel = mDomSelections[index]; + if (!sel) + return NS_ERROR_NULL_POINTER; + + int32_t scrollFlags = Selection::SCROLL_FOR_CARET_MOVE; + nsINode* focusNode = sel->GetFocusNode(); + if (focusNode && + (focusNode->IsEditable() || + (focusNode->IsElement() && + focusNode->AsElement()->State(). + HasState(NS_EVENT_STATE_MOZ_READWRITE)))) { + // If caret moves in editor, it should cause scrolling even if it's in + // overflow: hidden;. + scrollFlags |= Selection::SCROLL_OVERFLOW_HIDDEN; + } + + nsresult result = sel->GetIsCollapsed(&isCollapsed); + if (NS_FAILED(result)) { + return result; + } + + int32_t caretStyle = Preferences::GetInt("layout.selection.caret_style", 0); + if (caretStyle == 0 +#ifdef XP_WIN + && aAmount != eSelectLine +#endif + ) { + // Put caret at the selection edge in the |aDirection| direction. + caretStyle = 2; + } + + bool doCollapse = !isCollapsed && !aContinueSelection && caretStyle == 2 && + aAmount <= eSelectLine; + if (doCollapse) { + if (aDirection == eDirPrevious) { + PostReason(nsISelectionListener::COLLAPSETOSTART_REASON); + mHint = CARET_ASSOCIATE_AFTER; + } else { + PostReason(nsISelectionListener::COLLAPSETOEND_REASON); + mHint = CARET_ASSOCIATE_BEFORE; + } + } else { + PostReason(nsISelectionListener::KEYPRESS_REASON); + } + + AutoPrepareFocusRange prep(sel, aContinueSelection, false); + + if (aAmount == eSelectLine) { + result = FetchDesiredPos(desiredPos); + if (NS_FAILED(result)) { + return result; + } + SetDesiredPos(desiredPos); + } + + if (doCollapse) { + const nsRange* anchorFocusRange = sel->GetAnchorFocusRange(); + if (anchorFocusRange) { + nsINode* node; + int32_t offset; + if (aDirection == eDirPrevious) { + node = anchorFocusRange->GetStartParent(); + offset = anchorFocusRange->StartOffset(); + } else { + node = anchorFocusRange->GetEndParent(); + offset = anchorFocusRange->EndOffset(); + } + sel->Collapse(node, offset); + } + sel->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, + nsIPresShell::ScrollAxis(), + nsIPresShell::ScrollAxis(), scrollFlags); + return NS_OK; + } + + nsIFrame *frame; + int32_t offsetused = 0; + result = sel->GetPrimaryFrameForFocusNode(&frame, &offsetused, + visualMovement); + + if (NS_FAILED(result) || !frame) + return NS_FAILED(result) ? result : NS_ERROR_FAILURE; + + //set data using mLimiter to stop on scroll views. If we have a limiter then we stop peeking + //when we hit scrollable views. If no limiter then just let it go ahead + nsPeekOffsetStruct pos(aAmount, eDirPrevious, offsetused, desiredPos, + true, mLimiter != nullptr, true, visualMovement, + aContinueSelection); + + nsBidiDirection paraDir = nsBidiPresUtils::ParagraphDirection(frame); + + CaretAssociateHint tHint(mHint); //temporary variable so we dont set mHint until it is necessary + switch (aAmount){ + case eSelectCharacter: + case eSelectCluster: + case eSelectWord: + case eSelectWordNoSpace: + InvalidateDesiredPos(); + pos.mAmount = aAmount; + pos.mDirection = (visualMovement && paraDir == NSBIDI_RTL) + ? nsDirection(1 - aDirection) : aDirection; + break; + case eSelectLine: + pos.mAmount = aAmount; + pos.mDirection = aDirection; + break; + case eSelectBeginLine: + case eSelectEndLine: + InvalidateDesiredPos(); + pos.mAmount = aAmount; + pos.mDirection = (visualMovement && paraDir == NSBIDI_RTL) + ? nsDirection(1 - aDirection) : aDirection; + break; + default: + return NS_ERROR_FAILURE; + } + + if (NS_SUCCEEDED(result = frame->PeekOffset(&pos)) && pos.mResultContent) + { + nsIFrame *theFrame; + int32_t currentOffset, frameStart, frameEnd; + + if (aAmount <= eSelectWordNoSpace) + { + // For left/right, PeekOffset() sets pos.mResultFrame correctly, but does not set pos.mAttachForward, + // so determine the hint here based on the result frame and offset: + // If we're at the end of a text frame, set the hint to ASSOCIATE_BEFORE to indicate that we + // want the caret displayed at the end of this frame, not at the beginning of the next one. + theFrame = pos.mResultFrame; + theFrame->GetOffsets(frameStart, frameEnd); + currentOffset = pos.mContentOffset; + if (frameEnd == currentOffset && !(frameStart == 0 && frameEnd == 0)) + tHint = CARET_ASSOCIATE_BEFORE; + else + tHint = CARET_ASSOCIATE_AFTER; + } else { + // For up/down and home/end, pos.mResultFrame might not be set correctly, or not at all. + // In these cases, get the frame based on the content and hint returned by PeekOffset(). + tHint = pos.mAttach; + theFrame = GetFrameForNodeOffset(pos.mResultContent, pos.mContentOffset, + tHint, ¤tOffset); + if (!theFrame) + return NS_ERROR_FAILURE; + + theFrame->GetOffsets(frameStart, frameEnd); + } + + if (context->BidiEnabled()) + { + switch (aAmount) { + case eSelectBeginLine: + case eSelectEndLine: { + // In Bidi contexts, PeekOffset calculates pos.mContentOffset + // differently depending on whether the movement is visual or logical. + // For visual movement, pos.mContentOffset depends on the direction- + // ality of the first/last frame on the line (theFrame), and the caret + // directionality must correspond. + FrameBidiData bidiData = theFrame->GetBidiData(); + SetCaretBidiLevel(visualMovement ? bidiData.embeddingLevel + : bidiData.baseLevel); + break; + } + default: + // If the current position is not a frame boundary, it's enough just + // to take the Bidi level of the current frame + if ((pos.mContentOffset != frameStart && + pos.mContentOffset != frameEnd) || + eSelectLine == aAmount) { + SetCaretBidiLevel(theFrame->GetEmbeddingLevel()); + } + else { + BidiLevelFromMove(mShell, pos.mResultContent, pos.mContentOffset, + aAmount, tHint); + } + } + } + result = TakeFocus(pos.mResultContent, pos.mContentOffset, pos.mContentOffset, + tHint, aContinueSelection, false); + } else if (aAmount <= eSelectWordNoSpace && aDirection == eDirNext && + !aContinueSelection) { + // Collapse selection if PeekOffset failed, we either + // 1. bumped into the BRFrame, bug 207623 + // 2. had select-all in a text input (DIV range), bug 352759. + bool isBRFrame = frame->GetType() == nsGkAtoms::brFrame; + sel->Collapse(sel->GetFocusNode(), sel->FocusOffset()); + // Note: 'frame' might be dead here. + if (!isBRFrame) { + mHint = CARET_ASSOCIATE_BEFORE; // We're now at the end of the frame to the left. + } + result = NS_OK; + } + if (NS_SUCCEEDED(result)) + { + result = mDomSelections[index]-> + ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, + nsIPresShell::ScrollAxis(), nsIPresShell::ScrollAxis(), + scrollFlags); + } + + return result; +} + +//END nsFrameSelection methods + + +//BEGIN nsFrameSelection methods + +NS_IMETHODIMP +Selection::ToString(nsAString& aReturn) +{ + // We need Flush_Style here to make sure frames have been created for + // the selected content. Use mFrameSelection->GetShell() which returns + // null if the Selection has been disconnected (the shell is Destroyed). + nsCOMPtr<nsIPresShell> shell = + mFrameSelection ? mFrameSelection->GetShell() : nullptr; + if (!shell) { + aReturn.Truncate(); + return NS_OK; + } + shell->FlushPendingNotifications(Flush_Style); + + return ToStringWithFormat("text/plain", + nsIDocumentEncoder::SkipInvisibleContent, + 0, aReturn); +} + +void +Selection::Stringify(nsAString& aResult) +{ + // Eat the error code + ToString(aResult); +} + +NS_IMETHODIMP +Selection::ToStringWithFormat(const char* aFormatType, uint32_t aFlags, + int32_t aWrapCol, nsAString& aReturn) +{ + ErrorResult result; + NS_ConvertUTF8toUTF16 format(aFormatType); + ToStringWithFormat(format, aFlags, aWrapCol, aReturn, result); + if (result.Failed()) { + return result.StealNSResult(); + } + return NS_OK; +} + +void +Selection::ToStringWithFormat(const nsAString& aFormatType, uint32_t aFlags, + int32_t aWrapCol, nsAString& aReturn, + ErrorResult& aRv) +{ + nsresult rv = NS_OK; + NS_ConvertUTF8toUTF16 formatType( NS_DOC_ENCODER_CONTRACTID_BASE ); + formatType.Append(aFormatType); + nsCOMPtr<nsIDocumentEncoder> encoder = + do_CreateInstance(NS_ConvertUTF16toUTF8(formatType).get(), &rv); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + nsIPresShell* shell = GetPresShell(); + if (!shell) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + nsIDocument *doc = shell->GetDocument(); + + nsCOMPtr<nsIDOMDocument> domDoc = do_QueryInterface(doc); + NS_ASSERTION(domDoc, "Need a document"); + + // Flags should always include OutputSelectionOnly if we're coming from here: + aFlags |= nsIDocumentEncoder::OutputSelectionOnly; + nsAutoString readstring; + readstring.Assign(aFormatType); + rv = encoder->Init(domDoc, readstring, aFlags); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + encoder->SetSelection(this); + if (aWrapCol != 0) + encoder->SetWrapColumn(aWrapCol); + + rv = encoder->EncodeToString(aReturn); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +NS_IMETHODIMP +Selection::SetInterlinePosition(bool aHintRight) +{ + ErrorResult result; + SetInterlinePosition(aHintRight, result); + if (result.Failed()) { + return result.StealNSResult(); + } + return NS_OK; +} + +void +Selection::SetInterlinePosition(bool aHintRight, ErrorResult& aRv) +{ + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection + return; + } + mFrameSelection->SetHint(aHintRight ? CARET_ASSOCIATE_AFTER : CARET_ASSOCIATE_BEFORE); +} + +NS_IMETHODIMP +Selection::GetInterlinePosition(bool* aHintRight) +{ + ErrorResult result; + *aHintRight = GetInterlinePosition(result); + if (result.Failed()) { + return result.StealNSResult(); + } + return NS_OK; +} + +bool +Selection::GetInterlinePosition(ErrorResult& aRv) +{ + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection + return false; + } + return mFrameSelection->GetHint() == CARET_ASSOCIATE_AFTER; +} + +Nullable<int16_t> +Selection::GetCaretBidiLevel(mozilla::ErrorResult& aRv) const +{ + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return Nullable<int16_t>(); + } + nsBidiLevel caretBidiLevel = mFrameSelection->GetCaretBidiLevel(); + return (caretBidiLevel & BIDI_LEVEL_UNDEFINED) ? + Nullable<int16_t>() : Nullable<int16_t>(caretBidiLevel); +} + +void +Selection::SetCaretBidiLevel(const Nullable<int16_t>& aCaretBidiLevel, mozilla::ErrorResult& aRv) +{ + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); + return; + } + if (aCaretBidiLevel.IsNull()) { + mFrameSelection->UndefineCaretBidiLevel(); + } else { + mFrameSelection->SetCaretBidiLevel(aCaretBidiLevel.Value()); + } +} + +nsPrevNextBidiLevels +nsFrameSelection::GetPrevNextBidiLevels(nsIContent *aNode, + uint32_t aContentOffset, + bool aJumpLines) const +{ + return GetPrevNextBidiLevels(aNode, aContentOffset, mHint, aJumpLines); +} + +nsPrevNextBidiLevels +nsFrameSelection::GetPrevNextBidiLevels(nsIContent* aNode, + uint32_t aContentOffset, + CaretAssociateHint aHint, + bool aJumpLines) const +{ + // Get the level of the frames on each side + nsIFrame *currentFrame; + int32_t currentOffset; + int32_t frameStart, frameEnd; + nsDirection direction; + + nsPrevNextBidiLevels levels; + levels.SetData(nullptr, nullptr, 0, 0); + + currentFrame = GetFrameForNodeOffset(aNode, aContentOffset, + aHint, ¤tOffset); + if (!currentFrame) + return levels; + + currentFrame->GetOffsets(frameStart, frameEnd); + + if (0 == frameStart && 0 == frameEnd) + direction = eDirPrevious; + else if (frameStart == currentOffset) + direction = eDirPrevious; + else if (frameEnd == currentOffset) + direction = eDirNext; + else { + // we are neither at the beginning nor at the end of the frame, so we have no worries + nsBidiLevel currentLevel = currentFrame->GetEmbeddingLevel(); + levels.SetData(currentFrame, currentFrame, currentLevel, currentLevel); + return levels; + } + + nsIFrame *newFrame; + int32_t offset; + bool jumpedLine, movedOverNonSelectableText; + nsresult rv = currentFrame->GetFrameFromDirection(direction, false, + aJumpLines, true, + &newFrame, &offset, &jumpedLine, + &movedOverNonSelectableText); + if (NS_FAILED(rv)) + newFrame = nullptr; + + FrameBidiData currentBidi = currentFrame->GetBidiData(); + nsBidiLevel currentLevel = currentBidi.embeddingLevel; + nsBidiLevel newLevel = newFrame ? newFrame->GetEmbeddingLevel() + : currentBidi.baseLevel; + + // If not jumping lines, disregard br frames, since they might be positioned incorrectly. + // XXX This could be removed once bug 339786 is fixed. + if (!aJumpLines) { + if (currentFrame->GetType() == nsGkAtoms::brFrame) { + currentFrame = nullptr; + currentLevel = currentBidi.baseLevel; + } + if (newFrame && newFrame->GetType() == nsGkAtoms::brFrame) { + newFrame = nullptr; + newLevel = currentBidi.baseLevel; + } + } + + if (direction == eDirNext) + levels.SetData(currentFrame, newFrame, currentLevel, newLevel); + else + levels.SetData(newFrame, currentFrame, newLevel, currentLevel); + + return levels; +} + +nsresult +nsFrameSelection::GetFrameFromLevel(nsIFrame *aFrameIn, + nsDirection aDirection, + nsBidiLevel aBidiLevel, + nsIFrame **aFrameOut) const +{ + NS_ENSURE_STATE(mShell); + nsBidiLevel foundLevel = 0; + nsIFrame *foundFrame = aFrameIn; + + nsCOMPtr<nsIFrameEnumerator> frameTraversal; + nsresult result; + nsCOMPtr<nsIFrameTraversal> trav(do_CreateInstance(kFrameTraversalCID,&result)); + if (NS_FAILED(result)) + return result; + + result = trav->NewFrameTraversal(getter_AddRefs(frameTraversal), + mShell->GetPresContext(), aFrameIn, + eLeaf, + false, // aVisual + false, // aLockInScrollView + false, // aFollowOOFs + false // aSkipPopupChecks + ); + if (NS_FAILED(result)) + return result; + + do { + *aFrameOut = foundFrame; + if (aDirection == eDirNext) + frameTraversal->Next(); + else + frameTraversal->Prev(); + + foundFrame = frameTraversal->CurrentItem(); + if (!foundFrame) + return NS_ERROR_FAILURE; + foundLevel = foundFrame->GetEmbeddingLevel(); + + } while (foundLevel > aBidiLevel); + + return NS_OK; +} + + +nsresult +nsFrameSelection::MaintainSelection(nsSelectionAmount aAmount) +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + mMaintainedAmount = aAmount; + + const nsRange* anchorFocusRange = + mDomSelections[index]->GetAnchorFocusRange(); + if (anchorFocusRange && aAmount != eSelectNoAmount) { + mMaintainRange = anchorFocusRange->CloneRange(); + return NS_OK; + } + + mMaintainRange = nullptr; + return NS_OK; +} + + +/** After moving the caret, its Bidi level is set according to the following rules: + * + * After moving over a character with left/right arrow, set to the Bidi level of the last moved over character. + * After Home and End, set to the paragraph embedding level. + * After up/down arrow, PageUp/Down, set to the lower level of the 2 surrounding characters. + * After mouse click, set to the level of the current frame. + * + * The following two methods use GetPrevNextBidiLevels to determine the new Bidi level. + * BidiLevelFromMove is called when the caret is moved in response to a keyboard event + * + * @param aPresShell is the presentation shell + * @param aNode is the content node + * @param aContentOffset is the new caret position, as an offset into aNode + * @param aAmount is the amount of the move that gave the caret its new position + * @param aHint is the hint indicating in what logical direction the caret moved + */ +void nsFrameSelection::BidiLevelFromMove(nsIPresShell* aPresShell, + nsIContent* aNode, + uint32_t aContentOffset, + nsSelectionAmount aAmount, + CaretAssociateHint aHint) +{ + switch (aAmount) { + + // Movement within the line: the new cursor Bidi level is the level of the + // last character moved over + case eSelectCharacter: + case eSelectCluster: + case eSelectWord: + case eSelectWordNoSpace: + case eSelectBeginLine: + case eSelectEndLine: + case eSelectNoAmount: + { + nsPrevNextBidiLevels levels = GetPrevNextBidiLevels(aNode, aContentOffset, + aHint, false); + + SetCaretBidiLevel(aHint == CARET_ASSOCIATE_BEFORE ? + levels.mLevelBefore : levels.mLevelAfter); + break; + } + /* + // Up and Down: the new cursor Bidi level is the smaller of the two surrounding characters + case eSelectLine: + case eSelectParagraph: + GetPrevNextBidiLevels(aContext, aNode, aContentOffset, &firstFrame, &secondFrame, &firstLevel, &secondLevel); + aPresShell->SetCaretBidiLevel(std::min(firstLevel, secondLevel)); + break; + */ + + default: + UndefineCaretBidiLevel(); + } +} + +/** + * BidiLevelFromClick is called when the caret is repositioned by clicking the mouse + * + * @param aNode is the content node + * @param aContentOffset is the new caret position, as an offset into aNode + */ +void nsFrameSelection::BidiLevelFromClick(nsIContent *aNode, + uint32_t aContentOffset) +{ + nsIFrame* clickInFrame=nullptr; + int32_t OffsetNotUsed; + + clickInFrame = GetFrameForNodeOffset(aNode, aContentOffset, mHint, &OffsetNotUsed); + if (!clickInFrame) + return; + + SetCaretBidiLevel(clickInFrame->GetEmbeddingLevel()); +} + + +bool +nsFrameSelection::AdjustForMaintainedSelection(nsIContent *aContent, + int32_t aOffset) +{ + if (!mMaintainRange) + return false; + + if (!aContent) { + return false; + } + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return false; + + nsINode* rangeStartNode = mMaintainRange->GetStartParent(); + nsINode* rangeEndNode = mMaintainRange->GetEndParent(); + int32_t rangeStartOffset = mMaintainRange->StartOffset(); + int32_t rangeEndOffset = mMaintainRange->EndOffset(); + + int32_t relToStart = + nsContentUtils::ComparePoints(rangeStartNode, rangeStartOffset, + aContent, aOffset); + int32_t relToEnd = + nsContentUtils::ComparePoints(rangeEndNode, rangeEndOffset, + aContent, aOffset); + + // If aContent/aOffset is inside the maintained selection, or if it is on the + // "anchor" side of the maintained selection, we need to do something. + if ((relToStart < 0 && relToEnd > 0) || + (relToStart > 0 && + mDomSelections[index]->GetDirection() == eDirNext) || + (relToEnd < 0 && + mDomSelections[index]->GetDirection() == eDirPrevious)) { + // Set the current range to the maintained range. + mDomSelections[index]->ReplaceAnchorFocusRange(mMaintainRange); + if (relToStart < 0 && relToEnd > 0) { + // We're inside the maintained selection, just keep it selected. + return true; + } + // Reverse the direction of the selection so that the anchor will be on the + // far side of the maintained selection, relative to aContent/aOffset. + mDomSelections[index]->SetDirection(relToStart > 0 ? eDirPrevious : eDirNext); + } + return false; +} + + +nsresult +nsFrameSelection::HandleClick(nsIContent* aNewFocus, + uint32_t aContentOffset, + uint32_t aContentEndOffset, + bool aContinueSelection, + bool aMultipleSelection, + CaretAssociateHint aHint) +{ + if (!aNewFocus) + return NS_ERROR_INVALID_ARG; + + InvalidateDesiredPos(); + + if (!aContinueSelection) { + mMaintainRange = nullptr; + if (!IsValidSelectionPoint(this, aNewFocus)) { + mAncestorLimiter = nullptr; + } + } + + // Don't take focus when dragging off of a table + if (!mDragSelectingCells) + { + BidiLevelFromClick(aNewFocus, aContentOffset); + PostReason(nsISelectionListener::MOUSEDOWN_REASON + nsISelectionListener::DRAG_REASON); + if (aContinueSelection && + AdjustForMaintainedSelection(aNewFocus, aContentOffset)) + return NS_OK; //shift clicked to maintained selection. rejected. + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + AutoPrepareFocusRange prep(mDomSelections[index], aContinueSelection, aMultipleSelection); + return TakeFocus(aNewFocus, aContentOffset, aContentEndOffset, aHint, + aContinueSelection, aMultipleSelection); + } + + return NS_OK; +} + +void +nsFrameSelection::HandleDrag(nsIFrame *aFrame, nsPoint aPoint) +{ + if (!aFrame || !mShell) + return; + + nsresult result; + nsIFrame *newFrame = 0; + nsPoint newPoint; + + result = ConstrainFrameAndPointToAnchorSubtree(aFrame, aPoint, &newFrame, newPoint); + if (NS_FAILED(result)) + return; + if (!newFrame) + return; + + nsIFrame::ContentOffsets offsets = + newFrame->GetContentOffsetsFromPoint(newPoint); + if (!offsets.content) + return; + + if (newFrame->IsSelected() && + AdjustForMaintainedSelection(offsets.content, offsets.offset)) + return; + + // Adjust offsets according to maintained amount + if (mMaintainRange && + mMaintainedAmount != eSelectNoAmount) { + + nsINode* rangenode = mMaintainRange->GetStartParent(); + int32_t rangeOffset = mMaintainRange->StartOffset(); + int32_t relativePosition = + nsContentUtils::ComparePoints(rangenode, rangeOffset, + offsets.content, offsets.offset); + + nsDirection direction = relativePosition > 0 ? eDirPrevious : eDirNext; + nsSelectionAmount amount = mMaintainedAmount; + if (amount == eSelectBeginLine && direction == eDirNext) + amount = eSelectEndLine; + + int32_t offset; + nsIFrame* frame = GetFrameForNodeOffset(offsets.content, offsets.offset, + CARET_ASSOCIATE_AFTER, &offset); + + if (frame && amount == eSelectWord && direction == eDirPrevious) { + // To avoid selecting the previous word when at start of word, + // first move one character forward. + nsPeekOffsetStruct charPos(eSelectCharacter, eDirNext, offset, + nsPoint(0, 0), false, mLimiter != nullptr, + false, false, false); + if (NS_SUCCEEDED(frame->PeekOffset(&charPos))) { + frame = charPos.mResultFrame; + offset = charPos.mContentOffset; + } + } + + nsPeekOffsetStruct pos(amount, direction, offset, nsPoint(0, 0), + false, mLimiter != nullptr, false, false, false); + + if (frame && NS_SUCCEEDED(frame->PeekOffset(&pos)) && pos.mResultContent) { + offsets.content = pos.mResultContent; + offsets.offset = pos.mContentOffset; + } + } + + HandleClick(offsets.content, offsets.offset, offsets.offset, + true, false, offsets.associate); +} + +nsresult +nsFrameSelection::StartAutoScrollTimer(nsIFrame *aFrame, + nsPoint aPoint, + uint32_t aDelay) +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + return mDomSelections[index]->StartAutoScrollTimer(aFrame, aPoint, aDelay); +} + +void +nsFrameSelection::StopAutoScrollTimer() +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return; + + mDomSelections[index]->StopAutoScrollTimer(); +} + +/** +hard to go from nodes to frames, easy the other way! + */ +nsresult +nsFrameSelection::TakeFocus(nsIContent* aNewFocus, + uint32_t aContentOffset, + uint32_t aContentEndOffset, + CaretAssociateHint aHint, + bool aContinueSelection, + bool aMultipleSelection) +{ + if (!aNewFocus) + return NS_ERROR_NULL_POINTER; + + NS_ENSURE_STATE(mShell); + + if (!IsValidSelectionPoint(this,aNewFocus)) + return NS_ERROR_FAILURE; + + // Clear all table selection data + mSelectingTableCellMode = 0; + mDragSelectingCells = false; + mStartSelectedCell = nullptr; + mEndSelectedCell = nullptr; + mAppendStartSelectedCell = nullptr; + mHint = aHint; + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + Maybe<Selection::AutoUserInitiated> userSelect; + if (IsUserSelectionReason()) { + userSelect.emplace(mDomSelections[index]); + } + + //traverse through document and unselect crap here + if (!aContinueSelection) {//single click? setting cursor down + uint32_t batching = mBatching;//hack to use the collapse code. + bool changes = mChangesDuringBatching; + mBatching = 1; + + if (aMultipleSelection) { + // Remove existing collapsed ranges as there's no point in having + // non-anchor/focus collapsed ranges. + mDomSelections[index]->RemoveCollapsedRanges(); + + RefPtr<nsRange> newRange = new nsRange(aNewFocus); + + newRange->SetStart(aNewFocus, aContentOffset); + newRange->SetEnd(aNewFocus, aContentOffset); + mDomSelections[index]->AddRange(newRange); + mBatching = batching; + mChangesDuringBatching = changes; + } else { + bool oldDesiredPosSet = mDesiredPosSet; //need to keep old desired position if it was set. + mDomSelections[index]->Collapse(aNewFocus, aContentOffset); + mDesiredPosSet = oldDesiredPosSet; //now reset desired pos back. + mBatching = batching; + mChangesDuringBatching = changes; + } + if (aContentEndOffset != aContentOffset) { + mDomSelections[index]->Extend(aNewFocus, aContentEndOffset); + } + + //find out if we are inside a table. if so, find out which one and which cell + //once we do that, the next time we get a takefocus, check the parent tree. + //if we are no longer inside same table ,cell then switch to table selection mode. + // BUT only do this in an editor + + NS_ENSURE_STATE(mShell); + bool editableCell = false; + RefPtr<nsPresContext> context = mShell->GetPresContext(); + if (context) { + nsCOMPtr<nsIHTMLEditor> editor = do_QueryInterface(nsContentUtils::GetHTMLEditor(context)); + if (editor) { + nsINode* cellparent = GetCellParent(aNewFocus); + nsCOMPtr<nsINode> editorHostNode = editor->GetActiveEditingHost(); + editableCell = cellparent && editorHostNode && + nsContentUtils::ContentIsDescendantOf(cellparent, editorHostNode); + if (editableCell) { + mCellParent = cellparent; +#ifdef DEBUG_TABLE_SELECTION + printf(" * TakeFocus - Collapsing into new cell\n"); +#endif + } + } + } + } + else { + // Now update the range list: + if (aContinueSelection && aNewFocus) + { + int32_t offset; + nsINode *cellparent = GetCellParent(aNewFocus); + if (mCellParent && cellparent && cellparent != mCellParent) //switch to cell selection mode + { +#ifdef DEBUG_TABLE_SELECTION +printf(" * TakeFocus - moving into new cell\n"); +#endif + WidgetMouseEvent event(false, eVoidEvent, nullptr, + WidgetMouseEvent::eReal); + + // Start selecting in the cell we were in before + nsINode* parent = ParentOffset(mCellParent, &offset); + if (parent) + HandleTableSelection(parent, offset, + nsISelectionPrivate::TABLESELECTION_CELL, &event); + + // Find the parent of this new cell and extend selection to it + parent = ParentOffset(cellparent, &offset); + + // XXXX We need to REALLY get the current key shift state + // (we'd need to add event listener -- let's not bother for now) + event.mModifiers &= ~MODIFIER_SHIFT; //aContinueSelection; + if (parent) + { + mCellParent = cellparent; + // Continue selection into next cell + HandleTableSelection(parent, offset, + nsISelectionPrivate::TABLESELECTION_CELL, &event); + } + } + else + { + // XXXX Problem: Shift+click in browser is appending text selection to selected table!!! + // is this the place to erase seleced cells ????? + if (mDomSelections[index]->GetDirection() == eDirNext && aContentEndOffset > aContentOffset) //didn't go far enough + { + mDomSelections[index]->Extend(aNewFocus, aContentEndOffset);//this will only redraw the diff + } + else + mDomSelections[index]->Extend(aNewFocus, aContentOffset); + } + } + } + + // Don't notify selection listeners if batching is on: + if (GetBatching()) + return NS_OK; + return NotifySelectionListeners(SelectionType::eNormal); +} + + +SelectionDetails* +nsFrameSelection::LookUpSelection(nsIContent *aContent, + int32_t aContentOffset, + int32_t aContentLength, + bool aSlowCheck) const +{ + if (!aContent || !mShell) + return nullptr; + + SelectionDetails* details = nullptr; + + for (size_t j = 0; j < kPresentSelectionTypeCount; j++) { + if (mDomSelections[j]) { + mDomSelections[j]->LookUpSelection(aContent, aContentOffset, + aContentLength, &details, + ToSelectionType(1 << j), + aSlowCheck); + } + } + + return details; +} + +void +nsFrameSelection::SetDragState(bool aState) +{ + if (mDragState == aState) + return; + + mDragState = aState; + + if (!mDragState) + { + mDragSelectingCells = false; + // Notify that reason is mouse up. + PostReason(nsISelectionListener::MOUSEUP_REASON); + NotifySelectionListeners(SelectionType::eNormal); + } +} + +Selection* +nsFrameSelection::GetSelection(SelectionType aSelectionType) const +{ + int8_t index = GetIndexFromSelectionType(aSelectionType); + if (index < 0) + return nullptr; + + return mDomSelections[index]; +} + +nsresult +nsFrameSelection::ScrollSelectionIntoView(SelectionType aSelectionType, + SelectionRegion aRegion, + int16_t aFlags) const +{ + int8_t index = GetIndexFromSelectionType(aSelectionType); + if (index < 0) + return NS_ERROR_INVALID_ARG; + + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + nsIPresShell::ScrollAxis verticalScroll = nsIPresShell::ScrollAxis(); + int32_t flags = Selection::SCROLL_DO_FLUSH; + if (aFlags & nsISelectionController::SCROLL_SYNCHRONOUS) { + flags |= Selection::SCROLL_SYNCHRONOUS; + } else if (aFlags & nsISelectionController::SCROLL_FIRST_ANCESTOR_ONLY) { + flags |= Selection::SCROLL_FIRST_ANCESTOR_ONLY; + } + if (aFlags & nsISelectionController::SCROLL_OVERFLOW_HIDDEN) { + flags |= Selection::SCROLL_OVERFLOW_HIDDEN; + } + if (aFlags & nsISelectionController::SCROLL_CENTER_VERTICALLY) { + verticalScroll = nsIPresShell::ScrollAxis( + nsIPresShell::SCROLL_CENTER, nsIPresShell::SCROLL_IF_NOT_FULLY_VISIBLE); + } + if (aFlags & nsISelectionController::SCROLL_FOR_CARET_MOVE) { + flags |= Selection::SCROLL_FOR_CARET_MOVE; + } + + // After ScrollSelectionIntoView(), the pending notifications might be + // flushed and PresShell/PresContext/Frames may be dead. See bug 418470. + RefPtr<Selection> sel = mDomSelections[index]; + return sel->ScrollIntoView(aRegion, verticalScroll, + nsIPresShell::ScrollAxis(), flags); +} + +nsresult +nsFrameSelection::RepaintSelection(SelectionType aSelectionType) +{ + int8_t index = GetIndexFromSelectionType(aSelectionType); + if (index < 0) + return NS_ERROR_INVALID_ARG; + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + NS_ENSURE_STATE(mShell); + +// On macOS, update the selection cache to the new active selection +// aka the current selection. +#ifdef XP_MACOSX + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + // Check an active window exists otherwise there cannot be a current selection + // and that it's a normal selection. + if (fm->GetActiveWindow() && aSelectionType == SelectionType::eNormal) { + UpdateSelectionCacheOnRepaintSelection(mDomSelections[index]); + } +#endif + return mDomSelections[index]->Repaint(mShell->GetPresContext()); +} + +nsIFrame* +nsFrameSelection::GetFrameForNodeOffset(nsIContent* aNode, + int32_t aOffset, + CaretAssociateHint aHint, + int32_t* aReturnOffset) const +{ + if (!aNode || !aReturnOffset || !mShell) + return nullptr; + + if (aOffset < 0) + return nullptr; + + if (!aNode->GetPrimaryFrame() && + !mShell->FrameManager()->GetDisplayContentsStyleFor(aNode)) { + return nullptr; + } + + nsIFrame* returnFrame = nullptr; + nsCOMPtr<nsIContent> theNode; + + while (true) { + *aReturnOffset = aOffset; + + theNode = aNode; + + if (aNode->IsElement()) { + int32_t childIndex = 0; + int32_t numChildren = theNode->GetChildCount(); + + if (aHint == CARET_ASSOCIATE_BEFORE) { + if (aOffset > 0) { + childIndex = aOffset - 1; + } else { + childIndex = aOffset; + } + } else { + NS_ASSERTION(aHint == CARET_ASSOCIATE_AFTER, "unknown direction"); + if (aOffset >= numChildren) { + if (numChildren > 0) { + childIndex = numChildren - 1; + } else { + childIndex = 0; + } + } else { + childIndex = aOffset; + } + } + + if (childIndex > 0 || numChildren > 0) { + nsCOMPtr<nsIContent> childNode = theNode->GetChildAt(childIndex); + + if (!childNode) { + break; + } + + theNode = childNode; + } + + // Now that we have the child node, check if it too + // can contain children. If so, descend into child. + if (theNode->IsElement() && + theNode->GetChildCount() && + !theNode->HasIndependentSelection()) { + aNode = theNode; + aOffset = aOffset > childIndex ? theNode->GetChildCount() : 0; + continue; + } else { + // Check to see if theNode is a text node. If it is, translate + // aOffset into an offset into the text node. + + nsCOMPtr<nsIDOMText> textNode = do_QueryInterface(theNode); + if (textNode) { + if (theNode->GetPrimaryFrame()) { + if (aOffset > childIndex) { + uint32_t textLength = 0; + nsresult rv = textNode->GetLength(&textLength); + if (NS_FAILED(rv)) { + break; + } + + *aReturnOffset = (int32_t)textLength; + } else { + *aReturnOffset = 0; + } + } else { + int32_t numChildren = aNode->GetChildCount(); + int32_t newChildIndex = + aHint == CARET_ASSOCIATE_BEFORE ? childIndex - 1 : childIndex + 1; + + if (newChildIndex >= 0 && newChildIndex < numChildren) { + nsCOMPtr<nsIContent> newChildNode = aNode->GetChildAt(newChildIndex); + if (!newChildNode) { + return nullptr; + } + + aNode = newChildNode; + aOffset = aHint == CARET_ASSOCIATE_BEFORE ? aNode->GetChildCount() : 0; + continue; + } else { + // newChildIndex is illegal which means we're at first or last + // child. Just use original node to get the frame. + theNode = aNode; + } + } + } + } + } + + // If the node is a ShadowRoot, the frame needs to be adjusted, + // because a ShadowRoot does not get a frame. Its children are rendered + // as children of the host. + mozilla::dom::ShadowRoot* shadowRoot = + mozilla::dom::ShadowRoot::FromNode(theNode); + if (shadowRoot) { + theNode = shadowRoot->GetHost(); + } + + returnFrame = theNode->GetPrimaryFrame(); + if (!returnFrame) { + if (aHint == CARET_ASSOCIATE_BEFORE) { + if (aOffset > 0) { + --aOffset; + continue; + } else { + break; + } + } else { + int32_t end = theNode->GetChildCount(); + if (aOffset < end) { + ++aOffset; + continue; + } else { + break; + } + } + } + + break; + } // end while + + if (!returnFrame) + return nullptr; + + // If we ended up here and were asked to position the caret after a visible + // break, let's return the frame on the next line instead if it exists. + if (aOffset > 0 && (uint32_t) aOffset >= aNode->Length() && + theNode == aNode->GetLastChild()) { + nsIFrame* newFrame; + nsLayoutUtils::IsInvisibleBreak(theNode, &newFrame); + if (newFrame) { + returnFrame = newFrame; + *aReturnOffset = 0; + } + } + + // find the child frame containing the offset we want + returnFrame->GetChildFrameContainingOffset(*aReturnOffset, aHint == CARET_ASSOCIATE_AFTER, + &aOffset, &returnFrame); + return returnFrame; +} + +void +nsFrameSelection::CommonPageMove(bool aForward, + bool aExtend, + nsIScrollableFrame* aScrollableFrame) +{ + // expected behavior for PageMove is to scroll AND move the caret + // and remain relative position of the caret in view. see Bug 4302. + + //get the frame from the scrollable view + + nsIFrame* scrolledFrame = aScrollableFrame->GetScrolledFrame(); + if (!scrolledFrame) + return; + + // find out where the caret is. + // we should know mDesiredPos value of nsFrameSelection, but I havent seen that behavior in other windows applications yet. + nsISelection* domSel = GetSelection(SelectionType::eNormal); + if (!domSel) { + return; + } + + nsRect caretPos; + nsIFrame* caretFrame = nsCaret::GetGeometry(domSel, &caretPos); + if (!caretFrame) + return; + + //need to adjust caret jump by percentage scroll + nsSize scrollDelta = aScrollableFrame->GetPageScrollAmount(); + + if (aForward) + caretPos.y += scrollDelta.height; + else + caretPos.y -= scrollDelta.height; + + caretPos += caretFrame->GetOffsetTo(scrolledFrame); + + // get a content at desired location + nsPoint desiredPoint; + desiredPoint.x = caretPos.x; + desiredPoint.y = caretPos.y + caretPos.height/2; + nsIFrame::ContentOffsets offsets = + scrolledFrame->GetContentOffsetsFromPoint(desiredPoint); + + if (!offsets.content) + return; + + // scroll one page + mozilla::Telemetry::Accumulate(mozilla::Telemetry::SCROLL_INPUT_METHODS, + (uint32_t) ScrollInputMethod::MainThreadScrollPage); + aScrollableFrame->ScrollBy(nsIntPoint(0, aForward ? 1 : -1), + nsIScrollableFrame::PAGES, + nsIScrollableFrame::SMOOTH); + + // place the caret + HandleClick(offsets.content, offsets.offset, + offsets.offset, aExtend, false, CARET_ASSOCIATE_AFTER); +} + +nsresult +nsFrameSelection::PhysicalMove(int16_t aDirection, int16_t aAmount, + bool aExtend) +{ + NS_ENSURE_STATE(mShell); + // Flush out layout, since we need it to be up to date to do caret + // positioning. + mShell->FlushPendingNotifications(Flush_Layout); + + if (!mShell) { + return NS_OK; + } + + // Check that parameters are safe + if (aDirection < 0 || aDirection > 3 || aAmount < 0 || aAmount > 1) { + return NS_ERROR_FAILURE; + } + + nsPresContext *context = mShell->GetPresContext(); + if (!context) { + return NS_ERROR_FAILURE; + } + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + RefPtr<Selection> sel = mDomSelections[index]; + if (!sel) { + return NS_ERROR_NULL_POINTER; + } + + // Map the abstract movement amounts (0-1) to direction-specific + // selection units. + static const nsSelectionAmount inlineAmount[] = + { eSelectCluster, eSelectWord }; + static const nsSelectionAmount blockPrevAmount[] = + { eSelectLine, eSelectBeginLine }; + static const nsSelectionAmount blockNextAmount[] = + { eSelectLine, eSelectEndLine }; + + struct PhysicalToLogicalMapping { + nsDirection direction; + const nsSelectionAmount *amounts; + }; + static const PhysicalToLogicalMapping verticalLR[4] = { + { eDirPrevious, blockPrevAmount }, // left + { eDirNext, blockNextAmount }, // right + { eDirPrevious, inlineAmount }, // up + { eDirNext, inlineAmount } // down + }; + static const PhysicalToLogicalMapping verticalRL[4] = { + { eDirNext, blockNextAmount }, + { eDirPrevious, blockPrevAmount }, + { eDirPrevious, inlineAmount }, + { eDirNext, inlineAmount } + }; + static const PhysicalToLogicalMapping horizontal[4] = { + { eDirPrevious, inlineAmount }, + { eDirNext, inlineAmount }, + { eDirPrevious, blockPrevAmount }, + { eDirNext, blockNextAmount } + }; + + WritingMode wm; + nsIFrame *frame = nullptr; + int32_t offsetused = 0; + if (NS_SUCCEEDED(sel->GetPrimaryFrameForFocusNode(&frame, &offsetused, + true))) { + if (frame) { + if (!frame->StyleContext()->IsTextCombined()) { + wm = frame->GetWritingMode(); + } else { + // Using different direction for horizontal-in-vertical would + // make it hard to navigate via keyboard. Inherit the moving + // direction from its parent. + MOZ_ASSERT(frame->GetType() == nsGkAtoms::textFrame); + wm = frame->GetParent()->GetWritingMode(); + MOZ_ASSERT(wm.IsVertical(), "Text combined " + "can only appear in vertical text"); + } + } + } + + const PhysicalToLogicalMapping& mapping = + wm.IsVertical() + ? wm.IsVerticalLR() ? verticalLR[aDirection] : verticalRL[aDirection] + : horizontal[aDirection]; + + nsresult rv = MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount], + eVisual); + if (NS_FAILED(rv)) { + // If we tried to do a line move, but couldn't move in the given direction, + // then we'll "promote" this to a line-edge move instead. + if (mapping.amounts[aAmount] == eSelectLine) { + rv = MoveCaret(mapping.direction, aExtend, mapping.amounts[aAmount + 1], + eVisual); + } + // And if it was a next-word move that failed (which can happen when + // eat_space_to_next_word is true, see bug 1153237), then just move forward + // to the line-edge. + else if (mapping.amounts[aAmount] == eSelectWord && + mapping.direction == eDirNext) { + rv = MoveCaret(eDirNext, aExtend, eSelectEndLine, eVisual); + } + } + + return rv; +} + +nsresult +nsFrameSelection::CharacterMove(bool aForward, bool aExtend) +{ + return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectCluster, + eUsePrefStyle); +} + +nsresult +nsFrameSelection::CharacterExtendForDelete() +{ + return MoveCaret(eDirNext, true, eSelectCluster, eLogical); +} + +nsresult +nsFrameSelection::CharacterExtendForBackspace() +{ + return MoveCaret(eDirPrevious, true, eSelectCharacter, eLogical); +} + +nsresult +nsFrameSelection::WordMove(bool aForward, bool aExtend) +{ + return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectWord, + eUsePrefStyle); +} + +nsresult +nsFrameSelection::WordExtendForDelete(bool aForward) +{ + return MoveCaret(aForward ? eDirNext : eDirPrevious, true, eSelectWord, + eLogical); +} + +nsresult +nsFrameSelection::LineMove(bool aForward, bool aExtend) +{ + return MoveCaret(aForward ? eDirNext : eDirPrevious, aExtend, eSelectLine, + eUsePrefStyle); +} + +nsresult +nsFrameSelection::IntraLineMove(bool aForward, bool aExtend) +{ + if (aForward) { + return MoveCaret(eDirNext, aExtend, eSelectEndLine, eLogical); + } else { + return MoveCaret(eDirPrevious, aExtend, eSelectBeginLine, eLogical); + } +} + +nsresult +nsFrameSelection::SelectAll() +{ + nsCOMPtr<nsIContent> rootContent; + if (mLimiter) + { + rootContent = mLimiter;//addrefit + } + else if (mAncestorLimiter) { + rootContent = mAncestorLimiter; + } + else + { + NS_ENSURE_STATE(mShell); + nsIDocument *doc = mShell->GetDocument(); + if (!doc) + return NS_ERROR_FAILURE; + rootContent = doc->GetRootElement(); + if (!rootContent) + return NS_ERROR_FAILURE; + } + int32_t numChildren = rootContent->GetChildCount(); + PostReason(nsISelectionListener::NO_REASON); + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + AutoPrepareFocusRange prep(mDomSelections[index], false, false); + return TakeFocus(rootContent, 0, numChildren, CARET_ASSOCIATE_BEFORE, false, false); +} + +//////////END FRAMESELECTION + +void +nsFrameSelection::StartBatchChanges() +{ + mBatching++; +} + +void +nsFrameSelection::EndBatchChanges(int16_t aReason) +{ + mBatching--; + NS_ASSERTION(mBatching >=0,"Bad mBatching"); + + if (mBatching == 0 && mChangesDuringBatching) { + int16_t postReason = PopReason() | aReason; + PostReason(postReason); + mChangesDuringBatching = false; + NotifySelectionListeners(SelectionType::eNormal); + } +} + + +nsresult +nsFrameSelection::NotifySelectionListeners(SelectionType aSelectionType) +{ + int8_t index = GetIndexFromSelectionType(aSelectionType); + if (index >=0 && mDomSelections[index]) + { + return mDomSelections[index]->NotifySelectionListeners(); + } + return NS_ERROR_FAILURE; +} + +// Start of Table Selection methods + +static bool IsCell(nsIContent *aContent) +{ + return aContent->IsAnyOfHTMLElements(nsGkAtoms::td, nsGkAtoms::th); +} + +nsITableCellLayout* +nsFrameSelection::GetCellLayout(nsIContent *aCellContent) const +{ + NS_ENSURE_TRUE(mShell, nullptr); + nsITableCellLayout *cellLayoutObject = + do_QueryFrame(aCellContent->GetPrimaryFrame()); + return cellLayoutObject; +} + +nsresult +nsFrameSelection::ClearNormalSelection() +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + return mDomSelections[index]->RemoveAllRanges(); +} + +static nsIContent* +GetFirstSelectedContent(nsRange* aRange) +{ + if (!aRange) { + return nullptr; + } + + NS_PRECONDITION(aRange->GetStartParent(), "Must have start parent!"); + NS_PRECONDITION(aRange->GetStartParent()->IsElement(), + "Unexpected parent"); + + return aRange->GetStartParent()->GetChildAt(aRange->StartOffset()); +} + +// Table selection support. +// TODO: Separate table methods into a separate nsITableSelection interface +nsresult +nsFrameSelection::HandleTableSelection(nsINode* aParentContent, + int32_t aContentOffset, + int32_t aTarget, + WidgetMouseEvent* aMouseEvent) +{ + NS_ENSURE_TRUE(aParentContent, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(aMouseEvent, NS_ERROR_NULL_POINTER); + + if (mDragState && mDragSelectingCells && (aTarget & nsISelectionPrivate::TABLESELECTION_TABLE)) + { + // We were selecting cells and user drags mouse in table border or inbetween cells, + // just do nothing + return NS_OK; + } + + nsresult result = NS_OK; + + nsIContent *childContent = aParentContent->GetChildAt(aContentOffset); + + // When doing table selection, always set the direction to next so + // we can be sure that anchorNode's offset always points to the + // selected cell + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + mDomSelections[index]->SetDirection(eDirNext); + + // Stack-class to wrap all table selection changes in + // BeginBatchChanges() / EndBatchChanges() + SelectionBatcher selectionBatcher(mDomSelections[index]); + + int32_t startRowIndex, startColIndex, curRowIndex, curColIndex; + if (mDragState && mDragSelectingCells) + { + // We are drag-selecting + if (aTarget != nsISelectionPrivate::TABLESELECTION_TABLE) + { + // If dragging in the same cell as last event, do nothing + if (mEndSelectedCell == childContent) + return NS_OK; + +#ifdef DEBUG_TABLE_SELECTION + printf(" mStartSelectedCell = %p, mEndSelectedCell = %p, childContent = %p \n", + mStartSelectedCell.get(), mEndSelectedCell.get(), childContent); +#endif + // aTarget can be any "cell mode", + // so we can easily drag-select rows and columns + // Once we are in row or column mode, + // we can drift into any cell to stay in that mode + // even if aTarget = TABLESELECTION_CELL + + if (mSelectingTableCellMode == nsISelectionPrivate::TABLESELECTION_ROW || + mSelectingTableCellMode == nsISelectionPrivate::TABLESELECTION_COLUMN) + { + if (mEndSelectedCell) + { + // Also check if cell is in same row/col + result = GetCellIndexes(mEndSelectedCell, startRowIndex, startColIndex); + if (NS_FAILED(result)) return result; + result = GetCellIndexes(childContent, curRowIndex, curColIndex); + if (NS_FAILED(result)) return result; + +#ifdef DEBUG_TABLE_SELECTION +printf(" curRowIndex = %d, startRowIndex = %d, curColIndex = %d, startColIndex = %d\n", curRowIndex, startRowIndex, curColIndex, startColIndex); +#endif + if ((mSelectingTableCellMode == nsISelectionPrivate::TABLESELECTION_ROW && startRowIndex == curRowIndex) || + (mSelectingTableCellMode == nsISelectionPrivate::TABLESELECTION_COLUMN && startColIndex == curColIndex)) + return NS_OK; + } +#ifdef DEBUG_TABLE_SELECTION +printf(" Dragged into a new column or row\n"); +#endif + // Continue dragging row or column selection + return SelectRowOrColumn(childContent, mSelectingTableCellMode); + } + else if (mSelectingTableCellMode == nsISelectionPrivate::TABLESELECTION_CELL) + { +#ifdef DEBUG_TABLE_SELECTION +printf("HandleTableSelection: Dragged into a new cell\n"); +#endif + // Trick for quick selection of rows and columns + // Hold down shift, then start selecting in one direction + // If next cell dragged into is in same row, select entire row, + // if next cell is in same column, select entire column + if (mStartSelectedCell && aMouseEvent->IsShift()) + { + result = GetCellIndexes(mStartSelectedCell, startRowIndex, startColIndex); + if (NS_FAILED(result)) return result; + result = GetCellIndexes(childContent, curRowIndex, curColIndex); + if (NS_FAILED(result)) return result; + + if (startRowIndex == curRowIndex || + startColIndex == curColIndex) + { + // Force new selection block + mStartSelectedCell = nullptr; + mDomSelections[index]->RemoveAllRanges(); + + if (startRowIndex == curRowIndex) + mSelectingTableCellMode = nsISelectionPrivate::TABLESELECTION_ROW; + else + mSelectingTableCellMode = nsISelectionPrivate::TABLESELECTION_COLUMN; + + return SelectRowOrColumn(childContent, mSelectingTableCellMode); + } + } + + // Reselect block of cells to new end location + return SelectBlockOfCells(mStartSelectedCell, childContent); + } + } + // Do nothing if dragging in table, but outside a cell + return NS_OK; + } + else + { + // Not dragging -- mouse event is down or up + if (mDragState) + { +#ifdef DEBUG_TABLE_SELECTION +printf("HandleTableSelection: Mouse down event\n"); +#endif + // Clear cell we stored in mouse-down + mUnselectCellOnMouseUp = nullptr; + + if (aTarget == nsISelectionPrivate::TABLESELECTION_CELL) + { + bool isSelected = false; + + // Check if we have other selected cells + nsIContent* previousCellNode = + GetFirstSelectedContent(GetFirstCellRange()); + if (previousCellNode) + { + // We have at least 1 other selected cell + + // Check if new cell is already selected + nsIFrame *cellFrame = childContent->GetPrimaryFrame(); + if (!cellFrame) return NS_ERROR_NULL_POINTER; + isSelected = cellFrame->IsSelected(); + } + else + { + // No cells selected -- remove non-cell selection + mDomSelections[index]->RemoveAllRanges(); + } + mDragSelectingCells = true; // Signal to start drag-cell-selection + mSelectingTableCellMode = aTarget; + // Set start for new drag-selection block (not appended) + mStartSelectedCell = childContent; + // The initial block end is same as the start + mEndSelectedCell = childContent; + + if (isSelected) + { + // Remember this cell to (possibly) unselect it on mouseup + mUnselectCellOnMouseUp = childContent; +#ifdef DEBUG_TABLE_SELECTION +printf("HandleTableSelection: Saving mUnselectCellOnMouseUp\n"); +#endif + } + else + { + // Select an unselected cell + // but first remove existing selection if not in same table + if (previousCellNode && + !IsInSameTable(previousCellNode, childContent)) + { + mDomSelections[index]->RemoveAllRanges(); + // Reset selection mode that is cleared in RemoveAllRanges + mSelectingTableCellMode = aTarget; + } + + return SelectCellElement(childContent); + } + + return NS_OK; + } + else if (aTarget == nsISelectionPrivate::TABLESELECTION_TABLE) + { + //TODO: We currently select entire table when clicked between cells, + // should we restrict to only around border? + // *** How do we get location data for cell and click? + mDragSelectingCells = false; + mStartSelectedCell = nullptr; + mEndSelectedCell = nullptr; + + // Remove existing selection and select the table + mDomSelections[index]->RemoveAllRanges(); + return CreateAndAddRange(aParentContent, aContentOffset); + } + else if (aTarget == nsISelectionPrivate::TABLESELECTION_ROW || aTarget == nsISelectionPrivate::TABLESELECTION_COLUMN) + { +#ifdef DEBUG_TABLE_SELECTION +printf("aTarget == %d\n", aTarget); +#endif + + // Start drag-selecting mode so multiple rows/cols can be selected + // Note: Currently, nsFrame::GetDataForTableSelection + // will never call us for row or column selection on mouse down + mDragSelectingCells = true; + + // Force new selection block + mStartSelectedCell = nullptr; + mDomSelections[index]->RemoveAllRanges(); + // Always do this AFTER RemoveAllRanges + mSelectingTableCellMode = aTarget; + return SelectRowOrColumn(childContent, aTarget); + } + } + else + { +#ifdef DEBUG_TABLE_SELECTION + printf("HandleTableSelection: Mouse UP event. mDragSelectingCells=%d, mStartSelectedCell=%p\n", + mDragSelectingCells, mStartSelectedCell.get()); +#endif + // First check if we are extending a block selection + int32_t rangeCount; + result = mDomSelections[index]->GetRangeCount(&rangeCount); + if (NS_FAILED(result)) + return result; + + if (rangeCount > 0 && aMouseEvent->IsShift() && + mAppendStartSelectedCell && mAppendStartSelectedCell != childContent) + { + // Shift key is down: append a block selection + mDragSelectingCells = false; + return SelectBlockOfCells(mAppendStartSelectedCell, childContent); + } + + if (mDragSelectingCells) + mAppendStartSelectedCell = mStartSelectedCell; + + mDragSelectingCells = false; + mStartSelectedCell = nullptr; + mEndSelectedCell = nullptr; + + // Any other mouseup actions require that Ctrl or Cmd key is pressed + // else stop table selection mode + bool doMouseUpAction = false; +#ifdef XP_MACOSX + doMouseUpAction = aMouseEvent->IsMeta(); +#else + doMouseUpAction = aMouseEvent->IsControl(); +#endif + if (!doMouseUpAction) + { +#ifdef DEBUG_TABLE_SELECTION + printf("HandleTableSelection: Ending cell selection on mouseup: mAppendStartSelectedCell=%p\n", + mAppendStartSelectedCell.get()); +#endif + return NS_OK; + } + // Unselect a cell only if it wasn't + // just selected on mousedown + if( childContent == mUnselectCellOnMouseUp) + { + // Scan ranges to find the cell to unselect (the selection range to remove) + // XXXbz it's really weird that this lives outside the loop, so once we + // find one we keep looking at it even if we find no more cells... + nsINode* previousCellParent = nullptr; +#ifdef DEBUG_TABLE_SELECTION +printf("HandleTableSelection: Unselecting mUnselectCellOnMouseUp; rangeCount=%d\n", rangeCount); +#endif + for( int32_t i = 0; i < rangeCount; i++) + { + // Strong reference, because sometimes we want to remove + // this range, and then we might be the only owner. + RefPtr<nsRange> range = mDomSelections[index]->GetRangeAt(i); + if (!range) return NS_ERROR_NULL_POINTER; + + nsINode* parent = range->GetStartParent(); + if (!parent) return NS_ERROR_NULL_POINTER; + + int32_t offset = range->StartOffset(); + // Be sure previous selection is a table cell + nsIContent* child = parent->GetChildAt(offset); + if (child && IsCell(child)) + previousCellParent = parent; + + // We're done if we didn't find parent of a previously-selected cell + if (!previousCellParent) break; + + if (previousCellParent == aParentContent && offset == aContentOffset) + { + // Cell is already selected + if (rangeCount == 1) + { +#ifdef DEBUG_TABLE_SELECTION +printf("HandleTableSelection: Unselecting single selected cell\n"); +#endif + // This was the only cell selected. + // Collapse to "normal" selection inside the cell + mStartSelectedCell = nullptr; + mEndSelectedCell = nullptr; + mAppendStartSelectedCell = nullptr; + //TODO: We need a "Collapse to just before deepest child" routine + // Even better, should we collapse to just after the LAST deepest child + // (i.e., at the end of the cell's contents)? + return mDomSelections[index]->Collapse(childContent, 0); + } +#ifdef DEBUG_TABLE_SELECTION +printf("HandleTableSelection: Removing cell from multi-cell selection\n"); +#endif + // Unselecting the start of previous block + // XXX What do we use now! + if (childContent == mAppendStartSelectedCell) + mAppendStartSelectedCell = nullptr; + + // Deselect cell by removing its range from selection + return mDomSelections[index]->RemoveRange(range); + } + } + mUnselectCellOnMouseUp = nullptr; + } + } + } + return result; +} + +nsresult +nsFrameSelection::SelectBlockOfCells(nsIContent *aStartCell, nsIContent *aEndCell) +{ + NS_ENSURE_TRUE(aStartCell, NS_ERROR_NULL_POINTER); + NS_ENSURE_TRUE(aEndCell, NS_ERROR_NULL_POINTER); + mEndSelectedCell = aEndCell; + + nsresult result = NS_OK; + + // If new end cell is in a different table, do nothing + nsIContent* table = IsInSameTable(aStartCell, aEndCell); + if (!table) { + return NS_OK; + } + + // Get starting and ending cells' location in the cellmap + int32_t startRowIndex, startColIndex, endRowIndex, endColIndex; + result = GetCellIndexes(aStartCell, startRowIndex, startColIndex); + if(NS_FAILED(result)) return result; + result = GetCellIndexes(aEndCell, endRowIndex, endColIndex); + if(NS_FAILED(result)) return result; + + if (mDragSelectingCells) + { + // Drag selecting: remove selected cells outside of new block limits + UnselectCells(table, startRowIndex, startColIndex, endRowIndex, endColIndex, + true); + } + + // Note that we select block in the direction of user's mouse dragging, + // which means start cell may be after the end cell in either row or column + return AddCellsToSelection(table, startRowIndex, startColIndex, + endRowIndex, endColIndex); +} + +nsresult +nsFrameSelection::UnselectCells(nsIContent *aTableContent, + int32_t aStartRowIndex, + int32_t aStartColumnIndex, + int32_t aEndRowIndex, + int32_t aEndColumnIndex, + bool aRemoveOutsideOfCellRange) +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + nsTableWrapperFrame* tableFrame = do_QueryFrame(aTableContent->GetPrimaryFrame()); + if (!tableFrame) + return NS_ERROR_FAILURE; + + int32_t minRowIndex = std::min(aStartRowIndex, aEndRowIndex); + int32_t maxRowIndex = std::max(aStartRowIndex, aEndRowIndex); + int32_t minColIndex = std::min(aStartColumnIndex, aEndColumnIndex); + int32_t maxColIndex = std::max(aStartColumnIndex, aEndColumnIndex); + + // Strong reference because we sometimes remove the range + RefPtr<nsRange> range = GetFirstCellRange(); + nsIContent* cellNode = GetFirstSelectedContent(range); + NS_PRECONDITION(!range || cellNode, "Must have cellNode if had a range"); + + int32_t curRowIndex, curColIndex; + while (cellNode) + { + nsresult result = GetCellIndexes(cellNode, curRowIndex, curColIndex); + if (NS_FAILED(result)) + return result; + +#ifdef DEBUG_TABLE_SELECTION + if (!range) + printf("RemoveCellsToSelection -- range is null\n"); +#endif + + if (range) { + if (aRemoveOutsideOfCellRange) { + if (curRowIndex < minRowIndex || curRowIndex > maxRowIndex || + curColIndex < minColIndex || curColIndex > maxColIndex) { + + mDomSelections[index]->RemoveRange(range); + // Since we've removed the range, decrement pointer to next range + mSelectedCellIndex--; + } + + } else { + // Remove cell from selection if it belongs to the given cells range or + // it is spanned onto the cells range. + nsTableCellFrame* cellFrame = + tableFrame->GetCellFrameAt(curRowIndex, curColIndex); + + int32_t origRowIndex, origColIndex; + cellFrame->GetRowIndex(origRowIndex); + cellFrame->GetColIndex(origColIndex); + uint32_t actualRowSpan = + tableFrame->GetEffectiveRowSpanAt(origRowIndex, origColIndex); + uint32_t actualColSpan = + tableFrame->GetEffectiveColSpanAt(curRowIndex, curColIndex); + if (origRowIndex <= maxRowIndex && maxRowIndex >= 0 && + origRowIndex + actualRowSpan - 1 >= static_cast<uint32_t>(minRowIndex) && + origColIndex <= maxColIndex && maxColIndex >= 0 && + origColIndex + actualColSpan - 1 >= static_cast<uint32_t>(minColIndex)) { + + mDomSelections[index]->RemoveRange(range); + // Since we've removed the range, decrement pointer to next range + mSelectedCellIndex--; + } + } + } + + range = GetNextCellRange(); + cellNode = GetFirstSelectedContent(range); + NS_PRECONDITION(!range || cellNode, "Must have cellNode if had a range"); + } + + return NS_OK; +} + +nsresult +nsFrameSelection::AddCellsToSelection(nsIContent *aTableContent, + int32_t aStartRowIndex, + int32_t aStartColumnIndex, + int32_t aEndRowIndex, + int32_t aEndColumnIndex) +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + nsTableWrapperFrame* tableFrame = do_QueryFrame(aTableContent->GetPrimaryFrame()); + if (!tableFrame) // Check that |table| is a table. + return NS_ERROR_FAILURE; + + nsresult result = NS_OK; + int32_t row = aStartRowIndex; + while(true) + { + int32_t col = aStartColumnIndex; + while(true) + { + nsTableCellFrame* cellFrame = tableFrame->GetCellFrameAt(row, col); + + // Skip cells that are spanned from previous locations or are already selected + if (cellFrame) { + int32_t origRow, origCol; + cellFrame->GetRowIndex(origRow); + cellFrame->GetColIndex(origCol); + if (origRow == row && origCol == col && !cellFrame->IsSelected()) { + result = SelectCellElement(cellFrame->GetContent()); + if (NS_FAILED(result)) return result; + } + } + // Done when we reach end column + if (col == aEndColumnIndex) break; + + if (aStartColumnIndex < aEndColumnIndex) + col ++; + else + col--; + } + if (row == aEndRowIndex) break; + + if (aStartRowIndex < aEndRowIndex) + row++; + else + row--; + } + return result; +} + +nsresult +nsFrameSelection::RemoveCellsFromSelection(nsIContent *aTable, + int32_t aStartRowIndex, + int32_t aStartColumnIndex, + int32_t aEndRowIndex, + int32_t aEndColumnIndex) +{ + return UnselectCells(aTable, aStartRowIndex, aStartColumnIndex, + aEndRowIndex, aEndColumnIndex, false); +} + +nsresult +nsFrameSelection::RestrictCellsToSelection(nsIContent *aTable, + int32_t aStartRowIndex, + int32_t aStartColumnIndex, + int32_t aEndRowIndex, + int32_t aEndColumnIndex) +{ + return UnselectCells(aTable, aStartRowIndex, aStartColumnIndex, + aEndRowIndex, aEndColumnIndex, true); +} + +nsresult +nsFrameSelection::SelectRowOrColumn(nsIContent *aCellContent, uint32_t aTarget) +{ + if (!aCellContent) return NS_ERROR_NULL_POINTER; + + nsIContent* table = GetParentTable(aCellContent); + if (!table) return NS_ERROR_NULL_POINTER; + + // Get table and cell layout interfaces to access + // cell data based on cellmap location + // Frames are not ref counted, so don't use an nsCOMPtr + nsTableWrapperFrame* tableFrame = do_QueryFrame(table->GetPrimaryFrame()); + if (!tableFrame) return NS_ERROR_FAILURE; + nsITableCellLayout *cellLayout = GetCellLayout(aCellContent); + if (!cellLayout) return NS_ERROR_FAILURE; + + // Get location of target cell: + int32_t rowIndex, colIndex; + nsresult result = cellLayout->GetCellIndexes(rowIndex, colIndex); + if (NS_FAILED(result)) return result; + + // Be sure we start at proper beginning + // (This allows us to select row or col given ANY cell!) + if (aTarget == nsISelectionPrivate::TABLESELECTION_ROW) + colIndex = 0; + if (aTarget == nsISelectionPrivate::TABLESELECTION_COLUMN) + rowIndex = 0; + + nsCOMPtr<nsIContent> firstCell, lastCell; + while (true) { + // Loop through all cells in column or row to find first and last + nsCOMPtr<nsIContent> curCellContent = + tableFrame->GetCellAt(rowIndex, colIndex); + if (!curCellContent) + break; + + if (!firstCell) + firstCell = curCellContent; + + lastCell = curCellContent.forget(); + + // Move to next cell in cellmap, skipping spanned locations + if (aTarget == nsISelectionPrivate::TABLESELECTION_ROW) + colIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex); + else + rowIndex += tableFrame->GetEffectiveRowSpanAt(rowIndex, colIndex); + } + + // Use SelectBlockOfCells: + // This will replace existing selection, + // but allow unselecting by dragging out of selected region + if (firstCell && lastCell) + { + if (!mStartSelectedCell) + { + // We are starting a new block, so select the first cell + result = SelectCellElement(firstCell); + if (NS_FAILED(result)) return result; + mStartSelectedCell = firstCell; + } + nsCOMPtr<nsIContent> lastCellContent = do_QueryInterface(lastCell); + result = SelectBlockOfCells(mStartSelectedCell, lastCellContent); + + // This gets set to the cell at end of row/col, + // but we need it to be the cell under cursor + mEndSelectedCell = aCellContent; + return result; + } + +#if 0 +// This is a more efficient strategy that appends row to current selection, +// but doesn't allow dragging OFF of an existing selection to unselect! + do { + // Loop through all cells in column or row + result = tableLayout->GetCellDataAt(rowIndex, colIndex, + getter_AddRefs(cellElement), + curRowIndex, curColIndex, + rowSpan, colSpan, + actualRowSpan, actualColSpan, + isSelected); + if (NS_FAILED(result)) return result; + // We're done when cell is not found + if (!cellElement) break; + + + // Check spans else we infinitely loop + NS_ASSERTION(actualColSpan, "actualColSpan is 0!"); + NS_ASSERTION(actualRowSpan, "actualRowSpan is 0!"); + + // Skip cells that are already selected or span from outside our region + if (!isSelected && rowIndex == curRowIndex && colIndex == curColIndex) + { + result = SelectCellElement(cellElement); + if (NS_FAILED(result)) return result; + } + // Move to next row or column in cellmap, skipping spanned locations + if (aTarget == nsISelectionPrivate::TABLESELECTION_ROW) + colIndex += actualColSpan; + else + rowIndex += actualRowSpan; + } + while (cellElement); +#endif + + return NS_OK; +} + +nsIContent* +nsFrameSelection::GetFirstCellNodeInRange(nsRange *aRange) const +{ + if (!aRange) return nullptr; + + nsINode* startParent = aRange->GetStartParent(); + if (!startParent) + return nullptr; + + int32_t offset = aRange->StartOffset(); + + nsIContent* childContent = startParent->GetChildAt(offset); + if (!childContent) + return nullptr; + // Don't return node if not a cell + if (!IsCell(childContent)) + return nullptr; + + return childContent; +} + +nsRange* +nsFrameSelection::GetFirstCellRange() +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return nullptr; + + nsRange* firstRange = mDomSelections[index]->GetRangeAt(0); + if (!GetFirstCellNodeInRange(firstRange)) { + return nullptr; + } + + // Setup for next cell + mSelectedCellIndex = 1; + + return firstRange; +} + +nsRange* +nsFrameSelection::GetNextCellRange() +{ + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return nullptr; + + nsRange* range = mDomSelections[index]->GetRangeAt(mSelectedCellIndex); + + // Get first node in next range of selection - test if it's a cell + if (!GetFirstCellNodeInRange(range)) { + return nullptr; + } + + // Setup for next cell + mSelectedCellIndex++; + + return range; +} + +nsresult +nsFrameSelection::GetCellIndexes(nsIContent *aCell, + int32_t &aRowIndex, + int32_t &aColIndex) +{ + if (!aCell) return NS_ERROR_NULL_POINTER; + + aColIndex=0; // initialize out params + aRowIndex=0; + + nsITableCellLayout *cellLayoutObject = GetCellLayout(aCell); + if (!cellLayoutObject) return NS_ERROR_FAILURE; + return cellLayoutObject->GetCellIndexes(aRowIndex, aColIndex); +} + +nsIContent* +nsFrameSelection::IsInSameTable(nsIContent *aContent1, + nsIContent *aContent2) const +{ + if (!aContent1 || !aContent2) return nullptr; + + nsIContent* tableNode1 = GetParentTable(aContent1); + nsIContent* tableNode2 = GetParentTable(aContent2); + + // Must be in the same table. Note that we want to return false for + // the test if both tables are null. + return (tableNode1 == tableNode2) ? tableNode1 : nullptr; +} + +nsIContent* +nsFrameSelection::GetParentTable(nsIContent *aCell) const +{ + if (!aCell) { + return nullptr; + } + + for (nsIContent* parent = aCell->GetParent(); parent; + parent = parent->GetParent()) { + if (parent->IsHTMLElement(nsGkAtoms::table)) { + return parent; + } + } + + return nullptr; +} + +nsresult +nsFrameSelection::SelectCellElement(nsIContent *aCellElement) +{ + nsIContent *parent = aCellElement->GetParent(); + + // Get child offset + int32_t offset = parent->IndexOf(aCellElement); + + return CreateAndAddRange(parent, offset); +} + +nsresult +Selection::getTableCellLocationFromRange(nsRange* aRange, + int32_t* aSelectionType, + int32_t* aRow, int32_t* aCol) +{ + if (!aRange || !aSelectionType || !aRow || !aCol) + return NS_ERROR_NULL_POINTER; + + *aSelectionType = nsISelectionPrivate::TABLESELECTION_NONE; + *aRow = 0; + *aCol = 0; + + // Must have access to frame selection to get cell info + if (!mFrameSelection) return NS_OK; + + nsresult result = GetTableSelectionType(aRange, aSelectionType); + if (NS_FAILED(result)) return result; + + // Don't fail if range does not point to a single table cell, + // let aSelectionType tell user if we don't have a cell + if (*aSelectionType != nsISelectionPrivate::TABLESELECTION_CELL) + return NS_OK; + + // Get the child content (the cell) pointed to by starting node of range + // We do minimal checking since GetTableSelectionType assures + // us that this really is a table cell + nsCOMPtr<nsIContent> content = do_QueryInterface(aRange->GetStartParent()); + if (!content) + return NS_ERROR_FAILURE; + + nsIContent *child = content->GetChildAt(aRange->StartOffset()); + if (!child) + return NS_ERROR_FAILURE; + + //Note: This is a non-ref-counted pointer to the frame + nsITableCellLayout *cellLayout = mFrameSelection->GetCellLayout(child); + if (NS_FAILED(result)) + return result; + if (!cellLayout) + return NS_ERROR_FAILURE; + + return cellLayout->GetCellIndexes(*aRow, *aCol); +} + +nsresult +Selection::addTableCellRange(nsRange* aRange, bool* aDidAddRange, + int32_t* aOutIndex) +{ + if (!aDidAddRange || !aOutIndex) + return NS_ERROR_NULL_POINTER; + + *aDidAddRange = false; + *aOutIndex = -1; + + if (!mFrameSelection) + return NS_OK; + + if (!aRange) + return NS_ERROR_NULL_POINTER; + + nsresult result; + + // Get if we are adding a cell selection and the row, col of cell if we are + int32_t newRow, newCol, tableMode; + result = getTableCellLocationFromRange(aRange, &tableMode, &newRow, &newCol); + if (NS_FAILED(result)) return result; + + // If not adding a cell range, we are done here + if (tableMode != nsISelectionPrivate::TABLESELECTION_CELL) + { + mFrameSelection->mSelectingTableCellMode = tableMode; + // Don't fail if range isn't a selected cell, aDidAddRange tells caller if we didn't proceed + return NS_OK; + } + + // Set frame selection mode only if not already set to a table mode + // so we don't lose the select row and column flags (not detected by getTableCellLocation) + if (mFrameSelection->mSelectingTableCellMode == TABLESELECTION_NONE) + mFrameSelection->mSelectingTableCellMode = tableMode; + + *aDidAddRange = true; + return AddItem(aRange, aOutIndex); +} + +//TODO: Figure out TABLESELECTION_COLUMN and TABLESELECTION_ALLCELLS +nsresult +Selection::GetTableSelectionType(nsIDOMRange* aDOMRange, + int32_t* aTableSelectionType) +{ + if (!aDOMRange || !aTableSelectionType) + return NS_ERROR_NULL_POINTER; + nsRange* range = static_cast<nsRange*>(aDOMRange); + + *aTableSelectionType = nsISelectionPrivate::TABLESELECTION_NONE; + + // Must have access to frame selection to get cell info + if(!mFrameSelection) return NS_OK; + + nsINode* startNode = range->GetStartParent(); + if (!startNode) return NS_ERROR_FAILURE; + + nsINode* endNode = range->GetEndParent(); + if (!endNode) return NS_ERROR_FAILURE; + + // Not a single selected node + if (startNode != endNode) return NS_OK; + + int32_t startOffset = range->StartOffset(); + int32_t endOffset = range->EndOffset(); + + // Not a single selected node + if ((endOffset - startOffset) != 1) + return NS_OK; + + nsIContent* startContent = static_cast<nsIContent*>(startNode); + if (!(startNode->IsElement() && startContent->IsHTMLElement())) { + // Implies a check for being an element; if we ever make this work + // for non-HTML, need to keep checking for elements. + return NS_OK; + } + + if (startContent->IsHTMLElement(nsGkAtoms::tr)) + { + *aTableSelectionType = nsISelectionPrivate::TABLESELECTION_CELL; + } + else //check to see if we are selecting a table or row (column and all cells not done yet) + { + nsIContent *child = startNode->GetChildAt(startOffset); + if (!child) + return NS_ERROR_FAILURE; + + if (child->IsHTMLElement(nsGkAtoms::table)) + *aTableSelectionType = nsISelectionPrivate::TABLESELECTION_TABLE; + else if (child->IsHTMLElement(nsGkAtoms::tr)) + *aTableSelectionType = nsISelectionPrivate::TABLESELECTION_ROW; + } + + return NS_OK; +} + +nsresult +nsFrameSelection::CreateAndAddRange(nsINode *aParentNode, int32_t aOffset) +{ + if (!aParentNode) return NS_ERROR_NULL_POINTER; + + RefPtr<nsRange> range = new nsRange(aParentNode); + + // Set range around child at given offset + nsresult result = range->SetStart(aParentNode, aOffset); + if (NS_FAILED(result)) return result; + result = range->SetEnd(aParentNode, aOffset+1); + if (NS_FAILED(result)) return result; + + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + return mDomSelections[index]->AddRange(range); +} + +// End of Table Selection + +void +nsFrameSelection::SetAncestorLimiter(nsIContent *aLimiter) +{ + if (mAncestorLimiter != aLimiter) { + mAncestorLimiter = aLimiter; + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return; + + if (!IsValidSelectionPoint(this, mDomSelections[index]->GetFocusNode())) { + ClearNormalSelection(); + if (mAncestorLimiter) { + PostReason(nsISelectionListener::NO_REASON); + TakeFocus(mAncestorLimiter, 0, 0, CARET_ASSOCIATE_BEFORE, false, false); + } + } + } +} + +//END nsFrameSelection methods + + +//BEGIN nsISelection interface implementations + + + +nsresult +nsFrameSelection::DeleteFromDocument() +{ + nsresult res; + + // If we're already collapsed, then we do nothing (bug 719503). + bool isCollapsed; + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + if (!mDomSelections[index]) + return NS_ERROR_NULL_POINTER; + + mDomSelections[index]->GetIsCollapsed( &isCollapsed); + if (isCollapsed) + { + return NS_OK; + } + + RefPtr<Selection> selection = mDomSelections[index]; + for (uint32_t rangeIdx = 0; rangeIdx < selection->RangeCount(); ++rangeIdx) { + RefPtr<nsRange> range = selection->GetRangeAt(rangeIdx); + res = range->DeleteContents(); + if (NS_FAILED(res)) + return res; + } + + // Collapse to the new location. + // If we deleted one character, then we move back one element. + // FIXME We don't know how to do this past frame boundaries yet. + if (isCollapsed) + mDomSelections[index]->Collapse(mDomSelections[index]->GetAnchorNode(), mDomSelections[index]->AnchorOffset()-1); + else if (mDomSelections[index]->AnchorOffset() > 0) + mDomSelections[index]->Collapse(mDomSelections[index]->GetAnchorNode(), mDomSelections[index]->AnchorOffset()); +#ifdef DEBUG + else + printf("Don't know how to set selection back past frame boundary\n"); +#endif + + return NS_OK; +} + +void +nsFrameSelection::SetDelayedCaretData(WidgetMouseEvent* aMouseEvent) +{ + if (aMouseEvent) { + mDelayedMouseEventValid = true; + mDelayedMouseEventIsShift = aMouseEvent->IsShift(); + mDelayedMouseEventClickCount = aMouseEvent->mClickCount; + } else { + mDelayedMouseEventValid = false; + } +} + +void +nsFrameSelection::DisconnectFromPresShell() +{ + RefPtr<AccessibleCaretEventHub> eventHub = mShell->GetAccessibleCaretEventHub(); + if (eventHub) { + int8_t index = GetIndexFromSelectionType(SelectionType::eNormal); + mDomSelections[index]->RemoveSelectionListener(eventHub); + } + + StopAutoScrollTimer(); + for (size_t i = 0; i < kPresentSelectionTypeCount; i++) { + mDomSelections[i]->Clear(nullptr); + } + mShell = nullptr; +} + +//END nsISelection interface implementations + +#if 0 +#pragma mark - +#endif + +// mozilla::dom::Selection implementation + +// note: this can return a nil anchor node + +Selection::Selection() + : mCachedOffsetForFrame(nullptr) + , mDirection(eDirNext) + , mSelectionType(SelectionType::eNormal) + , mUserInitiated(false) + , mSelectionChangeBlockerCount(0) +{ +} + +Selection::Selection(nsFrameSelection* aList) + : mFrameSelection(aList) + , mCachedOffsetForFrame(nullptr) + , mDirection(eDirNext) + , mSelectionType(SelectionType::eNormal) + , mUserInitiated(false) + , mSelectionChangeBlockerCount(0) +{ +} + +Selection::~Selection() +{ + setAnchorFocusRange(-1); + + uint32_t count = mRanges.Length(); + for (uint32_t i = 0; i < count; ++i) { + mRanges[i].mRange->SetSelection(nullptr); + } + + if (mAutoScrollTimer) { + mAutoScrollTimer->Stop(); + mAutoScrollTimer = nullptr; + } + + mScrollEvent.Revoke(); + + if (mCachedOffsetForFrame) { + delete mCachedOffsetForFrame; + mCachedOffsetForFrame = nullptr; + } +} + +nsIDocument* +Selection::GetParentObject() const +{ + nsIPresShell* shell = GetPresShell(); + if (shell) { + return shell->GetDocument(); + } + return nullptr; +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(Selection) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(Selection) + // Unlink the selection listeners *before* we do RemoveAllRanges since + // we don't want to notify the listeners during JS GC (they could be + // in JS!). + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelectionListeners) + tmp->RemoveAllRanges(); + NS_IMPL_CYCLE_COLLECTION_UNLINK(mFrameSelection) + NS_IMPL_CYCLE_COLLECTION_UNLINK_PRESERVED_WRAPPER +NS_IMPL_CYCLE_COLLECTION_UNLINK_END +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(Selection) + { + uint32_t i, count = tmp->mRanges.Length(); + for (i = 0; i < count; ++i) { + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRanges[i].mRange) + } + } + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mAnchorFocusRange) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFrameSelection) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelectionListeners) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE_SCRIPT_OBJECTS +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END +NS_IMPL_CYCLE_COLLECTION_TRACE_WRAPPERCACHE(Selection) + +// QueryInterface implementation for Selection +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(Selection) + NS_WRAPPERCACHE_INTERFACE_MAP_ENTRY + NS_INTERFACE_MAP_ENTRY(nsISelection) + NS_INTERFACE_MAP_ENTRY(nsISelectionPrivate) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISelection) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(Selection) +NS_IMPL_CYCLE_COLLECTING_RELEASE(Selection) + + +NS_IMETHODIMP +Selection::GetAnchorNode(nsIDOMNode** aAnchorNode) +{ + nsINode* anchorNode = GetAnchorNode(); + if (anchorNode) { + return CallQueryInterface(anchorNode, aAnchorNode); + } + + *aAnchorNode = nullptr; + return NS_OK; +} + +nsINode* +Selection::GetAnchorNode() +{ + if (!mAnchorFocusRange) + return nullptr; + + if (GetDirection() == eDirNext) { + return mAnchorFocusRange->GetStartParent(); + } + + return mAnchorFocusRange->GetEndParent(); +} + +NS_IMETHODIMP +Selection::GetAnchorOffset(int32_t* aAnchorOffset) +{ + *aAnchorOffset = static_cast<int32_t>(AnchorOffset()); + return NS_OK; +} + +// note: this can return a nil focus node +NS_IMETHODIMP +Selection::GetFocusNode(nsIDOMNode** aFocusNode) +{ + nsINode* focusNode = GetFocusNode(); + if (focusNode) { + return CallQueryInterface(focusNode, aFocusNode); + } + + *aFocusNode = nullptr; + return NS_OK; +} + +nsINode* +Selection::GetFocusNode() +{ + if (!mAnchorFocusRange) + return nullptr; + + if (GetDirection() == eDirNext){ + return mAnchorFocusRange->GetEndParent(); + } + + return mAnchorFocusRange->GetStartParent(); +} + +NS_IMETHODIMP +Selection::GetFocusOffset(int32_t* aFocusOffset) +{ + *aFocusOffset = static_cast<int32_t>(FocusOffset()); + return NS_OK; +} + +void +Selection::setAnchorFocusRange(int32_t indx) +{ + if (indx >= (int32_t)mRanges.Length()) + return; + if (indx < 0) //release all + { + mAnchorFocusRange = nullptr; + } + else{ + mAnchorFocusRange = mRanges[indx].mRange; + } +} + +uint32_t +Selection::AnchorOffset() +{ + if (!mAnchorFocusRange) + return 0; + + if (GetDirection() == eDirNext){ + return mAnchorFocusRange->StartOffset(); + } + + return mAnchorFocusRange->EndOffset(); +} + +uint32_t +Selection::FocusOffset() +{ + if (!mAnchorFocusRange) + return 0; + + if (GetDirection() == eDirNext){ + return mAnchorFocusRange->EndOffset(); + } + + return mAnchorFocusRange->StartOffset(); +} + +static nsresult +CompareToRangeStart(nsINode* aCompareNode, int32_t aCompareOffset, + nsRange* aRange, int32_t* aCmp) +{ + nsINode* start = aRange->GetStartParent(); + NS_ENSURE_STATE(aCompareNode && start); + // If the nodes that we're comparing are not in the same document, + // assume that aCompareNode will fall at the end of the ranges. + if (aCompareNode->GetComposedDoc() != start->GetComposedDoc() || + !start->GetComposedDoc()) { + *aCmp = 1; + } else { + *aCmp = nsContentUtils::ComparePoints(aCompareNode, aCompareOffset, + start, aRange->StartOffset()); + } + return NS_OK; +} + +static nsresult +CompareToRangeEnd(nsINode* aCompareNode, int32_t aCompareOffset, + nsRange* aRange, int32_t* aCmp) +{ + nsINode* end = aRange->GetEndParent(); + NS_ENSURE_STATE(aCompareNode && end); + // If the nodes that we're comparing are not in the same document, + // assume that aCompareNode will fall at the end of the ranges. + if (aCompareNode->GetComposedDoc() != end->GetComposedDoc() || + !end->GetComposedDoc()) { + *aCmp = 1; + } else { + *aCmp = nsContentUtils::ComparePoints(aCompareNode, aCompareOffset, + end, aRange->EndOffset()); + } + return NS_OK; +} + +// Selection::FindInsertionPoint +// +// Binary searches the given sorted array of ranges for the insertion point +// for the given node/offset. The given comparator is used, and the index +// where the point should appear in the array is placed in *aInsertionPoint. +// +// If there is an item in the array equal to the input point, we will return +// the index of this item. + +nsresult +Selection::FindInsertionPoint( + nsTArray<RangeData>* aElementArray, + nsINode* aPointNode, int32_t aPointOffset, + nsresult (*aComparator)(nsINode*,int32_t,nsRange*,int32_t*), + int32_t* aPoint) +{ + *aPoint = 0; + int32_t beginSearch = 0; + int32_t endSearch = aElementArray->Length(); // one beyond what to check + + if (endSearch) { + int32_t center = endSearch - 1; // Check last index, then binary search + do { + nsRange* range = (*aElementArray)[center].mRange; + + int32_t cmp; + nsresult rv = aComparator(aPointNode, aPointOffset, range, &cmp); + NS_ENSURE_SUCCESS(rv, rv); + + if (cmp < 0) { // point < cur + endSearch = center; + } else if (cmp > 0) { // point > cur + beginSearch = center + 1; + } else { // found match, done + beginSearch = center; + break; + } + center = (endSearch - beginSearch) / 2 + beginSearch; + } while (endSearch - beginSearch > 0); + } + + *aPoint = beginSearch; + return NS_OK; +} + +// Selection::SubtractRange +// +// A helper function that subtracts aSubtract from aRange, and adds +// 1 or 2 RangeData objects representing the remaining non-overlapping +// difference to aOutput. It is assumed that the caller has checked that +// aRange and aSubtract do indeed overlap + +nsresult +Selection::SubtractRange(RangeData* aRange, nsRange* aSubtract, + nsTArray<RangeData>* aOutput) +{ + nsRange* range = aRange->mRange; + + // First we want to compare to the range start + int32_t cmp; + nsresult rv = CompareToRangeStart(range->GetStartParent(), + range->StartOffset(), + aSubtract, &cmp); + NS_ENSURE_SUCCESS(rv, rv); + + // Also, make a comparison to the range end + int32_t cmp2; + rv = CompareToRangeEnd(range->GetEndParent(), + range->EndOffset(), + aSubtract, &cmp2); + NS_ENSURE_SUCCESS(rv, rv); + + // If the existing range left overlaps the new range (aSubtract) then + // cmp < 0, and cmp2 < 0 + // If it right overlaps the new range then cmp > 0 and cmp2 > 0 + // If it fully contains the new range, then cmp < 0 and cmp2 > 0 + + if (cmp2 > 0) { + // We need to add a new RangeData to the output, running from + // the end of aSubtract to the end of range + RefPtr<nsRange> postOverlap = new nsRange(aSubtract->GetEndParent()); + + rv = + postOverlap->SetStart(aSubtract->GetEndParent(), aSubtract->EndOffset()); + NS_ENSURE_SUCCESS(rv, rv); + rv = + postOverlap->SetEnd(range->GetEndParent(), range->EndOffset()); + NS_ENSURE_SUCCESS(rv, rv); + if (!postOverlap->Collapsed()) { + if (!aOutput->InsertElementAt(0, RangeData(postOverlap))) + return NS_ERROR_OUT_OF_MEMORY; + (*aOutput)[0].mTextRangeStyle = aRange->mTextRangeStyle; + } + } + + if (cmp < 0) { + // We need to add a new RangeData to the output, running from + // the start of the range to the start of aSubtract + RefPtr<nsRange> preOverlap = new nsRange(range->GetStartParent()); + + nsresult rv = + preOverlap->SetStart(range->GetStartParent(), range->StartOffset()); + NS_ENSURE_SUCCESS(rv, rv); + rv = + preOverlap->SetEnd(aSubtract->GetStartParent(), aSubtract->StartOffset()); + NS_ENSURE_SUCCESS(rv, rv); + + if (!preOverlap->Collapsed()) { + if (!aOutput->InsertElementAt(0, RangeData(preOverlap))) + return NS_ERROR_OUT_OF_MEMORY; + (*aOutput)[0].mTextRangeStyle = aRange->mTextRangeStyle; + } + } + + return NS_OK; +} + +void +Selection::UserSelectRangesToAdd(nsRange* aItem, nsTArray<RefPtr<nsRange>>& aRangesToAdd) +{ + aItem->ExcludeNonSelectableNodes(&aRangesToAdd); + if (aRangesToAdd.IsEmpty()) { + ErrorResult err; + nsINode* node = aItem->GetStartContainer(err); + if (node && node->IsContent() && node->AsContent()->GetEditingHost()) { + // A contenteditable node with user-select:none, for example. + // Allow it to have a collapsed selection (for the caret). + aItem->Collapse(GetDirection() == eDirPrevious); + aRangesToAdd.AppendElement(aItem); + } + } +} + +nsresult +Selection::AddItem(nsRange* aItem, int32_t* aOutIndex, bool aNoStartSelect) +{ + if (!aItem) + return NS_ERROR_NULL_POINTER; + if (!aItem->IsPositioned()) + return NS_ERROR_UNEXPECTED; + + NS_ASSERTION(aOutIndex, "aOutIndex can't be null"); + + if (mUserInitiated) { + AutoTArray<RefPtr<nsRange>, 4> rangesToAdd; + *aOutIndex = -1; + + nsIDocument* doc = GetParentObject(); + bool selectEventsEnabled = + nsFrameSelection::sSelectionEventsEnabled || + (doc && nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())); + + if (!aNoStartSelect && + mSelectionType == SelectionType::eNormal && + selectEventsEnabled && Collapsed() && + !IsBlockingSelectionChangeEvents()) { + // First, we generate the ranges to add with a scratch range, which is a + // clone of the original range passed in. We do this seperately, because the + // selectstart event could have caused the world to change, and required + // ranges to be re-generated + RefPtr<nsRange> scratchRange = aItem->CloneRange(); + UserSelectRangesToAdd(scratchRange, rangesToAdd); + bool newRangesNonEmpty = rangesToAdd.Length() > 1 || + (rangesToAdd.Length() == 1 && !rangesToAdd[0]->Collapsed()); + + MOZ_ASSERT(!newRangesNonEmpty || nsContentUtils::IsSafeToRunScript()); + if (newRangesNonEmpty && nsContentUtils::IsSafeToRunScript()) { + // We consider a selection to be starting if we are currently collapsed, + // and the selection is becoming uncollapsed, and this is caused by a user + // initiated event. + bool defaultAction = true; + + // The spec currently doesn't say that we should dispatch this event + // on text controls, so for now we only support doing that under a + // pref, disabled by default. + // See https://github.com/w3c/selection-api/issues/53. + bool dispatchEvent = true; + nsCOMPtr<nsINode> target = aItem->GetStartParent(); + if (nsFrameSelection::sSelectionEventsOnTextControlsEnabled) { + // Get the first element which isn't in a native anonymous subtree + while (target && target->IsInNativeAnonymousSubtree()) { + target = target->GetParent(); + } + } else { + if (target->IsInNativeAnonymousSubtree()) { + // This is a selection under a text control, so don't dispatch the + // event. + dispatchEvent = false; + } + } + + if (dispatchEvent) { + nsContentUtils::DispatchTrustedEvent(GetParentObject(), target, + NS_LITERAL_STRING("selectstart"), + true, true, &defaultAction); + + if (!defaultAction) { + return NS_OK; + } + + // As we just dispatched an event to the DOM, something could have + // changed under our feet. Re-generate the rangesToAdd array, and ensure + // that the range we are about to add is still valid. + if (!aItem->IsPositioned()) { + return NS_ERROR_UNEXPECTED; + } + } + } + + // The scratch ranges we generated may be invalid now, throw them out + rangesToAdd.ClearAndRetainStorage(); + } + + // Generate the ranges to add + UserSelectRangesToAdd(aItem, rangesToAdd); + size_t newAnchorFocusIndex = + GetDirection() == eDirPrevious ? 0 : rangesToAdd.Length() - 1; + for (size_t i = 0; i < rangesToAdd.Length(); ++i) { + int32_t index; + nsresult rv = AddItemInternal(rangesToAdd[i], &index); + NS_ENSURE_SUCCESS(rv, rv); + if (i == newAnchorFocusIndex) { + *aOutIndex = index; + rangesToAdd[i]->SetIsGenerated(false); + } else { + rangesToAdd[i]->SetIsGenerated(true); + } + } + return NS_OK; + } + return AddItemInternal(aItem, aOutIndex); +} + +nsresult +Selection::AddItemInternal(nsRange* aItem, int32_t* aOutIndex) +{ + MOZ_ASSERT(aItem); + MOZ_ASSERT(aItem->IsPositioned()); + MOZ_ASSERT(aOutIndex); + + *aOutIndex = -1; + + // a common case is that we have no ranges yet + if (mRanges.Length() == 0) { + if (!mRanges.AppendElement(RangeData(aItem))) + return NS_ERROR_OUT_OF_MEMORY; + aItem->SetSelection(this); + + *aOutIndex = 0; + return NS_OK; + } + + int32_t startIndex, endIndex; + nsresult rv = GetIndicesForInterval(aItem->GetStartParent(), + aItem->StartOffset(), + aItem->GetEndParent(), + aItem->EndOffset(), false, + &startIndex, &endIndex); + NS_ENSURE_SUCCESS(rv, rv); + + if (endIndex == -1) { + // All ranges start after the given range. We can insert our range at + // position 0, knowing there are no overlaps (handled below) + startIndex = 0; + endIndex = 0; + } else if (startIndex == -1) { + // All ranges end before the given range. We can insert our range at + // the end of the array, knowing there are no overlaps (handled below) + startIndex = mRanges.Length(); + endIndex = startIndex; + } + + // If the range is already contained in mRanges, silently succeed + bool sameRange = EqualsRangeAtPoint(aItem->GetStartParent(), + aItem->StartOffset(), + aItem->GetEndParent(), + aItem->EndOffset(), startIndex); + if (sameRange) { + *aOutIndex = startIndex; + return NS_OK; + } + + if (startIndex == endIndex) { + // The new range doesn't overlap any existing ranges + if (!mRanges.InsertElementAt(startIndex, RangeData(aItem))) + return NS_ERROR_OUT_OF_MEMORY; + aItem->SetSelection(this); + *aOutIndex = startIndex; + return NS_OK; + } + + // We now know that at least 1 existing range overlaps with the range that + // we are trying to add. In fact, the only ranges of interest are those at + // the two end points, startIndex and endIndex - 1 (which may point to the + // same range) as these may partially overlap the new range. Any ranges + // between these indices are fully overlapped by the new range, and so can be + // removed + nsTArray<RangeData> overlaps; + if (!overlaps.InsertElementAt(0, mRanges[startIndex])) + return NS_ERROR_OUT_OF_MEMORY; + + if (endIndex - 1 != startIndex) { + if (!overlaps.InsertElementAt(1, mRanges[endIndex - 1])) + return NS_ERROR_OUT_OF_MEMORY; + } + + // Remove all the overlapping ranges + for (int32_t i = startIndex; i < endIndex; ++i) { + mRanges[i].mRange->SetSelection(nullptr); + } + mRanges.RemoveElementsAt(startIndex, endIndex - startIndex); + + nsTArray<RangeData> temp; + for (int32_t i = overlaps.Length() - 1; i >= 0; i--) { + nsresult rv = SubtractRange(&overlaps[i], aItem, &temp); + NS_ENSURE_SUCCESS(rv, rv); + } + + // Insert the new element into our "leftovers" array + int32_t insertionPoint; + rv = FindInsertionPoint(&temp, aItem->GetStartParent(), + aItem->StartOffset(), CompareToRangeStart, + &insertionPoint); + NS_ENSURE_SUCCESS(rv, rv); + + if (!temp.InsertElementAt(insertionPoint, RangeData(aItem))) + return NS_ERROR_OUT_OF_MEMORY; + + // Merge the leftovers back in to mRanges + if (!mRanges.InsertElementsAt(startIndex, temp)) + return NS_ERROR_OUT_OF_MEMORY; + + for (uint32_t i = 0; i < temp.Length(); ++i) { + temp[i].mRange->SetSelection(this); + } + + *aOutIndex = startIndex + insertionPoint; + return NS_OK; +} + +nsresult +Selection::RemoveItem(nsRange* aItem) +{ + if (!aItem) + return NS_ERROR_NULL_POINTER; + + // Find the range's index & remove it. We could use FindInsertionPoint to + // get O(log n) time, but that requires many expensive DOM comparisons. + // For even several thousand items, this is probably faster because the + // comparisons are so fast. + int32_t idx = -1; + uint32_t i; + for (i = 0; i < mRanges.Length(); i ++) { + if (mRanges[i].mRange == aItem) { + idx = (int32_t)i; + break; + } + } + if (idx < 0) + return NS_ERROR_INVALID_ARG; + + mRanges.RemoveElementAt(idx); + aItem->SetSelection(nullptr); + return NS_OK; +} + +nsresult +Selection::RemoveCollapsedRanges() +{ + uint32_t i = 0; + while (i < mRanges.Length()) { + if (mRanges[i].mRange->Collapsed()) { + nsresult rv = RemoveItem(mRanges[i].mRange); + NS_ENSURE_SUCCESS(rv, rv); + } else { + ++i; + } + } + return NS_OK; +} + +nsresult +Selection::Clear(nsPresContext* aPresContext) +{ + setAnchorFocusRange(-1); + + for (uint32_t i = 0; i < mRanges.Length(); ++i) { + mRanges[i].mRange->SetSelection(nullptr); + selectFrames(aPresContext, mRanges[i].mRange, false); + } + mRanges.Clear(); + + // Reset direction so for more dependable table selection range handling + SetDirection(eDirNext); + + // If this was an ATTENTION selection, change it back to normal now + if (mFrameSelection && + mFrameSelection->GetDisplaySelection() == + nsISelectionController::SELECTION_ATTENTION) { + mFrameSelection->SetDisplaySelection(nsISelectionController::SELECTION_ON); + } + + return NS_OK; +} + +NS_IMETHODIMP +Selection::GetType(int16_t* aType) +{ + NS_ENSURE_ARG_POINTER(aType); + *aType = ToRawSelectionType(Type()); + return NS_OK; +} + +// RangeMatches*Point +// +// Compares the range beginning or ending point, and returns true if it +// exactly matches the given DOM point. + +static inline bool +RangeMatchesBeginPoint(nsRange* aRange, nsINode* aNode, int32_t aOffset) +{ + return aRange->GetStartParent() == aNode && aRange->StartOffset() == aOffset; +} + +static inline bool +RangeMatchesEndPoint(nsRange* aRange, nsINode* aNode, int32_t aOffset) +{ + return aRange->GetEndParent() == aNode && aRange->EndOffset() == aOffset; +} + +// Selection::EqualsRangeAtPoint +// +// Utility method for checking equivalence of two ranges. + +bool +Selection::EqualsRangeAtPoint( + nsINode* aBeginNode, int32_t aBeginOffset, + nsINode* aEndNode, int32_t aEndOffset, + int32_t aRangeIndex) +{ + if (aRangeIndex >=0 && aRangeIndex < (int32_t) mRanges.Length()) { + nsRange* range = mRanges[aRangeIndex].mRange; + if (RangeMatchesBeginPoint(range, aBeginNode, aBeginOffset) && + RangeMatchesEndPoint(range, aEndNode, aEndOffset)) + return true; + } + return false; +} + +// Selection::GetRangesForInterval +// +// XPCOM wrapper for the nsTArray version + +NS_IMETHODIMP +Selection::GetRangesForInterval(nsIDOMNode* aBeginNode, int32_t aBeginOffset, + nsIDOMNode* aEndNode, int32_t aEndOffset, + bool aAllowAdjacent, + uint32_t* aResultCount, + nsIDOMRange*** aResults) +{ + if (!aBeginNode || ! aEndNode || ! aResultCount || ! aResults) + return NS_ERROR_NULL_POINTER; + + *aResultCount = 0; + *aResults = nullptr; + + nsTArray<RefPtr<nsRange>> results; + ErrorResult result; + nsCOMPtr<nsINode> beginNode = do_QueryInterface(aBeginNode); + nsCOMPtr<nsINode> endNode = do_QueryInterface(aEndNode); + NS_ENSURE_TRUE(beginNode && endNode, NS_ERROR_NULL_POINTER); + GetRangesForInterval(*beginNode, aBeginOffset, *endNode, aEndOffset, + aAllowAdjacent, results, result); + if (result.Failed()) { + return result.StealNSResult(); + } + *aResultCount = results.Length(); + if (*aResultCount == 0) { + return NS_OK; + } + + *aResults = static_cast<nsIDOMRange**> + (moz_xmalloc(sizeof(nsIDOMRange*) * *aResultCount)); + NS_ENSURE_TRUE(*aResults, NS_ERROR_OUT_OF_MEMORY); + + for (uint32_t i = 0; i < *aResultCount; i++) { + (*aResults)[i] = results[i].forget().take(); + } + return NS_OK; +} + + +void +Selection::GetRangesForInterval(nsINode& aBeginNode, int32_t aBeginOffset, + nsINode& aEndNode, int32_t aEndOffset, + bool aAllowAdjacent, + nsTArray<RefPtr<nsRange>>& aReturn, + mozilla::ErrorResult& aRv) +{ + nsTArray<nsRange*> results; + nsresult rv = GetRangesForIntervalArray(&aBeginNode, aBeginOffset, + &aEndNode, aEndOffset, + aAllowAdjacent, &results); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + aReturn.SetLength(results.Length()); + for (uint32_t i = 0; i < results.Length(); ++i) { + aReturn[i] = results[i]; // AddRefs + } +} + +// Selection::GetRangesForIntervalArray +// +// Fills a nsTArray with the ranges overlapping the range specified by +// the given endpoints. Ranges in the selection exactly adjacent to the +// input range are not returned unless aAllowAdjacent is set. +// +// For example, if the following ranges were in the selection +// (assume everything is within the same node) +// +// Start Offset: 0 2 7 9 +// End Offset: 2 5 9 10 +// +// and passed aBeginOffset of 2 and aEndOffset of 9, then with +// aAllowAdjacent set, all the ranges should be returned. If +// aAllowAdjacent was false, the ranges [2, 5] and [7, 9] only +// should be returned +// +// Now that overlapping ranges are disallowed, there can be a maximum of +// 2 adjacent ranges + +nsresult +Selection::GetRangesForIntervalArray(nsINode* aBeginNode, int32_t aBeginOffset, + nsINode* aEndNode, int32_t aEndOffset, + bool aAllowAdjacent, + nsTArray<nsRange*>* aRanges) +{ + aRanges->Clear(); + int32_t startIndex, endIndex; + nsresult res = GetIndicesForInterval(aBeginNode, aBeginOffset, + aEndNode, aEndOffset, aAllowAdjacent, + &startIndex, &endIndex); + NS_ENSURE_SUCCESS(res, res); + + if (startIndex == -1 || endIndex == -1) + return NS_OK; + + for (int32_t i = startIndex; i < endIndex; i++) { + if (!aRanges->AppendElement(mRanges[i].mRange)) + return NS_ERROR_OUT_OF_MEMORY; + } + + return NS_OK; +} + +// Selection::GetIndicesForInterval +// +// Works on the same principle as GetRangesForIntervalArray above, however +// instead this returns the indices into mRanges between which the +// overlapping ranges lie. + +nsresult +Selection::GetIndicesForInterval(nsINode* aBeginNode, int32_t aBeginOffset, + nsINode* aEndNode, int32_t aEndOffset, + bool aAllowAdjacent, + int32_t* aStartIndex, int32_t* aEndIndex) +{ + int32_t startIndex; + int32_t endIndex; + + if (!aStartIndex) + aStartIndex = &startIndex; + if (!aEndIndex) + aEndIndex = &endIndex; + + *aStartIndex = -1; + *aEndIndex = -1; + + if (mRanges.Length() == 0) + return NS_OK; + + bool intervalIsCollapsed = aBeginNode == aEndNode && + aBeginOffset == aEndOffset; + + // Ranges that end before the given interval and begin after the given + // interval can be discarded + int32_t endsBeforeIndex; + if (NS_FAILED(FindInsertionPoint(&mRanges, aEndNode, aEndOffset, + &CompareToRangeStart, + &endsBeforeIndex))) { + return NS_OK; + } + + if (endsBeforeIndex == 0) { + nsRange* endRange = mRanges[endsBeforeIndex].mRange; + + // If the interval is strictly before the range at index 0, we can optimize + // by returning now - all ranges start after the given interval + if (!RangeMatchesBeginPoint(endRange, aEndNode, aEndOffset)) + return NS_OK; + + // We now know that the start point of mRanges[0].mRange equals the end of + // the interval. Thus, when aAllowadjacent is true, the caller is always + // interested in this range. However, when excluding adjacencies, we must + // remember to include the range when both it and the given interval are + // collapsed to the same point + if (!aAllowAdjacent && !(endRange->Collapsed() && intervalIsCollapsed)) + return NS_OK; + } + *aEndIndex = endsBeforeIndex; + + int32_t beginsAfterIndex; + if (NS_FAILED(FindInsertionPoint(&mRanges, aBeginNode, aBeginOffset, + &CompareToRangeEnd, + &beginsAfterIndex))) { + return NS_OK; + } + if (beginsAfterIndex == (int32_t) mRanges.Length()) + return NS_OK; // optimization: all ranges are strictly before us + + if (aAllowAdjacent) { + // At this point, one of the following holds: + // endsBeforeIndex == mRanges.Length(), + // endsBeforeIndex points to a range whose start point does not equal the + // given interval's start point + // endsBeforeIndex points to a range whose start point equals the given + // interval's start point + // In the final case, there can be two such ranges, a collapsed range, and + // an adjacent range (they will appear in mRanges in that order). For this + // final case, we need to increment endsBeforeIndex, until one of the + // first two possibilites hold + while (endsBeforeIndex < (int32_t) mRanges.Length()) { + nsRange* endRange = mRanges[endsBeforeIndex].mRange; + if (!RangeMatchesBeginPoint(endRange, aEndNode, aEndOffset)) + break; + endsBeforeIndex++; + } + + // Likewise, one of the following holds: + // beginsAfterIndex == 0, + // beginsAfterIndex points to a range whose end point does not equal + // the given interval's end point + // beginsOnOrAfter points to a range whose end point equals the given + // interval's end point + // In the final case, there can be two such ranges, an adjacent range, and + // a collapsed range (they will appear in mRanges in that order). For this + // final case, we only need to take action if both those ranges exist, and + // we are pointing to the collapsed range - we need to point to the + // adjacent range + nsRange* beginRange = mRanges[beginsAfterIndex].mRange; + if (beginsAfterIndex > 0 && beginRange->Collapsed() && + RangeMatchesEndPoint(beginRange, aBeginNode, aBeginOffset)) { + beginRange = mRanges[beginsAfterIndex - 1].mRange; + if (RangeMatchesEndPoint(beginRange, aBeginNode, aBeginOffset)) + beginsAfterIndex--; + } + } else { + // See above for the possibilities at this point. The only case where we + // need to take action is when the range at beginsAfterIndex ends on + // the given interval's start point, but that range isn't collapsed (a + // collapsed range should be included in the returned results). + nsRange* beginRange = mRanges[beginsAfterIndex].mRange; + if (RangeMatchesEndPoint(beginRange, aBeginNode, aBeginOffset) && + !beginRange->Collapsed()) + beginsAfterIndex++; + + // Again, see above for the meaning of endsBeforeIndex at this point. + // In particular, endsBeforeIndex may point to a collaped range which + // represents the point at the end of the interval - this range should be + // included + if (endsBeforeIndex < (int32_t) mRanges.Length()) { + nsRange* endRange = mRanges[endsBeforeIndex].mRange; + if (RangeMatchesBeginPoint(endRange, aEndNode, aEndOffset) && + endRange->Collapsed()) + endsBeforeIndex++; + } + } + + NS_ASSERTION(beginsAfterIndex <= endsBeforeIndex, + "Is mRanges not ordered?"); + NS_ENSURE_STATE(beginsAfterIndex <= endsBeforeIndex); + + *aStartIndex = beginsAfterIndex; + *aEndIndex = endsBeforeIndex; + return NS_OK; +} + +NS_IMETHODIMP +Selection::GetPrimaryFrameForAnchorNode(nsIFrame** aReturnFrame) +{ + if (!aReturnFrame) + return NS_ERROR_NULL_POINTER; + + int32_t frameOffset = 0; + *aReturnFrame = 0; + nsCOMPtr<nsIContent> content = do_QueryInterface(GetAnchorNode()); + if (content && mFrameSelection) + { + *aReturnFrame = mFrameSelection-> + GetFrameForNodeOffset(content, AnchorOffset(), + mFrameSelection->GetHint(), &frameOffset); + if (*aReturnFrame) + return NS_OK; + } + return NS_ERROR_FAILURE; +} + +NS_IMETHODIMP +Selection::GetPrimaryFrameForFocusNode(nsIFrame** aReturnFrame, + int32_t* aOffsetUsed, + bool aVisual) +{ + if (!aReturnFrame) + return NS_ERROR_NULL_POINTER; + + nsCOMPtr<nsIContent> content = do_QueryInterface(GetFocusNode()); + if (!content || !mFrameSelection) + return NS_ERROR_FAILURE; + + int32_t frameOffset = 0; + *aReturnFrame = 0; + if (!aOffsetUsed) + aOffsetUsed = &frameOffset; + + CaretAssociationHint hint = mFrameSelection->GetHint(); + + if (aVisual) { + nsBidiLevel caretBidiLevel = mFrameSelection->GetCaretBidiLevel(); + + return nsCaret::GetCaretFrameForNodeOffset(mFrameSelection, + content, FocusOffset(), hint, caretBidiLevel, aReturnFrame, aOffsetUsed); + } + + *aReturnFrame = mFrameSelection-> + GetFrameForNodeOffset(content, FocusOffset(), + hint, aOffsetUsed); + if (!*aReturnFrame) + return NS_ERROR_FAILURE; + + return NS_OK; +} + +//select all content children of aContent +nsresult +Selection::SelectAllFramesForContent(nsIContentIterator* aInnerIter, + nsIContent* aContent, + bool aSelected) +{ + nsresult result = aInnerIter->Init(aContent); + nsIFrame *frame; + if (NS_SUCCEEDED(result)) + { + // First select frame of content passed in + frame = aContent->GetPrimaryFrame(); + if (frame && frame->GetType() == nsGkAtoms::textFrame) { + nsTextFrame* textFrame = static_cast<nsTextFrame*>(frame); + textFrame->SetSelectedRange(0, aContent->GetText()->GetLength(), + aSelected, mSelectionType); + } + // Now iterated through the child frames and set them + while (!aInnerIter->IsDone()) { + nsCOMPtr<nsIContent> innercontent = + do_QueryInterface(aInnerIter->GetCurrentNode()); + + frame = innercontent->GetPrimaryFrame(); + if (frame) { + if (frame->GetType() == nsGkAtoms::textFrame) { + nsTextFrame* textFrame = static_cast<nsTextFrame*>(frame); + textFrame->SetSelectedRange(0, innercontent->GetText()->GetLength(), + aSelected, mSelectionType); + } else { + frame->InvalidateFrameSubtree(); // frame continuations? + } + } + + aInnerIter->Next(); + } + + return NS_OK; + } + + return NS_ERROR_FAILURE; +} + +/** + * The idea of this helper method is to select or deselect "top to bottom", + * traversing through the frames + */ +nsresult +Selection::selectFrames(nsPresContext* aPresContext, nsRange* aRange, + bool aSelect) +{ + if (!mFrameSelection || !aPresContext || !aPresContext->GetPresShell()) { + // nothing to do + return NS_OK; + } + MOZ_ASSERT(aRange); + + if (mFrameSelection->GetTableCellSelection()) { + nsINode* node = aRange->GetCommonAncestor(); + nsIFrame* frame = node->IsContent() ? node->AsContent()->GetPrimaryFrame() + : aPresContext->FrameManager()->GetRootFrame(); + if (frame) { + frame->InvalidateFrameSubtree(); + } + return NS_OK; + } + + nsCOMPtr<nsIContentIterator> iter = NS_NewContentSubtreeIterator(); + iter->Init(aRange); + + // Loop through the content iterator for each content node; for each text + // node, call SetSelected on it: + nsCOMPtr<nsIContent> content = do_QueryInterface(aRange->GetStartParent()); + if (!content) { + // Don't warn, bug 1055722 + return NS_ERROR_UNEXPECTED; + } + + // We must call first one explicitly + if (content->IsNodeOfType(nsINode::eTEXT)) { + nsIFrame* frame = content->GetPrimaryFrame(); + // The frame could be an SVG text frame, in which case we'll ignore it. + if (frame && frame->GetType() == nsGkAtoms::textFrame) { + nsTextFrame* textFrame = static_cast<nsTextFrame*>(frame); + uint32_t startOffset = aRange->StartOffset(); + uint32_t endOffset; + if (aRange->GetEndParent() == content) { + endOffset = aRange->EndOffset(); + } else { + endOffset = content->Length(); + } + textFrame->SetSelectedRange(startOffset, endOffset, aSelect, + mSelectionType); + } + } + + iter->First(); + nsCOMPtr<nsIContentIterator> inneriter = NS_NewContentIterator(); + for (iter->First(); !iter->IsDone(); iter->Next()) { + content = do_QueryInterface(iter->GetCurrentNode()); + SelectAllFramesForContent(inneriter, content, aSelect); + } + + // We must now do the last one if it is not the same as the first + if (aRange->GetEndParent() != aRange->GetStartParent()) { + nsresult res; + content = do_QueryInterface(aRange->GetEndParent(), &res); + NS_ENSURE_SUCCESS(res, res); + NS_ENSURE_TRUE(content, res); + + if (content->IsNodeOfType(nsINode::eTEXT)) { + nsIFrame* frame = content->GetPrimaryFrame(); + // The frame could be an SVG text frame, in which case we'll ignore it. + if (frame && frame->GetType() == nsGkAtoms::textFrame) { + nsTextFrame* textFrame = static_cast<nsTextFrame*>(frame); + textFrame->SetSelectedRange(0, aRange->EndOffset(), aSelect, + mSelectionType); + } + } + } + return NS_OK; +} + + +// Selection::LookUpSelection +// +// This function is called when a node wants to know where the selection is +// over itself. +// +// Usually, this is called when we already know there is a selection over +// the node in question, and we only need to find the boundaries of it on +// that node. This is when slowCheck is false--a strict test is not needed. +// Other times, the caller has no idea, and wants us to test everything, +// so we are supposed to determine whether there is a selection over the +// node at all. +// +// A previous version of this code used this flag to do less work when +// inclusion was already known (slowCheck=false). However, our tree +// structure allows us to quickly determine ranges overlapping the node, +// so we just ignore the slowCheck flag and do the full test every time. +// +// PERFORMANCE: a common case is that we are doing a fast check with exactly +// one range in the selection. In this case, this function is slower than +// brute force because of the overhead of checking the tree. We can optimize +// this case to make it faster by doing the same thing the previous version +// of this function did in the case of 1 range. This would also mean that +// the aSlowCheck flag would have meaning again. + +NS_IMETHODIMP +Selection::LookUpSelection(nsIContent* aContent, int32_t aContentOffset, + int32_t aContentLength, + SelectionDetails** aReturnDetails, + SelectionType aSelectionType, + bool aSlowCheck) +{ + nsresult rv; + if (!aContent || ! aReturnDetails) + return NS_ERROR_NULL_POINTER; + + // it is common to have no ranges, to optimize that + if (mRanges.Length() == 0) + return NS_OK; + + nsTArray<nsRange*> overlappingRanges; + rv = GetRangesForIntervalArray(aContent, aContentOffset, + aContent, aContentOffset + aContentLength, + false, + &overlappingRanges); + NS_ENSURE_SUCCESS(rv, rv); + if (overlappingRanges.Length() == 0) + return NS_OK; + + for (uint32_t i = 0; i < overlappingRanges.Length(); i++) { + nsRange* range = overlappingRanges[i]; + nsINode* startNode = range->GetStartParent(); + nsINode* endNode = range->GetEndParent(); + int32_t startOffset = range->StartOffset(); + int32_t endOffset = range->EndOffset(); + + int32_t start = -1, end = -1; + if (startNode == aContent && endNode == aContent) { + if (startOffset < (aContentOffset + aContentLength) && + endOffset > aContentOffset) { + // this range is totally inside the requested content range + start = std::max(0, startOffset - aContentOffset); + end = std::min(aContentLength, endOffset - aContentOffset); + } + // otherwise, range is inside the requested node, but does not intersect + // the requested content range, so ignore it + } else if (startNode == aContent) { + if (startOffset < (aContentOffset + aContentLength)) { + // the beginning of the range is inside the requested node, but the + // end is outside, select everything from there to the end + start = std::max(0, startOffset - aContentOffset); + end = aContentLength; + } + } else if (endNode == aContent) { + if (endOffset > aContentOffset) { + // the end of the range is inside the requested node, but the beginning + // is outside, select everything from the beginning to there + start = 0; + end = std::min(aContentLength, endOffset - aContentOffset); + } + } else { + // this range does not begin or end in the requested node, but since + // GetRangesForInterval returned this range, we know it overlaps. + // Therefore, this node is enclosed in the range, and we select all + // of it. + start = 0; + end = aContentLength; + } + if (start < 0) + continue; // the ranges do not overlap the input range + + SelectionDetails* details = new SelectionDetails; + + details->mNext = *aReturnDetails; + details->mStart = start; + details->mEnd = end; + details->mSelectionType = aSelectionType; + RangeData *rd = FindRangeData(range); + if (rd) { + details->mTextRangeStyle = rd->mTextRangeStyle; + } + *aReturnDetails = details; + } + return NS_OK; +} + +NS_IMETHODIMP +Selection::Repaint(nsPresContext* aPresContext) +{ + int32_t arrCount = (int32_t)mRanges.Length(); + + if (arrCount < 1) + return NS_OK; + + int32_t i; + + for (i = 0; i < arrCount; i++) + { + nsresult rv = selectFrames(aPresContext, mRanges[i].mRange, true); + + if (NS_FAILED(rv)) { + return rv; + } + } + + return NS_OK; +} + +NS_IMETHODIMP +Selection::GetCanCacheFrameOffset(bool* aCanCacheFrameOffset) +{ + NS_ENSURE_ARG_POINTER(aCanCacheFrameOffset); + + if (mCachedOffsetForFrame) + *aCanCacheFrameOffset = mCachedOffsetForFrame->mCanCacheFrameOffset; + else + *aCanCacheFrameOffset = false; + + return NS_OK; +} + +NS_IMETHODIMP +Selection::SetCanCacheFrameOffset(bool aCanCacheFrameOffset) +{ + if (!mCachedOffsetForFrame) { + mCachedOffsetForFrame = new CachedOffsetForFrame; + } + + mCachedOffsetForFrame->mCanCacheFrameOffset = aCanCacheFrameOffset; + + // clean up cached frame when turn off cache + // fix bug 207936 + if (!aCanCacheFrameOffset) { + mCachedOffsetForFrame->mLastCaretFrame = nullptr; + } + + return NS_OK; +} + +NS_IMETHODIMP +Selection::GetCachedFrameOffset(nsIFrame* aFrame, int32_t inOffset, + nsPoint& aPoint) +{ + if (!mCachedOffsetForFrame) { + mCachedOffsetForFrame = new CachedOffsetForFrame; + } + + nsresult rv = NS_OK; + if (mCachedOffsetForFrame->mCanCacheFrameOffset && + mCachedOffsetForFrame->mLastCaretFrame && + (aFrame == mCachedOffsetForFrame->mLastCaretFrame) && + (inOffset == mCachedOffsetForFrame->mLastContentOffset)) + { + // get cached frame offset + aPoint = mCachedOffsetForFrame->mCachedFrameOffset; + } + else + { + // Recalculate frame offset and cache it. Don't cache a frame offset if + // GetPointFromOffset fails, though. + rv = aFrame->GetPointFromOffset(inOffset, &aPoint); + if (NS_SUCCEEDED(rv) && mCachedOffsetForFrame->mCanCacheFrameOffset) { + mCachedOffsetForFrame->mCachedFrameOffset = aPoint; + mCachedOffsetForFrame->mLastCaretFrame = aFrame; + mCachedOffsetForFrame->mLastContentOffset = inOffset; + } + } + + return rv; +} + +NS_IMETHODIMP +Selection::GetAncestorLimiter(nsIContent** aContent) +{ + if (mFrameSelection) { + nsCOMPtr<nsIContent> c = mFrameSelection->GetAncestorLimiter(); + c.forget(aContent); + } + return NS_OK; +} + +NS_IMETHODIMP +Selection::SetAncestorLimiter(nsIContent* aContent) +{ + if (mFrameSelection) { + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + frameSelection->SetAncestorLimiter(aContent); + } + return NS_OK; +} + +RangeData* +Selection::FindRangeData(nsIDOMRange* aRange) +{ + NS_ENSURE_TRUE(aRange, nullptr); + for (uint32_t i = 0; i < mRanges.Length(); i++) { + if (mRanges[i].mRange == aRange) + return &mRanges[i]; + } + return nullptr; +} + +NS_IMETHODIMP +Selection::SetTextRangeStyle(nsIDOMRange* aRange, + const TextRangeStyle& aTextRangeStyle) +{ + NS_ENSURE_ARG_POINTER(aRange); + RangeData *rd = FindRangeData(aRange); + if (rd) { + rd->mTextRangeStyle = aTextRangeStyle; + } + return NS_OK; +} + +nsresult +Selection::StartAutoScrollTimer(nsIFrame* aFrame, nsPoint& aPoint, + uint32_t aDelay) +{ + NS_PRECONDITION(aFrame, "Need a frame"); + + nsresult result; + if (!mFrameSelection) + return NS_OK;//nothing to do + + if (!mAutoScrollTimer) + { + mAutoScrollTimer = new nsAutoScrollTimer(); + + result = mAutoScrollTimer->Init(mFrameSelection, this); + + if (NS_FAILED(result)) + return result; + } + + result = mAutoScrollTimer->SetDelay(aDelay); + + if (NS_FAILED(result)) + return result; + + return DoAutoScroll(aFrame, aPoint); +} + +nsresult +Selection::StopAutoScrollTimer() +{ + if (mAutoScrollTimer) { + return mAutoScrollTimer->Stop(); + } + return NS_OK; +} + +nsresult +Selection::DoAutoScroll(nsIFrame* aFrame, nsPoint& aPoint) +{ + NS_PRECONDITION(aFrame, "Need a frame"); + + if (mAutoScrollTimer) + (void)mAutoScrollTimer->Stop(); + + nsPresContext* presContext = aFrame->PresContext(); + nsCOMPtr<nsIPresShell> shell = presContext->PresShell(); + nsRootPresContext* rootPC = presContext->GetRootPresContext(); + if (!rootPC) + return NS_OK; + nsIFrame* rootmostFrame = rootPC->PresShell()->FrameManager()->GetRootFrame(); + nsWeakFrame weakRootFrame(rootmostFrame); + nsWeakFrame weakFrame(aFrame); + // Get the point relative to the root most frame because the scroll we are + // about to do will change the coordinates of aFrame. + nsPoint globalPoint = aPoint + aFrame->GetOffsetToCrossDoc(rootmostFrame); + + bool done = false; + bool didScroll; + while (true) { + didScroll = shell->ScrollFrameRectIntoView( + aFrame, nsRect(aPoint, nsSize(0, 0)), + nsIPresShell::ScrollAxis(), nsIPresShell::ScrollAxis(), + 0); + if (!weakFrame || !weakRootFrame) { + return NS_OK; + } + if (!didScroll && !done) { + // If aPoint is at the screen edge then try to scroll anyway, once. + RefPtr<nsDeviceContext> dx = shell->GetViewManager()->GetDeviceContext(); + nsRect screen; + dx->GetRect(screen); + nsPoint screenPoint = globalPoint + + rootmostFrame->GetScreenRectInAppUnits().TopLeft(); + nscoord onePx = nsPresContext::AppUnitsPerCSSPixel(); + nscoord scrollAmount = 10 * onePx; + if (std::abs(screen.x - screenPoint.x) <= onePx) { + aPoint.x -= scrollAmount; + } else if (std::abs(screen.XMost() - screenPoint.x) <= onePx) { + aPoint.x += scrollAmount; + } else if (std::abs(screen.y - screenPoint.y) <= onePx) { + aPoint.y -= scrollAmount; + } else if (std::abs(screen.YMost() - screenPoint.y) <= onePx) { + aPoint.y += scrollAmount; + } else { + break; + } + done = true; + continue; + } + break; + } + + // Start the AutoScroll timer if necessary. + if (didScroll && mAutoScrollTimer) { + nsPoint presContextPoint = globalPoint - + shell->FrameManager()->GetRootFrame()->GetOffsetToCrossDoc(rootmostFrame); + mAutoScrollTimer->Start(presContext, presContextPoint); + } + + return NS_OK; +} + + +/** RemoveAllRanges zeroes the selection + */ +NS_IMETHODIMP +Selection::RemoveAllRanges() +{ + ErrorResult result; + RemoveAllRanges(result); + return result.StealNSResult(); +} + +void +Selection::RemoveAllRanges(ErrorResult& aRv) +{ + if (!mFrameSelection) + return; // nothing to do + RefPtr<nsPresContext> presContext = GetPresContext(); + nsresult result = Clear(presContext); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + + // Turn off signal for table selection + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + frameSelection->ClearTableCellSelection(); + + result = frameSelection->NotifySelectionListeners(GetType()); + // Also need to notify the frames! + // PresShell::CharacterDataChanged should do that on DocumentChanged + if (NS_FAILED(result)) { + aRv.Throw(result); + } +} + +/** AddRange adds the specified range to the selection + * @param aRange is the range to be added + */ +NS_IMETHODIMP +Selection::AddRange(nsIDOMRange* aDOMRange) +{ + if (!aDOMRange) { + return NS_ERROR_NULL_POINTER; + } + nsRange* range = static_cast<nsRange*>(aDOMRange); + ErrorResult result; + AddRange(*range, result); + return result.StealNSResult(); +} + +void +Selection::AddRange(nsRange& aRange, ErrorResult& aRv) +{ + return AddRangeInternal(aRange, GetParentObject(), aRv); +} + +void +Selection::AddRangeInternal(nsRange& aRange, nsIDocument* aDocument, + ErrorResult& aRv) +{ + nsINode* rangeRoot = aRange.GetRoot(); + if (aDocument != rangeRoot && (!rangeRoot || + aDocument != rangeRoot->GetComposedDoc())) { + // http://w3c.github.io/selection-api/#dom-selection-addrange + // "... if the root of the range's boundary points are the document + // associated with context object. Otherwise, this method must do nothing." + return; + } + + // This inserts a table cell range in proper document order + // and returns NS_OK if range doesn't contain just one table cell + bool didAddRange; + int32_t rangeIndex; + nsresult result = addTableCellRange(&aRange, &didAddRange, &rangeIndex); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + + if (!didAddRange) { + result = AddItem(&aRange, &rangeIndex); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + } + + if (rangeIndex < 0) { + return; + } + + setAnchorFocusRange(rangeIndex); + + // Make sure the caret appears on the next line, if at a newline + if (mSelectionType == SelectionType::eNormal) { + SetInterlinePosition(true); + } + + RefPtr<nsPresContext> presContext = GetPresContext(); + selectFrames(presContext, &aRange, true); + + if (!mFrameSelection) + return;//nothing to do + + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + result = frameSelection->NotifySelectionListeners(GetType()); + if (NS_FAILED(result)) { + aRv.Throw(result); + } +} + +// Selection::RemoveRange +// +// Removes the given range from the selection. The tricky part is updating +// the flags on the frames that indicate whether they have a selection or +// not. There could be several selection ranges on the frame, and clearing +// the bit would cause the selection to not be drawn, even when there is +// another range on the frame (bug 346185). +// +// We therefore find any ranges that intersect the same nodes as the range +// being removed, and cause them to set the selected bits back on their +// selected frames after we've cleared the bit from ours. + +nsresult +Selection::RemoveRange(nsIDOMRange* aDOMRange) +{ + if (!aDOMRange) { + return NS_ERROR_INVALID_ARG; + } + nsRange* range = static_cast<nsRange*>(aDOMRange); + ErrorResult result; + RemoveRange(*range, result); + return result.StealNSResult(); +} + +void +Selection::RemoveRange(nsRange& aRange, ErrorResult& aRv) +{ + nsresult rv = RemoveItem(&aRange); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + + nsINode* beginNode = aRange.GetStartParent(); + nsINode* endNode = aRange.GetEndParent(); + + if (!beginNode || !endNode) { + // Detached range; nothing else to do here. + return; + } + + // find out the length of the end node, so we can select all of it + int32_t beginOffset, endOffset; + if (endNode->IsNodeOfType(nsINode::eTEXT)) { + // Get the length of the text. We can't just use the offset because + // another range could be touching this text node but not intersect our + // range. + beginOffset = 0; + endOffset = static_cast<nsIContent*>(endNode)->TextLength(); + } else { + // For non-text nodes, the given offsets should be sufficient. + beginOffset = aRange.StartOffset(); + endOffset = aRange.EndOffset(); + } + + // clear the selected bit from the removed range's frames + RefPtr<nsPresContext> presContext = GetPresContext(); + selectFrames(presContext, &aRange, false); + + // add back the selected bit for each range touching our nodes + nsTArray<nsRange*> affectedRanges; + rv = GetRangesForIntervalArray(beginNode, beginOffset, + endNode, endOffset, + true, &affectedRanges); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return; + } + for (uint32_t i = 0; i < affectedRanges.Length(); i++) { + selectFrames(presContext, affectedRanges[i], true); + } + + int32_t cnt = mRanges.Length(); + if (&aRange == mAnchorFocusRange) { + // Reset anchor to LAST range or clear it if there are no ranges. + setAnchorFocusRange(cnt - 1); + + // When the selection is user-created it makes sense to scroll the range + // into view. The spell-check selection, however, is created and destroyed + // in the background. We don't want to scroll in this case or the view + // might appear to be moving randomly (bug 337871). + if (mSelectionType != SelectionType::eSpellCheck && cnt > 0) { + ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION); + } + } + + if (!mFrameSelection) + return;//nothing to do + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + rv = frameSelection->NotifySelectionListeners(GetType()); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + + + +/* + * Collapse sets the whole selection to be one point. + */ +NS_IMETHODIMP +Selection::Collapse(nsIDOMNode* aParentNode, int32_t aOffset) +{ + nsCOMPtr<nsINode> parentNode = do_QueryInterface(aParentNode); + return Collapse(parentNode, aOffset); +} + +NS_IMETHODIMP +Selection::CollapseNative(nsINode* aParentNode, int32_t aOffset) +{ + return Collapse(aParentNode, aOffset); +} + +nsresult +Selection::Collapse(nsINode* aParentNode, int32_t aOffset) +{ + if (!aParentNode) + return NS_ERROR_INVALID_ARG; + + ErrorResult result; + Collapse(*aParentNode, static_cast<uint32_t>(aOffset), result); + return result.StealNSResult(); +} + +void +Selection::Collapse(nsINode& aParentNode, uint32_t aOffset, ErrorResult& aRv) +{ + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection + return; + } + + nsCOMPtr<nsINode> parentNode = &aParentNode; + + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + frameSelection->InvalidateDesiredPos(); + if (!IsValidSelectionPoint(frameSelection, parentNode)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + nsresult result; + + RefPtr<nsPresContext> presContext = GetPresContext(); + if (!presContext || presContext->Document() != parentNode->OwnerDoc()) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + // Delete all of the current ranges + Clear(presContext); + + // Turn off signal for table selection + frameSelection->ClearTableCellSelection(); + + // Hack to display the caret on the right line (bug 1237236). + if (frameSelection->GetHint() != CARET_ASSOCIATE_AFTER && + parentNode->IsContent()) { + int32_t frameOffset; + nsTextFrame* f = + do_QueryFrame(nsCaret::GetFrameAndOffset(this, parentNode, + aOffset, &frameOffset)); + if (f && f->IsAtEndOfLine() && f->HasSignificantTerminalNewline()) { + if ((parentNode->AsContent() == f->GetContent() && + f->GetContentEnd() == int32_t(aOffset)) || + (parentNode == f->GetContent()->GetParentNode() && + parentNode->IndexOf(f->GetContent()) + 1 == int32_t(aOffset))) { + frameSelection->SetHint(CARET_ASSOCIATE_AFTER); + } + } + } + + RefPtr<nsRange> range = new nsRange(parentNode); + result = range->SetEnd(parentNode, aOffset); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + result = range->SetStart(parentNode, aOffset); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + +#ifdef DEBUG_SELECTION + nsCOMPtr<nsIContent> content = do_QueryInterface(parentNode); + nsCOMPtr<nsIDocument> doc = do_QueryInterface(parentNode); + printf ("Sel. Collapse to %p %s %d\n", parentNode.get(), + content ? nsAtomCString(content->NodeInfo()->NameAtom()).get() + : (doc ? "DOCUMENT" : "???"), + aOffset); +#endif + + int32_t rangeIndex = -1; + result = AddItem(range, &rangeIndex); + if (NS_FAILED(result)) { + aRv.Throw(result); + return; + } + setAnchorFocusRange(0); + selectFrames(presContext, range, true); + result = frameSelection->NotifySelectionListeners(GetType()); + if (NS_FAILED(result)) { + aRv.Throw(result); + } +} + +/* + * Sets the whole selection to be one point + * at the start of the current selection + */ +NS_IMETHODIMP +Selection::CollapseToStart() +{ + ErrorResult result; + CollapseToStart(result); + return result.StealNSResult(); +} + +void +Selection::CollapseToStart(ErrorResult& aRv) +{ + int32_t cnt; + nsresult rv = GetRangeCount(&cnt); + if (NS_FAILED(rv) || cnt <= 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Get the first range + nsRange* firstRange = mRanges[0].mRange; + if (!firstRange) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mFrameSelection) { + int16_t reason = mFrameSelection->PopReason() | nsISelectionListener::COLLAPSETOSTART_REASON; + mFrameSelection->PostReason(reason); + } + nsINode* parent = firstRange->GetStartParent(); + if (!parent) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + Collapse(*parent, firstRange->StartOffset(), aRv); +} + +/* + * Sets the whole selection to be one point + * at the end of the current selection + */ +NS_IMETHODIMP +Selection::CollapseToEnd() +{ + ErrorResult result; + CollapseToEnd(result); + return result.StealNSResult(); +} + +void +Selection::CollapseToEnd(ErrorResult& aRv) +{ + int32_t cnt; + nsresult rv = GetRangeCount(&cnt); + if (NS_FAILED(rv) || cnt <= 0) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + // Get the last range + nsRange* lastRange = mRanges[cnt - 1].mRange; + if (!lastRange) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + if (mFrameSelection) { + int16_t reason = mFrameSelection->PopReason() | nsISelectionListener::COLLAPSETOEND_REASON; + mFrameSelection->PostReason(reason); + } + nsINode* parent = lastRange->GetEndParent(); + if (!parent) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + Collapse(*parent, lastRange->EndOffset(), aRv); +} + +/* + * IsCollapsed -- is the whole selection just one point, or unset? + */ +bool +Selection::IsCollapsed() const +{ + uint32_t cnt = mRanges.Length(); + if (cnt == 0) { + return true; + } + + if (cnt != 1) { + return false; + } + + return mRanges[0].mRange->Collapsed(); +} + +/* virtual */ +bool +Selection::Collapsed() +{ + return IsCollapsed(); +} + +NS_IMETHODIMP +Selection::GetIsCollapsed(bool* aIsCollapsed) +{ + NS_ENSURE_TRUE(aIsCollapsed, NS_ERROR_NULL_POINTER); + + *aIsCollapsed = IsCollapsed(); + return NS_OK; +} + +NS_IMETHODIMP +Selection::GetRangeCount(int32_t* aRangeCount) +{ + *aRangeCount = (int32_t)RangeCount(); + + return NS_OK; +} + +NS_IMETHODIMP +Selection::GetRangeAt(int32_t aIndex, nsIDOMRange** aReturn) +{ + ErrorResult result; + *aReturn = GetRangeAt(aIndex, result); + NS_IF_ADDREF(*aReturn); + return result.StealNSResult(); +} + +nsRange* +Selection::GetRangeAt(uint32_t aIndex, ErrorResult& aRv) +{ + nsRange* range = GetRangeAt(aIndex); + if (!range) { + aRv.Throw(NS_ERROR_DOM_INDEX_SIZE_ERR); + return nullptr; + } + + return range; +} + +nsRange* +Selection::GetRangeAt(int32_t aIndex) const +{ + RangeData empty(nullptr); + return mRanges.SafeElementAt(aIndex, empty).mRange; +} + +/* +utility function +*/ +nsresult +Selection::SetAnchorFocusToRange(nsRange* aRange) +{ + NS_ENSURE_STATE(mAnchorFocusRange); + + bool collapsed = Collapsed(); + + nsresult res = RemoveItem(mAnchorFocusRange); + if (NS_FAILED(res)) + return res; + + int32_t aOutIndex = -1; + res = AddItem(aRange, &aOutIndex, !collapsed); + if (NS_FAILED(res)) + return res; + setAnchorFocusRange(aOutIndex); + + return NS_OK; +} + +void +Selection::ReplaceAnchorFocusRange(nsRange* aRange) +{ + NS_ENSURE_TRUE_VOID(mAnchorFocusRange); + RefPtr<nsPresContext> presContext = GetPresContext(); + if (presContext) { + selectFrames(presContext, mAnchorFocusRange, false); + SetAnchorFocusToRange(aRange); + selectFrames(presContext, mAnchorFocusRange, true); + } +} + +void +Selection::AdjustAnchorFocusForMultiRange(nsDirection aDirection) +{ + if (aDirection == mDirection) { + return; + } + SetDirection(aDirection); + + if (RangeCount() <= 1) { + return; + } + + nsRange* firstRange = GetRangeAt(0); + nsRange* lastRange = GetRangeAt(RangeCount() - 1); + + if (mDirection == eDirPrevious) { + firstRange->SetIsGenerated(false); + lastRange->SetIsGenerated(true); + setAnchorFocusRange(0); + } else { // aDir == eDirNext + firstRange->SetIsGenerated(true); + lastRange->SetIsGenerated(false); + setAnchorFocusRange(RangeCount() - 1); + } +} + +/* +Notes which might come in handy for extend: + +We can tell the direction of the selection by asking for the anchors selection +if the begin is less than the end then we know the selection is to the "right". +else it is a backwards selection. +a = anchor +1 = old cursor +2 = new cursor + + if (a <= 1 && 1 <=2) a,1,2 or (a1,2) + if (a < 2 && 1 > 2) a,2,1 + if (1 < a && a <2) 1,a,2 + if (a > 2 && 2 >1) 1,2,a + if (2 < a && a <1) 2,a,1 + if (a > 1 && 1 >2) 2,1,a +then execute +a 1 2 select from 1 to 2 +a 2 1 deselect from 2 to 1 +1 a 2 deselect from 1 to a select from a to 2 +1 2 a deselect from 1 to 2 +2 1 a = continue selection from 2 to 1 +*/ + + +/* + * Extend extends the selection away from the anchor. + * We don't need to know the direction, because we always change the focus. + */ +NS_IMETHODIMP +Selection::Extend(nsIDOMNode* aParentNode, int32_t aOffset) +{ + nsCOMPtr<nsINode> parentNode = do_QueryInterface(aParentNode); + return Extend(parentNode, aOffset); +} + +NS_IMETHODIMP +Selection::ExtendNative(nsINode* aParentNode, int32_t aOffset) +{ + return Extend(aParentNode, aOffset); +} + +nsresult +Selection::Extend(nsINode* aParentNode, int32_t aOffset) +{ + if (!aParentNode) + return NS_ERROR_INVALID_ARG; + + ErrorResult result; + Extend(*aParentNode, static_cast<uint32_t>(aOffset), result); + return result.StealNSResult(); +} + +void +Selection::Extend(nsINode& aParentNode, uint32_t aOffset, ErrorResult& aRv) +{ + // First, find the range containing the old focus point: + if (!mAnchorFocusRange) { + aRv.Throw(NS_ERROR_DOM_INVALID_STATE_ERR); + return; + } + + if (!mFrameSelection) { + aRv.Throw(NS_ERROR_NOT_INITIALIZED); // Can't do selection + return; + } + + nsresult res; + if (!IsValidSelectionPoint(mFrameSelection, &aParentNode)) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + + RefPtr<nsPresContext> presContext = GetPresContext(); + if (!presContext || presContext->Document() != aParentNode.OwnerDoc()) { + aRv.Throw(NS_ERROR_FAILURE); + return; + } + +#ifdef DEBUG_SELECTION + nsDirection oldDirection = GetDirection(); +#endif + nsINode* anchorNode = GetAnchorNode(); + nsINode* focusNode = GetFocusNode(); + uint32_t anchorOffset = AnchorOffset(); + uint32_t focusOffset = FocusOffset(); + + RefPtr<nsRange> range = mAnchorFocusRange->CloneRange(); + + nsINode* startNode = range->GetStartParent(); + nsINode* endNode = range->GetEndParent(); + int32_t startOffset = range->StartOffset(); + int32_t endOffset = range->EndOffset(); + + //compare anchor to old cursor. + + // We pass |disconnected| to the following ComparePoints calls in order + // to avoid assertions. ComparePoints returns 1 in the disconnected case + // and we can end up in various cases below, but it is assumed that in + // any of the cases we end up, the nsRange implementation will collapse + // the range to the new point because we can not make a valid range with + // a disconnected point. This means that whatever range is currently + // selected will be cleared. + bool disconnected = false; + bool shouldClearRange = false; + int32_t result1 = nsContentUtils::ComparePoints(anchorNode, anchorOffset, + focusNode, focusOffset, + &disconnected); + //compare old cursor to new cursor + shouldClearRange |= disconnected; + int32_t result2 = nsContentUtils::ComparePoints(focusNode, focusOffset, + &aParentNode, aOffset, + &disconnected); + //compare anchor to new cursor + shouldClearRange |= disconnected; + int32_t result3 = nsContentUtils::ComparePoints(anchorNode, anchorOffset, + &aParentNode, aOffset, + &disconnected); + + // If the points are disconnected, the range will be collapsed below, + // resulting in a range that selects nothing. + if (shouldClearRange) { + // Repaint the current range with the selection removed. + selectFrames(presContext, range, false); + } + + RefPtr<nsRange> difRange = new nsRange(&aParentNode); + if ((result1 == 0 && result3 < 0) || (result1 <= 0 && result2 < 0)){//a1,2 a,1,2 + //select from 1 to 2 unless they are collapsed + range->SetEnd(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + SetDirection(eDirNext); + res = difRange->SetEnd(range->GetEndParent(), range->EndOffset()); + nsresult tmp = difRange->SetStart(focusNode, focusOffset); + if (NS_FAILED(tmp)) { + res = tmp; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + selectFrames(presContext, difRange , true); + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + else if (result1 == 0 && result3 > 0){//2, a1 + //select from 2 to 1a + SetDirection(eDirPrevious); + range->SetStart(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + selectFrames(presContext, range, true); + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + else if (result3 <= 0 && result2 >= 0) {//a,2,1 or a2,1 or a,21 or a21 + //deselect from 2 to 1 + res = difRange->SetEnd(focusNode, focusOffset); + difRange->SetStart(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + + range->SetEnd(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + selectFrames(presContext, difRange, false); // deselect now + difRange->SetEnd(range->GetEndParent(), range->EndOffset()); + selectFrames(presContext, difRange, true); // must reselect last node maybe more + } + else if (result1 >= 0 && result3 <= 0) {//1,a,2 or 1a,2 or 1,a2 or 1a2 + if (GetDirection() == eDirPrevious){ + res = range->SetStart(endNode, endOffset); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + SetDirection(eDirNext); + range->SetEnd(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + if (focusNode != anchorNode || focusOffset != anchorOffset) {//if collapsed diff dont do anything + res = difRange->SetStart(focusNode, focusOffset); + nsresult tmp = difRange->SetEnd(anchorNode, anchorOffset); + if (NS_FAILED(tmp)) { + res = tmp; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + //deselect from 1 to a + selectFrames(presContext, difRange , false); + } + else + { + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + //select from a to 2 + selectFrames(presContext, range , true); + } + else if (result2 <= 0 && result3 >= 0) {//1,2,a or 12,a or 1,2a or 12a + //deselect from 1 to 2 + difRange->SetEnd(aParentNode, aOffset, aRv); + res = difRange->SetStart(focusNode, focusOffset); + if (aRv.Failed()) { + return; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + SetDirection(eDirPrevious); + range->SetStart(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + selectFrames(presContext, difRange , false); + difRange->SetStart(range->GetStartParent(), range->StartOffset()); + selectFrames(presContext, difRange, true);//must reselect last node + } + else if (result3 >= 0 && result1 <= 0) {//2,a,1 or 2a,1 or 2,a1 or 2a1 + if (GetDirection() == eDirNext){ + range->SetEnd(startNode, startOffset); + } + SetDirection(eDirPrevious); + range->SetStart(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + //deselect from a to 1 + if (focusNode != anchorNode || focusOffset!= anchorOffset) {//if collapsed diff dont do anything + res = difRange->SetStart(anchorNode, anchorOffset); + nsresult tmp = difRange->SetEnd(focusNode, focusOffset); + if (NS_FAILED(tmp)) { + res = tmp; + } + tmp = SetAnchorFocusToRange(range); + if (NS_FAILED(tmp)) { + res = tmp; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + selectFrames(presContext, difRange, false); + } + else + { + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + //select from 2 to a + selectFrames(presContext, range , true); + } + else if (result2 >= 0 && result1 >= 0) {//2,1,a or 21,a or 2,1a or 21a + //select from 2 to 1 + range->SetStart(aParentNode, aOffset, aRv); + if (aRv.Failed()) { + return; + } + SetDirection(eDirPrevious); + res = difRange->SetEnd(focusNode, focusOffset); + nsresult tmp = difRange->SetStart(range->GetStartParent(), range->StartOffset()); + if (NS_FAILED(tmp)) { + res = tmp; + } + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + + selectFrames(presContext, difRange, true); + res = SetAnchorFocusToRange(range); + if (NS_FAILED(res)) { + aRv.Throw(res); + return; + } + } + + if (mRanges.Length() > 1) { + for (size_t i = 0; i < mRanges.Length(); ++i) { + nsRange* range = mRanges[i].mRange; + MOZ_ASSERT(range->IsInSelection()); + selectFrames(presContext, range, range->IsInSelection()); + } + } + + DEBUG_OUT_RANGE(range); +#ifdef DEBUG_SELECTION + if (GetDirection() != oldDirection) { + printf(" direction changed to %s\n", + GetDirection() == eDirNext? "eDirNext":"eDirPrevious"); + } + nsCOMPtr<nsIContent> content = do_QueryInterface(&aParentNode); + printf ("Sel. Extend to %p %s %d\n", content.get(), + nsAtomCString(content->NodeInfo()->NameAtom()).get(), aOffset); +#endif + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + res = frameSelection->NotifySelectionListeners(GetType()); + if (NS_FAILED(res)) { + aRv.Throw(res); + } +} + +NS_IMETHODIMP +Selection::SelectAllChildren(nsIDOMNode* aParentNode) +{ + ErrorResult result; + nsCOMPtr<nsINode> node = do_QueryInterface(aParentNode); + NS_ENSURE_TRUE(node, NS_ERROR_INVALID_ARG); + SelectAllChildren(*node, result); + return result.StealNSResult(); +} + +void +Selection::SelectAllChildren(nsINode& aNode, ErrorResult& aRv) +{ + if (mFrameSelection) { + mFrameSelection->PostReason(nsISelectionListener::SELECTALL_REASON); + } + SelectionBatcher batch(this); + + Collapse(aNode, 0, aRv); + if (aRv.Failed()) { + return; + } + + Extend(aNode, aNode.GetChildCount(), aRv); +} + +NS_IMETHODIMP +Selection::ContainsNode(nsIDOMNode* aNode, bool aAllowPartial, bool* aYes) +{ + if (!aYes) { + return NS_ERROR_NULL_POINTER; + } + *aYes = false; + + nsCOMPtr<nsINode> node = do_QueryInterface(aNode); + if (!node) { + return NS_ERROR_NULL_POINTER; + } + ErrorResult result; + *aYes = ContainsNode(*node, aAllowPartial, result); + return result.StealNSResult(); +} + +bool +Selection::ContainsNode(nsINode& aNode, bool aAllowPartial, ErrorResult& aRv) +{ + nsresult rv; + if (mRanges.Length() == 0) { + return false; + } + + // XXXbz this duplicates the GetNodeLength code in nsRange.cpp + uint32_t nodeLength; + bool isData = aNode.IsNodeOfType(nsINode::eDATA_NODE); + if (isData) { + nodeLength = static_cast<nsIContent&>(aNode).TextLength(); + } else { + nodeLength = aNode.GetChildCount(); + } + + nsTArray<nsRange*> overlappingRanges; + rv = GetRangesForIntervalArray(&aNode, 0, &aNode, nodeLength, + false, &overlappingRanges); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + return false; + } + if (overlappingRanges.Length() == 0) + return false; // no ranges overlap + + // if the caller said partial intersections are OK, we're done + if (aAllowPartial) { + return true; + } + + // text nodes always count as inside + if (isData) { + return true; + } + + // The caller wants to know if the node is entirely within the given range, + // so we have to check all intersecting ranges. + for (uint32_t i = 0; i < overlappingRanges.Length(); i++) { + bool nodeStartsBeforeRange, nodeEndsAfterRange; + if (NS_SUCCEEDED(nsRange::CompareNodeToRange(&aNode, overlappingRanges[i], + &nodeStartsBeforeRange, + &nodeEndsAfterRange))) { + if (!nodeStartsBeforeRange && !nodeEndsAfterRange) { + return true; + } + } + } + return false; +} + +class PointInRectChecker : public nsLayoutUtils::RectCallback { +public: + explicit PointInRectChecker(const nsPoint& aPoint) + : mPoint(aPoint) + , mMatchFound(false) + { + } + + void AddRect(const nsRect& aRect) override + { + mMatchFound = mMatchFound || aRect.Contains(mPoint); + } + + bool MatchFound() + { + return mMatchFound; + } + +private: + nsPoint mPoint; + bool mMatchFound; +}; + +bool +Selection::ContainsPoint(const nsPoint& aPoint) +{ + if (IsCollapsed()) { + return false; + } + PointInRectChecker checker(aPoint); + for (uint32_t i = 0; i < RangeCount(); i++) { + nsRange* range = GetRangeAt(i); + nsRange::CollectClientRectsAndText(&checker, nullptr, range, + range->GetStartParent(), range->StartOffset(), + range->GetEndParent(), range->EndOffset(), + true, false); + if (checker.MatchFound()) { + return true; + } + } + return false; +} + +nsPresContext* +Selection::GetPresContext() const +{ + nsIPresShell *shell = GetPresShell(); + if (!shell) { + return nullptr; + } + + return shell->GetPresContext(); +} + +nsIPresShell* +Selection::GetPresShell() const +{ + if (!mFrameSelection) + return nullptr;//nothing to do + + return mFrameSelection->GetShell(); +} + +nsIFrame * +Selection::GetSelectionAnchorGeometry(SelectionRegion aRegion, nsRect* aRect) +{ + if (!mFrameSelection) + return nullptr; // nothing to do + + NS_ENSURE_TRUE(aRect, nullptr); + + aRect->SetRect(0, 0, 0, 0); + + switch (aRegion) { + case nsISelectionController::SELECTION_ANCHOR_REGION: + case nsISelectionController::SELECTION_FOCUS_REGION: + return GetSelectionEndPointGeometry(aRegion, aRect); + case nsISelectionController::SELECTION_WHOLE_SELECTION: + break; + default: + return nullptr; + } + + NS_ASSERTION(aRegion == nsISelectionController::SELECTION_WHOLE_SELECTION, + "should only be SELECTION_WHOLE_SELECTION here"); + + nsRect anchorRect; + nsIFrame* anchorFrame = GetSelectionEndPointGeometry( + nsISelectionController::SELECTION_ANCHOR_REGION, &anchorRect); + if (!anchorFrame) + return nullptr; + + nsRect focusRect; + nsIFrame* focusFrame = GetSelectionEndPointGeometry( + nsISelectionController::SELECTION_FOCUS_REGION, &focusRect); + if (!focusFrame) + return nullptr; + + NS_ASSERTION(anchorFrame->PresContext() == focusFrame->PresContext(), + "points of selection in different documents?"); + // make focusRect relative to anchorFrame + focusRect += focusFrame->GetOffsetTo(anchorFrame); + + aRect->UnionRectEdges(anchorRect, focusRect); + return anchorFrame; +} + +nsIFrame * +Selection::GetSelectionEndPointGeometry(SelectionRegion aRegion, nsRect* aRect) +{ + if (!mFrameSelection) + return nullptr; // nothing to do + + NS_ENSURE_TRUE(aRect, nullptr); + + aRect->SetRect(0, 0, 0, 0); + + nsINode *node = nullptr; + uint32_t nodeOffset = 0; + nsIFrame *frame = nullptr; + + switch (aRegion) { + case nsISelectionController::SELECTION_ANCHOR_REGION: + node = GetAnchorNode(); + nodeOffset = AnchorOffset(); + break; + case nsISelectionController::SELECTION_FOCUS_REGION: + node = GetFocusNode(); + nodeOffset = FocusOffset(); + break; + default: + return nullptr; + } + + if (!node) + return nullptr; + + nsCOMPtr<nsIContent> content = do_QueryInterface(node); + NS_ENSURE_TRUE(content.get(), nullptr); + int32_t frameOffset = 0; + frame = mFrameSelection->GetFrameForNodeOffset(content, nodeOffset, + mFrameSelection->GetHint(), + &frameOffset); + if (!frame) + return nullptr; + + // Figure out what node type we have, then get the + // appropriate rect for it's nodeOffset. + bool isText = node->IsNodeOfType(nsINode::eTEXT); + + nsPoint pt(0, 0); + if (isText) { + nsIFrame* childFrame = nullptr; + frameOffset = 0; + nsresult rv = + frame->GetChildFrameContainingOffset(nodeOffset, + mFrameSelection->GetHint(), + &frameOffset, &childFrame); + if (NS_FAILED(rv)) + return nullptr; + if (!childFrame) + return nullptr; + + frame = childFrame; + + // Get the x coordinate of the offset into the text frame. + rv = GetCachedFrameOffset(frame, nodeOffset, pt); + if (NS_FAILED(rv)) + return nullptr; + } + + // Return the rect relative to the frame, with zero width. + if (isText) { + aRect->x = pt.x; + } else if (mFrameSelection->GetHint() == CARET_ASSOCIATE_BEFORE) { + // It's the frame's right edge we're interested in. + aRect->x = frame->GetRect().width; + } + aRect->height = frame->GetRect().height; + + return frame; +} + +NS_IMETHODIMP +Selection::ScrollSelectionIntoViewEvent::Run() +{ + if (!mSelection) + return NS_OK; // event revoked + + int32_t flags = Selection::SCROLL_DO_FLUSH | + Selection::SCROLL_SYNCHRONOUS; + + Selection* sel = mSelection; // workaround to satisfy static analysis + RefPtr<Selection> kungFuDeathGrip(sel); + mSelection->mScrollEvent.Forget(); + mSelection->ScrollIntoView(mRegion, mVerticalScroll, + mHorizontalScroll, mFlags | flags); + return NS_OK; +} + +nsresult +Selection::PostScrollSelectionIntoViewEvent( + SelectionRegion aRegion, + int32_t aFlags, + nsIPresShell::ScrollAxis aVertical, + nsIPresShell::ScrollAxis aHorizontal) +{ + // If we've already posted an event, revoke it and place a new one at the + // end of the queue to make sure that any new pending reflow events are + // processed before we scroll. This will insure that we scroll to the + // correct place on screen. + mScrollEvent.Revoke(); + + RefPtr<ScrollSelectionIntoViewEvent> ev = + new ScrollSelectionIntoViewEvent(this, aRegion, aVertical, aHorizontal, + aFlags); + nsresult rv = NS_DispatchToCurrentThread(ev); + NS_ENSURE_SUCCESS(rv, rv); + + mScrollEvent = ev; + return NS_OK; +} + +NS_IMETHODIMP +Selection::ScrollIntoView(SelectionRegion aRegion, bool aIsSynchronous, + int16_t aVPercent, int16_t aHPercent) +{ + ErrorResult result; + ScrollIntoView(aRegion, aIsSynchronous, aVPercent, aHPercent, result); + if (result.Failed()) { + return result.StealNSResult(); + } + return NS_OK; +} + +void +Selection::ScrollIntoView(int16_t aRegion, bool aIsSynchronous, + int16_t aVPercent, int16_t aHPercent, + ErrorResult& aRv) +{ + nsresult rv = ScrollIntoViewInternal(aRegion, aIsSynchronous, + nsIPresShell::ScrollAxis(aVPercent), + nsIPresShell::ScrollAxis(aHPercent)); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +NS_IMETHODIMP +Selection::ScrollIntoViewInternal(SelectionRegion aRegion, bool aIsSynchronous, + nsIPresShell::ScrollAxis aVertical, + nsIPresShell::ScrollAxis aHorizontal) +{ + return ScrollIntoView(aRegion, aVertical, aHorizontal, + aIsSynchronous ? Selection::SCROLL_SYNCHRONOUS : 0); +} + +nsresult +Selection::ScrollIntoView(SelectionRegion aRegion, + nsIPresShell::ScrollAxis aVertical, + nsIPresShell::ScrollAxis aHorizontal, + int32_t aFlags) +{ + if (!mFrameSelection) + return NS_OK;//nothing to do + + nsCOMPtr<nsIPresShell> presShell = mFrameSelection->GetShell(); + if (!presShell) + return NS_OK; + + if (mFrameSelection->GetBatching()) + return NS_OK; + + if (!(aFlags & Selection::SCROLL_SYNCHRONOUS)) + return PostScrollSelectionIntoViewEvent(aRegion, aFlags, + aVertical, aHorizontal); + + // Now that text frame character offsets are always valid (though not + // necessarily correct), the worst that will happen if we don't flush here + // is that some callers might scroll to the wrong place. Those should + // either manually flush if they're in a safe position for it or use the + // async version of this method. + if (aFlags & Selection::SCROLL_DO_FLUSH) { + presShell->FlushPendingNotifications(Flush_Layout); + + // Reget the presshell, since it might have been Destroy'ed. + presShell = mFrameSelection ? mFrameSelection->GetShell() : nullptr; + if (!presShell) + return NS_OK; + } + + // + // Scroll the selection region into view. + // + + nsRect rect; + nsIFrame* frame = GetSelectionAnchorGeometry(aRegion, &rect); + if (!frame) + return NS_ERROR_FAILURE; + + // Scroll vertically to get the caret into view, but only if the container + // is perceived to be scrollable in that direction (i.e. there is a visible + // vertical scrollbar or the scroll range is at least one device pixel) + aVertical.mOnlyIfPerceivedScrollableDirection = true; + + uint32_t flags = 0; + if (aFlags & Selection::SCROLL_FIRST_ANCESTOR_ONLY) { + flags |= nsIPresShell::SCROLL_FIRST_ANCESTOR_ONLY; + } + if (aFlags & Selection::SCROLL_OVERFLOW_HIDDEN) { + flags |= nsIPresShell::SCROLL_OVERFLOW_HIDDEN; + } + + if (aFlags & Selection::SCROLL_FOR_CARET_MOVE) { + mozilla::Telemetry::Accumulate(mozilla::Telemetry::SCROLL_INPUT_METHODS, + (uint32_t) ScrollInputMethod::MainThreadScrollCaretIntoView); + } + + presShell->ScrollFrameRectIntoView(frame, rect, aVertical, aHorizontal, + flags); + return NS_OK; +} + +NS_IMETHODIMP +Selection::AddSelectionListener(nsISelectionListener* aNewListener) +{ + if (!aNewListener) + return NS_ERROR_NULL_POINTER; + ErrorResult result; + AddSelectionListener(aNewListener, result); + if (result.Failed()) { + return result.StealNSResult(); + } + return NS_OK; +} + +void +Selection::AddSelectionListener(nsISelectionListener* aNewListener, + ErrorResult& aRv) +{ + bool result = mSelectionListeners.AppendObject(aNewListener); // AddRefs + if (!result) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +NS_IMETHODIMP +Selection::RemoveSelectionListener(nsISelectionListener* aListenerToRemove) +{ + if (!aListenerToRemove) + return NS_ERROR_NULL_POINTER; + ErrorResult result; + RemoveSelectionListener(aListenerToRemove, result); + if (result.Failed()) { + return result.StealNSResult(); + } + return NS_OK; +} + +void +Selection::RemoveSelectionListener(nsISelectionListener* aListenerToRemove, + ErrorResult& aRv) +{ + bool result = mSelectionListeners.RemoveObject(aListenerToRemove); // Releases + if (!result) { + aRv.Throw(NS_ERROR_FAILURE); + } +} + +nsresult +Selection::NotifySelectionListeners() +{ + if (!mFrameSelection) + return NS_OK;//nothing to do + + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + if (frameSelection->GetBatching()) { + frameSelection->SetDirty(); + return NS_OK; + } + nsCOMArray<nsISelectionListener> selectionListeners(mSelectionListeners); + int32_t cnt = selectionListeners.Count(); + if (cnt != mSelectionListeners.Count()) { + return NS_ERROR_OUT_OF_MEMORY; // nsCOMArray is fallible + } + + nsCOMPtr<nsIDOMDocument> domdoc; + nsIPresShell* ps = GetPresShell(); + if (ps) { + domdoc = do_QueryInterface(ps->GetDocument()); + } + + short reason = frameSelection->PopReason(); + for (int32_t i = 0; i < cnt; i++) { + selectionListeners[i]->NotifySelectionChanged(domdoc, this, reason); + } + return NS_OK; +} + +NS_IMETHODIMP +Selection::StartBatchChanges() +{ + if (mFrameSelection) { + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + frameSelection->StartBatchChanges(); + } + return NS_OK; +} + + + +NS_IMETHODIMP +Selection::EndBatchChanges() +{ + return EndBatchChangesInternal(); +} + +nsresult +Selection::EndBatchChangesInternal(int16_t aReason) +{ + if (mFrameSelection) { + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + frameSelection->EndBatchChanges(aReason); + } + return NS_OK; +} + +void +Selection::AddSelectionChangeBlocker() +{ + mSelectionChangeBlockerCount++; +} + +void +Selection::RemoveSelectionChangeBlocker() +{ + MOZ_ASSERT(mSelectionChangeBlockerCount > 0, + "mSelectionChangeBlockerCount has an invalid value - " + "maybe you have a mismatched RemoveSelectionChangeBlocker?"); + mSelectionChangeBlockerCount--; +} + +bool +Selection::IsBlockingSelectionChangeEvents() const +{ + return mSelectionChangeBlockerCount > 0; +} + +NS_IMETHODIMP +Selection::DeleteFromDocument() +{ + ErrorResult result; + DeleteFromDocument(result); + return result.StealNSResult(); +} + +void +Selection::DeleteFromDocument(ErrorResult& aRv) +{ + if (!mFrameSelection) + return;//nothing to do + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + nsresult rv = frameSelection->DeleteFromDocument(); + if (NS_FAILED(rv)) { + aRv.Throw(rv); + } +} + +NS_IMETHODIMP +Selection::Modify(const nsAString& aAlter, const nsAString& aDirection, + const nsAString& aGranularity) +{ + ErrorResult result; + Modify(aAlter, aDirection, aGranularity, result); + return result.StealNSResult(); +} + +void +Selection::Modify(const nsAString& aAlter, const nsAString& aDirection, + const nsAString& aGranularity, ErrorResult& aRv) +{ + // Silently exit if there's no selection or no focus node. + if (!mFrameSelection || !GetAnchorFocusRange() || !GetFocusNode()) { + return; + } + + if (!aAlter.LowerCaseEqualsLiteral("move") && + !aAlter.LowerCaseEqualsLiteral("extend")) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return; + } + + if (!aDirection.LowerCaseEqualsLiteral("forward") && + !aDirection.LowerCaseEqualsLiteral("backward") && + !aDirection.LowerCaseEqualsLiteral("left") && + !aDirection.LowerCaseEqualsLiteral("right")) { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return; + } + + // Line moves are always visual. + bool visual = aDirection.LowerCaseEqualsLiteral("left") || + aDirection.LowerCaseEqualsLiteral("right") || + aGranularity.LowerCaseEqualsLiteral("line"); + + bool forward = aDirection.LowerCaseEqualsLiteral("forward") || + aDirection.LowerCaseEqualsLiteral("right"); + + bool extend = aAlter.LowerCaseEqualsLiteral("extend"); + + nsSelectionAmount amount; + if (aGranularity.LowerCaseEqualsLiteral("character")) { + amount = eSelectCluster; + } else if (aGranularity.LowerCaseEqualsLiteral("word")) { + amount = eSelectWordNoSpace; + } else if (aGranularity.LowerCaseEqualsLiteral("line")) { + amount = eSelectLine; + } else if (aGranularity.LowerCaseEqualsLiteral("lineboundary")) { + amount = forward ? eSelectEndLine : eSelectBeginLine; + } else if (aGranularity.LowerCaseEqualsLiteral("sentence") || + aGranularity.LowerCaseEqualsLiteral("sentenceboundary") || + aGranularity.LowerCaseEqualsLiteral("paragraph") || + aGranularity.LowerCaseEqualsLiteral("paragraphboundary") || + aGranularity.LowerCaseEqualsLiteral("documentboundary")) { + aRv.Throw(NS_ERROR_NOT_IMPLEMENTED); + return; + } else { + aRv.Throw(NS_ERROR_DOM_SYNTAX_ERR); + return; + } + + // If the anchor doesn't equal the focus and we try to move without first + // collapsing the selection, MoveCaret will collapse the selection and quit. + // To avoid this, we need to collapse the selection first. + nsresult rv = NS_OK; + if (!extend) { + nsINode* focusNode = GetFocusNode(); + // We should have checked earlier that there was a focus node. + if (!focusNode) { + aRv.Throw(NS_ERROR_UNEXPECTED); + return; + } + uint32_t focusOffset = FocusOffset(); + Collapse(focusNode, focusOffset); + } + + // If the paragraph direction of the focused frame is right-to-left, + // we may have to swap the direction of movement. + nsIFrame *frame; + int32_t offset; + rv = GetPrimaryFrameForFocusNode(&frame, &offset, visual); + if (NS_SUCCEEDED(rv) && frame) { + nsBidiDirection paraDir = nsBidiPresUtils::ParagraphDirection(frame); + + if (paraDir == NSBIDI_RTL && visual) { + if (amount == eSelectBeginLine) { + amount = eSelectEndLine; + forward = !forward; + } else if (amount == eSelectEndLine) { + amount = eSelectBeginLine; + forward = !forward; + } + } + } + + // MoveCaret will return an error if it can't move in the specified + // direction, but we just ignore this error unless it's a line move, in which + // case we call nsISelectionController::CompleteMove to move the cursor to + // the beginning/end of the line. + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + rv = frameSelection->MoveCaret(forward ? eDirNext : eDirPrevious, + extend, amount, + visual ? nsFrameSelection::eVisual + : nsFrameSelection::eLogical); + + if (aGranularity.LowerCaseEqualsLiteral("line") && NS_FAILED(rv)) { + nsCOMPtr<nsISelectionController> shell = + do_QueryInterface(frameSelection->GetShell()); + if (!shell) + return; + shell->CompleteMove(forward, extend); + } +} + +/** SelectionLanguageChange modifies the cursor Bidi level after a change in keyboard direction + * @param aLangRTL is true if the new language is right-to-left or false if the new language is left-to-right + */ +NS_IMETHODIMP +Selection::SelectionLanguageChange(bool aLangRTL) +{ + if (!mFrameSelection) + return NS_ERROR_NOT_INITIALIZED; // Can't do selection + + RefPtr<nsFrameSelection> frameSelection = mFrameSelection; + + // if the direction of the language hasn't changed, nothing to do + nsBidiLevel kbdBidiLevel = aLangRTL ? NSBIDI_RTL : NSBIDI_LTR; + if (kbdBidiLevel == frameSelection->mKbdBidiLevel) { + return NS_OK; + } + + frameSelection->mKbdBidiLevel = kbdBidiLevel; + + nsresult result; + nsIFrame *focusFrame = 0; + + result = GetPrimaryFrameForFocusNode(&focusFrame, nullptr, false); + if (NS_FAILED(result)) { + return result; + } + if (!focusFrame) { + return NS_ERROR_FAILURE; + } + + int32_t frameStart, frameEnd; + focusFrame->GetOffsets(frameStart, frameEnd); + RefPtr<nsPresContext> context = GetPresContext(); + nsBidiLevel levelBefore, levelAfter; + if (!context) { + return NS_ERROR_FAILURE; + } + + nsBidiLevel level = focusFrame->GetEmbeddingLevel(); + int32_t focusOffset = static_cast<int32_t>(FocusOffset()); + if ((focusOffset != frameStart) && (focusOffset != frameEnd)) + // the cursor is not at a frame boundary, so the level of both the characters (logically) before and after the cursor + // is equal to the frame level + levelBefore = levelAfter = level; + else { + // the cursor is at a frame boundary, so use GetPrevNextBidiLevels to find the level of the characters + // before and after the cursor + nsCOMPtr<nsIContent> focusContent = do_QueryInterface(GetFocusNode()); + nsPrevNextBidiLevels levels = frameSelection-> + GetPrevNextBidiLevels(focusContent, focusOffset, false); + + levelBefore = levels.mLevelBefore; + levelAfter = levels.mLevelAfter; + } + + if (IS_SAME_DIRECTION(levelBefore, levelAfter)) { + // if cursor is between two characters with the same orientation, changing the keyboard language + // must toggle the cursor level between the level of the character with the lowest level + // (if the new language corresponds to the orientation of that character) and this level plus 1 + // (if the new language corresponds to the opposite orientation) + if ((level != levelBefore) && (level != levelAfter)) + level = std::min(levelBefore, levelAfter); + if (IS_SAME_DIRECTION(level, kbdBidiLevel)) + frameSelection->SetCaretBidiLevel(level); + else + frameSelection->SetCaretBidiLevel(level + 1); + } + else { + // if cursor is between characters with opposite orientations, changing the keyboard language must change + // the cursor level to that of the adjacent character with the orientation corresponding to the new language. + if (IS_SAME_DIRECTION(levelBefore, kbdBidiLevel)) + frameSelection->SetCaretBidiLevel(levelBefore); + else + frameSelection->SetCaretBidiLevel(levelAfter); + } + + // The caret might have moved, so invalidate the desired position + // for future usages of up-arrow or down-arrow + frameSelection->InvalidateDesiredPos(); + + return NS_OK; +} + +NS_IMETHODIMP_(nsDirection) +Selection::GetSelectionDirection() { + return mDirection; +} + +NS_IMETHODIMP_(void) +Selection::SetSelectionDirection(nsDirection aDirection) { + mDirection = aDirection; +} + +JSObject* +Selection::WrapObject(JSContext* aCx, JS::Handle<JSObject*> aGivenProto) +{ + return mozilla::dom::SelectionBinding::Wrap(aCx, this, aGivenProto); +} + +// AutoHideSelectionChanges +AutoHideSelectionChanges::AutoHideSelectionChanges(const nsFrameSelection* aFrame) + : AutoHideSelectionChanges( + aFrame ? aFrame->GetSelection(SelectionType::eNormal) : nullptr) +{} + +// nsAutoCopyListener + +nsAutoCopyListener* nsAutoCopyListener::sInstance = nullptr; + +NS_IMPL_ISUPPORTS(nsAutoCopyListener, nsISelectionListener) + +/* + * What we do now: + * On every selection change, we copy to the clipboard anew, creating a + * HTML buffer, a transferable, an nsISupportsString and + * a huge mess every time. This is basically what nsPresShell::DoCopy does + * to move the selection into the clipboard for Edit->Copy. + * + * What we should do, to make our end of the deal faster: + * Create a singleton transferable with our own magic converter. When selection + * changes (use a quick cache to detect ``real'' changes), we put the new + * nsISelection in the transferable. Our magic converter will take care of + * transferable->whatever-other-format when the time comes to actually + * hand over the clipboard contents. + * + * Other issues: + * - which X clipboard should we populate? + * - should we use a different one than Edit->Copy, so that inadvertant + * selections (or simple clicks, which currently cause a selection + * notification, regardless of if they're in the document which currently has + * selection!) don't lose the contents of the ``application''? Or should we + * just put some intelligence in the ``is this a real selection?'' code to + * protect our selection against clicks in other documents that don't create + * selections? + * - maybe we should just never clear the X clipboard? That would make this + * problem just go away, which is very tempting. + * + * On macOS, + * nsIClipboard::kSelectionCache is the flag for current selection cache. + * Set the current selection cache on the parent process in + * widget cocoa nsClipboard whenever selection changes. + */ + +NS_IMETHODIMP +nsAutoCopyListener::NotifySelectionChanged(nsIDOMDocument *aDoc, + nsISelection *aSel, int16_t aReason) +{ + if (mCachedClipboard == nsIClipboard::kSelectionCache) { + nsFocusManager* fm = nsFocusManager::GetFocusManager(); + // If no active window, do nothing because a current selection changed + // cannot occur unless it is in the active window. + if (!fm->GetActiveWindow()) { + return NS_OK; + } + } + + if (!(aReason & nsISelectionListener::MOUSEUP_REASON || + aReason & nsISelectionListener::SELECTALL_REASON || + aReason & nsISelectionListener::KEYPRESS_REASON)) + return NS_OK; //dont care if we are still dragging + + bool collapsed; + if (!aDoc || !aSel || + NS_FAILED(aSel->GetIsCollapsed(&collapsed)) || collapsed) { +#ifdef DEBUG_CLIPBOARD + fprintf(stderr, "CLIPBOARD: no selection/collapsed selection\n"); +#endif + // If on macOS, clear the current selection transferable cached + // on the parent process (nsClipboard) when the selection is empty. + if (mCachedClipboard == nsIClipboard::kSelectionCache) { + return nsCopySupport::ClearSelectionCache(); + } + /* clear X clipboard? */ + return NS_OK; + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc); + NS_ENSURE_TRUE(doc, NS_ERROR_FAILURE); + + // call the copy code + return nsCopySupport::HTMLCopy(aSel, doc, + mCachedClipboard, false); +} + +/** + * See Bug 1288453. + * + * Update the selection cache on repaint to handle when a pre-existing + * selection becomes active aka the current selection. + * + * 1. Change the current selection by click n dragging another selection. + * - Make a selection on content page. Make a selection in a text editor. + * - You can click n drag the content selection to make it active again. + * 2. Change the current selection when switching to a tab with a selection. + * - Make selection in tab. + * - Switching tabs will make its respective selection active. + * + * Therefore, we only update the selection cache on a repaint + * if the current selection being repainted is not an empty selection. + * + * If the current selection is empty. The current selection cache + * would be cleared by nsAutoCopyListener::NotifySelectionChanged. + */ +nsresult +nsFrameSelection::UpdateSelectionCacheOnRepaintSelection(Selection* aSel) +{ + nsIPresShell* ps = aSel->GetPresShell(); + if (!ps) { + return NS_OK; + } + nsCOMPtr<nsIDocument> aDoc = ps->GetDocument(); + + bool collapsed; + if (aDoc && aSel && + NS_SUCCEEDED(aSel->GetIsCollapsed(&collapsed)) && !collapsed) { + return nsCopySupport::HTMLCopy(aSel, aDoc, + nsIClipboard::kSelectionCache, false); + } + + return NS_OK; +} + +// SelectionChangeListener + +SelectionChangeListener::RawRangeData::RawRangeData(const nsRange* aRange) +{ + mozilla::ErrorResult rv; + mStartParent = aRange->GetStartContainer(rv); + rv.SuppressException(); + mEndParent = aRange->GetEndContainer(rv); + rv.SuppressException(); + mStartOffset = aRange->GetStartOffset(rv); + rv.SuppressException(); + mEndOffset = aRange->GetEndOffset(rv); + rv.SuppressException(); +} + +bool +SelectionChangeListener::RawRangeData::Equals(const nsRange* aRange) +{ + mozilla::ErrorResult rv; + bool eq = mStartParent == aRange->GetStartContainer(rv); + rv.SuppressException(); + eq = eq && mEndParent == aRange->GetEndContainer(rv); + rv.SuppressException(); + eq = eq && mStartOffset == aRange->GetStartOffset(rv); + rv.SuppressException(); + eq = eq && mEndOffset == aRange->GetEndOffset(rv); + rv.SuppressException(); + return eq; +} + +inline void +ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback& aCallback, + SelectionChangeListener::RawRangeData& aField, + const char* aName, + uint32_t aFlags = 0) +{ + ImplCycleCollectionTraverse(aCallback, aField.mStartParent, "mStartParent", aFlags); + ImplCycleCollectionTraverse(aCallback, aField.mEndParent, "mEndParent", aFlags); +} + +NS_IMPL_CYCLE_COLLECTION_CLASS(SelectionChangeListener) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(SelectionChangeListener) + tmp->mOldRanges.Clear(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(SelectionChangeListener) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mOldRanges); +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(SelectionChangeListener) + NS_INTERFACE_MAP_ENTRY(nsISupports) + NS_INTERFACE_MAP_ENTRY(nsISelectionListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(SelectionChangeListener) +NS_IMPL_CYCLE_COLLECTING_RELEASE(SelectionChangeListener) + +NS_IMETHODIMP +SelectionChangeListener::NotifySelectionChanged(nsIDOMDocument* aDoc, + nsISelection* aSel, int16_t aReason) +{ + RefPtr<Selection> sel = aSel->AsSelection(); + + nsIDocument* doc = sel->GetParentObject(); + if (!(doc && nsContentUtils::IsSystemPrincipal(doc->NodePrincipal())) && + !nsFrameSelection::sSelectionEventsEnabled) { + return NS_OK; + } + + // Check if the ranges have actually changed + // Don't bother checking this if we are hiding changes. + if (mOldRanges.Length() == sel->RangeCount() && !sel->IsBlockingSelectionChangeEvents()) { + bool changed = false; + + for (size_t i = 0; i < mOldRanges.Length(); i++) { + if (!mOldRanges[i].Equals(sel->GetRangeAt(i))) { + changed = true; + break; + } + } + + if (!changed) { + return NS_OK; + } + } + + // The ranges have actually changed, update the mOldRanges array + mOldRanges.ClearAndRetainStorage(); + for (size_t i = 0; i < sel->RangeCount(); i++) { + mOldRanges.AppendElement(RawRangeData(sel->GetRangeAt(i))); + } + + // If we are hiding changes, then don't do anything else. We do this after we + // update mOldRanges so that changes after the changes stop being hidden don't + // incorrectly trigger a change, even though they didn't change anything + if (sel->IsBlockingSelectionChangeEvents()) { + return NS_OK; + } + + // The spec currently doesn't say that we should dispatch this event on text + // controls, so for now we only support doing that under a pref, disabled by + // default. + // See https://github.com/w3c/selection-api/issues/53. + if (nsFrameSelection::sSelectionEventsOnTextControlsEnabled) { + nsCOMPtr<nsINode> target; + + // Check if we should be firing this event to a different node than the + // document. The limiter of the nsFrameSelection will be within the native + // anonymous subtree of the node we want to fire the event on. We need to + // climb up the parent chain to escape the native anonymous subtree, and then + // fire the event. + if (const nsFrameSelection* fs = sel->GetFrameSelection()) { + if (nsCOMPtr<nsIContent> root = fs->GetLimiter()) { + while (root && root->IsInNativeAnonymousSubtree()) { + root = root->GetParent(); + } + + target = root.forget(); + } + } + + // If we didn't get a target before, we can instead fire the event at the document. + if (!target) { + nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc); + target = doc.forget(); + } + + if (target) { + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(target, NS_LITERAL_STRING("selectionchange"), false); + asyncDispatcher->PostDOMEvent(); + } + } else { + if (const nsFrameSelection* fs = sel->GetFrameSelection()) { + if (nsCOMPtr<nsIContent> root = fs->GetLimiter()) { + if (root->IsInNativeAnonymousSubtree()) { + return NS_OK; + } + } + } + + nsCOMPtr<nsIDocument> doc = do_QueryInterface(aDoc); + if (doc) { + RefPtr<AsyncEventDispatcher> asyncDispatcher = + new AsyncEventDispatcher(doc, NS_LITERAL_STRING("selectionchange"), false); + asyncDispatcher->PostDOMEvent(); + } + } + + return NS_OK; +} |