/* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
* Tiny Look and Feel *
* *
* (C) Copyright 2003 - 2007 Hans Bickel *
* *
* For licensing information and credits, please refer to the *
* comment in file de.muntjak.tinylookandfeel.TinyLookAndFeel *
* *
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * */
package de.muntjak.tinylookandfeel;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.io.File;
import java.util.List;
import java.util.Vector;
import javax.swing.JFileChooser;
import javax.swing.SwingUtilities;
import javax.swing.event.ListDataEvent;
import javax.swing.filechooser.FileSystemView;
import javax.swing.plaf.basic.BasicDirectoryModel;
import javax.swing.plaf.basic.BasicFileChooserUI;
import sun.awt.shell.ShellFolder;
/**
* A copy of BasicDirectoryModel with access to the fileCache.
* @author Hans Bickel
*
*/
public class TinyDirectoryModel extends BasicDirectoryModel {
private JFileChooser filechooser = null;
// PENDING(jeff) pick the size more sensibly
private Vector fileCache = new Vector(50);
private LoadFilesThread loadThread = null;
private Vector files = null;
private Vector directories = null;
private int fetchID = 0;
private PropertyChangeSupport changeSupport;
private boolean busy = false;
public TinyDirectoryModel(JFileChooser fc) {
super(fc);
this.filechooser = fc;
validateFileCache();
}
public void propertyChange(PropertyChangeEvent e) {
String prop = e.getPropertyName();
if(prop == JFileChooser.DIRECTORY_CHANGED_PROPERTY
|| prop == JFileChooser.FILE_VIEW_CHANGED_PROPERTY
|| prop == JFileChooser.FILE_FILTER_CHANGED_PROPERTY
|| prop == JFileChooser.FILE_HIDING_CHANGED_PROPERTY
|| prop == JFileChooser.FILE_SELECTION_MODE_CHANGED_PROPERTY) {
validateFileCache();
}
else if("UI".equals(prop)) {
Object old = e.getOldValue();
if(old instanceof BasicFileChooserUI) {
BasicFileChooserUI ui = (BasicFileChooserUI)old;
BasicDirectoryModel model = ui.getModel();
if(model != null) {
model.invalidateFileCache();
}
}
}
else if("JFileChooserDialogIsClosingProperty".equals(prop)) {
invalidateFileCache();
}
}
/**
* This method is used to interrupt file loading thread.
*/
public void invalidateFileCache() {
if(loadThread != null) {
loadThread.interrupt();
loadThread.cancelRunnables();
loadThread = null;
}
}
public Vector getDirectories() {
synchronized (fileCache) {
if(directories != null) {
return directories;
}
Vector fls = getFiles();
return directories;
}
}
public Vector getFiles() {
synchronized (fileCache) {
if(files != null) {
return files;
}
files = new Vector();
directories = new Vector();
directories.addElement(filechooser.getFileSystemView()
.createFileObject(filechooser.getCurrentDirectory(), ".."));
for(int i = 0; i < getSize(); i++) {
File f = (File)fileCache.get(i);
if(filechooser.isTraversable(f)) {
directories.add(f);
}
else {
files.add(f);
}
}
return files;
}
}
public void validateFileCache() {
// Note: The super constructor will call this method
// but at this time filechooser is null
if(filechooser == null) return;
File currentDirectory = filechooser.getCurrentDirectory();
if(currentDirectory == null) {
return;
}
if(loadThread != null) {
loadThread.interrupt();
loadThread.cancelRunnables();
}
setBusy(true, ++fetchID);
loadThread = new LoadFilesThread(currentDirectory, fetchID);
loadThread.start();
}
/**
* Renames a file in the underlying file system.
*
* @param oldFile a <code>File</code> object representing
* the existing file
* @param newFile a <code>File</code> object representing
* the desired new file name
* @return <code>true</code> if rename succeeded,
* otherwise <code>false</code>
* @since 1.4
*/
public boolean renameFile(File oldFile, File newFile) {
synchronized (fileCache) {
if(oldFile.renameTo(newFile)) {
validateFileCache();
return true;
}
return false;
}
}
public void fireContentsChanged() {
// System.out.println("TinyDirectoryModel: firecontentschanged");
fireContentsChanged(this, 0, getSize() - 1);
}
public int getSize() {
return fileCache.size();
}
public boolean contains(Object o) {
return fileCache.contains(o);
}
public int indexOf(Object o) {
return fileCache.indexOf(o);
}
public Object getElementAt(int index) {
return fileCache.get(index);
}
public Vector getFileCache() {
return fileCache;
}
/**
* Obsolete - not used.
*/
public void intervalAdded(ListDataEvent e) {
}
/**
* Obsolete - not used.
*/
public void intervalRemoved(ListDataEvent e) {
}
protected void sort(Vector v) {
ShellFolder.sortFiles(v);
}
// Obsolete - not used
protected boolean lt(File a, File b) {
// First ignore case when comparing
int diff = a.getName().toLowerCase().compareTo(
b.getName().toLowerCase());
if(diff != 0) {
return diff < 0;
}
else {
// May differ in case (e.g. "mail" vs. "Mail")
return a.getName().compareTo(b.getName()) < 0;
}
}
class LoadFilesThread extends Thread {
File currentDirectory = null;
int fid;
Vector runnables = new Vector(10);
public LoadFilesThread(File currentDirectory, int fid) {
super("Basic L&F File Loading Thread");
this.currentDirectory = currentDirectory;
this.fid = fid;
}
private void invokeLater(Runnable runnable) {
runnables.addElement(runnable);
SwingUtilities.invokeLater(runnable);
}
public void run() {
run0();
setBusy(false, fid);
}
public void run0() {
FileSystemView fileSystem = filechooser.getFileSystemView();
File[] list = fileSystem.getFiles(currentDirectory, filechooser
.isFileHidingEnabled());
Vector acceptsList = new Vector();
if(isInterrupted()) {
return;
}
// run through the file list, add directories and selectable files to fileCache
for(int i = 0; i < list.length; i++) {
if(filechooser.accept(list[i])) {
acceptsList.addElement(list[i]);
}
}
if(isInterrupted()) {
return;
}
// First sort alphabetically by filename
sort(acceptsList);
Vector newDirectories = new Vector(50);
Vector newFiles = new Vector();
// run through list grabbing directories in chunks of ten
for(int i = 0; i < acceptsList.size(); i++) {
File f = (File)acceptsList.elementAt(i);
boolean isTraversable = filechooser.isTraversable(f);
if(isTraversable) {
newDirectories.addElement(f);
}
else if(!isTraversable && filechooser.isFileSelectionEnabled()) {
newFiles.addElement(f);
}
if(isInterrupted()) {
return;
}
}
Vector newFileCache = new Vector(newDirectories);
newFileCache.addAll(newFiles);
int newSize = newFileCache.size();
int oldSize = fileCache.size();
if(newSize > oldSize) {
//see if interval is added
int start = oldSize;
int end = newSize;
for(int i = 0; i < oldSize; i++) {
if(!newFileCache.get(i).equals(fileCache.get(i))) {
start = i;
for(int j = i; j < newSize; j++) {
if(newFileCache.get(j).equals(fileCache.get(i))) {
end = j;
break;
}
}
break;
}
}
if(start >= 0
&& end > start
&& newFileCache.subList(end, newSize).equals(
fileCache.subList(start, oldSize))) {
if(isInterrupted()) {
return;
}
invokeLater(new DoChangeContents(newFileCache.subList(
start, end), start, null, 0, fid));
newFileCache = null;
}
}
else if(newSize < oldSize) {
//see if interval is removed
int start = -1;
int end = -1;
for(int i = 0; i < newSize; i++) {
if(!newFileCache.get(i).equals(fileCache.get(i))) {
start = i;
end = i + oldSize - newSize;
break;
}
}
if(start >= 0
&& end > start
&& fileCache.subList(end, oldSize).equals(
newFileCache.subList(start, newSize))) {
if(isInterrupted()) {
return;
}
invokeLater(new DoChangeContents(null, 0, new Vector(
fileCache.subList(start, end)), start, fid));
newFileCache = null;
}
}
if(newFileCache != null && !fileCache.equals(newFileCache)) {
if(isInterrupted()) {
cancelRunnables(runnables);
}
invokeLater(new DoChangeContents(newFileCache, 0, fileCache, 0,
fid));
}
}
public void cancelRunnables(Vector runnables) {
for(int i = 0; i < runnables.size(); i++) {
((DoChangeContents)runnables.elementAt(i)).cancel();
}
}
public void cancelRunnables() {
cancelRunnables(runnables);
}
}
/**
* Adds a PropertyChangeListener to the listener list. The listener is
* registered for all bound properties of this class.
* <p>
* If <code>listener</code> is <code>null</code>,
* no exception is thrown and no action is performed.
*
* @param listener the property change listener to be added
*
* @see #removePropertyChangeListener
* @see #getPropertyChangeListeners
*
* @since 1.6
*/
public void addPropertyChangeListener(PropertyChangeListener listener) {
if(changeSupport == null) {
changeSupport = new PropertyChangeSupport(this);
}
changeSupport.addPropertyChangeListener(listener);
}
/**
* Removes a PropertyChangeListener from the listener list.
* <p>
* If listener is null, no exception is thrown and no action is performed.
*
* @param listener the PropertyChangeListener to be removed
*
* @see #addPropertyChangeListener
* @see #getPropertyChangeListeners
*
* @since 1.6
*/
public void removePropertyChangeListener(PropertyChangeListener listener) {
if(changeSupport != null) {
changeSupport.removePropertyChangeListener(listener);
}
}
/**
* Returns an array of all the property change listeners
* registered on this component.
*
* @return all of this component's <code>PropertyChangeListener</code>s
* or an empty array if no property change
* listeners are currently registered
*
* @see #addPropertyChangeListener
* @see #removePropertyChangeListener
* @see java.beans.PropertyChangeSupport#getPropertyChangeListeners
*
* @since 1.6
*/
public PropertyChangeListener[] getPropertyChangeListeners() {
if(changeSupport == null) {
return new PropertyChangeListener[0];
}
return changeSupport.getPropertyChangeListeners();
}
/**
* Support for reporting bound property changes for boolean properties.
* This method can be called when a bound property has changed and it will
* send the appropriate PropertyChangeEvent to any registered
* PropertyChangeListeners.
*
* @param propertyName the property whose value has changed
* @param oldValue the property's previous value
* @param newValue the property's new value
*
* @since 1.6
*/
protected void firePropertyChange(String propertyName, boolean oldValue,
boolean newValue) {
if(changeSupport != null) {
changeSupport.firePropertyChange(propertyName, oldValue, newValue);
}
}
/**
* Set the busy state for the model. The model is considered
* busy when it is running a separate (interruptable)
* thread in order to load the contents of a directory.
*/
private synchronized void setBusy(final boolean busy, int fid) {
if(fid == fetchID) {
boolean oldValue = this.busy;
this.busy = busy;
if(changeSupport != null && busy != oldValue) {
SwingUtilities.invokeLater(new Runnable() {
public void run() {
firePropertyChange("busy", !busy, busy);
}
});
}
}
}
class DoChangeContents implements Runnable {
private List addFiles;
private List remFiles;
private boolean doFire = true;
private int fid;
private int addStart = 0;
private int remStart = 0;
private int change;
public DoChangeContents(List addFiles, int addStart, List remFiles,
int remStart, int fid) {
this.addFiles = addFiles;
this.addStart = addStart;
this.remFiles = remFiles;
this.remStart = remStart;
this.fid = fid;
}
synchronized void cancel() {
doFire = false;
}
public synchronized void run() {
if(fetchID == fid && doFire) {
int remSize = (remFiles == null) ? 0 : remFiles.size();
int addSize = (addFiles == null) ? 0 : addFiles.size();
synchronized (fileCache) {
if(remSize > 0) {
fileCache.removeAll(remFiles);
}
if(addSize > 0) {
fileCache.addAll(addStart, addFiles);
}
files = null;
directories = null;
}
if(remSize > 0 && addSize == 0) {
fireIntervalRemoved(TinyDirectoryModel.this, remStart,
remStart + remSize - 1);
}
else if(addSize > 0 && remSize == 0
&& fileCache.size() > addSize) {
fireIntervalAdded(TinyDirectoryModel.this, addStart,
addStart + addSize - 1);
}
else {
fireContentsChanged();
}
}
}
}
}