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.");
}
}
};
}
Related
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.)
I have this problem with the first Hyperlink being alignment on top of TableCell.
I have tried almost everything and I could not get it to work.
colData.setCellFactory(e -> {
return new TableCell<TabelaShitjet, Hyperlink>(){
#Override
protected void updateItem(Hyperlink item, boolean empty) {
super.updateItem(item, empty);
if (!empty){
item.setOnAction(e -> {
TeDhenatBlerjes(Integer.parseInt(getTableView().getColumns().get(0).getCellData(getTableRow().getIndex())+""), item.getText());
});
setGraphic(item);
}
}
};
});
CONSTRUCTOR
public class TabelaShitjet {
private Hyperlink data;
public TabelaShitjet(String data){
this.data = new Hyperlink(data);
}
public Hyperlink getData() {
return data;
}
public void setData(Hyperlink data) {
this.data = data;
}
}
I've got no idea why exactly this happens, but if you remove the graphic when cell becomes empty by setting it to null, the problem seems to be fixed.
You should undo any modifications done to a cell when a item is added on a call of updateItem where the cell becomes empty anyway, since otherwise empty cells could be shown as if they were non-empty:
#Override
protected void updateItem(Hyperlink item, boolean empty) {
super.updateItem(item, empty);
if (!empty){
item.setOnAction(e -> {
TeDhenatBlerjes(Integer.parseInt(getTableView().getColumns().get(0).getCellData(getTableRow().getIndex())+""), item.getText());
});
}
// set graphic every time i.e. set it to null for empty cells
setGraphic(item);
}
In general the updateItem method should be implemented like this:
#Override
protected void updateItem(ItemType item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
// undo any updates that could have been made
// to make the cell look different from the empty cell
} else {
// update cell to display item
}
}
There TreeView, each element of which implements Viewable interface:
public interface Viewable {
enum ViewStyle {
NEW("-fx-background-color: b8faa7;"),
NEW_PARENT("-fx-background-color: b8ebbb;"),
LOCKED("-fx-background-color: adadad; "),
HAS_NO_DATA("-fx-background-color: eb8d8d;");
String style;
ViewStyle(String style){
this.style = style;
}
public String getStyle() {
return style;
}
}
ViewStyle getViewStyle();
void setViewStyle(ViewStyle style);
StringProperty styleProperty();
String getTreeItemTitle();
void setTreeItemTitle(String title);
StringProperty titleProperty();
}
Each element has its own .styleProperty(), and get value from ViewStyle.getStyle()
This property bind for each TreeCell.styleProperty():
treeView.setCellFactory(new Callback<TreeView<Viewable>, TreeCell<Viewable>>() {
#Override
public TreeCell<Viewable> call(TreeView<Viewable> param) {
return new TreeCell<Viewable>() {
#Override
protected void updateItem(Viewable item, boolean empty) {
textProperty().unbind();
styleProperty().unbind();
if (empty || item == null) {
setGraphic(null);
textProperty().set(null);
styleProperty().set(null);
return;
}
if (item != null) {
styleProperty().bind(item.styleProperty());
textProperty().bind(item.titleProperty());
}
super.updateItem(item, empty);
}
};
}
});
The problem is that tree cells are displayed ugly in the selection. That is the color of the selected cell does not change. Changing only the color of the letters (in accordance with the default theme), but it is not very convenient. Therefore, probably it is necessary to attach .css files. At the same time, I don't understand how to change the style (default and when selected) of the cell depending on the current ViewStyle.
You could simply change the css property to one that is only used for unselected cells (-fx-control-inner-background):
enum ViewStyle {
NEW("-fx-control-inner-background: b8faa7;"),
NEW_PARENT("-fx-control-inner-background: b8ebbb;"),
LOCKED("-fx-control-inner-background: adadad; "),
HAS_NO_DATA("-fx-control-inner-background: eb8d8d;");
Also note that you did something you shouldn't do in a overwritten version of the updateItem method: Not always call super.updateItem. This can lead to the filled/empty pseudoclasses not being assigned correctly and the item property of the TreeCell not containing the item from the latest updateItem call. You should do something like this instead:
#Override
protected void updateItem(Viewable item, boolean empty) {
textProperty().unbind();
styleProperty().unbind();
if (empty || item == null) {
setText(null);
setStyle(null);
} else {
styleProperty().bind(item.styleProperty());
textProperty().bind(item.titleProperty());
}
super.updateItem(item, empty);
}
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).
If I use playList.getSelectionModel().select(1) - the highlighted selection will be the second row of the playlist. If I use playList.getSelectionModel().select(-1) to have no rows selected (as suggested elsewhere on StackOverflow) the first row will be selected. Does anyone have any idea of why this is not working? I would like for the first two rows of the listview to never be eligible for selection.
I am using JavaFX-8.
public class AudioPlayerFXMLController implements Initializable {
#FXML
private ListView playList;
private static ObservableList<Object> playListItems;
private static final String NEW_PLAYLIST = "New PlayList";
private static final String FRIEND_PLAYLIST = "Friend's PlayList";
#Override
public void initialize(URL url, ResourceBundle rb) {
playListItems = FXCollections.observableArrayList();
playListItems.add(NEW_PLAYLIST);
playListItems.add(FRIEND_PLAYLIST);
playList.setItems(FXCollections.observableList(playListItems));
playList.setCellFactory(new Callback<ListView<String>, ListCell<String>>() {
#Override
public ListCell<String> call(ListView<String> list) {
return new ImageCell();
}
});
playList.getSelectionModel().select(-1);
}
static class ImageCell extends ListCell<String> {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if ((this.getIndex() == 0) || (this.getIndex() == 1)) {
ImageView addSymbol;
addSymbol = ImageViewBuilder.create().image(new Image("/images/ic_add_grey600_15dp.png")).build();
addSymbol.fitHeightProperty();
setText(item);
setGraphic(addSymbol);
} else {
setText(null);
setGraphic(null);
}
}
}
}
Depending on which exact (update) version of jdk8 you are using, this might be the fix for RT-25679 (partially reverted in RT-38517) AFAIK, there is no public api to disable the auto-focus/select for the collection views in general. Only ListView has (undocumented! beware, they can change without notice) entries in its properties that disable the default behaviour
// disable selecting the first item on focus gain - this is
// not what is expected in the ComboBox control (unlike the
// ListView control, which does this).
listView.getProperties().put("selectOnFocusGain", false);
// introduced between 8u20 and 8u40b7
// with this, testfailures back to normal
listView.getProperties().put("selectFirstRowByDefault", false);