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