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 "X". 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&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 "X". 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 "X". You can provide 167 * other test strings, for example: 168 * <ul> 169 * <li>"Xximeee"</li> 170 * <li>"ABCEDEFHIJKLMNOPQRSTUVWXYZ"</li> 171 * <li>"abcdefghijklmnopqrstuvwxyz"</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&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&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&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}