I am trying to have a custom ListView made of custom Cell based on a list of custom objects.
The custom object is class name called Message which contains a few fields for the message content, recipient, timestamp and status (read, sent etc.).
After looking at this question : Customize ListView in JavaFX with FXML I have successfully :
created a ListView with custom cells where the cell design is defined in a FXML file ;
associated a controller so that each cell data can be filled with the current item of the collection ;
However, I failed to link both : I cannot seem to find a way so that the current item of the ListView is sent the Cell Controller.
Here is my code for the cell factory and the ListView filling of items:
final ObservableList observableList = FXCollections.observableArrayList();
observableList.setAll(myMessages); //assume myMessage is a ArrayList<Message>
conversation.setItems(observableList); //the listview
conversation.setCellFactory(new Callback<ListView<Message>, ListCell<Message>>() {
#Override
public ConversationCell<Message> call(ListView<Message> listView) {
return new ConversationCell();
}
});
And now, the ConversationCell class :
public final class ConversationCell<Message> extends ListCell<Message> {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
ConversationCellController ccc = new ConversationCellController(null);
setGraphic(ccc.getView());
}
}
I cannot show the ConversationCellController but all I can say, this is where (in its constructor) I load the FXML file that designs the cell and then I can fill the values with the given Message item.
The getView() method returns the root pane that contains the now-filled-and-designed cell.
As I previously say, the designing work, but I cannot seem to link the ListView items with the CellFactory because in method
protected void updateItem(Message item, boolean empty)
empty is set to true and item is indeed null.
What can I do to make this work ?
All custom cell implementations that override updateItem(...) need to deal with the case where the cell is empty in that method. So you could do a naïve fix of this with
public final class ConversationCell<Message> extends ListCell<Message> {
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
// did you mean to pass null here, or item??
ConversationCellController ccc = new ConversationCellController(null);
setGraphic(ccc.getView());
}
}
}
However, this is not a good solution from the point of view of performance. You are loading the FXML every time updateItem(...) is called with a non-empty cell, and that's a pretty expensive operation (potentially involving file i/o, unzipping the FXML file from a jar file, parsing the file, lots of reflection, creating new UI elements, etc). You don't want to be asking the FX Application Thread to be doing all that work every time the user scrolls the list view by a few pixels. Instead, your cell should cache the node and should update it in the updateItem method:
public final class ConversationCell<Message> extends ListCell<Message> {
private final ConversationCellController ccc = new ConversationCellController(null);
private final Node view = ccc.getView();
#Override
protected void updateItem(Message item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setGraphic(null);
} else {
ccc.setItem(item);
setGraphic(view);
}
}
}
You should define a setItem(...) method in the ConversationCellController that updates the view (sets text on labels, etc etc) accordingly.
Related
I am having a standard TreeView in JavaFX with CheckBoxTreeItem in it. I've installed a listener to see when someone checks/ unchecks a checkbox. But I want that when someone check/unchecks a checkbox I trigger that checkboxitem's parent updateItem method and change his CSS ( for example if 3 or more childs are selected for a parent then change his color to red, otherwise green).
How can I do that?
rootItem.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), e -> {
if (e.getTreeItem().isLeaf()) {
TreeItem<String> treeItem = (TreeItem) e.getTreeItem();
CheckBoxTreeItem<String> parentItem = (CheckBoxTreeItem<String>) treeItem.getParent();
// how to call repaint for the parentItem????
}
});
treeView.setCellFactory(p -> new CheckBoxTreeCell<>() {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
// toggle the parent's CSS here
}
});
I agree with the answer by M. S. regarding the use of PseudoClass. However, you should not be trying to manually invoke updateItem. Instead, just add an EventHandler to listen for "check box selection changed" events. When an event occurs in a direct child, the parent should update the pseudo-class based on (using your example) whether or not 3+ children are selected.
Here's an example which also includes a "branch" PseudoClass so you can distinguish between a branch and a leaf in the CSS file:
import javafx.beans.InvalidationListener;
import javafx.beans.WeakInvalidationListener;
import javafx.css.PseudoClass;
import javafx.event.EventHandler;
import javafx.event.WeakEventHandler;
import javafx.scene.control.CheckBoxTreeItem;
import javafx.scene.control.CheckBoxTreeItem.TreeModificationEvent;
import javafx.scene.control.cell.CheckBoxTreeCell;
public class MyCheckBoxTreeCell<T> extends CheckBoxTreeCell<T> {
private static final PseudoClass BRANCH = PseudoClass.getPseudoClass("branch");
private static final PseudoClass THREE_CHILDREN_SELECTED = PseudoClass.getPseudoClass("three-children-selected");
// event handler to listen for selection changes in direct children
private final EventHandler<TreeModificationEvent<T>> handler = event -> {
/*
* Event starts from the source TreeItem and bubbles up the to the root. This means
* the first time getTreeItem() != event.getTreeItem() will be the source TreeItem's
* parent. We then consume the event to stop it propagating to the next parent.
*/
if (getTreeItem() != event.getTreeItem()) {
event.consume();
updatePseudoClasses();
}
};
private final WeakEventHandler<TreeModificationEvent<T>> weakHandler = new WeakEventHandler<>(handler);
// Used to listen for the "leaf" property of the TreeItem and update the BRANCH pseudo-class
private final InvalidationListener leafListener = observable -> updatePseudoClasses();
private final WeakInvalidationListener weakLeafListener = new WeakInvalidationListener(leafListener);
public MyCheckBoxTreeCell() {
getStyleClass().add("my-check-box-tree-cell");
// add listener to "treeItem" property to properly register and unregister
// the "leafListener" and "handler" instances.
treeItemProperty().addListener((observable, oldValue, newValue) -> {
if (oldValue != null) {
oldValue.leafProperty().removeListener(weakLeafListener);
oldValue.removeEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), weakHandler);
}
if (newValue != null) {
newValue.leafProperty().addListener(weakLeafListener);
newValue.addEventHandler(CheckBoxTreeItem.checkBoxSelectionChangedEvent(), weakHandler);
}
updatePseudoClasses();
});
}
private void updatePseudoClasses() {
/*
* Assumes the use of CheckBoxTreeItem for each TreeItem in the TreeView.
*
* This code is not the most efficient as it will recalculate both the BRANCH and
* THREE_CHILDREN_SELECTED pseudo-classes each time either possibly changes.
*/
var item = (CheckBoxTreeItem<T>) getTreeItem();
if (item == null) {
pseudoClassStateChanged(BRANCH, false);
pseudoClassStateChanged(THREE_CHILDREN_SELECTED, false);
} else {
pseudoClassStateChanged(BRANCH, !item.isLeaf());
int selected = 0;
for (var child : item.getChildren()) {
// only need to know if *at least* 3 children are selected
if (((CheckBoxTreeItem<T>) child).isSelected() && ++selected >= 3) {
break;
}
}
pseudoClassStateChanged(THREE_CHILDREN_SELECTED, selected >= 3);
}
}
// No need to override "updateItem(T,boolean)" as CheckBoxTreeCell provides
// the necessary implementation which can be customized via the StringConverter
// property.
}
And then your CSS file could look like:
.my-check-box-tree-cell:branch {
-fx-background-color: green;
-fx-text-fill: white;
}
.my-check-box-tree-cell:branch:three-children-selected {
-fx-background-color: red;
-fx-text-fill: white;
}
Addressing questions in comments:
Why wrapping every listener inside a weak one if we take care to unsubscribe it?
To decrease the chance of memory leaks. For instance, if you throw away the TreeView (without having cleared the root property) but maintain references to the TreeItems somewhere, then a non-weak handler/listener would hold the TreeCells and the TreeView in memory.
Why are you listening for leaf changes and when does it gets called?
To handle the case where TreeItems are dynamically added and/or removed. A TreeItem is a leaf if and only if its children list is empty. If an item is added, and the leaf now becomes a branch, we need to update the BRANCH pseudo-class in order to have the proper CSS applied. Same if an item is removed and a branch becomes a leaf.
This may or may not be relevant to your use case. If not, then feel free to remove this part of the implementation.
You check getTreeItem() != event.getTreeItem()) in the checkbox checked handler. Why? This will be called when a checkbox gets checked/ unchecked.
When you (un)check a CheckBoxTreeItem it fires an event. This event begins its journey at the CheckBoxTreeItem that was (un)checked. From there, it travels up (i.e. bubbles) the item hierarchy all the way to the root. At each item, any registered handlers will be invoked. Though if the event is consumed it does not proceed to the next parent item.
The reason we're adding the handler is to listen for any children being (un)checked—but only direct children. We don't care about changes in arbitrarily deep descendants nor the item the handler was registered to.
Since we only care about changes in direct children, we need to make sure we only react to events fired by said children. As the event is processed first by the item that was (un)checked, we need to not do anything in that first handler. We can do this by testing if the TreeItem of the containing TreeCell is the same one that fired the event, and the TreeModificationEvent#getTreeItem() method returns the item that caused the event to be fired (i.e. the item that was (un)checked). If they are the same instance, do nothing and let the event bubble up to the parent.
Now the parent item's handler is processing the event. This means getTreeItem() != event.getTreeItem() will return true and we enter the if block. This causes the update, if necessary, of the pseudo-classes' state. We then consume the event so it doesn't bubble up to the next parent; this effectively makes the handler only listen to events from direct children.
Note that if the parent item is not currently being displayed in the tree, then it will not be part of a cell. If its not part of a cell, it won't have had the handler added to it. Thus any non-displaying items won't be affected by any of this. This is okay since everything we're updating is purely visual; if an item isn't being displayed then there's no visuals to update.
You don't need to change it manually, you can use a PseudoClass:
private PseudoClass threeChildrenClass = PseudoClass.getPseudoClass("three-children");
tree.setCellFactory(param -> new CheckBoxTreeCell<String>() {
#Override
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (item == null || empty) {
setText(null);
setGraphic(null);
} else {
setText(item);
// Change the class based on the number of parent items
pseudoClassStateChanged(threeChildrenClass, hasThreeChildren(item));
}
}
});
In your CSS file:
.check-box-tree-cell:three-children {
-fx-background-color: red;
}
It looks like CheckBoxTreeCell doesn't have a built-in "checked" pseudo-class, you can add a "checked" PseudoClass and apply it when the tree cell is checked. Then you can call it like this:
.check-box-tree-cell:three-children:checked {
-fx-background-color: green;
}
As I wrote in the topic, I don't know how to do that.
When I add PDF to my ListViewit shows up to me a file directory, not a name.
I've tried to use a setCellFactory() method, but I also don't know how to use it
Sorry for my English.
Use a cellFactory that returns cells that display the item according to your needs:
ListView<File> listView = ...
listView.setCellFactory(lv -> new ListCell<File>() {
#Override
protected void updateItem(File item, boolean empty) {
super.updateItem(item, empty);
if (empty || item == null) {
// restore empty look of the cell
setText("");
} else {
// set cell contents based on item
setText(item.getName());
}
}
});
If you adjust this code be aware of the fact that ListView may change the item of a ListCell to different items and a cell may become empty after a item was associated with it.
I try to create TableColumn with ChoiceBoxTableCell. Choices in this ChoiceBox are dynamically generated (and changed over time) based on item associate with current row. I tried different approaches, but nothing seems to work.
I would like to have something like this:
private DataProvider dataProvider;
private TableColumn<Phone, String> testColumn;
public void initialize() {
testColumn.setCellFactory(param, phone -> new ChoiceBoxTableCell<Phone, String>(dataProvicer.get(phone)));
}
Where:
public interface DataProvider {
ObservableList<String> get(Phone phone);
}
This is my ideal code I would like to have, but as you know setCallFactory takes Callback with TableColumn<S,T> as function parameter and there is no way to access it within CellFactory. I could probably do some dirty and ugly hacks to get why I want, but I would love to have some nice solution.
A reminder of the basic mechanism: a cellFactory is used to create any cell of for the given column. The calling code (that is the VirtualFlow deep inside the implementation of table's skin) isn't interested in or don't event know which row the cell is created for. Also, it will be re-used - that is setting a new item - quite often. In all, the moment of creating the cell is not the right time to configure the cell with row-related data. This has to be done later, once the row is known: the most obvious candidate is updateItem(T, boolean).
Now back to the concrete ChoiceBoxTableCell: unfortunately, its implementation is too dumb and simply doesn't support dynamic updates of its choice items. So you need a custom extension which does support the dynamics. On the bright side: ChoiceBoxTableCell exposes its items, thus allowing to change its contents as needed.
As noted in the code comment, it turned out that the obvious hook didn't work out nicely. So had to move the config into the startEdit method.
Some code:
public interface ChoiceItemProvider<S, T> {
ObservableList<T> getItems(S source);
}
public class DynamicChoiceBoxTableCell<S, T> extends ChoiceBoxTableCell<S, T> {
private ChoiceItemProvider<S, T> provider;
public DynamicChoiceBoxTableCell(ChoiceItemProvider<S, T> provider) {
super();
this.provider = provider;
}
/**
* Not so obvious hook: overridden to update the items of the
* choiceBox.
*/
#Override
public void startEdit() {
super.startEdit();
updateItems();
}
/**
* Obvious hook: override to update the items of the choiceBox.
* Not fully working - for some reason, the current item isn't
* selected after starting the edit.
*/
#Override
public void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
// updateItems();
}
/**
* Dynamically updates the items to current rowItem.
*/
#SuppressWarnings("unchecked")
protected void updateItems() {
TableRow<S> tableRow = getTableRow();
S rowItem = tableRow != null ? tableRow.getItem() : null;
if (provider == null || rowItem == null) return;
if (provider != null) {
getItems().setAll(provider.getItems(rowItem));
}
}
}
Addendum re:
no ideal, because items won't be updated when it is already expanded
If you need that, you can bind the choiceBox' items to the items returned by the provider, that is instead of calling setAll(provider.getItems()) do:
Bindings.bindContent(getItems(), provider.getItems());
testColumn.setCellFactory(ChoiceBoxTableCell.forTableCoulmn(<dynamic list>));
This should work.
I currently have a ListView that is resizable in Width and I use a custom CellFactory that returns my custom ListCell objects.
I already read this:
Customize ListView in JavaFX with FXML and I use a similar construct like the one mentioned:
public class ListViewCell extends ListCell<String>{
#Override
public void updateItem(String string, boolean empty){
super.updateItem(string,empty);
if(string != null) {
...
setGraphic(myComponent);
}
}
I want myComponent to take the full size that is available inside the ListCell, however it seems that the setGraphic method limits the size of the given Node.
I can provide all my code if it is necessary, however I am not sure which parts are relevant and I do not want to post a big wall of code.
Try setting prefWidth property of your control to Infinity either via CSS or via API so as to provide its maximal expansion whithin container:
myComponent.setStyle("-fx-pref-width: Infinity");
I solved my Problem now, here is how:
class MyListCell extends ListCell<MyObject>
{
private final AnchorPane _myComponent;
public MyListCell()
{
...
this.setPrefWidth(0);
myComponent.prefWidthProperty().bind(this.widthProperty());
}
...
#Override
protected void updateItem(MyObject item, boolean empty)
{
...
setGraphic(_myComponent);
...
}
}
If this.setPrefWdith(0) is ommited, myComponent will grow inside the ListCell, but the Cell won't shrink if I shrink my ListView.
I have a class MyTreeTableView extending TreeTableView
I have implmented few methods to populate the treeview by passing an observable list in my derived class, which is not available in the original TreeTableView. Other than that there are no additional functionalities.
I am able to use this class and it works as intended when coding in Java
i.e
MyTreeTableView localtree = new MyTreeTableView(new TreeItem<>());
localtree.setItems(myobservedList);
If I add this instance to a scene everything works like a standard TreeTableView.. To add this instance to the scene I have to do this on the Java side.
I have to make my class MyTreeTableView in to a custom FXML component so that its accessible using scene builder for easy drag and drop.
Before venturing on to the custom component side of FXML, I tried to assign the localtreetable to a standard Java FX 8 TreeTableView which is defined in the FXML and is part of the existing scene.
I did the following
localtreetable.getColumns().addAll(col1,col2);
localtreetable.setShowRoot(false);
localtreetable.setItems(myobservedList);
JavaFX8TreeTableView.setRoot(localtreetable.getRoot());
col1 = (TreeTableColumn<Object, String>) JavaFX8TreeTableView.getColumns().get(0);
col2 = (TreeTableColumn<Object, String>) JavaFX8TreeTableView.getColumns().get(1);
//setCellValueFactory and CellFactories for col1 and col2 after this.
Here col1 and col2 are defined in the FXML and they are the columns of the JavaFX8TreeTableView which is also defined in the FXML.
With this hack, the JavaFX8TreeTableView shows up on the UI with the data populated in the
localtreetable object. However the UI is not correctly intended and the tree view is messed up. When I say messed up, the tree is in proper order, but the > is placed on the 4th letter of a tree item and a click on the arrow doesn't expand or close the tree item, you have to click it at some other location for it to expand and close it.
My first issue is how do I transpose values from localtreetable to a JavaFx8TreeTableView, so that I can populate values to an already defined TreeTableView
When migrating to Java8, there is bug in the JavaFX 2.2 while populating the TreeItem, branchExpand and branchCollapse events, some how there are empty TreeItem are getting added up. You can fix that bug by explicitly writing
setGraphic(null);
setText(null);
in the updateItem function
public void updateItem(String item, boolean empty) {
super.updateItem(item, empty);
if (empty) {
setText(null);
setGraphic(null);
} else {
if (isEditing()) {
if (textField != null) {
textField.setText(getString());
}
setText(null);
setGraphic(textField);
} else {
setText(getString());
setGraphic(getTreeItem().getGraphic());
if (
!getTreeItem().isLeaf()&&getTreeItem().getParent()!= null
){
setContextMenu(addMenu);
}
}
}
Refer
http://docs.oracle.com/javase/8/javafx/user-interface-tutorial/tree-view.htm#BABDEADA