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