1 /* 2 * Copyright (c) 1998, 2014, 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 @SuppressWarnings("serial") // Superclass is not serializable across versions 44 public class BasicDirectoryModel extends AbstractListModel<Object> implements PropertyChangeListener { 45 46 private JFileChooser filechooser = null; 47 // PENDING(jeff) pick the size more sensibly 48 private Vector<File> fileCache = new Vector<File>(50); 49 private LoadFilesThread loadThread = null; 50 private Vector<File> files = null; 51 private Vector<File> directories = null; 52 private int fetchID = 0; 53 54 private PropertyChangeSupport changeSupport; 55 56 private boolean busy = false; 57 58 /** 59 * Constructs a new instance of {@code BasicDirectoryModel}. 60 * 61 * @param filechooser an instance of {JFileChooser} 62 */ 63 public BasicDirectoryModel(JFileChooser filechooser) { 64 this.filechooser = filechooser; 65 validateFileCache(); 66 } 67 68 public void propertyChange(PropertyChangeEvent e) { 69 String prop = e.getPropertyName(); 70 if(prop == JFileChooser.DIRECTORY_CHANGED_PROPERTY || 71 prop == JFileChooser.FILE_VIEW_CHANGED_PROPERTY || 72 prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY || 73 prop == JFileChooser.FILE_HIDING_CHANGED_PROPERTY || 74 prop == JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) { 75 validateFileCache(); 76 } else if ("UI".equals(prop)) { 77 Object old = e.getOldValue(); 78 if (old instanceof BasicFileChooserUI) { 79 BasicFileChooserUI ui = (BasicFileChooserUI) old; 80 BasicDirectoryModel model = ui.getModel(); 81 if (model != null) { 82 model.invalidateFileCache(); 83 } 84 } 85 } else if ("JFileChooserDialogIsClosingProperty".equals(prop)) { 86 invalidateFileCache(); 87 } 88 } 89 90 /** 91 * This method is used to interrupt file loading thread. 92 */ 93 public void invalidateFileCache() { 94 if (loadThread != null) { 95 loadThread.interrupt(); 96 loadThread.cancelRunnables(); 97 loadThread = null; 98 } 99 } 100 101 /** 102 * Returns a list of directories. 103 * 104 * @return a list of directories 105 */ 106 public Vector<File> getDirectories() { 107 synchronized(fileCache) { 108 if (directories != null) { 109 return directories; 110 } 111 Vector<File> fls = getFiles(); 112 return directories; 113 } 114 } 115 116 /** 117 * Returns a list of files. 118 * 119 * @return a list of files 120 */ 121 public Vector<File> getFiles() { 122 synchronized(fileCache) { 123 if (files != null) { 124 return files; 125 } 126 files = new Vector<File>(); 127 directories = new Vector<File>(); 128 directories.addElement(filechooser.getFileSystemView().createFileObject( 129 filechooser.getCurrentDirectory(), "..") 130 ); 131 132 for (int i = 0; i < getSize(); i++) { 133 File f = fileCache.get(i); 134 if (filechooser.isTraversable(f)) { 135 directories.add(f); 136 } else { 137 files.add(f); 138 } 139 } 140 return files; 141 } 142 } 143 144 /** 145 * Validates content of file cache. 146 */ 147 public void validateFileCache() { 148 File currentDirectory = filechooser.getCurrentDirectory(); 149 if (currentDirectory == null) { 150 return; 151 } 152 if (loadThread != null) { 153 loadThread.interrupt(); 154 loadThread.cancelRunnables(); 155 } 156 157 setBusy(true, ++fetchID); 158 159 loadThread = new LoadFilesThread(currentDirectory, fetchID); 160 loadThread.start(); 161 } 162 163 /** 164 * Renames a file in the underlying file system. 165 * 166 * @param oldFile a <code>File</code> object representing 167 * the existing file 168 * @param newFile a <code>File</code> object representing 169 * the desired new file name 170 * @return <code>true</code> if rename succeeded, 171 * otherwise <code>false</code> 172 * @since 1.4 173 */ 174 public boolean renameFile(File oldFile, File newFile) { 175 synchronized(fileCache) { 176 if (oldFile.renameTo(newFile)) { 177 validateFileCache(); 178 return true; 179 } 180 return false; 181 } 182 } 183 184 /** 185 * Invoked when a content is changed. 186 */ 187 public void fireContentsChanged() { 188 fireContentsChanged(this, 0, getSize() - 1); 189 } 190 191 public int getSize() { 192 return fileCache.size(); 193 } 194 195 /** 196 * Returns {@code true} if an element {@code o} is in file cache, 197 * otherwise, returns {@code false}. 198 * 199 * @param o an element 200 * @return {@code true} if an element {@code o} is in file cache 201 */ 202 public boolean contains(Object o) { 203 return fileCache.contains(o); 204 } 205 206 /** 207 * Returns an index of element {@code o} in file cache. 208 * 209 * @param o an element 210 * @return an index of element {@code o} in file cache 211 */ 212 public int indexOf(Object o) { 213 return fileCache.indexOf(o); 214 } 215 216 public Object getElementAt(int index) { 217 return fileCache.get(index); 218 } 219 220 /** 221 * Obsolete - not used. 222 * @param e list data event 223 */ 224 public void intervalAdded(ListDataEvent e) { 225 } 226 227 /** 228 * Obsolete - not used. 229 * @param e list data event 230 */ 231 public void intervalRemoved(ListDataEvent e) { 232 } 233 234 /** 235 * Sorts a list of files. 236 * 237 * @param v a list of files 238 */ 239 protected void sort(Vector<? extends File> v){ 240 ShellFolder.sort(v); 241 } 242 243 /** 244 * Obsolete - not used 245 * @return a comparison of the file names 246 * @param a a file 247 * @param b another file 248 */ 249 protected boolean lt(File a, File b) { 250 // First ignore case when comparing 251 int diff = a.getName().toLowerCase().compareTo(b.getName().toLowerCase()); 252 if (diff != 0) { 253 return diff < 0; 254 } else { 255 // May differ in case (e.g. "mail" vs. "Mail") 256 return a.getName().compareTo(b.getName()) < 0; 257 } 258 } 259 260 261 class LoadFilesThread extends Thread { 262 File currentDirectory = null; 263 int fid; 264 Vector<DoChangeContents> runnables = new Vector<DoChangeContents>(10); 265 266 public LoadFilesThread(File currentDirectory, int fid) { 267 super("Basic L&F File Loading Thread"); 268 this.currentDirectory = currentDirectory; 269 this.fid = fid; 270 } 271 272 public void run() { 273 run0(); 274 setBusy(false, fid); 275 } 276 277 public void run0() { 278 FileSystemView fileSystem = filechooser.getFileSystemView(); 279 280 if (isInterrupted()) { 281 return; 282 } 283 284 File[] list = fileSystem.getFiles(currentDirectory, filechooser.isFileHidingEnabled()); 285 286 if (isInterrupted()) { 287 return; 288 } 289 290 final Vector<File> newFileCache = new Vector<File>(); 291 Vector<File> newFiles = new Vector<File>(); 292 293 // run through the file list, add directories and selectable files to fileCache 294 // Note that this block must be OUTSIDE of Invoker thread because of 295 // deadlock possibility with custom synchronized FileSystemView 296 for (File file : list) { 297 if (filechooser.accept(file)) { 298 boolean isTraversable = filechooser.isTraversable(file); 299 300 if (isTraversable) { 301 newFileCache.addElement(file); 302 } else if (filechooser.isFileSelectionEnabled()) { 303 newFiles.addElement(file); 304 } 305 306 if (isInterrupted()) { 307 return; 308 } 309 } 310 } 311 312 // First sort alphabetically by filename 313 sort(newFileCache); 314 sort(newFiles); 315 316 newFileCache.addAll(newFiles); 317 318 // To avoid loads of synchronizations with Invoker and improve performance we 319 // execute the whole block on the COM thread 320 DoChangeContents doChangeContents = ShellFolder.invoke(new Callable<DoChangeContents>() { 321 public DoChangeContents call() { 322 int newSize = newFileCache.size(); 323 int oldSize = fileCache.size(); 324 325 if (newSize > oldSize) { 326 //see if interval is added 327 int start = oldSize; 328 int end = newSize; 329 for (int i = 0; i < oldSize; i++) { 330 if (!newFileCache.get(i).equals(fileCache.get(i))) { 331 start = i; 332 for (int j = i; j < newSize; j++) { 333 if (newFileCache.get(j).equals(fileCache.get(i))) { 334 end = j; 335 break; 336 } 337 } 338 break; 339 } 340 } 341 if (start >= 0 && end > start 342 && newFileCache.subList(end, newSize).equals(fileCache.subList(start, oldSize))) { 343 if (isInterrupted()) { 344 return null; 345 } 346 return new DoChangeContents(newFileCache.subList(start, end), start, null, 0, fid); 347 } 348 } else if (newSize < oldSize) { 349 //see if interval is removed 350 int start = -1; 351 int end = -1; 352 for (int i = 0; i < newSize; i++) { 353 if (!newFileCache.get(i).equals(fileCache.get(i))) { 354 start = i; 355 end = i + oldSize - newSize; 356 break; 357 } 358 } 359 if (start >= 0 && end > start 360 && fileCache.subList(end, oldSize).equals(newFileCache.subList(start, newSize))) { 361 if (isInterrupted()) { 362 return null; 363 } 364 return new DoChangeContents(null, 0, new Vector<>(fileCache.subList(start, end)), start, fid); 365 } 366 } 367 if (!fileCache.equals(newFileCache)) { 368 if (isInterrupted()) { 369 cancelRunnables(runnables); 370 } 371 return new DoChangeContents(newFileCache, 0, fileCache, 0, fid); 372 } 373 return null; 374 } 375 }); 376 377 if (doChangeContents != null) { 378 runnables.addElement(doChangeContents); 379 SwingUtilities.invokeLater(doChangeContents); 380 } 381 } 382 383 384 public void cancelRunnables(Vector<DoChangeContents> runnables) { 385 for (DoChangeContents runnable : runnables) { 386 runnable.cancel(); 387 } 388 } 389 390 public void cancelRunnables() { 391 cancelRunnables(runnables); 392 } 393 } 394 395 396 /** 397 * Adds a PropertyChangeListener to the listener list. The listener is 398 * registered for all bound properties of this class. 399 * <p> 400 * If <code>listener</code> is <code>null</code>, 401 * no exception is thrown and no action is performed. 402 * 403 * @param listener the property change listener to be added 404 * 405 * @see #removePropertyChangeListener 406 * @see #getPropertyChangeListeners 407 * 408 * @since 1.6 409 */ 410 public void addPropertyChangeListener(PropertyChangeListener listener) { 411 if (changeSupport == null) { 412 changeSupport = new PropertyChangeSupport(this); 413 } 414 changeSupport.addPropertyChangeListener(listener); 415 } 416 417 /** 418 * Removes a PropertyChangeListener from the listener list. 419 * <p> 420 * If listener is null, no exception is thrown and no action is performed. 421 * 422 * @param listener the PropertyChangeListener to be removed 423 * 424 * @see #addPropertyChangeListener 425 * @see #getPropertyChangeListeners 426 * 427 * @since 1.6 428 */ 429 public void removePropertyChangeListener(PropertyChangeListener listener) { 430 if (changeSupport != null) { 431 changeSupport.removePropertyChangeListener(listener); 432 } 433 } 434 435 /** 436 * Returns an array of all the property change listeners 437 * registered on this component. 438 * 439 * @return all of this component's <code>PropertyChangeListener</code>s 440 * or an empty array if no property change 441 * listeners are currently registered 442 * 443 * @see #addPropertyChangeListener 444 * @see #removePropertyChangeListener 445 * @see java.beans.PropertyChangeSupport#getPropertyChangeListeners 446 * 447 * @since 1.6 448 */ 449 public PropertyChangeListener[] getPropertyChangeListeners() { 450 if (changeSupport == null) { 451 return new PropertyChangeListener[0]; 452 } 453 return changeSupport.getPropertyChangeListeners(); 454 } 455 456 /** 457 * Support for reporting bound property changes for boolean properties. 458 * This method can be called when a bound property has changed and it will 459 * send the appropriate PropertyChangeEvent to any registered 460 * PropertyChangeListeners. 461 * 462 * @param propertyName the property whose value has changed 463 * @param oldValue the property's previous value 464 * @param newValue the property's new value 465 * 466 * @since 1.6 467 */ 468 protected void firePropertyChange(String propertyName, 469 Object oldValue, Object newValue) { 470 if (changeSupport != null) { 471 changeSupport.firePropertyChange(propertyName, 472 oldValue, newValue); 473 } 474 } 475 476 477 /** 478 * Set the busy state for the model. The model is considered 479 * busy when it is running a separate (interruptable) 480 * thread in order to load the contents of a directory. 481 */ 482 private synchronized void setBusy(final boolean busy, int fid) { 483 if (fid == fetchID) { 484 boolean oldValue = this.busy; 485 this.busy = busy; 486 487 if (changeSupport != null && busy != oldValue) { 488 SwingUtilities.invokeLater(new Runnable() { 489 public void run() { 490 firePropertyChange("busy", !busy, busy); 491 } 492 }); 493 } 494 } 495 } 496 497 498 class DoChangeContents implements Runnable { 499 private List<File> addFiles; 500 private List<File> remFiles; 501 private boolean doFire = true; 502 private int fid; 503 private int addStart = 0; 504 private int remStart = 0; 505 506 public DoChangeContents(List<File> addFiles, int addStart, List<File> remFiles, int remStart, int fid) { 507 this.addFiles = addFiles; 508 this.addStart = addStart; 509 this.remFiles = remFiles; 510 this.remStart = remStart; 511 this.fid = fid; 512 } 513 514 synchronized void cancel() { 515 doFire = false; 516 } 517 518 public synchronized void run() { 519 if (fetchID == fid && doFire) { 520 int remSize = (remFiles == null) ? 0 : remFiles.size(); 521 int addSize = (addFiles == null) ? 0 : addFiles.size(); 522 synchronized(fileCache) { 523 if (remSize > 0) { 524 fileCache.removeAll(remFiles); 525 } 526 if (addSize > 0) { 527 fileCache.addAll(addStart, addFiles); 528 } 529 files = null; 530 directories = null; 531 } 532 if (remSize > 0 && addSize == 0) { 533 fireIntervalRemoved(BasicDirectoryModel.this, remStart, remStart + remSize - 1); 534 } else if (addSize > 0 && remSize == 0 && addStart + addSize <= fileCache.size()) { 535 fireIntervalAdded(BasicDirectoryModel.this, addStart, addStart + addSize - 1); 536 } else { 537 fireContentsChanged(); 538 } 539 } 540 } 541 } 542 }