2

I want to add a new row to a GridPane and make it visible within the containing ScrollPane.

Here's my test mule for Java 17, JFX 19.

package pkg.scrolltest;

// imports ellided

public class App extends Application {

    GridPane gridPane;
    ScrollPane sp;
    
    @Override
    public void start(Stage stage) {
        var scene = new Scene(getPane(), 640, 480);
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch();
    }
    
    public Parent getPane() {
        gridPane = new GridPane();
        gridPane.add(new Label("First label"), 0, 0);
        Button addButton = new Button("Add");
        addButton.setOnAction((event) -> {
            int rows = gridPane.getRowCount();
            gridPane.add(new Label("This is a label " + rows), 0, rows);
            sp.setVvalue(1);
        });
        sp = new ScrollPane(gridPane);
        VBox vb = new VBox(addButton, sp);
        
        return vb;
    }
}

The sp.setVvalue(1) should scroll to the end of the wrapped pane.

The problem is, at this point of the lifecycle, while the new label has been added to the GridPane, none of the layout has happened yet, so the GridPane has not got larger, and the ScrollPane has not had an opportunity to react to that growth.

The only way I've come up as a workaround is using a timer to fire the Vvalue change "later", after all of the layout has occurred.

Timer t = new Timer();
t.schedule(new TimerTask() {
    @Override
    public void run() {
        Platform.runLater(() -> {
            sp.setVvalue(1);
        });
    }
}, 50);

While this works, it's a bit hand wavy, and I'd like to think I could do this more deterministically.

This does not work:

gridPane.requestLayout();
sp.setVvalue(1);

As I understand it, requestLayout is just that -- a request, it does not happen in line. Rather it queues it up to happen later.

Is there a better way to do this?

EDIT: Thanks jewelsea, indeed forcing the layout did work. I'm curious why it did not for James_D

gridPane.add(label, 0, rows);
sp.applyCss();
sp.layout();
sp.setVvalue(1);

This worked a charm for me.

Will Hartung
  • 115,893
  • 19
  • 128
  • 203
  • If you really need to, see [generate a layout pass](https://stackoverflow.com/questions/26152642/get-the-height-of-a-node-in-javafx-generate-a-layout-pass). – jewelsea Jan 24 '23 at 17:02

2 Answers2

3

I can't make this work by generating a layout pass; maybe I am missing something and someone else can.

Edit: I was trying by laying out the GridPane, which seems natural to me (it is the GridPane that has to compute its new size). It seems forcing layout of the ScrollPane works.

Another way that works is to override GridPane.layoutChildren() to scroll the scroll pane when needed, but that seems like a bit of a hack (the GridPane really shouldn't be manipulating ancestor nodes in the scene graph).

A third, and reasonably clean, way is to use a listener on the height of the grid pane, and only call sp.setVvalue(1) after the height changes. Note this doesn't require any additional layout calculations, or change the natural order the layout is computed on the scene graph, so it might be considered preferable in some ways. On the other hand, it does assume the scroll pane's vvalue is orthogonal to the content's height. That should be the case but this does rely on the scroll pane implementation working as expected in some sense.

Something like:

public class App extends Application {


    GridPane gridPane;
    ScrollPane sp;

    private boolean scrollToNewNode ;

    @Override
    public void start(Stage stage) {
        var scene = new Scene(getPane(), 640, 480);
        stage.setScene(scene);
        stage.show();
    }

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

    public Parent getPane() {
        gridPane = new GridPane();
        gridPane.add(new Label("First label"), 0, 0);
        Button addButton = new Button("Add");
        addButton.setOnAction((event) -> {
            int rows = gridPane.getRowCount();
            scrollToNewNode=true;
            gridPane.add(new Label("This is a label " + rows), 0, rows);
        });
        sp = new ScrollPane(gridPane);

        gridPane.heightProperty().addListener((obs, oldHeight, newHeight) -> {
            if (scrollToNewNode) {
                sp.setVvalue(1);
                scrollToNewNode=false;
            }
        });
        VBox vb = new VBox(addButton, sp);

        return vb;
    }
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Curious why the forced layout did not work. – Will Hartung Jan 24 '23 at 20:57
  • @WillHartung Hmm. I was laying out the `GridPane`, not the `ScrollPane`. That makes sense (I suppose?). You should post the solution as an answer, not embed it in the question. – James_D Jan 24 '23 at 21:07
3

Thanks jewelsea, indeed forcing the layout did work.

gridPane.add(label, 0, rows);
sp.applyCss();
sp.layout();
sp.setVvalue(1);

This worked a charm for me.

Will Hartung
  • 115,893
  • 19
  • 128
  • 203