diff --git a/.gitignore b/.gitignore index 85bf76e..38c8a83 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,4 @@ project/target .externalToolBuilders/ /_local_/ /.bsp/ +.DS_Store diff --git a/.scalafmt.conf b/.scalafmt.conf index 3c2c1ae..d813225 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.2.1 +version = 3.5.8 runner.dialect = scala3 fileOverride { diff --git a/ReadMe.md b/ReadMe.md index c042a56..678cce7 100644 --- a/ReadMe.md +++ b/ReadMe.md @@ -13,16 +13,17 @@ Extras do not have direct corresponding concepts in JavaFX. 0. [Project Structure](#project-structure) 0. [SBT](#sbt) 0. [Features](#features) - 1. [Helper Methods](#helper-methods) - 1. [Simpler Display of Dialogs](#simpler-display-of-dialogs) - 1. [BusyWorker](#busyworker) - 1. [Simpler Use of FXML with MVCfx Pattern](#simpler-use-of-fxml-with-mvcfx-pattern) - 1. [Image Display Component](#imagedisplay-component) + 1. [Helper Methods](#helper-methods) + 1. [Simpler Display of Standard Dialogs](#simpler-display-of-standard-dialogs) + 1. [Easy Custom Dialogs](#easy-custom-dialogs) + 1. [BusyWorker](#busyworker) + 1. [Simpler Use of FXML with MVCfx Pattern](#simpler-use-of-fxml-with-mvcfx-pattern) + 1. [Image Display Component](#imagedisplay-component) 0. [Demos](#demos) - 1. [StopWatch Application](#stopwatch-application) - 1. [ShowMessage Demo](#showmessage-demo) - 1. [BusyWorker Demo](#busyworker-demo) - 1. [ImageDisplay Demo](#imagedisplay-demo) + 1. [StopWatch Application](#stopwatch-application) + 1. [ShowMessage Demo](#showmessage-demo) + 1. [BusyWorker Demo](#busyworker-demo) + 1. [ImageDisplay Demo](#imagedisplay-demo) 0. [Status](#status) 0. [Discussion and Support](#discussion-and-support) 0. [License](#license) @@ -57,7 +58,6 @@ The main helper methods: * `onFXAndWait` run code on FX Application thread and wait till finished * `offFX` run code a thread in parallel * `offFXAndWait` run code a thread and wait till finished -* `showException` show an exception dialog Example scheduling some code on FX Application thread @@ -73,16 +73,39 @@ Example execution some code on a separate thread and waiting for the result of c ```scala val x = offFXAndWait { - val a = 3 - val b = 7 - a * b + val a = 3 + val b = 7 + a * b } ``` -### Simpler Display of Dialogs +### Simpler Display of Standard Dialogs -The mixin `ShowMessage` makes it easier to display dialogs. It is typically used with a UI `Model`. The dialogs can be +Standard dialogs can be quickly displayed using functions provided my `ShowMessage`. For instance, + +```scala +import org.scalafx.extras.ShowMessage + +ShowMessage.information( + "Dialog Title", + "This is the information 'header'", + "This is the information detailed 'content'.", + parentWindow +) +``` + +Dialog types supported: + +* `confirmation` +* `confirmationYesNoCancel` +* `error` +* `exception` +* `information` +* `warning` + +`ShowMessage` can be also used as a mixin to be used within a class where there is the same `parentWindow`. +It is typically used with a UI `Model`. The dialogs can be displayed using a single method, like `showInformation`, `showConfirmation`. `ShowMessage` takes care of blocking parent windows and using parent icons in dialogs. It can also log warnings, errors, and exceptions when warnings, errors, and exceptions dialogs are displayed. @@ -90,20 +113,66 @@ exceptions dialogs are displayed. ```scala class MyUIModel extends Model with ShowMessage { - def onSomeUserAction(): Unit = { - // ... - showInformation("Dialog Title", - "This is the information \"header\"", - "This is the information detailed \"content\".") - // ... - } + def onSomeUserAction(): Unit = { + // ... + showInformation("Dialog Title", + "This is the information 'header'", + "This is the information detailed 'content'.") + // ... + } - // ... + // ... } ``` The demos module has a complete example of a simple application in `ShowMessageDemoApp`. +### Easy Custom Dialogs + +Custom dialogs can be quickly created using `GenericDialogFX` class. This class is particularly suited for creation of +input dialogs. + +There are 3 steps to using the `GenericDialogFX`: + +1. Creation, where elements of the dialog are appended vertically using `add*(...)` methods, for + instance,`addStringField(label, defaultText)` +2. User interaction, dialog is displayed using `showDialog()` method +3. Reading of input, once the dialog is closed, dialog content can be read using `next*()` methods. Content is read in + the order it is added. + +Here is en example: + +```scala +// Create a dialog +val dialog = + new GenericDialogFX( + title = "GenericDialogFX Demo", + header = "Fancy description can go here." + ) { + // Add fields + addCheckbox("Check me out!", defaultValue = false) + addCheckbox("Check me too!", defaultValue = true) + } + +// Show dialog to the user +dialog.showDialog() + +// Read input provided by the user +if (dialog.wasOKed) { + val select1 = dialog.nextBoolean() + val select2 = dialog.nextBoolean() + + println(s"Selection 1: $select1") + println(s"Selection 2: $select2") +} else { + println("Dialog was cancelled.") +} +``` + +![GenericDialogFX Demo](notes/assets/GenericDialogFX_2.png) + +A more elaborate example is in the `GenericDialogFXDemo`. + ### BusyWorker BusyWorker helps running a UI task on separate threads (other than the JavaFX Application thread). It will show busy @@ -129,7 +198,7 @@ new BusyWorker("Simple Task", parentWindow).doTask { () => Here is a little more elaborated example. It updates a progress message and progress indicator. ```scala - val buttonPane: Pane = +val buttonPane: Pane = ... val progressLabel: Label = ... diff --git a/build.sbt b/build.sbt index b8b8002..de7faa1 100644 --- a/build.sbt +++ b/build.sbt @@ -10,11 +10,11 @@ import scala.xml.{Node => XmlNode, NodeSeq => XmlNodeSeq, _} // JAR_BUILT_BY - Name to be added to Jar metadata field "Built-By" (defaults to System.getProperty("user.name") // -val projectVersion = "0.5.0" +val projectVersion = "0.5.0.2-SNAPSHOT" val versionTagDir = if (projectVersion.endsWith("SNAPSHOT")) "master" else "v." + projectVersion -val _scalaVersions = Seq("3.0.2", "2.13.7", "2.12.15") +val _scalaVersions = Seq("3.0.2", "2.13.8", "2.12.16") val _scalaVersion = _scalaVersions.head -val _javaFXVersion = "17.0.1" +val _javaFXVersion = "18.0.1" ThisBuild / version := projectVersion ThisBuild / crossScalaVersions := _scalaVersions @@ -24,18 +24,9 @@ ThisBuild / organization := "org.scalafx" publishArtifact := false publish / skip := true -lazy val OSName = System.getProperty("os.name") match { - case n if n.startsWith("Linux") => "linux" - case n if n.startsWith("Mac") => "mac" - case n if n.startsWith("Windows") => "win" - case _ => throw new Exception("Unknown platform!") -} - -lazy val JavaFXModuleNames = Seq("base", "controls", "fxml", "graphics", "media", "swing", "web") -lazy val JavaFXModuleLibsProvided: Seq[ModuleID] = - JavaFXModuleNames.map(m => "org.openjfx" % s"javafx-$m" % _javaFXVersion % "provided" classifier OSName) lazy val JavaFXModuleLibs: Seq[ModuleID] = - JavaFXModuleNames.map(m => "org.openjfx" % s"javafx-$m" % _javaFXVersion classifier OSName) + Seq("base", "controls", "fxml", "graphics", "media", "swing", "web") + .map(m => "org.openjfx" % s"javafx-$m" % _javaFXVersion) def isScala2(scalaVersion: String): Boolean = { CrossVersion.partialVersion(scalaVersion) match { @@ -93,14 +84,14 @@ lazy val scalaFXExtrasDemos = (project in file("scalafx-extras-demos")).settings publishArtifact := false, libraryDependencies ++= Seq( "com.typesafe.scala-logging" %% "scala-logging" % "3.9.4", - "ch.qos.logback" % "logback-classic" % "1.2.9" + "ch.qos.logback" % "logback-classic" % "1.2.11" ) ).dependsOn(scalaFXExtras % "compile;test->test") // Resolvers // Add snapshots to root project to enable compilation with Scala SNAPSHOT compiler, // e.g., 2.11.0-SNAPSHOT -resolvers += Resolver.sonatypeRepo("snapshots") +resolvers ++= Resolver.sonatypeOssRepos("snapshots") // Common settings lazy val scalaFXExtrasSettings = Seq( @@ -113,7 +104,8 @@ lazy val scalaFXExtrasSettings = Seq( "-deprecation", "-encoding", "utf8", - "-feature" + "-feature", + "-release", "8" ) ++ ( if (isScala2(scalaVersion.value)) @@ -161,15 +153,10 @@ lazy val scalaFXExtrasSettings = Seq( else Seq.empty[sbt.ModuleID] ), - javacOptions ++= Seq( - "-target", "1.8", - "-source", "1.8", - "-Xlint:deprecation" - ), libraryDependencies ++= Seq( - "org.scalafx" %% "scalafx" % "17.0.1-R26", - "org.scalatest" %% "scalatest" % "3.2.10" % "test" - ) ++ JavaFXModuleLibsProvided, + "org.scalafx" %% "scalafx" % "18.0.1-R27", + "org.scalatest" %% "scalatest" % "3.2.11" % "test" + ) ++ JavaFXModuleLibs, libraryDependencies ++= ( if (isScala2(scalaVersion.value)) Seq( @@ -200,7 +187,7 @@ lazy val scalaFXExtrasSettings = Seq( run / fork := true, Test / fork := true, Test / parallelExecution := false, - resolvers += Resolver.sonatypeRepo("snapshots"), + resolvers ++= Resolver.sonatypeOssRepos("snapshots"), // print junit-style XML for CI Test / testOptions += { val t = (Test / target).value diff --git a/notes/0.6.0.md b/notes/0.6.0.md new file mode 100644 index 0000000..d87b8d2 --- /dev/null +++ b/notes/0.6.0.md @@ -0,0 +1,50 @@ +### ScalaFX-Extras Release v.0.6.0 + +This release add a new class for convenient creation of input dialogs: `GenericDialogFX`. You can easily add controls to +he dialog then +read their values after the dialog was closed + +```scala +// Create a dialog +val dialog = + new GenericDialogFX( + title = "GenericDialogFX Demo", + header = "Fancy description can go here." + ) { + // Add fields + addCheckbox("Check me out!", defaultValue = false) + addCheckbox("Check me too!", defaultValue = true) + } + +// Show dialog to the user +dialog.showDialog() + +// Read input provided by the user +if (dialog.wasOKed) { + val select1 = dialog.nextBoolean() + val select2 = dialog.nextBoolean() + + println(s"Selection 1: $select1") + println(s"Selection 2: $select2") +} else { + println("Dialog was cancelled.") +} +``` + +The `scalafx-extras-demos` subproject has a more elaborated example. + +Enhancements: + +* Support creation of custom dialogs, like ImageJ's GenericDialog #16 +* Let any standard dialog be displayed with a one-liner #17 + +To post questions please use [Project Discussions][Discussions] or [ScalaFX Users Group][scalafx-users] + +[Discussions]: https://github.com/scalafx/scalafx-extras/discussions + +[scalafx-users]: https://groups.google.com/forum/#!forum/scalafx-users + +[Issue #16]: https://github.com/scalafx/scalafx-extras/issues/16 + +[Issue #17]: https://github.com/scalafx/scalafx-extras/issues/17 + diff --git a/notes/assets/GenericDialogFX_2.png b/notes/assets/GenericDialogFX_2.png new file mode 100644 index 0000000..1027741 Binary files /dev/null and b/notes/assets/GenericDialogFX_2.png differ diff --git a/project/build.properties b/project/build.properties index 2ba6e58..82434e8 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1,5 +1,5 @@ # -# Copyright (c) 2011-2021, ScalaFX Project +# Copyright (c) 2011-2022, ScalaFX Project # All rights reserved. # # Redistribution and use in source and binary forms, with or without @@ -24,5 +24,5 @@ # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # -sbt.version=1.6.0-RC1 +sbt.version=1.7.1 diff --git a/project/sbt-sonatype.sbt b/project/sbt-sonatype.sbt index 18eb216..96c26fd 100644 --- a/project/sbt-sonatype.sbt +++ b/project/sbt-sonatype.sbt @@ -1,3 +1,3 @@ // [https://github.com/xerial/sbt-sonatype] -addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.10") +addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.13") addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.1.1") \ No newline at end of file diff --git a/scalafx-extras-demos/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFXDemo.scala b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFXDemo.scala new file mode 100644 index 0000000..0ee3d5a --- /dev/null +++ b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFXDemo.scala @@ -0,0 +1,134 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import scalafx.Includes.* +import scalafx.application.JFXApp3 +import scalafx.geometry.{Insets, Pos} +import scalafx.scene.Scene +import scalafx.scene.control.{Button, ColorPicker} +import scalafx.scene.image.Image +import scalafx.scene.layout.{BorderPane, VBox} +import scalafx.scene.text.{Font, FontWeight} +import scalafx.stage.Window + +object GenericDialogFXDemo extends JFXApp3 { + + + def openGenericDialog(parentWindow: Option[Window]): Unit = { + + // We will use a custom control that is nt provided by GenericDialogFX + val myColorPicker = new ColorPicker() + + val dialog = + new GenericDialogFX( + title = "GenericDialogFX Demo", + header = "An attempt to emulate ImageJ's GenericDialog.", + parentWindow = parentWindow + ) { + addCheckbox("Check me out!", defaultValue = false) + addChoice("Make a choice", Array("A", "B", "C", "D"), "B") + addDirectoryField("Input images", "images") + addDirectoryField("Input masks", "masks", 33) + addFileField("Configuration file") + addStringField("Enter some text", "?", 24) + addMessage("My bold message", Font.font(Font.default.family, FontWeight.Bold, Font.default.size * 1.25)) + // Here will add a custom control that we are handling on our own + addNode("My custom control", myColorPicker) + addNumericField("What's your number", 23.17) + + addHelp("https://github.com/scalafx/scalafx-extras") + } + + dialog.showDialog() + + val (status, result) = if (dialog.wasOKed) { + val select1 = dialog.nextBoolean() + val choice2 = dialog.nextChoice() + val directory3 = dialog.nextString() + val directory4 = dialog.nextString() + val file5 = dialog.nextString() + val text6 = dialog.nextString() + // Read our custom control + val color7 = myColorPicker.value() + val number8 = dialog.nextNumber() + + ( + "GenericDialogFX was OKed.", + s"""Values of inputs: + | Selection 1 : $select1 + | Choice 2 : $choice2 + | Directory 3 : $directory3 + | Directory 4 : $directory4 + | File 5 : $file5 + | Text 6 : $text6 + | Color 7 : $color7 + | Number 8 : $number8 + |""".stripMargin + ) + + } else { + ( + "GenericDialogFX was cancelled", + "" + ) + } + + + // ShowMessage.information("GenericDialogFX Result", status, result, parentWindow) + + new GenericDialogFX("GenericDialogFX Result", status, parentWindow) { + addMessage(result, Font.font("Monospaced", Font.default.size)) + }.showDialog() + } + + + override def start(): Unit = { + + val button = new Button { + text = "Open GenericDialogFX" + onAction = () => openGenericDialog(Option(stage)) + maxWidth = Double.MaxValue + } + + stage = new JFXApp3.PrimaryStage { + icons += new Image("/org/scalafx/extras/sfx.png") + title = "StopWatch" + scene = new Scene { + root = new BorderPane { + center = new VBox { + spacing = 9 + alignment = Pos.Center + padding = Insets(50) + children ++= Seq(button) + } + } + } + } + } +} diff --git a/scalafx-extras-demos/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFXDemo2.scala b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFXDemo2.scala new file mode 100644 index 0000000..4f61b76 --- /dev/null +++ b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFXDemo2.scala @@ -0,0 +1,69 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import org.scalafx.extras.initFX + +object GenericDialogFXDemo2 { + + def main(args: Array[String]): Unit = { + + // Initialize JavaFX, so we can display the dialog + initFX() + + + // Create dialog + val dialog = + new GenericDialogFX( + title = "GenericDialogFX Demo", + header = "Fancy description can go here." + ) { + // Add fields + addCheckbox("Check me out!", defaultValue = false) + addCheckbox("Check me too!", defaultValue = true) + } + + // Show dialog to the user + dialog.showDialog() + + // Read input provided by the user + if (dialog.wasOKed) { + val select1 = dialog.nextBoolean() + val select2 = dialog.nextBoolean() + + println(s"Selection 1: $select1") + println(s"Selection 2: $select2") + } else { + println("Dialog was cancelled.") + } + + + // Use of initFX() requires explicit application exit + System.exit(0) + } +} diff --git a/scalafx-extras-demos/src/main/scala/org/scalafx/extras/image/ImageDisplayDemoApp.scala b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/image/ImageDisplayDemoApp.scala index 5ca420c..d249b57 100644 --- a/scalafx-extras-demos/src/main/scala/org/scalafx/extras/image/ImageDisplayDemoApp.scala +++ b/scalafx-extras-demos/src/main/scala/org/scalafx/extras/image/ImageDisplayDemoApp.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021, ScalaFX Project + * Copyright (c) 2011-2022, ScalaFX Project * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -28,7 +28,7 @@ package org.scalafx.extras.image import javafx.beans.binding as jfxbb -import org.scalafx.extras.showException +import org.scalafx.extras.ShowMessage import scalafx.Includes.* import scalafx.application.JFXApp3 import scalafx.application.JFXApp3.PrimaryStage @@ -104,21 +104,21 @@ object ImageDisplayDemoApp extends JFXApp3 { imageDisplay.image() = image } else { image.exception().printStackTrace() - showException( + ShowMessage.exception( title = "Open image...", message = "Failed to load image from file:\n" + file.getCanonicalPath, t = image.exception(), - ownerWindow = stage + parentWindow = stage ) } } catch { case ex: IllegalArgumentException => ex.printStackTrace() - showException( + ShowMessage.exception( title = "Open image...", message = "Failed to load image from file:\n" + file.getCanonicalPath, t = ex, - ownerWindow = stage + parentWindow = stage ) } diff --git a/scalafx-extras/src/main/scala-2/org/scalafx/extras/mvcfx/MVCfx.scala b/scalafx-extras/src/main/scala-2/org/scalafx/extras/mvcfx/MVCfx.scala index 823ecf8..85943da 100644 --- a/scalafx-extras/src/main/scala-2/org/scalafx/extras/mvcfx/MVCfx.scala +++ b/scalafx-extras/src/main/scala-2/org/scalafx/extras/mvcfx/MVCfx.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021, ScalaFX Project + * Copyright (c) 2011-2022, ScalaFX Project * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -93,7 +93,7 @@ abstract class MVCfx(fxmlFilePath: String) { override def failed(): Unit = { val message = s"Error while initializing view for '$title'." - showException(title, message, exceptionProperty.get(), Option(stage)) + ShowMessage.exception(title, message, exceptionProperty.get(), Option(stage)) } } ) diff --git a/scalafx-extras/src/main/scala-3/org/scalafx/extras/mvcfx/MVCfx.scala b/scalafx-extras/src/main/scala-3/org/scalafx/extras/mvcfx/MVCfx.scala index 15964be..a40f96f 100644 --- a/scalafx-extras/src/main/scala-3/org/scalafx/extras/mvcfx/MVCfx.scala +++ b/scalafx-extras/src/main/scala-3/org/scalafx/extras/mvcfx/MVCfx.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021, ScalaFX Project + * Copyright (c) 2011-2022, ScalaFX Project * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -107,7 +107,7 @@ abstract class MVCfx[T <: ControllerFX](fxmlFilePath: String)(implicit tag: Clas override def failed(): Unit = val message = s"Error while initializing view for '$title'." - showException(title, message, exceptionProperty.get(), Option(stage)) + ShowMessage.exception(title, message, exceptionProperty.get(), Option(stage)) ) stage diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/ShowMessage.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/ShowMessage.scala index 28aefb3..40b8a8d 100644 --- a/scalafx-extras/src/main/scala/org/scalafx/extras/ShowMessage.scala +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/ShowMessage.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021, ScalaFX Project + * Copyright (c) 2011-2022, ScalaFX Project * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -27,18 +27,172 @@ package org.scalafx.extras +import scalafx.Includes.* +import scalafx.scene.Node import scalafx.scene.control.Alert.AlertType -import scalafx.scene.control.{Alert, ButtonType} +import scalafx.scene.control.{Alert, ButtonType, Label, TextArea} +import scalafx.scene.layout.{GridPane, Priority} import scalafx.stage.Window +import java.io.{PrintWriter, StringWriter} + +object ShowMessage { + + private def showMessage(parentWindow: Option[Window]): ShowMessage = { + val pw = parentWindow + new ShowMessage { + override def parentWindow: Option[Window] = pw + } + } + + /** + * Show error dialog + * + * @param title dialog title + * @param header header text. + * @param content main content text. + * @param parentWindow owner window that will be blacked by the dialog. + */ + def error(title: String, header: String, content: String = "", parentWindow: Option[Window] = None): Unit = + showMessage(parentWindow).showError(title, header, content) + + /** + * Show a modal dialog with an expandable details about an exception (stack trace). + * + * @param title dialog title + * @param message message shown in the dialog header. + * @param t exception. + * @param ownerNode owner window that will be blacked by the dialog. Can be `null`. + */ + def exception(title: String, message: String, t: Throwable, ownerNode: Node): Unit = { + // Find parent window for the node + val parentWindow = + Option(ownerNode) + .flatMap(n => + Option(n.scene()) + .map(s => jfxWindow2sfx(s.window())) + ) + exception(title, message, t, parentWindow) + } + + /** + * Show a modal dialog with an expandable details about an exception (stack trace). + * + * @param title dialog title + * @param message message shown in the dialog header. + * @param t exception. + * @param parentWindow owner window that will be blacked by the dialog. Can be `null` to match JavaFX convention. + */ + def exception(title: String, message: String, t: Throwable, parentWindow: Window): Unit = + exception(title, message, t, Option(parentWindow)) + + /** + * Show a modal dialog with an expandable details about an exception (stack trace). + * + * @param title dialog title + * @param message message shown in the dialog header. + * @param t exception. + * @param parentWindow owner window that will be blacked by the dialog. + */ + def exception(title: String, message: String, t: Throwable, parentWindow: Option[Window] = None): Unit = { + t.printStackTrace() + + // Rename to avoid name clashes + val dialogTitle = title + + // Create expandable Exception. + val exceptionText = { + val sw = new StringWriter() + val pw = new PrintWriter(sw) + t.printStackTrace(pw) + sw.toString + } + val label = new Label("The exception stack trace was:") + val textArea = new TextArea { + text = exceptionText + editable = false + wrapText = true + maxWidth = Double.MaxValue + maxHeight = Double.MaxValue + vgrow = Priority.Always + hgrow = Priority.Always + } + val expContent = new GridPane { + maxWidth = Double.MaxValue + add(label, 0, 0) + add(textArea, 0, 1) + } + + onFXAndWait { + new Alert(AlertType.Error) { + initOwner(parentWindow.orNull) + this.title = dialogTitle + headerText = message + contentText = Option(t.getMessage).getOrElse("") + // Set expandable Exception into the dialog pane. + dialogPane().expandableContent = expContent + }.showAndWait() + } + } + + /** + * Show information dialog + * + * @param title dialog title + * @param header header text. + * @param content main content text. + * @param parentWindow owner window that will be blacked by the dialog. + */ + def information(title: String, header: String, content: String = "", parentWindow: Option[Window] = None): Unit = + showMessage(parentWindow).showInformation(title, header, content) + + /** + * Show warning dialog + * + * @param title dialog title + * @param header header text. + * @param content main content text. + * @param parentWindow owner window that will be blacked by the dialog. + */ + def warning(title: String, header: String, content: String, parentWindow: Option[Window] = None): Unit = + showMessage(parentWindow).showWarning(title, header, content) + + /** + * Show a confirmation dialog with "OK" and "Cancel" buttons. + * + * @param title dialog title. + * @param header header text. + * @param content content text. + * @param parentWindow owner window that will be blacked by the dialog. + * @return `true` when user selected 'OK' and `false` when user selected `Cancel` or dismissed the dialog. + */ + def confirmation(title: String, header: String, content: String = "", parentWindow: Option[Window] = None): Boolean = + showMessage(parentWindow).showConfirmation(title, header, content) + + /** + * Show a confirmation dialog with "OK", "No", and "Cancel" buttons. + * + * @param title dialog title. + * @param header header text. + * @param content content text. + * @param parentWindow owner window that will be blacked by the dialog. + * @return `Some(true)` when user selected 'OK', `Some(false)` when user selected `No`, + * and `None` user selected `Cancel` or dismissed the dialog. + */ + def confirmationYesNoCancel(title: String, header: String, content: String = "", + parentWindow: Option[Window] = None): Option[Boolean] = + showMessage(parentWindow).showConfirmationYesNoCancel(title, header, content) + +} + /** - * Mixin that adds ability to easily show message dialogs. - * A messageLogger can be provided, so when the error or warning dialogs are shown, they are also logged. - * - * A ShowMessage mixin will typically be used with the [[org.scalafx.extras.mvcfx.ModelFX ModelFX]]. - * - * @author Jarek Sacha - */ + * Mixin that adds ability to easily show message dialogs. + * A messageLogger can be provided, so when the error or warning dialogs are shown, they are also logged. + * + * A ShowMessage mixin will typically be used with the [[org.scalafx.extras.mvcfx.ModelFX ModelFX]]. + * + * @author Jarek Sacha + */ trait ShowMessage { /** @@ -82,7 +236,7 @@ trait ShowMessage { */ def showException(title: String, message: String, t: Throwable): Unit = { messageLogger.foreach(_.error(s"<$title> $message", t)) - org.scalafx.extras.showException(title, message, t, parentWindow) + ShowMessage.exception(title, message, t, parentWindow) } /** diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/DefaultLastDirectoryHandler.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/DefaultLastDirectoryHandler.scala new file mode 100644 index 0000000..ae5c924 --- /dev/null +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/DefaultLastDirectoryHandler.scala @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import java.io.File +import java.util.concurrent.atomic.AtomicReference + +/** + * Default implementation of the `LastDirectoryHandler`. + */ +class DefaultLastDirectoryHandler extends LastDirectoryHandler { + private val _lastDirectory: AtomicReference[File] = new AtomicReference[File](new File(".")) + + def lastDirectory: File = _lastDirectory.get() + + def lastDirectory_=(newDir: File): Unit = { + require(newDir != null, "Argument `newDir` cannot be `null`.") + _lastDirectory.set(newDir) + } +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/DirectorySelectionField.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/DirectorySelectionField.scala new file mode 100644 index 0000000..d35f0de --- /dev/null +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/DirectorySelectionField.scala @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import scalafx.application.Platform +import scalafx.beans.property.StringProperty +import scalafx.scene.Node +import scalafx.scene.control.{Button, TextField} +import scalafx.scene.layout.{HBox, Priority} +import scalafx.stage.{DirectoryChooser, Window} + +import java.io.File +import scala.annotation.tailrec + +object DirectorySelectionField { + + /** + * Find existing part of the input file path. If the input file exists return that file otherwise look for first + * existing parent + */ + @tailrec + def existingOrParent(file: File): File = + if (file.exists()) file + else existingOrParent(file.getCanonicalFile.getParentFile) +} + +/** + * Directory selection control, accessible through `view`. The text field shows the path, the button allow browsing to + * select the directory. + */ +class DirectorySelectionField(val title: String, + val ownerWindow: Option[Window], + val lastDirectoryHandler: LastDirectoryHandler = new DefaultLastDirectoryHandler()) { + + import DirectorySelectionField.* + + private lazy val chooser: DirectoryChooser = new DirectoryChooser() { + this.title = DirectorySelectionField.this.title + } + + private var _view: Option[Node] = None + val path: StringProperty = new StringProperty("") + + def view: Node = { + if (_view.isEmpty) { + _view = Option(buildView()) + } + _view.get + } + + private def buildView(): Node = { + + val textField = new TextField() { + hgrow = Priority.Always + maxWidth = Double.MaxValue + text <==> path + } + + // Make sure that end of the file name is visible + textField.text.onChange { (_, _, _) => + val location = textField.text.length.get() + Platform.runLater { + textField.positionCaret(location) + } + } + + val button = new Button("Browse") { + onAction = _ => { + val initialPath = path.value + if (initialPath.trim.nonEmpty) { + chooser.initialDirectory = existingOrParent(new File(initialPath)) + } else { + chooser.initialDirectory = lastDirectoryHandler.lastDirectory + } + + val selection = chooser.showDialog(ownerWindow.orNull) + + Option(selection).foreach { s => + path.value = s.getCanonicalPath + lastDirectoryHandler.lastDirectory = s + } + } + } + + new HBox(3, textField, button) + } + +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/FileSelectionField.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/FileSelectionField.scala new file mode 100644 index 0000000..8d1e138 --- /dev/null +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/FileSelectionField.scala @@ -0,0 +1,102 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import scalafx.application.Platform +import scalafx.beans.property.StringProperty +import scalafx.scene.Node +import scalafx.scene.control.{Button, TextField} +import scalafx.scene.layout.{HBox, Priority} +import scalafx.stage.{FileChooser, Window} + +import java.io.File + +/** + * File selection control, accessible through `view`. The text field shows the path, the button allow browsing to + * select the File. + */ +class FileSelectionField(val title: String, + val ownerWindow: Option[Window], + val lastDirectoryHandler: LastDirectoryHandler = new DefaultLastDirectoryHandler()) { + private lazy val fileChooser: FileChooser = new FileChooser() { + this.title = FileSelectionField.this.title + } + val path: StringProperty = new StringProperty("") + private var _view: Option[Node] = None + + def view: Node = { + if (_view.isEmpty) { + _view = Option(buildView()) + } + _view.get + } + + private def buildView(): Node = { + + val textField = new TextField() { + hgrow = Priority.Always + maxWidth = Double.MaxValue + text <==> path + } + + // Make sure that end of the file name is visible + textField.text.onChange { (_, _, _) => + val location = textField.text.length.get() + Platform.runLater { + textField.positionCaret(location) + } + } + + val button = new Button("Browse") { + onAction = _ => { + val initialPath = path.value + if (initialPath.trim.nonEmpty) { + val file = new File(initialPath) + fileChooser.initialFileName = file.getName + if (file.getParentFile.exists()) { + fileChooser.initialDirectory = file.getParentFile + } + } else { + val parent = lastDirectoryHandler.lastDirectory + if (parent.exists()) + fileChooser.initialDirectory = parent + } + + val selection = fileChooser.showOpenDialog(ownerWindow.orNull) + + Option(selection).foreach { s => + path.value = s.getCanonicalPath + lastDirectoryHandler.lastDirectory = s + } + } + } + + new HBox(3, textField, button) + } + +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFX.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFX.scala new file mode 100644 index 0000000..e14abbe --- /dev/null +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/GenericDialogFX.scala @@ -0,0 +1,515 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import org.scalafx.extras.onFXAndWait +import scalafx.Includes.* +import scalafx.application.Platform +import scalafx.beans.property.StringProperty +import scalafx.collections.ObservableBuffer +import scalafx.geometry.Insets +import scalafx.scene.Node +import scalafx.scene.control.* +import scalafx.scene.control.ButtonBar.ButtonData +import scalafx.scene.layout.{ColumnConstraints, GridPane, Priority} +import scalafx.scene.text.Font +import scalafx.stage.Window + +import java.net.URL +import scala.collection.mutable.ListBuffer + +object GenericDialogFX { + + /** + * @param buttonPressed + * button used to close the dialog + */ + private case class Result(buttonPressed: Option[ButtonType]) +} + +/** + * A helper for crating custom dialogs. Particularly suited for creation of input dialogs. + * + * There are 3 steps to using a dialog: + * 1. Creation, where elements of the dialog are appended vertically using `add*(...)` methods, + * for instance, `addStringField(label, defaultText)` + * 2. User interaction, dialog is displayed using `showDialog()` method + * 3. Reading of input, once the dialog is closed, dialog content can be read using `next*()` methods. + * Content is read in the order it is added. + * + * Here is en example: + * + * {{{ + * val dialog = + * new GenericDialogFX( + * title = "GenericDialogFX Demo", + * "Fancy description can go here." + * ) { + * addCheckbox("Check me out!", defaultValue = false) + * addCheckbox("Check me too!", defaultValue = true) + * } + * + * dialog.showDialog() + * + * if (dialog.wasOKed) { + * val select1 = dialog.nextBoolean() + * val select2 = dialog.nextBoolean() + * + * println(s"Selection 1: $select1") + * println(s"Selection 2: $select2") + * } + * }}} + * + * @param title dialogs title + * @param header dialog header + * @param parentWindow optional parent window that will be blocked when this dialog is displayed. + * @param lastDirectoryHandler customize how directory selections are remembered between uses of the dialog. Used with `addDirectoryField` and `addFileField`. + */ +class GenericDialogFX(val title: String, + val header: String = "", + val parentWindow: Option[Window] = None, + val lastDirectoryHandler: LastDirectoryHandler = new DefaultLastDirectoryHandler()) { + require(title != null, "Argument 'title' cannot be 'null'") + require(header != null, "Argument 'header' cannot be 'null'") + require(parentWindow != null, "Argument 'parentWindow' cannot be 'null'") + require(lastDirectoryHandler != null, "Argument 'lastDirectoryHandler' cannot be 'null'") + + import GenericDialogFX.* + + lazy private val _helpLabel: String = "Help" + private val ButtonTypeHelp = new ButtonType(helpLabel, ButtonData.Help) + private val _labeledControls = ListBuffer.empty[(String, Node)] + private val _checkBoxes = ListBuffer.empty[CheckBox] + private val _choiceBoxes = ListBuffer.empty[ChoiceBox[String]] + private val _numberTextFields = ListBuffer.empty[NumberTextField] + private val _stringProperties = ListBuffer.empty[StringProperty] + private val _grid: GridPane = new GridPane() { + hgap = 5 + vgap = 5 + padding = Insets(10, 10, 10, 10) + maxWidth = Double.MaxValue + hgrow = Priority.Always + private val constrains = Seq( + new ColumnConstraints(), + new ColumnConstraints(), + new ColumnConstraints(), + new ColumnConstraints() { + hgrow = Priority.Always + } + ) + columnConstraints.addAll(constrains.map(_.delegate) *) + } + private var _wasOKed = false + private var _rowIndex = 0 + private var _checkBoxNextIndex = 0 + private var _choiceBoxNextIndex = 0 + private var _numberTextFieldNextIndex = 0 + private var _stringPropertyNextIndex = 0 + private var _helpURLOption: Option[String] = None + + /** + * Adds a checkbox. + * + * @param label the label + * @param defaultValue the initial state + */ + def addCheckbox(label: String, defaultValue: Boolean): Unit = { + val label2 = label.replace('_', ' ') + + val checkBox = new CheckBox() + checkBox.selected = defaultValue + + _grid.add(new Label(label2), 0, _rowIndex) + _grid.add(checkBox, 1, _rowIndex) + _rowIndex += 1 + + _labeledControls.append((label, checkBox)) + _checkBoxes += checkBox + } + + + def addChoice(label: String, items: Array[String], defaultItem: String): Unit = { + + require(items.contains(defaultItem)) + + val label2 = label.replace('_', ' ') + + val choiceBox = new ChoiceBox[String](ObservableBuffer.from(items)) + choiceBox.selectionModel.value.select(defaultItem) + + _grid.add(new Label(label2), 0, _rowIndex) + _grid.add(choiceBox, 1, _rowIndex) + _rowIndex += 1 + + _labeledControls.append((label, choiceBox)) + _choiceBoxes += choiceBox + + } + + /** + * Adds a choice list. + * + * @param label the label + * @param items items on the list + * @param defaultItem the initial item, must be equal to one of the `items` + */ + def addChoice(label: String, items: Seq[String], defaultItem: String): Unit = + addChoice(label, items.toArray, defaultItem) + + + /** + * Adds a directory text field and "Browse" button, where the field width is determined by the length of + * 'defaultPath', with a minimum of 25 columns. Use nextString to retrieve the directory path. + * + * @param label the label + * @param defaultPath initial path + */ + def addDirectoryField(label: String, defaultPath: String): Unit = { + val columns = + if (defaultPath != null) Math.max(defaultPath.length, 25) + else 25 + addDirectoryField(label, defaultPath, columns) + } + + def addDirectoryField(label: String, defaultPath: String, columns: Int): Unit = { + val label2 = label.replace('_', ' ') + + val directorySelectionField = new DirectorySelectionField(label2, parentWindow, lastDirectoryHandler) + directorySelectionField.path.value = defaultPath + + _grid.add(new Label(label2), 0, _rowIndex) + _grid.add(directorySelectionField.view, 1, _rowIndex, GridPane.Remaining, 1) + _rowIndex += 1 + + _labeledControls.append((label, directorySelectionField.view)) + _stringProperties += directorySelectionField.path + } + + /** + * Adds a file text field and "Browse" button, where the field width is determined by the length of + * 'defaultPath', with a minimum of 25 columns. Use nextString to retrieve the file path. + * + * @param label the label + * @param defaultPath initial path + */ + def addFileField(label: String, defaultPath: String = ""): Unit = { + val label2 = label.replace('_', ' ') + + val fileSelectionField = new FileSelectionField(label2, parentWindow, lastDirectoryHandler) + fileSelectionField.path.value = defaultPath + + _grid.add(new Label(label2), 0, _rowIndex) + _grid.add(fileSelectionField.view, 1, _rowIndex, GridPane.Remaining, 1) + _rowIndex += 1 + + _labeledControls.append((label, fileSelectionField.view)) + _stringProperties += fileSelectionField.path + } + + /** + * Adds a "Help" button that opens the specified URL in the default browser. + * + * @param url the URL to open in the default browser + */ + def addHelp(url: String): Unit = { + _helpURLOption = Option(url) + } + + /** + * Adds a message consisting of one or more lines of text. + * + * That message cannot be edited, cannot be edited with a `next*()` + * + * @param message message + * @param font font used to render the message + */ + def addMessage(message: String, font: Font): Unit = { + addMessage(message, Option(font)) + } + + /** + * Adds a message consisting of one or more lines of text. + * + * That message cannot be edited, cannot be edited with a `next*()` + * + * @param message message + * @param font Optional font used to render the message + */ + + def addMessage(message: String, font: Option[Font] = None): Unit = { + val label = Label(message) + font.foreach(label.font = _) + + _grid.add(label, 0, _rowIndex, GridPane.Remaining, 1) + _rowIndex += 1 + } + + /** Add a custom node that will occupy a whole row */ + def addNode(node: Node): Unit = { + _grid.add(node, 0, _rowIndex, GridPane.Remaining, 1) + _rowIndex += 1 + } + + /** Add a custom node + * + * @param label the label + * @param node custom node. It is not included in `next*()` readouts, needs to be handled by the creator + */ + def addNode(label: String, node: Node): Unit = { + val label2 = label.replace('_', ' ') + + _grid.add(new Label(label2), 0, _rowIndex) + _grid.add(node, 1, _rowIndex, GridPane.Remaining, 1) + _rowIndex += 1 + } + + /** + * Add a numeric field. + * + * @param label the label + * @param defaultValue the initial value + */ + def addNumericField(label: String, defaultValue: Double): Unit = { + val decimalPlaces = if (defaultValue.toInt == defaultValue) 0 else 3 + val columnWidth = if (decimalPlaces == 3) 8 else 6 + addNumericField(label, defaultValue, decimalPlaces, columnWidth, "") + } + + /** + * Add a numeric field. + * + * @param label the label + * @param defaultValue the initial value + * @param decimalPlaces number of decimal places to display + * @param columnWidth number of columns used to display the number + * @param units text displayed after the number + */ + def addNumericField(label: String, + defaultValue: Double, + decimalPlaces: Int, + columnWidth: Int, + units: String): Unit = { + require(columnWidth > 0) + + val label2 = label.replace('_', ' ') + val textField = new NumberTextField(decimalPlaces) { + prefColumnCount = columnWidth + model.value = defaultValue + } + + _grid.add(new Label(label2), 0, _rowIndex) + _grid.add(textField, 1, _rowIndex) + _grid.add(new Label(units), 2, _rowIndex) + _rowIndex += 1 + + _labeledControls.append((label, textField)) + _numberTextFields += textField + } + + + /** + * Adds an 8 column text field. + * + * @param label the label + * @param defaultText the text initially displayed + */ + def addStringField(label: String, defaultText: String): Unit = { + addStringField(label, defaultText, 8) + } + + /** + * Adds a text field. + * + * @param label the label + * @param defaultText text initially displayed + * @param columns width of the text field + */ + def addStringField(label: String, defaultText: String, columns: Int): Unit = { + val label2 = label.replace('_', ' ') + val textField = new TextField() { + prefColumnCount = columns + text.value = defaultText + } + + _grid.add(new Label(label2), 0, _rowIndex) + _grid.add(textField, 1, _rowIndex) + _rowIndex += 1 + + _labeledControls.append((label, textField)) + _stringProperties += textField.text + } + + + /** + * Returns the state of the next checkbox + */ + def nextBoolean(): Boolean = { + require(_checkBoxNextIndex < _checkBoxes.size) + + val next = _checkBoxes(_checkBoxNextIndex).selected.value + _checkBoxNextIndex += 1 + + next + } + + // noinspection AccessorLikeMethodIsEmptyParen + def nextChoice(): String = { + require(_choiceBoxNextIndex < _choiceBoxes.size) + + val next = _choiceBoxes(_choiceBoxNextIndex).selectionModel.value.selectedItem.value + _choiceBoxNextIndex += 1 + + next + } + + /** + * Returns the value of the next number + */ + def nextNumber(): Double = { + require(_numberTextFieldNextIndex < _numberTextFields.size) + + val next = _numberTextFields(_numberTextFieldNextIndex).model.value.value + _numberTextFieldNextIndex += 1 + + next.doubleValue() + } + + /** + * Returns the value of the next string + */ + def nextString(): String = { + require(_stringPropertyNextIndex < _stringProperties.size) + + val next = _stringProperties(_stringPropertyNextIndex).value + _stringPropertyNextIndex += 1 + + next + } + + /** + * Display the dialog and block till the dialog is closed + */ + def showDialog(): Unit = { + + onFXAndWait { + + // Create the custom dialog. + val dialog = new Dialog[Result]() { + parentWindow.foreach(initOwner) + this.title = GenericDialogFX.this.title + if (header.nonEmpty) { + headerText = header + } + resizable = true + } + + dialog.dialogPane().buttonTypes = + if (_helpURLOption.isDefined) + Seq(ButtonType.OK, ButtonType.Cancel, ButtonTypeHelp) + else + Seq(ButtonType.OK, ButtonType.Cancel) + + // Place to add validation to enable OK button + // // Enable/Disable OK button depending on whether data is validated + // val okButton = dialog.dialogPane().lookupButton(ButtonType.OK) + // okButton.disable = true + + // // Do some validation (disable when username is empty). + // username.text.onChange { (_, _, newValue) => + // okButton.disable = newValue.trim().isEmpty + // } + + dialog.dialogPane().content = _grid + + // Request focus on the first label by default + _labeledControls.headOption.foreach(l => Platform.runLater(l._2.requestFocus())) + + // Pressing any of the "official" dialog pane buttons will come with close request. + // If a help button was pressed we do not want to close this dialog, but we want to display Help dialog + dialog.onCloseRequest = e => { + e.getSource match { + case d: javafx.scene.control.Dialog[Result] => + if (d.getResult.buttonPressed.forall(_ == ButtonTypeHelp)) { + // Show help + showHelp() + // Cancel closing request + e.consume() + } + case _ => + } + } + + // When an "official" button is clicked, convert the result containing that button. + // We use it to detect when Help button is pressed + dialog.resultConverter = dialogButton => Result(Option(dialogButton)) + + // We could use some more digested result + val result = dialog.showAndWait() + + _wasOKed = result.contains(Result(Some(ButtonType.OK))) + } + } + + private def showHelp(): Unit = { + _helpURLOption.foreach { helpURL => + if (helpURL.startsWith("")) { + // val title1 = title + " " + helpLabel + // if (this.isInstanceOf[NonBlockingGenericDialog]) new HTMLDialog(title, helpURL, false) // non blocking + // else new HTMLDialog(this, title, helpURL) //modal + ??? + } else { + // val `macro` = "call('ij.plugin.BrowserLauncher.open', '" + helpURL + "');" + // new MacroRunner(`macro`) // open on separate thread using BrowserLauncher + val url = new URL(helpURL) + Utils.openWebpage(url) + // ij.plugin.BrowserLauncher.open(helpURL) + } + } + } + + /** + * `true` if the dialog was closed by cancelling + */ + def wasCanceled: Boolean = !_wasOKed + + + /** + * `true` if the dialog was closed using OK button + */ + def wasOKed: Boolean = _wasOKed + + /** + * Label used for the help button + */ + def helpLabel: String = _helpLabel + + // def helpLabel_=(label: String): Unit = { + // require(label != null, "Argument 'label' cannot be null.") + // _helpLabel = label + // } +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/LastDirectoryHandler.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/LastDirectoryHandler.scala new file mode 100644 index 0000000..de430c4 --- /dev/null +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/LastDirectoryHandler.scala @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import java.io.File + +/** + * Customize how directory selections are remembered between uses of the dialog. + */ +trait LastDirectoryHandler { + + /** Returns the directory that contains the last file opened or saved, or default directory. */ + def lastDirectory: File + + /** Sets the directory containing the last file opened by the user. */ + def lastDirectory_=(newDir: File): Unit +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/NumberTextField.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/NumberTextField.scala new file mode 100644 index 0000000..873a59c --- /dev/null +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/NumberTextField.scala @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import scalafx.scene.control.{TextField, TextFormatter} +import scalafx.util.converter.FormatStringConverter + +import java.text.DecimalFormat + +class NumberTextField(decimalPlaces: Int = 6) extends TextField { + private val format = { + val pattern = + if (decimalPlaces > 0) { + "0." + ("0" * decimalPlaces) + } else + "0" + + new DecimalFormat(pattern) + } + private val converter = new FormatStringConverter[Number](format) + + val model = new TextFormatter(converter) + + textFormatter = model +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/Utils.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/Utils.scala new file mode 100644 index 0000000..adc8368 --- /dev/null +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/generic_dialog/Utils.scala @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2011-2022, 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.generic_dialog + +import java.awt.Desktop +import java.net.{URI, URL} + +object Utils { + + + /** + * Opens URL using the default browser. + * + * @param url the URL to be displayed in the user default browser + * @see Utils.openWebpage(uri: URI) for exceptions thrown + */ + def openWebpage(url: URL): Unit = openWebpage(url.toURI) + + /** + * Opens URI using the default browser. + * + * @param uri the URI to be displayed in the user default browser + * @throws IllegalArgumentException if uri is `null` + * @throws HeadlessException if GraphicsEnvironment.isHeadless() returns `true` + * @throws UnsupportedOperationException if this Java Desktop class is not supported on the current platform or + * the current platform does not support the Desktop.Action.BROWSE action + * @throws IOException if the user default browser is not found, or it fails to be launched, + * or the default handler application failed to be launched + * @throws SecurityException if a security manager exists and it denies the + * `AWTPermission("showWindowWithoutWarningBanner")` permission, + * or the calling thread is not allowed to create a subprocess + */ + def openWebpage(uri: URI): Unit = { + + require(uri != null, "Argument 'uri' cannot be null.") + + if (Desktop.isDesktopSupported && Desktop.getDesktop.isSupported(Desktop.Action.BROWSE)) + Desktop.getDesktop.browse(uri) + } +} diff --git a/scalafx-extras/src/main/scala/org/scalafx/extras/package.scala b/scalafx-extras/src/main/scala/org/scalafx/extras/package.scala index 9925e4f..a406247 100644 --- a/scalafx-extras/src/main/scala/org/scalafx/extras/package.scala +++ b/scalafx-extras/src/main/scala/org/scalafx/extras/package.scala @@ -1,5 +1,5 @@ /* - * Copyright (c) 2011-2021, ScalaFX Project + * Copyright (c) 2011-2022, ScalaFX Project * All rights reserved. * * Redistribution and use in source and binary forms, with or without @@ -29,15 +29,10 @@ package org.scalafx import javafx.concurrent as jfxc import javafx.embed.swing.JFXPanel -import scalafx.Includes.* import scalafx.application.Platform import scalafx.scene.Node -import scalafx.scene.control.Alert.AlertType -import scalafx.scene.control.{Alert, Label, TextArea} -import scalafx.scene.layout.{GridPane, Priority} import scalafx.stage.Window -import java.io.{PrintWriter, StringWriter} import java.util.concurrent /** @@ -135,85 +130,47 @@ package object extras { } /** - * Show a modal dialog with an expandable details about an exception (stack trace). - * - * @param title dialog title - * @param message message shown in the dialog header. - * @param t exception. - * @param ownerWindow owner window that will be blacked by the dialog. Can be `null`. - */ - def showException(title: String, message: String, t: Throwable, ownerWindow: Node): Unit = { - val parentWindow = Option(ownerWindow).flatMap(n => Option(n.scene()).map(s => jfxWindow2sfx(s.window()))) - showException(title, message, t, parentWindow) - } + * Show a modal dialog with an expandable details about an exception (stack trace). + * + * @param title dialog title + * @param message message shown in the dialog header. + * @param t exception. + * @param ownerWindow owner window that will be blacked by the dialog. Can be `null`. + */ + @deprecated("Use org.scalafx.extras.ShowMessage.exception()", "0.6.0") + def showException(title: String, message: String, t: Throwable, ownerWindow: Node): Unit = + ShowMessage.exception(title, message, t, ownerWindow) /** - * Show a modal dialog with an expandable details about an exception (stack trace). - * - * @param title dialog title - * @param message message shown in the dialog header. - * @param t exception. - * @param ownerWindow owner window that will be blacked by the dialog. Can be `null` to match JavaFX convention. - */ - def showException(title: String, message: String, t: Throwable, ownerWindow: Window): Unit = { - showException(title, message, t, Option(ownerWindow)) - } + * Show a modal dialog with an expandable details about an exception (stack trace). + * + * @param title dialog title + * @param message message shown in the dialog header. + * @param t exception. + * @param ownerWindow owner window that will be blacked by the dialog. Can be `null` to match JavaFX convention. + */ + @deprecated("Use org.scalafx.extras.ShowMessage.exception()", "0.6.0") + def showException(title: String, message: String, t: Throwable, ownerWindow: Window): Unit = + ShowMessage.exception(title, message, t, ownerWindow) /** - * Show a modal dialog with an expandable details about an exception (stack trace). - * - * @param title dialog title - * @param message message shown in the dialog header. - * @param t exception. - * @param ownerWindow owner window that will be blacked by the dialog. - */ - def showException(title: String, message: String, t: Throwable, ownerWindow: Option[Window] = None): Unit = { - t.printStackTrace() - - // Rename to avoid name clashes - val dialogTitle = title - - // Create expandable Exception. - val exceptionText = { - val sw = new StringWriter() - val pw = new PrintWriter(sw) - t.printStackTrace(pw) - sw.toString - } - val label = new Label("The exception stack trace was:") - val textArea = new TextArea { - text = exceptionText - editable = false - wrapText = true - maxWidth = Double.MaxValue - maxHeight = Double.MaxValue - vgrow = Priority.Always - hgrow = Priority.Always - } - val expContent = new GridPane { - maxWidth = Double.MaxValue - add(label, 0, 0) - add(textArea, 0, 1) - } - - onFXAndWait { - new Alert(AlertType.Error) { - initOwner(ownerWindow.orNull) - this.title = dialogTitle - headerText = message - contentText = Option(t.getMessage).getOrElse("") - // Set expandable Exception into the dialog pane. - dialogPane().expandableContent = expContent - }.showAndWait() - } - } + * Show a modal dialog with an expandable details about an exception (stack trace). + * + * @param title dialog title + * @param message message shown in the dialog header. + * @param t exception. + * @param ownerWindow owner window that will be blacked by the dialog. + */ + @deprecated("Use org.scalafx.extras.ShowMessage.exception()", "0.6.0") + def showException(title: String, message: String, t: Throwable, ownerWindow: Option[Window] = None): Unit = + ShowMessage.exception(title, message, t, ownerWindow) /** - * Run task on a named daemon thread. - * - * @param task to run - * @param name name for the thread to run the operation. Useful for debugging. - */ + * Run task on a named daemon thread. + * + * @param task to run + * @param name name for the thread to run the operation. Useful for debugging. + */ def runTask[T](task: javafx.concurrent.Task[T], name: String): Unit = { val th = new Thread(task, name) th.setDaemon(true)