0

I'm trying to fill a listview with the artist and title of songs using the open() method. To achieve this I created the artist and title ArrayLists and merged them using the create() method.

The problem is, when I try to run create() inside open() nothing happens. However, if I assign the create() method to a different button and click it after using the filechooser everything works fine.

So, I would like to know if it is possible to run the create() method after the open() method using only one button via fxml or regular java code.

public class PLController implements Initializable {

@Override
public void initialize(URL location, ResourceBundle resources) {        
    list.setItems(visibleList);
}

List<File> filelist = new ArrayList<File>();
ArrayList<String> title = new ArrayList<String>();
ArrayList<String> artist = new ArrayList<String>();
ObservableList<String> visibleList = FXCollections.observableArrayList();

@FXML
ListView<String> list;
@FXML
Button impButton;

public void create(){
    for(int i = 0; i < title.size(); i++){
    visibleList.add(artist.get(i) +" - " +title.get(i));
    Collections.sort(visibleList);
    }
}

public void handleMetadata(String key, Object value){
    if (key.equals("title")){
        title.add(value.toString());
    }
    if (key.equals("artist")){
        artist.add(value.toString());
    }
}

public void open(){
    FileChooser chooser = new FileChooser();
    filelist = chooser.showOpenMultipleDialog(impButton.getScene().getWindow());
    for(File f:filelist){
    try {
        Media media = new Media(f.toURI().toURL().toString());
        media.getMetadata().addListener(new MapChangeListener<String, Object>(){
            @Override
            public void onChanged(Change<? extends String, ? extends Object> change) {
                if(change.wasAdded()) {
                    handleMetadata(change.getKey(), change.getValueAdded());
                }
            }
        }); 
    } catch (MalformedURLException e) {
        e.printStackTrace();
        }
    }create(); //Nothing happens
}
Peralta
  • 180
  • 1
  • 12
  • What does "nothing happens" mean? What do you expect to happen? Why are you sorting the list on all iterations of the loop? Are you aware of [`Platform.runLater`](http://stackoverflow.com/questions/12984310/javafx-response-to-swingutilities-invokelater)? – m0skit0 May 04 '15 at 17:31
  • It means literally nothing happens, I expect the `listview` to be filled with the values in `visibleList`. I was not aware of `Platform.runLater`, will check it out. – Peralta May 04 '15 at 18:02
  • Make sure the list is not empty. If it is not, you should **always** update the GUI in the UI thread (in case of JavaFX through `Platform.runLater`). – m0skit0 May 04 '15 at 18:24
  • Where is `visibleList` associated with the `ListView`? – James_D May 04 '15 at 18:29
  • @m0skit0 apparently the list is empty while the `open()` method doesn't end, therefore I cannot call `create()` inside it. @James_D at `list.setItems(visibleList);`4th line – Peralta May 04 '15 at 18:44
  • Peralta: Thanks - sorry I missed that somehow... @m0skit0 The listener on the metadata will be invoked on the FX Application Thread anyway. – James_D May 04 '15 at 20:00

2 Answers2

1

As others have pointed out, the Media object does not have its metadata initialized immediately. (It needs to read data from the URL and populate those metadata as it receives them.) That is why the metadata are exposed as an ObservableMap. When you reach the end of your open() method, it is highly unlikely that the metadata will have been initialized, so your create() method will not see any data at that point.

What you need to do is observe the map, and update the ListView once both the artist and title are available. The best way to do this, in my opinion, is to encapsulate the information you want into a separate class:

public class Video {
    private final Media media ;
    private final ReadOnlyStringWrapper artist = new ReadOnlyStringWrapper("Unknown");
    private final ReadOnlyStringWrapper title = new ReadOnlyStringWrapper("Title");

    public Video(File file) {
        try {
            this.media = new Media(file.toURI().toURL().toExternalForm());

            artist.bind(Bindings.createStringBinding(() -> {
                Object a = media.getMetadata().get("artist");
                return a == null ? "Unknown" : a.toString();
            }, media.getMetadata()));

            title.bind(Bindings.createStringBinding(() -> {
                Object t = media.getMetadata().get("title");
                return t == null ? "Unknown" : t.toString();
            }, media.getMetadata()));
        }
        catch (Exception e) {
            throw new RuntimeException("Could not create Video for "+file, e);
        }
    }

    public ReadOnlyStringProperty titleProperty() {
        return title.getReadOnlyProperty();
    }

    public ReadOnlyStringProperty artistProperty() {
        return artist.getReadOnlyProperty();
    }

    public final String getTitle() {
        return title.get();
    }

    public final String getArtist() {
        return artist.get();
    }

    public final Media getMedia() {
        return media ;
    }
}

Now you can create a ListView<Video> to display the videos. Use a cell factory to display the artist and the title in the format you want. You can make sure that the observable list fires updates when either the artist or title properties change, and you can keep it sorted via a SortedList.

@FXML
private ListView<Video> list ;

private ObservableList<Video> visibleList ;

public void initialize() {
    visibleList = FXCollections.observableArrayList(
        // make list fire updates when artist or title change:
        v -> new Observable[] {v.artistProperty(), v.titleProperty()});

    list.setItems(new SortedList<>(list, Comparator.comparing(this::formatVideo)));

    list.setCellFactory(lv -> new ListCell<Video>() {
        @Override
        public void updateItem(Video item, boolean empty) {
            super.updateItem(item, empty) ;
            setText(formatVideo(item));
        }
    });
}

@FXML
private void open() {
    FileChooser chooser = new FileChooser();
    List<File> fileList = chooser.showOpenMultipleDialog(impButton.getScene().getWindow());
    if (fileList != null) {
        fileList.stream()
        .map(Video::new)
        .forEach(visibleList::add);
    }
}

private String formatVideo(Video v) {
    if (v == null) return "" ;
    return String.format("%s - %s", v.getArtist(), v.getTitle());
}
James_D
  • 201,275
  • 16
  • 291
  • 322
  • Thanks, lots of concepts here I don't understand though, I'll study a bit and try to implement this – Peralta May 04 '15 at 23:01
0

Simply creating a Media object and assigning a listener to it won't fire the code in the listener. So the title list in your code remains empty. The create() method is called, but since you are iterating over an empty list, nothing actually happens.

Use a debugger or add some logging information in such cases.

Also, you should sort the list after the for loop, not every time you add an item.

Roland
  • 18,114
  • 12
  • 62
  • 93
  • But why does it work if I call `create()` with a separate button? – Peralta May 04 '15 at 18:11
  • Because of a race condition. Eventually the change listener fires. After that your title list is filled. And after that you click the button manually. Just output some logging and you'll see. – Roland May 04 '15 at 19:17
  • So how do I make the `create()` method wait for the listener to fire? Just add `.wait(x amount of time)`? – Peralta May 04 '15 at 19:54
  • Invoke the logic to update `visibleList` from inside the handler on the metadata. You need to be a little careful, as that handler will be invoked each time the map changes. – James_D May 04 '15 at 19:57