001: /*
002: * Copyright (c) 2002-2007 JGoodies Karsten Lentzsch. All Rights Reserved.
003: *
004: * Redistribution and use in source and binary forms, with or without
005: * modification, are permitted provided that the following conditions are met:
006: *
007: * o Redistributions of source code must retain the above copyright notice,
008: * this list of conditions and the following disclaimer.
009: *
010: * o Redistributions in binary form must reproduce the above copyright notice,
011: * this list of conditions and the following disclaimer in the documentation
012: * and/or other materials provided with the distribution.
013: *
014: * o Neither the name of JGoodies Karsten Lentzsch nor the names of
015: * its contributors may be used to endorse or promote products derived
016: * from this software without specific prior written permission.
017: *
018: * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
019: * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
020: * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
021: * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
022: * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
023: * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
024: * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
025: * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
026: * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
027: * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
028: * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
029: */
030:
031: package com.jgoodies.forms.util;
032:
033: import java.awt.Component;
034: import java.awt.Font;
035: import java.awt.FontMetrics;
036: import java.beans.PropertyChangeEvent;
037: import java.beans.PropertyChangeListener;
038: import java.beans.PropertyChangeSupport;
039: import java.util.HashMap;
040: import java.util.Map;
041: import java.util.logging.Logger;
042:
043: import javax.swing.JButton;
044: import javax.swing.JPanel;
045: import javax.swing.UIManager;
046:
047: /**
048: * This is the default implementation of the {@link UnitConverter} interface.
049: * It converts horizontal and vertical dialog base units to pixels.<p>
050: *
051: * The horizontal base unit is equal to the average width, in pixels,
052: * of the characters in the system font; the vertical base unit is equal
053: * to the height, in pixels, of the font.
054: * Each horizontal base unit is equal to 4 horizontal dialog units;
055: * each vertical base unit is equal to 8 vertical dialog units.<p>
056: *
057: * The DefaultUnitConverter computes dialog base units using a default font
058: * and a test string for the average character width. You can configure
059: * the font and the test string via the bound Bean properties
060: * <em>defaultDialogFont</em> and <em>averageCharacterWidthTestString</em>.
061: * See also Microsoft's suggestion for a custom computation
062: * <a href="http://msdn.microsoft.com/library/default.asp?url=/library/en-us/dnwue/html/ch14e.asp">here</a>.<p>
063: *
064: * Since the Forms 1.1 this converter logs font information at
065: * the <code>CONFIG</code> level.
066: *
067: * @version $Revision: 1.5 $
068: * @author Karsten Lentzsch
069: * @see UnitConverter
070: * @see com.jgoodies.forms.layout.Size
071: * @see com.jgoodies.forms.layout.Sizes
072: */
073: public final class DefaultUnitConverter extends AbstractUnitConverter {
074:
075: // public static final String UPPERCASE_ALPHABET =
076: // "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
077: //
078: // public static final String LOWERCASE_ALPHABET =
079: // "abcdefghijklmnopqrstuvwxyz";
080:
081: private final static Logger LOGGER = Logger
082: .getLogger(DefaultUnitConverter.class.getName());
083:
084: /**
085: * Holds the sole instance that will be lazily instantiated.
086: */
087: private static DefaultUnitConverter instance;
088:
089: /**
090: * Holds the string that is used to compute the average character width.
091: * By default this is just "X".
092: */
093: private String averageCharWidthTestString = "X";
094:
095: /**
096: * Holds a custom font that is used to compute the global dialog base units.
097: * If not set, a fallback font is is lazily created in method
098: * #getCachedDefaultDialogFont, which in turn looks up a font
099: * in method #lookupDefaultDialogFont.
100: */
101: private Font defaultDialogFont;
102:
103: /**
104: * If any <code>PropertyChangeListeners</code> have been registered,
105: * the <code>changeSupport</code> field describes them.
106: *
107: * @serial
108: * @see #addPropertyChangeListener(PropertyChangeListener)
109: * @see #addPropertyChangeListener(String, PropertyChangeListener)
110: * @see #removePropertyChangeListener(PropertyChangeListener)
111: * @see #removePropertyChangeListener(String, PropertyChangeListener)
112: */
113: private PropertyChangeSupport changeSupport;
114:
115: // Cached *****************************************************************
116:
117: /**
118: * Holds the cached global dialog base units that are used if
119: * a component is not (yet) available - for example in a Border.
120: */
121: private DialogBaseUnits cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
122:
123: /**
124: * Maps <code>FontMetrics</code> to horizontal dialog base units.
125: * This is a second-level cache, that stores dialog base units
126: * for a <code>FontMetrics</code> object.
127: */
128: private Map cachedDialogBaseUnits = new HashMap();
129:
130: /**
131: * Holds a cached default dialog font that is used as fallback,
132: * if no default dialog font has been set.
133: *
134: * @see #getDefaultDialogFont()
135: * @see #setDefaultDialogFont(Font)
136: */
137: private Font cachedDefaultDialogFont = null;
138:
139: // Instance Creation and Access *******************************************
140:
141: /**
142: * Constructs a DefaultUnitConverter and registers
143: * a listener that handles changes in the look&feel.
144: */
145: private DefaultUnitConverter() {
146: UIManager
147: .addPropertyChangeListener(new LookAndFeelChangeHandler());
148: changeSupport = new PropertyChangeSupport(this );
149: }
150:
151: /**
152: * Lazily instantiates and returns the sole instance.
153: *
154: * @return the lazily instantiated sole instance
155: */
156: public static DefaultUnitConverter getInstance() {
157: if (instance == null) {
158: instance = new DefaultUnitConverter();
159: }
160: return instance;
161: }
162:
163: // Access to Bound Properties *********************************************
164:
165: /**
166: * Returns the string used to compute the average character width.
167: * By default it is initialized to "X".
168: *
169: * @return the test string used to compute the average character width
170: */
171: public String getAverageCharacterWidthTestString() {
172: return averageCharWidthTestString;
173: }
174:
175: /**
176: * Sets a string that will be used to compute the average character width.
177: * By default it is initialized to "X". You can provide
178: * other test strings, for example:
179: * <ul>
180: * <li>"Xximeee"</li>
181: * <li>"ABCEDEFHIJKLMNOPQRSTUVWXYZ"</li>
182: * <li>"abcdefghijklmnopqrstuvwxyz"</li>
183: * </ul>
184: *
185: * @param newTestString the test string to be used
186: * @throws IllegalArgumentException if the test string is empty
187: * @throws NullPointerException if the test string is <code>null</code>
188: */
189: public void setAverageCharacterWidthTestString(String newTestString) {
190: if (newTestString == null)
191: throw new NullPointerException(
192: "The test string must not be null.");
193: if (newTestString.length() == 0)
194: throw new IllegalArgumentException(
195: "The test string must not be empty.");
196:
197: String oldTestString = averageCharWidthTestString;
198: averageCharWidthTestString = newTestString;
199: changeSupport.firePropertyChange(
200: "averageCharacterWidthTestString", oldTestString,
201: newTestString);
202: }
203:
204: /**
205: * Returns the dialog font that is used to compute the dialog base units.
206: * If a default dialog font has been set using
207: * {@link #setDefaultDialogFont(Font)}, this font will be returned.
208: * Otherwise a cached fallback will be lazily created.
209: *
210: * @return the font used to compute the dialog base units
211: */
212: public Font getDefaultDialogFont() {
213: return defaultDialogFont != null ? defaultDialogFont
214: : getCachedDefaultDialogFont();
215: }
216:
217: /**
218: * Sets a dialog font that will be used to compute the dialog base units.
219: *
220: * @param newFont the default dialog font to be set
221: */
222: public void setDefaultDialogFont(Font newFont) {
223: Font oldFont = defaultDialogFont; // Don't use the getter
224: defaultDialogFont = newFont;
225: changeSupport.firePropertyChange("defaultDialogFont", oldFont,
226: newFont);
227: }
228:
229: // Implementing Abstract Superclass Behavior ******************************
230:
231: /**
232: * Returns the cached or computed horizontal dialog base units.
233: *
234: * @param component a Component that provides the font and graphics
235: * @return the horizontal dialog base units
236: */
237: protected double getDialogBaseUnitsX(Component component) {
238: return getDialogBaseUnits(component).x;
239: }
240:
241: /**
242: * Returns the cached or computed vertical dialog base units
243: * for the given component.
244: *
245: * @param component a Component that provides the font and graphics
246: * @return the vertical dialog base units
247: */
248: protected double getDialogBaseUnitsY(Component component) {
249: return getDialogBaseUnits(component).y;
250: }
251:
252: // Compute and Cache Global and Components Dialog Base Units **************
253:
254: /**
255: * Lazily computes and answer the global dialog base units.
256: * Should be re-computed if the l&f, platform, or screen changes.
257: *
258: * @return a cached DialogBaseUnits object used globally if no container is available
259: */
260: private DialogBaseUnits getGlobalDialogBaseUnits() {
261: if (cachedGlobalDialogBaseUnits == null) {
262: cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
263: }
264: return cachedGlobalDialogBaseUnits;
265: }
266:
267: /**
268: * Looks up and returns the dialog base units for the given component.
269: * In case the component is <code>null</code> the global dialog base units
270: * are answered.<p>
271: *
272: * Before we compute the dialog base units we check whether they
273: * have been computed and cached before - for the same component
274: * <code>FontMetrics</code>.
275: *
276: * @param c the component that provides the graphics object
277: * @return the DialogBaseUnits object for the given component
278: */
279: private DialogBaseUnits getDialogBaseUnits(Component c) {
280: if (c == null) { // || (font = c.getFont()) == null) {
281: // logInfo("Missing font metrics: " + c);
282: return getGlobalDialogBaseUnits();
283: }
284: FontMetrics fm = c.getFontMetrics(getDefaultDialogFont());
285: DialogBaseUnits dialogBaseUnits = (DialogBaseUnits) cachedDialogBaseUnits
286: .get(fm);
287: if (dialogBaseUnits == null) {
288: dialogBaseUnits = computeDialogBaseUnits(fm);
289: cachedDialogBaseUnits.put(fm, dialogBaseUnits);
290: }
291: return dialogBaseUnits;
292: }
293:
294: /**
295: * Computes and returns the horizontal dialog base units.
296: * Honors the font, font size and resolution.<p>
297: *
298: * Implementation Note: 14dluY map to 22 pixel for 8pt Tahoma on 96 dpi.
299: * I could not yet manage to compute the Microsoft compliant font height.
300: * Therefore this method adds a correction value that seems to work
301: * well with the vast majority of desktops.<p>
302: *
303: * TODO: Revise the computation of vertical base units as soon as
304: * there are more information about the original computation
305: * in Microsoft environments.
306: *
307: * @param metrics the FontMetrics used to measure the dialog font
308: * @return the horizontal and vertical dialog base units
309: */
310: private DialogBaseUnits computeDialogBaseUnits(FontMetrics metrics) {
311: double averageCharWidth = computeAverageCharWidth(metrics,
312: averageCharWidthTestString);
313: int ascent = metrics.getAscent();
314: double height = ascent > 14 ? ascent : ascent + (15 - ascent)
315: / 3;
316: DialogBaseUnits dialogBaseUnits = new DialogBaseUnits(
317: averageCharWidth, height);
318: LOGGER.config("Computed dialog base units " + dialogBaseUnits
319: + " for: " + metrics.getFont());
320: return dialogBaseUnits;
321: }
322:
323: /**
324: * Computes the global dialog base units. The current implementation
325: * assumes a fixed 8pt font and on 96 or 120 dpi. A better implementation
326: * should ask for the main dialog font and should honor the current
327: * screen resolution.<p>
328: *
329: * Should be re-computed if the l&f, platform, or screen changes.
330: *
331: * @return a DialogBaseUnits object used globally if no container is available
332: */
333: private DialogBaseUnits computeGlobalDialogBaseUnits() {
334: LOGGER.config("Computing global dialog base units...");
335: Font dialogFont = getDefaultDialogFont();
336: FontMetrics metrics = createDefaultGlobalComponent()
337: .getFontMetrics(dialogFont);
338: DialogBaseUnits globalDialogBaseUnits = computeDialogBaseUnits(metrics);
339: return globalDialogBaseUnits;
340: }
341:
342: /**
343: * Lazily creates and returns a fallback for the dialog font
344: * that is used to compute the dialog base units.
345: * This fallback font is cached and will be reset if the L&F changes.
346: *
347: * @return the cached fallback font used to compute the dialog base units
348: */
349: private Font getCachedDefaultDialogFont() {
350: if (cachedDefaultDialogFont == null) {
351: cachedDefaultDialogFont = lookupDefaultDialogFont();
352: }
353: return cachedDefaultDialogFont;
354: }
355:
356: /**
357: * Looks up and returns the font used by buttons.
358: * First, tries to request the button font from the UIManager;
359: * if this fails a JButton is created and asked for its font.
360: *
361: * @return the font used for a standard button
362: */
363: private Font lookupDefaultDialogFont() {
364: Font buttonFont = UIManager.getFont("Button.font");
365: return buttonFont != null ? buttonFont : new JButton()
366: .getFont();
367: }
368:
369: /**
370: * Creates and returns a component that is used to lookup the default
371: * font metrics. The current implementation creates a <code>JPanel</code>.
372: * Since this panel has no parent, it has no toolkit assigned. And so,
373: * requesting the font metrics will end up using the default toolkit
374: * and its deprecated method <code>ToolKit#getFontMetrics()</code>.<p>
375: *
376: * TODO: Consider publishing this method and providing a setter, so that
377: * an API user can set a realized component that has a toolkit assigned.
378: *
379: * @return a component used to compute the default font metrics
380: */
381: private Component createDefaultGlobalComponent() {
382: return new JPanel(null);
383: }
384:
385: /**
386: * Invalidates the caches. Resets the global dialog base units,
387: * clears the Map from <code>FontMetrics</code> to dialog base units,
388: * and resets the fallback for the default dialog font.
389: * This is invoked after a change of the look&feel.
390: */
391: private void invalidateCaches() {
392: cachedGlobalDialogBaseUnits = null;
393: cachedDialogBaseUnits.clear();
394: cachedDefaultDialogFont = null;
395: }
396:
397: // Managing Property Change Listeners **********************************
398:
399: /**
400: * Adds a PropertyChangeListener to the listener list. The listener is
401: * registered for all bound properties of this class.<p>
402: *
403: * If listener is null, no exception is thrown and no action is performed.
404: *
405: * @param listener the PropertyChangeListener to be added
406: *
407: * @see #removePropertyChangeListener(PropertyChangeListener)
408: * @see #removePropertyChangeListener(String, PropertyChangeListener)
409: * @see #addPropertyChangeListener(String, PropertyChangeListener)
410: */
411: public synchronized void addPropertyChangeListener(
412: PropertyChangeListener listener) {
413: changeSupport.addPropertyChangeListener(listener);
414: }
415:
416: /**
417: * Removes a PropertyChangeListener from the listener list. This method
418: * should be used to remove PropertyChangeListeners that were registered
419: * for all bound properties of this class.<p>
420: *
421: * If listener is null, no exception is thrown and no action is performed.
422: *
423: * @param listener the PropertyChangeListener to be removed
424: *
425: * @see #addPropertyChangeListener(PropertyChangeListener)
426: * @see #addPropertyChangeListener(String, PropertyChangeListener)
427: * @see #removePropertyChangeListener(String, PropertyChangeListener)
428: */
429: public synchronized void removePropertyChangeListener(
430: PropertyChangeListener listener) {
431: changeSupport.removePropertyChangeListener(listener);
432: }
433:
434: /**
435: * Adds a PropertyChangeListener to the listener list for a specific
436: * property. The specified property may be user-defined.<p>
437: *
438: * Note that if this Model is inheriting a bound property, then no event
439: * will be fired in response to a change in the inherited property.<p>
440: *
441: * If listener is null, no exception is thrown and no action is performed.
442: *
443: * @param propertyName one of the property names listed above
444: * @param listener the PropertyChangeListener to be added
445: *
446: * @see #removePropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
447: * @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
448: */
449: public synchronized void addPropertyChangeListener(
450: String propertyName, PropertyChangeListener listener) {
451: changeSupport.addPropertyChangeListener(propertyName, listener);
452: }
453:
454: /**
455: * Removes a PropertyChangeListener from the listener list for a specific
456: * property. This method should be used to remove PropertyChangeListeners
457: * that were registered for a specific bound property.<p>
458: *
459: * If listener is null, no exception is thrown and no action is performed.
460: *
461: * @param propertyName a valid property name
462: * @param listener the PropertyChangeListener to be removed
463: *
464: * @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
465: * @see #removePropertyChangeListener(java.beans.PropertyChangeListener)
466: */
467: public synchronized void removePropertyChangeListener(
468: String propertyName, PropertyChangeListener listener) {
469: changeSupport.removePropertyChangeListener(propertyName,
470: listener);
471: }
472:
473: // Helper Code ************************************************************
474:
475: /**
476: * Describes horizontal and vertical dialog base units.
477: */
478: private static final class DialogBaseUnits {
479:
480: final double x;
481: final double y;
482:
483: DialogBaseUnits(double dialogBaseUnitsX, double dialogBaseUnitsY) {
484: this .x = dialogBaseUnitsX;
485: this .y = dialogBaseUnitsY;
486: }
487:
488: public String toString() {
489: return "DBU(x=" + x + "; y=" + y + ")";
490: }
491: }
492:
493: /**
494: * Listens to changes of the Look and Feel and invalidates the cache.
495: */
496: private final class LookAndFeelChangeHandler implements
497: PropertyChangeListener {
498: public void propertyChange(PropertyChangeEvent evt) {
499: invalidateCaches();
500: }
501: }
502:
503: }
|