Making your Swing App Mac/OSX compliant

Running your plain Vanilla Swing on a Mac, for the first time, will be quite surprising. Your app will stand out, in a bad way, among all other Mac apps because Swing's menu bar is not compliant with OS X's menu bar.

Main difficulties are as follows:
  • Your JFrame will have its own menu bar - your menu items will not be shown on the (shared) menu bar

  • Using the Command key as the standard accelerator

  • Application menu is named after the (fully qualified name of the) main class

  • Supporting the standard "Preferences..." application menu item

  • Not breaking compatibility with the other OSes (Linux/Windows)


Alvin J. Alexander has written a fairly detailed tutorial about this subject but it is somewhat outdated now. When I tried using classes from com.apple.eawt (as suggested there) Eclipse complained:

Access restriction: The type Application is not accessible due to restriction on required library /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Classes/ui.jar

(In addition, any static dependency on such Apple-specific classes will break the program when it is runs on Linux/Windows).

On the other hand, Eirik Bjørsnøs Macify library provides a good solution to this difficulty, but it does not address the first three points from above.

Here's a short program (github, zip) that shows how these issues can be completely solved. It was derived from both Alvin's and Eirik's articles. It contains Eirik's Macify library (as a .jar) and these two Java classes:

// Launcher.java
package com.blogspot.javadots.swingmac;

import javax.swing.*;
import org.simplericity.macify.eawt.*;

public class Launcher {

private static void macSetup(String appName) {
String os = System.getProperty("os.name").toLowerCase();
boolean isMac = os.startsWith("mac os x");

if(!isMac)
return;

System.setProperty("apple.laf.useScreenMenuBar", "true");
System.setProperty("com.apple.mrj.application.apple.menu.about.name",
appName);
}

public static void main(String[] args) throws Exception {
macSetup("swing-mac");
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());

SwingUtilities.invokeLater(new Runnable() {

@Override
public void run() {
Application app = new DefaultApplication();
Main main = new Main();
app.addApplicationListener(main.getApplicationListener());

app.addPreferencesMenuItem();
app.setEnabledPreferencesMenu(true);
}
});
}
}

// Main.java
package com.blogspot.javadots.swingmac;

import java.awt.Toolkit;
import java.awt.event.KeyEvent;
import javax.swing.*;
import org.simplericity.macify.eawt.*;

public class Main {

private JFrame f = new JFrame();
private MyApplicationListener listener = new MyApplicationListener();

public Main() {

JMenuBar mb = new JMenuBar();
f.setJMenuBar(mb);
JMenu m = new JMenu("File");
mb.add(m);

addItem(m, "Open", KeyEvent.VK_O);
addItem(m, "Save", KeyEvent.VK_S);
addItem(m, "Save As", KeyEvent.VK_A);
addItem(m, "Import", KeyEvent.VK_I);
addItem(m, "Export", KeyEvent.VK_E);

f.setTitle("Main");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setSize(400, 300);
f.setVisible(true);
}

private void addItem(JMenu m, String name, int accelerator) {
JMenuItem mi = new JMenuItem(name);
mi.setAccelerator(KeyStroke.getKeyStroke(accelerator,
Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()));
m.add(mi);
}

public ApplicationListener getApplicationListener() {
return listener;
}

// Must be public!!
public class MyApplicationListener implements ApplicationListener {

private void handle(ApplicationEvent event, String message) {
JOptionPane.showMessageDialog(f, message);
event.setHandled(true);
}

public void handleAbout(ApplicationEvent event) {
handle(event, "aboutAction");
}

public void handleOpenApplication(ApplicationEvent event) {
// Ok, we know our application started
// Not much to do about that..
}

public void handleOpenFile(ApplicationEvent event) {
handle(event, "openFileInEditor: " + event.getFilename());
}

public void handlePreferences(ApplicationEvent event) {
handle(event, "preferencesAction");
}

public void handlePrintFile(ApplicationEvent event) {
handle(event, "Sorry, printing not implemented");
}

public void handleQuit(ApplicationEvent event) {
handle(event, "exitAction");
System.exit(0);
}

public void handleReOpenApplication(ApplicationEvent event) {
event.setHandled(true);
f.setVisible(true);
}
}
}



Here are the key points. The order is highly important.
  1. Create a dedicated "Launcher" class to carry out the setup phase. A dedicated class ensures that setup takes place before any other UI interaction.
  2. Set system property apple.laf.useScreenMenuBar to true.
  3. Set system property om.apple.mrj.application.apple.menu.about.name to the application's name.
  4. Use Look&Feel to UIManager.getSystemLookAndFeelClassName()
  5. Use Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() to obtain an OS-correct accelerator key.
  6. Register an application listener with Macify's Application object. The listener's implementing class must be declared as public.
  7. Call addPreferencesMenuItem() and setEnabledPreferencesMenu(true) on that application object.


The resulting code will work just fine on either Mac/Linux/Windows. The Macify library uses reflection to dynamically discover OSX's runtime system and to wire your listener to it. The use of reflection allows your code to compile on any operating system and also to run fine on Linux/Windows where OSX runtime is clearly not present.

1 comments :: Making your Swing App Mac/OSX compliant

  1. Thanks. Very cool.

Post a Comment