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