pjs/editor/base/nsTextEditRules.cpp

1330 строки
43 KiB
C++

/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*-
*
* The contents of this file are subject to the Netscape Public
* License Version 1.1 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.mozilla.org/NPL/
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* The Original Code is mozilla.org code.
*
* The Initial Developer of the Original Code is Netscape
* Communications Corporation. Portions created by Netscape are
* Copyright (C) 1998 Netscape Communications Corporation. All
* Rights Reserved.
*
* Contributor(s):
*/
#include "nsTextEditRules.h"
#include "nsEditor.h"
#include "nsHTMLEditUtils.h"
#include "nsCOMPtr.h"
#include "nsIDOMNode.h"
#include "nsIDOMElement.h"
#include "nsIDOMNodeList.h"
#include "nsISelection.h"
#include "nsISelectionPrivate.h"
#include "nsIDOMRange.h"
#include "nsIDOMCharacterData.h"
#include "nsIContent.h"
#include "nsIContentIterator.h"
#include "nsIEnumerator.h"
#include "nsLayoutCID.h"
#include "nsIEditProperty.h"
#include "nsEditorUtils.h"
#include "EditTxn.h"
#include "TypeInState.h"
#include "nsIPref.h"
#ifdef IBMBIDI
#include "nsIPresContext.h"
#include "nsIFrameSelection.h"
#endif // IBMBIDI
static NS_DEFINE_CID(kContentIteratorCID, NS_CONTENTITERATOR_CID);
static NS_DEFINE_IID(kRangeCID, NS_RANGE_CID);
static NS_DEFINE_CID(kPrefServiceCID, NS_PREF_CID);
#define CANCEL_OPERATION_IF_READONLY_OR_DISABLED \
if ((mFlags & nsIPlaintextEditor::eEditorReadonlyMask) || (mFlags & nsIPlaintextEditor::eEditorDisabledMask)) \
{ \
*aCancel = PR_TRUE; \
return NS_OK; \
};
nsresult
NS_NewTextEditRules(nsIEditRules** aInstancePtrResult)
{
nsTextEditRules * rules = new nsTextEditRules();
if (rules)
return rules->QueryInterface(NS_GET_IID(nsIEditRules), (void**) aInstancePtrResult);
return NS_ERROR_OUT_OF_MEMORY;
}
/********************************************************
* Constructor/Destructor
********************************************************/
nsTextEditRules::nsTextEditRules()
: mEditor(nsnull)
, mPasswordText()
, mBogusNode(nsnull)
, mBody(nsnull)
, mFlags(0) // initialized to 0 ("no flags set"). Real initial value is given in Init()
, mActionNesting(0)
, mLockRulesSniffing(PR_FALSE)
, mTheAction(0)
{
NS_INIT_REFCNT();
}
nsTextEditRules::~nsTextEditRules()
{
// do NOT delete mEditor here. We do not hold a ref count to mEditor. mEditor owns our lifespan.
}
/********************************************************
* XPCOM Cruft
********************************************************/
NS_IMPL_ADDREF(nsTextEditRules)
NS_IMPL_RELEASE(nsTextEditRules)
NS_IMPL_QUERY_INTERFACE1(nsTextEditRules, nsIEditRules)
/********************************************************
* Public methods
********************************************************/
NS_IMETHODIMP
nsTextEditRules::Init(nsPlaintextEditor *aEditor, PRUint32 aFlags)
{
if (!aEditor) { return NS_ERROR_NULL_POINTER; }
mEditor = aEditor; // we hold a non-refcounted reference back to our editor
// call SetFlags only aftet mEditor has been initialized!
SetFlags(aFlags);
nsCOMPtr<nsISelection> selection;
mEditor->GetSelection(getter_AddRefs(selection));
NS_ASSERTION(selection, "editor cannot get selection");
// remember our root node
nsCOMPtr<nsIDOMElement> bodyElement;
nsresult res = mEditor->GetRootElement(getter_AddRefs(bodyElement));
if (NS_FAILED(res)) return res;
if (!bodyElement) return NS_ERROR_NULL_POINTER;
mBody = do_QueryInterface(bodyElement);
if (!mBody) return NS_ERROR_FAILURE;
// put in a magic br if needed
res = CreateBogusNodeIfNeeded(selection); // this method handles null selection, which should never happen anyway
if (NS_FAILED(res)) return res;
// create a range that is the entire body contents
nsCOMPtr<nsIDOMRange> wholeDoc = do_CreateInstance(kRangeCID);
if (!wholeDoc) return NS_ERROR_NULL_POINTER;
wholeDoc->SetStart(mBody,0);
nsCOMPtr<nsIDOMNodeList> list;
res = mBody->GetChildNodes(getter_AddRefs(list));
if (NS_FAILED(res) || !list) return res?res:NS_ERROR_FAILURE;
PRUint32 listCount;
res = list->GetLength(&listCount);
if (NS_FAILED(res)) return res;
res = wholeDoc->SetEnd(mBody,listCount);
if (NS_FAILED(res)) return res;
// replace newlines in that range with breaks
res = ReplaceNewlines(wholeDoc);
return res;
}
NS_IMETHODIMP
nsTextEditRules::GetFlags(PRUint32 *aFlags)
{
if (!aFlags) { return NS_ERROR_NULL_POINTER; }
*aFlags = mFlags;
return NS_OK;
}
NS_IMETHODIMP
nsTextEditRules::SetFlags(PRUint32 aFlags)
{
if (mFlags == aFlags) return NS_OK;
// XXX - this won't work if body element already has
// a style attribute on it, don't know why.
// SetFlags() is really meant to only be called once
// and at editor init time.
PRBool willBePlaintext = (aFlags & nsIPlaintextEditor::eEditorPlaintextMask) != 0;
PRBool alreadyPlaintext = (mFlags & nsIPlaintextEditor::eEditorPlaintextMask) != 0;
mFlags = aFlags;
return NS_OK;
}
NS_IMETHODIMP
nsTextEditRules::BeforeEdit(PRInt32 action, nsIEditor::EDirection aDirection)
{
if (mLockRulesSniffing) return NS_OK;
nsAutoLockRulesSniffing lockIt(this);
if (!mActionNesting)
{
// let rules remember the top level action
mTheAction = action;
}
mActionNesting++;
return NS_OK;
}
NS_IMETHODIMP
nsTextEditRules::AfterEdit(PRInt32 action, nsIEditor::EDirection aDirection)
{
if (mLockRulesSniffing) return NS_OK;
nsAutoLockRulesSniffing lockIt(this);
NS_PRECONDITION(mActionNesting>0, "bad action nesting!");
nsresult res = NS_OK;
if (!--mActionNesting)
{
nsCOMPtr<nsISelection>selection;
res = mEditor->GetSelection(getter_AddRefs(selection));
if (NS_FAILED(res)) return res;
// detect empty doc
res = CreateBogusNodeIfNeeded(selection);
if (NS_FAILED(res)) return res;
#ifdef IBMBIDI
/* After inserting text the cursor Bidi level must be set to the level of the inserted text.
* This is difficult, because we cannot know what the level is until after the Bidi algorithm
* is applied to the whole paragraph.
*
* So we set the cursor Bidi level to UNDEFINED here, and the caret code will set it correctly later
*/
if (action == nsEditor::kOpInsertText) {
nsCOMPtr<nsIPresShell> shell;
mEditor->GetPresShell(getter_AddRefs(shell));
if (shell) {
shell->UndefineCursorBidiLevel();
}
}
#endif
}
return res;
}
NS_IMETHODIMP
nsTextEditRules::WillDoAction(nsISelection *aSelection,
nsRulesInfo *aInfo,
PRBool *aCancel,
PRBool *aHandled)
{
// null selection is legal
if (!aInfo || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; }
#if defined(DEBUG_ftang)
printf("nsTextEditRules::WillDoAction action= %d", aInfo->action);
#endif
*aCancel = PR_FALSE;
*aHandled = PR_FALSE;
// my kingdom for dynamic cast
nsTextRulesInfo *info = NS_STATIC_CAST(nsTextRulesInfo*, aInfo);
switch (info->action)
{
case kInsertBreak:
return WillInsertBreak(aSelection, aCancel, aHandled);
case kInsertText:
case kInsertTextIME:
return WillInsertText(info->action,
aSelection,
aCancel,
aHandled,
info->inString,
info->outString,
info->maxLength);
case kDeleteSelection:
return WillDeleteSelection(aSelection, info->collapsedAction, aCancel, aHandled);
case kUndo:
return WillUndo(aSelection, aCancel, aHandled);
case kRedo:
return WillRedo(aSelection, aCancel, aHandled);
case kSetTextProperty:
return WillSetTextProperty(aSelection, aCancel, aHandled);
case kRemoveTextProperty:
return WillRemoveTextProperty(aSelection, aCancel, aHandled);
case kOutputText:
return WillOutputText(aSelection,
info->outputFormat,
info->outString,
aCancel,
aHandled);
case kInsertElement: // i had thought this would be html rules only. but we put pre elements
// into plaintext mail when doing quoting for reply! doh!
return WillInsert(aSelection, aCancel);
}
return NS_ERROR_FAILURE;
}
NS_IMETHODIMP
nsTextEditRules::DidDoAction(nsISelection *aSelection,
nsRulesInfo *aInfo, nsresult aResult)
{
// dont let any txns in here move the selection around behind our back.
// Note that this won't prevent explicit selection setting from working.
nsAutoTxnsConserveSelection dontSpazMySelection(mEditor);
if (!aSelection || !aInfo)
return NS_ERROR_NULL_POINTER;
// my kingdom for dynamic cast
nsTextRulesInfo *info = NS_STATIC_CAST(nsTextRulesInfo*, aInfo);
switch (info->action)
{
case kInsertBreak:
return DidInsertBreak(aSelection, aResult);
case kInsertText:
case kInsertTextIME:
return DidInsertText(aSelection, aResult);
case kDeleteSelection:
return DidDeleteSelection(aSelection, info->collapsedAction, aResult);
case kUndo:
return DidUndo(aSelection, aResult);
case kRedo:
return DidRedo(aSelection, aResult);
case kSetTextProperty:
return DidSetTextProperty(aSelection, aResult);
case kRemoveTextProperty:
return DidRemoveTextProperty(aSelection, aResult);
case kOutputText:
return DidOutputText(aSelection, aResult);
}
// Don't fail on transactions we don't handle here!
return NS_OK;
}
NS_IMETHODIMP
nsTextEditRules::DocumentIsEmpty(PRBool *aDocumentIsEmpty)
{
if (!aDocumentIsEmpty)
return NS_ERROR_NULL_POINTER;
*aDocumentIsEmpty = (mBogusNode.get() != nsnull);
return NS_OK;
}
/********************************************************
* Protected methods
********************************************************/
nsresult
nsTextEditRules::WillInsert(nsISelection *aSelection, PRBool *aCancel)
{
if (!aSelection || !aCancel)
return NS_ERROR_NULL_POINTER;
CANCEL_OPERATION_IF_READONLY_OR_DISABLED
// initialize out param
*aCancel = PR_FALSE;
// check for the magic content node and delete it if it exists
if (mBogusNode)
{
mEditor->DeleteNode(mBogusNode);
mBogusNode = do_QueryInterface(nsnull);
}
return NS_OK;
}
nsresult
nsTextEditRules::DidInsert(nsISelection *aSelection, nsresult aResult)
{
return NS_OK;
}
nsresult
nsTextEditRules::WillInsertBreak(nsISelection *aSelection, PRBool *aCancel, PRBool *aHandled)
{
if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; }
CANCEL_OPERATION_IF_READONLY_OR_DISABLED
*aHandled = PR_FALSE;
if (mFlags & nsIPlaintextEditor::eEditorSingleLineMask) {
*aCancel = PR_TRUE;
}
else
{
*aCancel = PR_FALSE;
// if the selection isn't collapsed, delete it.
PRBool bCollapsed;
nsresult res = aSelection->GetIsCollapsed(&bCollapsed);
if (NS_FAILED(res)) return res;
if (!bCollapsed)
{
res = mEditor->DeleteSelection(nsIEditor::eNone);
if (NS_FAILED(res)) return res;
}
res = WillInsert(aSelection, aCancel);
if (NS_FAILED(res)) return res;
// initialize out param
// we want to ignore result of WillInsert()
*aCancel = PR_FALSE;
}
return NS_OK;
}
nsresult
nsTextEditRules::DidInsertBreak(nsISelection *aSelection, nsresult aResult)
{
// we only need to execute the stuff below if we are a plaintext editor.
// html editors have a different mechanism for putting in mozBR's
// (because there are a bunch more places you have to worry about it in html)
if (!nsIPlaintextEditor::eEditorPlaintextMask & mFlags) return NS_OK;
// if we are at the end of the document, we need to insert
// a special mozBR following the normal br, and then set the
// selection to stick to the mozBR.
PRInt32 selOffset;
nsCOMPtr<nsIDOMNode> nearNode, selNode, root, temp;
nsCOMPtr<nsIDOMElement> rootElem;
nsresult res;
res = mEditor->GetStartNodeAndOffset(aSelection, address_of(selNode), &selOffset);
if (NS_FAILED(res)) return res;
// confirm we are at end of document
if (selOffset == 0) return NS_OK; // cant be after a br if we are at offset 0
res = mEditor->GetRootElement(getter_AddRefs(rootElem));
if (NS_FAILED(res)) return res;
root = do_QueryInterface(rootElem);
if (!root) return NS_ERROR_NULL_POINTER;
if (selNode != root) return NS_OK; // must be inside text node or somewhere other than end of root
temp = mEditor->GetChildAt(selNode, selOffset);
if (temp) return NS_OK; // cant be at end of there is a node after us.
nearNode = mEditor->GetChildAt(selNode, selOffset-1);
if (nearNode && nsHTMLEditUtils::IsBreak(nearNode) && !nsHTMLEditUtils::IsMozBR(nearNode))
{
nsCOMPtr<nsISelection> sel(aSelection);
nsCOMPtr<nsISelectionPrivate>selPrivate(do_QueryInterface(sel));
// need to insert special moz BR. Why? Because if we don't
// the user will see no new line for the break. Also, things
// like table cells won't grow in height.
nsCOMPtr<nsIDOMNode> brNode;
res = CreateMozBR(selNode, selOffset, address_of(brNode));
if (NS_FAILED(res)) return res;
res = nsEditor::GetNodeLocation(brNode, address_of(selNode), &selOffset);
if (NS_FAILED(res)) return res;
selPrivate->SetInterlinePosition(PR_TRUE);
res = aSelection->Collapse(selNode,selOffset);
if (NS_FAILED(res)) return res;
}
return res;
}
nsresult
nsTextEditRules::WillInsertText(PRInt32 aAction,
nsISelection *aSelection,
PRBool *aCancel,
PRBool *aHandled,
const nsString *inString,
nsString *outString,
PRInt32 aMaxLength)
{
if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; }
if (inString->IsEmpty() && (aAction != kInsertTextIME))
{
// HACK: this is a fix for bug 19395
// I can't outlaw all empty insertions
// because IME transaction depend on them
// There is more work to do to make the
// world safe for IME.
*aCancel = PR_TRUE;
*aHandled = PR_FALSE;
return NS_OK;
}
// initialize out param
*aCancel = PR_FALSE;
*aHandled = PR_TRUE;
nsresult res;
nsCOMPtr<nsIDOMNode> selNode;
PRInt32 selOffset;
PRInt32 start=0; PRInt32 end=0;
// handle docs with a max length
// NOTE, this function copies inString into outString for us.
res = TruncateInsertionIfNeeded(aSelection, inString, outString, aMaxLength);
if (NS_FAILED(res)) return res;
// handle password field docs
if (mFlags & nsIPlaintextEditor::eEditorPasswordMask)
{
res = mEditor->GetTextSelectionOffsets(aSelection, start, end);
NS_ASSERTION((NS_SUCCEEDED(res)), "getTextSelectionOffsets failed!");
if (NS_FAILED(res)) return res;
}
// if the selection isn't collapsed, delete it.
PRBool bCollapsed;
res = aSelection->GetIsCollapsed(&bCollapsed);
if (NS_FAILED(res)) return res;
if (!bCollapsed)
{
res = mEditor->DeleteSelection(nsIEditor::eNone);
if (NS_FAILED(res)) return res;
}
res = WillInsert(aSelection, aCancel);
if (NS_FAILED(res)) return res;
// initialize out param
// we want to ignore result of WillInsert()
*aCancel = PR_FALSE;
// handle password field data
// this has the side effect of changing all the characters in aOutString
// to the replacement character
if (mFlags & nsIPlaintextEditor::eEditorPasswordMask)
{
res = EchoInsertionToPWBuff(start, end, outString);
if (NS_FAILED(res)) return res;
}
// People have lots of different ideas about what text fields
// should do with multiline pastes. See bugs 21032, 23485, 23485, 50935.
// The four possible options are:
// 0. paste newlines intact
// 1. paste up to the first newline
// 2. replace newlines with spaces
// 3. strip newlines
// So find out what we're expected to do:
enum {
ePasteIntact = 0, ePasteFirstLine = 1,
eReplaceWithSpaces = 2, eStripNewlines = 3
};
PRInt32 singleLineNewlineBehavior = 1;
nsresult rv;
NS_WITH_SERVICE(nsIPref, prefs, NS_PREF_CONTRACTID, &rv);
if (NS_SUCCEEDED(rv) && prefs)
rv = prefs->GetIntPref("editor.singleLine.pasteNewlines",
&singleLineNewlineBehavior);
if (nsIPlaintextEditor::eEditorSingleLineMask & mFlags)
{
if (singleLineNewlineBehavior == eReplaceWithSpaces)
outString->ReplaceChar(CRLF, ' ');
else if (singleLineNewlineBehavior == eStripNewlines)
outString->StripChars(CRLF);
else if (singleLineNewlineBehavior == ePasteFirstLine)
{
PRInt32 firstCRLF = outString->FindCharInSet(CRLF);
if (firstCRLF > 0)
outString->Truncate(firstCRLF);
}
else // even if we're pasting newlines, don't paste leading/trailing ones
outString->Trim(CRLF, PR_TRUE, PR_TRUE);
}
// get the (collapsed) selection location
res = mEditor->GetStartNodeAndOffset(aSelection, address_of(selNode), &selOffset);
if (NS_FAILED(res)) return res;
// dont put text in places that cant have it
nsAutoString textTag; textTag.AssignWithConversion("__moz_text");
if (!mEditor->IsTextNode(selNode) && !mEditor->CanContainTag(selNode, textTag))
return NS_ERROR_FAILURE;
// we need to get the doc
nsCOMPtr<nsIDOMDocument>doc;
res = mEditor->GetDocument(getter_AddRefs(doc));
if (NS_FAILED(res)) return res;
if (!doc) return NS_ERROR_NULL_POINTER;
if (aAction == kInsertTextIME)
{
res = mEditor->InsertTextImpl(*outString, address_of(selNode), &selOffset, doc);
if (NS_FAILED(res)) return res;
}
else // aAction == kInsertText
{
// find where we are
nsCOMPtr<nsIDOMNode> curNode = selNode;
PRInt32 curOffset = selOffset;
// is our text going to be PREformatted?
// We remember this so that we know how to handle tabs.
PRBool isPRE;
res = mEditor->IsPreformatted(selNode, &isPRE);
if (NS_FAILED(res)) return res;
// dont spaz my selection in subtransactions
nsAutoTxnsConserveSelection dontSpazMySelection(mEditor);
nsSubsumeStr subStr;
const PRUnichar *unicodeBuf = outString->GetUnicode();
nsCOMPtr<nsIDOMNode> unused;
PRInt32 pos = 0;
// for efficiency, break out the pre case seperately. This is because
// its a lot cheaper to search the input string for only newlines than
// it is to search for both tabs and newlines.
if (isPRE)
{
char newlineChar = '\n';
while (unicodeBuf && (pos != -1) && ((PRUint32)pos < outString->Length()))
{
PRInt32 oldPos = pos;
PRInt32 subStrLen;
pos = outString->FindChar(newlineChar, PR_FALSE, oldPos);
if (pos != -1)
{
subStrLen = pos - oldPos;
// if first char is newline, then use just it
if (subStrLen == 0)
subStrLen = 1;
}
else
{
subStrLen = outString->Length() - oldPos;
pos = outString->Length();
}
subStr.Subsume((PRUnichar*)&unicodeBuf[oldPos], PR_FALSE, subStrLen);
// is it a return?
if (subStr.EqualsWithConversion("\n"))
{
if (nsIPlaintextEditor::eEditorSingleLineMask & mFlags)
{
NS_ASSERTION((singleLineNewlineBehavior == ePasteIntact),
"Newline improperly getting into single-line edit field!");
res = mEditor->InsertTextImpl(subStr, address_of(curNode), &curOffset, doc);
}
else
res = mEditor->CreateBRImpl(address_of(curNode), &curOffset, address_of(unused), nsIEditor::eNone);
pos++;
}
else
{
res = mEditor->InsertTextImpl(subStr, address_of(curNode), &curOffset, doc);
}
if (NS_FAILED(res)) return res;
}
}
else
{
char specialChars[] = {'\t','\n',0};
nsAutoString tabString; tabString.AssignWithConversion(" ");
while (unicodeBuf && (pos != -1) && ((PRUint32)pos < outString->Length()))
{
PRInt32 oldPos = pos;
PRInt32 subStrLen;
pos = outString->FindCharInSet(specialChars, oldPos);
if (pos != -1)
{
subStrLen = pos - oldPos;
// if first char is newline, then use just it
if (subStrLen == 0)
subStrLen = 1;
}
else
{
subStrLen = outString->Length() - oldPos;
pos = outString->Length();
}
subStr.Subsume((PRUnichar*)&unicodeBuf[oldPos], PR_FALSE, subStrLen);
// is it a tab?
if (subStr.EqualsWithConversion("\t"))
{
res = mEditor->InsertTextImpl(tabString, address_of(curNode), &curOffset, doc);
pos++;
}
// is it a return?
else if (subStr.EqualsWithConversion("\n"))
{
res = mEditor->CreateBRImpl(address_of(curNode), &curOffset, address_of(unused), nsIEditor::eNone);
pos++;
}
else
{
res = mEditor->InsertTextImpl(subStr, address_of(curNode), &curOffset, doc);
}
if (NS_FAILED(res)) return res;
}
}
if (curNode) aSelection->Collapse(curNode, curOffset);
}
return res;
}
nsresult
nsTextEditRules::DidInsertText(nsISelection *aSelection,
nsresult aResult)
{
return DidInsert(aSelection, aResult);
}
nsresult
nsTextEditRules::WillSetTextProperty(nsISelection *aSelection, PRBool *aCancel, PRBool *aHandled)
{
if (!aSelection || !aCancel || !aHandled)
{ return NS_ERROR_NULL_POINTER; }
nsresult res = NS_OK;
// XXX: should probably return a success value other than NS_OK that means "not allowed"
if (nsIPlaintextEditor::eEditorPlaintextMask & mFlags) {
*aCancel = PR_TRUE;
}
return res;
}
nsresult
nsTextEditRules::DidSetTextProperty(nsISelection *aSelection, nsresult aResult)
{
return NS_OK;
}
nsresult
nsTextEditRules::WillRemoveTextProperty(nsISelection *aSelection, PRBool *aCancel, PRBool *aHandled)
{
if (!aSelection || !aCancel || !aHandled)
{ return NS_ERROR_NULL_POINTER; }
nsresult res = NS_OK;
// XXX: should probably return a success value other than NS_OK that means "not allowed"
if (nsIPlaintextEditor::eEditorPlaintextMask & mFlags) {
*aCancel = PR_TRUE;
}
return res;
}
nsresult
nsTextEditRules::DidRemoveTextProperty(nsISelection *aSelection, nsresult aResult)
{
return NS_OK;
}
nsresult
nsTextEditRules::WillDeleteSelection(nsISelection *aSelection,
nsIEditor::EDirection aCollapsedAction,
PRBool *aCancel,
PRBool *aHandled)
{
if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; }
CANCEL_OPERATION_IF_READONLY_OR_DISABLED
// initialize out param
*aCancel = PR_FALSE;
*aHandled = PR_FALSE;
// if there is only bogus content, cancel the operation
if (mBogusNode) {
*aCancel = PR_TRUE;
return NS_OK;
}
#ifdef IBMBIDI // Test for distance between caret and text that will be deleted
nsCOMPtr<nsIDOMNode> node;
PRInt32 offset;
nsresult res = mEditor->GetStartNodeAndOffset(aSelection, address_of(node), &offset);
if (NS_FAILED(res)) return res;
if (!node) return NS_ERROR_FAILURE;
nsCOMPtr<nsIPresShell> shell;
mEditor->GetPresShell(getter_AddRefs(shell));
if (shell)
{
nsCOMPtr<nsIPresContext> context;
shell->GetPresContext(getter_AddRefs(context));
if (context)
{
PRBool bidiEnabled;
context->BidiEnabled(bidiEnabled);
if (bidiEnabled)
{
nsCOMPtr<nsIFrameSelection> frameSelection;
shell->GetFrameSelection(getter_AddRefs(frameSelection));
if (frameSelection)
{
nsCOMPtr<nsIContent> content = do_QueryInterface(node);
if (content)
{
nsIFrame *deleteInFrame;
PRInt32 frameOffset;
shell->GetPrimaryFrameFor(content, &deleteInFrame);
if (deleteInFrame)
{
PRUint8 currentCursorLevel;
long frameLevel;
res = deleteInFrame->GetChildFrameContainingOffset(offset, nsIEditor::eNext==aCollapsedAction, &frameOffset, &deleteInFrame);
if (NS_SUCCEEDED(res) && deleteInFrame)
{
shell->GetCursorBidiLevel(&currentCursorLevel);
nsCOMPtr<nsIAtom> embeddingLevel = NS_NewAtom("EmbeddingLevel");
deleteInFrame->GetBidiProperty(context, embeddingLevel, (void**)&frameLevel);
shell->SetCursorBidiLevel(frameLevel);
if (currentCursorLevel != frameLevel)
{
*aCancel = PR_TRUE;
return NS_OK;
}
}
}
}
}
}
}
}
#endif // IBMBIDI
if (mFlags & nsIPlaintextEditor::eEditorPasswordMask)
{
// manage the password buffer
PRInt32 start, end;
mEditor->GetTextSelectionOffsets(aSelection, start, end);
if (end==start)
{ // collapsed selection
if (nsIEditor::ePrevious==aCollapsedAction && 0<start) { // del back
mPasswordText.Cut(start-1, 1);
}
else if (nsIEditor::eNext==aCollapsedAction) { // del forward
mPasswordText.Cut(start, 1);
}
// otherwise nothing to do for this collapsed selection
}
else { // extended selection
mPasswordText.Cut(start, end-start);
}
#ifdef DEBUG_buster
char *password = mPasswordText.ToNewCString();
printf("mPasswordText is %s\n", password);
nsCRT::free(password);
#endif
}
return NS_OK;
}
// if the document is empty, insert a bogus text node with a &nbsp;
// if we ended up with consecutive text nodes, merge them
nsresult
nsTextEditRules::DidDeleteSelection(nsISelection *aSelection,
nsIEditor::EDirection aCollapsedAction,
nsresult aResult)
{
nsresult res = aResult; // if aResult is an error, we just return it
if (!aSelection) { return NS_ERROR_NULL_POINTER; }
PRBool isCollapsed;
aSelection->GetIsCollapsed(&isCollapsed);
NS_ASSERTION(PR_TRUE==isCollapsed, "selection not collapsed after delete selection.");
if (NS_SUCCEEDED(res)) // only do this work if DeleteSelection completed successfully
{
// if we don't have an empty document, check the selection to see if any collapsing is necessary
if (!mBogusNode)
{
// get the node that contains the selection point
nsCOMPtr<nsIDOMNode>anchor;
PRInt32 offset;
res = aSelection->GetAnchorNode(getter_AddRefs(anchor));
if (NS_FAILED(res)) return res;
if (!anchor) return NS_ERROR_NULL_POINTER;
res = aSelection->GetAnchorOffset(&offset);
if (NS_FAILED(res)) return res;
// selectedNode is either the anchor itself,
// or if anchor has children, it's the referenced child node
// XXX ----------------------------------------------- XXX
// I believe this s wrong. Assuming anchor and focus
// alwas correspond to selection endpoints, it is possible
// for the first node after the selectin start point to not
// be selected. As an example consider a selection that
// starts right before a <ul>, and ends after the first character
// in the text of the first list item. Really all that is
// selected is one letter of text, not the <ul>
nsCOMPtr<nsIDOMNode> selectedNode = do_QueryInterface(anchor);
PRBool hasChildren=PR_FALSE;
anchor->HasChildNodes(&hasChildren);
if (PR_TRUE==hasChildren)
{ // if anchor has children, set selectedNode to the child pointed at
nsCOMPtr<nsIDOMNodeList> anchorChildren;
res = anchor->GetChildNodes(getter_AddRefs(anchorChildren));
if ((NS_SUCCEEDED(res)) && anchorChildren) {
res = anchorChildren->Item(offset, getter_AddRefs(selectedNode));
}
}
if ((NS_SUCCEEDED(res)) && selectedNode && !DeleteEmptyTextNode(selectedNode))
{
nsCOMPtr<nsIDOMCharacterData>selectedNodeAsText;
selectedNodeAsText = do_QueryInterface(selectedNode);
if (selectedNodeAsText && mEditor->IsEditable(selectedNode))
{
nsCOMPtr<nsIDOMNode> siblingNode;
selectedNode->GetPreviousSibling(getter_AddRefs(siblingNode));
if (siblingNode && !DeleteEmptyTextNode(siblingNode))
{
nsCOMPtr<nsIDOMCharacterData>siblingNodeAsText;
siblingNodeAsText = do_QueryInterface(siblingNode);
if (siblingNodeAsText && mEditor->IsEditable(siblingNode))
{
PRUint32 siblingLength; // the length of siblingNode before the join
siblingNodeAsText->GetLength(&siblingLength);
nsCOMPtr<nsIDOMNode> parentNode;
res = selectedNode->GetParentNode(getter_AddRefs(parentNode));
if (NS_FAILED(res)) return res;
if (!parentNode) return NS_ERROR_NULL_POINTER;
res = mEditor->JoinNodes(siblingNode, selectedNode, parentNode);
// selectedNode will remain after the join, siblingNode is removed
}
}
selectedNode->GetNextSibling(getter_AddRefs(siblingNode));
if (siblingNode && !DeleteEmptyTextNode(siblingNode))
{
nsCOMPtr<nsIDOMCharacterData>siblingNodeAsText;
siblingNodeAsText = do_QueryInterface(siblingNode);
if (siblingNodeAsText && mEditor->IsEditable(siblingNode))
{
PRUint32 selectedNodeLength; // the length of siblingNode before the join
selectedNodeAsText->GetLength(&selectedNodeLength);
nsCOMPtr<nsIDOMNode> parentNode;
res = selectedNode->GetParentNode(getter_AddRefs(parentNode));
if (NS_FAILED(res)) return res;
if (!parentNode) return NS_ERROR_NULL_POINTER;
res = mEditor->JoinNodes(selectedNode, siblingNode, parentNode);
if (NS_FAILED(res)) return res;
// selectedNode will remain after the join, siblingNode is removed
}
}
}
}
}
}
return res;
}
nsresult
nsTextEditRules::WillUndo(nsISelection *aSelection, PRBool *aCancel, PRBool *aHandled)
{
if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; }
CANCEL_OPERATION_IF_READONLY_OR_DISABLED
// initialize out param
*aCancel = PR_FALSE;
*aHandled = PR_FALSE;
return NS_OK;
}
/* the idea here is to see if the magic empty node has suddenly reappeared as the res of the undo.
* if it has, set our state so we remember it.
* There is a tradeoff between doing here and at redo, or doing it everywhere else that might care.
* Since undo and redo are relatively rare, it makes sense to take the (small) performance hit here.
*/
nsresult
nsTextEditRules:: DidUndo(nsISelection *aSelection, nsresult aResult)
{
nsresult res = aResult; // if aResult is an error, we return it.
if (!aSelection) { return NS_ERROR_NULL_POINTER; }
if (NS_SUCCEEDED(res))
{
if (mBogusNode) {
mBogusNode = do_QueryInterface(nsnull);
}
else
{
nsCOMPtr<nsIDOMElement> theBody;
nsCOMPtr<nsIDOMNode> node;
res = mEditor->GetRootElement(getter_AddRefs(theBody));
if (NS_FAILED(res)) return res;
if (!theBody) return NS_ERROR_FAILURE;
res = mEditor->GetLeftmostChild(theBody,getter_AddRefs(node));
if (NS_FAILED(res)) return res;
if (!node) return NS_ERROR_NULL_POINTER;
if (mEditor->IsMozEditorBogusNode(node))
mBogusNode = do_QueryInterface(node);
}
}
return res;
}
nsresult
nsTextEditRules::WillRedo(nsISelection *aSelection, PRBool *aCancel, PRBool *aHandled)
{
if (!aSelection || !aCancel || !aHandled) { return NS_ERROR_NULL_POINTER; }
CANCEL_OPERATION_IF_READONLY_OR_DISABLED
// initialize out param
*aCancel = PR_FALSE;
*aHandled = PR_FALSE;
return NS_OK;
}
nsresult
nsTextEditRules::DidRedo(nsISelection *aSelection, nsresult aResult)
{
nsresult res = aResult; // if aResult is an error, we return it.
if (!aSelection) { return NS_ERROR_NULL_POINTER; }
if (NS_SUCCEEDED(res))
{
if (mBogusNode) {
mBogusNode = do_QueryInterface(nsnull);
}
else
{
nsCOMPtr<nsIDOMElement> theBody;
res = mEditor->GetRootElement(getter_AddRefs(theBody));
if (NS_FAILED(res)) return res;
if (!theBody) return NS_ERROR_FAILURE;
nsAutoString tagName; tagName.AssignWithConversion("div");
nsCOMPtr<nsIDOMNodeList> nodeList;
res = theBody->GetElementsByTagName(tagName, getter_AddRefs(nodeList));
if (NS_FAILED(res)) return res;
if (nodeList)
{
PRUint32 len;
nodeList->GetLength(&len);
if (len != 1) return NS_OK; // only in the case of one div could there be the bogus node
nsCOMPtr<nsIDOMNode>node;
nodeList->Item(0, getter_AddRefs(node));
if (!node) return NS_ERROR_NULL_POINTER;
if (mEditor->IsMozEditorBogusNode(node))
mBogusNode = do_QueryInterface(node);
}
}
}
return res;
}
nsresult
nsTextEditRules::WillOutputText(nsISelection *aSelection,
const nsString *aOutputFormat,
nsString *aOutString,
PRBool *aCancel,
PRBool *aHandled)
{
// null selection ok
if (!aOutString || !aOutputFormat || !aCancel || !aHandled)
{ return NS_ERROR_NULL_POINTER; }
// initialize out param
*aCancel = PR_FALSE;
*aHandled = PR_FALSE;
if (PR_TRUE == aOutputFormat->EqualsWithConversion("text/plain"))
{ // only use these rules for plain text output
if (mFlags & nsIPlaintextEditor::eEditorPasswordMask)
{
*aOutString = mPasswordText;
*aHandled = PR_TRUE;
}
else if (mBogusNode)
{ // this means there's no content, so output null string
aOutString->SetLength(0);
*aHandled = PR_TRUE;
}
}
return NS_OK;
}
nsresult
nsTextEditRules::DidOutputText(nsISelection *aSelection, nsresult aResult)
{
return NS_OK;
}
nsresult
nsTextEditRules::ReplaceNewlines(nsIDOMRange *aRange)
{
if (!aRange) return NS_ERROR_NULL_POINTER;
// convert any newlines in editable, preformatted text nodes
// into normal breaks. this is because layout wont give us a place
// to put the cursor on empty lines otherwise.
nsCOMPtr<nsIContentIterator> iter;
nsCOMPtr<nsISupports> isupports;
PRUint32 nodeCount,j;
nsCOMPtr<nsISupportsArray> arrayOfNodes;
// make an isupportsArray to hold a list of nodes
nsresult res = NS_NewISupportsArray(getter_AddRefs(arrayOfNodes));
if (NS_FAILED(res)) return res;
// need an iterator
res = nsComponentManager::CreateInstance(kContentIteratorCID,
nsnull,
NS_GET_IID(nsIContentIterator),
getter_AddRefs(iter));
if (NS_FAILED(res)) return res;
res = iter->Init(aRange);
if (NS_FAILED(res)) return res;
// gather up a list of editable preformatted text nodes
while (NS_ENUMERATOR_FALSE == iter->IsDone())
{
nsCOMPtr<nsIDOMNode> node;
nsCOMPtr<nsIContent> content;
res = iter->CurrentNode(getter_AddRefs(content));
if (NS_FAILED(res)) return res;
node = do_QueryInterface(content);
if (!node) return NS_ERROR_FAILURE;
if (mEditor->IsTextNode(node) && mEditor->IsEditable(node))
{
PRBool isPRE;
res = mEditor->IsPreformatted(node, &isPRE);
if (NS_FAILED(res)) return res;
if (isPRE)
{
isupports = do_QueryInterface(node);
arrayOfNodes->AppendElement(isupports);
}
}
res = iter->Next();
if (NS_FAILED(res)) return res;
}
// replace newlines with breaks. have to do this left to right,
// since inserting the break can split the text node, and the
// original node becomes the righthand node.
char newlineChar[] = {'\n',0};
res = arrayOfNodes->Count(&nodeCount);
if (NS_FAILED(res)) return res;
for (j = 0; j < nodeCount; j++)
{
isupports = dont_AddRef(arrayOfNodes->ElementAt(0));
nsCOMPtr<nsIDOMNode> brNode, theNode( do_QueryInterface(isupports) );
nsCOMPtr<nsIDOMCharacterData> textNode( do_QueryInterface(theNode) );
arrayOfNodes->RemoveElementAt(0);
// find the newline
PRInt32 offset;
nsAutoString tempString;
do
{
textNode->GetData(tempString);
offset = tempString.FindCharInSet(newlineChar);
if (offset == -1) break; // done with this node
// delete the newline
EditTxn *txn;
// note 1: we are not telling edit listeners about these because they don't care
// note 2: we are not wrapping these in a placeholder because we know they already are,
// or, failing that, undo is disabled
res = mEditor->CreateTxnForDeleteText(textNode, offset, 1, (DeleteTextTxn**)&txn);
if (NS_FAILED(res)) return res;
if (!txn) return NS_ERROR_OUT_OF_MEMORY;
res = mEditor->Do(txn);
if (NS_FAILED(res)) return res;
// The transaction system (if any) has taken ownwership of txn
NS_IF_RELEASE(txn);
// insert a break
res = mEditor->CreateBR(textNode, offset, address_of(brNode));
if (NS_FAILED(res)) return res;
} while (1); // break used to exit while loop
}
return res;
}
nsresult
nsTextEditRules::CreateBogusNodeIfNeeded(nsISelection *aSelection)
{
if (!aSelection) { return NS_ERROR_NULL_POINTER; }
if (!mEditor) { return NS_ERROR_NULL_POINTER; }
if (mBogusNode) return NS_OK; // let's not create more than one, ok?
// tell rules system to not do any post-processing
nsAutoRules beginRulesSniffing(mEditor, nsEditor::kOpIgnore, nsIEditor::eNone);
if (!mBody) return NS_ERROR_NULL_POINTER;
// now we've got the body tag.
// iterate the body tag, looking for editable content
// if no editable content is found, insert the bogus node
PRBool needsBogusContent=PR_TRUE;
nsCOMPtr<nsIDOMNode>bodyChild;
nsresult res = mBody->GetFirstChild(getter_AddRefs(bodyChild));
while ((NS_SUCCEEDED(res)) && bodyChild)
{
if (mEditor->IsMozEditorBogusNode(bodyChild) || mEditor->IsEditable(bodyChild))
{
needsBogusContent = PR_FALSE;
break;
}
nsCOMPtr<nsIDOMNode>temp;
bodyChild->GetNextSibling(getter_AddRefs(temp));
bodyChild = do_QueryInterface(temp);
}
if (needsBogusContent)
{
// create a br
nsCOMPtr<nsIDOMDocument> domDoc;
res = mEditor->GetDocument(getter_AddRefs(domDoc));
nsCOMPtr<nsIDOMElement>brElement;
nsCOMPtr<nsIContent> newContent;
nsString qualifiedTag;
qualifiedTag.AssignWithConversion("br");
res = mEditor->CreateHTMLContent(qualifiedTag, getter_AddRefs(newContent));
brElement = do_QueryInterface(newContent);
if (NS_FAILED(res)) return res;
// set mBogusNode to be the newly created <br>
mBogusNode = do_QueryInterface(brElement);
if (!mBogusNode) return NS_ERROR_NULL_POINTER;
// give it a special attribute
brElement->SetAttribute( NS_ConvertASCIItoUCS2(nsEditor::kMOZEditorBogusNodeAttr),
NS_ConvertASCIItoUCS2(nsEditor::kMOZEditorBogusNodeValue) );
// put the node in the document
res = mEditor->InsertNode(mBogusNode,mBody,0);
if (NS_FAILED(res)) return res;
// set selection
aSelection->Collapse(mBody,0);
}
return res;
}
nsresult
nsTextEditRules::TruncateInsertionIfNeeded(nsISelection *aSelection,
const nsString *aInString,
nsString *aOutString,
PRInt32 aMaxLength)
{
if (!aSelection || !aInString || !aOutString) {return NS_ERROR_NULL_POINTER;}
nsresult res = NS_OK;
*aOutString = *aInString;
if ((-1 != aMaxLength) && (mFlags & nsIPlaintextEditor::eEditorPlaintextMask))
{
// Get the current text length.
// Get the length of inString.
// Get the length of the selection.
// If selection is collapsed, it is length 0.
// Subtract the length of the selection from the len(doc)
// since we'll delete the selection on insert.
// This is resultingDocLength.
// If (resultingDocLength) is at or over max, cancel the insert
// If (resultingDocLength) + (length of input) > max,
// set aOutString to subset of inString so length = max
PRInt32 docLength;
res = mEditor->GetTextLength(&docLength);
if (NS_FAILED(res)) { return res; }
PRInt32 start, end;
res = mEditor->GetTextSelectionOffsets(aSelection, start, end);
if (NS_FAILED(res)) { return res; }
PRInt32 selectionLength = end-start;
if (selectionLength<0) { selectionLength *= (-1); }
PRInt32 resultingDocLength = docLength - selectionLength;
if (resultingDocLength >= aMaxLength)
{
aOutString->SetLength(0);
return res;
}
else
{
PRInt32 inCount = aOutString->Length();
if ((inCount+resultingDocLength) > aMaxLength)
{
aOutString->Truncate(aMaxLength-resultingDocLength);
}
}
}
return res;
}
nsresult
nsTextEditRules::EchoInsertionToPWBuff(PRInt32 aStart, PRInt32 aEnd, nsString *aOutString)
{
if (!aOutString) {return NS_ERROR_NULL_POINTER;}
// manage the password buffer
mPasswordText.Insert(*aOutString, aStart);
// change the output to '*' only
PRInt32 length = aOutString->Length();
PRInt32 i;
aOutString->SetLength(0);
for (i=0; i<length; i++)
aOutString->AppendWithConversion('*');
return NS_OK;
}
///////////////////////////////////////////////////////////////////////////
// CreateMozBR: put a BR node with moz attribute at {aNode, aOffset}
//
nsresult
nsTextEditRules::CreateMozBR(nsIDOMNode *inParent, PRInt32 inOffset, nsCOMPtr<nsIDOMNode> *outBRNode)
{
if (!inParent || !outBRNode) return NS_ERROR_NULL_POINTER;
nsresult res = mEditor->CreateBR(inParent, inOffset, outBRNode);
if (NS_FAILED(res)) return res;
// give it special moz attr
nsCOMPtr<nsIDOMElement> brElem = do_QueryInterface(*outBRNode);
if (brElem)
{
res = mEditor->SetAttribute(brElem, NS_ConvertASCIItoUCS2("type"), NS_ConvertASCIItoUCS2("_moz"));
if (NS_FAILED(res)) return res;
}
return res;
}
PRBool
nsTextEditRules::DeleteEmptyTextNode(nsIDOMNode *aNode)
{
if (aNode)
{
nsCOMPtr<nsIDOMCharacterData>nodeAsText;
nodeAsText = do_QueryInterface(aNode);
if (nodeAsText)
{
PRUint32 len;
nodeAsText->GetLength(&len);
if (!len)
{
mEditor->DeleteNode(aNode);
return PR_TRUE;
}
}
}
return PR_FALSE;
}