ListBox updateContent() doesn't repaint if the number of rows hasn't changed

Hi,
I’m using a ListBox in my plugin preset browser to show the list of presets. The presets are organized in two groups: factory and user. When switching between the user and factory group, updateContent() is called and the ListBoxModel pulls the relevant list of presets. This normally triggers a repaint, but if both factory and user lists are the same size, I have to call repaint() manually for it to refresh. Is it a bug or a feature? Now I can compare the sizes and do a manual repaint() for this special case, but this does sound like a workaround…

Here’s a relevant bit of code:

class ListBoxModelBase : public ListBoxModel {
public:
    virtual String getTextForRowNumber(int rowNumber) = 0;

    void paintListBoxItem(int rowNumber,
                          Graphics& g,
                          int width, int height,
                          bool rowIsSelected) final override {
        // Background
        g.setColour(rowIsSelected ? Theme::Colours::accent : Theme::Colours::comboBoxBackground);
        g.fillRect(g.getClipBounds().withTrimmedBottom(1));

        // Text
        g.setFont(Theme::listBoxFontHeight);
        g.setColour(Theme::Colours::normalLight);
        g.drawText(getTextForRowNumber(rowNumber), 5, 0, width, height, Justification::centredLeft, true);
    };

    std::function<void(int lastRowSelected)> onSelectedRowsChanged = nullptr;
};

#pragma mark -

class SourcesListBoxModel : public ListBoxModelBase {
public:
    SourcesListBoxModel(PresetManager& presetManager) : presetManager(presetManager) {}

    int getNumRows() final override {
        return static_cast<decltype(getNumRows())>(presetManager.presetSourceNames.size());
    }

    String getTextForRowNumber(int rowNumber) final override {
        return presetManager.presetSourceNames[rowNumber];
    }

    void selectedRowsChanged(int lastRowSelected) final override {
        if (lastRowSelected < PresetManager::Source::Factory
            || lastRowSelected > PresetManager::Source::User) {
            return;
        }

        if (onSelectedRowsChanged) {
            onSelectedRowsChanged(lastRowSelected);
        }
    }

    PresetManager& presetManager;
};

#pragma mark -

class PresetsListBoxModel : public ListBoxModelBase {
public:
    PresetsListBoxModel(PresetManager& presetManager) : presetManager(presetManager) {}

    int getNumRows() final override {
        currentPresets = presetManager.getCurrentPresets();
        return static_cast<decltype(getNumRows())>(currentPresets.size());
    };

    String getTextForRowNumber(int rowNumber) final override {
        return currentPresets[rowNumber].getName();
    }

    void selectedRowsChanged(int lastRowSelected) final override {
        if (lastRowSelected > 0) {
            presetManager.loadPreset(currentPresets[lastRowSelected]);
        }
    }

    PresetManager& presetManager;
    std::vector<Preset> currentPresets;
};

#pragma mark -

class PresetBrowser : public Component {
public:
    PresetBrowser(PresetManager& presetManager)
        : Component()
        , presetManager(presetManager)
        , sourcesListBoxModel(presetManager)
        , presetsListBoxModel(presetManager)
    {
        sourcesListBoxModel.onSelectedRowsChanged = [&] (int lastRowSelected) {
            presetManager.setCurrentSource(static_cast<PresetManager::Source>(lastRowSelected));
            presetsListBox.updateContent();
        };

        sourcesListBox.setModel(&sourcesListBoxModel);
        sourcesListBox.setRowHeight(Theme::listBoxRowHeight);
        addAndMakeVisible(sourcesListBox);

        presetsListBox.setModel(&presetsListBoxModel);
        presetsListBox.setRowHeight(Theme::listBoxRowHeight);
        addAndMakeVisible(presetsListBox);

        addAndMakeVisible(deleteButton);
        addAndMakeVisible(saveButton);
    }

    void paint(Graphics& g) final override {
        /// ...
    }

    void resized() final override {
        /// ...
    }

private:
    PresetManager& presetManager;

    SourcesListBoxModel sourcesListBoxModel;
    ListBox sourcesListBox;

    PresetsListBoxModel presetsListBoxModel;
    ListBox presetsListBox;

    TextButton deleteButton { "Delete" };
    TextButton saveButton { "Save" };
};
2 Likes

Just trapped into this behavior too.
Feels more like a bug to me…
Anyone from Juce could comment?

Edit: Okay, I found other topics related to this.
I’ll just call both ‘updateContent’ and ‘repaint’ always.
But it feels a bit strange…
As long as number of items are different, only ‘updateContent’ seems to repaint all rows.
And only if they are the same, I also need to call ‘repaint’, which means I would need to keep track of the number of items before and after refreshing the underlying data and only call ‘repaint’ when the number was the same, if I would want to avoid a call to ‘repaint’ otherwise.

I’m just wondering: how bad is a single repaint() call?

Can’t you just call it in any case? Why care if the number of rows is the same? This isn’t going to occur very often, if I understand your situation.

Hallo Stephen!
Thanks for your reply.
As said in my edit:
‘I’ll just call both ‘updateContent’ and ‘repaint’ always.’

But I’m also wondering how bad a single repaint() call would be.
If it wasn’t bad, why is it not just done in ‘updateContent()’ so that this function just would do what one would expect and one would not always have to call '‘updateContent()’ and ‘repaint()’ in pair?

1 Like

I would go even one step further, why isn’t the ListBox an observer of the model by default?

It is totally weird that you need the model where you do changes and then the ListBox and call updateContent() there…