1 /*
   2  * Copyright (c) 2011, 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 com.apple.laf;
  27 
  28 import java.beans.*;
  29 import java.io.File;
  30 import java.util.*;
  31 
  32 import javax.swing.*;
  33 import javax.swing.event.ListDataEvent;
  34 import javax.swing.filechooser.FileSystemView;
  35 import javax.swing.table.AbstractTableModel;
  36 
  37 /**
  38  * NavServices-like implementation of a file Table
  39  *
  40  * Some of it came from BasicDirectoryModel
  41  */
  42 class AquaFileSystemModel extends AbstractTableModel implements PropertyChangeListener {
  43     private final JTable fFileList;
  44     private LoadFilesThread loadThread = null;
  45     private Vector<File> files = null;
  46 
  47     JFileChooser filechooser = null;
  48     Vector<SortableFile> fileCache = null;
  49     Object fileCacheLock;
  50 
  51     Vector<File> directories = null;
  52     int fetchID = 0;
  53 
  54     private final boolean fSortAscending[] = {true, true};
  55     // private boolean fSortAscending = true;
  56     private boolean fSortNames = true;
  57     private final String[] fColumnNames;
  58     public final static String SORT_BY_CHANGED = "sortByChanged";
  59     public final static String SORT_ASCENDING_CHANGED = "sortAscendingChanged";
  60 
  61     public AquaFileSystemModel(final JFileChooser filechooser, final JTable filelist, final String[] colNames) {
  62         fileCacheLock = new Object();
  63         this.filechooser = filechooser;
  64         fFileList = filelist;
  65         fColumnNames = colNames;
  66         validateFileCache();
  67         updateSelectionMode();
  68     }
  69 
  70     void updateSelectionMode() {
  71         // Save dialog lists can't be multi select, because all we're selecting is the next folder to open
  72         final boolean b = filechooser.isMultiSelectionEnabled() && filechooser.getDialogType() != JFileChooser.SAVE_DIALOG;
  73         fFileList.setSelectionMode(b ? ListSelectionModel.MULTIPLE_INTERVAL_SELECTION : ListSelectionModel.SINGLE_SELECTION);
  74     }
  75 
  76     public void propertyChange(final PropertyChangeEvent e) {
  77         final String prop = e.getPropertyName();
  78         if (prop == JFileChooser.DIRECTORY_CHANGED_PROPERTY || prop == JFileChooser.FILE_VIEW_CHANGED_PROPERTY || prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY || prop == JFileChooser.FILE_HIDING_CHANGED_PROPERTY) {
  79             invalidateFileCache();
  80             validateFileCache();
  81         } else if (prop.equals(JFileChooser.MULTI_SELECTION_ENABLED_CHANGED_PROPERTY)) {
  82             updateSelectionMode();
  83         } else if (prop == JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) {
  84             invalidateFileCache();
  85             validateFileCache();
  86         }
  87         if (prop == SORT_BY_CHANGED) {// $ Ought to just resort
  88             fSortNames = (((Integer)e.getNewValue()).intValue() == 0);
  89             invalidateFileCache();
  90             validateFileCache();
  91             fFileList.repaint();
  92         }
  93         if (prop == SORT_ASCENDING_CHANGED) {
  94             final int sortColumn = (fSortNames ? 0 : 1);
  95             fSortAscending[sortColumn] = ((Boolean)e.getNewValue()).booleanValue();
  96             invalidateFileCache();
  97             validateFileCache();
  98             fFileList.repaint();
  99         }
 100     }
 101 
 102     public void invalidateFileCache() {
 103         files = null;
 104         directories = null;
 105 
 106         synchronized(fileCacheLock) {
 107             if (fileCache != null) {
 108                 final int lastRow = fileCache.size();
 109                 fileCache = null;
 110                 fireTableRowsDeleted(0, lastRow);
 111             }
 112         }
 113     }
 114 
 115     public Vector<File> getDirectories() {
 116         if (directories != null) { return directories; }
 117         return directories;
 118     }
 119 
 120     public Vector<File> getFiles() {
 121         if (files != null) { return files; }
 122         files = new Vector<File>();
 123         directories = new Vector<File>();
 124         directories.addElement(filechooser.getFileSystemView().createFileObject(filechooser.getCurrentDirectory(), ".."));
 125 
 126         synchronized(fileCacheLock) {
 127             for (int i = 0; i < fileCache.size(); i++) {
 128                 final SortableFile sf = fileCache.elementAt(i);
 129                 final File f = sf.fFile;
 130                 if (filechooser.isTraversable(f)) {
 131                     directories.addElement(f);
 132                 } else {
 133                     files.addElement(f);
 134                 }
 135             }
 136         }
 137 
 138         return files;
 139     }
 140 
 141     public void runWhenDone(final Runnable runnable){
 142          synchronized (fileCacheLock) {
 143              if (loadThread != null) {
 144                  if (loadThread.isAlive()) {
 145                      loadThread.queuedTasks.add(runnable);
 146                      return;
 147                  }
 148              }
 149 
 150              SwingUtilities.invokeLater(runnable);
 151          }
 152      }
 153 
 154     public void validateFileCache() {
 155         final File currentDirectory = filechooser.getCurrentDirectory();
 156 
 157         if (currentDirectory == null) {
 158             invalidateFileCache();
 159             return;
 160         }
 161 
 162         if (loadThread != null) {
 163             // interrupt
 164             loadThread.interrupt();
 165         }
 166 
 167         fetchID++;
 168 
 169         // PENDING(jeff) pick the size more sensibly
 170         invalidateFileCache();
 171         synchronized(fileCacheLock) {
 172             fileCache = new Vector<SortableFile>(50);
 173         }
 174 
 175         loadThread = new LoadFilesThread(currentDirectory, fetchID);
 176         loadThread.start();
 177     }
 178 
 179     public int getColumnCount() {
 180         return 2;
 181     }
 182 
 183     public String getColumnName(final int col) {
 184         return fColumnNames[col];
 185     }
 186 
 187     public Class<? extends Object> getColumnClass(final int col) {
 188         if (col == 0) return File.class;
 189         return Date.class;
 190     }
 191 
 192     public int getRowCount() {
 193         synchronized(fileCacheLock) {
 194             if (fileCache != null) {
 195                 return fileCache.size();
 196             }
 197             return 0;
 198         }
 199     }
 200 
 201     // SAK: Part of fix for 3168263. The fileCache contains
 202     // SortableFiles, so when finding a file in the list we need to
 203     // first create a sortable file.
 204     public boolean contains(final File o) {
 205         synchronized(fileCacheLock) {
 206             if (fileCache != null) {
 207                 return fileCache.contains(new SortableFile(o));
 208             }
 209             return false;
 210         }
 211     }
 212 
 213     public int indexOf(final File o) {
 214         synchronized(fileCacheLock) {
 215             if (fileCache != null) {
 216                 final boolean isAscending = fSortNames ? fSortAscending[0] : fSortAscending[1];
 217                 final int row = fileCache.indexOf(new SortableFile(o));
 218                 return isAscending ? row : fileCache.size() - row - 1;
 219             }
 220             return 0;
 221         }
 222     }
 223 
 224     // AbstractListModel interface
 225     public Object getElementAt(final int row) {
 226         return getValueAt(row, 0);
 227     }
 228 
 229     // AbstractTableModel interface
 230 
 231     public Object getValueAt(int row, final int col) {
 232         if (row < 0 || col < 0) return null;
 233         final boolean isAscending = fSortNames ? fSortAscending[0] : fSortAscending[1];
 234         synchronized(fileCacheLock) {
 235             if (fileCache != null) {
 236                 if (!isAscending) row = fileCache.size() - row - 1;
 237                 return fileCache.elementAt(row).getValueAt(col);
 238             }
 239             return null;
 240         }
 241     }
 242 
 243     // PENDING(jeff) - implement
 244     public void intervalAdded(final ListDataEvent e) {
 245     }
 246 
 247     // PENDING(jeff) - implement
 248     public void intervalRemoved(final ListDataEvent e) {
 249     }
 250 
 251     protected void sort(final Vector<Object> v) {
 252         if (fSortNames) sSortNames.quickSort(v, 0, v.size() - 1);
 253         else sSortDates.quickSort(v, 0, v.size() - 1);
 254     }
 255 
 256     // Liberated from the 1.1 SortDemo
 257     //
 258     // This is a generic version of C.A.R Hoare's Quick Sort
 259     // algorithm. This will handle arrays that are already
 260     // sorted, and arrays with duplicate keys.<BR>
 261     //
 262     // If you think of a one dimensional array as going from
 263     // the lowest index on the left to the highest index on the right
 264     // then the parameters to this function are lowest index or
 265     // left and highest index or right. The first time you call
 266     // this function it will be with the parameters 0, a.length - 1.
 267     //
 268     // @param a an integer array
 269     // @param lo0 left boundary of array partition
 270     // @param hi0 right boundary of array partition
 271     abstract class QuickSort {
 272         final void quickSort(final Vector<Object> v, final int lo0, final int hi0) {
 273             int lo = lo0;
 274             int hi = hi0;
 275             SortableFile mid;
 276 
 277             if (hi0 > lo0) {
 278                 // Arbitrarily establishing partition element as the midpoint of
 279                 // the array.
 280                 mid = (SortableFile)v.elementAt((lo0 + hi0) / 2);
 281 
 282                 // loop through the array until indices cross
 283                 while (lo <= hi) {
 284                     // find the first element that is greater than or equal to
 285                     // the partition element starting from the left Index.
 286                     //
 287                     // Nasty to have to cast here. Would it be quicker
 288                     // to copy the vectors into arrays and sort the arrays?
 289                     while ((lo < hi0) && lt((SortableFile)v.elementAt(lo), mid)) {
 290                         ++lo;
 291                     }
 292 
 293                     // find an element that is smaller than or equal to
 294                     // the partition element starting from the right Index.
 295                     while ((hi > lo0) && lt(mid, (SortableFile)v.elementAt(hi))) {
 296                         --hi;
 297                     }
 298 
 299                     // if the indexes have not crossed, swap
 300                     if (lo <= hi) {
 301                         swap(v, lo, hi);
 302                         ++lo;
 303                         --hi;
 304                     }
 305                 }
 306 
 307                 // If the right index has not reached the left side of array
 308                 // must now sort the left partition.
 309                 if (lo0 < hi) {
 310                     quickSort(v, lo0, hi);
 311                 }
 312 
 313                 // If the left index has not reached the right side of array
 314                 // must now sort the right partition.
 315                 if (lo < hi0) {
 316                     quickSort(v, lo, hi0);
 317                 }
 318 
 319             }
 320         }
 321 
 322         private final void swap(final Vector<Object> a, final int i, final int j) {
 323             final Object T = a.elementAt(i);
 324             a.setElementAt(a.elementAt(j), i);
 325             a.setElementAt(T, j);
 326         }
 327 
 328         protected abstract boolean lt(SortableFile a, SortableFile b);
 329     }
 330 
 331     class QuickSortNames extends QuickSort {
 332         protected boolean lt(final SortableFile a, final SortableFile b) {
 333             final String aLower = a.fName.toLowerCase();
 334             final String bLower = b.fName.toLowerCase();
 335             return aLower.compareTo(bLower) < 0;
 336         }
 337     }
 338 
 339     class QuickSortDates extends QuickSort {
 340         protected boolean lt(final SortableFile a, final SortableFile b) {
 341             return a.fDateValue < b.fDateValue;
 342         }
 343     }
 344 
 345     // for speed in sorting, displaying
 346     class SortableFile /* extends FileView */{
 347         File fFile;
 348         String fName;
 349         long fDateValue;
 350         Date fDate;
 351 
 352         SortableFile(final File f) {
 353             fFile = f;
 354             fName = fFile.getName();
 355             fDateValue = fFile.lastModified();
 356             fDate = new Date(fDateValue);
 357         }
 358 
 359         public Object getValueAt(final int col) {
 360             if (col == 0) return fFile;
 361             return fDate;
 362         }
 363 
 364         public boolean equals(final Object other) {
 365             final SortableFile otherFile = (SortableFile)other;
 366             return otherFile.fFile.equals(fFile);
 367         }
 368     }
 369 
 370     class LoadFilesThread extends Thread {
 371         Vector<Runnable> queuedTasks = new Vector<Runnable>();
 372         File currentDirectory = null;
 373         int fid;
 374 
 375         public LoadFilesThread(final File currentDirectory, final int fid) {
 376             super("Aqua L&F File Loading Thread");
 377             this.currentDirectory = currentDirectory;
 378             this.fid = fid;
 379         }
 380 
 381         public void run() {
 382             final Vector<DoChangeContents> runnables = new Vector<DoChangeContents>(10);
 383             final FileSystemView fileSystem = filechooser.getFileSystemView();
 384 
 385             final File[] list = fileSystem.getFiles(currentDirectory, filechooser.isFileHidingEnabled());
 386 
 387             final Vector<Object> acceptsList = new Vector<Object>();
 388 
 389             for (final File element : list) {
 390                 // Return all files to the file chooser. The UI will disable or enable
 391                 // the file name if the current filter approves.
 392                 acceptsList.addElement(new SortableFile(element));
 393             }
 394 
 395             // Sort based on settings.
 396             sort(acceptsList);
 397 
 398             // Don't separate directories from files
 399             Vector<SortableFile> chunk = new Vector<SortableFile>(10);
 400             final int listSize = acceptsList.size();
 401             // run through list grabbing file/dirs in chunks of ten
 402             for (int i = 0; i < listSize;) {
 403                 SortableFile f;
 404                 for (int j = 0; j < 10 && i < listSize; j++, i++) {
 405                     f = (SortableFile)acceptsList.elementAt(i);
 406                     chunk.addElement(f);
 407                 }
 408                 final DoChangeContents runnable = new DoChangeContents(chunk, fid);
 409                 runnables.addElement(runnable);
 410                 SwingUtilities.invokeLater(runnable);
 411                 chunk = new Vector<SortableFile>(10);
 412                 if (isInterrupted()) {
 413                     // interrupted, cancel all runnables
 414                     cancelRunnables(runnables);
 415                     return;
 416                 }
 417             }
 418 
 419             synchronized (fileCacheLock) {
 420                 for (final Runnable r : queuedTasks) {
 421                     SwingUtilities.invokeLater(r);
 422                 }
 423             }
 424         }
 425 
 426         public void cancelRunnables(final Vector<DoChangeContents> runnables) {
 427             for (int i = 0; i < runnables.size(); i++) {
 428                 runnables.elementAt(i).cancel();
 429             }
 430         }
 431     }
 432 
 433     class DoChangeContents implements Runnable {
 434         private Vector<SortableFile> contentFiles;
 435         private boolean doFire = true;
 436         private final Object lock = new Object();
 437         private final int fid;
 438 
 439         public DoChangeContents(final Vector<SortableFile> files, final int fid) {
 440             this.contentFiles = files;
 441             this.fid = fid;
 442         }
 443 
 444         synchronized void cancel() {
 445             synchronized(lock) {
 446                 doFire = false;
 447             }
 448         }
 449 
 450         public void run() {
 451             if (fetchID == fid) {
 452                 synchronized(lock) {
 453                     if (doFire) {
 454                         synchronized(fileCacheLock) {
 455                             if (fileCache != null) {
 456                                 for (int i = 0; i < contentFiles.size(); i++) {
 457                                     fileCache.addElement(contentFiles.elementAt(i));
 458                                     fireTableRowsInserted(i, i);
 459                                 }
 460                             }
 461                         }
 462                     }
 463                     contentFiles = null;
 464                     directories = null;
 465                 }
 466             }
 467         }
 468     }
 469 
 470     final QuickSortNames sSortNames = new QuickSortNames();
 471     final QuickSortDates sSortDates = new QuickSortDates();
 472 }