TableRow style class not sticking to row when ordering TableView - java

I want to add/remove a style class to a table row based on a boolean in the row item.
Adding and removing the class works as expected with the following code. But when I click on the column header to reorder the table, the style sticks to the row id instead of the row item. Meaning if before ordering the first row was styled, after ordering the style is still on the first row instead of the row at the new position.
setRowFactory(table -> {
TableRow<PowerPlantPM> row = new TableRow<>() {
#Override
protected void updateItem(PowerPlantPM pp, boolean empty) {
super.updateItem(pp, empty);
if (!empty && pp != null) {
pp.savedProperty().addListener((observable, oldValue, newValue) -> {
if (newValue) {
getStyleClass().remove("unsaved");
} else {
getStyleClass().add("unsaved");
}
});
// the following binding works (including ordering), but is not what I want because of the ":selected" pseudo class
// styleProperty().bind(Bindings.when(pp.savedProperty()).then("").otherwise("-fx-background-color: #f2dede"));
}
}
};
return row;
});
I hope it is clear what I want to achieve. How do I get the style to stick to the row item when reordering?

A TableRow is reused as much as possible, in your updateItem you need to query the corresponding property, not add a listener to it. The listener will only fire if the property changes, but the TableRow may asked to redraw on a different position, or a different item.
protected void updateItem(PowerPlantPM pp, boolean empty) {
super.updateItem(pp, empty);
if (!empty && pp != null) {
if (!pp.isSaved()) {
getStyleClass().add("unsaved");
} else {
getStyleClass().remove("unsaved");
}
.....
}
}
Create your ObservableList with the properties it should watch with
ObservableListFX<PowerPlantPM> powerplants =
Collections.observableArrayList(pp -> new Observable[] { pp.savedProperty() });
This list will report changes on the items for the properties you returned in the Observable[].

You never unregister the listener from the old items. Also the listener is not called for the initial value of the property. Even if it was, your code could result in the same style class being added multiple times to a node. Furthermore cells may become empty. You need to remove the style class in that case too.
To avoid adding the same style class multiple times use a pseudoclass:
final PseudoClass unsaved = PseudoClass.getPseudoClass("unsaved");
setRowFactory(table -> {
TableRow<PowerPlantPM> row = new TableRow<>() {
private final ChangeListener<Boolean> listener = (observable, oldValue, newValue) -> {
pseudoClassStateChanged(unsaved, !newValue);
};
#Override
protected void updateItem(PowerPlantPM pp, boolean empty) {
PowerPlantPM oldItem = getItem();
if (oldItem != null) {
// remove old listener
oldItem.savedProperty().removeListener(listener);
}
super.updateItem(pp, empty);
if (empty || pp == null) {
// remove pseudoclass from empty cell
pseudoClassStateChanged(unsaved, false);
} else {
// add new listener & handle initial value
pp.savedProperty().addListener(listener);
pseudoClassStateChanged(unsaved, pp.isSaved());
}
}
};
return row;
});
(Of course you need to adjust your CSS selectors to use :unsaved instead of .unsaved.)

Related

JavaFX ListCell updateItem executes twice?

I am trying to create custom cells in a ListView , but every time I add a new item, the updateItem(TextFlow item, Boolean empty) is executed twice: one time it receives null and true, and the second time it does not (!null and false)
If I do not implement the setCellFactory method, then I can add the items to the table without problems.
ListView without custom cellFactory
However, when I do implement it, it simply creates 10 empty cells (where is the content?).
ListView with custom cellFactory
public class Controller implements Initializable {
#FXML
private ListView <TextFlow> console;
private ObservableList<TextFlow> data = FXCollections.observableArrayList();
public void initialize(URL location, ResourceBundle resources) {
console.setCellFactory(new Callback<ListView<TextFlow>, ListCell<TextFlow>>() {
#Override
public ListCell<TextFlow> call(ListView<TextFlow> param) {
return new ListCell<TextFlow>() {
#Override
protected void updateItem(TextFlow item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setItem(item);
setStyle("-fx-control-inner-background: blue;");
} else {
System.out.println("Item is null.");
}
}
};
}
});
for (int i = 0 ; i < 10; i++) {
Text txt = getStyledText("This is item number " + i + ".");
TextFlow textFlow = new TextFlow();
textFlow.getChildren().add(txt);
data.add(textFlow);
}
console.setItems(data);
}
private Text getStyledText (String inputText) {
Text text = new Text(inputText);
text.setFont(new Font("Courier New",12));
text.setFill(Paint.valueOf("#000000"));
return text;
}
}
updateItem can be called an arbitrary amount of times, different items may be passed and the cell can go from empty to non-empty and the other way round. ListView creates about as many cells as you see on screen and fills them with items. E.g. scrolling or modifications of the items list or resizing of the ListView can result in updates.
For this reason any cell needs to be able to deal with an arbitrary sequence of items (or null+empty) being passed to the updateItem method.
Furthermore you should avoid invoking setItem yourself, since super.updateItem does that already. Use setGraphic instead, if you want to display the item in the cell:
#Override
public ListCell<TextFlow> call(ListView<TextFlow> param) {
return new ListCell<TextFlow>() {
#Override
protected void updateItem(TextFlow item, boolean empty) {
super.updateItem(item, empty);
if (item != null) {
setStyle("-fx-control-inner-background: blue;");
setGraphic(item);
} else {
setStyle(null);
setGraphic(null);
System.out.println("Item is null.");
}
}
};
}

Color table cell depending on cell data

I found a good example on the Internet how to do this, in fact it does not work. Below is the code with my comments, please help to figure out what's what and why it doesn't work.
#FXML
private void initialize() {
timeContractColumn.setCellFactory(column -> {
return new TableCell<MainData, LocalDate>() { // MainData - model,
// where all variables and collections for storing data are stored
// timeContractColumn - stores the entered date, therefore, LocalDate
#Override
protected void updateItem(LocalDate item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) { //If the cell is empty
setText(null);
setStyle("");
} else { //If the cell is not empty
setText(item.toString()); //We place the data in the cell
System.out.println(item);
// We get here all the information about this line.
MainData auxPerson = getTableView().getItems().get(getIndex());
System.out.println(auxPerson.toString());
// Change the style if ...
if (auxPerson.getTimeContract().equals("2019-04-09")) {
setTextFill(Color.RED);
setStyle("-fx-background-color: yellow");
} else {
//Here we see whether the row of this cell is highlighted or not
if(getTableView().getSelectionModel().getSelectedItems().contains(auxPerson))
setTextFill(Color.WHITE);
else
setTextFill(Color.BLACK);
}
}
}
};
});
}
Fabian was right, I did not take into account that I was comparing different objects, thanks for all the advice.
if (auxPerson.getTimeContract().toString().equals("2019-04-11"))

JavaFX search in listview

I want to search in listview and my code is working but not well enough. The problem is when i write few characters in the search text field not only the result appears, but the rest of the items also appear...
The code:
// Wrap the ObservableList in a FilteredList (initially display all data).
FilteredList<Client> filteredData = new FilteredList<>(main.getClientListData(),p -> true);
//Set the filter Predicate whenever the filter changes.
searchUserTF.textProperty().addListener((observable, oldValue, newValue) -> {
filteredData.setPredicate(client ->{
// If filter text is empty, display all persons.
if(newValue == null || newValue.isEmpty()){
return true;
}
// Compare first name and last name of every client with filter text.
String lowerCaseFilter = newValue.toLowerCase();
if(client.getFirstname().toLowerCase().contains(lowerCaseFilter)){
return true; //filter matches first name
}else if(client.getLastname().toLowerCase().contains(lowerCaseFilter)){
return true; //filter matches last name
}
return false; //Does not match
});
});
//Wrap the FilteredList in a SortedList.
SortedList<Client> sortedData = new SortedList<>(filteredData);
//put the sorted list into the listview
clientListView.setItems(sortedData);
Photos:
Original list:
Sorted list:
ListCell implementation:
clientListView.setCellFactory(new Callback<ListView<Client>, ListCell<Client>>() {
#Override
public ListCell<Client> call(ListView<Client> param) {
final Label leadLbl = new Label();
final Tooltip tooltip = new Tooltip();
final ListCell<Client> cell = new ListCell<Client>(){
#Override
public void updateItem(Client item, boolean empty){
super.updateItem(item,empty);
if(item != null){
leadLbl.setText(item.getFirstname());
setText(item.getFirstname()+" "+item.getLastname());
tooltip.setText(item.getFirstname());
setTooltip(tooltip);
}
}
};
return cell;
}
});
Your problem is not in your filter logic.
I think you have a bad ListCell implementation that does not clear its text when it is supposed to display nothing.
Update
Yeah, your cell renderer does not clear the cell when the item is null. Try something like this:
final ListCell<Client> cell = new ListCell<Client>(){
#Override
public void updateItem(Client item, boolean empty){
super.updateItem(item,empty);
if(item != null){
leadLbl.setText(item.getFirstname());
setText(item.getFirstname()+" "+item.getLastname());
tooltip.setText(item.getFirstname());
setTooltip(tooltip);
}
else {
leadLbl.setText("");
setText("");
}
}
};

Change color in table view in Java Fx. ( depending on a value of the table ) [duplicate]

This question is related to this. Now I want to colour the row where field value equals to some value.
#FXML
private TableView<FaDeal> tv_mm_view;
#FXML
private TableColumn<FaDeal, String> tc_inst;
tc_inst.setCellValueFactory(cellData -> new SimpleStringProperty(""+cellData.getValue().getInstrumentId()));
tc_inst.setCellFactory(column -> new TableCell<FaDeal, String>() {
#Override
protected void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
} else {
setText(item);
// Style row where balance < 0 with a different color.
TableRow currentRow = getTableRow();
if (item.equals("1070")) {
currentRow.setStyle("-fx-background-color: tomato;");
} else currentRow.setStyle("");
}
}
});
The problem is I don't want to show tc_inst in my table. For this reason I set visible checkbox in SceneBuilder to false. In this case colouring part doesn't work at all. How can hide tc_inst so that colouring works?
Use a row factory, instead of a cell factory, if you want to change the color of the whole row:
tv_mm_view.setRowFactory(tv -> new TableRow<FaDeal>() {
#Override
public void updateItem(FaDeal item, boolean empty) {
super.updateItem(item, empty) ;
if (item == null) {
setStyle("");
} else if (item.getInstrumentId().equals("1070")) {
setStyle("-fx-background-color: tomato;");
} else {
setStyle("");
}
}
});
Note that if the value of instrumentId changes while the row is displayed, then the color will not change automatically with the above code, unless you do some additional work. The simplest way to make that happen would be to construct your items list with an extractor which returned the instrumentIdProperty() (assuming you are using the JavaFX property pattern in FaDeal).

Why is the ChangeListener associated to a CheckBox running multiple times in the TreeCell

I am having a problem that I cannot figure out. I am taking a TreeView called treeModel and setting cells using setCellFactory as can be seen by the code. Now within the updateItem, I am setting a CheckBox as a graphic and am associating it with the CheckBoxTreeItem custom class called CheckBoxTreeItemModel. Now every time updateItem runs a new CheckBox is created and a new ChangeListener is created for it.
Now at first everything looks normal. Then I expand the direct children of the root, and begin checking item, but the listener seems to be called multiple time. For every level of TreeItems that is expanded, that is how many times the listener is called on one of the descendants of root. If I click on a child a few leaves down a parent, those listeners are then called multiple times as well. Its weird behavior that might be hard to explain, but the point is I don't think the listener is suppose to be called that many times. Its as if its cached. The problem code is below. Any help understanding why this may be happening would be greatly appreciated.
treeModel.setCellFactory(new Callback<TreeView<String>, TreeCell<String>>() {
#Override
public TreeCell<String> call(TreeView<String> param) {
return new TreeCell<String>() {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
final TreeCell<String> currCell = this;
this.setOnMouseClicked(new EventHandler<MouseEvent>() {
/*mouse event stuff completely unrelated to problem*/
});
if (empty) {
setText(null);
setGraphic(null);
}
else {
TreeItem<String> treeItem = getTreeItem();
if (treeItem instanceof CheckBoxTreeItemModel) {
System.out.println("Being called.");
final CheckBoxTreeItemModel chkTreeItem = (CheckBoxTreeItemModel) treeItem;
setText(item.toString());
CheckBox chk = new CheckBox();
chk.setSelected(chkTreeItem.getDeleteTick());
if(chkTreeItem.getListener() == null) {
ChangeListener<Boolean> listener = new ChangeListener<Boolean>() {
#Override
public void changed(ObservableValue<? extends Boolean> observable,
Boolean oldValue, Boolean newValue) {
if(newValue) {
//was checked
System.out.println(chkTreeItem.toString()+" was checked!");
chkTreeItem.setDeleteTick(newValue);
}
else {
System.out.println(chkTreeItem.toString()+" was un-checked!");
chkTreeItem.setDeleteTick(newValue);
}
}//end of changed method
};
chkTreeItem.setListener(listener);
}
chk.selectedProperty().removeListener(chkTreeItem.getListener());
chk.selectedProperty().addListener(chkTreeItem.getListener());
chk.indeterminateProperty().bindBidirectional(chkTreeItem.indeterminateProperty());
chk.selectedProperty().bindBidirectional(chkTreeItem.selectedProperty());
setGraphic(chk);
}
else {
setText(item.toString());
setGraphic(null);
}
}
}//end of updateItem
};
}//end of the call method
});
Suggested Approach
I advise scrapping most of your code and using inbuilt CheckBoxTreeCell and CheckBoxTreeItem classes instead. I'm not sure if the inbuilt cells match your requirements, but even if they don't, you could examine the source code for them and compare it with yours and it (should) start to give you a good idea on where you are going wrong.
Potential Issues in Your Code
Reproducing your issue would require more code than you currently supply. But some things to look for are:
Removing and adding the same listener is pointless:
// first line is redundant.
// all listener code is probably unnecessary as you already bindBidirectional.
chk.selectedProperty().removeListener(chkTreeItem.getListener());
chk.selectedProperty().addListener(chkTreeItem.getListener());
updateItem might be called many times for a given cell. don't create new nodes for the cell everytime, instead re-use existing nodes created for the cell.
// replace with a lazily instantiated CheckBox member reference in TreeCell instance.
CheckBox chk = new CheckBox();
You bindBiDirectional but never unbind those bound values.
// should also unbind this values.
chk.indeterminateProperty().bindBidirectional(chkTreeItem.indeterminateProperty());
chk.selectedProperty().bindBidirectional(chkTreeItem.selectedProperty());
Sample Code
Sample updateItem code from the inbuilt CheckBoxTreeCell code:
public class CheckBoxTreeCell<T> extends DefaultTreeCell<T> {
. . .
private final CheckBox checkBox;
private ObservableValue<Boolean> booleanProperty;
private BooleanProperty indeterminateProperty;
. . .
#Override public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
StringConverter c = getConverter();
TreeItem<T> treeItem = getTreeItem();
// update the node content
setText(c != null ? c.toString(treeItem) : (treeItem == null ? "" : treeItem.toString()));
checkBox.setGraphic(treeItem == null ? null : treeItem.getGraphic());
setGraphic(checkBox);
// uninstall bindings
if (booleanProperty != null) {
checkBox.selectedProperty().unbindBidirectional((BooleanProperty)booleanProperty);
}
if (indeterminateProperty != null) {
checkBox.indeterminateProperty().unbindBidirectional(indeterminateProperty);
}
// install new bindings.
// We special case things when the TreeItem is a CheckBoxTreeItem
if (treeItem instanceof CheckBoxTreeItem) {
CheckBoxTreeItem<T> cbti = (CheckBoxTreeItem<T>) treeItem;
booleanProperty = cbti.selectedProperty();
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
indeterminateProperty = cbti.indeterminateProperty();
checkBox.indeterminateProperty().bindBidirectional(indeterminateProperty);
} else {
Callback<TreeItem<T>, ObservableValue<Boolean>> callback = getSelectedStateCallback();
if (callback == null) {
throw new NullPointerException(
"The CheckBoxTreeCell selectedStateCallbackProperty can not be null");
}
booleanProperty = callback.call(treeItem);
if (booleanProperty != null) {
checkBox.selectedProperty().bindBidirectional((BooleanProperty)booleanProperty);
}
}
}
}
. . .
}

Categories

Resources