/*******************************************************************************
* Copyright (c) 2000, 2008 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.gef.commands;
import java.util.ArrayList;
import java.util.EventObject;
import java.util.List;
import java.util.Stack;
/**
* An implementation of a command stack. A stack manages the executing, undoing, and
* redoing of {@link Command Commands}. Executed commands are pushed onto a a stack for
* undoing later. Commands which are undone are pushed onto a redo stack. Whenever a new
* command is executed, the Redo stack is flushed.
* <P>
* A CommandStack contains a dirty property. This property can be used to determine
* when persisting changes is required. The stack is dirty whenever the last executed or
* redone command is different than the command that was at the top of the undo stack when
* {@link #markSaveLocation()} was last called. Initially, the undo stack is
* empty, and not dirty.
*
* @author hudsonr
*/
public class CommandStack {
/**
* Constant indicating notification after a command has been executed (value is 8).
*/
public static final int POST_EXECUTE = 8;
/**
* Constant indicating notification after a command has been redone (value is 16).
*/
public static final int POST_REDO = 16;
/**
* Constant indicating notification after a command has been undone (value is 32).
*/
public static final int POST_UNDO = 32;
/**
* A bit-mask indicating notification after a command has done something. Currently this
* includes after a command has been undone, redone, or executed. This will include new
* events should they be introduced in the future.
* <P>Usage<BR/>
* <PRE>if ((commandStackEvent.getDetail() & CommandStack.POST_MASK) != 0) {
* //Do something, like:
* stopBatchingChanges();
* }</PRE>
*/
public static final int POST_MASK = new Integer(POST_EXECUTE | POST_UNDO | POST_REDO).intValue();
/**
* Constant indicating notification prior to executing a command (value is 1).
*/
public static final int PRE_EXECUTE = 1;
/**
* Constant indicating notification prior to redoing a command (value is 2).
*/
public static final int PRE_REDO = 2;
/**
* Constant indicating notification prior to undoing a command (value is 4).
*/
public static final int PRE_UNDO = 4;
/**
* A bit-mask indicating notification before a command makes a change. Currently this
* includes before a command has been undone, redone, or executed. This will include new
* events should they be introduced in the future.
* <P>Usage<BR/>
* <PRE>if ((commandStackEvent.getDetail() & CommandStack.PRE_MASK) != 0) {
* //Do something, like:
* startBatchingChanges();
* }</PRE>
*/
static final int PRE_MASK = new Integer(PRE_EXECUTE | PRE_UNDO | PRE_REDO).intValue();
private List eventListeners = new ArrayList();
/**
* The list of {@link CommandStackListener}s.
* @deprecated This field should not be referenced, use {@link #notifyListeners()}
*/
protected List listeners = new ArrayList();
private Stack redoable = new Stack();
private int saveLocation = 0;
private Stack undoable = new Stack();
private int undoLimit = 0;
/**
* Constructs a new command stack. By default, there is no undo limit, and isDirty() will
* return <code>false</code>.
*/
public CommandStack() { }
/**
* Appends the listener to the list of command stack listeners. Multiple adds result in
* multiple notifications.
* @since 3.1
* @param listener the event listener
*/
public void addCommandStackEventListener(CommandStackEventListener listener) {
eventListeners.add(listener);
}
/**
* Appends the listener to the list of command stack listeners. Multiple adds will result
* in multiple notifications.
* @param listener the listener
*/
public void addCommandStackListener(CommandStackListener listener) {
listeners.add(listener);
}
/**
* @return <code>true</code> if it is appropriate to call {@link #redo()}.
*/
public boolean canRedo() {
return !redoable.isEmpty();
}
/**
* @return <code>true</code> if {@link #undo()} can be called
*/
public boolean canUndo() {
if (undoable.size() == 0)
return false;
return ((Command)undoable.lastElement()).canUndo();
}
/**
* This will <code>dispose()</code> all the commands in both the undo and redo stack. Both
* stacks will be empty afterwards.
*/
public void dispose() {
flushUndo();
flushRedo();
}
/**
* Executes the specified Command if possible. Prior to executing the command, a
* CommandStackEvent for {@link #PRE_EXECUTE} will be fired to event listeners.
* Similarly, after attempting to execute the command, an event for {@link #POST_EXECUTE}
* will be fired. If the execution of the command completely normally, stack listeners
* will receive {@link CommandStackListener#commandStackChanged(EventObject) stackChanged}
* notification.
* <P>
* If the command is <code>null</code> or cannot be executed, nothing happens.
* @param command the Command to execute
* @see CommandStackEventListener
*/
public void execute(Command command) {
if (command == null || !command.canExecute())
return;
flushRedo();
notifyListeners(command, PRE_EXECUTE);
try {
command.execute();
if (getUndoLimit() > 0) {
while (undoable.size() >= getUndoLimit()) {
((Command)undoable.remove(0)).dispose();
if (saveLocation > -1)
saveLocation--;
}
}
if (saveLocation > undoable.size())
saveLocation = -1; //The save point was somewhere in the redo stack
undoable.push(command);
notifyListeners();
} finally {
notifyListeners(command, POST_EXECUTE);
}
}
/**
* Flushes the entire stack and resets the save location to zero. This method might be
* called when performing "revert to saved".
*/
public void flush() {
flushRedo();
flushUndo();
saveLocation = 0;
notifyListeners();
}
private void flushRedo() {
while (!redoable.isEmpty())
((Command)redoable.pop()).dispose();
}
private void flushUndo() {
while (!undoable.isEmpty())
((Command)undoable.pop()).dispose();
}
/**
* @return an array containing all commands in the order they were executed
*/
public Object[] getCommands() {
List commands = new ArrayList(undoable);
for (int i = redoable.size() - 1; i >= 0; i--) {
commands.add(redoable.get(i));
}
return commands.toArray();
}
/**
* Peeks at the top of the <i>redo</i> stack. This is useful for describing to the User
* what will be redone. The returned <code>Command</code> has a label describing it.
* @return the top of the <i>redo</i> stack, which may be <code>null</code>
*/
public Command getRedoCommand() {
return redoable.isEmpty() ? null : (Command)redoable.peek();
}
/**
* Peeks at the top of the <i>undo</i> stack. This is useful for describing to the User
* what will be undone. The returned <code>Command</code> has a label describing it.
* @return the top of the <i>undo</i> stack, which may be <code>null</code>
*/
public Command getUndoCommand() {
return undoable.isEmpty() ? null : (Command)undoable.peek();
}
/**
* Returns the undo limit. The undo limit is the maximum number of atomic operations that
* the User can undo. <code>-1</code> is used to indicate no limit.
* @return the undo limit
*/
public int getUndoLimit() {
return undoLimit;
}
/**
* Returns true if the stack is dirty. The stack is dirty whenever the last executed or
* redone command is different than the command that was at the top of the undo stack when
* {@link #markSaveLocation()} was last called.
* @return <code>true</code> if the stack is dirty
*/
public boolean isDirty() {
return undoable.size() != saveLocation;
}
/**
* Marks the last executed or redone Command as the point at which the changes were saved.
* Calculation of {@link #isDirty()} will be based on this checkpoint.
*/
public void markSaveLocation() {
saveLocation = undoable.size();
notifyListeners();
}
/**
* Sends notification to all {@link CommandStackListener}s.
* @deprecated
*/
protected void notifyListeners() {
EventObject event = new EventObject(this);
for (int i = 0; i < listeners.size(); i++)
((CommandStackListener)listeners.get(i))
.commandStackChanged(event);
}
/**
* Notifies command stack event listeners that the command stack has changed to the
* specified state.
*
* @param command the command
* @param state the current stack state
* @since 3.2
*/
protected void notifyListeners(Command command, int state) {
CommandStackEvent event = new CommandStackEvent(this, command, state);
for (int i = 0; i < eventListeners.size(); i++)
((CommandStackEventListener)eventListeners.get(i)).stackChanged(event);
}
/**
* Calls redo on the Command at the top of the <i>redo</i> stack, and pushes that Command
* onto the <i>undo</i> stack. This method should only be called when {@link #canUndo()}
* returns <code>true</code>.
*/
public void redo() {
//Assert.isTrue(canRedo())
if (!canRedo())
return;
Command command = (Command)redoable.pop();
notifyListeners(command, PRE_REDO);
try {
command.redo();
undoable.push(command);
notifyListeners();
} finally {
notifyListeners(command, POST_REDO);
}
}
/**
* Removes the first occurrence of the specified listener.
* @param listener the listener
*/
public void removeCommandStackEventListener(CommandStackEventListener listener) {
eventListeners.remove(listener);
}
/**
* Removes the first occurrence of the specified listener.
* @param listener the listener
*/
public void removeCommandStackListener(CommandStackListener listener) {
listeners.remove(listener);
}
/**
* Sets the undo limit. The undo limit is the maximum number of atomic operations that the
* User can undo. <code>-1</code> is used to indicate no limit.
* @param undoLimit the undo limit
*/
public void setUndoLimit(int undoLimit) {
this.undoLimit = undoLimit;
}
/**
* Undoes the most recently executed (or redone) Command. The Command is popped from the
* undo stack to and pushed onto the redo stack. This method should only be called when
* {@link #canUndo()} returns <code>true</code>.
*/
public void undo() {
//Assert.isTrue(canUndo());
Command command = (Command)undoable.pop();
notifyListeners(command, PRE_UNDO);
try {
command.undo();
redoable.push(command);
notifyListeners();
} finally {
notifyListeners(command, POST_UNDO);
}
}
}