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