001/*
002 * $Id: LookAndFeelAddons.java 4109 2012-01-20 15:23:31Z kschaefe $
003 *
004 * Copyright 2004 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.beans.PropertyChangeEvent;
024import java.beans.PropertyChangeListener;
025import java.lang.reflect.Method;
026import java.security.AccessController;
027import java.security.PrivilegedAction;
028import java.util.ArrayList;
029import java.util.Iterator;
030import java.util.List;
031import java.util.ServiceLoader;
032import java.util.logging.Level;
033import java.util.logging.Logger;
034
035import javax.swing.JComponent;
036import javax.swing.LookAndFeel;
037import javax.swing.UIDefaults;
038import javax.swing.UIManager;
039import javax.swing.plaf.ComponentUI;
040import javax.swing.plaf.UIResource;
041
042import org.jdesktop.swingx.painter.Painter;
043
044/**
045 * Provides additional pluggable UI for new components added by the library. By default, the library
046 * uses the pluggable UI returned by {@link #getBestMatchAddonClassName()}.
047 * <p>
048 * The default addon can be configured using the <code>swing.addon</code> system property as follow:
049 * <ul>
050 * <li>on the command line, <code>java -Dswing.addon=ADDONCLASSNAME ...</code></li>
051 * <li>at runtime and before using the library components
052 * <code>System.getProperties().put("swing.addon", ADDONCLASSNAME);</code></li>
053 * </ul>
054 * <p>
055 * The default {@link #getCrossPlatformAddonClassName() cross platform addon} can be configured
056 * using the <code>swing.crossplatformlafaddon</code> system property as follow:
057 * <ul>
058 * <li>on the command line, <code>java -Dswing.crossplatformlafaddon=ADDONCLASSNAME ...</code></li>
059 * <li>at runtime and before using the library components
060 * <code>System.getProperties().put("swing.crossplatformlafaddon", ADDONCLASSNAME);</code> <br>
061 * Note: changing this property after the UI has been initialized may result in unexpected behavior.
062 * </li>
063 * </ul>
064 * <p>
065 * The addon can also be installed directly by calling the {@link #setAddon(String)}method. For
066 * example, to install the Windows addons, add the following statement
067 * <code>LookAndFeelAddons.setAddon("org.jdesktop.swingx.plaf.windows.WindowsLookAndFeelAddons");</code>.
068 * 
069 * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a>
070 * @author Karl Schaefer
071 */
072@SuppressWarnings("nls")
073public abstract class LookAndFeelAddons {
074
075    private static List<ComponentAddon> contributedComponents = new ArrayList<ComponentAddon>();
076
077    /**
078     * Key used to ensure the current UIManager has been populated by the LookAndFeelAddons.
079     */
080    private static final Object APPCONTEXT_INITIALIZED = new Object();
081
082    private static boolean trackingChanges = false;
083    private static PropertyChangeListener changeListener;
084
085    static {
086        // load the default addon
087        String addonClassname = getBestMatchAddonClassName();
088
089        try {
090            addonClassname = System.getProperty("swing.addon", addonClassname);
091        } catch (SecurityException e) {
092            // security exception may arise in Java Web Start
093        }
094
095        try {
096            setAddon(addonClassname);
097        } catch (Exception e) {
098            // PENDING(fred) do we want to log an error and continue with a default
099            // addon class or do we just fail?
100            throw new ExceptionInInitializerError(e);
101        }
102
103        setTrackingLookAndFeelChanges(true);
104    }
105
106    private static LookAndFeelAddons currentAddon;
107
108    /**
109     * Determines if the addon is a match for the {@link UIManager#getLookAndFeel() current Look and
110     * Feel}.
111     * 
112     * @return {@code true} if this addon matches (is compatible); {@code false} otherwise
113     */
114    protected boolean matches() {
115        return false;
116    }
117
118    /**
119     * Determines if the addon is a match for the system Look and Feel.
120     * 
121     * @return {@code true} if this addon matches (is compatible with) the system Look and Feel;
122     *         {@code false} otherwise
123     */
124    protected boolean isSystemAddon() {
125        return false;
126    }
127
128    /**
129     * Initializes the look and feel addon. This method is
130     * 
131     * @see #uninitialize
132     * @see UIManager#setLookAndFeel
133     */
134    public void initialize() {
135        for (Iterator<ComponentAddon> iter = contributedComponents.iterator(); iter.hasNext();) {
136            ComponentAddon addon = iter.next();
137            addon.initialize(this);
138        }
139    }
140
141    public void uninitialize() {
142        for (Iterator<ComponentAddon> iter = contributedComponents.iterator(); iter.hasNext();) {
143            ComponentAddon addon = iter.next();
144            addon.uninitialize(this);
145        }
146    }
147
148    /**
149     * Adds the given defaults in UIManager.
150     * 
151     * Note: the values are added only if they do not exist in the existing look and feel defaults.
152     * This makes it possible for look and feel implementors to override SwingX defaults.
153     * 
154     * Note: the array is traversed in reverse order. If a key is found twice in the array, the
155     * key/value with the highest position in the array gets precedence over the other key in the
156     * array
157     * 
158     * @param keysAndValues
159     */
160    public void loadDefaults(Object[] keysAndValues) {
161        // Go in reverse order so the most recent keys get added first...
162        for (int i = keysAndValues.length - 2; i >= 0; i = i - 2) {
163            if (UIManager.getLookAndFeelDefaults().get(keysAndValues[i]) == null) {
164                UIManager.getLookAndFeelDefaults().put(keysAndValues[i], keysAndValues[i + 1]);
165            }
166        }
167    }
168
169    public void unloadDefaults(@SuppressWarnings("unused") Object[] keysAndValues) {
170        // commented after Issue 446.
171        /*
172         * for (int i = 0, c = keysAndValues.length; i < c; i = i + 2) {
173         * UIManager.getLookAndFeelDefaults().put(keysAndValues[i], null); }
174         */
175    }
176
177    public static void setAddon(String addonClassName) throws InstantiationException,
178            IllegalAccessException, ClassNotFoundException {
179        setAddon(Class.forName(addonClassName));
180    }
181
182    public static void setAddon(Class<?> addonClass) throws InstantiationException,
183            IllegalAccessException {
184        LookAndFeelAddons addon = (LookAndFeelAddons) addonClass.newInstance();
185        setAddon(addon);
186    }
187
188    public static void setAddon(LookAndFeelAddons addon) {
189        if (currentAddon != null) {
190            currentAddon.uninitialize();
191        }
192
193        addon.initialize();
194        currentAddon = addon;
195        // JW: we want a marker to discover if the LookAndFeelDefaults have been
196        // swept from under our feet. The following line looks suspicious,
197        // as it is setting a user default instead of a LF default. User defaults
198        // are not touched when resetting a LF
199        UIManager.put(APPCONTEXT_INITIALIZED, Boolean.TRUE);
200        // trying to fix #784-swingx: frequent NPE on getUI
201        // JW: we want a marker to discover if the LookAndFeelDefaults have been
202        // swept from under our feet.
203        UIManager.getLookAndFeelDefaults().put(APPCONTEXT_INITIALIZED, Boolean.TRUE);
204    }
205
206    public static LookAndFeelAddons getAddon() {
207        return currentAddon;
208    }
209
210    private static ClassLoader getClassLoader() {
211        ClassLoader cl = null;
212        
213        try {
214            cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
215                @Override
216                public ClassLoader run() {
217                    return LookAndFeelAddons.class.getClassLoader();
218                }
219            });
220        } catch (SecurityException ignore) { }
221        
222        if (cl == null) {
223            final Thread t = Thread.currentThread();
224            
225            try {
226                cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
227                    @Override
228                    public ClassLoader run() {
229                        return t.getContextClassLoader();
230                    }
231                });
232            } catch (SecurityException ignore) { }
233        }
234        
235        if (cl == null) {
236            try {
237                cl = AccessController.doPrivileged(new PrivilegedAction<ClassLoader>() {
238                    @Override
239                    public ClassLoader run() {
240                        return ClassLoader.getSystemClassLoader();
241                    }
242                });
243            } catch (SecurityException ignore) { }
244        }
245        
246        return cl;
247    }
248    
249    /**
250     * Based on the current look and feel (as returned by <code>UIManager.getLookAndFeel()</code>),
251     * this method returns the name of the closest <code>LookAndFeelAddons</code> to use.
252     * 
253     * @return the addon matching the currently installed look and feel
254     */
255    public static String getBestMatchAddonClassName() {
256        LookAndFeel laf = UIManager.getLookAndFeel();
257        String className = null;
258
259        if (UIManager.getCrossPlatformLookAndFeelClassName().equals(laf.getClass().getName())) {
260            className = getCrossPlatformAddonClassName();
261        } else if (UIManager.getSystemLookAndFeelClassName().equals(laf.getClass().getName())) {
262            className = getSystemAddonClassName();
263        } else {
264            ServiceLoader<LookAndFeelAddons> addonLoader = ServiceLoader.load(LookAndFeelAddons.class,
265                    getClassLoader());
266
267            for (LookAndFeelAddons addon : addonLoader) {
268                if (addon.matches()) {
269                    className = addon.getClass().getName();
270                    break;
271                }
272            }
273        }
274
275        if (className == null) {
276            className = getSystemAddonClassName();
277        }
278
279        return className;
280    }
281
282    public static String getCrossPlatformAddonClassName() {
283        try {
284            return AccessController.doPrivileged(new PrivilegedAction<String>() {
285                @Override
286                public String run() {
287                    return System.getProperty("swing.crossplatformlafaddon",
288                            "org.jdesktop.swingx.plaf.metal.MetalLookAndFeelAddons");
289                }
290            });
291        } catch (SecurityException ignore) {
292        }
293
294        return "org.jdesktop.swing.plaf.metal.MetalLookAndFeelAddons";
295    }
296
297    /**
298     * Gets the addon best suited for the operating system where the virtual machine is running.
299     * 
300     * @return the addon matching the native operating system platform.
301     */
302    public static String getSystemAddonClassName() {
303        ServiceLoader<LookAndFeelAddons> addonLoader = ServiceLoader.load(LookAndFeelAddons.class,
304                getClassLoader());
305        String className = null;
306
307        for (LookAndFeelAddons addon : addonLoader) {
308            if (addon.isSystemAddon()) {
309                className = addon.getClass().getName();
310                break;
311            }
312        }
313
314        if (className == null) {
315            className = getCrossPlatformAddonClassName();
316        }
317
318        return className;
319    }
320
321    /**
322     * Each new component added by the library will contribute its default UI classes, colors and
323     * fonts to the LookAndFeelAddons. See {@link ComponentAddon}.
324     * 
325     * @param component
326     */
327    public static void contribute(ComponentAddon component) {
328        contributedComponents.add(component);
329
330        if (currentAddon != null) {
331            // make sure to initialize any addons added after the
332            // LookAndFeelAddons has been installed
333            component.initialize(currentAddon);
334        }
335    }
336
337    /**
338     * Removes the contribution of the given addon
339     * 
340     * @param component
341     */
342    public static void uncontribute(ComponentAddon component) {
343        contributedComponents.remove(component);
344
345        if (currentAddon != null) {
346            component.uninitialize(currentAddon);
347        }
348    }
349
350    /**
351     * Workaround for IDE mixing up with classloaders and Applets environments. Consider this method
352     * as API private. It must not be called directly.
353     * 
354     * @param component
355     * @param expectedUIClass
356     * @return an instance of expectedUIClass
357     */
358    public static ComponentUI getUI(JComponent component, Class<?> expectedUIClass) {
359        maybeInitialize();
360
361        // solve issue with ClassLoader not able to find classes
362        String uiClassname = (String) UIManager.get(component.getUIClassID());
363        // possible workaround and more debug info on #784
364        if (uiClassname == null) {
365            Logger logger = Logger.getLogger("LookAndFeelAddons");
366            logger.warning("Failed to retrieve UI for " + component.getClass().getName()
367                    + " with UIClassID " + component.getUIClassID());
368            if (logger.isLoggable(Level.FINE)) {
369                logger.fine("Existing UI defaults keys: "
370                        + new ArrayList<Object>(UIManager.getDefaults().keySet()));
371            }
372            // really ugly hack. Should be removed as soon as we figure out what is causing the
373            // issue
374            uiClassname = "org.jdesktop.swingx.plaf.basic.Basic" + expectedUIClass.getSimpleName();
375        }
376        try {
377            Class<?> uiClass = Class.forName(uiClassname);
378            UIManager.put(uiClassname, uiClass);
379        } catch (ClassNotFoundException e) {
380            // we ignore the ClassNotFoundException
381        }
382
383        ComponentUI ui = UIManager.getUI(component);
384
385        if (expectedUIClass.isInstance(ui)) {
386            return ui;
387        } else if (ui == null) {
388            barkOnUIError("no ComponentUI class for: " + component);
389        } else {
390            String realUI = ui.getClass().getName();
391            Class<?> realUIClass = null;
392
393            try {
394                realUIClass = expectedUIClass.getClassLoader().loadClass(realUI);
395            } catch (ClassNotFoundException e) {
396                barkOnUIError("failed to load class " + realUI);
397            }
398
399            if (realUIClass != null) {
400                try {
401                    Method createUIMethod = realUIClass.getMethod("createUI",
402                            new Class[] { JComponent.class });
403
404                    return (ComponentUI) createUIMethod.invoke(null, new Object[] { component });
405                } catch (NoSuchMethodException e) {
406                    barkOnUIError("static createUI() method not found in " + realUIClass);
407                } catch (Exception e) {
408                    barkOnUIError("createUI() failed for " + component + " " + e);
409                }
410            }
411        }
412
413        return null;
414    }
415
416    // this is how core UIDefaults yells about bad components; we do the same
417    private static void barkOnUIError(String message) {
418        System.err.println(message);
419        new Error().printStackTrace();
420    }
421
422    /**
423     * With applets, if you reload the current applet, the UIManager will be reinitialized (entries
424     * previously added by LookAndFeelAddons will be removed) but the addon will not reinitialize
425     * because addon initialize itself through the static block in components and the classes do not
426     * get reloaded. This means component.updateUI will fail because it will not find its UI.
427     * 
428     * This method ensures LookAndFeelAddons get re-initialized if needed. It must be called in
429     * every component updateUI methods.
430     */
431    private static synchronized void maybeInitialize() {
432        if (currentAddon != null) {
433            // this is to ensure "UIManager#maybeInitialize" gets called and the
434            // LAFState initialized
435            UIDefaults defaults = UIManager.getLookAndFeelDefaults();
436            // if (!UIManager.getBoolean(APPCONTEXT_INITIALIZED)) {
437            // JW: trying to fix #784-swingx: frequent NPE in getUI
438            // moved the "marker" property into the LookAndFeelDefaults
439            if (!defaults.getBoolean(APPCONTEXT_INITIALIZED)) {
440                setAddon(currentAddon);
441            }
442        }
443    }
444
445    //
446    // TRACKING OF THE CURRENT LOOK AND FEEL
447    //
448    private static class UpdateAddon implements PropertyChangeListener {
449        @Override
450        public void propertyChange(PropertyChangeEvent evt) {
451            try {
452                setAddon(getBestMatchAddonClassName());
453            } catch (Exception e) {
454                // should not happen
455                throw new RuntimeException(e);
456            }
457        }
458    }
459
460    /**
461     * If true, everytime the Swing look and feel is changed, the addon which best matches the
462     * current look and feel will be automatically selected.
463     * 
464     * @param tracking
465     *            true to automatically update the addon, false to not automatically track the
466     *            addon. Defaults to false.
467     * @see #getBestMatchAddonClassName()
468     */
469    public static synchronized void setTrackingLookAndFeelChanges(boolean tracking) {
470        if (trackingChanges != tracking) {
471            if (tracking) {
472                if (changeListener == null) {
473                    changeListener = new UpdateAddon();
474                }
475                UIManager.addPropertyChangeListener(changeListener);
476            } else {
477                if (changeListener != null) {
478                    UIManager.removePropertyChangeListener(changeListener);
479                }
480                changeListener = null;
481            }
482            trackingChanges = tracking;
483        }
484    }
485
486    /**
487     * @return true if the addon will be automatically change to match the current look and feel
488     * @see #setTrackingLookAndFeelChanges(boolean)
489     */
490    public static synchronized boolean isTrackingLookAndFeelChanges() {
491        return trackingChanges;
492    }
493
494    /**
495     * Convenience method for setting a component's background painter property with a value from
496     * the defaults. The painter is only set if the painter is {@code null} or an instance of
497     * {@code UIResource}.
498     * 
499     * @param c
500     *            component to set the painter on
501     * @param painter
502     *            key specifying the painter
503     * @throws NullPointerException
504     *             if the component or painter is {@code null}
505     * @throws IllegalArgumentException
506     *             if the component does not contain the "backgroundPainter" property or the
507     *             property cannot be set
508     */
509    public static void installBackgroundPainter(JComponent c, String painter) {
510        Class<?> clazz = c.getClass();
511
512        try {
513            Method getter = clazz.getMethod("getBackgroundPainter");
514            Method setter = clazz.getMethod("setBackgroundPainter", Painter.class);
515
516            Painter<?> p = (Painter<?>) getter.invoke(c);
517
518            if (p == null || p instanceof UIResource) {
519                setter.invoke(c, UIManagerExt.getPainter(painter));
520            }
521        } catch (RuntimeException e) {
522            throw e;
523        } catch (Exception e) {
524            throw new IllegalArgumentException("cannot set painter on " + c.getClass());
525        }
526    }
527
528    /**
529     * Convenience method for uninstalling a background painter. If the painter of the component is
530     * a {@code UIResource}, it is set to {@code null}.
531     * 
532     * @param c
533     *            component to uninstall the painter on
534     * @throws NullPointerException
535     *             if {@code c} is {@code null}
536     * @throws IllegalArgumentException
537     *             if the component does not contain the "backgroundPainter" property or the
538     *             property cannot be set
539     */
540    public static void uninstallBackgroundPainter(JComponent c) {
541        Class<?> clazz = c.getClass();
542
543        try {
544            Method getter = clazz.getMethod("getBackgroundPainter");
545            Method setter = clazz.getMethod("setBackgroundPainter", Painter.class);
546
547            Painter<?> p = (Painter<?>) getter.invoke(c);
548
549            if (p == null || p instanceof UIResource) {
550                setter.invoke(c, (Painter<?>) null);
551            }
552        } catch (RuntimeException e) {
553            throw e;
554        } catch (Exception e) {
555            throw new IllegalArgumentException("cannot set painter on " + c.getClass());
556        }
557    }
558}