com.jgoodies.forms.util.DefaultUnitConverter Maven / Gradle / Ivy
Show all versions of jgoodies-forms Show documentation
/*
* Copyright (c) 2002-2014 JGoodies Software GmbH. 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 Software GmbH 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 static com.jgoodies.common.base.Preconditions.checkNotBlank;
import static com.jgoodies.common.internal.Messages.MUST_NOT_BE_BLANK;
import java.awt.Component;
import java.awt.Font;
import java.awt.FontMetrics;
import java.util.logging.Level;
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.
*
* 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.
*
* 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
* defaultDialogFont and averageCharacterWidthTestString.
* See also Microsoft's suggestion for a custom computation
* custom computation.
* More information how to use dialog units in screen design can be found
* in Microsoft's
* Design
* Specifications and Guidelines.
*
* Since the Forms 1.1 this converter logs font information at
* the {@code CONFIG} level.
*
* @version $Revision: 1.23 $
* @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 PROPERTY_AVERAGE_CHARACTER_WIDTH_TEST_STRING =
"averageCharacterWidthTestString";
public static final String PROPERTY_DEFAULT_DIALOG_FONT =
"defaultDialogFont";
/**
* @since 1.6
*/
public static final String OLD_AVERAGE_CHARACTER_TEST_STRING =
"X";
/**
* @since 1.4
*/
public static final String MODERN_AVERAGE_CHARACTER_TEST_STRING =
"abcdefghijklmnopqrstuvwxyz0123456789";
/**
* @since 1.4
*/
public static final String BALANCED_AVERAGE_CHARACTER_TEST_STRING =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final 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.
* Since 1.6 the default value is the balanced average character test
* string, where it was just "X" before.
*/
private String averageCharWidthTestString =
BALANCED_AVERAGE_CHARACTER_TEST_STRING;
/**
* Holds a custom font that is used to compute the global dialog base units.
* If not set, a fallback font is is lazily created in method
* #getCachedDefaultDialogFont, which in turn looks up a font
* in method #lookupDefaultDialogFont.
*/
private Font defaultDialogFont;
// Cached *****************************************************************
/**
* Holds the lazily created cached global dialog base units that are used
* if a component is not (yet) available - for example in a Border.
*/
private DialogBaseUnits cachedGlobalDialogBaseUnits = null;
/**
* Holds the horizontal dialog base units that are valid
* for the FontMetrics stored in {@code cachedFontMetrics}.
*/
private DialogBaseUnits cachedDialogBaseUnits = null;
/**
* Holds the FontMetrics used to compute the per-component dialog units.
* The latter are valid, if a FontMetrics equals this stored metrics.
*/
private FontMetrics cachedFontMetrics = null;
/**
* Holds a cached default dialog font that is used as fallback,
* if no default dialog font has been set.
*
* @see #getDefaultDialogFont()
* @see #setDefaultDialogFont(Font)
*/
private Font cachedDefaultDialogFont = null;
// Instance Creation and Access *******************************************
/**
* Constructs a DefaultUnitConverter and registers
* a listener that handles changes in the look&feel.
*/
private DefaultUnitConverter() {
}
/**
* 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
* {@link #BALANCED_AVERAGE_CHARACTER_TEST_STRING}.
*
* @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
* {@link #BALANCED_AVERAGE_CHARACTER_TEST_STRING}. You can provide
* other test strings, for example:
*
* - "Xximeee"
* - "ABCEDEFHIJKLMNOPQRSTUVWXYZ"
* - "abcdefghijklmnopqrstuvwxyz"
*
*
* @param newTestString the test string to be used
* @throws NullPointerException if {@code newTestString} is {@code null}
* @throws IllegalArgumentException if {@code newTestString} is empty or whitespace
*/
public void setAverageCharacterWidthTestString(String newTestString) {
checkNotBlank(newTestString, MUST_NOT_BE_BLANK, "test string");
String oldTestString = averageCharWidthTestString;
averageCharWidthTestString = newTestString;
firePropertyChange(
PROPERTY_AVERAGE_CHARACTER_WIDTH_TEST_STRING,
oldTestString,
newTestString);
}
/**
* Returns the dialog font that is used to compute the dialog base units.
* If a default dialog font has been set using
* {@link #setDefaultDialogFont(Font)}, this font will be returned.
* Otherwise a cached fallback will be lazily created.
*
* @return the font used to compute the dialog base units
*/
public Font getDefaultDialogFont() {
return defaultDialogFont != null
? defaultDialogFont
: getCachedDefaultDialogFont();
}
/**
* 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;
clearCache();
firePropertyChange(PROPERTY_DEFAULT_DIALOG_FONT, 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
*/
@Override
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
*/
@Override
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} the global dialog base units
* are answered.
*
* Before we compute the dialog base units we check whether they
* have been computed and cached before - for the same component
* {@code FontMetrics}.
*
* @param c the component that provides the graphics object
* @return the DialogBaseUnits object for the given component
*/
private DialogBaseUnits getDialogBaseUnits(Component c) {
FormUtils.ensureValidCache();
if (c == null) { // || (font = c.getFont()) == null) {
// logInfo("Missing font metrics: " + c);
return getGlobalDialogBaseUnits();
}
FontMetrics fm = c.getFontMetrics(getDefaultDialogFont());
if (fm.equals(cachedFontMetrics)) {
return cachedDialogBaseUnits;
}
DialogBaseUnits dialogBaseUnits = computeDialogBaseUnits(fm);
cachedFontMetrics = fm;
cachedDialogBaseUnits = dialogBaseUnits;
return dialogBaseUnits;
}
/**
* Computes and returns the horizontal dialog base units.
* Honors the font, font size and resolution.
*
* 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.
*
* 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);
if (LOGGER.isLoggable(Level.CONFIG)) {
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.
*
* 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;
}
/**
* Lazily creates and returns a fallback for the dialog font
* that is used to compute the dialog base units.
* This fallback font is cached and will be reset if the L&F changes.
*
* @return the cached fallback font used to compute the dialog base units
*/
private Font getCachedDefaultDialogFont() {
FormUtils.ensureValidCache();
if (cachedDefaultDialogFont == null) {
cachedDefaultDialogFont = lookupDefaultDialogFont();
}
return cachedDefaultDialogFont;
}
/**
* 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 static 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}.
* 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()}.
*
* 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 static Component createDefaultGlobalComponent() {
return new JPanel(null);
}
/**
* Invalidates the caches. Resets the global dialog base units,
* clears the Map from {@code FontMetrics} to dialog base units,
* and resets the fallback for the default dialog font.
* This is invoked after a change of the look&feel.
*/
void clearCache() {
cachedGlobalDialogBaseUnits = null;
cachedFontMetrics = null;
cachedDefaultDialogFont = null;
}
// 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;
}
@Override
public String toString() {
return "DBU(x=" + x + "; y=" + y + ")";
}
}
}