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