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 */ 223 public void intervalAdded(ListDataEvent e) { 224 } 225 226 /** 227 * Obsolete - not used. 228 */ 229 public void intervalRemoved(ListDataEvent e) { 230 } 231 232 /** 233 * Sorts a list of files. 234 * 235 * @param v a list of files 236 */ 237 protected void sort(Vector<? extends File> v){ 238 ShellFolder.sort(v); 239 } 240 241 // Obsolete - not used 242 protected boolean lt(File a, File b) { 243 // First ignore case when comparing 244 int diff = a.getName().toLowerCase().compareTo(b.getName().toLowerCase()); 245 if (diff != 0) { 246 return diff < 0; 247 } else { 248 // May differ in case (e.g. "mail" vs. "Mail") 249 return a.getName().compareTo(b.getName()) < 0; 250 } 251 } 252 253 254 class LoadFilesThread extends Thread { 255 File currentDirectory = null; 256 int fid; 257 Vector<DoChangeContents> runnables = new Vector<DoChangeContents>(10); 258 259 public LoadFilesThread(File currentDirectory, int fid) { 260 super("Basic L&F File Loading Thread"); 261 this.currentDirectory = currentDirectory; 262 this.fid = fid; 263 } 264 265 public void run() { 266 run0(); 267 setBusy(false, fid); 268 } 269 270 public void run0() { 271 FileSystemView fileSystem = filechooser.getFileSystemView(); 272 273 if (isInterrupted()) { 274 return; 275 } 276 277 File[] list = fileSystem.getFiles(currentDirectory, filechooser.isFileHidingEnabled()); 278 279 if (isInterrupted()) { 280 return; 281 } 282 283 final Vector<File> newFileCache = new Vector<File>(); 284 Vector<File> newFiles = new Vector<File>(); 285 286 // run through the file list, add directories and selectable files to fileCache 287 // Note that this block must be OUTSIDE of Invoker thread because of 288 // deadlock possibility with custom synchronized FileSystemView 289 for (File file : list) { 290 if (filechooser.accept(file)) { 291 boolean isTraversable = filechooser.isTraversable(file); 292 293 if (isTraversable) { 294 newFileCache.addElement(file); 295 } else if (filechooser.isFileSelectionEnabled()) { 296 newFiles.addElement(file); 297 } 298 299 if (isInterrupted()) { 300 return; 301 } 302 } 303 } 304 305 // First sort alphabetically by filename 306 sort(newFileCache); 307 sort(newFiles); 308 309 newFileCache.addAll(newFiles); 310 311 // To avoid loads of synchronizations with Invoker and improve performance we 312 // execute the whole block on the COM thread 313 DoChangeContents doChangeContents = ShellFolder.invoke(new Callable<DoChangeContents>() { 314 public DoChangeContents call() { 315 int newSize = newFileCache.size(); 316 int oldSize = fileCache.size(); 317 318 if (newSize > oldSize) { 319 //see if interval is added 320 int start = oldSize; 321 int end = newSize; 322 for (int i = 0; i < oldSize; i++) { 323 if (!newFileCache.get(i).equals(fileCache.get(i))) { 324 start = i; 325 for (int j = i; j < newSize; j++) { 326 if (newFileCache.get(j).equals(fileCache.get(i))) { 327 end = j; 328 break; 329 } 330 } 331 break; 332 } 333 } 334 if (start >= 0 && end > start 335 && newFileCache.subList(end, newSize).equals(fileCache.subList(start, oldSize))) { 336 if (isInterrupted()) { 337 return null; 338 } 339 return new DoChangeContents(newFileCache.subList(start, end), start, null, 0, fid); 340 } 341 } else if (newSize < oldSize) { 342 //see if interval is removed 343 int start = -1; 344 int end = -1; 345 for (int i = 0; i < newSize; i++) { 346 if (!newFileCache.get(i).equals(fileCache.get(i))) { 347 start = i; 348 end = i + oldSize - newSize; 349 break; 350 } 351 } 352 if (start >= 0 && end > start 353 && fileCache.subList(end, oldSize).equals(newFileCache.subList(start, newSize))) { 354 if (isInterrupted()) { 355 return null; 356 } 357 return new DoChangeContents(null, 0, new Vector<>(fileCache.subList(start, end)), start, fid); 358 } 359 } 360 if (!fileCache.equals(newFileCache)) { 361 if (isInterrupted()) { 362 cancelRunnables(runnables); 363 } 364 return new DoChangeContents(newFileCache, 0, fileCache, 0, fid); 365 } 366 return null; 367 } 368 }); 369 370 if (doChangeContents != null) { 371 runnables.addElement(doChangeContents); 372 SwingUtilities.invokeLater(doChangeContents); 373 } 374 } 375 376 377 public void cancelRunnables(Vector<DoChangeContents> runnables) { 378 for (DoChangeContents runnable : runnables) { 379 runnable.cancel(); 380 } 381 } 382 383 public void cancelRunnables() { 384 cancelRunnables(runnables); 385 } 386 } 387 388 389 /** 390 * Adds a PropertyChangeListener to the listener list. The listener is 391 * registered for all bound properties of this class. 392 * <p> 393 * If <code>listener</code> is <code>null</code>, 394 * no exception is thrown and no action is performed. 395 * 396 * @param listener the property change listener to be added 397 * 398 * @see #removePropertyChangeListener 399 * @see #getPropertyChangeListeners 400 * 401 * @since 1.6 402 */ 403 public void addPropertyChangeListener(PropertyChangeListener listener) { 404 if (changeSupport == null) { 405 changeSupport = new PropertyChangeSupport(this); 406 } 407 changeSupport.addPropertyChangeListener(listener); 408 } 409 410 /** 411 * Removes a PropertyChangeListener from the listener list. 412 * <p> 413 * If listener is null, no exception is thrown and no action is performed. 414 * 415 * @param listener the PropertyChangeListener to be removed 416 * 417 * @see #addPropertyChangeListener 418 * @see #getPropertyChangeListeners 419 * 420 * @since 1.6 421 */ 422 public void removePropertyChangeListener(PropertyChangeListener listener) { 423 if (changeSupport != null) { 424 changeSupport.removePropertyChangeListener(listener); 425 } 426 } 427 428 /** 429 * Returns an array of all the property change listeners 430 * registered on this component. 431 * 432 * @return all of this component's <code>PropertyChangeListener</code>s 433 * or an empty array if no property change 434 * listeners are currently registered 435 * 436 * @see #addPropertyChangeListener 437 * @see #removePropertyChangeListener 438 * @see java.beans.PropertyChangeSupport#getPropertyChangeListeners 439 * 440 * @since 1.6 441 */ 442 public PropertyChangeListener[] getPropertyChangeListeners() { 443 if (changeSupport == null) { 444 return new PropertyChangeListener[0]; 445 } 446 return changeSupport.getPropertyChangeListeners(); 447 } 448 449 /** 450 * Support for reporting bound property changes for boolean properties. 451 * This method can be called when a bound property has changed and it will 452 * send the appropriate PropertyChangeEvent to any registered 453 * PropertyChangeListeners. 454 * 455 * @param propertyName the property whose value has changed 456 * @param oldValue the property's previous value 457 * @param newValue the property's new value 458 * 459 * @since 1.6 460 */ 461 protected void firePropertyChange(String propertyName, 462 Object oldValue, Object newValue) { 463 if (changeSupport != null) { 464 changeSupport.firePropertyChange(propertyName, 465 oldValue, newValue); 466 } 467 } 468 469 470 /** 471 * Set the busy state for the model. The model is considered 472 * busy when it is running a separate (interruptable) 473 * thread in order to load the contents of a directory. 474 */ 475 private synchronized void setBusy(final boolean busy, int fid) { 476 if (fid == fetchID) { 477 boolean oldValue = this.busy; 478 this.busy = busy; 479 480 if (changeSupport != null && busy != oldValue) { 481 SwingUtilities.invokeLater(new Runnable() { 482 public void run() { 483 firePropertyChange("busy", !busy, busy); 484 } 485 }); 486 } 487 } 488 } 489 490 491 class DoChangeContents implements Runnable { 492 private List<File> addFiles; 493 private List<File> remFiles; 494 private boolean doFire = true; 495 private int fid; 496 private int addStart = 0; 497 private int remStart = 0; 498 499 public DoChangeContents(List<File> addFiles, int addStart, List<File> remFiles, int remStart, int fid) { 500 this.addFiles = addFiles; 501 this.addStart = addStart; 502 this.remFiles = remFiles; 503 this.remStart = remStart; 504 this.fid = fid; 505 } 506 507 synchronized void cancel() { 508 doFire = false; 509 } 510 511 public synchronized void run() { 512 if (fetchID == fid && doFire) { 513 int remSize = (remFiles == null) ? 0 : remFiles.size(); 514 int addSize = (addFiles == null) ? 0 : addFiles.size(); 515 synchronized(fileCache) { 516 if (remSize > 0) { 517 fileCache.removeAll(remFiles); 518 } 519 if (addSize > 0) { 520 fileCache.addAll(addStart, addFiles); 521 } 522 files = null; 523 directories = null; 524 } 525 if (remSize > 0 && addSize == 0) { 526 fireIntervalRemoved(BasicDirectoryModel.this, remStart, remStart + remSize - 1); 527 } else if (addSize > 0 && remSize == 0 && addStart + addSize <= fileCache.size()) { 528 fireIntervalAdded(BasicDirectoryModel.this, addStart, addStart + addSize - 1); 529 } else { 530 fireContentsChanged(); 531 } 532 } 533 } 534 } 535 }