I want to create a desktop application with Clojure.
Some time ago I wrote one using seesaw which is a really good clojure library for using swing. But I want to move on and give JavaFX a try. It comes with the oracle jdk 7 and is planed to be included in openjdk …well… 9 … but it’s better than nothing.
There are some tutorials for using JavaFX with Clojure, but not a whole lot. Most of them using gen-class to extend the Application class. This won’t work well with the repl. But we love our repl.
Daniel Ziltener suggests another approach in his Blogpost. He basicly wraps all JavaFX code in a “run-now” macro to make sure it is executed in the JavaFX thread. This is a very neat approach, and we will use this to start our application.
He also wrotes some helpers for dealing with JavaFx called ClojureFx. I haven’t look deeply into it, but it looks like it’s the run-now part and some helpers for building Components and adding Listener.
But I want to use the JavaFX Scene Builder (so hopefully won’t need any builders) and I want to give Functional Reactive Programming a try (in form of weavejesters reagi).
This was quite easy :). The Builder will produce an fxml file, which we want to store in the resource-folder of a new leiningen project.
You can look at the full code at github.
As mentioned we will need the “run-now” macro, so we create a namespace to borrow it and use it in the core namespace.
For loading the fxml file we need to import the FXMLLoader and the StageBuilder and Scene to start the app.
(make sure you have the jfxrt.jar in classpath by either using the oracle jre or pulling it in your jre manually)
(ns stupid-fx-app.core (:use stupid-fx-app.run-now) (:import [ javafx.fxml FXMLLoader] [ javafx.stage StageBuilder] [ javafx.scene Scene]))
Now we create a Stage, load the fxml file and start the app:
(def stage (run-now (doto (.build (StageBuilder/create)) (.setTitle "stupid-fx-app") (.setScene (Scene. (FXMLLoader/load (clojure.java.io/resource "stupid-app.fxml"))))))) (run-now (.show stage))
Cool stuff, but for now the App doesn’t do alot.
Before it can do anything else than looking cool, we have to think about things it should do. Because it’s a stupid-app we will go with the following things:
- Every click on the button should rotate the TextField for some degrees
- Every input in the TextField should be parsed to a color and change the Button’s text-color (after submit it via enter)
Sounds stupid enough for me. Let’s settle this!
So how to get our actions out of the App?
Essentially there are two ways to achieve this. Writing Controllers (meh.. sounds javaish) or using the script feature of JavaFX. We will go with the script feature (and hope there is no performance issue, I absolutly have no idea). By script-feature I mean the following: You can put any code directly into the fxml file, if there is an implementation of JSR-223.
So we’ll need to get a clojure implemenation in our classpath by adding
to our leiningen dependencies (There are some other jars, but I didn’t found one using clojure 1.5.1).
We also need reagi, so we add
Make sure to restart your repl or use magic to get this jar into the classpath.
A spinal to help
Before we can pimp our fxml to do cool stuff with scripts, we need another helper namespace.
It will manage the main event stream, where everything comes together, so it is called:
(ns stupid-fx-app.spinal (:require [reagi.core :as r]) (def spinal (r/events))) (defn push [event] (r/push! spinal event))
We define an reagi event stream and a small function to push events onto it. The idea behind this is to get JavaFX to push events onto this stream, so we can use reagi to react to them in a FRP manner.
Now we have everything we need extend the fxml in the Scene Builder.
First we give an id to the Button and the TextField, to find them later.
section properties / javaFX CSS / Button id = rotateButton ; TextField id = buttonText
(use 'stupid-fx-app.spinal) (push event)
in there. This has to be done for the Button and the TextField.
After this we can save the fxml. There is only one problem left with the fxml. We haven’t defined the script-language we want to use. It seems like this is not possible via the scene builder, so we open the fxml with an editor and add
after the xml tag.
If the app is still open, we need to close it and reload it with the changed fxml. Now let’s check out the button.
Nothing is happening? Well not now, but let’s bring the spinal into the namespace (via use or require in the ns def) and deref it:
=> @spinal #<ActionEvent javafx.event.ActionEvent[ source=Button[id=rotateButton, styleClass=button]]>
Hello ActionEvent! Now we can go for our stupid stuff. First we might want to filter the spinal to have different streams for ButtonClicks and TextBoxChanges. To do this we need reagi in the core ns (here required as r).
Of course we could have done this by pushing on different streams from the app, but I want all events to be in one place.
(def button-stream (r/filter #(= "rotateButton" (.getId (.getSource %))) spinal)) (def text-stream (r/filter #(= "buttonText" (.getId (.getSource %))) spinal))
(remember the ids we set in the Scene Builder?)
The result are two streams holding the different events.
If you want to make sure, just deref the streams and do things in the app.
Now we map some side-effects onto these streams.
(defn rotate [node] (.setRotate node (+ (.getRotate node) 20))) (r/map (fn [event] (let (-> (.getSource event) (.getScene) (.lookup "#rotateButton") (change-text-color text)))) text-stream)
TextField should rotate with every click. (Of course we can change the rotation degree by simply redef the rotate-fn). Half way there.
We need to import
[ javafx.scene.paint Color]
for coloring and write the code for changing color.
(defn change-text-color [node text] (try (.setTextFill node (Color/valueOf text)) (catch IllegalArgumentException e))) (r/map (fn [event] (-> (.getSource event) (.getScene) (.lookup "#rotateButton") (change-text-color text))) text-stream)
So here we are, rotating TextFields and changing ButtonTextColors! 🙂
Nothing is Flawlass
There were some plans I changed while writing this, because it was not working. So I wasn’t able to change the Text of the button and I couldn’t react on the textfield input immediately (without pressing enter) (“on key typed” event worked but the event was faster processed than the text in the field really changed).
I can’t say if I just haven’t found the right knob or this whole approach is as stupid as the app, but I had fun playing around with this stuff and will probably go on.