2005-12-05

Using the GTK LAF to access GNOME stock icons

I've written a lot about little bits of Mac OS polish for Java programs, but I'm anal about polish on all operating systems. One reason I have such a visceral dislike of Win32 is that no-one there seems to care. When I used and developed for Win32, I expended at least as much effort on getting the pixels right there. I haven't paid much attention to Unix, though, until recently.

One problem is that there are so many different Unix desktops. Even assuming that we only care about GNOME (which isn't unreasonable), there hasn't been a good way to target GNOME from portable Java. Even in Java 5, the GTK LAF is a joke. It neither looks nor feels like GTK+. And using any kind of Java GNOME bindings defeats much of the purpose of using Java, so that's not an option.

Most of the stuff I work on disables the GTK LAF in favor of the cross-platform LAF, except on Java 6. On Java 6, the GTK LAF is coming along nicely. It's using Apple's trick of calling native code to do the actual rendering. (Actually, Werner Randelshofer's Quaqua LAF is a lot more convincing than Apple's effort and it works by using images of the various UI elements. But Apple's solution ought to be better if they had the right kind of person working on it. Werner has to include separate sets of images for each version of Mac OS he supports, whereas Apple's JNI ought to just do the right thing.)

Java 6 build 62's GTK LAF seems to have a few performance problems (at least on Linux/amd64; I haven't tried it on anything else yet) but appearance-wise it's close enough to mostly convince me. The menu separator problem has been fixed, and table headers look okay now. The most glaring remaining problem is that disabled menu items have missing icons rather than grayed-out icons.

Anyway, in anticipation of being able to use the GTK LAF with a straight face, I looked in to using GNOME stock icons on menu items. I knew that they were being used for buttons, and it turns out if you look at GTKStyle.java that there's support for all the different sizes, including the menu size. Unfortunately the GKStyle.GTKStockIcon constructor isn't accessible, but that's what reflection's for.

Here's a snapshot of the code from salma-hayek for giving convenient access to the GNOME stock icon functionality locked away in the GTK LAF:

package e.gui;

import e.util.*;
import javax.swing.*;

public class GnomeStockIcon {
/**
* These are type-safe versions of the hard-coded numbers in GTKStyle, for
* use with getGnomeStockIcon.
*/
public enum Size {
GTK_ICON_SIZE_INVALID,
GTK_ICON_SIZE_MENU, // 16 x 16
GTK_ICON_SIZE_SMALL_TOOLBAR, // 18x18
GTK_ICON_SIZE_LARGE_TOOLBAR, // 24x24
GTK_ICON_SIZE_BUTTON, // 20x20
GTK_ICON_SIZE_DND, // 32x32
GTK_ICON_SIZE_DIALOG // 48x48
}

private GnomeStockIcon() {
}

/**
* Sets the SMALL_ICON property to the given GNOME stock icon, assuming
* it's available. The SMALL_ICON property is left unchanged otherwise.
*/
public static void useStockIcon(AbstractAction action, String name) {
// FIXME: for Java 6, use Action.LARGE_ICON_KEY too.
Icon icon = getStockIcon(name, Size.GTK_ICON_SIZE_MENU);
if (icon != null) {
action.putValue(AbstractAction.SMALL_ICON, icon);
}
}

/**
* Returns an Icon for one of the GNOME stock icons. If the icon is not
* available for any reason, you'll get null. (Not using the GTK LAF is
* one reason why.)
* The GNOME header file listing the possible strings is here:
* http://cvs.gnome.org/viewcvs/gtk%2B/gtk/gtkstock.h?view=markup
*/
public static Icon getStockIcon(String name, Size size) {
if (GuiUtilities.isGtk() == false) {
return null;
}

Icon icon = null;
try {
Class gtkStockIconClass =
Class.forName("com.sun.java.swing.plaf.gtk.GTKStyle$GTKStockIcon");
java.lang.reflect.Constructor constructor =
gtkStockIconClass.getDeclaredConstructor(String.class, int.class);
constructor.setAccessible(true);
icon = (Icon) constructor.newInstance(name, size.ordinal());
} catch (Exception ex) {
// Sorry! No icon for you!
}
return icon;
}
}

And here's an example of how you'd use it in one of your Action classes:

public NewFileAction() {
super(ACTION_NAME);
putValue(ACCELERATOR_KEY, GuiUtilities.makeKeyStroke("N", false));
GnomeStockIcon.useStockIcon(this, "gtk-new");
}

You can see a list of the available names and pictures of the icons they correspond to in the GTK Stock Icons documentation.

Obviously, there's more to do here. It would be nice to support more general "stock action" configuration. We could set the NAME, ACCELERATOR, and description properties for all platforms, and the MNEMONIC property everywhere but Mac OS. (Man, I hate programs that use mnemonics on Mac OS. I don't think the Aqua LAF should even render them. Ugh.)

Also, when Java 6 is more widespread, we'll want to support LARGE_ICON, as mentioned in the FIXME comment above.

There's also the question of what order menu items should come in on any given platform. I haven't thought about it much or tried to implement it yet, but that sounds like it needs something to sort the menu after you've added your items.