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