Bug 1892041 - Part 1: Update test262 features. r=spidermonkey-reviewers,dminor
[gecko.git] / dom / events / TextComposition.cpp
blob8761a8bbba5bd57e1557b43b9f9eded4c98e6e8b
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 static uint32_t GetOrCreateCompositionId(WidgetCompositionEvent* aEvent) {
49 // If we're in the parent process, return new composition ID.
50 if (XRE_IsParentProcess()) {
51 static uint32_t sNextCompositionId = 1u;
52 if (MOZ_UNLIKELY(sNextCompositionId == UINT32_MAX)) {
53 sNextCompositionId = 1u;
55 // FYI: When we send the event to a remote process, TextComposition will
56 // set aEvent->mCompositionId to this value. Therefore, we don't need to
57 // set it here.
58 return sNextCompositionId++;
60 // If aEvent comes from the parent process, the event has composition ID
61 // considered by the parent process. Then, we should use it.
62 // Otherwise, aEvent is synthesized in this process, it won't cross the
63 // process boundary between this process and the parent process. Therefore,
64 // we don't need to set meaningful composition ID for the text composition.
65 return aEvent->mCompositionId;
68 /******************************************************************************
69 * TextComposition
70 ******************************************************************************/
72 bool TextComposition::sHandlingSelectionEvent = false;
74 TextComposition::TextComposition(nsPresContext* aPresContext, nsINode* aNode,
75 BrowserParent* aBrowserParent,
76 WidgetCompositionEvent* aCompositionEvent)
77 : mPresContext(aPresContext),
78 mNode(aNode),
79 mBrowserParent(aBrowserParent),
80 mNativeContext(aCompositionEvent->mNativeIMEContext),
81 mCompositionId(GetOrCreateCompositionId(aCompositionEvent)),
82 mCompositionStartOffset(0),
83 mTargetClauseOffsetInComposition(0),
84 mCompositionStartOffsetInTextNode(UINT32_MAX),
85 mCompositionLengthInTextNode(UINT32_MAX),
86 mIsSynthesizedForTests(aCompositionEvent->mFlags.mIsSynthesizedForTests),
87 mIsComposing(false),
88 mIsEditorHandlingEvent(false),
89 mIsRequestingCommit(false),
90 mIsRequestingCancel(false),
91 mRequestedToCommitOrCancel(false),
92 mHasDispatchedDOMTextEvent(false),
93 mHasReceivedCommitEvent(false),
94 mWasNativeCompositionEndEventDiscarded(false),
95 mAllowControlCharacters(
96 StaticPrefs::dom_compositionevent_allow_control_characters()),
97 mWasCompositionStringEmpty(true) {
98 MOZ_ASSERT(aCompositionEvent->mNativeIMEContext.IsValid());
101 void TextComposition::Destroy() {
102 mPresContext = nullptr;
103 mNode = nullptr;
104 mBrowserParent = nullptr;
105 mContainerTextNode = nullptr;
106 mCompositionStartOffsetInTextNode = UINT32_MAX;
107 mCompositionLengthInTextNode = UINT32_MAX;
108 // TODO: If the editor is still alive and this is held by it, we should tell
109 // this being destroyed for cleaning up the stuff.
112 void TextComposition::OnCharacterDataChanged(
113 Text& aText, const CharacterDataChangeInfo& aInfo) {
114 if (mContainerTextNode != &aText ||
115 mCompositionStartOffsetInTextNode == UINT32_MAX ||
116 mCompositionLengthInTextNode == UINT32_MAX) {
117 return;
120 // Ignore changes after composition string.
121 if (aInfo.mChangeStart >=
122 mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) {
123 return;
126 // If the change ends before the composition string, we need only to adjust
127 // the start offset.
128 if (aInfo.mChangeEnd <= mCompositionStartOffsetInTextNode) {
129 MOZ_ASSERT(aInfo.LengthOfRemovedText() <=
130 mCompositionStartOffsetInTextNode);
131 mCompositionStartOffsetInTextNode -= aInfo.LengthOfRemovedText();
132 mCompositionStartOffsetInTextNode += aInfo.mReplaceLength;
133 return;
136 // If this is caused by a splitting text node, the composition string
137 // may be split out to the new right node. In the case,
138 // CompositionTransaction::DoTransaction handles it with warking the
139 // following text nodes. Therefore, we should NOT shrink the composing
140 // range for avoind breaking the fix of bug 1310912. Although the handling
141 // looks buggy so that we need to move the handling into here later.
142 if (aInfo.mDetails &&
143 aInfo.mDetails->mType == CharacterDataChangeInfo::Details::eSplit) {
144 return;
147 // If the change removes/replaces the last character of the composition
148 // string, we should shrink the composition range before the change start.
149 // Then, the replace string will be never updated by coming composition
150 // updates.
151 if (aInfo.mChangeEnd >=
152 mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) {
153 // If deleting the first character of the composition string, collapse IME
154 // selection temporarily. Updating composition string will insert new
155 // composition string there.
156 if (aInfo.mChangeStart <= mCompositionStartOffsetInTextNode) {
157 mCompositionStartOffsetInTextNode = aInfo.mChangeStart;
158 mCompositionLengthInTextNode = 0u;
159 return;
161 // If some characters in the composition still stay, composition range
162 // should be shrunken.
163 MOZ_ASSERT(aInfo.mChangeStart > mCompositionStartOffsetInTextNode);
164 mCompositionLengthInTextNode =
165 aInfo.mChangeStart - mCompositionStartOffsetInTextNode;
166 return;
169 // If removed range starts in the composition string, we need only adjust
170 // the length to make composition range contain the replace string.
171 if (aInfo.mChangeStart >= mCompositionStartOffsetInTextNode) {
172 MOZ_ASSERT(aInfo.LengthOfRemovedText() <= mCompositionLengthInTextNode);
173 mCompositionLengthInTextNode -= aInfo.LengthOfRemovedText();
174 mCompositionLengthInTextNode += aInfo.mReplaceLength;
175 return;
178 // If preceding characers of the composition string is also removed, new
179 // composition start will be there and new composition ends at current
180 // position.
181 const uint32_t removedLengthInCompositionString =
182 aInfo.mChangeEnd - mCompositionStartOffsetInTextNode;
183 mCompositionStartOffsetInTextNode = aInfo.mChangeStart;
184 mCompositionLengthInTextNode -= removedLengthInCompositionString;
185 mCompositionLengthInTextNode += aInfo.mReplaceLength;
188 bool TextComposition::IsValidStateForComposition(nsIWidget* aWidget) const {
189 return !Destroyed() && aWidget && !aWidget->Destroyed() &&
190 mPresContext->GetPresShell() &&
191 !mPresContext->PresShell()->IsDestroying();
194 bool TextComposition::MaybeDispatchCompositionUpdate(
195 const WidgetCompositionEvent* aCompositionEvent) {
196 MOZ_RELEASE_ASSERT(!mBrowserParent);
198 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
199 return false;
202 // Note that we don't need to dispatch eCompositionUpdate event even if
203 // mHasDispatchedDOMTextEvent is false and eCompositionCommit event is
204 // dispatched with empty string immediately after eCompositionStart
205 // because composition string has never been changed from empty string to
206 // non-empty string in such composition even if selected string was not
207 // empty string (mLastData isn't set to selected text when this receives
208 // eCompositionStart).
209 if (mLastData == aCompositionEvent->mData) {
210 return true;
212 CloneAndDispatchAs(aCompositionEvent, eCompositionUpdate);
213 return IsValidStateForComposition(aCompositionEvent->mWidget);
216 BaseEventFlags TextComposition::CloneAndDispatchAs(
217 const WidgetCompositionEvent* aCompositionEvent, EventMessage aMessage,
218 nsEventStatus* aStatus, EventDispatchingCallback* aCallBack) {
219 MOZ_RELEASE_ASSERT(!mBrowserParent);
221 MOZ_ASSERT(IsValidStateForComposition(aCompositionEvent->mWidget),
222 "Should be called only when it's safe to dispatch an event");
224 WidgetCompositionEvent compositionEvent(aCompositionEvent->IsTrusted(),
225 aMessage, aCompositionEvent->mWidget);
226 compositionEvent.mTimeStamp = aCompositionEvent->mTimeStamp;
227 compositionEvent.mData = aCompositionEvent->mData;
228 compositionEvent.mNativeIMEContext = aCompositionEvent->mNativeIMEContext;
229 compositionEvent.mOriginalMessage = aCompositionEvent->mMessage;
230 compositionEvent.mFlags.mIsSynthesizedForTests =
231 aCompositionEvent->mFlags.mIsSynthesizedForTests;
233 nsEventStatus dummyStatus = nsEventStatus_eConsumeNoDefault;
234 nsEventStatus* status = aStatus ? aStatus : &dummyStatus;
235 if (aMessage == eCompositionUpdate) {
236 mLastData = compositionEvent.mData;
237 mLastRanges = aCompositionEvent->mRanges;
240 DispatchEvent(&compositionEvent, status, aCallBack, aCompositionEvent);
241 return compositionEvent.mFlags;
244 void TextComposition::DispatchEvent(
245 WidgetCompositionEvent* aDispatchEvent, nsEventStatus* aStatus,
246 EventDispatchingCallback* aCallBack,
247 const WidgetCompositionEvent* aOriginalEvent) {
248 if (aDispatchEvent->mMessage == eCompositionChange) {
249 aDispatchEvent->mFlags.mOnlySystemGroupDispatchInContent = true;
251 RefPtr<nsINode> node = mNode;
252 RefPtr<nsPresContext> presContext = mPresContext;
253 EventDispatcher::Dispatch(node, presContext, aDispatchEvent, nullptr, aStatus,
254 aCallBack);
256 OnCompositionEventDispatched(aDispatchEvent);
259 void TextComposition::OnCompositionEventDiscarded(
260 WidgetCompositionEvent* aCompositionEvent) {
261 // Note that this method is never called for synthesized events for emulating
262 // commit or cancel composition.
264 MOZ_ASSERT(aCompositionEvent->IsTrusted(),
265 "Shouldn't be called with untrusted event");
267 if (mBrowserParent) {
268 Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent,
269 mCompositionId);
272 // XXX If composition events are discarded, should we dispatch them with
273 // runnable event? However, even if we do so, it might make native IME
274 // confused due to async modification. Especially when native IME is
275 // TSF.
276 if (!aCompositionEvent->CausesDOMCompositionEndEvent()) {
277 return;
280 mWasNativeCompositionEndEventDiscarded = true;
283 static inline bool IsControlChar(uint32_t aCharCode) {
284 return aCharCode < ' ' || aCharCode == 0x7F;
287 static size_t FindFirstControlCharacter(const nsAString& aStr) {
288 const char16_t* sourceBegin = aStr.BeginReading();
289 const char16_t* sourceEnd = aStr.EndReading();
291 for (const char16_t* source = sourceBegin; source < sourceEnd; ++source) {
292 if (*source != '\t' && IsControlChar(*source)) {
293 return source - sourceBegin;
297 return -1;
300 static void RemoveControlCharactersFrom(nsAString& aStr,
301 TextRangeArray* aRanges) {
302 size_t firstControlCharOffset = FindFirstControlCharacter(aStr);
303 if (firstControlCharOffset == (size_t)-1) {
304 return;
307 nsAutoString copy(aStr);
308 const char16_t* sourceBegin = copy.BeginReading();
309 const char16_t* sourceEnd = copy.EndReading();
311 char16_t* dest = aStr.BeginWriting();
312 if (NS_WARN_IF(!dest)) {
313 return;
316 char16_t* curDest = dest + firstControlCharOffset;
317 size_t i = firstControlCharOffset;
318 for (const char16_t* source = sourceBegin + firstControlCharOffset;
319 source < sourceEnd; ++source) {
320 if (*source == '\t' || *source == '\n' || !IsControlChar(*source)) {
321 *curDest = *source;
322 ++curDest;
323 ++i;
324 } else if (aRanges) {
325 aRanges->RemoveCharacter(i);
329 aStr.SetLength(curDest - dest);
332 nsString TextComposition::CommitStringIfCommittedAsIs() const {
333 nsString result(mLastData);
334 if (!mAllowControlCharacters) {
335 RemoveControlCharactersFrom(result, nullptr);
337 if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() &&
338 mLastData == IDEOGRAPHIC_SPACE) {
339 return EmptyString();
341 return result;
344 void TextComposition::DispatchCompositionEvent(
345 WidgetCompositionEvent* aCompositionEvent, nsEventStatus* aStatus,
346 EventDispatchingCallback* aCallBack, bool aIsSynthesized) {
347 mWasCompositionStringEmpty = mString.IsEmpty();
349 if (aCompositionEvent->IsFollowedByCompositionEnd()) {
350 mHasReceivedCommitEvent = true;
353 // If this instance has requested to commit or cancel composition but
354 // is not synthesizing commit event, that means that the IME commits or
355 // cancels the composition asynchronously. Typically, iBus behaves so.
356 // Then, synthesized events which were dispatched immediately after
357 // the request has already committed our editor's composition string and
358 // told it to web apps. Therefore, we should ignore the delayed events.
359 if (mRequestedToCommitOrCancel && !aIsSynthesized) {
360 *aStatus = nsEventStatus_eConsumeNoDefault;
361 return;
364 // If the content is a container of BrowserParent, composition should be in
365 // the remote process.
366 if (mBrowserParent) {
367 Unused << mBrowserParent->SendCompositionEvent(*aCompositionEvent,
368 mCompositionId);
369 aCompositionEvent->StopPropagation();
370 if (aCompositionEvent->CausesDOMTextEvent()) {
371 mLastData = aCompositionEvent->mData;
372 mLastRanges = aCompositionEvent->mRanges;
373 // Although, the composition event hasn't been actually handled yet,
374 // emulate an editor to be handling the composition event.
375 EditorWillHandleCompositionChangeEvent(aCompositionEvent);
376 EditorDidHandleCompositionChangeEvent();
378 return;
381 if (!mAllowControlCharacters) {
382 RemoveControlCharactersFrom(aCompositionEvent->mData,
383 aCompositionEvent->mRanges);
385 if (aCompositionEvent->mMessage == eCompositionCommitAsIs) {
386 NS_ASSERTION(!aCompositionEvent->mRanges,
387 "mRanges of eCompositionCommitAsIs should be null");
388 aCompositionEvent->mRanges = nullptr;
389 NS_ASSERTION(aCompositionEvent->mData.IsEmpty(),
390 "mData of eCompositionCommitAsIs should be empty string");
391 if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() &&
392 mLastData == IDEOGRAPHIC_SPACE) {
393 // If the last data is an ideographic space (FullWidth space), it might be
394 // a placeholder character of some Chinese IME. So, committing with
395 // this data might not be expected by users. Let's use empty string.
396 aCompositionEvent->mData.Truncate();
397 } else {
398 aCompositionEvent->mData = mLastData;
400 } else if (aCompositionEvent->mMessage == eCompositionCommit) {
401 NS_ASSERTION(!aCompositionEvent->mRanges,
402 "mRanges of eCompositionCommit should be null");
403 aCompositionEvent->mRanges = nullptr;
406 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
407 *aStatus = nsEventStatus_eConsumeNoDefault;
408 return;
411 // IME may commit composition with empty string for a commit request or
412 // with non-empty string for a cancel request. We should prevent such
413 // unexpected result. E.g., web apps may be confused if they implement
414 // autocomplete which attempts to commit composition forcibly when the user
415 // selects one of suggestions but composition string is cleared by IME.
416 // Note that most Chinese IMEs don't expose actual composition string to us.
417 // They typically tell us an IDEOGRAPHIC SPACE or empty string as composition
418 // string. Therefore, we should hack it only when:
419 // 1. committing string is empty string at requesting commit but the last
420 // data isn't IDEOGRAPHIC SPACE.
421 // 2. non-empty string is committed at requesting cancel.
422 if (!aIsSynthesized && (mIsRequestingCommit || mIsRequestingCancel)) {
423 nsString* committingData = nullptr;
424 switch (aCompositionEvent->mMessage) {
425 case eCompositionEnd:
426 case eCompositionChange:
427 case eCompositionCommitAsIs:
428 case eCompositionCommit:
429 committingData = &aCompositionEvent->mData;
430 break;
431 default:
432 NS_WARNING(
433 "Unexpected event comes during committing or "
434 "canceling composition");
435 break;
437 if (committingData) {
438 if (mIsRequestingCommit && committingData->IsEmpty() &&
439 mLastData != IDEOGRAPHIC_SPACE) {
440 committingData->Assign(mLastData);
441 } else if (mIsRequestingCancel && !committingData->IsEmpty()) {
442 committingData->Truncate();
447 bool dispatchEvent = true;
448 bool dispatchDOMTextEvent = aCompositionEvent->CausesDOMTextEvent();
450 // When mIsComposing is false but the committing string is different from
451 // the last data (E.g., previous eCompositionChange event made the
452 // composition string empty or didn't have clause information), we don't
453 // need to dispatch redundant DOM text event. (But note that we need to
454 // dispatch eCompositionChange event if we have not dispatched
455 // eCompositionChange event yet and commit string replaces selected string
456 // with empty string since selected string hasn't been replaced with empty
457 // string yet.)
458 if (dispatchDOMTextEvent &&
459 aCompositionEvent->mMessage != eCompositionChange && !mIsComposing &&
460 mHasDispatchedDOMTextEvent && mLastData == aCompositionEvent->mData) {
461 dispatchEvent = dispatchDOMTextEvent = false;
464 // widget may dispatch redundant eCompositionChange event
465 // which modifies neither composition string, clauses nor caret
466 // position. In such case, we shouldn't dispatch DOM events.
467 if (dispatchDOMTextEvent &&
468 aCompositionEvent->mMessage == eCompositionChange &&
469 mLastData == aCompositionEvent->mData && mRanges &&
470 aCompositionEvent->mRanges &&
471 mRanges->Equals(*aCompositionEvent->mRanges)) {
472 dispatchEvent = dispatchDOMTextEvent = false;
475 if (dispatchDOMTextEvent) {
476 if (!MaybeDispatchCompositionUpdate(aCompositionEvent)) {
477 return;
481 if (dispatchEvent) {
482 // If the composition event should cause a DOM text event, we should
483 // overwrite the event message as eCompositionChange because due to
484 // the limitation of mapping between event messages and DOM event types,
485 // we cannot map multiple event messages to a DOM event type.
486 if (dispatchDOMTextEvent &&
487 aCompositionEvent->mMessage != eCompositionChange) {
488 mHasDispatchedDOMTextEvent = true;
489 aCompositionEvent->mFlags = CloneAndDispatchAs(
490 aCompositionEvent, eCompositionChange, aStatus, aCallBack);
491 } else {
492 if (aCompositionEvent->mMessage == eCompositionChange) {
493 mHasDispatchedDOMTextEvent = true;
495 DispatchEvent(aCompositionEvent, aStatus, aCallBack);
497 } else {
498 *aStatus = nsEventStatus_eConsumeNoDefault;
501 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
502 return;
505 // Emulate editor behavior of compositionchange event (DOM text event) handler
506 // if no editor handles composition events.
507 if (dispatchDOMTextEvent && !HasEditor()) {
508 EditorWillHandleCompositionChangeEvent(aCompositionEvent);
509 EditorDidHandleCompositionChangeEvent();
512 if (aCompositionEvent->CausesDOMCompositionEndEvent()) {
513 // Dispatch a compositionend event if it's necessary.
514 if (aCompositionEvent->mMessage != eCompositionEnd) {
515 CloneAndDispatchAs(aCompositionEvent, eCompositionEnd);
517 MOZ_ASSERT(!mIsComposing, "Why is the editor still composing?");
518 MOZ_ASSERT(!HasEditor(), "Why does the editor still keep to hold this?");
521 MaybeNotifyIMEOfCompositionEventHandled(aCompositionEvent);
524 // static
525 void TextComposition::HandleSelectionEvent(
526 nsPresContext* aPresContext, BrowserParent* aBrowserParent,
527 WidgetSelectionEvent* aSelectionEvent) {
528 // If the content is a container of BrowserParent, composition should be in
529 // the remote process.
530 if (aBrowserParent) {
531 Unused << aBrowserParent->SendSelectionEvent(*aSelectionEvent);
532 aSelectionEvent->StopPropagation();
533 return;
536 AutoRestore<bool> saveHandlingSelectionEvent(sHandlingSelectionEvent);
537 sHandlingSelectionEvent = true;
539 if (RefPtr<IMEContentObserver> contentObserver =
540 IMEStateManager::GetActiveContentObserver()) {
541 contentObserver->MaybeHandleSelectionEvent(aPresContext, aSelectionEvent);
542 return;
545 ContentEventHandler handler(aPresContext);
546 // XXX During setting selection, a selection listener may change selection
547 // again. In such case, sHandlingSelectionEvent doesn't indicate if
548 // the selection change is caused by a selection event. However, it
549 // must be non-realistic scenario.
550 handler.OnSelectionEvent(aSelectionEvent);
553 uint32_t TextComposition::GetSelectionStartOffset() {
554 nsCOMPtr<nsIWidget> widget = mPresContext->GetRootWidget();
555 WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
556 widget);
557 // Due to a bug of widget, mRanges may not be nullptr even though composition
558 // string is empty. So, we need to check it here for avoiding to return
559 // odd start offset.
560 if (!mLastData.IsEmpty() && mRanges && mRanges->HasClauses()) {
561 querySelectedTextEvent.InitForQuerySelectedText(
562 ToSelectionType(mRanges->GetFirstClause()->mRangeType));
563 } else {
564 NS_WARNING_ASSERTION(
565 !mLastData.IsEmpty() || !mRanges || !mRanges->HasClauses(),
566 "Shouldn't have empty clause info when composition string is empty");
567 querySelectedTextEvent.InitForQuerySelectedText(SelectionType::eNormal);
570 // The editor which has this composition is observed by active
571 // IMEContentObserver, we can use the cache of it.
572 RefPtr<IMEContentObserver> contentObserver =
573 IMEStateManager::GetActiveContentObserver();
574 bool doQuerySelection = true;
575 if (contentObserver) {
576 if (contentObserver->IsObserving(*this)) {
577 doQuerySelection = false;
578 contentObserver->HandleQueryContentEvent(&querySelectedTextEvent);
580 // If another editor already has focus, we cannot retrieve selection
581 // in the editor which has this composition...
582 else if (NS_WARN_IF(contentObserver->GetPresContext() == mPresContext)) {
583 return 0; // XXX Is this okay?
587 // Otherwise, using slow path (i.e., compute every time with
588 // ContentEventHandler)
589 if (doQuerySelection) {
590 ContentEventHandler handler(mPresContext);
591 handler.HandleQueryContentEvent(&querySelectedTextEvent);
594 if (NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection())) {
595 return 0; // XXX Is this okay?
597 return querySelectedTextEvent.mReply->AnchorOffset();
600 void TextComposition::OnCompositionEventDispatched(
601 const WidgetCompositionEvent* aCompositionEvent) {
602 MOZ_RELEASE_ASSERT(!mBrowserParent);
604 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) {
605 return;
608 // Every composition event may cause changing composition start offset,
609 // especially when there is no composition string. Therefore, we need to
610 // update mCompositionStartOffset with the latest offset.
612 MOZ_ASSERT(aCompositionEvent->mMessage != eCompositionStart ||
613 mWasCompositionStringEmpty,
614 "mWasCompositionStringEmpty should be true if the dispatched "
615 "event is eCompositionStart");
617 if (mWasCompositionStringEmpty &&
618 !aCompositionEvent->CausesDOMCompositionEndEvent()) {
619 // If there was no composition string, current selection start may be the
620 // offset for inserting composition string.
621 // Update composition start offset with current selection start.
622 mCompositionStartOffset = GetSelectionStartOffset();
623 mTargetClauseOffsetInComposition = 0;
626 if (aCompositionEvent->CausesDOMTextEvent()) {
627 mTargetClauseOffsetInComposition = aCompositionEvent->TargetClauseOffset();
631 void TextComposition::OnStartOffsetUpdatedInChild(uint32_t aStartOffset) {
632 mCompositionStartOffset = aStartOffset;
635 void TextComposition::MaybeNotifyIMEOfCompositionEventHandled(
636 const WidgetCompositionEvent* aCompositionEvent) {
637 if (aCompositionEvent->mMessage != eCompositionStart &&
638 !aCompositionEvent->CausesDOMTextEvent()) {
639 return;
642 RefPtr<IMEContentObserver> contentObserver =
643 IMEStateManager::GetActiveContentObserver();
644 // When IMEContentObserver is managing the editor which has this composition,
645 // composition event handled notification should be sent after the observer
646 // notifies all pending notifications. Therefore, we should use it.
647 // XXX If IMEContentObserver suddenly loses focus after here and notifying
648 // widget of pending notifications, we won't notify widget of composition
649 // event handled. Although, this is a bug but it should be okay since
650 // destroying IMEContentObserver notifies IME of blur. So, native IME
651 // handler can treat it as this notification too.
652 if (contentObserver && contentObserver->IsObserving(*this)) {
653 contentObserver->MaybeNotifyCompositionEventHandled();
654 return;
656 // Otherwise, e.g., this composition is in non-active window, we should
657 // notify widget directly.
658 NotifyIME(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED);
661 void TextComposition::DispatchCompositionEventRunnable(
662 EventMessage aEventMessage, const nsAString& aData,
663 bool aIsSynthesizingCommit) {
664 nsContentUtils::AddScriptRunner(new CompositionEventDispatcher(
665 this, mNode, aEventMessage, aData, aIsSynthesizingCommit));
668 nsresult TextComposition::RequestToCommit(nsIWidget* aWidget, bool aDiscard) {
669 MOZ_ASSERT(this == IMEStateManager::GetTextCompositionFor(aWidget));
670 // If this composition is already requested to be committed or canceled,
671 // or has already finished in IME, we don't need to request it again because
672 // request from this instance shouldn't cause committing nor canceling current
673 // composition in IME, and even if the first request failed, new request
674 // won't success, probably. And we shouldn't synthesize events for
675 // committing or canceling composition twice or more times.
676 if (!CanRequsetIMEToCommitOrCancelComposition()) {
677 return NS_OK;
680 RefPtr<TextComposition> kungFuDeathGrip(this);
681 const nsAutoString lastData(mLastData);
683 if (IMEStateManager::CanSendNotificationToWidget()) {
684 AutoRestore<bool> saveRequestingCancel(mIsRequestingCancel);
685 AutoRestore<bool> saveRequestingCommit(mIsRequestingCommit);
686 if (aDiscard) {
687 mIsRequestingCancel = true;
688 mIsRequestingCommit = false;
689 } else {
690 mIsRequestingCancel = false;
691 mIsRequestingCommit = true;
693 // FYI: CompositionEvents caused by a call of NotifyIME() may be
694 // discarded by PresShell if it's not safe to dispatch the event.
695 nsresult rv = aWidget->NotifyIME(
696 IMENotification(aDiscard ? REQUEST_TO_CANCEL_COMPOSITION
697 : REQUEST_TO_COMMIT_COMPOSITION));
698 if (NS_WARN_IF(NS_FAILED(rv))) {
699 return rv;
703 mRequestedToCommitOrCancel = true;
705 // If the request is performed synchronously, this must be already destroyed.
706 if (Destroyed()) {
707 return NS_OK;
710 // Otherwise, synthesize the commit in content.
711 nsAutoString data(aDiscard ? EmptyString() : lastData);
712 if (data == mLastData) {
713 DispatchCompositionEventRunnable(eCompositionCommitAsIs, u""_ns, true);
714 } else {
715 DispatchCompositionEventRunnable(eCompositionCommit, data, true);
717 return NS_OK;
720 nsresult TextComposition::NotifyIME(IMEMessage aMessage) {
721 NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE);
722 return IMEStateManager::NotifyIME(aMessage, mPresContext, mBrowserParent);
725 void TextComposition::EditorWillHandleCompositionChangeEvent(
726 const WidgetCompositionEvent* aCompositionChangeEvent) {
727 mIsComposing = aCompositionChangeEvent->IsComposing();
728 mRanges = aCompositionChangeEvent->mRanges;
729 mIsEditorHandlingEvent = true;
731 MOZ_ASSERT(
732 mLastData == aCompositionChangeEvent->mData,
733 "The text of a compositionchange event must be same as previous data "
734 "attribute value of the latest compositionupdate event");
737 void TextComposition::OnEditorDestroyed() {
738 MOZ_RELEASE_ASSERT(!mBrowserParent);
740 MOZ_ASSERT(!mIsEditorHandlingEvent,
741 "The editor should have stopped listening events");
742 nsCOMPtr<nsIWidget> widget = GetWidget();
743 if (NS_WARN_IF(!widget)) {
744 // XXX If this could happen, how do we notify IME of destroying the editor?
745 return;
748 // Try to cancel the composition.
749 RequestToCommit(widget, true);
752 void TextComposition::EditorDidHandleCompositionChangeEvent() {
753 mString = mLastData;
754 mIsEditorHandlingEvent = false;
757 void TextComposition::StartHandlingComposition(EditorBase* aEditorBase) {
758 MOZ_RELEASE_ASSERT(!mBrowserParent);
760 MOZ_ASSERT(!HasEditor(), "There is a handling editor already");
761 mEditorBaseWeak = do_GetWeakReference(static_cast<nsIEditor*>(aEditorBase));
764 void TextComposition::EndHandlingComposition(EditorBase* aEditorBase) {
765 MOZ_RELEASE_ASSERT(!mBrowserParent);
767 #ifdef DEBUG
768 RefPtr<EditorBase> editorBase = GetEditorBase();
769 MOZ_ASSERT(!editorBase || editorBase == aEditorBase,
770 "Another editor handled the composition?");
771 #endif // #ifdef DEBUG
772 mEditorBaseWeak = nullptr;
775 already_AddRefed<EditorBase> TextComposition::GetEditorBase() const {
776 nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditorBaseWeak);
777 RefPtr<EditorBase> editorBase = static_cast<EditorBase*>(editor.get());
778 return editorBase.forget();
781 bool TextComposition::HasEditor() const {
782 return mEditorBaseWeak && mEditorBaseWeak->IsAlive();
785 RawRangeBoundary TextComposition::FirstIMESelectionStartRef() const {
786 RefPtr<EditorBase> editorBase = GetEditorBase();
787 if (!editorBase) {
788 return RawRangeBoundary();
791 nsISelectionController* selectionController =
792 editorBase->GetSelectionController();
793 if (NS_WARN_IF(!selectionController)) {
794 return RawRangeBoundary();
797 const nsRange* firstRange = nullptr;
798 static const SelectionType kIMESelectionTypes[] = {
799 SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause,
800 SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause};
801 for (auto selectionType : kIMESelectionTypes) {
802 dom::Selection* selection =
803 selectionController->GetSelection(ToRawSelectionType(selectionType));
804 if (!selection) {
805 continue;
807 const uint32_t rangeCount = selection->RangeCount();
808 for (const uint32_t i : IntegerRange(rangeCount)) {
809 MOZ_ASSERT(selection->RangeCount() == rangeCount);
810 const nsRange* range = selection->GetRangeAt(i);
811 MOZ_ASSERT(range);
812 if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
813 MOZ_UNLIKELY(NS_WARN_IF(!range->GetStartContainer()))) {
814 continue;
816 if (!firstRange) {
817 firstRange = range;
818 continue;
820 // In most cases, all composition string should be in same text node.
821 if (firstRange->GetStartContainer() == range->GetStartContainer()) {
822 if (firstRange->StartOffset() > range->StartOffset()) {
823 firstRange = range;
825 continue;
827 // However, if web apps have inserted different nodes in composition
828 // string, composition string may span 2 or more nodes.
829 if (firstRange->GetStartContainer()->GetNextSibling() ==
830 range->GetStartContainer()) {
831 // Fast path for some known applications like Google Keep.
832 firstRange = range;
833 continue;
835 // Unfortunately, really slow path.
836 // The ranges should always have a common ancestor, hence, be comparable.
837 if (*nsContentUtils::ComparePoints(range->StartRef(),
838 firstRange->StartRef()) == -1) {
839 firstRange = range;
843 return firstRange ? firstRange->StartRef().AsRaw() : RawRangeBoundary();
846 RawRangeBoundary TextComposition::LastIMESelectionEndRef() const {
847 RefPtr<EditorBase> editorBase = GetEditorBase();
848 if (!editorBase) {
849 return RawRangeBoundary();
852 nsISelectionController* selectionController =
853 editorBase->GetSelectionController();
854 if (NS_WARN_IF(!selectionController)) {
855 return RawRangeBoundary();
858 const nsRange* lastRange = nullptr;
859 static const SelectionType kIMESelectionTypes[] = {
860 SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause,
861 SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause};
862 for (auto selectionType : kIMESelectionTypes) {
863 dom::Selection* selection =
864 selectionController->GetSelection(ToRawSelectionType(selectionType));
865 if (!selection) {
866 continue;
868 const uint32_t rangeCount = selection->RangeCount();
869 for (const uint32_t i : IntegerRange(rangeCount)) {
870 MOZ_ASSERT(selection->RangeCount() == rangeCount);
871 const nsRange* range = selection->GetRangeAt(i);
872 MOZ_ASSERT(range);
873 if (MOZ_UNLIKELY(NS_WARN_IF(!range)) ||
874 MOZ_UNLIKELY(NS_WARN_IF(!range->GetEndContainer()))) {
875 continue;
877 if (!lastRange) {
878 lastRange = range;
879 continue;
881 // In most cases, all composition string should be in same text node.
882 if (lastRange->GetEndContainer() == range->GetEndContainer()) {
883 if (lastRange->EndOffset() < range->EndOffset()) {
884 lastRange = range;
886 continue;
888 // However, if web apps have inserted different nodes in composition
889 // string, composition string may span 2 or more nodes.
890 if (lastRange->GetEndContainer() ==
891 range->GetEndContainer()->GetNextSibling()) {
892 // Fast path for some known applications like Google Keep.
893 lastRange = range;
894 continue;
896 // Unfortunately, really slow path.
897 // The ranges should always have a common ancestor, hence, be comparable.
898 if (*nsContentUtils::ComparePoints(lastRange->EndRef(),
899 range->EndRef()) == -1) {
900 lastRange = range;
904 return lastRange ? lastRange->EndRef().AsRaw() : RawRangeBoundary();
907 /******************************************************************************
908 * TextComposition::CompositionEventDispatcher
909 ******************************************************************************/
911 TextComposition::CompositionEventDispatcher::CompositionEventDispatcher(
912 TextComposition* aTextComposition, nsINode* aEventTarget,
913 EventMessage aEventMessage, const nsAString& aData,
914 bool aIsSynthesizedEvent)
915 : Runnable("TextComposition::CompositionEventDispatcher"),
916 mTextComposition(aTextComposition),
917 mEventTarget(aEventTarget),
918 mData(aData),
919 mEventMessage(aEventMessage),
920 mIsSynthesizedEvent(aIsSynthesizedEvent) {}
922 NS_IMETHODIMP
923 TextComposition::CompositionEventDispatcher::Run() {
924 // The widget can be different from the widget which has dispatched
925 // composition events because GetWidget() returns a widget which is proper
926 // for calling NotifyIME(). However, this must no be problem since both
927 // widget should share native IME context. Therefore, even if an event
928 // handler uses the widget for requesting IME to commit or cancel, it works.
929 nsCOMPtr<nsIWidget> widget(mTextComposition->GetWidget());
930 if (!mTextComposition->IsValidStateForComposition(widget)) {
931 return NS_OK; // cannot dispatch any events anymore
934 RefPtr<nsPresContext> presContext = mTextComposition->mPresContext;
935 nsCOMPtr<nsINode> eventTarget = mEventTarget;
936 RefPtr<BrowserParent> browserParent = mTextComposition->mBrowserParent;
937 nsEventStatus status = nsEventStatus_eIgnore;
938 switch (mEventMessage) {
939 case eCompositionStart: {
940 WidgetCompositionEvent compStart(true, eCompositionStart, widget);
941 compStart.mNativeIMEContext = mTextComposition->mNativeContext;
942 WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
943 widget);
944 ContentEventHandler handler(presContext);
945 handler.OnQuerySelectedText(&querySelectedTextEvent);
946 NS_ASSERTION(querySelectedTextEvent.Succeeded(),
947 "Failed to get selected text");
948 if (querySelectedTextEvent.FoundSelection()) {
949 compStart.mData = querySelectedTextEvent.mReply->DataRef();
951 compStart.mFlags.mIsSynthesizedForTests =
952 mTextComposition->IsSynthesizedForTests();
953 IMEStateManager::DispatchCompositionEvent(
954 eventTarget, presContext, browserParent, &compStart, &status, nullptr,
955 mIsSynthesizedEvent);
956 break;
958 case eCompositionChange:
959 case eCompositionCommitAsIs:
960 case eCompositionCommit: {
961 WidgetCompositionEvent compEvent(true, mEventMessage, widget);
962 compEvent.mNativeIMEContext = mTextComposition->mNativeContext;
963 if (mEventMessage != eCompositionCommitAsIs) {
964 compEvent.mData = mData;
966 compEvent.mFlags.mIsSynthesizedForTests =
967 mTextComposition->IsSynthesizedForTests();
968 IMEStateManager::DispatchCompositionEvent(
969 eventTarget, presContext, browserParent, &compEvent, &status, nullptr,
970 mIsSynthesizedEvent);
971 break;
973 default:
974 MOZ_CRASH("Unsupported event");
976 return NS_OK;
979 /******************************************************************************
980 * TextCompositionArray
981 ******************************************************************************/
983 TextCompositionArray::index_type TextCompositionArray::IndexOf(
984 const NativeIMEContext& aNativeIMEContext) {
985 if (!aNativeIMEContext.IsValid()) {
986 return NoIndex;
988 for (index_type i = Length(); i > 0; --i) {
989 if (ElementAt(i - 1)->GetNativeIMEContext() == aNativeIMEContext) {
990 return i - 1;
993 return NoIndex;
996 TextCompositionArray::index_type TextCompositionArray::IndexOf(
997 nsIWidget* aWidget) {
998 return IndexOf(aWidget->GetNativeIMEContext());
1001 TextCompositionArray::index_type TextCompositionArray::IndexOf(
1002 nsPresContext* aPresContext) {
1003 for (index_type i = Length(); i > 0; --i) {
1004 if (ElementAt(i - 1)->GetPresContext() == aPresContext) {
1005 return i - 1;
1008 return NoIndex;
1011 TextCompositionArray::index_type TextCompositionArray::IndexOf(
1012 nsPresContext* aPresContext, nsINode* aNode) {
1013 index_type index = IndexOf(aPresContext);
1014 if (index == NoIndex) {
1015 return NoIndex;
1017 nsINode* node = ElementAt(index)->GetEventTargetNode();
1018 return node == aNode ? index : NoIndex;
1021 TextComposition* TextCompositionArray::GetCompositionFor(nsIWidget* aWidget) {
1022 index_type i = IndexOf(aWidget);
1023 if (i == NoIndex) {
1024 return nullptr;
1026 return ElementAt(i);
1029 TextComposition* TextCompositionArray::GetCompositionFor(
1030 const WidgetCompositionEvent* aCompositionEvent) {
1031 index_type i = IndexOf(aCompositionEvent->mNativeIMEContext);
1032 if (i == NoIndex) {
1033 return nullptr;
1035 return ElementAt(i);
1038 TextComposition* TextCompositionArray::GetCompositionFor(
1039 nsPresContext* aPresContext) {
1040 index_type i = IndexOf(aPresContext);
1041 if (i == NoIndex) {
1042 return nullptr;
1044 return ElementAt(i);
1047 TextComposition* TextCompositionArray::GetCompositionFor(
1048 nsPresContext* aPresContext, nsINode* aNode) {
1049 index_type i = IndexOf(aPresContext, aNode);
1050 if (i == NoIndex) {
1051 return nullptr;
1053 return ElementAt(i);
1056 TextComposition* TextCompositionArray::GetCompositionInContent(
1057 nsPresContext* aPresContext, nsIContent* aContent) {
1058 // There should be only one composition per content object.
1059 for (index_type i = Length(); i > 0; --i) {
1060 nsINode* node = ElementAt(i - 1)->GetEventTargetNode();
1061 if (node && node->IsInclusiveDescendantOf(aContent)) {
1062 return ElementAt(i - 1);
1065 return nullptr;
1068 } // namespace mozilla