/*
* @(#)UndoManager.java 1.35 03/12/19
*
* Copyright 2004 Sun Microsystems, Inc. All rights reserved.
* SUN PROPRIETARY/CONFIDENTIAL. Use is subject to license terms.
*/
package javax.swing.undo;
import javax.swing.event.*;
import javax.swing.UIManager;
import java.util.*;
/**
* Concrete subclass of CompoundEdit
* which can serve as a UndoableEditListener
,
* consolidating the UndoableEditEvents
from a
* variety of sources, and undoing or redoing them one at a time.
*
* Unlike AbstractUndoableEdit
and CompoundEdit
,
* the public methods of this
* class are synchronized, and should be safe to call from multiple
* threads. This should make UndoManager
* a convenient marshall for sets of undoable JavaBeans.
*
* 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 JavaBeansTM
* has been added to the java.beans
package.
* Please see {@link java.beans.XMLEncoder}.
*
* @author Ray Ryan
* @version 1.35, 12/19/03
*/
public class UndoManager extends CompoundEdit implements UndoableEditListener {
int indexOfNextAdd;
int limit;
public UndoManager() {
super();
indexOfNextAdd = 0;
limit = 100;
edits.ensureCapacity(limit);
}
/**
* Returns the maximum number of edits this UndoManager will
* hold. Default value is 100.
*
* @see #addEdit
* @see #setLimit
*/
public synchronized int getLimit() {
return limit;
}
/**
* Empty the undo manager, sending each edit a die message
* in the process.
*/
public synchronized void discardAllEdits() {
Enumeration cursor = edits.elements();
while (cursor.hasMoreElements()) {
UndoableEdit e = (UndoableEdit)cursor.nextElement();
e.die();
}
edits = new Vector(limit);
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.
}
/**
* Reduce the number of queued edits to a range of size limit,
* centered on indexOfNextAdd.
*/
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);
}
}
}
/**
* Tell the edits in the given range (inclusive) to die, and
* remove them from edits. from > to is a no-op.
*/
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 = (UndoableEdit)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);
}
}
/**
* Set the maximum number of edits this UndoManager will hold. If
* edits need to be discarded to shrink the limit, they will be
* told to die in the reverse of the order that they were added.
*
* @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 the next significant edit to be undone if undo is
* called. May return null
*/
protected UndoableEdit editToBeUndone() {
int i = indexOfNextAdd;
while (i > 0) {
UndoableEdit edit = (UndoableEdit)edits.elementAt(--i);
if (edit.isSignificant()) {
return edit;
}
}
return null;
}
/**
* Returns the the next significant edit to be redone if redo is
* called. May return null
*/
protected UndoableEdit editToBeRedone() {
int count = edits.size();
int i = indexOfNextAdd;
while (i < count) {
UndoableEdit edit = (UndoableEdit)edits.elementAt(i++);
if (edit.isSignificant()) {
return edit;
}
}
return null;
}
/**
* Undoes all changes from indexOfNextAdd to edit. Updates indexOfNextAdd accordingly.
*/
protected void undoTo(UndoableEdit edit) throws CannotUndoException {
boolean done = false;
while (!done) {
UndoableEdit next = (UndoableEdit)edits.elementAt(--indexOfNextAdd);
next.undo();
done = next == edit;
}
}
/**
* Redoes all changes from indexOfNextAdd to edit. Updates indexOfNextAdd accordingly.
*/
protected void redoTo(UndoableEdit edit) throws CannotRedoException {
boolean done = false;
while (!done) {
UndoableEdit next = (UndoableEdit)edits.elementAt(indexOfNextAdd++);
next.redo();
done = next == edit;
}
}
/**
* Undo or redo as appropriate. Suitable for binding to an action
* that toggles between these two functions. Only makes sense
* to send this if limit == 1.
*
* @see #canUndoOrRedo
* @see #getUndoOrRedoPresentationName
*/
public synchronized void undoOrRedo() throws CannotRedoException,
CannotUndoException {
if (indexOfNextAdd == edits.size()) {
undo();
} else {
redo();
}
}
/**
* Return true if calling undoOrRedo will undo or redo. Suitable
* for deciding to enable a command that toggles between the two
* functions, which only makes sense to use if limit == 1.
*
* @see #undoOrRedo
*/
public synchronized boolean canUndoOrRedo() {
if (indexOfNextAdd == edits.size()) {
return canUndo();
} else {
return canRedo();
}
}
/**
* If this UndoManager is inProgress, undo the last significant
* UndoableEdit before indexOfNextAdd, and all insignificant edits back to
* it. Updates indexOfNextAdd accordingly.
*
*
If not inProgress, indexOfNextAdd is ignored and super's routine is * called.
* * @see CompoundEdit#end */ public synchronized void undo() throws CannotUndoException { if (inProgress) { UndoableEdit edit = editToBeUndone(); if (edit == null) { throw new CannotUndoException(); } undoTo(edit); } else { super.undo(); } } /** * Overridden to preserve usual semantics: returns true if an undo * operation would be successful now, false otherwise */ public synchronized boolean canUndo() { if (inProgress) { UndoableEdit edit = editToBeUndone(); return edit != null && edit.canUndo(); } else { return super.canUndo(); } } /** * If thisUndoManager
is inProgress
,
* redoes the last significant UndoableEdit
at
* indexOfNextAdd
or after, and all insignificant
* edits up to it. Updates indexOfNextAdd
accordingly.
*
* If not inProgress
, indexOfNextAdd
* is ignored and super's routine is called.
If not inProgress
, acts as a
* CompoundEdit
.
*
* @param anEdit the edit to be added
* @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;
}
/**
* Sending end() to an UndoManager turns it into a plain old
* (ended) CompoundEdit.
*
*
Calls super's end() method (making inProgress false), then * sends die() to the unreachable edits at indexOfNextAdd and * beyond, in the reverse of the order in which they were added. * * @see CompoundEdit#end */ public synchronized void end() { super.end(); this.trimEdits(indexOfNextAdd, edits.size()-1); } /** * Return the appropriate name for a command that toggles between * undo and redo. Only makes sense to use such a command if limit * == 1 and we're not in progress. */ public synchronized String getUndoOrRedoPresentationName() { if (indexOfNextAdd == edits.size()) { return getUndoPresentationName(); } else { return getRedoPresentationName(); } } /** * If inProgress, returns getUndoPresentationName of the * significant edit that will be undone when undo() is invoked. * If there is none, returns AbstractUndoableEdit.undoText from the * defaults table. * *
If not inProgress, acts as a CompoundEdit
* * @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(); } } /** * If inProgress, returns getRedoPresentationName of the * significant edit that will be redone when redo() is invoked. * If there is none, returns AbstractUndoableEdit.redoText from the * defaults table. * *If not inProgress, acts as a CompoundEdit
* * @see #redo * @see CompoundEdit#getUndoPresentationName */ public synchronized String getRedoPresentationName() { if (inProgress) { if (canRedo()) { return editToBeRedone().getRedoPresentationName(); } else { return UIManager.getString("AbstractUndoableEdit.redoText"); } } else { return super.getRedoPresentationName(); } } /** * Called by the UndoabledEdit sources this UndoManager listens * to. Calls addEdit with e.getEdit(). * * @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; } }