/*******************************************************************************
* 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.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)) {
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.
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 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(String choice) {
lafChoice = choice;
}
// -------------------------- Options ----------------------------------------------
private boolean isDefaultSwtFontPropagated = false;
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 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(boolean val) {
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 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(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 (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 (ClassNotFoundException e) {
} catch (InstantiationException e) {
} catch (IllegalAccessException e) {
} catch (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(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.
PrintStream origSystemErr = System.err;
try {
System.setErr(
new PrintStream(origSystemErr) {
public void print(String s) {
throw new UnsupportedLookAndFeelRuntimeException(s);
}
public void println(String s) {
throw new UnsupportedLookAndFeelRuntimeException(s);
}
});
UIManager.setLookAndFeel(laf);
} catch (UnsupportedLookAndFeelRuntimeException e) {
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 {
private static final long serialVersionUID = 1L;
UnsupportedLookAndFeelRuntimeException(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(Font swtFont, FontData[] swtFontData) {
assert EventQueue.isDispatchThread(); // On AWT event thread
java.awt.Font awtFont = ResourceConverter.getInstance().convertFont(swtFont, swtFontData);
if (isSwtDefaultFontPropagated()
&& !swtFont.getDevice().isDisposed()
&& (lastPropagatedSwtFont != swtFont)) {
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(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.
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(Component component, Color foreground, boolean preserveDefaults) {
assert EventQueue.isDispatchThread();
assert component != null;
ResourceConverter converter = ResourceConverter.getInstance();
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(Component component, Color background, boolean preserveDefaults) {
assert EventQueue.isDispatchThread();
assert component != null;
ResourceConverter converter = ResourceConverter.getInstance();
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(LookAndFeelHandler instance) {
theHandler = instance;
}
}