Improving Groovy Console’s GUI

Groovy is a very appealing language that I really enjoy using from time to time. Unfortunately, as many other languages, Groovy lacks decent development environments. Even though this shortcoming is being addressed by the Eclipse and IntelliJ, the Groovy Console remains a valuable tool to quickly test an expression or a script.

The console works fine and achieve its purpose really well. However, its GUI is somewhat lacking and integrates badly with Mac OS X. The following screenshot shows what the ordinary Groovy Console looks like on Mac OS X:

Groovy Console on Mac OS X

Mac OS X users will easily spot the problems with this user interface. The menu bar belong to the window rather than to the screen and the shortcuts are wrong (using Ctrl instead of Cmd.) Also, not shown in the screenshot, the About and Quit menu items are not in the appropriate location. After a few tweaks, here is the new UI:

New Groovy Console on Mac OS X

There are three steps required to achieve this kind of integration with Mac OS X, two of them which you are probably already aware of. First, you need to tell Apple’s VM to move the menu bar to the top of screen, out of the application’s frame. At this stage, you also need to give your application its real name, otherwise Mac OS X will use the main class’ fully qualified name (in this case, groovy.ui.Console.) The Groovy code to achieve this is the following:

System.setProperty("apple.laf.useScreenMenuBar", "true")
System.setProperty("com.apple.mrj.application.apple.menu.about.name", "GroovyConsole")
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())

Make sure to set the properties before you initialize any frame and you will get the desired result. The second part of our “Mac OS Xification” is to use the appropriate shortcut keys in the menus. The java.awt.Toolkit class offers the getMenuShortcutKeyMask() method which returns the system’s default shortcut modifier (Ctrl on Windows and Linux, Cmd on Mac OS X.) Again, using this in your application is quite simple:

def createShortcutWithModifier = { key, modifier -> KeyStroke.getKeyStroke(key,
    Toolkit.getDefaultToolkit().getMenuShortcutKeyMask() | modifier) }
def createShortcut = { key -> createShortcutWithModifier(key, 0) }

// Uses Cmd-Shift-N/Ctrl+Shift+N
def newWindowAction = action(
    name:'New Window', closure: this.&fileNewWindow, mnemonic: 'W',
    accelerator: createShortcutWithModifier(KeyEvent.VK_N, KeyEvent.SHIFT_DOWN_MASK)
)

// Uses Cmd-O/Ctrl+O
def openAction = action(
    name:'Open', closure: this.&fileOpen, mnemonic: 'O',
    accelerator: createShortcut(KeyEvent.VK_O)
)

In this sample code, we just defined Swing actions with correct menu shortcuts. Creating a menu bar with those actions is easy with Groovy’s SwingBuilder. Building the menus is also the opportunity for us to introduce the last trick regarding Mac OS X integration: handling the About and Quit menu items correctly.

def isMac = { System.getProperty("mrj.version") != null }

menuBar {
    menu(text:'File', mnemonic: 'F') {
        menuItem() { action(newFileAction) }
        menuItem() { action(newWindowAction) }
        menuItem() { action(openAction) }
        separator()
        menuItem() { action(saveAction) }
        if (!isMac()) {
            separator()
            menuItem() { action(exitAction) }
        }
    }
    // More menus...
}

This piece of code does not add the exit menu item when the application is running on Mac OS X. Every Mac OS X application shows this menu item in the system controlled menu named after the application itself. This menu appears in bold on the second screenshot. The main problem is to figure out to hook our code to this menu. In my modified version of the Groovy console, here is how I did it:

if (isMac()) {
    ConsoleMacOsSupport.handleMacOs(this.&exit, this.&showAbout)
}

The isMac() could be avoided if ConsoleMacOsSupport was cleverer, but I decided to go with a simple yet effective implementation. As you can see, this class exposes a static method to which you can pass two method references. The methods bound that way will be invoked by the system when the user clicks either the About or Quit menu item in the application’s menu. The support class is also written in Groovy:

package groovy.ui

import com.apple.mrj.*

class ConsoleMacOsSupport implements MRJQuitHandler, MRJAboutHandler {
    static initialized = false
	
    def quitHandler
    def aboutHandler

    public void handleAbout() {
        aboutHandler()
    }

    public void handleQuit() {
        quitHandler()
    }

    public static void handleMacOs(quit, about) {
        if (!initialized) {
            initialized = true
            def handler = new ConsoleMacOsSupport(quitHandler:quit, aboutHandler:about)
            MRJApplicationUtils.registerAboutHandler(handler)
            MRJApplicationUtils.registerQuitHandler(handler)
        }
    }
}

ConsoleMacOsSupport simply delegates two Apple VM’s specific handlers to our Groovy methods. Since Groovy won’t even bother with this class at runtime if it’s not required, it is safe to keep it in the Windows and Linux distribution. This kind of work would usually be achieved through reflection in Java to avoid compile-time headaches.

Groovy makes the use of those handles even sweeter thanks to optional method parameters. For instance, Mac OS X’s quit handler does not expect any argument when invoked. However, we would like to bind the same method to a regular JMenuItem when the application is executed on Linux or Windows. In this case we are actually writing an ActionListener, and a parameter of type EventObject is therefore expected. Groovy is here to make it simple:

// The action's closure (action listener) is bound to the exit(EventObject) method
def exitAction = action(
    name:'Quit', closure: this.&exit, mnemonic: 'Q',
    accelerator: createShortcut(KeyEvent.VK_Q)
)

// The MRJQuitHandler is bound to the exit() method
ConsoleMacOsSupport.handleMacOs(this.&exit, this.&showAbout)

// Optional evt argument
void exit(EventObject evt = null) {
    // Cleanup, etc.
}

Even though I did not used this feature in the Groovy Console, you can also install a handler to hook into Mac OS X’s Preferences menu items. If your application offers some sort of options panel or configuration dialog, make sure to bind it to this menu item because that’s where users expect to find this feature. Last but not least, all those examples work also in pure Java and should not required much work to transpose.

11 Responses to “Improving Groovy Console’s GUI”

  1. Great post Romain! Perhaps some on these tricks may be added as an SwingBuilder extension?

  2. Robert says:

    Did you tell the Groovy folks? Please?

  3. Romain Guy says:

    Yes, it’s been posted on their mailing list.

  4. I thought MRJ was EOLĀ“d. I use com.apple.eawt.Application hooked up via reflection. That way the Apple JAR file is not on my build path.

    http://developer.apple.com/samplecode/OSXAdapter/index.html

    I also use isMac = (os.name == “Mac OS X”) and isUsingAppleLAF = UIManager.getLookAndFeel().getName().indexOf(“Mac OS X”) != -1 because of EAWT.

    Your status bar could override getPreferredSize and return a fixed height on OS X while the font size should be -2 derived. That way the grabber is as tall as the status bar. I know that this is potentially not Leopard/DPI/PPI/future proof.. but that time.. that sorrow.

  5. Romain Guy says:

    Thanks for the pointer to the Application stuff. I’ll take a look. I didn’t bother with checking the look and feel since I explicitly set it to be Apple’s.

    Good point about the status bar too.

  6. “That way the Apple JAR file is not on my build path.”

    Ignore that sentence. You need the JAR file to create the adapter, but use reflection to hook it up. End pedantic :)

  7. Mark Stewart says:

    Fingers crossed that the fine Groovy folk will add some of this for 1.1 final.

    The screenshot links are broken, btw.

  8. sountee says:

    Very good forum! Good info!
    http://unikont.com
    The Author, you simply – super hero!

  9. bajm euysobxa fpyhrxik vxbrphmwt xrhivwpmb lwaj oukryhivf

  10. hey guys, i just came here when i did an quick yahoo search. Fine website you got here! Keep it up!