/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ /* vim: set ts=2 sw=2 et 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/. */ #include "ContentEventHandler.h" #include "IMEContentObserver.h" #include "mozilla/AsyncEventDispatcher.h" #include "mozilla/IMEStateManager.h" #include "mozilla/TextComposition.h" #include "mozilla/dom/Element.h" #include "nsAutoPtr.h" #include "nsContentUtils.h" #include "nsGkAtoms.h" #include "nsIAtom.h" #include "nsIContent.h" #include "nsIDocument.h" #include "nsIDOMDocument.h" #include "nsIDOMRange.h" #include "nsIFrame.h" #include "nsINode.h" #include "nsIPresShell.h" #include "nsISelectionController.h" #include "nsISelectionPrivate.h" #include "nsISupports.h" #include "nsIWidget.h" #include "nsPresContext.h" #include "nsThreadUtils.h" #include "nsWeakReference.h" namespace mozilla { using namespace widget; NS_IMPL_ISUPPORTS5(IMEContentObserver, nsIMutationObserver, nsISelectionListener, nsIReflowObserver, nsIScrollObserver, nsISupportsWeakReference) IMEContentObserver::IMEContentObserver() { } void IMEContentObserver::Init(nsIWidget* aWidget, nsPresContext* aPresContext, nsIContent* aContent) { mWidget = aWidget; mEditableNode = IMEStateManager::GetRootEditableNode(aPresContext, aContent); if (!mEditableNode) { return; } nsIPresShell* presShell = aPresContext->PresShell(); // get selection and root content nsCOMPtr selCon; if (mEditableNode->IsNodeOfType(nsINode::eCONTENT)) { nsIFrame* frame = static_cast(mEditableNode.get())->GetPrimaryFrame(); NS_ENSURE_TRUE_VOID(frame); frame->GetSelectionController(aPresContext, getter_AddRefs(selCon)); } else { // mEditableNode is a document selCon = do_QueryInterface(presShell); } NS_ENSURE_TRUE_VOID(selCon); selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(mSelection)); NS_ENSURE_TRUE_VOID(mSelection); nsCOMPtr selDomRange; if (NS_SUCCEEDED(mSelection->GetRangeAt(0, getter_AddRefs(selDomRange)))) { nsRange* selRange = static_cast(selDomRange.get()); NS_ENSURE_TRUE_VOID(selRange && selRange->GetStartParent()); mRootContent = selRange->GetStartParent()-> GetSelectionRootContent(presShell); } else { mRootContent = mEditableNode->GetSelectionRootContent(presShell); } if (!mRootContent && mEditableNode->IsNodeOfType(nsINode::eDOCUMENT)) { // The document node is editable, but there are no contents, this document // is not editable. return; } NS_ENSURE_TRUE_VOID(mRootContent); if (IMEStateManager::IsTestingIME()) { nsIDocument* doc = aPresContext->Document(); (new AsyncEventDispatcher(doc, NS_LITERAL_STRING("MozIMEFocusIn"), false, false))->RunDOMEventWhenSafe(); } aWidget->NotifyIME(IMENotification(NOTIFY_IME_OF_FOCUS)); // NOTIFY_IME_OF_FOCUS might cause recreating IMEContentObserver // instance via IMEStateManager::UpdateIMEState(). So, this // instance might already have been destroyed, check it. if (!mRootContent) { return; } mDocShell = aPresContext->GetDocShell(); ObserveEditableNode(); } void IMEContentObserver::ObserveEditableNode() { MOZ_ASSERT(mSelection); MOZ_ASSERT(mRootContent); mUpdatePreference = mWidget->GetIMEUpdatePreference(); if (mUpdatePreference.WantSelectionChange()) { // add selection change listener nsCOMPtr selPrivate(do_QueryInterface(mSelection)); NS_ENSURE_TRUE_VOID(selPrivate); nsresult rv = selPrivate->AddSelectionListener(this); NS_ENSURE_SUCCESS_VOID(rv); } if (mUpdatePreference.WantTextChange()) { // add text change observer mRootContent->AddMutationObserver(this); } if (mUpdatePreference.WantPositionChanged() && mDocShell) { // Add scroll position listener and reflow observer to detect position and // size changes mDocShell->AddWeakScrollObserver(this); mDocShell->AddWeakReflowObserver(this); } } void IMEContentObserver::Destroy() { // If CreateTextStateManager failed, mRootContent will be null, // and we should not call NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR)) if (mRootContent) { if (IMEStateManager::IsTestingIME() && mEditableNode) { nsIDocument* doc = mEditableNode->OwnerDoc(); (new AsyncEventDispatcher(doc, NS_LITERAL_STRING("MozIMEFocusOut"), false, false))->RunDOMEventWhenSafe(); } mWidget->NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR)); } // Even if there are some pending notification, it'll never notify the widget. mWidget = nullptr; if (mUpdatePreference.WantSelectionChange() && mSelection) { nsCOMPtr selPrivate(do_QueryInterface(mSelection)); if (selPrivate) { selPrivate->RemoveSelectionListener(this); } } mSelection = nullptr; if (mUpdatePreference.WantTextChange() && mRootContent) { mRootContent->RemoveMutationObserver(this); } if (mUpdatePreference.WantPositionChanged() && mDocShell) { mDocShell->RemoveWeakScrollObserver(this); mDocShell->RemoveWeakReflowObserver(this); } mRootContent = nullptr; mEditableNode = nullptr; mDocShell = nullptr; mUpdatePreference.mWantUpdates = nsIMEUpdatePreference::NOTIFY_NOTHING; } bool IMEContentObserver::IsManaging(nsPresContext* aPresContext, nsIContent* aContent) { if (!mSelection || !mRootContent || !mEditableNode) { return false; // failed to initialize. } if (!mRootContent->IsInDoc()) { return false; // the focused editor has already been reframed. } return mEditableNode == IMEStateManager::GetRootEditableNode(aPresContext, aContent); } bool IMEContentObserver::IsEditorHandlingEventForComposition() const { if (!mWidget) { return false; } nsRefPtr composition = IMEStateManager::GetTextCompositionFor(mWidget); if (!composition) { return false; } return composition->IsEditorHandlingEvent(); } nsresult IMEContentObserver::GetSelectionAndRoot(nsISelection** aSelection, nsIContent** aRootContent) const { if (!mEditableNode || !mSelection) { return NS_ERROR_NOT_AVAILABLE; } NS_ASSERTION(mSelection && mRootContent, "uninitialized content observer"); NS_ADDREF(*aSelection = mSelection); NS_ADDREF(*aRootContent = mRootContent); return NS_OK; } // Helper class, used for selection change notification class SelectionChangeEvent : public nsRunnable { public: SelectionChangeEvent(IMEContentObserver* aDispatcher, bool aCausedByComposition) : mDispatcher(aDispatcher) , mCausedByComposition(aCausedByComposition) { MOZ_ASSERT(mDispatcher); } NS_IMETHOD Run() { if (mDispatcher->GetWidget()) { IMENotification notification(NOTIFY_IME_OF_SELECTION_CHANGE); notification.mSelectionChangeData.mCausedByComposition = mCausedByComposition; mDispatcher->GetWidget()->NotifyIME(notification); } return NS_OK; } private: nsRefPtr mDispatcher; bool mCausedByComposition; }; nsresult IMEContentObserver::NotifySelectionChanged(nsIDOMDocument* aDOMDocument, nsISelection* aSelection, int16_t aReason) { bool causedByComposition = IsEditorHandlingEventForComposition(); if (causedByComposition && !mUpdatePreference.WantChangesCausedByComposition()) { return NS_OK; } int32_t count = 0; nsresult rv = aSelection->GetRangeCount(&count); NS_ENSURE_SUCCESS(rv, rv); if (count > 0 && mWidget) { nsContentUtils::AddScriptRunner( new SelectionChangeEvent(this, causedByComposition)); } return NS_OK; } // Helper class, used for position change notification class PositionChangeEvent MOZ_FINAL : public nsRunnable { public: PositionChangeEvent(IMEContentObserver* aDispatcher) : mDispatcher(aDispatcher) { MOZ_ASSERT(mDispatcher); } NS_IMETHOD Run() { if (mDispatcher->GetWidget()) { mDispatcher->GetWidget()->NotifyIME( IMENotification(NOTIFY_IME_OF_POSITION_CHANGE)); } return NS_OK; } private: nsRefPtr mDispatcher; }; void IMEContentObserver::ScrollPositionChanged() { if (mWidget) { nsContentUtils::AddScriptRunner(new PositionChangeEvent(this)); } } NS_IMETHODIMP IMEContentObserver::Reflow(DOMHighResTimeStamp aStart, DOMHighResTimeStamp aEnd) { if (mWidget) { nsContentUtils::AddScriptRunner(new PositionChangeEvent(this)); } return NS_OK; } NS_IMETHODIMP IMEContentObserver::ReflowInterruptible(DOMHighResTimeStamp aStart, DOMHighResTimeStamp aEnd) { if (mWidget) { nsContentUtils::AddScriptRunner(new PositionChangeEvent(this)); } return NS_OK; } // Helper class, used for text change notification class TextChangeEvent : public nsRunnable { public: TextChangeEvent(IMEContentObserver* aDispatcher, uint32_t aStart, uint32_t aOldEnd, uint32_t aNewEnd, bool aCausedByComposition) : mDispatcher(aDispatcher) , mStart(aStart) , mOldEnd(aOldEnd) , mNewEnd(aNewEnd) , mCausedByComposition(aCausedByComposition) { MOZ_ASSERT(mDispatcher); } NS_IMETHOD Run() { if (mDispatcher->GetWidget()) { IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE); notification.mTextChangeData.mStartOffset = mStart; notification.mTextChangeData.mOldEndOffset = mOldEnd; notification.mTextChangeData.mNewEndOffset = mNewEnd; notification.mTextChangeData.mCausedByComposition = mCausedByComposition; mDispatcher->GetWidget()->NotifyIME(notification); } return NS_OK; } private: nsRefPtr mDispatcher; uint32_t mStart, mOldEnd, mNewEnd; bool mCausedByComposition; }; void IMEContentObserver::CharacterDataChanged(nsIDocument* aDocument, nsIContent* aContent, CharacterDataChangeInfo* aInfo) { NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT), "character data changed for non-text node"); bool causedByComposition = IsEditorHandlingEventForComposition(); if (causedByComposition && !mUpdatePreference.WantChangesCausedByComposition()) { return; } uint32_t offset = 0; // get offsets of change and fire notification nsresult rv = ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, aContent, aInfo->mChangeStart, &offset); NS_ENSURE_SUCCESS_VOID(rv); uint32_t oldEnd = offset + aInfo->mChangeEnd - aInfo->mChangeStart; uint32_t newEnd = offset + aInfo->mReplaceLength; nsContentUtils::AddScriptRunner( new TextChangeEvent(this, offset, oldEnd, newEnd, causedByComposition)); } void IMEContentObserver::NotifyContentAdded(nsINode* aContainer, int32_t aStartIndex, int32_t aEndIndex) { bool causedByComposition = IsEditorHandlingEventForComposition(); if (causedByComposition && !mUpdatePreference.WantChangesCausedByComposition()) { return; } uint32_t offset = 0; nsresult rv = ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, aContainer, aStartIndex, &offset); NS_ENSURE_SUCCESS_VOID(rv); // get offset at the end of the last added node nsIContent* childAtStart = aContainer->GetChildAt(aStartIndex); uint32_t addingLength = 0; rv = ContentEventHandler::GetFlatTextOffsetOfRange(childAtStart, aContainer, aEndIndex, &addingLength); NS_ENSURE_SUCCESS_VOID(rv); if (!addingLength) { return; } nsContentUtils::AddScriptRunner( new TextChangeEvent(this, offset, offset, offset + addingLength, causedByComposition)); } void IMEContentObserver::ContentAppended(nsIDocument* aDocument, nsIContent* aContainer, nsIContent* aFirstNewContent, int32_t aNewIndexInContainer) { NotifyContentAdded(aContainer, aNewIndexInContainer, aContainer->GetChildCount()); } void IMEContentObserver::ContentInserted(nsIDocument* aDocument, nsIContent* aContainer, nsIContent* aChild, int32_t aIndexInContainer) { NotifyContentAdded(NODE_FROM(aContainer, aDocument), aIndexInContainer, aIndexInContainer + 1); } void IMEContentObserver::ContentRemoved(nsIDocument* aDocument, nsIContent* aContainer, nsIContent* aChild, int32_t aIndexInContainer, nsIContent* aPreviousSibling) { bool causedByComposition = IsEditorHandlingEventForComposition(); if (causedByComposition && !mUpdatePreference.WantChangesCausedByComposition()) { return; } uint32_t offset = 0; nsresult rv = ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, NODE_FROM(aContainer, aDocument), aIndexInContainer, &offset); NS_ENSURE_SUCCESS_VOID(rv); // get offset at the end of the deleted node int32_t nodeLength = aChild->IsNodeOfType(nsINode::eTEXT) ? static_cast(aChild->TextLength()) : std::max(static_cast(aChild->GetChildCount()), 1); MOZ_ASSERT(nodeLength >= 0, "The node length is out of range"); uint32_t textLength = 0; rv = ContentEventHandler::GetFlatTextOffsetOfRange(aChild, aChild, nodeLength, &textLength); NS_ENSURE_SUCCESS_VOID(rv); if (!textLength) { return; } nsContentUtils::AddScriptRunner( new TextChangeEvent(this, offset, offset + textLength, offset, causedByComposition)); } static nsIContent* GetContentBR(dom::Element* aElement) { if (!aElement->IsNodeOfType(nsINode::eCONTENT)) { return nullptr; } nsIContent* content = static_cast(aElement); return content->IsHTML(nsGkAtoms::br) ? content : nullptr; } void IMEContentObserver::AttributeWillChange(nsIDocument* aDocument, dom::Element* aElement, int32_t aNameSpaceID, nsIAtom* aAttribute, int32_t aModType) { nsIContent *content = GetContentBR(aElement); mPreAttrChangeLength = content ? ContentEventHandler::GetNativeTextLength(content) : 0; } void IMEContentObserver::AttributeChanged(nsIDocument* aDocument, dom::Element* aElement, int32_t aNameSpaceID, nsIAtom* aAttribute, int32_t aModType) { bool causedByComposition = IsEditorHandlingEventForComposition(); if (causedByComposition && !mUpdatePreference.WantChangesCausedByComposition()) { return; } nsIContent *content = GetContentBR(aElement); if (!content) { return; } uint32_t postAttrChangeLength = ContentEventHandler::GetNativeTextLength(content); if (postAttrChangeLength == mPreAttrChangeLength) { return; } uint32_t start; nsresult rv = ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, content, 0, &start); NS_ENSURE_SUCCESS_VOID(rv); nsContentUtils::AddScriptRunner( new TextChangeEvent(this, start, start + mPreAttrChangeLength, start + postAttrChangeLength, causedByComposition)); } } // namespace mozilla