1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
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;
131 comboBoxModel = new ChooserComboBoxModel( this );
132
133 bogusJTree = new JTree();
134 bogusJTree.setModel( model );
135
136 init();
137 displayHome();
138
139 stopListening();
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
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
177
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
215
216
217
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
438 comboBoxModel.fireContentsChanged();
439
440
441 Object displayedObject = displayPath.getLastPathComponent();
442
443
444
445
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
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
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
502
503 public void treeNodesChanged( TreeModelEvent evt )
504 {
505
506
507
508
509
510
511 updateContents();
512
513
514
515
516
517
518
519 }
520
521 public void treeNodesInserted( TreeModelEvent evt )
522 {
523
524 }
525
526 public void treeNodesRemoved( TreeModelEvent evt )
527 {
528
529 }
530
531 public void treeStructureChanged( TreeModelEvent evt )
532 {
533 if ( ( evt.getTreePath().equals( displayPath ) )
534 || ( evt.getTreePath().isDescendant( displayPath ) ) )
535 {
536
537 }
538
539 displayHome();
540 }
541
542
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
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
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
600
601
602
603
604
605
606
607
608
609
610 return result;
611 }
612
613
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
641 if ( index != -1 )
642 {
643 Object parent = displayPath.getLastPathComponent();
644 Object child = getModel().getChild( parent, index );
645
646 if ( getModel().getChildCount( child ) > 0 )
647 {
648
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