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