JavaFx - String and FlowPane (row?) within tableView? - javafx

I'm currently trying to implement the following:
A TableView with an ObservableList as dataset, with two columns, each of which contains Strings (names of the players). This part is easy enough.
Once a Player(name) is clicked, a custom FlowPane should be injected below the selected player. If another player is clicked, the flowpane should disappear and be injected below the currently clicked player.
The below code implements the TableView (minus the mouse listener part). Please help me let the FlowPane span the entire row. I'm guessing I need a RowFactory but have no clue how to make it work for my purposes :)
Also, apparently both my columns now show the same data. Confusing :) Is there a way to tell one column to use half the data set and the other column the other half? I obviously don't want my data shown twice.
public class main extends Application
{
public static void main(String[] args)
{
launch(args);
}
#Override
public void start(Stage stage) throws Exception
{
try
{
FlowPane f = new FlowPane();
Scene scene = new Scene(f, 300, 200);
Player p1 = new Player("player 1 ");
Player p2 = new Player("player 2 ");
Player p3 = new Player("player 3 ");
ArrayList<Object> players = new ArrayList<>();
players.add(p1);
players.add(p2);
players.add(p3);
ObservableList<Object> observableList = FXCollections.observableArrayList(players);
TableView<Object> table = createTableView(observableList, 300, 200);
f.getChildren().add(table);
injectFlowPane(table);
stage.setScene(scene);
stage.show();
}
catch (Exception e)
{
e.printStackTrace();
}
}
public TableView<Object> createTableView(ObservableList<Object> items, double width, double height)
{
TableView<Object> table = new TableView<>();
table.setItems(items);
table.getColumns().add(createTableColumn(width / 2));
table.getColumns().add(createTableColumn(width / 2));
table.setMinSize(width, height);
table.setPrefSize(width, height);
table.setMaxSize(width, height);
return table;
}
private TableColumn<Object, Object> createTableColumn(double width)
{
TableColumn<Object, Object> tableColumn = new TableColumn<>();
tableColumn.setCellFactory(
new Callback<TableColumn<Object, Object>, TableCell<Object, Object>>() {
#Override
public TableCell<Object, Object> call(TableColumn<Object, Object> arg0)
{
return new PlayerCell();
}
});
tableColumn.setCellValueFactory(cellDataFeatures -> {
Object item = cellDataFeatures.getValue();
return new SimpleObjectProperty<>(item);
});
tableColumn.setMinWidth(width);
return tableColumn;
}
private void injectFlowPane(TableView<Object> table)
{
FlowPane f = new FlowPane();
f.setMinSize(50, 50);
f.setBackground(new Background(new BackgroundFill(Color.DARKGREEN, CornerRadii.EMPTY, Insets.EMPTY)));
table.getItems().add(1, f);
}
}
public class PlayerCell extends TableCell<Object, Object>
{
#Override
protected void updateItem(Object item, boolean empty)
{
super.updateItem(item, false);
// if (empty)
if (item != null)
{
if (item instanceof Player)
{
setText(((Player) item).getName());
setGraphic(null);
}
else if (item instanceof FlowPane)
{
setGraphic((FlowPane) item);
}
else
{
setText("N/A");
setGraphic(null);
}
}
else
{
setText(null);
setGraphic(null);
}
}
}
public class Player
{
private String name;
public Player(String name)
{
this.name = name;
}
public String getName()
{
return name;
}
}
EDIT:
I have now implemented James_D's ExpandingTableRow, which works neatly as far as showing the FlowPane below the selected TableRow is concerned. I have also managed to change my datastructures so that each column now shows different players instead of the same ones in each column.
However, the FlowPane that is created should actually depend on the actual player(cell) that is clicked within the row. In James' example: a different FlowPane would be created if the FirstName or LastName was selected (even for the same row). The FlowPane should be shown the same way - below the selected row - but it's a different, new FlowPane depending on if FirstName was clicked, or if LastName was clicked. How can I manage to do this?
I've looked at using:
table.getSelectionModel().setCellSelectionEnabled(true);
But this actually seems to disable James_d's solution.

This solution works only in Java 9 and later.
The display of a row is managed by a TableRow, and the actual layout of that row is performed by its skin (a TableRowSkin). So to manage this, you need a subclass of TableRow that installs a custom skin.
The row implementation is pretty straightforward: in this example I added a property for the "additional content" to be displayed when the row is selected. It also overrides the createDefaultSkin() method to specify a custom skin implementation.
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.scene.Node;
import javafx.scene.control.Skin;
import javafx.scene.control.TableRow;
public class ExpandingTableRow<T> extends TableRow<T> {
private final ObjectProperty<Node> selectedRowContent = new SimpleObjectProperty<>();
public final ObjectProperty<Node> selectedRowContentProperty() {
return this.selectedRowContent;
}
public final Node getSelectedRowContent() {
return this.selectedRowContentProperty().get();
}
public final void setSelectedRowContent(final Node selectedRowContent) {
this.selectedRowContentProperty().set(selectedRowContent);
}
public ExpandingTableRow(Node selectedRowContent) {
super();
setSelectedRowContent(selectedRowContent);
}
public ExpandingTableRow() {
this(null);
}
#Override
protected Skin<?> createDefaultSkin() {
return new ExpandingTableRowSkin<T>(this);
}
}
The skin implementation has to do the layout work. It needs to override the methods that compute the height, accounting for the height of the extra content if needed, and it needs to override the layoutChildren() method, to position the additional content, if needed. Finally, it must manage the additional content, adding or removing the additional content if the selected state of the row changes (or if the additional content itself is changed).
import javafx.scene.control.skin.TableRowSkin;
public class ExpandingTableRowSkin<T> extends TableRowSkin<T> {
private ExpandingTableRow<T> row;
public ExpandingTableRowSkin(ExpandingTableRow<T> row) {
super(row);
this.row = row;
row.selectedRowContentProperty().addListener((obs, oldContent, newContent) -> {
if (oldContent != null) {
getChildren().remove(oldContent);
}
if (newContent != null && row.isSelected()) {
getChildren().add(newContent);
}
if (row.getTableView() != null) {
row.getTableView().requestLayout();
}
});
row.selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected && row.getSelectedRowContent() != null
&& !getChildren().contains(row.getSelectedRowContent())) {
getChildren().add(row.getSelectedRowContent());
} else {
getChildren().remove(row.getSelectedRowContent());
}
});
}
#Override
protected double computeMaxHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().maxHeight(width);
}
return super.computeMaxHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computeMinHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().minHeight(width);
}
return super.computeMinHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected double computePrefHeight(double width, double topInset, double rightInset, double bottomInset,
double leftInset) {
if (row.isSelected() && row.getSelectedRowContent() != null) {
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset)
+ row.getSelectedRowContent().prefHeight(width);
}
return super.computePrefHeight(width, topInset, rightInset, bottomInset, leftInset);
}
#Override
protected void layoutChildren(double x, double y, double w, double h) {
if (row.isSelected()) {
double rowHeight = super.computePrefHeight(w, snappedTopInset(), snappedRightInset(), snappedBottomInset(),
snappedLeftInset());
super.layoutChildren(x, y, w, rowHeight);
row.getSelectedRowContent().resizeRelocate(x, y + rowHeight, w, h - rowHeight);
} else {
super.layoutChildren(x, y, w, h);
}
}
}
Finally, a test (using the usual example from Oracle, or a version of it):
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.FlowPane;
import javafx.stage.Stage;
public class ExpandingTableRowTest extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Person> table = new TableView<>();
table.getColumns().add(column("First Name", Person::firstNameProperty));
table.getColumns().add(column("Last Name", Person::lastNameProperty));
table.setRowFactory(tv -> {
Label label = new Label();
FlowPane flowPane = new FlowPane(label);
TableRow<Person> row = new ExpandingTableRow<>(flowPane) {
#Override
protected void updateItem(Person person, boolean empty) {
super.updateItem(person, empty);
if (empty) {
label.setText(null);
} else {
label.setText(String.format("Some additional information about %s %s here",
person.getFirstName(), person.getLastName()));
}
}
};
return row;
});
table.getItems().addAll(
new Person("Jacob", "Smith"),
new Person("Isabella", "Johnson"),
new Person("Ethan", "Williams"),
new Person("Emma", "Jones"),
new Person("Michael", "Brown")
);
Scene scene = new Scene(table);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S, T> TableColumn<S, T> column(String title, Function<S, ObservableValue<T>> property) {
TableColumn<S, T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
return col;
}
public static class Person {
private final StringProperty firstName = new SimpleStringProperty();
private final StringProperty lastName = new SimpleStringProperty();
public Person(String firstName, String lastName) {
setFirstName(firstName);
setLastName(lastName);
}
public final StringProperty firstNameProperty() {
return this.firstName;
}
public final String getFirstName() {
return this.firstNameProperty().get();
}
public final void setFirstName(final String firstName) {
this.firstNameProperty().set(firstName);
}
public final StringProperty lastNameProperty() {
return this.lastName;
}
public final String getLastName() {
return this.lastNameProperty().get();
}
public final void setLastName(final String lastName) {
this.lastNameProperty().set(lastName);
}
}
public static void main(String[] args) {
launch(args);
}
}
As you can see, a little refinement of the style and sizing may be needed to get this production-ready, but this shows the approach that will work.

Related

Dynamically change style of multiple JavaFX TableRow

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();
}
}

Get the running count of an object in TableView in a column

I have two types of Objects in a table, for simplicity, I will call them Dog and Cat, both of these objects extend from Animal. I am attempting to have a running count of the objects. For the moment I will focus on the Dog object.
I have a TableColumn that I've created a value factory for, and it looks like this:
column.setCellFactory(callback -> new TableCell<>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(!empty) {
if (getTableView().getItems().get(getIndex()) instanceof Dog) {
int count = setDogCount(getTableView(), getIndex(), 0);
setText(String.valueOf(count));
} else {
setText("");
}
} else {
setText("");
}
}
});
The recursive method I use is here:
private int setDogCount(TableView<Animal> table, int index, int count){
if(index == 0){
if(table.getItems().get(index) instanceof Dog) {
return count + 1;
} else {
return count;
}
}
if(table.getItems().get(index) instanceof Dog){
return setDogCount(table, --index, ++count);
}else {
return setDogCount(table, --index, count);
}
}
This actually works about 95% of the time. The only times it breaks is when some sorts occur. It breaks because cell factories are only called when they are sorted, so if a sort does not occur, then it won't update the count. This leads to some multiple counts of Dog depending on the circumstances.
QUESTION:
So, is there a way to have it update only one column on sort? I would like to try to avoid refreshing the entire table, and I was hoping there is a better way.
Thanks!
Edit:
By "if a sort does not occur" I mean that if that object was not moved from its current index, it does not call the cell factory.
Edit 2:
Here is a picture to see what I am facing.
Before Sorting -> After Sorting:
As you can see, index 0 switched with index 3, thus changing the count column appropriately, but now I have no number 1, and two number 3s. This is because it only updates the rows that were switched.
Edit 3:
Here is the small application to see the changes. When you run it, please click on the Commands Known column once to see what I am up against.
import javafx.application.Application;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
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 Main extends Application {
#Override
public void start(Stage primaryStage) throws Exception{
BorderPane root = new BorderPane();
TableView<Animal> table = new TableView<>();
TableColumn<Animal, String> count = new TableColumn<>("Count");
TableColumn<Animal, String> name = new TableColumn<>("Name");
TableColumn<Animal, String> sound = new TableColumn<>("Sound");
TableColumn<Animal, String> commandsKnown = new TableColumn<>("Commands Known");
table.getColumns().addAll(count, name, sound, commandsKnown);
root.setCenter(table);
count.setCellFactory(callback -> new TableCell<>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (!empty) {
if (getTableView().getItems().get(getIndex()) instanceof Dog) {
int count = setDogCount(getTableView(), getIndex(), 0);
setText(String.valueOf(count));
} else {
setText("");
}
} else {
setText("");
}
}
});
name.setCellValueFactory(data -> data.getValue().nameProperty());
sound.setCellValueFactory(data -> data.getValue().soundProperty());
commandsKnown.setCellValueFactory(data -> {
if(data.getValue() instanceof Dog){
return ((Dog) data.getValue()).commandsKnownProperty();
}
return new SimpleStringProperty("");
});
ObservableList<Animal> animals = FXCollections.observableArrayList();
animals.add(new Dog("Tweeter", "Woof", "Sit, rollover, shake, drop"));
animals.add(new Dog("Sub Woofer", "Woof", "Sit, rollover, shake"));
animals.add(new Cat("Kitter Cat", "Meow"));
animals.add(new Dog("Bass", "Woof", "Sit, rollover, shake, fetch"));
table.setItems(animals);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private int setDogCount(TableView<Animal> table, int index, int count){
if(index == 0){
if(table.getItems().get(index) instanceof Dog) {
return count + 1;
} else {
return count;
}
}
if(table.getItems().get(index) instanceof Dog){
return setDogCount(table, --index, ++count);
}else {
return setDogCount(table, --index, count);
}
}
public static void main(String[] args) {
launch(args);
}
public class Animal{
StringProperty name = new SimpleStringProperty();
StringProperty sound = new SimpleStringProperty();
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getSound() {
return sound.get();
}
public StringProperty soundProperty() {
return sound;
}
public void setSound(String sound) {
this.sound.set(sound);
}
}
public class Dog extends Animal{
StringProperty commandsKnown = new SimpleStringProperty();
public Dog(String name, String sound, String commandsKnown){
setName(name);
setSound(sound);
setCommandsKnown(commandsKnown);
}
public String getCommandsKnown() {
return commandsKnown.get();
}
public StringProperty commandsKnownProperty() {
return commandsKnown;
}
public void setCommandsKnown(String commandsKnown) {
this.commandsKnown.set(commandsKnown);
}
}
public class Cat extends Animal{
public Cat(String name, String sound){
setName(name);
setSound(sound);
}
}
}
To expand a bit on my comment:
a cell definitely is the wrong place to change the data (even if it's
meta) ... do it in the model ;)
As model I meant not only your data object/list but also all state that is related to the data, even if it's also related to the view, as f.i. a running counter. You must model that relation somehow outside of the view.
Below is an example that models the relation by an additional list for the counters, one property per owner. It's the task of the app to keep it in sync with the owners as shown in the table whenever anything that effects the counters changes (f.i. when the list is sorted or the pet changed or anything).
The code:
public class TableWithExternalCounterSO extends Application {
/**
* Updates the counter data from the given source list, assuming that
* both have the same size (if that's not true, adjust counter size
* as needed)
*/
private void updateDogCounterFrom(ObservableList<ObjectProperty<Integer>> dogCounter,
ObservableList<? extends PetOwner> owners) {
int count = 0;
for (int i = 0; i < owners.size(); i++) {
PetOwner owner = owners.get(i);
if (owner.petProperty().get() == Pet.DOG) {
dogCounter.get(i).set(++count);
} else {
dogCounter.get(i).set(-1);
}
}
}
private Parent createContent() {
// the base data
ObservableList<PetOwner> owners = PetOwner.owners();
// a list for the counters, that must be kept in sync with changes in the table
ObservableList<ObjectProperty<Integer>> dogCounter = FXCollections.observableArrayList();
owners.forEach(owner -> dogCounter.add(new SimpleObjectProperty<Integer>(-1)));
// initial sync
updateDogCounterFrom(dogCounter, owners);
SortedList<PetOwner> sorted = new SortedList<>(owners);
sorted.addListener((ListChangeListener<? super PetOwner>) c -> {
// sync after change
updateDogCounterFrom(dogCounter, c.getList());
});
TableView<PetOwner> table = new TableView<>(sorted);
sorted.comparatorProperty().bind(table.comparatorProperty());
TableColumn<PetOwner, String> name = new TableColumn<>("Name");
name.setCellValueFactory(new PropertyValueFactory<>("name"));
TableColumn<PetOwner, Pet> pet = new TableColumn<>("Pet");
pet.setCellValueFactory(new PropertyValueFactory<>("pet"));
TableColumn<PetOwner, Integer> dogIndex = new TableColumn<>("Running Dog#");
dogIndex.setSortable(false);
dogIndex.setCellValueFactory(cd -> {
// astonishingly, this is called for every cell after sorting,
// that is all cells are newly created
int index = cd.getTableView().getItems().indexOf(cd.getValue());
return dogCounter.get(index);
});
dogIndex.setCellFactory(cb -> {
return new TableCell<PetOwner, Integer>() {
#Override
protected void updateItem(Integer item, boolean empty) {
super.updateItem(item, empty);
if (empty || item != null && item.intValue() < 0) {
setText("");
} else {
setText(String.valueOf(item));
}
}
};
});
table.getColumns().addAll(name, pet, dogIndex);
BorderPane pane = new BorderPane(table);
return pane;
}
private enum Pet {
CAT, DOG
}
public static class PetOwner {
ObjectProperty<Pet> pet;
StringProperty name;
PetOwner(String name, Pet pet) {
this.pet = new SimpleObjectProperty<>(this, "pet", pet);
this.name = new SimpleStringProperty(this, "name", name);
}
public ObjectProperty<Pet> petProperty() {
return pet;
}
public StringProperty nameProperty() {
return name;
}
public static ObservableList<PetOwner> owners() {
ObservableList<PetOwner> owners = FXCollections.observableArrayList();
for (int i = 0; i < 20; i++) {
owners.add(new PetOwner("O " + i, i % 3 == 0 ? Pet.CAT : Pet.DOG) );
}
return owners;
}
#Override
public String toString() {
return name.get( ) + " " + pet.get();
}
}
I suggest you use a static variable to keep up with the Dog's ID.
Example code below:
Full Code:
Dog Class
/**
*
* #author blj0011
*/
public class Dog extends Animal
{
static int dogCounter = 0;
private String commnads;
private final int dogId;
public Dog(String name, String sound, String commands)
{
super(name, sound);
this.commnads = commands;
dogId = ++dogCounter;
}
public String getCommnads()
{
return commnads;
}
public void setCommnads(String commnads)
{
this.commnads = commnads;
}
public int getDogId()
{
return dogId;
}
}
Cat Class
/**
*
* #author blj0011
*/
public class Cat extends Animal
{
public Cat(String name, String sound)
{
super(name, sound);
}
}
Main
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application
{
#Override
public void start(Stage primaryStage) throws Exception
{
BorderPane root = new BorderPane();
TableView<Animal> table = new TableView<>();
TableColumn<Animal, String> count = new TableColumn<>("Count");
TableColumn<Animal, String> name = new TableColumn<>("Name");
TableColumn<Animal, String> sound = new TableColumn<>("Sound");
TableColumn<Animal, String> commandsKnown = new TableColumn<>("Commands Known");
table.getColumns().addAll(count, name, sound, commandsKnown);
root.setCenter(table);
count.setCellValueFactory(new PropertyValueFactory("dogId"));
name.setCellValueFactory(new PropertyValueFactory("name"));
sound.setCellValueFactory(new PropertyValueFactory("sound"));
commandsKnown.setCellValueFactory(new PropertyValueFactory("commands"));
ObservableList<Animal> animals = FXCollections.observableArrayList();
animals.add(new Dog("Tweeter", "Woof", "Sit, rollover, shake, drop"));
animals.add(new Dog("Sub Woofer", "Woof", "Sit, rollover, shake"));
animals.add(new Cat("Kitter Cat", "Meow"));
animals.add(new Dog("Bass", "Woof", "Sit, rollover, shake, fetch"));
table.setItems(animals);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
private int setDogCount(TableView<Animal> table, int index, int count)
{
if (index == 0) {
if (table.getItems().get(index) instanceof Dog) {
return count + 1;
}
else {
return count;
}
}
if (table.getItems().get(index) instanceof Dog) {
return setDogCount(table, --index, ++count);
}
else {
return setDogCount(table, --index, count);
}
}
public static void main(String[] args)
{
launch(args);
}
}
SortedList code:
import java.util.Comparator;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.collections.transformation.SortedList;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application
{
#Override
public void start(Stage primaryStage) throws Exception
{
BorderPane root = new BorderPane();
TableView<Animal> table = new TableView<>();
TableColumn<Animal, String> count = new TableColumn<>("Count");
TableColumn<Animal, String> name = new TableColumn<>("Name");
TableColumn<Animal, String> sound = new TableColumn<>("Sound");
TableColumn<Animal, String> commandsKnown = new TableColumn<>("Commands Known");
table.getColumns().addAll(count, name, sound, commandsKnown);
root.setCenter(table);
count.setCellValueFactory(new PropertyValueFactory("dogId"));
name.setCellValueFactory(new PropertyValueFactory("name"));
sound.setCellValueFactory(new PropertyValueFactory("sound"));
commandsKnown.setCellValueFactory(new PropertyValueFactory("commands"));
ObservableList<Animal> animals = FXCollections.observableArrayList();
animals.add(new Dog("Tweeter", "Woof", "Sit, rollover, shake, drop"));
animals.add(new Dog("Sub Woofer", "Woof", "Sit, rollover, shake"));
animals.add(new Cat("Kitter Cat", "Meow"));
animals.add(new Cat("Kitter Cat 2", "Meow"));
animals.add(new Dog("Bass", "Woof", "Sit, rollover, shake, fetch"));
SortedList<Animal> sortedList = new SortedList(animals);
Comparator<Animal> comparator = (c, d) -> {
if (c instanceof Cat && d instanceof Dog) {
return -1;
}
else if (c instanceof Dog && d instanceof Cat) {
return 1;
}
return 0;
};
comparator.thenComparing((c, d) -> {
if (c instanceof Cat && d instanceof Dog) {
return -1;
}
else if (c instanceof Dog && d instanceof Cat) {
return 1;
}
else {
if (c instanceof Cat && d instanceof Cat) {
return c.getName().compareTo(d.getName());
}
else {
Dog tempDog1 = (Dog) c;
Dog tempDog2 = (Dog) d;
if (tempDog1.getDogId() > tempDog2.getDogId()) {
return 1;
}
}
return 0;
}
});
sortedList.setComparator(comparator);
table.setItems(sortedList);
primaryStage.setScene(new Scene(root));
primaryStage.show();
}
public static void main(String[] args)
{
launch(args);
}
}

JavaFX TableView get row from button cell

Referring to question JavaFX TableView custom cell rendering split menu button, i'm able to render split menu button in every row. I've updated my code as suggested by James_D and in the answer by Keyur Bhanderi.
The question is about get value of the row where split menu is located without selecting row before click.
Update: Added images to view output
The images below show the output, every button i click.
Updated SplitMenuCellFactory.java
package com.example.splimenubtn;
import java.util.List;
import javafx.scene.control.ContentDisplay;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
public class SplitMenuCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private List<MenuItemFactory<T>> menuItems;
public SplitMenuCellFactory() {}
public SplitMenuCellFactory(List<MenuItemFactory<T>> items) {
menuItems = items;
}
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
setText(null);
} else {
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
if (getTableRow() != null) {
setGraphic(new SplitMenuButtonFactory<>(menuItems, getTableRow().getIndex()).buildButton());
}
}
}
};
}
}
Update, adding missing class
SplitMenuButtonFactory.java
package com.example.splimenubtn;
import java.util.List;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SplitMenuButton;
public class SplitMenuButtonFactory<T> {
private List<MenuItemFactory<T>> menuItems;
private int rIndex = 0;
public SplitMenuButtonFactory(List<MenuItemFactory<T>> items) {
menuItems = items;
}
public SplitMenuButtonFactory(List<MenuItemFactory<T>> items, int rI) {
menuItems = items;
rIndex = rI;
}
public SplitMenuButton buildButton() {
SplitMenuButton menuBtn = new SplitMenuButton();
// menuBtn.getItems().addAll(menuItems);
for (MenuItemFactory<?> mIF : menuItems) {
MenuItem btn = mIF.setRowIndex(rIndex).buildMenuItem();
if (mIF.isDefault()) {
menuBtn.setText(btn.getText());
menuBtn.setOnAction(btn.getOnAction());
}
menuBtn.getItems().add(btn);
}
return menuBtn;
}
}
MenuItemsFactory.java
package com.example.splimenubtn;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.control.MenuItem;
import javafx.scene.control.TableView;
public class MenuItemFactory<S> {
private MenuItemActions itemType;
private String itemLbl;
private TableView<S> table;
private boolean defaultAction;
private int rIndex = 0;
public MenuItemFactory(MenuItemActions itemType, String itemLabel, boolean dA) {
this.itemType = itemType;
itemLbl = itemLabel;
defaultAction = dA;
}
public MenuItemFactory(MenuItemActions itemType, String itemLabel, TableView<S> t, boolean dA) {
this.itemType = itemType;
itemLbl = itemLabel;
defaultAction = dA;
table = t;
}
public MenuItemFactory<S> setDataList(TableView<S> t) {
table = t;
return this;
}
public boolean isDefault() {
return defaultAction;
}
public MenuItemFactory<S> setRowIndex(int rI) {
rIndex = rI;
return this;
}
public MenuItem buildMenuItem() {
MenuItem mI = new MenuItem();
switch (itemType) {
case DETAILS:
mI.setText(itemLbl);
mI.setOnAction(handleDetails());
break;
case EDIT:
mI.setText(itemLbl);
mI.setOnAction(handleEdit());
break;
case DELETE:
mI.setText(itemLbl);
mI.setOnAction(handleDelete());
break;
default:
break;
}
return mI;
}
private EventHandler<ActionEvent> handleDetails() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DETAIL REQUEST ***");
}
};
}
private EventHandler<ActionEvent> handleEdit() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** EDIT REQUESTED ***");
table.getSelectionModel().select(rIndex);
System.out.println("*** " + table.getSelectionModel().getSelectedItem().toString() + " ***");
}
};
}
private EventHandler<ActionEvent> handleDelete() {
return new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent aE) {
System.out.println("*** DELETE REQUESTED ***");
System.out.println("*** " + table.getSelectionModel().getSelectedItem().toString() + " ***");
}
};
}
}
But when i click on the button, i'm getting always last value.
How can i get the object in the row where button is?
Any help or suggestion that point me to right direction is appreciated.
Simply use the TableCell to retrieve the value in the onAction event handler (or whatever you use in the product of the SplitMenuButtonFactory you're not showing to us).
Simplified example
public static SplitMenuButton createSplitMenuButton(final TableCell cell) {
SplitMenuButton result = new SplitMenuButton();
result.setOnAction(evt -> {
TableRow row = cell.getTableRow();
System.out.println("row item: " +row.getItem());
});
return result;
}
Furthermore it's better to reuse the SplitMenuButton in the cell and updating it instead of recerating it every time the cell item changes.
#Override
public TableCell<S, T> call(TableColumn<S, T> param) {
return new TableCell<S, T>() {
private final SplitMenuButton button = createSplitMenuButton(this);
{
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
updateMenuButton(button, item); // placeholder for updating the button according to the new item
setGraphic(button);
}
}
};
}

How do i add multiple combo boxes to JavaFX that once a item is selected the cost is display underneath?

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();
}

Type to edit in TableView

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);
}
});

Resources