source.ca.odell.glazedlists.UndoRedoSupport Maven / Gradle / Ivy
Show all versions of glazedlists_java15 Show documentation
/* Glazed Lists (c) 2003-2006 */
/* http://publicobject.com/glazedlists/ publicobject.com,*/
/* O'Dell Engineering Ltd.*/
package ca.odell.glazedlists;
import ca.odell.glazedlists.event.ListEvent;
import ca.odell.glazedlists.event.ListEventListener;
import javax.swing.event.EventListenerList;
import java.util.*;
/**
* UndoRedoSupport, as the name suggests, will provide generic support for
* undoing and redoing groups of changes to an {@link EventList}. The
* granularity of each undoable edit is determined by the ListEvent from which
* it was generated.
*
* Not every change described in a ListEvent results in an undoable edit.
* Specifically, a mutation of a list element IN PLACE does
* not produce an undoable edit. For example, an {@link ObservableElementList}
* which observes a change of an element, or a call to {@link List#set} with
* the same object at that index produce a ListEvent that does not have a
* corresponding {@link Edit} object. These ListEvents are ignored because they
* lack sufficient information to undo or redo the change.
*
*
In general UndoRedoSupport only makes sense for use with a
* {@link BasicEventList} or a trivial wrapper around a BasicEventList which
* does not affect the order or type of the elements, such as an
* {@link ObservableElementList}. Advanced transformations, such as
* {@link SortedList} or {@link FilterList} will not work as expected with this
* UndoRedoSupport class since their contents are controlled by information
* outside of themselves ({@link Comparator}s and
* {@link ca.odell.glazedlists.matchers.Matcher}s).
*
*
This class is agnostic to any particular GUI toolkit. As such it may be
* used in a headless environment or can also be bound to a specific toolkit.
*
* @author James Lemieux
*/
public final class UndoRedoSupport {
/** A wrapper around the true source EventList provides control over the granularity of ListEvents it produces. */
private TransactionList txSource;
/** A ListEventListener that watches the {@link #txSource} and in turn broadcasts an {@link Edit} object to all {@link Listener}s */
private ListEventListener txSourceListener = new TXSourceListener();
/** A data structure storing all registered {@link Listener}s. */
private final EventListenerList listenerList = new EventListenerList();
/**
* A count which, when greater than 0, indicates a ListEvent must be
* ignored by this UndoRedoSupport because it was caused by an undo or
* redo. An int is used rather than a boolean flag so that nested
* undos/redos are properly handled.
*/
private int ignoreListEvent = 0;
/**
* A list of the elements in the source
prior to the most
* recent change. It is necessary to track the prior elements since
* list removals broadcast ListEvents which do not include the removed
* element as part of the ListEvent. We use this list to locate
* removed elements when constructing objects that can undo the change.
* todo remove this list when ListEvent can reliably furnish us with a deleted value
*/
private List priorElements;
private UndoRedoSupport(EventList source) {
// build a TransactionList that does NOT support rollback - we don't
// need it and it relies on UndoRedoSupport, so we would have
this.txSource = new TransactionList(source, false);
this.txSource.addListEventListener(txSourceListener);
this.priorElements = new ArrayList(source);
}
/**
* Add a {@link Listener} which will receive a callback when an undoable
* edit occurs on the given source {@link EventList}.
*/
public void addUndoSupportListener(Listener l) {
listenerList.add(Listener.class, l);
}
/**
* Remove a {@link Listener} from receiving a callback when an undoable
* edit occurs on the given source {@link EventList}.
*/
public void removeUndoSupportListener(Listener l) {
listenerList.remove(Listener.class, l);
}
/**
* Notifies all registered {@link Listener}s of the given edit
.
*/
private void fireUndoableEditHappened(Edit edit) {
Object[] listeners = listenerList.getListenerList();
for (int i = listeners.length-2; i>=0; i-=2) {
if (listeners[i]== Listener.class)
((Listener) listeners[i+1]).undoableEditHappened(edit);
}
}
/**
* Installs support for undoing and redoing changes to the given
* source
. To be notified of undoable changes, a
* {@link Listener} must be registered on the object that is returned by
* this method. That Listener object will typically add the {@link Edit}
* it is given over to whatever data structure is managing all undo/redo
* functions for the entire application.
*
* @param source the EventList on which to provide undo/redo capabilities
* @return an instance of UndoRedoSupport through which the undo/redo behaviour
* can be customized
*/
public static UndoRedoSupport install(EventList source) {
return new UndoRedoSupport(source);
}
/**
* This method removes undo/redo support from the {@link EventList} it was
* installed on. This method is useful when the {@link EventList} must
* outlive the UndoRedoSupport object itself. The UndoRedoSupport object will become
* available for garbage collection independently of the {@link EventList}
* it decorated with behaviour.
*/
public void uninstall() {
txSource.dispose();
txSource.removeListEventListener(txSourceListener);
txSource = null;
priorElements = null;
}
/**
* This Listener watches the TransactionList for changes and responds by
* created an {@link Edit} and broadcasting that object to all registered
* {@link Listener}s.
*/
private class TXSourceListener implements ListEventListener {
public void listChanged(ListEvent listChanges) {
// if an undo or redo caused this ListEvent, it is not an undoable edit
if (ignoreListEvent > 0)
return;
// build a CompositeEdit that describes the ListEvent and provides methods for undoing and redoing it
final CompositeEdit edit = new CompositeEdit();
while (listChanges.next()) {
final int changeIndex = listChanges.getIndex();
final int changeType = listChanges.getType();
// provide an AddEdit to the CompositeEdit
if (changeType == ListEvent.INSERT) {
final E inserted = txSource.get(changeIndex);
priorElements.add(changeIndex, inserted);
edit.add(new AddEdit(txSource, changeIndex, inserted));
// provide a RemoveEdit to the CompositeEdit
} else if (changeType == ListEvent.DELETE) {
// try to get the previous value through the ListEvent
E deleted = listChanges.getOldValue();
E deletedElementFromPrivateCopy = priorElements.remove(changeIndex);
// if the ListEvent could not give us the previous value, use the value from priorElements
if (deleted == ListEvent.UNKNOWN_VALUE)
deleted = deletedElementFromPrivateCopy;
edit.add(new RemoveEdit(txSource, changeIndex, deleted));
// provide an UpdateEdit to the CompositeEdit
} else if (changeType == ListEvent.UPDATE) {
E previousValue = listChanges.getOldValue();
// if the ListEvent could not give us the previous value, use the value from priorElements
if (previousValue == ListEvent.UNKNOWN_VALUE)
previousValue = priorElements.get(changeIndex);
final E newValue = txSource.get(changeIndex);
// if a different object is present at the index
if (newValue != previousValue) {
priorElements.set(changeIndex, newValue);
edit.add(new UpdateEdit(txSource, changeIndex, newValue, previousValue));
}
}
}
// if the edit has real contents, broadcast it
if (!edit.isEmpty())
fireUndoableEditHappened(edit.getSimplestEdit());
}
}
/**
* Implementations of this Listener interface should be registered with an
* UndoRedoSupport object via {@link UndoRedoSupport#addUndoSupportListener}. They
* will be notified of each undoable edit that occurs to the given EventList.
*/
public interface Listener extends EventListener {
/**
* Notified of each undoable edit applied to the given EventList.
*/
public void undoableEditHappened(Edit edit);
}
/**
* Provides an easy interface to undo/redo a ListEvent in its entirety.
* At any point in time it is only possible to do one, and only one, of
* {@link #undo} and {@link #redo}. To determine which one is allowed, use
* {@link #canUndo()} and {@link #canRedo()}.
*/
public interface Edit {
/** Undo the edit. */
public void undo();
/** Returns true if this edit may be undone. */
public boolean canUndo();
/** Re-applies the edit. */
public void redo();
/** Returns true if this edit may be redone. */
public boolean canRedo();
}
private abstract class AbstractEdit implements Edit {
/** Initially the Edit can be undone but not redone. */
protected boolean canUndo = true;
public void undo() {
// validate that we can proceed with the undo
if (!canUndo())
throw new IllegalStateException("The Edit is in an incorrect state for undoing");
ignoreListEvent++;
try {
undoImpl();
} finally {
ignoreListEvent--;
}
canUndo = false;
}
public void redo() {
// validate that we can proceed with the redo
if (!canRedo())
throw new IllegalStateException("The Edit is in an incorrect state for redoing");
ignoreListEvent++;
try {
redoImpl();
} finally {
ignoreListEvent--;
}
canUndo = true;
}
protected abstract void undoImpl();
protected abstract void redoImpl();
public final boolean canUndo() { return canUndo; }
public final boolean canRedo() { return !canUndo; }
}
/**
* An Edit which acts as a container for finer-grained Edit objects.
*/
final class CompositeEdit extends AbstractEdit {
/** The edits in the order they were made. */
private final List edits = new ArrayList();
/** Adds a single Edit to this container of Edits. */
void add(Edit edit) { edits.add(edit); }
/** Returns true if this container of Edits is empty; false otherwise. */
private boolean isEmpty() { return edits.isEmpty(); }
/** Returns the single Edit contained within this composite, if only one exists, otherwise it returns this entire CompositeEdit. */
private Edit getSimplestEdit() { return edits.size() == 1 ? edits.get(0) : this; }
public void undoImpl() {
txSource.beginEvent();
try {
// undo the Edits in reverse order they were applied
for (ListIterator i = edits.listIterator(edits.size()); i.hasPrevious();)
i.previous().undo();
} finally {
txSource.commitEvent();
}
}
public void redoImpl() {
txSource.beginEvent();
try {
// re-apply each edit in their original order
for (Edit edit : edits)
edit.redo();
} finally {
txSource.commitEvent();
}
}
}
/**
* A base class implementing common logic and storage for the specific kind
* of Edits which can occur to a single index in the EventList.
*/
private abstract class AbstractSimpleEdit extends AbstractEdit {
protected final EventList source;
protected final int index;
protected final E value;
protected AbstractSimpleEdit(EventList source, int index, E value) {
this.source = source;
this.index = index;
this.value = value;
}
}
/**
* A class describing an undoable Add to an EventList.
*/
private final class AddEdit extends AbstractSimpleEdit {
public AddEdit(EventList source, int index, E value) {
super(source, index, value);
}
public void undoImpl() { source.remove(index); }
public void redoImpl() { source.add(index, value); }
}
/**
* A class describing an undoable Remove to an EventList.
*/
private final class RemoveEdit extends AbstractSimpleEdit {
public RemoveEdit(EventList source, int index, E value) {
super(source, index, value);
}
public void undoImpl() { source.add(index, value); }
public void redoImpl() { source.remove(index); }
}
/**
* A class describing an undoable Update to an EventList.
*/
private final class UpdateEdit extends AbstractSimpleEdit {
private final E oldValue;
public UpdateEdit(EventList source, int index, E value, E oldValue) {
super(source, index, value);
this.oldValue = oldValue;
}
public void undoImpl() { source.set(index, oldValue); }
public void redoImpl() { source.set(index, value); }
}
}