001/* BasicDirectoryModel.java --
002   Copyright (C) 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
038package javax.swing.plaf.basic;
039
040import java.beans.PropertyChangeEvent;
041import java.beans.PropertyChangeListener;
042import java.io.File;
043import java.util.Collections;
044import java.util.Comparator;
045import java.util.Iterator;
046import java.util.List;
047import java.util.Vector;
048import javax.swing.AbstractListModel;
049import javax.swing.JFileChooser;
050import javax.swing.SwingUtilities;
051import javax.swing.event.ListDataEvent;
052import javax.swing.filechooser.FileSystemView;
053
054
055/**
056 * Implements an AbstractListModel for directories where the source
057 * of the files is a JFileChooser object.
058 *
059 * This class is used for sorting and ordering the file list in
060 * a JFileChooser L&F object.
061 */
062public class BasicDirectoryModel extends AbstractListModel
063  implements PropertyChangeListener
064{
065  /** The list of files itself */
066  private Vector contents;
067
068  /**
069   * The directories in the list.
070   */
071  private Vector directories;
072
073  /**
074   * The files in the list.
075   */
076  private Vector files;
077
078  /** The listing mode of the associated JFileChooser,
079      either FILES_ONLY, DIRECTORIES_ONLY or FILES_AND_DIRECTORIES */
080  private int listingMode;
081
082  /** The JFileCooser associated with this model */
083  private JFileChooser filechooser;
084
085  /**
086   * The thread that loads the file view.
087   */
088  private DirectoryLoadThread loadThread;
089
090  /**
091   * This thread is responsible for loading file lists from the
092   * current directory and updating the model.
093   */
094  private class DirectoryLoadThread extends Thread
095  {
096
097    /**
098     * Updates the Swing list model.
099     */
100    private class UpdateSwingRequest
101      implements Runnable
102    {
103
104      private List added;
105      private int addIndex;
106      private List removed;
107      private int removeIndex;
108      private boolean cancel;
109
110      UpdateSwingRequest(List add, int ai, List rem, int ri)
111      {
112        added = add;
113        addIndex = ai;
114        removed = rem;
115        removeIndex = ri;
116        cancel = false;
117      }
118
119      public void run()
120      {
121        if (! cancel)
122          {
123            int numRemoved = removed == null ? 0 : removed.size();
124            int numAdded = added == null ? 0 : added.size();
125            synchronized (contents)
126              {
127                if (numRemoved > 0)
128                  contents.removeAll(removed);
129                if (numAdded > 0)
130                  contents.addAll(added);
131
132                files = null;
133                directories = null;
134              }
135            if (numRemoved > 0 && numAdded == 0)
136              fireIntervalRemoved(BasicDirectoryModel.this, removeIndex,
137                                  removeIndex + numRemoved - 1);
138            else if (numRemoved == 0 && numAdded > 0)
139              fireIntervalAdded(BasicDirectoryModel.this, addIndex,
140                                addIndex + numAdded - 1);
141            else
142              fireContentsChanged();
143          }
144      }
145
146      void cancel()
147      {
148        cancel = true;
149      }
150    }
151
152    /**
153     * The directory beeing loaded.
154     */
155    File directory;
156
157    /**
158     * Stores all UpdateSwingRequests that are sent to the event queue.
159     */
160    private UpdateSwingRequest pending;
161
162    /**
163     * Creates a new DirectoryLoadThread that loads the specified
164     * directory.
165     *
166     * @param dir the directory to load
167     */
168    DirectoryLoadThread(File dir)
169    {
170      super("Basic L&F directory loader");
171      directory = dir;
172    }
173
174    public void run()
175    {
176      FileSystemView fsv = filechooser.getFileSystemView();
177      File[] files = fsv.getFiles(directory,
178                                  filechooser.isFileHidingEnabled());
179
180      // Occasional check if we have been interrupted.
181      if (isInterrupted())
182        return;
183
184      // Check list for accepted files.
185      Vector accepted = new Vector();
186      for (int i = 0; i < files.length; i++)
187        {
188          if (filechooser.accept(files[i]))
189            accepted.add(files[i]);
190        }
191
192      // Occasional check if we have been interrupted.
193      if (isInterrupted())
194        return;
195
196      // Sort list.
197      sort(accepted);
198
199      // Now split up directories from files so that we get the directories
200      // listed before the files.
201      Vector newFiles = new Vector();
202      Vector newDirectories = new Vector();
203      for (Iterator i = accepted.iterator(); i.hasNext();)
204        {
205          File f = (File) i.next();
206          boolean traversable = filechooser.isTraversable(f);
207          if (traversable)
208            newDirectories.add(f);
209          else if (! traversable && filechooser.isFileSelectionEnabled())
210            newFiles.add(f);
211
212          // Occasional check if we have been interrupted.
213          if (isInterrupted())
214            return;
215
216        }
217
218      // Build up new file cache. Try to update only the changed elements.
219      // This will be important for actions like adding new files or
220      // directories inside a large file list.
221      Vector newCache = new Vector(newDirectories);
222      newCache.addAll(newFiles);
223
224      int newSize = newCache.size();
225      int oldSize = contents.size();
226      if (newSize < oldSize)
227        {
228          // Check for removed interval.
229          int start = -1;
230          int end = -1;
231          boolean found = false;
232          for (int i = 0; i < newSize && !found; i++)
233            {
234              if (! newCache.get(i).equals(contents.get(i)))
235                {
236                  start = i;
237                  end = i + oldSize - newSize;
238                  found = true;
239                }
240            }
241          if (start >= 0 && end > start
242              && contents.subList(end, oldSize)
243                                    .equals(newCache.subList(start, newSize)))
244            {
245              // Occasional check if we have been interrupted.
246              if (isInterrupted())
247                return;
248
249              Vector removed = new Vector(contents.subList(start, end));
250              UpdateSwingRequest r = new UpdateSwingRequest(null, 0,
251                                                            removed, start);
252              invokeLater(r);
253              newCache = null;
254            }
255        }
256      else if (newSize > oldSize)
257        {
258          // Check for inserted interval.
259          int start = oldSize;
260          int end = newSize;
261          boolean found = false;
262          for (int i = 0; i < oldSize && ! found; i++)
263            {
264              if (! newCache.get(i).equals(contents.get(i)))
265                {
266                  start = i;
267                  boolean foundEnd = false;
268                  for (int j = i; j < newSize && ! foundEnd; j++)
269                    {
270                      if (newCache.get(j).equals(contents.get(i)))
271                        {
272                          end = j;
273                          foundEnd = true;
274                        }
275                    }
276                  end = i + oldSize - newSize;
277                }
278            }
279          if (start >= 0 && end > start
280              && newCache.subList(end, newSize)
281                                    .equals(contents.subList(start, oldSize)))
282            {
283              // Occasional check if we have been interrupted.
284              if (isInterrupted())
285                return;
286
287              List added = newCache.subList(start, end);
288              UpdateSwingRequest r = new UpdateSwingRequest(added, start,
289                                                            null, 0);
290              invokeLater(r);
291              newCache = null;
292            }
293        }
294
295      // Handle complete list changes (newCache != null).
296      if (newCache != null && ! contents.equals(newCache))
297        {
298          // Occasional check if we have been interrupted.
299          if (isInterrupted())
300            return;
301          UpdateSwingRequest r = new UpdateSwingRequest(newCache, 0,
302                                                        contents, 0);
303          invokeLater(r);
304        }
305    }
306
307    /**
308     * Wraps SwingUtilities.invokeLater() and stores the request in
309     * a Vector so that we can still cancel it later.
310     *
311     * @param update the request to invoke
312     */
313    private void invokeLater(UpdateSwingRequest update)
314    {
315      pending = update;
316      SwingUtilities.invokeLater(update);
317    }
318
319    /**
320     * Cancels all pending update requests that might be in the AWT
321     * event queue.
322     */
323    void cancelPending()
324    {
325      if (pending != null)
326        pending.cancel();
327    }
328  }
329
330  /** A Comparator class/object for sorting the file list. */
331  private Comparator comparator = new Comparator()
332    {
333      public int compare(Object o1, Object o2)
334      {
335        if (lt((File) o1, (File) o2))
336          return -1;
337        else
338          return 1;
339      }
340    };
341
342  /**
343   * Creates a new BasicDirectoryModel object.
344   *
345   * @param filechooser DOCUMENT ME!
346   */
347  public BasicDirectoryModel(JFileChooser filechooser)
348  {
349    this.filechooser = filechooser;
350    filechooser.addPropertyChangeListener(this);
351    listingMode = filechooser.getFileSelectionMode();
352    contents = new Vector();
353    validateFileCache();
354  }
355
356  /**
357   * Returns whether a given (File) object is included in the list.
358   *
359   * @param o - The file object to test.
360   *
361   * @return <code>true</code> if the list contains the given object.
362   */
363  public boolean contains(Object o)
364  {
365    return contents.contains(o);
366  }
367
368  /**
369   * Fires a content change event.
370   */
371  public void fireContentsChanged()
372  {
373    fireContentsChanged(this, 0, getSize() - 1);
374  }
375
376  /**
377   * Returns a Vector of (java.io.File) objects containing
378   * the directories in this list.
379   *
380   * @return a Vector
381   */
382  public Vector<File> getDirectories()
383  {
384    // Synchronize this with the UpdateSwingRequest for the case when
385    // contents is modified.
386    synchronized (contents)
387      {
388        Vector dirs = directories;
389        if (dirs == null)
390          {
391            // Initializes this in getFiles().
392            getFiles();
393            dirs = directories;
394          }
395        return dirs;
396      }
397  }
398
399  /**
400   * Returns the (java.io.File) object at
401   * an index in the list.
402   *
403   * @param index The list index
404   * @return a File object
405   */
406  public Object getElementAt(int index)
407  {
408    if (index > getSize() - 1)
409      return null;
410    return contents.elementAt(index);
411  }
412
413  /**
414   * Returns a Vector of (java.io.File) objects containing
415   * the files in this list.
416   *
417   * @return a Vector
418   */
419  public Vector<File>  getFiles()
420  {
421    synchronized (contents)
422      {
423        Vector f = files;
424        if (f == null)
425          {
426            f = new Vector();
427            Vector d = new Vector(); // Directories;
428            for (Iterator i = contents.iterator(); i.hasNext();)
429              {
430                File file = (File) i.next();
431                if (filechooser.isTraversable(file))
432                  d.add(file);
433                else
434                  f.add(file);
435              }
436            files = f;
437            directories = d;
438          }
439        return f;
440      }
441  }
442
443  /**
444   * Returns the size of the list, which only includes directories
445   * if the JFileChooser is set to DIRECTORIES_ONLY.
446   *
447   * Otherwise, both directories and files are included in the count.
448   *
449   * @return The size of the list.
450   */
451  public int getSize()
452  {
453    return contents.size();
454  }
455
456  /**
457   * Returns the index of an (java.io.File) object in the list.
458   *
459   * @param o The object - normally a File.
460   *
461   * @return the index of that object, or -1 if it is not in the list.
462   */
463  public int indexOf(Object o)
464  {
465    return contents.indexOf(o);
466  }
467
468  /**
469   * Obsoleted method which does nothing.
470   */
471  public void intervalAdded(ListDataEvent e)
472  {
473    // obsoleted
474  }
475
476  /**
477   * Obsoleted method which does nothing.
478   */
479  public void intervalRemoved(ListDataEvent e)
480  {
481    // obsoleted
482  }
483
484  /**
485   * Obsoleted method which does nothing.
486   */
487  public void invalidateFileCache()
488  {
489    // obsoleted
490  }
491
492  /**
493   * Less than, determine the relative order in the list of two files
494   * for sorting purposes.
495   *
496   * The order is: directories < files, and thereafter alphabetically,
497   * using the default locale collation.
498   *
499   * @param a the first file
500   * @param b the second file
501   *
502   * @return <code>true</code> if a > b, <code>false</code> if a < b.
503   */
504  protected boolean lt(File a, File b)
505  {
506    boolean aTrav = filechooser.isTraversable(a);
507    boolean bTrav = filechooser.isTraversable(b);
508
509    if (aTrav == bTrav)
510      {
511        String aname = a.getName().toLowerCase();
512        String bname = b.getName().toLowerCase();
513        return (aname.compareTo(bname) < 0) ? true : false;
514      }
515    else
516      {
517        if (aTrav)
518          return true;
519        else
520          return false;
521      }
522  }
523
524  /**
525   * Listens for a property change; the change in file selection mode of the
526   * associated JFileChooser. Reloads the file cache on that event.
527   *
528   * @param e - A PropertyChangeEvent.
529   */
530  public void propertyChange(PropertyChangeEvent e)
531  {
532    String property = e.getPropertyName();
533    if (property.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY)
534        || property.equals(JFileChooser.FILE_FILTER_CHANGED_PROPERTY)
535        || property.equals(JFileChooser.FILE_HIDING_CHANGED_PROPERTY)
536        || property.equals(JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY)
537        || property.equals(JFileChooser.FILE_VIEW_CHANGED_PROPERTY)
538        )
539      {
540        validateFileCache();
541      }
542  }
543
544  /**
545   * Renames a file - However, does <I>not</I> re-sort the list
546   * or replace the old file with the new one in the list.
547   *
548   * @param oldFile The old file
549   * @param newFile The new file name
550   *
551   * @return <code>true</code> if the rename succeeded
552   */
553  public boolean renameFile(File oldFile, File newFile)
554  {
555    return oldFile.renameTo( newFile );
556  }
557
558  /**
559   * Sorts a Vector of File objects.
560   *
561   * @param v The Vector to sort.
562   */
563  protected void sort(Vector<? extends File> v)
564  {
565    Collections.sort(v, comparator);
566  }
567
568  /**
569   * Re-loads the list of files
570   */
571  public void validateFileCache()
572  {
573    File dir = filechooser.getCurrentDirectory();
574    if (dir != null)
575      {
576        // Cancel all pending requests.
577        if (loadThread != null)
578          {
579            loadThread.interrupt();
580            loadThread.cancelPending();
581          }
582        loadThread = new DirectoryLoadThread(dir);
583        loadThread.start();
584      }
585  }
586}