3

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?

ctrueden
  • 6,751
  • 3
  • 37
  • 69
  • What do you mean about keeping the menus in sync? The technique I'm using is to create a brand new copy each time, and I'm not experiencing this problem yet, so I am curious as to what I might not have noticed. – Hakanai Feb 13 '14 at 05:37
  • @Trejkaz For small menus, copying may be OK. But when you have many menu items, there is substantial memory & time overhead to making a new copy for each new `JFrame`. As I said above, with ~500 menu items in the tree, appearance of new `JFrame`s is delayed by seconds! As for the synchronization issue, it matters if your menu structure ever changes: A) you add a new menu item; B) you remove a menu item; C) you rename a menu item; D) you use `JCheckBoxMenuItem` or `JRadioButtonMenuItem`, since those have associated state which may need to be kept synchronized across the different `JFrame`s. – ctrueden Feb 20 '14 at 21:31
  • I wondered if you were talking about check boxes and radio buttons yeah. In those cases, you would share the ButtonModel between the two menus so that Swing itself is already keeping them in sync. I guess we're not up to 500 menu items yet, but I wouldn't call it small either. 100 ~ 200? It doesn't take too long if you already have the Action objects in an action map and are just looking them up. That said, it seems this issue has finally been fixed, so it might just be possible to use the default menu bar anyway. – Hakanai Feb 20 '14 at 22:21
  • @Trejkaz What do you mean by "this issue has finally been fixed"? Do you mean that Java 7 now supports `setDefaultMenuBar`? Do you have a link to a relevant issue or article? – ctrueden Feb 20 '14 at 23:19
  • http://bugs.java.com/view_bug.do?bug_id=8022667 says fixed in 7u60. I don't know when it's out, but the early access is available now. The commits for the fix were about half a year ago... – Hakanai Feb 24 '14 at 02:34
  • @Trejkaz Thanks! I'd be interested to hear comments from anyone who tries this out. In particular, I wonder whether the problems I mention above (non-working accelerators and JVM shutdown blocking) have been resolved with this fix. – ctrueden Feb 24 '14 at 22:32
  • Still waiting for 7u60 to come out (checking literally daily), but Java 8 is out and it appears to fix both issues for me. – Hakanai May 14 '14 at 01:05

2 Answers2

3

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?

ctrueden
  • 6,751
  • 3
  • 37
  • 69
  • 1
    While your solution kind of works, personnally I would find it unsatisfying. The best solution is to go for an MVC design for your menus. Create a "Menu-model" which centralizes your menu hiearchy, your actions, your shorcuts and everything. Create the proper views which displays that model. Then, for each frame, create a different view but based on the same model. When model changes, the view will change everywhere. Read also [The Use of Multiple JFrames, Good/Bad Practice?](http://stackoverflow.com/a/9554657/928711). – Guillaume Polet Apr 23 '13 at 18:05
  • @GuillaumePolet: Thanks for your suggestion. Actually, my application does exactly that: https://github.com/imagej/imagej/blob/2dcd8e4d/core/core/src/main/java/imagej/menu/ShadowMenu.java#L63. It is rather obsessively MVC, actually. Unfortunately, ultimately one must still create a `JMenuBar` and attach it to each Swing window. Can you elaborate on how MVC could sidestep this issue? – ctrueden Apr 23 '13 at 18:36
  • @GuillaumePolet: As for the use of multiple `JFrame`s being bad practice, the context of that link is very specific to the question it answers, and I think inapplicable here. Anyway, for compatibility and usability, our application is modeled after a previous version with a multi-window (i.e., SDI) design, which we must preserve. (We do also have an MDI implementation, though.) On a broader level, I disagree that using multiple windows is "bad, bad practice"; even OS X itself ships with many multi-window applications: Safari, Terminal, TextEdit, Preview, etc. – ctrueden Apr 23 '13 at 18:46
  • @trashgod: Like I said, we have an MDI UI as well which uses `JInternalFrame`. But we still need an SDI version with multiple windows as well. But using modeless dialogs is a very interesting suggestion; I hadn't realized that a `JDialog` inherits its parent's `JMenuBar`. Unfortunately, this approach results in all the child dialogs lacking taskbar entries on Windows, so e.g. you cannot Alt+Tab between windows anymore, which may not be an acceptable tradeoff. – ctrueden Apr 23 '13 at 22:04
  • @ctrueden: Interesting; on OS X, command-tab switches among applications but not multiple frames, for [example](http://stackoverflow.com/a/3245805/230513); I use `Action` & key bindings to switch among modeless dialogs. Sorry I overlooked this earlier. – trashgod Apr 24 '13 at 15:12
  • @trashgod: The more I think about using `JDialog`s, the more I like it... but only on OS X. I will take a crack at updating our UI to use `JDialog` windows on OS X but keep using `JFrame` windows on other platforms. I think this will provide the behavior our users expect cross-platform. If you post an answer about using modeless `JDialog`s I will accept it, since I think it is better than my answer's `windowActivated` hack. By the way, in case you didn't know: you can cycle between windows of an app on OS X using Cmd+backtick. :-) – ctrueden Apr 24 '13 at 15:28
  • @ctrueden: Ah, my cerebellum knows about command-backtick, but I may have forgotten. :-) – trashgod Apr 24 '13 at 16:18
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.

Community
  • 1
  • 1
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
  • 1
    Thanks, I like this solution better than the `windowActivated` approach because the menu bar stays consistent, with no flash or delay when switching active windows. And this approach should work broadly across different OS X versions. However, the behavior of dialogs on other platforms such as Windows may be undesirable (e.g., dialogs do not have taskbar entries). So a mixed approach of using `JDialog` on OS X and `JFrame` on other platforms may be needed in some cases. Still, this is the best solution I have seen so far. – ctrueden Apr 24 '13 at 16:24