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