// **********************************************************************
//
// <copyright>
//
// BBN Technologies
// 10 Moulton Street
// Cambridge, MA 02138
// (617) 873-8000
//
// Copyright (C) BBNT Solutions LLC. All rights reserved.
//
// </copyright>
// **********************************************************************
//
// $Source: /cvs/distapps/openmap/src/openmap/com/bbn/openmap/proj/ProjectionStack.java,v $
// $RCSfile: ProjectionStack.java,v $
// $Revision: 1.8 $
// $Date: 2009/01/21 01:24:41 $
// $Author: dietrick $
//
// **********************************************************************
package com.bbn.openmap.event;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.EmptyStackException;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import com.bbn.openmap.OMComponent;
/**
* Provides a stack of actions that can be undone/redone. The UndoEvent objects
* contain the information needed to implement the actions. The UndoStack is
* told to push UndoEvents on their source objects by UndoStackTriggers using a
* regular ActionListener/ActionEvent mechanism. The UndoStack is an
* ActionListener to the triggers (GUI buttons/menu items that say Undo or
* Redo).
*/
public class UndoStack
extends OMComponent
implements ActionListener {
private final static Logger logger = Logger.getLogger("com.bbn.openmap.event.UndoStack");
public final static int DEFAULT_MAX_SIZE = 10;
public final static int REMEMBER_ALL = -1;
/**
* The notion of the current state is important. When the user does
* something, a component that is interested in undo/redo should push a
* current state to this stack. This current state reflects the state of
* some object at the current time. If another state gets set, then the old
* state is now put on the undo stack. If the undo command is given to this
* stack, then an event is popped off the undo stack, set to be the current
* state. The old state gets pushed to the redo stack.
*/
protected transient UndoEvent currentState;
protected int stackSize = DEFAULT_MAX_SIZE;
public final static transient String UndoCmd = "undo";
public final static transient String RedoCmd = "redo";
public final static transient String ClearUndoCmd = "clearUndoStack";
public final static transient String ClearRedoCmd = "clearRedoStack";
public final static transient String ClearCmd = "clearStacks";
protected final Stack<UndoEvent> undoStack;
protected final Stack<UndoEvent> redoStack;
protected final UndoStackSupport triggers;
/**
*/
public UndoStack() {
redoStack = new Stack<UndoEvent>();
undoStack = new Stack<UndoEvent>();
triggers = new UndoStackSupport();
}
/**
* Sets the current state of some object on the stack. The stack doesn't
* care what that event/state represents, since it has all the info needed
* to tell components how to get back to this state later. Anyway, this
* current state kinda hangs out in limbo. If another event/state comes in,
* the current state gets pushed on the undo stack. The redo stack gets
* cleared, since a new state path forward has been established.
*
* @param event
*/
public void setTheWayThingsAre(UndoEvent event) {
if (currentState != null) {
rememberLastThing(currentState);
}
currentState = event;
if (logger.isLoggable(Level.FINE)) {
logger.fine("making (" + currentState.getDescription() + ") current state");
}
// We have a new path forward, undefined, so clear out old path forward
redoStack.clear();
fireStackStatus();
}
public void actionPerformed(ActionEvent ae) {
String command = ae.getActionCommand().intern();
if (logger.isLoggable(Level.FINE)) {
logger.fine("Received command: " + command);
}
if (UndoCmd.equalsIgnoreCase(command) && undoStack != null && !undoStack.empty()) {
undo();
} else if (RedoCmd.equalsIgnoreCase(command) && redoStack != null && !redoStack.empty()) {
redo();
} else {
clearStacks((ClearUndoCmd.equalsIgnoreCase(command) || ClearCmd.equalsIgnoreCase(command)),
(ClearRedoCmd.equalsIgnoreCase(command) || ClearCmd.equalsIgnoreCase(command)));
}
}
/**
* Put a new UndoEvent on the backStack, to remember for later in case we
* need to back up.
*
* @param event UndoEvent.
*/
protected synchronized void rememberLastThing(UndoEvent event) {
if (undoStack.size() >= stackSize) {
undoStack.removeElementAt(0);
}
if (logger.isLoggable(Level.FINE)) {
logger.fine("remembering (" + event.getDescription() + ")");
}
undoStack.push(event);
}
/**
* Take a UndoEvent off the backStack, and push it on the forward stack, and
* invoke the new currentState so the source component gets modified.
*/
protected synchronized void undo() {
if (currentState != null) {
while (redoStack.size() >= stackSize) {
redoStack.removeElementAt(0);
logger.info("making room for " + currentState.getDescription());
}
redoStack.push(currentState);
if (logger.isLoggable(Level.FINE)) {
logger.fine("making last current state (" + currentState.getDescription() + ") on redo stack");
}
}
currentState = undoStack.pop();
if (logger.isLoggable(Level.FINE)) {
logger.fine("making top undo state (" + currentState.getDescription() + ") current state");
}
if (currentState != null) {
if (logger.isLoggable(Level.FINE)) {
logger.fine("calling setState on " + currentState.getDescription());
}
currentState.setState();
}
fireStackStatus();
}
/**
* Take a UndoEvent off the forwardStack, and push it on the backStack.
*/
protected synchronized void redo() {
if (currentState != null) {
while (undoStack.size() >= stackSize) {
undoStack.removeElementAt(0);
}
undoStack.push(currentState);
}
currentState = redoStack.pop();
if (currentState != null) {
currentState.setState();
}
fireStackStatus();
}
/**
* Clear out the chosen undo stacks and fire an event to update the triggers
* on stack status. Also sets the current state, which isn't held by either
* stack, to null.
*
* @param clearUndoStack clear out the undo stack.
* @param clearRedoStack clear out the redo stack.
*/
public synchronized void clearStacks(boolean clearUndoStack, boolean clearRedoStack) {
if (clearUndoStack && undoStack != null) {
undoStack.clear();
}
if (clearRedoStack && redoStack != null) {
redoStack.clear();
}
currentState = null;
fireStackStatus();
}
public void fireStackStatus() {
if (triggers != null) {
UndoEvent undoEvent = getWhatWillHappenNextFromStack(undoStack);
UndoEvent redoEvent = getWhatWillHappenNextFromStack(redoStack);
if (logger.isLoggable(Level.FINE)) {
logger.fine("back enabled: " + (undoEvent != null) + ", forward enabled: " + (redoEvent != null));
}
triggers.fireStackStatus(undoEvent, redoEvent);
}
}
protected UndoEvent getWhatWillHappenNextFromStack(Stack<UndoEvent> stack) {
UndoEvent nextThing = null;
try {
nextThing = stack.peek();
} catch (EmptyStackException ese) {
// Noop, event will stay null, that's OK and acceptable.
}
return nextThing;
}
/**
* UndoStackTriggers should call this method to add themselves for stack
* notifications, and all will be well.
*/
public void addUndoStackTrigger(UndoStackTrigger trigger) {
trigger.addActionListener(this);
UndoEvent undoEvent = getWhatWillHappenNextFromStack(undoStack);
UndoEvent redoEvent = getWhatWillHappenNextFromStack(redoStack);
triggers.add(trigger);
trigger.updateUndoStackStatus(undoEvent, redoEvent);
}
/**
* UndoStackTriggers should call this method to remove themselves from stack
* notifications, and all will be well.
*/
public void removeUndoStackTrigger(UndoStackTrigger trigger) {
trigger.removeActionListener(this);
if (triggers != null) {
triggers.remove(trigger);
}
}
}