diff --git a/scalafx-extras-demos/src/main/scala/org/scalafx/extras/BusyWorkerErrorHandlingDemo.scala b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/BusyWorkerErrorHandlingDemo.scala new file mode 100644 index 0000000..a09695c --- /dev/null +++ b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/BusyWorkerErrorHandlingDemo.scala @@ -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) + } + } + } + } + } + } +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/BusyWorker.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/BusyWorker.scala index 8432fce..2be0a63 100644 --- a/scalafx-extras/src/main/scala/org/scalafx/extras/BusyWorker.scala +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/BusyWorker.scala @@ -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 @@ -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 @@ -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. @@ -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] = { @@ -425,7 +428,7 @@ class BusyWorker private ( } _busyWorkloadName = name - def resetProgress(): Unit = { + def finishUp(): Unit = { Platform.runLater { _progressValue.unbind() _progressMessage.unbind() @@ -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 @@ -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)