/*
* Copyright 2010-2015 Institut Pasteur.
*
* This file is part of Icy.
*
* Icy is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* Icy 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 for more details.
*
* You should have received a copy of the GNU General Public License
* along with Icy. If not, see <http://www.gnu.org/licenses/>.
*/
package icy.undo;
import java.util.ArrayList;
import java.util.List;
import javax.swing.UIManager;
import javax.swing.event.EventListenerList;
import javax.swing.event.UndoableEditEvent;
import javax.swing.event.UndoableEditListener;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoableEdit;
/**
* Custom UndoManager for Icy.
*
* @author Stephane
*/
// public class IcyUndoManager extends UndoManager implements IcyUndoableEditListener
public class IcyUndoManager extends AbstractUndoableEdit implements UndoableEditListener
{
/**
*
*/
private static final long serialVersionUID = 3080107472163005941L;
private static final int INITIAL_LIMIT = 64;
/**
* owner of UndoManager
*/
protected final Object owner;
/**
* The collection of <code>AbstractIcyUndoableEdit</code>s
* undone/redone "en masse" by this <code>CompoundEdit</code>.
*/
protected List<AbstractIcyUndoableEdit> edits;
/**
* listeners
*/
protected final EventListenerList listeners;
/**
* internals
*/
protected int indexOfNextAdd;
protected int limit;
public IcyUndoManager(Object owner, int limit)
{
super();
edits = new ArrayList<AbstractIcyUndoableEdit>(INITIAL_LIMIT / 2);
this.owner = owner;
listeners = new EventListenerList();
indexOfNextAdd = 0;
this.limit = limit;
}
public IcyUndoManager(Object owner)
{
this(owner, INITIAL_LIMIT);
}
/**
* @return the owner
*/
public Object getOwner()
{
return owner;
}
/**
* 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 <code>die</code> message
* in the process.
*
* @see AbstractUndoableEdit#die
*/
public void discardAllEdits()
{
if (trimEdits(0, edits.size() - 1))
fireChangeEvent();
}
/**
* Remove future edits (the ones to redo) from the undo manager sending each edit a
* <code>die</code> message in the process.
*
* @param keep
* number of future edits to keep (1 means we keep only the next edit)
* @see AbstractUndoableEdit#die
*/
public synchronized void discardFutureEdits(int keep)
{
if (trimEdits(indexOfNextAdd + keep, edits.size() - 1))
fireChangeEvent();
}
/**
* Remove future edits (the ones to redo) from the undo manager sending each edit a
* <code>die</code> message in the process.
*
* @see AbstractUndoableEdit#die
*/
public synchronized void discardFutureEdits()
{
discardFutureEdits(0);
}
/**
* Remove previous edits (the ones to undo) from the undo manager sending each edit a
* <code>die</code> message
* in the process.
*
* @param keep
* number of previous edits to keep (1 mean we keep only the last edit)
* @see AbstractUndoableEdit#die
*/
public synchronized void discardOldEdits(int keep)
{
if (trimEdits(0, indexOfNextAdd - (1 + keep)))
fireChangeEvent();
}
/**
* Remove old edits (the ones to undo) from the undo manager sending each edit a
* <code>die</code> message in the process.
*
* @see AbstractUndoableEdit#die
*/
public synchronized void discardOldEdits()
{
discardOldEdits(0);
}
/**
* Reduces the number of queued edits to a range of size limit,
* centered on the index of the next edit.
*/
protected boolean trimForLimit()
{
boolean result = false;
if (limit >= 0)
{
synchronized (edits)
{
final int size = edits.size();
if (size > limit)
{
final 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;
}
if (trimEdits(keepTo + 1, size - 1))
result = true;
if (trimEdits(0, keepFrom - 1))
result = true;
}
}
}
return result;
}
/**
* Removes edits in the specified range.
* All edits in the given range (inclusive, and in reverse order)
* will have <code>die</code> invoked on them and are removed from
* the list of edits. This has no effect if <code>from</code> > <code>to</code>.
*
* @param from
* the minimum index to remove
* @param to
* the maximum index to remove
* @return <code>true</code> if some edits has been removed.
*/
protected boolean trimEdits(int from, int to)
{
if (from <= to)
{
synchronized (edits)
{
for (int i = to; i >= from; i--)
{
edits.get(i).die();
edits.remove(i);
}
}
if (indexOfNextAdd > to)
indexOfNextAdd -= to - from + 1;
else if (indexOfNextAdd >= from)
indexOfNextAdd = from;
return true;
}
return false;
}
/**
* Sets the maximum number of edits this <code>UndoManager</code> holds. A value less than 0
* indicates the number of edits is not limited. If edits need to be discarded
* to shrink the limit, <code>die</code> 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 #addEdit
* @see #getLimit
*/
public synchronized void setLimit(int l)
{
limit = l;
if (trimForLimit())
fireChangeEvent();
}
/**
* Returns the the next significant edit to be undone if <code>undo</code> is invoked. This
* returns <code>null</code> if there are no edits to be undone.
*
* @return the next significant edit to be undone
*/
protected AbstractIcyUndoableEdit editToBeUndone()
{
synchronized (edits)
{
int i = indexOfNextAdd;
while (i > 0)
{
final AbstractIcyUndoableEdit edit = edits.get(--i);
if (edit.isSignificant())
return edit;
}
}
return null;
}
/**
* Returns the the next significant edit to be redone if <code>redo</code> is invoked. This
* returns <code>null</code> if there are no edits to be redone.
*
* @return the next significant edit to be redone
*/
protected AbstractIcyUndoableEdit editToBeRedone()
{
synchronized (edits)
{
final int count = edits.size();
int i = indexOfNextAdd;
while (i < count)
{
final AbstractIcyUndoableEdit edit = edits.get(i++);
if (edit.isSignificant())
return edit;
}
}
return null;
}
/**
* Undoes all changes.
*
* @throws CannotUndoException
* if one of the edits throws <code>CannotUndoException</code>
*/
public void undoAll() throws CannotUndoException
{
while (indexOfNextAdd > 0)
{
final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd);
next.undo();
}
// automatically remove useless edits
if (!canRedo())
discardFutureEdits();
fireChangeEvent();
}
/**
* Undoes all changes from the index of the next edit to <code>edit</code>, updating the index
* of the next edit appropriately.
*
* @throws CannotUndoException
* if one of the edits throws <code>CannotUndoException</code>
*/
protected void undoTo(AbstractIcyUndoableEdit edit) throws CannotUndoException
{
boolean done = false;
while (!done)
{
final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd);
next.undo();
done = (next == edit);
}
}
/**
* Undoes the appropriate edits. This invokes <code>undo</code> 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 <code>CannotUndoException</code> or there are no edits
* to be undone
* @see #canUndo
* @see #editToBeUndone
*/
@Override
public synchronized void undo() throws CannotUndoException
{
final AbstractIcyUndoableEdit edit = editToBeUndone();
if (edit == null)
throw new CannotUndoException();
undoTo(edit);
// automatically remove useless edits
if (!canRedo())
discardFutureEdits();
fireChangeEvent();
}
/**
* Returns true if edits may be undone. This returns true if there are any edits to be undone
* (<code>editToBeUndone</code> returns non-<code>null</code>).
*
* @return true if there are edits to be undone
* @see #editToBeUndone
*/
@Override
public synchronized boolean canUndo()
{
final AbstractIcyUndoableEdit edit = editToBeUndone();
return (edit != null) && edit.canUndo();
}
/**
* Redoes all changes from the index of the next edit to <code>edit</code>, updating the index
* of the next edit appropriately.
*
* @throws CannotRedoException
* if one of the edits throws <code>CannotRedoException</code>
*/
protected void redoTo(AbstractIcyUndoableEdit edit) throws CannotRedoException
{
boolean done = false;
while (!done)
{
final AbstractIcyUndoableEdit next = edits.get(indexOfNextAdd++);
next.redo();
done = (next == edit);
}
}
/**
* Redo the appropriate edits. This invokes <code>redo</code> 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 <code>CannotRedoException</code> or there are no edits
* to be redone
* @see CompoundEdit#end
* @see #canRedo
* @see #editToBeRedone
*/
@Override
public synchronized void redo() throws CannotRedoException
{
AbstractIcyUndoableEdit edit = editToBeRedone();
if (edit == null)
throw new CannotRedoException();
redoTo(edit);
// automatically remove useless edits
if (!canUndo())
discardOldEdits();
fireChangeEvent();
}
/**
* Returns true if edits may be redone. If <code>end</code> has
* been invoked, this returns the value from super. Otherwise,
* this returns true if there are any edits to be redone
* (<code>editToBeRedone</code> returns non-<code>null</code>).
*
* @return true if there are edits to be redone
* @see CompoundEdit#canRedo
* @see #editToBeRedone
*/
@Override
public synchronized boolean canRedo()
{
final AbstractIcyUndoableEdit edit = editToBeRedone();
return (edit != null) && edit.canRedo();
}
/**
* Undo or redo all changes until the specified edit.<br>
* The specified edit should be in "done" state after the operation.<br>
* That means redo operation is inclusive while undo is exclusive.<br>
* To undo all operations just use undoAll().
*/
public synchronized void undoOrRedoTo(AbstractIcyUndoableEdit edit) throws CannotRedoException, CannotUndoException
{
final int index = getIndex(edit);
// can undo or redo ?
if (index != -1)
{
// we want indexOfNextAdd to change to (index + 1)
while ((indexOfNextAdd - 1) > index)
{
// process undo
final AbstractIcyUndoableEdit next = edits.get(--indexOfNextAdd);
next.undo();
}
while (indexOfNextAdd <= index)
{
// process undo
AbstractIcyUndoableEdit next = edits.get(indexOfNextAdd++);
next.redo();
}
// automatically remove useless edits
if (!canUndo())
discardOldEdits();
if (!canRedo())
discardFutureEdits();
// notify change
fireChangeEvent();
}
}
/**
* Returns the last <code>AbstractIcyUndoableEdit</code> in <code>edits</code>, or
* <code>null</code> if <code>edits</code> is empty.
*/
protected AbstractIcyUndoableEdit lastEdit()
{
synchronized (edits)
{
int count = edits.size();
if (count > 0)
return edits.get(count - 1);
}
return null;
}
@Override
public boolean addEdit(UndoableEdit anEdit)
{
if (anEdit instanceof AbstractIcyUndoableEdit)
addEdit((AbstractIcyUndoableEdit) anEdit);
return super.addEdit(anEdit);
}
/**
* Adds an <code>AbstractIcyUndoableEdit</code> to this <code>UndoManager</code>, if it's
* possible. This
* removes all edits from the index of the next edit to the end of the edits
* list.
*
* @param anEdit
* the edit to be added
* @see CompoundEdit#addEdit
*/
public synchronized void addEdit(AbstractIcyUndoableEdit anEdit)
{
synchronized (edits)
{
// 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);
final AbstractIcyUndoableEdit last = lastEdit();
// If this is the first edit received, just add it.
// Otherwise, give the last one a chance to absorb the new
// one. If it won't, give the new one a chance to absorb
// the last one.
if (last == null)
edits.add(anEdit);
else if (!last.addEdit(anEdit))
{
// try to replace current edit
if (anEdit.replaceEdit(last))
edits.set(edits.size() - 1, anEdit);
else
// simply add the new edit
edits.add(anEdit);
}
// make sure the indexOfNextAdd is pointed at the right place
indexOfNextAdd = edits.size();
// enforce the limit
trimForLimit();
}
// notify change
fireChangeEvent();
}
/**
* Prevent the last edit inserted in the UndoManager to be merged with the next inserted edit
* (even if they are compatible)
*/
public void noMergeForNextEdit()
{
// set last edit to un-mergeable
if (indexOfNextAdd > 0)
edits.get(indexOfNextAdd - 1).setMergeable(false);
}
/**
* Returns a description of the undoable form of this edit.
* 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 this returns the value from
* the <code>UIManager</code> property "AbstractUndoableEdit.undoText".
*
* @return a description of the undoable form of this edit
* @see #undo
* @see CompoundEdit#getUndoPresentationName
*/
@Override
public synchronized String getUndoPresentationName()
{
if (canUndo())
return editToBeUndone().getUndoPresentationName();
return UIManager.getString("AbstractUndoableEdit.undoText");
}
/**
* Returns a description of the redoable form of this edit.
* 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 this returns the value from
* the <code>UIManager</code> property "AbstractUndoableEdit.redoText".
*
* @return a description of the redoable form of this edit
* @see #redo
* @see CompoundEdit#getRedoPresentationName
*/
@Override
public synchronized String getRedoPresentationName()
{
if (canRedo())
return editToBeRedone().getRedoPresentationName();
return UIManager.getString("AbstractUndoableEdit.redoText");
}
/**
* Add the specified listener to listeners list
*/
public void addListener(IcyUndoManagerListener listener)
{
listeners.add(IcyUndoManagerListener.class, listener);
}
/**
* Remove the specified listener from listeners list
*/
public void removeListener(IcyUndoManagerListener listener)
{
listeners.remove(IcyUndoManagerListener.class, listener);
}
/**
* Get listeners list
*/
public IcyUndoManagerListener[] getListeners()
{
return listeners.getListeners(IcyUndoManagerListener.class);
}
/**
* fire change event
*/
private void fireChangeEvent()
{
for (IcyUndoManagerListener listener : listeners.getListeners(IcyUndoManagerListener.class))
listener.undoManagerChanged(this);
}
/**
* Retrieve all edits in the UndoManager
*/
public List<AbstractIcyUndoableEdit> getAllEdits()
{
synchronized (edits)
{
return new ArrayList<AbstractIcyUndoableEdit>(edits);
}
}
/**
* Get number of edit in UndoManager
*/
public int getEditsCount()
{
return edits.size();
}
/**
* Get the index in list of specified edit
*/
public int getIndex(AbstractIcyUndoableEdit e)
{
return edits.indexOf(e);
}
/**
* Get the index in list of specified significant edit
*/
public int getSignificantIndex(AbstractIcyUndoableEdit e)
{
int result = 0;
synchronized (edits)
{
for (AbstractIcyUndoableEdit edit : edits)
{
if (edit.isSignificant())
{
if (edit == e)
return result;
result++;
}
}
}
return -1;
}
/**
* Get number of significant edit before the specified position in UndoManager
*/
public int getSignificantEditsCountBefore(int index)
{
int result = 0;
synchronized (edits)
{
for (int i = 0; i < index; i++)
if (edits.get(i).isSignificant())
result++;
}
return result;
}
/**
* Get number of significant edit in UndoManager
*/
public int getSignificantEditsCount()
{
int result = 0;
synchronized (edits)
{
for (AbstractIcyUndoableEdit edit : edits)
if (edit.isSignificant())
result++;
}
return result;
}
/**
* Get edit of specified index
*/
public AbstractIcyUndoableEdit getEdit(int index)
{
synchronized (edits)
{
if (index < edits.size())
return edits.get(index);
}
return null;
}
/**
* Get significant edit of specified index
*/
public AbstractIcyUndoableEdit getSignificantEdit(int index)
{
int i = 0;
synchronized (edits)
{
for (AbstractIcyUndoableEdit edit : edits)
{
if (edit.isSignificant())
{
if (i == index)
return edit;
i++;
}
}
}
return null;
}
/**
* Return the next insert index.
*/
public int getNextAddIndex()
{
return indexOfNextAdd;
}
/**
* Get index of first edit from specified source
*/
public int getFirstEditIndex(Object source)
{
if (source == null)
return -1;
synchronized (edits)
{
for (int i = 0; i < edits.size(); i++)
if ((edits.get(i)).getSource() == source)
return i;
}
return -1;
}
/**
* Get index of last edit from specified source
*/
public int getLastEditIndex(Object source)
{
if (source == null)
return -1;
synchronized (edits)
{
for (int i = edits.size() - 1; i >= 0; i--)
if ((edits.get(i)).getSource() == source)
return i;
}
return -1;
}
/**
* Discard edits from specified source by sending each edit a <code>die</code> message
* in the process.
*/
public void discardEdits(Object source)
{
synchronized (edits)
{
final int lastIndex = getLastEditIndex(source);
if (lastIndex != -1)
{
final List<AbstractIcyUndoableEdit> validEdits = new ArrayList<AbstractIcyUndoableEdit>();
// keep valid edits
for (int i = lastIndex + 1; i < edits.size(); i++)
validEdits.add(edits.get(i));
// remove all edits
edits.clear();
indexOfNextAdd = 0;
// add valid edits
for (AbstractIcyUndoableEdit edit : validEdits)
edits.add(edit);
// make sure the indexOfNextAdd is pointed at the right place
indexOfNextAdd = edits.size();
// notify we removed some edits
fireChangeEvent();
}
}
}
@Override
public void undoableEditHappened(UndoableEditEvent e)
{
addEdit(e.getEdit());
}
}