/*
* Copyright (c) 2008, SQL Power Group Inc.
*
* This file is part of SQL Power Library.
*
* SQL Power Library 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 3 of the License, or
* (at your option) any later version.
*
* SQL Power Library 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 ca.sqlpower.object.undo;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.CompoundEdit;
import javax.swing.undo.UndoManager;
import javax.swing.undo.UndoableEdit;
import org.apache.log4j.Logger;
import ca.sqlpower.object.AbstractSPListener;
import ca.sqlpower.object.SPChildEvent;
import ca.sqlpower.object.SPListener;
import ca.sqlpower.object.SPObject;
import ca.sqlpower.sqlobject.SQLDatabase;
import ca.sqlpower.util.SQLPowerUtils;
import ca.sqlpower.util.TransactionEvent;
/**
*
*
*/
public class SPObjectUndoManager extends UndoManager implements NotifyingUndoManager {
private static final Logger logger = Logger.getLogger(SPObjectUndoManager.class);
/**
* Converts received SQLObjectEvents into UndoableEdits, PropertyChangeEvents
* into specific edits and adds them to an UndoManager.
*/
public class SPObjectUndoableEventAdapter implements SPListener, PropertyChangeListener {
private final class CompEdit extends CompoundEdit {
String toolTip;
public CompEdit(String toolTip) {
super();
this.toolTip = toolTip;
}
@Override
public String getPresentationName() {
return toolTip;
}
@Override
public String getUndoPresentationName() {
return "Undo " + getPresentationName();
}
@Override
public String getRedoPresentationName() {
return "Redo " + getPresentationName();
}
@Override
public boolean canUndo() {
return super.canUndo() && edits.size() > 0;
}
@Override
public boolean canRedo() {
return super.canRedo() && edits.size() > 0;
}
@Override
public String toString() {
StringBuffer sb = new StringBuffer();
for (Object o : edits) {
sb.append(o).append("\n");
}
return sb.toString();
}
@Override
public void undo() throws CannotUndoException {
List<SPObject> ancestorList = SQLPowerUtils.getAncestorList(spObjectRoot);
SPObject absoluteRoot;
if (ancestorList.isEmpty()) {
absoluteRoot = spObjectRoot;
} else {
absoluteRoot = ancestorList.get(0);
}
try {
absoluteRoot.begin("Undoing compound edit " + getUndoPresentationName());
super.undo();
absoluteRoot.commit();
} catch (RuntimeException e) {
absoluteRoot.rollback(e.getMessage());
throw e;
}
}
@Override
public void redo() throws CannotRedoException {
List<SPObject> ancestorList = SQLPowerUtils.getAncestorList(spObjectRoot);
SPObject absoluteRoot;
if (ancestorList.isEmpty()) {
absoluteRoot = spObjectRoot;
} else {
absoluteRoot = ancestorList.get(0);
}
try {
absoluteRoot.begin("Redoing compound edit " + getRedoPresentationName());
super.redo();
absoluteRoot.commit();
} catch (RuntimeException e) {
absoluteRoot.rollback(e.getMessage());
throw e;
}
}
}
/**
* This listener needs to be attached to all of the ancestors of the
* root node this outer class listener listens to. This listener will
* listen for compound event start and finish points and update the
* transaction count. This ensures if an ancestor is in a transaction
* and updates the nodes we are interested in undoing the events are
* collected correctly.
*/
private final SPListener ancestorListener = new AbstractSPListener() {
@Override
public void transactionStarted(TransactionEvent e) {
compoundGroupStart(e.getMessage());
}
@Override
public void transactionEnded(TransactionEvent e) {
compoundGroupEnd();
}
};
private CompoundEdit ce;
private int compoundEditStackCount;
/**
* Tracks if the listener should add itself to new children on the object it is listening to.
*/
protected boolean addListenerToChildren = true;
/**
* If we are in a compound edit the removed objects will be stored here
* so this listener can be removed from the objects when the transaction
* completes.
*/
private final List<SPObject> removedObjects = new ArrayList<SPObject>();
/**
* {@link #attachToObject(SPObject)} must be called after this
* constructor has been called to ensure the ancestors are correctly
* listened to.
*/
public SPObjectUndoableEventAdapter() {
ce = null;
compoundEditStackCount = 0;
}
/**
* {@link #attachToObject(SPObject)} must be called after this
* constructor has been called to ensure the ancestors are correctly
* listened to.
*/
public SPObjectUndoableEventAdapter(boolean addListenerToChildren) {
this();
this.addListenerToChildren = addListenerToChildren;
}
/**
* Attaches the listener to listen for transactions in the ancestor.
*
* @param root
* The ancestors of this root will be listened to along with
* the root itself. This ensures the transactions will be
* taken into account correctly. In order for the ancestors
* to be listened to correctly this object must be connected
* to the SPObject tree already. The root is the object this
* listener was just added to. Because the undo manager's lives
* for the life of the object tree this listener does not need
* to be removed to ensure memory cleanup.
*/
public void attachToObject(SPObject root) {
for (SPObject ancestor : SQLPowerUtils.getAncestorList(root)) {
ancestor.addSPListener(ancestorListener);
}
}
/**
* You should not undo when in a compound edit
*
* @return
*/
public boolean canUndoOrRedo() {
if (ce == null) {
return true;
} else {
return false;
}
}
/**
* Begins a compound edit. Compound edits can be nested, so every call
* to this method has to be balanced with a call to
* {@link #compoundGroupEnd()}.
*
* fires a state changed event when a new compound edit is created
*/
private void compoundGroupStart(String toolTip) {
if (SPObjectUndoManager.this.isUndoOrRedoing())
return;
compoundEditStackCount++;
if (compoundEditStackCount == 1) {
ce = new CompEdit(toolTip);
fireStateChanged();
}
if (logger.isDebugEnabled()) {
logger.debug("compoundGroupStart: edit stack =" + compoundEditStackCount);
}
}
/**
* Ends a compound edit. Compound edits can be nested, so every call to
* this method has to be preceeded by a call to
* {@link #compoundGroupStart()}.
*
* @throws IllegalStateException
* if there wasn't already a compound edit in progress.
*/
private void compoundGroupEnd() {
if (SPObjectUndoManager.this.isUndoOrRedoing())
return;
if (compoundEditStackCount <= 0) {
throw new IllegalStateException("No compound edit in progress");
}
compoundEditStackCount--;
if (compoundEditStackCount == 0)
returnToEditState();
if (logger.isDebugEnabled()) {
logger.debug("compoundGroupEnd: edit stack =" + compoundEditStackCount + " ce=" + ce);
}
}
private void addEdit(UndoableEdit undoEdit) {
if (logger.isDebugEnabled()) {
logger.debug("Adding new edit: " + undoEdit);
}
// if we are not in a compound edit
if (compoundEditStackCount == 0) {
if (!loading) {
SPObjectUndoManager.this.addEdit(undoEdit);
}
} else {
ce.addEdit(undoEdit);
}
}
public void childAdded(SPChildEvent e) {
if (SPObjectUndoManager.this.isUndoOrRedoing())
return;
addEdit(new SPObjectChildEdit(e));
if (addListenerToChildren) {
SQLPowerUtils.listenToHierarchy(e.getChild(), this);
removedObjects.remove(e.getChild());
}
}
public void childRemoved(SPChildEvent e) {
if (SPObjectUndoManager.this.isUndoOrRedoing())
return;
if (addListenerToChildren) {
if (compoundEditStackCount == 0) {
SQLPowerUtils.unlistenToHierarchy(e.getChild(), this);
} else {
removedObjects.add(e.getChild());
}
}
addEdit(new SPObjectChildEdit(e));
}
/**
* XXX This should take an event of a type specific to SPObjects.
*/
public void propertyChanged(PropertyChangeEvent e) {
if (SPObjectUndoManager.this.isUndoOrRedoing())
return;
if (!loading && e.getPropertyName().equals("UUID")) {
throw new IllegalStateException("Cannot undo UUID changes. " +
"This event can only occur during loading. Event " + e);
}
if (e.getSource() instanceof SQLDatabase &&
e.getPropertyName().equals("shortDisplayName")) {
// this is not undoable at this time.
} else {
SPObjectPropertyChangeUndoableEdit undoEvent =
new SPObjectPropertyChangeUndoableEdit(e);
addEdit(undoEvent);
}
}
/**
* Packs property change event into PropertyChangeEdit and then adds
* to the undo manager.
*/
public void propertyChange(PropertyChangeEvent evt) {
if (SPObjectUndoManager.this.isUndoOrRedoing()) {
return;
}
if (!loading && evt.getPropertyName().equals("UUID")) {
throw new IllegalStateException("Cannot undo UUID changes. " +
"This event can only occur during loading. Event " + evt);
}
PropertyChangeEdit edit = new PropertyChangeEdit(evt);
addEdit(edit);
}
/**
* Return to a single edit state from a compound edit state
*/
private void returnToEditState() {
if (compoundEditStackCount != 0) {
throw new IllegalStateException("The compound edit stack (" + compoundEditStackCount + ") should be 0");
}
if (ce != null) {
for (SPObject removedChild : removedObjects) {
removedChild.removeSPListener(this);
}
removedObjects.clear();
ce.end();
if (ce.canUndo() && !loading) {
if (logger.isDebugEnabled())
logger.debug("Adding compound edit " + ce + " to undo manager");
SPObjectUndoManager.this.addEdit(ce);
} else {
if (logger.isDebugEnabled())
logger.debug("Compound edit " + ce + " is not undoable so we are not adding it");
}
ce = null;
}
fireStateChanged();
logger.debug("Returning to regular state");
}
public void transactionStarted(TransactionEvent e) {
compoundGroupStart(e.getMessage());
}
public void transactionEnded(TransactionEvent e) {
compoundGroupEnd();
}
public void transactionRollback(TransactionEvent e) {
// TODO figure out what this should do. As of writing, nothing
// would cause a rollback, but this will probably change.
}
}
protected final SPObjectUndoableEventAdapter eventAdapter = new SPObjectUndoableEventAdapter();
private boolean undoing;
private boolean redoing;
/**
* The undo manager behaves slightly differently when a project is being
* loaded. Edits will not be created while the undo manager is loading a
* project because they do not represent the true undos of the project.
* Additionally, UUID changes will be allowed, which are not allowed at
* other times.
*/
private boolean loading = false;
private List<ChangeListener> changeListeners = new ArrayList<ChangeListener>();
/**
* The root object that this undo manager is listening to. More objects
* may be listened to by undo managers that extend this class so this
* may not be the only 'root'.
*/
private final SPObject spObjectRoot;
public SPObjectUndoManager(SPObject objectRoot) {
this.spObjectRoot = objectRoot;
init(spObjectRoot);
}
private final void init(SPObject objectRoot) {
SQLPowerUtils.listenToHierarchy(objectRoot, eventAdapter);
eventAdapter.attachToObject(objectRoot);
}
/**
* Adds then given edit to this undo manager.
*
* <p>
* Warning: Edits added here do not respect compounding. You can add a whole
* CompoundEdit here, but if the current state of the undo manager is that
* it's in a compound edit, it doesn't matter. You will get individual edits
* when you add individual edits.
*/
public synchronized boolean addEdit(UndoableEdit anEdit) {
if (!(isUndoing() || isRedoing())) {
if (logger.isDebugEnabled())
logger.debug("Added new undoableEdit to undo manager " + anEdit);
boolean success = super.addEdit(anEdit);
fireStateChanged();
return success;
}
// processing an edit so we pretend to absorb this edit
return true;
}
/**
* Calls super.undo() then refreshes the undo/redo actions.
*/
@Override
public synchronized void undo() throws CannotUndoException {
if (logger.isDebugEnabled()) {
logger.debug("Undoing");
}
undoing = true;
super.undo();
fireStateChanged();
undoing = false;
}
/**
* Calls super.redo() then refreshes the undo/redo actions.
*/
@Override
public synchronized void redo() throws CannotRedoException {
redoing = true;
super.redo();
fireStateChanged();
redoing = false;
}
@Override
public synchronized boolean canUndo() {
return super.canUndo() && eventAdapter.canUndoOrRedo();
}
@Override
public synchronized boolean canRedo() {
return super.canRedo() && eventAdapter.canUndoOrRedo();
}
/* Public getters and setters appear after this point */
public int getUndoableEditCount() {
if (editToBeUndone() == null)
return 0;
int count;
// edits is a 0 based vector
count = this.edits.indexOf(this.editToBeUndone()) + 1;
return count;
}
public int getRedoableEditCount() {
if (editToBeRedone() == null)
return 0;
int count;
count = edits.size() - this.edits.indexOf(this.editToBeRedone());
return count;
}
public boolean isRedoing() {
return redoing;
}
public boolean isUndoing() {
return undoing;
}
public boolean isUndoOrRedoing() {
return undoing || redoing;
}
/**
* Returns the event adapter for SPObjects and compound events. This
* is an implementation detail specific to undo/redo on the relational
* play pen, and it will be going away soon.
*/
public SPObjectUndoableEventAdapter getEventAdapter() {
return eventAdapter;
}
// Change event support
/* (non-Javadoc)
* @see ca.sqlpower.architect.undo.NotifyingUndoManager#addChangeListener(javax.swing.event.ChangeListener)
*/
public void addChangeListener(ChangeListener l) {
changeListeners.add(l);
}
/* (non-Javadoc)
* @see ca.sqlpower.architect.undo.NotifyingUndoManager#removeChangeListener(javax.swing.event.ChangeListener)
*/
public void removeChangeListener(ChangeListener l) {
changeListeners.remove(l);
}
/**
* Notifies listeners that the undo/redo list might have changed.
*/
public void fireStateChanged() {
ChangeEvent event = new ChangeEvent(this);
for (ChangeListener l : changeListeners) {
l.stateChanged(event);
}
}
public String printUndoVector() {
StringBuffer sb = new StringBuffer();
for (Object o : edits) {
sb.append(o).append("\n");
}
return sb.toString();
}
public void setLoading(boolean loading) {
this.loading = loading;
}
}