From 7ae35f3205c6370d54d6b081b9e4bad87022015f Mon Sep 17 00:00:00 2001 From: Jarek Sacha Date: Thu, 11 Jul 2024 19:27:10 -0400 Subject: [PATCH] Add component for display of progress of batch processing tasks #33 --- ReadMe.md | 7 +- .../ProgressStatusDemoApp.scala | 82 ++++++++ .../progress_dialog/impl/ProgressStatus.fxml | 83 ++++++++ .../ProgressStatusDialog.scala | 184 ++++++++++++++++++ .../progress_dialog/impl/ProgressStatus.scala | 37 ++++ .../impl/ProgressStatusController.scala | 73 +++++++ .../impl/ProgressStatusModel.scala | 45 +++++ 7 files changed, 510 insertions(+), 1 deletion(-) create mode 100644 scalafx-extras-demos/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDemoApp.scala create mode 100644 scalafx-extras/src/main/resources/org/scalafx/extras/progress_dialog/impl/ProgressStatus.fxml create mode 100644 scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDialog.scala create mode 100644 scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatus.scala create mode 100644 scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusController.scala create mode 100644 scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusModel.scala diff --git a/ReadMe.md b/ReadMe.md index 9c110e0..d0f5684 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -24,6 +24,7 @@ Extras do not have direct corresponding concepts in JavaFX. * [Example 2](#example-2) * [Simpler Use of FXML with MVCfx Pattern](#simpler-use-of-fxml-with-mvcfx-pattern) * [ImageDisplay Component](#imagedisplay-component) + * [Batch Processing and Progress Dialog](#batch-processing-and-progress-dialog) * [Demos](#demos) * [StopWatch Application](#stopwatch-application) * [ShowMessage Demo](#showmessage-demo) @@ -328,9 +329,13 @@ The demos module has a complete example of a simple application: [StopWatchApp][ ### ImageDisplay Component -ImageDisplay Component is an image view with ability to zoom in, zoom out, zoom to fit. It can also automatically resize +ImageDisplay Component is an image view with the ability to zoom in, zoom out, zoom to fit. It can also automatically resize to parent size. +### Batch Processing and Progress Dialog + +Work in progress, see `ProgressStatusDemoApp` for example of use + Demos ----- diff --git a/scalafx-extras-demos/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDemoApp.scala b/scalafx-extras-demos/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDemoApp.scala new file mode 100644 index 0000000..bf10532 --- /dev/null +++ b/scalafx-extras-demos/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDemoApp.scala @@ -0,0 +1,82 @@ +/* + * Copyright (c) 2011-2024, ScalaFX Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the ScalaFX Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE SCALAFX PROJECT OR ITS CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.scalafx.extras.progress_dialog + +import org.scalafx.extras +import org.scalafx.extras.{initFX, offFX, onFX, onFXAndWait} +import scalafx.application.Platform + +/** + * Example showing use of ProgressStatusDialog + */ +object ProgressStatusDemoApp { + + // TODO implement simulated processing using batch processing backend + + def main(args: Array[String]): Unit = { + + initFX() + Platform.implicitExit = true + + val progressStatusDialog = onFXAndWait { + new ProgressStatusDialog("Processing sample tasks", None) + } + + progressStatusDialog.abortFlag.onChange { (_, _, newValue) => + if newValue then { + // Do not block UI, but wait till shutdown completed + offFX { + // Simulate delay due to shut down + Thread.sleep(3000) + onFX { + progressStatusDialog.close() + } + } + } + } + + onFXAndWait { + progressStatusDialog.show() + } + val n = 500 + for i <- 1 to n if !progressStatusDialog.abortFlag.value do { + onFX { + progressStatusDialog.statusText.value = s"Processing item $i / $n" + progressStatusDialog.progress.value = i / n.toDouble + } + Thread.sleep(250) + } + + // In case of abort leave to abort handler o close the dialog when shutdown actions are complete + if !progressStatusDialog.abortFlag.value then + onFX { + progressStatusDialog.close() + } + } + +} diff --git a/scalafx-extras/src/main/resources/org/scalafx/extras/progress_dialog/impl/ProgressStatus.fxml b/scalafx-extras/src/main/resources/org/scalafx/extras/progress_dialog/impl/ProgressStatus.fxml new file mode 100644 index 0000000..d4e42f9 --- /dev/null +++ b/scalafx-extras/src/main/resources/org/scalafx/extras/progress_dialog/impl/ProgressStatus.fxml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + + + +
+
diff --git a/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDialog.scala b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDialog.scala new file mode 100644 index 0000000..d1dcde9 --- /dev/null +++ b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/ProgressStatusDialog.scala @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2011-2024, ScalaFX Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the ScalaFX Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE SCALAFX PROJECT OR ITS CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.scalafx.extras.progress_dialog + +import javafx.concurrent as jfxc +import org.scalafx.extras +import org.scalafx.extras.* +import org.scalafx.extras.progress_dialog.impl.ProgressStatus +import scalafx.Includes.* +import scalafx.beans.property.* +import scalafx.geometry.{Insets, Pos} +import scalafx.scene.Scene +import scalafx.scene.control.Button +import scalafx.scene.layout.BorderPane +import scalafx.stage.{Stage, Window} + +import java.time.Duration + +class ProgressStatusDialog(dialogTitle: String, parentWindow: Option[Window]) { + + private val elapsedTimeService = new ElapsedTimeService() + private val progressStatus = new ProgressStatus() + + progressStatus.model.statusText.value = + "------------------------------------------------------------------------------" + + val abortFlag: BooleanProperty = BooleanProperty(false) + + def updateETA(): Unit = { + val strVal = { + val progress = progressStatus.model.progress.value + if progress <= 0 then { + "?" + } else { + // TODO: prevent jittering of estimate when progress value changes, + // compute running average of last predictions or something... + val et = elapsedTimeService.elapsedTime.value + val eta: Long = (et * (1 - progress) / progress).ceil.toLong + formatDuration(Duration.ofMillis(eta)) + } + } + progressStatus.model.etaTimeText.value = strVal + } + + elapsedTimeService.elapsedTime.onChange { (_, _, newValue) => + progressStatus.model.elapsedTimeText.value = formatDuration(Duration.ofMillis(newValue.longValue())) + updateETA() + } + + private val abortButton = new Button { + text = "Abort batch processing" + padding = Insets(7) + margin = Insets(7) + onAction = _ => + extras.offFXAndWait { + // TODO show abort status + onFX { + progressStatus.model.statusText.value = "Aborting processing" + } + onFX { + abortFlag.value = true + } + } + disable <== abortFlag + alignmentInParent = Pos.Center + } + + private val dialog: Stage = new Stage { + initOwner(parentWindow.orNull) + parentWindow.foreach { w => + w.delegate match { + case s: javafx.stage.Stage => + icons ++= s.icons + case x => + throw new Exception(s"Invalid parent window delegate: $x") + } + } + title = dialogTitle + resizable = false + scene = new Scene { + root = new BorderPane { + padding = Insets(14) + center = progressStatus.view + bottom = abortButton + } + parentWindow.foreach(w => stylesheets = w.scene().stylesheets) + } + + onShown = _ => { + // TODO: prevent double initialization + elapsedTimeService.doStart() + } + onCloseRequest = e => { + abortFlag.value = true + // Do not allow to close the window + e.consume() + } + } + + private def formatDuration(duration: Duration): String = { + val seconds = duration.getSeconds + val absSeconds = Math.abs(seconds) + val positive = String.format("%d:%02d:%02d", absSeconds / 3600, (absSeconds % 3600) / 60, absSeconds % 60) + if seconds < 0 then "-" + positive + else positive + } + + private class ElapsedTimeService extends jfxc.ScheduledService[Long] { + + private var startTime: Long = _ + + private val _elapsedTime = new ReadOnlyLongWrapper() + + /** Elapsed time in milliseconds */ + val elapsedTime: ReadOnlyLongProperty = _elapsedTime.readOnlyProperty + + this.period = 250.ms + + override def createTask(): jfxc.Task[Long] = () => { + val ct = System.currentTimeMillis() + val et = ct - startTime + onFX { _elapsedTime.value = et } + et + } + + def doStart(): Unit = { + this.restart() + startTime = System.currentTimeMillis() + onFX { + _elapsedTime.value = 0 + } + } + } + + def window: Window = dialog + + def progress: DoubleProperty = progressStatus.model.progress + + def statusText: StringProperty = progressStatus.model.statusText + + def totalCount: StringProperty = progressStatus.model.totalCountText + + def processedCount: StringProperty = progressStatus.model.processedCountText + + def successfulCount: StringProperty = progressStatus.model.successfulCountText + + def failedCount: StringProperty = progressStatus.model.failedCountText + + def cancelledCount: StringProperty = progressStatus.model.cancelledCountText + + def close(): Unit = { + elapsedTimeService.cancel() + dialog.close() + } + + def show(): Unit = { + dialog.show() + } +} diff --git a/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatus.scala b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatus.scala new file mode 100644 index 0000000..b8807be --- /dev/null +++ b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatus.scala @@ -0,0 +1,37 @@ +/* + * Copyright (c) 2011-2024, ScalaFX Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the ScalaFX Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE SCALAFX PROJECT OR ITS CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.scalafx.extras.progress_dialog.impl + +import org.scalafx.extras.mvcfx.MVCfx + +class ProgressStatus(val model: ProgressStatusModel = new ProgressStatusModel()) + extends MVCfx[ProgressStatusController]( + "/org/scalafx/extras/progress_dialog/impl/ProgressStatus.fxml" + ): + + override protected def controllerInstance: ProgressStatusController = new ProgressStatusController(model) diff --git a/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusController.scala b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusController.scala new file mode 100644 index 0000000..f10f7a1 --- /dev/null +++ b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusController.scala @@ -0,0 +1,73 @@ +/* + * Copyright (c) 2011-2024, ScalaFX Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the ScalaFX Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE SCALAFX PROJECT OR ITS CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.scalafx.extras.progress_dialog.impl + +import javafx.fxml as jfxf +import javafx.scene.control as jfxsc +import org.scalafx.extras.mvcfx.ControllerFX +import scalafx.Includes.* + +class ProgressStatusController(model: ProgressStatusModel) extends ControllerFX: + + @jfxf.FXML + private var statusLabel: jfxsc.Label = _ + @jfxf.FXML + private var progressBar: jfxsc.ProgressBar = _ + @jfxf.FXML + private var elapsedTimeLabel: jfxsc.Label = _ + @jfxf.FXML + private var etaTimeLabel: jfxsc.Label = _ + + @jfxf.FXML + private var totalCountLabel: jfxsc.Label = _ + @jfxf.FXML + private var processedCountLabel: jfxsc.Label = _ + @jfxf.FXML + private var successfulCountLabel: jfxsc.Label = _ + @jfxf.FXML + private var failedCountLabel: jfxsc.Label = _ + @jfxf.FXML + private var cancelledCountLabel: jfxsc.Label = _ + + @jfxf.FXML + private var failedListButton: jfxsc.Button = _ + + override def initialize(): Unit = + model.statusText <==> statusLabel.text + model.progress <==> progressBar.progress + model.elapsedTimeText <==> elapsedTimeLabel.text + model.etaTimeText <==> etaTimeLabel.text + + model.totalCountText <==> totalCountLabel.text + model.processedCountText <==> processedCountLabel.text + model.successfulCountText <==> successfulCountLabel.text + model.failedCountText <==> failedCountLabel.text + model.cancelledCountText <==> cancelledCountLabel.text + + failedListButton.disable = true + failedListButton.visible = false diff --git a/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusModel.scala b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusModel.scala new file mode 100644 index 0000000..1d0cc52 --- /dev/null +++ b/scalafx-extras/src/main/scala-3/org/scalafx/extras/progress_dialog/impl/ProgressStatusModel.scala @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2011-2024, ScalaFX Project + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * Neither the name of the ScalaFX Project nor the + * names of its contributors may be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE SCALAFX PROJECT OR ITS CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED + * AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.scalafx.extras.progress_dialog.impl + +import org.scalafx.extras.mvcfx.ModelFX +import scalafx.beans.property.{DoubleProperty, StringProperty} + +class ProgressStatusModel extends ModelFX { + + val statusText = new StringProperty() + val progress = new DoubleProperty() + val elapsedTimeText = new StringProperty() + val etaTimeText = new StringProperty() + + val totalCountText = new StringProperty() + val processedCountText = new StringProperty() + val successfulCountText = new StringProperty() + val failedCountText = new StringProperty() + val cancelledCountText = new StringProperty() +}