/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=8 sts=2 et sw=2 tw=80: */ /* 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_IMEContentObserver_h #define mozilla_IMEContentObserver_h #include "mozilla/Attributes.h" #include "mozilla/EditorBase.h" #include "mozilla/dom/Selection.h" #include "nsCOMPtr.h" #include "nsCycleCollectionParticipant.h" #include "nsIDocShell.h" // XXX Why does only this need to be included here? #include "nsIReflowObserver.h" #include "nsIScrollObserver.h" #include "nsIWidget.h" #include "nsStubDocumentObserver.h" #include "nsStubMutationObserver.h" #include "nsThreadUtils.h" #include "nsWeakReference.h" class nsIContent; class nsINode; class nsPresContext; namespace mozilla { class EventStateManager; class TextComposition; namespace dom { class Selection; } // namespace dom // IMEContentObserver notifies widget of any text and selection changes // in the currently focused editor class IMEContentObserver final : public nsStubMutationObserver, public nsIReflowObserver, public nsIScrollObserver, public nsSupportsWeakReference { public: typedef widget::IMENotification::SelectionChangeData SelectionChangeData; typedef widget::IMENotification::TextChangeData TextChangeData; typedef widget::IMENotification::TextChangeDataBase TextChangeDataBase; typedef widget::IMENotificationRequests IMENotificationRequests; typedef widget::IMEMessage IMEMessage; IMEContentObserver(); NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_CYCLE_COLLECTION_CLASS_AMBIGUOUS(IMEContentObserver, nsIReflowObserver) NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATAWILLCHANGE NS_DECL_NSIMUTATIONOBSERVER_CHARACTERDATACHANGED NS_DECL_NSIMUTATIONOBSERVER_CONTENTAPPENDED NS_DECL_NSIMUTATIONOBSERVER_CONTENTINSERTED NS_DECL_NSIMUTATIONOBSERVER_CONTENTREMOVED NS_DECL_NSIREFLOWOBSERVER // nsIScrollObserver virtual void ScrollPositionChanged() override; /** * OnSelectionChange() is called when selection is changed in the editor. */ void OnSelectionChange(dom::Selection& aSelection); bool OnMouseButtonEvent(nsPresContext* aPresContext, WidgetMouseEvent* aMouseEvent); nsresult HandleQueryContentEvent(WidgetQueryContentEvent* aEvent); /** * Init() initializes the instance, i.e., retrieving necessary objects and * starts to observe something. * Be aware, callers of this method need to guarantee that the instance * won't be released during calling this. * * @param aWidget The widget which can access native IME. * @param aPresContext The PresContext which has aContent. * @param aContent An editable element or a plugin host element which * user may use IME in. * Or nullptr if this will observe design mode * document. * @param aEditorBase When aContent is an editable element or nullptr, * non-nullptr referring an editor instance which * manages aContent. * Otherwise, i.e., this will observe a plugin content, * should be nullptr. */ void Init(nsIWidget* aWidget, nsPresContext* aPresContext, nsIContent* aContent, EditorBase* aEditorBase); /** * Destroy() finalizes the instance, i.e., stops observing contents and * clearing the members. * Be aware, callers of this method need to guarantee that the instance * won't be released during calling this. */ void Destroy(); /** * Returns false if the instance refers some objects and observing them. * Otherwise, true. */ bool Destroyed() const; /** * IMEContentObserver is stored by EventStateManager during observing. * DisconnectFromEventStateManager() is called when EventStateManager stops * storing the instance. */ void DisconnectFromEventStateManager(); /** * MaybeReinitialize() tries to restart to observe the editor's root node. * This is useful when the editor is reframed and all children are replaced * with new node instances. * Be aware, callers of this method need to guarantee that the instance * won't be released during calling this. * * @return Returns true if the instance is managing the content. * Otherwise, false. */ bool MaybeReinitialize(nsIWidget* aWidget, nsPresContext* aPresContext, nsIContent* aContent, EditorBase* aEditorBase); bool IsManaging(nsPresContext* aPresContext, nsIContent* aContent) const; bool IsManaging(const TextComposition* aTextComposition) const; bool WasInitializedWithPlugin() const; bool WasInitializedWith(const EditorBase& aEditorBase) const { return mEditorBase == &aEditorBase; } bool IsEditorHandlingEventForComposition() const; bool KeepAliveDuringDeactive() const { return mIMENotificationRequests && mIMENotificationRequests->WantDuringDeactive(); } nsIWidget* GetWidget() const { return mWidget; } void SuppressNotifyingIME(); void UnsuppressNotifyingIME(); nsPresContext* GetPresContext() const; nsresult GetSelectionAndRoot(dom::Selection** aSelection, nsIContent** aRoot) const; /** * TryToFlushPendingNotifications() should be called when pending events * should be flushed. This tries to run the queued IMENotificationSender. * Doesn't do anything in child processes where flushing happens * asynchronously unless aAllowAsync is false. */ void TryToFlushPendingNotifications(bool aAllowAsync); /** * MaybeNotifyCompositionEventHandled() posts composition event handled * notification into the pseudo queue. */ void MaybeNotifyCompositionEventHandled(); /** * Following methods are called when the editor: * - an edit action handled. * - before handling an edit action. * - canceled handling an edit action after calling BeforeEditAction(). */ void OnEditActionHandled(); void BeforeEditAction(); void CancelEditAction(); private: ~IMEContentObserver() = default; enum State { eState_NotObserving, eState_Initializing, eState_StoppedObserving, eState_Observing }; State GetState() const; bool InitWithEditor(nsPresContext* aPresContext, nsIContent* aContent, EditorBase* aEditorBase); bool InitWithPlugin(nsPresContext* aPresContext, nsIContent* aContent); bool IsInitializedWithPlugin() const { return !mEditorBase; } void OnIMEReceivedFocus(); void Clear(); bool IsObservingContent(nsPresContext* aPresContext, nsIContent* aContent) const; bool IsReflowLocked() const; bool IsSafeToNotifyIME() const; bool IsEditorComposing() const; // Following methods are called by DocumentObserver when // beginning to update the contents and ending updating the contents. void BeginDocumentUpdate(); void EndDocumentUpdate(); // Following methods manages added nodes during a document change. /** * MaybeNotifyIMEOfAddedTextDuringDocumentChange() may send text change * notification caused by the nodes added between mFirstAddedContent in * mFirstAddedContainer and mLastAddedContent in * mLastAddedContainer and forgets the range. */ void MaybeNotifyIMEOfAddedTextDuringDocumentChange(); /** * IsInDocumentChange() returns true while the DOM tree is being modified * with mozAutoDocUpdate. E.g., it's being modified by setting innerHTML or * insertAdjacentHTML(). This returns false when user types something in * the focused editor editor. */ bool IsInDocumentChange() const { return mDocumentObserver && mDocumentObserver->IsUpdating(); } /** * Forget the range of added nodes during a document change. */ void ClearAddedNodesDuringDocumentChange(); /** * HasAddedNodesDuringDocumentChange() returns true when this stores range * of nodes which were added into the DOM tree during a document change but * have not been sent to IME. Note that this should always return false when * IsInDocumentChange() returns false. */ bool HasAddedNodesDuringDocumentChange() const { return mFirstAddedContainer && mLastAddedContainer; } /** * Returns true if the passed-in node in aParent is the next node of * mLastAddedContent in pre-order tree traversal of the DOM. */ bool IsNextNodeOfLastAddedNode(nsINode* aParent, nsIContent* aChild) const; void PostFocusSetNotification(); void MaybeNotifyIMEOfFocusSet(); void PostTextChangeNotification(); void MaybeNotifyIMEOfTextChange(const TextChangeDataBase& aTextChangeData); void CancelNotifyingIMEOfTextChange(); void PostSelectionChangeNotification(); void MaybeNotifyIMEOfSelectionChange(bool aCausedByComposition, bool aCausedBySelectionEvent, bool aOccurredDuringComposition); void PostPositionChangeNotification(); void MaybeNotifyIMEOfPositionChange(); void CancelNotifyingIMEOfPositionChange(); void PostCompositionEventHandledNotification(); void NotifyContentAdded(nsINode* aContainer, nsIContent* aFirstContent, nsIContent* aLastContent); void ObserveEditableNode(); /** * NotifyIMEOfBlur() notifies IME of blur. */ void NotifyIMEOfBlur(); /** * UnregisterObservers() unregisters all listeners and observers. */ void UnregisterObservers(); void FlushMergeableNotifications(); bool NeedsTextChangeNotification() const { return mIMENotificationRequests && mIMENotificationRequests->WantTextChange(); } bool NeedsPositionChangeNotification() const { return mIMENotificationRequests && mIMENotificationRequests->WantPositionChanged(); } void ClearPendingNotifications() { mNeedsToNotifyIMEOfFocusSet = false; mNeedsToNotifyIMEOfTextChange = false; mNeedsToNotifyIMEOfSelectionChange = false; mNeedsToNotifyIMEOfPositionChange = false; mNeedsToNotifyIMEOfCompositionEventHandled = false; mTextChangeData.Clear(); } bool NeedsToNotifyIMEOfSomething() const { return mNeedsToNotifyIMEOfFocusSet || mNeedsToNotifyIMEOfTextChange || mNeedsToNotifyIMEOfSelectionChange || mNeedsToNotifyIMEOfPositionChange || mNeedsToNotifyIMEOfCompositionEventHandled; } /** * UpdateSelectionCache() updates mSelectionData with the latest selection. * This should be called only when IsSafeToNotifyIME() returns true. * * Note that this does nothing if WasInitializedWithPlugin() returns true. */ bool UpdateSelectionCache(bool aRequireFlush = true); nsCOMPtr mWidget; // mFocusedWidget has the editor observed by the instance. E.g., if the // focused editor is in XUL panel, this should be the widget of the panel. // On the other hand, mWidget is its parent which handles IME. nsCOMPtr mFocusedWidget; RefPtr mSelection; nsCOMPtr mRootContent; nsCOMPtr mEditableNode; nsCOMPtr mDocShell; RefPtr mEditorBase; /** * Helper classes to notify IME. */ class AChangeEvent : public Runnable { protected: enum ChangeEventType { eChangeEventType_Focus, eChangeEventType_Selection, eChangeEventType_Text, eChangeEventType_Position, eChangeEventType_CompositionEventHandled }; explicit AChangeEvent(const char* aName, IMEContentObserver* aIMEContentObserver) : Runnable(aName), mIMEContentObserver(do_GetWeakReference( static_cast(aIMEContentObserver))) { MOZ_ASSERT(aIMEContentObserver); } already_AddRefed GetObserver() const { nsCOMPtr observer = do_QueryReferent(mIMEContentObserver); return observer.forget().downcast(); } nsWeakPtr mIMEContentObserver; /** * CanNotifyIME() checks if mIMEContentObserver can and should notify IME. */ bool CanNotifyIME(ChangeEventType aChangeEventType) const; /** * IsSafeToNotifyIME() checks if it's safe to noitify IME. */ bool IsSafeToNotifyIME(ChangeEventType aChangeEventType) const; }; class IMENotificationSender : public AChangeEvent { public: explicit IMENotificationSender(IMEContentObserver* aIMEContentObserver) : AChangeEvent("IMENotificationSender", aIMEContentObserver), mIsRunning(false) {} NS_IMETHOD Run() override; void Dispatch(nsIDocShell* aDocShell); private: void SendFocusSet(); void SendSelectionChange(); void SendTextChange(); void SendPositionChange(); void SendCompositionEventHandled(); bool mIsRunning; }; // mQueuedSender is, it was put into the event queue but not run yet. RefPtr mQueuedSender; /** * IMEContentObserver is a mutation observer of mRootContent. However, * it needs to know the beginning of content changes and end of it too for * reducing redundant computation of text offset with ContentEventHandler. * Therefore, it needs helper class to listen only them since if * both mutations were observed by IMEContentObserver directly, each * methods need to check if the changing node is in mRootContent but it's * too expensive. */ class DocumentObserver final : public nsStubDocumentObserver { public: explicit DocumentObserver(IMEContentObserver& aIMEContentObserver) : mIMEContentObserver(&aIMEContentObserver), mDocumentUpdating(0) {} NS_DECL_CYCLE_COLLECTION_CLASS(DocumentObserver) NS_DECL_CYCLE_COLLECTING_ISUPPORTS NS_DECL_NSIDOCUMENTOBSERVER_BEGINUPDATE NS_DECL_NSIDOCUMENTOBSERVER_ENDUPDATE void Observe(dom::Document*); void StopObserving(); void Destroy(); bool Destroyed() const { return !mIMEContentObserver; } bool IsObserving() const { return mDocument != nullptr; } bool IsUpdating() const { return mDocumentUpdating != 0; } private: DocumentObserver() = delete; virtual ~DocumentObserver() { Destroy(); } RefPtr mIMEContentObserver; RefPtr mDocument; uint32_t mDocumentUpdating; }; RefPtr mDocumentObserver; /** * FlatTextCache stores flat text length from start of the content to * mNodeOffset of mContainerNode. */ struct FlatTextCache { // mContainerNode and mNode represent a point in DOM tree. E.g., // if mContainerNode is a div element, mNode is a child. nsCOMPtr mContainerNode; // mNode points to the last child which participates in the current // mFlatTextLength. If mNode is null, then that means that the end point for // mFlatTextLength is immediately before the first child of mContainerNode. nsCOMPtr mNode; // Length of flat text generated from contents between the start of content // and a child node whose index is mNodeOffset of mContainerNode. uint32_t mFlatTextLength; FlatTextCache() : mFlatTextLength(0) {} void Clear() { mContainerNode = nullptr; mNode = nullptr; mFlatTextLength = 0; } void Cache(nsINode* aContainer, nsINode* aNode, uint32_t aFlatTextLength) { MOZ_ASSERT(aContainer, "aContainer must not be null"); MOZ_ASSERT(!aNode || aNode->GetParentNode() == aContainer, "aNode must be either null or a child of aContainer"); mContainerNode = aContainer; mNode = aNode; mFlatTextLength = aFlatTextLength; } bool Match(nsINode* aContainer, nsINode* aNode) const { return aContainer == mContainerNode && aNode == mNode; } }; // mEndOfAddedTextCache caches text length from the start of content to // the end of the last added content only while an edit action is being // handled by the editor and no other mutation (e.g., removing node) // occur. FlatTextCache mEndOfAddedTextCache; // mStartOfRemovingTextRangeCache caches text length from the start of content // to the start of the last removed content only while an edit action is being // handled by the editor and no other mutation (e.g., adding node) occur. FlatTextCache mStartOfRemovingTextRangeCache; // mFirstAddedContainer is parent node of first added node in current // document change. So, this is not nullptr only when a node was added // during a document change and the change has not been included into // mTextChangeData yet. // Note that this shouldn't be in cycle collection since this is not nullptr // only during a document change. nsCOMPtr mFirstAddedContainer; // mLastAddedContainer is parent node of last added node in current // document change. So, this is not nullptr only when a node was added // during a document change and the change has not been included into // mTextChangeData yet. // Note that this shouldn't be in cycle collection since this is not nullptr // only during a document change. nsCOMPtr mLastAddedContainer; // mFirstAddedContent is the first node added in mFirstAddedContainer. nsCOMPtr mFirstAddedContent; // mLastAddedContent is the last node added in mLastAddedContainer; nsCOMPtr mLastAddedContent; TextChangeData mTextChangeData; // mSelectionData is the last selection data which was notified. The // selection information is modified by UpdateSelectionCache(). The reason // of the selection change is modified by MaybeNotifyIMEOfSelectionChange(). SelectionChangeData mSelectionData; EventStateManager* mESM; const IMENotificationRequests* mIMENotificationRequests; uint32_t mSuppressNotifications; int64_t mPreCharacterDataChangeLength; // mSendingNotification is a notification which is now sending from // IMENotificationSender. When the value is NOTIFY_IME_OF_NOTHING, it's // not sending any notification. IMEMessage mSendingNotification; bool mIsObserving; bool mIMEHasFocus; bool mNeedsToNotifyIMEOfFocusSet; bool mNeedsToNotifyIMEOfTextChange; bool mNeedsToNotifyIMEOfSelectionChange; bool mNeedsToNotifyIMEOfPositionChange; bool mNeedsToNotifyIMEOfCompositionEventHandled; // mIsHandlingQueryContentEvent is true when IMEContentObserver is handling // WidgetQueryContentEvent with ContentEventHandler. bool mIsHandlingQueryContentEvent; }; } // namespace mozilla #endif // mozilla_IMEContentObserver_h