package org.limewire.ui.swing.util;
import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileSystemView;
import javax.swing.filechooser.FileView;
import javax.swing.plaf.FileChooserUI;
import org.limewire.collection.FixedsizeForgetfulHashMap;
import org.limewire.collection.FixedsizeForgetfulHashSet;
import org.limewire.concurrent.ExecutorsHelper;
import org.limewire.core.settings.SharingSettings;
import org.limewire.ui.swing.settings.SwingUiSettings;
import org.limewire.util.FileUtils;
import org.limewire.util.MediaType;
import org.limewire.util.OSUtils;
import org.limewire.util.SystemUtils;
import org.limewire.util.SystemUtils.SpecialLocations;
/** A FileIconController that attempts to return native icons. */
public class NativeFileIconController implements FileIconController {
/** The view that retrieves the icon from the filesystem. */
private final SmartFileView VIEW;
/** A mapping from String (extension) to Icon. */
private final Map<String, Icon> EXTENSIONS = new HashMap<String, Icon>();
/**
* A marker null icon so we don't create a file everytime
* if the icon can't be found.
*/
private final Icon NULL = new ImageIcon();
/**
* Constructs the NativeFileIconController.
* This constructor may block as the JFileChooser
* is constructed.
*/
NativeFileIconController() {
SmartFileView view = getNativeFileView();
if(view == null) {
VIEW = null;
} else {
VIEW = new DelegateFileView(view);
if(SwingUiSettings.PRELOAD_NATIVE_ICONS.getValue())
preload();
}
}
/** Returns true if loading succeeded and the view hasn't screwed up. */
public boolean isValid() {
return VIEW != null && VIEW.isViewAvailable();
}
/** Returns true if we have requested this file recently. */
public boolean isIconForFileAvailable(File f) {
return VIEW.isIconCached(f);
}
/**
* Retrieves the native FileView.
*/
private SmartFileView getNativeFileView() {
// Deadlocks happen on Windows when using file-chooser based view.
if(OSUtils.isWindows())
return constructFSVView();
else
return constructFileChooserView();
}
/**
* Constructs a JFileChooser-based FileView.
* This doesn't work consistently on Java 1.5.0_10 on Windows,
* because it deadlocks if called outside of the Swing thread.
* @return
*/
private SmartFileView constructFileChooserView() {
// This roundabout way of getting the FileView is necessary for the
// following reasons:
// 1) We need the native UI's FileView to get the correct icons,
// because the Metal UI's icons are terrible.
// 2) We cannot just call getFileView(chooser) once retrieving the
// native UI, because FileChooserUI tends to delegate calls
// to the JFileChooser, and it seems to require that it
// the UI be set on the chooser.
// 3) setUI is a protected method of JFileChooser (of JComponent),
// so we need to have the anonymous class with an extended
// constructor.
// 4) Even after constructing the JFileChooser, using getIcon on it
// doesn't work well, so we need to do it directly on the FileView.
// 5) In order to get the correct file view, it needs to be explicitly
// set, otherwise it reverts to the UI's FileView, using UIManager,
// which may actually be a different UI.
// 6) The NullPointerException must be caught because sometimes
// the Windows JFileChooser throws an NPE while constructing.
// The ArrayIndexOutOfBoundsException is a workaround also,
// as that error seems to be thrown occasionally in Java 1.6.
JFileChooser chooser = null;
// If after 10 times we still can't set it just give up.
for(int i = 0; i < 10; i++) {
try {
chooser = new JFileChooser() {
{
FileChooserUI ui =
(FileChooserUI)GuiUtils.getNativeUI(this);
setUI(ui);
setFileView(ui.getFileView(this));
}
};
} catch(NullPointerException ignored) {
} catch(ArrayIndexOutOfBoundsException ignored) {
}
if(chooser != null)
break;
}
if(chooser == null) {
return null;
} else {
return new SmartChooserView(chooser.getFileView());
}
}
/**
* Constructs a FileSystemView-based FileView.
* Just to be safe, we do this on the Swing thread, since we've seen
* deadlocks when constructing JFileChooser ones outside the Swing thread.
*/
public SmartFileView constructFSVView() {
final AtomicReference<SmartFileView> ref = new AtomicReference<SmartFileView>();
SwingUtils.invokeAndWait(new Runnable() {
public void run() {
try {
ref.set(new FSVFileView());
} catch(Throwable err) {
// If an error constructing FSF view, ignore.
ref.set(null);
}
}
});
return ref.get();
}
/**
* Returns the native file icon for a file, if it exists.
* If it doesn't exist, returns the icon for the extension of the file.
*/
public Icon getIconForFile(File f) {
if (f == null)
return null;
// We return the icon if it was previously cached,
// or if it exists. We cannot get a nonexistant (non-cached)
// file from the view, otherwise they'll be spurious exceptions.
if(VIEW.isIconCached(f) || f.exists()) {
return VIEW.getIcon(f);
} else {
String extension = FileUtils.getFileExtension(f);
if (!extension.isEmpty())
return getIconForExtension(extension);
else
return null;
}
}
/**
* Returns the icon associated with the extension.
*/
/* TODO: Implement better.
*/
public Icon getIconForExtension(String ext) {
ext = ext.trim().toLowerCase();
Icon icon = EXTENSIONS.get(ext);
// If we already had a cached icon..
if(icon != null) {
// So long as it wasn't the NULL marker,
// return that icon.
if(icon != NULL)
return icon;
else
return null;
}
// If we don't know the icon for this extension yet,
// then create a temporary file, get icon, cache it,
// and return it.
File dir = SharingSettings.INCOMPLETE_DIRECTORY.get();
File tmp = new File(dir, ".LimeWireIconFinder." + ext);
if(tmp.exists()) {
icon = VIEW.getIcon(tmp);
} else {
try {
FileUtils.touch(tmp);
icon = VIEW.getIcon(tmp);
if(icon == null)
icon = NULL;
} catch(IOException fnfe) {
icon = NULL;
}
}
tmp.delete();
EXTENSIONS.put(ext, icon);
VIEW.removeFromCache(tmp); // do not need to keep it around.
return icon;
}
/**
* Preloads a bunch of icons.
*/
private void preload() {
ExecutorService queue = ExecutorsHelper.newProcessingQueue("IconLoader");
final MediaType[] types = MediaType.getDefaultMediaTypes();
final AtomicBoolean continueLoading = new AtomicBoolean(true);
for(int i = 0; i < types.length && continueLoading.get(); i++) {
final Set exts = types[i].getExtensions();
for(Iterator j = exts.iterator(); j.hasNext() && continueLoading.get(); ) {
final String next = (String)j.next();
queue.execute(new Runnable() {
public void run() {
SwingUtils.invokeAndWait(new Runnable() {
public void run() {
getIconForExtension(next);
if(!VIEW.isViewAvailable())
continueLoading.set(false);
}
});
}
});
}
}
}
/** A smarter FileView. */
private static abstract class SmartFileView extends FileView {
/** Checks to see if the given icon is cached. */
public abstract boolean isIconCached(File f);
/** Removes a file from the cache, if possible. */
public abstract boolean removeFromCache(File f);
/** Determines if this view is working. By default, returns true. */
public boolean isViewAvailable() {
return true;
}
}
/**
* Delegates to another FileView, catching NPEs & UnsatisfiedLinkErrors.
*
* The NPE catching is required because of poorly built methods in
* javax.swing.filechooser.FileSystemView that print true
* exceptions to System.err and return null, instead of
* letting the exception propagate.
* <p>
* The ULE catching is required because of strange Swing errors
* that can't find the native code to:
* sun.awt.shell.Win32ShellFolder2.getFileSystemPath(I)Ljava/lang/String; *
* See: LWC-1174.
*/
private static class DelegateFileView extends SmartFileView {
private final SmartFileView DELEGATE;
private boolean linkFailed = false;
DelegateFileView(SmartFileView real) {
DELEGATE = real;
}
@Override
public boolean isViewAvailable() {
return !linkFailed;
}
@Override
public Icon getIcon(final File f) {
try {
Icon icon = DELEGATE.getIcon(f);
return icon;
} catch(NullPointerException npe) {
return null;
} catch(UnsatisfiedLinkError ule) {
linkFailed = true;
return null;
}
}
@Override
public String getDescription(File f) {
try {
return DELEGATE.getDescription(f);
} catch(NullPointerException npe) {
return null;
} catch(UnsatisfiedLinkError ule) {
linkFailed = true;
return null;
}
}
@Override
public String getName(File f) {
try {
return DELEGATE.getName(f);
} catch (NullPointerException npe) {
return null;
} catch(UnsatisfiedLinkError ule) {
linkFailed = true;
return null;
}
}
@Override
public String getTypeDescription(File f) {
try {
return DELEGATE.getTypeDescription(f);
} catch (NullPointerException npe) {
return null;
} catch(UnsatisfiedLinkError ule) {
linkFailed = true;
return null;
}
}
@Override
public Boolean isTraversable(File f) {
try {
return DELEGATE.isTraversable(f);
} catch (NullPointerException npe) {
return null;
} catch(UnsatisfiedLinkError ule) {
linkFailed = true;
return null;
}
}
@Override
public boolean isIconCached(File f) {
return DELEGATE.isIconCached(f);
}
/** Does nothing. */
@Override
public boolean removeFromCache(File f) {
return DELEGATE.removeFromCache(f);
}
}
/**
* A FileSystemView FileView.
*/
private static class FSVFileView extends SmartFileView {
private final FileSystemView VIEW = FileSystemView.getFileSystemView();
private final Map<File, Icon> CACHE = new FixedsizeForgetfulHashMap<File, Icon>(50000);
FSVFileView() {
// Explicitly put the icon for roots as gotten from their roots description.
// For some reason, getting the icon for the desktop requires that you pass
// the File as retrieved from getRoots instead of a file at the desktop's
// location on FS.
File[] roots = VIEW.getRoots();
for(int i = 0; i < roots.length; i++) {
CACHE.put(new File(roots[i].getPath()), VIEW.getSystemIcon(roots[i]));
}
// Similarly, with the My Documents folder, the icon is only valid if it's
// retrieved as the child from the root.
// Do not execute this code on windows 7. The default logic works fine.
// On windows 7 this breaks because View.getRoots only returns one root,
// the path to the Desktop folder.
// We bypass this code on Windows Vista. It fails because the
// Documents folder no longer has a desktop icon on Vista.
if(OSUtils.isWindows() && !OSUtils.isWindowsVista() && !OSUtils.isWindows7() && roots.length == 1) {
String path = SystemUtils.getSpecialPath(SpecialLocations.DOCUMENTS);
if(path != null) {
File documents = new File(path);
File child = VIEW.getChild(roots[0], documents.getName());
if(child != null) {
CACHE.put(documents, VIEW.getSystemIcon(child));
}
}
}
}
@Override
public String getDescription(File f) {
return VIEW.getSystemTypeDescription(f);
}
@Override
public Icon getIcon(final File f) {
Icon icon = CACHE.get(f);
if(icon == null) {
icon = VIEW.getSystemIcon(f);
CACHE.put(f, icon);
}
return icon;
}
@Override
public String getName(File f) {
return VIEW.getSystemDisplayName(f);
}
@Override
public String getTypeDescription(File f) {
return VIEW.getSystemTypeDescription(f);
}
@Override
public Boolean isTraversable(File f) {
return VIEW.isTraversable(f);
}
@Override
public boolean isIconCached(File f) {
return CACHE.containsKey(f);
}
/** Removes the given file from the cache. */
@Override
public boolean removeFromCache(File f) {
return CACHE.remove(f) != null;
}
}
/**
* A wrapper around JFileChooser's view that returns true for caching
* once a file has been looked up.
*/
private static class SmartChooserView extends SmartFileView {
/** The view this uses. */
private final FileView DELEGATE;
/** A set of the most recently requested Files. */
private final Set<File> CACHE = new FixedsizeForgetfulHashSet<File>(5000, 1000);
public SmartChooserView(FileView delegate) {
DELEGATE = delegate;
}
@Override
public String getDescription(File f) {
return DELEGATE.getDescription(f);
}
@Override
public Icon getIcon(File f) {
CACHE.add(f);
return DELEGATE.getIcon(f);
}
@Override
public String getName(File f) {
return DELEGATE.getName(f);
}
@Override
public String getTypeDescription(File f) {
return DELEGATE.getTypeDescription(f);
}
@Override
public Boolean isTraversable(File f) {
return DELEGATE.isTraversable(f);
}
@Override
public boolean isIconCached(File f) {
return CACHE.contains(f);
}
@Override
public boolean removeFromCache(File f) {
return CACHE.remove(f);
}
}
}