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 }