Question

I have a cross-platform Java application with a Swing user interface. On OS X, the application uses the screen menu bar for a more native user experience.

In general, the application creates one JFrame per document. The screen menu bar must remain consistent across all of these windows. I have tried several ways of doing it, and found only one consistent and performant solution, which is adequate but not perfect. I am posting this question in case anyone else has a better approach, and with the hope that this information helps others.

Some approaches that do not work:

Attach same menu bar to multiple windows

I tried adding the same JMenuBar to multiple JFrame instances, but Swing only supports a JMenuBar being attached to a single JFrame at a time, even as a screen menu bar.

I also tested with an AWT MenuBar rather than a JMenuBar, but the same phenomenon occurs. And MenuBar has many limitations compared to JMenuBar (e.g., no icons), so let's proceed with the requirement that we want a JMenuBar.

Clone the menu bar

One common solution is to create a copy of the JMenuBar for each new JFrame. However, there are at least two problems with that. First, you must keep the menu bars in sync. While you can use listeners to do it, it is a lot of extra code just to handle the OS X platform. However, the second and more serious issue is performance: if you have a complex menu bar with hundreds of menu items, cloning the menu bar is very slow. We found that this approach delayed the appearance of new windows by several seconds!

Use a default menu bar

A new method was added to Apple's Java library in Java for OS X v10.6 Update 1 and 10.5 Update 6: Application.setDefaultMenuBar(JMenuBar).

The stated purpose of this method is to provide a menu bar when no JFrame is active, but it also shows the default menu bar when a JFrame with no JMenuBar of its own is active.

However, there are several major problems with the setDefaultMenuBar functionality:

  1. Accelerators do not work. I avoided this problem in our application by handling all key presses ourselves, but it is still unfortunate.
  2. As of December 2012, setDefaultMenuBar was still not available on Java7. We obviously want to avoid using deprecated or unsupported APIs.
  3. Most critically, calling setDefaultMenuBar prevents the JVM from shutting down properly. Even a subsequent call to setDefaultMenuBar(null) does not free the necessary resources.

In short, setDefaultMenuBar does not seem like a safe and robust way to go at all.

So, the question is: What is the most reliable, performant and compatible (across versions of OS X) way to implement a consistent screen JMenuBar?

Was it helpful?

Solution 2

You may be able to leverage JDialog, which inherits its parent's JMenuBar. To keep the dialogs modeless, you can use

  • PropertyChangeEvent to communicate among the dialogs and the main JFrame, as suggested here.

  • Action and Key Bindings to navigate among the dialogs.

OTHER TIPS

The solution I found which works sufficiently well is to listen for windowActivated events by adding a WindowListener to each window of the application. Then, set the newly activated window's JMenuBar to the one and only menu bar we want to display.

Here is an example:

import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowListener;

import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.WindowConstants;

/**
 * On OS X, with a screen menu bar, you can "hot-swap" a JMenuBar between
 * multiple JFrames when each is activated. However, there is a flash each
 * time the active window changes, where the menu bar disappears momentarily.
 * But it is a small price to pay to be able to reuse the same menu bar!
 */
public class HotSwapJMenuBarOSX {

  public static void main(final String[] args) {
    System.setProperty("apple.laf.useScreenMenuBar", "true");

    final JMenuBar menuBar = new JMenuBar();
    final JMenu file = new JMenu("File");
    menuBar.add(file);
    final JMenuItem fileNew = new JMenuItem("New");
    file.add(fileNew);

    final JFrame frame1 = new JFrame("First");
    frame1.getContentPane().add(new JButton("First"));
    frame1.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

    final JFrame frame2 = new JFrame("Second");
    frame2.getContentPane().add(new JButton("Second"));
    frame2.setDefaultCloseOperation(WindowConstants.DISPOSE_ON_CLOSE);

    // hot-swap the menu bar to newly activated windows
    final WindowListener listener = new WindowAdapter() {
      @Override
      public void windowActivated(WindowEvent e) {
        ((JFrame) e.getWindow()).setJMenuBar(menuBar);
      }
    };
    frame1.addWindowListener(listener);
    frame2.addWindowListener(listener);

    final int offsetX = 200, offsetY = 50;
    frame1.pack();
    frame1.setLocation(offsetX, offsetY);
    frame1.setVisible(true);
    frame2.pack();
    frame2.setLocation(frame1.getWidth() + offsetX + 10, offsetY);
    frame2.setVisible(true);
  }

}

With this approach, both frames show the same menu bar, and when both frames are gone the JVM exits cleanly without needing to explicitly call System.exit(int).

Unfortunately, this approach is not perfect: the menu bar disappears briefly each time the active window changes. Anyone know a better way?

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top