~ubuntu-branches/ubuntu/trusty/libjgoodies-forms-java/trusty

« back to all changes in this revision

Viewing changes to src/core/com/jgoodies/forms/util/DefaultUnitConverter.java

  • Committer: Bazaar Package Importer
  • Author(s): Varun Hiremath
  • Date: 2008-02-25 10:57:07 UTC
  • mfrom: (1.2.1 upstream) (2.1.2 hardy)
  • Revision ID: james.westby@ubuntu.com-20080225105707-pe51fdbcq1dt3vi6
Tags: 1.2.0-1
New upstream release

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
/*
 
2
 * Copyright (c) 2002-2008 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;
 
34
import java.awt.Font;
 
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;
 
40
import java.util.Map;
 
41
import java.util.logging.Logger;
 
42
 
 
43
import javax.swing.JButton;
 
44
import javax.swing.JPanel;
 
45
import javax.swing.UIManager;
 
46
 
 
47
/**
 
48
 * This is the default implementation of the {@link UnitConverter} interface.
 
49
 * It converts horizontal and vertical dialog base units to pixels.<p>
 
50
 *
 
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>
 
56
 *
 
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
 
64
 * in Microsoft's
 
65
 * <a href="http://msdn2.microsoft.com/en-us/library/ms997619">Design
 
66
 * Specifications and Guidelines</a>.<p>
 
67
 *
 
68
 * Since the Forms 1.1 this converter logs font information at
 
69
 * the <code>CONFIG</code> level.
 
70
 *
 
71
 * @version $Revision: 1.13 $
 
72
 * @author  Karsten Lentzsch
 
73
 * @see     UnitConverter
 
74
 * @see     com.jgoodies.forms.layout.Size
 
75
 * @see     com.jgoodies.forms.layout.Sizes
 
76
 */
 
77
public final class DefaultUnitConverter extends AbstractUnitConverter {
 
78
 
 
79
//    public static final String UPPERCASE_ALPHABET =
 
80
//        "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
 
81
//
 
82
//    public static final String LOWERCASE_ALPHABET =
 
83
//        "abcdefghijklmnopqrstuvwxyz";
 
84
 
 
85
    private static final Logger LOGGER =
 
86
        Logger.getLogger(DefaultUnitConverter.class.getName());
 
87
 
 
88
 
 
89
    /**
 
90
     * Holds the sole instance that will be lazily instantiated.
 
91
     */
 
92
    private static DefaultUnitConverter instance;
 
93
 
 
94
 
 
95
    /**
 
96
     * Holds the string that is used to compute the average character width.
 
97
     * By default this is just &quot;X&quot;.
 
98
     */
 
99
    private String averageCharWidthTestString = "X";
 
100
 
 
101
 
 
102
    /**
 
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.
 
107
     */
 
108
    private Font defaultDialogFont;
 
109
 
 
110
 
 
111
    /**
 
112
     * If any <code>PropertyChangeListeners</code> have been registered,
 
113
     * the <code>changeSupport</code> field describes them.
 
114
     *
 
115
     * @serial
 
116
     * @see #addPropertyChangeListener(PropertyChangeListener)
 
117
     * @see #addPropertyChangeListener(String, PropertyChangeListener)
 
118
     * @see #removePropertyChangeListener(PropertyChangeListener)
 
119
     * @see #removePropertyChangeListener(String, PropertyChangeListener)
 
120
     */
 
121
    private final PropertyChangeSupport changeSupport;
 
122
 
 
123
 
 
124
    // Cached *****************************************************************
 
125
 
 
126
    /**
 
127
     * Holds the cached global dialog base units that are used if
 
128
     * a component is not (yet) available - for example in a Border.
 
129
     */
 
130
    private DialogBaseUnits cachedGlobalDialogBaseUnits =
 
131
        computeGlobalDialogBaseUnits();
 
132
 
 
133
    /**
 
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.
 
137
     */
 
138
    private final Map cachedDialogBaseUnits = new HashMap();
 
139
 
 
140
    /**
 
141
     * Holds a cached default dialog font that is used as fallback,
 
142
     * if no default dialog font has been set.
 
143
     *
 
144
     * @see #getDefaultDialogFont()
 
145
     * @see #setDefaultDialogFont(Font)
 
146
     */
 
147
    private Font cachedDefaultDialogFont = null;
 
148
 
 
149
 
 
150
    // Instance Creation and Access *******************************************
 
151
 
 
152
    /**
 
153
     * Constructs a DefaultUnitConverter and registers
 
154
     * a listener that handles changes in the look&amp;feel.
 
155
     */
 
156
    private DefaultUnitConverter() {
 
157
        UIManager.addPropertyChangeListener(new LookAndFeelChangeHandler());
 
158
        changeSupport = new PropertyChangeSupport(this);
 
159
    }
 
160
 
 
161
 
 
162
    /**
 
163
     * Lazily instantiates and returns the sole instance.
 
164
     *
 
165
     * @return the lazily instantiated sole instance
 
166
     */
 
167
    public static DefaultUnitConverter getInstance() {
 
168
        if (instance == null) {
 
169
            instance = new DefaultUnitConverter();
 
170
        }
 
171
        return instance;
 
172
    }
 
173
 
 
174
 
 
175
    // Access to Bound Properties *********************************************
 
176
 
 
177
    /**
 
178
     * Returns the string used to compute the average character width.
 
179
     * By default it is initialized to &quot;X&quot;.
 
180
     *
 
181
     * @return the test string used to compute the average character width
 
182
     */
 
183
    public String getAverageCharacterWidthTestString() {
 
184
        return averageCharWidthTestString;
 
185
    }
 
186
 
 
187
    /**
 
188
     * Sets a string that will be used to compute the average character width.
 
189
     * By default it is initialized to &quot;X&quot;. You can provide
 
190
     * other test strings, for example:
 
191
     * <ul>
 
192
     *  <li>&quot;Xximeee&quot;</li>
 
193
     *  <li>&quot;ABCEDEFHIJKLMNOPQRSTUVWXYZ&quot;</li>
 
194
     *  <li>&quot;abcdefghijklmnopqrstuvwxyz&quot;</li>
 
195
     * </ul>
 
196
     *
 
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>
 
200
     */
 
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.");
 
206
 
 
207
        String oldTestString = averageCharWidthTestString;
 
208
        averageCharWidthTestString = newTestString;
 
209
        changeSupport.firePropertyChange("averageCharacterWidthTestString",
 
210
            oldTestString, newTestString);
 
211
    }
 
212
 
 
213
 
 
214
    /**
 
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.
 
219
     *
 
220
     * @return the font used to compute the dialog base units
 
221
     */
 
222
    public Font getDefaultDialogFont() {
 
223
        return defaultDialogFont != null
 
224
            ? defaultDialogFont
 
225
            : getCachedDefaultDialogFont();
 
226
    }
 
227
 
 
228
    /**
 
229
     * Sets a dialog font that will be used to compute the dialog base units.
 
230
     *
 
231
     * @param newFont   the default dialog font to be set
 
232
     */
 
233
    public void setDefaultDialogFont(Font newFont) {
 
234
        Font oldFont = defaultDialogFont; // Don't use the getter
 
235
        defaultDialogFont = newFont;
 
236
        invalidateCaches();
 
237
        changeSupport.firePropertyChange("defaultDialogFont", oldFont, newFont);
 
238
    }
 
239
 
 
240
 
 
241
    // Implementing Abstract Superclass Behavior ******************************
 
242
 
 
243
    /**
 
244
     * Returns the cached or computed horizontal dialog base units.
 
245
     *
 
246
     * @param component     a Component that provides the font and graphics
 
247
     * @return the horizontal dialog base units
 
248
     */
 
249
    protected double getDialogBaseUnitsX(Component component) {
 
250
        return getDialogBaseUnits(component).x;
 
251
    }
 
252
 
 
253
    /**
 
254
     * Returns the cached or computed vertical dialog base units
 
255
     * for the given component.
 
256
     *
 
257
     * @param component     a Component that provides the font and graphics
 
258
     * @return the vertical dialog base units
 
259
     */
 
260
    protected double getDialogBaseUnitsY(Component component) {
 
261
        return getDialogBaseUnits(component).y;
 
262
    }
 
263
 
 
264
 
 
265
    // Compute and Cache Global and Components Dialog Base Units **************
 
266
 
 
267
    /**
 
268
     * Lazily computes and answer the global dialog base units.
 
269
     * Should be re-computed if the l&amp;f, platform, or screen changes.
 
270
     *
 
271
     * @return a cached DialogBaseUnits object used globally if no container is available
 
272
     */
 
273
    private DialogBaseUnits getGlobalDialogBaseUnits() {
 
274
        if (cachedGlobalDialogBaseUnits == null) {
 
275
            cachedGlobalDialogBaseUnits = computeGlobalDialogBaseUnits();
 
276
        }
 
277
        return cachedGlobalDialogBaseUnits;
 
278
    }
 
279
 
 
280
    /**
 
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
 
283
     * are answered.<p>
 
284
     *
 
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>.
 
288
     *
 
289
     * @param c  the component that provides the graphics object
 
290
     * @return the DialogBaseUnits object for the given component
 
291
     */
 
292
    private DialogBaseUnits getDialogBaseUnits(Component c) {
 
293
        if (c == null) { // || (font = c.getFont()) == null) {
 
294
            // logInfo("Missing font metrics: " + c);
 
295
            return getGlobalDialogBaseUnits();
 
296
        }
 
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);
 
302
        }
 
303
        return dialogBaseUnits;
 
304
    }
 
305
 
 
306
    /**
 
307
     * Computes and returns the horizontal dialog base units.
 
308
     * Honors the font, font size and resolution.<p>
 
309
     *
 
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>
 
314
     *
 
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.
 
318
     *
 
319
     * @param metrics  the FontMetrics used to measure the dialog font
 
320
     * @return the horizontal and vertical dialog base units
 
321
     */
 
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);
 
329
        LOGGER.config(
 
330
            "Computed dialog base units "
 
331
                + dialogBaseUnits
 
332
                + " for: "
 
333
                + metrics.getFont());
 
334
        return dialogBaseUnits;
 
335
    }
 
336
 
 
337
    /**
 
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>
 
342
     *
 
343
     * Should be re-computed if the l&amp;f, platform, or screen changes.
 
344
     *
 
345
     * @return a DialogBaseUnits object used globally if no container is available
 
346
     */
 
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;
 
353
    }
 
354
 
 
355
    /**
 
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&amp;F changes.
 
359
     *
 
360
     * @return the cached fallback font used to compute the dialog base units
 
361
     */
 
362
    private Font getCachedDefaultDialogFont() {
 
363
        if (cachedDefaultDialogFont == null) {
 
364
            cachedDefaultDialogFont = lookupDefaultDialogFont();
 
365
        }
 
366
        return cachedDefaultDialogFont;
 
367
    }
 
368
 
 
369
    /**
 
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.
 
373
     *
 
374
     * @return the font used for a standard button
 
375
     */
 
376
    private Font lookupDefaultDialogFont() {
 
377
        Font buttonFont = UIManager.getFont("Button.font");
 
378
        return buttonFont != null
 
379
            ? buttonFont
 
380
            : new JButton().getFont();
 
381
    }
 
382
 
 
383
    /**
 
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>
 
389
     *
 
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.
 
392
     *
 
393
     * @return a component used to compute the default font metrics
 
394
     */
 
395
    private Component createDefaultGlobalComponent() {
 
396
        return new JPanel(null);
 
397
    }
 
398
 
 
399
    /**
 
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&amp;feel.
 
404
     */
 
405
    private void invalidateCaches() {
 
406
        cachedGlobalDialogBaseUnits = null;
 
407
        cachedDialogBaseUnits.clear();
 
408
        cachedDefaultDialogFont = null;
 
409
    }
 
410
 
 
411
 
 
412
     // Managing Property Change Listeners **********************************
 
413
 
 
414
    /**
 
415
     * Adds a PropertyChangeListener to the listener list. The listener is
 
416
     * registered for all bound properties of this class.<p>
 
417
     *
 
418
     * If listener is null, no exception is thrown and no action is performed.
 
419
     *
 
420
     * @param listener      the PropertyChangeListener to be added
 
421
     *
 
422
     * @see #removePropertyChangeListener(PropertyChangeListener)
 
423
     * @see #removePropertyChangeListener(String, PropertyChangeListener)
 
424
     * @see #addPropertyChangeListener(String, PropertyChangeListener)
 
425
     */
 
426
    public synchronized void addPropertyChangeListener(
 
427
                                            PropertyChangeListener listener) {
 
428
        changeSupport.addPropertyChangeListener(listener);
 
429
    }
 
430
 
 
431
 
 
432
    /**
 
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>
 
436
     *
 
437
     * If listener is null, no exception is thrown and no action is performed.
 
438
     *
 
439
     * @param listener      the PropertyChangeListener to be removed
 
440
     *
 
441
     * @see #addPropertyChangeListener(PropertyChangeListener)
 
442
     * @see #addPropertyChangeListener(String, PropertyChangeListener)
 
443
     * @see #removePropertyChangeListener(String, PropertyChangeListener)
 
444
     */
 
445
    public synchronized void removePropertyChangeListener(
 
446
                                        PropertyChangeListener listener) {
 
447
        changeSupport.removePropertyChangeListener(listener);
 
448
    }
 
449
 
 
450
 
 
451
    /**
 
452
     * Adds a PropertyChangeListener to the listener list for a specific
 
453
     * property. The specified property may be user-defined.<p>
 
454
     *
 
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>
 
457
     *
 
458
     * If listener is null, no exception is thrown and no action is performed.
 
459
     *
 
460
     * @param propertyName      one of the property names listed above
 
461
     * @param listener          the PropertyChangeListener to be added
 
462
     *
 
463
     * @see #removePropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
 
464
     * @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
 
465
     */
 
466
    public synchronized void addPropertyChangeListener(
 
467
                                        String propertyName,
 
468
                                        PropertyChangeListener listener) {
 
469
        changeSupport.addPropertyChangeListener(propertyName, listener);
 
470
    }
 
471
 
 
472
 
 
473
    /**
 
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>
 
477
     *
 
478
     * If listener is null, no exception is thrown and no action is performed.
 
479
     *
 
480
     * @param propertyName      a valid property name
 
481
     * @param listener          the PropertyChangeListener to be removed
 
482
     *
 
483
     * @see #addPropertyChangeListener(java.lang.String, java.beans.PropertyChangeListener)
 
484
     * @see #removePropertyChangeListener(java.beans.PropertyChangeListener)
 
485
     */
 
486
    public synchronized void removePropertyChangeListener(
 
487
                                        String propertyName,
 
488
                                        PropertyChangeListener listener) {
 
489
        changeSupport.removePropertyChangeListener(propertyName, listener);
 
490
    }
 
491
 
 
492
 
 
493
    // Helper Code ************************************************************
 
494
 
 
495
    /**
 
496
     * Describes horizontal and vertical dialog base units.
 
497
     */
 
498
    private static final class DialogBaseUnits {
 
499
 
 
500
        final double x;
 
501
        final double y;
 
502
 
 
503
        DialogBaseUnits(double dialogBaseUnitsX, double dialogBaseUnitsY) {
 
504
            this.x = dialogBaseUnitsX;
 
505
            this.y = dialogBaseUnitsY;
 
506
        }
 
507
 
 
508
        public String toString() {
 
509
            return "DBU(x=" + x + "; y=" + y + ")";
 
510
        }
 
511
    }
 
512
 
 
513
 
 
514
    /**
 
515
     * Listens to changes of the Look and Feel and invalidates the cache.
 
516
     */
 
517
    private final class LookAndFeelChangeHandler implements PropertyChangeListener {
 
518
        public void propertyChange(PropertyChangeEvent evt) {
 
519
            invalidateCaches();
 
520
        }
 
521
    }
 
522
 
 
523
}