JavaFX Applets Meet Google Chrome

In the JFX Custom Nodes category of this blog, graphics designer Mark Dingman of Malden Labs and I have been collaborating on an imaginary Sound Beans application. This category contains a growing series of posts in which we are demonstrating how to create JavaFX UI custom controls. This series also provide a case study in how a graphics designer and an application developer can work together effectively in developing JavaFX applications. Today I'd like to highlight the recent Google Chrome browser announcement by showing you how to create and run a JavaFX applet in Chrome. Here's a screenshot of the TableNode example from an earlier post running as a JavaFX applet in Chrome:


TableNodeExampleApplet

To try this out, first obtain Google Chrome and install it. Then obtain Java SE 6 Update 10 and install it as well. By the way, installing Java SE 6 update 10 will enable this JavaFX applet to run on Firefox 3 and Internet Explorer as well. Go ahead and run this example, being sure to scroll the custom TableNode control and to click on its rows. Also, select the Burn icon and move the slider to demonstrate the custom ProgressNode control.


Looking at the Code

In addition to the ButtonNode.fx, MenuNode.fx, DeckNode.fx, ProgressNode.fx and TableNode.fx files from previous posts in this series, you'll need the following files:

TableNodeExampleApplet.fx:

/*
* TableNodeExampleApplet.fx -
* An example of using the TableNode custom node in an Applet. It also
* demonstrates the ProgressNode, DeckNode, MenuNode and ButtonNode
* custom nodes
*
* Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
* to demonstrate how to create custom nodes and applets in JavaFX
*/
package com.javafxpert.table_node_example.ui;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.Object;
import java.lang.System;
import com.javafxpert.custom_node.DeckNode;
import com.javafxpert.custom_node.TableNode;
import com.javafxpert.custom_node.ProgressNode;
import com.javafxpert.custom_node.ButtonNode;
import com.javafxpert.custom_node.MenuNode;
import com.javafxpert.table_node_example.model.TableNodeExampleModel;

var deckRef:DeckNode;

Application {
var model = TableNodeExampleModel.getInstance();
var stageRef:Stage;
var menuRef:MenuNode;
stage:
stageRef = Stage {
fill: Color.BLACK
content: [
deckRef = DeckNode {
fadeInDur: 700ms
content: [
// The "Splash" page
Group {
var vboxRef:VBox;
var splashFont =
Font {
name: "Sans serif"
style: FontStyle.BOLD
size: 12
};
id: "Splash"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/splashpage.png"
}
},
vboxRef = VBox {
translateX: bind stageRef.width - vboxRef.getWidth() - 10
translateY: 215
spacing: 1
content: [
Text {
content: "A Fictitious Audio Application that Demonstrates"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Creating JavaFX Custom Nodes"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Application Developer: Jim Weaver"
fill: Color.WHITE
font: splashFont
},
Text {
content: "Graphics Designer: Mark Dingman"
fill: Color.WHITE
font: splashFont
},
]
}
]
},
// The "Play" page
VBox {
var tableNode:TableNode
id: "Play"
spacing: 4
content: [
Group {
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/playing_currently.png"
}
},
Text {
textOrigin: TextOrigin.TOP
content: bind "{tableNode.selectedIndex}"
font: Font {
size: 24
}
}
]
},
tableNode = TableNode {
height: 135
rowHeight: 25
rowSpacing: 2
columnWidths: [150, 247, 25, 70]
tableFill: Color.BLACK
rowFill: Color.#1c1c1c
selectedRowFill: Color.#2d2d2d
selectedIndex: -1
vertScrollbarWidth: 20
vertScrollbarFill: LinearGradient {
startX: 0.0
startY: 0.0
endX: 1.0
endY: 0.0
stops: [
Stop {
offset: 0.0
color: Color.#0b0b0b
},
Stop {
offset: 1.0
color: Color.#343434
}
]
}
vertScrollbarThumbFill: Color.#efefef
content: bind
for (obj in model.playlistObjects) {
if (obj instanceof String)
Text {
textOrigin: TextOrigin.TOP
fill: Color.#b7b7b7
content: obj as String
font:
Font {
size: 11
}
}
else if (obj instanceof Image)
ImageView {
image: obj as Image
}
else
null
}
onSelectionChange:
function(row:Integer):Void {
System.out.println("Table row #{row} selected");
}
}
]
},
// The "Burn" page
Group {
var vboxRef:VBox;
id: "Burn"
content: [
vboxRef = VBox {
translateX: bind stageRef.width / 2 - vboxRef.getWidth() / 2
translateY: bind stageRef.height / 2 - vboxRef.getHeight() / 2
spacing: 15
content: [
Text {
textOrigin: TextOrigin.TOP
content: "Burning custom playlist to CD..."
font:
Font {
name: "Sans serif"
style: FontStyle.PLAIN
size: 22
}
fill: Color.#d3d3d3
},
ProgressNode {
width: 430
height: 15
progressPercentColor: Color.#bfdfef
progressTextColor: Color.#0c1515
progressText: bind "{model.remainingBurnTime} Remaining"
progressFill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.#00c0ff
},
Stop {
offset: 0.20
color: Color.#00acea
},
Stop {
offset: 1.0
color: Color.#0070ae
},
]
}
barFill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.#707070
},
Stop {
offset: 1.0
color: Color.#585858
},
]
}
progress: bind model.burnProgressPercent / 100.0
},
ComponentView {
component:
FlowPanel {
background: Color.BLACK
content: [
Label {
text: "Slide to simulate burn progress:"
foreground: Color.#d3d3d3
},
Slider {
orientation: Orientation.HORIZONTAL
minimum: 0
maximum: 100
value: bind model.burnProgressPercent with inverse
preferredSize: [200, 20]
}
]
}
}
]
}
]
},
// The "Config" page
Group {
id: "Config"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/config.png"
}
}
]
},
// The "Help" page
Group {
id: "Help"
content: [
ImageView {
image:
Image {
url: "{__DIR__}images/help.png"
}
}
]
}
]
},
menuRef = MenuNode {
translateX: bind stageRef.width / 2 - menuRef.getWidth() / 2
translateY: bind stageRef.height - menuRef.getHeight()
buttons: [
ButtonNode {
title: "Play"
imageURL: "{__DIR__}icons/play.png"
action:
function():Void {
deckRef.visibleNodeId = "Play";
}
},
ButtonNode {
title: "Burn"
imageURL: "{__DIR__}icons/burn.png"
action:
function():Void {
deckRef.visibleNodeId = "Burn";
}
},
ButtonNode {
title: "Config"
imageURL: "{__DIR__}icons/config.png"
action:
function():Void {
deckRef.visibleNodeId = "Config";
}
},
ButtonNode {
title: "Help"
imageURL: "{__DIR__}icons/help.png"
action:
function():Void {
deckRef.visibleNodeId = "Help";
}
},
]
}
]
}
}


Note that the Application class has a stage attribute just as the Frame had in previous examples. Here's the TableNodeExamplePage.html file that you'll open in your browser. The draggable param, by the way, enables that neat "pull the applet out of the browser" trick that I'll show you in a bit:




archive="javafxrt.jar, Scenario.jar, javafxgui.jar, javafx-swing.jar, TableNodeExample.jar">







Finally, here's the Java Web Start TableNodeExampleApplet.jnlp file that is used by the HTML file above:




TableNodeExampleApplet
JMentor
TableNodeExampleApplet
TableNodeExampleApplet
















name="TableNodeExampleApplet"
main-class="javafx.application.Applet"
width="500"
height="400">


Dragging the Applet out of the Browser and onto the Desktop

As shown in the following screenshot, one of the cool features of Java SE 6 update 10 is that you can drag a Java or JavaFX applet out of the browser and onto the desktop. By default, you press the Alt key while dragging the applet:

TableNodeExampleApplet-Drag

Here is our JavaFX Applet living happily on the desktop after the browser has been closed, and the user has selected the Burn page:

TableNodeExampleApplet-Dragged

Google Chrome will be a Driving Force for RIA

According to Google, Java SE 6 Update 10 is the version that must be used in order to run Java in the Chrome browser. As I've mentioned previously, one of the objectives of Java SE 6 Update 10 is to solve the JRE and Java/JavaFX deployment issues. Because Google Chrome is destined to be a great, cross-platform browser, and because it requires the version of Java that makes rich-client Java/JavaFX programs feasible, this will increase the adoption rate of JavaFX applets and applications.

read more “JavaFX Applets Meet Google Chrome”

Using the Java Deployment Toolkit with JavaFX Applets

Using the Java Deployment Toolkit with JavaFX Applets

First, let me apologize for resurrecting the very humble JavaFX program shown below, but I want to keep this example very succinct. This will enable you to use it as "starter code" for JavaFX applet deployment. Note: To see more functional JavaFX programs, please see articles in the JFX Custom Nodes category.

BindToFunctionApplet_SDK_Preview

Note: Thanks to reader "mbien" (see comments) for pointing our that the colors of the original applet in this post were hideous (my words). I then consulted graphics designer Mark Dingman of Malden Labs who gave me a graphical mock-up from which I created the above applet. Here's the code for this applet, updated for the JavaFX SDK preview:

/*

* BindToFunctionApplet.fx - A compiled JavaFX program that demonstrates
* how to create JavaFX applets.
* It also demonstrates binding to a function.
*
* Developed 2008 by Jim Weaver (development) and Mark Dingman (graphic design)
* to serve as a JavaFX Script example.
*/
package com.javafxpert.bind_to_function;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.geometry.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.Math;

class CircleModel {
attribute diameter:Integer;

bound function getArea():Number {
Math.PI * Math.pow(diameter / 2, 2);
}
}

Application {
var cModel = CircleModel {};
var componentViewRef:ComponentView;
var stageRef:Stage;
stage:
stageRef = Stage {
var labelFont = Font {
name: "Sans Serif"
style: FontStyle.PLAIN
size: 32
}
fill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.rgb(0, 168, 255)
},
Stop {
offset: 1.0
color: Color.rgb(0, 65, 103)
}
]
}
content: [
Circle {
centerX: 250
centerY: 250
radius: bind cModel.diameter / 2
fill:
LinearGradient {
startX: 0.0
startY: 0.0
endX: 0.0
endY: 1.0
stops: [
Stop {
offset: 0.0
color: Color.rgb(74, 74, 74)
},
Stop {
offset: 1.0
color: Color.rgb(9, 9, 9)
}
]
}
},
Text {
font: labelFont
x: 30
y: 70
fill: Color.BLACK
content: bind "Diameter: {cModel.diameter}"
},
Text {
font: labelFont
x: 260
y: 70
fill: Color.BLACK
content: bind "Area: {%3.2f cModel.getArea()}"
},
componentViewRef = ComponentView {
transform: bind
Translate.translate(40, stageRef.height - 30 -
componentViewRef.getHeight())
component:
Slider {
minimum: 0
maximum: 400
preferredSize: bind [stageRef.width - 80, 20]
value: bind cModel.diameter with inverse
}
}
]
}
}

Why Use the Java Deployment Toolkit for Java Applets?

According to Sun's Java Deployment Toolkit overview page, "Desktop clients have a wide variety of Java Platforms installed, from the Microsft VM to Sun's latest Java SE 6 updates. They run various operating systems from Sun, Microsoft, Apple, Red Hat, and others, and are connected to the internet at a wide range of connection speeds. How are content providers to deliver Java content to all of these clients with the best possible user experience?

Various sources have published JavaScript techniques for detecting and deploying the Java Platform for use by Java Plug-In applets and Java Web Start applications. These scripts generally have serious limitations and fail to support the varied combinations of browser, OS, and configuration options found on today's clients.

The Java Deployment Toolkit allows developers to easily deploy applets and applications to a large variety of clients with JavaScripts. It also provides advice on using some of the most powerful features available in Java Web Start and Java Plug-In, and an outline of the differences between these two deployment vehicles.
"

In a nutshell, the Java Deployment Toolkit is a JavaScript library maintained by Sun and always available at runtime by your HTML code. This library has several methods that perform tasks such as sensing Java-related infrastructure and installing the JRE on client machines. We'll use one of these methods, namely runApplet, to run a JavaFX applet with a specified minimum JRE version. Here's the HTML and JavaScript code I'm using to deploy today's example applet:







Notice that the above code enables dragging the applet onto the desktop, as well as using Pack200 formatted JAR files, if the client machine has Java SE 6 update 10 installed. Give the applet a whirl to see its deployment behavior on your machine. By the way, according to the Java SE 6 Update 10 plug-in docs, "by default, the gesture to drag the applet out of the web browser is Alt + Left click + Drag."

read more “Using the Java Deployment Toolkit with JavaFX Applets”

Six Degrees of Aynsley Dunbar: BandmatesFX Example JavaFX App Continued

So far in this Freebase Contributing Artists App series, we've been developing a JavaFX application that enables the user to navigate connections among musicians. I'm calling this application BandmatesFX, and it uses the JSONHandler feature of the JFXtras open source library to easily query the Freebase.com database.

Note: While using this app, I noticed that drummer Aynsley Dunbar has played with lots of bands over the years, so he's a good starting point for trying to navigate to another given musician in six or less degrees. Try, for example, to navigate from Aynsley Dunbar to Roy Orbison. Here's one way in five degrees:

  1. Aynsley Dunbar played in The Animals, with Andy Summers
  2. Andy Summers played in The Police, with Sting
  3. Sting played in Band Aid, with Paul McCartney
  4. Paul McCartney played in The Quarrymen (and of course The Beatles), with George Harrison
  5. George Harrison played in The Traveling Wilburys, with Roy Orbison

Today I'm going to point out some enhancements to this application, and the updated code, which are reflected in the following screenshot:

BandmatesFX_Aynsley_small


These enhancements include:

  • The user can type in an artist name, and with each keystroke the Freebase database web service is invoked, returning suggested names from which the user can choose.
  • Band and artist pictures are presented in a cover-flow style, using the JFXtras Shelf component. In addition, the JFXtras TitledBorder component is employed to enclose and label the cover-flows.
  • When the user clicks the central image in the bottom (e.g. Steve Smith in the screenshot above), that artist becomes the featured artist at the top of the page. The group cover-flow images are updated to reflect the groups in which the artist has played, and the member cover-flow images contain the artists that have played in the currently selected group.

One of the cool feature of the JFXtras Shelf component is that the mouse wheel moves the images horizontally on the Shelf, enabling the user to navigate even more quickly than clicking on one of the images. Before invoking this application from the Web Start link at the end of this article, take a look at the main script of the program (named BandmatesFX.fx) in the listing below:

/*
* BandmatesMain.fx
*
* Uses Freebase and JFXtras with JavaFX to explore connections
* between musical artists
*
* Developed by James L. Weaver to demonstrate using JavaFX and JFXtras
*/

package javafxpert;

import javafx.io.http.HttpRequest;
import javafx.animation.transition.*;
import javafx.geometry.HPos;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ListView;
import javafx.scene.control.TextBox;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.stage.Stage;

import org.jfxtras.data.pull.*;
import org.jfxtras.scene.border.TitledBorder;
import org.jfxtras.scene.control.Shelf;

/**
* Height of the images that we'll be working with
*/
def IMAGE_HEIGHT:Integer = 140;

/**
* Artist for which we're searching bandmates
*/
var artistToSearch:String = "/en/aynsley_dunbar";

/**
* Name for which we're finding matches and ids
*/
var nameToFind:String = "Aynsley Dunbar";

var textBoxRef:TextBox;
var listViewRef:ListView;
var selectedNameIndex = bind listViewRef.selectedIndex on replace {
def selectedId = freebaseSearchResult.result[selectedNameIndex].id;
if (selectedId != "") {
println("selectedId:{selectedId}");
nameToFind = freebaseSearchResult.result[selectedNameIndex].name;
obtainGroupsForArtist(selectedId);
}
};

/**
* Hover text that contains the name of the artist or group
*/
var nameHoverText:String;

/**
* A reference to the HTTP request, for the purpose of monitoring progress
*/
var req:HttpRequest;

/**
* The root class that will hold the object graph from the JSON results
*/
var freebaseResult:FreebaseResult;

/**
* The root class that will hold the object graph from the JSON results
*/
var freebaseSearchResult:FreebaseSearchResult;

/**
* The base URL for the freebase query
*/
def freebaseURL = "http://www.freebase.com/api/service/mqlread?";

/**
* The base URL for a freebase search request
*/
def freebaseSearchURL = "http://www.freebase.com/api/service/search?";

/**
* The base URL to get a freebase image
*/
def freebaseImageURL = "http://img.freebase.com/api/trans/image_thumb";

var artistsNode:Node;

/**
* Fade in transition for artists
*/
def shelfFadeIn = FadeTransition {
node: bind artistsNode
duration: 1500ms
fromValue: 0.2
toValue: 1.0
}

/**
* Indexe into groups list
*/
var groupOneIndex:Integer on replace {
shelfFadeIn.playFromStart();

// Choose the middle one
def numArtists:Integer = sizeof freebaseResult.result.
musicGroupMemberMembership[groupOneIndex].
group.musicMusicalGroupMember;
artistTwoIndex = if (numArtists > 1) numArtists / 2 else 0;

groupOneName = freebaseResult.result.
musicGroupMemberMembership[groupOneIndex].group.name;
};

/**
* Name of the currently selected band
*/
var groupOneName:String;

/**
* Index into artists list
*/
var artistTwoIndex:Integer;

/**
* Create a search query and invoke the JSON handler
*/
function obtainIdForArtistPartialName(artistPartialName:String) {
listViewRef.visible = true;
nameToFind = artistPartialName;
def partialName = artistPartialName.replace(" ", "+");
var searchUrl = "{freebaseSearchURL}prefix={partialName}&type=/music/artist&limit=10&mql_output=[\{\"id\":null,\"name\":null\}]";

println("searchUrl:{searchUrl}");
var albumHandler:JSONHandler = JSONHandler {
rootClass: "javafxpert.FreebaseSearchResult"
onDone: function(obj, isSequence): Void {
freebaseSearchResult = obj as FreebaseSearchResult;
println("# of search results:{sizeof freebaseSearchResult.result},freebaseSearchResult:{freebaseSearchResult.code}");
req.stop();
}
};
req = HttpRequest {
location: searchUrl
onInput: function(is: java.io.InputStream) {
albumHandler.parse(is);
}
};
req.start();
}

/**
* Create the Freebase query and invoke the JSON handler
*/
function obtainGroupsForArtist(artistFreebaseId:String) {
listViewRef.visible = false;
artistToSearch = artistFreebaseId;
var queryUrl = "{freebaseURL}query=\{\"query\":"
" \{ "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}], "
" \"/music/group_member/membership\": [\{ "
" \"group\": \{ "
" \"name\": null, "
" \"id\": null, "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}], "
" \"/music/musical_group/member\": [\{ "
" \"member\": \{ "
" \"name\": null, "
" \"id\": null, "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}] "
" \} "
" \}] "
" \} "
" \}], "
" \"id\": \"{artistFreebaseId}\", "
" \"name\": null, "
" \"type\": \"/music/artist\" "
" \} \}";

println("queryUrl:{queryUrl}");
var albumHandler:JSONHandler = JSONHandler {
rootClass: "javafxpert.FreebaseResult"
onDone: function(obj, isSequence): Void {
freebaseResult = obj as FreebaseResult;
println("# of bands:{sizeof freebaseResult.result.musicGroupMemberMembership}");
req.stop();

// Choose the middle one
def numGroups:Integer = sizeof freebaseResult.result.musicGroupMemberMembership;
groupOneIndex = if (numGroups > 1) numGroups / 2 else 0;
}
};
req = HttpRequest {
location: queryUrl
onInput: function(is: java.io.InputStream) {
albumHandler.parse(is);
}
};
req.start();
}
var sceneRef:Scene;

Stage {
title: bind "BandmatesFX - {nameToFind}"
scene: sceneRef = Scene {
width: 1000
height: 700
content: [
ImageView {
image: Image {
url: "http://www.popsci.com/files/imagecache/article_image_large/files/articles/guitar.gif"
}
fitWidth: bind sceneRef.width
preserveRatio: true
opacity: 0.1
},
VBox {
nodeHPos: HPos.CENTER
layoutY: 10
spacing: 10
content: [
HBox {
spacing: 10
content: [
ImageView {
image: bind Image {
url: "{freebaseImageURL}{artistToSearch}?maxheight={IMAGE_HEIGHT}"
}
},
VBox {
content: [
textBoxRef = TextBox {
promptText: "Enter artist name"
text: bind nameToFind with inverse
columns: 20
font: Font.font(null, 14)
action:function():Void {
if (nameToFind != "" and (req.percentDone == 0.0 or req.percentDone == 100.0)) {
obtainIdForArtistPartialName(nameToFind);
}
}
onKeyTyped:function(ke:KeyEvent) {
println("req.percentDone:{req.percentDone}");
if (textBoxRef.rawText != "" and (req.percentDone == 0.0 or req.percentDone == 100.0)) {
obtainIdForArtistPartialName(textBoxRef.rawText);
}
}
},
listViewRef = ListView {
visible: false
height: 100
items: bind for (rslt in freebaseSearchResult.result) {
rslt.name
}
layoutInfo: LayoutInfo {
width: bind textBoxRef.width - 10
height: 80
}
}
]
},
ProgressIndicator {
progress: bind req.progress
},
Label {
text: bind nameHoverText
font: Font.font(null, FontWeight.BOLD, 18)
}
]
},
TitledBorder {
text: bind "{nameToFind} played in these groups:"
font: Font.font(null, FontWeight.BOLD, 14)
lineColor: Color.GREY
node: Shelf {
reflection: false
showScrollBar: false
showText: true
imageUrls: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
"{freebaseImageURL}{grp.group.id}?maxheight={IMAGE_HEIGHT}"
}
imageNames: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
grp.group.name;
}
index: bind groupOneIndex with inverse
thumbnailWidth: IMAGE_HEIGHT
thumbnailHeight: IMAGE_HEIGHT
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
},
artistsNode = Panel {
content: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
TitledBorder {
text: bind "{groupOneName} has had the following members:"
font: Font.font(null, FontWeight.BOLD, 14)
lineColor: Color.GREY
node: Shelf {
visible: bind indexof grp == groupOneIndex
reflection: false
showScrollBar: false
showText: true
imageUrls: bind for (mmbr in freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember) {
"{freebaseImageURL}{mmbr.member.id}?maxheight={IMAGE_HEIGHT}"
}
imageNames: bind for (mmbr in freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember) {
mmbr.member.name
}
onCenterImagePressed:function(me:MouseEvent):Void {
var mmbr = freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember[artistTwoIndex];
nameToFind = mmbr.member.name;
artistToSearch = mmbr.member.id;
obtainGroupsForArtist(mmbr.member.id);
}
index: bind artistTwoIndex with inverse
thumbnailWidth: IMAGE_HEIGHT
thumbnailHeight: IMAGE_HEIGHT
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
}
}
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
]
}
]
}
}
obtainGroupsForArtist(artistToSearch);


To try out this application, click the Web Start link below. Since this application is a work in progress, I'll give you a couple of pointers:

  • When typing the name of an artist in the text box, each key invokes a web service, so wait until the progress indicator is solid before typing the final character (I'll make this smoother in a future iteration).
  • Not all artists are in groups, and I'm not doing the necessary exception handling yet to catch this in the response from Freebase. I know that many artists such as Eric Clapton, Bob Dylan, and Steve Winwood work, as well as any artist that you can click in the bottom cover flow (by definition they are members of a group).

Webstartsmall2

For more background on this application, see the previous article in this series. As always, please leave a comment if you have any questions. Also, if you have any suggestions for future enhancements, please leave a comment as well.

read more “Six Degrees of Aynsley Dunbar: BandmatesFX Example JavaFX App Continued”

Freebase Contributing Artists App

Six Degrees of Aynsley Dunbar: BandmatesFX Example JavaFX App Continued

So far in this Freebase Contributing Artists App series, we've been developing a JavaFX application that enables the user to navigate connections among musicians. I'm calling this application BandmatesFX, and it uses the JSONHandler feature of the JFXtras open source library to easily query the Freebase.com database.

Note: While using this app, I noticed that drummer Aynsley Dunbar has played with lots of bands over the years, so he's a good starting point for trying to navigate to another given musician in six or less degrees. Try, for example, to navigate from Aynsley Dunbar to Roy Orbison. Here's one way in five degrees:

  1. Aynsley Dunbar played in The Animals, with Andy Summers
  2. Andy Summers played in The Police, with Sting
  3. Sting played in Band Aid, with Paul McCartney
  4. Paul McCartney played in The Quarrymen (and of course The Beatles), with George Harrison
  5. George Harrison played in The Traveling Wilburys, with Roy Orbison

Today I'm going to point out some enhancements to this application, and the updated code, which are reflected in the following screenshot:

BandmatesFX_Aynsley_small


These enhancements include:

  • The user can type in an artist name, and with each keystroke the Freebase database web service is invoked, returning suggested names from which the user can choose.
  • Band and artist pictures are presented in a cover-flow style, using the JFXtras Shelf component. In addition, the JFXtras TitledBorder component is employed to enclose and label the cover-flows.
  • When the user clicks the central image in the bottom (e.g. Steve Smith in the screenshot above), that artist becomes the featured artist at the top of the page. The group cover-flow images are updated to reflect the groups in which the artist has played, and the member cover-flow images contain the artists that have played in the currently selected group.

One of the cool feature of the JFXtras Shelf component is that the mouse wheel moves the images horizontally on the Shelf, enabling the user to navigate even more quickly than clicking on one of the images. Before invoking this application from the Web Start link at the end of this article, take a look at the main script of the program (named BandmatesFX.fx) in the listing below:

/*
* BandmatesMain.fx
*
* Uses Freebase and JFXtras with JavaFX to explore connections
* between musical artists
*
* Developed by James L. Weaver to demonstrate using JavaFX and JFXtras
*/

package javafxpert;

import javafx.io.http.HttpRequest;
import javafx.animation.transition.*;
import javafx.geometry.HPos;
import javafx.scene.*;
import javafx.scene.control.Label;
import javafx.scene.control.ProgressIndicator;
import javafx.scene.control.ListView;
import javafx.scene.control.TextBox;
import javafx.scene.image.*;
import javafx.scene.input.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.stage.Stage;

import org.jfxtras.data.pull.*;
import org.jfxtras.scene.border.TitledBorder;
import org.jfxtras.scene.control.Shelf;

/**
* Height of the images that we'll be working with
*/
def IMAGE_HEIGHT:Integer = 140;

/**
* Artist for which we're searching bandmates
*/
var artistToSearch:String = "/en/aynsley_dunbar";

/**
* Name for which we're finding matches and ids
*/
var nameToFind:String = "Aynsley Dunbar";

var textBoxRef:TextBox;
var listViewRef:ListView;
var selectedNameIndex = bind listViewRef.selectedIndex on replace {
def selectedId = freebaseSearchResult.result[selectedNameIndex].id;
if (selectedId != "") {
println("selectedId:{selectedId}");
nameToFind = freebaseSearchResult.result[selectedNameIndex].name;
obtainGroupsForArtist(selectedId);
}
};

/**
* Hover text that contains the name of the artist or group
*/
var nameHoverText:String;

/**
* A reference to the HTTP request, for the purpose of monitoring progress
*/
var req:HttpRequest;

/**
* The root class that will hold the object graph from the JSON results
*/
var freebaseResult:FreebaseResult;

/**
* The root class that will hold the object graph from the JSON results
*/
var freebaseSearchResult:FreebaseSearchResult;

/**
* The base URL for the freebase query
*/
def freebaseURL = "http://www.freebase.com/api/service/mqlread?";

/**
* The base URL for a freebase search request
*/
def freebaseSearchURL = "http://www.freebase.com/api/service/search?";

/**
* The base URL to get a freebase image
*/
def freebaseImageURL = "http://img.freebase.com/api/trans/image_thumb";

var artistsNode:Node;

/**
* Fade in transition for artists
*/
def shelfFadeIn = FadeTransition {
node: bind artistsNode
duration: 1500ms
fromValue: 0.2
toValue: 1.0
}

/**
* Indexe into groups list
*/
var groupOneIndex:Integer on replace {
shelfFadeIn.playFromStart();

// Choose the middle one
def numArtists:Integer = sizeof freebaseResult.result.
musicGroupMemberMembership[groupOneIndex].
group.musicMusicalGroupMember;
artistTwoIndex = if (numArtists > 1) numArtists / 2 else 0;

groupOneName = freebaseResult.result.
musicGroupMemberMembership[groupOneIndex].group.name;
};

/**
* Name of the currently selected band
*/
var groupOneName:String;

/**
* Index into artists list
*/
var artistTwoIndex:Integer;

/**
* Create a search query and invoke the JSON handler
*/
function obtainIdForArtistPartialName(artistPartialName:String) {
listViewRef.visible = true;
nameToFind = artistPartialName;
def partialName = artistPartialName.replace(" ", "+");
var searchUrl = "{freebaseSearchURL}prefix={partialName}&type=/music/artist&limit=10&mql_output=[\{\"id\":null,\"name\":null\}]";

println("searchUrl:{searchUrl}");
var albumHandler:JSONHandler = JSONHandler {
rootClass: "javafxpert.FreebaseSearchResult"
onDone: function(obj, isSequence): Void {
freebaseSearchResult = obj as FreebaseSearchResult;
println("# of search results:{sizeof freebaseSearchResult.result},freebaseSearchResult:{freebaseSearchResult.code}");
req.stop();
}
};
req = HttpRequest {
location: searchUrl
onInput: function(is: java.io.InputStream) {
albumHandler.parse(is);
}
};
req.start();
}

/**
* Create the Freebase query and invoke the JSON handler
*/
function obtainGroupsForArtist(artistFreebaseId:String) {
listViewRef.visible = false;
artistToSearch = artistFreebaseId;
var queryUrl = "{freebaseURL}query=\{\"query\":"
" \{ "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}], "
" \"/music/group_member/membership\": [\{ "
" \"group\": \{ "
" \"name\": null, "
" \"id\": null, "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}], "
" \"/music/musical_group/member\": [\{ "
" \"member\": \{ "
" \"name\": null, "
" \"id\": null, "
" \"/common/topic/image\": [\{ "
" \"id\": null "
" \}] "
" \} "
" \}] "
" \} "
" \}], "
" \"id\": \"{artistFreebaseId}\", "
" \"name\": null, "
" \"type\": \"/music/artist\" "
" \} \}";

println("queryUrl:{queryUrl}");
var albumHandler:JSONHandler = JSONHandler {
rootClass: "javafxpert.FreebaseResult"
onDone: function(obj, isSequence): Void {
freebaseResult = obj as FreebaseResult;
println("# of bands:{sizeof freebaseResult.result.musicGroupMemberMembership}");
req.stop();

// Choose the middle one
def numGroups:Integer = sizeof freebaseResult.result.musicGroupMemberMembership;
groupOneIndex = if (numGroups > 1) numGroups / 2 else 0;
}
};
req = HttpRequest {
location: queryUrl
onInput: function(is: java.io.InputStream) {
albumHandler.parse(is);
}
};
req.start();
}
var sceneRef:Scene;

Stage {
title: bind "BandmatesFX - {nameToFind}"
scene: sceneRef = Scene {
width: 1000
height: 700
content: [
ImageView {
image: Image {
url: "http://www.popsci.com/files/imagecache/article_image_large/files/articles/guitar.gif"
}
fitWidth: bind sceneRef.width
preserveRatio: true
opacity: 0.1
},
VBox {
nodeHPos: HPos.CENTER
layoutY: 10
spacing: 10
content: [
HBox {
spacing: 10
content: [
ImageView {
image: bind Image {
url: "{freebaseImageURL}{artistToSearch}?maxheight={IMAGE_HEIGHT}"
}
},
VBox {
content: [
textBoxRef = TextBox {
promptText: "Enter artist name"
text: bind nameToFind with inverse
columns: 20
font: Font.font(null, 14)
action:function():Void {
if (nameToFind != "" and (req.percentDone == 0.0 or req.percentDone == 100.0)) {
obtainIdForArtistPartialName(nameToFind);
}
}
onKeyTyped:function(ke:KeyEvent) {
println("req.percentDone:{req.percentDone}");
if (textBoxRef.rawText != "" and (req.percentDone == 0.0 or req.percentDone == 100.0)) {
obtainIdForArtistPartialName(textBoxRef.rawText);
}
}
},
listViewRef = ListView {
visible: false
height: 100
items: bind for (rslt in freebaseSearchResult.result) {
rslt.name
}
layoutInfo: LayoutInfo {
width: bind textBoxRef.width - 10
height: 80
}
}
]
},
ProgressIndicator {
progress: bind req.progress
},
Label {
text: bind nameHoverText
font: Font.font(null, FontWeight.BOLD, 18)
}
]
},
TitledBorder {
text: bind "{nameToFind} played in these groups:"
font: Font.font(null, FontWeight.BOLD, 14)
lineColor: Color.GREY
node: Shelf {
reflection: false
showScrollBar: false
showText: true
imageUrls: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
"{freebaseImageURL}{grp.group.id}?maxheight={IMAGE_HEIGHT}"
}
imageNames: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
grp.group.name;
}
index: bind groupOneIndex with inverse
thumbnailWidth: IMAGE_HEIGHT
thumbnailHeight: IMAGE_HEIGHT
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
},
artistsNode = Panel {
content: bind for (grp in freebaseResult.result.musicGroupMemberMembership) {
TitledBorder {
text: bind "{groupOneName} has had the following members:"
font: Font.font(null, FontWeight.BOLD, 14)
lineColor: Color.GREY
node: Shelf {
visible: bind indexof grp == groupOneIndex
reflection: false
showScrollBar: false
showText: true
imageUrls: bind for (mmbr in freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember) {
"{freebaseImageURL}{mmbr.member.id}?maxheight={IMAGE_HEIGHT}"
}
imageNames: bind for (mmbr in freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember) {
mmbr.member.name
}
onCenterImagePressed:function(me:MouseEvent):Void {
var mmbr = freebaseResult.result.
musicGroupMemberMembership[indexof grp].
group.musicMusicalGroupMember[artistTwoIndex];
nameToFind = mmbr.member.name;
artistToSearch = mmbr.member.id;
obtainGroupsForArtist(mmbr.member.id);
}
index: bind artistTwoIndex with inverse
thumbnailWidth: IMAGE_HEIGHT
thumbnailHeight: IMAGE_HEIGHT
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
}
}
layoutInfo: LayoutInfo {
width: bind sceneRef.width
height: IMAGE_HEIGHT + 120
}
}
]
}
]
}
}
obtainGroupsForArtist(artistToSearch);


To try out this application, click the Web Start link below. Since this application is a work in progress, I'll give you a couple of pointers:

  • When typing the name of an artist in the text box, each key invokes a web service, so wait until the progress indicator is solid before typing the final character (I'll make this smoother in a future iteration).
  • Not all artists are in groups, and I'm not doing the necessary exception handling yet to catch this in the response from Freebase. I know that many artists such as Eric Clapton, Bob Dylan, and Steve Winwood work, as well as any artist that you can click in the bottom cover flow (by definition they are members of a group).

Webstartsmall2

For more background on this application, see the previous article in this series. As always, please leave a comment if you have any questions. Also, if you have any suggestions for future enhancements, please leave a comment as well.

read more “Freebase Contributing Artists App”

Educational JavaFX Script Apps

JavaFX in the Wild: Sapphire!

Stockholm-09-Horizontal

With JavaFX 1.0 having been released just a few days ago, I'm pleased that we're observing more JavaFX applications "in the wild". Here's one that I became aware of recently, named Sapphire, developed by The Standards Company.

Picture 1


According to the website in which you can launch this JavaFX application:

"Sapphire is an advanced classroom observation tool. It provides an easy-to-use interface, multiple platform deployments, and interchangeable observation forms which can be customized to fit most any situation. The Community Edition [which is the version that you can try out from the link above] is a demonstration version requested by the Oklahoma State Department of Education and configured to complement the "Time on Task" observation form released in October 2008."

Some JavaFX Technical Notes from Ben Jones of The Standards Company: Real-time Charting in JavaFX

One of the features of JavaFX that makes it attractive to real-time application developers is its ability to tie graphical elements to models through the binding mechanism. This mechanism makes it possible for the current state of the model to be reflected in the interface without the need to create any type of polling system.

In our Sapphire application, we collect real-time data from classroom observations. The observer records classroom events by pressing various buttons. These events are displayed in several dashboard type displays which are actively tied to supporting models. One of these displays in a time lime of events created by a series of colored rectangles. The size, placement and color of the rectangles represent different relationships in the event data. Every time an event is pressed, the data changes and the bound display automatically updates.

To achieve this, you only need three things: a data model, a display element, and an event driver of some type. Let's take a look at one of the visual building blocks, the TimeOnTaskBlock class:

public class TimeOnTaskBlock extends CustomNode {

public var aboveColor:Color = Color.GREEN;
public var belowColor:Color = Color.RED;

public var seconds:Integer;
public var startingMark:Integer;
public var zoneWidth:Integer = 600;
public var zoneSeconds:Integer = 3600;
public var height:Integer = 100;
public var maxSegments:Number = 5;
public var segment:Number = 0;
public var x:Integer;
public var scale:Number = 0.95;

public override function create(): Node {
return Group {

var maxDisplayHeight:Number = height/2;

content: [
Rectangle {
x: bind zoneWidth * startingMark / zoneSeconds;
y: bind height/2 - scale * ((maxDisplayHeight) * segment/maxSegments)
width: bind zoneWidth * seconds / zoneSeconds;
height: bind scale * (maxDisplayHeight * segment/maxSegments)
fill: bind aboveColor
strokeWidth:0
},
Rectangle {
x: bind zoneWidth * startingMark / zoneSeconds;
y: bind height / 2
width: bind zoneWidth * seconds / zoneSeconds;
height: bind scale * (maxDisplayHeight - (maxDisplayHeight * segment/maxSegments))
fill: bind belowColor
strokeWidth:0
}
]
effect: Lighting {
light: DistantLight { azimuth: 225 elevation: 60 }
surfaceScale: 2
}
};
}
}

This custom node represents what will be displayed. It is a pair of scaled rectangles stacked on top of each other. The various attributes define shape, size, and placement. Notice the use of the the DistantLight effect to give the rectangles in the graph some depth. The next building block up from this is another custom node that handles the visual orchestration of the time blocks nodes. The DisplayTimeChart node is effectively the dashboard element being displayed. It has a data member holding a sequence of TimeOnTaskBlock nodes. Let's take a peek at this node:

public class TimeDisplayChart extends CustomNode {

public var width:Integer;
public var height:Integer;
public var zoneSeconds:Integer;
public var zoneWidth:Integer;
public var background:Color;
public var timer:SapphireClock;
public var timeMark:Integer = 0;
public var currentSegment:Number = 0;
public var displayTicker:Integer;
public var displayBlocks:TimeOnTaskBlock[];
public var onColor:Color = Color.#96ff96;
public var offColor:Color = Color.#ff9696;

public override function create(): Node {
return Group {
content: [
Group {
content:[
Rectangle {
width: width
height: height
fill: background
arcWidth:10
arcHeight:10
stroke:Color.WHITE
},
Group {
translateX:2
content:[
Group {
content: bind displayBlocks
}
]
},
Group {
translateX:0
content:[
TimeOnTaskBlock {
aboveColor:bind onColor
belowColor:bind offColor
seconds: bind timer.ticker - timeMark
startingMark: bind timeMark
segment:bind currentSegment
}
]
},
Line {
startX:0
startY: height / 2
endX: zoneWidth
endY: height / 2
strokeWidth: 1
stroke: Color.BLACK
},
Group {
content: bind
for (i in [1..zoneSeconds - 2] where (i mod 300 == 0)) {
Line {
startX: bind zoneWidth * i / zoneSeconds
startY:0
endX: bind zoneWidth * i / zoneSeconds
endY: bind height
strokeWidth: 1
stroke: Color.BLACK
}
}
}
]
clip: Rectangle {
x:-1,
y:-1
width: bind width + 4
height: bind height + 4
}
},
Group {
translateX: 0
translateY: bind height + 5
content: bind
for (i in [1..zoneSeconds] where (i mod 300 == 0)) {
Text {
textAlignment: TextAlignment.RIGHT
textOrigin: TextOrigin.TOP
fill:Color.WHITE
font: Font {
size: 12
}
x: (zoneWidth * i / zoneSeconds) - 7
content: "{i / 60}"
}
}
}
]
};
}
}

The TimeDisplayChart serves a couple of purposes. First it builds the overall dashboard element. This defines the usual display space, necessary labeling and element placements. Please note two aspects of this class: the group where its content is bound to the displayBlocks sequence and the second group composed of a solo TimeOnTaskBlock. By binding the content of a group to a data source, any changes in the data source will be made available to the group automatically. This effectively produces a self-updating display since we bound a displayable node. The second group which is a single instance of the TimeOnTaskBlock serves another purpose. It covers the span of time since the last data change and the current time. Since the data model stores only past events, we need a means of showing data yet to be stored. It's real-time after all.

To place all of this into an application, an instance of TimeDisplayChart is placed into a group as follows:

Group {
translateY:310
translateX:10
content:[
TimeDisplayChart{
width:600
height:100
zoneSeconds:3600
zoneWidth:600
background:Color.#969696
timer: bind timer
displayTicker: bind displayTicker
currentSegment: bind appState.currentSegment
timeMark: bind appState.currentTimeMark
onColor: bind appState.onColor
offColor:bind appState.offColor
displayBlocks: bind displayblocks
}
]
}

Here we can see the various attributes of the custom node being bound to different data sources and conditions that exit within the larger context of the application. The bindings create an active communication channel between application state data and the graphical elements. It's convenient, responsive, and easy to manage during the development process.

JavaFX in the Enterprise

The technical notes that Ben Jones supplied above demonstrate that graphing real-time data is very achievable in JavaFX. This is one of the capabilities required by many enterprise applications, so I'd like to see an open source initiative that provides a library of JavaFX graphs (that have a contemporary RIA look and feel, of course).

Anyway, play with Sapphire, and please give The Standards Company your feedback on this very specialized JavaFX application. Also, if you have any JavaFX applications in the wild, please let me know at jim.weaver [at] javafxpert.com. At the moment I'm particularly interested in showcasing JavaFX apps that communicate with a server, such as enterprise applications and mashups.

read more “Educational JavaFX Script Apps”

Creating Sliders in Compiled JavaFX Script

Beginning with the Romain Guy's Magic InfiniteProgressPanel post, I'm in the process of showcasing some JavaFX Script UI components. I want you to get a feel for what components are available and how to use them in your code. Today's feature is the Slider widget, and here's a screenshot of an example program that uses it:

Sliderexample_2

As you move the slider, the Text graphic shown above changes. Note that I chose a font that attempts to anthropomorphize the numbers. :-)

Here's this example program's code:

/*
* CompiledSpinner.fx - A compiled JavaFX program that demonstrates
* the Slider widget.
*
*/

import javafx.ui.*;
import javafx.ui.canvas.*;

Frame {
var displayNum:Number = 0.0
height: 300
width: 380
title: "Slider Example"
background: Color.WHITE
content:
BorderPanel {
center:
Canvas {
content:
Text {
font:
Font {
face: FontFace.BITSTREAM_VERA_SANS_MONO
style: FontStyle.BOLD
size: 96
}
x: 20
y: 40
stroke: Color.RED
fill: Color.RED
content: bind "{displayNum}"
}
}
bottom:
Slider {
min: 0
max: 100
border:
TitledBorder {
title: "Speed:"
}
value: bind displayNum with inverse
minorTickSpacing: 5
majorTickSpacing: 10
paintTicks: true
paintLabels: true
labels: [
SliderLabel {
value: 0
label:
SimpleLabel {
text: "0"
}
},
SliderLabel {
value: 50
label:
SimpleLabel {
text: "50"
}
},
SliderLabel {
value: 100
label:
SimpleLabel {
text: "100"
}
}
]
}
}
visible: true
}

Noteworthy Concepts

This weblog has covered most of the concepts in this example, but I do want to point out a couple of things:

  • Up to this point I've used the faceName attribute of the Font class, but lately I've been using the face attribute. The difference is that faceName is a String, and face is of type FontFace. You can see the available constants for various font faces in FontFace.fx located in the javafx.ui package.
  • When binding a UI component that the user can modify (like this Slider) to a variable, the bind is bi-directional. Bi-directional binding is expressed by adding with inverse at the end, as seen in the following statement from the code example:
        value: bind displayNum with inverse

Have fun with this example, and please post a comment if you have any questions, or if there are particular UI components that you'd like to see featured soon.

read more “Creating Sliders in Compiled JavaFX Script”

Romain Guy's Magic InfiniteProgressPanel

I've mentioned how simple, elegant and powerful JavaFX Script is. Here's a perfect example of these three qualities. A few weeks ago I wanted to show a progress indicator in a multi-tier application that has a rich client developed in JavaFX Script. Whenever the client is waiting for a response from the server, I wanted to show this progress indicator.

Infiniteprogresspanel_2

The screenshot above shows the InfiniteProgressPanel widget that I used from the JavaFX Script UI library. It was invented by Romain Guy, a co-author of the book Filthy Rich Clients. It is a great alternative to a progress bar due to the fact that you don't have to continually calculate what percent complete the operation is. This is because the progress bar is circular (and therefore infinite). In addition, it is very easy to use: You just place the InfiniteProgressPanel in the UI containment hierarchy, and bind its progress attribute to a Boolean variable that is true whenever you want the progress indicator to appear. Here's the code for this compiled JavaFX Script example:

/*
* CompiledInfiniteProgress.fx
*
* Developed 2007 by James L. Weaver (jim.weaver at lat-inc.com)
* to serve as a compiled JavaFX Script example.
*/

import javafx.ui.*;
import java.lang.System;

Frame {
var busy = false
title: "Infinite Progress Panel Demo"
width: 400
height: 400
background: Color.WHITE
visible: true
onClose:
function() {
System.exit(0);
}
content:
InfiniteProgressPanel {
progress: bind busy
content:
FlowPanel {
content: [
Button {
text: "Get Busy"
action:
function() {
busy = true;
ConfirmDialog {
title: "Patience is a Virtue"
message: "Simulating a busy condition"
visible:true
onYes:
function() {
busy = false
}
};
}
}
]
}
}
}

Please compile and run this example and try it out for yourself! When you click the Get Busy button, the confirmation dialog will be displayed and the infinite progress indicator will appear. When you dismiss the dialog by clicking the OK button, the progress indicator will disappear.

By the way, the next several posts are going to highlight various UI widgets similar to the way this post does. I'd like to give you an appreciation of the rich set of widgets available in JavaFX Script, and how easy they are to use.

JavaFX Script Boot Camp Announcement

As a heads-up, I will be offering a JavaFX Script 2.5 day "boot camp" on Wednesday, April 9 through Friday, April 11, 2008 (ending at noon) in Indianapolis, Indiana. This course is designed to get you quickly up to speed in JavaFX Script application development. A primary reference for this course is my JavaFX Script book, but the course has its own syllabus which includes material covered in the book as well as up to the minute developments in compiled JavaFX Script. Registration will open soon, and for this pilot class I am accepting 12 students. The cost of this pilot class will be 900 USD per student. Additional students from the same organization will be 600 USD. You'll need to bring your laptop computer with the latest versions of the JavaFX Script downloads (which I'll specify in more detail as the class date approaches). The prerequisite for the class will be the completion of a JavaFX Script programming assignment that I'll post soon to this weblog. I'm looking forward to teaching this class and hope that you can attend!

read more “Romain Guy's Magic InfiniteProgressPanel”