diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /editor/libeditor/CompositionTransaction.cpp | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'editor/libeditor/CompositionTransaction.cpp')
-rw-r--r-- | editor/libeditor/CompositionTransaction.cpp | 390 |
1 files changed, 390 insertions, 0 deletions
diff --git a/editor/libeditor/CompositionTransaction.cpp b/editor/libeditor/CompositionTransaction.cpp new file mode 100644 index 0000000000..136718201a --- /dev/null +++ b/editor/libeditor/CompositionTransaction.cpp @@ -0,0 +1,390 @@ +/* -*- 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 "CompositionTransaction.h" + +#include "mozilla/EditorBase.h" // mEditorBase +#include "mozilla/SelectionState.h" // RangeUpdater +#include "mozilla/TextComposition.h" // TextComposition +#include "mozilla/dom/Selection.h" // local var +#include "mozilla/dom/Text.h" // mTextNode +#include "nsAString.h" // params +#include "nsDebug.h" // for NS_ASSERTION, etc +#include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc +#include "nsRange.h" // local var +#include "nsISelectionController.h" // for nsISelectionController constants +#include "nsQueryObject.h" // for do_QueryObject + +namespace mozilla { + +using namespace dom; + +// static +already_AddRefed<CompositionTransaction> CompositionTransaction::Create( + EditorBase& aEditorBase, const nsAString& aStringToInsert, + const EditorDOMPointInText& aPointToInsert) { + MOZ_ASSERT(aPointToInsert.IsSetAndValid()); + + TextComposition* composition = aEditorBase.GetComposition(); + MOZ_RELEASE_ASSERT(composition); + // XXX Actually, we get different text node and offset from editor in some + // cases. If composition stores text node, we should use it and offset + // in it. + EditorDOMPointInText pointToInsert; + if (Text* textNode = composition->GetContainerTextNode()) { + pointToInsert.Set(textNode, composition->XPOffsetInTextNode()); + NS_WARNING_ASSERTION( + pointToInsert.GetContainerAsText() == + composition->GetContainerTextNode(), + "The editor tries to insert composition string into different node"); + NS_WARNING_ASSERTION( + pointToInsert.Offset() == composition->XPOffsetInTextNode(), + "The editor tries to insert composition string into different offset"); + } else { + pointToInsert = aPointToInsert; + } + RefPtr<CompositionTransaction> transaction = + new CompositionTransaction(aEditorBase, aStringToInsert, pointToInsert); + // XXX Now, it might be better to modify the text node information of + // the TextComposition instance in DoTransaction() because updating + // the information before changing actual DOM tree is pretty odd. + composition->OnCreateCompositionTransaction( + aStringToInsert, pointToInsert.ContainerAsText(), pointToInsert.Offset()); + return transaction.forget(); +} + +CompositionTransaction::CompositionTransaction( + EditorBase& aEditorBase, const nsAString& aStringToInsert, + const EditorDOMPointInText& aPointToInsert) + : mTextNode(aPointToInsert.ContainerAsText()), + mOffset(aPointToInsert.Offset()), + mReplaceLength(aEditorBase.GetComposition()->XPLengthInTextNode()), + mRanges(aEditorBase.GetComposition()->GetRanges()), + mStringToInsert(aStringToInsert), + mEditorBase(&aEditorBase), + mFixed(false) { + MOZ_ASSERT(mTextNode->TextLength() >= mOffset); +} + +NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase, + mEditorBase, mTextNode) +// mRangeList can't lead to cycles + +NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction) +NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) +NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase) +NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase) + +NS_IMETHODIMP CompositionTransaction::DoTransaction() { + if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Fail before making any changes if there's no selection controller + if (NS_WARN_IF(!mEditorBase->GetSelectionController())) { + return NS_ERROR_NOT_AVAILABLE; + } + + OwningNonNull<EditorBase> editorBase = *mEditorBase; + OwningNonNull<Text> textNode = *mTextNode; + + // Advance caret: This requires the presentation shell to get the selection. + if (mReplaceLength == 0) { + ErrorResult error; + editorBase->DoInsertText(textNode, mOffset, mStringToInsert, error); + if (error.Failed()) { + NS_WARNING("EditorBase::DoInsertText() failed"); + return error.StealNSResult(); + } + editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset, + mStringToInsert.Length()); + } else { + // If composition string is split to multiple text nodes, we should put + // whole new composition string to the first text node and remove the + // compostion string in other nodes. + uint32_t replaceableLength = textNode->TextLength() - mOffset; + ErrorResult error; + editorBase->DoReplaceText(textNode, mOffset, mReplaceLength, + mStringToInsert, error); + if (error.Failed()) { + NS_WARNING("EditorBase::DoReplaceText() failed"); + return error.StealNSResult(); + } + + // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing + // this transaction will remove whole composition string. Therefore, + // selection should be restored at start of composition string. + // XXX Perhaps, this is a bug of our selection managemnt at undoing. + editorBase->RangeUpdaterRef().SelAdjDeleteText(textNode, mOffset, + replaceableLength); + // But some ranges which after the composition string should be restored + // as-is. + editorBase->RangeUpdaterRef().SelAdjInsertText(textNode, mOffset, + mStringToInsert.Length()); + + if (replaceableLength < mReplaceLength) { + // XXX Perhaps, scanning following sibling text nodes with composition + // string length which we know is wrong because there may be + // non-empty text nodes which are inserted by JS. Instead, we + // should remove all text in the ranges of IME selections. + int32_t remainLength = mReplaceLength - replaceableLength; + IgnoredErrorResult ignoredError; + for (nsIContent* nextSibling = textNode->GetNextSibling(); + nextSibling && nextSibling->IsText() && remainLength; + nextSibling = nextSibling->GetNextSibling()) { + OwningNonNull<Text> followingTextNode = + *static_cast<Text*>(nextSibling); + uint32_t textLength = followingTextNode->TextLength(); + editorBase->DoDeleteText(followingTextNode, 0, remainLength, + ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "EditorBase::DoDeleteText() failed, but ignored"); + ignoredError.SuppressException(); + // XXX Needs to check whether the text is deleted as expected. + editorBase->RangeUpdaterRef().SelAdjDeleteText(followingTextNode, 0, + remainLength); + remainLength -= textLength; + } + } + } + + nsresult rv = SetSelectionForRanges(); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "CompositionTransaction::SetSelectionForRanges() failed"); + return rv; +} + +NS_IMETHODIMP CompositionTransaction::UndoTransaction() { + if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) { + return NS_ERROR_NOT_AVAILABLE; + } + + // Get the selection first so we'll fail before making any changes if we + // can't get it + RefPtr<Selection> selection = mEditorBase->GetSelection(); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_NOT_AVAILABLE; + } + + OwningNonNull<EditorBase> editorBase = *mEditorBase; + OwningNonNull<Text> textNode = *mTextNode; + ErrorResult error; + editorBase->DoDeleteText(textNode, mOffset, mStringToInsert.Length(), error); + if (error.Failed()) { + NS_WARNING("EditorBase::DoDeleteText() failed"); + return error.StealNSResult(); + } + + // set the selection to the insertion point where the string was removed + nsresult rv = selection->CollapseInLimiter(textNode, mOffset); + NS_ASSERTION(NS_SUCCEEDED(rv), "Selection::CollapseInLimiter() failed"); + return rv; +} + +NS_IMETHODIMP CompositionTransaction::Merge(nsITransaction* aOtherTransaction, + bool* aDidMerge) { + if (NS_WARN_IF(!aOtherTransaction) || NS_WARN_IF(!aDidMerge)) { + return NS_ERROR_INVALID_ARG; + } + *aDidMerge = false; + + // Check to make sure we aren't fixed, if we are then nothing gets merged. + if (mFixed) { + return NS_OK; + } + + RefPtr<EditTransactionBase> otherTransactionBase = + aOtherTransaction->GetAsEditTransactionBase(); + if (!otherTransactionBase) { + return NS_OK; + } + + // If aTransaction is another CompositionTransaction then merge it + CompositionTransaction* otherCompositionTransaction = + otherTransactionBase->GetAsCompositionTransaction(); + if (!otherCompositionTransaction) { + return NS_OK; + } + + // We merge the next IME transaction by adopting its insert string. + mStringToInsert = otherCompositionTransaction->mStringToInsert; + mRanges = otherCompositionTransaction->mRanges; + *aDidMerge = true; + return NS_OK; +} + +void CompositionTransaction::MarkFixed() { mFixed = true; } + +/* ============ private methods ================== */ + +nsresult CompositionTransaction::SetSelectionForRanges() { + if (NS_WARN_IF(!mEditorBase) || NS_WARN_IF(!mTextNode)) { + return NS_ERROR_NOT_AVAILABLE; + } + OwningNonNull<EditorBase> editorBase = *mEditorBase; + OwningNonNull<Text> textNode = *mTextNode; + RefPtr<TextRangeArray> ranges = mRanges; + nsresult rv = SetIMESelection(editorBase, textNode, mOffset, + mStringToInsert.Length(), ranges); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "CompositionTransaction::SetIMESelection() failed"); + return rv; +} + +// static +nsresult CompositionTransaction::SetIMESelection( + EditorBase& aEditorBase, Text* aTextNode, uint32_t aOffsetInNode, + uint32_t aLengthOfCompositionString, const TextRangeArray* aRanges) { + RefPtr<Selection> selection = aEditorBase.GetSelection(); + if (NS_WARN_IF(!selection)) { + return NS_ERROR_NOT_INITIALIZED; + } + + SelectionBatcher selectionBatcher(selection); + + // First, remove all selections of IME composition. + static const RawSelectionType kIMESelections[] = { + nsISelectionController::SELECTION_IME_RAWINPUT, + nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT, + nsISelectionController::SELECTION_IME_CONVERTEDTEXT, + nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT}; + + nsCOMPtr<nsISelectionController> selectionController = + aEditorBase.GetSelectionController(); + if (NS_WARN_IF(!selectionController)) { + return NS_ERROR_NOT_INITIALIZED; + } + + IgnoredErrorResult ignoredError; + for (uint32_t i = 0; i < ArrayLength(kIMESelections); ++i) { + RefPtr<Selection> selectionOfIME = + selectionController->GetSelection(kIMESelections[i]); + if (!selectionOfIME) { + NS_WARNING("nsISelectionController::GetSelection() failed"); + continue; + } + selectionOfIME->RemoveAllRanges(ignoredError); + NS_WARNING_ASSERTION(!ignoredError.Failed(), + "Selection::RemoveAllRanges() failed, but ignored"); + ignoredError.SuppressException(); + } + + // 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. + nsresult rv = NS_OK; + 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 == TextRangeType::eCaret) { + NS_ASSERTION(!setCaret, "The ranges already has caret position"); + NS_ASSERTION(!textRange.Length(), + "EditorBase doesn't support wide caret"); + int32_t caretOffset = static_cast<int32_t>( + aOffsetInNode + + std::min(textRange.mStartOffset, aLengthOfCompositionString)); + MOZ_ASSERT(caretOffset >= 0 && + static_cast<uint32_t>(caretOffset) <= maxOffset); + rv = selection->CollapseInLimiter(aTextNode, caretOffset); + NS_WARNING_ASSERTION( + NS_SUCCEEDED(rv), + "Selection::CollapseInLimiter() failed, but might be ignored"); + setCaret = setCaret || NS_SUCCEEDED(rv); + if (!setCaret) { + continue; + } + // If caret range is specified explicitly, we should show the caret if + // it should be so. + aEditorBase.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<nsRange> clauseRange; + int32_t startOffset = static_cast<int32_t>( + aOffsetInNode + + std::min(textRange.mStartOffset, aLengthOfCompositionString)); + MOZ_ASSERT(startOffset >= 0 && + static_cast<uint32_t>(startOffset) <= maxOffset); + int32_t endOffset = static_cast<int32_t>( + aOffsetInNode + + std::min(textRange.mEndOffset, aLengthOfCompositionString)); + MOZ_ASSERT(endOffset >= startOffset && + static_cast<uint32_t>(endOffset) <= maxOffset); + clauseRange = nsRange::Create(aTextNode, startOffset, aTextNode, endOffset, + IgnoreErrors()); + if (!clauseRange) { + NS_WARNING("nsRange::Create() failed, but might be ignored"); + break; + } + + // Set the range of the clause to selection. + RefPtr<Selection> selectionOfIME = selectionController->GetSelection( + ToRawSelectionType(textRange.mRangeType)); + if (!selectionOfIME) { + NS_WARNING( + "nsISelectionController::GetSelection() failed, but might be " + "ignored"); + break; + } + + IgnoredErrorResult ignoredError; + selectionOfIME->AddRangeAndSelectFramesAndNotifyListeners(*clauseRange, + ignoredError); + if (ignoredError.Failed()) { + NS_WARNING( + "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed, but " + "might be ignored"); + break; + } + + // Set the style of the clause. + rv = selectionOfIME->SetTextRangeStyle(clauseRange, textRange.mRangeStyle); + if (NS_FAILED(rv)) { + NS_WARNING("Selection::SetTextRangeStyle() failed, but might be ignored"); + 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<int32_t>(aOffsetInNode + aLengthOfCompositionString); + MOZ_ASSERT(caretOffset >= 0 && + static_cast<uint32_t>(caretOffset) <= maxOffset); + rv = selection->CollapseInLimiter(aTextNode, caretOffset); + NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), + "Selection::CollapseInLimiter() failed"); + + // If caret range isn't specified explicitly, we should hide the caret. + // Hiding the caret benefits a Windows build (see bug 555642 comment #6). + // However, when there is no range, we should keep showing caret. + if (countOfRanges) { + aEditorBase.HideCaret(true); + } + } + + return rv; +} + +} // namespace mozilla |