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    }