/*
* 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 * |
* 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 * |
* 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 * |
* 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 * |
* 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 Vectordie
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;
}
}