behaviour duplicate and inverse rows in a tableView (javafx) - javafx

I have simplified the code to the following in an update of tableview.
For some reasons I do not use the property of JavaFx as one would expect, perhaps this behaviour is linked to this. Coming from JTable and observer pattern I wanted to try how to implement this in javafx. However the data of my table are all fine but when I am trying to change the colour of the background some more rows appears outside the range of the table. I have checked the size of the Observable list and it is as expected.
I cannot really see or understand how the datas.set generates this behaviour, would it be something that I miss with my code or then an expected behaviour with Javafx tableview.
Basically the logic used is I receive an update from another class (which I simulate here with the Thread), then I will update the corresponding row of the observable list with the updated data.
Thanks.
package application;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.stage.Stage;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
try {
Pane root = new Pane();
root.getChildren().add(new TableModel().getTable());
Scene scene = new Scene(root,400,800);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
} catch(Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
launch(args);
}
}
package application;
import java.util.Random;
import javafx.application.Platform;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.control.TableCell;
import javafx.scene.control.TableColumn;
import javafx.scene.control.TableColumn.CellDataFeatures;
import javafx.scene.control.TableView;
import javafx.util.Callback;
public class TableModel{
private TableView<TableData> table = new TableView<>();
private ObservableList<TableData> datas;
#SuppressWarnings("unchecked")
public TableModel() {
datas = FXCollections.observableArrayList();
final TableColumn<TableData, Integer> cId = new TableColumn<>("ID");
cId.setCellValueFactory(
new Callback<TableColumn.CellDataFeatures<TableData, Integer>, ObservableValue<Integer>>() {
#Override
public ObservableValue<Integer> call(CellDataFeatures<TableData, Integer> param) {
return new SimpleObjectProperty<Integer>(param.getValue().getId());
}
});
cId.setCellFactory(e -> new TableCell<TableData, Integer>() {
#Override
public void updateItem(Integer item, boolean empty) {
// Always invoke super constructor.
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
setText(String.valueOf(item));
if (getIndex() == 0) {
this.setStyle("-fx-background-color: green;");
}else {
this.setStyle("-fx-background-color: red;");
}
}
}
});
table.getColumns().addAll(cId);
table.setColumnResizePolicy(TableView.CONSTRAINED_RESIZE_POLICY);
table.setItems(datas);
table.setPrefSize(150, 450);
datas.add(0, new TableData(0));
datas.add(1, new TableData(0));
datas.add(2, new TableData(0));
datas.add(3, new TableData(0));
datas.add(4, new TableData(0));
new Thread(()->{
while(true) {
Random rand = new Random(10);
update(rand.nextInt());
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}).start(); ;
}
public TableView<TableData> getTable() {
return table;
}
public void update(int num) {
TableData data = new TableData(num);
Platform.runLater(() -> {
datas.set(0, data);
});
}
private class TableData {
private SimpleIntegerProperty cId = new SimpleIntegerProperty();
public TableData(Integer conid) {
this.cId.setValue(conid);
}
public Integer getId() {
return cId.get();
}
}
}
Output with datas.set(0, data);
Output without datas.set(0, data);

There are 4 issues with your code:
Issue 1
final TableColumn<TableData, Integer> cId = new TableColumn<>("ID");
cId.setCellValueFactory(
new Callback<TableColumn.CellDataFeatures<TableData, Integer>, ObservableValue<Integer>>() {
#Override
public ObservableValue<Integer> call(CellDataFeatures<TableData, Integer> param) {
return new SimpleObjectProperty<Integer>(param.getValue().getId());
}
});
This prevents changes of the property from triggering updates automatically. E.g.
table.getItems().get(0).cId.set(5);
would not result in an update of the table. To fix this return the property itself, not a newly created property that is initialized with the property's value at the time of the cellValueFactory's execution.
final TableColumn<TableData, Number> cId = new TableColumn<>("ID");
cId.setCellValueFactory(
new Callback<TableColumn.CellDataFeatures<TableData, Number>, ObservableValue<Number>>() {
#Override
public ObservableValue<Number> call(TableColumn.CellDataFeatures<TableData, Number> param) {
return param.getValue().cId;
}
});
Issue 2
#Override
public void updateItem(Integer item, boolean empty) {
// Always invoke super constructor.
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
setText(String.valueOf(item));
if (getIndex() == 0) {
this.setStyle("-fx-background-color: green;");
}else {
this.setStyle("-fx-background-color: red;");
}
}
}
This implementation is incorrect given the fact that cells may become empty after being filled with a value. You should make sure the "empty" look is restored in case the cell becomes empty. This requires you to clear the style property:
#Override
protected void updateItem(Number item, boolean empty) {
// Always invoke supertype's implementation
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setStyle(null);
} else {
setText(item.toString());
if (getIndex() == 0) {
this.setStyle("-fx-background-color: green;");
} else {
this.setStyle("-fx-background-color: red;");
}
}
}
Issues 3 & 4
new Thread(()->{
while(true) {
Random rand = new Random(10);
update(rand.nextInt());
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
}).start();
This thread will remain alive even if the GUI is shut down preventing the program from terminating. You can easily fix this by using a daemon thread.
Futhermore Random initialized with a seed returns deterministic results. Since you reinitialize Random with the same seed in every iteration of the loop, the value rand.nextInt() returns is always the same. You need to move the initialisation of the Random outside of the loop.
Thread thread = new Thread(() -> {
Random rand = new Random(10);
while (true) {
update(rand.nextInt());
try {
Thread.sleep(3000);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
});
thread.setDaemon(true);
thread.start();

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 to set text and color of multiple ListView items when clicked

I know there are many related questions about this but maybe I'm missing something because I can't get the behavior I'm expecting, to work.
#FXML
private ListView<String> guiList;
void performAction(Actions action) {
try {
Task<String> task = new Task<>() {
#Override
public String call() {
String mySelection = Context.getInstance().getSelected();
ArrayList<String> selectedList = Context.getInstance().getItemsClicked();
if (selectedList == null) {
selectedList = new ArrayList<>();
}
selectedList.add(mySelection);
Context.getInstance().setItemsClicked(selectedList);
guiList.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> param) {
ListCell<String> cell = new ListCell<String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if(item != null && item.matches(mySelection)) {
setText(mySelection + " [" + action + "]");
setFont(Font.font(Font.getDefault().getFamily(), FontWeight.BOLD, Font.getDefault().getSize()));
setStyle("-fx-text-fill: green;");
} else {
setText(item);
}
}
};
return cell;
}
});
return "";
}
};
} catch (Exception e) {
}
}
When I click in an item of guiList, the text is changed, gets bold and shows in green color but I don't understand why I need the else statement. If I don't use it, all the other items of the list disappear.
I ask this because I want to change ALL of the items I click and in the current behavior, the changes are only made in the last one clicked.
Here is on approach. Use an object that has a Boolean variable to keeps up with if the item has been selected.
KeyCode 1
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) - > {
if (t1 != null) {
t1.setSelected(true);
}
}));
Key Code 2
lvMain.setCellFactory(lv - > new ListCell < MyItem > () {
#Override
public void updateItem(MyItem item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
setText(item.getText());
if (item.isSelected()) {
setTextFill(Color.RED);
}
}
}
});
Main
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
import javafx.scene.control.ListCell;
import javafx.scene.control.ListView;
import javafx.scene.control.SelectionMode;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.scene.paint.Color;
import javafx.stage.Stage;
/**
*
* #author blj0011
*/
public class App extends Application {
#Override
public void start(Stage primaryStage) {
ListView<MyItem> lvMain = new ListView();//Create ListView
lvMain.getSelectionModel().setSelectionMode(SelectionMode.MULTIPLE);//Change ListView selection mode to multiple
ObservableList<MyItem> items = FXCollections.observableArrayList(new MyItem("Julia"), new MyItem("Ian"), new MyItem("Sue"), new MyItem("Matthew"), new MyItem("Hannah"));//ObseravableList that will be used to set the ListView
lvMain.setItems(items);//Set the ListView's items
lvMain.setCellFactory(lv -> new ListCell<MyItem>()
{
#Override
public void updateItem(MyItem item, boolean empty)
{
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
}
else {
setText(item.getText());
if(item.isSelected())
{
setTextFill(Color.RED);
}
}
}
});
lvMain.getSelectionModel().selectedItemProperty().addListener(((ov, t, t1) -> {
if(t1 != null)
{
t1.setSelected(true);
}
}));
VBox vbox = new VBox();
vbox.getChildren().addAll(lvMain);
StackPane root = new StackPane();
root.getChildren().add(vbox);
Scene scene = new Scene(root, 300, 250);
primaryStage.setTitle("Hello World!");
primaryStage.setScene(scene);
primaryStage.show();
}
/**
* #param args the command line arguments
*/
public static void main(String[] args) {
launch(args);
}
}
MyItem
/**
*
* #author Sed
*/
public class MyItem {
private String text;
private boolean selected;
public MyItem(String text) {
this.text = text;
this.selected = false;
}
public boolean isSelected() {
return selected;
}
public void setSelected(boolean isSelected) {
this.selected = isSelected;
}
public String getText() {
return text;
}
public void setText(String text) {
this.text = text;
}
}
Output
I think a better solution would be to use the ListView's built in multiple selection or have your cells have a ToggleButton. When the ToggleButton is on, change the color of the text. When it is off, change the color back to it's original state.

JavaFX Disable TableColumn based on checkbox state

Am looking to disable a TableColumn<CustomObject, String> tableColumn based on a field value in the CustomObject only when the TableColumn<CustomObject, Boolean> tableColumnTwo checkbox is checked. I can disable the textbox inside public void updateItem(String s, boolean empty) however not sure how to check the state of checkbox inside updateItem
Below is the relevant code snippet, would highly appreciate if anyone can shed light on this
#FXML
private TableColumn<CustomObject, Boolean> tableColumnTwo;
#FXML
private TableColumn<CustomObject, String> tableColumn;
tableColumn.setCellFactory(
new Callback<TableColumn<CustomObject, String>, TableCell<CustomObject, String>>() {
#Override
public TableCell<CustomObject, String> call(TableColumn<CustomObject, String> paramTableColumn) {
return new TextFieldTableCell<CustomObject, String>(new DefaultStringConverter()) {
#Override
public void updateItem(String s, boolean empty) {
super.updateItem(s, empty);
TableRow<CustomObject> currentRow = getTableRow();
if(currentRow.getItem() != null && !empty) {
if (currentRow.getItem().getPetrified() == false) { // Need to check if checkbox is checked or not
setDisable(true);
setEditable(false);
this.setStyle("-fx-background-color: red");
} else {
setDisable(false);
setEditable(true);
setStyle("");
}
}
}
};
}
});
You can add a listener on the checkbox, which when checked will cause the table refresh.
data = FXCollections.observableArrayList(new Callback<CustomObject, Observable[]>() {
#Override
public Observable[] call(CustomObject param) {
return new Observable[]{param.petrifiedProperty()};
}
});
data.addListener(new ListChangeListener<CustomObject>() {
#Override
public void onChanged(ListChangeListener.Change<? extends CustomObject> c) {
while (c.next()) {
if (c.wasUpdated()) {
tableView.setItems(null);
tableView.layout();
tableView.setItems(FXCollections.observableList(data));
}
}
}
});
Your cellFactory would remain the same and would get called when a checkbox is checked/unchecked.
Usually, we expect cells being updated whenever they are notified about a change in the underlying data. To make certain that a notification is fired by the data on changing a property of an item, we need a list with an extractor on the properties that we are interested in, something like:
ObservableList<CustomObject> data = FXCollections.observableArrayList(
c -> new Observable[] {c.petrifiedProperty()}
);
With that in place the list fires a list change of type update whenever the pretified property changes.
Unfortunately, that's not enough due to a bug in fx: cells are not updated when receiving a listChange of type update from the underlying items. A dirty way around (read: don't use once the bug is fixed, it's using emergency api!) is to install a listener on the items and call table.refresh() when receiving an update.
An example:
import java.util.logging.Logger;
//import de.swingempire.fx.util.FXUtils;
import javafx.application.Application;
import javafx.beans.Observable;
import javafx.collections.FXCollections;
import javafx.collections.ListChangeListener;
import javafx.collections.ObservableList;
import javafx.scene.Scene;
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.PropertyValueFactory;
import javafx.scene.control.cell.TextFieldTableCell;
import javafx.scene.layout.BorderPane;
import javafx.stage.Stage;
import javafx.util.converter.DefaultStringConverter;
/**
* CheckBoxTableCell: update editable state of one column based of
* the boolean in another column
* https://stackoverflow.com/q/46290417/203657
*
* Bug in skins: cell not updated on listChange.wasUpdated
*
* reported as
* https://bugs.openjdk.java.net/browse/JDK-8187665
*/
#SuppressWarnings({ "rawtypes", "unchecked" })
public class TableViewUpdateBug extends Application {
/**
* TableCell that updates state based on another value in the row.
*/
public static class DisableTextFieldTableCel extends TextFieldTableCell {
public DisableTextFieldTableCel() {
super(new DefaultStringConverter());
}
/**
* Just to see whether or not this is called on update notification
* from the items (it's not)
*/
#Override
public void updateIndex(int index) {
super.updateIndex(index);
// LOG.info("called? " + index);
}
/**
* Implemented to change background based on
* visible property of row item.
*/
#Override
public void updateItem(Object item, boolean empty) {
super.updateItem(item, empty);
TableRow<TableColumn> currentRow = getTableRow();
boolean editable = false;
if (!empty && currentRow != null) {
TableColumn column = currentRow.getItem();
if (column != null) {
editable = column.isVisible();
}
}
if (!empty) {
setDisable(!editable);
setEditable(editable);
if (editable) {
this.setStyle("-fx-background-color: red");
} else {
this.setStyle("-fx-background-color: green");
}
} else {
setStyle("-fx-background-color: null");
}
}
}
#Override
public void start(Stage primaryStage) {
// data: list of tableColumns with extractor on visible property
ObservableList<TableColumn> data = FXCollections.observableArrayList(
c -> new Observable[] {c.visibleProperty()});
data.addAll(new TableColumn("first"), new TableColumn("second"));
TableView<TableColumn> table = new TableView<>(data);
table.setEditable(true);
// hack-around: call refresh
data.addListener((ListChangeListener) c -> {
boolean wasUpdated = false;
boolean otherChange = false;
while(c.next()) {
if (c.wasUpdated()) {
wasUpdated = true;
} else {
otherChange = true;
}
}
if (wasUpdated && !otherChange) {
table.refresh();
}
//FXUtils.prettyPrint(c);
});
TableColumn<TableColumn, String> text = new TableColumn<>("Text");
text.setCellFactory(c -> new DisableTextFieldTableCel());
text.setCellValueFactory(new PropertyValueFactory<>("text"));
TableColumn<TableColumn, Boolean> visible = new TableColumn<>("Visible");
visible.setCellValueFactory(new PropertyValueFactory<>("visible"));
visible.setCellFactory(CheckBoxTableCell.forTableColumn(visible));
table.getColumns().addAll(text, visible);
BorderPane root = new BorderPane(table);
Scene scene = new Scene(root, 300, 150);
primaryStage.setScene(scene);
primaryStage.show();
}
public static void main(String[] args) {
launch(args);
}
#SuppressWarnings("unused")
private static final Logger LOG = Logger
.getLogger(TableViewUpdateBug.class.getName());
}

Automatically updating/styling TreeView's TreeCells when BooleanProperty is true in seperate TableView

What's the easiest way to have a TreeView's cells auto-refresh with a new style when a condition is met in a separate TableView?
I'm currently setting the TreeCells' styles in the updateItem() method in the TreeView cell factory, but this only fires off if the user adds or removes something in the TreeView. I want to be able to change the style of a given TreeCell if I check off all 3 checkboxes in a separate dialog box.
I'm currently able to monitor the number of checked checkboxes with a BooleanProperty and an IntegerProperty, but I have no idea how I'm supposed to "auto-update" or call a TreeView refresh when a TreeItem's Object's BooleanProperty changes.
Any help is greatly appreciated.
You can set the style in the TreeCell whenever a boolean property on the value underlying the TreeCell is updated (via a binding).
return new TreeCell<Message>() {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
styleProperty.set(null);
} else {
setText(item.getText());
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
Full Sample
import javafx.application.Application;
import javafx.beans.binding.Bindings;
import javafx.beans.property.*;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.*;
import javafx.scene.control.cell.CheckBoxTableCell;
import javafx.scene.control.cell.PropertyValueFactory;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import javafx.util.Callback;
public class TreeViewSample extends Application {
public static void main(String[] args) {
launch(args);
}
#Override
public void start(Stage stage) {
ObservableList<Message> messages = FXCollections.observableArrayList();
TreeItem<Message> rootItem = new TreeItem<> (new Message("Inbox"));
rootItem.setExpanded(true);
for (int i = 1; i < 6; i++) {
Message message = new Message("Message" + i);
messages.add(message);
TreeItem<Message> item = new TreeItem<> (message);
rootItem.getChildren().add(item);
}
TreeView<Message> tree = new TreeView<> (rootItem);
tree.setCellFactory(new Callback<TreeView<Message>, TreeCell<Message>>() {
#Override
public TreeCell<Message> call(TreeView<Message> param) {
return new TreeCell<Message>() {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
styleProperty.set(null);
} else {
setText(item.getText());
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
}
});
TableView<Message> tableView = new TableView<>();
tableView.setEditable(true);
TableColumn<Message, String> textCol = new TableColumn<>("Text");
textCol.setCellValueFactory(new PropertyValueFactory<>("text"));
tableView.getColumns().add(textCol);
TableColumn<Message, Boolean> readCol = new TableColumn<>("Read");
readCol.setCellValueFactory(new PropertyValueFactory<>("read"));
readCol.setCellFactory(CheckBoxTableCell.forTableColumn(readCol));
readCol.setEditable(true);
tableView.getColumns().add(readCol);
tableView.setItems(messages);
VBox root = new VBox(10, tree, tableView);
root.setPadding(new Insets(10));
stage.setScene(new Scene(root, 300, 250));
stage.show();
}
public class Message {
private StringProperty text = new SimpleStringProperty();
private BooleanProperty read = new SimpleBooleanProperty(false);
public Message(String msgText) {
text.set(msgText);
}
public String getText() {
return text.get();
}
public StringProperty textProperty() {
return text;
}
public void setText(String text) {
this.text.set(text);
}
public boolean isRead() {
return read.get();
}
public BooleanProperty readProperty() {
return read;
}
public void setRead(boolean read) {
this.read.set(read);
}
}
}
I'm trying to the bind the graphicProperty to the same BooleanProperty and change the image based on the value.
Example using a binding of an Image within an ImageView associated with the cell.
Image unreadImage = new Image("http://icons.iconarchive.com/icons/oxygen-icons.org/oxygen/16/Status-mail-unread-new-icon.png");
Image readImage = new Image("http://icons.iconarchive.com/icons/icons8/ios7/16/Messaging-Read-Message-icon.png");
. . .
return new TreeCell<Message>() {
ImageView imageView = new ImageView();
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
styleProperty().unbind();
imageView.imageProperty().unbind();
if (empty || item == null || item.getText() == null) {
setText(null);
setGraphic(null);
styleProperty().set(null);
} else {
setText(item.getText());
setGraphic(imageView);
imageView.imageProperty().bind(
Bindings.when(
item.readProperty()
).then(readImage)
.otherwise(unreadImage)
);
styleProperty().bind(
Bindings.when(
item.readProperty()
).then("-fx-background-color: red;")
.otherwise("-fx-background-color: null;")
);
}
}
};
An alternate (and possibly preferable) way to handle this from above is to instead get the style class or psuedoclass of the cell and update that based upon the boolean property. Then define the style in a separate CSS stylesheet. The output of the sample below is the same as the graphic based sample above.
mail.css
.readable:read {
-fx-background-color: red;
-fx-graphic: url(
"http://icons.iconarchive.com/icons/icons8/ios7/16/Messaging-Read-Message-icon.png"
);
}
.readable:unread {
-fx-graphic: url(
"http://icons.iconarchive.com/icons/oxygen-icons.org/oxygen/16/Status-mail-unread-new-icon.png"
);
}
Pseudo-class based code snippet:
PseudoClass READ_PSEUDO_CLASS = PseudoClass.getPseudoClass("read");
PseudoClass UNREAD_PSEUDO_CLASS = PseudoClass.getPseudoClass("unread");
tree.setCellFactory(new Callback<TreeView<Message>, TreeCell<Message>>() {
#Override
public TreeCell<Message> call(TreeView<Message> param) {
return new TreeCell<Message>() {
private ChangeListener<Boolean> readChangeListener = (observable, oldValue, newValue) -> {
pseudoClassStateChanged(READ_PSEUDO_CLASS, newValue);
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, !newValue);
};
Message priorItem = null;
{
getStyleClass().add("readable");
}
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
if (priorItem != null) {
priorItem.readProperty().removeListener(readChangeListener);
}
priorItem = item;
if (empty || item == null || item.getText() == null) {
setText(null);
pseudoClassStateChanged(READ_PSEUDO_CLASS, false);
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, false);
} else {
item.readProperty().addListener(readChangeListener);
setText(item.getText());
pseudoClassStateChanged(READ_PSEUDO_CLASS, item.isRead());
pseudoClassStateChanged(UNREAD_PSEUDO_CLASS, !item.isRead());
}
}
};
}
});

update CheckBoxTreeCell styles from outside process

I am building my first javafx (2.2) application. The user selects a number of tasks to execute, by selecting checkboxes in a treeview.
I am trying to figure out how, after a task completes, to change the style of the related TreeCell.
public class WorkbenchSscce extends Application {
public static void main(String...args) {
launch(args);
}
#Override
public void start(Stage stage) throws Exception {
final CheckBoxTreeItem<String> rootNode = new CheckBoxTreeItem<>("parent");
final CheckBoxTreeItem<String> taskOne = new CheckBoxTreeItem<>("task one");
final CheckBoxTreeItem<String> taskTwo = new CheckBoxTreeItem<>("task two");
rootNode.getChildren().addAll(taskOne, taskTwo);
TreeView<String> treeView = new TreeView<>(rootNode);
treeView.setEditable(true);
treeView.setCellFactory(CheckBoxTreeCell.<String>forTreeView());
treeView.setShowRoot(false);
Button executeButton = new Button("Execute");
executeButton.setOnMouseClicked(new EventHandler<MouseEvent>() {
#Override
public void handle(MouseEvent mouseEvent) {
if (taskOne.isSelected()) {
executeTask(1);
/**
* ?????
* give the TreeCell for taskOne a green background, to indicate it is complete
* ?????
*/
}
if (taskTwo.isSelected()) {
executeTask(2);
/**
* ?????
* give the TreeCell for taskTwo a green background, to indicate it is complete
* ?????
*/
}
}
});
VBox box = new VBox();
box.getChildren().addAll(treeView, executeButton);
Scene scene = new Scene(box);
stage.setScene(scene);
stage.show();
}
public void executeTask(int input) {
// do something
}
}
I can see how to style the CheckBoxTreeCells at creation time.
I see how to change styles when user events happen to the TreeView (using EventListeners).
But I can't see how to style a tree cell when the source of the event is internal to the application. See comments in the MouseEvent handler above.
The key is to observe the state of the Task (I used a Service instead of a Task in this example, as it can be run multiple times) from the cell factory. To do this, you need the data type of the TreeItem to be something that has an observable property representing the current state of the task/service. The easiest way to do this, if you can, is to make the data type of the TreeItems the Task itself (so conceptually, your TreeView is displaying Tasks).
This is slightly subtle as the item (i.e. Task) represented by a given cell can change. In this example I just observe the cell's item property, removing a listener that observes the task's state from an item the cell is no longer representing and adding the listener to the item it now represents. If you use the EasyBind framework (and Java 8, which it requires), you can clean this up a bit, doing something like
EasyBind.select(cell.itemProperty())
.selectObject(Service::stateProperty)
.addListener((ov, oldState, newState) -> updateCell(cell) );
Full example (using JavaFX 2.2, though I compiled under Java 8, so some Java 8 features may have snuck in):
import java.util.Arrays;
import java.util.HashSet;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import javafx.application.Application;
import javafx.beans.property.BooleanProperty;
import javafx.beans.property.ReadOnlyStringProperty;
import javafx.beans.property.ReadOnlyStringWrapper;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.concurrent.Service;
import javafx.concurrent.Task;
import javafx.concurrent.Worker.State;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.TreeCell;
import javafx.scene.control.TreeItem;
import javafx.scene.control.TreeView;
import javafx.scene.control.cell.CheckBoxTreeCell;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;
import javafx.util.Callback;
import javafx.util.StringConverter;
public class Main extends Application {
#Override
public void start(Stage primaryStage) {
final BorderPane root = new BorderPane();
final TreeView<SelectableService> tree = new TreeView<>();
final TreeItem<SelectableService> treeRoot = new TreeItem<>(new SelectableService("Parent"));
for (int i=1; i<=10; i++) {
treeRoot.getChildren().add(new TreeItem<>(new SelectableService("Task "+i)));
}
tree.setRoot(treeRoot);
final Button startButton = new Button("Start selected tasks");
startButton.setOnAction(new EventHandler<ActionEvent>() {
#Override
public void handle(ActionEvent event) {
for (SelectableService service : findSelectedTasks(treeRoot)) {
service.restart();
}
}
});
final HBox controls = new HBox(5);
controls.getChildren().add(startButton);
controls.setPadding(new Insets(10));
controls.setAlignment(Pos.CENTER);
root.setCenter(tree);
root.setBottom(controls);
tree.setCellFactory(new Callback<TreeView<SelectableService>, TreeCell<SelectableService>>() {
#Override
public TreeCell<SelectableService> call(TreeView<SelectableService> param) {
return createCell();
}
});
Scene scene = new Scene(root, 400, 600);
scene.getStylesheets().add(getClass().getResource("application.css").toExternalForm());
primaryStage.setScene(scene);
primaryStage.show();
}
private CheckBoxTreeCell<SelectableService> createCell() {
// CheckBoxTreeCell whose check box state is mapped to the selected property of the task:
final CheckBoxTreeCell<SelectableService> cell = new CheckBoxTreeCell<SelectableService>(new Callback<TreeItem<SelectableService>, ObservableValue<Boolean>>() {
#Override
public ObservableValue<Boolean> call(TreeItem<SelectableService> treeItem) {
SelectableService task = treeItem.getValue();
if (task != null) {
return task.selectedProperty();
} else {
return null ;
}
}
});
final ChangeListener<State> taskStateListener = new ChangeListener<State>() {
#Override
public void changed(
ObservableValue<? extends State> observable,
State oldValue, State newValue) {
updateCell(cell);
}
};
cell.itemProperty().addListener(new ChangeListener<SelectableService>() {
#Override
public void changed(
ObservableValue<? extends SelectableService> observable,
SelectableService oldTask, SelectableService newTask) {
if (oldTask != null) {
oldTask.stateProperty().removeListener(taskStateListener);
}
if (newTask != null) {
newTask.stateProperty().addListener(taskStateListener);
}
updateCell(cell);
}
});
cell.setConverter(new StringConverter<TreeItem<SelectableService>>() {
#Override
public String toString(TreeItem<SelectableService> treeItem) {
SelectableService task = treeItem.getValue();
if (task == null) {
return null ;
} else {
return task.getName();
}
}
#Override
public TreeItem<SelectableService> fromString(String string) {
// Not supported
throw new UnsupportedOperationException("Uneditable tree cell does not create SelectableTasks");
}
});
return cell;
}
private void updateCell(CheckBoxTreeCell<SelectableService> cell) {
cell.getStyleClass().removeAll(Arrays.asList("running", "finished", "failed"));
SelectableService task = cell.getItem();
if (task != null) {
State state = task.getState();
// Update style class:
if (state == State.RUNNING) {
cell.getStyleClass().add("running");
} else if (state == State.SUCCEEDED) {
cell.getStyleClass().add("finished");
} else if (state == State.FAILED){
cell.getStyleClass().add("failed");
}
}
}
private Set<SelectableService> findSelectedTasks(TreeItem<SelectableService> treeItem) {
Set<SelectableService> selectedTasks = new HashSet<>();
addTaskAndChildTasksIfSelected(selectedTasks, treeItem) ;
return selectedTasks ;
}
private void addTaskAndChildTasksIfSelected(Set<SelectableService> selectedTasks, TreeItem<SelectableService> treeItem) {
SelectableService task = treeItem.getValue();
if (task != null && task.isSelected()) {
selectedTasks.add(task);
}
for (TreeItem<SelectableService> child : treeItem.getChildren()) {
addTaskAndChildTasksIfSelected(selectedTasks, child);
}
}
public static class SelectableService extends Service<Void> {
private final BooleanProperty selected = new SimpleBooleanProperty(this, "selected", false);
public final BooleanProperty selectedProperty() {
return this.selected;
}
public final boolean isSelected() {
return this.selectedProperty().get();
}
public final void setSelected(final boolean selected) {
this.selectedProperty().set(selected);
}
private final ReadOnlyStringWrapper name = new ReadOnlyStringWrapper(this, "name");
private final void setName(String name) {
this.name.set(name);
}
public final String getName() {
return name.get() ;
}
public final ReadOnlyStringProperty nameProperty() {
return name.getReadOnlyProperty();
}
public SelectableService(String name) {
setExecutor(Executors.newCachedThreadPool(new ThreadFactory() {
#Override
public Thread newThread(Runnable r) {
Thread t = new Thread(r);
t.setDaemon(true);
return t ;
}
}));
setName(name);
}
#Override
public Task<Void> createTask() {
return new Task<Void>() {
#Override
public Void call() throws Exception {
// just a mock task: pauses for a random time, then throws an exception with
// probability 0.25
Random rng = new Random();
Thread.sleep(2000 + rng.nextInt(2000));
if (rng.nextDouble() < 0.25) {
throw new Exception("Task failed");
}
return null ;
}
};
}
}
public static void main(String[] args) {
launch(args);
}
}
application.css is simply
.finished {
-fx-background: green ;
}
.failed {
-fx-background: red ;
}
.running {
-fx-background: yellow ;
}
This is quite considerably cleaner in Java 8, by the way, but since you posted JavaFX 2.2-style code, I assumed you were still using the old version. Java 8 also allows you to use pseudoclasses for the css style, which is a bit nicer (and in general has better performance, though it's a moot point here).

Resources