2
* Copyright (c) 2002-2008 JGoodies Karsten Lentzsch. All Rights Reserved.
4
* Redistribution and use in source and binary forms, with or without
5
* modification, are permitted provided that the following conditions are met:
7
* o Redistributions of source code must retain the above copyright notice,
8
* this list of conditions and the following disclaimer.
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.
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.
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.
31
package com.jgoodies.forms.util;
33
import java.awt.Component;
35
import java.awt.FontMetrics;
36
import java.beans.PropertyChangeEvent;
37
import java.beans.PropertyChangeListener;
38
import java.beans.PropertyChangeSupport;
39
import java.util.HashMap;
41
import java.util.logging.Logger;
43
import javax.swing.JButton;
44
import javax.swing.JPanel;
45
import javax.swing.UIManager;
48
* This is the default implementation of the {@link UnitConverter} interface.
49
* It converts horizontal and vertical dialog base units to pixels.<p>
51
* The horizontal base unit is equal to the average width, in pixels,
52
* of the characters in the system font; the vertical base unit is equal
53
* to the height, in pixels, of the font.
54
* Each horizontal base unit is equal to 4 horizontal dialog units;
55
* each vertical base unit is equal to 8 vertical dialog units.<p>
57
* The DefaultUnitConverter computes dialog base units using a default font
58
* and a test string for the average character width. You can configure
59
* the font and the test string via the bound Bean properties
60
* <em>defaultDialogFont</em> and <em>averageCharacterWidthTestString</em>.
61
* See also Microsoft's suggestion for a custom computation
62
* <a href="http://support.microsoft.com/default.aspx?scid=kb;EN-US;125681">custom computation</a>.
63
* More information how to use dialog units in screen design can be found
65
* <a href="http://msdn2.microsoft.com/en-us/library/ms997619">Design
66
* Specifications and Guidelines</a>.<p>
68
* Since the Forms 1.1 this converter logs font information at
69
* the <code>CONFIG</code> level.
71
* @version $Revision: 1.13 $
72
* @author Karsten Lentzsch
74
* @see com.jgoodies.forms.layout.Size
75
* @see com.jgoodies.forms.layout.Sizes
77
public final class DefaultUnitConverter extends AbstractUnitConverter {
79
// public static final String UPPERCASE_ALPHABET =
80
// "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
82
// public static final String LOWERCASE_ALPHABET =
83
// "abcdefghijklmnopqrstuvwxyz";
85
private static final Logger LOGGER =
86
Logger.getLogger(DefaultUnitConverter.class.getName());
90
* Holds the sole instance that will be lazily instantiated.
92
private static DefaultUnitConverter instance;
96
* Holds the string that is used to compute the average character width.
97
* By default this is just "X".
99
private String averageCharWidthTestString = "X";
103
* Holds a custom font that is used to compute the global dialog base units.
104
* If not set, a fallback font is is lazily created in method
105
* #getCachedDefaultDialogFont, which in turn looks up a font
106
* in method #lookupDefaultDialogFont.
108
private Font defaultDialogFont;
112
* If any <code>PropertyChangeListeners</code> have been registered,
113
* the <code>changeSupport</code> field describes them.
116
* @see #addPropertyChangeListener(PropertyChangeListener)
117
* @see #addPropertyChangeListener(String, PropertyChangeListener)
118
* @see #removePropertyChangeListener(PropertyChangeListener)
119
* @see #removePropertyChangeListener(String, PropertyChangeListener)
121
private final PropertyChangeSupport changeSupport;
124
// Cached *****************************************************************
127
* Holds the cached global dialog base units that are used if
128
* a component is not (yet) available - for example in a Border.
130
private DialogBaseUnits cachedGlobalDialogBaseUnits =
131
computeGlobalDialogBaseUnits();
134
* Maps <code>FontMetrics</code> to horizontal dialog base units.
135
* This is a second-level cache, that stores dialog base units
136
* for a <code>FontMetrics</code> object.
138
private final Map cachedDialogBaseUnits = new HashMap();
141
* Holds a cached default dialog font that is used as fallback,
142
* if no default dialog font has been set.
144
* @see #getDefaultDialogFont()
145
* @see #setDefaultDialogFont(Font)
147
private Font cachedDefaultDialogFont = null;
150
// Instance Creation and Access *******************************************
153
* Constructs a DefaultUnitConverter and registers
154
* a listener that handles changes in the look&feel.
156
private DefaultUnitConverter() {
157
UIManager.addPropertyChangeListener(new LookAndFeelChangeHandler());
158
changeSupport = new PropertyChangeSupport(this);
163
* Lazily instantiates and returns the sole instance.
165
* @return the lazily instantiated sole instance
167
public static DefaultUnitConverter getInstance() {
168
if (instance == null) {
169
instance = new DefaultUnitConverter();
175
// Access to Bound Properties *********************************************
178
* Returns the string used to compute the average character width.
179
* By default it is initialized to "X".
181
* @return the test string used to compute the average character width
183
public String getAverageCharacterWidthTestString() {
184
return averageCharWidthTestString;
188
* Sets a string that will be used to compute the average character width.
189
* By default it is initialized to "X". You can provide
190
* other test strings, for example:
192
* <li>"Xximeee"</li>
193
* <li>"ABCEDEFHIJKLMNOPQRSTUVWXYZ"</li>
194
* <li>"abcdefghijklmnopqrstuvwxyz"</li>
197
* @param newTestString the test string to be used
198
* @throws IllegalArgumentException if the test string is empty
199
* @throws NullPointerException if the test string is <code>null</code>
201
public void setAverageCharacterWidthTestString(String newTestString) {
202
if (newTestString == null)
203
throw new NullPointerException("The test string must not be null.");
204
if (newTestString.length() == 0)
205
throw new IllegalArgumentException("The test string must not be empty.");
207
String oldTestString = averageCharWidthTestString;
208
averageCharWidthTestString = newTestString;
209
changeSupport.firePropertyChange("averageCharacterWidthTestString",
210
oldTestString, newTestString);
215
* Returns the dialog font that is used to compute the dialog base units.
216
* If a default dialog font has been set using
217
* {@link #setDefaultDialogFont(Font)}, this font will be returned.
218
* Otherwise a cached fallback will be lazily created.
220
* @return the font used to compute the dialog base units
222
public Font getDefaultDialogFont() {
223
return defaultDialogFont != null
225
: getCachedDefaultDialogFont();
229
* Sets a dialog font that will be used to compute the dialog base units.
231
* @param newFont the default dialog font to be set
233
public void setDefaultDialogFont(Font newFont) {
234
Font oldFont = defaultDialogFont; // Don't use the getter
235
defaultDialogFont = newFont;
237
changeSupport.firePropertyChange("defaultDialogFont", oldFont, newFont);
241
// Implementing Abstract Superclass Behavior ******************************
244
* Returns the cached or computed horizontal dialog base units.
246
* @param component a Component that provides the font and graphics
247
* @return the horizontal dialog base units
249
protected double getDialogBaseUnitsX(Component component) {
250
return getDialogBaseUnits(component).x;
254
* Returns the cached or computed vertical dialog base units
255
* for the given component.
257
* @param component a Component that provides the font and graphics
258
* @return the vertical dialog base units
260
protected double getDialogBaseUnitsY(Component component) {
261
return getDialogBaseUnits(component).y;
265
// Compute and Cache Global and Components Dialog Base Units **************
268
* Lazily computes and answer the global dialog base units.
269
* Should be re-computed if the l&f, platform, or screen changes.
271
* @return a cached DialogBaseUnits object used globally if no container is available
273
private DialogBaseUnits getGlobalDialogBaseUnits() {
274
if (cachedGlobalDialogBaseUnits == null) {
275
cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
277
return cachedGlobalDialogBaseUnits;
281
* Looks up and returns the dialog base units for the given component.
282
* In case the component is <code>null</code> the global dialog base units
285
* Before we compute the dialog base units we check whether they
286
* have been computed and cached before - for the same component
287
* <code>FontMetrics</code>.
289
* @param c the component that provides the graphics object
290
* @return the DialogBaseUnits object for the given component
292
private DialogBaseUnits getDialogBaseUnits(Component c) {
293
if (c == null) { // || (font = c.getFont()) == null) {
294
// logInfo("Missing font metrics: " + c);
295
return getGlobalDialogBaseUnits();
297
FontMetrics fm = c.getFontMetrics(getDefaultDialogFont());
298
DialogBaseUnits dialogBaseUnits = (DialogBaseUnits) cachedDialogBaseUnits.get(fm);
299
if (dialogBaseUnits == null) {
300
dialogBaseUnits = computeDialogBaseUnits(fm);
301
cachedDialogBaseUnits.put(fm, dialogBaseUnits);
303
return dialogBaseUnits;
307
* Computes and returns the horizontal dialog base units.
308
* Honors the font, font size and resolution.<p>
310
* Implementation Note: 14dluY map to 22 pixel for 8pt Tahoma on 96 dpi.
311
* I could not yet manage to compute the Microsoft compliant font height.
312
* Therefore this method adds a correction value that seems to work
313
* well with the vast majority of desktops.<p>
315
* TODO: Revise the computation of vertical base units as soon as
316
* there are more information about the original computation
317
* in Microsoft environments.
319
* @param metrics the FontMetrics used to measure the dialog font
320
* @return the horizontal and vertical dialog base units
322
private DialogBaseUnits computeDialogBaseUnits(FontMetrics metrics) {
323
double averageCharWidth =
324
computeAverageCharWidth(metrics, averageCharWidthTestString);
325
int ascent = metrics.getAscent();
326
double height = ascent > 14 ? ascent : ascent + (15 - ascent) / 3;
327
DialogBaseUnits dialogBaseUnits =
328
new DialogBaseUnits(averageCharWidth, height);
330
"Computed dialog base units "
333
+ metrics.getFont());
334
return dialogBaseUnits;
338
* Computes the global dialog base units. The current implementation
339
* assumes a fixed 8pt font and on 96 or 120 dpi. A better implementation
340
* should ask for the main dialog font and should honor the current
341
* screen resolution.<p>
343
* Should be re-computed if the l&f, platform, or screen changes.
345
* @return a DialogBaseUnits object used globally if no container is available
347
private DialogBaseUnits computeGlobalDialogBaseUnits() {
348
LOGGER.config("Computing global dialog base units...");
349
Font dialogFont = getDefaultDialogFont();
350
FontMetrics metrics = createDefaultGlobalComponent().getFontMetrics(dialogFont);
351
DialogBaseUnits globalDialogBaseUnits = computeDialogBaseUnits(metrics);
352
return globalDialogBaseUnits;
356
* Lazily creates and returns a fallback for the dialog font
357
* that is used to compute the dialog base units.
358
* This fallback font is cached and will be reset if the L&F changes.
360
* @return the cached fallback font used to compute the dialog base units
362
private Font getCachedDefaultDialogFont() {
363
if (cachedDefaultDialogFont == null) {
364
cachedDefaultDialogFont = lookupDefaultDialogFont();
366
return cachedDefaultDialogFont;
370
* Looks up and returns the font used by buttons.
371
* First, tries to request the button font from the UIManager;
372
* if this fails a JButton is created and asked for its font.
374
* @return the font used for a standard button
376
private Font lookupDefaultDialogFont() {
377
Font buttonFont = UIManager.getFont("Button.font");
378
return buttonFont != null
380
: new JButton().getFont();
384
* Creates and returns a component that is used to lookup the default
385
* font metrics. The current implementation creates a <code>JPanel</code>.
386
* Since this panel has no parent, it has no toolkit assigned. And so,
387
* requesting the font metrics will end up using the default toolkit
388
* and its deprecated method <code>ToolKit#getFontMetrics()</code>.<p>
390
* TODO: Consider publishing this method and providing a setter, so that
391
* an API user can set a realized component that has a toolkit assigned.
393
* @return a component used to compute the default font metrics
395
private Component createDefaultGlobalComponent() {
396
return new JPanel(null);
400
* Invalidates the caches. Resets the global dialog base units,
401
* clears the Map from <code>FontMetrics</code> to dialog base units,
402
* and resets the fallback for the default dialog font.
403
* This is invoked after a change of the look&feel.
405
private void invalidateCaches() {
406
cachedGlobalDialogBaseUnits = null;
407
cachedDialogBaseUnits.clear();
408
cachedDefaultDialogFont = null;
412
// Managing Property Change Listeners **********************************
415
* Adds a PropertyChangeListener to the listener list. The listener is
416
* registered for all bound properties of this class.<p>
418
* If listener is null, no exception is thrown and no action is performed.
420
* @param listener the PropertyChangeListener to be added
422
* @see #removePropertyChangeListener(PropertyChangeListener)
423
* @see #removePropertyChangeListener(String, PropertyChangeListener)
424
* @see #addPropertyChangeListener(String, PropertyChangeListener)
426
public synchronized void addPropertyChangeListener(
427
PropertyChangeListener listener) {
428
changeSupport.addPropertyChangeListener(listener);
433
* Removes a PropertyChangeListener from the listener list. This method
434
* should be used to remove PropertyChangeListeners that were registered
435
* for all bound properties of this class.<p>
437
* If listener is null, no exception is thrown and no action is performed.
439
* @param listener the PropertyChangeListener to be removed
441
* @see #addPropertyChangeListener(PropertyChangeListener)
442
* @see #addPropertyChangeListener(String, PropertyChangeListener)
443
* @see #removePropertyChangeListener(String, PropertyChangeListener)
445
public synchronized void removePropertyChangeListener(
446
PropertyChangeListener listener) {
447
changeSupport.removePropertyChangeListener(listener);
452
* Adds a PropertyChangeListener to the listener list for a specific
453
* property. The specified property may be user-defined.<p>
455
* Note that if this Model is inheriting a bound property, then no event
456
* will be fired in response to a change in the inherited property.<p>
458
* If listener is null, no exception is thrown and no action is performed.
460
* @param propertyName one of the property names listed above
461
* @param listener the PropertyChangeListener to be added
463
* @see #removePropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
464
* @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
466
public synchronized void addPropertyChangeListener(
468
PropertyChangeListener listener) {
469
changeSupport.addPropertyChangeListener(propertyName, listener);
474
* Removes a PropertyChangeListener from the listener list for a specific
475
* property. This method should be used to remove PropertyChangeListeners
476
* that were registered for a specific bound property.<p>
478
* If listener is null, no exception is thrown and no action is performed.
480
* @param propertyName a valid property name
481
* @param listener the PropertyChangeListener to be removed
483
* @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
484
* @see #removePropertyChangeListener(java.beans.PropertyChangeListener)
486
public synchronized void removePropertyChangeListener(
488
PropertyChangeListener listener) {
489
changeSupport.removePropertyChangeListener(propertyName, listener);
493
// Helper Code ************************************************************
496
* Describes horizontal and vertical dialog base units.
498
private static final class DialogBaseUnits {
503
DialogBaseUnits(double dialogBaseUnitsX, double dialogBaseUnitsY) {
504
this.x = dialogBaseUnitsX;
505
this.y = dialogBaseUnitsY;
508
public String toString() {
509
return "DBU(x=" + x + "; y=" + y + ")";
515
* Listens to changes of the Look and Feel and invalidates the cache.
517
private final class LookAndFeelChangeHandler implements PropertyChangeListener {
518
public void propertyChange(PropertyChangeEvent evt) {