blob: f6a0d771bf4cf53aca46d7f5a06de60f10cda509 [file] [log] [blame]
Dianne Hackborn3aa49b62013-04-26 16:39:17 -07001/*
2 * Copyright (C) 2013 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package android.content;
18
Mathew Inwood5c0d3542018-08-14 13:54:31 +010019import android.annotation.UnsupportedAppUsage;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070020import android.os.Parcel;
21import android.os.Parcelable;
22import android.os.ParcelableParcel;
23import android.text.TextUtils;
James Cook761a4b32015-02-23 16:17:28 -080024import android.util.ArrayMap;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070025
26import java.util.ArrayList;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070027
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>
Dianne Hackbornb811e642013-09-04 17:43:56 -070054 *
55 * @hide
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070056 */
57public class UndoManager {
James Cook761a4b32015-02-23 16:17:28 -080058 // 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 */);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070061 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
Mathew Inwood5c0d3542018-08-14 13:54:31 +010090 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -070091 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
James Cookf59152c2015-02-26 18:03:58 -0800110 owner = new UndoOwner(tag, this);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700111 owner.mData = data;
112 mOwners.put(tag, owner);
113 return owner;
114 }
115
116 void removeOwner(UndoOwner owner) {
117 // XXX need to figure out how to prune.
118 if (false) {
119 mOwners.remove(owner.mTag);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700120 }
121 }
122
123 /**
James Cookd2026682015-03-03 14:40:14 -0800124 * Flatten the current undo state into a Parcel object, which can later be restored
125 * with {@link #restoreInstanceState(android.os.Parcel, java.lang.ClassLoader)}.
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700126 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100127 @UnsupportedAppUsage
James Cookd2026682015-03-03 14:40:14 -0800128 public void saveInstanceState(Parcel p) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700129 if (mUpdateCount > 0) {
130 throw new IllegalStateException("Can't save state while updating");
131 }
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700132 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();
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700148 while (i > 0) {
149 p.writeInt(2);
150 i--;
151 mRedos.get(i).writeToParcel(p);
152 }
153 p.writeInt(0);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700154 }
155
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);
James Cookf143ace2015-03-02 13:27:51 -0800164 out.writeInt(owner.mOpCount);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700165 mNextSavedIdx++;
166 }
167 }
168
169 /**
James Cookd2026682015-03-03 14:40:14 -0800170 * 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
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700172 * 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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100176 @UnsupportedAppUsage
James Cookd2026682015-03-03 14:40:14 -0800177 public void restoreInstanceState(Parcel p, ClassLoader loader) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700178 if (mUpdateCount > 0) {
179 throw new IllegalStateException("Can't save state while updating");
180 }
181 forgetUndos(null, -1);
182 forgetRedos(null, -1);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700183 mHistorySize = p.readInt();
184 mStateOwners = new UndoOwner[p.readInt()];
185
186 int stype;
187 while ((stype=p.readInt()) != 0) {
James Cookd2026682015-03-03 14:40:14 -0800188 UndoState ustate = new UndoState(this, p, loader);
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700189 if (stype == 1) {
190 mUndos.add(0, ustate);
191 } else {
192 mRedos.add(0, ustate);
193 }
194 }
195 }
196
197 UndoOwner restoreOwner(Parcel in) {
198 int idx = in.readInt();
199 UndoOwner owner = mStateOwners[idx];
200 if (owner == null) {
201 String tag = in.readString();
James Cookf143ace2015-03-02 13:27:51 -0800202 int opCount = in.readInt();
James Cookf59152c2015-02-26 18:03:58 -0800203 owner = new UndoOwner(tag, this);
James Cookf143ace2015-03-02 13:27:51 -0800204 owner.mOpCount = opCount;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700205 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 */
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 */
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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100237 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700238 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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100275 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700276 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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100304 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700305 public boolean isInUndo() {
306 return mInUndo;
307 }
308
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100309 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700310 public int forgetUndos(UndoOwner[] owners, int count) {
311 if (count < 0) {
312 count = mUndos.size();
313 }
314
315 int removed = 0;
James Cookd2026682015-03-03 14:40:14 -0800316 int i = 0;
317 while (i < mUndos.size() && removed < count) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700318 UndoState state = mUndos.get(i);
319 if (count > 0 && matchOwners(state, owners)) {
320 state.destroy();
321 mUndos.remove(i);
322 removed++;
James Cookd2026682015-03-03 14:40:14 -0800323 } else {
324 i++;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700325 }
326 }
327
328 return removed;
329 }
330
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100331 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700332 public int forgetRedos(UndoOwner[] owners, int count) {
333 if (count < 0) {
334 count = mRedos.size();
335 }
336
337 int removed = 0;
James Cookd2026682015-03-03 14:40:14 -0800338 int i = 0;
339 while (i < mRedos.size() && removed < count) {
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700340 UndoState state = mRedos.get(i);
341 if (count > 0 && matchOwners(state, owners)) {
342 state.destroy();
343 mRedos.remove(i);
344 removed++;
James Cookd2026682015-03-03 14:40:14 -0800345 } else {
346 i++;
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700347 }
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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100358 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700359 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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100378 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700379 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 */
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 */
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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100418 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700419 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
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 */
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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100451 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700452 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 */
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 */
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 */
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 */
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 */
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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100526 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700527 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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100556 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700557 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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100583 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700584 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
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 */
Mathew Inwood5c0d3542018-08-14 13:54:31 +0100632 @UnsupportedAppUsage
Dianne Hackborn3aa49b62013-04-26 16:39:17 -0700633 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 */
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
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
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
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
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
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
763 UndoState(UndoManager manager, int commitId) {
764 mManager = manager;
765 mCommitId = commitId;
766 }
767
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
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
800 int getCommitId() {
801 return mCommitId;
802 }
803
804 void setLabel(CharSequence label) {
805 mLabel = label;
806 }
807
808 void updateLabel(CharSequence label) {
809 if (mLabel != null) {
810 mLabel = label;
811 }
812 }
813
814 CharSequence getLabel() {
815 return mLabel;
816 }
817
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
827 void makeExecuted() {
828 mExecuted = true;
829 }
830
831 boolean canMerge() {
832 return mCanMerge && !mExecuted;
833 }
834
835 int countOperations() {
836 return mOperations.size();
837 }
838
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
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
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
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
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
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
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
927 void undo() {
928 for (int i=mOperations.size()-1; i>=0; i--) {
929 mOperations.get(i).undo();
930 }
931 }
932
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
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}