/* Wotonomy: OpenStep design patterns for pure Java applications. Copyright (C) 2001 Intersect Software Corporation This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. This 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with this library; if not, see http://www.gnu.org */ package net.wotonomy.ui.swing; import java.awt.Component; import java.lang.ref.Reference; import java.lang.ref.WeakReference; import java.util.Enumeration; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Vector; import javax.swing.tree.TreePath; import net.wotonomy.control.EODataSource; import net.wotonomy.control.EODelayedObserver; import net.wotonomy.control.EOEditingContext; import net.wotonomy.control.EOObjectStore; import net.wotonomy.control.EOObserverCenter; import net.wotonomy.control.EOQualifier; import net.wotonomy.control.PropertyDataSource; import net.wotonomy.foundation.NSArray; import net.wotonomy.foundation.NSDictionary; import net.wotonomy.foundation.NSMutableDictionary; import net.wotonomy.foundation.NSNotification; import net.wotonomy.foundation.internal.ValueConverter; import net.wotonomy.foundation.internal.WotonomyException; import net.wotonomy.ui.EODisplayGroup; import net.wotonomy.ui.swing.TreeModelAssociation.DelegatingTreeDataSource; /** * DisplayGroupNodes are used as nodes in the TreeModelAssociation's * implementation of TreeModel, and is tightly coupled with TreeModelAssociation * and MasterDetailAssociation.
*
* * Even though it is no longer package access, don't rely on this class because * we want to have the option of completely replacing this approach in the * future. * * @author michael@mpowers.net * @author $Author: cgruber $ * @version $Revision: 904 $ */ abstract public class DisplayGroupNode extends EODisplayGroup { protected TreeModelAssociation parentAssociation; protected EODelayedObserver targetObserver; protected NSMutableDictionary childNodes; protected EODisplayGroup parentGroup; protected Object target; protected boolean isFetched; protected boolean isFetchNeeded; protected boolean useParentOrderings; protected boolean useParentQualifier; /** * Constructor for all nodes. Root node must have a null target. */ public DisplayGroupNode(TreeModelAssociation aParentAssociation, EODisplayGroup aParentGroup, Object aTarget) { //new net.wotonomy.ui.swing.util.StackTraceInspector( ""+aTarget ); //System.out.println( "DisplayGroupNode.new: " + aTarget ); parentAssociation = aParentAssociation; target = null; targetObserver = null; parentGroup = aParentGroup; childNodes = new NSMutableDictionary(); isFetched = false; isFetchNeeded = false; useParentOrderings = true; useParentQualifier = true; EODataSource parentSource = null; if (parentGroup != null) { parentSource = parentGroup.dataSource(); } else if (parentAssociation.titlesDisplayGroup != null) { parentSource = parentAssociation.titlesDisplayGroup.dataSource(); } // create child datasource if (aTarget != null) // not root node { if (parentAssociation.childrenKey != null) { if (parentSource == null) { throw new WotonomyException("Need a data source when children aspect is bound."); } NSArray displayedObjects = parentGroup.displayedObjects(); EODataSource childSource = parentSource.dataSourceQualifiedByKey(parentAssociation.childrenKey); childSource.qualifyWithRelationshipKey(parentAssociation.childrenKey, aTarget); // create new display group using child data source this.setDataSource(childSource); // establish observer for target object setTarget(aTarget); } else // only titles is bound { // establish observer for target object setTarget(aTarget); setDataSource(new PropertyDataSource() { public NSArray fetchObjects() { return new NSArray(); } }); } } else // else root node { // root node uses PropertyDataSource by default if (parentSource == null) { setDataSource(new PropertyDataSource() { public NSArray fetchObjects() { if (parentGroup != null) { return parentGroup.displayedObjects(); } return null; } }); } else { // root node uses parent source directly setDataSource(parentSource); } } } /** * Overridden to unregister as an editor of the editing context, since we don't * directly present a user interface. */ public void setDataSource(EODataSource aDataSource) { super.setDataSource(aDataSource); if ((aDataSource != null) && (aDataSource.editingContext() != null)) { aDataSource.editingContext().removeEditor(this); } } /** * Returns whether the node should call fetch(). */ protected boolean isFetched() { if (isFetchNeeded()) { setFetchNeeded(false); fetch(); } return isFetched; } /** * Sets whether the node should call fetch(). */ protected void setFetched(boolean fetched) { //System.out.println( "DisplayGroupNode.setFetched: " + fetched + " : " + this + " : " + target ); //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); isFetched = fetched; } /** * Returns whether the node is in need of a refetch. */ protected boolean isFetchNeeded() { return isFetchNeeded; } /** * Returns whether the node should call fetch(). */ protected void setFetchNeeded(boolean fetchNeeded) { //System.out.println( "DisplayGroupNode.setFetchNeeded: " + fetchNeeded + " : " + this + " : " + target ); //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); isFetchNeeded = fetchNeeded; } /** * Subclasses should override this method to fire an appropriate insertion * event. */ protected void fireNodesInserted(Object[] path, int[] indexes, Object[] objects) { //System.out.println( "fireNodesInserted: " + this ); parentAssociation.fireTreeNodesInserted(this, path, indexes, objects); } /** * Subclasses should override this method to fire an appropriate change event. */ protected void fireNodesChanged(Object[] path, int[] indexes, Object[] objects) { //System.out.println( "fireNodesChanged: " + this ); parentAssociation.fireTreeNodesChanged(this, path, indexes, objects); } /** * Subclasses should override this method to fire an appropriate deletion event. */ protected void fireNodesRemoved(Object[] path, int[] indexes, Object[] objects) { //System.out.println( "fireNodesRemoved: " + this ); parentAssociation.fireTreeNodesRemoved(this, path, indexes, objects); } /** * Subclasses should override this method to fire an appropriate event. */ protected void fireStructureChanged(Object[] path, int[] indexes, Object[] objects) { parentAssociation.fireTreeStructureChanged(this, path, indexes, objects); } /** * Overridden to broadcast a tree event after super executes. */ public void insertObjectAtIndex(Object anObject, int anIndex) { int count = getChildCount(); // gets old count if (target == null) { // if root node, forward to parent: // circumventing delegating data source, if any EODataSource dataSource = parentGroup.dataSource(); if (dataSource instanceof DelegatingTreeDataSource) { parentGroup.setDataSource(((DelegatingTreeDataSource) dataSource).delegateDataSource); } parentGroup.insertObjectAtIndex(anObject, anIndex); if (dataSource instanceof DelegatingTreeDataSource) { parentGroup.setDataSource(dataSource); } return; // prevent event from firing (?) } else // not root node { super.insertObjectAtIndex(anObject, anIndex); } } /** * Overridden to broadcast a tree event after super executes. */ public boolean deleteObjectAtIndex(int anIndex) { boolean result; Object node = getChildNodeAt(anIndex); if (target == null) { // if root node, forward to parent: result = parentGroup.deleteObjectAtIndex(anIndex); } else // not root node { result = super.deleteObjectAtIndex(anIndex); } return result; } /** * Returns the child node that corresponds to the specified index, creating it * if necessary. The index must be within bounds or an exception is thrown. */ public DisplayGroupNode getChildNodeAt(int anIndex) { boolean wasFetched = isFetched(); if (!wasFetched) fetch(); Object o = displayedObjects.objectAtIndex(anIndex); DisplayGroupNode result = getChildNodeForObject(o); if (result == null) { result = createChildNodeForObject(o); } return result; } /** * Returns a child node that corresponds to the specified object, returning null * if not found. */ protected DisplayGroupNode getChildNodeForObject(Object anObject) { return (DisplayGroupNode) childNodes.objectForKey(new ReferenceKey(anObject)); } /** * Creates a child node that corresponds to the specified object. */ private DisplayGroupNode createChildNodeForObject(Object anObject) { DisplayGroupNode result = parentAssociation.createNode(this, anObject); childNodes.setObjectForKey(result, new ReferenceKey(anObject)); return result; } /** * Returns a tree path of all DisplayGroupNodes leading to this node, including * the root node (but excluding the titles display group). */ public TreePath treePath() { List path = new LinkedList(); EODisplayGroup node = this; while (node instanceof DisplayGroupNode) { // insert at head of list path.add(0, node); node = ((DisplayGroupNode) node).parentGroup; } return new TreePath(path.toArray()); } /** * Overridden to return the parent group's sort ordering if useParentOrderings * is true. useParentOrderings is true by default. */ public NSArray sortOrderings() { if ((useParentOrderings) && (parentGroup != null)) { return parentGroup.sortOrderings(); } return super.sortOrderings(); } /** * Overridden to set useParentOrderings to false, or true if aList is null. */ public void setSortOrderings(List aList) { if (aList == null) { useParentOrderings = true; } else { useParentOrderings = false; super.setSortOrderings(aList); } } /** * Overridden to return the parent group's qualifier if useParentQualifier is * true. useParentQualifier is true by default. */ public EOQualifier qualifier() { if ((useParentQualifier) && (parentGroup != null)) { return parentGroup.qualifier(); } return super.qualifier(); } /** * Overridden to set useParentQualifier to false, or true if aList is null. */ public void setQualifier(EOQualifier aQualifier) { if (aQualifier == null) { useParentQualifier = true; } else { useParentQualifier = false; super.setQualifier(aQualifier); } } /** * Overridden to set isFetched to true. */ public boolean fetch() { //System.out.println( "DisplayGroupNode.fetch: " + this + " : " ); //if ( getClass().getName().indexOf( "Activity" ) != -1 ) //{ // new net.wotonomy.ui.swing.util.StackTraceInspector( this.toString() ); //} // set flag setFetched(true); // skip root node if (target == null) return true; // requalify dataSource().qualifyWithRelationshipKey(parentAssociation.childrenKey, target); // call to super return super.fetch(); //boolean result = super.fetch(); //System.out.println( displayedObjects() ); //return result; } /** * Returns the object at the appropriate index in the parent display group. */ public Object object() { // if root node if (target == null) { return parentAssociation.rootLabel(); } return target; } /** * Returns the string value of the title property on the object in the parent * display group corresponding to this index. The tree renderer asks JTrees to * call this method to retrieve a value for display. */ public String toString() { Object result = getUserObject(); if (result == null) result = "[null]"; return result.toString(); } // parts of interface TreeNode public int getChildCount() { if (!isFetched()) fetch(); //if ( toString().indexOf("154.16406")!=-1){ //System.out.println( "getChildCount: " + displayedObjects.count() + " : " + this ); //new RuntimeException().printStackTrace(); //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); //} return displayedObjects.count(); } public int getIndex(DisplayGroupNode node) { if (!isFetched()) fetch(); return displayedObjects.indexOfObject(((DisplayGroupNode) node).target); } public boolean getAllowsChildren() { return true; } public boolean isLeaf() { // if not root node and isLeaf aspect is bound if ((target != null) && (parentGroup != null) && (parentAssociation.leafKey != null)) { Object value; if (parentAssociation.leafDisplayGroup != null) { value = parentGroup.valueForObject(target, parentAssociation.leafKey); } else { value = parentAssociation.leafKey; } // getBoolean returns true for zero, among other things Object result = ValueConverter.getBoolean(value); if (result != null) { return ((Boolean) result).booleanValue(); } } // otherwise, we have to fetch and return count return (getChildCount() == 0); } public Enumeration children() { int count = getChildCount(); Vector v = new Vector(); for (int i = 0; i < count; i++) { v.add(getChildNodeAt(i)); } return v.elements(); } // parts of interface MutableTreeNode public void insert(DisplayGroupNode aChild, int anIndex) { insertObjectAtIndex(((DisplayGroupNode) aChild).object(), anIndex); } public void remove(int index) { deleteObjectAtIndex(index); } /** * Removes the node at the index corresponding to the index of the object. */ public void remove(DisplayGroupNode node) { remove(getIndex(node)); } /** * Removes our object from the parent display group. */ public void removeFromParent() { int index = parentGroup.displayedObjects().indexOfIdenticalObject(target); if (index != NSArray.NotFound) { parentGroup.deleteObjectAtIndex(index); } else { throw new WotonomyException("Object not found in parent group: " + target); } } /** * Removes our object from the parent display group and adds it to the end of * the specified node's children. */ public void setParent(DisplayGroupNode newParent) { removeFromParent(); newParent.insertObjectAtIndex(object(), newParent.displayedObjects.size()); } /** * Returns the value of the displayed property in the parent display group at * the index that corresponds to the index of this node. */ public Object getUserObject() { return valueForKey(parentAssociation.titlesKey); } /** * Sets the value of the displayed property in the parent display group at the * index that corresponds to the index of this node. */ public void setUserObject(Object aValue) { setValueForKey(aValue, parentAssociation.titlesKey); } /** * Returns a value from the object in the parent display group at the index that * corresponds to the index of this node. For the root node, if the titles key * is specified, the root label is returned, otherwise null is returned. */ public Object valueForKey(String aKey) { // if root node if (target == null) { // compare by ref is okay for strings if (aKey == parentAssociation.titlesKey) { return parentAssociation.rootLabel(); } return null; } return parentGroup.valueForObject(target, aKey); } /** * Sets a value on the object in the parent display group at the index that * corresponds to the index of this node. For the root node, this method only * works if aKey is the titlesAspect's key, otherwise does nothing. */ public void setValueForKey(Object aValue, String aKey) { // if root node, return. if (target == null) { // compare by ref is okay for strings if (aKey == parentAssociation.titlesKey) { parentAssociation.setRootLabel(aValue); // how to handle root node? tree event docs don't say. fireNodesChanged(treePath().getPath(), new int[] { 0 }, new Object[] { this }); } return; } parentGroup.setValueForObject(aValue, target, aKey); } /** * Perform any clean up in this method. The node will not be reused after this * method is called. This implementation removes itself from the parent's set of * child nodes, sets target and datasource to null, and then calls * disposeChildNodes(). */ protected void dispose() { // System.out.println( "dispose: " + this.getClass().getName() + " : " + this ); if (parentGroup != null) { ((DisplayGroupNode) parentGroup).childNodes.remove(new ReferenceKey(target)); } setTarget((Object) null); setDataSource(null); disposeChildNodes(); } /** * Calls dispose() on all child nodes. */ protected void disposeChildNodes() { Iterator i = new LinkedList(childNodes.values()).iterator(); while (i.hasNext()) { ((DisplayGroupNode) i.next()).dispose(); } } /** * Called after the target object posts a change notification. This * implementation re-fetches which triggers updateDisplayedObjects to broadcast * any tree events. This method marks the parent object as changed if: (1) this * object is not registered in the editing context of the titles display group's * data source (if any), AND (2) the children key is not in the list of * attributes of the parent object's EOClassDescription. */ public void targetChanged() { // if not root node if (target != null) { // if we're not root and not fetched, stop here. // FIXME: with this, some nodes have old values when moved. // FIXME: without this, nodes are unnecessarily fetched. // FIXME: might have parent modify isFetched of certain child nodes. if (isFetched()) { fetch(); } else // not fetched - just update the display { updateDisplayedObjects(); } /* * //disabling this for performance reasons: //might reenable later or find an * alternate approach // check to see if we need to mark the parent object as * changed EOEditingContext context = dataSource().editingContext(); if ( ( * context == null ) || ( context.globalIDForObject( target ) == null ) ) { * DisplayGroupNode parentNode = (DisplayGroupNode) parentGroup; if ( * parentNode.target != null ) { // only notify if childrenKey is an attribute * of parentDesc // (and therefore not a toOne or toMany relationship) * EOClassDescription parentDesc = EOClassDescription.classDescriptionForClass( * parentNode.target.getClass() ); if ( parentDesc.attributeKeys().contains( * parentAssociation.childrenKey ) ) { // only notify if no context is already * observing the object // and we are an attribute key * EOObserverCenter.notifyObserversObjectWillChange( parentNode.target ); } } } */ } else // root node { setObjectArray(parentAssociation.titlesDisplayGroup.displayedObjects()); } // finally, broadcast change event for this node // even though we're not sure if the displayed value changed. fireNodeChanged(); } /** * Fires a change event for this node. */ public void fireNodeChanged() { // if not root node if (target != null) { int index = ((DisplayGroupNode) parentGroup).getIndex(this); if ((index != -1) && (treePath().getParentPath() != null)) { fireNodesChanged(treePath().getParentPath().getPath(), new int[] { index }, new Object[] { this }); } } } Object[] previouslyDisplayedObjects = new Object[0]; /** * Overridden to call to super, fire any tree events, and then call * updateDisplayedObjects on all fetched child nodes. This method compares this * node's displayed objects against the list of child nodes, synchronizes them, * and then broadcasts only the necessary events to bring the view component up * to date. */ public void updateDisplayedObjects() { //System.out.println( "updateDisplayedObjects: " + " : " + this ); //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace(); //new RuntimeException().printStackTrace(); super.updateDisplayedObjects(); // diff lists boolean proceed = true; Object[] oldObjects = previouslyDisplayedObjects; Object[] newObjects = displayedObjects.toArray(); if (oldObjects.length == newObjects.length) { proceed = false; for (int i = 0; i < newObjects.length; i++) { if (oldObjects[i] != newObjects[i]) { proceed = true; break; } } } // this should be set before firing the change events // in case some clients end up calling this again. previouslyDisplayedObjects = newObjects; DisplayGroupNode node; Iterator i = childNodes.values().iterator(); while (i.hasNext()) { node = (DisplayGroupNode) i.next(); if (!node.isFetchNeeded()) { node.updateDisplayedObjects(); } } if (proceed) { //System.out.println( "DisplayGroupNode.firingEventsForChanges: " ); //new RuntimeException().printStackTrace(); fireEventsForChanges(oldObjects, newObjects); } } /** * Called by processRecentChanges to analyze the differences between the lists * and broadcast the appropriate events. */ protected void fireEventsForChanges(Object[] oldObjects, Object[] newObjects) { // structure changed causes havoc while // establishing connection in some cases // if ( oldObjects.length == 0 || newObjects.length == 0 ) // { // fireStructureChanged( treePath().getPath(), null, null ); // return; // } int insertCount = 0; int deleteCount = 0; Object[] inserts = new Object[newObjects.length]; Object[] deletes = new Object[oldObjects.length]; int i; int n = -1, o = -1; // last match int n1 = 0, o1 = 0; // current match test int n2 = 0, o2 = 0; // scan ahead while (o1 < oldObjects.length && n1 < newObjects.length) { if (newObjects[n1] == oldObjects[o1]) { // mark as match and continue o = o1; n = n1; } else { // scan ahead for the next match, if any o2 = o1; n2 = n1; while (o2 < oldObjects.length || n2 < newObjects.length) { if (o2 < oldObjects.length && newObjects[n1] == oldObjects[o2]) { // run o1 to o2: mark as deletes for (i = o1; i < o2; i++) { // System.out.println( "delete : " + i ); deletes[i] = oldObjects[i]; deleteCount++; } o1 = o2; // reset test o = o1; // set match n = n1; // set match break; } if (n2 < newObjects.length && newObjects[n2] == oldObjects[o1]) { // run n1 to n2: mark as inserts for (i = n1; i < n2; i++) { // System.out.println( "insert : " + i ); inserts[i] = newObjects[i]; insertCount++; } n1 = n2; // reset test n = n1; // set match o = o1; // set match break; } o2++; n2++; } } if (n != n1) { inserts[n1] = newObjects[n1]; insertCount++; deletes[o1] = oldObjects[o1]; deleteCount++; // increment even though no match: // the new object was marked as inserted and // the old object was marked as deleted. n = n1; o = o1; } o1++; n1++; } // run o to end of oldObjects: mark as deletes for (i = o + 1; i < oldObjects.length; i++) { // System.out.println( "delete : " + i ); deletes[i] = oldObjects[i]; deleteCount++; } // run n to end of newObjects: mark as inserts for (i = n + 1; i < newObjects.length; i++) { // System.out.println( "insert : " + i ); inserts[i] = newObjects[i]; insertCount++; } //System.out.println( "done : " //+ o + " : " + o1 + " : " + o2 + " :: " + n + " : " + n1 + " : " + n2 ); //System.out.println( new NSArray( newObjects ) ); //System.out.println( new NSArray( inserts ) ); //System.out.println( new NSArray( deletes ) ); //System.out.println( new NSArray( oldObjects ) ); int c; Object[] nodes; int[] indices; // broadcast delete event c = 0; nodes = new Object[deleteCount]; indices = new int[deleteCount]; for (i = 0; i < deletes.length; i++) { if (deletes[i] != null) { indices[c] = i; nodes[c] = getChildNodeForObject(deletes[i]); c++; } } if (c > 0) { // fireNodeChanged(); // force the jtree to get the correct child count fireNodesRemoved(treePath().getPath(), indices, nodes); } deletes = nodes; // retain for dispose check // broadcast insert event c = 0; nodes = new Object[insertCount]; indices = new int[insertCount]; for (i = 0; i < inserts.length; i++) { if (inserts[i] != null) { indices[c] = i; nodes[c] = getChildNodeForObject(inserts[i]); if (nodes[c] == null) { nodes[c] = createChildNodeForObject(newObjects[i]); } c++; } } if (c > 0) { fireNodesInserted(treePath().getPath(), indices, nodes); } // dispose any delete nodes not on insert list int j; boolean found; for (i = 0; i < deletes.length; i++) { for (j = 0; j < nodes.length; j++) { if (deletes[i] == nodes[j]) break; } // did not break early, so not found, so dispose if (j == nodes.length) { ((DisplayGroupNode) deletes[i]).dispose(); } } } /** * Sets the target object and creates an registers a target observer. If target * was not previously null, the existing observer is unregistered. Protected * access so subclasses and TreeModelAssociation can update our target. */ public void setTarget(Object aTarget) { if (target != null) { EOObserverCenter.removeObserver(targetObserver, target); targetObserver.discardPendingNotification(); } if (aTarget != null) { target = aTarget; targetObserver = new TargetObserver(this); EOObserverCenter.addObserver(targetObserver, target); } } /** * Returns the parent display group, or null if parent is root. */ public DisplayGroupNode getParentGroup() { if (parentGroup instanceof DisplayGroupNode) { return (DisplayGroupNode) parentGroup; } // presumably the root node return null; } /** * Gets all descendants of the this node. */ public List getDescendants() { return getDescendants(this, true); } /** * Gets only the descendants of the this node whose children has been loaded - * no fetching will occur. Useful for load-on-demand trees. */ public List getLoadedDescendants() { return getDescendants(this, false); } // breadth first traversal implementation /** * Returns a list of all descendants of the specified node. Unfetched nodes are * traversed only if forceLoad is true. This implementation is a breadth-first * traversal of the nodes starting at the specified node. */ static private List getDescendants(DisplayGroupNode aNode, boolean forceLoad) { if (!forceLoad && !aNode.isFetched) return NSArray.EmptyArray; LinkedList result = new LinkedList(); LinkedList queue = new LinkedList(); queue.add(aNode); while (!queue.isEmpty()) { checkNode((DisplayGroupNode) queue.removeFirst(), queue, result, forceLoad); } return result; } /** * Adds each fetched child node of the specified node to the result set * (optionally forcing the child node to load) and adding child node to the end * of the queue. */ static private void checkNode(DisplayGroupNode aNode, LinkedList aQueue, LinkedList aResult, boolean forceLoad) { DisplayGroupNode child; int count = aNode.getChildCount(); for (int i = 0; i < count; i++) { child = aNode.getChildNodeAt(i); // add to queue if node has fetched children if ((!child.isFetched) && (forceLoad)) { child.fetch(); } if (child.isFetched) { aQueue.addLast(child); } aResult.add(child); } } /** * Overridden to not fetch on InvalidateAllObjectsInStoreNotification unless * we've already been fetched, preserving the load-on-demand functionality. */ public void objectsInvalidatedInEditingContext(NSNotification aNotification) { if (EOObjectStore.InvalidatedAllObjectsInStoreNotification.equals(aNotification.name())) { //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: " + aNotification.name() ); if (parentAssociation.isVisible(this) && targetObserver != null) { targetObserver.objectWillChange(target); // force ui to update fireNodeChanged(); } else // make sure we fetch children when we do become visible setFetchNeeded(true); return; } else if ((EOEditingContext.ObjectsChangedInEditingContextNotification.equals(aNotification.name())) || (EOEditingContext.EditingContextDidSaveChangesNotification.equals(aNotification.name()))) { int index; Enumeration e; boolean didChange = false; NSDictionary userInfo = aNotification.userInfo(); // if our target object was deleted NSArray deletes = (NSArray) userInfo.objectForKey(EOObjectStore.DeletedKey); if (deletes.indexOfIdenticalObject(target) != NSArray.NotFound) { //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: delete: " + this + " : " + aNotification.name() ); if (parentAssociation.isVisible(this) && targetObserver != null) { targetObserver.objectWillChange(target); // force ui to update fireNodeChanged(); } else // make sure we fetch children when we do become visible setFetchNeeded(true); return; } // if our target object was invalidated NSArray invalidates = (NSArray) userInfo.objectForKey(EOObjectStore.InvalidatedKey); if (invalidates != null && invalidates.indexOfIdenticalObject(target) != NSArray.NotFound) { //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: invalidate: " + this + " : " + aNotification.name() ); if (parentAssociation.isVisible(this) && targetObserver != null) { targetObserver.objectWillChange(target); // force ui to update fireNodeChanged(); } else // make sure we fetch children when we do become visible setFetchNeeded(true); return; } // if our target object was updated, set fetchNeeded plus fire changed event NSArray updates = (NSArray) userInfo.objectForKey(EOObjectStore.UpdatedKey); if (updates.indexOfIdenticalObject(target) != NSArray.NotFound) { if (parentAssociation.isVisible(this) && targetObserver != null) { targetObserver.objectWillChange(target); // force ui to update fireNodeChanged(); if (object() instanceof Component) ((Component) object()).repaint(); } else // make sure we fetch children when we do become visible setFetchNeeded(true); return; } } super.objectsInvalidatedInEditingContext(aNotification); } // inner classes /** * Private class used to force a hashmap to perform key comparisons by * reference. */ private class ReferenceKey { private int hashCode; private Object referent; public ReferenceKey(Object anObject) { referent = anObject; hashCode = anObject.hashCode(); } /** * Returns the actual key's hash code. */ public int hashCode() { return hashCode; } /** * Compares by reference. */ public boolean equals(Object anObject) { if (anObject instanceof ReferenceKey) { return ((ReferenceKey) anObject).referent == referent; } return false; } } /** * A private class to observe the target object of this node. */ private class TargetObserver extends EODelayedObserver { Reference ref; /** * Pass in the display group node that will be updated when the target changes. */ public TargetObserver(DisplayGroupNode aDisplayGroup) { ref = new WeakReference(aDisplayGroup); } /** * Repopulate our display group, and calculate the deltas so we can broadcast * appropriate events. */ public void subjectChanged() { DisplayGroupNode node = (DisplayGroupNode) ref.get(); if (node == null) return; // node is null if gc'd. // FIXME: should un-register self from observer center?? node.targetChanged(); } } } /* * $Log$ Revision 1.2 2006/02/18 23:19:05 cgruber Update imports and maven * dependencies. * * Revision 1.1 2006/02/16 13:22:22 cgruber Check in all sources in * eclipse-friendly maven-enabled packages. * * Revision 1.64 2003/08/06 23:07:52 chochos general code cleanup (mostly, * removing unused imports) * * Revision 1.63 2003/06/06 14:20:07 mpowers getLoadedDescendants was forcing a * fetch of the node it was called on. * * Revision 1.62 2003/06/03 14:48:33 mpowers Clean-up of notification handling * for updates/invalidation/etc. Now fetching immediately on notification if the * node is visible. This averts the infamous IndexOutOfBoundsException that * occurs if fetching happens during repaint, because the BasicTreeUI is caching * the number of child nodes before painting begins. * * Revision 1.61 2003/01/18 23:33:29 mpowers Fixing the build. * * Revision 1.60 2002/05/31 15:03:10 mpowers Fixes for the previous fix. Fat * props to yjcheung. * * Revision 1.59 2002/05/28 15:31:36 mpowers Fix for updateDisplayedObjects for * a subtle case where a node appears in the position that another node was * moved from. * * Revision 1.58 2002/05/24 14:42:02 mpowers Prevent repeat events from firing * if firing events loops back. * * Revision 1.57 2002/04/23 19:12:28 mpowers Reimplemented fireEventsForChanges. * Fitter and happier. * * Revision 1.56 2002/04/19 21:18:45 mpowers Removed tree event coalescing, * which was causing way too many problems. The fireChangeEvent algorithm is way * faster than before, so we should still be better off than before. At least * now, we don't have to track whether the view component has encountered a * particular node. * * Revision 1.55 2002/04/19 20:53:22 mpowers Now firing event fewer events in * fireEventsForChanges. * * Revision 1.54 2002/04/15 21:52:50 mpowers Tightening up TreeModelAssociation * and DisplayGroupNode. Now only firing root structure changed once. Now * disposing of root's children. Better event coalescing. * * Revision 1.53 2002/04/12 20:35:20 mpowers Now correctly setting parent * display group and data source on creation. * * Revision 1.52 2002/04/10 21:20:04 mpowers Better handling for tree nodes when * working with editing contexts. Better handling for invalidation. No longer * broadcasting events when nodes have not been "registered" in the tree. * * Revision 1.51 2002/04/03 20:13:36 mpowers Now differentiating between node * instantiation caused by model expansion (user initiated) and by modifications * to the model. Dispose now disposes all children. * * Revision 1.50 2002/03/23 16:20:27 mpowers Optimized processRecentChanges, * minimized tree events. * * Revision 1.49 2002/03/11 03:15:06 mpowers Optimized processRecentChanges, * minimize event firing, coalescing changes. Still need a better diff algorithm * to avoid removing nodes. * * Revision 1.48 2002/03/10 00:59:39 mpowers Interim version: coalesces calls to * process recent changes. Still does not handle rearranged nodes. * * Revision 1.47 2002/03/09 17:33:45 mpowers Nodes now track their child nodes * by reference, not index. * * Revision 1.46 2002/03/08 23:19:07 mpowers Added getParentGroup to * DisplayGroupNode. * * Revision 1.45 2002/03/06 13:04:16 mpowers Implemented cascading qualifiers in * tree nodes. * * Revision 1.44 2002/02/27 23:19:17 mpowers Refactoring of TreeAssociation to * create TreeModelAssociation parent. * * Revision 1.42 2002/02/19 22:28:46 mpowers DisplayGroupNodes immediately * unregister themselves as editors. * * Revision 1.41 2002/02/13 16:27:38 mpowers Exposing setTarget. * * Revision 1.40 2001/11/02 20:55:46 mpowers Now using fixed index to send node * removed events. This preserves the expanded state of the nodes in the * corresponding jtree. * * Revision 1.39 2001/09/21 21:09:25 mpowers Exposed more fields as protected. * * Revision 1.38 2001/09/19 15:36:08 mpowers Refined behavior for isFetched * after notification handling. * * Revision 1.37 2001/09/13 14:51:18 mpowers DisplayGroupNodes now dispose * themselves and mark their parent for update when they receive notification * that their target has been deleted. * * Revision 1.36 2001/09/10 14:10:24 mpowers Fix for notification handling. * * Revision 1.35 2001/07/30 16:17:01 mpowers Minor code cleanup. * * Revision 1.34 2001/07/18 22:13:39 mpowers getLoadedDescendants now works as * advertised. Now correctly handling invalidateAllObjects notification. * * Revision 1.33 2001/07/18 13:03:32 mpowers TreeNodes now refetch only on * demand. Previously, once a node had been fetched, it was always refetched * after an invalidate, even if the node was not being displayed. * * Revision 1.32 2001/06/18 14:10:28 mpowers Cleaned up event firing: no longer * firing insert or remove events twice. * * Revision 1.31 2001/06/09 16:15:39 mpowers Revised the targetChanged scheme * because oldObjects and newObjects were identical after the target object is * invalidated. * * Revision 1.30 2001/05/21 22:17:19 mpowers Fix for tree out-of-synch problems * when nodes are inserted. * * Revision 1.29 2001/05/18 21:07:46 mpowers Playing with refresh options. * * Revision 1.28 2001/05/14 15:25:43 mpowers DisplayGroupNodes now only respond * to InvalidateAllObjectsInStore if they are already fetched. * * Revision 1.27 2001/05/08 19:55:58 mpowers Fix for node children not * refreshing after sibling was inserted. * * Revision 1.26 2001/05/08 18:47:34 mpowers Minor fixes for d3. * * Revision 1.25 2001/05/06 22:22:55 mpowers Debugging. * * Revision 1.24 2001/05/04 14:42:58 mpowers Now getting stored values in * KeyValueCoding. MasterDetail now marks dirty based on whether it's an * attribute or relation. Implemented editing context marker. * * Revision 1.23 2001/05/02 18:00:43 mpowers Removed debug code. * * Revision 1.22 2001/05/02 17:31:20 mpowers DisplayGroupNode now does a better * job determining when to mark its parent dirty. * * Revision 1.21 2001/05/01 00:52:32 mpowers Implemented breadth-first traversal * of tree for node. * * Revision 1.20 2001/04/26 01:15:19 mpowers Major clean-up of DisplayGroupNode: * fitter, happier, more productive. * * Revision 1.19 2001/04/22 23:13:35 mpowers Minor bug. * * Revision 1.18 2001/04/22 23:05:33 mpowers Totally revised DisplayGroupNode so * each object gets its own node (so the nodes are no longer fixed by index). * * Revision 1.17 2001/04/21 23:05:12 mpowers A fairly major revisiting. I've * decided to scrap the pass-thru approach where every node simply represents an * index and not an object. The next update will have each node correspond to a * specific object. * * Revision 1.16 2001/04/13 16:37:37 mpowers Handling bounds checking. * * Revision 1.15 2001/04/03 20:36:01 mpowers Fixed * refaulting/reverting/invalidating to be self-consistent. * * Revision 1.14 2001/03/27 17:45:51 mpowers More index bounds checking. * * Revision 1.13 2001/03/22 21:25:42 mpowers Fixed some nasty issues with * jtree's internal state and array bounds. * * Revision 1.12 2001/03/19 22:18:58 mpowers Root node now mirrors contents of * titles display group. * * Revision 1.11 2001/03/19 21:38:36 mpowers Improved redisplay after edit. * Editing nodes off root now works. * * Revision 1.10 2001/03/09 22:08:38 mpowers Removed unused line. * * Revision 1.9 2001/03/07 16:41:04 mpowers Now checking size of parent * displayed objects array so that we don't get array out of bounds execeptions * from isLeaf() or object() when those messages are called after the * TreeAssociation fires a nodesDeleted event. I believe that JTree is * mistakenly rendering those nodes one last time before erasing them. * * Revision 1.8 2001/03/06 23:21:27 mpowers Now only notifying parent if the * object is not registered in the editing context, if any. * * Revision 1.7 2001/02/20 16:38:55 mpowers MasterDetailAssociations now observe * their controlled display group's objects for changes to that the parent * object will be marked as updated. Before, only inserts and deletes to an * object's items are registered. Also, moved ObservableArray to package access. * * Revision 1.6 2001/02/17 16:52:05 mpowers Changes in imports to support * building with jdk1.1 collections. * * Revision 1.5 2001/01/31 17:59:52 mpowers Fixed isLeaf aspect of * TreeAssociation. * * Revision 1.4 2001/01/25 02:16:25 mpowers TreeAssociation now returns * DisplayGroupNode.getUserObject. * * Revision 1.3 2001/01/24 18:14:40 mpowers Fixed problem with leaving children * aspect unspecified. * * Revision 1.2 2001/01/24 16:35:37 mpowers Improved documentation on * TreeAssociation. SortOrderings are now inherited from parent nodes. Updates * after sorting are still lost on TreeController. * * Revision 1.1 2001/01/24 14:17:12 mpowers Major revision to TreeAssociation. * Can now add and remove nodes. DisplayGroupNode is now it's own class. * * */