From 6df1b5900c4a34cc1f31a9163c32f7ba19c662c2 Mon Sep 17 00:00:00 2001 From: Oliver Date: Sun, 15 Jan 2023 16:25:09 -0500 Subject: [PATCH] Work in progress on the formula editor --- .../okennedy/cells/WebsocketConnection.scala | 2 +- .../src/net/okennedy/cells/state/Table.scala | 6 +- cells/ui/css/cells.scss | 74 +++++++++++++++++++ .../src/net/okennedy/cells/sheet/Table.scala | 66 +++++++++++++---- .../cells/widgets/FormulaEditor.scala | 57 ++++++++++++++ 5 files changed, 186 insertions(+), 19 deletions(-) create mode 100644 cells/ui/src/net/okennedy/cells/widgets/FormulaEditor.scala diff --git a/cells/server/src/net/okennedy/cells/WebsocketConnection.scala b/cells/server/src/net/okennedy/cells/WebsocketConnection.scala index 8f6c96d..ae17f88 100644 --- a/cells/server/src/net/okennedy/cells/WebsocketConnection.scala +++ b/cells/server/src/net/okennedy/cells/WebsocketConnection.scala @@ -14,7 +14,7 @@ class WebsocketConnection(val id: Int, channel: WsChannelActor) event match { case Ws.Text(data) => - println(s"ZZZ: $data") + // println(s"ZZZ: $data") Json.parse(data).as[WebsocketRequest] match { case WebsocketHello(_) => for(table <- CellsServer.layout.tables.values) diff --git a/cells/server/src/net/okennedy/cells/state/Table.scala b/cells/server/src/net/okennedy/cells/state/Table.scala index 5dc83bb..20f86d9 100644 --- a/cells/server/src/net/okennedy/cells/state/Table.scala +++ b/cells/server/src/net/okennedy/cells/state/Table.scala @@ -61,7 +61,7 @@ class Table(val id: Identifier) case SeqReplace(pos, _) => op.map { _.copy(id = columns(pos).id) } case SeqDelete(_) => op } - println(s"Update: $op") + // println(s"Update: $op") opWithValidId.bufferApply(columns) WebsocketConnection.broadcast( UpdateTableColumns(id, opWithValidId) @@ -101,13 +101,13 @@ class Table(val id: Identifier) { this.x = x this.y = y - println(s"Position now $x, $y") + // println(s"Position now $x, $y") WebsocketConnection.broadcast(SetTablePosition(id, x, y)) } def update(op: TableRequest): Unit = { - println(s"Table update: $op") + // println(s"Table update: $op") op match { case RequestSetTablePosition(_, x, y) => setPosition(x, y) case RequestUpdateColumns(_, cop) => updateColumns(cop) diff --git a/cells/ui/css/cells.scss b/cells/ui/css/cells.scss index aa53eeb..399af13 100644 --- a/cells/ui/css/cells.scss +++ b/cells/ui/css/cells.scss @@ -167,4 +167,78 @@ body margin-top: 2px; border: dashed lightgrey 2px; } + + .formulaEditor + { + position: absolute; + + .pointer + { + position: absolute; + top: -10px; + left: -4px; + color: #aaa; + font-size: 20px; + } + .editorBody + { + $editorWidth: 200px; + $buttonSize: 20px; + $spacing: 8px; + $editorPadding: 2px; + + position: absolute; + border: solid 1px #aaa; + border-radius: 2px; + top: 5px; + left: -16px; + vertical-align: middle; + padding: 8px; + background-color: #ddd; + width: $editorWidth; + height: $buttonSize + 4px; + + .inputArea + { + position: absolute; + left: $spacing; + top: $spacing; + width: $editorWidth - 2 * $buttonSize - 3 * $spacing; + height: $buttonSize; + background-color: white; + padding: 2px; + border: 1px solid #ccc; + cursor: text; + + &:focus + { + border: 1px solid #aaa; + } + } + + .button + { + position: absolute; + top: $editorPadding + $spacing; + width: $buttonSize; + height: $buttonSize; + border-radius: $buttonSize / 2; + text-align: center; + color: white; + cursor: pointer; + + &.cancel + { + right: $buttonSize + 2 * $spacing; + background: red; + } + + &.accept + { + right: $spacing; + background: green; + } + } + } + } } \ No newline at end of file diff --git a/cells/ui/src/net/okennedy/cells/sheet/Table.scala b/cells/ui/src/net/okennedy/cells/sheet/Table.scala index 3095f75..0cf9413 100644 --- a/cells/ui/src/net/okennedy/cells/sheet/Table.scala +++ b/cells/ui/src/net/okennedy/cells/sheet/Table.scala @@ -28,6 +28,7 @@ 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) { @@ -46,6 +47,7 @@ class Table(val id: Identifier, connection: Connection) this.columns.set(ser.columns) this.rows.set(ser.rows) ) + startEditing(columns.now()(1).id, rows.now()(1).id) } def process(op: TableOp): Unit = @@ -114,6 +116,28 @@ class Table(val id: Identifier, connection: Connection) 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") @@ -151,6 +175,20 @@ class Table(val id: Identifier, connection: Connection) 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"), @@ -262,7 +300,17 @@ class Table(val id: Identifier, connection: Connection) 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) + } ) } } @@ -288,13 +336,7 @@ class Table(val id: Identifier, connection: Connection) // We can't peek into colStream without violating ownership, so just // recompute the position val (pos, height) = - 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, width), _) => x - }.right.get + positionAndHeightOfRow(rowKey) DragFrame.height( x.now(), @@ -339,13 +381,7 @@ class Table(val id: Identifier, connection: Connection) // We can't peek into rowStream without violating ownership, so just // recompute the position val (pos, width) = - 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 + positionAndWidthOfColumn(colKey) DragFrame.width( x.now()+pos+Constants.GUTTER_WIDTH, diff --git a/cells/ui/src/net/okennedy/cells/widgets/FormulaEditor.scala b/cells/ui/src/net/okennedy/cells/widgets/FormulaEditor.scala new file mode 100644 index 0000000..ffb8891 --- /dev/null +++ b/cells/ui/src/net/okennedy/cells/widgets/FormulaEditor.scala @@ -0,0 +1,57 @@ +package net.okennedy.cells.widgets + +import org.scalajs.dom +import com.raquo.laminar.api.L._ +import com.raquo.laminar.nodes.ReactiveHtmlElement + +class FormulaEditor(x: Int, y: Int) + extends Widget +{ + val formula = Var[String](initial = "Hihihihi") + + val inputArea = + input( + className("inputArea"), + `type`("text"), + controlled( + value <-- formula, + onInput.mapToValue --> formula.writer + ) + ) + + def cancel(): Unit = + { + println("Cancelled") + } + + def accept(): Unit = + { + println(s"Accepted with ${formula.now()}") + } + + val root = + div( + className("formulaEditor"), + styleAttr(s"left: ${x}px; top: ${y}px"), + div( + className("pointer"), + Icon("caret-up") + ), + div( + className("editorBody"), + inputArea, + div( className("cancel button"), Icon("times"), + onClick --> { evt => cancel() } + ), + div( className("accept button"), Icon("check"), + onClick --> { evt => accept() } + ) + ) + ) +} + +object FormulaEditor +{ + def apply(x: Int, y: Int) = + Widgets.register(new FormulaEditor(x, y)) +} \ No newline at end of file