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