Problem
I'd like to switch to edit mode in my TableView as soon as I type. I don't want to doubleclick or press to enter on each and every cell first, that's annoying.
I've come up with the following piece of code. Problem is that it is more or less side-effect programming and I suspect troubles. When you use KEY_RELEASED in order to switch the table into edit mode, the 1st key press gets lost.
So you have to use KEY_PRESSED. It all seems to work fine now, but once in a while you get a race condition and the caret in the TextField cell editor is before the typed text instead of after it. But when you continue typing, then the text gets appended correctly after the existing text.
It appears okay, but from a developing point of view it seems like a mess with race conditions.
Question
Does anyone have a proper way of doing a "type-to-edit" functionality?
Code
Here's the code I've got so far:
public class InlineEditingTableView extends Application {
private final ObservableList<Data> data =
FXCollections.observableArrayList(
new Data(1.,5.),
new Data(2.,6.),
new Data(3.,7.),
new Data(4.,8.)
);
private TableView<Data> table;
#Override
public void start(Stage stage) {
// create edtiable table
table = new TableView<Data>();
table.setEditable(true);
// column 1 contains numbers
TableColumn<Data, Number> number1Col = new TableColumn<>("Number 1");
number1Col.setMinWidth(100);
number1Col.setCellValueFactory( cellData -> cellData.getValue().number1Property());
number1Col.setCellFactory( createNumberCellFactory());
number1Col.setOnEditCommit(new EventHandler<CellEditEvent<Data, Number>>() {
#Override
public void handle(CellEditEvent<Data, Number> t) {
System.out.println( t);
// ((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setFirstName(t.getNewValue());
}
});
// column 2 contains numbers
TableColumn<Data, Number> number2Col = new TableColumn<>("Number 2");
number2Col.setMinWidth(100);
number2Col.setCellValueFactory( cellData -> cellData.getValue().number2Property());
number2Col.setCellFactory( createNumberCellFactory());
// add columns & data to table
table.setItems(data);
table.getColumns().addAll( number1Col, number2Col);
// switch to edit mode on keypress
// this must be KeyEvent.KEY_PRESSED so that the key gets forwarded to the editing cell; it wouldn't be forwarded on KEY_RELEASED
table.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode() == KeyCode.ENTER) {
// event.consume(); // don't consume the event or else the values won't be updated;
return;
}
// switch to edit mode on keypress, but only if we aren't already in edit mode
if( table.getEditingCell() == null) {
if( event.getCode().isLetterKey() || event.getCode().isDigitKey()) {
TablePosition focusedCellPosition = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPosition.getRow(), focusedCellPosition.getTableColumn());
}
}
}
});
table.addEventFilter(KeyEvent.KEY_RELEASED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent event) {
if( event.getCode() == KeyCode.ENTER) {
table.getSelectionModel().selectBelowCell();
}
}
});
// single cell selection mode
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().selectFirst();
// add nodes to stage
BorderPane root = new BorderPane();
root.setCenter(table);
Scene scene = new Scene( root, 800,600);
stage.setScene(scene);
stage.show();
}
/**
* Number cell factory which converts strings to numbers and vice versa.
* #return
*/
private Callback<TableColumn<Data, Number>, TableCell<Data, Number>> createNumberCellFactory() {
Callback<TableColumn<Data, Number>, TableCell<Data, Number>> factory = TextFieldTableCell.forTableColumn( new StringConverter<Number>() {
#Override
public Number fromString(String string) {
return Double.parseDouble(string);
}
#Override
public String toString(Number object) {
return object.toString();
}
});
return factory;
}
/**
* Table data container
*/
public static class Data {
private final SimpleDoubleProperty number1;
private final SimpleDoubleProperty number2;
private Data( Double number1, Double number2) {
this.number1 = new SimpleDoubleProperty(number1);
this.number2 = new SimpleDoubleProperty(number2);
}
public final DoubleProperty number1Property() {
return this.number1;
}
public final double getNumber1() {
return this.number1Property().get();
}
public final void setNumber1(final double number1) {
this.number1Property().set(number1);
}
public final DoubleProperty number2Property() {
return this.number2;
}
public final double getNumber2() {
return this.number2Property().get();
}
public final void setNumber2(final double number2) {
this.number2Property().set(number2);
}
}
public static void main(String[] args) {
launch(args);
}
}
To edit immediately on clicking a cell, it makes more sense to me to have the TextFields permanently displayed in the table, instead of transitioning to a special "edit mode" and switch from a Label to a TextField. (I would think of this as having all cells always in "edit mode", which I think makes sense with the behavior you want.)
If that kind of UI works for your requirements, you can just render text fields in the cell and bind bidirectionally the text field's textProperty to the appropriate property in your model. The tricky part here is getting hold of that property: you have to go from the cell to the table row, then to the item for the table row, and then to the property you need. At any time, one of those may change (possibly to null), so you have to deal with those possibilities.
Give the usual example:
public class Person {
// ...
public StringProperty firstNameProperty() { ... }
// etc...
}
You can do
TableView<Person> table = new TableView<>();
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(col -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
ChangeListener<Person> rowItemListener = (obs, oldPerson, newPerson) -> {
if (oldPerson != null) {
textField.textProperty().unbindBidirectional(((Person) oldPerson).firstNameProperty());
}
if (newPerson != null) {
textField.textProperty().bindBidirectional(((Person) newPerson).firstNameProperty());
}
};
cell.tableRowProperty().addListener((obs, oldRow, newRow) -> {
if (oldRow != null) {
oldRow.itemProperty().removeListener(rowItemListener);
if (oldRow.getItem() != null) {
textField.textProperty().unbindBidirectional(((Person) oldRow.getItem()).firstNameProperty());
}
}
if (newRow != null) {
newRow.itemProperty().addListener(rowItemListener);
if (newRow.getItem() != null) {
textField.textProperty().bindBidirectional(((Person) newRow.getItem()).firstNameProperty());
}
}
});
return cell ;
});
You can greatly reduce the code complexity here by using the EasyBind framework, which provides (among other things) ways to get "properties of properties" with appropriate handling for null:
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
firstNameCol.setCellValueFactory(cellData -> cellData.getValue().firstNameProperty());
firstNameCol.setCellFactory(col -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
textField.textProperty().bindBidirectional(
EasyBind.monadic(cell.tableRowProperty())
.selectProperty(TableRow::itemProperty)
.selectProperty(p -> ((Person)p).firstNameProperty()));
return cell ;
});
Here is a complete example, where I factored the cell factory code above into a more general method:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import org.fxmisc.easybind.EasyBind;
public class LiveTableViewCell extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getItems().addAll(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
table.getColumns().addAll(
createColumn("First Name", Person::firstNameProperty),
createColumn("Last Name", Person::lastNameProperty),
createColumn("Email", Person::emailProperty)
);
Button button = new Button("Debug");
button.setOnAction(e -> table.getItems().stream().map(p -> String.format("%s %s %s", p.getFirstName(), p.getLastName(), p.getEmail())).forEach(System.out::println));
primaryStage.setScene(new Scene(new BorderPane(table, null, null, button, null), 600, 120));
primaryStage.show();
}
private TableColumn<Person, String> createColumn(String title, Function<Person, Property<String>> property) {
TableColumn<Person, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> {
TableCell<Person, String> cell = new TableCell<>();
TextField textField = new TextField();
// Example of maintaining selection behavior when text field gains
// focus. You can also call getSelectedCells().add(...) on the selection
// model if you want to maintain multiple selected cells, etc.
textField.focusedProperty().addListener((obs, wasFocused, isFocused) -> {
if (isFocused) {
cell.getTableView().getSelectionModel().select(cell.getIndex(), cell.getTableColumn());
}
});
cell.graphicProperty().bind(Bindings.when(cell.emptyProperty())
.then((Node)null)
.otherwise(textField));
// If not using EasyBind, you need the following commented-out code in place of the next statement:
// ChangeListener<Person> rowItemListener = (obs, oldPerson, newPerson) -> {
// if (oldPerson != null) {
// textField.textProperty().unbindBidirectional(property.apply((Person)oldPerson));
// }
// if (newPerson != null) {
// textField.textProperty().bindBidirectional(property.apply((Person)newPerson));
// }
// };
// cell.tableRowProperty().addListener((obs, oldRow, newRow) -> {
// if (oldRow != null) {
// oldRow.itemProperty().removeListener(rowItemListener);
// if (oldRow.getItem() != null) {
// textField.textProperty().unbindBidirectional(property.apply((Person)oldRow.getItem()));
// }
// }
// if (newRow != null) {
// newRow.itemProperty().addListener(rowItemListener);
// if (newRow.getItem() != null) {
// textField.textProperty().bindBidirectional(property.apply((Person)newRow.getItem()));
// }
// }
// });
textField.textProperty().bindBidirectional(EasyBind.monadic(cell.tableRowProperty())
.selectProperty(TableRow::itemProperty)
.selectProperty(p -> (property.apply((Person)p))));
return cell ;
});
return col ;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}
(The annoying downcasts here are because TableCell<S,T>.getTableRow() returns a raw TableRow object, instead of a TableRow<S>, for reasons I have never understood.)
I think you can avoid it by implementing custom text field tablecell, where you can put the caret at the end of the item text manually on entering edit mode.
Another approach is to enter edit mode on focus:
table.getFocusModel().focusedCellProperty().addListener(
( ObservableValue<? extends TablePosition> observable, TablePosition oldValue, TablePosition newValue ) ->
{
if ( newValue != null )
{
Platform.runLater( () ->
{
table.edit( newValue.getRow(), newValue.getTableColumn() );
} );
}
}
);
a couple of years late, but I actually found a solution to this (using a Robot).
this.setOnKeyTyped(x -> {
String typed = x.getCharacter();
//can make editing start only when certain keys (e.g. digits) are typed.
if(typed != null && typed.matches("[0-9]")) {
Robot robot = new Robot();
robot.keyPress(KeyCode.ENTER);
}
});
Related
I have a TableView where every row has a ContextMenu like in the image below.
When I click on the first MenuItem called ("Contrassegna riga come analizzata"), I want all selected rows of the TableView (in the example above the ones starting with 22002649 and 22016572) to change color.
If they are already coloured, I want them to remove it.
I tried with the following code but it obviously works only with the last selected row and not with others
tableView.setRowFactory(
new Callback<TableView, TableRow>() {
#Override
public TableRow call(TableView tableView0) {
final TableRow row = new TableRow<>();
final ContextMenu rowMenu = new ContextMenu();
final PseudoClass checkedPC = PseudoClass.getPseudoClass("checked");
MenuItem doneRiga = new MenuItem("Contrassegna riga come analizzata");
doneRiga.setOnAction(j -> {
if (!row.getPseudoClassStates().contains(checkedPC))
row.pseudoClassStateChanged(checkedPC, true);
else
row.pseudoClassStateChanged(checkedPC, false);
});
MenuItem doneArticolo = new MenuItem("Contrassegna articolo come analizzato");
rowMenu.getItems().addAll(doneRiga, doneArticolo);
return row;
}
});
Consequently I obtain the following result
Any suggestions? Thank you
This is really a duplicate of Programmatically change the TableView row appearance, but since that question is quite old, here is a solution using more modern Java idioms.
Typically your model class should contain observable properties for all data that is required to view it. In this case, your table items can be either "analyzed" or "not analyzed", so they would usually have a boolean property to represent that. For example:
public class Item {
private final StringProperty name = new SimpleStringProperty();
private final BooleanProperty analyzed = new SimpleBooleanProperty();
public Item(String name) {
setName(name);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public boolean getAnalyzed() {
return analyzed.get();
}
public BooleanProperty analyzedProperty() {
return analyzed;
}
public void setAnalyzed(boolean analyzed) {
this.analyzed.set(analyzed);
}
}
Your table row needs to do two things:
Observe the analyzedProperty() of the current item it is displaying, so it updates the state if that property changes. Note this mean if the item changes, it needs to remove a listener from the old item (i.e. stop observing the property in the old item) and add a listener to the new item (start observing the property in the new item).
If the item changes, update the state of the row to reflect the analyzed state of the new item.
A table row implementation that does this looks like:
TableRow<Item> row = new TableRow<>(){
private final ChangeListener<Boolean> analyzedListener = (obs, wasAnalyzed, isNowAnalyzed) ->
updateState(isNowAnalyzed);
{
// Make sure we are observing the analyzedProperty on the current item
itemProperty().addListener((obs, oldItem, newItem) -> {
if (oldItem != null) {
oldItem.analyzedProperty().removeListener(analyzedListener);
}
if (newItem != null) {
newItem.analyzedProperty().addListener(analyzedListener);
}
});
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
updateState(false);
} else {
updateState(item.getAnalyzed());
}
}
private void updateState(boolean analyzed) {
pseudoClassStateChanged(analyzedPC, analyzed);
}
};
Note that in JavaFX 19 you can use the flatMap() API to simplify this code considerably:
TableRow<Item> row = new TableRow<>();
row.itemProperty()
.flatMap(Item::analyzedProperty)
.orElse(false)
.addListener((obs, wasAnalyzed, isNowAnalyzed) -> {
row.pseudoClassStateChanged(analyzedPC, isNowAnalyzed);
});
Now to change the state of the selected items, you just need to iterate through them and toggle the analyzed state:
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyzed");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
item.setAnalyzed(! item.getAnalyzed());
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
Putting it all together in a complete example:
package org.jamesd.examples.highlightrows;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
public class HelloApplication extends Application {
private static final PseudoClass analyzedPC = PseudoClass.getPseudoClass("analyzed");
#Override
public void start(Stage stage) throws IOException {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> column = new TableColumn<>("Item");
column.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(column);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.setRowFactory(tc -> {
TableRow<Item> row = new TableRow<>();
row.itemProperty()
.flatMap(Item::analyzedProperty)
.orElse(false)
.addListener((obs, wasAnalyzed, isNowAnalyzed) -> {
row.pseudoClassStateChanged(analyzedPC, isNowAnalyzed);
});
// Prior to JavaFX 19 you need something like the following (which is probably less robust):
// TableRow<Item> row = new TableRow<>(){
// private final ChangeListener<Boolean> analyzedListener = (obs, wasAnalyzed, isNowAnalyzed) ->
// updateState(isNowAnalyzed);
//
// {
// // Make sure we are observing the analyzedProperty on the current item
// itemProperty().addListener((obs, oldItem, newItem) -> {
// if (oldItem != null) {
// oldItem.analyzedProperty().removeListener(analyzedListener);
// }
// if (newItem != null) {
// newItem.analyzedProperty().addListener(analyzedListener);
// }
// });
// }
// #Override
// protected void updateItem(Item item, boolean empty) {
// super.updateItem(item, empty);
// if (empty || item == null) {
// updateState(false);
// } else {
// updateState(item.getAnalyzed());
// }
// }
//
// private void updateState(boolean analyzed) {
// pseudoClassStateChanged(analyzedPC, analyzed);
// }
// };
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyzed");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
item.setAnalyzed(! item.getAnalyzed());
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
return row;
});
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i));
}
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final BooleanProperty analyzed = new SimpleBooleanProperty();
public Item(String name) {
setName(name);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public boolean getAnalyzed() {
return analyzed.get();
}
public BooleanProperty analyzedProperty() {
return analyzed;
}
public void setAnalyzed(boolean analyzed) {
this.analyzed.set(analyzed);
}
}
public static void main(String[] args) {
launch();
}
}
with the stylesheet style.css (in the same package as the application class):
.table-row-cell:analyzed {
-fx-control-inner-background: derive(green, 20%);
-fx-control-inner-background-alt: green;
-fx-selection-bar: #00b140;
}
If for some reason you cannot change the model class (Item in the code above), you need to track which items are "analyzed" separately in a way that can be observed. An ObservableList could be used for this:
final ObservableList<Item> analyzedItems = FXCollections.observableArrayList();
Now the table row can observe that list, and update the CSS pseudoclass if the list changes:
TableRow<Item> row = new TableRow<>(){
{
// If the list of analyzed items changes, make sure the state is correct:
analyzedItems.addListener((ListChangeListener.Change<? extends Item> change) -> {
updateState(analyzedItems.contains(getItem()));
});
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
updateState(false);
} else {
updateState(analyzedItems.contains(item));
}
}
private void updateState(boolean analyzed) {
pseudoClassStateChanged(analyzedPC, analyzed);
}
};
and you can toggle the state by adding or removing items from the list of analyzed items accordingly:
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyze");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
if (analyzedItems.contains(item)) {
analyzedItems.remove(item);
} else {
analyzedItems.add(item);
}
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
The complete example in this case looks like;
package org.jamesd.examples.highlightrows;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import java.io.IOException;
import java.util.List;
public class HelloApplication extends Application {
private static final PseudoClass analyzedPC = PseudoClass.getPseudoClass("analyzed");
#Override
public void start(Stage stage) throws IOException {
TableView<Item> table = new TableView<>();
TableColumn<Item, String> column = new TableColumn<>("Item");
column.setCellValueFactory(data -> data.getValue().nameProperty());
table.getColumns().add(column);
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
final ObservableList<Item> analyzedItems = FXCollections.observableArrayList();
table.setRowFactory(tc -> {
TableRow<Item> row = new TableRow<>(){
{
// If the list of analyzed items changes, make sure the state is correct:
analyzedItems.addListener((ListChangeListener.Change<? extends Item> change) -> {
updateState(analyzedItems.contains(getItem()));
});
}
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
updateState(false);
} else {
updateState(analyzedItems.contains(item));
}
}
private void updateState(boolean analyzed) {
pseudoClassStateChanged(analyzedPC, analyzed);
}
};
ContextMenu menu = new ContextMenu();
MenuItem analyzedMI = new MenuItem("Analyze");
analyzedMI.setOnAction(e -> {
// Toggle analyzed state of selected items
List<Item> selectedItems = row.getTableView().getSelectionModel().getSelectedItems();
for (Item item : selectedItems) {
if (analyzedItems.contains(item)) {
analyzedItems.remove(item);
} else {
analyzedItems.add(item);
}
}
});
menu.getItems().add(analyzedMI);
row.setContextMenu(menu);
return row;
});
for (int i = 1 ; i <= 20 ; i++) {
table.getItems().add(new Item("Item "+i));
}
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root);
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
public Item(String name) {
setName(name);
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
}
public static void main(String[] args) {
launch();
}
}
I have a JavaFX TableView that I'm populating with an ObservableList of Tasks. I've been trying to create a column that displays the index of each row, which serves as the ID for the tasks in the table, but I've tried the method here and similar methods from other sources with little success.
My code for reference, which has no superficial errors (as far as Eclipse can tell):
#FXML private TableColumn<Task, String> taskIndexCol;
Callback<TableColumn<Task, String>, TableCell<Task, String>> cb =
new Callback<TableColumn<Task, String>, TableCell<Task, String>>(){
#Override
public TableCell<Task, String> call(TableColumn<Task, String> col) {
TableCell<Task, String> cell = new TableCell<Task, String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null) {
setText("");
} else {
setText(getIndex()+"");
}
}
};
return cell;
}
};
taskIndexCol.setCellFactory(cb);
Currently, my code gives me a NullPointerException when I try to set the CellFactory of the column. I've tried it with a populated task list, but that didn't help. I've been stumped for a very long time -- and theoretically this should be pretty easy, since it's just numbering the rows? It feels like I'm jumping through a million hoops to do something frustratingly simple.
Edit: The last line gives me the NPE.
It's impossible to tell the cause of the Null pointer exception, because you haven't shown the stack trace, identified the line which throws the exception, or posted enough of your code (none of the code in your callback can throw a null pointer exception, so something is wrong somewhere else).
For your actual cell implementation, you didn't show if you had a cellValueFactory set on the column. If you don't, then the item will always be null, and so you will never see any text in the cells in that column. You can check the empty property (or method parameter) as a means to check if the cell is in an empty row or one with actual data. (Note this means the column really doesn't need to provide any data at all: it can be a TableColumn<Task, Void>.)
Additionally, it's probably safer to rely on using updateIndex(...) instead of updateItem(...). updateIndex is guaranteed to be called when the index changes; if you implement updateItem you are assuming the index is set before the item, which means you are relying on an implementation detail.
Your code is a lot shorter and easier to read if you use Java 8 lambda expressions:
taskIndexCol.setCellFactory(col -> new TableCell<Task, String>() {
#Override
protected void updateIndex(int index) {
super.updateIndex(index);
if (isEmpty() || index < 0) {
setText(null);
} else {
setText(Integer.toString(index));
}
}
});
or alternatively
taskIndexCol.setCellFactory(col -> {
TableCell<Task, String> cell = new TableCell<>();
cell.textProperty().bind(Bindings.when(cell.emptyProperty())
.then("")
.otherwise(cell.indexProperty().asString()));
return cell ;
});
Here is a SSCCE:
import java.util.function.Function;
import static javafx.beans.binding.Bindings.when ;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableViewWithIndexColumn extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.setEditable(true);
table.getItems().addAll(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson",
"isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com"));
TableColumn<Person, String> firstNameCol = createColumn("First Name",
Person::firstNameProperty, 150);
TableColumn<Person, String> lastNameCol = createColumn("Last Name",
Person::lastNameProperty, 150);
TableColumn<Person, String> emailCol = createColumn("Email",
Person::emailProperty, 150);
// index column doesn't even need data...
TableColumn<Person, Void> indexCol = createColumn("Index", person -> null, 50);
// cell factory to display the index:
// indexCol.setCellFactory(col -> {
//
// // just a default table cell:
// TableCell<Person, Void> cell = new TableCell<>();
//
// cell.textProperty().bind(
// when(cell.emptyProperty())
// .then("")
// .otherwise(cell.indexProperty().asString()));
//
// return cell ;
// });
indexCol.setCellFactory(col -> new TableCell<Person, Void>() {
#Override
public void updateIndex(int index) {
super.updateIndex(index);
if (isEmpty() || index < 0) {
setText(null);
} else {
setText(Integer.toString(index));
}
}
});
table.getColumns().add(indexCol);
table.getColumns().add(firstNameCol);
table.getColumns().add(lastNameCol);
table.getColumns().add(emailCol);
primaryStage.setScene(new Scene(new BorderPane(table), 600, 400));
primaryStage.show();
}
private <S, T> TableColumn<S, T> createColumn(String title,
Function<S, ObservableValue<T>> property, double width) {
TableColumn<S, T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setPrefWidth(width);
return col;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person() {
this("", "", "");
}
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}
I would like to add multiple combo boxes to JavaFX that after the user has selected an item the cost of that item will be displayed under the combo box. Also that the total cost of all the selected items will be displayed at the bottom. I know how to make one combo box that will display the cost of one item selected but can't figure out how to make multiple ones and to display the cost of everything selected
import javafx.application.Application;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.ComboBox;
import javafx.scene.control.TextField;
import javafx.scene.layout.VBox;
import javafx.scene.text.Text;
import javafx.stage.Stage;
import javafx.util.StringConverter;
import javafx.collections.FXCollections;
public class Animals extends Application {
Stage window;
Scene scene;
Button button;
ComboBox<Animal> comboBox = new ComboBox<Animal>();
Text textNamePrice = new Text();
static public TextField[] tfLetters = new TextField[37];
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) throws Exception {
window = primaryStage;
window.setTitle("ComboBox ");
button = new Button("Submit");
comboBox = new ComboBox<Animal>();
comboBox.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object.getName();
}
#Override
public Animal fromString(String string) {
return null;
}
});
comboBox.setItems(FXCollections.observableArrayList(new Animal("Dog", 30.12), new Animal("Cat", 23.23),
new Animal("Bird", 15.0)));
comboBox.valueProperty().addListener((obs, oldVal, newVal) -> {
String selectionText = "Price of the " + newVal.getName() + " is : " + newVal.getPrice();
System.out.println(selectionText);
textNamePrice.setText(selectionText);
});
VBox layout = new VBox(10);
layout.setPadding(new Insets(60, 60, 60, 60));
layout.getChildren().addAll(comboBox, textNamePrice, button);
scene = new Scene(layout, 500, 350);
window.setScene(scene);
window.show();
}
public class Animal {
private String name;
private Double price;
public Double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, Double price) {
this.name = name;
this.price = price;
}
}
}
It's probabls easiest to use a custom Node type AnimalChooser for displaying the ComboBox + price. This way the functionality for one selection+price display can be handled in one place. Also you can provide a price property based on the selection to sum them up from you application class.
The following example places all those AnimalChoosers in an VBox and adds a listener to the child list to add and remove listeners to/from the child list, should it be modified, which would allow you to dynamically add/remove those AnimalChooser to/from the VBox and still get a properly updated sum.
public class Animal {
private final String name;
// primitive type should be prefered here
private final double price;
public double getPrice() {
return price;
}
public String getName() {
return name;
}
public Animal(String name, double price) {
this.name = name;
this.price = price;
}
}
public class AnimalChooser extends VBox {
private final ComboBox<Animal> animalCombo;
private final ReadOnlyDoubleWrapper price;
private final Text text;
public AnimalChooser(ObservableList<Animal> items) {
setSpacing(5);
animalCombo = new ComboBox<>(items);
// converter for using a custom string representation of Animal in the
// combobox
animalCombo.setConverter(new StringConverter<Animal>() {
#Override
public String toString(Animal object) {
return object == null ? "" : object.getName();
}
#Override
public Animal fromString(String string) {
if (string == null || string.isEmpty()) {
return null;
} else {
// find suitable animal from list
Animal animal = null;
for (Animal item : items) {
if (string.equals(item.getName())) {
animal = item;
break;
}
}
return animal;
}
}
});
text = new Text();
price = new ReadOnlyDoubleWrapper();
getChildren().addAll(animalCombo, text);
// bind price value to price property
price.bind(Bindings.createDoubleBinding(new Callable<Double>() {
#Override
public Double call() throws Exception {
Animal animal = animalCombo.getValue();
return animal == null ? 0d : animal.getPrice();
}
}, animalCombo.valueProperty()));
// bind text to content of Text node
text.textProperty().bind(Bindings.when(animalCombo.valueProperty().isNull()).then("").otherwise(price.asString("%.2f $")));
}
public final double getPrice() {
return this.price.get();
}
public final ReadOnlyDoubleProperty priceProperty() {
return this.price.getReadOnlyProperty();
}
}
#Override
public void start(Stage primaryStage) {
VBox animalChoosers = new VBox(20);
ObservableList<Animal> animals = FXCollections.observableArrayList(
new Animal("cat", 1000.99),
new Animal("dog", 20.50),
new Animal("goldfish", 15.22)
);
final DoubleProperty total = new SimpleDoubleProperty();
InvalidationListener listener = new InvalidationListener() {
#Override
public void invalidated(Observable observable) {
double sum = 0d;
for (Node n : animalChoosers.getChildren()) {
AnimalChooser chooser = (AnimalChooser) n;
sum += chooser.getPrice();
}
total.set(sum);
}
};
// just in case you want to add AnimalChooser s dynamially to animalChoosers
animalChoosers.getChildren().addListener(new ListChangeListener<Node>() {
#Override
public void onChanged(ListChangeListener.Change<? extends Node> c) {
while (c.next()) {
// add remove listeners updating the total
for (Node n : c.getRemoved()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().removeListener(listener);
}
for (Node n : c.getAddedSubList()) {
AnimalChooser chooser = (AnimalChooser) n;
chooser.priceProperty().addListener(listener);
}
}
listener.invalidated(null);
}
});
for (int i = 0; i < 10; i++) {
animalChoosers.getChildren().add(new AnimalChooser(animals));
}
BorderPane root = new BorderPane(animalChoosers);
Text totalText = new Text();
totalText.textProperty().bind(total.asString("total: %.2f $"));
root.setBottom(totalText);
BorderPane.setMargin(totalText, new Insets(20));
Scene scene = new Scene(root);
primaryStage.setScene(scene);
primaryStage.show();
}
I need editable cells for JavaFX TableView. The default TextFieldTableCell requires the user to press enter to commit a change. I think a typical user expects the change to be kept when clicking outside the cell. All the features I want include:
Single-click selects the cell and
Another single-click on a cell, in the selected cell, or enter press, starts editing.
A double-click on a cell starts editing.
Pressing enter commits the changes to the cell
Changing mouse focus anywhere outside the cell commits the changes to the cell
I found a EditCell version in this post
It fulfills the first 4 requirements and partially the 5th, but when the user clicks on another populated cell in the table the edit changes are lost. The focus listener is triggered, but no commit. Clicking on an empty cell or another scene element commits the changes.
There is a supposedly a solution provided in post
However, the solution only contains snippets of code instead of a working example. I was not able to implement it.
Can anyone help put the pieces together and demo a class than extends TableCell that has all the features I listed above?
I'm probably a little late to this party but here goes.
Not being able to commit the value of the changed cell is probably due to the default implementation of the commitEdit method in TableCell, as it treats a loss of focus as a cancel-action by default.
However, user James_D created a nice workaround here
EDIT:
Example class based on James_D's work
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.event.Event;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.StringConverter;
public class TableViewCommitOnFocusLoss extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(createColumn("First Name", Person::firstNameProperty));
table.getColumns().add(createColumn("Last Name", Person::lastNameProperty));
table.getColumns().add(createColumn("Email", Person::emailProperty));
table.getItems().addAll(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
Button showDataButton = new Button("Debug data");
showDataButton.setOnAction(event -> table.getItems().stream()
.map(p -> String.format("%s %s", p.getFirstName(), p.getLastName()))
.forEach(System.out::println));
Scene scene = new Scene(new BorderPane(table, null, null, showDataButton, null), 880, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private <T> TableColumn<T, String> createColumn(String title, Function<T, StringProperty> property) {
TableColumn<T, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> EditCell.createStringEditCell());
return col ;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
private final StringProperty email = new SimpleStringProperty();
public Person(String firstName, String lastName, String email) {
setFirstName(firstName);
setLastName(lastName);
setEmail(email);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final java.lang.String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final java.lang.String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final java.lang.String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final java.lang.String lastName) {
this.lastNameProperty().set(lastName);
}
public final StringProperty emailProperty() {
return this.email;
}
public final java.lang.String getEmail() {
return this.emailProperty().get();
}
public final void setEmail(final java.lang.String email) {
this.emailProperty().set(email);
}
}
public static void main(String[] args) {
launch(args);
}
}
class EditCell<S, T> extends TableCell<S, T> {
// Text field for editing
// TODO: allow this to be a plugable control.
private final TextField textField = new TextField();
// Converter for converting the text in the text field to the user type, and vice-versa:
private final StringConverter<T> converter ;
public EditCell(StringConverter<T> converter) {
this.converter = converter ;
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.getCode() == KeyCode.RIGHT) {
getTableView().getSelectionModel().selectRightCell();
event.consume();
} else if (event.getCode() == KeyCode.LEFT) {
getTableView().getSelectionModel().selectLeftCell();
event.consume();
} else if (event.getCode() == KeyCode.UP) {
getTableView().getSelectionModel().selectAboveCell();
event.consume();
} else if (event.getCode() == KeyCode.DOWN) {
getTableView().getSelectionModel().selectBelowCell();
event.consume();
}
});
}
/**
* Convenience converter that does nothing (converts Strings to themselves and vice-versa...).
*/
public static final StringConverter<String> IDENTITY_CONVERTER = new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
};
/**
* Convenience method for creating an EditCell for a String value.
* #return
*/
public static <S> EditCell<S, String> createStringEditCell() {
return new EditCell<S, String>(IDENTITY_CONVERTER);
}
// set the text of the text field and display the graphic
#Override
public void startEdit() {
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 bails if we are not editing...
if (! isEditing() && ! 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<S,T>(table, getIndex(), column),
TableColumn.editCommitEvent(), item);
Event.fireEvent(column, event);
}
}
super.commitEdit(item);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
}
I'd like to create a table with the following features:
Edit on key press
Enter key = next row
Tab key = next column
Escape key = cancel edit
Below is a code which implements these features. The values should be committed on focus lost. Problem: They aren't committed. The focus change event is fired, the values would be correct according to the console output, but in the end the values in the table cells are the old ones.
Does anyone know how to prevent this and how do you get the current EditingCell object so that I can invoke commit manually? After all there should be some kind of verifier invoked which prevents changing the focus if the values aren't correct.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.EventHandler;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellEditEvent;
import javafx.scene.control.TablePosition;
import javafx.scene.control.TableView;
import javafx.scene.control.TextField;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.VBox;
import javafx.scene.text.Font;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TableViewInlineEditDemo extends Application {
private final TableView<Person> table = new TableView<>();
private final ObservableList<Person> data =
FXCollections.observableArrayList(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com"));
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
Scene scene = new Scene(new Group());
stage.setWidth(450);
stage.setHeight(550);
final Label label = new Label("Address Book");
label.setFont(new Font("Arial", 20));
table.setEditable(true);
Callback<TableColumn<Person, String>, TableCell<Person, String>> cellFactory = (TableColumn<Person, String> p) -> new EditingCell();
TableColumn<Person, String> firstNameCol = new TableColumn<>("First Name");
TableColumn<Person, String> lastNameCol = new TableColumn<>("Last Name");
TableColumn<Person, String> emailCol = new TableColumn<>("Email");
firstNameCol.setMinWidth(100);
firstNameCol.setCellValueFactory(new PropertyValueFactory<>("firstName"));
firstNameCol.setCellFactory(cellFactory);
firstNameCol.setOnEditCommit((CellEditEvent<Person, String> t) -> {
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setFirstName(t.getNewValue());
});
lastNameCol.setMinWidth(100);
lastNameCol.setCellValueFactory(new PropertyValueFactory<>("lastName"));
lastNameCol.setCellFactory(cellFactory);
lastNameCol.setOnEditCommit((CellEditEvent<Person, String> t) -> {
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setLastName(t.getNewValue());
});
emailCol.setMinWidth(200);
emailCol.setCellValueFactory(new PropertyValueFactory<>("email"));
emailCol.setCellFactory(cellFactory);
emailCol.setOnEditCommit((CellEditEvent<Person, String> t) -> {
((Person) t.getTableView().getItems().get(t.getTablePosition().getRow())).setEmail(t.getNewValue());
});
table.setItems(data);
table.getColumns().addAll(firstNameCol, lastNameCol, emailCol);
// edit mode on keypress
table.addEventFilter(KeyEvent.KEY_PRESSED, new EventHandler<KeyEvent>() {
#Override
public void handle(KeyEvent e) {
if( e.getCode() == KeyCode.TAB) { // commit should be performed implicitly via focusedProperty, but isn't
table.getSelectionModel().selectNext();
e.consume();
return;
}
else if( e.getCode() == KeyCode.ENTER) { // commit should be performed implicitly via focusedProperty, but isn't
table.getSelectionModel().selectBelowCell();
e.consume();
return;
}
// switch to edit mode on keypress, but only if we aren't already in edit mode
if( table.getEditingCell() == null) {
if( e.getCode().isLetterKey() || e.getCode().isDigitKey()) {
TablePosition focusedCellPosition = table.getFocusModel().getFocusedCell();
table.edit(focusedCellPosition.getRow(), focusedCellPosition.getTableColumn());
}
}
}
});
// single cell selection mode
table.getSelectionModel().setCellSelectionEnabled(true);
table.getSelectionModel().selectFirst();
final VBox vbox = new VBox();
vbox.getChildren().addAll(label, table);
((Group) scene.getRoot()).getChildren().addAll(vbox);
stage.setScene(scene);
stage.show();
}
class EditingCell extends TableCell<Person, String> {
private TextField textField;
public EditingCell() {
}
#Override
public void startEdit() {
if (!isEmpty()) {
super.startEdit();
createTextField();
setText(null);
setGraphic(textField);
textField.requestFocus(); // must be before selectAll() or the caret would be in wrong position
textField.selectAll();
}
}
#Override
public void cancelEdit() {
super.cancelEdit();
setText((String) getItem());
setGraphic(null);
}
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(null);
}
}
}
private void createTextField() {
textField = new TextField(getString());
textField.setMinWidth(this.getWidth() - this.getGraphicTextGap() * 2);
// commit on focus lost
textField.focusedProperty().addListener((ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) -> {
if( oldValue = true && newValue == false) {
System.out.println( "Focus lost, current value: " + textField.getText());
commitEdit();
}
});
// cancel edit on ESC
textField.addEventFilter(KeyEvent.KEY_RELEASED, e -> {
if( e.getCode() == KeyCode.ESCAPE) {
cancelEdit();
}
});
}
private String getString() {
return getItem() == null ? "" : getItem().toString();
}
private boolean commitEdit() {
super.commitEdit(textField.getText());
return true; // TODO: add verifier and check if commit was possible
}
}
public static class Person {
private final SimpleStringProperty firstName;
private final SimpleStringProperty lastName;
private final SimpleStringProperty email;
private Person(String fName, String lName, String email) {
this.firstName = new SimpleStringProperty(fName);
this.lastName = new SimpleStringProperty(lName);
this.email = new SimpleStringProperty(email);
}
public String getFirstName() {
return firstName.get();
}
public void setFirstName(String fName) {
firstName.set(fName);
}
public String getLastName() {
return lastName.get();
}
public void setLastName(String fName) {
lastName.set(fName);
}
public String getEmail() {
return email.get();
}
public void setEmail(String fName) {
email.set(fName);
}
}
}
Thank you very much!
Edit: I've narrowed it down. It seems the problem is that the JavaFX code cancels the edit mode when the focus changes. That's bad.
public Cell() {
setText(null); // default to null text, to match the null item
// focusTraversable is styleable through css. Calling setFocusTraversable
// makes it look to css like the user set the value and css will not
// override. Initializing focusTraversable by calling set on the
// CssMetaData ensures that css will be able to override the value.
((StyleableProperty<Boolean>)(WritableValue<Boolean>)focusTraversableProperty()).applyStyle(null, Boolean.FALSE);
getStyleClass().addAll(DEFAULT_STYLE_CLASS);
/**
* Indicates whether or not this cell has focus. For example, a
* ListView defines zero or one cell as being the "focused" cell. This cell
* would have focused set to true.
*/
super.focusedProperty().addListener(new InvalidationListener() {
#Override public void invalidated(Observable property) {
pseudoClassStateChanged(PSEUDO_CLASS_FOCUSED, isFocused()); // TODO is this necessary??
// The user has shifted focus, so we should cancel the editing on this cell
if (!isFocused() && isEditing()) {
cancelEdit();
}
}
});
// initialize default pseudo-class state
pseudoClassStateChanged(PSEUDO_CLASS_EMPTY, true);
}
I got curious and did some background research.
You are facing the problem of a well-known bug in the JavaFX.
Background
When you call commitEdit(textField.getText()), the first thing it does is to check the value of isEditing() and returns if the value is false, without committing.
public void commitEdit(T newValue) {
if (! isEditing()) return;
... // Rest of the things
}
Why does it return false?
As you have probably found out, as soon as you press TAB or ENTER to change your selection, cancelEdit() is called which sets the TableCell.isEditing() to false. By the time the commitEdit() inside textField's focus property listener is called, isEditing() is already returning false.
Solutions / Hacks
There have been on going discussion on the Topic in JavaFX community. People in there have posted hacks, which you are most welcome to look at.
TableView, TreeView, ListView - Clicking outside of the edited cell, node, or entry should commit the value
TableCell - commit on focus lost not possible in every case
There is a hack shown in a SO thread, which seems to get the job done, although I haven't tried it (yet).
I've run into the same issue and I solved it by combining these two code snippets:
https://gist.github.com/james-d/be5bbd6255a4640a5357
https://gist.github.com/abhinayagarwal/9383881
Custom TableCell implementation
public class EditCell<S, T> extends TableCell<S, T> {
private final TextField textField = new TextField();
// Converter for converting the text in the text field to the user type, and vice-versa:
private final StringConverter<T> converter;
/**
* Creates and initializes an edit cell object.
*
* #param converter
* the converter to convert from and to strings
*/
public EditCell(StringConverter<T> converter) {
this.converter = converter;
itemProperty().addListener((obx, oldItem, newItem) -> {
setText(newItem != null ? this.converter.toString(newItem) : null);
});
setGraphic(this.textField);
setContentDisplay(ContentDisplay.TEXT_ONLY);
this.textField.setOnAction(evt -> {
commitEdit(this.converter.fromString(this.textField.getText()));
});
this.textField.focusedProperty().addListener((obs, wasFocused, isNowFocused) -> {
if (!isNowFocused) {
commitEdit(this.converter.fromString(this.textField.getText()));
}
});
this.textField.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ESCAPE) {
this.textField.setText(this.converter.toString(getItem()));
cancelEdit();
event.consume();
} else if (event.getCode() == KeyCode.TAB) {
commitEdit(this.converter.fromString(this.textField.getText()));
TableColumn<S, ?> nextColumn = getNextColumn(!event.isShiftDown());
if (nextColumn != null) {
getTableView().getSelectionModel().clearAndSelect(getTableRow().getIndex(), nextColumn);
getTableView().edit(getTableRow().getIndex(), nextColumn);
}
}
});
}
/**
* Convenience converter that does nothing (converts Strings to themselves and vice-versa...).
*/
public static final StringConverter<String> IDENTITY_CONVERTER = new StringConverter<String>() {
#Override
public String toString(String object) {
return object;
}
#Override
public String fromString(String string) {
return string;
}
};
/**
* Convenience method for creating an EditCell for a String value.
*
* #return the edit cell
*/
public static <S> EditCell<S, String> createStringEditCell() {
return new EditCell<S, String>(IDENTITY_CONVERTER);
}
// set the text of the text field and display the graphic
#Override
public void startEdit() {
super.startEdit();
this.textField.setText(this.converter.toString(getItem()));
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
this.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 bails if we are not editing...
if (!isEditing() && !item.equals(getItem())) {
TableView<S> table = getTableView();
if (table != null) {
TableColumn<S, T> column = getTableColumn();
CellEditEvent<S, T> event = new CellEditEvent<>(table,
new TablePosition<S, T>(table, getIndex(), column),
TableColumn.editCommitEvent(), item);
Event.fireEvent(column, event);
}
}
super.commitEdit(item);
setContentDisplay(ContentDisplay.TEXT_ONLY);
}
/**
* Finds and returns the next editable column.
*
* #param forward
* indicates whether to search forward or backward from the current column
* #return the next editable column or {#code null} if there is no next column available
*/
private TableColumn<S, ?> getNextColumn(boolean forward) {
List<TableColumn<S, ?>> columns = new ArrayList<>();
for (TableColumn<S, ?> column : getTableView().getColumns()) {
columns.addAll(getEditableColumns(column));
}
// There is no other column that supports editing.
if (columns.size() < 2) { return null; }
int currentIndex = columns.indexOf(getTableColumn());
int nextIndex = currentIndex;
if (forward) {
nextIndex++;
if (nextIndex > columns.size() - 1) {
nextIndex = 0;
}
} else {
nextIndex--;
if (nextIndex < 0) {
nextIndex = columns.size() - 1;
}
}
return columns.get(nextIndex);
}
/**
* Returns all editable columns of a table column (supports nested columns).
*
* #param root
* the table column to check for editable columns
* #return a list of table columns which are editable
*/
private List<TableColumn<S, ?>> getEditableColumns(TableColumn<S, ?> root) {
List<TableColumn<S, ?>> columns = new ArrayList<>();
if (root.getColumns().isEmpty()) {
// We only want the leaves that are editable.
if (root.isEditable()) {
columns.add(root);
}
return columns;
} else {
for (TableColumn<S, ?> column : root.getColumns()) {
columns.addAll(getEditableColumns(column));
}
return columns;
}
}
}
Controller
#FXML
private void initialize() {
table.getSelectionModel().setCellSelectionEnabled(true);
table.setEditable(true);
table.getColumns().add(createColumn("First Name", Person::firstNameProperty));
table.getColumns().add(createColumn("Last Name", Person::lastNameProperty));
table.getColumns().add(createColumn("Email", Person::emailProperty));
table.getItems().addAll(
new Person("Jacob", "Smith", "jacob.smith#example.com"),
new Person("Isabella", "Johnson", "isabella.johnson#example.com"),
new Person("Ethan", "Williams", "ethan.williams#example.com"),
new Person("Emma", "Jones", "emma.jones#example.com"),
new Person("Michael", "Brown", "michael.brown#example.com")
);
table.setOnKeyPressed(event -> {
TablePosition<Person, ?> pos = table.getFocusModel().getFocusedCell() ;
if (pos != null && event.getCode().isLetterKey()) {
table.edit(pos.getRow(), pos.getTableColumn());
}
});
}
private <T> TableColumn<T, String> createColumn(String title, Function<T, StringProperty> property) {
TableColumn<T, String> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(column -> EditCell.createStringEditCell());
return col;
}
My proposal to solve this atrocity is the following (sorry for missing JavaDoc).
This is a cancel-to-commit redirection solution. I tested it under LINUX with Java 1.8.0-121. Here, the only way how to discard a cell editor is to press ESCAPE.
import javafx.beans.binding.Bindings;
import javafx.scene.Node;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyEvent;
public abstract class AutoCommitTableCell<S,T> extends TableCell<S,T>
{
private Node field;
private boolean startEditing;
private T defaultValue;
/** #return a newly created input field. */
protected abstract Node newInputField();
/** #return the current value of the input field. */
protected abstract T getInputValue();
/** Sets given value to the input field. */
protected abstract void setInputValue(T value);
/** #return the default in case item is null, must be never null, else cell will not be editable. */
protected abstract T getDefaultValue();
/** #return converts the given value to a string, being the cell-renderer representation. */
protected abstract String inputValueToText(T value);
#Override
public void startEdit() {
try {
startEditing = true;
super.startEdit(); // updateItem() will be called
setInputValue(getItem());
}
finally {
startEditing = false;
}
}
/** Redirects to commitEdit(). Leaving the cell should commit, just ESCAPE should cancel. */
#Override
public void cancelEdit() {
// avoid JavaFX NullPointerException when calling commitEdit()
getTableView().edit(getIndex(), getTableColumn());
commitEdit(getInputValue());
}
private void cancelOnEscape() {
if (defaultValue != null) { // canceling default means writing null
setItem(defaultValue = null);
setText(null);
setInputValue(null);
}
super.cancelEdit();
}
#Override
protected void updateItem(T newValue, boolean empty) {
if (startEditing && newValue == null)
newValue = (defaultValue = getDefaultValue());
super.updateItem(newValue, empty);
if (empty || newValue == null) {
setText(null);
setGraphic(null);
}
else {
setText(inputValueToText(newValue));
setGraphic(startEditing || isEditing() ? getInputField() : null);
}
}
protected final Node getInputField() {
if (field == null) {
field = newInputField();
// a cell-editor won't be committed or canceled automatically by JFX
field.addEventFilter(KeyEvent.KEY_PRESSED, event -> {
if (event.getCode() == KeyCode.ENTER || event.getCode() == KeyCode.TAB)
commitEdit(getInputValue());
else if (event.getCode() == KeyCode.ESCAPE)
cancelOnEscape();
});
contentDisplayProperty().bind(
Bindings.when(editingProperty())
.then(ContentDisplay.GRAPHIC_ONLY)
.otherwise(ContentDisplay.TEXT_ONLY)
);
}
return field;
}
}
You can extend this class to support any data type.
Example for a String field is (Person is an example bean):
import javafx.scene.Node;
import javafx.scene.control.TextField;
import jfx.examples.tablebinding.PersonsModel.Person;
public class StringTableCell extends AutoCommitTableCell<Person,String>
{
#Override
protected String getInputValue() {
return ((TextField) getInputField()).getText();
}
#Override
protected void setInputValue(String value) {
((TextField) getInputField()).setText(value);
}
#Override
protected String getDefaultValue() {
return "";
}
#Override
protected Node newInputField() {
return new TextField();
}
#Override
protected String inputValueToText(String newValue) {
return newValue;
}
}
To be applied in this way:
final TableColumn<Person,String> nameColumn = new TableColumn<Person,String>("Name");
nameColumn.setCellValueFactory(
cellDataFeatures -> cellDataFeatures.getValue().nameProperty());
nameColumn.setCellFactory(
cellDataFeatures -> new StringTableCell());
I had found a simple solution which works in my case for TableCells. The idea is to forget about commitEdit at focus lost. Let javafx do its work, and then just update the value of the previously edited cell.
abstract class EditingTextCell<T, V> extends TableCell<T, V> {
protected TextField textField;
private T editedItem;
#Override
public void startEdit() {
...
textField.focusedProperty().addListener((t, oldval, newval) -> {
if (!newval) {
setItemValue(editedItem, textField.getText());
}
});
editedItem = (T) getTableRow().getItem();
}
public abstract void setItemValue(T item, String text);
...
}
so, the only trick is to implement the setItemValue() in such a way that it updates the correct part of the item.