1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38 package org.xnap.gui.table;
39
40 import java.awt.Color;
41 import java.awt.Component;
42 import java.awt.Dimension;
43 import java.awt.Graphics;
44 import java.awt.*;
45 import java.awt.event.*;
46 import java.awt.event.MouseEvent;
47 import java.util.EventObject;
48
49 import javax.swing.AbstractCellEditor;
50 import javax.swing.JTable;
51 import javax.swing.JTree;
52 import javax.swing.ListSelectionModel;
53 import javax.swing.*;
54 import javax.swing.UIManager;
55 import javax.swing.border.Border;
56 import javax.swing.event.ListSelectionEvent;
57 import javax.swing.event.ListSelectionListener;
58 import javax.swing.table.TableCellEditor;
59 import javax.swing.table.TableCellRenderer;
60 import javax.swing.table.TableColumnModel;
61 import javax.swing.tree.DefaultTreeCellRenderer;
62 import javax.swing.tree.DefaultTreeSelectionModel;
63 import javax.swing.tree.TreeCellRenderer;
64 import javax.swing.tree.TreeModel;
65 import javax.swing.tree.TreePath;
66
67 /***
68 * This example shows how to create a simple JTreeTable component,
69 * by using a JTree as a renderer (and editor) for the cells in a
70 * particular column in the JTable.
71 *
72 * @version 1.2 10/27/98
73 *
74 * @author Philip Milne
75 * @author Scott Violet
76 */
77 public class JTreeTable extends ColoredTable {
78 /*** A subclass of JTree. */
79 protected TreeTableCellRenderer tree;
80
81 public JTreeTable(TreeTableModel treeTableModel) {
82 this(treeTableModel, null);
83 }
84
85 public JTreeTable(TreeTableModel treeTableModel, TableColumnModel cm) {
86 super(null, cm);
87
88 setModel(treeTableModel);
89
90
91 setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());
92
93
94 setShowGrid(false);
95
96
97 setIntercellSpacing(new Dimension(0, 0));
98
99
100
101
102
103
104
105
106
107
108 int rowHeight = UIManager.getInt("Tree.rowHeight");
109 if (rowHeight > 0) {
110 setRowHeight(rowHeight);
111 }
112 else {
113 if (tree.getRowHeight() < 1) {
114
115 setRowHeight(20);
116 }
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
130
131
132 setDefaultEditor(TreeTableModel.class, new TreeTableCellEditor());
133 }
134
135
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 * Overrides <code>JComponent</code>'s <code>getToolTipText</code>
154 * method in order to allow the renderer's tips to be used
155 * if it has text set.
156 * <p>
157 * <bold>Note:</bold> For <code>JTable</code> to properly display
158 * tooltips of its renderers
159 * <code>JTable</code> must be a registered component with the
160 * <code>ToolTipManager</code>.
161 * This is done automatically in <code>initializeLocalVars</code>,
162 * but if at a later point <code>JTable</code> is told
163 * <code>setToolTipText(null)</code> it will unregister the table
164 * component, and no tips from renderers will display anymore.
165 *
166 * @see JComponent#getToolTipText
167 */
168 public String getToolTipText(MouseEvent event) {
169 String tip = null;
170 Point p = event.getPoint();
171
172
173 int hitColumnIndex = columnAtPoint(p);
174 int hitRowIndex = rowAtPoint(p);
175
176 if ((hitColumnIndex != -1) && (hitRowIndex != -1)) {
177 TableCellRenderer renderer = getCellRenderer(hitRowIndex, hitColumnIndex);
178 Component component = prepareRenderer(renderer, hitRowIndex, hitColumnIndex);
179
180
181
182 if (component instanceof TreeTableCellRenderer) {
183 tip = ((TreeTableCellRenderer)component).getToolTipText(event);
184 }
185 else if (component instanceof JComponent) {
186
187 Rectangle cellRect = getCellRect(hitRowIndex, hitColumnIndex, false);
188 p.translate(-cellRect.x, -cellRect.y);
189 MouseEvent newEvent = new MouseEvent
190 (component, event.getID(),
191 event.getWhen(), event.getModifiers(),
192 p.x, p.y, event.getClickCount(),
193 event.isPopupTrigger());
194
195 tip = ((JComponent)component).getToolTipText(newEvent);
196 }
197 }
198
199
200 if (tip == null)
201 tip = getToolTipText();
202
203 return tip;
204 }
205
206
207 /***
208 * Returns the actual row that is editing as
209 * <code>getEditingRow</code> will always return -1. */
210 private int realEditingRow()
211 {
212 return editingRow;
213 }
214
215 /***
216 * This is overriden to invoke supers implementation, and then,
217 * if the receiver is editing a Tree column, the editors bounds is
218 * reset. The reason we have to do this is because JTable doesn't
219 * think the table is being edited, as <code>getEditingRow</code> returns
220 * -1, and therefore doesn't automaticly resize the editor for us.
221 */
222 public void sizeColumnsToFit(int resizingColumn) {
223 super.sizeColumnsToFit(resizingColumn);
224 if (getEditingColumn() != -1 && getColumnClass(editingColumn) ==
225 TreeTableModel.class) {
226 Rectangle cellRect = getCellRect(realEditingRow(),
227 getEditingColumn(), false);
228 Component component = getEditorComponent();
229 component.setBounds(cellRect);
230 component.validate();
231 }
232 }
233
234 /***
235 * Overridden to pass the new rowHeight to the tree.
236 */
237 public void setRowHeight(int rowHeight) {
238 super.setRowHeight(rowHeight);
239 if (tree != null && tree.getRowHeight() != rowHeight) {
240 tree.setRowHeight(getRowHeight());
241 }
242 }
243
244 public void setModel(TreeTableModel treeTableModel) {
245 boolean newTree;
246
247 if (tree == null) {
248
249 tree = new TreeTableCellRenderer(treeTableModel);
250 newTree = true;
251 }
252 else {
253 tree.setModel(treeTableModel);
254 newTree = false;
255 }
256
257
258 super.setModel(new TreeTableModelAdapter(treeTableModel, tree));
259
260 if (newTree) {
261
262 ListToTreeSelectionModelWrapper selectionWrapper = new
263 ListToTreeSelectionModelWrapper();
264 tree.setSelectionModel(selectionWrapper);
265 setSelectionModel(selectionWrapper.getListSelectionModel());
266
267
268 setDefaultRenderer(TreeTableModel.class, tree);
269 }
270 }
271
272 public void setModel(TreeTableModelAdapter treeTableModelAdapter) {
273 boolean newTree;
274
275 if (tree == null) {
276
277 tree = new TreeTableCellRenderer(treeTableModelAdapter.treeTableModel);
278 newTree = true;
279 }
280 else {
281 tree.setModel(treeTableModelAdapter.treeTableModel);
282 newTree = false;
283 }
284
285
286 super.setModel(treeTableModelAdapter);
287
288 if (newTree) {
289
290 ListToTreeSelectionModelWrapper selectionWrapper = new
291 ListToTreeSelectionModelWrapper();
292 tree.setSelectionModel(selectionWrapper);
293 setSelectionModel(selectionWrapper.getListSelectionModel());
294
295
296 setDefaultRenderer(TreeTableModel.class, tree);
297 }
298 }
299
300 /***
301 * Returns the tree that is being shared between the model.
302 */
303 public JTree getTree() {
304 return tree;
305 }
306
307 /***
308 * Overriden to invoke repaint for the particular location if
309 * the column contains the tree. This is done as the tree editor does
310 * not fill the bounds of the cell, we need the renderer to paint
311 * the tree in the background, and then draw the editor over it.
312 */
313 public boolean editCellAt(int row, int column, EventObject e){
314 boolean retValue = super.editCellAt(row, column, e);
315 if (retValue && getColumnClass(column) == TreeTableModel.class) {
316 repaint(getCellRect(row, column, false));
317 }
318 return retValue;
319 }
320
321 /***
322 * A TreeCellRenderer that displays a JTree.
323 */
324 public class TreeTableCellRenderer extends JTree implements
325 TableCellRenderer {
326 /*** Last table/tree row asked to renderer. */
327 protected int visibleRow;
328 /*** Border to draw around the tree, if this is non-null, it will
329 * be painted. */
330 protected Border highlightBorder;
331
332 public TreeTableCellRenderer(TreeModel model) {
333 super(model);
334 }
335
336 /***
337 * updateUI is overridden to set the colors of the Tree's renderer
338 * to match that of the table.
339 */
340 public void updateUI() {
341 super.updateUI();
342
343
344 TreeCellRenderer tcr = getCellRenderer();
345 if (tcr instanceof DefaultTreeCellRenderer) {
346 DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer)tcr);
347
348
349
350
351 dtcr.setTextSelectionColor(UIManager.getColor
352 ("Table.selectionForeground"));
353 dtcr.setBackgroundSelectionColor(UIManager.getColor
354 ("Table.selectionBackground"));
355 }
356 }
357
358 /***
359 * Sets the row height of the tree, and forwards the row height to
360 * the table.
361 */
362 public void setRowHeight(int rowHeight) {
363 if (rowHeight > 0) {
364 super.setRowHeight(rowHeight);
365 if (JTreeTable.this != null &&
366 JTreeTable.this.getRowHeight() != rowHeight) {
367 JTreeTable.this.setRowHeight(this.getRowHeight());
368 }
369 }
370 }
371
372 /***
373 * This is overridden to set the height to match that of the JTable.
374 */
375 public void setBounds(int x, int y, int w, int h) {
376 super.setBounds(x, 0, w, JTreeTable.this.getHeight());
377 }
378
379 /***
380 * Sublcassed to translate the graphics such that the last visible
381 * row will be drawn at 0,0.
382 */
383 public void paint(Graphics g) {
384 g.translate(0, -visibleRow * this.getRowHeight());
385 super.paint(g);
386
387 if (highlightBorder != null) {
388 highlightBorder.paintBorder(this, g, 0, visibleRow *
389 this.getRowHeight(), this.getWidth(),
390 this.getRowHeight());
391 }
392 }
393
394 /***
395 * TreeCellRenderer method. Overridden to update the visible row.
396 */
397 public Component getTableCellRendererComponent(JTable table,
398 Object value,
399 boolean isSelected,
400 boolean hasFocus,
401 int row, int column) {
402 Color background;
403 Color foreground;
404
405 if(isSelected) {
406 background = table.getSelectionBackground();
407 foreground = table.getSelectionForeground();
408 }
409 else {
410 background = JTreeTable.this.getBackgroundColor(row);
411 foreground = table.getForeground();
412 }
413 highlightBorder = null;
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429 visibleRow = row;
430 this.setBackground(background);
431
432 TreeCellRenderer tcr = getCellRenderer();
433 if (tcr instanceof DefaultTreeCellRenderer) {
434 DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer)tcr);
435 if (isSelected) {
436 dtcr.setTextSelectionColor(foreground);
437 dtcr.setBackgroundSelectionColor(background);
438 }
439 else {
440 dtcr.setTextNonSelectionColor(foreground);
441 dtcr.setBackgroundNonSelectionColor(background);
442 }
443 }
444 return this;
445 }
446 }
447
448
449 /***
450 * An editor that can be used to edit the tree column. This extends
451 * DefaultCellEditor and uses a JTextField (actually, TreeTableTextField)
452 * to perform the actual editing.
453 * <p>To support editing of the tree column we can not make the tree
454 * editable. The reason this doesn't work is that you can not use
455 * the same component for editing and renderering. The table may have
456 * the need to paint cells, while a cell is being edited. If the same
457 * component were used for the rendering and editing the component would
458 * be moved around, and the contents would change. When editing, this
459 * is undesirable, the contents of the text field must stay the same,
460 * including the caret blinking, and selections persisting. For this
461 * reason the editing is done via a TableCellEditor.
462 * <p>Another interesting thing to be aware of is how tree positions
463 * its render and editor. The render/editor is responsible for drawing the
464 * icon indicating the type of node (leaf, branch...). The tree is
465 * responsible for drawing any other indicators, perhaps an additional
466 * +/- sign, or lines connecting the various nodes. So, the renderer
467 * is positioned based on depth. On the other hand, table always makes
468 * its editor fill the contents of the cell. To get the allusion
469 * that the table cell editor is part of the tree, we don't want the
470 * table cell editor to fill the cell bounds. We want it to be placed
471 * in the same manner as tree places it editor, and have table message
472 * the tree to paint any decorations the tree wants. Then, we would
473 * only have to worry about the editing part. The approach taken
474 * here is to determine where tree would place the editor, and to override
475 * the <code>reshape</code> method in the JTextField component to
476 * nudge the textfield to the location tree would place it. Since
477 * JTreeTable will paint the tree behind the editor everything should
478 * just work. So, that is what we are doing here. Determining of
479 * the icon position will only work if the TreeCellRenderer is
480 * an instance of DefaultTreeCellRenderer. If you need custom
481 * TreeCellRenderers, that don't descend from DefaultTreeCellRenderer,
482 * and you want to support editing in JTreeTable, you will have
483 * to do something similiar.
484 */
485
486
487
488
489
490 /***
491 * Overriden to determine an offset that tree would place the
492 * editor at. The offset is determined from the
493 * <code>getRowBounds</code> JTree method, and additionaly
494 * from the icon DefaultTreeCellRenderer will use.
495 * <p>The offset is then set on the TreeTableTextField component
496 * created in the constructor, and returned.
497 */
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529 /***
530 * This is overriden to forward the event to the tree. This will
531 * return true if the click count >= 3, or the event is null.
532 */
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570 /***
571 * Component used by TreeTableCellEditor. The only thing this does
572 * is to override the <code>reshape</code> method, and to ALWAYS
573 * make the x location be <code>offset</code>.
574 */
575
576
577
578
579
580
581
582
583
584 /***
585 * TreeTableCellEditor implementation. Component returned is the
586 * JTree.
587 */
588 public class TreeTableCellEditor extends AbstractCellEditor implements
589 TableCellEditor {
590 public Component getTableCellEditorComponent(JTable table,
591 Object value,
592 boolean isSelected,
593 int r, int c) {
594 return tree;
595 }
596
597 public Object getCellEditorValue()
598 {
599 return null;
600 }
601
602 /***
603 * Overridden to return false, and if the event is a mouse event
604 * it is forwarded to the tree.<p>
605 * The behavior for this is debatable, and should really be offered
606 * as a property. By returning false, all keyboard actions are
607 * implemented in terms of the table. By returning true, the
608 * tree would get a chance to do something with the keyboard
609 * events. For the most part this is ok. But for certain keys,
610 * such as left/right, the tree will expand/collapse where as
611 * the table focus should really move to a different column. Page
612 * up/down should also be implemented in terms of the table.
613 * By returning false this also has the added benefit that clicking
614 * outside of the bounds of the tree node, but still in the tree
615 * column will select the row, whereas if this returned true
616 * that wouldn't be the case.
617 * <p>By returning false we are also enforcing the policy that
618 * the tree will never be editable (at least by a key sequence).
619 */
620 public boolean isCellEditable(EventObject e) {
621 if (e instanceof MouseEvent) {
622 MouseEvent me = (MouseEvent)e;
623
624
625
626
627
628
629 if (me.getModifiers() == 0 ||
630 me.getModifiers() == InputEvent.BUTTON1_MASK) {
631 for (int counter = getColumnCount() - 1; counter >= 0;
632 counter--) {
633 if (getColumnClass(counter) == TreeTableModel.class) {
634 MouseEvent newME = new MouseEvent
635 (JTreeTable.this.tree, me.getID(),
636 me.getWhen(), me.getModifiers(),
637 me.getX() - getCellRect(0, counter, true).x,
638 me.getY(), me.getClickCount(),
639 me.isPopupTrigger());
640 JTreeTable.this.tree.dispatchEvent(newME);
641 break;
642 }
643 }
644 }
645 }
646 return false;
647 }
648 }
649
650 /***
651 * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
652 * to listen for changes in the ListSelectionModel it maintains. Once
653 * a change in the ListSelectionModel happens, the paths are updated
654 * in the DefaultTreeSelectionModel.
655 */
656 class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
657 /*** Set to true when we are updating the ListSelectionModel. */
658 protected boolean updatingListSelectionModel;
659
660 public ListToTreeSelectionModelWrapper() {
661 super();
662 getListSelectionModel().addListSelectionListener
663 (createListSelectionListener());
664 }
665
666 /***
667 * Returns the list selection model. ListToTreeSelectionModelWrapper
668 * listens for changes to this model and updates the selected paths
669 * accordingly.
670 */
671 ListSelectionModel getListSelectionModel() {
672 return listSelectionModel;
673 }
674
675 /***
676 * This is overridden to set <code>updatingListSelectionModel</code>
677 * and message super. This is the only place DefaultTreeSelectionModel
678 * alters the ListSelectionModel.
679 */
680 public void resetRowSelection() {
681 if(!updatingListSelectionModel) {
682 updatingListSelectionModel = true;
683 try {
684 super.resetRowSelection();
685 }
686 finally {
687 updatingListSelectionModel = false;
688 }
689 }
690
691
692
693
694
695 }
696
697 /***
698 * Creates and returns an instance of ListSelectionHandler.
699 */
700 protected ListSelectionListener createListSelectionListener() {
701 return new ListSelectionHandler();
702 }
703
704 /***
705 * If <code>updatingListSelectionModel</code> is false, this will
706 * reset the selected paths from the selected rows in the list
707 * selection model.
708 */
709 protected void updateSelectedPathsFromSelectedRows() {
710 if(!updatingListSelectionModel) {
711 updatingListSelectionModel = true;
712 try {
713
714
715 int min = listSelectionModel.getMinSelectionIndex();
716 int max = listSelectionModel.getMaxSelectionIndex();
717
718 this.clearSelection();
719 if(min != -1 && max != -1) {
720 for(int counter = min; counter <= max; counter++) {
721 if(listSelectionModel.isSelectedIndex(counter)) {
722 TreePath selPath = tree.getPathForRow
723 (counter);
724
725 if(selPath != null) {
726 addSelectionPath(selPath);
727 }
728 }
729 }
730 }
731 }
732 finally {
733 updatingListSelectionModel = false;
734 }
735 }
736 }
737
738 /***
739 * Class responsible for calling updateSelectedPathsFromSelectedRows
740 * when the selection of the list changse.
741 */
742 class ListSelectionHandler implements ListSelectionListener {
743 public void valueChanged(ListSelectionEvent e) {
744 updateSelectedPathsFromSelectedRows();
745 }
746 }
747 }
748 }