I'm experimenting with a bit of Scala gui programming (my first project in scala, so I thought I'd start with something simple). But I seem to have got stuck at something that seems like it should be relatively trivial. I have a class that extends scala.swing.MainFrame, and I'd like to detect when a user presses a key when that window has focus. Funny thing is I don't seem to be able to find any way to get that event to fire.
I found an example of how someone else had got around the problem here: http://houseofmirrors.googlecode.com/svn/trunk/src/src/main/scala/HouseGui.scala but they seem to have reverted to using the Java Swing API, which is a little disappointing. Does anyone know if there's a more idiomatic way of intercepting events?
This seems to work with Scala 2.9
package fi.harjum.swing
import scala.swing._
import scala.swing.event._
import java.awt.event._
object KeyEventTest extends SimpleSwingApplication {
def top = new MainFrame {
val label = new Label {
text = "No click yet"
}
contents = new BoxPanel(Orientation.Vertical) {
contents += label
border = Swing.EmptyBorder(30,30,10,10)
listenTo(keys)
reactions += {
case KeyPressed(_, Key.Space, _, _) =>
label.text = "Space is down"
case KeyReleased(_, Key.Space, _, _) =>
label.text = "Space is up"
}
focusable = true
requestFocus
}
}
}
In addition to listening to this.keys you should also call requestFocus on the component or set focusable=true, if it is Panel or derived class.
I expect you need to listen to this.keys (where this is the element of the GUI receiving the keyboard events). See the equivalent question about mouse event.
My solution for this required me to do the following:
class MyFrame extends MainFrame {
this.peer.addKeyListener(new KeyListener() {
def keyPressed(e:KeyEvent) {
println("key pressed")
}
def keyReleased(e:KeyEvent) {
println("key released")
}
def keyTyped(e:KeyEvent) {
println("key typed")
}
})
}
This only seemed to work though if there were no button objects attached to this component, or any of it's children.
Rather than falling back to java events all components have keys that publishes these events (so MainFrame does not). Not sure what the best solution is but it's always possible to wrap everything in the frame inside a Component and listen to its keys.
Related
In the following minimal Scala Swing app, if I choose File->Exit (which calls sys.exit), or File->Quit (which calls SimpleSwingApplication.quit) then the application exits. If I choose File->Close (which calls Frame.close) then the window closes but the application doesn't exit (e.g. when run using java -jar myexample.jar it doesn't return to the prompt -- I have to ^C to kill it).
import scala.swing._
import swing.event.{Key,WindowClosed,WindowClosing}
object GUI extends SimpleSwingApplication {
def top = new Frame {
import javax.swing.WindowConstants.EXIT_ON_CLOSE
peer.setDefaultCloseOperation(EXIT_ON_CLOSE)
menuBar = new MenuBar {
contents += new Menu("File") {
mnemonic = Key.F
contents += new MenuItem(Action("Exit") { sys.exit(0) }) { mnemonic = Key.X }
contents += new MenuItem(Action("Close") { close() }) { mnemonic = Key.C }
contents += new MenuItem(Action("Quit") { quit }) { mnemonic = Key.Q }
}
}
}
}
I've tried a few different things, all with the same behavior:
use MainFrame instead of Frame
add a handler to reactions that matches WindowClosing and call sys.exit from there
added a reaction to both a Frame subclass (not shown here) and
to the SimpleSwingApplication
EXIT_ON_CLOSE seemed like it should do the job, but whether it's there or not doesn't matter
set default close operation to DISPOSE_ON_CLOSE
override Frame.closeOperation to call sys.exit, but this doesn't seem to get called at all (I added a print there and it doesn't result in any output)
Inspired by this answer, if I change the File->Close handler to call dispose instead of close, the app exits. The docs for MainFrame say:
Shuts down the framework and quits the application when closed.
Which made me think that close should work.
Why do I have to call dispose instead of close here? Why doesn't EXIT_ON_CLOSE or overriding the closeOperation work?
The method you're calling on swing.Window is:
def close() { peer setVisible false }
which obviously isn't what you might assume.
Alternatively, you can use GUI.quit().
I have a simple swing application in Scala. Work is done by a separate object but progress must be periodically reported to the GUI which launched it. The problem is that the updates are only visible once the Thread has completed its task. I've tried adding various calls to repaint() top.peer.repaint() and Thread.sleep(0) all to no avail and all the while feeling that the fact that I'm resorting to adding these is a sign I'm doing something wrong.
I remember struggling and overcoming this issue with this back when I used to develop in Java and have tried to structure my solution based on what I recall being the right approach but I must be missing something.
Here is a simple example which reproduces the problem:
import scala.swing._
import scala.swing.event.ButtonClicked
import BorderPanel.Position._
import java.awt.EventQueue
class HeavyLifter extends Runnable {
override def run = {
UpdateInterface.say("Performing Useful Work")
for (i <- 0 until Int.MaxValue) {
UpdateInterface.say(i.toString)
}
}
}
object UpdateInterface extends SimpleSwingApplication {
private val txtLog = new TextArea(32,64) {
editable = false
}
private val scrollPane = new ScrollPane(txtLog)
private val btnGo = new Button("Go")
def say(strWhat : String) = {
txtLog.append(strWhat + "\n")
}
def top = new MainFrame {
contents = new BorderPanel {
listenTo(btnGo)
reactions += {
case ButtonClicked(`btnGo`) => EventQueue.invokeLater(new HeavyLifter)
}
layout(scrollPane) = Center
layout(btnGo) = South
}
}
}
Update I've accepted the answer which drew attention to the cause of the problem. I stupidly thought that since the worker was a Runnable that EventQueue.invokeLater would spawn a separate thread for it. I have been pointed in the direction of Swing Worker which is probably the right way to go.
The EventQueue.invokeLater cause the loop to run in the dispatch thread (i.e the UI thread), which basically means that the UI thread is occupied to do the for loop and once that is done then it will update the UI and perform other tasks, after all a thread can do a single thing at a time. This is the reason your UI gets updated after the loop is completed.
What you can do is, run the worker runnable in a new thread and from that worker code dispatch a new runnable, for each update, (whose run method updated the text area value) using EventQueue.invokeLater.
I am trying to add a ScrollBar. The ScrollBar will iterate through the documents displayed. However, I am having trouble receiving an event when the scroll bar changes. I'm not sure what I need to listen to, and I'm not sure what event I should be responding to. I tried the following and I get some events, but I don't think these are the ideal events to handle.
listenTo(scrollBar)
listenTo(scrollBar.keys)
listenTo(scrollBar.mouse.moves)
listenTo(scrollBar.mouse.wheel)
listenTo(scrollBar.mouse.clicks)
For example, I only get MouseClicked, MousePressed, and MouseReleased when I click inside the scrollbar--not when I click on the arrows to actually change the value.
I found this discussion about scroll bars not receiving events properly, but it's two years old. As far as I can tell, the author did not follow up an file a ticket. Maybe he found a workaround.
Any ideas?
Good question. Clicking on arrows isn't handled by ScrollBar, it's handled by ScrollBarUI. I believe that default implementation(or at least base class for majority of ScrollBarUI implementations) is BasicScrollBarUI.
If you'll take a look into source of javax.swing.plaf.basic.BasicScrollBarUI it has incrButton and decrButton buttons and they are components you want listen to.
P.S. I had similar need for having custom (key) listener for my Slider and having custom ui which exposes needed components/model(as you could see almost all components are protected so you easy could access them in subclasses and expose via public getters) worked great for me. I did that in plain java though, maybe in scala you can listen to buttons just by specyfying property name.
One more piece of Scala Swing being broken. The Adjustable trait seems to be completely hollow, nothing is wired.
The following works:
class ScrollBarAlive extends swing.ScrollBar {
me =>
peer.addAdjustmentListener(new java.awt.event.AdjustmentListener {
def adjustmentValueChanged(e: java.awt.event.AdjustmentEvent) {
publish(new swing.event.ValueChanged(me))
}
})
}
Test:
import swing._
object ScrollBarTest extends SimpleSwingApplication {
lazy val top = new Frame {
val label = new Label { text = "0" }
val scroll = new ScrollBarAlive {
orientation = Orientation.Horizontal
listenTo(this)
reactions += {
case event.ValueChanged(_) =>
label.text = value.toString + (if (valueIsAjusting) " A" else "")
}
}
contents = new BorderPanel {
add(label, BorderPanel.Position.North)
add(scroll, BorderPanel.Position.South)
}
pack().centerOnScreen()
open()
}
}
A proper implementation would also introduce a subtype of AdjustingEvent.
This is my first experiment using Swing with Scala, and have a few questions about my code below. All it does is to produce a window with a coloured rectangle that changes colour. Please feel free to answer one or any of the questions.
1) I used a Java ActionListener below because I couldn't work out how to get javax.swing.Timer to work as a Publisher. Is there a way to use the Scala model, i.e. listenTo() - or is this the way to do it?
2) My overridden preferredSize value in the Panel doesn't seem to work: the window comes up minimized. In my Java version I override the getPreferredSize method, but there is no such method in Panel, so I assumed this is the way to do it, but why doesn't it work?
3) paintComponent isn't documented at all in the Scala API documentation. I assume this is because it is protected access in Java, but it seems like an oversight. Am I correct to override paintComponent or is it hidden because I'm supposed to use the documented paint method instead?
4) Scala doesn't seem to have getWidth() and getHeight() methods on components - is it standard to use size.width and size.height?
import swing._
import java.awt.{Graphics, Color}
import java.awt.event.{ActionEvent, ActionListener}
import javax.swing.Timer
object ColorPanel extends SimpleSwingApplication {
private var c: Color = new Color(0)
def top = new MainFrame {
title = "Flash!"
contents = p
}
val p = new Panel with ActionListener {
override val preferredSize = new Dimension(200, 200)
override def paintComponent(g: Graphics2D) {
g.setColor(c)
g.fillRect(0, 0, size.width, size.height)
}
def actionPerformed(e: ActionEvent) {
c = new Color((c.getRGB() + 1000) % 16777216)
repaint
}
}
val timer = new Timer(100, p)
timer.start()
}
No immediate answer. But your approach is certainly ok. I don't see though why your observer should be the panel. I would create an anonymous ActionListener directly with the timer, and instead add a specific method to that panel, like def animateColor() { ... }
You can use preferredSize = new Dimension(200, 200)
According to this quasi official document, yes : http://www.scala-lang.org/sid/8 (section 6 Custom Painting)
A bit stupid indeed to instantiate a new Dimension all the time. But if you look exactly at the example the SID, it does the same, uses size.height. In super high performance code, you may want to call directly into the underlying peer (peer.getWidth)
This is a continuation to my last question. I used the MVC pattern with swing components and code goes like this
import scala.swing
import scala.swing.event._
case object MyBusinessEvent extends Event
class MyController extends Publisher {
val form = new MyForm
listenTo(form)
reactions += {
case MyBusinessEvent => //handle event code here
}
}
class MyForm extends Publisher {
val ui = new GridBagPanel {
val c = new Constraints
.... more code here
}
val button1 = new Button("Button 1")
//add button to panel
listenTo(button1)
reactions += {
case ButtonClicked(_) => publish(MyBusinessEvent)
}
}
However with multiple buttons the program hangs up and seems to stop publishing events. Is there any way to fix this? Thanks
Although from the truncated example it's not clear what the problem is, my guess is you are creating a cycle which unfortunately can happen quite easy with swing and MVC. that is, one model gets updated as part of listening to another model, and that again triggers an update in the other model. In java swing you have two choices,
a) temporarily remove the listener that is updating the model (e.g. do a removeActionListener before calling setSelectedItem on a JComboBox, and then afterwards re-register with addActionListener)
b) check for the event source (getSource on an java.util.EventObject) and ignore events in the model that originated from that very same model.
Now scala swing is more simplified, so you don't have event object and event sources. You can add though a source in your custom event case class. And it might be feasible to remove the reaction and re-add it after setting a model's state.
To avoid the cycles mentioned in 0__'s answer I have a trait like this:
trait Editable extends Publisher {
private var _editing = false
def editing = _editing
def editing_=(b: Boolean) = _editing = b
override def publish(e: Event) {
if(!editing) super.publish(e)
}
}
which I mix-in in the components I want to have more control about the events fired. So, when I want to change something without firing any event, I simply put the code between editing = true and editing = false.