RSS feed
<< October 1, 2007 | Home | October 3, 2007 >>

Making your Java app shine on OS X

Making your Java application look and feel like a native OS X application has until recently been one of those things that is hard to get right. There are various solutions floating around, but they tend to force you to build and package the application on a Mac and they often involve some manual steps.

Last week I released the OS X Application Bundle Maven plugin. This plugin solves the packaging part of creating Java applications for OS X. But packaging is just one piece of the Java-on-a-Mac puzzle. You also need to integrate properly with the application menu and with the dock.

As an example, I've created a demo application called TinyNotepad. By default it looks like this on OS X:


There are a few issues with this example that makes it stand out as a non-native application. First, the name of the application is "org.simplericity.macify.Example" which happens to be the name of the main class. Second, Mac OS X has a global menu bar, so we need to move the menu from the window frame to the top of the screen. We can solve both of these issues by using the mentioned Maven plugin:

<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>osxappbundle-maven-plugin</artifactId>
<version>1.0-alpha-2-SNAPSHOT</version>
<executions>
<execution>
<goals>
<goal>bundle</goal>
</goals>
<configuration>
<mainClass>org.simplericity.macify.Example</mainClass>
<bundleName>tinyNotepad</bundleName>
<vmOptions>-Dcom.apple.macos.useScreenMenuBar=true</vmOptions>
 </configuration>
</execution>
</executions>
</plugin>

Now the name on the application menu is the much nicer "tinyNotepad" and the menu bar has moved to the top of the screen:

Let's take a look inside the menus. The file menu has the normal new, open,  save and exit items.


Additionally, I've added a Preferences item that lets the user configure tinyNotepad:

The "Help" menu only contains the "About" item:

By clicking on the "About" item we get this  dialog:


The application menu is OS X specific. Apple's design guidelines mandates that this menu contains an  "About application". Additionally it may contain a menu item for editing preferences. It always contains the "Quit application" item. Selecing "Quit application" (or using the Command-Q shortcut) will kill our application immediately without asking the user for verification. The application menu of tinyNotepad looks like this:

This about menu item isn't hooked up to our about dialog. Clicking on it gives us this generic dialog:



As you can see, the integration with the application menu isn't quite as good as it should be. So to clean up things we need to:

  • Attach the about menu item to our own about dialog
  • Add a preferences menu item and attach it to our preferences dialog
  • Hook into the "Quit application" menu item and allow our users to verfy that they really want to quit
  • Hide the "about" and "preferences" and "exit" menu items if running on OS X so they don't appear two places.

Apple offers the Apple Java Extensions API that lets us do just this type of integration with the application menu. However, adding that as a compile dependency to our application makes it impossible to build on other platforms without redistributing the Apple API.

That's why I created the Macify Library. This library basically provides the same features as Apple's, it automatically detects if it's running on a Mac and it integrates with the Apple API entirely through reflection.

This means you can  develop and distribute your application without redistributing Apple's API and you don't need to worry about any licensing issues. If your app runs on Windows, it will work fine. If you run it on a Mac it will run just as fine, and as a bonus it will be integrate properly.

So let's do some coding and use the Macify Library to make tinyNotePad shine on OS X. All our integration will be done through the Application interface. There is a default implementation of this called DefaultApplication:

Application application = new DefaultApplication();
MacifyExample example = new MacifyExample();
example.setApplication(application);

In the MacifyExample class we can now use the Application instance to register an ApplicationListener and add the preferences menu item:

application.addApplicationListener(this);
application.addPreferencesMenuItem();
application.setEnabledPreferencesMenu(true);

Then we make MacifyExample implement the ApplicationListener interface and it's methods:

    public void handleAbout(ApplicationEvent event) {
aboutAction.actionPerformed(null);
event.setHandled(true);
}

public void handleOpenApplication(ApplicationEvent event) {
// Ok, we know our application started
// Not much to do about that..
}

public void handleOpenFile(ApplicationEvent event) {
openFileInEditor(new File(event.getFilename()));
}

public void handlePreferences(ApplicationEvent event) {
preferencesAction.actionPerformed(null);
}

public void handlePrintFile(ApplicationEvent event) {
JOptionPane.showMessageDialog(this, "Sorry, printing not implemented");
}

public void handleQuit(ApplicationEvent event) {
exitAction.actionPerformed(null);
}

public void handleReopenApplication(ApplicationEvent event) {
setVisible(true);
}

The final thing to do is to not add the About and Preferences menu items if we're running on OS X since they are already in place in the application menu.

if( !application.isMac()) {
file.add(preferencesAction);
file.add(exitAction);
help.add(aboutAction);
}

That's all there is to it. We have now made tinyNotepad really shine on the Mac OS X platform.

A bonus feature..

If you're an Apple Mail user, you've probably noticed how the Mail icon updates in the dock and the appcation switcher when you have new, unread mail, like this:


This feature is fully supported in the Macify library. The setApplicationIconImage method updates the dock icon while getApplicationIconImage returns the current icon.

Let's add a method to our example that updates the icon with a red circle containing the number of files that have been opened. The method looks like this:

private void incrementIcon(int num) {
BufferedImage newIcon = new BufferedImage(originalIcon.getWidth(), originalIcon.getHeight(), BufferedImage.TYPE_INT_ARGB);

Graphics2D graphics = (Graphics2D) newIcon.getGraphics();

graphics.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
graphics.setColor(Color.decode("#E40000"));

graphics.drawImage(originalIcon, 0, 0, null);

graphics.fillOval(originalIcon.getWidth()-40, 0, 35, 35);

graphics.setColor(Color.WHITE);
graphics.setFont(new Font("Helvetica", Font.BOLD, 23));
graphics.drawString(Integer.toString(num), originalIcon.getWidth()-28, 25);

graphics.dispose();

application.setApplicationIconImage(newIcon);
}

originalIcon is a BufferedImage we fetched by calling getApplicationIconImage on startup. The result looks like this:

You'll find the full example in Subversion here:

http://simplericity.org/svn/simplericity/trunk/macify-example

The source code of Macify Library itself is located here:

http://simplericity.org/svn/simplericity/trunk/macify

You'll find version 1.0 of the Macify Library  in the Simplericity Maven repository:

http://simplericity.org/repository/





Tags :