001: /*
002: * Copyright (C) 2005 Jeff Tassin
003: *
004: * This library is free software; you can redistribute it and/or
005: * modify it under the terms of the GNU Lesser General Public
006: * License as published by the Free Software Foundation; either
007: * version 2.1 of the License, or (at your option) any later version.
008: *
009: * This library is distributed in the hope that it will be useful,
010: * but WITHOUT ANY WARRANTY; without even the implied warranty of
011: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012: * Lesser General Public License for more details.
013: *
014: * You should have received a copy of the GNU Lesser General Public
015: * License along with this library; if not, write to the Free Software
016: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
017: */
018:
019: package com.jeta.swingbuilder.gui.utils;
020:
021: import java.awt.Component;
022: import java.awt.Font;
023: import java.awt.FontMetrics;
024: import java.awt.Toolkit;
025: import java.beans.PropertyChangeEvent;
026: import java.beans.PropertyChangeListener;
027: import java.util.HashMap;
028: import java.util.Map;
029:
030: import javax.swing.UIManager;
031:
032: import com.jgoodies.forms.util.AbstractUnitConverter;
033:
034: /**
035: */
036: public class Units extends AbstractUnitConverter {
037: private static final int DTP_RESOLUTION = 72;
038:
039: private static Units instance;
040:
041: /**
042: * Holds the string that is used to compute the average character width. By
043: * default this is just "X".
044: */
045: private String averageCharWidthTestString = "X";
046:
047: /**
048: * Holds the font that is used to compute the global dialog base units. By
049: * default it is lazily created in method #getDefaultDialogFont, which in
050: * turn looks up a font in method #lookupDefaultDialogFont.
051: */
052: private Font defaultDialogFont;
053:
054: /**
055: * Holds the cached global dialog base units that are used if a component is
056: * not (yet) available - for example in a Border.
057: */
058: private DialogBaseUnits cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
059:
060: /**
061: * Maps <code>FontMetrics</code> to horizontal dialog base units. This is
062: * a second-level cache, that stores dialog base units for a
063: * <code>FontMetrics</code> object.
064: */
065: private Map cachedDialogBaseUnits = new HashMap();
066:
067: public double pixelAsInch(int pixels, Component component) {
068: return pixelAsInch(pixels, getScreenResolution(component));
069: }
070:
071: public double pixelAsMillimeter(int pixels, Component component) {
072: return pixelAsMillimeter(pixels, getScreenResolution(component));
073: }
074:
075: public double pixelAsCentimeter(int pixels, Component component) {
076: return pixelAsCentimeter(pixels, getScreenResolution(component));
077: }
078:
079: public int pixelAsPoint(int pixels, Component component) {
080: return pixelAsPoint(pixels, getScreenResolution(component));
081: }
082:
083: public int pixelAsDialogUnitX(int pixels, Component c) {
084: return pixelAsDialogUnitX(pixels, getDialogBaseUnitsX(c));
085: }
086:
087: public int pixelAsDialogUnitY(int pixels, Component c) {
088: return pixelAsDialogUnitY(pixels, getDialogBaseUnitsY(c));
089: }
090:
091: protected final double pixelAsInch(int pixels, int dpi) {
092: return (double) pixels / (double) dpi;
093: }
094:
095: protected final double pixelAsMillimeter(int pixels, int dpi) {
096: return (pixels * 254.0) / (10.0 * dpi);
097: }
098:
099: protected final double pixelAsCentimeter(int pixels, int dpi) {
100: return (pixels * 254.0) / (100.0 * dpi);
101: }
102:
103: protected final int pixelAsPoint(int pixels, int dpi) {
104: return Math.round((pixels * DTP_RESOLUTION) / dpi);
105: }
106:
107: protected int pixelAsDialogUnitX(int pixels, double dialogBaseUnitsX) {
108: return (int) Math.round(pixels * 4 / dialogBaseUnitsX);
109: }
110:
111: protected int pixelAsDialogUnitY(int pixels, double dialogBaseUnitsY) {
112: return (int) Math.round(pixels * 8 / dialogBaseUnitsY);
113: }
114:
115: private Units() {
116: UIManager.addPropertyChangeListener(new LAFChangeHandler());
117: }
118:
119: /**
120: * Lazily instantiates and returns the sole instance.
121: */
122: public static Units getInstance() {
123: if (instance == null) {
124: instance = new Units();
125: }
126: return instance;
127: }
128:
129: /**
130: * Returns the string used to compute the average character width. By
131: * default it is initialized to "X".
132: *
133: * @return the test string used to compute the average character width
134: */
135: public String getAverageCharacterWidthTestString() {
136: return averageCharWidthTestString;
137: }
138:
139: /**
140: * Lazily creates and returns the dialog font used to compute the dialog
141: * base units.
142: *
143: * @return the font used to compute the dialog base units
144: */
145: public Font getDefaultDialogFont() {
146: if (defaultDialogFont == null) {
147: defaultDialogFont = lookupDefaultDialogFont();
148: }
149: return defaultDialogFont;
150: }
151:
152: /**
153: * Sets a dialog font that will be used to compute the dialog base units.
154: *
155: * @param newFont
156: * the default dialog font to be set
157: */
158: public void setDefaultDialogFont(Font newFont) {
159: Font oldFont = defaultDialogFont; // Don't use the getter
160: defaultDialogFont = newFont;
161: }
162:
163: /**
164: * Answers the cached or computed horizontal dialog base units.
165: *
166: * @param component
167: * a Component that provides the font and graphics
168: * @return the horizontal dialog base units
169: */
170: protected double getDialogBaseUnitsX(Component component) {
171: return getDialogBaseUnits(component).x;
172: }
173:
174: /**
175: * Answers the cached or computed vertical dialog base units for the given
176: * component.
177: *
178: * @param component
179: * a Component that provides the font and graphics
180: * @return the vertical dialog base units
181: */
182: protected double getDialogBaseUnitsY(Component component) {
183: return getDialogBaseUnits(component).y;
184: }
185:
186: // Compute and Cache Global and Components Dialog Base Units **************
187:
188: /**
189: * Lazily computes and answer the global dialog base units. Should be
190: * re-computed if the l&f, platform, or screen changes.
191: */
192: private DialogBaseUnits getGlobalDialogBaseUnits() {
193: if (cachedGlobalDialogBaseUnits == null) {
194: cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
195: }
196: return cachedGlobalDialogBaseUnits;
197: }
198:
199: /**
200: * Looks up and answers the dialog base units for the given component. In
201: * case the component is <code>null</code> the global dialog base units
202: * are answered.
203: * <p>
204: * Before we compute the dialog base units, we check wether they have been
205: * computed and cached before - for the same component
206: * <code>FontMetrics</code>.
207: *
208: * @param c
209: * the component that provides the graphics object
210: * @return
211: */
212: private DialogBaseUnits getDialogBaseUnits(Component c) {
213: if (c == null) { // || (font = c.getFont()) == null) {
214: return getGlobalDialogBaseUnits();
215: }
216: FontMetrics fm = c.getFontMetrics(getDefaultDialogFont());
217: DialogBaseUnits dialogBaseUnits = (DialogBaseUnits) cachedDialogBaseUnits
218: .get(fm);
219: if (dialogBaseUnits == null) {
220: dialogBaseUnits = computeDialogBaseUnits(fm);
221: cachedDialogBaseUnits.put(fm, dialogBaseUnits);
222: }
223: return dialogBaseUnits;
224: }
225:
226: /**
227: * Computes and answers the horizontal dialog base units. Honors the font,
228: * font size and resolution.
229: * <p>
230: * Implementation Note: 14dluY map to 22 pixel for 8pt Tahoma on 96 dpi. I
231: * could not yet manage to compute the Microsoft compliant font height.
232: * Therefore this method adds a correction value that seems to work well
233: * with the vast majority of desktops. Anyway, I plan to revise this, as
234: * soon as I have more information about the original computation for
235: * vertical dialog base units.
236: *
237: * @return the horizontal and vertical dialog base units
238: */
239: private DialogBaseUnits computeDialogBaseUnits(FontMetrics metrics) {
240: double averageCharWidth = computeAverageCharWidth(metrics,
241: averageCharWidthTestString);
242: int ascent = metrics.getAscent();
243: double height = ascent > 14 ? ascent : ascent + (15 - ascent)
244: / 3;
245: DialogBaseUnits dialogBaseUnits = new DialogBaseUnits(
246: averageCharWidth, height);
247: return dialogBaseUnits;
248: }
249:
250: /**
251: * Computes the global dialog base units. The current implementation assumes
252: * a fixed 8pt font and on 96 or 120 dpi. A better implementation should ask
253: * for the main dialog font and should honor the current screen resolution.
254: * <p>
255: * Should be re-computed if the l&f, platform, or screen changes.
256: */
257: private DialogBaseUnits computeGlobalDialogBaseUnits() {
258: Font dialogFont = getDefaultDialogFont();
259: FontMetrics metrics = Toolkit.getDefaultToolkit()
260: .getFontMetrics(dialogFont);
261: DialogBaseUnits globalDialogBaseUnits = computeDialogBaseUnits(metrics);
262: return globalDialogBaseUnits;
263: }
264:
265: /**
266: * Looks up and returns the font used by buttons. First, tries to request
267: * the button font from the UIManager; if this fails a JButton is created
268: * and asked for its font.
269: *
270: * @return the font used for a standard button
271: */
272: private Font lookupDefaultDialogFont() {
273: Font buttonFont = UIManager.getFont("Button.font");
274: return buttonFont != null ? buttonFont
275: : new javax.swing.JButton().getFont();
276: }
277:
278: /**
279: * Invalidates the caches. Resets the global dialog base units and clears
280: * the Map from <code>FontMetrics</code> to dialog base units. This is
281: * invoked after a change of the look&feel.
282: */
283: private void invalidateCaches() {
284: cachedGlobalDialogBaseUnits = null;
285: cachedDialogBaseUnits.clear();
286: }
287:
288: private static class DialogBaseUnits {
289: final double x;
290: final double y;
291:
292: DialogBaseUnits(double dialogBaseUnitsX, double dialogBaseUnitsY) {
293: this .x = dialogBaseUnitsX;
294: this .y = dialogBaseUnitsY;
295: }
296:
297: public String toString() {
298: return "DBU(x=" + x + "; y=" + y + ")";
299: }
300: }
301:
302: // Listens to changes of the Look and Feel and invalidates a cache
303: private class LAFChangeHandler implements PropertyChangeListener {
304: public void propertyChange(PropertyChangeEvent evt) {
305: invalidateCaches();
306: }
307: }
308: }
|