/*
* Copyright 2003-2010 Tufts University Licensed under the
* Educational Community License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may
* obtain a copy of the License at
*
* http://www.osedu.org/licenses/ECL-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an "AS IS"
* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
* or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
package tufts.vue;
import tufts.Util;
import static tufts.Util.*;
import java.util.*;
import java.awt.Point;
import java.awt.Color;
import java.awt.geom.Point2D;
import javax.swing.Action;
/**
* Records all changes that take place in a LWMap (as seen from
* LWCEvent delivery off the LWMap) and provides for arbitrarily
* marking named points of rollback.
*
* For robustness, if the application fails to mark any changes,
* they'll either have been rolled into another undo action, or
* stuffed into an un-named Undo action if they attempt an undo while
* there are unmarked changes.
*
* @version $Revision: $ / $Date: $ / $Author: sfraize $
* @author Scott Fraize
*/
public class UndoManager
implements LWComponent.Listener
{
private static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(UndoManager.class);
private boolean mUndoUnderway = false;
private boolean mRedoUnderway = false;
private boolean mCleanupUnderway = false;
/** The list of undo actions (named groups of property changes) */
private UndoActionList UndoList = new UndoActionList(VueResources.getString("menu.edit.undo"));
/** The list of redo actions (named groups of property changes generated from Undo's) */
private UndoActionList RedoList = new UndoActionList(VueResources.getString("menu.edit.redo"));
/** The map who's modifications we're tracking. */
protected LWMap mMap;
/** All recorded changes since last mark, mapped by component (for detecting & ignoring repeats) */
private Map mComponentChanges = new HashMap();
/** All recorded changes since last mark, marked for sequential processing */
//private List mUndoSequence = new ArrayList();
/** The last LWCEvent we didn't ignore since last mark -- used for guessing at good Undo action title names */
private LWCEvent mLastEvent;
///** The total number of recorded or compressed changes since last mark (will be >= mUndoSequence.size()) */
//private int mChangeCount;
private int mEventsSeenSinceLastMark;
/** The current collector of changes, to be permanently recorded and named when a user "mark" is established by a GUI */
private UndoAction mCurrentUndo;
/** map of threads currently attched to a particular undo mark */
private Map<Thread,UndoMark> mThreadsWithMark = Collections.synchronizedMap(new HashMap());
///** map of threads currently attched to no undo manager (events to discard) */
//private static Map ThreadsToIgnore = Collections.synchronizedMap(new HashMap());
private boolean isSuspended = false;
public UndoManager(LWMap map)
{
mMap = map;
mCurrentUndo = new UndoAction();
map.addLWCListener(this);
//VUE.addActiveListener(LWMap.class, this);
updateGlobalActionLabels(); // make sure actions disabled at start
}
/**
* This aggregates a sequence of property changes under a single
* named user action.
*
* A named sequence of triples: LWComponent, property key, and old value.
* LWCEvents are undone in the reverse order that they happened: the changes
* are peeled back.
*/
private static class UndoAction {
private String name = null;
private List undoSequence; // list of UndoItem's -- will be sorted by sequence index before first use
/** The total number of recorded or compressed changes that happened on our watch (will be >= undoSequence.size()) */
private int eventCount = 0;
private boolean sorted = false;
private List<Thread> attachedThreads;
UndoAction() {
undoSequence = new ArrayList();
}
/*
UndoAction(String name, List undoSequence) {
this.name = name;
this.undoSequence = undoSequence;
}
*/
synchronized void addAttachedThread(Thread t) {
if (attachedThreads == null)
attachedThreads = new ArrayList();
attachedThreads.add(t);
}
int changeCount() {
return undoSequence.size();
}
int size() { return undoSequence.size(); }
void mark(String name) {
this.name = name;
if (DEBUG.UNDO) {
Log.debug(this + " MARKED with [" + name + "]");
//tufts.Util.printStackTrace(this + " MARKED with [" + name + "]");
}
}
boolean isIncomplete() {
return size() > 0 && name != null;
}
boolean isMarked() {
return name != null;
}
// synchronized void undo() {
// try {
// sUndoUnderway = true;
// run_undo();
// } finally {
// sUndoUnderway = false;
// }
// }
// todo: if there are any UndoableThread's attached to us, they should
// ideally be interrupted.
//private synchronized void run_undo() {
synchronized void undoAggregateUserAction() {
if (DEBUG.UNDO) Log.debug(this + " undoing sequence of size " + changeCount());
if (attachedThreads != null) {
// First: interrupt any running threads that may yet deliver events
// to this UndoAction.
if (DEBUG.Enabled) Log.debug(this + " making sure " + attachedThreads.size() + " attached threads are stopped");
for (Thread t : attachedThreads) {
if (t.isAlive()) {
if (DEBUG.Enabled) Log.debug(this + " INTERRUPTING " + t);
t.interrupt();
}
}
// clear all attached threads: only need to interrupt the first time.
attachedThreads.clear();
attachedThreads = null;
}
if (!sorted) {
Collections.sort(undoSequence);
sorted = true;
if (DEBUG.UNDO){
System.out.println("=======================================================");
VueUtil.dumpCollection(undoSequence);
System.out.println("-------------------------------------------------------");
}
}
//boolean hierarchyChanged = false; // now handled in fireUserActionCompleted
//-------------------------------------------------------
// First, process all hierarchy events
//-------------------------------------------------------
ListIterator<UndoItem> i = undoSequence.listIterator(undoSequence.size());
while (i.hasPrevious()) {
UndoItem undoItem = i.previous();
if (undoItem.propKey == LWKey.HierarchyChanging) {
undoItem.undo();
//hierarchyChanged = true;
}
}
//-------------------------------------------------------
// Second, process all property change events
//-------------------------------------------------------
i = undoSequence.listIterator(undoSequence.size());
while (i.hasPrevious()) {
UndoItem undoItem = i.previous();
if (undoItem.propKey != LWKey.HierarchyChanging)
undoItem.undo();
}
// if (hierarchyChanged)
// VUE.getSelection().clearDeleted();
}
String getName() {
return name;
}
/**
* massage the name of the property to produce a more human
* presentable name for the undo action.
*/
String getDisplayName() {
if (DEBUG.UNDO) return this.name + " {" + changeCount() + "}";
String display = null;
try {
display = produceDisplayName();
} catch (Throwable t) {
Log.error(Util.tags(this.name), t);
}
return display == null ? this.name : display;
}
private String produceDisplayName() {
if (this.name == LWKey.HierarchyChanging)
return "(Hierarchy Change)"; // shouldn't see this
String display = "";
String uName = this.name;
if (uName.startsWith("hier."))
uName = uName.substring(5);
// Replace all '.' with ' ' and capitalize first letter of each word
uName = uName.replace('-', '.');
String[] words = uName.split("\\.");
for (int i = 0; i < words.length; i++) {
final String word = words[i];
if (word.length() == 0)
return null; // if seen multiple dots in a row, presume pre-formatted
if (Character.isLowerCase(word.charAt(0)))
words[i] = Character.toUpperCase(word.charAt(0)) + word.substring(1);
if (i > 0)
display += " ";
display += words[i];
}
return display;
}
public String toString() {
int s = size();
return "UndoAction@" + Integer.toHexString(hashCode()) + "["
+ (name==null?"":name)
+ (s<10?" ":"") + s + " changes"
+ " from " + eventCount + " events"
+ "]";
}
}
/**
* A single property change on a single component.
*/
private static class UndoItem implements Comparable
{
LWComponent component;
Object propKey;
Object oldValue;
int order; // for sorting; highest values are most recent changes
UndoItem(LWComponent c, Object propertyKey, Object oldValue, int order) {
this.component = c;
this.propKey = propertyKey;
this.oldValue = oldValue;
this.order = order;
}
void undo() {
if (DEBUG.UNDO) Log.debug("UNDOING: " + this);
if (propKey == LWKey.HierarchyChanging) {
undoHierarchyChange((LWContainer) component, oldValue);
} else if (oldValue instanceof Undoable) {
((Undoable)oldValue).undo();
} else {
if (DEBUG.TEST) {
// ANIMATED UNDO CODE:
try {
Object curValue = component.getPropertyValue(propKey);
if (curValue != null)
undoAnimated();
} catch (Exception e) {
System.err.println("Exception during animated undo of [" + propKey + "] on " + component);
if (oldValue != null)
System.err.println("\toldValue is " + oldValue.getClass() + " " + oldValue);
e.printStackTrace();
}
}
if (component.isOrphan()) {
// For the hairy event's that LWGroups produce when created inside groups.
// we'd be getting a zombie event complaint if we did this. Turns out if we
// skip the property value completely, this is actually something we want to do
// to help group inside group undo/redo (zombie complaint shows up on undo, and
// then on redo, a location event goes thru that we actually don't want -- this
// was the conversion to local coordinates.
if (DEBUG.Enabled) Log.debug("SKIPPING undo item for deleted (parentless) component: " + component + "; " + this);
} else
component.undoProperty(propKey, oldValue);
}
}
private void undoAnimated() {
// redo not working if we suspend events here... please don't tell me redo was
// happening by capturing the zillions of animated events...
// Also going to be tricky: animating through changes in a bunch of nodes at the same
// time -- right now a group drag animates each one back into place one at a time in
// sequence... [ ScalarUndo, perhaps the new default, would fix this, as well as
// handle the zillions of location events problem, and if sharing is enabled, the
// network IO problem ]
// Also: SEE COMMENT in LWLink.getPropertyValue
// experimental for animated presentation
//component.getChangeSupport().setEventsSuspended();
if (oldValue instanceof Point) animatedChange((Point)oldValue);
else if (oldValue instanceof Point2D) animatedChange((Point2D)oldValue);
else if (oldValue instanceof Color) animatedChange((Color)oldValue);
else if (oldValue instanceof Size) animatedChange((Size)oldValue);
else if (oldValue instanceof Integer) animatedChange((Integer)oldValue);
else if (oldValue instanceof Float) animatedChange((Float)oldValue);
else if (oldValue instanceof Double) animatedChange((Double)oldValue);
//component.getChangeSupport().setEventsResumed();
}
private static final int segments = 5;
private static void repaint() {
//VUE.getActiveMap().notify("repaint");
VUE.getActiveViewer().paintImmediately();
//try { Thread.sleep(100); } catch (Exception e) {}
}
// TODO: THESE ARE CRAP: should be using a takeProperty, not setProperty, so no events are
// generated, and we only issue a LWKey.RepaintComponent each time (badly named tho) --
// perhaps RepaintImmediate, or even just Repaint if we handle it properly. Todo: will
// need to find and handle any cases where the properly change effects other things, tho I
// only think that could be the case for font-size -> full-font-style?
private void animatedChange(Size endValue) {
Size curValue = (Size) component.getPropertyValue(propKey);
final float winc = (endValue.width - curValue.width) / segments;
final float hinc = (endValue.height - curValue.height) / segments;
Size value = new Size(curValue);
for (int i = 0; i < segments; i++) {
value.width += winc;
value.height += hinc;
component.setProperty(propKey, value);
repaint();
}
}
private void animatedChange(Float endValue) {
Float curValue = (Float) component.getPropertyValue(propKey);
final float inc = (endValue.floatValue() - curValue.floatValue()) / segments;
Float value;
for (int i = 1; i < segments+1; i++) {
value = new Float(curValue.intValue() + inc * i);
component.setProperty(propKey, value);
repaint();
}
}
private void animatedChange(Double endValue) {
Double curValue = (Double) component.getPropertyValue(propKey);
final double inc = (endValue.doubleValue() - curValue.doubleValue()) / segments;
Double value;
for (int i = 1; i < segments+1; i++) {
value = new Double(curValue.intValue() + inc * i);
component.setProperty(propKey, value);
repaint();
}
}
private void animatedChange(Integer endValue) {
Integer curValue = (Integer) component.getPropertyValue(propKey);
final float inc = (endValue.intValue() - curValue.intValue()) / segments;
Integer value;
for (int i = 1; i < segments+1; i++) {
value = Integer.valueOf((int) (curValue.intValue() + inc * i));
component.setProperty(propKey, value);
repaint();
}
}
private void animatedChange(Color endValue) {
Color curValue = (Color) component.getPropertyValue(propKey);
final int rinc = (endValue.getRed() - curValue.getRed()) / segments;
final int ginc = (endValue.getGreen() - curValue.getGreen()) / segments;
final int binc = (endValue.getBlue() - curValue.getBlue()) / segments;
Color value;
for (int i = 1; i < segments+1; i++) {
value = new Color(curValue.getRed() + rinc * i,
curValue.getGreen() + ginc * i,
curValue.getBlue() + binc * i);
component.setProperty(propKey, value);
repaint();
}
}
private void animatedChange(Point2D endValue) {
Point2D curValue = (Point2D) component.getPropertyValue(propKey);
final double xinc = (endValue.getX() - curValue.getX()) / segments;
final double yinc = (endValue.getY() - curValue.getY()) / segments;
Point2D.Float value = new Point2D.Float((float)curValue.getX(), (float)curValue.getY());
//Point2D.Double value = new Point2D.Double(curValue.getX(), curValue.getY());
for (int i = 0; i < segments; i++) {
value.x += xinc;
value.y += yinc;
component.setProperty(propKey, value);
repaint();
}
}
private void animatedChange(Point endValue) {
Point curValue = (Point) component.getPropertyValue(propKey);
final double xinc = (endValue.getX() - curValue.getX()) / segments;
final double yinc = (endValue.getY() - curValue.getY()) / segments;
Point value = new Point(curValue);
for (int i = 0; i < segments; i++) {
value.x += xinc;
value.y += yinc;
component.setProperty(propKey, value);
repaint();
}
}
// TODO: detect condition that indicates we've got an event/undo event generation problem
// on the front end: let us know if ever the same component get's multiple DIFFERENT
// reparentings during a single undo: e.g., it appears in more than one hier.changing event
// for different components, so whichever one comes last is the one it ends up being parented to...
private static void undoHierarchyChange(final LWContainer parent, final Object oldValue)
{
if (DEBUG.UNDO) System.out.println("\trestoring children of " + parent + " to " + oldValue);
parent.notify(LWKey.HierarchyChanging); // this event important for REDO
// Create data for synthesized ChildrenAdded & ChildrenRemoved events
// For our purposes here, new/added are the the old values we're restoring,
// and old/removed are the current values we're replacing.
final List newChildList = (List) oldValue;
final List oldChildList = parent.mChildren;
final List childrenAdded;
final List childrenRemoved;
if (newChildList == LWComponent.NO_CHILDREN) {
childrenAdded = Collections.EMPTY_LIST;
} else {
childrenAdded = new ArrayList(newChildList);
if (oldChildList != LWComponent.NO_CHILDREN)
childrenAdded.removeAll(oldChildList);
}
if (oldChildList == LWComponent.NO_CHILDREN) {
childrenRemoved = Collections.EMPTY_LIST;
} else {
childrenRemoved = new ArrayList(oldChildList);
if (newChildList != LWComponent.NO_CHILDREN)
childrenRemoved.removeAll(newChildList);
}
//-------------------------------------------------------
// Do the swap in of the old list of children:
//-------------------------------------------------------
parent.mChildren = (List) oldValue;
//-------------------------------------------------------
// Now make sure all the children are properly parented,
// and none of them are marked as deleted.
//-------------------------------------------------------
// TODO: this apparently never handled the REDO DELETE case, which would
// ensure objects were once again removed from the model. The problem is
// that just because a node is in the childrenRemoved list, it doesn't mean
// it's being deleted -- it may just be in the process of being reparented
// elsewhere. This means that REDO of deletes are leaving orphaned objects
// out there with a non-null parent reference, and a missing DELETED bit,
// which also means they're not getting cleared from the selection on a REDO
// DELETE.
// This could be handled via tracking LWKey.Deleted events, but that would
// be a ton of extra events to record for large deletes. The best way would
// be to add a ChildrenDeleted event, issued in place of or in addition to
// ChildrenRemoved, which could easily be done in LWContainer.removeChildren
// when context == REMOVE_DELETE.
// When we get around to fixing this, may want to see if we can do away with
// the ChildrenAdded/ChildrenRemoved events, which are somewhat redundant
// with the HierarchyChanged event, and rarely listened for (tho LWContainer
// currently does NOT issue HierarchyChanged on adds/removes -- just
// ChildrenAdded/ChildrenRemoved!). The only place we currently listen
// for ChildrenAdded/ChildrenRemoved that couldn't immediately be replaced
// by HierarchyChanged is in the OutlineViewHierarchyModel impl.
if (parent.mChildren != LWComponent.NO_CHILDREN) {
if (parent instanceof LWPathway) {
// Special case for pathways. todo: something cleaner (pathways don't "own" their children)
Log.error("LWPathway's don't have real children: " + parent + "; for children " + parent.mChildren);
} else {
for (LWComponent child : parent.mChildren) {
if (child.isDeleted())
child.restoreToModel(); // todo: take parent as argument, skip setParent
child.setParent(parent);
}
// for (LWComponent child : parent.mChildren) {
// if (parent instanceof LWPathway) {
// // Special case for pathways. todo: something cleaner (pathways don't "own" their children)
// //((LWPathway)parent).addChildRefs(child);
// Util.printStackTrace("LWPathway's don't have real children: " + parent + "; for child " + child);
// } else {
// if (child.isDeleted())
// child.restoreToModel(); // todo: take parent as argument, skip setParent
// child.setParent(parent);
// //child.reparentNotify(parent);
// }
// }
}
}
parent.layout();
// issue synthesized ChildrenAddded and/or ChildrenRemoved events
if (childrenAdded.size() > 0) {
if (DEBUG.UNDO) Log.debug("Synthetic event " + LWKey.ChildrenAdded + " " + childrenAdded);
parent.notify(LWKey.ChildrenAdded, childrenAdded);
}
if (childrenRemoved.size() > 0) {
if (DEBUG.UNDO) Log.debug("Synthetic event " + LWKey.ChildrenRemoved + " " + childrenRemoved);
parent.notify(LWKey.ChildrenRemoved, childrenRemoved);
}
// issue the general hierarchy change event
parent.notify(LWKey.HierarchyChanged);
}
public int compareTo(Object o) {
return order - ((UndoItem)o).order;
}
@Override public String toString() {
Object old = oldValue;
if (oldValue instanceof Collection) {
Collection c = (Collection) oldValue;
if (c.size() > 1)
old = c.getClass().getName() + "{" + c.size() + "}";
}
return "UndoItem["
+ order + (order<10?" ":"")
+ " " + TERM_CYAN + propKey + TERM_CLEAR
+ " " + component
+ " old=" + old
+ "]";
}
}
private static class UndoActionList extends ArrayList
{
private final String name;
private int current = -1;
UndoActionList(String name) {
this.name = name;
}
public boolean add(Object o) {
// when adding, flush anything after the top
if (current < size() - 1) {
int s = current + 1;
int e = size();
if (DEBUG.UNDO) out("flushing " + s + " to " + e + " in " + this);
removeRange(s, e);
}
if (DEBUG.UNDO) out("adding: " + o);
super.add(o);
current = size() - 1;
return true;
}
UndoAction pop() {
if (current < 0)
return null;
return (UndoAction) get(current--);
}
UndoAction peek() {
if (current < 0)
return null;
return (UndoAction) get(current);
}
void advance() {
current++;
if (current >= size())
throw new IllegalStateException(this + " top >= size()");
}
public void clear() {
super.clear();
current = -1;
}
int top() {
return current;
}
private void out(String s) {
System.out.println("\tUAL[" + name + "] " + s);
}
public String toString() {
return "UndoActionList[" + name + " top=" + top() + " size=" + size() + "]";
}
public void add(int index, Object element) { throw new UnsupportedOperationException(); }
public Object remove(int index) { throw new UnsupportedOperationException(); }
}
// public void activeChanged(ActiveEvent<LWMap> e)
// {
// // We really don't need every undo manager listening for
// // active map changes -- a single listener for the active map
// // that then tells the active map's undo manager, if it has
// // one, to update the menu labels in the global VUE would
// // suffice, tho this works just fine with minimal overhead.
// if (e.active == mMap)
// updateGlobalActionLabels();
// }
public void updateGlobalActionLabels() {
setActionLabel(Actions.Undo, UndoList);
setActionLabel(Actions.Redo, RedoList);
}
/* If we are asked to do an undo (or redo), and find modifications
* on the undo list that have not been collected into a user mark,
* this is a problem -- all changes should be collected into a
* user umark (otherwise, for instance, creating a new node would
* show up as separate undo actions for every property that was
* set on the node during it's contstruction). If we find
* unmarked modifications, we report it to the console, and
* create a synthetic mark for all the unmarked changes, and
* name the last property on the list (which we could make
* look "normal" if there was only one unmarked change, but
* we want to know if this is happening.)
*/
private boolean checkAndHandleUnmarkedChanges() {
if (mCurrentUndo.isIncomplete()) {
new Throwable(this + " UNMARKED CHANGES! " + mComponentChanges).printStackTrace();
java.awt.Toolkit.getDefaultToolkit().beep();
boolean olddb = DEBUG.UNDO;
DEBUG.UNDO = true;
markChangesAsUndo("Unnamed Actions [last=" + mLastEvent.getName() + "]"); // collect whatever's there
DEBUG.UNDO = olddb;
return true;
}
return false;
}
private boolean mRedoCaptured = false; // debug
public synchronized void redo()
{
checkAndHandleUnmarkedChanges();
mRedoCaptured = false;
UndoAction redoAction = RedoList.pop();
if (DEBUG.UNDO) System.out.println(this + " redoing " + redoAction);
if (redoAction != null) {
try {
mRedoUnderway = true;
mUndoUnderway = true;
redoAction.undoAggregateUserAction();
//runCleanupTaskPhase(false);
} finally {
mUndoUnderway = false;
mRedoUnderway = false;
}
UndoList.advance();
fireUserActionCompleted();
}
updateGlobalActionLabels();
}
public synchronized void undo()
{
checkAndHandleUnmarkedChanges();
UndoAction undoAction = UndoList.pop();
if (DEBUG.UNDO) System.out.println("\n" + this + " undoing " + undoAction);
if (undoAction != null) {
mRedoCaptured = false;
try {
mUndoUnderway = true;
undoAction.undoAggregateUserAction();
//runCleanupTaskPhase(false);
} finally {
mUndoUnderway = false;
}
RedoList.add(collectChangesAsUndoAction(undoAction.name));
fireUserActionCompleted();
}
updateGlobalActionLabels();
// We've undo everything: we can mark the map as having no modifications
if (UndoList.peek() == null)
mMap.markAsSaved();
}
private void setActionLabel(Action a, UndoActionList undoList) {
String label = undoList.name;
if (DEBUG.UNDO) label += "#" + undoList.top() + "["+undoList.size()+"]";
if (undoList.top() >= 0) {
label += " " + undoList.peek().getDisplayName();
if (DEBUG.UNDO) System.out.println(this + " now available: '" + label + "'");
a.setEnabled(true);
} else
a.setEnabled(false);
a.putValue(Action.NAME, label);
}
/** figure the name of the undo action from the last LWCEvent we stored
* an old property value for */
public void mark() {
markChangesAsUndo(null);
}
/**
* If only one property changed, use the name of that property,
* otherwise use the @param aggregateName for the group of property
* changes that took place.
*/
public void mark(String aggregateName) {
String name = aggregateName;
if (name == null && mLastEvent != null) // going to need to put last event into UndoAction..
name = mLastEvent.getName();
markChangesAsUndo(name);
// String name = null;
// if (mCurrentUndo.size() == 1 && mLastEvent != null) // going to need to put last event into UndoAction..
// name = mLastEvent.getName();
// else
// name = aggregateName;
// markChangesAsUndo(name);
}
/**
* We use LinkedHashMap's to maintain insertion order.
* This class just adds a type name (for the kind of task) to the list.
*/
private static class TaskMap extends java.util.LinkedHashMap<Object,Runnable> {
final String type;
public TaskMap(String name) {
type = name;
}
public TaskMap clone() {
return (TaskMap) super.clone();
}
public String toString() {
return "TaskMap[" + type + " n=" + size() + "]";
}
}
// Keeping two list is a quick and easy way to support two levels of priority.
// If we end up needing more, implement a real priority system.
/** main/default cleanup tasks */
private final TaskMap mCleanupTasks = new TaskMap("Cleanup");
/** low/last priority tasks */
private final TaskMap mLastTasks = new TaskMap("Last");
/**
* @see addCleanupTask(Object taskKey, Runnable task)
* This defaults the taskKey to the task object itself.
**/
public void addCleanupTask(Runnable task) {
addCleanupTask(task, task);
}
// Note: This impl doesn't currently allow more than one cleanup task per LWComponent without
// creating a new non-static inner class Runnable to differentiate between the type of task to
// be run (E.g., LWGroup.DisperseOrNormalize). This isn't much a problem, but if ever we get
// to a point where we've got LOTS of cleanup tasks, we may want to extend this to allow a key
// that is based on the LWComponent.hashCode() + <some-string>, so we don't have to create all
// those Runnable implementing class instances. Se we'd use something other than Runnable,
// (e.g., Taskable), that takes a key argument so that a single calls on the the LWComponent
// (e.g., runTask("TaskA"), runTask("TaskB")), would allow it to switch out based on the given
// task key to run the different cleanup tasks needed by that LWComponent (the LWComponent
// itself would always become the primary key, and a string argument would be come the
// secondary key).
/** @return true if there are already any cleanup tasks with the given key */
public boolean hasCleanupTask(Object taskKey) {
return mCleanupTasks.containsKey(taskKey);
}
/** @return true if there are already any cleanup tasks with the given key */
public boolean hasLastTask(Object taskKey) {
return mLastTasks.containsKey(taskKey);
}
public boolean hasCleanupTasks() {
return mCleanupTasks.size() > 0 || mLastTasks.size() > 0;
}
void dumpCleanupTasks() {
Util.dump(mCleanupTasks, "cleanupTasks");
Util.dump(mLastTasks, "lastTasks");
}
/**
* Add a task to be run just before the next mark. If code somewhere has decided it
* needs to check that state at the end of all current user operations (which is
* when we create undo-marks, explicitly throughout the code at the end of known
* user action control points), it can add a task, which will run (possibly
* generating more events and adding to the undo queue) just before the current undo
* event queue is collected into an undo action as the mark is established.
* E.g., LWGroup's add a task any time children are removed, so it can run at the
* end to find out if it should auto-disperse itself (if it has only 0 or 1 members,
* and hasn't already been deleted). This is because many different operations may
* remove children from a group, and we don't want to track them all and don't care
* how they operate -- we just want to know what state we're left in when the dust
* settles.
* Tasks are NOT RUN during Undo/Redo -- they are "one-way" operations intended to
* maintain model integrity / transactional integrity. Once they've run and made
* changes to the model, the events generated by these changes are sufficient for
* what needs to happen during undo/redo -- the task is no longer needed.
* For maximum reliability, the same task should be able to run multiple times with
* no ill effect, as the UndoManager may run the same task more than once if new
* tasks come in while cleanup is being run. E.g., to use contrived examples, a
* task that did nothing ever than increment a counter, or say blindly add 10 to the
* x-value of a component, it not really a cleanup task, tho a task that ensured the
* width of an object was always twice it's height is just fine: it's enforcing
* a constraint (as long as there are no other tasks that also made the height
* depend on the width in any way, as we could wind up with competing tasks,
* neither of which will allow the model the ultimately resolve to a stable state).
* @param taskKey - a key that can be used to check to see
* if something with the same key is already waiting to be
* run at the next mark.
* @param task -- a Runnable
*/
public void addCleanupTask(Object taskKey, Runnable task) {
addTask(mCleanupTasks, taskKey, task);
}
public void addLastTask(Object taskKey, Runnable task) {
//if (DEBUG.WORK || DEBUG.UNDO) System.out.println(TERM_RED + "addLastTask " + taskKey + " " + task + TERM_CLEAR);
addTask(mLastTasks, taskKey, task);
}
private void addTask(TaskMap taskMap, Object taskKey, Runnable task) {
//if (mUndoUnderway && !(task instanceof LWLink.Recompute)) { // TODO: TEMP HACK TEMP HACK
if (mUndoUnderway) {
Util.printStackTrace(this + "; ignoring task during undo/redo in "
+ "\n\ttaskKey: " + taskKey
+ "\n\t task: " + task);
return;
}
synchronized (taskMap) {
if (DEBUG.WORK || DEBUG.UNDO) {
System.out.println(TERM_RED
+ "ADDING "
+ (mCleanupUnderway?"CASCADE ":"")
+ taskMap.type + " TASK: " + task
+ TERM_CLEAR
+ (taskKey == task ? "" : (" key=" + taskKey))
);
}
final Runnable prior = taskMap.put(taskKey, task);
if (prior != null)
Util.printStackTrace("over-wrote existing cleanup task (won't be run): " + prior + " taskKey: " + taskKey);
}
}
private void runCleanupTaskPhase(boolean debug) {
synchronized (mCleanupTasks) {
synchronized (mLastTasks) {
if (mCleanupUnderway) {
Util.printStackTrace("serious problem: cleanup already underway!");
return;
}
try {
mCleanupUnderway = true;
runPrioritizedCleanupTasks(debug);
} finally {
mCleanupUnderway = false;
}
}
}
}
private static final int MaxRecurse = 10;
// should be run inside a synchronized block against mCleanupTasks & mLastTasks
private void runPrioritizedCleanupTasks(boolean debug) {
if (mCurrentUndo.size() == 0) {
// note: in the case of undo usage by the LWIcon class, this is now normal
if (DEBUG.Enabled) Log.debug("Running cleanup tasks with an empty undo queue: " + this);
debug = true;
} else if (!debug)
debug = DEBUG.WORK || DEBUG.UNDO;
int recurseCount = 0; // first one doesn't count
do {
if (debug && recurseCount > 0)
System.out.println(TERM_RED + "CLEANUP TASKS: extra model cleanup pass #" + recurseCount + TERM_CLEAR);
if (mCleanupTasks.size() > 0)
runCleanupTasks(debug, mCleanupTasks);
if (mLastTasks.size() > 0)
runCleanupTasks(debug, mLastTasks);
} while (++recurseCount < MaxRecurse && (mCleanupTasks.size() > 0 || mLastTasks.size() > 0));
if (recurseCount > 1) {
if (recurseCount >= MaxRecurse)
Util.printStackTrace(this + " cleanup task recursion count exceeded max at " + recurseCount);
else
Log.info("note: UndoManager cleanup task recusion count reached " + recurseCount + " extra passes before model settled down.");
}
// When we clear this out has very complex semantics. Theoretically
// each list should be cleaned out as run, tho practically, doing
// mCleanupTasks now as opposed to before running last tasks prevents
// the queueing of new tasks during the last task that already
// ran as cleanup tasks. This may not be what we want it the end,
// tho it suits us for now.
//mCleanupTasks.clear();
//mLastTasks.clear();
}
// should be run inside a synchronized block against mCleanupTasks & mLastTasks
private void runCleanupTasks(boolean debug, TaskMap taskMap)
{
if (debug)
coutln(TERM_CYAN, "\nHANDLING " + taskMap + " TASKS in " + Thread.currentThread());
//final Set<Map.Entry<Object,Runnable>> entrySet = taskMap.clone().entrySet();
//final Iterator<Map.Entry<Object,Runnable>> iterator = entrySet.iterator();
//out("ITERABLE: " + Util.tag(entrySet));
//out("ITERATOR: " + Util.tag(iterator));
//while (iterator.hasNext()) {
// todo performance: we clone the task list here in case any more tasks
// come in while these tasks are running. This is a bit of a blunt
// instrument to handle the concurrent modification problem, but it's
// very reliable. If our tasks maps get large enough, we may want another
// solution.
int count = 0;
for (Map.Entry<Object,Runnable> e : taskMap.clone().entrySet()) {
count++;
//final Map.Entry<Object,Runnable> e = iterator.next();
final Runnable task = e.getValue();
final Object key = e.getKey();
if (debug) {
coutln(TERM_CYAN,
"RUNNING " + taskMap.type + " TASK #" + count + ": " + task
+ TERM_CLEAR
+ (task == key ? "" : " key: " + key));
}
task.run();
// now remove the task from live taskMap, which is may be adding new tasks
// (we iterate through a clone of the list
taskMap.remove(key);
}
if (debug) {
if (taskMap.size() > 0) coutln(TERM_RED, "CASCADED TASKS: " + taskMap);
coutln(TERM_CYAN, "COMPLETED " + count + " " + taskMap.type + " TASKS.");
}
}
/** @return true if we're undoing or redoing */
public boolean isUndoing() {
return mUndoUnderway;
}
public synchronized void markChangesAsUndo(String name)
{
synchronized (mCleanupTasks) {
synchronized (mLastTasks) {
// Can we skip running the cleanup tasks if there's nothing in the undo
// queue? Do we NEED to do that, in case of multiple "just in case" marks,
// where only the last one actually had anything, and we DON'T want to run
// the cleanup tasks till then?
if (mCleanupTasks.size() > 0 || mLastTasks.size() > 0)
runCleanupTaskPhase(false);
}
}
boolean addUndoable = true;
if (mCurrentUndo.size() == 0) // if nothing changed, don't bother adding an UndoAction
addUndoable = false;
else if (name == null) {
if (mLastEvent == null)
addUndoable = false;
else
name = mLastEvent.getName();
}
if (addUndoable) {
UndoList.add(collectChangesAsUndoAction(name));
RedoList.clear();
fireUserActionCompleted();
updateGlobalActionLabels();
} else {
checkAndHandleSelectionCleanups();
}
}
private synchronized UndoAction collectChangesAsUndoAction(String name)
{
if (DEBUG.UNDO) out("collectChangesAsUndoAction " + name);
final UndoAction markedUndo = mCurrentUndo;
markedUndo.mark(name);
resetMark();
return markedUndo;
}
public synchronized void resetMark() {
//mUndoSequence = new ArrayList();
mCurrentUndo = new UndoAction();
mComponentChanges.clear();
mLastEvent = null;
mEventsSeenSinceLastMark = 0;
//mChangeCount = 0;
}
void flush() {
UndoList.clear();
RedoList.clear();
mComponentChanges.clear();
if (VUE.getActiveMap() == mMap)
updateGlobalActionLabels();
}
/**
* Store a key in the given UndoableThread that tells the UndoManager what UndoAction
* is affected by LWCEvents coming from that thread. This must be called BEFORE any
* events in the given thread have been marked. To ensure this, make sure this is
* called before the thread has been started.
*/
void attachThreadToNextMark(UndoableThread t) {
if (t.isAlive())
throw new Error(t + ": not safe to attach an UndoAction to an already started thread");
t.setMarker(mCurrentUndo);
}
private static class UndoMark {
final UndoManager manager;
final UndoAction action;
UndoMark(UndoManager m) {
manager = m;
action = manager.mCurrentUndo;
}
public String toString() {
String s = "UndoMark[";
if (action != manager.mCurrentUndo)
s += action + " / ";
return s + manager + "]";
}
}
/**
* @return a key that marks the current location in the undo queue,
* that can be used to attach a subsequent thread to. That is,
* by taking the returned key and later calling attachThreadToMark
* in another thread, all further events received by the UndoManager from
* that thread will be attched in the undo queue at the location
* of the given mark. This may return null, which means there
* is no current UndoManager listening for events.
*/
public static Object getKeyForNextMark(LWComponent c) {
LWMap map = c.getMap();
if (map == null)
return null;
//throw new Error("Component not yet in map: can't search for undo manager " + c);
UndoManager undoManager = map.getUndoManager();
if (undoManager == null)
return null;
else
return undoManager.getKeyForNextMark();
//return currentManager.getStringKeyForNextMark();
}
public Object getKeyForNextMark() {
UndoMark mark = new UndoMark(this);
if (DEBUG.UNDO || DEBUG.THREAD) out("GENERATED MARK " + mark);
return mark;
}
/**
* Attach the current thread to the location in the undo queue marked by the given key.
*
* @param undoActionKey key obtained from getKeyForNextMark, which may be null,
* in which case this method does nothing.
*/
static void attachCurrentThreadToMark(Object undoActionKey) {
if (undoActionKey != null) {
Thread thread = Thread.currentThread();
//if (!thread.getName().startsWith("Image Fetcher"))
//new Throwable("Warning: attaching mark to non-Image Fetch thread: " + thread).printStackTrace();
// extract the mark, because it contains the manager we need to insert the thread:mark mapping
UndoMark mark = (UndoMark) undoActionKey;
// store the mark in the appropriate UndoManager, and notify of error if thread was already marked
UndoMark existingMark = mark.manager.mThreadsWithMark.get(thread);
if (existingMark != null) {
if (existingMark != mark) {
Log.warn(thread + " CONFLICTING MARKS:"
+ "\n\texisting: " + existingMark
+ "\n\t newer: " + mark);
} else if (DEBUG.Enabled) {
Log.debug(thread + " already tied mark " + mark + " grouping as one undo for now");
}
// this seems to actually be "working" as we get two undoables... ?
} else {
mark.manager.mThreadsWithMark.put(thread, mark);
mark.action.addAttachedThread(thread);
if (DEBUG.UNDO || DEBUG.THREAD) Log.debug("ATTACHED " + mark + " to " + thread);
}
/*
UndoMark existingMark = (UndoMark) mark.manager.mThreadsWithMark.put(thread, mark);
if (existingMark != null)
new Throwable("Error: " + thread
+ " was tied to mark " + existingMark
+ ", superceeded by " + mark).printStackTrace();
*/
}
else if (DEBUG.UNDO||DEBUG.THREAD) System.out.println("null UndoMark");
}
static void detachCurrentThread(Object undoActionKey) {
if (undoActionKey != null) {
if (DEBUG.THREAD) Util.printStackTrace("detachCurrentThread: " + Thread.currentThread());
UndoMark mark = (UndoMark) undoActionKey;
mark.manager.mThreadsWithMark.remove(Thread.currentThread());
// Tell everyone listing it's time to repaint.
// todo: only need to do this if a property actually changed during this thread
mark.manager.mMap.notify(mark.manager, LWKey.RepaintAsync);
// todo: messy to require two events here..
// This event will tell the ACTIVE viewer to repaint:
//mark.manager.mMap.notify(mark.manager, LWKey.Repaint);
// This event is for all NON-ACTIVE viewers, or Panners, other listeners, etc:
//mark.manager.mMap.notify(mark.manager, LWKey.UserActionCompleted);
}
/*
final Thread thread = Thread.currentThread();
final String tn = thread.getName();
if (tn.startsWith(UNDO_ACTION_TAG)) {
thread.setName(tn.substring(tn.indexOf(')') + 2));
System.out.println("Released thread " + thread);
}
*/
}
/*
private Map taggedUndoActions = new HashMap();
private static final String UNDO_ACTION_TAG = "+VUA@(";
static void attachCurrentThreadToStringMark(String undoActionKey) {
if (undoActionKey != null) {
Thread t = Thread.currentThread();
if (!t.getName().startsWith("Image Fetcher")) {
new Throwable("Warning: attaching mark to non-Image Fetch thread: " + t).printStackTrace();
}
// todo: cleaner if we kept a map of Thread:UndoAction's, but a tad slower
String newName = undoActionKey + " " + t.getName();
t.setName(newName);
if (DEBUG.UNDO || DEBUG.THREAD) System.out.println("Applied key " + undoActionKey + " to " + t);
}
}
private String _getStringKeyForNextMark() {
final String currentUndoKey = Integer.toHexString(mCurrentUndo.hashCode());
synchronized (this) {
taggedUndoActions.put(currentUndoKey, mCurrentUndo);
}
return UNDO_ACTION_TAG + currentUndoKey + ")";
}
*/
private boolean selectionCleanupForHidden;
private boolean selectionCleanupForDeleted;
private void checkAndHandleSelectionCleanups() {
if (DEBUG.UNDO) Log.debug("checkAndHandleSelectionCleanups: hideCleanup="
+ selectionCleanupForHidden
+ " deletedCleanup=" + selectionCleanupForDeleted);
if (selectionCleanupForHidden) {
selectionCleanupForHidden = false;
VUE.getSelection().clearHidden();
}
if (selectionCleanupForDeleted) {
selectionCleanupForDeleted = false;
VUE.getSelection().clearDeleted();
}
}
private void fireUserActionCompleted() {
checkAndHandleSelectionCleanups();
mMap.notify(this, LWKey.UserActionCompleted);
}
public void setSuspended(boolean suspend) {
isSuspended = suspend;
}
/**
* Every event anywhere in the map we're listening to, including events as a result of
* an Undo or Redo, will get delivered to us here. If the event has an old value in
* it and we're not in a Redo, we save it for later Undo/Redo (this includes if the
* event is a result of a current Undo). If it's a hierarchy event (e.g., add /
* remove / delete / forward / back, etc) we handle it specially.
*/
public void LWCChanged(final LWCEvent e) {
if (isSuspended) {
if (DEBUG.Enabled) Log.debug("suspended for: " + e);
return;
}
if (e.key == LWKey.Hidden || e.key == LWKey.Collapsed) {
// technically, we only need to flag this if the LWComponent in the event is
// also currently selected, tho theoretically there could be a list of
// components marked as hidden all at once (each of which we'd have to
// check), even tho we only currently fire list-based LWCEvent's for
// hierarchy events.
selectionCleanupForHidden = true;
} else if (e.key == LWKey.HierarchyChanging) {
// todo: better to check LWKey.Deleting?
selectionCleanupForDeleted = true;
}
if (mRedoUnderway) // ignore everything during redo
return;
if (e.key == LWKey.Repaint || e.key == LWKey.RepaintAsync) // ignore these
return;
if (mUndoUnderway) {
if (!mRedoCaptured && mCurrentUndo.size() > 0) {
Util.printStackTrace("Undo Error: have changes at start of redo record:"
+ "\n\t current UndoAction: " + mCurrentUndo
+ "\n\tcurrent change count: " + mComponentChanges
+ "\n\t UndoManager: " + this
+ "\n\t incoming event: " + e
);
}
mRedoCaptured = true;
if (DEBUG.UNDO) System.out.print("\tredo: " + e);
} else if (!mCleanupUnderway) {
if (DEBUG.UNDO) Log.debug(this + " " + e);
if (mCurrentUndo.size() == 0 && mCleanupTasks.size() > 0 && mEventsSeenSinceLastMark <= 0) {
// This can happen if a task is adding during a new user action,
// and we've seen events, but none of them have had an old value
// (adding to the Undo queue). Todo: track this so we know
// if we've seen ANY events, so we can still see this warning,
// which is important for catching undo/task bugs.
// [ Now that we handle cascading tasks, it's safer NOT to run these. ]
//Util.printStackTrace("Undo Warning: have un-run cleanup tasks on first incoming event (running now):"
if (DEBUG.Enabled)
Util.printClassTrace("tufts.vue",
"Undo Warning: have un-run cleanup tasks on first incoming event:"
+ "\n\t UndoManager: " + this
+ "\n\t# of cleanup tasks: " + mCleanupTasks.size()
+ "\n\t cleanup tasks: " + mCleanupTasks
//+ "\n\t events since mark: " + mEventsSeenSinceLastMark
+ "\n\t incoming event: " + e
);
// Fallback:
// Run them now, to at least ensure as much model integrity as we can.
// We defintely don't want to run them after more model changes take place,
// they either need to be run or purged now.
//runCleanupTasks(true);
}
}
mEventsSeenSinceLastMark++;
captureEvent(e);
}
private void captureEvent(LWCEvent e)
{
if (e.key == LWKey.HierarchyChanging || e.getName().startsWith("hier.")) {
recordEvent(e, true);
} else if (e.hasOldValue()) {
recordEvent(e, false);
} else {
if (DEBUG.UNDO) {
System.out.println(" (ignored: no old value)");
if (DEBUG.META) new Throwable().printStackTrace();
}
}
}
private void recordEvent(LWCEvent e, boolean hierarchyEvent)
{
final UndoAction relevantUndoAction;
final Map perComponentChanges;
final Thread thread = Thread.currentThread();
if (thread instanceof UndoableThread) {
relevantUndoAction = (UndoAction) ((UndoableThread)thread).getMarker();
if (relevantUndoAction == null) {
// This can happen if there was no UndoManager at the time
// the UndoableThread was started, such as when loading
// a map (we don't assign an undo manager to a map
// until it's fully loaded). In this case, there's
// nothing to do: these property changes weren't supposed
// to be undoable in the first place.
return;
}
if (DEBUG.UNDO || DEBUG.THREAD)
System.out.println("\nHandling UndoableThread " + thread + " event " + e);
perComponentChanges = null; // we can live w/out "compression" for changes on UndoableThread's
if (DEBUG.UNDO || DEBUG.THREAD) System.out.println("\n" + thread + " initiating change in " + relevantUndoAction);
} else if (mThreadsWithMark.size() > 0 && mThreadsWithMark.containsKey(thread)) {
//-----------------------------------------------------------------------------
// NOTE: this will no longer work except for network-IO image threads, as
// other image handling operations now happen in a thread-pool. We'd need
// some way of getting a general key out of Images code (maybe the Task
// object?) that is stored in a changing ThreadLocal variable as the tasks
// run. This is an argument for generally supporting some kind of a
// TRANSACTION object in the model, which would be used for things like
// this, as well as bits for user v.s. internal, etc. A general update/edit
// context.
//-----------------------------------------------------------------------------
final UndoMark mark = (UndoMark) mThreadsWithMark.get(thread);
if (DEBUG.UNDO || DEBUG.THREAD)
Log.debug("FOUND MARK FOR CURRENT THREAD " + thread
+ "\n\t mark: " + mark
+ "\n\tevent: " + e);
relevantUndoAction = mark.action;
perComponentChanges = null;
/* This code allowed us to tag a thread by tweaking it's name. Not a very safe method.
} else if (thread.getName().startsWith(UNDO_ACTION_TAG)) {
String key = thread.getName().substring(UNDO_ACTION_TAG.length());
key = key.substring(0, key.indexOf(')'));
final UndoAction taggedUndo;
synchronized (this) {
taggedUndo = (UndoAction) taggedUndoActions.get(key);
}
System.out.println("Got from key [" + key + "] " + taggedUndo + " for " + e);
relevantUndoAction = taggedUndo;
perComponentChanges = null;
*/
} else {
//------------------------------------------------------------------
// This is almost always where we wind up: all the above code
// in this method is just in case we got an event from
// a spawned thread, such as an ImageFetcher.
//------------------------------------------------------------------
relevantUndoAction = mCurrentUndo;
perComponentChanges = mComponentChanges;
}
if (hierarchyEvent)
recordUndoableChangeEvent(relevantUndoAction,
perComponentChanges,
LWKey.HierarchyChanging,
(LWContainer) e.getSource(), // parent
HIERARCHY_CHANGE_TAG);
else
recordUndoableChangeEvent(relevantUndoAction,
perComponentChanges,
e.key,
e.getComponent(),
e.oldValue);
mLastEvent = e;
//recordHierarchyChangingEvent(e, relevantUndoAction, perComponentChanges);
//recordPropertyChangeEvent(e, relevantUndoAction, perComponentChanges);
}
private static final Object HIERARCHY_CHANGE_TAG = "hierarchy.change";
// private void XrecordHierarchyChangingEvent(LWCEvent e, UndoAction undoAction, Map perComponentChanges)
// {
// LWContainer parent = (LWContainer) e.getSource();
// //recordUndoableChangeEvent(mHierarchyChanges, LWKey.HierarchyChanging, parent, HIERARCHY_CHANGE);
// //recordUndoableChangeEvent(um.mUndoSequence, um.mComponentChanges, LWKey.HierarchyChanging, parent, HIERARCHY_CHANGE);
// recordUndoableChangeEvent(undoAction, perComponentChanges, LWKey.HierarchyChanging, parent, HIERARCHY_CHANGE_TAG);
// mLastEvent = e;
// }
// private void XrecordPropertyChangeEvent(LWCEvent e, UndoAction undoAction, Map perComponentChanges)
// {
// // e.getComponent can really be list... todo: warn us if list (should only be for hier events)
// //recordUndoableChangeEvent(mPropertyChanges, e.getKey(), e.getComponent(), e.getOldValue());
// recordUndoableChangeEvent(undoAction, perComponentChanges, e.key, e.getComponent(), e.getOldValue());
// mLastEvent = e;
// }
/**
* Record a property change to a given component with the given property key. Our
* objective is to store one old value (the oldest) for each changed
* component:propertyKey pair. As such, we can "compress" repeat events for the same
* component:propertyKey pair. E.g., a single component is dragged across a map, and
* we get continuous events with propertyKey "location" for that component. We only
* need to store the old value from the FIRST of these events, and we can toss away
* the old value from all subsequent "location" changes to that same component, as
* these are just intermediate values over the course of one single user action.
*
* Note: if we did store all the intermediate values, along with the time each one
* happened (UndoItem.order would become a long, set to System.currentTimeMillis() --
* inefficient but easy), we'd actually be recording all user activity on the map, and
* be able to play back how they used the tool, or use it to construct demo's or such.
* Although our "animated undo" would be a much more efficient way of getting
* essentially the same behavior (interpolate the intermediate values instead of
* recording them all).
*
*
*/
//private static void recordUndoableChangeEvent(List undoSequence,Map componentChanges,Object propertyKey,LWComponent component,Object oldValue)
private static void recordUndoableChangeEvent(UndoAction undoAction,
Map perComponentChanges,
Object propertyKey,
LWComponent component,
Object oldValue)
{
boolean compressed = false; // already had one of these props: can ignore all subsequent
Map allChangesToComponent = null;
TaggedPropertyValue alreadyStoredValue = null; // a value already stored for this (component,propertyKey)
if (perComponentChanges != null) {
// If we have a map of existing components, we can do compression (don't have it for UndoableThread's)
// We look for find existing changes to this particular component, if any.
allChangesToComponent = (Map) perComponentChanges.get(component);
if (allChangesToComponent == null) {
// No prior changes to this component: create a new map for this component for remembering changes in
allChangesToComponent = new HashMap();
perComponentChanges.put(component, allChangesToComponent);
} else {
// If we already have a change to the same component with the same propertyKey, we can
//if (DEBUG.UNDO) System.out.println("\tfound existing component " + c);
//Object value = allChangesToComponent.get(propertyKey);
alreadyStoredValue = (TaggedPropertyValue) allChangesToComponent.get(propertyKey);
if (alreadyStoredValue != null) {
if (DEBUG.UNDO) System.out.println(" (compressed)");
compressed = true;
} else if (propertyKey == LWKey.HierarchyChanging && allChangesToComponent.containsKey(LWKey.Created)) {
// this will happen once for every damn link auto-grabbed at the end of new group creation.
// We may want to re-enabled that auto-grabbing at group creation time for LWLinks...
// Also, is happening on new master slide creation...
if (DEBUG.WORK) Log.debug("UndoManager: compressing hier change event for newly created component: " + component);
if (DEBUG.UNDO) System.out.println(" (compressed:NEW COMPONENT IGNORES HIER CHANGES)");
compressed = true;
}
}
}
if (compressed) {
// If compressed, still make sure the current property change UndoItem is
// marked as being at the current end of the undo sequence.
if (undoAction.size() > 1 && alreadyStoredValue != null) {
//UndoItem undoItem = (UndoItem) undoSequence.get(alreadyStoredValue.index);
UndoItem undoItem = (UndoItem) undoAction.undoSequence.get(alreadyStoredValue.index);
if (DEBUG.UNDO&&DEBUG.META) System.out.println("Moving index "
+alreadyStoredValue.index+" to end index "+undoAction.eventCount
+ " " + undoItem);
undoItem.order = undoAction.eventCount++;
}
} else {
if (oldValue == HIERARCHY_CHANGE_TAG) {
final LWContainer container = (LWContainer) component;
if (container.mChildren == LWComponent.NO_CHILDREN)
oldValue = LWComponent.NO_CHILDREN;
else
oldValue = ((ArrayList)container.mChildren).clone(); // TODO: impl dependency on ArrayList
}
if (allChangesToComponent != null)
allChangesToComponent.put(propertyKey, new TaggedPropertyValue(undoAction.size(), oldValue));
undoAction.undoSequence.add(new UndoItem(component, propertyKey, oldValue, undoAction.eventCount));
undoAction.eventCount++;
if (DEBUG.UNDO) {
System.out.println(" (stored: " + oldValue + ")");
//if (DEBUG.META)
//else System.out.println(" (stored)");
}
}
}
private static class TaggedPropertyValue {
int index; // index into undoSequence of this property value
Object value;
TaggedPropertyValue(int index, Object value) {
this.index = index;
this.value = value;
}
public String toString() {
return value + "~" + index;
}
}
private void out(String s) {
Log.debug(mCurrentUndo + ": " + s);
//Log.debug(this + ": " + s);
}
private static void coutln(String termColor, String s) {
System.out.println(termColor + s + TERM_CLEAR);
}
private String paramString() {
return "" + mCurrentUndo;
}
public String toString()
{
return ""+mCurrentUndo;
// return "UndoManager[" + mMap.getLabel() + " "
// + mCurrentUndo
// + "]"
// //+ hashCode()
// ;
}
}