/* -*- 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 "IMETextTxn.h" #include "mozilla/DebugOnly.h" // for DebugOnly #include "mozilla/mozalloc.h" // for operator new #include "mozilla/TextEvents.h" // for TextRangeStyle #include "nsAString.h" // for nsAString_internal::Length, etc #include "nsAutoPtr.h" // for nsRefPtr #include "nsDebug.h" // for NS_ASSERTION, etc #include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc #include "nsIDOMCharacterData.h" // for nsIDOMCharacterData #include "nsIDOMRange.h" // for nsRange::SetEnd, etc #include "nsIContent.h" // for nsIContent #include "nsIEditor.h" // for nsIEditor #include "nsIPresShell.h" // for SelectionType #include "nsISelection.h" // for nsISelection #include "nsISelectionController.h" // for nsISelectionController, etc #include "nsISelectionPrivate.h" // for nsISelectionPrivate #include "nsISupportsImpl.h" // for nsRange::AddRef, etc #include "nsISupportsUtils.h" // for NS_ADDREF_THIS, NS_RELEASE #include "nsITransaction.h" // for nsITransaction #include "nsRange.h" // for nsRange #include "nsString.h" // for nsString using namespace mozilla; // #define DEBUG_IMETXN IMETextTxn::IMETextTxn() : EditTxn() { } NS_IMPL_CYCLE_COLLECTION_INHERITED(IMETextTxn, EditTxn, mElement) // mRangeList can't lead to cycles NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMETextTxn) if (aIID.Equals(IMETextTxn::GetCID())) { *aInstancePtr = (void*)(IMETextTxn*)this; NS_ADDREF_THIS(); return NS_OK; } else NS_INTERFACE_MAP_END_INHERITING(EditTxn) NS_IMETHODIMP IMETextTxn::Init(nsIDOMCharacterData *aElement, uint32_t aOffset, uint32_t aReplaceLength, TextRangeArray *aTextRangeArray, const nsAString &aStringToInsert, nsIEditor *aEditor) { NS_ENSURE_ARG_POINTER(aElement); mElement = aElement; mOffset = aOffset; mReplaceLength = aReplaceLength; mStringToInsert = aStringToInsert; mEditor = aEditor; mRanges = aTextRangeArray; mFixed = false; return NS_OK; } NS_IMETHODIMP IMETextTxn::DoTransaction(void) { #ifdef DEBUG_IMETXN printf("Do IME Text element = %p replace = %d len = %d\n", mElement.get(), mReplaceLength, mStringToInsert.Length()); #endif nsCOMPtr selCon; mEditor->GetSelectionController(getter_AddRefs(selCon)); NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED); // advance caret: This requires the presentation shell to get the selection. nsresult result; if (mReplaceLength == 0) { result = mElement->InsertData(mOffset, mStringToInsert); } else { result = mElement->ReplaceData(mOffset, mReplaceLength, mStringToInsert); } if (NS_SUCCEEDED(result)) { result = SetSelectionForRanges(); } return result; } NS_IMETHODIMP IMETextTxn::UndoTransaction(void) { #ifdef DEBUG_IMETXN printf("Undo IME Text element = %p\n", mElement.get()); #endif nsCOMPtr selCon; mEditor->GetSelectionController(getter_AddRefs(selCon)); NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED); nsresult result = mElement->DeleteData(mOffset, mStringToInsert.Length()); if (NS_SUCCEEDED(result)) { // set the selection to the insertion point where the string was removed nsCOMPtr selection; result = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); if (NS_SUCCEEDED(result) && selection) { result = selection->Collapse(mElement, mOffset); NS_ASSERTION((NS_SUCCEEDED(result)), "selection could not be collapsed after undo of IME insert."); } } return result; } NS_IMETHODIMP IMETextTxn::Merge(nsITransaction *aTransaction, bool *aDidMerge) { NS_ASSERTION(aDidMerge, "illegal vaule- null ptr- aDidMerge"); NS_ASSERTION(aTransaction, "illegal vaule- null ptr- aTransaction"); NS_ENSURE_TRUE(aDidMerge && aTransaction, NS_ERROR_NULL_POINTER); #ifdef DEBUG_IMETXN printf("Merge IME Text element = %p\n", mElement.get()); #endif // // check to make sure we aren't fixed, if we are then nothing get's absorbed // if (mFixed) { *aDidMerge = false; return NS_OK; } // // if aTransaction is another IMETextTxn then absorb it // IMETextTxn* otherTxn = nullptr; nsresult result = aTransaction->QueryInterface(IMETextTxn::GetCID(),(void**)&otherTxn); if (otherTxn && NS_SUCCEEDED(result)) { // // we absorb the next IME transaction by adopting its insert string as our own // mStringToInsert = otherTxn->mStringToInsert; mRanges = otherTxn->mRanges; *aDidMerge = true; #ifdef DEBUG_IMETXN printf("IMETextTxn assimilated IMETextTxn:%p\n", aTransaction); #endif NS_RELEASE(otherTxn); return NS_OK; } *aDidMerge = false; return NS_OK; } NS_IMETHODIMP IMETextTxn::MarkFixed(void) { mFixed = true; return NS_OK; } NS_IMETHODIMP IMETextTxn::GetTxnDescription(nsAString& aString) { aString.AssignLiteral("IMETextTxn: "); aString += mStringToInsert; return NS_OK; } /* ============ protected methods ================== */ static SelectionType ToSelectionType(uint32_t aTextRangeType) { switch(aTextRangeType) { case NS_TEXTRANGE_RAWINPUT: return nsISelectionController::SELECTION_IME_RAWINPUT; case NS_TEXTRANGE_SELECTEDRAWTEXT: return nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT; case NS_TEXTRANGE_CONVERTEDTEXT: return nsISelectionController::SELECTION_IME_CONVERTEDTEXT; case NS_TEXTRANGE_SELECTEDCONVERTEDTEXT: return nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT; default: MOZ_CRASH("Selection type is invalid"); return nsISelectionController::SELECTION_NORMAL; } } nsresult IMETextTxn::SetSelectionForRanges() { nsCOMPtr selCon; mEditor->GetSelectionController(getter_AddRefs(selCon)); NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED); nsCOMPtr selection; nsresult rv = selCon->GetSelection(nsISelectionController::SELECTION_NORMAL, getter_AddRefs(selection)); NS_ENSURE_SUCCESS(rv, rv); nsCOMPtr selPriv(do_QueryInterface(selection)); rv = selPriv->StartBatchChanges(); NS_ENSURE_SUCCESS(rv, rv); // First, remove all selections of IME composition. static const SelectionType kIMESelections[] = { nsISelectionController::SELECTION_IME_RAWINPUT, nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT, nsISelectionController::SELECTION_IME_CONVERTEDTEXT, nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT }; for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) { nsCOMPtr selectionOfIME; if (NS_FAILED(selCon->GetSelection(kIMESelections[i], getter_AddRefs(selectionOfIME)))) { continue; } DebugOnly rv = selectionOfIME->RemoveAllRanges(); NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to remove all ranges of IME selection"); } // Set caret position and selection of IME composition with TextRangeArray. bool setCaret = false; uint32_t countOfRanges = mRanges ? mRanges->Length() : 0; #ifdef DEBUG // When this sets selection (caret) offset to out of the content of // the editor, let's crash the process only on debug build. That makes such // bugs detectable with automated tests. uint32_t maxOffset = UINT32_MAX; mElement->GetLength(&maxOffset); #endif // The mStringToInsert may be truncated if maxlength attribute value doesn't // allow to input all text of this composition. So, we can get actual length // of the inserted string from it. uint32_t insertedLength = mStringToInsert.Length(); for (uint32_t i = 0; i < countOfRanges; ++i) { const TextRange& textRange = mRanges->ElementAt(i); // Caret needs special handling since its length may be 0 and if it's not // specified explicitly, we need to handle it ourselves later. if (textRange.mRangeType == NS_TEXTRANGE_CARETPOSITION) { NS_ASSERTION(!setCaret, "The ranges already has caret position"); NS_ASSERTION(!textRange.Length(), "nsEditor doesn't support wide caret"); int32_t caretOffset = static_cast( mOffset + std::min(textRange.mStartOffset, insertedLength)); MOZ_ASSERT(caretOffset >= 0 && static_cast(caretOffset) <= maxOffset); rv = selection->Collapse(mElement, caretOffset); setCaret = setCaret || NS_SUCCEEDED(rv); NS_ASSERTION(setCaret, "Failed to collapse normal selection"); continue; } // If the clause length is 0, it's should be a bug. if (!textRange.Length()) { NS_WARNING("Any clauses must not be empty"); continue; } nsRefPtr clauseRange; int32_t startOffset = static_cast( mOffset + std::min(textRange.mStartOffset, insertedLength)); MOZ_ASSERT(startOffset >= 0 && static_cast(startOffset) <= maxOffset); int32_t endOffset = static_cast( mOffset + std::min(textRange.mEndOffset, insertedLength)); MOZ_ASSERT(endOffset >= startOffset && static_cast(endOffset) <= maxOffset); rv = nsRange::CreateRange(mElement, startOffset, mElement, endOffset, getter_AddRefs(clauseRange)); if (NS_FAILED(rv)) { NS_WARNING("Failed to create a DOM range for a clause of composition"); break; } // Set the range of the clause to selection. nsCOMPtr selectionOfIME; rv = selCon->GetSelection(ToSelectionType(textRange.mRangeType), getter_AddRefs(selectionOfIME)); if (NS_FAILED(rv)) { NS_WARNING("Failed to get IME selection"); break; } rv = selectionOfIME->AddRange(clauseRange); if (NS_FAILED(rv)) { NS_WARNING("Failed to add selection range for a clause of composition"); break; } // Set the style of the clause. nsCOMPtr selectionOfIMEPriv = do_QueryInterface(selectionOfIME); if (!selectionOfIMEPriv) { NS_WARNING("Failed to get nsISelectionPrivate interface from selection"); continue; // Since this is additional feature, we can continue this job. } rv = selectionOfIMEPriv->SetTextRangeStyle(clauseRange, textRange.mRangeStyle); if (NS_FAILED(rv)) { NS_WARNING("Failed to set selection style"); break; // but this is unexpected... } } // If the ranges doesn't include explicit caret position, let's set the // caret to the end of composition string. if (!setCaret) { int32_t caretOffset = static_cast(mOffset + insertedLength); MOZ_ASSERT(caretOffset >= 0 && static_cast(caretOffset) <= maxOffset); rv = selection->Collapse(mElement, caretOffset); NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to set caret at the end of composition string"); } rv = selPriv->EndBatchChanges(); NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to end batch changes"); return rv; }