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