Bringing your Java Application to Mac OS X Part Two

   
   

Articles Index


Apple has just released Java 2 Standard Edition (J2SE) 1.4.1 for Mac OS X. Mac OS X has shipped with J2SE 1.3.1 installed from the beginning. Now J2SE 1.4.1 is available to all Jaguar (Mac OS X version 10.2) owners by using the software update at Apple's Java product page. Much of the time porting J2SE 1.4.1 to the Mac has been spent moving the GUI elements from the Carbon framework to the Cocoa framework. This means it is easier for your Java applications to take advantage of Mac OS X specific features when running on the platform and to look and feel more like native applications.

In this series we focus on tuning your cross-platform Java application so that it looks and feels more like a native application when running on Mac OS X without changing the look and feel on other platforms. In these articles, most of the examples are presented as small isolated instances of platform-specific improvements you can make. In a shipping application, you would combine these changes so as not to have to clutter your code with conditional logic of the form if you are on this platform do the following, else do this.

We continue to use the open source unit testing application JUnit for the examples. Last time we made JUnit more Mac-like by adjusting run-time properties. This time you make changes to the source code. Expand the JUnit zip file that you can freely download from the JUnit homepage. Next expand the src.jar file. You also need the com.apple.eawt package that is included with Apple's J2SE 1.4.1 distribution in the ui.jar file that you find in the Classes directory. You can get the latest Apple Java developer releases from the Apple Developer Connection by registering for free.

Eliminating JUnit's Menu on the Mac


Figure 1. The Automatically generated Application Menu

The JUnit Menu contains only two menu items: About and Exit. Mac users expect Quit and not Exit. The difference seems small but it marks your application as being built with the Mac in mind as a target platform. In general, you can accommodate the differences by using java.util.ResourceBundle to store the different names for menus and menu items the same way you would store localizations. You could also use parallel menu structures for the two platforms. In this particular case, Mac OS X includes both About and Quit in the application menu.

This menu automatically appears first in the menu bar in every application. In Part One, you set runtime properties to place the menu bar at the top of the screen and to display the application name in the Menu and Dock as well as with the About, Quit, and Hide menu items.

All menu items are fully functional. This includes the About menu item. You don't have to write any code if you want to use the default About box provided for all Java applications. It isn't very informative or attractive so you will probably want to customize it.

You display the custom About box that is included with JUnit with a few changes to your source code. The JUnit menubar code can be easily changed to detect whether or not the application is running on a Mac. If it isn't, we'll display the included JUnit menu as it is. If JUnit is running on Mac OS X then we'll customize the existing Application menu to display the custom JUnit About box. Locate the createMenus() method in the JUnit.swingui.TestRunner class.


Figure 2. The Default About Box for Java Applications


protected void createMenus(JMenuBar mb) {
    mb.add(create
             JUnitMenu());
}
          

To check whether or not you're running on a Mac, get the value of the System property mrj.version. We don't care what the value is -- just that it isn't null. All Mac JVMs have an associated mrj.version. If you check the property and null is returned then you know you aren't running on a Mac. This new version of the createMenus() method checks for the operating system and only displays the existing JUnit menu if it is not a Mac.


protected void createMenus(JMenuBar mb) {
     
                   if (System.getProperty("mrj.version") == null) {   
       mb.add(create
                   JUnitMenu());
     
                   } else {                                               
      
                       // the Mac specific code will go here           
     
                   }                                                             
}
                

Configuring the About Menu Item

JUnit has a custom About box that is displayed when the About menu item in the JUnit menu is selected. We are providing the same functionality in the Application menu by overriding the handleAbout() method so that it calls the AboutDialog class distributed with JUnit. The handleAbout() method looks like this.

public void handleAbout(ApplicationEvent event) {
    new AboutDialog(new JFrame()).show();
}

The handleAbout() method is declared in the ApplicationListener interface. We extend the ApplicationAdaptor that provides empty implementations of all of the methods. Now we only have to override the handleAbout() method. The following will be an inner class in a moment.

                    class AboutBoxHandler extends ApplicationAdapter {
     public void handleAbout(ApplicationEvent event) {
         new AboutDialog(new JFrame()).show();
     }
                    }
                

To isolate the Mac OS X specific changes from the existing JUnit code base, create the class MacOSAboutHandler in the java.swingui package. MacOSAboutHandler extends com.apple.eawt.Application. As with other event handlers you know from the Swing library, you register the AboutBoxHandler as a listener. You see in the code listing that we set this up in the constructor. You could accomplish the same thing with an anonymous inner class.

package  
                   JUnit.swingui;

import com.apple.eawt.ApplicationAdapter;
import com.apple.eawt.ApplicationEvent;
import com.apple.eawt.Application;
import javax.swing.JFrame;

                    public class MacOSAboutHandler extends Application {      public MacOSAboutHandler() {         addApplicationListener(new AboutBoxHandler());     }

    class AboutBoxHandler extends ApplicationAdapter {
        public void handleAbout(ApplicationEvent event) {
            new AboutDialog(new JFrame()).show();
        }
    }
                    }
                

Return to the TestRunner code and create an instance of MacOSAboutHandler in the createMenus() method.

protected void createMenus(JMenuBar mb) {
    if (System.getProperty("mrj.version") == null) {   
        mb.add(create
                   JUnitMenu());
     }  else {                                               
         
                      new MacOSAboutHandler();            
     }                                                              
}
                

Run this new version on a Windows box and the behavior is the same as it was before we changed any code. Run the revised version under Mac OS X and when the user selects the About JUnit menu item, they see this custom About Box.


Figure 3. The JUnit About Box

The Human Interface

Apple has put a lot of time into defining the Human Interface (HI) Guidelines for Mac OS X. Because Apple has mapped the Swing and AWT components to the Cocoa framework, compliance with many of the Human Interface Guidelines is built in as long as you use the standard widgets. In JUnit we see two examples of where the Java code and the Apple HI guidelines aren't in concert.

The first is subtle. Run JUnit and look at the test hierarchy and you'll see something like this.


Figure 4. No Vertical Scroll Bar

Expand one of the nodes and you will see something like this.


Figure 5. The Bar Appears when Needed

If the text in the underlying scroll pane occupied the entire horizontal area, then the appearance and disappearance of the vertical scroll bar would mean that different parts of the underlying pane are visible when a vertical scroll is required. The Apple HI Guidelines prefer that the vertical scroll bar always be visible like this.


Figure 6. The Bar Appears when Needed

This is less jarring for the user. The scroll bar itself is less obtrusive. Making this fix is quite easy. First ask if it is important that the scroll bar not be visible on other platforms. If so, then you'll need to query whether you're on a Mac as we did above. A quick look at the Failures tab tells you that this distinction is not important to the JUnit authors. There the vertical scroll bar is always visible. Apple's HI Guidelines are also in place so that there is a consistency in the user experience.

With that settled, making the code change is easy. Look in the constructor in JUnit.swingui.TestSuitePanel for this line.

fScrollTree= new JScrollPane(fTree); 

Change it to this.

fScrollTree= new JScrollPane(fTree,JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
                                 JScrollPane.HORIZONTAL_SCROLLBAR_ALWAYS);

The scroll bar is an example of an Apple HI Guideline that is not enforced by Apple's Java implementation.

Keeping the Bar Blue

There is a fundamental problem with running JUnit on Mac OS X that we've overlooked until now. As you've run the JUnit test suites you should have seen something like this.


Figure 7. Green is good, Red is bad, Blue is...

All of the information is here but it's not visually clear. When JUnit runs on other platforms, the progress bar is green when all tests are passing and red when a single test fails. One of the benefits of JUnit is that it is obvious when everything is passing -- the bar is green. This conflicts with the Apple HI Guidelines which don't allow you to change the color of buttons or progress bars or other widgets.

There are many ways to handle this.

  1. Create a custom widget using a JPanel that grows to fill the rectangle like a home made progress bar. This doesn't have the true Aqua dimensional effect, but it does allow you to color the bar red and green. Also, the fewer custom components we depend on, the more robust our application tends to be.
  2. Use the heavyweight widget based on AWT components found in JUnit.awtui.ProgressBar. This also won't have the correct Aqua look, but more important are the inherent problems in mixing lightweight and heavyweight components.
  3. Convince Apple to change the behavior of progress bars that are representing JProgressBars. Several bugs have been filed against this behavior but it does require a decision at Apple to vary the behavior of their standard widgets in Java applications. Java applications would again be clearly different than native applications.
  4. Reflect the color in a different component. The easiest change is to make a slight change to the way JUnit presents itself on Mac OS X and leave it the way it is on other platforms.


In the spirit of eXtreme Programming, we pursue the last option as it is the simplest thing that could possibly work. We keep the progress bar as the progress bar. On other platforms it will indicate both progress and whether the tests are passing or failing. On Mac OS X it will indicate only progress. We'll color the background of the status bar, the text field at the bottom of the screen, green or red to indicate success or failure. While introducing this new behavior, we try to make as few changes to the existing codebase as possible.

Extending the ProgressBar

Take a look at the existing JUnit.swingui.ProgressBar code.

import java.awt.Color;
import javax.swing.*;

/**
 * A progress bar showing the green/red status
 */
class ProgressBar extends JProgressBar {
        boolean fError= false;

        public ProgressBar() {
                super();
                setForeground(getStatusColor());
    }

        private Color getStatusColor() {    
                if (fError)
                        return Color.red;
                return Color.green;
        }
                
        public void reset() {
                fError= false;
                
                   setForeground(getStatusColor());
                setValue(0);
        }
        
        public void start(int total) {
                setMaximum(total);
                reset();
        }
        
        public void step(int value, boolean successful) {
                setValue(value);
                if (!fError && !successful) {
                        fError= true;
                 
                   setForeground(getStatusColor());
                }
        }
}
                

Take the two calls to setForeground() that aren't in the constructor and extract them into a method named updateBarColor().

//...
class ProgressBar extends JProgressBar { //...
                
        public void reset() {
                fError= false;
            
                    updateBarColor();
                setValue(0);
        }
        
        public void start(int total) {
                setMaximum(total);
                reset();
        }
        
        public void step(int value, boolean successful) {
                setValue(value);
                if (!fError && !successful) {
                    fError= true;
                   
                    updateBarColor());
                }
        }
        protected void updateBarColor(){
        setForeground(getStatusColor());
    }
}
                

Create a subclass of ProgressBar named MacProgressBar that changes the color of the status bar and not the progress bar.

package  
                   JUnit.swingui;

import javax.swing.JTextField;


public class MacProgressBar extends ProgressBar{
     
                   private JTextField component;

    public MacProgressBar(JTextField component) {
        super();
        
                   this.component=component;
     }

    
                   protected void updateBarColor() {         component.setBackground(getStatusColor());     }
}
                

We pass in the status bar as an argument of the constructor. The updateBarColor() method then is able to update the color of the status bar instead.

Take another look at ProgressBar. There are two more points to make.

//...
class ProgressBar extends JProgressBar {
        boolean fError= false;

        public ProgressBar() {
                super();
                 
                   setForeground(getStatusColor());
    }

         
                   protected Color getStatusColor() {    
                if (fError)
                        return Color.red;
                return Color.green;
        } //...
                

First, the access level of the getStatusColor() method must be changed from private to at least protected because it is used by the MacProgressBar subclass. Second, we can't use the updateBarColor() method call in place of the setForeground() method call in the constructor. This results in a NullPointerException because the updateBarColor() method in MacProgressBar is called before the instance of MacProgressBar exists. Leaving the setForeground() call in does no harm to MacProgressBar as a the progress bar on Mac OS X just ignores the color change.

Updating the TestRunner

Look at the createUI() method in JUnit.swingui.TestRunner. It is more than fifty lines. If our prime directive weren't to make as few changes as possible to the existing code base, we might be tempted to refactor it into smaller methods and subclass TestRunner. We take a more direct approach and leave further refactoring as an exercise.

Look in the createUI() method for this line.

fProgressIndicator = new ProgressBar();

We change it as follows.

  • Test whether or not we are running on a Mac
  • If we aren't on Mac OS X then fProgressIndicator is a ProgressBar
  • If we are on Mac OS X then fProgressIndicator is a MacProgressBar that has a handle to the fStatusLine
  • We move the instantiation of the fstatusLine before this code.


Here's the resulting code.

fStatusLine = createStatusLine();  //moved from below
if (System.getProperty("mrj.version") == null) {   
    fProgressIndicator = new ProgressBar();
}  else {                                                   
    fProgressIndicator = new MacProgressBar(fStatusLine); 
}                        

Now when you run a suite of passing tests you'll see this.


Figure 8. The JUnit Green Bar on Mac OS X

And a suite with one or more failing tests looks like this.


Figure 9. The JUnit Red Bar on Mac OS X

Summary

Your Java applications will run on a Mac, whether or not you initially targeted the platform. In these two articles you've seen that very little extra tuning is required on your part to make most applications look and feel more like native Mac OS X applications. Next you can turn your attention to performance tuning for the platform and to packaging and deploying your application.

Resources

For more information about conforming to the Aqua Look and Feel using runtime properties read the first article in this series and see the Runtime System Properties section of the Apple Java release notes.

You can get access to tech notes and previews of Apple technology by joining the Apple Developer Connection (ADC). You can also check for the latest news on Apple's Java sites for end-users.

For more articles about Java programming for the Mac OS X, check out the the O'Reilly MacDevcenter series Java Programming on the Mac.


Have a question about programming? Use Java Online Support.