001 /*
002 * $Id: JXTreeTable.java,v 1.33 2006/05/14 08:12:20 dmouse Exp $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
020 */
021
022
023 package org.jdesktop.swingx;
024 import java.awt.Component;
025 import java.awt.Dimension;
026 import java.awt.Graphics;
027 import java.awt.Point;
028 import java.awt.Rectangle;
029 import java.awt.event.ActionEvent;
030 import java.awt.event.InputEvent;
031 import java.awt.event.MouseEvent;
032 import java.beans.PropertyChangeEvent;
033 import java.beans.PropertyChangeListener;
034 import java.util.Enumeration;
035 import java.util.EventObject;
036
037 import javax.swing.ActionMap;
038 import javax.swing.Icon;
039 import javax.swing.JComponent;
040 import javax.swing.JTable;
041 import javax.swing.JTree;
042 import javax.swing.ListSelectionModel;
043 import javax.swing.SwingUtilities;
044 import javax.swing.UIManager;
045 import javax.swing.border.Border;
046 import javax.swing.event.ListSelectionEvent;
047 import javax.swing.event.ListSelectionListener;
048 import javax.swing.event.TreeExpansionEvent;
049 import javax.swing.event.TreeExpansionListener;
050 import javax.swing.event.TreeModelEvent;
051 import javax.swing.event.TreeModelListener;
052 import javax.swing.event.TreeSelectionListener;
053 import javax.swing.event.TreeWillExpandListener;
054 import javax.swing.plaf.UIResource;
055 import javax.swing.table.AbstractTableModel;
056 import javax.swing.table.TableCellRenderer;
057 import javax.swing.table.TableModel;
058 import javax.swing.tree.DefaultTreeCellRenderer;
059 import javax.swing.tree.DefaultTreeSelectionModel;
060 import javax.swing.tree.TreeCellRenderer;
061 import javax.swing.tree.TreePath;
062 import javax.swing.tree.TreeSelectionModel;
063
064 import org.jdesktop.swingx.decorator.ComponentAdapter;
065 import org.jdesktop.swingx.treetable.AbstractTreeTableModel;
066 import org.jdesktop.swingx.treetable.DefaultTreeTableModel;
067 import org.jdesktop.swingx.treetable.TreeTableCellEditor;
068 import org.jdesktop.swingx.treetable.TreeTableModel;
069
070
071 /**
072 * <p><code>JXTreeTable</code> is a specialized {@link javax.swing.JTable table}
073 * consisting of a single column in which to display hierarchical data, and any
074 * number of other columns in which to display regular data. The interface for
075 * the data model used by a <code>JXTreeTable</code> is
076 * {@link org.jdesktop.swingx.treetable.TreeTableModel}. It extends the
077 * {@link javax.swing.tree.TreeModel} interface to allow access to cell data by
078 * column indices within each node of the tree hierarchy.</p>
079 *
080 * <p>The most straightforward way create and use a <code>JXTreeTable</code>, is to
081 * first create a suitable data model for it, and pass that to a
082 * <code>JXTreeTable</code> constructor, as shown below:
083 * <pre>
084 * TreeTableModel treeTableModel = new FileSystemModel(); // any TreeTableModel
085 * JXTreeTable treeTable = new JXTreeTable(treeTableModel);
086 * JScrollPane scrollpane = new JScrollPane(treeTable);
087 * </pre>
088 * See {@link javax.swing.JTable} for an explanation of why putting the treetable
089 * inside a scroll pane is necessary.</p>
090 *
091 * <p>A single treetable model instance may be shared among more than one
092 * <code>JXTreeTable</code> instances. To access the treetable model, always call
093 * {@link #getTreeTableModel() getTreeTableModel} and
094 * {@link #setTreeTableModel(org.jdesktop.swingx.treetable.TreeTableModel) setTreeTableModel}.
095 * <code>JXTreeTable</code> wraps the supplied treetable model inside a private
096 * adapter class to adapt it to a {@link javax.swing.table.TableModel}. Although
097 * the model adapter is accessible through the {@link #getModel() getModel} method, you
098 * should avoid accessing and manipulating it in any way. In particular, each
099 * model adapter instance is tightly bound to a single table instance, and any
100 * attempt to share it with another table (for example, by calling
101 * {@link #setModel(javax.swing.table.TableModel) setModel})
102 * will throw an <code>IllegalArgumentException</code>!
103 *
104 * @author Philip Milne
105 * @author Scott Violet
106 * @author Ramesh Gupta
107 */
108 public class JXTreeTable extends JXTable {
109
110 /**
111 * Key for clientProperty to decide whether to apply hack around #168-jdnc.
112 */
113 public static final String DRAG_HACK_FLAG_KEY = "treeTable.dragHackFlag";
114 /**
115 * Renderer used to render cells within the
116 * {@link #isHierarchical(int) hierarchical} column.
117 * renderer extends JXTree and implements TableCellRenderer
118 */
119 private TreeTableCellRenderer renderer;
120
121 /**
122 * Constructs a JXTreeTable using a
123 * {@link org.jdesktop.swingx.treetable.DefaultTreeTableModel}.
124 */
125 public JXTreeTable() {
126 this(new DefaultTreeTableModel());
127 }
128
129 /**
130 * Constructs a JXTreeTable using the specified
131 * {@link org.jdesktop.swingx.treetable.TreeTableModel}.
132 *
133 * @param treeModel model for the JXTreeTable
134 */
135 public JXTreeTable(TreeTableModel treeModel) {
136 // Implementation note:
137 // Make sure that the SAME instance of treeModel is passed to the
138 // constructor for TreeTableCellRenderer as is passed in the first
139 // argument to the following chained constructor for this JXTreeTable:
140 this(treeModel, new JXTreeTable.TreeTableCellRenderer(treeModel));
141 }
142
143 /**
144 * Constructs a <code>JXTreeTable</code> using the specified
145 * {@link org.jdesktop.swingx.treetable.TreeTableModel} and
146 * {@link org.jdesktop.swingx.JXTreeTable.TreeTableCellRenderer}. The renderer
147 * must have been constructed using the same instance of
148 * {@link org.jdesktop.swingx.treetable.TreeTableModel} as passed to this
149 * constructor.
150 *
151 * @param treeModel model for the JXTreeTable
152 * @param renderer cell renderer for the tree portion of this JXTreeTable instance.
153 * @throws IllegalArgumentException if an attempt is made to instantiate
154 * JXTreeTable and TreeTableCellRenderer with different instances of TreeTableModel.
155 */
156 private JXTreeTable(TreeTableModel treeModel, TreeTableCellRenderer renderer) {
157 // To avoid unnecessary object creation, such as the construction of a
158 // DefaultTableModel, it is better to invoke super(TreeTableModelAdapter)
159 // directly, instead of first invoking super() followed by a call to
160 // setTreeTableModel(TreeTableModel).
161
162 // Adapt tree model to table model before invoking super()
163 super(new TreeTableModelAdapter(treeModel, renderer));
164
165 // Enforce referential integrity; bail on fail
166 if (treeModel != renderer.getModel()) { // do not use assert here!
167 throw new IllegalArgumentException("Mismatched TreeTableModel");
168 }
169
170 // renderer-related initialization -- also called from setTreeTableModel()
171 init(renderer); // private method
172 initActions();
173 // disable sorting
174 super.setSortable(false);
175 // Install the default editor.
176 setDefaultEditor(AbstractTreeTableModel.hierarchicalColumnClass,
177 new TreeTableCellEditor(renderer));
178
179 // No grid.
180 setShowGrid(false); // superclass default is "true"
181
182 // Default intercell spacing
183 setIntercellSpacing(spacing); // for both row margin and column margin
184
185 // JTable supports row margins and intercell spacing, but JTree doesn't.
186 // We must reconcile the differences in the semantics of rowHeight as
187 // understood by JTable and JTree by overriding both setRowHeight() and
188 // setRowMargin();
189 adminSetRowHeight(getRowHeight());
190 setRowMargin(getRowMargin()); // call overridden setRowMargin()
191
192 }
193
194
195 private void initActions() {
196 // Register the actions that this class can handle.
197 ActionMap map = getActionMap();
198 map.put("expand-all", new Actions("expand-all"));
199 map.put("collapse-all", new Actions("collapse-all"));
200 }
201
202 /**
203 * A small class which dispatches actions.
204 * TODO: Is there a way that we can make this static?
205 */
206 private class Actions extends UIAction {
207 Actions(String name) {
208 super(name);
209 }
210
211 public void actionPerformed(ActionEvent evt) {
212 if ("expand-all".equals(getName())) {
213 expandAll();
214 }
215 else if ("collapse-all".equals(getName())) {
216 collapseAll();
217 }
218 }
219 }
220
221 /**
222 * overridden to do nothing.
223 *
224 * TreeTable is not sortable by default, because
225 * Sorters/Filters currently don't work properly.
226 *
227 */
228 @Override
229 public void setSortable(boolean sortable) {
230 // no-op
231 }
232
233 /**
234 * <p>Sets whether the table draws horizontal lines between cells. It draws
235 * the lines if <code>show</code> is true; otherwise it doesn't. By default,
236 * a table draws the lines.</p>
237 *
238 * <p>If you want the lines to be drawn, make sure that the row margin or
239 * horizontal intercell spacing is greater than zero.</p>
240 *
241 * @param show true, if horizontal lines should be drawn; false, if lines
242 * should not be drawn
243 * @see javax.swing.JTable#getShowHorizontalLines() getShowHorizontalLines
244 * @see #setRowMargin(int) setRowMargin
245 * @see javax.swing.JTable#setIntercellSpacing(java.awt.Dimension) setIntercellSpacing
246 */
247 @Override
248 public void setShowHorizontalLines(boolean show) {
249 super.setShowHorizontalLines(show);
250 }
251
252 /**
253 * <p>Sets whether the table draws vertical lines between cells. It draws
254 * the lines if <code>show</code> is true; otherwise it doesn't. By default,
255 * a table draws the lines.</p>
256 *
257 * <p>If you want the lines to be drawn, make sure that the column margin or
258 * vertical intercell spacing is greater than zero.</p>
259 *
260 * @param show true, if vertical lines should be drawn; false, if lines
261 * should not be drawn
262 * @see javax.swing.JTable#getShowVerticalLines() getShowVerticalLines
263 * @see #setColumnMargin(int) setColumnMargin
264 * @see javax.swing.JTable#setIntercellSpacing(java.awt.Dimension) setIntercellSpacing
265 */
266 @Override
267 public void setShowVerticalLines(boolean show) {
268 super.setShowVerticalLines(show);
269 }
270
271 /**
272 * Overriden to invoke repaint for the particular location if
273 * the column contains the tree. This is done as the tree editor does
274 * not fill the bounds of the cell, we need the renderer to paint
275 * the tree in the background, and then draw the editor over it.
276 * You should not need to call this method directly.
277 *
278 * {@inheritDoc}
279 */
280 @Override
281 public boolean editCellAt(int row, int column, EventObject e) {
282 expandOrCollapseNode(column, e); // RG: Fix Issue 49!
283 boolean canEdit = super.editCellAt(row, column, e);
284 if (canEdit && isHierarchical(column)) {
285 repaint(getCellRect(row, column, false));
286 }
287 return canEdit;
288 }
289
290 private void expandOrCollapseNode(int column, EventObject e) {
291 if (!(e instanceof MouseEvent)) return;
292 if (!isHierarchical(column)) return;
293 MouseEvent me = (MouseEvent) e;
294 if (hackAroundDragEnabled(me)) {
295 /*
296 * Hack around #168-jdnc:
297 * dirty little hack mentioned in the forum
298 * discussion about the issue: fake a mousePressed if drag enabled.
299 * The usability is slightly impaired because the expand/collapse
300 * is effectively triggered on released only (drag system intercepts
301 * and consumes all other).
302 */
303 me = new MouseEvent((Component)me.getSource(),
304 MouseEvent.MOUSE_PRESSED,
305 me.getWhen(),
306 me.getModifiers(),
307 me.getX(),
308 me.getY(),
309 me.getClickCount(),
310 me.isPopupTrigger());
311
312 }
313 // If the modifiers are not 0 (or the left mouse button),
314 // tree may try and toggle the selection, and table
315 // will then try and toggle, resulting in the
316 // selection remaining the same. To avoid this, we
317 // only dispatch when the modifiers are 0 (or the left mouse
318 // button).
319 if (me.getModifiers() == 0 ||
320 me.getModifiers() == InputEvent.BUTTON1_MASK) {
321 int savedHeight = renderer.getRowHeight();
322 renderer.setRowHeight(getRowHeight());
323 MouseEvent pressed = new MouseEvent
324 (renderer,
325 me.getID(),
326 me.getWhen(),
327 me.getModifiers(),
328 me.getX() - getCellRect(0, column, false).x,
329 me.getY(),
330 me.getClickCount(),
331 me.isPopupTrigger());
332 renderer.dispatchEvent(pressed);
333 // For Mac OS X, we need to dispatch a MOUSE_RELEASED as well
334 MouseEvent released = new MouseEvent
335 (renderer,
336 java.awt.event.MouseEvent.MOUSE_RELEASED,
337 pressed.getWhen(),
338 pressed.getModifiers(),
339 pressed.getX(),
340 pressed.getY(),
341 pressed.getClickCount(),
342 pressed.isPopupTrigger());
343 renderer.dispatchEvent(released);
344 renderer.setRowHeight(savedHeight);
345 }
346 }
347
348 /**
349 * decides whether we want to apply the hack for #168-jdnc.
350 * here: returns true if dragEnabled() and the improved drag
351 * handling is not activated (or the system property is not accessible).
352 * The given mouseEvent is not analysed.
353 *
354 * PENDING: Mustang?
355 *
356 * @param me the mouseEvent that triggered a editCellAt
357 * @return true if the hack should be applied.
358 */
359 protected boolean hackAroundDragEnabled(MouseEvent me) {
360 Boolean dragHackFlag = (Boolean) getClientProperty(DRAG_HACK_FLAG_KEY);
361 if (dragHackFlag == null) {
362 // access and store the system property as a client property once
363 String priority = null;
364 try {
365 priority = System.getProperty("sun.swing.enableImprovedDragGesture");
366
367 } catch (Exception ex) {
368 // found some foul expression or failed to read the property
369 }
370 dragHackFlag = (priority == null);
371 putClientProperty(DRAG_HACK_FLAG_KEY, dragHackFlag);
372 }
373 return getDragEnabled() && dragHackFlag;
374 }
375
376 /**
377 * Overridden to provide a workaround for BasicTableUI anomaly. Make sure
378 * the UI never tries to resize the editor. The UI currently uses different
379 * techniques to paint the renderers and editors. So, overriding setBounds()
380 * is not the right thing to do for an editor. Returning -1 for the
381 * editing row in this case, ensures the editor is never painted.
382 *
383 * {@inheritDoc}
384 */
385 @Override
386 public int getEditingRow() {
387 return isHierarchical(editingColumn) ? -1 : editingRow;
388 }
389
390 /**
391 * Returns the actual row that is editing as <code>getEditingRow</code>
392 * will always return -1.
393 */
394 private int realEditingRow() {
395 return editingRow;
396 }
397
398 /**
399 * Sets the data model for this JXTreeTable to the specified
400 * {@link org.jdesktop.swingx.treetable.TreeTableModel}. The same data model
401 * may be shared by any number of JXTreeTable instances.
402 *
403 * @param treeModel data model for this JXTreeTable
404 */
405 public void setTreeTableModel(TreeTableModel treeModel) {
406 TreeTableModel old = getTreeTableModel();
407 renderer.setModel(treeModel);
408 ((TreeTableModelAdapter)getModel()).setTreeTableModel(treeModel);
409 // Enforce referential integrity; bail on fail
410 // JW: when would that happen? we just set it...
411 if (treeModel != renderer.getModel()) { // do not use assert here!
412 throw new IllegalArgumentException("Mismatched TreeTableModel");
413 }
414 // Install the default editor.
415 // JW: change to re-use the editor!
416 setDefaultEditor(AbstractTreeTableModel.hierarchicalColumnClass,
417 new TreeTableCellEditor(renderer));
418
419 // JTable supports row margins and intercell spacing, but JTree doesn't.
420 // We must reconcile the differences in the semantics of rowHeight as
421 // understood by JTable and JTree by overriding both setRowHeight() and
422 // setRowMargin();
423 adminSetRowHeight(getRowHeight());
424 setRowMargin(getRowMargin()); // call overridden setRowMargin()
425 firePropertyChange("treeTableModel", old, getTreeTableModel());
426 }
427
428 /**
429 * Returns the underlying TreeTableModel for this JXTreeTable.
430 *
431 * @return the underlying TreeTableModel for this JXTreeTable
432 */
433 public TreeTableModel getTreeTableModel() {
434 return ((TreeTableModelAdapter) getModel()).getTreeTableModel();
435 }
436
437 /**
438 * <p>Overrides superclass version to make sure that the specified
439 * {@link javax.swing.table.TableModel} is compatible with JXTreeTable before
440 * invoking the inherited version.</p>
441 *
442 * <p>Because JXTreeTable internally adapts an
443 * {@link org.jdesktop.swingx.treetable.TreeTableModel} to make it a compatible
444 * TableModel, <b>this method should never be called directly</b>. Use
445 * {@link #setTreeTableModel(org.jdesktop.swingx.treetable.TreeTableModel) setTreeTableModel} instead.</p>
446 *
447 * <p>While it is possible to obtain a reference to this adapted
448 * version of the TableModel by calling {@link javax.swing.JTable#getModel()},
449 * any attempt to call setModel() with that adapter will fail because
450 * the adapter might have been bound to a different JXTreeTable instance. If
451 * you want to extract the underlying TreeTableModel, which, by the way,
452 * <em>can</em> be shared, use {@link #getTreeTableModel() getTreeTableModel}
453 * instead</p>.
454 *
455 * @param tableModel must be a TreeTableModelAdapter
456 * @throws IllegalArgumentException if the specified tableModel is not an
457 * instance of TreeTableModelAdapter
458 */
459 @Override
460 public final void setModel(TableModel tableModel) { // note final keyword
461 if (tableModel instanceof TreeTableModelAdapter) {
462 if (((TreeTableModelAdapter) tableModel).getTreeTable() == null) {
463 // Passing the above test ensures that this method is being
464 // invoked either from JXTreeTable/JTable constructor or from
465 // setTreeTableModel(TreeTableModel)
466 super.setModel(tableModel); // invoke superclass version
467
468 ((TreeTableModelAdapter) tableModel).bind(this); // permanently bound
469 // Once a TreeTableModelAdapter is bound to any JXTreeTable instance,
470 // invoking JXTreeTable.setModel() with that adapter will throw an
471 // IllegalArgumentException, because we really want to make sure
472 // that a TreeTableModelAdapter is NOT shared by another JXTreeTable.
473 }
474 else {
475 throw new IllegalArgumentException("model already bound");
476 }
477 }
478 else {
479 throw new IllegalArgumentException("unsupported model type");
480 }
481 }
482
483 /**
484 * Throws UnsupportedOperationException because variable height rows are
485 * not supported.
486 *
487 * @param row ignored
488 * @param rowHeight ignored
489 * @throws UnsupportedOperationException because variable height rows are
490 * not supported
491 */
492 @Override
493 public final void setRowHeight(int row, int rowHeight) {
494 throw new UnsupportedOperationException("variable height rows not supported");
495 }
496
497 /**
498 * Sets the row height for this JXTreeTable. Reconciles semantic differences
499 * between JTable and JTree regarding row height.
500 *
501 * @param rowHeight height of a row
502 */
503 @Override
504 public void setRowHeight(int rowHeight) {
505 super.setRowHeight(rowHeight);
506 adjustTreeRowHeight(); // JTree doesn't have setRowMargin. So adjust.
507 }
508
509 /**
510 * <p>Sets the margin between columns.</p>
511 *
512 * <p>If you set the column margin to zero, make sure that you also set
513 * <code>showVerticalLines</code> to <code>false</code>.</p>
514 *
515 * @param columnMargin margin between columns; must be greater than or equal to zero.
516 * @see #setShowVerticalLines(boolean) setShowVerticalLines
517 */
518 @Override
519 public void setColumnMargin(int columnMargin) {
520 super.setColumnMargin(columnMargin);
521 }
522
523 /**
524 * <p>Overridden to ensure that private renderer state is kept in sync with the
525 * state of the component. Calls the inherited version after performing the
526 * necessary synchronization. If you override this method, make sure you call
527 * this version from your version of this method.</p>
528 *
529 * <p>If you set row margin to zero, make sure that you also set
530 * <code>showHorizontalLines</code> to <code>false</code>.</p>
531 *
532 * @param rowMargin margin or intercell spacing between rows
533 * @see #setShowHorizontalLines(boolean) setShowHorizontalLines
534 */
535 @Override
536 public void setRowMargin(int rowMargin) {
537 // No need to override setIntercellSpacing, because the change in
538 // rowMargin will be funneled through this method anyway.
539 super.setRowMargin(rowMargin);
540 adjustTreeRowHeight(); // JTree doesn't have setRowMargin. So adjust.
541 }
542
543 /**
544 * Reconciles semantic differences between JTable and JTree regarding
545 * row height.
546 */
547 private void adjustTreeRowHeight() {
548 final int treeRowHeight = rowHeight + (rowMargin << 1);
549 if (renderer != null && renderer.getRowHeight() != treeRowHeight) {
550 renderer.setRowHeight(treeRowHeight);
551 }
552 }
553
554 /**
555 * <p>Overridden to ensure that private renderer state is kept in sync with the
556 * state of the component. Calls the inherited version after performing the
557 * necessary synchronization. If you override this method, make sure you call
558 * this version from your version of this method.</p>
559 *
560 * <p>This version maps the selection mode used by the renderer to match the
561 * selection mode specified for the table. Specifically, the modes are mapped
562 * as follows:
563 * <pre>
564 * ListSelectionModel.SINGLE_INTERVAL_SELECTION: TreeSelectionModel.CONTIGUOUS_TREE_SELECTION;
565 * ListSelectionModel.MULTIPLE_INTERVAL_SELECTION: TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION;
566 * any other (default): TreeSelectionModel.SINGLE_TREE_SELECTION;
567 * </pre>
568 *
569 * {@inheritDoc}
570 *
571 * @param mode any of the table selection modes
572 */
573 @Override
574 public void setSelectionMode(int mode) {
575 if (renderer != null) {
576 switch (mode) {
577 case ListSelectionModel.SINGLE_INTERVAL_SELECTION: {
578 renderer.getSelectionModel().setSelectionMode(
579 TreeSelectionModel.CONTIGUOUS_TREE_SELECTION);
580 break;
581 }
582 case ListSelectionModel.MULTIPLE_INTERVAL_SELECTION: {
583 renderer.getSelectionModel().setSelectionMode(
584 TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION);
585 break;
586 }
587 default: {
588 renderer.getSelectionModel().setSelectionMode(
589 TreeSelectionModel.SINGLE_TREE_SELECTION);
590 break;
591 }
592 }
593 }
594 super.setSelectionMode(mode);
595 }
596
597 /**
598 * Overrides superclass version to provide support for cell decorators.
599 *
600 * @param renderer the <code>TableCellRenderer</code> to prepare
601 * @param row the row of the cell to render, where 0 is the first row
602 * @param column the column of the cell to render, where 0 is the first column
603 * @return the <code>Component</code> used as a stamp to render the specified cell
604 */
605 @Override
606 public Component prepareRenderer(TableCellRenderer renderer, int row,
607 int column) {
608
609 Component component = super.prepareRenderer(renderer, row, column);
610 // MUST ALWAYS ACCESS dataAdapter through accessor method!!!
611 ComponentAdapter adapter = getComponentAdapter();
612 adapter.row = row;
613 adapter.column = column;
614
615 return applyRenderer(component, adapter);
616 }
617
618 /**
619 * Performs necessary housekeeping before the renderer is actually applied.
620 *
621 * @param component
622 * @param adapter component data adapter
623 * @throws NullPointerException if the specified component or adapter is null
624 */
625 protected Component applyRenderer(Component component,
626 ComponentAdapter adapter) {
627 if (component == null) {
628 throw new IllegalArgumentException("null component");
629 }
630 if (adapter == null) {
631 throw new IllegalArgumentException("null component data adapter");
632 }
633
634 if (isHierarchical(adapter.column)) {
635 // After all decorators have been applied, make sure that relevant
636 // attributes of the table cell renderer are applied to the
637 // tree cell renderer before the hierarchical column is rendered!
638 TreeCellRenderer tcr = renderer.getCellRenderer();
639 if (tcr instanceof JXTree.DelegatingRenderer) {
640 tcr = ((JXTree.DelegatingRenderer) tcr).getDelegateRenderer();
641
642 }
643 if (tcr instanceof DefaultTreeCellRenderer) {
644 DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
645 if (adapter.isSelected()) {
646 dtcr.setTextSelectionColor(component.getForeground());
647 dtcr.setBackgroundSelectionColor(component.getBackground());
648 } else {
649 dtcr.setTextNonSelectionColor(component.getForeground());
650 dtcr.setBackgroundNonSelectionColor(component.getBackground());
651 }
652 }
653 }
654 return component;
655 }
656
657 /**
658 * Sets the specified TreeCellRenderer as the Tree cell renderer.
659 *
660 * @param cellRenderer to use for rendering tree cells.
661 */
662 public void setTreeCellRenderer(TreeCellRenderer cellRenderer) {
663 if (renderer != null) {
664 renderer.setCellRenderer(cellRenderer);
665 }
666 }
667
668 public TreeCellRenderer getTreeCellRenderer() {
669 return renderer.getCellRenderer();
670 }
671
672
673 @Override
674 public String getToolTipText(MouseEvent event) {
675 int column = columnAtPoint(event.getPoint());
676 if (isHierarchical(column)) {
677 int row = rowAtPoint(event.getPoint());
678 return renderer.getToolTipText(event, row, column);
679 }
680 return super.getToolTipText(event);
681 }
682
683 /**
684 * Sets the specified icon as the icon to use for rendering collapsed nodes.
685 *
686 * @param icon to use for rendering collapsed nodes
687 */
688 public void setCollapsedIcon(Icon icon) {
689 renderer.setCollapsedIcon(icon);
690 }
691
692 /**
693 * Sets the specified icon as the icon to use for rendering expanded nodes.
694 *
695 * @param icon to use for rendering expanded nodes
696 */
697 public void setExpandedIcon(Icon icon) {
698 renderer.setExpandedIcon(icon);
699 }
700
701 /**
702 * Sets the specified icon as the icon to use for rendering open container nodes.
703 *
704 * @param icon to use for rendering open nodes
705 */
706 public void setOpenIcon(Icon icon) {
707 renderer.setOpenIcon(icon);
708 }
709
710 /**
711 * Sets the specified icon as the icon to use for rendering closed container nodes.
712 *
713 * @param icon to use for rendering closed nodes
714 */
715 public void setClosedIcon(Icon icon) {
716 renderer.setClosedIcon(icon);
717 }
718
719 /**
720 * Sets the specified icon as the icon to use for rendering leaf nodes.
721 *
722 * @param icon to use for rendering leaf nodes
723 */
724 public void setLeafIcon(Icon icon) {
725 renderer.setLeafIcon(icon);
726 }
727
728 /**
729 * Overridden to ensure that private renderer state is kept in sync with the
730 * state of the component. Calls the inherited version after performing the
731 * necessary synchronization. If you override this method, make sure you call
732 * this version from your version of this method.
733 */
734 @Override
735 public void clearSelection() {
736 if (renderer != null) {
737 renderer.clearSelection();
738 }
739 super.clearSelection();
740 }
741
742 /**
743 * Collapses all nodes in the treetable.
744 */
745 public void collapseAll() {
746 renderer.collapseAll();
747 }
748
749 /**
750 * Expands all nodes in the treetable.
751 */
752 public void expandAll() {
753 renderer.expandAll();
754 }
755
756 /**
757 * Collapses the node at the specified path in the treetable.
758 *
759 * @param path path of the node to collapse
760 */
761 public void collapsePath(TreePath path) {
762 renderer.collapsePath(path);
763 }
764
765 /**
766 * Expands the the node at the specified path in the treetable.
767 *
768 * @param path path of the node to expand
769 */
770 public void expandPath(TreePath path) {
771 renderer.expandPath(path);
772 }
773
774 /**
775 * Makes sure all the path components in path are expanded (except
776 * for the last path component) and scrolls so that the
777 * node identified by the path is displayed. Only works when this
778 * <code>JTree</code> is contained in a <code>JScrollPane</code>.
779 *
780 * (doc copied from JTree)
781 *
782 * PENDING: JW - where exactly do we want to scroll? Here: the scroll
783 * is in vertical direction only. Might need to show the tree column?
784 *
785 * @param path the <code>TreePath</code> identifying the node to
786 * bring into view
787 */
788 public void scrollPathToVisible(TreePath path) {
789 if (path == null) return;
790 renderer.makeVisible(path);
791 int row = getRowForPath(path);
792 scrollRowToVisible(row);
793 }
794
795
796 /**
797 * Collapses the row in the treetable. If the specified row index is
798 * not valid, this method will have no effect.
799 */
800 public void collapseRow(int row) {
801 renderer.collapseRow(row);
802 }
803
804 /**
805 * Expands the specified row in the treetable. If the specified row index is
806 * not valid, this method will have no effect.
807 */
808 public void expandRow(int row) {
809 renderer.expandRow(row);
810 }
811
812
813 /**
814 * Determines whether or not the root node from the TreeModel is visible.
815 *
816 * @param visible true, if the root node is visible; false, otherwise
817 */
818 public void setRootVisible(boolean visible) {
819 renderer.setRootVisible(visible);
820 // JW: the revalidate forces the root to appear after a
821 // toggling a visible from an initially invisible root.
822 // JTree fires a propertyChange on the ROOT_VISIBLE_PROPERTY
823 // BasicTreeUI reacts by (ultimately) calling JTree.treeDidChange
824 // which revalidate the tree part.
825 // Might consider to listen for the propertyChange (fired only if there
826 // actually was a change) instead of revalidating unconditionally.
827 revalidate();
828 repaint();
829 }
830
831 /**
832 * Returns true if the root node of the tree is displayed.
833 *
834 * @return true if the root node of the tree is displayed
835 */
836 public boolean isRootVisible() {
837 return renderer.isRootVisible();
838 }
839
840 /**
841 * Returns true if the value identified by path is currently viewable, which
842 * means it is either the root or all of its parents are expanded. Otherwise,
843 * this method returns false.
844 *
845 * @return true, if the value identified by path is currently viewable;
846 * false, otherwise
847 */
848 public boolean isVisible(TreePath path) {
849 return renderer.isVisible(path);
850 }
851
852 /**
853 * Returns true if the node identified by path is currently expanded.
854 * Otherwise, this method returns false.
855 *
856 * @param path path
857 * @return true, if the value identified by path is currently expanded;
858 * false, otherwise
859 */
860 public boolean isExpanded(TreePath path) {
861 return renderer.isExpanded(path);
862 }
863
864 /**
865 * Returns true if the node at the specified display row is currently expanded.
866 * Otherwise, this method returns false.
867 *
868 * @param row row
869 * @return true, if the node at the specified display row is currently expanded.
870 * false, otherwise
871 */
872 public boolean isExpanded(int row) {
873 return renderer.isExpanded(row);
874 }
875
876 /**
877 * Returns true if the node identified by path is currently collapsed,
878 * this will return false if any of the values in path are currently not
879 * being displayed.
880 *
881 * @param path path
882 * @return true, if the value identified by path is currently collapsed;
883 * false, otherwise
884 */
885 public boolean isCollapsed(TreePath path) {
886 return renderer.isCollapsed(path);
887 }
888
889 /**
890 * Returns true if the node at the specified display row is collapsed.
891 *
892 * @param row row
893 * @return true, if the node at the specified display row is currently collapsed.
894 * false, otherwise
895 */
896 public boolean isCollapsed(int row) {
897 return renderer.isCollapsed(row);
898 }
899
900
901 /**
902 * Returns an <code>Enumeration</code> of the descendants of the
903 * path <code>parent</code> that
904 * are currently expanded. If <code>parent</code> is not currently
905 * expanded, this will return <code>null</code>.
906 * If you expand/collapse nodes while
907 * iterating over the returned <code>Enumeration</code>
908 * this may not return all
909 * the expanded paths, or may return paths that are no longer expanded.
910 *
911 * @param parent the path which is to be examined
912 * @return an <code>Enumeration</code> of the descendents of
913 * <code>parent</code>, or <code>null</code> if
914 * <code>parent</code> is not currently expanded
915 */
916
917 public Enumeration getExpandedDescendants(TreePath parent) {
918 return renderer.getExpandedDescendants(parent);
919 }
920
921
922 /**
923 * Sets the value of the <code>expandsSelectedPaths</code> property for the tree
924 * part. This property specifies whether the selected paths should be expanded.
925 *
926 * @param expand true, if selected paths should be expanded; false, otherwise
927 */
928 public void setExpandsSelectedPaths(boolean expand) {
929 renderer.setExpandsSelectedPaths(expand);
930 }
931
932 /**
933 * Returns the value of the <code>expandsSelectedPaths</code> property.
934 *
935 * @return the value of the <code>expandsSelectedPaths</code> property
936 */
937 public boolean getExpandsSelectedPaths() {
938 return renderer.getExpandsSelectedPaths();
939 }
940
941 /**
942 * Returns the TreePath for a given x,y location.
943 *
944 * @param x x value
945 * @param y y value
946 *
947 * @return the <code>TreePath</code> for the givern location.
948 */
949 public TreePath getPathForLocation(int x, int y) {
950 int row = rowAtPoint(new Point(x,y));
951 if (row == -1) {
952 return null;
953 }
954 return renderer.getPathForRow(row);
955 }
956
957 /**
958 * Returns the TreePath for a given row.
959 *
960 * @param row
961 *
962 * @return the <code>TreePath</code> for the given row.
963 */
964 public TreePath getPathForRow(int row) {
965 return renderer.getPathForRow(row);
966 }
967
968 /**
969 * Returns the row for a given TreePath.
970 *
971 * @param path
972 * @return the row for the given <code>TreePath</code>.
973 */
974 public int getRowForPath(TreePath path) {
975 return renderer.getRowForPath(path);
976 }
977
978 /**
979 * Sets the value of the <code>scrollsOnExpand</code> property for the tree
980 * part. This property specifies whether the expanded paths should be scrolled
981 * into view. In a look and feel in which a tree might not need to scroll
982 * when expanded, this property may be ignored.
983 *
984 * @param scroll true, if expanded paths should be scrolled into view;
985 * false, otherwise
986 */
987 public void setScrollsOnExpand(boolean scroll) {
988 renderer.setScrollsOnExpand(scroll);
989 }
990
991 /**
992 * Returns the value of the <code>scrollsOnExpand</code> property.
993 *
994 * @return the value of the <code>scrollsOnExpand</code> property
995 */
996 public boolean getScrollsOnExpand() {
997 return renderer.getScrollsOnExpand();
998 }
999
1000 /**
1001 * Sets the value of the <code>showsRootHandles</code> property for the tree
1002 * part. This property specifies whether the node handles should be displayed.
1003 * If handles are not supported by a particular look and feel, this property
1004 * may be ignored.
1005 *
1006 * @param visible true, if root handles should be shown; false, otherwise
1007 */
1008 public void setShowsRootHandles(boolean visible) {
1009 renderer.setShowsRootHandles(visible);
1010 repaint();
1011 }
1012
1013 /**
1014 * Returns the value of the <code>showsRootHandles</code> property.
1015 *
1016 * @return the value of the <code>showsRootHandles</code> property
1017 */
1018 public boolean getShowsRootHandles() {
1019 return renderer.getShowsRootHandles();
1020 }
1021
1022 /**
1023 * Adds a listener for <code>TreeExpansion</code> events.
1024 *
1025 * TODO (JW): redirect event source to this.
1026 *
1027 * @param tel a TreeExpansionListener that will be notified
1028 * when a tree node is expanded or collapsed
1029 */
1030 public void addTreeExpansionListener(TreeExpansionListener tel) {
1031 renderer.addTreeExpansionListener(tel);
1032 }
1033
1034 /**
1035 * Removes a listener for <code>TreeExpansion</code> events.
1036 * @param tel the <code>TreeExpansionListener</code> to remove
1037 */
1038 public void removeTreeExpansionListener(TreeExpansionListener tel) {
1039 renderer.removeTreeExpansionListener(tel);
1040 }
1041
1042 /**
1043 * Adds a listener for <code>TreeSelection</code> events.
1044 * TODO (JW): redirect event source to this.
1045 *
1046 * @param tsl a TreeSelectionListener that will be notified
1047 * when a tree node is selected or deselected
1048 */
1049 public void addTreeSelectionListener(TreeSelectionListener tsl) {
1050 renderer.addTreeSelectionListener(tsl);
1051 }
1052
1053 /**
1054 * Removes a listener for <code>TreeSelection</code> events.
1055 * @param tsl the <code>TreeSelectionListener</code> to remove
1056 */
1057 public void removeTreeSelectionListener(TreeSelectionListener tsl) {
1058 renderer.removeTreeSelectionListener(tsl);
1059 }
1060
1061 /**
1062 * Adds a listener for <code>TreeWillExpand</code> events.
1063 * TODO (JW): redirect event source to this.
1064 *
1065 * @param tel a TreeWillExpandListener that will be notified
1066 * when a tree node will be expanded or collapsed
1067 */
1068 public void addTreeWillExpandListener(TreeWillExpandListener tel) {
1069 renderer.addTreeWillExpandListener(tel);
1070 }
1071
1072 /**
1073 * Removes a listener for <code>TreeWillExpand</code> events.
1074 * @param tel the <code>TreeWillExpandListener</code> to remove
1075 */
1076 public void removeTreeWillExpandListener(TreeWillExpandListener tel) {
1077 renderer.removeTreeWillExpandListener(tel);
1078 }
1079
1080
1081 /**
1082 * Returns the selection model for the tree portion of the this treetable.
1083 *
1084 * @return selection model for the tree portion of the this treetable
1085 */
1086 public TreeSelectionModel getTreeSelectionModel() {
1087 return renderer.getSelectionModel(); // RG: Fix JDNC issue 41
1088 }
1089
1090 /**
1091 * Overriden to invoke supers implementation, and then,
1092 * if the receiver is editing a Tree column, the editors bounds is
1093 * reset. The reason we have to do this is because JTable doesn't
1094 * think the table is being edited, as <code>getEditingRow</code> returns
1095 * -1, and therefore doesn't automaticly resize the editor for us.
1096 */
1097 @Override
1098 public void sizeColumnsToFit(int resizingColumn) {
1099 /** TODO: Review wrt doLayout() */
1100 super.sizeColumnsToFit(resizingColumn);
1101 // rg:changed
1102 if (getEditingColumn() != -1 && isHierarchical(editingColumn)) {
1103 Rectangle cellRect = getCellRect(realEditingRow(),
1104 getEditingColumn(), false);
1105 Component component = getEditorComponent();
1106 component.setBounds(cellRect);
1107 component.validate();
1108 }
1109 }
1110
1111 /**
1112 * Overridden to message super and forward the method to the tree.
1113 * Since the tree is not actually in the component hieachy it will
1114 * never receive this unless we forward it in this manner.
1115 */
1116 @Override
1117 public void updateUI() {
1118 super.updateUI();
1119 if (renderer != null) {
1120 // final int savedHeight = renderer.getRowHeight();
1121 renderer.updateUI();
1122 // renderer.setRowHeight(savedHeight);
1123
1124 // Do this so that the editor is referencing the current renderer
1125 // from the tree. The renderer can potentially change each time
1126 // laf changes.
1127 setDefaultEditor(AbstractTreeTableModel.hierarchicalColumnClass,
1128 new TreeTableCellEditor(renderer));
1129
1130 if (getBackground() == null || getBackground()instanceof UIResource) {
1131 setBackground(renderer.getBackground());
1132 }
1133 }
1134 }
1135
1136 /**
1137 * Determines if the specified column contains hierarchical nodes.
1138 *
1139 * @param column zero-based index of the column
1140 * @return true if the class of objects in the specified column implement
1141 * the {@link javax.swing.tree.TreeNode} interface; false otherwise.
1142 */
1143 @Override
1144 public boolean isHierarchical(int column) {
1145 return AbstractTreeTableModel.hierarchicalColumnClass.isAssignableFrom(
1146 getColumnClass(column));
1147 }
1148
1149 /**
1150 * Initializes this JXTreeTable and permanently binds the specified renderer
1151 * to it.
1152 *
1153 * @param renderer private tree/renderer permanently and exclusively bound
1154 * to this JXTreeTable.
1155 */
1156 private void init(TreeTableCellRenderer renderer) {
1157 this.renderer = renderer;
1158 // Force the JTable and JTree to share their row selection models.
1159 ListToTreeSelectionModelWrapper selectionWrapper =
1160 new ListToTreeSelectionModelWrapper();
1161
1162 // JW: when would that happen?
1163 if (renderer != null) {
1164 renderer.bind(this); // IMPORTANT: link back!
1165 renderer.setSelectionModel(selectionWrapper);
1166 }
1167
1168 setSelectionModel(selectionWrapper.getListSelectionModel());
1169 setDefaultRenderer(AbstractTreeTableModel.hierarchicalColumnClass,
1170 renderer);
1171
1172 // propagate the lineStyle property to the renderer
1173 PropertyChangeListener l = new PropertyChangeListener() {
1174
1175 public void propertyChange(PropertyChangeEvent evt) {
1176 JXTreeTable.this.renderer.putClientProperty(evt.getPropertyName(), evt.getNewValue());
1177
1178 }
1179
1180 };
1181 addPropertyChangeListener("JTree.lineStyle", l);
1182
1183 }
1184
1185
1186 /**
1187 * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel
1188 * to listen for changes in the ListSelectionModel it maintains. Once
1189 * a change in the ListSelectionModel happens, the paths are updated
1190 * in the DefaultTreeSelectionModel.
1191 */
1192 class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel {
1193 /** Set to true when we are updating the ListSelectionModel. */
1194 protected boolean updatingListSelectionModel;
1195
1196 public ListToTreeSelectionModelWrapper() {
1197 super();
1198 getListSelectionModel().addListSelectionListener
1199 (createListSelectionListener());
1200 }
1201
1202 /**
1203 * Returns the list selection model. ListToTreeSelectionModelWrapper
1204 * listens for changes to this model and updates the selected paths
1205 * accordingly.
1206 */
1207 ListSelectionModel getListSelectionModel() {
1208 return listSelectionModel;
1209 }
1210
1211 /**
1212 * This is overridden to set <code>updatingListSelectionModel</code>
1213 * and message super. This is the only place DefaultTreeSelectionModel
1214 * alters the ListSelectionModel.
1215 */
1216 @Override
1217 public void resetRowSelection() {
1218 if (!updatingListSelectionModel) {
1219 updatingListSelectionModel = true;
1220 try {
1221 super.resetRowSelection();
1222 }
1223 finally {
1224 updatingListSelectionModel = false;
1225 }
1226 }
1227 // Notice how we don't message super if
1228 // updatingListSelectionModel is true. If
1229 // updatingListSelectionModel is true, it implies the
1230 // ListSelectionModel has already been updated and the
1231 // paths are the only thing that needs to be updated.
1232 }
1233
1234 /**
1235 * Creates and returns an instance of ListSelectionHandler.
1236 */
1237 protected ListSelectionListener createListSelectionListener() {
1238 return new ListSelectionHandler();
1239 }
1240
1241 /**
1242 * If <code>updatingListSelectionModel</code> is false, this will
1243 * reset the selected paths from the selected rows in the list
1244 * selection model.
1245 */
1246 protected void updateSelectedPathsFromSelectedRows() {
1247 if (!updatingListSelectionModel) {
1248 updatingListSelectionModel = true;
1249 try {
1250 // This is way expensive, ListSelectionModel needs an
1251 // enumerator for iterating.
1252 int min = listSelectionModel.getMinSelectionIndex();
1253 int max = listSelectionModel.getMaxSelectionIndex();
1254
1255 clearSelection();
1256 if (min != -1 && max != -1) {
1257 for (int counter = min; counter <= max; counter++) {
1258 if (listSelectionModel.isSelectedIndex(counter)) {
1259 TreePath selPath = renderer.getPathForRow(
1260 counter);
1261
1262 if (selPath != null) {
1263 addSelectionPath(selPath);
1264 }
1265 }
1266 }
1267 }
1268 }
1269 finally {
1270 updatingListSelectionModel = false;
1271 }
1272 }
1273 }
1274
1275 /**
1276 * Class responsible for calling updateSelectedPathsFromSelectedRows
1277 * when the selection of the list changse.
1278 */
1279 class ListSelectionHandler implements ListSelectionListener {
1280 public void valueChanged(ListSelectionEvent e) {
1281 updateSelectedPathsFromSelectedRows();
1282 }
1283 }
1284 }
1285
1286 private static class TreeTableModelAdapter extends AbstractTableModel {
1287 private TreeModelListener treeModelListener;
1288 /**
1289 * Maintains a TreeTableModel and a JTree as purely implementation details.
1290 * Developers can plug in any type of custom TreeTableModel through a
1291 * JXTreeTable constructor or through setTreeTableModel().
1292 *
1293 * @param model Underlying data model for the JXTreeTable that will ultimately
1294 * be bound to this TreeTableModelAdapter
1295 * @param tree TreeTableCellRenderer instantiated with the same model as
1296 * specified by the model parameter of this constructor
1297 * @throws IllegalArgumentException if a null model argument is passed
1298 * @throws IllegalArgumentException if a null tree argument is passed
1299 */
1300 TreeTableModelAdapter(TreeTableModel model, JTree tree) {
1301 assert model != null;
1302 assert tree != null;
1303
1304 this.tree = tree; // need tree to implement getRowCount()
1305 setTreeTableModel(model);
1306
1307 tree.addTreeExpansionListener(new TreeExpansionListener() {
1308 // Don't use fireTableRowsInserted() here; the selection model
1309 // would get updated twice.
1310 public void treeExpanded(TreeExpansionEvent event) {
1311 fireTableDataChanged();
1312 }
1313
1314 public void treeCollapsed(TreeExpansionEvent event) {
1315 fireTableDataChanged();
1316 }
1317 });
1318 }
1319
1320 /**
1321 *
1322 * @param model must not be null!
1323 */
1324 public void setTreeTableModel(TreeTableModel model) {
1325 TreeTableModel old = getTreeTableModel();
1326 if (old != null) {
1327 old.removeTreeModelListener(getTreeModelListener());
1328 }
1329 this.model = model;
1330 // Install a TreeModelListener that can update the table when
1331 // tree changes.
1332 model.addTreeModelListener(getTreeModelListener());
1333 fireTableStructureChanged();
1334 }
1335
1336 /**
1337 * @return <code>TreeModelListener</code>
1338 */
1339 private TreeModelListener getTreeModelListener() {
1340 if (treeModelListener == null) {
1341 treeModelListener = new TreeModelListener() {
1342 // We use delayedFireTableDataChanged as we can
1343 // not be guaranteed the tree will have finished processing
1344 // the event before us.
1345 public void treeNodesChanged(TreeModelEvent e) {
1346 delayedFireTableDataChanged(e, 0);
1347 }
1348
1349 public void treeNodesInserted(TreeModelEvent e) {
1350 delayedFireTableDataChanged(e, 1);
1351 }
1352
1353 public void treeNodesRemoved(TreeModelEvent e) {
1354 delayedFireTableDataChanged(e, 2);
1355 }
1356
1357 public void treeStructureChanged(TreeModelEvent e) {
1358 delayedFireTableDataChanged();
1359 }
1360 };
1361 }
1362 return treeModelListener;
1363 }
1364
1365 /**
1366 * Returns the real TreeTableModel that is wrapped by this TreeTableModelAdapter.
1367 *
1368 * @return the real TreeTableModel that is wrapped by this TreeTableModelAdapter
1369 */
1370 public TreeTableModel getTreeTableModel() {
1371 return model;
1372 }
1373
1374 /**
1375 * Returns the JXTreeTable instance to which this TreeTableModelAdapter is
1376 * permanently and exclusively bound. For use by
1377 * {@link org.jdesktop.swingx.JXTreeTable#setModel(javax.swing.table.TableModel)}.
1378 *
1379 * @return JXTreeTable to which this TreeTableModelAdapter is permanently bound
1380 */
1381 protected JXTreeTable getTreeTable() {
1382 return treeTable;
1383 }
1384
1385 /**
1386 * Immutably binds this TreeTableModelAdapter to the specified JXTreeTable.
1387 *
1388 * @param treeTable the JXTreeTable instance that this adapter is bound to.
1389 */
1390 protected final void bind(JXTreeTable treeTable) {
1391 // Suppress potentially subversive invocation!
1392 // Prevent clearing out the deck for possible hijack attempt later!
1393 if (treeTable == null) {
1394 throw new IllegalArgumentException("null treeTable");
1395 }
1396
1397 if (this.treeTable == null) {
1398 this.treeTable = treeTable;
1399 }
1400 else {
1401 throw new IllegalArgumentException("adapter already bound");
1402 }
1403 }
1404
1405 // Wrappers, implementing TableModel interface.
1406 // TableModelListener management provided by AbstractTableModel superclass.
1407
1408 @Override
1409 public Class getColumnClass(int column) {
1410 return model.getColumnClass(column);
1411 }
1412
1413 public int getColumnCount() {
1414 return model.getColumnCount();
1415 }
1416
1417 @Override
1418 public String getColumnName(int column) {
1419 return model.getColumnName(column);
1420 }
1421
1422 public int getRowCount() {
1423 return tree.getRowCount();
1424 }
1425
1426 public Object getValueAt(int row, int column) {
1427 return model.getValueAt(nodeForRow(row), column);
1428 }
1429
1430 @Override
1431 public boolean isCellEditable(int row, int column) {
1432 return model.isCellEditable(nodeForRow(row), column);
1433 }
1434
1435 @Override
1436 public void setValueAt(Object value, int row, int column) {
1437 model.setValueAt(value, nodeForRow(row), column);
1438 }
1439
1440 protected Object nodeForRow(int row) {
1441 return tree.getPathForRow(row).getLastPathComponent();
1442 }
1443
1444 /**
1445 * Invokes fireTableDataChanged after all the pending events have been
1446 * processed. SwingUtilities.invokeLater is used to handle this.
1447 */
1448 private void delayedFireTableDataChanged() {
1449 SwingUtilities.invokeLater(new Runnable() {
1450 public void run() {
1451 fireTableDataChanged();
1452 }
1453 });
1454 }
1455
1456 /**
1457 * Invokes fireTableDataChanged after all the pending events have been
1458 * processed. SwingUtilities.invokeLater is used to handle this.
1459 */
1460 private void delayedFireTableDataChanged(final TreeModelEvent tme, final int typeChange) {
1461 SwingUtilities.invokeLater(new Runnable() {
1462 public void run() {
1463 int indices[] = tme.getChildIndices();
1464 TreePath path = tme.getTreePath();
1465 if (indices != null) {
1466 if (tree.isExpanded(path)) { // Dont bother to update if the parent
1467 // node is collapsed
1468 int startingRow = tree.getRowForPath(path)+1;
1469 int min = Integer.MAX_VALUE;
1470 int max = Integer.MIN_VALUE;
1471 for (int i=0;i<indices.length;i++) {
1472 if (indices[i] < min) {
1473 min = indices[i];
1474 }
1475 if (indices[i] > max) {
1476 max = indices[i];
1477 }
1478 }
1479 switch (typeChange) {
1480 case 0 :
1481 fireTableRowsUpdated(startingRow + min, startingRow+max);
1482 break;
1483 case 1:
1484 fireTableRowsInserted(startingRow + min, startingRow+max);
1485 break;
1486 case 2:
1487 fireTableRowsDeleted(startingRow + min, startingRow+max);
1488 break;
1489 }
1490 } else {
1491 // not expanded - but change might effect appearance of parent
1492 // Issue #82-swingx
1493 int row = tree.getRowForPath(path);
1494 // fix Issue #247-swingx: prevent accidental structureChanged
1495 // for collapsed path
1496 // in this case row == -1, which == TableEvent.HEADER_ROW
1497 if (row >= 0) fireTableRowsUpdated(row, row);
1498 }
1499 }
1500 else { // case where the event is fired to identify root.
1501 fireTableDataChanged();
1502 }
1503 }
1504 });
1505 }
1506
1507
1508
1509 private TreeTableModel model; // immutable
1510 private final JTree tree; // immutable
1511 private JXTreeTable treeTable = null; // logically immutable
1512 }
1513
1514 static class TreeTableCellRenderer extends JXTree implements
1515 TableCellRenderer {
1516 // Force user to specify TreeTableModel instead of more general TreeModel
1517 public TreeTableCellRenderer(TreeTableModel model) {
1518 super(model);
1519 putClientProperty("JTree.lineStyle", "None");
1520 setRootVisible(false); // superclass default is "true"
1521 setShowsRootHandles(true); // superclass default is "false"
1522 /** TODO: Support truncated text directly in DefaultTreeCellRenderer. */
1523 setOverwriteRendererIcons(true);
1524 setCellRenderer(new ClippedTreeCellRenderer());
1525 }
1526
1527 /**
1528 * Hack around #297-swingx: tooltips shown at wrong row.
1529 *
1530 * The problem is that - due to much tricksery when rendering
1531 * the tree - the given coordinates are rather useless. As a
1532 * consequence, super maps to wrong coordinates. This takes
1533 * over completely.
1534 *
1535 * PENDING: bidi?
1536 *
1537 * @param event the mouseEvent in treetable coordinates
1538 * @param row the view row index
1539 * @param column the view column index
1540 * @return the tooltip as appropriate for the given row
1541 */
1542 private String getToolTipText(MouseEvent event, int row, int column) {
1543 if (row < 0) return null;
1544 TreeCellRenderer renderer = getCellRenderer();
1545 TreePath path = getPathForRow(row);
1546 Object lastPath = path.getLastPathComponent();
1547 Component rComponent = renderer.getTreeCellRendererComponent
1548 (this, lastPath, isRowSelected(row),
1549 isExpanded(row), getModel().isLeaf(lastPath), row,
1550 true);
1551
1552 if(rComponent instanceof JComponent) {
1553 Rectangle pathBounds = getPathBounds(path);
1554 Rectangle cellRect = treeTable.getCellRect(row, column, false);
1555 // JW: what we are after
1556 // is the offset into the hierarchical column
1557 // then intersect this with the pathbounds
1558 Point mousePoint = event.getPoint();
1559 // translate to coordinates relative to cell
1560 mousePoint.translate(-cellRect.x, -cellRect.y);
1561 // translate horizontally to
1562 mousePoint.translate(-pathBounds.x, 0);
1563 // show tooltip only if over renderer?
1564 // if (mousePoint.x < 0) return null;
1565 // p.translate(-pathBounds.x, -pathBounds.y);
1566 MouseEvent newEvent = new MouseEvent(rComponent, event.getID(),
1567 event.getWhen(),
1568 event.getModifiers(),
1569 mousePoint.x,
1570 mousePoint.y,
1571 // p.x, p.y,
1572 event.getClickCount(),
1573 event.isPopupTrigger());
1574
1575 return ((JComponent)rComponent).getToolTipText(newEvent);
1576 }
1577
1578 return null;
1579 }
1580
1581 /**
1582 * Immutably binds this TreeTableModelAdapter to the specified JXTreeTable.
1583 * For internal use by JXTreeTable only.
1584 *
1585 * @param treeTable the JXTreeTable instance that this renderer is bound to
1586 */
1587 public final void bind(JXTreeTable treeTable) {
1588 // Suppress potentially subversive invocation!
1589 // Prevent clearing out the deck for possible hijack attempt later!
1590 if (treeTable == null) {
1591 throw new IllegalArgumentException("null treeTable");
1592 }
1593
1594 if (this.treeTable == null) {
1595 this.treeTable = treeTable;
1596 }
1597 else {
1598 throw new IllegalArgumentException("renderer already bound");
1599 }
1600 }
1601
1602 /**
1603 * updateUI is overridden to set the colors of the Tree's renderer
1604 * to match that of the table.
1605 */
1606 @Override
1607 public void updateUI() {
1608 super.updateUI();
1609 // Make the tree's cell renderer use the table's cell selection
1610 // colors.
1611 // TODO JW: need to revisit...
1612 // a) the "real" of a JXTree is always wrapped into a DelegatingRenderer
1613 // consequently the if-block never executes
1614 // b) even if it does it probably (?) should not
1615 // unconditionally overwrite custom selection colors.
1616 // Check for UIResources instead.
1617 TreeCellRenderer tcr = getCellRenderer();
1618 if (tcr instanceof DefaultTreeCellRenderer) {
1619 DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr);
1620 // For 1.1 uncomment this, 1.2 has a bug that will cause an
1621 // exception to be thrown if the border selection color is null.
1622 dtcr.setBorderSelectionColor(null);
1623 dtcr.setTextSelectionColor(
1624 UIManager.getColor("Table.selectionForeground"));
1625 dtcr.setBackgroundSelectionColor(
1626 UIManager.getColor("Table.selectionBackground"));
1627 }
1628 }
1629
1630 /**
1631 * Sets the row height of the tree, and forwards the row height to
1632 * the table.
1633 */
1634 @Override
1635 public void setRowHeight(int rowHeight) {
1636 super.setRowHeight(rowHeight);
1637 if (rowHeight > 0) {
1638 // JW: setting the largeModel property is suggested in
1639 // #25-swingx.
1640 // backing out: leads to NPEs and icons not showing
1641 // setLargeModel(true);
1642 if (treeTable != null) {
1643 // Reconcile semantic differences between JTable and JTree
1644 final int tableRowMargin = treeTable.getRowMargin();
1645 assert tableRowMargin >= 0;
1646 final int tableRowHeight = rowHeight - (tableRowMargin << 1);
1647 if (treeTable.getRowHeight() != tableRowHeight) {
1648 treeTable.adminSetRowHeight(tableRowHeight);
1649 }
1650 }
1651 }
1652 // else {
1653 // setLargeModel(false);
1654 // }
1655 }
1656
1657 /**
1658 * This is overridden to set the height to match that of the JTable.
1659 */
1660 @Override
1661 public void setBounds(int x, int y, int w, int h) {
1662 if (treeTable != null) {
1663 y = 0;
1664 // It is not enough to set the height to treeTable.getHeight()
1665 h = treeTable.getRowCount() * this.getRowHeight();
1666 }
1667 super.setBounds(x, y, w, h);
1668 }
1669
1670 /**
1671 * Sublcassed to translate the graphics such that the last visible row
1672 * will be drawn at 0,0.
1673 */
1674 @Override
1675 public void paint(Graphics g) {
1676 int rowMargin = treeTable.getRowMargin();
1677 // MUST account for rowMargin for precise positioning.
1678 // Offset by (rowMargin * 3)/2, and remember to offset by an
1679 // additional pixel if rowMargin is odd!
1680 int margins = rowMargin + (rowMargin >> 1) + (rowMargin % 2);
1681 int translationOffset = margins + visibleRow * getRowHeight();
1682 g.translate(0, -translationOffset);
1683
1684 hierarchicalColumnWidth = getWidth();
1685 super.paint(g);
1686
1687 // Draw the Table border if we have focus.
1688 if (highlightBorder != null) {
1689 // #170: border not drawn correctly
1690 // JW: position the border to be drawn in translated area
1691 // still not satifying in all cases...
1692 // RG: Now it satisfies (at least for the row margins)
1693 // Still need to make similar adjustments for column margins...
1694 highlightBorder.paintBorder(this, g, 0, translationOffset,
1695 getWidth(),
1696 // uhhh getRowHeight() + 1 - 2 * (margins)
1697 getRowHeight() - (rowMargin << 1) - rowMargin); // RG:
1698 // subtract
1699 // (rowMargin
1700 // * 3)
1701 }
1702 }
1703
1704 public Component getTableCellRendererComponent(JTable table,
1705 Object value,
1706 boolean isSelected, boolean hasFocus, int row, int column) {
1707 assert table == treeTable;
1708
1709 if (isSelected) {
1710 setBackground(table.getSelectionBackground());
1711 setForeground(table.getSelectionForeground());
1712 }
1713 else {
1714 setBackground(table.getBackground());
1715 setForeground(table.getForeground());
1716 }
1717
1718 highlightBorder = null;
1719 if (treeTable != null) {
1720 if (treeTable.realEditingRow() == row &&
1721 treeTable.getEditingColumn() == column) {
1722 }
1723 else if (hasFocus) {
1724 highlightBorder = UIManager.getBorder(
1725 "Table.focusCellHighlightBorder");
1726 }
1727 }
1728
1729
1730 visibleRow = row;
1731
1732 return this;
1733 }
1734
1735 private class ClippedTreeCellRenderer extends DefaultTreeCellRenderer {
1736 public void paint(Graphics g) {
1737 String fullText = super.getText();
1738 // getText() calls tree.convertValueToText();
1739 // tree.convertValueToText() should call treeModel.convertValueToText(), if possible
1740
1741 String shortText = SwingUtilities.layoutCompoundLabel(
1742 this, g.getFontMetrics(), fullText, getIcon(),
1743 getVerticalAlignment(), getHorizontalAlignment(),
1744 getVerticalTextPosition(), getHorizontalTextPosition(),
1745 getItemRect(itemRect), iconRect, textRect,
1746 getIconTextGap());
1747
1748 /** TODO: setText is more heavyweight than we want in this
1749 * situation. Make JLabel.text protected instead of private.
1750 */
1751
1752 setText(shortText); // temporarily truncate text
1753 super.paint(g);
1754 setText(fullText); // restore full text
1755 }
1756
1757 private Rectangle getItemRect(Rectangle itemRect) {
1758 getBounds(itemRect);
1759 itemRect.width = hierarchicalColumnWidth - itemRect.x;
1760 return itemRect;
1761 }
1762
1763 // Rectangles filled in by SwingUtilities.layoutCompoundLabel();
1764 private final Rectangle iconRect = new Rectangle();
1765 private final Rectangle textRect = new Rectangle();
1766 // Rectangle filled in by this.getItemRect();
1767 private final Rectangle itemRect = new Rectangle();
1768 }
1769
1770 /** Border to draw around the tree, if this is non-null, it will
1771 * be painted. */
1772 protected Border highlightBorder = null;
1773 protected JXTreeTable treeTable = null;
1774 protected int visibleRow = 0;
1775
1776 // A JXTreeTable may not have more than one hierarchical column
1777 private int hierarchicalColumnWidth = 0;
1778 }
1779
1780 /**
1781 * Returns the adapter that knows how to access the component data model.
1782 * The component data adapter is used by filters, sorters, and highlighters.
1783 *
1784 * @return the adapter that knows how to access the component data model
1785 */
1786 @Override
1787 protected ComponentAdapter getComponentAdapter() {
1788 if (dataAdapter == null) {
1789 dataAdapter = new TreeTableDataAdapter(this);
1790 }
1791 // MUST ALWAYS ACCESS dataAdapter through accessor method!!!
1792 return dataAdapter;
1793 }
1794
1795
1796 private final static Dimension spacing = new Dimension(0, 2);
1797
1798 protected static class TreeTableDataAdapter extends JXTable.TableAdapter {
1799 private final JXTreeTable table;
1800
1801 /**
1802 * Constructs a <code>TreeTableDataAdapter</code> for the specified
1803 * target component.
1804 *
1805 * @param component the target component
1806 */
1807 public TreeTableDataAdapter(JXTreeTable component) {
1808 super(component);
1809 table = component;
1810 }
1811 public JXTreeTable getTreeTable() {
1812 return table;
1813 }
1814
1815 @Override
1816 public boolean isExpanded() {
1817 return super.isExpanded(); /** TODO: implement this method */
1818 }
1819
1820 @Override
1821 public boolean hasFocus() {
1822 boolean focus = super.hasFocus(); /** TODO: implement this method */
1823 return focus;
1824 }
1825
1826 @Override
1827 public boolean isLeaf() {
1828 return super.isLeaf(); /** TODO: implement this method */
1829 }
1830 /**
1831 *
1832 * @return true if the cell identified by this adapter displays hierarchical
1833 * nodes; false otherwise
1834 */
1835 @Override
1836 public boolean isHierarchical() {
1837 return table.isHierarchical(column);
1838 }
1839 }
1840
1841 }