/*******************************************************************************
* 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;
}
}