MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Java Programming on the Mac Write Twice, Run Anywhere

by Daniel H. Steinberg, presenter at the O'Reilly Mac OS X Conference
09/06/2002

One of the earlier promises of Java was that you could write an application once and run it on any platform with a JVM. For Swing applications, the various looks and feels solve the big issues. Bring a 100 percent Swing application to a Mac OS X box and your application automatically benefits from many of the features of Aqua's look and feel. Run the same application on a Windows machine and the application looks mostly like a native Windows application.

Fortunately, the big changes don't require much work on your part. Apple takes care of porting the look and feel of your favorite widgets and provides you with runtime properties to customize and fine-tune the appearance and placement. But what about the small things. Take a look at the File menu and menu bar in this application:


Screen shot.
What's "Exit" doing in this dropdown menu? Looks like a Windows term to me.

At first blush this looks great to someone developing Java applications on a Windows box. The menubar is at the top of the screen. The keyboard accelerators are the Command key and not the Control key that you'd expect on a Windows box. Boy, it sure looks right.

There are, however, tell-tale signs that this is just a port of a Windows application. For one thing, the Exit at the bottom looks really out of place. No Mac application would use Exit. A Mac application would use Quit and in Mac OS X it would be the last menu item in the Application menu. What is meant by the Send item? It may sound as if I'm splitting hairs and not appreciating the hard work done to bring this application this far. Remember that Mac users and Windows users have different expectations for their menus. If you want to write a cross-platform Java application that feels right to both audiences, you're going to need to address these differences. This advice also applies to Mac developers who are hoping their Java application will be adopted by Windows users. You have to be familiar with the culture you're trying to reach. In this article, we'll take a look at some basic techniques for customizing your menus for both platforms at once. As always, suggestions for future articles and comments can be sent to me at DSteinberg@core.com.

Comparing Mac and Windows Menu Bars for Native Apps

As a first step, you should set system properties to help improve the Java experience on a Mac. For example, set the system property com.apple.macos.useScreenMenuBar to true to display a menu bar assigned with the method setJMenuBar() at the top of the screen instead of at the top of the JFrame that owns it. You can read more about this and other runtime properties in the recent technote from Apple available in tech note 2031.

For the most part, runtime properties don't require changes to the underlying code. It's true that to take advantage of the different look and feels on different platforms you can't hardcode the look and feel in. Also, as noted above, if you have a JFrame myFrame then you can only move the JMenuBar myMenuBar if it has been associated with myFrame using code like myFrame.setJMenuBar(myMenuBar). If you specify the name to be displayed in the application menu for the about option using com.apple.mrj.application.apple.menu.about.name you'll need to enable it by creating the code for an AboutHandler.

To fine-tune the menu bars for the different platforms we're going to have to create different versions of the menu bar and of some of the menus and menu items. To see where we're heading, let's take a look at the File menus for the TextEdit application on Mac OS X and for the WordPad application in Windows 2000. Here's a flash from the past from the ghost of standardized tests gone by:

Screen shot.  Screen shot.
Compare and contrast the these pictures. Indeed the look is different.

Next to the Apple and Application menus in the TextEdit example, the menus include File, Edit, Format, Window, and Help. The WordPad menus are similar but not identical. There you see menus for File, Edit, View, Insert, Format, and Help. In this article, we'll only customize the File menus for both and leave the others as unimplemented menus. So we'll create a Mac and a Windows version of the menu bar. Each will have their custom version of the File menu. The other menus will just be JMenus with the labels set appropriately.

Download the example files for this article here.

Take another look at the File menus and you'll notice that there are common elements. Both versions have New, Open, Save, Save As, Print, and Page Setup menu items. We'll put these in a package that we'll call commonGUI. Even in these common elements there appear to be differences that we'll need to account for. You'll see that the TextEdit menu uses the command key for accelerators while the WordPad menu uses the control key. This difference is very easy to accommodate as the actual key being modified is the same for both menus (N for New, O for Open, and so on). There is a subtle difference with the New menu item. On the Mac the label reads "New" and on Windows it reads "New...". We'll just create a New menu item in the commonGUI package and extend it in the MacGUI package. Similarly, the Page Setup and Save As menu items have corresponding keyboard accelerators on the Mac but not on Windows. We'll again create a base class that we extend. For those menu items that exist only on the Windows or Mac version, we'll create a corresponding class within the WindowsGUI and MacGUI packages.

Note: Of course, there are many ways to solve any programming problem. You might look at the duplicated code and recognize that we could have used Resource bundles for the menu items that were common to both platforms. We'll save that solution for another article.

As you support more menus and menu items you may find it appropriate to introduce Abstract Factories and Factory Methods. You'll find these described in the Gang of Four book on Design Patterns. I'm following the lead of Joshua Kerievsky who advises that we don't begin with Design Patterns but refactor towards them when the need becomes apparent. You can read the latest draft of his "Refactoring to Patterns" book on the Refactoring page of the industrial logic site.

Setting Up the Two Menu Bars

Let's start by setting up the basic framework.

To help you keep track of where we're heading, when we're finished, the directory structure will look like this.

Screen shot.
Directory structure for basic framework.

Create the first version of the MacJMenuBar. This just puts appropriately labeled JMenus into the JMenuBar.

package MacGUI;

import javax.swing.JMenuBar;
import javax.swing.JMenu;

public class MacJMenuBar extends JMenuBar{
  public MacJMenuBar(){
    add(new JMenu("File"));
    add(new JMenu("Edit"));
    add(new JMenu("Format"));
    add(new JMenu("Window"));
    add(new JMenu("Help"));
  }
}

The code for the WindowsJMenuBar is the same except for the labels for some of the JMenus.

package WindowsGUI;

import javax.swing.JMenuBar;
import javax.swing.JMenu;

public class WindowsJMenuBar extends JMenuBar{
  public WindowsJMenuBar(){
    add(new JMenu("File"));
    add(new JMenu("Edit"));
    add(new JMenu("View"));
    add(new JMenu("Insert"));
    add(new JMenu("Format"));
    add(new JMenu("Help"));
  }
}

Choosing the Right Menu Bar

Now we'll create the MainFrame class. This is a JFrame that contains the main() method for this application and chooses the correct JMenuBar for the current operating system. Other than add a name to the title bar and adjust the size of the JFrame, the constructor includes the key call to the method getCorrectMenuBar().

In this method we check to see if we are running on a Mac by checking the value of the System property mrj.version. Note that this is the method recommended by Apple. This value is only set on the Mac so if it isn't null, we know our application is running on a Mac and can create the Mac version of the JMenuBar. Otherwise we'll create the Windows version. Incidentally, the initial version of Java shipping on Jaguar has this property set to 3.3. Here's the first version of the MainFrame class.

import javax.swing.JMenuBar;
import javax.swing.JFrame;
import MacGUI.MacJMenuBar;
import WindowsGUI.WindowsJMenuBar;

public class MainFrame extends JFrame{

  public MainFrame(){
    super("Write Twice Run Anywhere");
    setJMenuBar(getCorrectMenuBar());
    setSize(400,400);
    setVisible(true);
  }
  private JMenuBar getCorrectMenuBar(){
    if (System.getProperty("mrj.version")!=null){ 
      return new MacJMenuBar();
    } else return new WindowsJMenuBar();
  }

  public static void main(String [] args){
    new MainFrame();
  }
}

Compile and run this program and you'll see this menu bar.

Screen shot.
Close, but no prize. The menu bar still isn't in the right place.

Arrrrgh, that's not right. The menu bar isn't in the right place. It's inside of the JFrame not up in the system menu bar spot where it belongs. We need to set a system property. We can do that when we run the application, but let's go ahead a put it in the code. If we discover that the application is running on a Mac, we'll set the system property to display the JMenuBar in the correct location. Here's the revised version of the getCorrectMenuBar() method.

private JMenuBar getCorrectMenuBar(){
   if (System.getProperty("mrj.version")!=null){
      
System.setProperty("com.apple.macos.useScreenMenuBar","true");
      return new MacJMenuBar();
   } else return new WindowsJMenuBar();
}

With this simple addition, as you can see below, the menu bar is moved when the application runs on a Mac. The menu bar stays at the top of the JFrame when the application runs on other platforms.

Screen shot.
Ahhh, that's better. Now things look right!


Creating the Menu Items

The Open menu item is exactly the same in both the Windows and the Mac versions.

Here's the code for OpenMenuItem.

package commonGUI;

import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
import java.awt.event.KeyEvent;
import java.awt.Toolkit;

public class OpenMenuItem extends JMenuItem {
  public OpenMenuItem(){
    super("Open...");
    setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, 
             Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
  }
}

The highlighted line above is where all the magic happens. The first parameter being passed into the getKeyStroke() method is the key event that corresponds to typing the letter "O". On a Mac you type Command - O to select the Open menu item and on Windows you type Control - O. The second parameter retrieves the mask for the menu shortcut key on the platform your application is running. Rather than hardcode in the Control key or the Command key, you retrieve the information at runtime. First the Toolkit's static method getDefaultToolkit() gives you a handle to the current AWT toolkit. Then that toolkit is queried for the mask for the menu shortcut key. On a Windows box it returns the mask for the Control key and on a Mac it returns the mask for the Command key. This allows the OpenMenuItem to look as it should in both environments.

The Page Setup menu item is common to both the Mac and Windows File menus but it has an accelerator assigned on the Mac but not on Windows. We can build a common parent class and place it in the commonGUI package and then extend it on the Mac and add the accelerator. When you add behavior to Page Setup you can then do it in the common parent class and reduce the amount of duplicated code. Here's the root class, PageSetupMenuItem.

package commonGUI;

import javax.swing.JMenuItem;

public class PageSetupMenuItem extends JMenuItem{
  public PageSetupMenuItem(){
    super("Page Setup...");
  }
}

Now extend PageSetupMenuItem and add the keyboard accelerator in the constructor. Remember that the no argument constructor in the parent class will be the first thing called by the constructor in MacPageSetupMenuItem.

package MacGUI;

import commonGUI.PageSetupMenuItem;

import javax.swing.KeyStroke;
import java.awt.Toolkit;
import java.awt.event.KeyEvent;

public class MacPageSetupMenuItem extends PageSetupMenuItem{
  public MacPageSetupMenuItem(){
    setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_P, 
             Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()
             + java.awt.Event.SHIFT_MASK));
  }
}

There are two things to note about the highlighted code above. First, because it is Mac specific code we could have hard coded the choice of the menu shortcut mask. This version makes it easier to pull up to the parent class if we need to refactor later. The second issue is that there are actually two masks being specified here. If you look back at the image of the TextEdit File menu you can see that the shortcut for Page Setup requires that you press both the Command and Shift keys while typing the letter P. The values of the masks are powers of two and so you pass in more than one by adding them together. The component pieces of the sum can easily be determined.

Learning Java

Related Reading

Learning Java
By Patrick Niemeyer, Jonathan Knudsen

Cleaning Up the Loose Ends

You can use these three menu item examples from the last section to create the code for the remaining menu items. Once that is done you'll need to create the FileMenu classes for the MacGUI package and for the WindowsGUI package. Here's the MacGUI version.

package MacGUI;

import javax.swing.JMenu;
import commonGUI.*;

public class FileMenu extends JMenu{
  public FileMenu(){
    super("File");
    add(new MacNewMenuItem());
    add(new OpenMenuItem());
    add(new JMenu("Open Recent"));
    addSeparator();
    add(new CloseMenuItem());
    add(new SaveMenuItem());
    add(new MacSaveAsMenuItem());
    add(new SaveAllMenuItem());
    add(new RevertToSavedMenuItem());
    addSeparator();
    add(new MacPageSetupMenuItem());
    add(new PrintMenuItem());
  }
}

Again, make the necessary adjustments for the WindowsGUI version. The final task is to go back and fix the MacJMenuBar and WindowsJMenuBar. We had them create an ordinary JMenu with the label "File". We need to go back and change the code so that we are actually creating a FileMenu object. Here's the fixed version of MacJMenuBar.

package MacGUI;

import javax.swing.JMenuBar;
import javax.swing.JMenu;

public class MacJMenuBar extends JMenuBar{
  public MacJMenuBar(){
    add(new FileMenu()); // instead of add(new JMenu("File"));
    add(new JMenu("Edit"));
    add(new JMenu("Format"));
    add(new JMenu("Window"));
    add(new JMenu("Help"));
  }
}

Summary

In this article we tuned a Java application so that it could meet the expectations of two disparate audiences on their home platforms. The application still looks much like a Windows application when run on a Windows machine and now it looks much more like a Mac application when run on a Mac. A combination of using System properties and a minimal amount of parallel code improves the user experience. The next step is to add Mac-specific functionality such as an About Handler--but that's another article.

Daniel H. Steinberg is the editor for the new series of Mac Developer titles for the Pragmatic Programmers. He writes feature articles for Apple's ADC web site and is a regular contributor to Mac Devcenter. He has presented at Apple's Worldwide Developer Conference, MacWorld, MacHack and other Mac developer conferences.


Read more Java Programming on the Mac columns.

Return to the Mac DevCenter.


Copyright © 2009 O'Reilly Media, Inc.