001    /* DefaultTreeSelectionModel.java 
002       Copyright (C) 2002, 2004, 2005, 2006 Free Software Foundation, Inc.
003       
004    This file is part of GNU Classpath.
005    
006    GNU Classpath is free software; you can redistribute it and/or modify
007    it under the terms of the GNU General Public License as published by
008    the Free Software Foundation; either version 2, or (at your option)
009    any later version.
010    
011    GNU Classpath is distributed in the hope that it will be useful, but
012    WITHOUT ANY WARRANTY; without even the implied warranty of
013    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
014    General Public License for more details.
015    
016    You should have received a copy of the GNU General Public License
017    along with GNU Classpath; see the file COPYING.  If not, write to the
018    Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
019    02110-1301 USA.
020    
021    Linking this library statically or dynamically with other modules is
022    making a combined work based on this library.  Thus, the terms and
023    conditions of the GNU General Public License cover the whole
024    combination.
025    
026    As a special exception, the copyright holders of this library give you
027    permission to link this library with independent modules to produce an
028    executable, regardless of the license terms of these independent
029    modules, and to copy and distribute the resulting executable under
030    terms of your choice, provided that you also meet, for each linked
031    independent module, the terms and conditions of the license of that
032    module.  An independent module is a module which is not derived from
033    or based on this library.  If you modify this library, you may extend
034    this exception to your version of the library, but you are not
035    obligated to do so.  If you do not wish to do so, delete this
036    exception statement from your version. */
037    
038    
039    package javax.swing.tree;
040    
041    import java.beans.PropertyChangeListener;
042    import java.io.IOException;
043    import java.io.ObjectInputStream;
044    import java.io.ObjectOutputStream;
045    import java.io.Serializable;
046    import java.util.Arrays;
047    import java.util.BitSet;
048    import java.util.EventListener;
049    import java.util.HashSet;
050    import java.util.Iterator;
051    import java.util.Vector;
052    
053    import javax.swing.DefaultListSelectionModel;
054    import javax.swing.event.EventListenerList;
055    import javax.swing.event.SwingPropertyChangeSupport;
056    import javax.swing.event.TreeSelectionEvent;
057    import javax.swing.event.TreeSelectionListener;
058    
059    /**
060     * The implementation of the default tree selection model. The installed
061     * listeners are notified about the path and not the row changes. If you
062     * specifically need to track the row changes, register the listener for the
063     * expansion events.
064     * 
065     * @author Andrew Selkirk
066     * @author Audrius Meskauskas
067     */
068    public class DefaultTreeSelectionModel
069        implements Cloneable, Serializable, TreeSelectionModel
070    {
071    
072      /**
073       * According to the API docs, the method
074       * {@link DefaultTreeSelectionModel#notifyPathChange} should
075       * expect instances of a class PathPlaceHolder in the Vector parameter.
076       * This seems to be a non-public class, so I can only make guesses about the
077       * use of it.
078       */
079      private static class PathPlaceHolder
080      {
081        /**
082         * The path that we wrap.
083         */
084        TreePath path;
085    
086        /**
087         * Indicates if the path is new or already in the selection.
088         */
089        boolean isNew;
090    
091        /**
092         * Creates a new instance.
093         *
094         * @param p the path to wrap
095         * @param n if the path is new or already in the selection
096         */
097        PathPlaceHolder(TreePath p, boolean n)
098        {
099          path = p;
100          isNew = n;
101        }
102      }
103    
104      /**
105       * Use serialVersionUID for interoperability.
106       */
107      static final long serialVersionUID = 3288129636638950196L;
108    
109      /**
110       * The name of the selection mode property.
111       */
112      public static final String SELECTION_MODE_PROPERTY = "selectionMode";
113    
114      /**
115       * Our Swing property change support.
116       */
117      protected SwingPropertyChangeSupport changeSupport;
118    
119      /**
120       * The current selection.
121       */
122      protected TreePath[] selection;
123    
124      /**
125       * Our TreeSelectionListeners.
126       */
127      protected EventListenerList listenerList;
128    
129      /**
130       * The current RowMapper.
131       */
132      protected transient RowMapper rowMapper;
133    
134      /**
135       * The current listSelectionModel.
136       */
137      protected DefaultListSelectionModel listSelectionModel;
138    
139      /**
140       * The current selection mode.
141       */
142      protected int selectionMode;
143    
144      /**
145       * The path that has been added last.
146       */
147      protected TreePath leadPath;
148    
149      /**
150       * The index of the last added path.
151       */
152      protected int leadIndex;
153    
154      /**
155       * The row of the last added path according to the RowMapper.
156       */
157      protected int leadRow = -1;
158    
159      /**
160       * A supporting datastructure that is used in addSelectionPaths() and
161       * removeSelectionPaths(). It contains currently selected paths.
162       *
163       * @see #addSelectionPaths(TreePath[])
164       * @see #removeSelectionPaths(TreePath[])
165       * @see #setSelectionPaths(TreePath[])
166       */
167      private transient HashSet selectedPaths;
168    
169      /**
170       * A supporting datastructure that is used in addSelectionPaths() and
171       * removeSelectionPaths(). It contains the paths that are added or removed.
172       *
173       * @see #addSelectionPaths(TreePath[])
174       * @see #removeSelectionPaths(TreePath[])
175       * @see #setSelectionPaths(TreePath[])
176       */
177      private transient HashSet tmpPaths;
178    
179      /**
180       * Constructs a new DefaultTreeSelectionModel.
181       */
182      public DefaultTreeSelectionModel()
183      {
184        setSelectionMode(DISCONTIGUOUS_TREE_SELECTION);
185        listSelectionModel = new DefaultListSelectionModel();
186        listenerList = new EventListenerList();
187        leadIndex = -1;
188        tmpPaths = new HashSet();
189        selectedPaths = new HashSet();
190      }
191    
192      /**
193       * Creates a clone of this DefaultTreeSelectionModel with the same selection.
194       * The cloned instance will have the same registered listeners, the listeners
195       * themselves will not be cloned. The selection will be cloned.
196       * 
197       * @exception CloneNotSupportedException should not be thrown here
198       * @return a copy of this DefaultTreeSelectionModel
199       */
200      public Object clone() throws CloneNotSupportedException
201      {
202        DefaultTreeSelectionModel cloned = 
203          (DefaultTreeSelectionModel) super.clone();
204        cloned.changeSupport = null;
205        cloned.selection = (TreePath[]) selection.clone();
206        cloned.listenerList = new EventListenerList();
207        cloned.listSelectionModel =
208          (DefaultListSelectionModel) listSelectionModel.clone();
209        cloned.selectedPaths = new HashSet();
210        cloned.tmpPaths = new HashSet();
211    
212        return cloned;
213      }
214    
215      /**
216       * Returns a string that shows this object's properties.
217       * The returned string lists the selected tree rows, if any.
218       * 
219       * @return a string that shows this object's properties
220       */
221      public String toString() 
222      {
223        if (isSelectionEmpty())
224          return "[selection empty]";
225        else
226          {
227            StringBuffer b = new StringBuffer("selected rows: [");
228            for (int i = 0; i < selection.length; i++)
229              {
230                b.append(getRow(selection[i]));
231                b.append(' ');
232              }
233            b.append(", lead " + getLeadSelectionRow());
234            return b.toString();
235          }
236      }
237    
238      /**
239       * writeObject
240       * 
241       * @param value0 TODO
242       * @exception IOException TODO
243       */
244      private void writeObject(ObjectOutputStream value0) throws IOException
245      {
246        // TODO
247      }
248    
249      /**
250       * readObject
251       * 
252       * @param value0 TODO
253       * @exception IOException TODO
254       * @exception ClassNotFoundException TODO
255       */
256      private void readObject(ObjectInputStream value0) throws IOException,
257          ClassNotFoundException
258      {
259        // TODO
260      }
261    
262      /**
263       * Sets the RowMapper that should be used to map between paths and their rows.
264       * 
265       * @param mapper the RowMapper to set
266       * @see RowMapper
267       */
268      public void setRowMapper(RowMapper mapper)
269      {
270        rowMapper = mapper;
271        resetRowSelection();
272      }
273    
274      /**
275       * Returns the RowMapper that is currently used to map between paths and their
276       * rows.
277       * 
278       * @return the current RowMapper
279       * @see RowMapper
280       */
281      public RowMapper getRowMapper()
282      {
283        return rowMapper;
284      }
285    
286      /**
287       * Sets the current selection mode. Possible values are
288       * {@link #SINGLE_TREE_SELECTION}, {@link #CONTIGUOUS_TREE_SELECTION} and
289       * {@link #DISCONTIGUOUS_TREE_SELECTION}.
290       * 
291       * @param mode the selection mode to be set
292       * @see #getSelectionMode
293       * @see #SINGLE_TREE_SELECTION
294       * @see #CONTIGUOUS_TREE_SELECTION
295       * @see #DISCONTIGUOUS_TREE_SELECTION
296       */
297      public void setSelectionMode(int mode)
298      {
299        int oldMode = selectionMode;
300        selectionMode = mode;
301        // Make sure we have a valid selection mode.
302        if (selectionMode != SINGLE_TREE_SELECTION
303            && selectionMode != CONTIGUOUS_TREE_SELECTION
304            && selectionMode != DISCONTIGUOUS_TREE_SELECTION)
305          selectionMode = DISCONTIGUOUS_TREE_SELECTION;
306    
307        // Fire property change event.
308        if (oldMode != selectionMode && changeSupport != null)
309          changeSupport.firePropertyChange(SELECTION_MODE_PROPERTY, oldMode,
310                                           selectionMode);
311      }
312    
313      /**
314       * Returns the current selection mode.
315       * 
316       * @return the current selection mode
317       * @see #setSelectionMode
318       * @see #SINGLE_TREE_SELECTION
319       * @see #CONTIGUOUS_TREE_SELECTION
320       * @see #DISCONTIGUOUS_TREE_SELECTION
321       */
322      public int getSelectionMode()
323      {
324        return selectionMode;
325      }
326    
327      /**
328       * Sets this path as the only selection. If this changes the selection the
329       * registered TreeSelectionListeners are notified.
330       * 
331       * @param path the path to set as selection
332       */
333      public void setSelectionPath(TreePath path)
334      {
335        TreePath[] paths = null;
336        if (path != null)
337          paths = new TreePath[]{ path };
338        setSelectionPaths(paths);
339      }
340      
341      /**
342       * Get the number of the tree row for the given path.
343       * 
344       * @param path the tree path
345       * @return the tree row for this path or -1 if the path is not visible.
346       */
347      int getRow(TreePath path)
348      {
349        RowMapper mapper = getRowMapper();
350    
351        if (mapper instanceof AbstractLayoutCache)
352          {
353            // The absolute majority of cases, unless the TreeUI is very
354            // seriously rewritten
355            AbstractLayoutCache ama = (AbstractLayoutCache) mapper;
356            return ama.getRowForPath(path);
357          }
358        else if (mapper != null)
359          {
360            // Generic non optimized implementation.
361            int[] rows = mapper.getRowsForPaths(new TreePath[] { path });
362            if (rows.length == 0)
363              return - 1;
364            else
365              return rows[0];
366          }
367        return -1;
368      }
369    
370      /**
371       * Sets the paths as selection. This method checks for duplicates and removes
372       * them. If this changes the selection the registered TreeSelectionListeners
373       * are notified.
374       * 
375       * @param paths the paths to set as selection
376       */
377      public void setSelectionPaths(TreePath[] paths)
378      {
379        int oldLength = 0;
380        if (selection != null)
381          oldLength = selection.length;
382        int newLength = 0;
383        if (paths != null)
384          newLength = paths.length;
385        if (newLength > 0 || oldLength > 0)
386          {
387            // For SINGLE_TREE_SELECTION and for CONTIGUOUS_TREE_SELECTION with
388            // a non-contiguous path, we only allow the first path element.
389            if ((selectionMode == SINGLE_TREE_SELECTION && newLength > 1)
390                || (selectionMode == CONTIGUOUS_TREE_SELECTION && newLength > 0
391                    && ! arePathsContiguous(paths)))
392              {
393                paths = new TreePath[] { paths[0] };
394                newLength = 1;
395              }
396            // Find new paths.
397            Vector changedPaths = null;
398            tmpPaths.clear();
399            int validPaths = 0;
400            TreePath oldLeadPath = leadPath;
401            for (int i = 0; i < newLength; i++)
402              {
403                if (paths[i] != null && ! tmpPaths.contains(paths[i]))
404                  {
405                    validPaths++;
406                    tmpPaths.add(paths[i]);
407                    if (! selectedPaths.contains(paths[i]))
408                      {
409                        if (changedPaths == null)
410                          changedPaths = new Vector();
411                        changedPaths.add(new PathPlaceHolder(paths[i], true));
412                      }
413                    leadPath = paths[i];
414                  }
415              }
416            // Put together the new selection.
417            TreePath[] newSelection = null;
418            if (validPaths != 0)
419              {
420                if (validPaths != newLength)
421                  {
422                    // Some of the paths are already selected, put together
423                    // the new selection carefully.
424                    newSelection = new TreePath[validPaths];
425                    Iterator newPaths = tmpPaths.iterator();
426                    validPaths = 0;
427                    for (int i = 0; newPaths.hasNext(); i++)
428                      newSelection[i] = (TreePath) newPaths.next();
429                  }
430                else
431                  {
432                    newSelection = new TreePath[paths.length];
433                    System.arraycopy(paths, 0, newSelection, 0, paths.length);
434                  }
435              }
436    
437            // Find paths that have been selected, but are no more.
438            for (int i = 0; i < oldLength; i++)
439              {
440                if (selection[i] != null && ! tmpPaths.contains(selection[i]))
441                  {
442                    if (changedPaths == null)
443                      changedPaths = new Vector();
444                    changedPaths.add(new PathPlaceHolder(selection[i], false));
445                  }
446              }
447    
448            // Perform changes and notification.
449            selection = newSelection;
450            HashSet tmp = selectedPaths;
451            selectedPaths = tmpPaths;
452            tmpPaths = tmp;
453            tmpPaths.clear();
454    
455            // Not necessary, but required according to the specs and to tests.
456            if (selection != null)
457              insureUniqueness();
458            updateLeadIndex();
459            resetRowSelection();
460            if (changedPaths != null && changedPaths.size() > 0)
461              notifyPathChange(changedPaths, oldLeadPath);
462          }
463      }
464    
465      /**
466       * Adds a path to the list of selected paths. This method checks if the path
467       * is already selected and doesn't add the same path twice. If this changes
468       * the selection the registered TreeSelectionListeners are notified.
469       * 
470       * The lead path is changed to the added path. This also happen if the 
471       * passed path was already selected before.
472       * 
473       * @param path the path to add to the selection
474       */
475      public void addSelectionPath(TreePath path)
476      {
477        if (path != null)
478          {
479            TreePath[] add = new TreePath[]{ path };
480            addSelectionPaths(add);
481          }
482      }
483    
484      /**
485       * Adds the paths to the list of selected paths. This method checks if the
486       * paths are already selected and doesn't add the same path twice. If this
487       * changes the selection the registered TreeSelectionListeners are notified.
488       * 
489       * @param paths the paths to add to the selection
490       */
491      public void addSelectionPaths(TreePath[] paths)
492      {
493        int length = paths != null ? paths.length : 0;
494        if (length > 0)
495          {
496            if (selectionMode == SINGLE_TREE_SELECTION)
497              setSelectionPaths(paths);
498            else if (selectionMode == CONTIGUOUS_TREE_SELECTION
499                     &&  ! canPathsBeAdded(paths))
500              {
501                if (arePathsContiguous(paths))
502                  setSelectionPaths(paths);
503                else
504                  setSelectionPaths(new TreePath[] { paths[0] });
505              }
506            else
507              {
508                Vector changedPaths = null;
509                tmpPaths.clear();
510                int validPaths = 0;
511                TreePath oldLeadPath = leadPath;
512                int oldPaths = 0;
513                if (selection != null)
514                  oldPaths = selection.length;
515                int i;
516                for (i = 0; i < length; i++)
517                  {
518                    if (paths[i] != null)
519                      {
520                        if (! selectedPaths.contains(paths[i]))
521                          {
522                            validPaths++;
523                            if (changedPaths == null)
524                              changedPaths = new Vector();
525                            changedPaths.add(new PathPlaceHolder(paths[i], true));
526                            selectedPaths.add(paths[i]);
527                            tmpPaths.add(paths[i]);
528                          }
529                        leadPath = paths[i];
530                      }
531                  }
532                if (validPaths > 0)
533                  {
534                    TreePath[] newSelection = new TreePath[oldPaths + validPaths];
535                    if (oldPaths > 0)
536                      System.arraycopy(selection, 0, newSelection, 0, oldPaths);
537                    if (validPaths != paths.length)
538                      {
539                        // Some of the paths are already selected, put together
540                        // the new selection carefully.
541                        Iterator newPaths = tmpPaths.iterator();
542                        i = oldPaths;
543                        while (newPaths.hasNext())
544                          {
545                            newSelection[i] = (TreePath) newPaths.next();
546                            i++;
547                          }
548                      }
549                    else
550                      System.arraycopy(paths, 0, newSelection, oldPaths,
551                                       validPaths);
552                    selection = newSelection;
553                    insureUniqueness();
554                    updateLeadIndex();
555                    resetRowSelection();
556                    if (changedPaths != null && changedPaths.size() > 0)
557                      notifyPathChange(changedPaths, oldLeadPath);
558                  }
559                else
560                  leadPath = oldLeadPath;
561                tmpPaths.clear();
562              }
563          }
564      }
565    
566      /**
567       * Removes the path from the selection. If this changes the selection the
568       * registered TreeSelectionListeners are notified.
569       * 
570       * @param path the path to remove
571       */
572      public void removeSelectionPath(TreePath path)
573      {
574        if (path != null)
575          removeSelectionPaths(new TreePath[]{ path });
576      }
577    
578      /**
579       * Removes the paths from the selection. If this changes the selection the
580       * registered TreeSelectionListeners are notified.
581       * 
582       * @param paths the paths to remove
583       */
584      public void removeSelectionPaths(TreePath[] paths)
585      {
586        if (paths != null && selection != null && paths.length > 0)
587          {
588            if (! canPathsBeRemoved(paths))
589              clearSelection();
590            else
591              {
592                Vector pathsToRemove = null;
593                for (int i = paths.length - 1; i >= 0; i--)
594                  {
595                    if (paths[i] != null && selectedPaths.contains(paths[i]))
596                      {
597                        if (pathsToRemove == null)
598                          pathsToRemove = new Vector();
599                        selectedPaths.remove(paths[i]);
600                        pathsToRemove.add(new PathPlaceHolder(paths[i],
601                                                              false));
602                      }
603                  }
604                if (pathsToRemove != null)
605                  {
606                    int numRemove = pathsToRemove.size();
607                    TreePath oldLead = leadPath;
608                    if (numRemove == selection.length)
609                      selection = null;
610                    else
611                      {
612                        selection = new TreePath[selection.length - numRemove];
613                        Iterator keep = selectedPaths.iterator();
614                        for (int valid = 0; keep.hasNext(); valid++)
615                          selection[valid] = (TreePath) keep.next();
616                      }
617                    // Update lead path.
618                    if (leadPath != null && ! selectedPaths.contains(leadPath))
619                      {
620                        if (selection != null)
621                          leadPath = selection[selection.length - 1];
622                        else
623                          leadPath = null;
624                      }
625                    else if (selection != null)
626                      leadPath = selection[selection.length - 1];
627                    else
628                      leadPath = null;
629                    updateLeadIndex();
630                    resetRowSelection();
631                    notifyPathChange(pathsToRemove, oldLead);
632                  }
633              }
634          }
635      }
636    
637      /**
638       * Returns the first path in the selection. This is especially useful when the
639       * selectionMode is {@link #SINGLE_TREE_SELECTION}.
640       * 
641       * @return the first path in the selection
642       */
643      public TreePath getSelectionPath()
644      {
645        if ((selection == null) || (selection.length == 0))
646          return null;
647        else
648          return selection[0];
649      }
650    
651      /**
652       * Returns the complete selection.
653       * 
654       * @return the complete selection
655       */
656      public TreePath[] getSelectionPaths()
657      {
658        return selection;
659      }
660    
661      /**
662       * Returns the number of paths in the selection.
663       * 
664       * @return the number of paths in the selection
665       */
666      public int getSelectionCount()
667      {
668        if (selection == null)
669          return 0;
670        else
671          return selection.length;
672      }
673    
674      /**
675       * Checks if a given path is in the selection.
676       * 
677       * @param path the path to check
678       * @return <code>true</code> if the path is in the selection,
679       *         <code>false</code> otherwise
680       */
681      public boolean isPathSelected(TreePath path)
682      {
683        if (selection == null)
684          return false;
685    
686        for (int i = 0; i < selection.length; i++)
687          {
688            if (selection[i].equals(path))
689              return true;
690          }
691        return false;
692      }
693    
694      /**
695       * Checks if the selection is empty.
696       * 
697       * @return <code>true</code> if the selection is empty, <code>false</code>
698       *         otherwise
699       */
700      public boolean isSelectionEmpty()
701      {
702        return (selection == null) || (selection.length == 0);
703      }
704    
705      /**
706       * Removes all paths from the selection. Fire the unselection event.
707       */
708      public void clearSelection()
709      {
710        if (selection != null)
711          {
712            int selectionLength = selection.length;
713            boolean[] news = new boolean[selectionLength];
714            Arrays.fill(news, false);
715            TreeSelectionEvent event = new TreeSelectionEvent(this, selection,
716                                                              news, leadPath,
717                                                              null);
718            leadPath = null;
719            leadIndex = 0;
720            leadRow = 0;
721            selectedPaths.clear();
722            selection = null;
723            resetRowSelection();
724            fireValueChanged(event);
725          }
726      }
727    
728      /**
729       * Adds a <code>TreeSelectionListener</code> object to this model.
730       * 
731       * @param listener the listener to add
732       */
733      public void addTreeSelectionListener(TreeSelectionListener listener)
734      {
735        listenerList.add(TreeSelectionListener.class, listener);
736      }
737    
738      /**
739       * Removes a <code>TreeSelectionListener</code> object from this model.
740       * 
741       * @param listener the listener to remove
742       */
743      public void removeTreeSelectionListener(TreeSelectionListener listener)
744      {
745        listenerList.remove(TreeSelectionListener.class, listener);
746      }
747    
748      /**
749       * Returns all <code>TreeSelectionListener</code> added to this model.
750       * 
751       * @return an array of listeners
752       * @since 1.4
753       */
754      public TreeSelectionListener[] getTreeSelectionListeners()
755      {
756        return (TreeSelectionListener[]) getListeners(TreeSelectionListener.class);
757      }
758    
759      /**
760       * fireValueChanged
761       * 
762       * @param event the event to fire.
763       */
764      protected void fireValueChanged(TreeSelectionEvent event)
765      {
766        TreeSelectionListener[] listeners = getTreeSelectionListeners();
767    
768        for (int i = 0; i < listeners.length; ++i)
769          listeners[i].valueChanged(event);
770      }
771    
772      /**
773       * Returns all added listeners of a special type.
774       * 
775       * @param listenerType the listener type
776       * @return an array of listeners
777       * @since 1.3
778       */
779      public <T extends EventListener> T[] getListeners(Class<T> listenerType)
780      {
781        return listenerList.getListeners(listenerType);
782      }
783    
784      /**
785       * Returns the currently selected rows.
786       * 
787       * @return the currently selected rows
788       */
789      public int[] getSelectionRows()
790      {
791        int[] rows = null;
792        if (rowMapper != null && selection != null)
793          {
794            rows = rowMapper.getRowsForPaths(selection);
795            if (rows != null)
796              {
797                // Find invisible rows.
798                int invisible = 0;
799                for (int i = rows.length - 1; i >= 0; i--)
800                  {
801                    if (rows[i] == -1)
802                      invisible++;
803                    
804                  }
805                // Clean up invisible rows.
806                if (invisible > 0)
807                  {
808                    if (invisible == rows.length)
809                      rows = null;
810                    else
811                      {
812                        int[] newRows = new int[rows.length - invisible];
813                        int visCount = 0;
814                        for (int i = rows.length - 1; i >= 0; i--)
815                          {
816                            if (rows[i] != -1)
817                              {
818                                newRows[visCount] = rows[i];
819                                visCount++;
820                              }
821                          }
822                        rows = newRows;
823                      }
824                  }
825              }
826          }
827        return rows;
828      }
829    
830      /**
831       * Returns the smallest row index from the selection.
832       * 
833       * @return the smallest row index from the selection
834       */
835      public int getMinSelectionRow()
836      {
837        return listSelectionModel.getMinSelectionIndex();
838      }
839    
840      /**
841       * Returns the largest row index from the selection.
842       * 
843       * @return the largest row index from the selection
844       */
845      public int getMaxSelectionRow()
846      {
847        return listSelectionModel.getMaxSelectionIndex();
848      }
849    
850      /**
851       * Checks if a particular row is selected.
852       * 
853       * @param row the index of the row to check
854       * @return <code>true</code> if the row is in this selection,
855       *         <code>false</code> otherwise
856       * @throws NullPointerException if the row mapper is not set (can only happen
857       *           if the user has plugged in the custom incorrect TreeUI
858       *           implementation.
859       */
860      public boolean isRowSelected(int row)
861      {
862        return listSelectionModel.isSelectedIndex(row);
863      }
864    
865      /**
866       * Updates the mappings from TreePaths to row indices.
867       */
868      public void resetRowSelection()
869      {
870        listSelectionModel.clearSelection();
871        if (selection != null && rowMapper != null)
872          {
873            int[] rows = rowMapper.getRowsForPaths(selection);
874            // Update list selection model.
875            for (int i = 0; i < rows.length; i++)
876              {
877                int row = rows[i];
878                if (row != -1)
879                  listSelectionModel.addSelectionInterval(row, row);
880              }
881            // Update lead selection.
882            if (leadIndex != -1 && rows != null)
883              leadRow = rows[leadIndex];
884            else if (leadPath != null)
885              {
886                TreePath[] tmp = new TreePath[]{ leadPath };
887                rows = rowMapper.getRowsForPaths(tmp);
888                leadRow = rows != null ? rows[0] : -1;
889              }
890            else
891              leadRow = -1;
892            insureRowContinuity();
893          }
894        else
895          leadRow = -1;
896      }
897    
898      /**
899       * getLeadSelectionRow
900       * 
901       * @return int
902       */
903      public int getLeadSelectionRow()
904      {
905        return leadRow;
906      }
907    
908      /**
909       * getLeadSelectionPath
910       * 
911       * @return TreePath
912       */
913      public TreePath getLeadSelectionPath()
914      {
915        return leadPath;
916      }
917    
918      /**
919       * Adds a <code>PropertyChangeListener</code> object to this model.
920       * 
921       * @param listener the listener to add.
922       */
923      public void addPropertyChangeListener(PropertyChangeListener listener)
924      {
925        if (changeSupport == null)
926          changeSupport = new SwingPropertyChangeSupport(this);
927        changeSupport.addPropertyChangeListener(listener);
928      }
929    
930      /**
931       * Removes a <code>PropertyChangeListener</code> object from this model.
932       * 
933       * @param listener the listener to remove.
934       */
935      public void removePropertyChangeListener(PropertyChangeListener listener)
936      {
937        if (changeSupport != null)
938          changeSupport.removePropertyChangeListener(listener);
939      }
940    
941      /**
942       * Returns all added <code>PropertyChangeListener</code> objects.
943       * 
944       * @return an array of listeners.
945       * @since 1.4
946       */
947      public PropertyChangeListener[] getPropertyChangeListeners()
948      {
949        PropertyChangeListener[] listeners = null;
950        if (changeSupport != null)
951          listeners = changeSupport.getPropertyChangeListeners();
952        else
953          listeners = new PropertyChangeListener[0];
954        return listeners;
955      }
956    
957      /**
958       * Makes sure the currently selected paths are valid according to the current
959       * selectionMode. If the selectionMode is set to
960       * {@link #CONTIGUOUS_TREE_SELECTION} and the selection isn't contiguous then
961       * the selection is reset to the first set of contguous paths. If the
962       * selectionMode is set to {@link #SINGLE_TREE_SELECTION} and the selection
963       * has more than one path, the selection is reset to the contain only the
964       * first path.
965       */
966      protected void insureRowContinuity()
967      {
968        if (selectionMode == CONTIGUOUS_TREE_SELECTION && selection != null
969            && rowMapper != null)
970          {
971            int min = listSelectionModel.getMinSelectionIndex();
972            if (min != -1)
973              {
974                int max = listSelectionModel.getMaxSelectionIndex();
975                for (int i = min; i <= max; i++)
976                  {
977                    if (! listSelectionModel.isSelectedIndex(i))
978                      {
979                        if (i == min)
980                          clearSelection();
981                        else
982                          {
983                            TreePath[] newSelection = new TreePath[i - min];
984                            int[] rows = rowMapper.getRowsForPaths(selection);
985                            for (int j = 0; j < rows.length; j++)
986                              {
987                                if (rows[j] < i)
988                                  newSelection[rows[j] - min] = selection[j];
989                              }
990                            setSelectionPaths(newSelection);
991                            break;
992                          }
993                      }
994                  }
995              }
996          }
997        else if (selectionMode == SINGLE_TREE_SELECTION && selection != null
998            && selection.length > 1)
999          setSelectionPath(selection[0]);
1000      }
1001      
1002      /**
1003       * Returns <code>true</code> if the paths are contiguous (take subsequent
1004       * rows in the diplayed tree view. The method returns <code>true</code> if
1005       * we have no RowMapper assigned.
1006       * 
1007       * @param paths the paths to check for continuity
1008       * @return <code>true</code> if the paths are contiguous or we have no
1009       *         RowMapper assigned
1010       */
1011      protected boolean arePathsContiguous(TreePath[] paths)
1012      {
1013        if (rowMapper == null || paths.length < 2)
1014          return true;
1015    
1016        int length = paths.length;
1017        TreePath[] tmp = new TreePath[1];
1018        tmp[0] = paths[0];
1019        int min = rowMapper.getRowsForPaths(tmp)[0];
1020        BitSet selected = new BitSet();
1021        int valid = 0;
1022        for (int i = 0; i < length; i++)
1023          {
1024            if (paths[i] != null)
1025              {
1026                tmp[0] = paths[i];
1027                int[] rows = rowMapper.getRowsForPaths(tmp);
1028                if (rows == null)
1029                  return false; // No row mapping yet, can't be selected.
1030                int row = rows[0];
1031                if (row == -1 || row < (min - length) || row > (min + length))
1032                  return false; // Not contiguous.
1033                min = Math.min(min, row);
1034                if (! selected.get(row))
1035                  {
1036                    selected.set(row);
1037                    valid++;
1038                  }
1039                
1040              }
1041          }
1042        int max = valid + min;
1043        for (int i = min; i < max; i++)
1044          if (! selected.get(i))
1045            return false; // Not contiguous.
1046        return true;
1047      }
1048    
1049      /**
1050       * Checks if the paths can be added. This returns <code>true</code> if:
1051       * <ul>
1052       * <li><code>paths</code> is <code>null</code> or empty</li>
1053       * <li>we have no RowMapper assigned</li>
1054       * <li>nothing is currently selected</li>
1055       * <li>selectionMode is {@link #DISCONTIGUOUS_TREE_SELECTION}</li>
1056       * <li>adding the paths to the selection still results in a contiguous set of
1057       * paths</li>
1058       * 
1059       * @param paths the paths to check
1060       * @return <code>true</code> if the paths can be added with respect to the
1061       *         selectionMode
1062       */
1063      protected boolean canPathsBeAdded(TreePath[] paths)
1064      {
1065        if (paths == null || paths.length == 0 || rowMapper == null
1066            || selection == null || selectionMode == DISCONTIGUOUS_TREE_SELECTION)
1067          return true;
1068    
1069        BitSet selected = new BitSet();
1070        int min = listSelectionModel.getMinSelectionIndex();
1071        int max = listSelectionModel.getMaxSelectionIndex();
1072        TreePath[] tmp = new TreePath[1];
1073        if (min != -1)
1074          {
1075            // Set the bitmask of selected elements.
1076            for (int i = min; i <= max; i++)
1077              selected.set(i);
1078          }
1079        else
1080          {
1081            tmp[0] = paths[0];
1082            min = rowMapper.getRowsForPaths(tmp)[0];
1083            max = min;
1084          }
1085        // Mark new paths as selected.
1086        for (int i = paths.length - 1; i >= 0; i--)
1087          {
1088            if (paths[i] != null)
1089              {
1090                tmp[0] = paths[i];
1091                int[] rows = rowMapper.getRowsForPaths(tmp);
1092                if (rows == null)
1093                  return false; // Now row mapping yet, can't be selected.
1094                int row = rows[0];
1095                if (row == -1)
1096                  return false; // Now row mapping yet, can't be selected.
1097                min = Math.min(min, row);
1098                max = Math.max(max, row);
1099                selected.set(row);
1100              }
1101          }
1102        // Now look if the new selection would be contiguous.
1103        for (int i = min; i <= max; i++)
1104          if (! selected.get(i))
1105            return false;
1106        return true;
1107      }
1108      
1109      /**
1110       * Checks if the paths can be removed without breaking the continuity of the
1111       * selection according to selectionMode.
1112       * 
1113       * @param paths the paths to check
1114       * @return <code>true</code> if the paths can be removed with respect to the
1115       *         selectionMode
1116       */
1117      protected boolean canPathsBeRemoved(TreePath[] paths)
1118      {
1119        if (rowMapper == null || isSelectionEmpty()
1120            || selectionMode == DISCONTIGUOUS_TREE_SELECTION)
1121          return true;
1122        
1123        HashSet set = new HashSet();
1124        for (int i = 0; i < selection.length; i++)
1125          set.add(selection[i]);
1126        
1127        for (int i = 0; i < paths.length; i++)
1128          set.remove(paths[i]);
1129        
1130        TreePath[] remaining = new TreePath[set.size()];
1131        Iterator iter = set.iterator();
1132        
1133        for (int i = 0; i < remaining.length; i++)
1134          remaining[i] = (TreePath) iter.next();
1135        
1136        return arePathsContiguous(remaining);
1137      }
1138    
1139      /**
1140       * Notify the installed listeners that the given patches have changed. This
1141       * method will call listeners if invoked, but it is not called from the
1142       * implementation of this class.
1143       * 
1144       * @param vPaths the vector of the changed patches
1145       * @param oldLeadSelection the old selection index
1146       */
1147      protected void notifyPathChange(Vector vPaths, TreePath oldLeadSelection)
1148      {
1149    
1150        int numChangedPaths = vPaths.size();
1151        boolean[] news = new boolean[numChangedPaths];
1152        TreePath[] paths = new TreePath[numChangedPaths];
1153        for (int i = 0; i < numChangedPaths; i++)
1154          {
1155            PathPlaceHolder p = (PathPlaceHolder) vPaths.get(i);
1156            news[i] = p.isNew;
1157            paths[i] = p.path;
1158          }
1159    
1160        TreeSelectionEvent event = new TreeSelectionEvent(this, paths, news,
1161                                                          oldLeadSelection,
1162                                                          leadPath);
1163        fireValueChanged(event);
1164      }
1165    
1166      /**
1167       * Updates the lead selection row number after changing the lead selection
1168       * path.
1169       */
1170      protected void updateLeadIndex()
1171      {
1172        leadIndex = -1;
1173        if (leadPath != null)
1174          {
1175            leadRow = -1;
1176            if (selection == null)
1177              leadPath = null;
1178            else
1179              {
1180                for (int i = selection.length - 1; i >= 0 && leadIndex == -1; i--)
1181                  {
1182                    if (selection[i] == leadPath)
1183                      leadIndex = i;
1184                  }
1185              }
1186          }
1187      }
1188    
1189      /**
1190       * This method exists due historical reasons and returns without action
1191       * (unless overridden). For compatibility with the applications that override
1192       * it, it is still called from the {@link #setSelectionPaths(TreePath[])} and
1193       * {@link #addSelectionPaths(TreePath[])}.
1194       */
1195      protected void insureUniqueness()
1196      {
1197        // Following the API 1.4, the method should return without action.
1198      }
1199    }