Heating Up Clojure & Swing

Most Swing examples don’t translate well into Clojure because they are so thoroughly embedded in the object-oriented paradigm.

A typical Swing example has a main class that extends a container class and implements some *Listener interface.  Clojure beginners who try to port these examples may think they need to mimic that same structure.

In fact, there are few situations where Swing forces you to create a subclass.  Most involve event listeners and can be handled adequately with Clojure’s proxy.

In this post, we’ll develop a complete, albeit small, Swing application: the classic Temperature Converter tutorial.  But ours will be better.  It can convert both Celsius to Fahrenheit and vice-versa, and it does the conversion immediately without waiting for a button click.

Gentleman, start your REPLs.

Start by importing all the classes we will need for this example:

(import '(javax.swing JLabel JPanel JFrame JTextField)
        '(javax.swing.event DocumentListener)
        '(java.awt GridBagLayout GridBagConstraints Insets))

Before we get to any GUI stuff, we need two functions to convert between degrees Celsius and degrees Fahrenheit:

(defn f-to-c [f]
  (* (- f 32) 5/9))

(defn c-to-f [c]
  (+ (* c 9/5) 32))

Note the use of Clojure Ratios.

Next, we need a couple of helper functions. First, the parse function will take a String typed by the user and convert it to a number. If the user types an invalid number, parse returns nil, but it can handle extra spaces:

(defn parse [s]
  (try (Double/parseDouble (.trim s))
       (catch NumberFormatException e nil)))

The display function does the opposite. It takes a number, rounds it to the nearest Integer (because Integers are prettier), and returns a String.

(defn display [n]
  (str (Math/round (float n))))

You can test these functions in the REPL:

(f-to-c 212) ;; => 100
(c-to-f 0)   ;; => 32

(parse "22.5")   ;; => 22.5
(parse " 22 ")   ;; => 22.0
(parse "foobar") ;; => nil

(display 22.5) ;; => "23"
(display 22/7) ;; => "3"

Now, time for some GUI goo!

Our temperature converter will have two text fields, one for Celsius and one for Fahrenheit, like this:

Temperature Converter GUI screenshot

As soon as the user types in either text field, the other text field will immediately change to show the converted value. So to convert Celsius to Fahrenheit, just type into the “Celsius” box. To go the other way, type into the “Fahrenheit” box.

I chose this design because it has two elements that behave very similarly, allowing me to show off some higher-order functions.

Let’s start with updating. We have two text fields. We want to update one, call it the “target” field, by converting the value of the other, call it the “source” field. But we never want to update a field while the user is typing in it! That would then trigger another update, causing an infinite loop.

Here’s the function:

(defn update-temp [source target convert]
  (when (.isFocusOwner source)
    (if-let [n (parse (.getText source))]
      (.setText target (display (convert n)))
      (.setText target ""))))

The source and target arguments are JTextFields, which we will create later. The update-temp function will be called every time one of the text fields emits a change event, so first we have to make sure that the source field (the one that triggered the event) has the keyboard focus. If it does, we try to read the source field’s contents. If parse is successful, we can calculate the value for the target field using the supplied convert function, otherwise we just set the target to be blank.

To make this work, we need to attach listeners to each of the JTextFields. We can write a function for that too:

(defn listen-temp [source target f]
  (.. source getDocument
      (addDocumentListener
       (proxy [DocumentListener] []
         (insertUpdate [e] (update-temp source target f))
         (removeUpdate [e] (update-temp source target f))
         (changedUpdate [e] )))))

Swing implements a Model-View-Controller paradigm for text fields. So to listen for changes in a text field, we actually want to listen to the Document model associated with that text field. The DocumentListener insertUpdate and removeUpdate methods are called whenever the Document changes. The changedUpdate method is not interesting to us, but we still have to provide an empty implementation in the proxy.

So now we’ve got general-purpose functions for updating one text field whenever another changes. Suppose we create two text fields named celsius and fahrenheit. Wiring them together only takes 2 lines:

(listen-temp celsius fahrenheit c-to-f)
(listen-temp fahrenheit celsius f-to-c)

And that’s exactly what you’ll see in the final app, below.

All that’s left is the layout. I’ll re-use the GridBagLayout macros from my previous post. Here they are again, for those of you following along at home:

(defmacro set-grid! [constraints field value]
  `(set! (. ~constraints ~(symbol (name field)))
         ~(if (keyword? value)
            `(. GridBagConstraints ~(symbol (name value)))
            value)))

(defmacro grid-bag-layout [container & body]
  (let [c (gensym "c")
        cntr (gensym "cntr")]
    `(let [~c (new java.awt.GridBagConstraints)
           ~cntr ~container]
       ~@(loop [result '() body body]
           (if (empty? body)
             (reverse result)
             (let [expr (first body)]
               (if (keyword? expr)
                 (recur (cons `(set-grid! ~c ~expr
                                          ~(second body))
                              result)
                        (next (next body)))
                 (recur (cons `(.add ~cntr ~expr ~c)
                              result)
                        (next body)))))))))

And, last but not least, here’s the app itself:

(defn temp-app []
  (let [celsius (JTextField. 3)
        fahrenheit (JTextField. 3)
        panel (doto (JPanel. (GridBagLayout.))
                (grid-bag-layout
                 :gridx 0, :gridy 0, :anchor :LINE_END
                 :insets (Insets. 5 5 5 5)
                 (JLabel. "Degrees Celsius:")
                 :gridy 1
                 (JLabel. "Degrees Fahrenheit:")
                 :gridx 1, :gridy 0, :anchor :LINE_START
                 celsius
                 :gridy 1
                 fahrenheit))]
    (listen-temp celsius fahrenheit c-to-f)
    (listen-temp fahrenheit celsius f-to-c)
    (doto (JFrame. "Temperature Converter")
      (.setContentPane panel)
      (.pack)
      (.setVisible true))))

Run the function (temp-app) and you’ve got yourself a temperature converter.  Makes me feel all warm and GUI inside.

5 thoughts on “Heating Up Clojure & Swing

  1. verec

    Nice touch.

    But it should be pointed out that by default this is executed from the REPL thread which the main thread (that which ‘public static void main(String[] args)’ is called from) and this is a bad thing as everything in Swing should be called from the event dispatch thread.

    This is easily fixed though: (SwingUtilites/invokeLater temp-app) will do the job, as every Clojure fn is also a java.lang.Runnable.

  2. Stuart Post author

    verec wrote: “everything in Swing should be called from the event dispatch thread.”

    Yes, I’ve beet putting off a discussion of the event dispatch thread. But you are entirely correct. Perhaps it’s time for me to write about concurrency in Swing.

  3. Pingback: Destillat 08-01-2010 | duetsch.info - Open Source, Wet-, Web-, Software

  4. markc

    @veree

    You had a typo that caused me a bit of confusion. Should be:

    (javax.swing.SwingUtilities/invokeLater temp-app) ;; note the extra “i”

Comments are closed.