CompositionTransaction.cpp (20358B)
1 /* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ 2 /* This Source Code Form is subject to the terms of the Mozilla Public 3 * License, v. 2.0. If a copy of the MPL was not distributed with this 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 5 6 #include "CompositionTransaction.h" 7 8 #include "mozilla/EditorBase.h" // mEditorBase 9 #include "mozilla/Logging.h" 10 #include "mozilla/SelectionState.h" // RangeUpdater 11 #include "mozilla/TextComposition.h" // TextComposition 12 #include "mozilla/TextEditor.h" // TextEditor 13 #include "mozilla/ToString.h" 14 #include "mozilla/dom/Selection.h" // local var 15 #include "mozilla/dom/Text.h" // mTextNode 16 #include "nsAString.h" // params 17 #include "nsDebug.h" // for NS_ASSERTION, etc 18 #include "nsError.h" // for NS_SUCCEEDED, NS_FAILED, etc 19 #include "nsRange.h" // local var 20 #include "nsISelectionController.h" // for nsISelectionController constants 21 #include "nsQueryObject.h" // for do_QueryObject 22 23 namespace mozilla { 24 25 using namespace dom; 26 27 // static 28 already_AddRefed<CompositionTransaction> CompositionTransaction::Create( 29 EditorBase& aEditorBase, const nsAString& aStringToInsert, 30 const EditorDOMPointInText& aPointToInsert) { 31 MOZ_ASSERT(aPointToInsert.IsSetAndValid()); 32 33 TextComposition* composition = aEditorBase.GetComposition(); 34 MOZ_RELEASE_ASSERT(composition); 35 // XXX Actually, we get different text node and offset from editor in some 36 // cases. If composition stores text node, we should use it and offset 37 // in it. 38 EditorDOMPointInText pointToInsert; 39 if (Text* textNode = composition->GetContainerTextNode()) { 40 pointToInsert.Set(textNode, composition->XPOffsetInTextNode()); 41 NS_WARNING_ASSERTION( 42 pointToInsert.GetContainerAs<Text>() == 43 composition->GetContainerTextNode(), 44 "The editor tries to insert composition string into different node"); 45 NS_WARNING_ASSERTION( 46 pointToInsert.Offset() == composition->XPOffsetInTextNode(), 47 "The editor tries to insert composition string into different offset"); 48 } else { 49 pointToInsert = aPointToInsert; 50 } 51 RefPtr<CompositionTransaction> transaction = 52 aEditorBase.IsTextEditor() 53 ? new CompositionTransaction(aEditorBase, aStringToInsert, 54 pointToInsert) 55 : new CompositionInTextNodeTransaction(aEditorBase, aStringToInsert, 56 pointToInsert); 57 return transaction.forget(); 58 } 59 60 CompositionTransaction::CompositionTransaction( 61 EditorBase& aEditorBase, const nsAString& aStringToInsert, 62 const EditorDOMPointInText& aPointToInsert) 63 : mOffset(aPointToInsert.Offset()), 64 mReplaceLength(aEditorBase.GetComposition()->XPLengthInTextNode()), 65 mRanges(aEditorBase.GetComposition()->GetRanges()), 66 mStringToInsert(aStringToInsert), 67 mEditorBase(&aEditorBase), 68 mFixed(false) { 69 MOZ_ASSERT(aPointToInsert.ContainerAs<Text>()->TextDataLength() >= mOffset); 70 } 71 72 std::ostream& operator<<(std::ostream& aStream, 73 const CompositionTransaction& aTransaction) { 74 const auto* transactionForHTMLEditor = 75 aTransaction.GetAsCompositionInTextNodeTransaction(); 76 if (transactionForHTMLEditor) { 77 return aStream << *transactionForHTMLEditor; 78 } 79 aStream << "{ mOffset=" << aTransaction.mOffset 80 << ", mReplaceLength=" << aTransaction.mReplaceLength 81 << ", mRanges={ Length()=" << aTransaction.mRanges->Length() << " }" 82 << ", mStringToInsert=\"" 83 << NS_ConvertUTF16toUTF8(aTransaction.mStringToInsert).get() << "\"" 84 << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }"; 85 return aStream; 86 } 87 88 NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionTransaction, EditTransactionBase, 89 mEditorBase) 90 // mRangeList can't lead to cycles 91 92 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionTransaction) 93 NS_INTERFACE_MAP_END_INHERITING(EditTransactionBase) 94 NS_IMPL_ADDREF_INHERITED(CompositionTransaction, EditTransactionBase) 95 NS_IMPL_RELEASE_INHERITED(CompositionTransaction, EditTransactionBase) 96 97 Text* CompositionTransaction::GetTextNode() const { 98 if (MOZ_UNLIKELY(!mEditorBase)) { 99 return nullptr; 100 } 101 if (TextEditor* const textEditor = mEditorBase->GetAsTextEditor()) { 102 return textEditor->GetTextNode(); 103 } 104 MOZ_ASSERT(GetAsCompositionInTextNodeTransaction()); 105 return GetAsCompositionInTextNodeTransaction()->mTextNode; 106 } 107 108 NS_IMETHODIMP CompositionTransaction::DoTransaction() { 109 MOZ_LOG(GetLogModule(), LogLevel::Info, 110 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__, 111 ToString(*this).c_str())); 112 113 if (NS_WARN_IF(!mEditorBase)) { 114 return NS_ERROR_NOT_AVAILABLE; 115 } 116 const RefPtr<Text> textNode = GetTextNode(); 117 if (NS_WARN_IF(!textNode)) { 118 return NS_ERROR_NOT_AVAILABLE; 119 } 120 121 // Fail before making any changes if there's no selection controller 122 if (NS_WARN_IF(!mEditorBase->GetSelectionController())) { 123 return NS_ERROR_NOT_AVAILABLE; 124 } 125 126 const OwningNonNull<EditorBase> editorBase = *mEditorBase; 127 128 // Advance caret: This requires the presentation shell to get the selection. 129 if (mReplaceLength == 0) { 130 IgnoredErrorResult error; 131 editorBase->DoInsertText(*textNode, mOffset, mStringToInsert, error); 132 if (error.Failed()) { 133 NS_WARNING("EditorBase::DoInsertText() failed"); 134 return error.StealNSResult(); 135 } 136 editorBase->RangeUpdaterRef().SelAdjInsertText(*textNode, mOffset, 137 mStringToInsert.Length()); 138 } else { 139 // If composition string is split to multiple text nodes, we should put 140 // whole new composition string to the first text node and remove the 141 // compostion string in other nodes. 142 // TODO: This should be handled by `TextComposition` because this assumes 143 // that composition string has never touched by JS. However, it 144 // would occur if the web app is a corrabolation software which 145 // multiple users can modify anyware in an editor. 146 // TODO: And if composition starts from a following text node, the offset 147 // here is outdated and it will cause inserting composition string 148 // **before** the proper point from point of view of the users. 149 uint32_t replaceableLength = textNode->TextLength() - mOffset; 150 IgnoredErrorResult error; 151 editorBase->DoReplaceText(*textNode, mOffset, mReplaceLength, 152 mStringToInsert, error); 153 if (error.Failed()) { 154 NS_WARNING("EditorBase::DoReplaceText() failed"); 155 return error.StealNSResult(); 156 } 157 158 // Don't use RangeUpdaterRef().SelAdjReplaceText() here because undoing 159 // this transaction will remove whole composition string. Therefore, 160 // selection should be restored at start of composition string. 161 // XXX Perhaps, this is a bug of our selection managemnt at undoing. 162 editorBase->RangeUpdaterRef().SelAdjDeleteText(*textNode, mOffset, 163 replaceableLength); 164 // But some ranges which after the composition string should be restored 165 // as-is. 166 editorBase->RangeUpdaterRef().SelAdjInsertText(*textNode, mOffset, 167 mStringToInsert.Length()); 168 169 if (replaceableLength < mReplaceLength) { 170 // XXX Perhaps, scanning following sibling text nodes with composition 171 // string length which we know is wrong because there may be 172 // non-empty text nodes which are inserted by JS. Instead, we 173 // should remove all text in the ranges of IME selections. 174 uint32_t remainLength = mReplaceLength - replaceableLength; 175 IgnoredErrorResult ignoredError; 176 for (nsIContent* nextSibling = textNode->GetNextSibling(); 177 nextSibling && nextSibling->IsText() && remainLength; 178 nextSibling = nextSibling->GetNextSibling()) { 179 OwningNonNull<Text> followingTextNode = 180 *static_cast<Text*>(nextSibling); 181 uint32_t textLength = followingTextNode->TextLength(); 182 editorBase->DoDeleteText(followingTextNode, 0, remainLength, 183 ignoredError); 184 NS_WARNING_ASSERTION(!ignoredError.Failed(), 185 "EditorBase::DoDeleteText() failed, but ignored"); 186 ignoredError.SuppressException(); 187 // XXX Needs to check whether the text is deleted as expected. 188 editorBase->RangeUpdaterRef().SelAdjDeleteText(followingTextNode, 0, 189 remainLength); 190 remainLength -= textLength; 191 } 192 } 193 } 194 195 nsresult rv = SetSelectionForRanges(); 196 NS_WARNING_ASSERTION( 197 NS_SUCCEEDED(rv), 198 "CompositionTransaction::SetSelectionForRanges() failed"); 199 200 if (TextComposition* composition = editorBase->GetComposition()) { 201 composition->OnUpdateCompositionInEditor(mStringToInsert, *textNode, 202 mOffset); 203 } 204 205 return rv; 206 } 207 208 NS_IMETHODIMP CompositionTransaction::UndoTransaction() { 209 MOZ_LOG(GetLogModule(), LogLevel::Info, 210 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__, 211 ToString(*this).c_str())); 212 213 if (NS_WARN_IF(!mEditorBase)) { 214 return NS_ERROR_NOT_AVAILABLE; 215 } 216 const RefPtr<Text> textNode = GetTextNode(); 217 if (NS_WARN_IF(!textNode)) { 218 return NS_ERROR_NOT_AVAILABLE; 219 } 220 221 const OwningNonNull<EditorBase> editorBase = *mEditorBase; 222 IgnoredErrorResult error; 223 editorBase->DoDeleteText(*textNode, mOffset, mStringToInsert.Length(), error); 224 if (MOZ_UNLIKELY(error.Failed())) { 225 NS_WARNING("EditorBase::DoDeleteText() failed"); 226 return error.StealNSResult(); 227 } 228 229 // set the selection to the insertion point where the string was removed 230 editorBase->CollapseSelectionTo(EditorRawDOMPoint(textNode, mOffset), error); 231 NS_ASSERTION(!error.Failed(), "EditorBase::CollapseSelectionTo() failed"); 232 return error.StealNSResult(); 233 } 234 235 NS_IMETHODIMP CompositionTransaction::RedoTransaction() { 236 MOZ_LOG(GetLogModule(), LogLevel::Info, 237 ("%p CompositionTransaction::%s this=%s", this, __FUNCTION__, 238 ToString(*this).c_str())); 239 return DoTransaction(); 240 } 241 242 NS_IMETHODIMP CompositionTransaction::Merge(nsITransaction* aOtherTransaction, 243 bool* aDidMerge) { 244 MOZ_LOG(GetLogModule(), LogLevel::Debug, 245 ("%p CompositionTransaction::%s(aOtherTransaction=%p) this=%s", this, 246 __FUNCTION__, aOtherTransaction, ToString(*this).c_str())); 247 248 if (NS_WARN_IF(!aOtherTransaction) || NS_WARN_IF(!aDidMerge)) { 249 return NS_ERROR_INVALID_ARG; 250 } 251 *aDidMerge = false; 252 253 // Check to make sure we aren't fixed, if we are then nothing gets merged. 254 if (mFixed) { 255 MOZ_LOG(GetLogModule(), LogLevel::Debug, 256 ("%p CompositionTransaction::%s returned false due to fixed", this, 257 __FUNCTION__)); 258 return NS_OK; 259 } 260 261 RefPtr<EditTransactionBase> otherTransactionBase = 262 aOtherTransaction->GetAsEditTransactionBase(); 263 if (!otherTransactionBase) { 264 MOZ_LOG(GetLogModule(), LogLevel::Debug, 265 ("%p CompositionTransaction::%s returned false due to not edit " 266 "transaction", 267 this, __FUNCTION__)); 268 return NS_OK; 269 } 270 271 // If aTransaction is another CompositionTransaction then merge it 272 CompositionTransaction* otherCompositionTransaction = 273 otherTransactionBase->GetAsCompositionTransaction(); 274 if (!otherCompositionTransaction) { 275 return NS_OK; 276 } 277 278 // We merge the next IME transaction by adopting its insert string. 279 mStringToInsert = otherCompositionTransaction->mStringToInsert; 280 mRanges = otherCompositionTransaction->mRanges; 281 *aDidMerge = true; 282 MOZ_LOG(GetLogModule(), LogLevel::Debug, 283 ("%p CompositionTransaction::%s returned true", this, __FUNCTION__)); 284 return NS_OK; 285 } 286 287 void CompositionTransaction::MarkFixed() { mFixed = true; } 288 289 /* ============ private methods ================== */ 290 291 nsresult CompositionTransaction::SetSelectionForRanges() { 292 if (NS_WARN_IF(!mEditorBase)) { 293 return NS_ERROR_NOT_AVAILABLE; 294 } 295 const RefPtr<Text> textNode = GetTextNode(); 296 if (NS_WARN_IF(!textNode)) { 297 return NS_ERROR_NOT_AVAILABLE; 298 } 299 const OwningNonNull<EditorBase> editorBase = *mEditorBase; 300 RefPtr<TextRangeArray> ranges = mRanges; 301 nsresult rv = SetIMESelection(editorBase, textNode, mOffset, 302 mStringToInsert.Length(), ranges); 303 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 304 "CompositionTransaction::SetIMESelection() failed"); 305 return rv; 306 } 307 308 // static 309 nsresult CompositionTransaction::SetIMESelection( 310 EditorBase& aEditorBase, Text* aTextNode, uint32_t aOffsetInNode, 311 uint32_t aLengthOfCompositionString, const TextRangeArray* aRanges) { 312 RefPtr<Selection> selection = aEditorBase.GetSelection(); 313 if (NS_WARN_IF(!selection)) { 314 return NS_ERROR_NOT_INITIALIZED; 315 } 316 317 SelectionBatcher selectionBatcher(selection, __FUNCTION__); 318 319 // First, remove all selections of IME composition. 320 static const RawSelectionType kIMESelections[] = { 321 nsISelectionController::SELECTION_IME_RAWINPUT, 322 nsISelectionController::SELECTION_IME_SELECTEDRAWTEXT, 323 nsISelectionController::SELECTION_IME_CONVERTEDTEXT, 324 nsISelectionController::SELECTION_IME_SELECTEDCONVERTEDTEXT}; 325 326 nsCOMPtr<nsISelectionController> selectionController = 327 aEditorBase.GetSelectionController(); 328 if (NS_WARN_IF(!selectionController)) { 329 return NS_ERROR_NOT_INITIALIZED; 330 } 331 332 IgnoredErrorResult ignoredError; 333 for (uint32_t i = 0; i < std::size(kIMESelections); ++i) { 334 RefPtr<Selection> selectionOfIME = 335 selectionController->GetSelection(kIMESelections[i]); 336 if (!selectionOfIME) { 337 NS_WARNING("nsISelectionController::GetSelection() failed"); 338 continue; 339 } 340 selectionOfIME->RemoveAllRanges(ignoredError); 341 NS_WARNING_ASSERTION(!ignoredError.Failed(), 342 "Selection::RemoveAllRanges() failed, but ignored"); 343 ignoredError.SuppressException(); 344 } 345 346 // Set caret position and selection of IME composition with TextRangeArray. 347 bool setCaret = false; 348 uint32_t countOfRanges = aRanges ? aRanges->Length() : 0; 349 350 #ifdef DEBUG 351 // Bounds-checking on debug builds 352 uint32_t maxOffset = aTextNode->Length(); 353 #endif 354 355 // NOTE: composition string may be truncated when it's committed and 356 // maxlength attribute value doesn't allow input of all text of this 357 // composition. 358 nsresult rv = NS_OK; 359 for (uint32_t i = 0; i < countOfRanges; ++i) { 360 const TextRange& textRange = aRanges->ElementAt(i); 361 362 // Caret needs special handling since its length may be 0 and if it's not 363 // specified explicitly, we need to handle it ourselves later. 364 if (textRange.mRangeType == TextRangeType::eCaret) { 365 NS_ASSERTION(!setCaret, "The ranges already has caret position"); 366 NS_ASSERTION(!textRange.Length(), 367 "EditorBase doesn't support wide caret"); 368 CheckedUint32 caretOffset(aOffsetInNode); 369 caretOffset += 370 std::min(textRange.mStartOffset, aLengthOfCompositionString); 371 MOZ_ASSERT(caretOffset.isValid()); 372 MOZ_ASSERT(caretOffset.value() <= maxOffset); 373 rv = selection->CollapseInLimiter(aTextNode, caretOffset.value()); 374 NS_WARNING_ASSERTION( 375 NS_SUCCEEDED(rv), 376 "Selection::CollapseInLimiter() failed, but might be ignored"); 377 setCaret = setCaret || NS_SUCCEEDED(rv); 378 if (!setCaret) { 379 continue; 380 } 381 // If caret range is specified explicitly, we should show the caret if 382 // it should be so. 383 aEditorBase.HideCaret(false); 384 continue; 385 } 386 387 // If the clause length is 0, it should be a bug. 388 if (!textRange.Length()) { 389 NS_WARNING("Any clauses must not be empty"); 390 continue; 391 } 392 393 RefPtr<nsRange> clauseRange; 394 CheckedUint32 startOffset = aOffsetInNode; 395 startOffset += std::min(textRange.mStartOffset, aLengthOfCompositionString); 396 MOZ_ASSERT(startOffset.isValid()); 397 MOZ_ASSERT(startOffset.value() <= maxOffset); 398 CheckedUint32 endOffset = aOffsetInNode; 399 endOffset += std::min(textRange.mEndOffset, aLengthOfCompositionString); 400 MOZ_ASSERT(endOffset.isValid()); 401 MOZ_ASSERT(endOffset.value() >= startOffset.value()); 402 MOZ_ASSERT(endOffset.value() <= maxOffset); 403 clauseRange = nsRange::Create(aTextNode, startOffset.value(), aTextNode, 404 endOffset.value(), IgnoreErrors()); 405 if (!clauseRange) { 406 NS_WARNING("nsRange::Create() failed, but might be ignored"); 407 break; 408 } 409 410 // Set the range of the clause to selection. 411 RefPtr<Selection> selectionOfIME = selectionController->GetSelection( 412 ToRawSelectionType(textRange.mRangeType)); 413 if (!selectionOfIME) { 414 NS_WARNING( 415 "nsISelectionController::GetSelection() failed, but might be " 416 "ignored"); 417 break; 418 } 419 420 IgnoredErrorResult ignoredError; 421 selectionOfIME->AddRangeAndSelectFramesAndNotifyListeners(*clauseRange, 422 ignoredError); 423 if (ignoredError.Failed()) { 424 NS_WARNING( 425 "Selection::AddRangeAndSelectFramesAndNotifyListeners() failed, but " 426 "might be ignored"); 427 break; 428 } 429 430 // Set the style of the clause. 431 rv = selectionOfIME->SetTextRangeStyle(clauseRange, textRange.mRangeStyle); 432 if (NS_FAILED(rv)) { 433 NS_WARNING("Selection::SetTextRangeStyle() failed, but might be ignored"); 434 break; // but this is unexpected... 435 } 436 } 437 438 // If the ranges doesn't include explicit caret position, let's set the 439 // caret to the end of composition string. 440 if (!setCaret) { 441 CheckedUint32 caretOffset = aOffsetInNode; 442 caretOffset += aLengthOfCompositionString; 443 MOZ_ASSERT(caretOffset.isValid()); 444 MOZ_ASSERT(caretOffset.value() <= maxOffset); 445 rv = selection->CollapseInLimiter(aTextNode, caretOffset.value()); 446 NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), 447 "Selection::CollapseInLimiter() failed"); 448 449 // If caret range isn't specified explicitly, we should hide the caret. 450 // Hiding the caret benefits a Windows build (see bug 555642 comment #6). 451 // However, when there is no range, we should keep showing caret. 452 if (countOfRanges) { 453 aEditorBase.HideCaret(true); 454 } 455 } 456 457 return rv; 458 } 459 460 /****************************************************************************** 461 * mozilla::CompositionInTextNodeTransaction 462 ******************************************************************************/ 463 464 CompositionInTextNodeTransaction::CompositionInTextNodeTransaction( 465 EditorBase& aEditorBase, const nsAString& aStringToInsert, 466 const EditorDOMPointInText& aPointToInsert) 467 : CompositionTransaction(aEditorBase, aStringToInsert, aPointToInsert), 468 mTextNode(aPointToInsert.ContainerAs<Text>()) { 469 MOZ_ASSERT(aEditorBase.IsHTMLEditor()); 470 } 471 472 std::ostream& operator<<(std::ostream& aStream, 473 const CompositionInTextNodeTransaction& aTransaction) { 474 aStream << "{ mTextNode=" << aTransaction.mTextNode.get(); 475 if (aTransaction.mTextNode) { 476 aStream << " (" << *aTransaction.mTextNode << ")"; 477 } 478 aStream << ", mOffset=" << aTransaction.mOffset 479 << ", mReplaceLength=" << aTransaction.mReplaceLength 480 << ", mRanges={ Length()=" << aTransaction.mRanges->Length() << " }" 481 << ", mStringToInsert=\"" 482 << NS_ConvertUTF16toUTF8(aTransaction.mStringToInsert).get() << "\"" 483 << ", mEditorBase=" << aTransaction.mEditorBase.get() << " }"; 484 return aStream; 485 } 486 487 NS_IMPL_CYCLE_COLLECTION_INHERITED(CompositionInTextNodeTransaction, 488 CompositionTransaction, mTextNode) 489 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(CompositionInTextNodeTransaction) 490 NS_INTERFACE_MAP_END_INHERITING(CompositionTransaction) 491 NS_IMPL_ADDREF_INHERITED(CompositionInTextNodeTransaction, 492 CompositionTransaction) 493 NS_IMPL_RELEASE_INHERITED(CompositionInTextNodeTransaction, 494 CompositionTransaction) 495 496 } // namespace mozilla