/* -*- 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/dom/Selection.h" // local var #include "mozilla/dom/Text.h" // mTextNode #include "mozilla/Preferences.h" // nsCaret Visibility #include "nsAString.h" // params #include "nsDebug.h" // for NS_ASSERTION, etc #include "nsEditor.h" // mEditor #include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc #include "nsIPresShell.h" // nsISelectionController constants #include "nsRange.h" // local var #include "nsQueryObject.h" // for do_QueryObject using namespace mozilla; using namespace mozilla::dom; /*static*/ bool IMETextTxn::sCaretsExtendedVisibility = false; IMETextTxn::IMETextTxn(Text& aTextNode, uint32_t aOffset, uint32_t aReplaceLength, TextRangeArray* aTextRangeArray, const nsAString& aStringToInsert, nsEditor& aEditor) : EditTxn() , mTextNode(&aTextNode) , mOffset(aOffset) , mReplaceLength(aReplaceLength) , mRanges(aTextRangeArray) , mStringToInsert(aStringToInsert) , mEditor(aEditor) , mFixed(false) { static bool addedPrefs = false; if (!addedPrefs) { mozilla::Preferences::AddBoolVarCache(&sCaretsExtendedVisibility, "layout.accessiblecaret.extendedvisibility"); addedPrefs = true; } } IMETextTxn::~IMETextTxn() { } NS_IMPL_CYCLE_COLLECTION_INHERITED(IMETextTxn, EditTxn, mTextNode) // mRangeList can't lead to cycles NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(IMETextTxn) if (aIID.Equals(NS_GET_IID(IMETextTxn))) { foundInterface = static_cast(this); } else NS_INTERFACE_MAP_END_INHERITING(EditTxn) NS_IMPL_ADDREF_INHERITED(IMETextTxn, EditTxn) NS_IMPL_RELEASE_INHERITED(IMETextTxn, EditTxn) NS_IMETHODIMP IMETextTxn::DoTransaction() { // Fail before making any changes if there's no selection controller 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 res; if (mReplaceLength == 0) { res = mTextNode->InsertData(mOffset, mStringToInsert); } else { res = mTextNode->ReplaceData(mOffset, mReplaceLength, mStringToInsert); } NS_ENSURE_SUCCESS(res, res); res = SetSelectionForRanges(); NS_ENSURE_SUCCESS(res, res); return NS_OK; } NS_IMETHODIMP IMETextTxn::UndoTransaction() { // Get the selection first so we'll fail before making any changes if we // can't get it RefPtr selection = mEditor.GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); nsresult res = mTextNode->DeleteData(mOffset, mStringToInsert.Length()); NS_ENSURE_SUCCESS(res, res); // set the selection to the insertion point where the string was removed res = selection->Collapse(mTextNode, mOffset); NS_ASSERTION(NS_SUCCEEDED(res), "Selection could not be collapsed after undo of IME insert."); NS_ENSURE_SUCCESS(res, res); return NS_OK; } NS_IMETHODIMP IMETextTxn::Merge(nsITransaction* aTransaction, bool* aDidMerge) { NS_ENSURE_ARG_POINTER(aTransaction && aDidMerge); // Check to make sure we aren't fixed, if we are then nothing gets absorbed if (mFixed) { *aDidMerge = false; return NS_OK; } // If aTransaction is another IMETextTxn then absorb it RefPtr otherTxn = do_QueryObject(aTransaction); if (otherTxn) { // We absorb the next IME transaction by adopting its insert string mStringToInsert = otherTxn->mStringToInsert; mRanges = otherTxn->mRanges; *aDidMerge = true; return NS_OK; } *aDidMerge = false; return NS_OK; } void IMETextTxn::MarkFixed() { mFixed = true; } NS_IMETHODIMP IMETextTxn::GetTxnDescription(nsAString& aString) { aString.AssignLiteral("IMETextTxn: "); aString += mStringToInsert; return NS_OK; } /* ============ private 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() { return SetIMESelection(mEditor, mTextNode, mOffset, mStringToInsert.Length(), mRanges); } // static nsresult IMETextTxn::SetIMESelection(nsEditor& aEditor, Text* aTextNode, uint32_t aOffsetInNode, uint32_t aLengthOfCompositionString, const TextRangeArray* aRanges) { RefPtr selection = aEditor.GetSelection(); NS_ENSURE_TRUE(selection, NS_ERROR_NOT_INITIALIZED); nsresult rv = selection->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 }; nsCOMPtr selCon; aEditor.GetSelectionController(getter_AddRefs(selCon)); NS_ENSURE_TRUE(selCon, NS_ERROR_NOT_INITIALIZED); for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) { nsCOMPtr selectionOfIME; if (NS_FAILED(selCon->GetSelection(kIMESelections[i], getter_AddRefs(selectionOfIME)))) { continue; } 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 = aRanges ? aRanges->Length() : 0; #ifdef DEBUG // Bounds-checking on debug builds uint32_t maxOffset = aTextNode->Length(); #endif // NOTE: composition string may be truncated when it's committed and // maxlength attribute value doesn't allow input of all text of this // composition. for (uint32_t i = 0; i < countOfRanges; ++i) { const TextRange& textRange = aRanges->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( aOffsetInNode + std::min(textRange.mStartOffset, aLengthOfCompositionString)); MOZ_ASSERT(caretOffset >= 0 && static_cast(caretOffset) <= maxOffset); rv = selection->Collapse(aTextNode, caretOffset); setCaret = setCaret || NS_SUCCEEDED(rv); if (NS_WARN_IF(!setCaret)) { continue; } // If caret range is specified explicitly, we should show the caret if // it should be so. aEditor.HideCaret(false); continue; } // If the clause length is 0, it should be a bug. if (!textRange.Length()) { NS_WARNING("Any clauses must not be empty"); continue; } RefPtr clauseRange; int32_t startOffset = static_cast( aOffsetInNode + std::min(textRange.mStartOffset, aLengthOfCompositionString)); MOZ_ASSERT(startOffset >= 0 && static_cast(startOffset) <= maxOffset); int32_t endOffset = static_cast( aOffsetInNode + std::min(textRange.mEndOffset, aLengthOfCompositionString)); MOZ_ASSERT(endOffset >= startOffset && static_cast(endOffset) <= maxOffset); rv = nsRange::CreateRange(aTextNode, startOffset, aTextNode, 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(aOffsetInNode + aLengthOfCompositionString); MOZ_ASSERT(caretOffset >= 0 && static_cast(caretOffset) <= maxOffset); rv = selection->Collapse(aTextNode, caretOffset); NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to set caret at the end of composition string"); // If caret range isn't specified explicitly, we should hide the caret. // Hiding the caret benefits a Windows build (see bug 555642 comment #6), // but causes loss of Fennec AccessibleCaret visibility during Caret drag. if (!sCaretsExtendedVisibility) { aEditor.HideCaret(true); } } rv = selection->EndBatchChangesInternal(); NS_ASSERTION(NS_SUCCEEDED(rv), "Failed to end batch changes"); return rv; }