1

Hiho,

I have a problem with getting ListView to update "nicely". It's a playlist with a bunch of playlist items. Basically, when the style or content of an item changes, I want it to change in the ListView. Currently, I refresh the whole list, which works I guess but it seems like a really poor (unclear) solution to me (and it flickers). Is there a way to refresh/repaint a specific item? I haven't been able to find any.

For reference, each item needs to be updated when the following happens:

  • The file related to the item is being read and eg it fails; or the metadata is retrieved.
  • Or; from user input, eg when changing the current song.

Can I make use of a Listener somehow? I've looked at bindings etc but I don't seem to find what I'm looking for. Any help greatly appreciated

Edited again: Working code below

Initialise the list:

protected Playlist(List<PlaylistItem> list){
    ...

    // initialise the items
    backup = FXCollections.observableArrayList(list);
    setItems(backup);

    // Use a custom CellFactory
    setCellFactory(new Callback<ListView<PlaylistItem>, ListCell<PlaylistItem>>() {
        @Override
        public ListCell<PlaylistItem> call(ListView<PlaylistItem> list) {
            return new PlaylistCell();
        }
    });

    ...
}

The cells created by the factory:

private class PlaylistCell extends ListCell<PlaylistItem> {
    private PlaylistItem lastItem = null;
    private final BooleanProperty booleanProperty = new SimpleBooleanProperty(false);

    /** Add a listener to the boolean property upon construction */
    private PlaylistCell() {
        booleanProperty.addListener( new ChangeListener<Boolean>() {
            @Override
            public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) {
                if (newValue) { updateItem(lastItem, lastItem == null); };
            }
        });
    }

    @Override
    public void updateItem(PlaylistItem item, boolean empty) {
        super.updateItem(item, empty);
        booleanProperty.set(false);

        if (lastItem != item){
            // remove the pointer if we change item
            if (lastItem != null && booleanProperty == lastItem.getBooleanProperty())
                lastItem.setBooleanProperty(null);
            // and attach it to the new item
            if (item != null)
                item.setBooleanProperty(booleanProperty);
        }

        // redraw the cell
        if (!empty && item != null) {
            lastItem = item;

            // current song in bold
            if (item.equals(current)) {
                setId("current-item");
            } else{
                setId(null);
            }

            // mark queued songs & update text
            if (queue.contains(item)) {
                int i = queue.indexOf(item);
                super.setText(item.toString() + "\t (" + (i + 1) + (i != queue.lastIndexOf(item) ? ", ...)": ')'));
            } else {
                super.setText(item.toString());
            }
        }

        // draw an empty cell
        else {
            lastItem = null;
            setText(null);
            setId(null);
        }

    }

}

And then when I eg double-click an item it's to current or when I queue an item, I set the BooleanProperty to true (ie has changed) to trigger the update call.

  • 3
    Can you post some code showing what you are doing? Specifically, if the items in the list use [JavaFX properties](http://www.oracle.com/pls/topic/lookup?ctx=javase80&id=JFXBD107) you can just do this by specifying a list with an [extractor](http://stackoverflow.com/questions/23822550/listview-is-not-reflecting-changes). – James_D Jan 18 '17 at 13:00
  • James_D: Added some code. I tried using a StringProperty quickly but it seemed overly complicated for something so simple since, as far as I understood, I would need one listener for each item. I'll look at extractors a bit either way because I am also able to search through the list. Thanks – user6502063 Jan 30 '17 at 15:25
  • There's not really enough code there to understand what you are doing. But, as I said, you should use an `ObservableList` with an extractor (see link to API docs in previous comment). Also see http://stackoverflow.com/questions/23822550/listview-is-not-reflecting-changes – James_D Jan 30 '17 at 15:30
  • Thanks for your help. I ended up tying a BooleanProperty (java fx bean?) with a ChangeListener to each CellFactory, and each CellFactory to the current item being displayed. Then I made any changes to the Item modify the bean property and updates fire that way. A bit cumbersome but at least I don't have a bunch of CellFactories waiting to be garbage collected (I think :D) I'll probably have to implement an extractor for searching though – user6502063 Feb 01 '17 at 16:32
  • I think you have some major misunderstandings about how this works. There is only one cell factory for the entire list view, and cells (which you represent by a class called `CellFactory`, for some bizarre reason) exist essentially only for the visible cells. So I don't understand what the memory management concerns are - I think you are creating issues that don't exist. If you use an extractor on your list, then the cell's `updateItem(...)` method gets invoked when the property changes, so I don't understand why you would create extra properties specifically for the cell. – James_D Feb 01 '17 at 17:28
  • Ah, yeah, you are right. I was thinking that was very odd. I updated the code again now. So, what I meant I have done is: tie a BooleanProperty with a listener to each Cell and attach it to the item it is displaying. When an item changes and it has a BooleanProperty attached, I set that property to true and the listener fires the updateItem();. I.e., for each visible cell, there's a property and a listener. I wasn't sure how to detect "changes" in another way without heaps of pointers. Thanks a lot for your patience btw. I'll look into Extractors until next time I fiddle with this. – user6502063 Feb 01 '17 at 18:19

1 Answers1

0

Assuming queue is an ObservableList<PlaylistItem> of items in a queue to be played, and current is an ObjectProperty<PlaylistItem> (or something similar) representing the currently playing item, I think you just need something like

private class PlaylistCell extends ListCell<PlaylistItem> {

    public PlaylistCell() {

        textProperty().bind(Bindings.createStringBinding(this::calculateText,
            itemProperty(), emptyProperty(), current, queue);
        idProperty().bind(Bindings
            .when(current.isEqualTo(itemProperty())
            .then("current-item")
            .otherwise(""));
    }

    private String calculateText() {
        if (isEmpty() || getItem() == null) return null ;

        PlaylistItem item = getItem();

        if (queue.contains(item)) {
            int index = queue.indexOf(item) ;
            boolean unique = index == queue.lastIndexOf(item);
            return String.format("%s (%d%s)", item ,index + 1, (unique ? "" : ", ..."));
        } else {
            return item.toString();
        }
    }
}

I'd recommend using a CSS PseudoClass instead of manipulating the id:

private class PlaylistCell extends ListCell<PlaylistItem> {

    private final BooleanBinding isCurrent = current.isEqualTo(itemProperty());
    private final PseudoClass currentPC = PseudoClass.getPseudoClass("current");

    public PlaylistCell() {

        textProperty().bind(Bindings.createStringBinding(this::calculateText,
            itemProperty(), emptyProperty(), current, queue);
        isCurrent.addListener((obs, wasCurrent, isNowCurrent) -> 
            pseudoClassStateChanged(currentPC, isNowCurrent));
    }

    // ...
}

and then change the CSS to use the selector

.list-cell:current { /* ... */ }

instead of

#current-item { /* ... *. }

As an alternative to creating the bindings, you could also create your data list with an extractor, which will cause the usual updateItem() method to be invoked when either the currently playing item, or the queue, change:

ObservableList<PlaylistItem> items = FXCollections.observableArrayList(
    item -> new Observable[] { current, queue /* , other properties the toString() depends on...*/ });

ListView<PlaylistItem> listView = new ListView<>();
listView.setItems(items);

and then

public class PlaylistCell extends ListCell<PlayListItem> {

    private final PseudoClass currentPC = PseudoClass.getPseudoClass("current");


    @Override
    protected void updateItem(PlaylistItem item, boolean empty) {

        pseudoClassStateChanged(currentPC, current.isEqualTo(item));

        if (empty) {
            setText(null);
        } else {
            if (queue.contains(item)) {
                int index = queue.indexOf(item) ;
                boolean unique = index == queue.lastIndexOf(item);
                return String.format("%s (%d%s)", item ,index + 1, (unique ? "" : ", ..."));
            } else {
                return item.toString();
            }
        }
    }
}
Community
  • 1
  • 1
James_D
  • 201,275
  • 16
  • 291
  • 322