« JavaFX Applets Meet Google Chrome | Main | Using the Java Deployment Toolkit with JavaFX Applets »

September 13, 2008

Vikings and Wizards in JavaFX

Stockholm-09-Horizontal

I have three objectives for today's article:

  1. Continue teaching you how to create UI custom controls in JavaFX.  This lesson is a new addition to the JFX Custom Nodes category, and it provides an infrastructure on which you can easily create "wizards".

  2. Show my appreciation to the JavaZone committee for inviting me to speak on JavaFX at JavaZone 2008 in Oslo, Norway, which occurred on September 17 & 18.

  3. Show my appreciation to the Jfokus committee for inviting me to speak on JavaFX at Jfokus in Stockholm, Sweden which will occur on February 27 & 28, 2009.  Also, as noted in the banner above, I will be teaching a two day public JavaFX course entitled "Rich Internet Application Development with JavaFX"

JfokusLogo

To accomplish these objectives, I've created a "wizard" (in the spirit of fun) in which you can discover whether you are a Viking or not.  Having visited Norway in the past, and having talked to the Jfokus leadership , I've found that Norwegians and Swedes tend to be very proud of their Viking heritage.  The program in this post was created out of respect for that sentiment, and I consulted one of the Norwegian JavaZone organizers for fun ideas to include in this wizard.  As with the rest of the series, the designers at Malden Labs created the graphical mock-ups and assets.  Here are a couple of screenshots of the wizard that I call "Are You a Viking?"

VikingWizard-Shaving   

As shown in the previous screen shot, this wizard asks the user questions, The user's response to a given question determine what wizard page will be shown next. If you answer the questions as a true Viking would, then the wizard will congratulate you with the page shown in the screenshot below:

VikingWizard-Congrats
 

Go ahead and try out the program.  You'll need JRE 6, and please note that Java SE 6 Update 10 will give you a faster deployment experience.

Creating a Wizard

In addition to using some of the classes from the JFX Custom Nodes category of the blog (namely the MenuNode, ButtonNode and DeckNode classes), this example introduces three more classes: WizardNode, WizardPoint and OptionsNode.  Before showing you the code for these new classes, I’d like you to see the main program in this "Are You a Viking?" example, which is in a file named WizardNodeExampleMain.fx:

/*
 *  WizardNodeExampleMain.fx - 
 *  An example of using the WizardNode custom node.  It also demonstrates
 *  the OptionsNode, MenuNode and ButtonNode custom nodes, as well as the
 *  WizardPoint class.
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes in JavaFX
 */
package com.javafxpert.wizard_node_example.ui;

import javafx.application.*;
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.image.*;
import javafx.scene.layout.*;
import javafx.scene.paint.*;
import javafx.scene.text.*;
import javafx.scene.transform.*;
import java.lang.System;
import com.javafxpert.custom_node.*;

var wizardRef:WizardNode;

Frame {
  var stageRef:Stage;
  var menuRef:MenuNode;
  var pageFont =
    Font {
      name: "Sans serif"
      size: 28
    };
  title: "WizardNode Example"
  width: 505
  height: 400
  visible: true
  stage:
    stageRef = Stage {
      fill: Color.BLACK
      content: [
        wizardRef = WizardNode {
          flowPath: 
            WizardPoint {
              nodeID: "Shaving"  // The id attribute of the Node to be displayed
              nextPoints: [
                WizardPoint {
                  advanceState: "ElectricRazor"
                  nodeID: "NotViking"
                },
                WizardPoint {
                  advanceState: "SafetyRazor"
                  nodeID: "NotViking"
                },
                WizardPoint {
                  advanceState: "StraightRazor"
                  nodeID: "NotViking"
                },
                WizardPoint {
                  advanceState: "ShaveWithSword"
                  nodeID: "DiscoveredAmerica"
                  nextPoints: [
                    WizardPoint {
                      advanceState: "ChristopherColumbus"
                      nodeID: "NotViking"
                    },
                    WizardPoint {
                      advanceState: "LeifEricson"
                      nodeID: "WearHelmetHowOften"
                      nextPoints: [
                        WizardPoint {
                          advanceState: "AlwaysWearsHelmet"
                          nodeID: "OwnWoodenBoat"
                          nextPoints: [
                            WizardPoint {
                              advanceState: "FiberglassBoat"
                              nodeID: "NotViking"
                            },
                            WizardPoint {
                              advanceState: "WoodenBoat"
                              nodeID: "IsViking"
                            },
                            WizardPoint {
                              advanceState: "NoBoat"
                              nodeID: "NotViking"
                            },
                          ]
                        },
                        WizardPoint {
                          advanceState: "WearsHelmetOnOccasions"
                          nodeID: "NotViking"
                        },
                        WizardPoint {
                          advanceState: "WearsHelmetOncePerWeek"
                          nodeID: "NotViking"
                        },
                        WizardPoint {
                          advanceState: "NeverWearsHelmet"
                          nodeID: "NotViking"
                        },
                      ]
                    },
                    WizardPoint {
                      advanceState: "TheBeatles"
                      nodeID: "NotViking"
                    },
                  ]
                },
              ]
            }
          fadeInDur: 700ms
          canCancel: true
          canFinish: false
          backgroundNode:
            Group {
              content: [
                ImageView {
                  image: 
                    Image {
                      url: "{__DIR__}images/viking_jprep_background.png"
                    } 
                },
                ImageView {
                  image: 
                    Image {
                      url: "{__DIR__}images/viking_jprep_helmet.png"
                    } 
                },
              ]
            }
          content: [
            Group {
              var optionsNode:OptionsNode;
              transform: Translate.translate(50, 130);
              id: "Shaving"
              content: [
                optionsNode = OptionsNode {
                  heading: "How Do You Shave?"
                  options: [
                    "I shave with an electric razor",
                    "I shave with a safety razor",
                    "I shave with a straight razor",
                    "I shave with my sword",
                  ]
                  optionAdvanceStates: [
                    "ElectricRazor",
                    "SafetyRazor",
                    "StraightRazor",
                    "ShaveWithSword"
                  ]
                  action:
                    function():Void {
                      wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;  
                    }
                }
              ]
            },
            Group {
              var optionsNode:OptionsNode;
              transform: Translate.translate(50, 130);
              id: "DiscoveredAmerica"
              content: [
                optionsNode = OptionsNode {
                  heading: "Who Discovered America?"
                  options: [
                    "Christopher Columbus",
                    "Leif Ericson",
                    "The Beatles",
                  ]
                  optionAdvanceStates: [
                    "ChristopherColumbus",
                    "LeifEricson",
                    "TheBeatles",
                  ]
                  action:
                    function():Void {
                      wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;  
                    }
                }
              ]
            },
            Group {
              var optionsNode:OptionsNode;
              transform: Translate.translate(50, 130);
              id: "WearHelmetHowOften"
              content: [
                optionsNode = OptionsNode {
                  heading: "How Often Do You Wear Your Helmet?"
                  options: [
                    "I always wear it, even in bed",
                    "Only on special occasions",
                    "Once a week",
                    "Never - it gives me Helmet Hair"
                  ]
                  optionAdvanceStates: [
                    "AlwaysWearsHelmet",
                    "WearsHelmetOnOccasions",
                    "WearsHelmetOncePerWeek",
                    "NeverWearsHelmet",
                  ]
                  action:
                    function():Void {
                      wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;  
                    }
                }
              ]
            },
            Group {
              var optionsNode:OptionsNode;
              transform: Translate.translate(50, 130);
              id: "OwnWoodenBoat"
              content: [
                optionsNode = OptionsNode {
                  heading: "Do You Own a Wooden Boat?"
                  options: [
                    "No, mine is fiberglass",
                    "Of course it's made out of wood!",
                    "I don't own a boat",
                  ]
                  optionAdvanceStates: [
                    "FiberglassBoat",
                    "WoodenBoat",
                    "NoBoat",
                  ]
                  action:
                    function():Void {
                      wizardRef.candidateAdvanceState = optionsNode.optionAdvanceState;  
                    }
                }
              ]
            },
            HBox {
              id: "IsViking"
              transform: Translate.translate(50, 110);
              content: [
                ImageView {
                  image: 
                    Image {
                      url: "{__DIR__}images/viking_yes.png"
                    } 
                },
                VBox {
                  transform: Translate.translate(0, 20);
                  spacing: 40
                  content: [
                    Text {
                      textOrigin: TextOrigin.TOP
                      content: "Congratulations!"
                      fill: Color.WHITE
                      font: pageFont
                    },
                    Text {
                      textOrigin: TextOrigin.TOP
                      content: "You are a Viking!"
                      fill: Color.WHITE
                      font: pageFont
                    },
                  ]
                }
              ]
            },
            HBox {
              id: "NotViking"
              transform: Translate.translate(50, 110);
              content: [
                ImageView {
                  image: 
                    Image {
                      url: "{__DIR__}images/viking_no.png"
                    } 
                },
                VBox {
                  transform: Translate.translate(0, 20);
                  spacing: 40
                  content: [
                    Text {
                      textOrigin: TextOrigin.TOP
                      content: "Sorry! You are most"
                      fill: Color.WHITE
                      font: pageFont
                    },
                    Text {
                      textOrigin: TextOrigin.TOP
                      content: "likely not a Viking!"
                      fill: Color.WHITE
                      font: pageFont
                    },
                  ]
                }
              ]
            },
          ]
        },
        menuRef = MenuNode {
          translateX: bind stageRef.width / 2 - menuRef.getWidth() / 2
          translateY: bind stageRef.height - menuRef.getHeight() * 0.75
          buttons: [
            ButtonNode {
              imageURL: "{__DIR__}images/viking_jprep_btn_previous.png"
              enabled: bind wizardRef.canGoBack
              scale: 1.0
              action:
                function():Void {
                  wizardRef.goBack();
                }
            },
            ButtonNode {
              imageURL: "{__DIR__}images/viking_jprep_btn_next.png"
              enabled: bind wizardRef.canAdvance
              scale: 1.0
              action:
                function():Void {
                  wizardRef.advance();
                }
              },
            ButtonNode {
              imageURL: "{__DIR__}images/viking_jprep_btn_cancel.png"
              enabled: bind wizardRef.canCancel
              scale: 1.0
              action:
                function():Void {
                  wizardRef.reset();
                }
            },
            ButtonNode {
              imageURL: "{__DIR__}images/viking_jprep_btn_finish.png"
              enabled: bind wizardRef.atEndpoint
              scale: 1.0
              action:
                function():Void {
                  wizardRef.reset();
                }
            },
          ]
        }
      ]
    }
  closeAction:
    function():Void {
      System.exit(0);
    }
}

As shown above, one of the important features of the wizard is what I call its flow path. The flowPath attribute of the WizardNode class enables you to articulate all of the points in the wizard’s flow, as well as to articulate the navigation paths between points. Each point in a wizard’s flow path is represented by a WizardPoint instamce, and each navigation path between points is represented by an element in the nextPoints sequence attribute. The nextPoints attribute contains a sequence of WizardPoint instances, each of which holds an advanceState string attribute that enables navigation to that point. As you can see from the listing above, each WizardPoint instance also contains a nodeID attribute that matches the id attribute of a Node (held within the content sequence attribute of the WizardNode). Looking at that attribute in the listing above, you’ll notice that all of the wizard pages (Node instances) are held in that sequence. As you’ll soon see, it is the responsibility of the WizardNode class to cause the associated Node instance to show after navigating to a given wizard point.

Another important attribute in the WizardNode class is backgroundNode. This attribute enables you to create a background that will show behind each page of the wizard. Take a look at the WizardNode.fx listing below, and notice that it extends the DeckNode class, which contains the functionality of switching between Node instances by their id attributes.


/*
 *  WizardNode.fx - 
 *  A custom node that functions as a "wizard", progressively displaying
 *  UI nodes according to a supplied "flow".  Each node, typically as a result
 *  of user interaction, may set a state that determines which node of the flow
 *  will be displayed. 
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes in JavaFX
 */

package com.javafxpert.custom_node;
 
import javafx.lang.*;
import java.lang.System;

/*
 *  A custom node that functions as a "wizard", progressively displaying
 *  UI nodes according to a supplied "flow".  Each node, typically as a result
 *  of user interaction, may set a state that determines which node of the flow
 *  will be displayed. 
 */
public class WizardNode extends DeckNode { 

  /**
   * The flow of the wizard, expressed as a series of related
   * WizardPoint instances
   */
  public attribute flowPath:WizardPoint;
  
  /**
   * The current path that the user is taking through the wizard, expressed as
   * a sequence of WizardPoint instances
   */
  public attribute currentPath:WizardPoint[];

  /**
   * Determines whether the user can advance to the next wizard page
   */
  public attribute canAdvance:Boolean = false;

  /**
   * Determines whether the user can go back to the previous wizard page
   */
  public attribute canGoBack:Boolean = bind sizeof currentPath > 1;

  /**
   * Determines whether the user can cancel out of this wizard
   */
  public attribute canCancel:Boolean;

  /**
   * Determines whether the user can finish this wizard
   */
  public attribute canFinish:Boolean;

  /**
   * Determines whether the user is at and end point in the wizard,
   * which is defined by not having any nextPoints
   */
  public attribute atEndpoint:Boolean = bind
    (sizeof currentPoint.nextPoints) == 0;

  /**
   * A reference to the WizardPoint instance that the user is currently on
   */
  private attribute currentPoint:WizardPoint on replace {
    if (currentPoint != null) {
      visibleNodeId = currentPoint.nodeID;
      canAdvance = false;
    }
  };

  postinit {
    reset();
  }

  /**
   * Resets the wizard to its initial state
   */
  public function reset():Void {
    currentPoint = flowPath;
    currentPath = [];
    insert currentPoint into currentPath;
  }

  /**
   * The requested advanceState to be navigated to from this point.  If this is
   * a valid advanceState, the canAdvance attribute is set to true, otherwise
   * it is set to false
   */
  public attribute candidateAdvanceState:String on replace {
    if (candidateAdvanceState != "") {
      var newPoint = currentPoint.getPointByAdvanceState(candidateAdvanceState);
      canAdvance = newPoint != null;
    }
  }

  /**
   * Request that the current WizardPoint be changed to the candidateAdvanceState
   * TODO: Consider throwing an exception if unsuccessful
   */
  public function advance():Void {
    var newPoint = currentPoint.getPointByAdvanceState(candidateAdvanceState);
    if (newPoint != null) {
      currentPoint = newPoint;
      
      // Put this WizardPoint on the current path through the flow path
      insert currentPoint into currentPath;
    }
  }

  /**
   * Request that the current WizardPoint be changed to the one that precedes
   * it in the current path that the user has taken through the flow.  In other
   * words, go back.
   * TODO: Consider throwing an exception if unsuccessful
   */
  public function goBack():Void {
    var idx = sizeof currentPath - 1;
    if (idx > 0) { 
      delete currentPath[idx];    
      // Put this WizardPoint on the current path through the flow path
      currentPoint = currentPath[idx - 1];
    }
  }
}  


Here is the listing for the WizardPoint class mention previously:

/*
 *  WizardPoint.fx - 
 *  A point in the flow of a "wizard", that is related to a given wizard page
 *  (graphical Node).  Wizard point instances are related to each other in
 *  a network of points that determines the potential flows within the wizard.
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes in JavaFX
 */

package com.javafxpert.custom_node;
 
import java.lang.System;

/**
 *  A point in the flow of a "wizard", that is related to a given wizard page
 *  (graphical Node).  Wizard point instances are related to each other in
 *  a network of points that determines the potential flows within the wizard.
 */
public class WizardPoint { 

  /**
   * The id attribute of the Node (wizard page) to be displayed
   */
  public attribute nodeID:String;

  /**
   * A String that controls the condition by which this WizardPoint may be
   * landed upon. If advanceState is an empty string, then this point may be 
   * landed upon by default
   */
  public attribute advanceState:String;

  /**
   * The WizardPoint instances that may be navigated to from this point.
   */
  public attribute nextPoints:WizardPoint[]; //TODO make sure that this creates an empty sequence, not a null reference

  /**
   * Returns the WizardPoint instance that may be navigated to from this point
   * for a given advanceState
   * TODO: Decide whether this is needed
   */
  public function getPointByAdvanceState(advanceState:String):WizardPoint {
    var points = 
      for (point in nextPoints where point.advanceState == advanceState) point;
    if (sizeof points > 0) points[0]
    else null;
  }
}  


Notice also in the WizardNodeExampleMain.fx listing shown previously that I'm using an OptionsNode custom node in some of the wizard pages. This class makes it quick and easy to articulate a set of mutually exclusive options as well as a heading for the list of options. It was designed for use with this wizard functionality, as it is capable of holding an advanceState for each of the options and storing the advanceState for an option that the user selected. Take a look at the listing for the OptionsNode.fx class below:

/*
 *  OptionsNode.fx - 
 *  A custom node that enables the user to choose 
 *  from a list of options.
 *
 *  Developed 2008 by James L. Weaver (jim.weaver at lat-inc.com)
 *  to demonstrate how to create custom nodes in JavaFX
 */

package com.javafxpert.custom_node;
 
import javafx.ext.swing.*;
import javafx.scene.*;
import javafx.scene.paint.*;
import java.lang.System;

public class OptionsNode extends CustomNode { 

  /**
   * A header for the options
   */
  public attribute heading:String;
    
  /**
   * A sequence containing the choice of options
   */
  public attribute options:String[];
  
  /**
   * A sequence containing the WizardNode advanceState associated with the
   * options.
   */
  public attribute optionAdvanceStates:String[];
  
  /**
   * The font to use for the heading
   */
  public attribute headingFont = 
    Font {
      name: "Sans serif"
      size: 24
    };
  
  /**
   * The font to use for the options
   */
  public attribute optionsFont = 
    Font {
      name: "Sans serif"
      size: 22
    };
  
  /**
   * The color to use for the text
   */
  public attribute textColor = Color.rgb(253, 253, 253);
    
  /**
   * The advanceState associated with a selected option
   */
  public attribute optionAdvanceState:String;
    
  /**
   * The action function attribute that is executed when the
   * a selection is made
   */
  public attribute action:function():Void;
   
  /**
   * Reset the selected radio buttons
   */
  public function reset():Void {
    toggleGroup.clearSelection(); 
  }

  /**
   * Called when this page is shown in the wizard.  Reset the radio button
   * selections
   */
  public function onShow():Void {
    reset(); 
  }

  /**
   * A reference to the ToggleGroup associated with the RadioButton instances
   */ 
  private attribute toggleGroup:ToggleGroup = ToggleGroup{}; 
   
  /**
   * Create the Node
   */
  public function create():Node {
    ComponentView {
      component:
        GridPanel {
          background: 
            Color.rgb(255, 255, 255, 0.0) // Transparent
          rows: bind sizeof options + 1
          columns: 1
          content: bind [
            Label {
              text: heading
              font: headingFont
              foreground: textColor
            },
            for (option in options)
              RadioButton {
                var idx:Integer = indexof option;
                toggleGroup: toggleGroup
                text: option
                font: optionsFont
                foreground: textColor
                action:
                  function():Void {
                    optionAdvanceState = optionAdvanceStates[idx];
                    action();  
                  }
              }
          ]
        }
    }
  }
}  


Room for Improvement

There is at least one area of functionality in which I'd like to make an improvement:  Currently, a wizard page is a subclass of Node, but it may be good to define a custom node (perhaps named WizardPageNode) that has life-cycle functions and some helper functionality.  I welcome your thoughts on this, as well as any other input or questions that you have.  In either case, please post a comment.

Also, if you're at JFocus, please introduce yourself as I'd like to meet you!

Thanks,
Jim Weaver
JavaFXpert.com

TrackBack

TrackBack URL for this entry:
http://www.typepad.com/services/trackback/6a00e54f133d6988340105349f4223970b

Listed below are links to weblogs that reference Vikings and Wizards in JavaFX:

Comments

"did you check your code @ JavaFX 1.0 (Dec 4th 2008) because I am trying the code you supplied and i got a lot of errors"

adr choul,

All of the posts in this JFX Custom Nodes category were written in the JavaFX Preview SDK (prior to the JavaFX SDK 1.0 release), and some of the syntax is different. I plan to update it.

Thanks,
Jim Weaver

very good effort but did you check your code @ JavaFX 1.0 (Dec 4th 2008) because I am trying the code you supplied and i got a lot of errors ... e.g netBeans says it doesnt support "attribute" anymore.. only def or var.. any suggestions?

i get a lot of errors from this tutorial when i try it out @ NetBeans 6.5 with JavaFX 1.0(4th December 2008).

create a Java FX project with 2 packages (com.javafxpert.custom_node and com.javafxpert.wizard_node_example.ui) and 4 classes OptionsNode.fx WizardNode.fx WizardPoint.fx(1st package) and WizardNodeExampleMain(2nd package)

A lot of errors, e.g it doesnt accept attribute as a parameter, it only accepts def or var(Netbeans), also cannot find symbol Font, cannot find symbol ToggleGroup, doesnt allow public function create in OptionsNode.fx, cant extend DeckNode in WizardNode.fx etc.. any ideas?

"I don't see the backgroundNode Attribute declared in the WizardNode, am I missing something?"

Dear gc,

Very nice catch! It is inherited from the DeckNode class, whose listing I didn't update in the "Getting Decked" post. The code in that post is now updated:
http://learnjavafx.typepad.com/weblog/2008/07/getting-decked.html

Thanks!
Jim Weaver

I don't see the backgroundNode Attribute declared in the WizardNode, am I missing something?

Very nice,
But font is nearly unreadable :-(
Ubuntu Linux with
java version "1.6.0_07"
Java(TM) SE Runtime Environment (build 1.6.0_07-b06)
Java HotSpot(TM) Server VM (build 10.0-b23, mixed mode)

You can see a Screenshot here:
http://habales.de/img/vikings.png

"The font color for questions isn't rendered correctly in JRE 6_01 under Windows XP 64 bit."

Rovas,

What color are you seeing? Would you please email a screenshot to me (my email address is in the About link in the margin).

Thanks,
Jim Weaver

Very nice.

The font color for questions isn' t rendered correctly in JRE 6_01 under Windows XP 64 bit.
kyber vikings did shave and groomed their hair. Read here http://en.wikipedia.org/wiki/Viking#Common_misconceptions

kyber,

Good catch! I'll have to say something (nicely) to my Viking consultants :-)

Jim Weaver

That one is a trick question. Vikings don't shave.

Verify your Comment

Previewing your Comment

This is only a preview. Your comment has not yet been posted.

Working...
Your comment could not be posted. Error type:
Your comment has been posted. Post another comment

The letters and numbers you entered did not match the image. Please try again.

As a final step before posting your comment, enter the letters and numbers you see in the image below. This prevents automated programs from posting comments.

Having trouble reading this image? View an alternate.

Working...

Post a comment

My Photo

Upcoming Speaking Engagements:


  • Stephen Chin and Jim Weaver speaking about JavaFX Platform

  • Speaking on JavaFX and Java at Øredev in Malmö, Sweden on 2-6 November, 2009

Upcoming JavaFX Training:


  • Developing Secure, Rich Internet Applications Hosted on a Variety of Clients Using JavaFX Technology

Enter your email address:

Delivered by FeedBurner

Available now as early access eBook


  • Click book image above to obtain eBook

Twitter Updates

    follow me on Twitter

    Affiliations:

    DZone Links:


    July 2009

    Sun Mon Tue Wed Thu Fri Sat
          1 2 3 4
    5 6 7 8 9 10 11
    12 13 14 15 16 17 18
    19 20 21 22 23 24 25
    26 27 28 29 30 31  

    Disclaimer:

    • By reading this site, you are agreeing that under no circumstances will Veriana Networks, Inc. or its affiliates be responsible for (1) any information contained on or omitted from the site, (2) any person's reliance on any such information, whether or not the information is correct, current or complete, (3) the consequences of any action you or any other person takes or fails to take, whether or not based on information provided by or as a result of the use of the sites.