KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > com > jgoodies > forms > util > DefaultUnitConverter


1 /*
2  * Copyright (c) 2003 JGoodies Karsten Lentzsch. All Rights Reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions are met:
6  *
7  * o Redistributions of source code must retain the above copyright notice,
8  * this list of conditions and the following disclaimer.
9  *
10  * o Redistributions in binary form must reproduce the above copyright notice,
11  * this list of conditions and the following disclaimer in the documentation
12  * and/or other materials provided with the distribution.
13  *
14  * o Neither the name of JGoodies Karsten Lentzsch nor the names of
15  * its contributors may be used to endorse or promote products derived
16  * from this software without specific prior written permission.
17  *
18  * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
19  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
20  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
22  * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23  * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24  * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS;
25  * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
26  * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
27  * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,
28  * EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29  */

30
31 package com.jgoodies.forms.util;
32
33 import java.awt.Component JavaDoc;
34 import java.awt.Font JavaDoc;
35 import java.awt.FontMetrics JavaDoc;
36 import java.awt.Toolkit JavaDoc;
37 import java.beans.PropertyChangeEvent JavaDoc;
38 import java.beans.PropertyChangeListener JavaDoc;
39 import java.beans.PropertyChangeSupport JavaDoc;
40 import java.util.HashMap JavaDoc;
41 import java.util.Map JavaDoc;
42
43 import javax.swing.JButton JavaDoc;
44 import javax.swing.UIManager JavaDoc;
45
46 /**
47  * This is the default implementation of the {@link UnitConverter} interface.
48  * It converts horizontal and vertical dialog base units to pixels.
49  * <p>
50  * The horizontal base unit is equal to the average width, in pixels,
51  * of the characters in the system font; the vertical base unit is equal
52  * to the height, in pixels, of the font.
53  * Each horizontal base unit is equal to 4 horizontal dialog units;
54  * each vertical base unit is equal to 8 vertical dialog units.
55  * <p>
56  * The DefaultUnitConverter computes dialog base units using a default font
57  * and a test string for the average character width. You can configure
58  * the font and the test string via the bound Bean properties
59  * <i>defaultDialogFont</i> and <i>averageCharacterWidthTestString</i>.
60  *
61  * @author Karsten Lentzsch
62  * @version $Revision: 1.2 $
63  * @see UnitConverter
64  * @see com.jgoodies.forms.layout.Size
65  * @see com.jgoodies.forms.layout.Sizes
66  */

67 public final class DefaultUnitConverter extends AbstractUnitConverter {
68     
69 // public static final String UPPERCASE_ALPHABET =
70
// "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
71
//
72
// public static final String LOWERCASE_ALPHABET =
73
// "abcdefghijklmnopqrstuvwxyz";
74

75     /**
76      * Holds the sole instance that will be lazily instantiated.
77      */

78     private static DefaultUnitConverter instance;
79
80
81     /**
82      * Holds the string that is used to compute the average character width.
83      * By default this is just &quot;X&quot;.
84      */

85     private String JavaDoc averageCharWidthTestString = "X";
86     
87     
88     /**
89      * Holds the font that is used to compute the global dialog base units.
90      * By default it is lazily created in method #getDefaultDialogFont,
91      * which in turn looks up a font in method #lookupDefaultDialogFont.
92      */

93     private Font JavaDoc defaultDialogFont;
94     
95     
96     /**
97      * If any <code>PropertyChangeListeners</code> have been registered,
98      * the <code>changeSupport</code> field describes them.
99      *
100      * @serial
101      * @see #addPropertyChangeListener
102      * @see #removePropertyChangeListener
103      * @see #firePropertyChange
104      */

105     private PropertyChangeSupport JavaDoc changeSupport;
106     
107
108     // Cached *****************************************************************
109

110     /**
111      * Holds the cached global dialog base units that are used if
112      * a component is not (yet) available - for example in a Border.
113      */

114     private DialogBaseUnits cachedGlobalDialogBaseUnits =
115         computeGlobalDialogBaseUnits();
116         
117     /**
118      * Maps <code>FontMetrics</code> to horizontal dialog base units.
119      * This is a second-level cache, that stores dialog base units
120      * for a <code>FontMetrics</code> object.
121      */

122     private Map JavaDoc cachedDialogBaseUnits = new HashMap JavaDoc();
123
124
125     // Instance Creation and Access *******************************************
126

127     /**
128      * Constructs a <code>DefaultFontUnitConverter</code> and registers
129      * a listener that handles changes in the look&amp;feel.
130      */

131     private DefaultUnitConverter () {
132         UIManager.addPropertyChangeListener(new LAFChangeHandler());
133     }
134     
135     /**
136      * Lazily instantiates and returns the sole instance.
137      */

138     public static DefaultUnitConverter getInstance() {
139         if (instance == null) {
140             instance = new DefaultUnitConverter();
141         }
142         return instance;
143     }
144     
145     
146     // Access to Bound Properties *********************************************
147

148     /**
149      * Returns the string used to compute the average character width.
150      * By default it is initialized to &quot;X&quot;.
151      *
152      * @return the test string used to compute the average character width
153      */

154     public String JavaDoc getAverageCharacterWidthTestString() {
155         return averageCharWidthTestString;
156     }
157     
158     /**
159      * Sets a string that will be used to compute the average character width.
160      * By default it is initialized to &quot;X&quot;. You can provide
161      * other test strings, for example:
162      * <ul>
163      * <li>&quot;Xximeee&quot;</li>
164      * <li>&quot;ABCEDEFHIJKLMNOPQRSTUVWXYZ&quot;</li>
165      * <li>&quot;abcdefghijklmnopqrstuvwxyz&quot;</li>
166      * </ul>
167      *
168      * @param newTestString the test string to be used
169      * @throws IllegalArgumentException if the test string is empty
170      * @throws NullPointerException if the test string is <code>null</code>
171      */

172     public void setAverageCharacterWidthTestString(String JavaDoc newTestString) {
173         if (newTestString == null)
174             throw new NullPointerException JavaDoc("The test string must not be null.");
175         if (newTestString.length() == 0)
176             throw new IllegalArgumentException JavaDoc("The test string must not be empty.");
177             
178         String JavaDoc oldTestString = averageCharWidthTestString;
179         averageCharWidthTestString = newTestString;
180         changeSupport.firePropertyChange("averageCharacterWidthTestString",
181             oldTestString, newTestString);
182     }
183     
184
185     /**
186      * Lazily creates and returns the dialog font used to compute
187      * the dialog base units.
188      *
189      * @return the font used to compute the dialog base units
190      */

191     public Font JavaDoc getDefaultDialogFont() {
192         if (defaultDialogFont == null) {
193             defaultDialogFont = lookupDefaultDialogFont();
194         }
195         return defaultDialogFont;
196     }
197     
198     /**
199      * Sets a dialog font that will be used to compute the dialog base units.
200      *
201      * @param newFont the default dialog font to be set
202      */

203     public void setDefaultDialogFont(Font JavaDoc newFont) {
204         Font JavaDoc oldFont = defaultDialogFont; // Don't use the getter
205
defaultDialogFont = newFont;
206         changeSupport.firePropertyChange("defaultDialogFont", oldFont, newFont);
207     }
208     
209
210     // Implementing Abstract Superclass Behavior ******************************
211

212     /**
213      * Answers the cached or computed horizontal dialog base units.
214      *
215      * @param component a Component that provides the font and graphics
216      * @return the horizontal dialog base units
217      */

218     protected double getDialogBaseUnitsX(Component JavaDoc component) {
219         return getDialogBaseUnits(component).x;
220     }
221     
222     /**
223      * Answers the cached or computed vertical dialog base units
224      * for the given component.
225      *
226      * @param component a Component that provides the font and graphics
227      * @return the vertical dialog base units
228      */

229     protected double getDialogBaseUnitsY(Component JavaDoc component) {
230         return getDialogBaseUnits(component).y;
231     }
232
233
234     // Compute and Cache Global and Components Dialog Base Units **************
235

236     /**
237      * Lazily computes and answer the global dialog base units.
238      * Should be re-computed if the l&amp;f, platform, or screen changes.
239      */

240     private DialogBaseUnits getGlobalDialogBaseUnits() {
241         if (cachedGlobalDialogBaseUnits == null) {
242             cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
243         }
244         return cachedGlobalDialogBaseUnits;
245     }
246     
247     /**
248      * Looks up and answers the dialog base units for the given component.
249      * In case the component is <code>null</code> the global dialog base units
250      * are answered.
251      * <p>
252      * Before we compute the dialog base units, we check wether they
253      * have been computed and cached before - for the same component
254      * <code>FontMetrics</code>.
255      *
256      * @param c the component that provides the graphics object
257      * @return
258      */

259     private DialogBaseUnits getDialogBaseUnits(Component JavaDoc c) {
260         if (c == null) { // || (font = c.getFont()) == null) {
261
logInfo("Missing font metrics: " + c);
262             return getGlobalDialogBaseUnits();
263         }
264         FontMetrics JavaDoc fm = c.getFontMetrics(getDefaultDialogFont());
265         DialogBaseUnits dialogBaseUnits = (DialogBaseUnits) cachedDialogBaseUnits.get(fm);
266         if (dialogBaseUnits == null) {
267             dialogBaseUnits = computeDialogBaseUnits(fm);
268             cachedDialogBaseUnits.put(fm, dialogBaseUnits);
269         }
270         return dialogBaseUnits;
271     }
272     
273     /**
274      * Computes and answers the horizontal dialog base units.
275      * Honors the font, font size and resolution.
276      * <p>
277      * Implementation Note: 14dluY map to 22 pixel for 8pt Tahoma on 96 dpi.
278      * I could not yet manage to compute the Microsoft compliant font height.
279      * Therefore this method adds a correction value that seems to work
280      * well with the vast majority of desktops.
281      * Anyway, I plan to revise this, as soon as I have more information
282      * about the original computation for vertical dialog base units.
283      *
284      * @return the horizontal and vertical dialog base units
285      */

286     private DialogBaseUnits computeDialogBaseUnits(FontMetrics JavaDoc metrics) {
287         double averageCharWidth =
288             computeAverageCharWidth(metrics, averageCharWidthTestString);
289         int ascent = metrics.getAscent();
290         double height = ascent > 14 ? ascent : ascent + (15 - ascent) / 3;
291         DialogBaseUnits dialogBaseUnits =
292             new DialogBaseUnits(averageCharWidth, height);
293         logInfo(
294             "Computed dialog base units "
295                 + dialogBaseUnits
296                 + " for: "
297                 + metrics.getFont());
298         return dialogBaseUnits;
299     }
300
301     /**
302      * Computes the global dialog base units. The current implementation
303      * assumes a fixed 8pt font and on 96 or 120 dpi. A better implementation
304      * should ask for the main dialog font and should honor the current
305      * screen resolution.
306      * <p>
307      * Should be re-computed if the l&amp;f, platform, or screen changes.
308      */

309     private DialogBaseUnits computeGlobalDialogBaseUnits() {
310         logInfo("Computing global dialog base units...");
311         Font JavaDoc dialogFont = getDefaultDialogFont();
312         FontMetrics JavaDoc metrics = Toolkit.getDefaultToolkit().getFontMetrics(dialogFont);
313         DialogBaseUnits globalDialogBaseUnits = computeDialogBaseUnits(metrics);
314         return globalDialogBaseUnits;
315 // boolean isLowResolution =
316
// Toolkit.getDefaultToolkit().getScreenResolution() < 112;
317
// return isLowResolution
318
// ? new DialogBaseUnits(6, 11)
319
// : new DialogBaseUnits(8, 14);
320
}
321     
322     /**
323      * Looks up and returns the font used by buttons. First, tries
324      * to request the button font from the UIManager; if this fails
325      * a JButton is created and asked for its font.
326      *
327      * @return the font used for a standard button
328      */

329     private Font JavaDoc lookupDefaultDialogFont() {
330         Font JavaDoc buttonFont = UIManager.getFont("Button.font");
331         return buttonFont != null
332             ? buttonFont
333             : new JButton JavaDoc().getFont();
334     }
335     
336     /**
337      * Invalidates the caches. Resets the global dialog base units
338      * and clears the Map from <code>FontMetrics</code> to dialog base units.
339      * This is invoked after a change of the look&amp;feel.
340      */

341     private void invalidateCaches() {
342         cachedGlobalDialogBaseUnits = null;
343         cachedDialogBaseUnits.clear();
344     }
345     
346     
347      // Managing Property Change Listeners **********************************
348

349     /**
350      * Adds a PropertyChangeListener to the listener list. The listener is
351      * registered for all bound properties of this class.
352      * <p>
353      * If listener is null, no exception is thrown and no action is
354      * performed.
355      *
356      * @param listener the PropertyChangeListener to be added
357      *
358      * @see #removePropertyChangeListener
359      * @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
360      */

361     public final synchronized void addPropertyChangeListener(
362                                             PropertyChangeListener JavaDoc listener) {
363         if (listener == null) {
364             return;
365         }
366         if (changeSupport == null) {
367             changeSupport = new PropertyChangeSupport JavaDoc(this);
368         }
369         changeSupport.addPropertyChangeListener(listener);
370     }
371     
372     
373     /**
374      * Removes a PropertyChangeListener from the listener list. This method
375      * should be used to remove PropertyChangeListeners that were registered
376      * for all bound properties of this class.
377      * <p>
378      * If listener is null, no exception is thrown and no action is performed.
379      *
380      * @param listener the PropertyChangeListener to be removed
381      *
382      * @see #addPropertyChangeListener
383      * @see #removePropertyChangeListener(java.lang.String,java.beans.PropertyChangeListener)
384      */

385     public final synchronized void removePropertyChangeListener(
386                                         PropertyChangeListener JavaDoc listener) {
387         if (listener == null || changeSupport == null) {
388             return;
389         }
390         changeSupport.removePropertyChangeListener(listener);
391     }
392     
393     
394     /**
395      * Adds a PropertyChangeListener to the listener list for a specific
396      * property. The specified property may be user-defined.
397      * <p>
398      * Note that if this Model is inheriting a bound property, then no event
399      * will be fired in response to a change in the inherited property.
400      * <p>
401      * If listener is null, no exception is thrown and no action is performed.
402      *
403      * @param propertyName one of the property names listed above
404      * @param listener the PropertyChangeListener to be added
405      *
406      * @see #removePropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
407      * @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
408      */

409     public final synchronized void addPropertyChangeListener(
410                                         String JavaDoc propertyName,
411                                         PropertyChangeListener JavaDoc listener) {
412         if (listener == null) {
413             return;
414         }
415         if (changeSupport == null) {
416             changeSupport = new java.beans.PropertyChangeSupport JavaDoc(this);
417         }
418         changeSupport.addPropertyChangeListener(propertyName, listener);
419     }
420     
421     
422     /**
423      * Removes a PropertyChangeListener from the listener list for a specific
424      * property. This method should be used to remove PropertyChangeListeners
425      * that were registered for a specific bound property.
426      * <p>
427      * If listener is null, no exception is thrown and no action is performed.
428      *
429      * @param propertyName a valid property name
430      * @param listener the PropertyChangeListener to be removed
431      *
432      * @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
433      * @see #removePropertyChangeListener(java.beans.PropertyChangeListener)
434      */

435     public final synchronized void removePropertyChangeListener(
436                                         String JavaDoc propertyName,
437                                         PropertyChangeListener JavaDoc listener) {
438         if (listener == null || changeSupport == null) {
439             return;
440         }
441         changeSupport.removePropertyChangeListener(propertyName, listener);
442     }
443     
444     
445     // Helper Code ************************************************************
446

447     /**
448      * Logs an info message to the console.
449      * @param message the message to log
450      */

451     private void logInfo(String JavaDoc message) {
452 // System.out.println("INFO (DefaultUnitConverter) " + message);
453
}
454     
455     
456     // Describes horizontal and vertical dialog base units.
457
private static class DialogBaseUnits {
458         
459         final double x;
460         final double y;
461         
462         DialogBaseUnits(double dialogBaseUnitsX, double dialogBaseUnitsY) {
463             this.x = dialogBaseUnitsX;
464             this.y = dialogBaseUnitsY;
465         }
466         
467         public String JavaDoc toString() {
468             return "DBU(x=" + x + "; y=" + y + ")";
469         }
470     }
471     
472     // Listens to changes of the Look and Feel and invalidates a cache
473
private class LAFChangeHandler implements PropertyChangeListener JavaDoc {
474         public void propertyChange(PropertyChangeEvent JavaDoc evt) {
475             invalidateCaches();
476         }
477     }
478     
479 }
Popular Tags