11

I have Scene which is set to the Scene of my primaryStage that - amongst other nodes - contains a VBox with a TableView and some buttons. When I take a snapshot on a row in the table using TableRow.snapshot(null, null), the size of the Scene is changed. The width is changed by about 10 pixels while the height is changed by about 40 - sometimes more than 600 (!) - pixels.

This happens because Node.snapshot(null, null) invokes Scene.doCSSLayoutSyncForSnapshot(Node node) which seems to get the preferred size of all nodes in the size and recalculate the size using that. This somehow returns the wrong values since my nodes only has preferred sizes specified and looks great before this method is invoked. Is there any way to prevent this?

The size change is a problem, but it is also a problem that the primary stage doesn't change size with the Scene that it contains.

I have tried to create an MCVE reproducing the issue, but after a few days of trying to do this, I am still unable to reproduce the problem. The original program contains around 2000 lines of code that I don't want to post here.

Why would Scene.doCSSLayoutSyncForSnapshot(Node node) compromise my layout when it is properly laid out in the first place? Can I somehow make sure that the layout is properly synced before this method is invoked to make sure that it doesn't change anything?

Jonatan Stenbacka
  • 1,824
  • 2
  • 23
  • 48
  • well you can try to set constraints on your Nodes prior the evoking that function and undo them, i mean, `setMinSize(Pane.USE_PREF_SIZE,Pane.USE_PREF_SIZE);` and `setMaxSize(Pane.USE_PREF_SIZE,Pane.USE_PREF_SIZE);` it might help from the way you described your problem, :) – Elltz May 28 '16 at 10:42
  • Thanks for not posting all 2000 lines of code. But if you can write a short, compilable sample to reproduce the issue that would be helpful. Otherwise a link to all your code if you can't reproduce a small sample? – Amber May 31 '16 at 19:17
  • I suggest to use [Scenic View](http://fxexperience.com/scenic-view/) which allows you to view all properties on an View during runtime of you GUI. This will let you at least check if the problem you describe is really the problem. You can also maybe post some screen shots using Scenic View. This tool also allows you to view all events which happen in you GUI. I use it a lot :-) – Westranger Jun 03 '16 at 07:16
  • Thanks for that tool! I will probably have great use of it in the future. But I was already certain that the problem I described was really the problem, since I've added listeners to every size property in the view (a lot more work than it would've been with Scenic View, so thanks again for that tool). Sadly I've not had the time to post a link to my code due to work, since I need to mask a some variable names etc before I can post it. I will try to do this before the bounty expires. – Jonatan Stenbacka Jun 03 '16 at 10:17
  • How do you initialize the scene? Based on the implementation of `Scene.doCSSLayoutSyncForSnapshot` it looks like if you don't use the `init(width, height)` method your scene will be resized based on the layoutX/Y, translateX/Y and layoutBounds width/height. – vl4d1m1r4 Jun 03 '16 at 13:38
  • I just create a new scene with my `BorderPane` as the parameter: `Scene scene = new Scene(rootLayout);`. I can't find this `init(width, height)` you are talking about, how can I invoke it? – Jonatan Stenbacka Jun 03 '16 at 14:28

2 Answers2

1

Solved the issue. Had to copy my whole project and then remove parts of the code until the issue disappeared.

Anyway. I basically had three components in my application. A navigation component, a table compontent, and a status bar compontent. It looked like this:

app sketch

The problem I had was that the width of the status bar and the width and height of the table component was increased whenever I took a snapshot of a row in the table.

Apparently, this was due to the padding of the status bar compontent. It had a right and left padding of 5 pixels, and once I removed the padding, the problem disappeared.

The added 10 pixels in width made the BorderPane that contained all of this expand with the same amount of pixels, and since the table width was bound to the BorderPane width, it increased by the same amount. What I still don't understand though, is why the Stage that contains the BorderPane doesn't adjust to the new width.

The component was properly padded before Scene.doCSSLayoutSyncForSnapshot(Node node) was invoked, so I don't understand why the extra width of ten pixels is added.

Anyhow: Removing the padding from the status bar component and instead padding the components inside the status bar fixed the issue. If someone has a good explanation for this, I'm all ears.

Here's a MCVE where you can reproduce the issue by dragging a row in the table:

import java.io.File;
import java.sql.SQLException;
import java.util.ArrayList;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Region;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class MCVE extends Application {

    private Stage primaryStage;
    private BorderPane rootLayout;
    private VBox detailsView;
    private StatusBar statusBar;

    public void start(Stage primaryStage) throws SQLException {

        this.primaryStage = primaryStage;
        this.primaryStage.setTitle("MCVE");

        initRootLayout();
        showStatusBar();
        showDetailsView();

        detailsView.prefWidthProperty().bind(rootLayout.widthProperty());
        detailsView.prefHeightProperty().bind(rootLayout.heightProperty());
    }

    @Override
    public void init() throws Exception {
        super.init();

    }

    public void initRootLayout() {
        rootLayout = new BorderPane();

        primaryStage.setWidth(1000);
        primaryStage.setHeight(600);

        Scene scene = new Scene(rootLayout);
        primaryStage.setScene(scene);
        primaryStage.show();
    }

    public void showStatusBar() {
        statusBar = new StatusBar();
        rootLayout.setBottom(statusBar);
    }

    public void showDetailsView() {
        detailsView = new VBox();
        rootLayout.setCenter(detailsView);

        setDetailsView(new Table(this));

        detailsView.prefHeightProperty().bind(primaryStage.heightProperty());
        detailsView.setMaxHeight(Region.USE_PREF_SIZE);
    }

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

    public VBox getDetailsView() {
        return detailsView;
    }

    public void setDetailsView(Node content) {
        detailsView.getChildren().add(0, content);
    }

    public StatusBar getStatusBar() {
        return statusBar;
    }

    class StatusBar extends HBox {
        public StatusBar() {
            setPadding(new Insets(0, 5, 0, 5));

            HBox leftBox = new HBox(10);

            getChildren().addAll(leftBox);

            /**
             * CONTROL SIZES
             */
            setPrefHeight(28);
            setMinHeight(28);
            setMaxHeight(28);

            // Leftbox takes all the space not occupied by the helpbox.
            leftBox.prefWidthProperty().bind(widthProperty());

            setStyle("-fx-border-color: black;");
        }
    }

    class Table extends TableView<ObservableList<String>> {

        private ObservableList<ObservableList<String>> data;

        public Table(MCVE app) {

            prefWidthProperty().bind(app.getDetailsView().widthProperty());
            prefHeightProperty()
                    .bind(app.getDetailsView().heightProperty());

            widthProperty().addListener((obs, oldValue, newValue) -> {
                System.out.println("Table width: " + newValue);
            });

            setRowFactory(r -> {
                TableRow<ObservableList<String>> row = new TableRow<ObservableList<String>>();

                row.setOnDragDetected(e -> {
                    Dragboard db = row.startDragAndDrop(TransferMode.ANY);
                    db.setDragView(row.snapshot(null, null));

                    ArrayList<File> files = new ArrayList<File>();

                    // We create a clipboard and put all of the files that
                    // was selected into the clipboard.
                    ClipboardContent filesToCopyClipboard = new ClipboardContent();
                    filesToCopyClipboard.putFiles(files);

                    db.setContent(filesToCopyClipboard);
                });

                row.setOnDragDone(e -> {
                    e.consume();
                });

                return row;
            });

            ObservableList<String> columnNames = FXCollections.observableArrayList("Col1", "col2", "Col3", "Col4");

            data = FXCollections.observableArrayList();

            for (int i = 0; i < columnNames.size(); i++) {
                final int colIndex = i;

                TableColumn<ObservableList<String>, String> column = new TableColumn<ObservableList<String>, String>(
                        columnNames.get(i));

                column.setCellValueFactory((param) -> new SimpleStringProperty(param.getValue().get(colIndex).toString()));

                    getColumns().add(column);
            }

            // Adds all of the data from the rows the data list.
            for (int i = 0; i < 100; i++) {
                // Each column from the row is a String in the list.
                ObservableList<String> row = FXCollections.observableArrayList();

                row.add("Column 1");
                row.add("Column 2");
                row.add("Column 3");
                row.add("Column 4");

                // Adds the row to data.
                data.add(row);
            }

            // Adds all of the rows in data to the table.
            setItems(data);
        }
    }
}
Jonatan Stenbacka
  • 1,824
  • 2
  • 23
  • 48
0

This answer talks about it a little bit Set scene width and height

but after diving into the source code I found that the resizing in snapshot is conditional on the scene never having a size set by one of its constructors.

You can only set a scene's size in its constructors and never again. That makes a little bit of sense, since its otherwise only used to size the window that contains it. It is unfortunate that the snapshot code is not smart enough to use the window's dimensions when set by the user in addition to the scene's possible user settings.

None of this prevents resizing later, so if you depend on taking snapshots, you may want to make a best practice out of using the Scene constructors which take a width and height and sending them something above 0

Jody Sowald
  • 342
  • 3
  • 18