JavaFx - String and FlowPane (row?) within tableView?
Asked Answered
J

1

5

I'm currently trying to implement the following:

  • A TableView with an ObservableList as dataset, with two columns, each of which contains Strings (names of the players). This part is easy enough.

  • Once a Player(name) is clicked, a custom FlowPane should be injected below the selected player. If another player is clicked, the flowpane should disappear and be injected below the currently clicked player.

The below code implements the TableView (minus the mouse listener part). Please help me let the FlowPane span the entire row. I'm guessing I need a RowFactory but have no clue how to make it work for my purposes :)

Also, apparently both my columns now show the same data. Confusing :) Is there a way to tell one column to use half the data set and the other column the other half? I obviously don't want my data shown twice.

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

    @Override
    public void start(Stage stage) throws Exception
    {
        try
        {

            FlowPane f = new FlowPane();
            Scene scene = new Scene(f, 300, 200);

            Player p1 = new Player("player 1 ");
            Player p2 = new Player("player 2 ");
            Player p3 = new Player("player 3 ");

            ArrayList<Object> players = new ArrayList<>();
            players.add(p1);
            players.add(p2);
            players.add(p3);

            ObservableList<Object> observableList = FXCollections.observableArrayList(players);
            TableView<Object> table = createTableView(observableList, 300, 200);

            f.getChildren().add(table);
            injectFlowPane(table);

            stage.setScene(scene);
            stage.show();
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }

    }

    public TableView<Object> createTableView(ObservableList<Object> items, double width, double height)
    {
        TableView<Object> table = new TableView<>();

        table.setItems(items);

        table.getColumns().add(createTableColumn(width / 2));
        table.getColumns().add(createTableColumn(width / 2));


        table.setMinSize(width, height);
        table.setPrefSize(width, height);
        table.setMaxSize(width, height);
        return table;
    }

    private TableColumn<Object, Object> createTableColumn(double width)
    {
        TableColumn<Object, Object> tableColumn = new TableColumn<>();
        tableColumn.setCellFactory(
                new Callback<TableColumn<Object, Object>, TableCell<Object, Object>>() {
                    @Override
                    public TableCell<Object, Object> call(TableColumn<Object, Object> arg0)
                    {
                        return new PlayerCell();
                    }
                });

        tableColumn.setCellValueFactory(cellDataFeatures -> {

            Object item = cellDataFeatures.getValue();
            return new SimpleObjectProperty<>(item);

        });

        tableColumn.setMinWidth(width);

        return tableColumn;
    }

    private void injectFlowPane(TableView<Object> table)
    {
        FlowPane f = new FlowPane();
        f.setMinSize(50, 50);
        f.setBackground(new Background(new BackgroundFill(Color.DARKGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
        table.getItems().add(1, f);
    }

}

public class PlayerCell extends TableCell<Object, Object>
{
    @Override
    protected void updateItem(Object item, boolean empty)
    {
        super.updateItem(item, false);

        //      if (empty)

        if (item != null)
        {

            if (item instanceof Player)
            {
                setText(((Player) item).getName());
                setGraphic(null);
            }
            else if (item instanceof FlowPane)
            {
                setGraphic((FlowPane) item);
            }
            else
            {
                setText("N/A");
                setGraphic(null);
            }
        }
        else
        {
            setText(null);
            setGraphic(null);
        }

    }
}


public class Player
{
    private String name;

    public Player(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return name;
    }

}

EDIT:

I have now implemented James_D's ExpandingTableRow, which works neatly as far as showing the FlowPane below the selected TableRow is concerned. I have also managed to change my datastructures so that each column now shows different players instead of the same ones in each column.

However, the FlowPane that is created should actually depend on the actual player(cell) that is clicked within the row. In James' example: a different FlowPane would be created if the FirstName or LastName was selected (even for the same row). The FlowPane should be shown the same way - below the selected row - but it's a different, new FlowPane depending on if FirstName was clicked, or if LastName was clicked. How can I manage to do this?

I've looked at using:

table.getSelectionModel().setCellSelectionEnabled(true);

But this actually seems to disable James_d's solution.

Jerz answered 7/3, 2018 at 17:51 Comment(6)
Start here How to create a Minimal, Complete, and Verifiable example. No one wants to write a ton of code.Passed
Do you want the FlowPane to only be 'below' the PlayerCell or across the whole row ?Octavie
It indeed needs to be across the whole row :)Jerz
My initial (now deleted) comment "create a table row subclass" was essentially correct, but needed a little more work than I had envisioned. The layout of the table row is managed by its skin, so you also need to subclass the skin (which is public API in Java 9). Proof-of-concept level solution in answer.Abominable
Also: not really clear what you want to display in the columns. Your Player class only has one value to display, so why are there two columns?Abominable
I can see how that would be confusing. The TableView is going to take up +- 1/3rd of the stage - which depends on screen resolution - since that's what the injectable flowpane needs. With a single playername column that leaves a lot empty space :-) Therefore I figured 2 columns, both of which would use playernames. The data is already sorted on playername and would need to be distributed row-wise, not column-wise. No clue at this point how I'll manage that, one thing at a time I guess :)Jerz
A
6

This solution works only in Java 9 and later.

The display of a row is managed by a TableRow, and the actual layout of that row is performed by its skin (a TableRowSkin). So to manage this, you need a subclass of TableRow that installs a custom skin.

The row implementation is pretty straightforward: in this example I added a property for the "additional content" to be displayed when the row is selected. It also overrides the createDefaultSkin() method to specify a custom skin implementation.

import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TableRow;

public class ExpandingTableRow<T> extends TableRow<T> {

    private final ObjectProperty<Node> selectedRowContent = new SimpleObjectProperty<>();

    public final ObjectProperty<Node> selectedRowContentProperty() {
        return this.selectedRowContent;
    }


    public final Node getSelectedRowContent() {
        return this.selectedRowContentProperty().get();
    }


    public final void setSelectedRowContent(final Node selectedRowContent) {
        this.selectedRowContentProperty().set(selectedRowContent);
    }

    public ExpandingTableRow(Node selectedRowContent) {
        super();
        setSelectedRowContent(selectedRowContent);
    }

    public ExpandingTableRow() {
        this(null);
    }

    @Override
    protected Skin<?> createDefaultSkin() {
        return new ExpandingTableRowSkin<T>(this);
    }

}

The skin implementation has to do the layout work. It needs to override the methods that compute the height, accounting for the height of the extra content if needed, and it needs to override the layoutChildren() method, to position the additional content, if needed. Finally, it must manage the additional content, adding or removing the additional content if the selected state of the row changes (or if the additional content itself is changed).

import javafx.scene.control.skin.TableRowSkin;

public class ExpandingTableRowSkin<T> extends TableRowSkin<T> {

    private ExpandingTableRow<T> row;

    public ExpandingTableRowSkin(ExpandingTableRow<T> row) {
        super(row);
        this.row = row;

        row.selectedRowContentProperty().addListener((obs, oldContent, newContent) -> {
            if (oldContent != null) {
                getChildren().remove(oldContent);
            }
            if (newContent != null && row.isSelected()) {
                getChildren().add(newContent);
            }
            if (row.getTableView() != null) {
                row.getTableView().requestLayout();
            }
        });
        row.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
            if (isNowSelected && row.getSelectedRowContent() != null
                    && !getChildren().contains(row.getSelectedRowContent())) {
                getChildren().add(row.getSelectedRowContent());
            } else {
                getChildren().remove(row.getSelectedRowContent());
            }
        });
    }

    @Override
    protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset,
            double leftInset) {
        if (row.isSelected() && row.getSelectedRowContent() != null) {
            return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset)
                    + row.getSelectedRowContent().maxHeight(width);
        }
        return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
    }

    @Override
    protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
            double leftInset) {
        if (row.isSelected() && row.getSelectedRowContent() != null) {
            return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset)
                    + row.getSelectedRowContent().minHeight(width);
        }
        return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
    }

    @Override
    protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
            double leftInset) {
        if (row.isSelected() && row.getSelectedRowContent() != null) {
            return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset)
                    + row.getSelectedRowContent().prefHeight(width);
        }
        return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
    }

    @Override
    protected void layoutChildren(double x, double y, double w, double h) {
        if (row.isSelected()) {
            double rowHeight = super.computePrefHeight(w, snappedTopInset(), snappedRightInset(), snappedBottomInset(),
                    snappedLeftInset());
            super.layoutChildren(x, y, w, rowHeight);
            row.getSelectedRowContent().resizeRelocate(x, y + rowHeight, w, h - rowHeight);
        } else {
            super.layoutChildren(x, y, w, h);
        }
    }

}

Finally, a test (using the usual example from Oracle, or a version of it):

import java.util.function.Function;

import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;

public class ExpandingTableRowTest extends Application {

    @Override
    public void start(Stage primaryStage) {
        TableView<Person> table = new TableView<>();
        table.getColumns().add(column("First Name", Person::firstNameProperty));
        table.getColumns().add(column("Last Name", Person::lastNameProperty));

        table.setRowFactory(tv -> {
            Label label = new Label();
            FlowPane flowPane = new FlowPane(label);
            TableRow<Person> row = new ExpandingTableRow<>(flowPane) {
                @Override
                protected void updateItem(Person person, boolean empty) {
                    super.updateItem(person, empty);
                    if (empty) {
                        label.setText(null);
                    } else {
                        label.setText(String.format("Some additional information about %s %s here",
                                person.getFirstName(), person.getLastName()));
                    }
                }
            };
            return row;
        });

        table.getItems().addAll(
                new Person("Jacob", "Smith"), 
                new Person("Isabella", "Johnson"),
                new Person("Ethan", "Williams"), 
                new Person("Emma", "Jones"),
                new Person("Michael", "Brown")
        );

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

    private static <S, T> TableColumn<S, T> column(String title, Function<S, ObservableValue<T>> property) {
        TableColumn<S, T> col = new TableColumn<>(title);
        col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
        return col;
    }

    public static class Person {
        private final StringProperty firstName = new SimpleStringProperty();
        private final StringProperty lastName = new SimpleStringProperty();

        public Person(String firstName, String lastName) {
            setFirstName(firstName);
            setLastName(lastName);
        }

        public final StringProperty firstNameProperty() {
            return this.firstName;
        }

        public final String getFirstName() {
            return this.firstNameProperty().get();
        }

        public final void setFirstName(final String firstName) {
            this.firstNameProperty().set(firstName);
        }

        public final StringProperty lastNameProperty() {
            return this.lastName;
        }

        public final String getLastName() {
            return this.lastNameProperty().get();
        }

        public final void setLastName(final String lastName) {
            this.lastNameProperty().set(lastName);
        }

    }

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

As you can see, a little refinement of the style and sizing may be needed to get this production-ready, but this shows the approach that will work.

enter image description here

Abominable answered 8/3, 2018 at 14:25 Comment(3)
Really nice solution - this will help me out when we move up to Java 9.Stedmann
I'm trying not to drool. Will see if I can manage to implement (parts of) this - luckily I'm already on jdk 9.Jerz
@Jerz This particular example plays slightly better with an HBox than a FlowPane: if you call setWrapText(true) on the label and wrap it in an HBox instead, it will respect the width of the table. (I don't use FlowPanes much; I assume this is because of how FlowPane manages its layout.) Simply setting the background color (e.g. via CSS) on the content (FlowPane, or whatever) will remove the cell borders.Abominable

© 2022 - 2024 — McMap. All rights reserved.