Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Listeners and reactions in Scala Swing

I've done a fair bit of searching and some trial and error in eclipse, but there seems to be a gap in my understanding of listeners and reactions when writing a GUI in Scala using Swing.

Does each listener get a reactions block, or do I register listeners on all components that might generate an event and react to each on in a large reactions block with case statements?

Where exactly do the listeners and reaction blocks belong.

Here's an abbreviated version of my GUI code:

import scala.swing._
import scala.swing.event.ButtonClicked
import scala.swing.event.KeyTyped
import scala.swing.event.KeyPressed

object HumanGUI extends SimpleGUIApplication {

 val basicPane = new java.awt.Dimension(800, 200)
 val botPane = new java.awt.Dimension(400, 200)
 val felt = new java.awt.Color(35, 125, 35)

 def top = new MainFrame {

    title = "Blackjack GUI"

    val ConnectionPanel = new BoxPanel(Orientation.Vertical) {
      background = felt
      preferredSize = new java.awt.Dimension(155, 90)
      minimumSize = preferredSize
      maximumSize = preferredSize

      val ipAddressLabel = new Label("House IP:")
      ipAddressLabel.foreground = java.awt.Color.WHITE
      ipAddressLabel.horizontalTextPosition = scala.swing.Alignment.Left

      val portLabel = new Label("House port:")
      portLabel.foreground = java.awt.Color.WHITE
      portLabel.horizontalTextPosition = scala.swing.Alignment.Left

      val ipAddressTextField = new TextField
      val portTextField = new TextField

      contents += ipAddressLabel
      contents += ipAddressTextField
      contents += portLabel
      contents += portTextField
    }

    val DetailPanel = new BoxPanel(Orientation.Vertical) {
      background = felt
      preferredSize = new java.awt.Dimension(100, 160)
      minimumSize = preferredSize
      maximumSize = preferredSize

      val nameLabel = new Label("Your name:")
      nameLabel.foreground = java.awt.Color.WHITE
      nameLabel.horizontalTextPosition = scala.swing.Alignment.Left

      val bankrollLabel = new Label("Bankroll:")
      bankrollLabel.foreground = java.awt.Color.WHITE
      bankrollLabel.horizontalTextPosition = scala.swing.Alignment.Left

      val betLabel = new Label("Bet:")
      betLabel.foreground = java.awt.Color.WHITE
      betLabel.horizontalTextPosition = scala.swing.Alignment.Left

      val nameTextField = new TextField
      val bankrollTextField = new TextField
      val betTextField = new TextField

      val goButton = new Button("Go!")

      contents += nameLabel
      contents += nameTextField
      contents += bankrollLabel
      contents += bankrollTextField
      contents += betLabel
      contents += betTextField
      contents += goButton
    }

    val PlayPanel = new BoxPanel(Orientation.Vertical) {
      background = felt
      val hitButton = new Button("Hit")
      val stayButton = new Button("Stay")
      val doubleButton = new Button("Double")
      val quitButton = new Button("Quit")

      contents += hitButton
      contents += stayButton
      contents += doubleButton
      contents += quitButton
    }

    val playerPanel = new BoxPanel(Orientation.Horizontal) {
      background = felt
      border = new javax.swing.border.LineBorder(java.awt.Color.WHITE)
      preferredSize = basicPane
      minimumSize = basicPane
      maximumSize = basicPane
      opaque = true

      contents += ConnectionPanel
      contents += DetailPanel
      contents += PlayPanel
    }

    contents = new BoxPanel(Orientation.Vertical) {
      contents += playerPanel
    }
  }
}

So the question is where do I put my listeners and reaction blocks?
I want to react to the buttons in PlayPanel, and the text fields in both ConnectionPanel and DetailPanel.
Do I put the listeners and reaction blocks as close to the elements that I'm interested as possible, or do I put a big block of listeners and reactions at the end of the MainFrame section?
Does it even matter?

EDIT
I've made significant progress and have much of what I need working, along with a better understanding of the concepts I wasn't getting before.

This excerpt from Odersky's "Programming in Scala" was what helped me the most. Specifically, the example from this page:

http://www.artima.com/pins1ed/gui-programming.html

The code is from the first edition of the text, so I question whether or not there's a better way in Scala 2.9, but it was clear an concise and summed up what I was misunderstanding.

From the example, which is a simple fahrenheit to celsius converter, I came to understand that the listener and the reactions blocks belongs after the contents block for the MainFrame.

so I ended up with:

object HumanGUI extends SimpleSwingGUIApplication {
  def top = new MainFrame {
    title = "My Blackjack GUI"

    //The fields I want to work with are instantiated as object
    object ipAddressTextField extends TextField { columns = 15 }
    object portNumberTextField extends TextField {columns = 5 }

    //other panels, objects, etc would go here
    val OtherPanel = new BoxPanel(Orientation.Horizontal) {
       label = "Other Panel"
    }

    //and here we have the contents += block for the mainframe, other panels, etc from 
    //above would be added to the main frame here
    contents = new BoxPanel(Orientation.Vertical) {
      contents += ipAddressTextField
      contents += portNumberTextField
    }

    //here's the listen to, listening on the object created above, and it's enclosed in 
    //in backticks, a good explanation of that is found in the link below
    listenTo(`ipAddressTextField`)
    reactions += {
      case EditDone('ipAddressTextField`) =>
        //do something!
    }
  }

Need clarification on Scala literal identifiers (backticks)

So it seems that the answer to my question is that the listenTo and reactions blocks belong in the MainFrame block, but should appear after it's contents += { //contents } block.

Additional trial and error in eclipse shows that while this solution works for me, there is clearly much more that I don't understand. For example, while I was unable to get listeners for KeyPress events to work if I tried to listen and react to them in within the
val OtherPanel = new BoxPanel(Orientation.Horizontal) { }
portion of the above code, I was able to get a button registered and working like this:

val OtherPanel = new BoxPanel(Orientation.Horizontal) {
  val betLabel = new Label("Bet:")
  val betTextField = new TextField
  val goButton = new Button("Go!")

  listenTo(goButton)
  reactions += {
    case ButtonClicked(b) =>
      betTextField.text = "Go!"
  }

  contents += betLabel
  contents += betTextField
  contents += goButton
}

Why this worked but my attempts to do something along the lines of

val OtherPanel = new BoxPanel(Orientation.Horizontal) {
  val betLabel = new Label("Bet:")
  val betTextField = new TextField
  val goButton = new Button("Go!")

listenTo(betTextField)
reactions += {
  case KeyTyped(betTextField, Enter, _, _) => {
    println("Caught enter")
  }

  contents += betLabel
  contents += betTextField
  contents += goButton
}

didn't work is still baffling me. I'm assuming that it should work and I'm just doing something wrong. Perhaps that melding that approach with a case EditDone instead of a case KeyTyped(,,,) would have worked but I'm a little too burnt out right now to follow up on that.

I haven't accepted an answer yet because I'm hoping that someone who sees this can clarify the points I still don't understand. Should that not happen and the question remain unanswered for a few days I will likely accept @som-snytt's answer as his code was very helpful.

like image 551
NickAbbey Avatar asked Nov 02 '12 16:11

NickAbbey


1 Answers

Swing is educational, and Scala-Swing is educational. Especially if the course is "History of Swing: The Rise and Fall."

My first Scala program also used Swing. I've forgotten the details, but I'll share what I can see in the source.

Apparently, I had a main UI component called LightBox that handled some UI events, and also a mediator component LightBoxMediator that coordinated.

The interesting part would be, using cake pattern for composition, and moving business logic (or game logic) interaction into a component that "mediates" for the UI proper. The LightBox publishes events, too.

So the answer to your question would be: exploit the publisher framework, but distinguish UI events from application events. (This little game also had actor-based controllers.)

Maybe this suffices to illustrate the separation of concerns:

/**
 * Draws the House of Mirrors.
 * The LightBox is just a list of rays (line segments) and gates (various objects).
 * The UI emits requests to move and rotate gates.
 */
class LightBox extends Panel {

  this.peer.addComponentListener(
    new ComponentAdapter {
      override def componentResized(e: ComponentEvent) {
        if (e.getID == ComponentEvent.COMPONENT_RESIZED && e.getComponent == LightBox.this.peer) {
          calculateScale()
        }
      }
    }
  )

  listenTo(mouse.clicks, mouse.moves, mouse.wheel, keys)

  reactions += {
    case KeyPressed(_, Key.N, _, _) => highlightNextMoveableGate()
    case KeyPressed(_, Key.P, _, _) => highlightPreviousMoveableGate()
    case e: MousePressed => startDrag(e)
    case e: MouseDragged => doDrag(e)
    case e: MouseReleased => endDrag(e)
    case e: MouseWheelMoved => wheeling(e)
    case _ => null // println ("Unreacted event")
  }

and the mediator

trait ViewComponents {
  this: ControllerComponents with ModelComponents =>

  val lightBoxMediator: LightBoxMediator
  val statusBarMediator: StatusBarMediator
  val statusIconMediator: StatusIconMediator
  val applicationMediator: ApplicationMediator

  /**
   * Handles update notifications from the application
   * and user input from the LightBox.
   */
  class LightBoxMediator(val ui: LightBox) extends Reactor with Observing {

    /** Attempt to track our selection across updates: the point is where the gate should end up. */
    private var selectionContinuity: (Option[Gate], Option[Point]) = (None, None)

    listenTo(ui, ui.keys, ui.mouse.clicks)

    reactions += {
      case KeyPressed(_, Key.Q, _, _) => sys.exit()
      case KeyPressed(_, Key.Space, _, _) => rotateSelectedGate()
      case KeyPressed(_, Key.Enter, _, _) => rotateOtherwiseSelectedGate()
      case KeyPressed(_, Key.Up, _, _) => moveUp()
      case KeyPressed(_, Key.Down, _, _) => moveDown()
      case KeyPressed(_, Key.Left, _, _) => moveLeft()
      case KeyPressed(_, Key.Right, _, _) => moveRight()
      case KeyPressed(_, Key.PageUp, _, _) => previousLevel()
      case KeyPressed(_, Key.PageDown, _, _) => nextLevel()
      case DragEvent(from, to) => handleDrag(from, to)
      case ClickEvent(where, button) => handleClick(where, button)
      //case x => println("Unreacted event " + x)
    }

    observe(controller.modelEvents) { e => e match {
        case LevelLoaded(v) => onLevelLoaded(v)
        case TraceResult(s) => onTrace(s)
        case unknown => println("Lightbox mediator ignored: "+ unknown)
      }
      true
    }

Just noticed the additional questions. By coincidence, I was cleaning up old code, actually a tiny app to grab images from sfgate.com (which stopped working when they changed the site, of course; but usually you can right-click-save now), and I happened to notice the following comment about resubscribing. I vaguely remember the bit about UIElement being a LazyPublisher, because I remember the head slap. But if I hadn't written the meager comment, that info would have been lost to ancient history.

I think somebody wants to support scala-swing and will probably take care of the head slaps.

package com.maqicode.sfg.jfc

import java.awt.Color
import java.awt.Color.{WHITE => White, RED => Red}
import java.net.{URI, URISyntaxException}
import javax.swing._

import swing.TextField
import swing.event.{EditDone, MouseEntered, ValueChanged}

import com.maqicode.sfg.BadGateURLException
import com.maqicode.sfg.GateUrlTranslator.translate

abstract class URIField extends TextField {

  reactions += {
    case e: EditDone => editDone(e)
    case other: ValueChanged => editing(other)
    case m: MouseEntered => onMouseEntered()
    case _ => null
  }
  // necessary to resubscribe this so that onFirstSubscribe registers ActionListener
  listenTo(this, mouse.moves)

  def onMouseEntered() {
    val t: Option[String] = ClipboardInput.contents
    if (t.isDefined && t.get != this.text) {
      this.text = t.get
      submitURL(t.get)
    }
  }

  def editing(e: ValueChanged) {
    clearError()
  }

  def editDone(e: EditDone) {
    submitURL(this.text)
  }

  def submitURL(s: String) {
    val u = s.trim
    if (!u.isEmpty)
      try {
        submitURI(translate(new URI(u)))
        clearError()
      } catch {
        case t: BadGateURLException => flagError()
        case t: URISyntaxException => flagError()
      }
  }

  def flagError() {
    colorCode(Red)
  }

  def clearError() {
    colorCode(White)
  }

  private def colorCode(c: Color) {
    if (this.background != c) this.background = c
  }

  def submitURI(uri: URI): Unit
}
like image 128
som-snytt Avatar answered Oct 18 '22 09:10

som-snytt