The same JAR for *nix and MacOS X

Java's promise of write-once, run-anywhere isn't quite a perfect reality. Software that runs on the Mac has a couple of little quirks that really deserve attention for the truly polished GUI application.

There's nothing wrong with not taking the trouble to make your application look good on a Mac, but by going the extra mile, your work will truly look as though it belonged on a mac, while not having to maintain a separate version.

So what's the deal?

That raises the question: what's to be done differently when you're on a mac?

The answer can be found at Apple's developer site in Technical Note 2042. In particular, we're interested in the section that talks about the Application Menu.

The Application Menu is a menu that exists on the mac by default, and has some actions associated with it. The application should register handlers for those menu items and not put coresponding menu items elsewhere in the JMainMenuBar.

The code to register these events looks something like this:


import com.apple.mrj.*;

class SpecialMacHandler
	implements MRJQuitHandler, MRJPrefsHandler, MRJAboutHandler {
	Program us;
	public SpecialMacHandler(Program theProgram) {
		us = theProgram;
		System.setProperty("com.apple.macos.useScreenMenubar", "true");
		System.setProperty("com.apple.mrj.application.apple.menu.about.name", "MyProgram");
		MRJApplicationUtils.registerAboutHandler(this);
		MRJApplicationUtils.registerPrefsHandler(this);
		MRJApplicationUtils.registerQuitHandler(this);
	}
	void HandleAbout() {
		us.About();
	}
	void HandlePrefs() {
		us.Prefs();
	}
	void HandleQuit() {
		SwingUtilities.invokeLater(new Runnable() {
			public void run() {
				us.Quit();
			}
		});
		throw new IllegalStateException("Let the quit handler do it");
	}
}

In that snippet, Program represents the class of the main program. It must have public methods for About, Prefs and Quit somewhere in it that do whatever you require for those menu items.

Incidently, the need to wrap the HandleQuit innards with SwingUtilities.invokeLater() is a workaround for a known bug in the MacOS X JRE. It was fixed in Jaguar, but the workaround is harmless and avoids problems on older JREs.

Of course, this code will not even compile properly on non-Mac platforms. To get it to compile, you need to supply a dummy public class MRJApplicationUtils implementation. But you can't package the dummy implementation with your code, because it might be prefered on a mac in place of the system implementation. But if you don't include an implementation of those classes, then this class will fail to link at run-time on non-macs. Oh my! What a quandry!

Update! I am told that the classloader indeed will guarantee to prefer the system implementation over a shim MRJApplicationUtils implementation in the jar, so I guess another way to do this is to simply provide an implementation of MRJApplicationUtils that has empty methods. It turns out, though, that you'll also have to provide dummy interface classes for all of the arguments to the register*Handler() methods if you do this.

Reflection to the rescue

If we were to simply compile the above class and insert it into a program's JAR file having done nothing else, the resulting JAR would run unchanged. The class would be totally ignored. Why? Because there is no reference in any loaded class to it.

This is the key. So long as no other class in our program directly references our "contaminated" class, we won't wind up depending on the com.apple.mrj classes (which don't exist on non-mac platforms). But we need an instance of the contaminated class on the acceptable platform. That is, we really want to do this:

	if (isMac()) {
		new SpecialMacHandler(this);
	}
but we will land in hot water because the class loader doesn't know what isMac() will return. It will only see that SpecialMacHandler wants com.apple.mrj.MRJApplicationUtils and fail to start the program on non-Macs. We must achieve the same end without referencing the class directly in the code. Reflection offers us the way. The reflected version of the same code looks like this:

	if (isMac()) {
		try {
			Object[] args = { this };
			Class[] arglist = { Program.class };
			Class mac_class = class.forName("SpecialMacHandler");
			Constructor new_one = mac_class.getConstructor(arglist);
			new_one.newInstance(args);
		}
		catch(Exception e) {
			System.out.println(e);
		}
	}
(The use of this in args implies that we are either in the constructor for or a method of class Program)

This will give us a new instance of the special mac handler when we're on a mac, but not anywhere else.

Oh, incidently, here's how you tell if you're on a mac (again, stright from TN2042):

	public boolean isMac() {
		return System.getProperty("mrj.version") != null;
	}
This will let you do things like remove the About, Prefs and Quit menu items from your JMenuBar.