blob: c58a84aadc813de5c967082e72d24646e8a5281c [file] [log] [blame]
---
layout: post
title: Adventures with GroovyFX
date: '2022-12-12T00:00:00+00:00'
categories: groovy
---
<p>This blog looks at a <a href="http://groovyfx.org/" target="_blank">GroovyFX</a> version of a <a href="https://donraab.medium.com/my-weird-and-wonderful-first-adventures-with-javafx-6efe3b1923c8" target="_blank">ToDo application originally written in JavaFX</a>. First we start with a <code>ToDoCategory</code> enum of our ToDo categories:</p><p><img src="https://blogs.apache.org/groovy/mediaresource/453d4f49-c4f0-4f77-977e-863d2f8c47d2" alt="TodoEnum.png"></p>
<p>We will have a <code>ToDoItem</code> class containing the todo task, the previously mentioned category and the due date.</p><pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'JetBrains Mono',monospace;font-size:11pt;"><span style="color:#bbb529;">@Canonical<br></span><span style="color:#bbb529;">@JsonIncludeProperties</span>([<span style="color:#6a8759;">'task'</span>, <span style="color:#6a8759;">'category'</span>, <span style="color:#6a8759;">'date'</span>])<br><span style="color:#bbb529;">@FXBindable<br></span><span style="color:#cc7832;">class </span>ToDoItem {<br> String <span style="color:#9876aa;">task<br></span><span style="color:#9876aa;"> </span>ToDoCategory <span style="color:#9876aa;">category<br></span><span style="color:#9876aa;"> </span>LocalDate <span style="color:#9876aa;">date<br></span>}<br></pre>
<p>It's annotated with <code>@JsonIncludeProperties</code> to allow easy serialization to/from JSON format, to provide easy persistence, and <code>@FXBindable</code>&nbsp;which eliminates the boilerplate required to define JavaFX properties.</p>
<p>Next, we'll define some helper variables:</p>
<pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'JetBrains Mono',monospace;font-size:11pt;"><span style="color:#cc7832;">var </span>file = <span style="color:#6a8759;">'todolist.json' </span><span style="color:#cc7832;">as </span>File<br><span style="color:#cc7832;">var </span>mapper = <span style="color:#cc7832;">new </span>ObjectMapper().registerModule(<span style="color:#cc7832;">new </span>JavaTimeModule())<br><span style="color:#cc7832;">var </span>open = <span style="font-weight:bold;">{ </span>mapper.readValue(it, <span style="color:#cc7832;">new </span>TypeReference&lt;List&lt;ToDoItem&gt;&gt;() {}) <span style="font-weight:bold;">}<br></span><span style="color:#cc7832;">var </span>init = file.exists() ? open(file) : []<br><span style="color:#cc7832;">var </span>items = FXCollections.<span style="color:#9876aa;font-style:italic;">observableList</span>(init)<br><span style="color:#cc7832;">var </span>close = <span style="font-weight:bold;">{ </span>mapper.writeValue(file, items) <span style="font-weight:bold;">}<br></span><span style="color:#cc7832;">var </span>table, task, category, date, images = [:]<br><span style="color:#cc7832;">var </span>urls = ToDoCategory.<span style="color:#9876aa;font-style:italic;">values</span>().collectEntries <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span>[it, <span style="color:#6a8759;">"emoji/</span>$<span style="font-weight:bold;">{</span>Integer.<span style="color:#9876aa;font-style:italic;">toHexString</span>(it.<span style="color:#9876aa;">emoji</span>.codePointAt(<span style="color:#6897bb;">0</span>))<span style="font-weight:bold;">}</span><span style="color:#6a8759;">.png"</span>]<br><span style="font-weight:bold;">}<br></span></pre>
<p>Here, <code>mapper</code> serializes and deserializes our top-level domain object (the ToDo list) into JSON using the <a href="https://github.com/FasterXML/jackson" target="_blank">Jackson library</a>. The&nbsp;<code>open</code> and <code>close</code>&nbsp;Closures do the reading and writing respectively.
</p>
<p>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 multi-colored images. This is achieved using the icons from&nbsp;<span style="background-color: rgb(245, 245, 245); color: rgb(51, 51, 51); font-family: Menlo, Monaco, Consolas, &quot;Courier New&quot;, monospace; font-size: 13px;"><a href="https://github.com/pavlobu/emoji-text-flow-javafx" target="_blank">https://github.com/pavlobu/emoji-text-flow-javafx</a>.</span>&nbsp;The application is perfectly functional without them (and the approximately 20 lines for the&nbsp;<code>cellFactory</code> and <code>cellValueFactory</code> 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.</p>
<p>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.</p>
<pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'JetBrains Mono',monospace;font-size:11pt;"><span style="color:#cc7832;">def </span>graphicLabelFactory = <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span><span style="color:#cc7832;">new </span>ListCell&lt;ToDoCategory&gt;() {<br> <span style="color:#cc7832;">void </span>updateItem(ToDoCategory cat, <span style="color:#cc7832;">boolean </span>empty) {<br> <span style="color:#cc7832;">super</span>.updateItem(cat, empty)<br> <span style="color:#cc7832;">if </span>(!empty) {<br> <span style="color:#9876aa;">graphic </span>= <span style="color:#cc7832;">new </span>Label(cat.name()).tap <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span><span style="color:#9876aa;">graphic </span>= <span style="color:#cc7832;">new </span>ImageView(images[cat])<br> <span style="font-weight:bold;">}<br></span><span style="font-weight:bold;"> </span>}<br> }<br> }<br><span style="font-weight:bold;">}<br></span></pre>
<p>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.</p>
<pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'JetBrains Mono',monospace;font-size:11pt;"><span style="color:#cc7832;">def </span>graphicCellFactory = <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span><span style="color:#cc7832;">new </span>TableCell&lt;ToDoItem, ToDoItem&gt;() {<br> <span style="color:#cc7832;">void </span>updateItem(ToDoItem item, <span style="color:#cc7832;">boolean </span>empty) {<br> <span style="color:#9876aa;">graphic </span>= empty ? <span style="color:#cc7832;">null </span>: <span style="color:#cc7832;">new </span>ImageView(images[item.<span style="color:#9876aa;">category</span>])<br> <span style="color:#9876aa;">alignment </span>= Pos.<span style="color:#9876aa;font-style:italic;">CENTER<br></span><span style="color:#9876aa;font-style:italic;"> </span>}<br> }<br><span style="font-weight:bold;">}<br></span></pre>
<p>Finally, with these definitions out of the way, we can define our GroovyFX application for manipulating our ToDo list:</p><pre style="background-color:#2b2b2b;color:#a9b7c6;font-family:'JetBrains Mono',monospace;font-size:11pt;"><span style="color:#9876aa;font-style:italic;">start </span><span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span>stage(<span style="color:#6a8759;">title</span>: <span style="color:#6a8759;">'GroovyFX ToDo Demo'</span>, <span style="color:#6a8759;">show</span>: <span style="color:#cc7832;">true</span>, <span style="color:#6a8759;">onCloseRequest</span>: close) <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span>urls.each <span style="font-weight:bold;">{ </span>k, v <span style="font-weight:bold;">-&gt; </span>images[k] = image(<span style="color:#6a8759;">url</span>: v, <span style="color:#6a8759;">width</span>: <span style="color:#6897bb;">24</span>, <span style="color:#6a8759;">height</span>: <span style="color:#6897bb;">24</span>) <span style="font-weight:bold;">}<br></span><span style="font-weight:bold;"> </span>scene <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span>gridPane(<span style="color:#6a8759;">hgap</span>: <span style="color:#6897bb;">10</span>, <span style="color:#6a8759;">vgap</span>: <span style="color:#6897bb;">10</span>, <span style="color:#6a8759;">padding</span>: <span style="color:#6897bb;">20</span>) <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span>columnConstraints(<span style="color:#6a8759;">minWidth</span>: <span style="color:#6897bb;">80</span>, <span style="color:#6a8759;">halignment</span>: <span style="color:#6a8759;">'right'</span>)<br> columnConstraints(<span style="color:#6a8759;">prefWidth</span>: <span style="color:#6897bb;">250</span>)<br><br> label(<span style="color:#6a8759;">'Task:'</span>, <span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">1</span>, <span style="color:#6a8759;">column</span>: <span style="color:#6897bb;">0</span>)<br> task = textField(<span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">1</span>, <span style="color:#6a8759;">column</span>: <span style="color:#6897bb;">1</span>, <span style="color:#6a8759;">hgrow</span>: <span style="color:#6a8759;">'always'</span>)<br><br> label(<span style="color:#6a8759;">'Category:'</span>, <span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">2</span>, <span style="color:#6a8759;">column</span>: <span style="color:#6897bb;">0</span>)<br> category = comboBox(<span style="color:#6a8759;">items</span>: ToDoCategory.<span style="color:#9876aa;font-style:italic;">values</span>().toList(),<br> <span style="color:#6a8759;">cellFactory</span>: graphicLabelFactory, <span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">2</span>, <span style="color:#6a8759;">column</span>: <span style="color:#6897bb;">1</span>)<br><br> label(<span style="color:#6a8759;">'Date:'</span>, <span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">3</span>, <span style="color:#6a8759;">column</span>: <span style="color:#6897bb;">0</span>)<br> date = datePicker(<span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">3</span>, <span style="color:#6a8759;">column</span>: <span style="color:#6897bb;">1</span>)<br><br> table = tableView(<span style="color:#6a8759;">items</span>: items, <span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">4</span>, <span style="color:#6a8759;">columnSpan</span>: <span style="color:#9876aa;font-style:italic;">REMAINING</span>,<br> <span style="color:#6a8759;">onMouseClicked</span>: <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span><span style="color:#cc7832;">var </span>item = items[table.selectionModel.selectedIndex.value]<br> task.text = item.task<br> category.value = item.category<br> date.value = item.date<br> <span style="font-weight:bold;">}</span>) <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span>tableColumn(<span style="color:#6a8759;">property</span>: <span style="color:#6a8759;">'task'</span>, <span style="color:#6a8759;">text</span>: <span style="color:#6a8759;">'Task'</span>, <span style="color:#6a8759;">prefWidth</span>: <span style="color:#6897bb;">200</span>)<br> tableColumn(<span style="color:#6a8759;">property</span>: <span style="color:#6a8759;">'category'</span>, <span style="color:#6a8759;">text</span>: <span style="color:#6a8759;">'Category'</span>, <span style="color:#6a8759;">prefWidth</span>: <span style="color:#6897bb;">80</span>,<br> <span style="color:#6a8759;">cellValueFactory</span>: <span style="font-weight:bold;">{ </span><span style="color:#cc7832;">new </span>ReadOnlyObjectWrapper(it.value) <span style="font-weight:bold;">}</span>,<br> <span style="color:#6a8759;">cellFactory</span>: graphicCellFactory)<br> tableColumn(<span style="color:#6a8759;">property</span>: <span style="color:#6a8759;">'date'</span>, <span style="color:#6a8759;">text</span>: <span style="color:#6a8759;">'Date'</span>, <span style="color:#6a8759;">prefWidth</span>: <span style="color:#6897bb;">90</span>, <span style="color:#6a8759;">type</span>: Date)<br> <span style="font-weight:bold;">}<br></span><span style="font-weight:bold;"><br></span><span style="font-weight:bold;"> </span>hbox(<span style="color:#6a8759;">row</span>: <span style="color:#6897bb;">5</span>, <span style="color:#6a8759;">columnSpan</span>: <span style="color:#9876aa;font-style:italic;">REMAINING</span>, <span style="color:#6a8759;">alignment</span>: CENTER, <span style="color:#6a8759;">spacing</span>: <span style="color:#6897bb;">10</span>) <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span>button(<span style="color:#6a8759;">'Add'</span>, <span style="color:#6a8759;">onAction</span>: <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span><span style="color:#cc7832;">if </span>(task.text &amp;&amp; category.value &amp;&amp; date.value) {<br> items &lt;&lt; <span style="color:#cc7832;">new </span>ToDoItem(task.text, category.value, date.value)<br> }<br> <span style="font-weight:bold;">}</span>)<br> button(<span style="color:#6a8759;">'Update'</span>, <span style="color:#6a8759;">onAction</span>: <span style="font-weight:bold;">{</span>
<span style="color:#cc7832;">if </span>(task.text &amp;&amp; category.value &amp;&amp; date.value
&amp;&amp; !table.selectionModel.empty) {
<span style="color:#cc7832;">var </span>item = items[table.selectionModel.selectedIndex.value]<br> item.task = task.text<br> item.category = category.value<br> item.date = date.value<br> }<br> <span style="font-weight:bold;">}</span>)<br> button(<span style="color:#6a8759;">'Remove'</span>, <span style="color:#6a8759;">onAction</span>: <span style="font-weight:bold;">{<br></span><span style="font-weight:bold;"> </span><span style="color:#cc7832;">if </span>(!table.selectionModel.empty)<br> items.removeAt(table.selectionModel.selectedIndex.value)<br> <span style="font-weight:bold;">}</span>)<br> <span style="font-weight:bold;">}<br></span><span style="font-weight:bold;"> }<br></span><span style="font-weight:bold;"> }<br></span><span style="font-weight:bold;"> }<br></span><span style="font-weight:bold;">}<br></span></pre>
<p>We could have somewhat separated the concerns of application logic and display logic by placing the GUI part of this app in an&nbsp;<code>fxml</code> file. For our purposes however, we'll keep the whole application in one source file and use Groovy's declarative builder style.</p>
<p>Here is the application in use:</p><p><img src="https://blogs.apache.org/groovy/mediaresource/62c91bfb-6594-4858-8368-e17e518b7c26" alt="TodoScreenshot.png"><br></p>
<h3>Further information</h3>
<p>The code for this application can be found here:&nbsp;<a href="https://github.com/paulk-asert/groovyfx-todo" target="_blank">https://github.com/paulk-asert/groovyfx-todo</a>.</p><p>It's a Groovy 3 and JDK 8 application but see this <a href="https://blogs.apache.org/groovy/entry/reading-and-writing-csv-files" target="_blank">blog</a> if you want to see Jackson deserialization of classes and records (and Groovy's emulated records) from CSV files using the most recent Groovy and JDK versions.</p>