TextComposition.cpp (41262B)
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/. */ 6 7 #include "TextComposition.h" 8 9 #include "ContentEventHandler.h" 10 #include "IMEContentObserver.h" 11 #include "IMEStateManager.h" 12 #include "mozilla/AutoRestore.h" 13 #include "mozilla/EditorBase.h" 14 #include "mozilla/EventDispatcher.h" 15 #include "mozilla/IMEStateManager.h" 16 #include "mozilla/IntegerRange.h" 17 #include "mozilla/MiscEvents.h" 18 #include "mozilla/PresShell.h" 19 #include "mozilla/RangeBoundary.h" 20 #include "mozilla/StaticPrefs_dom.h" 21 #include "mozilla/StaticPrefs_intl.h" 22 #include "mozilla/TextEvents.h" 23 #include "mozilla/dom/BrowserParent.h" 24 #include "nsContentUtils.h" 25 #include "nsIContent.h" 26 #include "nsIMutationObserver.h" 27 #include "nsPresContext.h" 28 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 35 36 #ifdef XP_MACOSX 37 # undef TextRange 38 # undef TextRangeArray 39 # undef Comment 40 #endif 41 42 using namespace mozilla::widget; 43 44 namespace mozilla { 45 46 #define IDEOGRAPHIC_SPACE (u"\x3000"_ns) 47 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; 54 } 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++; 59 } 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; 66 } 67 68 /****************************************************************************** 69 * TextComposition 70 ******************************************************************************/ 71 72 bool TextComposition::sHandlingSelectionEvent = false; 73 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 mIsRequestingCommit(false), 89 mIsRequestingCancel(false), 90 mRequestedToCommitOrCancel(false), 91 mHasDispatchedDOMTextEvent(false), 92 mHasReceivedCommitEvent(false), 93 mWasNativeCompositionEndEventDiscarded(false), 94 mAllowControlCharacters( 95 StaticPrefs::dom_compositionevent_allow_control_characters()), 96 mWasCompositionStringEmpty(true) { 97 MOZ_ASSERT(aCompositionEvent->mNativeIMEContext.IsValid()); 98 } 99 100 void TextComposition::Destroy() { 101 mPresContext = nullptr; 102 mNode = nullptr; 103 mBrowserParent = nullptr; 104 mContainerTextNode = nullptr; 105 mCompositionStartOffsetInTextNode = UINT32_MAX; 106 mCompositionLengthInTextNode = UINT32_MAX; 107 // TODO: If the editor is still alive and this is held by it, we should tell 108 // this being destroyed for cleaning up the stuff. 109 } 110 111 void TextComposition::OnCharacterDataChanged( 112 Text& aText, const CharacterDataChangeInfo& aInfo) { 113 if (mContainerTextNode != &aText || 114 mCompositionStartOffsetInTextNode == UINT32_MAX || 115 mCompositionLengthInTextNode == UINT32_MAX) { 116 return; 117 } 118 119 // Ignore changes after composition string. 120 if (aInfo.mChangeStart >= 121 mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) { 122 return; 123 } 124 125 // If the change ends before the composition string, we need only to adjust 126 // the start offset. 127 if (aInfo.mChangeEnd <= mCompositionStartOffsetInTextNode) { 128 MOZ_ASSERT(aInfo.LengthOfRemovedText() <= 129 mCompositionStartOffsetInTextNode); 130 mCompositionStartOffsetInTextNode -= aInfo.LengthOfRemovedText(); 131 mCompositionStartOffsetInTextNode += aInfo.mReplaceLength; 132 return; 133 } 134 135 // If this is caused by a splitting text node, the composition string 136 // may be split out to the new right node. In the case, 137 // CompositionTransaction::DoTransaction handles it with walking the 138 // following text nodes. Therefore, we should NOT shrink the composing 139 // range for avoind breaking the fix of bug 1310912. Although the handling 140 // looks buggy so that we need to move the handling into here later. 141 if (aInfo.mDetails && 142 aInfo.mDetails->mType == CharacterDataChangeInfo::Details::eSplit) { 143 return; 144 } 145 146 // If the change removes/replaces the last character of the composition 147 // string, we should shrink the composition range before the change start. 148 // Then, the replace string will be never updated by coming composition 149 // updates. 150 if (aInfo.mChangeEnd >= 151 mCompositionStartOffsetInTextNode + mCompositionLengthInTextNode) { 152 // If deleting the first character of the composition string, collapse IME 153 // selection temporarily. Updating composition string will insert new 154 // composition string there. 155 if (aInfo.mChangeStart <= mCompositionStartOffsetInTextNode) { 156 mCompositionStartOffsetInTextNode = aInfo.mChangeStart; 157 mCompositionLengthInTextNode = 0u; 158 return; 159 } 160 // If some characters in the composition still stay, composition range 161 // should be shrunken. 162 MOZ_ASSERT(aInfo.mChangeStart > mCompositionStartOffsetInTextNode); 163 mCompositionLengthInTextNode = 164 aInfo.mChangeStart - mCompositionStartOffsetInTextNode; 165 return; 166 } 167 168 // If removed range starts in the composition string, we need only adjust 169 // the length to make composition range contain the replace string. 170 if (aInfo.mChangeStart >= mCompositionStartOffsetInTextNode) { 171 if (!mCompositionLengthInTextNode) { 172 // However, don't extend composition range if there is no composition 173 // string. 174 return; 175 } 176 MOZ_ASSERT(aInfo.LengthOfRemovedText() <= mCompositionLengthInTextNode); 177 mCompositionLengthInTextNode -= aInfo.LengthOfRemovedText(); 178 mCompositionLengthInTextNode += aInfo.mReplaceLength; 179 return; 180 } 181 182 // If preceding characters of the composition string is also removed, new 183 // composition start will be there and new composition ends at current 184 // position. 185 const uint32_t removedLengthInCompositionString = 186 aInfo.mChangeEnd - mCompositionStartOffsetInTextNode; 187 mCompositionStartOffsetInTextNode = aInfo.mChangeStart; 188 if (!mCompositionLengthInTextNode) { 189 // However, don't extend composition range if there is no composition 190 // string. 191 return; 192 } 193 mCompositionLengthInTextNode -= removedLengthInCompositionString; 194 mCompositionLengthInTextNode += aInfo.mReplaceLength; 195 } 196 197 bool TextComposition::IsValidStateForComposition(nsIWidget* aWidget) const { 198 return !Destroyed() && aWidget && !aWidget->Destroyed() && 199 mPresContext->GetPresShell() && 200 !mPresContext->PresShell()->IsDestroying(); 201 } 202 203 bool TextComposition::MaybeDispatchCompositionUpdate( 204 const WidgetCompositionEvent* aCompositionEvent) { 205 MOZ_RELEASE_ASSERT(!mBrowserParent); 206 207 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { 208 return false; 209 } 210 211 // Note that we don't need to dispatch eCompositionUpdate event even if 212 // mHasDispatchedDOMTextEvent is false and eCompositionCommit event is 213 // dispatched with empty string immediately after eCompositionStart 214 // because composition string has never been changed from empty string to 215 // non-empty string in such composition even if selected string was not 216 // empty string (mLastData isn't set to selected text when this receives 217 // eCompositionStart). 218 if (mLastData == aCompositionEvent->mData) { 219 // Even if the new composition event does not update the composition string, 220 // it may change IME selection. 221 mLastRanges = aCompositionEvent->mRanges; 222 return true; 223 } 224 CloneAndDispatchAs(aCompositionEvent, eCompositionUpdate); 225 return IsValidStateForComposition(aCompositionEvent->mWidget); 226 } 227 228 BaseEventFlags TextComposition::CloneAndDispatchAs( 229 const WidgetCompositionEvent* aCompositionEvent, EventMessage aMessage, 230 nsEventStatus* aStatus, EventDispatchingCallback* aCallBack) { 231 MOZ_RELEASE_ASSERT(!mBrowserParent); 232 233 MOZ_ASSERT(IsValidStateForComposition(aCompositionEvent->mWidget), 234 "Should be called only when it's safe to dispatch an event"); 235 236 WidgetCompositionEvent compositionEvent(aCompositionEvent->IsTrusted(), 237 aMessage, aCompositionEvent->mWidget); 238 compositionEvent.mTimeStamp = aCompositionEvent->mTimeStamp; 239 compositionEvent.mData = aCompositionEvent->mData; 240 compositionEvent.mNativeIMEContext = aCompositionEvent->mNativeIMEContext; 241 compositionEvent.mOriginalMessage = aCompositionEvent->mMessage; 242 compositionEvent.mFlags.mIsSynthesizedForTests = 243 aCompositionEvent->mFlags.mIsSynthesizedForTests; 244 245 nsEventStatus dummyStatus = nsEventStatus_eConsumeNoDefault; 246 nsEventStatus* status = aStatus ? aStatus : &dummyStatus; 247 if (aMessage == eCompositionUpdate) { 248 mLastData = compositionEvent.mData; 249 mLastRanges = aCompositionEvent->mRanges; 250 } 251 252 DispatchEvent(&compositionEvent, status, aCallBack, aCompositionEvent); 253 return compositionEvent.mFlags; 254 } 255 256 void TextComposition::DispatchEvent( 257 WidgetCompositionEvent* aDispatchEvent, nsEventStatus* aStatus, 258 EventDispatchingCallback* aCallBack, 259 const WidgetCompositionEvent* aOriginalEvent) { 260 if (aDispatchEvent->mMessage == eCompositionChange) { 261 aDispatchEvent->mFlags.mOnlySystemGroupDispatchInContent = true; 262 } 263 RefPtr<nsINode> node = mNode; 264 RefPtr<nsPresContext> presContext = mPresContext; 265 EventDispatcher::Dispatch(node, presContext, aDispatchEvent, nullptr, aStatus, 266 aCallBack); 267 268 OnCompositionEventDispatched(aDispatchEvent); 269 } 270 271 void TextComposition::OnCompositionEventDiscarded( 272 WidgetCompositionEvent* aCompositionEvent) { 273 // Note that this method is never called for synthesized events for emulating 274 // commit or cancel composition. 275 276 MOZ_ASSERT(aCompositionEvent->IsTrusted(), 277 "Shouldn't be called with untrusted event"); 278 279 if (mBrowserParent) { 280 (void)mBrowserParent->SendCompositionEvent(*aCompositionEvent, 281 mCompositionId); 282 } 283 284 // XXX If composition events are discarded, should we dispatch them with 285 // runnable event? However, even if we do so, it might make native IME 286 // confused due to async modification. Especially when native IME is 287 // TSF. 288 if (!aCompositionEvent->CausesDOMCompositionEndEvent()) { 289 return; 290 } 291 292 mWasNativeCompositionEndEventDiscarded = true; 293 } 294 295 static inline bool IsControlChar(uint32_t aCharCode) { 296 return aCharCode < ' ' || aCharCode == 0x7F; 297 } 298 299 static size_t FindFirstControlCharacter(const nsAString& aStr) { 300 const char16_t* sourceBegin = aStr.BeginReading(); 301 const char16_t* sourceEnd = aStr.EndReading(); 302 303 for (const char16_t* source = sourceBegin; source < sourceEnd; ++source) { 304 if (*source != '\t' && IsControlChar(*source)) { 305 return source - sourceBegin; 306 } 307 } 308 309 return -1; 310 } 311 312 static void RemoveControlCharactersFrom(nsAString& aStr, 313 TextRangeArray* aRanges) { 314 size_t firstControlCharOffset = FindFirstControlCharacter(aStr); 315 if (firstControlCharOffset == (size_t)-1) { 316 return; 317 } 318 319 nsAutoString copy(aStr); 320 const char16_t* sourceBegin = copy.BeginReading(); 321 const char16_t* sourceEnd = copy.EndReading(); 322 323 char16_t* dest = aStr.BeginWriting(); 324 if (NS_WARN_IF(!dest)) { 325 return; 326 } 327 328 char16_t* curDest = dest + firstControlCharOffset; 329 size_t i = firstControlCharOffset; 330 for (const char16_t* source = sourceBegin + firstControlCharOffset; 331 source < sourceEnd; ++source) { 332 if (*source == '\t' || *source == '\n' || !IsControlChar(*source)) { 333 *curDest = *source; 334 ++curDest; 335 ++i; 336 } else if (aRanges) { 337 aRanges->RemoveCharacter(i); 338 } 339 } 340 341 aStr.SetLength(curDest - dest); 342 } 343 344 nsString TextComposition::CommitStringIfCommittedAsIs() const { 345 nsString result(mLastData); 346 if (!mAllowControlCharacters) { 347 RemoveControlCharactersFrom(result, nullptr); 348 } 349 if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() && 350 mLastData == IDEOGRAPHIC_SPACE) { 351 return EmptyString(); 352 } 353 return result; 354 } 355 356 void TextComposition::DispatchCompositionEvent( 357 WidgetCompositionEvent* aCompositionEvent, nsEventStatus* aStatus, 358 EventDispatchingCallback* aCallBack, bool aIsSynthesized) { 359 mWasCompositionStringEmpty = mString.IsEmpty(); 360 361 if (aCompositionEvent->IsFollowedByCompositionEnd()) { 362 mHasReceivedCommitEvent = true; 363 } 364 365 // If this instance has requested to commit or cancel composition but 366 // is not synthesizing commit event, that means that the IME commits or 367 // cancels the composition asynchronously. Typically, iBus behaves so. 368 // Then, synthesized events which were dispatched immediately after 369 // the request has already committed our editor's composition string and 370 // told it to web apps. Therefore, we should ignore the delayed events. 371 if (mRequestedToCommitOrCancel && !aIsSynthesized) { 372 *aStatus = nsEventStatus_eConsumeNoDefault; 373 return; 374 } 375 376 // If the content is a container of BrowserParent, composition should be in 377 // the remote process. 378 if (mBrowserParent) { 379 (void)mBrowserParent->SendCompositionEvent(*aCompositionEvent, 380 mCompositionId); 381 aCompositionEvent->StopPropagation(); 382 if (aCompositionEvent->CausesDOMTextEvent()) { 383 mLastData = aCompositionEvent->mData; 384 mLastRanges = aCompositionEvent->mRanges; 385 // Although, the composition event hasn't been actually handled yet, 386 // emulate an editor to be handling the composition event. 387 EditorWillHandleCompositionChangeEvent(aCompositionEvent); 388 EditorDidHandleCompositionChangeEvent(); 389 } 390 return; 391 } 392 393 if (!mAllowControlCharacters) { 394 RemoveControlCharactersFrom(aCompositionEvent->mData, 395 aCompositionEvent->mRanges); 396 } 397 if (aCompositionEvent->mMessage == eCompositionCommitAsIs) { 398 NS_ASSERTION(!aCompositionEvent->mRanges, 399 "mRanges of eCompositionCommitAsIs should be null"); 400 aCompositionEvent->mRanges = nullptr; 401 NS_ASSERTION(aCompositionEvent->mData.IsEmpty(), 402 "mData of eCompositionCommitAsIs should be empty string"); 403 if (StaticPrefs::intl_ime_remove_placeholder_character_at_commit() && 404 mLastData == IDEOGRAPHIC_SPACE) { 405 // If the last data is an ideographic space (FullWidth space), it might be 406 // a placeholder character of some Chinese IME. So, committing with 407 // this data might not be expected by users. Let's use empty string. 408 aCompositionEvent->mData.Truncate(); 409 } else { 410 aCompositionEvent->mData = mLastData; 411 } 412 } else if (aCompositionEvent->mMessage == eCompositionCommit) { 413 NS_ASSERTION(!aCompositionEvent->mRanges, 414 "mRanges of eCompositionCommit should be null"); 415 aCompositionEvent->mRanges = nullptr; 416 } 417 418 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { 419 *aStatus = nsEventStatus_eConsumeNoDefault; 420 return; 421 } 422 423 // IME may commit composition with empty string for a commit request or 424 // with non-empty string for a cancel request. We should prevent such 425 // unexpected result. E.g., web apps may be confused if they implement 426 // autocomplete which attempts to commit composition forcibly when the user 427 // selects one of suggestions but composition string is cleared by IME. 428 // Note that most Chinese IMEs don't expose actual composition string to us. 429 // They typically tell us an IDEOGRAPHIC SPACE or empty string as composition 430 // string. Therefore, we should hack it only when: 431 // 1. committing string is empty string at requesting commit but the last 432 // data isn't IDEOGRAPHIC SPACE. 433 // 2. non-empty string is committed at requesting cancel. 434 if (!aIsSynthesized && (mIsRequestingCommit || mIsRequestingCancel)) { 435 nsString* committingData = nullptr; 436 switch (aCompositionEvent->mMessage) { 437 case eCompositionEnd: 438 case eCompositionChange: 439 case eCompositionCommitAsIs: 440 case eCompositionCommit: 441 committingData = &aCompositionEvent->mData; 442 break; 443 default: 444 NS_WARNING( 445 "Unexpected event comes during committing or " 446 "canceling composition"); 447 break; 448 } 449 if (committingData) { 450 if (mIsRequestingCommit && committingData->IsEmpty() && 451 mLastData != IDEOGRAPHIC_SPACE) { 452 committingData->Assign(mLastData); 453 } else if (mIsRequestingCancel && !committingData->IsEmpty()) { 454 committingData->Truncate(); 455 } 456 } 457 } 458 459 bool dispatchEvent = true; 460 bool dispatchDOMTextEvent = aCompositionEvent->CausesDOMTextEvent(); 461 462 // When mIsComposing is false but the committing string is different from 463 // the last data (E.g., previous eCompositionChange event made the 464 // composition string empty or didn't have clause information), we don't 465 // need to dispatch redundant DOM text event. (But note that we need to 466 // dispatch eCompositionChange event if we have not dispatched 467 // eCompositionChange event yet and commit string replaces selected string 468 // with empty string since selected string hasn't been replaced with empty 469 // string yet.) 470 if (dispatchDOMTextEvent && 471 aCompositionEvent->mMessage != eCompositionChange && !mIsComposing && 472 mHasDispatchedDOMTextEvent && mLastData == aCompositionEvent->mData) { 473 dispatchEvent = dispatchDOMTextEvent = false; 474 } 475 476 // widget may dispatch redundant eCompositionChange event 477 // which modifies neither composition string, clauses nor caret 478 // position. In such case, we shouldn't dispatch DOM events. 479 if (dispatchDOMTextEvent && 480 aCompositionEvent->mMessage == eCompositionChange && 481 mLastData == aCompositionEvent->mData && mRanges && 482 aCompositionEvent->mRanges && 483 mRanges->Equals(*aCompositionEvent->mRanges)) { 484 dispatchEvent = dispatchDOMTextEvent = false; 485 } 486 487 if (dispatchDOMTextEvent) { 488 if (!MaybeDispatchCompositionUpdate(aCompositionEvent)) { 489 return; 490 } 491 } 492 493 if (dispatchEvent) { 494 // If the composition event should cause a DOM text event, we should 495 // overwrite the event message as eCompositionChange because due to 496 // the limitation of mapping between event messages and DOM event types, 497 // we cannot map multiple event messages to a DOM event type. 498 if (dispatchDOMTextEvent && 499 aCompositionEvent->mMessage != eCompositionChange) { 500 mHasDispatchedDOMTextEvent = true; 501 aCompositionEvent->mFlags = CloneAndDispatchAs( 502 aCompositionEvent, eCompositionChange, aStatus, aCallBack); 503 } else { 504 if (aCompositionEvent->mMessage == eCompositionChange) { 505 mHasDispatchedDOMTextEvent = true; 506 } 507 DispatchEvent(aCompositionEvent, aStatus, aCallBack); 508 } 509 } else { 510 *aStatus = nsEventStatus_eConsumeNoDefault; 511 } 512 513 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { 514 return; 515 } 516 517 // Emulate editor behavior of compositionchange event (DOM text event) handler 518 // if no editor handles composition events. 519 if (dispatchDOMTextEvent && !HasEditor()) { 520 EditorWillHandleCompositionChangeEvent(aCompositionEvent); 521 EditorDidHandleCompositionChangeEvent(); 522 } 523 524 if (aCompositionEvent->CausesDOMCompositionEndEvent()) { 525 // Dispatch a compositionend event if it's necessary. 526 if (aCompositionEvent->mMessage != eCompositionEnd) { 527 CloneAndDispatchAs(aCompositionEvent, eCompositionEnd); 528 } 529 MOZ_ASSERT(!mIsComposing, "Why is the editor still composing?"); 530 MOZ_ASSERT(!HasEditor(), "Why does the editor still keep to hold this?"); 531 } 532 533 MaybeNotifyIMEOfCompositionEventHandled(aCompositionEvent); 534 } 535 536 // static 537 void TextComposition::HandleSelectionEvent( 538 nsPresContext* aPresContext, BrowserParent* aBrowserParent, 539 WidgetSelectionEvent* aSelectionEvent) { 540 // If the content is a container of BrowserParent, composition should be in 541 // the remote process. 542 if (aBrowserParent) { 543 (void)aBrowserParent->SendSelectionEvent(*aSelectionEvent); 544 aSelectionEvent->StopPropagation(); 545 return; 546 } 547 548 AutoRestore<bool> saveHandlingSelectionEvent(sHandlingSelectionEvent); 549 sHandlingSelectionEvent = true; 550 551 if (RefPtr<IMEContentObserver> contentObserver = 552 IMEStateManager::GetActiveContentObserver()) { 553 contentObserver->MaybeHandleSelectionEvent(aPresContext, aSelectionEvent); 554 return; 555 } 556 557 ContentEventHandler handler(aPresContext); 558 // XXX During setting selection, a selection listener may change selection 559 // again. In such case, sHandlingSelectionEvent doesn't indicate if 560 // the selection change is caused by a selection event. However, it 561 // must be non-realistic scenario. 562 handler.OnSelectionEvent(aSelectionEvent); 563 } 564 565 uint32_t TextComposition::GetSelectionStartOffset() { 566 nsCOMPtr<nsIWidget> widget = mPresContext->GetRootWidget(); 567 WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, 568 widget); 569 // Due to a bug of widget, mRanges may not be nullptr even though composition 570 // string is empty. So, we need to check it here for avoiding to return 571 // odd start offset. 572 if (!mLastData.IsEmpty() && mRanges && mRanges->HasClauses()) { 573 querySelectedTextEvent.InitForQuerySelectedText( 574 ToSelectionType(mRanges->GetFirstClause()->mRangeType)); 575 } else { 576 NS_WARNING_ASSERTION( 577 !mLastData.IsEmpty() || !mRanges || !mRanges->HasClauses(), 578 "Shouldn't have empty clause info when composition string is empty"); 579 querySelectedTextEvent.InitForQuerySelectedText(SelectionType::eNormal); 580 } 581 582 // The editor which has this composition is observed by active 583 // IMEContentObserver, we can use the cache of it. 584 RefPtr<IMEContentObserver> contentObserver = 585 IMEStateManager::GetActiveContentObserver(); 586 bool doQuerySelection = true; 587 if (contentObserver) { 588 if (contentObserver->IsObserving(*this)) { 589 doQuerySelection = false; 590 contentObserver->HandleQueryContentEvent(&querySelectedTextEvent); 591 } 592 // If another editor already has focus, we cannot retrieve selection 593 // in the editor which has this composition... 594 else if (NS_WARN_IF(contentObserver->GetPresContext() == mPresContext)) { 595 return 0; // XXX Is this okay? 596 } 597 } 598 599 // Otherwise, using slow path (i.e., compute every time with 600 // ContentEventHandler) 601 if (doQuerySelection) { 602 ContentEventHandler handler(mPresContext); 603 handler.HandleQueryContentEvent(&querySelectedTextEvent); 604 } 605 606 if (NS_WARN_IF(querySelectedTextEvent.DidNotFindSelection())) { 607 return 0; // XXX Is this okay? 608 } 609 return querySelectedTextEvent.mReply->AnchorOffset(); 610 } 611 612 void TextComposition::OnCompositionEventDispatched( 613 const WidgetCompositionEvent* aCompositionEvent) { 614 MOZ_RELEASE_ASSERT(!mBrowserParent); 615 616 if (!IsValidStateForComposition(aCompositionEvent->mWidget)) { 617 return; 618 } 619 620 // Every composition event may cause changing composition start offset, 621 // especially when there is no composition string. Therefore, we need to 622 // update mCompositionStartOffset with the latest offset. 623 624 MOZ_ASSERT(aCompositionEvent->mMessage != eCompositionStart || 625 mWasCompositionStringEmpty, 626 "mWasCompositionStringEmpty should be true if the dispatched " 627 "event is eCompositionStart"); 628 629 if (mWasCompositionStringEmpty && 630 !aCompositionEvent->CausesDOMCompositionEndEvent()) { 631 // If there was no composition string, current selection start may be the 632 // offset for inserting composition string. 633 // Update composition start offset with current selection start. 634 mCompositionStartOffset = GetSelectionStartOffset(); 635 mTargetClauseOffsetInComposition = 0; 636 } 637 638 if (aCompositionEvent->CausesDOMTextEvent()) { 639 mTargetClauseOffsetInComposition = aCompositionEvent->TargetClauseOffset(); 640 } 641 } 642 643 void TextComposition::OnStartOffsetUpdatedInChild(uint32_t aStartOffset) { 644 mCompositionStartOffset = aStartOffset; 645 } 646 647 void TextComposition::MaybeNotifyIMEOfCompositionEventHandled( 648 const WidgetCompositionEvent* aCompositionEvent) { 649 if (aCompositionEvent->mMessage != eCompositionStart && 650 !aCompositionEvent->CausesDOMTextEvent()) { 651 return; 652 } 653 654 RefPtr<IMEContentObserver> contentObserver = 655 IMEStateManager::GetActiveContentObserver(); 656 // When IMEContentObserver is managing the editor which has this composition, 657 // composition event handled notification should be sent after the observer 658 // notifies all pending notifications. Therefore, we should use it. 659 // XXX If IMEContentObserver suddenly loses focus after here and notifying 660 // widget of pending notifications, we won't notify widget of composition 661 // event handled. Although, this is a bug but it should be okay since 662 // destroying IMEContentObserver notifies IME of blur. So, native IME 663 // handler can treat it as this notification too. 664 if (contentObserver && contentObserver->IsObserving(*this)) { 665 contentObserver->MaybeNotifyCompositionEventHandled(); 666 return; 667 } 668 // Otherwise, e.g., this composition is in non-active window, we should 669 // notify widget directly. 670 NotifyIME(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED); 671 } 672 673 void TextComposition::DispatchCompositionEventRunnable( 674 EventMessage aEventMessage, const nsAString& aData, 675 bool aIsSynthesizingCommit) { 676 nsContentUtils::AddScriptRunner(new CompositionEventDispatcher( 677 this, mNode, aEventMessage, aData, aIsSynthesizingCommit)); 678 } 679 680 nsresult TextComposition::RequestToCommit(nsIWidget* aWidget, bool aDiscard) { 681 MOZ_ASSERT(this == IMEStateManager::GetTextCompositionFor(aWidget)); 682 // If this composition is already requested to be committed or canceled, 683 // or has already finished in IME, we don't need to request it again because 684 // request from this instance shouldn't cause committing nor canceling current 685 // composition in IME, and even if the first request failed, new request 686 // won't success, probably. And we shouldn't synthesize events for 687 // committing or canceling composition twice or more times. 688 if (!CanRequsetIMEToCommitOrCancelComposition()) { 689 return NS_OK; 690 } 691 692 RefPtr<TextComposition> kungFuDeathGrip(this); 693 const nsAutoString lastData(mLastData); 694 695 if (IMEStateManager::CanSendNotificationToWidget()) { 696 AutoRestore<bool> saveRequestingCancel(mIsRequestingCancel); 697 AutoRestore<bool> saveRequestingCommit(mIsRequestingCommit); 698 if (aDiscard) { 699 mIsRequestingCancel = true; 700 mIsRequestingCommit = false; 701 } else { 702 mIsRequestingCancel = false; 703 mIsRequestingCommit = true; 704 } 705 // FYI: CompositionEvents caused by a call of NotifyIME() may be 706 // discarded by PresShell if it's not safe to dispatch the event. 707 nsresult rv = aWidget->NotifyIME( 708 IMENotification(aDiscard ? REQUEST_TO_CANCEL_COMPOSITION 709 : REQUEST_TO_COMMIT_COMPOSITION)); 710 if (NS_WARN_IF(NS_FAILED(rv))) { 711 return rv; 712 } 713 } 714 715 mRequestedToCommitOrCancel = true; 716 717 // If the request is performed synchronously, this must be already destroyed. 718 if (Destroyed()) { 719 return NS_OK; 720 } 721 722 // Otherwise, synthesize the commit in content. 723 nsAutoString data(aDiscard ? EmptyString() : lastData); 724 if (data == mLastData) { 725 DispatchCompositionEventRunnable(eCompositionCommitAsIs, u""_ns, true); 726 } else { 727 DispatchCompositionEventRunnable(eCompositionCommit, data, true); 728 } 729 return NS_OK; 730 } 731 732 nsresult TextComposition::NotifyIME(IMEMessage aMessage) { 733 NS_ENSURE_TRUE(mPresContext, NS_ERROR_NOT_AVAILABLE); 734 return IMEStateManager::NotifyIME(aMessage, mPresContext, mBrowserParent); 735 } 736 737 void TextComposition::EditorWillHandleCompositionChangeEvent( 738 const WidgetCompositionEvent* aCompositionChangeEvent) { 739 mIsComposing = aCompositionChangeEvent->IsComposing(); 740 mRanges = aCompositionChangeEvent->mRanges; 741 mEditorIsHandlingEvent = true; 742 743 MOZ_ASSERT( 744 mLastData == aCompositionChangeEvent->mData, 745 "The text of a compositionchange event must be same as previous data " 746 "attribute value of the latest compositionupdate event"); 747 } 748 749 void TextComposition::OnEditorDestroyed() { 750 MOZ_RELEASE_ASSERT(!mBrowserParent); 751 752 MOZ_ASSERT(!mEditorIsHandlingEvent, 753 "The editor should have stopped listening events"); 754 nsCOMPtr<nsIWidget> widget = GetWidget(); 755 if (NS_WARN_IF(!widget)) { 756 // XXX If this could happen, how do we notify IME of destroying the editor? 757 return; 758 } 759 760 // Try to cancel the composition. 761 RequestToCommit(widget, true); 762 } 763 764 void TextComposition::EditorDidHandleCompositionChangeEvent() { 765 mString = mLastData; 766 mEditorIsHandlingEvent = false; 767 } 768 769 void TextComposition::StartHandlingComposition(EditorBase* aEditorBase) { 770 MOZ_RELEASE_ASSERT(!mBrowserParent); 771 772 MOZ_ASSERT(!HasEditor(), "There is a handling editor already"); 773 mEditorBaseWeak = do_GetWeakReference(static_cast<nsIEditor*>(aEditorBase)); 774 } 775 776 void TextComposition::EndHandlingComposition(EditorBase* aEditorBase) { 777 MOZ_RELEASE_ASSERT(!mBrowserParent); 778 779 #ifdef DEBUG 780 RefPtr<EditorBase> editorBase = GetEditorBase(); 781 MOZ_ASSERT(!editorBase || editorBase == aEditorBase, 782 "Another editor handled the composition?"); 783 #endif // #ifdef DEBUG 784 mEditorBaseWeak = nullptr; 785 } 786 787 already_AddRefed<EditorBase> TextComposition::GetEditorBase() const { 788 nsCOMPtr<nsIEditor> editor = do_QueryReferent(mEditorBaseWeak); 789 RefPtr<EditorBase> editorBase = static_cast<EditorBase*>(editor.get()); 790 return editorBase.forget(); 791 } 792 793 bool TextComposition::HasEditor() const { 794 return mEditorBaseWeak && mEditorBaseWeak->IsAlive(); 795 } 796 797 RawRangeBoundary TextComposition::FirstIMESelectionStartRef() const { 798 RefPtr<EditorBase> editorBase = GetEditorBase(); 799 if (!editorBase) { 800 return RawRangeBoundary(); 801 } 802 803 nsISelectionController* selectionController = 804 editorBase->GetSelectionController(); 805 if (NS_WARN_IF(!selectionController)) { 806 return RawRangeBoundary(); 807 } 808 809 const nsRange* firstRange = nullptr; 810 static const SelectionType kIMESelectionTypes[] = { 811 SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause, 812 SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause}; 813 for (auto selectionType : kIMESelectionTypes) { 814 dom::Selection* selection = 815 selectionController->GetSelection(ToRawSelectionType(selectionType)); 816 if (!selection) { 817 continue; 818 } 819 const uint32_t rangeCount = selection->RangeCount(); 820 for (const uint32_t i : IntegerRange(rangeCount)) { 821 MOZ_ASSERT(selection->RangeCount() == rangeCount); 822 const nsRange* range = selection->GetRangeAt(i); 823 MOZ_ASSERT(range); 824 if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || 825 MOZ_UNLIKELY(NS_WARN_IF(!range->GetStartContainer()))) { 826 continue; 827 } 828 if (!firstRange) { 829 firstRange = range; 830 continue; 831 } 832 // In most cases, all composition string should be in same text node. 833 if (firstRange->GetStartContainer() == range->GetStartContainer()) { 834 if (firstRange->StartOffset() > range->StartOffset()) { 835 firstRange = range; 836 } 837 continue; 838 } 839 // However, if web apps have inserted different nodes in composition 840 // string, composition string may span 2 or more nodes. 841 if (firstRange->GetStartContainer()->GetNextSibling() == 842 range->GetStartContainer()) { 843 // Fast path for some known applications like Google Keep. 844 firstRange = range; 845 continue; 846 } 847 // Unfortunately, really slow path. 848 // The ranges should always have a common ancestor, hence, be comparable. 849 if (*nsContentUtils::ComparePoints(range->StartRef(), 850 firstRange->StartRef()) == -1) { 851 firstRange = range; 852 } 853 } 854 } 855 return firstRange ? firstRange->StartRef().AsRaw() : RawRangeBoundary(); 856 } 857 858 RawRangeBoundary TextComposition::LastIMESelectionEndRef() const { 859 RefPtr<EditorBase> editorBase = GetEditorBase(); 860 if (!editorBase) { 861 return RawRangeBoundary(); 862 } 863 864 nsISelectionController* selectionController = 865 editorBase->GetSelectionController(); 866 if (NS_WARN_IF(!selectionController)) { 867 return RawRangeBoundary(); 868 } 869 870 const nsRange* lastRange = nullptr; 871 static const SelectionType kIMESelectionTypes[] = { 872 SelectionType::eIMERawClause, SelectionType::eIMESelectedRawClause, 873 SelectionType::eIMEConvertedClause, SelectionType::eIMESelectedClause}; 874 for (auto selectionType : kIMESelectionTypes) { 875 dom::Selection* selection = 876 selectionController->GetSelection(ToRawSelectionType(selectionType)); 877 if (!selection) { 878 continue; 879 } 880 const uint32_t rangeCount = selection->RangeCount(); 881 for (const uint32_t i : IntegerRange(rangeCount)) { 882 MOZ_ASSERT(selection->RangeCount() == rangeCount); 883 const nsRange* range = selection->GetRangeAt(i); 884 MOZ_ASSERT(range); 885 if (MOZ_UNLIKELY(NS_WARN_IF(!range)) || 886 MOZ_UNLIKELY(NS_WARN_IF(!range->GetEndContainer()))) { 887 continue; 888 } 889 if (!lastRange) { 890 lastRange = range; 891 continue; 892 } 893 // In most cases, all composition string should be in same text node. 894 if (lastRange->GetEndContainer() == range->GetEndContainer()) { 895 if (lastRange->EndOffset() < range->EndOffset()) { 896 lastRange = range; 897 } 898 continue; 899 } 900 // However, if web apps have inserted different nodes in composition 901 // string, composition string may span 2 or more nodes. 902 if (lastRange->GetEndContainer() == 903 range->GetEndContainer()->GetNextSibling()) { 904 // Fast path for some known applications like Google Keep. 905 lastRange = range; 906 continue; 907 } 908 // Unfortunately, really slow path. 909 // The ranges should always have a common ancestor, hence, be comparable. 910 if (*nsContentUtils::ComparePoints(lastRange->EndRef(), 911 range->EndRef()) == -1) { 912 lastRange = range; 913 } 914 } 915 } 916 return lastRange ? lastRange->EndRef().AsRaw() : RawRangeBoundary(); 917 } 918 919 /****************************************************************************** 920 * TextComposition::CompositionEventDispatcher 921 ******************************************************************************/ 922 923 TextComposition::CompositionEventDispatcher::CompositionEventDispatcher( 924 TextComposition* aTextComposition, nsINode* aEventTarget, 925 EventMessage aEventMessage, const nsAString& aData, 926 bool aIsSynthesizedEvent) 927 : Runnable("TextComposition::CompositionEventDispatcher"), 928 mTextComposition(aTextComposition), 929 mEventTarget(aEventTarget), 930 mData(aData), 931 mEventMessage(aEventMessage), 932 mIsSynthesizedEvent(aIsSynthesizedEvent) {} 933 934 NS_IMETHODIMP 935 TextComposition::CompositionEventDispatcher::Run() { 936 // The widget can be different from the widget which has dispatched 937 // composition events because GetWidget() returns a widget which is proper 938 // for calling NotifyIME(). However, this must no be problem since both 939 // widget should share native IME context. Therefore, even if an event 940 // handler uses the widget for requesting IME to commit or cancel, it works. 941 nsCOMPtr<nsIWidget> widget(mTextComposition->GetWidget()); 942 if (!mTextComposition->IsValidStateForComposition(widget)) { 943 return NS_OK; // cannot dispatch any events anymore 944 } 945 946 RefPtr<nsPresContext> presContext = mTextComposition->mPresContext; 947 nsCOMPtr<nsINode> eventTarget = mEventTarget; 948 RefPtr<BrowserParent> browserParent = mTextComposition->mBrowserParent; 949 nsEventStatus status = nsEventStatus_eIgnore; 950 switch (mEventMessage) { 951 case eCompositionStart: { 952 WidgetCompositionEvent compStart(true, eCompositionStart, widget); 953 compStart.mNativeIMEContext = mTextComposition->mNativeContext; 954 WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText, 955 widget); 956 ContentEventHandler handler(presContext); 957 handler.OnQuerySelectedText(&querySelectedTextEvent); 958 NS_ASSERTION(querySelectedTextEvent.Succeeded(), 959 "Failed to get selected text"); 960 if (querySelectedTextEvent.FoundSelection()) { 961 compStart.mData = querySelectedTextEvent.mReply->DataRef(); 962 } 963 compStart.mFlags.mIsSynthesizedForTests = 964 mTextComposition->IsSynthesizedForTests(); 965 IMEStateManager::DispatchCompositionEvent( 966 eventTarget, presContext, browserParent, &compStart, &status, nullptr, 967 mIsSynthesizedEvent); 968 break; 969 } 970 case eCompositionChange: 971 case eCompositionCommitAsIs: 972 case eCompositionCommit: { 973 WidgetCompositionEvent compEvent(true, mEventMessage, widget); 974 compEvent.mNativeIMEContext = mTextComposition->mNativeContext; 975 if (mEventMessage != eCompositionCommitAsIs) { 976 compEvent.mData = mData; 977 } 978 compEvent.mFlags.mIsSynthesizedForTests = 979 mTextComposition->IsSynthesizedForTests(); 980 IMEStateManager::DispatchCompositionEvent( 981 eventTarget, presContext, browserParent, &compEvent, &status, nullptr, 982 mIsSynthesizedEvent); 983 break; 984 } 985 default: 986 MOZ_CRASH("Unsupported event"); 987 } 988 return NS_OK; 989 } 990 991 /****************************************************************************** 992 * TextCompositionArray 993 ******************************************************************************/ 994 995 TextCompositionArray::index_type TextCompositionArray::IndexOf( 996 const NativeIMEContext& aNativeIMEContext) { 997 if (!aNativeIMEContext.IsValid()) { 998 return NoIndex; 999 } 1000 for (index_type i = Length(); i > 0; --i) { 1001 if (ElementAt(i - 1)->GetNativeIMEContext() == aNativeIMEContext) { 1002 return i - 1; 1003 } 1004 } 1005 return NoIndex; 1006 } 1007 1008 TextCompositionArray::index_type TextCompositionArray::IndexOf( 1009 nsIWidget* aWidget) { 1010 return IndexOf(aWidget->GetNativeIMEContext()); 1011 } 1012 1013 TextCompositionArray::index_type TextCompositionArray::IndexOf( 1014 nsPresContext* aPresContext) { 1015 for (index_type i = Length(); i > 0; --i) { 1016 if (ElementAt(i - 1)->GetPresContext() == aPresContext) { 1017 return i - 1; 1018 } 1019 } 1020 return NoIndex; 1021 } 1022 1023 TextCompositionArray::index_type TextCompositionArray::IndexOf( 1024 nsPresContext* aPresContext, nsINode* aNode) { 1025 index_type index = IndexOf(aPresContext); 1026 if (index == NoIndex) { 1027 return NoIndex; 1028 } 1029 nsINode* node = ElementAt(index)->GetEventTargetNode(); 1030 return node == aNode ? index : NoIndex; 1031 } 1032 1033 TextComposition* TextCompositionArray::GetCompositionFor(nsIWidget* aWidget) { 1034 index_type i = IndexOf(aWidget); 1035 if (i == NoIndex) { 1036 return nullptr; 1037 } 1038 return ElementAt(i); 1039 } 1040 1041 TextComposition* TextCompositionArray::GetCompositionFor( 1042 const WidgetCompositionEvent* aCompositionEvent) { 1043 index_type i = IndexOf(aCompositionEvent->mNativeIMEContext); 1044 if (i == NoIndex) { 1045 return nullptr; 1046 } 1047 return ElementAt(i); 1048 } 1049 1050 TextComposition* TextCompositionArray::GetCompositionFor( 1051 nsPresContext* aPresContext) { 1052 index_type i = IndexOf(aPresContext); 1053 if (i == NoIndex) { 1054 return nullptr; 1055 } 1056 return ElementAt(i); 1057 } 1058 1059 TextComposition* TextCompositionArray::GetCompositionFor( 1060 nsPresContext* aPresContext, nsINode* aNode) { 1061 index_type i = IndexOf(aPresContext, aNode); 1062 if (i == NoIndex) { 1063 return nullptr; 1064 } 1065 return ElementAt(i); 1066 } 1067 1068 TextComposition* TextCompositionArray::GetCompositionInContent( 1069 nsPresContext* aPresContext, nsIContent* aContent) { 1070 // There should be only one composition per content object. 1071 for (TextComposition* const composition : Reversed(*this)) { 1072 nsINode* node = composition->GetEventTargetNode(); 1073 if (node && node->IsInclusiveFlatTreeDescendantOf(aContent)) { 1074 return composition; 1075 } 1076 } 1077 return nullptr; 1078 } 1079 1080 } // namespace mozilla