package com.limegroup.gnutella.gui;
import java.awt.Color;
import java.awt.Font;
import java.io.File;
import java.io.FilenameFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Locale;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Map;
import java.util.HashMap;
import java.util.Iterator;
import javax.swing.BorderFactory;
import javax.swing.ImageIcon;
import javax.swing.JComponent;
import javax.swing.LookAndFeel;
import javax.swing.UIDefaults;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.plaf.ComponentUI;
import javax.swing.plaf.FontUIResource;
import javax.swing.plaf.metal.MetalTheme;
import javax.swing.plaf.metal.DefaultMetalTheme;
import javax.swing.plaf.metal.MetalLookAndFeel;
import java.lang.reflect.InvocationTargetException;
import com.limegroup.gnutella.gui.themes.LimeLookAndFeel;
import com.limegroup.gnutella.gui.themes.LimePlasticTheme;
import com.limegroup.gnutella.gui.themes.ThemeFileHandler;
import com.limegroup.gnutella.gui.themes.ThemeSettings;
import com.limegroup.gnutella.settings.ApplicationSettings;
import com.limegroup.gnutella.util.CommonUtils;
import com.limegroup.gnutella.util.Expand;
/**
* Manages application resources, including the custom <tt>LookAndFeel</tt>,
* the locale-specific <tt>String</tt> instances, and any <tt>Icon</tt>
* instances needed by the application.
*/
//2345678|012345678|012345678|012345678|012345678|012345678|012345678|012345678|
public final class ResourceManager {
/**
* Instance of this <tt>ResourceManager</tt>, following singleton.
*/
private static ResourceManager _instance;
/**
* The constant name of the native Windows library.
*/
private static final String WINDOWS_LIBRARY_NAME = "LimeWire20";
/**
* Constant for the relative path of the gui directory.
*/
private static final String GUI_PATH =
"com/limegroup/gnutella/gui/";
/**
* Constant for the relative path of the resources directory.
*/
private static final String RESOURCES_PATH =
GUI_PATH + "resources/";
/**
* Constant for the relative path of the images directory.
*/
private static final String IMAGES_PATH =
GUI_PATH + "images/";
/**
* Boolean status that controls whever the shared <tt>Locale</tt> instance
* needs to be loaded, and locale-specific options need to be setup.
*/
private static boolean _localeOptionsSet;
/**
* Static variable for the loaded <tt>Locale</tt> instance.
*/
private static Locale _locale;
/**
* The <tt>ResourceBundle</tt> instance to use for loading
* locale-specific resources.
*/
private static ResourceBundle _resourceBundle;
/**
* Locale-specific option, set from the loaded resource bundle, and that
* control the preferred appearence and layout of the GUI for the loaded
* locale. Complex scripts (such as Chinese, Korean, Japanese) should
* not be displayed with bold characters if they are small (below 12
* 12 points).
*/
private static boolean _useBold;
/**
* Locale-specific option, set from the loaded resource bundle, and that
* control the preferred appearence and layout of the GUI for the loaded
* locale. Semitic scripts (such as Hebrew, Arabic, Urdu, Farsi, Divehi)
* and Thai should be used with a right-to-left directionality in the GUI
* layout. Needed here to extend Swing with Java 1.1.8 (on Mac OS 8/9),
* which does not handle java.awt.ComponentOrientation.
*/
private static boolean _useLeftToRight;
/**
* Boolean for whether or not the installer has been shared.
*/
private static boolean _installerShared = false;
/**
* Boolean for whether or not the font-size has been reduced.
*/
private static boolean _fontReduced = false;
/**
* The default MetalTheme.
*/
private static MetalTheme _defaultTheme = null;
/**
* Whether or not LimeWire was started in the 'brushed metal'
* look.
*/
private final boolean BRUSHED_METAL;
/**
* Whether or not the WINDOWS_LIBRARY was able to load.
*/
private final boolean LOADED_TRAY_LIBRARY;
/** Cache of theme images (name as String -> image as ImageIcon) */
private static final HashMap THEME_IMAGES = new HashMap();
/**
* Statically initialize necessary resources.
*/
static {
resetLocaleOptions();
}
static void resetLocaleOptions() {
_localeOptionsSet = false;
setLocaleOptions();
}
static void setLocaleOptions() {
if (!_localeOptionsSet) {
if(ApplicationSettings.LANGUAGE.getValue().equals(""))
ApplicationSettings.LANGUAGE.setValue("en");
_locale = new Locale(
ApplicationSettings.LANGUAGE.getValue(),
ApplicationSettings.COUNTRY.getValue(),
ApplicationSettings.LOCALE_VARIANT.getValue());
_resourceBundle = ResourceBundle.getBundle(
"MessagesBundle", _locale);
_useBold = !
Boolean.valueOf(getStringResource("DISABLE_BOLD_CHARACTERS")).
booleanValue();
_useLeftToRight = !
Boolean.valueOf(getStringResource("LAYOUT_RIGHT_TO_LEFT")).
booleanValue();
_localeOptionsSet = true;
}
}
/**
* Returns the <tt>Locale</tt> instance currently in use.
*
* @return the <tt>Locale</tt> instance currently in use
*/
static Locale getLocale() {
return _locale;
}
/**
* Determines whether or not the current locale language is English.
* Note that the user setting may be empty, defaulting to the running
* system locale which may be other than English. Here we check the
* effective locale seen in the MessagesBundle.
*/
static boolean hasLocalizedTipsOfTheDay() {
return Boolean.valueOf(getStringResource("HAS_TIPS_OF_THE_DAY")).booleanValue();
}
/**
* Returns the TOTD resource bundle.
*/
static ResourceBundle getTOTDResourceBundle() {
return ResourceBundle.getBundle("totd/TOTD", _locale);
}
/**
* Returns the XML resource bundle for the given schema.
* @param String schema name
* (not the URI but name returned by LimeXMLSchema.getDisplayString)
* @return ResourceBundle
*/
static ResourceBundle getXMLResourceBundle(String name) {
return ResourceBundle.getBundle("xml.display." + name, _locale);
}
/**
* Determines whever or not bold style can safely be used in the current
* locale for displaying text with a size lower than 1 pica (12 points).
*
* @return <tt>true</tt> if bold style can safely be used in this
* locale (simple scripts), <tt>false</tt> otherwise (complex scripts)
*/
static final boolean useBold() {
setLocaleOptions();
return _useBold;
}
/**
* Determines whever the standard left-to-right orientation should be used
* in the current locale.
*
* @return <tt>true</tt> if bold characters should be used in this
* locale, <tt>false</tt> otherwise (Semitic scripts).
*/
static final boolean isLeftToRight() {
setLocaleOptions();
return _useLeftToRight;
}
/**
* Returns the locale-specific String from the resource manager.
*
* @return an internationalized <tt>String</tt> instance
* corresponding with the <tt>resourceKey</tt>
*/
static final String getStringResource(final String resourceKey) {
return _resourceBundle.getString(resourceKey);
}
/**
* Serves as a single point of access for any icons that should be accessed
* directly from the file system for themes.
*
* @param name The name of the image (excluding the extension) to locate.
* @return a new <tt>ImageIcon</tt> instance for the specified file,
* or <tt>null</tt> if the resource could not be loaded
*/
static final ImageIcon getThemeImage(final String name) {
if(name == null)
throw new NullPointerException("null image name");
ImageIcon icon = null;
// First try to get theme image from cache
icon = (ImageIcon)THEME_IMAGES.get(name);
if(icon != null)
return icon;
File themeDir = ThemeSettings.THEME_DIR.getValue();
// Next try to get from themes.
icon = getImageFromURL(new File(themeDir, name).getPath(), true);
if(icon != null && icon.getImage() != null) {
THEME_IMAGES.put(name, icon);
return icon;
}
// Then try to get from com/limegroup/gnutella/images resources
icon = getImageFromURL(IMAGES_PATH + name, false);
if(icon != null && icon.getImage() != null) {
THEME_IMAGES.put(name, icon);
return icon;
}
// no resource? error.
throw new MissingResourceException(
"image: " + name + " doesn't exist.", null, null);
}
/**
* Retrieves an icon from the specified path in the filesystem.
*/
static final ImageIcon getImageFromPath(String loc) {
return getImageFromURL(loc, true);
}
/**
* Retrieves an icon from a URL-style path.
*
* If 'file' is true, location is treated as a file, otherwise
* it is treated as a resource.
*
* This tries, in order, the exact location, the location as a png,
* and the location as a gif.
*/
private static final ImageIcon getImageFromURL(String location, boolean file) {
// no theme, try backup image.
URL img = toURL(location, file);
if(img != null)
return new ImageIcon(img);
// try with png
img = toURL(location + ".png", file);
if(img != null)
return new ImageIcon(img);
// try with gif
img = toURL(location + ".gif", file);
if(img != null)
return new ImageIcon(img);
return null;
}
/**
* Makes a URL out of a location, as either a file or a resource.
*/
private static final URL toURL(String location, boolean file) {
if(file) {
File f = new File(location);
if(f.exists()) {
try {
return f.toURL();
} catch(MalformedURLException murl) {
return null;
}
} else {
return null;
}
} else {
return getURL(location);
}
}
/**
* Returns a new <tt>URL</tt> instance for the specified file in the
* "resources" directory.
*
* @param FILE_NAME the name of the resource file
* @return a new <tt>URL</tt> instance for the desired file, or
* <tt>null</tt> if the <tt>URL</tt> could not be loaded
*/
static URL getURLResource(final String FILE_NAME) {
return ResourceManager.getURL(RESOURCES_PATH + FILE_NAME);
}
/**
* Returns a new <tt>URL</tt> instance for the resource at the
* specified local path. The path should be the full path within
* the jar file, such as: <p>
*
* com/limegroup/gnutella/gui/images/searching.gif<p>
*
* @param PATH the path to the resource file within the jar
* @return a new <tt>URL</tt> instance for the desired file, or
* <tt>null</tt> if the <tt>URL</tt> could not be loaded
*/
private static URL getURL(final String PATH) {
ClassLoader cl = ResourceManager.class.getClassLoader();
if (cl == null) {
return ClassLoader.getSystemResource(PATH);
}
URL url = cl.getResource(PATH);
if (url == null) {
return ClassLoader.getSystemResource(PATH);
}
return url;
}
/**
* Instance accessor following singleton.
*/
public static final ResourceManager instance() {
if (_instance == null)
_instance = new ResourceManager();
return _instance;
}
/**
* Private constructor to ensure that a <tt>ResourceManager</tt>
* cannot be constructed from outside this class.
*/
private ResourceManager() {
GUIMediator.setSplashScreenString(
GUIMediator.getStringResource("SPLASH_STATUS_RESOURCE_MANAGER"));
if(!ThemeFileHandler.isCurrent() || !ThemeSettings.isValid()) {
ThemeSettings.THEME_FILE.revertToDefault();
ThemeSettings.THEME_DIR.revertToDefault();
ThemeFileHandler.reload();
}
String bMetal = System.getProperty("apple.awt.brushMetalLook");
BRUSHED_METAL = bMetal != null && bMetal.equalsIgnoreCase("true");
themeChanged();
try {
validateLocaleAndFonts();
} catch(NullPointerException npe) {
// ignore, can't do much about it -- internal ignorable error.
}
// The Windows library is not stored in any jar, and is
// already in the appropriate location, so copying the resource
// file is pointless (and doesn't work also).
if (CommonUtils.isWindows()) {
boolean loaded = false;
try {
System.loadLibrary(WINDOWS_LIBRARY_NAME);
loaded = true;
} catch(UnsatisfiedLinkError ule) {}
LOADED_TRAY_LIBRARY = loaded;
} else if (CommonUtils.isLinux()){
boolean loaded = false;
try {
System.loadLibrary("tray");
loaded = true;
} catch (UnsatisfiedLinkError ule){}
LOADED_TRAY_LIBRARY = loaded;
}else {
LOADED_TRAY_LIBRARY = false;
}
try {
unpackWarFiles();
unpackVersionFile();
} catch(IOException e) {
GUIMediator.showInternalError(e);
}
}
/**
* Validates the locale, determining if the current locale's resources
* can be displayed using the current fonts. If not, then the locale
* is reset to English.
*
* This prevents the UI from appearing as all boxes.
*/
public void validateLocaleAndFonts() {
// OSX can always display everything, and if it can't,
// we have no way of correcting things 'cause canDisplayUpTo
// is broken on it.
if(CommonUtils.isMacOSX())
return;
String s = getStringResource("LOCALE_LANGUAGE_NAME");
if(!checkUIFonts("dialog", s)) {
// if it couldn't display, revert the locale to english.
ApplicationSettings.LANGUAGE.setValue("en");
ApplicationSettings.COUNTRY.setValue("");
ApplicationSettings.LOCALE_VARIANT.setValue("");
resetLocaleOptions();
}
// Ensure that the Table.font can always display intl characters
// since we can always get i18n stuff there, but only if we'd actually
// be capable of displaying an intl character with the font...
// unicode string == country name of simplified chinese
String i18n = "\u4e2d\u56fd";
checkFont("TextField.font", "dialog", i18n, true);
checkFont("Table.font", "dialog", i18n, true);
checkFont("ProgressBar.font", "dialog", i18n, true);
checkFont("TabbedPane.font", "dialog", i18n, true);
}
/**
* Alters all Fonts in UIManager to use Dialog, to correctly display
* foreign strings.
*/
private boolean checkUIFonts(String newFont, String testString) {
String[] comps = new String[] {
"TextField.font",
"PasswordField.font",
"TextArea.font",
"TextPane.font",
"EditorPane.font",
"FormattedTextField.font",
"Button.font",
"CheckBox.font",
"RadioButton.font",
"ToggleButton.font",
"ProgressBar.font",
"ComboBox.font",
"InternalFrame.titleFont",
"DesktopIcon.font",
"TitledBorder.font",
"Label.font",
"List.font",
"TabbedPane.font",
"Table.font",
"TableHeader.font",
"MenuBar.font",
"Menu.font",
"Menu.acceleratorFont",
"MenuItem.font",
"MenuItem.acceleratorFont",
"PopupMenu.font",
"CheckBoxMenuItem.font",
"CheckBoxMenuItem.acceleratorFont",
"RadioButtonMenuItem.font",
"RadioButtonMenuItem.acceleratorFont",
"Spinner.font",
"Tree.font",
"ToolBar.font",
"OptionPane.messageFont",
"OptionPane.buttonFont",
"ToolTip.font",
};
boolean displayable = false;
for(int i = 0; i < comps.length; i++)
displayable |= checkFont(comps[i], newFont, testString, false);
// Then do it the automagic way.
// note that this could work all the time (without requiring the above)
// if Java 1.4 didn't introduce Locales, and it could even still work
// if they offered a way to get all the keys of possible resources.
for(Iterator i = UIManager.getDefaults().entrySet().iterator(); i.hasNext(); ) {
Map.Entry next = (Map.Entry)i.next();
if(next.getValue() instanceof Font) {
Font f = (Font)next.getValue();
if(f != null && !newFont.equalsIgnoreCase(f.getName())) {
if(!GUIUtils.canDisplay(f, testString)) {
f = new Font(newFont, f.getStyle(), f.getSize());
if(GUIUtils.canDisplay(f, testString)) {
next.setValue(f);
displayable = true;
}
}
}
}
}
return displayable;
}
/**
* Updates the font of a given fontName to be newName.
*/
private boolean checkFont(String fontName, String newName, String testString, boolean force) {
boolean displayable = true;
Font f = UIManager.getFont(fontName);
if(f != null && !newName.equalsIgnoreCase(f.getName())) {
if(!GUIUtils.canDisplay(f, testString) || force) {
f = new Font(newName, f.getStyle(), f.getSize());
if(GUIUtils.canDisplay(f, testString))
UIManager.put(fontName, f);
else
displayable = false;
}
} else if (f != null) {
displayable = GUIUtils.canDisplay(f, testString);
} else {
displayable = false;
}
return displayable;
}
/**
* Determines if the tray library has loaded.
*/
public boolean isTrayLibraryLoaded() {
return LOADED_TRAY_LIBRARY;
}
/**
* Determines if the brushed metal property is set.
*/
public boolean isBrushedMetalSet() {
return BRUSHED_METAL;
}
/**
* Updates to the current theme.
*/
public void themeChanged() {
THEME_IMAGES.clear();
try {
if(ThemeSettings.isOtherTheme()) {
// in case this is using metal ...
UIManager.put("swing.boldMetal", Boolean.FALSE);
if(_defaultTheme != null)
MetalLookAndFeel.setCurrentTheme(_defaultTheme);
String other = ThemeSettings.getOtherLF();
UIManager.setLookAndFeel(other);
} else if(ThemeSettings.isNativeTheme()) {
if(CommonUtils.isWindows() && isPlasticWindowsAvailable()) {
try {
UIManager.setLookAndFeel("com.jgoodies.plaf.windows.ExtWindowsLookAndFeel");
} catch (NullPointerException npe) {
UIManager.setLookAndFeel(UIManager.getCrossPlatformLookAndFeelClassName());
}
} else
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
if(CommonUtils.isMacOSX()) {
if(!_fontReduced) {
_fontReduced = true;
reduceFont("Label.font");
reduceFont("Table.font");
}
UIManager.put("List.focusCellHighlightBorder", BorderFactory.createEmptyBorder(1, 1, 1, 1));
UIManager.put("ScrollPane.border", BorderFactory.createMatteBorder(1, 1, 1, 1, Color.lightGray));
}
} else {
if(isPlasticAvailable()) {
if(_defaultTheme == null)
_defaultTheme = getDefaultTheme();
LimePlasticTheme.installThisTheme();
UIManager.setLookAndFeel("com.jgoodies.plaf.plastic.PlasticXPLookAndFeel");
LimeLookAndFeel.installUIManagerDefaults();
} else {
UIManager.setLookAndFeel(new LimeLookAndFeel());
}
}
UIManager.put("Tree.leafIcon", UIManager.getIcon("Tree.closedIcon"));
// remove split pane borders
UIManager.put("SplitPane.border", BorderFactory.createEmptyBorder());
if(!CommonUtils.isMacOSX())
UIManager.put("Table.focusRowHighlightBorder", UIManager.get("Table.focusCellHighlightBorder"));
UIManager.put("Table.focusCellHighlightBorder", BorderFactory.createEmptyBorder(1, 1, 1, 1));
// Add a bolded text version of simple text.
Font normal = UIManager.getFont("Table.font");
FontUIResource bold = new FontUIResource(
normal.getName(), Font.BOLD, normal.getSize());
UIManager.put("Table.font.bold", bold);
} catch (UnsupportedLookAndFeelException e) {
throw new ExceptionInInitializerError(e);
} catch (ClassNotFoundException e) {
throw new ExceptionInInitializerError(e);
} catch (InstantiationException e) {
throw new ExceptionInInitializerError(e);
} catch (IllegalAccessException e) {
throw new ExceptionInInitializerError(e);
}
}
/**
* Gets the current theme (or defaults to MetalTheme) if possible.
*/
private MetalTheme getDefaultTheme() {
MetalTheme theme = null;
if(CommonUtils.isJava15OrLater()) {
try {
theme = (MetalTheme)MetalLookAndFeel.class.getMethod("getCurrentTheme", null).invoke(null, null);
} catch(IllegalAccessException iae) {
} catch(InvocationTargetException ite) {
} catch(NoSuchMethodException nsme) {
}
}
if(theme == null)
theme = new DefaultMetalTheme();
return theme;
}
/**
* Determines if the PlasticXP Theme is available.
*/
private boolean isPlasticAvailable() {
try {
Class plastic = Class.forName("com.jgoodies.plaf.plastic.PlasticXPLookAndFeel");
return plastic != null;
} catch(ClassNotFoundException cnfe) {
return false;
}
}
/**
* Determines if the Plastic Windows Theme is available.
*/
private boolean isPlasticWindowsAvailable() {
try {
Class plastic = Class.forName("com.jgoodies.plaf.windows.ExtWindowsLookAndFeel");
return plastic != null;
} catch(ClassNotFoundException cnfe) {
return false;
}
}
/**
* Updates the component to use the native UI resource.
*/
static ComponentUI getNativeUI(JComponent c) {
ComponentUI ret = null;
String name = UIManager.getSystemLookAndFeelClassName();
if(name != null) {
try {
Class clazz = Class.forName(name);
LookAndFeel lf = (LookAndFeel)clazz.newInstance();
lf.initialize();
UIDefaults def = lf.getDefaults();
ret = def.getUI(c);
} catch(ExceptionInInitializerError e) {
} catch(ClassNotFoundException e) {
} catch(LinkageError e) {
} catch(IllegalAccessException e) {
} catch(InstantiationException e) {
} catch(SecurityException e) {
} catch(ClassCastException e) {
}
}
// if any of those failed, default to the current UI.
if(ret == null)
ret = UIManager.getUI(c);
return ret;
}
/**
* Reduces the size of a font in UIManager.
*/
private static void reduceFont(String name) {
Font oldFont = UIManager.getFont(name);
FontUIResource newFont =
new FontUIResource(oldFont.getName(), oldFont.getStyle(),
oldFont.getSize() - 2);
UIManager.put(name, newFont);
}
/**
* Unpacks any war files in the current directory.
*/
private static void unpackWarFiles() throws IOException {
//Unpack any .war files, and put into appropriate dirs.
File currDir = CommonUtils.getCurrentDirectory();
String[] warFiles = (currDir).list(
new FilenameFilter() {
//the files to be accepted to be returned
public boolean accept(File dir, String name) {
return name.endsWith(".war");
}
});
if (warFiles == null) {
// no war files to expand -- don't worry about it
return;
}
File destDir = CommonUtils.getUserSettingsDir();
if(!destDir.isDirectory()) {
throw new IOException("settings dir not a directory: "+destDir);
}
if(!destDir.canWrite()) {
throw new IOException("cannot write to the settings dir: "+destDir);
}
for (int i = 0; i < warFiles.length; i++) {
if(warFiles[i].equals("xml.war")) {
// force schemas to always be recopied.
Expand.expandFile(new File(warFiles[i]),
destDir, false,
new String[] { "xml/schemas/" } );
} else {
Expand.expandFile(new File(warFiles[i]), destDir);
}
}
}
/**
* Unpacks the update.ver file.
*/
private void unpackVersionFile() throws IOException {
File userHome = CommonUtils.getUserSettingsDir();
File verFile = new File("update.ver");
Expand.expandFile(verFile, userHome);
}
}