xy.reflect.ui.undo.ModificationStack Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (C) 2018 OTK Software
*
* This program 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.
*
* This program 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.
*
* The GNU General Public License allows you also to freely redistribute
* the libraries under the same license, if you provide the terms of the
* GNU General Public License with them and add the following
* copyright notice at the appropriate place (with a link to
* http://javacollection.net/reflectionui/ web site when possible).
******************************************************************************/
package xy.reflect.ui.undo;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Stack;
import xy.reflect.ui.util.Accessor;
import xy.reflect.ui.util.ReflectionUIError;
/**
* This is an undo management class. it allows to undo/redo actions performed
* using instances of {@link IModification}.
*
* Objects should be exclusively modified through the same modification stack.
* If a modification occurs but cannot be logged in the modification stack for
* any reason, then {@link #invalidate()} should be called to inform the
* modification stack.
*
* @author olitank
*
*/
public class ModificationStack {
protected Stack undoStack = new Stack();
protected Stack redoStack = new Stack();
protected String name;
protected Stack compositeStack = new Stack();
protected List listeners = new ArrayList();
protected boolean invalidated = false;
protected boolean wasInvalidated = false;
protected long stateVersion = 0;
protected IModificationListener internalListener = new IModificationListener() {
@Override
public void handleUdno(IModification undoModification) {
stateVersion--;
}
@Override
public void handleRedo(IModification modification) {
stateVersion++;
}
@Override
public void handlePush(IModification undoModification) {
stateVersion++;
}
@Override
public void handleInvalidationCleared() {
}
@Override
public void handleInvalidate() {
stateVersion++;
}
};
protected IModificationListener allListenersProxy = new IModificationListener() {
@Override
public void handlePush(IModification undoModification) {
internalListener.handlePush(undoModification);
for (IModificationListener listener : new ArrayList(
ModificationStack.this.listeners)) {
listener.handlePush(undoModification);
}
}
@Override
public void handleUdno(IModification undoModification) {
internalListener.handleUdno(undoModification);
for (IModificationListener listener : new ArrayList(
ModificationStack.this.listeners)) {
listener.handleUdno(undoModification);
}
}
@Override
public void handleRedo(IModification modification) {
internalListener.handleRedo(modification);
for (IModificationListener listener : new ArrayList(
ModificationStack.this.listeners)) {
listener.handleRedo(modification);
}
}
@Override
public void handleInvalidate() {
internalListener.handleInvalidate();
for (IModificationListener listener : new ArrayList(
ModificationStack.this.listeners)) {
listener.handleInvalidate();
}
}
@Override
public void handleInvalidationCleared() {
internalListener.handleInvalidationCleared();
for (IModificationListener listener : new ArrayList(
ModificationStack.this.listeners)) {
listener.handleInvalidationCleared();
}
}
};
/**
* Constructs a modification stack having the specified name.
*
* @param name
* The name.
*/
public ModificationStack(String name) {
this.name = name;
}
/**
* @return the name of this modification stack.
*/
public String getName() {
return name;
}
/**
* @return whether this modification stack is currently invalidated.
*/
public boolean isInvalidated() {
return invalidated;
}
/**
* @return whether this modification stack has been at least once invalidated.
*/
public boolean wasInvalidated() {
return wasInvalidated;
}
/**
* Adds the specified listener to the modification stack.
*
* @param listener
* The listener.
*/
public void addListener(IModificationListener listener) {
listeners.add(listener);
}
/**
* Removes the specified listener from the modification stack.
*
* @param listener
* The listener.
*/
public void removeListener(IModificationListener listener) {
listeners.remove(listener);
}
/**
* @return all the modification stack listeners.
*/
public IModificationListener[] getListeners() {
return listeners.toArray(new IModificationListener[listeners.size()]);
}
/**
* Executes the specified modification and stores its opposite modification in
* the undo stack.
*
* @param modification
* The modification.
*/
public void apply(IModification modification) {
try {
pushUndo(modification.applyAndGetOpposite());
} catch (IrreversibleModificationException e) {
invalidate();
}
}
/**
* Stores the specified modification undo modification in the undo stack.
*
* @param undoModification
* The undo modification.
* @return true only and only if the specified undo modification is not null.
*/
public boolean pushUndo(IModification undoModification) {
if (undoModification.isNull()) {
return false;
}
if (compositeStack.size() > 0) {
compositeStack.peek().pushUndo(undoModification);
return true;
}
validate();
undoStack.push(undoModification);
redoStack.clear();
allListenersProxy.handlePush(undoModification);
return true;
}
/**
* @return the number of remaining undo modifications.
*/
public int getUndoSize() {
return undoStack.size();
}
/**
* @return the number of remaining redo modifications.
*/
public int getRedoSize() {
return redoStack.size();
}
/**
* Execute the next undo modification.
*
* @throws ReflectionUIError
* If there is no remaining undo modification or if a composite
* modification is being created.
*/
public void undo() {
if (compositeStack.size() > 0) {
throw new ReflectionUIError("Cannot undo while composite modification creation is ongoing");
}
if (undoStack.size() == 0) {
return;
}
IModification undoModif = undoStack.pop();
try {
redoStack.push(undoModif.applyAndGetOpposite());
} catch (IrreversibleModificationException e) {
invalidate();
return;
}
allListenersProxy.handleUdno(undoModif);
}
/**
* Execute the next redo modification.
*
* @throws ReflectionUIError
* If there is no remaining redo modification or if a composite
* modification is being created.
*/
public void redo() {
if (compositeStack.size() > 0) {
throw new ReflectionUIError("Cannot redo while composite modification creation is ongoing");
}
if (redoStack.size() == 0) {
return;
}
IModification modif = redoStack.pop();
try {
undoStack.push(modif.applyAndGetOpposite());
} catch (IrreversibleModificationException e) {
invalidate();
return;
}
allListenersProxy.handleRedo(modif);
}
/**
* Execute all the undo modifications.
*/
public void undoAll() {
while (undoStack.size() > 0) {
undo();
}
}
/**
* @return the stack of undo modifications.
*/
public IModification[] getUndoModifications() {
return undoStack.toArray(new IModification[undoStack.size()]);
}
/**
* @return the stack of redo modifications.
*/
public IModification[] getRedoModifications() {
return redoStack.toArray(new IModification[redoStack.size()]);
}
/**
* Begins the creation of a composite modification. Following this method call,
* all the modifications that will be added to this stack will be packed into a
* unique modification until the call of
* {@link #endComposite(String, UndoOrder)} to finalize the composite
* modification creation or {@link #abortComposite()} to cancel it. Note that
* calling this method multiple times before making the related calls to
* {@link #endComposite(String, UndoOrder)} or {@link #abortComposite()} will
* result in the creation of inner composite modifications.
*/
public void beginComposite() {
if (!isInComposite()) {
validate();
}
compositeStack.push(new ModificationStack("(composite level " + compositeStack.size() + ") " + name));
}
/**
* @return true if a call to {@link #beginComposite()} have been performed but
* the call to the related {@link #endComposite(String, UndoOrder)} or
* {@link #abortComposite()} has not been performed yet.
*/
public boolean isInComposite() {
return compositeStack.size() > 0;
}
/**
* @param title
* The composite modification title.
* @param order
* The composite modification undo order.
* @return true if a potential modification was detected since the call of
* {@link #beginComposite()}. Note that true will also be returned if
* the composite modification creation have been aborted because of an
* invalidation.
*/
public boolean endComposite(String title, UndoOrder order) {
if (invalidated) {
abortComposite();
return true;
}
ModificationStack topComposite = compositeStack.pop();
ModificationStack compositeParent;
if (compositeStack.size() > 0) {
compositeParent = compositeStack.peek();
} else {
compositeParent = this;
}
IModification[] undoModifs = topComposite.getUndoModifications();
if (order == UndoOrder.getInverse()) {
List list = new ArrayList(Arrays.asList(undoModifs));
Collections.reverse(list);
list.toArray(undoModifs);
}
CompositeModification compositeUndoModif = new CompositeModification(AbstractModification.getUndoTitle(title),
order, undoModifs);
return compositeParent.pushUndo(compositeUndoModif);
}
/**
* Cancels a composite modification creation initiated by a preceding call to
* {@link #beginComposite()}.
*/
public void abortComposite() {
compositeStack.pop();
}
/**
* Convenient composite modification creation method to that calls
* {@link #beginComposite()}, performs the specified action and call
* {@link #endComposite(String, UndoOrder)} or {@link #abortComposite()}.
*
* @param title
* The composite modification title.
* @param order
* The composite modification undo order.
* @param action
* The method {@link Accessor#get()} will be called from this
* parameter object before the current method returns. It should push
* the children undo modifications in the current modification stack
* and return true if a potential modification is detected.
* @return whether a potential modification was detected.
*/
public boolean insideComposite(String title, UndoOrder order, Accessor action) {
beginComposite();
boolean modificationDetected;
try {
modificationDetected = action.get();
} catch (Throwable t) {
invalidate();
abortComposite();
throw new ReflectionUIError(t);
}
if (modificationDetected) {
return endComposite(title, order);
} else {
abortComposite();
return false;
}
}
/**
* Informs the modification stack of the current undo management inconsistency.
* Subsequently {@link #isInvalidated()} will return true and the undo and redo
* stacks will be emptied to ensure that the undo management remains consistent.
* This invalidation state will be cleared if an undo modification gets added
* afterwards.
*/
public void invalidate() {
wasInvalidated = invalidated = true;
allListenersProxy.handleInvalidate();
}
protected void validate() {
if (invalidated) {
redoStack.clear();
undoStack.clear();
compositeStack.clear();
invalidated = false;
allListenersProxy.handleInvalidationCleared();
}
}
/**
* Resets the modification stack. Unlike the method {@link #invalidate()}
* calling {@link #isInvalidated()} and {@link #wasInvalidated()} will be set to
* return false.
*/
public void forget() {
if (compositeStack.size() > 0) {
throw new ReflectionUIError("Cannot forget while composite modification creation is ongoing");
}
undoStack.clear();
redoStack.clear();
invalidated = wasInvalidated = false;
allListenersProxy.handleInvalidate();
}
/**
* @return whether there are remaining undo modifications.
*/
public Boolean canUndo() {
return (undoStack.size() > 0) && !isInvalidated();
}
/**
* @return whether there are remaining redo modifications.
*/
public Boolean canRedo() {
return (redoStack.size() > 0) && !isInvalidated();
}
/**
* @return whether the objects managed by this modification stack can be
* reverted to their initial state (in other terms, there are remaining
* undo modifications and the modification stack was never invalidated).
*/
public Boolean canReset() {
return canUndo() && !wasInvalidated();
}
/**
* @return whether objects managed by this modification stack are in their
* initial state (in other terms, there are no remaining undo
* modifications and the modification stack was never invalidated).
*/
public boolean isNull() {
if (undoStack.size() > 0) {
return false;
}
if (wasInvalidated()) {
return false;
}
return true;
}
/**
* @param title
* The title of the new composite modification.
* @return a composite modification containing the current undo modification
* stack.
*/
public IModification toCompositeUndoModification(String title) {
return new CompositeModification(title, UndoOrder.getNormal(), getUndoModifications());
}
/**
* @return a number identifying the current state of all the objects managed by
* this modification stack. If this value does not change between 2
* calls then the managed objects have not changed or their changes have
* successfully been reverted.
*/
public long getStateVersion() {
return stateVersion;
}
@Override
public String toString() {
return ModificationStack.class.getSimpleName() + "[" + name + "]";
}
}