Cosmittus
Cosmittus

Reputation: 637

JavaFX: 'disabling' TableView rows and columns

I have a TableView and wish to allow the user to disable individual rows and columns.

The table data are provided by an API so the exact number of rows and columns is not known until run-time, though typically it will be 4-30 rows and 15-25 columns. Ideally the first column / row would be a header and the last would be a footer (i.e.: a summary of the data above / to the side of it). Once the user has performed some other action, they should be able to disable a row or column - preferably by clicking the header - and so remove those data from the summary.

The row / column should still be visible. Sorting is not required. Editing is not required.

I am aware that JavaFX only really supports a column header, not a row header, and certainly not footers for either. This leads me to think that the best option would be to:

Is this the best method? Are there any third-party libraries which might make the job easier? It it even worth dumping the TableView altogether and using a GridPane?

Thank you.

Upvotes: 0

Views: 4393

Answers (1)

Cosmittus
Cosmittus

Reputation: 637

As @Jacks has pointed out, there doesn't seem to be a short-cut for this. So here is how I've solved the problem.

First, we hide the table header and allow cells to be selected individually. The updateTable() method is called every n seconds; it simulates a ScheduledService API call. The MyRow object is just a wrapper for an ObservableList<TableCell>, and we use that because we don't know how many columns there will be until run-time.

COLUMN_HEADER, ROW_FOOTER, etc. are enums.

private void updateTable() {
    Pane header = (Pane) myTable.lookup("TableHeaderRow");
    header.setVisible(false);
    myTable.getSelectionModel().setCellSelectionEnabled(true);
    myTable.setLayoutY(-header.getHeight());
    myTable.autosize();

    List<MyRow> list = new CopyOnWriteArrayList();
    Integer maxRows = 5, maxColumns = 5; // For example
    MyRow row;
    for (int rowId = 0; rowId <= maxRows; rowId++) {
        Boolean isEmpty = myTable.getColumns().isEmpty();
        row = new MyRow();
        Integer total = 0;
        for (int colId = 0; colId <= maxColumns; colId++) {
            if (isEmpty) {
                myTable.getColumns().add(createReactiveColumn());
            }
            TableCell cell = new MyTableCell();
            if (rowId == 0 && colId == 0) {
                // Top-left cell
            } else if (rowId == 0) {
                // Column title
                cell.setUserData(COLUMN_HEADER);
                cell.setText("CH" + Integer.toString(colId));
            } else if (colId == 0) {
                // Row title
                cell.setUserData(ROW_HEADER);
                cell.setText("RH" + Integer.toString(rowId));
            } else if (colId == maxColumns) {
                // Row 'footer'
                cell.setUserData(ROW_FOOTER);
                cell.setText(Integer.toString(total));
            } else if (rowId == maxRows) {
                // Column 'footer'
                cell.setUserData(COLUMN_FOOTER);
                cell.setText("CF" + Integer.toString(rowId));
            } else {
                // Data
                Integer r = new Random().nextInt(9); // Simulate API data
                if (!this.disabledColumns.contains(myTable.getColumns().get(colId))) {
                    total += r; // Sum of each enabled cell
                }
                cell.setUserData(DATA);
                cell.setText(Integer.toString(r));
            }
            row.add(cell);
        }
        list.add(row);
    }
    myTable.getItems().setAll(list);
}

The TableColumn just employs the setCellFactory() method:

private TableColumn<MyRow, String> createReactiveColumn() {

    TableColumn<MyRow, String> column = new TableColumn<MyRow, String>();
    column.setCellFactory(new Callback<TableColumn<MyRow, String>, TableCell<MyRow, String>>() {
        @Override
        public TableCell<MyRow, String> call(TableColumn<MyRow, String> param) {
            return new MyTableCell();
        }
    });
    column.setSortable(FALSE);
    column.setMinWidth(40d);

    return column;
}

MyTableCell adds a MOUSE_CLICKED Event Handler, as well as copying the text from the updated TableCell.

private class MyTableCell extends TableCell<MyRow, String> {

    Boolean hasEventHandler = FALSE;

    @Override
    protected void updateItem(String item, boolean empty) {
        super.updateItem(item, empty);
        if (!empty && getTableRow() != null && getTableRow().getItem() != null) {
            MyRow er = (MyRow) getTableRow().getItem();
            TableCell cell = er.get(getTableView().getColumns().indexOf(getTableColumn()));
            this.setText(cell.getText());
            if (cell.getUserData() instanceof MyTableCellEnum) {
                MyTableCellEnumcellType = (MyTableCellEnum) cell.getUserData();
                if (null != cellType && !hasEventHandler) {
                    switch (cellType) {
                        case COLUMN_HEADER:
                            addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent event) {
                                    toggleColumn(getTableColumn());
                                }
                            });
                            hasEventHandler = TRUE;
                            break;
                        case ROW_HEADER:
                            addEventHandler(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent event) {
                                    toggleRow(getTableRow());
                                }
                            });
                            hasEventHandler = TRUE;
                            break;
                        case DATA:
                            addEventFilter(MouseEvent.MOUSE_CLICKED, new EventHandler<MouseEvent>() {
                                @Override
                                public void handle(MouseEvent event) {
                                    // Do other action on selection
                                }
                            });
                            break;
                        default:
                            break;
                    }
                }
            }
        }
    }
}

Finally, the toggle methods just swap the row / column between a Set of disabled TableRow / TableColumn objects and update the CSS:

private void toggleRow(TableRow tableRow) {
    if (this.disabledRows.contains(tableRow)) {
        this.disabledRows.remove(tableRow);
        tableRow.getStyleClass().remove("cell-disabled");
    } else {
        this.disabledRows.add(tableRow);
        tableRow.getStyleClass().add("cell-disabled");
    }
}

private void toggleColumn(TableColumn tableColumn) {
    System.out.println(tableColumn);
    if (this.disabledColumns.contains(tableColumn)) {
        this.disabledColumns.remove(tableColumn);
        tableColumn.getStyleClass().remove("cell-disabled");
    } else {
        this.disabledColumns.add(tableColumn);
        tableColumn.getStyleClass().add("cell-disabled");
    }
}

It works and I'm mostly happy with it. The inclusion of the hasEventHandler Boolean is not ideal: it seems a bit hacky but I couldn't find any other way of registering an Event Handler only once while ensuring that it actually works.

Comments and suggestions for improvement are welcome. I'll leave it a few days before accepting my own answer in case of a better idea.

Upvotes: 1

Related Questions