Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

JavaFx - String and FlowPane (row?) within tableView?

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.

like image 803
Simon Avatar asked Mar 07 '18 17:03

Simon


1 Answers

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

like image 197
James_D Avatar answered Oct 22 '22 17:10

James_D