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