Reputation: 637
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
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