org.eclipse.jface.resource.FontRegistry Maven / Gradle / Ivy
/*******************************************************************************
* Copyright (c) 2000, 2019 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
* Red Hat Inc. - bug 544026
*******************************************************************************/
package org.eclipse.jface.resource;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.MissingResourceException;
import java.util.ResourceBundle;
import java.util.Set;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jface.util.Policy;
import org.eclipse.jface.util.Util;
import org.eclipse.pde.api.tools.annotations.NoExtend;
import org.eclipse.swt.SWT;
import org.eclipse.swt.SWTException;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.widgets.Display;
/**
* A font registry maintains a mapping between symbolic font names
* and SWT fonts.
*
* 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.
*
*
* A special constructor is provided for populating a font registry
* from a property files using the standard Java resource bundle mechanism.
*
*
* Methods are provided for registering listeners that will be kept
* apprised of changes to list of registered fonts.
*
*
* Clients may instantiate this class (it was not designed to be subclassed).
*
*
* Since 3.0 this class extends ResourceRegistry.
*/
@NoExtend
public class FontRegistry extends ResourceRegistry {
/**
* FontRecord is a private helper class that holds onto a font
* and can be used to generate its bold and italic version.
*/
private class FontRecord {
Font baseFont;
Font boldFont;
Font italicFont;
FontData[] baseData;
/**
* Create a new instance of the receiver based on the
* plain font and the data for it.
* @param plainFont The base looked up font.
* @param data The data used to look it up.
*/
FontRecord(Font plainFont, FontData[] data) {
baseFont = plainFont;
baseData = data;
}
/**
* Dispose any of the fonts created for this record.
*/
void dispose() {
baseFont.dispose();
if (boldFont != null) {
boldFont.dispose();
}
if (italicFont != null) {
italicFont.dispose();
}
}
/**
* Return the base Font.
* @return Font
*/
public Font getBaseFont() {
return baseFont;
}
/**
* Return the bold Font. Create a bold version
* of the base font to get it.
* @return Font
*/
public Font getBoldFont() {
if (boldFont != null) {
return boldFont;
}
FontData[] boldData = getModifiedFontData(SWT.BOLD);
boldFont = new Font(Display.getCurrent(), boldData);
return boldFont;
}
/**
* Get a version of the base font data with the specified
* style.
* @param style the new style
* @return the font data with the style {@link FontData#FontData(String, int, int)}
* @see SWT#ITALIC
* @see SWT#NORMAL
* @see SWT#BOLD
* @todo Generated comment
*/
private FontData[] getModifiedFontData(int style) {
FontData[] styleData = new FontData[baseData.length];
for (int i = 0; i < styleData.length; i++) {
FontData base = baseData[i];
styleData[i] = new FontData(base.getName(), base.getHeight(),
base.getStyle() | style);
}
return styleData;
}
/**
* Return the italic Font. Create an italic version of the
* base font to get it.
* @return Font
*/
public Font getItalicFont() {
if (italicFont != null) {
return italicFont;
}
FontData[] italicData = getModifiedFontData(SWT.ITALIC);
italicFont = new Font(Display.getCurrent(), italicData);
return italicFont;
}
/**
* Add any fonts that were allocated for this record to the
* stale fonts. Anything that matches the default font will
* be skipped.
* @param defaultFont The system default.
*/
void addAllocatedFontsToStale(Font defaultFont) {
//Return all of the fonts allocated by the receiver.
//if any of them are the defaultFont then don't bother.
if (defaultFont != baseFont && baseFont != null) {
staleFonts.add(baseFont);
}
if (defaultFont != boldFont && boldFont != null) {
staleFonts.add(boldFont);
}
if (defaultFont != italicFont && italicFont != null) {
staleFonts.add(italicFont);
}
}
}
/**
* Table of known fonts, keyed by symbolic font name
* (key type: String
,
* value type: FontRecord
.
*/
private Map stringToFontRecord = new HashMap<>(7);
/**
* Table of known font data, keyed by symbolic font name
* (key type: String
,
* value type: org.eclipse.swt.graphics.FontData[]
).
*/
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<>();
/**
* Runnable that cleans up the manager on disposal of the display.
*/
protected Runnable displayRunnable = this::clearCaches;
private boolean displayDisposeHooked;
private final boolean cleanOnDisplayDisposal;
/**
* Creates an empty font registry.
*
* There must be an SWT Display created in the current
* thread before calling this method.
*
*/
public FontRegistry() {
this(Display.getCurrent(), true);
}
/**
* Creates a font registry and initializes its content from a property file.
*
* There must be an SWT Display created in the current thread before calling
* this method.
*
*
* The OS name (retrieved using System.getProperty("os.name")
) is
* converted to lowercase, purged of whitespace, and appended as suffix
* (separated by an underscore '_'
) to the given location string to
* yield the base name of a resource bundle acceptable to
* ResourceBundle.getBundle
. The standard Java resource bundle
* mechanism is then used to locate and open the appropriate properties file,
* taking into account locale specific variations.
*
*
* For example, on the Windows 2000 operating system the location string
* "com.example.myapp.Fonts"
yields the base name
* "com.example.myapp.Fonts_windows2000"
. For the US English
* locale, this further elaborates to the resource bundle name
* "com.example.myapp.Fonts_windows2000_en_us"
.
*
*
* If no appropriate OS-specific resource bundle is found, the process is
* repeated using the location as the base bundle name.
*
*
* The property file contains entries that look like this:
*
*
* 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
*
*
* Each entry maps a symbolic font names (the font registry keys) with a
* ".n
" suffix to standard font names on the right. The
* suffix indicated order of preference: ".0
" indicates the first
* choice, ".1
" indicates the second choice, and so on.
*
* The following example shows how to use the font registry:
*
*
*
* FontRegistry registry = new FontRegistry("com.example.myapp.fonts");
* Font font = registry.get("textfont");
* control.setFont(font);
* ...
*
*
* @param location the name of the resource bundle
* @param loader 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);
cleanOnDisplayDisposal = true;
hookDisplayDispose(display);
}
/**
* Load the FontRegistry using the ClassLoader from the PlatformUI
* plug-in
*
* This method should only be called from the UI thread. If you are not on the UI
* thread then wrap the call with a
* PlatformUI.getWorkbench().getDisplay().synchExec()
in order to
* guarantee the correct result. Failure to do this may result in an {@link
* SWTException} being thrown.
*
* @param location the location to read the resource bundle from
* @throws MissingResourceException Thrown if a resource is missing
*/
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 = Util.getWS();
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) {
this(display, true);
}
/**
* Creates an empty font registry.
*
* @param display
* the Display
* @param cleanOnDisplayDisposal
* whether all fonts allocated by this FontRegistry
* should be disposed when the display is disposed
* @since 3.1
*/
public FontRegistry(Display display, boolean cleanOnDisplayDisposal) {
Assert.isNotNull(display);
this.cleanOnDisplayDisposal = cleanOnDisplayDisposal;
if (cleanOnDisplayDisposal) {
hookDisplayDispose(display);
}
}
/**
* 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.
*
* @param fonts the font list
* @param display the display used
* @return the font data of the like describe above
*
* @deprecated use bestDataArray in order to support Motif multiple entry
* fonts.
*/
@Deprecated
public FontData bestData(FontData[] fonts, Display display) {
for (FontData fd : fonts) {
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];
}
//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
.
*
* @param fonts list of fonts
* @param display the display
* @return font data like described above
* @deprecated use filterData in order to preserve
* multiple entry fonts on Motif
*/
@Deprecated
public FontData[] bestDataArray(FontData[] fonts, Display display) {
FontData bestData = bestData(fonts, display);
if (bestData == null) {
return null;
}
FontData[] datas = new FontData[1];
datas[0] = bestData;
return datas;
}
/**
* Removes from the list all fonts that do not exist in this system.
* If none are valid, return the first irregardless. If the list is
* empty return null
.
*
* @param fonts the fonts to check
* @param display the display to check against
* @return the list of fonts that have been found on this system
* @since 3.1
*/
public FontData [] filterData(FontData [] fonts, Display display) {
ArrayList good = new ArrayList<>(fonts.length);
for (FontData fd : fonts) {
if (fd == null) {
continue;
}
FontData[] fixedFonts = display.getFontList(fd.getName(), false);
if (isFixedFont(fixedFonts, fd)) {
good.add(fd);
}
FontData[] scalableFonts = display.getFontList(fd.getName(), true);
if (scalableFonts.length > 0) {
good.add(fd);
}
}
//None of the provided datas are valid. Return the
//first one as it is at least the first choice.
if (good.isEmpty() && fonts.length > 0) {
good.add(fonts[0]);
}
else if (fonts.length == 0) {
return null;
}
return good.toArray(new FontData[good.size()]);
}
/**
* Creates a new font with the given font datas or null
* if there is no data.
* @return FontRecord for the new Font or null
.
*/
private FontRecord createFont(String symbolicName, FontData[] fonts) {
Display display = Display.getCurrent();
if (display == null) {
return null;
}
if (cleanOnDisplayDisposal && !displayDisposeHooked) {
hookDisplayDispose(display);
}
FontData[] validData = filterData(fonts, display);
if (validData.length == 0) {
//Nothing specified
return null;
}
//Do not fire the update from creation as it is not a property change
put(symbolicName, validData, false);
Font newFont = new Font(display, validData);
return new FontRecord(newFont, validData);
}
/**
* Calculates the default font and returns the result.
* This method creates a font that must be disposed.
*/
Font calculateDefaultFont() {
Display current = Display.getCurrent();
if (current == null) // can't do much without Display
SWT.error(SWT.ERROR_THREAD_INVALID_ACCESS);
return new Font(current, current.getSystemFont().getFontData());
}
/**
* Returns the default font data. Creates it if necessary.
*
* This method should only be called from the UI thread. If you are not on the UI
* thread then wrap the call with a
* PlatformUI.getWorkbench().getDisplay().synchExec()
in order to
* guarantee the correct result. Failure to do this may result in an {@link
* SWTException} being thrown.
*
* @return Font
*/
public Font defaultFont() {
return defaultFontRecord().getBaseFont();
}
/**
* Returns the font descriptor for the font 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 descriptor (never null)
*
* @since 3.3
*/
public FontDescriptor getDescriptor(String symbolicName) {
Assert.isNotNull(symbolicName);
return FontDescriptor.createFrom(getFontData(symbolicName));
}
/**
* Returns the default font record.
*/
private FontRecord defaultFontRecord() {
FontRecord record = stringToFontRecord.get(JFaceResources.DEFAULT_FONT);
if (record != null) {
return record;
}
FontData[] fontData = stringToFontData.get(JFaceResources.DEFAULT_FONT);
if (fontData != null) {
record = createFont(JFaceResources.DEFAULT_FONT, fontData);
}
if (record == null) {
Font defaultFont = calculateDefaultFont();
record = createFont(JFaceResources.DEFAULT_FONT, defaultFont.getFontData());
defaultFont.dispose();
}
stringToFontRecord.put(JFaceResources.DEFAULT_FONT, record);
return record;
}
/**
* Returns the default font data. Creates it if necessary.
*/
private FontData[] defaultFontData() {
return defaultFontRecord().baseData;
}
/**
* 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.
*
* This method should only be called from the UI thread. If you are not on the UI
* thread then wrap the call with a
* PlatformUI.getWorkbench().getDisplay().synchExec()
in order to
* guarantee the correct result. Failure to do this may result in an {@link
* SWTException} being thrown.
*
* @param symbolicName symbolic font name
* @return the font
*/
public Font get(String symbolicName) {
return getFontRecord(symbolicName).getBaseFont();
}
/**
* Returns the bold font associated with the given symbolic font name.
* Returns the bolded default font if there is no special value associated
* with that name.
*
* This method should only be called from the UI thread. If you are not on the UI
* thread then wrap the call with a
* PlatformUI.getWorkbench().getDisplay().synchExec()
in order to
* guarantee the correct result. Failure to do this may result in an {@link
* SWTException} being thrown.
*
* @param symbolicName symbolic font name
* @return the font
* @since 3.0
*/
public Font getBold(String symbolicName) {
return getFontRecord(symbolicName).getBoldFont();
}
/**
* Returns the italic font associated with the given symbolic font name.
* Returns the italic default font if there is no special value associated
* with that name.
*
* This method should only be called from the UI thread. If you are not on the UI
* thread then wrap the call with a
* PlatformUI.getWorkbench().getDisplay().synchExec()
in order to
* guarantee the correct result. Failure to do this may result in an {@link
* SWTException} being thrown.
*
* @param symbolicName symbolic font name
* @return the font
* @since 3.0
*/
public Font getItalic(String symbolicName) {
return getFontRecord(symbolicName).getItalicFont();
}
/**
* Return the font record for the key.
* @param symbolicName The key for the record.
* @return FontRecord
*/
private FontRecord getFontRecord(String symbolicName) {
Assert.isNotNull(symbolicName);
Object result = stringToFontRecord.get(symbolicName);
if (result != null) {
return (FontRecord) result;
}
result = stringToFontData.get(symbolicName);
FontRecord fontRecord;
if (result == null) {
fontRecord = defaultFontRecord();
} else {
fontRecord = createFont(symbolicName, (FontData[]) result);
}
if (fontRecord == null) {
fontRecord = defaultFontRecord();
if (Display.getCurrent() == null) { // log error but don't throw an exception to preserve existing functionality
String msg = "Unable to create font \"" + symbolicName + "\" in a non-UI thread. Using default font instead."; //$NON-NLS-1$ //$NON-NLS-2$
Policy.logException(new SWTException(msg));
return fontRecord; // don't add it to the cache; if later asked from UI thread, a proper font will be created
}
}
stringToFontRecord.put(symbolicName, fontRecord);
return fontRecord;
}
@Override
public Set getKeySet() {
return Collections.unmodifiableSet(stringToFontData.keySet());
}
@Override
public boolean hasValueFor(String fontKey) {
return stringToFontData.containsKey(fontKey);
}
@Override
protected void clearCaches() {
Iterator iterator = stringToFontRecord.values().iterator();
while (iterator.hasNext()) {
Object next = iterator.next();
((FontRecord) next).dispose();
}
disposeFonts(staleFonts.iterator());
stringToFontRecord.clear();
staleFonts.clear();
displayDisposeHooked = false;
}
/**
* 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) {
displayDisposeHooked = true;
display.disposeExec(displayRunnable);
}
/**
* 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 (FontData fixed : fixedFonts) {
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.
*
* 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.
*
*
* @param symbolicName the symbolic font name
* @param fontData an Array of FontData
*/
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.
*
* 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.
*
*
* @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 = stringToFontData.get(symbolicName);
if (Arrays.equals(existing, fontData)) {
return;
}
FontRecord oldFont = stringToFontRecord
.remove(symbolicName);
stringToFontData.put(symbolicName, fontData);
if (update) {
fireMappingChanged(symbolicName, existing, fontData);
}
if (oldFont != null) {
oldFont.addAllocatedFontsToStale(defaultFontRecord().getBaseFont());
}
}
/**
* 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 = 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 = 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));
}
}
}
/**
* Returns the font descriptor for the JFace default font.
*
* @return the font descriptor for the JFace default font
* @since 3.3
*/
public FontDescriptor defaultFontDescriptor() {
return FontDescriptor.createFrom(defaultFontData());
}
}