RSS feed
<< Packaging Java apps for OS X with Maven | Home | The extra dot in dot-com >>

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 :


Re: Making your Java app shine on OS X

This was cool. A really usefull addition would be Macify for Windows. It could make Windows shine like OSX... :-)

Re: Making your Java app shine on OS X

Cool utility!  I was looking for a way to update the dock icon via java and
your solution is the best I've found.

One suggestion:  in the DefaultApplication class, save the reflection results wherever possible to increase performance.

Re: Making your Java app shine on OS X

This Macify library (especially the bonus feature) is something I have been looking for for a long time.

Re: Making your Java app shine on OS X

Hi, 

This looks like a great plugin, but I couldn't get it working.

I'm trying to use it with Netbeans 6.01.

All I want to do is move the menu and change the application menu's name.

The latter I can do fine, but I can't seem to get the menu to budge.

This is the code I've been using in the pom file:

 <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>osxappbundle-maven-plugin</artifactId>
                <configuration>
                    <mainClass>com.bupa.RWFindReplace.RWFindReplaceMain</mainClass>
                     <bundleName>Rapid Weaver Find and Replace</bundleName>
                            <additionalClasspath>
                                <path>/System/Library/Java/</path>
                            </additionalClasspath>
                            <vmOptions>-Dcom.apple.macos.useScreenMenuBar=true</vmOptions>
                </configuration>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>bundle</goal>
                        </goals>
                    </execution>
               </executions>
           </plugin>

Can you see any stupid mistakes there?  What else could be preventing the menu from moving?

Sorry to be a pain,

Mike

Re: Making your Java app shine on OS X

Hi Mike, The vmOptions feature was added after the last release of the plugin and you need to use alpha-2-SNAPSHOT to get that currently. I'll try to get out an alpha-2 release next week.

Re: Making your Java app shine on OS X

Hi I run the following: defaultApplication.setApplicationIconImage(bufferedImage); and receive: NSRuntime.loadLibrary(/usr/lib/java/libObjCJava.dylib) error. java.lang.UnsatisfiedLinkError: /usr/lib/java/libObjCJava.A.dylib: at java.lang.ClassLoader$NativeLibrary.load(Native Method) at java.lang.ClassLoader.loadLibrary0(ClassLoader.java:1822) at java.lang.ClassLoader.loadLibrary(ClassLoader.java:1702) at java.lang.Runtime.load0(Runtime.java:770) at java.lang.System.load(System.java:1005) at com.apple.cocoa.foundation.NSRuntime.loadLibrary(NSRuntime.java:127) at com.apple.cocoa.foundation.NSRuntime.<clinit>(NSRuntime.java:35) at com.apple.cocoa.foundation.NSObject.<clinit>(NSObject.java:27) at java.lang.Class.forName0(Native Method) at java.lang.Class.forName(Class.java:169) at org.simplericity.macify.eawt.DefaultApplication.setApplicationIconImage(DefaultApplication.java:272) The entire app crashes, and that is not a good way of handling errors. Does it call System.exit() ??

Re: Making your Java app shine on OS X

Hi again The problem is that the cocoa API is deprecated and no longer supported by Apple.

Re: Making your Java app shine on OS X

Thanks,

This is a known issue. I'll probably upgrade to a new Mac with Tiger shortly which might help me resolve these problem.

Eirik,

Re: Making your Java app shine on OS X

Hi Eirik,

I successfully tested the ApplicationListener part of your library on Snow Leopard (Java 6).

However, you need to change the method handleReopenApplication with handleReOpenApplication in ApplicationListener, to match the corresponding Apple library method.

Thank you for you great idea of using reflection and proxy, which makes our app integrate with Mac OSX while also running on other OSes using the same code base.

Bruno Grieder

Re: Making your Java app shine on OS X

I really like your macify. Simply how it should be :) Unfortunately setDefaultMenuBar is missing. Could you be so kind and add this quickly? Thank You!

Re: Making your Java app shine on OS X

Hello Eirik,

What is the license for macify library? I would like to use it in a commercial software, I wonder if it is okay?

Alla

Re: Making your Java app shine on OS X

Macify is licensed under the Apache License Version 2.0, which means it is available for commercial use given that you follow the terms of the license.

Re: Making your Java app shine on OS X

Cool, thank you.

Re: Making your Java app shine on OS X

Thank you Eirik! You took care of all the hard work by setting up reflection for the com.apple.eawt classes and let me focus on making my application. I really appreciate that.

Re: Making your Java app shine on OS X

In class DefaultApplication

    public void removePreferencesMenuItem() {
        if (isMac()) {
            callMethod("removeAboutMenuItem");<----HERE
        } else {
            this.preferencesMenuItemPresent = false;
        }
    }

should be "removePreferencesMenuItem".

Add a comment Send a TrackBack