958 lines
32 KiB
Java
958 lines
32 KiB
Java
/*
|
|
* 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.compat.annotation.UnsupportedAppUsage;
|
|
import android.os.Build;
|
|
import android.os.Parcel;
|
|
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.
|
|
*
|
|
* <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.
|
|
*/
|
|
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.
|
|
*/
|
|
public static final int MERGE_MODE_UNIQUE = 1;
|
|
|
|
/**
|
|
* Always allow merge with the last undo state, if possible.
|
|
*/
|
|
public static final int MERGE_MODE_ANY = 2;
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public UndoManager() {
|
|
}
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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 {@link #restoreInstanceState(android.os.Parcel, java.lang.ClassLoader)}.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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 {@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.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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.
|
|
*/
|
|
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.
|
|
*/
|
|
public int getHistorySize() {
|
|
return mHistorySize;
|
|
}
|
|
|
|
/**
|
|
* 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.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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 <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.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
public boolean isInUndo() {
|
|
return mInUndo;
|
|
}
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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;
|
|
}
|
|
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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.
|
|
* @param owners If non-null, only those states containing an operation with one of
|
|
* the owners supplied here will be counted.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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.
|
|
* @param owners If non-null, only those states containing an operation with one of
|
|
* the owners supplied here will be counted.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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.
|
|
* @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.
|
|
* @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 {@link #endUpdate}.
|
|
* @param label Optional user-visible label for this new undo state.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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 {@link #beginUpdate}.
|
|
*/
|
|
public boolean isInUpdate() {
|
|
return mUpdateCount > 0;
|
|
}
|
|
|
|
/**
|
|
* Forcibly set a new for the new undo state being built within a {@link #beginUpdate}.
|
|
* Any existing label will be replaced with this one.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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 {@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 {@link #beginUpdate} has been called without a matching
|
|
* {@link #endUpdate} call.
|
|
*/
|
|
public int getUpdateNestingLevel() {
|
|
return mUpdateCount;
|
|
}
|
|
|
|
/**
|
|
* 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 {@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 {@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 {@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}.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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.
|
|
* @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}.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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
|
|
* {@link #beginUpdate}.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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 {@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.
|
|
*/
|
|
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
|
|
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 {@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, android.content.UndoOperation.class);
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|