Source code

Revision control

Copy as Markdown

Other Tools

/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* vim: set ts=8 sts=2 et sw=2 tw=80: */
/* 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 "ContentEventHandler.h"
#include "IMEContentObserver.h"
#include "mozilla/Assertions.h"
#include "mozilla/AsyncEventDispatcher.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/ErrorResult.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/Logging.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/PresShell.h"
#include "mozilla/StaticPrefs_test.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextControlElement.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/AncestorIterator.h"
#include "mozilla/dom/Document.h"
#include "mozilla/dom/Element.h"
#include "mozilla/dom/Selection.h"
#include "nsContentUtils.h"
#include "nsAtom.h"
#include "nsDocShell.h"
#include "nsGkAtoms.h"
#include "nsIContent.h"
#include "nsIFrame.h"
#include "nsINode.h"
#include "nsISelectionController.h"
#include "nsISupports.h"
#include "nsIWeakReferenceUtils.h"
#include "nsIWidget.h"
#include "nsPresContext.h"
#include "nsRange.h"
#include "nsRefreshDriver.h"
#include "WritingModes.h"
#include "nsString.h"
namespace mozilla {
using RawNodePosition = ContentEventHandler::RawNodePosition;
using namespace dom;
using namespace widget;
LazyLogModule sIMECOLog("IMEContentObserver");
LazyLogModule sCacheLog("IMEContentObserverCache");
static const char* ToChar(bool aBool) { return aBool ? "true" : "false"; }
/******************************************************************************
* mozilla::IMEContentObserver
******************************************************************************/
NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver)
// Note that we don't need to add mFirstAddedContainer nor
// mLastAddedContainer to cycle collection because they are non-null only
// during short time and shouldn't be touched while they are non-null.
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver)
nsAutoScriptBlocker scriptBlocker;
tmp->NotifyIMEOfBlur();
tmp->UnregisterObservers();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelection)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootElement)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditableNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditorBase)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocumentObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndOfAddedTextCache.mContainerNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndOfAddedTextCache.mContent)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartOfRemovingTextRangeCache.mContainerNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartOfRemovingTextRangeCache.mContent)
NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
tmp->mIMENotificationRequests = nullptr;
tmp->mESM = nullptr;
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IMEContentObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mWidget)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mFocusedWidget)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mSelection)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootElement)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditableNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditorBase)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocumentObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndOfAddedTextCache.mContainerNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndOfAddedTextCache.mContent)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
mStartOfRemovingTextRangeCache.mContainerNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mStartOfRemovingTextRangeCache.mContent)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver)
NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
NS_INTERFACE_MAP_ENTRY(nsIReflowObserver)
NS_INTERFACE_MAP_ENTRY(nsIScrollObserver)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsIReflowObserver)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver)
NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver)
IMEContentObserver::IMEContentObserver() {
#ifdef DEBUG
// TODO: Make this test as GTest.
mTextChangeData.Test();
#endif
}
void IMEContentObserver::Init(nsIWidget& aWidget, nsPresContext& aPresContext,
Element* aElement, EditorBase& aEditorBase) {
State state = GetState();
if (NS_WARN_IF(state == eState_Observing)) {
return; // Nothing to do.
}
bool firstInitialization = state != eState_StoppedObserving;
if (!firstInitialization) {
// If this is now trying to initialize with new contents, all observers
// should be registered again for simpler implementation.
UnregisterObservers();
Clear();
}
mESM = aPresContext.EventStateManager();
mESM->OnStartToObserveContent(this);
mWidget = &aWidget;
mIMENotificationRequests = &mWidget->IMENotificationRequestsRef();
if (!InitWithEditor(aPresContext, aElement, aEditorBase)) {
MOZ_LOG(sIMECOLog, LogLevel::Error,
("0x%p Init() FAILED, due to InitWithEditor() "
"failure",
this));
Clear();
return;
}
if (firstInitialization) {
// Now, try to send NOTIFY_IME_OF_FOCUS to IME via the widget.
MaybeNotifyIMEOfFocusSet();
// When this is called first time, IME has not received NOTIFY_IME_OF_FOCUS
// yet since NOTIFY_IME_OF_FOCUS will be sent to widget asynchronously.
// So, we need to do nothing here. After NOTIFY_IME_OF_FOCUS has been
// sent, OnIMEReceivedFocus() will be called and content, selection and/or
// position changes will be observed
return;
}
// When this is called after editor reframing (i.e., the root editable node
// is also recreated), IME has usually received NOTIFY_IME_OF_FOCUS. In this
// case, we need to restart to observe content, selection and/or position
// changes in new root editable node.
ObserveEditableNode();
if (!NeedsToNotifyIMEOfSomething()) {
return;
}
// Some change events may wait to notify IME because this was being
// initialized. It is the time to flush them.
FlushMergeableNotifications();
}
void IMEContentObserver::OnIMEReceivedFocus() {
// While Init() notifies IME of focus, pending layout may be flushed
// because the notification may cause querying content. Then, recursive
// call of Init() with the latest content may occur. In such case, we
// shouldn't keep first initialization which notified IME of focus.
if (GetState() != eState_Initializing) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p OnIMEReceivedFocus(), "
"but the state is not \"initializing\", so does nothing",
this));
return;
}
// NOTIFY_IME_OF_FOCUS might cause recreating IMEContentObserver
// instance via IMEStateManager::UpdateIMEState(). So, this
// instance might already have been destroyed, check it.
if (!mRootElement) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p OnIMEReceivedFocus(), "
"but mRootElement has already been cleared, so does nothing",
this));
return;
}
// Start to observe which is needed by IME when IME actually has focus.
ObserveEditableNode();
if (!NeedsToNotifyIMEOfSomething()) {
return;
}
// Some change events may wait to notify IME because this was being
// initialized. It is the time to flush them.
FlushMergeableNotifications();
}
bool IMEContentObserver::InitWithEditor(nsPresContext& aPresContext,
Element* aElement,
EditorBase& aEditorBase) {
// mEditableNode is one of
// - Anonymous <div> in <input> or <textarea>
// - Editing host if it's not in the design mode
// - Document if it's in the design mode
mEditableNode = IMEStateManager::GetRootEditableNode(aPresContext, aElement);
if (NS_WARN_IF(!mEditableNode)) {
return false;
}
mEditorBase = &aEditorBase;
RefPtr<PresShell> presShell = aPresContext.GetPresShell();
// get selection and root content
nsCOMPtr<nsISelectionController> selCon;
if (mEditableNode->IsContent()) {
nsIFrame* frame = mEditableNode->AsContent()->GetPrimaryFrame();
if (NS_WARN_IF(!frame)) {
return false;
}
frame->GetSelectionController(&aPresContext, getter_AddRefs(selCon));
} else {
// mEditableNode is a document
selCon = presShell;
}
if (NS_WARN_IF(!selCon)) {
return false;
}
mSelection = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL);
if (NS_WARN_IF(!mSelection)) {
return false;
}
if (mEditorBase->IsTextEditor()) {
mRootElement = mEditorBase->GetRoot(); // The anonymous <div>
MOZ_ASSERT(mRootElement);
MOZ_ASSERT(mRootElement->GetFirstChild());
if (auto* text = Text::FromNodeOrNull(
mRootElement ? mRootElement->GetFirstChild() : nullptr)) {
mTextControlValueLength = ContentEventHandler::GetNativeTextLength(*text);
}
mIsTextControl = true;
} else if (const nsRange* selRange = mSelection->GetRangeAt(0)) {
MOZ_ASSERT(!mIsTextControl);
if (NS_WARN_IF(!selRange->GetStartContainer())) {
return false;
}
// If an editing host has focus, mRootElement is it.
// Otherwise, if we're in the design mode, mRootElement is the <body> if
// there is and startContainer is not outside of the <body>. Otherwise, the
// document element is used instead.
nsCOMPtr<nsINode> startContainer = selRange->GetStartContainer();
mRootElement = Element::FromNodeOrNull(
startContainer->GetSelectionRootContent(presShell));
} else {
MOZ_ASSERT(!mIsTextControl);
// If an editing host has focus, mRootElement is it.
// Otherwise, if we're in the design mode, mRootElement is the <body> if
// there is. Otherwise, the document element is used instead.
nsCOMPtr<nsINode> editableNode = mEditableNode;
mRootElement = Element::FromNodeOrNull(
editableNode->GetSelectionRootContent(presShell));
}
if (!mRootElement && mEditableNode->IsDocument()) {
// The document node is editable, but there are no contents, this document
// is not editable.
return false;
}
if (NS_WARN_IF(!mRootElement)) {
return false;
}
mDocShell = aPresContext.GetDocShell();
if (NS_WARN_IF(!mDocShell)) {
return false;
}
mDocumentObserver = new DocumentObserver(*this);
return true;
}
void IMEContentObserver::Clear() {
mEditorBase = nullptr;
mSelection = nullptr;
mEditableNode = nullptr;
mRootElement = nullptr;
mDocShell = nullptr;
// Should be safe to clear mDocumentObserver here even though it grabs
// this instance in most cases because this is called by Init() or Destroy().
// The callers of Init() grab this instance with local RefPtr.
// The caller of Destroy() also grabs this instance with local RefPtr.
// So, this won't cause refcount of this instance become 0.
mDocumentObserver = nullptr;
}
void IMEContentObserver::ObserveEditableNode() {
MOZ_RELEASE_ASSERT(mSelection);
MOZ_RELEASE_ASSERT(mRootElement);
MOZ_RELEASE_ASSERT(GetState() != eState_Observing);
// If this is called before sending NOTIFY_IME_OF_FOCUS (it's possible when
// the editor is reframed before sending NOTIFY_IME_OF_FOCUS asynchronously),
// the notification requests of mWidget may be different from after the widget
// receives NOTIFY_IME_OF_FOCUS. So, this should be called again by
// OnIMEReceivedFocus() which is called after sending NOTIFY_IME_OF_FOCUS.
if (!mIMEHasFocus) {
MOZ_ASSERT(!mWidget || mNeedsToNotifyIMEOfFocusSet ||
mSendingNotification == NOTIFY_IME_OF_FOCUS,
"Wow, OnIMEReceivedFocus() won't be called?");
return;
}
mIsObserving = true;
if (mEditorBase) {
mEditorBase->SetIMEContentObserver(this);
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p ObserveEditableNode(), starting to observe 0x%p (%s)", this,
mRootElement.get(), ToString(*mRootElement).c_str()));
mRootElement->AddMutationObserver(this);
// If it's in a document (should be so), we can use document observer to
// reduce redundant computation of text change offsets.
Document* doc = mRootElement->GetComposedDoc();
if (doc) {
RefPtr<DocumentObserver> documentObserver = mDocumentObserver;
documentObserver->Observe(doc);
}
if (mDocShell) {
// Add scroll position listener and reflow observer to detect position
// and size changes
mDocShell->AddWeakScrollObserver(this);
mDocShell->AddWeakReflowObserver(this);
}
}
void IMEContentObserver::NotifyIMEOfBlur() {
// Prevent any notifications to be sent IME.
nsCOMPtr<nsIWidget> widget;
mWidget.swap(widget);
mIMENotificationRequests = nullptr;
// If we hasn't been set focus, we shouldn't send blur notification to IME.
if (!mIMEHasFocus) {
return;
}
// mWidget must have been non-nullptr if IME has focus.
MOZ_RELEASE_ASSERT(widget);
RefPtr<IMEContentObserver> kungFuDeathGrip(this);
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p NotifyIMEOfBlur(), sending NOTIFY_IME_OF_BLUR", this));
// For now, we need to send blur notification in any condition because
// we don't have any simple ways to send blur notification asynchronously.
// After this call, Destroy() or Unlink() will stop observing the content
// and forget everything. Therefore, if it's not safe to send notification
// when script blocker is unlocked, we cannot send blur notification after
// that and before next focus notification.
// Anyway, as far as we know, IME doesn't try to query content when it loses
// focus. So, this may not cause any problem.
mIMEHasFocus = false;
IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_BLUR), widget);
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p NotifyIMEOfBlur(), sent NOTIFY_IME_OF_BLUR", this));
}
void IMEContentObserver::UnregisterObservers() {
if (!mIsObserving) {
return;
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p UnregisterObservers(), stop observing 0x%p (%s)", this,
mRootElement.get(),
mRootElement ? ToString(*mRootElement).c_str() : "nullptr"));
mIsObserving = false;
if (mEditorBase) {
mEditorBase->SetIMEContentObserver(nullptr);
}
if (mSelection) {
mSelectionData.Clear();
mFocusedWidget = nullptr;
}
if (mRootElement) {
mRootElement->RemoveMutationObserver(this);
}
if (mDocumentObserver) {
RefPtr<DocumentObserver> documentObserver = mDocumentObserver;
documentObserver->StopObserving();
}
if (mDocShell) {
mDocShell->RemoveWeakScrollObserver(this);
mDocShell->RemoveWeakReflowObserver(this);
}
}
nsPresContext* IMEContentObserver::GetPresContext() const {
return mESM ? mESM->GetPresContext() : nullptr;
}
void IMEContentObserver::Destroy() {
// WARNING: When you change this method, you have to check Unlink() too.
// Note that don't send any notifications later from here. I.e., notify
// IMEStateManager of the blur synchronously because IMEStateManager needs to
// stop notifying the main process if this is requested by the main process.
NotifyIMEOfBlur();
UnregisterObservers();
Clear();
mWidget = nullptr;
mIMENotificationRequests = nullptr;
if (mESM) {
mESM->OnStopObservingContent(this);
mESM = nullptr;
}
}
bool IMEContentObserver::Destroyed() const { return !mWidget; }
void IMEContentObserver::DisconnectFromEventStateManager() { mESM = nullptr; }
bool IMEContentObserver::MaybeReinitialize(nsIWidget& aWidget,
nsPresContext& aPresContext,
Element* aElement,
EditorBase& aEditorBase) {
if (!IsObservingContent(aPresContext, aElement)) {
return false;
}
if (GetState() == eState_StoppedObserving) {
Init(aWidget, aPresContext, aElement, aEditorBase);
}
return IsObserving(aPresContext, aElement);
}
bool IMEContentObserver::IsObserving(const nsPresContext& aPresContext,
const Element* aElement) const {
if (GetState() != eState_Observing) {
return false;
}
// If aElement is not a text control, aElement is an editing host or entire
// the document is editable in the design mode. Therefore, return false if
// we're observing an anonymous subtree of a text control.
if (!aElement || !aElement->IsTextControlElement() ||
!static_cast<const TextControlElement*>(aElement)
->IsSingleLineTextControlOrTextArea()) {
if (mIsTextControl) {
return false;
}
}
// If aElement is a text control, return true if we're observing the anonymous
// subtree of aElement. Therefore, return false if we're observing with
// HTMLEditor.
else if (!mIsTextControl) {
return false;
}
return IsObservingContent(aPresContext, aElement);
}
bool IMEContentObserver::IsBeingInitializedFor(
const nsPresContext& aPresContext, const Element* aElement,
const EditorBase& aEditorBase) const {
return GetState() == eState_Initializing && mEditorBase == &aEditorBase &&
IsObservingContent(aPresContext, aElement);
}
bool IMEContentObserver::IsObserving(
const TextComposition& aTextComposition) const {
if (GetState() != eState_Observing) {
return false;
}
nsPresContext* const presContext = aTextComposition.GetPresContext();
if (NS_WARN_IF(!presContext)) {
return false;
}
if (presContext != GetPresContext()) {
return false; // observing different document
}
auto* const elementHavingComposition =
Element::FromNodeOrNull(aTextComposition.GetEventTargetNode());
bool isObserving = IsObservingContent(*presContext, elementHavingComposition);
#ifdef DEBUG
if (isObserving) {
if (mIsTextControl) {
MOZ_ASSERT(elementHavingComposition);
MOZ_ASSERT(elementHavingComposition->IsTextControlElement(),
"Should've never started to observe non-text-control element");
// XXX Our fake focus move has not been implemented properly. So, the
// following assertions may fail, but I don't like to make the failures
// cause crash even in debug builds because it may block developers to
// debug web-compat issues. On the other hand, it'd be nice if we can
// detect the bug with automated tests. Therefore, the following
// assertions are NS_ASSERTION.
NS_ASSERTION(static_cast<TextControlElement*>(elementHavingComposition)
->IsSingleLineTextControlOrTextArea(),
"Should've stopped observing when the type is changed");
NS_ASSERTION(!elementHavingComposition->IsInDesignMode(),
"Should've stopped observing when the design mode started");
} else if (elementHavingComposition) {
NS_ASSERTION(
!elementHavingComposition->IsTextControlElement() ||
!static_cast<TextControlElement*>(elementHavingComposition)
->IsSingleLineTextControlOrTextArea(),
"Should've never started to observe text-control element or "
"stopped observing it when the type is changed");
} else {
MOZ_ASSERT(presContext->GetPresShell());
MOZ_ASSERT(presContext->GetPresShell()->GetDocument());
NS_ASSERTION(
presContext->GetPresShell()->GetDocument()->IsInDesignMode(),
"Should be observing entire the document only in the design mode");
}
}
#endif // #ifdef DEBUG
return isObserving;
}
IMEContentObserver::State IMEContentObserver::GetState() const {
if (!mSelection || !mRootElement || !mEditableNode) {
return eState_NotObserving; // failed to initialize or finalized.
}
if (!mRootElement->IsInComposedDoc()) {
// the focused editor has already been reframed.
return eState_StoppedObserving;
}
return mIsObserving ? eState_Observing : eState_Initializing;
}
bool IMEContentObserver::IsObservingContent(const nsPresContext& aPresContext,
const Element* aElement) const {
return mEditableNode ==
IMEStateManager::GetRootEditableNode(aPresContext, aElement);
}
bool IMEContentObserver::IsEditorHandlingEventForComposition() const {
if (!mWidget) {
return false;
}
RefPtr<TextComposition> composition =
IMEStateManager::GetTextCompositionFor(mWidget);
if (!composition) {
return false;
}
return composition->EditorIsHandlingLatestChange();
}
bool IMEContentObserver::IsEditorComposing() const {
// Note that don't use TextComposition here. The important thing is,
// whether the editor already started to handle composition because
// web contents can change selection, text content and/or something from
// compositionstart event listener which is run before EditorBase handles it.
if (NS_WARN_IF(!mEditorBase)) {
return false;
}
return mEditorBase->IsIMEComposing();
}
nsresult IMEContentObserver::GetSelectionAndRoot(Selection** aSelection,
Element** aRootElement) const {
if (!mEditableNode || !mSelection) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_ASSERTION(mSelection && mRootElement, "uninitialized content observer");
NS_ADDREF(*aSelection = mSelection);
NS_ADDREF(*aRootElement = mRootElement);
return NS_OK;
}
void IMEContentObserver::OnSelectionChange(Selection& aSelection) {
if (!mIsObserving) {
return;
}
if (mWidget) {
bool causedByComposition = IsEditorHandlingEventForComposition();
bool causedBySelectionEvent = TextComposition::IsHandlingSelectionEvent();
bool duringComposition = IsEditorComposing();
MaybeNotifyIMEOfSelectionChange(causedByComposition, causedBySelectionEvent,
duringComposition);
}
}
void IMEContentObserver::ScrollPositionChanged() {
if (!NeedsPositionChangeNotification()) {
return;
}
MaybeNotifyIMEOfPositionChange();
}
NS_IMETHODIMP
IMEContentObserver::Reflow(DOMHighResTimeStamp aStart,
DOMHighResTimeStamp aEnd) {
if (!NeedsPositionChangeNotification()) {
return NS_OK;
}
MaybeNotifyIMEOfPositionChange();
return NS_OK;
}
NS_IMETHODIMP
IMEContentObserver::ReflowInterruptible(DOMHighResTimeStamp aStart,
DOMHighResTimeStamp aEnd) {
if (!NeedsPositionChangeNotification()) {
return NS_OK;
}
MaybeNotifyIMEOfPositionChange();
return NS_OK;
}
nsresult IMEContentObserver::HandleQueryContentEvent(
WidgetQueryContentEvent* aEvent) {
// If the instance has normal selection cache and the query event queries
// normal selection's range, it should use the cached selection which was
// sent to the widget. However, if this instance has already received new
// selection change notification but hasn't updated the cache yet (i.e.,
// not sending selection change notification to IME, don't use the cached
// value. Note that don't update selection cache here since if you update
// selection cache here, IMENotificationSender won't notify IME of selection
// change because it looks like that the selection isn't actually changed.
const bool isSelectionCacheAvailable = aEvent->mUseNativeLineBreak &&
mSelectionData.IsInitialized() &&
!mNeedsToNotifyIMEOfSelectionChange;
if (isSelectionCacheAvailable && aEvent->mMessage == eQuerySelectedText &&
aEvent->mInput.mSelectionType == SelectionType::eNormal) {
aEvent->EmplaceReply();
if (mSelectionData.HasRange()) {
aEvent->mReply->mOffsetAndData.emplace(mSelectionData.mOffset,
mSelectionData.String(),
OffsetAndDataFor::SelectedString);
aEvent->mReply->mReversed = mSelectionData.mReversed;
}
aEvent->mReply->mContentsRoot = mRootElement;
aEvent->mReply->mWritingMode = mSelectionData.GetWritingMode();
// The selection cache in IMEContentObserver must always have been in
// an editing host (or an editable anonymous <div> element). Therefore,
// we set mIsEditableContent to true here even though it's already been
// blurred or changed its editable state but the selection cache has not
// been invalidated yet.
aEvent->mReply->mIsEditableContent = true;
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p HandleQueryContentEvent(aEvent={ "
"mMessage=%s, mReply=%s })",
this, ToChar(aEvent->mMessage), ToString(aEvent->mReply).c_str()));
return NS_OK;
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p HandleQueryContentEvent(aEvent={ mMessage=%s })", this,
ToChar(aEvent->mMessage)));
// If we can make the event's input offset absolute with TextComposition or
// mSelection, we should set it here for reducing the cost of computing
// selection start offset. If ContentEventHandler receives a
// WidgetQueryContentEvent whose input offset is relative to insertion point,
// it computes current selection start offset (this may be expensive) and
// make the offset absolute value itself.
// Note that calling MakeOffsetAbsolute() makes the event a query event with
// absolute offset. So, ContentEventHandler doesn't pay any additional cost
// after calling MakeOffsetAbsolute() here.
if (aEvent->mInput.mRelativeToInsertionPoint &&
aEvent->mInput.IsValidEventMessage(aEvent->mMessage)) {
RefPtr<TextComposition> composition =
IMEStateManager::GetTextCompositionFor(aEvent->mWidget);
if (composition) {
uint32_t compositionStart = composition->NativeOffsetOfStartComposition();
if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(compositionStart))) {
return NS_ERROR_FAILURE;
}
} else if (isSelectionCacheAvailable && mSelectionData.HasRange()) {
const uint32_t selectionStart = mSelectionData.mOffset;
if (NS_WARN_IF(!aEvent->mInput.MakeOffsetAbsolute(selectionStart))) {
return NS_ERROR_FAILURE;
}
}
}
AutoRestore<bool> handling(mIsHandlingQueryContentEvent);
mIsHandlingQueryContentEvent = true;
ContentEventHandler handler(GetPresContext());
nsresult rv = handler.HandleQueryContentEvent(aEvent);
if (NS_WARN_IF(Destroyed())) {
// If this has already destroyed during querying the content, the query
// is outdated even if it's succeeded. So, make the query fail.
aEvent->mReply.reset();
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p HandleQueryContentEvent(), WARNING, "
"IMEContentObserver has been destroyed during the query, "
"making the query fail",
this));
return rv;
}
if (aEvent->Succeeded() &&
NS_WARN_IF(aEvent->mReply->mContentsRoot != mRootElement)) {
// Focus has changed unexpectedly, so make the query fail.
aEvent->mReply.reset();
}
return rv;
}
nsresult IMEContentObserver::MaybeHandleSelectionEvent(
nsPresContext* aPresContext, WidgetSelectionEvent* aEvent) {
MOZ_ASSERT(aEvent);
MOZ_ASSERT(aEvent->mMessage == eSetSelection);
NS_ASSERTION(!mNeedsToNotifyIMEOfSelectionChange,
"Selection cache has not been updated yet");
MOZ_LOG(
sIMECOLog, LogLevel::Debug,
("0x%p MaybeHandleSelectionEvent(aEvent={ "
"mMessage=%s, mOffset=%u, mLength=%u, mReversed=%s, "
"mExpandToClusterBoundary=%s, mUseNativeLineBreak=%s }), "
"mSelectionData=%s",
this, ToChar(aEvent->mMessage), aEvent->mOffset, aEvent->mLength,
ToChar(aEvent->mReversed), ToChar(aEvent->mExpandToClusterBoundary),
ToChar(aEvent->mUseNativeLineBreak), ToString(mSelectionData).c_str()));
// When we have Selection cache, and the caller wants to set same selection
// range, we shouldn't try to compute same range because it may be impossible
// if the range boundary is around element boundaries which won't be
// serialized with line breaks like close tags of inline elements. In that
// case, inserting new text at different point may be different from intention
// of users or web apps which set current selection.
// FIXME: We cache only selection data computed with native line breaker
// lengths. Perhaps, we should improve the struct to have both data of
// offset and length. E.g., adding line break counts for both offset and
// length.
if (!mNeedsToNotifyIMEOfSelectionChange && aEvent->mUseNativeLineBreak &&
mSelectionData.IsInitialized() && mSelectionData.HasRange() &&
mSelectionData.StartOffset() == aEvent->mOffset &&
mSelectionData.Length() == aEvent->mLength) {
if (RefPtr<Selection> selection = mSelection) {
selection->ScrollIntoView(nsISelectionController::SELECTION_FOCUS_REGION);
}
aEvent->mSucceeded = true;
return NS_OK;
}
ContentEventHandler handler(aPresContext);
return handler.OnSelectionEvent(aEvent);
}
bool IMEContentObserver::OnMouseButtonEvent(nsPresContext& aPresContext,
WidgetMouseEvent& aMouseEvent) {
if (!mIMENotificationRequests ||
!mIMENotificationRequests->WantMouseButtonEventOnChar()) {
return false;
}
if (!aMouseEvent.IsTrusted() || aMouseEvent.DefaultPrevented() ||
!aMouseEvent.mWidget) {
return false;
}
// Now, we need to notify only mouse down and mouse up event.
switch (aMouseEvent.mMessage) {
case eMouseUp:
case eMouseDown:
break;
default:
return false;
}
if (NS_WARN_IF(!mWidget) || NS_WARN_IF(mWidget->Destroyed())) {
return false;
}
WidgetQueryContentEvent queryCharAtPointEvent(true, eQueryCharacterAtPoint,
aMouseEvent.mWidget);
queryCharAtPointEvent.mRefPoint = aMouseEvent.mRefPoint;
ContentEventHandler handler(&aPresContext);
handler.OnQueryCharacterAtPoint(&queryCharAtPointEvent);
if (NS_WARN_IF(queryCharAtPointEvent.Failed()) ||
queryCharAtPointEvent.DidNotFindChar()) {
return false;
}
// The widget might be destroyed during querying the content since it
// causes flushing layout.
if (!mWidget || NS_WARN_IF(mWidget->Destroyed())) {
return false;
}
// The result character rect is relative to the top level widget.
// We should notify it with offset in the widget.
nsIWidget* topLevelWidget = mWidget->GetTopLevelWidget();
if (topLevelWidget && topLevelWidget != mWidget) {
queryCharAtPointEvent.mReply->mRect.MoveBy(
topLevelWidget->WidgetToScreenOffset() -
mWidget->WidgetToScreenOffset());
}
// The refPt is relative to its widget.
// We should notify it with offset in the widget.
if (aMouseEvent.mWidget != mWidget) {
queryCharAtPointEvent.mRefPoint +=
aMouseEvent.mWidget->WidgetToScreenOffset() -
mWidget->WidgetToScreenOffset();
}
IMENotification notification(NOTIFY_IME_OF_MOUSE_BUTTON_EVENT);
notification.mMouseButtonEventData.mEventMessage = aMouseEvent.mMessage;
notification.mMouseButtonEventData.mOffset =
queryCharAtPointEvent.mReply->StartOffset();
notification.mMouseButtonEventData.mCursorPos =
queryCharAtPointEvent.mRefPoint;
notification.mMouseButtonEventData.mCharRect =
queryCharAtPointEvent.mReply->mRect;
notification.mMouseButtonEventData.mButton = aMouseEvent.mButton;
notification.mMouseButtonEventData.mButtons = aMouseEvent.mButtons;
notification.mMouseButtonEventData.mModifiers = aMouseEvent.mModifiers;
nsresult rv = IMEStateManager::NotifyIME(notification, mWidget);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
bool consumed = (rv == NS_SUCCESS_EVENT_CONSUMED);
if (consumed) {
aMouseEvent.PreventDefault();
}
return consumed;
}
void IMEContentObserver::CharacterDataWillChange(
nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
if (!aContent->IsText()) {
return; // Ignore if it's a comment node or something other invisible data
// node.
}
MOZ_ASSERT(mPreCharacterDataChangeLength < 0,
"CharacterDataChanged() should've reset "
"mPreCharacterDataChangeLength");
if (!NeedsTextChangeNotification() ||
!nsContentUtils::IsInSameAnonymousTree(mRootElement, aContent)) {
return;
}
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
// Although we don't assume this change occurs while this is storing
// the range of added consecutive nodes, if it actually happens, we need to
// flush them since this change may occur before or in the range. So, it's
// safe to flush pending computation of mTextChangeData before handling this.
if (mAddedContentCache.HasCache()) {
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
}
mPreCharacterDataChangeLength = ContentEventHandler::GetNativeTextLength(
*aContent->AsText(), aInfo.mChangeStart, aInfo.mChangeEnd);
MOZ_ASSERT(
mPreCharacterDataChangeLength >= aInfo.mChangeEnd - aInfo.mChangeStart,
"The computed length must be same as or larger than XP length");
}
void IMEContentObserver::CharacterDataChanged(
nsIContent* aContent, const CharacterDataChangeInfo& aInfo) {
if (!aContent->IsText()) {
return; // Ignore if it's a comment node or something other invisible data
// node.
}
// Let TextComposition have a change to update composition string range in
// the text node if the change is caused by the web apps.
if (mWidget && !IsEditorHandlingEventForComposition()) {
if (RefPtr<TextComposition> composition =
IMEStateManager::GetTextCompositionFor(mWidget)) {
composition->OnCharacterDataChanged(*aContent->AsText(), aInfo);
}
}
if (!NeedsTextChangeNotification() ||
!nsContentUtils::IsInSameAnonymousTree(mRootElement, aContent)) {
return;
}
if (mAddedContentCache.HasCache()) {
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
}
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
MOZ_ASSERT(
!mAddedContentCache.HasCache(),
"The stored range should be flushed before actually the data is changed");
int64_t removedLength = mPreCharacterDataChangeLength;
mPreCharacterDataChangeLength = -1;
MOZ_ASSERT(removedLength >= 0,
"mPreCharacterDataChangeLength should've been set by "
"CharacterDataWillChange()");
uint32_t offset = 0;
if (mIsTextControl) {
// If we're observing a text control, mRootElement is the anonymous <div>
// element which has only one text node and/or invisible <br> element.
// TextEditor assumes this structure when it handles editing commands.
// Therefore, it's safe to assume same things here.
MOZ_ASSERT(mRootElement->GetFirstChild() == aContent);
if (aInfo.mChangeStart) {
offset = ContentEventHandler::GetNativeTextLength(*aContent->AsText(), 0,
aInfo.mChangeStart);
}
} else {
nsresult rv = ContentEventHandler::GetFlatTextLengthInRange(
RawNodePosition::BeforeFirstContentOf(*mRootElement),
RawNodePosition(aContent, aInfo.mChangeStart), mRootElement, &offset,
LINE_BREAK_TYPE_NATIVE);
if (NS_WARN_IF(NS_FAILED(rv))) {
return;
}
}
uint32_t newLength = ContentEventHandler::GetNativeTextLength(
*aContent->AsText(), aInfo.mChangeStart,
aInfo.mChangeStart + aInfo.mReplaceLength);
uint32_t oldEnd = offset + static_cast<uint32_t>(removedLength);
uint32_t newEnd = offset + newLength;
TextChangeData data(offset, oldEnd, newEnd,
IsEditorHandlingEventForComposition(),
IsEditorComposing());
MaybeNotifyIMEOfTextChange(data);
}
void IMEContentObserver::ContentAdded(nsINode* aContainer,
nsIContent* aFirstContent,
nsIContent* aLastContent) {
if (!NeedsTextChangeNotification() ||
!nsContentUtils::IsInSameAnonymousTree(mRootElement, aFirstContent)) {
return;
}
// We can skip everything when a padding <br> element is added since its text
// length is 0.
if (aFirstContent == aLastContent) {
if (const auto* brElement = HTMLBRElement::FromNode(aFirstContent)) {
if (MOZ_LIKELY(!brElement->HasChildNodes()) &&
(brElement->IsPaddingForEmptyEditor() ||
brElement->IsPaddingForEmptyLastLine())) {
return;
}
}
}
MOZ_ASSERT(IsInDocumentChange());
MOZ_ASSERT_IF(aFirstContent, aFirstContent->GetParentNode() == aContainer);
MOZ_ASSERT_IF(aLastContent, aLastContent->GetParentNode() == aContainer);
// While a document change, new nodes should be added consecutively in a
// container node. Therefore, we can cache the first added node and the last
// added node until ending the document change at least. Then, we can avoid
// to compute first added node offset in the flattened text repeatedly.
bool needToCache = true;
if (mAddedContentCache.HasCache()) {
MOZ_DIAGNOSTIC_ASSERT(aFirstContent->GetParentNode() ==
aLastContent->GetParentNode());
if (mAddedContentCache.IsInRange(*aFirstContent, mRootElement)) {
// The new content nodes are in the range, we can include their text
// length when we flush the cached range later. Therefore, we need to
// do nothing in this case.
needToCache = false;
MOZ_LOG(sCacheLog, LogLevel::Info,
("ContentAdded: mAddedContentCache already caches the give "
"content nodes"));
MOZ_ASSERT(mAddedContentCache.IsInRange(*aLastContent, mRootElement));
}
// When new nodes are inserted in a different container, let's flush the
// preceding content first. Then, we should restart to cache the new
// inserted nodes.
else if (!mAddedContentCache.CanMergeWith(*aFirstContent, *aLastContent,
mRootElement)) {
MOZ_LOG(sCacheLog, LogLevel::Info,
("ContentAdded: mAddedContentCache was cached not in current "
"document change and new content nodes cannot be merged"));
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
OffsetAndLengthAdjustments differences;
Result<std::pair<uint32_t, uint32_t>, nsresult> offsetAndLength =
mAddedContentCache.ComputeFlatTextRangeBeforeInsertingNewContent(
*aFirstContent, *aLastContent, mRootElement, differences);
if (NS_WARN_IF(offsetAndLength.isErr())) {
MOZ_LOG(sCacheLog, LogLevel::Error,
("ContentAdded: "
"AddedContentCache::"
"ComputeFlatTextRangeExcludingInsertingNewContent() failed"));
mAddedContentCache.Clear(__FUNCTION__);
return;
}
NotifyIMEOfCachedConsecutiveNewNodes(
__FUNCTION__, Some(offsetAndLength.inspect().first),
Some(offsetAndLength.inspect().second), differences);
mAddedContentCache.Clear(__FUNCTION__);
}
}
mEndOfAddedTextCache.ContentAdded(__FUNCTION__, *aFirstContent, *aLastContent,
Nothing(), mRootElement);
mStartOfRemovingTextRangeCache.ContentAdded(
__FUNCTION__, *aFirstContent, *aLastContent, Nothing(), mRootElement);
if (!needToCache) {
return;
}
// Okay, now, we can start to cache new nodes or merge the range of new
// nodes with the cached range.
if (!mAddedContentCache.TryToCache(*aFirstContent, *aLastContent,
mRootElement)) {
// Flush the old range first.
MOZ_LOG(sCacheLog, LogLevel::Info,
("ContentAdded: called during a document change flushed "
"previous added nodes (aFirstContent=%s, aLastContent=%s)",
ToString(RefPtr<nsINode>(aFirstContent)).c_str(),
ToString(RefPtr<nsINode>(aLastContent)).c_str()));
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
MOZ_ASSERT(!mAddedContentCache.HasCache());
MOZ_ALWAYS_TRUE(mAddedContentCache.TryToCache(*aFirstContent, *aLastContent,
mRootElement));
}
}
void IMEContentObserver::NotifyIMEOfCachedConsecutiveNewNodes(
const char* aCallerName,
const Maybe<uint32_t>& aOffsetOfFirstContent /* = Nothing() */,
const Maybe<uint32_t>& aLengthOfContentNNodes /* = Nothing() */,
const OffsetAndLengthAdjustments& aAdjustments /* = Nothing() */) {
MOZ_ASSERT(mAddedContentCache.HasCache());
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p "
"IMEContentObserver::NotifyIMEOfCachedConsecutiveNewNodes(), "
"flushing stored consecutive nodes",
this));
MOZ_LOG(sCacheLog, LogLevel::Info,
("NotifyIMEOfCachedConsecutiveNewNodes: called by %s "
"(mAddedContentCache=%s)",
aCallerName, ToString(mAddedContentCache).c_str()));
// If 2 <div> elements are inserted into the DOM, we wan't the text length
// from start of the first <div> (including line break caused by its open
// tag) to end of the second <div>. I.e., we want to compute:
// ...{<div>.....</div><div>......</div>}...
// ^ ^ ^ ^
// | mFirst | |
// | mLast |
// offset (offset + length)
Maybe<uint32_t> offset =
aOffsetOfFirstContent.isSome()
? aOffsetOfFirstContent
: mEndOfAddedTextCache.GetFlatTextLengthBeforeContent(
*mAddedContentCache.mFirst, mRootElement);
if (offset.isNothing()) {
Result<uint32_t, nsresult> textLengthBeforeFirstContentOrError =
FlatTextCache::ComputeTextLengthBeforeContent(
*mAddedContentCache.mFirst, mRootElement);
if (NS_WARN_IF(textLengthBeforeFirstContentOrError.isErr())) {
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
MOZ_LOG(
sCacheLog, LogLevel::Error,
("NotifyContentAdded: failed to compute text length before mFirst"));
mAddedContentCache.Clear(__FUNCTION__);
return;
}
offset = Some(textLengthBeforeFirstContentOrError.unwrap());
}
Maybe<uint32_t> length = aLengthOfContentNNodes;
if (aLengthOfContentNNodes.isNothing()) {
Result<uint32_t, nsresult> addingLengthOrError =
FlatTextCache::ComputeTextLengthStartOfContentToEndOfContent(
*mAddedContentCache.mFirst, *mAddedContentCache.mLast,
mRootElement);
if (NS_WARN_IF(addingLengthOrError.isErr())) {
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
MOZ_LOG(sCacheLog, LogLevel::Error,
("NotifyContentAdded: failed to compute text length of added"));
mAddedContentCache.Clear(__FUNCTION__);
return;
}
length = Some(addingLengthOrError.inspect());
}
// If multiple lines are being inserted in an HTML editor, next call of
// NotifyContentAdded() is for adding next node. Therefore, caching the text
// length can skip to compute the text length before the adding node and
// before of it.
mEndOfAddedTextCache.CacheFlatTextLengthBeforeEndOfContent(
__FUNCTION__, *mAddedContentCache.mLast,
aAdjustments.AdjustedEndOffset(*offset + *length), mRootElement);
mStartOfRemovingTextRangeCache.ContentAdded(
__FUNCTION__, *mAddedContentCache.mFirst, *mAddedContentCache.mLast,
Some(aAdjustments.AdjustedEndOffset(*offset + *length)), mRootElement);
mAddedContentCache.Clear(__FUNCTION__);
if (*length == 0u) {
return;
}
TextChangeData data(*offset, *offset, *offset + *length,
IsEditorHandlingEventForComposition(),
IsEditorComposing());
MaybeNotifyIMEOfTextChange(data);
}
void IMEContentObserver::ContentAppended(nsIContent* aFirstNewContent) {
nsIContent* parent = aFirstNewContent->GetParent();
MOZ_ASSERT(parent);
ContentAdded(parent, aFirstNewContent, parent->GetLastChild());
}
void IMEContentObserver::ContentInserted(nsIContent* aChild) {
MOZ_ASSERT(aChild);
ContentAdded(aChild->GetParentNode(), aChild, aChild);
}
void IMEContentObserver::ContentWillBeRemoved(nsIContent* aChild) {
if (!NeedsTextChangeNotification() ||
!nsContentUtils::IsInSameAnonymousTree(mRootElement, aChild)) {
return;
}
// We can skip everything when padding <br> element is removed since its text
// length is 0.
if (const auto* brElement = HTMLBRElement::FromNode(aChild)) {
if (MOZ_LIKELY(!brElement->HasChildNodes()) &&
(brElement->IsPaddingForEmptyEditor() ||
brElement->IsPaddingForEmptyLastLine())) {
return;
}
}
const Result<uint32_t, nsresult> textLengthOrError =
FlatTextCache::ComputeTextLengthOfContent(*aChild, mRootElement,
ForRemoval::Yes);
if (NS_WARN_IF(textLengthOrError.isErr())) {
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
mAddedContentCache.Clear(__FUNCTION__);
return;
}
if (mAddedContentCache.HasCache()) {
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
MOZ_DIAGNOSTIC_ASSERT(!mAddedContentCache.HasCache());
}
nsINode* containerNode = aChild->GetParentNode();
MOZ_ASSERT(containerNode);
mEndOfAddedTextCache.ContentWillBeRemoved(
*aChild, textLengthOrError.inspect(), mRootElement);
Maybe<uint32_t> offset =
mStartOfRemovingTextRangeCache.GetFlatTextLengthBeforeContent(
*aChild, mRootElement, ForRemoval::Yes);
nsIContent* const prevSibling = aChild->GetPreviousSibling();
if (offset.isSome()) {
// Update the cache because next remove may be the previous or the next
// sibling removal. So, caching offset of currently removing content node
// makes us skip computing offset of next removal.
if (prevSibling) {
mStartOfRemovingTextRangeCache.CacheFlatTextLengthBeforeEndOfContent(
__FUNCTION__, *prevSibling, *offset, mRootElement);
} else {
mStartOfRemovingTextRangeCache.CacheFlatTextLengthBeforeFirstContent(
__FUNCTION__, *containerNode, *offset, mRootElement);
}
} else {
if (prevSibling) {
// When we compute preceding text length of the removing content node, we
// cannot make the range cross the removing node boundary because
// containerNode->ComputeIndexOf(aChild) returns Nothing so that
// ContentEventHandler fails to compute the length. Therefore, if a <div>
// is being removed, we want to compute the length of `...}<div>`.
if (NS_WARN_IF(
NS_FAILED(mStartOfRemovingTextRangeCache
.ComputeAndCacheFlatTextLengthBeforeEndOfContent(
__FUNCTION__, *prevSibling, mRootElement)))) {
return;
}
} else {
// At removing a child node of containerNode, we need the line break
// caused by open tag of containerNode.
if (NS_WARN_IF(
NS_FAILED(mStartOfRemovingTextRangeCache
.ComputeAndCacheFlatTextLengthBeforeFirstContent(
__FUNCTION__, *containerNode, mRootElement)))) {
return;
}
}
offset = Some(mStartOfRemovingTextRangeCache.GetFlatTextLength());
}
// We do not need a text change notification since removing aChild does not
// change flattened text and no pending added length.
if (textLengthOrError.inspect() == 0u) {
return;
}
TextChangeData data(*offset, *offset + textLengthOrError.inspect(), *offset,
IsEditorHandlingEventForComposition(),
IsEditorComposing());
MaybeNotifyIMEOfTextChange(data);
}
MOZ_CAN_RUN_SCRIPT_BOUNDARY void IMEContentObserver::ParentChainChanged(
nsIContent* aContent) {
// When the observing element itself is directly removed from the document
// without a focus move, i.e., it's the root of the removed document fragment
// and the editor was handling the design mode, we have already stopped
// observing the element because IMEStateManager::OnRemoveContent() should
// have already been called for it and the instance which was observing the
// node has already been destroyed. Therefore, this is called only when
// this is observing the <body> in the design mode and it's disconnected from
// the tree by an <html> element removal. Even in this case, IMEStateManager
// never gets a focus change notification, but we need to notify IME of focus
// change because we cannot interact with IME anymore due to no editable
// content. Therefore, this method notifies IMEStateManager of the
// disconnection of the observing node to emulate a blur from the editable
// content.
MOZ_ASSERT(mIsObserving);
OwningNonNull<IMEContentObserver> observer(*this);
IMEStateManager::OnParentChainChangedOfObservingElement(observer);
}
void IMEContentObserver::OnTextControlValueChangedWhileNotObservable(
const nsAString& aNewValue) {
MOZ_ASSERT(mEditorBase);
MOZ_ASSERT(mEditorBase->IsTextEditor());
if (!mTextControlValueLength && aNewValue.IsEmpty()) {
return;
}
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p OnTextControlValueChangedWhileNotObservable()", this));
uint32_t newLength = ContentEventHandler::GetNativeTextLength(aNewValue);
TextChangeData data(0, mTextControlValueLength, newLength, false, false);
MaybeNotifyIMEOfTextChange(data);
}
void IMEContentObserver::BeginDocumentUpdate() {
MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p BeginDocumentUpdate()", this));
}
void IMEContentObserver::EndDocumentUpdate() {
MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p EndDocumentUpdate()", this));
if (mAddedContentCache.HasCache() && !EditorIsHandlingEditSubAction()) {
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
}
}
void IMEContentObserver::SuppressNotifyingIME() {
mSuppressNotifications++;
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p SuppressNotifyingIME(), mSuppressNotifications=%u", this,
mSuppressNotifications));
}
void IMEContentObserver::UnsuppressNotifyingIME() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p UnsuppressNotifyingIME(), mSuppressNotifications=%u", this,
mSuppressNotifications));
if (!mSuppressNotifications || --mSuppressNotifications) {
return;
}
FlushMergeableNotifications();
}
void IMEContentObserver::OnEditActionHandled() {
MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p OnEditActionHandled()", this));
if (mAddedContentCache.HasCache()) {
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
}
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
FlushMergeableNotifications();
}
void IMEContentObserver::BeforeEditAction() {
MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p BeforeEditAction()", this));
if (mAddedContentCache.HasCache()) {
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
}
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
}
void IMEContentObserver::CancelEditAction() {
MOZ_LOG(sIMECOLog, LogLevel::Debug, ("0x%p CancelEditAction()", this));
if (mAddedContentCache.HasCache()) {
NotifyIMEOfCachedConsecutiveNewNodes(__FUNCTION__);
}
mEndOfAddedTextCache.Clear(__FUNCTION__);
mStartOfRemovingTextRangeCache.Clear(__FUNCTION__);
FlushMergeableNotifications();
}
bool IMEContentObserver::EditorIsHandlingEditSubAction() const {
return mEditorBase && mEditorBase->IsInEditSubAction();
}
void IMEContentObserver::PostFocusSetNotification() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p PostFocusSetNotification()", this));
mNeedsToNotifyIMEOfFocusSet = true;
}
void IMEContentObserver::PostTextChangeNotification() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p PostTextChangeNotification(mTextChangeData=%s)", this,
ToString(mTextChangeData).c_str()));
MOZ_ASSERT(mTextChangeData.IsValid(),
"mTextChangeData must have text change data");
mNeedsToNotifyIMEOfTextChange = true;
// Even if the observer hasn't received selection change, selection in the
// flat text may have already been changed. For example, when previous `<p>`
// element of another `<p>` element which contains caret is removed by a DOM
// mutation, selection change event won't be fired, but selection start offset
// should be decreased by the length of removed `<p>` element.
// In such case, HandleQueryContentEvent shouldn't use the selection cache
// anymore. Therefore, we also need to post selection change notification
// too. eQuerySelectedText event may be dispatched at sending a text change
// notification.
mNeedsToNotifyIMEOfSelectionChange = true;
}
void IMEContentObserver::PostSelectionChangeNotification() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p PostSelectionChangeNotification(), mSelectionData={ "
"mCausedByComposition=%s, mCausedBySelectionEvent=%s }",
this, ToChar(mSelectionData.mCausedByComposition),
ToChar(mSelectionData.mCausedBySelectionEvent)));
mNeedsToNotifyIMEOfSelectionChange = true;
}
void IMEContentObserver::MaybeNotifyIMEOfFocusSet() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p MaybeNotifyIMEOfFocusSet()", this));
PostFocusSetNotification();
FlushMergeableNotifications();
}
void IMEContentObserver::MaybeNotifyIMEOfTextChange(
const TextChangeDataBase& aTextChangeData) {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p MaybeNotifyIMEOfTextChange(aTextChangeData=%s)", this,
ToString(aTextChangeData).c_str()));
if (mEditorBase && mEditorBase->IsTextEditor()) {
MOZ_DIAGNOSTIC_ASSERT(static_cast<int64_t>(mTextControlValueLength) +
aTextChangeData.Difference() >=
0);
mTextControlValueLength += aTextChangeData.Difference();
}
mTextChangeData += aTextChangeData;
PostTextChangeNotification();
FlushMergeableNotifications();
}
void IMEContentObserver::CancelNotifyingIMEOfTextChange() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p CancelNotifyingIMEOfTextChange()", this));
mTextChangeData.Clear();
mNeedsToNotifyIMEOfTextChange = false;
}
void IMEContentObserver::MaybeNotifyIMEOfSelectionChange(
bool aCausedByComposition, bool aCausedBySelectionEvent,
bool aOccurredDuringComposition) {
MOZ_LOG(
sIMECOLog, LogLevel::Debug,
("0x%p MaybeNotifyIMEOfSelectionChange(aCausedByComposition=%s, "
"aCausedBySelectionEvent=%s, aOccurredDuringComposition)",
this, ToChar(aCausedByComposition), ToChar(aCausedBySelectionEvent)));
mSelectionData.AssignReason(aCausedByComposition, aCausedBySelectionEvent,
aOccurredDuringComposition);
PostSelectionChangeNotification();
FlushMergeableNotifications();
}
void IMEContentObserver::MaybeNotifyIMEOfPositionChange() {
MOZ_LOG(sIMECOLog, LogLevel::Verbose,
("0x%p MaybeNotifyIMEOfPositionChange()", this));
// If reflow is caused by ContentEventHandler during PositionChangeEvent
// sending NOTIFY_IME_OF_POSITION_CHANGE, we don't need to notify IME of it
// again since ContentEventHandler returns the result including this reflow's
// result.
if (mIsHandlingQueryContentEvent &&
mSendingNotification == NOTIFY_IME_OF_POSITION_CHANGE) {
MOZ_LOG(sIMECOLog, LogLevel::Verbose,
("0x%p MaybeNotifyIMEOfPositionChange(), ignored since caused by "
"ContentEventHandler during sending NOTIFY_IME_OF_POSITION_CHANGE",
this));
return;
}
PostPositionChangeNotification();
FlushMergeableNotifications();
}
void IMEContentObserver::CancelNotifyingIMEOfPositionChange() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p CancelNotifyIMEOfPositionChange()", this));
mNeedsToNotifyIMEOfPositionChange = false;
}
void IMEContentObserver::MaybeNotifyCompositionEventHandled() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p MaybeNotifyCompositionEventHandled()", this));
PostCompositionEventHandledNotification();
FlushMergeableNotifications();
}
bool IMEContentObserver::UpdateSelectionCache(bool aRequireFlush /* = true */) {
MOZ_ASSERT(IsSafeToNotifyIME());
mSelectionData.ClearSelectionData();
// XXX Cannot we cache some information for reducing the cost to compute
// selection offset and writing mode?
WidgetQueryContentEvent querySelectedTextEvent(true, eQuerySelectedText,
mWidget);
querySelectedTextEvent.mNeedsToFlushLayout = aRequireFlush;
ContentEventHandler handler(GetPresContext());
handler.OnQuerySelectedText(&querySelectedTextEvent);
if (NS_WARN_IF(querySelectedTextEvent.Failed()) ||
NS_WARN_IF(querySelectedTextEvent.mReply->mContentsRoot !=
mRootElement)) {
return false;
}
mFocusedWidget = querySelectedTextEvent.mReply->mFocusedWidget;
mSelectionData.Assign(querySelectedTextEvent);
// WARNING: Don't set the reason of selection change here because it should be
// set the reason at sending the notification.
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p UpdateSelectionCache(), mSelectionData=%s", this,
ToString(mSelectionData).c_str()));
return true;
}
void IMEContentObserver::PostPositionChangeNotification() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p PostPositionChangeNotification()", this));
mNeedsToNotifyIMEOfPositionChange = true;
}
void IMEContentObserver::PostCompositionEventHandledNotification() {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p PostCompositionEventHandledNotification()", this));
mNeedsToNotifyIMEOfCompositionEventHandled = true;
}
bool IMEContentObserver::IsReflowLocked() const {
nsPresContext* presContext = GetPresContext();
if (NS_WARN_IF(!presContext)) {
return false;
}
PresShell* presShell = presContext->GetPresShell();
if (NS_WARN_IF(!presShell)) {
return false;
}
// During reflow, we shouldn't notify IME because IME may query content
// synchronously. Then, it causes ContentEventHandler will try to flush
// pending notifications during reflow.
return presShell->IsReflowLocked();
}
bool IMEContentObserver::IsSafeToNotifyIME() const {
// If this is already detached from the widget, this doesn't need to notify
// anything.
if (!mWidget) {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IsSafeToNotifyIME(), it's not safe because of no widget",
this));
return false;
}
// Don't notify IME of anything if it's not good time to do it.
if (mSuppressNotifications) {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IsSafeToNotifyIME(), it's not safe because of no widget",
this));
return false;
}
if (!mESM || NS_WARN_IF(!GetPresContext())) {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IsSafeToNotifyIME(), it's not safe because of no "
"EventStateManager and/or PresContext",
this));
return false;
}
// If it's in reflow, we should wait to finish the reflow.
// FYI: This should be called again from Reflow() or ReflowInterruptible().
if (IsReflowLocked()) {
MOZ_LOG(
sIMECOLog, LogLevel::Debug,
("0x%p IsSafeToNotifyIME(), it's not safe because of reflow locked",
this));
return false;
}
// If we're in handling an edit action, this method will be called later.
if (EditorIsHandlingEditSubAction()) {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IsSafeToNotifyIME(), it's not safe because of focused "
"editor handling somethings",
this));
return false;
}
return true;
}
void IMEContentObserver::FlushMergeableNotifications() {
if (!IsSafeToNotifyIME()) {
// So, if this is already called, this should do nothing.
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p FlushMergeableNotifications(), Warning, do nothing due to "
"unsafe to notify IME",
this));
return;
}
// Notifying something may cause nested call of this method. For example,
// when somebody notified one of the notifications may dispatch query content
// event. Then, it causes flushing layout which may cause another layout
// change notification.
if (mQueuedSender) {
// So, if this is already called, this should do nothing.
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p FlushMergeableNotifications(), Warning, do nothing due to "
"already flushing pending notifications",
this));
return;
}
// If text change notification and/or position change notification becomes
// unnecessary, let's cancel them.
if (mNeedsToNotifyIMEOfTextChange && !NeedsTextChangeNotification()) {
CancelNotifyingIMEOfTextChange();
}
if (mNeedsToNotifyIMEOfPositionChange && !NeedsPositionChangeNotification()) {
CancelNotifyingIMEOfPositionChange();
}
if (!NeedsToNotifyIMEOfSomething()) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p FlushMergeableNotifications(), Warning, due to no pending "
"notifications",
this));
return;
}
// NOTE: Reset each pending flag because sending notification may cause
// another change.
MOZ_LOG(
sIMECOLog, LogLevel::Info,
("0x%p FlushMergeableNotifications(), creating IMENotificationSender...",
this));
// If contents in selection range is modified, the selection range still
// has removed node from the tree. In such case, ContentIterator won't
// work well. Therefore, we shouldn't use AddScriptRunner() here since
// it may kick runnable event immediately after DOM tree is changed but
// the selection range isn't modified yet.
mQueuedSender = new IMENotificationSender(this);
mQueuedSender->Dispatch(mDocShell);
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p FlushMergeableNotifications(), finished", this));
}
void IMEContentObserver::TryToFlushPendingNotifications(bool aAllowAsync) {
// If a sender instance is sending notifications, we shouldn't try to create
// a new sender again because the sender will recreate by itself if there are
// new pending notifications.
if (mSendingNotification != NOTIFY_IME_OF_NOTHING) {
return;
}
// When the caller allows to put off notifying IME, we can wait the next
// call of this method or to run the queued sender.
if (mQueuedSender && XRE_IsContentProcess() && aAllowAsync) {
return;
}
if (!mQueuedSender) {
// If it was not safe to dispatch notifications when the pending
// notifications are posted, this may not have IMENotificationSender
// instance because it couldn't dispatch it, e.g., when an edit sub-action
// is being handled in the editor, we shouldn't do it even if it's safe to
// run script. Therefore, we need to create the sender instance here in the
// case.
if (!NeedsToNotifyIMEOfSomething()) {
return;
}
mQueuedSender = new IMENotificationSender(this);
}
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p TryToFlushPendingNotifications(), performing queued "
"IMENotificationSender forcibly",
this));
RefPtr<IMENotificationSender> queuedSender = mQueuedSender;
queuedSender->Run();
}
/******************************************************************************
* mozilla::IMEContentObserver::AChangeEvent
******************************************************************************/
bool IMEContentObserver::AChangeEvent::CanNotifyIME(
ChangeEventType aChangeEventType) const {
RefPtr<IMEContentObserver> observer = GetObserver();
if (NS_WARN_IF(!observer)) {
return false;
}
const LogLevel debugOrVerbose =
aChangeEventType == ChangeEventType::eChangeEventType_Position
? LogLevel::Verbose
: LogLevel::Debug;
if (aChangeEventType == eChangeEventType_CompositionEventHandled) {
if (observer->mWidget) {
return true;
}
MOZ_LOG(sIMECOLog, debugOrVerbose,
("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME of "
"composition event handled because of no widget",
this));
return false;
}
State state = observer->GetState();
// If it's not initialized, we should do nothing.
if (state == eState_NotObserving) {
MOZ_LOG(sIMECOLog, debugOrVerbose,
("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME because "
"of not observing",
this));
return false;
}
// If setting focus, just check the state.
if (aChangeEventType == eChangeEventType_Focus) {
if (!observer->mIMEHasFocus) {
return true;
}
MOZ_LOG(sIMECOLog, debugOrVerbose,
("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME of focus "
"change because of already focused",
this));
NS_WARNING("IME already has focus");
return false;
}
// If we've not notified IME of focus yet, we shouldn't notify anything.
if (!observer->mIMEHasFocus) {
MOZ_LOG(sIMECOLog, debugOrVerbose,
("0x%p AChangeEvent::CanNotifyIME(), Cannot notify IME because "
"of not focused",
this));
return false;
}
// If IME has focus, IMEContentObserver must hold the widget.
MOZ_ASSERT(observer->mWidget);
return true;
}
bool IMEContentObserver::AChangeEvent::IsSafeToNotifyIME(
ChangeEventType aChangeEventType) const {
const LogLevel warningOrVerbose =
aChangeEventType == ChangeEventType::eChangeEventType_Position
? LogLevel::Verbose
: LogLevel::Warning;
if (NS_WARN_IF(!nsContentUtils::IsSafeToRunScript())) {
MOZ_LOG(sIMECOLog, warningOrVerbose,
("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify "
"IME because of not safe to run script",
this));
return false;
}
RefPtr<IMEContentObserver> observer = GetObserver();
if (!observer) {
MOZ_LOG(sIMECOLog, warningOrVerbose,
("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify "
"IME because of no observer",
this));
return false;
}
// While we're sending a notification, we shouldn't send another notification
// recursively.
if (observer->mSendingNotification != NOTIFY_IME_OF_NOTHING) {
MOZ_LOG(sIMECOLog, warningOrVerbose,
("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify "
"IME because of the observer sending another notification",
this));
return false;
}
State state = observer->GetState();
if (aChangeEventType == eChangeEventType_Focus) {
if (NS_WARN_IF(state != eState_Initializing && state != eState_Observing)) {
MOZ_LOG(sIMECOLog, warningOrVerbose,
("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot "
"notify IME of focus because of not observing",
this));
return false;
}
} else if (aChangeEventType == eChangeEventType_CompositionEventHandled) {
// It doesn't need to check the observing status.
} else if (state != eState_Observing) {
MOZ_LOG(sIMECOLog, warningOrVerbose,
("0x%p AChangeEvent::IsSafeToNotifyIME(), Warning, Cannot notify "
"IME because of not observing",
this));
return false;
}
return observer->IsSafeToNotifyIME();
}
/******************************************************************************
* mozilla::IMEContentObserver::IMENotificationSender
******************************************************************************/
void IMEContentObserver::IMENotificationSender::Dispatch(
nsIDocShell* aDocShell) {
if (XRE_IsContentProcess() && aDocShell) {
if (RefPtr<nsPresContext> presContext = aDocShell->GetPresContext()) {
if (nsRefreshDriver* refreshDriver = presContext->RefreshDriver()) {
refreshDriver->AddEarlyRunner(this);
return;
}
}
}
NS_DispatchToCurrentThread(this);
}
NS_IMETHODIMP
IMEContentObserver::IMENotificationSender::Run() {
if (NS_WARN_IF(mIsRunning)) {
MOZ_LOG(
sIMECOLog, LogLevel::Error,
("0x%p IMENotificationSender::Run(), FAILED, due to called recursively",
this));
return NS_OK;
}
RefPtr<IMEContentObserver> observer = GetObserver();
if (!observer) {
return NS_OK;
}
AutoRestore<bool> running(mIsRunning);
mIsRunning = true;
// This instance was already performed forcibly.
if (observer->mQueuedSender != this) {
return NS_OK;
}
// NOTE: Reset each pending flag because sending notification may cause
// another change.
if (observer->mNeedsToNotifyIMEOfFocusSet) {
observer->mNeedsToNotifyIMEOfFocusSet = false;
SendFocusSet();
observer->mQueuedSender = nullptr;
// If it's not safe to notify IME of focus, SendFocusSet() sets
// mNeedsToNotifyIMEOfFocusSet true again. For guaranteeing to send the
// focus notification later, we should put a new sender into the queue but
// this case must be rare. Note that if mIMEContentObserver is already
// destroyed, mNeedsToNotifyIMEOfFocusSet is never set true again.
if (observer->mNeedsToNotifyIMEOfFocusSet) {
MOZ_ASSERT(!observer->mIMEHasFocus);
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::Run(), posting "
"IMENotificationSender to current thread",
this));
observer->mQueuedSender = new IMENotificationSender(observer);
observer->mQueuedSender->Dispatch(observer->mDocShell);
return NS_OK;
}
// This is the first notification to IME. So, we don't need to notify
// anymore since IME starts to query content after it gets focus.
observer->ClearPendingNotifications();
return NS_OK;
}
if (observer->mNeedsToNotifyIMEOfTextChange) {
observer->mNeedsToNotifyIMEOfTextChange = false;
SendTextChange();
}
// If a text change notification causes another text change again, we should
// notify IME of that before sending a selection change notification.
if (!observer->mNeedsToNotifyIMEOfTextChange) {
// Be aware, PuppetWidget depends on the order of this. A selection change
// notification should not be sent before a text change notification because
// PuppetWidget shouldn't query new text content every selection change.
if (observer->mNeedsToNotifyIMEOfSelectionChange) {
observer->mNeedsToNotifyIMEOfSelectionChange = false;
SendSelectionChange();
}
}
// If a text change notification causes another text change again or a
// selection change notification causes either a text change or another
// selection change, we should notify IME of those before sending a position
// change notification.
if (!observer->mNeedsToNotifyIMEOfTextChange &&
!observer->mNeedsToNotifyIMEOfSelectionChange) {
if (observer->mNeedsToNotifyIMEOfPositionChange) {
observer->mNeedsToNotifyIMEOfPositionChange = false;
SendPositionChange();
}
}
// Composition event handled notification should be sent after all the
// other notifications because this notifies widget of finishing all pending
// events are handled completely.
if (!observer->mNeedsToNotifyIMEOfTextChange &&
!observer->mNeedsToNotifyIMEOfSelectionChange &&
!observer->mNeedsToNotifyIMEOfPositionChange) {
if (observer->mNeedsToNotifyIMEOfCompositionEventHandled) {
observer->mNeedsToNotifyIMEOfCompositionEventHandled = false;
SendCompositionEventHandled();
}
}
observer->mQueuedSender = nullptr;
// If notifications caused some new change, we should notify them now.
if (observer->NeedsToNotifyIMEOfSomething()) {
if (observer->GetState() == eState_StoppedObserving) {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::Run(), waiting "
"IMENotificationSender to be reinitialized",
this));
} else {
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::Run(), posting "
"IMENotificationSender to current thread",
this));
observer->mQueuedSender = new IMENotificationSender(observer);
observer->mQueuedSender->Dispatch(observer->mDocShell);
}
}
return NS_OK;
}
void IMEContentObserver::IMENotificationSender::SendFocusSet() {
RefPtr<IMEContentObserver> observer = GetObserver();
if (!observer) {
return;
}
if (!CanNotifyIME(eChangeEventType_Focus)) {
// If IMEContentObserver has already gone, we don't need to notify IME of
// focus.
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendFocusSet(), Warning, does not "
"send notification due to impossible to notify IME of focus",
this));
observer->ClearPendingNotifications();
return;
}
if (!IsSafeToNotifyIME(eChangeEventType_Focus)) {
MOZ_LOG(
sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendFocusSet(), Warning, does not send "
"notification due to unsafe, retrying to send NOTIFY_IME_OF_FOCUS...",
this));
observer->PostFocusSetNotification();
return;
}
observer->mIMEHasFocus = true;
// Initialize selection cache with the first selection data. However, this
// may be handled synchronously when the editor gets focus. In that case,
// some frames may be dirty and they may be required to get caret frame in
// ContentEventHandler::Init() to get the nearest widget from the selection.
// Therefore, we need to update selection cache with flushing the pending
// notifications.
observer->UpdateSelectionCache(true);
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p IMENotificationSender::SendFocusSet(), sending "
"NOTIFY_IME_OF_FOCUS...",
this));
MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING);
observer->mSendingNotification = NOTIFY_IME_OF_FOCUS;
IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_FOCUS),
observer->mWidget);
observer->mSendingNotification = NOTIFY_IME_OF_NOTHING;
// IMENotificationRequests referred by ObserveEditableNode() may be different
// before or after widget receives NOTIFY_IME_OF_FOCUS. Therefore, we need
// to guarantee to call ObserveEditableNode() after sending
// NOTIFY_IME_OF_FOCUS.
observer->OnIMEReceivedFocus();
MOZ_LOG(
sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::SendFocusSet(), sent NOTIFY_IME_OF_FOCUS",
this));
}
void IMEContentObserver::IMENotificationSender::SendSelectionChange() {
RefPtr<IMEContentObserver> observer = GetObserver();
if (!observer) {
return;
}
if (!CanNotifyIME(eChangeEventType_Selection)) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendSelectionChange(), Warning, "
"does not send notification due to impossible to notify IME of "
"selection change",
this));
return;
}
if (!IsSafeToNotifyIME(eChangeEventType_Selection)) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendSelectionChange(), Warning, "
"does not send notification due to unsafe, retrying to send "
"NOTIFY_IME_OF_SELECTION_CHANGE...",
this));
observer->PostSelectionChangeNotification();
return;
}
SelectionChangeData lastSelChangeData = observer->mSelectionData;
if (NS_WARN_IF(!observer->UpdateSelectionCache())) {
MOZ_LOG(sIMECOLog, LogLevel::Error,
("0x%p IMENotificationSender::SendSelectionChange(), FAILED, due "
"to UpdateSelectionCache() failure",
this));
return;
}
// The state may be changed since querying content causes flushing layout.
if (!CanNotifyIME(eChangeEventType_Selection)) {
MOZ_LOG(sIMECOLog, LogLevel::Error,
("0x%p IMENotificationSender::SendSelectionChange(), FAILED, due "
"to flushing layout having changed something",
this));
return;
}
// If the selection isn't changed actually, we shouldn't notify IME of
// selection change.
SelectionChangeData& newSelChangeData = observer->mSelectionData;
if (lastSelChangeData.IsInitialized() &&
lastSelChangeData.EqualsRangeAndDirectionAndWritingMode(
newSelChangeData)) {
MOZ_LOG(
sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::SendSelectionChange(), not notifying IME "
"of NOTIFY_IME_OF_SELECTION_CHANGE due to not changed actually",
this));
return;
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p IMENotificationSender::SendSelectionChange(), sending "
"NOTIFY_IME_OF_SELECTION_CHANGE... newSelChangeData=%s",
this, ToString(newSelChangeData).c_str()));
IMENotification notification(NOTIFY_IME_OF_SELECTION_CHANGE);
notification.SetData(observer->mSelectionData);
MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING);
observer->mSendingNotification = NOTIFY_IME_OF_SELECTION_CHANGE;
IMEStateManager::NotifyIME(notification, observer->mWidget);
observer->mSendingNotification = NOTIFY_IME_OF_NOTHING;
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::SendSelectionChange(), sent "
"NOTIFY_IME_OF_SELECTION_CHANGE",
this));
}
void IMEContentObserver::IMENotificationSender::SendTextChange() {
RefPtr<IMEContentObserver> observer = GetObserver();
if (!observer) {
return;
}
if (!CanNotifyIME(eChangeEventType_Text)) {
MOZ_LOG(
sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendTextChange(), Warning, does not "
"send notification due to impossible to notify IME of text change",
this));
return;
}
if (!IsSafeToNotifyIME(eChangeEventType_Text)) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendTextChange(), Warning, does "
"not send notification due to unsafe, retrying to send "
"NOTIFY_IME_OF_TEXT_CHANGE...",
this));
observer->PostTextChangeNotification();
return;
}
// If text change notification is unnecessary anymore, just cancel it.
if (!observer->NeedsTextChangeNotification()) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendTextChange(), Warning, "
"canceling sending NOTIFY_IME_OF_TEXT_CHANGE",
this));
observer->CancelNotifyingIMEOfTextChange();
return;
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p IMENotificationSender::SendTextChange(), sending "
"NOTIFY_IME_OF_TEXT_CHANGE... mIMEContentObserver={ "
"mTextChangeData=%s }",
this, ToString(observer->mTextChangeData).c_str()));
IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE);
notification.SetData(observer->mTextChangeData);
observer->mTextChangeData.Clear();
MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING);
observer->mSendingNotification = NOTIFY_IME_OF_TEXT_CHANGE;
IMEStateManager::NotifyIME(notification, observer->mWidget);
observer->mSendingNotification = NOTIFY_IME_OF_NOTHING;
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::SendTextChange(), sent "
"NOTIFY_IME_OF_TEXT_CHANGE",
this));
}
void IMEContentObserver::IMENotificationSender::SendPositionChange() {
RefPtr<IMEContentObserver> observer = GetObserver();
if (!observer) {
return;
}
if (!CanNotifyIME(eChangeEventType_Position)) {
MOZ_LOG(sIMECOLog, LogLevel::Verbose,
("0x%p IMENotificationSender::SendPositionChange(), Warning, "
"does not send notification due to impossible to notify IME of "
"position change",
this));
return;
}
if (!IsSafeToNotifyIME(eChangeEventType_Position)) {
MOZ_LOG(sIMECOLog, LogLevel::Verbose,
("0x%p IMENotificationSender::SendPositionChange(), Warning, "
"does not send notification due to unsafe, retrying to send "
"NOTIFY_IME_OF_POSITION_CHANGE...",
this));
observer->PostPositionChangeNotification();
return;
}
// If position change notification is unnecessary anymore, just cancel it.
if (!observer->NeedsPositionChangeNotification()) {
MOZ_LOG(sIMECOLog, LogLevel::Verbose,
("0x%p IMENotificationSender::SendPositionChange(), Warning, "
"canceling sending NOTIFY_IME_OF_POSITION_CHANGE",
this));
observer->CancelNotifyingIMEOfPositionChange();
return;
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p IMENotificationSender::SendPositionChange(), sending "
"NOTIFY_IME_OF_POSITION_CHANGE...",
this));
MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING);
observer->mSendingNotification = NOTIFY_IME_OF_POSITION_CHANGE;
IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_POSITION_CHANGE),
observer->mWidget);
observer->mSendingNotification = NOTIFY_IME_OF_NOTHING;
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::SendPositionChange(), sent "
"NOTIFY_IME_OF_POSITION_CHANGE",
this));
}
void IMEContentObserver::IMENotificationSender::SendCompositionEventHandled() {
RefPtr<IMEContentObserver> observer = GetObserver();
if (!observer) {
return;
}
if (!CanNotifyIME(eChangeEventType_CompositionEventHandled)) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendCompositionEventHandled(), "
"Warning, does not send notification due to impossible to notify "
"IME of composition event handled",
this));
return;
}
if (!IsSafeToNotifyIME(eChangeEventType_CompositionEventHandled)) {
MOZ_LOG(sIMECOLog, LogLevel::Warning,
("0x%p IMENotificationSender::SendCompositionEventHandled(), "
"Warning, does not send notification due to unsafe, retrying to "
"send NOTIFY_IME_OF_POSITION_CHANGE...",
this));
observer->PostCompositionEventHandledNotification();
return;
}
MOZ_LOG(sIMECOLog, LogLevel::Info,
("0x%p IMENotificationSender::SendCompositionEventHandled(), sending "
"NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED...",
this));
MOZ_RELEASE_ASSERT(observer->mSendingNotification == NOTIFY_IME_OF_NOTHING);
observer->mSendingNotification = NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED;
IMEStateManager::NotifyIME(
IMENotification(NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED),
observer->mWidget);
observer->mSendingNotification = NOTIFY_IME_OF_NOTHING;
MOZ_LOG(sIMECOLog, LogLevel::Debug,
("0x%p IMENotificationSender::SendCompositionEventHandled(), sent "
"NOTIFY_IME_OF_COMPOSITION_EVENT_HANDLED",
this));
}
/******************************************************************************
* mozilla::IMEContentObserver::DocumentObservingHelper
******************************************************************************/
NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver::DocumentObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver::DocumentObserver)
// StopObserving() releases mIMEContentObserver and mDocument.
tmp->StopObserving();
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(IMEContentObserver::DocumentObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mIMEContentObserver)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocument)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver::DocumentObserver)
NS_INTERFACE_MAP_ENTRY(nsIDocumentObserver)
NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
NS_INTERFACE_MAP_ENTRY(nsISupports)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver::DocumentObserver)
NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver::DocumentObserver)
void IMEContentObserver::DocumentObserver::Observe(Document* aDocument) {
MOZ_ASSERT(aDocument);
// Guarantee that aDocument won't be destroyed during a call of
// StopObserving().
RefPtr<Document> newDocument = aDocument;
StopObserving();
mDocument = std::move(newDocument);
mDocument->AddObserver(this);
}
void IMEContentObserver::DocumentObserver::StopObserving() {
if (!IsObserving()) {
return;
}
// Grab IMEContentObserver which could be destroyed during method calls.
RefPtr<IMEContentObserver> observer = std::move(mIMEContentObserver);
// Stop observing the document first.
RefPtr<Document> document = std::move(mDocument);
document->RemoveObserver(this);
// Notify IMEContentObserver of ending of document updates if this already
// notified it of beginning of document updates.
for (; IsUpdating(); --mDocumentUpdating) {
// FYI: IsUpdating() returns true until mDocumentUpdating becomes 0.
// However, IsObserving() returns false now because mDocument was
// already cleared above. Therefore, this method won't be called
// recursively.
observer->EndDocumentUpdate();
}
}
void IMEContentObserver::DocumentObserver::Destroy() {
StopObserving();
mIMEContentObserver = nullptr;
}
void IMEContentObserver::DocumentObserver::BeginUpdate(Document* aDocument) {
if (NS_WARN_IF(Destroyed()) || NS_WARN_IF(!IsObserving())) {
return;
}
mIMEContentObserver->BeginDocumentUpdate();
mDocumentUpdating++;
}
void IMEContentObserver::DocumentObserver::EndUpdate(Document* aDocument) {
if (NS_WARN_IF(Destroyed()) || NS_WARN_IF(!IsObserving()) ||
NS_WARN_IF(!IsUpdating())) {
return;
}
mDocumentUpdating--;
mIMEContentObserver->EndDocumentUpdate();
}
/******************************************************************************
* mozilla::IMEContentObserver::FlatTextCache
******************************************************************************/
void IMEContentObserver::FlatTextCache::Clear(const char* aCallerName) {
if (!HasCache()) {
return;
}
MOZ_LOG(sCacheLog, LogLevel::Info,
("%s.Clear: called by %s", mInstanceName, aCallerName));
mContainerNode = nullptr;
mContent = nullptr;
mFlatTextLength = 0;
}
nsresult IMEContentObserver::FlatTextCache::
ComputeAndCacheFlatTextLengthBeforeEndOfContent(
const char* aCallerName, const nsIContent& aContent,
const Element* aRootElement) {
MOZ_ASSERT(aRootElement);
MOZ_ASSERT(aContent.GetParentNode());
uint32_t length = 0;
nsresult rv = ContentEventHandler::GetFlatTextLengthInRange(
RawNodePosition::BeforeFirstContentOf(*aRootElement),
RawNodePosition::After(aContent), aRootElement, &length,
LineBreakType::LINE_BREAK_TYPE_NATIVE);
if (NS_FAILED(rv)) {
Clear(aCallerName);
return rv;
}
CacheFlatTextLengthBeforeEndOfContent(aCallerName, aContent, length,
aRootElement);
return NS_OK;
}
void IMEContentObserver::FlatTextCache::CacheFlatTextLengthBeforeEndOfContent(
const char* aCallerName, const nsIContent& aContent,
uint32_t aFlatTextLength, const dom::Element* aRootElement) {
mContainerNode = aContent.GetParentNode();
mContent = const_cast<nsIContent*>(&aContent);
mFlatTextLength = aFlatTextLength;
MOZ_ASSERT(IsCachingToEndOfContent());
MOZ_LOG(sCacheLog, LogLevel::Info,
("%s.%s: called by %s -> %s", mInstanceName, __FUNCTION__,
aCallerName, ToString(*this).c_str()));
AssertValidCache(aRootElement);
}
nsresult IMEContentObserver::FlatTextCache::
ComputeAndCacheFlatTextLengthBeforeFirstContent(
const char* aCallerName, const nsINode& aContainer,
const Element* aRootElement) {
MOZ_ASSERT(aRootElement);
const Result<uint32_t, nsresult>
lengthIncludingLineBreakCausedByOpenTagOfContainer =
FlatTextCache::ComputeTextLengthBeforeFirstContentOf(aContainer,
aRootElement);
if (MOZ_UNLIKELY(
lengthIncludingLineBreakCausedByOpenTagOfContainer.isErr())) {
Clear(__FUNCTION__);
return lengthIncludingLineBreakCausedByOpenTagOfContainer.inspectErr();
}
CacheFlatTextLengthBeforeFirstContent(
aCallerName, aContainer,
lengthIncludingLineBreakCausedByOpenTagOfContainer.inspect(),
aRootElement);
return NS_OK;
}
void IMEContentObserver::FlatTextCache::CacheFlatTextLengthBeforeFirstContent(
const char* aCallerName, const nsINode& aContainer,
uint32_t aFlatTextLength, const dom::Element* aRootElement) {
mContainerNode = const_cast<nsINode*>(&aContainer);
mContent = nullptr;
mFlatTextLength = aFlatTextLength;
MOZ_ASSERT(IsCachingToStartOfContainer());
MOZ_LOG(sCacheLog, LogLevel::Info,
("%s.%s: called by %s -> %s", mInstanceName, __FUNCTION__,
aCallerName, ToString(*this).c_str()));
AssertValidCache(aRootElement);
}
Maybe<uint32_t>
IMEContentObserver::FlatTextCache::GetFlatTextLengthBeforeContent(
const nsIContent& aContent, const dom::Element* aRootElement,
ForRemoval aForRemoval) const {
MOZ_ASSERT(aRootElement);
if (!mContainerNode) {
return Nothing();
}
nsIContent* const prevSibling = aContent.GetPreviousSibling();
if (IsCachingToStartOfContainer()) {
MOZ_ASSERT(!mContent);
// If aContent is the first child of mContainerNode and we're caching text
// length before first child of mContainerNode, we're caching the result
// as-is..
if (!prevSibling && mContainerNode == aContent.GetParentNode()) {
return Some(mFlatTextLength);
}
return Nothing();
}
MOZ_ASSERT(IsCachingToEndOfContent());
MOZ_ASSERT(mContent);
// If we're caching text length before end of previous sibling of aContent,
// the cached length is the result of this call.
if (mContent == prevSibling) {
return Some(mFlatTextLength);
}
// If we're caching text length before end of aContent, aContent siblings
// may be being removed backward because aContent is the previous sibling of
// previously removed node. We should return the length with computing the
// text length of aContent because it's much faster than computing the length
// starting from the root element especially when there are a lot of preceding
// content.
if (mContent == &aContent) {
const Result<uint32_t, nsresult> textLength =
FlatTextCache::ComputeTextLengthOfContent(aContent, aRootElement,
aForRemoval);
if (NS_WARN_IF(textLength.isErr()) ||
NS_WARN_IF(mFlatTextLength < textLength.inspect())) {
return Nothing();
}
return Some(mFlatTextLength - textLength.inspect());
}
return Nothing();
}
Maybe<uint32_t> IMEContentObserver::FlatTextCache::GetFlatTextOffsetOnInsertion(
const nsIContent& aFirstContent, const nsIContent& aLastContent,
const dom::Element* aRootElement) const {
MOZ_ASSERT(aRootElement);
MOZ_ASSERT(aFirstContent.GetParentNode() == aLastContent.GetParentNode());
MOZ_ASSERT(!aFirstContent.IsBeingRemoved());
MOZ_ASSERT(!aLastContent.IsBeingRemoved());
if (!mContainerNode || mContainerNode != aFirstContent.GetParentNode()) {
return Nothing();
}
if (IsCachingToStartOfContainer()) {
MOZ_ASSERT(!mContent);
// If aFirstContent is the first child of mContainerNode, we're caching the
// result as-is.
if (mContainerNode->GetFirstChild() == &aFirstContent) {
return Some(mFlatTextLength);
}
return Nothing();
}
MOZ_ASSERT(IsCachingToEndOfContent());
MOZ_ASSERT(mContent);
MOZ_ASSERT(mContent != &aFirstContent);
MOZ_ASSERT(mContent != &aLastContent);
// When the content nodes are inserted forward, we may cache text length
// before end of last inserted content. If so, mContent should be the
// previous sibling of aFirstContent. Then, we can return the cached length
// simply.
if (mContent == aFirstContent.GetPreviousSibling()) {
return Some(mFlatTextLength);
}
// When the content nodes inserted backward, we may cache text length before
// the end of the last inserted content which is next or latter sibling of
// aLastContent. In this case, we can compute the length with the cache with
// computing text length starting from the next sibling of aLastContent to
// mContent which were previously inserted. That must be faster than
// computing the length starting from the root element.
if (mContent == aLastContent.GetNextSibling() ||
aLastContent.ComputeIndexInParentNode().valueOr(UINT32_MAX) <
mContent->ComputeIndexInParentNode().valueOr(0u)) {
Result<uint32_t, nsresult> previouslyInsertedTextLengthOrError =
FlatTextCache::ComputeTextLengthStartOfContentToEndOfContent(
*aLastContent.GetNextSibling(), *mContent, aRootElement);
if (NS_WARN_IF(previouslyInsertedTextLengthOrError.isErr()) ||
NS_WARN_IF(mFlatTextLength <
previouslyInsertedTextLengthOrError.inspect())) {
return Nothing();
}
// mFlatTextLength contains the last inserted text length, but it does not
// contain text length starting from aFirstContent to aLastContent.
// Therefore, subtracting the last inserted text length from mFlatTextLength
// equals the text length before aFirstContent.
return Some(mFlatTextLength - previouslyInsertedTextLengthOrError.unwrap());
}
return Nothing();
}
/* static */
Result<uint32_t, nsresult>
IMEContentObserver::FlatTextCache::ComputeTextLengthOfContent(
const nsIContent& aContent, const dom::Element* aRootElement,
ForRemoval aForRemoval) {
MOZ_ASSERT(aRootElement);
if (const Text* textNode = Text::FromNode(aContent)) {
return ContentEventHandler::GetNativeTextLength(*textNode);
}
if (aForRemoval == ForRemoval::Yes) {
// When we compute the text length of the removing content node, we need to
// select all children in the removing node because of the same reason
// above. Therefore, if a <div> is being removed, we want to compute
// `{<div>...}</div>`. In this case, we want to include the open tag of
// aRemovingContent if it's an element to add the line break if it's caused
// by the open tag. However, we have no way to specify it with
// RawNodePosition, but ContentEventHandler::GetFlatTextLengthInRange()
// treats the range as the start container is selected. Therefore, we
// should use a RawNodePosition setting its container to the removed node.
uint32_t textLength = 0;
RawNodePosition start(const_cast<nsIContent*>(&aContent), 0u);
start.mAfterOpenTag = false;
nsresult rv = ContentEventHandler::GetFlatTextLengthInRange(
start, RawNodePosition::AtEndOf(aContent), aRootElement, &textLength,
LineBreakType::LINE_BREAK_TYPE_NATIVE, /* aIsRemovingNode = */ true);
if (NS_FAILED(rv)) {
return Err(rv);
}
return textLength;
}
return ComputeTextLengthStartOfContentToEndOfContent(aContent, aContent,
aRootElement);
}
/* static */
Result<uint32_t, nsresult>
IMEContentObserver::FlatTextCache::ComputeTextLengthBeforeContent(
const nsIContent& aContent, const dom::Element* aRootElement) {
uint32_t textLengthBeforeContent = 0;
nsresult rv = ContentEventHandler::GetFlatTextLengthInRange(
RawNodePosition::BeforeFirstContentOf(*aRootElement),
RawNodePosition::Before(aContent), aRootElement, &textLengthBeforeContent,
LineBreakType::LINE_BREAK_TYPE_NATIVE);
if (NS_FAILED(rv)) {
return Err(rv);
}
return textLengthBeforeContent;
}
/* static */
Result<uint32_t, nsresult> IMEContentObserver::FlatTextCache::
ComputeTextLengthStartOfContentToEndOfContent(
const nsIContent& aStartContent, const nsIContent& aEndContent,
const dom::Element* aRootElement) {
uint32_t textLength = 0;
nsresult rv = ContentEventHandler::GetFlatTextLengthInRange(
RawNodePosition::Before(aStartContent),
RawNodePosition::After(aEndContent), aRootElement, &textLength,
LineBreakType::LINE_BREAK_TYPE_NATIVE);
if (NS_FAILED(rv)) {
return Err(rv);
}
return textLength;
}
/* static */
Result<uint32_t, nsresult>
IMEContentObserver::FlatTextCache::ComputeTextLengthBeforeFirstContentOf(
const nsINode& aContainer, const dom::Element* aRootElement) {
uint32_t lengthIncludingLineBreakCausedByOpenTagOfContent = 0;
nsresult rv = ContentEventHandler::GetFlatTextLengthInRange(
RawNodePosition::BeforeFirstContentOf(*aRootElement),
// Include the line break caused by open tag of aContainer if it's an
// element when we cache text length before first content of aContainer.
RawNodePosition(const_cast<nsINode*>(&aContainer), nullptr), aRootElement,
&lengthIncludingLineBreakCausedByOpenTagOfContent,
LineBreakType::LINE_BREAK_TYPE_NATIVE);
if (NS_FAILED(rv)) {
return Err(rv);
}
return lengthIncludingLineBreakCausedByOpenTagOfContent;
}
void IMEContentObserver::FlatTextCache::AssertValidCache(
const Element* aRootElement) const {
#ifdef DEBUG
if (MOZ_LIKELY(
!StaticPrefs::test_ime_content_observer_assert_valid_cache())) {
return;
}
MOZ_ASSERT(aRootElement);
if (!mContainerNode) {
return;
}
MOZ_ASSERT(mContainerNode->IsInclusiveDescendantOf(aRootElement));
MOZ_ASSERT_IF(mContent, mContent->IsInclusiveDescendantOf(aRootElement));
if (IsCachingToEndOfContent()) {
MOZ_ASSERT(mContent);
Result<uint32_t, nsresult> offset =
FlatTextCache::ComputeTextLengthBeforeContent(*mContent, aRootElement);
MOZ_ASSERT(offset.isOk());
Result<uint32_t, nsresult> length =
FlatTextCache::ComputeTextLengthStartOfContentToEndOfContent(
*mContent, *mContent, aRootElement);
MOZ_ASSERT(length.isOk());
if (mFlatTextLength != offset.inspect() + length.inspect()) {
nsAutoString innerHTMLOfEditable;
const_cast<Element*>(aRootElement)
->GetInnerHTML(innerHTMLOfEditable, IgnoreErrors());
NS_WARNING(
nsPrintfCString(
"mFlatTextLength=%u, offset: %u, length: %u, mContainerNode:%s, "
"mContent=%s (%s)",
mFlatTextLength, offset.inspect(), length.inspect(),
ToString(mContainerNode).c_str(), ToString(*mContent).c_str(),
NS_ConvertUTF16toUTF8(innerHTMLOfEditable).get())
.get());
}
MOZ_ASSERT(mFlatTextLength == offset.inspect() + length.inspect());
return;
}
MOZ_ASSERT(!mContent);
MOZ_ASSERT(mContainerNode->IsContent());
Result<uint32_t, nsresult> offset =
ComputeTextLengthBeforeFirstContentOf(*mContainerNode, aRootElement);
MOZ_ASSERT(offset.isOk());
if (mFlatTextLength != offset.inspect()) {
nsAutoString innerHTMLOfEditable;
const_cast<Element*>(aRootElement)
->GetInnerHTML(innerHTMLOfEditable, IgnoreErrors());
NS_WARNING(nsPrintfCString(
"mFlatTextLength=%u, offset: %u, mContainerNode:%s (%s)",
mFlatTextLength, offset.inspect(),
ToString(mContainerNode).c_str(),
NS_ConvertUTF16toUTF8(innerHTMLOfEditable).get())
.get());
}
MOZ_ASSERT(mFlatTextLength == offset.inspect());
#endif // #ifdef DEBUG
}
void IMEContentObserver::FlatTextCache::ContentAdded(
const char* aCallerName, const nsIContent& aFirstContent,
const nsIContent& aLastContent, const Maybe<uint32_t>& aAddedFlatTextLength,
const Element* aRootElement) {
MOZ_ASSERT(nsContentUtils::ComparePoints(
RawRangeBoundary(aFirstContent.GetParentNode(),
aFirstContent.GetPreviousSibling()),
RawRangeBoundary(aLastContent.GetParentNode(),
aLastContent.GetPreviousSibling()))
.value() <= 0);
if (!mContainerNode) {
return; // No cache.
}
// We can keep cache without anything if the next sibling is the first added
// content.
if (mContent && &aFirstContent == mContent->GetNextSibling()) {
return;
}
if (IsCachingToStartOfContainer()) {
MOZ_ASSERT(!mContent);
// We can keep the cache if added nodes are children of mContainerNode since
// we cache the text length before its first child.
if (mContainerNode == aFirstContent.GetParentNode()) {
AssertValidCache(aRootElement);
return;
}
// Let's clear the cache for avoiding to do anything expensive for a hot
// path only for not frequent cases. Be aware, this is a hot code path
// here. Therefore, expensive computation would make the DOM mutation
// slower.
Clear(aCallerName);
return;
}
MOZ_ASSERT(IsCachingToEndOfContent());
MOZ_ASSERT(mContent);
if (aAddedFlatTextLength.isSome() &&
aLastContent.GetNextSibling() == mContent) {
// If we cache test length before end of next sibling of the last added
// content node, we can update the cached text simply.
CacheFlatTextLengthBeforeEndOfContent(
aCallerName, *mContent, mFlatTextLength + *aAddedFlatTextLength,
aRootElement);
return;
}
// Let's clear the cache for avoiding to do anything expensive for a hot
// path only for not frequent cases. Be aware, this is a hot code path here.
// Therefore, expensive computation would make the DOM mutation slower.
Clear(aCallerName);
}
void IMEContentObserver::FlatTextCache::ContentWillBeRemoved(
const nsIContent& aContent, uint32_t aFlatTextLengthOfContent,
const Element* aRootElement) {
if (!mContainerNode) {
return; // No cache.
}
// We can keep the cache without anything if the next sibling is removed.
if (mContent && mContent == aContent.GetPreviousSibling()) {
return;
}
if (IsCachingToStartOfContainer()) {
MOZ_ASSERT(!mContent);
// We're caching text length before first child of mContainerNode.
// Therefore, if a child of mContainerNode is being removed, we can keep the
// cache.
if (mContainerNode == aContent.GetParentNode()) {
AssertValidCache(aRootElement);
return;
}
// Let's clear the cache for avoiding to do anything expensive for a hot
// path only for not frequent cases. Be aware, this is a hot code path
// here. Therefore, expensive computation would make the DOM mutation
// slower.
Clear("FlatTextCache::ContentRemoved");
return;
}
MOZ_ASSERT(IsCachingToEndOfContent());
if (&aContent == mContent) {
MOZ_ASSERT(mFlatTextLength >= aFlatTextLengthOfContent);
if (NS_WARN_IF(mFlatTextLength < aFlatTextLengthOfContent)) {
Clear("FlatTextCache::ContentRemoved");
return;
}
// We're caching text length before end of aContent. So, if there is a
// previous sibling, we can cache text length before aContent with
// subtracting the text length caused by aContent from the cached value.
if (nsIContent* prevSibling = aContent.GetPreviousSibling()) {
CacheFlatTextLengthBeforeEndOfContent(
"FlatTextCache::ContentRemoved", *prevSibling,
mFlatTextLength - aFlatTextLengthOfContent, aRootElement);
return;
}
// Otherwise, i.e., if aContent is first child of mContainerNode, we can
// cache text length before first content of mContainerNode with subtracting
// the text length caused by aContent from the cached value.
CacheFlatTextLengthBeforeFirstContent(
"FlatTextCache::ContentRemoved", *mContainerNode,
mFlatTextLength - aFlatTextLengthOfContent, aRootElement);
return;
}
// Let's clear the cache for avoiding to do anything expensive for a hot
// path only for not frequent cases. Be aware, this is a hot code path here.
// Therefore, expensive computation would make the DOM mutation slower.
Clear("FlatTextCache::ContentRemoved");
}
/******************************************************************************
* mozilla::IMEContentObserver::AddedContentCache
******************************************************************************/
void IMEContentObserver::AddedContentCache::Clear(const char* aCallerName) {
mFirst = nullptr;
mLast = nullptr;
MOZ_LOG(sCacheLog, LogLevel::Info,
("AddedContentCache::Clear: called by %s", aCallerName));
}
bool IMEContentObserver::AddedContentCache::IsInRange(
const nsIContent& aContent, const dom::Element* aRootElement) const {
MOZ_ASSERT(HasCache());
// First, try to find sibling of mFirst from the ancestor chain of aContent.
const nsIContent* sibling = [&]() -> const nsIContent* {
const nsIContent* maybeSibling = &aContent;
const nsIContent* const container = mFirst->GetParent();
for (const nsIContent* ancestor : aContent.AncestorsOfType<nsIContent>()) {
if (ancestor == container) {
return maybeSibling;
}
if (ancestor == aRootElement) {
return nullptr;
}
maybeSibling = ancestor;
}
return nullptr;
}();
if (!sibling) {
return false; // Not in same container node
}
// Let's avoid to compute indices...
if (mFirst == sibling || mLast == sibling ||
(mFirst != mLast && (mFirst->GetNextSibling() == sibling ||
sibling->GetNextSibling() == mLast))) {
return true;
}
if (mFirst == mLast || sibling->GetNextSibling() == mFirst ||
mLast->GetNextSibling() == sibling || !sibling->GetPreviousSibling() ||
!sibling->GetNextSibling()) {
return false;
}
const Maybe<uint32_t> index = aContent.ComputeIndexInParentNode();
MOZ_ASSERT(index.isSome());
const Maybe<uint32_t> firstIndex = mFirst->ComputeIndexInParentNode();
MOZ_ASSERT(firstIndex.isSome());
const Maybe<uint32_t> lastIndex = mLast->ComputeIndexInParentNode();
MOZ_ASSERT(lastIndex.isSome());
return firstIndex.value() < index.value() &&
index.value() < lastIndex.value();
}
bool IMEContentObserver::AddedContentCache::CanMergeWith(
const nsIContent& aFirstContent, const nsIContent& aLastContent,
const dom::Element* aRootElement) const {
MOZ_ASSERT(HasCache());
if (aLastContent.GetNextSibling() == mFirst ||
mLast->GetNextSibling() == &aFirstContent) {
return true;
}
MOZ_DIAGNOSTIC_ASSERT(aFirstContent.GetParentNode() ==
aLastContent.GetParentNode());
if (mFirst->GetParentNode() != aFirstContent.GetParentNode()) {
return false;
}
const Maybe<uint32_t> newFirstIndex =
aFirstContent.ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(newFirstIndex.isSome());
const Maybe<uint32_t> newLastIndex =
&aFirstContent == &aLastContent ? newFirstIndex
: aLastContent.ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(newLastIndex.isSome());
const Maybe<uint32_t> currentFirstIndex = mFirst->ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(currentFirstIndex.isSome());
const Maybe<uint32_t> currentLastIndex =
mFirst == mLast ? currentFirstIndex : mLast->ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(currentLastIndex.isSome());
MOZ_ASSERT(!(newFirstIndex.value() < currentFirstIndex.value() &&
newLastIndex.value() > currentLastIndex.value()),
"New content nodes shouldn't contain mFirst nor mLast");
MOZ_ASSERT(!(newFirstIndex.value() < currentFirstIndex.value() &&
newLastIndex.value() > currentFirstIndex.value()),
"New content nodes shouldn't contain mFirst");
MOZ_ASSERT(!(newFirstIndex.value() < currentLastIndex.value() &&
newLastIndex.value() > currentLastIndex.value()),
"New content nodes shouldn't contain mLast");
return *newFirstIndex > *currentFirstIndex &&
*newLastIndex < *currentLastIndex;
}
bool IMEContentObserver::AddedContentCache::TryToCache(
const nsIContent& aFirstContent, const nsIContent& aLastContent,
const dom::Element* aRootElement) {
if (!HasCache()) {
mFirst = const_cast<nsIContent*>(&aFirstContent);
mLast = const_cast<nsIContent*>(&aLastContent);
MOZ_LOG(
sCacheLog, LogLevel::Info,
("AddedContentCache::TryToCache: Starting to cache the range: %s - %s",
ToString(mFirst).c_str(), ToString(mLast).c_str()));
return true;
}
MOZ_ASSERT(mFirst != &aFirstContent);
MOZ_ASSERT(mLast != &aLastContent);
if (aLastContent.GetNextSibling() == mFirst) {
MOZ_ASSERT(CanMergeWith(aFirstContent, aLastContent, aRootElement));
mFirst = const_cast<nsIContent*>(&aFirstContent);
MOZ_LOG(
sCacheLog, LogLevel::Info,
("AddedContentCache::TryToCache: Extending the range backward (to %s)",
ToString(mFirst).c_str()));
return true;
}
if (mLast->GetNextSibling() == &aFirstContent) {
MOZ_ASSERT(CanMergeWith(aFirstContent, aLastContent, aRootElement));
mLast = const_cast<nsIContent*>(&aLastContent);
MOZ_LOG(
sCacheLog, LogLevel::Info,
("AddedContentCache::TryToCache: Extending the range forward (to %s)",
ToString(mLast).c_str()));
return true;
}
MOZ_DIAGNOSTIC_ASSERT(aFirstContent.GetParentNode() ==
aLastContent.GetParentNode());
if (mFirst->GetParentNode() != aFirstContent.GetParentNode()) {
MOZ_ASSERT(!CanMergeWith(aFirstContent, aLastContent, aRootElement));
return false;
}
const Maybe<uint32_t> newFirstIndex =
aFirstContent.ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(newFirstIndex.isSome());
const Maybe<uint32_t> newLastIndex =
&aFirstContent == &aLastContent ? newFirstIndex
: aLastContent.ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(newLastIndex.isSome());
const Maybe<uint32_t> currentFirstIndex = mFirst->ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(currentFirstIndex.isSome());
const Maybe<uint32_t> currentLastIndex =
mFirst == mLast ? currentFirstIndex : mLast->ComputeIndexInParentNode();
MOZ_RELEASE_ASSERT(currentLastIndex.isSome());
MOZ_ASSERT(!(newFirstIndex.value() < currentFirstIndex.value() &&
newLastIndex.value() > currentLastIndex.value()),
"New content nodes shouldn't contain mFirst nor mLast");
MOZ_ASSERT(!(newFirstIndex.value() < currentFirstIndex.value() &&
newLastIndex.value() > currentFirstIndex.value()),
"New content nodes shouldn't contain mFirst");
MOZ_ASSERT(!(newFirstIndex.value() < currentLastIndex.value() &&
newLastIndex.value() > currentLastIndex.value()),
"New content nodes shouldn't contain mLast");
if (*newFirstIndex > *currentFirstIndex &&
*newLastIndex < *currentLastIndex) {
MOZ_ASSERT(CanMergeWith(aFirstContent, aLastContent, aRootElement));
MOZ_LOG(sCacheLog, LogLevel::Info,
("AddedContentCache::TryToCache: New nodes in the range"));
return true;
}
MOZ_ASSERT(!CanMergeWith(aFirstContent, aLastContent, aRootElement));
return false;
}
Result<std::pair<uint32_t, uint32_t>, nsresult> IMEContentObserver::
AddedContentCache::ComputeFlatTextRangeBeforeInsertingNewContent(
const nsIContent& aNewFirstContent, const nsIContent& aNewLastContent,
const dom::Element* aRootElement,
OffsetAndLengthAdjustments& aDifferences) const {
MOZ_ASSERT(HasCache());
const Maybe<int32_t> newLastContentComparedWithCachedFirstContent =
nsContentUtils::ComparePoints(
RawRangeBoundary(aNewLastContent.GetParentNode(),
aNewLastContent.GetPreviousSibling()),
RawRangeBoundary(mFirst->GetParentNode(),
mFirst->GetPreviousSibling()));
MOZ_RELEASE_ASSERT(newLastContentComparedWithCachedFirstContent.isSome());
MOZ_ASSERT(*newLastContentComparedWithCachedFirstContent != 0);
MOZ_ASSERT((*nsContentUtils::ComparePoints(
RawRangeBoundary(aNewFirstContent.GetParentNode(),
aNewFirstContent.GetPreviousSibling()),
RawRangeBoundary(mFirst->GetParentNode(),
mFirst->GetPreviousSibling())) > 0) ==
(*newLastContentComparedWithCachedFirstContent > 0),
"New nodes shouldn't contain mFirst");
const Maybe<int32_t> newFirstContentComparedWithCachedLastContent =
mLast->GetNextSibling() == &aNewFirstContent
? Some(1)
: nsContentUtils::ComparePoints(
RawRangeBoundary(aNewFirstContent.GetParentNode(),
aNewFirstContent.GetPreviousSibling()),
// aNewFirstContent and aNewLastContent may be descendants of
// mLast. Then, we need to ignore the new length. Therefore,
// we need to compare aNewFirstContent position with next
// sibling of mLast.
RawRangeBoundary(mLast->GetParentNode(), mLast));
MOZ_RELEASE_ASSERT(newFirstContentComparedWithCachedLastContent.isSome());
MOZ_ASSERT(*newFirstContentComparedWithCachedLastContent != 0);
MOZ_ASSERT((*newFirstContentComparedWithCachedLastContent > 0) ==
(*nsContentUtils::ComparePoints(
RawRangeBoundary(aNewLastContent.GetParentNode(),
aNewLastContent.GetPreviousSibling()),
RawRangeBoundary(mLast->GetParentNode(), mLast)) > 0),
"New nodes shouldn't contain mLast");
Result<uint32_t, nsresult> length =
FlatTextCache::ComputeTextLengthStartOfContentToEndOfContent(
*mFirst, *mLast, aRootElement);
if (NS_WARN_IF(length.isErr())) {
return length.propagateErr();
}
Result<uint32_t, nsresult> offset =
FlatTextCache::ComputeTextLengthBeforeContent(*mFirst, aRootElement);
if (NS_WARN_IF(offset.isErr())) {
return offset.propagateErr();
}
// If new content nodes are after the cached range, we can just ignore the
// new content nodes.
if (*newFirstContentComparedWithCachedLastContent == 1u) {
aDifferences = OffsetAndLengthAdjustments{0, 0};
return std::make_pair(offset.inspect(), length.inspect());
}
Result<uint32_t, nsresult> newLength =
FlatTextCache::ComputeTextLengthStartOfContentToEndOfContent(
aNewFirstContent, aNewLastContent, aRootElement);
if (NS_WARN_IF(newLength.isErr())) {
return newLength.propagateErr();
}
// If new content nodes are in the cached range, we need to subtract the new
// content length from cached content length.
if (*newLastContentComparedWithCachedFirstContent == 1u) {
MOZ_RELEASE_ASSERT(length.inspect() >= newLength.inspect());
aDifferences = OffsetAndLengthAdjustments{0, newLength.inspect()};
return std::make_pair(offset.inspect(),
length.inspect() - newLength.inspect());
}
// If new content nodes are before the cached range, we need to subtract the
// new content length from cached offset.
MOZ_RELEASE_ASSERT(offset.inspect() >= newLength.inspect());
aDifferences = OffsetAndLengthAdjustments{newLength.inspect(), 0};
return std::make_pair(offset.inspect() - newLength.inspect(),
length.inspect());
}
} // namespace mozilla