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 {
private 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 javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TabPane;
import javafx.scene.paint.Color;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import javafx.stage.Window;
import org.codehaus.griffon.runtime.javafx.artifact.AbstractJavaFXGriffonView;
import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.io.File;
import java.util.Collections;
@ArtifactProviderFor(GriffonView.class)
public class ContainerView extends AbstractJavaFXGriffonView {
private ContainerController controller;
private ContainerModel model;
@FXML
private TabPane tabGroup;
private FileChooser fileChooser;
@MVCMember
public void setController(@Nonnull ContainerController controller) {
this.controller = controller;
}
@MVCMember
public void setModel(@Nonnull ContainerModel model) {
this.model = model;
}
@Nonnull
public TabPane getTabGroup() {
return tabGroup;
}
@Override
public void initUI() {
Stage stage = (Stage) getApplication()
.createApplicationContainer(Collections.emptyMap());
stage.setTitle(getApplication().getConfiguration().getAsString("application.title"));
stage.setWidth(480);
stage.setHeight(320);
stage.setScene(init());
getApplication().getWindowManager().attach("mainWindow", stage);
fileChooser = new FileChooser();
fileChooser.setTitle(getApplication().getConfiguration().getAsString("application.title", "Open File"));
}
// build the UI
private Scene init() {
Scene scene = new Scene(new Group());
scene.setFill(Color.WHITE);
scene.getStylesheets().add("bootstrapfx.css");
Node node = loadFromFXML();
((Group) scene.getRoot()).getChildren().addAll(node);
connectActions(node, controller);
tabGroup.getSelectionModel().selectedItemProperty().addListener((v, o, n) -> model.setMvcIdentifier(n != null ? n.getId() : null));
Action saveAction = actionFor(controller, "save");
model.getDocumentModel().addPropertyChangeListener("dirty", (e) -> saveAction.setEnabled((Boolean) e.getNewValue()));
return scene;
}
@Nullable
public File selectFile() {
Window window = (Window) getApplication().getWindowManager().getStartingWindow();
return fileChooser.showOpenDialog(window);
}
}
Here we find a Scene
whose contents come from an FXML file. The file name is determined using a naming convention, in
this case it’s the fully qualified View class name without the View
suffix. These are the contents of said file.
Also, the view
registers an anonymous javafx.beans.value.ChangeListener
to listen to tab selection
changes and update the documentModel property found in the model
.
griffon-app/resources/editor/container.fxml
<?xml version="1.0" encoding="UTF-8"?>
<!--
SPDX-License-Identifier: Apache-2.0
Copyright 2008-2021 the original author or authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<?import javafx.scene.control.Menu?>
<?import javafx.scene.control.MenuBar?>
<?import javafx.scene.control.MenuItem?>
<?import javafx.scene.control.SeparatorMenuItem?>
<?import javafx.scene.control.TabPane?>
<?import javafx.scene.layout.BorderPane?>
<BorderPane maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" prefHeight="400.0"
prefWidth="600.0" xmlns="http://javafx.com/javafx/8" xmlns:fx="http://javafx.com/fxml/1"
fx:controller="editor.ContainerController">
<top>
<MenuBar BorderPane.alignment="CENTER">
<Menu mnemonicParsing="false" text="File">
<MenuItem mnemonicParsing="false" text="Open" fx:id="openActionTarget"/>
<MenuItem mnemonicParsing="false" text="Close" fx:id="closeActionTarget"/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem mnemonicParsing="false" text="Save" fx:id="saveActionTarget"/>
<SeparatorMenuItem mnemonicParsing="false"/>
<MenuItem mnemonicParsing="false" text="Quit" fx:id="quitActionTarget"/>
</Menu>
</MenuBar>
</top>
<center>
<TabPane fx:id="tabGroup" prefHeight="200.0" prefWidth="200.0" tabClosingPolicy="UNAVAILABLE"
BorderPane.alignment="CENTER"/>
</center>
</BorderPane>
This file defines a MenuBar
and a tab container (a TabPane
) 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 FileChooser
that will be used to select files for reading.
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
.
There is no mnemonic support in JavaFX.
griffon-app/i18n/messages.properties
editor.ContainerController.action.Open.accelerator = Meta+O
editor.ContainerController.action.Close.accelerator = Meta+W
editor.ContainerController.action.Save.accelerator = Meta+S
editor.ContainerController.action.Quit.accelerator = Meta+Q
editor.ContainerController.action.Open.icon = org.kordamp.ikonli.javafx.FontIcon|fa-folder-open
editor.ContainerController.action.Close.icon = org.kordamp.ikonli.javafx.FontIcon|fa-window-close
editor.ContainerController.action.Save.icon = org.kordamp.ikonli.javafx.FontIcon|fa-save
editor.ContainerController.action.Quit.icon = org.kordamp.ikonli.javafx.FontIcon|fa-power-off
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.