Skip to content

Commit

Permalink
Add component for display of progress of batch processing tasks #33
Browse files Browse the repository at this point in the history
  • Loading branch information
jpsacha committed Jul 12, 2024
1 parent 9f0bd01 commit 7ae35f3
Show file tree
Hide file tree
Showing 7 changed files with 510 additions and 1 deletion.
7 changes: 6 additions & 1 deletion ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
-----
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?xml version="1.0" encoding="UTF-8"?>

<!--
~ 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.
-->

<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.control.ProgressBar?>
<?import javafx.scene.layout.BorderPane?>
<?import javafx.scene.layout.ColumnConstraints?>
<?import javafx.scene.layout.GridPane?>
<?import javafx.scene.layout.RowConstraints?>

<BorderPane xmlns="http://javafx.com/javafx/18" xmlns:fx="http://javafx.com/fxml/1" fx:controller="org.scalafx.extras.progress_dialog.impl.ProgressStatusController">
<center>
<GridPane hgap="7.0" prefWidth="500.0" vgap="3.0" BorderPane.alignment="CENTER">
<columnConstraints>
<ColumnConstraints hgrow="NEVER" />
<ColumnConstraints hgrow="ALWAYS" />
<ColumnConstraints hgrow="ALWAYS" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="50.0" />
<ColumnConstraints hgrow="ALWAYS" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="50.0" />
<ColumnConstraints hgrow="SOMETIMES" />
<ColumnConstraints hgrow="ALWAYS" minWidth="10.0" prefWidth="50.0" />
</columnConstraints>
<rowConstraints>
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" prefHeight="30.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
<RowConstraints minHeight="10.0" vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
<RowConstraints vgrow="SOMETIMES" />
</rowConstraints>
<children>
<Label fx:id="statusLabel" maxWidth="1.7976931348623157E308" minWidth="400.0" GridPane.columnSpan="2147483647" />
<ProgressBar fx:id="progressBar" maxWidth="1.7976931348623157E308" prefWidth="200.0" progress="0.0" GridPane.columnSpan="2147483647" GridPane.rowIndex="1" />
<Label text="Elapsed" GridPane.halignment="RIGHT" GridPane.rowIndex="2" />
<Label text="ETA" GridPane.halignment="RIGHT" GridPane.rowIndex="3" />
<Label fx:id="elapsedTimeLabel" text="0:00:00" GridPane.columnIndex="1" GridPane.rowIndex="2" />
<Label fx:id="etaTimeLabel" text="0:00:00" GridPane.columnIndex="1" GridPane.rowIndex="3" />
<Label text="h:mm:ss" GridPane.columnIndex="2" GridPane.rowIndex="2" />
<Label text="h:mm:ss" GridPane.columnIndex="2" GridPane.rowIndex="3" />
<Label text="Total" GridPane.columnIndex="4" GridPane.halignment="RIGHT" GridPane.rowIndex="2" />
<Label text="Processed" GridPane.columnIndex="4" GridPane.halignment="RIGHT" GridPane.rowIndex="3" />
<Label text="Successful" GridPane.columnIndex="4" GridPane.halignment="RIGHT" GridPane.rowIndex="4" />
<Label text="Failed" GridPane.columnIndex="4" GridPane.halignment="RIGHT" GridPane.rowIndex="5" />
<Label text="Cancelled" GridPane.columnIndex="4" GridPane.halignment="RIGHT" GridPane.rowIndex="6" />
<Label fx:id="totalCountLabel" text="?" GridPane.columnIndex="5" GridPane.halignment="RIGHT" GridPane.rowIndex="2" />
<Label fx:id="processedCountLabel" text="?" GridPane.columnIndex="5" GridPane.halignment="RIGHT" GridPane.rowIndex="3" />
<Label fx:id="successfulCountLabel" text="?" GridPane.columnIndex="5" GridPane.halignment="RIGHT" GridPane.rowIndex="4" />
<Label fx:id="failedCountLabel" text="?" GridPane.columnIndex="5" GridPane.halignment="RIGHT" GridPane.rowIndex="5" />
<Label fx:id="cancelledCountLabel" text="?" GridPane.columnIndex="5" GridPane.halignment="RIGHT" GridPane.rowIndex="6" />
<Button fx:id="failedListButton" mnemonicParsing="false" text="..." GridPane.columnIndex="6" GridPane.rowIndex="5" />
</children>
</GridPane>
</center>
</BorderPane>
Original file line number Diff line number Diff line change
@@ -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()
}
}
Loading

0 comments on commit 7ae35f3

Please sign in to comment.