Glimmer - Using Ruby to Build SWT User Interfaces
Join the DZone community and get the full member experience.
Join For FreeGlimmer is a JRuby DSL that enables easy and efficient authoring of user-interfaces using the robust platform-independent Eclipse SWT library. Glimmer comes with built-in data-binding support to greatly facilitate synchronizing UI with domain models. The goal of the Glimmer project is to create a JRuby framework on top of Eclipse technologies to enable easy and efficient authoring of desktop applications by taking advantage of the Ruby language. With Glimmer having just become an Eclipse project, it's a good time to find out more.
Philosophy
Glimmer's design philosophy can be summarized as follows:
- Concise and DRY
- Asks for minimum info needed to accomplish task
- Convention over configuration
- As predictable as possible for existing SWT developers
Conventions
Since Glimmer relies on Ruby, it is different in its syntax and conventions from what typical Java SWT developers would expect:
- Method parentheses are optional
- Java-vs-Ruby example: show() => show
- Method names follow underscored syntax
- Java-vs-Ruby example: addListener => add_listener
- Classes are constructed using the new(...) method (as opposed to new keyword):
- Java-vs-Ruby example: new GridLayout() => GridLayout.new
Download
Please download Glimmer from RubyForge: https://rubyforge.org/projects/glimmer/
NOTE: Glimmer is moving to Eclipse.org. Please visit http://andymaleh.blogspot.com for up-to-date news on the move and the upcoming download location on the Eclipse website.
Installation
Extract the Glimmer zip file and follow the installation instructions in the README file.
NOTE: While Glimmer is platform-independent, its functionality has only been verified on Windows. Feedback from Mac and Linux users would be greatly appreciated.
Tutorial
Let's start with a very simple Glimmer Hello World example:
shell { label { text “Hello World!” } }
This will render the following:
[img_assist|nid=3586|title=|desc=|link=none|align=undefined|width=126|height=48]
In the SWT library a shell represents an application's window. It acts as a frame around the application widgets, which are visual components that display information and/or enable interaction with the user.
One widget that was used in the Hello World example is the label widget, which simply displays text on the screen. Shell is also considered a widget, except it is a special kind of widget called composite.
The shell keyword, which declared the application's shell, was followed by a block of code encased in curly braces. This block contains the shell content declarations, such as the Hello World label. The label keyword was also followed by a block of code. However, this block contained a property declaration for the label, stating that the text value is “Hello World!”
So, to declare a widget, simply state its name followed by a block of code. The block may specify property values or nest other widget declarations for composite widgets.
Now, let's move on to a more advanced example:
shell { text "User Profile" composite { layout GridLayout.new(2, false) group { text "Name" layout GridLayout.new(2, false) layout_data GridData.new(fill, fill, true, true) label {text "First"}; text {text "Bullet"} label {text "Last"}; text {text "Tooth"} } group { layout_data GridData.new(fill, fill, true, true) text "Gender" button(radio) {text "Male"; selection true} button(radio) {text "Female"} } group { layout_data GridData.new(fill, fill, true, true) text "Role" button(check) {text "Student"; selection true} button(check) {text "Employee"; selection true} } group { text "Experience" layout RowLayout.new layout_data GridData.new(fill, fill, true, true) spinner {selection 5}; label {text "years"} } button { text "save" layout_data GridData.new(right, center, true, true) } button { text "close" layout_data GridData.new(left, center, true, true) } } }.open
This will render the following:
[img_assist|nid=3587|title=|desc=|link=none|align=undefined|width=195|height=209]
The example contains a variation of widgets from SWT:
- Composite: a widget that can simply contain other widgets and manage their layout
- Group: Similar to Composite except that it usually has a border and a title.
- Text Field: Enables user to type in text information
- Checkbox Button: Allows user to make a selection from different options
- Radio Button: Allows user to make a selection between options that are mutually exclusive
- Spinner: Enables user to type in numeric information or spinning a number selection by mouse
- Push Button: Enables user to initiate actions
Given that Glimmer relies on the Eclipse SWT library, developers may consult the SWT API as a reference on all the widgets, including their properties and layout options: http://help.eclipse.org/stable/nftopic/org.eclipse.platform.doc.isv/reference/api/index.html
Keep in mind the following rules when reading the SWT API:
- Any widget available in SWT, including custom widgets written by developers, can be accessed from Glimmer by downcasing/underscoring the widget's name (e.g. Composite -> composite, LabledText -> labeled_text)
- Properties available on SWT widgets are specified by listing them followed by their values, each on a line or separated by semicolons within the widget's block (e.g. label {text "Username:"; font some_font}) Property names are also downcased/underscored in Glimmer.
SWT widgets must have a style value specified, which is a constant available on the “SWT” class. Glimmer generally hides that by relying on smart defaults.
Here is a listing of the defaults configured in Glimmer:
text: SWT::BORDER
table: SWT::BORDER
spinner: SWT::BORDER
button: SWT::PUSH
Nonetheless, to customize a widget, a style value may be optionally specified within parentheses after the widget name. For an example, “button(SWT::RADIO)” renders a radio button and “button(SWT::CHECK)” renders a checkbox button.
Glimmer's syntax also has syntactic sugar for specifying the style. Simply state the name of the style in the standard Ruby downcased/underscored format without the “SWT::” prefix. For example, button(SWT::RADIO) becomes button(radio).
SWT composite widgets, such as shell, composite, and group can have a layout manager that lays out child widgets according to a certain pattern without the need to specify the (x, y) position of each child widget explicitly. Layout managers come in many flavors, such as GridLayout, offering a grid-like layout; FillLayout, allowing child widgets to fill the whole available area; and RowLayout, rendering child widgets one after the other in a row by default.
Glimmer is configured with smart defaults for layout managers too:
shell: FillLayout
composite: GridLayout with one column
group: GridLayout with one column
GridLayout is a particularly useful SWT layout, so I will go over it in a little more detail here. GridLayout allows you to lay widgets out in a grid similar to HTML tables. To instantiate a custom GridLayout, you must specify the number of columns and whether they are of equal width or not. Here is a block of code demonstrating a group box having a GridLayout with 2 columns of unequal width:
group { layout GridLayout.new(2, false) }
Now, suppose we add four elements to that group box:
group { layout GridLayout.new(2, false) label {text "First"}; text {text "Bullet"} label {text "Last"}; text {text "Tooth"} }
The specified GridLayout will lay out the child widgets in the grid from left to right and top to bottom:
- The label with the text “First” will go into the 1st column of the 1st row.
- The text box with the text “Bullet” will go into the 2nd column of the 1st row.
- The label with the text “Last” will go into the 1st column of the 2nd row.
- The text box with the text “Tooth” will go into the 2nd column of the 2nd row.
The group was actually a part of the advanced example illustrated earlier. It was given a title (by specifying the text attribute,) and the widget declarations were written in a way that maps visually to how they appear on the screen. Notice how text box declarations are on the same line as the label declarations since both the label and text box go under the same row, which helps improve code readability and maintainability:
group { text "Name" layout GridLayout.new(2, false) layout_data GridData.new(fill, fill, true, true) label {text "First"}; text {text "Bullet"} label {text "Last"}; text {text "Tooth"} }
That renders the following:
[img_assist|nid=3588|title=|desc=|link=none|align=undefined|width=89|height=73]
Layout of specific widgets may be further customized by specifying layout data. For GridLayout, layout data is specified through GridData objects.
For example, we may decide to have the text boxes in the previous example have a greater width:
group { layout GridLayout.new(2, false) label {text "First"}; text { text "Bullet" layout_data GridData.new(100, default) } label {text "Last"}; text { text "Tooth" layout_data GridData.new(100, default) } }
This renders the following:
[img_assist|nid=3589|title=|desc=|link=none|align=undefined|width=125|height=73]
The used GridData constructor takes two parameters: width hint and height hint.
The width was set to 100 pixels for both text boxes. The height was kept at the default value (SWT::DEFAULT)
For more details about GridLayout, GridData, and other layout managers, please refer to the SWT API documentation.
So far we have covered how to construct user-interfaces that can display data and gather input from the user. Next, we will demonstrate how to perform work based on actions taken by the user.
SWT widgets can be monitored for certain user-interface events, such as mouse clicks, focus gain and loss, and key presses.
With the original SWT API, events can be monitored by adding listeners to widgets. For example, to monitor the push of a button, you would add a SelectionListener that does some work in its widgetSelected event method.
With Glimmer, events can be monitored by declaring their name (following Ruby conventions) prefixed by “on”
Here is an example of how to monitor button selection:
import org.eclipse.swt.widgets.MessageBox @shell1 = shell { composite { button { text 'Save' on_widget_selected { message_box = MessageBox.new(@shell1.widget, SWT::NULL) message_box.text = 'Information' message_box.message = 'Saved!' message_box.open } } } } @shell1.open
This renders the following:
[img_assist|nid=3590|title=|desc=|link=none|align=undefined|width=170|height=145]
On click of the button, a message box is opened to let the user know that the information entered is saved.
MessageBox is a class from SWT that represents message dialogs. It was imported using the JRuby import method. Its constructor takes a parent and style. To obtain the parent, we assigned the shell object to a Ruby class variable @shell1. Since Glimmer wraps all SWT constructed objects with Glimmer decorators ( e.g. Shell is wrapped with RShell,) to obtain the SWT Shell class and pass it as the parent to the MessageBox constructor, the widget method was called (e.g. @shell1.widget.)
In the original SWT API, MessageBox has setter methods to set its text and message attributes. However in JRuby, the developer has the option to set them following the Ruby attribute conventions (e.g. message_box.text = 'value') because JRuby automatically enhances all Java objects with methods that follow the Ruby convention.
Another example that benefits from event monitoring is field validation on loss of focus. For example, let's say we are validating the ZIP code on an address form, and we would like to display an error message if its value does not have a valid ZIP code format (e.g. 12345 or 12345-1234,) here is how we would do it with Glimmer (please add the following code before the button in the previous example):
import org.eclipse.swt.widgets.MessageBox @shell1 = shell { composite { label { text "ZIP Code" } text { on_focus_lost { |focus_event| zip_code = focus_event.widget.text unless zip_code =~ /^\d{5}([-]\d{4})?$/ message_box = MessageBox.new(@shell1.widget, SWT::NULL) message_box.text = 'Validation Error' message_box.message = 'Format must match ##### or #####-####' message_box.open focus_event.widget.set_focus end } } button { text 'Save' on_widget_selected { message_box = MessageBox.new(@shell1.widget, SWT::NULL) message_box.message = 'Saved!' message_box.open } } } } @shell1.open
Here is what it produces:
[img_assist|nid=3591|title=|desc=|link=none|align=undefined|width=346|height=151]
Notice how the on_focus_lost block has a FocusEvent object as a parameter. This parameter may be specified optionally whenever some information is needed from the event object.
Again, this maps to the focusLost method on the FocusListener class in the original SWT API, which also takes a FocusEvent object as a parameter.
While widgets in the original SWT API have a setFocus event to grab the user interface focus, in JRuby set_focus may be used instead following the Ruby naming conventions.
Now, in order to cleanly separate event-driven behavior from user-interface code, we can rely on Glimmer's data-binding support. Stay tuned for the next tutorial, which will cover data-binding and how to achieve clean code separation with the Model-View-Presenter pattern.
References:
Glimmer Eclipse Technology Project Proposal: http://www.eclipse.org/proposals/glimmer/
Glimmer Newsgroup: http://www.eclipse.org/newsportal/thread.php?group=eclipse.technology.glimmer
Glimmer at RubyForge: http://rubyforge.org/projects/glimmer/
Author Blog: http://andymaleh.blogspot.com
Andy Maleh (andy at obtiva.com), Senior Consultant, Obtiva Corp.
Opinions expressed by DZone contributors are their own.
Comments