Workaround for borderless Java Swing menus on Linux

Thursday, April 12. 2012

Workaround for borderless Java Swing menus on Linux

Since years Linux users are plagued by a bug which affects the rendering of Swing popup menus. When using a modern GTK theme (Like Gnome's Adwaita or Ubuntu's Ambiance and Radiance) then menus in Java Swing applications have no borders and no separators:

Several bug reports exists (Like #6925412) but it seems to be unclear if this is a bug in the affected GTK themes or a bug in Java's GTKLookAndFeel class. Fact is: I'm pretty sure this bug will not be fixed anytime soon so we have to find a workaround and in this article I will show you two different approaches.

Fixing the theme itself

The first workaround is fixing the theme itself as suggested in the bug report mentioned above. If you are using Gnome's Adwaita theme then you want to edit the file /usr/share/themes/Adwaita/gtk-2.0/gtkrc. You have to set the xthickness and ythickness values of the styles menu and separator_menu_item to a minimum of 1. Here is an excerpt of this file with the changes highlighted in bold:

...
style "menu" {
        xthickness = 1
        ythickness = 1
        ...
}
...
style "separator_menu_item" {
        xthickness = 1
        ythickness = 1
        ...
}
...

This fixes ALL Java Swing applications without modifying these applications itself. So this workaround is recommended if you are using applications like Netbeans for example. The workaround might have a negative impact on native GTK 2 applications. But so far I haven't seen any problems.

Alternatively you can fix the theme in a user-specific GTK theme file. Advantage: Your changes will not be overwritten by gnome updates. The file you want to create (Also create the directory structure) is ~/.themes/Default/gtk-2.0-key/gtkrc. You just have to put the fixed thickness values into the file. Everything else will still be read from the global theme:

style "menu" {
        xthickness = 1
        ythickness = 1
}

style "separator_menu_item" {
        ythickness = 1
}

Fixing the application

If you are writing your own Swing application and you don't want to enforce your users to fix the problem with the above workaround then you can apply a workaround directly to your application. I found a pretty naive workaround somewhere on the internet which simply adds borders to all popup menus when the GTK look and feel is detected but that's not a good idea because not all themes are affected by this bug and such a naive workaround will most likely destroy the look-and-feel of these themes. So I created a more complex workaround which uses reflection to change the thickness values deep in the GTK style objects of Swing to a minimum value of 1. If a theme defines a higher value then this value is not changed. So working themes are not affected. Here is the code:

/**
 * Swing menus are looking pretty bad on Linux when the GTK LaF is used (See
 * bug #6925412). It will most likely never be fixed anytime soon so this
 * method provides a workaround for it. It uses reflection to change the GTK
 * style objects of Swing so popup menu borders have a minimum thickness of
 * 1 and menu separators have a minimum vertical thickness of 1.
 */
public static void installGtkPopupBugWorkaround()
{
    // Get current look-and-feel implementation class
    LookAndFeel laf = UIManager.getLookAndFeel();
    Class<?> lafClass = laf.getClass();
    
    // Do nothing when not using the problematic LaF
    if (!lafClass.getName().equals(
        "com.sun.java.swing.plaf.gtk.GTKLookAndFeel")) return;
    
    // We do reflection from here on. Failure is silently ignored. The
    // workaround is simply not installed when something goes wrong here
    try
    {
        // Access the GTK style factory
        Field field = lafClass.getDeclaredField("styleFactory");
        boolean accessible = field.isAccessible();
        field.setAccessible(true);
        Object styleFactory = field.get(laf);
        field.setAccessible(accessible);
    
        // Fix the horizontal and vertical thickness of popup menu style
        Object style = getGtkStyle(styleFactory, new JPopupMenu(),
            "POPUP_MENU");
        fixGtkThickness(style, "yThickness");
        fixGtkThickness(style, "xThickness");
    
        // Fix the vertical thickness of the popup menu separator style
        style = getGtkStyle(styleFactory, new JSeparator(), 
            "POPUP_MENU_SEPARATOR");
        fixGtkThickness(style, "yThickness");
    }
    catch (Exception e)
    {
        // Silently ignored. Workaround can't be applied.
    }
}

/**
 * Called internally by installGtkPopupBugWorkaround to fix the thickness
 * of a GTK style field by setting it to a minimum value of 1.
 * 
 * @param style
 *            The GTK style object.
 * @param fieldName
 *            The field name.
 * @throws Exception
 *             When reflection fails.
 */
private static void fixGtkThickness(Object style, String fieldName)
    throws Exception
{
    Field field = style.getClass().getDeclaredField(fieldName);
    boolean accessible = field.isAccessible();
    field.setAccessible(true);
    field.setInt(style, Math.max(1, field.getInt(style)));
    field.setAccessible(accessible);
}

/**
 * Called internally by installGtkPopupBugWorkaround. Returns a specific
 * GTK style object.
 * 
 * @param styleFactory
 *            The GTK style factory.
 * @param component
 *            The target component of the style.
 * @param regionName
 *            The name of the target region of the style.
 * @return The GTK style.
 * @throws Exception
 *             When reflection fails.
 */
private static Object getGtkStyle(Object styleFactory,
    JComponent component, String regionName) throws Exception
{
    // Create the region object
    Class<?> regionClass = Class.forName("javax.swing.plaf.synth.Region");
    Field field = regionClass.getField(regionName);
    Object region = field.get(regionClass);
    
    // Get and return the style
    Class<?> styleFactoryClass = styleFactory.getClass();
    Method method = styleFactoryClass.getMethod("getStyle",
        new Class<?>[] { JComponent.class, regionClass });
    boolean accessible = method.isAccessible();
    method.setAccessible(true);
    Object style = method.invoke(styleFactory, component, region);
    method.setAccessible(accessible);
    return style;
}

Simply call the installGtkPopupBugWorkaround method once after setting the LaF. All menus in your application and all menu separators will then be automatically fixed. The workaround may silently fail when Oracle changes the implementation of the GTK Look-And-Feel classes but hopefully the workaround will no longer be necessary anyway when Oracle finally changes these parts of the implementation. Up to now the workaround works fine with Oracle Java 6, Oracle Java 7, OpenJDK 6 and OpenJDK 7.

Posted in Java, Linux | Comment (1)
Carlos C Soto at 2012-08-30 00:54
Thanks a lot for this article, you made my desktop look better and me to understand why there was no borders in Netbeans menus

Using Debian GNU/Linux testing (wheezy) with Gnome 3.4.2

Enclosing asterisks marks text as bold (*word*), underscore are made via _word_.