Tag Archives: node

JavaFX 2 GameTutorial Part 3

The Expanse

figure 1 The Expanse – Input (Keyboard & Mouse) Demo

Introduction

This is part 3 of a six part series related to a JavaFX 2 Game Tutorial. If you’ve missed Part 1 and Part 2, I encourage you to go through them before beginning this tutorial. To recap Part 2 I discussed the inner workings of a gaming loop where we used an animation (JavaFX Timeline) to update sprites, check collisions, and clean up game world elements. I then felt compelled to create a simple game engine to enable the ease of developing 2D games. This tutorial is about using the game engine and demonstrating input using your mouse and keyboard. In this tutorial I will give you some background history, event handling fundamentals, a demo game, and finally the implementation. The demo will showcase a spaceship capable of shooting at floating spheres similar to the video game Asteroids. If you want to run the demo, scroll down and click on the WebStart button below. Please read the requirements before launching the game.

History

Back in the day (during the 80s) as a kid growing up there were arcade centers, bowling alleys, pizza parlors, and 7 Eleven stores where I spent huge amounts of time putting quarters on the glass display areas to be next in line to the guy who was currently playing an intense video game. As everyone was crowded around him watching him beat the all time high score we all cheered as we witnessed greatness. One of those incredibly awesome arcade games was ‘Asteroids‘ created by Atari Inc.(to play visit play.vg)

Speaking of high scores, not too many folks know, but Scott Safran (February 3, 1967 – March 27, 1989) had the highest record of all time playing Asteroids. He achieved this at his local 7-Eleven conveniece store by playing for approximately twenty hours non stop. Later in life (while still young), he passed away from a tragic accident on March 27, 1989. In honor of Scott, I created this tutorial. I hope people will remember him as one of the greatest video gamers of all time (I’m sure a good brother and son also).

Regarding the game, Asteroids, vector based hardware was used to render shapes as opposed to raster graphics (bitmap). On an added note, Space Invaders by Midway Inc. was created using raster graphics. It’s exciting to point out that there are discussions about JavaFX 2.x having the ability to use bitmaps called the JavaFX Canvas Node which can provide raster graphics to enable developers to take advantage of pixel level manipulations. I am still amazed at the construction of these arcade style cabinets which house the CRT, motherboard, and the controllers (input devices) such as buttons, joystick, track balls, and turning knobs.

Classic Arcade Games

Below are some classic arcade games with many types of input devices:

  • Buttons only: Asteroids, Space Invaders, Rip Off, Phoenix
  • Joystick only: Q*bert, PacMan
  • Turn knob only: Pong
  • Track ball only: Marble Madness
  • Steering column and buttonsStar Wars, Pole position, Spy Hunter
  • Bike handle bars: Stunt Cycle, Paper Boy
  • Buttons and Throttle bar: Lunar Lander
  • Periscope and Buttons: Sea Wolf
  • Buttons and Yoke: Tron, Battle Zone
  • Buttons, Turn knob, and Yoke:  Star Trek, Tempest
  • Buttons and track ball: Missile Command, Centipede
  • Buttons and Joystick: Defender, Gauntlet, Frogger, Joust, Berzerk, Mario Bros., Donkey Kong, Xevious, Galaga, Kung Fu, Contra, Street Fighter, Double Dragon, Ninja magic (or spirit), DigDug, Dragon’s Lair.

Input / (Mouse, Keyboard)

Leaving the past behind, we currently encounter new kinds of input devices such as touch screens, accelerometers, infrared receivers, cameras, etc. The most common input on the desktop today is the mouse and keyboard. Of course, touch screen is all the rage with mobile devices and tablets, however in this tutorial we will only be focusing on the ‘Mouse‘ and ‘Keyboard‘ as inputs to control your game. Based on the JavaFX Roadmapmulti-touch input is in the works (by the time you read this it’s already implemented).

When intercepting keyboard and mouse events, JavaFX 2.x has many types of events which provide an opportunity for the developer to implement event handlers that intercept the events triggered. The JavaFX 2.x API for the Node or Scene contains many methods having the prefix ‘on’ such as the onMousePressProperty() or onKeyPressProperty() method. Whenever you implement these methods you will simply implement the handle() method using Java’s generic type to specify the event object to be passed in for interrogation. So, when you instantiate an EventHandler<MouseEvent> class you will implement a handle() method that takes a MouseEvent as a parameter to be passed in.

The code snippets shown below add two event handlers to the JavaFX Scene. The first handler will respond to mouse events. In our simple game when a mouse press occurs, this handler will respond by firing the weapon or navigating the ship. The second handler shown below will respond to key event. When a key is pressed, this handler will process KeyEvent objects. In our game, the keystroke ‘2‘ will change your secondary weapon into a bigger blaster (slower). Any other keystroke will default back to the smaller blaster (faster).

Move ship and Fire weapon

        EventHandler fireOrMove = new EventHandler() {
            @Override
            public void handle(MouseEvent event) {
                if (event.getButton() == MouseButton.PRIMARY) {
                   // Fire weapon systems. On Windows left mouse button
                } else if (event.getButton() == MouseButton.SECONDARY) {
                   // Navigate ship thrust. On Windows right mouse button
                }
            }
        };
        primaryStage.getScene().setOnMousePressed(fireOrMove);

Changing the weapon

        EventHandler changeWeapons = new EventHandler() {
           @Override
           public void handle(KeyEvent event) {
               myShip.changeWeapon(event.getCode());
           }
        };
        primaryStage.getScene().setOnKeyPressed(changeWeapons);

JavaFX 2 Input Demo – ‘The Expanse’

The simple demo game will be a mix between StarCraft and Asteroids. When using the mouse to navigate the ship it will resemble StarCraft’s Battle Cruiser. If you remember from Part 2 of this series, I created spheres bouncing around. I reused the code from Part 2 ‘Atom Smasher’ to act as asteroids like in the famous arcade game. Except in this game you cannot get harmed at all. The objective is to fire your weapon at the spheres before they hit other spheres which implode upon impact. Because this is a simple tutorial, or even a game in its early stages of development, the game doesn’t keep track of the score. I encourage you to go to GitHub to download the code and enhance the game. Later, you will see a high level UML class diagram describing the classes that make up the game. For the sake of brevity, I will not be going through each class in great detail, but I trust you will visit GitHub here: https://github.com/carldea/JFXGen for all the demos and source code.

Requirements:

  • Java 7 or later
  • JavaFX 2.1 or later
  • Windows XP or later (Should be available soon for Linux/MacOS)
A simple Asteroid type game called ‘The Expanse’.

Instructions:

  • Right mouse click (on Windows) to fly ship.
  • Left mouse click (left click on Windows mouse) to fire weapon.
  • Key press ‘2’ to change to large missiles. (blue circular projectiles)
  • Other key press defaults to smaller missiles. (red circular projectiles)
Part 3 Demo
Part 3 ‘The Expanse’

Shown below is figure 2 of a high level class diagram depicting all the classes created for this demo. The GameWorld and Sprite classes are part of the game engine from the previous post. The rest of the classes are new which make up this demo for this tutorial.


InputPart3

The InputPart3 is the driver or main JavaFX application that runs the game. This creates a GameWorld object to be initialized and starts the game loop.
Shown below is the source code of the main JavaFX application InputPart3 . Click to expand.

package carlfx.demos.navigateship;

import carlfx.gameengine.GameWorld;
import javafx.application.Application;
import javafx.stage.Stage;

/**
 * The main driver of the game.
 * @author cdea
 */
public class InputPart3 extends Application {

    GameWorld gameWorld = new TheExpanse(59, "JavaFX 2 GameTutorial Part 3 - Input");
    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) {
        // setup title, scene, stats, controls, and actors.
        gameWorld.initialize(primaryStage);

        // kick off the game loop
        gameWorld.beginGameLoop();

        // display window
        primaryStage.show();
    }

}

TheExpanse

The TheExpanse class inherits from the GameWorld class.  This is practically identical to Part 2’s ‘AtomSmasher’ where the driver application will invoke the GameWorld instance’s initialize() method to set up all the game elements such as the input, spaceship, and those pesky floating spheres. The job of this class is to make sure asteroids or spheres bounce off walls and remove any missiles which reach the edge of the screen. It’s main responsibly is to manage the assets and create new levels. When there are no moving objects and the player moves the ship on the screen new spheres will be generated for the next level. The key take away from this class is the setupInput() method. The setupInput() method which I created is responsible for establishing your event handlers to be able to listen to key events and mouse event.

package carlfx.demos.navigateship;

import carlfx.gameengine.GameWorld;
import carlfx.gameengine.Sprite;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.scene.input.MouseButton;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.stage.Stage;

import java.util.Random;

/**
 * This is a simple game world simulating a bunch of spheres looking
 * like atomic particles colliding with each other. When the game loop begins
 * the user will notice random spheres (atomic particles) floating and
 * colliding. The user will navigate his/her ship by right clicking the mouse to
 * trust forward and left click to fire weapon to atoms.
 *
 * @author cdea
 */
public class TheExpanse extends GameWorld {

    // mouse pt label
    Label mousePtLabel = new Label();

    // mouse press pt label
    Label mousePressPtLabel = new Label();

    TextField xCoordinate = new TextField("234");
    TextField yCoordinate = new TextField("200");
    Button moveShipButton = new Button("Rotate ship");

    Ship myShip = new Ship();

    public TheExpanse(int fps, String title) {
        super(fps, title);
    }

    /**
     * Initialize the game world by adding sprite objects.
     *
     * @param primaryStage The game window or primary stage.
     */
    @Override
    public void initialize(final Stage primaryStage) {
        // Sets the window title
        primaryStage.setTitle(getWindowTitle());
        //primaryStage.setFullScreen(true);

        // Create the scene
        setSceneNodes(new Group());
        setGameSurface(new Scene(getSceneNodes(), 800, 600));
        getGameSurface().setFill(Color.BLACK);
        primaryStage.setScene(getGameSurface());
        // Setup Game input
        setupInput(primaryStage);

        // Create many spheres
        generateManySpheres(2);

        // Display the number of spheres visible.
        // Create a button to add more spheres.
        // Create a button to freeze the game loop.
        //final Timeline gameLoop = getGameLoop();
        getSpriteManager().addSprites(myShip);
        getSceneNodes().getChildren().add(myShip.node);

        // mouse point
        VBox stats = new VBox();

        HBox row1 = new HBox();
        mousePtLabel.setTextFill(Color.WHITE);
        row1.getChildren().add(mousePtLabel);
        HBox row2 = new HBox();
        mousePressPtLabel.setTextFill(Color.WHITE);
        row2.getChildren().add(mousePressPtLabel);

        stats.getChildren().add(row1);
        stats.getChildren().add(row2);

        // mouse point
        HBox enterCoord1 = new HBox();
        enterCoord1.getChildren().add(xCoordinate);
        enterCoord1.getChildren().add(yCoordinate);
        enterCoord1.getChildren().add(moveShipButton);
        stats.getChildren().add(enterCoord1);
        moveShipButton.setOnAction(new EventHandler() {
            @Override
            public void handle(ActionEvent actionEvent) {
                double x = Double.parseDouble(xCoordinate.getText());
                double y = Double.parseDouble(yCoordinate.getText());
                myShip.plotCourse(x, y, false);
            }
        });

        // ===================================================
        // Debugging purposes
        // uncomment to test mouse press and rotation angles.
        //getSceneNodes().getChildren().add(stats);
    }

    /**
     * Sets up the mouse input.
     *
     * @param primaryStage The primary stage (app window).
     */
    private void setupInput(Stage primaryStage) {
        System.out.println("Ship's center is (" + myShip.getCenterX() + ", " + myShip.getCenterY() + ")");

        EventHandler fireOrMove = new EventHandler() {
            @Override
            public void handle(MouseEvent event) {
                mousePressPtLabel.setText("Mouse Press PT = (" + event.getX() + ", " + event.getY() + ")");
                if (event.getButton() == MouseButton.PRIMARY) {
                    // Aim
                    myShip.plotCourse(event.getX(), event.getY(), false);
                    // fire
                    Missile m1 = myShip.fire();
                    getSpriteManager().addSprites(m1);
                    getSceneNodes().getChildren().add(0, m1.node);
                } else if (event.getButton() == MouseButton.SECONDARY) {
                    // determine when all atoms are not on the game surface. Ship should be one sprite left.
                    if (getSpriteManager().getAllSprites().size()                         generateManySpheres(30);
                    }

                    // stop ship from moving forward
                    myShip.applyTheBrakes(event.getX(), event.getY());
                    // move forward and rotate ship
                    myShip.plotCourse(event.getX(), event.getY(), true);
                }

            }
        };

        // Initialize input
        primaryStage.getScene().setOnMousePressed(fireOrMove);
        //addEventHandler(MouseEvent.MOUSE_PRESSED, me);

        // set up stats
        EventHandler changeWeapons = new EventHandler() {
            @Override
            public void handle(KeyEvent event) {
                myShip.changeWeapon(event.getCode());
            }
        };
        primaryStage.getScene().setOnKeyPressed(changeWeapons);

        // set up stats
        EventHandler showMouseMove = new EventHandler() {
            @Override
            public void handle(MouseEvent event) {
                mousePtLabel.setText("Mouse PT = (" + event.getX() + ", " + event.getY() + ")");
            }
        };

        primaryStage.getScene().setOnMouseMoved(showMouseMove);
    }

    /**
     * Make some more space spheres (Atomic particles)
     *
     * @param numSpheres The number of random sized, color, and velocity atoms to generate.
     */
    private void generateManySpheres(int numSpheres) {
        Random rnd = new Random();
        Scene gameSurface = getGameSurface();
        for (int i = 0; i < numSpheres; i++) {             Color c = Color.rgb(rnd.nextInt(255), rnd.nextInt(255), rnd.nextInt(255));             Atom b = new Atom(rnd.nextInt(15) + 5, c, true);             Circle circle = b.getAsCircle();             // random 0 to 2 + (.0 to 1) * random (1 or -1)             b.vX = (rnd.nextInt(2) + rnd.nextDouble()) * (rnd.nextBoolean() ? 1 : -1);             b.vY = (rnd.nextInt(2) + rnd.nextDouble()) * (rnd.nextBoolean() ? 1 : -1);             // random x between 0 to width of scene             double newX = rnd.nextInt((int) gameSurface.getWidth());             // check for the right of the width newX is greater than width              // minus radius times 2(width of sprite)             if (newX > (gameSurface.getWidth() - (circle.getRadius() * 2))) {
                newX = gameSurface.getWidth() - (circle.getRadius() * 2);
            }

            // check for the bottom of screen the height newY is greater than height
            // minus radius times 2(height of sprite)
            double newY = rnd.nextInt((int) gameSurface.getHeight());
            if (newY > (gameSurface.getHeight() - (circle.getRadius() * 2))) {
                newY = gameSurface.getHeight() - (circle.getRadius() * 2);
            }

            circle.setTranslateX(newX);
            circle.setTranslateY(newY);
            circle.setVisible(true);
            circle.setId(b.toString());
            circle.setCache(true);
            circle.setCacheHint(CacheHint.SPEED);
            circle.setManaged(false);
            // add to actors in play (sprite objects)
            getSpriteManager().addSprites(b);

            // add sprite's
            getSceneNodes().getChildren().add(0, b.node);

        }
    }

    /**
     * Each sprite will update it's velocity and bounce off wall borders.
     *
     * @param sprite - An atomic particle (a sphere).
     */
    @Override
    protected void handleUpdate(Sprite sprite) {
        // advance object
        sprite.update();
        if (sprite instanceof Missile) {
            removeMissiles((Missile) sprite);
        } else {
            bounceOffWalls(sprite);
        }
    }

    /**
     * Change the direction of the moving object when it encounters the walls.
     *
     * @param sprite The sprite to update based on the wall boundaries.
     *               TODO The ship has got issues.
     */
    private void bounceOffWalls(Sprite sprite) {
        // bounce off the walls when outside of boundaries

        Node displayNode;
        if (sprite instanceof Ship) {
            displayNode = sprite.node;//((Ship)sprite).getCurrentShipImage();
        } else {
            displayNode = sprite.node;
        }
        // Get the group node's X and Y but use the ImageView to obtain the width.
        if (sprite.node.getTranslateX() > (getGameSurface().getWidth() - displayNode.getBoundsInParent().getWidth()) ||
                displayNode.getTranslateX() < 0) {             // bounce the opposite direction             sprite.vX = sprite.vX * -1;         }         // Get the group node's X and Y but use the ImageView to obtain the height.         if (sprite.node.getTranslateY() > getGameSurface().getHeight() - displayNode.getBoundsInParent().getHeight() ||
                sprite.node.getTranslateY() < 0) {             sprite.vY = sprite.vY * -1;         }     }     /**      * Remove missiles when they reach the wall boundaries.      *      * @param missile The missile to remove based on the wall boundaries.      */     private void removeMissiles(Missile missile) {         // bounce off the walls when outside of boundaries         if (missile.node.getTranslateX() > (getGameSurface().getWidth() -
                missile.node.getBoundsInParent().getWidth()) ||
                missile.node.getTranslateX() < 0) {             getSpriteManager().addSpritesToBeRemoved(missile);             getSceneNodes().getChildren().remove(missile.node);         }         if (missile.node.getTranslateY() > getGameSurface().getHeight() -
                missile.node.getBoundsInParent().getHeight() ||
                missile.node.getTranslateY() < 0) {

            getSpriteManager().addSpritesToBeRemoved(missile);
            getSceneNodes().getChildren().remove(missile.node);
        }
    }

    /**
     * How to handle the collision of two sprite objects. Stops the particle
     * by zeroing out the velocity if a collision occurred.
     *
     * @param spriteA Sprite from the first list.
     * @param spriteB Sprite from the second list.
     * @return boolean returns a true if the two sprites have collided otherwise false.
     */
    @Override
    protected boolean handleCollision(Sprite spriteA, Sprite spriteB) {
        if (spriteA != spriteB) {
            if (spriteA.collide(spriteB)) {
                if (spriteA instanceof Atom && spriteB instanceof Atom) {

                    ((Atom) spriteA).implode(this); // will remove from the Scene onFinish()
                    ((Atom) spriteB).implode(this);
                    getSpriteManager().addSpritesToBeRemoved(spriteA, spriteB);

                    return true;
                }
            }
        }

        return false;
    }

}

Ship

The Ship class represents our cool looking spaceship. The Ship class inherits from the Sprite class to help us contain velocity information (vector). This class will also contain a doubly linked list containing 32 ImageView (RotatedShipImage) instances which represent each direction to simulate the ship rotating about its center (centroid). At some point I want to change this by making a single SVGPath object to be rotated (I know there are trade offs). For this tutorial I implemented the ship by taking ImageViews objects to be rotated 32 direction evenly from 0 to 360 degrees. Shown below in Figure 3 are all 32 directions using 32 ImageView instances and a single Image object of the image of a spaceship to simulate the rotation about its center (pivot point).

Figure 3: 32 Directions to simulate a rotation

When animating the ship rotating I simply set all but the current image visible using the setVisible(true) method on the ImageView node.

Disclaimer: In gaming you will inevitably encounter math (Trigonometry). If you are interested and want to dig deeper please look at the source code of the TheExpanse class’ initialize() method. At the end of the method uncomment the statement: getSceneNodes().getChildren().add(stats);  . This will display controls which will allow you to use to debug and inspect mouse press coordinates. Also, you can see output in your console (stdout) relating to angles, vectors, etc.

The Ship’s member variables:

  • turnDirection – enum DIRECTION with Clockwise, CounterClockwise, and Neither
  • u – Vec object which contains a vector in relation to the center of the ship coordinates denoting the starting direction the ship begins to rotate
  • directionalShips – list of RotatedShipImage objects each having a previous and next reference to other RotatedShipImage objects. Zero degrees (uIndex=0) is when the spaceship facing east. When rotating a JavaFX nodes going in a counter clockwise direction is positive numbers in degrees
  • uIndex – index of the current RotatedShipImage in the directionalShips list that is to be displayed
  • vIndex – index of the RotatedShipImage in the directionalShips list that is to be displayed at the end of the rotation animation
  • stopArea – a JavaFX Circle with a radius for the ship to know when to stop the ship from moving
  • flipBook – A JavaFX Group containing all RotatedShipImage objects (32). The group is rendered on the Scene. Like a flip book in animation each RotatedShipImage will be determined to be displayed based on uIndex and vIndex
  • keyCode – a JavaFX KeyCode will help determine if a key press to help change your weapon (character ‘2’)

The Ship’s member functions:

  • update() – Updates the ships velocity and direction. Also will determine when to stop moving.
  • getCurrentShipImage() – Based on the uIndex it returns the ImageView that is the current ship direction image that is being displayed
  • getCenterX() – returns the screen’s X coordinate of the center of the ship
  • getCenterY() – returns the screen’s X coordinate of the center of the ship
  • plotCourse(double screenX, double screenY, boolean thrust) – After the user clicks their mouse on the screen this method will calculate the angle to rotate the ship and change the velocity to thrust toward the coordinates onto the destination point. When using the Vec object the screen coordinates will be converted to Cartesian coordinates for determining the angle between two vectors (U and V).
  • turnShip() – The plotCourse() method calls turnShip() method to perform the actual animation of the rotation of the ship
  • applyTheBrakes(double screenX, double screenY) – After the user has chosen (right mouse click) where the ship will navigate to applyTheBrakes() method simply sets up the stopArea (Circle) object to let the ship know when to stop
  • fire() – Returns a Missile (Sprite) object for the game engine to put into the scene. Each missile contains the same direction of the ship with a scaled up velocity (increase speed). Should be faster than the ship can fly.
  •  changeWeapon(KeyCode keyCode) – After the user (player) hit the key stroke of a ‘2’ the weapon will change to create a larger missile projectile but slightly slower. Any other key press will be the default weapon that creates small missile projectiles which are faster moving.
Shown below is figure 4 of a class diagram displaying the members of the class Ship.

Ship Class Diagram

Figure 4: Ship class diagram

Shown below is the source code of the Ship class. Click to expand.

package carlfx.demos.navigateship;

import carlfx.gameengine.Sprite;
import javafx.animation.KeyFrame;
import javafx.animation.Timeline;
import javafx.animation.TimelineBuilder;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.CacheHint;
import javafx.scene.Group;
import javafx.scene.Node;
import javafx.scene.image.Image;
import javafx.scene.input.KeyCode;
import javafx.scene.paint.Color;
import javafx.scene.shape.Circle;
import javafx.util.Duration;
import java.util.ArrayList;
import java.util.List;

/**
 * A space ship with 32 directions
 * When two atoms collide each will fade and become removed from the scene. The
 * method called implode() implements a fade transition effect.
 *
 * @author cdea
 */
public class Ship extends Sprite {

    /**
     * 360 degree turn
     */
    private final static int TWO_PI_DEGREES = 360;

    /**
     * Number of ship frames and directions the ship is pointing nose
     */
    private final static int NUM_DIRECTIONS = 32;

    /**
     * The angle of one direction (adjacent directions) (11.25 degrees)
     */
    private final static float UNIT_ANGLE_PER_FRAME = ((float) TWO_PI_DEGREES / NUM_DIRECTIONS);

    /**
     * Amount of time it takes the ship to move 180 degrees in milliseconds.
     */
    private final static int MILLIS_TURN_SHIP_180_DEGREES = 300;

    /**
     * When the ship turns on each direction one amount of time for one frame or turn of the ship. (18.75 milliseconds)
     */
    private final static float MILLIS_PER_FRAME = (float) MILLIS_TURN_SHIP_180_DEGREES / (NUM_DIRECTIONS / 2);

    /**
     * All possible turn directions Clockwise, Counter Clockwise, or Neither when the user clicks mouse around ship
     */
    private enum DIRECTION {
        CLOCKWISE, COUNTER_CLOCKWISE, NEITHER
    }

    /**
     * Velocity amount used vector when ship moves forward. scale vector of ship. See flipBook translateX and Y.
     */
    private final static float THRUST_AMOUNT = 3.3f;

    /***/
    private final static float MISSILE_THRUST_AMOUNT = 6.3F;

    /**
     * Angle in degrees to rotate ship.
     */

    /**
     * Current turning direction. default is NEITHER. Clockwise and Counter Clockwise.
     */
    private DIRECTION turnDirection = DIRECTION.NEITHER;

    /**
     * The current starting position of the vector or coordinate where the nose of the ship is pointing towards.
     */
    private Vec u; // current or start vector

    /**
     * All ImageViews of all the possible image frames for each direction the ship is pointing. ie: 32 directions.
     */
    private final List directionalShips = new ArrayList<>();

    /**
     * The Timeline instance to animate the ship rotating using images. This is an optical illusion similar to page
     * flipping as each frame is displayed the previous visible attribute is set to false. No rotation is happening.
     */
    private Timeline rotateShipTimeline;

    /**
     * The current index into the list of ImageViews representing each direction of the ship. Zero is the ship
     * pointing to the right or zero degrees.
     */
    private int uIndex = 0;

    /**
     * The end index into the list of ImageViews representing each direction of the ship. Zero is the ship
     * pointing to the right or zero degrees.
     */
    private int vIndex = 0;

    /**
     * The spot where the user has right clicked letting the engine check the ship's center is in this area.
     */
    private final Circle stopArea = new Circle();

    /**
     * A group contain all of the ship image view nodes.
     */
    private final Group flipBook = new Group();

    /**
     * A key code will be used for weapon selection.
     */
    private KeyCode keyCode;

    public Ship() {

        // Load one image.
        Image shipImage = new Image(getClass().getClassLoader().getResource("ship.png").toExternalForm(), true);
        stopArea.setRadius(40);
        RotatedShipImage prev = null;

        // create all the number of directions based on a unit angle. 360 divided by NUM_DIRECTIONS
        for (int i = 0; i < NUM_DIRECTIONS; i++) {
            RotatedShipImage imageView = new RotatedShipImage();
            imageView.setImage(shipImage);
            imageView.setRotate(-1 * i * UNIT_ANGLE_PER_FRAME);
            imageView.setCache(true);
            imageView.setCacheHint(CacheHint.SPEED);
            imageView.setManaged(false);
            imageView.prev = prev;
            imageView.setVisible(false);
            directionalShips.add(imageView);
            if (prev != null) {
                prev.next = imageView;
            }
            prev = imageView;
            flipBook.getChildren().add(imageView);
        }
        RotatedShipImage firstShip = directionalShips.get(0);
        firstShip.prev = prev;
        prev.next = firstShip;
        // set javafx node to an image
        firstShip.setVisible(true);
        node = flipBook;
        flipBook.setTranslateX(200);
        flipBook.setTranslateY(300);

    }

    /**
     * Change the velocity of the atom particle.
     */
    @Override
    public void update() {
        flipBook.setTranslateX(flipBook.getTranslateX() + vX);
        flipBook.setTranslateY(flipBook.getTranslateY() + vY);

        if (stopArea.contains(getCenterX(), getCenterY())) {
            vX = 0;
            vY = 0;
        }

    }

    private RotatedShipImage getCurrentShipImage() {
        return directionalShips.get(uIndex);
    }

    /**
     * The center X coordinate of the current visible image. See <code>getCurrentShipImage()</code> method.
     *
     * @return The scene or screen X coordinate.
     */
    public double getCenterX() {
        RotatedShipImage shipImage = getCurrentShipImage();
        return node.getTranslateX() + (shipImage.getBoundsInLocal().getWidth() / 2);
    }

    /**
     * The center Y coordinate of the current visible image. See <code>getCurrentShipImage()</code> method.
     *
     * @return The scene or screen Y coordinate.
     */
    public double getCenterY() {
        RotatedShipImage shipImage = getCurrentShipImage();
        return node.getTranslateY() + (shipImage.getBoundsInLocal().getHeight() / 2);
    }

    /**
     * Determines the angle between it's starting position and ending position (Similar to a clock's second hand).
     * When the user is shooting the ship nose will point in the direction of the mouse press using the primary button.
     * When the user is thrusting to a location on the screen the right click mouse will pass true to the thrust
     * parameter.
     *
     * @param screenX The mouse press' screen x coordinate.
     * @param screenY The mouse press' screen ycoordinate.
     * @param thrust  Thrust ship forward or not. True move forward otherwise false.
     */
    public void plotCourse(double screenX, double screenY, boolean thrust) {
        // get center of ship
        double sx = getCenterX();
        double sy = getCenterY();

        // get user's new turn position based on mouse click
        Vec v = new Vec(screenX, screenY, sx, sy);
        if (u == null) {
            u = new Vec(1, 0);
        }

        double atan2RadiansU = Math.atan2(u.y, u.x);
        double atan2DegreesU = Math.toDegrees(atan2RadiansU);

        double atan2RadiansV = Math.atan2(v.y, v.x);
        double atan2DegreesV = Math.toDegrees(atan2RadiansV);

        double angleBetweenUAndV = atan2DegreesV - atan2DegreesU;

        // if abs value is greater than 180 move counter clockwise
        //(or opposite of what is determined)
        double absAngleBetweenUAndV = Math.abs(angleBetweenUAndV);
        boolean goOtherWay = false;
        if (absAngleBetweenUAndV > 180) {
            if (angleBetweenUAndV < 0) {                 turnDirection = DIRECTION.COUNTER_CLOCKWISE;                 goOtherWay = true;             } else if (angleBetweenUAndV > 0) {
                turnDirection = DIRECTION.CLOCKWISE;
                goOtherWay = true;
            } else {
                turnDirection = Ship.DIRECTION.NEITHER;
            }
        } else {
            if (angleBetweenUAndV < 0) {                 turnDirection = Ship.DIRECTION.CLOCKWISE;             } else if (angleBetweenUAndV > 0) {
                turnDirection = Ship.DIRECTION.COUNTER_CLOCKWISE;
            } else {
                turnDirection = Ship.DIRECTION.NEITHER;
            }
        }

        double degreesToMove = absAngleBetweenUAndV;
        if (goOtherWay) {
            degreesToMove = TWO_PI_DEGREES - absAngleBetweenUAndV;
        }

        //int q = v.quadrant();

        uIndex = Math.round((float) (atan2DegreesU / UNIT_ANGLE_PER_FRAME));
        if (uIndex < 0) {
            uIndex = NUM_DIRECTIONS + uIndex;
        }
        vIndex = Math.round((float) (atan2DegreesV / UNIT_ANGLE_PER_FRAME));
        if (vIndex < 0) {             vIndex = NUM_DIRECTIONS + vIndex;         }         String debugMsg = turnDirection +                 " U [m(" + u.mx + ", " + u.my + ")  => c(" + u.x + ", " + u.y + ")] " +
                " V [m(" + v.mx + ", " + v.my + ")  => c(" + v.x + ", " + v.y + ")] " +
                " start angle: " + atan2DegreesU +
                " end angle:" + atan2DegreesV +
                " Angle between: " + degreesToMove +
                " Start index: " + uIndex +
                " End index: " + vIndex;

        System.out.println(debugMsg);

        if (thrust) {
            vX = Math.cos(atan2RadiansV) * THRUST_AMOUNT;
            vY = -Math.sin(atan2RadiansV) * THRUST_AMOUNT;
        }
        turnShip();

        u = v;
    }

    private void turnShip() {

        final Duration oneFrameAmt = Duration.millis(MILLIS_PER_FRAME);
        RotatedShipImage startImage = directionalShips.get(uIndex);
        RotatedShipImage endImage = directionalShips.get(vIndex);
        List frames = new ArrayList<>();

        RotatedShipImage currImage = startImage;

        int i = 1;
        while (true) {

            final Node displayNode = currImage;

            KeyFrame oneFrame = new KeyFrame(oneFrameAmt.multiply(i),
                    new EventHandler() {

                        @Override
                        public void handle(javafx.event.ActionEvent event) {
                            // make all ship images invisible
                            for (RotatedShipImage shipImg : directionalShips) {
                                shipImg.setVisible(false);
                            }
                            // make current ship image visible
                            displayNode.setVisible(true);

                            // update the current index
                            //uIndex = directionalShips.indexOf(displayNode);
                        }
                    }); // oneFrame

            frames.add(oneFrame);

            if (currImage == endImage) {
                break;
            }
            if (turnDirection == DIRECTION.CLOCKWISE) {
                currImage = currImage.prev;
            }
            if (turnDirection == DIRECTION.COUNTER_CLOCKWISE) {
                currImage = currImage.next;
            }
            i++;
        }

        if (rotateShipTimeline != null) {
            rotateShipTimeline.stop();
            rotateShipTimeline.getKeyFrames().clear();
            rotateShipTimeline.getKeyFrames().addAll(frames);
        } else {
            // sets the game world's game loop (Timeline)
            rotateShipTimeline = TimelineBuilder.create()
                    .keyFrames(frames)
                    .build();

        }

        rotateShipTimeline.playFromStart();

    }

    /**
     * Stops the ship from thrusting forward.
     *
     * @param screenX the screen's X coordinate to stop the ship.
     * @param screenY the screen's Y coordinate to stop the ship.
     */
    public void applyTheBrakes(double screenX, double screenY) {
        stopArea.setCenterX(screenX);
        stopArea.setCenterY(screenY);
    }

    public Missile fire() {
        Missile m1;

        float slowDownAmt = 0;
        if (KeyCode.DIGIT2 == keyCode) {
            m1 = new Missile(10, Color.BLUE);
            slowDownAmt = 2.3f;
        } else {
            m1 = new Missile(Color.RED);
        }
        // velocity vector of the missile
        m1.vX = Math.cos(Math.toRadians(uIndex * UNIT_ANGLE_PER_FRAME)) * (MISSILE_THRUST_AMOUNT - slowDownAmt);
        m1.vY = -Math.sin(Math.toRadians(uIndex * UNIT_ANGLE_PER_FRAME)) * (MISSILE_THRUST_AMOUNT - slowDownAmt);

        // make the missile launch in the direction of the current direction of the ship nose. based on the
        // current frame (uIndex) into the list of image view nodes.
        RotatedShipImage shipImage = directionalShips.get(uIndex);

        // start to appear in the center of the ship to come out the direction of the nose of the ship.
        double offsetX = (shipImage.getBoundsInLocal().getWidth() - m1.node.getBoundsInLocal().getWidth()) / 2;
        double offsetY = (shipImage.getBoundsInLocal().getHeight() - m1.node.getBoundsInLocal().getHeight()) / 2;

        // initial launch of the missile
        m1.node.setTranslateX(node.getTranslateX() + offsetX + m1.vX);
        m1.node.setTranslateY(node.getTranslateY() + offsetY + m1.vY);
        return m1;
    }

    public void changeWeapon(KeyCode keyCode) {
        this.keyCode = keyCode;
    }

}

Vec

The Vec class is a simple helper container class to assist in holding a mouse click’s screen coordinates and converting them to Cartesian coordinates based on the center of a sprite, image, or shape. This class is used to help determine the angle between two vectors [Math.atan2(y,x)]. By determining the angle the ship is able to perform the rotation animation of the sprite image.

Shown below is the source code of the Vec class. Click to expand.

package carlfx.demos.navigateship;

/**
 * This class represents a container class to hold a Vector in space and direction
 * the ship will move. Assuming the center of the ship is the origin the angles can
 * be determined by a unit circle via Cartesian coordinates.
 * When the user clicks on the screen the mouse coordinates or screen coordinates
 * will be stored into the mx and my instance variables.
 * The x and y data members are converted to cartesian coordinates before storing.
 *
 * I purposefully left out getters and setters. In gaming just keep things minimalistic.
 * @author cdea
 */
public class Vec {
    public double mx;
    public double my;

    public double x;
    public double y;

    /**
     * This is a default constructor which will take a Cartesian coordinate.
     * @param x  X coordinate of a point on a Cartesian system.
     * @param y  Y coordinate of a point on a Cartesian system.
     */
    public Vec(float x, float y) {
        this.x = x;
        this.y = y;
    }

    /**
     * Constructor will convert mouse click points into Cartesian coordinates based on the sprite's center point as
     * the origin.
     * @param mx  Mouse press' screen X coordinate.
     * @param my  Mouse press' screen Y coordinate.
     * @param centerX Screen X coordinate of the center of the ship sprite.
     * @param centerY Screen Y coordinate of the center of the ship sprite.
     */
    public Vec(double mx, double my, double centerX, double centerY) {
        this.x = convertX(mx, centerX);
        this.y = convertY(my, centerY);
        this.mx = mx;
        this.my = my;
    }

    /**
     * Returns a Cartesian coordinate system's quadrant from 1 to 4.
*
     *     first quadrant - 1 upper right
     *     second quadrant - 2 upper left
     *     third quadrant - 3 lower left
     *     fourth quadrant - 4 lower right
     *
     * @return int quadrant number 1 through 4
     */
    public int quadrant() {
        int q = 0;
        if (x > 0 && y > 0) {
            q =1;
        } else if (x < 0 && y > 0) {
            q = 2;
        } else if (x < 0 && y < 0) {             q = 3;         } else if (x > 0 && y < 0) {
            q = 4;
        }
        return q;
    }
    @Override
    public String toString(){
        return "(" + x + "," + y + ") quadrant=" + quadrant();
    }
    /**
     * Converts point's X screen coordinate into a Cartesian system.
     * @param mouseX Converts the mouse X coordinate into Cartesian system based on the ship center X (originX).
     * @param originX The ship center point's X coordinate.
     * @return  double value of a Cartesian system X coordinate based on the origin X.
     */
    static double convertX(double mouseX, double originX) {
        return mouseX - originX;
    }

    /**
     * Converts point's Y screen coordinate into a Cartesian system.
     * @param mouseY  Converts the mouse Y coordinate into Cartesian system based on the ship center Y (originY).
     * @param originY The ship center point's Y coordinate.
     * @return  double value of a Cartesian system Y coordinate based on the origin Y.
     */
    static double convertY(double mouseY, double originY) {
        return originY - mouseY;
    }

}

RotatedShipImage

A RotatedShipImage class inherits from a JavaFX’s ImageView class, but also contains references to a previous and a next RotatedShipImage instances making up a doubly linked list. Figure 3 depicts 32 images of the of the “ship.png” rendered in each RotatedShipImage which are all placed in a JavaFX Group node. When the ship appears to rotate, one image is displayed at a time.

Shown below is the source code of the RotatedShipImage class. Click to expand.

package carlfx.demos.navigateship;

import javafx.scene.image.ImageView;

/**
 * Represents a double link list to assist in the rotation of the ship.
 * This helps to move clockwise and counter clockwise.
 */
public class RotatedShipImage extends ImageView {
    public RotatedShipImage next;
    public RotatedShipImage prev;
}

Missile

The Missile class inherits from the Atom class. The Missile acts as a marker class to differentiate between spheres (asteroids) and missiles. When missiles are created, they will contain the same direction of the ship (where the ship’s nose is pointing) with a larger velocity.

Shown below is the source code of the Missile class. Click to expand.

package carlfx.demos.navigateship;

import javafx.scene.paint.Color;

/**
 * A missile projectile without the radial gradient.
 */
public class Missile extends Atom {
    public Missile(Color fill) {
        super(5, fill, false);
    }

    public Missile(int radius, Color fill) {
        super(radius, fill, true);
    }
}

Conclusion

Input is so vital to any game play that it is often hard to get right. Older game engines will poll during a game loop. When using JavaFX 2.x’s event handling, you  implement the type of event to be added to the scene graph or to an individual Node object. Hopefully in the future, we will see more ingenious input devices used in gaming (see Oracle’s Java Technology Evangelist Simon Ritter).  Keep your eyes open for Part 4 which deals with collision detection. So, stay tuned and feel free to comment.

References:

7-Eleven: http://www.7-eleven.com

Playing Asteroids: http://www.play.vg/games/4-Asteroids.html

Asteroids: http://en.wikipedia.org/wiki/Asteroids_(video_game)

Scott Safran: http://en.wikipedia.org/wiki/Scott_Safran

Arcade in Backyard: http://www.themysteryworld.com/2011/02/guy-builds-video-arcade-in-his-back.html

StarWars downsized: http://techland.time.com/2012/04/26/man-builds-16-scale-star-wars-arcade-game/

Trigonometry: http://en.wikipedia.org/wiki/Trigonometry

JavaFX Node API: http://docs.oracle.com/javafx/2/api/javafx/scene/Node.html

JavaFX Scene API: http://docs.oracle.com/javafx/2/api/javafx/scene/Scene.html

JavaFX SVGPath API: http://docs.oracle.com/javafx/2/api/javafx/scene/shape/SVGPath.html

Multi-Touch and Gestures Support: http://www.oracle.com/technetwork/java/javafx/overview/roadmap-1446331.html

Pro JavaFX 2 Apress publishing – pg. 62 chapter 2 section on “Handling Input Events” . http://www.apress.com/9781430268727

Java 7 Recipes Apress publishing- pg. 602 chapter 16 Recipe 16-3 “Animating Shapes Along a Path”. http://www.apress.com/9781430240563

Video Game arcade cabinet: http://en.wikipedia.org/wiki/Video_game_arcade_cabinet

Raster Graphics: http://en.wikipedia.org/wiki/Raster_graphics

Part 3 source code at the GitHub: https://github.com/carldea/JFXGen/tree/master/demos/navigateship

JavaFX Canvas Node: http://mail.openjdk.java.net/pipermail/openjfx-dev/2012-April/001210.html

JavaFX- Optimizing Performance for JavaFX Applications: http://www.parleys.com/#st=5&id=2738&sl=0

Oracle’s Java Technology Evangelist Simon Ritter: https://blogs.oracle.com/javaone/entry/interfacing_with_the_interface_javafx

Video Game High School episode 1: http://www.rocketjump.com/?video=vghs-episode-1

Video Game High School episode 2: http://www.rocketjump.com/?video=vghs-episode-2-5

JavaFX – A Poor Man’s Form Designer

Person Form - Using Poor Man's Form Designer

Person Form

Introduction (Updated 2)

NOTE: If you’ve been here before and found this blog entry useful please note that I have updated it again (for good reason).  Let me explain why and what I’ve updated. Since I have found that I was becoming quite productive generating forms, I wanted a more decoupled approach, so I re-factored the source code to be more reusable and easily up-datable. To use the form designer for your applications you can skip down to the section called “Using the Form Designer” to help you create your own forms in no time at all! (I also had a crack at making the form a little more attractive too).

What is a Form Designer?

Software developers who build form based applications will often struggle with positioning controls dynamically while a window resizes. This is better known as “Layout Management”. Many developers will rely on GUI builders that provide a “WYSIWYG” ability allowing a designer to drag and drop components onto a canvas area. On the other hand some developers prefer to hand code GUI form screens. There are pros and cons to using both strategies; however in this article I will strike a balance by building a form designer to allow a developer to assist them in hand coding GUI form screens (sound strange, just read on). While the JavaFX community has anticipated for GUI designer tools, I couldn’t wait. So, I decided to create the “Poor Man’s Form Designer”. This simple designer tool uses the popular MigLayout by Mikael Grev and was ported over to the JavaFX platform by Dean Iverson (an author of the book “Pro JavaFX Platform”) into the JFXtras project. The PMFD‘s source code consists of one file Main.fx and just at around 370 lines of code including comments. After the Conclusion section of this article you will see the full source code to the demo and form designer. Simply click on the source code links to expand and your on your way to developing beautiful looking forms!

Poor Man's Form Designer Constraints Window

Poor Man's Form Designer Constraints Window

Note: You will notice I forgot to add column constraints. The designer now has the ability to adjust the column constraints.

Demo

Demo

Instructions:

  1. Click Launch button above
  2. Go to the MigLayout cheat sheet at http://migcalendar.com/miglayout/cheatsheet.html to keep handy when tweaking the “Form Design Constraint Editor” window.
  3. Go to the “Form Design Constraint Editor” and click the dump button. Here it will dump all the current constraints for the following areas: Layout, Column/Row and Components constraints.
  4. Go to the “Form Design Constraint Editor” window under the Component Constraint area is a combo box please select firstNameLabel, then tab to the constraint text field (just right of combo box) and type “align left“. Click Apply button to update changes.
  5. Be sure to observe the Form View window and you’ll notice the “First Name:” label is left justified beside the first name text field.
  6. Play around with other constraint areas. Cut and paste from the cheat sheet. The cheat sheet is divided in three sections: Layout Constraints, Column & Row Constraints and Component constraints. Don’t forget to hit the apply button.
  7. You’ll notice in the component constraint area when selecting a control in the combo box the previous constraint is preserved and displayed in the constraint text box.
  8. Go to the “Form Design Constraint Editor” and click the dump button. Here it will dump all the current constraints for the following areas: Layout, Column/Row and Components constraints.
  9. Copy and paste those dumped constraints to assist in your hand coded GUI Form. (See below “Using the Form Designer” section, Steps 3 & 4)

Disclaimer: While the coding details below seem strange,  you may want to visit Dean’s Pleasing Software blog entry “MigLayout for JavaFX Reloaded” regarding how to Get started with MigLayout and JavaFX. http://pleasingsoftware.blogspot.com/search?q=layout

Using the Form Designer

Step 1: Create a new JavaFX project / setup to include JFXtras libraries and MigLayout jar files

Step 2: Copy Source code poormans_form_designer.fx expand put into your project. (check package statement)

Step 3: Create a form (class) which extends MigLayout from the JFXtras project. Simply add two attributes:

  1. componentsConstraints – Sequence of String objects
  2. nodesToLayout – Sequence of Node objects

Although this seems odd, I plan to possibly create a Mixin class or a derived MigLayout class to carry these attributes. Internally each Node contains a layoutInfo which contains a MigNodeLayoutInfo object (see http://jfxtras.org/portal/core/-/wiki/JFXtras/MigLayout for details). You should notice that the nameform.fx does not have dependencies to the designer library code, thus allowing the form developer to use CustomNode if they choose.

Create a Form (see nameform.fx for full source code)

public class NameForm extends MigLayout {
  public var componentsConstraints:String[] = [];
  public var nodesToLayout:Node[] = [];

  init {

    // ... other controls
    var firstNameLabel:Label = Label {
      id: "firstNameLabel"
      text: "First Name"

      font : Font {
          size: 18
      }
    };

    var firstNameField:TextBox = TextBox {
      id:"firstNameField"
    };
    // ... other controls

   nodesToLayout = [
      sectionFullName,  // 0
      instructionsText, // 1
      firstNameLabel,   // 2
      firstNameField,   // 3
      // ... all controls
   ];
    // +---------------------------------------------------
    // ! Poor Man's designer constraints info goes below
    // +---------------------------------------------------
    // [BEGIN]

    constraints = "";
    columns = "[pref]10[fill]";
    rows = "[pref]10[pref]";
    componentsConstraints = [
      "span, growx, wrap",                // fullNameTitle 0
      "span, wrap 15px",               // instructionsText 1
      "align right",                     // firstNameLabel 2
      "span, w min:100:300, wrap",       // firstNameField 3
      "align right",                    // middleNameLabel 4
      "growx, w min:100:300, wrap",     // middleNameField 5
      "align right",                      // lastNameLabel 6
      "growx,  w min:100:300, wrap",      // lastNameField 7
      "align right",                    // suffixNameLabel 8
      " w min:100:300, wrap 15",        // suffixNameField 9
      "span, wrap",                           // dobTitle 10
      "align right",                          // dobLabel 11
      " w min:100:pref, wrap 15",             // dobField 12
      "span, wrap",                           // pobTitle 13
      "align right",                      // pobCityLabel 14
      "span, growx,  w min:100:300, wrap",  // pobCityField 15
      "align right",                    // pobCountyLabel 16
      "span, growx,  w min:100:300, wrap",  // pobCountyField 17
      "align right",                     // pobStateLabel 18
      "span, w pref:100:pref + 10, wrap",  // pobStateField 19
    ];

    // [END]
    // +---------------------------------------------------
    // ! Poor Man's designer constraints info goes above
    // +---------------------------------------------------
  }

  // IMPORTANT - This will generate mig nodes to be put
  // into your content to be displayed in the scene.
  postinit {
    content = for (i in [0.. sizeof nodesToLayout -1]) {
      migNode(nodesToLayout[i], componentsConstraints[i]);
    }
  }

}

Note: Next, will be Step 3, Launching your newly created MigLayout form into the designer. Once you are happy with the constraints you may press the “Dump” button to output the code that you will cut and paste into your form above. Place code between your [BEGIN] and [END] comment tags (Between lines 32 & 60)

Step 3: Launching your newly created MigLayout form into the designer (see Main.fx for full source code below conclusion)

Create a Main.fx to launch form into the Poor Man’s Form Designer tool. (see poormans_form_designer.fx for full source code below conclusion )

  // http://pleasingsoftware.blogspot.com/search?q=layout
  // Create name form
  def nameForm:nameform.NameForm = NameForm{
  };

  // Remove content, due to nodes placed in the migNameForm later
  delete nameForm.content;

  // Create a miglayout designer object
  def designer:MigLayoutDesigner = MigLayoutDesigner{
      layoutConstraint: nameForm.constraints
      columnsConstraint: nameForm.columns
      rowsConstraint: nameForm.rows
      componentsConstraints: nameForm.componentsConstraints
      nodesToLayout: nameForm.nodesToLayout
  };

  // launch designer
  poormans_form_designer.launch(designer, null, null);

Troubleshooting

  • Missing imports or resolving – NetBeans: Control-Shift-I . Eclipse: Control-Shift-O . In NetBeans when using script level functions or classes inside a script file just use the ‘*’ wild card. ie: import poormans_form_designer.*;
  • Script file does not contain correct package name. ie: package xyz;
  • Form does not have two attributes called nodesToLayout and componentsConstraints.
  • If Form is extending from MigLayout it should set its content using the two known attributes. ie: content = for (i in [0.. sizeof nodesToLayout -1]) {
    migNode(nodesToLayout[i], componentsConstraints[i]);
    }

Enhancements

Above you will see hand coded controls that eventually get displayed onto the Form View window (Person Form). Since the the GUI control code elements are simply sequential, I believe it would be quite easy to add, insert and remove controls dynamically onto the Form View window area.  Also, possibly a property sheet window for controls, skins and behavior swapping.

Conclusion

The main goal is to properly lay out components in order to create a nice looking forms and also to learn the popular layout framework library MigLayout with it’s constraints language syntax. Using JavaFX‘s binding allows the form designer tool to enable the user to easily tweak constraints on the fly without having to rerun an application GUI for every adjustment made during the layout process in a form view. Another interesting thing to note is that since we are in the JavaFX world we can layout shapes, custom nodes and graphics (not just regular form controls). As a reminder regarding the use of JFXtras MigLayout, users should read about the known issues, please go to JFXtras MigLayout wiki at: http://jfxtras.org/portal/core/-/wiki/JFXtras/MigLayout . Well, by the time you read this blog entry I’m sure a nice GUI form designer/builder tool will be available.

The source code is listed below (click source code link to expand):

Note: I am in the process of putting the zipped up project onto the JFXtras.org for easy downloading.

Requirements:

  • Java 6 JDK or greater
  • JavaFX 1.2.1 SDK
  • JFXtras version 0.5 jars
  • latest Miglayout jar

Main.fx –

/*
 * Main.fx
 * @author Carl Dea
 * Created on Jan 9, 2010, 9:06:36 PM
 */
package migtest3;

import javafx.stage.Stage;
import migtest3.nameform.*;
import javafx.scene.Scene;
import org.jfxtras.scene.ResizableScene;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.paint.Color;
import migtest3.poormans_form_designer.*;

function run(__ARGS__ : String[]) {

  // Create name form
  def nameForm:nameform.NameForm = NameForm{
  };

  // Remove content, due to nodes placed in the migNameForm later
  delete nameForm.content;

  // Create a miglayout designer object
  def designer:MigLayoutDesigner = MigLayoutDesigner{
      layoutConstraint: nameForm.constraints
      columnsConstraint: nameForm.columns
      rowsConstraint: nameForm.rows
      componentsConstraints: nameForm.componentsConstraints
      nodesToLayout: nameForm.nodesToLayout
  };

  // Create a custom scene. Note: this overrides default
  // scene inside launch PMFD script function.
  var newDesignerViewScene:Scene = ResizableScene {
    fill: LinearGradient {
      startX : 0.0
      startY : 0.0
      endX : 1.0
      endY : 0.0
      stops: [
        Stop {
          color : Color.rgb(251, 251, 251)
          offset: 0.0
        },
        Stop {
          color : Color.rgb(240, 240, 240)
          offset: 1.0
        },
     ]
    }
    content: [designer.dynamicMigLayout] // use the dynamicMigLayout
  }; // scene

  // create a custom Stage. Note: this overrides default
  // Stage inside launch PMFD script function.
  var newDesignerStageView = Stage {
      y:150
      x:100
      width: 490
      height: 600
      scene:newDesignerViewScene
      visible:true
  };

  // launch designer
  poormans_form_designer.launch(designer, newDesignerViewScene, newDesignerStageView);
}

nameform.fx –

/*
 * nameform.fx
 *
 * @author Carl Dea
 * Created on Jan 9, 2010, 11:03:57 AM
 */

package migtest3;

import javafx.ext.swing.SwingComboBox;
import javafx.scene.control.Label;
import javafx.scene.control.TextBox;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.Text;
import javafx.scene.Node;
import javafx.scene.CustomNode;
import javafx.ext.swing.SwingComboBoxItem;
import javafx.scene.Group;
import javafx.scene.shape.Rectangle;
import org.jfxtras.scene.layout.MigLayout;

public var US_STATES = [
  "AL","AK","AS","AZ","AR","CA","CO", "CT","DE","DC","FM","FL","GA","GU","HI","ID","IL","IN","IA","KS",
  "KY","LA","ME","MH","MD","MA","MI","MN","MS","MO","MT","NE","NV","NH","NJ","NM","NY","NC","ND","MP",
  "OH","OK","OR","PW","PA","PR","RI","SC","SD","TN","TX","UT","VT","VI","VA","WA","WV","WI","WY"
];

public function comboItemSeqCreator(names:String[]):SwingComboBoxItem[]{
  for( n in names) SwingComboBoxItem{ text:n };
}

public class SectionTitleHeader extends CustomNode {
  public-init var text:String;
  public var height;
  public var width;
  public var rectangleColor:Color on replace {
    if (not FX.isInitialized(rectangleColor)){
      rectangleColor = Color.rgb(0,130,171);
    }
  };

  public override function create():Node{
    var title = Group {
      content: [
        Rectangle {
          x: 10,
          y: height - 85,
          arcHeight: 5,
          arcWidth: 5,
          width: bind width - 30,
          height: 30,
          stroke:Color.BLACK
          fill: rectangleColor
          opacity: .5
        },
        Text {
          x: 15,
          y: height - 65,
          content: text
          fill: Color.WHITE
          font: Font {
            name: "Arial Bold"
            letterSpacing: .20
            size: 20
          }
        }
      ]
    }; // title
    return title;
  }

}

public class NameForm extends MigLayout {
  public var componentsConstraints:String[] = [];
  public var nodesToLayout:Node[] = [];

  init {

    var sectionFullName:SectionTitleHeader = SectionTitleHeader {
      id:"fullNameTitle"
      text:"1  Full Name"
      width:bind sectionFullName.scene.stage.width
      height:100
      rectangleColor:Color.BLUE
    }

    def instructions:String =
      "- If you have only initials in your name, use them "
      "and enter (I/O) after the initial(s). \n"
      "- If you have no middle name, enter \"NMN\".\n"
      "- If you are \"Jr.,\" \"Sr.,\" etc. enter this in the box after your middle name.\n";

    var instructionsText:Text = Text {
      id: "instructionsText"
      content: instructions
      fill:Color.BLACK
      font : Font {
        embolden:true
        size: 14
      }
    };

    var firstNameLabel:Label = Label {
      id: "firstNameLabel"
      text: "First Name"

      font : Font {
          size: 18
      }
    };

    var firstNameField:TextBox = TextBox {
      id:"firstNameField"
    };

    var middleNameLabel:Label = Label {
      id: "middleNameLabel"
      text: "Middle Name"
      font : Font {
        size: 18
      }
    };

    var middleNameField:TextBox = TextBox {
      id:"middleNameField"
    };

    var lastNameLabel:Label = Label {
      id: "lastNameLabel"
      text: "Last Name"
      font : Font {
        size: 18
      }
    };

    var lastNameField:TextBox = TextBox {
      id:"lastNameField"
    };

    var suffixNameLabel:Label = Label {
      id: "suffixNameLabel"
      text: "Suffix"
      font : Font {
        size: 18
      }
    };

    var suffixNameField:TextBox = TextBox {
      id:"suffixNameField"
    };

    var sectionDob:SectionTitleHeader = SectionTitleHeader {
      id:"dobTitle"
      text:"2  Date of Birth"
      width: bind sectionDob.scene.stage.width
      height:100
      rectangleColor:Color.BLUE
    }

    var dobLabel:Label = Label {
      id: "dobLabel"
      text: "DOB"
      font : Font {
        size: 18
      }
    };

    var dobField:TextBox = TextBox {
      id:"dobField"
    };

    var sectionPlaceOfBirth:SectionTitleHeader = SectionTitleHeader {
      id: "pobTitle"
      text: "3  Place of Birth"
      width: bind sectionPlaceOfBirth.scene.stage.width
      height:100
      rectangleColor:Color.BLUE
    }
    var pobCityLabel:Label = Label {
      id: "pobCityLabel"
      text: "City"
      font : Font {
        size: 18
      }
    };

    var pobCityField:TextBox = TextBox {
      id:"pobCityField"
    };
    var pobCountyLabel:Label = Label {
      id: "pobCountyLabel"
      text: "County"
      font : Font {
        size: 18
      }
    };

    var pobCountyField:TextBox = TextBox {
      id:"pobCountyField"
    };

    var pobStateLabel:Label = Label {
      id: "pobStateLabel"
      text: "State"
      font : Font {
        size: 18
      }
    };
    var statesComboItems = comboItemSeqCreator(US_STATES);
    var pobStateField:SwingComboBox = SwingComboBox {
       id: "pobStateField"
       items: [statesComboItems]
       visible: true
    }

    var changeFieldsOpacity:Node[] = [
      firstNameField,
      middleNameField,
      lastNameField,
      suffixNameField,
      dobField,
      pobCityField,
      pobCountyField,
      pobStateField,
    ];
    changeOpacity(changeFieldsOpacity, .80);

   nodesToLayout = [
      sectionFullName,  // 0
      instructionsText, // 1
      firstNameLabel,   // 2
      firstNameField,   // 3
      middleNameLabel,  // 4
      middleNameField,  // 5
      lastNameLabel,    // 6
      lastNameField,    // 7
      suffixNameLabel,  // 8
      suffixNameField,  // 9
      sectionDob,       // 10
      dobLabel,         // 11
      dobField,         // 12
      sectionPlaceOfBirth,// 13
      pobCityLabel,     // 14
      pobCityField,     // 15
      pobCountyLabel,   // 16
      pobCountyField,   // 17
      pobStateLabel,    // 18
      pobStateField,    // 19
    ];

    // +---------------------------------------------------
    // ! Poor Man's designer constraints info goes below
    // +---------------------------------------------------
    // [BEGIN]

    constraints = "";
    columns = "[pref]10[fill]";
    rows = "[pref]10[pref]";
    componentsConstraints = [
      "span, growx, wrap",                // fullNameTitle 0
      "span, wrap 15px",               // instructionsText 1
      "align right",                     // firstNameLabel 2
      "span, w min:100:300, wrap",       // firstNameField 3
      "align right",                    // middleNameLabel 4
      "growx, w min:100:300, wrap",     // middleNameField 5
      "align right",                      // lastNameLabel 6
      "growx,  w min:100:300, wrap",      // lastNameField 7
      "align right",                    // suffixNameLabel 8
      " w min:100:300, wrap 15",        // suffixNameField 9
      "span, wrap",                           // dobTitle 10
      "align right",                          // dobLabel 11
      " w min:100:pref, wrap 15",             // dobField 12
      "span, wrap",                           // pobTitle 13
      "align right",                      // pobCityLabel 14
      "span, growx,  w min:100:300, wrap",  // pobCityField 15
      "align right",                    // pobCountyLabel 16
      "span, growx,  w min:100:300, wrap",  // pobCountyField 17
      "align right",                     // pobStateLabel 18
      "span, w pref:100:pref + 10, wrap",  // pobStateField 19
    ];

    // [END]
    // +---------------------------------------------------
    // ! Poor Man's designer constraints info goes above
    // +---------------------------------------------------
  }

  // IMPORTANT - This will generate mig nodes to be put
  // into your content to be displayed in the scene.
  postinit {
    content = for (i in [0.. sizeof nodesToLayout -1]) {
      migNode(nodesToLayout[i], componentsConstraints[i]);
    }
  }

}
public function changeOpacity(nodes:Node[], opacityLevel:Float):Void {
  for (n in nodes) {
    n.opacity = opacityLevel;
  }

}

poormans_form_designer.fx –

/*
 * poormans_form_designer.fx
 *
 * @author Carl Dea
 * Created on Jan 2, 2010, 8:05:06 PM
 */
package migtest3;

import javafx.ext.swing.SwingComboBox;
import javafx.ext.swing.SwingComboBoxItem;
import javafx.ext.swing.SwingComponent;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextBox;
import javafx.scene.paint.Color;
import javafx.scene.paint.LinearGradient;
import javafx.scene.paint.Stop;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Sequences;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import org.jfxtras.scene.ResizableScene;
import org.jfxtras.scene.layout.MigLayout;
import org.jfxtras.scene.layout.MigLayout.*;
import org.jfxtras.stage.JFXDialog;

/** Title of the designer */
var designViewTitle:String = "Poor Man's Form Designer ver 0.2";

/**
 * This makes a node MigLayout capable.
 *
 */
public class MigLayoutDesigner {
  public var layoutConstraint:String = "";
  public var columnsConstraint:String = "";
  public var rowsConstraint:String = "";
  public var componentsConstraints:String[] = [];
  public var nodesToLayout:Node[];
  public var migNodesToLayout:Node[] = bind generateMigNodes(nodesToLayout, componentsConstraints);
  protected var dynamicMigLayout:MigLayout;

  postinit {
    dynamicMigLayout = createBindableMigLayout();
  }

//  public abstract function createMigLayoutNode():Void;
  /**
   * Creates a bound MigLayout.
   */
  bound public function createBindableMigLayout() {
    MigLayout {
        constraints: bind layoutConstraint
        columns: bind columnsConstraint
        rows: bind rowsConstraint
        content: bind migNodesToLayout
    }
  }
}

/**
 * Generates a sequence of Nodes wrapped with migNode function.
 */
bound public function generateMigNodes(nodes:Node[], componentConstraintStrings:String[]){
    for (i in [0.. sizeof nodes -1]) {
      migNode(nodes[i], componentConstraintStrings[i]);
    }
}

/**
 * @param migForm - a MigLayoutCapable form. Inherits from the mixin MigLayoutCapable.
 * @param designerViewScene - The designer View of the Scene. If null one will be created.
 * @param designerViewStage - The designer View of the Stage (Window). If null one will be created.
 */
public function launch(migForm:MigLayoutDesigner, designerViewScene:Scene, designerViewStage:Stage) {

  // List Comprehensions - Strings representing nodes' id
  var listIds = for( n in migForm.nodesToLayout) n.id;

  // List comprehensions - SwingComboBoxItems
  var comboboxItems = for( n in listIds) SwingComboBoxItem{ text:n };

  // Combo Box control with ids of each widget on form.
  var idListBox:SwingComboBox = SwingComboBox {
    text : bind componentIdSelection with inverse;
    items: [comboboxItems]
  }

  // select first one as default
  idListBox.selectedIndex = 0;

  ////////////////////////////////////////////////////
  // Layout constraints section
  ////////////////////////////////////////////////////
  var layoutConstraintLabel:Label = Label {
    text: "Layout Constraint"
     font: Font {
        embolden:true
        size: 20
     }
  }
  var layoutConstraintTextBox:TextBox = TextBox {
    text: migForm.layoutConstraint
    selectOnFocus: true
  }

  // Updates the layout constraint when pressed
  var layoutConstraintApplyButton:Button = Button {
    text: "Apply"
    action: function() {
        migForm.layoutConstraint = layoutConstraintTextBox.text;
    }
  }

  ////////////////////////////////////////////////////
  // Column constraints section
  ////////////////////////////////////////////////////
  var columnsConstraintLabel:Label = Label {
    text: "Columns Constraint"
     font: Font {
        embolden:true
        size: 20
     }
  }

  // Constraint textbox
  var columnsConstraintTextBox:TextBox = TextBox {
    text: migForm.columnsConstraint
    columns: 30
  };

  // Updates the columns constraint when pressed
  var columnsConstraintApplyButton:Button = Button {
     text: "Apply"
     action: function() {
        migForm.columnsConstraint = columnsConstraintTextBox.text;
     }
  };

  ////////////////////////////////////////////////////
  // Row constraints section
  ////////////////////////////////////////////////////
  var rowsConstraintLabel:Label = Label {
    text: "Rows Constraint"
     font: Font {
        embolden:true
        size: 20
     }
  }

  // Constraint textbox
  var rowsConstraintTextBox:TextBox = TextBox {
    text: migForm.rowsConstraint
    columns: 30
  };

  // Updates the rows constraint when pressed
  var rowsConstrainApplyButton:Button = Button {
     text: "Apply"
     action: function() {
        migForm.rowsConstraint = rowsConstraintTextBox.text;
     }
  };

  ////////////////////////////////////////////////////
  // Component constraints section
  ////////////////////////////////////////////////////
  var componentsConstraintLabel:Label = Label {
     text: "Nodes Constraints"
     font: Font {
        embolden:true
        size: 20
     }
  };

  // Constraint textbox
  var componentConstraintTextBox:TextBox = TextBox {
      columns: 30
  };

  var componentIdSelection: String on replace {
      componentConstraintTextBox.text = migForm.componentsConstraints [
        Sequences.indexByIdentity(listIds, componentIdSelection)
      ];
  };

  var componentConstraintApplyButton:Button = Button {
     text: "Apply"
     action: function() {
        for (i in [0.. sizeof migForm.nodesToLayout -1]) {
           if (migForm.nodesToLayout[i].id.equalsIgnoreCase(componentIdSelection)) {
              migForm.componentsConstraints[i] = componentConstraintTextBox.text;
              break;
           }
        }
     } // action
  };

  ////////////////////////////////////////////////////
  // MigLayout Dump constraints section
  ////////////////////////////////////////////////////
  var miglayoutDumpLabel:Label = Label {
     text: "MigLayout Constraints Dump"
     font: Font {
        embolden:true
        size: 20
     }
  };

  var constraintsDumpButton:Button = Button {
     text: "Dump"
     action: function() {
        //var miglayout:MigLayout = personFormScene.content[0] as MigLayout;
        var buffer:String;
        var migStructure:String;
        buffer = "constraints = \"{migForm.layoutConstraint}\";\n"
           "columns = \"{migForm.columnsConstraint}\"; \n"
           "rows = \"{migForm.rowsConstraint}\";\n"
           "componentsConstraints = [\n";

        for (i in [0.. sizeof listIds -1]) {
          //buffer2 = "{buffer2}\"  id: {listIds[i]} - {migForm.componentsConstraints[i]}\n";
          var strElement = "\"{migForm.componentsConstraints[i]}\", // {listIds[i]} {i} \n";
          var spaces = generateSpaces(55 - strElement.length());
          buffer = "{buffer}  \"{migForm.componentsConstraints[i]}\", {spaces} // {listIds[i]} {i} \n";
        }
        buffer = "{buffer}];\n";
        constraintsDumpTextArea.setText("{buffer}");
     }
  };

  // set up a swing wrapped text area box to hold dumped constraints.
  var constraintsDumpTextArea:JTextArea = new JTextArea();
  constraintsDumpTextArea.setColumns(40);
  constraintsDumpTextArea.setRows(20);
  constraintsDumpTextArea.setAutoscrolls(true);
  var sp = new JScrollPane(constraintsDumpTextArea);
  var constraintsDumpTextAreaJfx = SwingComponent.wrap(sp);

  // constrains for the constraint configure window.
  var editorComponentsConstraints = [
     "wrap",
     "growx",
     "wrap",
     "span, newline 15px, wrap",
     "growx",
     "wrap",
     "span, newline 15px, wrap",
     "growx",
     "wrap",
     "span, newline 15px, wrap",
     "growx",
     "grow",
     "",
     "newline 20px",
     "span, align right, wrap",
     "span, growx"
  ];

  var inputPanelToLayout = [
      layoutConstraintLabel,    // 0
      layoutConstraintTextBox,  // 1
      layoutConstraintApplyButton,// 2
      columnsConstraintLabel,   // 3
      columnsConstraintTextBox, // 4
      columnsConstraintApplyButton,// 5
      rowsConstraintLabel,      // 6
      rowsConstraintTextBox,    // 7
      rowsConstrainApplyButton, // 8
      componentsConstraintLabel,// 9
      idListBox,                // 10
      componentConstraintTextBox,// 11
      componentConstraintApplyButton, // 12
      miglayoutDumpLabel,       // 13
      constraintsDumpButton,    // 14
      constraintsDumpTextAreaJfx,// 15
    ];

// convert to miglayout type nodes wrapped.
var editorMigNodesToLayout:Node[] = bind generateMigNodes(inputPanelToLayout, editorComponentsConstraints);

// check if user of the api sent in a Scene or not.
var newDesignerViewScene:Scene = designerViewScene;
  if (designerViewScene == null) {
    newDesignerViewScene = ResizableScene {
      fill:  LinearGradient {
        startX : 0.0
        startY : 0.0
        endX : 1.0
        endY : 0.0
        stops: [
          Stop {
            color : Color.DARKTURQUOISE
            offset: 0.0
          },
          Stop {
            color : Color.WHITE
            offset: 1.0
          },
        ] // stops
      } // fill
      content: [migForm.dynamicMigLayout] // content
    }; // scene
  }

  // check if user of the api sent in a Stage or not.
  var newDesignerStageView:Stage = designerViewStage;

  if (designerViewStage == null) {
    // Form View (Person Form)
    newDesignerStageView = Stage {
      y:150
      x:100
      width: 500
      height: 500
      title: designViewTitle
      scene: newDesignerViewScene
      visible:true
      opacity: .94
    }
  } else {
    newDesignerStageView.scene = newDesignerViewScene;
    if ("".equals(newDesignerStageView.title.trim())) {
      newDesignerStageView.title = designViewTitle;
    }
  }

  // Generate Scene for design view if one doesn't exist
  var constraintInputWindowScene:Scene = ResizableScene {
     fill: Color.GRAY

     content: [
        MigLayout {
           content:bind inputPanelToLayout
        },
     ] // content
  };

  // Form Design Constraint Editor.
  var constraintInputWindow:JFXDialog = JFXDialog{
     title: "Form Design Constraints Editor"
     owner:newDesignerStageView
     x: newDesignerStageView.x + newDesignerStageView.width
     y: 150
     modal:false
     visible:true
     scene:constraintInputWindowScene
     height:500
     width: 600
  }
}

/** Assisting dump button to display constraints nicely
 * to ease cut and paste in to Form for the user.
 * @param numSpaces
 */
function generateSpaces(numSpaces:Integer):String {
  var spaces:String = "";
  for (i in [1..numSpaces]) {
    spaces = "{spaces} ";
  }
  return spaces;
}

References

MigLayout by Dean Iverson – Pleasing Softwarehttp://pleasingsoftware.blogspot.com/search?q=layout

JavaFX and Layouts by Amy Fowlerhttp://weblogs.java.net/blog/aim/archive/2009/09/10/javafx12-layout

JFXtras & MigLayouthttp://jfxtras.googlecode.com/svn/site/javadoc/release-0.5/org.jfxtras.scene.layout/org.jfxtras.scene.layout.MigLayout.html

MigLayout Cheatsheet http://migcalendar.com/miglayout/cheatsheet.html

JFXtras – Miglayout wikihttp://jfxtras.org/portal/core/-/wiki/JFXtras/MigLayout

Swing GUI Builder (formerly Project Matisse)http://netbeans.org/features/java/swing.html

Abeille Form Designerhttps://abeille.dev.java.net/

JForm Designerhttp://www.formdev.com/

WindowBuilderhttp://www.instantiations.com/windowbuilder/

Swing tutorialhttp://java.sun.com/docs/books/tutorial/uiswing/

SWThttp://www.eclipse.org/swt/

Swing layouthttp://java.sun.com/docs/books/tutorial/uiswing/layout/using.html

Swing GUI Builder (formerly Project Matisse)

JavaFX Forms Framework Part 3

Introduction

Matthew 7:13-14

Matthew 7:13-14

This is the third installment of a series of blog entries relating to a proof of concept for a JavaFX Forms Framework. If you missed the beginning of the series you may go to Part 1 and Part 2. We will take a look at code snippets relating to how the FXForms Framework was implemented. If you want to jump right into the code you may download it here or browse the source code here from the JFXtras Samples area. To those who are following the series will notice similarities to JGoodies Bindings and Validation libraries, it is because of those libraries and presentations which basically inspired me to create this MVC forms framework in JavaFX.

Disclaimer: Most of the code snippets will deal with the user of the API’s perspective as opposed to the implementer of the API’s perspective in order to keep the blog entry short. I will try my best to touch on areas regarding the framework’s underlying implementation. I advise people to check the code project out, review it and run it.

A thing I’d like to bring to your attention is that I decided to refactor the code a little and add a new feature to the FXForms Framework. I refactored the form to reference an instance of a presentation model instead of inheriting from it. As I mentioned in Part 2 the form will be independent of the presentation model. This provides different validation contexts while reusing the same form. An example of this situation is when a user uses a form to ‘Add’ information versus an ‘Edit’ of the form information. The new feature added is the ability to validate a field as the user is typing into the text box and positions an icon to indicate an error, warning or information to the user. Another feature might be to add tool tips when the mouse hovers over the icon similar to JGoodies’ IconFeedbackPanel behavior in Java Swing. Before going further into the implementation details you may want to launch the demo to get a feel for the behavior of the entry form with validation and icon indicators.

Demo

Demo

Instructions:

  • Enter numbers or symbols into the first, last and middle name field.
  • Click on the check box to swap the JavaBean for the form.
  • Observe the underlying bean values changing.

Next are the steps on how to develop the demo using the FXForms Framework.

Developer Steps

  1. Create a JavaBean representing a domain object.
  2. Create a Presentation Model with validation for a Form.
  3. Create a Form
  4. Associate a Presentation Model to Form
  5. Use the Form in an application

Detailed Steps

Step 1. Create a JavaBean representing a domain object.

// Java
public class PersonBean extends DomainModel{
    public static final String FIRST_NAME_PROPERTY = "firstName";
    public static final String MIDDLE_NAME_PROPERTY = "middleName";
... // more strings naming properties
    private String firstName;
    private String middleName;
... // more attributes

    /**
     * Returns first name of the person.
     * @return
     */
    public String getFirstName() {
        return firstName;
    }

    /**
     * Sets the first name of the person.
     * @param firstName
     */
    public void setFirstName(String firstName) {
        String old = this.firstName;
        this.firstName = firstName;
        firePropertyChange(FIRST_NAME_PROPERTY, old, firstName);
    }
... // the rest of the methods.
}

PersonBean.java – A domain object containing property change support.

DomainModel.java – Abstract base class containing property change support.

Step 2. Create a Presentation Model with validation for a Form

As I mentioned earlier about reusing the same form with different presentation models. Below you will see an ‘Add Form’ with validation on the Last Name field. The error icon indicates that the last name may not contain symbols and numbers, but allowing letters, apostrophe or hyphen in the name. You will notice the red error icon beside the ‘last name’ text field.

Add Form validation on last name field.

Add Form validation on last name field.

    // JavaFX
    var personForm:NameForm = NameForm{
        presentationModel:domain.model.personpresentationmodel.AddPersonPM{}
    };
    personForm.presentationModel.jBean = new PersonBean();

Next, you will see an ‘Edit Form’ with no validation on the ‘Last Name’ field. But, there is validation on the ‘First Name’ field. The warning icon indicates that the first name can contain symbols and numbers, letters, apostrophe or hyphen, but isn’t recommended. You will notice the yellow warning icon beside the ‘first name‘ field.

Edit Form no validation for Last Name field

Edit Form no validation for Last Name field

    // JavaFX
    personForm.presentationModel = domain.model.personpresentationmodel.EditPersonPM{}
    personForm.presentationModel.jBean = personBean2;

*Note: The examples above are two hypothetical use cases, I mocked up those forms to help illustrate different validation contexts. The demo app uses an edit presentation model that does validate on the ‘last name‘ field.

Edit Person Presentation Model w/Validation

// JavaFX
public class EditPersonPM extends fxforms.model.model.PresentationModel {

   /** Validate the first name field */
   var validateFirstName =  Validator{
       id:PersonBean.FIRST_NAME_PROPERTY
       public override function validate(value:Object){
           return validateName(value, PersonBean.FIRST_NAME_PROPERTY, "Warning");
       }
   };
... // more validators
   postinit {
       addValidator(validateLastName);
       addValidator(validateFirstName);
       addValidator(validateMiddleName);
   }
}
// Script level function
/**
 * Using regular expression allow letters, apostrophe, hyphen
 */
function validateName(value:Object, propName:String, messageType:String){ // use friendly names, short names, etc.
    var results = ValidationResult{};
    var strValue:String = value as String;
    var found:Boolean = Pattern.matches("^[a-zA-Z,'.\\-\\s]*$", strValue);
    if (not found) {
        var message:FieldMessage = FieldMessage{
            id:propName
            messageType:messageType
            errorId:"123"
            errorMessage:"No symbols in names except - or ' (apostrophe)"
        }
        results.addMessage(message);
    }
    return results;
}

Line 01: Class EditPersonPM extends fxforms.model.model.PresentationModel
Line 04: var validateFirstName is an instance of a Validator
Line 12: Adds all Validators to the presentation model
Line 21: Script level function to be used in each validator
Line 24: Regular expression to allow letters, apostrophe and hyphen characters only.
Line 26: Creation of the message when Validator validates.

personpresentationmodel.fx – The edit presentation model for a person name form.

model.fx – Contains presentation model and value model implementation.

validation.fx – Contains the validator, message, result classes.

Step 3. Create a Form

Edit Person Form (Screen mockup)

0 full name panel
+-1------------------------+ // VBox with 3 things
! +-2--------------------+ ! // HBox with 2 things
! ! [ 3 ] [ 4           ]! ! // Label(section) and Label(title)
! +----------------------+ !
! +-5--------------------+ ! // HBox with 2 things
! ! [ 6 ] [ 7  ]         ! ! // Label(spacer} and Text(instructions)
! !                      ! ! // wrapping text abilities
! +----------------------+ !
! +-8--------------------+ ! // HBox with 2 things
! ! +-9---+ +-10-------+ ! ! // VBox_9(labels) Vbox_10(textbox)
! ! ![11] ! ! [15]     ! ! ! // Label(lastname) TextBox()
! ! ![12] ! ! [16]     ! ! ! // Label(firstName) TextBox()
! ! ![13] ! ! [17]     ! ! ! // Label(mi)  TextBox()
! ! ![14] ! ! [18]     ! ! ! // Label(suffix)  TextBox()
! ! +-----+ +----------+ ! !
! +----------------------+ !
+--------------------------+

NameForm inherits from Form
Form inherits from CustomNode

// JavaFX
public class NameForm extends fxforms.ui.form.Form {
   public override function create():Node{
        // 0 main panel
        var mainPanel:Panel = Panel{
            content:[]
        }
        ... // more layouts and widgets

        var firstLabel:Label = Label {
            text: "First Name"
            hpos:HPos.RIGHT
            font : Font {
                size: 18
            }
            layoutInfo: LayoutInfo { minWidth: 100 width: 150 maxWidth: 200 }
        };
        ... // more code
        var lastNameTextBox:TextBox = fxforms.ui.controls.MyTextBox {
            id:"lastName"
            columns:20
        };
        var miNameTextBox:TextBox = fxforms.ui.controls.MyTextBox {
            id:"middleName"
            columns:20
        };
        ... // more fields

        // *** NOTE: This is for easy lookup. And relating to Scene.lookup(id) bug in 1.2.
        guiFields = [lastNameTextBox, firstNameTextBox, miNameTextBox, suffixNameTextBox];
        presentationModel.addGuiFields(guiFields);
        return mainPanel;
    } // create()
} // NameForm

NameForm.fx – This represents a Form containing a person’s name information.

form.fx – This is the base class which contains the presentation model for forms binding behavior.

controls.fx – This contains all registered GUI controls for observing value model value changes. Currently only one control exists the TextBox control.

*Note: The ideal way to build forms is using the JFXtras MigLayout library. To learn more take a look at Dean Iverson’s blog entry called “MigLayout for JavaFX Reloaded“.

Step 4. Associate a Presentation Model to Form

// JavaFX
var personForm:NameForm = NameForm{
    presentationModel:domain.model.personpresentationmodel.EditPersonPM{}
    translateX: bind slideFormX
};
var personBean2:domain.model.PersonBean = new domain.model.PersonBean();
personBean2.setFirstName("Squidward");
personBean2.setLastName("Tentacles");
personBean2.setMiddleName("Nickelodeon");
personBean2.setSuffixName("Sr.");

// set presentation model with domain object
personForm.presentationModel.jBean = personBean2;

Line 02: Associate presentation model to form
Line 05: Create an instance of a JavaBean
Line 12: Bind bean to presentation model and form

Once the presentation model and form are assembled binding an existing Java object is a snap. In part 4 on enhancing this process would be to create a factory to obtain meta information of the form to retrieve nested properties within a POJO/JavaBean off of the JavaFX main thread (desktop profile is the EDT). This effort will help alleviate from the dreaded Hibernate lazy init exception when using detached objects. So, making sure you don’t block the GUI thread is a big deal when it comes to user experience.

Step 5. Use the Form in an application

    var switchPersonButton:CheckBox = CheckBox {
            text: bind personToSwitchText
            width: 100
            translateX: 5
            translateY: bind mainScene.height - switchPersonButton.height - 5
            allowTriState: false
            selected: false
            override var onMouseReleased = function(e:MouseEvent):Void {
                if (selected){
                   personToSwitchText = "Sponge Bob";
                   personForm.presentationModel.jBean = personBean1;
                } else {
                   personToSwitchText = "Squidward";
                   personForm.presentationModel.jBean = personBean2;
                }
            }
        };

    var mainScene:Scene = Scene {
        fill: LinearGradient {
                    startX: 0
                    startY: 0
                    endX: 0
                    endY: 1
                    stops: [
                        Stop { offset: 0.1 color: Color.ORANGE },
                        Stop { offset: 1.0 color: Color.YELLOW },
                    ]
                }
        content: [personForm, switchPersonButton, backButton, nextButton]
    };

Main.fx – The main application file to launch the application

Value Model

The ‘Value Model‘ is probably the most important aspect of how the Forms binding works. The value model is a model that holds a single value that notifies registered listeners that a value has changed. Registered listeners will likely be GUI controls and JavaBean properties. Bidirectional binding occurs when a bean property value changes, which notifies the value model which updates the  GUI control value. This holds true when going in the other direction too, such as the user changes the value of the GUI control which notifies the value model which updates the JavaBean property value.

model.fx – Contains presentation model and value model implementations.

Conclusion

I feel the implementation of the fxforms framework using JavaFX was extremely easy and is a lot less code compared to a Java equivalent of a Swing/SWT forms framework, also carrying additional overhead using 3rd party libraries for binding and validation. As JavaFX matures with more controls the forms framework would need to be flexible enough to add any controls to handle custom bindings such as list models.  Next we will look at Part 4 Enhancements . As always any feedback is welcome!

References

Validation presentation by Karsten Lentzsch – http://www.jgoodies.com/articles/validation.pdf

JGoodies: http://www.jgoodies.com/

JGoodies Support: http://www.jgoodies.com/products/purchase.html

The Unknown JavaBean by Richard Bair – http://weblogs.java.net/blog/rbair/archive/2006/05/the_unknown_jav.html

JavaFX – JMS Unexpected Null Pointer Exception http://blogs.sun.com/clarkeman/entry/javafx_jms_unexpected_null_pointer

JFXtras Community Site – http://jfxtras.org/portal/home

My Triangle Space Ship – JavaFX media sound

Introduction

I grew up with old arcade games such as Asteroids and thought about making a simple ship, yes the famous triangle in JavaFX. I had to dig up my old trigonometry book. Remember unit circles? Although, I have learned that in JavaFX there is an attribute called rotation it is important to know that it is in degrees, so converting them to radian measure is key to making your velocity correct. Please keep in mind there are simple optimizations for my calculation and would need refactoring. Another goal was to play sound effects. Click below to launch the demo. The objective is to fly around and shoot at the box on the left. My little triangle space ship has powerful missiles!

Update

The I do not have a place to host the media Sound files and the demo will not play the sound effects. I am still looking at a way to put into the jar file and use them.

My Triangle Space Ship

My Triangle Space Ship

Commands:

Left arrow – move left
Right arrow – move right
Up arrow – fly forward
Down arrow – stop
Space bar – Fire missles


Click here (JavaWebstart): Play My Triangle Space Ship

Problem

  1. Learn rotation
  2. play sound effects

Solution

  1. Rotation is a Float which is in degrees at a pivot point or center of a rectangular bounded region. But for triangles or polygons use the translateX and translateY or the centroid formula.
  2. Use JavaFX’s media

var missleLaunch = Media{
source: “demos/fxfun/my_triangle_ship/Missile_SE.mp3”
}

var missleHit = Media{
source: “demos/fxfun/my_triangle_ship/explosion2.mp3”
}
var player = MediaPlayer{
media: missleLaunch,
autoPlay: false
}