tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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