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