Attempts at improving ListBox/TableListBox and their models


#1

I’ve been doing some work with ListBox, TableListBox, and their respective models lately, and have been disappointed in them compared to many of the other Juce classes. I haven’t fully tested everything yet, but I’m at a good point to throw this all out there for comments from Jules and anyone else who might have some insight. Since I’m not intimately familiar with the Juce code base, I’m expecting to hear that there is something wrong with these changes, but hopefully they’re not all bad.

Change # 1
ListBox has the getComponentForRow() method, so I added the (oddly) lacking getComponentForCell() to TableListBox.

[code]juce_TableListBox.h line 288

/** Finds the cell component for a given row and column in the table.

  The component returned will have been created using refreshComponentForCell().

  If the component for this cell is off-screen or if the cell is out-of-range,
  this will return 0.

  @see ListBox::getComponentForRowNumber

/
Component
getComponentForCell(int rowNumber, int columnId);

juce_TableListBox.h line 288

Component* TableListBox::getComponentForCell(int rowNumber, int columnId){
TableListRowComp rowComp= dynamic_cast<TableListRowComp>(getComponentForRowNumber(rowNumber));
jassert(rowComp);
return rowComp->findChildComponentForColumn(columnId);
}
[/code]

Change # 2
I added a ListBox pointer to ListBoxModel. I know this makes them more tightly coupled than perhaps desired, but it seems that the model almost always needs to deal with row selections. It’s tedious and error prone to add this pointer to each derived model, and I don’t particularly like the multiple inheritance solution I’ve seen elsewhere. I like my suggested solution because I can keep all the data handling code in the one derived model class, and display it in a standard ListBox. (I did the same for TableListBox/TableListBoxModel)

Change # 3
I’ve noticed that when using a ListBoxModel that has custom components, mouse handling becomes very laborious. Ideally, the ListBox and ListBoxModel would respond to clicks as normal, and any special mouse handling could then be passed to the row’s component(s) via the listBoxItemClicked() method. Digging around in the Juce code, I implemented something that seems to work.

–In Component, I added a ComponentFlag allowParentToHandleMouse and appropriate getter/setter.
–In MouseInputSourceInternal, I added the following method and called it in each of the sendMouseXXX methods.

[code]Component* bubbleUpToParent(Component *originator)
{
Component *sendToComp= originator;
while(sendToComp && sendToComp->allowParentToHandleMouse()) sendToComp= sendToComp->getParentComponent();
if(sendToComp == NULL) sendToComp= originator;
return sendToComp;
}

void sendMouseEnter (Component* const comp, const Point<int>& screenPos, const int64 time)
{
Component *actual= bubbleUpToParent(comp);
    //DBG ("Mouse " + JuceString (source.getIndex()) + " enter: " + comp->globalPositionToRelative (screenPos).toString() + " - Comp: " + JuceString::toHexString ((int) comp));
    actual->internalMouseEnter (source, actual->globalPositionToRelative (screenPos), time);
}[/code]--In ComboBox, I overrode setAllowParentToHandleMouse to additionally be called on the ComboBox's label. Also in ComboBox::lookAndFeelChanged(), I called that setter on the new label.

–In the TableDemo code, I added the following, which causes the row to be selected and the combobox to be activated with the same click.

[code]//Added to TableDemoComponent
void cellClicked(int rowNumber, int columnId, const MouseEvent& e){
Component *comp= parentTableListBox->getComponentForCell(rowNumber, columnId);
RatingColumnCustomComponent wrapper= dynamic_cast<RatingColumnCustomComponent>(comp);
if(wrapper){
wrapper->getComboBox()->mouseDown(e);
}
}

// Added to RatingColumnCustomComponent…
ComboBox* getComboBox() {
return comboBox;
}
//…and in its constructor
setAllowParentToHandleClicks(true);
comboBox->setAllowParentToHandleClicks(true);
[/code]


#2

Thanks, always good to get in-depth feedback!

  1. Ok, good request, I’ll add something to do that.

  2. But a model could be used in more than one listbox simultaneously…

  3. Agh! No! Allowing mouse events to get bounced upwards would be an endless source of small, irritating bugs. You’d hit all kinds of situations where e.g. a component gets a mouse down, while the mouseinputsource thinks that a different one currently has the mouse down; or a component gets a mouse down without a mouseenter, or gets a mouseup without a mousedown, or gets a mouse down without a mouseup when focus leaves the app, etc, etc, etc… It’d be a PITA to debug.
    Perhaps just something like a ListBox::handleMouseDownOnRow() type of method that you could call would do the job?


#3

i hahve a request in that subject.

could the model methods getNumRows paintRowbackground and others have some way to find out witch listBox is calling them, one model for a feq list boxes but each list box with different elements.


#4

I can’t really see an argument for adding the listbox to getNumRows, because that method should really be pure enough not to care about the component that it’s in. (If it did need to know, then you should be using more than one model object instead). I guess that adding the listbox to paintRowBackground could be handy, though by changing the parameters, I’d break everyone’s code, and am a bit reluctant to do that.


#5

Well i have two list boxes and two buttons between them -> <- that add remove items from one to another, kind of selection for multiple items, and wanted to use one model since it handles those items. But if i have to i’ll use multiple models


#6

If you have multiple lists that contain different items, they should certainly be done as separate objects, even if they’re the same class.


#7

it’s two lists that display the same group of objects, just that one list displays those “active” and the other those “inactive” it’s the same source of data, but two displays.


#8

I’d probably do that with a model class that has a flag to say whether it’s showing selected or unselected objects, and use two instances, possibly both sharing a common data object if there’s a lot of data.


#9
  1. Great, thanks!

  2. I know that’s the theory, but I couldn’t off the top of my head think of an actual example where I’d want to use the same model instance in more than one ListBox. Could you give me a good example?

  3. I had a feeling there would be problems like that. I’m not sure I understand your handleMouseDownOnRow suggestion though. Where would it be called from?

Here is what I wish could be done with ListBox/ListBoxModel:
[list]-I rarely, if ever, want to derive from the ListBox class.
-The ListBoxModel should handle all data display and data manipulation.
-The components from refreshComponentForRow() should not have to dispatch calls to the ListBox so it can handle things like row selection.
-Components within a ListBox can react to mouse events after the ListBox/ListBoxModel have.[/list]


#10

I don’t think I’ve used it like that myself, but it’s inevitable that people will be using it like that, so it’s a change that could break a lot of code.

[quote]
3. I had a feeling there would be problems like that. I’m not sure I understand your handleMouseDownOnRow suggestion though. Where would it be called from? [/quote]

I just meant that because the basic problem is just something that could be called from your component’s mouseDown, which would provide the basic selection logic.

I’ve sometimes needed row components that specifically don’t select the row when you click it, or which select it in an unusual way. I think it’s better to have an “opt-in” approach for this - if you’re writing a custom component, then it’s because you need to do something very specific, and I don’t think it’s sensible for the parent component to start grabbing its events and using them in possibly an inappropriate way. Instead, your component should be able to implement whatever it needs to do without interference.

And don’t forget that if areas of your row should be transparent to clicks, you can just use a hitTest or setInterceptsMouseClicks to let the clicks go straight through to the default handler.


#11

Adding a pointer couldn’t break any existing code, but I could see it being a source of confusion and bugs for people trying to use it. Obviously, its value would not make sense if the model were used in more than one ListBox. Personally, I like having it so I’ll keep it in my build, but it does make sense to keep it out of the library.

I had seen setInterceptsMouseClicks earlier, but somehow gotten it in my head that it wasn’t what I needed. That’s definitely an easy way to use custom components for display purposes. Editable components are still a bit trickier though. For a ComboBox with setInterceptsMouseClicks(false, false), it would be nice if I could call showPopup from listBoxItemClicked, but it’s private. Is there any reason it can’t be made public to allow this? It seems analogous to Label::showEditor(), which is public.


#12

Well, the idea of manually popping up a combo box isn’t something I’d do myself - you’d lose things like the ability to hold down the mouse and drag to select an item, and I’m sure there’d be all sorts of other subtle problems. But you’re right, the showPopup method should probably be public.

Maybe what you’re after is just something as simple as a base class that provides the basic listbox functionality, so you could just call the superclass’s mouseDown method from your own mouseDown if you wanted to let it handle the selection.