001 /*
002 * $Id: UIManagerExt.java 2757 2008-02-26 04:31:50Z kschaefe $
003 *
004 * Copyright 2007 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
020 */
021 package org.jdesktop.swingx.plaf;
022
023 import java.awt.Color;
024 import java.awt.Dimension;
025 import java.awt.Font;
026 import java.awt.Insets;
027 import java.awt.Shape;
028 import java.util.Enumeration;
029 import java.util.HashMap;
030 import java.util.Locale;
031 import java.util.Map;
032 import java.util.MissingResourceException;
033 import java.util.ResourceBundle;
034 import java.util.Vector;
035
036 import javax.swing.Icon;
037 import javax.swing.UIDefaults;
038 import javax.swing.UIManager;
039 import javax.swing.border.Border;
040 import javax.swing.plaf.BorderUIResource;
041 import javax.swing.plaf.ColorUIResource;
042 import javax.swing.plaf.DimensionUIResource;
043 import javax.swing.plaf.FontUIResource;
044 import javax.swing.plaf.IconUIResource;
045 import javax.swing.plaf.InsetsUIResource;
046 import javax.swing.plaf.UIResource;
047
048 import org.jdesktop.swingx.painter.Painter;
049 import org.jdesktop.swingx.util.Contract;
050
051 /**
052 * A utility class for obtaining configuration properties from the
053 * {@code UIDefaults}. This class handles SwingX-specific L&F needs, such as
054 * the installation of painters and shapes. There are several categories of
055 * utility methods:
056 * <ul>
057 * <li>Support for the safe creation of {@code UIResource}s.</li>
058 * <li>Support for new {@code UIResource} types, such as
059 * {@code PainterUIResource}.</li>
060 * <li>Support for the dynamic localization of {@code UIDefaults}.</li>
061 * <li>Support for returning non-{@code String} localizations from
062 * {@code ResourceBundle}s.</li>
063 * </ul>
064 * <h3>Safe Methods</h3>
065 * <p>
066 * The {@code getSafeXXX} methods are designed for use with
067 * {@code LookAndFeelAddon}s. Any addon that attempts to obtain a property
068 * defined in the defaults (available from {@code UIManager.get}) to set a
069 * property that will be added to the defaults for the addon should use the
070 * "safe" methods. The methods ensure that a valid value is always returned and
071 * that value is a {@code UIResource}.
072 * </p>
073 * <h3>Support for New Types</h3>
074 * <p>
075 * {@code UIManagerExt} supports the retrieval of new {@code UIResource} types.
076 * There is a {@code getXXX} method for every {@code UIResource} subtype in the
077 * {@code org.jdesktop.swingx.plaf} package.
078 * </p>
079 * <h3>Support for Dynamic Localization</h3>
080 * <p>
081 * {@code UIManagerExt} enables dynamic localization by supporting
082 * {@code ResourceBundle}s. The
083 * {@linkplain UIDefaults#addResourceBundle(String)} allows resource bundles to
084 * be added to the {@code UIDefaults}. While there is support for this feature
085 * in core, there is a bug with the class loader that prevents user added
086 * bundles from working correctly when used via Web Start. Therefore,
087 * {@code UIManagerExt} defines methods to add and remove resource bundles.
088 * These are the only methods that SwingX classes should use when adding
089 * resource bundles to the defaults. Since {@code UIManagerExt} is maintaining
090 * the bundles, any localized {@code String}s <b>must</b> be retrieved from
091 * the {@code getString} methods in this class.
092 * </p>
093 * <h3>Support for Non-{@code String} Localization Values</h3>
094 * <p>
095 * All methods work by first determining if the value is present
096 * {@code UIDefaults}. If the value is not present, then the installed
097 * {@code ResourceBundle}s are queried. {@code UIManagerExt} will attempt to
098 * convert any returned value to the appropriate type. For instance,
099 * {@code getInt} uses {@code Integer.decode} to convert {@code String}s
100 * returned from the bundle into {@code int}s.
101 * </p>
102 *
103 * @author Karl George Schaefer
104 *
105 * @see UIManager
106 * @see UIDefaults
107 */
108 public class UIManagerExt {
109 /**
110 * Used to replicate the resource bundle behavior from the
111 * {@code UIDefaults}.
112 */
113 private static class UIDefaultsExt {
114 //use vector; we want synchronization
115 private Vector<String> resourceBundles;
116
117 /**
118 * Maps from a Locale to a cached Map of the ResourceBundle. This is done
119 * so as to avoid an exception being thrown when a value is asked for.
120 * Access to this should be done while holding a lock on the
121 * UIDefaults, eg synchronized(this).
122 */
123 private Map<Locale, Map<String, String>> resourceCache;
124
125 UIDefaultsExt() {
126 resourceCache = new HashMap<Locale, Map<String,String>>();
127 }
128
129 //should this just return String?
130 private Object getFromResourceBundle(Object key, Locale l) {
131
132 if( resourceBundles == null ||
133 resourceBundles.isEmpty() ||
134 !(key instanceof String) ) {
135 return null;
136 }
137
138 // A null locale means use the default locale.
139 if( l == null ) {
140 l = Locale.getDefault();
141 }
142
143 synchronized(this) {
144 return getResourceCache(l).get((String)key);
145 }
146 }
147
148 /**
149 * Returns a Map of the known resources for the given locale.
150 */
151 private Map<String, String> getResourceCache(Locale l) {
152 Map<String, String> values = (Map<String, String>) resourceCache.get(l);
153
154 if (values == null) {
155 values = new HashMap<String, String>();
156 for (int i=resourceBundles.size()-1; i >= 0; i--) {
157 String bundleName = (String)resourceBundles.get(i);
158
159 try {
160 ResourceBundle b = ResourceBundle.
161 getBundle(bundleName, l, UIManagerExt.class.getClassLoader());
162 Enumeration<String> keys = b.getKeys();
163
164 while (keys.hasMoreElements()) {
165 String key = (String)keys.nextElement();
166
167 if (values.get(key) == null) {
168 Object value = b.getObject(key);
169
170 values.put(key, (String) value);
171 }
172 }
173 } catch( MissingResourceException mre ) {
174 // Keep looking
175 }
176 }
177 resourceCache.put(l, values);
178 }
179 return values;
180 }
181
182 public synchronized void addResourceBundle(String bundleName) {
183 if( bundleName == null ) {
184 return;
185 }
186 if( resourceBundles == null ) {
187 resourceBundles = new Vector<String>(5);
188 }
189 if (!resourceBundles.contains(bundleName)) {
190 resourceBundles.add( bundleName );
191 resourceCache.clear();
192 }
193 }
194
195 public synchronized void removeResourceBundle( String bundleName ) {
196 if( resourceBundles != null ) {
197 resourceBundles.remove( bundleName );
198 }
199 resourceCache.clear();
200 }
201 }
202
203 private static UIDefaultsExt uiDefaultsExt = new UIDefaultsExt();
204
205 private UIManagerExt() {
206 //does nothing
207 }
208
209 /**
210 * Adds a resource bundle to the list of resource bundles that are searched
211 * for localized values. Resource bundles are searched in the reverse order
212 * they were added. In other words, the most recently added bundle is
213 * searched first.
214 *
215 * @param bundleName
216 * the base name of the resource bundle to be added
217 * @see java.util.ResourceBundle
218 * @see #removeResourceBundle
219 */
220 public static void addResourceBundle(String bundleName) {
221 uiDefaultsExt.addResourceBundle(bundleName);
222 }
223
224 /**
225 * Removes a resource bundle from the list of resource bundles that are
226 * searched for localized defaults.
227 *
228 * @param bundleName
229 * the base name of the resource bundle to be removed
230 * @see java.util.ResourceBundle
231 * @see #addResourceBundle
232 */
233 public static void removeResourceBundle(String bundleName) {
234 uiDefaultsExt.removeResourceBundle(bundleName);
235 }
236
237 /**
238 * Returns a string from the defaults. If the value for {@code key} is not a
239 * {@code String}, {@code null} is returned.
240 *
241 * @param key
242 * an {@code Object} specifying the string
243 * @return the {@code String} object
244 * @throws NullPointerException
245 * if {@code key} is {@code null}
246 */
247 public static String getString(Object key) {
248 return getString(key, null);
249 }
250
251 /**
252 * Returns a string from the defaults. If the value for {@code key} is not a
253 * {@code String}, {@code null} is returned.
254 *
255 * @param key
256 * an {@code Object} specifying the string
257 * @param l
258 * the {@code Locale} for which the painter is desired; refer
259 * to {@code UIDefaults} for details on how a {@code null}
260 * {@code Locale} is handled
261 * @return the {@code String} object
262 * @throws NullPointerException
263 * if {@code key} is {@code null}
264 */
265 public static String getString(Object key, Locale l) {
266 Object value = UIManager.get(key, l);
267
268 if (value instanceof String) {
269 return (String) value;
270 }
271
272 //only return resource bundle if not in UIDefaults
273 if (value == null) {
274 value = uiDefaultsExt.getFromResourceBundle(key, l);
275
276 if (value instanceof String) {
277 return (String) value;
278 }
279 }
280
281 return null;
282 }
283
284 /**
285 * Returns an integer from the defaults. If the value for {@code key} is not
286 * an {@code int}, {@code 0} is returned.
287 *
288 * @param key
289 * an {@code Object} specifying the integer
290 * @return the {@code int}
291 * @throws NullPointerException
292 * if {@code key} is {@code null}
293 */
294 public static int getInt(Object key) {
295 return getInt(key, null);
296 }
297
298 /**
299 * Returns an integer from the defaults. If the value for {@code key} is not
300 * an {@code int}, {@code 0} is returned.
301 *
302 * @param key
303 * an {@code Object} specifying the integer
304 * @param l
305 * the {@code Locale} for which the integer is desired; refer
306 * to {@code UIDefaults} for details on how a {@code null}
307 * {@code Locale} is handled
308 * @return the {@code int}
309 * @throws NullPointerException
310 * if {@code key} is {@code null}
311 */
312 public static int getInt(Object key, Locale l) {
313 Object value = UIManager.get(key, l);
314
315 if (value instanceof Integer) {
316 return (Integer) value;
317 }
318
319 if (value == null) {
320 value = uiDefaultsExt.getFromResourceBundle(key, l);
321
322 if (value instanceof Integer) {
323 return (Integer) value;
324 }
325
326 if (value instanceof String) {
327 try {
328 return Integer.decode((String) value);
329 } catch (NumberFormatException e) {
330 // ignore - the entry was not parseable, can't do anything
331 // JW: should we log it?
332 }
333 }
334 }
335
336 return 0;
337 }
338
339 /**
340 * Returns an Boolean from the defaults. If the value for {@code key} is not
341 * a {@code boolean}, {@code false} is returned.
342 *
343 * @param key
344 * an {@code Object} specifying the Boolean
345 * @return the {@code boolean}
346 * @throws NullPointerException
347 * if {@code key} is {@code null}
348 */
349 public static boolean getBoolean(Object key) {
350 return getBoolean(key, null);
351 }
352
353 /**
354 * Returns an Boolean from the defaults. If the value for {@code key} is not
355 * a {@code boolean}, {@code false} is returned.
356 *
357 * @param key
358 * an {@code Object} specifying the Boolean
359 * @param l
360 * the {@code Locale} for which the Boolean is desired; refer
361 * to {@code UIDefaults} for details on how a {@code null}
362 * {@code Locale} is handled
363 * @return the {@code boolean}
364 * @throws NullPointerException
365 * if {@code key} is {@code null}
366 */
367 public static boolean getBoolean(Object key, Locale l) {
368 Object value = UIManager.get(key, l);
369
370 if (value instanceof Boolean) {
371 return (Boolean) value;
372 }
373
374 //only return resource bundle if not in UIDefaults
375 if (value == null) {
376 value = uiDefaultsExt.getFromResourceBundle(key, l);
377
378 if (value instanceof Boolean) {
379 return (Boolean) value;
380 }
381
382 if (value instanceof String) {
383 return Boolean.valueOf((String) value);
384 }
385 }
386
387 return false;
388 }
389
390 /**
391 * Returns a color from the defaults. If the value for {@code key} is not
392 * a {@code Color}, {@code null} is returned.
393 *
394 * @param key
395 * an {@code Object} specifying the color
396 * @return the {@code Color} object
397 * @throws NullPointerException
398 * if {@code key} is {@code null}
399 */
400 public static Color getColor(Object key) {
401 return getColor(key, null);
402 }
403
404 /**
405 * Returns a color from the defaults. If the value for {@code key} is not
406 * a {@code Color}, {@code null} is returned.
407 *
408 * @param key
409 * an {@code Object} specifying the color
410 * @param l
411 * the {@code Locale} for which the color is desired; refer
412 * to {@code UIDefaults} for details on how a {@code null}
413 * {@code Locale} is handled
414 * @return the {@code Color} object
415 * @throws NullPointerException
416 * if {@code key} is {@code null}
417 */
418 public static Color getColor(Object key, Locale l) {
419 Object value = UIManager.get(key, l);
420
421 if (value instanceof Color) {
422 return (Color) value;
423 }
424
425 //only return resource bundle if not in UIDefaults
426 if (value == null) {
427 value = uiDefaultsExt.getFromResourceBundle(key, l);
428
429 if (value instanceof Color) {
430 return (Color) value;
431 }
432
433 if (value instanceof String) {
434 try {
435 return Color.decode((String) value);
436 } catch (NumberFormatException e) {
437 // incorrect format; does nothing
438 }
439 }
440 }
441
442 return null;
443 }
444
445 //TODO: Font.decode always returns a valid font. This is not acceptable for UIManager
446 // /**
447 // * Returns a font from the defaults. If the value for {@code key} is not
448 // * a {@code Font}, {@code null} is returned.
449 // *
450 // * @param key
451 // * an {@code Object} specifying the font
452 // * @return the {@code Font} object
453 // * @throws NullPointerException
454 // * if {@code key} is {@code null}
455 // */
456 // public static Font getFont(Object key) {
457 // return getFont(key, null);
458 // }
459 //
460 // /**
461 // * Returns a font from the defaults. If the value for {@code key} is not
462 // * a {@code Font}, {@code null} is returned.
463 // *
464 // * @param key
465 // * an {@code Object} specifying the font
466 // * @param l
467 // * the {@code Locale} for which the font is desired; refer
468 // * to {@code UIDefaults} for details on how a {@code null}
469 // * {@code Locale} is handled
470 // * @return the {@code Font} object
471 // * @throws NullPointerException
472 // * if {@code key} is {@code null}
473 // */
474 // public static Font getFont(Object key, Locale l) {
475 // Object value = UIManager.get(key, l);
476 //
477 // if (value instanceof Font) {
478 // return (Font) value;
479 // }
480 //
481 // //only return resource bundle if not in UIDefaults
482 // if (value == null) {
483 // value = uiDefaultsExt.getFromResourceBundle(key, l);
484 //
485 // if (value instanceof Font) {
486 // return (Font) value;
487 // }
488 //
489 // if (value instanceof String) {
490 // return Font.decode((String) value);
491 // }
492 // }
493 //
494 // return null;
495 // }
496
497 /**
498 * Returns a shape from the defaults. If the value for {@code key} is not a
499 * {@code Shape}, {@code null} is returned.
500 *
501 * @param key an {@code Object} specifying the shape
502 * @return the {@code Shape} object
503 * @throws NullPointerException if {@code key} is {@code null}
504 */
505 public static Shape getShape(Object key) {
506 Object value = UIManager.getDefaults().get(key);
507 return (value instanceof Shape) ? (Shape) value : null;
508 }
509
510 /**
511 * Returns a shape from the defaults that is appropriate for the given
512 * locale. If the value for {@code key} is not a {@code Shape},
513 * {@code null} is returned.
514 *
515 * @param key
516 * an {@code Object} specifying the shape
517 * @param l
518 * the {@code Locale} for which the shape is desired; refer
519 * to {@code UIDefaults} for details on how a {@code null}
520 * {@code Locale} is handled
521 * @return the {@code Shape} object
522 * @throws NullPointerException
523 * if {@code key} is {@code null}
524 */
525 public static Shape getShape(Object key, Locale l) {
526 Object value = UIManager.getDefaults().get(key, l);
527 return (value instanceof Shape) ? (Shape) value : null;
528 }
529
530 /**
531 * Returns a painter from the defaults. If the value for {@code key} is not
532 * a {@code Painter}, {@code null} is returned.
533 *
534 * @param key
535 * an {@code Object} specifying the painter
536 * @return the {@code Painter} object
537 * @throws NullPointerException
538 * if {@code key} is {@code null}
539 */
540 public static Painter<?> getPainter(Object key) {
541 Object value = UIManager.getDefaults().get(key);
542 return (value instanceof Painter) ? (Painter<?>) value : null;
543 }
544
545 /**
546 * Returns a painter from the defaults that is appropriate for the given
547 * locale. If the value for {@code key} is not a {@code Painter},
548 * {@code null} is returned.
549 *
550 * @param key
551 * an {@code Object} specifying the painter
552 * @param l
553 * the {@code Locale} for which the painter is desired; refer
554 * to {@code UIDefaults} for details on how a {@code null}
555 * {@code Locale} is handled
556 * @return the {@code Painter} object
557 * @throws NullPointerException
558 * if {@code key} is {@code null}
559 */
560 public static Painter<?> getPainter(Object key, Locale l) {
561 Object value = UIManager.getDefaults().get(key, l);
562 return (value instanceof Painter) ? (Painter<?>) value : null;
563 }
564
565 /**
566 * Returns a border from the defaults. If the value for {@code key} is not a
567 * {@code Border}, {@code defaultBorder} is returned.
568 *
569 * @param key
570 * an {@code Object} specifying the border
571 * @param defaultBorder
572 * the border to return if the border specified by
573 * {@code key} does not exist
574 * @return the {@code Border} object
575 * @throws NullPointerException
576 * if {@code key} or {@code defaultBorder} is {@code null}
577 */
578 public static Border getSafeBorder(Object key, Border defaultBorder) {
579 Contract.asNotNull(defaultBorder, "defaultBorder cannot be null");
580
581 Border safeBorder = UIManager.getBorder(key);
582
583 if (safeBorder == null) {
584 safeBorder = defaultBorder;
585 }
586
587 if (!(safeBorder instanceof UIResource)) {
588 safeBorder = new BorderUIResource(safeBorder);
589 }
590
591 return safeBorder;
592 }
593
594 /**
595 * Returns a color from the defaults. If the value for {@code key} is not a
596 * {@code Color}, {@code defaultColor} is returned.
597 *
598 * @param key
599 * an {@code Object} specifying the color
600 * @param defaultColor
601 * the color to return if the color specified by {@code key}
602 * does not exist
603 * @return the {@code Color} object
604 * @throws NullPointerException
605 * if {@code key} or {@code defaultColor} is {@code null}
606 */
607 public static Color getSafeColor(Object key, Color defaultColor) {
608 Contract.asNotNull(defaultColor, "defaultColor cannot be null");
609
610 Color safeColor = UIManager.getColor(key);
611
612 if (safeColor == null) {
613 safeColor = defaultColor;
614 }
615
616 if (!(safeColor instanceof UIResource)) {
617 safeColor = new ColorUIResource(safeColor);
618 }
619
620 return safeColor;
621 }
622
623 /**
624 * Returns a dimension from the defaults. If the value for {@code key} is
625 * not a {@code Dimension}, {@code defaultDimension} is returned.
626 *
627 * @param key
628 * an {@code Object} specifying the dimension
629 * @param defaultDimension
630 * the dimension to return if the dimension specified by
631 * {@code key} does not exist
632 * @return the {@code Dimension} object
633 * @throws NullPointerException
634 * if {@code key} or {@code defaultColor} is {@code null}
635 */
636 public static Dimension getSafeDimension(Object key, Dimension defaultDimension) {
637 Contract.asNotNull(defaultDimension, "defaultDimension cannot be null");
638
639 Dimension safeDimension = UIManager.getDimension(key);
640
641 if (safeDimension == null) {
642 safeDimension = defaultDimension;
643 }
644
645 if (!(safeDimension instanceof UIResource)) {
646 safeDimension = new DimensionUIResource(safeDimension.width, safeDimension.height);
647 }
648
649 return safeDimension;
650 }
651
652 /**
653 * Returns a font from the defaults. If the value for {@code key} is not a
654 * {@code Font}, {@code defaultFont} is returned.
655 *
656 * @param key
657 * an {@code Object} specifying the font
658 * @param defaultFont
659 * the font to return if the font specified by {@code key}
660 * does not exist
661 * @return the {@code Font} object
662 * @throws NullPointerException
663 * if {@code key} or {@code defaultFont} is {@code null}
664 */
665 public static Font getSafeFont(Object key, Font defaultFont) {
666 Contract.asNotNull(defaultFont, "defaultFont cannot be null");
667
668 Font safeFont = UIManager.getFont(key);
669
670 if (safeFont == null) {
671 safeFont = defaultFont;
672 }
673
674 if (!(safeFont instanceof UIResource)) {
675 safeFont = new FontUIResource(safeFont);
676 }
677
678 return safeFont;
679 }
680
681 /**
682 * Returns an icon from the defaults. If the value for {@code key} is not a
683 * {@code Icon}, {@code defaultIcon} is returned.
684 *
685 * @param key
686 * an {@code Object} specifying the icon
687 * @param defaultIcon
688 * the icon to return if the icon specified by {@code key}
689 * does not exist
690 * @return the {@code Icon} object
691 * @throws NullPointerException
692 * if {@code key} or {@code defaultIcon} is {@code null}
693 */
694 public static Icon getSafeIcon(Object key, Icon defaultIcon) {
695 Contract.asNotNull(defaultIcon, "defaultIcon cannot be null");
696
697 Icon safeIcon = UIManager.getIcon(key);
698
699 if (safeIcon == null) {
700 safeIcon = defaultIcon;
701 }
702
703 if (!(safeIcon instanceof UIResource)) {
704 safeIcon = new IconUIResource(safeIcon);
705 }
706
707 return safeIcon;
708 }
709
710 /**
711 * Returns an insets from the defaults. If the value for {@code key} is not
712 * a {@code Insets}, {@code defaultInsets} is returned.
713 *
714 * @param key
715 * an {@code Object} specifying the insets
716 * @param defaultInsets
717 * the insets to return if the insets specified by
718 * {@code key} does not exist
719 * @return the {@code Insets} object
720 * @throws NullPointerException
721 * if {@code key} or {@code defaultInsets} is {@code null}
722 */
723 public static Insets getSafeInsets(Object key, Insets defaultInsets) {
724 Contract.asNotNull(defaultInsets, "defaultInsets cannot be null");
725
726 Insets safeInsets = UIManager.getInsets(key);
727
728 if (safeInsets == null) {
729 safeInsets = defaultInsets;
730 }
731
732 if (!(safeInsets instanceof UIResource)) {
733 safeInsets = new InsetsUIResource(safeInsets.top, safeInsets.left,
734 safeInsets.bottom, safeInsets.right);
735 }
736
737 return safeInsets;
738 }
739 }