001    /*
002     * $Id: LookAndFeelAddons.java,v 1.15 2005/12/10 11:33:36 l2fprod Exp $
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    
030    import javax.swing.JComponent;
031    import javax.swing.UIManager;
032    import javax.swing.plaf.ComponentUI;
033    
034    import org.jdesktop.swingx.plaf.aqua.AquaLookAndFeelAddons;
035    import org.jdesktop.swingx.plaf.metal.MetalLookAndFeelAddons;
036    import org.jdesktop.swingx.plaf.motif.MotifLookAndFeelAddons;
037    import org.jdesktop.swingx.plaf.windows.WindowsClassicLookAndFeelAddons;
038    import org.jdesktop.swingx.plaf.windows.WindowsLookAndFeelAddons;
039    import org.jdesktop.swingx.util.OS;
040    
041    /**
042     * Provides additional pluggable UI for new components added by the
043     * library. By default, the library uses the pluggable UI returned by
044     * {@link #getBestMatchAddonClassName()}.
045     * <p>
046     * The default addon can be configured using the
047     * <code>swing.addon</code> system property as follow:
048     * <ul>
049     * <li>on the command line,
050     * <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>
053     * </li>
054     * </ul>
055     * <p>
056     * The addon can also be installed directly by calling the
057     * {@link #setAddon(String)}method. For example, to install the
058     * Windows addons, add the following statement
059     * <code>LookAndFeelAddons.setAddon("org.jdesktop.swingx.plaf.windows.WindowsLookAndFeelAddons");</code>.
060     * 
061     * @author <a href="mailto:fred@L2FProd.com">Frederic Lavigne</a> 
062     */
063    public class LookAndFeelAddons {
064    
065      private static List<ComponentAddon> contributedComponents =
066        new ArrayList<ComponentAddon>();
067    
068      /**
069       * Key used to ensure the current UIManager has been populated by the
070       * LookAndFeelAddons.
071       */
072      private static final Object APPCONTEXT_INITIALIZED = new Object();
073    
074      private static boolean trackingChanges = false;
075      private static PropertyChangeListener changeListener;    
076      
077      static {
078        // load the default addon
079        String addonClassname = getBestMatchAddonClassName();
080        try {
081          addonClassname = System.getProperty("swing.addon", addonClassname);
082        } catch (SecurityException e) {
083          // security exception may arise in Java Web Start
084        }
085    
086        try {
087          setAddon(addonClassname);
088        } catch (Exception e) {
089          // PENDING(fred) do we want to log an error and continue with a default
090          // addon class or do we just fail?
091          throw new ExceptionInInitializerError(e);
092        }
093        
094        setTrackingLookAndFeelChanges(true);
095          
096        // this addon ensure resource bundle gets added to lookandfeel defaults
097        // and re-added by #maybeInitialize if needed      
098        contribute(new AbstractComponentAddon("MinimumAddon") {
099          @Override
100          protected void addBasicDefaults(LookAndFeelAddons addon, List<Object> defaults) {
101            addResource(defaults, "org.jdesktop.swingx.plaf.resources.swingx");
102          }
103        });
104      }
105    
106      private static LookAndFeelAddons currentAddon;
107    
108      public void initialize() {
109        for (Iterator<ComponentAddon> iter = contributedComponents.iterator(); iter
110          .hasNext();) {
111          ComponentAddon addon = iter.next();
112          addon.initialize(this);
113        }
114      }
115    
116      public void uninitialize() {
117        for (Iterator<ComponentAddon> iter = contributedComponents.iterator(); iter
118          .hasNext();) {
119          ComponentAddon addon = iter.next();
120          addon.uninitialize(this);
121        }
122      }
123    
124      /**
125       * Adds the given defaults in UIManager.
126       * 
127       * @param keysAndValues
128       */
129      public void loadDefaults(Object[] keysAndValues) {
130        for (int i = 0, c = keysAndValues.length; i < c; i = i + 2) {
131          UIManager.getLookAndFeelDefaults().put(keysAndValues[i], keysAndValues[i + 1]);
132        }
133      }
134    
135      public void unloadDefaults(Object[] keysAndValues) {
136        for (int i = 0, c = keysAndValues.length; i < c; i = i + 2) {
137          UIManager.getLookAndFeelDefaults().put(keysAndValues[i], null);
138        }
139      }
140    
141      public static void setAddon(String addonClassName)
142        throws InstantiationException, IllegalAccessException,
143        ClassNotFoundException {
144        setAddon(Class.forName(addonClassName));
145      }
146    
147      public static void setAddon(Class addonClass) throws InstantiationException,
148        IllegalAccessException {
149        LookAndFeelAddons addon = (LookAndFeelAddons)addonClass.newInstance();
150        setAddon(addon);
151      }
152       
153      public static void setAddon(LookAndFeelAddons addon) {
154        if (currentAddon != null) {
155          currentAddon.uninitialize();
156        }
157    
158        addon.initialize();
159        currentAddon = addon;    
160        UIManager.put(APPCONTEXT_INITIALIZED, Boolean.TRUE);
161      }
162    
163      public static LookAndFeelAddons getAddon() {
164        return currentAddon;
165      }
166    
167      /**
168       * Based on the current look and feel (as returned by
169       * <code>UIManager.getLookAndFeel()</code>), this method returns
170       * the name of the closest <code>LookAndFeelAddons</code> to use.
171       * 
172       * @return the addon matching the currently installed look and feel
173       */
174      public static String getBestMatchAddonClassName() {
175        String lnf = UIManager.getLookAndFeel().getClass().getName();
176        String addon;
177        if (UIManager.getCrossPlatformLookAndFeelClassName().equals(lnf)) {
178          addon = MetalLookAndFeelAddons.class.getName();
179        } else if (UIManager.getSystemLookAndFeelClassName().equals(lnf)) {
180          addon = getSystemAddonClassName();
181        } else if ("com.sun.java.swing.plaf.windows.WindowsLookAndFeel".equals(lnf) ||
182          "com.jgoodies.looks.windows.WindowsLookAndFeel".equals(lnf)) {
183          if (OS.isUsingWindowsVisualStyles()) {
184            addon = WindowsLookAndFeelAddons.class.getName();
185          } else {
186            addon = WindowsClassicLookAndFeelAddons.class.getName();
187          }
188        } else if ("com.sun.java.swing.plaf.windows.WindowsClassicLookAndFeel"
189          .equals(lnf)) {
190          addon = WindowsClassicLookAndFeelAddons.class.getName();
191        } else if (UIManager.getLookAndFeel().getID().equals("Motif")) {
192          addon = MotifLookAndFeelAddons.class.getName();
193        } else {
194          addon = getSystemAddonClassName();
195        }
196        return addon;
197      }
198    
199      /**
200       * Gets the addon best suited for the operating system where the
201       * virtual machine is running.
202       * 
203       * @return the addon matching the native operating system platform.
204       */
205      public static String getSystemAddonClassName() {
206        String addon = WindowsClassicLookAndFeelAddons.class.getName();
207    
208        if (OS.isMacOSX()) {
209          addon = AquaLookAndFeelAddons.class.getName();
210        } else if (OS.isWindows()) {
211          // see whether of not visual styles are used
212          if (OS.isUsingWindowsVisualStyles()) {
213            addon = WindowsLookAndFeelAddons.class.getName();
214          } else {
215            addon = WindowsClassicLookAndFeelAddons.class.getName();
216          }
217        }
218    
219        return addon;
220      }
221    
222      /**
223       * Each new component added by the library will contribute its
224       * default UI classes, colors and fonts to the LookAndFeelAddons.
225       * See {@link ComponentAddon}.
226       * 
227       * @param component
228       */
229      public static void contribute(ComponentAddon component) {
230        contributedComponents.add(component);
231    
232        if (currentAddon != null) {
233          // make sure to initialize any addons added after the
234          // LookAndFeelAddons has been installed
235          component.initialize(currentAddon);
236        }
237      }
238    
239      /**
240       * Removes the contribution of the given addon
241       * 
242       * @param component
243       */
244      public static void uncontribute(ComponentAddon component) {
245        contributedComponents.remove(component);
246        
247        if (currentAddon != null) {
248          component.uninitialize(currentAddon);
249        }
250      }
251    
252      /**
253       * Workaround for IDE mixing up with classloaders and Applets environments.
254       * Consider this method as API private. It must not be called directly.
255       * 
256       * @param component
257       * @param expectedUIClass
258       * @return an instance of expectedUIClass 
259       */
260      public static ComponentUI getUI(JComponent component, Class expectedUIClass) {
261        maybeInitialize();
262    
263        // solve issue with ClassLoader not able to find classes
264        String uiClassname = (String)UIManager.get(component.getUIClassID());
265        try {
266          Class uiClass = Class.forName(uiClassname);
267          UIManager.put(uiClassname, uiClass);
268        } catch (ClassNotFoundException e) {
269          // we ignore the ClassNotFoundException
270        }
271        
272        ComponentUI ui = UIManager.getUI(component);
273        
274        if (expectedUIClass.isInstance(ui)) {
275          return ui;
276        } else {
277          String realUI = ui.getClass().getName();
278          Class realUIClass;
279          try {
280            realUIClass = expectedUIClass.getClassLoader()
281            .loadClass(realUI);
282          } catch (ClassNotFoundException e) {
283            throw new RuntimeException("Failed to load class " + realUI, e);
284          }
285          Method createUIMethod = null;
286          try {
287            createUIMethod = realUIClass.getMethod("createUI", new Class[]{JComponent.class});
288          } catch (NoSuchMethodException e1) {
289            throw new RuntimeException("Class " + realUI + " has no method createUI(JComponent)");
290          }
291          try {
292            return (ComponentUI)createUIMethod.invoke(null, new Object[]{component});
293          } catch (Exception e2) {
294            throw new RuntimeException("Failed to invoke " + realUI + "#createUI(JComponent)");
295          }
296        }
297      }
298      
299      /**
300       * With applets, if you reload the current applet, the UIManager will be
301       * reinitialized (entries previously added by LookAndFeelAddons will be
302       * removed) but the addon will not reinitialize because addon initialize
303       * itself through the static block in components and the classes do not get
304       * reloaded. This means component.updateUI will fail because it will not find
305       * its UI.
306       * 
307       * This method ensures LookAndFeelAddons get re-initialized if needed. It must
308       * be called in every component updateUI methods.
309       */
310      private static synchronized void maybeInitialize() {
311        if (currentAddon != null) {
312          // this is to ensure "UIManager#maybeInitialize" gets called and the
313          // LAFState initialized
314          UIManager.getLookAndFeelDefaults();
315          
316          if (!UIManager.getBoolean(APPCONTEXT_INITIALIZED)) {
317            setAddon(currentAddon);
318          }
319        }
320      }
321      
322      //
323      // TRACKING OF THE CURRENT LOOK AND FEEL
324      //
325      private static class UpdateAddon implements PropertyChangeListener {
326        public void propertyChange(PropertyChangeEvent evt) {
327          try {
328            setAddon(getBestMatchAddonClassName());
329          } catch (Exception e) {
330            // should not happen
331            throw new RuntimeException(e);
332          }
333        }
334      }
335      
336      /**
337       * If true, everytime the Swing look and feel is changed, the addon which
338       * best matches the current look and feel will be automatically selected.
339       * 
340       * @param tracking
341       *          true to automatically update the addon, false to not automatically
342       *          track the addon. Defaults to false.
343       * @see #getBestMatchAddonClassName()
344       */
345      public static synchronized void setTrackingLookAndFeelChanges(boolean tracking) {
346        if (trackingChanges != tracking) {
347          if (tracking) {
348            if (changeListener == null) {
349              changeListener = new UpdateAddon();
350            }
351            UIManager.addPropertyChangeListener(changeListener);
352          } else {
353            if (changeListener != null) {
354              UIManager.removePropertyChangeListener(changeListener);
355            }
356            changeListener = null;
357          }
358          trackingChanges = tracking;
359        }
360      }
361      
362      /**
363       * @return true if the addon will be automatically change to match the current
364       *         look and feel
365       * @see #setTrackingLookAndFeelChanges(boolean)
366       */
367      public static synchronized boolean isTrackingLookAndFeelChanges() {
368        return trackingChanges;
369      }
370       
371    }