001/*
002  The Broad Institute
003  SOFTWARE COPYRIGHT NOTICE AGREEMENT
004  This software and its documentation are copyright (2003-2008) by the
005  Broad Institute/Massachusetts Institute of Technology. All rights are
006  reserved.
007
008  This software is supplied without any warranty or guaranteed support
009  whatsoever. Neither the Broad Institute nor MIT can be responsible for its
010  use, misuse, or functionality.
011*/
012
013
014/*
015    @(#)JTreeTable.java 1.2 98/10/27
016    Copyright 1997, 1998 by Sun Microsystems, Inc.,
017    901 San Antonio Road, Palo Alto, California, 94303, U.S.A.
018    All rights reserved.
019    This software is the confidential and proprietary information
020    of Sun Microsystems, Inc. ("Confidential Information").  You
021    shall not disclose such Confidential Information and shall use
022    it only in accordance with the terms of the license agreement
023    you entered into with Sun.
024  */
025
026package ca.bc.webarts.widgets.treetable;
027//package org.genomespace.gsui.ui.treetable;
028
029import java.awt.Color;
030import java.awt.Component;
031import java.awt.Dimension;
032import java.awt.Graphics;
033import java.awt.Rectangle;
034import java.awt.event.*;
035import java.util.EventObject;
036import javax.swing.*;
037import javax.swing.event.*;
038import javax.swing.table.*;
039import javax.swing.tree.*;
040//import org.jdesktop.swing.treetable.TreeTableModel;
041
042import org.jdesktop.swingx.treetable.TreeTableModel;
043
044/**
045 *  This example shows how to create a simple JTreeTable component, by using a
046 *  JTree as a tree (and editor) for the cells in a particular column in the
047 *  JTable.
048 *
049 * @author     Philip Milne
050 * @author     Scott Violet
051 * @version    1.2 10/27/98
052 */
053public class JTreeTable extends JTable {
054   /**  A subclass of JTree. */
055   protected TreeTableCellRenderer tree;
056   TreeTableModelAdapter tableModel;
057
058   public JTreeTable(TreeTableModel treeTableModel) {
059      super();
060
061      // Create the tree. It will be used as a tree and editor.
062      tree = new TreeTableCellRenderer(treeTableModel);
063      tree.setRootVisible(false);
064
065      // Install a tableModel representing the visible rows in the tree.
066      tableModel = new TreeTableModelAdapter(treeTableModel, tree);
067      super.setModel(tableModel);
068
069      // Force the JTable and JTree to share their row selection models.
070      ListToTreeSelectionModelWrapper selectionWrapper = new
071            ListToTreeSelectionModelWrapper();
072      tree.setSelectionModel(selectionWrapper);
073      setSelectionModel(selectionWrapper.getListSelectionModel());
074      // Install the tree editor tree and editor.
075      setDefaultRenderer(TreeTableModel.class, tree);
076      setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());
077
078      // No grid.
079      setShowGrid(false);
080
081      // No intercell spacing
082      setIntercellSpacing(new Dimension(0, 0));
083
084      // And update the height of the trees row to match that of
085      // the table.
086      if(tree.getRowHeight() < 1) {
087         // Metal looks better like this.
088         setRowHeight(18);
089      }
090      NoHighlightRenderer r = new NoHighlightRenderer();
091      defaultRenderersByColumnClass.put(String.class, r);
092
093    //  addMouseListener(new MouseAdapter() {
094      //   public void mouseClicked(MouseEvent e) {
095        //    expandOrCollapseNode(e);
096         //}
097      //});
098   }
099
100   public static class NoHighlightRenderer extends DefaultTableCellRenderer {
101        public Component getTableCellRendererComponent(JTable table,
102            Object value,
103            boolean isSelected, boolean hasFocus,
104            int r, int c) {
105        return super.getTableCellRendererComponent(table,
106             value,
107             isSelected, false,
108             r,  c);
109      }
110   }
111
112
113   public void addTreeSelectionListener(javax.swing.event.TreeSelectionListener l) {
114      tree.addTreeSelectionListener(l);
115   }
116
117   public TreePath getSelectionPath() {
118      return tree.getSelectionPath();
119   }
120
121   public boolean editCellAt(int row, int column, EventObject e) {
122      expandOrCollapseNode(e);// RG: Fix Issue 49!
123      boolean canEdit = super.editCellAt(row, column, e);
124      if(canEdit && isHierarchical(column)) {
125         repaint(getCellRect(row, column, false));
126      }
127      return canEdit;
128   }
129
130
131   /**
132    *  Overridden to message super and forward the method to the tree. Since the
133    *  tree is not actually in the component hieachy it will never receive this
134    *  unless we forward it in this manner.
135    */
136   public void updateUI() {
137      super.updateUI();
138      if(tree != null) {
139         tree.updateUI();
140      }
141      // Use the tree's default foreground and background colors in the
142      // table.
143      LookAndFeel.installColorsAndFont(this, "Tree.background",
144            "Tree.foreground", "Tree.font");
145   }
146
147   private void expandOrCollapseNode(EventObject e) {
148      if(e instanceof MouseEvent) {
149         MouseEvent me = (MouseEvent) e;
150         // If the modifiers are not 0 (or the left mouse button),
151         // tree may try and toggle the selection, and table
152         // will then try and toggle, resulting in the
153         // selection remaining the same. To avoid this, we
154         // only dispatch when the modifiers are 0 (or the left mouse
155         // button).
156         if(me.getModifiers() == 0 ||
157               me.getModifiers() == java.awt.event.InputEvent.BUTTON1_MASK) {
158            int count = getColumnCount();
159
160            for(int i = 0; i < count; i++) {
161               if(isHierarchical(i)) {
162                  int savedHeight = tree.getRowHeight();
163                  tree.setRowHeight(getRowHeight());
164                  MouseEvent pressed = new MouseEvent
165                        (tree,
166                        me.getID(),
167                        me.getWhen(),
168                        me.getModifiers(),
169                        me.getX() - getCellRect(0, i, false).x,
170                        me.getY(),
171                        me.getClickCount(),
172                        me.isPopupTrigger());
173                  tree.dispatchEvent(pressed);
174                  // For Mac OS X, we need to dispatch a MOUSE_RELEASED as well
175                  MouseEvent released = new MouseEvent
176                        (tree,
177                        java.awt.event.MouseEvent.MOUSE_RELEASED,
178                        pressed.getWhen(),
179                        pressed.getModifiers(),
180                        pressed.getX(),
181                        pressed.getY(),
182                        pressed.getClickCount(),
183                        pressed.isPopupTrigger());
184                  tree.dispatchEvent(released);
185                  tree.setRowHeight(savedHeight);
186                  break;
187               }
188            }
189         }
190      }
191   }
192
193
194   /**
195    *  Selects the node identified by the specified path. If any component of
196    *  the path is hidden (under a collapsed node), and getExpandsSelectedPaths
197    *  is true it is exposed (made viewable).
198    *
199    * @param  path  The new selectionPath value
200    */
201   public void setSelectionPath(TreePath path) {
202      tree.setSelectionPath(path);
203   }
204
205
206   /**
207    *  Overridden to pass the new rowHeight to the tree.
208    *
209    * @param  rowHeight  The new rowHeight value
210    */
211   public void setRowHeight(int rowHeight) {
212      super.setRowHeight(rowHeight);
213      if(tree != null && tree.getRowHeight() != rowHeight) {
214         tree.setRowHeight(getRowHeight());
215      }
216   }
217
218
219   /**
220    *  Determines if the specified column contains hierarchical nodes.
221    *
222    * @param  column  zero-based index of the column
223    * @return         true if the class of objects in the specified column
224    *      implement the {@link javax.swing.tree.TreeNode} interface; false
225    *      otherwise.
226    */
227   public boolean isHierarchical(int column) {
228      return TreeTableModel.class.isAssignableFrom(
229            getColumnClass(column));
230   }
231
232
233   public boolean isExpanded(TreePath path) {
234      return tree.isExpanded(path);
235   }
236
237
238   /**
239    *  Returns the TreePath for a given x,y location.
240    *
241    * @param  x  x value
242    * @param  y  y value
243    * @return    the <code>TreePath</code> for the givern location.
244    */
245   public TreePath getPathForLocation(int x, int y) {
246      int row = rowAtPoint(new java.awt.Point(x, y));
247      if(row == -1) {
248         return null;
249      }
250      return tree.getPathForRow(row);
251   }
252
253
254   /*
255       Workaround for BasicTableUI anomaly. Make sure the UI never tries to
256       paint the editor. The UI currently uses different techniques to
257       paint the trees and editors and overriding setBounds() below
258       is not the right thing to do for an editor. Returning -1 for the
259       editing row in this case, ensures the editor is never painted.
260     */
261   public int getEditingRow() {
262      return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1 :
263            editingRow;
264   }
265
266
267   /**
268    *  Returns the tree that is being shared between the model.
269    *
270    * @return    The tree value
271    */
272   public JTree getTree() {
273      return tree;
274   }
275
276
277   /**
278    *  A TreeCellRenderer that displays a JTree.
279    *
280    * @author    Joshua Gould
281    */
282   public class TreeTableCellRenderer extends JTree implements
283         TableCellRenderer {
284      /**  Last table/tree row asked to tree. */
285      protected int visibleRow;
286
287
288      public TreeTableCellRenderer(TreeModel model) {
289         super(model);
290      }
291
292
293      /**
294       *  updateUI is overridden to set the colors of the Tree's tree to match
295       *  that of the table.
296       */
297      public void updateUI() {
298         super.updateUI();
299         // Make the tree's cell tree use the table's cell selection
300         // colors.
301         TreeCellRenderer tcr = getCellRenderer();
302         if(tcr instanceof DefaultTreeCellRenderer) {
303            DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
304            // For 1.1 uncomment this, 1.2 has a bug that will cause an
305            // exception to be thrown if the border selection color is
306            // null.
307            // dtcr.setBorderSelectionColor(null);
308
309            //     dtcr.setTextSelectionColor(UIManager.getColor  ("Table.selectionForeground"));
310            dtcr.setBackgroundSelectionColor(UIManager.getColor
311                  ("Table.selectionBackground"));
312
313
314            dtcr.setTextSelectionColor(Color.white); // TODO: HACK: until I can figure out how to use the look and feel properly to set the colors
315
316
317         }
318      }
319
320
321      /**
322       *  Sublcassed to translate the graphics such that the last visible row
323       *  will be drawn at 0,0.
324       *
325       * @param  g  Description of the Parameter
326       */
327      public void paint(Graphics g) {
328         g.translate(0, -visibleRow * getRowHeight());
329         super.paint(g);
330      }
331
332
333      /**
334       *  Sets the row height of the tree, and forwards the row height to the
335       *  table.
336       *
337       * @param  rowHeight  The new rowHeight value
338       */
339      public void setRowHeight(int rowHeight) {
340         if(rowHeight > 0) {
341            super.setRowHeight(rowHeight);
342            if(JTreeTable.this != null &&
343                  JTreeTable.this.getRowHeight() != rowHeight) {
344               JTreeTable.this.setRowHeight(getRowHeight());
345            }
346         }
347      }
348
349
350      /**
351       *  This is overridden to set the height to match that of the JTable.
352       *
353       * @param  x  The new bounds value
354       * @param  y  The new bounds value
355       * @param  w  The new bounds value
356       * @param  h  The new bounds value
357       */
358      public void setBounds(int x, int y, int w, int h) {
359         super.setBounds(x, 0, w, JTreeTable.this.getHeight());
360      }
361
362
363      /**
364       *  TreeCellRenderer method. Overridden to update the visible row.
365       *
366       * @param  table       Description of the Parameter
367       * @param  value       Description of the Parameter
368       * @param  isSelected  Description of the Parameter
369       * @param  hasFocus    Description of the Parameter
370       * @param  row         Description of the Parameter
371       * @param  column      Description of the Parameter
372       * @return             The tableCellRendererComponent value
373       */
374      public Component getTableCellRendererComponent(JTable table,
375            Object value,
376            boolean isSelected,
377            boolean hasFocus,
378            int row, int column) {
379         if(isSelected) {
380            setBackground(table.getSelectionBackground());
381         } else {
382            setBackground(table.getBackground());
383         }
384
385         visibleRow = row;
386         return this;
387      }
388   }
389
390
391   /**
392    *  TreeTableCellEditor implementation. Component returned is the JTree.
393    *
394    * @author    Joshua Gould
395    */
396   public class TreeTableCellEditor extends AbstractCellEditor implements
397         TableCellEditor {
398      public Component getTableCellEditorComponent(JTable table,
399            Object value,
400            boolean isSelected,
401            int r, int c) {
402         return tree;
403      }
404
405
406      /**
407       *  Overridden to return false, and if the event is a mouse event it is
408       *  forwarded to the tree.<p>
409       *
410       *  The behavior for this is debatable, and should really be offered as a
411       *  property. By returning false, all keyboard actions are implemented in
412       *  terms of the table. By returning true, the tree would get a chance to
413       *  do something with the keyboard events. For the most part this is ok.
414       *  But for certain keys, such as left/right, the tree will
415       *  expand/collapse where as the table focus should really move to a
416       *  different column. Page up/down should also be implemented in terms of
417       *  the table. By returning false this also has the added benefit that
418       *  clicking outside of the bounds of the tree node, but still in the tree
419       *  column will select the row, whereas if this returned true that
420       *  wouldn't be the case. <p>
421       *
422       *  By returning false we are also enforcing the policy that the tree will
423       *  never be editable (at least by a key sequence).
424       *
425       * @param  e  Description of the Parameter
426       * @return    The cellEditable value
427       */
428      public boolean isCellEditable(EventObject e) {
429         if(e instanceof MouseEvent) {
430            for(int counter = getColumnCount() - 1; counter >= 0;
431                  counter--) {
432               if(getColumnClass(counter) == TreeTableModel.class) {
433                  MouseEvent me = (MouseEvent) e;
434                  MouseEvent newME = new MouseEvent(tree, me.getID(),
435                        me.getWhen(), me.getModifiers(),
436                        me.getX() - getCellRect(0, counter, true).x,
437                        me.getY(), me.getClickCount(),
438                        me.isPopupTrigger());
439                  tree.dispatchEvent(newME);
440                  break;
441               }
442            }
443         }
444         return false;
445      }
446   }
447
448
449   /**
450    *  ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel to
451    *  listen for changes in the ListSelectionModel it maintains. Once a change
452    *  in the ListSelectionModel happens, the paths are updated in the
453    *  DefaultTreeSelectionModel.
454    *
455    * @author    Joshua Gould
456    */
457   class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
458      /**  Set to true when we are updating the ListSelectionModel. */
459      protected boolean updatingListSelectionModel;
460
461
462      public ListToTreeSelectionModelWrapper() {
463         super();
464         getListSelectionModel().addListSelectionListener
465               (createListSelectionListener());
466      }
467
468
469      /**
470       *  This is overridden to set <code>updatingListSelectionModel</code> and
471       *  message super. This is the only place DefaultTreeSelectionModel alters
472       *  the ListSelectionModel.
473       */
474      public void resetRowSelection() {
475         if(!updatingListSelectionModel) {
476            updatingListSelectionModel = true;
477            try {
478               super.resetRowSelection();
479            } finally {
480               updatingListSelectionModel = false;
481            }
482         }
483         // Notice how we don't message super if
484         // updatingListSelectionModel is true. If
485         // updatingListSelectionModel is true, it implies the
486         // ListSelectionModel has already been updated and the
487         // paths are the only thing that needs to be updated.
488      }
489
490
491      /**
492       *  Creates and returns an instance of ListSelectionHandler.
493       *
494       * @return    Description of the Return Value
495       */
496      protected ListSelectionListener createListSelectionListener() {
497         return new ListSelectionHandler();
498      }
499
500
501      /**
502       *  If <code>updatingListSelectionModel</code> is false, this will reset
503       *  the selected paths from the selected rows in the list selection model.
504       */
505      protected void updateSelectedPathsFromSelectedRows() {
506         if(!updatingListSelectionModel) {
507            updatingListSelectionModel = true;
508            try {
509               // This is way expensive, ListSelectionModel needs an
510               // enumerator for iterating.
511               int min = listSelectionModel.getMinSelectionIndex();
512               int max = listSelectionModel.getMaxSelectionIndex();
513
514               clearSelection();
515               if(min != -1 && max != -1) {
516                  for(int counter = min; counter <= max; counter++) {
517                     if(listSelectionModel.isSelectedIndex(counter)) {
518                        TreePath selPath = tree.getPathForRow
519                              (counter);
520
521                        if(selPath != null) {
522                           addSelectionPath(selPath);
523                        }
524                     }
525                  }
526               }
527            } finally {
528               updatingListSelectionModel = false;
529            }
530         }
531      }
532
533
534      /**
535       *  Returns the list selection model. ListToTreeSelectionModelWrapper
536       *  listens for changes to this model and updates the selected paths
537       *  accordingly.
538       *
539       * @return    The listSelectionModel value
540       */
541      ListSelectionModel getListSelectionModel() {
542         return listSelectionModel;
543      }
544
545
546      /**
547       *  Class responsible for calling updateSelectedPathsFromSelectedRows when
548       *  the selection of the list changse.
549       *
550       * @author    Joshua Gould
551       */
552      class ListSelectionHandler implements ListSelectionListener {
553         public void valueChanged(ListSelectionEvent e) {
554            updateSelectedPathsFromSelectedRows();
555         }
556      }
557   }
558}
559