RSS

Visual Feedback on an Abstract Parsing Tree with JavaFX

24 Dec

I honestly didn’t expect to be writing this, but it seems fair.

In the past few editions, I’ve been discussing the AST. It can be overwhelmingly complicated for a complete program; so I’ve been using a simple, single line equation as the sample. Unfortunately, that isn’t very realistic; and it would be very helpful to have a procedurally generated visual tree available. That tree is what this lesson is all about.

At first I considered using a graphical tree style, like javax.swing.JTree; but that can be painfully over-simplistic in itself. I would prefer to outline the material the same way I would draw it on a white board (which, if you’re wondering, I do). The best way to do this? JavaFX.

JavaFX whiteboard abstract syntax tree

Graphical AST tree rendering, through JavaFX/F3

If you aren’t familiar with JavaFX, please do me a favor and tolerate the name. It was originally F3, for Form-Follows-Function. I kind of liked F3, until some marketer decided that “JavaFX” sounded better. Functionally speaking, it’s an excellent revision on how user interfaces are designed in Java. I fully stand behind it. It allows for XML structuring and CSS styling, just like a web page, to more hard-coded controls. This is much, much faster; and it allows for significant beauty in user interfaces. However, it works very differently from things like Swing and AWT; and while I’m certain that it isn’t the first API to do so, it takes some getting used to.

I fully intend to write a true tutorial on all of JavaFX on some point. Do you need to understand it to understand translators? Absolutely not. However, this code does work. It is not part of the Github repository, as it is technically a tangential project; but the same license (GNU GPL) applies to it and you are welcome to copy it token for token. I’ll put it up on Github as I get the chance. I’ll make a few minor comments along the way to help you follow it.

1. The Basic Application

We have exceedingly few needs for our app. It simply reads a program from a stream, parses it, and feeds the parse tree to a custom node, which displays it graphically. Accordingly, the program code is rather small. I’ll begin by displaying it, then I’ll spend a moment piecing it together in English for you.

package oberlin.builder.gui;

import oberlin.algebra.builder.AlgebraicBuilder;
import oberlin.builder.parser.ast.AST;
import javafx.application.*;
import javafx.scene.*;
import javafx.scene.layout.*;
import javafx.stage.*;

public class GUIMain extends Application {

    public static void main(String...args) {
        launch(args);
    }

    @Override
    public void start(Stage primaryStage) throws Exception {
        Pane root = new Pane();
        root.getStyleClass().add("backing");
        
        Scene scene = new Scene(root);
        scene.getStylesheets().add(GUIMain.class.getResource("tree.css").toExternalForm());
        
        primaryStage.setScene(scene);
        root.setMinWidth(640.0);
        root.setMinHeight(480.0);
        
        populate(root);
        
        primaryStage.show();
        primaryStage.centerOnScreen();
    }
    
    private void populate(Pane p) {
        /*
         * This is simply an example, so I've ignored input for now.
         * In theory, you would replace the line below (containing
         * hard code) with an input loop.
         */
        AST program = (new AlgebraicBuilder()).getParseTree("1+2");
        
        p.getChildren().add(new GUITree(program));
    }
}

All JavaFX/F3 programs begin with Application.launch(String…args). JavaFX programs run in what is effectively their own thread, and more so than with Swing-based programs. Launch parses arguments and stores them in their own object, appropriately called Parameters. They can be accessed, at any point later on, via Application.getParameters(). Our available overloads and customizations cut out for a moment, then come back in in the start(Stage) method.

Stage is basically where Frame would be; but it’s a little more complicated than that. Unlike Swing and AWT, which were designed to be platform independent, JavaFX is designed to be hardware context independent. What you are writing here will work equally well on a PC, tablet, and smart phone; as well as anything else built (now or later) that maintains a JavaFX compatibility standard. Thus, what might otherwise be called a frame or window is referred to as a stage, as it might be neither of those things.

You’ll notice that the instantiated Pane is given a style class. If you aren’t familiar with CSS, a style class is what’s used to differentiate between one element and any number of others which, otherwise, would look exactly like it. Thus, it allows CSS to pick and choose which elements of the layout it is styling at a given moment. I’ve chosen “backing” as the name for this element, as it is the backboard of our tree. You will also note that, two lines later, the CSS file itself is loaded.

Next, a Scene is created. Scenes are critically important, and distinct from stages. While a stage represents the context that the layout is drawn in, the scene represents the actual controls and constraints within that space. Thus, while many aspects of Stage are immutable (and unknowable), Scene allows for greater flexibility. JavaFX sees to it that they correspond, so don’t worry about that.

Scene is styled through its root element, which in this case is our pane. You’ll notice that instead of the stricter setWidth() and setHeight() that you might be familiar with from Swing, we are setting a minimum on these bounds. That minimum is not guaranteed, as the display may not be capable of it, but it is treated as a general rule to be followed if at all possible. In this case, I’m going for classic analog low-def TV resolution, 640 width by 480 height. (Looking back, those numbers might be inadequate, but for now they’re quite functional.) If this is too small for you, the frame—if it is a frame, anyway—is easily resizable.

Populate() is a method I wrote to add the paraphernalia to the scene; but note that afterwards we call show(). This is very important, as otherwise our stage will be constructed in memory, but never displayed to the screen. Additionally, there will be no way to kill the JavaFX thread save for a hard interrupt. Once shown, the closing of the primary stage will flag the program to terminate.

1.1. Populate

It’s a generally good habit, but not a necessary one, to populate your frame in a separate and dedicated method. This is what I do here, even though for the moment, I only have one control to add.

The AST method should be old news; it’s a stand-in, for the moment, for an actual code-reading portion. (I’m assuming that you’re looking to compile more than just “1+2”.) GUITree is a custom JavaFX node, which I will explain next. Note that to add a node to a program, you must take some structure (not yet visible) in the scene graph (stemming from your chosen root), and get its Children as a modifiable list. Then, you must add that node to the list.

Note that after a stage is visible, precious little of the scene can be changed save for through the constraints built into it. I’m not going to touch on Expressions and Bindings here, but know that if you pull something that doesn’t play by JavaFX’s rulebook, it will throw an ApplicationException and your program will not launch. Thankfully, while exceedingly picky, that rulebook is small. If you call show() and then try and add a child, you will have problems; it must be the other way around.

If you’re curious, hiding a rendered stage does not count for making it modifiable. You must give it your entire concept first, then make it visible. If you’re familiar with OpenGL, you’ll already understand why.

2. The Tree Itself

The tree is a custom JavaFX node, which I admit is rarely necessary. Still, most of the entities that make it work are core to the API.

package oberlin.builder.gui;

import oberlin.builder.parser.ast.AST;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.scene.control.Tooltip;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.shape.CubicCurve;

import java.util.function.IntSupplier;

public class GUITree extends AnchorPane {
    private Bounds bounds = new BoundingBox(0, 0, 640, 480);
    private AnchorPane framing = new AnchorPane();
    private double edgeSize = 0.10;    //ten percent additional length beyond edges of framing
    
    public GUITree(AST ast) {
        this.setMinWidth(bounds.getWidth() * (1 + edgeSize));
        this.setMinHeight(bounds.getHeight() * (1 + edgeSize));
        
        configureFraming();
        
        addNode(ast);
    }
    
    private void configureFraming() {
        framing.setLayoutX(edgeSize * (bounds.getWidth() / 2.0));
        framing.setLayoutY(edgeSize * (bounds.getHeight() / 2.0));
        framing.setMinWidth(bounds.getWidth());
        framing.setMinHeight(bounds.getHeight());
        
        this.getChildren().add(framing);
    }
    
    private ASTNode addNode(AST ast) {
        return this.addNode(ast, new Marker(0), new Counter(), 0, null);
    }
    
    private ASTNode addNode(AST ast, IntSupplier stepsDown, IntSupplier stepsAcross, int index, ASTNode parent) {
        ASTNode node = new ASTNode(ast, stepsDown.getAsInt(), stepsAcross.getAsInt());
        
        //AnchorPane stuff
        calculateAnchoring(node, parent);
        
        framing.getChildren().add(index ++, node);
        
        final StringBuilder tooltipText = new StringBuilder();
        IntSupplier across = new Counter();
        for(AST kid : ast.getContainedNodes()) {
            tooltipText.append(kid.getClass().getSimpleName()).append(" ");
            ASTNode child = addNode(kid,
                    new Marker(stepsDown.getAsInt() + 1),
                    across,
                    index,
                    node);
            CubicCurve cubic = createCubicCurve(node.getNoodleRoot(), child.getTopCenter());
            framing.getChildren().add(cubic);
        }
        node.getType().setTooltip(new Tooltip(tooltipText.toString()));
        
        return node;
    }
    
    private CubicCurve createCubicCurve(Point2D p1, Point2D p2) {
        CubicCurve curve = new CubicCurve();
        
        curve.setStartX(p1.getX());
        curve.setStartY(p1.getY());
        
        curve.setEndX(p2.getX());
        curve.setEndY(p2.getY());
        
        curve.setControlX1(p1.getX());
        curve.setControlY1(p2.getY());
        
        curve.setControlX2(p2.getX());
        curve.setControlY2(p1.getY());
        
        curve.setStroke(Color.BLACK);
        curve.setStrokeWidth(2.0);
        curve.setFill(Color.TRANSPARENT);
        
        return curve;
    }
    
    private void calculateAnchoring(ASTNode node, ASTNode parent) {
        node.setOrigin(new Point2D(parent == null ? (bounds.getWidth() - node.getBounds().getWidth())/2.0 :
            justifyX(node, parent), justifyY(node)));
        AnchorPane.setTopAnchor(node, node.getOrigin().getY());
        AnchorPane.setLeftAnchor(node, node.getOrigin().getX());
    }
    
    private Double justifyX(ASTNode node, ASTNode parent) {
        final double parentCenter = (parent.getOrigin().getX() + (parent.getBounds().getWidth() / 2.0)
                + parent.getNoodleRoot().getX()) / 2.0;
        final double center = parentCenter
                - node.getBounds().getWidth()
                        * (parent.getAST().getElementCount()) / 2.0; 
        return center + node.getOrigin().getX();
    }
    
    private Double justifyY(ASTNode node) {
        return node.getOrigin().getY();
    }
}

That was a bit much at once, I know. The central pane, called “framing”, is 640 by 480. Framing is offset in each direction by a 5% inset, via the convenient features of AnchorPane.

AnchorPane is one of the few prepared ways to control where a node is rendered, with precision, in JavaFX. You may often need to keep your own tabs on where it is rendered, as getMinX() and getMaxX() will return zero more often than you will believe. However, through direct layout control, you can still manage them.

The method addNode(…) adds a custom object called ASTNode. I’ll cite it for you here.

package oberlin.builder.gui;

import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.BoundingBox;
import javafx.geometry.Bounds;
import javafx.geometry.Point2D;
import javafx.geometry.Pos;
import javafx.scene.control.Label;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.text.TextAlignment;
import oberlin.builder.parser.ast.AST;

class ASTNode extends VBox {
    private Bounds bounds = new BoundingBox(0, 0, 100, 40);
    private Point2D origin = new Point2D(0, 0);
    private final double expanse = 1.10;
    
    private final AST ast;
    
    private Label type;
    private Label hash;
    
    private ObservableList<ASTNode> kids = FXCollections.observableArrayList();
    
    public ASTNode(AST ast) {
        this.ast = ast;
        type = new Label(ast.getClass().getSimpleName().toString());
        type.setTextAlignment(TextAlignment.CENTER);
        type.setAlignment(Pos.CENTER);
        
        hash = new Label(Long.toHexString(ast.hashCode()).toUpperCase());
        hash.setTextAlignment(TextAlignment.CENTER);
        hash.setAlignment(Pos.CENTER);
        
        VBox vbox = new VBox(new StackPane(type), new StackPane(hash));
        vbox.getStyleClass().add("node");
        vbox.setMinWidth(bounds.getWidth());
        vbox.setMinHeight(bounds.getHeight());
        this.getChildren().add(vbox);
        
        for(AST kid : ast.getContainedNodes()) {
            addKid(new ASTNode(kid));
        }
        
    }
    
    public Point2D getNoodleRoot() {
        return new Point2D(getOrigin().getX() + (getBounds().getWidth() / 2),
                getOrigin().getY() + getBounds().getHeight());
    }

    public ASTNode(AST ast, int level) {
        this(ast);
        
        origin = new Point2D(0, level * bounds.getHeight() * expanse);
    }
    
    public ASTNode(AST ast, int levelDown, int levelAcross) {
        this(ast);
        
        origin = new Point2D(getStepAcrossSize(levelAcross), getStepDownSize(levelDown));
    }
    
    public double getStepDownSize(int steps) {
        return steps * bounds.getHeight() * expanse;
    }
    
    public double getStepAcrossSize(int steps) {
        return steps * bounds.getWidth() * expanse;
    }
    public void addKid(ASTNode astNode) {
        this.kids.add(astNode);
    }
    
    public ObservableList<ASTNode> getKids() {
        return kids;
    }
    
    public Bounds getBounds() {
        return bounds;
    }
    
    public Point2D getOrigin() {
        return origin;
    }
    
    public Point2D getTopCenter() {
        return new Point2D(
                getOrigin().getX() + (getBounds().getWidth()/2),
                getOrigin().getY());
    }
    
    public Label getType() {
        return type;
    }
    
    public void setOrigin(Point2D p) {
        this.origin = p;
    }
    
    public AST getAST() {
        return this.ast;
    }
}

ASTNode is a JavaFX Node as well. It simply maintains a reference to the AST itself, and the general presentation of that AST on the tree. There isn’t a lot here. If you’re wondering what VBox is, it’s an abbreviation for “vertical box”. (Naming a class after an abbreviation is bad practice, but it’s long since done by powers above me; I tolerate it as much as I do “AST”.)

Speaking of bad practice, this would ideally actually use Bindings, but I wrote this in a bit of a rush today and will have to correct that in the future. It is also bad practice to repeat data, which is exactly what this program is doing by re-storing the label text in a separate field. All the same…

I’m going to gloss over a lot of the configuration of the labels, as it’s relatively standard. Know that like any other pane in JavaFX, a VBox can be initialized with a list of its bounded nodes; also, a StackPane has the default behavior of centering its own bounded nodes.

The last thing done in the constructor is the creation of additional ASTNodes for each child node of the abstract syntax tree.  Each of them, in turn, renders their own children. This is not perfect, there is a substantial chance that two lists of nodes will overlap one another; however, it is already excellent for debugging visitor pattern based content. In the end, the GUITree renders each node in an assigned place, with a curved cubic line (technically called a “noodle”) connecting it to its parent and its children.

How does it do that? With IntSuppliers.

3. The IntSuppliers

There are only two of these.

package oberlin.builder.gui;

import java.util.function.IntSupplier;

/**
 * For downward counts; always returns provided number.
 * 
 * @author © Michael Eric Oberlin Dec 23, 2014
 *
 */
class Marker implements IntSupplier {
    private int fix;
    
    public Marker(int fix) {
        this.fix = fix;
    }
    
    @Override
    public int getAsInt() {
        return fix;
    }
}


package oberlin.builder.gui;

import java.util.function.IntSupplier;

/**
 * For counts across; always returns next consecutive number.
 * 
 * @author © Michael Eric Oberlin Dec 23, 2014
 *
 */
class Counter implements IntSupplier {
    private int count;
    
    @Override
    public int getAsInt() {
        return count++;
    }
    
}

IntSuppliers (and really all Suppliers) are part of the java.util.function package, new to Java 8. The great advantage of this package is that a function, or any functional interface, allows you to specify a method that serves as a primitive with conditionally defined values. I know that’s a leap, but I’ve been doing it since long before it was formally adopted into the language and it’s a central totem of functional languages.

We could, in theory and practice, use incrementing and decrementing integers in place of either of these. The problem is that the code gets a lot longer and a lot more cluttered when you do. I prefer the sublime simplicity of packing such behavior into an interface.

Of course, these are not everything. There is one, final, issue.

4. What was that that you said about “CSS”?

The CSS is specific to JavaFX; a complete listing of all of the properties is available here. If you are unfamiliar with the syntax of CSS, you can find an excellent tutorial on it (for HTML, at least) at W3Schools. It isn’t as versatile as Java or C, but its creators pulled many of its properties from C-like languages.

tree.css:

.backing {
    -fx-background-color: lightyellow;
    -fx-insets: 0;
    -fx-padding: 15;
    -fx-spacing: 10;
}

.node {
    -fx-background-color: lightblue;
    -fx-background-radius: 5.0;
    -fx-border-color: black;
    -fx-border-radius: 5.0; 
}

Keep this in the same folder as GUIMain, and it will find it as written.

The CSS styling of JavaFX controls is capable of everything HTML 5 is and then some. It’s an excellent fusion of programming and markup. I encourage you to play with the layout of GUIMain’s scene, and the actual program fed to the builder.

Advertisements
 

Tags: , , , , , , , , , , , , , , , ,

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

 
%d bloggers like this: