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.

