Search code examples
javauser-interfacejavafxtableviewmedia-player

How to highlight current playing song in playlist TableView?


I am programming some sort of a music player app, and I have my playlist table on the left, and I'm trying to highlight the current playing song in the playlist. This works fine when I play the song by double clicking it from the playlist, however, my problem is when a new song automatically plays after the current one is over, or when I click the next or previous buttons.

The way I handled the highlighting by double clicking, is by doing it directly in the row factory and adding it and adding that functionality to the TableRow.onMouseClick Event Handler.

I've been trying to get this to work from the next and previous buttons for the past couple of days with no success.

My current idea is as follows: add some sort of custom event listener in the row factory, which listens to a change in the current played index in the playlist it self, but I'm not sure how to implement that, as I am very new to JavaFX and GUI in general.

Here is a pic of how the app looks when playing a song by double clicking it: Application

Here are some of the relevant code parts:

A couple of global variables:

private TableRow<PlaylistTable> lastPlayed;
private TableRow<PlaylistTable> currentPlayed;

The PlaylistTable Constructor:

private final Song song;
private final SimpleStringProperty songTitle;
private final SimpleStringProperty artist;
private final SimpleStringProperty album;
private final SimpleStringProperty trackNumber;

public PlaylistTable(LibraryTable row){
    this.song = row.getSong();
    this.songTitle = new SimpleStringProperty(row.getSongTitle());
    this.artist = new SimpleStringProperty(row.getArtist());
    this.album = new SimpleStringProperty(row.getAlbum());
    this.trackNumber = new SimpleStringProperty(row.getTrackNumber());
}

The row Factory with the onMouseClick Event Handler inside the initialize method of the controller class:

 playlistTable.setRowFactory(e -> {
        TableRow<PlaylistTable> row = new TableRow<>();

        row.setOnMouseClicked(e1 -> {

            if (e1.getButton() == MouseButton.PRIMARY && e1.getClickCount() == 2) {
                if (lastPlayed == null) {
                    lastPlayed = row;
                } else if (lastPlayed != row) {
                    lastPlayed.setStyle("");  //("-fx-background-color: #39CCCC;-fx-border-color: white; -fx-text-fill: white;");
                    lastPlayed = row;
                }
                playSongFromPlaylist();
                currentPlayed = row;

                row.setStyle("-fx-background-color: #FFDC00;-fx-border-color: black; -fx-text-fill: white;");
                System.out.println("highlighting row");


            }
        });

When pressing the next button.

public void onNext() {
    if (mediaPlayer != null) {
        MediaPlayer.Status status = mediaPlayer.getStatus();
        if (status == MediaPlayer.Status.PLAYING || status == MediaPlayer.Status.PAUSED) {
            mediaPlayer.stop();
            playlist.next().orElse(0);
            playSong();
           /* Song song = playlist.getCurrentSong().get();
            File songFile = song.getFilePath().toAbsolutePath().toFile();
            currentSong = new Media(songFile.toURI().toString());
            mediaPlayer = new MediaPlayer(currentSong);
            mediaPlayer.setAutoPlay(false);
            mediaPlayer.play();
            currentSongName.setText(song.getMetadata().getSongTitle().orElse("Unknown Song"));
            playing = true;
            playButton.setText("\u23F8");*/
        } else if (status == MediaPlayer.Status.READY) {
            playlist.setCurrent(0);
            playSong();
        }
    }

This method calls the actual method which plays a song, which is playSongFromPlaylist, but that method is a bit irrelevant here as it takes care or everything, actually controlling the music player it self and the way it looks.

public void playSong() {
    if (!playlist.getCurrentSong().isPresent()) {
        System.out.println("returning");
        return;
    }

    System.out.println(playlistTable.getSelectionModel().getSelectedIndex());
    int currentIndex = playlist.getCurrent().get();
    Song song = playlist.getCurrentSong().get();
    System.out.println("current index: " + currentIndex);

    playlistTable.getSelectionModel().select(currentIndex);
    selectedFromButtons = true;
    System.out.println(playlistTable.getSelectionModel().getSelectedIndex());
    playSongFromPlaylist();
}

Solution

  • Probably the simplest way would be to introduce a property for the currently playing song and (un)set a CSS pseudoclass on item and song change:

    final ObjectProperty<PlaylistTable> currentSong = new SimpleObjectProperty();
    final PseudoClass playing = PseudoClass.getPseudoClass("playing");
    
    playlistTable.setRowFactory(e -> {
    
        final TableRow<PlaylistTable> row = new TableRow<>();
        InvalidationListener listener = o -> {
            row.pseudoClassStateChanged(playing, currentSong.get() == row.getItem());
        };
        row.itemProperty().addListener(listener);
        currentSong.addListener(listener);
    
        ...
    
        return row;
    });
    

    By adding the following style sheet to the scene you can style the rows:

    /* filled pseudoclass is important here to prevent selection
       of empty rows when no song is playing */
    .table-view .table-row-cell:filled:playing {
        -fx-background-color: #FFDC00;
        -fx-border-color: black;
        -fx-text-fill: white;
    }
    

    This allows you to highlight the row containing the song currently playing simply by setting currentSong's value.

    You need to remove the styling from the event handler. This wouldn't work properly anyway, since the the row's item property can be modified by TableView.

    Note: You may also want to add different rules to display a song that is both playing and selected/focused differently.