Skip to content

Commit

Permalink
Respect overrides for SimpleTask onSucceeded, onCancelled, onFailed. …
Browse files Browse the repository at this point in the history
…Remove double error messages with custom error handling #28
  • Loading branch information
jpsacha committed Apr 3, 2024
1 parent 93e40b8 commit 168f506
Show file tree
Hide file tree
Showing 2 changed files with 245 additions and 31 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/*
* 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

import org.scalafx.extras.BusyWorker.SimpleTask
import scalafx.Includes.*
import scalafx.application.JFXApp3
import scalafx.application.JFXApp3.PrimaryStage
import scalafx.concurrent.WorkerStateEvent
import scalafx.geometry.{Insets, Pos}
import scalafx.scene.Scene
import scalafx.scene.control.{Button, Label, ProgressBar, ToolBar}
import scalafx.scene.image.Image
import scalafx.scene.layout.{BorderPane, HBox, Priority, VBox}

import java.util.concurrent.Future
import scala.util.{Failure, Success, Try}

/**
* An application illustrating use of `BusyWorker` and handling of exception in execution of the task.
*/
object BusyWorkerErrorHandlingDemo extends JFXApp3 {

override def start(): Unit = {

val progressLabel = new Label("") {
hgrow = Priority.Always
maxWidth = Double.MaxValue
}
val progressBar = new ProgressBar() {
progress = 0
}

// noinspection ConvertExpressionToSAM
lazy val buttonPane = new VBox { parentNode =>
spacing = 9
alignment = Pos.Center
padding = Insets(21)
children ++= Seq(
new Button("No errors") {
onAction = () =>
busyWorker.doTask("Task 1")(
new SimpleTask[String] {
override def call(): String = {
val maxItems = 10
for (i <- 1 to maxItems) {
println(i)
message() = s"Processing item $i/$maxItems"
progress() = (i - 1) / 10.0
Thread.sleep(250)
}
progress() = 1
"Done"
}

override def onFinish(result: Future[String], successful: Boolean): Unit = {
// Any onFinish after running a task would happen here.
println(s"Task completion was successful: '$successful'")
if (successful) {
println(s"Task produced result: '${result.get()}'")
}
}
}
)
maxWidth = Double.MaxValue
},
new Button("Exception on 3 - default error handling") {
onAction = () =>
busyWorker.doTask("Task 2")(
new SimpleTask[String] {
override def call(): String = {
val maxItems = 10
for (i <- 1 to maxItems) {
println(i)
message() = s"Processing item $i/$maxItems"
progress() = (i - 1) / 10.0
Thread.sleep(250)

if (i == 3) {
throw new Exception("Simulating task failure.")
}
}
progress() = 1
"Done"
}

override def onFinish(result: Future[String], successful: Boolean): Unit = {
// Any onFinish after running a task would happen here.
println(s"Task completion was successful: '$successful'")
if (successful) {
println(s"Task produced result: '${result.get()}'")
}
}
}
)
maxWidth = Double.MaxValue
},
new Button("Exception on 3 - custom error handling") {
onAction = () =>
busyWorker.doTask("Task 3")(
new SimpleTask[String] {
override def call(): String = {
val maxItems = 10
for (i <- 1 to maxItems) {
println(i)
message() = s"Processing item $i/$maxItems"
progress() = (i - 1) / 10.0
Thread.sleep(250)

if (i == 3) {
throw new Exception("Simulating task failure.")
}
}
progress() = 1
"Done"
}

override def onFailed(e: WorkerStateEvent): Unit =
// Mark event as consumed to prevent default error handlig
e.consume()

override def onFinish(result: Future[String], successful: Boolean): Unit = {
Try(result.get()) match {
case Success(value) =>
ShowMessage.information(
"Task Success",
s"Task completion was successful: '$successful'",
s"Task produced result: '$value'",
parent.value
)
case Failure(exception) =>
ShowMessage.exception("Task Failure", "Custom error handling", exception, parent.value)
}
}
}
)
maxWidth = Double.MaxValue
}
).map(_.delegate)
}

lazy val busyWorker: BusyWorker =
new BusyWorker(title = "BusyWorker Error Handling Demo", disabledNode = buttonPane) {
progressLabel.text <== progressMessage
progressBar.progress <== progressValue
}

stage = new PrimaryStage {
scene = new Scene {
icons += new Image("/org/scalafx/extras/sfx.png")
title = "BusyWorker Error Handling Demo"
root = {
new BorderPane {
padding = Insets(7)
top = new ToolBar()
center = buttonPane
bottom = new HBox {
spacing = 3
children ++= Seq(progressLabel, progressBar)
}
}
}
}
}
}
}
85 changes: 54 additions & 31 deletions scalafx-extras/src/main/scala/org/scalafx/extras/BusyWorker.scala
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2011-2021, ScalaFX Project
* Copyright (c) 2011-2024, ScalaFX Project
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
Expand Down Expand Up @@ -32,7 +32,7 @@ import org.scalafx.extras.BusyWorker.SimpleTask
import scalafx.Includes.*
import scalafx.application.Platform
import scalafx.beans.property.*
import scalafx.concurrent.Worker
import scalafx.concurrent.{Worker, WorkerStateEvent}
import scalafx.scene.{Cursor, Node}
import scalafx.stage.Window

Expand Down Expand Up @@ -82,8 +82,16 @@ object BusyWorker {
* Method called whenever the state of the Task has transitioned to the FAILED state.
* This method is invoked on the FX Application Thread after any listeners of the state property and after the
* Task has been fully transitioned to the new state.
*
* Implementation should consume the event to stop execution of automated error handling by
* the `BusyWorker` for this event.
*
* When custom error handling is implemented, it may be more convenient to do it in `onFinish`,
* as more information about the failure can be accessed there using `Try(result.get)`.
*
* @param e event state issued during the failure.
*/
def onFailed(): Unit = {}
def onFailed(e: WorkerStateEvent): Unit = {}

/**
* Message that can be updated while task is executed.
Expand Down Expand Up @@ -385,33 +393,28 @@ class BusyWorker private (
val jfxTask = new javafx.concurrent.Task[R] {
override def call(): R = task.call()

override def scheduled(): Unit = task.onScheduled()

override def running(): Unit = task.onRunning()

override def succeeded(): Unit = task.onSucceeded()

override def cancelled(): Unit = task.onCancelled()

override def failed(): Unit = task.onFailed()
onScheduledProperty.value = () => task.onScheduled()
onRunningProperty.value = () => task.onRunning()
onSucceededProperty.value = () => task.onSucceeded()
onCancelledProperty.value = () => task.onCancelled()
onFailedProperty.value = e => task.onFailed(e)

task.message.onChange((_, _, newValue) => updateMessage(newValue))
task.progress.onChange((_, _, newValue) => updateProgress(newValue.doubleValue(), 1.0))

}
_doTask(jfxTask, task.onFinish, name)
}

/**
* @param task task to run
* @param cleanup operation to perform after task completed (success or failure)
* @param name name of the thread on which to run the task/
* @param task task to run
* @param onFinish operation to perform after task completed (success or failure)
* @param name name of the thread on which to run the task/
* @tparam R type of the task return value
* @return future representing value returned by the task.
*/
private def _doTask[R](
task: javafx.concurrent.Task[R],
cleanup: (Future[R], Boolean) => Unit,
onFinish: (Future[R], Boolean) => Unit,
name: String = title
): Future[R] = {

Expand All @@ -425,7 +428,7 @@ class BusyWorker private (
}
_busyWorkloadName = name

def resetProgress(): Unit = {
def finishUp(): Unit = {
Platform.runLater {
_progressValue.unbind()
_progressMessage.unbind()
Expand All @@ -442,7 +445,7 @@ class BusyWorker private (
}
}

cleanup(task, task.state.value == Worker.State.Succeeded.delegate)
onFinish(task, task.state.value == Worker.State.Succeeded.delegate)
}

// Prepare task for execution
Expand All @@ -456,21 +459,41 @@ class BusyWorker private (
}
}

task.onSucceeded = () => resetProgress()
task.onCancelled = () => resetProgress()
task.onFailed = () => {
task.getException match {
case NonFatal(t) =>
val message =
s"Unexpected error while performing a UI task: '$name'. " // + Option(t.getMessage).getOrElse("")
showException(title, message, t)
case t =>
// Propagate fatal errors
throw t
// Preserve existing handler, if there is one
def appendHandler(
property: ObjectProperty[javafx.event.EventHandler[javafx.concurrent.WorkerStateEvent]],
op: WorkerStateEvent => Unit
): Unit = {
val oldHandler = property.value
property.value = { (e: javafx.concurrent.WorkerStateEvent) =>
Option(oldHandler).foreach(_.handle(e))
op(e)
}
resetProgress()
}

appendHandler(task.onSucceeded, _ => finishUp())
appendHandler(task.onCancelled, _ => finishUp())
appendHandler(
task.onFailed,
event =>
try {
if (!event.isConsumed) {
// Exising handler should consume events to prevent activation of the default error handler.
task.getException match {
case NonFatal(t) =>
val message =
s"Unexpected error while performing a UI task: '$name'. " // + Option(t.getMessage).getOrElse("")
showException(title, message, t)
case t =>
// Propagate fatal errors
throw t
}
}
} finally {
finishUp()
}
)

// Run task on a separate thread
val th = new Thread(task, name)
th.setDaemon(true)
Expand Down

0 comments on commit 168f506

Please sign in to comment.