Refactored FX code into it's own class +

misc changes
This commit is contained in:
Rohan Sircar 2020-12-20 15:19:37 +05:30
parent 029cbbd5ac
commit 6ef5c9778e
9 changed files with 191 additions and 174 deletions

View File

@ -1,14 +1,21 @@
package nova.monadic_sfx package nova.monadic_sfx
import java.util.concurrent.TimeUnit
import scala.util.Random
import com.softwaremill.macwire._ import com.softwaremill.macwire._
import io.odin.Logger import io.odin.Logger
import monix.bio.IO
import monix.bio.Task import monix.bio.Task
import monix.eval.Coeval
import monix.{eval => me}
import nova.monadic_sfx.executors.Schedulers import nova.monadic_sfx.executors.Schedulers
import nova.monadic_sfx.implicits.JFXButton import nova.monadic_sfx.implicits.JFXButton
import nova.monadic_sfx.implicits.JavaFXMonixObservables._ import nova.monadic_sfx.implicits.JavaFXMonixObservables._
import nova.monadic_sfx.ui.MyFxApp import nova.monadic_sfx.ui.MyFxApp
import nova.monadic_sfx.ui.components.router.BrainNotWorking
import nova.monadic_sfx.ui.components.router.FXRouter import nova.monadic_sfx.ui.components.router.FXRouter
import nova.monadic_sfx.ui.components.router.Page
import nova.monadic_sfx.ui.components.todo.TodoListStore import nova.monadic_sfx.ui.components.todo.TodoListStore
import nova.monadic_sfx.ui.components.todo.TodoListView import nova.monadic_sfx.ui.components.todo.TodoListView
import org.gerweck.scalafx.util._ import org.gerweck.scalafx.util._
@ -18,12 +25,16 @@ import scalafx.beans.property.ObjectProperty
import scalafx.beans.property.StringProperty import scalafx.beans.property.StringProperty
import scalafx.collections.ObservableBuffer import scalafx.collections.ObservableBuffer
import scalafx.geometry.Insets import scalafx.geometry.Insets
import scalafx.geometry.Pos
import scalafx.scene.Parent
import scalafx.scene.Scene import scalafx.scene.Scene
import scalafx.scene.control.Label
import scalafx.scene.control.TableColumn import scalafx.scene.control.TableColumn
import scalafx.scene.control.TableView import scalafx.scene.control.TableView
import scalafx.scene.layout.BorderPane
import scalafx.scene.layout.FlowPane
import scalafx.scene.layout.HBox import scalafx.scene.layout.HBox
import scalafx.scene.paint.Color import scalafx.scene.layout.Priority
import scalafx.scene.shape.Rectangle
class MainApp( class MainApp(
// spawnProtocol: ActorSystem[SpawnProtocol.Command], // spawnProtocol: ActorSystem[SpawnProtocol.Command],
@ -31,136 +42,108 @@ class MainApp(
startTime: Long startTime: Long
)(implicit logger: Logger[Task]) { )(implicit logger: Logger[Task]) {
lazy val addTodoButton = new JFXButton {
text = "Add"
}
lazy val addTodoObs = addTodoButton.observableAction
// lazy val todoListView = TodoListView.defaultListView
lazy val _scene = new Scene { lazy val _scene = new Scene {
root = new HBox { root = new HBox {
padding = Insets(20) padding = Insets(20)
content = new Rectangle {
width = 400
height = 200
fill = Color.DeepSkyBlue
}
children ++= Seq(
// new JFXButton {
// text = "DummyButton"
// },
// new JFXButton {
// text = "DummyButton2"
// },
// addTodoButton,
// Test.ttv
// todoListView
)
} }
} }
private lazy val stage = new PrimaryStage { private lazy val stage = new PrimaryStage {
title = "Simple ScalaFX App" title = "Simple ScalaFX App"
scene = _scene scene = _scene
width = 1000 width = 640
height = 400 height = 480
} }
// implicit val l = logger
// implicit val sp = spawnProtocol
val program = for { val program = for {
(fxApp, fxAppFib) <- wire[MyFxApp].init(stage) (fxApp, fxAppFib) <- wire[MyFxApp].init(stage)
// _ <- Task(fxApp.stage = stage) _ <-
// .executeOn(schedulers.fx) wire[MainAppDelegate].init
// .delayExecution(2000.millis) .flatMap(mainSceneNode => Task(_scene.getChildren += mainSceneNode))
// todoComponent <- createTodoComponent .executeOn(schedulers.fx)
// _ <- toIO( currentTime <- IO.clock.realTime(TimeUnit.MILLISECONDS)
// addTodoObs
// .mapEval(_ =>
// toTask(todoComponent.send(TodoListComponent.Add(Todo(1, "blah"))))
// )
// .completedL
// .executeOn(schedulers.fx)
// .startAndForget
// )
_ <- createTodoComponent.executeOn(schedulers.fx)
router <- Task(BrainNotWorking.router)
routerStore <- router.store(BrainNotWorking.Page.Home, logger)
routerNode <- for {
node <-
Task
.deferAction(implicit s =>
Task(new HBox {
children <-- router
.render(BrainNotWorking.resolver)(routerStore)
.map(_.delegate)
})
)
.executeOn(schedulers.fx)
_ <- Task.deferFuture(
routerStore.onNext(FXRouter.Replace(BrainNotWorking.Page.UserHome(1)))
)
} yield node
// _ <-
// BrainNotWorking
// .routerTask(logger)
// .flatMap(node => Task(_scene.getChildren += node))
// .executeOn(schedulers.fx)
_ <- Task(_scene.getChildren += routerNode).executeOn(schedulers.fx)
_ <- logger.info( _ <- logger.info(
s"Application started in ${(System.currentTimeMillis() - startTime) / 1000f} seconds" s"Application started in ${(currentTime - startTime) / 1000f} seconds"
) )
_ <- fxAppFib.join _ <- fxAppFib.join
} yield () } yield ()
// def createTodoComponent: Task[TodoListComponent] = { }
// for {
// channel <-
// ConcurrentChannel
// .of[Task, TodoListComponent.Complete, TodoListComponent.Command]
// scheduler = schedulers.fx
// lv <- TodoListView.defaultListView2.executeOn(scheduler)
// // todoLV = new TodoListView(lv)
// todoComponent <- wire[TodoListComponent.Props].create
// // TODO make this a "message pass" instead of mutating directly
// _ <- Task(_scene.getChildren += lv).executeOn(scheduler)
// // _ <- toIO(
// // delObs
// // .doOnNext(_ => toTask(logger.debug("Pressed delete")))
// // .doOnNext(todo =>
// // toTask(
// // for {
// // _ <- logger.debug(s"Got todo $todo")
// // _ <- todoComponent.send(TodoListComponent.Delete(todo.id))
// // // _ <- Task.sequence(
// // // lst.map(todo =>
// // // todoComponent.send(TodoListComponent.Delete(todo.id))
// // // )
// // // )
// // } yield ()
// // )
// // )
// // .completedL
// // ).startAndForget
// // _ <- toIO(
// // editObs
// // .doOnNext(_ => toTask(logger.debug("Pressed edit")))
// // .completedL
// // ).startAndForget
// } yield todoComponent
// }
def createTodoComponent: Task[Unit] = class MainAppDelegate(schedulers: Schedulers)(implicit logger: Logger[Task]) {
val buttonStyle = """| -fx-padding: 0.7em 0.57em;
| -fx-font-size: 14px;
| -jfx-button-type: RAISED;
| -fx-background-color: rgb(77,102,204);
| -fx-pref-width: 200;
| -fx-text-fill: WHITE; """.stripMargin
def init =
for { for {
store <- TodoListStore(logger) router <- Task.pure(new FXRouter[Page])
rootNode <- TodoListView(store) routerStore <- router.store(Page.Home, logger)
_ <- Task(_scene.getChildren += rootNode) todoStore <- TodoListStore(logger)
} yield () todoComponent <- TodoListView(todoStore)
resolver: PartialFunction[Page, Task[Parent]] = {
case Page.Home =>
Task(new Label {
text = "HomePage"
})
case Page.UserHome(id0) =>
Task(new Label {
text = s"User Home, Id = $id0"
})
case Page.Todo =>
Task(todoComponent)
}
routerNode <-
Task
.deferAction(implicit s =>
Task(new HBox {
//TODO find a better way to do this
var oldValue: Option[Parent] = None
children <-- router
.render(resolver)(routerStore)
// call cancel on the old component to cancel all subscriptions
.doOnNextF(newValue =>
Coeval { oldValue.foreach(_ => ()) } >> Coeval {
oldValue = Some(newValue)
}
)
.map(_.delegate)
})
)
mainSceneNode <- Task.deferAction(implicit s =>
Task(new BorderPane {
hgrow = Priority.Always
vgrow = Priority.Always
center = routerNode
bottom = new FlowPane {
alignment = Pos.Center
hgap = 20
children = Seq(
new JFXButton {
text = "Todo"
style = buttonStyle
obsAction
.useLazyEval(
me.Task.pure(FXRouter.Replace(Page.Todo))
) --> routerStore
},
new JFXButton {
text = "UserHome"
style = buttonStyle
obsAction
.useLazyEval(
me.Task(FXRouter.Replace(Page.UserHome(Random.nextInt(20))))
) --> routerStore
}
)
}
})
)
} yield mainSceneNode
} }
class TestModel(_name: String, _age: Int) { class TestModel(_name: String, _age: Int) {

View File

@ -19,23 +19,23 @@ trait MainModule extends ActorModule with UiModule with HttpModule {
"nova.monadic_sfx.util.reactive.Store" -> storeLogger "nova.monadic_sfx.util.reactive.Store" -> storeLogger
) )
.withFallback(defaultLogger) .withFallback(defaultLogger)
.withAsync() .withAsync(timeWindow = 1.millis)
def makeLogger = def makeLogger =
for { for {
defaultLogger <- consoleLogger[Task]() defaultLogger <- consoleLogger[Task]()
.withAsync(timeWindow = 1.millis) |+| fileLogger[Task]( .withAsync(timeWindow = 1.millis) |+| fileLogger[Task](
"application.log" "application.log"
).withAsync() ).withAsync(timeWindow = 1.millis)
middlewareLogger <- middlewareLogger <-
consoleLogger[ consoleLogger[
Task Task
](formatter = Middlewares.format) ](formatter = Middlewares.format)
.withMinimalLevel(Level.Trace) .withMinimalLevel(Level.Trace)
.withAsync() |+| fileLogger[Task]( .withAsync(timeWindow = 1.millis) |+| fileLogger[Task](
"stores.log", "stores.log",
formatter = Middlewares.format formatter = Middlewares.format
).withAsync() ).withAsync(timeWindow = 1.millis)
routerLogger <- routerLogger(defaultLogger, middlewareLogger) routerLogger <- routerLogger(defaultLogger, middlewareLogger)
} yield (routerLogger) } yield (routerLogger)
} }

View File

@ -12,7 +12,7 @@ class ActionObservableExecutor[T](
) extends AnyVal { ) extends AnyVal {
def -->(sub: Observer[T])(implicit s: Scheduler) = def -->(sub: Observer[T])(implicit s: Scheduler) =
delegate delegate
.doOnNext(el => me.Task(sub.onNext(el))) .doOnNext(el => me.Task.deferFuture(sub.onNext(el)) >> me.Task.unit)
.subscribe() .subscribe()
} }

View File

@ -31,7 +31,8 @@ class MyFxApp(val schedulers: Schedulers)(implicit logger: Logger[Task]) {
def init(stage: => PrimaryStage, delay: FiniteDuration = 200.millis) = def init(stage: => PrimaryStage, delay: FiniteDuration = 200.millis) =
for { for {
_ <- logger.info("Starting FX App") _ <- logger.info("Starting FX App")
fib <- Task(internal.main(Array.empty)).start fib <-
Task(internal.main(Array.empty)).start.executeOn(schedulers.blocking)
_ <- Task.sleep(200.millis) _ <- Task.sleep(200.millis)
_ <- Task(internal.stage = stage) _ <- Task(internal.stage = stage)
.executeOn(schedulers.fx) .executeOn(schedulers.fx)

View File

@ -1,47 +1,52 @@
package nova.monadic_sfx.ui.components.router package nova.monadic_sfx.ui.components.router
import enumeratum._ import io.circe.Codec
import io.circe.Decoder
import io.circe.Encoder import io.circe.Encoder
import io.circe.generic.JsonCodec import io.circe.generic.JsonCodec
import io.circe.generic.semiauto._
import io.odin.Logger import io.odin.Logger
import monix.bio.Task import monix.bio.Task
import nova.monadic_sfx.util.IOUtils import nova.monadic_sfx.util.IOUtils
import nova.monadic_sfx.util.reactive.Middlewares
import nova.monadic_sfx.util.reactive.Reducer import nova.monadic_sfx.util.reactive.Reducer
import nova.monadic_sfx.util.reactive.Store import nova.monadic_sfx.util.reactive.Store
import scalafx.scene.Parent import scalafx.scene.Parent
import scalafx.scene.control.Label
object FXRouter { object FXRouter {
final case class State[P](page: P) final case class State[P](page: P)
@JsonCodec // @JsonCodec
sealed abstract class Action[T] sealed abstract class Action[+T]
// final case object Init extends Action
final case class Replace[T](p: T) extends Action[T] final case class Replace[T](p: T) extends Action[T]
object Action {
implicit def codec[T: Encoder: Decoder]: Codec[Action[T]] = deriveCodec
}
type FXStore[P] = Store[Action[P], State[P]] type FXStore[P] = Store[Action[P], State[P]]
// def resolver2 = resolver.lift.andThen(_.getOrElse(notFound))
// def resolver: PartialFunction[P <: Enum[P]][P, Parent] = {
// case Home => new TextField
// }
} }
class FXRouter[P <: EnumEntry]( class FXRouter[P](
)(implicit E: Encoder[P]) { )(implicit E: Encoder[P], D: Decoder[P]) {
import FXRouter._ import FXRouter._
def store(initialPage: P, logger: Logger[Task]) = def store(initialPage: P, logger: Logger[Task]): Task[FXStore[P]] =
Task.deferAction(implicit s => Task.deferAction(implicit s =>
Store.createL[Action[P], State[P]]( for {
Replace(initialPage), mw <- Middlewares.actionLoggerMiddleware[Action[P], State[P]](
State(initialPage), logger,
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _) "RouterStore"
// Seq(actionLoggerMiddleware(logger, "RouterStore")) )
) store <- Store.createL[Action[P], State[P]](
Replace(initialPage),
State(initialPage),
Reducer.withOptionalEffects[Task, Action[P], State[P]](reducer _),
Seq(mw)
)
} yield store
) )
def reducer( def reducer(
@ -67,28 +72,12 @@ class FXRouter[P <: EnumEntry](
} }
} }
object BrainNotWorking { @JsonCodec
sealed trait Page
@JsonCodec object Page {
sealed trait Page extends EnumEntry final case object Home extends Page
object Page extends Enum[Page] { final case class UserHome(id: Int) extends Page
val values = findValues final case object Todo extends Page
final case object Home extends Page
final case class UserHome(id: Int) extends Page
}
def resolver: PartialFunction[Page, Task[Parent]] = {
case Page.Home =>
Task(new Label {
text = "HomePage"
})
case Page.UserHome(id0) =>
Task(new Label {
text = s"User Home, Id = $id0"
})
}
val router = new FXRouter[Page]
} }
// case class State() // case class State()

View File

@ -16,7 +16,7 @@ import scalafx.Includes._
import scalafx.beans.property.ObjectProperty import scalafx.beans.property.ObjectProperty
import scalafx.beans.property.StringProperty import scalafx.beans.property.StringProperty
import scalafx.geometry.Insets import scalafx.geometry.Insets
import scalafx.scene.Node import scalafx.scene.Parent
import scalafx.scene.control.ContextMenu import scalafx.scene.control.ContextMenu
import scalafx.scene.control.ListCell import scalafx.scene.control.ListCell
import scalafx.scene.control.SelectionMode import scalafx.scene.control.SelectionMode
@ -25,17 +25,12 @@ import scalafx.scene.text.Text
object TodoListView { object TodoListView {
def apply( def apply(
store: MonixProSubject[ store: Store[TodoListStore.Action, TodoListStore.State]
TodoListStore.Action, ): Task[Parent] =
(TodoListStore.Action, TodoListStore.State)
]
): Task[Node] =
Task.deferAction(implicit s => Task.deferAction(implicit s =>
Task { Task {
val cc = CompositeCancelable() val cc = CompositeCancelable()
val todos = val todos = store.map { case (_, state) => state.todos }
store.map { case (_, state) => state.todos }
// Todo(-1, "").some
val _selectedItems = ObjectProperty(Seq.empty[Todo]) val _selectedItems = ObjectProperty(Seq.empty[Todo])
new HBox { new HBox {

View File

@ -0,0 +1,44 @@
package nova.monadic_sfx.util
import monix.bio.Task
import monix.bio.UIO
import monix.catnap.MVar
/**
* Synchronization wrapper for a mutable object
*
* @param obj the mutable object
* @param lock lock for synchronization
*/
class SynchedObject[A](obj: A, lock: MLock) {
def modify(f: A => Task[Unit]): Task[Unit] =
lock.greenLight(f(obj))
def get: Task[A] = lock.greenLight(Task(obj))
}
object SynchedObject {
def apply[A](obj: A) =
MVar[Task]
.of(())
.map(m => new MLock(m))
.flatMap(lock => Task(new SynchedObject(obj, lock)))
}
final class MLock(mvar: MVar[Task, Unit]) {
def acquire: Task[Unit] =
mvar.take
def release: Task[Unit] =
mvar.put(())
def greenLight[A](fa: Task[A]): Task[A] =
for {
_ <- acquire
a <- fa.doOnCancel(
release.onErrorHandleWith(ex => UIO(println(ex.getMessage())))
)
_ <- release
} yield a
}

View File

@ -21,7 +21,7 @@ import monix.reactive.Observable
final case class StoreInfo[A]( final case class StoreInfo[A](
name: String, name: String,
action: A, action: A,
time: LocalDateTime = LocalDateTime.now() time: LocalDateTime
) )
object StoreInfo { object StoreInfo {
@ -68,7 +68,8 @@ object Middlewares {
Task((obs: Observable[(A, M)]) => Task((obs: Observable[(A, M)]) =>
obs.doOnNextF { obs.doOnNextF {
case (a, _) => case (a, _) =>
logger.debug(StoreInfo(name, a)) Task(LocalDateTime.now())
.flatMap(curTime => logger.debug(StoreInfo(name, a, curTime)))
} }
) )
) )

View File

@ -1,5 +1,7 @@
package nova.monadic_sfx.util.reactive package nova.monadic_sfx.util.reactive
import java.time.LocalDateTime
import io.circe.Encoder import io.circe.Encoder
import io.odin.Logger import io.odin.Logger
import monix.bio.Task import monix.bio.Task
@ -72,9 +74,11 @@ object Store {
val obs = subject val obs = subject
.doOnNextF(action => .doOnNextF(action =>
logger.debug( Task(LocalDateTime.now()).flatMap(curTime =>
StoreInfo(storeName, action) logger.debug(
) // .executeOn(Scheduler.global) StoreInfo(storeName, action, curTime)
)
)
) )
// .doOnNextF(action => Coeval(println(action))) // .doOnNextF(action => Coeval(println(action)))
.scanEvalF[Task, (A, M)](Task.pure(initialAction -> initialState))( .scanEvalF[Task, (A, M)](Task.pure(initialAction -> initialState))(