Commit f11b4708 authored by David's avatar David

Can do the updates via an out-of-band observable!

parent 377113d5
package `is`.kow.deskscreen.ticker
import com.github.thomasnield.rxkotlinfx.doOnNextFx
import io.reactivex.BackpressureStrategy
import io.reactivex.Flowable
import io.reactivex.Completable
import io.reactivex.Observable
import io.reactivex.processors.PublishProcessor
import io.reactivex.disposables.Disposable
import io.reactivex.schedulers.Schedulers
import javafx.animation.Animation
import javafx.animation.KeyFrame
......@@ -22,17 +21,17 @@ import java.time.LocalDateTime
import java.time.ZonedDateTime
import java.time.format.DateTimeFormatter
import java.util.concurrent.ConcurrentLinkedQueue
import java.util.concurrent.Executors
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
//Generic the shit out of this!
data class TickerEntry<T : Node>(
val title: String,
val content: T,
val reschedule: Boolean = false,
val update: ((content: T) -> Unit)? = null
val update: ((content: T) -> Unit)? = null,
val updateObservable: ((content: T) -> Completable)? = null
//TODO: need to add a size buffer, some dynamic things get weird
* Display a ticker view of things
......@@ -82,23 +81,40 @@ class TickerView : View() {
//Lets create one that does a time
//This causes the lag that causes them to smear together, need to update the rendering logic to not be threaded
// per each item
val time = TickerEntry("CurrentTime", createText("one")) { text ->
val time = TickerEntry("CurrentTime", createText("one"), update = { text ->
//Automatically called by the thing on the right thread...
text.text =
//TODO: needs a way of cancelling
//TODO: with a subscription, I need a way to unsub it later
val time2 = TickerEntry("CurrentTimeObserved", createText("one"), updateObservable = { text ->
Observable.interval(1, TimeUnit.SECONDS)
.doOnNextFx {
text.text =
.doOnNext {
logger.debug("Updating time! -- $it")
val entries2 = listOf(
time2 as TickerEntry<Node>,
TickerEntry<Node>("time", createText(
val entries = listOf(
//TODO: I have to put way too much crap into that to make this work, something isn't right
time as TickerEntry<Node>,
TickerEntry<Node>("blorp", createText("Third!")),
TickerEntry<Node>("blerp", createText("this is the second")),
time as TickerEntry<Node>,
TickerEntry<Node>("test", createText(sampleText)),
TickerEntry<Node>("more", createText("Some more words")),
//time as TickerEntry<Node>,
//TickerEntry<Node>("test", createText(sampleText)),
//TickerEntry<Node>("more", createText("Some more words")),
TickerEntry<Node>("another", createText("This is happening")),
TickerEntry<Node>("time", createText(
.concatMap { tickerEntry ->
......@@ -114,6 +130,13 @@ class TickerView : View() {
.delay(30, TimeUnit.SECONDS)
.doOnNext {
logger.debug("Adding older things")
......@@ -122,7 +145,7 @@ class MarqueeView : View() {
private val logger = KotlinLogging.logger {}
private val OFFSET = 5.0 //Amount of space between entries!
// val tickEntries = PublishProcessor.create<TickerEntry<Node>>()
// val tickEntries = PublishProcessor.create<TickerEntry<Node>>()
private val activeTicks = ConcurrentLinkedQueue<TickerEntry<Node>>() //This might not need to be threadsafe, only one thing is adding/removing it
private val queuedTicks = ConcurrentLinkedQueue<TickerEntry<Node>>() //This one does, multiple threads!
......@@ -149,49 +172,12 @@ class MarqueeView : View() {
root.clip = rectangle
// //TODO: I don't have a good way of cleaning up the executor service. Need to figure that out
// //Create a backpressure based flowable that only processes one item at a time.
// Flowable.create<TickerEntry<Node>>({ emitter ->
// tickEntries
// .observeOn(Schedulers.computation())
// .doOnNext {
// logger.debug("About to emit!")
// emitter.onNext(it) //NOTE: This blocks a thread
// logger.debug("Emitted incoming ticker change!")
// }
// .subscribe()
// //There isn't really a cancellation thing...
// },
// BackpressureStrategy.BUFFER) //Auto buffer of 128
// .observeOn(Schedulers.from(singleThread))
// //.onBackpressureBuffer(1024)
// .doOnNext {
// logger.debug("Backpressure driven work for $it")
// }
// .map {
// //start an animation, but don't release another until it's past a point
// //Except cannot.... or consume a blocking, but that's gross, probably easier tho....
// //do some kind of callback thing like for steam
// val clearedSubject = PublishProcessor.create<ClearedTick>()
// Pair(it, clearedSubject)
// }
// .doOnNextFx {
// //This will fire off the request to animate the entry, and not move forward until the next thing passes. I think
// animateText(it.first, it.second)
// }
// .map {
// //NOTE: thread gets blocked here, not sure if it matters.
// it.second.blockingFirst()
// }
// .subscribe()
startAnimation() //Fire up the animation process
//TODO: need to have a way of monitoring the queue and active to pause/start the whole thing again
//Maybe that doesn't matter? Will have to monitor CPU usage
fun enqueueTickEntry (entry: TickerEntry<Node>) {
fun enqueueTickEntry(entry: TickerEntry<Node>) {
......@@ -203,19 +189,21 @@ class MarqueeView : View() {
val timeline = Timeline() //TODO: need to find a way to pause/restart the timeline when it's empty
fun lastOneCleared() : Boolean {
fun lastOneCleared(): Boolean {
//Determine if the last entry in the activeQueue has cleared
val last = activeTicks.last()
return last.content.layoutBounds.width + last.content.layoutX + OFFSET <= root.width
val subscriptions = HashMap<TickerEntry<Node>, Disposable>()
//I want any rendering delays to slow the whole thing, not just the individual item. Also then it's less threads
val updateFrame = KeyFrame(Duration.millis(35.0), EventHandler<ActionEvent> {
//This appears to move too fast!
if(activeTicks.isEmpty() || lastOneCleared()) {
if (activeTicks.isEmpty() || lastOneCleared()) {
val newTickerEntry: TickerEntry<Node>? = queuedTicks.poll()
if(newTickerEntry != null) {
if (newTickerEntry != null) {
//Can take one from the queued thing, and plop it in here to start rendering
//TODO: all the pre-setup here
//Then just put it into the active queue, so it will start processing like normal
......@@ -242,13 +230,26 @@ class MarqueeView : View() {
//Now I ned to figure out how to remove it
entry.content.removeFromParent() //Is this legit?
activeTicks.remove(entry) //no longer here, shouldn't ruin the loop
if (subscriptions.containsKey(entry)) {
subscriptions[entry]!!.dispose() //This should cancel it
logger.debug("disposing observable for ${entry.title}")
} else {
content.layoutX = content.layoutX - 1 //TODO: I should probably do animation in larger chunks perhaps, so that it can use less CPU?
//TODO: this might be where I need to make other animation happen
if (entry.update != null) {
//TODO: I think I cannot update an entry while it's being animated? I'm not sure
//At the least, i don't have to pause it, lets see if I can thread that
if (entry.updateObservable != null && !subscriptions.containsKey(entry)) {
//Start up the observable that updates the UI
logger.debug("Starting observable for ${entry.title}")
val disposable = entry.updateObservable.invoke(entry.content).subscribe()
subscriptions.put(entry, disposable)
//Need a thing to track these with
......@@ -265,61 +266,4 @@ class MarqueeView : View() {
//Will need to fire one of these for each thing, with some space detection, based on when one is ending.
private fun animateText(entry: TickerEntry<Node>, publishSubject: PublishProcessor<ClearedTick>) {
val content = entry.content
val emptyStart = root.prefWidth //It's weird, like a race condition
logger.debug("NEW LAYOUT START X: ${emptyStart}")
content.layoutX = emptyStart //I think this is right
root.add(content) //stick it in there, and start animating it
val cleared = AtomicBoolean(false)
val timeline = Timeline()
//TODO: need to establish a single timeline, not a timeline per element
//I want any rendering delays to slow the whole thing, not just the individual item. Also then it's less threads
val updateFrame = KeyFrame(Duration.millis(35.0), EventHandler<ActionEvent> {
//I don't intend to bounce this
val textWidth = content.layoutBounds.width
val paneWidth = root.width
val layoutX = content.layoutX
//instead I want to move completely from the right all the way off until it's gone
//end state of this text is the entire width of the text off the right so layoutX <= 0 - textWidth - offset
if (textWidth + layoutX + OFFSET <= paneWidth && !cleared.get()) {
//There's enough room to start another, how can I notify this?
publishSubject.onComplete() //taht should make it repeat
if (layoutX <= 0 - textWidth - (2 * OFFSET)) {
//Done animating, it's finished
} else {
content.layoutX = content.layoutX - 1 //TODO: I should probably do animation in larger chunks perhaps, so that it can use less CPU?
//TODO: this might be where I need to make other animation happen
if (entry.update != null) {
//Will have to figure out how to inject changes in here
timeline.cycleCount = Animation.INDEFINITE //Because we're going to manually tell it to stop
timeline.onFinished = EventHandler<ActionEvent> {
//This never gets triggered, even when timeline.stop() is called.
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment