001    /* BasicDirectoryModel.java --
002       Copyright (C) 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    package javax.swing.plaf.basic;
039    
040    import java.beans.PropertyChangeEvent;
041    import java.beans.PropertyChangeListener;
042    import java.io.File;
043    import java.util.Collections;
044    import java.util.Comparator;
045    import java.util.Iterator;
046    import java.util.List;
047    import java.util.Vector;
048    import javax.swing.AbstractListModel;
049    import javax.swing.JFileChooser;
050    import javax.swing.SwingUtilities;
051    import javax.swing.event.ListDataEvent;
052    import 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     */
062    public 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    }