001 /* 002 * $Id: JXTreeTable.java 3387 2009-07-09 12:04:57Z kleopatra $ 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 025 import java.awt.Color; 026 import java.awt.Component; 027 import java.awt.Graphics; 028 import java.awt.Point; 029 import java.awt.Rectangle; 030 import java.awt.event.ActionEvent; 031 import java.awt.event.InputEvent; 032 import java.awt.event.MouseEvent; 033 import java.beans.PropertyChangeEvent; 034 import java.beans.PropertyChangeListener; 035 import java.util.ArrayList; 036 import java.util.Enumeration; 037 import java.util.EventObject; 038 import java.util.List; 039 import java.util.logging.Logger; 040 041 import javax.swing.ActionMap; 042 import javax.swing.Icon; 043 import javax.swing.JComponent; 044 import javax.swing.JTable; 045 import javax.swing.JTree; 046 import javax.swing.ListSelectionModel; 047 import javax.swing.RowSorter; 048 import javax.swing.SwingUtilities; 049 import javax.swing.UIManager; 050 import javax.swing.border.Border; 051 import javax.swing.event.ListSelectionEvent; 052 import javax.swing.event.ListSelectionListener; 053 import javax.swing.event.TableModelEvent; 054 import javax.swing.event.TreeExpansionEvent; 055 import javax.swing.event.TreeExpansionListener; 056 import javax.swing.event.TreeModelEvent; 057 import javax.swing.event.TreeModelListener; 058 import javax.swing.event.TreeSelectionListener; 059 import javax.swing.event.TreeWillExpandListener; 060 import javax.swing.table.AbstractTableModel; 061 import javax.swing.table.TableCellEditor; 062 import javax.swing.table.TableCellRenderer; 063 import javax.swing.table.TableModel; 064 import javax.swing.tree.DefaultTreeCellRenderer; 065 import javax.swing.tree.DefaultTreeSelectionModel; 066 import javax.swing.tree.TreeCellRenderer; 067 import javax.swing.tree.TreePath; 068 import javax.swing.tree.TreeSelectionModel; 069 070 import org.jdesktop.swingx.decorator.ComponentAdapter; 071 import org.jdesktop.swingx.renderer.StringValue; 072 import org.jdesktop.swingx.renderer.StringValues; 073 import org.jdesktop.swingx.rollover.RolloverProducer; 074 import org.jdesktop.swingx.rollover.RolloverRenderer; 075 import org.jdesktop.swingx.tree.DefaultXTreeCellRenderer; 076 import org.jdesktop.swingx.treetable.DefaultTreeTableModel; 077 import org.jdesktop.swingx.treetable.TreeTableCellEditor; 078 import org.jdesktop.swingx.treetable.TreeTableModel; 079 080 /** 081 * <p><code>JXTreeTable</code> is a specialized {@link javax.swing.JTable table} 082 * consisting of a single column in which to display hierarchical data, and any 083 * number of other columns in which to display regular data. The interface for 084 * the data model used by a <code>JXTreeTable</code> is 085 * {@link org.jdesktop.swingx.treetable.TreeTableModel}. It extends the 086 * {@link javax.swing.tree.TreeModel} interface to allow access to cell data by 087 * column indices within each node of the tree hierarchy.</p> 088 * 089 * <p>The most straightforward way create and use a <code>JXTreeTable</code>, is to 090 * first create a suitable data model for it, and pass that to a 091 * <code>JXTreeTable</code> constructor, as shown below: 092 * <pre> 093 * TreeTableModel treeTableModel = new FileSystemModel(); // any TreeTableModel 094 * JXTreeTable treeTable = new JXTreeTable(treeTableModel); 095 * JScrollPane scrollpane = new JScrollPane(treeTable); 096 * </pre> 097 * See {@link javax.swing.JTable} for an explanation of why putting the treetable 098 * inside a scroll pane is necessary.</p> 099 * 100 * <p>A single treetable model instance may be shared among more than one 101 * <code>JXTreeTable</code> instances. To access the treetable model, always call 102 * {@link #getTreeTableModel() getTreeTableModel} and 103 * {@link #setTreeTableModel(org.jdesktop.swingx.treetable.TreeTableModel) setTreeTableModel}. 104 * <code>JXTreeTable</code> wraps the supplied treetable model inside a private 105 * adapter class to adapt it to a {@link javax.swing.table.TableModel}. Although 106 * the model adapter is accessible through the {@link #getModel() getModel} method, you 107 * should avoid accessing and manipulating it in any way. In particular, each 108 * model adapter instance is tightly bound to a single table instance, and any 109 * attempt to share it with another table (for example, by calling 110 * {@link #setModel(javax.swing.table.TableModel) setModel}) 111 * will throw an <code>IllegalArgumentException</code>! 112 * 113 * @author Philip Milne 114 * @author Scott Violet 115 * @author Ramesh Gupta 116 */ 117 public class JXTreeTable extends JXTable { 118 @SuppressWarnings("unused") 119 private static final Logger LOG = Logger.getLogger(JXTreeTable.class 120 .getName()); 121 /** 122 * Key for clientProperty to decide whether to apply hack around #168-jdnc. 123 */ 124 public static final String DRAG_HACK_FLAG_KEY = "treeTable.dragHackFlag"; 125 /** 126 * Key for clientProperty to decide whether to apply hack around #766-swingx. 127 */ 128 public static final String DROP_HACK_FLAG_KEY = "treeTable.dropHackFlag"; 129 /** 130 * Renderer used to render cells within the 131 * {@link #isHierarchical(int) hierarchical} column. 132 * renderer extends JXTree and implements TableCellRenderer 133 */ 134 private TreeTableCellRenderer renderer; 135 136 /** 137 * Editor used to edit cells within the 138 * {@link #isHierarchical(int) hierarchical} column. 139 */ 140 private TreeTableCellEditor hierarchicalEditor; 141 142 private TreeTableHacker treeTableHacker; 143 private boolean consumedOnPress; 144 145 /** 146 * Constructs a JXTreeTable using a 147 * {@link org.jdesktop.swingx.treetable.DefaultTreeTableModel}. 148 */ 149 public JXTreeTable() { 150 this(new DefaultTreeTableModel()); 151 } 152 153 /** 154 * Constructs a JXTreeTable using the specified 155 * {@link org.jdesktop.swingx.treetable.TreeTableModel}. 156 * 157 * @param treeModel model for the JXTreeTable 158 */ 159 public JXTreeTable(TreeTableModel treeModel) { 160 this(new JXTreeTable.TreeTableCellRenderer(treeModel)); 161 } 162 163 /** 164 * Constructs a <code>JXTreeTable</code> using the specified 165 * {@link org.jdesktop.swingx.JXTreeTable.TreeTableCellRenderer}. 166 * 167 * @param renderer 168 * cell renderer for the tree portion of this JXTreeTable 169 * instance. 170 */ 171 private JXTreeTable(TreeTableCellRenderer renderer) { 172 // To avoid unnecessary object creation, such as the construction of a 173 // DefaultTableModel, it is better to invoke 174 // super(TreeTableModelAdapter) directly, instead of first invoking 175 // super() followed by a call to setTreeTableModel(TreeTableModel). 176 177 // Adapt tree model to table model before invoking super() 178 super(new TreeTableModelAdapter(renderer)); 179 180 // renderer-related initialization 181 init(renderer); // private method 182 initActions(); 183 // disable sorting 184 super.setSortable(false); 185 super.setAutoCreateRowSorter(false); 186 super.setRowSorter(null); 187 // no grid 188 setShowGrid(false, false); 189 190 hierarchicalEditor = new TreeTableCellEditor(renderer); 191 192 // // No grid. 193 // setShowGrid(false); // superclass default is "true" 194 // 195 // // Default intercell spacing 196 // setIntercellSpacing(spacing); // for both row margin and column margin 197 198 } 199 200 /** 201 * Initializes this JXTreeTable and permanently binds the specified renderer 202 * to it. 203 * 204 * @param renderer private tree/renderer permanently and exclusively bound 205 * to this JXTreeTable. 206 */ 207 private void init(TreeTableCellRenderer renderer) { 208 this.renderer = renderer; 209 assert ((TreeTableModelAdapter) getModel()).tree == this.renderer; 210 211 // Force the JTable and JTree to share their row selection models. 212 ListToTreeSelectionModelWrapper selectionWrapper = 213 new ListToTreeSelectionModelWrapper(); 214 215 // JW: when would that happen? 216 if (renderer != null) { 217 renderer.bind(this); // IMPORTANT: link back! 218 renderer.setSelectionModel(selectionWrapper); 219 } 220 // adjust the tree's rowHeight to this.rowHeight 221 adjustTreeRowHeight(getRowHeight()); 222 223 setSelectionModel(selectionWrapper.getListSelectionModel()); 224 225 // propagate the lineStyle property to the renderer 226 PropertyChangeListener l = new PropertyChangeListener() { 227 228 public void propertyChange(PropertyChangeEvent evt) { 229 JXTreeTable.this.renderer.putClientProperty(evt.getPropertyName(), evt.getNewValue()); 230 231 } 232 233 }; 234 addPropertyChangeListener("JTree.lineStyle", l); 235 236 } 237 238 239 240 private void initActions() { 241 // Register the actions that this class can handle. 242 ActionMap map = getActionMap(); 243 map.put("expand-all", new Actions("expand-all")); 244 map.put("collapse-all", new Actions("collapse-all")); 245 } 246 247 /** 248 * A small class which dispatches actions. 249 * TODO: Is there a way that we can make this static? 250 */ 251 private class Actions extends UIAction { 252 Actions(String name) { 253 super(name); 254 } 255 256 public void actionPerformed(ActionEvent evt) { 257 if ("expand-all".equals(getName())) { 258 expandAll(); 259 } 260 else if ("collapse-all".equals(getName())) { 261 collapseAll(); 262 } 263 } 264 } 265 266 /** 267 * {@inheritDoc} <p> 268 * Overridden to do nothing. 269 * 270 * TreeTable is not sortable because there is no equivalent to 271 * RowSorter (which is targeted to linear structures) for 272 * hierarchical data. 273 * 274 */ 275 @Override 276 public void setSortable(boolean sortable) { 277 // no-op 278 } 279 280 281 282 /** 283 * {@inheritDoc} <p> 284 * Overridden to do nothing. 285 * 286 * TreeTable is not sortable because there is no equivalent to 287 * RowSorter (which is targeted to linear structures) for 288 * hierarchical data. 289 * 290 */ 291 @Override 292 public void setAutoCreateRowSorter(boolean autoCreateRowSorter) { 293 } 294 295 /** 296 * {@inheritDoc} <p> 297 * Overridden to do nothing. 298 * 299 * TreeTable is not sortable because there is no equivalent to 300 * RowSorter (which is targeted to linear structures) for 301 * hierarchical data. 302 * 303 */ 304 @Override 305 public void setRowSorter(RowSorter<? extends TableModel> sorter) { 306 } 307 308 /** 309 * {@inheritDoc} <p> 310 * 311 * Overridden to keep the tree's enabled in synch. 312 */ 313 @Override 314 public void setEnabled(boolean enabled) { 315 renderer.setEnabled(enabled); 316 super.setEnabled(enabled); 317 } 318 319 320 /** 321 * {@inheritDoc} <p> 322 * 323 * Overridden to keep the tree's selectionBackground in synch. 324 */ 325 @Override 326 public void setSelectionBackground(Color selectionBackground) { 327 // happens on instantiation, updateUI is called before the renderer is installed 328 if (renderer != null) 329 renderer.setSelectionBackground(selectionBackground); 330 super.setSelectionBackground(selectionBackground); 331 } 332 333 /** 334 * {@inheritDoc} <p> 335 * 336 * Overridden to keep the tree's selectionForeground in synch. 337 */ 338 @Override 339 public void setSelectionForeground(Color selectionForeground) { 340 // happens on instantiation, updateUI is called before the renderer is installed 341 if (renderer != null) 342 renderer.setSelectionForeground(selectionForeground); 343 super.setSelectionForeground(selectionForeground); 344 } 345 346 /** 347 * Overriden to invoke repaint for the particular location if 348 * the column contains the tree. This is done as the tree editor does 349 * not fill the bounds of the cell, we need the renderer to paint 350 * the tree in the background, and then draw the editor over it. 351 * You should not need to call this method directly. <p> 352 * 353 * Additionally, there is tricksery involved to expand/collapse 354 * the nodes. 355 * 356 * {@inheritDoc} 357 */ 358 @Override 359 public boolean editCellAt(int row, int column, EventObject e) { 360 getTreeTableHacker().hitHandleDetectionFromEditCell(column, e); // RG: Fix Issue 49! 361 boolean canEdit = super.editCellAt(row, column, e); 362 if (canEdit && isHierarchical(column)) { 363 repaint(getCellRect(row, column, false)); 364 } 365 return canEdit; 366 } 367 368 /** 369 * Overridden to enable hit handle detection a mouseEvent which triggered 370 * a expand/collapse. 371 */ 372 @Override 373 protected void processMouseEvent(MouseEvent e) { 374 // BasicTableUI selects on released if the pressed had been 375 // consumed. So we try to fish for the accompanying released 376 // here and consume it as wll. 377 if ((e.getID() == MouseEvent.MOUSE_RELEASED) && consumedOnPress) { 378 consumedOnPress = false; 379 e.consume(); 380 return; 381 } 382 if (getTreeTableHacker().hitHandleDetectionFromProcessMouse(e)) { 383 // Issue #332-swing: hacking around selection loss. 384 // prevent the 385 // _table_ selection by consuming the mouseEvent 386 // if it resulted in a expand/collapse 387 consumedOnPress = true; 388 e.consume(); 389 return; 390 } 391 consumedOnPress = false; 392 super.processMouseEvent(e); 393 } 394 395 396 protected TreeTableHacker getTreeTableHacker() { 397 if (treeTableHacker == null) { 398 treeTableHacker = createTreeTableHacker(); 399 } 400 return treeTableHacker; 401 } 402 403 protected TreeTableHacker createTreeTableHacker() { 404 // return new TreeTableHacker(); 405 return new TreeTableHackerExt(); 406 // return new TreeTableHackerExt2(); 407 } 408 409 /** 410 * Temporary class to have all the hacking at one place. Naturally, it will 411 * change a lot. The base class has the "stable" behaviour as of around 412 * jun2006 (before starting the fix for 332-swingx). <p> 413 * 414 * specifically: 415 * 416 * <ol> 417 * <li> hitHandleDetection triggeredn in editCellAt 418 * </ol> 419 * 420 */ 421 public class TreeTableHacker { 422 423 protected boolean expansionChangedFlag; 424 425 /** 426 * Decision whether the handle hit detection 427 * should be done in processMouseEvent or editCellAt. 428 * Here: returns false. 429 * 430 * @return true for handle hit detection in processMouse, false 431 * for editCellAt. 432 */ 433 protected boolean isHitDetectionFromProcessMouse() { 434 return false; 435 } 436 437 /** 438 * Entry point for hit handle detection called from editCellAt, 439 * does nothing if isHitDetectionFromProcessMouse is true; 440 * 441 * @see #isHitDetectionFromProcessMouse() 442 */ 443 public void hitHandleDetectionFromEditCell(int column, EventObject e) { 444 if (!isHitDetectionFromProcessMouse()) { 445 expandOrCollapseNode(column, e); 446 } 447 } 448 449 /** 450 * Entry point for hit handle detection called from processMouse. 451 * Does nothing if isHitDetectionFromProcessMouse is false. 452 * 453 * @return true if the mouseEvent triggered an expand/collapse in 454 * the renderer, false otherwise. 455 * 456 * @see #isHitDetectionFromProcessMouse() 457 */ 458 public boolean hitHandleDetectionFromProcessMouse(MouseEvent e) { 459 if (!isHitDetectionFromProcessMouse()) 460 return false; 461 int col = columnAtPoint(e.getPoint()); 462 return ((col >= 0) && expandOrCollapseNode(columnAtPoint(e 463 .getPoint()), e)); 464 } 465 466 /** 467 * Complete editing if collapsed/expanded. 468 * <p> 469 * 470 * Is: first try to stop editing before falling back to cancel. 471 * <p> 472 * This is part of fix for #730-swingx - editingStopped not always 473 * called. The other part is to call this from the renderer before 474 * expansion related state has changed. 475 * <p> 476 * 477 * Was: any editing is always cancelled. 478 * <p> 479 * This is a rude fix to #120-jdnc: data corruption on collapse/expand 480 * if editing. This is called from the renderer after expansion related 481 * state has changed. 482 * 483 */ 484 protected void completeEditing() { 485 if (isEditing()) { 486 boolean success = getCellEditor().stopCellEditing(); 487 if (!success) { 488 getCellEditor().cancelCellEditing(); 489 } 490 } 491 } 492 493 /** 494 * Tricksery to make the tree expand/collapse. 495 * <p> 496 * 497 * This might be - indirectly - called from one of two places: 498 * <ol> 499 * <li> editCellAt: original, stable but buggy (#332, #222) the table's 500 * own selection had been changed due to the click before even entering 501 * into editCellAt so all tree selection state is lost. 502 * 503 * <li> processMouseEvent: the idea is to catch the mouseEvent, check 504 * if it triggered an expanded/collapsed, consume and return if so or 505 * pass to super if not. 506 * </ol> 507 * 508 * <p> 509 * widened access for testing ... 510 * 511 * 512 * @param column the column index under the event, if any. 513 * @param e the event which might trigger a expand/collapse. 514 * 515 * @return this methods evaluation as to whether the event triggered a 516 * expand/collaps 517 */ 518 protected boolean expandOrCollapseNode(int column, EventObject e) { 519 if (!isHierarchical(column)) 520 return false; 521 if (!mightBeExpansionTrigger(e)) 522 return false; 523 boolean changedExpansion = false; 524 MouseEvent me = (MouseEvent) e; 525 if (hackAroundDragEnabled(me)) { 526 /* 527 * Hack around #168-jdnc: dirty little hack mentioned in the 528 * forum discussion about the issue: fake a mousePressed if drag 529 * enabled. The usability is slightly impaired because the 530 * expand/collapse is effectively triggered on released only 531 * (drag system intercepts and consumes all other). 532 */ 533 me = new MouseEvent((Component) me.getSource(), 534 MouseEvent.MOUSE_PRESSED, me.getWhen(), me 535 .getModifiers(), me.getX(), me.getY(), me 536 .getClickCount(), me.isPopupTrigger()); 537 538 } 539 // If the modifiers are not 0 (or the left mouse button), 540 // tree may try and toggle the selection, and table 541 // will then try and toggle, resulting in the 542 // selection remaining the same. To avoid this, we 543 // only dispatch when the modifiers are 0 (or the left mouse 544 // button). 545 if (me.getModifiers() == 0 546 || me.getModifiers() == InputEvent.BUTTON1_MASK) { 547 MouseEvent pressed = new MouseEvent(renderer, me.getID(), me 548 .getWhen(), me.getModifiers(), me.getX() 549 - getCellRect(0, column, false).x, me.getY(), me 550 .getClickCount(), me.isPopupTrigger()); 551 renderer.dispatchEvent(pressed); 552 // For Mac OS X, we need to dispatch a MOUSE_RELEASED as well 553 MouseEvent released = new MouseEvent(renderer, 554 java.awt.event.MouseEvent.MOUSE_RELEASED, pressed 555 .getWhen(), pressed.getModifiers(), pressed 556 .getX(), pressed.getY(), pressed 557 .getClickCount(), pressed.isPopupTrigger()); 558 renderer.dispatchEvent(released); 559 if (expansionChangedFlag) { 560 changedExpansion = true; 561 } 562 } 563 expansionChangedFlag = false; 564 return changedExpansion; 565 } 566 567 protected boolean mightBeExpansionTrigger(EventObject e) { 568 if (!(e instanceof MouseEvent)) return false; 569 MouseEvent me = (MouseEvent) e; 570 if (!SwingUtilities.isLeftMouseButton(me)) return false; 571 return me.getID() == MouseEvent.MOUSE_PRESSED; 572 } 573 574 /** 575 * called from the renderer's setExpandedPath after 576 * all expansion-related updates happend. 577 * 578 */ 579 protected void expansionChanged() { 580 expansionChangedFlag = true; 581 } 582 583 } 584 585 /** 586 * 587 * Note: currently this class looks a bit funny (only overriding 588 * the hit decision method). That's because the "experimental" code 589 * as of the last round moved to stable. But I expect that there's more 590 * to come, so I leave it here. 591 * 592 * <ol> 593 * <li> hit handle detection in processMouse 594 * </ol> 595 */ 596 public class TreeTableHackerExt extends TreeTableHacker { 597 598 599 /** 600 * Here: returns true. 601 * @inheritDoc 602 */ 603 @Override 604 protected boolean isHitDetectionFromProcessMouse() { 605 return true; 606 } 607 608 } 609 610 /** 611 * Patch for #471-swingx: no selection on click in hierarchical column 612 * outside of node-text. Mar 2007. 613 * <p> 614 * 615 * Note: this solves the selection issue but is not bidi-compliant - in RToL 616 * contexts the expansion/collapse handles aren't detected and consequently 617 * are disfunctional. 618 * 619 * @author tiberiu@dev.java.net 620 */ 621 public class TreeTableHackerExt2 extends TreeTableHackerExt { 622 @Override 623 protected boolean expandOrCollapseNode(int column, EventObject e) { 624 if (!isHierarchical(column)) 625 return false; 626 if (!mightBeExpansionTrigger(e)) 627 return false; 628 boolean changedExpansion = false; 629 MouseEvent me = (MouseEvent) e; 630 if (hackAroundDragEnabled(me)) { 631 /* 632 * Hack around #168-jdnc: dirty little hack mentioned in the 633 * forum discussion about the issue: fake a mousePressed if drag 634 * enabled. The usability is slightly impaired because the 635 * expand/collapse is effectively triggered on released only 636 * (drag system intercepts and consumes all other). 637 */ 638 me = new MouseEvent((Component) me.getSource(), 639 MouseEvent.MOUSE_PRESSED, me.getWhen(), me 640 .getModifiers(), me.getX(), me.getY(), me 641 .getClickCount(), me.isPopupTrigger()); 642 } 643 // If the modifiers are not 0 (or the left mouse button), 644 // tree may try and toggle the selection, and table 645 // will then try and toggle, resulting in the 646 // selection remaining the same. To avoid this, we 647 // only dispatch when the modifiers are 0 (or the left mouse 648 // button). 649 if (me.getModifiers() == 0 650 || me.getModifiers() == InputEvent.BUTTON1_MASK) { 651 // compute where the mouse point is relative to the tree 652 // renderer 653 Point treeMousePoint = getTreeMousePoint(column, me); 654 int treeRow = renderer.getRowForLocation(treeMousePoint.x, 655 treeMousePoint.y); 656 int row = 0; 657 if (treeRow < 0) { 658 row = renderer.getClosestRowForLocation(treeMousePoint.x, 659 treeMousePoint.y); 660 Rectangle bounds = renderer.getRowBounds(row); 661 if (bounds == null) { 662 row = -1; 663 } else { 664 if ((bounds.y + bounds.height < treeMousePoint.y) 665 || bounds.x > treeMousePoint.x) { 666 row = -1; 667 } 668 } 669 // make sure the expansionChangedFlag is set to false for 670 // the case that up in the tree nothing happens 671 expansionChangedFlag = false; 672 } 673 674 if ((treeRow >= 0) || ((treeRow < 0) && (row < 0))) { 675 // default selection 676 MouseEvent pressed = new MouseEvent(renderer, me.getID(), 677 me.getWhen(), me.getModifiers(), treeMousePoint.x, 678 treeMousePoint.y, me.getClickCount(), me 679 .isPopupTrigger()); 680 renderer.dispatchEvent(pressed); 681 // For Mac OS X, we need to dispatch a MOUSE_RELEASED as 682 // well 683 MouseEvent released = new MouseEvent(renderer, 684 java.awt.event.MouseEvent.MOUSE_RELEASED, pressed 685 .getWhen(), pressed.getModifiers(), pressed 686 .getX(), pressed.getY(), pressed 687 .getClickCount(), pressed.isPopupTrigger()); 688 renderer.dispatchEvent(released); 689 } 690 if (expansionChangedFlag) { 691 changedExpansion = true; 692 } 693 } 694 expansionChangedFlag = false; 695 return changedExpansion; 696 } 697 698 /** 699 * This is a patch provided for Issue #980-swingx which should 700 * improve the bidi-compliance. Still doesn't work in our 701 * visual tests... 702 * 703 * @param column the column index under the event, if any. 704 * @param e the event which might trigger a expand/collapse. 705 * @return the Point adjusted for bidi 706 */ 707 protected Point getTreeMousePoint(int column, MouseEvent me) { 708 // return new Point(me.getX() 709 // - getCellRect(0, column, false).x, me.getY()); 710 Rectangle tableCellRect = getCellRect(0, column, false); 711 712 if( getComponentOrientation().isLeftToRight() ) { 713 return new Point(me.getX() - tableCellRect.x, me.getY()); 714 } 715 716 int x = (me.getX() - tableCellRect.x) - tableCellRect.width - 10; 717 return new Point(x, me.getY()); 718 } 719 } 720 721 /** 722 * decides whether we want to apply the hack for #168-jdnc. here: returns 723 * true if dragEnabled() and the improved drag handling is not activated (or 724 * the system property is not accessible). The given mouseEvent is not 725 * analysed. 726 * 727 * PENDING: Mustang? 728 * 729 * @param me the mouseEvent that triggered a editCellAt 730 * @return true if the hack should be applied. 731 */ 732 protected boolean hackAroundDragEnabled(MouseEvent me) { 733 Boolean dragHackFlag = (Boolean) getClientProperty(DRAG_HACK_FLAG_KEY); 734 if (dragHackFlag == null) { 735 // access and store the system property as a client property once 736 String priority = null; 737 try { 738 priority = System.getProperty("sun.swing.enableImprovedDragGesture"); 739 740 } catch (Exception ex) { 741 // found some foul expression or failed to read the property 742 } 743 dragHackFlag = (priority == null); 744 putClientProperty(DRAG_HACK_FLAG_KEY, dragHackFlag); 745 } 746 return getDragEnabled() && dragHackFlag; 747 } 748 749 /** 750 * Overridden to provide a workaround for BasicTableUI anomaly. Make sure 751 * the UI never tries to resize the editor. The UI currently uses different 752 * techniques to paint the renderers and editors. So, overriding setBounds() 753 * is not the right thing to do for an editor. Returning -1 for the 754 * editing row in this case, ensures the editor is never painted. 755 * 756 * {@inheritDoc} 757 */ 758 @Override 759 public int getEditingRow() { 760 return isHierarchical(editingColumn) ? -1 : editingRow; 761 } 762 763 /** 764 * Returns the actual row that is editing as <code>getEditingRow</code> 765 * will always return -1. 766 */ 767 private int realEditingRow() { 768 return editingRow; 769 } 770 771 /** 772 * Sets the data model for this JXTreeTable to the specified 773 * {@link org.jdesktop.swingx.treetable.TreeTableModel}. The same data model 774 * may be shared by any number of JXTreeTable instances. 775 * 776 * @param treeModel data model for this JXTreeTable 777 */ 778 public void setTreeTableModel(TreeTableModel treeModel) { 779 TreeTableModel old = getTreeTableModel(); 780 // boolean rootVisible = isRootVisible(); 781 // setRootVisible(false); 782 renderer.setModel(treeModel); 783 // setRootVisible(rootVisible); 784 785 firePropertyChange("treeTableModel", old, getTreeTableModel()); 786 } 787 788 /** 789 * Returns the underlying TreeTableModel for this JXTreeTable. 790 * 791 * @return the underlying TreeTableModel for this JXTreeTable 792 */ 793 public TreeTableModel getTreeTableModel() { 794 return (TreeTableModel) renderer.getModel(); 795 } 796 797 /** 798 * <p>Overrides superclass version to make sure that the specified 799 * {@link javax.swing.table.TableModel} is compatible with JXTreeTable before 800 * invoking the inherited version.</p> 801 * 802 * <p>Because JXTreeTable internally adapts an 803 * {@link org.jdesktop.swingx.treetable.TreeTableModel} to make it a compatible 804 * TableModel, <b>this method should never be called directly</b>. Use 805 * {@link #setTreeTableModel(org.jdesktop.swingx.treetable.TreeTableModel) setTreeTableModel} instead.</p> 806 * 807 * <p>While it is possible to obtain a reference to this adapted 808 * version of the TableModel by calling {@link javax.swing.JTable#getModel()}, 809 * any attempt to call setModel() with that adapter will fail because 810 * the adapter might have been bound to a different JXTreeTable instance. If 811 * you want to extract the underlying TreeTableModel, which, by the way, 812 * <em>can</em> be shared, use {@link #getTreeTableModel() getTreeTableModel} 813 * instead</p>. 814 * 815 * @param tableModel must be a TreeTableModelAdapter 816 * @throws IllegalArgumentException if the specified tableModel is not an 817 * instance of TreeTableModelAdapter 818 */ 819 @Override 820 public final void setModel(TableModel tableModel) { // note final keyword 821 if (tableModel instanceof TreeTableModelAdapter) { 822 if (((TreeTableModelAdapter) tableModel).getTreeTable() == null) { 823 // Passing the above test ensures that this method is being 824 // invoked either from JXTreeTable/JTable constructor or from 825 // setTreeTableModel(TreeTableModel) 826 super.setModel(tableModel); // invoke superclass version 827 828 ((TreeTableModelAdapter) tableModel).bind(this); // permanently bound 829 // Once a TreeTableModelAdapter is bound to any JXTreeTable instance, 830 // invoking JXTreeTable.setModel() with that adapter will throw an 831 // IllegalArgumentException, because we really want to make sure 832 // that a TreeTableModelAdapter is NOT shared by another JXTreeTable. 833 } 834 else { 835 throw new IllegalArgumentException("model already bound"); 836 } 837 } 838 else { 839 throw new IllegalArgumentException("unsupported model type"); 840 } 841 } 842 843 844 845 @Override 846 public void tableChanged(TableModelEvent e) { 847 if (isStructureChanged(e) || isUpdate(e)) { 848 super.tableChanged(e); 849 } else { 850 resizeAndRepaint(); 851 } 852 } 853 854 /** 855 * Throws UnsupportedOperationException because variable height rows are 856 * not supported. 857 * 858 * @param row ignored 859 * @param rowHeight ignored 860 * @throws UnsupportedOperationException because variable height rows are 861 * not supported 862 */ 863 @Override 864 public final void setRowHeight(int row, int rowHeight) { 865 throw new UnsupportedOperationException("variable height rows not supported"); 866 } 867 868 /** 869 * Sets the row height for this JXTreeTable and forwards the 870 * row height to the renderering tree. 871 * 872 * @param rowHeight height of a row. 873 */ 874 @Override 875 public void setRowHeight(int rowHeight) { 876 super.setRowHeight(rowHeight); 877 adjustTreeRowHeight(getRowHeight()); 878 } 879 880 /** 881 * Forwards tableRowHeight to tree. 882 * 883 * @param tableRowHeight height of a row. 884 */ 885 protected void adjustTreeRowHeight(int tableRowHeight) { 886 if (renderer != null && renderer.getRowHeight() != tableRowHeight) { 887 renderer.setRowHeight(tableRowHeight); 888 } 889 } 890 891 /** 892 * Forwards treeRowHeight to table. This is for completeness only: the 893 * rendering tree is under our total control, so we don't expect 894 * any external call to tree.setRowHeight. 895 * 896 * @param treeRowHeight height of a row. 897 */ 898 protected void adjustTableRowHeight(int treeRowHeight) { 899 if (getRowHeight() != treeRowHeight) { 900 adminSetRowHeight(treeRowHeight); 901 } 902 } 903 904 905 /** 906 * <p>Overridden to ensure that private renderer state is kept in sync with the 907 * state of the component. Calls the inherited version after performing the 908 * necessary synchronization. If you override this method, make sure you call 909 * this version from your version of this method.</p> 910 * 911 * <p>This version maps the selection mode used by the renderer to match the 912 * selection mode specified for the table. Specifically, the modes are mapped 913 * as follows: 914 * <pre> 915 * ListSelectionModel.SINGLE_INTERVAL_SELECTION: TreeSelectionModel.CONTIGUOUS_TREE_SELECTION; 916 * ListSelectionModel.MULTIPLE_INTERVAL_SELECTION: TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION; 917 * any other (default): TreeSelectionModel.SINGLE_TREE_SELECTION; 918 * </pre> 919 * 920 * {@inheritDoc} 921 * 922 * @param mode any of the table selection modes 923 */ 924 @Override 925 public void setSelectionMode(int mode) { 926 if (renderer != null) { 927 switch (mode) { 928 case ListSelectionModel.SINGLE_INTERVAL_SELECTION: { 929 renderer.getSelectionModel().setSelectionMode( 930 TreeSelectionModel.CONTIGUOUS_TREE_SELECTION); 931 break; 932 } 933 case ListSelectionModel.MULTIPLE_INTERVAL_SELECTION: { 934 renderer.getSelectionModel().setSelectionMode( 935 TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); 936 break; 937 } 938 default: { 939 renderer.getSelectionModel().setSelectionMode( 940 TreeSelectionModel.SINGLE_TREE_SELECTION); 941 break; 942 } 943 } 944 } 945 super.setSelectionMode(mode); 946 } 947 948 /** 949 * {@inheritDoc} <p> 950 * 951 * Overridden to decorate the tree's renderer after calling super. 952 * At that point, it is only the tree itself that has been decorated. 953 * 954 * @param renderer the <code>TableCellRenderer</code> to prepare 955 * @param row the row of the cell to render, where 0 is the first row 956 * @param column the column of the cell to render, where 0 is the first column 957 * @return the <code>Component</code> used as a stamp to render the specified cell 958 * 959 * @see #applyRenderer(Component, ComponentAdapter) 960 */ 961 @Override 962 public Component prepareRenderer(TableCellRenderer renderer, int row, 963 int column) { 964 Component component = super.prepareRenderer(renderer, row, column); 965 return applyRenderer(component, getComponentAdapter(row, column)); 966 } 967 968 /** 969 * Performs configuration of the tree's renderer if the adapter's column is 970 * the hierarchical column, does nothing otherwise. 971 * <p> 972 * 973 * Note: this is legacy glue if the treeCellRenderer is of type 974 * DefaultTreeCellRenderer. In that case the renderer's 975 * background/foreground/Non/Selection colors are set to the tree's 976 * background/foreground depending on the adapter's selection state. Does 977 * nothing if the treeCellRenderer is backed by a ComponentProvider. 978 * 979 * @param component the rendering component 980 * @param adapter component data adapter 981 * @throws NullPointerException if the specified component or adapter is 982 * null 983 */ 984 protected Component applyRenderer(Component component, 985 ComponentAdapter adapter) { 986 if (component == null) { 987 throw new IllegalArgumentException("null component"); 988 } 989 if (adapter == null) { 990 throw new IllegalArgumentException("null component data adapter"); 991 } 992 993 if (isHierarchical(adapter.column)) { 994 // After all decorators have been applied, make sure that relevant 995 // attributes of the table cell renderer are applied to the 996 // tree cell renderer before the hierarchical column is rendered! 997 TreeCellRenderer tcr = renderer.getCellRenderer(); 998 if (tcr instanceof JXTree.DelegatingRenderer) { 999 tcr = ((JXTree.DelegatingRenderer) tcr).getDelegateRenderer(); 1000 1001 } 1002 if (tcr instanceof DefaultTreeCellRenderer) { 1003 1004 DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr); 1005 // this effectively overwrites the dtcr settings 1006 if (adapter.isSelected()) { 1007 dtcr.setTextSelectionColor(component.getForeground()); 1008 dtcr.setBackgroundSelectionColor(component.getBackground()); 1009 } else { 1010 dtcr.setTextNonSelectionColor(component.getForeground()); 1011 dtcr.setBackgroundNonSelectionColor(component 1012 .getBackground()); 1013 } 1014 } 1015 } 1016 return component; 1017 } 1018 1019 /** 1020 * Sets the specified TreeCellRenderer as the Tree cell renderer. 1021 * 1022 * @param cellRenderer to use for rendering tree cells. 1023 */ 1024 public void setTreeCellRenderer(TreeCellRenderer cellRenderer) { 1025 if (renderer != null) { 1026 renderer.setCellRenderer(cellRenderer); 1027 } 1028 } 1029 1030 public TreeCellRenderer getTreeCellRenderer() { 1031 return renderer.getCellRenderer(); 1032 } 1033 1034 1035 @Override 1036 public String getToolTipText(MouseEvent event) { 1037 int column = columnAtPoint(event.getPoint()); 1038 if (isHierarchical(column)) { 1039 int row = rowAtPoint(event.getPoint()); 1040 return renderer.getToolTipText(event, row, column); 1041 } 1042 return super.getToolTipText(event); 1043 } 1044 1045 /** 1046 * Sets the specified icon as the icon to use for rendering collapsed nodes. 1047 * 1048 * @param icon to use for rendering collapsed nodes 1049 * 1050 * @see JXTree#setCollapsedIcon(Icon) 1051 */ 1052 public void setCollapsedIcon(Icon icon) { 1053 renderer.setCollapsedIcon(icon); 1054 } 1055 1056 /** 1057 * Sets the specified icon as the icon to use for rendering expanded nodes. 1058 * 1059 * @param icon to use for rendering expanded nodes 1060 * 1061 * @see JXTree#setExpandedIcon(Icon) 1062 */ 1063 public void setExpandedIcon(Icon icon) { 1064 renderer.setExpandedIcon(icon); 1065 } 1066 1067 /** 1068 * Sets the specified icon as the icon to use for rendering open container nodes. 1069 * 1070 * @param icon to use for rendering open nodes 1071 * 1072 * @see JXTree#setOpenIcon(Icon) 1073 */ 1074 public void setOpenIcon(Icon icon) { 1075 renderer.setOpenIcon(icon); 1076 } 1077 1078 /** 1079 * Sets the specified icon as the icon to use for rendering closed container nodes. 1080 * 1081 * @param icon to use for rendering closed nodes 1082 * 1083 * @see JXTree#setClosedIcon(Icon) 1084 */ 1085 public void setClosedIcon(Icon icon) { 1086 renderer.setClosedIcon(icon); 1087 } 1088 1089 /** 1090 * Sets the specified icon as the icon to use for rendering leaf nodes. 1091 * 1092 * @param icon to use for rendering leaf nodes 1093 * 1094 * @see JXTree#setLeafIcon(Icon) 1095 */ 1096 public void setLeafIcon(Icon icon) { 1097 renderer.setLeafIcon(icon); 1098 } 1099 1100 /** 1101 * Property to control whether per-tree icons should be 1102 * copied to the renderer on setTreeCellRenderer. <p> 1103 * 1104 * The default value is false. 1105 * 1106 * @param overwrite a boolean to indicate if the per-tree Icons should 1107 * be copied to the new renderer on setTreeCellRenderer. 1108 * 1109 * @see #isOverwriteRendererIcons() 1110 * @see #setLeafIcon(Icon) 1111 * @see #setOpenIcon(Icon) 1112 * @see #setClosedIcon(Icon) 1113 * @see JXTree#setOverwriteRendererIcons(boolean) 1114 */ 1115 public void setOverwriteRendererIcons(boolean overwrite) { 1116 renderer.setOverwriteRendererIcons(overwrite); 1117 } 1118 1119 1120 /** 1121 * Returns a boolean indicating whether the per-tree icons should be 1122 * copied to the renderer on setTreeCellRenderer. 1123 * 1124 * @return true if a TreeCellRenderer's icons will be overwritten with the 1125 * tree's Icons, false if the renderer's icons will be unchanged. 1126 * 1127 * @see #setOverwriteRendererIcons(boolean) 1128 * @see #setLeafIcon(Icon) 1129 * @see #setOpenIcon(Icon) 1130 * @see #setClosedIcon(Icon) 1131 * @see JXTree#isOverwriteRendererIcons() 1132 * 1133 */ 1134 public boolean isOverwriteRendererIcons() { 1135 return renderer.isOverwriteRendererIcons(); 1136 } 1137 1138 /** 1139 * Overridden to ensure that private renderer state is kept in sync with the 1140 * state of the component. Calls the inherited version after performing the 1141 * necessary synchronization. If you override this method, make sure you call 1142 * this version from your version of this method. 1143 */ 1144 @Override 1145 public void clearSelection() { 1146 if (renderer != null) { 1147 renderer.clearSelection(); 1148 } 1149 super.clearSelection(); 1150 } 1151 1152 /** 1153 * Collapses all nodes in the treetable. 1154 */ 1155 public void collapseAll() { 1156 renderer.collapseAll(); 1157 } 1158 1159 /** 1160 * Expands all nodes in the treetable. 1161 */ 1162 public void expandAll() { 1163 renderer.expandAll(); 1164 } 1165 1166 /** 1167 * Collapses the node at the specified path in the treetable. 1168 * 1169 * @param path path of the node to collapse 1170 */ 1171 public void collapsePath(TreePath path) { 1172 renderer.collapsePath(path); 1173 } 1174 1175 /** 1176 * Expands the the node at the specified path in the treetable. 1177 * 1178 * @param path path of the node to expand 1179 */ 1180 public void expandPath(TreePath path) { 1181 renderer.expandPath(path); 1182 } 1183 1184 /** 1185 * Makes sure all the path components in path are expanded (except 1186 * for the last path component) and scrolls so that the 1187 * node identified by the path is displayed. Only works when this 1188 * <code>JTree</code> is contained in a <code>JScrollPane</code>. 1189 * 1190 * (doc copied from JTree) 1191 * 1192 * PENDING: JW - where exactly do we want to scroll? Here: the scroll 1193 * is in vertical direction only. Might need to show the tree column? 1194 * 1195 * @param path the <code>TreePath</code> identifying the node to 1196 * bring into view 1197 */ 1198 public void scrollPathToVisible(TreePath path) { 1199 renderer.scrollPathToVisible(path); 1200 // if (path == null) return; 1201 // renderer.makeVisible(path); 1202 // int row = getRowForPath(path); 1203 // scrollRowToVisible(row); 1204 } 1205 1206 1207 /** 1208 * Collapses the row in the treetable. If the specified row index is 1209 * not valid, this method will have no effect. 1210 */ 1211 public void collapseRow(int row) { 1212 renderer.collapseRow(row); 1213 } 1214 1215 /** 1216 * Expands the specified row in the treetable. If the specified row index is 1217 * not valid, this method will have no effect. 1218 */ 1219 public void expandRow(int row) { 1220 renderer.expandRow(row); 1221 } 1222 1223 1224 /** 1225 * Returns true if the value identified by path is currently viewable, which 1226 * means it is either the root or all of its parents are expanded. Otherwise, 1227 * this method returns false. 1228 * 1229 * @return true, if the value identified by path is currently viewable; 1230 * false, otherwise 1231 */ 1232 public boolean isVisible(TreePath path) { 1233 return renderer.isVisible(path); 1234 } 1235 1236 /** 1237 * Returns true if the node identified by path is currently expanded. 1238 * Otherwise, this method returns false. 1239 * 1240 * @param path path 1241 * @return true, if the value identified by path is currently expanded; 1242 * false, otherwise 1243 */ 1244 public boolean isExpanded(TreePath path) { 1245 return renderer.isExpanded(path); 1246 } 1247 1248 /** 1249 * Returns true if the node at the specified display row is currently expanded. 1250 * Otherwise, this method returns false. 1251 * 1252 * @param row row 1253 * @return true, if the node at the specified display row is currently expanded. 1254 * false, otherwise 1255 */ 1256 public boolean isExpanded(int row) { 1257 return renderer.isExpanded(row); 1258 } 1259 1260 /** 1261 * Returns true if the node identified by path is currently collapsed, 1262 * this will return false if any of the values in path are currently not 1263 * being displayed. 1264 * 1265 * @param path path 1266 * @return true, if the value identified by path is currently collapsed; 1267 * false, otherwise 1268 */ 1269 public boolean isCollapsed(TreePath path) { 1270 return renderer.isCollapsed(path); 1271 } 1272 1273 /** 1274 * Returns true if the node at the specified display row is collapsed. 1275 * 1276 * @param row row 1277 * @return true, if the node at the specified display row is currently collapsed. 1278 * false, otherwise 1279 */ 1280 public boolean isCollapsed(int row) { 1281 return renderer.isCollapsed(row); 1282 } 1283 1284 1285 /** 1286 * Returns an <code>Enumeration</code> of the descendants of the 1287 * path <code>parent</code> that 1288 * are currently expanded. If <code>parent</code> is not currently 1289 * expanded, this will return <code>null</code>. 1290 * If you expand/collapse nodes while 1291 * iterating over the returned <code>Enumeration</code> 1292 * this may not return all 1293 * the expanded paths, or may return paths that are no longer expanded. 1294 * 1295 * @param parent the path which is to be examined 1296 * @return an <code>Enumeration</code> of the descendents of 1297 * <code>parent</code>, or <code>null</code> if 1298 * <code>parent</code> is not currently expanded 1299 */ 1300 1301 public Enumeration<?> getExpandedDescendants(TreePath parent) { 1302 return renderer.getExpandedDescendants(parent); 1303 } 1304 1305 1306 /** 1307 * Returns the TreePath for a given x,y location. 1308 * 1309 * @param x x value 1310 * @param y y value 1311 * 1312 * @return the <code>TreePath</code> for the givern location. 1313 */ 1314 public TreePath getPathForLocation(int x, int y) { 1315 int row = rowAtPoint(new Point(x,y)); 1316 if (row == -1) { 1317 return null; 1318 } 1319 return renderer.getPathForRow(row); 1320 } 1321 1322 /** 1323 * Returns the TreePath for a given row. 1324 * 1325 * @param row 1326 * 1327 * @return the <code>TreePath</code> for the given row. 1328 */ 1329 public TreePath getPathForRow(int row) { 1330 return renderer.getPathForRow(row); 1331 } 1332 1333 /** 1334 * Returns the row for a given TreePath. 1335 * 1336 * @param path 1337 * @return the row for the given <code>TreePath</code>. 1338 */ 1339 public int getRowForPath(TreePath path) { 1340 return renderer.getRowForPath(path); 1341 } 1342 1343 //------------------------------ exposed Tree properties 1344 1345 /** 1346 * Determines whether or not the root node from the TreeModel is visible. 1347 * 1348 * @param visible true, if the root node is visible; false, otherwise 1349 */ 1350 public void setRootVisible(boolean visible) { 1351 renderer.setRootVisible(visible); 1352 // JW: the revalidate forces the root to appear after a 1353 // toggling a visible from an initially invisible root. 1354 // JTree fires a propertyChange on the ROOT_VISIBLE_PROPERTY 1355 // BasicTreeUI reacts by (ultimately) calling JTree.treeDidChange 1356 // which revalidate the tree part. 1357 // Might consider to listen for the propertyChange (fired only if there 1358 // actually was a change) instead of revalidating unconditionally. 1359 revalidate(); 1360 repaint(); 1361 } 1362 1363 /** 1364 * Returns true if the root node of the tree is displayed. 1365 * 1366 * @return true if the root node of the tree is displayed 1367 */ 1368 public boolean isRootVisible() { 1369 return renderer.isRootVisible(); 1370 } 1371 1372 1373 /** 1374 * Sets the value of the <code>scrollsOnExpand</code> property for the tree 1375 * part. This property specifies whether the expanded paths should be scrolled 1376 * into view. In a look and feel in which a tree might not need to scroll 1377 * when expanded, this property may be ignored. 1378 * 1379 * @param scroll true, if expanded paths should be scrolled into view; 1380 * false, otherwise 1381 */ 1382 public void setScrollsOnExpand(boolean scroll) { 1383 renderer.setScrollsOnExpand(scroll); 1384 } 1385 1386 /** 1387 * Returns the value of the <code>scrollsOnExpand</code> property. 1388 * 1389 * @return the value of the <code>scrollsOnExpand</code> property 1390 */ 1391 public boolean getScrollsOnExpand() { 1392 return renderer.getScrollsOnExpand(); 1393 } 1394 1395 /** 1396 * Sets the value of the <code>showsRootHandles</code> property for the tree 1397 * part. This property specifies whether the node handles should be displayed. 1398 * If handles are not supported by a particular look and feel, this property 1399 * may be ignored. 1400 * 1401 * @param visible true, if root handles should be shown; false, otherwise 1402 */ 1403 public void setShowsRootHandles(boolean visible) { 1404 renderer.setShowsRootHandles(visible); 1405 repaint(); 1406 } 1407 1408 /** 1409 * Returns the value of the <code>showsRootHandles</code> property. 1410 * 1411 * @return the value of the <code>showsRootHandles</code> property 1412 */ 1413 public boolean getShowsRootHandles() { 1414 return renderer.getShowsRootHandles(); 1415 } 1416 1417 /** 1418 * Sets the value of the <code>expandsSelectedPaths</code> property for the tree 1419 * part. This property specifies whether the selected paths should be expanded. 1420 * 1421 * @param expand true, if selected paths should be expanded; false, otherwise 1422 */ 1423 public void setExpandsSelectedPaths(boolean expand) { 1424 renderer.setExpandsSelectedPaths(expand); 1425 } 1426 1427 /** 1428 * Returns the value of the <code>expandsSelectedPaths</code> property. 1429 * 1430 * @return the value of the <code>expandsSelectedPaths</code> property 1431 */ 1432 public boolean getExpandsSelectedPaths() { 1433 return renderer.getExpandsSelectedPaths(); 1434 } 1435 1436 1437 /** 1438 * Returns the number of mouse clicks needed to expand or close a node. 1439 * 1440 * @return number of mouse clicks before node is expanded 1441 */ 1442 public int getToggleClickCount() { 1443 return renderer.getToggleClickCount(); 1444 } 1445 1446 /** 1447 * Sets the number of mouse clicks before a node will expand or close. 1448 * The default is two. 1449 * 1450 * @param clickCount the number of clicks required to expand/collapse a node. 1451 */ 1452 public void setToggleClickCount(int clickCount) { 1453 renderer.setToggleClickCount(clickCount); 1454 } 1455 1456 /** 1457 * Returns true if the tree is configured for a large model. 1458 * The default value is false. 1459 * 1460 * @return true if a large model is suggested 1461 * @see #setLargeModel 1462 */ 1463 public boolean isLargeModel() { 1464 return renderer.isLargeModel(); 1465 } 1466 1467 /** 1468 * Specifies whether the UI should use a large model. 1469 * (Not all UIs will implement this.) <p> 1470 * 1471 * <strong>NOTE</strong>: this method is exposed for completeness - 1472 * currently it's not recommended 1473 * to use a large model because there are some issues 1474 * (not yet fully understood), namely 1475 * issue #25-swingx, and probably #270-swingx. 1476 * 1477 * @param newValue true to suggest a large model to the UI 1478 */ 1479 public void setLargeModel(boolean newValue) { 1480 renderer.setLargeModel(newValue); 1481 // JW: random method calling ... doesn't help 1482 // renderer.treeDidChange(); 1483 // revalidate(); 1484 // repaint(); 1485 } 1486 1487 //------------------------------ exposed tree listeners 1488 1489 /** 1490 * Adds a listener for <code>TreeExpansion</code> events. 1491 * 1492 * TODO (JW): redirect event source to this. 1493 * 1494 * @param tel a TreeExpansionListener that will be notified 1495 * when a tree node is expanded or collapsed 1496 */ 1497 public void addTreeExpansionListener(TreeExpansionListener tel) { 1498 renderer.addTreeExpansionListener(tel); 1499 } 1500 1501 /** 1502 * Removes a listener for <code>TreeExpansion</code> events. 1503 * @param tel the <code>TreeExpansionListener</code> to remove 1504 */ 1505 public void removeTreeExpansionListener(TreeExpansionListener tel) { 1506 renderer.removeTreeExpansionListener(tel); 1507 } 1508 1509 /** 1510 * Adds a listener for <code>TreeSelection</code> events. 1511 * TODO (JW): redirect event source to this. 1512 * 1513 * @param tsl a TreeSelectionListener that will be notified 1514 * when a tree node is selected or deselected 1515 */ 1516 public void addTreeSelectionListener(TreeSelectionListener tsl) { 1517 renderer.addTreeSelectionListener(tsl); 1518 } 1519 1520 /** 1521 * Removes a listener for <code>TreeSelection</code> events. 1522 * @param tsl the <code>TreeSelectionListener</code> to remove 1523 */ 1524 public void removeTreeSelectionListener(TreeSelectionListener tsl) { 1525 renderer.removeTreeSelectionListener(tsl); 1526 } 1527 1528 /** 1529 * Adds a listener for <code>TreeWillExpand</code> events. 1530 * TODO (JW): redirect event source to this. 1531 * 1532 * @param tel a TreeWillExpandListener that will be notified 1533 * when a tree node will be expanded or collapsed 1534 */ 1535 public void addTreeWillExpandListener(TreeWillExpandListener tel) { 1536 renderer.addTreeWillExpandListener(tel); 1537 } 1538 1539 /** 1540 * Removes a listener for <code>TreeWillExpand</code> events. 1541 * @param tel the <code>TreeWillExpandListener</code> to remove 1542 */ 1543 public void removeTreeWillExpandListener(TreeWillExpandListener tel) { 1544 renderer.removeTreeWillExpandListener(tel); 1545 } 1546 1547 1548 /** 1549 * Returns the selection model for the tree portion of the this treetable. 1550 * 1551 * @return selection model for the tree portion of the this treetable 1552 */ 1553 public TreeSelectionModel getTreeSelectionModel() { 1554 return renderer.getSelectionModel(); // RG: Fix JDNC issue 41 1555 } 1556 1557 /** 1558 * Overriden to invoke supers implementation, and then, 1559 * if the receiver is editing a Tree column, the editors bounds is 1560 * reset. The reason we have to do this is because JTable doesn't 1561 * think the table is being edited, as <code>getEditingRow</code> returns 1562 * -1, and therefore doesn't automaticly resize the editor for us. 1563 */ 1564 @Override 1565 public void sizeColumnsToFit(int resizingColumn) { 1566 /** TODO: Review wrt doLayout() */ 1567 super.sizeColumnsToFit(resizingColumn); 1568 // rg:changed 1569 if (getEditingColumn() != -1 && isHierarchical(editingColumn)) { 1570 Rectangle cellRect = getCellRect(realEditingRow(), 1571 getEditingColumn(), false); 1572 Component component = getEditorComponent(); 1573 component.setBounds(cellRect); 1574 component.validate(); 1575 } 1576 } 1577 1578 1579 /** 1580 * Determines if the specified column is defined as the hierarchical column. 1581 * 1582 * @param column 1583 * zero-based index of the column in view coordinates 1584 * @return true if the column is the hierarchical column; false otherwise. 1585 * @throws IllegalArgumentException 1586 * if the column is less than 0 or greater than or equal to the 1587 * column count 1588 */ 1589 public boolean isHierarchical(int column) { 1590 if (column < 0 || column >= getColumnCount()) { 1591 throw new IllegalArgumentException("column must be valid, was" + column); 1592 } 1593 1594 return (getHierarchicalColumn() == column); 1595 } 1596 1597 /** 1598 * Returns the index of the hierarchical column. This is the column that is 1599 * displayed as the tree. 1600 * 1601 * @return the index of the hierarchical column, -1 if there is 1602 * no hierarchical column 1603 * 1604 */ 1605 public int getHierarchicalColumn() { 1606 return convertColumnIndexToView(((TreeTableModel) renderer.getModel()).getHierarchicalColumn()); 1607 } 1608 1609 /** 1610 * {@inheritDoc} 1611 */ 1612 @Override 1613 public TableCellRenderer getCellRenderer(int row, int column) { 1614 if (isHierarchical(column)) { 1615 return renderer; 1616 } 1617 1618 return super.getCellRenderer(row, column); 1619 } 1620 1621 /** 1622 * {@inheritDoc} 1623 */ 1624 @Override 1625 public TableCellEditor getCellEditor(int row, int column) { 1626 if (isHierarchical(column)) { 1627 return hierarchicalEditor; 1628 } 1629 1630 return super.getCellEditor(row, column); 1631 } 1632 1633 1634 @Override 1635 public void updateUI() { 1636 super.updateUI(); 1637 updateHierarchicalRendererEditor(); 1638 } 1639 1640 /** 1641 * Updates Ui of renderer/editor for the hierarchical column. Need to do so 1642 * manually, as not accessible by the default lookup. 1643 */ 1644 protected void updateHierarchicalRendererEditor() { 1645 if (renderer != null) { 1646 SwingUtilities.updateComponentTreeUI(renderer); 1647 } 1648 } 1649 1650 /** 1651 * {@inheritDoc} <p> 1652 * 1653 * Overridden to message the tree directly if the column is the view index of 1654 * the hierarchical column. <p> 1655 * 1656 * PENDING JW: revisit once we switch to really using a table renderer. As is, it's 1657 * a quick fix for #821-swingx: string rep for hierarchical column incorrect. 1658 */ 1659 @Override 1660 public String getStringAt(int row, int column) { 1661 if (isHierarchical(column)) { 1662 return getHierarchicalStringAt(row); 1663 } 1664 return super.getStringAt(row, column); 1665 } 1666 1667 /** 1668 * Returns the String representation of the hierarchical column at the given 1669 * row. <p> 1670 * 1671 * @param row the row index in view coordinates 1672 * @return the string representation of the hierarchical column at the given row. 1673 * 1674 * @see #getStringAt(int, int) 1675 */ 1676 private String getHierarchicalStringAt(int row) { 1677 return renderer.getStringAt(row); 1678 } 1679 1680 /** 1681 * ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel 1682 * to listen for changes in the ListSelectionModel it maintains. Once 1683 * a change in the ListSelectionModel happens, the paths are updated 1684 * in the DefaultTreeSelectionModel. 1685 */ 1686 class ListToTreeSelectionModelWrapper extends DefaultTreeSelectionModel { 1687 /** Set to true when we are updating the ListSelectionModel. */ 1688 protected boolean updatingListSelectionModel; 1689 1690 public ListToTreeSelectionModelWrapper() { 1691 super(); 1692 getListSelectionModel().addListSelectionListener 1693 (createListSelectionListener()); 1694 } 1695 1696 /** 1697 * Returns the list selection model. ListToTreeSelectionModelWrapper 1698 * listens for changes to this model and updates the selected paths 1699 * accordingly. 1700 */ 1701 ListSelectionModel getListSelectionModel() { 1702 return listSelectionModel; 1703 } 1704 1705 /** 1706 * This is overridden to set <code>updatingListSelectionModel</code> 1707 * and message super. This is the only place DefaultTreeSelectionModel 1708 * alters the ListSelectionModel. 1709 */ 1710 @Override 1711 public void resetRowSelection() { 1712 if (!updatingListSelectionModel) { 1713 updatingListSelectionModel = true; 1714 try { 1715 super.resetRowSelection(); 1716 } 1717 finally { 1718 updatingListSelectionModel = false; 1719 } 1720 } 1721 // Notice how we don't message super if 1722 // updatingListSelectionModel is true. If 1723 // updatingListSelectionModel is true, it implies the 1724 // ListSelectionModel has already been updated and the 1725 // paths are the only thing that needs to be updated. 1726 } 1727 1728 /** 1729 * Creates and returns an instance of ListSelectionHandler. 1730 */ 1731 protected ListSelectionListener createListSelectionListener() { 1732 return new ListSelectionHandler(); 1733 } 1734 1735 /** 1736 * If <code>updatingListSelectionModel</code> is false, this will 1737 * reset the selected paths from the selected rows in the list 1738 * selection model. 1739 */ 1740 protected void updateSelectedPathsFromSelectedRows() { 1741 if (!updatingListSelectionModel) { 1742 updatingListSelectionModel = true; 1743 try { 1744 if (listSelectionModel.isSelectionEmpty()) { 1745 clearSelection(); 1746 } else { 1747 // This is way expensive, ListSelectionModel needs an 1748 // enumerator for iterating. 1749 int min = listSelectionModel.getMinSelectionIndex(); 1750 int max = listSelectionModel.getMaxSelectionIndex(); 1751 1752 List<TreePath> paths = new ArrayList<TreePath>(); 1753 for (int counter = min; counter <= max; counter++) { 1754 if (listSelectionModel.isSelectedIndex(counter)) { 1755 TreePath selPath = renderer.getPathForRow( 1756 counter); 1757 1758 if (selPath != null) { 1759 paths.add(selPath); 1760 } 1761 } 1762 } 1763 setSelectionPaths(paths.toArray(new TreePath[paths.size()])); 1764 // need to force here: usually the leadRow is adjusted 1765 // in resetRowSelection which is disabled during this method 1766 leadRow = leadIndex; 1767 } 1768 } 1769 finally { 1770 updatingListSelectionModel = false; 1771 } 1772 } 1773 } 1774 1775 /** 1776 * Class responsible for calling updateSelectedPathsFromSelectedRows 1777 * when the selection of the list changse. 1778 */ 1779 class ListSelectionHandler implements ListSelectionListener { 1780 public void valueChanged(ListSelectionEvent e) { 1781 if (!e.getValueIsAdjusting()) { 1782 updateSelectedPathsFromSelectedRows(); 1783 } 1784 } 1785 } 1786 } 1787 1788 /** 1789 * 1790 */ 1791 protected static class TreeTableModelAdapter extends AbstractTableModel { 1792 private TreeModelListener treeModelListener; 1793 1794 /** 1795 * Maintains a TreeTableModel and a JTree as purely implementation details. 1796 * Developers can plug in any type of custom TreeTableModel through a 1797 * JXTreeTable constructor or through setTreeTableModel(). 1798 * 1799 * @param model Underlying data model for the JXTreeTable that will ultimately 1800 * be bound to this TreeTableModelAdapter 1801 * @param tree TreeTableCellRenderer instantiated with the same model as 1802 * specified by the model parameter of this constructor 1803 * @throws IllegalArgumentException if a null model argument is passed 1804 * @throws IllegalArgumentException if a null tree argument is passed 1805 */ 1806 TreeTableModelAdapter(JTree tree) { 1807 assert tree != null; 1808 1809 this.tree = tree; // need tree to implement getRowCount() 1810 tree.getModel().addTreeModelListener(getTreeModelListener()); 1811 tree.addTreeExpansionListener(new TreeExpansionListener() { 1812 // Don't use fireTableRowsInserted() here; the selection model 1813 // would get updated twice. 1814 public void treeExpanded(TreeExpansionEvent event) { 1815 updateAfterExpansionEvent(event); 1816 } 1817 1818 public void treeCollapsed(TreeExpansionEvent event) { 1819 updateAfterExpansionEvent(event); 1820 } 1821 }); 1822 tree.addPropertyChangeListener("model", new PropertyChangeListener() { 1823 public void propertyChange(PropertyChangeEvent evt) { 1824 TreeTableModel model = (TreeTableModel) evt.getOldValue(); 1825 model.removeTreeModelListener(getTreeModelListener()); 1826 1827 model = (TreeTableModel) evt.getNewValue(); 1828 model.addTreeModelListener(getTreeModelListener()); 1829 1830 fireTableStructureChanged(); 1831 } 1832 }); 1833 } 1834 1835 /** 1836 * updates the table after having received an TreeExpansionEvent.<p> 1837 * 1838 * @param event the TreeExpansionEvent which triggered the method call. 1839 */ 1840 protected void updateAfterExpansionEvent(TreeExpansionEvent event) { 1841 // moved to let the renderer handle directly 1842 // treeTable.getTreeTableHacker().setExpansionChangedFlag(); 1843 // JW: delayed fire leads to a certain sluggishness occasionally? 1844 fireTableDataChanged(); 1845 } 1846 1847 /** 1848 * Returns the JXTreeTable instance to which this TreeTableModelAdapter is 1849 * permanently and exclusively bound. For use by 1850 * {@link org.jdesktop.swingx.JXTreeTable#setModel(javax.swing.table.TableModel)}. 1851 * 1852 * @return JXTreeTable to which this TreeTableModelAdapter is permanently bound 1853 */ 1854 protected JXTreeTable getTreeTable() { 1855 return treeTable; 1856 } 1857 1858 /** 1859 * Immutably binds this TreeTableModelAdapter to the specified JXTreeTable. 1860 * 1861 * @param treeTable the JXTreeTable instance that this adapter is bound to. 1862 */ 1863 protected final void bind(JXTreeTable treeTable) { 1864 // Suppress potentially subversive invocation! 1865 // Prevent clearing out the deck for possible hijack attempt later! 1866 if (treeTable == null) { 1867 throw new IllegalArgumentException("null treeTable"); 1868 } 1869 1870 if (this.treeTable == null) { 1871 this.treeTable = treeTable; 1872 } 1873 else { 1874 throw new IllegalArgumentException("adapter already bound"); 1875 } 1876 } 1877 1878 // Wrappers, implementing TableModel interface. 1879 // TableModelListener management provided by AbstractTableModel superclass. 1880 1881 @Override 1882 public Class<?> getColumnClass(int column) { 1883 return ((TreeTableModel) tree.getModel()).getColumnClass(column); 1884 } 1885 1886 public int getColumnCount() { 1887 return ((TreeTableModel) tree.getModel()).getColumnCount(); 1888 } 1889 1890 @Override 1891 public String getColumnName(int column) { 1892 return ((TreeTableModel) tree.getModel()).getColumnName(column); 1893 } 1894 1895 public int getRowCount() { 1896 return tree.getRowCount(); 1897 } 1898 1899 public Object getValueAt(int row, int column) { 1900 // Issue #270-swingx: guard against invisible row 1901 Object node = nodeForRow(row); 1902 return node != null ? ((TreeTableModel) tree.getModel()).getValueAt(node, column) : null; 1903 } 1904 1905 @Override 1906 public boolean isCellEditable(int row, int column) { 1907 // Issue #270-swingx: guard against invisible row 1908 Object node = nodeForRow(row); 1909 return node != null ? ((TreeTableModel) tree.getModel()).isCellEditable(node, column) : false; 1910 } 1911 1912 @Override 1913 public void setValueAt(Object value, int row, int column) { 1914 // Issue #270-swingx: guard against invisible row 1915 Object node = nodeForRow(row); 1916 if (node != null) { 1917 ((TreeTableModel) tree.getModel()).setValueAt(value, node, column); 1918 } 1919 } 1920 1921 protected Object nodeForRow(int row) { 1922 // Issue #270-swingx: guard against invisible row 1923 TreePath path = tree.getPathForRow(row); 1924 return path != null ? path.getLastPathComponent() : null; 1925 } 1926 1927 /** 1928 * @return <code>TreeModelListener</code> 1929 */ 1930 private TreeModelListener getTreeModelListener() { 1931 if (treeModelListener == null) { 1932 treeModelListener = new TreeModelListener() { 1933 1934 public void treeNodesChanged(TreeModelEvent e) { 1935 // LOG.info("got tree event: changed " + e); 1936 delayedFireTableDataUpdated(e); 1937 } 1938 1939 // We use delayedFireTableDataChanged as we can 1940 // not be guaranteed the tree will have finished processing 1941 // the event before us. 1942 public void treeNodesInserted(TreeModelEvent e) { 1943 delayedFireTableDataChanged(e, 1); 1944 } 1945 1946 public void treeNodesRemoved(TreeModelEvent e) { 1947 // LOG.info("got tree event: removed " + e); 1948 delayedFireTableDataChanged(e, 2); 1949 } 1950 1951 public void treeStructureChanged(TreeModelEvent e) { 1952 // ?? should be mapped to structureChanged -- JW 1953 if (isTableStructureChanged(e)) { 1954 delayedFireTableStructureChanged(); 1955 } else { 1956 delayedFireTableDataChanged(); 1957 } 1958 } 1959 }; 1960 } 1961 1962 return treeModelListener; 1963 } 1964 1965 /** 1966 * Decides if the given treeModel structureChanged should 1967 * trigger a table structureChanged. Returns true if the 1968 * source path is the root or null, false otherwise.<p> 1969 * 1970 * PENDING: need to refine? "Marker" in Event-Object? 1971 * 1972 * @param e the TreeModelEvent received in the treeModelListener's 1973 * treeStructureChanged 1974 * @return a boolean indicating whether the given TreeModelEvent 1975 * should trigger a structureChanged. 1976 */ 1977 private boolean isTableStructureChanged(TreeModelEvent e) { 1978 if ((e.getTreePath() == null) || 1979 (e.getTreePath().getParentPath() == null)) return true; 1980 return false; 1981 } 1982 1983 /** 1984 * Invokes fireTableDataChanged after all the pending events have been 1985 * processed. SwingUtilities.invokeLater is used to handle this. 1986 */ 1987 private void delayedFireTableStructureChanged() { 1988 SwingUtilities.invokeLater(new Runnable() { 1989 public void run() { 1990 fireTableStructureChanged(); 1991 } 1992 }); 1993 } 1994 1995 /** 1996 * Invokes fireTableDataChanged after all the pending events have been 1997 * processed. SwingUtilities.invokeLater is used to handle this. 1998 */ 1999 private void delayedFireTableDataChanged() { 2000 SwingUtilities.invokeLater(new Runnable() { 2001 public void run() { 2002 fireTableDataChanged(); 2003 } 2004 }); 2005 } 2006 2007 /** 2008 * Invokes fireTableDataChanged after all the pending events have been 2009 * processed. SwingUtilities.invokeLater is used to handle this. 2010 * Allowed event types: 1 for insert, 2 for delete 2011 */ 2012 private void delayedFireTableDataChanged(final TreeModelEvent tme, final int typeChange) { 2013 if ((typeChange < 1 ) || (typeChange > 2)) 2014 throw new IllegalArgumentException("Event type must be 1 or 2, was " + typeChange); 2015 // expansion state before invoke may be different 2016 // from expansion state in invoke 2017 final boolean expanded = tree.isExpanded(tme.getTreePath()); 2018 // quick test if tree throws for unrelated path. Seems like not. 2019 // tree.getRowForPath(new TreePath("dummy")); 2020 SwingUtilities.invokeLater(new Runnable() { 2021 public void run() { 2022 int indices[] = tme.getChildIndices(); 2023 TreePath path = tme.getTreePath(); 2024 // quick test to see if bailing out is an option 2025 // if (false) { 2026 if (indices != null) { 2027 if (expanded) { // Dont bother to update if the parent 2028 // node is collapsed 2029 // indices must in ascending order, as per TreeEvent/Listener doc 2030 int min = indices[0]; 2031 int max = indices[indices.length - 1]; 2032 int startingRow = tree.getRowForPath(path) + 1; 2033 min = startingRow + min; 2034 max = startingRow + max; 2035 switch (typeChange) { 2036 case 1: 2037 // LOG.info("rows inserted: path " + path + "/" + min + "/" 2038 // + max); 2039 fireTableRowsInserted(min, max); 2040 break; 2041 case 2: 2042 // LOG.info("rows deleted path " + path + "/" + min + "/" 2043 // + max); 2044 fireTableRowsDeleted(min, max); 2045 break; 2046 } 2047 } else { 2048 // not expanded - but change might effect appearance 2049 // of parent 2050 // Issue #82-swingx 2051 int row = tree.getRowForPath(path); 2052 // fix Issue #247-swingx: prevent accidental 2053 // structureChanged 2054 // for collapsed path 2055 // in this case row == -1, which == 2056 // TableEvent.HEADER_ROW 2057 if (row >= 0) 2058 fireTableRowsUpdated(row, row); 2059 } 2060 } else { // case where the event is fired to identify 2061 // root. 2062 fireTableDataChanged(); 2063 } 2064 } 2065 }); 2066 } 2067 2068 /** 2069 * This is used for updated only. PENDING: not necessary to delay? 2070 * Updates are never structural changes which are the critical. 2071 * 2072 * @param tme 2073 */ 2074 protected void delayedFireTableDataUpdated(final TreeModelEvent tme) { 2075 final boolean expanded = tree.isExpanded(tme.getTreePath()); 2076 SwingUtilities.invokeLater(new Runnable() { 2077 public void run() { 2078 int indices[] = tme.getChildIndices(); 2079 TreePath path = tme.getTreePath(); 2080 if (indices != null) { 2081 if (expanded) { // Dont bother to update if the parent 2082 // node is collapsed 2083 Object children[] = tme.getChildren(); 2084 // can we be sure that children.length > 0? 2085 // int min = tree.getRowForPath(path.pathByAddingChild(children[0])); 2086 // int max = tree.getRowForPath(path.pathByAddingChild(children[children.length -1])); 2087 int min = Integer.MAX_VALUE; 2088 int max = Integer.MIN_VALUE; 2089 for (int i = 0; i < indices.length; i++) { 2090 Object child = children[i]; 2091 TreePath childPath = path 2092 .pathByAddingChild(child); 2093 int index = tree.getRowForPath(childPath); 2094 if (index < min) { 2095 min = index; 2096 } 2097 if (index > max) { 2098 max = index; 2099 } 2100 } 2101 // LOG.info("Updated: parentPath/min/max" + path + "/" + min + "/" + max); 2102 // JW: the index is occasionally - 1 - need further digging 2103 fireTableRowsUpdated(Math.max(0, min), Math.max(0, max)); 2104 } else { 2105 // not expanded - but change might effect appearance 2106 // of parent Issue #82-swingx 2107 int row = tree.getRowForPath(path); 2108 // fix Issue #247-swingx: prevent accidental structureChanged 2109 // for collapsed path in this case row == -1, 2110 // which == TableEvent.HEADER_ROW 2111 if (row >= 0) 2112 fireTableRowsUpdated(row, row); 2113 } 2114 } else { // case where the event is fired to identify 2115 // root. 2116 fireTableDataChanged(); 2117 } 2118 } 2119 }); 2120 2121 } 2122 2123 private final JTree tree; // immutable 2124 private JXTreeTable treeTable = null; // logically immutable 2125 } 2126 2127 static class TreeTableCellRenderer extends JXTree implements 2128 TableCellRenderer 2129 // need to implement RolloverRenderer 2130 // PENDING JW: method name clash rolloverRenderer.isEnabled and 2131 // component.isEnabled .. don't extend, use? And change 2132 // the method name in rolloverRenderer? 2133 // commented - so doesn't show the rollover cursor. 2134 // 2135 // , RolloverRenderer 2136 { 2137 private PropertyChangeListener rolloverListener; 2138 2139 // Force user to specify TreeTableModel instead of more general 2140 // TreeModel 2141 public TreeTableCellRenderer(TreeTableModel model) { 2142 super(model); 2143 putClientProperty("JTree.lineStyle", "None"); 2144 setRootVisible(false); // superclass default is "true" 2145 setShowsRootHandles(true); // superclass default is "false" 2146 /** 2147 * TODO: Support truncated text directly in 2148 * DefaultTreeCellRenderer. 2149 */ 2150 // removed as fix for #769-swingx: defaults for treetable should be same as tree 2151 // setOverwriteRendererIcons(true); 2152 // setCellRenderer(new DefaultTreeRenderer()); 2153 setCellRenderer(new ClippedTreeCellRenderer()); 2154 } 2155 2156 2157 /** 2158 * {@inheritDoc} <p> 2159 * 2160 * Overridden to hack around #766-swingx: cursor flickering in DnD 2161 * when dragging over tree column. This is a core bug (#6700748) related 2162 * to painting the rendering component on a CellRendererPane. A trick 2163 * around is to let this return false. <p> 2164 * 2165 * This implementation applies the trick, that is returns false always. 2166 * The hack can be disabled by setting the treeTable's client property 2167 * DROP_HACK_FLAG_KEY to Boolean.FALSE. 2168 * 2169 */ 2170 @Override 2171 public boolean isVisible() { 2172 return shouldApplyDropHack() ? false : super.isVisible(); 2173 } 2174 2175 2176 /** 2177 * Returns a boolean indicating whether the drop hack should be applied. 2178 * 2179 * @return a boolean indicating whether the drop hack should be applied. 2180 */ 2181 protected boolean shouldApplyDropHack() { 2182 return !Boolean.FALSE.equals(treeTable.getClientProperty(DROP_HACK_FLAG_KEY)); 2183 } 2184 2185 2186 /** 2187 * Hack around #297-swingx: tooltips shown at wrong row. 2188 * 2189 * The problem is that - due to much tricksery when rendering the tree - 2190 * the given coordinates are rather useless. As a consequence, super 2191 * maps to wrong coordinates. This takes over completely. 2192 * 2193 * PENDING: bidi? 2194 * 2195 * @param event the mouseEvent in treetable coordinates 2196 * @param row the view row index 2197 * @param column the view column index 2198 * @return the tooltip as appropriate for the given row 2199 */ 2200 private String getToolTipText(MouseEvent event, int row, int column) { 2201 if (row < 0) return null; 2202 String toolTip = null; 2203 TreeCellRenderer renderer = getCellRenderer(); 2204 TreePath path = getPathForRow(row); 2205 Object lastPath = path.getLastPathComponent(); 2206 Component rComponent = renderer.getTreeCellRendererComponent 2207 (this, lastPath, isRowSelected(row), 2208 isExpanded(row), getModel().isLeaf(lastPath), row, 2209 true); 2210 2211 if(rComponent instanceof JComponent) { 2212 Rectangle pathBounds = getPathBounds(path); 2213 Rectangle cellRect = treeTable.getCellRect(row, column, false); 2214 // JW: what we are after 2215 // is the offset into the hierarchical column 2216 // then intersect this with the pathbounds 2217 Point mousePoint = event.getPoint(); 2218 // translate to coordinates relative to cell 2219 mousePoint.translate(-cellRect.x, -cellRect.y); 2220 // translate horizontally to 2221 mousePoint.translate(-pathBounds.x, 0); 2222 // show tooltip only if over renderer? 2223 // if (mousePoint.x < 0) return null; 2224 // p.translate(-pathBounds.x, -pathBounds.y); 2225 MouseEvent newEvent = new MouseEvent(rComponent, event.getID(), 2226 event.getWhen(), 2227 event.getModifiers(), 2228 mousePoint.x, 2229 mousePoint.y, 2230 // p.x, p.y, 2231 event.getClickCount(), 2232 event.isPopupTrigger()); 2233 2234 toolTip = ((JComponent)rComponent).getToolTipText(newEvent); 2235 } 2236 if (toolTip != null) { 2237 return toolTip; 2238 } 2239 return getToolTipText(); 2240 } 2241 2242 /** 2243 * {@inheritDoc} <p> 2244 * 2245 * Overridden to not automatically de/register itself from/to the ToolTipManager. 2246 * As rendering component it is not considered to be active in any way, so the 2247 * manager must not listen. 2248 */ 2249 @Override 2250 public void setToolTipText(String text) { 2251 putClientProperty(TOOL_TIP_TEXT_KEY, text); 2252 } 2253 2254 /** 2255 * Immutably binds this TreeTableModelAdapter to the specified JXTreeTable. 2256 * For internal use by JXTreeTable only. 2257 * 2258 * @param treeTable the JXTreeTable instance that this renderer is bound to 2259 */ 2260 public final void bind(JXTreeTable treeTable) { 2261 // Suppress potentially subversive invocation! 2262 // Prevent clearing out the deck for possible hijack attempt later! 2263 if (treeTable == null) { 2264 throw new IllegalArgumentException("null treeTable"); 2265 } 2266 2267 if (this.treeTable == null) { 2268 this.treeTable = treeTable; 2269 // commented because still has issus 2270 // bindRollover(); 2271 } 2272 else { 2273 throw new IllegalArgumentException("renderer already bound"); 2274 } 2275 } 2276 2277 /** 2278 * Install rollover support. 2279 * Not used - still has issues. 2280 * - not bidi-compliant 2281 * - no coordinate transformation for hierarchical column != 0 2282 * - method name clash enabled 2283 * - keyboard triggered click unreliable (triggers the treetable) 2284 * ... 2285 */ 2286 @SuppressWarnings("unused") 2287 private void bindRollover() { 2288 setRolloverEnabled(treeTable.isRolloverEnabled()); 2289 treeTable.addPropertyChangeListener(getRolloverListener()); 2290 } 2291 2292 2293 /** 2294 * @return 2295 */ 2296 private PropertyChangeListener getRolloverListener() { 2297 if (rolloverListener == null) { 2298 rolloverListener = createRolloverListener(); 2299 } 2300 return rolloverListener; 2301 } 2302 2303 /** 2304 * Creates and returns a property change listener for 2305 * table's rollover related properties. 2306 * 2307 * This implementation 2308 * - Synchs the tree's rolloverEnabled 2309 * - maps rollover cell from the table to the cell 2310 * (still incomplete: first column only) 2311 * 2312 * @return 2313 */ 2314 protected PropertyChangeListener createRolloverListener() { 2315 PropertyChangeListener l = new PropertyChangeListener() { 2316 2317 public void propertyChange(PropertyChangeEvent evt) { 2318 if ((treeTable == null) || (treeTable != evt.getSource())) 2319 return; 2320 if ("rolloverEnabled".equals(evt.getPropertyName())) { 2321 setRolloverEnabled(((Boolean) evt.getNewValue()).booleanValue()); 2322 } 2323 if (RolloverProducer.ROLLOVER_KEY.equals(evt.getPropertyName())){ 2324 rollover(evt); 2325 } 2326 } 2327 2328 private void rollover(PropertyChangeEvent evt) { 2329 boolean isHierarchical = isHierarchical((Point)evt.getNewValue()); 2330 putClientProperty(evt.getPropertyName(), isHierarchical ? 2331 new Point((Point) evt.getNewValue()) : null); 2332 } 2333 2334 private boolean isHierarchical(Point point) { 2335 if (point != null) { 2336 int column = point.x; 2337 if (column >= 0) { 2338 return treeTable.isHierarchical(column); 2339 } 2340 } 2341 return false; 2342 } 2343 @SuppressWarnings("unused") 2344 Point rollover = new Point(-1, -1); 2345 }; 2346 return l; 2347 } 2348 2349 /** 2350 * {@inheritDoc} <p> 2351 * 2352 * Overridden to produce clicked client props only. The 2353 * rollover are produced by a propertyChangeListener to 2354 * the table's corresponding prop. 2355 * 2356 */ 2357 @Override 2358 protected RolloverProducer createRolloverProducer() { 2359 return new RolloverProducer() { 2360 2361 /** 2362 * Overridden to do nothing. 2363 * 2364 * @param e 2365 * @param property 2366 */ 2367 @Override 2368 protected void updateRollover(MouseEvent e, String property, boolean fireAlways) { 2369 if (CLICKED_KEY.equals(property)) { 2370 super.updateRollover(e, property, fireAlways); 2371 } 2372 } 2373 @Override 2374 protected void updateRolloverPoint(JComponent component, 2375 Point mousePoint) { 2376 JXTree tree = (JXTree) component; 2377 int row = tree.getClosestRowForLocation(mousePoint.x, mousePoint.y); 2378 Rectangle bounds = tree.getRowBounds(row); 2379 if (bounds == null) { 2380 row = -1; 2381 } else { 2382 if ((bounds.y + bounds.height < mousePoint.y) || 2383 bounds.x > mousePoint.x) { 2384 row = -1; 2385 } 2386 } 2387 int col = row < 0 ? -1 : 0; 2388 rollover.x = col; 2389 rollover.y = row; 2390 } 2391 2392 }; 2393 } 2394 2395 2396 @Override 2397 public void scrollRectToVisible(Rectangle aRect) { 2398 treeTable.scrollRectToVisible(aRect); 2399 } 2400 2401 @Override 2402 protected void setExpandedState(TreePath path, boolean state) { 2403 treeTable.getTreeTableHacker().completeEditing(); 2404 super.setExpandedState(path, state); 2405 treeTable.getTreeTableHacker().expansionChanged(); 2406 2407 } 2408 2409 /** 2410 * updateUI is overridden to set the colors of the Tree's renderer 2411 * to match that of the table. 2412 */ 2413 @Override 2414 public void updateUI() { 2415 super.updateUI(); 2416 // Make the tree's cell renderer use the table's cell selection 2417 // colors. 2418 // TODO JW: need to revisit... 2419 // a) the "real" of a JXTree is always wrapped into a DelegatingRenderer 2420 // consequently the if-block never executes 2421 // b) even if it does it probably (?) should not 2422 // unconditionally overwrite custom selection colors. 2423 // Check for UIResources instead. 2424 TreeCellRenderer tcr = getCellRenderer(); 2425 if (tcr instanceof DefaultTreeCellRenderer) { 2426 DefaultTreeCellRenderer dtcr = ((DefaultTreeCellRenderer) tcr); 2427 // For 1.1 uncomment this, 1.2 has a bug that will cause an 2428 // exception to be thrown if the border selection color is null. 2429 dtcr.setBorderSelectionColor(null); 2430 dtcr.setTextSelectionColor( 2431 UIManager.getColor("Table.selectionForeground")); 2432 dtcr.setBackgroundSelectionColor( 2433 UIManager.getColor("Table.selectionBackground")); 2434 } 2435 } 2436 2437 /** 2438 * Sets the row height of the tree, and forwards the row height to 2439 * the table. 2440 * 2441 * 2442 */ 2443 @Override 2444 public void setRowHeight(int rowHeight) { 2445 // JW: can't ... updateUI invoked with rowHeight = 0 2446 // hmmm... looks fishy ... 2447 // if (rowHeight <= 0) throw 2448 // new IllegalArgumentException("the rendering tree must have a fixed rowHeight > 0"); 2449 super.setRowHeight(rowHeight); 2450 if (rowHeight > 0) { 2451 if (treeTable != null) { 2452 treeTable.adjustTableRowHeight(rowHeight); 2453 } 2454 } 2455 } 2456 2457 2458 /** 2459 * This is overridden to set the height to match that of the JTable. 2460 */ 2461 @Override 2462 public void setBounds(int x, int y, int w, int h) { 2463 if (treeTable != null) { 2464 y = 0; 2465 // It is not enough to set the height to treeTable.getHeight() 2466 h = treeTable.getRowCount() * this.getRowHeight(); 2467 // int hierarchicalC = treeTable.getHierarchicalColumn(); 2468 // if (hierarchicalC >= 0) { 2469 // TableColumn column = treeTable.getColumn(hierarchicalC); 2470 // w = Math.min(w, column.getWidth()); 2471 // } 2472 } 2473 super.setBounds(x, y, w, h); 2474 } 2475 2476 /** 2477 * Sublcassed to translate the graphics such that the last visible row 2478 * will be drawn at 0,0. 2479 */ 2480 @Override 2481 public void paint(Graphics g) { 2482 Rectangle cellRect = treeTable.getCellRect(visibleRow, 0, false); 2483 g.translate(0, -cellRect.y); 2484 2485 hierarchicalColumnWidth = getWidth(); 2486 super.paint(g); 2487 2488 // Draw the Table border if we have focus. 2489 if (highlightBorder != null) { 2490 // #170: border not drawn correctly 2491 // JW: position the border to be drawn in translated area 2492 // still not satifying in all cases... 2493 // RG: Now it satisfies (at least for the row margins) 2494 // Still need to make similar adjustments for column margins... 2495 highlightBorder.paintBorder(this, g, 0, cellRect.y, 2496 getWidth(), cellRect.height); 2497 } 2498 } 2499 2500 public void doClick() { 2501 if ((getCellRenderer() instanceof RolloverRenderer) 2502 && ((RolloverRenderer) getCellRenderer()).isEnabled()) { 2503 ((RolloverRenderer) getCellRenderer()).doClick(); 2504 } 2505 2506 } 2507 2508 public Component getTableCellRendererComponent(JTable table, 2509 Object value, 2510 boolean isSelected, boolean hasFocus, int row, int column) { 2511 assert table == treeTable; 2512 // JW: quick fix for the tooltip part of #794-swingx: 2513 // visual properties must be reset in each cycle. 2514 // reverted - otherwise tooltip per Highlighter doesn't work 2515 // 2516 // setToolTipText(null); 2517 2518 if (isSelected) { 2519 setBackground(table.getSelectionBackground()); 2520 setForeground(table.getSelectionForeground()); 2521 } 2522 else { 2523 setBackground(table.getBackground()); 2524 setForeground(table.getForeground()); 2525 } 2526 2527 highlightBorder = null; 2528 if (treeTable != null) { 2529 if (treeTable.realEditingRow() == row && 2530 treeTable.getEditingColumn() == column) { 2531 } 2532 else if (hasFocus) { 2533 highlightBorder = UIManager.getBorder( 2534 "Table.focusCellHighlightBorder"); 2535 } 2536 } 2537 2538 visibleRow = row; 2539 2540 return this; 2541 } 2542 2543 private class ClippedTreeCellRenderer extends DefaultXTreeCellRenderer 2544 implements StringValue 2545 { 2546 @SuppressWarnings("unused") 2547 private boolean inpainting; 2548 private String shortText; 2549 @Override 2550 public void paint(Graphics g) { 2551 String fullText = super.getText(); 2552 2553 shortText = SwingUtilities.layoutCompoundLabel( 2554 this, g.getFontMetrics(), fullText, getIcon(), 2555 getVerticalAlignment(), getHorizontalAlignment(), 2556 getVerticalTextPosition(), getHorizontalTextPosition(), 2557 getItemRect(itemRect), iconRect, textRect, 2558 getIconTextGap()); 2559 2560 /** TODO: setText is more heavyweight than we want in this 2561 * situation. Make JLabel.text protected instead of private. 2562 */ 2563 2564 try { 2565 inpainting = true; 2566 // TODO JW: don't - override getText to return the short version 2567 // during painting 2568 setText(shortText); // temporarily truncate text 2569 super.paint(g); 2570 } finally { 2571 inpainting = false; 2572 setText(fullText); // restore full text 2573 } 2574 } 2575 2576 2577 private Rectangle getItemRect(Rectangle itemRect) { 2578 getBounds(itemRect); 2579 // LOG.info("rect" + itemRect); 2580 itemRect.width = hierarchicalColumnWidth - itemRect.x; 2581 return itemRect; 2582 } 2583 2584 @Override 2585 public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf, int row, boolean hasFocus) { 2586 return super.getTreeCellRendererComponent(tree, getHierarchicalTableValue(value), sel, expanded, leaf, 2587 row, hasFocus); 2588 } 2589 2590 2591 /** 2592 * 2593 * @param node the node in the treeModel as passed into the TreeCellRenderer 2594 * @return the corresponding value of the hierarchical cell in the TreeTableModel 2595 */ 2596 private Object getHierarchicalTableValue(Object node) { 2597 Object val = node; 2598 2599 if (treeTable != null) { 2600 int treeColumn = treeTable.getTreeTableModel().getHierarchicalColumn(); 2601 Object o = null; 2602 if (treeColumn >= 0) { 2603 // following is unreliable during a paint cycle 2604 // somehow interferes with BasicTreeUIs painting cache 2605 // o = treeTable.getValueAt(row, treeColumn); 2606 // ask the model - that's always okay 2607 // might blow if the TreeTableModel is strict in 2608 // checking the containment of the value and 2609 // this renderer is called for sizing with a prototype 2610 o = treeTable.getTreeTableModel().getValueAt(node, treeColumn); 2611 } 2612 val = o; 2613 } 2614 return val; 2615 } 2616 2617 /** 2618 * {@inheritDoc} <p> 2619 */ 2620 public String getString(Object node) { 2621 // int treeColumn = treeTable.getTreeTableModel().getHierarchicalColumn(); 2622 // if (treeColumn >= 0) { 2623 // return StringValues.TO_STRING.getString(treeTable.getTreeTableModel().getValueAt(value, treeColumn)); 2624 // } 2625 return StringValues.TO_STRING.getString(getHierarchicalTableValue(node)); 2626 } 2627 2628 // Rectangles filled in by SwingUtilities.layoutCompoundLabel(); 2629 private final Rectangle iconRect = new Rectangle(); 2630 private final Rectangle textRect = new Rectangle(); 2631 // Rectangle filled in by this.getItemRect(); 2632 private final Rectangle itemRect = new Rectangle(); 2633 } 2634 2635 /** Border to draw around the tree, if this is non-null, it will 2636 * be painted. */ 2637 protected Border highlightBorder = null; 2638 protected JXTreeTable treeTable = null; 2639 protected int visibleRow = 0; 2640 2641 // A JXTreeTable may not have more than one hierarchical column 2642 private int hierarchicalColumnWidth = 0; 2643 2644 } 2645 2646 /** 2647 * Returns the adapter that knows how to access the component data model. 2648 * The component data adapter is used by filters, sorters, and highlighters. 2649 * 2650 * @return the adapter that knows how to access the component data model 2651 */ 2652 @Override 2653 protected ComponentAdapter getComponentAdapter() { 2654 if (dataAdapter == null) { 2655 dataAdapter = new TreeTableDataAdapter(this); 2656 } 2657 return dataAdapter; 2658 } 2659 2660 2661 protected static class TreeTableDataAdapter extends JXTable.TableAdapter { 2662 private final JXTreeTable table; 2663 2664 /** 2665 * Constructs a <code>TreeTableDataAdapter</code> for the specified 2666 * target component. 2667 * 2668 * @param component the target component 2669 */ 2670 public TreeTableDataAdapter(JXTreeTable component) { 2671 super(component); 2672 table = component; 2673 } 2674 2675 public JXTreeTable getTreeTable() { 2676 return table; 2677 } 2678 2679 /** 2680 * {@inheritDoc} 2681 */ 2682 @Override 2683 public boolean isExpanded() { 2684 return table.isExpanded(row); 2685 } 2686 2687 /** 2688 * {@inheritDoc} 2689 */ 2690 @Override 2691 public int getDepth() { 2692 return table.getPathForRow(row).getPathCount() - 1; 2693 } 2694 2695 /** 2696 * {@inheritDoc} 2697 */ 2698 @Override 2699 public boolean isLeaf() { 2700 // Issue #270-swingx: guard against invisible row 2701 TreePath path = table.getPathForRow(row); 2702 if (path != null) { 2703 return table.getTreeTableModel().isLeaf(path.getLastPathComponent()); 2704 } 2705 // JW: this is the same as BasicTreeUI.isLeaf. 2706 // Shouldn't happen anyway because must be called for visible rows only. 2707 return true; 2708 } 2709 /** 2710 * 2711 * @return true if the cell identified by this adapter displays hierarchical 2712 * nodes; false otherwise 2713 */ 2714 @Override 2715 public boolean isHierarchical() { 2716 return table.isHierarchical(column); 2717 } 2718 2719 /** 2720 * {@inheritDoc} <p> 2721 * 2722 * Overridden to fix #821-swingx: string rep of hierarchical column incorrect. 2723 * In this case we must delegate to the tree directly if hidden, the visible 2724 * is handled by the TreeTable itself.<p> 2725 * 2726 * PENDING JW: revisit once we switch to really using a table renderer. 2727 */ 2728 @Override 2729 public String getFilteredStringAt(int row, int column) { 2730 if (table.getTreeTableModel().getHierarchicalColumn() == column) { 2731 if (modelToView(column) < 0) { 2732 // hidden hierarchical column, access directly 2733 return table.getHierarchicalStringAt(row); 2734 } 2735 } 2736 return super.getFilteredStringAt(row, column); 2737 } 2738 2739 /** 2740 * {@inheritDoc} <p> 2741 * 2742 * Overridden to fix #821-swingx: string rep of hierarchical column incorrect. 2743 * In this case we must delegate to the tree directly if hidden, the visible 2744 * is handled by the TreeTable itself.<p> 2745 * 2746 * PENDING JW: revisit once we switch to really using a table renderer. 2747 */ 2748 @Override 2749 public String getStringAt(int row, int column) { 2750 if (table.getTreeTableModel().getHierarchicalColumn() == column) { 2751 if (modelToView(column) < 0) { 2752 // hidden hierarchical column, access directly 2753 return table.getHierarchicalStringAt(row); 2754 } 2755 } 2756 return super.getStringAt(row, column); 2757 } 2758 2759 } 2760 2761 }