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.awt.EventQueue;
22  import java.awt.Rectangle;
23  import java.awt.event.FocusEvent;
24  import java.awt.event.FocusListener;
25  import java.util.Enumeration;
26  import java.util.Iterator;
27  import java.util.Vector;
28  
29  import javax.swing.JTree;
30  import javax.swing.event.TreeExpansionEvent;
31  import javax.swing.event.TreeExpansionListener;
32  import javax.swing.event.TreeSelectionEvent;
33  import javax.swing.event.TreeWillExpandListener;
34  import javax.swing.tree.ExpandVetoException;
35  import javax.swing.tree.TreePath;
36  
37  import net.wotonomy.foundation.NSArray;
38  import net.wotonomy.foundation.NSMutableArray;
39  import net.wotonomy.ui.EODisplayGroup;
40  
41  /***
42  * TreeAssociation is a TreeModelAssociation further
43  * customized for JTrees.  It binds a JTree to a display group's
44  * list of displayable objects, each of which may have
45  * a list of child objects managed by another display
46  * group, and so on.  TreeAssociation works exactly
47  * like a ListAssociation, with the additional capability
48  * to specify a "Children" aspect, that will allow child
49  * objects to be retrieved from a parent display group.
50  * Note that the children aspect requires the bound
51  * display group to have a DataSource that can vend a
52  * DataSource appropriate for the bound key  That data
53  * source is then used to create data sources for
54  * child nodes, and so on.
55  *
56  * <ul>
57  *
58  * <li>titles: a property convertable to a string for
59  * display in the nodes of the tree.  The objects in
60  * the bound display group will be used to populate the
61  * initial root nodes of the tree (more accurately,
62  * children of the offscreen root node in the tree).</li>
63  *
64  * <li>children: a property of a node value that returns
65  * zero, one or many objects, each of which will correspond
66  * to a child node for the corresponding node in the tree.
67  * The data source of the bound display group is replaced 
68  * a data source that populates the display group with
69  * the visible nodes in the tree component as determined by 
70  * calling fetchObjectsIntoChildrenGroup.
71  * If this aspect is not bound, the tree behaves like a list.
72  * <br><br>
73  * Binding this aspect with a null display group is the same
74  * as binding it with the titles display group.  
75  * In this configuration the contents of the titles 
76  * display group will be replaced with the visible nodes in the
77  * tree component, as specified above, replacing the existing 
78  * data source.  
79  * <br><br>
80  * In that case, the display groups for the nodes in
81  * the tree will still use the original data source
82  * for resolving their children key, and programmatically
83  * setting the contents of the display group will still 
84  * repopulate the root nodes of the tree.
85  * </li>
86  *
87  * <li>isLeaf: a property of a node value that returns
88  * a value convertable to a boolean value (aside from
89  * an actual boolean value, zeroes evaluate to true,
90  * as does any String containing "yes" or "true" or that
91  * is convertable to a number equal to zero; other values
92  * evaluate to false).  
93  * <br><br>
94  * If the isLeaf aspect is not bound,
95  * the tree must force nodes to load their children to
96  * determine whether they are leaf nodes (in effect
97  * loading the grandchildren for any expanded node).
98  * If bound, child loading is deferred until the node
99  * is actually expanded.  
100 * <br><br>
101 * For example, binding this value to a null
102 * display group and the key "false" will result in a
103 * deferred-loading tree that works much like Windows
104 * Explorer's network volume browser - all nodes appear
105 * with "pluses" until they are expanded.
106 * <br><br>
107 * Note that the display group is ignored: the property
108 * will be applied directly to the object corresponding
109 * to the node.</li>
110 *
111 * </ul>
112 *
113 * All other usage is as TreeModelAssociation.
114 *
115 * @author michael@mpowers.net
116 * @author $Author: cgruber $
117 * @version $Revision: 904 $
118 */
119 public class TreeAssociation extends TreeModelAssociation
120     implements FocusListener, TreeExpansionListener, TreeWillExpandListener, Runnable
121 {
122     private boolean isExpanding;
123     private Vector nodeQueue;
124     private Vector structureQueue;
125     private boolean isRunning;
126     
127     /***
128     * Constructor expecting a JTree.
129     */
130     public TreeAssociation ( Object anObject )
131     {
132         super( anObject );
133         init();
134     }
135 
136     /***
137     * Constructor expecting a JTree or similar component
138     * and specifying a label for the root node.
139     */
140     public TreeAssociation( Object anObject, Object aRootLabel )
141     {
142         super( anObject );
143         init();
144         rootLabel = aRootLabel;
145         rootNode.setUserObject( aRootLabel );
146     }
147     
148     // convenience
149     private JTree component()
150     {
151         return (JTree) object();
152     }
153 
154     /***
155     * Called by both constructors.
156     */
157     protected void init()
158     {
159         isExpanding = false;
160         isRunning = false;
161         nodeQueue = new Vector();
162         structureQueue = new Vector();
163         component().addFocusListener( this );
164         component().addTreeExpansionListener( this );
165         component().addTreeWillExpandListener( this );
166     }
167 
168     /***
169     * Returns whether this class can control the specified
170     * object.
171     */
172     public static boolean isUsableWithObject ( Object anObject )
173     {
174         return ( anObject instanceof JTree );
175     }
176 
177     /***
178     * Overridden to not fire events during initial population.
179     */
180     public void establishConnection ()
181     {
182         isExpanding = true;
183         super.establishConnection();
184         isExpanding = false;
185     }
186     
187     // interface TreeSelectionListener
188 
189     public void valueChanged(TreeSelectionEvent e)
190     {
191         if ( ! isListening ) return;
192 // NOTE: This approach causes focus rectangle to perceptibly trail
193 // the selection rectangle, presumably because we're called after the
194 // new selection has been processed but before the focus is processed.
195 // Users don't like it, so we're going with a preference to use
196 // invokeLater.
197 /*
198         // paint immediately before updating the display group and
199         //   before any potentially lengthy second-order effects happen:
200         //   this improves user-perceived responsiveness of big apps
201         if ( object() instanceof javax.swing.JComponent )
202         {
203             javax.swing.JComponent component = (javax.swing.JComponent)object();
204             component.paintImmediately( component.getBounds() );
205         }
206         selectFromSelectionModel();
207 */
208 
209 // NOTE: This approach uses invoke later to cause the update of
210 // the display group (which could be lengthly if that in turn
211 // causes other things to update) to happen after the tree repaints.
212 // Users like this because it "feels faster", but developers have
213 // to remember that if they listen to tree selection events they
214 // will have to do a similar invoke later if they check the display
215 // group.
216 
217         Runnable select = new Runnable()
218         {
219             public void run()
220             {
221                 selectFromSelectionModel();
222             }
223         };
224         
225         if ( selectionPaintedImmediately )
226         {
227             if ( object() instanceof java.awt.Component )
228             {
229                 ((java.awt.Component)object()).repaint();
230             }
231             EventQueue.invokeLater( select );
232         }
233         else
234         {
235             select.run();
236         }
237     }
238 
239     /***
240     * Overridden to check whether the node is visible 
241     * in the tree on screen.  Offscreen in a scrollpane
242     * does not count.
243     */
244     public boolean isVisible(Object node)
245     {
246         JTree tree = (JTree) object();
247         TreePath path = ((DisplayGroupNode)node).treePath();
248         if ( tree.isVisible( path ) )
249         {
250             Rectangle rowRect = tree.getPathBounds( path );
251             if ( rowRect != null )
252             {
253                 Rectangle visible = tree.getVisibleRect();
254                 if ( visible != null )
255                 {
256 //System.out.println( "isVisible: intersects: " + visible.intersects( rowRect ) );        
257                     return visible.intersects( rowRect );
258                 }
259             }
260         }
261 //System.out.println( "isVisible: false" );        
262         return false;
263     }
264 
265     /***
266     * Fires a tree nodes changed event to all listeners.
267     * Provided as a convenience if you need to make manual
268     * changes to the tree model.
269     */
270     public void fireTreeNodesChanged(final Object source,
271                                      final Object[] path,
272                                      final int[] childIndices,
273                                      final Object[] children)
274     {
275         if ( !isExpanding )
276         {
277             for ( int i = 0; i < children.length; i++ )
278             {
279                 nodeQueue.add( children[i] );
280             }
281             if ( !isRunning )
282             {
283                 isRunning = true;
284                 EventQueue.invokeLater( this );
285             }
286         }
287     }
288 
289     /***
290     * Fires a tree nodes inserted event to all listeners.
291     * Provided as a convenience if you need to make manual
292     * changes to the tree model.
293     */
294     public void fireTreeNodesInserted(Object source,
295                                      Object[] path,
296                                      int[] childIndices,
297                                      Object[] children)
298     {
299         if ( !isExpanding )
300         {
301             super.fireTreeNodesInserted( source, path, childIndices, children );
302             EventQueue.invokeLater( this );
303         }
304     }
305 
306     /***
307     * Fires a tree nodes removed event to all listeners.
308     * Provided as a convenience if you need to make manual
309     * changes to the tree model.
310     */
311     public void fireTreeNodesRemoved(Object source,
312                                     Object[] path,
313                                     int[] childIndices,
314                                     Object[] children)
315     {
316         if ( !isExpanding )
317         {
318             super.fireTreeNodesRemoved( source, path, childIndices, children );
319             EventQueue.invokeLater( this );
320         }
321     }
322 
323     /***
324     * Fires a tree structure changed event to all listeners.
325     * Provided as a convenience if you need to make manual
326     * changes to the tree model.
327     */
328     public void fireTreeStructureChanged(Object source,
329                                     Object[] path,
330                                     int[] childIndices,
331                                     Object[] children)
332     {
333         if ( !isExpanding )
334         {
335             structureQueue.add( path[path.length-1] );
336             if ( !isRunning )
337             {
338                 isRunning = true;
339                 EventQueue.invokeLater( this );
340             }
341         }
342     }
343 
344     /***
345     * Overridden to return all visible rows in the tree.
346     */
347     public NSArray objectsFetchedIntoChildrenGroup()
348     {
349         JTree tree = (JTree) object();
350         NSMutableArray objectList = new NSMutableArray();
351         
352         int count = tree.getRowCount();
353         for ( int i = 0; i < count; i++ )
354         {
355             objectList.add(  
356                 ((DisplayGroupNode) tree.getPathForRow( i ).getLastPathComponent()).object() );
357         }
358 
359 //new net.wotonomy.ui.swing.util.StackTraceInspector( Integer.toString( objectList.size() )  );
360         return objectList;
361     }
362 
363     // interface FocusListener
364     
365 	/***
366 	* Notifies of beginning of edit.
367 	*/
368     public void focusGained(FocusEvent evt)
369     {
370 		Object o;
371 		EODisplayGroup displayGroup;
372         Enumeration e = aspects().objectEnumerator();
373 		while ( e.hasMoreElements() )
374 		{
375 			displayGroup =
376 				displayGroupForAspect( e.nextElement().toString() );
377 			if ( displayGroup != null )
378 			{
379 				displayGroup.associationDidBeginEditing( this );
380 			}
381 		}
382     }
383 
384 	/***
385 	* Updates object on focus lost and notifies of end of edit.
386 	*/
387     public void focusLost(FocusEvent evt)
388     {
389         if ( ! component().isEditing() )
390         {
391             Object o;
392             EODisplayGroup displayGroup;
393             Enumeration e = aspects().objectEnumerator();
394             while ( e.hasMoreElements() )
395             {
396                 displayGroup =
397                     displayGroupForAspect( e.nextElement().toString() );
398                 if ( displayGroup != null )
399                 {
400                     displayGroup.associationDidEndEditing( this );
401                 }
402             }
403         }
404     }
405     
406     // interface TreeWillExpandListener
407     
408     public void treeWillExpand(TreeExpansionEvent event)
409                     throws ExpandVetoException
410     {
411         isExpanding = true;
412     }
413                     
414     public void treeWillCollapse(TreeExpansionEvent event)
415                       throws ExpandVetoException                    
416     {
417         // do nothing
418     }
419                     
420     // interface TreeExpansionListener
421     
422     /***
423     * Updates the children display group, if any.
424     */
425     public void treeExpanded(TreeExpansionEvent event)
426     { //System.out.println( "treeExpanded: " + event.getPath().getLastPathComponent() );        
427         isExpanding = false;
428         if ( childrenDisplayGroup != null )
429         {
430             removeAsListener(); // prevent data source refetch: see fetchObjects()
431             childrenDisplayGroup.fetch();
432             addAsListener();
433         }
434     }
435     
436     /***
437     * Updates the children display group, if any.
438     */
439     public void treeCollapsed(TreeExpansionEvent event)
440     {
441         if ( childrenDisplayGroup != null )
442         {
443             removeAsListener(); // prevent data source refetch: see fetchObjects()
444             childrenDisplayGroup.fetch();
445             addAsListener();
446         }
447     }
448     
449     // interface Runnable
450     
451     /***
452     * Fires any queued node changed and structure changed events.
453     * Typically invoked on a delayed event loop.
454     */
455     public void run()
456     {
457         DisplayGroupNode node;    
458         int index;
459         Iterator i;
460         
461         i = nodeQueue.iterator();
462         while ( i.hasNext() )
463         {
464             node = (DisplayGroupNode) i.next();
465             index = ((DisplayGroupNode)node.parentGroup).getIndex( node );
466             if ( ( index != -1 )
467             && ( node.treePath().getParentPath() != null ) )
468             {
469                 super.fireTreeNodesChanged( 
470                     node,
471                     node.treePath().getParentPath().getPath(),
472                     new int[] { index },
473                     new Object[] { node } );
474             }
475         }
476         nodeQueue.clear();
477         
478         i = structureQueue.iterator();
479         while ( i.hasNext() )
480         {
481             node = (DisplayGroupNode) i.next();
482             super.fireTreeStructureChanged( 
483                 node,
484                 node.treePath().getPath(),
485                 null,
486                 null );
487         }
488         structureQueue.clear();
489         
490         isRunning = false;
491 /*      
492         EventQueue.invokeLater( new Runnable() { public void run() {
493             ((JTree)object()).treeDidChange();
494             ((JTree)object()).getParent().invalidate();
495             ((JTree)object()).getParent().validate();
496             ((JTree)object()).getParent().update( ((JTree)object()).getGraphics() );
497             
498 //            ((JTree)object()).getParent().doLayout();
499 //            ((JTree)object()).getParent().repaint();
500 //            ((JTree)object()).repaint();
501 //            ((JTree)object()).updateUI();
502         } } );
503 */       
504     }
505 }
506 
507 /*
508  * $Log$
509  * Revision 1.2  2006/02/18 23:19:05  cgruber
510  * Update imports and maven dependencies.
511  *
512  * Revision 1.1  2006/02/16 13:22:22  cgruber
513  * Check in all sources in eclipse-friendly maven-enabled packages.
514  *
515  * Revision 1.55  2004/02/05 02:18:50  mpowers
516  * Super was calling back into this class before init() was called.
517  *
518  * Revision 1.54  2003/08/06 23:07:52  chochos
519  * general code cleanup (mostly, removing unused imports)
520  *
521  * Revision 1.53  2003/06/03 14:49:48  mpowers
522  * Now correctly calculating isVisible based on the component visible rect.
523  * Now deferring node changed events to a later event queue to allow repaints
524  * to happen after all changes have taken effect.
525  *
526  * Revision 1.52  2002/05/03 21:41:18  mpowers
527  * No longer clearing the selection model when updating from display group:
528  * we now only modify if a change needs to be made.
529  * No longer listening for selection change during firing of delete events:
530  * delete events cause JTree's to update their selection model.
531  * Fix for paintsSelectionImmediately: TreeAssociation.processRecentChanges()
532  * must happen after the screen is painted, or the selection is not displayed.
533  *
534  * Revision 1.51  2002/04/19 21:18:45  mpowers
535  * Removed tree event coalescing, which was causing way too many problems.
536  * The fireChangeEvent algorithm is way faster than before, so we should
537  * still be better off than before.  At least now, we don't have to track
538  * whether the view component has encountered a particular node.
539  *
540  * Revision 1.49  2002/04/12 21:05:58  mpowers
541  * Now distinguishing changes in titles group even better.
542  *
543  * Revision 1.48  2002/04/12 20:36:31  mpowers
544  * Now distinguishing between changes made on titles group by tree expansion
545  * versus external changes which should cause us to repopulate root nodes.
546  *
547  * Revision 1.47  2002/04/10 21:20:04  mpowers
548  * Better handling for tree nodes when working with editing contexts.
549  * Better handling for invalidation.  No longer broadcasting events
550  * when nodes have not been "registered" in the tree.
551  *
552  * Revision 1.46  2002/03/27 20:44:53  mpowers
553  * Added isVisible test for node.
554  *
555  * Revision 1.45  2002/03/08 23:19:57  mpowers
556  * Refactoring of DelegatingTreeDataSource to facilitate binding of titles
557  * and children aspects to the same display group.
558  *
559  * Revision 1.44  2002/03/07 23:04:36  mpowers
560  * Refining TreeColumnAssociation.
561  *
562  * Revision 1.43  2002/03/06 13:04:15  mpowers
563  * Implemented cascading qualifiers in tree nodes.
564  *
565  * Revision 1.42  2002/03/05 23:18:28  mpowers
566  * Added documentation.
567  * Added isSelectionPaintedImmediate and isSelectionTracking attributes
568  * to TableAssociation.
569  * Added getTableAssociation to TableColumnAssociation.
570  *
571  * Revision 1.41  2002/03/01 23:42:08  mpowers
572  * Implemented TreeColumnAssociation, and updated documentation.
573  *
574  * Revision 1.40  2002/03/01 20:41:39  mpowers
575  * Now a focus listener and an expansion listener.
576  *
577  * Revision 1.39  2002/02/27 23:19:17  mpowers
578  * Refactoring of TreeAssociation to create TreeModelAssociation parent.
579  *
580  *
581  */
582