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 }