/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* 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/. */ #ifndef mozilla_SelectionState_h #define mozilla_SelectionState_h #include "mozilla/EditorDOMPoint.h" #include "mozilla/OwningNonNull.h" #include "nsCOMPtr.h" #include "nsDirection.h" #include "nsINode.h" #include "nsTArray.h" #include "nscore.h" class nsCycleCollectionTraversalCallback; class nsRange; namespace mozilla { class RangeUpdater; namespace dom { class Element; class Selection; class Text; } // namespace dom /** * A helper struct for saving/setting ranges. */ struct RangeItem final { RangeItem() : mStartOffset(0), mEndOffset(0) {} private: // Private destructor, to discourage deletion outside of Release(): ~RangeItem() = default; public: void StoreRange(nsRange& aRange); void StoreRange(const EditorRawDOMPoint& aStartPoint, const EditorRawDOMPoint& aEndPoint) { MOZ_ASSERT(aStartPoint.IsSet()); MOZ_ASSERT(aEndPoint.IsSet()); mStartContainer = aStartPoint.GetContainer(); mStartOffset = aStartPoint.Offset(); mEndContainer = aEndPoint.GetContainer(); mEndOffset = aEndPoint.Offset(); } void Clear() { mStartContainer = mEndContainer = nullptr; mStartOffset = mEndOffset = 0; } already_AddRefed GetRange(); bool IsCollapsed() const { return mStartContainer == mEndContainer && mStartOffset == mEndOffset; } bool IsSet() const { return mStartContainer && mEndContainer; } EditorDOMPoint StartPoint() const { return EditorDOMPoint(mStartContainer, mStartOffset); } EditorDOMPoint EndPoint() const { return EditorDOMPoint(mEndContainer, mEndOffset); } EditorRawDOMPoint StartRawPoint() const { return EditorRawDOMPoint(mStartContainer, mStartOffset); } EditorRawDOMPoint EndRawPoint() const { return EditorRawDOMPoint(mEndContainer, mEndOffset); } NS_INLINE_DECL_MAIN_THREAD_ONLY_CYCLE_COLLECTING_NATIVE_REFCOUNTING(RangeItem) NS_DECL_CYCLE_COLLECTION_NATIVE_CLASS(RangeItem) nsCOMPtr mStartContainer; nsCOMPtr mEndContainer; int32_t mStartOffset; // TODO: Change this to uint32_t int32_t mEndOffset; // TODO: Change this to uint32_t }; /** * mozilla::SelectionState * * Class for recording selection info. Stores selection as collection of * { {startnode, startoffset} , {endnode, endoffset} } tuples. Can't store * ranges since dom gravity will possibly change the ranges. */ class SelectionState final { public: SelectionState(); ~SelectionState() { Clear(); } void SaveSelection(dom::Selection& aSelection); MOZ_CAN_RUN_SCRIPT_BOUNDARY nsresult RestoreSelection(dom::Selection& aSelection); bool IsCollapsed() const; bool Equals(SelectionState& aOther) const; void Clear(); bool IsEmpty() const; private: AutoTArray, 1> mArray; nsDirection mDirection; friend class RangeUpdater; friend void ImplCycleCollectionTraverse(nsCycleCollectionTraversalCallback&, SelectionState&, const char*, uint32_t); friend void ImplCycleCollectionUnlink(SelectionState&); }; inline void ImplCycleCollectionTraverse( nsCycleCollectionTraversalCallback& aCallback, SelectionState& aField, const char* aName, uint32_t aFlags = 0) { ImplCycleCollectionTraverse(aCallback, aField.mArray, aName, aFlags); } inline void ImplCycleCollectionUnlink(SelectionState& aField) { ImplCycleCollectionUnlink(aField.mArray); } class MOZ_STACK_CLASS RangeUpdater final { public: RangeUpdater(); void RegisterRangeItem(RangeItem& aRangeItem); void DropRangeItem(RangeItem& aRangeItem); void RegisterSelectionState(SelectionState& aSelectionState); void DropSelectionState(SelectionState& aSelectionState); // editor selection gravity routines. Note that we can't always depend on // DOM Range gravity to do what we want to the "real" selection. For // instance, if you move a node, that corresponds to deleting it and // reinserting it. DOM Range gravity will promote the selection out of the // node on deletion, which is not what you want if you know you are // reinserting it. template nsresult SelAdjCreateNode(const EditorDOMPointBase& aPoint); template nsresult SelAdjInsertNode(const EditorDOMPointBase& aPoint); void SelAdjDeleteNode(nsINode& aNode); nsresult SelAdjSplitNode(nsIContent& aRightNode, nsIContent& aNewLeftNode); nsresult SelAdjJoinNodes(nsINode& aLeftNode, nsINode& aRightNode, nsINode& aParent, int32_t aOffset, int32_t aOldLeftNodeLength); void SelAdjInsertText(const dom::Text& aTextNode, int32_t aOffset, const nsAString& aString); nsresult SelAdjDeleteText(const dom::Text& aTextNode, int32_t aOffset, int32_t aLength); // the following gravity routines need will/did sandwiches, because the other // gravity routines will be called inside of these sandwiches, but should be // ignored. void WillReplaceContainer() { // XXX Isn't this possible with mutation event listener? NS_WARNING_ASSERTION(!mLocked, "Has already been locked"); mLocked = true; } void DidReplaceContainer(const dom::Element& aRemovedElement, dom::Element& aInsertedElement); void WillRemoveContainer() { // XXX Isn't this possible with mutation event listener? NS_WARNING_ASSERTION(!mLocked, "Has already been locked"); mLocked = true; } void DidRemoveContainer(const dom::Element& aRemovedElement, nsINode& aRemovedElementContainerNode, uint32_t aOldOffsetOfRemovedElement, uint32_t aOldChildCountOfRemovedElement); void WillInsertContainer() { // XXX Isn't this possible with mutation event listener? NS_WARNING_ASSERTION(!mLocked, "Has already been locked"); mLocked = true; } void DidInsertContainer() { NS_WARNING_ASSERTION(mLocked, "Not locked"); mLocked = false; } void WillMoveNode() { mLocked = true; } void DidMoveNode(const nsINode& aOldParent, int32_t aOldOffset, const nsINode& aNewParent, int32_t aNewOffset); private: // TODO: A lot of loop in these methods check whether each item `nullptr` or // not. We should make it not nullable later. nsTArray> mArray; bool mLocked; }; /** * Helper class for using SelectionState. Stack based class for doing * preservation of dom points across editor actions. */ class MOZ_STACK_CLASS AutoTrackDOMPoint final { public: AutoTrackDOMPoint() = delete; AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, nsCOMPtr* aNode, int32_t* aOffset) : mRangeUpdater(aRangeUpdater), mNode(aNode), mOffset(aOffset), mPoint(nullptr), mRangeItem(do_AddRef(new RangeItem())) { mRangeItem->mStartContainer = *mNode; mRangeItem->mEndContainer = *mNode; mRangeItem->mStartOffset = *mOffset; mRangeItem->mEndOffset = *mOffset; mRangeUpdater.RegisterRangeItem(mRangeItem); } AutoTrackDOMPoint(RangeUpdater& aRangeUpdater, EditorDOMPoint* aPoint) : mRangeUpdater(aRangeUpdater), mNode(nullptr), mOffset(nullptr), mPoint(aPoint), mRangeItem(do_AddRef(new RangeItem())) { mRangeItem->mStartContainer = mPoint->GetContainer(); mRangeItem->mEndContainer = mPoint->GetContainer(); mRangeItem->mStartOffset = mPoint->Offset(); mRangeItem->mEndOffset = mPoint->Offset(); mRangeUpdater.RegisterRangeItem(mRangeItem); } ~AutoTrackDOMPoint() { mRangeUpdater.DropRangeItem(mRangeItem); if (mPoint) { // Setting `mPoint` with invalid DOM point causes hitting `NS_ASSERTION()` // and the number of times may be too many. (E.g., 1533913.html hits // over 700 times!) We should just put warning instead. if (NS_WARN_IF(!mRangeItem->mStartContainer) || NS_WARN_IF(mRangeItem->mStartOffset < 0)) { mPoint->Clear(); return; } if (NS_WARN_IF(mRangeItem->mStartContainer->Length() < static_cast(mRangeItem->mStartOffset))) { mPoint->SetToEndOf(mRangeItem->mStartContainer); return; } mPoint->Set(mRangeItem->mStartContainer, mRangeItem->mStartOffset); return; } *mNode = mRangeItem->mStartContainer; *mOffset = mRangeItem->mStartOffset; } private: RangeUpdater& mRangeUpdater; // Allow tracking nsINode until nsNode is gone nsCOMPtr* mNode; int32_t* mOffset; EditorDOMPoint* mPoint; OwningNonNull mRangeItem; }; /** * Another helper class for SelectionState. Stack based class for doing * Will/DidReplaceContainer() */ class MOZ_STACK_CLASS AutoReplaceContainerSelNotify final { public: AutoReplaceContainerSelNotify() = delete; // FYI: Marked as `MOZ_CAN_RUN_SCRIPT` for avoiding to use strong pointers // for the members. MOZ_CAN_RUN_SCRIPT AutoReplaceContainerSelNotify( RangeUpdater& aRangeUpdater, dom::Element& aOriginalElement, dom::Element& aNewElement) : mRangeUpdater(aRangeUpdater), mOriginalElement(aOriginalElement), mNewElement(aNewElement) { mRangeUpdater.WillReplaceContainer(); } ~AutoReplaceContainerSelNotify() { mRangeUpdater.DidReplaceContainer(mOriginalElement, mNewElement); } private: RangeUpdater& mRangeUpdater; dom::Element& mOriginalElement; dom::Element& mNewElement; }; /** * Another helper class for SelectionState. Stack based class for doing * Will/DidRemoveContainer() */ class MOZ_STACK_CLASS AutoRemoveContainerSelNotify final { public: AutoRemoveContainerSelNotify() = delete; AutoRemoveContainerSelNotify(RangeUpdater& aRangeUpdater, const EditorDOMPoint& aAtRemovingElement) : mRangeUpdater(aRangeUpdater), mRemovingElement(*aAtRemovingElement.GetChild()->AsElement()), mParentNode(*aAtRemovingElement.GetContainer()), mOffsetInParent(aAtRemovingElement.Offset()), mChildCountOfRemovingElement(mRemovingElement->GetChildCount()) { MOZ_ASSERT(aAtRemovingElement.IsSet()); mRangeUpdater.WillRemoveContainer(); } ~AutoRemoveContainerSelNotify() { mRangeUpdater.DidRemoveContainer(mRemovingElement, mParentNode, mOffsetInParent, mChildCountOfRemovingElement); } private: RangeUpdater& mRangeUpdater; OwningNonNull mRemovingElement; OwningNonNull mParentNode; uint32_t mOffsetInParent; uint32_t mChildCountOfRemovingElement; }; /** * Another helper class for SelectionState. Stack based class for doing * Will/DidInsertContainer() * XXX The lock state isn't useful if the edit action is triggered from * a mutation event listener so that looks like that we can remove * this class. */ class MOZ_STACK_CLASS AutoInsertContainerSelNotify final { private: RangeUpdater& mRangeUpdater; public: AutoInsertContainerSelNotify() = delete; explicit AutoInsertContainerSelNotify(RangeUpdater& aRangeUpdater) : mRangeUpdater(aRangeUpdater) { mRangeUpdater.WillInsertContainer(); } ~AutoInsertContainerSelNotify() { mRangeUpdater.DidInsertContainer(); } }; /** * Another helper class for SelectionState. Stack based class for doing * Will/DidMoveNode() */ class MOZ_STACK_CLASS AutoMoveNodeSelNotify final { public: AutoMoveNodeSelNotify() = delete; AutoMoveNodeSelNotify(RangeUpdater& aRangeUpdater, const EditorDOMPoint& aOldPoint, const EditorDOMPoint& aNewPoint) : mRangeUpdater(aRangeUpdater), mOldParent(*aOldPoint.GetContainer()), mNewParent(*aNewPoint.GetContainer()), mOldOffset(aOldPoint.Offset()), mNewOffset(aNewPoint.Offset()) { MOZ_ASSERT(aOldPoint.IsSet()); MOZ_ASSERT(aNewPoint.IsSet()); mRangeUpdater.WillMoveNode(); } ~AutoMoveNodeSelNotify() { mRangeUpdater.DidMoveNode(mOldParent, mOldOffset, mNewParent, mNewOffset); } EditorRawDOMPoint ComputeInsertionPoint() const { if (&mOldParent == &mNewParent && mOldOffset < mNewOffset) { return EditorRawDOMPoint(&mNewParent, mNewOffset - 1); } return EditorRawDOMPoint(&mNewParent, mNewOffset); } private: RangeUpdater& mRangeUpdater; nsINode& mOldParent; nsINode& mNewParent; uint32_t mOldOffset; uint32_t mNewOffset; }; } // namespace mozilla #endif // #ifndef mozilla_SelectionState_h