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}