0

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;                                               
             }
         }
     }
Andreas
  • 347
  • 2
  • 9
  • Have a look at https://stackoverflow.com/questions/46474385/how-to-find-the-indices-of-the-visible-rows-in-a-tableview-in-javafx-9, which has similar functionality for table view rows. – James_D Nov 12 '17 at 20:39
  • Thank you for your answer. I did a quick code review of the sample you provided which shows pretty much the same technic used with TableView. However, my problem lies in multithreading issue rather than in calculation logic. So, basically, I need to be sure item was added and rendered in factory before I call getItemVisibility() factory method in calling thread. – Andreas Nov 12 '17 at 20:51
  • http://stackoverflow.com/help/mcve – kleopatra Nov 12 '17 at 23:25
  • which part of "minimal, complete, verifiable example" is so hard to understand that you can't provide it ;) – kleopatra Nov 13 '17 at 16:09
  • I have a question in return. Which part of this topic is so hard to understand for you that you need me to provide more of a "minimal, complete, verifiable example"? – Andreas Nov 13 '17 at 19:17
  • I added some details but this is not simple command line 10 rows problem. It is complex JavaFX based solution and putting "minimal, complete, verifiable example" is not possible. Most of the problems/solutions here don't need to be "minimal, complete, verifiable" to grasp and help. If, of course, it is possible for some individual to grasp and help... – Andreas Nov 13 '17 at 19:35
  • it's always possible - most probably the problem is in the code you are not showing ;) stripping down to the barest bones that demonstrates the problem is always part of the digging process, whether or not you post a question so there's no additional effort on your side. On the other hand: without that example it would be crystal ball reading on our side .. rarely anybody has enough free time to invest in such a 1 : infinity chance of hitting the underlying reason ... – kleopatra Nov 13 '17 at 23:39
  • Stop editing your question with minor edits to bump the post. – Andy Nov 15 '17 at 21:50

1 Answers1

0

After several days of mangling with cross-threads management, I concluded that the best possible way of solving the issue is indeed moving the calculation logic inside cell factory itself thus doing everything in UI thread. So, basically it works like a charm:

//Add and remove item when cell is reused for different item
            final ChangeListener<T> itemChangedEventHandler = (observable, oldValue, newValue) -> {
               synchronized (TrackingListCellFactory.this) {
                    if (oldValue != null) {
                        visibleItems.remove(oldValue);
                    }

                    if (newValue != null) {
                        visibleItems.add(newValue);
                        updateMessengerServiceControlModel(newValue, MessageStatus.MessageStatusEnum.SEEN);           
                    }                  
                }
            };

            //Update set when bounds of item change
            final ChangeListener<Object> boundsChangedHandler = (observable, oldValue, newValue) -> {
                synchronized (TrackingListCellFactory.this) {
                    T item = cell.getItem();                    

                    if (item != null) {
                        visibleItems.add(item);
                        updateMessengerServiceControlModel(item, MessageStatus.MessageStatusEnum.SEEN);
                    }                    
                }
            };

            cell.itemProperty().addListener(itemChangedEventHandler);

            //Must update either if cell changes bounds, or if cell moves within scene (e.g. by scrolling):
            cell.boundsInLocalProperty().addListener(boundsChangedHandler);
            cell.localToSceneTransformProperty().addListener(boundsChangedHandler); 

            return cell;

IMHO, this is more clean and elegant solution than using item indexes etc. Synchronization block can be removed if visible items are not read from another thread. This will increase factory performance.

Andreas
  • 347
  • 2
  • 9