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