Make individual cell editable in JavaFX tableview - javafx

I am writing a program that displays a JavaFX table. I understand how to make all the data in a specific column editable via "Column.setCellFactory(TextFieldTableCell.forTableColumn());"
However I would like to make some of the cells editable and others immutable. Is this possible? Moreover, I would like editable cells to either have a border or have a unique font color.

Yes, this is possible: the TableCell has an editable property inherited from the Cell class. You need to arrange that the cell sets its editable property accordingly when the item changes (and possibly if the condition governing when it should be editable changes).
In the example below, I create a default cell factory using TextFieldTableCell.forTableColumn(), and then create another cell factory. The custom cell factory invokes the default cell factory (to get the standard TextField behavior), then observes the itemProperty of the cell and updates the editableProperty accordingly (in this simple example, only cells with an even value are editable).
To add the border, you need to update the style somehow. The best way to do this is to define a pseudoclass for "editable" and use an external style sheet to manage the style for editable cells.
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ConditionallyEditableTableCell extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setEditable(true);
TableColumn<Item, String> nameCol = createCol("Name", Item::nameProperty);
TableColumn<Item, Number> canEditCol = createCol("Value", Item::valueProperty);
PseudoClass editableCssClass = PseudoClass.getPseudoClass("editable");
Callback<TableColumn<Item, String>, TableCell<Item, String>> defaultTextFieldCellFactory
= TextFieldTableCell.<Item>forTableColumn();
nameCol.setCellFactory(col -> {
TableCell<Item, String> cell = defaultTextFieldCellFactory.call(col);
cell.itemProperty().addListener((obs, oldValue, newValue) -> {
TableRow row = cell.getTableRow();
if (row == null) {
cell.setEditable(false);
} else {
Item item = (Item) cell.getTableRow().getItem();
if (item == null) {
cell.setEditable(false);
} else {
cell.setEditable(item.getValue() % 2 == 0);
}
}
cell.pseudoClassStateChanged(editableCssClass, cell.isEditable());
});
return cell ;
});
table.getColumns().addAll(canEditCol, nameCol);
for (int i=1; i<=20; i++) {
table.getItems().add(new Item("Item "+i, i));
}
Scene scene = new Scene(new BorderPane(table), 600, 400);
scene.getStylesheets().add("conditionally-editable-table-cell.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private <S,T> TableColumn<S,T> createCol(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 Item {
private final IntegerProperty value = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
And the stylesheet, conditionally-editable-table-cell.css:
.table-cell:editable {
-fx-border-color: red ;
}

Related

How can I style a TableView row adjacent to a selected row?

How can I style a TableView row that is adjacent to the currently selected row?
The reason I want to do this is to change the color of the border around the row, both top and bottom, while the row is selected.
The way I currently style the cell borders (i.e., the table's grid) is by having a background color with a 1px inset on the bottom of the cell. Essentially, each cell draws its own bottom border.
This means that if I want to change both the "top" and bottom border of a selected row, I also need to be able to style the cell above the currently selected cell.
You can add a listener to the table row's index, and the table's selected index, which updates a custom CSS pseudoclass on the table row:
TableView<MyTableType> table = ... ;
PseudoClass beforeSelected = PseudoClass.getPseudoClass("before-selected");
table.setRowFactory(tv -> {
TableRow<MyTableType> row = new TableRow<>();
ChangeListener<Number> listener = (obs, oldValue, newValue) -> {
if (row.isEmpty()) {
row.pseudoClassStateChanged(beforeSelected, false);
} else {
row.pseudoClassStateChanged(beforeSelected,
row.getIndex() == table.getSelectionModel().getSelectedIndex() - 1);
}
};
row.indexProperty().addListener(listener);
table.getSelectionModel().selectedIndexProperty().addListener(listener);
return row ;
});
Then in your CSS file, you can style the table row preceding the selected row with the selector
.table-row-cell:before-selected {
/* styles here... */
}
Here's a complete working example:
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.Property;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ChangeListener;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class StyleTest extends Application {
#Override
public void start(Stage stage) throws Exception {
TableView<Item> table = new TableView<>();
PseudoClass beforeSelected = PseudoClass.getPseudoClass("before-selected");
table.setRowFactory(tv -> {
TableRow<Item> row = new TableRow<>();
ChangeListener<Number> listener = (obs, oldValue, newValue) -> {
if (row.isEmpty()) {
row.pseudoClassStateChanged(beforeSelected, false);
} else {
row.pseudoClassStateChanged(beforeSelected,
row.getIndex() == table.getSelectionModel().getSelectedIndex() - 1);
}
};
row.indexProperty().addListener(listener);
table.getSelectionModel().selectedIndexProperty().addListener(listener);
return row ;
});
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
for (int i=1 ; i <= 100 ; i++) {
table.getItems().add(new Item("Item "+i, i));
}
Scene scene = new Scene(new BorderPane(table));
scene.getStylesheets().add(getClass().getResource("style.css").toExternalForm());
stage.setScene(scene);
stage.show();
}
private static <S,T> TableColumn<S,T> column(String title, Function<S, Property<T>> prop) {
TableColumn<S,T> column = new TableColumn<>(title);
column.setCellValueFactory(cellData -> prop.apply(cellData.getValue()));
return column ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
Application.launch(args);
}
}
and an example style.css (which just colors the previous row green...):
.table-row-cell:before-selected {
-fx-background: #00b140 ;
}

JavaFX - bind properties of corresponding TableRows in different TableViews

I have two TableViews in the same scene that are closely related. I want to set up a listener such that when the user hovers a certain row in one table, the row with the same index in the other table is "hovered" as well.
I'm trying to solve this with a custom row factory tableView.setRowFactory(...). Inside the factory call(...) method I can toggle a CSS pseudo-class (.myclass:hover) on the target row, like:
row.hoverProperty().addListener((obs, o, n) -> {
myOtherTable.[get row here].pseudoClassStateChanged(PseudoClass.getPseudoClass("hover"), true);
});
As you can see in my factory method I have a reference to the second TableView object, myOtherTable. I guess I have to get hold of its TableRow objects to go ahead and set the pseudo class, but I can't figure out how.
Maybe is there a better way to do this?
Create a single property representing the index of the hovered row, and a PseudoClass:
IntegerProperty hoveredRowIndex = new SimpleIntegerProperty(-1);
PseudoClass appearHovered = PseudoClass.getPseudoClass("appear-hovered");
Now create a row factory that creates table rows that observe this value and their own index:
Callback<TableView<T>, TableCell<T>> rowFactory = tv -> {
TableRow<T> row = new TableRow<T>() {
private BooleanBinding shouldAppearHovered = Bindings.createBooleanBinding(
() -> getIndex() != -1 && getIndex() == hoveredRowIndex.get(), indexProperty(),
hoveredRowIndex);
{
shouldAppearHovered.addListener(
(obs, wasHovered, isNowHovered) -> pseudoClassStateChanged(appearHovered, isNowHovered));
hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered) {
hoveredRowIndex.set(getIndex());
} else {
hoveredRowIndex.set(-1);
}
});
}
};
return row;
};
(Replace T with the actual type of the table.)
And now use the row factory for both tables. You can use the CSS selector
.table-row-cell:appear-hovered {
/* ... */
}
to style the rows that should appear to be hovered, or use
.table-row-cell:appear-hovered .table-cell {
/* ... */
}
to style individual cells in that row.
Here's a SSCCE:
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.css.PseudoClass;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
public class ConnectedHoverTables extends Application {
private IntegerProperty hoveredRowIndex = new SimpleIntegerProperty(-1);
private PseudoClass appearHovered = PseudoClass.getPseudoClass("appear-hovered");
#Override
public void start(Stage primaryStage) {
HBox root = new HBox(10, createTable(), createTable());
root.setPadding(new Insets(20));
Scene scene = new Scene(root);
scene.getStylesheets().add("style.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private TableView<Item> createTable() {
TableView<Item> table = new TableView<>();
table.setRowFactory(tv -> {
TableRow<Item> row = new TableRow<Item>() {
private BooleanBinding shouldAppearHovered = Bindings.createBooleanBinding(
() -> getIndex() != -1 && getIndex() == hoveredRowIndex.get(), indexProperty(),
hoveredRowIndex);
{
shouldAppearHovered.addListener(
(obs, wasHovered, isNowHovered) -> pseudoClassStateChanged(appearHovered, isNowHovered));
hoverProperty().addListener((obs, wasHovered, isNowHovered) -> {
if (isNowHovered) {
hoveredRowIndex.set(getIndex());
} else {
hoveredRowIndex.set(-1);
}
});
}
};
return row;
});
table.setOnMouseClicked(e -> System.gc());
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
table.getItems().setAll(createData());
return table;
}
private List<Item> createData() {
Random rng = new Random();
List<Item> items = new ArrayList<>();
for (int i = 1; i <= 100; i++) {
Item item = new Item("Item " + i, rng.nextInt(1000));
items.add(item);
}
return items;
}
private <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 Item {
private final StringProperty name = new SimpleStringProperty();
private final IntegerProperty value = new SimpleIntegerProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
If I remember correctly, you can't access directly a row of a TableView. The only way to get the row index is to access the attribute indexProperty when you define the CellFactory.
I advise you to create rather a personalized extending TableRow or TableCell object where you can stock an id or something like that...

JavaFX TableView: open detail information between rows on click

I'm trying to display travel connections in a TableView. So far that works like a charm. Now I'm kinda stuck trying to get details of a connection to be displayed in between table rows. This should happen on selecting a table item.
The problem is, that the details are in a different format than the connections I'm displaying. So I would need to put a panel between two table rows. Is this at all possible?
The "proper" way to do this would be to create a custom skin for TableRow and use a rowFactory on the table that returned a TableRow with the custom skin installed. However, since skin classes are not public API at the time of writing (note though that they will be in Java 9), this would mean implementing the skin class entirely from scratch (laying out the table cells, etc), which would be pretty difficult.
A less "official" approach, but one that's a little easier, is just to override the various layout methods in the TableRow directly, and hook into the superclass implementation.
This works, but feels a little fragile:
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Node;
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.BorderPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
public class TableWithCustomRow extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.setRowFactory(tv -> new TableRow<Item>() {
Node detailsPane ;
{
selectedProperty().addListener((obs, wasSelected, isNowSelected) -> {
if (isNowSelected) {
getChildren().add(detailsPane);
} else {
getChildren().remove(detailsPane);
}
this.requestLayout();
});
detailsPane = createDetailsPane(itemProperty());
}
#Override
protected double computePrefHeight(double width) {
if (isSelected()) {
return super.computePrefHeight(width)+detailsPane.prefHeight(getWidth());
} else {
return super.computePrefHeight(width);
}
}
#Override
protected void layoutChildren() {
super.layoutChildren();
if (isSelected()) {
double width = getWidth();
double paneHeight = detailsPane.prefHeight(width);
detailsPane.resizeRelocate(0, getHeight()-paneHeight, width, paneHeight);
}
}
});
Random random = new Random();
for (int i = 1 ; i <= 100 ; i++) {
table.getItems().add(new Item("Item "+i, random.nextInt(100)));
}
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
Scene scene = new Scene(table, 800, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private Node createDetailsPane(ObjectProperty<Item> item) {
BorderPane detailsPane = new BorderPane();
Label detailsLabel = new Label();
VBox labels = new VBox(5, new Label("These are the"), detailsLabel);
labels.setAlignment(Pos.CENTER_LEFT);
labels.setPadding(new Insets(2, 2, 2, 16));
detailsPane.setCenter(labels);
Label icon = new Label("Icon");
icon.setStyle("-fx-background-color: aqua; -fx-text-fill: darkgreen; -fx-font-size:18;");
BorderPane.setMargin(icon, new Insets(6));
icon.setMinSize(40, 40);
detailsPane.setLeft(icon);
detailsPane.setStyle("-fx-background-color: -fx-background; -fx-background: skyblue;");
item.addListener((obs, oldItem, newItem) -> {
if (newItem == null) {
detailsLabel.setText("");
} else {
detailsLabel.setText("details for "+newItem.getName());
}
});
return detailsPane ;
}
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()));
col.setPrefWidth(150);
return col ;
}
public static class Item {
private final StringProperty name = new SimpleStringProperty() ;
private final IntegerProperty value = new SimpleIntegerProperty() ;
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final java.lang.String getName() {
return this.nameProperty().get();
}
public final void setName(final java.lang.String name) {
this.nameProperty().set(name);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
}
public static void main(String[] args) {
launch(args);
}
}
This gives the following:

JavaFX TableView: copy text as rendered in cell

I want to implement copy functionality in a TableView. The text to be copied should be the actual text that is rendered in the cell, not the .toString version of the data model to be rendered, that is, it should be the .getText of the cell.
There are several ways of getting the data from a cell. However to get the rendered cell text contents, the procedure seems to be like this:
Get the cell data.
Get the cell factory.
Use the factory to create a cell.
Use the cell's updateItem method to render the data, then getText to get the rendered text.
The last step is not possible due to updateItem being protected.
How can I access the rendered text of any given cell in a TableView?
The process you outline involves getting the text (i.e. data) from the view (the cell), which violates the principles behind the MVC/MVP design. From a practical perspective, it involves creating UI elements (which are expensive to create) to essentially manipulate data (which is typically much less expensive to create and process). Additionally, depending on exactly what you're doing, the UI elements may impose additional threading constraints on your code (as they are essentially single-threaded).
If you need to use the "formatting text" functionality outside of the cell, you should factor it out elsewhere and reuse it in both the "copy" functionality you need and in the cell. At a minimum, this could be done by making the "format text" functionality part of the cell factory:
import java.util.function.Function;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.util.Callback;
public class FormattingTableCellFactory<S, T> implements Callback<TableColumn<S, T>, TableCell<S, T>> {
private final Function<T, String> formatter ;
public FormattingTableCellFactory(Function<T, String> formatter) {
this.formatter = formatter ;
}
public FormattingTableCellFactory() {
this(T::toString);
}
public final Function<T, String> getFormatter() {
return formatter ;
}
#Override
public TableCell<S,T> call(TableColumn<S,T> col) {
return new TableCell<S,T>() {
#Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
setText(item == null ? null : formatter.apply(item));
}
};
}
}
(Obviously you could extend this to produce more sophisticated cells with graphical content, etc.)
And now your copy functionality can simply apply the formatter to the data, without reference to any actual cells. Here's a SSCCE:
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.function.Function;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.DoubleProperty;
import javafx.beans.property.SimpleDoubleProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.SelectionMode;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class Main extends Application {
private String copy(TableView<Product> table) {
StringBuilder sb = new StringBuilder();
for (Product p : table.getSelectionModel().getSelectedItems()) {
List<String> data = new ArrayList<>();
for (TableColumn<Product, ?> column : table.getColumns()) {
Function<Object, String> formatter = ((FormattingTableCellFactory) column.getCellFactory()).getFormatter();
data.add(formatter.apply(column.getCellObservableValue(p).getValue()));
}
sb.append(String.join("\t", data)).append("\n");
}
return sb.toString() ;
}
#Override
public void start(Stage primaryStage) {
TableView<Product> table = new TableView<>();
table.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
table.getColumns().add(column("Product", Product::nameProperty, String::toString));
NumberFormat currencyFormat = NumberFormat.getCurrencyInstance();
table.getColumns().add(column("Price", Product::priceProperty, currencyFormat::format));
Random rng = new Random();
for (int i = 1; i <= 100; i++) {
table.getItems().add(new Product("Product "+i, rng.nextDouble()*100));
}
Button copy = new Button("Copy");
copy.setOnAction(e -> System.out.println(copy(table)));
copy.disableProperty().bind(Bindings.isEmpty(table.getSelectionModel().getSelectedItems()));
BorderPane root = new BorderPane(table);
BorderPane.setAlignment(copy, Pos.CENTER);
BorderPane.setMargin(copy, new Insets(10));
root.setBottom(copy);
Scene scene = new Scene(root, 600, 600);
primaryStage.setScene(scene);
primaryStage.show();
}
private static <S,T> TableColumn<S,T> column(String title, Function<S,ObservableValue<T>> property, Function<T,String> formatter) {
TableColumn<S,T> col = new TableColumn<>(title);
col.setCellValueFactory(cellData -> property.apply(cellData.getValue()));
col.setCellFactory(new FormattingTableCellFactory<>(formatter));
return col ;
}
public static class Product {
private final StringProperty name = new SimpleStringProperty();
private final DoubleProperty price = new SimpleDoubleProperty() ;
public Product(String name, double price) {
setName(name);
setPrice(price);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
public final DoubleProperty priceProperty() {
return this.price;
}
public final double getPrice() {
return this.priceProperty().get();
}
public final void setPrice(final double price) {
this.priceProperty().set(price);
}
}
public static void main(String[] args) {
launch(args);
}
}
You can get rid of the less typesafe code at the expense of less flexibility:
private final Function<String, String> defaultFormatter = Function.identity() ;
private final Function<Number, String> priceFormatter = DecimalFormat.getCurrencyInstance()::format ;
private String copy(TableView<Product> table) {
return table.getSelectionModel().getSelectedItems().stream().map(product ->
String.format("%s\t%s",
defaultFormatter.apply(product.getName()),
priceFormatter.apply(product.getPrice()))
).collect(Collectors.joining("\n"));
}
and
table.getColumns().add(column("Product", Product::nameProperty, defaultFormatter));
table.getColumns().add(column("Price", Product::priceProperty, priceFormatter));

Determine a JavaFX table row details when reusing tableview context menu

I have a requirement similar to this example.
In the EventHandler callback, how do I determine which row was clicked on?
#Override
public void handle(ActionEvent event) {
// how do I get the row details when reusing context menu and handler code?
}
I am sharing the context menu because I have to add a CheckMenuItem who's state is "global" to the table, i.e. if its selected on any row, I want to show it as checked when I click on any other row in the table.
Use a row factory and one context menu per row, as in the question you linked.
For the "global" CheckMenuItem, create a BooleanProperty and bidirectionally bind the CheckMenuItems' selected properties to it.
SSCCE:
import java.util.function.Function;
import java.util.stream.IntStream;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.scene.Scene;
import javafx.scene.control.CheckMenuItem;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.control.SeparatorMenuItem;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableRow;
import javafx.scene.control.TableView;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
public class TableWithContextMenu extends Application {
#Override
public void start(Stage primaryStage) {
TableView<Item> table = new TableView<>();
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
BooleanProperty globalSelection = new SimpleBooleanProperty();
table.setRowFactory(t -> {
TableRow<Item> row = new TableRow<>();
ContextMenu contextMenu = new ContextMenu();
MenuItem item1 = new MenuItem("Do something");
item1.setOnAction(e -> System.out.println("Do something with "+row.getItem().getName()));
MenuItem item2 = new MenuItem("Do something else");
item2.setOnAction(e -> System.out.println("Do something else with "+row.getItem().getName()));
CheckMenuItem item3 = new CheckMenuItem("Global selection");
item3.selectedProperty().bindBidirectional(globalSelection);
contextMenu.getItems().addAll(item1, item2, new SeparatorMenuItem(), item3);
row.emptyProperty().addListener((obs, wasEmpty, isEmpty) -> {
if (isEmpty) {
row.setContextMenu(null);
} else {
row.setContextMenu(contextMenu);
}
});
return row ;
});
IntStream.rangeClosed(1, 25).mapToObj(i -> new Item("Item "+i, i)).forEach(table.getItems()::add);
primaryStage.setScene(new Scene(new BorderPane(table), 800, 600));
primaryStage.show();
}
private <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 Item {
private final IntegerProperty value = new SimpleIntegerProperty();
private final StringProperty name = new SimpleStringProperty();
public Item(String name, int value) {
setName(name);
setValue(value);
}
public final IntegerProperty valueProperty() {
return this.value;
}
public final int getValue() {
return this.valueProperty().get();
}
public final void setValue(final int value) {
this.valueProperty().set(value);
}
public final StringProperty nameProperty() {
return this.name;
}
public final String getName() {
return this.nameProperty().get();
}
public final void setName(final String name) {
this.nameProperty().set(name);
}
}
public static void main(String[] args) {
launch(args);
}
}

Resources