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    }