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/. */
6 #include "mozilla/TransactionManager.h"
8 #include "mozilla/Assertions.h"
9 #include "mozilla/mozalloc.h"
10 #include "mozilla/TransactionStack.h"
14 #include "nsISupportsBase.h"
15 #include "nsISupportsUtils.h"
16 #include "nsITransaction.h"
17 #include "nsITransactionListener.h"
18 #include "nsIWeakReference.h"
19 #include "TransactionItem.h"
23 TransactionManager::TransactionManager(int32_t aMaxTransactionCount
)
24 : mMaxTransactionCount(aMaxTransactionCount
),
25 mDoStack(TransactionStack::FOR_UNDO
),
26 mUndoStack(TransactionStack::FOR_UNDO
),
27 mRedoStack(TransactionStack::FOR_REDO
) {}
29 NS_IMPL_CYCLE_COLLECTION_CLASS(TransactionManager
)
31 NS_IMPL_CYCLE_COLLECTION_UNLINK_BEGIN(TransactionManager
)
32 NS_IMPL_CYCLE_COLLECTION_UNLINK(mListeners
)
33 tmp
->mDoStack
.DoUnlink();
34 tmp
->mUndoStack
.DoUnlink();
35 tmp
->mRedoStack
.DoUnlink();
36 NS_IMPL_CYCLE_COLLECTION_UNLINK_END
38 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_BEGIN(TransactionManager
)
39 NS_IMPL_CYCLE_COLLECTION_TRAVERSE(mListeners
)
40 tmp
->mDoStack
.DoTraverse(cb
);
41 tmp
->mUndoStack
.DoTraverse(cb
);
42 tmp
->mRedoStack
.DoTraverse(cb
);
43 NS_IMPL_CYCLE_COLLECTION_TRAVERSE_END
45 NS_INTERFACE_MAP_BEGIN_CYCLE_COLLECTION(TransactionManager
)
46 NS_INTERFACE_MAP_ENTRY(nsITransactionManager
)
47 NS_INTERFACE_MAP_ENTRY(nsISupportsWeakReference
)
48 NS_INTERFACE_MAP_ENTRY_AMBIGUOUS(nsISupports
, nsITransactionManager
)
51 NS_IMPL_CYCLE_COLLECTING_ADDREF(TransactionManager
)
52 NS_IMPL_CYCLE_COLLECTING_RELEASE(TransactionManager
)
55 TransactionManager::DoTransaction(nsITransaction
* aTransaction
) {
56 NS_ENSURE_TRUE(aTransaction
, NS_ERROR_NULL_POINTER
);
58 bool doInterrupt
= false;
60 nsresult rv
= WillDoNotify(aTransaction
, &doInterrupt
);
68 rv
= BeginTransaction(aTransaction
, nullptr);
70 DidDoNotify(aTransaction
, rv
);
74 rv
= EndTransaction(false);
76 nsresult rv2
= DidDoNotify(aTransaction
, rv
);
77 if (NS_SUCCEEDED(rv
)) {
81 // XXX The result of EndTransaction() or DidDoNotify() if EndTransaction()
87 TransactionManager::UndoTransaction() { return Undo(); }
89 nsresult
TransactionManager::Undo() {
90 // It's possible to be called Undo() again while the transaction manager is
91 // executing a transaction's DoTransaction() method. If this happens,
92 // the Undo() request is ignored, and we return NS_ERROR_FAILURE. This
93 // may occur if a mutation event listener calls document.execCommand("undo").
94 if (!mDoStack
.IsEmpty()) {
95 return NS_ERROR_FAILURE
;
98 // Peek at the top of the undo stack. Don't remove the transaction
99 // until it has successfully completed.
100 RefPtr
<TransactionItem
> transactionItem
= mUndoStack
.Peek();
101 if (!transactionItem
) {
102 // Bail if there's nothing on the stack.
106 nsCOMPtr
<nsITransaction
> transaction
= transactionItem
->GetTransaction();
107 bool doInterrupt
= false;
108 nsresult rv
= WillUndoNotify(transaction
, &doInterrupt
);
116 rv
= transactionItem
->UndoTransaction(this);
117 if (NS_SUCCEEDED(rv
)) {
118 transactionItem
= mUndoStack
.Pop();
119 mRedoStack
.Push(transactionItem
.forget());
122 nsresult rv2
= DidUndoNotify(transaction
, rv
);
123 if (NS_SUCCEEDED(rv
)) {
127 // XXX The result of UndoTransaction() or DidUndoNotify() if UndoTransaction()
133 TransactionManager::RedoTransaction() { return Redo(); }
135 nsresult
TransactionManager::Redo() {
136 // It's possible to be called Redo() again while the transaction manager is
137 // executing a transaction's DoTransaction() method. If this happens,
138 // the Redo() request is ignored, and we return NS_ERROR_FAILURE. This
139 // may occur if a mutation event listener calls document.execCommand("redo").
140 if (!mDoStack
.IsEmpty()) {
141 return NS_ERROR_FAILURE
;
144 // Peek at the top of the redo stack. Don't remove the transaction
145 // until it has successfully completed.
146 RefPtr
<TransactionItem
> transactionItem
= mRedoStack
.Peek();
147 if (!transactionItem
) {
148 // Bail if there's nothing on the stack.
152 nsCOMPtr
<nsITransaction
> transaction
= transactionItem
->GetTransaction();
153 bool doInterrupt
= false;
154 nsresult rv
= WillRedoNotify(transaction
, &doInterrupt
);
162 rv
= transactionItem
->RedoTransaction(this);
163 if (NS_SUCCEEDED(rv
)) {
164 transactionItem
= mRedoStack
.Pop();
165 mUndoStack
.Push(transactionItem
.forget());
168 nsresult rv2
= DidRedoNotify(transaction
, rv
);
169 if (NS_SUCCEEDED(rv
)) {
173 // XXX The result of RedoTransaction() or DidRedoNotify() if RedoTransaction()
179 TransactionManager::Clear() {
180 return ClearUndoRedo() ? NS_OK
: NS_ERROR_FAILURE
;
184 TransactionManager::BeginBatch(nsISupports
* aData
) {
185 nsresult rv
= BeginBatchInternal(aData
);
186 if (NS_WARN_IF(NS_FAILED(rv
))) {
192 nsresult
TransactionManager::BeginBatchInternal(nsISupports
* aData
) {
193 // We can batch independent transactions together by simply pushing
194 // a dummy transaction item on the do stack. This dummy transaction item
195 // will be popped off the do stack, and then pushed on the undo stack
197 bool doInterrupt
= false;
198 nsresult rv
= WillBeginBatchNotify(&doInterrupt
);
206 rv
= BeginTransaction(0, aData
);
208 nsresult rv2
= DidBeginBatchNotify(rv
);
209 if (NS_SUCCEEDED(rv
)) {
213 // XXX The result of BeginTransaction() or DidBeginBatchNotify() if
214 // BeginTransaction() succeeded.
219 TransactionManager::EndBatch(bool aAllowEmpty
) {
220 nsresult rv
= EndBatchInternal(aAllowEmpty
);
221 if (NS_WARN_IF(NS_FAILED(rv
))) {
227 nsresult
TransactionManager::EndBatchInternal(bool aAllowEmpty
) {
228 // XXX: Need to add some mechanism to detect the case where the transaction
229 // at the top of the do stack isn't the dummy transaction, so we can
230 // throw an error!! This can happen if someone calls EndBatch() within
231 // the DoTransaction() method of a transaction.
233 // For now, we can detect this case by checking the value of the
234 // dummy transaction's mTransaction field. If it is our dummy
235 // transaction, it should be nullptr. This may not be true in the
236 // future when we allow users to execute a transaction when beginning
238 RefPtr
<TransactionItem
> transactionItem
= mDoStack
.Peek();
239 if (!transactionItem
) {
240 return NS_ERROR_FAILURE
;
242 nsCOMPtr
<nsITransaction
> transaction
= transactionItem
->GetTransaction();
244 return NS_ERROR_FAILURE
;
247 bool doInterrupt
= false;
248 nsresult rv
= WillEndBatchNotify(&doInterrupt
);
256 rv
= EndTransaction(aAllowEmpty
);
257 nsresult rv2
= DidEndBatchNotify(rv
);
258 if (NS_SUCCEEDED(rv
)) {
262 // XXX The result of EndTransaction() or DidEndBatchNotify() if
263 // EndTransaction() succeeded.
268 TransactionManager::GetNumberOfUndoItems(int32_t* aNumItems
) {
269 *aNumItems
= static_cast<int32_t>(NumberOfUndoItems());
270 MOZ_ASSERT(*aNumItems
>= 0);
275 TransactionManager::GetNumberOfRedoItems(int32_t* aNumItems
) {
276 *aNumItems
= static_cast<int32_t>(NumberOfRedoItems());
277 MOZ_ASSERT(*aNumItems
>= 0);
282 TransactionManager::GetMaxTransactionCount(int32_t* aMaxCount
) {
283 NS_ENSURE_TRUE(aMaxCount
, NS_ERROR_NULL_POINTER
);
284 *aMaxCount
= mMaxTransactionCount
;
289 TransactionManager::SetMaxTransactionCount(int32_t aMaxCount
) {
290 return EnableUndoRedo(aMaxCount
) ? NS_OK
: NS_ERROR_FAILURE
;
293 bool TransactionManager::EnableUndoRedo(int32_t aMaxTransactionCount
) {
294 // It is illegal to call EnableUndoRedo() while the transaction manager is
295 // executing a transaction's DoTransaction() method because the undo and redo
296 // stacks might get pruned. If this happens, the EnableUndoRedo() request is
297 // ignored, and we return false.
298 if (NS_WARN_IF(!mDoStack
.IsEmpty())) {
302 // If aMaxTransactionCount is 0, it means to disable undo/redo.
303 if (!aMaxTransactionCount
) {
306 mMaxTransactionCount
= 0;
310 // If aMaxTransactionCount is less than zero, the user wants unlimited
311 // levels of undo! No need to prune the undo or redo stacks.
312 if (aMaxTransactionCount
< 0) {
313 mMaxTransactionCount
= -1;
317 // If new max transaction count is greater than or equal to current max
318 // transaction count, we don't need to remove any transactions.
319 if (mMaxTransactionCount
>= 0 &&
320 mMaxTransactionCount
<= aMaxTransactionCount
) {
321 mMaxTransactionCount
= aMaxTransactionCount
;
325 // If aMaxTransactionCount is greater than the number of transactions that
326 // currently exist on the undo and redo stack, there is no need to prune the
327 // undo or redo stacks.
328 size_t numUndoItems
= NumberOfUndoItems();
329 size_t numRedoItems
= NumberOfRedoItems();
330 size_t total
= numUndoItems
+ numRedoItems
;
331 size_t newMaxTransactionCount
= static_cast<size_t>(aMaxTransactionCount
);
332 if (newMaxTransactionCount
> total
) {
333 mMaxTransactionCount
= aMaxTransactionCount
;
337 // Try getting rid of some transactions on the undo stack! Start at
338 // the bottom of the stack and pop towards the top.
339 for (; numUndoItems
&& (numRedoItems
+ numUndoItems
) > newMaxTransactionCount
;
341 RefPtr
<TransactionItem
> transactionItem
= mUndoStack
.PopBottom();
342 MOZ_ASSERT(transactionItem
);
345 // If necessary, get rid of some transactions on the redo stack! Start at
346 // the bottom of the stack and pop towards the top.
347 for (; numRedoItems
&& (numRedoItems
+ numUndoItems
) > newMaxTransactionCount
;
349 RefPtr
<TransactionItem
> transactionItem
= mRedoStack
.PopBottom();
350 MOZ_ASSERT(transactionItem
);
353 mMaxTransactionCount
= aMaxTransactionCount
;
358 TransactionManager::PeekUndoStack(nsITransaction
** aTransaction
) {
359 MOZ_ASSERT(aTransaction
);
360 *aTransaction
= PeekUndoStack().take();
364 already_AddRefed
<nsITransaction
> TransactionManager::PeekUndoStack() {
365 RefPtr
<TransactionItem
> transactionItem
= mUndoStack
.Peek();
366 if (!transactionItem
) {
369 return transactionItem
->GetTransaction();
373 TransactionManager::PeekRedoStack(nsITransaction
** aTransaction
) {
374 MOZ_ASSERT(aTransaction
);
375 *aTransaction
= PeekRedoStack().take();
379 already_AddRefed
<nsITransaction
> TransactionManager::PeekRedoStack() {
380 RefPtr
<TransactionItem
> transactionItem
= mRedoStack
.Peek();
381 if (!transactionItem
) {
384 return transactionItem
->GetTransaction();
387 nsresult
TransactionManager::BatchTopUndo() {
388 if (mUndoStack
.GetSize() < 2) {
389 // Not enough transactions to merge into one batch.
393 RefPtr
<TransactionItem
> lastUndo
= mUndoStack
.Pop();
394 MOZ_ASSERT(lastUndo
, "There should be at least two transactions.");
396 RefPtr
<TransactionItem
> previousUndo
= mUndoStack
.Peek();
397 MOZ_ASSERT(previousUndo
, "There should be at least two transactions.");
399 nsresult rv
= previousUndo
->AddChild(lastUndo
);
401 // Transfer data from the transactions that is going to be
402 // merged to the transaction that it is being merged with.
403 nsCOMArray
<nsISupports
>& lastData
= lastUndo
->GetData();
404 nsCOMArray
<nsISupports
>& previousData
= previousUndo
->GetData();
405 NS_ENSURE_TRUE(previousData
.AppendObjects(lastData
), NS_ERROR_UNEXPECTED
);
410 nsresult
TransactionManager::RemoveTopUndo() {
411 if (mUndoStack
.IsEmpty()) {
415 RefPtr
<TransactionItem
> lastUndo
= mUndoStack
.Pop();
420 TransactionManager::AddListener(nsITransactionListener
* aListener
) {
421 if (NS_WARN_IF(!aListener
)) {
422 return NS_ERROR_INVALID_ARG
;
424 return AddTransactionListener(*aListener
) ? NS_OK
: NS_ERROR_FAILURE
;
428 TransactionManager::RemoveListener(nsITransactionListener
* aListener
) {
429 if (NS_WARN_IF(!aListener
)) {
430 return NS_ERROR_INVALID_ARG
;
432 return RemoveTransactionListener(*aListener
) ? NS_OK
: NS_ERROR_FAILURE
;
436 TransactionManager::ClearUndoStack() {
437 if (NS_WARN_IF(!mDoStack
.IsEmpty())) {
438 return NS_ERROR_FAILURE
;
445 TransactionManager::ClearRedoStack() {
446 if (NS_WARN_IF(!mDoStack
.IsEmpty())) {
447 return NS_ERROR_FAILURE
;
453 nsresult
TransactionManager::WillDoNotify(nsITransaction
* aTransaction
,
455 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
456 nsITransactionListener
* listener
= mListeners
[i
];
457 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
459 nsresult rv
= listener
->WillDo(this, aTransaction
, aInterrupt
);
460 if (NS_FAILED(rv
) || *aInterrupt
) {
467 nsresult
TransactionManager::DidDoNotify(nsITransaction
* aTransaction
,
468 nsresult aDoResult
) {
469 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
470 nsITransactionListener
* listener
= mListeners
[i
];
471 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
473 nsresult rv
= listener
->DidDo(this, aTransaction
, aDoResult
);
481 nsresult
TransactionManager::WillUndoNotify(nsITransaction
* aTransaction
,
483 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
484 nsITransactionListener
* listener
= mListeners
[i
];
485 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
487 nsresult rv
= listener
->WillUndo(this, aTransaction
, aInterrupt
);
488 if (NS_FAILED(rv
) || *aInterrupt
) {
495 nsresult
TransactionManager::DidUndoNotify(nsITransaction
* aTransaction
,
496 nsresult aUndoResult
) {
497 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
498 nsITransactionListener
* listener
= mListeners
[i
];
499 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
501 nsresult rv
= listener
->DidUndo(this, aTransaction
, aUndoResult
);
509 nsresult
TransactionManager::WillRedoNotify(nsITransaction
* aTransaction
,
511 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
512 nsITransactionListener
* listener
= mListeners
[i
];
513 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
515 nsresult rv
= listener
->WillRedo(this, aTransaction
, aInterrupt
);
516 if (NS_FAILED(rv
) || *aInterrupt
) {
523 nsresult
TransactionManager::DidRedoNotify(nsITransaction
* aTransaction
,
524 nsresult aRedoResult
) {
525 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
526 nsITransactionListener
* listener
= mListeners
[i
];
527 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
529 nsresult rv
= listener
->DidRedo(this, aTransaction
, aRedoResult
);
537 nsresult
TransactionManager::WillBeginBatchNotify(bool* aInterrupt
) {
538 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
539 nsITransactionListener
* listener
= mListeners
[i
];
540 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
542 nsresult rv
= listener
->WillBeginBatch(this, aInterrupt
);
543 if (NS_FAILED(rv
) || *aInterrupt
) {
550 nsresult
TransactionManager::DidBeginBatchNotify(nsresult aResult
) {
551 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
552 nsITransactionListener
* listener
= mListeners
[i
];
553 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
555 nsresult rv
= listener
->DidBeginBatch(this, aResult
);
563 nsresult
TransactionManager::WillEndBatchNotify(bool* aInterrupt
) {
564 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
565 nsITransactionListener
* listener
= mListeners
[i
];
566 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
568 nsresult rv
= listener
->WillEndBatch(this, aInterrupt
);
569 if (NS_FAILED(rv
) || *aInterrupt
) {
576 nsresult
TransactionManager::DidEndBatchNotify(nsresult aResult
) {
577 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
578 nsITransactionListener
* listener
= mListeners
[i
];
579 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
581 nsresult rv
= listener
->DidEndBatch(this, aResult
);
589 nsresult
TransactionManager::WillMergeNotify(nsITransaction
* aTop
,
590 nsITransaction
* aTransaction
,
592 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
593 nsITransactionListener
* listener
= mListeners
[i
];
594 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
596 nsresult rv
= listener
->WillMerge(this, aTop
, aTransaction
, aInterrupt
);
597 if (NS_FAILED(rv
) || *aInterrupt
) {
604 nsresult
TransactionManager::DidMergeNotify(nsITransaction
* aTop
,
605 nsITransaction
* aTransaction
,
607 nsresult aMergeResult
) {
608 for (int32_t i
= 0, lcount
= mListeners
.Count(); i
< lcount
; i
++) {
609 nsITransactionListener
* listener
= mListeners
[i
];
610 NS_ENSURE_TRUE(listener
, NS_ERROR_FAILURE
);
613 listener
->DidMerge(this, aTop
, aTransaction
, aDidMerge
, aMergeResult
);
621 nsresult
TransactionManager::BeginTransaction(nsITransaction
* aTransaction
,
622 nsISupports
* aData
) {
623 // XXX: POSSIBLE OPTIMIZATION
624 // We could use a factory that pre-allocates/recycles transaction items.
625 RefPtr
<TransactionItem
> transactionItem
= new TransactionItem(aTransaction
);
628 nsCOMArray
<nsISupports
>& data
= transactionItem
->GetData();
629 data
.AppendObject(aData
);
632 mDoStack
.Push(transactionItem
);
634 nsresult rv
= transactionItem
->DoTransaction();
636 transactionItem
= mDoStack
.Pop();
642 nsresult
TransactionManager::EndTransaction(bool aAllowEmpty
) {
643 RefPtr
<TransactionItem
> transactionItem
= mDoStack
.Pop();
644 if (!transactionItem
) {
645 return NS_ERROR_FAILURE
;
648 nsCOMPtr
<nsITransaction
> transaction
= transactionItem
->GetTransaction();
649 if (!transaction
&& !aAllowEmpty
) {
650 // If we get here, the transaction must be a dummy batch transaction
651 // created by BeginBatch(). If it contains no children, get rid of it!
653 transactionItem
->GetNumberOfChildren(&nc
);
659 // Check if the transaction is transient. If it is, there's nothing
660 // more to do, just return.
661 bool isTransient
= false;
662 nsresult rv
= transaction
? transaction
->GetIsTransient(&isTransient
) : NS_OK
;
663 if (NS_FAILED(rv
) || isTransient
|| !mMaxTransactionCount
) {
664 // XXX: Should we be clearing the redo stack if the transaction
665 // is transient and there is nothing on the do stack?
669 // Check if there is a transaction on the do stack. If there is,
670 // the current transaction is a "sub" transaction, and should
671 // be added to the transaction at the top of the do stack.
672 RefPtr
<TransactionItem
> topTransactionItem
= mDoStack
.Peek();
673 if (topTransactionItem
) {
674 // XXX: What do we do if this fails?
675 return topTransactionItem
->AddChild(transactionItem
);
678 // The transaction succeeded, so clear the redo stack.
681 // Check if we can coalesce this transaction with the one at the top
682 // of the undo stack.
683 topTransactionItem
= mUndoStack
.Peek();
684 if (transaction
&& topTransactionItem
) {
685 bool didMerge
= false;
686 nsCOMPtr
<nsITransaction
> topTransaction
=
687 topTransactionItem
->GetTransaction();
688 if (topTransaction
) {
689 bool doInterrupt
= false;
690 rv
= WillMergeNotify(topTransaction
, transaction
, &doInterrupt
);
691 NS_ENSURE_SUCCESS(rv
, rv
);
694 rv
= topTransaction
->Merge(transaction
, &didMerge
);
696 DidMergeNotify(topTransaction
, transaction
, didMerge
, rv
);
697 if (NS_SUCCEEDED(rv
)) {
701 // XXX: What do we do if this fails?
710 // Check to see if we've hit the max level of undo. If so,
711 // pop the bottom transaction off the undo stack and release it!
712 int32_t sz
= mUndoStack
.GetSize();
713 if (mMaxTransactionCount
> 0 && sz
>= mMaxTransactionCount
) {
714 RefPtr
<TransactionItem
> overflow
= mUndoStack
.PopBottom();
717 // Push the transaction on the undo stack:
718 mUndoStack
.Push(transactionItem
.forget());
722 } // namespace mozilla