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 }