FX My Life V: Whose Node Is It Anyway?

When we last left our intrepid program, we had an issue with the main window, in that if we set the center pane directly:

bp = ((BorderPane) scene.getRoot());
bp.setCenter(FXMLLoader.load(getClass().getResource( "main.fxml" )));

The window was nicely centered and reasonably large. But if we used our Node Controller:

NodeController.me.activate("main", central);

The window came up in the upper left, not centered and not very large.

At this point, I would like to use Scene View, a neat-sounding utility for that can apparently connect to a running JavaFX app and show you the application’s scene graph, because it seems a lot like Node Controller is putting the main window into the wrong place. Sort of like Scene Builder’s Document pane, but live.

I would like to use Scene View, but when I download it and try to build it, I get this error:

java.lang.UnsupportedClassVersionError: org/openjfx/gradle/JavaFXPlugin has been compiled by a more recent version of the Java Runtime (class file version 55.0), this version of the Java Runtime only recognizes class file versions up to 52.0

Well, hell. This gives me the option of trying to roll back my Java version, maybe, or…maybe fork this and update it…or…well, a lot of things that are not quite en pointe for our journey. (Note: Although pronounced “on point”, the phrase “en pointe” actually refers to ballerina’s being on their tippie-toes. But that’s not exactly en pointe.)

So, let’s just try to debug things the old-fashioned way. With an IDE running on a computer many times more powerful than all the ones that successfully landed and returned from the moon.

If we put a breakpoint where we set the center panel in the Main function:

And then step over it, we can see the following value in bp, which represents our BorderPane:

So far, so good. Now let’s kill this, comment out the code to setCenter — but leave in the bp because we’re going to want to compare:

bp = ((BorderPane) scene.getRoot());
// bp.setCenter(FXMLLoader.load(getClass().getResource( "main.fxml" )));

and set the center via our Node Controller’s activate method.

Actually, the place we should set a breakpoint is the code which fires after the animation is done:

And now if we look at Main’s bp field, we see:

So, before, we were putting the main window’s FlowPane directly into the BorderPane’s Center field, and now we’re taking the central Pane, identified in the SceneBuilder like so:

and putting our FlowPane into that.

Hmmm. The assumption was that Node Controller would take the parent of a given child node and replace that node. But a BorderPane doesn’t have a single entry point for children, it has top, left, right, bottom and center!

Confessions

I didn’t go through the whole process of creating the NodeController with you, though in my defense that’s mostly because I had forgotten it by the time I got to writing it down. (Mostly I write this blog as I go.) In trying to generalize the animation, I realized I couldn’t use my strategy of replacing one node with another under the original node’s parent because not all nodes have parents, at least not in the sense I needed (a parent with a single replaceable child).

Adding the central Pane allowed me to fudge this, but had the unfortunate side-effect we’re wrestling with now. Now, I can get around the issue of what to do at the end of the animation by passing in an Event that is to be fired at that end:

public void initialize() {
NodeController.me.activate("main", (Pane) Main.me.bp.getCenter(), event -> {
Main.me.bp.setCenter(NodeController.me.node("main"));
});

And we can change NodeController by having it require said Event:

protected void activate(String name, Pane owner, EventHandler<ActionEvent> e) {
Node node = nodeMap.get(name);
node.translateXProperty().set(owner.getWidth());
owner.getChildren().remove(node);
owner.getChildren().add(node);

KeyValue kv = new KeyValue(node.translateXProperty(), 0, Interpolator.EASE_IN);
KeyFrame kf = new KeyFrame(Duration.seconds(0.25), kv);

Timeline timeline = new Timeline();
timeline.getKeyFrames().add(kf);
timeline.setOnFinished(e);
timeline.play();
}

And we can make this transparent to any code we currently have that uses activate with a little polymorphism:

protected void activate(String name, Pane owner) {
Node node = nodeMap.get(name);
activate(name, owner, event -> {
owner.getChildren().setAll(node);
});
}

But while that should get us to the desired end product, we’re still stuck because the central panel in our Main window’s BorderPane starts out as null.

I was forced to pull a lot of stuff out of Controller:

package sample;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.layout.Pane;

public class Controller {

@FXML
public Pane central;
public NodeController nodeController;
public static Controller me;

public Controller() {
me = this;
nodeController = new NodeController();
loadNode("tic-tac-toe", "tttgrid.fxml");
loadNode("main", "main.fxml");
}

public void loadNode(String name, String resource) {
try {
nodeController.addNode(name, FXMLLoader.load(getClass().getResource(resource)));
} catch(Exception e) {
System.out.println(e.getMessage());
}
}
}

Main now looks like this:

package sample;

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;

public class Main extends Application {

public static Main me;
public ScreenController screenController; //For replacing EVERYTHING
public NodeController nodeController; //For just replacing nodes (primarily in the central pane)
public BorderPane bp;

@Override
public void start(Stage primaryStage) throws Exception{
me = this;
primaryStage.setTitle("Fun 'n' Games with JavaFX");
Scene scene = new Scene(new Group(), 800, 600);
scene.getStylesheets().add(getClass().getResource("styles.css").toExternalForm());
primaryStage.setScene(scene);

screenController = new ScreenController(scene); //not using, currently
screenController.addScreen("sample", FXMLLoader.load(getClass().getResource( "sample.fxml" )));
switchScreen("sample");
bp = ((BorderPane) scene.getRoot());

NodeController.me.activate("main", (Pane) scene.getRoot(), event -> {
Main.me.bp.setCenter(NodeController.me.node("main")); //odd, perhaps, to setAll after adding above, but the point is we want to remove anything there without caring what IS there
});
primaryStage.show();
}

public void switchScreen(String name) {
screenController.activate(name);
}

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

This gets us the tile window in the center where we want it, but now, though our TTT tile event does fire when it’s clicked, it’s not actually doing the animation any more. (*sigh*)

Where Did We Go Wrong?

It’s pretty clear that we went wrong when we created the NodeController — and quite frankly, I blame you. Why didn’t you stop me? (No excuses about how I didn’t ask your opinion or even tell you about it.)

What was our primary goal? Well, we wanted to centralize the animated transitioning between aspects of our games. We started with Screen Controller, which worked out fine because replacing the screen just requires calling setRoot on the main scene.

But is it possible to just sort-of generically swap nodes out? I’m not so sure. We were going under the illusion created by the happy graph that all the FX books show, but a BorderPane — and probably not just BorderPane, now that I think about it — is specifically more complicated than just “I’m a node with lots of children!” It’s got five separate branches, Bob! Top, left, center, bottom and right.

If we fix this up so that it looks closer to what we had in mind, we’ll need this code in Main:

@Override
public void start(Stage primaryStage) throws Exception{
me = this;
primaryStage.setTitle("Fun 'n' Games with JavaFX");
Scene scene = new Scene(new Group(), 800, 600);
scene.getStylesheets().add(getClass().getResource("styles.css").toExternalForm());
primaryStage.setScene(scene);

screenController = new ScreenController(scene); //not using, currently
screenController.addScreen("sample", FXMLLoader.load(getClass().getResource( "sample.fxml" )));
switchScreen("sample");
bp = ((BorderPane) scene.getRoot());

NodeController.me.activate("main", bp, event -> {
bp.getChildren().remove(NodeController.me.node("main"));
bp.setCenter(NodeController.me.node("main"));
});
primaryStage.show();
}

And we’ll basically have to copy that code in our TileController:

package sample;

import javafx.scene.input.MouseEvent;
import javafx.scene.layout.Pane;

public class TileController {

public void openTTT(MouseEvent mouseEvent) {
NodeController.me.activate("tic-tac-toe", (Pane) Main.me.bp.getCenter(), event -> {
((Pane) Main.me.bp.getCenter()).getChildren().clear();
Main.me.bp.setCenter(NodeController.me.node("tic-tac-toe"));
});
}
}

In switching out the TTT we can use the generic:

((Pane) Main.me.bp.getCenter()).getChildren().clear();

to erase whatever’s in the BorderPane’s center. I do not know, currently, why that line doesn’t work in Main. (And to be specific, it “works” but then calling setCenter gets the complaint that a duplicate child has been added. So somewhere, somehow, we’re adding “main” to before here.)

Clean Up

So far we’ve been very lasseiz-faire about what things are named in our app, and it’s been…okay. But occasionally it’s confusing, so let’s do some of the bargain-basement refactoring known as renaming.

Let’s change our package from “sample” to “fxgames”.

Our Application can still be called Main.

The main BorderPane with the button bar on it will be called frame(.fxml).

The main flow panel with the tiles (containing our tic-tac-toe and future game icons that the user clicks on to start them) will be called tilespanel(.fxml).

The “tttgrid.fxml” and everything else can stay the same for now. We’ll revisit all this stuff as the app grows.

I am a poor, wayfaring stranger, traveling through this world of woe.

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store