blob: e93ed35507d0edeb30fce9e0b0e27446ba9444f6 [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2003 IBM Corporation and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Common Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/cpl-v10.html
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jface.resource;
import java.util.*;
import org.eclipse.jface.util.*;
import org.eclipse.swt.SWT;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Shell;
/**
* A font registry maintains a mapping between symbolic font names
* and SWT fonts.
* <p>
* A font registry owns all of the font objects registered
* with it, and automatically disposes of them when the SWT Display
* that creates the fonts is disposed. Because of this, clients do
* not need to (indeed, must not attempt to) dispose of font
* objects themselves.
* </p>
* <p>
* A special constructor is provided for populating a font registry
* from a property files using the standard Java resource bundle mechanism.
* </p>
* <p>
* Methods are provided for registering listeners that will be kept
* apprised of changes to list of registed fonts.
* </p>
* <p>
* Clients may instantiate this class (it was not designed to be subclassed).
* </p>
*/
public class FontRegistry {
/**
* List of property change listeners
* (element type: <code>org.eclipse.jface.util.IPropertyChangeListener</code>).
*/
private ListenerList listeners = new ListenerList();
/**
* Table of known fonts, keyed by symbolic font name
* (key type: <code>String</code>,
* value type: <code>org.eclipse.swt.graphics.Font</code>.
*/
private Map stringToFont = new HashMap(7);
/**
* Table of known font data, keyed by symbolic font name
* (key type: <code>String</code>,
* value type: <code>org.eclipse.swt.graphics.FontData[]</code>).
*/
private Map stringToFontData = new HashMap(7);
/**
* Collection of Fonts that are now stale to be disposed
* when it is safe to do so (i.e. on shutdown).
* @see List
*/
private List staleFonts = new ArrayList();
/**
* Creates an empty font registry.
* <p>
* There must be an SWT Display created in the current
* thread before calling this method.
* </p>
*/
public FontRegistry() {
Display display = Display.getCurrent();
Assert.isNotNull(display);
hookDisplayDispose(display);
}
/**
* Creates a font registry and initializes its content from
* a property file.
* <p>
* There must be an SWT Display created in the current
* thread before calling this method.
* </p>
* <p>
* The OS name (retrieved using <code>System.getProperty("os.name")</code>)
* is converted to lowercase, purged of whitespace, and appended
* as suffix (separated by an underscore <code>'_'</code>) to the given
* location string to yield the base name of a resource bundle
* acceptable to <code>ResourceBundle.getBundle</code>.
* The standard Java resource bundle mechanism is then used to locate
* and open the appropriate properties file, taking into account
* locale specific variations.
* </p>
* <p>
* For example, on the Windows 2000 operating system the location string
* <code>"com.example.myapp.Fonts"</code> yields the base name
* <code>"com.example.myapp.Fonts_windows2000"</code>. For the US English locale,
* this further elaborates to the resource bundle name
* <code>"com.example.myapp.Fonts_windows2000_en_us"</code>.
* </p>
* <p>
* If no appropriate OS-specific resource bundle is found, the
* process is repeated using the location as the base bundle name.
* </p>
* <p>
* The property file contains entries that look like this:
* <pre>
* textfont.0=MS Sans Serif-regular-10
* textfont.1=Times New Roman-regular-10
*
* titlefont.0=MS Sans Serif-regular-12
* titlefont.1=Times New Roman-regular-12
* </pre>
* Each entry maps a symbolic font names (the font registry keys) with
* a "<code>.<it>n</it></code> suffix to standard font names
* on the right. The suffix indicated order of preference:
* "<code>.0</code>" indicates the first choice,
* "<code>.1</code>" indicates the second choice, and so on.
* </p>
* The following example shows how to use the font registry:
* <pre>
* FontRegistry registry = new FontRegistry("com.example.myapp.fonts");
* Font font = registry.get("textfont");
* control.setFont(font);
* ...
* </pre>
*
* @param location the name of the resource bundle
* @param the ClassLoader to use to find the resource bundle
* @exception MissingResourceException if the resource bundle cannot be found
* @since 2.1
*/
public FontRegistry(String location, ClassLoader loader)
throws MissingResourceException {
Display display = Display.getCurrent();
Assert.isNotNull(display);
// FIXE: need to respect loader
//readResourceBundle(location, loader);
readResourceBundle(location);
hookDisplayDispose(display);
}
/**
* Load the FontRegistry using the ClassLoader from the PlatformUI
* plug-in
*/
public FontRegistry(String location) throws MissingResourceException {
// FIXE:
// this(location, WorkbenchPlugin.getDefault().getDescriptor().getPluginClassLoader());
this(location, null);
}
/**
* Read the resource bundle at location. Look for a file with the
* extension _os_ws first, then _os then just the name.
* @param location - String - the location of the file.
*/
private void readResourceBundle(String location) {
String osname = System.getProperty("os.name").trim(); //$NON-NLS-1$
String wsname = SWT.getPlatform();
osname = StringConverter.removeWhiteSpaces(osname).toLowerCase();
wsname = StringConverter.removeWhiteSpaces(wsname).toLowerCase();
String OSLocation = location;
String WSLocation = location;
ResourceBundle bundle = null;
if (osname != null) {
OSLocation = location + "_" + osname; //$NON-NLS-1$
if (wsname != null)
WSLocation = OSLocation + "_" + wsname; //$NON-NLS-1$
}
try {
bundle = ResourceBundle.getBundle(WSLocation);
readResourceBundle(bundle, WSLocation);
} catch (MissingResourceException wsException) {
try {
bundle = ResourceBundle.getBundle(OSLocation);
readResourceBundle(bundle, WSLocation);
} catch (MissingResourceException osException) {
if (location != OSLocation) {
bundle = ResourceBundle.getBundle(location);
readResourceBundle(bundle, WSLocation);
} else
throw osException;
}
}
}
/**
* Creates an empty font registry.
*
* @param display the Display
*/
public FontRegistry(Display display) {
Assert.isNotNull(display);
hookDisplayDispose(display);
}
/**
* Adds a property change listener to this registry.
*
* @param listener a property change listener
*/
public void addListener(IPropertyChangeListener listener) {
listeners.add(listener);
}
/**
* Find the first valid fontData in the provided list.
* If none are valid return the first one regardless.
* If the list is empty return null.
* Return null if one cannot be found.
* @deprecated use bestDataArray in order to support
* Motif multiple entry fonts.
*/
public FontData bestData(FontData[] fonts, Display display) {
for (int i = 0; i < fonts.length; i++) {
FontData fd = fonts[i];
if (fd == null)
break;
FontData[] fixedFonts = display.getFontList(fd.getName(), false);
if (isFixedFont(fixedFonts, fd)) {
return fd;
}
FontData[] scalableFonts = display.getFontList(fd.getName(), true);
if (scalableFonts.length > 0) {
return fd;
}
}
//None of the provided datas are valid. Return the
//first one as it is at least the first choice.
if (fonts.length > 0)
return fonts[0];
else
//Nothing specified
return null;
}
/**
* Find the first valid fontData in the provided list.
* If none are valid return the first one regardless.
* If the list is empty return null.
*/
public FontData[] bestDataArray(FontData[] fonts, Display display) {
FontData bestData = bestData(fonts, display);
if (bestData == null)
return null;
else {
FontData[] datas = new FontData[1];
datas[0] = bestData;
return datas;
}
}
/**
* Creates a new font with the given font datas or nulk
* if there is no data.
*/
private Font createFont(String symbolicName, FontData[] fonts) {
Display display = Display.getCurrent();
if(display == null)
return null;
FontData[] validData = bestDataArray(fonts, display);
if (validData == null) {
//Nothing specified
return null;
} else {
//Do not fire the update from creation as it is not a property change
put(symbolicName, validData, false);
return new Font(display, validData);
}
}
/**
* Returns the default font. Creates it if necessary.
*/
Font defaultFont() {
Display current = Display.getCurrent();
if (current == null) {
Shell shell = new Shell();
Font font = shell.getFont();
shell.dispose();
return font;
} else
return current.getSystemFont();
}
/**
* Returns the default font data. Creates it if necessary.
*/
private FontData[] defaultFontData() {
return defaultFont().getFontData();
}
/**
* Fires a PropertyChangeEvent.
*/
private void fireFontMappingChanged(String name, FontData[] oldValue, FontData[] newValue) {
final String finalName = name;
final Object[] listeners = this.listeners.getListeners();
if (listeners.length > 0) {
// FIXME: need to do this without dependency on org.eclipse.core.runtime
// Platform.run(new SafeRunnable(JFaceResources.getString("FontRegistry.changeError")) { //$NON-NLS-1$
// public void run() {
PropertyChangeEvent event =
new PropertyChangeEvent(this, finalName, oldValue, newValue);
for (int i = 0; i < listeners.length; ++i) {
((IPropertyChangeListener) listeners[i]).propertyChange(event);
}
// }
// });
}
}
/**
* Returns the font data associated with the given symbolic font name.
* Returns the default font data if there is no special value associated
* with that name.
*
* @param symbolicName symbolic font name
* @return the font
*/
public FontData[] getFontData(String symbolicName) {
Assert.isNotNull(symbolicName);
Object result = stringToFontData.get(symbolicName);
if (result == null)
return defaultFontData();
return (FontData[]) result;
}
/**
* Returns the font associated with the given symbolic font name.
* Returns the default font if there is no special value associated
* with that name.
*
* @param symbolicName symbolic font name
* @return the font
*/
public Font get(String symbolicName) {
Assert.isNotNull(symbolicName);
Object result = stringToFont.get(symbolicName);
if (result != null)
return (Font) result;
result = stringToFontData.get(symbolicName);
if (result == null)
return defaultFont();
// Create the font and update the mapping so it can
// be shared.
Font font = createFont(symbolicName, (FontData[]) result);
// Note, the font may be null if the create() failed. Put a mapping
// in for this font to prevent repeated attempts to allocate the
// same font.
if (font == null)
return defaultFont();
stringToFont.put(symbolicName, font);
return font;
}
/**
* Return whether or not the receiver has a value
* for the supplied fontKey.
*
* @param String. The key for the font.
* @return boolean. true if there is a key for the font.
*/
public boolean hasValueFor(String fontKey){
return stringToFontData.containsKey(fontKey);
}
/**
* Shut downs this resource registry and disposes of all registered fonts.
*/
private void handleDisplayDispose() {
disposeFonts(stringToFont.values().iterator());
disposeFonts(staleFonts.iterator());
stringToFont.clear();
staleFonts.clear();
listeners.clear();
}
/**
* Dispose of all of the fonts in this iterator.
* @param Iterator over Collection of Font
*/
private void disposeFonts(Iterator iterator) {
while (iterator.hasNext()) {
Object next = iterator.next();
((Font) next).dispose();
}
}
/**
* Hook a dispose listener on the SWT display.
*/
private void hookDisplayDispose(Display display) {
display.disposeExec(new Runnable() {
public void run() {
handleDisplayDispose();
}
});
}
/**
* Checks whether the given font is in the list of fixed fonts.
*/
private boolean isFixedFont(FontData[] fixedFonts, FontData fd) {
// Can't use FontData.equals() since some values aren't
// set if a fontdata isn't used.
int height = fd.getHeight();
String name = fd.getName();
for (int i = 0; i < fixedFonts.length; i++) {
FontData fixed = fixedFonts[i];
if (fixed.getHeight() == height && fixed.getName().equals(name))
return true;
}
return false;
}
/**
* Converts a String into a FontData object.
*/
private FontData makeFontData(String value)
throws MissingResourceException {
try {
return StringConverter.asFontData(value.trim());
} catch (DataFormatException e) {
throw new MissingResourceException("Wrong font data format. Value is: \"" + value + "\"", getClass().getName(), value); //$NON-NLS-2$//$NON-NLS-1$
}
}
/**
* Adds (or replaces) a font to this font registry under the given
* symbolic name.
* <p>
* A property change event is reported whenever the mapping from
* a symbolic name to a font changes. The source of the event is
* this registry; the property name is the symbolic font name.
* </p>
*
* @param symbolicName the symbolic font name
* @param fontData an Array of FontData
* @param update - fire a font mapping changed if true. False
* if this method is called from the get method as no setting
* has changed.
*/
public void put(String symbolicName, FontData[] fontData) {
put(symbolicName, fontData, true);
}
/**
* Adds (or replaces) a font to this font registry under the given
* symbolic name.
* <p>
* A property change event is reported whenever the mapping from
* a symbolic name to a font changes. The source of the event is
* this registry; the property name is the symbolic font name.
* </p>
*
* @param symbolicName the symbolic font name
* @param fontData an Array of FontData
* @param update - fire a font mapping changed if true. False
* if this method is called from the get method as no setting
* has changed.
*/
private void put(
String symbolicName,
FontData[] fontData,
boolean update) {
Assert.isNotNull(symbolicName);
Assert.isNotNull(fontData);
FontData[] existing = (FontData[]) stringToFontData.get(symbolicName);
if (Arrays.equals(existing, fontData))
return;
Font oldFont = (Font) stringToFont.remove(symbolicName);
stringToFontData.put(symbolicName, fontData);
if (update)
fireFontMappingChanged(symbolicName,existing,fontData);
if (oldFont == defaultFont())
return;
if (oldFont != null)
staleFonts.add(oldFont);
}
/**
* Reads the resource bundle. This puts FontData[] objects
* in the mapping table. These will lazily be turned into
* real Font objects when requested.
*/
private void readResourceBundle(ResourceBundle bundle, String bundleName)
throws MissingResourceException {
Enumeration keys = bundle.getKeys();
while (keys.hasMoreElements()) {
String key = (String) keys.nextElement();
int pos = key.lastIndexOf('.');
if (pos == -1) {
stringToFontData.put(
key,
new FontData[] { makeFontData(bundle.getString(key))});
} else {
String name = key.substring(0, pos);
int i = 0;
try {
i = Integer.parseInt(key.substring(pos + 1));
} catch (NumberFormatException e) {
//Panic the file can not be parsed.
throw new MissingResourceException("Wrong key format ", bundleName, key); //$NON-NLS-1$
}
FontData[] elements = (FontData[]) stringToFontData.get(name);
if (elements == null) {
elements = new FontData[8];
stringToFontData.put(name, elements);
}
if (i > elements.length) {
FontData[] na = new FontData[i + 8];
System.arraycopy(elements, 0, na, 0, elements.length);
elements = na;
stringToFontData.put(name, elements);
}
elements[i] = makeFontData(bundle.getString(key));
}
}
}
/**
* Removes the given listener from this registry.
* Has no affect if the listener is not registered.
*
* @param listener a property change listener
*/
public void removeListener(IPropertyChangeListener listener) {
listeners.remove(listener);
}
}