TableListBox not repainting new rows

Hello, obligatory I am a beginner/new to C++/new to JUCE message. My code probably is quite amateurish, I hope you’ll forgive me for that! I am using Windows and VS22 if that is of any relevance.

I am currently working on a uni project, part of this project is getting files from the user and adding them to a playlist (TableListBox) which they can then select from to be played.

I have the following class which is utilising the TableListBox:

PlaylistComponent::PlaylistComponent()
{
    trackTitles.push_back("Wheels on the bus");
    
    tableComponent.getHeader().addColumn("Track Title", 1, 250);
    tableComponent.getHeader().addColumn("", 2, 50);
    tableComponent.getHeader().addColumn("Deck", 3, 100);
    tableComponent.setModel(this);

    addAndMakeVisible(tableComponent);

}

PlaylistComponent::~PlaylistComponent()
{
    tableComponent.setModel(nullptr); // This is important to avoid a dangling pointer
}

void PlaylistComponent::paint (juce::Graphics& g)
{
    /* This demo code just fills the component's background and
       draws some placeholder text to get you started.

       You should replace everything in this method with your own
       drawing code..
    */

    g.fillAll (getLookAndFeel().findColour (juce::ResizableWindow::backgroundColourId));   // clear the background

    g.setColour (juce::Colours::grey);
    g.drawRect (getLocalBounds(), 1);   // draw an outline around the component

    g.setColour (juce::Colours::white);
    g.setFont (14.0f);
    g.drawText ("PlaylistComponent", getLocalBounds(),
                juce::Justification::centred, true);   // draw some placeholder text
    DBG("paint");
}

void PlaylistComponent::resized()
{
    tableComponent.setBounds(0,0,getWidth(), getHeight());
    DBG("resized");
}

int PlaylistComponent::getNumRows()
{
    return trackTitles.size();
    DBG("getNumRows");
}

void PlaylistComponent::paintRowBackground(juce::Graphics& g, 
                                           int rowNumber, 
                                           int width, 
                                           int height, 
                                           bool rowIsSelected)
{
    if (rowIsSelected)
    {
        g.fillAll(juce::Colours::orange);
    }
    else {
        g.fillAll(juce::Colours::darkgrey);
    }
    DBG("PaintRowBackground");
}

void PlaylistComponent::paintCell(juce::Graphics& g, 
                                  int rowNumber, 
                                  int columnID, 
                                  int width, 
                                  int height, 
                                  bool rowIsSelected)
{
    // Debug output to log the content of each cell
    DBG("Painting cell: Row = " << rowNumber << ", ColumnID = " << columnID);

    // Debug output to log the track title being rendered
    DBG("Track title: " << trackTitles[rowNumber]);
    
    //Populate rows with track titles
    g.drawText(trackTitles[rowNumber], 2, 0, width - 4, height, juce::Justification::centredLeft, true);
    DBG("paintCell");
}

juce::Component* PlaylistComponent::refreshComponentForCell(int rowNumber, 
                                                            int columnID, 
                                                            bool isRowSelected, 
                                                            Component* existingComponentToUpdate)
{
    //Add play button
    if (columnID == 2)
    {
        if (existingComponentToUpdate == nullptr)
        {
            juce::TextButton* btn = new juce::TextButton("play");
            juce::String id{std::to_string(rowNumber)};
            btn->setComponentID(id);
            btn->addListener(this);
            existingComponentToUpdate = btn;
        }
    }
    //Add deck selector
    else if (columnID == 3)
    {
        if (existingComponentToUpdate == nullptr) 
        {
            juce::ComboBox* deck = new juce::ComboBox;
            deck->addItem("Deck 1", 1);
            deck->addItem("Deck 2", 2);
            juce::String id{ std::to_string(rowNumber) };
            deck->setComponentID(id);
            deck->addListener(this);
            existingComponentToUpdate = deck;
        }

    }
    DBG("refreshComponentForCell");
    return existingComponentToUpdate;
}

void PlaylistComponent::buttonClicked(juce::Button* button)
{
    //get row ID
    int id = std::stoi(button->getComponentID().toStdString());

    //check which deck has been selected
    if (id == deckOne)
    {
        DBG(trackTitles[id] << " playing on Deck 1");
        //loadFromPlaylist(audioFile, deckOne);
    }
    else if (id == deckTwo)
    {
        DBG(trackTitles[id] << " playing on Deck 2");
        //MainComponent.loadFromPlaylist(audioFile, deckTwo);
    }
    else {
        //pop up box?
        DBG("PlaylistComponent::buttonClicked, Deck selection not recognised");
    }
}

void PlaylistComponent::comboBoxChanged(juce::ComboBox* deckMenu)
{
    //assign row ID to a deck
    if (deckMenu->getSelectedId() == 1)
    {
        deckOne = std::stoi(deckMenu->getComponentID().toStdString());
    }
    else if (deckMenu->getSelectedId() == 2)
    {
        deckTwo = std::stoi(deckMenu->getComponentID().toStdString());
    }
    else
    {
        DBG("PlaylistComponent::comboBoxChanged, Deck selection not recognised");
    }
}

void PlaylistComponent::loadURL()
{
    auto fileChooserFlags = juce::FileBrowserComponent::canSelectFiles;

    fChooser.launchAsync(fileChooserFlags, [this](const juce::FileChooser& chooser)
    {
        juce::File chosenFile = chooser.getResult();
        addToPlaylist(juce::URL{ chosenFile });
    });
}

void PlaylistComponent::addToPlaylist(juce::URL audioFile)
{

    // Check if the audioFile already exists in trackList
    auto it = std::find(trackList.begin(), trackList.end(), audioFile);
    if (it != trackList.end()) {
        // File already exists in the playlist
        DBG(audioFile.getFileName().toStdString() << " already exists in the playlist");
        return;
    }
    // Add the file to the playlist
    trackList.push_back(audioFile);
    DBG(audioFile.getFileName().toStdString() << " added to playlist");

    bool trackExists = false;
    for (const auto& trackTitle : trackTitles) {
        if (trackTitle == audioFile.getFileName().toStdString()) {
            // Track exists in trackTitles
            trackExists = true;
            break;
        }
    }

    if (!trackExists) {
        // Track does not exist in trackTitles
        DBG("Track does not exist in trackTitles.");
        trackTitles.push_back(audioFile.getFileName().toStdString());
        
        //Reset the TableListBox model
        auto* model = tableComponent.getModel();
        tableComponent.setModel(nullptr);
        tableComponent.setModel(model);
    }
    else {
        // Track exists in trackTitles
        DBG("Track does exist in trackTitles.");
    }

    for (std::string title : trackTitles)
    {
        DBG(title);
    }

    int numRows = tableComponent.getNumRows();
    DBG("Number of rows: " << numRows);

    tableComponent.updateContent();
    tableComponent.repaint();
    
}

In my function addToPlaylist I get the user’s file and after some checks I add this file’s name to the trackTitles vector. After this I call the updateContent and repaint to try and get the new trackTitle element to be displayed on a new row - but nothing happens. I’ve got some DBGs printing to my immediate window which resullt in the following messages being printed:

PaintRowBackground
Painting cell: Row = 0, ColumnID = 1
Track title: Wheels on the bus
paintCell
////////bunch of file warnings and assertions which don’t seem to have any affect on the function of the programme?///////
newTrack.mp3 added to playlist
Track does not exist in trackTitles.
refreshComponentForCell
refreshComponentForCell
refreshComponentForCell
refreshComponentForCell
refreshComponentForCell
refreshComponentForCell
Wheels on the bus
newTrack.mp3
Number of rows: 2

After “paintCell” is printed I press my load button, get a few warnings and then my messages start printing indicating that I’m successfully reading the file and pushing it onto my vector. But the new row doesn’t appear so I added some DBGs which show that the TableListBox does have a second row, but paintCell and paintRowBackground are not being called. I checked to see if resize would cause the second row to be painted but it wasn’t, and I also checked to see if rearranging the columns would cause the second row to be painted and it didn’t. (I saw some previous forum posts which successfully got their new rows to paint when they did this). Both times only the original “Wheels on the Bus” row was painted for me.

At this point I’m a bit stumped, I’m not sure what else to try to get the new row to paint. Any help/suggestions are appreciated.

What happens if there is an existingComponentToUpdate do you actually update it? A quick ski suggests this is a problem.

That whole refreshComponentForCell confused me when I was new :slight_smile:

Maybe you need:

 if (existingComponentToUpdate == nullptr)
        {
            juce::TextButton* btn = new juce::TextButton("play");
            juce::String id{std::to_string(rowNumber)};
            btn->setComponentID(id);
            btn->addListener(this);
            existingComponentToUpdate = btn;
        }
else
{
.... something here?!?

}

Rest of your code doesn’t look too terrible :slight_smile:

Using an enum/constexpr static int for the column names would maybe make it clearer. You could use the onClicked() method with a lambda to avoid having to use the buttonClicked callback.

You’ll probably find it easier to make it look good if you make some kind of wrapper CellComponent that holds the juce text editor/button or whatever so you can position it with some padding inside the cell. Maybe you don’t care for this project though!

int id = std::stoi(button->getComponentID().toStdString());

Not sure I like this too much. If you put a lambda onClicked method when you create the button you won’t need to butcher the component ID to include the row number though!

Also if you do keep this you can use JUCE::String::getIntValue() which saves the cast to std::String.

Hi Jim,

I added more detailed DBG messages to refreshComponentForCell as follows:

//Add play button
if (columnID == 2)
{
    if (existingComponentToUpdate == nullptr)
    {
        juce::TextButton* btn = new juce::TextButton("play");
        juce::String id{std::to_string(rowNumber)};
        btn->setComponentID(id);
        btn->addListener(this);
        existingComponentToUpdate = btn;
        DBG("CREATING TEXTBUTTON");
    }
    else {
        DBG("existingComponentToUpdate is not null TEXTBUTTON");
    }
}
//Add deck selector
else if (columnID == 3)
{
    if (existingComponentToUpdate == nullptr) 
    {
        juce::ComboBox* deck = new juce::ComboBox;
        deck->addItem("Deck 1", 1);
        deck->addItem("Deck 2", 2);
        juce::String id{ std::to_string(rowNumber) };
        deck->setComponentID(id);
        deck->addListener(this);
        existingComponentToUpdate = deck;
        DBG("CREATING COMBOBOX");
    }
    else {
        DBG("existingComponentToUpdate is not null COMBOBOX");
    }
}
else {
    DBG("Column ID not accepted" << std::to_string(columnID));
}

return existingComponentToUpdate;

aon_inspired.mp3 added to playlist
Track does not exist in trackTitles.
Wheels on the bus
aon_inspired.mp3
Number of rows: 2
Column ID not accepted1
existingComponentToUpdate is not null TEXTBUTTON
existingComponentToUpdate is not null COMBOBOX
Column ID not accepted1
CREATING TEXTBUTTON
CREATING COMBOBOX

From what I can see, refreshComponentForCell is not being sent the new row 2?

I tried manually calling refreshComponentForCell:

if (!trackExists) {
    // Track does not exist in trackTitles
    DBG("Track does not exist in trackTitles.");
    trackTitles.push_back(audioFile.getFileName().toStdString());
    
    refreshComponentForCell(2, 1,false,nullptr);
    refreshComponentForCell(2, 2, false, nullptr);
    refreshComponentForCell(2, 3, false, nullptr);
}

And my DBG messages looked like this:
aon_inspired.mp3 added to playlist
Track does not exist in trackTitles.
Column ID not accepted1
CREATING TEXTBUTTON
CREATING COMBOBOX
Wheels on the bus
aon_inspired.mp3
Number of rows: 2
Column ID not accepted1
existingComponentToUpdate is not null TEXTBUTTON
existingComponentToUpdate is not null COMBOBOX
Column ID not accepted1
CREATING TEXTBUTTON
CREATING COMBOBOX

So refreshComponentForCell is able to read my new row, but the TableListBox still does not show the new row in the app.

It is important to understand, that refreshComponentForCell is an optimisation to avoid creating zillions of components.

The Table will only create as many components as visible on the screen.
When you scroll and a component goes out of the visible, the TableComponent will recycle this component calling the refreshComponentForCell.

So your bespoke component needs a way to reflect a different row/column of your table model when it is supplied to refreshComponentForCell.

I didn’t read deeply into your code yet, just skimmed over it

1 Like

I don’t understand what you mean (though it is interesting that it recycles components that are out of view, thanks for explaining that). I did a DBG message and refreshComponentForCell was called six times (once for each column of the two rows I have) so it is definitely able to see the difference between the two rows.

That is definitely a bad idea. This method is a callback in your model for the TableComponent to call.

For me the API works best when I wonder, what does it want from me in plain English:

  • hey I want to display contents of the cell at x and y
  • btw. the row is selected
  • and here is a leftover component you can use (or a nullptr, you have to create one)

Which I would implement like this:

struct PlaylistEntry
{
    juce::String name;
    double duration;
}
std::vector<PlaylistEntry> tracks;

void refreshComponentForCell (int row, int column, bool selected, juce::Component* existingComponentToUpdate) override
{
    // try to convert the existing component to our type:
    auto* myComponent = dynamic_cast<MyComponent*>(existingComponentToUpdate);

    if (existingComponentToUpdate && myComponent == nullptr)
    {
        // I cannot use that component, let's get rid of it
        delete existingComponentToUpdate;
    }

    if (!myComponent)
        myComponent = new (MyComponent());

    // adapt the component to the data
    const auto& entry = tracks [row];

    if (column == 0)
        myComponent->setText (entry.name);
    else if (column == 1)
        myComponent->setText (entry.duration);

    return myComponent;        
}

TBH I have used this mostly with ListBox. It seems not so well suited for a Table, since it might make sense to have different component types per column. In this case you can just ignore the optimisation, delete any incoming component and create the type that works best for that column (e.g. if the decks is a comboBox, the Title a Label and a play column a Button.

But your MyComponent could also switch between the different column modes… there are different ways to do it

Haha I realised it was a bad idea as soon as I strarted hitting assertions for leaks! It did help me establish that refreshComponentForCell was being called successfully but I don’t think I’ll be trying it again.

You did inspire me to change my implementation a little bit so I’m now using a Lable to display my text rather than just relying on the paintCell function.

What I don’t really understand is how your suggested changes to my code within refreshComponentForCell will help my new row in the table be displayed?

When this happens you aren’t, I don’t think, updating your component with the new data.

I’ve done some more detailed DBG messages:

NewTrack.mp3 added to playlist
Track does not exist in trackTitles.
Wheels on the bus
NewTrack.mp3
Number of rows: 2
row number: 0
existingComponentToUpdate is not null TEXTBUTTON
row number: 0
existingComponentToUpdate is not null PLAYBUTTON
row number: 0
existingComponentToUpdate is not null COMBOBOX
row number: 1
CREATING TEXTBUTTON
row number: 1
CREATING PLAYBUTTON
row number: 1
CREATING COMBOBOX

The conditional doesn’t accept because there is no nullptr as the component for that row has already been created, as you can see the components are being created for the new track after that

Basic refreshComponentForCell code ought to look like

{
 if(existingCompToUpdate != nullptr)
{
 existingCompToUpdate->setNewData(data[rowNumber]);
}
else
{
 existingCompToUpdate = new Comp(data[rowNumber]);
}
 return existingCompToUpdate;
};

If you want to simplify it you can always do it more like this (at a tiny performance loss) which is a lot easier to think about (where you just always make a new component).

{
 delete existingCompToUpdate;
 return new Comp(data[rowNumber]);
};

Yeah that doesn’t seem to have any affect, I’m not convinced that the issue lies in refreshComponentForCell, but thanks :smile:

1 Like

Ah well, in case anyone is reading this I am almost at a solution, I created a new button separate from my TableListBoxModel and got that button to call getNumRows() at which point I learned getNumRows() was never recognising the new elements pushed on to my vector.

By declaring my vector as static I can get the rows to appear on resizing, obviously its not ideal only getting the new rows to appear on a resize so I’ll continue working on it but at least on mystery has been solved.