gecko-dev/editor/libeditor/TextEditor.cpp

2082 строки
60 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
/* 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 "mozilla/TextEditor.h"
#include "EditAggregateTransaction.h"
#include "HTMLEditRules.h"
#include "InternetCiter.h"
#include "TextEditUtils.h"
#include "gfxFontUtils.h"
#include "mozilla/Assertions.h"
#include "mozilla/EditAction.h"
#include "mozilla/EditorDOMPoint.h"
#include "mozilla/EditorUtils.h" // AutoPlaceholderBatch, AutoRules
#include "mozilla/HTMLEditor.h"
#include "mozilla/IMEStateManager.h"
#include "mozilla/mozalloc.h"
#include "mozilla/Preferences.h"
#include "mozilla/TextEditRules.h"
#include "mozilla/TextComposition.h"
#include "mozilla/TextEvents.h"
#include "mozilla/TextServicesDocument.h"
#include "mozilla/dom/Selection.h"
#include "mozilla/dom/Event.h"
#include "mozilla/dom/Element.h"
#include "nsAString.h"
#include "nsCRT.h"
#include "nsCaret.h"
#include "nsCharTraits.h"
#include "nsComponentManagerUtils.h"
#include "nsContentCID.h"
#include "nsContentList.h"
#include "nsCopySupport.h"
#include "nsDebug.h"
#include "nsDependentSubstring.h"
#include "nsError.h"
#include "nsGkAtoms.h"
#include "nsIAbsorbingTransaction.h"
#include "nsIClipboard.h"
#include "nsIContent.h"
#include "nsIContentIterator.h"
#include "nsIDOMNode.h"
#include "nsIDocumentEncoder.h"
#include "nsINode.h"
#include "nsIPresShell.h"
#include "nsISelectionController.h"
#include "nsISupportsPrimitives.h"
#include "nsITransferable.h"
#include "nsIWeakReferenceUtils.h"
#include "nsNameSpaceManager.h"
#include "nsLiteralString.h"
#include "nsReadableUtils.h"
#include "nsServiceManagerUtils.h"
#include "nsString.h"
#include "nsStringFwd.h"
#include "nsTextNode.h"
#include "nsUnicharUtils.h"
#include "nsXPCOM.h"
class nsIOutputStream;
class nsISupports;
namespace mozilla {
using namespace dom;
template already_AddRefed<Element>
TextEditor::InsertBrElementWithTransaction(
Selection& aSelection,
const EditorDOMPoint& aPointToInsert,
EDirection aSelect);
template already_AddRefed<Element>
TextEditor::InsertBrElementWithTransaction(
Selection& aSelection,
const EditorRawDOMPoint& aPointToInsert,
EDirection aSelect);
TextEditor::TextEditor()
: mWrapColumn(0)
, mMaxTextLength(-1)
, mInitTriggerCounter(0)
, mNewlineHandling(nsIPlaintextEditor::eNewlinesPasteToFirst)
#ifdef XP_WIN
, mCaretStyle(1)
#else
, mCaretStyle(0)
#endif
{
// check the "single line editor newline handling"
// and "caret behaviour in selection" prefs
GetDefaultEditorPrefs(mNewlineHandling, mCaretStyle);
}
TextEditor::~TextEditor()
{
// Remove event listeners. Note that if we had an HTML editor,
// it installed its own instead of these
RemoveEventListeners();
if (mRules)
mRules->DetachEditor();
}
NS_IMPL_CYCLE_COLLECTION_CLASS(TextEditor)
NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN_INHERITED(TextEditor, EditorBase)
if (tmp->mRules)
tmp->mRules->DetachEditor();
NS_IMPL_CYCLE_COLLECTION_UNLINK(mRules)
NS_IMPL_CYCLE_COLLECTION_UNLINK(mCachedDocumentEncoder)
NS_IMPL_CYCLE_COLLECTION_UNLINK_END
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRules)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mCachedDocumentEncoder)
NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase)
NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase)
NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TextEditor)
NS_INTERFACE_MAP_ENTRY(nsIPlaintextEditor)
NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport)
NS_INTERFACE_MAP_END_INHERITING(EditorBase)
nsresult
TextEditor::Init(nsIDocument& aDoc,
Element* aRoot,
nsISelectionController* aSelCon,
uint32_t aFlags,
const nsAString& aInitialValue)
{
if (mRules) {
mRules->DetachEditor();
}
nsresult rulesRv = NS_OK;
{
// block to scope AutoEditInitRulesTrigger
AutoEditInitRulesTrigger rulesTrigger(this, rulesRv);
// Init the base editor
nsresult rv = EditorBase::Init(aDoc, aRoot, aSelCon, aFlags, aInitialValue);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
NS_ENSURE_SUCCESS(rulesRv, rulesRv);
// mRules may not have been initialized yet, when this is called via
// HTMLEditor::Init.
if (mRules) {
mRules->SetInitialValue(aInitialValue);
}
return NS_OK;
}
static int32_t sNewlineHandlingPref = -1,
sCaretStylePref = -1;
static void
EditorPrefsChangedCallback(const char* aPrefName, void *)
{
if (!nsCRT::strcmp(aPrefName, "editor.singleLine.pasteNewlines")) {
sNewlineHandlingPref =
Preferences::GetInt("editor.singleLine.pasteNewlines",
nsIPlaintextEditor::eNewlinesPasteToFirst);
} else if (!nsCRT::strcmp(aPrefName, "layout.selection.caret_style")) {
sCaretStylePref = Preferences::GetInt("layout.selection.caret_style",
#ifdef XP_WIN
1);
if (!sCaretStylePref) {
sCaretStylePref = 1;
}
#else
0);
#endif
}
}
// static
void
TextEditor::GetDefaultEditorPrefs(int32_t& aNewlineHandling,
int32_t& aCaretStyle)
{
if (sNewlineHandlingPref == -1) {
Preferences::RegisterCallbackAndCall(EditorPrefsChangedCallback,
"editor.singleLine.pasteNewlines");
Preferences::RegisterCallbackAndCall(EditorPrefsChangedCallback,
"layout.selection.caret_style");
}
aNewlineHandling = sNewlineHandlingPref;
aCaretStyle = sCaretStylePref;
}
void
TextEditor::BeginEditorInit()
{
mInitTriggerCounter++;
}
nsresult
TextEditor::EndEditorInit()
{
MOZ_ASSERT(mInitTriggerCounter > 0, "ended editor init before we began?");
mInitTriggerCounter--;
if (mInitTriggerCounter) {
return NS_OK;
}
nsresult rv = InitRules();
if (NS_FAILED(rv)) {
return rv;
}
// Throw away the old transaction manager if this is not the first time that
// we're initializing the editor.
ClearUndoRedo();
EnableUndoRedo();
return NS_OK;
}
NS_IMETHODIMP
TextEditor::SetDocumentCharacterSet(const nsACString& characterSet)
{
nsresult rv = EditorBase::SetDocumentCharacterSet(characterSet);
NS_ENSURE_SUCCESS(rv, rv);
// Update META charset element.
nsCOMPtr<nsIDocument> doc = GetDocument();
if (NS_WARN_IF(!doc)) {
return NS_ERROR_NOT_INITIALIZED;
}
if (UpdateMetaCharset(*doc, characterSet)) {
return NS_OK;
}
RefPtr<nsContentList> headList =
doc->GetElementsByTagName(NS_LITERAL_STRING("head"));
if (NS_WARN_IF(!headList)) {
return NS_OK;
}
nsCOMPtr<nsIContent> headNode = headList->Item(0);
if (NS_WARN_IF(!headNode)) {
return NS_OK;
}
// Create a new meta charset tag
EditorRawDOMPoint atStartOfHeadNode(headNode, 0);
RefPtr<Element> metaElement =
CreateNodeWithTransaction(*nsGkAtoms::meta, atStartOfHeadNode);
if (NS_WARN_IF(!metaElement)) {
return NS_OK;
}
// Set attributes to the created element
if (characterSet.IsEmpty()) {
return NS_OK;
}
// not undoable, undo should undo CreateNodeWithTransaction().
metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv,
NS_LITERAL_STRING("Content-Type"), true);
metaElement->SetAttr(kNameSpaceID_None, nsGkAtoms::content,
NS_LITERAL_STRING("text/html;charset=") +
NS_ConvertASCIItoUTF16(characterSet),
true);
return NS_OK;
}
bool
TextEditor::UpdateMetaCharset(nsIDocument& aDocument,
const nsACString& aCharacterSet)
{
// get a list of META tags
RefPtr<nsContentList> metaList =
aDocument.GetElementsByTagName(NS_LITERAL_STRING("meta"));
if (NS_WARN_IF(!metaList)) {
return false;
}
for (uint32_t i = 0; i < metaList->Length(true); ++i) {
nsCOMPtr<nsIContent> metaNode = metaList->Item(i);
MOZ_ASSERT(metaNode);
if (!metaNode->IsElement()) {
continue;
}
nsAutoString currentValue;
metaNode->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv,
currentValue);
if (!FindInReadable(NS_LITERAL_STRING("content-type"),
currentValue,
nsCaseInsensitiveStringComparator())) {
continue;
}
metaNode->AsElement()->GetAttr(kNameSpaceID_None, nsGkAtoms::content,
currentValue);
NS_NAMED_LITERAL_STRING(charsetEquals, "charset=");
nsAString::const_iterator originalStart, start, end;
originalStart = currentValue.BeginReading(start);
currentValue.EndReading(end);
if (!FindInReadable(charsetEquals, start, end,
nsCaseInsensitiveStringComparator())) {
continue;
}
// set attribute to <original prefix> charset=text/html
RefPtr<Element> metaElement = metaNode->AsElement();
MOZ_ASSERT(metaElement);
nsresult rv =
SetAttributeWithTransaction(*metaElement, *nsGkAtoms::content,
Substring(originalStart, start) +
charsetEquals +
NS_ConvertASCIItoUTF16(aCharacterSet));
return NS_SUCCEEDED(rv);
}
return false;
}
NS_IMETHODIMP
TextEditor::InitRules()
{
if (!mRules) {
// instantiate the rules for this text editor
mRules = new TextEditRules();
}
return mRules->Init(this);
}
nsresult
TextEditor::HandleKeyPressEvent(WidgetKeyboardEvent* aKeyboardEvent)
{
// NOTE: When you change this method, you should also change:
// * editor/libeditor/tests/test_texteditor_keyevent_handling.html
// * editor/libeditor/tests/test_htmleditor_keyevent_handling.html
//
// And also when you add new key handling, you need to change the subclass's
// HandleKeyPressEvent()'s switch statement.
if (IsReadonly() || IsDisabled()) {
// When we're not editable, the events handled on EditorBase.
return EditorBase::HandleKeyPressEvent(aKeyboardEvent);
}
if (NS_WARN_IF(!aKeyboardEvent)) {
return NS_ERROR_UNEXPECTED;
}
MOZ_ASSERT(aKeyboardEvent->mMessage == eKeyPress,
"HandleKeyPressEvent gets non-keypress event");
switch (aKeyboardEvent->mKeyCode) {
case NS_VK_META:
case NS_VK_WIN:
case NS_VK_SHIFT:
case NS_VK_CONTROL:
case NS_VK_ALT:
// These keys are handled on EditorBase
return EditorBase::HandleKeyPressEvent(aKeyboardEvent);
case NS_VK_BACK:
if (aKeyboardEvent->IsControl() || aKeyboardEvent->IsAlt() ||
aKeyboardEvent->IsMeta() || aKeyboardEvent->IsOS()) {
return NS_OK;
}
DeleteSelectionAsAction(nsIEditor::ePrevious, nsIEditor::eStrip);
aKeyboardEvent->PreventDefault(); // consumed
return NS_OK;
case NS_VK_DELETE:
// on certain platforms (such as windows) the shift key
// modifies what delete does (cmd_cut in this case).
// bailing here to allow the keybindings to do the cut.
if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() ||
aKeyboardEvent->IsOS()) {
return NS_OK;
}
DeleteSelectionAsAction(nsIEditor::eNext, nsIEditor::eStrip);
aKeyboardEvent->PreventDefault(); // consumed
return NS_OK;
case NS_VK_TAB: {
if (IsTabbable()) {
return NS_OK; // let it be used for focus switching
}
if (aKeyboardEvent->IsShift() || aKeyboardEvent->IsControl() ||
aKeyboardEvent->IsAlt() || aKeyboardEvent->IsMeta() ||
aKeyboardEvent->IsOS()) {
return NS_OK;
}
// else we insert the tab straight through
aKeyboardEvent->PreventDefault();
return OnInputText(NS_LITERAL_STRING("\t"));
}
case NS_VK_RETURN:
if (IsSingleLineEditor() || !aKeyboardEvent->IsInputtingLineBreak()) {
return NS_OK;
}
aKeyboardEvent->PreventDefault();
return OnInputParagraphSeparator();
}
if (!aKeyboardEvent->IsInputtingText()) {
// we don't PreventDefault() here or keybindings like control-x won't work
return NS_OK;
}
aKeyboardEvent->PreventDefault();
nsAutoString str(aKeyboardEvent->mCharCode);
return OnInputText(str);
}
nsresult
TextEditor::OnInputText(const nsAString& aStringToInsert)
{
AutoPlaceholderBatch batch(this, nsGkAtoms::TypingTxnName);
nsresult rv = InsertTextAsAction(aStringToInsert);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
TextEditor::OnInputParagraphSeparator()
{
AutoPlaceholderBatch batch(this, nsGkAtoms::TypingTxnName);
nsresult rv = InsertParagraphSeparatorAsAction();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
template<typename PT, typename CT>
already_AddRefed<Element>
TextEditor::InsertBrElementWithTransaction(
Selection& aSelection,
const EditorDOMPointBase<PT, CT>& aPointToInsert,
EDirection aSelect /* = eNone */)
{
if (NS_WARN_IF(!aPointToInsert.IsSet())) {
return nullptr;
}
// We need to insert a <br> node.
RefPtr<Element> newBRElement;
if (aPointToInsert.IsInTextNode()) {
EditorDOMPoint pointInContainer;
if (aPointToInsert.IsStartOfContainer()) {
// Insert before the text node.
pointInContainer.Set(aPointToInsert.GetContainer());
if (NS_WARN_IF(!pointInContainer.IsSet())) {
return nullptr;
}
} else if (aPointToInsert.IsEndOfContainer()) {
// Insert after the text node.
pointInContainer.Set(aPointToInsert.GetContainer());
if (NS_WARN_IF(!pointInContainer.IsSet())) {
return nullptr;
}
DebugOnly<bool> advanced = pointInContainer.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset to after the text node");
} else {
MOZ_DIAGNOSTIC_ASSERT(aPointToInsert.IsSetAndValid());
// Unfortunately, we need to split the text node at the offset.
ErrorResult error;
nsCOMPtr<nsIContent> newLeftNode =
SplitNodeWithTransaction(aPointToInsert, error);
if (NS_WARN_IF(error.Failed())) {
error.SuppressException();
return nullptr;
}
Unused << newLeftNode;
// Insert new <br> before the right node.
pointInContainer.Set(aPointToInsert.GetContainer());
}
// Create a <br> node.
newBRElement = CreateNodeWithTransaction(*nsGkAtoms::br, pointInContainer);
if (NS_WARN_IF(!newBRElement)) {
return nullptr;
}
} else {
newBRElement = CreateNodeWithTransaction(*nsGkAtoms::br, aPointToInsert);
if (NS_WARN_IF(!newBRElement)) {
return nullptr;
}
}
switch (aSelect) {
case eNone:
break;
case eNext: {
aSelection.SetInterlinePosition(true, IgnoreErrors());
// Collapse selection after the <br> node.
EditorRawDOMPoint afterBRElement(newBRElement);
if (afterBRElement.IsSet()) {
DebugOnly<bool> advanced = afterBRElement.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to advance offset after the <br> element");
ErrorResult error;
aSelection.Collapse(afterBRElement, error);
NS_WARNING_ASSERTION(!error.Failed(),
"Failed to collapse selection after the <br> element");
} else {
NS_WARNING("The <br> node is not in the DOM tree?");
}
break;
}
case ePrevious: {
aSelection.SetInterlinePosition(true, IgnoreErrors());
// Collapse selection at the <br> node.
EditorRawDOMPoint atBRElement(newBRElement);
if (atBRElement.IsSet()) {
ErrorResult error;
aSelection.Collapse(atBRElement, error);
NS_WARNING_ASSERTION(!error.Failed(),
"Failed to collapse selection at the <br> element");
} else {
NS_WARNING("The <br> node is not in the DOM tree?");
}
break;
}
default:
NS_WARNING("aSelect has invalid value, the caller need to set selection "
"by itself");
break;
}
return newBRElement.forget();
}
nsresult
TextEditor::ExtendSelectionForDelete(Selection* aSelection,
nsIEditor::EDirection* aAction)
{
bool bCollapsed = aSelection->IsCollapsed();
if (*aAction == eNextWord ||
*aAction == ePreviousWord ||
(*aAction == eNext && bCollapsed) ||
(*aAction == ePrevious && bCollapsed) ||
*aAction == eToBeginningOfLine ||
*aAction == eToEndOfLine) {
nsCOMPtr<nsISelectionController> selCont;
GetSelectionController(getter_AddRefs(selCont));
NS_ENSURE_TRUE(selCont, NS_ERROR_NO_INTERFACE);
switch (*aAction) {
case eNextWord: {
nsresult rv = selCont->WordExtendForDelete(true);
// DeleteSelectionWithTransaction() doesn't handle these actions
// because it's inside batching, so don't confuse it:
*aAction = eNone;
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
case ePreviousWord: {
nsresult rv = selCont->WordExtendForDelete(false);
*aAction = eNone;
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
case eNext: {
nsresult rv = selCont->CharacterExtendForDelete();
// Don't set aAction to eNone (see Bug 502259)
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
case ePrevious: {
// Only extend the selection where the selection is after a UTF-16
// surrogate pair or a variation selector.
// For other cases we don't want to do that, in order
// to make sure that pressing backspace will only delete the last
// typed character.
EditorRawDOMPoint atStartOfSelection =
EditorBase::GetStartPoint(aSelection);
if (NS_WARN_IF(!atStartOfSelection.IsSet())) {
return NS_ERROR_FAILURE;
}
// node might be anonymous DIV, so we find better text node
EditorRawDOMPoint insertionPoint =
FindBetterInsertionPoint(atStartOfSelection);
if (insertionPoint.IsInTextNode()) {
const nsTextFragment* data =
insertionPoint.GetContainerAsText()->GetText();
uint32_t offset = insertionPoint.Offset();
if ((offset > 1 &&
NS_IS_LOW_SURROGATE(data->CharAt(offset - 1)) &&
NS_IS_HIGH_SURROGATE(data->CharAt(offset - 2))) ||
(offset > 0 &&
gfxFontUtils::IsVarSelector(data->CharAt(offset - 1)))) {
nsresult rv = selCont->CharacterExtendForBackspace();
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
}
}
return NS_OK;
}
case eToBeginningOfLine: {
// Try to move to end
selCont->IntraLineMove(true, false);
// Select to beginning
nsresult rv = selCont->IntraLineMove(false, true);
*aAction = eNone;
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
case eToEndOfLine: {
nsresult rv = selCont->IntraLineMove(true, true);
*aAction = eNext;
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
// For avoiding several compiler warnings
default:
return NS_OK;
}
}
return NS_OK;
}
NS_IMETHODIMP
TextEditor::DeleteSelection(EDirection aAction,
EStripWrappers aStripWrappers)
{
nsresult rv = DeleteSelectionAsAction(aAction, aStripWrappers);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
TextEditor::DeleteSelectionAsAction(EDirection aDirection,
EStripWrappers aStripWrappers)
{
MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
if (!mRules) {
return NS_ERROR_NOT_INITIALIZED;
}
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
// delete placeholder txns merge.
AutoPlaceholderBatch batch(this, nsGkAtoms::DeleteTxnName);
AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aDirection);
// pre-process
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
// If there is an existing selection when an extended delete is requested,
// platforms that use "caret-style" caret positioning collapse the
// selection to the start and then create a new selection.
// Platforms that use "selection-style" caret positioning just delete the
// existing selection without extending it.
if (!selection->IsCollapsed()) {
switch (aDirection) {
case eNextWord:
case ePreviousWord:
case eToBeginningOfLine:
case eToEndOfLine: {
if (mCaretStyle != 1) {
aDirection = eNone;
break;
}
ErrorResult error;
selection->CollapseToStart(error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
break;
}
default:
break;
}
}
RulesInfo ruleInfo(EditAction::deleteSelection);
ruleInfo.collapsedAction = aDirection;
ruleInfo.stripWrappers = aStripWrappers;
bool cancel, handled;
nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
NS_ENSURE_SUCCESS(rv, rv);
if (!cancel && !handled) {
rv = DeleteSelectionWithTransaction(aDirection, aStripWrappers);
}
if (!cancel) {
// post-process
rv = rules->DidDoAction(selection, &ruleInfo, rv);
}
return rv;
}
nsresult
TextEditor::DeleteSelectionWithTransaction(EDirection aDirection,
EStripWrappers aStripWrappers)
{
MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip);
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_NOT_INITIALIZED;
}
RefPtr<EditAggregateTransaction> deleteSelectionTransaction;
nsCOMPtr<nsINode> deleteNode;
int32_t deleteCharOffset = 0, deleteCharLength = 0;
if (!selection->IsCollapsed() || aDirection != eNone) {
deleteSelectionTransaction =
CreateTxnForDeleteSelection(aDirection,
getter_AddRefs(deleteNode),
&deleteCharOffset,
&deleteCharLength);
if (NS_WARN_IF(!deleteSelectionTransaction)) {
return NS_ERROR_FAILURE;
}
}
RefPtr<CharacterData> deleteCharData =
CharacterData::FromNodeOrNull(deleteNode);
AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aDirection);
if (mRules && mRules->AsHTMLEditRules()) {
if (!deleteNode) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->WillDeleteSelection(selection);
} else if (!deleteCharData) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->WillDeleteNode(deleteNode);
}
}
// Notify nsIEditActionListener::WillDelete[Selection|Text]
if (!mActionListeners.IsEmpty()) {
if (!deleteNode) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->WillDeleteSelection(selection);
}
} else if (deleteCharData) {
AutoActionListenerArray listeners(mActionListeners);
for (auto& listener : listeners) {
listener->WillDeleteText(deleteCharData, deleteCharOffset, 1);
}
}
}
// Delete the specified amount
nsresult rv = DoTransaction(deleteSelectionTransaction);
if (mRules && mRules->AsHTMLEditRules() && deleteCharData) {
RefPtr<HTMLEditRules> htmlEditRules = mRules->AsHTMLEditRules();
htmlEditRules->DidDeleteText(deleteNode, deleteCharOffset, 1);
}
if (mTextServicesDocument && NS_SUCCEEDED(rv) &&
deleteNode && !deleteCharData) {
RefPtr<TextServicesDocument> textServicesDocument = mTextServicesDocument;
textServicesDocument->DidDeleteNode(deleteNode);
}
// Notify nsIEditActionListener::DidDelete[Selection|Text|Node]
{
AutoActionListenerArray listeners(mActionListeners);
if (!deleteNode) {
for (auto& listener : mActionListeners) {
listener->DidDeleteSelection(selection);
}
} else if (deleteCharData) {
for (auto& listener : mActionListeners) {
listener->DidDeleteText(deleteCharData, deleteCharOffset, 1, rv);
}
} else {
for (auto& listener : mActionListeners) {
listener->DidDeleteNode(deleteNode->AsDOMNode(), rv);
}
}
}
return rv;
}
already_AddRefed<Element>
TextEditor::DeleteSelectionAndCreateElement(nsAtom& aTag)
{
nsresult rv = DeleteSelectionAndPrepareToCreateNode();
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return nullptr;
}
EditorRawDOMPoint pointToInsert(selection->AnchorRef());
if (!pointToInsert.IsSet()) {
return nullptr;
}
RefPtr<Element> newElement = CreateNodeWithTransaction(aTag, pointToInsert);
// We want the selection to be just after the new node
EditorRawDOMPoint afterNewElement(newElement);
MOZ_ASSERT(afterNewElement.IsSetAndValid());
DebugOnly<bool> advanced = afterNewElement.AdvanceOffset();
NS_WARNING_ASSERTION(advanced,
"Failed to move offset next to the new element");
ErrorResult error;
selection->Collapse(afterNewElement, error);
if (NS_WARN_IF(error.Failed())) {
// XXX Even if it succeeded to create new element, this returns error
// when Selection.Collapse() fails something. This could occur with
// mutation observer or mutation event listener.
error.SuppressException();
return nullptr;
}
return newElement.forget();
}
nsresult
TextEditor::DeleteSelectionAndPrepareToCreateNode()
{
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_NOT_INITIALIZED;
}
if (NS_WARN_IF(!selection->GetAnchorFocusRange())) {
return NS_OK;
}
if (!selection->GetAnchorFocusRange()->Collapsed()) {
nsresult rv = DeleteSelectionAsAction(eNone, eStrip);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
MOZ_ASSERT(selection->GetAnchorFocusRange() &&
selection->GetAnchorFocusRange()->Collapsed(),
"Selection not collapsed after delete");
}
// If the selection is a chardata node, split it if necessary and compute
// where to put the new node
EditorDOMPoint atAnchor(selection->AnchorRef());
if (NS_WARN_IF(!atAnchor.IsSet()) || !atAnchor.IsInDataNode()) {
return NS_OK;
}
if (NS_WARN_IF(!atAnchor.GetContainer()->GetParentNode())) {
return NS_ERROR_FAILURE;
}
if (atAnchor.IsStartOfContainer()) {
EditorRawDOMPoint atAnchorContainer(atAnchor.GetContainer());
if (NS_WARN_IF(!atAnchorContainer.IsSetAndValid())) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
selection->Collapse(atAnchorContainer, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
if (atAnchor.IsEndOfContainer()) {
EditorRawDOMPoint afterAnchorContainer(atAnchor.GetContainer());
if (NS_WARN_IF(!afterAnchorContainer.AdvanceOffset())) {
return NS_ERROR_FAILURE;
}
ErrorResult error;
selection->Collapse(afterAnchorContainer, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
ErrorResult error;
nsCOMPtr<nsIContent> newLeftNode = SplitNodeWithTransaction(atAnchor, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
EditorRawDOMPoint atRightNode(atAnchor.GetContainer());
if (NS_WARN_IF(!atRightNode.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(atRightNode.IsSetAndValid());
selection->Collapse(atRightNode, error);
if (NS_WARN_IF(error.Failed())) {
return error.StealNSResult();
}
return NS_OK;
}
NS_IMETHODIMP
TextEditor::InsertText(const nsAString& aStringToInsert)
{
nsresult rv = InsertTextAsAction(aStringToInsert);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
nsresult
TextEditor::InsertTextAsAction(const nsAString& aStringToInsert)
{
if (!mRules) {
return NS_ERROR_NOT_INITIALIZED;
}
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
EditAction opID = EditAction::insertText;
if (ShouldHandleIMEComposition()) {
opID = EditAction::insertIMEText;
}
AutoPlaceholderBatch batch(this, nullptr);
AutoRules beginRulesSniffing(this, opID, nsIEditor::eNext);
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
nsAutoString resultString;
// XXX can we trust instring to outlive ruleInfo,
// XXX and ruleInfo not to refer to instring in its dtor?
//nsAutoString instring(aStringToInsert);
RulesInfo ruleInfo(opID);
ruleInfo.inString = &aStringToInsert;
ruleInfo.outString = &resultString;
ruleInfo.maxLength = mMaxTextLength;
bool cancel, handled;
nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (!cancel && !handled) {
// we rely on rules code for now - no default implementation
}
if (cancel) {
return NS_OK;
}
// post-process
return rules->DidDoAction(selection, &ruleInfo, NS_OK);
}
NS_IMETHODIMP
TextEditor::InsertLineBreak()
{
return InsertParagraphSeparatorAsAction();
}
nsresult
TextEditor::InsertParagraphSeparatorAsAction()
{
if (!mRules) {
return NS_ERROR_NOT_INITIALIZED;
}
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
AutoPlaceholderBatch beginBatching(this);
AutoRules beginRulesSniffing(this, EditAction::insertBreak, nsIEditor::eNext);
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
RulesInfo ruleInfo(EditAction::insertBreak);
ruleInfo.maxLength = mMaxTextLength;
bool cancel, handled;
nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
if (NS_WARN_IF(NS_FAILED(rv))) {
// XXX DidDoAction() won't be called when WillDoAction() returns error.
// Perhaps, we should move the code between WillDoAction() and
// DidDoAction() to a new method and guarantee that DidDoAction() is
// always called after WillDoAction().
return rv;
}
if (!cancel && !handled) {
// get the (collapsed) selection location
nsRange* firstRange = selection->GetRangeAt(0);
if (NS_WARN_IF(!firstRange)) {
return NS_ERROR_FAILURE;
}
EditorRawDOMPoint pointToInsert(firstRange->StartRef());
if (NS_WARN_IF(!pointToInsert.IsSet())) {
return NS_ERROR_FAILURE;
}
MOZ_ASSERT(pointToInsert.IsSetAndValid());
// don't put text in places that can't have it
if (!pointToInsert.IsInTextNode() &&
!CanContainTag(*pointToInsert.GetContainer(),
*nsGkAtoms::textTagName)) {
return NS_ERROR_FAILURE;
}
// we need to get the doc
nsCOMPtr<nsIDocument> doc = GetDocument();
if (NS_WARN_IF(!doc)) {
return NS_ERROR_NOT_INITIALIZED;
}
// don't change my selection in subtransactions
AutoTransactionsConserveSelection dontChangeMySelection(this);
// insert a linefeed character
EditorRawDOMPoint pointAfterInsertedLineBreak;
rv = InsertTextWithTransaction(*doc, NS_LITERAL_STRING("\n"), pointToInsert,
&pointAfterInsertedLineBreak);
if (NS_WARN_IF(!pointAfterInsertedLineBreak.IsSet())) {
rv = NS_ERROR_NULL_POINTER; // don't return here, so DidDoAction is called
}
if (NS_SUCCEEDED(rv)) {
// set the selection to the correct location
MOZ_ASSERT(!pointAfterInsertedLineBreak.GetChild(),
"After inserting text into a text node, pointAfterInsertedLineBreak."
"GetChild() should be nullptr");
rv = selection->Collapse(pointAfterInsertedLineBreak);
if (NS_SUCCEEDED(rv)) {
// see if we're at the end of the editor range
EditorRawDOMPoint endPoint = GetEndPoint(selection);
if (endPoint == pointAfterInsertedLineBreak) {
// SetInterlinePosition(true) means we want the caret to stick to the
// content on the "right". We want the caret to stick to whatever is
// past the break. This is because the break is on the same line we
// were on, but the next content will be on the following line.
selection->SetInterlinePosition(true, IgnoreErrors());
}
}
}
}
if (!cancel) {
// post-process, always called if WillInsertBreak didn't return cancel==true
rv = rules->DidDoAction(selection, &ruleInfo, rv);
}
return rv;
}
nsresult
TextEditor::SetText(const nsAString& aString)
{
if (NS_WARN_IF(!mRules)) {
return NS_ERROR_NOT_INITIALIZED;
}
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
// delete placeholder txns merge.
AutoPlaceholderBatch batch(this, nullptr);
AutoRules beginRulesSniffing(this, EditAction::setText, nsIEditor::eNext);
// pre-process
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_NULL_POINTER;
}
RulesInfo ruleInfo(EditAction::setText);
ruleInfo.inString = &aString;
ruleInfo.maxLength = mMaxTextLength;
bool cancel;
bool handled;
nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (cancel) {
return NS_OK;
}
if (!handled) {
// We want to select trailing BR node to remove all nodes to replace all,
// but TextEditor::SelectEntireDocument doesn't select that BR node.
if (rules->DocumentIsEmpty()) {
// if it's empty, don't select entire doc - that would select
// the bogus node
Element* rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_FAILURE;
}
rv = selection->Collapse(rootElement, 0);
} else {
rv = EditorBase::SelectEntireDocument(selection);
}
if (NS_SUCCEEDED(rv)) {
if (aString.IsEmpty()) {
rv = DeleteSelectionAsAction(eNone, eStrip);
NS_WARNING_ASSERTION(NS_FAILED(rv), "Failed to remove all text");
} else {
rv = InsertTextAsAction(aString);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert the new text");
}
}
}
// post-process
return rules->DidDoAction(selection, &ruleInfo, rv);
}
bool
TextEditor::EnsureComposition(WidgetCompositionEvent& aCompositionEvent)
{
if (mComposition) {
return true;
}
// The compositionstart event must cause creating new TextComposition
// instance at being dispatched by IMEStateManager.
mComposition = IMEStateManager::GetTextCompositionFor(&aCompositionEvent);
if (!mComposition) {
// However, TextComposition may be committed before the composition
// event comes here.
return false;
}
mComposition->StartHandlingComposition(this);
return true;
}
nsresult
TextEditor::OnCompositionStart(WidgetCompositionEvent& aCompositionStartEvent)
{
if (NS_WARN_IF(mComposition)) {
return NS_OK;
}
if (IsPasswordEditor()) {
if (NS_WARN_IF(!mRules)) {
return NS_ERROR_FAILURE;
}
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
rules->ResetIMETextPWBuf();
}
EnsureComposition(aCompositionStartEvent);
NS_WARNING_ASSERTION(mComposition, "Failed to get TextComposition instance?");
return NS_OK;
}
nsresult
TextEditor::OnCompositionChange(WidgetCompositionEvent& aCompsitionChangeEvent)
{
MOZ_ASSERT(aCompsitionChangeEvent.mMessage == eCompositionChange,
"The event should be eCompositionChange");
if (!EnsureComposition(aCompsitionChangeEvent)) {
return NS_OK;
}
nsIPresShell* presShell = GetPresShell();
if (NS_WARN_IF(!presShell)) {
return NS_ERROR_NOT_INITIALIZED;
}
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return NS_ERROR_FAILURE;
}
// NOTE: TextComposition should receive selection change notification before
// CompositionChangeEventHandlingMarker notifies TextComposition of the
// end of handling compositionchange event because TextComposition may
// need to ignore selection changes caused by composition. Therefore,
// CompositionChangeEventHandlingMarker must be destroyed after a call
// of NotifiyEditorObservers(eNotifyEditorObserversOfEnd) or
// NotifiyEditorObservers(eNotifyEditorObserversOfCancel) which notifies
// TextComposition of a selection change.
MOZ_ASSERT(!mPlaceholderBatch,
"UpdateIMEComposition() must be called without place holder batch");
TextComposition::CompositionChangeEventHandlingMarker
compositionChangeEventHandlingMarker(mComposition, &aCompsitionChangeEvent);
RefPtr<nsCaret> caretP = presShell->GetCaret();
nsresult rv;
{
AutoPlaceholderBatch batch(this, nsGkAtoms::IMETxnName);
MOZ_ASSERT(mIsInEditAction,
"AutoPlaceholderBatch should've notified the observes of before-edit");
rv = InsertTextAsAction(aCompsitionChangeEvent.mData);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
"Failed to insert new composition string");
if (caretP) {
caretP->SetSelection(selection);
}
}
// If still composing, we should fire input event via observer.
// Note that if the composition will be committed by the following
// compositionend event, we don't need to notify editor observes of this
// change.
// NOTE: We must notify after the auto batch will be gone.
if (!aCompsitionChangeEvent.IsFollowedByCompositionEnd()) {
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
}
return rv;
}
void
TextEditor::OnCompositionEnd(WidgetCompositionEvent& aCompositionEndEvent)
{
if (NS_WARN_IF(!mComposition)) {
return;
}
// commit the IME transaction..we can get at it via the transaction mgr.
// Note that this means IME won't work without an undo stack!
if (mTransactionManager) {
nsCOMPtr<nsITransaction> txn = mTransactionManager->PeekUndoStack();
nsCOMPtr<nsIAbsorbingTransaction> plcTxn = do_QueryInterface(txn);
if (plcTxn) {
DebugOnly<nsresult> rv = plcTxn->Commit();
NS_ASSERTION(NS_SUCCEEDED(rv),
"nsIAbsorbingTransaction::Commit() failed");
}
}
// Composition string may have hidden the caret. Therefore, we need to
// cancel it here.
HideCaret(false);
// FYI: mComposition still keeps storing container text node of committed
// string, its offset and length. However, they will be invalidated
// soon since its Destroy() will be called by IMEStateManager.
mComposition->EndHandlingComposition(this);
mComposition = nullptr;
// notify editor observers of action
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
}
already_AddRefed<nsIContent>
TextEditor::GetInputEventTargetContent()
{
nsCOMPtr<nsIContent> target = do_QueryInterface(mEventTarget);
return target.forget();
}
nsresult
TextEditor::DocumentIsEmpty(bool* aIsEmpty)
{
NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED);
if (mRules->HasBogusNode()) {
*aIsEmpty = true;
return NS_OK;
}
// Even if there is no bogus node, we should be detected as empty document
// if all the children are text nodes and these have no content.
Element* rootElement = GetRoot();
if (!rootElement) {
*aIsEmpty = true;
return NS_OK;
}
for (nsIContent* child = rootElement->GetFirstChild();
child; child = child->GetNextSibling()) {
if (!EditorBase::IsTextNode(child) ||
static_cast<nsTextNode*>(child)->TextDataLength()) {
*aIsEmpty = false;
return NS_OK;
}
}
*aIsEmpty = true;
return NS_OK;
}
NS_IMETHODIMP
TextEditor::GetDocumentIsEmpty(bool* aDocumentIsEmpty)
{
return DocumentIsEmpty(aDocumentIsEmpty);
}
NS_IMETHODIMP
TextEditor::GetTextLength(int32_t* aCount)
{
NS_ASSERTION(aCount, "null pointer");
// initialize out params
*aCount = 0;
// special-case for empty document, to account for the bogus node
bool docEmpty;
nsresult rv = GetDocumentIsEmpty(&docEmpty);
NS_ENSURE_SUCCESS(rv, rv);
if (docEmpty) {
return NS_OK;
}
dom::Element *rootElement = GetRoot();
NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER);
nsCOMPtr<nsIContentIterator> iter =
do_CreateInstance("@mozilla.org/content/post-content-iterator;1", &rv);
NS_ENSURE_SUCCESS(rv, rv);
uint32_t totalLength = 0;
iter->Init(rootElement);
for (; !iter->IsDone(); iter->Next()) {
nsCOMPtr<nsINode> currentNode = iter->GetCurrentNode();
if (IsTextNode(currentNode) && IsEditable(currentNode)) {
totalLength += currentNode->Length();
}
}
*aCount = totalLength;
return NS_OK;
}
NS_IMETHODIMP
TextEditor::GetWrapWidth(int32_t* aWrapColumn)
{
NS_ENSURE_TRUE( aWrapColumn, NS_ERROR_NULL_POINTER);
*aWrapColumn = mWrapColumn;
return NS_OK;
}
//
// See if the style value includes this attribute, and if it does,
// cut out everything from the attribute to the next semicolon.
//
static void CutStyle(const char* stylename, nsString& styleValue)
{
// Find the current wrapping type:
int32_t styleStart = styleValue.Find(stylename, true);
if (styleStart >= 0) {
int32_t styleEnd = styleValue.Find(";", false, styleStart);
if (styleEnd > styleStart) {
styleValue.Cut(styleStart, styleEnd - styleStart + 1);
} else {
styleValue.Cut(styleStart, styleValue.Length() - styleStart);
}
}
}
NS_IMETHODIMP
TextEditor::SetWrapWidth(int32_t aWrapColumn)
{
SetWrapColumn(aWrapColumn);
// Make sure we're a plaintext editor, otherwise we shouldn't
// do the rest of this.
if (!IsPlaintextEditor()) {
return NS_OK;
}
// Ought to set a style sheet here ...
// Probably should keep around an mPlaintextStyleSheet for this purpose.
dom::Element *rootElement = GetRoot();
NS_ENSURE_TRUE(rootElement, NS_ERROR_NULL_POINTER);
// Get the current style for this root element:
nsAutoString styleValue;
rootElement->GetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue);
// We'll replace styles for these values:
CutStyle("white-space", styleValue);
CutStyle("width", styleValue);
CutStyle("font-family", styleValue);
// If we have other style left, trim off any existing semicolons
// or whitespace, then add a known semicolon-space:
if (!styleValue.IsEmpty()) {
styleValue.Trim("; \t", false, true);
styleValue.AppendLiteral("; ");
}
// Make sure we have fixed-width font. This should be done for us,
// but it isn't, see bug 22502, so we have to add "font: -moz-fixed;".
// Only do this if we're wrapping.
if (IsWrapHackEnabled() && aWrapColumn >= 0) {
styleValue.AppendLiteral("font-family: -moz-fixed; ");
}
// and now we're ready to set the new whitespace/wrapping style.
if (aWrapColumn > 0) {
// Wrap to a fixed column.
styleValue.AppendLiteral("white-space: pre-wrap; width: ");
styleValue.AppendInt(aWrapColumn);
styleValue.AppendLiteral("ch;");
} else if (!aWrapColumn) {
styleValue.AppendLiteral("white-space: pre-wrap;");
} else {
styleValue.AppendLiteral("white-space: pre;");
}
return rootElement->SetAttr(kNameSpaceID_None, nsGkAtoms::style, styleValue, true);
}
NS_IMETHODIMP
TextEditor::SetWrapColumn(int32_t aWrapColumn)
{
mWrapColumn = aWrapColumn;
return NS_OK;
}
NS_IMETHODIMP
TextEditor::GetNewlineHandling(int32_t* aNewlineHandling)
{
NS_ENSURE_ARG_POINTER(aNewlineHandling);
*aNewlineHandling = mNewlineHandling;
return NS_OK;
}
NS_IMETHODIMP
TextEditor::SetNewlineHandling(int32_t aNewlineHandling)
{
mNewlineHandling = aNewlineHandling;
return NS_OK;
}
NS_IMETHODIMP
TextEditor::Undo(uint32_t aCount)
{
// If we don't have transaction in the undo stack, we shouldn't notify
// anybody of trying to undo since it's not useful notification but we
// need to pay some runtime cost.
if (!CanUndo()) {
return NS_OK;
}
// If there is composition, we shouldn't allow to undo with committing
// composition since Chrome doesn't allow it and it doesn't make sense
// because committing composition causes one transaction and Undo(1)
// undoes the committing composition.
if (GetComposition()) {
return NS_OK;
}
// Protect the edit rules object from dying.
RefPtr<TextEditRules> rules(mRules);
AutoUpdateViewBatch beginViewBatching(this);
NotifyEditorObservers(eNotifyEditorObserversOfBefore);
if (NS_WARN_IF(!CanUndo()) || NS_WARN_IF(Destroyed())) {
return NS_ERROR_FAILURE;
}
nsresult rv;
{
AutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone);
RulesInfo ruleInfo(EditAction::undo);
RefPtr<Selection> selection = GetSelection();
bool cancel, handled;
rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
if (!cancel && NS_SUCCEEDED(rv)) {
RefPtr<TransactionManager> transactionManager(mTransactionManager);
for (uint32_t i = 0; i < aCount; ++i) {
rv = transactionManager->Undo();
if (NS_WARN_IF(NS_FAILED(rv))) {
break;
}
DoAfterUndoTransaction();
}
rv = rules->DidDoAction(selection, &ruleInfo, rv);
}
}
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
return rv;
}
NS_IMETHODIMP
TextEditor::Redo(uint32_t aCount)
{
// If we don't have transaction in the redo stack, we shouldn't notify
// anybody of trying to redo since it's not useful notification but we
// need to pay some runtime cost.
if (!CanRedo()) {
return NS_OK;
}
// If there is composition, we shouldn't allow to redo with committing
// composition since Chrome doesn't allow it and it doesn't make sense
// because committing composition causes removing all transactions from
// the redo queue. So, it becomes impossible to redo anything.
if (GetComposition()) {
return NS_OK;
}
// Protect the edit rules object from dying.
RefPtr<TextEditRules> rules(mRules);
AutoUpdateViewBatch beginViewBatching(this);
NotifyEditorObservers(eNotifyEditorObserversOfBefore);
if (NS_WARN_IF(!CanRedo()) || NS_WARN_IF(Destroyed())) {
return NS_ERROR_FAILURE;
}
nsresult rv;
{
AutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone);
RulesInfo ruleInfo(EditAction::redo);
RefPtr<Selection> selection = GetSelection();
bool cancel, handled;
rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
if (!cancel && NS_SUCCEEDED(rv)) {
RefPtr<TransactionManager> transactionManager(mTransactionManager);
for (uint32_t i = 0; i < aCount; ++i) {
nsresult rv = transactionManager->Redo();
if (NS_WARN_IF(NS_FAILED(rv))) {
break;
}
DoAfterRedoTransaction();
}
rv = rules->DidDoAction(selection, &ruleInfo, rv);
}
}
NotifyEditorObservers(eNotifyEditorObserversOfEnd);
return rv;
}
bool
TextEditor::CanCutOrCopy(PasswordFieldAllowed aPasswordFieldAllowed)
{
RefPtr<Selection> selection = GetSelection();
if (!selection) {
return false;
}
if (aPasswordFieldAllowed == ePasswordFieldNotAllowed &&
IsPasswordEditor()) {
return false;
}
return !selection->IsCollapsed();
}
bool
TextEditor::FireClipboardEvent(EventMessage aEventMessage,
int32_t aSelectionType,
bool* aActionTaken)
{
if (aEventMessage == ePaste) {
CommitComposition();
}
nsCOMPtr<nsIPresShell> presShell = GetPresShell();
NS_ENSURE_TRUE(presShell, false);
RefPtr<Selection> selection = GetSelection();
if (!selection) {
return false;
}
if (!nsCopySupport::FireClipboardEvent(aEventMessage, aSelectionType,
presShell, selection, aActionTaken)) {
return false;
}
// If the event handler caused the editor to be destroyed, return false.
// Otherwise return true to indicate that the event was not cancelled.
return !mDidPreDestroy;
}
NS_IMETHODIMP
TextEditor::Cut()
{
bool actionTaken = false;
if (FireClipboardEvent(eCut, nsIClipboard::kGlobalClipboard, &actionTaken)) {
DeleteSelectionAsAction(eNone, eStrip);
}
return actionTaken ? NS_OK : NS_ERROR_FAILURE;
}
NS_IMETHODIMP
TextEditor::CanCut(bool* aCanCut)
{
NS_ENSURE_ARG_POINTER(aCanCut);
// Cut is always enabled in HTML documents
nsCOMPtr<nsIDocument> doc = GetDocument();
*aCanCut = (doc && doc->IsHTMLOrXHTML()) ||
(IsModifiable() && CanCutOrCopy(ePasswordFieldNotAllowed));
return NS_OK;
}
NS_IMETHODIMP
TextEditor::Copy()
{
bool actionTaken = false;
FireClipboardEvent(eCopy, nsIClipboard::kGlobalClipboard, &actionTaken);
return actionTaken ? NS_OK : NS_ERROR_FAILURE;
}
NS_IMETHODIMP
TextEditor::CanCopy(bool* aCanCopy)
{
NS_ENSURE_ARG_POINTER(aCanCopy);
// Copy is always enabled in HTML documents
nsCOMPtr<nsIDocument> doc = GetDocument();
*aCanCopy = (doc && doc->IsHTMLOrXHTML()) ||
CanCutOrCopy(ePasswordFieldNotAllowed);
return NS_OK;
}
NS_IMETHODIMP
TextEditor::CanDelete(bool* aCanDelete)
{
NS_ENSURE_ARG_POINTER(aCanDelete);
*aCanDelete = IsModifiable() && CanCutOrCopy(ePasswordFieldAllowed);
return NS_OK;
}
// Used by OutputToString
already_AddRefed<nsIDocumentEncoder>
TextEditor::GetAndInitDocEncoder(const nsAString& aFormatType,
uint32_t aFlags,
const nsACString& aCharset)
{
nsCOMPtr<nsIDocumentEncoder> docEncoder;
if (!mCachedDocumentEncoder ||
!mCachedDocumentEncoderType.Equals(aFormatType)) {
nsAutoCString formatType(NS_DOC_ENCODER_CONTRACTID_BASE);
LossyAppendUTF16toASCII(aFormatType, formatType);
docEncoder = do_CreateInstance(formatType.get());
if (NS_WARN_IF(!docEncoder)) {
return nullptr;
}
mCachedDocumentEncoder = docEncoder;
mCachedDocumentEncoderType = aFormatType;
} else {
docEncoder = mCachedDocumentEncoder;
}
nsCOMPtr<nsIDocument> doc = GetDocument();
NS_ASSERTION(doc, "Need a document");
nsresult rv =
docEncoder->NativeInit(
doc, aFormatType,
aFlags | nsIDocumentEncoder::RequiresReinitAfterOutput);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
if (!aCharset.IsEmpty() && !aCharset.EqualsLiteral("null")) {
docEncoder->SetCharset(aCharset);
}
int32_t wc;
(void) GetWrapWidth(&wc);
if (wc >= 0) {
(void) docEncoder->SetWrapColumn(wc);
}
// Set the selection, if appropriate.
// We do this either if the OutputSelectionOnly flag is set,
// in which case we use our existing selection ...
if (aFlags & nsIDocumentEncoder::OutputSelectionOnly) {
RefPtr<Selection> selection = GetSelection();
if (NS_WARN_IF(!selection)) {
return nullptr;
}
rv = docEncoder->SetSelection(selection);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
}
// ... or if the root element is not a body,
// in which case we set the selection to encompass the root.
else {
dom::Element* rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return nullptr;
}
if (!rootElement->IsHTMLElement(nsGkAtoms::body)) {
rv = docEncoder->SetNativeContainerNode(rootElement);
if (NS_WARN_IF(NS_FAILED(rv))) {
return nullptr;
}
}
}
return docEncoder.forget();
}
NS_IMETHODIMP
TextEditor::OutputToString(const nsAString& aFormatType,
uint32_t aFlags,
nsAString& aOutputString)
{
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
nsString resultString;
RulesInfo ruleInfo(EditAction::outputText);
ruleInfo.outString = &resultString;
ruleInfo.flags = aFlags;
// XXX Struct should store a nsAReadable*
nsAutoString str(aFormatType);
ruleInfo.outputFormat = &str;
bool cancel, handled;
nsresult rv = rules->WillDoAction(nullptr, &ruleInfo, &cancel, &handled);
if (cancel || NS_FAILED(rv)) {
return rv;
}
if (handled) {
// This case will get triggered by password fields.
aOutputString.Assign(*(ruleInfo.outString));
return rv;
}
nsAutoCString charsetStr;
rv = GetDocumentCharacterSet(charsetStr);
if (NS_FAILED(rv) || charsetStr.IsEmpty()) {
charsetStr.AssignLiteral("windows-1252");
}
nsCOMPtr<nsIDocumentEncoder> encoder =
GetAndInitDocEncoder(aFormatType, aFlags, charsetStr);
if (NS_WARN_IF(!encoder)) {
return NS_ERROR_FAILURE;
}
return encoder->EncodeToString(aOutputString);
}
NS_IMETHODIMP
TextEditor::InsertTextWithQuotations(const nsAString& aStringToInsert)
{
nsresult rv = InsertTextAsAction(aStringToInsert);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
TextEditor::PasteAsQuotation(int32_t aSelectionType)
{
// Get Clipboard Service
nsresult rv;
nsCOMPtr<nsIClipboard> clipboard(do_GetService("@mozilla.org/widget/clipboard;1", &rv));
NS_ENSURE_SUCCESS(rv, rv);
// Get the nsITransferable interface for getting the data from the clipboard
nsCOMPtr<nsITransferable> trans;
rv = PrepareTransferable(getter_AddRefs(trans));
if (NS_SUCCEEDED(rv) && trans) {
// Get the Data from the clipboard
clipboard->GetData(trans, aSelectionType);
// Now we ask the transferable for the data
// it still owns the data, we just have a pointer to it.
// If it can't support a "text" output of the data the call will fail
nsCOMPtr<nsISupports> genericDataObj;
uint32_t len;
nsAutoCString flav;
rv = trans->GetAnyTransferData(flav, getter_AddRefs(genericDataObj),
&len);
if (NS_FAILED(rv)) {
return rv;
}
if (flav.EqualsLiteral(kUnicodeMime) ||
flav.EqualsLiteral(kMozTextInternal)) {
nsCOMPtr<nsISupportsString> textDataObj ( do_QueryInterface(genericDataObj) );
if (textDataObj && len > 0) {
nsAutoString stuffToPaste;
textDataObj->GetData ( stuffToPaste );
AutoPlaceholderBatch beginBatching(this);
rv = InsertAsQuotation(stuffToPaste, 0);
}
}
}
return rv;
}
NS_IMETHODIMP
TextEditor::InsertAsQuotation(const nsAString& aQuotedText,
nsIDOMNode** aNodeInserted)
{
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
// Let the citer quote it for us:
nsString quotedStuff;
nsresult rv = InternetCiter::GetCiteString(aQuotedText, quotedStuff);
NS_ENSURE_SUCCESS(rv, rv);
// It's best to put a blank line after the quoted text so that mails
// written without thinking won't be so ugly.
if (!aQuotedText.IsEmpty() && (aQuotedText.Last() != char16_t('\n'))) {
quotedStuff.Append(char16_t('\n'));
}
// get selection
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER);
AutoPlaceholderBatch beginBatching(this);
AutoRules beginRulesSniffing(this, EditAction::insertText, nsIEditor::eNext);
// give rules a chance to handle or cancel
RulesInfo ruleInfo(EditAction::insertElement);
bool cancel, handled;
rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled);
NS_ENSURE_SUCCESS(rv, rv);
if (cancel) {
return NS_OK; // Rules canceled the operation.
}
if (!handled) {
rv = InsertTextAsAction(quotedStuff);
NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "Failed to insert quoted text");
// XXX Should set *aNodeInserted to the first node inserted
if (aNodeInserted && NS_SUCCEEDED(rv)) {
*aNodeInserted = nullptr;
}
}
return rv;
}
NS_IMETHODIMP
TextEditor::PasteAsCitedQuotation(const nsAString& aCitation,
int32_t aSelectionType)
{
return NS_ERROR_NOT_IMPLEMENTED;
}
NS_IMETHODIMP
TextEditor::InsertAsCitedQuotation(const nsAString& aQuotedText,
const nsAString& aCitation,
bool aInsertHTML,
nsIDOMNode** aNodeInserted)
{
return InsertAsQuotation(aQuotedText, aNodeInserted);
}
nsresult
TextEditor::SharedOutputString(uint32_t aFlags,
bool* aIsCollapsed,
nsAString& aResult)
{
RefPtr<Selection> selection = GetSelection();
NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED);
*aIsCollapsed = selection->IsCollapsed();
if (!*aIsCollapsed) {
aFlags |= nsIDocumentEncoder::OutputSelectionOnly;
}
// If the selection isn't collapsed, we'll use the whole document.
return OutputToString(NS_LITERAL_STRING("text/plain"), aFlags, aResult);
}
NS_IMETHODIMP
TextEditor::Rewrap(bool aRespectNewlines)
{
int32_t wrapCol;
nsresult rv = GetWrapWidth(&wrapCol);
NS_ENSURE_SUCCESS(rv, NS_OK);
// Rewrap makes no sense if there's no wrap column; default to 72.
if (wrapCol <= 0) {
wrapCol = 72;
}
nsAutoString current;
bool isCollapsed;
rv = SharedOutputString(nsIDocumentEncoder::OutputFormatted
| nsIDocumentEncoder::OutputLFLineBreak,
&isCollapsed, current);
NS_ENSURE_SUCCESS(rv, rv);
nsString wrapped;
uint32_t firstLineOffset = 0; // XXX need to reset this if there is a selection
rv = InternetCiter::Rewrap(current, wrapCol, firstLineOffset,
aRespectNewlines, wrapped);
NS_ENSURE_SUCCESS(rv, rv);
if (isCollapsed) {
SelectAll();
}
return InsertTextWithQuotations(wrapped);
}
NS_IMETHODIMP
TextEditor::StripCites()
{
nsAutoString current;
bool isCollapsed;
nsresult rv = SharedOutputString(nsIDocumentEncoder::OutputFormatted,
&isCollapsed, current);
NS_ENSURE_SUCCESS(rv, rv);
nsString stripped;
rv = InternetCiter::StripCites(current, stripped);
NS_ENSURE_SUCCESS(rv, rv);
if (isCollapsed) {
rv = SelectAll();
NS_ENSURE_SUCCESS(rv, rv);
}
rv = InsertTextAsAction(stripped);
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
return NS_OK;
}
NS_IMETHODIMP
TextEditor::GetEmbeddedObjects(nsIArray** aNodeList)
{
if (NS_WARN_IF(!aNodeList)) {
return NS_ERROR_INVALID_ARG;
}
*aNodeList = nullptr;
return NS_OK;
}
/**
* All editor operations which alter the doc should be prefaced
* with a call to StartOperation, naming the action and direction.
*/
nsresult
TextEditor::StartOperation(EditAction opID,
nsIEditor::EDirection aDirection)
{
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
EditorBase::StartOperation(opID, aDirection); // will set mAction, mDirection
if (rules) {
return rules->BeforeEdit(mAction, mDirection);
}
return NS_OK;
}
/**
* All editor operations which alter the doc should be followed
* with a call to EndOperation.
*/
nsresult
TextEditor::EndOperation()
{
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
// post processing
nsresult rv = rules ? rules->AfterEdit(mAction, mDirection) : NS_OK;
EditorBase::EndOperation(); // will clear mAction, mDirection
return rv;
}
nsresult
TextEditor::SelectEntireDocument(Selection* aSelection)
{
if (!aSelection || !mRules) {
return NS_ERROR_NULL_POINTER;
}
// Protect the edit rules object from dying
RefPtr<TextEditRules> rules(mRules);
// is doc empty?
if (rules->DocumentIsEmpty()) {
// get root node
Element* rootElement = GetRoot();
if (NS_WARN_IF(!rootElement)) {
return NS_ERROR_FAILURE;
}
// if it's empty don't select entire doc - that would select the bogus node
return aSelection->Collapse(rootElement, 0);
}
SelectionBatcher selectionBatcher(aSelection);
nsresult rv = EditorBase::SelectEntireDocument(aSelection);
NS_ENSURE_SUCCESS(rv, rv);
// Don't select the trailing BR node if we have one
nsCOMPtr<nsIContent> childNode;
rv = GetEndChildNode(aSelection, getter_AddRefs(childNode));
if (NS_WARN_IF(NS_FAILED(rv))) {
return rv;
}
if (childNode) {
childNode = childNode->GetPreviousSibling();
}
if (childNode && TextEditUtils::IsMozBR(childNode)) {
int32_t parentOffset;
nsINode* parentNode = GetNodeLocation(childNode, &parentOffset);
return aSelection->Extend(parentNode, parentOffset);
}
return NS_OK;
}
EventTarget*
TextEditor::GetDOMEventTarget()
{
return mEventTarget;
}
nsresult
TextEditor::SetAttributeOrEquivalent(Element* aElement,
nsAtom* aAttribute,
const nsAString& aValue,
bool aSuppressTransaction)
{
if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) {
return NS_ERROR_INVALID_ARG;
}
return SetAttributeWithTransaction(*aElement, *aAttribute, aValue);
}
nsresult
TextEditor::RemoveAttributeOrEquivalent(Element* aElement,
nsAtom* aAttribute,
bool aSuppressTransaction)
{
if (NS_WARN_IF(!aElement) || NS_WARN_IF(!aAttribute)) {
return NS_ERROR_INVALID_ARG;
}
return RemoveAttributeWithTransaction(*aElement, *aAttribute);
}
} // namespace mozilla