/* 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. * * */