Saturday, August 13, 2011

Abstracting Properties GUIs

I've had a lot of ideas running around in my head related to things like government and education. However, I figured that before I posted on those I should do one that is directly related to programming, so here it is.

A problem I run into fairly frequently is having an object that has properties that I want a user to be able to interact with through a GUI. In the past this was typically something I encountered in Java. This last week I hit it when writing code for my textbook using Scala. Some "standard" approaches would be to a JavaBeans approach using reflection to make the display code generic of apply an MVC pattern. These are big hammers for what is often a fairly small problem. Those are also solutions that aren't going to fly early in a second semester course

To give you a feel for what I'm talking about, consider this code for the situation I'm talking about.

class Fractal extends DrawLeaf {
  private var (xmin,xmax,ymin,ymax) = (-1.5,0.5,-1.0,1.0)
  private var (width,height) = (600,600)
  private var maxIters = 100
  private var propPanel:Component = null

  // code we don't care about that uses data

  def propertiesPanel() : Component = {
    if(propPanel==null) {
      propPanel = new BorderPanel {
        layout += new GridPanel(properties.length,1) {
          contents += new BorderPanel {
            layout += new Label("X min") -> BorderPanel.Position.West
            layout += new TextField(xmin.toString) {
              listenTo(this)
              reactions += { case e:EditDone => xmin=text.toDouble; changed = true }
            } -> BorderPanel.Position.Center
          }
          contents += new BorderPanel {
            layout += new Label("X max") -> BorderPanel.Position.West
            layout += new TextField(xmax.toString) {
              listenTo(this)
              reactions += { case e:EditDone => xmax=text.toDouble; changed = true }
            } -> BorderPanel.Position.Center
          }
          contents += new BorderPanel {
            layout += new Label("Y min") -> BorderPanel.Position.West
            layout += new TextField(ymin.toString) {
              listenTo(this)
              reactions += { case e:EditDone => ymin=text.toDouble; changed = true }
            } -> BorderPanel.Position.Center
          }
          contents += new BorderPanel {
            layout += new Label("Y max") -> BorderPanel.Position.West
            layout += new TextField(ymax.toString) {
              listenTo(this)
              reactions += { case e:EditDone => ymax=text.toDouble; changed = true }
            } -> BorderPanel.Position.Center
          }
          contents += new BorderPanel {
            layout += new Label("Width") -> BorderPanel.Position.West
            layout += new TextField(width.toString) {
              listenTo(this)
              reactions += { case e:EditDone => width=text.toInt; changed = true }
            } -> BorderPanel.Position.Center
          }
          contents += new BorderPanel {
            layout += new Label("Height") -> BorderPanel.Position.West
            layout += new TextField(height.toString) {
              listenTo(this)
              reactions += { case e:EditDone => height=text.toInt; changed = true }
            } -> BorderPanel.Position.Center
          }
          contents += new BorderPanel {
            layout += new Label("Max Count") -> BorderPanel.Position.West
            layout += new TextField(maxCount.toString) {
              listenTo(this)
              reactions += { case e:EditDone => maxCount=text.toInt; changed = true }
            } -> BorderPanel.Position.Center
          }
        } -> BorderPanel.Position.North
      }
    }
    propPanel
  }
}
I have 7 properties here and a whole bunch of redundant code to set up a GUI for setting those properties. This code is ugly. When I write it, it kind of makes me want to cry. There are basically 7 copies of the same thing here with only minor variations. In Java I never found a way to deal with this that I liked that I could present at this level. The standard rule of "abstract that which varies" led to huge code overhead or writing a separate library for fields that could be edited via code.

There are three things that vary. The first is just a String. The second is a value that is converted to a String. The third is setting a value. The third one is the challenge. In C/C++ you could use references/pointers to handle the setting code here, but you would have bigger challenges if what you were setting weren't "primitive" types. In Java you don't have references so you need to encapsulate setting functionality. That means making an interface with a single method and making a bunch of anonymous inner classes. Huge overhead. The code winds up being even longer than the original and as such it isn't really significantly easier to modify.

Scala provides a much nicer solution, mainly because function literals can be written so succinctly. Having tuples helps too. So I make an array of tuples. Each element in the tuples corresponds to one of the things that varies. A loop can run through and add them all.

  private val properties:Seq[(String,() => Any,String => Unit)] = Seq(
    ("Real Min", () => rmin, s => rmin = s.toDouble),
    ("Real Max", () => rmax, s => rmax = s.toDouble),
    ("Imaginary Min", () => imin, s => imin = s.toDouble),
    ("Imaginary Max", () => imax, s => imax = s.toDouble),
    ("Width", () => width, s => width = s.toInt),
    ("Height", () => height, s => height = s.toInt),
    ("Max Count", () => maxCount, s => maxCount = s.toInt)
  )

  def propertiesPanel() : Component = {
    if(propPanel==null) {
      propPanel = new BorderPanel {
        layout += new GridPanel(properties.length,1) {
          for((propName,value,setter) <- properties) {
            contents += new BorderPanel {
              layout += new Label(propName) -> BorderPanel.Position.West
              layout += new TextField(value().toString) {
                listenTo(this)
                reactions += { case e:EditDone => setter(text); changed = true }
              } -> BorderPanel.Position.Center
            }
          }
        } -> BorderPanel.Position.North
      }
    }
    propPanel
  }
This code isn't just shorter, it is less brittle and easier to maintain. Want to add a new property? No problem. Declare the variable and add one line of code to the sequence. Need a more complex set? That's not a problem either.

As written here, this code won't handle things that don't go into text fields, but it isn't hard to imagine an extension that does that. I need to play with it some more, but I think the "properties sequence" that includes getter and setter functionality might be a great way to write a general library for properties that is easy to work with and maintain. Pull the GUI building part out and the presentation becomes independent of the data as well.

No comments:

Post a Comment