diff options
Diffstat (limited to 'dom/events/IMEContentObserver.cpp')
-rw-r--r-- | dom/events/IMEContentObserver.cpp | 2221 |
1 files changed, 2221 insertions, 0 deletions
diff --git a/dom/events/IMEContentObserver.cpp b/dom/events/IMEContentObserver.cpp new file mode 100644 index 0000000000..a8d946933a --- /dev/null +++ b/dom/events/IMEContentObserver.cpp @@ -0,0 +1,2221 @@ +/* -*- 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/PresShell.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextControlElement.h" +#include "mozilla/TextEvents.h" +#include "mozilla/dom/Element.h" +#include "mozilla/dom/Selection.h" +#include "nsContentUtils.h" +#include "nsGkAtoms.h" +#include "nsAtom.h" +#include "nsDocShell.h" +#include "nsIContent.h" +#include "mozilla/dom/Document.h" +#include "nsIFrame.h" +#include "nsINode.h" +#include "nsISelectionController.h" +#include "nsISupports.h" +#include "nsIWeakReferenceUtils.h" +#include "nsIWidget.h" +#include "nsPresContext.h" +#include "nsRange.h" +#include "nsRefreshDriver.h" +#include "WritingModes.h" + +namespace mozilla { + +using RawNodePosition = ContentEventHandler::RawNodePosition; +using RawNodePositionBefore = ContentEventHandler::RawNodePositionBefore; + +using namespace dom; +using namespace widget; + +LazyLogModule sIMECOLog("IMEContentObserver"); + +static const char* ToChar(bool aBool) { return aBool ? "true" : "false"; } + +// This method determines the node to use for the point before the current node. +// If you have the following aContent and aContainer, and want to represent the +// following point for `RawNodePosition` or `RawRangeBoundary`: +// +// <parent> {node} {node} | {node} </parent> +// ^ ^ ^ +// aContainer point aContent +// +// This function will shift `aContent` to the left into the format which +// `RawNodePosition` and `RawRangeBoundary` use: +// +// <parent> {node} {node} | {node} </parent> +// ^ ^ ^ +// aContainer result point +static nsIContent* PointBefore(nsINode* aContainer, nsIContent* aContent) { + if (aContent) { + return aContent->GetPreviousSibling(); + } + return aContainer->GetLastChild(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver + ******************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver) + +// Note that we don't need to add mFirstAddedContainer nor +// mLastAddedContainer to cycle collection because they are non-null only +// during short time and shouldn't be touched while they are non-null. + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver) + nsAutoScriptBlocker scriptBlocker; + + tmp->NotifyIMEOfBlur(); + tmp->UnregisterObservers(); + + NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelection) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditableNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorBase) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentObserver) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndOfAddedTextCache.mContainerNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartOfRemovingTextRangeCache.mContainerNode) + NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE + + tmp->mIMENotificationRequests = nullptr; + 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(mRootElement) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditableNode) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentObserver) + 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(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsIReflowObserver) + NS_INTERFACE_MAP_ENTRY(nsIScrollObserver) + NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference) + NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIReflowObserver) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver) + +IMEContentObserver::IMEContentObserver() { +#ifdef DEBUG + // TODO: Make this test as GTest. + mTextChangeData.Test(); +#endif +} + +void IMEContentObserver::Init(nsIWidget& aWidget, nsPresContext& aPresContext, + Element* aElement, EditorBase& aEditorBase) { + 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(); + } + + mESM = aPresContext.EventStateManager(); + mESM->OnStartToObserveContent(this); + + mWidget = &aWidget; + mIMENotificationRequests = &mWidget->IMENotificationRequestsRef(); + + if (!InitWithEditor(aPresContext, aElement, aEditorBase)) { + MOZ_LOG(sIMECOLog, LogLevel::Error, + ("0x%p Init() FAILED, due to InitWithEditor() " + "failure", + this)); + 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) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p OnIMEReceivedFocus(), " + "but the state is not \"initializing\", so does nothing", + this)); + return; + } + + // NOTIFY_IME_OF_FOCUS might cause recreating IMEContentObserver + // instance via IMEStateManager::UpdateIMEState(). So, this + // instance might already have been destroyed, check it. + if (!mRootElement) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p OnIMEReceivedFocus(), " + "but mRootElement has already been cleared, so does nothing", + this)); + 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, + Element* aElement, + EditorBase& aEditorBase) { + mEditableNode = IMEStateManager::GetRootEditableNode(aPresContext, aElement); + if (NS_WARN_IF(!mEditableNode)) { + return false; + } + + mEditorBase = &aEditorBase; + + RefPtr<PresShell> presShell = aPresContext.GetPresShell(); + + // get selection and root content + nsCOMPtr<nsISelectionController> selCon; + if (mEditableNode->IsContent()) { + nsIFrame* frame = mEditableNode->AsContent()->GetPrimaryFrame(); + if (NS_WARN_IF(!frame)) { + return false; + } + + frame->GetSelectionController(&aPresContext, getter_AddRefs(selCon)); + } else { + // mEditableNode is a document + selCon = presShell; + } + + if (NS_WARN_IF(!selCon)) { + return false; + } + + mSelection = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL); + if (NS_WARN_IF(!mSelection)) { + return false; + } + + if (mEditorBase->IsTextEditor()) { + mRootElement = mEditorBase->GetRoot(); // The anonymous <div> + MOZ_ASSERT(mRootElement); + MOZ_ASSERT(mRootElement->GetFirstChild()); + if (auto* text = Text::FromNodeOrNull( + mRootElement ? mRootElement->GetFirstChild() : nullptr)) { + mTextControlValueLength = ContentEventHandler::GetNativeTextLength(*text); + } + mIsTextControl = true; + } else if (const nsRange* selRange = mSelection->GetRangeAt(0)) { + MOZ_ASSERT(!mIsTextControl); + if (NS_WARN_IF(!selRange->GetStartContainer())) { + return false; + } + + nsCOMPtr<nsINode> startContainer = selRange->GetStartContainer(); + mRootElement = Element::FromNodeOrNull( + startContainer->GetSelectionRootContent(presShell)); + } else { + MOZ_ASSERT(!mIsTextControl); + nsCOMPtr<nsINode> editableNode = mEditableNode; + mRootElement = Element::FromNodeOrNull( + editableNode->GetSelectionRootContent(presShell)); + } + if (!mRootElement && mEditableNode->IsDocument()) { + // The document node is editable, but there are no contents, this document + // is not editable. + return false; + } + + if (NS_WARN_IF(!mRootElement)) { + return false; + } + + mDocShell = aPresContext.GetDocShell(); + if (NS_WARN_IF(!mDocShell)) { + return false; + } + + mDocumentObserver = new DocumentObserver(*this); + + return true; +} + +void IMEContentObserver::Clear() { + mEditorBase = nullptr; + mSelection = nullptr; + mEditableNode = nullptr; + mRootElement = nullptr; + mDocShell = nullptr; + // Should be safe to clear mDocumentObserver here even though it grabs + // this instance in most cases because this is called by Init() or Destroy(). + // The callers of Init() grab this instance with local RefPtr. + // The caller of Destroy() also grabs this instance with local RefPtr. + // So, this won't cause refcount of this instance become 0. + mDocumentObserver = nullptr; +} + +void IMEContentObserver::ObserveEditableNode() { + MOZ_RELEASE_ASSERT(mSelection); + MOZ_RELEASE_ASSERT(mRootElement); + 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 notification requests 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 (mEditorBase) { + mEditorBase->SetIMEContentObserver(this); + } + + mRootElement->AddMutationObserver(this); + // If it's in a document (should be so), we can use document observer to + // reduce redundant computation of text change offsets. + Document* doc = mRootElement->GetComposedDoc(); + if (doc) { + RefPtr<DocumentObserver> documentObserver = mDocumentObserver; + documentObserver->Observe(doc); + } + + if (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); + mIMENotificationRequests = nullptr; + + // 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 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 NotifyIMEOfBlur(), sent NOTIFY_IME_OF_BLUR", this)); +} + +void IMEContentObserver::UnregisterObservers() { + if (!mIsObserving) { + return; + } + mIsObserving = false; + + if (mEditorBase) { + mEditorBase->SetIMEContentObserver(nullptr); + } + + if (mSelection) { + mSelectionData.Clear(); + mFocusedWidget = nullptr; + } + + if (mRootElement) { + mRootElement->RemoveMutationObserver(this); + } + + if (mDocumentObserver) { + RefPtr<DocumentObserver> documentObserver = mDocumentObserver; + documentObserver->StopObserving(); + } + + if (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. + + // Note that don't send any notifications later from here. I.e., notify + // IMEStateManager of the blur synchronously because IMEStateManager needs to + // stop notifying the main process if this is requested by the main process. + NotifyIMEOfBlur(); + UnregisterObservers(); + Clear(); + + mWidget = nullptr; + mIMENotificationRequests = nullptr; + + 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, + Element* aElement, + EditorBase& aEditorBase) { + if (!IsObservingContent(aPresContext, aElement)) { + return false; + } + + if (GetState() == eState_StoppedObserving) { + Init(aWidget, aPresContext, aElement, aEditorBase); + } + return IsObserving(aPresContext, aElement); +} + +bool IMEContentObserver::IsObserving(const nsPresContext& aPresContext, + const Element* aElement) const { + if (GetState() != eState_Observing) { + return false; + } + // If aElement is not a text control, aElement is an editing host or entire + // the document is editable in the design mode. Therefore, return false if + // we're observing an anonymous subtree of a text control. + if (!aElement || !aElement->IsTextControlElement() || + !static_cast<const TextControlElement*>(aElement) + ->IsSingleLineTextControlOrTextArea()) { + if (mIsTextControl) { + return false; + } + } + // If aElement is a text control, return true if we're observing the anonymous + // subtree of aElement. Therefore, return false if we're observing with + // HTMLEditor. + else if (!mIsTextControl) { + return false; + } + return IsObservingContent(aPresContext, aElement); +} + +bool IMEContentObserver::IsBeingInitializedFor( + const nsPresContext& aPresContext, const Element* aElement, + const EditorBase& aEditorBase) const { + return GetState() == eState_Initializing && mEditorBase == &aEditorBase && + IsObservingContent(aPresContext, aElement); +} + +bool IMEContentObserver::IsObserving( + const TextComposition& aTextComposition) const { + if (GetState() != eState_Observing) { + return false; + } + nsPresContext* const presContext = aTextComposition.GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return false; + } + if (presContext != GetPresContext()) { + return false; // observing different document + } + auto* const elementHavingComposition = + Element::FromNodeOrNull(aTextComposition.GetEventTargetNode()); + bool isObserving = IsObservingContent(*presContext, elementHavingComposition); +#ifdef DEBUG + if (isObserving) { + if (mIsTextControl) { + MOZ_ASSERT(elementHavingComposition); + MOZ_ASSERT(elementHavingComposition->IsTextControlElement(), + "Should've never started to observe non-text-control element"); + // XXX Our fake focus move has not been implemented properly. So, the + // following assertions may fail, but I don't like to make the failures + // cause crash even in debug builds because it may block developers to + // debug web-compat issues. On the other hand, it'd be nice if we can + // detect the bug with automated tests. Therefore, the following + // assertions are NS_ASSERTION. + NS_ASSERTION(static_cast<TextControlElement*>(elementHavingComposition) + ->IsSingleLineTextControlOrTextArea(), + "Should've stopped observing when the type is changed"); + NS_ASSERTION(!elementHavingComposition->IsInDesignMode(), + "Should've stopped observing when the design mode started"); + } else if (elementHavingComposition) { + NS_ASSERTION( + !elementHavingComposition->IsTextControlElement() || + !static_cast<TextControlElement*>(elementHavingComposition) + ->IsSingleLineTextControlOrTextArea(), + "Should've never started to observe text-control element or " + "stopped observing it when the type is changed"); + } else { + MOZ_ASSERT(presContext->GetPresShell()); + MOZ_ASSERT(presContext->GetPresShell()->GetDocument()); + NS_ASSERTION( + presContext->GetPresShell()->GetDocument()->IsInDesignMode(), + "Should be observing entire the document only in the design mode"); + } + } +#endif // #ifdef DEBUG + return isObserving; +} + +IMEContentObserver::State IMEContentObserver::GetState() const { + if (!mSelection || !mRootElement || !mEditableNode) { + return eState_NotObserving; // failed to initialize or finalized. + } + if (!mRootElement->IsInComposedDoc()) { + // the focused editor has already been reframed. + return eState_StoppedObserving; + } + return mIsObserving ? eState_Observing : eState_Initializing; +} + +bool IMEContentObserver::IsObservingContent(const nsPresContext& aPresContext, + const Element* aElement) const { + return mEditableNode == + IMEStateManager::GetRootEditableNode(aPresContext, aElement); +} + +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. + if (NS_WARN_IF(!mEditorBase)) { + return false; + } + return mEditorBase->IsIMEComposing(); +} + +nsresult IMEContentObserver::GetSelectionAndRoot(Selection** aSelection, + Element** aRootElement) const { + if (!mEditableNode || !mSelection) { + return NS_ERROR_NOT_AVAILABLE; + } + + NS_ASSERTION(mSelection && mRootElement, "uninitialized content observer"); + NS_ADDREF(*aSelection = mSelection); + NS_ADDREF(*aRootElement = mRootElement); + return NS_OK; +} + +void IMEContentObserver::OnSelectionChange(Selection& aSelection) { + if (!mIsObserving) { + return; + } + + if (mWidget) { + bool causedByComposition = IsEditorHandlingEventForComposition(); + bool causedBySelectionEvent = TextComposition::IsHandlingSelectionEvent(); + bool duringComposition = IsEditorComposing(); + MaybeNotifyIMEOfSelectionChange(causedByComposition, causedBySelectionEvent, + duringComposition); + } +} + +void IMEContentObserver::ScrollPositionChanged() { + if (!NeedsPositionChangeNotification()) { + return; + } + + MaybeNotifyIMEOfPositionChange(); +} + +NS_IMETHODIMP +IMEContentObserver::Reflow(DOMHighResTimeStamp aStart, + DOMHighResTimeStamp aEnd) { + if (!NeedsPositionChangeNotification()) { + return NS_OK; + } + + MaybeNotifyIMEOfPositionChange(); + return NS_OK; +} + +NS_IMETHODIMP +IMEContentObserver::ReflowInterruptible(DOMHighResTimeStamp aStart, + DOMHighResTimeStamp aEnd) { + if (!NeedsPositionChangeNotification()) { + return NS_OK; + } + + 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. + const bool isSelectionCacheAvailable = aEvent->mUseNativeLineBreak && + mSelectionData.IsInitialized() && + !mNeedsToNotifyIMEOfSelectionChange; + if (isSelectionCacheAvailable && aEvent->mMessage == eQuerySelectedText && + aEvent->mInput.mSelectionType == SelectionType::eNormal) { + aEvent->EmplaceReply(); + if (mSelectionData.HasRange()) { + aEvent->mReply->mOffsetAndData.emplace(mSelectionData.mOffset, + mSelectionData.String(), + OffsetAndDataFor::SelectedString); + aEvent->mReply->mReversed = mSelectionData.mReversed; + } + aEvent->mReply->mContentsRoot = mRootElement; + aEvent->mReply->mWritingMode = mSelectionData.GetWritingMode(); + // The selection cache in IMEContentObserver must always have been in + // an editing host (or an editable anonymous <div> element). Therefore, + // we set mIsEditableContent to true here even though it's already been + // blurred or changed its editable state but the selection cache has not + // been invalidated yet. + aEvent->mReply->mIsEditableContent = true; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p HandleQueryContentEvent(aEvent={ " + "mMessage=%s, mReply=%s })", + this, ToChar(aEvent->mMessage), ToString(aEvent->mReply).c_str())); + return NS_OK; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p 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 && mSelectionData.HasRange()) { + const 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->mReply.reset(); + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p HandleQueryContentEvent(), WARNING, " + "IMEContentObserver has been destroyed during the query, " + "making the query fail", + this)); + return rv; + } + + if (aEvent->Succeeded() && + NS_WARN_IF(aEvent->mReply->mContentsRoot != mRootElement)) { + // Focus has changed unexpectedly, so make the query fail. + aEvent->mReply.reset(); + } + return rv; +} + +nsresult IMEContentObserver::MaybeHandleSelectionEvent( + nsPresContext* aPresContext, WidgetSelectionEvent* aEvent) { + MOZ_ASSERT(aEvent); + MOZ_ASSERT(aEvent->mMessage == eSetSelection); + NS_ASSERTION(!mNeedsToNotifyIMEOfSelectionChange, + "Selection cache has not been updated yet"); + + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p MaybeHandleSelectionEvent(aEvent={ " + "mMessage=%s, mOffset=%u, mLength=%u, mReversed=%s, " + "mExpandToClusterBoundary=%s, mUseNativeLineBreak=%s }), " + "mSelectionData=%s", + this, ToChar(aEvent->mMessage), aEvent->mOffset, aEvent->mLength, + ToChar(aEvent->mReversed), ToChar(aEvent->mExpandToClusterBoundary), + ToChar(aEvent->mUseNativeLineBreak), ToString(mSelectionData).c_str())); + + // When we have Selection cache, and the caller wants to set same selection + // range, we shouldn't try to compute same range because it may be impossible + // if the range boundary is around element boundaries which won't be + // serialized with line breaks like close tags of inline elements. In that + // case, inserting new text at different point may be different from intention + // of users or web apps which set current selection. + // FIXME: We cache only selection data computed with native line breaker + // lengths. Perhaps, we should improve the struct to have both data of + // offset and length. E.g., adding line break counts for both offset and + // length. + if (!mNeedsToNotifyIMEOfSelectionChange && aEvent->mUseNativeLineBreak && + mSelectionData.IsInitialized() && mSelectionData.HasRange() && + mSelectionData.StartOffset() == aEvent->mOffset && + mSelectionData.Length() == aEvent->mLength) { + if (RefPtr<Selection> selection = mSelection) { + selection->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION, + ScrollAxis(), ScrollAxis(), 0); + } + aEvent->mSucceeded = true; + return NS_OK; + } + + ContentEventHandler handler(aPresContext); + return handler.OnSelectionEvent(aEvent); +} + +bool IMEContentObserver::OnMouseButtonEvent(nsPresContext& aPresContext, + WidgetMouseEvent& aMouseEvent) { + if (!mIMENotificationRequests || + !mIMENotificationRequests->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; + } + + WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint, + aMouseEvent.mWidget); + queryCharAtPointEvent.mRefPoint = aMouseEvent.mRefPoint; + ContentEventHandler handler(&aPresContext); + handler.OnQueryCharacterAtPoint(&queryCharAtPointEvent); + if (NS_WARN_IF(queryCharAtPointEvent.Failed()) || + queryCharAtPointEvent.DidNotFindChar()) { + 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) { + queryCharAtPointEvent.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) { + queryCharAtPointEvent.mRefPoint += + aMouseEvent.mWidget->WidgetToScreenOffset() - + mWidget->WidgetToScreenOffset(); + } + + IMENotification notification(NOTIFY_IME_OF_MOUSE_BUTTON_EVENT); + notification.mMouseButtonEventData.mEventMessage = aMouseEvent.mMessage; + notification.mMouseButtonEventData.mOffset = + queryCharAtPointEvent.mReply->StartOffset(); + notification.mMouseButtonEventData.mCursorPos = + queryCharAtPointEvent.mRefPoint; + notification.mMouseButtonEventData.mCharRect = + queryCharAtPointEvent.mReply->mRect; + notification.mMouseButtonEventData.mButton = aMouseEvent.mButton; + notification.mMouseButtonEventData.mButtons = aMouseEvent.mButtons; + 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( + nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { + if (!aContent->IsText()) { + return; // Ignore if it's a comment node or something other invisible data + // node. + } + MOZ_ASSERT(mPreCharacterDataChangeLength < 0, + "CharacterDataChanged() should've reset " + "mPreCharacterDataChangeLength"); + + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aContent)) { + return; + } + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + + // Although we don't assume this change occurs while this is storing + // the range of added consecutive nodes, if it actually happens, we need to + // flush them since this change may occur before or in the range. So, it's + // safe to flush pending computation of mTextChangeData before handling this. + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); + + mPreCharacterDataChangeLength = ContentEventHandler::GetNativeTextLength( + *aContent->AsText(), 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( + nsIContent* aContent, const CharacterDataChangeInfo& aInfo) { + if (!aContent->IsText()) { + return; // Ignore if it's a comment node or something other invisible data + // node. + } + + // Let TextComposition have a change to update composition string range in + // the text node if the change is caused by the web apps. + if (mWidget && !IsEditorHandlingEventForComposition()) { + if (RefPtr<TextComposition> composition = + IMEStateManager::GetTextCompositionFor(mWidget)) { + composition->OnCharacterDataChanged(*aContent->AsText(), aInfo); + } + } + + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aContent)) { + return; + } + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + MOZ_ASSERT( + !HasAddedNodesDuringDocumentChange(), + "The stored range should be flushed before actually the data is changed"); + + int64_t removedLength = mPreCharacterDataChangeLength; + mPreCharacterDataChangeLength = -1; + + MOZ_ASSERT(removedLength >= 0, + "mPreCharacterDataChangeLength should've been set by " + "CharacterDataWillChange()"); + + uint32_t offset = 0; + if (mIsTextControl) { + // If we're observing a text control, mRootElement is the anonymous <div> + // element which has only one text node and/or invisible <br> element. + // TextEditor assumes this structure when it handles editing commands. + // Therefore, it's safe to assume same things here. + MOZ_ASSERT(mRootElement->GetFirstChild() == aContent); + if (aInfo.mChangeStart) { + offset = ContentEventHandler::GetNativeTextLength(*aContent->AsText(), 0, + aInfo.mChangeStart); + } + } else { + nsresult rv = ContentEventHandler::GetFlatTextLengthInRange( + RawNodePosition(mRootElement, 0u), + RawNodePosition(aContent, aInfo.mChangeStart), mRootElement, &offset, + LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + return; + } + } + + uint32_t newLength = ContentEventHandler::GetNativeTextLength( + *aContent->AsText(), 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, + nsIContent* aFirstContent, + nsIContent* aLastContent) { + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aFirstContent)) { + return; + } + + MOZ_ASSERT_IF(aFirstContent, aFirstContent->GetParentNode() == aContainer); + MOZ_ASSERT_IF(aLastContent, aLastContent->GetParentNode() == aContainer); + + mStartOfRemovingTextRangeCache.Clear(); + + // If it's in a document change, nodes are added consecutively. Therefore, + // if we cache the first node and the last node, we need to compute the + // range once. + // FYI: This is not true if the change caused by an operation in the editor. + if (IsInDocumentChange()) { + // Now, mEndOfAddedTextCache may be invalid if node is added before + // the last node in mEndOfAddedTextCache. Clear it. + mEndOfAddedTextCache.Clear(); + if (!HasAddedNodesDuringDocumentChange()) { + mFirstAddedContainer = mLastAddedContainer = aContainer; + mFirstAddedContent = aFirstContent; + mLastAddedContent = aLastContent; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p NotifyContentAdded(), starts to store consecutive added " + "nodes", + this)); + return; + } + // If first node being added is not next node of the last node, + // notify IME of the previous range first, then, restart to cache the + // range. + if (NS_WARN_IF(!IsNextNodeOfLastAddedNode(aContainer, aFirstContent))) { + // Flush the old range first. + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); + mFirstAddedContainer = aContainer; + mFirstAddedContent = aFirstContent; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p NotifyContentAdded(), starts to store consecutive added " + "nodes", + this)); + } + mLastAddedContainer = aContainer; + mLastAddedContent = aLastContent; + return; + } + MOZ_ASSERT(!HasAddedNodesDuringDocumentChange(), + "The cache should be cleared when document change finished"); + + uint32_t offset = 0; + nsresult rv = NS_OK; + if (!mEndOfAddedTextCache.Match(aContainer, + aFirstContent->GetPreviousSibling())) { + mEndOfAddedTextCache.Clear(); + rv = ContentEventHandler::GetFlatTextLengthInRange( + RawNodePosition(mRootElement, 0u), + RawNodePositionBefore(aContainer, + PointBefore(aContainer, aFirstContent)), + mRootElement, &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( + RawNodePositionBefore(aContainer, PointBefore(aContainer, aFirstContent)), + RawNodePosition(aContainer, aLastContent), mRootElement, &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, aLastContent, offset + addingLength); + + if (!addingLength) { + return; + } + + TextChangeData data(offset, offset, offset + addingLength, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); +} + +void IMEContentObserver::ContentAppended(nsIContent* aFirstNewContent) { + nsIContent* parent = aFirstNewContent->GetParent(); + MOZ_ASSERT(parent); + NotifyContentAdded(parent, aFirstNewContent, parent->GetLastChild()); +} + +void IMEContentObserver::ContentInserted(nsIContent* aChild) { + MOZ_ASSERT(aChild); + NotifyContentAdded(aChild->GetParentNode(), aChild, aChild); +} + +void IMEContentObserver::ContentRemoved(nsIContent* aChild, + nsIContent* aPreviousSibling) { + if (!NeedsTextChangeNotification() || + !nsContentUtils::IsInSameAnonymousTree(mRootElement, aChild)) { + return; + } + + mEndOfAddedTextCache.Clear(); + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); + + nsINode* containerNode = aChild->GetParentNode(); + MOZ_ASSERT(containerNode); + + uint32_t offset = 0; + nsresult rv = NS_OK; + if (!mStartOfRemovingTextRangeCache.Match(containerNode, aPreviousSibling)) { + // At removing a child node of aContainer, we need the line break caused + // by open tag of aContainer. Be careful when aPreviousSibling is nullptr. + + rv = ContentEventHandler::GetFlatTextLengthInRange( + RawNodePosition(mRootElement, 0u), + RawNodePosition(containerNode, aPreviousSibling), mRootElement, &offset, + LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + mStartOfRemovingTextRangeCache.Clear(); + return; + } + mStartOfRemovingTextRangeCache.Cache(containerNode, aPreviousSibling, + offset); + } else { + offset = mStartOfRemovingTextRangeCache.mFlatTextLength; + } + + // get offset at the end of the deleted node + uint32_t textLength = 0; + if (const Text* textNode = Text::FromNode(aChild)) { + textLength = ContentEventHandler::GetNativeTextLength(*textNode); + } else { + nsresult rv = ContentEventHandler::GetFlatTextLengthInRange( + RawNodePositionBefore(aChild, 0u), + RawNodePosition(aChild, aChild->GetChildCount()), mRootElement, + &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::ClearAddedNodesDuringDocumentChange() { + mFirstAddedContainer = mLastAddedContainer = nullptr; + mFirstAddedContent = mLastAddedContent = nullptr; + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p ClearAddedNodesDuringDocumentChange(), finished storing " + "consecutive nodes", + this)); +} + +bool IMEContentObserver::IsNextNodeOfLastAddedNode(nsINode* aParent, + nsIContent* aChild) const { + MOZ_ASSERT(aParent); + MOZ_ASSERT(aChild && aChild->GetParentNode() == aParent); + MOZ_ASSERT(mRootElement); + MOZ_ASSERT(HasAddedNodesDuringDocumentChange()); + + // If the parent node isn't changed, we can check that mLastAddedContent has + // aChild as its next sibling. + if (aParent == mLastAddedContainer) { + return !NS_WARN_IF(mLastAddedContent->GetNextSibling() != aChild); + } + + // If the parent node is changed, that means that the recorded last added node + // shouldn't have a sibling. + if (NS_WARN_IF(mLastAddedContent->GetNextSibling())) { + return false; + } + + // If the node is aParent is a descendant of mLastAddedContainer, + // aChild should be the first child in the new container. + if (mLastAddedContainer == aParent->GetParent()) { + return !NS_WARN_IF(aChild->GetPreviousSibling()); + } + + // Otherwise, we need to check it even with slow path. + nsIContent* nextContentOfLastAddedContent = + mLastAddedContent->GetNextNode(mRootElement->GetParentNode()); + if (NS_WARN_IF(!nextContentOfLastAddedContent)) { + return false; + } + if (NS_WARN_IF(nextContentOfLastAddedContent != aChild)) { + return false; + } + return true; +} + +void IMEContentObserver::MaybeNotifyIMEOfAddedTextDuringDocumentChange() { + if (!HasAddedNodesDuringDocumentChange()) { + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p " + "IMEContentObserver::MaybeNotifyIMEOfAddedTextDuringDocumentChange()" + ", flushing stored consecutive nodes", + this)); + + // Notify IME of text change which is caused by added nodes now. + + // First, compute offset of start of first added node from start of the + // editor. + uint32_t offset; + nsresult rv = ContentEventHandler::GetFlatTextLengthInRange( + RawNodePosition(mRootElement, 0u), + RawNodePosition(mFirstAddedContainer, + PointBefore(mFirstAddedContainer, mFirstAddedContent)), + mRootElement, &offset, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + ClearAddedNodesDuringDocumentChange(); + return; + } + + // Next, compute the text length of added nodes. + uint32_t length; + rv = ContentEventHandler::GetFlatTextLengthInRange( + RawNodePosition(mFirstAddedContainer, + PointBefore(mFirstAddedContainer, mFirstAddedContent)), + RawNodePosition(mLastAddedContainer, mLastAddedContent), mRootElement, + &length, LINE_BREAK_TYPE_NATIVE); + if (NS_WARN_IF(NS_FAILED(rv))) { + ClearAddedNodesDuringDocumentChange(); + return; + } + + // Finally, try to notify IME of the range. + TextChangeData data(offset, offset, offset + length, + IsEditorHandlingEventForComposition(), + IsEditorComposing()); + MaybeNotifyIMEOfTextChange(data); + ClearAddedNodesDuringDocumentChange(); +} + +void IMEContentObserver::OnTextControlValueChangedWhileNotObservable( + const nsAString& aNewValue) { + MOZ_ASSERT(mEditorBase); + MOZ_ASSERT(mEditorBase->IsTextEditor()); + if (!mTextControlValueLength && aNewValue.IsEmpty()) { + return; + } + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p OnTextControlValueChangedWhileNotObservable()", this)); + uint32_t newLength = ContentEventHandler::GetNativeTextLength(aNewValue); + TextChangeData data(0, mTextControlValueLength, newLength, false, false); + MaybeNotifyIMEOfTextChange(data); +} + +void IMEContentObserver::BeginDocumentUpdate() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p BeginDocumentUpdate(), HasAddedNodesDuringDocumentChange()=%s", + this, ToChar(HasAddedNodesDuringDocumentChange()))); + + // If we're not in a nested document update, this will return early, + // otherwise, it will handle flusing any changes currently pending before + // entering a nested document update. + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); +} + +void IMEContentObserver::EndDocumentUpdate() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p EndDocumentUpdate(), HasAddedNodesDuringDocumentChange()=%s", + this, ToChar(HasAddedNodesDuringDocumentChange()))); + + MaybeNotifyIMEOfAddedTextDuringDocumentChange(); +} + +void IMEContentObserver::SuppressNotifyingIME() { + mSuppressNotifications++; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p SuppressNotifyingIME(), mSuppressNotifications=%u", this, + mSuppressNotifications)); +} + +void IMEContentObserver::UnsuppressNotifyingIME() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p UnsuppressNotifyingIME(), mSuppressNotifications=%u", this, + mSuppressNotifications)); + + if (!mSuppressNotifications || --mSuppressNotifications) { + return; + } + FlushMergeableNotifications(); +} + +void IMEContentObserver::OnEditActionHandled() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p EditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::BeforeEditAction() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p BeforeEditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); +} + +void IMEContentObserver::CancelEditAction() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p CancelEditAction()", this)); + + mEndOfAddedTextCache.Clear(); + mStartOfRemovingTextRangeCache.Clear(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::PostFocusSetNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostFocusSetNotification()", this)); + + mNeedsToNotifyIMEOfFocusSet = true; +} + +void IMEContentObserver::PostTextChangeNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostTextChangeNotification(mTextChangeData=%s)", this, + ToString(mTextChangeData).c_str())); + + MOZ_ASSERT(mTextChangeData.IsValid(), + "mTextChangeData must have text change data"); + mNeedsToNotifyIMEOfTextChange = true; + // Even if the observer hasn't received selection change, selection in the + // flat text may have already been changed. For example, when previous `<p>` + // element of another `<p>` element which contains caret is removed by a DOM + // mutation, selection change event won't be fired, but selection start offset + // should be decreased by the length of removed `<p>` element. + // In such case, HandleQueryContentEvent shouldn't use the selection cache + // anymore. Therefore, we also need to post selection change notification + // too. eQuerySelectedText event may be dispatched at sending a text change + // notification. + mNeedsToNotifyIMEOfSelectionChange = true; +} + +void IMEContentObserver::PostSelectionChangeNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p 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 MaybeNotifyIMEOfFocusSet()", this)); + + PostFocusSetNotification(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::MaybeNotifyIMEOfTextChange( + const TextChangeDataBase& aTextChangeData) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p MaybeNotifyIMEOfTextChange(aTextChangeData=%s)", this, + ToString(aTextChangeData).c_str())); + + if (mEditorBase && mEditorBase->IsTextEditor()) { + MOZ_DIAGNOSTIC_ASSERT(static_cast<int64_t>(mTextControlValueLength) + + aTextChangeData.Difference() >= + 0); + mTextControlValueLength += aTextChangeData.Difference(); + } + + mTextChangeData += aTextChangeData; + PostTextChangeNotification(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::CancelNotifyingIMEOfTextChange() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p CancelNotifyingIMEOfTextChange()", this)); + mTextChangeData.Clear(); + mNeedsToNotifyIMEOfTextChange = false; +} + +void IMEContentObserver::MaybeNotifyIMEOfSelectionChange( + bool aCausedByComposition, bool aCausedBySelectionEvent, + bool aOccurredDuringComposition) { + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p 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::Verbose, + ("0x%p 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::Verbose, + ("0x%p MaybeNotifyIMEOfPositionChange(), ignored since caused by " + "ContentEventHandler during sending NOTIFY_IME_OF_POSITION_CHANGE", + this)); + return; + } + PostPositionChangeNotification(); + FlushMergeableNotifications(); +} + +void IMEContentObserver::CancelNotifyingIMEOfPositionChange() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p CancelNotifyIMEOfPositionChange()", this)); + mNeedsToNotifyIMEOfPositionChange = false; +} + +void IMEContentObserver::MaybeNotifyCompositionEventHandled() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p MaybeNotifyCompositionEventHandled()", this)); + + PostCompositionEventHandledNotification(); + FlushMergeableNotifications(); +} + +bool IMEContentObserver::UpdateSelectionCache(bool aRequireFlush /* = true */) { + MOZ_ASSERT(IsSafeToNotifyIME()); + + mSelectionData.ClearSelectionData(); + + // XXX Cannot we cache some information for reducing the cost to compute + // selection offset and writing mode? + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + mWidget); + querySelectedTextEvent.mNeedsToFlushLayout = aRequireFlush; + ContentEventHandler handler(GetPresContext()); + handler.OnQuerySelectedText(&querySelectedTextEvent); + if (NS_WARN_IF(querySelectedTextEvent.Failed()) || + NS_WARN_IF(querySelectedTextEvent.mReply->mContentsRoot != + mRootElement)) { + return false; + } + + mFocusedWidget = querySelectedTextEvent.mReply->mFocusedWidget; + mSelectionData.Assign(querySelectedTextEvent); + + // WARNING: Don't set the reason of selection change here because it should be + // set the reason at sending the notification. + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p UpdateSelectionCache(), mSelectionData=%s", this, + ToString(mSelectionData).c_str())); + + return true; +} + +void IMEContentObserver::PostPositionChangeNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostPositionChangeNotification()", this)); + + mNeedsToNotifyIMEOfPositionChange = true; +} + +void IMEContentObserver::PostCompositionEventHandledNotification() { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p PostCompositionEventHandledNotification()", this)); + + mNeedsToNotifyIMEOfCompositionEventHandled = true; +} + +bool IMEContentObserver::IsReflowLocked() const { + nsPresContext* presContext = GetPresContext(); + if (NS_WARN_IF(!presContext)) { + return false; + } + PresShell* 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) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of no widget", + this)); + return false; + } + + // Don't notify IME of anything if it's not good time to do it. + if (mSuppressNotifications) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of no widget", + this)); + return false; + } + + if (!mESM || NS_WARN_IF(!GetPresContext())) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of no " + "EventStateManager and/or PresContext", + this)); + 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()) { + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of reflow locked", + this)); + return false; + } + + // If we're in handling an edit action, this method will be called later. + if (mEditorBase && mEditorBase->IsInEditSubAction()) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IsSafeToNotifyIME(), it's not safe because of focused " + "editor handling somethings", + this)); + return false; + } + + return true; +} + +void IMEContentObserver::FlushMergeableNotifications() { + if (!IsSafeToNotifyIME()) { + // So, if this is already called, this should do nothing. + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p FlushMergeableNotifications(), Warning, do nothing 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::Warning, + ("0x%p FlushMergeableNotifications(), Warning, do nothing due to " + "already flushing pending notifications", + this)); + return; + } + + // If text change notification and/or position change notification becomes + // unnecessary, let's cancel them. + if (mNeedsToNotifyIMEOfTextChange && !NeedsTextChangeNotification()) { + CancelNotifyingIMEOfTextChange(); + } + if (mNeedsToNotifyIMEOfPositionChange && !NeedsPositionChangeNotification()) { + CancelNotifyingIMEOfPositionChange(); + } + + if (!NeedsToNotifyIMEOfSomething()) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p FlushMergeableNotifications(), Warning, due to no pending " + "notifications", + this)); + return; + } + + // NOTE: Reset each pending flag because sending notification may cause + // another change. + + MOZ_LOG( + sIMECOLog, LogLevel::Info, + ("0x%p FlushMergeableNotifications(), creating IMENotificationSender...", + this)); + + // If contents in selection range is modified, the selection range still + // has removed node from the tree. In such case, ContentIterator won't + // work well. Therefore, we shouldn't use AddScriptRunner() 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); + mQueuedSender->Dispatch(mDocShell); + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p FlushMergeableNotifications(), finished", this)); +} + +void IMEContentObserver::TryToFlushPendingNotifications(bool aAllowAsync) { + // If a sender instance is sending notifications, we shouldn't try to create + // a new sender again because the sender will recreate by itself if there are + // new pending notifications. + if (mSendingNotification != NOTIFY_IME_OF_NOTHING) { + return; + } + + // When the caller allows to put off notifying IME, we can wait the next + // call of this method or to run the queued sender. + if (mQueuedSender && XRE_IsContentProcess() && aAllowAsync) { + return; + } + + if (!mQueuedSender) { + // If it was not safe to dispatch notifications when the pending + // notifications are posted, this may not have IMENotificationSender + // instance because it couldn't dispatch it, e.g., when an edit sub-action + // is being handled in the editor, we shouldn't do it even if it's safe to + // run script. Therefore, we need to create the sender instance here in the + // case. + if (!NeedsToNotifyIMEOfSomething()) { + return; + } + mQueuedSender = new IMENotificationSender(this); + } + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p TryToFlushPendingNotifications(), performing queued " + "IMENotificationSender forcibly", + this)); + RefPtr<IMENotificationSender> queuedSender = mQueuedSender; + queuedSender->Run(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::AChangeEvent + ******************************************************************************/ + +bool IMEContentObserver::AChangeEvent::CanNotifyIME( + ChangeEventType aChangeEventType) const { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (NS_WARN_IF(!observer)) { + return false; + } + + const LogLevel debugOrVerbose = + aChangeEventType == ChangeEventType::eChangeEventType_Position + ? LogLevel::Verbose + : LogLevel::Debug; + + if (aChangeEventType == eChangeEventType_CompositionEventHandled) { + if (observer->mWidget) { + return true; + } + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME of " + "composition event handled because of no widget", + this)); + return false; + } + State state = observer->GetState(); + // If it's not initialized, we should do nothing. + if (state == eState_NotObserving) { + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME because " + "of not observing", + this)); + return false; + } + // If setting focus, just check the state. + if (aChangeEventType == eChangeEventType_Focus) { + if (!observer->mIMEHasFocus) { + return true; + } + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME of focus " + "change because of already focused", + this)); + NS_WARNING("IME already has focus"); + return false; + } + // If we've not notified IME of focus yet, we shouldn't notify anything. + if (!observer->mIMEHasFocus) { + MOZ_LOG(sIMECOLog, debugOrVerbose, + ("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME because " + "of not focused", + this)); + return false; + } + + // If IME has focus, IMEContentObserver must hold the widget. + MOZ_ASSERT(observer->mWidget); + + return true; +} + +bool IMEContentObserver::AChangeEvent::IsSafeToNotifyIME( + ChangeEventType aChangeEventType) const { + const LogLevel warningOrVerbose = + aChangeEventType == ChangeEventType::eChangeEventType_Position + ? LogLevel::Verbose + : LogLevel::Warning; + + if (NS_WARN_IF(!nsContentUtils::IsSafeToRunScript())) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of not safe to run script", + this)); + return false; + } + + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of no observer", + this)); + return false; + } + + // While we're sending a notification, we shouldn't send another notification + // recursively. + if (observer->mSendingNotification != NOTIFY_IME_OF_NOTHING) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of the observer sending another notification", + this)); + return false; + } + State state = observer->GetState(); + if (aChangeEventType == eChangeEventType_Focus) { + if (NS_WARN_IF(state != eState_Initializing && state != eState_Observing)) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot " + "notify IME of focus because of not observing", + this)); + return false; + } + } else if (aChangeEventType == eChangeEventType_CompositionEventHandled) { + // It doesn't need to check the observing status. + } else if (state != eState_Observing) { + MOZ_LOG(sIMECOLog, warningOrVerbose, + ("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify " + "IME because of not observing", + this)); + return false; + } + return observer->IsSafeToNotifyIME(); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::IMENotificationSender + ******************************************************************************/ + +void IMEContentObserver::IMENotificationSender::Dispatch( + nsIDocShell* aDocShell) { + if (XRE_IsContentProcess() && aDocShell) { + if (RefPtr<nsPresContext> presContext = aDocShell->GetPresContext()) { + if (nsRefreshDriver* refreshDriver = presContext->RefreshDriver()) { + refreshDriver->AddEarlyRunner(this); + return; + } + } + } + NS_DispatchToCurrentThread(this); +} + +NS_IMETHODIMP +IMEContentObserver::IMENotificationSender::Run() { + if (NS_WARN_IF(mIsRunning)) { + MOZ_LOG( + sIMECOLog, LogLevel::Error, + ("0x%p IMENotificationSender::Run(), FAILED, due to called recursively", + this)); + return NS_OK; + } + + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return NS_OK; + } + + AutoRestore<bool> running(mIsRunning); + mIsRunning = true; + + // This instance was already performed forcibly. + if (observer->mQueuedSender != this) { + return NS_OK; + } + + // NOTE: Reset each pending flag because sending notification may cause + // another change. + + if (observer->mNeedsToNotifyIMEOfFocusSet) { + observer->mNeedsToNotifyIMEOfFocusSet = false; + SendFocusSet(); + observer->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 (observer->mNeedsToNotifyIMEOfFocusSet) { + MOZ_ASSERT(!observer->mIMEHasFocus); + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::Run(), posting " + "IMENotificationSender to current thread", + this)); + observer->mQueuedSender = new IMENotificationSender(observer); + observer->mQueuedSender->Dispatch(observer->mDocShell); + 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. + observer->ClearPendingNotifications(); + return NS_OK; + } + + if (observer->mNeedsToNotifyIMEOfTextChange) { + observer->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 (!observer->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 (observer->mNeedsToNotifyIMEOfSelectionChange) { + observer->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 (!observer->mNeedsToNotifyIMEOfTextChange && + !observer->mNeedsToNotifyIMEOfSelectionChange) { + if (observer->mNeedsToNotifyIMEOfPositionChange) { + observer->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 (!observer->mNeedsToNotifyIMEOfTextChange && + !observer->mNeedsToNotifyIMEOfSelectionChange && + !observer->mNeedsToNotifyIMEOfPositionChange) { + if (observer->mNeedsToNotifyIMEOfCompositionEventHandled) { + observer->mNeedsToNotifyIMEOfCompositionEventHandled = false; + SendCompositionEventHandled(); + } + } + + observer->mQueuedSender = nullptr; + + // If notifications caused some new change, we should notify them now. + if (observer->NeedsToNotifyIMEOfSomething()) { + if (observer->GetState() == eState_StoppedObserving) { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::Run(), waiting " + "IMENotificationSender to be reinitialized", + this)); + } else { + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::Run(), posting " + "IMENotificationSender to current thread", + this)); + observer->mQueuedSender = new IMENotificationSender(observer); + observer->mQueuedSender->Dispatch(observer->mDocShell); + } + } + return NS_OK; +} + +void IMEContentObserver::IMENotificationSender::SendFocusSet() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Focus)) { + // If IMEContentObserver has already gone, we don't need to notify IME of + // focus. + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendFocusSet(), Warning, does not " + "send notification due to impossible to notify IME of focus", + this)); + observer->ClearPendingNotifications(); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Focus)) { + MOZ_LOG( + sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendFocusSet(), Warning, does not send " + "notification due to unsafe, retrying to send NOTIFY_IME_OF_FOCUS...", + this)); + observer->PostFocusSetNotification(); + return; + } + + observer->mIMEHasFocus = true; + // Initialize selection cache with the first selection data. +#ifdef XP_MACOSX + // We need to flush layout only on macOS because character coordinates are + // cached by cocoa with this call, but we don't have a way to update them + // after that. Therefore, we need the latest layout information right now. + observer->UpdateSelectionCache(true); +#else + // We avoid flushing for focus in the general case. + observer->UpdateSelectionCache(false); +#endif // #ifdef XP_MACOSX #else + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendFocusSet(), sending " + "NOTIFY_IME_OF_FOCUS...", + this)); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_FOCUS; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_FOCUS), + observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + // IMENotificationRequests 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. + observer->OnIMEReceivedFocus(); + + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendFocusSet(), sent NOTIFY_IME_OF_FOCUS", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendSelectionChange() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendSelectionChange(), Warning, " + "does not send notification due to impossible to notify IME of " + "selection change", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Selection)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendSelectionChange(), Warning, " + "does not send notification due to unsafe, retrying to send " + "NOTIFY_IME_OF_SELECTION_CHANGE...", + this)); + observer->PostSelectionChangeNotification(); + return; + } + + SelectionChangeData lastSelChangeData = observer->mSelectionData; + if (NS_WARN_IF(!observer->UpdateSelectionCache())) { + MOZ_LOG(sIMECOLog, LogLevel::Error, + ("0x%p 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::Error, + ("0x%p 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 = observer->mSelectionData; + if (lastSelChangeData.IsInitialized() && + lastSelChangeData.EqualsRangeAndDirectionAndWritingMode( + newSelChangeData)) { + MOZ_LOG( + sIMECOLog, LogLevel::Debug, + ("0x%p 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 IMENotificationSender::SendSelectionChange(), sending " + "NOTIFY_IME_OF_SELECTION_CHANGE... newSelChangeData=%s", + this, ToString(newSelChangeData).c_str())); + + IMENotification notification(NOTIFY_IME_OF_SELECTION_CHANGE); + notification.SetData(observer->mSelectionData); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_SELECTION_CHANGE; + IMEStateManager::NotifyIME(notification, observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendSelectionChange(), sent " + "NOTIFY_IME_OF_SELECTION_CHANGE", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendTextChange() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Text)) { + MOZ_LOG( + sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendTextChange(), Warning, does not " + "send notification due to impossible to notify IME of text change", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Text)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendTextChange(), Warning, does " + "not send notification due to unsafe, retrying to send " + "NOTIFY_IME_OF_TEXT_CHANGE...", + this)); + observer->PostTextChangeNotification(); + return; + } + + // If text change notification is unnecessary anymore, just cancel it. + if (!observer->NeedsTextChangeNotification()) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendTextChange(), Warning, " + "canceling sending NOTIFY_IME_OF_TEXT_CHANGE", + this)); + observer->CancelNotifyingIMEOfTextChange(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendTextChange(), sending " + "NOTIFY_IME_OF_TEXT_CHANGE... mIMEContentObserver={ " + "mTextChangeData=%s }", + this, ToString(observer->mTextChangeData).c_str())); + + IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE); + notification.SetData(observer->mTextChangeData); + observer->mTextChangeData.Clear(); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_TEXT_CHANGE; + IMEStateManager::NotifyIME(notification, observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendTextChange(), sent " + "NOTIFY_IME_OF_TEXT_CHANGE", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendPositionChange() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_Position)) { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p IMENotificationSender::SendPositionChange(), Warning, " + "does not send notification due to impossible to notify IME of " + "position change", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_Position)) { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p IMENotificationSender::SendPositionChange(), Warning, " + "does not send notification due to unsafe, retrying to send " + "NOTIFY_IME_OF_POSITION_CHANGE...", + this)); + observer->PostPositionChangeNotification(); + return; + } + + // If position change notification is unnecessary anymore, just cancel it. + if (!observer->NeedsPositionChangeNotification()) { + MOZ_LOG(sIMECOLog, LogLevel::Verbose, + ("0x%p IMENotificationSender::SendPositionChange(), Warning, " + "canceling sending NOTIFY_IME_OF_POSITION_CHANGE", + this)); + observer->CancelNotifyingIMEOfPositionChange(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendPositionChange(), sending " + "NOTIFY_IME_OF_POSITION_CHANGE...", + this)); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_POSITION_CHANGE; + IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_POSITION_CHANGE), + observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendPositionChange(), sent " + "NOTIFY_IME_OF_POSITION_CHANGE", + this)); +} + +void IMEContentObserver::IMENotificationSender::SendCompositionEventHandled() { + RefPtr<IMEContentObserver> observer = GetObserver(); + if (!observer) { + return; + } + + if (!CanNotifyIME(eChangeEventType_CompositionEventHandled)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), " + "Warning, does not send notification due to impossible to notify " + "IME of composition event handled", + this)); + return; + } + + if (!IsSafeToNotifyIME(eChangeEventType_CompositionEventHandled)) { + MOZ_LOG(sIMECOLog, LogLevel::Warning, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), " + "Warning, does not send notification due to unsafe, retrying to " + "send NOTIFY_IME_OF_POSITION_CHANGE...", + this)); + observer->PostCompositionEventHandledNotification(); + return; + } + + MOZ_LOG(sIMECOLog, LogLevel::Info, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), sending " + "NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED...", + this)); + + MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING); + observer->mSendingNotification = NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED; + IMEStateManager::NotifyIME( + IMENotification(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED), + observer->mWidget); + observer->mSendingNotification = NOTIFY_IME_OF_NOTHING; + + MOZ_LOG(sIMECOLog, LogLevel::Debug, + ("0x%p IMENotificationSender::SendCompositionEventHandled(), sent " + "NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED", + this)); +} + +/****************************************************************************** + * mozilla::IMEContentObserver::DocumentObservingHelper + ******************************************************************************/ + +NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver::DocumentObserver) + +NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver::DocumentObserver) + // StopObserving() releases mIMEContentObserver and mDocument. + tmp->StopObserving(); +NS_IMPL_CYCLE_COLLECTION_UNLINK_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IMEContentObserver::DocumentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMEContentObserver) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver::DocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver) + NS_INTERFACE_MAP_ENTRY(nsIMutationObserver) + NS_INTERFACE_MAP_ENTRY(nsISupports) +NS_INTERFACE_MAP_END + +NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver::DocumentObserver) +NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver::DocumentObserver) + +void IMEContentObserver::DocumentObserver::Observe(Document* aDocument) { + MOZ_ASSERT(aDocument); + + // Guarantee that aDocument won't be destroyed during a call of + // StopObserving(). + RefPtr<Document> newDocument = aDocument; + + StopObserving(); + + mDocument = std::move(newDocument); + mDocument->AddObserver(this); +} + +void IMEContentObserver::DocumentObserver::StopObserving() { + if (!IsObserving()) { + return; + } + + // Grab IMEContentObserver which could be destroyed during method calls. + RefPtr<IMEContentObserver> observer = std::move(mIMEContentObserver); + + // Stop observing the document first. + RefPtr<Document> document = std::move(mDocument); + document->RemoveObserver(this); + + // Notify IMEContentObserver of ending of document updates if this already + // notified it of beginning of document updates. + for (; IsUpdating(); --mDocumentUpdating) { + // FYI: IsUpdating() returns true until mDocumentUpdating becomes 0. + // However, IsObserving() returns false now because mDocument was + // already cleared above. Therefore, this method won't be called + // recursively. + observer->EndDocumentUpdate(); + } +} + +void IMEContentObserver::DocumentObserver::Destroy() { + StopObserving(); + mIMEContentObserver = nullptr; +} + +void IMEContentObserver::DocumentObserver::BeginUpdate(Document* aDocument) { + if (NS_WARN_IF(Destroyed()) || NS_WARN_IF(!IsObserving())) { + return; + } + mDocumentUpdating++; + mIMEContentObserver->BeginDocumentUpdate(); +} + +void IMEContentObserver::DocumentObserver::EndUpdate(Document* aDocument) { + if (NS_WARN_IF(Destroyed()) || NS_WARN_IF(!IsObserving()) || + NS_WARN_IF(!IsUpdating())) { + return; + } + mDocumentUpdating--; + mIMEContentObserver->EndDocumentUpdate(); +} + +} // namespace mozilla |