com.privatejgoodies.forms.util.DefaultUnitConverter Maven / Gradle / Ivy
/*
* Copyright (c) 2002-2013 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.privatejgoodies.forms.util;
import static com.privatejgoodies.common.base.Preconditions.checkNotBlank;
import static com.privatejgoodies.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.privatejgoodies.forms.layout.Size
* @see com.privatejgoodies.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 + ")";
}
}
}