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