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