001    package ui.treetable;
002    
003    import ui.model.TreeTableModel;
004    import ui.model.TreeTableModelAdapter;
005    import ui.editor.AbstractCellEditor;
006    
007    import javax.swing.*;
008    import javax.swing.event.*;
009    import javax.swing.tree.*;
010    import javax.swing.table.*;
011    
012    import java.awt.Dimension;
013    import java.awt.Component;
014    import java.awt.Graphics;
015    import java.awt.Rectangle;
016    
017    import java.awt.event.MouseEvent;
018    
019    import java.util.EventObject;
020    
021    /**
022     * This example shows how to create a simple JTreeTable component,
023     * by using a JTree as a renderer (and editor) for the cells in a
024     * particular column in the JTable.
025     *
026     * @version 1.2 10/27/98
027     *
028     * @author Philip Milne
029     * @author Scott Violet
030     */
031    public class JTreeTable extends JTable {
032        /** A subclass of JTree. */
033        protected TreeTableCellRenderer tree;
034    
035        public JTreeTable(TreeTableModel treeTableModel) {
036            super();
037    
038            // Create the tree. It will be used as a renderer and editor.
039            tree = new TreeTableCellRenderer(treeTableModel);
040    
041            // Install a tableModel representing the visible rows in the tree.
042            super.setModel(new TreeTableModelAdapter(treeTableModel, tree));
043    
044            // Force the JTable and JTree to share their row selection models.
045            ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper();
046            tree.setSelectionModel(selectionWrapper);
047            setSelectionModel(selectionWrapper.getListSelectionModel());
048    
049            // Install the tree editor renderer and editor.
050            setDefaultRenderer(TreeTableModel.class, tree);
051            setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());
052    
053            // No grid.
054            setShowGrid(false);
055    
056            // No intercell spacing
057            setIntercellSpacing(new Dimension(0, 0));
058    
059            // Update the height of the trees row to match that of the table. Metal looks better like this.
060            if (tree.getRowHeight() < 1) setRowHeight(18);
061    
062        }
063    
064        /**
065         * Overridden to message super and forward the method to the tree.
066         * Since the tree is not actually in the component hieachy it will
067         * never receive this unless we forward it in this manner.
068         */
069        public void updateUI() {
070            super.updateUI();
071            if(tree != null) tree.updateUI();
072            // Use the tree's default foreground and background colors in the table.
073            LookAndFeel.installColorsAndFont(this, "Tree.background", "Tree.foreground", "Tree.font");
074        }
075    
076        /* Workaround for BasicTableUI anomaly. Make sure the UI never tries to
077         * paint the editor. The UI currently uses different techniques to
078         * paint the renderers and editors and overriding setBounds() below
079         * is not the right thing to do for an editor. Returning -1 for the
080         * editing row in this case, ensures the editor is never painted.
081         */
082        public int getEditingRow() {
083            return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1 : editingRow;
084        }
085    
086        /**
087         * Overridden to pass the new rowHeight to the tree.
088         */
089        public void setRowHeight(int rowHeight) {
090            super.setRowHeight(rowHeight);
091            if (tree != null && tree.getRowHeight() != rowHeight) {
092                tree.setRowHeight(getRowHeight());
093            }
094        }
095    
096        /**
097         * Returns the tree that is being shared between the model.
098         */
099        public JTree getTree() {
100            return tree;
101        }
102    
103        /**
104         * A TreeCellRenderer that displays a JTree.
105         */
106        public class TreeTableCellRenderer extends JTree implements TableCellRenderer {
107            /** Last table/tree row asked to renderer. */
108            protected int visibleRow;
109    
110            public TreeTableCellRenderer(TreeModel model) {
111                super(model);
112            }
113    
114            /**
115             * updateUI is overridden to set the colors of the Tree's renderer
116             * to match that of the table.
117             */
118            public void updateUI() {
119                super.updateUI();
120                // Make the tree's cell renderer use the table's cell selection colors.
121                TreeCellRenderer tcr = getCellRenderer();
122                if (tcr instanceof DefaultTreeCellRenderer) {
123                    DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer)tcr);
124                    // For 1.1 uncomment this, 1.2 has a bug that will cause an
125                    // exception to be thrown if the border selection color is null.
126                    // dtcr.setBorderSelectionColor(null);
127                    dtcr.setTextSelectionColor(UIManager.getColor("Table.selectionForeground"));
128                    dtcr.setBackgroundSelectionColor(UIManager.getColor("Table.selectionBackground"));
129                }
130            }
131    
132            /**
133             * Sets the row height of the tree, and forwards the row height to
134             * the table.
135             */
136            public void setRowHeight(int rowHeight) {
137                if (rowHeight > 0) {
138                    super.setRowHeight(rowHeight);
139                    if (JTreeTable.this != null &&
140                        JTreeTable.this.getRowHeight() != rowHeight) {
141                        JTreeTable.this.setRowHeight(getRowHeight());
142                    }
143                }
144            }
145    
146            /**
147             * This is overridden to set the height to match that of the JTable.
148             */
149            public void setBounds(int x, int y, int w, int h) {
150                super.setBounds(x, 0, w, JTreeTable.this.getHeight());
151            }
152    
153            /**
154             * Sublcassed to translate the graphics such that the last visible
155             * row will be drawn at 0,0.
156             */
157            public void paint(Graphics g) {
158                g.translate(0, -visibleRow * getRowHeight());
159                super.paint(g);
160            }
161    
162            /**
163             * TreeCellRenderer method. Overridden to update the visible row.
164             */
165            public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
166                if(isSelected)
167                    setBackground(table.getSelectionBackground());
168                else
169                    setBackground(table.getBackground());
170    
171                visibleRow = row;
172                return this;
173            }
174        }
175    
176        /**
177         * TreeTableCellEditor implementation. Component returned is the
178         * JTree.
179         */
180        public class TreeTableCellEditor extends AbstractCellEditor implements TableCellEditor {
181            public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
182                return tree;
183            }
184    
185            /**
186             * Overridden to return false, and if the event is a mouse event
187             * it is forwarded to the tree.<p>
188             * The behavior for this is debatable, and should really be offered
189             * as a property. By returning false, all keyboard actions are
190             * implemented in terms of the table. By returning true, the
191             * tree would get a chance to do something with the keyboard
192             * events. For the most part this is ok. But for certain keys,
193             * such as left/right, the tree will expand/collapse where as
194             * the table focus should really move to a different column. Page
195             * up/down should also be implemented in terms of the table.
196             * By returning false this also has the added benefit that clicking
197             * outside of the bounds of the tree node, but still in the tree
198             * column will select the row, whereas if this returned true
199             * that wouldn't be the case.
200             * <p>By returning false we are also enforcing the policy that
201             * the tree will never be editable (at least by a key sequence).
202             */
203            public boolean isCellEditable(EventObject e) {
204                if (e instanceof MouseEvent) {
205                    for (int counter = getColumnCount() - 1; counter >= 0; counter--) {
206                        if (getColumnClass(counter) == TreeTableModel.class) {
207                            MouseEvent me = (MouseEvent)e;
208                            MouseEvent newME = new MouseEvent(tree, me.getID(),
209                                       me.getWhen(), me.getModifiers(),
210                                       me.getX() - getCellRect(0, counter, true).x,
211                                       me.getY(), me.getClickCount(),
212                                       me.isPopupTrigger());
213                            tree.dispatchEvent(newME);
214                            break;
215                        }
216                    }
217                }
218                return false;
219            }
220        }
221    
222    
223        /**
224         * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
225         * to listen for changes in the ListSelectionModel it maintains. Once
226         * a change in the ListSelectionModel happens, the paths are updated
227         * in the DefaultTreeSelectionModel.
228         */
229        class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
230            /** Set to true when we are updating the ListSelectionModel. */
231            protected boolean updatingListSelectionModel;
232    
233            public ListToTreeSelectionModelWrapper() {
234                super();
235                getListSelectionModel().addListSelectionListener(createListSelectionListener());
236            }
237    
238            /**
239             * Returns the list selection model. ListToTreeSelectionModelWrapper
240             * listens for changes to this model and updates the selected paths
241             * accordingly.
242             */
243            ListSelectionModel getListSelectionModel() {
244                return listSelectionModel;
245            }
246    
247            /**
248             * This is overridden to set <code>updatingListSelectionModel</code>
249             * and message super. This is the only place DefaultTreeSelectionModel
250             * alters the ListSelectionModel.
251             */
252            public void resetRowSelection() {
253                if(!updatingListSelectionModel) {
254                    updatingListSelectionModel = true;
255                    try {
256                        super.resetRowSelection();
257                    }
258                    finally {
259                        updatingListSelectionModel = false;
260                    }
261                }
262                // Notice how we don't message super if
263                // updatingListSelectionModel is true. If
264                // updatingListSelectionModel is true, it implies the
265                // ListSelectionModel has already been updated and the
266                // paths are the only thing that needs to be updated.
267            }
268    
269            /**
270             * Creates and returns an instance of ListSelectionHandler.
271             */
272            protected ListSelectionListener createListSelectionListener() {
273                return new ListSelectionHandler();
274            }
275    
276            /**
277             * If <code>updatingListSelectionModel</code> is false, this will
278             * reset the selected paths from the selected rows in the list
279             * selection model.
280             */
281            protected void updateSelectedPathsFromSelectedRows() {
282                if(!updatingListSelectionModel) {
283                    updatingListSelectionModel = true;
284                    try {
285                        // This is way expensive, ListSelectionModel needs an enumerator for iterating.
286                        int min = listSelectionModel.getMinSelectionIndex();
287                        int max = listSelectionModel.getMaxSelectionIndex();
288    
289                        clearSelection();
290                        if(min != -1 && max != -1) {
291                            for(int counter = min; counter <= max; counter++) {
292                                if(listSelectionModel.isSelectedIndex(counter)) {
293                                    TreePath selPath = tree.getPathForRow(counter);
294                                    if(selPath != null) addSelectionPath(selPath);
295                                }
296                            }
297                        }
298                    } finally {
299                        updatingListSelectionModel = false;
300                    }
301                }
302            }
303    
304            /**
305             * Class responsible for calling updateSelectedPathsFromSelectedRows
306             * when the selection of the list changse.
307             */
308            class ListSelectionHandler implements ListSelectionListener {
309                public void valueChanged(ListSelectionEvent e) {
310                    updateSelectedPathsFromSelectedRows();
311                }
312            }
313        }
314    }