/*
 * Copyright (C) 2013 The Android Open Source Project
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package android.content;

import android.os.Parcel;
import android.os.Parcelable;
import android.os.ParcelableParcel;
import android.text.TextUtils;
import android.util.ArrayMap;

import java.util.ArrayList;

Top-level class for managing and interacting with the global undo state for a document or application. This class supports both undo and redo and has helpers for merging undoable operations together as they are performed.

A single undoable operation is represented by UndoOperation which apps implement to define their undo/redo behavior. The UndoManager keeps a stack of undo states; each state can have one or more undo operations inside of it.

Updates to the stack must be done inside of a beginUpdate/endUpdate() pair. During this time you can add new operations to the stack with addOperation, retrieve and modify existing operations with getLastOperation, control the label shown to the user for this operation with setUndoLabel and suggestUndoLabel, etc.

Every {link UndoOperation} is associated with an UndoOwner, which identifies the data it belongs to. The owner is used to indicate how operations are dependent on each other -- operations with the same owner are dependent on others with the same owner. For example, you may have a document with multiple embedded objects. If the document itself and each embedded object use different owners, then you can provide undo semantics appropriate to the user's context: while within an embedded object, only edits to that object are seen and the user can undo/redo them without needing to impact edits in other objects; while within the larger document, all edits can be seen and the user must undo/redo them as a single stream.

@hide
/** * Top-level class for managing and interacting with the global undo state for * a document or application. This class supports both undo and redo and has * helpers for merging undoable operations together as they are performed. * * <p>A single undoable operation is represented by {@link UndoOperation} which * apps implement to define their undo/redo behavior. The UndoManager keeps * a stack of undo states; each state can have one or more undo operations * inside of it.</p> * * <p>Updates to the stack must be done inside of a {@link #beginUpdate}/{@link #endUpdate()} * pair. During this time you can add new operations to the stack with * {@link #addOperation}, retrieve and modify existing operations with * {@link #getLastOperation}, control the label shown to the user for this operation * with {@link #setUndoLabel} and {@link #suggestUndoLabel}, etc.</p> * * <p>Every {link UndoOperation} is associated with an {@link UndoOwner}, which identifies * the data it belongs to. The owner is used to indicate how operations are dependent * on each other -- operations with the same owner are dependent on others with the * same owner. For example, you may have a document with multiple embedded objects. If the * document itself and each embedded object use different owners, then you * can provide undo semantics appropriate to the user's context: while within * an embedded object, only edits to that object are seen and the user can * undo/redo them without needing to impact edits in other objects; while * within the larger document, all edits can be seen and the user must * undo/redo them as a single stream.</p> * * @hide */
public class UndoManager { // The common case is a single undo owner (e.g. for a TextView), so default to that capacity. private final ArrayMap<String, UndoOwner> mOwners = new ArrayMap<String, UndoOwner>(1 /* capacity */); private final ArrayList<UndoState> mUndos = new ArrayList<UndoState>(); private final ArrayList<UndoState> mRedos = new ArrayList<UndoState>(); private int mUpdateCount; private int mHistorySize = 20; private UndoState mWorking; private int mCommitId = 1; private boolean mInUndo; private boolean mMerged; private int mStateSeq; private int mNextSavedIdx; private UndoOwner[] mStateOwners;
Never merge with the last undo state.
/** * Never merge with the last undo state. */
public static final int MERGE_MODE_NONE = 0;
Allow merge with the last undo state only if it contains operations with the caller's owner.
/** * Allow merge with the last undo state only if it contains * operations with the caller's owner. */
public static final int MERGE_MODE_UNIQUE = 1;
Always allow merge with the last undo state, if possible.
/** * Always allow merge with the last undo state, if possible. */
public static final int MERGE_MODE_ANY = 2; public UndoOwner getOwner(String tag, Object data) { if (tag == null) { throw new NullPointerException("tag can't be null"); } if (data == null) { throw new NullPointerException("data can't be null"); } UndoOwner owner = mOwners.get(tag); if (owner != null) { if (owner.mData != data) { if (owner.mData != null) { throw new IllegalStateException("Owner " + owner + " already exists with data " + owner.mData + " but giving different data " + data); } owner.mData = data; } return owner; } owner = new UndoOwner(tag, this); owner.mData = data; mOwners.put(tag, owner); return owner; } void removeOwner(UndoOwner owner) { // XXX need to figure out how to prune. if (false) { mOwners.remove(owner.mTag); } }
Flatten the current undo state into a Parcel object, which can later be restored with restoreInstanceState(Parcel, ClassLoader).
/** * Flatten the current undo state into a Parcel object, which can later be restored * with {@link #restoreInstanceState(android.os.Parcel, java.lang.ClassLoader)}. */
public void saveInstanceState(Parcel p) { if (mUpdateCount > 0) { throw new IllegalStateException("Can't save state while updating"); } mStateSeq++; if (mStateSeq <= 0) { mStateSeq = 0; } mNextSavedIdx = 0; p.writeInt(mHistorySize); p.writeInt(mOwners.size()); // XXX eventually we need to be smart here about limiting the // number of undo states we write to not exceed X bytes. int i = mUndos.size(); while (i > 0) { p.writeInt(1); i--; mUndos.get(i).writeToParcel(p); } i = mRedos.size(); while (i > 0) { p.writeInt(2); i--; mRedos.get(i).writeToParcel(p); } p.writeInt(0); } void saveOwner(UndoOwner owner, Parcel out) { if (owner.mStateSeq == mStateSeq) { out.writeInt(owner.mSavedIdx); } else { owner.mStateSeq = mStateSeq; owner.mSavedIdx = mNextSavedIdx; out.writeInt(owner.mSavedIdx); out.writeString(owner.mTag); out.writeInt(owner.mOpCount); mNextSavedIdx++; } }
Restore an undo state previously created with saveInstanceState(Parcel). This will restore the UndoManager's state to almost exactly what it was at the point it had been previously saved; the only information not restored is the data object associated with each UndoOwner, which requires separate calls to getOwner(String, Object) to re-associate the owner with its data.
/** * Restore an undo state previously created with {@link #saveInstanceState(Parcel)}. This * will restore the UndoManager's state to almost exactly what it was at the point it had * been previously saved; the only information not restored is the data object * associated with each {@link UndoOwner}, which requires separate calls to * {@link #getOwner(String, Object)} to re-associate the owner with its data. */
public void restoreInstanceState(Parcel p, ClassLoader loader) { if (mUpdateCount > 0) { throw new IllegalStateException("Can't save state while updating"); } forgetUndos(null, -1); forgetRedos(null, -1); mHistorySize = p.readInt(); mStateOwners = new UndoOwner[p.readInt()]; int stype; while ((stype=p.readInt()) != 0) { UndoState ustate = new UndoState(this, p, loader); if (stype == 1) { mUndos.add(0, ustate); } else { mRedos.add(0, ustate); } } } UndoOwner restoreOwner(Parcel in) { int idx = in.readInt(); UndoOwner owner = mStateOwners[idx]; if (owner == null) { String tag = in.readString(); int opCount = in.readInt(); owner = new UndoOwner(tag, this); owner.mOpCount = opCount; mStateOwners[idx] = owner; mOwners.put(tag, owner); } return owner; }
Set the maximum number of undo states that will be retained.
/** * Set the maximum number of undo states that will be retained. */
public void setHistorySize(int size) { mHistorySize = size; if (mHistorySize >= 0 && countUndos(null) > mHistorySize) { forgetUndos(null, countUndos(null) - mHistorySize); } }
Return the current maximum number of undo states.
/** * Return the current maximum number of undo states. */
public int getHistorySize() { return mHistorySize; }
Perform undo of last/top count undo states. The states impacted by this can be limited through owners.
Params:
  • owners – Optional set of owners that should be impacted. If null, all undo states will be visible and available for undo. If non-null, only those states that contain one of the owners specified here will be visible.
  • count – Number of undo states to pop.
Returns:Returns the number of undo states that were actually popped.
/** * Perform undo of last/top <var>count</var> undo states. The states impacted * by this can be limited through <var>owners</var>. * @param owners Optional set of owners that should be impacted. If null, all * undo states will be visible and available for undo. If non-null, only those * states that contain one of the owners specified here will be visible. * @param count Number of undo states to pop. * @return Returns the number of undo states that were actually popped. */
public int undo(UndoOwner[] owners, int count) { if (mWorking != null) { throw new IllegalStateException("Can't be called during an update"); } int num = 0; int i = -1; mInUndo = true; UndoState us = getTopUndo(null); if (us != null) { us.makeExecuted(); } while (count > 0 && (i=findPrevState(mUndos, owners, i)) >= 0) { UndoState state = mUndos.remove(i); state.undo(); mRedos.add(state); count--; num++; } mInUndo = false; return num; }
Perform redo of last/top count undo states in the transient redo stack. The states impacted by this can be limited through owners.
Params:
  • owners – Optional set of owners that should be impacted. If null, all undo states will be visible and available for undo. If non-null, only those states that contain one of the owners specified here will be visible.
  • count – Number of undo states to pop.
Returns:Returns the number of undo states that were actually redone.
/** * Perform redo of last/top <var>count</var> undo states in the transient redo stack. * The states impacted by this can be limited through <var>owners</var>. * @param owners Optional set of owners that should be impacted. If null, all * undo states will be visible and available for undo. If non-null, only those * states that contain one of the owners specified here will be visible. * @param count Number of undo states to pop. * @return Returns the number of undo states that were actually redone. */
public int redo(UndoOwner[] owners, int count) { if (mWorking != null) { throw new IllegalStateException("Can't be called during an update"); } int num = 0; int i = -1; mInUndo = true; while (count > 0 && (i=findPrevState(mRedos, owners, i)) >= 0) { UndoState state = mRedos.remove(i); state.redo(); mUndos.add(state); count--; num++; } mInUndo = false; return num; }
Returns true if we are currently inside of an undo/redo operation. This is useful for editors to know whether they should be generating new undo state when they see edit operations happening.
/** * Returns true if we are currently inside of an undo/redo operation. This is * useful for editors to know whether they should be generating new undo state * when they see edit operations happening. */
public boolean isInUndo() { return mInUndo; } public int forgetUndos(UndoOwner[] owners, int count) { if (count < 0) { count = mUndos.size(); } int removed = 0; int i = 0; while (i < mUndos.size() && removed < count) { UndoState state = mUndos.get(i); if (count > 0 && matchOwners(state, owners)) { state.destroy(); mUndos.remove(i); removed++; } else { i++; } } return removed; } public int forgetRedos(UndoOwner[] owners, int count) { if (count < 0) { count = mRedos.size(); } int removed = 0; int i = 0; while (i < mRedos.size() && removed < count) { UndoState state = mRedos.get(i); if (count > 0 && matchOwners(state, owners)) { state.destroy(); mRedos.remove(i); removed++; } else { i++; } } return removed; }
Return the number of undo states on the undo stack.
Params:
  • owners – If non-null, only those states containing an operation with one of the owners supplied here will be counted.
/** * Return the number of undo states on the undo stack. * @param owners If non-null, only those states containing an operation with one of * the owners supplied here will be counted. */
public int countUndos(UndoOwner[] owners) { if (owners == null) { return mUndos.size(); } int count=0; int i=0; while ((i=findNextState(mUndos, owners, i)) >= 0) { count++; i++; } return count; }
Return the number of redo states on the undo stack.
Params:
  • owners – If non-null, only those states containing an operation with one of the owners supplied here will be counted.
/** * Return the number of redo states on the undo stack. * @param owners If non-null, only those states containing an operation with one of * the owners supplied here will be counted. */
public int countRedos(UndoOwner[] owners) { if (owners == null) { return mRedos.size(); } int count=0; int i=0; while ((i=findNextState(mRedos, owners, i)) >= 0) { count++; i++; } return count; }
Return the user-visible label for the top undo state on the stack.
Params:
  • owners – If non-null, will select the top-most undo state containing an operation with one of the owners supplied here.
/** * Return the user-visible label for the top undo state on the stack. * @param owners If non-null, will select the top-most undo state containing an * operation with one of the owners supplied here. */
public CharSequence getUndoLabel(UndoOwner[] owners) { UndoState state = getTopUndo(owners); return state != null ? state.getLabel() : null; }
Return the user-visible label for the top redo state on the stack.
Params:
  • owners – If non-null, will select the top-most undo state containing an operation with one of the owners supplied here.
/** * Return the user-visible label for the top redo state on the stack. * @param owners If non-null, will select the top-most undo state containing an * operation with one of the owners supplied here. */
public CharSequence getRedoLabel(UndoOwner[] owners) { UndoState state = getTopRedo(owners); return state != null ? state.getLabel() : null; }
Start creating a new undo state. Multiple calls to this function will nest until they are all matched by a later call to endUpdate.
Params:
  • label – Optional user-visible label for this new undo state.
/** * Start creating a new undo state. Multiple calls to this function will nest until * they are all matched by a later call to {@link #endUpdate}. * @param label Optional user-visible label for this new undo state. */
public void beginUpdate(CharSequence label) { if (mInUndo) { throw new IllegalStateException("Can't being update while performing undo/redo"); } if (mUpdateCount <= 0) { createWorkingState(); mMerged = false; mUpdateCount = 0; } mWorking.updateLabel(label); mUpdateCount++; } private void createWorkingState() { mWorking = new UndoState(this, mCommitId++); if (mCommitId < 0) { mCommitId = 1; } }
Returns true if currently inside of a beginUpdate.
/** * Returns true if currently inside of a {@link #beginUpdate}. */
public boolean isInUpdate() { return mUpdateCount > 0; }
Forcibly set a new for the new undo state being built within a beginUpdate. Any existing label will be replaced with this one.
/** * Forcibly set a new for the new undo state being built within a {@link #beginUpdate}. * Any existing label will be replaced with this one. */
public void setUndoLabel(CharSequence label) { if (mWorking == null) { throw new IllegalStateException("Must be called during an update"); } mWorking.setLabel(label); }
Set a new for the new undo state being built within a beginUpdate, but only if there is not a label currently set for it.
/** * Set a new for the new undo state being built within a {@link #beginUpdate}, but * only if there is not a label currently set for it. */
public void suggestUndoLabel(CharSequence label) { if (mWorking == null) { throw new IllegalStateException("Must be called during an update"); } mWorking.updateLabel(label); }
Return the number of times beginUpdate has been called without a matching endUpdate call.
/** * Return the number of times {@link #beginUpdate} has been called without a matching * {@link #endUpdate} call. */
public int getUpdateNestingLevel() { return mUpdateCount; }
Check whether there is an UndoOperation in the current beginUpdate undo state.
Params:
  • owner – Optional owner of the operation to look for. If null, will succeed if there is any operation; if non-null, will only succeed if there is an operation with the given owner.
Returns:Returns true if there is a matching operation in the current undo state.
/** * Check whether there is an {@link UndoOperation} in the current {@link #beginUpdate} * undo state. * @param owner Optional owner of the operation to look for. If null, will succeed * if there is any operation; if non-null, will only succeed if there is an operation * with the given owner. * @return Returns true if there is a matching operation in the current undo state. */
public boolean hasOperation(UndoOwner owner) { if (mWorking == null) { throw new IllegalStateException("Must be called during an update"); } return mWorking.hasOperation(owner); }
Return the most recent UndoOperation that was added to the update.
Params:
/** * Return the most recent {@link UndoOperation} that was added to the update. * @param mergeMode May be either {@link #MERGE_MODE_NONE} or {@link #MERGE_MODE_ANY}. */
public UndoOperation<?> getLastOperation(int mergeMode) { return getLastOperation(null, null, mergeMode); }
Return the most recent UndoOperation that was added to the update and has the given owner.
Params:
  • owner – Optional owner of last operation to retrieve. If null, the last operation regardless of owner will be retrieved; if non-null, the last operation matching the given owner will be retrieved.
  • mergeMode – May be either MERGE_MODE_NONE, MERGE_MODE_UNIQUE, or MERGE_MODE_ANY.
/** * Return the most recent {@link UndoOperation} that was added to the update and * has the given owner. * @param owner Optional owner of last operation to retrieve. If null, the last * operation regardless of owner will be retrieved; if non-null, the last operation * matching the given owner will be retrieved. * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE}, * or {@link #MERGE_MODE_ANY}. */
public UndoOperation<?> getLastOperation(UndoOwner owner, int mergeMode) { return getLastOperation(null, owner, mergeMode); }
Return the most recent UndoOperation that was added to the update and has the given owner.
Params:
  • clazz – Optional class of the last operation to retrieve. If null, the last operation regardless of class will be retrieved; if non-null, the last operation whose class is the same as the given class will be retrieved.
  • owner – Optional owner of last operation to retrieve. If null, the last operation regardless of owner will be retrieved; if non-null, the last operation matching the given owner will be retrieved.
  • mergeMode – May be either MERGE_MODE_NONE, MERGE_MODE_UNIQUE, or MERGE_MODE_ANY.
/** * Return the most recent {@link UndoOperation} that was added to the update and * has the given owner. * @param clazz Optional class of the last operation to retrieve. If null, the * last operation regardless of class will be retrieved; if non-null, the last * operation whose class is the same as the given class will be retrieved. * @param owner Optional owner of last operation to retrieve. If null, the last * operation regardless of owner will be retrieved; if non-null, the last operation * matching the given owner will be retrieved. * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE}, * or {@link #MERGE_MODE_ANY}. */
public <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner, int mergeMode) { if (mWorking == null) { throw new IllegalStateException("Must be called during an update"); } if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) { UndoState state = getTopUndo(null); UndoOperation<?> last; if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners()) && state.canMerge() && (last=state.getLastOperation(clazz, owner)) != null) { if (last.allowMerge()) { mWorking.destroy(); mWorking = state; mUndos.remove(state); mMerged = true; return (T)last; } } } return mWorking.getLastOperation(clazz, owner); }
Add a new UndoOperation to the current update.
Params:
/** * Add a new UndoOperation to the current update. * @param op The new operation to add. * @param mergeMode May be either {@link #MERGE_MODE_NONE}, {@link #MERGE_MODE_UNIQUE}, * or {@link #MERGE_MODE_ANY}. */
public void addOperation(UndoOperation<?> op, int mergeMode) { if (mWorking == null) { throw new IllegalStateException("Must be called during an update"); } UndoOwner owner = op.getOwner(); if (owner.mManager != this) { throw new IllegalArgumentException( "Given operation's owner is not in this undo manager."); } if (mergeMode != MERGE_MODE_NONE && !mMerged && !mWorking.hasData()) { UndoState state = getTopUndo(null); if (state != null && (mergeMode == MERGE_MODE_ANY || !state.hasMultipleOwners()) && state.canMerge() && state.hasOperation(op.getOwner())) { mWorking.destroy(); mWorking = state; mUndos.remove(state); mMerged = true; } } mWorking.addOperation(op); }
Finish the creation of an undo state, matching a previous call to beginUpdate.
/** * Finish the creation of an undo state, matching a previous call to * {@link #beginUpdate}. */
public void endUpdate() { if (mWorking == null) { throw new IllegalStateException("Must be called during an update"); } mUpdateCount--; if (mUpdateCount == 0) { pushWorkingState(); } } private void pushWorkingState() { int N = mUndos.size() + 1; if (mWorking.hasData()) { mUndos.add(mWorking); forgetRedos(null, -1); mWorking.commit(); if (N >= 2) { // The state before this one can no longer be merged, ever. // The only way to get back to it is for the user to perform // an undo. mUndos.get(N-2).makeExecuted(); } } else { mWorking.destroy(); } mWorking = null; if (mHistorySize >= 0 && N > mHistorySize) { forgetUndos(null, N - mHistorySize); } }
Commit the last finished undo state. This undo state can no longer be modified with further MERGE_MODE_UNIQUE or MERGE_MODE_ANY merge modes. If called while inside of an update, this will push any changes in the current update on to the undo stack and result with a fresh undo state, behaving as if endUpdate() had been called enough to unwind the current update, then the last state committed, and beginUpdate called to restore the update nesting.
Params:
  • owner – The optional owner to determine whether to perform the commit. If this is non-null, the commit will only execute if the current top undo state contains an operation with the given owner.
Returns:Returns an integer identifier for the committed undo state, which can later be used to try to uncommit the state to perform further edits on it.
/** * Commit the last finished undo state. This undo state can no longer be * modified with further {@link #MERGE_MODE_UNIQUE} or * {@link #MERGE_MODE_ANY} merge modes. If called while inside of an update, * this will push any changes in the current update on to the undo stack * and result with a fresh undo state, behaving as if {@link #endUpdate()} * had been called enough to unwind the current update, then the last state * committed, and {@link #beginUpdate} called to restore the update nesting. * @param owner The optional owner to determine whether to perform the commit. * If this is non-null, the commit will only execute if the current top undo * state contains an operation with the given owner. * @return Returns an integer identifier for the committed undo state, which * can later be used to try to uncommit the state to perform further edits on it. */
public int commitState(UndoOwner owner) { if (mWorking != null && mWorking.hasData()) { if (owner == null || mWorking.hasOperation(owner)) { mWorking.setCanMerge(false); int commitId = mWorking.getCommitId(); pushWorkingState(); createWorkingState(); mMerged = true; return commitId; } } else { UndoState state = getTopUndo(null); if (state != null && (owner == null || state.hasOperation(owner))) { state.setCanMerge(false); return state.getCommitId(); } } return -1; }
Attempt to undo a previous call to commitState. This will work if the undo state at the top of the stack has the given id, and has not been involved in an undo operation. Otherwise false is returned.
Params:
  • commitId – The identifier for the state to be uncommitted, as returned by commitState.
  • owner – Optional owner that must appear in the committed state.
Returns:Returns true if the uncommit is successful, else false.
/** * Attempt to undo a previous call to {@link #commitState}. This will work * if the undo state at the top of the stack has the given id, and has not been * involved in an undo operation. Otherwise false is returned. * @param commitId The identifier for the state to be uncommitted, as returned * by {@link #commitState}. * @param owner Optional owner that must appear in the committed state. * @return Returns true if the uncommit is successful, else false. */
public boolean uncommitState(int commitId, UndoOwner owner) { if (mWorking != null && mWorking.getCommitId() == commitId) { if (owner == null || mWorking.hasOperation(owner)) { return mWorking.setCanMerge(true); } } else { UndoState state = getTopUndo(null); if (state != null && (owner == null || state.hasOperation(owner))) { if (state.getCommitId() == commitId) { return state.setCanMerge(true); } } } return false; } UndoState getTopUndo(UndoOwner[] owners) { if (mUndos.size() <= 0) { return null; } int i = findPrevState(mUndos, owners, -1); return i >= 0 ? mUndos.get(i) : null; } UndoState getTopRedo(UndoOwner[] owners) { if (mRedos.size() <= 0) { return null; } int i = findPrevState(mRedos, owners, -1); return i >= 0 ? mRedos.get(i) : null; } boolean matchOwners(UndoState state, UndoOwner[] owners) { if (owners == null) { return true; } for (int i=0; i<owners.length; i++) { if (state.matchOwner(owners[i])) { return true; } } return false; } int findPrevState(ArrayList<UndoState> states, UndoOwner[] owners, int from) { final int N = states.size(); if (from == -1) { from = N-1; } if (from >= N) { return -1; } if (owners == null) { return from; } while (from >= 0) { UndoState state = states.get(from); if (matchOwners(state, owners)) { return from; } from--; } return -1; } int findNextState(ArrayList<UndoState> states, UndoOwner[] owners, int from) { final int N = states.size(); if (from < 0) { from = 0; } if (from >= N) { return -1; } if (owners == null) { return from; } while (from < N) { UndoState state = states.get(from); if (matchOwners(state, owners)) { return from; } from++; } return -1; } final static class UndoState { private final UndoManager mManager; private final int mCommitId; private final ArrayList<UndoOperation<?>> mOperations = new ArrayList<UndoOperation<?>>(); private ArrayList<UndoOperation<?>> mRecent; private CharSequence mLabel; private boolean mCanMerge = true; private boolean mExecuted; UndoState(UndoManager manager, int commitId) { mManager = manager; mCommitId = commitId; } UndoState(UndoManager manager, Parcel p, ClassLoader loader) { mManager = manager; mCommitId = p.readInt(); mCanMerge = p.readInt() != 0; mExecuted = p.readInt() != 0; mLabel = TextUtils.CHAR_SEQUENCE_CREATOR.createFromParcel(p); final int N = p.readInt(); for (int i=0; i<N; i++) { UndoOwner owner = mManager.restoreOwner(p); UndoOperation op = (UndoOperation)p.readParcelable(loader); op.mOwner = owner; mOperations.add(op); } } void writeToParcel(Parcel p) { if (mRecent != null) { throw new IllegalStateException("Can't save state before committing"); } p.writeInt(mCommitId); p.writeInt(mCanMerge ? 1 : 0); p.writeInt(mExecuted ? 1 : 0); TextUtils.writeToParcel(mLabel, p, 0); final int N = mOperations.size(); p.writeInt(N); for (int i=0; i<N; i++) { UndoOperation op = mOperations.get(i); mManager.saveOwner(op.mOwner, p); p.writeParcelable(op, 0); } } int getCommitId() { return mCommitId; } void setLabel(CharSequence label) { mLabel = label; } void updateLabel(CharSequence label) { if (mLabel != null) { mLabel = label; } } CharSequence getLabel() { return mLabel; } boolean setCanMerge(boolean state) { // Don't allow re-enabling of merging if state has been executed. if (state && mExecuted) { return false; } mCanMerge = state; return true; } void makeExecuted() { mExecuted = true; } boolean canMerge() { return mCanMerge && !mExecuted; } int countOperations() { return mOperations.size(); } boolean hasOperation(UndoOwner owner) { final int N = mOperations.size(); if (owner == null) { return N != 0; } for (int i=0; i<N; i++) { if (mOperations.get(i).getOwner() == owner) { return true; } } return false; } boolean hasMultipleOwners() { final int N = mOperations.size(); if (N <= 1) { return false; } UndoOwner owner = mOperations.get(0).getOwner(); for (int i=1; i<N; i++) { if (mOperations.get(i).getOwner() != owner) { return true; } } return false; } void addOperation(UndoOperation<?> op) { if (mOperations.contains(op)) { throw new IllegalStateException("Already holds " + op); } mOperations.add(op); if (mRecent == null) { mRecent = new ArrayList<UndoOperation<?>>(); mRecent.add(op); } op.mOwner.mOpCount++; } <T extends UndoOperation> T getLastOperation(Class<T> clazz, UndoOwner owner) { final int N = mOperations.size(); if (clazz == null && owner == null) { return N > 0 ? (T)mOperations.get(N-1) : null; } // First look for the top-most operation with the same owner. for (int i=N-1; i>=0; i--) { UndoOperation<?> op = mOperations.get(i); if (owner != null && op.getOwner() != owner) { continue; } // Return this operation if it has the same class that the caller wants. // Note that we don't search deeper for the class, because we don't want // to end up with a different order of operations for the same owner. if (clazz != null && op.getClass() != clazz) { return null; } return (T)op; } return null; } boolean matchOwner(UndoOwner owner) { for (int i=mOperations.size()-1; i>=0; i--) { if (mOperations.get(i).matchOwner(owner)) { return true; } } return false; } boolean hasData() { for (int i=mOperations.size()-1; i>=0; i--) { if (mOperations.get(i).hasData()) { return true; } } return false; } void commit() { final int N = mRecent != null ? mRecent.size() : 0; for (int i=0; i<N; i++) { mRecent.get(i).commit(); } mRecent = null; } void undo() { for (int i=mOperations.size()-1; i>=0; i--) { mOperations.get(i).undo(); } } void redo() { final int N = mOperations.size(); for (int i=0; i<N; i++) { mOperations.get(i).redo(); } } void destroy() { for (int i=mOperations.size()-1; i>=0; i--) { UndoOwner owner = mOperations.get(i).mOwner; owner.mOpCount--; if (owner.mOpCount <= 0) { if (owner.mOpCount < 0) { throw new IllegalStateException("Underflow of op count on owner " + owner + " in op " + mOperations.get(i)); } mManager.removeOwner(owner); } } } } }