Adventures with GroovyFX
Author: Paul King
Published: 2022-12-12 02:22PM
This blog looks at a GroovyFX version of a ToDo application originally written in JavaFX.
First we start with a ToDoCategory
enum of our ToDo categories:
enum ToDoCategory {
EXERCISE("🚴"),
WORK("📊"),
RELAX("🧘"),
TV("📺"),
READ("📚"),
EVENT("🎭"),
CODE("💻"),
COFFEE("☕️"),
EAT("🍽"),
SHOP("🛒"),
SLEEP("😴")
final String emoji
ToDoCategory(String emoji) {
this.emoji = emoji
}
}
We will have a ToDoItem
class containing the todo task, the previously mentioned category and the due date.
@Canonical
@JsonIncludeProperties(['task', 'category', 'date'])
@FXBindable
class ToDoItem {
final String task
final ToDoCategory category
final LocalDate date
}
It’s annotated with @JsonIncludeProperties
to allow easy serialization to/from JSON format, to provide easy persistence, and @FXBindable
which eliminates the boilerplate required to define JavaFX properties.
Next, we’ll define some helper variables:
var file = 'todolist.json' as File
var mapper = new ObjectMapper().registerModule(new JavaTimeModule())
var open = { mapper.readValue(it, new TypeReference<List<ToDoItem>>() {}) }
var init = file.exists() ? open(file) : []
var items = FXCollections.observableList(init)
var close = { mapper.writeValue(file, items) }
var table, task, category, date, images = [:]
var urls = ToDoCategory.values().collectEntries {
[it, "emoji/${Integer.toHexString(it.emoji.codePointAt(0))}.png"]
}
Here, mapper
serializes and deserializes our top-level domain object (the ToDo list) into JSON using the Jackson library. The open
and close
Closures do the reading and writing respectively.
For a bit of fun and only slightly more complexity, we have included some slightly nicer images in our application. JavaFX’s default emoji font rendering is a little sketchy on some platforms, and it’s not much work to have nice multicolored images. This is achieved using the icons from https://github.com/pavlobu/emoji-text-flow-javafx.
The application is perfectly functional without them (and the approximately 20 lines for the cellFactory
and cellValueFactory
definitions could be elided) but is prettier with the nicer images. We shrunk them to 1/3 their original size but we could certainly make them larger if we felt inclined.
Our application will have a combo box for selecting a ToDo item’s category. We’ll create a factory for the combo box so that each selection will be a label with both graphic and text components.
def graphicLabelFactory = {
new ListCell<ToDoCategory>() {
void updateItem(ToDoCategory cat, boolean empty) {
super.updateItem(cat, empty)
if (!empty) {
graphic = new Label(cat.name()).tap {
graphic = new ImageView(images[cat])
}
}
}
}
}
When displaying our ToDo list, we’ll use a table view. So, let’s create a factory for table cells that will use the pretty images as a centered graphic.
def graphicCellFactory = {
new TableCell<ToDoItem, ToDoItem>() {
void updateItem(ToDoItem item, boolean empty) {
graphic = empty ? null : new ImageView(images[item.category])
alignment = Pos.CENTER
}
}
}
Finally, with these definitions out of the way, we can define our GroovyFX application for manipulating our ToDo list:
start {
stage(title: 'GroovyFX ToDo Demo', show: true, onCloseRequest: close) {
urls.each { k, v -> images[k] = image(url: v, width: 24, height: 24) }
scene {
gridPane(hgap: 10, vgap: 10, padding: 20) {
columnConstraints(minWidth: 80, halignment: 'right')
columnConstraints(prefWidth: 250)
label('Task:', row: 1, column: 0)
task = textField(row: 1, column: 1, hgrow: 'always')
label('Category:', row: 2, column: 0)
category = comboBox(items: ToDoCategory.values().toList(),
cellFactory: graphicLabelFactory, row: 2, column: 1)
label('Date:', row: 3, column: 0)
date = datePicker(row: 3, column: 1)
table = tableView(items: items, row: 4, columnSpan: REMAINING,
onMouseClicked: {
var item = items[table.selectionModel.selectedIndex.value]
task.text = item.task
category.value = item.category
date.value = item.date
}) {
tableColumn(property: 'task', text: 'Task', prefWidth: 200)
tableColumn(property: 'category', text: 'Category', prefWidth: 80,
cellValueFactory: { new ReadOnlyObjectWrapper(it.value) },
cellFactory: graphicCellFactory)
tableColumn(property: 'date', text: 'Date', prefWidth: 90, type: Date)
}
hbox(row: 5, columnSpan: REMAINING, alignment: CENTER, spacing: 10) {
button('Add', onAction: {
if (task.text && category.value && date.value) {
items << new ToDoItem(task.text, category.value, date.value)
}
})
button('Update', onAction: {
if (task.text && category.value && date.value &&
!table.selectionModel.empty) {
items[table.selectionModel.selectedIndex.value] =
new ToDoItem(task.text, category.value, date.value)
}
})
button('Remove', onAction: {
if (!table.selectionModel.empty)
items.removeAt(table.selectionModel.selectedIndex.value)
})
}
}
}
}
}
We could have somewhat separated the concerns of application logic and display logic by placing the GUI part of this app in an fxml
file. For our purposes however, we’ll keep the whole application in one source file and use Groovy’s declarative builder style.
Here is the application in use:
Further information
The code for this application can be found here:
It’s a Groovy 3 and JDK 8 application but see this blog post if you want to see Jackson deserialization of classes and records (and Groovy’s emulated records) from CSV files using recent Groovy and JDK versions.