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