package com.limegroup.gnutella.gui;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.Properties;
import java.util.Set;
import javax.swing.Icon;
import javax.swing.ImageIcon;
import javax.swing.JFileChooser;
import javax.swing.filechooser.FileView;
import javax.swing.plaf.FileChooserUI;
import com.limegroup.gnutella.MediaType;
import com.limegroup.gnutella.gui.search.NamedMediaType;
import com.limegroup.gnutella.settings.SharingSettings;
import com.limegroup.gnutella.settings.UISettings;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.FileUtils;
import com.limegroup.gnutella.util.ManagedThread;
import com.limegroup.gnutella.util.ProcessingQueue;
import com.limegroup.gnutella.util.ThreadFactory;
/**
* Manages finding native icons for files and file types.
*/
public class IconManager {
/**
* The sole instance of this IconManager class.
*/
private static volatile IconManager INSTANCE;
/**
* The view that retrieves the icon from the filesystem.
*/
private FileView VIEW;
/**
* Whether we've given up trying to acquire the native view
*/
private boolean ignoreNativeView;
/**
* A mapping from String (extension) to Icon.
*/
private final Map EXTENSIONS = new HashMap();
/**
* 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();
/**
* Boolean for whether or not the file is required to be on disk
* to retrieve the icon for the given FileView.
*/
private final boolean REQUIRES_FILE;
/**
* A mapping of user-friendly names to the file name
* of the icon.
*/
private final Properties /* String -> String */ BUTTON_NAMES =
loadButtonNameMap();
/**
* A mapping of the file name of the icon to the icon itself,
* so we don't load the resource multiple times.
*/
private final Map /* String -> Icon */ BUTTON_CACHE = new HashMap();
/**
* Returns the sole instance of this IconManager class.
*/
public static IconManager instance() {
if (INSTANCE == null) {
synchronized(IconManager.class) {
if (INSTANCE == null)
INSTANCE = new IconManager();
}
}
return INSTANCE;
}
/**
* Constructs a new IconManager.
*/
private IconManager() {
boolean requiresFile = false;
// Set the FileView appropriately.
// Currently, Windows w/ Java 1.4+ & all OSX (?) javas
// can get the correct native icon.
// All others cannot.
// TODO: Find a way to look this up dynamically.
if(CommonUtils.isMacOSX() || CommonUtils.isWindows()) {
requiresFile = true;
getNativeFileView();
}
if(VIEW == null) {
requiresFile = false;
VIEW = new MediaFileView();
} else {
requiresFile = true;
VIEW = new DelegateFileView(VIEW);
}
REQUIRES_FILE = requiresFile;
// native view requires pre-loading, MediaFileView doesn't
if(!(VIEW instanceof MediaFileView))
preload();
}
/**
* Retrieves the native FileView.
*/
private void getNativeFileView() {
// 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.
// 7) Sometimes the construction of native-looking JFileChooser simply hangs.
// That's why we do it in a separate thread and wait some time for it to return.
Runnable r = new Runnable() {
public void run() {
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)ResourceManager.getNativeUI(this);
setUI(ui);
setFileView(ui.getFileView(this));
}
};
break;
} catch(NullPointerException ignored) {}
}
synchronized(IconManager.this) {
if (ignoreNativeView) // is it too late?
return;
if(chooser == null) {
VIEW = null;
} else {
VIEW = chooser.getFileView();
}
IconManager.this.notify();
}
}
};
ThreadFactory.startThread(r, "JFileChooserLoader");
synchronized(this) {
if (VIEW != null) // could have gotten it by now
return;
try {wait(4*1000);} catch(InterruptedException ignored) {}
// if we don't have the view by now the acquiring thread may have gotten stuck
// we want to proceed anyway
if (VIEW == null)
ignoreNativeView = true;
}
}
/**
* Returns the icon associated with this file.
* If the file does not exist, or no icon can be found, returns
* the icon associated with the extension.
*/
public Icon getIconForFile(File f) {
if(f == null)
return null;
// We must check f.exists first, otherwise there will be spurious
// exceptions when getting the icon from the view.
if(REQUIRES_FILE && f.exists()) {
return VIEW.getIcon(f);
} else {
String extension = FileUtils.getFileExtension(f);
if(extension != null)
return getIconForExtension(extension);
}
return null;
}
/**
* Returns the icon assocated with the extension.
* TODO: Implement better.
*/
public Icon getIconForExtension(String ext) {
if(!REQUIRES_FILE)
return VIEW.getIcon(new File("a." + ext));
ext = ext.toLowerCase();
Icon icon = (Icon)EXTENSIONS.get(ext);
if(icon != null) {
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.getValue();
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);
return icon;
}
/**
* Wipes out the button icon cache, so we can switch from large to small
* icons (or vice versa).
*/
public void wipeButtonIconCache() {
BUTTON_CACHE.clear();
}
/**
* Retrieves the icon for the specified button name.
*/
public Icon getIconForButton(String buttonName) {
String fileName = (String)BUTTON_NAMES.get(buttonName);
if(fileName == null)
return null;
ImageIcon icon = (ImageIcon)BUTTON_CACHE.get(fileName);
if(icon == NULL)
return null;
if(icon != null)
return icon;
try {
String retrieveName;
if(UISettings.SMALL_ICONS.getValue())
retrieveName = fileName + "_small";
else
retrieveName = fileName + "_large";
icon = ResourceManager.getThemeImage(retrieveName);
BUTTON_CACHE.put(fileName, icon);
} catch(MissingResourceException mre) {
// if neither small nor large existed, try once as exact
try {
icon = ResourceManager.getThemeImage(fileName);
BUTTON_CACHE.put(fileName, icon);
} catch(MissingResourceException mre2) {
BUTTON_CACHE.put(fileName, NULL);
}
}
return icon;
}
/**
* Retrieves the rollover image for the specified button name.
*/
public Icon getRolloverIconForButton(String buttonName) {
String fileName = (String)BUTTON_NAMES.get(buttonName);
if(fileName == null)
return null;
// See if we've already cached a brighter icon.
String rolloverName = fileName + "_rollover";
Icon rollover = (Icon)BUTTON_CACHE.get(rolloverName);
if(rollover == NULL)
return null;
if(rollover != null)
return rollover;
// Retrieve the initial icon, so we can brighten it.
Icon icon = (Icon)BUTTON_CACHE.get(fileName);
// no icon? no brightened icon.
if(icon == NULL || icon == null) {
BUTTON_CACHE.put(rolloverName, NULL);
return null;
}
// Make a brighter version of the icon, and cache it.
rollover = ImageManipulator.brighten(icon);
if(rollover == null)
BUTTON_CACHE.put(rolloverName, NULL);
else
BUTTON_CACHE.put(rolloverName, rollover);
return rollover;
}
private static Properties loadButtonNameMap() {
Properties p = new Properties();
URL url = ResourceManager.getURLResource("icon_mapping.properties");
InputStream is = null;
try {
if(url != null) {
is = new BufferedInputStream(url.openStream());
p.load(is);
}
} catch(IOException ignored) {
} finally {
if(is != null) {
try {
is.close();
} catch(IOException ignored) {}
}
}
return p;
}
/**
* Preloads a bunch of icons.
*/
private void preload() {
ProcessingQueue queue = new ProcessingQueue("IconLoader");
final MediaType[] types = MediaType.getDefaultMediaTypes();
for(int i = 0; i < types.length; i++) {
final Set exts = types[i].getExtensions();
for(Iterator j = exts.iterator(); j.hasNext(); ) {
final String next = (String)j.next();
queue.add(new Runnable() {
public void run() {
GUIMediator.safeInvokeAndWait(new Runnable() {
public void run() {
getIconForExtension(next);
}
});
}
});
}
}
}
/**
* A simple FileView for returning icons that match the MediaType's
* extension. Useful for when no native icon can be retrieved.
*/
private static class MediaFileView extends FileView {
public Icon getIcon(File f) {
String ext = FileUtils.getFileExtension(f);
NamedMediaType nmt = null;
if (ext != null)
nmt = NamedMediaType.getFromExtension(ext);
if(nmt == null)
nmt = NamedMediaType.getFromDescription("*"); // any type
return nmt.getIcon();
}
public String getDescription(File f) { return null; }
public String getName(File f) { return null; }
public String getTypeDescription(File f) { return null; }
public Boolean isTraversable(File f) { return Boolean.FALSE; }
}
/**
* Delegates to another FileView, catching NPEs.
*
* This 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 propogate.
*/
private static class DelegateFileView extends FileView {
private final FileView DELEGATE;
DelegateFileView(FileView real) {
DELEGATE = real;
}
public Icon getIcon(File f) {
try {
return DELEGATE.getIcon(f);
} catch(NullPointerException npe) {
return null;
}
}
public String getDescription(File f) {
return DELEGATE.getDescription(f);
}
public String getName(File f) {
return DELEGATE.getName(f);
}
public String getTypeDescription(File f) {
return DELEGATE.getTypeDescription(f);
}
public Boolean isTraversable(File f) {
return DELEGATE.isTraversable(f);
}
}
/**
* A simple FileView for returning null objects, when we can't get
* the view we want.
*/
private static class NullFileView extends FileView {
public String getDescription(File f) { return null; }
public Icon getIcon(File f) { return null; }
public String getName(File f) { return null; }
public String getTypeDescription(File f) { return null; }
public Boolean isTraversable(File f) { return Boolean.FALSE; }
}
}