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.components;
20  
21  import java.awt.BorderLayout;
22  import java.awt.Component;
23  import java.awt.Cursor;
24  import java.awt.Dimension;
25  import java.awt.GridBagConstraints;
26  import java.awt.GridBagLayout;
27  import java.awt.event.ActionEvent;
28  import java.awt.event.ActionListener;
29  import java.awt.event.MouseAdapter;
30  import java.awt.event.MouseEvent;
31  import java.util.Enumeration;
32  import java.util.LinkedList;
33  import java.util.List;
34  import java.util.Stack;
35  import java.util.Vector;
36  
37  import javax.swing.ComboBoxModel;
38  import javax.swing.JButton;
39  import javax.swing.JComboBox;
40  import javax.swing.JComponent;
41  import javax.swing.JList;
42  import javax.swing.JPanel;
43  import javax.swing.JScrollPane;
44  import javax.swing.JToolBar;
45  import javax.swing.JTree;
46  import javax.swing.ListCellRenderer;
47  import javax.swing.ListSelectionModel;
48  import javax.swing.UIManager;
49  import javax.swing.event.ListDataEvent;
50  import javax.swing.event.ListDataListener;
51  import javax.swing.event.ListSelectionEvent;
52  import javax.swing.event.ListSelectionListener;
53  import javax.swing.event.TreeModelEvent;
54  import javax.swing.event.TreeModelListener;
55  import javax.swing.event.TreeSelectionEvent;
56  import javax.swing.event.TreeSelectionListener;
57  import javax.swing.tree.DefaultMutableTreeNode;
58  import javax.swing.tree.DefaultTreeCellRenderer;
59  import javax.swing.tree.DefaultTreeModel;
60  import javax.swing.tree.DefaultTreeSelectionModel;
61  import javax.swing.tree.TreeCellRenderer;
62  import javax.swing.tree.TreeModel;
63  import javax.swing.tree.TreePath;
64  import javax.swing.tree.TreeSelectionModel;
65  
66  import net.wotonomy.foundation.internal.WotonomyException;
67  
68  /***
69  * TreeChooser is a FileChooser-like panel that
70  * uses a TreeModel as a data source.  It basically
71  * provides an alternative to JTree for rendering 
72  * and manipulating tree-like data.
73  *
74  * @author michael@mpowers.net
75  * @author $Author: cgruber $
76  * @version $Revision: 904 $
77  */
78  public class TreeChooser extends JPanel 
79      implements ActionListener, ListSelectionListener, 
80          TreeSelectionListener, TreeModelListener, ListCellRenderer
81  {   
82      /***
83      * The TreeChooser responds to this action command
84      * by calling displayPrevious().
85      */
86  	public static final String BACK = "Back";
87  
88      /***
89      * The TreeChooser responds to this action command
90      * by calling displayHome().
91      */
92  	public static final String HOME = "Home";
93  
94      /***
95      * The TreeChooser responds to this action command
96      * by calling displayParent().
97      */
98  	public static final String UP = "Up";
99  
100     /***
101     * The TreeChooser responds to this action command
102     * by attempting to navigate to the first node in
103     * the current selection and display that node's children.
104     */
105 	public static final String SELECT = "Select";
106 
107 	protected JList contents;
108 	protected JComboBox pathCombo; 
109 	protected JToolBar toolBar;
110 	
111 	protected TreeModel model;
112 	protected TreeSelectionModel selectionModel;
113     protected TreeCellRenderer renderer;
114     protected TreePath displayPath;
115     protected Stack pathStack;
116     protected int pathIndent;	
117     
118     private ChooserComboBoxModel comboBoxModel;
119     private JTree bogusJTree; // needed for tree cell renderer
120     private Dimension preferredSize;
121 	
122     public TreeChooser()
123     {
124         preferredSize = new Dimension( 300, 200 );
125         model = new DefaultTreeModel( new DefaultMutableTreeNode( "Root" ) );
126         displayPath = new TreePath( model.getRoot() );
127         selectionModel = new DefaultTreeSelectionModel();
128         renderer = new DefaultTreeCellRenderer();
129         pathStack = new Stack();
130         pathIndent = 0; // 16;
131         comboBoxModel = new ChooserComboBoxModel( this );
132         
133         bogusJTree = new JTree();
134         bogusJTree.setModel( model );
135 
136 		init();
137         displayHome();
138         
139         stopListening(); // clear existing listeners
140         startListening();
141 	}
142     
143     public Dimension getPreferredSize()
144     {
145         return preferredSize;
146     }
147     
148 	protected void init()
149 	{
150 		this.setLayout( new BorderLayout( 10, 10 ) );	
151 		
152 		contents = initList();
153         contents.getSelectionModel().setSelectionMode( 
154                 ListSelectionModel.MULTIPLE_INTERVAL_SELECTION );
155                 // synchs with DefaultTreeSelectionModel
156 
157 		JScrollPane scrollPane = new JScrollPane( contents );
158 		scrollPane.setPreferredSize( new Dimension( 200, 150 ) );
159 		this.add( scrollPane, BorderLayout.CENTER );
160 
161         Component previewPane = initPreviewPane();
162         if ( previewPane != null )
163         {
164             this.add( previewPane, BorderLayout.EAST );
165         }
166 		
167 		JPanel navigationPanel = new JPanel();
168 		navigationPanel.setLayout( new BorderLayout( 10, 10 ) );
169 		this.add( navigationPanel, BorderLayout.NORTH );
170         
171 		pathCombo = initComboBox();
172         if ( pathCombo != null )
173         {
174             pathCombo.setModel( comboBoxModel );
175 
176 			// put combo in a grid bag to handle varying
177 			//  heights of JToolBars across platforms
178 			JPanel panel = new JPanel();
179 			panel.setLayout( new GridBagLayout() );
180 			GridBagConstraints gbc = new GridBagConstraints();
181 			gbc.fill = GridBagConstraints.HORIZONTAL;
182 			gbc.weightx = 1.0;
183 			panel.add( pathCombo, gbc );
184             navigationPanel.add( panel, BorderLayout.CENTER );
185         }
186 		
187 		Component toolBar = initToolBar();
188         if ( toolBar != null )
189         {
190             navigationPanel.add( toolBar, BorderLayout.EAST );
191         }
192 		
193 	}
194     
195     /***
196     * Creates tool bar or return null if no tool bar is desired.
197     * This implementation returns a JToolBar containing buttons
198     * for BACK, UP, and HOME.
199     */ 
200     protected Component initToolBar()
201     {
202 		JToolBar toolBar = new JToolBar();
203 		toolBar.setFloatable( false );
204         JButton button;
205 		button = new JButton( UIManager.getIcon("FileChooser.upFolderIcon") );
206         button.setActionCommand( UP );
207         button.addActionListener( this );
208         toolBar.add( button );
209 		button = new JButton( UIManager.getIcon("FileChooser.homeFolderIcon") );
210         button.setActionCommand( HOME );
211         button.addActionListener( this );
212         toolBar.add( button );
213 /*        
214 		button = new JButton( UIManager.getIcon("FileChooser.newFolderIcon") );
215         button.setActionCommand( BACK );
216         button.addActionListener( this );
217         toolBar.add( button );
218 */        
219         return toolBar;
220     }
221     
222     /***
223     * Creates the component that is used to display a preview of the
224     * selected item(s) in the content area.  This component would listen
225     * to the selection model to update itself when the selected items change.
226     * Return null to omit this component.
227     * This implementation returns null.
228     */ 
229     protected Component initPreviewPane()
230     {
231         return null;
232     }
233     
234     /***
235     * Creates the JComboBox that is used to render the path leading to
236     * the displayed contents.  Return null to omit this combo box.
237     * This implementation returns a stock JComboBox that uses this
238     * class as its cell renderer.
239     */ 
240     protected JComboBox initComboBox()
241     {
242         JComboBox comboBox = new JComboBox();
243         comboBox.setRenderer( this );
244         return comboBox;
245     }
246     
247     /***
248     * Creates the JList that is used to render the path leading to
249     * the displayed contents.  This method may not return null.
250     * This implementation returns a stock JList that uses this
251     * class as its cell renderer and fires a SELECT action event
252     * on double click.
253     */ 
254     protected JList initList()
255     {
256         JList list = new JList();
257         list.setCellRenderer( this );
258         list.addMouseListener( new MouseAdapter() 
259         {
260             public void mouseClicked( MouseEvent evt )
261             {
262                 if ( evt.getClickCount() > 1 )
263                 {   
264                     actionPerformed( new ActionEvent( this, 0, SELECT ) );
265                 }
266             }
267         });
268         return list;
269     }
270     
271     /***
272     * Begins listening to the specified tree model 
273     * and tree selection model.
274     */
275     protected void startListening()
276     {
277         model.addTreeModelListener( this );
278         selectionModel.addTreeSelectionListener( this );
279         contents.addListSelectionListener( this );
280     }
281     
282     /***
283     * Stops listening to the specified tree model 
284     * and tree selection model.
285     */
286     protected void stopListening()
287     {
288         model.removeTreeModelListener( this );
289         selectionModel.removeTreeSelectionListener( this );
290         contents.removeListSelectionListener( this );
291     }
292     
293     /***
294     * Returns the TreeModel used by the TreeChooser.
295     */ 
296     public TreeModel getModel()
297     {
298         return model;
299     }
300     
301     /***
302     * Sets the TreeModel used by the TreeChooser.
303     */ 
304     public void setModel( TreeModel aTreeModel )
305     {
306         stopListening();
307         model = aTreeModel;
308         bogusJTree.setModel( aTreeModel );
309         pathStack.removeAllElements();
310         startListening();
311         displayHome();
312     }
313     
314     /***
315     * Returns the TreeSelectionModel used by the TreeChooser.
316     */ 
317     public TreeSelectionModel getSelectionModel()
318     {
319         return selectionModel;
320     }
321     
322     /***
323     * Sets the TreeSelectionModel used by the TreeChooser.
324     */ 
325     public void setSelectionModel( TreeSelectionModel aSelectionModel )
326     {
327         selectionModel = aSelectionModel;
328         if ( aSelectionModel.getSelectionMode() == 
329             TreeSelectionModel.SINGLE_TREE_SELECTION )
330         {
331             contents.getSelectionModel().setSelectionMode( 
332                 ListSelectionModel.SINGLE_SELECTION );
333         }
334         else
335         {
336             contents.getSelectionModel().setSelectionMode( 
337                 ListSelectionModel.MULTIPLE_INTERVAL_SELECTION );
338         }
339         updateSelection();
340     }
341     
342     /***
343     * Returns the TreeCellRenderer used by the TreeChooser.
344     */ 
345     public TreeCellRenderer getRenderer()
346     {
347         return renderer;
348     }
349     
350     /***
351     * Sets the TreeCellRenderer used by the TreeChooser.
352     */ 
353     public void setRenderer( TreeCellRenderer aRenderer )
354     {
355         renderer = aRenderer;
356         updateContents();
357     }
358 	
359     /***
360     * Displays the "home" directory.
361     * This implementation displays the root node's children.
362     */
363     public void displayHome()
364     {
365         setDisplayPath( null );        
366     }
367     
368     /***
369     * Displays the parent path of the currently displayed path.
370     */
371     public void displayParent()
372     {
373         setDisplayPath( displayPath.getParentPath() );    
374     }
375     
376     /***
377     * Displays the last displayed path before the current one,
378     * emulating the behavior of a "back" button.
379     */
380     public void displayPrevious()
381     {
382         if ( pathStack.empty() )
383         {
384             displayHome();   
385         }
386         else
387         {
388             setDisplayPathDirect( (TreePath) pathStack.pop() );
389             updateContents();
390         }
391     }
392     
393     /***
394     * Pushes the previous item onto the stack, sets
395     * the display path, and then updates the contents.
396     * If aPath is null, the root node's children are displayed.
397     */
398     public void setDisplayPath( TreePath aPath )
399     {
400         if ( aPath == null )
401         {
402             aPath = new TreePath( getModel().getRoot() );
403         }
404         if ( ! displayPath.equals ( aPath ) )
405         {
406             pathStack.push( displayPath );
407             setDisplayPathDirect( aPath );
408         }
409         updateContents();
410     }
411     
412     /***
413     * Sets the displayPath field and does not
414     * update the stack nor update the contents.
415     */
416     protected void setDisplayPathDirect( TreePath aPath )
417     {
418         displayPath = aPath;   
419     }
420     
421     /***
422     * Gets the currently displayed path.
423     */
424     public TreePath getDisplayPath()
425     {
426         return displayPath;   
427     }
428 
429     /***
430     * Called when selected path changes or when model indicates
431     * that the displayed path has changed.
432     */
433     protected void updateContents()
434     {
435         stopListening();
436 
437         // update combo box        
438         comboBoxModel.fireContentsChanged();
439         
440         // update list contents
441         Object displayedObject = displayPath.getLastPathComponent();
442 /*
443 //FIXME: this display group doesn't seem to be getting the sort orderings from parent        
444 if ( displayedObject instanceof net.wotonomy.ui.EODisplayGroup )        
445 System.out.println( ((net.wotonomy.ui.EODisplayGroup)displayedObject).displayedObjects() );            
446 */
447         int count = model.getChildCount( displayedObject );
448         Object[] children = new Object[ count ];
449         for ( int i = 0; i < count; i++ )
450         {
451             children[i] = model.getChild( displayedObject, i );
452         }
453         contents.setListData( children );
454         
455         startListening();
456 
457         // synchronize the selection        
458         updateSelection();
459     }
460     
461     /***
462     * Updates the selection in the list to reflect the
463     * selection in the tree selection model.
464     */
465     public void updateSelection()
466     {
467         int index;
468         Object last = displayPath.getLastPathComponent();
469         TreePath[] selectionPaths = selectionModel.getSelectionPaths();
470         if ( selectionPaths != null )
471         {
472             List selectedIndices = new LinkedList();
473             for ( int i = 0; i < selectionPaths.length; i++ )
474             {
475                 if ( displayPath.equals( selectionPaths[i].getParentPath() ) )
476                 {
477                     index = getModel().getIndexOfChild( 
478                         last, selectionPaths[i].getLastPathComponent() );
479                     if ( index != -1 ) 
480                     {
481                         selectedIndices.add( new Integer( index ) );
482                     }
483                     else // should never happen
484                     {
485                         throw new WotonomyException( 
486                             "Could not find child of displayed node." );
487                     }
488                 }
489             }
490             int[] selected = new int[ selectedIndices.size() ];
491             for ( int i = 0; i < selected.length; i++ )
492             {
493                 selected[i] = ((Integer)selectedIndices.get(i)).intValue();   
494             }
495             stopListening();
496             contents.setSelectedIndices( selected );
497             startListening();
498         }
499     }
500     
501     // interface TreeModelListener
502     
503     public void treeNodesChanged( TreeModelEvent evt )
504     { 
505 /*            
506         if ( displayPath.getLastPathComponent().toString().equals( 
507             evt.getTreePath().getLastPathComponent().toString() ) )
508         {
509 System.out.println( "TreeChooser.treeNodesChanged: " + count++ );  
510 */
511             updateContents();
512 /*            
513         }
514         else
515         {
516             System.out.println( evt.getTreePath() + " != " + displayPath );
517         }
518 */        
519     }
520     
521     public void treeNodesInserted( TreeModelEvent evt )
522     { 
523 //            updateContents();
524     }
525     
526     public void treeNodesRemoved( TreeModelEvent evt )
527     { 
528 //            updateContents();
529     }
530     
531     public void treeStructureChanged( TreeModelEvent evt )
532     {  
533         if ( ( evt.getTreePath().equals( displayPath ) )
534         || ( evt.getTreePath().isDescendant( displayPath ) ) )
535         {
536 //            setDisplayPath( evt.getTreePath() );
537         }
538         
539         displayHome();
540     }
541     
542     // interface TreeSelectionListener
543     
544     /***
545     * Called when the tree selection model's value changes.
546     * This is presumably an external change, so this calls
547     * updateSelection.
548     */
549     public void valueChanged( TreeSelectionEvent evt )
550     {
551         updateSelection();   
552     }
553 
554     // interface ListSelectionListener
555     
556     /***
557     * Called when user changes the selection in the list.
558     * This implementation updates the tree selection model
559     * with the corresponding selection.
560     */
561     public void valueChanged( ListSelectionEvent evt )
562     {
563         if ( ! evt.getValueIsAdjusting() )
564         {
565             Object last = displayPath.getLastPathComponent();
566             int[] selection = contents.getSelectedIndices();
567             TreePath[] selectionPaths = new TreePath[ selection.length ];
568             for ( int i = 0; i < selection.length; i++ )
569             {
570                 selectionPaths[i] = displayPath.pathByAddingChild(
571                     getModel().getChild( last, selection[i] ) );
572             }
573             selectionModel.setSelectionPaths( selectionPaths );
574         }
575               
576     }
577     
578     // interface ListCellRenderer
579 
580     /***
581     * This method returns the component returned by the tree cell renderer.
582     */
583     public Component getListCellRendererComponent(
584         JList list,
585         Object value,
586         int index,
587         boolean isSelected,
588         boolean cellHasFocus )
589     {
590         boolean isLeaf = ( model.isLeaf( value ) );
591         
592         bogusJTree.setForeground( list.getForeground() );
593         bogusJTree.setBackground( list.getBackground() );
594         
595         JComponent result = (JComponent) renderer.getTreeCellRendererComponent(
596             bogusJTree, value, isSelected, (list != contents), 
597                 isLeaf, index, cellHasFocus );
598 /*
599         if ( ( list != contents ) && ( index > -1 ) )
600         {
601             result.setBorder( 
602                 BorderFactory.createEmptyBorder( 0, index*pathIndent, 0, 0 ) );
603         }
604         else
605         {
606             result.setBorder( 
607                 BorderFactory.createEmptyBorder() );
608         }
609 */        
610         return result;
611     }
612     
613     // interface ActionListener
614 	
615     public void actionPerformed( ActionEvent evt )
616     {
617         String command = evt.getActionCommand();
618         
619         if ( HOME.equals( command ) )
620         {
621             displayHome();
622         }
623         else
624         if ( UP.equals( command ) )
625         {
626             displayParent();
627         }
628         else
629         if ( BACK.equals( command ) )
630         {
631             displayPrevious();
632         }
633         else
634         if ( SELECT.equals( command ) )
635         {
636             Cursor oldCursor = getCursor();
637             setCursor( Cursor.getPredefinedCursor( Cursor.WAIT_CURSOR ) );
638             
639             int index = contents.getSelectedIndex();
640             // if selection 
641             if ( index != -1 )
642             {
643                 Object parent = displayPath.getLastPathComponent();
644                 Object child = getModel().getChild( parent, index );
645                 // if selected item is not a leaf
646                 if ( getModel().getChildCount( child ) > 0 )
647                 {
648                     // navigate to selected item
649                     setDisplayPath( displayPath.pathByAddingChild( child ) );
650                 }
651             }
652             
653             setCursor( oldCursor );
654         }
655     
656     }
657     
658     private class ChooserComboBoxModel implements ComboBoxModel
659     {
660         TreeChooser treeChooser;
661         Vector listeners;
662         
663         ChooserComboBoxModel( TreeChooser aTreeChooser )
664         {
665             treeChooser = aTreeChooser; 
666             listeners = new Vector();
667         }
668         
669         public int getSize()
670         {
671             return treeChooser.displayPath.getPathCount(); 
672         }
673         
674         public Object getElementAt(int index)
675         {
676             return treeChooser.displayPath.getPathComponent( index );    
677         }
678         
679         public Object getSelectedItem()
680         {
681             return treeChooser.displayPath.getLastPathComponent();
682         }
683         
684         public void setSelectedItem(Object anItem)
685         {
686             if ( ! ( 
687                 treeChooser.displayPath.getLastPathComponent().equals( anItem ) ) )
688             {
689                 Object[] items = treeChooser.displayPath.getPath();
690                 TreePath path = new TreePath( getModel().getRoot() );
691                 for ( int i = 1; i < items.length; i++ )
692                 {
693                     if ( path.getLastPathComponent() == anItem )
694                     {
695                         treeChooser.setDisplayPath( path );
696                         return;
697                     }
698                     path = path.pathByAddingChild( items[i] );
699                 }
700             }
701         }
702         
703         public void addListDataListener(ListDataListener l) 
704         {
705             listeners.add( l );
706         }
707         
708         public void removeListDataListener(ListDataListener l) 
709         {
710             listeners.remove( l );
711         }
712         
713         public void fireContentsChanged()
714         {
715             Enumeration e = listeners.elements();
716             while ( e.hasMoreElements() )
717             {
718                 ((ListDataListener)e.nextElement()).contentsChanged( 
719                     new ListDataEvent( 
720                     this, ListDataEvent.CONTENTS_CHANGED, 0, getSize() ) );
721             }
722         }   
723     }
724     
725 }
726 
727