Home > clojure, seesaw > Seesaw Widget Binding

Seesaw Widget Binding

July 11th, 2011

Lately I’ve been working on the problem of binding widgets and widgets (and data) together in Seesaw. I think it’s in a state to throw it out there and solicit feedback from anyone who’s interested. This work is similar to the arrow work done in Hafni, but takes a somewhat different approach and builds on top of the pretty extensive infrastructure that’s already in place in Seesaw.

The full source the for example shown here is here.

Motivation

Let’s say you were creating a search box where the user can enter a regular expression. You’d like to give some feedback whether the expression is valid or not, say by turning the text box red and updating a status label. Additionally, you’d like an “enabled” checkbox which enables or disables the search. Yeah, it’s contrived, deal with it :)

So, the usual approach would be something like this:

  • Set up the widgets
  • Add a selection change listener to the check box. When it fires, check its value and set the enablement of the search box
  • Add a document change listener to the text box. When it fires, parse the regex, set the color of the text box and update the status label appropriately

Not horrible, but kind of a hassle. If you turn your head and squint, you can kind of see a couple data flows going on here. First, the boolean value of the checkbox flows to the boolean enablement of the searchbox. Second, the text of the search box is transformed into a regex and then into a color (or status label) depending on success of failure. That color is then fed into the background color of the search box.

Seesaw’s binding (from Java Beans Binding, better names welcome) framework let’s you express this dataflow more directly.

Solution

So, the framework’s workhorse function is (seesaw.bind/bind). It takes a list of “Bindables” (see below) and hooks their values together into a chain. When a value at the start of a chain changes, it’s passed through the rest of the chain. bind returns a composite Bindable which can be composed into other chains.

So, for example, we can bind the value of a text box to an atom:

  (let [txt (text)
        a   (atom)]
    (bind txt a))

when the user types in the text box, the atom’s value changes to match it. There are several bindables already supported:

  • Text boxes, labels, sliders, atoms, etc are all bindable in the way you (or at least I) would expect.
  • (property widget property-name) – bind to a property of a widget
  • (selection widget) – bind to the current selection of a widget
  • (transform f) – transform a value with a function and pass it along
  • (some pred) – like (clojure.core/some) only pass along truthy values returned by a predicate.
  • (tee ... bindables ...) – split (demux) the chain into several independent chains.

Now back to our example. Here’s the annotated code for binding the search pattern logic. Here pattern is a text box, and status is a label:

    (b/bind 
      ; As the text of the textbox changes ...
      pattern
      ; Convert it to a regex, or nil if it's invalid
      (b/transform #(try (re-pattern %) (catch Exception e nil)))
      ; Now tee into two chains ...
      (b/tee
        ; The first path sets the color of the text box depending
        ; on whether the regex was valid
        (b/bind 
          (b/transform #(if % "white" "lightcoral")) 
          (b/property pattern :background))
        ; The second path sets the text of the status label
        (b/bind 
          (b/transform #(if % "Ready" "Invalid regex")) 
          status)))

Kinda cool? Similarly, we can hook a checkbox to the textbox enablement:

    (b/bind (b/selection (select f [:#enable]))
            (b/property  (select f [:#search]) :enabled?))

In this example, we’re using Seesaw selectors to find the checkbox (:#enable) and search text box (:#search).

Conclusion

I think this covers a lot of the tedious UI state management tasks that come up. Although atom binding is supported, it doesn’t seem like something you want to be doing often. It’s just doesn’t seem Clojure-y to me. Maybe a bindable atom that works more like (clojure.core/swap!) than (clojure.core/reset!) would be more appealing.

I’m looking for feedback, so if you find this interesting, useful, or offensive, please let me know!

clojure, seesaw ,

  1. Myke
    July 13th, 2011 at 07:14 | #1

    Wow! Seesaw is getting better and better. A fresh approach after wxPython projects make me wanna move out old projects into Clojure+Seesaw rewrites :)

  2. July 13th, 2011 at 13:24 | #2

    @Myke
    Go for it! :)

  3. Vladimir
    January 11th, 2013 at 16:26 | #3

    HI, could you pleas tell me this function passed to the `(b/transform` has to be pure or it can be any function with side effect. For instance when atom has changed its value – make connection based on that value

  4. Vladimir
    January 11th, 2013 at 16:37 | #4

    @Vladimir
    Ok I see know that it is not a good way, even from the function name …

  5. January 11th, 2013 at 16:41 | #5

    @Vladimir
    Check out `(seesaw.bind/b-do)` if you want to take a side-effecty action when a binding value changes.

  6. Ywen
    June 11th, 2013 at 11:32 | #6

    When I see your b/bind example, there is really one thing that is shouting at me in my head: MONAD!

    This really _calls_ for the use of a monad. Expressed in terms of algo.monads, it could give something like that:

    (domonad swing-dataflow
    ; As the text of the textbox changes …
    [p (b/text pattern)
    ; Convert it to a regex, or nil if it's invalid
    regex (try (re-pattern p)
    (catch Exception e nil)))
    bg (if regex "white" "lightcoral")
    _ (b/config! pattern :background bg)
    ; The second path sets the text of the status label
    validity (if regex "Ready" "Invalid regex")
    _ (b/text! status validity)])

    Note that the b/tee’s has disappeared: the state of the data in your flow is now made explicit (here, the data that was fed into the branches of the tee is the variable ‘regex’).

    If you want to go deeper in theory, it actually fits a more general abstraction than monads: applicative functors. (but what applicative functors can do, monads can do, and I don’t know if algo.monads has its applicative counterpart in the Clojure ecosystem). And if you have a long time ahead of you (because there is a lot of content), you can check the FRP libraries in Haskell, they address the same need of reactive programming (data automatically flowing through the components).?

  7. Rulo
    December 14th, 2013 at 19:00 | #7

    How do you make a bidirectional bind? So if you change a text field content an atom is changed, and if you change the atom then the text field content is changed?

  8. December 14th, 2013 at 23:40 | #8

    @Rulo
    just call bind in both directions. (b/bind txt a) (b/bind a txt). In theory, that should work.

  1. February 12th, 2012 at 16:34 | #1