blob: 46c2c6e6abfb7c3afb2335661f7052461fbc6d3d [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;
James Cook761a4b32015-02-23 16:17:28 -080023import android.util.ArrayMap;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070024
25import java.util.ArrayList;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070026
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 {
James Cook761a4b32015-02-23 16:17:28 -080057 // The common case is a single undo owner (e.g. for a TextView), so default to that capacity.
58 private final ArrayMap<String, UndoOwner> mOwners =
59 new ArrayMap<String, UndoOwner>(1 /* capacity */);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070060 private final ArrayList<UndoState> mUndos = new ArrayList<UndoState>();
61 private final ArrayList<UndoState> mRedos = new ArrayList<UndoState>();
62 private int mUpdateCount;
63 private int mHistorySize = 20;
64 private UndoState mWorking;
65 private int mCommitId = 1;
66 private boolean mInUndo;
67 private boolean mMerged;
68
69 private int mStateSeq;
70 private int mNextSavedIdx;
71 private UndoOwner[] mStateOwners;
72
73 /**
74 * Never merge with the last undo state.
75 */
76 public static final int MERGE_MODE_NONE = 0;
77
78 /**
79 * Allow merge with the last undo state only if it contains
80 * operations with the caller's owner.
81 */
82 public static final int MERGE_MODE_UNIQUE = 1;
83
84 /**
85 * Always allow merge with the last undo state, if possible.
86 */
87 public static final int MERGE_MODE_ANY = 2;
88
89 public UndoOwner getOwner(String tag, Object data) {
90 if (tag == null) {
91 throw new NullPointerException("tag can't be null");
92 }
93 if (data == null) {
94 throw new NullPointerException("data can't be null");
95 }
96 UndoOwner owner = mOwners.get(tag);
97 if (owner != null) {
98 if (owner.mData != data) {
99 if (owner.mData != null) {
100 throw new IllegalStateException("Owner " + owner + " already exists with data "
101 + owner.mData + " but giving different data " + data);
102 }
103 owner.mData = data;
104 }
105 return owner;
106 }
107
James Cookf59152c2015-02-26 18:03:58 -0800108 owner = new UndoOwner(tag, this);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700109 owner.mData = data;
110 mOwners.put(tag, owner);
111 return owner;
112 }
113
114 void removeOwner(UndoOwner owner) {
115 // XXX need to figure out how to prune.
116 if (false) {
117 mOwners.remove(owner.mTag);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700118 }
119 }
120
121 /**
James Cookd2026682015-03-03 14:40:14 -0800122 * Flatten the current undo state into a Parcel object, which can later be restored
123 * with {@link #restoreInstanceState(android.os.Parcel, java.lang.ClassLoader)}.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700124 */
James Cookd2026682015-03-03 14:40:14 -0800125 public void saveInstanceState(Parcel p) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700126 if (mUpdateCount > 0) {
127 throw new IllegalStateException("Can't save state while updating");
128 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700129 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);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700152 }
153
154 void saveOwner(UndoOwner owner, Parcel out) {
155 if (owner.mStateSeq == mStateSeq) {
156 out.writeInt(owner.mSavedIdx);
157 } else {
158 owner.mStateSeq = mStateSeq;
159 owner.mSavedIdx = mNextSavedIdx;
160 out.writeInt(owner.mSavedIdx);
161 out.writeString(owner.mTag);
James Cookf143ace2015-03-02 13:27:51 -0800162 out.writeInt(owner.mOpCount);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700163 mNextSavedIdx++;
164 }
165 }
166
167 /**
James Cookd2026682015-03-03 14:40:14 -0800168 * Restore an undo state previously created with {@link #saveInstanceState(Parcel)}. This
169 * will restore the UndoManager's state to almost exactly what it was at the point it had
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700170 * 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 */
James Cookd2026682015-03-03 14:40:14 -0800174 public void restoreInstanceState(Parcel p, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700175 if (mUpdateCount > 0) {
176 throw new IllegalStateException("Can't save state while updating");
177 }
178 forgetUndos(null, -1);
179 forgetRedos(null, -1);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700180 mHistorySize = p.readInt();
181 mStateOwners = new UndoOwner[p.readInt()];
182
183 int stype;
184 while ((stype=p.readInt()) != 0) {
James Cookd2026682015-03-03 14:40:14 -0800185 UndoState ustate = new UndoState(this, p, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700186 if (stype == 1) {
187 mUndos.add(0, ustate);
188 } else {
189 mRedos.add(0, ustate);
190 }
191 }
192 }
193
194 UndoOwner restoreOwner(Parcel in) {
195 int idx = in.readInt();
196 UndoOwner owner = mStateOwners[idx];
197 if (owner == null) {
198 String tag = in.readString();
James Cookf143ace2015-03-02 13:27:51 -0800199 int opCount = in.readInt();
James Cookf59152c2015-02-26 18:03:58 -0800200 owner = new UndoOwner(tag, this);
James Cookf143ace2015-03-02 13:27:51 -0800201 owner.mOpCount = opCount;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700202 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;
James Cookd2026682015-03-03 14:40:14 -0800309 int i = 0;
310 while (i < mUndos.size() && removed < count) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700311 UndoState state = mUndos.get(i);
312 if (count > 0 && matchOwners(state, owners)) {
313 state.destroy();
314 mUndos.remove(i);
315 removed++;
James Cookd2026682015-03-03 14:40:14 -0800316 } else {
317 i++;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700318 }
319 }
320
321 return removed;
322 }
323
324 public int forgetRedos(UndoOwner[] owners, int count) {
325 if (count < 0) {
326 count = mRedos.size();
327 }
328
329 int removed = 0;
James Cookd2026682015-03-03 14:40:14 -0800330 int i = 0;
331 while (i < mRedos.size() && removed < count) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700332 UndoState state = mRedos.get(i);
333 if (count > 0 && matchOwners(state, owners)) {
334 state.destroy();
335 mRedos.remove(i);
336 removed++;
James Cookd2026682015-03-03 14:40:14 -0800337 } else {
338 i++;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700339 }
340 }
341
342 return removed;
343 }
344
345 /**
346 * Return the number of undo states on the undo stack.
347 * @param owners If non-null, only those states containing an operation with one of
348 * the owners supplied here will be counted.
349 */
350 public int countUndos(UndoOwner[] owners) {
351 if (owners == null) {
352 return mUndos.size();
353 }
354
355 int count=0;
356 int i=0;
357 while ((i=findNextState(mUndos, owners, i)) >= 0) {
358 count++;
359 i++;
360 }
361 return count;
362 }
363
364 /**
365 * Return the number of redo states on the undo stack.
366 * @param owners If non-null, only those states containing an operation with one of
367 * the owners supplied here will be counted.
368 */
369 public int countRedos(UndoOwner[] owners) {
370 if (owners == null) {
371 return mRedos.size();
372 }
373
374 int count=0;
375 int i=0;
376 while ((i=findNextState(mRedos, owners, i)) >= 0) {
377 count++;
378 i++;
379 }
380 return count;
381 }
382
383 /**
384 * Return the user-visible label for the top undo state on the stack.
385 * @param owners If non-null, will select the top-most undo state containing an
386 * operation with one of the owners supplied here.
387 */
388 public CharSequence getUndoLabel(UndoOwner[] owners) {
389 UndoState state = getTopUndo(owners);
390 return state != null ? state.getLabel() : null;
391 }
392
393 /**
394 * Return the user-visible label for the top redo state on the stack.
395 * @param owners If non-null, will select the top-most undo state containing an
396 * operation with one of the owners supplied here.
397 */
398 public CharSequence getRedoLabel(UndoOwner[] owners) {
399 UndoState state = getTopRedo(owners);
400 return state != null ? state.getLabel() : null;
401 }
402
403 /**
404 * Start creating a new undo state. Multiple calls to this function will nest until
405 * they are all matched by a later call to {@link #endUpdate}.
406 * @param label Optional user-visible label for this new undo state.
407 */
408 public void beginUpdate(CharSequence label) {
409 if (mInUndo) {
410 throw new IllegalStateException("Can't being update while performing undo/redo");
411 }
412 if (mUpdateCount <= 0) {
413 createWorkingState();
414 mMerged = false;
415 mUpdateCount = 0;
416 }
417
418 mWorking.updateLabel(label);
419 mUpdateCount++;
420 }
421
422 private void createWorkingState() {
423 mWorking = new UndoState(this, mCommitId++);
424 if (mCommitId < 0) {
425 mCommitId = 1;
426 }
427 }
428
429 /**
430 * Returns true if currently inside of a {@link #beginUpdate}.
431 */
432 public boolean isInUpdate() {
433 return mUpdateCount > 0;
434 }
435
436 /**
437 * Forcibly set a new for the new undo state being built within a {@link #beginUpdate}.
438 * Any existing label will be replaced with this one.
439 */
440 public void setUndoLabel(CharSequence label) {
441 if (mWorking == null) {
442 throw new IllegalStateException("Must be called during an update");
443 }
444 mWorking.setLabel(label);
445 }
446
447 /**
448 * Set a new for the new undo state being built within a {@link #beginUpdate}, but
449 * only if there is not a label currently set for it.
450 */
451 public void suggestUndoLabel(CharSequence label) {
452 if (mWorking == null) {
453 throw new IllegalStateException("Must be called during an update");
454 }
455 mWorking.updateLabel(label);
456 }
457
458 /**
459 * Return the number of times {@link #beginUpdate} has been called without a matching
460 * {@link #endUpdate} call.
461 */
462 public int getUpdateNestingLevel() {
463 return mUpdateCount;
464 }
465
466 /**
467 * Check whether there is an {@link UndoOperation} in the current {@link #beginUpdate}
468 * undo state.
469 * @param owner Optional owner of the operation to look for. If null, will succeed
470 * if there is any operation; if non-null, will only succeed if there is an operation
471 * with the given owner.
472 * @return Returns true if there is a matching operation in the current undo state.
473 */
474 public boolean hasOperation(UndoOwner owner) {
475 if (mWorking == null) {
476 throw new IllegalStateException("Must be called during an update");
477 }
478 return mWorking.hasOperation(owner);
479 }
480
481 /**
482 * Return the most recent {@link UndoOperation} that was added to the update.
483 * @param mergeMode May be either {@link #MERGE_MODE_NONE} or {@link #MERGE_MODE_ANY}.
484 */
485 public UndoOperation<?> getLastOperation(int mergeMode) {
486 return getLastOperation(null, null, mergeMode);
487 }
488
489 /**
490 * Return the most recent {@link UndoOperation} that was added to the update and
491 * has the given owner.
492 * @param owner Optional owner of last operation to retrieve. If null, the last
493 * operation regardless of owner will be retrieved; if non-null, the last operation
494 * matching the given owner will be retrieved.
495 * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
496 * or {@link #MERGE_MODE_ANY}.
497 */
498 public UndoOperation<?> getLastOperation(UndoOwner owner, int mergeMode) {
499 return getLastOperation(null, owner, mergeMode);
500 }
501
502 /**
503 * Return the most recent {@link UndoOperation} that was added to the update and
504 * has the given owner.
505 * @param clazz Optional class of the last operation to retrieve. If null, the
506 * last operation regardless of class will be retrieved; if non-null, the last
507 * operation whose class is the same as the given class will be retrieved.
508 * @param owner Optional owner of last operation to retrieve. If null, the last
509 * operation regardless of owner will be retrieved; if non-null, the last operation
510 * matching the given owner will be retrieved.
511 * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
512 * or {@link #MERGE_MODE_ANY}.
513 */
514 public <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner,
515 int mergeMode) {
516 if (mWorking == null) {
517 throw new IllegalStateException("Must be called during an update");
518 }
519 if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
520 UndoState state = getTopUndo(null);
521 UndoOperation<?> last;
522 if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
523 && state.canMerge() && (last=state.getLastOperation(clazz, owner)) != null) {
524 if (last.allowMerge()) {
525 mWorking.destroy();
526 mWorking = state;
527 mUndos.remove(state);
528 mMerged = true;
529 return (T)last;
530 }
531 }
532 }
533
534 return mWorking.getLastOperation(clazz, owner);
535 }
536
537 /**
538 * Add a new UndoOperation to the current update.
539 * @param op The new operation to add.
540 * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE},
541 * or {@link #MERGE_MODE_ANY}.
542 */
543 public void addOperation(UndoOperation<?> op, int mergeMode) {
544 if (mWorking == null) {
545 throw new IllegalStateException("Must be called during an update");
546 }
547 UndoOwner owner = op.getOwner();
548 if (owner.mManager != this) {
549 throw new IllegalArgumentException(
550 "Given operation's owner is not in this undo manager.");
551 }
552 if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) {
553 UndoState state = getTopUndo(null);
554 if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners())
555 && state.canMerge() && state.hasOperation(op.getOwner())) {
556 mWorking.destroy();
557 mWorking = state;
558 mUndos.remove(state);
559 mMerged = true;
560 }
561 }
562 mWorking.addOperation(op);
563 }
564
565 /**
566 * Finish the creation of an undo state, matching a previous call to
567 * {@link #beginUpdate}.
568 */
569 public void endUpdate() {
570 if (mWorking == null) {
571 throw new IllegalStateException("Must be called during an update");
572 }
573 mUpdateCount--;
574
575 if (mUpdateCount == 0) {
576 pushWorkingState();
577 }
578 }
579
580 private void pushWorkingState() {
581 int N = mUndos.size() + 1;
582
583 if (mWorking.hasData()) {
584 mUndos.add(mWorking);
585 forgetRedos(null, -1);
586 mWorking.commit();
587 if (N >= 2) {
588 // The state before this one can no longer be merged, ever.
589 // The only way to get back to it is for the user to perform
590 // an undo.
591 mUndos.get(N-2).makeExecuted();
592 }
593 } else {
594 mWorking.destroy();
595 }
596 mWorking = null;
597
598 if (mHistorySize >= 0 && N > mHistorySize) {
599 forgetUndos(null, N - mHistorySize);
600 }
601 }
602
603 /**
604 * Commit the last finished undo state. This undo state can no longer be
605 * modified with further {@link #MERGE_MODE_UNIQUE} or
606 * {@link #MERGE_MODE_ANY} merge modes. If called while inside of an update,
607 * this will push any changes in the current update on to the undo stack
608 * and result with a fresh undo state, behaving as if {@link #endUpdate()}
609 * had been called enough to unwind the current update, then the last state
610 * committed, and {@link #beginUpdate} called to restore the update nesting.
611 * @param owner The optional owner to determine whether to perform the commit.
612 * If this is non-null, the commit will only execute if the current top undo
613 * state contains an operation with the given owner.
614 * @return Returns an integer identifier for the committed undo state, which
615 * can later be used to try to uncommit the state to perform further edits on it.
616 */
617 public int commitState(UndoOwner owner) {
618 if (mWorking != null && mWorking.hasData()) {
619 if (owner == null || mWorking.hasOperation(owner)) {
620 mWorking.setCanMerge(false);
621 int commitId = mWorking.getCommitId();
622 pushWorkingState();
623 createWorkingState();
624 mMerged = true;
625 return commitId;
626 }
627 } else {
628 UndoState state = getTopUndo(null);
629 if (state != null && (owner == null || state.hasOperation(owner))) {
630 state.setCanMerge(false);
631 return state.getCommitId();
632 }
633 }
634 return -1;
635 }
636
637 /**
638 * Attempt to undo a previous call to {@link #commitState}. This will work
639 * if the undo state at the top of the stack has the given id, and has not been
640 * involved in an undo operation. Otherwise false is returned.
641 * @param commitId The identifier for the state to be uncommitted, as returned
642 * by {@link #commitState}.
643 * @param owner Optional owner that must appear in the committed state.
644 * @return Returns true if the uncommit is successful, else false.
645 */
646 public boolean uncommitState(int commitId, UndoOwner owner) {
647 if (mWorking != null && mWorking.getCommitId() == commitId) {
648 if (owner == null || mWorking.hasOperation(owner)) {
649 return mWorking.setCanMerge(true);
650 }
651 } else {
652 UndoState state = getTopUndo(null);
653 if (state != null && (owner == null || state.hasOperation(owner))) {
654 if (state.getCommitId() == commitId) {
655 return state.setCanMerge(true);
656 }
657 }
658 }
659 return false;
660 }
661
662 UndoState getTopUndo(UndoOwner[] owners) {
663 if (mUndos.size() <= 0) {
664 return null;
665 }
666 int i = findPrevState(mUndos, owners, -1);
667 return i >= 0 ? mUndos.get(i) : null;
668 }
669
670 UndoState getTopRedo(UndoOwner[] owners) {
671 if (mRedos.size() <= 0) {
672 return null;
673 }
674 int i = findPrevState(mRedos, owners, -1);
675 return i >= 0 ? mRedos.get(i) : null;
676 }
677
678 boolean matchOwners(UndoState state, UndoOwner[] owners) {
679 if (owners == null) {
680 return true;
681 }
682 for (int i=0; i<owners.length; i++) {
683 if (state.matchOwner(owners[i])) {
684 return true;
685 }
686 }
687 return false;
688 }
689
690 int findPrevState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
691 final int N = states.size();
692
693 if (from == -1) {
694 from = N-1;
695 }
696 if (from >= N) {
697 return -1;
698 }
699 if (owners == null) {
700 return from;
701 }
702
703 while (from >= 0) {
704 UndoState state = states.get(from);
705 if (matchOwners(state, owners)) {
706 return from;
707 }
708 from--;
709 }
710
711 return -1;
712 }
713
714 int findNextState(ArrayList<UndoState> states, UndoOwner[] owners, int from) {
715 final int N = states.size();
716
717 if (from < 0) {
718 from = 0;
719 }
720 if (from >= N) {
721 return -1;
722 }
723 if (owners == null) {
724 return from;
725 }
726
727 while (from < N) {
728 UndoState state = states.get(from);
729 if (matchOwners(state, owners)) {
730 return from;
731 }
732 from++;
733 }
734
735 return -1;
736 }
737
738 final static class UndoState {
739 private final UndoManager mManager;
740 private final int mCommitId;
741 private final ArrayList<UndoOperation<?>> mOperations = new ArrayList<UndoOperation<?>>();
742 private ArrayList<UndoOperation<?>> mRecent;
743 private CharSequence mLabel;
744 private boolean mCanMerge = true;
745 private boolean mExecuted;
746
747 UndoState(UndoManager manager, int commitId) {
748 mManager = manager;
749 mCommitId = commitId;
750 }
751
752 UndoState(UndoManager manager, Parcel p, ClassLoader loader) {
753 mManager = manager;
754 mCommitId = p.readInt();
755 mCanMerge = p.readInt() != 0;
756 mExecuted = p.readInt() != 0;
757 mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p);
758 final int N = p.readInt();
759 for (int i=0; i<N; i++) {
760 UndoOwner owner = mManager.restoreOwner(p);
761 UndoOperation op = (UndoOperation)p.readParcelable(loader);
762 op.mOwner = owner;
763 mOperations.add(op);
764 }
765 }
766
767 void writeToParcel(Parcel p) {
768 if (mRecent != null) {
769 throw new IllegalStateException("Can't save state before committing");
770 }
771 p.writeInt(mCommitId);
772 p.writeInt(mCanMerge ? 1 : 0);
773 p.writeInt(mExecuted ? 1 : 0);
774 TextUtils.writeToParcel(mLabel, p, 0);
775 final int N = mOperations.size();
776 p.writeInt(N);
777 for (int i=0; i<N; i++) {
778 UndoOperation op = mOperations.get(i);
779 mManager.saveOwner(op.mOwner, p);
780 p.writeParcelable(op, 0);
781 }
782 }
783
784 int getCommitId() {
785 return mCommitId;
786 }
787
788 void setLabel(CharSequence label) {
789 mLabel = label;
790 }
791
792 void updateLabel(CharSequence label) {
793 if (mLabel != null) {
794 mLabel = label;
795 }
796 }
797
798 CharSequence getLabel() {
799 return mLabel;
800 }
801
802 boolean setCanMerge(boolean state) {
803 // Don't allow re-enabling of merging if state has been executed.
804 if (state && mExecuted) {
805 return false;
806 }
807 mCanMerge = state;
808 return true;
809 }
810
811 void makeExecuted() {
812 mExecuted = true;
813 }
814
815 boolean canMerge() {
816 return mCanMerge && !mExecuted;
817 }
818
819 int countOperations() {
820 return mOperations.size();
821 }
822
823 boolean hasOperation(UndoOwner owner) {
824 final int N = mOperations.size();
825 if (owner == null) {
826 return N != 0;
827 }
828 for (int i=0; i<N; i++) {
829 if (mOperations.get(i).getOwner() == owner) {
830 return true;
831 }
832 }
833 return false;
834 }
835
836 boolean hasMultipleOwners() {
837 final int N = mOperations.size();
838 if (N <= 1) {
839 return false;
840 }
841 UndoOwner owner = mOperations.get(0).getOwner();
842 for (int i=1; i<N; i++) {
843 if (mOperations.get(i).getOwner() != owner) {
844 return true;
845 }
846 }
847 return false;
848 }
849
850 void addOperation(UndoOperation<?> op) {
851 if (mOperations.contains(op)) {
852 throw new IllegalStateException("Already holds " + op);
853 }
854 mOperations.add(op);
855 if (mRecent == null) {
856 mRecent = new ArrayList<UndoOperation<?>>();
857 mRecent.add(op);
858 }
859 op.mOwner.mOpCount++;
860 }
861
862 <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner) {
863 final int N = mOperations.size();
864 if (clazz == null && owner == null) {
865 return N > 0 ? (T)mOperations.get(N-1) : null;
866 }
867 // First look for the top-most operation with the same owner.
868 for (int i=N-1; i>=0; i--) {
869 UndoOperation<?> op = mOperations.get(i);
870 if (owner != null && op.getOwner() != owner) {
871 continue;
872 }
873 // Return this operation if it has the same class that the caller wants.
874 // Note that we don't search deeper for the class, because we don't want
875 // to end up with a different order of operations for the same owner.
876 if (clazz != null && op.getClass() != clazz) {
877 return null;
878 }
879 return (T)op;
880 }
881
882 return null;
883 }
884
885 boolean matchOwner(UndoOwner owner) {
886 for (int i=mOperations.size()-1; i>=0; i--) {
887 if (mOperations.get(i).matchOwner(owner)) {
888 return true;
889 }
890 }
891 return false;
892 }
893
894 boolean hasData() {
895 for (int i=mOperations.size()-1; i>=0; i--) {
896 if (mOperations.get(i).hasData()) {
897 return true;
898 }
899 }
900 return false;
901 }
902
903 void commit() {
904 final int N = mRecent != null ? mRecent.size() : 0;
905 for (int i=0; i<N; i++) {
906 mRecent.get(i).commit();
907 }
908 mRecent = null;
909 }
910
911 void undo() {
912 for (int i=mOperations.size()-1; i>=0; i--) {
913 mOperations.get(i).undo();
914 }
915 }
916
917 void redo() {
918 final int N = mOperations.size();
919 for (int i=0; i<N; i++) {
920 mOperations.get(i).redo();
921 }
922 }
923
924 void destroy() {
925 for (int i=mOperations.size()-1; i>=0; i--) {
926 UndoOwner owner = mOperations.get(i).mOwner;
927 owner.mOpCount--;
928 if (owner.mOpCount <= 0) {
929 if (owner.mOpCount < 0) {
930 throw new IllegalStateException("Underflow of op count on owner " + owner
931 + " in op " + mOperations.get(i));
932 }
933 mManager.removeOwner(owner);
934 }
935 }
936 }
937 }
938}