1 /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
2 /* vim: set ts=8 sts=2 et sw=2 tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
7 #include "ContentEventHandler.h"
8 #include "IMEContentObserver.h"
9 #include "IMEStateManager.h"
10 #include "nsContentUtils.h"
11 #include "nsIContent.h"
12 #include "nsIMutationObserver.h"
13 #include "nsPresContext.h"
14 #include "mozilla/AutoRestore.h"
15 #include "mozilla/EditorBase.h"
16 #include "mozilla/EventDispatcher.h"
17 #include "mozilla/IMEStateManager.h"
18 #include "mozilla/IntegerRange.h"
19 #include "mozilla/MiscEvents.h"
20 #include "mozilla/PresShell.h"
21 #include "mozilla/RangeBoundary.h"
22 #include "mozilla/StaticPrefs_dom.h"
23 #include "mozilla/StaticPrefs_intl.h"
24 #include "mozilla/TextComposition.h"
25 #include "mozilla/TextEvents.h"
26 #include "mozilla/Unused.h"
27 #include "mozilla/dom/BrowserParent.h"
30 // Some defiens will be conflict with OSX SDK
31 # define TextRange _TextRange
32 # define TextRangeArray _TextRangeArray
33 # define Comment _Comment
38 # undef TextRangeArray
42 using namespace mozilla::widget
;
46 #define IDEOGRAPHIC_SPACE (u"\x3000"_ns)
48 /******************************************************************************
50 ******************************************************************************/
52 bool TextComposition::sHandlingSelectionEvent
= false;
54 TextComposition::TextComposition(nsPresContext
* aPresContext
, nsINode
* aNode
,
55 BrowserParent
* aBrowserParent
,
56 WidgetCompositionEvent
* aCompositionEvent
)
57 : mPresContext(aPresContext
),
59 mBrowserParent(aBrowserParent
),
60 mNativeContext(aCompositionEvent
->mNativeIMEContext
),
61 mCompositionStartOffset(0),
62 mTargetClauseOffsetInComposition(0),
63 mCompositionStartOffsetInTextNode(UINT32_MAX
),
64 mCompositionLengthInTextNode(UINT32_MAX
),
65 mIsSynthesizedForTests(aCompositionEvent
->mFlags
.mIsSynthesizedForTests
),
67 mIsEditorHandlingEvent(false),
68 mIsRequestingCommit(false),
69 mIsRequestingCancel(false),
70 mRequestedToCommitOrCancel(false),
71 mHasDispatchedDOMTextEvent(false),
72 mHasReceivedCommitEvent(false),
73 mWasNativeCompositionEndEventDiscarded(false),
74 mAllowControlCharacters(
75 StaticPrefs::dom_compositionevent_allow_control_characters()),
76 mWasCompositionStringEmpty(true) {
77 MOZ_ASSERT(aCompositionEvent
->mNativeIMEContext
.IsValid());
80 void TextComposition::Destroy() {
81 mPresContext
= nullptr;
83 mBrowserParent
= nullptr;
84 mContainerTextNode
= nullptr;
85 mCompositionStartOffsetInTextNode
= UINT32_MAX
;
86 mCompositionLengthInTextNode
= UINT32_MAX
;
87 // TODO: If the editor is still alive and this is held by it, we should tell
88 // this being destroyed for cleaning up the stuff.
91 void TextComposition::OnCharacterDataChanged(
92 Text
& aText
, const CharacterDataChangeInfo
& aInfo
) {
93 if (mContainerTextNode
!= &aText
||
94 mCompositionStartOffsetInTextNode
== UINT32_MAX
||
95 mCompositionLengthInTextNode
== UINT32_MAX
) {
99 // Ignore changes after composition string.
100 if (aInfo
.mChangeStart
>=
101 mCompositionStartOffsetInTextNode
+ mCompositionLengthInTextNode
) {
105 // If the change ends before the composition string, we need only to adjust
107 if (aInfo
.mChangeEnd
<= mCompositionStartOffsetInTextNode
) {
108 MOZ_ASSERT(aInfo
.LengthOfRemovedText() <=
109 mCompositionStartOffsetInTextNode
);
110 mCompositionStartOffsetInTextNode
-= aInfo
.LengthOfRemovedText();
111 mCompositionStartOffsetInTextNode
+= aInfo
.mReplaceLength
;
115 // If this is caused by a splitting text node, the composition string
116 // may be split out to the new right node. In the case,
117 // CompositionTransaction::DoTransaction handles it with warking the
118 // following text nodes. Therefore, we should NOT shrink the composing
119 // range for avoind breaking the fix of bug 1310912. Although the handling
120 // looks buggy so that we need to move the handling into here later.
121 if (aInfo
.mDetails
&&
122 aInfo
.mDetails
->mType
== CharacterDataChangeInfo::Details::eSplit
) {
126 // If the change removes/replaces the last character of the composition
127 // string, we should shrink the composition range before the change start.
128 // Then, the replace string will be never updated by coming composition
130 if (aInfo
.mChangeEnd
>=
131 mCompositionStartOffsetInTextNode
+ mCompositionLengthInTextNode
) {
132 // If deleting the first character of the composition string, collapse IME
133 // selection temporarily. Updating composition string will insert new
134 // composition string there.
135 if (aInfo
.mChangeStart
<= mCompositionStartOffsetInTextNode
) {
136 mCompositionStartOffsetInTextNode
= aInfo
.mChangeStart
;
137 mCompositionLengthInTextNode
= 0u;
140 // If some characters in the composition still stay, composition range
141 // should be shrunken.
142 MOZ_ASSERT(aInfo
.mChangeStart
> mCompositionStartOffsetInTextNode
);
143 mCompositionLengthInTextNode
=
144 aInfo
.mChangeStart
- mCompositionStartOffsetInTextNode
;
148 // If removed range starts in the composition string, we need only adjust
149 // the length to make composition range contain the replace string.
150 if (aInfo
.mChangeStart
>= mCompositionStartOffsetInTextNode
) {
151 MOZ_ASSERT(aInfo
.LengthOfRemovedText() <= mCompositionLengthInTextNode
);
152 mCompositionLengthInTextNode
-= aInfo
.LengthOfRemovedText();
153 mCompositionLengthInTextNode
+= aInfo
.mReplaceLength
;
157 // If preceding characers of the composition string is also removed, new
158 // composition start will be there and new composition ends at current
160 const uint32_t removedLengthInCompositionString
=
161 aInfo
.mChangeEnd
- mCompositionStartOffsetInTextNode
;
162 mCompositionStartOffsetInTextNode
= aInfo
.mChangeStart
;
163 mCompositionLengthInTextNode
-= removedLengthInCompositionString
;
164 mCompositionLengthInTextNode
+= aInfo
.mReplaceLength
;
167 bool TextComposition::IsValidStateForComposition(nsIWidget
* aWidget
) const {
168 return !Destroyed() && aWidget
&& !aWidget
->Destroyed() &&
169 mPresContext
->GetPresShell() &&
170 !mPresContext
->PresShell()->IsDestroying();
173 bool TextComposition::MaybeDispatchCompositionUpdate(
174 const WidgetCompositionEvent
* aCompositionEvent
) {
175 MOZ_RELEASE_ASSERT(!mBrowserParent
);
177 if (!IsValidStateForComposition(aCompositionEvent
->mWidget
)) {
181 // Note that we don't need to dispatch eCompositionUpdate event even if
182 // mHasDispatchedDOMTextEvent is false and eCompositionCommit event is
183 // dispatched with empty string immediately after eCompositionStart
184 // because composition string has never been changed from empty string to
185 // non-empty string in such composition even if selected string was not
186 // empty string (mLastData isn't set to selected text when this receives
187 // eCompositionStart).
188 if (mLastData
== aCompositionEvent
->mData
) {
191 CloneAndDispatchAs(aCompositionEvent
, eCompositionUpdate
);
192 return IsValidStateForComposition(aCompositionEvent
->mWidget
);
195 BaseEventFlags
TextComposition::CloneAndDispatchAs(
196 const WidgetCompositionEvent
* aCompositionEvent
, EventMessage aMessage
,
197 nsEventStatus
* aStatus
, EventDispatchingCallback
* aCallBack
) {
198 MOZ_RELEASE_ASSERT(!mBrowserParent
);
200 MOZ_ASSERT(IsValidStateForComposition(aCompositionEvent
->mWidget
),
201 "Should be called only when it's safe to dispatch an event");
203 WidgetCompositionEvent
compositionEvent(aCompositionEvent
->IsTrusted(),
204 aMessage
, aCompositionEvent
->mWidget
);
205 compositionEvent
.mTime
= aCompositionEvent
->mTime
;
206 compositionEvent
.mTimeStamp
= aCompositionEvent
->mTimeStamp
;
207 compositionEvent
.mData
= aCompositionEvent
->mData
;
208 compositionEvent
.mNativeIMEContext
= aCompositionEvent
->mNativeIMEContext
;
209 compositionEvent
.mOriginalMessage
= aCompositionEvent
->mMessage
;
210 compositionEvent
.mFlags
.mIsSynthesizedForTests
=
211 aCompositionEvent
->mFlags
.mIsSynthesizedForTests
;
213 nsEventStatus dummyStatus
= nsEventStatus_eConsumeNoDefault
;
214 nsEventStatus
* status
= aStatus
? aStatus
: &dummyStatus
;
215 if (aMessage
== eCompositionUpdate
) {
216 mLastData
= compositionEvent
.mData
;
217 mLastRanges
= aCompositionEvent
->mRanges
;
220 DispatchEvent(&compositionEvent
, status
, aCallBack
, aCompositionEvent
);
221 return compositionEvent
.mFlags
;
224 void TextComposition::DispatchEvent(
225 WidgetCompositionEvent
* aDispatchEvent
, nsEventStatus
* aStatus
,
226 EventDispatchingCallback
* aCallBack
,
227 const WidgetCompositionEvent
* aOriginalEvent
) {
228 if (aDispatchEvent
->mMessage
== eCompositionChange
) {
229 aDispatchEvent
->mFlags
.mOnlySystemGroupDispatchInContent
= true;
231 RefPtr
<nsINode
> node
= mNode
;
232 RefPtr
<nsPresContext
> presContext
= mPresContext
;
233 EventDispatcher::Dispatch(node
, presContext
, aDispatchEvent
, nullptr, aStatus
,
236 OnCompositionEventDispatched(aDispatchEvent
);
239 void TextComposition::OnCompositionEventDiscarded(
240 WidgetCompositionEvent
* aCompositionEvent
) {
241 // Note that this method is never called for synthesized events for emulating
242 // commit or cancel composition.
244 MOZ_ASSERT(aCompositionEvent
->IsTrusted(),
245 "Shouldn't be called with untrusted event");
247 if (mBrowserParent
) {
248 // The composition event should be discarded in the child process too.
249 Unused
<< mBrowserParent
->SendCompositionEvent(*aCompositionEvent
);
252 // XXX If composition events are discarded, should we dispatch them with
253 // runnable event? However, even if we do so, it might make native IME
254 // confused due to async modification. Especially when native IME is
256 if (!aCompositionEvent
->CausesDOMCompositionEndEvent()) {
260 mWasNativeCompositionEndEventDiscarded
= true;
263 static inline bool IsControlChar(uint32_t aCharCode
) {
264 return aCharCode
< ' ' || aCharCode
== 0x7F;
267 static size_t FindFirstControlCharacter(const nsAString
& aStr
) {
268 const char16_t
* sourceBegin
= aStr
.BeginReading();
269 const char16_t
* sourceEnd
= aStr
.EndReading();
271 for (const char16_t
* source
= sourceBegin
; source
< sourceEnd
; ++source
) {
272 if (*source
!= '\t' && IsControlChar(*source
)) {
273 return source
- sourceBegin
;
280 static void RemoveControlCharactersFrom(nsAString
& aStr
,
281 TextRangeArray
* aRanges
) {
282 size_t firstControlCharOffset
= FindFirstControlCharacter(aStr
);
283 if (firstControlCharOffset
== (size_t)-1) {
287 nsAutoString
copy(aStr
);
288 const char16_t
* sourceBegin
= copy
.BeginReading();
289 const char16_t
* sourceEnd
= copy
.EndReading();
291 char16_t
* dest
= aStr
.BeginWriting();
292 if (NS_WARN_IF(!dest
)) {
296 char16_t
* curDest
= dest
+ firstControlCharOffset
;
297 size_t i
= firstControlCharOffset
;
298 for (const char16_t
* source
= sourceBegin
+ firstControlCharOffset
;
299 source
< sourceEnd
; ++source
) {
300 if (*source
== '\t' || *source
== '\n' || !IsControlChar(*source
)) {
304 } else if (aRanges
) {
305 aRanges
->RemoveCharacter(i
);
309 aStr
.SetLength(curDest
- dest
);
312 nsString
TextComposition::CommitStringIfCommittedAsIs() const {
313 nsString
result(mLastData
);
314 if (!mAllowControlCharacters
) {
315 RemoveControlCharactersFrom(result
, nullptr);
317 if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() &&
318 mLastData
== IDEOGRAPHIC_SPACE
) {
319 return EmptyString();
324 void TextComposition::DispatchCompositionEvent(
325 WidgetCompositionEvent
* aCompositionEvent
, nsEventStatus
* aStatus
,
326 EventDispatchingCallback
* aCallBack
, bool aIsSynthesized
) {
327 mWasCompositionStringEmpty
= mString
.IsEmpty();
329 if (aCompositionEvent
->IsFollowedByCompositionEnd()) {
330 mHasReceivedCommitEvent
= true;
333 // If this instance has requested to commit or cancel composition but
334 // is not synthesizing commit event, that means that the IME commits or
335 // cancels the composition asynchronously. Typically, iBus behaves so.
336 // Then, synthesized events which were dispatched immediately after
337 // the request has already committed our editor's composition string and
338 // told it to web apps. Therefore, we should ignore the delayed events.
339 if (mRequestedToCommitOrCancel
&& !aIsSynthesized
) {
340 *aStatus
= nsEventStatus_eConsumeNoDefault
;
344 // If the content is a container of BrowserParent, composition should be in
345 // the remote process.
346 if (mBrowserParent
) {
347 Unused
<< mBrowserParent
->SendCompositionEvent(*aCompositionEvent
);
348 aCompositionEvent
->StopPropagation();
349 if (aCompositionEvent
->CausesDOMTextEvent()) {
350 mLastData
= aCompositionEvent
->mData
;
351 mLastRanges
= aCompositionEvent
->mRanges
;
352 // Although, the composition event hasn't been actually handled yet,
353 // emulate an editor to be handling the composition event.
354 EditorWillHandleCompositionChangeEvent(aCompositionEvent
);
355 EditorDidHandleCompositionChangeEvent();
360 if (!mAllowControlCharacters
) {
361 RemoveControlCharactersFrom(aCompositionEvent
->mData
,
362 aCompositionEvent
->mRanges
);
364 if (aCompositionEvent
->mMessage
== eCompositionCommitAsIs
) {
365 NS_ASSERTION(!aCompositionEvent
->mRanges
,
366 "mRanges of eCompositionCommitAsIs should be null");
367 aCompositionEvent
->mRanges
= nullptr;
368 NS_ASSERTION(aCompositionEvent
->mData
.IsEmpty(),
369 "mData of eCompositionCommitAsIs should be empty string");
370 if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() &&
371 mLastData
== IDEOGRAPHIC_SPACE
) {
372 // If the last data is an ideographic space (FullWidth space), it might be
373 // a placeholder character of some Chinese IME. So, committing with
374 // this data might not be expected by users. Let's use empty string.
375 aCompositionEvent
->mData
.Truncate();
377 aCompositionEvent
->mData
= mLastData
;
379 } else if (aCompositionEvent
->mMessage
== eCompositionCommit
) {
380 NS_ASSERTION(!aCompositionEvent
->mRanges
,
381 "mRanges of eCompositionCommit should be null");
382 aCompositionEvent
->mRanges
= nullptr;
385 if (!IsValidStateForComposition(aCompositionEvent
->mWidget
)) {
386 *aStatus
= nsEventStatus_eConsumeNoDefault
;
390 // IME may commit composition with empty string for a commit request or
391 // with non-empty string for a cancel request. We should prevent such
392 // unexpected result. E.g., web apps may be confused if they implement
393 // autocomplete which attempts to commit composition forcibly when the user
394 // selects one of suggestions but composition string is cleared by IME.
395 // Note that most Chinese IMEs don't expose actual composition string to us.
396 // They typically tell us an IDEOGRAPHIC SPACE or empty string as composition
397 // string. Therefore, we should hack it only when:
398 // 1. committing string is empty string at requesting commit but the last
399 // data isn't IDEOGRAPHIC SPACE.
400 // 2. non-empty string is committed at requesting cancel.
401 if (!aIsSynthesized
&& (mIsRequestingCommit
|| mIsRequestingCancel
)) {
402 nsString
* committingData
= nullptr;
403 switch (aCompositionEvent
->mMessage
) {
404 case eCompositionEnd
:
405 case eCompositionChange
:
406 case eCompositionCommitAsIs
:
407 case eCompositionCommit
:
408 committingData
= &aCompositionEvent
->mData
;
412 "Unexpected event comes during committing or "
413 "canceling composition");
416 if (committingData
) {
417 if (mIsRequestingCommit
&& committingData
->IsEmpty() &&
418 mLastData
!= IDEOGRAPHIC_SPACE
) {
419 committingData
->Assign(mLastData
);
420 } else if (mIsRequestingCancel
&& !committingData
->IsEmpty()) {
421 committingData
->Truncate();
426 bool dispatchEvent
= true;
427 bool dispatchDOMTextEvent
= aCompositionEvent
->CausesDOMTextEvent();
429 // When mIsComposing is false but the committing string is different from
430 // the last data (E.g., previous eCompositionChange event made the
431 // composition string empty or didn't have clause information), we don't
432 // need to dispatch redundant DOM text event. (But note that we need to
433 // dispatch eCompositionChange event if we have not dispatched
434 // eCompositionChange event yet and commit string replaces selected string
435 // with empty string since selected string hasn't been replaced with empty
437 if (dispatchDOMTextEvent
&&
438 aCompositionEvent
->mMessage
!= eCompositionChange
&& !mIsComposing
&&
439 mHasDispatchedDOMTextEvent
&& mLastData
== aCompositionEvent
->mData
) {
440 dispatchEvent
= dispatchDOMTextEvent
= false;
443 // widget may dispatch redundant eCompositionChange event
444 // which modifies neither composition string, clauses nor caret
445 // position. In such case, we shouldn't dispatch DOM events.
446 if (dispatchDOMTextEvent
&&
447 aCompositionEvent
->mMessage
== eCompositionChange
&&
448 mLastData
== aCompositionEvent
->mData
&& mRanges
&&
449 aCompositionEvent
->mRanges
&&
450 mRanges
->Equals(*aCompositionEvent
->mRanges
)) {
451 dispatchEvent
= dispatchDOMTextEvent
= false;
454 if (dispatchDOMTextEvent
) {
455 if (!MaybeDispatchCompositionUpdate(aCompositionEvent
)) {
461 // If the composition event should cause a DOM text event, we should
462 // overwrite the event message as eCompositionChange because due to
463 // the limitation of mapping between event messages and DOM event types,
464 // we cannot map multiple event messages to a DOM event type.
465 if (dispatchDOMTextEvent
&&
466 aCompositionEvent
->mMessage
!= eCompositionChange
) {
467 mHasDispatchedDOMTextEvent
= true;
468 aCompositionEvent
->mFlags
= CloneAndDispatchAs(
469 aCompositionEvent
, eCompositionChange
, aStatus
, aCallBack
);
471 if (aCompositionEvent
->mMessage
== eCompositionChange
) {
472 mHasDispatchedDOMTextEvent
= true;
474 DispatchEvent(aCompositionEvent
, aStatus
, aCallBack
);
477 *aStatus
= nsEventStatus_eConsumeNoDefault
;
480 if (!IsValidStateForComposition(aCompositionEvent
->mWidget
)) {
484 // Emulate editor behavior of compositionchange event (DOM text event) handler
485 // if no editor handles composition events.
486 if (dispatchDOMTextEvent
&& !HasEditor()) {
487 EditorWillHandleCompositionChangeEvent(aCompositionEvent
);
488 EditorDidHandleCompositionChangeEvent();
491 if (aCompositionEvent
->CausesDOMCompositionEndEvent()) {
492 // Dispatch a compositionend event if it's necessary.
493 if (aCompositionEvent
->mMessage
!= eCompositionEnd
) {
494 CloneAndDispatchAs(aCompositionEvent
, eCompositionEnd
);
496 MOZ_ASSERT(!mIsComposing
, "Why is the editor still composing?");
497 MOZ_ASSERT(!HasEditor(), "Why does the editor still keep to hold this?");
500 MaybeNotifyIMEOfCompositionEventHandled(aCompositionEvent
);
504 void TextComposition::HandleSelectionEvent(
505 nsPresContext
* aPresContext
, BrowserParent
* aBrowserParent
,
506 WidgetSelectionEvent
* aSelectionEvent
) {
507 // If the content is a container of BrowserParent, composition should be in
508 // the remote process.
509 if (aBrowserParent
) {
510 Unused
<< aBrowserParent
->SendSelectionEvent(*aSelectionEvent
);
511 aSelectionEvent
->StopPropagation();
515 ContentEventHandler
handler(aPresContext
);
516 AutoRestore
<bool> saveHandlingSelectionEvent(sHandlingSelectionEvent
);
517 sHandlingSelectionEvent
= true;
518 // XXX During setting selection, a selection listener may change selection
519 // again. In such case, sHandlingSelectionEvent doesn't indicate if
520 // the selection change is caused by a selection event. However, it
521 // must be non-realistic scenario.
522 handler
.OnSelectionEvent(aSelectionEvent
);
525 uint32_t TextComposition::GetSelectionStartOffset() {
526 nsCOMPtr
<nsIWidget
> widget
= mPresContext
->GetRootWidget();
527 WidgetQueryContentEvent
querySelectedTextEvent(true, eQuerySelectedText
,
529 // Due to a bug of widget, mRanges may not be nullptr even though composition
530 // string is empty. So, we need to check it here for avoiding to return
532 if (!mLastData
.IsEmpty() && mRanges
&& mRanges
->HasClauses()) {
533 querySelectedTextEvent
.InitForQuerySelectedText(
534 ToSelectionType(mRanges
->GetFirstClause()->mRangeType
));
536 NS_WARNING_ASSERTION(
537 !mLastData
.IsEmpty() || !mRanges
|| !mRanges
->HasClauses(),
538 "Shouldn't have empty clause info when composition string is empty");
539 querySelectedTextEvent
.InitForQuerySelectedText(SelectionType::eNormal
);
542 // The editor which has this composition is observed by active
543 // IMEContentObserver, we can use the cache of it.
544 RefPtr
<IMEContentObserver
> contentObserver
=
545 IMEStateManager::GetActiveContentObserver();
546 bool doQuerySelection
= true;
547 if (contentObserver
) {
548 if (contentObserver
->IsManaging(this)) {
549 doQuerySelection
= false;
550 contentObserver
->HandleQueryContentEvent(&querySelectedTextEvent
);
552 // If another editor already has focus, we cannot retrieve selection
553 // in the editor which has this composition...
554 else if (NS_WARN_IF(contentObserver
->GetPresContext() == mPresContext
)) {
555 return 0; // XXX Is this okay?
559 // Otherwise, using slow path (i.e., compute every time with
560 // ContentEventHandler)
561 if (doQuerySelection
) {
562 ContentEventHandler
handler(mPresContext
);
563 handler
.HandleQueryContentEvent(&querySelectedTextEvent
);
566 if (NS_WARN_IF(querySelectedTextEvent
.DidNotFindSelection())) {
567 return 0; // XXX Is this okay?
569 return querySelectedTextEvent
.mReply
->AnchorOffset();
572 void TextComposition::OnCompositionEventDispatched(
573 const WidgetCompositionEvent
* aCompositionEvent
) {
574 MOZ_RELEASE_ASSERT(!mBrowserParent
);
576 if (!IsValidStateForComposition(aCompositionEvent
->mWidget
)) {
580 // Every composition event may cause changing composition start offset,
581 // especially when there is no composition string. Therefore, we need to
582 // update mCompositionStartOffset with the latest offset.
584 MOZ_ASSERT(aCompositionEvent
->mMessage
!= eCompositionStart
||
585 mWasCompositionStringEmpty
,
586 "mWasCompositionStringEmpty should be true if the dispatched "
587 "event is eCompositionStart");
589 if (mWasCompositionStringEmpty
&&
590 !aCompositionEvent
->CausesDOMCompositionEndEvent()) {
591 // If there was no composition string, current selection start may be the
592 // offset for inserting composition string.
593 // Update composition start offset with current selection start.
594 mCompositionStartOffset
= GetSelectionStartOffset();
595 mTargetClauseOffsetInComposition
= 0;
598 if (aCompositionEvent
->CausesDOMTextEvent()) {
599 mTargetClauseOffsetInComposition
= aCompositionEvent
->TargetClauseOffset();
603 void TextComposition::OnStartOffsetUpdatedInChild(uint32_t aStartOffset
) {
604 mCompositionStartOffset
= aStartOffset
;
607 void TextComposition::MaybeNotifyIMEOfCompositionEventHandled(
608 const WidgetCompositionEvent
* aCompositionEvent
) {
609 if (aCompositionEvent
->mMessage
!= eCompositionStart
&&
610 !aCompositionEvent
->CausesDOMTextEvent()) {
614 RefPtr
<IMEContentObserver
> contentObserver
=
615 IMEStateManager::GetActiveContentObserver();
616 // When IMEContentObserver is managing the editor which has this composition,
617 // composition event handled notification should be sent after the observer
618 // notifies all pending notifications. Therefore, we should use it.
619 // XXX If IMEContentObserver suddenly loses focus after here and notifying
620 // widget of pending notifications, we won't notify widget of composition
621 // event handled. Although, this is a bug but it should be okay since
622 // destroying IMEContentObserver notifies IME of blur. So, native IME
623 // handler can treat it as this notification too.
624 if (contentObserver
&& contentObserver
->IsManaging(this)) {
625 contentObserver
->MaybeNotifyCompositionEventHandled();
628 // Otherwise, e.g., this composition is in non-active window, we should
629 // notify widget directly.
630 NotifyIME(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED
);
633 void TextComposition::DispatchCompositionEventRunnable(
634 EventMessage aEventMessage
, const nsAString
& aData
,
635 bool aIsSynthesizingCommit
) {
636 nsContentUtils::AddScriptRunner(new CompositionEventDispatcher(
637 this, mNode
, aEventMessage
, aData
, aIsSynthesizingCommit
));
640 nsresult
TextComposition::RequestToCommit(nsIWidget
* aWidget
, bool aDiscard
) {
641 // If this composition is already requested to be committed or canceled,
642 // or has already finished in IME, we don't need to request it again because
643 // request from this instance shouldn't cause committing nor canceling current
644 // composition in IME, and even if the first request failed, new request
645 // won't success, probably. And we shouldn't synthesize events for
646 // committing or canceling composition twice or more times.
647 if (!CanRequsetIMEToCommitOrCancelComposition()) {
651 RefPtr
<TextComposition
> kungFuDeathGrip(this);
652 const nsAutoString
lastData(mLastData
);
654 if (IMEStateManager::CanSendNotificationToWidget()) {
655 AutoRestore
<bool> saveRequestingCancel(mIsRequestingCancel
);
656 AutoRestore
<bool> saveRequestingCommit(mIsRequestingCommit
);
658 mIsRequestingCancel
= true;
659 mIsRequestingCommit
= false;
661 mIsRequestingCancel
= false;
662 mIsRequestingCommit
= true;
664 // FYI: CompositionEvents caused by a call of NotifyIME() may be
665 // discarded by PresShell if it's not safe to dispatch the event.
666 nsresult rv
= aWidget
->NotifyIME(
667 IMENotification(aDiscard
? REQUEST_TO_CANCEL_COMPOSITION
668 : REQUEST_TO_COMMIT_COMPOSITION
));
669 if (NS_WARN_IF(NS_FAILED(rv
))) {
674 mRequestedToCommitOrCancel
= true;
676 // If the request is performed synchronously, this must be already destroyed.
681 // Otherwise, synthesize the commit in content.
682 nsAutoString
data(aDiscard
? EmptyString() : lastData
);
683 if (data
== mLastData
) {
684 DispatchCompositionEventRunnable(eCompositionCommitAsIs
, u
""_ns
, true);
686 DispatchCompositionEventRunnable(eCompositionCommit
, data
, true);
691 nsresult
TextComposition::NotifyIME(IMEMessage aMessage
) {
692 NS_ENSURE_TRUE(mPresContext
, NS_ERROR_NOT_AVAILABLE
);
693 return IMEStateManager::NotifyIME(aMessage
, mPresContext
, mBrowserParent
);
696 void TextComposition::EditorWillHandleCompositionChangeEvent(
697 const WidgetCompositionEvent
* aCompositionChangeEvent
) {
698 mIsComposing
= aCompositionChangeEvent
->IsComposing();
699 mRanges
= aCompositionChangeEvent
->mRanges
;
700 mIsEditorHandlingEvent
= true;
703 mLastData
== aCompositionChangeEvent
->mData
,
704 "The text of a compositionchange event must be same as previous data "
705 "attribute value of the latest compositionupdate event");
708 void TextComposition::OnEditorDestroyed() {
709 MOZ_RELEASE_ASSERT(!mBrowserParent
);
711 MOZ_ASSERT(!mIsEditorHandlingEvent
,
712 "The editor should have stopped listening events");
713 nsCOMPtr
<nsIWidget
> widget
= GetWidget();
714 if (NS_WARN_IF(!widget
)) {
715 // XXX If this could happen, how do we notify IME of destroying the editor?
719 // Try to cancel the composition.
720 RequestToCommit(widget
, true);
723 void TextComposition::EditorDidHandleCompositionChangeEvent() {
725 mIsEditorHandlingEvent
= false;
728 void TextComposition::StartHandlingComposition(EditorBase
* aEditorBase
) {
729 MOZ_RELEASE_ASSERT(!mBrowserParent
);
731 MOZ_ASSERT(!HasEditor(), "There is a handling editor already");
732 mEditorBaseWeak
= do_GetWeakReference(static_cast<nsIEditor
*>(aEditorBase
));
735 void TextComposition::EndHandlingComposition(EditorBase
* aEditorBase
) {
736 MOZ_RELEASE_ASSERT(!mBrowserParent
);
739 RefPtr
<EditorBase
> editorBase
= GetEditorBase();
740 MOZ_ASSERT(!editorBase
|| editorBase
== aEditorBase
,
741 "Another editor handled the composition?");
742 #endif // #ifdef DEBUG
743 mEditorBaseWeak
= nullptr;
746 already_AddRefed
<EditorBase
> TextComposition::GetEditorBase() const {
747 nsCOMPtr
<nsIEditor
> editor
= do_QueryReferent(mEditorBaseWeak
);
748 RefPtr
<EditorBase
> editorBase
= static_cast<EditorBase
*>(editor
.get());
749 return editorBase
.forget();
752 bool TextComposition::HasEditor() const {
753 return mEditorBaseWeak
&& mEditorBaseWeak
->IsAlive();
756 RawRangeBoundary
TextComposition::FirstIMESelectionStartRef() const {
757 RefPtr
<EditorBase
> editorBase
= GetEditorBase();
759 return RawRangeBoundary();
762 nsISelectionController
* selectionController
=
763 editorBase
->GetSelectionController();
764 if (NS_WARN_IF(!selectionController
)) {
765 return RawRangeBoundary();
768 const nsRange
* firstRange
= nullptr;
769 static const SelectionType kIMESelectionTypes
[] = {
770 SelectionType::eIMERawClause
, SelectionType::eIMESelectedRawClause
,
771 SelectionType::eIMEConvertedClause
, SelectionType::eIMESelectedClause
};
772 for (auto selectionType
: kIMESelectionTypes
) {
773 dom::Selection
* selection
=
774 selectionController
->GetSelection(ToRawSelectionType(selectionType
));
778 const uint32_t rangeCount
= selection
->RangeCount();
779 for (const uint32_t i
: IntegerRange(rangeCount
)) {
780 MOZ_ASSERT(selection
->RangeCount() == rangeCount
);
781 const nsRange
* range
= selection
->GetRangeAt(i
);
783 if (MOZ_UNLIKELY(NS_WARN_IF(!range
)) ||
784 MOZ_UNLIKELY(NS_WARN_IF(!range
->GetStartContainer()))) {
791 // In most cases, all composition string should be in same text node.
792 if (firstRange
->GetStartContainer() == range
->GetStartContainer()) {
793 if (firstRange
->StartOffset() > range
->StartOffset()) {
798 // However, if web apps have inserted different nodes in composition
799 // string, composition string may span 2 or more nodes.
800 if (firstRange
->GetStartContainer()->GetNextSibling() ==
801 range
->GetStartContainer()) {
802 // Fast path for some known applications like Google Keep.
806 // Unfortunately, really slow path.
807 // The ranges should always have a common ancestor, hence, be comparable.
808 if (*nsContentUtils::ComparePoints(range
->StartRef(),
809 firstRange
->StartRef()) == -1) {
814 return firstRange
? firstRange
->StartRef().AsRaw() : RawRangeBoundary();
817 RawRangeBoundary
TextComposition::LastIMESelectionEndRef() const {
818 RefPtr
<EditorBase
> editorBase
= GetEditorBase();
820 return RawRangeBoundary();
823 nsISelectionController
* selectionController
=
824 editorBase
->GetSelectionController();
825 if (NS_WARN_IF(!selectionController
)) {
826 return RawRangeBoundary();
829 const nsRange
* lastRange
= nullptr;
830 static const SelectionType kIMESelectionTypes
[] = {
831 SelectionType::eIMERawClause
, SelectionType::eIMESelectedRawClause
,
832 SelectionType::eIMEConvertedClause
, SelectionType::eIMESelectedClause
};
833 for (auto selectionType
: kIMESelectionTypes
) {
834 dom::Selection
* selection
=
835 selectionController
->GetSelection(ToRawSelectionType(selectionType
));
839 const uint32_t rangeCount
= selection
->RangeCount();
840 for (const uint32_t i
: IntegerRange(rangeCount
)) {
841 MOZ_ASSERT(selection
->RangeCount() == rangeCount
);
842 const nsRange
* range
= selection
->GetRangeAt(i
);
844 if (MOZ_UNLIKELY(NS_WARN_IF(!range
)) ||
845 MOZ_UNLIKELY(NS_WARN_IF(!range
->GetEndContainer()))) {
852 // In most cases, all composition string should be in same text node.
853 if (lastRange
->GetEndContainer() == range
->GetEndContainer()) {
854 if (lastRange
->EndOffset() < range
->EndOffset()) {
859 // However, if web apps have inserted different nodes in composition
860 // string, composition string may span 2 or more nodes.
861 if (lastRange
->GetEndContainer() ==
862 range
->GetEndContainer()->GetNextSibling()) {
863 // Fast path for some known applications like Google Keep.
867 // Unfortunately, really slow path.
868 // The ranges should always have a common ancestor, hence, be comparable.
869 if (*nsContentUtils::ComparePoints(lastRange
->EndRef(),
870 range
->EndRef()) == -1) {
875 return lastRange
? lastRange
->EndRef().AsRaw() : RawRangeBoundary();
878 /******************************************************************************
879 * TextComposition::CompositionEventDispatcher
880 ******************************************************************************/
882 TextComposition::CompositionEventDispatcher::CompositionEventDispatcher(
883 TextComposition
* aComposition
, nsINode
* aEventTarget
,
884 EventMessage aEventMessage
, const nsAString
& aData
,
885 bool aIsSynthesizedEvent
)
886 : Runnable("TextComposition::CompositionEventDispatcher"),
887 mTextComposition(aComposition
),
888 mEventTarget(aEventTarget
),
890 mEventMessage(aEventMessage
),
891 mIsSynthesizedEvent(aIsSynthesizedEvent
) {}
894 TextComposition::CompositionEventDispatcher::Run() {
895 // The widget can be different from the widget which has dispatched
896 // composition events because GetWidget() returns a widget which is proper
897 // for calling NotifyIME(). However, this must no be problem since both
898 // widget should share native IME context. Therefore, even if an event
899 // handler uses the widget for requesting IME to commit or cancel, it works.
900 nsCOMPtr
<nsIWidget
> widget(mTextComposition
->GetWidget());
901 if (!mTextComposition
->IsValidStateForComposition(widget
)) {
902 return NS_OK
; // cannot dispatch any events anymore
905 RefPtr
<nsPresContext
> presContext
= mTextComposition
->mPresContext
;
906 nsCOMPtr
<nsINode
> eventTarget
= mEventTarget
;
907 RefPtr
<BrowserParent
> browserParent
= mTextComposition
->mBrowserParent
;
908 nsEventStatus status
= nsEventStatus_eIgnore
;
909 switch (mEventMessage
) {
910 case eCompositionStart
: {
911 WidgetCompositionEvent
compStart(true, eCompositionStart
, widget
);
912 compStart
.mNativeIMEContext
= mTextComposition
->mNativeContext
;
913 WidgetQueryContentEvent
querySelectedTextEvent(true, eQuerySelectedText
,
915 ContentEventHandler
handler(presContext
);
916 handler
.OnQuerySelectedText(&querySelectedTextEvent
);
917 NS_ASSERTION(querySelectedTextEvent
.Succeeded(),
918 "Failed to get selected text");
919 if (querySelectedTextEvent
.FoundSelection()) {
920 compStart
.mData
= querySelectedTextEvent
.mReply
->DataRef();
922 compStart
.mFlags
.mIsSynthesizedForTests
=
923 mTextComposition
->IsSynthesizedForTests();
924 IMEStateManager::DispatchCompositionEvent(
925 eventTarget
, presContext
, browserParent
, &compStart
, &status
, nullptr,
926 mIsSynthesizedEvent
);
929 case eCompositionChange
:
930 case eCompositionCommitAsIs
:
931 case eCompositionCommit
: {
932 WidgetCompositionEvent
compEvent(true, mEventMessage
, widget
);
933 compEvent
.mNativeIMEContext
= mTextComposition
->mNativeContext
;
934 if (mEventMessage
!= eCompositionCommitAsIs
) {
935 compEvent
.mData
= mData
;
937 compEvent
.mFlags
.mIsSynthesizedForTests
=
938 mTextComposition
->IsSynthesizedForTests();
939 IMEStateManager::DispatchCompositionEvent(
940 eventTarget
, presContext
, browserParent
, &compEvent
, &status
, nullptr,
941 mIsSynthesizedEvent
);
945 MOZ_CRASH("Unsupported event");
950 /******************************************************************************
951 * TextCompositionArray
952 ******************************************************************************/
954 TextCompositionArray::index_type
TextCompositionArray::IndexOf(
955 const NativeIMEContext
& aNativeIMEContext
) {
956 if (!aNativeIMEContext
.IsValid()) {
959 for (index_type i
= Length(); i
> 0; --i
) {
960 if (ElementAt(i
- 1)->GetNativeIMEContext() == aNativeIMEContext
) {
967 TextCompositionArray::index_type
TextCompositionArray::IndexOf(
968 nsIWidget
* aWidget
) {
969 return IndexOf(aWidget
->GetNativeIMEContext());
972 TextCompositionArray::index_type
TextCompositionArray::IndexOf(
973 nsPresContext
* aPresContext
) {
974 for (index_type i
= Length(); i
> 0; --i
) {
975 if (ElementAt(i
- 1)->GetPresContext() == aPresContext
) {
982 TextCompositionArray::index_type
TextCompositionArray::IndexOf(
983 nsPresContext
* aPresContext
, nsINode
* aNode
) {
984 index_type index
= IndexOf(aPresContext
);
985 if (index
== NoIndex
) {
988 nsINode
* node
= ElementAt(index
)->GetEventTargetNode();
989 return node
== aNode
? index
: NoIndex
;
992 TextComposition
* TextCompositionArray::GetCompositionFor(nsIWidget
* aWidget
) {
993 index_type i
= IndexOf(aWidget
);
1000 TextComposition
* TextCompositionArray::GetCompositionFor(
1001 const WidgetCompositionEvent
* aCompositionEvent
) {
1002 index_type i
= IndexOf(aCompositionEvent
->mNativeIMEContext
);
1006 return ElementAt(i
);
1009 TextComposition
* TextCompositionArray::GetCompositionFor(
1010 nsPresContext
* aPresContext
) {
1011 index_type i
= IndexOf(aPresContext
);
1015 return ElementAt(i
);
1018 TextComposition
* TextCompositionArray::GetCompositionFor(
1019 nsPresContext
* aPresContext
, nsINode
* aNode
) {
1020 index_type i
= IndexOf(aPresContext
, aNode
);
1024 return ElementAt(i
);
1027 TextComposition
* TextCompositionArray::GetCompositionInContent(
1028 nsPresContext
* aPresContext
, nsIContent
* aContent
) {
1029 // There should be only one composition per content object.
1030 for (index_type i
= Length(); i
> 0; --i
) {
1031 nsINode
* node
= ElementAt(i
- 1)->GetEventTargetNode();
1032 if (node
&& node
->IsInclusiveDescendantOf(aContent
)) {
1033 return ElementAt(i
- 1);
1039 } // namespace mozilla