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