537 lines
17 KiB
Scala
537 lines
17 KiB
Scala
package net.okennedy.cells.sheet
|
|
|
|
import com.raquo.laminar.api.L._
|
|
import net.okennedy.cells.Identifier
|
|
import net.okennedy.cells.serialized
|
|
import com.raquo.airstream.core.Signal
|
|
import com.raquo.airstream.combine.MergeEventStream
|
|
import play.api.libs.json._
|
|
import com.raquo.airstream.core.EventStream
|
|
import com.raquo.airstream.flatten.ConcurrentEventStream
|
|
import com.raquo.airstream.flatten.FlattenStrategy.ConcurrentStreamStrategy
|
|
import com.raquo.airstream.flatten.FlattenStrategy
|
|
import net.okennedy.cells.widgets.DragFrame
|
|
import com.raquo.airstream.ownership.OneTimeOwner
|
|
import net.okennedy.cells.TableOp
|
|
import net.okennedy.cells.SetTablePosition
|
|
import com.raquo.airstream.core.Transaction
|
|
import net.okennedy.cells.CellsUI
|
|
import net.okennedy.cells.network.Connection
|
|
import net.okennedy.cells.RequestSetTablePosition
|
|
import net.okennedy.cells.UpdateTableColumns
|
|
import net.okennedy.cells.UpdateTableRows
|
|
import net.okennedy.cells.RequestUpdateRows
|
|
import net.okennedy.cells.RequestUpdateColumns
|
|
import net.okennedy.cells.widgets.Icon
|
|
import scala.collection.Searching.Found
|
|
import scala.collection.Searching.InsertionPoint
|
|
import net.okennedy.cells.SeqReplace
|
|
import net.okennedy.cells.SeqInsert
|
|
import net.okennedy.cells.SeqDelete
|
|
import net.okennedy.cells.widgets.FormulaEditor
|
|
|
|
class Table(val id: Identifier, connection: Connection)
|
|
{
|
|
implicit val tableOwner: Owner =
|
|
new OneTimeOwner( () => println(s"Accessing owner of table $id after killed") )
|
|
|
|
def this(ser: serialized.Table, connection: Connection) =
|
|
{
|
|
this(
|
|
ser.id,
|
|
connection
|
|
)
|
|
new Transaction( _ =>
|
|
this.x.set(ser.x)
|
|
this.y.set(ser.y)
|
|
this.columns.set(ser.columns)
|
|
this.rows.set(ser.rows)
|
|
)
|
|
startEditing(columns.now()(1).id, rows.now()(1).id)
|
|
}
|
|
|
|
def process(op: TableOp): Unit =
|
|
{
|
|
op match {
|
|
case SetTablePosition(_, newX, newY) =>
|
|
new Transaction( _ =>
|
|
x.set(newX); y.set(newY)
|
|
)
|
|
case UpdateTableColumns(_, update) =>
|
|
println(s"ColUpdate??? : $update")
|
|
columns.set(update(columns.now()))
|
|
case UpdateTableRows(_, update) =>
|
|
println(s"RowUpdate??? : $update")
|
|
rows.set(update(rows.now()))
|
|
}
|
|
}
|
|
|
|
val columns =
|
|
Var[Seq[serialized.ColSpec]](initial = Seq())
|
|
|
|
val columnGutters: Signal[Seq[(Identifier, Signal[ColumnGutter])]] =
|
|
columns.signal
|
|
.map {
|
|
_.foldLeft( (0, 0, Seq[ColumnGutter]()) ) {
|
|
case ((pos, idx, accum), col) =>
|
|
(pos+col.width, idx+1, accum :+ new ColumnGutter(col, pos, idx))
|
|
}._3
|
|
}
|
|
.split(_.spec.id)( project = (key, _, signal) => key -> signal)
|
|
|
|
val rows =
|
|
Var[Seq[serialized.RowSpec]](initial = Seq())
|
|
|
|
val rowGutters: Signal[Seq[(Identifier, Signal[RowGutter])]] =
|
|
rows.signal
|
|
.map {
|
|
_.foldLeft( (0, 0, Seq[RowGutter]()) ) {
|
|
case ((pos, idx, accum), col) =>
|
|
(pos+col.height, idx+1, accum :+ new RowGutter(col, pos, idx))
|
|
}._3
|
|
}
|
|
.split(_.spec.id)( project = (key, _, signal) => key -> signal)
|
|
|
|
val cells = Var[Map[(Identifier, Identifier), Cell]](initial =
|
|
rows.now().zipWithIndex.flatMap { case (r, ridx) =>
|
|
columns.now().zipWithIndex.map { case (c, cidx) =>
|
|
(r.id, c.id) -> new Cell("["+cidx+", "+ridx+"]")
|
|
}
|
|
}.toMap
|
|
)
|
|
|
|
val cellsByPosition: Signal[Map[(Identifier, Identifier), Signal[Cell]]] =
|
|
cells.signal.map { _.toSeq }.split( key = _._1 )( project =
|
|
(cellId, _, cellSignal) => (cellId, cellSignal.map { _._2 })
|
|
).map { _.toMap }
|
|
|
|
val x = Var(initial = 20)
|
|
val y = Var(initial = 20)
|
|
|
|
val width =
|
|
columns.signal.map { _.foldLeft(0)( (w, col) => w + col.width) }
|
|
.observe
|
|
|
|
val height =
|
|
rows.signal.map { _.foldLeft(0)( (h, row) => h + row.height) }
|
|
.observe
|
|
|
|
def positionAndHeightOfRow(rowKey: Identifier): (Int, Int) =
|
|
{
|
|
rows.now().foldLeft[Either[Int, (Int, Int)]]( Left(0) ) {
|
|
case (Left(x), row) if row.id == rowKey =>
|
|
Right(x -> row.height)
|
|
case (Left(x), row) =>
|
|
Left(x + row.height)
|
|
case (x@Right(pos, height), _) => x
|
|
}.right.get
|
|
}
|
|
|
|
def positionAndWidthOfColumn(colKey: Identifier): (Int, Int) =
|
|
{
|
|
columns.now().foldLeft[Either[Int, (Int, Int)]]( Left(0) ) {
|
|
case (Left(x), col) if col.id == colKey =>
|
|
Right(x -> col.width)
|
|
case (Left(x), col) =>
|
|
Left(x + col.width)
|
|
case (x@Right(pos, width), _) => x
|
|
}.right.get
|
|
}
|
|
|
|
def snapIndex(snaps: Seq[Int], excessStep: Int, value: Int): Int =
|
|
{
|
|
// println(s"Snap $value")
|
|
if(snaps.size > 0 && value < snaps.last){
|
|
snaps.search(value) match {
|
|
case Found(foundIndex) =>
|
|
// println(s"Found: $foundIndex");
|
|
foundIndex
|
|
case InsertionPoint(insertionPoint) =>
|
|
if(insertionPoint == 0) {
|
|
// println(s"Insert @ start");
|
|
0
|
|
} else if(insertionPoint >= snaps.length) {
|
|
// println(s"Insert @ end");
|
|
snaps.length - 1
|
|
} else {
|
|
val lowDiff = value - snaps(insertionPoint - 1)
|
|
val highDiff = snaps(insertionPoint) - value
|
|
// println(s"Insert @ [${insertionPoint-1}, $insertionPoint] : $lowDiff <-> $highDiff");
|
|
if(lowDiff > highDiff){ insertionPoint }
|
|
else { insertionPoint-1 }
|
|
}
|
|
}
|
|
} else {
|
|
val excess = value - snaps.lastOption.getOrElse(0) + (excessStep / 2)
|
|
// println(s"Over with excess: $excess")
|
|
(snaps.length - 1) + (excess / excessStep).toInt
|
|
}
|
|
}
|
|
|
|
def snapTo(snaps: Seq[Int], excessStep: Int, value: Int): Int =
|
|
{
|
|
val idx = snapIndex(snaps, excessStep, value)
|
|
if(idx < snaps.length){ snaps(idx) }
|
|
else { snaps.lastOption.getOrElse(0) + excessStep * (idx - (snaps.length -1)) }
|
|
}
|
|
|
|
def startEditing(colKey: Identifier, rowKey: Identifier): Unit =
|
|
{
|
|
val (cellX, cellWidth) =
|
|
positionAndWidthOfColumn(colKey)
|
|
val (cellY, cellHeight) =
|
|
positionAndHeightOfRow(rowKey)
|
|
|
|
println(s"Editing $colKey x $rowKey")
|
|
FormulaEditor(
|
|
cellX+x.now()+Constants.GUTTER_WIDTH+(cellWidth/2),
|
|
cellY+y.now()+Constants.GUTTER_HEIGHT+cellHeight
|
|
)
|
|
}
|
|
|
|
def root = div(
|
|
className("dataTable"),
|
|
|
|
// Table position and size information (reactive)
|
|
|
|
styleAttr <--
|
|
Signal.combine(x, y, width, height)
|
|
.map { case (x, y, w, h) =>
|
|
Seq(
|
|
s"left: ${x}px;",
|
|
s"top: ${y}px;",
|
|
s"width: ${w+Constants.GUTTER_WIDTH}px;",
|
|
s"height: ${h+Constants.GUTTER_HEIGHT}px;"
|
|
).mkString(" ")
|
|
},
|
|
|
|
// Initiate a Frame Drag session on click
|
|
|
|
onMouseDown --> {
|
|
(evt) =>
|
|
if(evt.button == 0){
|
|
DragFrame.position(
|
|
x.now(), y.now(),
|
|
width.now()+Constants.GUTTER_WIDTH,
|
|
height.now()+Constants.GUTTER_HEIGHT,
|
|
evt
|
|
) { (x, y) =>
|
|
connection.send(
|
|
RequestSetTablePosition(id, x, y)
|
|
)
|
|
}
|
|
evt.stopPropagation()
|
|
}
|
|
},
|
|
|
|
// Display column gutters
|
|
|
|
div(
|
|
className("columnGutters"),
|
|
styleAttr(s"left: ${Constants.GUTTER_WIDTH}px"),
|
|
children <--
|
|
columnGutters.map { _.map { case (_, signal) =>
|
|
div(
|
|
className("columnGutter"),
|
|
styleAttr <-- signal.map { case col =>
|
|
s"width: ${col.spec.width-Constants.BORDER}px; height: ${Constants.GUTTER_HEIGHT-2*Constants.BORDER}px; left: ${col.position}px"
|
|
},
|
|
child <-- signal.map { _.label }
|
|
)
|
|
}},
|
|
),
|
|
|
|
// Display row gutters
|
|
|
|
div(
|
|
className("rowGutters"),
|
|
styleAttr(s"top: ${Constants.GUTTER_HEIGHT}px"),
|
|
children <--
|
|
rowGutters.map { _.map { case (_, signal) =>
|
|
div(
|
|
className("rowGutter"),
|
|
styleAttr <-- signal.map { case row =>
|
|
s"width: ${Constants.GUTTER_WIDTH-2*Constants.BORDER}px; height: ${row.spec.height-Constants.BORDER}px; top: ${row.position}px"
|
|
},
|
|
div(
|
|
className("valignHack"),
|
|
child <-- signal.map { _.label }
|
|
)
|
|
)
|
|
}},
|
|
),
|
|
|
|
// Display the actual content
|
|
|
|
div(
|
|
className("content"),
|
|
styleAttr <--
|
|
Signal.combine(width, height)
|
|
.map { case (w, h) =>
|
|
Seq(
|
|
s"width: ${w}px;",
|
|
s"height: ${h}px;",
|
|
s"left: ${Constants.GUTTER_WIDTH}px;",
|
|
s"top: ${Constants.GUTTER_HEIGHT}px",
|
|
).mkString(" ")
|
|
},
|
|
children <--
|
|
rowGutters.map { _.map { case (rowKey, rowStream) =>
|
|
div(
|
|
className("row"),
|
|
styleAttr <--
|
|
rowStream.map { row =>
|
|
s"height: ${row.spec.height}px; top: ${row.position}px"
|
|
},
|
|
children <--
|
|
columnGutters.map { _.map { case (colKey, colStream) =>
|
|
div(
|
|
className("cell"),
|
|
styleAttr <--
|
|
colStream.map { col =>
|
|
s"width: ${col.spec.width}px; left: ${col.position}px"
|
|
},
|
|
child <--
|
|
cellsByPosition.flatMap {
|
|
_.get( (rowKey, colKey) )
|
|
.map { _.map { _.tag } }
|
|
.getOrElse {
|
|
Val(
|
|
div(className("cellBody empty"), "")
|
|
)
|
|
}
|
|
},
|
|
|
|
// onMouseDown is handled before onClick, so
|
|
// avoid the MouseDown event propagating to the
|
|
// parent container and triggering a drag
|
|
onMouseDown --> {
|
|
evt => evt.stopPropagation()
|
|
},
|
|
onClick --> {
|
|
evt => startEditing(colKey, rowKey)
|
|
}
|
|
)
|
|
|
|
} }
|
|
)
|
|
} },
|
|
),
|
|
|
|
// Display the resizing bars - Up<->Down
|
|
|
|
children <--
|
|
rowGutters.map { _.zipWithIndex.map {
|
|
case ((rowKey, rowStream), rowIdx) =>
|
|
div(
|
|
className("rowResizeBar"),
|
|
styleAttr <--
|
|
rowStream.map { col =>
|
|
s"top: ${Constants.GUTTER_HEIGHT + col.position + col.spec.height}px"
|
|
},
|
|
onMouseDown --> {
|
|
(evt) =>
|
|
if(evt.button == 0){
|
|
|
|
// We can't peek into colStream without violating ownership, so just
|
|
// recompute the position
|
|
val (pos, height) =
|
|
positionAndHeightOfRow(rowKey)
|
|
|
|
DragFrame.height(
|
|
x.now(),
|
|
y.now()+pos+Constants.GUTTER_HEIGHT,
|
|
width.now()+Constants.GUTTER_WIDTH,
|
|
height,
|
|
evt
|
|
) { newHeight =>
|
|
println(s"Drag $rowKey @ ($pos, ${pos+height}) -> $newHeight")
|
|
connection.send(
|
|
RequestUpdateRows(
|
|
id,
|
|
SeqReplace(
|
|
rowIdx,
|
|
rows.now().find { _.id == rowKey }.get
|
|
.copy( height = newHeight )
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
evt.stopPropagation()
|
|
}
|
|
)
|
|
} },
|
|
|
|
// Display the resizing bars - Left<->Right
|
|
|
|
children <--
|
|
columnGutters.map { _.zipWithIndex.map {
|
|
case ((colKey, colStream), colIdx) =>
|
|
div(
|
|
className("columnResizeBar"),
|
|
styleAttr <--
|
|
colStream.map { col =>
|
|
s"left: ${Constants.GUTTER_WIDTH + col.position + col.spec.width}px"
|
|
},
|
|
onMouseDown --> {
|
|
(evt) =>
|
|
if(evt.button == 0){
|
|
|
|
// We can't peek into rowStream without violating ownership, so just
|
|
// recompute the position
|
|
val (pos, width) =
|
|
positionAndWidthOfColumn(colKey)
|
|
|
|
DragFrame.width(
|
|
x.now()+pos+Constants.GUTTER_WIDTH,
|
|
y.now(),
|
|
width,
|
|
height.now()+Constants.GUTTER_HEIGHT,
|
|
evt
|
|
) { newWidth =>
|
|
println(s"Drag $colKey @ ($pos, ${pos+width}) -> $newWidth")
|
|
connection.send(
|
|
RequestUpdateColumns(
|
|
id,
|
|
SeqReplace(
|
|
colIdx,
|
|
columns.now().find { _.id == colKey }.get
|
|
.copy( width = newWidth )
|
|
)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
evt.stopPropagation()
|
|
}
|
|
)
|
|
} },
|
|
|
|
// Display the add/remove column widget
|
|
|
|
div(
|
|
className("addRemoveColumnWidget"),
|
|
Icon("caret-right"),
|
|
onMouseDown --> {
|
|
(evt) =>
|
|
if(evt.button == 0){
|
|
val snaps =
|
|
columns.now()
|
|
.foldLeft( Seq[Int]() ){
|
|
(accum, col) =>
|
|
accum :+ (accum.lastOption.getOrElse(0) + col.width )
|
|
}
|
|
// println(s"Snaps: ${snaps.mkString(", ")}")
|
|
DragFrame.snapWidth(
|
|
x.now()+Constants.GUTTER_WIDTH,
|
|
y.now(),
|
|
width.now(),
|
|
height.now()+Constants.GUTTER_HEIGHT,
|
|
evt
|
|
)(snapTo(snaps, Constants.DEFAULT_CELL_WIDTH, _)) { snap =>
|
|
val oldColCount = columns.now().length
|
|
val newColCount = snapIndex(snaps, Constants.DEFAULT_CELL_WIDTH, snap) + 1
|
|
|
|
var i = 0
|
|
if(oldColCount < newColCount)
|
|
{
|
|
for(i <- oldColCount until newColCount)
|
|
{
|
|
println(s"Adding column $i")
|
|
connection.send(
|
|
RequestUpdateColumns(
|
|
id,
|
|
SeqInsert(
|
|
i,
|
|
serialized.ColSpec(
|
|
id = Constants.NULL_UUID,
|
|
width = Constants.DEFAULT_CELL_WIDTH
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
} else if(oldColCount > newColCount)
|
|
{
|
|
for(i <- (oldColCount-1) to newColCount by -1)
|
|
{
|
|
println(s"Removing column $i")
|
|
connection.send(
|
|
RequestUpdateColumns(
|
|
id,
|
|
SeqDelete(i)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
evt.stopPropagation()
|
|
}
|
|
}
|
|
),
|
|
|
|
div(
|
|
className("addRemoveRowWidget"),
|
|
Icon("caret-down"),
|
|
onMouseDown --> {
|
|
(evt) =>
|
|
if(evt.button == 0){
|
|
val snaps =
|
|
rows.now()
|
|
.foldLeft( Seq[Int]() ){
|
|
(accum, row) =>
|
|
accum :+ (accum.lastOption.getOrElse(0) + row.height )
|
|
}
|
|
// println(s"Snaps: ${snaps.mkString(", ")}")
|
|
DragFrame.snapHeight(
|
|
x.now(),
|
|
y.now()+Constants.GUTTER_HEIGHT,
|
|
width.now()+Constants.GUTTER_WIDTH,
|
|
height.now(),
|
|
evt
|
|
)(snapTo(snaps, Constants.DEFAULT_CELL_HEIGHT, _)) { snap =>
|
|
val oldRowCount = rows.now().length
|
|
val newRowCount = snapIndex(snaps, Constants.DEFAULT_CELL_HEIGHT, snap) + 1
|
|
|
|
var i = 0
|
|
if(oldRowCount < newRowCount)
|
|
{
|
|
for(i <- oldRowCount until newRowCount)
|
|
{
|
|
println(s"Adding row $i")
|
|
connection.send(
|
|
RequestUpdateRows(
|
|
id,
|
|
SeqInsert(
|
|
i,
|
|
serialized.RowSpec(
|
|
id = Constants.NULL_UUID,
|
|
height = Constants.DEFAULT_CELL_HEIGHT
|
|
)
|
|
)
|
|
)
|
|
)
|
|
}
|
|
} else if(oldRowCount > newRowCount)
|
|
{
|
|
for(i <- (oldRowCount-1) to newRowCount by -1)
|
|
{
|
|
println(s"Removing row $i")
|
|
connection.send(
|
|
RequestUpdateRows(
|
|
id,
|
|
SeqDelete(i)
|
|
)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
evt.stopPropagation()
|
|
}
|
|
}
|
|
)
|
|
)
|
|
} |