1 /* 2 * Copyright (c) 1998, 2011, 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 }