blob: 1c2db47ad7d63db43886593bd36f5284331ad08a [file] [log] [blame]
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
19import android.os.Parcel;
20import android.os.Parcelable;
21import android.os.ParcelableParcel;
22import android.text.TextUtils;
23
24import java.util.ArrayList;
25import java.util.HashMap;
26
27/**
28 * Top-level class for managing and interacting with the global undo state for
29 * a document or application. This class supports both undo and redo and has
30 * helpers for merging undoable operations together as they are performed.
31 *
32 * <p>A single undoable operation is represented by {@link UndoOperation} which
33 * apps implement to define their undo/redo behavior. The UndoManager keeps
34 * a stack of undo states; each state can have one or more undo operations
35 * inside of it.</p>
36 *
37 * <p>Updates to the stack must be done inside of a {@link #beginUpdate}/{@link #endUpdate()}
38 * pair. During this time you can add new operations to the stack with
39 * {@link #addOperation}, retrieve and modify existing operations with
40 * {@link #getLastOperation}, control the label shown to the user for this operation
41 * with {@link #setUndoLabel} and {@link #suggestUndoLabel}, etc.</p>
42 *
43 * <p>Every {link UndoOperation} is associated with an {@link UndoOwner}, which identifies
44 * the data it belongs to. The owner is used to indicate how operations are dependent
45 * on each other -- operations with the same owner are dependent on others with the
46 * same owner. For example, you may have a document with multiple embedded objects. If the
47 * document itself and each embedded object use different owners, then you
48 * can provide undo semantics appropriate to the user's context: while within
49 * an embedded object, only edits to that object are seen and the user can
50 * undo/redo them without needing to impact edits in other objects; while
51 * within the larger document, all edits can be seen and the user must
52 * undo/redo them as a single stream.</p>
53 */
54public class UndoManager {
55 private final HashMap<String, UndoOwner> mOwners = new HashMap<String, UndoOwner>();
56 private final ArrayList<UndoState> mUndos = new ArrayList<UndoState>();
57 private final ArrayList<UndoState> mRedos = new ArrayList<UndoState>();
58 private int mUpdateCount;
59 private int mHistorySize = 20;
60 private UndoState mWorking;
61 private int mCommitId = 1;
62 private boolean mInUndo;
63 private boolean mMerged;
64
65 private int mStateSeq;
66 private int mNextSavedIdx;
67 private UndoOwner[] mStateOwners;
68
69 /**
70 * Never merge with the last undo state.
71 */
72 public static final int MERGE_MODE_NONE = 0;
73
74 /**
75 * Allow merge with the last undo state only if it contains
76 * operations with the caller's owner.
77 */
78 public static final int MERGE_MODE_UNIQUE = 1;
79
80 /**
81 * Always allow merge with the last undo state, if possible.
82 */
83 public static final int MERGE_MODE_ANY = 2;
84
85 public UndoOwner getOwner(String tag, Object data) {
86 if (tag == null) {
87 throw new NullPointerException("tag can't be null");
88 }
89 if (data == null) {
90 throw new NullPointerException("data can't be null");
91 }
92 UndoOwner owner = mOwners.get(tag);
93 if (owner != null) {
94 if (owner.mData != data) {
95 if (owner.mData != null) {
96 throw new IllegalStateException("Owner " + owner + " already exists with data "
97 + owner.mData + " but giving different data " + data);
98 }
99 owner.mData = data;
100 }
101 return owner;
102 }
103
104 owner = new UndoOwner(tag);
105 owner.mManager = this;
106 owner.mData = data;
107 mOwners.put(tag, owner);
108 return owner;
109 }
110
111 void removeOwner(UndoOwner owner) {
112 // XXX need to figure out how to prune.
113 if (false) {
114 mOwners.remove(owner.mTag);
115 owner.mManager = null;
116 }
117 }
118
119 /**
120 * Flatten the current undo state into a Parcelable object, which can later be restored
121 * with {@link #restoreInstanceState(android.os.Parcelable)}.
122 */
123 public Parcelable saveInstanceState() {
124 if (mUpdateCount > 0) {
125 throw new IllegalStateException("Can't save state while updating");
126 }
127 ParcelableParcel pp = new ParcelableParcel(getClass().getClassLoader());
128 Parcel p = pp.getParcel();
129 mStateSeq++;
130 if (mStateSeq <= 0) {
131 mStateSeq = 0;
132 }
133 mNextSavedIdx = 0;
134 p.writeInt(mHistorySize);
135 p.writeInt(mOwners.size());
136 // XXX eventually we need to be smart here about limiting the
137 // number of undo states we write to not exceed X bytes.
138 int i = mUndos.size();
139 while (i > 0) {
140 p.writeInt(1);
141 i--;
142 mUndos.get(i).writeToParcel(p);
143 }
144 i = mRedos.size();
145 p.writeInt(i);
146 while (i > 0) {
147 p.writeInt(2);
148 i--;
149 mRedos.get(i).writeToParcel(p);
150 }
151 p.writeInt(0);
152 return pp;
153 }
154
155 void saveOwner(UndoOwner owner, Parcel out) {
156 if (owner.mStateSeq == mStateSeq) {
157 out.writeInt(owner.mSavedIdx);
158 } else {
159 owner.mStateSeq = mStateSeq;
160 owner.mSavedIdx = mNextSavedIdx;
161 out.writeInt(owner.mSavedIdx);
162 out.writeString(owner.mTag);
163 mNextSavedIdx++;
164 }
165 }
166
167 /**
168 * Restore an undo state previously created with {@link #saveInstanceState()}. This will
169 * restore the UndoManager's state to almost exactly what it was at the point it had
170 * been previously saved; the only information not restored is the data object
171 * associated with each {@link UndoOwner}, which requires separate calls to
172 * {@link #getOwner(String, Object)} to re-associate the owner with its data.
173 */
174 public void restoreInstanceState(Parcelable state) {
175 if (mUpdateCount > 0) {
176 throw new IllegalStateException("Can't save state while updating");
177 }
178 forgetUndos(null, -1);
179 forgetRedos(null, -1);
180 ParcelableParcel pp = (ParcelableParcel)state;
181 Parcel p = pp.getParcel();
182 mHistorySize = p.readInt();
183 mStateOwners = new UndoOwner[p.readInt()];
184
185 int stype;
186 while ((stype=p.readInt()) != 0) {
187 UndoState ustate = new UndoState(this, p, pp.getClassLoader());
188 if (stype == 1) {
189 mUndos.add(0, ustate);
190 } else {
191 mRedos.add(0, ustate);
192 }
193 }
194 }
195
196 UndoOwner restoreOwner(Parcel in) {
197 int idx = in.readInt();
198 UndoOwner owner = mStateOwners[idx];
199 if (owner == null) {
200 String tag = in.readString();
201 owner = new UndoOwner(tag);
202 mStateOwners[idx] = owner;
203 mOwners.put(tag, owner);
204 }
205 return owner;
206 }
207
208 /**
209 * Set the maximum number of undo states that will be retained.
210 */
211 public void setHistorySize(int size) {
212 mHistorySize = size;
213 if (mHistorySize >= 0 && countUndos(null) > mHistorySize) {
214 forgetUndos(null, countUndos(null) - mHistorySize);
215 }
216 }
217
218 /**
219 * Return the current maximum number of undo states.
220 */
221 public int getHistorySize() {
222 return mHistorySize;
223 }
224
225 /**
226 * Perform undo of last/top <var>count</var> undo states. The states impacted
227 * by this can be limited through <var>owners</var>.
228 * @param owners Optional set of owners that should be impacted. If null, all
229 * undo states will be visible and available for undo. If non-null, only those
230 * states that contain one of the owners specified here will be visible.
231 * @param count Number of undo states to pop.
232 * @return Returns the number of undo states that were actually popped.
233 */
234 public int undo(UndoOwner[] owners, int count) {
235 if (mWorking != null) {
236 throw new IllegalStateException("Can't be called during an update");
237 }
238
239 int num = 0;
240 int i = -1;
241
242 mInUndo = true;
243
244 UndoState us = getTopUndo(null);
245 if (us != null) {
246 us.makeExecuted();
247 }
248
249 while (count > 0 && (i=findPrevState(mUndos, owners, i)) >= 0) {
250 UndoState state = mUndos.remove(i);
251 state.undo();
252 mRedos.add(state);
253 count--;
254 num++;
255 }
256
257 mInUndo = false;
258
259 return num;
260 }
261
262 /**
263 * Perform redo of last/top <var>count</var> undo states in the transient redo stack.
264 * The states impacted by this can be limited through <var>owners</var>.
265 * @param owners Optional set of owners that should be impacted. If null, all
266 * undo states will be visible and available for undo. If non-null, only those
267 * states that contain one of the owners specified here will be visible.
268 * @param count Number of undo states to pop.
269 * @return Returns the number of undo states that were actually redone.
270 */
271 public int redo(UndoOwner[] owners, int count) {
272 if (mWorking != null) {
273 throw new IllegalStateException("Can't be called during an update");
274 }
275
276 int num = 0;
277 int i = -1;
278
279 mInUndo = true;
280
281 while (count > 0 && (i=findPrevState(mRedos, owners, i)) >= 0) {
282 UndoState state = mRedos.remove(i);
283 state.redo();
284 mUndos.add(state);
285 count--;
286 num++;
287 }
288
289 mInUndo = false;
290
291 return num;
292 }
293
294 /**
295 * Returns true if we are currently inside of an undo/redo operation. This is
296 * useful for editors to know whether they should be generating new undo state
297 * when they see edit operations happening.
298 */
299 public boolean isInUndo() {
300 return mInUndo;
301 }
302
303 public int forgetUndos(UndoOwner[] owners, int count) {
304 if (count < 0) {
305 count = mUndos.size();
306 }
307
308 int removed = 0;
309 for (int i=0; i<mUndos.size() && removed < count; i++) {
310 UndoState state = mUndos.get(i);
311 if (count > 0 && matchOwners(state, owners)) {
312 state.destroy();
313 mUndos.remove(i);
314 removed++;
315 }
316 }
317
318 return removed;
319 }
320
321 public int forgetRedos(UndoOwner[] owners, int count) {
322 if (count < 0) {
323 count = mRedos.size();
324 }
325
326 int removed = 0;
327 for (int i=0; i<mRedos.size() && removed < count; i++) {
328 UndoState state = mRedos.get(i);
329 if (count > 0 && matchOwners(state, owners)) {
330 state.destroy();
331 mRedos.remove(i);
332 removed++;
333 }
334 }
335
336 return removed;
337 }
338
339 /**
340 * Return the number of undo states on the undo stack.
341 * @param owners If non-null, only those states containing an operation with one of
342 * the owners supplied here will be counted.
343 */
344 public int countUndos(UndoOwner[] owners) {
345 if (owners == null) {
346 return mUndos.size();
347 }
348
349 int count=0;
350 int i=0;
351 while ((i=findNextState(mUndos, owners, i)) >= 0) {
352 count++;
353 i++;
354 }
355 return count;
356 }
357
358 /**
359 * Return the number of redo states on the undo stack.
360 * @param owners If non-null, only those states containing an operation with one of
361 * the owners supplied here will be counted.
362 */
363 public int countRedos(UndoOwner[] owners) {
364 if (owners == null) {
365 return mRedos.size();
366 }
367
368 int count=0;
369 int i=0;
370 while ((i=findNextState(mRedos, owners, i)) >= 0) {
371 count++;
372 i++;
373 }
374 return count;
375 }
376
377 /**
378 * Return the user-visible label for the top undo state on the stack.
379 * @param owners If non-null, will select the top-most undo state containing an
380 * operation with one of the owners supplied here.
381 */
382 public CharSequence getUndoLabel(UndoOwner[] owners) {
383 UndoState state = getTopUndo(owners);
384 return state != null ? state.getLabel() : null;
385 }
386
387 /**
388 * Return the user-visible label for the top redo state on the stack.
389 * @param owners If non-null, will select the top-most undo state containing an
390 * operation with one of the owners supplied here.
391 */
392 public CharSequence getRedoLabel(UndoOwner[] owners) {
393 UndoState state = getTopRedo(owners);
394 return state != null ? state.getLabel() : null;
395 }
396
397 /**
398 * Start creating a new undo state. Multiple calls to this function will nest until
399 * they are all matched by a later call to {@link #endUpdate}.
400 * @param label Optional user-visible label for this new undo state.
401 */
402 public void beginUpdate(CharSequence label) {
403 if (mInUndo) {
404 throw new IllegalStateException("Can't being update while performing undo/redo");
405 }
406 if (mUpdateCount <= 0) {
407 createWorkingState();
408 mMerged = false;
409 mUpdateCount = 0;
410 }
411
412 mWorking.updateLabel(label);
413 mUpdateCount++;
414 }
415
416 private void createWorkingState() {
417 mWorking = new UndoState(this, mCommitId++);
418 if (mCommitId < 0) {
419 mCommitId = 1;
420 }
421 }
422
423 /**
424 * Returns true if currently inside of a {@link #beginUpdate}.
425 */
426 public boolean isInUpdate() {
427 return mUpdateCount > 0;
428 }
429
430 /**
431 * Forcibly set a new for the new undo state being built within a {@link #beginUpdate}.
432 * Any existing label will be replaced with this one.
433 */
434 public void setUndoLabel(CharSequence label) {
435 if (mWorking == null) {
436 throw new IllegalStateException("Must be called during an update");
437 }
438 mWorking.setLabel(label);
439 }
440
441 /**
442 * Set a new for the new undo state being built within a {@link #beginUpdate}, but
443 * only if there is not a label currently set for it.
444 */
445 public void suggestUndoLabel(CharSequence label) {
446 if (mWorking == null) {
447 throw new IllegalStateException("Must be called during an update");
448 }
449 mWorking.updateLabel(label);
450 }
451
452 /**
453 * Return the number of times {@link #beginUpdate} has been called without a matching
454 * {@link #endUpdate} call.
455 */
456 public int getUpdateNestingLevel() {
457 return mUpdateCount;
458 }
459
460 /**
461 * Check whether there is an {@link UndoOperation} in the current {@link #beginUpdate}
462 * undo state.
463 * @param owner Optional owner of the operation to look for. If null, will succeed
464 * if there is any operation; if non-null, will only succeed if there is an operation
465 * with the given owner.
466 * @return Returns true if there is a matching operation in the current undo state.
467 */
468 public boolean hasOperation(UndoOwner owner) {
469 if (mWorking == null) {
470 throw new IllegalStateException("Must be called during an update");
471 }
472 return mWorking.hasOperation(owner);
473 }
474
475 /**
476 * Return the most recent {@link UndoOperation} that was added to the update.
477 * @param mergeMode May be either {@link #MERGE_MODE_NONE} or {@link #MERGE_MODE_ANY}.
478 */
479 public UndoOperation<?> getLastOperation(int mergeMode) {
480 return getLastOperation(null, null, mergeMode);
481 }
482
483 /**
484 * Return the most recent {@link UndoOperation} that was added to the update and
485 * has the given owner.
486 * @param owner Optional owner of last operation to retrieve. If null, the last
487 * operation regardless of owner will be retrieved; if non-null, the last operation
488 * matching the given owner will be retrieved.
489 * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
490 * or {@link #MERGE_MODE_ANY}.
491 */
492 public UndoOperation<?> getLastOperation(UndoOwner owner, int mergeMode) {
493 return getLastOperation(null, owner, mergeMode);
494 }
495
496 /**
497 * Return the most recent {@link UndoOperation} that was added to the update and
498 * has the given owner.
499 * @param clazz Optional class of the last operation to retrieve. If null, the
500 * last operation regardless of class will be retrieved; if non-null, the last
501 * operation whose class is the same as the given class will be retrieved.
502 * @param owner Optional owner of last operation to retrieve. If null, the last
503 * operation regardless of owner will be retrieved; if non-null, the last operation
504 * matching the given owner will be retrieved.
505 * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
506 * or {@link #MERGE_MODE_ANY}.
507 */
508 public <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner,
509 int mergeMode) {
510 if (mWorking == null) {
511 throw new IllegalStateException("Must be called during an update");
512 }
513 if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
514 UndoState state = getTopUndo(null);
515 UndoOperation<?> last;
516 if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
517 && state.canMerge() && (last=state.getLastOperation(clazz, owner)) != null) {
518 if (last.allowMerge()) {
519 mWorking.destroy();
520 mWorking = state;
521 mUndos.remove(state);
522 mMerged = true;
523 return (T)last;
524 }
525 }
526 }
527
528 return mWorking.getLastOperation(clazz, owner);
529 }
530
531 /**
532 * Add a new UndoOperation to the current update.
533 * @param op The new operation to add.
534 * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
535 * or {@link #MERGE_MODE_ANY}.
536 */
537 public void addOperation(UndoOperation<?> op, int mergeMode) {
538 if (mWorking == null) {
539 throw new IllegalStateException("Must be called during an update");
540 }
541 UndoOwner owner = op.getOwner();
542 if (owner.mManager != this) {
543 throw new IllegalArgumentException(
544 "Given operation's owner is not in this undo manager.");
545 }
546 if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
547 UndoState state = getTopUndo(null);
548 if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
549 && state.canMerge() && state.hasOperation(op.getOwner())) {
550 mWorking.destroy();
551 mWorking = state;
552 mUndos.remove(state);
553 mMerged = true;
554 }
555 }
556 mWorking.addOperation(op);
557 }
558
559 /**
560 * Finish the creation of an undo state, matching a previous call to
561 * {@link #beginUpdate}.
562 */
563 public void endUpdate() {
564 if (mWorking == null) {
565 throw new IllegalStateException("Must be called during an update");
566 }
567 mUpdateCount--;
568
569 if (mUpdateCount == 0) {
570 pushWorkingState();
571 }
572 }
573
574 private void pushWorkingState() {
575 int N = mUndos.size() + 1;
576
577 if (mWorking.hasData()) {
578 mUndos.add(mWorking);
579 forgetRedos(null, -1);
580 mWorking.commit();
581 if (N >= 2) {
582 // The state before this one can no longer be merged, ever.
583 // The only way to get back to it is for the user to perform
584 // an undo.
585 mUndos.get(N-2).makeExecuted();
586 }
587 } else {
588 mWorking.destroy();
589 }
590 mWorking = null;
591
592 if (mHistorySize >= 0 && N > mHistorySize) {
593 forgetUndos(null, N - mHistorySize);
594 }
595 }
596
597 /**
598 * Commit the last finished undo state. This undo state can no longer be
599 * modified with further {@link #MERGE_MODE_UNIQUE} or
600 * {@link #MERGE_MODE_ANY} merge modes. If called while inside of an update,
601 * this will push any changes in the current update on to the undo stack
602 * and result with a fresh undo state, behaving as if {@link #endUpdate()}
603 * had been called enough to unwind the current update, then the last state
604 * committed, and {@link #beginUpdate} called to restore the update nesting.
605 * @param owner The optional owner to determine whether to perform the commit.
606 * If this is non-null, the commit will only execute if the current top undo
607 * state contains an operation with the given owner.
608 * @return Returns an integer identifier for the committed undo state, which
609 * can later be used to try to uncommit the state to perform further edits on it.
610 */
611 public int commitState(UndoOwner owner) {
612 if (mWorking != null && mWorking.hasData()) {
613 if (owner == null || mWorking.hasOperation(owner)) {
614 mWorking.setCanMerge(false);
615 int commitId = mWorking.getCommitId();
616 pushWorkingState();
617 createWorkingState();
618 mMerged = true;
619 return commitId;
620 }
621 } else {
622 UndoState state = getTopUndo(null);
623 if (state != null && (owner == null || state.hasOperation(owner))) {
624 state.setCanMerge(false);
625 return state.getCommitId();
626 }
627 }
628 return -1;
629 }
630
631 /**
632 * Attempt to undo a previous call to {@link #commitState}. This will work
633 * if the undo state at the top of the stack has the given id, and has not been
634 * involved in an undo operation. Otherwise false is returned.
635 * @param commitId The identifier for the state to be uncommitted, as returned
636 * by {@link #commitState}.
637 * @param owner Optional owner that must appear in the committed state.
638 * @return Returns true if the uncommit is successful, else false.
639 */
640 public boolean uncommitState(int commitId, UndoOwner owner) {
641 if (mWorking != null && mWorking.getCommitId() == commitId) {
642 if (owner == null || mWorking.hasOperation(owner)) {
643 return mWorking.setCanMerge(true);
644 }
645 } else {
646 UndoState state = getTopUndo(null);
647 if (state != null && (owner == null || state.hasOperation(owner))) {
648 if (state.getCommitId() == commitId) {
649 return state.setCanMerge(true);
650 }
651 }
652 }
653 return false;
654 }
655
656 UndoState getTopUndo(UndoOwner[] owners) {
657 if (mUndos.size() <= 0) {
658 return null;
659 }
660 int i = findPrevState(mUndos, owners, -1);
661 return i >= 0 ? mUndos.get(i) : null;
662 }
663
664 UndoState getTopRedo(UndoOwner[] owners) {
665 if (mRedos.size() <= 0) {
666 return null;
667 }
668 int i = findPrevState(mRedos, owners, -1);
669 return i >= 0 ? mRedos.get(i) : null;
670 }
671
672 boolean matchOwners(UndoState state, UndoOwner[] owners) {
673 if (owners == null) {
674 return true;
675 }
676 for (int i=0; i<owners.length; i++) {
677 if (state.matchOwner(owners[i])) {
678 return true;
679 }
680 }
681 return false;
682 }
683
684 int findPrevState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
685 final int N = states.size();
686
687 if (from == -1) {
688 from = N-1;
689 }
690 if (from >= N) {
691 return -1;
692 }
693 if (owners == null) {
694 return from;
695 }
696
697 while (from >= 0) {
698 UndoState state = states.get(from);
699 if (matchOwners(state, owners)) {
700 return from;
701 }
702 from--;
703 }
704
705 return -1;
706 }
707
708 int findNextState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
709 final int N = states.size();
710
711 if (from < 0) {
712 from = 0;
713 }
714 if (from >= N) {
715 return -1;
716 }
717 if (owners == null) {
718 return from;
719 }
720
721 while (from < N) {
722 UndoState state = states.get(from);
723 if (matchOwners(state, owners)) {
724 return from;
725 }
726 from++;
727 }
728
729 return -1;
730 }
731
732 final static class UndoState {
733 private final UndoManager mManager;
734 private final int mCommitId;
735 private final ArrayList<UndoOperation<?>> mOperations = new ArrayList<UndoOperation<?>>();
736 private ArrayList<UndoOperation<?>> mRecent;
737 private CharSequence mLabel;
738 private boolean mCanMerge = true;
739 private boolean mExecuted;
740
741 UndoState(UndoManager manager, int commitId) {
742 mManager = manager;
743 mCommitId = commitId;
744 }
745
746 UndoState(UndoManager manager, Parcel p, ClassLoader loader) {
747 mManager = manager;
748 mCommitId = p.readInt();
749 mCanMerge = p.readInt() != 0;
750 mExecuted = p.readInt() != 0;
751 mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
752 final int N = p.readInt();
753 for (int i=0; i<N; i++) {
754 UndoOwner owner = mManager.restoreOwner(p);
755 UndoOperation op = (UndoOperation)p.readParcelable(loader);
756 op.mOwner = owner;
757 mOperations.add(op);
758 }
759 }
760
761 void writeToParcel(Parcel p) {
762 if (mRecent != null) {
763 throw new IllegalStateException("Can't save state before committing");
764 }
765 p.writeInt(mCommitId);
766 p.writeInt(mCanMerge ? 1 : 0);
767 p.writeInt(mExecuted ? 1 : 0);
768 TextUtils.writeToParcel(mLabel, p, 0);
769 final int N = mOperations.size();
770 p.writeInt(N);
771 for (int i=0; i<N; i++) {
772 UndoOperation op = mOperations.get(i);
773 mManager.saveOwner(op.mOwner, p);
774 p.writeParcelable(op, 0);
775 }
776 }
777
778 int getCommitId() {
779 return mCommitId;
780 }
781
782 void setLabel(CharSequence label) {
783 mLabel = label;
784 }
785
786 void updateLabel(CharSequence label) {
787 if (mLabel != null) {
788 mLabel = label;
789 }
790 }
791
792 CharSequence getLabel() {
793 return mLabel;
794 }
795
796 boolean setCanMerge(boolean state) {
797 // Don't allow re-enabling of merging if state has been executed.
798 if (state && mExecuted) {
799 return false;
800 }
801 mCanMerge = state;
802 return true;
803 }
804
805 void makeExecuted() {
806 mExecuted = true;
807 }
808
809 boolean canMerge() {
810 return mCanMerge && !mExecuted;
811 }
812
813 int countOperations() {
814 return mOperations.size();
815 }
816
817 boolean hasOperation(UndoOwner owner) {
818 final int N = mOperations.size();
819 if (owner == null) {
820 return N != 0;
821 }
822 for (int i=0; i<N; i++) {
823 if (mOperations.get(i).getOwner() == owner) {
824 return true;
825 }
826 }
827 return false;
828 }
829
830 boolean hasMultipleOwners() {
831 final int N = mOperations.size();
832 if (N <= 1) {
833 return false;
834 }
835 UndoOwner owner = mOperations.get(0).getOwner();
836 for (int i=1; i<N; i++) {
837 if (mOperations.get(i).getOwner() != owner) {
838 return true;
839 }
840 }
841 return false;
842 }
843
844 void addOperation(UndoOperation<?> op) {
845 if (mOperations.contains(op)) {
846 throw new IllegalStateException("Already holds " + op);
847 }
848 mOperations.add(op);
849 if (mRecent == null) {
850 mRecent = new ArrayList<UndoOperation<?>>();
851 mRecent.add(op);
852 }
853 op.mOwner.mOpCount++;
854 }
855
856 <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner) {
857 final int N = mOperations.size();
858 if (clazz == null && owner == null) {
859 return N > 0 ? (T)mOperations.get(N-1) : null;
860 }
861 // First look for the top-most operation with the same owner.
862 for (int i=N-1; i>=0; i--) {
863 UndoOperation<?> op = mOperations.get(i);
864 if (owner != null && op.getOwner() != owner) {
865 continue;
866 }
867 // Return this operation if it has the same class that the caller wants.
868 // Note that we don't search deeper for the class, because we don't want
869 // to end up with a different order of operations for the same owner.
870 if (clazz != null && op.getClass() != clazz) {
871 return null;
872 }
873 return (T)op;
874 }
875
876 return null;
877 }
878
879 boolean matchOwner(UndoOwner owner) {
880 for (int i=mOperations.size()-1; i>=0; i--) {
881 if (mOperations.get(i).matchOwner(owner)) {
882 return true;
883 }
884 }
885 return false;
886 }
887
888 boolean hasData() {
889 for (int i=mOperations.size()-1; i>=0; i--) {
890 if (mOperations.get(i).hasData()) {
891 return true;
892 }
893 }
894 return false;
895 }
896
897 void commit() {
898 final int N = mRecent != null ? mRecent.size() : 0;
899 for (int i=0; i<N; i++) {
900 mRecent.get(i).commit();
901 }
902 mRecent = null;
903 }
904
905 void undo() {
906 for (int i=mOperations.size()-1; i>=0; i--) {
907 mOperations.get(i).undo();
908 }
909 }
910
911 void redo() {
912 final int N = mOperations.size();
913 for (int i=0; i<N; i++) {
914 mOperations.get(i).redo();
915 }
916 }
917
918 void destroy() {
919 for (int i=mOperations.size()-1; i>=0; i--) {
920 UndoOwner owner = mOperations.get(i).mOwner;
921 owner.mOpCount--;
922 if (owner.mOpCount <= 0) {
923 if (owner.mOpCount < 0) {
924 throw new IllegalStateException("Underflow of op count on owner " + owner
925 + " in op " + mOperations.get(i));
926 }
927 mManager.removeOwner(owner);
928 }
929 }
930 }
931 }
932}