Home

Listening to the mouse

Listening to the mouse

So far, our Canvas class only displays the board, it does not yet react to our attempts to play the game.

We need to start listening to the mouse, using:

  listenTo(mouse.clicks)
We only need clicks with the mouse buttons. If we also needed movements of the mouse pointer, we should listen to mouse.moves, if we also needed the scroll wheel, we should listen to mouse.wheel.

The events published by mouse.clicks are scala.swing.event.MousePressed, scala.swing.event.MouseReleased, and scala.swing.event.MouseClicked. A mouse click means that a mouse button is pressed and released at the same position.

All events have five parameters. The first three are the source component, the Point indicating the coordinates where the event happens, and the "modifiers", meaning the button that was pressed and if Shift, Alt, or Control were held on the keyboard.

We only need the coordinates, so our first attempt at handling mouse clicks looks like this:

class Canvas(val board: Board) extends Component {
  preferredSize = new Dimension(320, 320)

  listenTo(mouse.clicks)
  reactions += {
    case MouseClicked(_, p, _, _, _) => mouseClick(p.x, p.y)
  }

  // returns squareSide, x0, y0, wid
  private def squareGeometry: (Int, Int, Int, Int) = {
    val d = size
    val squareSide = d.height min d.width
    val x0 = (d.width - squareSide)/2
    val y0 = (d.height - squareSide)/2
    (squareSide, x0, y0, squareSide/3)
  }

  private def mouseClick(x: Int, y: Int) {
    val (squareSide, x0, y0, wid) = squareGeometry
    if (x0 <= x && x < x0 + squareSide &&
	y0 <= y && y < y0 + squareSide) {
      val col = (x - x0) / wid
      val row = (y - y0) / wid
      printf("Mouse pressed in field %d, %d\n", col, row)
    }
  }
    
  override def paintComponent(g : Graphics2D) {
    g.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, 
		       java.awt.RenderingHints.VALUE_ANTIALIAS_ON)
    g.setColor(Color.WHITE);
    val d = size
    g.fillRect(0,0, d.width, d.height)
    val (squareSide, x0, y0, wid) = squareGeometry
    g.setColor(Color.BLACK)
    // vertical lines
    for (x <- 1 to 2)
      g.draw(new Line2D.Double(x0 + x * wid, y0, x0 + x * wid, y0 + squareSide))
    // horizontal lines
    for (y <- 1 to 2)
      g.draw(new Line2D.Double(x0, y0 + y * wid, x0 + squareSide, y0 + y * wid))
    g.setStroke(new BasicStroke(3f))
    for (x <- 0 until 3) {
      for (y <- 0 until 3) {
	board(x, y) match {
	  case 1 =>
	    g.setColor(Color.RED)
	    g.draw(new Ellipse2D.Double(x0 + x * wid + 10, y0 + y * wid + 10, 
					wid - 20, wid - 20))
	  case 2 =>
	    g.setColor(new Color(0, 160, 0))
	    val x1 = x0 + x * wid + 10
	    val y1 = y0 + y * wid + 10
	    g.draw(new Line2D.Double(x1, y1, x1 + wid - 20, y1 + wid - 20))
	    g.draw(new Line2D.Double(x1, y1 + wid - 20, x1 + wid - 20, y1))
	  case _ => // draw nothing
	}
      }
    }
  }
}

So far this only prints out the field where the user pressed the mouse. We somehow have to send these coordinates back to the user interface so that it can update the Tic-Tac-Toe board. If canvas had a reference to the UI object, it could do this through a method call. A more elegant way is to use an event for this as well.

We define an event class:

 
case class TicTacToeEvent(x: Int, y: Int) extends Event

The Canvas object, when it detects a mouse click, computes the mouse coordinates and then publishes an event:

  private def mouseClick(x: Int, y: Int) {
    val (squareSide, x0, y0, wid) = squareGeometry
    if (x0 <= x && x < x0 + squareSide &&
	y0 <= y && y < y0 + squareSide) {
      val col = (x - x0) / wid
      val row = (y - y0) / wid
      publish(TicTacToeEvent(col, row))
    }
  }

Finally, the UI class listens to the canvas and handles the TicTacToeEvent:

  listenTo(canvas)
  reactions += {
    case TicTacToeEvent(x, y) => 
      println("Tic Tac Toe event at " + x + " " + y)
  }

Now it remains to add the necessary logic to the Board class. Here is the complete program tictactoe3.scala:

import scala.swing._
import scala.swing.event._
import java.awt.{Color,Graphics2D,BasicStroke}
import java.awt.geom._

// --------------------------------------------------------------------

class Board {
  private var player = 1
  private val grid = Array(0, 0, 0,
			   0, 0, 0,
			   0, 0, 0)

  def apply(x: Int, y: Int): Int = grid(3 * y + x)
  def currentPlayer: Int = player
  def play(x: Int, y: Int) {
    if (this(x, y) == 0) {
      grid(3 * y + x) = player
      player = 3 - player
    }
  }
  def restart() {
    for (i <- 0 until 9)
      grid(i) = 0
    player = 1
  }
}

// --------------------------------------------------------------------

case class TicTacToeEvent(x: Int, y: Int) extends Event

// --------------------------------------------------------------------

class Canvas(val board: Board) extends Component {
  preferredSize = new Dimension(320, 320)

  listenTo(mouse.clicks)
  reactions += {
    case MouseClicked(_, p, _, _, _) => mouseClick(p.x, p.y)
  }

  // returns squareSide, x0, y0, wid
  private def squareGeometry: (Int, Int, Int, Int) = {
    val d = size
    val squareSide = d.height min d.width
    val x0 = (d.width - squareSide)/2
    val y0 = (d.height - squareSide)/2
    (squareSide, x0, y0, squareSide/3)
  }

  private def mouseClick(x: Int, y: Int) {
    val (squareSide, x0, y0, wid) = squareGeometry
    if (x0 <= x && x < x0 + squareSide &&
	y0 <= y && y < y0 + squareSide) {
      val col = (x - x0) / wid
      val row = (y - y0) / wid
      publish(TicTacToeEvent(col, row))
    }
  }
    
  override def paintComponent(g : Graphics2D) {
    g.setRenderingHint(java.awt.RenderingHints.KEY_ANTIALIASING, 
		       java.awt.RenderingHints.VALUE_ANTIALIAS_ON)
    g.setColor(Color.WHITE);
    val d = size
    g.fillRect(0,0, d.width, d.height)
    val (squareSide, x0, y0, wid) = squareGeometry
    g.setColor(Color.BLACK)
    // vertical lines
    for (x <- 1 to 2)
      g.draw(new Line2D.Double(x0 + x * wid, y0, x0 + x * wid, y0 + squareSide))
    // horizontal lines
    for (y <- 1 to 2)
      g.draw(new Line2D.Double(x0, y0 + y * wid, x0 + squareSide, y0 + y * wid))
    g.setStroke(new BasicStroke(3f))
    for (x <- 0 until 3) {
      for (y <- 0 until 3) {
	board(x, y) match {
	  case 1 =>
	    g.setColor(Color.RED)
	    g.draw(new Ellipse2D.Double(x0 + x * wid + 10, y0 + y * wid + 10, 
					wid - 20, wid - 20))
	  case 2 =>
	    g.setColor(new Color(0, 160, 0))
	    val x1 = x0 + x * wid + 10
	    val y1 = y0 + y * wid + 10
	    g.draw(new Line2D.Double(x1, y1, x1 + wid - 20, y1 + wid - 20))
	    g.draw(new Line2D.Double(x1, y1 + wid - 20, x1 + wid - 20, y1))
	  case _ => // draw nothing
	}
      }
    }
  }
}

// --------------------------------------------------------------------

class UI(val board: Board) extends MainFrame {
  private def restrictHeight(s: Component) {
    s.maximumSize = new Dimension(Short.MaxValue, s.preferredSize.height)
  }

  title = "Tic Tac Toe #3"

  val canvas = new Canvas(board)
  val newGameButton = Button("New Game") { newGame() }
  val turnLabel = new Label("Player 1's turn")
  turnLabel.foreground = Color.BLUE
  val quitButton = Button("Quit") { sys.exit(0) }
  val buttonLine = new BoxPanel(Orientation.Horizontal) {
    contents += newGameButton
    contents += Swing.HGlue
    contents += turnLabel
    contents += Swing.HGlue
    contents += quitButton
  }

  // make sure that resizing only changes the TicTacToeDisplay
  restrictHeight(buttonLine)
  
  contents = new BoxPanel(Orientation.Vertical) {
    contents += canvas
    contents += Swing.VStrut(10)
    contents += buttonLine
    border = Swing.EmptyBorder(10, 10, 10, 10)
  }

  listenTo(canvas)
  reactions += {
    case TicTacToeEvent(x, y) => 
      board.play(x, y)
      updateLabelAndBoard()
  }

  def updateLabelAndBoard() {
    turnLabel.text = "Player %d's turn".format(board.currentPlayer)
    canvas.repaint()
  }

  def newGame() {
    board.restart()
    updateLabelAndBoard()
  }
}

object TicTacToeThree {
  def main(args: Array[String]) {
    val board = new Board
    val ui = new UI(board)
    ui.visible = true
  }
}

One more important point: After we modified the Board object, we need to get the Canvas to display the new contents of the board. This is done by calling the method repaint() on the Canvas object.

Forgetting to to this is a typical mistake: When the program doesn't behave correctly, you wonder if your logic is wrong—but in reality the logic is right, and it's only the display that is not being updated.

Note that the program doesn't notice when one player has won or the game ended in a tie. This is left as an exercise for the reader.