| --- |
| 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> 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<List<ToDoItem>>() {}) <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 <code>open</code> and <code>close</code> 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 <span style="background-color: rgb(245, 245, 245); color: rgb(51, 51, 51); font-family: Menlo, Monaco, Consolas, "Courier New", 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> The application is perfectly functional without them (and the approximately 20 lines for the <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<ToDoCategory>() {<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<ToDoItem, ToDoItem>() {<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;">-> </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 && category.value && date.value) {<br> items << <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 && category.value && date.value
|
| && !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 <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: <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> |