diff options
Diffstat (limited to 'editor/libeditor/TextEditor.cpp')
-rw-r--r-- | editor/libeditor/TextEditor.cpp | 1638 |
1 files changed, 1638 insertions, 0 deletions
diff --git a/editor/libeditor/TextEditor.cpp b/editor/libeditor/TextEditor.cpp new file mode 100644 index 000000000..8fe824e11 --- /dev/null +++ b/editor/libeditor/TextEditor.cpp @@ -0,0 +1,1638 @@ +/* -*- 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 "InternetCiter.h" +#include "TextEditUtils.h" +#include "gfxFontUtils.h" +#include "mozilla/Assertions.h" +#include "mozilla/EditorUtils.h" // AutoEditBatch, AutoRules +#include "mozilla/mozalloc.h" +#include "mozilla/Preferences.h" +#include "mozilla/TextEditRules.h" +#include "mozilla/TextComposition.h" +#include "mozilla/TextEvents.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 "nsCopySupport.h" +#include "nsDebug.h" +#include "nsDependentSubstring.h" +#include "nsError.h" +#include "nsGkAtoms.h" +#include "nsIClipboard.h" +#include "nsIContent.h" +#include "nsIContentIterator.h" +#include "nsIDOMCharacterData.h" +#include "nsIDOMDocument.h" +#include "nsIDOMElement.h" +#include "nsIDOMEventTarget.h" +#include "nsIDOMKeyEvent.h" +#include "nsIDOMNode.h" +#include "nsIDOMNodeList.h" +#include "nsIDocumentEncoder.h" +#include "nsIEditorIMESupport.h" +#include "nsIEditRules.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 "nsSubstringTuple.h" +#include "nsUnicharUtils.h" +#include "nsXPCOM.h" + +class nsIOutputStream; +class nsISupports; + +namespace mozilla { + +using namespace dom; + +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_END + +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN_INHERITED(TextEditor, EditorBase) + NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mRules) +NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END + +NS_IMPL_ADDREF_INHERITED(TextEditor, EditorBase) +NS_IMPL_RELEASE_INHERITED(TextEditor, EditorBase) + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION_INHERITED(TextEditor) + NS_INTERFACE_MAP_ENTRY(nsIPlaintextEditor) + NS_INTERFACE_MAP_ENTRY(nsIEditorMailSupport) +NS_INTERFACE_MAP_END_INHERITING(EditorBase) + + +NS_IMETHODIMP +TextEditor::Init(nsIDOMDocument* aDoc, + nsIContent* aRoot, + nsISelectionController* aSelCon, + uint32_t aFlags, + const nsAString& aInitialValue) +{ + NS_PRECONDITION(aDoc, "bad arg"); + NS_ENSURE_TRUE(aDoc, NS_ERROR_NULL_POINTER); + + 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::RegisterCallback(EditorPrefsChangedCallback, + "editor.singleLine.pasteNewlines"); + EditorPrefsChangedCallback("editor.singleLine.pasteNewlines", nullptr); + Preferences::RegisterCallback(EditorPrefsChangedCallback, + "layout.selection.caret_style"); + EditorPrefsChangedCallback("layout.selection.caret_style", nullptr); + } + + aNewlineHandling = sNewlineHandlingPref; + aCaretStyle = sCaretStylePref; +} + +void +TextEditor::BeginEditorInit() +{ + mInitTriggerCounter++; +} + +nsresult +TextEditor::EndEditorInit() +{ + NS_PRECONDITION(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. + EnableUndo(false); + EnableUndo(true); + 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<nsIDOMDocument> domdoc = GetDOMDocument(); + NS_ENSURE_TRUE(domdoc, NS_ERROR_NOT_INITIALIZED); + + if (UpdateMetaCharset(domdoc, characterSet)) { + return NS_OK; + } + + nsCOMPtr<nsIDOMNodeList> headList; + rv = domdoc->GetElementsByTagName(NS_LITERAL_STRING("head"), getter_AddRefs(headList)); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(headList, NS_OK); + + nsCOMPtr<nsIDOMNode> headNode; + headList->Item(0, getter_AddRefs(headNode)); + NS_ENSURE_TRUE(headNode, NS_OK); + + // Create a new meta charset tag + nsCOMPtr<nsIDOMNode> resultNode; + rv = CreateNode(NS_LITERAL_STRING("meta"), headNode, 0, getter_AddRefs(resultNode)); + NS_ENSURE_SUCCESS(rv, NS_ERROR_FAILURE); + NS_ENSURE_TRUE(resultNode, NS_OK); + + // Set attributes to the created element + if (characterSet.IsEmpty()) { + return NS_OK; + } + + nsCOMPtr<dom::Element> metaElement = do_QueryInterface(resultNode); + if (!metaElement) { + return NS_OK; + } + + // not undoable, undo should undo CreateNode + 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(nsIDOMDocument* aDocument, + const nsACString& aCharacterSet) +{ + MOZ_ASSERT(aDocument); + // get a list of META tags + nsCOMPtr<nsIDOMNodeList> list; + nsresult rv = aDocument->GetElementsByTagName(NS_LITERAL_STRING("meta"), + getter_AddRefs(list)); + NS_ENSURE_SUCCESS(rv, false); + NS_ENSURE_TRUE(list, false); + + nsCOMPtr<nsINodeList> metaList = do_QueryInterface(list); + + uint32_t listLength = 0; + metaList->GetLength(&listLength); + + for (uint32_t i = 0; i < listLength; ++i) { + nsCOMPtr<nsIContent> metaNode = metaList->Item(i); + MOZ_ASSERT(metaNode); + + if (!metaNode->IsElement()) { + continue; + } + + nsAutoString currentValue; + metaNode->GetAttr(kNameSpaceID_None, nsGkAtoms::httpEquiv, currentValue); + + if (!FindInReadable(NS_LITERAL_STRING("content-type"), + currentValue, + nsCaseInsensitiveStringComparator())) { + continue; + } + + metaNode->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 + nsCOMPtr<nsIDOMElement> metaElement = do_QueryInterface(metaNode); + MOZ_ASSERT(metaElement); + rv = EditorBase::SetAttribute(metaElement, NS_LITERAL_STRING("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); +} + + +NS_IMETHODIMP +TextEditor::GetIsDocumentEditable(bool* aIsDocumentEditable) +{ + NS_ENSURE_ARG_POINTER(aIsDocumentEditable); + + nsCOMPtr<nsIDOMDocument> doc = GetDOMDocument(); + *aIsDocumentEditable = doc && IsModifiable(); + + return NS_OK; +} + +bool +TextEditor::IsModifiable() +{ + return !IsReadonly(); +} + +nsresult +TextEditor::HandleKeyPressEvent(nsIDOMKeyEvent* aKeyEvent) +{ + // 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(aKeyEvent); + } + + WidgetKeyboardEvent* nativeKeyEvent = + aKeyEvent->AsEvent()->WidgetEventPtr()->AsKeyboardEvent(); + NS_ENSURE_TRUE(nativeKeyEvent, NS_ERROR_UNEXPECTED); + NS_ASSERTION(nativeKeyEvent->mMessage == eKeyPress, + "HandleKeyPressEvent gets non-keypress event"); + + switch (nativeKeyEvent->mKeyCode) { + case NS_VK_META: + case NS_VK_WIN: + case NS_VK_SHIFT: + case NS_VK_CONTROL: + case NS_VK_ALT: + case NS_VK_BACK: + case NS_VK_DELETE: + // These keys are handled on EditorBase + return EditorBase::HandleKeyPressEvent(aKeyEvent); + case NS_VK_TAB: { + if (IsTabbable()) { + return NS_OK; // let it be used for focus switching + } + + if (nativeKeyEvent->IsShift() || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + return NS_OK; + } + + // else we insert the tab straight through + aKeyEvent->AsEvent()->PreventDefault(); + return TypedText(NS_LITERAL_STRING("\t"), eTypedText); + } + case NS_VK_RETURN: + if (IsSingleLineEditor() || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + return NS_OK; + } + aKeyEvent->AsEvent()->PreventDefault(); + return TypedText(EmptyString(), eTypedBreak); + } + + // NOTE: On some keyboard layout, some characters are inputted with Control + // key or Alt key, but at that time, widget sets FALSE to these keys. + if (!nativeKeyEvent->mCharCode || nativeKeyEvent->IsControl() || + nativeKeyEvent->IsAlt() || nativeKeyEvent->IsMeta() || + nativeKeyEvent->IsOS()) { + // we don't PreventDefault() here or keybindings like control-x won't work + return NS_OK; + } + aKeyEvent->AsEvent()->PreventDefault(); + nsAutoString str(nativeKeyEvent->mCharCode); + return TypedText(str, eTypedText); +} + +/* This routine is needed to provide a bottleneck for typing for logging + purposes. Can't use HandleKeyPress() (above) for that since it takes + a nsIDOMKeyEvent* parameter. So instead we pass enough info through + to TypedText() to determine what action to take, but without passing + an event. + */ +NS_IMETHODIMP +TextEditor::TypedText(const nsAString& aString, ETypingAction aAction) +{ + AutoPlaceHolderBatch batch(this, nsGkAtoms::TypingTxnName); + + switch (aAction) { + case eTypedText: + return InsertText(aString); + case eTypedBreak: + return InsertLineBreak(); + default: + // eTypedBR is only for HTML + return NS_ERROR_FAILURE; + } +} + +already_AddRefed<Element> +TextEditor::CreateBRImpl(nsCOMPtr<nsINode>* aInOutParent, + int32_t* aInOutOffset, + EDirection aSelect) +{ + nsCOMPtr<nsIDOMNode> parent(GetAsDOMNode(*aInOutParent)); + nsCOMPtr<nsIDOMNode> br; + // We ignore the retval, and assume it's fine if the br is non-null + CreateBRImpl(address_of(parent), aInOutOffset, address_of(br), aSelect); + *aInOutParent = do_QueryInterface(parent); + nsCOMPtr<Element> ret(do_QueryInterface(br)); + return ret.forget(); +} + +nsresult +TextEditor::CreateBRImpl(nsCOMPtr<nsIDOMNode>* aInOutParent, + int32_t* aInOutOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + EDirection aSelect) +{ + NS_ENSURE_TRUE(aInOutParent && *aInOutParent && aInOutOffset && outBRNode, NS_ERROR_NULL_POINTER); + *outBRNode = nullptr; + + // we need to insert a br. unfortunately, we may have to split a text node to do it. + nsCOMPtr<nsIDOMNode> node = *aInOutParent; + int32_t theOffset = *aInOutOffset; + nsCOMPtr<nsIDOMCharacterData> nodeAsText = do_QueryInterface(node); + NS_NAMED_LITERAL_STRING(brType, "br"); + nsCOMPtr<nsIDOMNode> brNode; + if (nodeAsText) { + int32_t offset; + uint32_t len; + nodeAsText->GetLength(&len); + nsCOMPtr<nsIDOMNode> tmp = GetNodeLocation(node, &offset); + NS_ENSURE_TRUE(tmp, NS_ERROR_FAILURE); + if (!theOffset) { + // we are already set to go + } else if (theOffset == (int32_t)len) { + // update offset to point AFTER the text node + offset++; + } else { + // split the text node + nsresult rv = SplitNode(node, theOffset, getter_AddRefs(tmp)); + NS_ENSURE_SUCCESS(rv, rv); + tmp = GetNodeLocation(node, &offset); + } + // create br + nsresult rv = CreateNode(brType, tmp, offset, getter_AddRefs(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + *aInOutParent = tmp; + *aInOutOffset = offset+1; + } else { + nsresult rv = CreateNode(brType, node, theOffset, getter_AddRefs(brNode)); + NS_ENSURE_SUCCESS(rv, rv); + (*aInOutOffset)++; + } + + *outBRNode = brNode; + if (*outBRNode && (aSelect != eNone)) { + int32_t offset; + nsCOMPtr<nsIDOMNode> parent = GetNodeLocation(*outBRNode, &offset); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + if (aSelect == eNext) { + // position selection after br + selection->SetInterlinePosition(true); + selection->Collapse(parent, offset + 1); + } else if (aSelect == ePrevious) { + // position selection before br + selection->SetInterlinePosition(true); + selection->Collapse(parent, offset); + } + } + return NS_OK; +} + + +NS_IMETHODIMP +TextEditor::CreateBR(nsIDOMNode* aNode, + int32_t aOffset, + nsCOMPtr<nsIDOMNode>* outBRNode, + EDirection aSelect) +{ + nsCOMPtr<nsIDOMNode> parent = aNode; + int32_t offset = aOffset; + return CreateBRImpl(address_of(parent), &offset, outBRNode, aSelect); +} + +nsresult +TextEditor::InsertBR(nsCOMPtr<nsIDOMNode>* outBRNode) +{ + NS_ENSURE_TRUE(outBRNode, NS_ERROR_NULL_POINTER); + *outBRNode = nullptr; + + // calling it text insertion to trigger moz br treatment by rules + AutoRules beginRulesSniffing(this, EditAction::insertText, nsIEditor::eNext); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + if (!selection->Collapsed()) { + nsresult rv = DeleteSelection(nsIEditor::eNone, nsIEditor::eStrip); + NS_ENSURE_SUCCESS(rv, rv); + } + + nsCOMPtr<nsIDOMNode> selNode; + int32_t selOffset; + nsresult rv = + GetStartNodeAndOffset(selection, getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + + rv = CreateBR(selNode, selOffset, outBRNode); + NS_ENSURE_SUCCESS(rv, rv); + + // position selection after br + selNode = GetNodeLocation(*outBRNode, &selOffset); + selection->SetInterlinePosition(true); + return selection->Collapse(selNode, selOffset+1); +} + +nsresult +TextEditor::ExtendSelectionForDelete(Selection* aSelection, + nsIEditor::EDirection* aAction) +{ + bool bCollapsed = aSelection->Collapsed(); + + 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); + + nsresult rv; + switch (*aAction) { + case eNextWord: + rv = selCont->WordExtendForDelete(true); + // DeleteSelectionImpl doesn't handle these actions + // because it's inside batching, so don't confuse it: + *aAction = eNone; + break; + case ePreviousWord: + rv = selCont->WordExtendForDelete(false); + *aAction = eNone; + break; + case eNext: + rv = selCont->CharacterExtendForDelete(); + // Don't set aAction to eNone (see Bug 502259) + break; + 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. + nsCOMPtr<nsIDOMNode> node; + int32_t offset; + rv = GetStartNodeAndOffset(aSelection, getter_AddRefs(node), &offset); + NS_ENSURE_SUCCESS(rv, rv); + NS_ENSURE_TRUE(node, NS_ERROR_FAILURE); + + // node might be anonymous DIV, so we find better text node + FindBetterInsertionPoint(node, offset); + + if (IsTextNode(node)) { + nsCOMPtr<nsIDOMCharacterData> charData = do_QueryInterface(node); + if (charData) { + nsAutoString data; + rv = charData->GetData(data); + NS_ENSURE_SUCCESS(rv, rv); + + if ((offset > 1 && + NS_IS_LOW_SURROGATE(data[offset - 1]) && + NS_IS_HIGH_SURROGATE(data[offset - 2])) || + (offset > 0 && + gfxFontUtils::IsVarSelector(data[offset - 1]))) { + rv = selCont->CharacterExtendForBackspace(); + } + } + } + break; + } + case eToBeginningOfLine: + selCont->IntraLineMove(true, false); // try to move to end + rv = selCont->IntraLineMove(false, true); // select to beginning + *aAction = eNone; + break; + case eToEndOfLine: + rv = selCont->IntraLineMove(true, true); + *aAction = eNext; + break; + default: // avoid several compiler warnings + rv = NS_OK; + break; + } + return rv; + } + return NS_OK; +} + +nsresult +TextEditor::DeleteSelection(EDirection aAction, + EStripWrappers aStripWrappers) +{ + MOZ_ASSERT(aStripWrappers == eStrip || aStripWrappers == eNoStrip); + + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + // delete placeholder txns merge. + AutoPlaceHolderBatch batch(this, nsGkAtoms::DeleteTxnName); + AutoRules beginRulesSniffing(this, EditAction::deleteSelection, aAction); + + // 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->Collapsed() && + (aAction == eNextWord || aAction == ePreviousWord || + aAction == eToBeginningOfLine || aAction == eToEndOfLine)) { + if (mCaretStyle == 1) { + nsresult rv = selection->CollapseToStart(); + NS_ENSURE_SUCCESS(rv, rv); + } else { + aAction = eNone; + } + } + + TextRulesInfo ruleInfo(EditAction::deleteSelection); + ruleInfo.collapsedAction = aAction; + ruleInfo.stripWrappers = aStripWrappers; + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + rv = DeleteSelectionImpl(aAction, aStripWrappers); + } + if (!cancel) { + // post-process + rv = rules->DidDoAction(selection, &ruleInfo, rv); + } + return rv; +} + +NS_IMETHODIMP +TextEditor::InsertText(const nsAString& aStringToInsert) +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + EditAction opID = EditAction::insertText; + if (ShouldHandleIMEComposition()) { + opID = EditAction::insertIMEText; + } + AutoPlaceHolderBatch batch(this, nullptr); + AutoRules beginRulesSniffing(this, opID, nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + nsAutoString resultString; + // XXX can we trust instring to outlive ruleInfo, + // XXX and ruleInfo not to refer to instring in its dtor? + //nsAutoString instring(aStringToInsert); + TextRulesInfo ruleInfo(opID); + ruleInfo.inString = &aStringToInsert; + ruleInfo.outString = &resultString; + ruleInfo.maxLength = mMaxTextLength; + + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, 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, rv); +} + +NS_IMETHODIMP +TextEditor::InsertLineBreak() +{ + if (!mRules) { + return NS_ERROR_NOT_INITIALIZED; + } + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertBreak, nsIEditor::eNext); + + // pre-process + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_TRUE(selection, NS_ERROR_NULL_POINTER); + + TextRulesInfo ruleInfo(EditAction::insertBreak); + ruleInfo.maxLength = mMaxTextLength; + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + NS_ENSURE_SUCCESS(rv, rv); + if (!cancel && !handled) { + // get the (collapsed) selection location + NS_ENSURE_STATE(selection->GetRangeAt(0)); + nsCOMPtr<nsINode> selNode = selection->GetRangeAt(0)->GetStartParent(); + int32_t selOffset = selection->GetRangeAt(0)->StartOffset(); + NS_ENSURE_STATE(selNode); + + // don't put text in places that can't have it + if (!IsTextNode(selNode) && !CanContainTag(*selNode, + *nsGkAtoms::textTagName)) { + return NS_ERROR_FAILURE; + } + + // we need to get the doc + nsCOMPtr<nsIDocument> doc = GetDocument(); + NS_ENSURE_TRUE(doc, NS_ERROR_NOT_INITIALIZED); + + // don't spaz my selection in subtransactions + AutoTransactionsConserveSelection dontSpazMySelection(this); + + // insert a linefeed character + rv = InsertTextImpl(NS_LITERAL_STRING("\n"), address_of(selNode), + &selOffset, doc); + if (!selNode) { + rv = NS_ERROR_NULL_POINTER; // don't return here, so DidDoAction is called + } + if (NS_SUCCEEDED(rv)) { + // set the selection to the correct location + rv = selection->Collapse(selNode, selOffset); + if (NS_SUCCEEDED(rv)) { + // see if we're at the end of the editor range + nsCOMPtr<nsIDOMNode> endNode; + int32_t endOffset; + rv = GetEndNodeAndOffset(selection, + getter_AddRefs(endNode), &endOffset); + + if (NS_SUCCEEDED(rv) && + endNode == GetAsDOMNode(selNode) && endOffset == selOffset) { + // 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); + } + } + } + } + + if (!cancel) { + // post-process, always called if WillInsertBreak didn't return cancel==true + rv = rules->DidDoAction(selection, &ruleInfo, rv); + } + return rv; +} + +nsresult +TextEditor::BeginIMEComposition(WidgetCompositionEvent* aEvent) +{ + NS_ENSURE_TRUE(!mComposition, NS_OK); + + if (IsPasswordEditor()) { + NS_ENSURE_TRUE(mRules, NS_ERROR_NULL_POINTER); + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + TextEditRules* textEditRules = static_cast<TextEditRules*>(rules.get()); + textEditRules->ResetIMETextPWBuf(); + } + + return EditorBase::BeginIMEComposition(aEvent); +} + +nsresult +TextEditor::UpdateIMEComposition(nsIDOMEvent* aDOMTextEvent) +{ + MOZ_ASSERT(aDOMTextEvent, "aDOMTextEvent must not be nullptr"); + + WidgetCompositionEvent* compositionChangeEvent = + aDOMTextEvent->WidgetEventPtr()->AsCompositionEvent(); + NS_ENSURE_TRUE(compositionChangeEvent, NS_ERROR_INVALID_ARG); + MOZ_ASSERT(compositionChangeEvent->mMessage == eCompositionChange, + "The internal event should be eCompositionChange"); + + if (!EnsureComposition(compositionChangeEvent)) { + return NS_OK; + } + + nsCOMPtr<nsIPresShell> ps = GetPresShell(); + NS_ENSURE_TRUE(ps, NS_ERROR_NOT_INITIALIZED); + + RefPtr<Selection> selection = GetSelection(); + NS_ENSURE_STATE(selection); + + // 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, compositionChangeEvent); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + + RefPtr<nsCaret> caretP = ps->GetCaret(); + + nsresult rv; + { + AutoPlaceHolderBatch batch(this, nsGkAtoms::IMETxnName); + + rv = InsertText(compositionChangeEvent->mData); + + 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 (!compositionChangeEvent->IsFollowedByCompositionEnd()) { + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + } + + return rv; +} + +already_AddRefed<nsIContent> +TextEditor::GetInputEventTargetContent() +{ + nsCOMPtr<nsIContent> target = do_QueryInterface(mEventTarget); + return target.forget(); +} + +NS_IMETHODIMP +TextEditor::GetDocumentIsEmpty(bool* aDocumentIsEmpty) +{ + NS_ENSURE_TRUE(aDocumentIsEmpty, NS_ERROR_NULL_POINTER); + + NS_ENSURE_TRUE(mRules, NS_ERROR_NOT_INITIALIZED); + + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + return rules->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<nsIDOMNode> currentNode = do_QueryInterface(iter->GetCurrentNode()); + nsCOMPtr<nsIDOMCharacterData> textNode = do_QueryInterface(currentNode); + if (textNode && IsEditable(currentNode)) { + uint32_t length; + textNode->GetLength(&length); + totalLength += length; + } + } + + *aCount = totalLength; + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::SetMaxTextLength(int32_t aMaxTextLength) +{ + mMaxTextLength = aMaxTextLength; + return NS_OK; +} + +NS_IMETHODIMP +TextEditor::GetMaxTextLength(int32_t* aMaxTextLength) +{ + NS_ENSURE_TRUE(aMaxTextLength, NS_ERROR_INVALID_POINTER); + *aMaxTextLength = mMaxTextLength; + 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) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + AutoUpdateViewBatch beginViewBatching(this); + + ForceCompositionEnd(); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + + AutoRules beginRulesSniffing(this, EditAction::undo, nsIEditor::eNone); + + TextRulesInfo ruleInfo(EditAction::undo); + RefPtr<Selection> selection = GetSelection(); + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + + if (!cancel && NS_SUCCEEDED(rv)) { + rv = EditorBase::Undo(aCount); + rv = rules->DidDoAction(selection, &ruleInfo, rv); + } + + NotifyEditorObservers(eNotifyEditorObserversOfEnd); + return rv; +} + +NS_IMETHODIMP +TextEditor::Redo(uint32_t aCount) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + AutoUpdateViewBatch beginViewBatching(this); + + ForceCompositionEnd(); + + NotifyEditorObservers(eNotifyEditorObserversOfBefore); + + AutoRules beginRulesSniffing(this, EditAction::redo, nsIEditor::eNone); + + TextRulesInfo ruleInfo(EditAction::redo); + RefPtr<Selection> selection = GetSelection(); + bool cancel, handled; + nsresult rv = rules->WillDoAction(selection, &ruleInfo, &cancel, &handled); + + if (!cancel && NS_SUCCEEDED(rv)) { + rv = EditorBase::Redo(aCount); + 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->Collapsed(); +} + +bool +TextEditor::FireClipboardEvent(EventMessage aEventMessage, + int32_t aSelectionType, + bool* aActionTaken) +{ + if (aEventMessage == ePaste) { + ForceCompositionEnd(); + } + + 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)) { + DeleteSelection(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; +} + +// Shared between OutputToString and OutputToStream +NS_IMETHODIMP +TextEditor::GetAndInitDocEncoder(const nsAString& aFormatType, + uint32_t aFlags, + const nsACString& aCharset, + nsIDocumentEncoder** encoder) +{ + nsresult rv = NS_OK; + + nsAutoCString formatType(NS_DOC_ENCODER_CONTRACTID_BASE); + LossyAppendUTF16toASCII(aFormatType, formatType); + nsCOMPtr<nsIDocumentEncoder> docEncoder (do_CreateInstance(formatType.get(), &rv)); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMDocument> domDoc = do_QueryReferent(mDocWeak); + NS_ASSERTION(domDoc, "Need a document"); + + rv = docEncoder->Init(domDoc, aFormatType, aFlags); + NS_ENSURE_SUCCESS(rv, rv); + + 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(); + NS_ENSURE_STATE(selection); + rv = docEncoder->SetSelection(selection); + NS_ENSURE_SUCCESS(rv, rv); + } + // ... 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(); + NS_ENSURE_TRUE(rootElement, NS_ERROR_FAILURE); + if (!rootElement->IsHTMLElement(nsGkAtoms::body)) { + rv = docEncoder->SetNativeContainerNode(rootElement); + NS_ENSURE_SUCCESS(rv, rv); + } + } + + docEncoder.forget(encoder); + return NS_OK; +} + + +NS_IMETHODIMP +TextEditor::OutputToString(const nsAString& aFormatType, + uint32_t aFlags, + nsAString& aOutputString) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> rules(mRules); + + nsString resultString; + TextRulesInfo ruleInfo(EditAction::outputText); + ruleInfo.outString = &resultString; + // 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("ISO-8859-1"); + } + + nsCOMPtr<nsIDocumentEncoder> encoder; + rv = GetAndInitDocEncoder(aFormatType, aFlags, charsetStr, getter_AddRefs(encoder)); + NS_ENSURE_SUCCESS(rv, rv); + return encoder->EncodeToString(aOutputString); +} + +NS_IMETHODIMP +TextEditor::OutputToStream(nsIOutputStream* aOutputStream, + const nsAString& aFormatType, + const nsACString& aCharset, + uint32_t aFlags) +{ + nsresult rv; + + // special-case for empty document when requesting plain text, + // to account for the bogus text node. + // XXX Should there be a similar test in OutputToString? + if (aFormatType.EqualsLiteral("text/plain")) { + bool docEmpty; + rv = GetDocumentIsEmpty(&docEmpty); + NS_ENSURE_SUCCESS(rv, rv); + + if (docEmpty) { + return NS_OK; // Output nothing. + } + } + + nsCOMPtr<nsIDocumentEncoder> encoder; + rv = GetAndInitDocEncoder(aFormatType, aFlags, aCharset, + getter_AddRefs(encoder)); + + NS_ENSURE_SUCCESS(rv, rv); + + return encoder->EncodeToStream(aOutputStream); +} + +NS_IMETHODIMP +TextEditor::InsertTextWithQuotations(const nsAString& aStringToInsert) +{ + return InsertText(aStringToInsert); +} + +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 ); + AutoEditBatch beginBatching(this); + rv = InsertAsQuotation(stuffToPaste, 0); + } + } + } + + return rv; +} + +NS_IMETHODIMP +TextEditor::InsertAsQuotation(const nsAString& aQuotedText, + nsIDOMNode** aNodeInserted) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> 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); + + AutoEditBatch beginBatching(this); + AutoRules beginRulesSniffing(this, EditAction::insertText, nsIEditor::eNext); + + // give rules a chance to handle or cancel + TextRulesInfo 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 = InsertText(quotedStuff); + + // 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->Collapsed(); + + 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); + } + + return InsertText(stripped); +} + +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. + */ +NS_IMETHODIMP +TextEditor::StartOperation(EditAction opID, + nsIEditor::EDirection aDirection) +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> 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. + */ +NS_IMETHODIMP +TextEditor::EndOperation() +{ + // Protect the edit rules object from dying + nsCOMPtr<nsIEditRules> 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 + nsCOMPtr<nsIEditRules> rules(mRules); + + // is doc empty? + bool bDocIsEmpty; + if (NS_SUCCEEDED(rules->DocumentIsEmpty(&bDocIsEmpty)) && bDocIsEmpty) { + // get root node + nsCOMPtr<nsIDOMElement> rootElement = do_QueryInterface(GetRoot()); + NS_ENSURE_TRUE(rootElement, 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 + int32_t selOffset; + nsCOMPtr<nsIDOMNode> selNode; + rv = GetEndNodeAndOffset(aSelection, getter_AddRefs(selNode), &selOffset); + NS_ENSURE_SUCCESS(rv, rv); + + nsCOMPtr<nsIDOMNode> childNode = GetChildAt(selNode, selOffset - 1); + + if (childNode && TextEditUtils::IsMozBR(childNode)) { + int32_t parentOffset; + nsCOMPtr<nsIDOMNode> parentNode = GetNodeLocation(childNode, &parentOffset); + + return aSelection->Extend(parentNode, parentOffset); + } + + return NS_OK; +} + +already_AddRefed<EventTarget> +TextEditor::GetDOMEventTarget() +{ + nsCOMPtr<EventTarget> copy = mEventTarget; + return copy.forget(); +} + + +nsresult +TextEditor::SetAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + const nsAString& aValue, + bool aSuppressTransaction) +{ + return EditorBase::SetAttribute(aElement, aAttribute, aValue); +} + +nsresult +TextEditor::RemoveAttributeOrEquivalent(nsIDOMElement* aElement, + const nsAString& aAttribute, + bool aSuppressTransaction) +{ + return EditorBase::RemoveAttribute(aElement, aAttribute); +} + +} // namespace mozilla |