I’m writing a GUI in Scala, and I’ve run across a strange problem while trying to register Button Events in a foreach statement: It should be that for every element objecti in a list of objects (object0 … objectn), a corresponding Button x = buttoni is retrieved and a given Box is subscribed to it with box.listenTo(x). When the Button is pushed, some action related to the objecti should be performed (in this case, println("Event triggered: " + event)):
import scala.swing.ComboBox
import scala.collection.mutable.Buffer
import scala.swing.Button
import scala.swing.event.ButtonClicked
import scala.swing.Action
import scala.swing.SimpleSwingApplication
import scala.swing.MainFrame
import scala.swing.GridPanel
import scala.swing.BorderPanel
object EventSet extends SimpleSwingApplication {
object PhoneKeyEvent extends Enumeration {
val Key1 = Value("1")
val Key2 = Value("2")
}
/* Constants */
private val DisplayHistory = Buffer[String]()
private val KeypadKeyEvents = List(
PhoneKeyEvent.Key1, PhoneKeyEvent.Key2)
private val PhoneKeyEventButtonNames = Map(
PhoneKeyEvent.Key1 -> "1",
PhoneKeyEvent.Key2 -> "2"
)
/* End constants */
private var PhoneKeyEventButtons = Map[PhoneKeyEvent.Value, Button]()
private def createDisplay() : ComboBox[String] = {
new ComboBox(DisplayHistory) {
// Listen to keypad keys
// Get the set of all keypad key events
val keypadEvents = List(PhoneKeyEvent.Key1, PhoneKeyEvent.Key2)
println("keypadEvents: " + keypadEvents)
keypadEvents.foreach({ event =>
println("event: " + event)
// Listen to each button representing a keypad key event
var keypadEventButton = PhoneKeyEventButtons(event)
println("keypadEventButton: " + keypadEventButton)
listenTo(keypadEventButton)
reactions += {
case ButtonClicked(keypadEventButton) => {
// TODO: fix strange bug here: adds all possible inputs
println("Event triggered: " + event)
// selection.item = selection.item + event
}
}
})
}
}
private def createPhoneControllerPanel() : BorderPanel = {
new BorderPanel() {
val keypadControlPanel = createPhoneKeyEventTypeControlPanel(KeypadKeyEvents)
add(keypadControlPanel, BorderPanel.Position.Center)
add(createDisplay(), BorderPanel.Position.North)
focusable = true
requestFocus
}
}
/**
* Creates a new {@link Button} for a given {@link PhoneKeyEvent} and adds
* the button to the global map of such buttons to their respective events;
* that means multiple buttons cannot be created for the same key event.
*/
private def createPhoneKeyEventButton(phoneKeyEvent: PhoneKeyEvent.Value) : Button = {
// Only one button can be created per key event
require(!PhoneKeyEventButtons.contains(phoneKeyEvent),
{System.err.println("A Button for the PhoneKeyEvent " + phoneKeyEvent + "has already been created.")})
val keyEventButtonName = PhoneKeyEventButtonNames(phoneKeyEvent)
val result = new Button(Action(keyEventButtonName) {
println("Key event button pressed: " + phoneKeyEvent)
})
// Add the button to the map of all created key event buttons
PhoneKeyEventButtons += phoneKeyEvent -> result
return result
}
private def createPhoneKeyEventTypeControlPanel(keyEvents : Iterable[PhoneKeyEvent.Value]) : GridPanel = {
new GridPanel(4, 3) {
// Get the intersection of all key events of the given type and the events with button names
keyEvents.foreach(phoneKeyEvent => contents += createPhoneKeyEventButton(phoneKeyEvent))
}
}
override def top = new MainFrame {
contents = createPhoneControllerPanel()
}
}
However, I get some very strange behaviour, where clicking any Button results in all such object actions are triggered — See the program output:
keypadEvents: List(1, 2)
event: 1
keypadEventButton: scala.swing wrapper scala.swing.Button$$anon$1[,0,0,0x0,invalid,alignmentX=0.0,alignmentY=0.5,border=
javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@7633f09,flags=296,maximumSize=,minimumSize=,preferredSize=,de
faultIcon=,disabledIcon=,disabledSelectedIcon=,margin=javax.swing.plaf.InsetsUIResource[top=2,left=14,bottom=2,right=14]
,paintBorder=true,paintFocus=true,pressedIcon=,rolloverEnabled=true,rolloverIcon=,rolloverSelectedIcon=,selectedIcon=,te
xt=1,defaultCapable=true]
event: 2
keypadEventButton: scala.swing wrapper scala.swing.Button$$anon$1[,0,0,0x0,invalid,alignmentX=0.0,alignmentY=0.5,border=
javax.swing.plaf.BorderUIResource$CompoundBorderUIResource@7633f09,flags=296,maximumSize=,minimumSize=,preferredSize=,de
faultIcon=,disabledIcon=,disabledSelectedIcon=,margin=javax.swing.plaf.InsetsUIResource[top=2,left=14,bottom=2,right=14]
,paintBorder=true,paintFocus=true,pressedIcon=,rolloverEnabled=true,rolloverIcon=,rolloverSelectedIcon=,selectedIcon=,te
xt=2,defaultCapable=true]
Key event button pressed: 1
Event triggered: 1
Event triggered: 2
Key event button pressed: 2
Event triggered: 1
Event triggered: 2
I’m completely at a loss as to why this is happening; I’m quite new at Scala anyway, so it’s quite unfamiliar territory, but I’ve tried fiddling with a lot of stuff and snooped around in the Swing source code, and still clueless… how can every value of a reference inside of a loop be used in every iteration? or how can every event be triggered by Swing at once? or…?
Edit: Here are two minimised versions, both of which behave differently:
import scala.swing.SimpleSwingApplication
object ButtonEvents extends SimpleSwingApplication {
import scala.swing.Button
import scala.swing.event.ButtonClicked
import scala.swing.Action
import scala.swing.MainFrame
import scala.swing.FlowPanel
override def top = new MainFrame {
contents = new FlowPanel {
val button1 = new Button(Action("1") {
println("Button 1 pressed")
})
contents += button1
val button2 = new Button(Action("2") {
println("Button 2 pressed")
})
contents += button2
val buttons = List(button1, button2)
buttons.foreach({ button =>
listenTo(button)
reactions += {
case ButtonClicked(button) => {
println("Event triggered: " + button.text)
}
}
})
}
}
}
Prints:
Button 1 pressed
Event triggered: 1
Event triggered: 1
Button 2 pressed
Event triggered: 2
Event triggered: 2
And a version which seems to behave correctly (but I’m not sure why):
import scala.swing.SimpleSwingApplication
object ButtonEvents extends SimpleSwingApplication {
import scala.swing.Button
import scala.swing.event.ButtonClicked
import scala.swing.Action
import scala.swing.MainFrame
import scala.swing.FlowPanel
override def top = new MainFrame {
contents = new FlowPanel {
val button1 = new Button(Action("1") {
println("Button 1 pressed")
})
contents += button1
val button2 = new Button(Action("2") {
println("Button 2 pressed")
})
contents += button2
val buttons = Map("1" -> button1, "2" -> button2)
buttons.foreach({ eventButton =>
listenTo(eventButton._2)
reactions += {
case ButtonClicked(eventButton._2) => {
println("Event triggered: " + eventButton._1)
}
}
})
}
}
}
Prints (correct):
Button 1 pressed
Event triggered: 1
Button 2 pressed
Event triggered: 2
In the line
you are creating a new
val keypadEventButtonand assigning it to whatever is insideButtonClicked(). Changing the line tocase ButtonClicked(abstractButton)will still work and display same problem.I’m guessing you are expecting this to match the use of
keypadEventButtonon the preceding lines. You probably want to create one reaction and then use theabstractButtonto tell what button has been pressed.