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.