| <!DOCTYPE html> |
| <!--[if lt IE 7]> <html class="no-js lt-ie9 lt-ie8 lt-ie7"> <![endif]--> |
| <!--[if IE 7]> <html class="no-js lt-ie9 lt-ie8"> <![endif]--> |
| <!--[if IE 8]> <html class="no-js lt-ie9"> <![endif]--> |
| <!--[if gt IE 8]><!--> <html class="no-js"> <!--<![endif]--><head> |
| <meta charset='utf-8'/><meta http-equiv='X-UA-Compatible' content='IE=edge'/><meta name='viewport' content='width=device-width, initial-scale=1'/><meta name='keywords' content='groovy, groovyfx, javafx, emoji, jackson databind'/><meta name='description' content='This blog looks at a GroovyFX TODO application.'/><title>The Apache Groovy programming language - Blogs - Adventures with GroovyFX</title><link href='../img/favicon.ico' type='image/x-ico' rel='icon'/><link rel='stylesheet' type='text/css' href='../css/bootstrap.css'/><link rel='stylesheet' type='text/css' href='../css/font-awesome.min.css'/><link rel='stylesheet' type='text/css' href='../css/style.css'/><link rel='stylesheet' type='text/css' href='https://cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.css'/> |
| </head><body> |
| <div id='fork-me'> |
| <a href='https://github.com/apache/groovy'> |
| <img style='position: fixed; top: 20px; right: -58px; border: 0; z-index: 100; transform: rotate(45deg);' src='/img/horizontal-github-ribbon.png'/> |
| </a> |
| </div><div id='st-container' class='st-container st-effect-9'> |
| <nav class='st-menu st-effect-9' id='menu-12'> |
| <h2 class='icon icon-lab'>Socialize</h2><ul> |
| <li> |
| <a href='https://groovy-lang.org/mailing-lists.html' class='icon'><span class='fa fa-envelope'></span> Discuss on the mailing-list</a> |
| </li><li> |
| <a href='https://twitter.com/ApacheGroovy' class='icon'><span class='fa fa-twitter'></span> Groovy on Twitter</a> |
| </li><li> |
| <a href='https://groovy-lang.org/events.html' class='icon'><span class='fa fa-calendar'></span> Events and conferences</a> |
| </li><li> |
| <a href='https://github.com/apache/groovy' class='icon'><span class='fa fa-github'></span> Source code on GitHub</a> |
| </li><li> |
| <a href='https://groovy-lang.org/reporting-issues.html' class='icon'><span class='fa fa-bug'></span> Report issues in Jira</a> |
| </li><li> |
| <a href='http://stackoverflow.com/questions/tagged/groovy' class='icon'><span class='fa fa-stack-overflow'></span> Stack Overflow questions</a> |
| </li><li> |
| <a href='http://groovycommunity.com/' class='icon'><span class='fa fa-slack'></span> Slack Community</a> |
| </li> |
| </ul> |
| </nav><div class='st-pusher'> |
| <div class='st-content'> |
| <div class='st-content-inner'> |
| <!--[if lt IE 7]> |
| <p class="browsehappy">You are using an <strong>outdated</strong> browser. Please <a href="http://browsehappy.com/">upgrade your browser</a> to improve your experience.</p> |
| <![endif]--><div><div class='navbar navbar-default navbar-static-top' role='navigation'> |
| <div class='container'> |
| <div class='navbar-header'> |
| <button type='button' class='navbar-toggle' data-toggle='collapse' data-target='.navbar-collapse'> |
| <span class='sr-only'></span><span class='icon-bar'></span><span class='icon-bar'></span><span class='icon-bar'></span> |
| </button><a class='navbar-brand' href='../index.html'> |
| <i class='fa fa-star'></i> Apache Groovy |
| </a> |
| </div><div class='navbar-collapse collapse'> |
| <ul class='nav navbar-nav navbar-right'> |
| <li class=''><a href='https://groovy-lang.org/learn.html'>Learn</a></li><li class=''><a href='https://groovy-lang.org/documentation.html'>Documentation</a></li><li class=''><a href='/download.html'>Download</a></li><li class=''><a href='https://groovy-lang.org/support.html'>Support</a></li><li class=''><a href='/'>Contribute</a></li><li class=''><a href='https://groovy-lang.org/ecosystem.html'>Ecosystem</a></li><li class=''><a href='/blog'>Blog posts</a></li><li class=''><a href='https://groovy.apache.org/events.html'></a></li><li> |
| <a data-effect='st-effect-9' class='st-trigger' href='#'>Socialize</a> |
| </li><li class=''> |
| <a href='../search.html'> |
| <i class='fa fa-search'></i> |
| </a> |
| </li> |
| </ul> |
| </div> |
| </div> |
| </div><div id='content' class='page-1'><div class='row'><div class='row-fluid'><div class='col-lg-3'><ul class='nav-sidebar'><li><a href='./'>Blog index</a></li><li class='active'><a href='#doc'>Adventures with GroovyFX</a></li><li><a href='#_further_information' class='anchor-link'>Further information</a></li></ul><br/><ul class='nav-sidebar'><li style='padding: 0.35em 0.625em; background-color: #eee'><span>Related posts</span></li><li><a href='./deep-learning-and-eclipse-collections'>Deep Learning and Eclipse Collections</a></li><li><a href='./reading-and-writing-csv-files'>Reading and Writing CSV files with Groovy</a></li><li><a href='./zipping-collections-with-groovy'>Zipping Collections with Groovy</a></li><li><a href='./helloworldemoji'>Hello World with Emojis</a></li><li><a href='./fruity-eclipse-collections'>Fruity Eclipse Collections</a></li><li><a href='./set-operations-with-groovy'>Set Operators with Groovy</a></li><li><a href='./seasons-greetings-emoji'>Season’s Greetings with Emojis</a></li></ul></div><div class='col-lg-8 col-lg-pull-0'><a name='doc'></a><h1>Adventures with GroovyFX</h1><p><span>Author: <i>Paul King</i></span><br/><span>Published: 2022-12-12 02:22PM</span></p><hr/><div id="preamble"> |
| <div class="sectionbody"> |
| <div class="paragraph"> |
| <p>This blog looks at a <a href="http://groovyfx.org/">GroovyFX</a> version of a <a href="https://donraab.medium.com/my-weird-and-wonderful-first-adventures-with-javafx-6efe3b1923c8">ToDo application originally written in JavaFX</a>. |
| First we start with a <code>ToDoCategory</code> enum of our ToDo categories:</p> |
| </div> |
| <div class="listingblock"> |
| <div class="content"> |
| <pre class="prettyprint highlight"><code data-lang="groovy">enum ToDoCategory { |
| EXERCISE("🚴"), |
| WORK("📊"), |
| RELAX("🧘"), |
| TV("📺"), |
| READ("📚"), |
| EVENT("🎭"), |
| CODE("💻"), |
| COFFEE("☕️"), |
| EAT("🍽"), |
| SHOP("🛒"), |
| SLEEP("😴") |
| |
| final String emoji |
| |
| ToDoCategory(String emoji) { |
| this.emoji = emoji |
| } |
| }</code></pre> |
| </div> |
| </div> |
| <div class="paragraph"> |
| <p>We will have a <code>ToDoItem</code> class containing the todo task, the previously mentioned category and the due date.</p> |
| </div> |
| <div class="listingblock"> |
| <div class="content"> |
| <pre class="prettyprint highlight"><code data-lang="groovy">@Canonical |
| @JsonIncludeProperties(['task', 'category', 'date']) |
| @FXBindable |
| class ToDoItem { |
| final String task |
| final ToDoCategory category |
| final LocalDate date |
| }</code></pre> |
| </div> |
| </div> |
| <div class="paragraph"> |
| <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> |
| </div> |
| <div class="paragraph"> |
| <p>Next, we’ll define some helper variables:</p> |
| </div> |
| <div class="listingblock"> |
| <div class="content"> |
| <pre class="prettyprint highlight"><code data-lang="groovy">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"] |
| }</code></pre> |
| </div> |
| </div> |
| <div class="paragraph"> |
| <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">Jackson library</a>. The <code>open</code> and <code>close</code> Closures do the reading and writing respectively.</p> |
| </div> |
| <div class="paragraph"> |
| <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 multicolored images. This is achieved using the icons from <a href="https://github.com/pavlobu/emoji-text-flow-javafx" class="bare">https://github.com/pavlobu/emoji-text-flow-javafx</a>. |
| 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> |
| </div> |
| <div class="paragraph"> |
| <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> |
| </div> |
| <div class="listingblock"> |
| <div class="content"> |
| <pre class="prettyprint highlight"><code data-lang="groovy">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]) |
| } |
| } |
| } |
| } |
| }</code></pre> |
| </div> |
| </div> |
| <div class="paragraph"> |
| <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> |
| </div> |
| <div class="listingblock"> |
| <div class="content"> |
| <pre class="prettyprint highlight"><code data-lang="groovy">def graphicCellFactory = { |
| new TableCell<ToDoItem, ToDoItem>() { |
| void updateItem(ToDoItem item, boolean empty) { |
| graphic = empty ? null : new ImageView(images[item.category]) |
| alignment = Pos.CENTER |
| } |
| } |
| }</code></pre> |
| </div> |
| </div> |
| <div class="paragraph"> |
| <p>Finally, with these definitions out of the way, we can define our GroovyFX application for manipulating our ToDo list:</p> |
| </div> |
| <div class="listingblock"> |
| <div class="content"> |
| <pre class="prettyprint highlight"><code data-lang="groovy">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) |
| }) |
| } |
| } |
| } |
| } |
| }</code></pre> |
| </div> |
| </div> |
| <div class="paragraph"> |
| <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> |
| </div> |
| <div class="paragraph"> |
| <p>Here is the application in use: |
| <span class="image"><img src="img/TodoScreenshot.png" alt="TodoScreenshot"></span></p> |
| </div> |
| </div> |
| </div> |
| <div class="sect1"> |
| <h2 id="_further_information">Further information</h2> |
| <div class="sectionbody"> |
| <div class="paragraph"> |
| <p>The code for this application can be found here:</p> |
| </div> |
| <div class="paragraph"> |
| <p><a href="https://github.com/paulk-asert/groovyfx-todo" class="bare">https://github.com/paulk-asert/groovyfx-todo</a></p> |
| </div> |
| <div class="paragraph"> |
| <p>It’s a Groovy 3 and JDK 8 application but see this |
| <a href="https://groovy.apache.org/blog/reading-and-writing-csv-files">blog post</a> |
| 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.</p> |
| </div> |
| </div> |
| </div></div></div></div></div><footer id='footer'> |
| <div class='row'> |
| <div class='colset-3-footer'> |
| <div class='col-1'> |
| <h1>Groovy</h1><ul> |
| <li><a href='https://groovy-lang.org/learn.html'>Learn</a></li><li><a href='https://groovy-lang.org/documentation.html'>Documentation</a></li><li><a href='/download.html'>Download</a></li><li><a href='https://groovy-lang.org/support.html'>Support</a></li><li><a href='/'>Contribute</a></li><li><a href='https://groovy-lang.org/ecosystem.html'>Ecosystem</a></li><li><a href='/blog'>Blog posts</a></li><li><a href='https://groovy.apache.org/events.html'></a></li> |
| </ul> |
| </div><div class='col-2'> |
| <h1>About</h1><ul> |
| <li><a href='https://github.com/apache/groovy'>Source code</a></li><li><a href='https://groovy-lang.org/security.html'>Security</a></li><li><a href='https://groovy-lang.org/learn.html#books'>Books</a></li><li><a href='https://groovy-lang.org/thanks.html'>Thanks</a></li><li><a href='http://www.apache.org/foundation/sponsorship.html'>Sponsorship</a></li><li><a href='https://groovy-lang.org/faq.html'>FAQ</a></li><li><a href='https://groovy-lang.org/search.html'>Search</a></li> |
| </ul> |
| </div><div class='col-3'> |
| <h1>Socialize</h1><ul> |
| <li><a href='https://groovy-lang.org/mailing-lists.html'>Discuss on the mailing-list</a></li><li><a href='https://twitter.com/ApacheGroovy'>Groovy on Twitter</a></li><li><a href='https://groovy-lang.org/events.html'>Events and conferences</a></li><li><a href='https://github.com/apache/groovy'>Source code on GitHub</a></li><li><a href='https://groovy-lang.org/reporting-issues.html'>Report issues in Jira</a></li><li><a href='http://stackoverflow.com/questions/tagged/groovy'>Stack Overflow questions</a></li><li><a href='http://groovycommunity.com/'>Slack Community</a></li> |
| </ul> |
| </div><div class='col-right'> |
| <p> |
| The Groovy programming language is supported by the <a href='http://www.apache.org'>Apache Software Foundation</a> and the Groovy community. |
| </p><div text-align='right'> |
| <img src='../img/asf_logo.png' title='The Apache Software Foundation' alt='The Apache Software Foundation' style='width:60%'/> |
| </div><p>Apache® and the Apache feather logo are either registered trademarks or trademarks of The Apache Software Foundation.</p> |
| </div> |
| </div><div class='clearfix'>© 2003-2024 the Apache Groovy project — Groovy is Open Source: <a href='http://www.apache.org/licenses/LICENSE-2.0.html' alt='Apache 2 License'>license</a>, <a href='https://privacy.apache.org/policies/privacy-policy-public.html'>privacy policy</a>.</div> |
| </div> |
| </footer></div> |
| </div> |
| </div> |
| </div> |
| </div><script src='../js/vendor/jquery-1.10.2.min.js' defer></script><script src='../js/vendor/classie.js' defer></script><script src='../js/vendor/bootstrap.js' defer></script><script src='../js/vendor/sidebarEffects.js' defer></script><script src='../js/vendor/modernizr-2.6.2.min.js' defer></script><script src='../js/plugins.js' defer></script><script src='https://cdnjs.cloudflare.com/ajax/libs/prettify/r298/prettify.min.js'></script><script>document.addEventListener('DOMContentLoaded',prettyPrint)</script><script> |
| (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){ |
| (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o), |
| m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m) |
| })(window,document,'script','//www.google-analytics.com/analytics.js','ga'); |
| |
| ga('create', 'UA-257558-10', 'auto'); |
| ga('send', 'pageview'); |
| </script> |
| </body></html> |