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