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 
AquaFileSystemModel(final JFileChooser filechooser, final JTable filelist, final String[] colNames)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 
updateSelectionMode()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 
propertyChange(final PropertyChangeEvent e)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 
invalidateFileCache()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 
getDirectories()126     public Vector<File> getDirectories() {
127         if (directories != null) { return directories; }
128         return directories;
129     }
130 
getFiles()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 
runWhenDone(final Runnable runnable)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 
validateFileCache()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 
getColumnCount()189     public int getColumnCount() {
190         return 2;
191     }
192 
getColumnName(final int col)193     public String getColumnName(final int col) {
194         return fColumnNames[col];
195     }
196 
getColumnClass(final int col)197     public Class<? extends Object> getColumnClass(final int col) {
198         if (col == 0) return File.class;
199         return Date.class;
200     }
201 
getRowCount()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.
contains(final File o)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 
indexOf(final File o)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
getElementAt(final int row)235     public Object getElementAt(final int row) {
236         return getValueAt(row, 0);
237     }
238 
239     // AbstractTableModel interface
240 
getValueAt(int row, final int col)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
intervalAdded(final ListDataEvent e)254     public void intervalAdded(final ListDataEvent e) {
255     }
256 
257     // PENDING(jeff) - implement
intervalRemoved(final ListDataEvent e)258     public void intervalRemoved(final ListDataEvent e) {
259     }
260 
sort(final Vector<Object> v)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 {
quickSort(final Vector<Object> v, final int lo0, final int hi0)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 
swap(final Vector<Object> a, final int i, final int j)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 
lt(SortableFile a, SortableFile b)338         protected abstract boolean lt(SortableFile a, SortableFile b);
339     }
340 
341     class QuickSortNames extends QuickSort {
lt(final SortableFile a, final SortableFile b)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 {
lt(final SortableFile a, final SortableFile b)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 
SortableFile(final File f)362         SortableFile(final File f) {
363             fFile = f;
364             fName = fFile.getName();
365             fDateValue = fFile.lastModified();
366             fDate = new Date(fDateValue);
367         }
368 
getValueAt(final int col)369         public Object getValueAt(final int col) {
370             if (col == 0) return fFile;
371             return fDate;
372         }
373 
equals(final Object other)374         public boolean equals(final Object other) {
375             final SortableFile otherFile = (SortableFile)other;
376             return otherFile.fFile.equals(fFile);
377         }
378 
379         @Override
hashCode()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 
FilesLoader(final File currentDirectory, final int fid)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
run()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 
cancelRunnables(final Vector<DoChangeContents> runnables)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 
DoChangeContents(final Vector<SortableFile> files, final int fid)458         public DoChangeContents(final Vector<SortableFile> files, final int fid) {
459             this.contentFiles = files;
460             this.fid = fid;
461         }
462 
cancel()463         synchronized void cancel() {
464             synchronized(lock) {
465                 doFire = false;
466             }
467         }
468 
run()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 }
492