netbeans 9 rc1 is here, and it’s very good

It’s been a long time coming. Netbeans, which is now a part of the Apache Foundation, announced Netbeans 9 RC1 back on May 28 (go to https://netbeans.apache.org/ to stay abreast of Netbeans’ news and to download the latest releases).

RC1 downloads as a zip file, which I was able to unzip onto my MBP. That leaves the complete application sitting in a netbeans folder. Step into netbeans/bin and execute the netbeans shell script to bring up Netbeans 9. No problems, no issues. I like the fact it doesn’t require an installer and frankly hope they keep it this way. It even found my older Netbeans 8.2 installation configuration and, miracle of miracles, found and loaded my Darcula (that’s spelled correctly, by the way) LAF plugin. Darcula is working just fine as you can see in the screen shot. All of this is working with Java 10.0.1. The project loaded above builds and executes just fine under Java 10.

I said earlier that Netbeans 9 has been a long time coming. That’s because it was originally slated to be released the same time Java 9 was released back in September 2017. Here we are mid-2018, a good nine months after the initially planned release date. I know a lot of effort went in to making this happen, what with the shift out from Oracle and to Apache, which required additional effort to remove all copyright notices from all the source files (I was a lurker to the ongoing process). They finally finished to the point where they are releasing a near-final Netbeans 9. Of course, if you were following along, you could pull a copy of the Netbeans source and build it yourself, which I did a number of times just to observe the progress. Netbeans 9 is now reaching an official release point.

I’m very happy to see Netbeans at this stage, and I know it will be officially released Real Soon Now. What will transpire after the official Netbeans 9 release is anyone’s guess. Once again Oracle has roiled the community with its change to how their Java is licensed and paid for; they want business use to be subscription based. Although individual seats appear low cost, multiply that across a large enterprise and the costs literally add up very quickly. The only way out would be open source Java, specifically OpenJDK, which I took a look at when I installed Linux Mint 19. Linux Mint 19 comes with OpenJDK 10. I’ve already found dissatisfaction with OpenJDK 10; JavaFX is a separate package, openjfx, and even though I installed that via apt, my pre-built JavaFX applications still fail to run because of issues with JavaFX under OpenJDK/OpenJFX. No, I won’t install Oracle Java on Linux Mint 19. I have, instead, installed Google’s Go. For UI work I’ll look at Javascript/Typescript tied to a Go HTTP RESTful framework. That’s an outgrowth of my work with Go on the Raspberry Pi, which I’ve written about elsewhere.

The group behind the release of Netbeans 9 is to be commended for their hard work and dedication. They did a superb job from what I’ve been able to see so far. For those that intend to continue working with Java, have been waiting for Netbeans 9, and plan to move to Java 11, Netbeans 9 is an excellent tool.

experimenting with javafx and java 8, part 2

As I continued to work a bit with the example application from yesterday, I started adding more code to the ResizeableCavas class so it would draw more stuff. In particular I wanted a grid drawn underneath the target as shown above. That’s when the aversion to too many lines of code in a given method or class kicked in, and I reorganized the application to break up functionality into something more manageable and reusable.

Thus, the creation of a CanvasLayer interface and two implementations using the interface, and some simple extensions to add those layers to the ResizeableCanvas. New code to follow.

package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;

/**
 *
 * @author wbeebe
 */
public interface CanvasLayer {
    void draw(GraphicsContext gc, double width, double height);
}

And the two implementations.

package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;

/**
 *
 * @author wbeebe
 */
public class GridLayer implements CanvasLayer {

    @Override
    public void draw(GraphicsContext gc, double width, double height) {
        gc.clearRect(0, 0, width, height);
        // lighter than LIGHTGRAY
        gc.setStroke(Color.rgb(230, 230, 230));

        // Create all the vertical lines.
        //
        for (double i = 0; i <= width; i += 10) {
            gc.setLineWidth((i % 100) == 0 ? 2 : 1);
            gc.strokeLine(i, 0, i, height-2);
        }

        // Create all the horizontal lines.
        //
        for (double j = 0; j <= height; j += 10) {
            gc.setLineWidth((j % 100) == 0 ? 2 : 1);
            gc.strokeLine(0, j, width-2, j);
        }
    }
}
package borderpaneexample;

import javafx.scene.canvas.GraphicsContext;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;

/**
 *
 * @author wbeebe
 */
public class TargetLayer implements CanvasLayer {

    @Override
    public void draw(GraphicsContext gc, double width, double height) {
        gc.fillOval((width - 40)/2.0, (height - 40)/2.0, 40, 40);

        Font font = gc.getFont();
        gc.fillText("Width: " + Double.toString(width), width/2 + 2, font.getSize());

        // Some interesting graphics context manipulation. Move to the left
        // edge and print the height, rotated 90 degrees parallel to the
        // left edge.
        //
        gc.save();
        gc.translate(font.getSize(), height/2);
        gc.rotate(-90);
        gc.fillText("Height: " + Double.toString(height), 2, 0);
        gc.restore();

        gc.setStroke(Color.RED);
        gc.setLineWidth(2);
        gc.strokeLine(0, 0, width, height);
        gc.strokeLine(0, height, width, 0);
        gc.strokeLine(width/2.0, 0, width/2.0, height);
        gc.strokeLine(0, height/2.0, width, height/2.0);
    }
}

And now the reworked ResizeableCanvas. A new method was implemented to add CanvasLayers to be drawn. All changes to support a list of CanvasLayers are highlighted.

package borderpaneexample;

import java.util.ArrayList;
import java.util.List;
import javafx.scene.canvas.Canvas;
import javafx.scene.canvas.GraphicsContext;
import javafx.scene.layout.Region;

/**
 *
 * @author wbeebe
 */

public class ResizeableCanvas extends Canvas {
    List<CanvasLayer>canvasLayerList;
    /**
     * ResizeableCanvas provides a resizeable Canvas.
     * 
     * Resizeable Canvas requires that it be wrapped in a Region.
 
 To use, follow these steps:
 1) Instantiate a wrap around Region instance, such as Pane;
 2) Instantiate a ResizeableCanvas with the Region class instance;
 3) Call the Region's getChildren().add(...) method to add the
    ResizeableCanvas instance to the Region instance.
 
 This is especially effective, and required, if you want a resizable
 Canvas in the center of a BorderPane and want to use any of the other
 BorderPane regions.
 
 Internally the Resizable Canvas is bound to the wrapper Region's
 width and heigh property; when the wrapper Region is resized, then
 the Resiable Canvas' resizeDraw() method is called, producing the effect of
 resizing through redrawing.
 
 For this to work, isResizable() must be overridden to return true,
 prefWidth() overridden to return the current width, and
 prefHeight() overridden to return the current height.
     * 
     * @param region 
     */
    public ResizeableCanvas(Region region) {
        this.canvasLayerList = new ArrayList<>();
        widthProperty().bind(region.widthProperty());
        heightProperty().bind(region.heightProperty());
        widthProperty().addListener(event -> resizeDraw() );
        heightProperty().addListener(event -> resizeDraw() );
    }

    /**
     * Basically shows how to use the GraphicsContext to resizeDraw into the Canvas.
     * Any graphic operation that can be supported by a Canvas can be performed
     * here on a resize event.
     */
    private void resizeDraw() {
        double width = getWidth();
        double height = getHeight();
        GraphicsContext gc = getGraphicsContext2D();

        canvasLayerList.forEach((canvasLayer) -> {
            canvasLayer.draw(gc, width, height);
        });
    }

    public void addLayer(CanvasLayer canvasLayer) {
        canvasLayerList.add(canvasLayer);
    }

    @Override
    public boolean isResizable() {
      return true;
    }
 
    @Override
    public double prefWidth(double height) {
      return getWidth();
    }
 
    @Override
    public double prefHeight(double width) {
      return getHeight();
    }
}

And finally the main class. Again, code for implementing the ResizeableCanvas and its changes are highlighted.

package borderpaneexample;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

/**
 *
 * @author wbeebe
 */
public class BorderPaneExample extends Application {
    
    @Override
    public void start(Stage primaryStage) {
        BorderPane borderPane = new BorderPane();

        Button rightButton = new Button("Right");
        rightButton.setOnAction((ActionEvent event) -> {
            System.out.println("Right button");
        });

        Button leftButton = new Button("Left");
        leftButton.setOnAction((ActionEvent event) -> {
            System.out.println("Left button");
        });

        Button topButton = new Button("Top");
        topButton.setOnAction((ActionEvent event) -> {
            System.out.println("Top button");
        });

        Button bottomButton = new Button("Bottom");
        bottomButton.setOnAction((ActionEvent event) -> {
            System.out.println("Bottom button");
        });
        
        Pane centerPane = new Pane();
        ResizeableCanvas resizeableCanvas = new ResizeableCanvas(centerPane);
        centerPane.getChildren().add(resizeableCanvas);
        centerPane.setStyle(
            "-fx-padding: 0;" +
            "-fx-border-style: solid inside;" +
            "-fx-border-width: 1;" +
            "-fx-border-insets: 0;" +
            "-fx-border-radius: 0;" +
            "-fx-border-color: #000;");
        resizeableCanvas.addLayer(new GridLayer());
        resizeableCanvas.addLayer(new TargetLayer());

        borderPane.setCenter(centerPane);
        borderPane.setRight(rightButton);
        BorderPane.setAlignment(rightButton, Pos.CENTER_RIGHT);
        VBox vbox = makeVBox();
        vbox.getChildren().add(leftButton);
        borderPane.setLeft(vbox);
        BorderPane.setAlignment(leftButton, Pos.CENTER_LEFT);
        HBox hbox = makeHBox();
        hbox.getChildren().add(topButton);
        borderPane.setTop(hbox);
        BorderPane.setAlignment(topButton, Pos.TOP_CENTER);
        borderPane.setBottom(bottomButton);
        BorderPane.setAlignment(bottomButton, Pos.BOTTOM_CENTER);
        borderPane.setStyle(
            "-fx-padding: 2;" +
            "-fx-border-style: solid inside;" +
            "-fx-border-width: 2;" +
            "-fx-border-insets: 2;" +
            "-fx-border-radius: 0;" +
            "-fx-border-color: #cccccc;");


        Scene scene = new Scene(borderPane, 800, 600);
        primaryStage.setTitle("BorderPane Example with Canvas");
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) {
        launch(args);
    }

    public HBox makeHBox() {
        HBox hbox = new HBox();
        hbox.setPadding(new Insets(5, 5, 5, 5));
        hbox.setSpacing(5);
        hbox.setStyle("-fx-background-color: #cccccc;");
        return hbox;
    }

    public VBox makeVBox() {
        VBox vbox = new VBox();
        vbox.setPadding(new Insets(5, 5, 5, 5));
        vbox.setSpacing(5);
        vbox.setStyle("-fx-background-color: #dddddd;");
        return vbox;
    }
}

The order that CanvasLayers are added is important, as that’s the order in which they are rendered on a call to ResizeableCanvas’ resizeDraw() method. It’s also important to realize that the lowest layer to render should have a call to GraphicsContext’s clearRect(…) method in order to clear the Canvas area before anything is redrawn. Else ResizeableCanvas rapidly turns into a mess every time the application is resized. Comment out the addition of GridLayer (line 55 above) so that only TargetLayer is added and watch what happens as you resize the application.

Not sure at this point what to do next. A map could easily be displayed in a layer, but ResizeableCanvas doesn’t know about a viewport into an arbitrarily larger context, or the ability to move around within it using sliders. Something else to contemplate.