0
votes

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:

  • Remove the TableView headers entirely.
  • Style the first and last columns / rows as headers / footers.
  • setCellSelectionEnabled to true
  • If the selected cell is a header cell, 'disable' the row or column; probably through a combination of CSS styling and removing the cell value from the summary.
  • If the selected cell is not a header cell, perform a different action.
  • If the header cell is clicked again, re-enable the row or column.

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.

1
Have you looked at the functions setRowFactory (from TableView) and setCellFactory (from TableColumn) ? There are many examples on the net, and it allows the programmer to customize the cell / column behaviors by settings listeners to it. From this, you can then perform actions if a column / cell is, for example, clicked. - Jacks
I have seen those. My question probably should have included references to them because, if there isn't a short-cut of some kind, then those two methods will probably be essential. - Cosmittus
As far as I know, there is no real "short-cut" for this. Just take an example of these functions and override the call() function to create cells or columns as you wish. You can also add listeners when setting the factories in order to trigger an action when your cell (or header, or even component inside the cell) has been clicked. Hope this helps, if not I can provide you a small example. - Jacks
Also, edit your question to post your JavaFX version, and what you tried / looked at - Jacks

1 Answers

1
votes

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.