Making your Swing App Mac/OSX compliant

5 comments
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.

Top Ten Getting-Started-With-Dojo Tips

7 comments
You heard good things about the Dojo library. What is the absolute minimum you need to know in order to start coding effectively with Dojo?

Discalimer: This post does not claim that Dojo is better than JQuery nor the converse. Each library has its strengths. My personal view is that JQuery offers a well designed programming model (the $("selector") thing is ingenious). On the other hand, Dojo currently offers a more extensive set of standard widgets.


#1: Importing Modules

dojo.xd.js is the basic module. Once you imported it into your page via <script src=".."> you can use dojo.require('fully.qualified.name') to import additional Dojo modules.

<html>
<head>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dijit.form.Button");
</script>
</head>
<body/>
</html>


#2: addOnLoad()

addOnLoad() lets you register a function that will be called once page loading is finished. This is Dojo's cross-browser-compatible way to hook the onLoad event.

<html>
<head>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>

<script type="text/javascript">
dojo.addOnLoad(function() { alert("hi"); })
</script>
</head>
<body/>
</html>


#3: Widget creation -- Programmatic Style

There are two ways to create widgets: Programmatically (shown here) and declaratively (see tip #5). Either way, you must first import the corresponding module via a require() call.

In the programmatic you create a widget by calling its constructors, which typically takes two parameters:

  • options: a plain Javascript object specifying widget-specific options

  • id: The ID of a DOM node which will host this new widget


In the example below, a button widget is created via new dijit.form.Button({}, "click-me-button"), which means: no options; ID of hosting element is "click-me-button".


<html>
<head>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dijit.form.Button");
dojo.addOnLoad(function() {
var button = new dijit.form.Button({}, "click-me-button");
button.attr("label", "Click Me");
});
</script>
<link rel="stylesheet" type="text/css"
href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"/>
</head>
<body class="claro">
<div id="click-me-button"/>
</body>
</html>


#4: Overriding a Widget's Methods

Any method defined in the constructor's first parameter will be attached to the newly created widget, thereby overriding an existing method with the same name. The code below overrides the onClick method of the Button widget.

Initial values for the widget's properties can be specified in a similar manner: { label: "Click me" }


<html>
<head>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dijit.form.Button");
dojo.addOnLoad(function() {
new dijit.form.Button({
onClick: function() { alert("Thank you!"); },
label: "Click me!"
}, "click-me-button");
});
</script>
<link rel="stylesheet" type="text/css"
href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"/>
</head>
<body class="claro">
<div id="click-me-button"/>
</body>
</html>


#5: Widget creation -- Declarative Style

The declarative style lets you define widget by using HTML markup. To enable this you MUST specify djConfig="parseOnLoad: true" at the <script src="dojo.xd.js"> element.

You can then use a dojoType="dijit.form.Button" HTML-attribute to tell the Dojo parser to create a button widget that will be hosted by the enclosing HTML element.

A nested <script type="dojo/method" event="onClick" args="evt"> element will define a callback method for the onClick event. A nested <script type="dojo/connect"> element will define code that will be executed when the widget is created.


<html>
<head>
<script djConfig="parseOnLoad: true" type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dijit.form.Button");
</script>

<link rel="stylesheet" type="text/css"
href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"/>

</head>
<body class="claro">
<div dojoType="dijit.form.Button">
<script type="dojo/connect">
this.attr("label", "Click Me!");
</script>
<script type="dojo/method" event="onClick" args="evt">
alert("Thank you!");
</script>
</div>
</body>
</html>


#6: Defining widget variables -- Declarative Style

If you add a jsId="myButton" attribute to an HTML element that defines a Dojo widget (i.e. has a dojoType attribute) the Dojo parser will assign the widget to a global variable named myButton.

This allows programmatic access to a declaratively-defined widget.


<html>
<head>
<script djConfig="parseOnLoad: true" type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dijit.form.Button");
dojo.addOnLoad(function() {
alert("Press OK to change style");
myButton.attr("style", "color:red; font-weight:bold;");
});
</script>

<link rel="stylesheet" type="text/css"
href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"/>

</head>
<body class="claro">
<div dojoType="dijit.form.Button" jsId="myButton">
A simple button
</div>
</body>
</html>


#7: Obtaining the associated DOM node

Widgets and DOM nodes are distinct objects. In order to get the DOM node associated with a widget, use the widget's .domNode property.

                
<html>
<head>
<script djConfig="parseOnLoad: true" type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dijit.form.Button");
dojo.addOnLoad(function() {
myButton.domNode.innerHTML = myButton.domNode.innerHTML.bold();
});
</script>

<link rel="stylesheet" type="text/css"
href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"/>

</head>

<body class="claro">
<div dojoType="dijit.form.Button" jsId="myButton">
A simple button
</div>
</body>
</html>


#8: Looking up a DOM node

dojo.byId("someId") is Dojo's cross-browser-compatible way to obtain a DOM node by its ID.


<html>
<head>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.addOnLoad(function() {
dojo.byId("some.div").innerHTML = "found it!";
});
</script>
</head>
<body>
<div id="some.div"/>
</body>
</html>


#9: The Widget -> Model -> Store Pattern

Sophisticated Dojo Widgets, such as the Tree or the DataGrid widgets rely on the following structure:

The widget observes a model which observes a data store which maintains the actual data.

There are several (predefined) models that can work with each widget. The Tree widget, for example, can work with either a TreeStoreModel or a ForestStoreModel. These models can work with stores such as ItemFileWriteStore or ItemFileReadStore.


<html>
<head>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dojo.data.ItemFileWriteStore");
dojo.require( "dijit.Tree" );

function initPage() {
var store = new dojo.data.ItemFileWriteStore({ data:
{
identifier: 'id',
label: 'name',
items: [
{ id: 1, name: 'Star Wars Saga', root: true,
children:[{_reference: 2}, {_reference: 3}, {_reference: 4}] },
{ id: 2, name: 'Star Wars' },
{ id: 3, name: 'The Empire Strikes Back' },
{ id: 4, name: 'Return of the Jedi' },
]
}
});

var treeModel = new dijit.tree.ForestStoreModel({
store: store,
query: { 'root': true }
});

var widget = new dijit.Tree({model: treeModel, showRoot: false }, "div-tree")
}

dojo.addOnLoad(initPage);
</script>
<link rel="stylesheet" type="text/css"
href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"/>
</head>

<body class="claro">
<div id="div-tree"></div>
</body>
</html>


#10: Change the Widget's Content by Mutating its Data Store

When you're dealing with Widget-Model-Store setup and you want to change the content displayed by the widget, the correct way to do it is to change the data store object. These changes will be propagated along the observation chain and will eventually be reflected at the UI.

Obviously, the store object must support mutations. Read-only stores (such as: ItemFileReadStore) will not work for you, here.

The code below invokes store.deleteItem() when the "Delete 'Return of the Jedi'" button is clicked. The associated Tree widget is automagically updated.


<html>
<head>
<script type="text/javascript"
src="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dojo/dojo.xd.js">
</script>
<script type="text/javascript">
dojo.require("dijit.form.Button");
dojo.require("dojo.data.ItemFileWriteStore");
dojo.require( "dijit.Tree" );

function initPage() {
var store = new dojo.data.ItemFileWriteStore({ data:
{
identifier: 'id',
label: 'name',
items: [
{ id: 1, name: 'Star Wars Saga', root: true,
children:[{_reference: 2}, {_reference: 3}, {_reference: 4}] },
{ id: 2, name: 'Star Wars' },
{ id: 3, name: 'The Empire Strikes Back' },
{ id: 4, name: 'Return of the Jedi' },
]
}
});

var treeModel = new dijit.tree.ForestStoreModel({
store: store,
query: { 'root': true }
});

var widget = new dijit.Tree({model: treeModel, showRoot: false }, "div-tree");

new dijit.form.Button({
label: "Delete 'Return of the Jedi'",
onClick: function() {
store.fetchItemByIdentity({ identity: 4, onItem: function(item) {
store.deleteItem(item);
} });
}
}, "div-button");
}

dojo.addOnLoad(initPage);
</script>
<link rel="stylesheet" type="text/css"
href="http://ajax.googleapis.com/ajax/libs/dojo/1.5/dijit/themes/claro/claro.css"/>
</head>

<body class="claro">
<div id="div-button"></div>
<div id="div-tree"></div>
</body>
</html>