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