/*
* Freeplane - mind map editor
* Copyright (C) 2008 Dimitry Polivaev
*
* This file author is Dimitry Polivaev
*
* This program 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 2 of the License, or
* (at your option) any later version.
*
* This program 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 this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.freeplane.core.undo;
import java.awt.EventQueue;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import org.freeplane.core.util.LogUtils;
public class UndoHandler implements IUndoHandler {
final private List<ChangeListener> listeners;
private class RedoAction implements ActionListener {
public void actionPerformed(final ActionEvent e) {
redo();
}
}
private class UndoAction implements ActionListener {
public void actionPerformed(final ActionEvent e) {
undo();
}
}
public static final int COMMIT_DELAY = 2;
private static class ActorList extends LinkedList<IActor> {
private static final long serialVersionUID = 1L;
int commitDelay = COMMIT_DELAY;
}
/**
*
*/
private static final int MAX_ENTRIES = 100;
private static final long TIME_TO_BEGIN_NEW_ACTION = 100;
private boolean actionFrameStarted;
private ListIterator<IActor> actorIterator;
private ActorList actorList;
private boolean isUndoActionRunning = false;
final private ActionListener redoAction;
private long timeOfLastAdd;
final private LinkedList<ActorList> transactionList;
final private LinkedList<ListIterator<IActor>> transactionIteratorList;
final private ActionListener undoAction;
private boolean deactivated;
private final ChangeEvent event;
public UndoHandler() {
actionFrameStarted = false;
deactivated = false;
listeners = new LinkedList<ChangeListener>();
actorList = new ActorList();
transactionList = new LinkedList<ActorList>();
transactionIteratorList = new LinkedList<ListIterator<IActor>>();
actorIterator = actorList.listIterator();
redoAction = new RedoAction();
timeOfLastAdd = 0;
undoAction = new UndoAction();
event = new ChangeEvent(this);
}
public void deactivate() {
deactivated = true;
fireStateChanged();
startActionFrame();
}
/*
* (non-Javadoc)
* @see
* freeplane.base.undo.UndoHandler#addActor(freeplane.base.undo.UndoableActor
* )
*/
public void addActor(final IActor actor) {
resetRedo();
actorList.commitDelay = COMMIT_DELAY;
final long currentTime = System.currentTimeMillis();
if (deactivated) {
if (!actionFrameStarted && currentTime - timeOfLastAdd > UndoHandler.TIME_TO_BEGIN_NEW_ACTION) {
deactivated = false;
}
else {
if (actorList.size() > 0) {
actorList.clear();
actorIterator = actorList.listIterator();
}
return;
}
}
if ((actorList.size() > 0)
&& (actionFrameStarted || currentTime - timeOfLastAdd < UndoHandler.TIME_TO_BEGIN_NEW_ACTION)) {
final IActor lastActor = actorIterator.previous();
CompoundActor compoundActor;
if (!(lastActor instanceof CompoundActor)) {
compoundActor = new CompoundActor();
compoundActor.add(lastActor);
actorIterator.set(compoundActor);
}
else {
compoundActor = (CompoundActor) lastActor;
}
compoundActor.add(actor);
actorIterator.next();
}
else {
actorIterator.add(actor);
final int maxEntries = UndoHandler.MAX_ENTRIES;
while (actorList.size() > maxEntries) {
actorList.removeFirst();
actorIterator = actorList.listIterator(actorList.size());
}
}
startActionFrame();
timeOfLastAdd = currentTime;
fireStateChanged();
}
private void fireStateChanged() {
for (final ChangeListener listener : listeners) {
listener.stateChanged(event);
}
}
public boolean canRedo() {
return actorIterator.hasNext();
}
public boolean canUndo() {
return actorIterator.hasPrevious();
}
public void commit() {
resetRedo();
final CompoundActor compoundActor = new CompoundActor(actorList);
actionFrameStarted = false;
timeOfLastAdd = 0;
if (transactionList.isEmpty()) {
// FIXME: this happens when new Maps are closed via the scripting API. Fix the basic error instead.
LogUtils.warn("transactionList is empty on UndoHandler.commit()");
return;
}
actorList = transactionList.removeLast();
actorIterator = transactionIteratorList.removeLast();
if (!compoundActor.isEmpty()) {
addActor(compoundActor);
actionFrameStarted = false;
timeOfLastAdd = 0;
}
else {
fireStateChanged();
}
}
public void delayedCommit() {
if (actorList.commitDelay == 0) {
commit();
return;
}
EventQueue.invokeLater(new Runnable() {
public void run() {
actorList.commitDelay--;
delayedCommit();
}
});
}
public void delayedRollback() {
if (actorList.commitDelay == 0) {
rollback();
return;
}
EventQueue.invokeLater(new Runnable() {
public void run() {
actorList.commitDelay--;
delayedRollback();
}
});
}
public String getLastDescription() {
final String description;
if (canUndo()) {
description = actorList.getLast().getDescription();
}
else {
description = null;
}
return description;
}
/*
* (non-Javadoc)
* @see freeplane.base.undo.UndoHandler#getRedoAction()
*/
public ActionListener getRedoAction() {
return redoAction;
}
/*
* (non-Javadoc)
* @see freeplane.base.undo.UndoHandler#getUndoAction()
*/
public ActionListener getUndoAction() {
return undoAction;
}
public boolean isUndoActionRunning() {
return isUndoActionRunning;
}
/*
* (non-Javadoc)
* @see freeplane.base.undo.UndoHandler#redo()
*/
public void redo() {
if (canRedo()) {
final IActor redoActor = actorIterator.next();
isUndoActionRunning = true;
redoActor.act();
isUndoActionRunning = false;
fireStateChanged();
}
}
public void resetRedo() {
while (canRedo()) {
actorIterator.next();
actorIterator.remove();
}
fireStateChanged();
}
public void rollback() {
try {
isUndoActionRunning = true;
while (actorIterator.hasPrevious()) {
final IActor actor = actorIterator.previous();
actor.undo();
}
}
finally {
isUndoActionRunning = false;
}
if (transactionList.isEmpty()) {
// FIXME: got here if exceptions occur after opening a map via the scripting API. Fix the basic error instead.
LogUtils.warn("transactionList is empty on UndoHandler.rollback()");
return;
}
actorList = transactionList.removeLast();
actorIterator = transactionIteratorList.removeLast();
fireStateChanged();
}
private void startActionFrame() {
if (actionFrameStarted == false && EventQueue.isDispatchThread()) {
actionFrameStarted = true;
EventQueue.invokeLater(new Runnable() {
public void run() {
actionFrameStarted = false;
}
});
}
}
public void forceNewTransaction() {
timeOfLastAdd = 0;
actionFrameStarted = false;
}
public void startTransaction() {
transactionList.addLast(actorList);
transactionIteratorList.addLast(actorIterator);
final ActorList newActorList = new ActorList();
actorList = newActorList;
actorIterator = newActorList.listIterator();
}
/*
* (non-Javadoc)
* @see freeplane.base.undo.UndoHandler#undo()
*/
public void undo() {
if (canUndo()) {
final IActor actor = actorIterator.previous();
try {
isUndoActionRunning = true;
actor.undo();
}
finally {
isUndoActionRunning = false;
fireStateChanged();
}
}
}
public void addChangeListener(final ChangeListener listener) {
listeners.add(listener);
}
public void removeChangeListener(final ChangeListener listener) {
listeners.remove(listener);
}
public int getTransactionLevel() {
return transactionList.size();
}
}