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