View Javadoc

1   /*
2   Wotonomy: OpenStep design patterns for pure Java applications.
3   Copyright (C) 2000 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.util.Enumeration;
22  import java.util.LinkedList;
23  import java.util.List;
24  import java.util.Vector;
25  
26  import javax.swing.SwingUtilities;
27  import javax.swing.event.TreeModelEvent;
28  import javax.swing.event.TreeModelListener;
29  import javax.swing.event.TreeSelectionEvent;
30  import javax.swing.event.TreeSelectionListener;
31  import javax.swing.tree.TreeModel;
32  import javax.swing.tree.TreePath;
33  import javax.swing.tree.TreeSelectionModel;
34  
35  import net.wotonomy.control.EOClassDescription;
36  import net.wotonomy.control.EODataSource;
37  import net.wotonomy.control.EODelayedObserver;
38  import net.wotonomy.control.EOEditingContext;
39  import net.wotonomy.control.EOObserverCenter;
40  import net.wotonomy.control.EOObserverProxy;
41  import net.wotonomy.control.OrderedDataSource;
42  import net.wotonomy.control.PropertyDataSource;
43  import net.wotonomy.foundation.NSArray;
44  import net.wotonomy.foundation.NSMutableArray;
45  import net.wotonomy.foundation.NSSelector;
46  import net.wotonomy.foundation.internal.WotonomyException;
47  import net.wotonomy.ui.EOAssociation;
48  import net.wotonomy.ui.EODisplayGroup;
49  
50  /***
51  * TreeModelAssociation binds a JTree or similar component
52  * that uses a TreeModel to a display group's
53  * list of displayable objects, each of which may have
54  * a list of child objects managed by another display
55  * group, and so on.  TreeModelAssociation works exactly
56  * like a ListAssociation, with the additional capability
57  * to specify a "Children" aspect, that will allow child
58  * objects to be retrieved from a parent display group.
59  *
60  * <ul>
61  *
62  * <li>titles: a property convertable to a string for
63  * display in the nodes of the tree.  The objects in
64  * the bound display group will be used to populate the
65  * initial root nodes of the tree (more accurately,
66  * children of the offscreen root node in the tree).</li>
67  *
68  * <li>children: a property of a node value that returns
69  * zero, one or many objects, each of which will correspond
70  * to a child node for the corresponding node in the tree.
71  * The data source of the bound display group is replaced 
72  * a data source that populates the display group with
73  * the selection in the tree component as determined by 
74  * calling fetchObjectsIntoChildrenGroup.
75  * If this aspect is not bound, the tree behaves like a list.
76  * <br><br>
77  * Binding this aspect with a null display group is the same
78  * as binding it with the titles display group.  
79  * In this configuration the contents of the titles 
80  * display group will be replaced with the selection in the
81  * tree component, as specified above, replacing the existing 
82  * data source.  
83  * <br><br>
84  * In that case, the display groups for the nodes in
85  * the tree will still use the original data source
86  * for resolving their children key, and programmatically
87  * setting the contents of the display group will still 
88  * repopulate the root nodes of the tree.
89  * </li>
90  *
91  * <li>isLeaf: a property of a node value that returns
92  * a value convertable to a boolean value (aside from
93  * an actual boolean value, zeroes evaluate to true,
94  * as does any String containing "yes" or "true" or that
95  * is convertable to a number equal to zero; other values
96  * evaluate to false).  
97  * <br><br>
98  * If the isLeaf aspect is not bound,
99  * the tree must force nodes to load their children to
100 * determine whether they are leaf nodes (in effect
101 * loading the grandchildren for any expanded node).
102 * If bound, child loading is deferred until the node
103 * is actually expanded.  
104 * <br><br>
105 * For example, binding this value to a null
106 * display group and the key "false" will result in a
107 * deferred-loading tree that works much like Windows
108 * Explorer's network volume browser - all nodes appear
109 * with "pluses" until they are expanded.
110 * <br><br>
111 * Note that the display group is ignored: the property
112 * will be applied directly to the object corresponding
113 * to the node.</li>
114 *
115 * </ul>
116 *
117 * This class acts as the TreeModel for the controlled
118 * component: calling yourcomponent.getModel() will
119 * return this association.  The tree model methods on
120 * this class are public and may be used to affect changes
121 * on the controlled components.<br><br>
122 *
123 * The titles display group's contents are inserted
124 * into a new display group that acts as the root node.
125 * After that point, changes in the titles display group
126 * will cause the tree model to reset itself, creating
127 * a new display group for the root node.
128 * <br><br>
129 *
130 * If a separate display group is bound to the children 
131 * aspect, it will
132 * be used to hold the selected objects and their siblings
133 * and selection will be maintained there, and the titles
134 * display group selection will not be updated.
135 * Any editing or detail associations should in that case
136 * be attached to the children display group, not the titles
137 * group. <br><br>
138 *
139 * Each node in the tree is an EODisplayGroup that
140 * contains the child objects of the object it represents
141 * in the tree.  These objects can be programmatically
142 * inserted, updated, or removed using DisplayGroup
143 * methods.  Each node's takes its parent group's
144 * sortOrderings until a sort ordering is explicitly
145 * specified - setting a sort ordering to null will resume
146 * using the parent group's sort ordering.<br><br>
147 *
148 * Each node in the tree also implements MutableTreeNode.
149 * The value that a node represents is the titles property
150 * value of the object in the parent's displayed objects
151 * list at the index corresponding to the index of the node.
152 * Calling toString on a node returns the string representation
153 * of the titles property value, and setUserObject will update
154 * that value directly in the corresponding object.
155 * Moving a node from one parent to another will remove the
156 * actual object in the parent display group and insert it
157 * into the destination display group.<br><br>
158 *
159 * In short, any nodes obtained from this class'
160 * implementation of TreeModel may be cast as either
161 * EODisplayGroup or MutableTreeNode and maybe be
162 * programmatically manipulated in either manner.
163 *
164 * @author michael@mpowers.net
165 * @author $Author: cgruber $
166 * @version $Revision: 904 $
167 */
168 public class TreeModelAssociation extends EOAssociation
169     implements TreeModel, TreeSelectionListener
170 {
171     static final NSArray aspects =
172         new NSArray( new Object[] {
173             TitlesAspect, ChildrenAspect, IsLeafAspect
174         } );
175     static final NSArray aspectSignatures =
176         new NSArray( new Object[] {
177             AttributeToOneAspectSignature
178         } );
179     static final NSArray objectKeysTaken =
180         new NSArray( new Object[] {
181             "model"
182         } );
183 
184     private final static NSSelector getSelectionModel =
185         new NSSelector( "getSelectionModel" );
186     private final static NSSelector setModel =
187         new NSSelector( "setModel",
188         new Class[] { TreeModel.class } );
189 
190     EODisplayGroup titlesDisplayGroup, childrenDisplayGroup, leafDisplayGroup;
191     String titlesKey, childrenKey, leafKey;
192     DisplayGroupNode rootNode;
193     Vector listeners;
194     Object rootLabel;
195 
196     TreeSelectionModel selectionModel;
197     boolean selectionPaintedImmediately;
198     
199     boolean insertingChild;
200     boolean insertingAfter;
201     
202     EOObserverProxy recentChangesObserver;
203 
204     private boolean pleaseSelectRootNode;
205     
206     /***
207     * Constructor expecting a JTree or any other object
208     * that has void setModel(TreeModel) and TreeModel getSelectionModel()
209     * methods.  This tree association will be used for the TreeModel.
210     * The root node will be labeled "Root".  <br><br>
211     *
212     * As an alternate way to use a TreeModelAssociation, you may pass a
213     * TreeSelectionModel to the constructor and then manually set your
214     * component to use this class as its TreeModel.
215     */
216     public TreeModelAssociation ( Object anObject )
217     {
218         super( anObject );
219 
220         titlesDisplayGroup = null;
221         titlesKey = null;
222         childrenDisplayGroup = null;
223         childrenKey = null;
224         leafDisplayGroup = null;
225         leafKey = null;
226         listeners = new Vector();
227 
228         selectionPaintedImmediately = false;
229         
230         // after display group nodes process recent changes
231         recentChangesObserver = new EOObserverProxy(
232             this, new NSSelector( "processRecentChanges" ), 
233             EODelayedObserver.ObserverPrioritySixth );
234         EOObserverCenter.addObserver( recentChangesObserver, this );
235         
236         insertingChild = true;
237         insertingAfter = true;
238 
239         pleaseSelectRootNode = false;
240 
241         rootLabel = "Root";
242         rootNode = createNode( null, null );
243     }
244 
245     /***
246     * Constructor expecting a JTree or similar component
247     * and specifying a label for the root node.
248     */
249     public TreeModelAssociation( Object anObject, Object aRootLabel )
250     {
251         this( anObject );
252         rootLabel = aRootLabel;
253         rootNode.setUserObject( aRootLabel );
254     }
255 
256     /***
257     * Gets the current root label.
258     */
259     public Object rootLabel()
260     {
261         return rootLabel;
262     }
263 
264     /***
265     * Gets the current root label.
266     */
267     public Object getRootLabel()
268     {
269         return rootLabel();
270     }
271 
272     /***
273     * Sets the root label.
274     */
275     public void setRootLabel( Object aLabel )
276     {
277         rootLabel = aLabel;
278     }
279 
280     /***
281     * Returns a List of aspect signatures whose contents
282     * correspond with the aspects list.  Each element is
283     * a string whose characters represent a capability of
284     * the corresponding aspect. <ul>
285     * <li>"A" attribute: the aspect can be bound to
286     * an attribute.</li>
287     * <li>"1" to-one: the aspect can be bound to a
288     * property that returns a single object.</li>
289     * <li>"M" to-one: the aspect can be bound to a
290     * property that returns multiple objects.</li>
291     * </ul>
292     * An empty signature "" means that the aspect can
293     * bind without needing a key.
294     * This implementation returns "A1M" for each
295     * element in the aspects array.
296     */
297     public static NSArray aspectSignatures ()
298     {
299         return aspectSignatures;
300     }
301 
302     /***
303     * Returns a List that describes the aspects supported
304     * by this class.  Each element in the list is the string
305     * name of the aspect.  This implementation returns an
306     * empty list.
307     */
308     public static NSArray aspects ()
309     {
310         return aspects;
311     }
312 
313     /***
314     * Returns a List of EOAssociation subclasses that,
315     * for the objects that are usable for this association,
316     * are less suitable than this association.
317     */
318     public static NSArray associationClassesSuperseded ()
319     {
320         return new NSArray();
321     }
322 
323     /***
324     * Returns whether this class can control the specified
325     * object.
326     */
327     public static boolean isUsableWithObject ( Object anObject )
328     {
329         return setModel.implementedByObject( anObject );
330     }
331 
332     /***
333     * Returns a List of properties of the controlled object
334     * that are controlled by this class.  For example,
335     * "stringValue", or "selected".
336     */
337     public static NSArray objectKeysTaken ()
338     {
339         return objectKeysTaken;
340     }
341 
342     /***
343     * Returns the aspect that is considered primary
344     * or default.  This is typically "value" or somesuch.
345     */
346     public static String primaryAspect ()
347     {
348         return TitlesAspect;
349     }
350 
351     /***
352     * Returns whether this association can bind to the
353     * specified display group on the specified key for
354     * the specified aspect.
355     */
356     public boolean canBindAspect (
357         String anAspect, EODisplayGroup aDisplayGroup, String aKey )
358     {
359         return ( aspects.containsObject( anAspect ) );
360     }
361 
362     /***
363     * Binds the specified aspect of this association to the
364     * specified key on the specified display group.
365     */
366     public void bindAspect (
367         String anAspect, EODisplayGroup aDisplayGroup, String aKey )
368     {
369         if ( TitlesAspect.equals( anAspect ) )
370         {
371             titlesDisplayGroup = aDisplayGroup;
372             titlesKey = aKey;
373         }
374         if ( ChildrenAspect.equals( anAspect ) )
375         {
376             childrenDisplayGroup = aDisplayGroup;
377             childrenKey = aKey;
378         }
379         if ( IsLeafAspect.equals( anAspect ) )
380         {
381             leafDisplayGroup = aDisplayGroup;
382             leafKey = aKey;
383         }
384         if ( childrenDisplayGroup == null )
385         {
386             childrenDisplayGroup = titlesDisplayGroup;
387         }
388         super.bindAspect( anAspect, aDisplayGroup, aKey );
389     }
390 
391     /***
392     * Establishes a connection between this association
393     * and the controlled object.  Subclasses should begin
394     * listening for events from their controlled object here.
395     */
396     public void establishConnection ()
397     {
398         if ( titlesDisplayGroup == null )
399         {
400             throw new WotonomyException( 
401                 "TreeModelAssociation: Titles aspect must be bound" );
402         }
403 
404         // populate the root node
405         rootNode = createNode( titlesDisplayGroup, null );
406         rootNode.setObjectArray( titlesDisplayGroup.displayedObjects() );
407         rootNode.setSortOrderings( titlesDisplayGroup.sortOrderings() );
408 
409         EODataSource dataSource = childrenDisplayGroup.dataSource();
410         if ( dataSource == null ) dataSource = titlesDisplayGroup.dataSource();
411         while ( dataSource instanceof DelegatingTreeDataSource )
412         {   // unwrap any existing delegating data sources
413             dataSource = ((DelegatingTreeDataSource)dataSource).delegateDataSource;
414         }
415         // create a new delegating data source
416         childrenDisplayGroup.setDataSource( 
417             new DelegatingTreeDataSource( this, dataSource ) );
418 
419         //TODO: find out why omitting this line causes weird selection behavior
420         childrenDisplayGroup.setSortOrderings( new NSArray() );
421             
422         // check for alternate usage
423         if ( object() instanceof TreeSelectionModel )
424         {
425             selectionModel = (TreeSelectionModel) object();
426         }
427         else // use specified object
428         {
429             try
430             {
431                 setModel.invoke( object(), new Object[] { this } );
432                 selectionModel = (TreeSelectionModel)
433                     getSelectionModel.invoke( object(), new Object[] {} );
434             }
435             catch ( Exception exc )
436             {
437                 throw new WotonomyException( exc );
438             }
439         }
440 
441         addAsListener();
442         super.establishConnection();
443 /*
444         fireRootStructureChanged();
445 
446 //        titlesGroupChanged = true;
447 //        subjectChanged();
448         
449         // update the children group
450         removeAsListener();
451         childrenDisplayGroup.fetch();
452         addAsListener();
453         
454         // update selection
455         selectFromDisplayGroup( titlesDisplayGroup );
456 */
457     }
458 
459     protected void fireRootStructureChanged()
460     {
461         int count = rootNode.displayedObjects().count();
462         int[] childIndices = new int[ count ];
463         Object[] children = new Object[ count ];
464         for ( int i = 0; i < count; i++ )
465         {
466             childIndices[i] = i;
467             children[i] = rootNode.getChildNodeAt( i );
468         }
469 
470         // must fire a tree structure changed with children,
471         //   otherwise the tree gets weird selection behavior
472         fireTreeStructureChanged( this, new Object[] { rootNode },
473             childIndices, children );
474     }
475 
476     /***
477     * Breaks the connection between this association and
478     * its object.  Override to stop listening for events
479     * from the object.
480     */
481     public void breakConnection ()
482     {
483         if ( childrenDisplayGroup != null )
484         {
485             if ( childrenDisplayGroup.dataSource() instanceof DelegatingTreeDataSource )
486             {
487                 if ( titlesDisplayGroup == childrenDisplayGroup )
488                 {
489                     titlesDisplayGroup.setDataSource( ((DelegatingTreeDataSource) 
490                         childrenDisplayGroup.dataSource()).delegateDataSource );
491                 }
492                 else
493                 {
494                     childrenDisplayGroup.setDataSource( null );
495                 }
496             }
497         }
498 
499         removeAsListener();
500         super.breakConnection();
501     }
502 
503     protected void addAsListener()
504     {
505         isListening = true;
506         selectionModel.addTreeSelectionListener( this );
507     }
508 
509     protected void removeAsListener()
510     {
511         isListening = false;
512         selectionModel.removeTreeSelectionListener( this );
513     }
514 
515     protected boolean isListening = false;
516     private boolean pleaseIgnore = false;
517     protected boolean titlesGroupChanged = false;
518     protected boolean childrenGroupChanged = false;
519     
520     /***
521     * Overridden to better discriminate what is changed.
522     */
523     public void objectWillChange( Object anObject )
524     {
525         if ( ! isListening ) return;
526         
527         if ( anObject == titlesDisplayGroup )
528         {
529             titlesGroupChanged = true;
530         }
531         if ( anObject == childrenDisplayGroup )
532         {
533             childrenGroupChanged = true;
534             if ( childrenDisplayGroup.qualifier() != null )
535             {
536                 if ( ( rootNode.qualifier() == null ) ||
537                      ! childrenDisplayGroup.qualifier().equals( rootNode.qualifier() ) )
538                 {
539                     // quietly move qualifier from children group to root node
540                     rootNode.setQualifier( childrenDisplayGroup.qualifier() );
541                     childrenDisplayGroup.setQualifier( null );
542                     rootNode.updateDisplayedObjects();
543                 }
544             }
545         }
546         super.objectWillChange( anObject );
547     }
548 
549     /***
550     * Called when either the selection or the contents
551     * of an associated display group have changed.
552     */
553     public void subjectChanged ()
554     {
555         // titles aspect
556         if ( titlesGroupChanged )
557         {
558             if ( titlesDisplayGroup.contentsChanged() )
559             {
560                 NSArray displayedObjects = titlesDisplayGroup.displayedObjects();
561                 NSArray childrenObjects;
562                 if ( titlesDisplayGroup != childrenDisplayGroup 
563                 ||   displayedObjects.count() 
564                         != (childrenObjects = objectsFetchedIntoChildrenGroup()).count() 
565                 || ! displayedObjects.containsAll( childrenObjects ) )
566                 {
567                     populateFromDisplayGroup( displayedObjects );
568                 }
569             }
570         }
571         
572         if ( childrenDisplayGroup.selectionChanged() && !childrenDisplayGroup.contentsChanged() )
573         {
574             selectFromDisplayGroup( childrenDisplayGroup );
575         }
576         
577         titlesGroupChanged = false;
578         childrenGroupChanged = false;
579     }
580 
581     /***
582     * Called by subjectChanged() in response to an external change in the titles display group.
583     */    
584     void populateFromDisplayGroup( List displayedObjects )
585     {
586         // trigger processRecentChanges
587         willChange();
588 
589         // workaround: see below
590         int previousCount = rootNode.previouslyDisplayedObjects.length;        
591         
592         // update the root node
593         rootNode.setObjectArray( displayedObjects );
594         
595         //FIXME: workaround for what appears to be a bug in JTree:
596         // if root node is not visible and has no children, insert events are ignored
597         if ( previousCount == 0 )
598         {
599             fireRootStructureChanged(); 
600         }
601     }
602 
603     /***
604     * Package access so DisplayGroupNode can replace the selection after an update.
605     */
606     void selectFromDisplayGroup( EODisplayGroup aDisplayGroup )
607     {  // System.out.println( "selectFromDisplayGroup: " + aDisplayGroup.selectedObjects() );
608 
609         removeAsListener();
610 
611         TreePath[] paths = selectionModel.getSelectionPaths();
612         NSArray selectedObjects = aDisplayGroup.selectedObjects();
613         
614         // assemble current selection list
615         List treeSelection = new LinkedList();
616         if ( paths != null )
617         {
618             for ( int i = 0; i < paths.length; i ++ )
619             {
620                 treeSelection.add( 
621                     ((DisplayGroupNode)paths[i].getLastPathComponent()).getUserObject() );
622             }
623         }
624 
625         if ( ! ( selectedObjects.size() == treeSelection.size()
626               && treeSelection.containsAll( selectedObjects ) ) )
627         {
628             selectionModel.clearSelection();
629     
630             // workaround to select root node from valueChanged()
631             if ( pleaseSelectRootNode )
632             {
633                 selectionModel.addSelectionPath( new TreePath( this.getRoot() ) );
634                 pleaseSelectRootNode = false;
635             }
636     
637             //FIXME: display group is assumed to have only one instance of each object
638             for ( int i = 0; i < selectedObjects.count(); i++ )
639             {
640                 //FIXME: selects only the first instance for now
641                 //selectionModel.addSelectionPaths(
642                 //    getPathsForObject(
643                 //        selectedObjects.objectAtIndex( i ) ) );
644                 selectionModel.addSelectionPath(
645                     getPathForObject(
646                         selectedObjects.objectAtIndex( i ) ) );
647             }
648         }
649 
650         addAsListener();
651     }
652 
653     /***
654     * Returns the first node found that represents the
655     * specified object, or null if not found.
656     * This implementation simply calls getPathForObject.
657     */
658     public Object getNodeForObject( Object anObject )
659     {
660         TreePath result = getPathForObject( anObject );
661         if ( result != null )
662         {
663             return result.getLastPathComponent();
664         }
665         return null;
666     }
667 
668     /***
669     * Returns the object represented by the specified node
670     * which must be a display group node from this tree.
671     */
672     public Object getObjectForNode( Object aNode )
673     {
674         if ( aNode instanceof DisplayGroupNode )
675         {
676             return ((DisplayGroupNode)aNode).getUserObject();
677         }
678 
679         // not a display group node
680         throw new WotonomyException(
681             "Not a display group node: " + aNode );
682     }
683 
684     /***
685     * Returns the tree path for the specified node,
686     * which must be a display group node from this tree.
687     */
688     public TreePath getPathForNode( Object aNode )
689     {
690         if ( aNode instanceof DisplayGroupNode )
691         {
692             return ((DisplayGroupNode)aNode).treePath();
693         }
694 
695         // not a display group node
696         throw new WotonomyException(
697             "Not a display group node: " + aNode );
698     }
699 
700     /***
701     * Returns the first tree path for the node that represents
702     * the specified object, or null if the object does not exist in this tree.
703     * This implementation does a breadth-first search of the tree
704     * for the object, looking only at nodes that have been loaded.
705     * This means that if the object does not exist in the tree,
706     * the entire tree must be traversed.
707     */
708     public TreePath getPathForObject( Object anObject )
709     {
710         return getPathForObjectInPath( anObject, new TreePath( this.getRoot() ) );
711     }
712 
713     /***
714     * Returns the tree path for the node that represents
715     * the specified object,
716     * or null if the object does not exist in this tree.
717     * This implementation does a breadth-first search of the tree
718     * for the object, looking only at nodes that have been loaded.
719     * This means that the entire tree is traversed.
720     */
721     public TreePath[] getPathsForObject( Object anObject )
722     {
723         return getPathsForObjectInPath( anObject, new TreePath( this.getRoot() ) );
724     }
725 
726     /***
727     * A breadth-first search of the tree starting
728     * at the specified tree path, comparing by reference.
729     * Returns immediately with the first match.
730     */
731     private TreePath getPathForObjectInPath( Object anObject, TreePath aPath )
732     {
733         LinkedList queue = new LinkedList();
734 
735         // add the specified path
736         queue.addLast( aPath );
737 
738         return processQueue( anObject, queue, null );
739     }
740 
741     /***
742     * A breadth-first search of the tree starting
743     * at the specified tree path, comparing by reference.
744     * The entire branch is searched before returning
745     * an array of all matches.
746     */
747     private TreePath[] getPathsForObjectInPath( Object anObject, TreePath aPath )
748     {
749         LinkedList queue = new LinkedList();
750 
751         // add the specified path
752         queue.addLast( aPath );
753 
754         List result = new LinkedList();
755         processQueue( anObject, queue, result );
756         TreePath[] paths = new TreePath[ result.size() ];
757         for ( int i = 0; i < paths.length; i++ )
758         {
759             paths[i] = (TreePath) result.get(i);
760         }
761         return paths;
762     }
763 
764     /***
765     * Processes the specified queue, appending results to aResult if it exists,
766     * or returning immediately with a TreePath is aResult is null.
767     */
768     private TreePath processQueue( Object anObject, LinkedList aQueue, List aResult )
769     {
770         TreePath path;
771         while ( ! aQueue.isEmpty() )
772         {
773             path = (TreePath) aQueue.removeFirst();
774             path = checkNode( anObject, path, aQueue );
775             if ( path != null )
776             {
777                 if ( aResult != null )
778                 {
779                     aResult.add( path );
780                 }
781                 else
782                 {
783                     return path;
784                 }
785             }
786         }
787         return null;
788     }
789 
790     /***
791     * Compares the specified object by reference each of the children of
792     * the node at the end of the specified tree path, adding nodes that
793     * do not match but have fetched object to the end of the specified queue.
794     * Returns the path of the first child node that matches the specified object,
795     * or null if no match was found.
796     */
797     private TreePath checkNode( Object anObject, TreePath aPath, LinkedList aQueue )
798     {
799         TreePath result = null;
800         Object child;
801         Object parent = aPath.getLastPathComponent();
802         int count = getChildCount( parent );
803 
804         for ( int i = 0; i < count; i++ )
805         {
806             child = getChild( parent, i );
807 
808             // add to queue if node has fetched children
809             if ( ((DisplayGroupNode)child).isFetched )
810             {
811                 aQueue.addLast( aPath.pathByAddingChild( child ) );
812             }
813 
814             // compares by reference
815             if ( ((DisplayGroupNode)child).object() == anObject )
816             {
817                 // assumes same object cannot be in display group twice
818                 result = aPath.pathByAddingChild( child );
819 //System.out.println( "TRUE: " + ((DisplayGroupNode)child).object() + " == " + anObject );
820             }
821 //            else
822 //            {
823 //System.out.println( ((DisplayGroupNode)child).object() + " != " + anObject );
824 //            }
825         }
826         return result;
827     }
828 
829     // interface TreeSelectionListener
830 
831     public void valueChanged(TreeSelectionEvent e)
832     {
833         if ( ! isListening ) return;
834         selectFromSelectionModel();
835     }
836 
837     /***
838     * Determines whether the selection should be painted
839     * immediately after the user clicks and therefore
840     * before the children display group is updated.
841     * When the children group is bound to many associations
842     * or is bound to a master-detail association, updating
843     * the display group can take a perceptibly long time.
844     * This property defaults to false.
845     * @see #setSelectionPaintedImmediately
846     */
847     public boolean isSelectionPaintedImmediately()
848     {
849         return selectionPaintedImmediately;
850     }
851 
852     /***
853     * Sets whether the selection should be painted immediately.
854     * Setting this property to true will let the tree paint
855     * first before the display group is updated.
856     * This means that any tree selection listers will
857     * also be notified before the display group is updated
858     * and will have to invokeLater if they want to see the
859     * updated display group.
860     */
861     public void setSelectionPaintedImmediately( boolean isImmediate )
862     {
863         selectionPaintedImmediately = isImmediate;
864     }
865 
866     /***
867     * Package access so DisplayGroupNode can replace the selection.
868     * Returns the display group containing the current selection: titles or children.
869     */
870     EODisplayGroup selectFromSelectionModel()
871     {  // System.out.print( "selectFromSelectionModel: " );
872         removeAsListener();
873         DisplayGroupNode node;
874         TreePath parentPath;
875         TreePath[] selectedPaths = selectionModel.getSelectionPaths();
876         NSMutableArray selectionList = new NSMutableArray();
877         if ( selectedPaths != null )
878         {
879             for ( int i = 0; i < selectedPaths.length; i++ )
880             {
881                 // root node is zero - ignore root node
882                 if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) )
883                 {
884                     // select root in selectFromDisplayGroup()
885                     pleaseSelectRootNode = true;
886                 }
887                 else
888                 {
889                     node = (DisplayGroupNode)
890                         selectedPaths[i].getLastPathComponent();
891                     Object o = node.object();
892 
893                     if ( selectionList.indexOfIdenticalObject(o) == NSArray.NotFound )
894                     {
895                         selectionList.addObject( o );
896                     }
897                 }
898             }
899         }
900         childrenDisplayGroup.fetch(); //note that we're not currently listening for changes
901         if ( ! childrenDisplayGroup.selectObjectsIdenticalTo( selectionList ) )
902         {
903             addAsListener(); // because we don't have a listener stack 
904             selectFromDisplayGroup( childrenDisplayGroup );
905             removeAsListener();
906         }
907         addAsListener();
908         return childrenDisplayGroup; // titles is now children if children not explicitly set
909     }
910 
911     // interface TreeModel
912 
913     public Object getRoot()
914     {
915         return rootNode;
916     }
917 
918     public Object getChild(Object parent, int index)
919     {
920         // interestingly, this gets called by
921         // BasicTreeUI.paintVerticalPartOfLeg for
922         // the last child of each expanded tree node.
923         Object result = ((DisplayGroupNode)parent).getChildNodeAt( index );
924 //((DisplayGroupNode)parent).suppressRecentChangeProcessing();
925 return result;
926 //        return ((DisplayGroupNode)parent).getChildNodeAt( index );
927     }
928 
929     public int getChildCount(Object parent)
930     {
931         int result = ((DisplayGroupNode)parent).getChildCount();
932 //((DisplayGroupNode)parent).suppressRecentChangeProcessing();
933 return result;
934 //        return ((DisplayGroupNode)parent).getChildCount();
935     }
936 
937     public boolean isLeaf(Object node)
938     {
939         boolean result = ((DisplayGroupNode)node).isLeaf();
940 //((DisplayGroupNode)node).suppressRecentChangeProcessing();
941 return result;
942 //        return ((DisplayGroupNode)node).isLeaf();
943     }
944 
945     /***
946     * Returns whether this node is visible in the UI.
947     * This implementation returns true.  
948     * <br><br> 
949     * Subclasses should return false if they can 
950     * determine that the node is not displayed or 
951     * expanded or otherwise visible.  Non-visible 
952     * nodes will fetch only when they are shown.
953     */
954     public boolean isVisible(Object node)
955     {
956         return true;
957     }
958 
959     public void valueForPathChanged(TreePath path, Object newValue)
960     {
961         ((DisplayGroupNode)path.getLastPathComponent()).setUserObject( newValue );
962     }
963 
964     public int getIndexOfChild(Object parent, Object child)
965     {
966         int result = ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child );
967 //((DisplayGroupNode)parent).suppressRecentChangeProcessing();
968 return result;
969 //        return ((DisplayGroupNode)parent).getIndex( (DisplayGroupNode) child );
970     }
971 
972     public void addTreeModelListener(TreeModelListener aListener)
973     {
974         listeners.add( aListener );
975     }
976     public void removeTreeModelListener(TreeModelListener aListener)
977     {
978         listeners.remove( aListener );
979     }
980 
981     /***
982     * Fires a tree nodes changed event to all listeners.
983     * Provided as a convenience if you need to make manual
984     * changes to the tree model.
985     */
986     public void fireTreeNodesChanged(Object source,
987                                     Object[] path,
988                                     int[] childIndices,
989                                     Object[] children)
990     {
991         
992         willChange(); // queue processRecentChanges
993         TreeModelEvent event = new TreeModelEvent(
994             source, path, childIndices, children );
995 //System.out.println( "fireTreeNodesChanged: " + event );
996         Enumeration it = listeners.elements();
997         while ( it.hasMoreElements() )
998         {
999             try
1000             {
1001                 ((TreeModelListener)it.nextElement()).treeNodesChanged( event );
1002             }
1003             catch ( Exception exc )
1004             {
1005                 System.out.println( "TreeModelAssociation.fireTreeNodesChanged: caught: " + exc );
1006                 System.out.println( "Source:" + source );
1007                 System.out.println( "Path:" );
1008                 for ( int i = 0; i < path.length; i++ )
1009                 {
1010                      System.out.print( path[i] + "-" );
1011                 }
1012                 System.out.println();
1013                 System.out.println( "Indices:" );
1014                 for ( int i = 0; i < childIndices.length; i++ )
1015                 {
1016                      System.out.print( childIndices[i] + "-" );
1017                 }
1018                 System.out.println();
1019                 System.out.println( "Children:" );
1020                 for ( int i = 0; i < children.length; i++ )
1021                 {
1022                      System.out.print( children[i] + "-" );
1023                 }
1024                 System.out.println();
1025                 exc.printStackTrace();
1026             }
1027         }
1028     }
1029 
1030     /***
1031     * Fires a tree nodes inserted event to all listeners.
1032     * Provided as a convenience if you need to make manual
1033     * changes to the tree model.
1034     */
1035     public void fireTreeNodesInserted(Object source,
1036                                      Object[] path,
1037                                      int[] childIndices,
1038                                      Object[] children)
1039     {
1040         
1041         willChange(); // queue processRecentChanges
1042         TreeModelEvent event = new TreeModelEvent(
1043             source, path, childIndices, children );
1044 //System.out.println( "fireTreeNodesInserted: " + event );
1045         Enumeration it = listeners.elements();
1046         while ( it.hasMoreElements() )
1047         {
1048             try
1049             {
1050                 ((TreeModelListener)it.nextElement()).treeNodesInserted( event );
1051             }
1052             catch ( Exception exc )
1053             {
1054                 System.out.println( "TreeModelAssociation.fireTreeNodesInserted: caught: " + exc );
1055             }
1056         }
1057     }
1058 
1059     /***
1060     * Fires a tree nodes removed event to all listeners.
1061     * Provided as a convenience if you need to make manual
1062     * changes to the tree model.
1063     */
1064     public void fireTreeNodesRemoved(Object source,
1065                                     Object[] path,
1066                                     int[] childIndices,
1067                                     Object[] children)
1068     {
1069         
1070         willChange(); // queue processRecentChanges
1071         TreeModelEvent event = new TreeModelEvent(
1072             source, path, childIndices, children );
1073 //System.out.println( "fireTreeNodesRemoved: " + event );
1074         Enumeration it = listeners.elements();
1075         while ( it.hasMoreElements() )
1076         {
1077             try
1078             {
1079                 //NOTE: removing nodes causes tree to fire selection change event
1080                 // which confuses us if we're rearranging nodes (when sorting, for example).
1081                 boolean wasListening = isListening;                
1082                 if ( wasListening ) isListening = false;
1083                 ((TreeModelListener)it.nextElement()).treeNodesRemoved( event );
1084                 if ( wasListening ) isListening = true;
1085             }
1086             catch ( Exception exc )
1087             {
1088                 System.out.println( "TreeModelAssociation.fireTreeNodesRemoved: caught: " + exc );
1089             }
1090         }
1091     }
1092 
1093     /***
1094     * Fires a tree structure changed event to all listeners.
1095     * Provided as a convenience if you need to make manual
1096     * changes to the tree model.
1097     */
1098     public void fireTreeStructureChanged(Object source,
1099                                     Object[] path,
1100                                     int[] childIndices,
1101                                     Object[] children)
1102     {
1103         
1104         willChange(); // queue processRecentChanges
1105         TreeModelEvent event = new TreeModelEvent(
1106             source, path, childIndices, children );
1107 //System.out.println( "fireStructureChanged: " + event );
1108         Enumeration it = listeners.elements();
1109         while ( it.hasMoreElements() )
1110         {
1111             ((TreeModelListener)it.nextElement()).treeStructureChanged( event );
1112         }
1113     }
1114 
1115     /***
1116     * Creates and returns a new display group node.
1117     */
1118     public DisplayGroupNode createNode( EODisplayGroup aParentGroup, Object anObject )
1119     {
1120         return new MutableDisplayGroupNode( this, aParentGroup, anObject );
1121     }
1122     
1123     /***
1124     * Gets whether new objects programmatically inserted into the children
1125     * display group should be inserted as a child of the first selected node.
1126     * If false, new objects are inserted as siblings of the first selected node.
1127     * Default value is true.
1128     */
1129     public boolean isInsertingChild()
1130     {
1131         return insertingChild;
1132     }
1133     
1134     /***
1135     * Sets whether new objects programmatically inserted into the children
1136     * display group should be inserted as a child of the first selected node.
1137     * If false, new objects are inserted as siblings of the first selected node.
1138     * Default value is true.
1139     */
1140     public void setInsertingChild( boolean asChild )
1141     {
1142         insertingChild = asChild;
1143     }
1144     
1145     /***
1146     * Determines where new objects programmatically inserted into the children
1147     * display group should be inserted, based on the value of insertingChild.
1148     * If insertingChild, isInsertingAfter causes objects to be inserted at 
1149     * the end of the selected node's child list; otherwise, objects are inserted
1150     * at the beginning of the list.  
1151     * If inserting as a sibling, isInsertingAfter causes objects to be inserted
1152     * before the selected node in the selected node's parent's child list; 
1153     * otherwise, objects are inserted after the selected node in the child list.
1154     * Default value is true.
1155     */
1156     public boolean isInsertingAfter()
1157     {
1158         return insertingAfter;
1159     }
1160     
1161     /***
1162     * Determines where new objects programmatically inserted into the children
1163     * display group should be inserted, based on the value of insertingChild.
1164     * If insertingChild, isInsertingAfter causes objects to be inserted at 
1165     * the end of the selected node's child list; otherwise, objects are inserted
1166     * at the beginning of the list.  
1167     * If inserting as a sibling, isInsertingAfter causes objects to be inserted
1168     * before the selected node in the selected node's parent's child list; 
1169     * otherwise, objects are inserted after the selected node in the child list.
1170     * Default value is true.
1171     */
1172     public void setInsertingAfter( boolean after )
1173     {
1174         insertingAfter = after;
1175     }
1176     
1177     /***
1178     * Called to by the children group's data source when it receives
1179     * an insertObject message, usually after an object has been inserted
1180     * into the children display group.  
1181     * Return the object that should be passed to the titles display 
1182     * group's data source's implementation of insertObject, or return 
1183     * null to prevent that method from being called. <br><br>
1184     * This implementation inserts the specified object into the tree
1185     * as determined by calling isInsertingChild and isInsertingAfter,
1186     * then returns the unmodified object.  If there's no selection, or
1187     * no selection model, the root node is assumed to be selected.  
1188     * And if the root node is selected, the new node will obviously be 
1189     * inserted as a child.  Override to customize.
1190     */
1191     protected Object objectInsertedIntoChildrenGroup( Object anObject )
1192     {
1193         // determine selection
1194         DisplayGroupNode selectedNode = (DisplayGroupNode) getRoot();
1195         if ( selectionModel != null )
1196         {
1197             // get selected path
1198             TreePath path = selectionModel.getSelectionPath();
1199         
1200             // get selected node
1201             if ( path != null )
1202             {
1203                 selectedNode = (DisplayGroupNode) path.getLastPathComponent();
1204             }
1205         }
1206         // determine location of insertion
1207         int index = 0;
1208         if ( ( isInsertingChild() ) || ( selectedNode == getRoot() ) )
1209         {
1210             if ( isInsertingAfter() )
1211             {
1212                 index = selectedNode.getChildCount();
1213             }
1214         }
1215         else // inserting as sibling
1216         {
1217             DisplayGroupNode parentNode = selectedNode.getParentGroup();
1218             index = parentNode.getIndex( selectedNode );
1219             if ( isInsertingAfter() )
1220             {
1221                 index++;
1222             }
1223             selectedNode = parentNode;
1224         }
1225         
1226         // insert and return
1227         selectedNode.insertObjectAtIndex( anObject, index );
1228         return anObject;
1229     }
1230 
1231     /***
1232     * Called to by the children group's data source when it receives
1233     * a deleteObject message, usually after an object has been deleted
1234     * from the children display group.  
1235     * Return the object that should be passed to the titles display 
1236     * group's data source's implementation of deleteObject, or return 
1237     * null to prevent that method from being called. <br><br>
1238     * This implementation deletes all instances of the selected object
1239     * from the tree nodes that are currently loaded, and returns the 
1240     * unmodified object.  Override to customize.
1241     */
1242     protected Object objectDeletedFromChildrenGroup( Object anObject )
1243     {
1244         TreePath[] paths = getPathsForObject( anObject );
1245         if ( paths != null )
1246         {
1247             for ( int i = 0; i < paths.length; i++ )
1248             {
1249                 ((DisplayGroupNode)paths[i].getLastPathComponent()).removeFromParent();
1250             }
1251         }
1252         return anObject;
1253     }
1254 
1255     /***
1256     * Called to by the children group's data source to populate it
1257     * with all selected nodes and their siblings.  To customize,
1258     * override this method, or specify a different data source for
1259     * the children display group.
1260     */
1261     protected NSArray objectsFetchedIntoChildrenGroup()
1262     {
1263         DisplayGroupNode node;
1264         TreePath parentPath;
1265         TreePath[] selectedPaths = selectionModel.getSelectionPaths();
1266         NSMutableArray objectList = new NSMutableArray();
1267         if ( selectedPaths != null )
1268         {
1269             for ( int i = 0; i < selectedPaths.length; i++ )
1270             {
1271                 // root node is zero - ignore root node
1272                 if ( ( selectedPaths[i].getLastPathComponent() == rootNode ) )
1273                 {
1274                         // select root in selectFromDisplayGroup()
1275                         pleaseSelectRootNode = true;
1276                 }
1277                 else
1278                 {
1279                     node = (DisplayGroupNode)
1280                         selectedPaths[i].getLastPathComponent();
1281                     Object o = node.object();
1282 
1283                     // add all children of parent to object list - includes self
1284                     if ( node.parentGroup != null )
1285                     {
1286                         Enumeration e =
1287                             node.parentGroup.displayedObjects().objectEnumerator();
1288                         while ( e.hasMoreElements() )
1289                         {
1290                             // add only if not already in list
1291                             o = e.nextElement();
1292                             if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound )
1293                             {
1294                                 objectList.addObject( o );
1295                             }
1296                         }
1297                     }
1298                     else // no parent node - add the node by itself
1299                     {
1300                         // add only if not already in list
1301                         if ( objectList.indexOfIdenticalObject(o) == NSArray.NotFound )
1302                         {
1303                             objectList.addObject( o );
1304                         }
1305                     }
1306                 }
1307             }
1308         }
1309         
1310         // if no selection
1311         if ( objectList.size() == 0 )
1312         {
1313             // populate with children of root
1314             objectList.addAll( rootNode.displayedObjects() );
1315         }
1316         return objectList;
1317     }
1318 
1319     /***
1320     * Queues processRecentChanges to be run in the event queue.
1321     */
1322     private void willChange()
1323     {
1324         EOObserverCenter.notifyObserversObjectWillChange( this );
1325     }
1326     
1327     /***
1328     * Tells the children display group to refetch, so that it reflects
1329     * any changes that were made in the node tree,
1330     * and then updates the selection in the selection model.
1331     * Triggered in response to willChange().
1332     */
1333     public void processRecentChanges()
1334     {
1335         Runnable update = new Runnable()
1336         {
1337             public void run()
1338             {
1339                 removeAsListener(); // prevent data source refetch: see fetchObjects()
1340                 childrenDisplayGroup.fetch();
1341                 addAsListener();
1342                 selectFromDisplayGroup( childrenDisplayGroup );
1343             }
1344         };
1345         if ( isListening )
1346         {
1347             if ( selectionPaintedImmediately )
1348             {
1349                 // if painting selection immediately, run even later
1350                 //   so that AWT's repaint event fires before we do.
1351                 SwingUtilities.invokeLater( update );
1352             }
1353             else
1354             {
1355                 // otherwise run now
1356                 update.run();
1357             }
1358         }
1359     }
1360 
1361     /***
1362     * Delegates most behaviors to the specified data source,
1363     * except fetchObjects, which calls fetchObjectsIntoChildrenGroup
1364     * on the tree model association.  If delegate is null,
1365     * calls are passed to the superclass which is a PropertyDataSource.
1366     */
1367     static class DelegatingTreeDataSource extends PropertyDataSource
1368     {
1369         TreeModelAssociation parentAssociation;
1370         EODataSource delegateDataSource;
1371         
1372         public DelegatingTreeDataSource( 
1373             TreeModelAssociation aTreeModelAssociation, EODataSource aDataSource )
1374         {
1375             parentAssociation = aTreeModelAssociation;
1376             delegateDataSource = aDataSource;
1377         }
1378         
1379         /***
1380         * Calls to delegateDataSource if it exists, otherwise
1381         * calls to super.
1382         */
1383         public Object createObject()
1384         { 
1385             if ( delegateDataSource != null ) 
1386             {
1387                 return delegateDataSource.createObject();
1388             }
1389             return super.createObject();
1390         }
1391     
1392         /***
1393         * Calls objectInsertedIntoChildrenGroup, and if not null
1394         * calls to delegateDataSource.insertObject if it exists,
1395         * and super.insertObjectAtIndex if not.
1396         */
1397         public void insertObjectAtIndex( Object anObject, int anIndex )
1398         {
1399             anObject = 
1400                 parentAssociation.objectInsertedIntoChildrenGroup( 
1401                     anObject );
1402             if ( anObject != null )
1403             {
1404                 if ( delegateDataSource != null ) 
1405                 {
1406                     if ( delegateDataSource instanceof OrderedDataSource )
1407                     {
1408                         ((OrderedDataSource)delegateDataSource).insertObjectAtIndex( anObject, anIndex );
1409                     }
1410                     else
1411                     {
1412                         delegateDataSource.insertObject( anObject );
1413                     }
1414                 }
1415                 else
1416                 {
1417                     super.insertObjectAtIndex( anObject, anIndex );
1418                 }
1419             }
1420         }
1421 
1422         /***
1423         * Calls objectDeletedIntoChildrenGroup, and if not null
1424         * calls to delegateDataSource if it exists.
1425         */
1426         public void deleteObject( Object anObject )
1427         {
1428             anObject = 
1429                 parentAssociation.objectDeletedFromChildrenGroup( 
1430                     anObject );
1431             if ( anObject != null )
1432             {
1433                 if ( delegateDataSource != null ) 
1434                 {
1435                     delegateDataSource.deleteObject( anObject );
1436                 }
1437                 super.deleteObject( anObject );
1438             }
1439         }
1440 
1441         /***
1442         * Overridden to return the delegate's editing context,
1443         * the titles display group's editing context, 
1444         * and failing that calling to super.
1445         */
1446         public EOEditingContext editingContext ()
1447         {
1448             EOEditingContext result = null;
1449             if ( delegateDataSource != null ) 
1450             {
1451                 result = delegateDataSource.editingContext();
1452             }
1453             if ( result == null )
1454             {
1455                 EODataSource parentDataSource = 
1456                     parentAssociation.titlesDisplayGroup.dataSource();
1457                 if ( parentDataSource != this && parentDataSource != null )
1458                 {
1459                     result = parentAssociation.titlesDisplayGroup.
1460                         dataSource().editingContext();
1461                 }
1462             }
1463             if ( result == null )
1464             {
1465                 result = super.editingContext();
1466             }
1467             return result;
1468         }
1469 
1470         /***
1471         * Returns a List containing the objects in this
1472         * data source.
1473         */
1474         public NSArray fetchObjects ()
1475         {
1476             // if titles group is doing double-duty as children group
1477             if ( parentAssociation.titlesDisplayGroup == parentAssociation.childrenDisplayGroup )
1478             {
1479                 // if we're not initiating this fetch
1480                 if ( parentAssociation.isListening )
1481                 {
1482                     // need to call to delegate to see if we should update values
1483                     if ( delegateDataSource != null )
1484                     {
1485 // System.out.println( "fetching from delegate (slow!)" );            
1486                         NSArray result = delegateDataSource.fetchObjects();
1487                         NSArray rootObjects = parentAssociation.rootNode.displayedObjects();
1488                         // if titles data source has different objects, return them
1489                         if ( rootObjects.count() != result.count() 
1490                         || ! rootObjects.containsAll( result ) )
1491                         {
1492                             // this will force the root node to repopulate in subjectChanged()
1493 //System.out.println( "fetchObjects: data source" );            
1494                             return result;
1495                         }
1496                     }
1497                 }
1498             }
1499             // otherwise: just repopulate the titles group
1500 //System.out.println( "fetchObjects: objectsFetchedIntoChildrenGroup" );            
1501             return parentAssociation.objectsFetchedIntoChildrenGroup();
1502         }
1503         
1504         /***
1505         * Returns a data source that is capable of
1506         * manipulating objects of the type returned by
1507         * applying the specified key to objects
1508         * vended by this data source.
1509         * @see #qualifyWithRelationshipKey
1510         */
1511         public EODataSource
1512             dataSourceQualifiedByKey ( String aKey )
1513         {
1514             if ( delegateDataSource != null )
1515             {
1516                 return delegateDataSource.dataSourceQualifiedByKey( aKey );
1517             }
1518             return null;
1519         }
1520 
1521         /***
1522         * Restricts this data source to vend those
1523         * objects that are associated with the specified
1524         * key on the specified object.
1525         */
1526         public void
1527             qualifyWithRelationshipKey (
1528             String aKey, Object anObject )
1529         {
1530             if ( delegateDataSource != null )
1531             {
1532                 delegateDataSource.qualifyWithRelationshipKey( aKey, anObject );
1533             }
1534         }
1535         
1536         /***
1537         * Returns the value from the delegateDataSource, if it exists.
1538         * Otherwise calls super.
1539         */
1540         public EOClassDescription classDescriptionForObjects()
1541         {
1542             if ( delegateDataSource != null ) 
1543             {
1544                 return delegateDataSource.classDescriptionForObjects();
1545             }
1546             return super.classDescriptionForObjects();
1547         }
1548 
1549     }
1550     
1551 }
1552 
1553 /*
1554  * $Log$
1555  * Revision 1.2  2006/02/18 23:19:05  cgruber
1556  * Update imports and maven dependencies.
1557  *
1558  * Revision 1.1  2006/02/16 13:22:22  cgruber
1559  * Check in all sources in eclipse-friendly maven-enabled packages.
1560  *
1561  * Revision 1.20  2003/08/06 23:07:52  chochos
1562  * general code cleanup (mostly, removing unused imports)
1563  *
1564  * Revision 1.19  2002/05/03 21:41:18  mpowers
1565  * No longer clearing the selection model when updating from display group:
1566  * we now only modify if a change needs to be made.
1567  * No longer listening for selection change during firing of delete events:
1568  * delete events cause JTree's to update their selection model.
1569  * Fix for paintsSelectionImmediately: TreeAssociation.processRecentChanges()
1570  * must happen after the screen is painted, or the selection is not displayed.
1571  *
1572  * Revision 1.18  2002/04/23 19:12:28  mpowers
1573  * Reimplemented fireEventsForChanges.  Fitter and happier.
1574  *
1575  * Revision 1.17  2002/04/19 21:18:46  mpowers
1576  * Removed tree event coalescing, which was causing way too many problems.
1577  * The fireChangeEvent algorithm is way faster than before, so we should
1578  * still be better off than before.  At least now, we don't have to track
1579  * whether the view component has encountered a particular node.
1580  *
1581  * Revision 1.16  2002/04/18 20:36:11  mpowers
1582  * TreeModelAssociation now populates children group before selected objects.
1583  * Got rid of the forceOnSync workaround for cancelled selection change.
1584  *
1585  * Revision 1.15  2002/04/15 21:52:50  mpowers
1586  * Tightening up TreeModelAssociation and DisplayGroupNode.
1587  * Now only firing root structure changed once.
1588  * Now disposing of root's children.
1589  * Better event coalescing.
1590  *
1591  * Revision 1.14  2002/04/12 21:05:58  mpowers
1592  * Now distinguishing changes in titles group even better.
1593  *
1594  * Revision 1.11  2002/04/10 21:20:04  mpowers
1595  * Better handling for tree nodes when working with editing contexts.
1596  * Better handling for invalidation.  No longer broadcasting events
1597  * when nodes have not been "registered" in the tree.
1598  *
1599  * Revision 1.10  2002/04/03 20:01:24  mpowers
1600  * Removed printlns.
1601  *
1602  * Revision 1.8  2002/03/11 03:16:28  mpowers
1603  * Better handling of change events; coalescing changes to children group.
1604  *
1605  * Revision 1.7  2002/03/08 23:19:57  mpowers
1606  * Refactoring of DelegatingTreeDataSource to facilitate binding of titles
1607  * and children aspects to the same display group.
1608  *
1609  * Revision 1.6  2002/03/07 23:04:36  mpowers
1610  * Refining TreeColumnAssociation.
1611  *
1612  * Revision 1.5  2002/03/06 13:04:16  mpowers
1613  * Implemented cascading qualifiers in tree nodes.
1614  *
1615  * Revision 1.4  2002/03/04 22:47:48  mpowers
1616  * Fixed sort ordering for titles group.  Optimization for delegate selection.
1617  *
1618  * Revision 1.3  2002/03/04 12:28:47  mpowers
1619  * Revised case where children and titles are bound to same display group.
1620  *
1621  * Revision 1.2  2002/03/01 23:42:09  mpowers
1622  * Implemented TreeColumnAssociation, and updated documentation.
1623  *
1624  * Revision 1.1  2002/02/27 23:19:17  mpowers
1625  * Refactoring of TreeAssociation to create TreeModelAssociation parent.
1626  *
1627  * Revision 1.38  2002/02/18 03:46:08  mpowers
1628  * Implemented TreeTableCellRenderer.
1629  *
1630  * Revision 1.37  2002/02/13 21:20:15  mpowers
1631  * Updated comments.
1632  *
1633  * Revision 1.36  2001/11/21 15:13:25  mpowers
1634  * Better repainting for selectionPaintedImmediately.
1635  * Better handling for selection with multiple instances of the same
1636  * object in the tree (from yjcheung).
1637  *
1638  * Revision 1.35  2001/11/20 19:13:51  mpowers
1639  * Finished implementation of children group's specialized data source.
1640  *
1641  * Revision 1.34  2001/11/19 16:30:37  mpowers
1642  * Tree repaint strategy is now a preference: selectionPaintedImmediately.
1643  *
1644  * Revision 1.33  2001/11/15 17:56:41  mpowers
1645  * Initial implementation of data source for the children display group.
1646  *
1647  * Revision 1.32  2001/11/14 00:05:54  mpowers
1648  * Eliminated the run later in favor of repainting the component immediately.
1649  * This makes things more predictable for users of the association that
1650  * want to listen to mouse or selection events on the tree.
1651  *
1652  * Revision 1.31  2001/11/02 20:43:15  mpowers
1653  * Fixes for delegate's shouldChangeSelection veto (from yjcheung).
1654  *
1655  * Revision 1.30  2001/10/29 20:42:56  mpowers
1656  * On selection change, repainting tree before notifying display group;
1657  * using NSRunLoop instead of SwingUtilities.
1658  *
1659  * Revision 1.29  2001/10/12 20:12:53  mpowers
1660  * Better handling of selection change vetoing when changing selection
1661  * to a node that is not the sibling of the originally selected node.
1662  *
1663  * Revision 1.28  2001/09/14 13:40:26  mpowers
1664  * User-initiated selection changes are now handled on the next event loop
1665  * so that the component repaints the new selection before any potentially
1666  * lengthy logic is triggered by the selection change.
1667  *
1668  * Revision 1.27  2001/09/10 14:10:03  mpowers
1669  * Tree now handles multiple instances of the same object.
1670  *
1671  * Revision 1.26  2001/07/18 13:03:32  mpowers
1672  * TreeNodes now refetch only on demand.  Previously, once a node had
1673  * been fetched, it was always refetched after an invalidate, even if
1674  * the node was not being displayed.
1675  *
1676  * Revision 1.25  2001/05/14 15:25:35  mpowers
1677  * No longer copying titles group's data source to children group.
1678  *
1679  * Revision 1.24  2001/05/08 18:47:34  mpowers
1680  * Minor fixes for d3.
1681  *
1682  * Revision 1.23  2001/05/01 00:52:32  mpowers
1683  * Implemented breadth-first traversal of tree for node.
1684  *
1685  * Revision 1.22  2001/04/26 01:15:19  mpowers
1686  * Major clean-up of DisplayGroupNode: fitter, happier, more productive.
1687  *
1688  * Revision 1.21  2001/04/22 23:13:35  mpowers
1689  * Minor bug.
1690  *
1691  * Revision 1.20  2001/04/22 23:05:33  mpowers
1692  * Totally revised DisplayGroupNode so each object gets its own node
1693  * (so the nodes are no longer fixed by index).
1694  *
1695  * Revision 1.19  2001/04/21 23:06:33  mpowers
1696  * A major revisiting to support the revising of DisplayGroupNode.
1697  *
1698  * Revision 1.18  2001/04/03 20:36:01  mpowers
1699  * Fixed refaulting/reverting/invalidating to be self-consistent.
1700  *
1701  * Revision 1.17  2001/03/29 21:35:08  mpowers
1702  * Now handling circular references in the graph.
1703  *
1704  * Revision 1.16  2001/03/22 21:25:42  mpowers
1705  * Fixed some nasty issues with jtree's internal state and array bounds.
1706  *
1707  * Revision 1.15  2001/03/19 21:37:58  mpowers
1708  * Improved refresh of titles display group.
1709  * Fixed dangling selection problem after refresh.
1710  *
1711  * Revision 1.14  2001/03/09 22:08:57  mpowers
1712  * Trying to handle the dangling reference problem after an update.
1713  *
1714  * Revision 1.13  2001/02/17 17:23:49  mpowers
1715  * More changes to support compiling with jdk1.1 collections.
1716  *
1717  * Revision 1.12  2001/01/25 02:16:25  mpowers
1718  * TreeModelAssociation now returns DisplayGroupNode.getUserObject.
1719  *
1720  * Revision 1.11  2001/01/24 18:14:40  mpowers
1721  * Fixed problem with leaving children aspect unspecified.
1722  *
1723  * Revision 1.10  2001/01/24 17:49:15  mpowers
1724  * Added getObjectForNode and getNodeForObject convenience methods.
1725  *
1726  * Revision 1.9  2001/01/24 17:44:11  mpowers
1727  * Renamed getPathForNode to getPathForObject to be more precise.
1728  * And created a new getPathForNode method.
1729  *
1730  * Revision 1.8  2001/01/24 17:20:29  mpowers
1731  * Children display group now holds siblings of selected objects
1732  * in addition to the selected objects.
1733  *
1734  * Revision 1.5  2001/01/19 23:21:15  mpowers
1735  * Fine tuning events broadcast from TreeModelAssociation.
1736  *
1737  * Revision 1.4  2001/01/18 21:27:29  mpowers
1738  * Major rework of TreeModelAssociation.
1739  *
1740  * Revision 1.2  2001/01/11 20:29:19  mpowers
1741  * Expanded access to tree event firing methods.
1742  *
1743  * Revision 1.1.1.1  2000/12/21 15:49:18  mpowers
1744  * Contributing wotonomy.
1745  *
1746  * Revision 1.20  2000/12/20 16:25:42  michael
1747  * Added log to all files.
1748  *
1749  *
1750  */
1751