tor-browser

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

TransactionManager.cpp (17352B)


      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 "TransactionManager.h"
      7 
      8 #include "mozilla/Assertions.h"
      9 #include "mozilla/HTMLEditor.h"
     10 #include "mozilla/mozalloc.h"
     11 #include "mozilla/TransactionStack.h"
     12 #include "nsCOMPtr.h"
     13 #include "nsDebug.h"
     14 #include "nsError.h"
     15 #include "nsISupports.h"
     16 #include "nsISupportsUtils.h"
     17 #include "nsITransaction.h"
     18 #include "nsIWeakReference.h"
     19 #include "TransactionItem.h"
     20 
     21 namespace mozilla {
     22 
     23 TransactionManager::TransactionManager(int32_t aMaxTransactionCount)
     24    : mMaxTransactionCount(aMaxTransactionCount),
     25      mDoStack(TransactionStack::FOR_UNDO),
     26      mUndoStack(TransactionStack::FOR_UNDO),
     27      mRedoStack(TransactionStack::FOR_REDO) {}
     28 
     29 NS_IMPL_CYCLE_COLLECTION_CLASS(TransactionManager)
     30 
     31 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TransactionManager)
     32  NS_IMPL_CYCLE_COLLECTION_UNLINK(mHTMLEditor)
     33  tmp->mDoStack.DoUnlink();
     34  tmp->mUndoStack.DoUnlink();
     35  tmp->mRedoStack.DoUnlink();
     36  NS_IMPL_CYCLE_COLLECTION_UNLINK_WEAK_REFERENCE
     37 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
     38 
     39 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TransactionManager)
     40  NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mHTMLEditor)
     41  tmp->mDoStack.DoTraverse(cb);
     42  tmp->mUndoStack.DoTraverse(cb);
     43  tmp->mRedoStack.DoTraverse(cb);
     44 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
     45 
     46 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransactionManager)
     47  NS_INTERFACE_MAP_ENTRY(nsITransactionManager)
     48  NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference)
     49  NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports, nsITransactionManager)
     50 NS_INTERFACE_MAP_END
     51 
     52 NS_IMPL_CYCLE_COLLECTING_ADDREF(TransactionManager)
     53 NS_IMPL_CYCLE_COLLECTING_RELEASE(TransactionManager)
     54 
     55 void TransactionManager::Attach(HTMLEditor& aHTMLEditor) {
     56  mHTMLEditor = &aHTMLEditor;
     57 }
     58 
     59 void TransactionManager::Detach(const HTMLEditor& aHTMLEditor) {
     60  MOZ_DIAGNOSTIC_ASSERT_IF(mHTMLEditor, &aHTMLEditor == mHTMLEditor);
     61  if (mHTMLEditor == &aHTMLEditor) {
     62    mHTMLEditor = nullptr;
     63  }
     64 }
     65 
     66 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
     67 TransactionManager::DoTransaction(nsITransaction* aTransaction) {
     68  if (NS_WARN_IF(!aTransaction)) {
     69    return NS_ERROR_INVALID_ARG;
     70  }
     71  OwningNonNull<nsITransaction> transaction = *aTransaction;
     72 
     73  nsresult rv = BeginTransaction(transaction, nullptr);
     74  if (NS_FAILED(rv)) {
     75    NS_WARNING("TransactionManager::BeginTransaction() failed");
     76    DidDoNotify(transaction, rv);
     77    return rv;
     78  }
     79 
     80  rv = EndTransaction(false);
     81  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
     82                       "TransactionManager::EndTransaction() failed");
     83 
     84  DidDoNotify(transaction, rv);
     85  return rv;
     86 }
     87 
     88 MOZ_CAN_RUN_SCRIPT NS_IMETHODIMP TransactionManager::UndoTransaction() {
     89  return Undo();
     90 }
     91 
     92 nsresult TransactionManager::Undo() {
     93  // It's possible to be called Undo() again while the transaction manager is
     94  // executing a transaction's DoTransaction() method.  If this happens,
     95  // the Undo() request is ignored, and we return NS_ERROR_FAILURE.  This
     96  // may occur if a mutation event listener calls document.execCommand("undo").
     97  if (NS_WARN_IF(!mDoStack.IsEmpty())) {
     98    return NS_ERROR_FAILURE;
     99  }
    100 
    101  // Peek at the top of the undo stack. Don't remove the transaction
    102  // until it has successfully completed.
    103  RefPtr<TransactionItem> transactionItem = mUndoStack.Peek();
    104  if (!transactionItem) {
    105    // Bail if there's nothing on the stack.
    106    return NS_OK;
    107  }
    108 
    109  nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
    110  nsresult rv = transactionItem->UndoTransaction(this);
    111  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    112                       "TransactionItem::UndoTransaction() failed");
    113  if (NS_SUCCEEDED(rv)) {
    114    transactionItem = mUndoStack.Pop();
    115    mRedoStack.Push(transactionItem.forget());
    116  }
    117 
    118  if (transaction) {
    119    DidUndoNotify(*transaction, rv);
    120  }
    121  return rv;
    122 }
    123 
    124 MOZ_CAN_RUN_SCRIPT_BOUNDARY NS_IMETHODIMP
    125 TransactionManager::RedoTransaction() {
    126  return Redo();
    127 }
    128 
    129 nsresult TransactionManager::Redo() {
    130  // It's possible to be called Redo() again while the transaction manager is
    131  // executing a transaction's DoTransaction() method.  If this happens,
    132  // the Redo() request is ignored, and we return NS_ERROR_FAILURE.  This
    133  // may occur if a mutation event listener calls document.execCommand("redo").
    134  if (NS_WARN_IF(!mDoStack.IsEmpty())) {
    135    return NS_ERROR_FAILURE;
    136  }
    137 
    138  // Peek at the top of the redo stack. Don't remove the transaction
    139  // until it has successfully completed.
    140  RefPtr<TransactionItem> transactionItem = mRedoStack.Peek();
    141  if (!transactionItem) {
    142    // Bail if there's nothing on the stack.
    143    return NS_OK;
    144  }
    145 
    146  nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
    147  nsresult rv = transactionItem->RedoTransaction(this);
    148  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    149                       "TransactionItem::RedoTransaction() failed");
    150  if (NS_SUCCEEDED(rv)) {
    151    transactionItem = mRedoStack.Pop();
    152    mUndoStack.Push(transactionItem.forget());
    153  }
    154 
    155  if (transaction) {
    156    DidRedoNotify(*transaction, rv);
    157  }
    158  return rv;
    159 }
    160 
    161 NS_IMETHODIMP TransactionManager::Clear() {
    162  return NS_WARN_IF(!ClearUndoRedo()) ? NS_ERROR_FAILURE : NS_OK;
    163 }
    164 
    165 NS_IMETHODIMP TransactionManager::BeginBatch(nsISupports* aData) {
    166  nsresult rv = BeginBatchInternal(aData);
    167  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    168                       "TransactionManager::BeginBatchInternal() failed");
    169  return rv;
    170 }
    171 
    172 nsresult TransactionManager::BeginBatchInternal(nsISupports* aData) {
    173  // We can batch independent transactions together by simply pushing
    174  // a dummy transaction item on the do stack. This dummy transaction item
    175  // will be popped off the do stack, and then pushed on the undo stack
    176  // in EndBatch().
    177  nsresult rv = BeginTransaction(nullptr, aData);
    178  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    179                       "TransactionManager::BeginTransaction() failed");
    180  return rv;
    181 }
    182 
    183 NS_IMETHODIMP TransactionManager::EndBatch(bool aAllowEmpty) {
    184  nsresult rv = EndBatchInternal(aAllowEmpty);
    185  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    186                       "TransactionManager::EndBatchInternal() failed");
    187  return rv;
    188 }
    189 
    190 nsresult TransactionManager::EndBatchInternal(bool aAllowEmpty) {
    191  // XXX: Need to add some mechanism to detect the case where the transaction
    192  //      at the top of the do stack isn't the dummy transaction, so we can
    193  //      throw an error!! This can happen if someone calls EndBatch() within
    194  //      the DoTransaction() method of a transaction.
    195  //
    196  //      For now, we can detect this case by checking the value of the
    197  //      dummy transaction's mTransaction field. If it is our dummy
    198  //      transaction, it should be nullptr. This may not be true in the
    199  //      future when we allow users to execute a transaction when beginning
    200  //      a batch!!!!
    201  RefPtr<TransactionItem> transactionItem = mDoStack.Peek();
    202  if (NS_WARN_IF(!transactionItem)) {
    203    return NS_ERROR_FAILURE;
    204  }
    205  nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
    206  if (NS_WARN_IF(transaction)) {
    207    return NS_ERROR_FAILURE;
    208  }
    209 
    210  nsresult rv = EndTransaction(aAllowEmpty);
    211  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    212                       "TransactionManager::EndTransaction() failed");
    213  return rv;
    214 }
    215 
    216 NS_IMETHODIMP TransactionManager::GetNumberOfUndoItems(int32_t* aNumItems) {
    217  *aNumItems = static_cast<int32_t>(NumberOfUndoItems());
    218  MOZ_ASSERT(*aNumItems >= 0);
    219  return NS_OK;
    220 }
    221 
    222 NS_IMETHODIMP TransactionManager::GetNumberOfRedoItems(int32_t* aNumItems) {
    223  *aNumItems = static_cast<int32_t>(NumberOfRedoItems());
    224  MOZ_ASSERT(*aNumItems >= 0);
    225  return NS_OK;
    226 }
    227 
    228 NS_IMETHODIMP TransactionManager::GetMaxTransactionCount(int32_t* aMaxCount) {
    229  if (NS_WARN_IF(!aMaxCount)) {
    230    return NS_ERROR_INVALID_ARG;
    231  }
    232  *aMaxCount = mMaxTransactionCount;
    233  return NS_OK;
    234 }
    235 
    236 NS_IMETHODIMP TransactionManager::SetMaxTransactionCount(int32_t aMaxCount) {
    237  return NS_WARN_IF(!EnableUndoRedo(aMaxCount)) ? NS_ERROR_FAILURE : NS_OK;
    238 }
    239 
    240 bool TransactionManager::EnableUndoRedo(int32_t aMaxTransactionCount) {
    241  // It is illegal to call EnableUndoRedo() while the transaction manager is
    242  // executing a transaction's DoTransaction() method because the undo and redo
    243  // stacks might get pruned.  If this happens, the EnableUndoRedo() request is
    244  // ignored, and we return false.
    245  if (NS_WARN_IF(!mDoStack.IsEmpty())) {
    246    return false;
    247  }
    248 
    249  // If aMaxTransactionCount is 0, it means to disable undo/redo.
    250  if (!aMaxTransactionCount) {
    251    mUndoStack.Clear();
    252    mRedoStack.Clear();
    253    mMaxTransactionCount = 0;
    254    return true;
    255  }
    256 
    257  // If aMaxTransactionCount is less than zero, the user wants unlimited
    258  // levels of undo! No need to prune the undo or redo stacks.
    259  if (aMaxTransactionCount < 0) {
    260    mMaxTransactionCount = -1;
    261    return true;
    262  }
    263 
    264  // If new max transaction count is greater than or equal to current max
    265  // transaction count, we don't need to remove any transactions.
    266  if (mMaxTransactionCount >= 0 &&
    267      mMaxTransactionCount <= aMaxTransactionCount) {
    268    mMaxTransactionCount = aMaxTransactionCount;
    269    return true;
    270  }
    271 
    272  // If aMaxTransactionCount is greater than the number of transactions that
    273  // currently exist on the undo and redo stack, there is no need to prune the
    274  // undo or redo stacks.
    275  size_t numUndoItems = NumberOfUndoItems();
    276  size_t numRedoItems = NumberOfRedoItems();
    277  size_t total = numUndoItems + numRedoItems;
    278  size_t newMaxTransactionCount = static_cast<size_t>(aMaxTransactionCount);
    279  if (newMaxTransactionCount > total) {
    280    mMaxTransactionCount = aMaxTransactionCount;
    281    return true;
    282  }
    283 
    284  // Try getting rid of some transactions on the undo stack! Start at
    285  // the bottom of the stack and pop towards the top.
    286  for (; numUndoItems && (numRedoItems + numUndoItems) > newMaxTransactionCount;
    287       numUndoItems--) {
    288    RefPtr<TransactionItem> transactionItem = mUndoStack.PopBottom();
    289    MOZ_ASSERT(transactionItem);
    290  }
    291 
    292  // If necessary, get rid of some transactions on the redo stack! Start at
    293  // the bottom of the stack and pop towards the top.
    294  for (; numRedoItems && (numRedoItems + numUndoItems) > newMaxTransactionCount;
    295       numRedoItems--) {
    296    RefPtr<TransactionItem> transactionItem = mRedoStack.PopBottom();
    297    MOZ_ASSERT(transactionItem);
    298  }
    299 
    300  mMaxTransactionCount = aMaxTransactionCount;
    301  return true;
    302 }
    303 
    304 NS_IMETHODIMP TransactionManager::PeekUndoStack(nsITransaction** aTransaction) {
    305  MOZ_ASSERT(aTransaction);
    306  *aTransaction = PeekUndoStack().take();
    307  return NS_OK;
    308 }
    309 
    310 already_AddRefed<nsITransaction> TransactionManager::PeekUndoStack() {
    311  RefPtr<TransactionItem> transactionItem = mUndoStack.Peek();
    312  if (!transactionItem) {
    313    return nullptr;
    314  }
    315  return transactionItem->GetTransaction();
    316 }
    317 
    318 NS_IMETHODIMP TransactionManager::PeekRedoStack(nsITransaction** aTransaction) {
    319  MOZ_ASSERT(aTransaction);
    320  *aTransaction = PeekRedoStack().take();
    321  return NS_OK;
    322 }
    323 
    324 already_AddRefed<nsITransaction> TransactionManager::PeekRedoStack() {
    325  RefPtr<TransactionItem> transactionItem = mRedoStack.Peek();
    326  if (!transactionItem) {
    327    return nullptr;
    328  }
    329  return transactionItem->GetTransaction();
    330 }
    331 
    332 already_AddRefed<nsITransaction> TransactionManager::PopUndoStack() {
    333  if (NS_WARN_IF(!mRedoStack.IsEmpty())) {
    334    return nullptr;
    335  }
    336  const RefPtr<TransactionItem> transactionItem = mUndoStack.Pop();
    337  if (NS_WARN_IF(!transactionItem)) {
    338    return nullptr;
    339  }
    340  return transactionItem->GetTransaction();
    341 }
    342 
    343 nsresult TransactionManager::BatchTopUndo() {
    344  if (mUndoStack.GetSize() < 2) {
    345    // Not enough transactions to merge into one batch.
    346    return NS_OK;
    347  }
    348 
    349  RefPtr<TransactionItem> lastUndo = mUndoStack.Pop();
    350  MOZ_ASSERT(lastUndo, "There should be at least two transactions.");
    351 
    352  RefPtr<TransactionItem> previousUndo = mUndoStack.Peek();
    353  MOZ_ASSERT(previousUndo, "There should be at least two transactions.");
    354 
    355  nsresult rv = previousUndo->AddChild(*lastUndo);
    356  NS_WARNING_ASSERTION(NS_SUCCEEDED(rv), "TransactionItem::AddChild() failed");
    357 
    358  // Transfer data from the transactions that is going to be
    359  // merged to the transaction that it is being merged with.
    360  nsCOMArray<nsISupports>& lastData = lastUndo->GetData();
    361  nsCOMArray<nsISupports>& previousData = previousUndo->GetData();
    362  if (!previousData.AppendObjects(lastData)) {
    363    NS_WARNING("nsISupports::AppendObjects() failed");
    364    return NS_ERROR_FAILURE;
    365  }
    366  lastData.Clear();
    367  return rv;
    368 }
    369 
    370 nsresult TransactionManager::RemoveTopUndo() {
    371  if (mUndoStack.IsEmpty()) {
    372    return NS_OK;
    373  }
    374 
    375  RefPtr<TransactionItem> lastUndo = mUndoStack.Pop();
    376  return NS_OK;
    377 }
    378 
    379 NS_IMETHODIMP TransactionManager::ClearUndoStack() {
    380  if (NS_WARN_IF(!mDoStack.IsEmpty())) {
    381    return NS_ERROR_FAILURE;
    382  }
    383  mUndoStack.Clear();
    384  return NS_OK;
    385 }
    386 
    387 NS_IMETHODIMP TransactionManager::ClearRedoStack() {
    388  if (NS_WARN_IF(!mDoStack.IsEmpty())) {
    389    return NS_ERROR_FAILURE;
    390  }
    391  mRedoStack.Clear();
    392  return NS_OK;
    393 }
    394 
    395 void TransactionManager::DidDoNotify(nsITransaction& aTransaction,
    396                                     nsresult aDoResult) {
    397  if (mHTMLEditor) {
    398    RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
    399    htmlEditor->DidDoTransaction(*this, aTransaction, aDoResult);
    400  }
    401 }
    402 
    403 void TransactionManager::DidUndoNotify(nsITransaction& aTransaction,
    404                                       nsresult aUndoResult) {
    405  if (mHTMLEditor) {
    406    RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
    407    htmlEditor->DidUndoTransaction(*this, aTransaction, aUndoResult);
    408  }
    409 }
    410 
    411 void TransactionManager::DidRedoNotify(nsITransaction& aTransaction,
    412                                       nsresult aRedoResult) {
    413  if (mHTMLEditor) {
    414    RefPtr<HTMLEditor> htmlEditor(mHTMLEditor);
    415    htmlEditor->DidRedoTransaction(*this, aTransaction, aRedoResult);
    416  }
    417 }
    418 
    419 nsresult TransactionManager::BeginTransaction(nsITransaction* aTransaction,
    420                                              nsISupports* aData) {
    421  // XXX: POSSIBLE OPTIMIZATION
    422  //      We could use a factory that pre-allocates/recycles transaction items.
    423  RefPtr<TransactionItem> transactionItem = new TransactionItem(aTransaction);
    424 
    425  if (aData) {
    426    nsCOMArray<nsISupports>& data = transactionItem->GetData();
    427    data.AppendObject(aData);
    428  }
    429 
    430  mDoStack.Push(transactionItem);
    431 
    432  nsresult rv = transactionItem->DoTransaction();
    433  if (NS_FAILED(rv)) {
    434    NS_WARNING("TransactionItem::DoTransaction() failed");
    435    transactionItem = mDoStack.Pop();
    436  }
    437  return rv;
    438 }
    439 
    440 nsresult TransactionManager::EndTransaction(bool aAllowEmpty) {
    441  RefPtr<TransactionItem> transactionItem = mDoStack.Pop();
    442  if (NS_WARN_IF(!transactionItem)) {
    443    return NS_ERROR_FAILURE;
    444  }
    445 
    446  nsCOMPtr<nsITransaction> transaction = transactionItem->GetTransaction();
    447  if (!transaction && !aAllowEmpty) {
    448    // If we get here, the transaction must be a dummy batch transaction
    449    // created by BeginBatch(). If it contains no children, get rid of it!
    450    if (!transactionItem->NumberOfChildren()) {
    451      return NS_OK;
    452    }
    453  }
    454 
    455  // Check if the transaction is transient. If it is, there's nothing
    456  // more to do, just return.
    457  if (transaction) {
    458    bool isTransient = false;
    459    nsresult rv = transaction->GetIsTransient(&isTransient);
    460    if (NS_FAILED(rv)) {
    461      NS_WARNING("nsITransaction::GetIsTransient() failed");
    462      return rv;
    463    }
    464    // XXX: Should we be clearing the redo stack if the transaction
    465    //      is transient and there is nothing on the do stack?
    466    if (isTransient) {
    467      return NS_OK;
    468    }
    469  }
    470 
    471  if (!mMaxTransactionCount) {
    472    return NS_OK;
    473  }
    474 
    475  // Check if there is a transaction on the do stack. If there is,
    476  // the current transaction is a "sub" transaction, and should
    477  // be added to the transaction at the top of the do stack.
    478  RefPtr<TransactionItem> topTransactionItem = mDoStack.Peek();
    479  if (topTransactionItem) {
    480    // XXX: What do we do if this fails?
    481    nsresult rv = topTransactionItem->AddChild(*transactionItem);
    482    NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    483                         "TransactionItem::AddChild() failed");
    484    return rv;
    485  }
    486 
    487  // The transaction succeeded, so clear the redo stack.
    488  mRedoStack.Clear();
    489 
    490  // Check if we can coalesce this transaction with the one at the top
    491  // of the undo stack.
    492  topTransactionItem = mUndoStack.Peek();
    493  if (transaction && topTransactionItem) {
    494    bool didMerge = false;
    495    nsCOMPtr<nsITransaction> topTransaction =
    496        topTransactionItem->GetTransaction();
    497    if (topTransaction) {
    498      nsresult rv = topTransaction->Merge(transaction, &didMerge);
    499      if (didMerge) {
    500        return rv;
    501      }
    502      NS_WARNING_ASSERTION(NS_SUCCEEDED(rv),
    503                           "nsITransaction::Merge() failed, but ignored");
    504    }
    505  }
    506 
    507  // Check to see if we've hit the max level of undo. If so,
    508  // pop the bottom transaction off the undo stack and release it!
    509  int32_t sz = mUndoStack.GetSize();
    510  if (mMaxTransactionCount > 0 && sz >= mMaxTransactionCount) {
    511    RefPtr<TransactionItem> overflow = mUndoStack.PopBottom();
    512  }
    513 
    514  // Push the transaction on the undo stack:
    515  mUndoStack.Push(transactionItem.forget());
    516  return NS_OK;
    517 }
    518 
    519 }  // namespace mozilla