/* -*- 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 node = mNode; RefPtr 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; } AutoRestore saveHandlingSelectionEvent(sHandlingSelectionEvent); sHandlingSelectionEvent = true; if (RefPtr contentObserver = IMEStateManager::GetActiveContentObserver()) { contentObserver->MaybeHandleSelectionEvent(aPresContext, aSelectionEvent); return; } ContentEventHandler handler(aPresContext); // 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 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 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 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 kungFuDeathGrip(this); const nsAutoString lastData(mLastData); if (IMEStateManager::CanSendNotificationToWidget()) { AutoRestore saveRequestingCancel(mIsRequestingCancel); AutoRestore 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 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(aEditorBase)); } void TextComposition::EndHandlingComposition(EditorBase* aEditorBase) { MOZ_RELEASE_ASSERT(!mBrowserParent); #ifdef DEBUG RefPtr editorBase = GetEditorBase(); MOZ_ASSERT(!editorBase || editorBase == aEditorBase, "Another editor handled the composition?"); #endif // #ifdef DEBUG mEditorBaseWeak = nullptr; } already_AddRefed TextComposition::GetEditorBase() const { nsCOMPtr editor = do_QueryReferent(mEditorBaseWeak); RefPtr editorBase = static_cast(editor.get()); return editorBase.forget(); } bool TextComposition::HasEditor() const { return mEditorBaseWeak && mEditorBaseWeak->IsAlive(); } RawRangeBoundary TextComposition::FirstIMESelectionStartRef() const { RefPtr 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 = 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 widget(mTextComposition->GetWidget()); if (!mTextComposition->IsValidStateForComposition(widget)) { return NS_OK; // cannot dispatch any events anymore } RefPtr presContext = mTextComposition->mPresContext; nsCOMPtr eventTarget = mEventTarget; RefPtr 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