Row Selection Mode
Despite my comment, it doesn't look like you need to enable cell selection for this. Taking inspiration from the implementation of CheckBoxTableCell
, your custom TableCell
should take some form of callback to get the model property; it can also require a StringConverter
, allowing you to use the TableCell
with more than just String
s. Here's an example:
import java.util.Objects;
import java.util.function.IntFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView.TableViewFocusModel;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
public class CustomTableCell<S, T> extends TableCell<S, T> {
public static <S> Callback<TableColumn<S, String>, TableCell<S, String>> forTableColumn(
IntFunction<Property<String>> extractor) {
return forTableColumn(extractor, new DefaultStringConverter());
}
public static <S, T> Callback<TableColumn<S, T>, TableCell<S, T>> forTableColumn(
IntFunction<Property<T>> extractor, StringConverter<T> converter) {
Objects.requireNonNull(extractor);
Objects.requireNonNull(converter);
return column -> new CustomTableCell<>(extractor, converter);
}
private final ObjectProperty<IntFunction<Property<T>>> extractor = new SimpleObjectProperty<>(this, "extractor");
public final void setExtractor(IntFunction<Property<T>> callback) { extractor.set(callback); }
public final IntFunction<Property<T>> getExtractor() { return extractor.get(); }
public final ObjectProperty<IntFunction<Property<T>>> extractorProperty() { return extractor; }
private final ObjectProperty<StringConverter<T>> converter = new SimpleObjectProperty<>(this, "converter");
public final void setConverter(StringConverter<T> converter) { this.converter.set(converter); }
public final StringConverter<T> getConverter() { return converter.get(); }
public final ObjectProperty<StringConverter<T>> converterProperty() { return converter; }
private Property<T> property;
private TextField textField;
public CustomTableCell(IntFunction<Property<T>> extractor, StringConverter<T> converter) {
setExtractor(extractor);
setConverter(converter);
// Assumes this TableCell will never become part of a different TableView
// after the first one. Also assumes the focus model of the TableView will
// never change. These are not great assumptions (especially the latter),
// but this is only an example.
tableViewProperty().addListener((obs, oldTable, newTable) ->
newTable.getFocusModel().focusedCellProperty().addListener((obs2, oldPos, newPos) -> {
if (getIndex() == newPos.getRow() && getTableColumn() == newPos.getTableColumn()) {
textField.requestFocus();
}
})
);
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
cleanUpProperty();
} else {
initializeTextField();
cleanUpProperty();
property = getExtractor().apply(getIndex());
Bindings.bindBidirectional(textField.textProperty(), property, getConverter());
setGraphic(textField);
if (getTableView().getFocusModel().isFocused(getIndex(), getTableColumn())) {
textField.requestFocus();
}
}
}
private void cleanUpProperty() {
if (property != null) {
Bindings.unbindBidirectional(textField.textProperty(), property);
property = null;
}
}
private void initializeTextField() {
if (textField == null) {
textField = new TextField();
textField.addEventFilter(KeyEvent.KEY_PRESSED, this::processArrowKeys);
textField.focusedProperty().addListener((observable, wasFocused, isFocused) -> {
if (isFocused) {
getTableView().getFocusModel().focus(getIndex(), getTableColumn());
}
});
}
}
private void processArrowKeys(KeyEvent event) {
if (event.getCode().isArrowKey()) {
event.consume();
TableViewFocusModel<S> model = getTableView().getFocusModel();
switch (event.getCode()) {
case UP:
model.focusAboveCell();
break;
case RIGHT:
model.focusRightCell();
break;
case DOWN:
model.focusBelowCell();
break;
case LEFT:
model.focusLeftCell();
break;
default:
throw new AssertionError(event.getCode().name());
}
getTableView().scrollTo(model.getFocusedCell().getRow());
getTableView().scrollToColumnIndex(model.getFocusedCell().getColumn());
}
}
}
The example is not exhaustive and makes assumptions that are not guaranteed, but it's only an example and so I leave any tweaks up to you. One such improvement might be to include a TextFormatter
somehow. That said, I believe it provides the basic functionality you're looking for.
To use this cell, you would only set the cellFactory
of each TableColumn
. It is not necessary to set the cellValueFactory
and doing so might actually be detrimental, depending on how updateItem
gets called. Basically, it'd look something like:
TableView<YourModel> table = ...;
TableColumn<YourModel, String> column = new TableColumn<>("Column");
column.setCellFactory(CustomTableCell.forTableColumn(i -> table.getItems().get(i).someProperty()));
table.getColumns().add(column);
Cell Selection Mode
This behavior you're attempting to implement seems inherently cell based, however, and as such it's probably better to enable cell selection. This allows the custom TableCell
to base it's behavior on selection, rather than focus, and leaves the arrow key handling to the TableView
. Here's a slightly modified version of the above example:
import java.util.Objects;
import java.util.function.IntFunction;
import javafx.beans.binding.Bindings;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleObjectProperty;
import javafx.event.EventDispatcher;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyEvent;
import javafx.util.Callback;
import javafx.util.StringConverter;
import javafx.util.converter.DefaultStringConverter;
public class CustomTableCell<S, T> extends TableCell<S, T> {
/*
* -- CODE OMITTED --
*
* The factory methods (forTableColumn) and properties (extractor
* and converter) have been omitted for brevity. They are defined
* and used exactly the same way as in the previous example.
*/
private Property<T> property;
private TextField textField;
public CustomTableCell(IntFunction<Property<T>> extractor, StringConverter<T> converter) {
setExtractor(extractor);
setConverter(converter);
}
@Override
public void updateSelected(boolean selected) {
super.updateSelected(selected);
if (selected && !isEmpty()) {
textField.requestFocus();
}
}
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
clearProperty();
} else {
initializeTextField();
clearProperty();
property = getExtractor().apply(getIndex());
Bindings.bindBidirectional(textField.textProperty(), property, getConverter());
setGraphic(textField);
if (isSelected()) {
textField.requestFocus();
}
}
}
private void clearProperty() {
if (property != null) {
Bindings.unbindBidirectional(textField.textProperty(), property);
textField.setText(null);
property = null;
}
}
private void initializeTextField() {
if (textField == null) {
textField = new TextField();
textField.focusedProperty().addListener((observable, wasFocused, isFocused) -> {
if (isFocused && !isSelected()) {
getTableView().getSelectionModel().clearAndSelect(getIndex(), getTableColumn());
}
});
/*
* TableView has key handlers that will select cells based on arrow keys being
* pressed, scrolling to them if necessary. I find this mechanism looks cleaner
* because, unlike TableView#scrollTo, it doesn't cause the cell to jump to the
* top of the TableView.
*
* The way this works is by bypassing the TextField if, and only if, the event
* is a KEY_PRESSED event and the pressed key is an arrow key. This lets the
* event bubble up back to the TableView and let it do what it needs to. All
* other key events are given to the TextField for normal processing.
*
* NOTE: The behavior being relied upon here is added by the default TableViewSkin
* and its corresponding TableViewBehavior. This may not work if a custom
* TableViewSkin skin is used.
*/
EventDispatcher oldDispatcher = textField.getEventDispatcher();
textField.setEventDispatcher((event, tail) -> {
if (event.getEventType() == KeyEvent.KEY_PRESSED
&& ((KeyEvent) event).getCode().isArrowKey()) {
return event;
} else {
return oldDispa