1 /*
   2  * Copyright (c) 1998, 2009, Oracle and/or its affiliates. All rights reserved.
   3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
   4  *
   5  * This code is free software; you can redistribute it and/or modify it
   6  * under the terms of the GNU General Public License version 2 only, as
   7  * published by the Free Software Foundation.  Oracle designates this
   8  * particular file as subject to the "Classpath" exception as provided
   9  * by Oracle in the LICENSE file that accompanied this code.
  10  *
  11  * This code is distributed in the hope that it will be useful, but WITHOUT
  12  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
  13  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
  14  * version 2 for more details (a copy is included in the LICENSE file that
  15  * accompanied this code).
  16  *
  17  * You should have received a copy of the GNU General Public License version
  18  * 2 along with this work; if not, write to the Free Software Foundation,
  19  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
  20  *
  21  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
  22  * or visit www.oracle.com if you need additional information or have any
  23  * questions.
  24  */
  25 
  26 package javax.swing.plaf.basic;
  27 
  28 import java.io.File;
  29 import java.util.*;
  30 import java.util.concurrent.Callable;
  31 import javax.swing.*;
  32 import javax.swing.filechooser.*;
  33 import javax.swing.event.*;
  34 import java.beans.*;
  35 
  36 import sun.awt.shell.ShellFolder;
  37 
  38 /**
  39  * Basic implementation of a file list.
  40  *
  41  * @author Jeff Dinkins
  42  */
  43 public class BasicDirectoryModel extends AbstractListModel<Object> implements PropertyChangeListener {
  44 
  45     private JFileChooser filechooser = null;
  46     // PENDING(jeff) pick the size more sensibly
  47     private Vector<File> fileCache = new Vector<File>(50);
  48     private LoadFilesThread loadThread = null;
  49     private Vector<File> files = null;
  50     private Vector<File> directories = null;
  51     private int fetchID = 0;
  52 
  53     private PropertyChangeSupport changeSupport;
  54 
  55     private boolean busy = false;
  56 
  57     public BasicDirectoryModel(JFileChooser filechooser) {
  58         this.filechooser = filechooser;
  59         validateFileCache();
  60     }
  61 
  62     public void propertyChange(PropertyChangeEvent e) {
  63         String prop = e.getPropertyName();
  64         if(prop == JFileChooser.DIRECTORY_CHANGED_PROPERTY ||
  65            prop == JFileChooser.FILE_VIEW_CHANGED_PROPERTY ||
  66            prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY ||
  67            prop == JFileChooser.FILE_HIDING_CHANGED_PROPERTY ||
  68            prop == JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) {
  69             validateFileCache();
  70         } else if ("UI".equals(prop)) {
  71             Object old = e.getOldValue();
  72             if (old instanceof BasicFileChooserUI) {
  73                 BasicFileChooserUI ui = (BasicFileChooserUI) old;
  74                 BasicDirectoryModel model = ui.getModel();
  75                 if (model != null) {
  76                     model.invalidateFileCache();
  77                 }
  78             }
  79         } else if ("JFileChooserDialogIsClosingProperty".equals(prop)) {
  80             invalidateFileCache();
  81         }
  82     }
  83 
  84     /**
  85      * This method is used to interrupt file loading thread.
  86      */
  87     public void invalidateFileCache() {
  88         if (loadThread != null) {
  89             loadThread.interrupt();
  90             loadThread.cancelRunnables();
  91             loadThread = null;
  92         }
  93     }
  94 
  95     public Vector<File> getDirectories() {
  96         synchronized(fileCache) {
  97             if (directories != null) {
  98                 return directories;
  99             }
 100             Vector fls = getFiles();
 101             return directories;
 102         }
 103     }
 104 
 105     public Vector<File> getFiles() {
 106         synchronized(fileCache) {
 107             if (files != null) {
 108                 return files;
 109             }
 110             files = new Vector<File>();
 111             directories = new Vector<File>();
 112             directories.addElement(filechooser.getFileSystemView().createFileObject(
 113                 filechooser.getCurrentDirectory(), "..")
 114             );
 115 
 116             for (int i = 0; i < getSize(); i++) {
 117                 File f = fileCache.get(i);
 118                 if (filechooser.isTraversable(f)) {
 119                     directories.add(f);
 120                 } else {
 121                     files.add(f);
 122                 }
 123             }
 124             return files;
 125         }
 126     }
 127 
 128     public void validateFileCache() {
 129         File currentDirectory = filechooser.getCurrentDirectory();
 130         if (currentDirectory == null) {
 131             return;
 132         }
 133         if (loadThread != null) {
 134             loadThread.interrupt();
 135             loadThread.cancelRunnables();
 136         }
 137 
 138         setBusy(true, ++fetchID);
 139 
 140         loadThread = new LoadFilesThread(currentDirectory, fetchID);
 141         loadThread.start();
 142     }
 143 
 144     /**
 145      * Renames a file in the underlying file system.
 146      *
 147      * @param oldFile a <code>File</code> object representing
 148      *        the existing file
 149      * @param newFile a <code>File</code> object representing
 150      *        the desired new file name
 151      * @return <code>true</code> if rename succeeded,
 152      *        otherwise <code>false</code>
 153      * @since 1.4
 154      */
 155     public boolean renameFile(File oldFile, File newFile) {
 156         synchronized(fileCache) {
 157             if (oldFile.renameTo(newFile)) {
 158                 validateFileCache();
 159                 return true;
 160             }
 161             return false;
 162         }
 163     }
 164 
 165 
 166     public void fireContentsChanged() {
 167         // System.out.println("BasicDirectoryModel: firecontentschanged");
 168         fireContentsChanged(this, 0, getSize()-1);
 169     }
 170 
 171     public int getSize() {
 172         return fileCache.size();
 173     }
 174 
 175     public boolean contains(Object o) {
 176         return fileCache.contains(o);
 177     }
 178 
 179     public int indexOf(Object o) {
 180         return fileCache.indexOf(o);
 181     }
 182 
 183     public Object getElementAt(int index) {
 184         return fileCache.get(index);
 185     }
 186 
 187     /**
 188      * Obsolete - not used.
 189      */
 190     public void intervalAdded(ListDataEvent e) {
 191     }
 192 
 193     /**
 194      * Obsolete - not used.
 195      */
 196     public void intervalRemoved(ListDataEvent e) {
 197     }
 198 
 199     protected void sort(Vector<? extends File> v){
 200         ShellFolder.sort(v);
 201     }
 202 
 203     // Obsolete - not used
 204     protected boolean lt(File a, File b) {
 205         // First ignore case when comparing
 206         int diff = a.getName().toLowerCase().compareTo(b.getName().toLowerCase());
 207         if (diff != 0) {
 208             return diff < 0;
 209         } else {
 210             // May differ in case (e.g. "mail" vs. "Mail")
 211             return a.getName().compareTo(b.getName()) < 0;
 212         }
 213     }
 214 
 215 
 216     class LoadFilesThread extends Thread {
 217         File currentDirectory = null;
 218         int fid;
 219         Vector<DoChangeContents> runnables = new Vector<DoChangeContents>(10);
 220 
 221         public LoadFilesThread(File currentDirectory, int fid) {
 222             super("Basic L&F File Loading Thread");
 223             this.currentDirectory = currentDirectory;
 224             this.fid = fid;
 225         }
 226 
 227         public void run() {
 228             run0();
 229             setBusy(false, fid);
 230         }
 231 
 232         public void run0() {
 233             FileSystemView fileSystem = filechooser.getFileSystemView();
 234 
 235             if (isInterrupted()) {
 236                 return;
 237             }
 238 
 239             File[] list = fileSystem.getFiles(currentDirectory, filechooser.isFileHidingEnabled());
 240 
 241             if (isInterrupted()) {
 242                 return;
 243             }
 244 
 245             final Vector<File> newFileCache = new Vector<File>();
 246             Vector<File> newFiles = new Vector<File>();
 247 
 248             // run through the file list, add directories and selectable files to fileCache
 249             // Note that this block must be OUTSIDE of Invoker thread because of
 250             // deadlock possibility with custom synchronized FileSystemView
 251             for (File file : list) {
 252                 if (filechooser.accept(file)) {
 253                     boolean isTraversable = filechooser.isTraversable(file);
 254 
 255                     if (isTraversable) {
 256                         newFileCache.addElement(file);
 257                     } else if (filechooser.isFileSelectionEnabled()) {
 258                         newFiles.addElement(file);
 259                     }
 260 
 261                     if (isInterrupted()) {
 262                         return;
 263                     }
 264                 }
 265             }
 266 
 267             // First sort alphabetically by filename
 268             sort(newFileCache);
 269             sort(newFiles);
 270 
 271             newFileCache.addAll(newFiles);
 272 
 273             // To avoid loads of synchronizations with Invoker and improve performance we
 274             // execute the whole block on the COM thread
 275             DoChangeContents doChangeContents = ShellFolder.invoke(new Callable<DoChangeContents>() {
 276                 public DoChangeContents call() {
 277                     int newSize = newFileCache.size();
 278                     int oldSize = fileCache.size();
 279 
 280                     if (newSize > oldSize) {
 281                         //see if interval is added
 282                         int start = oldSize;
 283                         int end = newSize;
 284                         for (int i = 0; i < oldSize; i++) {
 285                             if (!newFileCache.get(i).equals(fileCache.get(i))) {
 286                                 start = i;
 287                                 for (int j = i; j < newSize; j++) {
 288                                     if (newFileCache.get(j).equals(fileCache.get(i))) {
 289                                         end = j;
 290                                         break;
 291                                     }
 292                                 }
 293                                 break;
 294                             }
 295                         }
 296                         if (start >= 0 && end > start
 297                             && newFileCache.subList(end, newSize).equals(fileCache.subList(start, oldSize))) {
 298                             if (isInterrupted()) {
 299                                 return null;
 300                             }
 301                             return new DoChangeContents(newFileCache.subList(start, end), start, null, 0, fid);
 302                         }
 303                     } else if (newSize < oldSize) {
 304                         //see if interval is removed
 305                         int start = -1;
 306                         int end = -1;
 307                         for (int i = 0; i < newSize; i++) {
 308                             if (!newFileCache.get(i).equals(fileCache.get(i))) {
 309                                 start = i;
 310                                 end = i + oldSize - newSize;
 311                                 break;
 312                             }
 313                         }
 314                         if (start >= 0 && end > start
 315                             && fileCache.subList(end, oldSize).equals(newFileCache.subList(start, newSize))) {
 316                             if (isInterrupted()) {
 317                                 return null;
 318                             }
 319                             return new DoChangeContents(null, 0, new Vector(fileCache.subList(start, end)), start, fid);
 320                         }
 321                     }
 322                     if (!fileCache.equals(newFileCache)) {
 323                         if (isInterrupted()) {
 324                             cancelRunnables(runnables);
 325                         }
 326                         return new DoChangeContents(newFileCache, 0, fileCache, 0, fid);
 327                     }
 328                     return null;
 329                 }
 330             });
 331 
 332             if (doChangeContents != null) {
 333                 runnables.addElement(doChangeContents);
 334                 SwingUtilities.invokeLater(doChangeContents);
 335             }
 336         }
 337 
 338 
 339         public void cancelRunnables(Vector<DoChangeContents> runnables) {
 340             for (DoChangeContents runnable : runnables) {
 341                 runnable.cancel();
 342             }
 343         }
 344 
 345         public void cancelRunnables() {
 346             cancelRunnables(runnables);
 347         }
 348    }
 349 
 350 
 351     /**
 352      * Adds a PropertyChangeListener to the listener list. The listener is
 353      * registered for all bound properties of this class.
 354      * <p>
 355      * If <code>listener</code> is <code>null</code>,
 356      * no exception is thrown and no action is performed.
 357      *
 358      * @param    listener  the property change listener to be added
 359      *
 360      * @see #removePropertyChangeListener
 361      * @see #getPropertyChangeListeners
 362      *
 363      * @since 1.6
 364      */
 365     public void addPropertyChangeListener(PropertyChangeListener listener) {
 366         if (changeSupport == null) {
 367             changeSupport = new PropertyChangeSupport(this);
 368         }
 369         changeSupport.addPropertyChangeListener(listener);
 370     }
 371 
 372     /**
 373      * Removes a PropertyChangeListener from the listener list.
 374      * <p>
 375      * If listener is null, no exception is thrown and no action is performed.
 376      *
 377      * @param listener the PropertyChangeListener to be removed
 378      *
 379      * @see #addPropertyChangeListener
 380      * @see #getPropertyChangeListeners
 381      *
 382      * @since 1.6
 383      */
 384     public void removePropertyChangeListener(PropertyChangeListener listener) {
 385         if (changeSupport != null) {
 386             changeSupport.removePropertyChangeListener(listener);
 387         }
 388     }
 389 
 390     /**
 391      * Returns an array of all the property change listeners
 392      * registered on this component.
 393      *
 394      * @return all of this component's <code>PropertyChangeListener</code>s
 395      *         or an empty array if no property change
 396      *         listeners are currently registered
 397      *
 398      * @see      #addPropertyChangeListener
 399      * @see      #removePropertyChangeListener
 400      * @see      java.beans.PropertyChangeSupport#getPropertyChangeListeners
 401      *
 402      * @since 1.6
 403      */
 404     public PropertyChangeListener[] getPropertyChangeListeners() {
 405         if (changeSupport == null) {
 406             return new PropertyChangeListener[0];
 407         }
 408         return changeSupport.getPropertyChangeListeners();
 409     }
 410 
 411     /**
 412      * Support for reporting bound property changes for boolean properties.
 413      * This method can be called when a bound property has changed and it will
 414      * send the appropriate PropertyChangeEvent to any registered
 415      * PropertyChangeListeners.
 416      *
 417      * @param propertyName the property whose value has changed
 418      * @param oldValue the property's previous value
 419      * @param newValue the property's new value
 420      *
 421      * @since 1.6
 422      */
 423     protected void firePropertyChange(String propertyName,
 424                                       Object oldValue, Object newValue) {
 425         if (changeSupport != null) {
 426             changeSupport.firePropertyChange(propertyName,
 427                                              oldValue, newValue);
 428         }
 429     }
 430 
 431 
 432     /**
 433      * Set the busy state for the model. The model is considered
 434      * busy when it is running a separate (interruptable)
 435      * thread in order to load the contents of a directory.
 436      */
 437     private synchronized void setBusy(final boolean busy, int fid) {
 438         if (fid == fetchID) {
 439             boolean oldValue = this.busy;
 440             this.busy = busy;
 441 
 442             if (changeSupport != null && busy != oldValue) {
 443                 SwingUtilities.invokeLater(new Runnable() {
 444                     public void run() {
 445                         firePropertyChange("busy", !busy, busy);
 446                     }
 447                 });
 448             }
 449         }
 450     }
 451 
 452 
 453     class DoChangeContents implements Runnable {
 454         private List<File> addFiles;
 455         private List<File> remFiles;
 456         private boolean doFire = true;
 457         private int fid;
 458         private int addStart = 0;
 459         private int remStart = 0;
 460 
 461         public DoChangeContents(List<File> addFiles, int addStart, List<File> remFiles, int remStart, int fid) {
 462             this.addFiles = addFiles;
 463             this.addStart = addStart;
 464             this.remFiles = remFiles;
 465             this.remStart = remStart;
 466             this.fid = fid;
 467         }
 468 
 469         synchronized void cancel() {
 470                 doFire = false;
 471         }
 472 
 473         public synchronized void run() {
 474             if (fetchID == fid && doFire) {
 475                 int remSize = (remFiles == null) ? 0 : remFiles.size();
 476                 int addSize = (addFiles == null) ? 0 : addFiles.size();
 477                 synchronized(fileCache) {
 478                     if (remSize > 0) {
 479                         fileCache.removeAll(remFiles);
 480                     }
 481                     if (addSize > 0) {
 482                         fileCache.addAll(addStart, addFiles);
 483                     }
 484                     files = null;
 485                     directories = null;
 486                 }
 487                 if (remSize > 0 && addSize == 0) {
 488                     fireIntervalRemoved(BasicDirectoryModel.this, remStart, remStart + remSize - 1);
 489                 } else if (addSize > 0 && remSize == 0 && addStart + addSize <= fileCache.size()) {
 490                     fireIntervalAdded(BasicDirectoryModel.this, addStart, addStart + addSize - 1);
 491                 } else {
 492                     fireContentsChanged();
 493                 }
 494             }
 495         }
 496     }
 497 }