/******************************************************************************* * Copyright (c) 2007-2008 SAS Institute Inc., ILOG S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAS Institute Inc. - initial API and implementation * ILOG S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.albireo.core; import java.awt.Component; import java.awt.EventQueue; import java.io.PrintStream; import javax.swing.UIManager; import javax.swing.UnsupportedLookAndFeelException; import javax.swing.plaf.FontUIResource; import org.eclipse.albireo.internal.Platform; import org.eclipse.swt.graphics.Color; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; /** * This class deals with the customization of the look&feel * of Swing components embedded inside SWT. * <p> * It is customizable through the "replaceable singleton" design pattern. */ public class LookAndFeelHandler { // ======================================================================== // Accessors /** * This look&feel choice denotes the default Swing look&feel. * It may be platform dependent. */ public static final String LAFChoiceSwingDefault = "swing.defaultlaf"; /** * This look&feel choice denotes the cross-platform Swing * look&feel. * It is not platform dependent. * @see javax.swing.UIManager#getCrossPlatformLookAndFeelClassName() */ public static final String LAFChoiceCrossPlatform = "swing.crossplatformlaf"; /** * This look&feel choice denotes the Swing look&feel * that best approximates the system look&feel. * It is platform dependent. * @see javax.swing.UIManager#getSystemLookAndFeelClassName() */ public static final String LAFChoiceNativeSystem = "swing.systemlaf"; /** * This look&feel choice denotes the Swing look&feel * that best approximates the system look&feel, except that * the Gtk look&feel is used instead of the cross-platform * look&feel if SWT is based on Gtk and if it works fine. * (This typically affects Linux systems with KDE desktop.) * It is platform dependent. */ public static final String LAFChoiceNativeSystemPreferGtk = "swing.systemlaf+gtk"; /** * This look&feel choice denotes the Swing look&feel * that best approximates the system look&feel, except that * the Gtk look&feel is avoided. * (This typically affects Linux systems with Gtk desktop.) * It is platform dependent. */ public static final String LAFChoiceNativeSystemNoGtk = "swing.systemlaf-gtk"; private String lafChoice; // Set the default look&feel choice. { // On JDK 1.6, we have to avoid the Swing Gtk look&feel, to work around // https://bugs.eclipse.org/bugs/show_bug.cgi?id=126931 if ( Platform.JAVA_VERSION >= Platform.javaVersion ( 1, 6, 0 ) ) { this.lafChoice = LAFChoiceNativeSystemNoGtk; } else { // Set the default to LAFChoiceNativeSystemPreferGtk, so that on // Unix systems with a GNOME desktop and with themes supported by // the Swing Gtk look&feel this Gtk look&feel is used; it resembles // the system look&feel more closely than Metal or Nimbus. this.lafChoice = LAFChoiceNativeSystemPreferGtk; } } /** * Returns the look&feel choice. It can be a class name or one * the shorthands defined in this class. */ public String getLAFChoice () { return this.lafChoice; } /** * Specifies which look&feel should be used. * @param choice Either a class name, such as * <code>"javax.swing.plaf.metal.MetalLookAndFeel"</code>, * <code>"com.sun.java.swing.plaf.windows.WindowsLookAndFeel"</code>, * <code>"com.sun.java.swing.plaf.gtk.GTKLookAndFeel"</code>, * <code>"com.sun.java.swing.plaf.motif.MotifLookAndFeel"</code>, * <code>"apple.laf.AquaLookAndFeel"</code>, * or one of the shorthands * {@link #LAFChoiceSwingDefault}, * {@link #LAFChoiceCrossPlatform}, * {@link #LAFChoiceNativeSystem}, * {@link #LAFChoiceNativeSystemPreferGtk}, * {@link #LAFChoiceNativeSystemNoGtk}. */ public void setLAFChoice ( final String choice ) { this.lafChoice = choice; } // -------------------------- Options ---------------------------------------------- private boolean isDefaultSwtFontPropagated = true; private boolean isTooltipAlwaysShown = true; /** * Returns whether SWT default font changes are automatically propagated to * Swing. See {@link #setSwtDefaultFontPropagated(boolean)} for more * information. * * @return boolean */ public boolean isSwtDefaultFontPropagated () { return this.isDefaultSwtFontPropagated; } /** * Configures automatic propagation of changes to the SWT default font to * the underlying Swing look and feel. * <p> * The default value of this flag is <code>true</code>. * Normally, this ensures that Swing fonts will closely match the * corresponding SWT fonts, and that changes to the system font settings * will be obeyed by both Swing and SWT. However, Swing does not render * certain fonts very well (e.g. the default "Segoe UI" font in Windows * Vista). Setting the flag to <code>false</code> will disable the * propagation in cases where the Swing rendering is not acceptable, and * Swing will use its default fonts. Note, however, that if propagation is * disabled, changes to system font settings may not be detected by Swing. * * @param val boolean flag. If <code>true</code>, default fonts will be * propagated to Swing. */ public void setSwtDefaultFontPropagated ( final boolean val ) { this.isDefaultSwtFontPropagated = val; } /** * Returns whether Swing's default tooltip behavior will be changed to * be more consistent with SWT. See {@link #setTooltipAlwaysShown(boolean)} * for more information. * * @return the current isTooltipAlwaysShown flag */ public boolean isTooltipAlwaysShown () { return this.isTooltipAlwaysShown; } /** * Configures the tooltip behavior for Swing components. On some platforms, * Swing tooltips are not shown for inactive windows (including embedded * frames). By setting the flag to <code>true</code>, (the default value) * this Swing behavior is changed and tooltips are shown, whether or not * the window is inactive. This makes the Swing tooltips more consistent * with SWT tooltips. * <p> * Note: This change of behavior currently works only on JDK 1.6 and higher. * See sun bug <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6178004"> * 6178004</a> * * @param isTooltipAlwaysShown the new isTooltipAlwaysShown value to set */ public void setTooltipAlwaysShown ( final boolean isTooltipAlwaysShown ) { this.isTooltipAlwaysShown = isTooltipAlwaysShown; } // ======================================================================== // Overridable API private static final String GTK_LOOK_AND_FEEL_NAME = "com.sun.java.swing.plaf.gtk.GTKLookAndFeel"; //$NON-NLS-1$ /** * Sets the desired look&feel. * @see #setLAFChoice(String) */ public void setLookAndFeel () throws ClassNotFoundException, InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException { assert EventQueue.isDispatchThread (); // On AWT event thread if ( this.isTooltipAlwaysShown ) { // Try to turn on tooltips for inactive AWT windows. This is not the default // behavior on some platforms. Also, note that it will only work in // JDK1.6+ UIManager.put ( "ToolTipManager.enableToolTipMode", "allWindows" ); } // If the user has specified the Swing look&feel through the // system property, and the application has also specified the // look&feel, obey the user. The user is always right. if ( System.getProperty ( "swing.defaultlaf" ) != null ) { return; } String laf = getLAFChoice (); if ( LAFChoiceSwingDefault.equals ( laf ) ) { return; } else if ( LAFChoiceCrossPlatform.equals ( laf ) ) { laf = UIManager.getCrossPlatformLookAndFeelClassName (); } else if ( LAFChoiceNativeSystem.equals ( laf ) ) { laf = UIManager.getSystemLookAndFeelClassName (); } else if ( LAFChoiceNativeSystemPreferGtk.equals ( laf ) ) { laf = UIManager.getSystemLookAndFeelClassName (); if ( Platform.isGtk () && laf.equals ( UIManager.getCrossPlatformLookAndFeelClassName () ) ) { laf = GTK_LOOK_AND_FEEL_NAME; } if ( laf.equals ( GTK_LOOK_AND_FEEL_NAME ) ) { // Try the Gtk look&feel. try { doSetLookAndFeel ( GTK_LOOK_AND_FEEL_NAME ); return; } catch ( final ClassNotFoundException e ) { } catch ( final InstantiationException e ) { } catch ( final IllegalAccessException e ) { } catch ( final UnsupportedLookAndFeelException e ) { } // Second try: Use cross platform look and feel laf = UIManager.getCrossPlatformLookAndFeelClassName (); } } else if ( LAFChoiceNativeSystemNoGtk.equals ( laf ) ) { laf = UIManager.getSystemLookAndFeelClassName (); if ( GTK_LOOK_AND_FEEL_NAME.equals ( laf ) ) { laf = UIManager.getCrossPlatformLookAndFeelClassName (); } } doSetLookAndFeel ( laf ); } private static void doSetLookAndFeel ( final String laf ) throws ClassNotFoundException, InstantiationException, IllegalAccessException, UnsupportedLookAndFeelException { // Although UIManager.setLookAndFeel is specified to throw an // UnsupportedLookAndFeelException when the look&feel is not supported, // the com.sun.java.swing.plaf.gtk.GTKParser of JDK 1.5.0 reports // failures by doing System.err.println() and instead sets an unthemed // Gtk look&feel, which is very ugly. // Typically this happens on Linux systems with KDE desktop. The // $HOME/.gtkrc-2.0 file created by the KDE control center refers to // /opt/gnome/share/themes/Qt/gtk-2.0/gtkrc, which specifies a theming // engine "qtengine", which exists in C++ code but not in the // com.sun.java.swing.plaf.gtk mockup. The error message in this case // reads: // "/opt/gnome/share/themes/Qt/gtk-2.0/gtkrc:5: Engine "qtengine" is unsupported, ignoring" // // This can also happens even when running the GNOME desktop, when certain themes are selected. final PrintStream origSystemErr = System.err; try { System.setErr ( new PrintStream ( origSystemErr ) { @Override public void print ( final String s ) { throw new UnsupportedLookAndFeelRuntimeException ( s ); } @Override public void println ( final String s ) { throw new UnsupportedLookAndFeelRuntimeException ( s ); } } ); UIManager.setLookAndFeel ( laf ); } catch ( final UnsupportedLookAndFeelRuntimeException e ) { final UnsupportedLookAndFeelException newExc = new UnsupportedLookAndFeelException ( e.getMessage () ); newExc.initCause ( e ); throw newExc; } finally { System.setErr ( origSystemErr ); } } /** * This exception signals an unsupported look&feel. */ static class UnsupportedLookAndFeelRuntimeException extends RuntimeException { UnsupportedLookAndFeelRuntimeException ( final String message ) { super ( message ); } } // --------------------------- Font Management --------------------------- private Font lastPropagatedSwtFont; /** * Propagates the default SWT font to the Swing look&feel, * if allowed by {@link #isSwtDefaultFontPropagated()}. * In this implementation, this method calls the * {@link #updateLookAndFeelFonts(java.awt.Font)} method. * @param swtFont The default SWT font. * @param swtFontData Result of <code>swtFont.getFontData()</code>, * obtained on the SWT event thread. * @return The corresponding AWT font. * @see #isSwtDefaultFontPropagated() * @see #updateLookAndFeelFonts(java.awt.Font) */ public java.awt.Font propagateSwtFont ( final Font swtFont, final FontData[] swtFontData ) { assert EventQueue.isDispatchThread (); // On AWT event thread final java.awt.Font awtFont = ResourceConverter.getInstance ().convertFont ( swtFont, swtFontData ); if ( isSwtDefaultFontPropagated () && !swtFont.getDevice ().isDisposed () && this.lastPropagatedSwtFont != swtFont ) { this.lastPropagatedSwtFont = swtFont; // Update the look and feel defaults to use new font. // Swing should take care of this on its own, but it does not seem // to do it when mixed with SWT. updateLookAndFeelFonts ( awtFont ); } return awtFont; } /** * Changes the currently active Swing look&feel to use the given font * as primary font for everything. * @param awtFont A font. */ protected void updateLookAndFeelFonts ( final java.awt.Font awtFont ) { assert awtFont != null; assert EventQueue.isDispatchThread (); // On AWT event thread // The FontUIResource class marks the font as replaceable by the look and feel // implementation if font settings are later changed. final FontUIResource fontResource = new FontUIResource ( awtFont ); // Assign the new font to the relevant L&F font properties. These are // the properties that are initially assigned to the system font // under the Windows look and feel. // TODO: It's possible that other platforms will need other assignments. // TODO: This does not handle fonts other than the "system" font. // TODO: Swing does not render the Vista default Segoe UI font well. // Other fonts may change, and the Swing L&F may not be adjusting. UIManager.put ( "Button.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "CheckBox.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "ComboBox.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "EditorPane.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "Label.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "List.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "Panel.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "ProgressBar.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "RadioButton.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "ScrollPane.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "TabbedPane.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "Table.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "TableHeader.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "TextField.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "TextPane.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "TitledBorder.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "ToggleButton.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "TreeFont.font", fontResource ); //$NON-NLS-1$ UIManager.put ( "ViewportFont.font", fontResource ); //$NON-NLS-1$ } // --------------------------- Color Management --------------------------- /** * Propagates the foreground color from SWT to a given AWT/Swing component. * @param component An AWT/Swing component. * @param foreground The SWT foreground color. * @param preserveDefaults If true, the color will not be set on the * component if its foreground (whether specified * or inherited) is already the same as the given * foreground color. */ public void propagateSwtForeground ( final Component component, final Color foreground, final boolean preserveDefaults ) { assert EventQueue.isDispatchThread (); assert component != null; final ResourceConverter converter = ResourceConverter.getInstance (); final java.awt.Color fg = converter.convertColor ( foreground ); if ( !fg.equals ( component.getForeground () ) || !preserveDefaults ) { component.setForeground ( fg ); } } /** * Propagates the background color from SWT to a given AWT/Swing component. * @param component An AWT/Swing component. * @param background The SWT background color. * @param preserveDefaults If true, the color will not be set on the * component if its background (whether specified * or inherited) is already the same as the given * background color. */ public void propagateSwtBackground ( final Component component, final Color background, final boolean preserveDefaults ) { assert EventQueue.isDispatchThread (); assert component != null; final ResourceConverter converter = ResourceConverter.getInstance (); final java.awt.Color bg = converter.convertColor ( background ); if ( !bg.equals ( component.getBackground () ) || !preserveDefaults ) { component.setBackground ( bg ); } } // ======================================================================== // Singleton design pattern private static LookAndFeelHandler theHandler = new LookAndFeelHandler (); /** * Returns the currently active singleton of this class. */ public static LookAndFeelHandler getInstance () { return theHandler; } /** * Replaces the singleton of this class. * @param instance An instance of this class or of a customized subclass. */ public static void setInstance ( final LookAndFeelHandler instance ) { theHandler = instance; } }