diff options
Diffstat (limited to '')
-rw-r--r-- | dom/events/TextComposition.cpp | 1038 |
1 files changed, 1038 insertions, 0 deletions
diff --git a/dom/events/TextComposition.cpp b/dom/events/TextComposition.cpp new file mode 100644 index 0000000000..12a375bdb2 --- /dev/null +++ b/dom/events/TextComposition.cpp @@ -0,0 +1,1038 @@ +/* -*- 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 "ContentEventHandler.h" +#include "IMEContentObserver.h" +#include "IMEStateManager.h" +#include "nsContentUtils.h" +#include "nsIContent.h" +#include "nsIMutationObserver.h" +#include "nsPresContext.h" +#include "mozilla/AutoRestore.h" +#include "mozilla/EditorBase.h" +#include "mozilla/EventDispatcher.h" +#include "mozilla/IMEStateManager.h" +#include "mozilla/IntegerRange.h" +#include "mozilla/MiscEvents.h" +#include "mozilla/PresShell.h" +#include "mozilla/RangeBoundary.h" +#include "mozilla/StaticPrefs_dom.h" +#include "mozilla/StaticPrefs_intl.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.h" +#include "mozilla/Unused.h" +#include "mozilla/dom/BrowserParent.h" + +#ifdef XP_MACOSX +// Some defiens will be conflict with OSX SDK +# define TextRange _TextRange +# define TextRangeArray _TextRangeArray +# define Comment _Comment +#endif + +#ifdef XP_MACOSX +# undef TextRange +# undef TextRangeArray +# undef Comment +#endif + +using namespace mozilla::widget; + +namespace mozilla { + +#define IDEOGRAPHIC_SPACE (u"\x3000"_ns) + +/****************************************************************************** + * TextComposition + ******************************************************************************/ + +bool TextComposition::sHandlingSelectionEvent = false; + +TextComposition::TextComposition(nsPresContext* aPresContext, nsINode* aNode, + BrowserParent* aBrowserParent, + WidgetCompositionEvent* aCompositionEvent) + : mPresContext(aPresContext), + mNode(aNode), + mBrowserParent(aBrowserParent), + mNativeContext(aCompositionEvent->mNativeIMEContext), + mCompositionStartOffset(0), + mTargetClauseOffsetInComposition(0), + mCompositionStartOffsetInTextNode(UINT32_MAX), + mCompositionLengthInTextNode(UINT32_MAX), + mIsSynthesizedForTests(aCompositionEvent->mFlags.mIsSynthesizedForTests), + mIsComposing(false), + mIsEditorHandlingEvent(false), + mIsRequestingCommit(false), + mIsRequestingCancel(false), + mRequestedToCommitOrCancel(false), + mHasDispatchedDOMTextEvent(false), + mHasReceivedCommitEvent(false), + mWasNativeCompositionEndEventDiscarded(false), + mAllowControlCharacters( + StaticPrefs::dom_compositionevent_allow_control_characters()), + mWasCompositionStringEmpty(true) { + MOZ_ASSERT(aCompositionEvent->mNativeIMEContext.IsValid()); +} + +void TextComposition::Destroy() { + mPresContext = nullptr; + mNode = nullptr; + mBrowserParent = nullptr; + mContainerTextNode = nullptr; + mCompositionStartOffsetInTextNode = UINT32_MAX; + mCompositionLengthInTextNode = UINT32_MAX; + // TODO: If the editor is still alive and this is held by it, we should tell + // this being destroyed for cleaning up the stuff. +} + +void TextComposition::OnCharacterDataChanged( + Text& aText, const CharacterDataChangeInfo& aInfo) { + if (mContainerTextNode != &aText || + mCompositionStartOffsetInTextNode == UINT32_MAX || + mCompositionLengthInTextNode == UINT32_MAX) { + return; + } + + // Ignore changes after composition string. + if (aInfo.mChangeStart >= + mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) { + return; + } + + // If the change ends before the composition string, we need only to adjust + // the start offset. + if (aInfo.mChangeEnd <= mCompositionStartOffsetInTextNode) { + MOZ_ASSERT(aInfo.LengthOfRemovedText() <= + mCompositionStartOffsetInTextNode); + mCompositionStartOffsetInTextNode -= aInfo.LengthOfRemovedText(); + mCompositionStartOffsetInTextNode += aInfo.mReplaceLength; + return; + } + + // If this is caused by a splitting text node, the composition string + // may be split out to the new right node. In the case, + // CompositionTransaction::DoTransaction handles it with warking the + // following text nodes. Therefore, we should NOT shrink the composing + // range for avoind breaking the fix of bug 1310912. Although the handling + // looks buggy so that we need to move the handling into here later. + if (aInfo.mDetails && + aInfo.mDetails->mType == CharacterDataChangeInfo::Details::eSplit) { + return; + } + + // If the change removes/replaces the last character of the composition + // string, we should shrink the composition range before the change start. + // Then, the replace string will be never updated by coming composition + // updates. + if (aInfo.mChangeEnd >= + mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) { + // If deleting the first character of the composition string, collapse IME + // selection temporarily. Updating composition string will insert new + // composition string there. + if (aInfo.mChangeStart <= mCompositionStartOffsetInTextNode) { + mCompositionStartOffsetInTextNode = aInfo.mChangeStart; + mCompositionLengthInTextNode = 0u; + return; + } + // If some characters in the composition still stay, composition range + // should be shrunken. + MOZ_ASSERT(aInfo.mChangeStart > mCompositionStartOffsetInTextNode); + mCompositionLengthInTextNode = + aInfo.mChangeStart - mCompositionStartOffsetInTextNode; + return; + } + + // If removed range starts in the composition string, we need only adjust + // the length to make composition range contain the replace string. + if (aInfo.mChangeStart >= mCompositionStartOffsetInTextNode) { + MOZ_ASSERT(aInfo.LengthOfRemovedText() <= mCompositionLengthInTextNode); + mCompositionLengthInTextNode -= aInfo.LengthOfRemovedText(); + mCompositionLengthInTextNode += aInfo.mReplaceLength; + return; + } + + // If preceding characers of the composition string is also removed, new + // composition start will be there and new composition ends at current + // position. + const uint32_t removedLengthInCompositionString = + aInfo.mChangeEnd - mCompositionStartOffsetInTextNode; + mCompositionStartOffsetInTextNode = aInfo.mChangeStart; + mCompositionLengthInTextNode -= removedLengthInCompositionString; + mCompositionLengthInTextNode += aInfo.mReplaceLength; +} + +bool TextComposition::IsValidStateForComposition(nsIWidget* aWidget) const { + return !Destroyed() && aWidget && !aWidget->Destroyed() && + mPresContext->GetPresShell() && + !mPresContext->PresShell()->IsDestroying(); +} + +bool TextComposition::MaybeDispatchCompositionUpdate( + const WidgetCompositionEvent* aCompositionEvent) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + return false; + } + + // Note that we don't need to dispatch eCompositionUpdate event even if + // mHasDispatchedDOMTextEvent is false and eCompositionCommit event is + // dispatched with empty string immediately after eCompositionStart + // because composition string has never been changed from empty string to + // non-empty string in such composition even if selected string was not + // empty string (mLastData isn't set to selected text when this receives + // eCompositionStart). + if (mLastData == aCompositionEvent->mData) { + return true; + } + CloneAndDispatchAs(aCompositionEvent, eCompositionUpdate); + return IsValidStateForComposition(aCompositionEvent->mWidget); +} + +BaseEventFlags TextComposition::CloneAndDispatchAs( + const WidgetCompositionEvent* aCompositionEvent, EventMessage aMessage, + nsEventStatus* aStatus, EventDispatchingCallback* aCallBack) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + MOZ_ASSERT(IsValidStateForComposition(aCompositionEvent->mWidget), + "Should be called only when it's safe to dispatch an event"); + + WidgetCompositionEvent compositionEvent(aCompositionEvent->IsTrusted(), + aMessage, aCompositionEvent->mWidget); + compositionEvent.mTimeStamp = aCompositionEvent->mTimeStamp; + compositionEvent.mData = aCompositionEvent->mData; + compositionEvent.mNativeIMEContext = aCompositionEvent->mNativeIMEContext; + compositionEvent.mOriginalMessage = aCompositionEvent->mMessage; + compositionEvent.mFlags.mIsSynthesizedForTests = + aCompositionEvent->mFlags.mIsSynthesizedForTests; + + nsEventStatus dummyStatus = nsEventStatus_eConsumeNoDefault; + nsEventStatus* status = aStatus ? aStatus : &dummyStatus; + if (aMessage == eCompositionUpdate) { + mLastData = compositionEvent.mData; + mLastRanges = aCompositionEvent->mRanges; + } + + DispatchEvent(&compositionEvent, status, aCallBack, aCompositionEvent); + return compositionEvent.mFlags; +} + +void TextComposition::DispatchEvent( + WidgetCompositionEvent* aDispatchEvent, nsEventStatus* aStatus, + EventDispatchingCallback* aCallBack, + const WidgetCompositionEvent* aOriginalEvent) { + if (aDispatchEvent->mMessage == eCompositionChange) { + aDispatchEvent->mFlags.mOnlySystemGroupDispatchInContent = true; + } + RefPtr<nsINode> node = mNode; + RefPtr<nsPresContext> presContext = mPresContext; + EventDispatcher::Dispatch(node, presContext, aDispatchEvent, nullptr, aStatus, + aCallBack); + + OnCompositionEventDispatched(aDispatchEvent); +} + +void TextComposition::OnCompositionEventDiscarded( + WidgetCompositionEvent* aCompositionEvent) { + // Note that this method is never called for synthesized events for emulating + // commit or cancel composition. + + MOZ_ASSERT(aCompositionEvent->IsTrusted(), + "Shouldn't be called with untrusted event"); + + if (mBrowserParent) { + // The composition event should be discarded in the child process too. + Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent); + } + + // XXX If composition events are discarded, should we dispatch them with + // runnable event? However, even if we do so, it might make native IME + // confused due to async modification. Especially when native IME is + // TSF. + if (!aCompositionEvent->CausesDOMCompositionEndEvent()) { + return; + } + + mWasNativeCompositionEndEventDiscarded = true; +} + +static inline bool IsControlChar(uint32_t aCharCode) { + return aCharCode < ' ' || aCharCode == 0x7F; +} + +static size_t FindFirstControlCharacter(const nsAString& aStr) { + const char16_t* sourceBegin = aStr.BeginReading(); + const char16_t* sourceEnd = aStr.EndReading(); + + for (const char16_t* source = sourceBegin; source < sourceEnd; ++source) { + if (*source != '\t' && IsControlChar(*source)) { + return source - sourceBegin; + } + } + + return -1; +} + +static void RemoveControlCharactersFrom(nsAString& aStr, + TextRangeArray* aRanges) { + size_t firstControlCharOffset = FindFirstControlCharacter(aStr); + if (firstControlCharOffset == (size_t)-1) { + return; + } + + nsAutoString copy(aStr); + const char16_t* sourceBegin = copy.BeginReading(); + const char16_t* sourceEnd = copy.EndReading(); + + char16_t* dest = aStr.BeginWriting(); + if (NS_WARN_IF(!dest)) { + return; + } + + char16_t* curDest = dest + firstControlCharOffset; + size_t i = firstControlCharOffset; + for (const char16_t* source = sourceBegin + firstControlCharOffset; + source < sourceEnd; ++source) { + if (*source == '\t' || *source == '\n' || !IsControlChar(*source)) { + *curDest = *source; + ++curDest; + ++i; + } else if (aRanges) { + aRanges->RemoveCharacter(i); + } + } + + aStr.SetLength(curDest - dest); +} + +nsString TextComposition::CommitStringIfCommittedAsIs() const { + nsString result(mLastData); + if (!mAllowControlCharacters) { + RemoveControlCharactersFrom(result, nullptr); + } + if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() && + mLastData == IDEOGRAPHIC_SPACE) { + return EmptyString(); + } + return result; +} + +void TextComposition::DispatchCompositionEvent( + WidgetCompositionEvent* aCompositionEvent, nsEventStatus* aStatus, + EventDispatchingCallback* aCallBack, bool aIsSynthesized) { + mWasCompositionStringEmpty = mString.IsEmpty(); + + if (aCompositionEvent->IsFollowedByCompositionEnd()) { + mHasReceivedCommitEvent = true; + } + + // If this instance has requested to commit or cancel composition but + // is not synthesizing commit event, that means that the IME commits or + // cancels the composition asynchronously. Typically, iBus behaves so. + // Then, synthesized events which were dispatched immediately after + // the request has already committed our editor's composition string and + // told it to web apps. Therefore, we should ignore the delayed events. + if (mRequestedToCommitOrCancel && !aIsSynthesized) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + + // If the content is a container of BrowserParent, composition should be in + // the remote process. + if (mBrowserParent) { + Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent); + aCompositionEvent->StopPropagation(); + if (aCompositionEvent->CausesDOMTextEvent()) { + mLastData = aCompositionEvent->mData; + mLastRanges = aCompositionEvent->mRanges; + // Although, the composition event hasn't been actually handled yet, + // emulate an editor to be handling the composition event. + EditorWillHandleCompositionChangeEvent(aCompositionEvent); + EditorDidHandleCompositionChangeEvent(); + } + return; + } + + if (!mAllowControlCharacters) { + RemoveControlCharactersFrom(aCompositionEvent->mData, + aCompositionEvent->mRanges); + } + if (aCompositionEvent->mMessage == eCompositionCommitAsIs) { + NS_ASSERTION(!aCompositionEvent->mRanges, + "mRanges of eCompositionCommitAsIs should be null"); + aCompositionEvent->mRanges = nullptr; + NS_ASSERTION(aCompositionEvent->mData.IsEmpty(), + "mData of eCompositionCommitAsIs should be empty string"); + if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() && + mLastData == IDEOGRAPHIC_SPACE) { + // If the last data is an ideographic space (FullWidth space), it might be + // a placeholder character of some Chinese IME. So, committing with + // this data might not be expected by users. Let's use empty string. + aCompositionEvent->mData.Truncate(); + } else { + aCompositionEvent->mData = mLastData; + } + } else if (aCompositionEvent->mMessage == eCompositionCommit) { + NS_ASSERTION(!aCompositionEvent->mRanges, + "mRanges of eCompositionCommit should be null"); + aCompositionEvent->mRanges = nullptr; + } + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + *aStatus = nsEventStatus_eConsumeNoDefault; + return; + } + + // IME may commit composition with empty string for a commit request or + // with non-empty string for a cancel request. We should prevent such + // unexpected result. E.g., web apps may be confused if they implement + // autocomplete which attempts to commit composition forcibly when the user + // selects one of suggestions but composition string is cleared by IME. + // Note that most Chinese IMEs don't expose actual composition string to us. + // They typically tell us an IDEOGRAPHIC SPACE or empty string as composition + // string. Therefore, we should hack it only when: + // 1. committing string is empty string at requesting commit but the last + // data isn't IDEOGRAPHIC SPACE. + // 2. non-empty string is committed at requesting cancel. + if (!aIsSynthesized && (mIsRequestingCommit || mIsRequestingCancel)) { + nsString* committingData = nullptr; + switch (aCompositionEvent->mMessage) { + case eCompositionEnd: + case eCompositionChange: + case eCompositionCommitAsIs: + case eCompositionCommit: + committingData = &aCompositionEvent->mData; + break; + default: + NS_WARNING( + "Unexpected event comes during committing or " + "canceling composition"); + break; + } + if (committingData) { + if (mIsRequestingCommit && committingData->IsEmpty() && + mLastData != IDEOGRAPHIC_SPACE) { + committingData->Assign(mLastData); + } else if (mIsRequestingCancel && !committingData->IsEmpty()) { + committingData->Truncate(); + } + } + } + + bool dispatchEvent = true; + bool dispatchDOMTextEvent = aCompositionEvent->CausesDOMTextEvent(); + + // When mIsComposing is false but the committing string is different from + // the last data (E.g., previous eCompositionChange event made the + // composition string empty or didn't have clause information), we don't + // need to dispatch redundant DOM text event. (But note that we need to + // dispatch eCompositionChange event if we have not dispatched + // eCompositionChange event yet and commit string replaces selected string + // with empty string since selected string hasn't been replaced with empty + // string yet.) + if (dispatchDOMTextEvent && + aCompositionEvent->mMessage != eCompositionChange && !mIsComposing && + mHasDispatchedDOMTextEvent && mLastData == aCompositionEvent->mData) { + dispatchEvent = dispatchDOMTextEvent = false; + } + + // widget may dispatch redundant eCompositionChange event + // which modifies neither composition string, clauses nor caret + // position. In such case, we shouldn't dispatch DOM events. + if (dispatchDOMTextEvent && + aCompositionEvent->mMessage == eCompositionChange && + mLastData == aCompositionEvent->mData && mRanges && + aCompositionEvent->mRanges && + mRanges->Equals(*aCompositionEvent->mRanges)) { + dispatchEvent = dispatchDOMTextEvent = false; + } + + if (dispatchDOMTextEvent) { + if (!MaybeDispatchCompositionUpdate(aCompositionEvent)) { + return; + } + } + + if (dispatchEvent) { + // If the composition event should cause a DOM text event, we should + // overwrite the event message as eCompositionChange because due to + // the limitation of mapping between event messages and DOM event types, + // we cannot map multiple event messages to a DOM event type. + if (dispatchDOMTextEvent && + aCompositionEvent->mMessage != eCompositionChange) { + mHasDispatchedDOMTextEvent = true; + aCompositionEvent->mFlags = CloneAndDispatchAs( + aCompositionEvent, eCompositionChange, aStatus, aCallBack); + } else { + if (aCompositionEvent->mMessage == eCompositionChange) { + mHasDispatchedDOMTextEvent = true; + } + DispatchEvent(aCompositionEvent, aStatus, aCallBack); + } + } else { + *aStatus = nsEventStatus_eConsumeNoDefault; + } + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + return; + } + + // Emulate editor behavior of compositionchange event (DOM text event) handler + // if no editor handles composition events. + if (dispatchDOMTextEvent && !HasEditor()) { + EditorWillHandleCompositionChangeEvent(aCompositionEvent); + EditorDidHandleCompositionChangeEvent(); + } + + if (aCompositionEvent->CausesDOMCompositionEndEvent()) { + // Dispatch a compositionend event if it's necessary. + if (aCompositionEvent->mMessage != eCompositionEnd) { + CloneAndDispatchAs(aCompositionEvent, eCompositionEnd); + } + MOZ_ASSERT(!mIsComposing, "Why is the editor still composing?"); + MOZ_ASSERT(!HasEditor(), "Why does the editor still keep to hold this?"); + } + + MaybeNotifyIMEOfCompositionEventHandled(aCompositionEvent); +} + +// static +void TextComposition::HandleSelectionEvent( + nsPresContext* aPresContext, BrowserParent* aBrowserParent, + WidgetSelectionEvent* aSelectionEvent) { + // If the content is a container of BrowserParent, composition should be in + // the remote process. + if (aBrowserParent) { + Unused << aBrowserParent->SendSelectionEvent(*aSelectionEvent); + aSelectionEvent->StopPropagation(); + return; + } + + ContentEventHandler handler(aPresContext); + AutoRestore<bool> saveHandlingSelectionEvent(sHandlingSelectionEvent); + sHandlingSelectionEvent = true; + // XXX During setting selection, a selection listener may change selection + // again. In such case, sHandlingSelectionEvent doesn't indicate if + // the selection change is caused by a selection event. However, it + // must be non-realistic scenario. + handler.OnSelectionEvent(aSelectionEvent); +} + +uint32_t TextComposition::GetSelectionStartOffset() { + nsCOMPtr<nsIWidget> widget = mPresContext->GetRootWidget(); + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + widget); + // Due to a bug of widget, mRanges may not be nullptr even though composition + // string is empty. So, we need to check it here for avoiding to return + // odd start offset. + if (!mLastData.IsEmpty() && mRanges && mRanges->HasClauses()) { + querySelectedTextEvent.InitForQuerySelectedText( + ToSelectionType(mRanges->GetFirstClause()->mRangeType)); + } else { + NS_WARNING_ASSERTION( + !mLastData.IsEmpty() || !mRanges || !mRanges->HasClauses(), + "Shouldn't have empty clause info when composition string is empty"); + querySelectedTextEvent.InitForQuerySelectedText(SelectionType::eNormal); + } + + // The editor which has this composition is observed by active + // IMEContentObserver, we can use the cache of it. + RefPtr<IMEContentObserver> contentObserver = + IMEStateManager::GetActiveContentObserver(); + bool doQuerySelection = true; + if (contentObserver) { + if (contentObserver->IsManaging(*this)) { + doQuerySelection = false; + contentObserver->HandleQueryContentEvent(&querySelectedTextEvent); + } + // If another editor already has focus, we cannot retrieve selection + // in the editor which has this composition... + else if (NS_WARN_IF(contentObserver->GetPresContext() == mPresContext)) { + return 0; // XXX Is this okay? + } + } + + // Otherwise, using slow path (i.e., compute every time with + // ContentEventHandler) + if (doQuerySelection) { + ContentEventHandler handler(mPresContext); + handler.HandleQueryContentEvent(&querySelectedTextEvent); + } + + if (NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection())) { + return 0; // XXX Is this okay? + } + return querySelectedTextEvent.mReply->AnchorOffset(); +} + +void TextComposition::OnCompositionEventDispatched( + const WidgetCompositionEvent* aCompositionEvent) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { + return; + } + + // Every composition event may cause changing composition start offset, + // especially when there is no composition string. Therefore, we need to + // update mCompositionStartOffset with the latest offset. + + MOZ_ASSERT(aCompositionEvent->mMessage != eCompositionStart || + mWasCompositionStringEmpty, + "mWasCompositionStringEmpty should be true if the dispatched " + "event is eCompositionStart"); + + if (mWasCompositionStringEmpty && + !aCompositionEvent->CausesDOMCompositionEndEvent()) { + // If there was no composition string, current selection start may be the + // offset for inserting composition string. + // Update composition start offset with current selection start. + mCompositionStartOffset = GetSelectionStartOffset(); + mTargetClauseOffsetInComposition = 0; + } + + if (aCompositionEvent->CausesDOMTextEvent()) { + mTargetClauseOffsetInComposition = aCompositionEvent->TargetClauseOffset(); + } +} + +void TextComposition::OnStartOffsetUpdatedInChild(uint32_t aStartOffset) { + mCompositionStartOffset = aStartOffset; +} + +void TextComposition::MaybeNotifyIMEOfCompositionEventHandled( + const WidgetCompositionEvent* aCompositionEvent) { + if (aCompositionEvent->mMessage != eCompositionStart && + !aCompositionEvent->CausesDOMTextEvent()) { + return; + } + + RefPtr<IMEContentObserver> contentObserver = + IMEStateManager::GetActiveContentObserver(); + // When IMEContentObserver is managing the editor which has this composition, + // composition event handled notification should be sent after the observer + // notifies all pending notifications. Therefore, we should use it. + // XXX If IMEContentObserver suddenly loses focus after here and notifying + // widget of pending notifications, we won't notify widget of composition + // event handled. Although, this is a bug but it should be okay since + // destroying IMEContentObserver notifies IME of blur. So, native IME + // handler can treat it as this notification too. + if (contentObserver && contentObserver->IsManaging(*this)) { + contentObserver->MaybeNotifyCompositionEventHandled(); + return; + } + // Otherwise, e.g., this composition is in non-active window, we should + // notify widget directly. + NotifyIME(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED); +} + +void TextComposition::DispatchCompositionEventRunnable( + EventMessage aEventMessage, const nsAString& aData, + bool aIsSynthesizingCommit) { + nsContentUtils::AddScriptRunner(new CompositionEventDispatcher( + this, mNode, aEventMessage, aData, aIsSynthesizingCommit)); +} + +nsresult TextComposition::RequestToCommit(nsIWidget* aWidget, bool aDiscard) { + // If this composition is already requested to be committed or canceled, + // or has already finished in IME, we don't need to request it again because + // request from this instance shouldn't cause committing nor canceling current + // composition in IME, and even if the first request failed, new request + // won't success, probably. And we shouldn't synthesize events for + // committing or canceling composition twice or more times. + if (!CanRequsetIMEToCommitOrCancelComposition()) { + return NS_OK; + } + + RefPtr<TextComposition> kungFuDeathGrip(this); + const nsAutoString lastData(mLastData); + + if (IMEStateManager::CanSendNotificationToWidget()) { + AutoRestore<bool> saveRequestingCancel(mIsRequestingCancel); + AutoRestore<bool> saveRequestingCommit(mIsRequestingCommit); + if (aDiscard) { + mIsRequestingCancel = true; + mIsRequestingCommit = false; + } else { + mIsRequestingCancel = false; + mIsRequestingCommit = true; + } + // FYI: CompositionEvents caused by a call of NotifyIME() may be + // discarded by PresShell if it's not safe to dispatch the event. + nsresult rv = aWidget->NotifyIME( + IMENotification(aDiscard ? REQUEST_TO_CANCEL_COMPOSITION + : REQUEST_TO_COMMIT_COMPOSITION)); + if (NS_WARN_IF(NS_FAILED(rv))) { + return rv; + } + } + + mRequestedToCommitOrCancel = true; + + // If the request is performed synchronously, this must be already destroyed. + if (Destroyed()) { + return NS_OK; + } + + // Otherwise, synthesize the commit in content. + nsAutoString data(aDiscard ? EmptyString() : lastData); + if (data == mLastData) { + DispatchCompositionEventRunnable(eCompositionCommitAsIs, u""_ns, true); + } else { + DispatchCompositionEventRunnable(eCompositionCommit, data, true); + } + return NS_OK; +} + +nsresult TextComposition::NotifyIME(IMEMessage aMessage) { + NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); + return IMEStateManager::NotifyIME(aMessage, mPresContext, mBrowserParent); +} + +void TextComposition::EditorWillHandleCompositionChangeEvent( + const WidgetCompositionEvent* aCompositionChangeEvent) { + mIsComposing = aCompositionChangeEvent->IsComposing(); + mRanges = aCompositionChangeEvent->mRanges; + mIsEditorHandlingEvent = true; + + MOZ_ASSERT( + mLastData == aCompositionChangeEvent->mData, + "The text of a compositionchange event must be same as previous data " + "attribute value of the latest compositionupdate event"); +} + +void TextComposition::OnEditorDestroyed() { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + MOZ_ASSERT(!mIsEditorHandlingEvent, + "The editor should have stopped listening events"); + nsCOMPtr<nsIWidget> widget = GetWidget(); + if (NS_WARN_IF(!widget)) { + // XXX If this could happen, how do we notify IME of destroying the editor? + return; + } + + // Try to cancel the composition. + RequestToCommit(widget, true); +} + +void TextComposition::EditorDidHandleCompositionChangeEvent() { + mString = mLastData; + mIsEditorHandlingEvent = false; +} + +void TextComposition::StartHandlingComposition(EditorBase* aEditorBase) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + + MOZ_ASSERT(!HasEditor(), "There is a handling editor already"); + mEditorBaseWeak = do_GetWeakReference(static_cast<nsIEditor*>(aEditorBase)); +} + +void TextComposition::EndHandlingComposition(EditorBase* aEditorBase) { + MOZ_RELEASE_ASSERT(!mBrowserParent); + +#ifdef DEBUG + RefPtr<EditorBase> editorBase = GetEditorBase(); + MOZ_ASSERT(!editorBase || editorBase == aEditorBase, + "Another editor handled the composition?"); +#endif // #ifdef DEBUG + mEditorBaseWeak = nullptr; +} + +already_AddRefed<EditorBase> TextComposition::GetEditorBase() const { + nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditorBaseWeak); + RefPtr<EditorBase> editorBase = static_cast<EditorBase*>(editor.get()); + return editorBase.forget(); +} + +bool TextComposition::HasEditor() const { + return mEditorBaseWeak && mEditorBaseWeak->IsAlive(); +} + +RawRangeBoundary TextComposition::FirstIMESelectionStartRef() const { + RefPtr<EditorBase> editorBase = GetEditorBase(); + if (!editorBase) { + return RawRangeBoundary(); + } + + nsISelectionController* selectionController = + editorBase->GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + return RawRangeBoundary(); + } + + const nsRange* firstRange = nullptr; + static const SelectionType kIMESelectionTypes[] = { + SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause, + SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause}; + for (auto selectionType : kIMESelectionTypes) { + dom::Selection* selection = + selectionController->GetSelection(ToRawSelectionType(selectionType)); + if (!selection) { + continue; + } + const uint32_t rangeCount = selection->RangeCount(); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(selection->RangeCount() == rangeCount); + const nsRange* range = selection->GetRangeAt(i); + MOZ_ASSERT(range); + if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || + MOZ_UNLIKELY(NS_WARN_IF(!range->GetStartContainer()))) { + continue; + } + if (!firstRange) { + firstRange = range; + continue; + } + // In most cases, all composition string should be in same text node. + if (firstRange->GetStartContainer() == range->GetStartContainer()) { + if (firstRange->StartOffset() > range->StartOffset()) { + firstRange = range; + } + continue; + } + // However, if web apps have inserted different nodes in composition + // string, composition string may span 2 or more nodes. + if (firstRange->GetStartContainer()->GetNextSibling() == + range->GetStartContainer()) { + // Fast path for some known applications like Google Keep. + firstRange = range; + continue; + } + // Unfortunately, really slow path. + // The ranges should always have a common ancestor, hence, be comparable. + if (*nsContentUtils::ComparePoints(range->StartRef(), + firstRange->StartRef()) == -1) { + firstRange = range; + } + } + } + return firstRange ? firstRange->StartRef().AsRaw() : RawRangeBoundary(); +} + +RawRangeBoundary TextComposition::LastIMESelectionEndRef() const { + RefPtr<EditorBase> editorBase = GetEditorBase(); + if (!editorBase) { + return RawRangeBoundary(); + } + + nsISelectionController* selectionController = + editorBase->GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + return RawRangeBoundary(); + } + + const nsRange* lastRange = nullptr; + static const SelectionType kIMESelectionTypes[] = { + SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause, + SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause}; + for (auto selectionType : kIMESelectionTypes) { + dom::Selection* selection = + selectionController->GetSelection(ToRawSelectionType(selectionType)); + if (!selection) { + continue; + } + const uint32_t rangeCount = selection->RangeCount(); + for (const uint32_t i : IntegerRange(rangeCount)) { + MOZ_ASSERT(selection->RangeCount() == rangeCount); + const nsRange* range = selection->GetRangeAt(i); + MOZ_ASSERT(range); + if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || + MOZ_UNLIKELY(NS_WARN_IF(!range->GetEndContainer()))) { + continue; + } + if (!lastRange) { + lastRange = range; + continue; + } + // In most cases, all composition string should be in same text node. + if (lastRange->GetEndContainer() == range->GetEndContainer()) { + if (lastRange->EndOffset() < range->EndOffset()) { + lastRange = range; + } + continue; + } + // However, if web apps have inserted different nodes in composition + // string, composition string may span 2 or more nodes. + if (lastRange->GetEndContainer() == + range->GetEndContainer()->GetNextSibling()) { + // Fast path for some known applications like Google Keep. + lastRange = range; + continue; + } + // Unfortunately, really slow path. + // The ranges should always have a common ancestor, hence, be comparable. + if (*nsContentUtils::ComparePoints(lastRange->EndRef(), + range->EndRef()) == -1) { + lastRange = range; + } + } + } + return lastRange ? lastRange->EndRef().AsRaw() : RawRangeBoundary(); +} + +/****************************************************************************** + * TextComposition::CompositionEventDispatcher + ******************************************************************************/ + +TextComposition::CompositionEventDispatcher::CompositionEventDispatcher( + TextComposition* aComposition, nsINode* aEventTarget, + EventMessage aEventMessage, const nsAString& aData, + bool aIsSynthesizedEvent) + : Runnable("TextComposition::CompositionEventDispatcher"), + mTextComposition(aComposition), + mEventTarget(aEventTarget), + mData(aData), + mEventMessage(aEventMessage), + mIsSynthesizedEvent(aIsSynthesizedEvent) {} + +NS_IMETHODIMP +TextComposition::CompositionEventDispatcher::Run() { + // The widget can be different from the widget which has dispatched + // composition events because GetWidget() returns a widget which is proper + // for calling NotifyIME(). However, this must no be problem since both + // widget should share native IME context. Therefore, even if an event + // handler uses the widget for requesting IME to commit or cancel, it works. + nsCOMPtr<nsIWidget> widget(mTextComposition->GetWidget()); + if (!mTextComposition->IsValidStateForComposition(widget)) { + return NS_OK; // cannot dispatch any events anymore + } + + RefPtr<nsPresContext> presContext = mTextComposition->mPresContext; + nsCOMPtr<nsINode> eventTarget = mEventTarget; + RefPtr<BrowserParent> browserParent = mTextComposition->mBrowserParent; + nsEventStatus status = nsEventStatus_eIgnore; + switch (mEventMessage) { + case eCompositionStart: { + WidgetCompositionEvent compStart(true, eCompositionStart, widget); + compStart.mNativeIMEContext = mTextComposition->mNativeContext; + WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, + widget); + ContentEventHandler handler(presContext); + handler.OnQuerySelectedText(&querySelectedTextEvent); + NS_ASSERTION(querySelectedTextEvent.Succeeded(), + "Failed to get selected text"); + if (querySelectedTextEvent.FoundSelection()) { + compStart.mData = querySelectedTextEvent.mReply->DataRef(); + } + compStart.mFlags.mIsSynthesizedForTests = + mTextComposition->IsSynthesizedForTests(); + IMEStateManager::DispatchCompositionEvent( + eventTarget, presContext, browserParent, &compStart, &status, nullptr, + mIsSynthesizedEvent); + break; + } + case eCompositionChange: + case eCompositionCommitAsIs: + case eCompositionCommit: { + WidgetCompositionEvent compEvent(true, mEventMessage, widget); + compEvent.mNativeIMEContext = mTextComposition->mNativeContext; + if (mEventMessage != eCompositionCommitAsIs) { + compEvent.mData = mData; + } + compEvent.mFlags.mIsSynthesizedForTests = + mTextComposition->IsSynthesizedForTests(); + IMEStateManager::DispatchCompositionEvent( + eventTarget, presContext, browserParent, &compEvent, &status, nullptr, + mIsSynthesizedEvent); + break; + } + default: + MOZ_CRASH("Unsupported event"); + } + return NS_OK; +} + +/****************************************************************************** + * TextCompositionArray + ******************************************************************************/ + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + const NativeIMEContext& aNativeIMEContext) { + if (!aNativeIMEContext.IsValid()) { + return NoIndex; + } + for (index_type i = Length(); i > 0; --i) { + if (ElementAt(i - 1)->GetNativeIMEContext() == aNativeIMEContext) { + return i - 1; + } + } + return NoIndex; +} + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + nsIWidget* aWidget) { + return IndexOf(aWidget->GetNativeIMEContext()); +} + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + nsPresContext* aPresContext) { + for (index_type i = Length(); i > 0; --i) { + if (ElementAt(i - 1)->GetPresContext() == aPresContext) { + return i - 1; + } + } + return NoIndex; +} + +TextCompositionArray::index_type TextCompositionArray::IndexOf( + nsPresContext* aPresContext, nsINode* aNode) { + index_type index = IndexOf(aPresContext); + if (index == NoIndex) { + return NoIndex; + } + nsINode* node = ElementAt(index)->GetEventTargetNode(); + return node == aNode ? index : NoIndex; +} + +TextComposition* TextCompositionArray::GetCompositionFor(nsIWidget* aWidget) { + index_type i = IndexOf(aWidget); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionFor( + const WidgetCompositionEvent* aCompositionEvent) { + index_type i = IndexOf(aCompositionEvent->mNativeIMEContext); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionFor( + nsPresContext* aPresContext) { + index_type i = IndexOf(aPresContext); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionFor( + nsPresContext* aPresContext, nsINode* aNode) { + index_type i = IndexOf(aPresContext, aNode); + if (i == NoIndex) { + return nullptr; + } + return ElementAt(i); +} + +TextComposition* TextCompositionArray::GetCompositionInContent( + nsPresContext* aPresContext, nsIContent* aContent) { + // There should be only one composition per content object. + for (index_type i = Length(); i > 0; --i) { + nsINode* node = ElementAt(i - 1)->GetEventTargetNode(); + if (node && node->IsInclusiveDescendantOf(aContent)) { + return ElementAt(i - 1); + } + } + return nullptr; +} + +} // namespace mozilla |