In this post, we will check an example that create a SCells Spreadsheet. java
So we first check on the visual framework. react
The Visual framework
extends the ScrollPane, which gives it a Scroll-bars at the bottom and right. It contains two sub-component named Table and rowHeader.
Table display the data, while the rowHeader component contains the row-number headers at the left of the spreadsheet.
the GUI code that forms the skeleton is as follow. app
// the code as below package org.stairwaybook.scelles import swing._ class Spreadsheet(val height : Int, val width : Int) extends ScrollPane { val table = new Table(height, width) { rowHeight = 25 autoResizeMode = Table.AutoResizeMode.Off showGrid = true gridColor = new java.awt.Color(150, 150, 150) } val rowHeader = new ListView((0 until height) map (_.toString)) { fixedCellWidth = 30 fixedCellHeight = table.rowHeight } viewportView = table rowHeaderView = rowHeader }To try this rudimentary spreadsheet out, here is the main class.
// the code as below package org.stairwaybook.scelles import swing._ object Main extends SimpleGUIApplication { def top = new MainFrame { title = "ScalaSheet" contents = new Spreadsheet(100, 20) } }
Disconnectng data entry and display
A good UI code, may have demands that the code separation between UI and model. This is the same in Scala as well in elsewhere. ide
First, we need to refactor the UI code, we want to show the contents with meaning value when it is beeing edited or when it is just display the result. so we will use the renderedComponent method, it is an override method, and it works as follow. post
// the code as below package org.stairwaybook.scelles import swing._ class Spreadsheet(val height : Int, val width : Int) extends ScrollPane { // the data val cellMode = new Model(height, width) import cellMode._ val table = new Table(height, width) { rowHeight = 25 autoResizeMode = Table.AutoResizeMode.Off showGrid = true gridColor = new java.awt.Color(150, 150, 150) override def rendererComponent(isSelected : Boolean, hasFocus : Boolean, row : Int, column : Int) : Component = { if (hasFocus) new TextField(userData(row, column)) else new Label(cells(row)(column).toString) { xAlignment = Alignment.Right } } def userData(row : Int, column : Int) : String = { val v = this(row, column) if (v == null) "" else v.toString } } val rowHeader = new ListView((0 until height) map (_.toString)) { fixedCellWidth = 30 fixedCellHeight = table.rowHeight } viewportView = table rowHeaderView = rowHeader }as we see that we uses userData, and that uses cells from the Model class. here is the code of the Model class.
package org.stairwaybook.scelles import swing._ class Model(val height : Int, val width: Int) { case class Cell(row :Int, column : Int) val cells = new Array[Array[Cell]](height, width) for (i <- 0 until height; j <- 0 until width) cells(i)(j) = new Cell(i, j) }now we have basic Spreadsheet, but it does not allow us to do useful things such as computation, it just display string as you hasve inputed it.
Formulas
In reality , a spread sheet cell holds two things : An actual value and a formula to compute this value. there are three types of formulas in a spreadsheets
1. Numeric value such as 1.22, -3, or 0
2. Textual value such as Annual sales, Deprecation, or total
3. Formula that compute a new value from the content of cells, such as "=add(A1, B2)" or "=sum((mul(2, A2), C1: D16)"
Coord for cell coordinates such as A3
Range for cell ranges such as A3:B17
Number for floating-point numbers such as 3.1415
Textual for textual labels such as Deprecation
Application for function applications such as sum(A1, A2).
this
so we have defined the following Formula classes. lua
package org.stairwaybook.scelles trait Formula case class Coord(row :Int, column : Int) extends Formula { override def toString = ('A' + column).toChar .toString + row } case class Range(c1 : Coord, c2: Coord) extends Formula { override def toString = c1.toString + ":" + c2.toString } case class Number(value : Double) extends Formula { override def toString = value.toString } case class Textual(value : String) extends Formula { override def toString = value } // stands for a formula which should have a function name and a list of arguments. case class Application(function: String, arguments : List[Formula]) extends Formula { override def toString = function + arguments.mkString("(", ",", ")") } object Empty extends Textual("")
Parsing formulas
we need to have a way to parse the formula into a formula tree. spa
import scala.util.parsing.combinator._ object FormulaParsers extends RegexParsers { def ident : Parser[String] = """[a-zA-Z_]\w*""".r def decimal : Parser[String] = """-?\d+(\.\d*)?""".r // next, we will need to capture Cell def cell : Parser[Coord] = """[a-zA-Z_]\d+""".r ^^ { s => val column = s.charAt(0).toUpper - 'A' val row = s.substring(1).toInt Coord(row, column) } def range: Parser[Range] = cell ~":"~cell ^^ { case c1~":"~c2 => Range(c1, c2) } def number : Parser[Number] = decimal ^^ (d => Number(d.toDouble)) def application : Parser[Application] = ident ~ "(" ~ repsep(expr, ",") ~ ")" ^^ { case f ~ "(" ~ ps ~ ")" => Application(f, ps) } def expr : Parser[Formula] = range | cell | number | application def textual : Parser[Textual] = """[^=].*""".r ^^ Textual // this is very neat... , you can directly apply the Textual (as Function1) on the ParserReult[T] on the Textual case class def formula : Parser[Formula] = number | textual | "=" ~> expr def parse(input : String ) : Formula = parseAll(formula, input) match { case Success (e, _) => e case f : NoSuccess => Textual("[" + f.msg + "]") } } // end FormulaParsersthis is using the Combinator framework .
To support the formula on Cells, here is the new version of Model , which has the formula members. scala
import swing._ class Model(val height : Int, val width: Int) { case class Cell(row :Int, column : Int) { var formula : Formula = Empty override def toString = formula.toString } val cells = new Array[Array[Cell]](height, width) for (i <- 0 until height; j <- 0 until width) cells(i)(j) = new Cell(i, j) }now with the Formula, we still display the string just as the formula's string representation.
Evaluation
we create a new Evaluation trait, which has the evaluate method. code
trait Evaluator { this : Model => def evaluate(e: Formula) : Double = try { e match { case Coord(row, column) => cells(row)(column).value case Number(v) => v case Textual(_) => 0 case Application(function, arguments) => val argvals = arguments flatMap evalList operations(function)(argvals) } } catch { case ex : Exception => Double.NaN } type Op = List[Double] => Double val operations = new collection.mutable.HashMap[String, Op] private def evalList(e: Formula) : List[Double] = e match { case Range(_, _) => references(e) map (_.value) case _ => List(evaluate(e)) // the recursion goes here } def references(e : Formula) : List[Cell] = e match { case Coord (row, column) => List(cells(row)(column)) case Range(Coord(r1, c1), Coord(r2, c2)) => for (row <- (r1 to r2).toList; column <- c1 to c2) yield cells(row)(column) case Application(function, arguments) => arguments flatMap references case _ => List() } } // end Evaluatorthe key is the Application ,which calles operations, which is a map from function name to the function's application.
type Op = List[Double] => Double val operations = new collection.mutable.HashMap[String, Op]and to support evaluation on nested arguments, we have evalList.
private def evalList(e: Formula) : List[Double] = e match { case Range(_, _) => references(e) map (_.value) case _ => List(evaluate(e)) // the recursion goes here }and in order to support argument such as [C1:C5] or C1 and others types, we define a method references , which can turn a formula classes into a cell references. as follow.
def references(e : Formula) : List[Cell] = e match { case Coord (row, column) => List(cells(row)(column)) case Range(Coord(r1, c1), Coord(r2, c2)) => for (row <- (r1 to r2).toList; column <- c1 to c2) yield cells(row)(column) case Application(function, arguments) => arguments flatMap references case _ => List() }
equally, when we
Operations libraries
To actually convert the formula into the operations, so we can really convert to real calculation.
// the trait populate the operations table during its initialization. trait Arithmetic { this : Evaluator => operations += ( "add" -> { case List(x, y) => x + y }, "sub" -> { case List(x, y) => x - y }, "div" -> { case List(x, y) => x / y }, "mul" -> { case List(x, y) => x * y }, "mod" -> { case List(x, y) => x % y }, "sum" -> { xs => (0.0 /: xs) (_ + _) }, "prod" -> { xs => (1.0 /: xs) (_ * _) } ) }this trait does not export any exported members, we need to revise the Model class, here ist he model clases that introduced the Formula.
To support evaluation, Model now mix in Evaluator trait to support evaluate
import swing._ class Model(val height : Int, val width: Int) extends Evaluator with Arithmetic { case class Cell(row :Int, column : Int) { var formula : Formula = Empty def value = evaluate(formula) override def toString = formula match { case Textual(s) => s case _ => value.toString } } val cells = new Array[Array[Cell]](height, width) for (i <- 0 until height; j <- 0 until width) cells(i)(j) = new Cell(i, j) }
Change Propagation
so we have all the meat and potatos, but we don't have change propagations, if one of hte dependent cell changes, we are not being informed and the cell is not updated.
we need to extend the Model class. This we extend our Cell case class to let it extend from the Swing.event.Publisher class and we redefine the formula class to let it have the subscription and unsubcription wired up . as follow.
class Model(val height : Int, val width: Int) extends Evaluator with Arithmetic { case class Cell(row :Int, column : Int) extends Publisher { private var f : Formula = Empty def formula_=(f : Formula) { for (c <- references(formula)) deafTo(c) this.f = f for (c <- references(formula)) listenTo(c) value = evaluate(f) } def formula : Formula = f private var v : Double = 0 def value = evaluate(formula) def value_= (w: Double) { if (!(v == w || v.isNaN && w.isNaN)) { v = w publish(ValueChanged(this)) } } override def toString = formula match { case Textual(s) => s case _ => value.toString } // reactions is from the Publisher classes reactions += { case ValueChanged(_) => value = evaluate(formula) } } // end class Cell case class ValueChanged(cell : Cell) extends event.Event val cells = new Array[Array[Cell]](height, width) for (i <- 0 until height; j <- 0 until width) cells(i)(j) = new Cell(i, j) }in this code, we define a new event called ValueChanged, when a cell is given a different value, the event will be fired. and we need as well to handle the ValueChanged event (this is because some upstream cells when it call publich(ValueChanged(this)), the downstream should react and reevaluate the formula.
Now, the cell has changed, we need as well to extend the Spreadsheet class to 1. listen to all cells 2. create a handler for the ValueChanged event to redraw content for the cell. the key is to call cellUpdate(row, cell). here is the new Spreadsheet class.
import swing._ import swing.event._ class Spreadsheet(val height : Int, val width : Int) extends ScrollPane { // the data val cellMode = new Model(height, width) import cellMode._ val table = new Table(height, width) { rowHeight = 25 autoResizeMode = Table.AutoResizeMode.Off showGrid = true gridColor = new java.awt.Color(150, 150, 150) override def rendererComponent(isSelected : Boolean, hasFocus : Boolean, row : Int, column : Int) : Component = { if (hasFocus) new TextField(userData(row, column)) else new Label(cells(row)(column).toString) { xAlignment = Alignment.Right } } def userData(row : Int, column : Int) : String = { val v = this(row, column) if (v == null) "" else v.toString } reactions += { case TableUpdated(table, rows, column) => for (row <- rows) cells(row)(column).formula = FormulaParsers.parse(userData(row, column)) case ValueChanged(cell) => updateCell(cell.row, cell.column) // redraw the cell with the updateCell call } for (row <- cells; cell <- row) listenTo(cell) // wire up and listen to all the cells } val rowHeader = new ListView((0 until height) map (_.toString)) { fixedCellWidth = 30 fixedCellHeight = table.rowHeight } viewportView = table rowHeaderView = rowHeader }
The main class
To run the code, we need a Main class, it is as follow.
import swing._ object Main extends SimpleGUIApplication { def top = new MainFrame { title = "ScalaSheet" contents = new Spreadsheet(100, 20) } }run the following code
scala Main
and if you have the package statement above, you should run it like this:
scala org.stairwaybook.scelles.Mainor just
Main.main(Array[String]())if you are running from the Eclipse interactive shel.