Apply css on specific rows on javaFX - javafx

I'm using a TreeTableView in my project, and I would like to do something specific when the user selects a row:
I would like this row to have a different background color, but i would also like its childs and parents to have this color too.
I found a way to access every rows and children, but I just don't know how to specify this background color. I tried to do this using my customs TreeTableCells and adding the style in my updateItem method, but this method is not called each time an item is selected.
So i wanted to try to add listener in my treetableview, which seems to be a better idea, but in fact i'm not able to access the rows to give them any style.

The basic strategy here is:
Create CSS PseudoClass instances for the conditions you want to highlight (in the example below I created one for "child of selected" and one for "parent of selected")
Use a rowFactory to create rows for the table. The rows should update their pseudoclass state in the updateItem method, and should also update their pseudoclass state if the selected items change. You can place a listener on the table's selected items to do the second of these.
Add CSS in the external CSS file to style the rows the way you want, using the pseudoclasses you defined in the first step.
Here is 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.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.ListChangeListener.Change;
import javafx.css.PseudoClass;
import javafx.scene.Scene;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeTableColumn;
import javafx.scene.control.TreeTableRow;
import javafx.scene.control.TreeTableView;
import javafx.stage.Stage;
public class TreeTableViewHighlightSelectedPath extends Application {
private PseudoClass childOfSelected = PseudoClass.getPseudoClass("child-of-selected");
private PseudoClass parentOfSelected = PseudoClass.getPseudoClass("parent-of-selected");
#Override
public void start(Stage primaryStage) {
TreeTableView<Item> table = new TreeTableView<>(createRandomTree(50));
table.setRowFactory(ttv -> {
TreeTableRow<Item> row = new TreeTableRow<Item>() {
#Override
protected void updateItem(Item item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
pseudoClassStateChanged(parentOfSelected, false);
pseudoClassStateChanged(childOfSelected, false);
} else {
updateState(this);
}
}
};
table.getSelectionModel().getSelectedItems().addListener(
(Change<? extends TreeItem<Item>> c) -> updateState(row));
return row ;
});
table.getColumns().add(column("Item", Item::nameProperty));
table.getColumns().add(column("Value", Item::valueProperty));
Scene scene = new Scene(table, 800, 800);
scene.getStylesheets().add("table-row-highlight.css");
primaryStage.setScene(scene);
primaryStage.show();
}
private <T> void updateState(TreeTableRow<T> row) {
TreeTableView<T> table = row.getTreeTableView() ;
TreeItem<T> item = row.getTreeItem();
// if item is selected, just use default "selected" highlight,
// and set "child-of-selected" and "parent-of-selected" to false:
if (item == null || table.getSelectionModel().getSelectedItems().contains(item)) {
row.pseudoClassStateChanged(childOfSelected, false);
row.pseudoClassStateChanged(parentOfSelected, false);
return ;
}
// check to see if item is parent of any selected item:
for (TreeItem<T> selectedItem : table.getSelectionModel().getSelectedItems()) {
for (TreeItem<T> parent = selectedItem.getParent(); parent != null ; parent = parent.getParent()) {
if (parent == item) {
row.pseudoClassStateChanged(parentOfSelected, true);
row.pseudoClassStateChanged(childOfSelected, false);
return ;
}
}
}
// check to see if item is child of any selected item:
for (TreeItem<T> ancestor = item.getParent() ; ancestor != null ; ancestor = ancestor.getParent()) {
if (table.getSelectionModel().getSelectedItems().contains(ancestor)) {
row.pseudoClassStateChanged(childOfSelected, true);
row.pseudoClassStateChanged(parentOfSelected, false);
return ;
}
}
// if we got this far, clear both pseudoclasses:
row.pseudoClassStateChanged(childOfSelected, false);
row.pseudoClassStateChanged(parentOfSelected, false);
}
private <S,T> TreeTableColumn<S,T> column(String title, Function<S, ObservableValue<T>> property) {
TreeTableColumn<S,T> column = new TreeTableColumn<>(title);
column.setCellValueFactory(cellData -> property.apply(cellData.getValue().getValue()));
return column ;
}
private TreeItem<Item> createRandomTree(int numNodes) {
Random rng = new Random();
TreeItem<Item> root = new TreeItem<>(new Item("Item 1", rng.nextInt(1000)));
root.setExpanded(true);
List<TreeItem<Item>> items = new ArrayList<>();
items.add(root);
for (int i = 2 ; i <= numNodes; i++) {
Item item = new Item("Item "+i, rng.nextInt(1000));
TreeItem<Item> treeItem = new TreeItem<>(item);
treeItem.setExpanded(true);
items.get(rng.nextInt(items.size())).getChildren().add(treeItem);
items.add(treeItem);
}
return root ;
}
public static class Item {
private StringProperty name = new SimpleStringProperty();
private 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);
}
#Override
public String toString() {
return String.format("%s (%d)", getName(), getValue());
}
}
public static void main(String[] args) {
launch(args);
}
}
and the CSS file (table-row-highlight.css):
.tree-table-row-cell:child-of-selected {
-fx-background: green ;
}
.tree-table-row-cell:parent-of-selected {
-fx-background: salmon ;
}
This give the following:
This version highlights all descendant nodes and all ancestor nodes of the selected items in the tree. You can simplify the updateState() method if you only want immediate child and parent rows highlighted.

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

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

Interacting with custom CellFactory node adds row to TableView selection list?

I have a TableView with a CellFactory that places a ComboBox into one of the columns. The TableView has SelectionMode.MULTIPLE enabled but it is acting odd with the ComboBox cell.
When the users clicks on the ComboBox to select a value, that row is added to the list of selected rows. Instead, clicking on the ComboBox should either select that row and deselect all others (unless CTRL is being held), or it should not select the row at all, but only allow for interaction with the ComboBox.
I am not sure how to achieve this.
Here is a complete example to demonstrate the issue:
import javafx.application.Application;
import javafx.beans.property.ObjectProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.property.StringProperty;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.StringConverter;
enum Manufacturer {
HP, DELL, LENOVO, ASUS, ACER;
}
public class TableViewSelectionIssue extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage primaryStage) {
// Simple Interface
VBox root = new VBox(10);
root.setAlignment(Pos.CENTER);
root.setPadding(new Insets(10));
// Simple TableView
TableView<ComputerPart> tableView = new TableView<>();
TableColumn<ComputerPart, Manufacturer> colManufacturer = new TableColumn<>("Manufacturer");
TableColumn<ComputerPart, String> colItem = new TableColumn<>("Item");
tableView.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);
colManufacturer.setCellValueFactory(t -> t.getValue().manufacturerProperty());
colItem.setCellValueFactory(t -> t.getValue().itemNameProperty());
tableView.getColumns().addAll(colManufacturer, colItem);
// CellFactory to display ComboBox in colManufacturer
colManufacturer.setCellFactory(param -> new ManufacturerTableCell(colManufacturer, FXCollections.observableArrayList(Manufacturer.values())));
// Add sample items
tableView.getItems().addAll(
new ComputerPart("Keyboard"),
new ComputerPart("Mouse"),
new ComputerPart("Monitor"),
new ComputerPart("Motherboard"),
new ComputerPart("Hard Drive")
);
root.getChildren().add(tableView);
// Show the stage
primaryStage.setScene(new Scene(root));
primaryStage.setTitle("Sample");
primaryStage.show();
}
}
class ComputerPart {
private final ObjectProperty<Manufacturer> manufacturer = new SimpleObjectProperty<>();
private final StringProperty itemName = new SimpleStringProperty();
public ComputerPart(String itemName) {
this.itemName.set(itemName);
}
public Manufacturer getManufacturer() {
return manufacturer.get();
}
public void setManufacturer(Manufacturer manufacturer) {
this.manufacturer.set(manufacturer);
}
public ObjectProperty<Manufacturer> manufacturerProperty() {
return manufacturer;
}
public String getItemName() {
return itemName.get();
}
public void setItemName(String itemName) {
this.itemName.set(itemName);
}
public StringProperty itemNameProperty() {
return itemName;
}
}
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
ManufacturerTableCell(TableColumn<ComputerPart, Manufacturer> column, ObservableList<Manufacturer> items) {
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
this.cboStatus.setConverter(new StringConverter<Manufacturer>() {
#Override
public String toString(Manufacturer object) {
return object.name();
}
#Override
public Manufacturer fromString(String string) {
return null;
}
});
this.cboStatus.disableProperty().bind(column.editableProperty().not());
this.cboStatus.setOnShowing(event -> {
final TableView<ComputerPart> tableView = getTableView();
tableView.getSelectionModel().select(getTableRow().getIndex());
tableView.edit(tableView.getSelectionModel().getSelectedIndex(), column);
});
this.cboStatus.valueProperty().addListener((observable, oldValue, newValue) -> {
if (isEditing()) {
commitEdit(newValue);
column.getTableView().refresh();
}
});
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
if (empty) {
setGraphic(null);
} else {
this.cboStatus.setValue(item);
this.setGraphic(this.cboStatus);
}
}
}
The example begins with a predictable UI:
However, when interacting with the ComboBox in the Manufacturer column, the corresponding row is selected. This is expected for the first row, but it does not get deselected when interacting with another ComboBox.
How can I prevent subsequent interactions with a ComboBox from adding to the selected rows? It should behave like any other click on a TableRow, should it not?
I am using JDK 8u161.
Note: I understand there is a ComboBoxTableCell class available, but I've not been able to find any examples of how to use one properly; that is irrelevant to my question, though, unless the ComboBoxTableCell behaves differently.
Since you want an "always editing" cell, your implementation should behave more like CheckBoxTableCell than ComboBoxTableCell. The former bypasses the normal editing mechanism of the TableView. As a guess, I think it's your use of the normal editing mechanism that causes the selection issues—why exactly, I'm not sure.
Modifying your ManufactureTableCell to be more like CheckBoxTableCell, it'd look something like:
class ManufacturerTableCell extends TableCell<ComputerPart, Manufacturer> {
private final ComboBox<Manufacturer> cboStatus;
private final IntFunction<Property<Manufacturer>> extractor;
private Property<Manufacturer> property;
ManufacturerTableCell(IntFunction<Property<Manufacturer>> extractor, ObservableList<Manufacturer> items) {
this.extractor = extractor;
this.cboStatus = new ComboBox<>();
this.cboStatus.setItems(items);
// removed StringConverter for brevity (accidentally)
setContentDisplay(ContentDisplay.GRAPHIC_ONLY);
cboStatus.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
if (event.isShortcutDown()) {
getTableView().getSelectionModel().select(getIndex(), getTableColumn());
} else {
getTableView().getSelectionModel().clearAndSelect(getIndex(), getTableColumn());
}
event.consume();
});
}
#Override
protected void updateItem(Manufacturer item, boolean empty) {
super.updateItem(item, empty);
setText(null);
clearProperty();
if (empty) {
setGraphic(null);
} else {
property = extractor.apply(getIndex());
Bindings.bindBidirectional(cboStatus.valueProperty(), property);
setGraphic(cboStatus);
}
}
private void clearProperty() {
setGraphic(null);
if (property != null) {
Bindings.unbindBidirectional(cboStatus.valueProperty(), property);
}
}
}
And you'd install it like so:
// note you could probably share the same ObservableList between all cells
colManufacturer.setCellFactory(param ->
new ManufacturerTableCell(i -> tableView.getItems().get(i).manufacturerProperty(),
FXCollections.observableArrayList(Manufacturer.values())));
As already mentioned, the above implementation bypasses the normal editing mechanism; it ties the value of the ComboBox directly to the model item's property. The implementation also adds a MOUSE_PRESSED handler to the ComboBox that selects the row (or cell if using cell selection) as appropriate. Unfortunately, I'm not quite understanding how to implement selection when Shift is down so only "Press" and "Shortcut+Press" is handled.
The above works how I believe you want it to, but I could only test it out using JavaFX 12.

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...

Get ListCell via ListView

I have a ListView with my own ListCell<MyObject> implementation. Via a network signal, I receive an index of my ListCell that should be changed.
Over listView.getItems().get(index); there is no problem to access the model, but I want to make a layout change to the listCell with the received index and a layout change to the ListCell with the index+1;
How can I access the ListCell via the ListView?
I search for a method like this:
listView.getListCell(index);
Unfortunately right now there is no API to get List Cell by index or to get All children's(listcells) for ListView. One solution can be, define a new StringProperty specialIndicator in your MyObject class.
class MyObject {
....//u r properties
private StringProperty specialIndicator;
When ever you get index from network signal set this specialIndicator property of object and do forcerefresh of ListView
public void onReceivedNetWorkSignalIndex() {
listView.getItems().get(indexFromService).setSpecialIndicator("selected");
listView.getItems().get(indexFromService+1).setSpecialIndicator("selectedplusone");
//force refresh listview (it will trigger cellFactory again so that you can manipulate layout)
listView.setItems(null);
listView.setItems(allObjects);
}
As you already have custom Object ListView , i am assuming you already have custom cellFactory (if not you have to create one ) ,Modify your custom cell factory to handle this special Indicators
listView.setCellFactory(new Callback<ListView<MyObject>, ListCell<MyObject>>() {
#Override
public ListCell<MyObject> call(ListView<MyObject> myObjectListView) {
ListCell<MyObject> cell = new ListCell<MyObject>(){
#Override
protected void updateItem(MyObject myObject, boolean b) {
super.updateItem(myObject, b);
if(myObject != null) {
setText(myObject.getName());
if("selected".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/graphics for index retun from service." + myObject.getName());
} else if("selectedplusone".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/Graphics for index+1 returned from service" + myObject.getName());
}
myObject.setSpecialIndicator(""); // reset it back to empty
}
}
};
return cell;
}
});
Here is the whole sample Application ,you can look into it (in case the above explanation is not clear).
public class ListViewTest extends Application {
#Override
public void start(Stage stage) throws Exception {
VBox root = new VBox();
final ObservableList<MyObject> allObjects = FXCollections.observableArrayList(new MyObject("object0"), new MyObject("object1"),new MyObject("object2"),new MyObject("object3"),new MyObject("object4"));
final ListView<MyObject> listView = new ListView<>(allObjects);
listView.setCellFactory(new Callback<ListView<MyObject>, ListCell<MyObject>>() {
#Override
public ListCell<MyObject> call(ListView<MyObject> myObjectListView) {
ListCell<MyObject> cell = new ListCell<MyObject>(){
#Override
protected void updateItem(MyObject myObject, boolean b) {
super.updateItem(myObject, b);
if(myObject != null) {
setText(myObject.getName());
if("selected".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/graphics for index retun from service." + myObject.getName());
setText("I am selected Index from Service");
} else if("selectedplusone".equalsIgnoreCase(myObject.getSpecialIndicator())) {
System.out.println("Setting new CSS/Graphics for index+1 returned from service" + myObject.getName());
setText("I am selected Index +1 from Service");
}
myObject.setSpecialIndicator(""); // reset it back to empty
}
}
};
return cell;
}
});
Button serviceIndex2 = new Button("ServiceIndex2");
serviceIndex2.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent actionEvent) {
int indexFromService =2;
listView.getItems().get(indexFromService).setSpecialIndicator("selected");
listView.getItems().get(indexFromService+1).setSpecialIndicator("selectedplusone");
listView.setItems(null);
listView.setItems(allObjects);
}
});
root.getChildren().addAll(listView,serviceIndex2);
Scene scene = new Scene(root,500,500);
stage.setScene(scene);
stage.show();
}
public static void main(String[] args) {
launch(args);
}
class MyObject {
private StringProperty name;
private StringProperty specialIndicator;
MyObject(String name) {
this.name = new SimpleStringProperty(name);
this.specialIndicator = new SimpleStringProperty();
}
public String getName() {
return name.get();
}
public StringProperty nameProperty() {
return name;
}
public void setName(String name) {
this.name.set(name);
}
public String getSpecialIndicator() {
return specialIndicator.get();
}
public StringProperty specialIndicatorProperty() {
return specialIndicator;
}
public void setSpecialIndicator(String specialIndicator) {
this.specialIndicator.set(specialIndicator);
}
}
}
Here's a relatively simple approach, where there is just one "selected" index. Here I create a property to hold the index that is selected, and the cell factory just observes it, along with the cell's item property and index property, and sets the text via a binding. You could do something similar to set the graphic, if needed.
import java.util.concurrent.Callable;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewStyleAroundSelection extends Application {
#Override
public void start(Stage primaryStage) {
final ListView<String> listView = new ListView<>();
for (int i=1; i<=20; i++) {
listView.getItems().add("Item "+i);
}
final HBox controls = new HBox(5);
final Button button = new Button("Set selection");
final TextField indexField = new TextField();
final IntegerProperty selectionIndex = new SimpleIntegerProperty();
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
selectionIndex.set(Integer.parseInt(indexField.getText()));
} catch (NumberFormatException nfe) {
indexField.setText("");
}
}
});
controls.getChildren().addAll(new Label("Enter selection index:"), indexField, button);
final BorderPane root = new BorderPane();
root.setCenter(listView);
root.setBottom(controls);
listView.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> lv) {
final ListCell<String> cell = new ListCell<>();
cell.textProperty().bind(Bindings.createStringBinding(new Callable<String>() {
#Override
public String call() throws Exception {
if (cell.getItem() == null) {
return null ;
} else {
switch (cell.getIndex() - selectionIndex.get()) {
case -1: return cell.getItem() + " (selected item below)";
case 0: return cell.getItem() + " (selected)";
case 1: return cell.getItem() + " (selected item above)";
default: return cell.getItem();
}
}
}
}, cell.itemProperty(), cell.indexProperty(), selectionIndex));
return cell;
}
});
final Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
}
And here's a slightly more complex version. Here I have a custom data type which includes a boolean property. The update sets the boolean property of the specified item to true. The cell factory creates a cell, and observes the selected property both of the current item and of the previous item. Then, as before, it uses a binding to update the text of the cell.
import java.util.concurrent.Callable;
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.binding.BooleanBinding;
import javafx.beans.binding.IntegerBinding;
import javafx.beans.binding.ObjectBinding;
import javafx.beans.binding.StringBinding;
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.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.TextField;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class ListViewStyleAroundSelection extends Application {
#Override
public void start(Stage primaryStage) {
final ListView<MyDataType> listView = new ListView<>();
for (int i=0; i<=20; i++) {
listView.getItems().add(new MyDataType("Item "+i, false));
}
final HBox controls = new HBox(5);
controls.setPadding(new Insets(5));
final Button button = new Button("Set selection");
final TextField indexField = new TextField();
button.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
try {
int index = Integer.parseInt(indexField.getText());
if (index >= 0 && index < listView.getItems().size()) {
final MyDataType item = listView.getItems().get(index);
item.setSelected( ! item.isSelected() );
}
} catch (NumberFormatException nfe) {
indexField.setText("");
}
}
});
controls.getChildren().addAll(new Label("Enter selection index:"), indexField, button);
final BorderPane root = new BorderPane();
root.setCenter(listView);
root.setBottom(controls);
listView.setCellFactory(new Callback<ListView<MyDataType>, ListCell<MyDataType>>() {
#Override
public ListCell<MyDataType> call(ListView<MyDataType> lv) {
final ListCell<MyDataType> cell = new ListCell<>();
final IntegerBinding previousIndex = cell.indexProperty().subtract(1);
final ObjectBinding<MyDataType> previousItem = Bindings.valueAt(listView.getItems(), previousIndex);
final BooleanBinding previousItemSelected = Bindings.selectBoolean(previousItem, "selected");
final StringBinding thisItemName = Bindings.selectString(cell.itemProperty(), "name");
final BooleanBinding thisItemSelected = Bindings.selectBoolean(cell.itemProperty(), "selected");
cell.textProperty().bind(Bindings.createStringBinding(new Callable<String>() {
#Override
public String call() throws Exception {
if (cell.getItem() == null) {
return null ;
} else {
String value = cell.getItem().getName();
if (thisItemSelected.get()) {
value = value + " (selected) " ;
} else if (previousItemSelected.get()) {
value = value + " (selected item is above)";
}
return value ;
}
}
}, thisItemName, thisItemSelected, previousItemSelected));
return cell;
}
});
final Scene scene = new Scene(root, 600, 400);
primaryStage.setScene(scene);
primaryStage.show();
}
public static class MyDataType {
private final BooleanProperty selected ;
private final StringProperty name ;
public MyDataType(String name, boolean selected) {
this.name = new SimpleStringProperty(this, "name", name);
this.selected = new SimpleBooleanProperty(this, "selected", selected);
}
public final String getName() {
return name.get();
}
public final void setName(String name) {
this.name.set(name);
}
public final StringProperty nameProperty() {
return name ;
}
public final boolean isSelected() {
return selected.get();
}
public final void setSelected(boolean selected) {
this.selected.set(selected);
}
public final BooleanProperty selectedProperty() {
return selected;
}
}
public static void main(String[] args) {
launch(args);
}
}
Cell has a style class called ".cell"
public Cell getListCell(ListView list, int index){
Object[]cells = list.lookupAll(".cell").toArray();
return (Cell)cells[index];
}
This is the method I used to solve the same problem. Please note that getting the cell view is considered bad practice, and shouldn't be done in a normal context, updating cells should only be done through the model, my special case was that I wanted to fire an event manually as part of a workaround.
private ListCell<?> getListCell(ListView<?> listView, int cellIndex) {
if (cellIndex == -1) {
return null;
}
//Virtual Flow is the container of all list cells
//Each ListView has exactly one VirtualFlow which we are searching for
Optional<VirtualFlow> virtualFlowOptional = listView.getChildrenUnmodifiable()
.stream()
.filter(node -> node instanceof VirtualFlow)
.map(n -> (VirtualFlow) n)
.findFirst();
if (virtualFlowOptional.isEmpty()) {
return null;
}
VirtualFlow<ListCell<?>> virtualFlow = virtualFlowOptional.get();
return virtualFlow.getCell(cellIndex);
}

Resources