scalafx custom binding race condition - scala

I'm trying to use Amazon Rekognition to draw boxes around detected text in an image.
Here's a stripped-down JavaFX app that is supposed to do that:
object TextRekognizeGui extends JFXApp {
lazy val rekognition: Option[AmazonRekognition] =
RekogClient.get(config.AwsConfig.rekogEndpoint, config.AwsConfig.region)
private val config = MainConfig.loadConfig()
stage = new PrimaryStage {
scene = new Scene {
var imgFile: Option[File] = None
content = new HBox {
val imgFile: ObjectProperty[Option[File]] = ObjectProperty(None)
val img: ObjectProperty[Option[Image]] = ObjectProperty(None)
val lines: ObjectProperty[Seq[TextDetection]] = ObjectProperty(Seq.empty)
// These two print statements are always executed when we press the appropriate button.
img.onChange((_, _, newValue) => println("New image"))
lines.onChange((_, _, newValue) => println(s"${newValue.length} new text detections"))
children = Seq(
new VBox {
children = Seq(
new OpenButton(img, imgFile, lines, stage),
new RekogButton(img, imgFile, lines, rekognition)
)
},
new ResultPane(675, 425, img, lines))
}
}
}
stage.show
}
class ResultPane(width: Double, height: Double, img: ObjectProperty[Option[Image]],
textDetections: ObjectProperty[Seq[TextDetection]]) extends AnchorPane { private val emptyPane: Node = Rectangle(0, 0, width, height)
// This print statement, and the one below, are often not executed even when their dependencies change.
private val backdrop = Bindings.createObjectBinding[Node](
() => {println("new backdrop"); img.value.map(new MainImageView(_)).getOrElse(emptyPane)},
img
)
private val outlines = Bindings.createObjectBinding[Seq[Node]](
() => {println("new outlines"); textDetections.value map getTextOutline},
textDetections
)
This looks to me as though I followed the ScalaFX documentation's directions for creating a custom binding. When I click my "RekogButton", I expect to see the message " new text detections" along with the message "new outlines", but more often than not I see only the " new text detections" message.
The behavior appears to be the same throughout the lifetime of the program, but it only manifests on some runs. The same problem happens with the img -> background binding, but it manifests less often.
Why do my properties sometimes fail to call the bindings when they change?
edit 1
JavaFX stores the listeners associated with bindings as a weak reference, meaning they can still be garbage-collected unless another object holds a strong or soft reference to them. So it seems my app fails if the garbage collector happens to run between app initialization and the time the outlines are drawn.
This is odd, because I seem to have a strong reference to the binding -- the "delegate" field of the value "outlines" in the result pane. Looking into it more.

Related

Scala Swing skips repaint of the Frame

I am currently working on an implementation of the game Othello in Scala and so far it's working pretty nicely. When implementing the GUI though (using Scala Swing) I've stumbled across an issue that I can't seem to fix.
When playing against the computer opponent the Frame seems to be repainted only when the bot is done making its move.
The game is also playable through the terminal and doing so updates the Frame properly every time regardless of the player configuration (Player vs Player or Player vs Computer). Also playing player vs player using the GUI exclusively presents no issues at all.
It might be an oversight on my behalf, but so far I am unable to find a solution and would greatly appreciate any help.
So far I have tried various combinations of revalidating and repainting the individual panels, adding and removing listeners, changing my implementation of the reactor pattern to the one provided by Scala Swing, adding Thread.sleep to see if there could be a scheduling conflict of sorts.
import java.awt.Color
import othello.controller.Controller
import javax.swing.ImageIcon
import javax.swing.border.LineBorder
import scala.swing.event.MouseClicked
import scala.swing.{BorderPanel, BoxPanel, Dimension, FlowPanel, GridPanel, Label, Orientation}
class TablePanel(controller: Controller) extends FlowPanel {
val sides = 32
val sidesColor: Color = Color.lightGray
val squareSize = 52
def tableSize: Int = controller.board.size
def edgeLength: Int = tableSize * squareSize
def rows: BoxPanel = new BoxPanel(Orientation.Vertical) {
background = sidesColor
preferredSize = new Dimension(sides, edgeLength)
contents += new Label {
preferredSize = new Dimension(sides, sides)
}
contents += new GridPanel(tableSize, 1) {
background = sidesColor
for { i <- 1 to rows } contents += new Label(s"$i")
}
}
def columns: GridPanel = new GridPanel(1, tableSize) {
background = sidesColor
preferredSize = new Dimension(edgeLength, sides)
for { i <- 0 until columns } contents += new Label(s"${(i + 65).toChar}")
}
def table: GridPanel = new GridPanel(tableSize, tableSize) {
background = new Color(10, 90, 10)
for {
col <- 0 until columns
row <- 0 until rows
} contents += square(col, row)
}
def square(row: Int, col: Int): Label = new Label {
border = new LineBorder(new Color(30, 30, 30, 140), 1)
preferredSize = new Dimension(squareSize, squareSize)
icon = controller.board.valueOf(col, row) match {
case -1 => new ImageIcon("resources/big_dot.png")
case 0 => new ImageIcon("resources/empty.png")
case 1 => new ImageIcon("resources/black_shadow.png")
case 2 => new ImageIcon("resources/white_shadow.png")
}
listenTo(mouse.clicks)
reactions += {
case _: MouseClicked =>
if (controller.options.contains((col, row))) controller.set(col, row)
else if (controller.board.gameOver) controller.newGame()
else controller.highlight()
}
}
def redraw(): Unit = {
contents.clear
contents += new BorderPanel {
add(rows, BorderPanel.Position.West)
add(new BoxPanel(Orientation.Vertical) {
contents += columns
contents += table
}, BorderPanel.Position.East)
}
repaint
}
}
import scala.swing._
import othello.controller._
import othello.util.Observer
import scala.swing.event.Key
class SwingGui(controller: Controller) extends Frame with Observer {
controller.add(this)
lazy val tablePanel = new TablePanel(controller)
lazy val mainFrame: MainFrame = new MainFrame {
title = "Othello"
menuBar = menus
contents = tablePanel
centerOnScreen
// peer.setAlwaysOnTop(true)
resizable = false
visible = true
}
def menus: MenuBar = new MenuBar {
contents += new Menu("File") {
mnemonic = Key.F
contents += new MenuItem(Action("New Game") {
controller.newGame()
})
contents += new MenuItem(Action("Quit") {
controller.exit()
})
}
contents += new Menu("Edit") {
mnemonic = Key.E
contents += new MenuItem(Action("Undo") {
controller.undo()
})
contents += new MenuItem(Action("Redo") {
controller.redo()
})
}
contents += new Menu("Options") {
mnemonic = Key.O
contents += new MenuItem(Action("Highlight possible moves") {
controller.highlight()
})
contents += new MenuItem(Action("Reduce board size") {
controller.resizeBoard("-")
})
contents += new MenuItem(Action("Increase board size") {
controller.resizeBoard("+")
})
contents += new MenuItem(Action("Reset board size") {
controller.resizeBoard(".")
})
contents += new Menu("Game mode") {
contents += new MenuItem(Action("Player vs. Computer") {
controller.setupPlayers("1")
})
contents += new MenuItem(Action("Player vs. Player") {
controller.setupPlayers("2")
})
}
}
}
def update: Boolean = {
tablePanel.redraw()
mainFrame.pack
mainFrame.centerOnScreen
mainFrame.repaint
true
}
}
The expected behavior is a repainted Frame on every turn. The actual result is the Frame only being repainted after the opponent made a move. It only happens when playing player vs bot exclusively through clicking the UI.
I don't think the problem is in the code you have shown, but I would bet that you are blocking the "event dispatch thread" (the UI thread) with your AI computation for the computer player.
In a Swing app, there is a special thread called the "event dispatch thread" that is responsible for handling messages from the O/S including dealing with repaint messages. All UI event handlers will be invoked on this thread. If you use that thread to do any computations which take a long time (such as a computer move in a game like this), any UI updates will be blocked until the thread becomes free.
This tutorial has more info: https://docs.oracle.com/javase/tutorial/uiswing/concurrency/index.html
You need to move the AI onto a background thread and release the event dispatch thread to deal with repaints. This might be tricky to implement if you are not familiar with multi-threaded programs! Good luck.
As Rich suggested, the solution was concurrency. Making the search algorithm a Future-type solved the issue for now. It might not be the cleanest implementation as it is the first time I'm using Future, but this is what it looks like now:
def selectAndSet(): Future[_] = Future(if (!board.gameOver && player.isBot) {
new MoveSelector(this).select() match {
case Success(square) => set(square)
case _ => omitPlayer()
}
selectAndSet()
})(ExecutionContext.global)

Scalafx. Start the Alert by Timer

I have a main window with some information. I can't start the Alert by timer. I need to show alert every 10 seconds(with working main window) and by the alert's button change the Label's text in the main window.
I have this code, but it's not work:
object main extends JFXApp {
stage = new JFXApp.PrimaryStage() {
scene = new Scene {
val MyLabel = new Label("SomeText")
root = new VBox {
children = MyLabel
}
}
val ButtonTypeOne = new ButtonType("Change the text")
val ButtonTypeTwo = new ButtonType("No")
val alert1 = new Alert(AlertType.Warning) {
initOwner(stage)
title = "Warning!!!"
headerText = "Header"
contentText = "Do you need to change the text?"
buttonTypes = Seq(ButtonTypeOne, ButtonTypeTwo, ButtonType.Cancel)
}
val result = alert1.showAndWait()
val timerA = new PauseTransition(Duration(5000))
timerA.onFinished = { _ =>
result match {
case Some(ButtonTypeOne) => /*Here I need to change text in MyLabel */
case Some(ButtonTypeTwo) => None
case _ => None
}
timerA.playFromStart()
}
timerA.play
}
}
There are a few different problems in your code:
You display the alert only once, regardless of what's happening with the timer.
The code that reacts to the result of the alert in the timer always looks at the same initial result.
Unfortunately, even if you move the val result = alert1.showAndWait inside of the timer's animation update event, JavaFX makes it illegal to call showAndWait within such a routine.
The solution is to create an onHidden event handler for alert1, which reacts to it being closed. The button type used to close the dialog is then stored in alert1.result, so you can use that to determine what action to take.
I've added a StringProperty to assist with the changing of the label value. The following example should be a good starting point for what you want to achieve...
import scalafx.Includes._
import scalafx.animation.PauseTransition
import scalafx.application.JFXApp
import scalafx.application.JFXApp.PrimaryStage
import scalafx.beans.property.StringProperty
import scalafx.scene.Scene
import scalafx.scene.layout.VBox
import scalafx.scene.control.{Alert, ButtonType, Label}
import scalafx.scene.control.Alert.AlertType
import scalafx.util.Duration
object main extends JFXApp {
// String property for the text in myLabel.
val strProp = StringProperty("Some Text")
// Declare this so that it's accessible from timerA.onFinished.
val myLabel = new Label {
// Bind the text property to the value of strProp.
// When strProp's value changes, so does the label's text.
text <== strProp
}
stage = new PrimaryStage() {
scene = new Scene {
root = new VBox {
children = myLabel
}
}
}
// Custom buttons.
val ButtonTypeOne = new ButtonType("Change the text")
val ButtonTypeTwo = new ButtonType("No")
// Create the timer. This goes off after 5,000ms (5 seconds) - after play is called.
val timerA = new PauseTransition(Duration(5000))
// Alert dialog.
// Note: JavaFX forbids use of showAndWait within animation processing, so we must use
// an onHidden event instead.
val alert1 = new Alert(AlertType.Warning) {
initOwner(stage)
title = "Warning!!!"
headerText = "Header"
contentText = "Do you need to change the text?"
buttonTypes = Seq(ButtonTypeOne, ButtonTypeTwo, ButtonType.Cancel)
}
// React to the dialog being closed.
alert1.onHidden = {_ =>
alert1.result.value match {
// If button type one, change the property value.
// Note alert1.result.value is a JavaFX ButtonType, so use .delegate for the match.
case ButtonTypeOne.delegate => strProp.value = "Changed!"
// Otherwise, do nothing.
case _ =>
}
// Start the timer once more.
// This is going to be a very annoying app! ;-)
timerA.playFromStart()
}
// When the timer goes off, show the alert.
timerA.onFinished = {_ =>
alert1.show()
}
// Start the timer for the first time.
timerA.play
}

Passing information between two stages with ScalaFX fails due to not setting the field in controller

I am trying to understand the message passing possibilities of ScalaFX in combination with ScalaFXML. I created a small example, which has two views with controllers defined in the FXML. The first (or Main) view, is supposed to send a String to the second (or Dialog) view, by a button press.
I followed the example of the ScalaFXML Github page and used a trait in order to get the controller and it's possible for me to call a method on the Dialog controller, which sets a field to different value. This method gets called correctly, but the field isn't overwritten, when I check by clicking a button. Does this have something to do with ScalaFXML injecting the Controller?
Bellow I have the code for the two controllers
#sfxml
class MainController(val clicky: Button,
val inputText: TextField,
val resultText: TextArea) {
/** Creates a new window (in addition to the main window) and passes the contents of the text view to this window */
def onButtonPressed(): Unit = {
// Create the new window
val dialogFXML: String = "/Dialog.fxml"
val resource = getClass.getResource(dialogFXML)
val rootView = FXMLView(resource, NoDependencyResolver)
val loader = new FXMLLoader(resource, NoDependencyResolver)
loader.load()
val controller = loader.getController[AmountReceiver]
controller.setAmount(inputText.text.value)
val dialog = new Stage() {
title = "Add Person"
scene = new Scene(rootView)
resizable = false
initModality(Modality.ApplicationModal)
}
if (!dialog.showing()) {
dialog.show()
} else {
dialog.requestFocus()
}
}
}
and
#sfxml
class DialogController(val minAmountOfWords: Label,
val sendToMainButton: Button,
val input: TextArea) extends AmountReceiver {
var amount: String = "empty"
def submitText(): Unit = {
println(s"The actual amount is ${this.amount}")
// Close Dialog
quitDialog()
}
override def setAmount(amount: String): Unit = {
this.amount = amount
println(s"Setting the amount to ${this.amount}")
}
def quitDialog(): Unit = {
val stage: Stage = input.getScene.getWindow.asInstanceOf[Stage]
stage.close()
}
}
Running this code and entering "2" in the textfield, will print the following output:
Setting the amount to 2
The actual amount is empty
I actually figured it out myself. The problem lays within the rootView, which is received by creating a new FXMLView. In order access the correct controller, the rootView has to be received by the loader, which is already used to get the controller.
So calling
val rootView = loader.getRoot[jfxs.Parent]
instead of
val rootView = FXMLView(resource, NoDependencyResolver)
Like in the above example, the rootView is passed to the Scene constructor.

Automatic update of Button text on change

I'm currently learning scala, and making an encryption program with a basic scala swing UI.
I added 2 swing buttons which text is held by 2 var.
The code looks like this :
var encText = "Encrypt"
var decText = "Decrypt"
def top = new MainFrame {
title = "Data Guardian"
minimumSize = new Dimension(500, 200)
contents = new GridPanel(2, 2) {
hGap = 3; vGap = 3
contents += new Button {
text = encText
reactions += {
case ButtonClicked(_) => Main.startEnc
}
}
contents += new Button {
text = decText
reactions += {
case ButtonClicked(_) => Main.startDec
}
}
}
size = new Dimension(150, 40)
}
Those "text" var will be changed often during the encryption/decryption process by various methods, but when they do change, the text displayed on the buttons doesn't.
I'd like to know a way to make the displayed text of the buttons automatically change when the var that holds that text changes.
Thanks a lot for your insight :)
Make the strings private and write getters/setters that change the button text as a side-effect.
You'll need to give the buttons names, rather than having anonymous instances as you do above.

Periodic repaint of scala.swing.SimpleSwingApplication

If I run this example code
class ExampleGUIElement extends Panel
{
preferredSize = new Dimension(100, 100)
val rand : Random = Random
override def paintComponent(g: Graphics2D) = g.drawLine(rand.nextInt(10),rand.nextInt(10),rand.nextInt(90) + 10 ,rand.nextInt(90) + 10)
}
object GUITest extends SimpleSwingApplication
{
def top = new MainFrame
{
contents = new ExampleGUIElement
}
}
It obviously only shows the one line that has been draw initially. If I want a periodic repaint I normally would just make a Timer that calls repaint on the most outer JFrame.
But SimpleSwingApplication does not have this method, at least I can not find it, and calling top.repaint instead just does nothing. So how do you periodically repaint the whole thing?
Consider calling peer on the top (see MainFrame), and then calling repaint on that:
top.peer.repaint()
Also, don't forget to call top.pack().
I realize this is dated, but I extracted your Panel into a val and added a timer to repaint it:
def top = new MainFrame
{
val example = new ExampleGUIElement
contents = example
val timer=new javax.swing.Timer(250, Swing.ActionListener(e =>
{
example.repaint()
}))
timer.start()
}