| |
[Work in Progress]
Hello World: Your First F3 Program
It is traditional to provide the "Hello World" program as the first exposure to a new language, so here it is in F3:
Frame {
title: "Hello World F3"
width: 200
content: Label {
text: "Hello World"
}
visible: true
}
Running this program displays the following on the screen:
As you can see F3 provides a declarative syntax for expressing the structure and content of user interface components. To help you understand what's happening, let's rewrite the above program in a purely procedural fashion, similar to the way it's normally done in Swing:
var win = new Frame();
win.title = "Hello World F3";
win.width = 200;
var label = new Label();
label.text = "Hello World";
win.content = label;
win.visible = true;
This program is also a valid F3 program and it has exactly the same effect as the first one.
So what's happening (in both cases) is basically the following:
| I call the Frame class constructor to create a new Frame |
| I assign values to its title, width, visible, and content attributes |
| In the process of assigning the content attribute, I call the Label class constructor to create a new Label, and I assign its text attribute |
However, even in an extremely simple example like this one, the meaning of the program in the declarative syntax is tangibly easier to grasp.
Thus declarative programming consists of creating a program from a single expression. As in the first example above the root of that expression is often an object allocation expression (constructor) that generates the object graph that makes up your program.
The "Hello World" program has no dynamic behavior. To create a graphical user interface with dynamic behavior in F3, you create graphical user interface components whose properties depend on the attribute values of other objects. Those other objects can be anything you like - whatever representation of the state of your application you feel is suitable. Since the GUI component's properties depend on the properties of another object, whenever you modify that other object the GUI component will automatically reflect your changes. In this context the GUI component is typically referred to as the View and the "other" object as the Model. Here is a Model/View version of the "Hello World" program.
class HelloWorldModel {
attribute saying: String;
}
var model = HelloWorldModel {
saying: "Hello World"
};
var win = Frame {
title: "Hello World F3"
width: 200
content: Label {
text: bind model.saying
}
visible: true
};
Running this program displays the following:
If I programmatically change the model object's
saying attribute:
model.saying = "Goodbye Cruel World!";
The view automatically changes:
Notice that this example initializes the text attribute of the
label by applying the F3 bind operator to the saying
attribute of model. In this context, the bind operator
indicates incremental update. This means that whenever the value of
model.saying changes the text attribute of the label
will be updated to the same value.
In the case of input widgets, such as Buttons, CheckBoxes, TextFields, etc., the linkage between model attributes and GUI component attributes can be bidirectional.
Consider this example:
class HelloWorldModel {
attribute saying: String;
}
var model = HelloWorldModel {
saying: "Hello World"
};
var win = Frame {
title: bind "{model.saying} F3"
width: 200
content: TextField {
value: bind model.saying
}
visible: true
};
Running this program displays the following:
If I type something else into the text field and press Enter, the title of the window changes accordingly:
In this case the text field's value attribute is updated as a
result of user input (by the implementation of the TextField
class). When that happens the saying attribute of
model is updated to the same value. Since the expression assigned
to the title attribute of the window depends on the
saying attribute of model the expression is
reevaluated and the window's title attribute updated to the result.
You can bind any property of any component to an
incrementally evaluated expression. Such expressions can use conditional logic,
iteration, selection, etc. to produce dynamic content of any complexity, yet the
expression of such content remains declarative.
Borders and Layout Managers
In F3, using Borders and Layout Managers is also declarative. Each Swing/AWT layout manager is encapsulated in an F3 class that instantiates a JPanel with the specified layout manager. Components are added to the (underlying) JPanel in a declarative way using the attributes provided by the F3 class. Each Swing border type is also encapsulated by an F3 class that has attributes corresponding to the configuration options of that border. Here is a simple example using EmptyBorder and GridPanel. As you might expect,EmptyBorder corresponds to
javax.swing.border.EmptyBorder and GridPanel
corresponds to java.awt.GridLayout.
class ButtonClickModel {
attribute numClicks: Number;
}
var model = new ButtonClickModel();
var win = Frame {
width: 200
content: GridPanel {
border: EmptyBorder {
top: 30
left: 30
bottom: 30
right: 30
}
rows: 2
columns: 1
vgap: 10
cells:
[Button {
text: "I'm a button!"
mnemonic: I
action: operation() {
model.numClicks++;
}
},
Label {
text: bind "Number of button clicks: {model.numClicks}"
}]
}
visible: true
};
Running this program displays the following:
And after clicking the button a few times:
Note: see below for an explanation of the Button's
action and mnemonic attributes.
In this case I've configured the GridPanel to have one column, two rows, and
a vertical gap of 10 pixels between the rows by assigning values to its
columns, rows, and vgap attributes
(GridPanel also has an hgap attribute if you want to create a gap
between its columns). I've also given it an empty border 30 pixels on all sides.
I added the button and label by assigning them to its cells
attribute. The GridPanel implementation responds to insertions and
deletions to/from its cells attribute by adding or removing
components from its underlying JPanel.
Other layout managers are supported in F3 in a similar fashion. Here is a summary:
Layout Managers:
| F3 Widget | Layout Manager |
GridPanel |
GridLayout |
GridBagPanel |
GridBagLayout |
FlowPanel |
FlowLayout |
BorderPanel |
BorderLayout |
Box |
BoxLayout |
StackPanel |
Romain Guy's StackLayout |
CardPanel |
CardLayout |
GroupPanel |
org.jdesktop.layout.GroupLayout |
And here's a summary of the F3 Border classes:
Borders:
| F3 Border | Swing Border |
EmptyBorder |
EmptyBorder |
LineBorder |
LineBorder |
BevelBorder |
BevelBorder |
SoftBevelBorder |
SoftBevelBorder |
MatteBorder |
MatteBorder |
TitledBorder |
TitledBorder |
Menus
Let's add a simple menubar to the above example. The new code is shown in bold: import java.lang.System;
class ButtonClickModel {
attribute numClicks: Number;
}
var model = new ButtonClickModel();
Frame {
width: 200
menubar: MenuBar {
menus: Menu {
text: "File"
mnemonic: F
items: MenuItem {
text: "Exit"
mnemonic: X
accelerator: {
modifier: ALT
keyStroke: F4
}
action: operation() {
System.exit(0);
}
}
}
}
content: GridPanel {
border: EmptyBorder {
top: 30
left: 30
bottom: 30
right: 30
}
rows: 2
columns: 1
vgap: 10
cells:
[Button {
text: "I'm a button!"
mnemonic: I
action: operation() {
model.numClicks++;
}
},
Label {
text: bind "Number of button clicks: {model.numClicks}"
}]
}
visible: true
}
After running this program and pressing ALT+F the following is displayed:
As you can see I create a menubar by assigning a new instance of the MenuBar class to the
menubar attribute of the window. I add menus to the menubar by
assigning them to the menubar's menus attribute. In this case I've
only added one menu, but any expression returning a list of Menu objects could be used.
To define the menu I assigned values to its text,
mnemonic, and items attributes.
As you can probably tell the type of the text attribute is
String. The mnemonic attribute, however, is of type KeyStroke. Its value
F is an enumerated instance of the KeyStroke class. In
F3, in the context of an attribute initializer the enumerated values of the
attribute's static type (and static fields in the case of Java classes) can be
accessed without being qualified by the type name (elsewhere I would have to
refer to F as F:KeyStroke).
I've created one menu item, a MenuItem whose
text is "Exit" and whose mnemonic is X.
I've also assigned a value to its accelerator attribute. Note that
I actually omitted the type name Accelerator in the declaration of
the value. This is permitted. If the type name isn't present, the static type of
the attribute is used, in this case Accelerator. In addition,
I've initialized the accelerator's modifier and
keyStroke attributes using enumerations.
Finally, as you can probably tell, the action attribute of the
MenuItem has a function type (i.e its value is a
function, not an object). In this case I've written an inline
operation that simply exits the application by calling some Java
code.
The F3 Label class supports
HTML content. Using Labels you can create styled text and images
with HTML and CSS, very much like you would in a typical Web application. In
addition, by using F3 embedded expressions you can create dynamic HTML content
in a Swing application as easily as Web page authors can using tools like JSTL or Velocity.
Consider this example of a hypothetical shopping cart:
class Item {
attribute id: String;
attribute productId: String;
attribute description: String;
attribute inStock: Boolean;
attribute quantity: Number;
attribute listPrice: Number;
attribute totalCost: Number;
}
attribute Item.totalCost = bind quantity*listPrice;
class Cart {
attribute items: Item*;
attribute subTotal: Number;
}
operation sumItems(itemList:Item*) {
var result = 0.00;
for (item in itemList) {
result += item.totalCost;
}
return result;
}
attribute Cart.subTotal = bind sumItems(items);
var cart = Cart {
items:
[Item {
id: "UGLY"
productId: "D100"
description: "BullDog"
inStock: true
quantity: 1
listPrice: 97.50
},
Item {
id: "BITES"
productId: "D101"
description: "Pit Bull"
inStock: true
quantity: 1
listPrice: 127.50
}]
};
Frame {
content: Label {
text: bind
"<html>
<h2 align='center'>Shopping Cart</h2>
<table align='center' border='0' bgcolor='#008800' cellspacing='2' cellpadding='5'>
<tr bgcolor='#cccccc'>
<td><b>Item ID</b></td>
<td><b>Product ID</b></td>
<td><b>Description</b></td>
<td><b>In Stock?</b></td>
<td><b>Quantity</b></td>
<td><b>List Price</b></td>
<td><b>Total Cost</b></td>
<td> </td>
</tr>
{
if (sizeof cart.items == 0)
then "<tr bgcolor='#FFFF88'><td colspan='8'><b>Your cart is empty.</b></td></tr>"
else foreach (item in cart.items)
"<tr bgcolor='#FFFF88'>
<td>{item.id}</td>
<td>{item.productId}</td>
<td>{item.description}</td>
<td>{if item.inStock then "Yes" else "No"}</td>
<td>{item.quantity}</td>
<td align='right'>{item.listPrice}</td>
<td align='right'>{item.totalCost}</td>
<td> </td>
</tr>"
}
<tr bgcolor='#FFFF88'>
<td colspan='7' align='right'>
<b>Sub Total: ${cart.subTotal}</b>
</td>
<td> </td>
</tr>
</table>
</html>"
}
visible: true
}
Running the above program displays the following:
If I programmatically delete the cart items:
delete cart.items;
I see the following:
In the above example, embedded F3 expressions (shown in bold) dynamically create the rows of an HTML table and the content of the table cells. When the objects those expressions depend on change, the HTML content of the Label is automatically updated.
The above example is also interesting because it demonstrates the use of
expressions to define attribute values. The totalCost attribute of
the Item class and the subTotal attribute of the
Cart class are bound to expressions that compute their values.
Whenever the objects those expressions depend on change, the attribute value is
automatically recomputed and updated. Think of a spreadsheet where some cells
contain formulas that refer to other cells. When you enter data into those other
cells the values of the cells containing formulas that depend on them are
automatically updated.
Images in HTML
The F3 Label class actually
encapsulates a specialized JEditorPane
which uses a shared image cache that supports loading images from JAR files
using the Java class loader. Thus you can use HTML <img>
elements that reference image resources packaged with your application as if
they were normal file URL's.
Hyperlinking
The Label class also
supports HTML hyperlinking by embedding a special URL as the href
attribute of an HTML <a> element.
Such URL's are created with the F3 # operator. This operator
generates a stringified object reference that refers to its operand which
can later be dereferenced by the F3 ? operator. For example,
var a = 20;
var b = #a;
assert b instanceof String; // passes
var c = (Number) ?b;
assert a == c; // passes
The Label class's HTML
renderer recognizes such URLs in HTML <a href=url>
contexts and handles mouse clicks on such elements by deferencing the URL and if
its value refers to a function or operation it calls that function or operation.
For example, here's the previous Button-Click example rewritten to use a Label with a hyperlink instead of a Button:
class ButtonClickModel {
attribute numClicks: Number;
}
var model = new ButtonClickModel();
Frame {
width: 200
content: GridPanel {
border: EmptyBorder {
top: 30
left: 30
bottom: 30
right: 30
}
rows: 2
columns: 1
vgap: 10
cells:
[Label {
text: bind
"<html>
<a href='{#(operation() {model.numClicks++;})}'>
I'm a hyperlink!
</a>
</html>"
},
Label {
text: bind "Number of clicks: {model.numClicks}"
}]
}
visible: true
};
The expression in bold in the above example creates a new
operation that will increment the model's numClicks
attribute. Applying the # operator to that generates a URL that
refers to the operation which is then embedded into the HTML markup.
Running this program displays:
And after clicking the hyperlink twice:
GroupPanel, SimpleLabel, and TextField
This section will use a very simple example to introduce you to the F3 GroupPanel, SimpleLabel, and TextField classes.
The F3 GroupPanel
class encapsulates the Java.net GroupLayout class. GroupLayout is
a powerful layout manager that represents the contents of a panel as sets of
parallel horizontal and vertical groups. In F3 these parallel groups are simply
referred to as Rows and Columns. When you declare a
GroupPanel, you also declare Row and Column objects for each horizontal or
vertical grouping of components. Then when you add components, you assign the
appropriate Row and Column objects to the component's row and
column attributes. GroupPanel automatically inserts gaps between
components as defined by the current look-and-feel's style guidelines. By
declaring values for the alignment and resizable
attributes of a Row or Column object you can control the alignment of components
within that row or column, and whether the row or column can be resized.
The F3 TextField class
encapsulates the Swing JFormattedTextField
class. It has a value attribute that is updated whenever the user
presses Enter while the focus is on the text field or when the focus
moves to another component. You can control its width by assigning a numeric
value to its columns attribute, and its horizontal alignment by
assigning LEADING, CENTER, or TRAILING to
its horizontalAligment attribute. Finally, two
function-valued attributes are provided that allow you to perform
actions based on user interaction: action and
onChange. If you assign a function or operation to the
action attribute, your function will be called whenever the user
presses the Enter key. If you assign a function or operation to the
onChange attribute, your function or operation will be called
whenever the text field's value changes.
The F3 SimpleLabel class encapsulates the Swing JLabel class. SimpleLabel differs from Label in that it doesn't support hyperlinking and its preferred size is calculated differently.
Here what the example will look like:
And here's the code to create it:
class Model {
attribute firstName: String;
attribute lastName: String;
}
var model = Model {
firstName: "Joe"
lastName: "Smith"
};
Frame {
content: GroupPanel {
var firstNameRow = Row { alignment: BASELINE }
var lastNameRow = Row { alignment: BASELINE }
var labelsColumn = Column {
alignment: TRAILING
}
var fieldsColumn = Column {
alignment: LEADING
resizable: true
}
rows: [firstNameRow, lastNameRow]
columns: [labelsColumn, fieldsColumn]
content:
[SimpleLabel {
row: firstNameRow
column: labelsColumn
text: "First Name:"
},
TextField {
row: firstNameRow
column: fieldsColumn
columns: 25
value: bind model.firstName
},
SimpleLabel {
row: lastNameRow
column: labelsColumn
text: "Last Name:"
},
TextField {
row: lastNameRow
column: fieldsColumn
columns: 25
value: bind model.lastName
}]
}
visible: true
};
In the above example the code related to layout is in blue. This example
consists of two rows (one for the first name and one for the last name) and two
columns (one for the labels and one for the text fields). In the declaration of
the GroupPanel I've declared four variables (firstNameRow,
lastNameRow, labelsColumn, and
fieldsColumn) for the rows and columns. Then I assigned the two
Row's to the GroupPanel's rows attribute and the two Column's to
its columns attribute. Finally, as you can see, I added the labels
and text fields by assigning them to the GroupPanel's elements
attribute, and, as I declared the labels and text fields, I assigned their
row and column attributes appropriately.
Buttons
The F3 Button class encapsulates the Swing JButton component. To introduce you to the use of Buttons, I'll recreate a simple example from the Swing tutorial, which looks like this
class ButtonDemoModel {
attribute buttonEnabled: Boolean;
}
var model = ButtonDemoModel {
buttonEnabled: true
};
Frame {
title: "ButtonDemo"
content: FlowPanel {
content:
[Button {
text: "Disable middle button"
verticalTextPosition: CENTER
horizontalTextPosition: LEADING
icon: Image {
url: "http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/right.gif"
}
mnemonic: D
toolTipText: "Click this button to disable the middle button"
enabled: bind model.buttonEnabled
action: operation() {
model.buttonEnabled = false;
}
},
Button {
text: "Middle button"
icon: Image {
url: "http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/middle.gif"
}
verticalTextPosition: BOTTOM
horizontalTextPosition: CENTER
mnemonic: M
toolTipText: "This middle button does nothing when you click it."
enabled: bind model.buttonEnabled
},
Button {
text: "Enable middle button"
icon: Image {
url: "http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/left.gif"
}
mnemonic: E
toolTipText: "Click this button to enable the middle button"
action: operation() {
model.buttonEnabled = true;
}
enabled: bind not model.buttonEnabled
}]
}
visible: true
}
After clicking the left button, the example looks like this:
The enabled attribute of each of the three buttons is bound to
the buttonEnabled attribute of the model object. So when I modify
this attribute in the actions of the left and right buttons they
all reflect the change.
I've added images to the buttons by assigning Image objects to their
icon attributes.
F3 Image objects have a url attribute to which you
can assign a String containing a URL that points at the desired
image resource. F3 has an internal image cache that supports loading images from
JAR files using the Java class loader. As a result image resources packaged in
JAR files accessible to the class loader can be accessed by simply using
file: URLs.
TabbedPanes
To demonstrate the use of TabbedPane's, I'll define a model class that has attributes corresponding to those of the TabbedPane widget: class Model {
attribute tabPlacement: TabPlacement;
attribute tabLayout: TabLayout;
attribute tabCount: Integer;
attribute selectedTab: Integer;
}
and then I'll project an instance of of TabbedPane from an instance of this model:
var model = Model {
tabPlacement: TOP
tabLayout: WRAP
selectedTab: 3
tabCount: 5
};
Frame {
height: 300
width: 400
content: TabbedPane {
tabPlacement: bind model.tabPlacement
tabLayout: bind model.tabLayout
tabs: bind foreach (i in [1..model.tabCount])
Tab {
title: "Tab {i}"
toolTipText: "Tooltip {i}"
}
selectedIndex: bind model.selectedTab
}
visible: true
}
The code in bold shows the dependencies between the TabbedPane and the model. Having done this I can now change the appearance of the TabbedPane by modifying the model.
Tabs are added to a TabbedPane by assigning a list of Tab objects to its
tabs attribute. The TabPlacement and TabLayout classes define
enumerations (TOP, LEFT, BOTTOM, RIGHT and WRAP or SCROLL) that allow you to
control the location and layout of the tabs by assigning appropriate values to
the TabbedPane's tabPlacement and tabLayout
attributes. The TabbedPane's selectedIndex attribute determines
which tab's content is visible.
Running the above program displays the following:
Notice that the forth tab is selected. This is due to the fact that I
initialized the model's selectedTab attribute to the (zero-based)
index 3. Because in this example the TabbedPane's
selectedIndex attribute is bound to the value of
selectedTab, it is also updated, causing that tab to be selected.
If I programmatically change the model:
model.tabPlacement = BOTTOM;
the tabs move to the bottom:
Executing:
model.selectedTab = 0;
causes the first tab to become the selected tab:
Executing:
model.tabCount = 20;
causes 15 new tabs to be created and added to the tabbed pane:
Executing:
model.tabLayout = SCROLL;
shows:
And executing:
model.tabCount = 2;
causes all but the first two tabs to be removed:
ListBoxes
The F3 ListBox class provides the functionality of the Swing JList component but with a declarative interface.
As a demonstration, I'll recreate a simple example from the Swing tutorial (ListDemo) that initially looks like this:
In this example the ListBox contains a list of employee names. If I click the "Fire" button the selected employee is removed from the list. If I enter a new name into the TextField below the list, the "Hire" button becomes enabled, and if I click it, that name is added to the list. This example also demonstrates the use of BorderPanel and FlowPanel. A BorderPanel contains up to 5 components which can be placed at the top, left, bottom, right, or center of the panel. It stretches the left and right components vertically, the top and bottom components horizontally, and the center component in both directions. A FlowPanel contains a list of components which it lays out in a left to right flow, much like text in a paragraph. This example also shows the use of RigidArea, an invisible filler component which can be used to create space between other components.
class EmployeeModel {
attribute employees: String*;
attribute selectedEmployee: Number;
attribute newHireName: String;
}
var model = EmployeeModel {
employees:
["Alan Sommerer",
"Alison Huml",
"Kathy Walrath",
"Lisa Friendly",
"Mary Campione",
"Sharon Zakhour"]
};
Frame {
title: "ListBox Example"
content: BorderPanel {
center: ListBox {
selection: bind model.selectedEmployee
cells: bind foreach (emp in model.employees)
ListCell {
text: emp
}
}
bottom: FlowPanel {
content:
[Button {
text: "Fire"
action: operation() {
delete model.employees[model.selectedEmployee];
}
},
RigidArea {
width: 5
},
TextField {
columns: 30
value: bind model.newHireName
},
RigidArea {
width: 5
},
Button {
text: "Hire"
enabled: bind model.newHireName.length() > 0
action: operation() {
insert model.newHireName
after model.employees[model.selectedEmployee];
model.newHireName = "";
if (sizeof model.employees == 1) {
model.selectedEmployee = 0;
} else {
model.selectedEmployee++;
}
}
}]
}
}
visible: true
}
The code to create the ListBox is in bold. As you can see, I've created the
ListBox by assigning a list of ListCell objects to its
cells attribute. In this case the list of cells is projected from
the list of employees in the model. So as employees are added to or deleted from
the model the corresponding cells will be added to or deleted from the ListBox.
Whatever value you assign to a ListCell's text attribute will be
displayed when the cell is rendered. Although not required in this example, you
can also assign HTML markup to a ListCell's text attribute to
create styled text and/or images as the content of the cell.
The ListBox's selection attribute contains the indices of the
list's selected cells. In this case I've bound it to the model's
selectedEmployee attribute, so as I move the selection in the list,
the model's selectedEmployee will be updated. At the same time, if
I update the selectedEmployee attribute (as I do above in the
"Hire" button's action) the list's selection will reflect my
change.
After clicking "Fire" twice I see:
If I enter a new name in the text field:
SplitPanes
The F3 SplitPane is based on a custom Java component rather than the Swing JSplitPane class. Unlike JSplitPane it can contain multiple components. Like JSplitPane you can control its orientation and the amount of space assigned to each of its contained components. Let's look at an example, which will look like this:
This example consists of a horizontal split pane with two components. The left component is a ListBox which occupies 30% of the available space. The right component which occupies the remaining 70% of the space is actually a ScrollPane containing a CenterPanel (a panel that contains one component which it centers in its content area). The CenterPanel, in turn, contains an ImagePanel that displays the image associated with the selected item in the list box.
class ExampleModel {
attribute imageFiles: String*;
attribute selectedImageIndex: Number;
attribute selectedImageUrl: String;
}
var model = ExampleModel {
var: self
imageFiles: ["Bird.gif", "Cat.gif", "Dog.gif",
"Rabbit.gif", "Pig.gif", "dukeWaveRed.gif",
"kathyCosmo.gif", "lainesTongue.gif",
"left.gif", "middle.gif", "right.gif",
"stickerface.gif"]
selectedImageUrl: bind "http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/{self.imageFiles[self.selectedImageIndex]}"
};
Frame {
title: "SplitPane Example"
height: 400
width: 500
content: SplitPane {
orientation: HORIZONTAL
content:
[SplitView {
weight: 0.30
content: ListBox {
selection: bind model.selectedImageIndex
cells: bind foreach (file in model.imageFiles)
ListCell {
text: bind file
}
}
},
SplitView {
weight: 0.70
content: ScrollPane {
view: CenterPanel {
background: white
content: ImagePanel {
url: bind model.selectedImageUrl
}
}
}
}]
}
visible: true
}
The code related to the split pane is in bold. As you can see I've made this
a horizontal split pane by assigning HORIZONTAL to its
orientation attribute. I've added components to the SplitPane by
assigning a list of SplitView objects to its
content attribute. Each SplitView has two attributes,
weight and content. The weight attribute
determines how much of the available space is given to that panel when the
overall split pane is resized. Whatever component you assign to its
content attribute will be displayed inside that section of the
split pane.
RadioButton, RadioButtonMenuItem, ToggleButton, and ButtonGroup
The F3 RadioButton class encapsulates the Swing JRadioButton component. The F3 RadioButtonMenuItem class encapsulates the Swing JRadioButtonMenuItem component. The F3 ToggleButton class encapsulates the Swing JToggleButton component.These components have a strong similarity with each other, and with single selection ListBox's, ComboBox's, TabbedPane's, and CardPanel's, namely that all of these components allow you to "pick" one item from a list of items.
RadioButtons, RadioButtonMenuItems, and ToggleButtons are associated with a
list of items by means of the F3 ButtonGroup class (which as
you might expect corresponds to the Swing ButtonGroup).
Unlike the Swing class however, the F3 ButtonGroup also provides a selection
model similar to a (single-selection) ListBox. ButtonGroup's
selection attribute holds a numeric index that controls which
button among the buttons it contains is the selected one. If you assign a value
to this attribute, the button at that index will become selected, and any other
buttons will be deselected. Likewise, if the user selects a particular button,
the index of that button will be implicitly assigned to the ButtonGroup's
selection.
To demonstrate this I'll extend the example from the previous section to
include three button groups. The first will be associated with a list of
RadioButtonMenuItems in a menu. The second will be associated with a list of
RadioButtons placed into a four column GridPanel. Finally, the third will be
associated with a list of ToggleButtons placed into a single column GridPanel.
Each group of buttons will be projected from the same model objects as were the
ListBox's cells in the original example, and the
selection attribute of their ButtonGroup's will be bound to the
same model attribute as the ListBox's selection was in the
original. The result is that whatever selection you make in the ListBox, the
menu, the radio buttons, or the toggle buttons will be reflected in all the
others.
Here's what it looks like initially:
class ExampleModel {
attribute imageFiles: String*;
attribute selectedImageIndex: Number;
attribute selectedImageUrl: String;
}
var model = ExampleModel {
var: self
imageFiles: ["Bird.gif", "Cat.gif", "Dog.gif",
"Rabbit.gif", "Pig.gif", "dukeWaveRed.gif",
"kathyCosmo.gif", "lainesTongue.gif",
"left.gif", "middle.gif", "right.gif",
"stickerface.gif"]
selectedImageUrl: bind
"http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/{self.imageFiles[self.selectedImageIndex]}"
};
Frame {
menubar: MenuBar {
menus: Menu {
text: "File"
mnemonic: F
var buttonGroup = ButtonGroup {
selection: bind model.selectedImageIndex
}
items: foreach (imageName in model.imageFiles)
RadioButtonMenuItem {
buttonGroup: buttonGroup
text: imageName
}
}
}
title: "RadioButton/ToggleButton Example"
height: 400
width: 500
content: BorderPanel {
top: GridPanel {
rows: sizeof model.imageFiles / 4
columns: sizeof model.imageFiles % 4
var buttonGroup = ButtonGroup {
selection: bind model.selectedImageIndex
}
cells: foreach (imageName in model.imageFiles)
RadioButton {
buttonGroup: buttonGroup
text: imageName
}
}
right: GridPanel {
rows: sizeof model.imageFiles
columns: 1
var buttonGroup = ButtonGroup {
selection: bind model.selectedImageIndex
}
cells: foreach (imageName in model.imageFiles)
ToggleButton {
buttonGroup: buttonGroup
text: imageName
}
}
center: SplitPane {
orientation: HORIZONTAL
panels:
[SplitPanel {
weight: 0.30
content: ListBox {
selection: bind model.selectedImageIndex
cells: bind foreach (imageName in model.imageFiles)
ListCell {
text: bind imageName
}
}
},
SplitPanel {
weight: 0.70
content: ScrollPane {
view: CenterPanel {
background: white
content: ImagePanel {
url: bind model.selectedImageUrl
}
}
}
}]
}
}
visible: true
}
The code related to the ButtonGroups is in orange. As you can see you add
buttons to a button group by setting the button's buttonGroup
attribute to the desired ButtonGroup.
If I click on "Pig.gif" in the list box (or on the "Pig.gif" radio button, or on the "Pig.gif" toggle button) I see the following:
If I open the menu you can see that it also reflects the same selection:
ComboBoxes
The F3 ComboBox corresponds to the Swing JComboBox component. To demonstrate the use of ComboBoxes I'll add two of them to the previous example. The example will now look like this:
class ExampleModel {
attribute imageFiles: String*;
attribute selectedImageIndex: Number;
attribute selectedImageUrl: String;
}
var model = ExampleModel {
var: self
imageFiles: ["Bird.gif", "Cat.gif", "Dog.gif",
"Rabbit.gif", "Pig.gif", "dukeWaveRed.gif",
"kathyCosmo.gif", "lainesTongue.gif",
"left.gif", "middle.gif", "right.gif",
"stickerface.gif"]
selectedImageUrl: bind "http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/{self.imageFiles[self.selectedImageIndex]}"
};
Frame {
menubar: MenuBar {
menus: Menu {
text: "File"
mnemonic: F
var buttonGroup = ButtonGroup {
selection: bind model.selectedImageIndex
}
function makeRadioButton(buttonGroup, imageName) {
return RadioButtonMenuItem {
buttonGroup: buttonGroup
text: imageName
};
}
items: foreach (imageName in model.imageFiles)
makeRadioButton(buttonGroup, imageName)
}
}
title: "RadioButton/ToggleButton/ComboBox Example"
height: 400
width: 500
content: BorderPanel {
top: GridPanel {
rows: sizeof model.imageFiles / 4
columns: sizeof model.imageFiles % 4
var buttonGroup = ButtonGroup {
selection: bind model.selectedImageIndex
}
cells: foreach (imageName in model.imageFiles)
RadioButton {
buttonGroup: buttonGroup
text: imageName
}
}
right: GridPanel {
rows: sizeof model.imageFiles
columns: 1
var buttonGroup = ButtonGroup {
selection: bind model.selectedImageIndex
}
cells: foreach (imageName in model.imageFiles)
ToggleButton {
buttonGroup: buttonGroup
text: imageName
}
}
center: SplitPane {
orientation: HORIZONTAL
panels:
[SplitPanel {
weight: 0.30
content: ListBox {
selection: bind model.selectedImageIndex
cells: bind foreach (imageName in model.imageFiles)
ListCell {
text: bind imageName
}
}
},
SplitPanel {
weight: 0.70
content: BorderPanel {
top: ComboBox {
selection: bind model.selectedImageIndex
cells: bind foreach (imageName in model.imageFiles)
ComboBoxCell {
text: bind imageName
}
}
center: ScrollPane {
view: CenterPanel {
background: white
content: ImagePanel {
url: bind model.selectedImageUrl
}
}
}
}
}]
}
bottom: FlowPanel {
alignment: LEADING
content: ComboBox {
selection: bind model.selectedImageIndex
cells: bind foreach (imageName in model.imageFiles)
ComboBoxCell {
text: bind "<html>
<table>
<tr>
<td>
<img src='http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/{imageName}' height='32' width='32'></img>
</td>
<td>
{imageName}
</td>
</tr>
</table>
</html>"
}
}
}
}
visible: true
}
The code related to the ComboBoxes is in bold. You specify the content of a
ComboBox's drop-down list by assigning a list of ComboBoxCell's to its
cells attribute. The ComboBoxCell's text
attribute determines the appearance of the cell. You can create styled text
and/or images as the content of a cell by assigning HTML content to its
text attribute (as in the ComboBox in the lower left corner of the
window in this example). The ComboBox's numeric
selection attribute determines the selected cell. Programmatically
assigning a (zero-based) integer index to this attribute causes the cell at that
position to be selected. At the same time when the user selects a cell, the
index of the cell will be implicitly assigned to selection. In this
example I've bound the selection attribute of both ComboBoxes to
the same model attribute as the button groups and listbox in the original
example, and I've also projected the list of ComboBox cells from
the same model attribute as the button groups and the listbox. As a result, you
can select the image to be displayed through either ComboBox as well as through
the listbox or button groups.
If I open the second ComboBox, the example looks like this:
Trees
The F3 Tree class provides a declarative interface encapsulating the Swing JTree component. To introduce you to the use ofTrees, let's first create
a very simple example tree with no dynamic behavior:
Frame {
height: 400
width: 300
content: Tree {
root: TreeCell {
text: "Tree"
cells:
[TreeCell {
text: "colors"
cells:
[TreeCell {
text: "<html><font color='blue'>blue</font></html>"
},
TreeCell {
text: "<html><font color='red'>red</font></html>"
},
TreeCell {
text: "<html><font color='green'>green</font></html>"
}]
},
TreeCell {
text: "food"
cells:
[TreeCell {
text: "hot dogs"
},
TreeCell {
text: "pizza"
},
TreeCell {
text: "ravioli"
}]
}]
}
}
visible: true
}
Here's what it looks like when I run it:
To populate a Tree I assign an expression returning a TreeCell object to its
root attribute. A TreeCell represents one row in the tree. You
specify the child cells of a TreeCell by assigning a list of TreeCell objects to
its cells attribute. In addition, a TreeCell has a
text attribute which determines its visual appearance. As you can
see, you can assign HTML content to text to create styled text
and/or images as the content of a tree cell.
Next I'll recreate one of the Swing tutorial demos (called GenealogyExample) that displays the descendants or ancestors of a particular person.
When you run it, this example progam initially looks like this:
If I select a person in the tree, and then click one of the radio buttons, the selected person becomes the root of the tree, and depending on my selection, the ancestors or descendants of the person are shown as sub-nodes in the tree.
Below is the code for this example. The code related to the tree itself is in
bold. TreeCell has a Boolean selected attribute that
is set by the tree implementation as the user selects cells in the tree. At the
same time if you programatically assign a value to this attribute, the
corresponding tree cell will be selected or unselected depending on the value.
In this case the geneology of a person is a recursive data structure, so I've
defined the TreeCell's cells using an expression that makes
recursive function calls. Notice that I've used the bind lazy
operator rather than just the bind operator in the initialization
of the cells attribute, which indicates lazy evaluation.
This means that its right hand side is not evaluated until its left hand side is
accessed the first time. As a result the recursive calls to the
descendantTree() and ancestorTree() functions are not
actually performed until you expand a node in the tree and the tree requires
access to the node's child cells.
class GeneologyModel {
attribute people: Person*;
attribute selectedPerson: Person;
attribute showDescendants: Boolean;
}
class Person {
attribute selected: Boolean;
attribute father: Person;
attribute mother: Person;
attribute children: Person*;
attribute name: String;
}
// By defining these triggers I can populate the model
// by just assigning the mother and father attributes of a Person
trigger on Person.father = father {
insert this into father.children;
}
trigger on Person.mother = mother {
insert this into mother.children;
}
// Create and populate the model
var model = GeneologyModel {
var jack = Person {
selected: true
name: "Jack (great-granddaddy)"
}
var jean = Person {
name: "Jean (great-granny)"
}
var albert = Person {
name: "Albert (great-granddaddy)"
}
var rae = Person {
name: "Rae (great-granny)"
}
var paul = Person {
name: "Paul (great-granddaddy)"
}
var josie = Person {
name: "Josie (great-granny)"
}
var peter = Person {
father: jack
mother: jean
name: "Peter (grandpa)"
}
var zoe = Person {
father: jack
mother: jean
name: "Zoe (grandma)"
}
var simon = Person {
father: jack
mother: jean
name: "Simon (grandpa)"
}
var james = Person {
father: jack
mother: jean
name: "James (grandpa)"
}
var bertha = Person {
father: albert
mother: rae
name: "Bertha (grandma)"
}
var veronica = Person {
father: albert
mother: rae
name: "Veronica (grandma)"
}
var anne = Person {
father: albert
mother: rae
name: "Anne (grandma)"
}
var renee = Person {
father: albert
mother: rae
name: "Renee (grandma)"
}
var joseph = Person {
father: paul
mother: josie
name: "Joseph (grandpa)"
}
var isabelle = Person {
father: simon
mother: veronica
name: "Isabelle (mom)"
}
var frank = Person {
father: simon
mother: veronica
name: "Frank (dad)"
}
var louis = Person {
father: simon
mother: veronica
name: "Louis (dad)"
}
var laurence = Person {
father: james
mother: bertha
name: "Laurence (dad)"
}
var valerie = Person {
father: james
mother: bertha
name: "Valerie (mom)"
}
var marie = Person {
father: james
mother: bertha
name: "Marie (mom)"
}
var helen = Person {
father: joseph
mother: renee
name: "Helen (mom)"
}
var mark = Person {
father: joseph
mother: renee
name: "Mark (dad)"
}
var oliver = Person {
father: joseph
mother: renee
name: "Oliver (dad)"
}
var clement = Person {
father: laurence
mother: helen
name: "Clement (boy)"
}
var colin = Person {
father: laurence
mother: helen
name: "Colin (boy)"
}
people: [jack, jean, albert, rae, paul, josie,
peter, zoe, simon, james, bertha, anne,
renee, joseph, frank, louis, laurence,
valerie, marie, helen, mark, oliver,
clement, colin]
selectedPerson: jack
showDescendants: true
};
// Tree generation functions:
operation geneologyTree(p:Person, showDescendants:Boolean) {
if (showDescendants) {
return descendantTree(p);
} else {
return ancestorTree(p);
}
}
function descendantTree(p:Person) {
return TreeCell {
selected: bind p.selected
text: bind p.name
cells:
bind lazy
foreach (c in p.children)
descendantTree(c)
};
}
function ancestorTree(p:Person) {
return TreeCell {
selected: bind p.selected
text: bind p.name
cells:
bind lazy
foreach (a in [p.father, p.mother])
ancestorTree(a)
};
}
Frame {
title: "Genology Example"
height: 300
width: 300
content: BorderPanel {
top: FlowPanel {
var buttonGroup = new ButtonGroup()
content:
[RadioButton {
buttonGroup: buttonGroup
text: "Show Descendants"
selected: model.showDescendants
onChange: operation(newValue:Boolean) {
if (newValue) {
var selectedPerson = model.people[selected];
if (selectedPerson <> null) {
model.selectedPerson = selectedPerson;
}
model.showDescendants = true;
}
}
},
RadioButton {
buttonGroup: buttonGroup
text: "Show Ancestors"
onChange: operation(newValue:Boolean) {
if (newValue) {
var selectedPerson = model.people[selected];
if (selectedPerson <> null) {
model.selectedPerson = selectedPerson;
}
model.showDescendants = false;
}
}
}]
}
center: Tree {
showRootHandles: true
root: bind geneologyTree(model.selectedPerson,
model.showDescendants)
}
}
visible: true
}
If I expand all the nodes and select "Clement" the tree looks like this:
After clicking on "Show Ancestors", Clement becomes the root, and his parents are shown beneath him:
Tables
The F3 Table class
encapsulates the Swing JTable
component. To demonstrate the use of Tables, I'll create a slight
variation of one of the Swing tutorial examples (SimpleTableDemo) that looks
like this:
class Person {
attribute firstName: String;
attribute lastName: String;
attribute sport: String;
attribute numYears: Number;
attribute vegetarian: Boolean;
attribute selected: Boolean;
}
class TableDemoModel {
attribute people: Person*;
}
var model = TableDemoModel {
people:
[Person {
firstName: "Mary"
lastName: "Campione"
sport: "Snowboarding"
numYears: 5
vegetarian: false
},
Person {
firstName: "Alison"
lastName: "Huml"
sport: "Rowing"
numYears: 3
vegetarian: true
},
Person {
firstName: "Kathy"
lastName: "Walrath"
sport: "Knitting"
numYears: 2
vegetarian: false
},
Person {
firstName: "Sharon"
lastName: "Zakhour"
sport: "Speed reading"
numYears: 20
vegetarian: true
},
Person {
firstName: "Philip"
lastName: "Milne"
sport: "Pool"
numYears: 10
vegetarian: false
}]
};
Frame {
height: 120
width: 500
title: "SimpleTableDemo"
content: Table {
columns:
[TableColumn {
text: "First Name"
},
TableColumn {
text: "Last Name"
},
TableColumn {
text: "Sport"
width: 100
},
TableColumn {
text: "# of Years"
alignment: TRAILING
},
TableColumn {
text: "Vegetarian"
alignment: CENTER
}]
cells: bind foreach (p in model.people)
[TableCell {
text:bind p.firstName
selected: bind p.selected
},
TableCell {
text:bind p.lastName
},
TableCell {
text: bind p.sport
},
TableCell {
text: bind "{p.numYears}"
},
TableCell {
text: bind if p.vegetarian then "Yes" else "No"
toolTipText: bind "{p.firstName} {p.lastName} {if not p.vegetarian then "eats" else "does not eat"} meat"
}]
}
visible: true
}
The code related to the table is in bold. To create the Table
I've assigned a list of TableColumn objects to its
columns attribute, and a list of TableCell objects to its
cells attribute. Since in this case I assigned five
TableColumn objects, this table will have five columns. Since I
also assigned five TableCell objects for each person, there will be
one row for each person. The TableColumn's text
attribute determines the content of the column's header cell. Its
width and alignment attributes determine the column's
preferred width and horizontal alignment.
The F3 Table is a ScrollableWidget so
you don't have to explicitly add it to a scroll pane.
Text Components
To demonstrate the use of text components in F3, I'll create a slight variation of the text sampler demo from the Swing tutorial, which looks like this:
The F3 text components in this example correspond to Swing components as follows:
| F3 Widget | Swing Component |
TextField |
JFormattedTextField |
PasswordField |
JPasswordField |
TextArea |
JTextArea |
EditorPane |
JEditorPane |
TextPane |
JTextPane |
class TextSamplerModel {
attribute textFieldInput: String?;
}
var model = TextSamplerModel {
};
Frame {
title: "Text Sampler"
visible: true
content: SplitPane {
orientation: HORIZONTAL
panels:
[SplitPanel {
weight: 0.5
content:
BorderPanel {
top: GridBagPanel {
border: CompoundBorder {
borders:
[TitledBorder {
title: "Text Fields"
},
EmptyBorder {
top: 5
left: 5
bottom: 5
right: 5
}]
}
cells:
[GridCell {
anchor: EAST
gridx: 0
gridy: 0
content: SimpleLabel {
text: "TextField: "
}
},
GridCell {
anchor: WEST
fill: HORIZONTAL
weightx: 1
gridx: 1
gridy: 0
content: TextField {
action: operation(value:String) {
model.textFieldInput = value;
}
}
},
GridCell {
anchor: EAST
gridx: 0
gridy: 1
insets: {top: 2}
content: SimpleLabel {
text: "PasswordField: "
}
},
GridCell {
gridx: 1
gridy: 1
fill: HORIZONTAL
weightx: 1
insets: {top: 2}
content: PasswordField {
action: operation(value:String) {
model.textFieldInput = value;
}
}
},
GridCell {
anchor: WEST
weightx: 1.0
gridx: 0
gridy: 2
gridwidth: 2
fill: HORIZONTAL
content: SimpleLabel {
border: EmptyBorder {
top: 10
}
text: bind if model.textFieldInput == null then "Type text and then Return in a field" else "You typed \"{model.textFieldInput}\""
}
}]
}
center: BorderPanel {
border: CompoundBorder {
borders:
[TitledBorder {
title: "Plain Text"
},
EmptyBorder {
top: 5
left: 5
bottom: 5
right: 5
}]
}
center: TextArea {
font: new Font("Serif", Font.ITALIC, 16)
lineWrap: true
wrapStyleWord: true
text: "This is an editable TextArea that has been initialized with its text attribute. A text area is a \"plain\" text component, which means that although it can display text in any font, all of the text is in the same font"
}
}
}
},
SplitPanel {
weight: 0.5
content: SplitPane {
border: CompoundBorder {
borders:
[TitledBorder {
title: "Styled Text"
},
EmptyBorder {
top: 5
left: 5
bottom: 5
right: 5
}]
}
orientation: VERTICAL
panels:
[SplitPanel {
weight: 0.5
content: EditorPane {
opaque: true
preferredSize: {height: 250 width: 250}
contentType: HTML
editable: false
text: "<html>
<body>
<img src='http://java.sun.com/docs/books/tutorial/uiswing/components/example-1dot4/images/dukeWaveRed.gif' width='64' height='64'>
This is an uneditable <code>EditorPane</code>,
which was <em>initialized</em>
with <strong>HTML</strong> text <font size='-2'>but not from</font> a
<font size='+2'>URL</font>.
<p>
An editor pane uses specialized editor kits
to read, write, display, and edit text of
different formats.
</p>
<p>
The Swing text package includes editor kits
for plain text, HTML, and RTF.
</p>
<p>
You can also develop
custom editor kits for other formats.
</p>
</body></html>"
}
},
SplitPanel {
weight: 0.5
content: TextPane {
preferredSize: {height: 250 width: 250}
editable: true
content:
["This is an editable TextPane, another styled text component, which supports embedded icons...\n",
Image {url: "http://java.sun.com/docs/books/tutorial/uiswing/components/example-swing/images/Pig.gif"},
"\n...and embedded components...\n",
Button {
contentAreaFilled: false
icon: Image {url: "http://java.sun.com/docs/books/tutorial/uiswing/components/example-swing/images/sound.gif"}
},
"\nTextPane is a subclass of EditorPane that uses a StyledEditorKit and StyledDocument,\n and provides cover methods for interacting with those objects."]
}
}]
}
}]
}
}
Spinners and Sliders
The F3 Spinner and Slider classes correspond to Swing components as follows:
| F3 Widget | Swing Component |
Spinner |
JSpinner |
Slider |
JSlider |
To demonstrate their use I'll create a simple application that shows the temperature on both celsius and farenheit scales.
When I run it the example initially looks like this:
class Temp {
attribute celsius: Number;
attribute farenheit: Number;
attribute showCelsius: Boolean;
attribute showFarenheit: Boolean;
}
trigger on Temp.celsius = value {
farenheit = (9/5 * celsius + 32);
}
trigger on Temp.farenheit = value {
celsius = ((farenheit - 32) * 5/9);
}
Frame {
var temp = Temp {
farenheit: 32
showFarenheit: true
showCelsius: true
}
height: 300
width: 400
title: "Temperature"
content: Box {
orientation: VERTICAL
content:
[FlowPanel {
content:
[CheckBox {
text: "Show Celsius"
selected: bind temp.showCelsius
},
RigidArea {
width: 20
},
CheckBox {
text: "Show Farenheit"
selected: bind temp.showFarenheit
}]
},
Slider {
visible: bind temp.showCelsius
min: -100
max: 100
border: TitledBorder {title: "Celsius"}
value: bind temp.celsius
minorTickSpacing: 5
majorTickSpacing: 10
paintTicks: true
paintLabels: true
labels:
[SliderLabel {
value: 0
label: SimpleLabel {
text: "0"
}
},
SliderLabel {
value: 100
label: SimpleLabel {
text: "100"
}
}]
},
Slider {
visible: bind temp.showFarenheit
border: TitledBorder {title: "Farenheit"}
min: -148
max: 212
paintTicks: true
minorTickSpacing: 5
majorTickSpacing: 10
value: bind temp.farenheit
paintLabels: true
labels:
[SliderLabel {
value: 0
label: SimpleLabel {
text: "0"
}
},
SliderLabel {
value: 32
label: SimpleLabel {
text: "32"
}
},
SliderLabel {
value: 212
label: SimpleLabel {
text: "212"
}
}]
},
FlowPanel {
alignment: LEADING
content:
[SimpleLabel {
visible: bind temp.showCelsius
alignmentX: 1
text: "Celsius:"
},
Spinner {
visible: bind temp.showCelsius
min: -100
max: 100
value: bind temp.celsius
},
RigidArea {
width: 20
},
SimpleLabel {
visible: bind temp.showFarenheit
alignmentX: 1
text: "Farenheit:"
},
Spinner {
visible: bind temp.showFarenheit
min: -148
max: 212
value: bind temp.farenheit
}]
}]
}
visible: true
}
The code related to the Spinners and Sliders is in bold. Both Spinners and
Sliders have min and max attributes that determine
their ranges and a value attribute that determines the current
value.
In this example I've bound the value attributes of the celsius
Spinner and Slider to the model's celsius attribute, and I've bound
the value attributes of the farenheit Spinner and Slider to the
model's farenheit attribute. I've also defined triggers on the
model's celsius and farenheit attributes to update the
other attribute's value whenever one of them changes. As a result if I move
either slider or change either spinner, all of the others will reflect this
change.
For example, here's what it looks like if I set the temperature to 88 degrees farenheit:
Sliders also have several attributes that determine if and how tick lines
should be displayed. In addition, by assigning a list of SliderLabels to a Slider's
labels attribute you can attach labels to particular values. In
this example I've done that for the freezing and boiling points and for 0
degrees farenheit.