Overview

Tweaking the Menu Bar of JavaFX 8 Applications on OS X

8 Comments

JavaFX provides a simple means to create platform independent applications with a graphical UI. This platform independence, however, usually comes at a price. As the compiled code is supposed to run on all supported operating systems, JavaFX does not support all unique operating system specific UI elements. This is usually not a big problem, but it can be rather annoying in certain cases.

On OS X, the application’s menu bar is usually detached from the application’s main window and shown at the very top of the desktop instead. In contrast to the regular menu of a JavaFX application, this menu bar always contains the Apple menu and something that is called application menu. This application menu is created for every application regardless of whether it uses an own menu bar or not and contains items to show, hide and quit the application. Native Mac OS applications also use the application menu for example for preferences, the about menu and other application related menu items. Unfortunately, JavaFX does not provide any means to access this menu let alone adding new custom items to it. This can be particularly annoying when menu items are labeled โ€œJavaโ€ or contain the entire package name of your main class.

In this blog post, I describe how to access the application menu using Cocoa native bindings from Eclipse SWT to modify default application menu items and adding new custom items. With the resulting code, JavaFX applications are able to use the default Mac OS menu bar just like any native application.

The following example shows how to add a menu bar to a JavaFX application. It also sets the useSystemMenuBarProperty to true in order to put the menu bar at the very top of the screen instead of adding it to the application window.

@Override
public void start(Stage primaryStage) throws Exception {
  MenuBar menuBar = new MenuBar();
  menuBar.useSystemMenuBarProperty().set(true);
 
  Menu menu = new Menu("java");
  MenuItem item = new MenuItem("Test");
 
  menu.getItems().add(item);
  menuBar.getMenus().add(menu);
 
  primaryStage.setScene(new Scene(new Pane(menuBar)));
  primaryStage.show();
}

The problem with that code is that it does not affect the automatically generated application menu in any way. The menu items are just added to the right of the already existing application menu. Therefore, I created a small tool called NSMenuFX on Github that provides access to the generated menu bar using Eclipse SWT. For convenience, it allows for converting the auto generated Mac OS menu bar into a JavaFX MenuBar. This menu bar can then be modified just like any JavaFX menu bar and converted back to a Mac OS native NSMenuBar. In the following, I outline how to use this tool and how it uses Cocoa native bindings to provide the described functionality.

Accessing the auto generated menu bar with NSMenuFX is fairly simple, as outlined in the following example:

NSMenuBarAdapter adapter = new NSMenuBarAdapter();
MenuBar menuBar = adapter.getMenuBar();

The first Menu of menuBar now contains the application menu with all its default items. To access this menu, NSMenuBarAdapter uses the NSApplication class of Eclipse SWT. This is pretty much straight forward as NSApplication provides direct access to the application’s menu bar by calling

NSMenu mainMenu = NSApplication.sharedApplication().mainMenu()

The slightly more tricky part starts by converting the NSMenu to a JavaFX MenuBar, since the two concepts are slightly different. In JavaFX, there is one MenuBar object containing several Menus whereas each Menu can contain MenuItems or further Menus. As depicted in the example above, Cocoa does not have the concept of a menu bar object but uses a NSMenu object instead. This menu solely consists of NSMenuItems, whereas each of this items has a title and an optional submenu. The title of these items is what is shown in the OS X menu bar whereas the submenus are the menus that open when you click on them. The submenus also contain NSMenuItems with, again, optional submenus. To shine a bit more light on the Cocoa Menu structure, consider the following code snippet that loops over the items of the OS X menu bar. It uses the ToJavaFXConverter to convert submenus to the respective JavaFX Menu classes and adds them to a MenuBar.

NSArray itemArray = mainMenu.itemArray();
for (long i = 0; i < itemArray.count(); i++) {
  NSMenuItem item = new NSMenuItem(itemArray.objectAtIndex(i));
  bar.getMenus().add(toJavaFX.convert(item.submenu()));
}

The most interesting part of this code snippet is that the itemArray does not contain the actual NSMenuItems but only a sequence of Cocoa native object IDs. To bind a Java object to the respective native object, this object id must be passed in to the constructor. The convert method that is called subsequently then recurses through the submenu and converts all elements to their JavaFX counterparts.

Most aspects of the menu item conversion, like the title or the accelerator are pretty much straight forward and can be, more or less, directly translated. The menu itemโ€™s click action is, however, a bit more complicated as Cocoa uses selectors to call a given method whereas JavaFX uses EventHandler objects that are assigned to the menu item and called in case they are clicked. If you are not familiar with Objective-C, selectors can be loosely thought of as reflections defining method names that should be called on a given object. For the conversion from Cocoa to JavaFX, NSMenuBarAdapter creates a dummy EventHandler containing the selector address as well as a reference to the object on which the selector should be executed.

A bit more challenging is the conversion from JavaFX to Cocoa, as it requires the conversion from EventHandler objects to Objective-C selectors. Therefore, a new selector can be created at runtime by calling OS.sel_registerName(String name). By default, selectors are invoked on the delegate object of the application, which is an instance of SWTApplicationDelegate. To enable SWTApplicationDelegate to respond to the new selector, a respective method can be added at runtime as outlined in the following snippet taken from ToCocoaConverter. The implementation of the method is provided by a Callback object, which is called from Objective-C.

Callback callback = new Callback(new MenuHookObject(handler), "actionProc", 3);
long delegateClass = OS.objc_lookUpClass("SWTApplicationDelegate");
OS.class_addMethod(delegateClass, selector, callback.getAddress(), "@:@");

The constructor of the callback object takes a Java object, the name of the method that should be called and the number of parameters. As depicted above, MenuHookObject.actionProc takes three arguments, i.e. a reference to its own object, a reference to the selector as well as a reference to the calling object. The same parameters are used for the method that is added to the delegate object as indicated by the signature @:@ (@ represents a NSObject reference, : represents a selector reference). When actionProc is called, MenuHookObject just calls the JavaFX EventHandler that was passed in to its constructor.

By being able to convert a JavaFX MenuBar back to a Objective-C Menu, the auto generated menu bar can simple be replaced with a modified or even entirely new version of the menu bar. The following example shows how to use NSMenuFX to add an “About” menu to the generated application menu.

    // Create a new menu item
    MenuItem about = new MenuItem("About");
    about.setOnAction(new EventHandler<ActionEvent>() {
	@Override
	public void handle(ActionEvent event) {
	    // Open a new JavaFX dialog
	}
    });
 
    // Add the menu item as first element to the application menu
    menuBar.getMenus().get(0).getItems().add(0, about);
 
    // Update the menu bar
    adapter.setMenuBar(menuBar);

I hope this article provides you with some interesting insights about Cocoa native bindings and how to use them to tweak the Mac OS menu bar items from JavaFX. Feel free to contact me directly if you have any further questions or leave a comment right below this post.

Kommentare

  • 1. May 2015 von Nico

    That’s great. Are you planning to make NSMenuFX available on Maven Central? That would be even better …

    • 6. May 2015 von Jan Gassen

      Hi! Yes, it’s planned but I’d like to add some code to make it easier to work with NSMenuFX on other operating systems. I’m hoping to finish that within the next one or two weeks.

  • 12. May 2015 von Nico

    So far, I’ve got two issues I’m struggling with:
    1. Am I able to force the application title, i.e. the leftmost application menu, to be printed in bold (e.g. when using NSMenuBarAdapter#renameApplicationMenu(String))? Currently, it isn’t as opposed to other Mac applications’ menu bars.
    2. MenuItem#setDisable(boolean) has no effect as soon as I modify the menu bar by means of NSMenuBarAdapter.

    • 13. May 2015 von Jan Gassen

      Hi,

      regarding the bold text, I’m afraid there’s nothing (at least that I know of) you can do from Java code. What you can do is do set `CFBundleName` in a bundled Info.plist or use `-Xdock:name=AppTitle` when starting the application (although I’ve been never able to make that work…). There are ways to use bold text for menu items but unfortunately not for the menu title, sorry.

      Regarding the disable property, I’m not entirely sure if I understood the problem correctly. However, you discovered a bug that the disabled property was not converted from Cocoa to JavaFX. So if you set an item to disabled and transform the menubar back and forth, the item would have been enabled again. This is fixed now, so it probably already solves your issue. Otherwise, I’d be happy to support you with that, if you’d provide me with some more details.

      • 13. May 2015 von Nico

        Wrt. 1: Thanks for your explanation.

        Wrt. 2: Sorry for not being precise. Maybe I’ll try to explain it using your test classes: In JavaFXDefault (which doesn’t make use of NSMenuBarAdapter) I can add item.setDisable(true); which results in disabling of the particular menu item. However, inserting item3.setDisable(true); in SampleMenuBar (which uses NSMenuBarAdapter) has no effect.

        • 13. May 2015 von Jan Gassen

          Thanks for the hint, it’s fixed now. The problem was that Cocoa automatically enables menu items if possible by default. Fortunately, one can turn that off ๐Ÿ˜‰

Comment

Your email address will not be published. Required fields are marked *