Skip to content

Instantly share code, notes, and snippets.

@drdozer
Created April 5, 2012 14:29
Show Gist options
  • Save drdozer/2311463 to your computer and use it in GitHub Desktop.
Save drdozer/2311463 to your computer and use it in GitHub Desktop.
ultra-lightweight reactive swing
/** A mixin that exposes the text and document of any Swing component using the Document abstraction.
The text updates when the input is accepted by the component. The document text reflects what is typed. */
trait TextVar {
/** Things that must be provided by any class that this is mixed into. */
self : {
def getText(): String
def setText(s: String): Unit
def getDocument(): Document
def addActionListener(al: ActionListener): Unit
} =>
/** The text as a `Var`. */
val textVar = new Var[String](getText()) {
protected override def update(t: String) {
setText(t)
super.update(t)
}
}
self.addActionListener(new ActionListener {
def actionPerformed(e: ActionEvent) {
textVar := getText()
}
})
private def dTxt = {
val d = getDocument()
d.getText(0, d.getLength)
}
/** The document text as a var. */
val documentText = new Var[String](dTxt)
// initialize the listeners
getDocument().addDocumentListener(new DocumentListener {
def changedUpdate(e: DocumentEvent) { documentText := dTxt }
def removeUpdate(e: DocumentEvent) { documentText := dTxt }
def insertUpdate(e: DocumentEvent) { documentText := dTxt }
})
}
/** Mixin for components that can be enabled and disabled. */
trait EnabledVar {
self : {
def setEnabled(b: Boolean): Unit
def isEnabled(): Boolean
def addPropertyChangeListener(propertyName: String, listener: PropertyChangeListener): Unit
} =>
val enabledVar = new Var[Boolean](isEnabled()) {
enabledSelf =>
protected override def update(b: Boolean) {
setEnabled(b)
}
self.addPropertyChangeListener("enabled", new PropertyChangeListener() {
def propertyChange(evt: PropertyChangeEvent): Unit = enabledSelf := isEnabled()
})
}
}
import javax.swing._
object SwingDemo {
def main(args: Array[String]) {
val frame = new JFrame("Swing demo") {
frameSelf =>
add(new Box(BoxLayout.Y_AXIS) {
val firstName = Var[Option[String]](None)
val lastName = Var[Option[String]](None)
val fullName = firstName * lastName
add(new Box(BoxLayout.X_AXIS) {
add(new JLabel("First name:"))
add(new JTextField with TextVar {
setColumns(30)
documentText =>> (Option(_)) =>> (_ filter(_.length > 0)) =>> firstName
})
})
add(new Box(BoxLayout.X_AXIS) {
add(new JLabel("Last name:"))
add(new JTextField with TextVar {
setColumns(30)
documentText =>> (Option(_)) =>> (_ filter(_.length > 0)) =>> lastName
})
})
add(new Box(BoxLayout.X_AXIS) {
add(new JLabel("Full name:"))
add(new JLabel() {
fullName ->> { case (f, l) =>
val fn = f getOrElse "{first name}"
val ln = l getOrElse "{last name}"
setText(fn + " " + ln)
}
})
})
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)
})
}
frame.pack()
frame.setVisible(true)
}
}
/** A value that can be retrieved. */
trait Value[T] {
def apply(): T
}
/** A value that can be set. */
trait Settable[T] {
/** Set the value, and perform any necessary side-effects. */
def :=(t: T): Unit = update(t)
/** Do the actual value update. This should purely update the value. Do not trigger any side-effects. */
protected def update(t: T): Unit
}
/** A retrievable value that can be listened to for changes. */
trait Listenable[T] extends Value[T] {
listenableSelf =>
private var listeners: immutable.List[T => Unit] = Nil
/** Add a call-back for changes. */
def ->> (l: T => Unit): Unit = listeners = l::listeners
/** Inform all listeners of a change. */
protected def informListeners(): Unit = {
val t = apply()
for(l <- listeners) l(t)
}
/** Make a new `Listenable` from these two `Listenable`s that fires when either change their values. */
def * [U](lu: Listenable[U]): Listenable[(T, U)] = new Listenable[(T, U)] {
listenableSelf ->> (_ => informListeners())
lu ->> (_ => informListeners())
def apply(): (T, U) = (listenableSelf(), lu())
}
/** Get a new `Var` that contains this value mapped by some function. */
def =>> [U](f: (T => U)): Var[U] = {
val vu = Var(f(this()))
this ->> { t => vu := f(t) }
vu
}
/** Bind a `Settable` to this value. When ever this value changes, the `Settable` will be synced. */
def =>> (vt: Settable[T]): Unit = {
this ->> { vt := _ }
}
}
/** A value that exposes a value that you can change, listen to changes and set. */
trait Assignable[T] extends Listenable[T] with Settable[T] {
/** Assigning will both set the value and inform all listeners if the value is new. */
override abstract def :=(t: T): Unit = if(t != this()) {
val old = this()
super.:=(t)
if(old != this()) informListeners()
}
/** Apply a transformation function to this value. */
// fixme: This is badly named
def lift(f: T => T): Unit = this := (f(this()))
/** Generate a `Var` which is synced in both directions with this, according to a `f`orward and `backward` function. */
def <=> [U](f: (T => U), b: (U => T)): Var[U] = {
val vu = Var(f(this()))
this ->> { t => vu := f(t) }
vu ->> { u => this := b(u) }
vu
}
/** Sync this value directly with a `Var`, such that changes to either one will be reflected in the other. */
def <=> (vt: Var[T]) = {
this ->> { vt := _ }
vt ->> { this := _ }
vt := this()
}
}
/** An assignable variable. This stores the value directly. Use for your 'model' state. */
class Var[T] private () extends Assignable[T] {
private var value: T = _
def this(t: T) = {
this()
this.:=(t)
}
def apply(): T= value
protected def update(t: T): Unit = if(t != value) {
value = t
}
}
object Var {
def apply[T](t: T): Var[T] = new Var(t)
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment