summaryrefslogtreecommitdiffstats
path: root/widget/ContentCache.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'widget/ContentCache.cpp')
-rw-r--r--widget/ContentCache.cpp2011
1 files changed, 2011 insertions, 0 deletions
diff --git a/widget/ContentCache.cpp b/widget/ContentCache.cpp
new file mode 100644
index 0000000000..b6093fb3f7
--- /dev/null
+++ b/widget/ContentCache.cpp
@@ -0,0 +1,2011 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "ContentCache.h"
+
+#include <utility>
+
+#include "IMEData.h"
+#include "TextEvents.h"
+
+#include "mozilla/Assertions.h"
+#include "mozilla/IMEStateManager.h"
+#include "mozilla/IntegerPrintfMacros.h"
+#include "mozilla/Logging.h"
+#include "mozilla/RefPtr.h"
+#include "mozilla/TextComposition.h"
+#include "mozilla/dom/BrowserParent.h"
+#include "nsExceptionHandler.h"
+#include "nsIWidget.h"
+#include "nsPrintfCString.h"
+
+namespace mozilla {
+
+using namespace dom;
+using namespace widget;
+
+static const char* GetBoolName(bool aBool) { return aBool ? "true" : "false"; }
+
+static const char* GetNotificationName(const IMENotification* aNotification) {
+ if (!aNotification) {
+ return "Not notification";
+ }
+ return ToChar(aNotification->mMessage);
+}
+
+/*****************************************************************************
+ * mozilla::ContentCache
+ *****************************************************************************/
+
+LazyLogModule sContentCacheLog("ContentCacheWidgets");
+
+bool ContentCache::IsValid() const {
+ if (mText.isNothing()) {
+ // mSelection and mCaret depend on mText.
+ if (NS_WARN_IF(mSelection.isSome()) || NS_WARN_IF(mCaret.isSome())) {
+ return false;
+ }
+ } else {
+ // mSelection depends on mText.
+ if (mSelection.isSome() && NS_WARN_IF(!mSelection->IsValidIn(*mText))) {
+ return false;
+ }
+
+ // mCaret depends on mSelection.
+ if (mCaret.isSome() &&
+ (NS_WARN_IF(mSelection.isNothing()) ||
+ NS_WARN_IF(!mSelection->mHasRange) ||
+ NS_WARN_IF(mSelection->StartOffset() != mCaret->Offset()))) {
+ return false;
+ }
+ }
+
+ // mTextRectArray stores character rects around composition string.
+ // Note that even if we fail to collect the rects, we may keep storing
+ // mCompositionStart.
+ if (mTextRectArray.isSome()) {
+ if (NS_WARN_IF(mCompositionStart.isNothing())) {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+void ContentCache::AssertIfInvalid() const {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ if (IsValid()) {
+ return;
+ }
+
+ // This text will appear in the crash reports without any permissions.
+ // Do not use `ToString` here to avoid to expose unexpected data with
+ // changing the type or `operator<<()`.
+ nsPrintfCString info(
+ "ContentCache={ mText=%s, mSelection=%s, mCaret=%s, mTextRectArray=%s, "
+ "mCompositionStart=%s }\n",
+ // Don't expose mText.ref() value for protecting the user's privacy.
+ mText.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ Length()=%zu }", mText->Length()).get(),
+ mSelection.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ mAnchor=%u, mFocus=%u }", mSelection->mAnchor,
+ mSelection->mFocus)
+ .get(),
+ mCaret.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ mOffset=%u }", mCaret->mOffset).get(),
+ mTextRectArray.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("{ Length()=%u }", mTextRectArray->Length()).get(),
+ mCompositionStart.isNothing()
+ ? "Nothing"
+ : nsPrintfCString("%u", mCompositionStart.value()).get());
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(false, "Invalid ContentCache data");
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+}
+
+/*****************************************************************************
+ * mozilla::ContentCacheInChild
+ *****************************************************************************/
+
+void ContentCacheInChild::Clear() {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info, ("0x%p Clear()", this));
+
+ mCompositionStart.reset();
+ mLastCommit.reset();
+ mText.reset();
+ mSelection.reset();
+ mFirstCharRect.SetEmpty();
+ mCaret.reset();
+ mTextRectArray.reset();
+ mLastCommitStringTextRectArray.reset();
+ mEditorRect.SetEmpty();
+}
+
+void ContentCacheInChild::OnCompositionEvent(
+ const WidgetCompositionEvent& aCompositionEvent) {
+ if (aCompositionEvent.CausesDOMCompositionEndEvent()) {
+ RefPtr<TextComposition> composition =
+ IMEStateManager::GetTextCompositionFor(aCompositionEvent.mWidget);
+ if (composition) {
+ nsAutoString lastCommitString;
+ if (aCompositionEvent.mMessage == eCompositionCommitAsIs) {
+ lastCommitString = composition->CommitStringIfCommittedAsIs();
+ } else {
+ lastCommitString = aCompositionEvent.mData;
+ }
+ // We don't need to store canceling information because this is required
+ // by undoing of last commit (Kakutei-Undo of Japanese IME).
+ if (!lastCommitString.IsEmpty()) {
+ mLastCommit = Some(OffsetAndData<uint32_t>(
+ composition->NativeOffsetOfStartComposition(), lastCommitString));
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p OnCompositionEvent(), stored last composition string data "
+ "(aCompositionEvent={ mMessage=%s, mData=\"%s\"}, mLastCommit=%s)",
+ this, ToChar(aCompositionEvent.mMessage),
+ PrintStringDetail(
+ aCompositionEvent.mData,
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get(),
+ ToString(mLastCommit).c_str()));
+ return;
+ }
+ }
+ }
+ if (mLastCommit.isSome()) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p OnCompositionEvent(), resetting the last composition string "
+ "data (aCompositionEvent={ mMessage=%s, mData=\"%s\"}, "
+ "mLastCommit=%s)",
+ this, ToChar(aCompositionEvent.mMessage),
+ PrintStringDetail(aCompositionEvent.mData,
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get(),
+ ToString(mLastCommit).c_str()));
+ mLastCommit.reset();
+ }
+}
+
+bool ContentCacheInChild::CacheAll(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheAll(aWidget=0x%p, aNotification=%s)", this, aWidget,
+ GetNotificationName(aNotification)));
+
+ const bool textCached = CacheText(aWidget, aNotification);
+ const bool editorRectCached = CacheEditorRect(aWidget, aNotification);
+ AssertIfInvalid();
+ return (textCached || editorRectCached) && IsValid();
+}
+
+bool ContentCacheInChild::CacheSelection(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheSelection(aWidget=0x%p, aNotification=%s), mText=%s", this,
+ aWidget, GetNotificationName(aNotification),
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get()));
+
+ mSelection.reset();
+ mCaret.reset();
+
+ if (mText.isNothing()) {
+ return false;
+ }
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
+ aWidget);
+ aWidget->DispatchEvent(&querySelectedTextEvent, status);
+ if (NS_WARN_IF(querySelectedTextEvent.Failed())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheSelection(), FAILED, couldn't retrieve the selected text",
+ this));
+ // XXX Allowing selection-independent character rects makes things
+ // complicated in the parent...
+ }
+ // ContentCache should store only editable content. Therefore, if current
+ // selection root is not editable, we don't need to store the selection, i.e.,
+ // let's treat it as there is no selection. However, if we already have
+ // previously editable text, let's store the selection even if it becomes
+ // uneditable because not doing so would create odd situation. E.g., IME may
+ // fail only querying selection after succeeded querying text.
+ else if (NS_WARN_IF(!querySelectedTextEvent.mReply->mIsEditableContent)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheSelection(), FAILED, editable content had already been "
+ "blurred",
+ this));
+ AssertIfInvalid();
+ return false;
+ } else {
+ mSelection.emplace(querySelectedTextEvent);
+ }
+
+ return (CacheCaretAndTextRects(aWidget, aNotification) ||
+ querySelectedTextEvent.Succeeded()) &&
+ IsValid();
+}
+
+bool ContentCacheInChild::CacheCaret(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ mCaret.reset();
+
+ if (mSelection.isNothing()) {
+ return false;
+ }
+
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheCaret(aWidget=0x%p, aNotification=%s)", this, aWidget,
+ GetNotificationName(aNotification)));
+
+ if (mSelection->mHasRange) {
+ // XXX Should be mSelection.mFocus?
+ const uint32_t offset = mSelection->StartOffset();
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryCaretRectEvent(true, eQueryCaretRect, aWidget);
+ queryCaretRectEvent.InitForQueryCaretRect(offset);
+ aWidget->DispatchEvent(&queryCaretRectEvent, status);
+ if (NS_WARN_IF(queryCaretRectEvent.Failed())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheCaret(), FAILED, couldn't retrieve the caret rect "
+ "at offset=%u",
+ this, offset));
+ return false;
+ }
+ mCaret.emplace(offset, queryCaretRectEvent.mReply->mRect);
+ }
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheCaret(), Succeeded, mSelection=%s, mCaret=%s", this,
+ ToString(mSelection).c_str(), ToString(mCaret).c_str()));
+ AssertIfInvalid();
+ return IsValid();
+}
+
+bool ContentCacheInChild::CacheEditorRect(
+ nsIWidget* aWidget, const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheEditorRect(aWidget=0x%p, aNotification=%s)", this,
+ aWidget, GetNotificationName(aNotification)));
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryEditorRectEvent(true, eQueryEditorRect, aWidget);
+ aWidget->DispatchEvent(&queryEditorRectEvent, status);
+ if (NS_WARN_IF(queryEditorRectEvent.Failed())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheEditorRect(), FAILED, couldn't retrieve the editor rect",
+ this));
+ return false;
+ }
+ // ContentCache should store only editable content. Therefore, if current
+ // selection root is not editable, we don't need to store the editor rect,
+ // i.e., let's treat it as there is no focused editor.
+ if (NS_WARN_IF(!queryEditorRectEvent.mReply->mIsEditableContent)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheText(), FAILED, editable content had already been "
+ "blurred",
+ this));
+ return false;
+ }
+ mEditorRect = queryEditorRectEvent.mReply->mRect;
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheEditorRect(), Succeeded, mEditorRect=%s", this,
+ ToString(mEditorRect).c_str()));
+ return true;
+}
+
+bool ContentCacheInChild::CacheCaretAndTextRects(
+ nsIWidget* aWidget, const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheCaretAndTextRects(aWidget=0x%p, aNotification=%s)", this,
+ aWidget, GetNotificationName(aNotification)));
+
+ const bool caretCached = CacheCaret(aWidget, aNotification);
+ const bool textRectsCached = CacheTextRects(aWidget, aNotification);
+ AssertIfInvalid();
+ return (caretCached || textRectsCached) && IsValid();
+}
+
+bool ContentCacheInChild::CacheText(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheText(aWidget=0x%p, aNotification=%s)", this, aWidget,
+ GetNotificationName(aNotification)));
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextContentEvent(true, eQueryTextContent,
+ aWidget);
+ queryTextContentEvent.InitForQueryTextContent(0, UINT32_MAX);
+ aWidget->DispatchEvent(&queryTextContentEvent, status);
+ if (NS_WARN_IF(queryTextContentEvent.Failed())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheText(), FAILED, couldn't retrieve whole text", this));
+ mText.reset();
+ }
+ // ContentCache should store only editable content. Therefore, if current
+ // selection root is not editable, we don't need to store the text, i.e.,
+ // let's treat it as there is no editable text.
+ else if (NS_WARN_IF(!queryTextContentEvent.mReply->mIsEditableContent)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheText(), FAILED, editable content had already been "
+ "blurred",
+ this));
+ mText.reset();
+ } else {
+ mText = Some(nsString(queryTextContentEvent.mReply->DataRef()));
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheText(), Succeeded, mText=%s", this,
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor)
+ .get()));
+ }
+
+ // Forget last commit range if string in the range is different from the
+ // last commit string.
+ if (mLastCommit.isSome() &&
+ (mText.isNothing() ||
+ nsDependentSubstring(mText.ref(), mLastCommit->StartOffset(),
+ mLastCommit->Length()) != mLastCommit->DataRef())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Debug,
+ ("0x%p CacheText(), resetting the last composition string data "
+ "(mLastCommit=%s, current string=\"%s\")",
+ this, ToString(mLastCommit).c_str(),
+ PrintStringDetail(
+ nsDependentSubstring(mText.ref(), mLastCommit->StartOffset(),
+ mLastCommit->Length()),
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get()));
+ mLastCommit.reset();
+ }
+
+ // If we fail to get editable text content, it must mean that there is no
+ // focused element anymore or focused element is not editable. In this case,
+ // we should not get selection of non-editable content
+ if (MOZ_UNLIKELY(mText.isNothing())) {
+ mSelection.reset();
+ mCaret.reset();
+ mTextRectArray.reset();
+ AssertIfInvalid();
+ return false;
+ }
+
+ return CacheSelection(aWidget, aNotification);
+}
+
+bool ContentCacheInChild::QueryCharRect(nsIWidget* aWidget, uint32_t aOffset,
+ LayoutDeviceIntRect& aCharRect) const {
+ aCharRect.SetEmpty();
+
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, aWidget);
+ queryTextRectEvent.InitForQueryTextRect(aOffset, 1);
+ aWidget->DispatchEvent(&queryTextRectEvent, status);
+ if (NS_WARN_IF(queryTextRectEvent.Failed())) {
+ return false;
+ }
+ aCharRect = queryTextRectEvent.mReply->mRect;
+
+ // Guarantee the rect is not empty.
+ if (NS_WARN_IF(!aCharRect.Height())) {
+ aCharRect.SetHeight(1);
+ }
+ if (NS_WARN_IF(!aCharRect.Width())) {
+ aCharRect.SetWidth(1);
+ }
+ return true;
+}
+
+bool ContentCacheInChild::QueryCharRectArray(nsIWidget* aWidget,
+ uint32_t aOffset, uint32_t aLength,
+ RectArray& aCharRectArray) const {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextRectsEvent(true, eQueryTextRectArray,
+ aWidget);
+ queryTextRectsEvent.InitForQueryTextRectArray(aOffset, aLength);
+ aWidget->DispatchEvent(&queryTextRectsEvent, status);
+ if (NS_WARN_IF(queryTextRectsEvent.Failed())) {
+ aCharRectArray.Clear();
+ return false;
+ }
+ aCharRectArray = std::move(queryTextRectsEvent.mReply->mRectArray);
+ return true;
+}
+
+bool ContentCacheInChild::CacheTextRects(nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheTextRects(aWidget=0x%p, aNotification=%s), mCaret=%s", this,
+ aWidget, GetNotificationName(aNotification), ToString(mCaret).c_str()));
+
+ if (mSelection.isSome()) {
+ mSelection->ClearRects();
+ }
+
+ // Retrieve text rects in composition string if there is.
+ RefPtr<TextComposition> textComposition =
+ IMEStateManager::GetTextCompositionFor(aWidget);
+ if (textComposition) {
+ // mCompositionStart may be updated by some composition event handlers.
+ // So, let's update it with the latest information.
+ mCompositionStart = Some(textComposition->NativeOffsetOfStartComposition());
+ // Note that TextComposition::String() may not be modified here because
+ // it's modified after all edit action listeners are performed but this
+ // is called while some of them are performed.
+ // FYI: For supporting IME which commits composition and restart new
+ // composition immediately, we should cache next character of current
+ // composition too.
+ uint32_t length = textComposition->LastData().Length() + 1;
+ mTextRectArray = Some(TextRectArray(mCompositionStart.value()));
+ if (NS_WARN_IF(!QueryCharRectArray(aWidget, mTextRectArray->mStart, length,
+ mTextRectArray->mRects))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve text rect array of the composition string",
+ this));
+ mTextRectArray.reset();
+ }
+ } else {
+ mCompositionStart.reset();
+ mTextRectArray.reset();
+ }
+
+ if (mSelection.isSome()) {
+ // Set mSelection->mAnchorCharRects
+ // If we've already have the rect in mTextRectArray, save the query cost.
+ if (mSelection->mHasRange && mTextRectArray.isSome() &&
+ mTextRectArray->IsOffsetInRange(mSelection->mAnchor) &&
+ (!mSelection->mAnchor ||
+ mTextRectArray->IsOffsetInRange(mSelection->mAnchor - 1))) {
+ mSelection->mAnchorCharRects[eNextCharRect] =
+ mTextRectArray->GetRect(mSelection->mAnchor);
+ if (mSelection->mAnchor) {
+ mSelection->mAnchorCharRects[ePrevCharRect] =
+ mTextRectArray->GetRect(mSelection->mAnchor - 1);
+ }
+ }
+ // Otherwise, get it from content even if there is no selection ranges.
+ else {
+ RectArray rects;
+ const uint32_t startOffset = mSelection->mHasRange && mSelection->mAnchor
+ ? mSelection->mAnchor - 1u
+ : 0u;
+ const uint32_t length =
+ mSelection->mHasRange && mSelection->mAnchor ? 2u : 1u;
+ if (NS_WARN_IF(
+ !QueryCharRectArray(aWidget, startOffset, length, rects))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, couldn't retrieve text rect "
+ "array around the selection anchor (%s)",
+ this,
+ mSelection ? ToString(mSelection->mAnchor).c_str() : "Nothing"));
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty());
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mAnchorCharRects[eNextCharRect].IsEmpty());
+ } else if (rects.Length()) {
+ if (rects.Length() > 1) {
+ mSelection->mAnchorCharRects[ePrevCharRect] = rects[0];
+ mSelection->mAnchorCharRects[eNextCharRect] = rects[1];
+ } else {
+ mSelection->mAnchorCharRects[eNextCharRect] = rects[0];
+ MOZ_ASSERT(mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty());
+ }
+ }
+ }
+
+ // Set mSelection->mFocusCharRects
+ // If selection is collapsed (including no selection case), the focus char
+ // rects are same as the anchor char rects so that we can just copy them.
+ if (mSelection->IsCollapsed()) {
+ mSelection->mFocusCharRects[0] = mSelection->mAnchorCharRects[0];
+ mSelection->mFocusCharRects[1] = mSelection->mAnchorCharRects[1];
+ }
+ // If the selection range is in mTextRectArray, save the query cost.
+ else if (mTextRectArray.isSome() &&
+ mTextRectArray->IsOffsetInRange(mSelection->mFocus) &&
+ (!mSelection->mFocus ||
+ mTextRectArray->IsOffsetInRange(mSelection->mFocus - 1))) {
+ MOZ_ASSERT(mSelection->mHasRange);
+ mSelection->mFocusCharRects[eNextCharRect] =
+ mTextRectArray->GetRect(mSelection->mFocus);
+ if (mSelection->mFocus) {
+ mSelection->mFocusCharRects[ePrevCharRect] =
+ mTextRectArray->GetRect(mSelection->mFocus - 1);
+ }
+ }
+ // Otherwise, including no selection range cases, need to query the rects.
+ else {
+ MOZ_ASSERT(mSelection->mHasRange);
+ RectArray rects;
+ const uint32_t startOffset =
+ mSelection->mFocus ? mSelection->mFocus - 1u : 0u;
+ const uint32_t length = mSelection->mFocus ? 2u : 1u;
+ if (NS_WARN_IF(
+ !QueryCharRectArray(aWidget, startOffset, length, rects))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, couldn't retrieve text rect "
+ "array around the selection focus (%s)",
+ this,
+ mSelection ? ToString(mSelection->mFocus).c_str() : "Nothing"));
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mFocusCharRects[ePrevCharRect].IsEmpty());
+ MOZ_ASSERT_IF(mSelection.isSome(),
+ mSelection->mFocusCharRects[eNextCharRect].IsEmpty());
+ } else if (NS_WARN_IF(mSelection.isNothing())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, mSelection was reset during "
+ "the call of QueryCharRectArray",
+ this));
+ } else {
+ if (rects.Length() > 1) {
+ mSelection->mFocusCharRects[ePrevCharRect] = rects[0];
+ mSelection->mFocusCharRects[eNextCharRect] = rects[1];
+ } else if (rects.Length()) {
+ mSelection->mFocusCharRects[eNextCharRect] = rects[0];
+ MOZ_ASSERT(mSelection->mFocusCharRects[ePrevCharRect].IsEmpty());
+ }
+ }
+ }
+ }
+
+ // If there is a non-collapsed selection range, let's query the whole selected
+ // text rect. Note that the result cannot be computed from first character
+ // rect and last character rect of the selection because they both may be in
+ // middle of different line.
+ if (mSelection.isSome() && mSelection->mHasRange &&
+ !mSelection->IsCollapsed()) {
+ nsEventStatus status = nsEventStatus_eIgnore;
+ WidgetQueryContentEvent queryTextRectEvent(true, eQueryTextRect, aWidget);
+ queryTextRectEvent.InitForQueryTextRect(mSelection->StartOffset(),
+ mSelection->Length());
+ aWidget->DispatchEvent(&queryTextRectEvent, status);
+ if (NS_WARN_IF(queryTextRectEvent.Failed())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve text rect of whole selected text",
+ this));
+ } else {
+ mSelection->mRect = queryTextRectEvent.mReply->mRect;
+ }
+ }
+
+ // Even if there is no selection range, we should have the first character
+ // rect for the last resort of suggesting position of IME UI.
+ if (mSelection.isSome() && mSelection->mHasRange && !mSelection->mFocus) {
+ mFirstCharRect = mSelection->mFocusCharRects[eNextCharRect];
+ } else if (mSelection.isSome() && mSelection->mHasRange &&
+ mSelection->mFocus == 1) {
+ mFirstCharRect = mSelection->mFocusCharRects[ePrevCharRect];
+ } else if (mSelection.isSome() && mSelection->mHasRange &&
+ !mSelection->mAnchor) {
+ mFirstCharRect = mSelection->mAnchorCharRects[eNextCharRect];
+ } else if (mSelection.isSome() && mSelection->mHasRange &&
+ mSelection->mAnchor == 1) {
+ mFirstCharRect = mSelection->mFocusCharRects[ePrevCharRect];
+ } else if (mTextRectArray.isSome() && mTextRectArray->IsOffsetInRange(0u)) {
+ mFirstCharRect = mTextRectArray->GetRect(0u);
+ } else {
+ LayoutDeviceIntRect charRect;
+ if (MOZ_UNLIKELY(NS_WARN_IF(!QueryCharRect(aWidget, 0, charRect)))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve first char rect",
+ this));
+ mFirstCharRect.SetEmpty();
+ } else {
+ mFirstCharRect = charRect;
+ }
+ }
+
+ // Finally, let's cache the last commit string's character rects until
+ // selection change or something other editing because user may reconvert
+ // or undo the last commit. Then, IME requires the character rects for
+ // positioning their UI.
+ if (mLastCommit.isSome()) {
+ mLastCommitStringTextRectArray =
+ Some(TextRectArray(mLastCommit->StartOffset()));
+ if (mLastCommit->Length() == 1 && mSelection.isSome() &&
+ mSelection->mHasRange &&
+ mSelection->mAnchor - 1 == mLastCommit->StartOffset() &&
+ !mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty()) {
+ mLastCommitStringTextRectArray->mRects.AppendElement(
+ mSelection->mAnchorCharRects[ePrevCharRect]);
+ } else if (NS_WARN_IF(!QueryCharRectArray(
+ aWidget, mLastCommit->StartOffset(), mLastCommit->Length(),
+ mLastCommitStringTextRectArray->mRects))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p CacheTextRects(), FAILED, "
+ "couldn't retrieve text rect array of the last commit string",
+ this));
+ mLastCommitStringTextRectArray.reset();
+ mLastCommit.reset();
+ }
+ MOZ_ASSERT((mLastCommitStringTextRectArray.isSome()
+ ? mLastCommitStringTextRectArray->mRects.Length()
+ : 0) == (mLastCommit.isSome() ? mLastCommit->Length() : 0));
+ } else {
+ mLastCommitStringTextRectArray.reset();
+ }
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p CacheTextRects(), Succeeded, "
+ "mText=%s, mTextRectArray=%s, mSelection=%s, "
+ "mFirstCharRect=%s, mLastCommitStringTextRectArray=%s",
+ this,
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get(),
+ ToString(mTextRectArray).c_str(), ToString(mSelection).c_str(),
+ ToString(mFirstCharRect).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+ AssertIfInvalid();
+ return IsValid();
+}
+
+bool ContentCacheInChild::SetSelection(
+ nsIWidget* aWidget,
+ const IMENotification::SelectionChangeDataBase& aSelectionChangeData) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p SetSelection(aSelectionChangeData=%s), mText=%s", this,
+ ToString(aSelectionChangeData).c_str(),
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get()));
+
+ if (MOZ_UNLIKELY(mText.isNothing())) {
+ return false;
+ }
+
+ mSelection = Some(Selection(aSelectionChangeData));
+
+ if (mLastCommit.isSome()) {
+ // Forget last commit string range if selection is not collapsed
+ // at end of the last commit string.
+ if (!mSelection->mHasRange || !mSelection->IsCollapsed() ||
+ mSelection->mAnchor != mLastCommit->EndOffset()) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p SetSelection(), forgetting last commit composition data "
+ "(mSelection=%s, mLastCommit=%s)",
+ this, ToString(mSelection).c_str(), ToString(mLastCommit).c_str()));
+ mLastCommit.reset();
+ }
+ }
+
+ CacheCaret(aWidget);
+ CacheTextRects(aWidget);
+
+ return mSelection.isSome() && IsValid();
+}
+
+/*****************************************************************************
+ * mozilla::ContentCacheInParent
+ *****************************************************************************/
+
+ContentCacheInParent::ContentCacheInParent(BrowserParent& aBrowserParent)
+ : mBrowserParent(aBrowserParent),
+ mCommitStringByRequest(nullptr),
+ mPendingCommitLength(0),
+ mIsChildIgnoringCompositionEvents(false) {}
+
+void ContentCacheInParent::AssignContent(const ContentCache& aOther,
+ nsIWidget* aWidget,
+ const IMENotification* aNotification) {
+ MOZ_DIAGNOSTIC_ASSERT(aOther.IsValid());
+
+ mText = aOther.mText;
+ mSelection = aOther.mSelection;
+ mFirstCharRect = aOther.mFirstCharRect;
+ mCaret = aOther.mCaret;
+ mTextRectArray = aOther.mTextRectArray;
+ mLastCommitStringTextRectArray = aOther.mLastCommitStringTextRectArray;
+ mEditorRect = aOther.mEditorRect;
+
+ // Only when there is one composition, the TextComposition instance in this
+ // process is managing the composition in the remote process. Therefore,
+ // we shouldn't update composition start offset of TextComposition with
+ // old composition which is still being handled by the child process.
+ if (WidgetHasComposition() && mHandlingCompositions.Length() == 1 &&
+ mCompositionStart.isSome()) {
+ IMEStateManager::MaybeStartOffsetUpdatedInChild(aWidget,
+ mCompositionStart.value());
+ }
+
+ // When this instance allows to query content relative to composition string,
+ // we should modify mCompositionStart with the latest information in the
+ // remote process because now we have the information around the composition
+ // string.
+ mCompositionStartInChild = aOther.mCompositionStart;
+ if (WidgetHasComposition() || HasPendingCommit()) {
+ if (mCompositionStartInChild.isSome()) {
+ if (mCompositionStart.valueOr(UINT32_MAX) !=
+ mCompositionStartInChild.value()) {
+ mCompositionStart = mCompositionStartInChild;
+ mPendingCommitLength = 0;
+ }
+ } else if (mCompositionStart.isSome() && mSelection.isSome() &&
+ mSelection->mHasRange &&
+ mCompositionStart.value() != mSelection->StartOffset()) {
+ mCompositionStart = Some(mSelection->StartOffset());
+ mPendingCommitLength = 0;
+ }
+ }
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p AssignContent(aNotification=%s), "
+ "Succeeded, mText=%s, mSelection=%s, mFirstCharRect=%s, "
+ "mCaret=%s, mTextRectArray=%s, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, mCompositionStart=%s, "
+ "mPendingCommitLength=%u, mEditorRect=%s, "
+ "mLastCommitStringTextRectArray=%s",
+ this, GetNotificationName(aNotification),
+ PrintStringDetail(mText, PrintStringDetail::kMaxLengthForEditor).get(),
+ ToString(mSelection).c_str(), ToString(mFirstCharRect).c_str(),
+ ToString(mCaret).c_str(), ToString(mTextRectArray).c_str(),
+ GetBoolName(WidgetHasComposition()), mHandlingCompositions.Length(),
+ ToString(mCompositionStart).c_str(), mPendingCommitLength,
+ ToString(mEditorRect).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+}
+
+bool ContentCacheInParent::HandleQueryContentEvent(
+ WidgetQueryContentEvent& aEvent, nsIWidget* aWidget) const {
+ MOZ_ASSERT(aWidget);
+
+ // ContentCache doesn't store offset of its start with XP linebreaks.
+ // So, we don't support to query contents relative to composition start
+ // offset with XP linebreaks.
+ if (NS_WARN_IF(!aEvent.mUseNativeLineBreak)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to query with XP "
+ "linebreaks",
+ this));
+ return false;
+ }
+
+ if (NS_WARN_IF(!aEvent.mInput.IsValidOffset())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to invalid offset", this));
+ return false;
+ }
+
+ if (NS_WARN_IF(!aEvent.mInput.IsValidEventMessage(aEvent.mMessage))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to invalid event message",
+ this));
+ return false;
+ }
+
+ bool isRelativeToInsertionPoint = aEvent.mInput.mRelativeToInsertionPoint;
+ if (isRelativeToInsertionPoint) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ ("0x%p HandleQueryContentEvent(), "
+ "making offset absolute... aEvent={ mMessage=%s, mInput={ "
+ "mOffset=%" PRId64 ", mLength=%" PRIu32 " } }, "
+ "WidgetHasComposition()=%s, HasPendingCommit()=%s, "
+ "mCompositionStart=%" PRIu32 ", "
+ "mPendingCommitLength=%" PRIu32 ", mSelection=%s",
+ this, ToChar(aEvent.mMessage), aEvent.mInput.mOffset,
+ aEvent.mInput.mLength, GetBoolName(WidgetHasComposition()),
+ GetBoolName(HasPendingCommit()), mCompositionStart.valueOr(UINT32_MAX),
+ mPendingCommitLength, ToString(mSelection).c_str()));
+ if (WidgetHasComposition() || HasPendingCommit()) {
+ if (NS_WARN_IF(mCompositionStart.isNothing()) ||
+ NS_WARN_IF(!aEvent.mInput.MakeOffsetAbsolute(
+ mCompositionStart.value() + mPendingCommitLength))) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to "
+ "aEvent.mInput.MakeOffsetAbsolute(mCompositionStart + "
+ "mPendingCommitLength) failure, "
+ "mCompositionStart=%" PRIu32 ", mPendingCommitLength=%" PRIu32 ", "
+ "aEvent={ mMessage=%s, mInput={ mOffset=%" PRId64
+ ", mLength=%" PRIu32 " } }",
+ this, mCompositionStart.valueOr(UINT32_MAX), mPendingCommitLength,
+ ToChar(aEvent.mMessage), aEvent.mInput.mOffset,
+ aEvent.mInput.mLength));
+ return false;
+ }
+ } else if (NS_WARN_IF(mSelection.isNothing())) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to mSelection is "
+ "Nothing",
+ this));
+ return false;
+ } else if (NS_WARN_IF(mSelection->mHasRange)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to there is no "
+ "selection range, but the query requested with relative offset "
+ "from selection",
+ this));
+ return false;
+ } else if (NS_WARN_IF(!aEvent.mInput.MakeOffsetAbsolute(
+ mSelection->StartOffset() + mPendingCommitLength))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to "
+ "aEvent.mInput.MakeOffsetAbsolute(mSelection->StartOffset() + "
+ "mPendingCommitLength) failure, mSelection=%s, "
+ "mPendingCommitLength=%" PRIu32 ", aEvent={ mMessage=%s, "
+ "mInput={ mOffset=%" PRId64 ", mLength=%" PRIu32 " } }",
+ this, ToString(mSelection).c_str(), mPendingCommitLength,
+ ToChar(aEvent.mMessage), aEvent.mInput.mOffset,
+ aEvent.mInput.mLength));
+ return false;
+ }
+ }
+
+ switch (aEvent.mMessage) {
+ case eQuerySelectedText:
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ "
+ "mMessage=eQuerySelectedText }, aWidget=0x%p)",
+ this, aWidget));
+ if (MOZ_UNLIKELY(NS_WARN_IF(mSelection.isNothing()))) {
+ // If content cache hasn't been initialized properly, make the query
+ // failed.
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED because mSelection "
+ "is Nothing",
+ this));
+ return false;
+ }
+ MOZ_DIAGNOSTIC_ASSERT(mText.isSome());
+ MOZ_DIAGNOSTIC_ASSERT(mSelection->IsValidIn(*mText));
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ if (mSelection->mHasRange) {
+ if (MOZ_LIKELY(mText.isSome())) {
+ aEvent.mReply->mOffsetAndData.emplace(
+ mSelection->StartOffset(),
+ Substring(mText.ref(), mSelection->StartOffset(),
+ mSelection->Length()),
+ OffsetAndDataFor::SelectedString);
+ } else {
+ // TODO: Investigate this case. I find this during
+ // test_mousecapture.xhtml on Linux.
+ aEvent.mReply->mOffsetAndData.emplace(
+ 0u, EmptyString(), OffsetAndDataFor::SelectedString);
+ }
+ }
+ aEvent.mReply->mWritingMode = mSelection->mWritingMode;
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQuerySelectedText, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ case eQueryTextContent: {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ "
+ "mMessage=eQueryTextContent, mInput={ mOffset=%" PRId64
+ ", mLength=%u } }, aWidget=0x%p), mText->Length()=%zu",
+ this, aEvent.mInput.mOffset, aEvent.mInput.mLength, aWidget,
+ mText.isSome() ? mText->Length() : 0u));
+ if (MOZ_UNLIKELY(NS_WARN_IF(mText.isNothing()))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED because "
+ "there is no text data",
+ this));
+ return false;
+ }
+ const uint32_t inputOffset = aEvent.mInput.mOffset;
+ const uint32_t inputEndOffset = std::min<uint32_t>(
+ aEvent.mInput.EndOffset(), mText.isSome() ? mText->Length() : 0u);
+ if (MOZ_UNLIKELY(NS_WARN_IF(inputEndOffset < inputOffset))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED because "
+ "inputOffset=%u is larger than inputEndOffset=%u",
+ this, inputOffset, inputEndOffset));
+ return false;
+ }
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ const nsAString& textInQueriedRange =
+ inputEndOffset > inputOffset
+ ? static_cast<const nsAString&>(Substring(
+ mText.ref(), inputOffset, inputEndOffset - inputOffset))
+ : static_cast<const nsAString&>(EmptyString());
+ aEvent.mReply->mOffsetAndData.emplace(inputOffset, textInQueriedRange,
+ OffsetAndDataFor::EditorString);
+ // TODO: Support font ranges
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryTextContent, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+ case eQueryTextRect: {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent("
+ "aEvent={ mMessage=eQueryTextRect, mInput={ mOffset=%" PRId64
+ ", mLength=%u } }, aWidget=0x%p), mText->Length()=%zu",
+ this, aEvent.mInput.mOffset, aEvent.mInput.mLength, aWidget,
+ mText.isSome() ? mText->Length() : 0u));
+ // Note that if the query is relative to insertion point, the query was
+ // probably requested by native IME. In such case, we should return
+ // non-empty rect since returning failure causes IME showing its window
+ // at odd position.
+ LayoutDeviceIntRect textRect;
+ if (aEvent.mInput.mLength) {
+ if (MOZ_UNLIKELY(NS_WARN_IF(
+ !GetUnionTextRects(aEvent.mInput.mOffset, aEvent.mInput.mLength,
+ isRelativeToInsertionPoint, textRect)))) {
+ // XXX We don't have cache for this request.
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED to get union rect",
+ this));
+ return false;
+ }
+ } else {
+ // If the length is 0, we should return caret rect instead.
+ if (NS_WARN_IF(!GetCaretRect(aEvent.mInput.mOffset,
+ isRelativeToInsertionPoint, textRect))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED to get caret rect",
+ this));
+ return false;
+ }
+ }
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ aEvent.mReply->mRect = textRect;
+ const nsAString& textInQueriedRange =
+ mText.isSome() && aEvent.mInput.mOffset <
+ static_cast<int64_t>(
+ mText.isSome() ? mText->Length() : 0u)
+ ? static_cast<const nsAString&>(
+ Substring(mText.ref(), aEvent.mInput.mOffset,
+ mText->Length() >= aEvent.mInput.EndOffset()
+ ? aEvent.mInput.mLength
+ : UINT32_MAX))
+ : static_cast<const nsAString&>(EmptyString());
+ aEvent.mReply->mOffsetAndData.emplace(aEvent.mInput.mOffset,
+ textInQueriedRange,
+ OffsetAndDataFor::EditorString);
+ // XXX This may be wrong if storing range isn't in the selection range.
+ aEvent.mReply->mWritingMode =
+ mSelection.isSome() ? mSelection->mWritingMode : WritingMode();
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryTextRect mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+ case eQueryCaretRect: {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ mMessage=eQueryCaretRect, "
+ "mInput={ mOffset=%" PRId64
+ " } }, aWidget=0x%p), mText->Length()=%zu",
+ this, aEvent.mInput.mOffset, aWidget,
+ mText.isSome() ? mText->Length() : 0u));
+ // Note that if the query is relative to insertion point, the query was
+ // probably requested by native IME. In such case, we should return
+ // non-empty rect since returning failure causes IME showing its window
+ // at odd position.
+ LayoutDeviceIntRect caretRect;
+ if (NS_WARN_IF(!GetCaretRect(aEvent.mInput.mOffset,
+ isRelativeToInsertionPoint, caretRect))) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(),FAILED to get caret rect",
+ this));
+ return false;
+ }
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ aEvent.mReply->mRect = caretRect;
+ aEvent.mReply->mOffsetAndData.emplace(aEvent.mInput.mOffset,
+ EmptyString(),
+ OffsetAndDataFor::SelectedString);
+ // TODO: Set mWritingMode here
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryCaretRect, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+ case eQueryEditorRect:
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(aEvent={ "
+ "mMessage=eQueryEditorRect }, aWidget=0x%p)",
+ this, aWidget));
+ // XXX This query should fail if no editable elmenet has focus. Or,
+ // perhaps, should return rect of the window instead.
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ aEvent.mReply->mRect = mEditorRect;
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=eQueryEditorRect, mReply=%s }",
+ this, ToString(aEvent.mReply).c_str()));
+ return true;
+ default:
+ aEvent.EmplaceReply();
+ aEvent.mReply->mFocusedWidget = aWidget;
+ if (NS_WARN_IF(aEvent.Failed())) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Error,
+ ("0x%p HandleQueryContentEvent(), FAILED due to not set enough "
+ "data, aEvent={ mMessage=%s, mReply=%s }",
+ this, ToChar(aEvent.mMessage), ToString(aEvent.mReply).c_str()));
+ return false;
+ }
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p HandleQueryContentEvent(), Succeeded, aEvent={ "
+ "mMessage=%s, mReply=%s }",
+ this, ToChar(aEvent.mMessage), ToString(aEvent.mReply).c_str()));
+ return true;
+ }
+}
+
+bool ContentCacheInParent::GetTextRect(uint32_t aOffset,
+ bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aTextRect) const {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p GetTextRect(aOffset=%u, aRoundToExistingOffset=%s), "
+ "mTextRectArray=%s, mSelection=%s, mLastCommitStringTextRectArray=%s",
+ this, aOffset, GetBoolName(aRoundToExistingOffset),
+ ToString(mTextRectArray).c_str(), ToString(mSelection).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+
+ if (!aOffset) {
+ NS_WARNING_ASSERTION(!mFirstCharRect.IsEmpty(), "empty rect");
+ aTextRect = mFirstCharRect;
+ return !aTextRect.IsEmpty();
+ }
+ if (mSelection.isSome() && mSelection->mHasRange) {
+ if (aOffset == mSelection->mAnchor) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[eNextCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mAnchorCharRects[eNextCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ if (mSelection->mAnchor && aOffset == mSelection->mAnchor - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mAnchorCharRects[ePrevCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ if (aOffset == mSelection->mFocus) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[eNextCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mFocusCharRects[eNextCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ if (mSelection->mFocus && aOffset == mSelection->mFocus - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[ePrevCharRect].IsEmpty(), "empty rect");
+ aTextRect = mSelection->mFocusCharRects[ePrevCharRect];
+ return !aTextRect.IsEmpty();
+ }
+ }
+
+ if (mTextRectArray.isSome() && mTextRectArray->IsOffsetInRange(aOffset)) {
+ aTextRect = mTextRectArray->GetRect(aOffset);
+ return !aTextRect.IsEmpty();
+ }
+
+ if (mLastCommitStringTextRectArray.isSome() &&
+ mLastCommitStringTextRectArray->IsOffsetInRange(aOffset)) {
+ aTextRect = mLastCommitStringTextRectArray->GetRect(aOffset);
+ return !aTextRect.IsEmpty();
+ }
+
+ if (!aRoundToExistingOffset) {
+ aTextRect.SetEmpty();
+ return false;
+ }
+
+ if (mTextRectArray.isNothing() || !mTextRectArray->HasRects()) {
+ // If there are no rects in mTextRectArray, we should refer the start of
+ // the selection if there is because IME must query a char rect around it if
+ // there is no composition.
+ if (mSelection.isNothing()) {
+ // Unfortunately, there is no data about text rect...
+ aTextRect.SetEmpty();
+ return false;
+ }
+ aTextRect = mSelection->StartCharRect();
+ return !aTextRect.IsEmpty();
+ }
+
+ // Although we may have mLastCommitStringTextRectArray here and it must have
+ // previous character rects at selection. However, we should stop using it
+ // because it's stored really short time after commiting a composition.
+ // So, multiple query may return different rect and it may cause flickerling
+ // the IME UI.
+ uint32_t offset = aOffset;
+ if (offset < mTextRectArray->StartOffset()) {
+ offset = mTextRectArray->StartOffset();
+ } else {
+ offset = mTextRectArray->EndOffset() - 1;
+ }
+ aTextRect = mTextRectArray->GetRect(offset);
+ return !aTextRect.IsEmpty();
+}
+
+bool ContentCacheInParent::GetUnionTextRects(
+ uint32_t aOffset, uint32_t aLength, bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aUnionTextRect) const {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p GetUnionTextRects(aOffset=%u, "
+ "aLength=%u, aRoundToExistingOffset=%s), mTextRectArray=%s, "
+ "mSelection=%s, mLastCommitStringTextRectArray=%s",
+ this, aOffset, aLength, GetBoolName(aRoundToExistingOffset),
+ ToString(mTextRectArray).c_str(), ToString(mSelection).c_str(),
+ ToString(mLastCommitStringTextRectArray).c_str()));
+
+ CheckedInt<uint32_t> endOffset = CheckedInt<uint32_t>(aOffset) + aLength;
+ if (!endOffset.isValid()) {
+ return false;
+ }
+
+ if (mSelection.isSome() && !mSelection->IsCollapsed() &&
+ aOffset == mSelection->StartOffset() && aLength == mSelection->Length()) {
+ NS_WARNING_ASSERTION(!mSelection->mRect.IsEmpty(), "empty rect");
+ aUnionTextRect = mSelection->mRect;
+ return !aUnionTextRect.IsEmpty();
+ }
+
+ if (aLength == 1) {
+ if (!aOffset) {
+ NS_WARNING_ASSERTION(!mFirstCharRect.IsEmpty(), "empty rect");
+ aUnionTextRect = mFirstCharRect;
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (mSelection.isSome() && mSelection->mHasRange) {
+ if (aOffset == mSelection->mAnchor) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[eNextCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mAnchorCharRects[eNextCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (mSelection->mAnchor && aOffset == mSelection->mAnchor - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mAnchorCharRects[ePrevCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mAnchorCharRects[ePrevCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (aOffset == mSelection->mFocus) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[eNextCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mFocusCharRects[eNextCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ if (mSelection->mFocus && aOffset == mSelection->mFocus - 1) {
+ NS_WARNING_ASSERTION(
+ !mSelection->mFocusCharRects[ePrevCharRect].IsEmpty(),
+ "empty rect");
+ aUnionTextRect = mSelection->mFocusCharRects[ePrevCharRect];
+ return !aUnionTextRect.IsEmpty();
+ }
+ }
+ }
+
+ // Even if some text rects are not cached of the queried range,
+ // we should return union rect when the first character's rect is cached
+ // since the first character rect is important and the others are not so
+ // in most cases.
+
+ if (!aOffset && mSelection.isSome() && mSelection->mHasRange &&
+ aOffset != mSelection->mAnchor && aOffset != mSelection->mFocus &&
+ (mTextRectArray.isNothing() ||
+ !mTextRectArray->IsOffsetInRange(aOffset)) &&
+ (mLastCommitStringTextRectArray.isNothing() ||
+ !mLastCommitStringTextRectArray->IsOffsetInRange(aOffset))) {
+ // The first character rect isn't cached.
+ return false;
+ }
+
+ // Use mLastCommitStringTextRectArray only when it overlaps with aOffset
+ // even if aROundToExistingOffset is true for avoiding flickerling IME UI.
+ // See the last comment in GetTextRect() for the detail.
+ if (mLastCommitStringTextRectArray.isSome() &&
+ mLastCommitStringTextRectArray->IsOverlappingWith(aOffset, aLength)) {
+ aUnionTextRect =
+ mLastCommitStringTextRectArray->GetUnionRectAsFarAsPossible(
+ aOffset, aLength, aRoundToExistingOffset);
+ } else {
+ aUnionTextRect.SetEmpty();
+ }
+
+ if (mTextRectArray.isSome() &&
+ ((aRoundToExistingOffset && mTextRectArray->HasRects()) ||
+ mTextRectArray->IsOverlappingWith(aOffset, aLength))) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mTextRectArray->GetUnionRectAsFarAsPossible(
+ aOffset, aLength, aRoundToExistingOffset));
+ }
+
+ if (!aOffset) {
+ aUnionTextRect = aUnionTextRect.Union(mFirstCharRect);
+ }
+ if (mSelection.isSome() && mSelection->mHasRange) {
+ if (aOffset <= mSelection->mAnchor &&
+ mSelection->mAnchor < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mAnchorCharRects[eNextCharRect]);
+ }
+ if (mSelection->mAnchor && aOffset <= mSelection->mAnchor - 1 &&
+ mSelection->mAnchor - 1 < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mAnchorCharRects[ePrevCharRect]);
+ }
+ if (aOffset <= mSelection->mFocus &&
+ mSelection->mFocus < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mFocusCharRects[eNextCharRect]);
+ }
+ if (mSelection->mFocus && aOffset <= mSelection->mFocus - 1 &&
+ mSelection->mFocus - 1 < endOffset.value()) {
+ aUnionTextRect =
+ aUnionTextRect.Union(mSelection->mFocusCharRects[ePrevCharRect]);
+ }
+ }
+
+ return !aUnionTextRect.IsEmpty();
+}
+
+bool ContentCacheInParent::GetCaretRect(uint32_t aOffset,
+ bool aRoundToExistingOffset,
+ LayoutDeviceIntRect& aCaretRect) const {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p GetCaretRect(aOffset=%u, aRoundToExistingOffset=%s), "
+ "mCaret=%s, mTextRectArray=%s, mSelection=%s, mFirstCharRect=%s",
+ this, aOffset, GetBoolName(aRoundToExistingOffset),
+ ToString(mCaret).c_str(), ToString(mTextRectArray).c_str(),
+ ToString(mSelection).c_str(), ToString(mFirstCharRect).c_str()));
+
+ if (mCaret.isSome() && mCaret->mOffset == aOffset) {
+ aCaretRect = mCaret->mRect;
+ return true;
+ }
+
+ // Guess caret rect from the text rect if it's stored.
+ if (!GetTextRect(aOffset, aRoundToExistingOffset, aCaretRect)) {
+ // There might be previous character rect in the cache. If so, we can
+ // guess the caret rect with it.
+ if (!aOffset ||
+ !GetTextRect(aOffset - 1, aRoundToExistingOffset, aCaretRect)) {
+ aCaretRect.SetEmpty();
+ return false;
+ }
+
+ if (mSelection.isSome() && mSelection->mWritingMode.IsVertical()) {
+ aCaretRect.MoveToY(aCaretRect.YMost());
+ } else {
+ // XXX bidi-unaware.
+ aCaretRect.MoveToX(aCaretRect.XMost());
+ }
+ }
+
+ // XXX This is not bidi aware because we don't cache each character's
+ // direction. However, this is usually used by IME, so, assuming the
+ // character is in LRT context must not cause any problem.
+ if (mSelection.isSome() && mSelection->mWritingMode.IsVertical()) {
+ aCaretRect.SetHeight(mCaret.isSome() ? mCaret->mRect.Height() : 1);
+ } else {
+ aCaretRect.SetWidth(mCaret.isSome() ? mCaret->mRect.Width() : 1);
+ }
+ return true;
+}
+
+bool ContentCacheInParent::OnCompositionEvent(
+ const WidgetCompositionEvent& aCompositionEvent) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p OnCompositionEvent(aCompositionEvent={ "
+ "mMessage=%s, mData=\"%s\", mRanges->Length()=%zu }), "
+ "PendingEventsNeedingAck()=%u, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s, mCommitStringByRequest=0x%p",
+ this, ToChar(aCompositionEvent.mMessage),
+ PrintStringDetail(aCompositionEvent.mData,
+ PrintStringDetail::kMaxLengthForCompositionString)
+ .get(),
+ aCompositionEvent.mRanges ? aCompositionEvent.mRanges->Length() : 0,
+ PendingEventsNeedingAck(), GetBoolName(WidgetHasComposition()),
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents), mCommitStringByRequest));
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mDispatchedEventMessages.AppendElement(aCompositionEvent.mMessage);
+#endif // #ifdef MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ // We must be able to simulate the selection because
+ // we might not receive selection updates in time
+ if (!WidgetHasComposition()) {
+ if (mCompositionStartInChild.isSome()) {
+ // If there is pending composition in the remote process, let's use
+ // its start offset temporarily because this stores a lot of information
+ // around it and the user must look around there, so, showing some UI
+ // around it must make sense.
+ mCompositionStart = mCompositionStartInChild;
+ } else {
+ mCompositionStart = Some(mSelection.isSome() && mSelection->mHasRange
+ ? mSelection->StartOffset()
+ : 0u);
+ }
+ MOZ_ASSERT(aCompositionEvent.mMessage == eCompositionStart);
+ mHandlingCompositions.AppendElement(
+ HandlingCompositionData(aCompositionEvent.mCompositionId));
+ }
+
+ mHandlingCompositions.LastElement().mSentCommitEvent =
+ aCompositionEvent.CausesDOMCompositionEndEvent();
+ MOZ_ASSERT(mHandlingCompositions.LastElement().mCompositionId ==
+ aCompositionEvent.mCompositionId);
+
+ if (!WidgetHasComposition()) {
+ // mCompositionStart will be reset when commit event is completely handled
+ // in the remote process.
+ if (mHandlingCompositions.Length() == 1u) {
+ mPendingCommitLength = aCompositionEvent.mData.Length();
+ }
+ MOZ_ASSERT(HasPendingCommit());
+ } else if (aCompositionEvent.mMessage != eCompositionStart) {
+ mHandlingCompositions.LastElement().mCompositionString =
+ aCompositionEvent.mData;
+ }
+
+ // During REQUEST_TO_COMMIT_COMPOSITION or REQUEST_TO_CANCEL_COMPOSITION,
+ // widget usually sends a eCompositionChange and/or eCompositionCommit event
+ // to finalize or clear the composition, respectively. In this time,
+ // we need to intercept all composition events here and pass the commit
+ // string for returning to the remote process as a result of
+ // RequestIMEToCommitComposition(). Then, eCommitComposition event will
+ // be dispatched with the committed string in the remote process internally.
+ if (mCommitStringByRequest) {
+ if (aCompositionEvent.mMessage == eCompositionCommitAsIs) {
+ *mCommitStringByRequest =
+ mHandlingCompositions.LastElement().mCompositionString;
+ } else {
+ MOZ_ASSERT(aCompositionEvent.mMessage == eCompositionChange ||
+ aCompositionEvent.mMessage == eCompositionCommit);
+ *mCommitStringByRequest = aCompositionEvent.mData;
+ }
+ // We need to wait eCompositionCommitRequestHandled from the remote process
+ // in this case. Therefore, mPendingEventsNeedingAck needs to be
+ // incremented here.
+ if (!WidgetHasComposition()) {
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck++;
+ }
+ return false;
+ }
+
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck++;
+ return true;
+}
+
+void ContentCacheInParent::OnSelectionEvent(
+ const WidgetSelectionEvent& aSelectionEvent) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p OnSelectionEvent(aEvent={ "
+ "mMessage=%s, mOffset=%u, mLength=%u, mReversed=%s, "
+ "mExpandToClusterBoundary=%s, mUseNativeLineBreak=%s }), "
+ "PendingEventsNeedingAck()=%u, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s",
+ this, ToChar(aSelectionEvent.mMessage), aSelectionEvent.mOffset,
+ aSelectionEvent.mLength, GetBoolName(aSelectionEvent.mReversed),
+ GetBoolName(aSelectionEvent.mExpandToClusterBoundary),
+ GetBoolName(aSelectionEvent.mUseNativeLineBreak),
+ PendingEventsNeedingAck(), GetBoolName(WidgetHasComposition()),
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents)));
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ mDispatchedEventMessages.AppendElement(aSelectionEvent.mMessage);
+#endif // MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ mPendingSetSelectionEventNeedingAck++;
+}
+
+void ContentCacheInParent::OnEventNeedingAckHandled(nsIWidget* aWidget,
+ EventMessage aMessage,
+ uint32_t aCompositionId) {
+ // This is called when the child process receives WidgetCompositionEvent or
+ // WidgetSelectionEvent.
+
+ HandlingCompositionData* handlingCompositionData =
+ aMessage != eSetSelection ? GetHandlingCompositionData(aCompositionId)
+ : nullptr;
+
+ MOZ_LOG(sContentCacheLog, LogLevel::Info,
+ ("0x%p OnEventNeedingAckHandled(aWidget=0x%p, aMessage=%s, "
+ "aCompositionId=%" PRIu32
+ "), PendingEventsNeedingAck()=%u, WidgetHasComposition()=%s, "
+ "mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s, handlingCompositionData=0x%p",
+ this, aWidget, ToChar(aMessage), aCompositionId,
+ PendingEventsNeedingAck(), GetBoolName(WidgetHasComposition()),
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents),
+ handlingCompositionData));
+
+ // If we receive composition event messages for older one or invalid one,
+ // we should ignore them.
+ if (NS_WARN_IF(aMessage != eSetSelection && !handlingCompositionData)) {
+ return;
+ }
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ mReceivedEventMessages.AppendElement(aMessage);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ const bool isCommittedInChild =
+ // Commit requester in the remote process has committed the composition.
+ aMessage == eCompositionCommitRequestHandled ||
+ // The commit event has been handled normally in the remote process.
+ (!mIsChildIgnoringCompositionEvents &&
+ WidgetCompositionEvent::IsFollowedByCompositionEnd(aMessage));
+ const bool hasPendingCommit = HasPendingCommit();
+
+ if (isCommittedInChild) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ if (mHandlingCompositions.Length() == 1u) {
+ RemoveUnnecessaryEventMessageLog();
+ }
+
+ if (NS_WARN_IF(aMessage != eCompositionCommitRequestHandled &&
+ !handlingCompositionData->mSentCommitEvent)) {
+ nsPrintfCString info(
+ "\nReceived unexpected commit event message (%s) which we've "
+ "not sent yet\n\n",
+ ToChar(aMessage));
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false,
+ "Received unexpected commit event which has not been sent yet");
+ }
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+ // This should not occur, though. If we receive a commit notification for
+ // not the oldest composition, we should forget all older compositions.
+ size_t numberOfOutdatedCompositions = 1u;
+ for (auto& data : mHandlingCompositions) {
+ if (&data == handlingCompositionData) {
+ if (
+ // Don't put the info into the log when we've already sent commit
+ // event because it may be just inserting a character without
+ // composing state, but the remote process may move focus at
+ // eCompositionStart. This may happen with UI of IME to put only
+ // one character, e.g., the default Emoji picker of Windows.
+ !data.mSentCommitEvent &&
+ // In the normal case, only one message should remain, however,
+ // remaining 2 or more messages is also valid, for example, the
+ // remote process may have a composition update listener which
+ // takes a while. Then, we can have multiple pending messages.
+ data.mPendingEventsNeedingAck >= 1u) {
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Debug,
+ (" NOTE: BrowserParent has %" PRIu32
+ " pending composition messages for the handling composition, "
+ "but before they are handled in the remote process, the active "
+ "composition is commited by a request. "
+ "OnEventNeedingAckHandled() calls for them will be ignored",
+ data.mPendingEventsNeedingAck));
+ }
+ break;
+ }
+ if (MOZ_UNLIKELY(data.mPendingEventsNeedingAck)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Warning,
+ (" BrowserParent has %" PRIu32
+ " pending composition messages for an older composition than "
+ "the handling composition, but it'll be removed because newer "
+ "composition gets comitted in the remote process",
+ data.mPendingEventsNeedingAck));
+ }
+ numberOfOutdatedCompositions++;
+ }
+ mHandlingCompositions.RemoveElementsAt(0u, numberOfOutdatedCompositions);
+ handlingCompositionData = nullptr;
+
+ // Forget pending commit string length if it's handled in the remote
+ // process. Note that this doesn't care too old composition's commit
+ // string because in such case, we cannot return proper information
+ // to IME synchronously.
+ mPendingCommitLength = 0;
+ }
+
+ if (WidgetCompositionEvent::IsFollowedByCompositionEnd(aMessage)) {
+ // After the remote process receives eCompositionCommit(AsIs) event,
+ // it'll restart to handle composition events.
+ mIsChildIgnoringCompositionEvents = false;
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ if (NS_WARN_IF(!hasPendingCommit)) {
+ nsPrintfCString info(
+ "\nThere is no pending comment events but received "
+ "%s message from the remote child\n\n",
+ ToChar(aMessage));
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false,
+ "No pending commit events but received unexpected commit event");
+ }
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ } else if (aMessage == eCompositionCommitRequestHandled && hasPendingCommit) {
+ // If the remote process commits composition synchronously after
+ // requesting commit composition and we've already sent commit composition,
+ // it starts to ignore following composition events until receiving
+ // eCompositionStart event.
+ mIsChildIgnoringCompositionEvents = true;
+ }
+
+ // If neither widget (i.e., IME) nor the remote process has composition,
+ // now, we can forget composition string informations.
+ if (mHandlingCompositions.IsEmpty()) {
+ mCompositionStart.reset();
+ }
+
+ if (handlingCompositionData) {
+ if (NS_WARN_IF(!handlingCompositionData->mPendingEventsNeedingAck)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ nsPrintfCString info(
+ "\nThere is no pending events but received %s "
+ "message from the remote child\n\n",
+ ToChar(aMessage));
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "No pending event message but received unexpected event");
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ } else {
+ handlingCompositionData->mPendingEventsNeedingAck--;
+ }
+ } else if (aMessage == eSetSelection) {
+ if (NS_WARN_IF(!mPendingSetSelectionEventNeedingAck)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED && !defined(FUZZING_SNAPSHOT)
+ nsAutoCString info(
+ "\nThere is no pending set selection events but received from the "
+ "remote child\n\n");
+ AppendEventMessageLog(info);
+ CrashReporter::AppendAppNotesToCrashReport(info);
+ MOZ_DIAGNOSTIC_ASSERT(
+ false, "No pending event message but received unexpected event");
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ } else {
+ mPendingSetSelectionEventNeedingAck--;
+ }
+ }
+
+ if (!PendingEventsNeedingAck()) {
+ FlushPendingNotifications(aWidget);
+ }
+}
+
+bool ContentCacheInParent::RequestIMEToCommitComposition(
+ nsIWidget* aWidget, bool aCancel, uint32_t aCompositionId,
+ nsAString& aCommittedString) {
+ HandlingCompositionData* const handlingCompositionData =
+ GetHandlingCompositionData(aCompositionId);
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ ("0x%p RequestToCommitComposition(aWidget=%p, "
+ "aCancel=%s, aCompositionId=%" PRIu32
+ "), mHandlingCompositions.Length()=%zu, HasPendingCommit()=%s, "
+ "mIsChildIgnoringCompositionEvents=%s, "
+ "IMEStateManager::DoesBrowserParentHaveIMEFocus(&mBrowserParent)=%s, "
+ "WidgetHasComposition()=%s, mCommitStringByRequest=%p, "
+ "handlingCompositionData=0x%p",
+ this, aWidget, GetBoolName(aCancel), aCompositionId,
+ mHandlingCompositions.Length(), GetBoolName(HasPendingCommit()),
+ GetBoolName(mIsChildIgnoringCompositionEvents),
+ GetBoolName(
+ IMEStateManager::DoesBrowserParentHaveIMEFocus(&mBrowserParent)),
+ GetBoolName(WidgetHasComposition()), mCommitStringByRequest,
+ handlingCompositionData));
+
+ MOZ_ASSERT(!mCommitStringByRequest);
+
+ // If we don't know the composition ID, it must have already been committed
+ // in this process. In the case, we should do nothing here.
+ if (NS_WARN_IF(!handlingCompositionData)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eToUnknownCompositionReceived);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If we receive a commit result for not latest composition, this request is
+ // too late for IME. The remote process should wait following composition
+ // events for cleaning up TextComposition and handle the request as it's
+ // handled asynchronously.
+ if (handlingCompositionData != &mHandlingCompositions.LastElement()) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eToOldCompositionReceived);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If the composition has already been commit, th remote process will receive
+ // composition events and clean up TextComposition. So, this should do
+ // nothing and TextComposition should handle the request as it's handled
+ // asynchronously.
+ // XXX Perhaps, this is wrong because TextComposition in child process
+ // may commit the composition with current composition string in the
+ // remote process. I.e., it may be different from actual commit string
+ // which user typed. So, perhaps, we should return true and the commit
+ // string.
+ if (handlingCompositionData->mSentCommitEvent) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eToCommittedCompositionReceived);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If BrowserParent which has IME focus was already changed to different one,
+ // the request shouldn't be sent to IME because it's too late.
+ if (!IMEStateManager::DoesBrowserParentHaveIMEFocus(&mBrowserParent)) {
+ // Use the latest composition string which may not be handled in the
+ // remote process for avoiding data loss.
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eReceivedAfterBrowserParentBlur);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ aCommittedString = handlingCompositionData->mCompositionString;
+ // After we return true from here, i.e., without actually requesting IME
+ // to commit composition, we will receive eCompositionCommitRequestHandled
+ // pseudo event message from the remote process. So, we need to increment
+ // mPendingEventsNeedingAck here.
+ handlingCompositionData->mPendingEventsNeedingAck++;
+ return true;
+ }
+
+ RefPtr<TextComposition> composition =
+ IMEStateManager::GetTextCompositionFor(aWidget);
+ if (NS_WARN_IF(!composition)) {
+ MOZ_LOG(sContentCacheLog, LogLevel::Warning,
+ (" 0x%p RequestToCommitComposition(), "
+ "does nothing due to no composition",
+ this));
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eReceivedButNoTextComposition);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // If we receive a request for different composition, we must have already
+ // sent a commit event. So the remote process should handle it.
+ // XXX I think that this should never happen because we already checked
+ // whether handlingCompositionData is the latest composition or not.
+ // However, we don't want to commit different composition for the users.
+ // Therefore, let's handle the odd case here.
+ if (NS_WARN_IF(composition->Id() != aCompositionId)) {
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::
+ eReceivedButForDifferentTextComposition);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ mCommitStringByRequest = &aCommittedString;
+
+ // Request commit or cancel composition with TextComposition because we may
+ // have already requested to commit or cancel the composition or we may
+ // have already received eCompositionCommit(AsIs) event. Those status are
+ // managed by composition. So, if we don't request commit composition,
+ // we should do nothing with native IME here.
+ composition->RequestToCommit(aWidget, aCancel);
+
+ mCommitStringByRequest = nullptr;
+
+ MOZ_LOG(
+ sContentCacheLog, LogLevel::Info,
+ (" 0x%p RequestToCommitComposition(), "
+ "WidgetHasComposition()=%s, the composition %s committed synchronously",
+ this, GetBoolName(WidgetHasComposition()),
+ composition->Destroyed() ? "WAS" : "has NOT been"));
+
+ if (!composition->Destroyed()) {
+ // When the composition isn't committed synchronously, the remote process's
+ // TextComposition instance will synthesize commit events and wait to
+ // receive delayed composition events. When TextComposition instances both
+ // in this process and the remote process will be destroyed when delayed
+ // composition events received. TextComposition instance in the parent
+ // process will dispatch following composition events and be destroyed
+ // normally. On the other hand, TextComposition instance in the remote
+ // process won't dispatch following composition events and will be
+ // destroyed by IMEStateManager::DispatchCompositionEvent().
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eHandledAsynchronously);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return false;
+ }
+
+ // When the composition is committed synchronously, the commit string will be
+ // returned to the remote process. Then, PuppetWidget will dispatch
+ // eCompositionCommit event with the returned commit string (i.e., the value
+ // is aCommittedString of this method) and that causes destroying
+ // TextComposition instance in the remote process (Note that TextComposition
+ // instance in this process was already destroyed).
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ mRequestIMEToCommitCompositionResults.AppendElement(
+ RequestIMEToCommitCompositionResult::eHandledSynchronously);
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+ return true;
+}
+
+void ContentCacheInParent::MaybeNotifyIME(
+ nsIWidget* aWidget, const IMENotification& aNotification) {
+ if (!PendingEventsNeedingAck()) {
+ IMEStateManager::NotifyIME(aNotification, aWidget, &mBrowserParent);
+ return;
+ }
+
+ switch (aNotification.mMessage) {
+ case NOTIFY_IME_OF_SELECTION_CHANGE:
+ mPendingSelectionChange.MergeWith(aNotification);
+ break;
+ case NOTIFY_IME_OF_TEXT_CHANGE:
+ mPendingTextChange.MergeWith(aNotification);
+ break;
+ case NOTIFY_IME_OF_POSITION_CHANGE:
+ mPendingLayoutChange.MergeWith(aNotification);
+ break;
+ case NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED:
+ mPendingCompositionUpdate.MergeWith(aNotification);
+ break;
+ default:
+ MOZ_CRASH("Unsupported notification");
+ break;
+ }
+}
+
+void ContentCacheInParent::FlushPendingNotifications(nsIWidget* aWidget) {
+ MOZ_ASSERT(!PendingEventsNeedingAck());
+
+ // If the BrowserParent's widget has already gone, this can do nothing since
+ // widget is necessary to notify IME of something.
+ if (!aWidget) {
+ return;
+ }
+
+ // New notifications which are notified during flushing pending notifications
+ // should be merged again.
+ const bool pendingEventNeedingAckIncremented =
+ !mHandlingCompositions.IsEmpty();
+ if (pendingEventNeedingAckIncremented) {
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck++;
+ }
+
+ nsCOMPtr<nsIWidget> widget = aWidget;
+
+ // First, text change notification should be sent because selection change
+ // notification notifies IME of current selection range in the latest content.
+ // So, IME may need the latest content before that.
+ if (mPendingTextChange.HasNotification()) {
+ IMENotification notification(mPendingTextChange);
+ if (!widget->Destroyed()) {
+ mPendingTextChange.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ if (mPendingSelectionChange.HasNotification()) {
+ IMENotification notification(mPendingSelectionChange);
+ if (!widget->Destroyed()) {
+ mPendingSelectionChange.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ // Layout change notification should be notified after selection change
+ // notification because IME may want to query position of new caret position.
+ if (mPendingLayoutChange.HasNotification()) {
+ IMENotification notification(mPendingLayoutChange);
+ if (!widget->Destroyed()) {
+ mPendingLayoutChange.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ // Finally, send composition update notification because it notifies IME of
+ // finishing handling whole sending events.
+ if (mPendingCompositionUpdate.HasNotification()) {
+ IMENotification notification(mPendingCompositionUpdate);
+ if (!widget->Destroyed()) {
+ mPendingCompositionUpdate.Clear();
+ IMEStateManager::NotifyIME(notification, widget, &mBrowserParent);
+ }
+ }
+
+ // Decrement it which was incremented above.
+ if (!mHandlingCompositions.IsEmpty() && pendingEventNeedingAckIncremented &&
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck) {
+ mHandlingCompositions.LastElement().mPendingEventsNeedingAck--;
+ }
+
+ if (!PendingEventsNeedingAck() && !widget->Destroyed() &&
+ (mPendingTextChange.HasNotification() ||
+ mPendingSelectionChange.HasNotification() ||
+ mPendingLayoutChange.HasNotification() ||
+ mPendingCompositionUpdate.HasNotification())) {
+ FlushPendingNotifications(widget);
+ }
+}
+
+#if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+void ContentCacheInParent::RemoveUnnecessaryEventMessageLog() {
+ bool foundLastCompositionStart = false;
+ for (size_t i = mDispatchedEventMessages.Length(); i > 1; i--) {
+ if (mDispatchedEventMessages[i - 1] != eCompositionStart) {
+ continue;
+ }
+ if (!foundLastCompositionStart) {
+ // Find previous eCompositionStart of the latest eCompositionStart.
+ foundLastCompositionStart = true;
+ continue;
+ }
+ // Remove the messages before the last 2 sets of composition events.
+ mDispatchedEventMessages.RemoveElementsAt(0, i - 1);
+ break;
+ }
+ uint32_t numberOfCompositionCommitRequestHandled = 0;
+ foundLastCompositionStart = false;
+ for (size_t i = mReceivedEventMessages.Length(); i > 1; i--) {
+ if (mReceivedEventMessages[i - 1] == eCompositionCommitRequestHandled) {
+ numberOfCompositionCommitRequestHandled++;
+ }
+ if (mReceivedEventMessages[i - 1] != eCompositionStart) {
+ continue;
+ }
+ if (!foundLastCompositionStart) {
+ // Find previous eCompositionStart of the latest eCompositionStart.
+ foundLastCompositionStart = true;
+ continue;
+ }
+ // Remove the messages before the last 2 sets of composition events.
+ mReceivedEventMessages.RemoveElementsAt(0, i - 1);
+ break;
+ }
+
+ if (!numberOfCompositionCommitRequestHandled) {
+ // If there is no eCompositionCommitRequestHandled in
+ // mReceivedEventMessages, we don't need to store log of
+ // RequestIMEToCommmitComposition().
+ mRequestIMEToCommitCompositionResults.Clear();
+ } else {
+ // We need to keep all reason of eCompositionCommitRequestHandled, which
+ // is sent when mRequestIMEToCommitComposition() returns true.
+ // So, we can discard older log than the first
+ // eCompositionCommitRequestHandled in mReceivedEventMessages.
+ for (size_t i = mRequestIMEToCommitCompositionResults.Length(); i > 1;
+ i--) {
+ if (mRequestIMEToCommitCompositionResults[i - 1] ==
+ RequestIMEToCommitCompositionResult::
+ eReceivedAfterBrowserParentBlur ||
+ mRequestIMEToCommitCompositionResults[i - 1] ==
+ RequestIMEToCommitCompositionResult::eHandledSynchronously) {
+ --numberOfCompositionCommitRequestHandled;
+ if (!numberOfCompositionCommitRequestHandled) {
+ mRequestIMEToCommitCompositionResults.RemoveElementsAt(0, i - 1);
+ break;
+ }
+ }
+ }
+ }
+}
+
+void ContentCacheInParent::AppendEventMessageLog(nsACString& aLog) const {
+ aLog.AppendLiteral("Dispatched Event Message Log:\n");
+ for (EventMessage message : mDispatchedEventMessages) {
+ aLog.AppendLiteral(" ");
+ aLog.Append(ToChar(message));
+ aLog.AppendLiteral("\n");
+ }
+ aLog.AppendLiteral("\nReceived Event Message Log:\n");
+ for (EventMessage message : mReceivedEventMessages) {
+ aLog.AppendLiteral(" ");
+ aLog.Append(ToChar(message));
+ aLog.AppendLiteral("\n");
+ }
+ aLog.AppendLiteral("\nResult of RequestIMEToCommitComposition():\n");
+ for (RequestIMEToCommitCompositionResult result :
+ mRequestIMEToCommitCompositionResults) {
+ aLog.AppendLiteral(" ");
+ aLog.Append(ToReadableText(result));
+ aLog.AppendLiteral("\n");
+ }
+ aLog.AppendLiteral("\n");
+}
+
+#endif // #if MOZ_DIAGNOSTIC_ASSERT_ENABLED
+
+/*****************************************************************************
+ * mozilla::ContentCache::Selection
+ *****************************************************************************/
+
+ContentCache::Selection::Selection(
+ const WidgetQueryContentEvent& aQuerySelectedTextEvent)
+ : mAnchor(UINT32_MAX),
+ mFocus(UINT32_MAX),
+ mWritingMode(aQuerySelectedTextEvent.mReply->WritingModeRef()),
+ mHasRange(aQuerySelectedTextEvent.mReply->mOffsetAndData.isSome()) {
+ MOZ_ASSERT(aQuerySelectedTextEvent.mMessage == eQuerySelectedText);
+ MOZ_ASSERT(aQuerySelectedTextEvent.Succeeded());
+ if (mHasRange) {
+ mAnchor = aQuerySelectedTextEvent.mReply->AnchorOffset();
+ mFocus = aQuerySelectedTextEvent.mReply->FocusOffset();
+ }
+}
+
+/*****************************************************************************
+ * mozilla::ContentCache::TextRectArray
+ *****************************************************************************/
+
+LayoutDeviceIntRect ContentCache::TextRectArray::GetRect(
+ uint32_t aOffset) const {
+ LayoutDeviceIntRect rect;
+ if (IsOffsetInRange(aOffset)) {
+ rect = mRects[aOffset - mStart];
+ }
+ return rect;
+}
+
+LayoutDeviceIntRect ContentCache::TextRectArray::GetUnionRect(
+ uint32_t aOffset, uint32_t aLength) const {
+ LayoutDeviceIntRect rect;
+ if (!IsRangeCompletelyInRange(aOffset, aLength)) {
+ return rect;
+ }
+ for (uint32_t i = 0; i < aLength; i++) {
+ rect = rect.Union(mRects[aOffset - mStart + i]);
+ }
+ return rect;
+}
+
+LayoutDeviceIntRect ContentCache::TextRectArray::GetUnionRectAsFarAsPossible(
+ uint32_t aOffset, uint32_t aLength, bool aRoundToExistingOffset) const {
+ LayoutDeviceIntRect rect;
+ if (!HasRects() ||
+ (!aRoundToExistingOffset && !IsOverlappingWith(aOffset, aLength))) {
+ return rect;
+ }
+ uint32_t startOffset = std::max(aOffset, mStart);
+ if (aRoundToExistingOffset && startOffset >= EndOffset()) {
+ startOffset = EndOffset() - 1;
+ }
+ uint32_t endOffset = std::min(aOffset + aLength, EndOffset());
+ if (aRoundToExistingOffset && endOffset < mStart + 1) {
+ endOffset = mStart + 1;
+ }
+ if (NS_WARN_IF(endOffset < startOffset)) {
+ return rect;
+ }
+ for (uint32_t i = 0; i < endOffset - startOffset; i++) {
+ rect = rect.Union(mRects[startOffset - mStart + i]);
+ }
+ return rect;
+}
+
+} // namespace mozilla