TableView Future/Enhancements
Daniel Peintner
daniel.peintner at gmail.com
Tue Oct 13 12:19:31 UTC 2020
All,
I would like to ask some questions (and provide feedback) w.r.t. the
current TableView control in JavaFX. If this is not the *right* place to
ask such questions please let me know and I apologize.
The desire of a table (spreadsheet) control is to be editable and usable
also with the keyboard. Moreover if using it with a mouse a focus loss is
also mostly likely seen as a commit by a user (and not a revert as it is
now)
Some of the afore mentioned points are not (yet) realized and people seem
to be aware of the issue (see [1]). I am not sure if there is work planned
w.r.t. JDK-8089514 given that the work seems stalled...
Attached is an EditableTableCell application that supports some of the
desired features (but far not all). It uses some very dirty hacks to
achieve for example editing and walking through a table (I tend to say some
(all?) features should be built-in already).
With this email I would like to
1. Raise the question whether future work is planned in the area of
TableView
(e.g., TableView2 that used to be part of ControlsFX but I think belongs
to the core TableView)
2. Show some use cases people might have in mind when using a table
control.
The attached example allows one to
- walk through a table when in editing mode (up/down with cursor,
left/right with tabs)
e.g., double click in upper left corner and walk on with tab key
- (optionally) register a callable that adds a *new* row if end is
reached
- skip some columns that are not editable (like pulldown boxes)
Missing features
- better F2 edit support
- use the keyboard also with combo boxes or other cell controls
Please let me know whether there is interest in having such capabilities in
the built-in TableView control and how I can help. I am open for
suggestions.
(i.e. I think JDK-8089514 could be solved at least for TableView. Not sure
though about the other mentioned controls were I don't have the big picture)
Sorry again for the lengthy email and any feedback is welcome!
Thanks,
-- Daniel
[1] https://bugs.openjdk.java.net/browse/JDK-8089514
#############################
import javafx.application.Application;
import javafx.application.Platform;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.concurrent.Task;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.stage.Stage;
import javafx.util.Pair;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
import javafx.util.converter.IntegerStringConverter;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Callable;
public class EditableTableCellApplication extends Application {
public class EditableTableCell<S, T> extends TableCell<S, T> {
public final Object NOT_TRAVERSABLE = new Object();
// Text field for editing
// TODO: allow this to be a pluggable control
final TextField textField = new TextField();
// converter for converting the text in the text field to the user
type, and vice-versa
final StringConverter<T> converter;
// callable to create new row
final Callable<Void> callableOnRowEnd;
public EditableTableCell(StringConverter<T> converter) {
this(converter, false);
}
public EditableTableCell(StringConverter<T> converter, boolean
traverseEditableCell) {
this(converter, traverseEditableCell, null);
}
public EditableTableCell(StringConverter<T> converter, boolean
traverseEditableCell, Callable<Void> callableOnRowEnd) {
this.converter = converter;
this.callableOnRowEnd = callableOnRowEnd;
itemProperty().addListener((obx, oldItem, newItem) -> {
if (newItem == null) {
setText(null);
} else {
setText(converter.toString(newItem));
}
});
setGraphic(textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
textField.setOnAction(evt ->
commitEdit(this.converter.fromString(textField.getText())));
textField.focusedProperty().addListener((obs, wasFocused,
isNowFocused) -> {
if (!isNowFocused) {
commitEdit(this.converter.fromString(textField.getText()));
}
});
textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
textField.setText(converter.toString(getItem()));
cancelEdit();
event.consume();
} else if ((!event.isShiftDown() && event.getCode() ==
KeyCode.TAB)) {
event.consume();
getTableView().getSelectionModel().selectRightCell();
if (traverseEditableCell) {
Pair<TableColumn<S, ?>, Boolean> pairTcEnd =
getNextVisibleColumn(true);
if (pairTcEnd.getValue() != null &&
pairTcEnd.getValue()) {
// reached end -> new line
TableColumn<S, ?> tc0 =
getFirstTraversableColumn();
if (tc0 != null) {
//noinspection ConstantConditions
moveRowDown(tc0, event,
traverseEditableCell);
}
} else {
// move right in row
makeCellEditable(getTableRow().getIndex(),
getTableView(), pairTcEnd.getKey());
}
}
} else if ((event.isShiftDown() && event.getCode() ==
KeyCode.TAB)) {
if (traverseEditableCell) {
Pair<TableColumn<S, ?>, Boolean> pairTcEnd =
getNextVisibleColumn(false);
if (pairTcEnd.getValue() != null &&
pairTcEnd.getValue()) {
// reached end -> new line
TableColumn<S, ?> tcLast =
getLastTraversableColumn();
if (tcLast != null) {
//noinspection ConstantConditions
moveRowUp(tcLast, event,
traverseEditableCell);
}
} else {
// move left in row
makeCellEditable(getTableRow().getIndex(),
getTableView(), pairTcEnd.getKey());
}
}
} else if (event.getCode() == KeyCode.UP) {
moveRowUp(getTableColumn(), event,
traverseEditableCell);
} else if (event.getCode() == KeyCode.DOWN) {
moveRowDown(getTableColumn(), event,
traverseEditableCell);
}
});
}
// set the text of the text field and display the graphic
@Override
public void startEdit() {
if (!editableProperty().get()) {
return;
}
super.startEdit();
textField.setText(converter.toString(getItem()));
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
textField.requestFocus();
}
// revert to text display
@Override
public void cancelEdit() {
super.cancelEdit();
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
// commits the edit. Update property if possible and revert to text
display
@Override
public void commitEdit(T item) {
// This block is necessary to support commit on losing focus,
because the baked-in mechanism
// sets our editing state to false before we can intercept the
loss of focus.
// The default commitEdit(...) method simply fails if we are
not editing...
if (!isEditing() && item != null && !item.equals(getItem())) {
TableView<S> table = getTableView();
if (table != null) {
TableColumn<S, T> column = getTableColumn();
TableColumn.CellEditEvent<S, T> event = new
TableColumn.CellEditEvent<>(table,
new TablePosition<>(table, getIndex(), column),
TableColumn.editCommitEvent(), item);
Event.fireEvent(column, event);
}
}
super.commitEdit(item);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
private void makeCellEditable(int row2, TableView<S> tw,
TableColumn<S, ?> tc) {
if (tc != null) {
// HACK, JavaFX BUG!!!
Task<Void> task = new Task<Void>() {
@Override
protected Void call() throws Exception {
Thread.sleep(10);
Platform.runLater(() -> {
if (row2 >= 0 && row2 < tw.getItems().size()) {
tw.getSelectionModel().clearAndSelect(row2,
tc);
tw.edit(row2, tc);
}
});
return null;
}
};
Thread th = new Thread(task);
th.start();
}
}
private TableColumn<S, ?> getFirstTraversableColumn() {
for (TableColumn<S, ?> tc : getTableView().getColumns()) {
if (isTraversable(tc)) {
return tc;
}
}
return null;
}
private TableColumn<S, ?> getLastTraversableColumn() {
for (int i = getTableView().getColumns().size() - 1; i >= 0;
i--) {
TableColumn<S, ?> tc = getTableView().getColumns().get(i);
if (isTraversable(tc)) {
return tc;
}
}
return null;
}
private void moveRowDown(TableColumn<S, ?> tc, Event event, boolean
traverseEditableCell) {
int row2 = getTableRow().getIndex() + 1;
//noinspection StatementWithEmptyBody
if (row2 < getTableView().getItems().size()) {
// still ok --> normal select
} else {
// expand rows
if (callableOnRowEnd != null) {
try {
callableOnRowEnd.call();
} catch (Exception e) {
e.printStackTrace();
}
}
}
// select
getTableView().getSelectionModel().clearAndSelect(row2, tc);
commitEdit(converter.fromString(textField.getText())); //
needed (otherwise data lost)
event.consume();
if (traverseEditableCell) {
makeCellEditable(row2, getTableView(), tc);
}
}
private void moveRowUp(TableColumn<S, ?> tc, Event event, boolean
traverseEditableCell) {
int row2 = getTableRow().getIndex() - 1;
// select
getTableView().getSelectionModel().clearAndSelect(row2, tc);
commitEdit(converter.fromString(textField.getText())); //
needed (otherwise data lost)
event.consume();
if (traverseEditableCell) {
makeCellEditable(row2, getTableView(), tc);
}
}
private Pair<TableColumn<S, ?>, Boolean>
getNextVisibleColumn(boolean forward) {
List<TableColumn<S, ?>> columns = new ArrayList<>();
for (TableColumn<S, ?> column : getTableView().getColumns()) {
columns.addAll(getLeaves(column));
}
int nextIndex = columns.indexOf(getTableColumn());
TableColumn<S, ?> tc;
boolean reachedEdge = false;
do {
if (forward) {
nextIndex++;
if (nextIndex > columns.size() - 1) {
nextIndex = columns.size() - 1;
reachedEdge = true;
}
} else {
nextIndex--;
if (nextIndex < 0) {
nextIndex = 0;
reachedEdge = true;
}
}
tc = columns.get(nextIndex);
} while (!tc.isVisible() && !reachedEdge);
return new Pair<>(tc, reachedEdge);
}
private List<TableColumn<S, ?>> getLeaves(
TableColumn<S, ?> root) {
List<TableColumn<S, ?>> columns = new ArrayList<>();
if (root.getColumns().isEmpty()) {
// We only want the leaves that are editable / traversable
if (isTraversable(root)) {
columns.add(root);
}
} else {
for (TableColumn<S, ?> column : root.getColumns()) {
columns.addAll(getLeaves(column));
}
}
return columns;
}
private boolean isTraversable(TableColumn<S, ?> tc) {
if (tc != null) {
return (tc.isEditable() && tc.getUserData() !=
NOT_TRAVERSABLE);
}
return false;
}
}
public class Person {
final StringProperty firstName;
final StringProperty lastName;
final ObjectProperty<Integer> age;
public Person() {
this.firstName = new SimpleStringProperty();
this.lastName = new SimpleStringProperty();
this.age = new SimpleObjectProperty<>();
}
public Person(String firstName, String lastName, int age) {
this();
this.firstName.set(firstName);
this.lastName.set(lastName);
this.age.set(age);
}
public StringProperty firstNameProperty() {
return this.firstName;
}
public StringProperty lastNameProperty() {
return this.lastName;
}
public ObjectProperty<Integer> ageProperty() {
return this.age;
}
}
class CallableAdd implements Callable<Void> {
@Override
public Void call() {
detailData.add(new Person());
return null;
}
}
// The table's data
ObservableList<Person> detailData = FXCollections.observableArrayList();
@Override
public void start(Stage primaryStage) throws IOException {
TableView<Person> tableView = new TableView<>();
tableView.setEditable(true);
// without the following cell selection setting SHIFT+TAB does not
work
tableView.getSelectionModel().setCellSelectionEnabled(true);
boolean traversable = true;
CallableAdd callableAdd = new CallableAdd();
// remove empty row highlighting to better illustrate that *new*
rows are added by tabbing through
File f = File.createTempFile("style", ".css");
Files.write(Paths.get(f.getPath()), ".table-row-cell:empty
{-fx-background-color: white;} .table-row-cell:empty .table-cell
{-fx-border-width: 0px;}".getBytes());
tableView.getStylesheets().add("file:///" +
f.getAbsolutePath().replace("\\", "/"));
TableColumn<Person, String> columnFirstName = new
TableColumn<>("First Name");
columnFirstName.setCellValueFactory(param ->
param.getValue().firstNameProperty());
columnFirstName.setCellFactory(column -> new
EditableTableCell<>(new DefaultStringConverter(), traversable,
callableAdd));
columnFirstName.setEditable(true);
TableColumn<Person, String> columnLastName = new
TableColumn<>("Last Name");
columnLastName.setCellValueFactory(param ->
param.getValue().lastNameProperty());
columnLastName.setCellFactory(column -> new EditableTableCell<>(new
DefaultStringConverter(), traversable, callableAdd));
columnLastName.setEditable(true);
TableColumn<Person, Integer> columnAge = new TableColumn<>("Age");
columnAge.setCellValueFactory(param ->
param.getValue().ageProperty());
columnAge.setCellFactory(column -> new EditableTableCell<>(new
IntegerStringConverter(), traversable, callableAdd));
columnAge.setEditable(true);
tableView.getColumns().addAll(columnFirstName, columnLastName,
columnAge);
SortedList<Person> sortedData = new SortedList<>(detailData);
tableView.setItems(sortedData);
detailData.add(new Person("John", "Doe", 25));
detailData.add(new Person("Jane", "Deer", 30));
// create and show scene
Scene scene = new Scene(tableView, 600, 400);
primaryStage.setTitle("Editable TableCell Application");
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
More information about the openjfx-dev
mailing list