001: /*=============================================================================
002: * Copyright Texas Instruments 2002. All Rights Reserved.
003: *
004: * This program is free software; you can redistribute it and/or modify
005: * it under the terms of the GNU General Public License as published by
006: * the Free Software Foundation; either version 2 of the License, or
007: * (at your option) any later version.
008: *
009: * This program is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
012: * GNU General Public License for more details.
013: *
014: * You should have received a copy of the GNU General Public License
015: * along with this program; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018:
019: package ti.swing.treetable;
020:
021: import javax.swing.*;
022: import javax.swing.event.*;
023: import javax.swing.tree.*;
024: import javax.swing.table.*;
025:
026: import java.awt.Dimension;
027: import java.awt.Component;
028: import java.awt.Graphics;
029: import java.awt.Rectangle;
030:
031: import java.awt.event.MouseEvent;
032:
033: import java.util.EventObject;
034:
035: /**
036: * A combo tree and table component.
037: */
038: public class TreeTable extends JTable {
039: private TreeTableModel model;
040:
041: /**
042: */
043: protected TreeTableCellRenderer tree;
044:
045: /**
046: * Class Constructor.
047: */
048: public TreeTable() {
049: this (null);
050: }
051:
052: /**
053: * Class Constructor.
054: */
055: public TreeTable(TreeTableModel treeTableModel) {
056: super ();
057: if (treeTableModel != null)
058: setModel(treeTableModel);
059: }
060:
061: /**
062: * Get the tree-table model
063: */
064: public TreeTableModel getTreeTableModel() {
065: return model;
066: }
067:
068: /**
069: * Set the tree-table model
070: */
071: public void setModel(TreeTableModel treeTableModel) {
072: model = treeTableModel;
073:
074: // Create the tree. It will be used as a renderer and editor.
075: tree = new TreeTableCellRenderer(treeTableModel);
076:
077: // Install a tableModel representing the visible rows in the tree.
078: super .setModel(new TreeTableModelAdapter(treeTableModel, tree));
079:
080: // Force the JTable and JTree to share their row selection models.
081: ListToTreeSelectionModelWrapper selectionWrapper = new ListToTreeSelectionModelWrapper();
082: tree.setSelectionModel(selectionWrapper);
083: setSelectionModel(selectionWrapper.getListSelectionModel());
084:
085: // Install the tree editor renderer and editor.
086: setDefaultRenderer(TreeTableModel.class, tree);
087: setDefaultEditor(TreeTableModel.class,
088: new TreeTableCellEditor());
089:
090: setShowGrid(false);
091: setIntercellSpacing(new Dimension(0, 0));
092:
093: // And update the height of the trees row to match that of
094: // the table.
095: if (tree.getRowHeight() < 1) {
096: // Metal looks better like this.
097: setRowHeight(18);
098: }
099: }
100:
101: /**
102: * Overridden to message super and forward the method to the tree.
103: * Since the tree is not actually in the component hieachy it will
104: * never receive this unless we forward it in this manner.
105: */
106: public void updateUI() {
107: super .updateUI();
108:
109: if (tree != null)
110: tree.updateUI();
111:
112: // Use the tree's default foreground and background colors in the
113: // table.
114: LookAndFeel.installColorsAndFont(this , "Tree.background",
115: "Tree.foreground", "Tree.font");
116: }
117:
118: /**
119: * Workaround for BasicTableUI anomaly. Make sure the UI never tries to
120: * paint the editor. The UI currently uses different techniques to
121: * paint the renderers and editors and overriding setBounds() below
122: * is not the right thing to do for an editor. Returning -1 for the
123: * editing row in this case, ensures the editor is never painted.
124: */
125: public int getEditingRow() {
126: return (getColumnClass(editingColumn) == TreeTableModel.class) ? -1
127: : editingRow;
128: }
129:
130: /**
131: * Overridden to pass the new rowHeight to the tree.
132: */
133: public void setRowHeight(int rowHeight) {
134: super .setRowHeight(rowHeight);
135: if ((tree != null) && (tree.getRowHeight() != rowHeight))
136: tree.setRowHeight(getRowHeight());
137: }
138:
139: /**
140: * Returns the tree that is being shared between the model.
141: */
142: public JTree getTree() {
143: return tree;
144: }
145:
146: /**
147: * Set the selected tree path.
148: */
149: public void setSelectionPath(TreePath path) {
150: getTree().setSelectionPath(path);
151: }
152:
153: /**
154: * Set the selected tree paths.
155: */
156: public void setSelectionPaths(final TreePath[] paths) {
157: getTree().setSelectionPaths(paths);
158:
159: if (paths.length > 0) {
160: SwingUtilities.invokeLater(new Runnable() {
161: public void run() {
162: int row = getTree().getRowForPath(paths[0]);
163: getTree().expandPath(paths[0]);
164: getSelectionModel().setSelectionInterval(row, row);
165: scrollRectToVisible(getCellRect(row + 3, 0, true));
166: }
167: });
168: }
169: }
170:
171: /**
172: * Get the sected tree path
173: */
174: public TreePath getSelectionPath() {
175: return getTree().getSelectionPath();
176: }
177:
178: /**
179: * Get the selected tree paths
180: */
181: public TreePath[] getSelectionPaths() {
182: return getTree().getSelectionPaths();
183: }
184:
185: /**
186: */
187: public void addTreeSelectionListener(TreeSelectionListener l) {
188: getTree().addTreeSelectionListener(l);
189: }
190:
191: /**
192: */
193: public void removeTreeSelectionListener(TreeSelectionListener l) {
194: getTree().removeTreeSelectionListener(l);
195: }
196:
197: /**
198: * A TableCellRenderer that displays a JTree.
199: */
200: public class TreeTableCellRenderer extends JTree implements
201: TableCellRenderer {
202: /** Last table/tree row asked to renderer. */
203: protected int visibleRow;
204:
205: public TreeTableCellRenderer(TreeModel model) {
206: super (model);
207: }
208:
209: /**
210: * updateUI is overridden to set the colors of the Tree's renderer
211: * to match that of the table.
212: */
213: public void updateUI() {
214: super .updateUI();
215: // Make the tree's cell renderer use the table's cell selection
216: // colors.
217: TreeCellRenderer tcr = getCellRenderer();
218: if (tcr instanceof DefaultTreeCellRenderer) {
219: DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
220: dtcr.setTextSelectionColor(UIManager
221: .getColor("Table.selectionForeground"));
222: dtcr.setBackgroundSelectionColor(UIManager
223: .getColor("Table.selectionBackground"));
224: }
225: }
226:
227: /**
228: * Sets the row height of the tree, and forwards the row height to
229: * the table.
230: */
231: public void setRowHeight(int rowHeight) {
232: if (rowHeight > 0) {
233: super .setRowHeight(rowHeight);
234: if ((TreeTable.this != null)
235: && (TreeTable.this .getRowHeight() != rowHeight))
236: TreeTable.this .setRowHeight(this .getRowHeight());
237: }
238: }
239:
240: /**
241: * This is overridden to set the height to match that of the JTable.
242: */
243: public void setBounds(int x, int y, int w, int h) {
244: super .setBounds(x, 0, w, TreeTable.this .getHeight());
245: }
246:
247: /**
248: * Sublcassed to translate the graphics such that the last visible
249: * row will be drawn at 0,0.
250: */
251: public void paint(Graphics g) {
252: // XXX hack: the row sizes seem to be getting out of sync
253: if (TreeTable.this .getRowHeight() != this .getRowHeight())
254: TreeTable.this .setRowHeight(this .getRowHeight());
255:
256: g.translate(0, -visibleRow * getRowHeight());
257: super .paint(g);
258: }
259:
260: /**
261: * TableCellRenderer method. Overridden to update the visible row.
262: */
263: public Component getTableCellRendererComponent(JTable table,
264: Object value, boolean isSelected, boolean hasFocus,
265: int row, int column) {
266: if (isSelected)
267: setBackground(table.getSelectionBackground());
268: else
269: setBackground(table.getBackground());
270:
271: visibleRow = row;
272: return this ;
273: }
274: }
275:
276: /**
277: * TreeTableCellEditor implementation. Component returned is the
278: * JTree.
279: */
280: public class TreeTableCellEditor extends AbstractCellEditor
281: implements TableCellEditor {
282:
283: public Object getCellEditorValue() {
284: return null;
285: }
286:
287: public Component getTableCellEditorComponent(JTable table,
288: Object value, boolean isSelected, int r, int c) {
289: return tree;
290: }
291:
292: /**
293: * Overridden to return false, and if the event is a mouse event
294: * it is forwarded to the tree.<p>
295: * The behavior for this is debatable, and should really be offered
296: * as a property. By returning false, all keyboard actions are
297: * implemented in terms of the table. By returning true, the
298: * tree would get a chance to do something with the keyboard
299: * events. For the most part this is ok. But for certain keys,
300: * such as left/right, the tree will expand/collapse where as
301: * the table focus should really move to a different column. Page
302: * up/down should also be implemented in terms of the table.
303: * By returning false this also has the added benefit that clicking
304: * outside of the bounds of the tree node, but still in the tree
305: * column will select the row, whereas if this returned true
306: * that wouldn't be the case.
307: * <p>By returning false we are also enforcing the policy that
308: * the tree will never be editable (at least by a key sequence).
309: */
310: public boolean isCellEditable(EventObject e) {
311: if (e instanceof MouseEvent) {
312: for (int counter = getColumnCount() - 1; counter >= 0; counter--) {
313: if (getColumnClass(counter) == TreeTableModel.class) {
314: MouseEvent evt = (MouseEvent) e;
315: MouseEvent newEvt = new MouseEvent(tree, evt
316: .getID(), evt.getWhen(), evt
317: .getModifiers(), evt.getX()
318: - getCellRect(0, counter, true).x, evt
319: .getY(), evt.getClickCount(), evt
320: .isPopupTrigger());
321:
322: tree.dispatchEvent(newEvt);
323: break;
324: }
325: }
326: }
327: return false;
328: }
329: }
330:
331: /**
332: * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
333: * to listen for changes in the ListSelectionModel it maintains. Once
334: * a change in the ListSelectionModel happens, the paths are updated
335: * in the DefaultTreeSelectionModel.
336: */
337: class ListToTreeSelectionModelWrapper extends
338: DefaultTreeSelectionModel {
339: /** Set to true when we are updating the ListSelectionModel. */
340: protected boolean updatingListSelectionModel;
341:
342: public ListToTreeSelectionModelWrapper() {
343: super ();
344: getListSelectionModel().addListSelectionListener(
345: new ListSelectionHandler());
346: }
347:
348: /**
349: * Returns the list selection model. ListToTreeSelectionModelWrapper
350: * listens for changes to this model and updates the selected paths
351: * accordingly.
352: */
353: ListSelectionModel getListSelectionModel() {
354: return listSelectionModel;
355: }
356:
357: /**
358: * This is overridden to set <code>updatingListSelectionModel</code>
359: * and message super. This is the only place DefaultTreeSelectionModel
360: * alters the ListSelectionModel.
361: */
362: public void resetRowSelection() {
363: if (!updatingListSelectionModel) {
364: updatingListSelectionModel = true;
365: try {
366: super .resetRowSelection();
367: } finally {
368: updatingListSelectionModel = false;
369: }
370: }
371: }
372:
373: /**
374: * If <code>updatingListSelectionModel</code> is false, this will
375: * reset the selected paths from the selected rows in the list
376: * selection model.
377: */
378: protected void updateSelectedPathsFromSelectedRows() {
379: if (!updatingListSelectionModel) {
380: updatingListSelectionModel = true;
381: try {
382: // This is way expensive, ListSelectionModel needs an
383: // enumerator for iterating.
384: int min = listSelectionModel.getMinSelectionIndex();
385: int max = listSelectionModel.getMaxSelectionIndex();
386:
387: clearSelection();
388: if ((min != -1) && (max != -1)) {
389: for (int counter = min; counter <= max; counter++) {
390: if (listSelectionModel
391: .isSelectedIndex(counter)) {
392: TreePath selPath = tree
393: .getPathForRow(counter);
394:
395: if (selPath != null)
396: addSelectionPath(selPath);
397: }
398: }
399: }
400: } finally {
401: updatingListSelectionModel = false;
402: }
403: }
404: }
405:
406: // public void setSelectionPath( TreePath path )
407: // {
408: // System.out.println("setSelectionPath");
409: // super.setSelectionPath(path);
410: // }
411:
412: // public void setSelectionPaths( TreePath[] paths )
413: // {
414: // System.out.println("setSelectionPaths");
415: // super.setSelectionPaths(paths);
416: // }
417:
418: /**
419: * Class responsible for calling updateSelectedPathsFromSelectedRows
420: * when the selection of the list changse.
421: */
422: class ListSelectionHandler implements ListSelectionListener {
423: public void valueChanged(ListSelectionEvent e) {
424: updateSelectedPathsFromSelectedRows();
425: }
426: }
427: }
428:
429: /**
430: */
431: private static class TreeTableModelAdapter extends
432: AbstractTableModel {
433: JTree tree;
434: TreeTableModel treeTableModel;
435:
436: public TreeTableModelAdapter(TreeTableModel treeTableModel,
437: JTree tree) {
438: this .tree = tree;
439: this .treeTableModel = treeTableModel;
440:
441: tree.addTreeExpansionListener(new TreeExpansionListener() {
442:
443: // Don't use fireTableRowsInserted() here; the selection model
444: // would get updated twice.
445: public void treeExpanded(TreeExpansionEvent event) {
446: fireTableDataChanged();
447: }
448:
449: public void treeCollapsed(TreeExpansionEvent event) {
450: fireTableDataChanged();
451: }
452:
453: });
454:
455: // Install a TreeModelListener that can update the table when
456: // tree changes. We use delayedFireTableDataChanged as we can
457: // not be guaranteed the tree will have finished processing
458: // the event before us.
459: treeTableModel
460: .addTreeModelListener(new TreeModelListener() {
461:
462: public void treeNodesChanged(TreeModelEvent e) {
463: delayedFireTableDataChanged();
464: }
465:
466: public void treeNodesInserted(TreeModelEvent e) {
467: delayedFireTableDataChanged();
468: }
469:
470: public void treeNodesRemoved(TreeModelEvent e) {
471: delayedFireTableDataChanged();
472: }
473:
474: public void treeStructureChanged(
475: TreeModelEvent e) {
476: delayedFireTableDataChanged();
477: }
478:
479: });
480: }
481:
482: public int getColumnCount() {
483: return treeTableModel.getColumnCount();
484: }
485:
486: public String getColumnName(int column) {
487: return treeTableModel.getColumnName(column);
488: }
489:
490: public Class getColumnClass(int column) {
491: return treeTableModel.getColumnClass(column);
492: }
493:
494: public int getRowCount() {
495: return tree.getRowCount();
496: }
497:
498: protected Object nodeForRow(int row) {
499: TreePath treePath = tree.getPathForRow(row);
500: return treePath.getLastPathComponent();
501: }
502:
503: public Object getValueAt(int row, int column) {
504: return treeTableModel.getValueAt(nodeForRow(row), column);
505: }
506:
507: public boolean isCellEditable(int row, int column) {
508: return treeTableModel.isCellEditable(nodeForRow(row),
509: column);
510: }
511:
512: public void setValueAt(Object value, int row, int column) {
513: treeTableModel.setValueAt(value, nodeForRow(row), column);
514: }
515:
516: /**
517: * Invokes fireTableDataChanged after all the pending events have been
518: * processed. SwingUtilities.invokeLater is used to handle this.
519: */
520: protected void delayedFireTableDataChanged() {
521: SwingUtilities.invokeLater(new Runnable() {
522:
523: public void run() {
524: fireTableDataChanged();
525: }
526:
527: });
528: }
529: }
530: }
531:
532: /*
533: * Local Variables:
534: * tab-width: 2
535: * indent-tabs-mode: nil
536: * mode: java
537: * c-indentation-style: java
538: * c-basic-offset: 2
539: * eval: (c-set-offset 'substatement-open '0)
540: * eval: (c-set-offset 'case-label '+)
541: * eval: (c-set-offset 'inclass '+)
542: * eval: (c-set-offset 'inline-open '0)
543: * End:
544: */
|