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; } } }