001 /* BasicDirectoryModel.java -- 002 Copyright (C) 2005, 2006 Free Software Foundation, Inc. 003 004 This file is part of GNU Classpath. 005 006 GNU Classpath is free software; you can redistribute it and/or modify 007 it under the terms of the GNU General Public License as published by 008 the Free Software Foundation; either version 2, or (at your option) 009 any later version. 010 011 GNU Classpath is distributed in the hope that it will be useful, but 012 WITHOUT ANY WARRANTY; without even the implied warranty of 013 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 014 General Public License for more details. 015 016 You should have received a copy of the GNU General Public License 017 along with GNU Classpath; see the file COPYING. If not, write to the 018 Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 019 02110-1301 USA. 020 021 Linking this library statically or dynamically with other modules is 022 making a combined work based on this library. Thus, the terms and 023 conditions of the GNU General Public License cover the whole 024 combination. 025 026 As a special exception, the copyright holders of this library give you 027 permission to link this library with independent modules to produce an 028 executable, regardless of the license terms of these independent 029 modules, and to copy and distribute the resulting executable under 030 terms of your choice, provided that you also meet, for each linked 031 independent module, the terms and conditions of the license of that 032 module. An independent module is a module which is not derived from 033 or based on this library. If you modify this library, you may extend 034 this exception to your version of the library, but you are not 035 obligated to do so. If you do not wish to do so, delete this 036 exception statement from your version. */ 037 038 package javax.swing.plaf.basic; 039 040 import java.beans.PropertyChangeEvent; 041 import java.beans.PropertyChangeListener; 042 import java.io.File; 043 import java.util.ArrayList; 044 import java.util.Collections; 045 import java.util.Comparator; 046 import java.util.Enumeration; 047 import java.util.Iterator; 048 import java.util.List; 049 import java.util.Vector; 050 import javax.swing.AbstractListModel; 051 import javax.swing.JFileChooser; 052 import javax.swing.SwingUtilities; 053 import javax.swing.event.ListDataEvent; 054 import javax.swing.filechooser.FileSystemView; 055 056 057 /** 058 * Implements an AbstractListModel for directories where the source 059 * of the files is a JFileChooser object. 060 * 061 * This class is used for sorting and ordering the file list in 062 * a JFileChooser L&F object. 063 */ 064 public class BasicDirectoryModel extends AbstractListModel 065 implements PropertyChangeListener 066 { 067 /** The list of files itself */ 068 private Vector contents; 069 070 /** 071 * The directories in the list. 072 */ 073 private Vector directories; 074 075 /** 076 * The files in the list. 077 */ 078 private Vector files; 079 080 /** The listing mode of the associated JFileChooser, 081 either FILES_ONLY, DIRECTORIES_ONLY or FILES_AND_DIRECTORIES */ 082 private int listingMode; 083 084 /** The JFileCooser associated with this model */ 085 private JFileChooser filechooser; 086 087 /** 088 * The thread that loads the file view. 089 */ 090 private DirectoryLoadThread loadThread; 091 092 /** 093 * This thread is responsible for loading file lists from the 094 * current directory and updating the model. 095 */ 096 private class DirectoryLoadThread extends Thread 097 { 098 099 /** 100 * Updates the Swing list model. 101 */ 102 private class UpdateSwingRequest 103 implements Runnable 104 { 105 106 private List added; 107 private int addIndex; 108 private List removed; 109 private int removeIndex; 110 private boolean cancel; 111 112 UpdateSwingRequest(List add, int ai, List rem, int ri) 113 { 114 added = add; 115 addIndex = ai; 116 removed = rem; 117 removeIndex = ri; 118 cancel = false; 119 } 120 121 public void run() 122 { 123 if (! cancel) 124 { 125 int numRemoved = removed == null ? 0 : removed.size(); 126 int numAdded = added == null ? 0 : added.size(); 127 synchronized (contents) 128 { 129 if (numRemoved > 0) 130 contents.removeAll(removed); 131 if (numAdded > 0) 132 contents.addAll(added); 133 134 files = null; 135 directories = null; 136 } 137 if (numRemoved > 0 && numAdded == 0) 138 fireIntervalRemoved(BasicDirectoryModel.this, removeIndex, 139 removeIndex + numRemoved - 1); 140 else if (numRemoved == 0 && numAdded > 0) 141 fireIntervalAdded(BasicDirectoryModel.this, addIndex, 142 addIndex + numAdded - 1); 143 else 144 fireContentsChanged(); 145 } 146 } 147 148 void cancel() 149 { 150 cancel = true; 151 } 152 } 153 154 /** 155 * The directory beeing loaded. 156 */ 157 File directory; 158 159 /** 160 * Stores all UpdateSwingRequests that are sent to the event queue. 161 */ 162 private UpdateSwingRequest pending; 163 164 /** 165 * Creates a new DirectoryLoadThread that loads the specified 166 * directory. 167 * 168 * @param dir the directory to load 169 */ 170 DirectoryLoadThread(File dir) 171 { 172 super("Basic L&F directory loader"); 173 directory = dir; 174 } 175 176 public void run() 177 { 178 FileSystemView fsv = filechooser.getFileSystemView(); 179 File[] files = fsv.getFiles(directory, 180 filechooser.isFileHidingEnabled()); 181 182 // Occasional check if we have been interrupted. 183 if (isInterrupted()) 184 return; 185 186 // Check list for accepted files. 187 Vector accepted = new Vector(); 188 for (int i = 0; i < files.length; i++) 189 { 190 if (filechooser.accept(files[i])) 191 accepted.add(files[i]); 192 } 193 194 // Occasional check if we have been interrupted. 195 if (isInterrupted()) 196 return; 197 198 // Sort list. 199 sort(accepted); 200 201 // Now split up directories from files so that we get the directories 202 // listed before the files. 203 Vector newFiles = new Vector(); 204 Vector newDirectories = new Vector(); 205 for (Iterator i = accepted.iterator(); i.hasNext();) 206 { 207 File f = (File) i.next(); 208 boolean traversable = filechooser.isTraversable(f); 209 if (traversable) 210 newDirectories.add(f); 211 else if (! traversable && filechooser.isFileSelectionEnabled()) 212 newFiles.add(f); 213 214 // Occasional check if we have been interrupted. 215 if (isInterrupted()) 216 return; 217 218 } 219 220 // Build up new file cache. Try to update only the changed elements. 221 // This will be important for actions like adding new files or 222 // directories inside a large file list. 223 Vector newCache = new Vector(newDirectories); 224 newCache.addAll(newFiles); 225 226 int newSize = newCache.size(); 227 int oldSize = contents.size(); 228 if (newSize < oldSize) 229 { 230 // Check for removed interval. 231 int start = -1; 232 int end = -1; 233 boolean found = false; 234 for (int i = 0; i < newSize && !found; i++) 235 { 236 if (! newCache.get(i).equals(contents.get(i))) 237 { 238 start = i; 239 end = i + oldSize - newSize; 240 found = true; 241 } 242 } 243 if (start >= 0 && end > start 244 && contents.subList(end, oldSize) 245 .equals(newCache.subList(start, newSize))) 246 { 247 // Occasional check if we have been interrupted. 248 if (isInterrupted()) 249 return; 250 251 Vector removed = new Vector(contents.subList(start, end)); 252 UpdateSwingRequest r = new UpdateSwingRequest(null, 0, 253 removed, start); 254 invokeLater(r); 255 newCache = null; 256 } 257 } 258 else if (newSize > oldSize) 259 { 260 // Check for inserted interval. 261 int start = oldSize; 262 int end = newSize; 263 boolean found = false; 264 for (int i = 0; i < oldSize && ! found; i++) 265 { 266 if (! newCache.get(i).equals(contents.get(i))) 267 { 268 start = i; 269 boolean foundEnd = false; 270 for (int j = i; j < newSize && ! foundEnd; j++) 271 { 272 if (newCache.get(j).equals(contents.get(i))) 273 { 274 end = j; 275 foundEnd = true; 276 } 277 } 278 end = i + oldSize - newSize; 279 } 280 } 281 if (start >= 0 && end > start 282 && newCache.subList(end, newSize) 283 .equals(contents.subList(start, oldSize))) 284 { 285 // Occasional check if we have been interrupted. 286 if (isInterrupted()) 287 return; 288 289 List added = newCache.subList(start, end); 290 UpdateSwingRequest r = new UpdateSwingRequest(added, start, 291 null, 0); 292 invokeLater(r); 293 newCache = null; 294 } 295 } 296 297 // Handle complete list changes (newCache != null). 298 if (newCache != null && ! contents.equals(newCache)) 299 { 300 // Occasional check if we have been interrupted. 301 if (isInterrupted()) 302 return; 303 UpdateSwingRequest r = new UpdateSwingRequest(newCache, 0, 304 contents, 0); 305 invokeLater(r); 306 } 307 } 308 309 /** 310 * Wraps SwingUtilities.invokeLater() and stores the request in 311 * a Vector so that we can still cancel it later. 312 * 313 * @param update the request to invoke 314 */ 315 private void invokeLater(UpdateSwingRequest update) 316 { 317 pending = update; 318 SwingUtilities.invokeLater(update); 319 } 320 321 /** 322 * Cancels all pending update requests that might be in the AWT 323 * event queue. 324 */ 325 void cancelPending() 326 { 327 if (pending != null) 328 pending.cancel(); 329 } 330 } 331 332 /** A Comparator class/object for sorting the file list. */ 333 private Comparator comparator = new Comparator() 334 { 335 public int compare(Object o1, Object o2) 336 { 337 if (lt((File) o1, (File) o2)) 338 return -1; 339 else 340 return 1; 341 } 342 }; 343 344 /** 345 * Creates a new BasicDirectoryModel object. 346 * 347 * @param filechooser DOCUMENT ME! 348 */ 349 public BasicDirectoryModel(JFileChooser filechooser) 350 { 351 this.filechooser = filechooser; 352 filechooser.addPropertyChangeListener(this); 353 listingMode = filechooser.getFileSelectionMode(); 354 contents = new Vector(); 355 validateFileCache(); 356 } 357 358 /** 359 * Returns whether a given (File) object is included in the list. 360 * 361 * @param o - The file object to test. 362 * 363 * @return <code>true</code> if the list contains the given object. 364 */ 365 public boolean contains(Object o) 366 { 367 return contents.contains(o); 368 } 369 370 /** 371 * Fires a content change event. 372 */ 373 public void fireContentsChanged() 374 { 375 fireContentsChanged(this, 0, getSize() - 1); 376 } 377 378 /** 379 * Returns a Vector of (java.io.File) objects containing 380 * the directories in this list. 381 * 382 * @return a Vector 383 */ 384 public Vector<File> getDirectories() 385 { 386 // Synchronize this with the UpdateSwingRequest for the case when 387 // contents is modified. 388 synchronized (contents) 389 { 390 Vector dirs = directories; 391 if (dirs == null) 392 { 393 // Initializes this in getFiles(). 394 getFiles(); 395 dirs = directories; 396 } 397 return dirs; 398 } 399 } 400 401 /** 402 * Returns the (java.io.File) object at 403 * an index in the list. 404 * 405 * @param index The list index 406 * @return a File object 407 */ 408 public Object getElementAt(int index) 409 { 410 if (index > getSize() - 1) 411 return null; 412 return contents.elementAt(index); 413 } 414 415 /** 416 * Returns a Vector of (java.io.File) objects containing 417 * the files in this list. 418 * 419 * @return a Vector 420 */ 421 public Vector<File> getFiles() 422 { 423 synchronized (contents) 424 { 425 Vector f = files; 426 if (f == null) 427 { 428 f = new Vector(); 429 Vector d = new Vector(); // Directories; 430 for (Iterator i = contents.iterator(); i.hasNext();) 431 { 432 File file = (File) i.next(); 433 if (filechooser.isTraversable(file)) 434 d.add(file); 435 else 436 f.add(file); 437 } 438 files = f; 439 directories = d; 440 } 441 return f; 442 } 443 } 444 445 /** 446 * Returns the size of the list, which only includes directories 447 * if the JFileChooser is set to DIRECTORIES_ONLY. 448 * 449 * Otherwise, both directories and files are included in the count. 450 * 451 * @return The size of the list. 452 */ 453 public int getSize() 454 { 455 return contents.size(); 456 } 457 458 /** 459 * Returns the index of an (java.io.File) object in the list. 460 * 461 * @param o The object - normally a File. 462 * 463 * @return the index of that object, or -1 if it is not in the list. 464 */ 465 public int indexOf(Object o) 466 { 467 return contents.indexOf(o); 468 } 469 470 /** 471 * Obsoleted method which does nothing. 472 */ 473 public void intervalAdded(ListDataEvent e) 474 { 475 // obsoleted 476 } 477 478 /** 479 * Obsoleted method which does nothing. 480 */ 481 public void intervalRemoved(ListDataEvent e) 482 { 483 // obsoleted 484 } 485 486 /** 487 * Obsoleted method which does nothing. 488 */ 489 public void invalidateFileCache() 490 { 491 // obsoleted 492 } 493 494 /** 495 * Less than, determine the relative order in the list of two files 496 * for sorting purposes. 497 * 498 * The order is: directories < files, and thereafter alphabetically, 499 * using the default locale collation. 500 * 501 * @param a the first file 502 * @param b the second file 503 * 504 * @return <code>true</code> if a > b, <code>false</code> if a < b. 505 */ 506 protected boolean lt(File a, File b) 507 { 508 boolean aTrav = filechooser.isTraversable(a); 509 boolean bTrav = filechooser.isTraversable(b); 510 511 if (aTrav == bTrav) 512 { 513 String aname = a.getName().toLowerCase(); 514 String bname = b.getName().toLowerCase(); 515 return (aname.compareTo(bname) < 0) ? true : false; 516 } 517 else 518 { 519 if (aTrav) 520 return true; 521 else 522 return false; 523 } 524 } 525 526 /** 527 * Listens for a property change; the change in file selection mode of the 528 * associated JFileChooser. Reloads the file cache on that event. 529 * 530 * @param e - A PropertyChangeEvent. 531 */ 532 public void propertyChange(PropertyChangeEvent e) 533 { 534 String property = e.getPropertyName(); 535 if (property.equals(JFileChooser.DIRECTORY_CHANGED_PROPERTY) 536 || property.equals(JFileChooser.FILE_FILTER_CHANGED_PROPERTY) 537 || property.equals(JFileChooser.FILE_HIDING_CHANGED_PROPERTY) 538 || property.equals(JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) 539 || property.equals(JFileChooser.FILE_VIEW_CHANGED_PROPERTY) 540 ) 541 { 542 validateFileCache(); 543 } 544 } 545 546 /** 547 * Renames a file - However, does <I>not</I> re-sort the list 548 * or replace the old file with the new one in the list. 549 * 550 * @param oldFile The old file 551 * @param newFile The new file name 552 * 553 * @return <code>true</code> if the rename succeeded 554 */ 555 public boolean renameFile(File oldFile, File newFile) 556 { 557 return oldFile.renameTo( newFile ); 558 } 559 560 /** 561 * Sorts a Vector of File objects. 562 * 563 * @param v The Vector to sort. 564 */ 565 protected void sort(Vector<? extends File> v) 566 { 567 Collections.sort(v, comparator); 568 } 569 570 /** 571 * Re-loads the list of files 572 */ 573 public void validateFileCache() 574 { 575 File dir = filechooser.getCurrentDirectory(); 576 if (dir != null) 577 { 578 // Cancel all pending requests. 579 if (loadThread != null) 580 { 581 loadThread.interrupt(); 582 loadThread.cancelPending(); 583 } 584 loadThread = new DirectoryLoadThread(dir); 585 loadThread.start(); 586 } 587 } 588 } 589