1/*
2 * Copyright (c) 2001, 2008, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 *
23 */
24
25package sun.jvm.hotspot.ui.treetable;
26
27import java.awt.*;
28
29import javax.swing.*;
30import javax.swing.border.*;
31import javax.swing.event.*;
32import javax.swing.tree.*;
33import javax.swing.table.*;
34
35import java.awt.event.*;
36
37import java.util.EventObject;
38
39/**
40 * This example shows how to create a simple JTreeTable component,
41 * by using a JTree as a renderer (and editor) for the cells in a
42 * particular column in the JTable.
43 *
44 *
45 * @author Philip Milne
46 * @author Scott Violet
47 */
48public class JTreeTable extends JTable {
49    /** A subclass of JTree. */
50    protected TreeTableCellRenderer tree;
51
52    //////////////////////////
53    // Convenience routines //
54    //////////////////////////
55
56    private boolean treeEditable = true;
57    private boolean showsIcons   = true;
58
59    public boolean getTreeEditable() {
60        return treeEditable;
61    }
62
63    public void setTreeEditable(boolean editable) {
64        treeEditable = editable;
65    }
66
67    public boolean getShowsIcons() {
68        return showsIcons;
69    }
70
71    public void setShowsIcons(boolean show) {
72        showsIcons = show;
73    }
74
75    public void setRootVisible(boolean visible) {
76        tree.setRootVisible(visible);
77    }
78
79    public boolean getShowsRootHandles() {
80        return tree.getShowsRootHandles();
81    }
82
83    public void setShowsRootHandles(boolean newValue) {
84        tree.setShowsRootHandles(newValue);
85    }
86
87    public JTreeTable(TreeTableModel treeTableModel) {
88        super();
89
90        // Create the tree. It will be used as a renderer and editor.
91        tree = new TreeTableCellRenderer(treeTableModel);
92
93        // Install a tableModel representing the visible rows in the tree.
94        super.setModel(new TreeTableModelAdapter(treeTableModel, tree));
95
96        // Force the JTable and JTree to share their row selection models.
97        ListToTreeSelectionModelWrapper selectionWrapper = new
98                                ListToTreeSelectionModelWrapper();
99        tree.setSelectionModel(selectionWrapper);
100        setSelectionModel(selectionWrapper.getListSelectionModel());
101
102        // Install the tree editor renderer and editor.
103        setDefaultRenderer(TreeTableModel.class, tree);
104        setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());
105
106        // No grid.
107        setShowGrid(false);
108
109        // No intercell spacing
110        setIntercellSpacing(new Dimension(0, 0));
111
112        // And update the height of the trees row to match that of
113        // the table.
114        if (tree.getRowHeight() < 1) {
115            // Metal looks better like this.
116            setRowHeight(20);
117        }
118    }
119
120    /**
121     * Overridden to message super and forward the method to the tree.
122     * Since the tree is not actually in the component hieachy it will
123     * never receive this unless we forward it in this manner.
124     */
125    public void updateUI() {
126        super.updateUI();
127        if(tree != null) {
128            tree.updateUI();
129            // Do this so that the editor is referencing the current renderer
130            // from the tree. The renderer can potentially change each time
131            // laf changes.
132            setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());
133        }
134        // Use the tree's default foreground and background colors in the
135        // table.
136        LookAndFeel.installColorsAndFont(this, "Tree.background",
137                                         "Tree.foreground", "Tree.font");
138    }
139
140    /**
141     * Workaround for BasicTableUI anomaly. Make sure the UI never tries to
142     * resize the editor. The UI currently uses different techniques to
143     * paint the renderers and editors and overriding setBounds() below
144     * is not the right thing to do for an editor. Returning -1 for the
145     * editing row in this case, ensures the editor is never painted.
146     */
147    public int getEditingRow() {
148        return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1 :
149                editingRow;
150    }
151
152    /**
153     * Returns the actual row that is editing as <code>getEditingRow</code>
154     * will always return -1.
155     */
156    private int realEditingRow() {
157        return editingRow;
158    }
159
160    /**
161     * This is overriden to invoke supers implementation, and then,
162     * if the receiver is editing a Tree column, the editors bounds is
163     * reset. The reason we have to do this is because JTable doesn't
164     * think the table is being edited, as <code>getEditingRow</code> returns
165     * -1, and therefore doesn't automaticly resize the editor for us.
166     */
167    public void sizeColumnsToFit(int resizingColumn) {
168        super.sizeColumnsToFit(resizingColumn);
169        if (getEditingColumn() != -1 && getColumnClass(editingColumn) ==
170            TreeTableModel.class) {
171            Rectangle cellRect = getCellRect(realEditingRow(),
172                                             getEditingColumn(), false);
173            Component component = getEditorComponent();
174            component.setBounds(cellRect);
175            component.validate();
176        }
177    }
178
179    /**
180     * Overridden to pass the new rowHeight to the tree.
181     */
182    public void setRowHeight(int rowHeight) {
183        super.setRowHeight(rowHeight);
184        if (tree != null && tree.getRowHeight() != rowHeight) {
185            tree.setRowHeight(getRowHeight());
186        }
187    }
188
189    /**
190     * Returns the tree that is being shared between the model.
191     */
192    public JTree getTree() {
193        return tree;
194    }
195
196    /**
197     * Overriden to invoke repaint for the particular location if
198     * the column contains the tree. This is done as the tree editor does
199     * not fill the bounds of the cell, we need the renderer to paint
200     * the tree in the background, and then draw the editor over it.
201     */
202    public boolean editCellAt(int row, int column, EventObject e){
203        boolean retValue = super.editCellAt(row, column, e);
204        if (retValue && getColumnClass(column) == TreeTableModel.class) {
205            repaint(getCellRect(row, column, false));
206        }
207        return retValue;
208    }
209
210    /** A DefaultTreeCellRenderer which can optionally skip drawing
211        all icons. */
212    class JTreeTableCellRenderer extends DefaultTreeCellRenderer {
213        public Icon getClosedIcon()        { return (showsIcons ? super.getClosedIcon()        : null); }
214        public Icon getDefaultClosedIcon() { return (showsIcons ? super.getDefaultClosedIcon() : null); }
215        public Icon getDefaultLeafIcon()   { return (showsIcons ? super.getDefaultLeafIcon()   : null); }
216        public Icon getDefaultOpenIcon()   { return (showsIcons ? super.getDefaultOpenIcon()   : null); }
217        public Icon getLeafIcon()          { return (showsIcons ? super.getLeafIcon()          : null); }
218        public Icon getOpenIcon()          { return (showsIcons ? super.getOpenIcon()          : null); }
219    }
220
221    /**
222     * A TreeCellRenderer that displays a JTree.
223     */
224    public class TreeTableCellRenderer extends JTree implements
225                 TableCellRenderer {
226        /** Last table/tree row asked to renderer. */
227        protected int visibleRow;
228        /** Border to draw around the tree, if this is non-null, it will
229         * be painted. */
230        protected Border highlightBorder;
231
232        public TreeTableCellRenderer(TreeModel model) {
233            super(model);
234            setCellRenderer(new JTreeTableCellRenderer());
235        }
236
237        /**
238         * updateUI is overridden to set the colors of the Tree's renderer
239         * to match that of the table.
240         */
241        public void updateUI() {
242            super.updateUI();
243            // Make the tree's cell renderer use the table's cell selection
244            // colors.
245            TreeCellRenderer tcr = getCellRenderer();
246            if (tcr instanceof DefaultTreeCellRenderer) {
247                DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer)tcr);
248                // For 1.1 uncomment this, 1.2 has a bug that will cause an
249                // exception to be thrown if the border selection color is
250                // null.
251                // dtcr.setBorderSelectionColor(null);
252                dtcr.setTextSelectionColor(UIManager.getColor
253                                           ("Table.selectionForeground"));
254                dtcr.setBackgroundSelectionColor(UIManager.getColor
255                                                ("Table.selectionBackground"));
256            }
257        }
258
259        /**
260         * Sets the row height of the tree, and forwards the row height to
261         * the table.
262         */
263        public void setRowHeight(int rowHeight) {
264            if (rowHeight > 0) {
265                super.setRowHeight(rowHeight);
266                if (JTreeTable.this != null &&
267                    JTreeTable.this.getRowHeight() != rowHeight) {
268                    JTreeTable.this.setRowHeight(getRowHeight());
269                }
270            }
271        }
272
273        /**
274         * This is overridden to set the height to match that of the JTable.
275         */
276        public void setBounds(int x, int y, int w, int h) {
277            super.setBounds(x, 0, w, JTreeTable.this.getHeight());
278        }
279
280        /**
281         * Sublcassed to translate the graphics such that the last visible
282         * row will be drawn at 0,0.
283         */
284        public void paint(Graphics g) {
285            g.translate(0, -visibleRow * getRowHeight());
286            super.paint(g);
287            // Draw the Table border if we have focus.
288            if (highlightBorder != null) {
289                highlightBorder.paintBorder(this, g, 0, visibleRow *
290                                            getRowHeight(), getWidth(),
291                                            getRowHeight());
292            }
293        }
294
295        /**
296         * TreeCellRenderer method. Overridden to update the visible row.
297         */
298        public Component getTableCellRendererComponent(JTable table,
299                                                       Object value,
300                                                       boolean isSelected,
301                                                       boolean hasFocus,
302                                                       int row, int column) {
303            Color background;
304            Color foreground;
305
306            if(isSelected) {
307                background = table.getSelectionBackground();
308                foreground = table.getSelectionForeground();
309            }
310            else {
311                background = table.getBackground();
312                foreground = table.getForeground();
313            }
314            highlightBorder = null;
315            if (realEditingRow() == row && getEditingColumn() == column) {
316                background = UIManager.getColor("Table.focusCellBackground");
317                foreground = UIManager.getColor("Table.focusCellForeground");
318            }
319            else if (hasFocus) {
320                highlightBorder = UIManager.getBorder
321                                  ("Table.focusCellHighlightBorder");
322                if (isCellEditable(row, column)) {
323                    background = UIManager.getColor
324                                 ("Table.focusCellBackground");
325                    foreground = UIManager.getColor
326                                 ("Table.focusCellForeground");
327                }
328            }
329
330            visibleRow = row;
331            setBackground(background);
332
333            TreeCellRenderer tcr = getCellRenderer();
334            if (tcr instanceof DefaultTreeCellRenderer) {
335                DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer)tcr);
336                if (isSelected) {
337                    dtcr.setTextSelectionColor(foreground);
338                    dtcr.setBackgroundSelectionColor(background);
339                }
340                else {
341                    dtcr.setTextNonSelectionColor(foreground);
342                    dtcr.setBackgroundNonSelectionColor(background);
343                }
344            }
345            return this;
346        }
347    }
348
349
350    /**
351     * An editor that can be used to edit the tree column. This extends
352     * DefaultCellEditor and uses a JTextField (actually, TreeTableTextField)
353     * to perform the actual editing.
354     * <p>To support editing of the tree column we can not make the tree
355     * editable. The reason this doesn't work is that you can not use
356     * the same component for editing and renderering. The table may have
357     * the need to paint cells, while a cell is being edited. If the same
358     * component were used for the rendering and editing the component would
359     * be moved around, and the contents would change. When editing, this
360     * is undesirable, the contents of the text field must stay the same,
361     * including the caret blinking, and selections persisting. For this
362     * reason the editing is done via a TableCellEditor.
363     * <p>Another interesting thing to be aware of is how tree positions
364     * its render and editor. The render/editor is responsible for drawing the
365     * icon indicating the type of node (leaf, branch...). The tree is
366     * responsible for drawing any other indicators, perhaps an additional
367     * +/- sign, or lines connecting the various nodes. So, the renderer
368     * is positioned based on depth. On the other hand, table always makes
369     * its editor fill the contents of the cell. To get the allusion
370     * that the table cell editor is part of the tree, we don't want the
371     * table cell editor to fill the cell bounds. We want it to be placed
372     * in the same manner as tree places it editor, and have table message
373     * the tree to paint any decorations the tree wants. Then, we would
374     * only have to worry about the editing part. The approach taken
375     * here is to determine where tree would place the editor, and to override
376     * the <code>reshape</code> method in the JTextField component to
377     * nudge the textfield to the location tree would place it. Since
378     * JTreeTable will paint the tree behind the editor everything should
379     * just work. So, that is what we are doing here. Determining of
380     * the icon position will only work if the TreeCellRenderer is
381     * an instance of DefaultTreeCellRenderer. If you need custom
382     * TreeCellRenderers, that don't descend from DefaultTreeCellRenderer,
383     * and you want to support editing in JTreeTable, you will have
384     * to do something similiar.
385     */
386    public class TreeTableCellEditor extends DefaultCellEditor {
387        public TreeTableCellEditor() {
388            super(new TreeTableTextField());
389        }
390
391        /**
392         * Overriden to determine an offset that tree would place the
393         * editor at. The offset is determined from the
394         * <code>getRowBounds</code> JTree method, and additionaly
395         * from the icon DefaultTreeCellRenderer will use.
396         * <p>The offset is then set on the TreeTableTextField component
397         * created in the constructor, and returned.
398         */
399        public Component getTableCellEditorComponent(JTable table,
400                                                     Object value,
401                                                     boolean isSelected,
402                                                     int r, int c) {
403            Component component = super.getTableCellEditorComponent
404                (table, value, isSelected, r, c);
405            JTree t = getTree();
406            boolean rv = t.isRootVisible();
407            int offsetRow = rv ? r : r - 1;
408            Rectangle bounds = t.getRowBounds(offsetRow);
409            int offset = bounds.x;
410            TreeCellRenderer tcr = t.getCellRenderer();
411            if (tcr instanceof DefaultTreeCellRenderer) {
412                Object node = t.getPathForRow(offsetRow).
413                                getLastPathComponent();
414                Icon icon;
415                if (t.getModel().isLeaf(node))
416                    icon = ((DefaultTreeCellRenderer)tcr).getLeafIcon();
417                else if (tree.isExpanded(offsetRow))
418                    icon = ((DefaultTreeCellRenderer)tcr).getOpenIcon();
419                else
420                    icon = ((DefaultTreeCellRenderer)tcr).getClosedIcon();
421                if (icon != null) {
422                    offset += ((DefaultTreeCellRenderer)tcr).getIconTextGap() +
423                              icon.getIconWidth();
424                }
425            }
426            ((TreeTableTextField)getComponent()).offset = offset;
427            return component;
428        }
429
430        /**
431         * This is overriden to forward the event to the tree. This will
432         * return true if the click count >= 3, or the event is null.
433         */
434        public boolean isCellEditable(EventObject e) {
435            if (e instanceof MouseEvent) {
436                MouseEvent me = (MouseEvent)e;
437                // If the modifiers are not 0 (or the left mouse button),
438                // tree may try and toggle the selection, and table
439                // will then try and toggle, resulting in the
440                // selection remaining the same. To avoid this, we
441                // only dispatch when the modifiers are 0 (or the left mouse
442                // button).
443                if (me.getModifiers() == 0 ||
444                    me.getModifiers() == InputEvent.BUTTON1_MASK) {
445                    for (int counter = getColumnCount() - 1; counter >= 0;
446                         counter--) {
447                        if (getColumnClass(counter) == TreeTableModel.class) {
448                            MouseEvent newME = new MouseEvent
449                                  (JTreeTable.this.tree, me.getID(),
450                                   me.getWhen(), me.getModifiers(),
451                                   me.getX() - getCellRect(0, counter, true).x,
452                                   me.getY(), me.getClickCount(),
453                                   me.isPopupTrigger());
454                            JTreeTable.this.tree.dispatchEvent(newME);
455                            break;
456                        }
457                    }
458                }
459                if (me.getClickCount() >= 3) {
460                    return treeEditable;
461                }
462                return false;
463            }
464            if (e == null) {
465                return treeEditable;
466            }
467            return false;
468        }
469    }
470
471
472    /**
473     * Component used by TreeTableCellEditor. The only thing this does
474     * is to override the <code>reshape</code> method, and to ALWAYS
475     * make the x location be <code>offset</code>.
476     */
477    static class TreeTableTextField extends JTextField {
478        public int offset;
479
480        public void setBounds(int x, int y, int w, int h) {
481            int newX = Math.max(x, offset);
482            super.setBounds(newX, y, w - (newX - x), h);
483        }
484    }
485
486
487    /**
488     * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
489     * to listen for changes in the ListSelectionModel it maintains. Once
490     * a change in the ListSelectionModel happens, the paths are updated
491     * in the DefaultTreeSelectionModel.
492     */
493    class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
494        /** Set to true when we are updating the ListSelectionModel. */
495        protected boolean         updatingListSelectionModel;
496
497        public ListToTreeSelectionModelWrapper() {
498            super();
499            getListSelectionModel().addListSelectionListener
500                                    (createListSelectionListener());
501        }
502
503        /**
504         * Returns the list selection model. ListToTreeSelectionModelWrapper
505         * listens for changes to this model and updates the selected paths
506         * accordingly.
507         */
508        ListSelectionModel getListSelectionModel() {
509            return listSelectionModel;
510        }
511
512        /**
513         * This is overridden to set <code>updatingListSelectionModel</code>
514         * and message super. This is the only place DefaultTreeSelectionModel
515         * alters the ListSelectionModel.
516         */
517        public void resetRowSelection() {
518            if(!updatingListSelectionModel) {
519                updatingListSelectionModel = true;
520                try {
521                    super.resetRowSelection();
522                }
523                finally {
524                    updatingListSelectionModel = false;
525                }
526            }
527            // Notice how we don't message super if
528            // updatingListSelectionModel is true. If
529            // updatingListSelectionModel is true, it implies the
530            // ListSelectionModel has already been updated and the
531            // paths are the only thing that needs to be updated.
532        }
533
534        /**
535         * Creates and returns an instance of ListSelectionHandler.
536         */
537        protected ListSelectionListener createListSelectionListener() {
538            return new ListSelectionHandler();
539        }
540
541        /**
542         * If <code>updatingListSelectionModel</code> is false, this will
543         * reset the selected paths from the selected rows in the list
544         * selection model.
545         */
546        protected void updateSelectedPathsFromSelectedRows() {
547            if(!updatingListSelectionModel) {
548                updatingListSelectionModel = true;
549                try {
550                    // This is way expensive, ListSelectionModel needs an
551                    // enumerator for iterating.
552                    int        min = listSelectionModel.getMinSelectionIndex();
553                    int        max = listSelectionModel.getMaxSelectionIndex();
554
555                    clearSelection();
556                    if(min != -1 && max != -1) {
557                        for(int counter = min; counter <= max; counter++) {
558                            if(listSelectionModel.isSelectedIndex(counter)) {
559                                TreePath     selPath = tree.getPathForRow
560                                                            (counter);
561
562                                if(selPath != null) {
563                                    addSelectionPath(selPath);
564                                }
565                            }
566                        }
567                    }
568                }
569                finally {
570                    updatingListSelectionModel = false;
571                }
572            }
573        }
574
575        /**
576         * Class responsible for calling updateSelectedPathsFromSelectedRows
577         * when the selection of the list changse.
578         */
579        class ListSelectionHandler implements ListSelectionListener {
580            public void valueChanged(ListSelectionEvent e) {
581                updateSelectedPathsFromSelectedRows();
582            }
583        }
584    }
585}
586