Bug 1769952 - Fix running raptor on a Win10-64 VM r=sparky
[gecko.git] / dom / events / TextComposition.cpp
blob676da92d217470064cfce6486a9c61faec709a22
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"
29 #ifdef XP_MACOSX
30 // Some defiens will be conflict with OSX SDK
31 # define TextRange _TextRange
32 # define TextRangeArray _TextRangeArray
33 # define Comment _Comment
34 #endif
36 #ifdef XP_MACOSX
37 # undef TextRange
38 # undef TextRangeArray
39 # undef Comment
40 #endif
42 using namespace mozilla::widget;
44 namespace mozilla {
46 #define IDEOGRAPHIC_SPACE (u"\x3000"_ns)
48 /******************************************************************************
49 * TextComposition
50 ******************************************************************************/
52 bool TextComposition::sHandlingSelectionEvent = false;
54 TextComposition::TextComposition(nsPresContext* aPresContext, nsINode* aNode,
55 BrowserParent* aBrowserParent,
56 WidgetCompositionEvent* aCompositionEvent)
57 : mPresContext(aPresContext),
58 mNode(aNode),
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),
66 mIsComposing(false),
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;
82 mNode = 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) {
96 return;
99 // Ignore changes after composition string.
100 if (aInfo.mChangeStart >=
101 mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) {
102 return;
105 // If the change ends before the composition string, we need only to adjust
106 // the start offset.
107 if (aInfo.mChangeEnd <= mCompositionStartOffsetInTextNode) {
108 MOZ_ASSERT(aInfo.LengthOfRemovedText() <=
109 mCompositionStartOffsetInTextNode);
110 mCompositionStartOffsetInTextNode -= aInfo.LengthOfRemovedText();
111 mCompositionStartOffsetInTextNode += aInfo.mReplaceLength;
112 return;
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) {
123 return;
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
129 // updates.
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;
138 return;
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;
145 return;
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;
154 return;
157 // If preceding characers of the composition string is also removed, new
158 // composition start will be there and new composition ends at current
159 // position.
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)) {
178 return false;
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) {
189 return true;
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,
234 aCallBack);
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
255 // TSF.
256 if (!aCompositionEvent->CausesDOMCompositionEndEvent()) {
257 return;
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;
277 return -1;
280 static void RemoveControlCharactersFrom(nsAString& aStr,
281 TextRangeArray* aRanges) {
282 size_t firstControlCharOffset = FindFirstControlCharacter(aStr);
283 if (firstControlCharOffset == (size_t)-1) {
284 return;
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)) {
293 return;
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)) {
301 *curDest = *source;
302 ++curDest;
303 ++i;
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();
321 return result;
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;
341 return;
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();
357 return;
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();
376 } else {
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;
387 return;
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;
409 break;
410 default:
411 NS_WARNING(
412 "Unexpected event comes during committing or "
413 "canceling composition");
414 break;
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
436 // string yet.)
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)) {
456 return;
460 if (dispatchEvent) {
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);
470 } else {
471 if (aCompositionEvent->mMessage == eCompositionChange) {
472 mHasDispatchedDOMTextEvent = true;
474 DispatchEvent(aCompositionEvent, aStatus, aCallBack);
476 } else {
477 *aStatus = nsEventStatus_eConsumeNoDefault;
480 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
481 return;
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);
503 // static
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();
512 return;
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,
528 widget);
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
531 // odd start offset.
532 if (!mLastData.IsEmpty() && mRanges && mRanges->HasClauses()) {
533 querySelectedTextEvent.InitForQuerySelectedText(
534 ToSelectionType(mRanges->GetFirstClause()->mRangeType));
535 } else {
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)) {
577 return;
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()) {
611 return;
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();
626 return;
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()) {
648 return NS_OK;
651 RefPtr<TextComposition> kungFuDeathGrip(this);
652 const nsAutoString lastData(mLastData);
654 if (IMEStateManager::CanSendNotificationToWidget()) {
655 AutoRestore<bool> saveRequestingCancel(mIsRequestingCancel);
656 AutoRestore<bool> saveRequestingCommit(mIsRequestingCommit);
657 if (aDiscard) {
658 mIsRequestingCancel = true;
659 mIsRequestingCommit = false;
660 } else {
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))) {
670 return rv;
674 mRequestedToCommitOrCancel = true;
676 // If the request is performed synchronously, this must be already destroyed.
677 if (Destroyed()) {
678 return NS_OK;
681 // Otherwise, synthesize the commit in content.
682 nsAutoString data(aDiscard ? EmptyString() : lastData);
683 if (data == mLastData) {
684 DispatchCompositionEventRunnable(eCompositionCommitAsIs, u""_ns, true);
685 } else {
686 DispatchCompositionEventRunnable(eCompositionCommit, data, true);
688 return NS_OK;
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;
702 MOZ_ASSERT(
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?
716 return;
719 // Try to cancel the composition.
720 RequestToCommit(widget, true);
723 void TextComposition::EditorDidHandleCompositionChangeEvent() {
724 mString = mLastData;
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);
738 #ifdef DEBUG
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();
758 if (!editorBase) {
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));
775 if (!selection) {
776 continue;
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);
782 MOZ_ASSERT(range);
783 if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
784 MOZ_UNLIKELY(NS_WARN_IF(!range->GetStartContainer()))) {
785 continue;
787 if (!firstRange) {
788 firstRange = range;
789 continue;
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()) {
794 firstRange = range;
796 continue;
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.
803 firstRange = range;
804 continue;
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) {
810 firstRange = range;
814 return firstRange ? firstRange->StartRef().AsRaw() : RawRangeBoundary();
817 RawRangeBoundary TextComposition::LastIMESelectionEndRef() const {
818 RefPtr<EditorBase> editorBase = GetEditorBase();
819 if (!editorBase) {
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));
836 if (!selection) {
837 continue;
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);
843 MOZ_ASSERT(range);
844 if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
845 MOZ_UNLIKELY(NS_WARN_IF(!range->GetEndContainer()))) {
846 continue;
848 if (!lastRange) {
849 lastRange = range;
850 continue;
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()) {
855 lastRange = range;
857 continue;
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.
864 lastRange = range;
865 continue;
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) {
871 lastRange = range;
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),
889 mData(aData),
890 mEventMessage(aEventMessage),
891 mIsSynthesizedEvent(aIsSynthesizedEvent) {}
893 NS_IMETHODIMP
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,
914 widget);
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);
927 break;
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);
942 break;
944 default:
945 MOZ_CRASH("Unsupported event");
947 return NS_OK;
950 /******************************************************************************
951 * TextCompositionArray
952 ******************************************************************************/
954 TextCompositionArray::index_type TextCompositionArray::IndexOf(
955 const NativeIMEContext& aNativeIMEContext) {
956 if (!aNativeIMEContext.IsValid()) {
957 return NoIndex;
959 for (index_type i = Length(); i > 0; --i) {
960 if (ElementAt(i - 1)->GetNativeIMEContext() == aNativeIMEContext) {
961 return i - 1;
964 return NoIndex;
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) {
976 return i - 1;
979 return NoIndex;
982 TextCompositionArray::index_type TextCompositionArray::IndexOf(
983 nsPresContext* aPresContext, nsINode* aNode) {
984 index_type index = IndexOf(aPresContext);
985 if (index == NoIndex) {
986 return 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);
994 if (i == NoIndex) {
995 return nullptr;
997 return ElementAt(i);
1000 TextComposition* TextCompositionArray::GetCompositionFor(
1001 const WidgetCompositionEvent* aCompositionEvent) {
1002 index_type i = IndexOf(aCompositionEvent->mNativeIMEContext);
1003 if (i == NoIndex) {
1004 return nullptr;
1006 return ElementAt(i);
1009 TextComposition* TextCompositionArray::GetCompositionFor(
1010 nsPresContext* aPresContext) {
1011 index_type i = IndexOf(aPresContext);
1012 if (i == NoIndex) {
1013 return nullptr;
1015 return ElementAt(i);
1018 TextComposition* TextCompositionArray::GetCompositionFor(
1019 nsPresContext* aPresContext, nsINode* aNode) {
1020 index_type i = IndexOf(aPresContext, aNode);
1021 if (i == NoIndex) {
1022 return nullptr;
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);
1036 return nullptr;
1039 } // namespace mozilla