ListBox rowNum miracle


#1

Having searched and tried for hours now, this is my last resort.

Having a custom component class which has to go into a ListBox row.

Problem is that the rowNumber seems to wrap around in ListBoxModel’s call refreshComponentForRow (…)
(which gets called from private ListViewport) causing “alias” objects in the list view.

I found that this circumstance is dependent on the rowHeight itself.
When I decrease its size , wrap occurs proportional.

here is some code snip:

class ModulList : public ListBox,
public ListBoxModel

void ModulList::addItem()
{
++numRows;
updateContent();
}

ModulStrip * ModulList::addStrip()
{
ModulStrip *m = makeModulStrip();
modulArray.add(m); // seems to be redundant, however
m->setID(numRows);
return m;
}

void ModulList::paintListBoxItem( int rowNumber, Graphics& g, int width, int height, bool rowIsSelected )
{
return; // don’t needed - but pure virtual, so …
}

Component* ModulList::refreshComponentForRow (int rowNumber, bool isRowSelected, Component* existingComponentToUpdate)
{
if( rowNumber < getNumRows()){ // <-- this line seems necessary otherwise on creation it will get filled automagically up to "wrap number"
if (existingComponentToUpdate == 0)
existingComponentToUpdate = addStrip();
}
return existingComponentToUpdate;
}

Thanks


#2

Are you running into problems with your custom components and row numbers not matching up?

That’s the design: a few custom components are swapped in and out to fill a row. You have to work around that, you can’t make a fixed component to go in a specific row.

Bruce


#3

well thanks Bruce. I’m not sure I can understand (I’m a subWeenie, however)

For me the problem stems from the relation of numRow with rowHeight, which is reasonable, however. But - what is strange IMHO :

I have to set :: resized() accordingly , and along with it the ListViewport gets resized also - which isn’t quite logical for me. A scrolling View should have 2 vars;
1st for the display border and 2nd for its virtual real estate ( this is how in the ol Mac days mem had to be reserved ).
Exactly here is my confusion: ListBox::resized() acts on virtual real estate of its viewport AND derives rowNum from this.

Hm, work around this? I think I’ll redesign this to get rid of ListBox at all.

[sidetrack]

this monday is the second bloody monday in a months row (pun intended !) and heavy ( storm approaching - )

and speaking in financial terms: too many munchkins in pivotal positions had had driven us all so far.

to make a link: OS development seems to sport similar dwarfs - to undertake a task like JUCE indeed calls for a “fearless leader”

mucho cudos to you: Julian !!!

thanks

If there ain’t no help in this: I’ll redesign !


#4

Well, that’s not true.

The docs contain this important line for refreshComponentForRow:

The ListBox doesn’t map a fixed component to each row number, but it keeps a pool of components to be reused. An example: Your ListBoxModel reports 100 rows and 10 of them can be visible at the same time in the ListBoxs viewport. Now the ListBox will ask your model to return components for the 10 visible rows. This components are stored by the ListBox. If you scroll the list and a new item gets visible in the viewport the ListBox will again ask the model to return a component for this row and may provide you an exiting component you can update. This existing component may be one that was previously scrolled out of the viewport.

So the ListBox keeps only so many components around as there can be rows visible at the same time and reuses this components again and again while you’re scrolling the ListBox.

You can achieve a fixed mapping between rows and components but not with your current code. It seems to me that you assume refreshComponentForRow is only called once per row until the ListBox “knows” the component for each row. That’s not the case as I tried to explain above, but designing your ListBoxModel like this will help:

[code]void ModulList::addItem()
{
ModulStrip *m = makeModulStrip();
m->setID(modulArray.size());

modulArray.add(m);

updateContent();

}

int ModulList::getNumRows()
{
return modulArray.size();
}

Component* ModulList::refreshComponentForRow (int rowNumber, bool isRowSelected, Component* existingComponentToUpdate)
{
return modulArray[rowNumber];
}[/code]

When you add a new item to your ListBoxModel via calling your custom addItem method a new component is created and added to the modulArray. This array is not redundant (as you assumed) but it’s the key for a fixed mapping. In getNumRows you just return the size of the modulArray and in refreshComponentForRow just return the component from the array. This is safe because the ListBox will only ask for row numbers in range from 0 to getNumRows - 1 and that’re all valid indices into your modulArray.


#5

thnx photon . it works . but only as long scrolling direction is down . the moment one tries to scroll back up , you get a first chance exception w/ a bogus pointer being passed in addAndMakeVisible called from ListBoxRowComponent.
However.

Try inserting a slider in juce demo like this:[code]
// This is overloaded from TableListBoxModel, and must update any custom components that we’re using
Component* refreshComponentForCell (int rowNumber, int columnId, bool isRowSelected,
Component* existingComponentToUpdate)
{
if (columnId == 5) // If it’s the ratings column, we’ll return our custom component…
{
RatingColumnCustomComponent* ratingsBox = (RatingColumnCustomComponent*) existingComponentToUpdate;

        // If an existing component is being passed-in for updating, we'll re-use it, but
        // if not, we'll have to create one.
        if (ratingsBox == 0)
            ratingsBox = new RatingColumnCustomComponent (*this);

        ratingsBox->setRowAndColumn (rowNumber, columnId);

        return ratingsBox;
	}else if (columnId == 6) // just for the fun of it ...used same wrapper code for a slider here ...
	{
		SliderCustomComponent* slider = (SliderCustomComponent*) existingComponentToUpdate;

		if (slider == 0)
			slider = new SliderCustomComponent (*this);

		return slider;
	}
    else
    {
        // for any other column, just return 0, as we'll be painting these columns directly.

        jassert (existingComponentToUpdate == 0);
        return 0;
    }
}

[/code]

Now compile and run TableDemo. Change slider value in first row and then watch value of slider in row 25 ! !

Or change slider value in row 26 and watch slider in row 2 … etc.

This is the wrap around I was talking about !
If you set rowHeight to, say 100, wrap around will happen within smaller interval.

Thnx

hans


#6

Well, that’s not true.[/quote]

I take that back, you’re right.

Okay I missed the point that the ListBox will delete components on its own and that’s killing your components in the modulArray. So my suggestion doesn’t work that way, sorry.

[quote=“haskel”]Now compile and run TableDemo. Change slider value in first row and then watch value of slider in row 25 ! !

Or change slider value in row 26 and watch slider in row 2 … etc.

This is the wrap around I was talking about !
If you set rowHeight to, say 100, wrap around will happen within smaller interval.[/quote]

I didn’t test that yet, but do you say that …

a) if you change silder 1 at the same time you can see slider 25 changes also or …

b) if you change silder 1 and than scroll to silder 25 (that was not visble before) its value also changed but you didn’t touch it explicitly?

If b) than that’s the described behaviour of the ListBox. If a) than something is wrong here.

The basic point is that you can’t store the state of the component you want to display in a certain row in that component, because the ListBox reuses and deletes components on its own. If you took a closer look at the code you posted you’ll notice this line:

The state that the rating box display is not stored in the component, but the custom setRowAndColumn method fetches the data for the given row and column from the model. If the user changes the rating the rating box writes the changed data back to the model, see getRating/setRating in the table demo.

So if you do the same (separating the data from the component) for your ModulStrip component and build your refreshComponentForRow like the demo does, it should work.


#7

Sorry if it seems like a laborious way to write your list, but if you’ve got a list with a million rows (or even a thousand), you can’t create fixed components for them all! It’s important to be able to separate your data model from your UI.


#8

well - that took me a long time to understand.
I try to make a verbose version - it might be of help to other newbies…

  1. the ListBox maintains its own object list, which is economical in a way that only visible objects are stored. This is basically the viewPorts height div rowHeight. Thus
  2. It does not make sense to submit my own ObjectPtrs for they will be destroyed by ListBox as needed: out of sight , so delete em.
    But it makes sense to
  3. maintain a corresponding array of data.
    now this routine looks like

[code]
Component* ModulList::refreshComponentForRow (int rowNumber, bool isRowSelected, Component* existingComponentToUpdate)
{
if( rowNumber < getNumRows())
if (existingComponentToUpdate == 0)
existingComponentToUpdate = addStrip();

   if(existingComponentToUpdate != 0) 
          ((ModulStrip *) existingComponentToUpdate)->updateData(modulDataArray[rowNumber]);
	return existingComponentToUpdate;

}[/code]

This seems to work.

Thanks

hans