Bug 1162818 part.5 The offset of nsEditor::InsertTextImpl() should be minimum offset of IME selections if there is r=ehsan

This commit is contained in:
Masayuki Nakano 2015-06-05 02:06:09 +09:00
parent cb43121733
commit aa07a30e72
4 changed files with 179 additions and 40 deletions

View File

@ -26,6 +26,7 @@
#include "mozFlushType.h" // for mozFlushType::Flush_Frames
#include "mozISpellCheckingEngine.h"
#include "mozInlineSpellChecker.h" // for mozInlineSpellChecker
#include "mozilla/CheckedInt.h" // for CheckedInt
#include "mozilla/IMEStateManager.h" // for IMEStateManager
#include "mozilla/Preferences.h" // for Preferences
#include "mozilla/dom/Selection.h" // for Selection, etc
@ -2274,6 +2275,70 @@ NS_IMETHODIMP nsEditor::ScrollSelectionIntoView(bool aScrollToAnchor)
return NS_OK;
}
void
nsEditor::FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
int32_t& aOffset)
{
if (aNode->IsNodeOfType(nsINode::eTEXT)) {
// There is no "better" insertion point.
return;
}
if (!IsPlaintextEditor()) {
// We cannot find "better" insertion point in HTML editor.
// WARNING: When you add some code to find better node in HTML editor,
// you need to call this before calling InsertTextImpl() in
// nsHTMLEditRules.
return;
}
nsCOMPtr<nsINode> node = aNode;
int32_t offset = aOffset;
nsCOMPtr<nsINode> root = GetRoot();
if (aNode == root) {
// In some cases, aNode is the anonymous DIV, and offset is 0. To avoid
// injecting unneeded text nodes, we first look to see if we have one
// available. In that case, we'll just adjust node and offset accordingly.
if (offset == 0 && node->HasChildren() &&
node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) {
aNode = node->GetFirstChild();
aOffset = 0;
return;
}
// In some other cases, aNode is the anonymous DIV, and offset points to the
// terminating mozBR. In that case, we'll adjust aInOutNode and
// aInOutOffset to the preceding text node, if any.
if (offset > 0 && node->GetChildAt(offset - 1) &&
node->GetChildAt(offset - 1)->IsNodeOfType(nsINode::eTEXT)) {
NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX);
aNode = node->GetChildAt(offset - 1);
aOffset = static_cast<int32_t>(aNode->Length());
return;
}
}
// Sometimes, aNode is the mozBR element itself. In that case, we'll adjust
// the insertion point to the previous text node, if one exists, or to the
// parent anonymous DIV.
if (nsTextEditUtils::IsMozBR(node) && !offset) {
if (node->GetPreviousSibling() &&
node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) {
NS_ENSURE_TRUE_VOID(node->Length() <= INT32_MAX);
aNode = node->GetPreviousSibling();
aOffset = static_cast<int32_t>(aNode->Length());
return;
}
if (node->GetParentNode() && node->GetParentNode() == root) {
aNode = node->GetParentNode();
aOffset = 0;
return;
}
}
}
nsresult
nsEditor::InsertTextImpl(const nsAString& aStringToInsert,
nsCOMPtr<nsINode>* aInOutNode,
@ -2286,46 +2351,27 @@ nsEditor::InsertTextImpl(const nsAString& aStringToInsert,
NS_ENSURE_TRUE(aInOutNode && *aInOutNode && aInOutOffset && aDoc,
NS_ERROR_NULL_POINTER);
if (!ShouldHandleIMEComposition() && aStringToInsert.IsEmpty()) {
return NS_OK;
}
nsCOMPtr<nsINode> node = *aInOutNode;
uint32_t offset = static_cast<uint32_t>(*aInOutOffset);
// This method doesn't support over INT32_MAX length text since aInOutOffset
// is int32_t*.
CheckedInt<int32_t> lengthToInsert(aStringToInsert.Length());
NS_ENSURE_TRUE(lengthToInsert.isValid(), NS_ERROR_INVALID_ARG);
if (!node->IsNodeOfType(nsINode::eTEXT) && IsPlaintextEditor()) {
nsCOMPtr<nsINode> root = GetRoot();
// In some cases, node is the anonymous DIV, and offset is 0. To avoid
// injecting unneeded text nodes, we first look to see if we have one
// available. In that case, we'll just adjust node and offset accordingly.
if (node == root && offset == 0 && node->HasChildren() &&
node->GetFirstChild()->IsNodeOfType(nsINode::eTEXT)) {
node = node->GetFirstChild();
}
// In some other cases, node is the anonymous DIV, and offset points to the
// terminating mozBR. In that case, we'll adjust aInOutNode and
// aInOutOffset to the preceding text node, if any.
if (node == root && offset > 0 && node->GetChildAt(offset - 1) &&
node->GetChildAt(offset - 1)->IsNodeOfType(nsINode::eTEXT)) {
node = node->GetChildAt(offset - 1);
offset = node->Length();
}
// Sometimes, node is the mozBR element itself. In that case, we'll adjust
// the insertion point to the previous text node, if one exists, or to the
// parent anonymous DIV.
if (nsTextEditUtils::IsMozBR(node) && offset == 0) {
if (node->GetPreviousSibling() &&
node->GetPreviousSibling()->IsNodeOfType(nsINode::eTEXT)) {
node = node->GetPreviousSibling();
offset = node->Length();
} else if (node->GetParentNode() && node->GetParentNode() == root) {
node = node->GetParentNode();
}
}
}
nsCOMPtr<nsINode> node = *aInOutNode;
int32_t offset = *aInOutOffset;
// In some cases, the node may be the anonymous div elemnt or a mozBR
// element. Let's try to look for better insertion point in the nearest
// text node if there is.
FindBetterInsertionPoint(node, offset);
nsresult res;
if (ShouldHandleIMEComposition()) {
CheckedInt<int32_t> newOffset;
if (!node->IsNodeOfType(nsINode::eTEXT)) {
// create a text node
nsRefPtr<nsTextNode> newNode = aDoc->CreateTextNode(EmptyString());
@ -2334,18 +2380,24 @@ nsEditor::InsertTextImpl(const nsAString& aStringToInsert,
NS_ENSURE_SUCCESS(res, res);
node = newNode;
offset = 0;
newOffset = lengthToInsert;
} else {
newOffset = lengthToInsert + offset;
NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
}
res = InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(),
offset);
NS_ENSURE_SUCCESS(res, res);
offset += aStringToInsert.Length();
offset = newOffset.value();
} else {
if (node->IsNodeOfType(nsINode::eTEXT)) {
CheckedInt<int32_t> newOffset = lengthToInsert + offset;
NS_ENSURE_TRUE(newOffset.isValid(), NS_ERROR_FAILURE);
// we are inserting text into an existing text node.
res = InsertTextIntoTextNodeImpl(aStringToInsert, *node->GetAsText(),
offset);
NS_ENSURE_SUCCESS(res, res);
offset += aStringToInsert.Length();
offset = newOffset.value();
} else {
// we are inserting text into a non-text node. first we have to create a
// textnode (this also populates it with the text)
@ -2354,12 +2406,12 @@ nsEditor::InsertTextImpl(const nsAString& aStringToInsert,
res = InsertNode(*newNode, *node, offset);
NS_ENSURE_SUCCESS(res, res);
node = newNode;
offset = aStringToInsert.Length();
offset = lengthToInsert.value();
}
}
*aInOutNode = node;
*aInOutOffset = static_cast<int32_t>(offset);
*aInOutOffset = offset;
return NS_OK;
}
@ -2371,6 +2423,8 @@ nsEditor::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert,
{
nsRefPtr<EditTxn> txn;
bool isIMETransaction = false;
int32_t replacedOffset = 0;
int32_t replacedLength = 0;
// aSuppressIME is used when editor must insert text, yet this text is not
// part of the current IME operation. Example: adjusting whitespace around an
// IME insertion.
@ -2397,6 +2451,11 @@ nsEditor::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert,
txn = CreateTxnForIMEText(aStringToInsert);
isIMETransaction = true;
// All characters of the composition string will be replaced with
// aStringToInsert. So, we need to emulate to remove the composition
// string.
replacedOffset = mIMETextOffset;
replacedLength = mIMETextLength;
mIMETextLength = aStringToInsert.Length();
} else {
txn = CreateTxnForInsertText(aStringToInsert, aTextNode, aOffset);
@ -2415,6 +2474,11 @@ nsEditor::InsertTextIntoTextNodeImpl(const nsAString& aStringToInsert,
nsresult res = DoTransaction(txn);
EndUpdateViewBatch();
if (replacedLength) {
mRangeUpdater.SelAdjDeleteText(
static_cast<nsIDOMCharacterData*>(aTextNode.AsDOMNode()),
replacedOffset, replacedLength);
}
mRangeUpdater.SelAdjInsertText(aTextNode, aOffset, aStringToInsert);
// let listeners know what happened
@ -4722,8 +4786,10 @@ nsEditor::InitializeSelection(nsIDOMEventTarget* aFocusEventTarget)
// XXX If selection is changed during reframe, this doesn't work well!
nsRange* firstRange = selection->GetRangeAt(0);
NS_ENSURE_TRUE(firstRange, NS_ERROR_FAILURE);
nsCOMPtr<nsINode> parentNode = firstRange->GetStartParent();
Text* textNode = parentNode->GetAsText();
nsCOMPtr<nsINode> startNode = firstRange->GetStartParent();
int32_t startOffset = firstRange->StartOffset();
FindBetterInsertionPoint(startNode, startOffset);
Text* textNode = startNode->GetAsText();
MOZ_ASSERT(textNode,
"There must be text node if mIMETextLength is larger than 0");
if (textNode) {
@ -5111,3 +5177,49 @@ nsEditor::GetIsInEditAction(bool* aIsInEditAction)
*aIsInEditAction = mIsInEditAction;
return NS_OK;
}
int32_t
nsEditor::GetIMESelectionStartOffsetIn(nsINode* aTextNode)
{
MOZ_ASSERT(aTextNode, "aTextNode must not be nullptr");
nsCOMPtr<nsISelectionController> selectionController;
nsresult rv = GetSelectionController(getter_AddRefs(selectionController));
NS_ENSURE_SUCCESS(rv, -1);
NS_ENSURE_TRUE(selectionController, -1);
int32_t minOffset = INT32_MAX;
static const SelectionType kIMESelectionTypes[] = {
nsISelectionController::SELECTION_IME_RAWINPUT,
nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT,
nsISelectionController::SELECTION_IME_CONVERTEDTEXT,
nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT
};
for (auto selectionType : kIMESelectionTypes) {
nsRefPtr<Selection> selection = GetSelection(selectionType);
if (!selection) {
continue;
}
for (uint32_t i = 0; i < selection->RangeCount(); i++) {
nsRefPtr<nsRange> range = selection->GetRangeAt(i);
if (NS_WARN_IF(!range)) {
continue;
}
if (NS_WARN_IF(range->GetStartParent() != aTextNode)) {
// ignore the start offset...
} else {
MOZ_ASSERT(range->StartOffset() >= 0,
"start offset shouldn't be negative");
minOffset = std::min(minOffset, range->StartOffset());
}
if (NS_WARN_IF(range->GetEndParent() != aTextNode)) {
// ignore the end offset...
} else {
MOZ_ASSERT(range->EndOffset() >= 0,
"start offset shouldn't be negative");
minOffset = std::min(minOffset, range->EndOffset());
}
}
}
return minOffset < INT32_MAX ? minOffset : -1;
}

View File

@ -807,6 +807,19 @@ public:
virtual already_AddRefed<nsIDOMNode> FindUserSelectAllNode(nsIDOMNode* aNode) { return nullptr; }
/**
* GetIMESelectionStartOffsetIn() returns the start offset of IME selection in
* the aTextNode. If there is no IME selection, returns -1.
*/
int32_t GetIMESelectionStartOffsetIn(nsINode* aTextNode);
/**
* FindBetterInsertionPoint() tries to look for better insertion point which
* is typically the nearest text node and offset in it.
*/
void FindBetterInsertionPoint(nsCOMPtr<nsINode>& aNode,
int32_t& aOffset);
protected:
enum Tristate {
eTriUnset,

View File

@ -1333,15 +1333,21 @@ nsHTMLEditRules::WillInsertText(EditAction aAction,
if (aAction == EditAction::insertIMEText) {
// Right now the nsWSRunObject code bails on empty strings, but IME needs
// the InsertTextImpl() call to still happen since empty strings are meaningful there.
NS_ENSURE_STATE(mHTMLEditor);
// If there is one or more IME selections, its minimum offset should be
// the insertion point.
int32_t IMESelectionOffset =
mHTMLEditor->GetIMESelectionStartOffsetIn(selNode);
if (IMESelectionOffset >= 0) {
selOffset = IMESelectionOffset;
}
if (inString->IsEmpty())
{
NS_ENSURE_STATE(mHTMLEditor);
res = mHTMLEditor->InsertTextImpl(*inString, address_of(selNode),
&selOffset, doc);
}
else
{
NS_ENSURE_STATE(mHTMLEditor);
nsWSRunObject wsObj(mHTMLEditor, selNode, selOffset);
res = wsObj.InsertText(*inString, address_of(selNode), &selOffset, doc);
}

View File

@ -725,6 +725,14 @@ nsTextEditRules::WillInsertText(EditAction aAction,
if (aAction == EditAction::insertIMEText) {
NS_ENSURE_STATE(mEditor);
// Find better insertion point to insert text.
mEditor->FindBetterInsertionPoint(selNode, selOffset);
// If there is one or more IME selections, its minimum offset should be
// the insertion point.
int32_t IMESelectionOffset = mEditor->GetIMESelectionStartOffsetIn(selNode);
if (IMESelectionOffset >= 0) {
selOffset = IMESelectionOffset;
}
res = mEditor->InsertTextImpl(*outString, address_of(selNode), &selOffset, doc);
NS_ENSURE_SUCCESS(res, res);
} else {