Each file we open requires a bit of metadata to be handled by the editor application, for example its title and contents.
We’ll create a simple observable POJO that represents a Document
.
src/main/java/editor/Document.java
package editor;
import org.codehaus.griffon.runtime.core.AbstractObservable;
import java.io.File;
public class Document extends AbstractObservable {
private String title;
private String contents;
private boolean dirty;
private File file;
public Document() {
}
public Document(File file, String title) {
this.file = file;
this.title = title;
}
public String getTitle() {
return title;
}
public void setTitle(String title) {
firePropertyChange("title", this.title, this.title = title);
}
public String getContents() {
return contents;
}
public void setContents(String contents) {
firePropertyChange("contents", this.contents, this.contents = contents);
}
public boolean isDirty() {
return dirty;
}
public void setDirty(boolean dirty) {
firePropertyChange("dirty", this.dirty, this.dirty = dirty);
}
public File getFile() {
return file;
}
public void setFile(File file) {
firePropertyChange("file", this.file, this.file = file);
}
public void copyTo(Document doc) {
doc.title = title;
doc.contents = contents;
doc.dirty = dirty;
doc.file = file;
}
}
The title and contents properties should be self explanatory. We’ll use the dirty property to keep track of
changes. The final property, file, points to the File
object that was used to load the document; we’ll use this
value to save back edited changes.
Now imagine what happens when you have multiple tabs open in an editor; the save
and close
actions are context
sensitive, that is, they operate on the currently selected editor/tab. We need to replicate this behavior, in order
to do so we’ll use a presentation model for the Document
class, aptly named DocumentModel
.
src/main/java/editor/DocumentModel.java
package editor;
import java.beans.PropertyChangeListener;
import static griffon.util.GriffonClassUtils.setPropertyValue;
public class DocumentModel extends Document {
private Document document;
private final PropertyChangeListener proxyUpdater = (e) -> setPropertyValue(this, e.getPropertyName(), e.getNewValue());
public DocumentModel() {
addPropertyChangeListener("document", (e) -> {
if (e.getOldValue() instanceof Document) {
((Document) e.getOldValue()).removePropertyChangeListener(proxyUpdater);
}
if (e.getNewValue() instanceof Document) {
((Document) e.getNewValue()).addPropertyChangeListener(proxyUpdater);
((Document) e.getNewValue()).copyTo(DocumentModel.this);
}
});
}
public Document getDocument() {
return document;
}
public void setDocument(Document document) {
firePropertyChange("document", this.document, this.document = document);
}
}
The DocumentModel
class extends from Document
just as a convenience, it inherits all properties from Document
in
this way. It also defines a new property document which will hold the selected Document
.
Alright, we can move on to the ContainerModel
member of the container
MVC group (our main group). Here we’ll
see how the previous presentation model is put to good use.
griffon-app/models/editor/ContainerModel.java
package editor;
import griffon.core.artifact.GriffonModel;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonModel;
@ArtifactProviderFor(GriffonModel.class)
public class ContainerModel extends AbstractGriffonModel {
public static final String MVC_IDENTIFIER = "mvcIdentifier";
private final DocumentModel documentModel = new DocumentModel();
private String mvcIdentifier;
public ContainerModel() {
addPropertyChangeListener(MVC_IDENTIFIER, (e) -> {
Document document = null;
if (e.getNewValue() != null) {
EditorModel model = getApplication().getMvcGroupManager().getModel(mvcIdentifier, EditorModel.class);
document = model.getDocument();
} else {
document = new Document();
}
documentModel.setDocument(document);
});
}
public String getMvcIdentifier() {
return mvcIdentifier;
}
public void setMvcIdentifier(String mvcIdentifier) {
firePropertyChange(MVC_IDENTIFIER, this.mvcIdentifier, this.mvcIdentifier = mvcIdentifier);
}
public DocumentModel getDocumentModel() {
return documentModel;
}
}
This model keeps track of two items:
-
the identifier of the selected tab, represented by mvcIdentifier.
-
the document presentation model, represented by documentModel.
Notice that the documentModel property is declared as final; this means it will always have the same value, thus we
can use it to create stable bindings. This is the reason for making DocumentModel
a subclass of Document
. As you
can see the former listens to changes on the latter and copying the values over. This happens every time the application
changes the value of documentModel.document due to the PropertyChangeListener
s that were put into place.
Let’s move to the View. Open up ContainerView.java
and paste the following into it
griffon-app/views/editor/ContainerView.java
package editor;
import griffon.core.artifact.GriffonView;
import griffon.core.controller.Action;
import griffon.inject.MVCMember;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.swing.artifact.AbstractSwingGriffonView;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.JTabbedPane;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import java.awt.BorderLayout;
import java.awt.Image;
import java.awt.Toolkit;
import java.awt.Window;
import java.io.File;
import java.util.Collections;
import java.util.Map;
import static griffon.util.GriffonApplicationUtils.isMacOSX;
import static java.util.Arrays.asList;
import static javax.swing.WindowConstants.DO_NOTHING_ON_CLOSE;
@ArtifactProviderFor(GriffonView.class)
public class ContainerView extends AbstractSwingGriffonView {
@MVCMember @Nonnull
private ContainerModel model;
@MVCMember @Nonnull
private ContainerController controller;
private JTabbedPane tabGroup;
private JFileChooser fileChooser;
public JTabbedPane getTabGroup() {
return tabGroup;
}
@Override
public void initUI() {
JFrame window = (JFrame) getApplication()
.createApplicationContainer(Collections.<String, Object>emptyMap());
window.setName("mainWindow");
window.setTitle(getApplication().getConfiguration().getAsString("application.title"));
window.setSize(480, 320);
window.setDefaultCloseOperation(DO_NOTHING_ON_CLOSE);
window.setIconImage(getImage("/griffon-icon-48x48.png"));
window.setIconImages(asList(
getImage("/griffon-icon-48x48.png"),
getImage("/griffon-icon-32x32.png"),
getImage("/griffon-icon-16x16.png")
));
getApplication().getWindowManager().attach("mainWindow", window);
fileChooser = new JFileChooser();
Map<String, Action> actionMap = getApplication().getActionManager().actionsFor(controller);
Action saveAction = actionMap.get("save");
model.getDocumentModel().addPropertyChangeListener("dirty", (e) -> saveAction.setEnabled((Boolean) e.getNewValue()));
JMenu fileMenu = new JMenu("File");
fileMenu.add(new JMenuItem((javax.swing.Action) actionMap.get("open").getToolkitAction()));
fileMenu.add(new JMenuItem((javax.swing.Action) actionMap.get("close").getToolkitAction()));
fileMenu.addSeparator();
fileMenu.add(new JMenuItem((javax.swing.Action) actionMap.get("save").getToolkitAction()));
if (!isMacOSX()) {
fileMenu.addSeparator();
fileMenu.add(new JMenuItem((javax.swing.Action) actionMap.get("quit").getToolkitAction()));
}
JMenuBar menuBar = new JMenuBar();
menuBar.add(fileMenu);
window.setJMenuBar(menuBar);
window.getContentPane().setLayout(new BorderLayout());
tabGroup = new JTabbedPane();
tabGroup.addChangeListener(new ChangeListener() {
@Override
public void stateChanged(ChangeEvent e) {
JTabbedPane tabbedPane = (JTabbedPane) e.getSource();
int selectedIndex = tabbedPane.getSelectedIndex();
if (selectedIndex < 0) {
model.setMvcIdentifier(null);
} else {
JComponent tab = (JComponent) tabbedPane.getComponentAt(selectedIndex);
model.setMvcIdentifier((String) tab.getClientProperty(ContainerModel.MVC_IDENTIFIER));
}
}
});
window.getContentPane().add(tabGroup, BorderLayout.CENTER);
}
@Nullable
public File selectFile() {
Window window = (Window) getApplication().getWindowManager().getStartingWindow();
int result = fileChooser.showOpenDialog(window);
if (JFileChooser.APPROVE_OPTION == result) {
return new File(fileChooser.getSelectedFile().toString());
}
return null;
}
private Image getImage(String path) {
return Toolkit.getDefaultToolkit().getImage(ContainerView.class.getResource(path));
}
}
Here we find a Window
object containing a JMenuBar
and a tab container (a JTabbedPane
) named tabGroup.
This tab container is exposed to the outside world via a getter method; we’ll see why it’s done this way when the
second MVC group comes into play. The View is also responsible for managing a JFileChooser
that will be used to
select files for reading. Notice the conditional enabling of the save
action given the state of the dirty
property
coming from model.documentModel
. Also, the view
registers an anonymous javax.swing.event.ChangeListener
to listen
to tab selection changes and update the documentModel property found in the model
.
We can define a few of the action properties using a resource bundle, from the example the mnemonic and accelerator
properties. Paste the following into messages.properties
.
griffon-app/i18n/messages.properties
editor.ContainerController.action.Save.accelerator = meta S
editor.ContainerController.action.Save.mnemonic = S
editor.ContainerController.action.Open.accelerator = meta O
editor.ContainerController.action.Open.mnemonic = O
editor.ContainerController.action.Close.accelerator = meta W
editor.ContainerController.action.Close.mnemonic = W
editor.ContainerController.action.Quit.accelerator = meta Q
editor.ContainerController.action.Quit.mnemonic = Q
We’re almost done with the container
MVC group, what remains to be done is update the ContainerController
.
griffon-app/controllers/editor/ContainerController.java
package editor;
import griffon.core.artifact.GriffonController;
import griffon.metadata.ArtifactProviderFor;
import org.codehaus.griffon.runtime.core.artifact.AbstractGriffonController;
@ArtifactProviderFor(GriffonController.class)
public class ContainerController extends AbstractGriffonController {
private ContainerModel model;
private ContainerView view;
public void open() {
}
public void save() {
}
public void close() {
}
public void quit() {
getApplication().shutdown();
}
}
We’ve got 4 actions (open
, save
, close
and quit
) and nothing more for the time being. You can run the application
once again to verify that the code compiles and runs.