package ecologylab.appframework; import ecologylab.appframework.types.prefs.Pref; import ecologylab.appframework.types.prefs.PrefInt; import ecologylab.collections.SyncLinkedList; import ecologylab.generic.Debug; import ecologylab.oodss.logging.MixedInitiativeOp; /** * Undo and Redo Mixed Initiative Operations. * @author andruid, alexgrau */ public class UndoRedo<O extends MixedInitiativeOp> extends Debug{ /** * Synced Linked List of Undo Operations. */ protected final SyncLinkedList undoStack = new SyncLinkedList(); /** * Syned Linked List of Redo Operations */ protected final SyncLinkedList redoStack = new SyncLinkedList(); /** * Semaphor Object Used For Undo and Redo */ protected final Object undoRedoSemaphore = new Object(); /** * Sets the Int Prefs for the number of Undo/Redo Levels */ protected final PrefInt undoLevels = Pref.usePrefInt(UNDO_LEVELS, DEFAULT_UNDO_LEVELS); /** * Number of possible Undo/Redo levels */ protected static final int DEFAULT_UNDO_LEVELS = 32; public static final String UNDO_LEVELS = "undo_levels"; protected int humanUndoCount; protected int humanRedoCount; /** * Construct an Undo/Redo Object */ public UndoRedo(){ super(); } /** * Undo an Operation. Returns the Operations. * @return */ public MixedInitiativeOp undo(){ synchronized (undoRedoSemaphore){ MixedInitiativeOp op = popUndo(); if(op == null) return null; op.performAction(true); pushRedo(op); return op; } } /** * Redo an Operation. Returns the Operation. * @return */ public MixedInitiativeOp redo(){ synchronized (undoRedoSemaphore){ MixedInitiativeOp op = popRedo(); if (op == null) return null; op.performAction(false); pushUndo(op); return op; } } /** * Pushes an Operation onto the undo stack. * Clears the redo stack because a new operation has taken place. * @param op */ public MixedInitiativeOp pushOpToUndo(MixedInitiativeOp op){ pushUndo(op); clearRedo(); return op; } /** * Pushes an Operation onto the Undo stack. * If too many operations, removes oldest and adds op. * @param op */ public void pushUndo(MixedInitiativeOp op){ synchronized (undoStack){ while (undoStack.size() >= undoLevels.value()){ MixedInitiativeOp oldOp = (MixedInitiativeOp) undoStack.removeFirst(); if (oldOp.isHuman()) humanUndoCount--; oldOp.recycle(false); } undoStack.addLast(op); humanUndoCount++; } } /** * Pops an Operation from the Undo stack. * If empty, returns null. * @return */ public MixedInitiativeOp popUndo(){ synchronized (undoStack){ return undoStack.isEmpty() ? null : (MixedInitiativeOp) undoStack.removeLast(); } } /** * Peak an Operation from the Undo stack. * If empty, returns null. * @return */ public MixedInitiativeOp peekUndo(){ synchronized (undoStack){ return undoStack.isEmpty() ? null : (MixedInitiativeOp) undoStack.getLast(); } } /** * Pushes an Operation onto the Redo stack. * @param op */ private void pushRedo(MixedInitiativeOp op){ synchronized (redoStack){ redoStack.addLast(op); } } /** * Pops an Operation from the Redo stack. * If empty, then returns null. * @return */ private MixedInitiativeOp popRedo(){ synchronized (redoStack){ return redoStack.isEmpty() ? null : (MixedInitiativeOp) redoStack.removeLast(); } } /** * Clears both the undo and redo stacks. */ public synchronized void clear(){ synchronized (undoStack){ while (!undoStack.isEmpty()){ MixedInitiativeOp op = popUndo(); op.recycle(true); } } synchronized (redoStack){ while (!redoStack.isEmpty()){ MixedInitiativeOp op = popRedo(); op.recycle(true); } } } /** * Clears and recycles the redo stack after a new operation has been done. */ protected void clearRedo(){ synchronized (redoStack){ while (!redoStack.isEmpty()){ MixedInitiativeOp op = popRedo(); op.recycle(true); } } } }