diff options
Diffstat (limited to 'dom/events/IMEContentObserver.cpp')
-rw-r--r-- | dom/events/IMEContentObserver.cpp | 1869 |
1 files changed, 1869 insertions, 0 deletions
diff --git a/dom/events/IMEContentObserver.cpp b/dom/events/IMEContentObserver.cpp new file mode 100644 index 0000000000..a690cfa646 --- /dev/null +++ b/dom/events/IMEContentObserver.cpp @@ -0,0 +1,1869 @@ +/* -*- 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/. */ + +#include "mozilla/Logging.h" + +#include "ContentEventHandler.h" +#include "IMEContentObserver.h" +#include "mozilla/AsyncEventDispatcher.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/EventStateManager.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/MouseEvents.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsIAtom.h" +#include "nsIContent.h" +#include "nsIDocument.h" +#include "nsIDOMDocument.h" +#include "nsIDOMRange.h" +#include "nsIEditorIMESupport.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 "nsWeakReference.h" +#include "WritingModes.h" + +namespace mozilla { + +using namespace widget; + +LazyLogModule sIMECOLog("IMEContentObserver"); + +static const char* +ToChar(bool aBool) +{ + return aBool ? "true" : "false"; +} + +class WritingModeToString final : public nsAutoCString +{ +public: + explicit WritingModeToString(const WritingMode& aWritingMode) + { + if (!aWritingMode.IsVertical()) { + AssignLiteral("Horizontal"); + return; + } + if (aWritingMode.IsVerticalLR()) { + AssignLiteral("Vertical (LR)"); + return; + } + AssignLiteral("Vertical (RL)"); + } + virtual ~WritingModeToString() {} +}; + +class SelectionChangeDataToString final : public nsAutoCString +{ +public: + explicit SelectionChangeDataToString( + const IMENotification::SelectionChangeDataBase& aData) + { + if (!aData.IsValid()) { + AppendLiteral("{ IsValid()=false }"); + return; + } + AppendPrintf("{ mOffset=%u, ", aData.mOffset); + if (aData.mString->Length() > 20) { + AppendPrintf("mString.Length()=%u, ", aData.mString->Length()); + } else { + AppendPrintf("mString=\"%s\" (Length()=%u), ", + NS_ConvertUTF16toUTF8(*aData.mString).get(), + aData.mString->Length()); + } + AppendPrintf("GetWritingMode()=%s, mReversed=%s, mCausedByComposition=%s, " + "mCausedBySelectionEvent=%s }", + WritingModeToString(aData.GetWritingMode()).get(), + ToChar(aData.mReversed), + ToChar(aData.mCausedByComposition), + ToChar(aData.mCausedBySelectionEvent)); + } + virtual ~SelectionChangeDataToString() {} +}; + +class TextChangeDataToString final : public nsAutoCString +{ +public: + explicit TextChangeDataToString( + const IMENotification::TextChangeDataBase& aData) + { + if (!aData.IsValid()) { + AppendLiteral("{ IsValid()=false }"); + return; + } + AppendPrintf("{ mStartOffset=%u, mRemovedEndOffset=%u, mAddedEndOffset=%u, " + "mCausedOnlyByComposition=%s, " + "mIncludingChangesDuringComposition=%s, " + "mIncludingChangesWithoutComposition=%s }", + aData.mStartOffset, aData.mRemovedEndOffset, + aData.mAddedEndOffset, + ToChar(aData.mCausedOnlyByComposition), + ToChar(aData.mIncludingChangesDuringComposition), + ToChar(aData.mIncludingChangesWithoutComposition)); + } + virtual ~TextChangeDataToString() {} +}; + +/****************************************************************************** + * mozilla::IMEContentObserver + ******************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver) + nsAutoScriptBlocker scriptBlocker; + + tmp->NotifyIMEOfBlur(); + tmp->UnregisterObservers(); + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelection) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootContent) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditableNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditor) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndOfAddedTextCache.mContainerNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartOfRemovingTextRangeCache.mContainerNode) + + tmp->mUpdatePreference.mWantUpdates = nsIMEUpdatePreference::NOTIFY_NOTHING; + tmp->mESM = nullptr; +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IMEContentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWidget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFocusedWidget) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelection) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootContent) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditableNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditor) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndOfAddedTextCache.mContainerNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE( + mStartOfRemovingTextRangeCache.mContainerNode) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver) + NS_INTERFACE_MAP_ENTRY(nsISelectionListener) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsIReflowObserver) + NS_INTERFACE_MAP_ENTRY(nsIScrollObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY(nsIEditorObserver) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISelectionListener) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver) + +IMEContentObserver::IMEContentObserver() + : mESM(nullptr) + , mSuppressNotifications(0) + , mPreCharacterDataChangeLength(-1) + , mSendingNotification(NOTIFY_IME_OF_NOTHING) + , mIsObserving(false) + , mIMEHasFocus(false) + , mNeedsToNotifyIMEOfFocusSet(false) + , mNeedsToNotifyIMEOfTextChange(false) + , mNeedsToNotifyIMEOfSelectionChange(false) + , mNeedsToNotifyIMEOfPositionChange(false) + , mNeedsToNotifyIMEOfCompositionEventHandled(false) + , mIsHandlingQueryContentEvent(false) +{ +#ifdef DEBUG + mTextChangeData.Test(); +#endif +} + +void +IMEContentObserver::Init(nsIWidget* aWidget, + nsPresContext* aPresContext, + nsIContent* aContent, + nsIEditor* aEditor) +{ + State state = GetState(); + if (NS_WARN_IF(state == eState_Observing)) { + return; // Nothing to do. + } + + bool firstInitialization = state != eState_StoppedObserving; + if (!firstInitialization) { + // If this is now trying to initialize with new contents, all observers + // should be registered again for simpler implementation. + UnregisterObservers(); + // Clear members which may not be initialized again. + Clear(); + } + + mESM = aPresContext->EventStateManager(); + mESM->OnStartToObserveContent(this); + + mWidget = aWidget; + + if (aWidget->GetInputContext().mIMEState.mEnabled == IMEState::PLUGIN) { + if (!InitWithPlugin(aPresContext, aContent)) { + Clear(); + return; + } + } else { + if (!InitWithEditor(aPresContext, aContent, aEditor)) { + Clear(); + return; + } + } + + if (firstInitialization) { + // Now, try to send NOTIFY_IME_OF_FOCUS to IME via the widget. + MaybeNotifyIMEOfFocusSet(); + // When this is called first time, IME has not received NOTIFY_IME_OF_FOCUS + // yet since NOTIFY_IME_OF_FOCUS will be sent to widget asynchronously. + // So, we need to do nothing here. After NOTIFY_IME_OF_FOCUS has been + // sent, OnIMEReceivedFocus() will be called and content, selection and/or + // position changes will be observed + return; + } + + // When this is called after editor reframing (i.e., the root editable node + // is also recreated), IME has usually received NOTIFY_IME_OF_FOCUS. In this + // case, we need to restart to observe content, selection and/or position + // changes in new root editable node. + ObserveEditableNode(); + + if (!NeedsToNotifyIMEOfSomething()) { + return; + } + + // Some change events may wait to notify IME because this was being + // initialized. It is the time to flush them. + FlushMergeableNotifications(); +} + +void +IMEContentObserver::OnIMEReceivedFocus() +{ + // While Init() notifies IME of focus, pending layout may be flushed + // because the notification may cause querying content. Then, recursive + // call of Init() with the latest content may occur. In such case, we + // shouldn't keep first initialization which notified IME of focus. + if (GetState() != eState_Initializing) { + return; + } + + // 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; + } + + // Start to observe which is needed by IME when IME actually has focus. + ObserveEditableNode(); + + if (!NeedsToNotifyIMEOfSomething()) { + return; + } + + // Some change events may wait to notify IME because this was being + // initialized. It is the time to flush them. + FlushMergeableNotifications(); +} + +bool +IMEContentObserver::InitWithEditor(nsPresContext* aPresContext, + nsIContent* aContent, + nsIEditor* aEditor) +{ + MOZ_ASSERT(aEditor); + + mEditableNode = + IMEStateManager::GetRootEditableNode(aPresContext, aContent); + if (NS_WARN_IF(!mEditableNode)) { + return false; + } + + mEditor = aEditor; + if (NS_WARN_IF(!mEditor)) { + return false; + } + + nsIPresShell* presShell = aPresContext->PresShell(); + + // get selection and root content + nsCOMPtr<nsISelectionController> selCon; + if (mEditableNode->IsNodeOfType(nsINode::eCONTENT)) { + nsIFrame* frame = + static_cast<nsIContent*>(mEditableNode.get())->GetPrimaryFrame(); + if (NS_WARN_IF(!frame)) { + return false; + } + + frame->GetSelectionController(aPresContext, + getter_AddRefs(selCon)); + } else { + // mEditableNode is a document + selCon = do_QueryInterface(presShell); + } + + if (NS_WARN_IF(!selCon)) { + return false; + } + + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, + getter_AddRefs(mSelection)); + if (NS_WARN_IF(!mSelection)) { + return false; + } + + nsCOMPtr<nsIDOMRange> selDomRange; + if (NS_SUCCEEDED(mSelection->GetRangeAt(0, getter_AddRefs(selDomRange)))) { + nsRange* selRange = static_cast<nsRange*>(selDomRange.get()); + if (NS_WARN_IF(!selRange) || NS_WARN_IF(!selRange->GetStartParent())) { + return false; + } + + 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 false; + } + + if (NS_WARN_IF(!mRootContent)) { + return false; + } + + mDocShell = aPresContext->GetDocShell(); + if (NS_WARN_IF(!mDocShell)) { + return false; + } + + MOZ_ASSERT(!WasInitializedWithPlugin()); + + return true; +} + +bool +IMEContentObserver::InitWithPlugin(nsPresContext* aPresContext, + nsIContent* aContent) +{ + if (NS_WARN_IF(!aContent) || + NS_WARN_IF(aContent->GetDesiredIMEState().mEnabled != IMEState::PLUGIN)) { + return false; + } + nsIFrame* frame = aContent->GetPrimaryFrame(); + if (NS_WARN_IF(!frame)) { + return false; + } + nsCOMPtr<nsISelectionController> selCon; + frame->GetSelectionController(aPresContext, getter_AddRefs(selCon)); + if (NS_WARN_IF(!selCon)) { + return false; + } + selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, + getter_AddRefs(mSelection)); + if (NS_WARN_IF(!mSelection)) { + return false; + } + + mEditor = nullptr; + mEditableNode = aContent; + mRootContent = aContent; + + mDocShell = aPresContext->GetDocShell(); + if (NS_WARN_IF(!mDocShell)) { + return false; + } + + MOZ_ASSERT(WasInitializedWithPlugin()); + + return true; +} + +bool +IMEContentObserver::WasInitializedWithPlugin() const +{ + return mDocShell && !mEditor; +} + +void +IMEContentObserver::Clear() +{ + mEditor = nullptr; + mSelection = nullptr; + mEditableNode = nullptr; + mRootContent = nullptr; + mDocShell = nullptr; +} + +void +IMEContentObserver::ObserveEditableNode() +{ + MOZ_RELEASE_ASSERT(mSelection); + MOZ_RELEASE_ASSERT(mRootContent); + MOZ_RELEASE_ASSERT(GetState() != eState_Observing); + + // If this is called before sending NOTIFY_IME_OF_FOCUS (it's possible when + // the editor is reframed before sending NOTIFY_IME_OF_FOCUS asynchronously), + // the update preference of mWidget may be different from after the widget + // receives NOTIFY_IME_OF_FOCUS. So, this should be called again by + // OnIMEReceivedFocus() which is called after sending NOTIFY_IME_OF_FOCUS. + if (!mIMEHasFocus) { + MOZ_ASSERT(!mWidget || mNeedsToNotifyIMEOfFocusSet || + mSendingNotification == NOTIFY_IME_OF_FOCUS, + "Wow, OnIMEReceivedFocus() won't be called?"); + return; + } + + mIsObserving = true; + if (mEditor) { + mEditor->AddEditorObserver(this); + } + + mUpdatePreference = mWidget->GetIMEUpdatePreference(); + if (!WasInitializedWithPlugin()) { + // Add selection change listener only when this starts to observe + // non-plugin content since we cannot detect selection changes in + // plugins. + nsCOMPtr<nsISelectionPrivate> 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::NotifyIMEOfBlur() +{ + // Prevent any notifications to be sent IME. + nsCOMPtr<nsIWidget> widget; + mWidget.swap(widget); + + // If we hasn't been set focus, we shouldn't send blur notification to IME. + if (!mIMEHasFocus) { + return; + } + + // mWidget must have been non-nullptr if IME has focus. + MOZ_RELEASE_ASSERT(widget); + + RefPtr<IMEContentObserver> kungFuDeathGrip(this); + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMEContentObserver::NotifyIMEOfBlur(), " + "sending NOTIFY_IME_OF_BLUR", this)); + + // For now, we need to send blur notification in any condition because + // we don't have any simple ways to send blur notification asynchronously. + // After this call, Destroy() or Unlink() will stop observing the content + // and forget everything. Therefore, if it's not safe to send notification + // when script blocker is unlocked, we cannot send blur notification after + // that and before next focus notification. + // Anyway, as far as we know, IME doesn't try to query content when it loses + // focus. So, this may not cause any problem. + mIMEHasFocus = false; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR), widget); + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::NotifyIMEOfBlur(), " + "sent NOTIFY_IME_OF_BLUR", this)); +} + +void +IMEContentObserver::UnregisterObservers() +{ + if (!mIsObserving) { + return; + } + mIsObserving = false; + + if (mEditor) { + mEditor->RemoveEditorObserver(this); + } + + if (mSelection) { + nsCOMPtr<nsISelectionPrivate> selPrivate(do_QueryInterface(mSelection)); + if (selPrivate) { + selPrivate->RemoveSelectionListener(this); + } + mSelectionData.Clear(); + mFocusedWidget = nullptr; + } + + if (mUpdatePreference.WantTextChange() && mRootContent) { + mRootContent->RemoveMutationObserver(this); + } + + if (mUpdatePreference.WantPositionChanged() && mDocShell) { + mDocShell->RemoveWeakScrollObserver(this); + mDocShell->RemoveWeakReflowObserver(this); + } +} + +nsPresContext* +IMEContentObserver::GetPresContext() const +{ + return mESM ? mESM->GetPresContext() : nullptr; +} + +void +IMEContentObserver::Destroy() +{ + // WARNING: When you change this method, you have to check Unlink() too. + + NotifyIMEOfBlur(); + UnregisterObservers(); + Clear(); + + mWidget = nullptr; + mUpdatePreference.mWantUpdates = nsIMEUpdatePreference::NOTIFY_NOTHING; + + if (mESM) { + mESM->OnStopObservingContent(this); + mESM = nullptr; + } +} + +bool +IMEContentObserver::Destroyed() const +{ + return !mWidget; +} + +void +IMEContentObserver::DisconnectFromEventStateManager() +{ + mESM = nullptr; +} + +bool +IMEContentObserver::MaybeReinitialize(nsIWidget* aWidget, + nsPresContext* aPresContext, + nsIContent* aContent, + nsIEditor* aEditor) +{ + if (!IsObservingContent(aPresContext, aContent)) { + return false; + } + + if (GetState() == eState_StoppedObserving) { + Init(aWidget, aPresContext, aContent, aEditor); + } + return IsManaging(aPresContext, aContent); +} + +bool +IMEContentObserver::IsManaging(nsPresContext* aPresContext, + nsIContent* aContent) const +{ + return GetState() == eState_Observing && + IsObservingContent(aPresContext, aContent); +} + +bool +IMEContentObserver::IsManaging(const TextComposition* aComposition) const +{ + if (GetState() != eState_Observing) { + return false; + } + nsPresContext* presContext = aComposition->GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return false; + } + if (presContext != GetPresContext()) { + return false; // observing different document + } + nsINode* targetNode = aComposition->GetEventTargetNode(); + nsIContent* targetContent = + targetNode && targetNode->IsContent() ? targetNode->AsContent() : nullptr; + return IsObservingContent(presContext, targetContent); +} + +IMEContentObserver::State +IMEContentObserver::GetState() const +{ + if (!mSelection || !mRootContent || !mEditableNode) { + return eState_NotObserving; // failed to initialize or finalized. + } + if (!mRootContent->IsInComposedDoc()) { + // the focused editor has already been reframed. + return eState_StoppedObserving; + } + return mIsObserving ? eState_Observing : eState_Initializing; +} + +bool +IMEContentObserver::IsObservingContent(nsPresContext* aPresContext, + nsIContent* aContent) const +{ + return IsInitializedWithPlugin() ? + mRootContent == aContent && mRootContent != nullptr : + mEditableNode == IMEStateManager::GetRootEditableNode(aPresContext, + aContent); +} + +bool +IMEContentObserver::IsEditorHandlingEventForComposition() const +{ + if (!mWidget) { + return false; + } + RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(mWidget); + if (!composition) { + return false; + } + return composition->IsEditorHandlingEvent(); +} + +bool +IMEContentObserver::IsEditorComposing() const +{ + // Note that don't use TextComposition here. The important thing is, + // whether the editor already started to handle composition because + // web contents can change selection, text content and/or something from + // compositionstart event listener which is run before EditorBase handles it. + nsCOMPtr<nsIEditorIMESupport> editorIMESupport = do_QueryInterface(mEditor); + if (NS_WARN_IF(!editorIMESupport)) { + return false; + } + bool isComposing = false; + nsresult rv = editorIMESupport->GetComposing(&isComposing); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + return isComposing; +} + +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; +} + +nsresult +IMEContentObserver::NotifySelectionChanged(nsIDOMDocument* aDOMDocument, + nsISelection* aSelection, + int16_t aReason) +{ + int32_t count = 0; + nsresult rv = aSelection->GetRangeCount(&count); + NS_ENSURE_SUCCESS(rv, rv); + if (count > 0 && mWidget) { + bool causedByComposition = IsEditorHandlingEventForComposition(); + bool causedBySelectionEvent = TextComposition::IsHandlingSelectionEvent(); + bool duringComposition = IsEditorComposing(); + MaybeNotifyIMEOfSelectionChange(causedByComposition, + causedBySelectionEvent, + duringComposition); + } + return NS_OK; +} + +void +IMEContentObserver::ScrollPositionChanged() +{ + MaybeNotifyIMEOfPositionChange(); +} + +NS_IMETHODIMP +IMEContentObserver::Reflow(DOMHighResTimeStamp aStart, + DOMHighResTimeStamp aEnd) +{ + MaybeNotifyIMEOfPositionChange(); + return NS_OK; +} + +NS_IMETHODIMP +IMEContentObserver::ReflowInterruptible(DOMHighResTimeStamp aStart, + DOMHighResTimeStamp aEnd) +{ + MaybeNotifyIMEOfPositionChange(); + return NS_OK; +} + +nsresult +IMEContentObserver::HandleQueryContentEvent(WidgetQueryContentEvent* aEvent) +{ + // If the instance has normal selection cache and the query event queries + // normal selection's range, it should use the cached selection which was + // sent to the widget. However, if this instance has already received new + // selection change notification but hasn't updated the cache yet (i.e., + // not sending selection change notification to IME, don't use the cached + // value. Note that don't update selection cache here since if you update + // selection cache here, IMENotificationSender won't notify IME of selection + // change because it looks like that the selection isn't actually changed. + bool isSelectionCacheAvailable = + aEvent->mUseNativeLineBreak && mSelectionData.IsValid() && + !mNeedsToNotifyIMEOfSelectionChange; + if (isSelectionCacheAvailable && + aEvent->mMessage == eQuerySelectedText && + aEvent->mInput.mSelectionType == SelectionType::eNormal) { + aEvent->mReply.mContentsRoot = mRootContent; + aEvent->mReply.mHasSelection = !mSelectionData.IsCollapsed(); + aEvent->mReply.mOffset = mSelectionData.mOffset; + aEvent->mReply.mString = mSelectionData.String(); + aEvent->mReply.mWritingMode = mSelectionData.GetWritingMode(); + aEvent->mReply.mReversed = mSelectionData.mReversed; + aEvent->mSucceeded = true; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::HandleQueryContentEvent(aEvent={ " + "mMessage=%s })", this, ToChar(aEvent->mMessage))); + return NS_OK; + } + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::HandleQueryContentEvent(aEvent={ " + "mMessage=%s })", this, ToChar(aEvent->mMessage))); + + // If we can make the event's input offset absolute with TextComposition or + // mSelection, we should set it here for reducing the cost of computing + // selection start offset. If ContentEventHandler receives a + // WidgetQueryContentEvent whose input offset is relative to insertion point, + // it computes current selection start offset (this may be expensive) and + // make the offset absolute value itself. + // Note that calling MakeOffsetAbsolute() makes the event a query event with + // absolute offset. So, ContentEventHandler doesn't pay any additional cost + // after calling MakeOffsetAbsolute() here. + if (aEvent->mInput.mRelativeToInsertionPoint && + aEvent->mInput.IsValidEventMessage(aEvent->mMessage)) { + RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(aEvent->mWidget); + if (composition) { + uint32_t compositionStart = composition->NativeOffsetOfStartComposition(); + if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(compositionStart))) { + return NS_ERROR_FAILURE; + } + } else if (isSelectionCacheAvailable) { + uint32_t selectionStart = mSelectionData.mOffset; + if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(selectionStart))) { + return NS_ERROR_FAILURE; + } + } + } + + AutoRestore<bool> handling(mIsHandlingQueryContentEvent); + mIsHandlingQueryContentEvent = true; + ContentEventHandler handler(GetPresContext()); + nsresult rv = handler.HandleQueryContentEvent(aEvent); + if (NS_WARN_IF(Destroyed())) { + // If this has already destroyed during querying the content, the query + // is outdated even if it's succeeded. So, make the query fail. + aEvent->mSucceeded = false; + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMEContentObserver::HandleQueryContentEvent(), WARNING, " + "IMEContentObserver has been destroyed during the query, " + "making the query fail", this)); + return rv; + } + + if (!IsInitializedWithPlugin() && + NS_WARN_IF(aEvent->mReply.mContentsRoot != mRootContent)) { + // Focus has changed unexpectedly, so make the query fail. + aEvent->mSucceeded = false; + } + return rv; +} + +bool +IMEContentObserver::OnMouseButtonEvent(nsPresContext* aPresContext, + WidgetMouseEvent* aMouseEvent) +{ + if (!mUpdatePreference.WantMouseButtonEventOnChar()) { + return false; + } + if (!aMouseEvent->IsTrusted() || + aMouseEvent->DefaultPrevented() || + !aMouseEvent->mWidget) { + return false; + } + // Now, we need to notify only mouse down and mouse up event. + switch (aMouseEvent->mMessage) { + case eMouseUp: + case eMouseDown: + break; + default: + return false; + } + if (NS_WARN_IF(!mWidget) || NS_WARN_IF(mWidget->Destroyed())) { + return false; + } + + RefPtr<IMEContentObserver> kungFuDeathGrip(this); + + WidgetQueryContentEvent charAtPt(true, eQueryCharacterAtPoint, + aMouseEvent->mWidget); + charAtPt.mRefPoint = aMouseEvent->mRefPoint; + ContentEventHandler handler(aPresContext); + handler.OnQueryCharacterAtPoint(&charAtPt); + if (NS_WARN_IF(!charAtPt.mSucceeded) || + charAtPt.mReply.mOffset == WidgetQueryContentEvent::NOT_FOUND) { + return false; + } + + // The widget might be destroyed during querying the content since it + // causes flushing layout. + if (!mWidget || NS_WARN_IF(mWidget->Destroyed())) { + return false; + } + + // The result character rect is relative to the top level widget. + // We should notify it with offset in the widget. + nsIWidget* topLevelWidget = mWidget->GetTopLevelWidget(); + if (topLevelWidget && topLevelWidget != mWidget) { + charAtPt.mReply.mRect.MoveBy( + topLevelWidget->WidgetToScreenOffset() - + mWidget->WidgetToScreenOffset()); + } + // The refPt is relative to its widget. + // We should notify it with offset in the widget. + if (aMouseEvent->mWidget != mWidget) { + charAtPt.mRefPoint += aMouseEvent->mWidget->WidgetToScreenOffset() - + mWidget->WidgetToScreenOffset(); + } + + IMENotification notification(NOTIFY_IME_OF_MOUSE_BUTTON_EVENT); + notification.mMouseButtonEventData.mEventMessage = aMouseEvent->mMessage; + notification.mMouseButtonEventData.mOffset = charAtPt.mReply.mOffset; + notification.mMouseButtonEventData.mCursorPos.Set( + charAtPt.mRefPoint.ToUnknownPoint()); + notification.mMouseButtonEventData.mCharRect.Set( + charAtPt.mReply.mRect.ToUnknownRect()); + notification.mMouseButtonEventData.mButton = aMouseEvent->button; + notification.mMouseButtonEventData.mButtons = aMouseEvent->buttons; + notification.mMouseButtonEventData.mModifiers = aMouseEvent->mModifiers; + + nsresult rv = IMEStateManager::NotifyIME(notification, mWidget); + if (NS_WARN_IF(NS_FAILED(rv))) { + return false; + } + + bool consumed = (rv == NS_SUCCESS_EVENT_CONSUMED); + if (consumed) { + aMouseEvent->PreventDefault(); + } + return consumed; +} + +void +IMEContentObserver::CharacterDataWillChange(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ + NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT), + "character data changed for non-text node"); + MOZ_ASSERT(mPreCharacterDataChangeLength < 0, + "CharacterDataChanged() should've reset " + "mPreCharacterDataChangeLength"); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + mPreCharacterDataChangeLength = + ContentEventHandler::GetNativeTextLength(aContent, aInfo->mChangeStart, + aInfo->mChangeEnd); + MOZ_ASSERT(mPreCharacterDataChangeLength >= + aInfo->mChangeEnd - aInfo->mChangeStart, + "The computed length must be same as or larger than XP length"); +} + +void +IMEContentObserver::CharacterDataChanged(nsIDocument* aDocument, + nsIContent* aContent, + CharacterDataChangeInfo* aInfo) +{ + NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT), + "character data changed for non-text node"); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + + int64_t removedLength = mPreCharacterDataChangeLength; + mPreCharacterDataChangeLength = -1; + + MOZ_ASSERT(removedLength >= 0, + "mPreCharacterDataChangeLength should've been set by " + "CharacterDataWillChange()"); + + uint32_t offset = 0; + // get offsets of change and fire notification + nsresult rv = + ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootContent, 0), + NodePosition(aContent, aInfo->mChangeStart), + mRootContent, &offset, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + uint32_t newLength = + ContentEventHandler::GetNativeTextLength(aContent, aInfo->mChangeStart, + aInfo->mChangeStart + + aInfo->mReplaceLength); + + uint32_t oldEnd = offset + static_cast<uint32_t>(removedLength); + uint32_t newEnd = offset + newLength; + + TextChangeData data(offset, oldEnd, newEnd, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +void +IMEContentObserver::NotifyContentAdded(nsINode* aContainer, + int32_t aStartIndex, + int32_t aEndIndex) +{ + mStartOfRemovingTextRangeCache.Clear(); + + uint32_t offset = 0; + nsresult rv = NS_OK; + if (!mEndOfAddedTextCache.Match(aContainer, aStartIndex)) { + mEndOfAddedTextCache.Clear(); + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootContent, 0), + NodePositionBefore(aContainer, aStartIndex), + mRootContent, &offset, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED((rv)))) { + return; + } + } else { + offset = mEndOfAddedTextCache.mFlatTextLength; + } + + // get offset at the end of the last added node + uint32_t addingLength = 0; + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePositionBefore(aContainer, aStartIndex), + NodePosition(aContainer, aEndIndex), + mRootContent, &addingLength, + LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED((rv)))) { + mEndOfAddedTextCache.Clear(); + return; + } + + // If multiple lines are being inserted in an HTML editor, next call of + // NotifyContentAdded() is for adding next node. Therefore, caching the text + // length can skip to compute the text length before the adding node and + // before of it. + mEndOfAddedTextCache.Cache(aContainer, aEndIndex, offset + addingLength); + + if (!addingLength) { + return; + } + + TextChangeData data(offset, offset, offset + addingLength, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +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) +{ + mEndOfAddedTextCache.Clear(); + + nsINode* containerNode = NODE_FROM(aContainer, aDocument); + + uint32_t offset = 0; + nsresult rv = NS_OK; + if (!mStartOfRemovingTextRangeCache.Match(containerNode, aIndexInContainer)) { + // At removing a child node of aContainer, we need the line break caused + // by open tag of aContainer. Be careful when aIndexInContainer is 0. + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootContent, 0), + NodePosition(containerNode, aIndexInContainer), + mRootContent, &offset, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + mStartOfRemovingTextRangeCache.Clear(); + return; + } + mStartOfRemovingTextRangeCache.Cache(containerNode, aIndexInContainer, + offset); + } else { + offset = mStartOfRemovingTextRangeCache.mFlatTextLength; + } + + // get offset at the end of the deleted node + uint32_t textLength = 0; + if (aChild->IsNodeOfType(nsINode::eTEXT)) { + textLength = ContentEventHandler::GetNativeTextLength(aChild); + } else { + uint32_t nodeLength = static_cast<int32_t>(aChild->GetChildCount()); + rv = ContentEventHandler::GetFlatTextLengthInRange( + NodePositionBefore(aChild, 0), + NodePosition(aChild, nodeLength), + mRootContent, &textLength, + LINE_BREAK_TYPE_NATIVE, true); + if (NS_WARN_IF(NS_FAILED(rv))) { + mStartOfRemovingTextRangeCache.Clear(); + return; + } + } + + if (!textLength) { + return; + } + + TextChangeData data(offset, offset + textLength, offset, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +void +IMEContentObserver::AttributeWillChange(nsIDocument* aDocument, + dom::Element* aElement, + int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aNewValue) +{ + mPreAttrChangeLength = + ContentEventHandler::GetNativeTextLengthBefore(aElement, mRootContent); +} + +void +IMEContentObserver::AttributeChanged(nsIDocument* aDocument, + dom::Element* aElement, + int32_t aNameSpaceID, + nsIAtom* aAttribute, + int32_t aModType, + const nsAttrValue* aOldValue) +{ + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + + uint32_t postAttrChangeLength = + ContentEventHandler::GetNativeTextLengthBefore(aElement, mRootContent); + if (postAttrChangeLength == mPreAttrChangeLength) { + return; + } + uint32_t start; + nsresult rv = + ContentEventHandler::GetFlatTextLengthInRange( + NodePosition(mRootContent, 0), + NodePositionBefore(aElement, 0), + mRootContent, &start, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + + TextChangeData data(start, start + mPreAttrChangeLength, + start + postAttrChangeLength, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +void +IMEContentObserver::SuppressNotifyingIME() +{ + mSuppressNotifications++; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::SuppressNotifyingIME(), " + "mSuppressNotifications=%u", this, mSuppressNotifications)); +} + +void +IMEContentObserver::UnsuppressNotifyingIME() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::UnsuppressNotifyingIME(), " + "mSuppressNotifications=%u", this, mSuppressNotifications)); + + if (!mSuppressNotifications || --mSuppressNotifications) { + return; + } + FlushMergeableNotifications(); +} + +NS_IMETHODIMP +IMEContentObserver::EditAction() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::EditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + FlushMergeableNotifications(); + return NS_OK; +} + +NS_IMETHODIMP +IMEContentObserver::BeforeEditAction() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::BeforeEditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + return NS_OK; +} + +NS_IMETHODIMP +IMEContentObserver::CancelEditAction() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::CancelEditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + FlushMergeableNotifications(); + return NS_OK; +} + +void +IMEContentObserver::PostFocusSetNotification() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::PostFocusSetNotification()", this)); + + mNeedsToNotifyIMEOfFocusSet = true; +} + +void +IMEContentObserver::PostTextChangeNotification() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::PostTextChangeNotification(" + "mTextChangeData=%s)", + this, TextChangeDataToString(mTextChangeData).get())); + + MOZ_ASSERT(mTextChangeData.IsValid(), + "mTextChangeData must have text change data"); + mNeedsToNotifyIMEOfTextChange = true; +} + +void +IMEContentObserver::PostSelectionChangeNotification() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::PostSelectionChangeNotification(), " + "mSelectionData={ mCausedByComposition=%s, mCausedBySelectionEvent=%s }", + this, ToChar(mSelectionData.mCausedByComposition), + ToChar(mSelectionData.mCausedBySelectionEvent))); + + mNeedsToNotifyIMEOfSelectionChange = true; +} + +void +IMEContentObserver::MaybeNotifyIMEOfFocusSet() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::MaybeNotifyIMEOfFocusSet()", this)); + + PostFocusSetNotification(); + FlushMergeableNotifications(); +} + +void +IMEContentObserver::MaybeNotifyIMEOfTextChange( + const TextChangeDataBase& aTextChangeData) +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::MaybeNotifyIMEOfTextChange(" + "aTextChangeData=%s)", + this, TextChangeDataToString(aTextChangeData).get())); + + mTextChangeData += aTextChangeData; + PostTextChangeNotification(); + FlushMergeableNotifications(); +} + +void +IMEContentObserver::MaybeNotifyIMEOfSelectionChange( + bool aCausedByComposition, + bool aCausedBySelectionEvent, + bool aOccurredDuringComposition) +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::MaybeNotifyIMEOfSelectionChange(" + "aCausedByComposition=%s, aCausedBySelectionEvent=%s, " + "aOccurredDuringComposition)", + this, ToChar(aCausedByComposition), ToChar(aCausedBySelectionEvent))); + + mSelectionData.AssignReason(aCausedByComposition, + aCausedBySelectionEvent, + aOccurredDuringComposition); + PostSelectionChangeNotification(); + FlushMergeableNotifications(); +} + +void +IMEContentObserver::MaybeNotifyIMEOfPositionChange() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::MaybeNotifyIMEOfPositionChange()", this)); + // If reflow is caused by ContentEventHandler during PositionChangeEvent + // sending NOTIFY_IME_OF_POSITION_CHANGE, we don't need to notify IME of it + // again since ContentEventHandler returns the result including this reflow's + // result. + if (mIsHandlingQueryContentEvent && + mSendingNotification == NOTIFY_IME_OF_POSITION_CHANGE) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::MaybeNotifyIMEOfPositionChange(), " + "ignored since caused by ContentEventHandler during sending " + "NOTIY_IME_OF_POSITION_CHANGE", this)); + return; + } + PostPositionChangeNotification(); + FlushMergeableNotifications(); +} + +void +IMEContentObserver::MaybeNotifyCompositionEventHandled() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::MaybeNotifyCompositionEventHandled()", + this)); + + PostCompositionEventHandledNotification(); + FlushMergeableNotifications(); +} + +bool +IMEContentObserver::UpdateSelectionCache() +{ + MOZ_ASSERT(IsSafeToNotifyIME()); + + if (WasInitializedWithPlugin()) { + return false; + } + + mSelectionData.ClearSelectionData(); + + // XXX Cannot we cache some information for reducing the cost to compute + // selection offset and writing mode? + WidgetQueryContentEvent selection(true, eQuerySelectedText, mWidget); + ContentEventHandler handler(GetPresContext()); + handler.OnQuerySelectedText(&selection); + if (NS_WARN_IF(!selection.mSucceeded) || + NS_WARN_IF(selection.mReply.mContentsRoot != mRootContent)) { + return false; + } + + mFocusedWidget = selection.mReply.mFocusedWidget; + mSelectionData.mOffset = selection.mReply.mOffset; + *mSelectionData.mString = selection.mReply.mString; + mSelectionData.SetWritingMode(selection.GetWritingMode()); + mSelectionData.mReversed = selection.mReply.mReversed; + + // WARNING: Don't modify the reason of selection change here. + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::UpdateSelectionCache(), " + "mSelectionData=%s", + this, SelectionChangeDataToString(mSelectionData).get())); + + return mSelectionData.IsValid(); +} + +void +IMEContentObserver::PostPositionChangeNotification() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::PostPositionChangeNotification()", this)); + + mNeedsToNotifyIMEOfPositionChange = true; +} + +void +IMEContentObserver::PostCompositionEventHandledNotification() +{ + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::" + "PostCompositionEventHandledNotification()", this)); + + mNeedsToNotifyIMEOfCompositionEventHandled = true; +} + +bool +IMEContentObserver::IsReflowLocked() const +{ + nsPresContext* presContext = GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return false; + } + nsIPresShell* presShell = presContext->GetPresShell(); + if (NS_WARN_IF(!presShell)) { + return false; + } + // During reflow, we shouldn't notify IME because IME may query content + // synchronously. Then, it causes ContentEventHandler will try to flush + // pending notifications during reflow. + return presShell->IsReflowLocked(); +} + +bool +IMEContentObserver::IsSafeToNotifyIME() const +{ + // If this is already detached from the widget, this doesn't need to notify + // anything. + if (!mWidget) { + return false; + } + + // Don't notify IME of anything if it's not good time to do it. + if (mSuppressNotifications) { + return false; + } + + if (!mESM || NS_WARN_IF(!GetPresContext())) { + return false; + } + + // If it's in reflow, we should wait to finish the reflow. + // FYI: This should be called again from Reflow() or ReflowInterruptible(). + if (IsReflowLocked()) { + return false; + } + + // If we're in handling an edit action, this method will be called later. + bool isInEditAction = false; + if (mEditor && NS_SUCCEEDED(mEditor->GetIsInEditAction(&isInEditAction)) && + isInEditAction) { + return false; + } + + return true; +} + +void +IMEContentObserver::FlushMergeableNotifications() +{ + if (!IsSafeToNotifyIME()) { + // So, if this is already called, this should do nothing. + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::FlushMergeableNotifications(), " + "FAILED, due to unsafe to notify IME", this)); + return; + } + + // Notifying something may cause nested call of this method. For example, + // when somebody notified one of the notifications may dispatch query content + // event. Then, it causes flushing layout which may cause another layout + // change notification. + + if (mQueuedSender) { + // So, if this is already called, this should do nothing. + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::FlushMergeableNotifications(), " + "FAILED, due to already flushing pending notifications", this)); + return; + } + + if (!NeedsToNotifyIMEOfSomething()) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::FlushMergeableNotifications(), " + "FAILED, due to no pending notifications", this)); + return; + } + + // NOTE: Reset each pending flag because sending notification may cause + // another change. + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::FlushMergeableNotifications(), " + "creating IMENotificationSender...", this)); + + // If contents in selection range is modified, the selection range still + // has removed node from the tree. In such case, nsContentIterator won't + // work well. Therefore, we shouldn't use AddScriptRunnder() here since + // it may kick runnable event immediately after DOM tree is changed but + // the selection range isn't modified yet. + mQueuedSender = new IMENotificationSender(this); + NS_DispatchToCurrentThread(mQueuedSender); + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::FlushMergeableNotifications(), " + "finished", this)); +} + +void +IMEContentObserver::TryToFlushPendingNotifications() +{ + if (!mQueuedSender || mSendingNotification != NOTIFY_IME_OF_NOTHING) { + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::TryToFlushPendingNotifications(), " + "performing queued IMENotificationSender forcibly", this)); + RefPtr<IMENotificationSender> queuedSender = mQueuedSender; + queuedSender->Run(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::AChangeEvent + ******************************************************************************/ + +bool +IMEContentObserver::AChangeEvent::CanNotifyIME( + ChangeEventType aChangeEventType) const +{ + if (NS_WARN_IF(!mIMEContentObserver)) { + return false; + } + if (aChangeEventType == eChangeEventType_CompositionEventHandled) { + return mIMEContentObserver->mWidget != nullptr; + } + State state = mIMEContentObserver->GetState(); + // If it's not initialized, we should do nothing. + if (state == eState_NotObserving) { + return false; + } + // If setting focus, just check the state. + if (aChangeEventType == eChangeEventType_Focus) { + return !NS_WARN_IF(mIMEContentObserver->mIMEHasFocus); + } + // If we've not notified IME of focus yet, we shouldn't notify anything. + if (!mIMEContentObserver->mIMEHasFocus) { + return false; + } + + // If IME has focus, IMEContentObserver must hold the widget. + MOZ_ASSERT(mIMEContentObserver->mWidget); + + return true; +} + +bool +IMEContentObserver::AChangeEvent::IsSafeToNotifyIME( + ChangeEventType aChangeEventType) const +{ + if (NS_WARN_IF(!nsContentUtils::IsSafeToRunScript())) { + return false; + } + // While we're sending a notification, we shouldn't send another notification + // recursively. + if (mIMEContentObserver->mSendingNotification != NOTIFY_IME_OF_NOTHING) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::AChangeEvent::IsSafeToNotifyIME(), " + "putting off sending notification due to detecting recursive call, " + "mIMEContentObserver={ mSendingNotification=%s }", + this, ToChar(mIMEContentObserver->mSendingNotification))); + return false; + } + State state = mIMEContentObserver->GetState(); + if (aChangeEventType == eChangeEventType_Focus) { + if (NS_WARN_IF(state != eState_Initializing && state != eState_Observing)) { + return false; + } + } else if (aChangeEventType == eChangeEventType_CompositionEventHandled) { + // It doesn't need to check the observing status. + } else if (state != eState_Observing) { + return false; + } + return mIMEContentObserver->IsSafeToNotifyIME(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::IMENotificationSender + ******************************************************************************/ + +NS_IMETHODIMP +IMEContentObserver::IMENotificationSender::Run() +{ + if (NS_WARN_IF(mIsRunning)) { + MOZ_LOG(sIMECOLog, LogLevel::Error, + ("0x%p IMEContentObserver::IMENotificationSender::Run(), FAILED, " + "called recursively", this)); + return NS_OK; + } + + AutoRestore<bool> running(mIsRunning); + mIsRunning = true; + + // This instance was already performed forcibly. + if (mIMEContentObserver->mQueuedSender != this) { + return NS_OK; + } + + // NOTE: Reset each pending flag because sending notification may cause + // another change. + + if (mIMEContentObserver->mNeedsToNotifyIMEOfFocusSet) { + mIMEContentObserver->mNeedsToNotifyIMEOfFocusSet = false; + SendFocusSet(); + mIMEContentObserver->mQueuedSender = nullptr; + // If it's not safe to notify IME of focus, SendFocusSet() sets + // mNeedsToNotifyIMEOfFocusSet true again. For guaranteeing to send the + // focus notification later, we should put a new sender into the queue but + // this case must be rare. Note that if mIMEContentObserver is already + // destroyed, mNeedsToNotifyIMEOfFocusSet is never set true again. + if (mIMEContentObserver->mNeedsToNotifyIMEOfFocusSet) { + MOZ_ASSERT(!mIMEContentObserver->mIMEHasFocus); + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::Run(), " + "posting IMENotificationSender to current thread", this)); + mIMEContentObserver->mQueuedSender = + new IMENotificationSender(mIMEContentObserver); + NS_DispatchToCurrentThread(mIMEContentObserver->mQueuedSender); + return NS_OK; + } + // This is the first notification to IME. So, we don't need to notify + // anymore since IME starts to query content after it gets focus. + mIMEContentObserver->ClearPendingNotifications(); + return NS_OK; + } + + if (mIMEContentObserver->mNeedsToNotifyIMEOfTextChange) { + mIMEContentObserver->mNeedsToNotifyIMEOfTextChange = false; + SendTextChange(); + } + + // If a text change notification causes another text change again, we should + // notify IME of that before sending a selection change notification. + if (!mIMEContentObserver->mNeedsToNotifyIMEOfTextChange) { + // Be aware, PuppetWidget depends on the order of this. A selection change + // notification should not be sent before a text change notification because + // PuppetWidget shouldn't query new text content every selection change. + if (mIMEContentObserver->mNeedsToNotifyIMEOfSelectionChange) { + mIMEContentObserver->mNeedsToNotifyIMEOfSelectionChange = false; + SendSelectionChange(); + } + } + + // If a text change notification causes another text change again or a + // selection change notification causes either a text change or another + // selection change, we should notify IME of those before sending a position + // change notification. + if (!mIMEContentObserver->mNeedsToNotifyIMEOfTextChange && + !mIMEContentObserver->mNeedsToNotifyIMEOfSelectionChange) { + if (mIMEContentObserver->mNeedsToNotifyIMEOfPositionChange) { + mIMEContentObserver->mNeedsToNotifyIMEOfPositionChange = false; + SendPositionChange(); + } + } + + // Composition event handled notification should be sent after all the + // other notifications because this notifies widget of finishing all pending + // events are handled completely. + if (!mIMEContentObserver->mNeedsToNotifyIMEOfTextChange && + !mIMEContentObserver->mNeedsToNotifyIMEOfSelectionChange && + !mIMEContentObserver->mNeedsToNotifyIMEOfPositionChange) { + if (mIMEContentObserver->mNeedsToNotifyIMEOfCompositionEventHandled) { + mIMEContentObserver->mNeedsToNotifyIMEOfCompositionEventHandled = false; + SendCompositionEventHandled(); + } + } + + mIMEContentObserver->mQueuedSender = nullptr; + + // If notifications caused some new change, we should notify them now. + if (mIMEContentObserver->NeedsToNotifyIMEOfSomething()) { + if (mIMEContentObserver->GetState() == eState_StoppedObserving) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::Run(), " + "waiting IMENotificationSender to be reinitialized", this)); + } else { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::Run(), " + "posting IMENotificationSender to current thread", this)); + mIMEContentObserver->mQueuedSender = + new IMENotificationSender(mIMEContentObserver); + NS_DispatchToCurrentThread(mIMEContentObserver->mQueuedSender); + } + } + return NS_OK; +} + +void +IMEContentObserver::IMENotificationSender::SendFocusSet() +{ + if (!CanNotifyIME(eChangeEventType_Focus)) { + // If IMEContentObserver has already gone, we don't need to notify IME of + // focus. + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendFocusSet(), FAILED, due to impossible to notify IME of focus", + this)); + mIMEContentObserver->ClearPendingNotifications(); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Focus)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendFocusSet(), retrying to send NOTIFY_IME_OF_FOCUS...", this)); + mIMEContentObserver->PostFocusSetNotification(); + return; + } + + mIMEContentObserver->mIMEHasFocus = true; + // Initialize selection cache with the first selection data. + mIMEContentObserver->UpdateSelectionCache(); + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendFocusSet(), sending NOTIFY_IME_OF_FOCUS...", this)); + + MOZ_RELEASE_ASSERT(mIMEContentObserver->mSendingNotification == + NOTIFY_IME_OF_NOTHING); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_FOCUS; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_FOCUS), + mIMEContentObserver->mWidget); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + // nsIMEUpdatePreference referred by ObserveEditableNode() may be different + // before or after widget receives NOTIFY_IME_OF_FOCUS. Therefore, we need + // to guarantee to call ObserveEditableNode() after sending + // NOTIFY_IME_OF_FOCUS. + mIMEContentObserver->OnIMEReceivedFocus(); + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendFocusSet(), sent NOTIFY_IME_OF_FOCUS", this)); +} + +void +IMEContentObserver::IMENotificationSender::SendSelectionChange() +{ + if (!CanNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendSelectionChange(), FAILED, due to impossible to notify IME of " + "selection change", this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendSelectionChange(), retrying to send " + "NOTIFY_IME_OF_SELECTION_CHANGE...", this)); + mIMEContentObserver->PostSelectionChangeNotification(); + return; + } + + SelectionChangeData lastSelChangeData = mIMEContentObserver->mSelectionData; + if (NS_WARN_IF(!mIMEContentObserver->UpdateSelectionCache())) { + MOZ_LOG(sIMECOLog, LogLevel::Error, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendSelectionChange(), FAILED, due to UpdateSelectionCache() failure", + this)); + return; + } + + // The state may be changed since querying content causes flushing layout. + if (!CanNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendSelectionChange(), FAILED, due to flushing layout having changed " + "something", this)); + return; + } + + // If the selection isn't changed actually, we shouldn't notify IME of + // selection change. + SelectionChangeData& newSelChangeData = mIMEContentObserver->mSelectionData; + if (lastSelChangeData.IsValid() && + lastSelChangeData.mOffset == newSelChangeData.mOffset && + lastSelChangeData.String() == newSelChangeData.String() && + lastSelChangeData.GetWritingMode() == newSelChangeData.GetWritingMode() && + lastSelChangeData.mReversed == newSelChangeData.mReversed) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendSelectionChange(), not notifying IME of " + "NOTIFY_IME_OF_SELECTION_CHANGE due to not changed actually", this)); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendSelectionChange(), sending NOTIFY_IME_OF_SELECTION_CHANGE... " + "newSelChangeData=%s", + this, SelectionChangeDataToString(newSelChangeData).get())); + + IMENotification notification(NOTIFY_IME_OF_SELECTION_CHANGE); + notification.SetData(mIMEContentObserver->mSelectionData); + + MOZ_RELEASE_ASSERT(mIMEContentObserver->mSendingNotification == + NOTIFY_IME_OF_NOTHING); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_SELECTION_CHANGE; + IMEStateManager::NotifyIME(notification, mIMEContentObserver->mWidget); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendSelectionChange(), sent NOTIFY_IME_OF_SELECTION_CHANGE", this)); +} + +void +IMEContentObserver::IMENotificationSender::SendTextChange() +{ + if (!CanNotifyIME(eChangeEventType_Text)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendTextChange(), FAILED, due to impossible to notify IME of text " + "change", this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Text)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendTextChange(), retrying to send NOTIFY_IME_OF_TEXT_CHANGE...", + this)); + mIMEContentObserver->PostTextChangeNotification(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendTextChange(), sending NOTIFY_IME_OF_TEXT_CHANGE... " + "mIMEContentObserver={ mTextChangeData=%s }", + this, TextChangeDataToString(mIMEContentObserver->mTextChangeData).get())); + + IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE); + notification.SetData(mIMEContentObserver->mTextChangeData); + mIMEContentObserver->mTextChangeData.Clear(); + + MOZ_RELEASE_ASSERT(mIMEContentObserver->mSendingNotification == + NOTIFY_IME_OF_NOTHING); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_TEXT_CHANGE; + IMEStateManager::NotifyIME(notification, mIMEContentObserver->mWidget); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendTextChange(), sent NOTIFY_IME_OF_TEXT_CHANGE", this)); +} + +void +IMEContentObserver::IMENotificationSender::SendPositionChange() +{ + if (!CanNotifyIME(eChangeEventType_Position)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendPositionChange(), FAILED, due to impossible to notify IME of " + "position change", this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Position)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendPositionChange(), retrying to send " + "NOTIFY_IME_OF_POSITION_CHANGE...", this)); + mIMEContentObserver->PostPositionChangeNotification(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendPositionChange(), sending NOTIFY_IME_OF_POSITION_CHANGE...", this)); + + MOZ_RELEASE_ASSERT(mIMEContentObserver->mSendingNotification == + NOTIFY_IME_OF_NOTHING); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_POSITION_CHANGE; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_POSITION_CHANGE), + mIMEContentObserver->mWidget); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendPositionChange(), sent NOTIFY_IME_OF_POSITION_CHANGE", this)); +} + +void +IMEContentObserver::IMENotificationSender::SendCompositionEventHandled() +{ + if (!CanNotifyIME(eChangeEventType_CompositionEventHandled)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendCompositionEventHandled(), FAILED, due to impossible to notify " + "IME of composition event handled", this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_CompositionEventHandled)) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendCompositionEventHandled(), retrying to send " + "NOTIFY_IME_OF_POSITION_CHANGE...", this)); + mIMEContentObserver->PostCompositionEventHandledNotification(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendCompositionEventHandled(), sending " + "NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED...", this)); + + MOZ_RELEASE_ASSERT(mIMEContentObserver->mSendingNotification == + NOTIFY_IME_OF_NOTHING); + mIMEContentObserver->mSendingNotification = + NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED; + IMEStateManager::NotifyIME( + IMENotification(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED), + mIMEContentObserver->mWidget); + mIMEContentObserver->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMEContentObserver::IMENotificationSender::" + "SendCompositionEventHandled(), sent " + "NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED", this)); +} + +} // namespace mozilla |