001/*
002 * $Id: UIManagerExt.java 4028 2011-06-03 19:32:19Z 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 */
021package org.jdesktop.swingx.plaf;
022
023import java.awt.Color;
024import java.awt.Dimension;
025import java.awt.Font;
026import java.awt.Insets;
027import java.awt.Shape;
028import java.util.Enumeration;
029import java.util.HashMap;
030import java.util.Locale;
031import java.util.Map;
032import java.util.MissingResourceException;
033import java.util.ResourceBundle;
034import java.util.Vector;
035
036import javax.swing.Icon;
037import javax.swing.UIDefaults;
038import javax.swing.UIManager;
039import javax.swing.border.Border;
040import javax.swing.plaf.BorderUIResource;
041import javax.swing.plaf.ColorUIResource;
042import javax.swing.plaf.DimensionUIResource;
043import javax.swing.plaf.FontUIResource;
044import javax.swing.plaf.IconUIResource;
045import javax.swing.plaf.InsetsUIResource;
046import javax.swing.plaf.UIResource;
047
048import org.jdesktop.swingx.painter.Painter;
049import 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@SuppressWarnings("nls")
109public class UIManagerExt {
110    /**
111     * Used to replicate the resource bundle behavior from the
112     * {@code UIDefaults}.
113     */
114    private static class UIDefaultsExt {
115        //use vector; we want synchronization
116        private Vector<String> resourceBundles;
117
118        /**
119         * Maps from a Locale to a cached Map of the ResourceBundle. This is done
120         * so as to avoid an exception being thrown when a value is asked for.
121         * Access to this should be done while holding a lock on the
122         * UIDefaults, eg synchronized(this).
123         */
124        private Map<Locale, Map<String, String>> resourceCache;
125        
126        UIDefaultsExt() {
127            resourceCache = new HashMap<Locale, Map<String,String>>();
128        }
129        
130        //should this just return String?
131        private Object getFromResourceBundle(Object key, Locale l) {
132
133            if( resourceBundles == null ||
134                resourceBundles.isEmpty() ||
135                !(key instanceof String) ) {
136                return null;
137            }
138
139            // A null locale means use the default locale.
140            if( l == null ) {
141                    l = Locale.getDefault();
142            }
143
144            synchronized(this) {
145                return getResourceCache(l).get(key);
146            }
147        }
148
149        /**
150         * Returns a Map of the known resources for the given locale.
151         */
152        private Map<String, String> getResourceCache(Locale l) {
153            Map<String, String> values = resourceCache.get(l);
154
155            if (values == null) {
156                values = new HashMap<String, String>();
157                for (int i=resourceBundles.size()-1; i >= 0; i--) {
158                    String bundleName = resourceBundles.get(i);
159                    
160                    try {
161                        ResourceBundle b = ResourceBundle.
162                            getBundle(bundleName, l, UIManagerExt.class.getClassLoader());
163                        Enumeration<String> keys = b.getKeys();
164
165                        while (keys.hasMoreElements()) {
166                            String key = keys.nextElement();
167
168                            if (values.get(key) == null) {
169                                Object value = b.getObject(key);
170
171                                values.put(key, (String) value);
172                            }
173                        }
174                    } catch( MissingResourceException mre ) {
175                        // Keep looking
176                    }
177                }
178                resourceCache.put(l, values);
179            }
180            return values;
181        }
182
183        public synchronized void addResourceBundle(String bundleName) {
184            if( bundleName == null ) {
185                return;
186            }
187            if( resourceBundles == null ) {
188                resourceBundles = new Vector<String>(5);
189            }
190            if (!resourceBundles.contains(bundleName)) {
191                resourceBundles.add( bundleName );
192                resourceCache.clear();
193            }
194        }
195        
196        public synchronized void removeResourceBundle( String bundleName ) {
197            if( resourceBundles != null ) {
198                resourceBundles.remove( bundleName );
199            }
200            resourceCache.clear();
201        }
202    }
203    
204    private static UIDefaultsExt uiDefaultsExt = new UIDefaultsExt();
205    
206    private UIManagerExt() {
207        //does nothing
208    }
209    
210    /**
211     * Adds a resource bundle to the list of resource bundles that are searched
212     * for localized values. Resource bundles are searched in the reverse order
213     * they were added. In other words, the most recently added bundle is
214     * searched first.
215     * 
216     * @param bundleName
217     *                the base name of the resource bundle to be added
218     * @see java.util.ResourceBundle
219     * @see #removeResourceBundle
220     */
221    public static void addResourceBundle(String bundleName) {
222        uiDefaultsExt.addResourceBundle(bundleName);
223    }
224    
225    /**
226     * Removes a resource bundle from the list of resource bundles that are
227     * searched for localized defaults.
228     * 
229     * @param bundleName
230     *                the base name of the resource bundle to be removed
231     * @see java.util.ResourceBundle
232     * @see #addResourceBundle
233     */
234    public static void removeResourceBundle(String bundleName) {
235        uiDefaultsExt.removeResourceBundle(bundleName);
236    }
237
238    /**
239     * Returns a string from the defaults. If the value for {@code key} is not a
240     * {@code String}, {@code null} is returned.
241     * 
242     * @param key
243     *                an {@code Object} specifying the string
244     * @return the {@code String} object
245     * @throws NullPointerException
246     *                 if {@code key} is {@code null}
247     */
248    public static String getString(Object key) {
249        return getString(key, null);
250    }
251    
252    /**
253     * Returns a string from the defaults. If the value for {@code key} is not a
254     * {@code String}, {@code null} is returned.
255     * 
256     * @param key
257     *                an {@code Object} specifying the string
258     * @param l
259     *                the {@code Locale} for which the string is desired; refer
260     *                to {@code UIDefaults} for details on how a {@code null}
261     *                {@code Locale} is handled
262     * @return the {@code String} object
263     * @throws NullPointerException
264     *                 if {@code key} is {@code null}
265     */
266    public static String getString(Object key, Locale l) {
267        Object value = UIManager.get(key, l);
268        
269        if (value instanceof String) {
270            return (String) value;
271        }
272        
273        //only return resource bundle if not in UIDefaults
274        if (value == null) {
275            value = uiDefaultsExt.getFromResourceBundle(key, l);
276            
277            if (value instanceof String) {
278                return (String) value;
279            }
280        }
281        
282        return null;
283    }
284    
285    /**
286     * Returns an integer from the defaults. If the value for {@code key} is not
287     * an {@code int}, {@code 0} is returned.
288     * 
289     * @param key
290     *                an {@code Object} specifying the integer
291     * @return the {@code int}
292     * @throws NullPointerException
293     *                 if {@code key} is {@code null}
294     */
295    public static int getInt(Object key) {
296        return getInt(key, null);
297    }
298    
299    /**
300     * Returns an integer from the defaults. If the value for {@code key} is not
301     * an {@code int}, {@code 0} is returned.
302     * 
303     * @param key
304     *                an {@code Object} specifying the integer
305     * @param l
306     *                the {@code Locale} for which the integer is desired; refer
307     *                to {@code UIDefaults} for details on how a {@code null}
308     *                {@code Locale} is handled
309     * @return the {@code int}
310     * @throws NullPointerException
311     *                 if {@code key} is {@code null}
312     */
313    public static int getInt(Object key, Locale l) {
314        Object value = UIManager.get(key, l);
315        
316        if (value instanceof Integer) {
317            return (Integer) value;
318        }
319        
320        if (value == null) {
321            value = uiDefaultsExt.getFromResourceBundle(key, l);
322            
323            if (value instanceof Integer) {
324                return (Integer) value;
325            }
326            
327            if (value instanceof String) {
328                try {
329                    return Integer.decode((String) value);
330                } catch (NumberFormatException e) {
331                    // ignore - the entry was not parseable, can't do anything
332                    // JW: should we log it?
333                }
334            }
335        }
336        
337        return 0;
338    }
339    
340    /**
341     * Returns an Boolean from the defaults. If the value for {@code key} is not
342     * a {@code boolean}, {@code false} is returned.
343     * 
344     * @param key
345     *                an {@code Object} specifying the Boolean
346     * @return the {@code boolean}
347     * @throws NullPointerException
348     *                 if {@code key} is {@code null}
349     */
350    public static boolean getBoolean(Object key) {
351        return getBoolean(key, null);
352    }
353    
354    /**
355     * Returns an Boolean from the defaults. If the value for {@code key} is not
356     * a {@code boolean}, {@code false} is returned.
357     * 
358     * @param key
359     *                an {@code Object} specifying the Boolean
360     * @param l
361     *                the {@code Locale} for which the Boolean is desired; refer
362     *                to {@code UIDefaults} for details on how a {@code null}
363     *                {@code Locale} is handled
364     * @return the {@code boolean}
365     * @throws NullPointerException
366     *                 if {@code key} is {@code null}
367     */
368    public static boolean getBoolean(Object key, Locale l) {
369        Object value = UIManager.get(key, l);
370        
371        if (value instanceof Boolean) {
372            return (Boolean) value;
373        }
374        
375        //only return resource bundle if not in UIDefaults
376        if (value == null) {
377            value = uiDefaultsExt.getFromResourceBundle(key, l);
378            
379            if (value instanceof Boolean) {
380                return (Boolean) value;
381            }
382            
383            if (value instanceof String) {
384                return Boolean.valueOf((String) value);
385            }
386        }
387        
388        return false;
389    }
390    
391    /**
392     * Returns a color from the defaults. If the value for {@code key} is not
393     * a {@code Color}, {@code null} is returned.
394     * 
395     * @param key
396     *                an {@code Object} specifying the color
397     * @return the {@code Color} object
398     * @throws NullPointerException
399     *                 if {@code key} is {@code null}
400     */
401    public static Color getColor(Object key) {
402        return getColor(key, null);
403    }
404    
405    /**
406     * Returns a color from the defaults. If the value for {@code key} is not
407     * a {@code Color}, {@code null} is returned.
408     * 
409     * @param key
410     *                an {@code Object} specifying the color
411     * @param l
412     *                the {@code Locale} for which the color is desired; refer
413     *                to {@code UIDefaults} for details on how a {@code null}
414     *                {@code Locale} is handled
415     * @return the {@code Color} object
416     * @throws NullPointerException
417     *                 if {@code key} is {@code null}
418     */
419    public static Color getColor(Object key, Locale l) {
420        Object value = UIManager.get(key, l);
421        
422        if (value instanceof Color) {
423            return (Color) value;
424        }
425        
426        //only return resource bundle if not in UIDefaults
427        if (value == null) {
428            value = uiDefaultsExt.getFromResourceBundle(key, l);
429            
430            if (value instanceof Color) {
431                return (Color) value;
432            }
433            
434            if (value instanceof String) {
435                try {
436                    return Color.decode((String) value);
437                } catch (NumberFormatException e) {
438                    // incorrect format; does nothing
439                }
440            }
441        }
442        
443        return null;
444    }
445
446    //TODO: Font.decode always returns a valid font.  This is not acceptable for UIManager
447//    /**
448//     * Returns a font from the defaults. If the value for {@code key} is not
449//     * a {@code Font}, {@code null} is returned.
450//     * 
451//     * @param key
452//     *                an {@code Object} specifying the font
453//     * @return the {@code Font} object
454//     * @throws NullPointerException
455//     *                 if {@code key} is {@code null}
456//     */
457//    public static Font getFont(Object key) {
458//        return getFont(key, null);
459//    }
460//    
461//    /**
462//     * Returns a font from the defaults. If the value for {@code key} is not
463//     * a {@code Font}, {@code null} is returned.
464//     * 
465//     * @param key
466//     *                an {@code Object} specifying the font
467//     * @param l
468//     *                the {@code Locale} for which the font is desired; refer
469//     *                to {@code UIDefaults} for details on how a {@code null}
470//     *                {@code Locale} is handled
471//     * @return the {@code Font} object
472//     * @throws NullPointerException
473//     *                 if {@code key} is {@code null}
474//     */
475//    public static Font getFont(Object key, Locale l) {
476//        Object value = UIManager.get(key, l);
477//        
478//        if (value instanceof Font) {
479//            return (Font) value;
480//        }
481//        
482//        //only return resource bundle if not in UIDefaults
483//        if (value == null) {
484//            value = uiDefaultsExt.getFromResourceBundle(key, l);
485//            
486//            if (value instanceof Font) {
487//                return (Font) value;
488//            }
489//            
490//            if (value instanceof String) {
491//                return Font.decode((String) value);
492//            }
493//        }
494//        
495//        return null;
496//    }
497    
498    /**
499     * Returns a shape from the defaults. If the value for {@code key} is not a
500     * {@code Shape}, {@code null} is returned.
501     * 
502     * @param key an {@code Object} specifying the shape
503     * @return the {@code Shape} object
504     * @throws NullPointerException if {@code key} is {@code null}
505     */
506    public static Shape getShape(Object key) {
507        Object value = UIManager.getDefaults().get(key);
508        return (value instanceof Shape) ? (Shape) value : null;
509    }
510    
511    /**
512     * Returns a shape from the defaults that is appropriate for the given
513     * locale. If the value for {@code key} is not a {@code Shape},
514     * {@code null} is returned.
515     * 
516     * @param key
517     *                an {@code Object} specifying the shape
518     * @param l
519     *                the {@code Locale} for which the shape is desired; refer
520     *                to {@code UIDefaults} for details on how a {@code null}
521     *                {@code Locale} is handled
522     * @return the {@code Shape} object
523     * @throws NullPointerException
524     *                 if {@code key} is {@code null}
525     */
526    public static Shape getShape(Object key, Locale l) {
527        Object value = UIManager.getDefaults().get(key, l);
528        return (value instanceof Shape) ? (Shape) value : null;
529    }
530    
531    /**
532     * Returns a painter from the defaults. If the value for {@code key} is not
533     * a {@code Painter}, {@code null} is returned.
534     * 
535     * @param key
536     *                an {@code Object} specifying the painter
537     * @return the {@code Painter} object
538     * @throws NullPointerException
539     *                 if {@code key} is {@code null}
540     */
541    public static Painter<?> getPainter(Object key) {
542        Object value = UIManager.getDefaults().get(key);
543        return (value instanceof Painter<?>) ? (Painter<?>) value : null;
544    }
545    
546    /**
547     * Returns a painter from the defaults that is appropriate for the given
548     * locale. If the value for {@code key} is not a {@code Painter},
549     * {@code null} is returned.
550     * 
551     * @param key
552     *                an {@code Object} specifying the painter
553     * @param l
554     *                the {@code Locale} for which the painter is desired; refer
555     *                to {@code UIDefaults} for details on how a {@code null}
556     *                {@code Locale} is handled
557     * @return the {@code Painter} object
558     * @throws NullPointerException
559     *                 if {@code key} is {@code null}
560     */
561    public static Painter<?> getPainter(Object key, Locale l) {
562        Object value = UIManager.getDefaults().get(key, l);
563        return (value instanceof Painter<?>) ? (Painter<?>) value : null;
564    }
565    
566    /**
567     * Returns a border from the defaults. If the value for {@code key} is not a
568     * {@code Border}, {@code defaultBorder} is returned.
569     * 
570     * @param key
571     *                an {@code Object} specifying the border
572     * @param defaultBorder
573     *                the border to return if the border specified by
574     *                {@code key} does not exist
575     * @return the {@code Border} object
576     * @throws NullPointerException
577     *                 if {@code key} or {@code defaultBorder} is {@code null}
578     */
579    public static Border getSafeBorder(Object key, Border defaultBorder) {
580        Contract.asNotNull(defaultBorder, "defaultBorder cannot be null");
581        
582        Border safeBorder = UIManager.getBorder(key);
583        
584        if (safeBorder == null) {
585            safeBorder = defaultBorder;
586        }
587        
588        if (!(safeBorder instanceof UIResource)) {
589            safeBorder = new BorderUIResource(safeBorder);
590        }
591        
592        return safeBorder;
593    }
594    
595    /**
596     * Returns a color from the defaults. If the value for {@code key} is not a
597     * {@code Color}, {@code defaultColor} is returned.
598     * 
599     * @param key
600     *                an {@code Object} specifying the color
601     * @param defaultColor
602     *                the color to return if the color specified by {@code key}
603     *                does not exist
604     * @return the {@code Color} object
605     * @throws NullPointerException
606     *                 if {@code key} or {@code defaultColor} is {@code null}
607     */
608    public static Color getSafeColor(Object key, Color defaultColor) {
609        Contract.asNotNull(defaultColor, "defaultColor cannot be null");
610        
611        Color safeColor = UIManager.getColor(key);
612        
613        if (safeColor == null) {
614            safeColor = defaultColor;
615        }
616        
617        if (!(safeColor instanceof UIResource)) {
618            safeColor = new ColorUIResource(safeColor);
619        }
620        
621        return safeColor;
622    }
623    
624    /**
625     * Returns a dimension from the defaults. If the value for {@code key} is
626     * not a {@code Dimension}, {@code defaultDimension} is returned.
627     * 
628     * @param key
629     *                an {@code Object} specifying the dimension
630     * @param defaultDimension
631     *                the dimension to return if the dimension specified by
632     *                {@code key} does not exist
633     * @return the {@code Dimension} object
634     * @throws NullPointerException
635     *                 if {@code key} or {@code defaultColor} is {@code null}
636     */
637    public static Dimension getSafeDimension(Object key, Dimension defaultDimension) {
638        Contract.asNotNull(defaultDimension, "defaultDimension cannot be null");
639        
640        Dimension safeDimension = UIManager.getDimension(key);
641        
642        if (safeDimension == null) {
643            safeDimension = defaultDimension;
644        }
645        
646        if (!(safeDimension instanceof UIResource)) {
647            safeDimension = new DimensionUIResource(safeDimension.width, safeDimension.height);
648        }
649        
650        return safeDimension;
651    }
652    
653    /**
654     * Returns a font from the defaults. If the value for {@code key} is not a
655     * {@code Font}, {@code defaultFont} is returned.
656     * 
657     * @param key
658     *                an {@code Object} specifying the font
659     * @param defaultFont
660     *                the font to return if the font specified by {@code key}
661     *                does not exist
662     * @return the {@code Font} object
663     * @throws NullPointerException
664     *                 if {@code key} or {@code defaultFont} is {@code null}
665     */
666    public static Font getSafeFont(Object key, Font defaultFont) {
667        Contract.asNotNull(defaultFont, "defaultFont cannot be null");
668        
669        Font safeFont = UIManager.getFont(key);
670        
671        if (safeFont == null) {
672            safeFont = defaultFont;
673        }
674        
675        if (!(safeFont instanceof UIResource)) {
676            safeFont = new FontUIResource(safeFont);
677        }
678        
679        return safeFont;
680    }
681    
682    /**
683     * Returns an icon from the defaults. If the value for {@code key} is not a
684     * {@code Icon}, {@code defaultIcon} is returned.
685     * 
686     * @param key
687     *                an {@code Object} specifying the icon
688     * @param defaultIcon
689     *                the icon to return if the icon specified by {@code key}
690     *                does not exist
691     * @return the {@code Icon} object
692     * @throws NullPointerException
693     *                 if {@code key} or {@code defaultIcon} is {@code null}
694     */
695    public static Icon getSafeIcon(Object key, Icon defaultIcon) {
696        Contract.asNotNull(defaultIcon, "defaultIcon cannot be null");
697        
698        Icon safeIcon = UIManager.getIcon(key);
699        
700        if (safeIcon == null) {
701            safeIcon = defaultIcon;
702        }
703        
704        if (!(safeIcon instanceof UIResource)) {
705            safeIcon = new IconUIResource(safeIcon);
706        }
707        
708        return safeIcon;
709    }
710    
711    /**
712     * Returns an insets from the defaults. If the value for {@code key} is not
713     * a {@code Insets}, {@code defaultInsets} is returned.
714     * 
715     * @param key
716     *                an {@code Object} specifying the insets
717     * @param defaultInsets
718     *                the insets to return if the insets specified by
719     *                {@code key} does not exist
720     * @return the {@code Insets} object
721     * @throws NullPointerException
722     *                 if {@code key} or {@code defaultInsets} is {@code null}
723     */
724    public static Insets getSafeInsets(Object key, Insets defaultInsets) {
725        Contract.asNotNull(defaultInsets, "defaultInsets cannot be null");
726        
727        Insets safeInsets = UIManager.getInsets(key);
728        
729        if (safeInsets == null) {
730            safeInsets = defaultInsets;
731        }
732        
733        if (!(safeInsets instanceof UIResource)) {
734            safeInsets = new InsetsUIResource(safeInsets.top, safeInsets.left,
735                    safeInsets.bottom, safeInsets.right);
736        }
737        
738        return safeInsets;
739    }
740}