Task: Check if item is in visible region of ListView.
Solution: I have JavaFX ListView containing items to render. In order to figure which items are in visible region of ListView I implemented cell factory which calculates number of items being displayed to user.
So, basically, what I need to do:
1. Add item
2. Check if it is visible in ListView.
The problem: In order to calculate items, item adding thread (calling thread) must wait for cell factory to complete item adding operation and rendering. However, I don't know how to implement it as calling thread doesn't know when JavaFX UI thread finishes rendering using internal Quantum Toolkit mechanics. Cell factory items are being rendered in separate threads inside JavaFX which is not accessible to synchronize with.
Adding rough calling thread delay solves an issue which clearly indicates threads synchronizing issue but I need more elegant and clear solution.
public class MessengerServiceContext {
@Override
public void messageReceived(final MessageReceivedEvent messageReceivedEvent) {
...
//Calling thread method
messengerServiceControl.receiveMessage(messengerMessageData);
//Main thread is paused for several seconds to wait JavaFX UI threads.
//Ugly and erroneous
//Demonstrates the cause of the problem
try {
Thread.sleep(2000);
} catch (InterruptedException ex) { Logger.getLogger(MessengerServiceControl.class.getName()).log(Level.SEVERE, null, ex);
}
if (!messengerServiceControl.getMessageElementControlVisibility(messengerMessageData)) {
int newMessagesCount = getNewMessagesCount().get();
getNewMessagesCount().set(++newMessagesCount);
}
}
}
public class MessengerServiceControl implements Initializable {
...
private TrackingListCellFactory<MessageElementControl> messengerOutputWindowListViewCellFactory;
...
//Calling (message processing) thread method which inserts ListView item
public void receiveMessage(final MessengerMessageData messengerMessageData) {
//Calling MessengerServiceControl model method to insert items in ListView using JavaFX UI thread
MessageElementControl messageElementControl = model.createMessage(messengerMessageData, false);
//Tried scene and parent property
messageElementControl.sceneProperty().addListener((observable, oldValue, newValue) -> {
if (newValue != null) {
if (!getMessageElementControlVisibility(messengerMessageData)) {
int newMessagesCount = getNewMessagesCount().get();
getNewMessagesCount().set(++newMessagesCount);
}
}
}
boolean getMessageElementControlVisibility(final MessengerMessageData messengerMessageData) {
return messengerOutputWindowListViewCellFactory.getItemVisibility(messengerMessageData);
}
//Cell factory class which is responsible for items rendering:
private static class TrackingListCellFactory<T extends MessageElementControl> implements Callback<ListView<T>, ListCell<T>> {
//Items which have cells visible to the user
private final Set<T> visibleItems = new HashSet();
TrackingListCellFactory() {
}
boolean getItemVisibility(final MessengerMessageData messengerMessageData) {
synchronized (this) {
Optional<T> messageElementControlOptional = visibleItems.stream().filter((item) -> {
return item.getMessageData().getMessageCreatedDate().isEqual(messengerMessageData.getMessageCreatedDate());
}).findFirst();
return messageElementControlOptional.isPresent();
}
}
@Override
public ListCell<T> call(ListView<T> param) {
//Create cell that displays content
ListCell<T> cell = new ListCell<T>() {
@Override
protected void updateItem(T item, boolean empty) {
super.updateItem(item, empty);
if (!empty && item != null) {
setGraphic(item);
}
}
};
//Add and remove item when cell is reused for different item
cell.itemProperty().addListener((observable, oldItem, newItem) -> {
synchronized (TrackingListCellFactory.this) {
if (oldItem != null) {
visibleItems.remove(oldItem);
}
if (newItem != null) {
visibleItems.add(newItem);
}
}
});
//Update set when bounds of item change
ChangeListener<Object> boundsChangeHandler = (observable, oldValue, newValue) -> {
synchronized (TrackingListCellFactory.this) {
T item = cell.getItem();
if (item != null) {
visibleItems.add(item);
}
}
});
//Must update either if cell changes bounds, or if cell moves within scene (e.g.by scrolling)
cell.boundsInLocalProperty().addListener(boundsChangeHandler);
cell.localToSceneTransformProperty().addListener(boundsChangeHandler);
return cell;
}
}
}