Bug 1588745 - part 6: Post processing of setting value with TextEditor should be done before dispatching "input" event r=Ehsan

Currently, "input" event is fired when the `AutoScriptBlocker` in `SetValue()`
is deleted.  So, for keeping same behavior, the post processing after calling
`TextEditor` methods should be done before editor dispatches "input" event.

Fortunately, `TextInputListener::OnEditActionHandled()` is a good chance to
do that.  Therefore, this patch makes it notify `TextControlState` and
`AutoTextControlHandlingState`.

Note that ideally, each method of `TextEditor` should return
`NS_ERROR_OUT_OF_MEMORY` coming from
`AutoTextControlHandlingState::OnEditActionHandled()`.  However, it requires
a lot of changes in editor classes, and the case is really rare since editor
does not use fallible allocation.  Therefore, it must be okay to crash in
editor if `OnEditActionHandled()` returns `NS_ERROR_OUT_OF_MEMORY`.

Depends on D51395

Differential Revision: https://phabricator.services.mozilla.com/D51396
This commit is contained in:
Masayuki Nakano
2019-11-03 02:50:00 +00:00
parent c2dc961e9a
commit 39166981ab
8 changed files with 233 additions and 128 deletions

View File

@@ -796,6 +796,8 @@ nsresult TextInputSelectionController::CheckVisibilityContent(
TextInputListener::TextInputListener(nsITextControlElement* aTxtCtrlElement)
: mFrame(nullptr),
mTxtCtrlElement(aTxtCtrlElement),
mTextControlState(aTxtCtrlElement ? aTxtCtrlElement->GetTextControlState()
: nullptr),
mSelectionWasCollapsed(true),
mHadUndoItems(false),
mHadRedoItems(false),
@@ -985,40 +987,33 @@ TextInputListener::HandleEvent(Event* aEvent) {
return NS_OK;
}
void TextInputListener::OnEditActionHandled() {
if (!mFrame) {
// We've been disconnected from the TextControlState object, nothing to do
// here.
return;
nsresult TextInputListener::OnEditActionHandled(TextEditor& aTextEditor) {
if (mFrame) {
AutoWeakFrame weakFrame = mFrame;
nsITextControlFrame* frameBase = do_QueryFrame(mFrame);
nsTextControlFrame* frame = static_cast<nsTextControlFrame*>(frameBase);
NS_ASSERTION(frame, "Where is our frame?");
//
// Update the undo / redo menus
//
size_t numUndoItems = aTextEditor.NumberOfUndoItems();
size_t numRedoItems = aTextEditor.NumberOfRedoItems();
if ((numUndoItems && !mHadUndoItems) || (!numUndoItems && mHadUndoItems) ||
(numRedoItems && !mHadRedoItems) || (!numRedoItems && mHadRedoItems)) {
// Modify the menu if undo or redo items are different
UpdateTextInputCommands(NS_LITERAL_STRING("undo"));
mHadUndoItems = numUndoItems != 0;
mHadRedoItems = numRedoItems != 0;
}
if (weakFrame.IsAlive()) {
HandleValueChanged(frame);
}
}
AutoWeakFrame weakFrame = mFrame;
nsITextControlFrame* frameBase = do_QueryFrame(mFrame);
nsTextControlFrame* frame = static_cast<nsTextControlFrame*>(frameBase);
NS_ASSERTION(frame, "Where is our frame?");
//
// Update the undo / redo menus
//
RefPtr<TextEditor> textEditor = frame->GetTextEditor();
// Get the number of undo / redo items
size_t numUndoItems = textEditor->NumberOfUndoItems();
size_t numRedoItems = textEditor->NumberOfRedoItems();
if ((numUndoItems && !mHadUndoItems) || (!numUndoItems && mHadUndoItems) ||
(numRedoItems && !mHadRedoItems) || (!numRedoItems && mHadRedoItems)) {
// Modify the menu if undo or redo items are different
UpdateTextInputCommands(NS_LITERAL_STRING("undo"));
mHadUndoItems = numUndoItems != 0;
mHadRedoItems = numRedoItems != 0;
}
if (!weakFrame.IsAlive()) {
return;
}
HandleValueChanged(frame);
return mTextControlState ? mTextControlState->OnEditActionHandled() : NS_OK;
}
void TextInputListener::HandleValueChanged(nsTextControlFrame* aFrame) {
@@ -1155,9 +1150,100 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
}
}
/**
* WillSetValueWithTextEditor() is called when TextControlState sets
* value with its mTextEditor.
*/
void WillSetValueWithTextEditor() {
MOZ_ASSERT(Is(TextControlAction::SetValue));
MOZ_ASSERT(mTextControlState.mBoundFrame);
mTextControlFrame = mTextControlState.mBoundFrame;
// If we'reemulating user input, we don't need to manage mTextInputListener
// by ourselves since everything should be handled by TextEditor as normal
// user input.
if (mSetValueFlags & TextControlState::eSetValue_BySetUserInput) {
return;
}
// Otherwise, if we're setting the value programatically, we need to manage
// mTextInputListener by ourselves since TextEditor users special path
// for the performance.
mTextInputListener->SettingValue(true);
mTextInputListener->SetValueChanged(mSetValueFlags &
TextControlState::eSetValue_Notify);
mEditActionHandled = false;
}
/**
* OnEditActionHandled() is called when the TextEditor handles something
* and immediately before dispatching "input" event.
*/
MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE nsresult OnEditActionHandled() {
MOZ_ASSERT(!mEditActionHandled);
mEditActionHandled = true;
if (!Is(TextControlAction::SetValue)) {
return NS_OK;
}
if (!(mSetValueFlags & TextControlState::eSetValue_BySetUserInput)) {
mTextInputListener->SetValueChanged(true);
mTextInputListener->SettingValue(
mParent && mParent->IsHandling(TextControlAction::SetValue));
if (!(mSetValueFlags & TextControlState::eSetValue_Notify)) {
// Listener doesn't update frame, but it is required for
// placeholder
mTextControlState.ValueWasChanged(true);
}
}
if (!IsOriginalTextControlFrameAlive()) {
return SetValueWithoutTextEditorAgain() ? NS_OK : NS_ERROR_OUT_OF_MEMORY;
}
// The new value never includes line breaks caused by hard-wrap.
// So, mCachedValue can always cache the new value.
nsITextControlFrame* textControlFrame =
do_QueryFrame(mTextControlFrame.GetFrame());
return static_cast<nsTextControlFrame*>(textControlFrame)
->CacheValue(mSettingValue, fallible)
? NS_OK
: NS_ERROR_OUT_OF_MEMORY;
}
/**
* SetValueWithoutTextEditorAgain() should be called if the frame for
* mTextControlState was destroyed during setting value.
*/
MOZ_CAN_RUN_SCRIPT MOZ_MUST_USE bool SetValueWithoutTextEditorAgain() {
MOZ_ASSERT(!IsOriginalTextControlFrameAlive());
// If the frame was destroyed because of a flush somewhere inside
// TextEditor, mBoundFrame here will be nullptr. But it's also
// possible for the frame to go away because of another reason (such
// as deleting the existing selection -- see bug 574558), in which
// case we don't need to reset the value here.
if (mTextControlState.mBoundFrame) {
return true;
}
// XXX It's odd to drop flags except eSetValue_Notify. Probably, this
// intended to drop eSetValue_BySetUserInput and eSetValue_ByContent,
// but other flags are added later.
ErrorResult error;
AutoTextControlHandlingState handlingSetValueWithoutEditor(
mTextControlState, TextControlAction::SetValue, mSettingValue,
mOldValue, mSetValueFlags & TextControlState::eSetValue_Notify, error);
if (error.Failed()) {
MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY));
error.SuppressException();
return false;
}
return mTextControlState.SetValueWithoutTextEditor(
handlingSetValueWithoutEditor);
}
bool IsTextControlStateDestroyed() const {
return mTextControlStateDestroyed;
}
bool IsOriginalTextControlFrameAlive() const {
return const_cast<AutoTextControlHandlingState*>(this)
->mTextControlFrame.IsAlive();
}
bool HasEditActionHandled() const { return mEditActionHandled; }
bool Is(TextControlAction aTextControlAction) const {
return mTextControlAction == aTextControlAction;
}
@@ -1206,6 +1292,10 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
AutoTextControlHandlingState* const mParent;
TextControlState& mTextControlState;
// mTextControlFrame should be set immediately before calling methods
// which may destroy the frame. Then, you can check whether the frame
// was destroyed/replaced.
AutoWeakFrame mTextControlFrame;
// mTextCtrlElement grabs TextControlState::mTextCtrlElement since
// if the text control element releases mTextControlState, only this
// can guarantee the instance of the text control element.
@@ -1218,6 +1308,7 @@ class MOZ_STACK_CLASS AutoTextControlHandlingState {
uint32_t mSetValueFlags = 0;
TextControlAction const mTextControlAction;
bool mTextControlStateDestroyed = false;
bool mEditActionHandled = false;
};
/*****************************************************************************
@@ -1304,6 +1395,10 @@ void TextControlState::DeleteOrCacheForReuse() {
delete this;
}
nsresult TextControlState::OnEditActionHandled() {
return mHandlingState ? mHandlingState->OnEditActionHandled() : NS_OK;
}
Element* TextControlState::GetRootNode() {
return mBoundFrame ? mBoundFrame->GetRootNode() : nullptr;
}
@@ -2525,38 +2620,12 @@ bool TextControlState::SetValue(const nsAString& aValue,
AutoWeakFrame weakFrame(mBoundFrame);
SetValueWithTextEditor(handlingSetValue);
if (!weakFrame.IsAlive()) {
// If the frame was destroyed because of a flush somewhere inside
// InsertText, mBoundFrame here will be nullptr. But it's also
// possible for the frame to go away because of another reason (such
// as deleting the existing selection -- see bug 574558), in which
// case we don't need to reset the value here.
if (mBoundFrame) {
return true;
}
// XXX It's odd to drop flags except eSetValue_Notify. Probably, this
// intended to drop eSetValue_BySetUserInput and eSetValue_ByContent,
// but other flags are added later.
ErrorResult error;
AutoTextControlHandlingState handlingSetValueWithoutEditor(
*this, TextControlAction::SetValue,
handlingSetValue.GetSettingValue(), handlingSetValue.GetOldValue(),
handlingSetValue.GetSetValueFlags() & eSetValue_Notify, error);
if (error.Failed()) {
MOZ_ASSERT(error.ErrorCodeIs(NS_ERROR_OUT_OF_MEMORY));
error.SuppressException();
return false;
}
return SetValueWithoutTextEditor(handlingSetValueWithoutEditor);
if (!SetValueWithTextEditor(handlingSetValue)) {
return false;
}
// The new value never includes line breaks caused by hard-wrap.
// So, mCachedValue can always cache the new value.
if (!mBoundFrame->CacheValue(handlingSetValue.GetSettingValue(),
fallible)) {
return false;
if (!weakFrame.IsAlive()) {
return true;
}
} else if (!SetValueWithoutTextEditor(handlingSetValue)) {
return false;
@@ -2574,7 +2643,7 @@ bool TextControlState::SetValue(const nsAString& aValue,
return true;
}
void TextControlState::SetValueWithTextEditor(
bool TextControlState::SetValueWithTextEditor(
AutoTextControlHandlingState& aHandlingSetValue) {
MOZ_ASSERT(aHandlingSetValue.Is(TextControlAction::SetValue));
MOZ_ASSERT(mTextEditor);
@@ -2603,18 +2672,16 @@ void TextControlState::SetValueWithTextEditor(
mBoundFrame->GetText(currentValue);
}
AutoWeakFrame weakFrame(mBoundFrame);
// this is necessary to avoid infinite recursion
if (currentValue == aHandlingSetValue.GetSettingValue()) {
return;
return true;
}
RefPtr<TextEditor> textEditor = mTextEditor;
nsCOMPtr<Document> document = textEditor->GetDocument();
if (NS_WARN_IF(!document)) {
return;
return true;
}
// Time to mess with our security context... See comments in GetValue()
@@ -2627,14 +2694,12 @@ void TextControlState::SetValueWithTextEditor(
Selection* selection = mSelCon->GetSelection(SelectionType::eNormal);
SelectionBatcher selectionBatcher(selection);
if (NS_WARN_IF(!weakFrame.IsAlive())) {
return;
}
// get the flags, remove readonly, disabled and max-length,
// set the value, restore flags
AutoRestoreEditorState restoreState(textEditor);
aHandlingSetValue.WillSetValueWithTextEditor();
if (aHandlingSetValue.GetSetValueFlags() & eSetValue_BySetUserInput) {
// If the caller inserts text as part of user input, for example,
// autocomplete, we need to replace the text as "insert string"
@@ -2646,22 +2711,15 @@ void TextControlState::SetValueWithTextEditor(
// we're emulating the user's input. Passing nullptr as
// nsIPrincipal means that that may be user's input. So, let's
// do it.
DebugOnly<nsresult> rv = textEditor->ReplaceTextAsAction(
nsresult rv = textEditor->ReplaceTextAsAction(
aHandlingSetValue.GetSettingValue(), nullptr, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to set the new value");
return;
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"TextEditor::ReplaceTextAsAction() failed");
return rv != NS_ERROR_OUT_OF_MEMORY;
}
// If we're emulating user input, we don't need to manage
// mTextListener state since it should use same path as user
// typing something. On the other hand, if we're setting value
// programatically, we need to manage mTextListener by ourselves
// and suppress "input" events.
mTextListener->SettingValue(true);
bool notifyValueChanged =
!!(aHandlingSetValue.GetSetValueFlags() & eSetValue_Notify);
mTextListener->SetValueChanged(notifyValueChanged);
// Don't dispatch "beforeinput" event nor "input" event for setting value
// by script.
AutoInputEventSuppresser suppressInputEventDispatching(textEditor);
if (aHandlingSetValue.GetSetValueFlags() & eSetValue_ForXUL) {
@@ -2693,49 +2751,57 @@ void TextControlState::SetValueWithTextEditor(
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for
// now.
DebugOnly<nsresult> rv = textEditor->DeleteSelectionAsAction(
nsresult rv = textEditor->DeleteSelectionAsAction(
nsIEditor::eNone, nsIEditor::eStrip, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove the text");
} else {
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for
// now.
DebugOnly<nsresult> rv =
textEditor->InsertTextAsAction(insertValue, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new value");
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"TextEditor::DeleteSelectionAsAction() failed");
return rv != NS_ERROR_OUT_OF_MEMORY;
}
} else {
// On <input> or <textarea>, we shouldn't preserve existing undo
// transactions because other browsers do not preserve them too
// and not preserving transactions makes setting value faster.
AutoDisableUndo disableUndo(textEditor);
if (selection) {
// Since we don't use undo transaction, we don't need to store
// selection state. SetText will set selection to tail.
// Note that textEditor will collapse selection to the end.
// Therefore, it's safe to use RemoveAllRangesTemporarily()
// here.
selection->RemoveAllRangesTemporarily();
}
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for now.
textEditor->SetTextAsAction(aHandlingSetValue.GetSettingValue(), nullptr);
// Call the listener's HandleValueChanged() callback manually,
// since we don't use the transaction manager in this path and it
// could be that the editor would bypass calling the listener for
// that reason.
aHandlingSetValue.GetTextInputListener()->HandleValueChanged();
// event so that passing nullptr as nsIPrincipal is safe for
// now.
nsresult rv = textEditor->InsertTextAsAction(insertValue, nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"TextEditor::InsertTextAsAction() failed");
return rv != NS_ERROR_OUT_OF_MEMORY;
}
aHandlingSetValue.GetTextInputListener()->SetValueChanged(true);
aHandlingSetValue.GetTextInputListener()->SettingValue(false);
if (!notifyValueChanged) {
// Listener doesn't update frame, but it is required for
// placeholder
ValueWasChanged(true);
// On <input> or <textarea>, we shouldn't preserve existing undo
// transactions because other browsers do not preserve them too
// and not preserving transactions makes setting value faster.
AutoDisableUndo disableUndo(textEditor);
if (selection) {
// Since we don't use undo transaction, we don't need to store
// selection state. SetText will set selection to tail.
// Note that textEditor will collapse selection to the end.
// Therefore, it's safe to use RemoveAllRangesTemporarily()
// here.
selection->RemoveAllRangesTemporarily();
}
// In this case, we makes the editor stop dispatching "input"
// event so that passing nullptr as nsIPrincipal is safe for now.
nsresult rv =
textEditor->SetTextAsAction(aHandlingSetValue.GetSettingValue(), nullptr);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"TextEditor::SetTextAsAction() failed");
// Call the listener's OnEditActionHandled() callback manually if
// OnEditActionHandled() hasn't been called yet since TextEditor don't use
// the transaction manager in this path and it could be that the editor
// would bypass calling the listener for that reason.
if (!aHandlingSetValue.HasEditActionHandled()) {
nsresult rvOnEditActionHandled =
MOZ_KnownLive(aHandlingSetValue.GetTextInputListener())
->OnEditActionHandled(*textEditor);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rvOnEditActionHandled),
"TextInputListener::OnEditActionHandled() failed");
if (rv != NS_ERROR_OUT_OF_MEMORY) {
rv = rvOnEditActionHandled;
}
}
return rv != NS_ERROR_OUT_OF_MEMORY;
}
bool TextControlState::SetValueWithoutTextEditor(