In software development there are no silver bullets, but I am always looking out for the next bronze one.

Sunday, February 24, 2008

SQUIB Kicks (Part 1)

    

SQUIB (Scala's Quirky User Interface Builder) has been undergoing changes in the last month. New features include support for many event types beyond the simple ActionEvent. Instead of just showing cool things done using SQUIB (That will come later), I want to take a step back and a start a little tutorial on how to use SQUIB.


    


SQUIB gets its inspiration from JavaFX, but is not intended to be a clone of it. A design goal of SQUIB is to be lightweight, but enable developers to code GUIs in a more expressive manner with less lines of code.


    


For this tutorial, I've built a release and made it available at the project home on Google Code. Please download the main library jar, http://scala-squib.googlecode.com/files/scala_squibV0_4.jar.
SQUIB requires Scala 2.6 or later. I've only done basic validation the release candidates for Scala 2.7, but it seems to be working. The examples below will be Scala scripts instead of compiled applications to simplify execution by removing the compile step.


A "Hello World" frame

import tfd.scala.squib._

import javax.swing.WindowConstants

frame(
attributes( 'title -> "Hello",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE),
contents(
label( attributes('text->"Hello World") )
)
).pack

link
    

To execute the script, save the source code to a file (for example "helloWorld.scalash") or download from the link, Ensure the scala_squibV0_4.jar is in the CLASSPATH, and invoke the script using "scala <saved source file>". Screenshot for the first example script:



    

SQUIB uses a helper object, attributes, to convert variable number of arguments into a list passed into an apply method of an object that constructs the component. Objects, frame and label, are used to construct JFrames and JLabels respectively. Originally, the apply method was overloaded to handle various combination of options. Eventually this got burdensome, so I made the apply methods use pattern matching of variable arguments and pretty much made attributes optional. For example, the code below doesn't use attributes:



import tfd.scala.squib._

import javax.swing.WindowConstants

frame(
'title -> "Hello",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
contents(
label('text->"Hello World Again!"))
).pack

link
Screenshot:

    

Attributes names are not case sensitive. Setter methods corresponding to the attributes are invoked on the event queue via invokeAndWait if the current thread is not the event dispatch thread.



 

Buttons and Events
    


The next example is a button that simply changes its text when pressed:



import tfd.scala.squib._

import java.awt.event.ActionEvent
import javax.swing.{JButton, WindowConstants}

frame(
'title -> "Press Me",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
contents(
button(
'text->"Press Me",
events (
'actionPerformed -> { ae:ActionEvent =>
ae.getSource.asInstanceOf[JButton].setText("Pressed")
}
)
)
)
).pack
link
    
Screenshots before and after button pressed:





Event names are not case sensitive using this notation. Like attributes, events is also pretty much optional as well. However, there is one tiny risk. The pattern matcher could mistake an attribute for an event if by small chance, there is an attribute that is a Scala Function1[EventObject] object. This could not happen with an existing Swing component, but only with a custom component developed in Scala. The risk is very small and can be avoided by using events. Here is the equivalent code sans events:



import tfd.scala.squib._

import java.awt.event.ActionEvent
import javax.swing.{JButton, WindowConstants}

frame(
'title -> "Press Me",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
contents(
button(
'text->"Press Me",
'actionPerformed -> { ae:ActionEvent =>
ae.getSource.asInstanceOf[JButton].setText("Pressed")
}
)
)
).pack

link
    

Since there are a finite number of events within Swing, SQUIB has helper/factory objects for the most common events. Being the name of an object, the event specification does become case sensitive. Here an a example that uses such a helper for the actionPerformed event which is in the tfd.scala.squib.event package:



import tfd.scala.squib._
import tfd.scala.squib.event._

import java.awt.event.ActionEvent
import javax.swing.{JButton, WindowConstants}

frame(
'title -> "Press Me",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
contents(
button(
'text->"Press Me",
actionPerformed { ae:ActionEvent =>
ae.getSource.asInstanceOf[JButton].setText("Pressed")
}
)
)
).pack

link
    

The actionPerformed object makes the code a little tidier. Another benefit is that they can be passed a block without the EventObject parameter for those cases where the event handler is not "interested" in it. Example:


import tfd.scala.squib._
import tfd.scala.squib.event._

import javax.swing.{JButton, WindowConstants}

var clickCount = 0;

def clickCountText = "ClickCount: " + clickCount

frame(
'title -> "Press Me",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
'layout->gridlayout('rows->2, 'columns->1),
contents(
button('text->"Press Me",
actionPerformed {
clickCount += 1
label.id("clickCount").setText(clickCountText)
}
),
label("clickCount", 'text->clickCountText)
)
).pack

link
Screenshot:

    

Removing the unneeded "ActionEvent:ae => " cleans up the code even more. This example also introduces the concept of component ids. Every SQUIB component object (like "label", "button" or "frame") keeps a map of components indexed by a String. The "id" method takes a String and returns the last component of that type constructed with that id (if there is any). Each component type has a different map, so different component types can have the same id. In the previous example, looking up the id during every event is not optimal, since the event handlers are lexically scoped closures, that can access values or variables within the current scope requiring the lookup to be performed only once. Example:



import tfd.scala.squib._
import tfd.scala.squib.event._

import javax.swing.{JButton, WindowConstants}

var clickCount = 0;

def clickCountText = "ClickCount: " + clickCount

lazy val clickCountLabel = label.id("clickCount")

frame(
'title -> "Press Me",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
'layout->gridlayout('rows->2, 'columns->1),
contents(
button('text->"Press Me",
actionPerformed {
clickCount += 1
clickCountLabel.setText(clickCountText)
}
),
label("clickCount", 'text->clickCountText)
)
).pack

link
    

Since the label id is not set until after the block declaration, the val needs to be "lazy", so that it is not instantiated until the first time it is needed in the button event handler. Use of "lazy" is very helpful for SQUIB applications, because of situations like this.


    


If one doesn't want to use an id, the component itself can be referenced by a value and used:



import tfd.scala.squib._
import tfd.scala.squib.event._

import javax.swing.{JButton, WindowConstants}

var clickCount = 0;

def clickCountText = "ClickCount: " + clickCount

val clickCountLabel = label("clickCount", 'text->clickCountText)

frame(
'title -> "Press Me",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
'layout->gridlayout('rows->2, 'columns->1),
contents(
button('text->"Press Me",
actionPerformed {
clickCount += 1
clickCountLabel.setText(clickCountText)
}
),
clickCountLabel
)
).pack

link
    

Here is one last example of a button with mouse event handlers:



import tfd.scala.squib._
import tfd.scala.squib.event._

import javax.swing.{JButton, WindowConstants}

val defaultButtonText = "Please Press Me"

lazy val pressMeButton = button.id("pressMe")

frame(
'title -> "Press Me",
'visible -> true,
'defaultCloseOperation -> WindowConstants.DISPOSE_ON_CLOSE,
contents(
button("pressMe",
'text->defaultButtonText,
actionPerformed {
pressMeButton.setText("Ouch !!!!")
},
mouseEntered {
pressMeButton.setText("Don't Press Me")
},
mouseExited {
pressMeButton.setText(defaultButtonText)
}
)
)
).pack

link
Screenshots before and after the mouse entered the component:





    

Currently, not all events are supported by SQUIB. For example, HierarchyEvent is not supported. In the those cases, one would have to implement a HierarchyListener in the same manner as Java. Supported events are ActionEvent, ChangeEvent, ComponentEvent, FocusEvent, InternalFrameEvent, ItemEvent, KeyEvent, ListSelectionEvent, MouseEvent, MouseWheelEvent, and WindowEvent.


    

This should pretty good starting point for how to use the SQUIB library. There will be future posts that will demonstrate other aspects including layouts and the SceneGraph library.


"Cool Thing" for this post
    

I've created a version of a SceneGraph demo named "Intro". The Java version is demonstrated here, https://scenegraph-demos.dev.java.net/demos.html.
There is an also a link to the source on the demo page.
The Scala source can be viewed here, http://scala-squib.googlecode.com/svn/trunk/demo/src/tfd/scala/squib/demo/scenegraph/Intro.scala.
To execute the demo, one needs the scala_squibV0_4.jar linked above and the jar with SQUIB SceneGraph support downloaded from http://scala-squib.googlecode.com/files/scala_squib_scenegraphV0_4.jar. Also, required is the archive containing the SceneGraph library itself available here, http://download.java.net/javadesktop/scenario/releases/0.5/Scenario-0.5.jar , and a jar containing the compiled application and resources downloaded from
http://scala-squib.googlecode.com/files/scala_scenegraph_intro.jar. Execute via:

scala -cp scala_squibV0_4.jar;scala_squib_scenegraphV0_4.jar;Scenario-0.5.jar;scala_scenegraph_intro.jar tfd.scala.squib.demo.scenegraph.Intro

Screenshot of the SQUIB application which is pretty much identical to the Java version on the SceneGraph demos page:

    

I'll let readers draw their own conclusions comparing the Scala/SQUIB code with the Java code for the application.


    

My own experience with Scala was that at first, I found the syntax kind of "wonky", but once I got used to it, I found it to be well thought out and even intuitive. Features like type inference and first class function objects are sorely missed at times when developing in Java at work.

7 comments:

Eric Burke said...

I have always thought Swing made a fundamental mistake in mixing GUI layout with source code. This makes GUI builders problematic because they constantly have to parse and sync the GUI layout with source code. I guess in my mind, changing the language from Java to Scala does not solve this fundamental mistake. It might be better to design a system where the GUI layout is completely decoupled, so the GUI builder can generate all of the layout info completely independently of the GUI. This has certainly been done on other platforms, and has also been done in Java. The problem with doing it in the Java world is we'd have a proprietary format, so none of the major GUI builders would support it.

Anonymous said...

i am getting the following stack trace while trying out your framework. is it because it is not compatible with scala-2.7.0-final?

regard,

walter chang (weihsiu@gmail.com)

java.lang.NoSuchMethodError: scala.Iterator$.range(II)Lscala/Range;
at tfd.scala.squib.HasDefaultAttributes$class.checkInitMethodHash(HasDefaultAttributes.scala:85)
at tfd.scala.squib.BuiltComponent.checkInitMethodHash(BuiltComponent.scala:25)
at tfd.scala.squib.HasAttributes$class.doSetters(HasAttributes.scala:85)
at tfd.scala.squib.BuiltComponent.tfd$scala$squib$HasDefaultAttrib...

tdalton said...

The stack trace likely is caused by the 2.6.1 compiled code being ran using 2.7.
I've put a jar compiled using 2.7 here: http://scala-squib.googlecode.com/files/scala_squibV0_4-2_7.jar.

Fred Janon said...

Is there a way to reference Swing components with variables? I like to split my screen in 3 panels: top, center and bottom. I build each of them in a different method and aggregate them in another panel with a specific layout. That keeps the code clearer for me. The Groovy SwingBuilder has a 'widget' pseudo component for that. See code below. Sorry if it formats badly on the blog, I don't know where to post my question, there is no forum on code.google for the Squib project.

// Build a panel with a table used to select the attribute to be used as keys in the UPDATE statement
// The table as a column with a checkbox and another with the attribute names
def topPanel = setupAttributesAsKeysPanel(swingBuilder)

// Panel with the attributes to be updated and "Select all" and "Deselect all" buttons
// The table as a column with a checkbox and another with the activities attribute names to be updated
def centerPanel = setupAttributesToUpdatePanel(swingBuilder)

// Panel with "Update Activities" and "Close" buttons
def bottomPanel = setupButtonsPanel(swingBuilder)

// TODO: adjust the location dynamically
// With a JFrame, we need to enable the "Update Activities" button in the "ODBC Export" parent window in the close action handler
// frame = swingBuilder.frame(title: ODBCUPDATE, location: [300,100], windowClosing: closeButtonClicked,
frame = swingBuilder.dialog(owner: parent.frame, modal: true, title: ODBCUPDATE, location: [300,100],
windowOpened: onWindowOpened, windowClosing: doCloseWindow, defaultCloseOperation: WindowConstants.DO_NOTHING_ON_CLOSE, pack: true, show: false)
{
panel(border: emptyBorder(top:3,left:3,bottom:3,right:3))
{
borderLayout()
box(axis: BoxLayout.Y_AXIS)
{
widget(topPanel)
widget(centerPanel)
}
widget(bottomPanel, constraints: BorderLayout.SOUTH)
} // panel
} // frame
// Prevent the user to make the window any smaller than the minimum required to show all elements. The window can still be made larger
Dimension size = frame.getSize()
size.width = size.width + 100 // Make it a little bit fatter
frame.setMinimumSize(size)
frame.setSize(size)
frame.show()

tdalton said...

I created a google group for SQUIB and will be putting together a SQUIB example for Fred's comment shortly:

http://groups.google.com/group/scala-squib

Anonymous said...

I want not agree on it. I regard as nice post. Especially the appellation attracted me to read the unscathed story.

Anonymous said...

Amiable brief and this post helped me alot in my college assignement. Thanks you for your information.