View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2001 Intersect Software Corporation
4   
5   This library is free software; you can redistribute it and/or
6   modify it under the terms of the GNU Lesser General Public
7   License as published by the Free Software Foundation; either
8   version 2.1 of the License, or (at your option) any later version.
9   
10  This library is distributed in the hope that it will be useful,
11  but WITHOUT ANY WARRANTY; without even the implied warranty of
12  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
13  Lesser General Public License for more details.
14  
15  You should have received a copy of the GNU Lesser General Public
16  License along with this library; if not, see http://www.gnu.org
17  */
18  
19  package net.wotonomy.ui.swing;
20  
21  import java.awt.Component;
22  import java.lang.ref.Reference;
23  import java.lang.ref.WeakReference;
24  import java.util.Enumeration;
25  import java.util.Iterator;
26  import java.util.LinkedList;
27  import java.util.List;
28  import java.util.Vector;
29  
30  import javax.swing.tree.TreePath;
31  
32  import net.wotonomy.control.EODataSource;
33  import net.wotonomy.control.EODelayedObserver;
34  import net.wotonomy.control.EOEditingContext;
35  import net.wotonomy.control.EOObjectStore;
36  import net.wotonomy.control.EOObserverCenter;
37  import net.wotonomy.control.EOQualifier;
38  import net.wotonomy.control.PropertyDataSource;
39  import net.wotonomy.foundation.NSArray;
40  import net.wotonomy.foundation.NSDictionary;
41  import net.wotonomy.foundation.NSMutableDictionary;
42  import net.wotonomy.foundation.NSNotification;
43  import net.wotonomy.foundation.internal.ValueConverter;
44  import net.wotonomy.foundation.internal.WotonomyException;
45  import net.wotonomy.ui.EODisplayGroup;
46  import net.wotonomy.ui.swing.TreeModelAssociation.DelegatingTreeDataSource;
47  
48  /***
49  * DisplayGroupNodes are used as nodes in the
50  * TreeModelAssociation's implementation of TreeModel,
51  * and is tightly coupled with TreeModelAssociation
52  * and MasterDetailAssociation. <br><br>
53  *
54  * Even though it is no longer package access,
55  * don't rely on this class because we want to
56  * have the option of completely replacing this
57  * approach in the future.
58  *
59  * @author michael@mpowers.net
60  * @author $Author: cgruber $
61  * @version $Revision: 904 $
62  */
63      abstract public class DisplayGroupNode
64          extends EODisplayGroup
65      {
66          protected TreeModelAssociation parentAssociation;
67          protected EODelayedObserver targetObserver;
68          protected NSMutableDictionary childNodes;
69          protected EODisplayGroup parentGroup;
70          protected Object target;
71          protected boolean isFetched;
72          protected boolean isFetchNeeded;
73          protected boolean useParentOrderings;
74          protected boolean useParentQualifier;
75  
76          /***
77          * Constructor for all nodes.
78          * Root node must have a null target.
79          */
80          public DisplayGroupNode(
81              TreeModelAssociation aParentAssociation,
82              EODisplayGroup aParentGroup,
83              Object aTarget )
84          {
85  //new net.wotonomy.ui.swing.util.StackTraceInspector( ""+aTarget );
86  //System.out.println( "DisplayGroupNode.new: " + aTarget );            
87              parentAssociation = aParentAssociation;
88              target = null;
89              targetObserver = null;
90              parentGroup = aParentGroup;
91              childNodes = new NSMutableDictionary();
92              isFetched = false;
93              isFetchNeeded = false;
94              useParentOrderings = true;
95              useParentQualifier = true;
96  
97              EODataSource parentSource = null;
98              if ( parentGroup != null )
99              {
100                 parentSource = parentGroup.dataSource();
101             }
102             else
103             if ( parentAssociation.titlesDisplayGroup != null )
104             {
105                 parentSource = parentAssociation.titlesDisplayGroup.dataSource();
106             }
107 
108             // create child datasource
109             if ( aTarget != null ) // not root node
110             {
111                 if ( parentAssociation.childrenKey != null )
112                 {
113                     if ( parentSource == null )
114                     {
115                         throw new WotonomyException(
116                             "Need a data source when children aspect is bound." );
117                     }
118 
119                     NSArray displayedObjects = parentGroup.displayedObjects();
120                     EODataSource childSource = parentSource.dataSourceQualifiedByKey(
121                         parentAssociation.childrenKey );
122                     childSource.qualifyWithRelationshipKey(
123                         parentAssociation.childrenKey, aTarget );
124 
125                     // create new display group using child data source
126                     this.setDataSource( childSource );
127 
128                     // establish observer for target object
129                     setTarget( aTarget );
130                 }
131                 else // only titles is bound
132                 {
133                     // establish observer for target object
134                     setTarget( aTarget );
135                     
136                     setDataSource( new PropertyDataSource() 
137                     {
138                         public NSArray fetchObjects()
139                         {
140                             return new NSArray();
141                         }
142                     } );
143                 }
144             }
145             else // else root node
146             {
147                 // root node uses PropertyDataSource by default
148                 if ( parentSource == null )
149                 {
150                     setDataSource( new PropertyDataSource() 
151                     {
152                         public NSArray fetchObjects()
153                         {
154                             if ( parentGroup != null )
155                             {
156                                 return parentGroup.displayedObjects();
157                             }
158                             return null;
159                         }
160                     } );
161                 }
162                 else
163                 {
164                     // root node uses parent source directly
165                     setDataSource( parentSource );
166                 }
167             }
168         }
169         
170         /***
171         * Overridden to unregister as an editor of the editing context,
172         * since we don't directly present a user interface.
173         */
174         public void setDataSource ( EODataSource aDataSource )
175         {
176             super.setDataSource( aDataSource );
177             if ( ( aDataSource != null )
178             &&   ( aDataSource.editingContext() != null ) )
179             {
180                 aDataSource.editingContext().removeEditor( this );
181             }
182         }
183         
184         /***
185         * Returns whether the node should call fetch().
186         */
187         protected boolean isFetched()
188         {
189             if ( isFetchNeeded() )
190             {
191                 setFetchNeeded( false );
192                 fetch();
193             }
194             return isFetched;
195         }
196 
197         /***
198         * Sets whether the node should call fetch().
199         */
200         protected void setFetched( boolean fetched )
201         {
202 //System.out.println( "DisplayGroupNode.setFetched: " + fetched + " : " + this + " : " + target );
203 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
204             isFetched = fetched;
205         }
206 
207         /***
208         * Returns whether the node is in need of a refetch.
209         */
210         protected boolean isFetchNeeded()
211         {
212             return isFetchNeeded;
213         }
214 
215         /***
216         * Returns whether the node should call fetch().
217         */
218         protected void setFetchNeeded( boolean fetchNeeded )
219         {
220 //System.out.println( "DisplayGroupNode.setFetchNeeded: " + fetchNeeded + " : " + this + " : " + target );
221 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
222             isFetchNeeded = fetchNeeded;
223         }
224 
225         /***
226         * Subclasses should override this method to fire an appropriate insertion event.
227         */
228         protected void fireNodesInserted( Object[] path, int[] indexes, Object[] objects )
229         {
230 //System.out.println( "fireNodesInserted: " + this );            
231             parentAssociation.fireTreeNodesInserted(
232                 this, path, indexes, objects );
233         }
234 
235         /***
236         * Subclasses should override this method to fire an appropriate change event.
237         */
238         protected void fireNodesChanged( Object[] path, int[] indexes, Object[] objects )
239         {
240 //System.out.println( "fireNodesChanged: " + this );            
241             parentAssociation.fireTreeNodesChanged(
242                 this, path, indexes, objects );
243         }
244 
245         /***
246         * Subclasses should override this method to fire an appropriate deletion event.
247         */
248         protected void fireNodesRemoved( Object[] path, int[] indexes, Object[] objects )
249         {
250 //System.out.println( "fireNodesRemoved: " + this );            
251             parentAssociation.fireTreeNodesRemoved(
252                 this, path, indexes, objects );
253         }
254 
255         /***
256         * Subclasses should override this method to fire an appropriate event.
257         */
258         protected void fireStructureChanged( Object[] path, int[] indexes, Object[] objects )
259         {
260             parentAssociation.fireTreeStructureChanged(
261                 this, path, indexes, objects );
262         }
263 
264         /***
265         * Overridden to broadcast a tree event after super executes.
266         */
267         public void insertObjectAtIndex ( Object anObject, int anIndex )
268         {
269             int count = getChildCount(); // gets old count
270             if ( target == null )
271             {
272                 // if root node, forward to parent:
273                 // circumventing delegating data source, if any
274                 EODataSource dataSource = parentGroup.dataSource();
275                 if ( dataSource instanceof DelegatingTreeDataSource )
276                 {
277                     parentGroup.setDataSource( 
278                         ((DelegatingTreeDataSource)dataSource).delegateDataSource );
279                 }
280                 parentGroup.insertObjectAtIndex( anObject, anIndex );
281                 if ( dataSource instanceof DelegatingTreeDataSource )
282                 {
283                     parentGroup.setDataSource( dataSource );
284                 }
285                 return; // prevent event from firing (?)
286             }
287             else // not root node
288             {
289                 super.insertObjectAtIndex( anObject, anIndex );
290             }
291         }
292 
293         /***
294         * Overridden to broadcast a tree event after super executes.
295         */
296         public boolean deleteObjectAtIndex ( int anIndex )
297         {
298             boolean result;
299             Object node = getChildNodeAt( anIndex );
300             if ( target == null )
301             {
302                 // if root node, forward to parent:
303                 result = parentGroup.deleteObjectAtIndex( anIndex );
304             }
305             else // not root node
306             {
307                 result = super.deleteObjectAtIndex( anIndex );
308             }
309 
310             return result;
311         }
312 
313         /***
314         * Returns the child node that corresponds to the
315         * specified index, creating it if necessary.
316         * The index must be within bounds or an exception
317         * is thrown.
318         */
319         public DisplayGroupNode getChildNodeAt( int anIndex )
320         {
321             boolean wasFetched = isFetched();
322             if ( ! wasFetched ) fetch();
323             Object o = displayedObjects.objectAtIndex( anIndex );
324             DisplayGroupNode result = getChildNodeForObject( o );
325             if ( result == null )
326             {
327                 result = createChildNodeForObject( o );
328             }
329             return result;
330         }
331 
332         /***
333         * Returns a child node that corresponds to the
334         * specified object, returning null if not found.
335         */
336         protected DisplayGroupNode getChildNodeForObject( Object anObject )
337         {
338             return (DisplayGroupNode) 
339                 childNodes.objectForKey( new ReferenceKey( anObject ) );
340         }
341 
342         /***
343         * Creates a child node that corresponds to the
344         * specified object.
345         */
346         private DisplayGroupNode createChildNodeForObject( Object anObject )
347         {
348             DisplayGroupNode result = parentAssociation.createNode( this, anObject );
349             childNodes.setObjectForKey( result, new ReferenceKey( anObject ) );
350             return result;
351         }
352 
353         /***
354         * Returns a tree path of all DisplayGroupNodes leading
355         * to this node, including the root node (but excluding the
356         * titles display group).
357         */
358         public TreePath treePath()
359         {
360             List path = new LinkedList();
361             EODisplayGroup node = this;
362             while ( node instanceof DisplayGroupNode )
363             {
364                 // insert at head of list
365                 path.add( 0, node );
366                 node = ((DisplayGroupNode)node).parentGroup;
367             }
368             return new TreePath( path.toArray() );
369         }
370 
371         /***
372         * Overridden to return the parent group's
373         * sort ordering if useParentOrderings is true.
374         * useParentOrderings is true by default.
375         */
376         public NSArray sortOrderings()
377         {
378             if ( ( useParentOrderings )
379             && ( parentGroup != null ) )
380             {
381                 return parentGroup.sortOrderings();
382             }
383             return super.sortOrderings();
384         }
385 
386         /***
387         * Overridden to set useParentOrderings to false,
388         * or true if aList is null.
389         */
390         public void setSortOrderings ( List aList )
391         {
392             if ( aList == null )
393             {
394                 useParentOrderings = true;
395             }
396             else
397             {
398                 useParentOrderings = false;
399                 super.setSortOrderings( aList );
400             }
401         }
402 
403         /***
404         * Overridden to return the parent group's
405         * qualifier if useParentQualifier is true.
406         * useParentQualifier is true by default.
407         */
408         public EOQualifier qualifier()
409         {
410             if ( ( useParentQualifier )
411             && ( parentGroup != null ) )
412             {
413                 return parentGroup.qualifier();
414             }
415             return super.qualifier();
416         }
417 
418         /***
419         * Overridden to set useParentQualifier to false,
420         * or true if aList is null.
421         */
422         public void setQualifier ( EOQualifier aQualifier )
423         {
424             if ( aQualifier == null )
425             {
426                 useParentQualifier = true;
427             }
428             else
429             {
430                 useParentQualifier = false;
431                 super.setQualifier( aQualifier );
432             }
433         }
434 
435         /***
436         * Overridden to set isFetched to true.
437         */
438         public boolean fetch()
439         { 
440 //System.out.println( "DisplayGroupNode.fetch: " + this + " : " );
441 //if ( getClass().getName().indexOf( "Activity" ) != -1 )
442 //{
443 //    new net.wotonomy.ui.swing.util.StackTraceInspector( this.toString() );
444 //}
445             // set flag
446             setFetched( true );
447 
448             // skip root node
449             if ( target == null ) return true;
450 
451             // requalify
452             dataSource().qualifyWithRelationshipKey(
453                 parentAssociation.childrenKey, target );
454 
455             // call to super
456             return super.fetch();
457 
458 //boolean result = super.fetch();
459 //System.out.println( displayedObjects() );
460 //return result;
461         }
462 
463         /***
464         * Returns the object at the appropriate index
465         * in the parent display group.
466         */
467         public Object object()
468         {
469             // if root node
470             if ( target == null )
471             {
472                 return parentAssociation.rootLabel();
473             }
474             return target;
475         }
476 
477         /***
478         * Returns the string value of the title property
479         * on the object in the parent display group corresponding
480         * to this index.  The tree renderer asks JTrees to
481         * call this method to retrieve a value for display.
482         */
483         public String toString()
484         {
485             Object result = getUserObject();
486             if ( result == null ) result = "[null]";
487             return result.toString();
488         }
489 
490         // parts of interface TreeNode
491 
492         public int getChildCount()
493         {
494             if ( ! isFetched() ) fetch();
495 //if ( toString().indexOf("154.16406")!=-1){           
496 //System.out.println( "getChildCount: " + displayedObjects.count() + " : " + this );            
497 //new RuntimeException().printStackTrace();
498 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
499 //}
500             return displayedObjects.count();
501         }
502 
503         public int getIndex(DisplayGroupNode node)
504         {
505             if ( ! isFetched() ) fetch();
506             return displayedObjects.indexOfObject(
507                 ((DisplayGroupNode)node).target );
508         }
509 
510         public boolean getAllowsChildren()
511         {
512             return true;
513         }
514 
515         public boolean isLeaf()
516         {
517             // if not root node and isLeaf aspect is bound
518             if ( ( target != null )
519             &&   ( parentGroup != null )
520             &&   ( parentAssociation.leafKey != null ) )
521             {
522                 Object value;
523                 if ( parentAssociation.leafDisplayGroup != null )
524                 {
525                     value = parentGroup.valueForObject(
526                         target, parentAssociation.leafKey );
527                 }
528                 else
529                 {
530                     value = parentAssociation.leafKey;
531                 }
532 
533                 // getBoolean returns true for zero, among other things
534                 Object result = ValueConverter.getBoolean( value );
535                 if ( result != null )
536                 {
537                     return ((Boolean)result).booleanValue();
538                 }
539             }
540 
541             // otherwise, we have to fetch and return count
542             return ( getChildCount() == 0 );
543         }
544 
545         public Enumeration children()
546         {
547             int count = getChildCount();
548             Vector v = new Vector();
549             for ( int i = 0; i < count; i++ )
550             {
551                 v.add( getChildNodeAt( i ) );
552             }
553             return v.elements();
554         }
555 
556         // parts of interface MutableTreeNode
557 
558         public void insert(DisplayGroupNode aChild, int anIndex)
559         {
560             insertObjectAtIndex(
561                 ((DisplayGroupNode)aChild).object(), anIndex );
562         }
563 
564         public void remove(int index)
565         {
566             deleteObjectAtIndex( index );
567         }
568 
569         /***
570         * Removes the node at the index corresponding
571         * to the index of the object.
572         */
573         public void remove(DisplayGroupNode node)
574         {
575             remove( getIndex( node ) );
576         }
577 
578         /***
579         * Removes our object from the parent display group.
580         */
581         public void removeFromParent()
582         {
583             int index = parentGroup.displayedObjects().indexOfIdenticalObject( target );
584             if ( index != NSArray.NotFound )
585             {
586                 parentGroup.deleteObjectAtIndex( index );
587             }
588             else
589             {
590                 throw new WotonomyException(
591                     "Object not found in parent group: " + target );
592             }
593         }
594 
595         /***
596         * Removes our object from the parent display group
597         * and adds it to the end of the specified node's children.
598         */
599         public void setParent(DisplayGroupNode newParent)
600         {
601             removeFromParent();
602             newParent.insertObjectAtIndex(
603                 object(), newParent.displayedObjects.size() );
604         }
605 
606         /***
607         * Returns the value of the displayed property in the parent display group
608         * at the index that corresponds to the index of this node.
609         */
610         public Object getUserObject()
611         {
612             return valueForKey( parentAssociation.titlesKey );
613         }
614 
615         /***
616         * Sets the value of the displayed property in the parent display group
617         * at the index that corresponds to the index of this node.
618         */
619         public void setUserObject( Object aValue )
620         {
621             setValueForKey( aValue, parentAssociation.titlesKey );
622         }
623 
624         /***
625         * Returns a value from the object in the parent display group
626         * at the index that corresponds to the index of this node.
627         * For the root node, if the titles key is specified, the root
628         * label is returned, otherwise null is returned.
629         */
630         public Object valueForKey( String aKey )
631         {
632             // if root node
633             if ( target == null )
634             {
635                 // compare by ref is okay for strings
636                 if ( aKey == parentAssociation.titlesKey )
637                 {
638                     return parentAssociation.rootLabel();
639                 }
640                 return null;
641             }
642             return parentGroup.valueForObject( target, aKey );
643         }
644 
645         /***
646         * Sets a value on the object in the parent display group
647         * at the index that corresponds to the index of this node.
648         * For the root node, this method only works if aKey is the
649         * titlesAspect's key, otherwise does nothing.
650         */
651         public void setValueForKey(Object aValue, String aKey)
652         {
653             // if root node, return.
654             if ( target == null )
655             {
656                 // compare by ref is okay for strings
657                 if ( aKey == parentAssociation.titlesKey )
658                 {
659                     parentAssociation.setRootLabel( aValue );
660 
661                     // how to handle root node? tree event docs don't say.
662                     fireNodesChanged ( treePath().getPath(),
663                         new int[] { 0 },
664                         new Object[] { this } );
665                 }
666                 return;
667             }
668 
669             parentGroup.setValueForObject(
670                 aValue, target, aKey );
671         }
672 
673         /***
674         * Perform any clean up in this method.
675         * The node will not be reused after this method is called.
676         * This implementation removes itself from the parent's
677         * set of child nodes, sets target and datasource to null,
678         * and then calls disposeChildNodes().
679         */
680         protected void dispose()
681         { //System.out.println( "dispose: " + this.getClass().getName() + " : " + this );
682             if ( parentGroup != null )
683             { 
684                 ((DisplayGroupNode)parentGroup).childNodes.remove( 
685                     new ReferenceKey( target ) );
686             }
687             setTarget( (Object) null );
688             setDataSource( null );
689             disposeChildNodes();
690         }
691         
692         /***
693         * Calls dispose() on all child nodes.
694         */
695         protected void disposeChildNodes()
696         {
697             Iterator i = new LinkedList(childNodes.values()).iterator();
698             while ( i.hasNext() )
699             {
700                 ((DisplayGroupNode) i.next()).dispose();
701             }
702         }
703         
704         /***
705         * Called after the target object posts a change notification.
706         * This implementation re-fetches which triggers
707         * updateDisplayedObjects to broadcast any tree events.
708         * This method marks the parent object as changed if:
709         * (1) this object is not registered in the editing context
710         * of the titles display group's data source (if any), AND
711         * (2) the children key is not in the list of attributes
712         * of the parent object's EOClassDescription.
713         */
714         public void targetChanged()
715         {
716             // if not root node
717             if ( target != null )
718             {
719                 // if we're not root and not fetched, stop here.
720                 //FIXME: with this, some nodes have old values when moved.
721                 //FIXME: without this, nodes are unnecessarily fetched.
722                 //FIXME: might have parent modify isFetched of certain child nodes.
723                 if ( isFetched() )
724                 {
725                     fetch();
726                 }
727                 else // not fetched - just update the display
728                 {
729                     updateDisplayedObjects();
730                 }
731 /*
732 //disabling this for performance reasons:
733 //might reenable later or find an alternate approach
734                 // check to see if we need to mark the parent object as changed
735                 EOEditingContext context = dataSource().editingContext();
736                 if ( ( context == null )
737                 || ( context.globalIDForObject( target ) == null ) )
738                 {
739                     DisplayGroupNode parentNode = (DisplayGroupNode) parentGroup;
740                     if ( parentNode.target != null )
741                     {
742                         // only notify if childrenKey is an attribute of parentDesc
743                         //     (and therefore not a toOne or toMany relationship)
744                         EOClassDescription parentDesc =
745                             EOClassDescription.classDescriptionForClass(
746                                 parentNode.target.getClass() );
747                         if ( parentDesc.attributeKeys().contains( parentAssociation.childrenKey ) )
748                         {
749                             // only notify if no context is already observing the object
750                             //     and we are an attribute key
751                             EOObserverCenter.notifyObserversObjectWillChange( parentNode.target );
752                         }
753                     }
754                 }
755 */
756             }
757             else // root node
758             {
759                 setObjectArray( parentAssociation.titlesDisplayGroup.displayedObjects() );
760             }
761 
762             // finally, broadcast change event for this node
763             //   even though we're not sure if the displayed value changed.
764             fireNodeChanged();
765         }
766 
767         /***
768         * Fires a change event for this node.
769         */
770         public void fireNodeChanged()
771         {
772             // if not root node
773             if ( target != null )
774             {
775                 int index = ((DisplayGroupNode)parentGroup).getIndex( this );
776                 if ( ( index != -1 )
777                 && ( treePath().getParentPath() != null ) )
778                 {
779                     fireNodesChanged (
780                     treePath().getParentPath().getPath(),
781                     new int[] { index },
782                     new Object[] { this } );
783                 }
784             }
785         }
786 
787 Object[] previouslyDisplayedObjects = new Object[0];
788         /***
789         * Overridden to call to super, fire any tree events, and then 
790         * call updateDisplayedObjects on all fetched child nodes.
791         * This method compares this node's displayed objects against
792         * the list of child nodes, synchronizes them, and then broadcasts
793         * only the necessary events to bring the view component up to date.
794         */
795         public void updateDisplayedObjects()
796         { 
797 //System.out.println( "updateDisplayedObjects: " + " : " + this );
798 //net.wotonomy.ui.swing.util.StackTraceInspector.printShortStackTrace();
799 //new RuntimeException().printStackTrace();
800             super.updateDisplayedObjects();
801 
802             // diff lists
803             boolean proceed = true;
804             Object[] oldObjects = previouslyDisplayedObjects;
805             Object[] newObjects = displayedObjects.toArray();
806             if ( oldObjects.length == newObjects.length )
807             {
808                 proceed = false;
809                 for ( int i = 0; i < newObjects.length; i++ )
810                 {
811                     if ( oldObjects[i] != newObjects[i] )
812                     {
813                         proceed = true;
814                         break;
815                     }
816                 }
817             }
818             
819             // this should be set before firing the change events
820             //    in case some clients end up calling this again.
821             previouslyDisplayedObjects = newObjects;
822 
823             DisplayGroupNode node;
824             Iterator i = childNodes.values().iterator();
825             while ( i.hasNext() )
826             {
827                 node = (DisplayGroupNode) i.next();
828                 if ( !node.isFetchNeeded() )
829                 {
830                     node.updateDisplayedObjects();
831                 }
832             }
833             
834             if ( proceed )
835             {
836 //System.out.println( "DisplayGroupNode.firingEventsForChanges: " );                
837 //new RuntimeException().printStackTrace();
838                 fireEventsForChanges( oldObjects, newObjects );
839             }
840 
841         }
842 
843         /***
844         * Called by processRecentChanges to analyze the 
845         * differences between the lists and broadcast the
846         * appropriate events.
847         */
848         protected void fireEventsForChanges(
849             Object[] oldObjects, Object[] newObjects )
850         {
851             // structure changed causes havoc while 
852             //  establishing connection in some cases
853             //if ( oldObjects.length == 0 || newObjects.length == 0 )
854             //{
855             //    fireStructureChanged( treePath().getPath(), null, null );
856             //    return;
857             //}           
858             
859             int insertCount = 0;            
860             int deleteCount = 0;            
861             Object[] inserts = new Object[ newObjects.length ]; 
862             Object[] deletes = new Object[ oldObjects.length ]; 
863             
864             int i;
865             int n = -1, o = -1; // last match
866             int n1 = 0, o1 = 0; // current match test
867             int n2 = 0, o2 = 0; // scan ahead
868 
869             while ( o1 < oldObjects.length && n1 < newObjects.length )
870             {  
871                 if ( newObjects[n1] == oldObjects[o1] )
872                 {  
873                     // mark as match and continue
874                     o = o1;
875                     n = n1;
876                 }
877                 else 
878                 {
879                     // scan ahead for the next match, if any
880                     o2 = o1;
881                     n2 = n1;
882 
883                     while ( o2 < oldObjects.length || n2 < newObjects.length )
884                     {  
885                         if ( o2 < oldObjects.length && newObjects[n1] == oldObjects[o2] )
886                         {
887                             // run o1 to o2: mark as deletes
888                             for ( i = o1; i < o2; i++ )
889                             { // System.out.println( "delete : " + i );
890                                 deletes[i] = oldObjects[i];
891                                 deleteCount++;
892                             }
893                             o1 = o2; // reset test
894                             o = o1; // set match
895                             n = n1; // set match
896                             break;
897                         }
898                         if ( n2 < newObjects.length && newObjects[n2] == oldObjects[o1] )
899                         {
900                             // run n1 to n2: mark as inserts
901                             for ( i = n1; i < n2; i++ )
902                             { // System.out.println( "insert : " + i  );
903                                 inserts[i] = newObjects[i];
904                                 insertCount++;
905                             }
906                             n1 = n2; // reset test
907                             n = n1; // set match
908                             o = o1; // set match
909                             break;
910                         }
911                         o2++;
912                         n2++;
913                     }
914                 }
915                 if (n != n1)
916                 {
917                     inserts[n1] = newObjects[n1];
918                     insertCount++;
919                     deletes[o1] = oldObjects[o1];
920                     deleteCount++;
921                     //increment even though no match:
922                     //the new object was marked as inserted and
923                     //the old object was marked as deleted.
924                     n = n1;
925                     o = o1;
926                 }
927                 o1++;
928                 n1++;
929             }
930             
931             // run o to end of oldObjects: mark as deletes
932             for ( i = o+1; i < oldObjects.length; i++ )
933             { // System.out.println( "delete : " + i  );
934                 deletes[i] = oldObjects[i];
935                 deleteCount++;
936             }
937         
938             // run n to end of newObjects: mark as inserts
939             for ( i = n+1; i < newObjects.length; i++ )
940             { // System.out.println( "insert : " + i  );
941                 inserts[i] = newObjects[i];
942                 insertCount++;
943             }
944 
945 //System.out.println( "done   : " 
946 //+ o + " : " + o1 + " : " + o2 + " :: " + n + " : " + n1 + " : " + n2 );
947 //System.out.println( new NSArray( newObjects ) );
948 //System.out.println( new NSArray( inserts ) );
949 //System.out.println( new NSArray( deletes ) );
950 //System.out.println( new NSArray( oldObjects ) );
951                 
952             int c;
953             Object[] nodes;
954             int[] indices;
955 
956             // broadcast delete event
957             c = 0;
958             nodes = new Object[ deleteCount ];
959             indices = new int[ deleteCount ];
960             for ( i = 0; i < deletes.length; i++ )
961             {
962                 if ( deletes[i] != null )
963                 {
964                     indices[c] = i;
965                     nodes[c] = getChildNodeForObject( deletes[i] );
966                     c++;
967                 }
968             }
969             if ( c > 0 )
970             {
971 //                fireNodeChanged(); // force the jtree to get the correct child count
972                 fireNodesRemoved( treePath().getPath(), indices, nodes );
973             }
974             deletes = nodes; // retain for dispose check
975             
976             // broadcast insert event
977             c = 0;
978             nodes = new Object[ insertCount ];
979             indices = new int[ insertCount ];
980             for ( i = 0; i < inserts.length; i++ )
981             {
982                 if ( inserts[i] != null )
983                 {
984                     indices[c] = i;
985                     nodes[c] = getChildNodeForObject( inserts[i] );
986                     if ( nodes[c] == null )
987                     {
988                         nodes[c] = createChildNodeForObject( newObjects[i] );
989                     }
990                     c++;
991                 }
992             }
993             if ( c > 0 )
994             {
995                 fireNodesInserted( treePath().getPath(), indices, nodes );
996             }
997 
998             // dispose any delete nodes not on insert list
999             int j;
1000             boolean found;
1001             for ( i = 0; i < deletes.length; i++ )
1002             {
1003                 for ( j = 0; j < nodes.length; j++ )
1004                 {
1005                     if ( deletes[i] == nodes[j] ) break;
1006                 }
1007                 
1008                 // did not break early, so not found, so dispose
1009                 if ( j == nodes.length )
1010                 {
1011                     ((DisplayGroupNode)deletes[i]).dispose();
1012                 }
1013             }
1014         }
1015 
1016         /***
1017         * Sets the target object and creates an registers a target observer.
1018         * If target was not previously null, the existing observer is unregistered.
1019         * Protected access so subclasses and TreeModelAssociation can update our target.
1020         */
1021         public void setTarget( Object aTarget )
1022         {
1023             if ( target != null )
1024             {
1025                 EOObserverCenter.removeObserver( targetObserver, target );
1026                 targetObserver.discardPendingNotification();
1027             }
1028 
1029             if ( aTarget != null )
1030             {
1031                 target = aTarget;
1032                 targetObserver = new TargetObserver( this );
1033                 EOObserverCenter.addObserver( targetObserver, target );
1034             }
1035         }
1036 
1037     /***
1038     * Returns the parent display group, or null if parent is root.
1039     */
1040     public DisplayGroupNode getParentGroup()
1041     {
1042         if ( parentGroup instanceof DisplayGroupNode )
1043         {
1044             return (DisplayGroupNode)parentGroup;
1045         }
1046         // presumably the root node
1047         return null;
1048     }
1049         
1050     /***
1051     * Gets all descendants of the this node.
1052     */
1053     public List getDescendants()
1054     {
1055         return getDescendants( this, true );
1056     }
1057 
1058     /***
1059     * Gets only the descendants of the this node
1060     * whose children has been loaded - no fetching
1061     * will occur.  Useful for load-on-demand trees.
1062     */
1063     public List getLoadedDescendants()
1064     {
1065         return getDescendants( this, false );
1066     }
1067 
1068         // breadth first traversal implementation
1069 
1070     /***
1071     * Returns a list of all descendants of the
1072     * specified node. Unfetched nodes are traversed
1073     * only if forceLoad is true.
1074     * This implementation is a breadth-first traversal
1075     * of the nodes starting at the specified node.
1076     */
1077     static private List getDescendants( DisplayGroupNode aNode, boolean forceLoad )
1078     {
1079         if ( !forceLoad && !aNode.isFetched ) return NSArray.EmptyArray;
1080         
1081         LinkedList result = new LinkedList();
1082         LinkedList queue = new LinkedList();
1083 
1084         queue.add( aNode );
1085         while ( ! queue.isEmpty() )
1086         {
1087             checkNode( (DisplayGroupNode) queue.removeFirst(),
1088                 queue, result, forceLoad );
1089         }
1090 
1091         return result;
1092     }
1093 
1094     /***
1095     * Adds each fetched child node of the specified node to
1096     * the result set (optionally forcing the child node to load)
1097     * and adding child node to the end of the queue.
1098     */
1099     static private void checkNode( DisplayGroupNode aNode,
1100         LinkedList aQueue, LinkedList aResult, boolean forceLoad )
1101     {
1102         DisplayGroupNode child;
1103         int count = aNode.getChildCount();
1104 
1105         for ( int i = 0; i < count; i++ )
1106         {
1107             child = aNode.getChildNodeAt( i );
1108 
1109             // add to queue if node has fetched children
1110             if ( ( !child.isFetched ) && ( forceLoad ) )
1111             {
1112                 child.fetch();
1113             }
1114             if ( child.isFetched )
1115             {
1116                 aQueue.addLast( child );
1117             }
1118 
1119             aResult.add( child );
1120         }
1121     }
1122 
1123     /***
1124     * Overridden to not fetch on InvalidateAllObjectsInStoreNotification
1125     * unless we've already been fetched, preserving the load-on-demand
1126     * functionality.
1127     */
1128     public void objectsInvalidatedInEditingContext( NSNotification aNotification )
1129     {
1130         if ( EOObjectStore.InvalidatedAllObjectsInStoreNotification
1131             .equals( aNotification.name() ) )
1132         {
1133 //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: " + aNotification.name() );        
1134                 if ( parentAssociation.isVisible( this ) && targetObserver != null ) 
1135                 {
1136                     targetObserver.objectWillChange( target ); // force ui to update
1137                     fireNodeChanged();
1138                 }
1139                 else // make sure we fetch children when we do become visible
1140                     setFetchNeeded( true ); 
1141                 return;
1142         }
1143         else
1144         if ( ( EOEditingContext.ObjectsChangedInEditingContextNotification
1145             .equals( aNotification.name() ) )
1146         ||   ( EOEditingContext.EditingContextDidSaveChangesNotification
1147             .equals( aNotification.name() ) ) )
1148         {
1149             int index;
1150             Enumeration e;
1151             boolean didChange = false;
1152             NSDictionary userInfo = aNotification.userInfo();
1153 
1154             // if our target object was deleted
1155             NSArray deletes = (NSArray) userInfo.objectForKey(
1156                 EOObjectStore.DeletedKey );
1157             if ( deletes.indexOfIdenticalObject( target ) != NSArray.NotFound )
1158             {
1159 //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: delete: " + this + " : " + aNotification.name() );        
1160                 if ( parentAssociation.isVisible( this ) && targetObserver != null ) 
1161                 {
1162                     targetObserver.objectWillChange( target ); // force ui to update
1163                     fireNodeChanged();
1164                 }
1165                 else // make sure we fetch children when we do become visible
1166                     setFetchNeeded( true ); 
1167                 return;
1168             }
1169 
1170             // if our target object was invalidated
1171             NSArray invalidates = (NSArray) userInfo.objectForKey(
1172                 EOObjectStore.InvalidatedKey );
1173             if ( invalidates != null &&
1174                  invalidates.indexOfIdenticalObject( target ) != NSArray.NotFound )
1175             {
1176 //System.out.println( "DisplayGroupNode.objectsInvalidatedInEditingContext: invalidate: " + this + " : " + aNotification.name() );        
1177                 if ( parentAssociation.isVisible( this ) && targetObserver != null ) 
1178                 {
1179                     targetObserver.objectWillChange( target ); // force ui to update
1180                     fireNodeChanged();
1181                 }
1182                 else // make sure we fetch children when we do become visible
1183                     setFetchNeeded( true ); 
1184                 return;
1185             }
1186             
1187             // if our target object was updated, set fetchNeeded plus fire changed event
1188             NSArray updates = (NSArray) userInfo.objectForKey(
1189                 EOObjectStore.UpdatedKey );
1190             if ( updates.indexOfIdenticalObject( target ) != NSArray.NotFound )
1191             {
1192                 if ( parentAssociation.isVisible( this ) && targetObserver != null ) 
1193                 {
1194                     targetObserver.objectWillChange( target ); // force ui to update
1195                     fireNodeChanged();
1196                     if ( object() instanceof Component ) ((Component)object()).repaint();
1197                 }
1198                 else // make sure we fetch children when we do become visible
1199                     setFetchNeeded( true ); 
1200                 return;
1201             }
1202         }
1203 
1204         super.objectsInvalidatedInEditingContext( aNotification );
1205 
1206     }
1207 
1208     // inner classes
1209 
1210     /***
1211     * Private class used to force a hashmap to
1212     * perform key comparisons by reference.
1213     */
1214     private class ReferenceKey
1215     {
1216         private int hashCode;
1217         private Object referent;
1218 
1219         public ReferenceKey( Object anObject )
1220         {
1221             referent = anObject;
1222             hashCode = anObject.hashCode();
1223         }
1224 
1225         /***
1226         * Returns the actual key's hash code.
1227         */
1228         public int hashCode()
1229         {
1230             return hashCode;
1231         }
1232 
1233         /***
1234         * Compares by reference.
1235         */
1236         public boolean equals( Object anObject )
1237         {
1238             if ( anObject instanceof ReferenceKey )
1239             {
1240                 return ((ReferenceKey)anObject).referent == referent;
1241             }
1242             return false;
1243         }
1244     }
1245 
1246     /***
1247     * A private class to observe the target object of this node.
1248     */
1249     private class TargetObserver extends EODelayedObserver
1250     {
1251         Reference ref;
1252 
1253         /***
1254         * Pass in the display group node that will be updated
1255         * when the target changes.
1256         */
1257         public TargetObserver( DisplayGroupNode aDisplayGroup )
1258         {
1259             ref = new WeakReference( aDisplayGroup );
1260         }
1261 
1262         /***
1263         * Repopulate our display group, and calculate the deltas
1264         * so we can broadcast appropriate events.
1265         */
1266         public void subjectChanged ()
1267         {
1268             DisplayGroupNode node = (DisplayGroupNode) ref.get();
1269             if ( node == null ) return; // node is null if gc'd.
1270             //FIXME: should un-register self from observer center??
1271 
1272             node.targetChanged();
1273         }
1274     }
1275 
1276 }
1277 /*
1278  * $Log$
1279  * Revision 1.2  2006/02/18 23:19:05  cgruber
1280  * Update imports and maven dependencies.
1281  *
1282  * Revision 1.1  2006/02/16 13:22:22  cgruber
1283  * Check in all sources in eclipse-friendly maven-enabled packages.
1284  *
1285  * Revision 1.64  2003/08/06 23:07:52  chochos
1286  * general code cleanup (mostly, removing unused imports)
1287  *
1288  * Revision 1.63  2003/06/06 14:20:07  mpowers
1289  * getLoadedDescendants was forcing a fetch of the node it was called on.
1290  *
1291  * Revision 1.62  2003/06/03 14:48:33  mpowers
1292  * Clean-up of notification handling for updates/invalidation/etc.
1293  * Now fetching immediately on notification if the node is visible.
1294  * This averts the infamous IndexOutOfBoundsException that occurs
1295  * if fetching happens during repaint, because the BasicTreeUI is
1296  * caching the number of child nodes before painting begins.
1297  *
1298  * Revision 1.61  2003/01/18 23:33:29  mpowers
1299  * Fixing the build.
1300  *
1301  * Revision 1.60  2002/05/31 15:03:10  mpowers
1302  * Fixes for the previous fix.  Fat props to yjcheung.
1303  *
1304  * Revision 1.59  2002/05/28 15:31:36  mpowers
1305  * Fix for updateDisplayedObjects for a subtle case where a node appears in
1306  * the position that another node was moved from.
1307  *
1308  * Revision 1.58  2002/05/24 14:42:02  mpowers
1309  * Prevent repeat events from firing if firing events loops back.
1310  *
1311  * Revision 1.57  2002/04/23 19:12:28  mpowers
1312  * Reimplemented fireEventsForChanges.  Fitter and happier.
1313  *
1314  * Revision 1.56  2002/04/19 21:18:45  mpowers
1315  * Removed tree event coalescing, which was causing way too many problems.
1316  * The fireChangeEvent algorithm is way faster than before, so we should
1317  * still be better off than before.  At least now, we don't have to track
1318  * whether the view component has encountered a particular node.
1319  *
1320  * Revision 1.55  2002/04/19 20:53:22  mpowers
1321  * Now firing event fewer events in fireEventsForChanges.
1322  *
1323  * Revision 1.54  2002/04/15 21:52:50  mpowers
1324  * Tightening up TreeModelAssociation and DisplayGroupNode.
1325  * Now only firing root structure changed once.
1326  * Now disposing of root's children.
1327  * Better event coalescing.
1328  *
1329  * Revision 1.53  2002/04/12 20:35:20  mpowers
1330  * Now correctly setting parent display group and data source on creation.
1331  *
1332  * Revision 1.52  2002/04/10 21:20:04  mpowers
1333  * Better handling for tree nodes when working with editing contexts.
1334  * Better handling for invalidation.  No longer broadcasting events
1335  * when nodes have not been "registered" in the tree.
1336  *
1337  * Revision 1.51  2002/04/03 20:13:36  mpowers
1338  * Now differentiating between node instantiation caused by model expansion
1339  * (user initiated) and by modifications to the model.
1340  * Dispose now disposes all children.
1341  *
1342  * Revision 1.50  2002/03/23 16:20:27  mpowers
1343  * Optimized processRecentChanges, minimized tree events.
1344  *
1345  * Revision 1.49  2002/03/11 03:15:06  mpowers
1346  * Optimized processRecentChanges, minimize event firing, coalescing changes.
1347  * Still need a better diff algorithm to avoid removing nodes.
1348  *
1349  * Revision 1.48  2002/03/10 00:59:39  mpowers
1350  * Interim version: coalesces calls to process recent changes.
1351  * Still does not handle rearranged nodes.
1352  *
1353  * Revision 1.47  2002/03/09 17:33:45  mpowers
1354  * Nodes now track their child nodes by reference, not index.
1355  *
1356  * Revision 1.46  2002/03/08 23:19:07  mpowers
1357  * Added getParentGroup to DisplayGroupNode.
1358  *
1359  * Revision 1.45  2002/03/06 13:04:16  mpowers
1360  * Implemented cascading qualifiers in tree nodes.
1361  *
1362  * Revision 1.44  2002/02/27 23:19:17  mpowers
1363  * Refactoring of TreeAssociation to create TreeModelAssociation parent.
1364  *
1365  * Revision 1.42  2002/02/19 22:28:46  mpowers
1366  * DisplayGroupNodes immediately unregister themselves as editors.
1367  *
1368  * Revision 1.41  2002/02/13 16:27:38  mpowers
1369  * Exposing setTarget.
1370  *
1371  * Revision 1.40  2001/11/02 20:55:46  mpowers
1372  * Now using fixed index to send node removed events.  This preserves the
1373  * expanded state of the nodes in the corresponding jtree.
1374  *
1375  * Revision 1.39  2001/09/21 21:09:25  mpowers
1376  * Exposed more fields as protected.
1377  *
1378  * Revision 1.38  2001/09/19 15:36:08  mpowers
1379  * Refined behavior for isFetched after notification handling.
1380  *
1381  * Revision 1.37  2001/09/13 14:51:18  mpowers
1382  * DisplayGroupNodes now dispose themselves and mark their parent for update
1383  * when they receive notification that their target has been deleted.
1384  *
1385  * Revision 1.36  2001/09/10 14:10:24  mpowers
1386  * Fix for notification handling.
1387  *
1388  * Revision 1.35  2001/07/30 16:17:01  mpowers
1389  * Minor code cleanup.
1390  *
1391  * Revision 1.34  2001/07/18 22:13:39  mpowers
1392  * getLoadedDescendants now works as advertised.
1393  * Now correctly handling invalidateAllObjects notification.
1394  *
1395  * Revision 1.33  2001/07/18 13:03:32  mpowers
1396  * TreeNodes now refetch only on demand.  Previously, once a node had
1397  * been fetched, it was always refetched after an invalidate, even if
1398  * the node was not being displayed.
1399  *
1400  * Revision 1.32  2001/06/18 14:10:28  mpowers
1401  * Cleaned up event firing: no longer firing insert or remove events twice.
1402  *
1403  * Revision 1.31  2001/06/09 16:15:39  mpowers
1404  * Revised the targetChanged scheme because oldObjects and newObjects were
1405  * identical after the target object is invalidated.
1406  *
1407  * Revision 1.30  2001/05/21 22:17:19  mpowers
1408  * Fix for tree out-of-synch problems when nodes are inserted.
1409  *
1410  * Revision 1.29  2001/05/18 21:07:46  mpowers
1411  * Playing with refresh options.
1412  *
1413  * Revision 1.28  2001/05/14 15:25:43  mpowers
1414  * DisplayGroupNodes now only respond to InvalidateAllObjectsInStore
1415  * if they are already fetched.
1416  *
1417  * Revision 1.27  2001/05/08 19:55:58  mpowers
1418  * Fix for node children not refreshing after sibling was inserted.
1419  *
1420  * Revision 1.26  2001/05/08 18:47:34  mpowers
1421  * Minor fixes for d3.
1422  *
1423  * Revision 1.25  2001/05/06 22:22:55  mpowers
1424  * Debugging.
1425  *
1426  * Revision 1.24  2001/05/04 14:42:58  mpowers
1427  * Now getting stored values in KeyValueCoding.
1428  * MasterDetail now marks dirty based on whether it's an attribute
1429  * or relation.
1430  * Implemented editing context marker.
1431  *
1432  * Revision 1.23  2001/05/02 18:00:43  mpowers
1433  * Removed debug code.
1434  *
1435  * Revision 1.22  2001/05/02 17:31:20  mpowers
1436  * DisplayGroupNode now does a better job determining when to mark its
1437  * parent dirty.
1438  *
1439  * Revision 1.21  2001/05/01 00:52:32  mpowers
1440  * Implemented breadth-first traversal of tree for node.
1441  *
1442  * Revision 1.20  2001/04/26 01:15:19  mpowers
1443  * Major clean-up of DisplayGroupNode: fitter, happier, more productive.
1444  *
1445  * Revision 1.19  2001/04/22 23:13:35  mpowers
1446  * Minor bug.
1447  *
1448  * Revision 1.18  2001/04/22 23:05:33  mpowers
1449  * Totally revised DisplayGroupNode so each object gets its own node
1450  * (so the nodes are no longer fixed by index).
1451  *
1452  * Revision 1.17  2001/04/21 23:05:12  mpowers
1453  * A fairly major revisiting.  I've decided to scrap the pass-thru approach
1454  * where every node simply represents an index and not an object.
1455  * The next update will have each node correspond to a specific object.
1456  *
1457  * Revision 1.16  2001/04/13 16:37:37  mpowers
1458  * Handling bounds checking.
1459  *
1460  * Revision 1.15  2001/04/03 20:36:01  mpowers
1461  * Fixed refaulting/reverting/invalidating to be self-consistent.
1462  *
1463  * Revision 1.14  2001/03/27 17:45:51  mpowers
1464  * More index bounds checking.
1465  *
1466  * Revision 1.13  2001/03/22 21:25:42  mpowers
1467  * Fixed some nasty issues with jtree's internal state and array bounds.
1468  *
1469  * Revision 1.12  2001/03/19 22:18:58  mpowers
1470  * Root node now mirrors contents of titles display group.
1471  *
1472  * Revision 1.11  2001/03/19 21:38:36  mpowers
1473  * Improved redisplay after edit.  Editing nodes off root now works.
1474  *
1475  * Revision 1.10  2001/03/09 22:08:38  mpowers
1476  * Removed unused line.
1477  *
1478  * Revision 1.9  2001/03/07 16:41:04  mpowers
1479  * Now checking size of parent displayed objects array so that we don't
1480  * get array out of bounds execeptions from isLeaf() or object() when
1481  * those messages are called after the TreeAssociation fires a
1482  * nodesDeleted event.  I believe that JTree is mistakenly rendering
1483  * those nodes one last time before erasing them.
1484  *
1485  * Revision 1.8  2001/03/06 23:21:27  mpowers
1486  * Now only notifying parent if the object is not registered in the
1487  * editing context, if any.
1488  *
1489  * Revision 1.7  2001/02/20 16:38:55  mpowers
1490  * MasterDetailAssociations now observe their controlled display group's
1491  * objects for changes to that the parent object will be marked as updated.
1492  * Before, only inserts and deletes to an object's items are registered.
1493  * Also, moved ObservableArray to package access.
1494  *
1495  * Revision 1.6  2001/02/17 16:52:05  mpowers
1496  * Changes in imports to support building with jdk1.1 collections.
1497  *
1498  * Revision 1.5  2001/01/31 17:59:52  mpowers
1499  * Fixed isLeaf aspect of TreeAssociation.
1500  *
1501  * Revision 1.4  2001/01/25 02:16:25  mpowers
1502  * TreeAssociation now returns DisplayGroupNode.getUserObject.
1503  *
1504  * Revision 1.3  2001/01/24 18:14:40  mpowers
1505  * Fixed problem with leaving children aspect unspecified.
1506  *
1507  * Revision 1.2  2001/01/24 16:35:37  mpowers
1508  * Improved documentation on TreeAssociation.
1509  * SortOrderings are now inherited from parent nodes.
1510  * Updates after sorting are still lost on TreeController.
1511  *
1512  * Revision 1.1  2001/01/24 14:17:12  mpowers
1513  * Major revision to TreeAssociation.  Can now add and remove nodes.
1514  * DisplayGroupNode is now it's own class.
1515  *
1516  *
1517  */
1518