package org.obo.app.controller; import java.awt.event.ActionEvent; import java.util.ArrayList; import java.util.Collections; import java.util.List; import javax.swing.AbstractAction; import javax.swing.Action; import javax.swing.undo.AbstractUndoableEdit; import javax.swing.undo.CannotRedoException; import javax.swing.undo.CannotUndoException; import javax.swing.undo.UndoManager; import javax.swing.undo.UndoableEdit; import javax.swing.undo.UndoableEditSupport; import org.apache.log4j.Logger; import org.phenoscape.model.DataSet; import org.phenoscape.model.MatrixCellValue; /** * A wrapper over javax.swing.undo.UndoableEditSupport and * javax.swing.undo.UndoManager which provides a unified interface for managing * undo and redo for an application. This class provides undo and redo actions * which can be used in menu items and also provides support for tracking * whether there are unsaved edits. * * @author Jim Balhoff */ public class UndoController { private final UndoableEditSupport undoSupport = new UndoableEditSupport(); private final UndoManager undoManager = new UndoManager(); private final Action undo; private final Action redo; private int dirtyStack = 0; private boolean undoCleans = true; private List<UnsavedChangesListener> unsavedChangesListeners = new ArrayList<UnsavedChangesListener>(); private int ignoreStack = 0; private CoalescedUndoableEdit coalescedEdit = null; public UndoController() { this.undoSupport.addUndoableEditListener(this.undoManager); this.undo = new AbstractAction("Undo") { @Override public void actionPerformed(ActionEvent e) { undoManager.undo(); updateUndoRedoActions(); undid(); } }; this.redo = new AbstractAction("Redo") { @Override public void actionPerformed(ActionEvent e) { undoManager.redo(); updateUndoRedoActions(); redid(); } }; this.updateUndoRedoActions(); } public Action getUndoAction() { return this.undo; } public Action getRedoAction() { return this.redo; } public void discardAllEdits() { this.undoManager.discardAllEdits(); this.updateUndoRedoActions(); this.dirtyStack = 0; } public void markChangesSaved() { this.dirtyStack = 0; this.fireUnsavedChangesChanged(); } public boolean hasUnsavedChanges() { return this.dirtyStack != 0; } public void postEdit(UndoableEdit e) { if (!this.isIgnoringEdits()) { if (this.coalescedEdit != null && this.coalescedEdit != e) { this.coalescedEdit.pushEdit(e); } else { this.undoSupport.postEdit(e); this.updateUndoRedoActions(); this.edited(); } } } public void beginIgnoringEdits() { this.ignoreStack++; } public void endIgnoringEdits() { this.ignoreStack--; } private boolean isIgnoringEdits() { return this.ignoreStack != 0; } public void beginCoalescingEdits(String operationName) { this.coalescedEdit = new CoalescedUndoableEdit(operationName); this.postEdit(coalescedEdit); } public void endCoalescingEdits() { this.coalescedEdit = null; } private void updateUndoRedoActions() { this.undo.setEnabled(this.undoManager.canUndo()); this.undo.putValue(Action.NAME, this.undoManager.getUndoPresentationName()); this.redo.setEnabled(this.undoManager.canRedo()); this.redo.putValue(Action.NAME, this.undoManager.getRedoPresentationName()); } private void undid() { if (this.hasUnsavedChanges()) { if (this.undoCleans) { this.dirtyStack -= 1; } else { this.dirtyStack += 1; } } else { this.undoCleans = false; this.dirtyStack += 1; } this.fireUnsavedChangesChanged(); } private void redid() { if (this.hasUnsavedChanges()) { if (this.undoCleans) { this.dirtyStack += 1; } else { this.dirtyStack -= 1; } } else { this.undoCleans = true; this.dirtyStack += 1; } this.fireUnsavedChangesChanged(); } private void edited() { if (this.hasUnsavedChanges()) { if (this.undoCleans) { this.dirtyStack += 1; } else { // this should prevent dirtyStack from ever reaching 0 this.dirtyStack = 1; } } else { this.undoCleans = true; this.dirtyStack += 1; } this.fireUnsavedChangesChanged(); } public interface UnsavedChangesListener { public void setUnsavedChanges(boolean unsaved); } public void addUnsavedChangesListener(UnsavedChangesListener listener) { this.unsavedChangesListeners.add(listener); } public void removeUnsavedChangesListener(UnsavedChangesListener listener) { this.unsavedChangesListeners.remove(listener); } private void fireUnsavedChangesChanged() { for (UnsavedChangesListener listener : this.unsavedChangesListeners) { listener.setUnsavedChanges(this.hasUnsavedChanges()); } } private class CoalescedUndoableEdit extends AbstractUndoableEdit { final String presentationName; final List<UndoableEdit> edits = new ArrayList<UndoableEdit>(); public CoalescedUndoableEdit(String presentationName) { this.presentationName = presentationName; } public void pushEdit(UndoableEdit e) { this.edits.add(e); } @Override public String getPresentationName() { return this.presentationName; } @Override public void redo() throws CannotRedoException { log().debug("Reddoing operations: " + this.edits.size()); for (UndoableEdit edit : this.edits) { edit.redo(); } super.redo(); } @Override public void undo() throws CannotUndoException { log().debug("Undoing operations: " + this.edits.size()); super.undo(); final List<UndoableEdit> reversed = new ArrayList<UndoableEdit>(this.edits); Collections.reverse(reversed); for (UndoableEdit edit : reversed) { edit.undo(); } } } @SuppressWarnings("unused") private Logger log() { return Logger.getLogger(this.getClass()); } }