gecko-dev/dom/events/IMEContentObserver.cpp

1184 строки
37 KiB
C++
Исходник Обычный вид История

/* -*- 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/AsyncEventDispatcher.h"
#include "mozilla/AutoRestore.h"
#include "mozilla/EventStateManager.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/MouseEvents.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextEvents.h"
#include "mozilla/dom/Element.h"
#include "nsAutoPtr.h"
#include "nsContentUtils.h"
#include "nsGkAtoms.h"
#include "nsIAtom.h"
#include "nsIContent.h"
#include "nsIDocument.h"
#include "nsIDOMDocument.h"
#include "nsIDOMRange.h"
#include "nsIFrame.h"
#include "nsINode.h"
#include "nsIPresShell.h"
#include "nsISelectionController.h"
#include "nsISelectionPrivate.h"
#include "nsISupports.h"
#include "nsIWidget.h"
#include "nsPresContext.h"
#include "nsWeakReference.h"
#include "WritingModes.h"
namespace mozilla {
using namespace widget;
/******************************************************************************
* mozilla::IMEContentObserver
******************************************************************************/
NS_IMPL_CYCLE_COLLECTION_CLASS(IMEContentObserver)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(IMEContentObserver)
nsAutoScriptBlocker scriptBlocker;
tmp->NotifyIMEOfBlur();
tmp->UnregisterObservers();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mSelection)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRootContent)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditableNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mDocShell)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEditor)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mEndOfAddedTextCache.mContainerNode)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mStartOfRemovingTextRangeCache.mContainerNode)
tmp->mUpdatePreference.mWantUpdates = nsIMEUpdatePreference::NOTIFY_NOTHING;
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(mSelection)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRootContent)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditableNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mDocShell)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEditor)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mEndOfAddedTextCache.mContainerNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(
mStartOfRemovingTextRangeCache.mContainerNode)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMEContentObserver)
NS_INTERFACE_MAP_ENTRY(nsISelectionListener)
NS_INTERFACE_MAP_ENTRY(nsIMutationObserver)
NS_INTERFACE_MAP_ENTRY(nsIReflowObserver)
NS_INTERFACE_MAP_ENTRY(nsIScrollObserver)
NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
NS_INTERFACE_MAP_ENTRY(nsIEditorObserver)
NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsISelectionListener)
NS_INTERFACE_MAP_END
NS_IMPL_CYCLE_COLLECTING_ADDREF(IMEContentObserver)
NS_IMPL_CYCLE_COLLECTING_RELEASE(IMEContentObserver)
IMEContentObserver::IMEContentObserver()
: mESM(nullptr)
, mSuppressNotifications(0)
, mPreCharacterDataChangeLength(-1)
, mIsObserving(false)
, mIMEHasFocus(false)
, mIsFocusEventPending(false)
, mIsSelectionChangeEventPending(false)
, mSelectionChangeCausedOnlyByComposition(false)
, mSelectionChangeCausedOnlyBySelectionEvent(false)
, mIsPositionChangeEventPending(false)
, mIsFlushingPendingNotifications(false)
{
#ifdef DEBUG
mTextChangeData.Test();
#endif
}
void
IMEContentObserver::Init(nsIWidget* aWidget,
nsPresContext* aPresContext,
nsIContent* aContent,
nsIEditor* aEditor)
{
MOZ_ASSERT(aEditor, "aEditor must not be null");
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 members which may not be initialized again.
mRootContent = nullptr;
mEditor = nullptr;
mSelection = nullptr;
mDocShell = nullptr;
}
mESM = aPresContext->EventStateManager();
mESM->OnStartToObserveContent(this);
mWidget = aWidget;
mEditableNode =
IMEStateManager::GetRootEditableNode(aPresContext, aContent);
if (!mEditableNode) {
return;
}
mEditor = aEditor;
nsIPresShell* presShell = aPresContext->PresShell();
// get selection and root content
nsCOMPtr<nsISelectionController> selCon;
if (mEditableNode->IsNodeOfType(nsINode::eCONTENT)) {
nsIFrame* frame =
static_cast<nsIContent*>(mEditableNode.get())->GetPrimaryFrame();
NS_ENSURE_TRUE_VOID(frame);
frame->GetSelectionController(aPresContext,
getter_AddRefs(selCon));
} else {
// mEditableNode is a document
selCon = do_QueryInterface(presShell);
}
NS_ENSURE_TRUE_VOID(selCon);
selCon->GetSelection(nsISelectionController::SELECTION_NORMAL,
getter_AddRefs(mSelection));
NS_ENSURE_TRUE_VOID(mSelection);
nsCOMPtr<nsIDOMRange> selDomRange;
if (NS_SUCCEEDED(mSelection->GetRangeAt(0, getter_AddRefs(selDomRange)))) {
nsRange* selRange = static_cast<nsRange*>(selDomRange.get());
NS_ENSURE_TRUE_VOID(selRange && selRange->GetStartParent());
mRootContent = selRange->GetStartParent()->
GetSelectionRootContent(presShell);
} else {
mRootContent = mEditableNode->GetSelectionRootContent(presShell);
}
if (!mRootContent && mEditableNode->IsNodeOfType(nsINode::eDOCUMENT)) {
// The document node is editable, but there are no contents, this document
// is not editable.
return;
}
NS_ENSURE_TRUE_VOID(mRootContent);
if (firstInitialization) {
MaybeNotifyIMEOfFocusSet();
// 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 be occur. In such case, we
// shouldn't keep first initialization.
if (GetState() != eState_Initializing) {
return;
}
// NOTIFY_IME_OF_FOCUS might cause recreating IMEContentObserver
// instance via IMEStateManager::UpdateIMEState(). So, this
// instance might already have been destroyed, check it.
if (!mRootContent) {
return;
}
}
mDocShell = aPresContext->GetDocShell();
ObserveEditableNode();
// Some change events may wait to notify IME because this was being
// initialized. It is the time to flush them.
FlushMergeableNotifications();
}
void
IMEContentObserver::ObserveEditableNode()
{
MOZ_RELEASE_ASSERT(mEditor);
MOZ_RELEASE_ASSERT(mSelection);
MOZ_RELEASE_ASSERT(mRootContent);
MOZ_RELEASE_ASSERT(GetState() != eState_Observing);
mIsObserving = true;
mEditor->AddEditorObserver(this);
mUpdatePreference = mWidget->GetIMEUpdatePreference();
if (mUpdatePreference.WantSelectionChange()) {
// add selection change listener
nsCOMPtr<nsISelectionPrivate> selPrivate(do_QueryInterface(mSelection));
NS_ENSURE_TRUE_VOID(selPrivate);
nsresult rv = selPrivate->AddSelectionListener(this);
NS_ENSURE_SUCCESS_VOID(rv);
}
if (mUpdatePreference.WantTextChange()) {
// add text change observer
mRootContent->AddMutationObserver(this);
}
if (mUpdatePreference.WantPositionChanged() && 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);
// 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);
// 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);
}
void
IMEContentObserver::UnregisterObservers()
{
if (!mIsObserving) {
return;
}
mIsObserving = false;
if (mEditor) {
mEditor->RemoveEditorObserver(this);
}
if (mUpdatePreference.WantSelectionChange() && mSelection) {
nsCOMPtr<nsISelectionPrivate> selPrivate(do_QueryInterface(mSelection));
if (selPrivate) {
selPrivate->RemoveSelectionListener(this);
}
}
if (mUpdatePreference.WantTextChange() && mRootContent) {
mRootContent->RemoveMutationObserver(this);
}
if (mUpdatePreference.WantPositionChanged() && 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.
NotifyIMEOfBlur();
UnregisterObservers();
mEditor = nullptr;
mWidget = nullptr;
mSelection = nullptr;
mRootContent = nullptr;
mEditableNode = nullptr;
mDocShell = nullptr;
mUpdatePreference.mWantUpdates = nsIMEUpdatePreference::NOTIFY_NOTHING;
if (mESM) {
mESM->OnStopObservingContent(this);
mESM = nullptr;
}
}
void
IMEContentObserver::DisconnectFromEventStateManager()
{
mESM = nullptr;
}
bool
IMEContentObserver::MaybeReinitialize(nsIWidget* aWidget,
nsPresContext* aPresContext,
nsIContent* aContent,
nsIEditor* aEditor)
{
if (!IsObservingContent(aPresContext, aContent)) {
return false;
}
if (GetState() == eState_StoppedObserving) {
Init(aWidget, aPresContext, aContent, aEditor);
}
return IsManaging(aPresContext, aContent);
}
bool
IMEContentObserver::IsManaging(nsPresContext* aPresContext,
nsIContent* aContent)
{
return GetState() == eState_Observing &&
IsObservingContent(aPresContext, aContent);
}
IMEContentObserver::State
IMEContentObserver::GetState() const
{
if (!mSelection || !mRootContent || !mEditableNode) {
return eState_NotObserving; // failed to initialize or finalized.
}
if (!mRootContent->IsInComposedDoc()) {
// the focused editor has already been reframed.
return eState_StoppedObserving;
}
return mIsObserving ? eState_Observing : eState_Initializing;
}
bool
IMEContentObserver::IsObservingContent(nsPresContext* aPresContext,
nsIContent* aContent) const
{
return mEditableNode == IMEStateManager::GetRootEditableNode(aPresContext,
aContent);
}
bool
IMEContentObserver::IsEditorHandlingEventForComposition() const
{
if (!mWidget) {
return false;
}
nsRefPtr<TextComposition> composition =
IMEStateManager::GetTextCompositionFor(mWidget);
if (!composition) {
return false;
}
return composition->IsEditorHandlingEvent();
}
nsresult
IMEContentObserver::GetSelectionAndRoot(nsISelection** aSelection,
nsIContent** aRootContent) const
{
if (!mEditableNode || !mSelection) {
return NS_ERROR_NOT_AVAILABLE;
}
NS_ASSERTION(mSelection && mRootContent, "uninitialized content observer");
NS_ADDREF(*aSelection = mSelection);
NS_ADDREF(*aRootContent = mRootContent);
return NS_OK;
}
nsresult
IMEContentObserver::NotifySelectionChanged(nsIDOMDocument* aDOMDocument,
nsISelection* aSelection,
int16_t aReason)
{
int32_t count = 0;
nsresult rv = aSelection->GetRangeCount(&count);
NS_ENSURE_SUCCESS(rv, rv);
if (count > 0 && mWidget) {
bool causedByComposition = IsEditorHandlingEventForComposition();
bool causedBySelectionEvent = TextComposition::IsHandlingSelectionEvent();
MaybeNotifyIMEOfSelectionChange(causedByComposition,
causedBySelectionEvent);
}
return NS_OK;
}
void
IMEContentObserver::ScrollPositionChanged()
{
MaybeNotifyIMEOfPositionChange();
}
NS_IMETHODIMP
IMEContentObserver::Reflow(DOMHighResTimeStamp aStart,
DOMHighResTimeStamp aEnd)
{
MaybeNotifyIMEOfPositionChange();
return NS_OK;
}
NS_IMETHODIMP
IMEContentObserver::ReflowInterruptible(DOMHighResTimeStamp aStart,
DOMHighResTimeStamp aEnd)
{
MaybeNotifyIMEOfPositionChange();
return NS_OK;
}
bool
IMEContentObserver::OnMouseButtonEvent(nsPresContext* aPresContext,
WidgetMouseEvent* aMouseEvent)
{
if (!mUpdatePreference.WantMouseButtonEventOnChar()) {
return false;
}
if (!aMouseEvent->mFlags.mIsTrusted ||
aMouseEvent->mFlags.mDefaultPrevented ||
!aMouseEvent->widget) {
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;
}
nsRefPtr<IMEContentObserver> kungFuDeathGrip(this);
WidgetQueryContentEvent charAtPt(true, NS_QUERY_CHARACTER_AT_POINT,
aMouseEvent->widget);
charAtPt.refPoint = aMouseEvent->refPoint;
ContentEventHandler handler(aPresContext);
handler.OnQueryCharacterAtPoint(&charAtPt);
if (NS_WARN_IF(!charAtPt.mSucceeded) ||
charAtPt.mReply.mOffset == WidgetQueryContentEvent::NOT_FOUND) {
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) {
charAtPt.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->widget != mWidget) {
charAtPt.refPoint += aMouseEvent->widget->WidgetToScreenOffset() -
mWidget->WidgetToScreenOffset();
}
IMENotification notification(NOTIFY_IME_OF_MOUSE_BUTTON_EVENT);
notification.mMouseButtonEventData.mEventMessage = aMouseEvent->mMessage;
notification.mMouseButtonEventData.mOffset = charAtPt.mReply.mOffset;
notification.mMouseButtonEventData.mCursorPos.Set(
LayoutDeviceIntPoint::ToUntyped(charAtPt.refPoint));
notification.mMouseButtonEventData.mCharRect.Set(
LayoutDevicePixel::ToUntyped(charAtPt.mReply.mRect));
notification.mMouseButtonEventData.mButton = aMouseEvent->button;
notification.mMouseButtonEventData.mButtons = aMouseEvent->buttons;
notification.mMouseButtonEventData.mModifiers = aMouseEvent->modifiers;
nsresult rv = IMEStateManager::NotifyIME(notification, mWidget);
if (NS_WARN_IF(NS_FAILED(rv))) {
return false;
}
bool consumed = (rv == NS_SUCCESS_EVENT_CONSUMED);
aMouseEvent->mFlags.mDefaultPrevented = consumed;
return consumed;
}
void
IMEContentObserver::CharacterDataWillChange(nsIDocument* aDocument,
nsIContent* aContent,
CharacterDataChangeInfo* aInfo)
{
NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT),
"character data changed for non-text node");
MOZ_ASSERT(mPreCharacterDataChangeLength < 0,
"CharacterDataChanged() should've reset "
"mPreCharacterDataChangeLength");
mEndOfAddedTextCache.Clear();
mStartOfRemovingTextRangeCache.Clear();
bool causedByComposition = IsEditorHandlingEventForComposition();
if (!mTextChangeData.IsValid() && causedByComposition &&
!mUpdatePreference.WantChangesCausedByComposition()) {
return;
}
mPreCharacterDataChangeLength =
ContentEventHandler::GetNativeTextLength(aContent, 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(nsIDocument* aDocument,
nsIContent* aContent,
CharacterDataChangeInfo* aInfo)
{
NS_ASSERTION(aContent->IsNodeOfType(nsINode::eTEXT),
"character data changed for non-text node");
mEndOfAddedTextCache.Clear();
mStartOfRemovingTextRangeCache.Clear();
int64_t removedLength = mPreCharacterDataChangeLength;
mPreCharacterDataChangeLength = -1;
bool causedByComposition = IsEditorHandlingEventForComposition();
if (!mTextChangeData.IsValid() && causedByComposition &&
!mUpdatePreference.WantChangesCausedByComposition()) {
return;
}
MOZ_ASSERT(removedLength >= 0,
"mPreCharacterDataChangeLength should've been set by "
"CharacterDataWillChange()");
uint32_t offset = 0;
// get offsets of change and fire notification
nsresult rv =
ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, aContent,
aInfo->mChangeStart,
&offset,
LINE_BREAK_TYPE_NATIVE);
NS_ENSURE_SUCCESS_VOID(rv);
uint32_t newLength =
ContentEventHandler::GetNativeTextLength(aContent, 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, causedByComposition);
MaybeNotifyIMEOfTextChange(data);
}
void
IMEContentObserver::NotifyContentAdded(nsINode* aContainer,
int32_t aStartIndex,
int32_t aEndIndex)
{
mStartOfRemovingTextRangeCache.Clear();
bool causedByComposition = IsEditorHandlingEventForComposition();
if (!mTextChangeData.IsValid() && causedByComposition &&
!mUpdatePreference.WantChangesCausedByComposition()) {
return;
}
uint32_t offset = 0;
nsresult rv = NS_OK;
if (!mEndOfAddedTextCache.Match(aContainer, aStartIndex)) {
mEndOfAddedTextCache.Clear();
rv = ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, aContainer,
aStartIndex, &offset,
LINE_BREAK_TYPE_NATIVE);
if (NS_WARN_IF(NS_FAILED((rv)))) {
return;
}
} else {
offset = mEndOfAddedTextCache.mFlatTextLength;
}
// get offset at the end of the last added node
nsIContent* childAtStart = aContainer->GetChildAt(aStartIndex);
uint32_t addingLength = 0;
rv = ContentEventHandler::GetFlatTextOffsetOfRange(childAtStart, aContainer,
aEndIndex, &addingLength,
LINE_BREAK_TYPE_NATIVE);
if (NS_WARN_IF(NS_FAILED((rv)))) {
mEndOfAddedTextCache.Clear();
return;
}
// 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.Cache(aContainer, aEndIndex, offset + addingLength);
if (!addingLength) {
return;
}
TextChangeData data(offset, offset, offset + addingLength,
causedByComposition);
MaybeNotifyIMEOfTextChange(data);
}
void
IMEContentObserver::ContentAppended(nsIDocument* aDocument,
nsIContent* aContainer,
nsIContent* aFirstNewContent,
int32_t aNewIndexInContainer)
{
NotifyContentAdded(aContainer, aNewIndexInContainer,
aContainer->GetChildCount());
}
void
IMEContentObserver::ContentInserted(nsIDocument* aDocument,
nsIContent* aContainer,
nsIContent* aChild,
int32_t aIndexInContainer)
{
NotifyContentAdded(NODE_FROM(aContainer, aDocument),
aIndexInContainer, aIndexInContainer + 1);
}
void
IMEContentObserver::ContentRemoved(nsIDocument* aDocument,
nsIContent* aContainer,
nsIContent* aChild,
int32_t aIndexInContainer,
nsIContent* aPreviousSibling)
{
mEndOfAddedTextCache.Clear();
bool causedByComposition = IsEditorHandlingEventForComposition();
if (!mTextChangeData.IsValid() && causedByComposition &&
!mUpdatePreference.WantChangesCausedByComposition()) {
return;
}
nsINode* containerNode = NODE_FROM(aContainer, aDocument);
uint32_t offset = 0;
nsresult rv = NS_OK;
if (!mStartOfRemovingTextRangeCache.Match(containerNode, aIndexInContainer)) {
rv =
ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, containerNode,
aIndexInContainer, &offset,
LINE_BREAK_TYPE_NATIVE);
if (NS_WARN_IF(NS_FAILED(rv))) {
mStartOfRemovingTextRangeCache.Clear();
return;
}
mStartOfRemovingTextRangeCache.Cache(containerNode, aIndexInContainer,
offset);
} else {
offset = mStartOfRemovingTextRangeCache.mFlatTextLength;
}
// get offset at the end of the deleted node
int32_t nodeLength =
aChild->IsNodeOfType(nsINode::eTEXT) ?
static_cast<int32_t>(aChild->TextLength()) :
std::max(static_cast<int32_t>(aChild->GetChildCount()), 1);
MOZ_ASSERT(nodeLength >= 0, "The node length is out of range");
uint32_t textLength = 0;
rv = ContentEventHandler::GetFlatTextOffsetOfRange(aChild, aChild,
nodeLength, &textLength,
LINE_BREAK_TYPE_NATIVE);
if (NS_WARN_IF(NS_FAILED(rv))) {
mStartOfRemovingTextRangeCache.Clear();
return;
}
if (!textLength) {
return;
}
TextChangeData data(offset, offset + textLength, offset, causedByComposition);
MaybeNotifyIMEOfTextChange(data);
}
static nsIContent*
GetContentBR(dom::Element* aElement)
{
if (!aElement->IsNodeOfType(nsINode::eCONTENT)) {
return nullptr;
}
nsIContent* content = static_cast<nsIContent*>(aElement);
return content->IsHTMLElement(nsGkAtoms::br) ? content : nullptr;
}
void
IMEContentObserver::AttributeWillChange(nsIDocument* aDocument,
dom::Element* aElement,
int32_t aNameSpaceID,
nsIAtom* aAttribute,
int32_t aModType,
const nsAttrValue* aNewValue)
{
nsIContent *content = GetContentBR(aElement);
mPreAttrChangeLength = content ?
ContentEventHandler::GetNativeTextLength(content) : 0;
}
void
IMEContentObserver::AttributeChanged(nsIDocument* aDocument,
dom::Element* aElement,
int32_t aNameSpaceID,
nsIAtom* aAttribute,
int32_t aModType,
const nsAttrValue* aOldValue)
{
mEndOfAddedTextCache.Clear();
mStartOfRemovingTextRangeCache.Clear();
bool causedByComposition = IsEditorHandlingEventForComposition();
if (!mTextChangeData.IsValid() && causedByComposition &&
!mUpdatePreference.WantChangesCausedByComposition()) {
return;
}
nsIContent *content = GetContentBR(aElement);
if (!content) {
return;
}
uint32_t postAttrChangeLength =
ContentEventHandler::GetNativeTextLength(content);
if (postAttrChangeLength == mPreAttrChangeLength) {
return;
}
uint32_t start;
nsresult rv =
ContentEventHandler::GetFlatTextOffsetOfRange(mRootContent, content,
0, &start,
LINE_BREAK_TYPE_NATIVE);
NS_ENSURE_SUCCESS_VOID(rv);
TextChangeData data(start, start + mPreAttrChangeLength,
start + postAttrChangeLength, causedByComposition);
MaybeNotifyIMEOfTextChange(data);
}
NS_IMETHODIMP
IMEContentObserver::EditAction()
{
mEndOfAddedTextCache.Clear();
mStartOfRemovingTextRangeCache.Clear();
FlushMergeableNotifications();
return NS_OK;
}
NS_IMETHODIMP
IMEContentObserver::BeforeEditAction()
{
mEndOfAddedTextCache.Clear();
mStartOfRemovingTextRangeCache.Clear();
return NS_OK;
}
NS_IMETHODIMP
IMEContentObserver::CancelEditAction()
{
mEndOfAddedTextCache.Clear();
mStartOfRemovingTextRangeCache.Clear();
FlushMergeableNotifications();
return NS_OK;
}
void
IMEContentObserver::PostFocusSetNotification()
{
mIsFocusEventPending = true;
}
void
IMEContentObserver::PostTextChangeNotification(
const TextChangeDataBase& aTextChangeData)
{
mTextChangeData += aTextChangeData;
MOZ_ASSERT(mTextChangeData.IsValid(),
"mTextChangeData must have text change data");
}
void
IMEContentObserver::PostSelectionChangeNotification(
bool aCausedByComposition,
bool aCausedBySelectionEvent)
{
if (!mIsSelectionChangeEventPending) {
mSelectionChangeCausedOnlyByComposition = aCausedByComposition;
} else {
mSelectionChangeCausedOnlyByComposition =
mSelectionChangeCausedOnlyByComposition && aCausedByComposition;
}
if (!mSelectionChangeCausedOnlyBySelectionEvent) {
mSelectionChangeCausedOnlyBySelectionEvent = aCausedBySelectionEvent;
} else {
mSelectionChangeCausedOnlyBySelectionEvent =
mSelectionChangeCausedOnlyBySelectionEvent && aCausedBySelectionEvent;
}
mIsSelectionChangeEventPending = true;
}
bool
IMEContentObserver::UpdateSelectionCache()
{
MOZ_ASSERT(IsSafeToNotifyIME());
if (!mUpdatePreference.WantSelectionChange()) {
return false;
}
mSelectionData.Clear();
// XXX Cannot we cache some information for reducing the cost to compute
// selection offset and writing mode?
WidgetQueryContentEvent selection(true, NS_QUERY_SELECTED_TEXT, mWidget);
ContentEventHandler handler(GetPresContext());
handler.OnQuerySelectedText(&selection);
if (NS_WARN_IF(!selection.mSucceeded)) {
return false;
}
mSelectionData.mOffset = selection.mReply.mOffset;
*mSelectionData.mString = selection.mReply.mString;
mSelectionData.SetWritingMode(selection.GetWritingMode());
mSelectionData.mReversed = selection.mReply.mReversed;
mSelectionData.mCausedByComposition = false;
mSelectionData.mCausedBySelectionEvent = false;
return mSelectionData.IsValid();
}
void
IMEContentObserver::PostPositionChangeNotification()
{
mIsPositionChangeEventPending = true;
}
bool
IMEContentObserver::IsReflowLocked() const
{
nsPresContext* presContext = GetPresContext();
if (NS_WARN_IF(!presContext)) {
return false;
}
nsIPresShell* 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) {
return false;
}
// Don't notify IME of anything if it's not good time to do it.
if (mSuppressNotifications) {
return false;
}
if (!mESM || NS_WARN_IF(!GetPresContext())) {
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()) {
return false;
}
// If we're in handling an edit action, this method will be called later.
bool isInEditAction = false;
if (mEditor && NS_SUCCEEDED(mEditor->GetIsInEditAction(&isInEditAction)) &&
isInEditAction) {
return false;
}
return true;
}
void
IMEContentObserver::FlushMergeableNotifications()
{
if (!IsSafeToNotifyIME()) {
// So, if this is already called, this should do nothing.
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 (mIsFlushingPendingNotifications) {
// So, if this is already called, this should do nothing.
return;
}
AutoRestore<bool> flusing(mIsFlushingPendingNotifications);
mIsFlushingPendingNotifications = true;
// NOTE: Reset each pending flag because sending notification may cause
// another change.
if (mIsFocusEventPending) {
mIsFocusEventPending = false;
nsContentUtils::AddScriptRunner(new FocusSetEvent(this));
// This is the first notification to IME. So, we don't need to notify any
// more since IME starts to query content after it gets focus.
ClearPendingNotifications();
return;
}
if (mTextChangeData.IsValid()) {
nsContentUtils::AddScriptRunner(new TextChangeEvent(this, mTextChangeData));
}
// 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 (mIsSelectionChangeEventPending) {
mIsSelectionChangeEventPending = false;
nsContentUtils::AddScriptRunner(
new SelectionChangeEvent(this, mSelectionChangeCausedOnlyByComposition,
mSelectionChangeCausedOnlyBySelectionEvent));
}
if (mIsPositionChangeEventPending) {
mIsPositionChangeEventPending = false;
nsContentUtils::AddScriptRunner(new PositionChangeEvent(this));
}
// If notifications may cause new change, we should notify them now.
if (mTextChangeData.IsValid() ||
mIsSelectionChangeEventPending ||
mIsPositionChangeEventPending) {
nsRefPtr<AsyncMergeableNotificationsFlusher> asyncFlusher =
new AsyncMergeableNotificationsFlusher(this);
NS_DispatchToCurrentThread(asyncFlusher);
}
}
/******************************************************************************
* mozilla::IMEContentObserver::AChangeEvent
******************************************************************************/
bool
IMEContentObserver::AChangeEvent::CanNotifyIME() const
{
if (NS_WARN_IF(!mIMEContentObserver)) {
return false;
}
State state = mIMEContentObserver->GetState();
// If it's not initialized, we should do nothing.
if (state == eState_NotObserving) {
return false;
}
// If setting focus, just check the state.
if (mChangeEventType == eChangeEventType_Focus) {
return !NS_WARN_IF(mIMEContentObserver->mIMEHasFocus);
}
// If we've not notified IME of focus yet, we shouldn't notify anything.
if (!mIMEContentObserver->mIMEHasFocus) {
return false;
}
// If IME has focus, IMEContentObserver must hold the widget.
MOZ_ASSERT(mIMEContentObserver->mWidget);
return true;
}
bool
IMEContentObserver::AChangeEvent::IsSafeToNotifyIME() const
{
if (NS_WARN_IF(!nsContentUtils::IsSafeToRunScript())) {
return false;
}
State state = mIMEContentObserver->GetState();
if (mChangeEventType == eChangeEventType_Focus) {
if (NS_WARN_IF(state != eState_Initializing && state != eState_Observing)) {
return false;
}
} else if (state != eState_Observing) {
return false;
}
return mIMEContentObserver->IsSafeToNotifyIME();
}
/******************************************************************************
* mozilla::IMEContentObserver::FocusSetEvent
******************************************************************************/
NS_IMETHODIMP
IMEContentObserver::FocusSetEvent::Run()
{
if (!CanNotifyIME()) {
// If IMEContentObserver has already gone, we don't need to notify IME of
// focus.
mIMEContentObserver->ClearPendingNotifications();
return NS_OK;
}
if (!IsSafeToNotifyIME()) {
mIMEContentObserver->PostFocusSetNotification();
return NS_OK;
}
mIMEContentObserver->mIMEHasFocus = true;
// Initialize selection cache with the first selection data.
mIMEContentObserver->UpdateSelectionCache();
IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_FOCUS),
mIMEContentObserver->mWidget);
return NS_OK;
}
/******************************************************************************
* mozilla::IMEContentObserver::SelectionChangeEvent
******************************************************************************/
NS_IMETHODIMP
IMEContentObserver::SelectionChangeEvent::Run()
{
if (!CanNotifyIME()) {
return NS_OK;
}
if (!IsSafeToNotifyIME()) {
mIMEContentObserver->PostSelectionChangeNotification(
mCausedByComposition, mCausedBySelectionEvent);
return NS_OK;
}
SelectionChangeData lastSelChangeData = mIMEContentObserver->mSelectionData;
if (NS_WARN_IF(!mIMEContentObserver->UpdateSelectionCache())) {
return NS_OK;
}
// If the IME doesn't want selection change notifications caused by
// composition, we should do nothing anymore.
if (mCausedByComposition &&
!mIMEContentObserver->
mUpdatePreference.WantChangesCausedByComposition()) {
return NS_OK;
}
// The state may be changed since querying content causes flushing layout.
if (!CanNotifyIME()) {
return NS_OK;
}
// If the selection isn't changed actually, we shouldn't notify IME of
// selection change.
SelectionChangeData& newSelChangeData = mIMEContentObserver->mSelectionData;
if (lastSelChangeData.IsValid() &&
lastSelChangeData.mOffset == newSelChangeData.mOffset &&
lastSelChangeData.String() == newSelChangeData.String() &&
lastSelChangeData.GetWritingMode() == newSelChangeData.GetWritingMode() &&
lastSelChangeData.mReversed == newSelChangeData.mReversed) {
return NS_OK;
}
IMENotification notification(NOTIFY_IME_OF_SELECTION_CHANGE);
notification.SetData(mIMEContentObserver->mSelectionData,
mCausedByComposition, mCausedBySelectionEvent);
IMEStateManager::NotifyIME(notification, mIMEContentObserver->mWidget);
return NS_OK;
}
/******************************************************************************
* mozilla::IMEContentObserver::TextChangeEvent
******************************************************************************/
NS_IMETHODIMP
IMEContentObserver::TextChangeEvent::Run()
{
if (!CanNotifyIME()) {
return NS_OK;
}
if (!IsSafeToNotifyIME()) {
mIMEContentObserver->PostTextChangeNotification(mTextChangeData);
return NS_OK;
}
IMENotification notification(NOTIFY_IME_OF_TEXT_CHANGE);
notification.SetData(mTextChangeData);
IMEStateManager::NotifyIME(notification, mIMEContentObserver->mWidget);
return NS_OK;
}
/******************************************************************************
* mozilla::IMEContentObserver::PositionChangeEvent
******************************************************************************/
NS_IMETHODIMP
IMEContentObserver::PositionChangeEvent::Run()
{
if (!CanNotifyIME()) {
return NS_OK;
}
if (!IsSafeToNotifyIME()) {
mIMEContentObserver->PostPositionChangeNotification();
return NS_OK;
}
IMEStateManager::NotifyIME(IMENotification(NOTIFY_IME_OF_POSITION_CHANGE),
mIMEContentObserver->mWidget);
return NS_OK;
}
/******************************************************************************
* mozilla::IMEContentObserver::AsyncMergeableNotificationsFlusher
******************************************************************************/
NS_IMETHODIMP
IMEContentObserver::AsyncMergeableNotificationsFlusher::Run()
{
if (!CanNotifyIME()) {
return NS_OK;
}
mIMEContentObserver->FlushMergeableNotifications();
return NS_OK;
}
} // namespace mozilla