/* * Copyright (c) 1997, 2017, Oracle and/or its affiliates. All rights reserved. * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. * * This code is free software; you can redistribute it and/or modify it * under the terms of the GNU General Public License version 2 only, as * published by the Free Software Foundation. Oracle designates this * particular file as subject to the "Classpath" exception as provided * by Oracle in the LICENSE file that accompanied this code. * * This code is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License * version 2 for more details (a copy is included in the LICENSE file that * accompanied this code). * * You should have received a copy of the GNU General Public License version * 2 along with this work; if not, write to the Free Software Foundation, * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. * * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA * or visit www.oracle.com if you need additional information or have any * questions. */ package javax.swing.undo; import javax.swing.event.*; import javax.swing.UIManager; import java.util.*; import sun.swing.text.UndoableEditLockSupport; /** * {@code UndoManager} manages a list of {@code UndoableEdits}, * providing a way to undo or redo the appropriate edits. There are * two ways to add edits to an UndoManager. Add the edit * directly using the addEdit method, or add the * UndoManager to a bean that supports * UndoableEditListener. The following examples creates * an UndoManager and adds it as an * UndoableEditListener to a JTextField: *
 *   UndoManager undoManager = new UndoManager();
 *   JTextField tf = ...;
 *   tf.getDocument().addUndoableEditListener(undoManager);
 * 
*

* UndoManager maintains an ordered list of edits and the * index of the next edit in that list. The index of the next edit is * either the size of the current list of edits, or if * undo has been invoked it corresponds to the index * of the last significant edit that was undone. When * undo is invoked all edits from the index of the next * edit to the last significant edit are undone, in reverse order. * For example, consider an UndoManager consisting of the * following edits: A b c D. Edits with a * upper-case letter in bold are significant, those in lower-case * and italicized are insignificant. *

* * * *
Figure 1
* *
Figure 1 *
*

* As shown in figure 1, if D was just added, the * index of the next edit will be 4. Invoking undo * results in invoking undo on D and setting the * index of the next edit to 3 (edit c), as shown in the following * figure. *

* * * *
Figure 2
* *
Figure 2 *
*

* The last significant edit is A, so that invoking * undo again invokes undo on c, * b, and A, in that order, setting the index of the * next edit to 0, as shown in the following figure. *

* * * *
Figure 3
* *
Figure 3 *
*

* Invoking redo results in invoking redo on * all edits between the index of the next edit and the next * significant edit (or the end of the list). Continuing with the previous * example if redo were invoked, redo would in * turn be invoked on A, b and c. In addition * the index of the next edit is set to 3 (as shown in figure 2). *

* Adding an edit to an UndoManager results in * removing all edits from the index of the next edit to the end of * the list. Continuing with the previous example, if a new edit, * e, is added the edit D is removed from the list * (after having die invoked on it). If c is not * incorporated by the next edit * (c.addEdit(e) returns true), or replaced * by it (e.replaceEdit(c) returns true), * the new edit is added after c, as shown in the following * figure. *

* * * *
Figure 4
* *
Figure 4 *
*

* Once end has been invoked on an UndoManager * the superclass behavior is used for all UndoableEdit * methods. Refer to CompoundEdit for more details on its * behavior. *

* Unlike the rest of Swing, this class is thread safe. *

* Warning: * Serialized objects of this class will not be compatible with * future Swing releases. The current serialization support is * appropriate for short term storage or RMI between applications running * the same version of Swing. As of 1.4, support for long term storage * of all JavaBeans™ * has been added to the java.beans package. * Please see {@link java.beans.XMLEncoder}. * * @author Ray Ryan */ @SuppressWarnings("serial") // Same-version serialization only public class UndoManager extends CompoundEdit implements UndoableEditListener { private enum Action { UNDO, REDO, ANY } int indexOfNextAdd; int limit; /** * Creates a new UndoManager. */ public UndoManager() { super(); indexOfNextAdd = 0; limit = 100; edits.ensureCapacity(limit); } /** * Returns the maximum number of edits this {@code UndoManager} * holds. A value less than 0 indicates the number of edits is not * limited. * * @return the maximum number of edits this {@code UndoManager} holds * @see #addEdit * @see #setLimit */ public synchronized int getLimit() { return limit; } /** * Empties the undo manager sending each edit a die message * in the process. * * @see AbstractUndoableEdit#die */ public synchronized void discardAllEdits() { for (UndoableEdit e : edits) { e.die(); } edits = new Vector(); indexOfNextAdd = 0; // PENDING(rjrjr) when vector grows a removeRange() method // (expected in JDK 1.2), trimEdits() will be nice and // efficient, and this method can call that instead. } /** * Reduces the number of queued edits to a range of size limit, * centered on the index of the next edit. */ protected void trimForLimit() { if (limit >= 0) { int size = edits.size(); // System.out.print("limit: " + limit + // " size: " + size + // " indexOfNextAdd: " + indexOfNextAdd + // "\n"); if (size > limit) { int halfLimit = limit/2; int keepFrom = indexOfNextAdd - 1 - halfLimit; int keepTo = indexOfNextAdd - 1 + halfLimit; // These are ints we're playing with, so dividing by two // rounds down for odd numbers, so make sure the limit was // honored properly. Note that the keep range is // inclusive. if (keepTo - keepFrom + 1 > limit) { keepFrom++; } // The keep range is centered on indexOfNextAdd, // but odds are good that the actual edits Vector // isn't. Move the keep range to keep it legal. if (keepFrom < 0) { keepTo -= keepFrom; keepFrom = 0; } if (keepTo >= size) { int delta = size - keepTo - 1; keepTo += delta; keepFrom += delta; } // System.out.println("Keeping " + keepFrom + " " + keepTo); trimEdits(keepTo+1, size-1); trimEdits(0, keepFrom-1); } } } /** * Removes edits in the specified range. * All edits in the given range (inclusive, and in reverse order) * will have die invoked on them and are removed from * the list of edits. This has no effect if * from > to. * * @param from the minimum index to remove * @param to the maximum index to remove */ protected void trimEdits(int from, int to) { if (from <= to) { // System.out.println("Trimming " + from + " " + to + " with index " + // indexOfNextAdd); for (int i = to; from <= i; i--) { UndoableEdit e = edits.elementAt(i); // System.out.println("JUM: Discarding " + // e.getUndoPresentationName()); e.die(); // PENDING(rjrjr) when Vector supports range deletion (JDK // 1.2) , we can optimize the next line considerably. edits.removeElementAt(i); } if (indexOfNextAdd > to) { // System.out.print("...right..."); indexOfNextAdd -= to-from+1; } else if (indexOfNextAdd >= from) { // System.out.println("...mid..."); indexOfNextAdd = from; } // System.out.println("new index " + indexOfNextAdd); } } /** * Sets the maximum number of edits this UndoManager * holds. A value less than 0 indicates the number of edits is not * limited. If edits need to be discarded to shrink the limit, * die will be invoked on them in the reverse * order they were added. The default is 100. * * @param l the new limit * @throws RuntimeException if this {@code UndoManager} is not in progress * ({@code end} has been invoked) * @see #isInProgress * @see #end * @see #addEdit * @see #getLimit */ public synchronized void setLimit(int l) { if (!inProgress) throw new RuntimeException("Attempt to call UndoManager.setLimit() after UndoManager.end() has been called"); limit = l; trimForLimit(); } /** * Returns the next significant edit to be undone if undo * is invoked. This returns null if there are no edits * to be undone. * * @return the next significant edit to be undone */ protected UndoableEdit editToBeUndone() { int i = indexOfNextAdd; while (i > 0) { UndoableEdit edit = edits.elementAt(--i); if (edit.isSignificant()) { return edit; } } return null; } /** * Returns the next significant edit to be redone if redo * is invoked. This returns null if there are no edits * to be redone. * * @return the next significant edit to be redone */ protected UndoableEdit editToBeRedone() { int count = edits.size(); int i = indexOfNextAdd; while (i < count) { UndoableEdit edit = edits.elementAt(i++); if (edit.isSignificant()) { return edit; } } return null; } /** * Undoes all changes from the index of the next edit to * edit, updating the index of the next edit appropriately. * * @param edit the edit to be undo to * @throws CannotUndoException if one of the edits throws * CannotUndoException */ protected void undoTo(UndoableEdit edit) throws CannotUndoException { boolean done = false; while (!done) { UndoableEdit next = edits.elementAt(--indexOfNextAdd); next.undo(); done = next == edit; } } /** * Redoes all changes from the index of the next edit to * edit, updating the index of the next edit appropriately. * * @param edit the edit to be redo to * @throws CannotRedoException if one of the edits throws * CannotRedoException */ protected void redoTo(UndoableEdit edit) throws CannotRedoException { boolean done = false; while (!done) { UndoableEdit next = edits.elementAt(indexOfNextAdd++); next.redo(); done = next == edit; } } /** * Convenience method that invokes one of undo or * redo. If any edits have been undone (the index of * the next edit is less than the length of the edits list) this * invokes redo, otherwise it invokes undo. * * @see #canUndoOrRedo * @see #getUndoOrRedoPresentationName * @throws CannotUndoException if one of the edits throws * CannotUndoException * @throws CannotRedoException if one of the edits throws * CannotRedoException */ public void undoOrRedo() throws CannotRedoException, CannotUndoException { tryUndoOrRedo(Action.ANY); } /** * Returns true if it is possible to invoke undo or * redo. * * @return true if invoking canUndoOrRedo is valid * @see #undoOrRedo */ public synchronized boolean canUndoOrRedo() { if (indexOfNextAdd == edits.size()) { return canUndo(); } else { return canRedo(); } } /** * Undoes the appropriate edits. If end has been * invoked this calls through to the superclass, otherwise * this invokes undo on all edits between the * index of the next edit and the last significant edit, updating * the index of the next edit appropriately. * * @throws CannotUndoException if one of the edits throws * CannotUndoException or there are no edits * to be undone * @see CompoundEdit#end * @see #canUndo * @see #editToBeUndone */ public void undo() throws CannotUndoException { tryUndoOrRedo(Action.UNDO); } /** * Returns true if edits may be undone. If end has * been invoked, this returns the value from super. Otherwise * this returns true if there are any edits to be undone * (editToBeUndone returns non-null). * * @return true if there are edits to be undone * @see CompoundEdit#canUndo * @see #editToBeUndone */ public synchronized boolean canUndo() { if (inProgress) { UndoableEdit edit = editToBeUndone(); return edit != null && edit.canUndo(); } else { return super.canUndo(); } } /** * Redoes the appropriate edits. If end has been * invoked this calls through to the superclass. Otherwise * this invokes redo on all edits between the * index of the next edit and the next significant edit, updating * the index of the next edit appropriately. * * @throws CannotRedoException if one of the edits throws * CannotRedoException or there are no edits * to be redone * @see CompoundEdit#end * @see #canRedo * @see #editToBeRedone */ public void redo() throws CannotRedoException { tryUndoOrRedo(Action.REDO); } private void tryUndoOrRedo(Action action) { UndoableEditLockSupport lockSupport = null; boolean undo; synchronized (this) { if (action == Action.ANY) { undo = indexOfNextAdd == edits.size(); } else { undo = action == Action.UNDO; } if (inProgress) { UndoableEdit edit = undo ? editToBeUndone() : editToBeRedone(); if (edit == null) { throw undo ? new CannotUndoException() : new CannotRedoException(); } lockSupport = getEditLockSupport(edit); if (lockSupport == null) { if (undo) { undoTo(edit); } else { redoTo(edit); } return; } } else { if (undo) { super.undo(); } else { super.redo(); } return; } } // the edit synchronization is required while (true) { lockSupport.lockEdit(); UndoableEditLockSupport editLockSupport = null; try { synchronized (this) { if (action == Action.ANY) { undo = indexOfNextAdd == edits.size(); } if (inProgress) { UndoableEdit edit = undo ? editToBeUndone() : editToBeRedone(); if (edit == null) { throw undo ? new CannotUndoException() : new CannotRedoException(); } editLockSupport = getEditLockSupport(edit); if (editLockSupport == null || editLockSupport == lockSupport) { if (undo) { undoTo(edit); } else { redoTo(edit); } return; } } else { if (undo) { super.undo(); } else { super.redo(); } return; } } } finally { if (lockSupport != null) { lockSupport.unlockEdit(); } lockSupport = editLockSupport; } } } private UndoableEditLockSupport getEditLockSupport(UndoableEdit anEdit) { return anEdit instanceof UndoableEditLockSupport ? (UndoableEditLockSupport)anEdit : null; } /** * Returns true if edits may be redone. If end has * been invoked, this returns the value from super. Otherwise, * this returns true if there are any edits to be redone * (editToBeRedone returns non-null). * * @return true if there are edits to be redone * @see CompoundEdit#canRedo * @see #editToBeRedone */ public synchronized boolean canRedo() { if (inProgress) { UndoableEdit edit = editToBeRedone(); return edit != null && edit.canRedo(); } else { return super.canRedo(); } } /** * Adds an UndoableEdit to this * UndoManager, if it's possible. This removes all * edits from the index of the next edit to the end of the edits * list. If end has been invoked the edit is not added * and false is returned. If end hasn't * been invoked this returns true. * * @param anEdit the edit to be added * @return true if anEdit can be incorporated into this * edit * @see CompoundEdit#end * @see CompoundEdit#addEdit */ public synchronized boolean addEdit(UndoableEdit anEdit) { boolean retVal; // Trim from the indexOfNextAdd to the end, as we'll // never reach these edits once the new one is added. trimEdits(indexOfNextAdd, edits.size()-1); retVal = super.addEdit(anEdit); if (inProgress) { retVal = true; } // Maybe super added this edit, maybe it didn't (perhaps // an in progress compound edit took it instead. Or perhaps // this UndoManager is no longer in progress). So make sure // the indexOfNextAdd is pointed at the right place. indexOfNextAdd = edits.size(); // Enforce the limit trimForLimit(); return retVal; } /** * Turns this UndoManager into a normal * CompoundEdit. This removes all edits that have * been undone. * * @see CompoundEdit#end */ public synchronized void end() { super.end(); this.trimEdits(indexOfNextAdd, edits.size()-1); } /** * Convenience method that returns either * getUndoPresentationName or * getRedoPresentationName. If the index of the next * edit equals the size of the edits list, * getUndoPresentationName is returned, otherwise * getRedoPresentationName is returned. * * @return undo or redo name */ public synchronized String getUndoOrRedoPresentationName() { if (indexOfNextAdd == edits.size()) { return getUndoPresentationName(); } else { return getRedoPresentationName(); } } /** * Returns a description of the undoable form of this edit. * If end has been invoked this calls into super. * Otherwise if there are edits to be undone, this returns * the value from the next significant edit that will be undone. * If there are no edits to be undone and end has not * been invoked this returns the value from the UIManager * property "AbstractUndoableEdit.undoText". * * @return a description of the undoable form of this edit * @see #undo * @see CompoundEdit#getUndoPresentationName */ public synchronized String getUndoPresentationName() { if (inProgress) { if (canUndo()) { return editToBeUndone().getUndoPresentationName(); } else { return UIManager.getString("AbstractUndoableEdit.undoText"); } } else { return super.getUndoPresentationName(); } } /** * Returns a description of the redoable form of this edit. * If end has been invoked this calls into super. * Otherwise if there are edits to be redone, this returns * the value from the next significant edit that will be redone. * If there are no edits to be redone and end has not * been invoked this returns the value from the UIManager * property "AbstractUndoableEdit.redoText". * * @return a description of the redoable form of this edit * @see #redo * @see CompoundEdit#getRedoPresentationName */ public synchronized String getRedoPresentationName() { if (inProgress) { if (canRedo()) { return editToBeRedone().getRedoPresentationName(); } else { return UIManager.getString("AbstractUndoableEdit.redoText"); } } else { return super.getRedoPresentationName(); } } /** * An UndoableEditListener method. This invokes * addEdit with e.getEdit(). * * @param e the UndoableEditEvent the * UndoableEditEvent will be added from * @see #addEdit */ public void undoableEditHappened(UndoableEditEvent e) { addEdit(e.getEdit()); } /** * Returns a string that displays and identifies this * object's properties. * * @return a String representation of this object */ public String toString() { return super.toString() + " limit: " + limit + " indexOfNextAdd: " + indexOfNextAdd; } }