Thursday, November 05, 2009

Griffon Plugins and Addons

In this post, I'm going to talk a bit about Griffon plugins and addons--what they are, why they're useful, and how you can create your own.  As an example, I'll walk through the steps used to create the recently released Griffon Mail Plugin.

What are Plugins and Addons?

  • Plugins provide a mechanism to extend your Griffon application with new functionality at build-time.  They are commonly used to integrate external libraries, add new builders, and support testing tools.  The full list of Griffon plugins is available here.
  • Addons are a new feature in Griffon 0.2 that provide mechanism to extend your Griffon application with new functionality at run-time.  Addons can add items to builders, add MVC groups, and add/respond to runtime events.  An example of this is the Guice plugin, which also bundles an addon that starts up the Guice framework and injects services into your controllers as they are created.  Another example is the REST plugin which uses and addon to add new methods to controllers as they are created.
Plugins and Addons are conceptually very similar.  Both are about adding new functionality and you will often see plugins and addons on working hand in hand.

Creating your own Plugins and Addons
Creating your own Griffon plugins and addons is not too difficult.  We'll walk through the process by re-creating the Griffon mail plugin.  So without further ado, lets get started:

Step 1: Create the plugin
griffon create-plugin mail

Step 2: Edit the plugin metadata
cd mail
edit MailGriffonPlugin.groovy which Griffon created for you:
class MailGriffonPlugin {
    def version = 0.1
    def canBeGlobal = false
    def dependsOn = [:]

    // TODO Fill in your own info
    def author = "Josh Reed"
    def authorEmail = "jareed@andrill.org"
    def title = "Send email from your Griffon app"
    def description = 'Send email from your Griffon app'

    // URL to the plugin's documentation
    def documentation = "http://griffon.codehaus.org/Mail+Plugin"
}

Step 3: Add our plugin functionality
What you do here will depend on your plugin, but in this case we need to add the mail.jar file from the JavaMail API site to the plugins lib/ directory.  When the user installs our plugin, this jar file will be added to the application's classpath.

The JavaMail API requires a bit of work to send emails, so we'll also add a helper class that hides this code behind a single static method call.

Step 4: Package your plugin so you can test it
When you're done with your plugin, you will want to test it out.  To do this:
griffon package-plugin
This will compile your plugin and package it as a zip file called griffon-mail-0.1.zip.  You can then install this plugin in a regular Griffon application with a:
griffon install-plugin griffon-mail-0.1.zip

For the mail plugin, we can send an email by calling our helper class:
MailService.sendMail(mailhost: 'smtp.server.com', to: 'jareed@andrill.org'...)
from our controller or wherever.  This is OK but we can do one better by creating an Addon that injects the sendMail method into every controller.

Step 5: Creating an Addon
Head back to our plugin directory and issue a:
griffon create-addon mail
this will create a MailGriffonAddon.groovy file.
(Note: if you try this with 0.2 you may run into an error about 'createMVC'  This is a bug that has been fixed, but you can fix it yourself by editing your $GRIFFON_HOME/scripts/CreateAddon.groovy file.  The last line should say 'createAddon' instead of 'createMVC')

This is where we do our runtime magic:
import griffon.util.IGriffonApplication

class MailGriffonAddon {
    private IGriffonApplication application
 
    // add ourselves as a listener so we can respond to events
    def addonInit = { app ->
        application = app
        app.addApplicationEventListener(this)
    }
 
    // inject the 'sendMail' into our 
    def onNewInstance = { klass, type, instance ->
        def types = application.config.griffon?.mail?.injectInto ?: ["controller"]
        if (!types.contains(type)) return
        instance.metaClass.sendMail = { Map args -> MailService.sendMail(args)}
    }
}

With this done, we can: griffon package-plugin and install the plugin into our test app: griffon install-plugin griffon-mail-0.1.zip
With the addon in place, we can reference the sendMail() method directly on our controller.

Step 6: Release the plugin
If you have write access to the Griffon Subversion, you can release your plugin with a:
griffon release-plugin
If you don't have write access, ask on the griffon-dev list and someone can help you out.

I hope this demystifies the plugin and addon building process.

Wednesday, October 14, 2009

Multi-Document Griffon Applications

In this post we're going to build the skeleton of a multi-document (tabbed) Griffon application.  Tabbed interfaces are extremely common.  You're probably even viewing this in a web browser with a few open tabs as we speak. We'll be implementing the document-handling logic, so all you will need to do is add your own application-specific logic.

Let's dive in by creating a new Griffon application and creating a Document MVC group:
griffon create-app MultiDocumentExample
cd MultiDocumentExample
griffon create-mvc Document

In our application model, MultiDocumentExampleModel.groovy, we need to add some properties to keep track of open and active documents. We also need a separate 'state' class (more on that later):
import groovy.beans.Bindable

class MultiDocumentExampleModel {
    @Bindable Map activeDocument = null
    List openDocuments = []
    DocumentState state = new DocumentState()
}

@Bindable class DocumentState {
    boolean isDirty = false
}

Next up is the view: MultiDocumentExampleView.groovy. For this minimal example, we'll define some common actions and a tabbed pane for displaying the documents, but you can customize it to suit your application:
actions {
    action(
        id: 'newAction', 
        name:'New', 
        accelerator: shortcut('N'), 
        closure: controller.newAction
    )
    action(
        id: 'openAction', 
        name:'Open', 
        accelerator: shortcut('O'), 
        closure: controller.openAction
    )
    action(
        id: 'closeAction', 
        name:'Close', 
        accelerator: shortcut('W'), 
        closure: controller.closeAction, 
        enabled: bind { model.activeDocument != null }
    )
    action(
        id: 'saveAction', 
        name:'Save', 
        accelerator: shortcut('S'), 
        closure: controller.saveAction, 
        enabled: bind { model.state.isDirty }
    )
}

application(title: 'Multiple Document Example',
    size:[800,600], 
    //pack:true,
    //location:[50,50],
    locationByPlatform:true,
    iconImage: imageIcon('/griffon-icon-48x48.png').image,
    iconImages: [imageIcon('/griffon-icon-48x48.png').image,
        imageIcon('/griffon-icon-32x32.png').image,
        imageIcon('/griffon-icon-16x16.png').image],
    defaultCloseOperation: 0,
    windowClosing: { evt -> if (controller.canClose()) { app.shutdown() } }
) {
    menuBar(id: 'menuBar') {
        menu(text: 'File', mnemonic: 'F') {
            menuItem(newAction)
            menuItem(openAction)
            menuItem(closeAction)
            menuItem(saveAction)
        }
    }
 
    tabbedPane(id: 'documents', stateChanged: { evt -> 
        controller.activeDocumentChanged() 
    })
}

If you look close, you'll see I'm using the window closing trick I blogged awhile ago. The last piece of the puzzle is the controller: MultiDocumentExampleController.groovy. This is where we do the heavy lifting.
class MultiDocumentExampleController {
    int count = 0  // not strictly required
    def model
    def view

    void mvcGroupInit(Map args) {}

    void activeDocumentChanged() {
        // de-activate the existing active document
        if (model.activeDocument) { 
            model.activeDocument.controller.deactivate() 
        }

        // figure out the new active document
        int index = view.documents.selectedIndex
        model.activeDocument = index == -1 ? null : model.openDocuments[index]
        if (model.activeDocument) { 
            model.activeDocument.controller.activate(model.state) 
        }
    }
 
    boolean canClose() {
        return model.openDocuments.inject(true) { flag, doc -> 
            if (flag) flag &= doc.controller.close() }
    }

    def newAction = { evt = null ->
        // TODO: use an id that makes sense for your application, like a file name
        String id = "Document " + (count++) 
  
        def document = buildMVCGroup('Document', id, 
            tabs: view.documents, id: id, name: id /* TODO pass other args */)
        if (document.controller.open()) {
            model.openDocuments << document
            view.documents.addTab(document.model.name, document.view.root)
            view.documents.selectedIndex = view.documents.tabCount - 1
        } else {
            destroyMVCGroup(id)
        }
    }
 
    def openAction = { evt = null ->
        // TODO: pop up a open file dialog or whatever makes sense

        // TODO: use an id that makes sense for your application, like a file name
        String id = "Document" + (count++)

        // check to see if the document is already open
        def open = model.openDocuments.find { it.model.id == id }
        if (open) {
            view.documents.selectedIndex = model.openDocuments.indexOf(open)
        } else {
            def document = buildMVCGroup('Document', id, 
                tabs: view.documents, id: id, name: id /* TODO pass other args */)
            if (document.controller.open()) {
                model.openDocuments << document
                view.documents.addTab(document.model.name, document.view.root)
                view.documents.selectedIndex = view.documents.tabCount - 1
            } else {
                destroyMVCGroup(id)
            }
        }
    }
 
    def saveAction = { evt = null ->
        model.activeDocument.controller.save()
    }
 
    def closeAction = { evt = null ->
        if (model.activeDocument.controller.close()) {
            int index = model.openDocuments.indexOf(model.activeDocument)
            model.openDocuments.remove(index)
            view.documents.removeTabAt(index)
        }
    }
}

There's a fair amount of code but it's all pretty straightforward.  The only thing that warrants further discussion is activeDocumentChanged().  This method is called whenever the active tab in the tabbed pane is changed.  In it, we deactivate() the previously active document and then activate() the new document.  When activating, we pass in the state object I mentioned earlier.  The purpose of the state object is to allow us to use bindings that depend on the active document.  For example, we really only want the save action to be enabled when we have a document open and the document is dirty.  We can't bind directly to the active document's model because as soon as we open or switch to a new document, the binding is no longer correct.  We can, however, bind to the state object and then sync the document's state in the activate() method (or other methods).

The implementation of the Document MVC group is straightforward:
DocumentModel.groovy
import groovy.beans.Bindable

@Bindable class DocumentModel {
    String id
    String name = "Untitled"
    boolean isDirty = false
    DocumentState state
}

DocumentView.groovy
panel(id: 'root') {
    // TODO: put your view implementation here
    button(text: 'Mark Dirty', enabled: bind { !model.isDirty }, 
        actionPerformed: { controller.markDirty() })
    button(text: 'Mark Clean', enabled: bind { model.isDirty }, 
        actionPerformed: { controller.markClean() })
}


DocumentController.groovy
import javax.swing.JOptionPane

/**
 * A skeleton controller for a 'document'.
 */
class DocumentController {
    def model
    def view
    def tabs

    void mvcGroupInit(Map args) {
        tabs = args.tabs
    }

    /**
     * Called when the tab with this document gains focus.
     */
    void activate(state) {
        // save the state object so we can signal the 
        model.state = state
  
        // TODO: sync the model and document state
        state.isDirty = model.isDirty
    }
 
    /**
     * Called when the tab with this document loses focus.
     */
    void deactivate() {
        // forget the state object
        model.state = null
    }
 
    /**
     * Called when the document is initially opened.
     */
    boolean open() {
        // TODO: perform any opening tasks
        return true 
    }
 
    /**
     * Called when the document is closed.
     */
    boolean close() {
        if (model.isDirty) {
            switch (JOptionPane.showConfirmDialog(app.appFrames[0], 
                    "Save changes to '${model.name}'?", "Example", 
                    JOptionPane.YES_NO_CANCEL_OPTION)){
                case JOptionPane.YES_OPTION: return save()
                case JOptionPane.NO_OPTION: return true
                case JOptionPane.CANCEL_OPTION: return false
            }
        }
        return true
    }
 
    /**
     * Called when the document is saved.
     */
    boolean save() {
        // TODO: perform any saving tasks
        markClean()
        return true
    }
 
    /**
     * Marks this document as 'dirty'
     */
    void markDirty() {
        model.isDirty = true
        if (model.state) { model.state.isDirty = true }
        // TODO: update any other model/state properties
        setTitle(model.name + "*")
    }
 
    /**
     * Marks this document as 'clean'
     */
    void markClean() {
        model.isDirty = false
        if (model.state) { model.state.isDirty = false }
        // TODO: update any other model/state properties
        setTitle(model.name)
    }
 
    /**
     * Sets this document's tab title.
     */
    void setTitle(title) {
        int index = tabs.indexOfComponent(view.root)
        if (index != -1) {
            tabs.setTitleAt(index, title)
        }
    }
}

So there it is. You should be able to use this as a starting place for your next Griffon-based text editor, web browser, etc.  I'm personally using in PSICAT to manage the open diagrams:


I zipped up all of the code from this post and made it available for download.

Monday, October 12, 2009

Griffon Tip: Silly SwingBuilder Tricks

Here's two quick tips for working with radio buttons in SwingBuilder.  Radio buttons are created using SwingBuilder's radioButton() method.  To ensure only one is selected at a time, you must also associate them with a button group.   Ideally SwingBuilder would allow you to nest your radioButton()s inside a buttonGroup(), e.g.
buttonGroup() {
    // doesn't work
    radioButton(text: 'Option 1')
    radioButton(text: 'Option 2')
}

Unfortunately this doesn't work as buttonGroup() does not support nesting.  Marc Hedlund stumbled across this same issue and offers up one solution.  I like his solution but it means creating a separate variable to hold the button group.  I've found a bit nicer way to do it using Groovy's with keyword:
buttonGroup().with {
    add radioButton(text: 'Option 1')
    add radioButton(text: 'Option 2')
}

The advantage of this approach is that it conveys our intention better and we don't have to instance extra variables.

EDIT: the 'with' also passes in the created object so you could take it a step further and replace the add as well:
buttonGroup().with { group ->
    radioButton(text: 'Option 1', buttonGroup: group)
    radioButton(text: 'Option 2', buttonGroup: group)
}

As a bonus, here's how you can use mutual binding to keep a boolean in your model in sync with the radio buttons.  Let's say your model looks something like so:
class DemoModel {
    ...
    @Bindable boolean option1 = true
    ...
}

Using mutual binding, you can keep the model synced up without any explicit management code:
buttonGroup().with {
    add radioButton(text: 'Option 1', selected: bind(source: model, sourceProperty: 'option1', mutual:true))
    add radioButton(text: 'Option 2', selected: bind { !model.option1 })
}

Obviously this will only work for cases where you have an either-or choice.

Tuesday, October 06, 2009

Griffon Tip: MVC Groups Revisited

My last post about MVC groups caused a bit of confusion, so I thought I'd follow up with a bit of clarification and, as a bonus, I'll also throw in a new trick for working with MVC groups.

MVC Groups for Dialogs
The withMVC method I introduced in the previous post was meant to help manage the lifecycle of short-lived MVC groups, such as those used in simple dialogs.  In cases like this, the MVC group only has to exist for as long as the dialog is visible.  withMVC was very much inspired by File#withInputStream which handles opening and closing of the input stream like withMVC handles creating and destroying of the MVC group.  Below is a complete example of showing an MVC group as a dialog:

NewProjectWizardModel.groovy
@Bindable class NewProjectWizardModel {
    String name
    String filePath
}

NewProjectWizardView.groovy
panel(id:'options', layout: new MigLayout('fill'), border: etchedBorder()) { 
    // project name
    label('Name:')
    textField(text: bind(source: model, sourceProperty:'name', mutual:true), constraints: 'growx, span, wrap')
    
    // directory
    label('Directory:')
    textField(text: bind(source: model, sourceProperty:'filePath', mutual:true), constraints:'width min(200px), growx')
    button(action: browseAction, constraints: 'wrap')
}

NewProjectWizardController.groovy
class NewProjectWizardController {
    def model
    def view

    void mvcGroupInit(Map args) {}

    def actions = [
        browse': { evt = null ->
            def file = Dialogs.showSaveDirectoryDialog("New Project", null, app.appFrames[0])
            if (file) { 
                model.filePath = file.absolutePath
                if (!model.name) { model.name = file.name }
            }
        }
    ]

    def show() {
        if (Dialogs.showCustomDialog("Create New Project", view.options, app.appFrames[0])) {
            // perform some logic
        }
    }
}

Dialogs.showCustomDialog() just uses JOptionPane to show a custom dialog with a component from the view.  I can show this dialog in response to a menu option, a button click, or whatever with the snippet from last post:
withMVC('NewProjectWizard', 'newProject', [:]) { mvc ->
    def project = mvc.controller.show()
    if (project) {
        // do something
    }
}

Hopefully that's a bit more clear and shows how MVC groups can be used as dialogs.  Now let's look at another use of MVC groups: embedding them as components in other views.

Embedding MVC Groups in Views
MVC groups work well as dialogs but they can also be embedded directly in the views of other MVC groups.  This is useful because it allows you to create re-usable components that you can embed in your views as needed.  Embedding is relatively straightforward:
widget(id:'sidePanel', buildMVCGroup('Project', 'project').view.root)

We build the MVC group and grab a component from the view (in this case it was called 'root' but it could be any id).  If we need to access something in the group, we can reference it by id:
def model = app.models['id']
def view = app.views['id']
def controller = app.controllers['id']

Hopefully these tips help you use MVC groups a bit more effectively and bring a bit more modularity to your Griffon app.

Monday, October 05, 2009

Griffon Tip: MVC Groups

Recently I've been doing some refactoring of PSICAT to make better use of Griffon's MVC Groups.  I like MVC Groups because they provide self-contained building blocks that you can re-use throughout your application.  Below are a few tips that might be useful to others:

Creating MVC Groups
MVC Groups are created with the 'griffon create-mvc ' command.  This creates the appropriate files in the griffon-app directory structure and registers the MVC Group in Application.groovy.

When you want to use the new MVC group in your application, you have two ways to instantiate it: createMVCGroup and buildMVCGroup.  Both methods instantiate the MVC group but differ in the ordering of parameters passed to them and in what they return:
  1. List[model, view, controller] createMVCGroup(type, id, paramMap)
  2. Map['model': model, 'view':view, 'controller':controller, ...] buildMVCGroup(paramMap, type, id)
So if I had created an MVC group: 'griffon create-mvc Diagram', I could instantiate it in my application with either:
def list = createMVCGroup('Diagram', 'diagram1', [:])
or
def map = buildMVCGroup([:], 'Diagram', 'diagram1')


I was initially a bit confused as to why the parameters map was moved to the first argument for buildMVCGroup but then I remembered that Groovy lets you pass named parameters to a method and collects them all as a map.  This allows some nice syntactic sugar when creating an MVC group:
def map = buildMVCGroup('Diagram', 'diagram1', name: 'The Diagram', format: 'jpeg')


The other nifty thing is that Griffon automagically sets parameters passed when creating an MVC group to properties on the model (if applicable).  In the example above, if I had a name and a format properties on my model, they would be set to the values I passed instead of me having to explicitly set them in mvcGroupInit.


Destroying MVC Groups
While probably not critical, it's good practice to dispose of your MVC groups when you're done working with them.  You can do this by calling destroyMVCGroup with the id you passed when creating the group.  I find myself using many short-lived MVC groups.  To ensure I disposed of things properly, I whipped up a little method that simplifies things:
def withMVC(String type, String id, Map params, Closure closure) {
    closure(buildMVCGroup(params, type, id))
    destroyMVCGroup(id)
}
NOTE: you could do the same trick of moving the map to the first argument if you wanted to be able to pass 'loose' named parameters to the method.

You would call it in your code like so:
withMVC('NewSectionDialog', 'newSection', [ project: model.project ]) { mvc ->
    def section = mvc.controller.show()
    if (section) { model.projectSections << section }
}
This creates the MVC group, hands it to the closure you pass, and then destroys the group when done.

MVC groups are a nice feature of Griffon that let you create re-usable building blocks within your application.  Hopefully this tip helps you use them more effectively.

Thursday, September 10, 2009

Happy Birthday Griffon

Well it looks like I'm a bit late to the party as James, Jim, Guillaume, and Andres already beat me to it, but I'll say it anyways: Happy Birthday Griffon!  As birthdays are a natural time for reflection, I thought I'd look through my blog archives to find my first Griffon post.  Although not one of my most original titles, My First Griffon App debuted on September 17, 2008 marking me as a happy Griffon user since nearly the beginning.  Since then I've written several other Griffon apps and a few more blog posts about Griffon.

It's been fun watching not only the project develop but also the enthusiastic community spring up around it.  As Jim mentioned in his post, Griffon's presence at JavaOne this year was a major highlight.  The fact that the major IDEs are adding Griffon support and Griffon in Action is set to release this March shows that Griffon truly has arrived.

So congrats to everyone involved.  If you haven't tried Griffon yet, what are you waiting for?  The upcoming 0.2 release is shaping up to be the best yet!

Wednesday, September 09, 2009

Griffon Tip: Mac Look and Feel on Snow Leopard

If you upgraded your Mac to Snow Leopard recently, you may have noticed that your Griffon apps no longer look the same as they used to on Leopard.  Aside from a few color differences, the biggest giveaway will be that your app no longer uses the standard top menubar.  Fortunately there is a simple fix if you want to use the more native look and feel.  Change your griffon-app/lifecycle/Initialize.groovy file to include the 'mac' look and feel first:
import groovy.swing.SwingBuilder
import griffon.util.GriffonPlatformHelper

GriffonPlatformHelper.tweakForNativePlatform(app)
SwingBuilder.lookAndFeel('mac', 'nimbus', 'gtk', ['metal', [boldFonts: false]])

The crux of the problem is SwingBuilder tries various L&Fs in the order you specify them and stops when it finds one that works.  Nimbus is the L&F for Java 6 but previous versions of Mac OS X shipped with Java 5.  So SwingBuilder would try Nimbus and it would fail then it would try the Mac look and feel which would work.  Snow Leopard ships with Java 6 so when SwingBuilder tried Nimbus, it worked and it never tried to set the Mac L&F.

EDIT: This will be the default behavior in the upcoming Griffon 0.2 release.  So you will only need this fix if you have Griffon apps from pre-0.2 days.