/*
* Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* o Redistributions of source code must retain the above copyright notice,
* this list of conditions and the following disclaimer.
*
* o Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* o Neither the name of JGoodies Karsten Lentzsch nor the names of
* its contributors may be used to endorse or promote products derived
* from this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
* OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
* EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.jgoodies.forms.util;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Logger;
import javax.swing.JButton;
import javax.swing.JPanel;
import javax.swing.UIManager;
/**
* This is the default implementation of the {@link UnitConverter} interface.
* It converts horizontal and vertical dialog base units to pixels.<p>
*
* The horizontal base unit is equal to the average width, in pixels,
* of the characters in the system font; the vertical base unit is equal
* to the height, in pixels, of the font.
* Each horizontal base unit is equal to 4 horizontal dialog units;
* each vertical base unit is equal to 8 vertical dialog units.<p>
*
* The DefaultUnitConverter computes dialog base units using a default font
* and a test string for the average character width. You can configure
* the font and the test string via the bound Bean properties
* <em>defaultDialogFont</em> and <em>averageCharacterWidthTestString</em>.
* See also Microsoft's suggestion for a custom computation
* <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwue/html/ch14e.asp">here</a>.<p>
*
* Since the Forms 1.1 this converter logs font information at
* the <code>CONFIG</code> level.
*
* @version $Revision$
* @author Karsten Lentzsch
* @see UnitConverter
* @see com.jgoodies.forms.layout.Size
* @see com.jgoodies.forms.layout.Sizes
*/
public final class DefaultUnitConverter extends AbstractUnitConverter {
// public static final String UPPERCASE_ALPHABET =
// "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
//
// public static final String LOWERCASE_ALPHABET =
// "abcdefghijklmnopqrstuvwxyz";
private final static Logger LOGGER = Logger
.getLogger(DefaultUnitConverter.class.getName());
/**
* Holds the sole instance that will be lazily instantiated.
*/
private static DefaultUnitConverter instance;
/**
* Holds the string that is used to compute the average character width.
* By default this is just "X".
*/
private String averageCharWidthTestString = "X";
/**
* Holds the font that is used to compute the global dialog base units.
* By default it is lazily created in method #getDefaultDialogFont,
* which in turn looks up a font in method #lookupDefaultDialogFont.
*/
private Font defaultDialogFont;
/**
* If any <code>PropertyChangeListeners</code> have been registered,
* the <code>changeSupport</code> field describes them.
*
* @serial
* @see #addPropertyChangeListener(PropertyChangeListener)
* @see #addPropertyChangeListener(String, PropertyChangeListener)
* @see #removePropertyChangeListener(PropertyChangeListener)
* @see #removePropertyChangeListener(String, PropertyChangeListener)
*/
private PropertyChangeSupport changeSupport;
// Cached *****************************************************************
/**
* Holds the cached global dialog base units that are used if
* a component is not (yet) available - for example in a Border.
*/
private DialogBaseUnits cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
/**
* Maps <code>FontMetrics</code> to horizontal dialog base units.
* This is a second-level cache, that stores dialog base units
* for a <code>FontMetrics</code> object.
*/
private Map cachedDialogBaseUnits = new HashMap();
// Instance Creation and Access *******************************************
/**
* Constructs a DefaultUnitConverter and registers
* a listener that handles changes in the look&feel.
*/
private DefaultUnitConverter() {
UIManager.addPropertyChangeListener(new LookAndFeelChangeHandler());
changeSupport = new PropertyChangeSupport(this);
}
/**
* Lazily instantiates and returns the sole instance.
*
* @return the lazily instantiated sole instance
*/
public static DefaultUnitConverter getInstance() {
if (instance == null) {
instance = new DefaultUnitConverter();
}
return instance;
}
// Access to Bound Properties *********************************************
/**
* Returns the string used to compute the average character width.
* By default it is initialized to "X".
*
* @return the test string used to compute the average character width
*/
public String getAverageCharacterWidthTestString() {
return averageCharWidthTestString;
}
/**
* Sets a string that will be used to compute the average character width.
* By default it is initialized to "X". You can provide
* other test strings, for example:
* <ul>
* <li>"Xximeee"</li>
* <li>"ABCEDEFHIJKLMNOPQRSTUVWXYZ"</li>
* <li>"abcdefghijklmnopqrstuvwxyz"</li>
* </ul>
*
* @param newTestString the test string to be used
* @exception IllegalArgumentException if the test string is empty
* @exception NullPointerException if the test string is <code>null</code>
*/
public void setAverageCharacterWidthTestString(String newTestString) {
if (newTestString == null) {
throw new NullPointerException("The test string must not be null.");
}
if (newTestString.length() == 0) {
throw new IllegalArgumentException(
"The test string must not be empty.");
}
String oldTestString = averageCharWidthTestString;
averageCharWidthTestString = newTestString;
changeSupport.firePropertyChange("averageCharacterWidthTestString",
oldTestString, newTestString);
}
/**
* Lazily creates and returns the dialog font used to compute
* the dialog base units.
*
* @return the font used to compute the dialog base units
*/
public Font getDefaultDialogFont() {
if (defaultDialogFont == null) {
defaultDialogFont = lookupDefaultDialogFont();
}
return defaultDialogFont;
}
/**
* Sets a dialog font that will be used to compute the dialog base units.
*
* @param newFont the default dialog font to be set
*/
public void setDefaultDialogFont(Font newFont) {
Font oldFont = defaultDialogFont; // Don't use the getter
defaultDialogFont = newFont;
changeSupport.firePropertyChange("defaultDialogFont", oldFont, newFont);
}
// Implementing Abstract Superclass Behavior ******************************
/**
* Returns the cached or computed horizontal dialog base units.
*
* @param component a Component that provides the font and graphics
* @return the horizontal dialog base units
*/
protected double getDialogBaseUnitsX(Component component) {
return getDialogBaseUnits(component).x;
}
/**
* Returns the cached or computed vertical dialog base units
* for the given component.
*
* @param component a Component that provides the font and graphics
* @return the vertical dialog base units
*/
protected double getDialogBaseUnitsY(Component component) {
return getDialogBaseUnits(component).y;
}
// Compute and Cache Global and Components Dialog Base Units **************
/**
* Lazily computes and answer the global dialog base units.
* Should be re-computed if the l&f, platform, or screen changes.
*
* @return a cached DialogBaseUnits object used globally if no container is available
*/
private DialogBaseUnits getGlobalDialogBaseUnits() {
if (cachedGlobalDialogBaseUnits == null) {
cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
}
return cachedGlobalDialogBaseUnits;
}
/**
* Looks up and returns the dialog base units for the given component.
* In case the component is <code>null</code> the global dialog base units
* are answered.<p>
*
* Before we compute the dialog base units we check whether they
* have been computed and cached before - for the same component
* <code>FontMetrics</code>.
*
* @param c the component that provides the graphics object
* @return the DialogBaseUnits object for the given component
*/
private DialogBaseUnits getDialogBaseUnits(Component c) {
if (c == null) { // || (font = c.getFont()) == null) {
// logInfo("Missing font metrics: " + c);
return getGlobalDialogBaseUnits();
}
FontMetrics fm = c.getFontMetrics(getDefaultDialogFont());
DialogBaseUnits dialogBaseUnits = (DialogBaseUnits) cachedDialogBaseUnits
.get(fm);
if (dialogBaseUnits == null) {
dialogBaseUnits = computeDialogBaseUnits(fm);
cachedDialogBaseUnits.put(fm, dialogBaseUnits);
}
return dialogBaseUnits;
}
/**
* Computes and returns the horizontal dialog base units.
* Honors the font, font size and resolution.<p>
*
* Implementation Note: 14dluY map to 22 pixel for 8pt Tahoma on 96 dpi.
* I could not yet manage to compute the Microsoft compliant font height.
* Therefore this method adds a correction value that seems to work
* well with the vast majority of desktops.<p>
*
* TODO: Revise the computation of vertical base units as soon as
* there are more information about the original computation
* in Microsoft environments.
*
* @param metrics the FontMetrics used to measure the dialog font
* @return the horizontal and vertical dialog base units
*/
private DialogBaseUnits computeDialogBaseUnits(FontMetrics metrics) {
double averageCharWidth = computeAverageCharWidth(metrics,
averageCharWidthTestString);
int ascent = metrics.getAscent();
double height = ascent > 14 ? ascent : ascent + (15 - ascent) / 3;
DialogBaseUnits dialogBaseUnits = new DialogBaseUnits(averageCharWidth,
height);
LOGGER.config("Computed dialog base units " + dialogBaseUnits
+ " for: " + metrics.getFont());
return dialogBaseUnits;
}
/**
* Computes the global dialog base units. The current implementation
* assumes a fixed 8pt font and on 96 or 120 dpi. A better implementation
* should ask for the main dialog font and should honor the current
* screen resolution.<p>
*
* Should be re-computed if the l&f, platform, or screen changes.
*
* @return a DialogBaseUnits object used globally if no container is available
*/
private DialogBaseUnits computeGlobalDialogBaseUnits() {
LOGGER.config("Computing global dialog base units...");
Font dialogFont = getDefaultDialogFont();
FontMetrics metrics = createDefaultGlobalComponent().getFontMetrics(
dialogFont);
DialogBaseUnits globalDialogBaseUnits = computeDialogBaseUnits(metrics);
return globalDialogBaseUnits;
}
/**
* Looks up and returns the font used by buttons.
* First, tries to request the button font from the UIManager;
* if this fails a JButton is created and asked for its font.
*
* @return the font used for a standard button
*/
private Font lookupDefaultDialogFont() {
Font buttonFont = UIManager.getFont("Button.font");
return buttonFont != null ? buttonFont : new JButton().getFont();
}
/**
* Creates and returns a component that is used to lookup the default
* font metrics. The current implementation creates a <code>JPanel</code>.
* Since this panel has no parent, it has no toolkit assigned. And so,
* requesting the font metrics will end up using the default toolkit
* and its deprecated method <code>ToolKit#getFontMetrics()</code>.<p>
*
* TODO: Consider publishing this method and providing a setter, so that
* an API user can set a realized component that has a toolkit assigned.
*
* @return a component used to compute the default font metrics
*/
private Component createDefaultGlobalComponent() {
return new JPanel(null);
}
/**
* Invalidates the caches. Resets the global dialog base units
* and clears the Map from <code>FontMetrics</code> to dialog base units.
* This is invoked after a change of the look&feel.
*/
private void invalidateCaches() {
cachedGlobalDialogBaseUnits = null;
cachedDialogBaseUnits.clear();
}
// Managing Property Change Listeners **********************************
/**
* Adds a PropertyChangeListener to the listener list. The listener is
* registered for all bound properties of this class.<p>
*
* If listener is null, no exception is thrown and no action is performed.
*
* @param listener the PropertyChangeListener to be added
*
* @see #removePropertyChangeListener(PropertyChangeListener)
* @see #removePropertyChangeListener(String, PropertyChangeListener)
* @see #addPropertyChangeListener(String, PropertyChangeListener)
*/
public synchronized void addPropertyChangeListener(
PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(listener);
}
/**
* Removes a PropertyChangeListener from the listener list. This method
* should be used to remove PropertyChangeListeners that were registered
* for all bound properties of this class.<p>
*
* If listener is null, no exception is thrown and no action is performed.
*
* @param listener the PropertyChangeListener to be removed
*
* @see #addPropertyChangeListener(PropertyChangeListener)
* @see #addPropertyChangeListener(String, PropertyChangeListener)
* @see #removePropertyChangeListener(String, PropertyChangeListener)
*/
public synchronized void removePropertyChangeListener(
PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(listener);
}
/**
* Adds a PropertyChangeListener to the listener list for a specific
* property. The specified property may be user-defined.<p>
*
* Note that if this Model is inheriting a bound property, then no event
* will be fired in response to a change in the inherited property.<p>
*
* If listener is null, no exception is thrown and no action is performed.
*
* @param propertyName one of the property names listed above
* @param listener the PropertyChangeListener to be added
*
* @see #removePropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
* @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
*/
public synchronized void addPropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
changeSupport.addPropertyChangeListener(propertyName, listener);
}
/**
* Removes a PropertyChangeListener from the listener list for a specific
* property. This method should be used to remove PropertyChangeListeners
* that were registered for a specific bound property.<p>
*
* If listener is null, no exception is thrown and no action is performed.
*
* @param propertyName a valid property name
* @param listener the PropertyChangeListener to be removed
*
* @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
* @see #removePropertyChangeListener(java.beans.PropertyChangeListener)
*/
public synchronized void removePropertyChangeListener(String propertyName,
PropertyChangeListener listener) {
changeSupport.removePropertyChangeListener(propertyName, listener);
}
// Helper Code ************************************************************
/**
* Describes horizontal and vertical dialog base units.
*/
private static final class DialogBaseUnits {
final double x;
final double y;
DialogBaseUnits(double dialogBaseUnitsX, double dialogBaseUnitsY) {
this.x = dialogBaseUnitsX;
this.y = dialogBaseUnitsY;
}
public String toString() {
return "DBU(x=" + x + "; y=" + y + ")";
}
}
/**
* Listens to changes of the Look and Feel and invalidates the cache.
*/
private final class LookAndFeelChangeHandler implements
PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
invalidateCaches();
}
}
}