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