001 package echopointng;
002
003 /*
004 * This file is part of the Echo Point Project. This project is a collection
005 * of Components that have extended the Echo Web Application Framework.
006 *
007 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
008 *
009 * The contents of this file are subject to the Mozilla Public License Version
010 * 1.1 (the "License"); you may not use this file except in compliance with
011 * the License. You may obtain a copy of the License at
012 * http://www.mozilla.org/MPL/
013 *
014 * Software distributed under the License is distributed on an "AS IS" basis,
015 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
016 * for the specific language governing rights and limitations under the
017 * License.
018 *
019 * Alternatively, the contents of this file may be used under the terms of
020 * either the GNU General Public License Version 2 or later (the "GPL"), or
021 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
022 * in which case the provisions of the GPL or the LGPL are applicable instead
023 * of those above. If you wish to allow use of your version of this file only
024 * under the terms of either the GPL or the LGPL, and not to allow others to
025 * use your version of this file under the terms of the MPL, indicate your
026 * decision by deleting the provisions above and replace them with the notice
027 * and other provisions required by the GPL or the LGPL. If you do not delete
028 * the provisions above, a recipient may use your version of this file under
029 * the terms of any one of the MPL, the GPL or the LGPL.
030 */
031
032 /*
033 * The design paradigm and class name used within have been taken directly from
034 * the java.swing package has been retro-fitted to work with the NextApp Echo web framework.
035 *
036 * This file was made part of the EchoPoint project on the 25/07/2002.
037 *
038 */
039 import java.io.Serializable;
040 import java.lang.ref.WeakReference;
041 import java.util.ArrayList;
042 import java.util.Collection;
043 import java.util.Enumeration;
044 import java.util.EventListener;
045 import java.util.HashMap;
046 import java.util.Hashtable;
047 import java.util.Iterator;
048 import java.util.Map;
049 import java.util.Stack;
050 import java.util.Vector;
051 import java.util.WeakHashMap;
052
053 import nextapp.echo2.app.Component;
054 import nextapp.echo2.app.Label;
055 import nextapp.echo2.app.Style;
056 import nextapp.echo2.app.event.ActionEvent;
057 import nextapp.echo2.app.event.ActionListener;
058 import echopointng.tree.DefaultMutableTreeNode;
059 import echopointng.tree.DefaultTreeCellRenderer;
060 import echopointng.tree.DefaultTreeIcons;
061 import echopointng.tree.DefaultTreeModel;
062 import echopointng.tree.DefaultTreeSelectionModel;
063 import echopointng.tree.EmptyTreeSelectionModel;
064 import echopointng.tree.ExpandVetoException;
065 import echopointng.tree.RowMapper;
066 import echopointng.tree.TreeActionEventEx;
067 import echopointng.tree.TreeCellRenderer;
068 import echopointng.tree.TreeExpansionEvent;
069 import echopointng.tree.TreeExpansionListener;
070 import echopointng.tree.TreeIcons;
071 import echopointng.tree.TreeModel;
072 import echopointng.tree.TreeModelEvent;
073 import echopointng.tree.TreeModelListener;
074 import echopointng.tree.TreeNode;
075 import echopointng.tree.TreePath;
076 import echopointng.tree.TreeSelectionEvent;
077 import echopointng.tree.TreeSelectionListener;
078 import echopointng.tree.TreeSelectionModel;
079 import echopointng.tree.TreeWillExpandListener;
080
081 /**
082 * The <code>Tree</code> component displays data in a hierarchial manner. The
083 * API for this Component has based heavily on the Swing JTree class.
084 * <p>
085 * A <code>Tree</code> only renders the visible nodes and an event is
086 * generated to expand or collapse nodes as well as when the user clicks on a
087 * node. This allows the application to insert more nodes as necessary or on a
088 * "just in time" basis.
089 * <p>
090 * Tree also supports node selection via its TreeSelectionModel.
091 * <p>
092 * The <code>Tree</code> component supports "bubbling up" actions.
093 * <p>
094 * If the objects placed in the Tree implement <code>TreeNode</code> then an
095 * action command can be identified with each node. When a node is pressed, the
096 * associated action command will be raised with any
097 * <code>ActionListeners</code> of the Tree. If no action command is
098 * associated with a <code>TreeNode</code> then the path to the ancestors will
099 * be followed to look for an action command. If no action command can be found,
100 * then the action command of the tree itself will be used.
101 * <p>
102 * If you call <code>setNullActionCommandsRaiseEvents(false)</code> then
103 * action events will not be raised for TreeNodes that have null action
104 * commands. It is TRUE by default.
105 * <p>
106 *
107 * @see echopointng.tree.TreeModel
108 * @see echopointng.tree.DefaultTreeModel
109 * @see echopointng.tree.TreeCellRenderer
110 * @see echopointng.tree.DefaultTreeCellRenderer
111 * @see echopointng.tree.TreeNode
112 * @see echopointng.tree.MutableTreeNode
113 * @see echopointng.tree.DefaultMutableTreeNode
114 */
115 public class Tree extends AbleComponent {
116
117 /**
118 * Used as an INPUT name to indicate that a node is to be toggled, ie
119 * expanded or contracted
120 */
121 public static final String INPUT_TOGGLE = "toggle";
122
123 /**
124 * Used as an INPUT name to indicate that a node is to be selected
125 */
126 public static final String INPUT_SELECT = "select";
127
128 public final static String PROPERTY_ACTION_COMMAND = "actionCommand";
129
130 public final static String PROPERTY_CELL_RENDERER = "cellRenderer";
131
132 public final static String PROPERTY_LINES_DRAWN = "linesDrawn";
133
134 public final static String PROPERTY_NULL_ACTION_COMMANDS_RAISE_EVENTS = "nullActionCommandsRaiseEvents";
135
136 public final static String PROPERTY_ROOT_VISIBLE = "rootVisible";
137
138 public static final String PROPERTY_ROW_HEIGHT = "rowHeight";
139
140 public final static String PROPERTY_SELECTION_MODEL = "selectionModel";
141
142 public final static String PROPERTY_SHOWS_ROOT_HANDLES = "showsRootHandles";
143
144 public final static String PROPERTY_TREE_ICONS = "treeIcons";
145
146 public final static String PROPERTY_MODEL = "model";
147
148 public final static String PROPERTY_ROOT_AUTO_EXPANDED = "rootAutoExpanded";
149
150 public final static String PROPERTY_PARTIAL_UPDATE_SUPPORT = "partialUpdateSupport";
151
152 public final static String PROPERTY_CELL_WIDTH_CONTRAINED = "cellWidthContrained";
153
154 public final static String PROPERTY_SELECTION_INCLUDES_ICON = "selectionIncludesIcon";
155
156 public final static String PROPERTY_SCROLL_INTO_VIEW_USED = "scrollIntoViewUsed";
157
158 public static final String NODE_CHANGED_PROPERTY = "nodeChanged";
159
160 public static final String MODEL_STRUCTURE_CHANGED_PROPERTY = "modelStructureChanged";
161
162 /**
163 * The <code>TreeExpansionHandler</code> is a private handler for
164 * listening to our own expansion events.
165 */
166 private class TreeExpansionHandler implements Serializable, TreeExpansionListener {
167
168 /**
169 * @see echopointng.tree.TreeExpansionListener#treeCollapsed(TreeExpansionEvent)
170 */
171 public void treeCollapsed(TreeExpansionEvent e) {
172 markPathDirty(e.getPath(), false);
173 }
174
175 /**
176 * @see echopointng.tree.TreeExpansionListener#treeExpanded(TreeExpansionEvent)
177 */
178 public void treeExpanded(TreeExpansionEvent e) {
179 markPathDirty(e.getPath(), false);
180 lastExpandedPaths.add(e.getPath());
181 }
182 }
183
184 /**
185 * Listens to the model and updates the expandedState accordingly when nodes
186 * are removed, or changed.
187 */
188 protected class TreeModelHandler implements TreeModelListener, Serializable {
189
190 public void treeNodesChanged(TreeModelEvent e) {
191 markPathsDirty(e.getTreePath(), e.getChildren(), false);
192 }
193
194 public void treeNodesInserted(TreeModelEvent e) {
195 markPathsDirty(e.getTreePath(), e.getChildren(), true);
196 }
197
198 public void treeNodesRemoved(TreeModelEvent e) {
199 if (e == null) {
200 return;
201 }
202
203 TreePath parent = e.getTreePath();
204 Object[] children = e.getChildren();
205
206 if (children == null) {
207 return;
208 }
209
210 TreePath rPath;
211 Vector toRemove = new Vector(Math.max(1, children.length));
212
213 for (int counter = children.length - 1; counter >= 0; counter--) {
214 rPath = parent.pathByAddingChild(children[counter]);
215 if (expandedState.get(rPath) != null) {
216 toRemove.addElement(rPath);
217 }
218 }
219
220 if (toRemove.size() > 0) {
221 removeDescendantToggledPaths(toRemove.elements());
222 }
223
224 TreeModel model = getModel();
225
226 if (model == null || model.isLeaf(parent.getLastPathComponent())) {
227 expandedState.remove(parent);
228 }
229
230
231 // Don't need to mark the path as dirty - partial render removes all
232 // deleted nodes. An exception is when the parent node
233 // has gone from n to 0 children in which case its dirty because its
234 // icon must change from -/+ to a straight line
235 //
236 Object parentNode = parent.getLastPathComponent();
237 if (model != null && model.getChildCount(parentNode) == 0) {
238 markPathDirty(parent, false);
239 }
240 invalidate();
241 firePropertyChange(NODE_CHANGED_PROPERTY, null, null);
242 }
243
244 public void treeStructureChanged(TreeModelEvent e) {
245 if (e == null) {
246 return;
247 }
248
249 TreePath parent = e.getTreePath();
250
251 if (parent == null) {
252 return;
253 }
254
255 if (parent.getPathCount() == 1) {
256 clearToggledPaths();
257 TreeModel model = getModel();
258 if (model != null && !model.isLeaf(model.getRoot())) {
259 expandedState.put(parent, Boolean.TRUE);
260 }
261 } else if (expandedState.get(parent) != null) {
262 Vector toRemove = new Vector(1);
263 boolean isExpanded = isExpanded(parent);
264
265 toRemove.addElement(parent);
266 removeDescendantToggledPaths(toRemove.elements());
267 if (isExpanded) {
268 TreeModel model = getModel();
269
270 if (model == null || model.isLeaf(parent.getLastPathComponent()))
271 collapsePath(parent);
272 else
273 expandedState.put(parent, Boolean.TRUE);
274 }
275 }
276
277 invalidate();
278 firePropertyChange(MODEL_STRUCTURE_CHANGED_PROPERTY, null, null); // Forces
279 // full
280 // redraw
281 }
282 }
283
284 /**
285 * Handles the RowMapper interface for the tree
286 * <p>
287 */
288 protected class TreeRowMapper implements Serializable, RowMapper {
289
290 /**
291 * @see echopointng.tree.RowMapper#getRowsForPaths(echopointng.tree.TreePath[])
292 */
293 public int[] getRowsForPaths(TreePath[] paths) {
294 return Tree.this.getRowsForPaths(paths);
295 }
296 }
297
298 /**
299 * Handles creating a new TreeSelectionEvent with the Tree as the source and
300 * passing it off to all the listeners.
301 * <p>
302 */
303 protected class TreeSelectionForwarder implements Serializable, TreeSelectionListener {
304 /**
305 * Invoked by the TreeSelectionModel when the selection changes.
306 *
307 * @param e
308 * the TreeSelectionEvent generated by the TreeSelectionModel
309 */
310 public void valueChanged(TreeSelectionEvent e) {
311
312 // Forces redraw of selected nodes
313 TreePath[] paths = e.getPaths();
314 for (int i = 0; i < paths.length; i++) {
315 markPathDirty(paths[i], false);
316 }
317
318 TreeSelectionEvent newE;
319 newE = (TreeSelectionEvent) e.cloneWithSource(Tree.this);
320 fireValueChanged(newE);
321 }
322 }
323
324 /* Max number of stacks to keep around. */
325 private static int TEMP_STACK_SIZE = 15;
326
327 /* a HashMap of TreeNodes --> Component mappings */
328 private transient Map componentMap = new WeakHashMap();
329
330 /* a bit bucket for components */
331 // private DevNull devNull;
332 /* Used when setExpandedState is invoked, will be a Stack of Stacks. */
333 private transient Stack expandedStack;
334
335 /* Maps from TreePath to Boolean indicating whether a path is expanded. */
336 private transient Hashtable expandedState;
337
338 /* Records the last tree path that was expanded */
339 private transient Collection lastExpandedPaths = new ArrayList();
340
341 /* Internal watch for expansion events */
342 private TreeExpansionHandler expansionForwarder;
343
344 /* Creates a new event and passed it off the selectionListeners. */
345 private TreeSelectionForwarder selectionForwarder;
346
347 private TreeModelListener treeModelListener;
348
349 /* List of nodes which have changed since the last render */
350 private ArrayList dirtyPaths = new ArrayList();
351
352 /* indicates whether the Tree is valid or not */
353 private boolean valid;
354
355 public static final Style DEFAULT_STYLE;
356 static {
357 MutableStyleEx style = new MutableStyleEx();
358 style.setProperty(PROPERTY_ROOT_VISIBLE, true);
359 style.setProperty(PROPERTY_LINES_DRAWN, true);
360 style.setProperty(PROPERTY_SHOWS_ROOT_HANDLES, true);
361 style.setProperty(PROPERTY_SCROLL_INTO_VIEW_USED, true);
362 style.setProperty(PROPERTY_NULL_ACTION_COMMANDS_RAISE_EVENTS, true);
363
364 DEFAULT_STYLE = style;
365 }
366
367 /**
368 * Returns a <code>Tree</code> with a <code>DefaultMutableTreeNode</code>
369 * as its root, which displays the root node.
370 */
371 public Tree() {
372 this(new DefaultMutableTreeNode("root"), false);
373 }
374
375 /**
376 * Returns a <code>Tree</code> with the specified <code>TreeNode</code>
377 * as its root, which displays the root node.
378 */
379 public Tree(TreeNode root) {
380 this(root, false);
381 }
382
383 /**
384 * Returns a <code>Tree</code> with the specified <code>TreeNode</code>
385 * as its root, which displays the root node and which decides whether a
386 * node is a leaf node in the specified manner.
387 *
388 */
389 public Tree(TreeNode root, boolean asksAllowsChildren) {
390 this(new DefaultTreeModel(root, asksAllowsChildren));
391 }
392
393 /**
394 * Returns an instance of a <code>Tree</code> which displays the root node
395 * and is created using the specified data model.
396 *
397 */
398 public Tree(TreeModel newModel) {
399 super();
400 expandedStack = new Stack();
401 expandedState = new Hashtable();
402
403 DefaultTreeSelectionModel tempSelectionModel = new DefaultTreeSelectionModel();
404 tempSelectionModel.setRowMapper(new TreeRowMapper());
405 setSelectionModel(tempSelectionModel);
406
407 expansionForwarder = new TreeExpansionHandler();
408 addTreeExpansionListener(expansionForwarder);
409
410 setCellRenderer(new DefaultTreeCellRenderer());
411 setTreeIcons(new DefaultTreeIcons());
412 setModel(newModel);
413 }
414
415 /**
416 * @see nextapp.echo2.app.Component#processInput(java.lang.String,
417 * java.lang.Object)
418 */
419 public void processInput(String name, Object value) {
420 super.processInput(name, value);
421 if (getModel() == null) {
422 return;
423 }
424
425 TreePath path = (TreePath) value;
426
427 if (name.startsWith(INPUT_TOGGLE)) {
428 // and flip it
429 if (isExpanded(path)) {
430 collapsePath(path);
431 } else {
432 expandPath(path);
433 }
434
435 } else if (name.startsWith(INPUT_SELECT)) {
436 TreeSelectionModel selectionModel = getSelectionModel();
437 if (selectionModel != null) {
438 if (selectionModel.isPathSelected(path)) {
439 selectionModel.removeSelectionPath(path);
440 } else {
441 selectionModel.addSelectionPath(path);
442 }
443 }
444
445 Object[] nodes = path.getPath();
446 if (nodes.length > 0 && nodes[nodes.length - 1] instanceof TreeNode) {
447 Object originalNode = nodes[nodes.length - 1];
448 TreeNode node = (TreeNode) originalNode;
449
450 String actionCommand = node.getActionCommand();
451 while (actionCommand == null) {
452 if (node.getParent() != null) {
453 node = node.getParent();
454 actionCommand = node.getActionCommand();
455 } else {
456 break;
457 }
458 }
459 if (actionCommand == null) {
460 actionCommand = getActionCommand();
461 }
462 // TODO - add metakey support
463 ActionEvent e = new TreeActionEventEx(this, actionCommand, 0, originalNode);
464 fireActionPerformed(e);
465 }
466 }
467
468 invalidate();
469 }
470
471 /**
472 * Adds an <code>ActionListener</code>.
473 *
474 * @param l
475 * The <code>ActionListener</code> to be added.
476 */
477 public void addActionListener(ActionListener l) {
478 getEventListenerList().addListener(ActionListener.class, l);
479 }
480
481 /**
482 * Adds the node identified by the specified TreePath to the current
483 * selection. If any component of the path isn't viewable, it is made
484 * viewable.
485 *
486 * @param path
487 * the TreePath to add
488 */
489 public void addSelectionPath(TreePath path) {
490 makeVisible(path);
491 getSelectionModel().addSelectionPath(path);
492 }
493
494 /**
495 * Adds each path in the array of paths to the current selection. If any
496 * component of any of the paths isn't viewable, it is made viewable.
497 *
498 * @param paths
499 * an array of TreePath objects that specifies the nodes to add
500 */
501 public void addSelectionPaths(TreePath[] paths) {
502 if (paths != null) {
503 for (int counter = paths.length - 1; counter >= 0; counter--)
504 makeVisible(paths[counter]);
505 }
506 getSelectionModel().addSelectionPaths(paths);
507 }
508
509 /**
510 * Adds a listener for TreeExpansion events.
511 *
512 * @param tel
513 * a TreeExpansionListener that will be notified when a tree node
514 * is expanded or collapsed (a "negative expansion")
515 */
516 public void addTreeExpansionListener(TreeExpansionListener tel) {
517 getEventListenerList().addListener(TreeExpansionListener.class, tel);
518 }
519
520 /**
521 * Adds a listener for TreeSelection events.
522 *
523 * @param tsl
524 * the TreeSelectionListener that will be notified when a node is
525 * selected or deselected (a "negative selection")
526 */
527 public void addTreeSelectionListener(TreeSelectionListener tsl) {
528 getEventListenerList().addListener(TreeSelectionListener.class, tsl);
529 if (getEventListenerList().getListenerCount(TreeSelectionListener.class) != 0 && selectionForwarder == null) {
530 selectionForwarder = new TreeSelectionForwarder();
531 getSelectionModel().addTreeSelectionListener(selectionForwarder);
532 }
533 }
534
535 /**
536 * Adds a listener for TreeWillExpand events.
537 *
538 * @param tel
539 * a TreeWillExpandListener that will be notified when a tree
540 * node will be expanded or collapsed (a "negative expansion")
541 */
542 public void addTreeWillExpandListener(TreeWillExpandListener tel) {
543 getEventListenerList().addListener(TreeWillExpandListener.class, tel);
544 }
545
546 /**
547 * Ensures that all the nodes in the tree are collapsed
548 */
549 public void collapseAll() {
550 TreeModel model = getModel();
551 if (model != null) {
552 toggleAllNodes(new TreePath(model.getRoot()), false);
553 }
554 }
555
556 /**
557 * Ensures that the node identified by the specified path is collapsed and
558 * viewable.
559 *
560 * @param path
561 * the TreePath identifying a node
562 */
563 public void collapsePath(TreePath path) {
564 setExpandedState(path, false);
565 }
566
567 /**
568 * Ensures that all the nodes are expanded and viewable.
569 */
570 public void expandAll() {
571 TreeModel model = getModel();
572 if (model != null) {
573 toggleAllNodes(new TreePath(model.getRoot()), true);
574 }
575 }
576
577 /**
578 * Ensures that the node identified by the specified path is expanded and
579 * viewable.
580 *
581 * @param path
582 * the TreePath identifying a node
583 */
584 public void expandPath(TreePath path) {
585 TreeModel model = getModel();
586 if (path != null && model != null && !model.isLeaf(path.getLastPathComponent())) {
587 setExpandedState(path, true);
588 }
589 }
590
591 /**
592 * Notifies all listeners that have registered for this event type.
593 *
594 * @param e
595 * The <code>ActionEvent</code> to send.
596 */
597 public void fireActionPerformed(ActionEvent e) {
598 EventListener[] listeners = getEventListenerList().getListeners(ActionListener.class);
599 for (int index = 0; index < listeners.length; ++index) {
600 ((ActionListener) listeners[index]).actionPerformed(e);
601 }
602 }
603
604 /**
605 * Notify all listeners that have registered interest for notification on
606 * this event type. The event instance is lazily created using the
607 * parameters passed into the fire method.
608 *
609 * @param path
610 * the TreePath indicating the node that was collapsed
611 */
612 public void fireTreeCollapsed(TreePath path) {
613 Object[] listeners = getEventListenerList().getListeners(TreeExpansionListener.class);
614 TreeExpansionEvent e = null;
615
616 for (int index = 0; index < listeners.length; ++index) {
617 if (e == null)
618 e = new TreeExpansionEvent(this, path);
619 ((TreeExpansionListener) listeners[index]).treeCollapsed(e);
620 }
621 }
622
623 /**
624 * Notify all listeners that have registered interest for notification on
625 * this event type. The event instance is lazily created using the
626 * parameters passed into the fire method.
627 *
628 * @param path
629 * the TreePath indicating the node that was expanded
630 */
631 public void fireTreeExpanded(TreePath path) {
632 Object[] listeners = getEventListenerList().getListeners(TreeExpansionListener.class);
633 TreeExpansionEvent e = null;
634
635 for (int index = 0; index < listeners.length; ++index) {
636 if (e == null)
637 e = new TreeExpansionEvent(this, path);
638 ((TreeExpansionListener) listeners[index]).treeExpanded(e);
639 }
640 }
641
642 /**
643 * Notify all listeners that have registered interest for notification on
644 * this event type. The event instance is lazily created using the
645 * parameters passed into the fire method.
646 *
647 * @param path
648 * the TreePath indicating the node that was expanded
649 */
650 public void fireTreeWillCollapse(TreePath path) throws ExpandVetoException {
651 Object[] listeners = getEventListenerList().getListeners(TreeWillExpandListener.class);
652 TreeExpansionEvent e = null;
653
654 for (int index = 0; index < listeners.length; ++index) {
655 if (e == null)
656 e = new TreeExpansionEvent(this, path);
657 ((TreeWillExpandListener) listeners[index]).treeWillCollapse(e);
658 }
659
660 }
661
662 /**
663 * Notify all listeners that have registered interest for notification on
664 * this event type. The event instance is lazily created using the
665 * parameters passed into the fire method.
666 *
667 * @param path
668 * the TreePath indicating the node that was expanded
669 */
670 public void fireTreeWillExpand(TreePath path) throws ExpandVetoException {
671 Object[] listeners = getEventListenerList().getListeners(TreeWillExpandListener.class);
672 TreeExpansionEvent e = null;
673
674 for (int index = 0; index < listeners.length; ++index) {
675 if (e == null)
676 e = new TreeExpansionEvent(this, path);
677 ((TreeWillExpandListener) listeners[index]).treeWillExpand(e);
678 }
679 }
680
681 /**
682 * Returns the action command string of the <code>Tree</code>
683 *
684 * @return java.lang.String
685 */
686 public String getActionCommand() {
687 return (String) getProperty(PROPERTY_ACTION_COMMAND);
688 }
689
690 /**
691 * Returns the current TreeCellRenderer that is rendering each cell.
692 *
693 * @return the TreeCellRenderer that is rendering each cell
694 * @see echopointng.tree.TreeCellRenderer
695 */
696 public TreeCellRenderer getCellRenderer() {
697 return (TreeCellRenderer) getProperty(PROPERTY_CELL_RENDERER);
698 }
699
700 /**
701 * Returns the Component that will be used to render the provided tree node.
702 * If this method returns null, then no Component has been associated with a
703 * specified tree node and the TreeCellRenderer.getTreeCellRendererText()
704 * method will be used to render the Tree cell.
705 * <p>
706 * This method is primarily for use by the Tree UI peer rendering code. You
707 * should call the "invalidate" and then "validate" methods first before
708 * calling this method to ensure that all child Components are known and are
709 * valid.
710 *
711 * @param treeNode -
712 * the tree node in question
713 * @return - a component for the node or null
714 *
715 * @see TreeCellRenderer#getTreeCellRendererText(Tree, Object, boolean,
716 * boolean, boolean)
717 */
718 public Component getComponent(Object treeNode) {
719 WeakReference wr = (WeakReference) componentMap.get(treeNode);
720 return wr != null ? (Component) wr.get() : null;
721 }
722
723 /**
724 * Returns an Enumeration of the descendants of <code>path</code> that are
725 * currently expanded. If <code>path</code> is not currently expanded,
726 * this will return null. If you expand/collapse nodes while iterating over
727 * the returned Enumeration this may not return all the expanded paths, or
728 * may return paths that are no longer expanded.
729 */
730 public Enumeration getExpandedDescendants(TreePath parent) {
731 if (!isExpanded(parent))
732 return null;
733
734 Enumeration toggledPaths = expandedState.keys();
735 Vector elements = new Vector();
736 TreePath path;
737 Object value;
738
739 if (toggledPaths != null) {
740 while (toggledPaths.hasMoreElements()) {
741 path = (TreePath) toggledPaths.nextElement();
742 value = expandedState.get(path);
743 // Add the path if it is expanded, a descendant of parent,
744 // and it is visible (all parents expanded).
745 if (value != null && ((Boolean) value).booleanValue() && parent.isDescendant(path) && isVisible(path)) {
746 elements.addElement(path);
747 }
748 }
749 }
750 return elements.elements();
751 }
752
753 /**
754 * Returns the last path component in the first node of the current
755 * selection.
756 *
757 * @return the last Object in the first selected node's TreePath, or null if
758 * nothing is selected
759 */
760 public Object getLastSelectedPathComponent() {
761 TreePath selPath = getSelectionModel().getSelectionPath();
762 if (selPath != null)
763 return selPath.getLastPathComponent();
764 return null;
765 }
766
767 /**
768 * Returns the path of the last node added to the selection.
769 *
770 * @return the TreePath of the last node added to the selection.
771 */
772 public TreePath getLeadSelectionPath() {
773 return getSelectionModel().getLeadSelectionPath();
774 }
775
776 /**
777 * Returns the TreeModel that is providing the data.
778 *
779 * @return the TreeModel that is providing the data
780 */
781 public TreeModel getModel() {
782 return (TreeModel) getProperty(PROPERTY_MODEL);
783 }
784
785 /**
786 * This will return a collection of TresePath that have expanded within the
787 * Tree. This will be automatically cleared each time a Tree is rendered so
788 * dont rely on it.
789 *
790 * @return a collection of TresePath that have expanded within the Tree.
791 */
792 public Collection getLastExpandedPaths() {
793 return lastExpandedPaths;
794 }
795
796 /**
797 * Returns the row that the TreePath <code>path</code> is being displayed
798 * at. If the TreePath in <code>path</code> is not valid it will be set to
799 * -1.
800 */
801 public int getRowForPath(TreePath path) {
802 Object[] nodes = null;
803 if (getModel() != null) {
804 nodes = path.getPath();
805 }
806 //
807 // is it being displayed at all
808 if (nodes.length == 0 || !isExpanded(path))
809 return -1;
810
811 TreePath rootPath = new TreePath(getModel().getRoot());
812 Enumeration enumeration = getDescendantToggledPaths(rootPath);
813 Vector pathsVec = orderPathEnumeration(rootPath, enumeration);
814 for (int i = 0; i < pathsVec.size(); i++) {
815 TreePath testPath = (TreePath) pathsVec.get(i);
816 if (testPath.equals(path))
817 return i;
818 }
819 return -1;
820 }
821
822 /**
823 * Returns the rows that the TreePath instances in <code>path</code> are
824 * being displayed at. The returned array is an array of the same length as
825 * that passed in, and if one of the TreePaths in <code>path</code> is not
826 * valid its entry in the array should be set to -1.
827 */
828 public int[] getRowsForPaths(TreePath[] paths) {
829 if (paths == null)
830 return null;
831
832 int numPaths = paths.length;
833 int[] rows = new int[numPaths];
834 for (int counter = 0; counter < numPaths; counter++)
835 rows[counter] = getRowForPath(paths[counter]);
836 return rows;
837 }
838
839 /**
840 * Returns the row height to be used for each row within the Tree. If this
841 * value is 0 or less, then no row height will be used.
842 *
843 * @return int
844 */
845 public int getRowHeight() {
846 return getProperty(PROPERTY_ROW_HEIGHT, -1);
847 }
848
849 /**
850 * Returns the number of nodes selected.
851 *
852 * @return the number of nodes selected
853 */
854 public int getSelectionCount() {
855 return getSelectionModel().getSelectionCount();
856 }
857
858 /**
859 * Returns the model for selections. This should always return a non-null
860 * value. If you don't want to allow anything to be selected set the
861 * selection model to null, which forces an empty selection model to be
862 * used.
863 *
864 */
865 public TreeSelectionModel getSelectionModel() {
866 return (TreeSelectionModel) getProperty(PROPERTY_SELECTION_MODEL);
867 }
868
869 /**
870 * Returns the path to the first selected node.
871 *
872 * @return the TreePath for the first selected node, or null if nothing is
873 * currently selected
874 */
875 public TreePath getSelectionPath() {
876 return getSelectionModel().getSelectionPath();
877 }
878
879 /**
880 * Returns the paths of all selected values.
881 *
882 * @return an array of TreePath objects indicating the selected nodes, or
883 * null if nothing is currently selected.
884 */
885 public TreePath[] getSelectionPaths() {
886 return getSelectionModel().getSelectionPaths();
887 }
888
889 /**
890 * Returns true if handles for the root nodes are displayed.
891 *
892 */
893 public boolean getShowsRootHandles() {
894 return getProperty(PROPERTY_SHOWS_ROOT_HANDLES, true);
895 }
896
897 /**
898 * Returns the TreeIcons being used by the Tree
899 *
900 * @return the TreeIcons being used by the Tree
901 * @see echopointng.tree.TreeIcons
902 */
903 public TreeIcons getTreeIcons() {
904 return (TreeIcons) getProperty(PROPERTY_TREE_ICONS);
905 }
906
907 /**
908 * Returns true if the node identified by the path has ever been expanded.
909 *
910 * @param path =
911 * The TreePath in question
912 */
913 public boolean hasEverBeenExpanded(TreePath path) {
914 return (path != null && expandedState.get(path) != null);
915 }
916
917 /**
918 * Marks the Tree as needing to be re-rendered.
919 */
920 public void invalidate() {
921 valid = false;
922 // need this to convince Echo2 that the DOM has changed somehow
923 // firePropertyChange(null, null, null);
924 }
925
926 /**
927 * Returns true if the value identified by path is currently collapsed, this
928 * will return false if any of the values in path are currently not being
929 * displayed.
930 *
931 * @param path
932 * the TreePath to check
933 * @return true if any of the nodes in the node's path are collapsed, false
934 * if all nodes in the path are expanded
935 */
936 public boolean isCollapsed(TreePath path) {
937 return !isExpanded(path);
938 }
939
940 /**
941 * Returns true if the node identified by the path is currently expanded,
942 *
943 * @param path
944 * the TreePath specifying the node to check
945 * @return false if any of the nodes in the node's path are collapsed, true
946 * if all nodes in the path are expanded
947 */
948 public boolean isExpanded(TreePath path) {
949 if (path == null)
950 return false;
951
952 // Is this node expanded?
953 Object value = expandedState.get(path);
954 if (value == null || !((Boolean) value).booleanValue())
955 return false;
956
957 // It is, make sure its parent is also expanded.
958 TreePath parentPath = path.getParentPath();
959 if (parentPath != null)
960 return isExpanded(parentPath);
961 return true;
962 }
963
964 /**
965 * Returns the path for the specified row. If <code>row</code> is not
966 * visible, <code>null</code> is returned.
967 *
968 * @param row
969 * an integer specifying a row
970 * @return the <code>TreePath</code> to the specified node,
971 * <code>null</code> if <code>row < 0</code> or
972 * <code>row >= getRowCount()</code>
973 */
974 public TreePath getPathForRow(final int row) {
975 if (row < 0) {
976 return null;
977 }
978
979 // a simply call back implementation that stops at the given row.
980 TreePathNavigationListener pathNavigationListener = new TreePathNavigationListener() {
981 TreePath targetPath = null;
982
983 public boolean onTreePath(TreePath path, int pathRow) {
984 if (pathRow == row) {
985 targetPath = path;
986 return false;
987 }
988 return true;
989 }
990
991 public TreePath getLastEncounteredPath() {
992 return targetPath;
993 }
994 };
995 // call the method with the path listener to find the last encountered
996 // path
997 findAllVisibleRows(pathNavigationListener);
998 return pathNavigationListener.getLastEncounteredPath();
999 }
1000
1001 /**
1002 * Returns the number of rows that are currently being displayed in the
1003 * Tree.
1004 *
1005 * @return the number of rows that are being displayed in the Tree
1006 */
1007 public int getRowCount() {
1008 int rowCount = 0;
1009 if (getModel().getRoot() == null) {
1010 return 0;
1011 }
1012 TreePath rootPath = new TreePath(getModel().getRoot());
1013 if (!isExpanded(rootPath)) {
1014 return 1;
1015 }
1016 rowCount = findAllVisibleRows(null);
1017 return rowCount;
1018 }
1019
1020 /**
1021 * <code>TreePathListener</code> is a simply callback interface that is
1022 * callde when a TreePath is encountered during a model navigation.
1023 */
1024 private interface TreePathNavigationListener {
1025 /**
1026 * @return false if the navigation is to be stopped at the current
1027 * TreePath.
1028 */
1029 boolean onTreePath(TreePath path, int pathRow);
1030
1031 /**
1032 * @return the TreePath that as last encountered during navigation.
1033 */
1034 TreePath getLastEncounteredPath();
1035 }
1036
1037 /**
1038 * This will traverse the Tree in a breath first manner and count all the
1039 * visible rows. If the pathNavigationListener object is non null then it
1040 * will inform it each time it encounters a new TreePath.
1041 * <p>
1042 * If the pathNavigationListener tells it to stop, then -1 is returned as
1043 * the rowCount
1044 *
1045 * @param pathNavigationListener -
1046 * an optional callback interface for indicating when a TreePath
1047 * has been encountered.
1048 *
1049 * @return a count of all the visible TreePaths or -1 if it is told to stop
1050 */
1051 private int findAllVisibleRows(TreePathNavigationListener pathNavigationListener) {
1052 TreeModel model = getModel();
1053 Object rootNode = model.getRoot();
1054 TreePath rootPath = new TreePath(rootNode);
1055 int rowCount = 0;
1056 if (isRootVisible()) {
1057 rowCount++;
1058 if (pathNavigationListener != null) {
1059 if (!pathNavigationListener.onTreePath(rootPath, rowCount - 1)) {
1060 return -1;
1061 }
1062 }
1063 }
1064 if (!model.isLeaf(model.getRoot()) && isExpanded(rootPath)) {
1065 int childRowCount = findAllVisibleRows(rootNode, rootPath, pathNavigationListener, rowCount);
1066 if (childRowCount == -1) {
1067 return -1;
1068 }
1069 rowCount = childRowCount;
1070 }
1071 return rowCount;
1072 }
1073
1074 /**
1075 * This will navigate down the TreeModel until it is told not to or it finds
1076 * all visible nodes. If the pathNavigationListener tells it to stop, then
1077 * -1 is returned as the rowCount
1078 *
1079 * @see Tree#findAllVisibleRows(TreePathNavigationListener)
1080 */
1081 private int findAllVisibleRows(Object parentNode, TreePath parentPath, TreePathNavigationListener pathNavigationListener, int currentRowCount) {
1082 TreeModel model = getModel();
1083 int rowCount = currentRowCount;
1084 int cc = model.getChildCount(parentNode);
1085 for (int i = 0; i < cc; i++) {
1086 Object childNode = model.getChild(parentNode, i);
1087 TreePath childPath = new TreePath(parentPath, childNode);
1088 rowCount++;
1089 if (pathNavigationListener != null) {
1090 if (!pathNavigationListener.onTreePath(childPath, rowCount - 1)) {
1091 return -1;
1092 }
1093 }
1094 if (!model.isLeaf(childNode) && isExpanded(childPath)) {
1095 int childRowCount = findAllVisibleRows(childNode, childPath, pathNavigationListener, rowCount);
1096 if (childRowCount == -1) {
1097 return -1;
1098 }
1099 rowCount = childRowCount;
1100 }
1101 }
1102 return rowCount;
1103 }
1104
1105 /**
1106 * Returns true if the node at the specified display row is currently
1107 * expanded.
1108 *
1109 * @param row
1110 * the row to check, where 0 is the first row in the display
1111 * @return true if the node is currently expanded, otherwise false
1112 */
1113 public boolean isExpanded(int row) {
1114 TreePath path = getPathForRow(row);
1115 if (path != null) {
1116 Boolean value = (Boolean) expandedState.get(path);
1117 return (value != null && value.booleanValue());
1118 }
1119 return false;
1120 }
1121
1122 /**
1123 * Returns true if the node at the specified display row is collapsed.
1124 *
1125 * @param row
1126 * the row to check, where 0 is the first row in the display
1127 * @return true if the node is currently collapsed, otherwise false
1128 */
1129 public boolean isCollapsed(int row) {
1130 return !isExpanded(row);
1131 }
1132
1133 /**
1134 * Ensures that the node in the specified row is expanded and viewable.
1135 * <p>
1136 * If <code>row</code> is < 0 or >=<code>getRowCount</code> this will
1137 * have no effect.
1138 *
1139 * @param row
1140 * an integer specifying a display row, where 0 is the first row
1141 * in the display
1142 */
1143 public void expandRow(int row) {
1144 expandPath(getPathForRow(row));
1145 }
1146
1147 /**
1148 * Ensures that the node in the specified row is collapsed.
1149 * <p>
1150 * If <code>row</code> is < 0 or >=<code>getRowCount</code> this will
1151 * have no effect.
1152 *
1153 * @param row
1154 * an integer specifying a display row, where 0 is the first row
1155 * in the display
1156 */
1157 public void collapseRow(int row) {
1158 collapsePath(getPathForRow(row));
1159 }
1160
1161 /**
1162 * Returns true if lines are drawn between tree nodes.
1163 *
1164 * @return boolean
1165 */
1166 public boolean isLinesDrawn() {
1167 return getProperty(PROPERTY_LINES_DRAWN, true);
1168 }
1169
1170 /**
1171 * @return true if the last expanded node will be scroll into view
1172 */
1173 public boolean isScrollIntoViewUsed() {
1174 return getProperty(PROPERTY_SCROLL_INTO_VIEW_USED, true);
1175 }
1176
1177 /**
1178 * Tree has the ability to "scroll" a peer or child into view when a node is
1179 * expanded. This provides a more pleasant user experience. If this flag is
1180 * ste to true, the Tree will "scroll" the peer of an expanded node into
1181 * view and if it has not peer then it will "scroll" the last child of the
1182 * expanded node into view.
1183 * <p>
1184 * If you child node counts are quite high, you may not want this behaviour
1185 * and hence you should set it to false.
1186 *
1187 * @param newValue
1188 * a new boolean flag value
1189 */
1190 public void setScrollIntoViewUsed(boolean newValue) {
1191 setProperty(PROPERTY_SCROLL_INTO_VIEW_USED, newValue);
1192 }
1193
1194 /**
1195 * Returns TRUE of TreeNode's with 'null' action commands will still raise
1196 * events by bubbling up the Tree.
1197 *
1198 * @return boolean
1199 */
1200 public boolean isNullActionCommandsRaiseEvents() {
1201 return getProperty(PROPERTY_NULL_ACTION_COMMANDS_RAISE_EVENTS, true);
1202 }
1203
1204 /**
1205 * Returns true if the item identified by the path is currently selected.
1206 *
1207 * @param path
1208 * a TreePath identifying a node
1209 * @return true if the node is selected
1210 */
1211 public boolean isPathSelected(TreePath path) {
1212 return getSelectionModel().isPathSelected(path);
1213 }
1214
1215 /**
1216 * Returns true if the root node of the tree is displayed.
1217 *
1218 */
1219 public boolean isRootVisible() {
1220 return getProperty(PROPERTY_ROOT_VISIBLE, true);
1221 }
1222
1223 /**
1224 * Returns true if the root node of the tree is expanded when the TreeModel
1225 * is first set.
1226 *
1227 */
1228 public boolean isRootAutoExpanded() {
1229 return getProperty(PROPERTY_ROOT_AUTO_EXPANDED, true);
1230 }
1231
1232 /**
1233 * Returns true if the selection is currently empty.
1234 *
1235 * @return true if the selection is currently empty
1236 */
1237 public boolean isSelectionEmpty() {
1238 return getSelectionModel().isSelectionEmpty();
1239 }
1240
1241 /**
1242 * Returns true if the Tree cells use as much UI width as possible or false
1243 * if the cells try to contrain their UI width.
1244 *
1245 * @return true if the Tree cells use as much UI width as possible or false
1246 * if the cells try to contrain their UI width.
1247 */
1248 public boolean isCellWidthConstrained() {
1249 return getProperty(PROPERTY_CELL_WIDTH_CONTRAINED, false);
1250 }
1251
1252 /**
1253 * Set to true if the Tree cells use as much UI width as possible or false
1254 * if the cells try to contrain their UI width.
1255 *
1256 * @param newValue -
1257 * true if the Tree cells use as much UI width as possible or
1258 * false if the cells try to contrain their UI width.
1259 */
1260 public void setCellWidthConstrained(boolean newValue) {
1261 setProperty(PROPERTY_CELL_WIDTH_CONTRAINED, newValue);
1262 }
1263
1264 /**
1265 * @return true if the selection of a tree node includes the area around the
1266 * node icon or false if it does not.
1267 */
1268 public boolean isSelectionIncludesIcon() {
1269 return getProperty(PROPERTY_SELECTION_INCLUDES_ICON, true);
1270 }
1271
1272 /**
1273 * Set to true if the selection of a tree node includes the area around the
1274 * node icon or false if it does not.
1275 *
1276 * @param newValue -
1277 * true if the selection of a tree node includes the area around
1278 * the node icon or false if it does not.
1279 */
1280 public void setSelectionIncludesIcon(boolean newValue) {
1281 setProperty(PROPERTY_SELECTION_INCLUDES_ICON, newValue);
1282 }
1283
1284 /**
1285 * Returns true if the value identified by path is currently viewable, which
1286 * means it is either the root or all of its parents are exapnded ,
1287 * Otherwise, this method returns false.
1288 *
1289 * @return true if the node is viewable, otherwise false
1290 */
1291 public boolean isVisible(TreePath path) {
1292 if (path != null) {
1293 TreePath parentPath = path.getParentPath();
1294 if (parentPath != null)
1295 return isExpanded(parentPath);
1296 return true;
1297 }
1298 return false;
1299 }
1300
1301 /**
1302 * Ensures that the node identified by path is currently viewable.
1303 *
1304 * @param path
1305 * the TreePath to make visible
1306 */
1307 public void makeVisible(TreePath path) {
1308 if (path != null) {
1309 TreePath parentPath = path.getParentPath();
1310 if (parentPath != null) {
1311 expandPath(parentPath);
1312 }
1313 }
1314 }
1315
1316 /**
1317 * Removes an <code>ActionListener</code>.
1318 *
1319 * @param l
1320 * The <code>ActionListener</code> to be removed.
1321 */
1322 public void removeActionListener(ActionListener l) {
1323 getEventListenerList().removeListener(ActionListener.class, l);
1324 }
1325
1326 /**
1327 * Removes any descendants of the TreePaths in <code>toRemove</code> that
1328 * have been expanded.
1329 */
1330 protected void removeDescendantToggledPaths(Enumeration toRemove) {
1331 if (toRemove != null) {
1332 while (toRemove.hasMoreElements()) {
1333 Enumeration descendants = getDescendantToggledPaths((TreePath) toRemove.nextElement());
1334
1335 if (descendants != null) {
1336 while (descendants.hasMoreElements()) {
1337 expandedState.remove(descendants.nextElement());
1338 }
1339 }
1340 }
1341 }
1342 }
1343
1344 /**
1345 * Removes the node identified by the specified path from the current
1346 * selection.
1347 *
1348 * @param path
1349 * the TreePath identifying a node
1350 */
1351 public void removeSelectionPath(TreePath path) {
1352 this.getSelectionModel().removeSelectionPath(path);
1353 }
1354
1355 /**
1356 * Removes the nodes identified by the specified paths from the current
1357 * selection.
1358 *
1359 * @param paths
1360 * an array of TreePath objects that specifies the nodes to
1361 * remove
1362 */
1363 public void removeSelectionPaths(TreePath[] paths) {
1364 this.getSelectionModel().removeSelectionPaths(paths);
1365 }
1366
1367 /**
1368 * Removes a listener for TreeExpansion events.
1369 *
1370 * @param tel
1371 * the TreeExpansionListener to remove
1372 */
1373 public void removeTreeExpansionListener(TreeExpansionListener tel) {
1374 getEventListenerList().removeListener(TreeExpansionListener.class, tel);
1375 }
1376
1377 /**
1378 * Removes a TreeSelection listener.
1379 *
1380 * @param tsl
1381 * the TreeSelectionListener to remove
1382 */
1383 public void removeTreeSelectionListener(TreeSelectionListener tsl) {
1384 getEventListenerList().removeListener(TreeSelectionListener.class, tsl);
1385 if (getEventListenerList().getListenerCount(TreeSelectionListener.class) == 0 && selectionForwarder != null) {
1386 getSelectionModel().removeTreeSelectionListener(selectionForwarder);
1387 selectionForwarder = null;
1388 }
1389 }
1390
1391 /**
1392 * Removes a listener for TreeWillExpand events.
1393 *
1394 * @param tel
1395 * the TreeWillExpandListener to remove
1396 */
1397 public void removeTreeWillExpandListener(TreeWillExpandListener tel) {
1398 getEventListenerList().removeListener(TreeWillExpandListener.class, tel);
1399 }
1400
1401 /**
1402 * Sets the action command string of the Tree. Note that action commands
1403 * bubble up from the <code>TreeNode</code>, via their parents to the
1404 * Tree itself.
1405 *
1406 * @param newActionCommand
1407 * String
1408 */
1409 public void setActionCommand(String newActionCommand) {
1410 setProperty(PROPERTY_ACTION_COMMAND, newActionCommand);
1411 }
1412
1413 /**
1414 * Sets the TreeCellRenderer that will be used to draw each cell.
1415 * <p>
1416 * NOTE : Some TreeCellRender implementations such as
1417 * DefaultTreeeCellRenderer are implemented as a Component. While this is
1418 * perfectly valid, please note that the implementation is not added a a
1419 * child of Tree and hence inheritied styles from the component hierarchy do
1420 * not apply. Not adding the implementation as a child component allows for
1421 * "static" implementations of TreeCellRender that are derived from
1422 * Component.
1423 *
1424 * @param newCellRenderer
1425 * the TreeCellRenderer that is to render each cell
1426 * @see echopointng.tree.TreeCellRenderer
1427 */
1428 public void setCellRenderer(TreeCellRenderer newCellRenderer) {
1429 if (newCellRenderer == null) {
1430 throw new IllegalArgumentException("The TreeCellRenderer must not be null");
1431 }
1432 if (!newCellRenderer.equals(getCellRenderer())) {
1433 setProperty(PROPERTY_CELL_RENDERER, newCellRenderer);
1434 componentMap.clear(); // must clear this as new Components could
1435 // be generated
1436 invalidate();
1437 }
1438 }
1439
1440 /**
1441 * @see nextapp.echo2.app.Component#setEnabled(boolean)
1442 */
1443 public void setEnabled(boolean newValue) {
1444 if (newValue != isEnabled()) {
1445 super.setEnabled(newValue);
1446 invalidate();
1447 }
1448 }
1449
1450 /**
1451 * Sets whether lines are drawn between tree nodes
1452 *
1453 * @param newDrawLines
1454 * boolean
1455 */
1456 public void setLinesDrawn(boolean newDrawLines) {
1457 setProperty(PROPERTY_LINES_DRAWN, Boolean.valueOf(newDrawLines));
1458 }
1459
1460 /**
1461 * If this is set to true, the Tree rendering code is given a hint on
1462 * whether it can perform partial updates to speed up visual performance. By
1463 * default this is true. Partial update support costs more in terms of
1464 * server memory because the old state must be tracked. But it can result in
1465 * better visual performance.
1466 *
1467 * @param newValue -
1468 * a true value cause partial updates to be used by the rendering
1469 */
1470 public void setPartialUpdateSupport(boolean newValue) {
1471 setProperty(PROPERTY_PARTIAL_UPDATE_SUPPORT, Boolean.valueOf(newValue));
1472 }
1473
1474 /**
1475 * Returns true if the partial update support is currently enabled.
1476 * <p>
1477 * If this is set to true, the Tree rendering code is given a hint on
1478 * whether it can perform partial updates to speed up visual performance. By
1479 * default this is true. Partial update support costs more in terms of
1480 * server memory because the old state must be tracked. But it can result in
1481 * better visual performance.
1482 *
1483 * @return true if the partial update support is currently enabled.
1484 */
1485 public boolean getPartialUpdateSupport() {
1486 return getProperty(PROPERTY_PARTIAL_UPDATE_SUPPORT, true);
1487 }
1488
1489 /**
1490 * Sets the TreeModel that will provide the data.
1491 *
1492 * @param newTreeModel -
1493 * the TreeModel that is to provide the data
1494 */
1495 public void setModel(TreeModel newTreeModel) {
1496 TreeModel oldModel = getModel();
1497
1498 if (newTreeModel == null || !newTreeModel.equals(oldModel)) {
1499
1500 if (oldModel != null && treeModelListener != null) {
1501 oldModel.removeTreeModelListener(treeModelListener);
1502 }
1503
1504 setProperty(PROPERTY_MODEL, newTreeModel);
1505 clearToggledPaths();
1506
1507 if (newTreeModel != null) {
1508 if (treeModelListener == null) {
1509 treeModelListener = createTreeModelListener();
1510 }
1511 if (treeModelListener != null) {
1512 newTreeModel.addTreeModelListener(treeModelListener);
1513 }
1514 if (newTreeModel.getRoot() != null) {
1515 if (isRootAutoExpanded() && !newTreeModel.isLeaf(newTreeModel.getRoot())) {
1516 expandedState.put(new TreePath(newTreeModel.getRoot()), Boolean.TRUE);
1517 }
1518 }
1519 }
1520
1521 invalidate();
1522 }
1523 }
1524
1525 /**
1526 * Sets whether TreeNode null action commands will cause an event to be
1527 * raised.
1528 *
1529 * @param newValue
1530 * boolean
1531 */
1532 public void setNullActionCommandsRaiseEvents(boolean newValue) {
1533 setProperty(PROPERTY_NULL_ACTION_COMMANDS_RAISE_EVENTS, newValue);
1534 }
1535
1536 /**
1537 * Determines whether or not the root node from the TreeModel is visible.
1538 *
1539 */
1540 public void setRootVisible(boolean rootVisible) {
1541 if (rootVisible != isRootVisible()) {
1542 setProperty(PROPERTY_ROOT_VISIBLE, rootVisible);
1543 invalidate();
1544 }
1545 }
1546
1547 /**
1548 * Determines whether or not the root node from the TreeModel is
1549 * automatically expanded when the model is set into the Tree.
1550 * <p>
1551 * By default it is and hence the 2nd child level of the TreeModel will be
1552 * accessed and shown.
1553 * <p>
1554 * If this is false, then only the root node will be accessed and displayed
1555 * when a TreeModel is first set and the Tree is rendered.
1556 *
1557 */
1558 public void setRootAutoExpanded(boolean rootExpanded) {
1559 if (rootExpanded != isRootAutoExpanded()) {
1560 setProperty(PROPERTY_ROOT_AUTO_EXPANDED, rootExpanded);
1561 invalidate();
1562 }
1563 }
1564
1565 /**
1566 * Sets the row height to be used for each row within the Tree. If this
1567 * value is 0 or less, then no row height will be used.
1568 *
1569 * @param newRowHeight
1570 * int
1571 */
1572 public void setRowHeight(int newRowHeight) {
1573 setProperty(PROPERTY_ROW_HEIGHT, newRowHeight);
1574 }
1575
1576 /**
1577 * Sets the tree's selection model. When a null value is specified an empty
1578 * electionModel is used, which does not allow selections.
1579 *
1580 */
1581 public void setSelectionModel(TreeSelectionModel newSelectionModel) {
1582 if (newSelectionModel == null) {
1583 newSelectionModel = EmptyTreeSelectionModel.getInstance();
1584 }
1585
1586 TreeSelectionModel oldValue = getSelectionModel();
1587
1588 if (!newSelectionModel.equals(oldValue)) {
1589 if (oldValue != null && selectionForwarder != null) {
1590 oldValue.removeTreeSelectionListener(selectionForwarder);
1591 }
1592
1593 setProperty(PROPERTY_SELECTION_MODEL, newSelectionModel);
1594
1595 if (selectionForwarder == null) {
1596 selectionForwarder = new TreeSelectionForwarder();
1597 }
1598 newSelectionModel.addTreeSelectionListener(selectionForwarder);
1599
1600 invalidate();
1601 }
1602 }
1603
1604 /**
1605 * Selects the node identified by the specified path. If any component of
1606 * the path is hidden (under a collapsed node), it is exposed (made
1607 * viewable).
1608 *
1609 * @param path
1610 * the TreePath specifying the node to select
1611 */
1612 public void setSelectionPath(TreePath path) {
1613 makeVisible(path);
1614 getSelectionModel().setSelectionPath(path);
1615 }
1616
1617 /**
1618 * Selects the nodes identified by the specified array of paths. If any
1619 * component in any of the paths is hidden (under a collapsed node), it is
1620 * exposed (made viewable).
1621 *
1622 * @param paths
1623 * an array of TreePath objects that specifies the nodes to
1624 * select
1625 */
1626 public void setSelectionPaths(TreePath[] paths) {
1627 if (paths != null) {
1628 for (int counter = paths.length - 1; counter >= 0; counter--)
1629 makeVisible(paths[counter]);
1630 }
1631 getSelectionModel().setSelectionPaths(paths);
1632 }
1633
1634 /**
1635 * Determines whether the node handles are to be displayed.
1636 *
1637 */
1638 public void setShowsRootHandles(boolean newValue) {
1639 setProperty(PROPERTY_SHOWS_ROOT_HANDLES, newValue);
1640 TreeModel model = getModel();
1641 if (model != null) {
1642 if (model.getRoot() != null) {
1643 expandPath(new TreePath(model.getRoot()));
1644 }
1645 }
1646 }
1647
1648 /**
1649 * Sets the TreeIcons being used by the Tree
1650 *
1651 * @param newTreeIcons -
1652 * the new icons to use
1653 * @see TreeIcons
1654 */
1655 public void setTreeIcons(TreeIcons newTreeIcons) {
1656 if (newTreeIcons == null) {
1657 throw new IllegalArgumentException("Non null TreeIcons required");
1658 }
1659 if (!newTreeIcons.equals(getTreeIcons())) {
1660 setProperty(PROPERTY_TREE_ICONS, newTreeIcons);
1661 invalidate();
1662 }
1663 }
1664
1665 /**
1666 * @see nextapp.echo2.app.Component#setVisible(boolean)
1667 */
1668 public void setVisible(boolean newValue) {
1669 if (newValue != isVisible()) {
1670 super.setVisible(newValue);
1671 invalidate();
1672 }
1673 }
1674
1675 /**
1676 * Toggles all nodes as expanded or collapsed depending on <i>expand </i>
1677 * starting from <i>parentPath</i>.
1678 */
1679 public void toggleAllNodes(TreePath parentPath, boolean expand) {
1680 Object parent = parentPath.getLastPathComponent();
1681 toggleAllNodesImpl(parent, parentPath, expand);
1682 }
1683
1684 /*
1685 * And implementation that keeps track of the TreePaths so we dont have to
1686 * call getPathToRoot() on the model.
1687 */
1688 private void toggleAllNodesImpl(Object parent, TreePath parentPath, boolean expand) {
1689
1690 TreeModel model = getModel();
1691 if (model == null) {
1692 return;
1693 }
1694
1695 if (!model.isLeaf(parent)) {
1696 int cc = model.getChildCount(parent);
1697
1698 for (int i = 0; i < cc; i++) {
1699 Object node = model.getChild(parent, i);
1700 TreePath childPath = new TreePath(parentPath, node);
1701
1702 if (!model.isLeaf(node)) {
1703 toggleAllNodesImpl(node, childPath, expand);
1704 } else {
1705 // must be a tip of the trunk so expand/collapse it
1706 setExpandedState(childPath, expand);
1707 }
1708 }
1709
1710 // If we are collapsing all then we need to collapse each branch
1711 // node rather than just the leaves
1712 if (!expand) {
1713 setExpandedState(parentPath, false);
1714 }
1715 }
1716 }
1717
1718 /**
1719 * Creates and returns an instance of TreeModelListener. The returned object
1720 * is responsible for updating the expanded state when the TreeModel
1721 * changes.
1722 */
1723 protected TreeModelListener createTreeModelListener() {
1724 return new TreeModelHandler();
1725 }
1726
1727 /**
1728 * Clears the cache of toggled tree paths. This does NOT send out any
1729 * TreeExpansionListener events.
1730 */
1731 protected void clearToggledPaths() {
1732 if (expandedState != null)
1733 expandedState.clear();
1734 }
1735
1736 /**
1737 * Notify all listeners that have registered interest for notification on
1738 * this event type.
1739 *
1740 * @param e
1741 * the TreeSelectionEvent generated by the TreeSelectionModel
1742 * when a node is selected or deselected
1743 */
1744 protected void fireValueChanged(TreeSelectionEvent e) {
1745 Object[] listeners = getEventListenerList().getListeners(TreeSelectionListener.class);
1746 for (int i = 0; i < listeners.length; i++) {
1747 ((TreeSelectionListener) listeners[i]).valueChanged(e);
1748 }
1749 }
1750
1751 /**
1752 * Returns an Enumeration of TreePaths that have been expanded that are
1753 * descendants of <code>parent</code>.
1754 */
1755 protected Enumeration getDescendantToggledPaths(TreePath parent) {
1756 if (parent == null)
1757 return null;
1758
1759 Vector descendants = new Vector();
1760 Enumeration nodes = expandedState.keys();
1761 TreePath path;
1762
1763 while (nodes.hasMoreElements()) {
1764 path = (TreePath) nodes.nextElement();
1765 if (parent.isDescendant(path))
1766 descendants.addElement(path);
1767 }
1768 return descendants.elements();
1769 }
1770
1771 /**
1772 * Sets the expanded state of the path. If <code>state</code> is true, all
1773 * parents of <code>path</code> as well as <code>path</code> are marked
1774 * as expanded.
1775 * <p>
1776 * If <code>state</code> is false, all parents of <code>path</code> are
1777 * marked EXPANDED, but <code>path</code> itself is marked collapsed.
1778 * <p>
1779 * This will fail if a TreeWillExpandListener vetos it.
1780 */
1781 protected void setExpandedState(TreePath path, boolean state) {
1782 if (path != null) {
1783 // Make sure all parents of path are expanded.
1784 Stack stack;
1785 TreePath parentPath = path.getParentPath();
1786 if (expandedStack.size() == 0) {
1787 stack = new Stack();
1788 } else {
1789 stack = (Stack) expandedStack.pop();
1790 }
1791 try {
1792 while (parentPath != null) {
1793 if (isExpanded(parentPath)) {
1794 parentPath = null;
1795 } else {
1796 stack.push(parentPath);
1797 parentPath = parentPath.getParentPath();
1798 }
1799 }
1800 for (int counter = stack.size() - 1; counter >= 0; counter--) {
1801 parentPath = (TreePath) stack.pop();
1802 if (!isExpanded(parentPath)) {
1803 try {
1804 fireTreeWillExpand(parentPath);
1805 } catch (ExpandVetoException eve) {
1806 return;
1807 }
1808 expandedState.put(parentPath, Boolean.TRUE);
1809 fireTreeExpanded(parentPath);
1810 }
1811 }
1812 } finally {
1813 if (expandedStack.size() < TEMP_STACK_SIZE) {
1814 stack.removeAllElements();
1815 expandedStack.push(stack);
1816 }
1817 }
1818 if (!state) {
1819 // collapse last path.
1820 Object cValue = expandedState.get(path);
1821 if (cValue != null && ((Boolean) cValue).booleanValue()) {
1822 try {
1823 fireTreeWillCollapse(path);
1824 } catch (ExpandVetoException eve) {
1825 return;
1826 }
1827 expandedState.put(path, Boolean.FALSE);
1828 fireTreeCollapsed(path);
1829 }
1830 } else {
1831 // Expand last path.
1832 Object cValue = expandedState.get(path);
1833
1834 if (cValue == null || !((Boolean) cValue).booleanValue()) {
1835 try {
1836 fireTreeWillExpand(path);
1837 } catch (ExpandVetoException eve) {
1838 return;
1839 }
1840 expandedState.put(path, Boolean.TRUE);
1841 fireTreeExpanded(path);
1842 }
1843 }
1844 }
1845 }
1846
1847 /**
1848 * Returns true if the hash table contains the nominated path or equivalent.
1849 */
1850 private boolean orderContainsTreePath(Hashtable hash, TreePath path) {
1851 for (Enumeration enumeration = hash.elements(); enumeration.hasMoreElements();) {
1852 TreePath testPath = (TreePath) enumeration.nextElement();
1853 if (testPath.equals(path))
1854 return true;
1855 }
1856 return false;
1857 }
1858
1859 /**
1860 * Orders an enumeration of paths for use within the RowMapper. It makes a
1861 * breadth first descent of the tree and finds each node in the Enumeration
1862 * and its it in an Vector, ordered from the top down starting from parent
1863 * path.
1864 */
1865 private Vector orderPathEnumeration(TreePath parentPath, Enumeration enumeration) {
1866
1867 Hashtable unsortedSet = new Hashtable();
1868 Vector sortedSet = new Vector();
1869 if (getModel() == null)
1870 return sortedSet;
1871
1872 for (; enumeration.hasMoreElements();) {
1873 TreePath path = (TreePath) enumeration.nextElement();
1874 unsortedSet.put(path.toString(), path);
1875 }
1876
1877 // down throught the tree please
1878 if (orderContainsTreePath(unsortedSet, parentPath))
1879 sortedSet.add(parentPath);
1880 orderPathSet(parentPath, unsortedSet, sortedSet);
1881
1882 return sortedSet;
1883 }
1884
1885 /**
1886 * Recursive function to travel down the tree and find tree paths within the
1887 * unsorted set and put them in breathfirst order with the sorted set.
1888 */
1889 private void orderPathSet(TreePath parentPath, Hashtable unsortedSet, Vector sortedSet) {
1890
1891 // now run down the whole tree, starting at parent, and
1892 // add any elements we find as we find them.
1893 Object node = parentPath.getLastPathComponent();
1894 if (!getModel().isLeaf(node) && isExpanded(parentPath)) {
1895 int cc = getModel().getChildCount(node);
1896 for (int i = 0; i < cc; i++) {
1897 Object child = getModel().getChild(node, i);
1898
1899 TreePath targetPath = parentPath.pathByAddingChild(child);
1900 if (orderContainsTreePath(unsortedSet, targetPath))
1901 sortedSet.add(targetPath);
1902 if (!getModel().isLeaf(child)) {
1903 // recurse
1904 orderPathSet(targetPath, unsortedSet, sortedSet);
1905 }
1906 }
1907 }
1908 }
1909
1910 /**
1911 * Checks the given node to see if it contains a Component and adds it as a
1912 * child of the Tree. If the cell renderer returns text, then the component
1913 * call will not be made. This ensures that the TreeCellrenderer contract is
1914 * kept.
1915 */
1916 private void doComponentCheck(TreePath path, TreeModel model, Map referenceMap) {
1917 Object node = path.getLastPathComponent();
1918
1919 // Do not get a new component every time if it has already been created,
1920 // unless the path is dirty
1921 Component cellComponent = getComponent(node);
1922 if (cellComponent != null) {
1923
1924 if (dirtyPaths.contains(path)) {
1925 // Remove Component if being used to render this node
1926 componentMap.remove(node);
1927 remove(cellComponent);
1928
1929 } else {
1930 referenceMap.put(cellComponent, cellComponent);
1931 return;
1932 }
1933 }
1934
1935 boolean isExpanded = isExpanded(path);
1936 boolean isSelected = isPathSelected(path);
1937 boolean isLeaf = model.isLeaf(node);
1938
1939 Label l = getCellRenderer().getTreeCellRendererText(this, node, isSelected, isExpanded, isLeaf);
1940 if (l == null) {
1941 cellComponent = getCellRenderer().getTreeCellRendererComponent(this, node, isSelected, isExpanded, isLeaf);
1942 if (cellComponent != null) {
1943 if (!this.isAncestorOf(cellComponent)) {
1944 this.add(cellComponent);
1945 }
1946
1947 componentMap.put(node, new WeakReference(cellComponent));
1948 if (referenceMap != null) {
1949 referenceMap.put(cellComponent, cellComponent);
1950 }
1951 }
1952 }
1953 }
1954
1955 /**
1956 * Recursive call to validate a TreePath. All reference components are
1957 * placed in the componentMap key by the Tree node and all referenced
1958 * components are also placed in the referenceMap keyed by the component.
1959 *
1960 * The referenceMap may be null in which case we dont use it.
1961 */
1962 private void doPathValidation(TreePath path, Map referenceMap) {
1963 if (path == null)
1964 return;
1965
1966 TreeModel model = getModel();
1967 //
1968 // check if the node has a component that needs to placed in the
1969 // hierarchy
1970 //
1971 doComponentCheck(path, model, referenceMap);
1972 //
1973 // find out if the current node is NOT a leaf and hence
1974 // we can go down to it.
1975 boolean isExpanded = isExpanded(path);
1976 Object node = path.getLastPathComponent();
1977 if (!model.isLeaf(node) && isExpanded) {
1978 int cc = model.getChildCount(node);
1979 for (int i = 0; i < cc; i++) {
1980 Object nodeChild = model.getChild(node, i);
1981 TreePath childPath = new TreePath(path, nodeChild);
1982 doComponentCheck(childPath, model, referenceMap);
1983 //
1984 // if we have a server side tree, then we dont want to invoke
1985 // the cell
1986 // renderer any more than we have to. Therefore we only go down
1987 // the
1988 // tree path if the path is visible and hence child components
1989 // might be
1990 // needed. We alway descend for a client side tree
1991 //
1992 isExpanded = isExpanded(childPath);
1993 if (isExpanded) {
1994 doPathValidation(childPath, referenceMap);
1995 }
1996 }
1997 }
1998 }
1999
2000 /**
2001 * Called just before the tree is rendered. We run through the tree nodes
2002 * and try and find any Components that may be rendered by the tree. These
2003 * will be the Components that are returned by the
2004 * TreeCellRenderer#getTreeCellRendererComponent(..) method.
2005 * <p>
2006 * We then add them as children of the tree (if they are not already
2007 * ancestors) so that a component rendering peer is created properly for
2008 * each cell.
2009 *
2010 */
2011 public void validate() {
2012 super.validate();
2013 if (!valid) {
2014 valid = true;
2015
2016 TreeModel model = getModel();
2017 if (model == null) {
2018 return;
2019 }
2020
2021 Object root = model.getRoot();
2022 if (root == null) {
2023 return;
2024 }
2025
2026 TreeCellRenderer renderer = getCellRenderer();
2027 if (renderer == null) {
2028 throw new IllegalStateException("The Tree has no TreeCellRenderer");
2029 }
2030
2031 HashMap referenceMap = new HashMap();
2032
2033 //
2034 // validate the tree path
2035 //
2036 TreePath path = new TreePath(root);
2037 doPathValidation(path, referenceMap);
2038
2039 //
2040 // if we have child components that are no longer referenced in the
2041 // model then we need to get rid of them. We use the referenceMap so
2042 // that can find components keyed by themselves.
2043 //
2044 // componentMap contains WeakReferences to components, keyed by
2045 // Nodes.
2046 // If the components are no longer in the referenceMap, remove them
2047 // from the componentMap
2048 Iterator iter = componentMap.values().iterator();
2049 while (iter.hasNext()) {
2050 WeakReference wr = (WeakReference) iter.next();
2051 Component component = (Component) wr.get();
2052 if (!referenceMap.containsKey(component)) {
2053 iter.remove();
2054 this.remove(component);
2055 }
2056 }
2057
2058 }
2059 }
2060
2061 /**
2062 * @return the collection of pending changed paths (needed for partial
2063 * updates when rendering)
2064 */
2065 public Collection getDirtyPaths() {
2066 return dirtyPaths;
2067 }
2068
2069 /**
2070 * Marks an array of nodes as being dirty - ie requiring re-rendering.
2071 * Should be called whenever the state of a node is changed or a node is
2072 * moved within the tree.
2073 *
2074 * @param parentPath -
2075 * current path to the parent of the nodes
2076 * @param nodes -
2077 * array of nodes to be marked dirty
2078 * @param markChildren -
2079 * whether to mark child nodes as dirty too (true when moving a
2080 * node)
2081 */
2082 private void markPathsDirty(TreePath parentPath, Object[] nodes, boolean markChildren) {
2083
2084 for (int i = nodes.length - 1; i >= 0; i--) {
2085 TreePath path = parentPath.pathByAddingChild(nodes[i]);
2086 markPathDirty(path, markChildren);
2087 }
2088 }
2089
2090 /**
2091 * Registers a node as being dirty - ie requiring re-rendering. Should be
2092 * called whenever the state of a node is changed or a node is moved within
2093 * the tree.
2094 *
2095 * @param path -
2096 * current path to the node
2097 * @param markChildren -
2098 * whether to mark child nodes as dirty too (true when moving a
2099 * node)
2100 */
2101 private void markPathDirty(TreePath path, boolean markChildren) {
2102
2103 invalidate();
2104
2105 if (!dirtyPaths.contains(path)) {
2106 dirtyPaths.add(path);
2107 firePropertyChange(NODE_CHANGED_PROPERTY, null, null); // Forces
2108 // partial
2109 // redraw
2110 }
2111
2112 Object node = path.getLastPathComponent();
2113
2114 // Recursively mark child paths as dirty if requested
2115 TreeModel model = getModel();
2116 if (markChildren && !model.isLeaf(node)) { // && isExpanded(path)
2117
2118 int cc = model.getChildCount(node);
2119 for (int i = 0; i < cc; i++) {
2120 Object nodeChild = model.getChild(node, i);
2121 markPathDirty(new TreePath(path, nodeChild), true);
2122 }
2123 }
2124
2125 }
2126
2127 }