TableListBox::refreshComponentForCell issue?


#1

Hello, all.

The gross symptom: TableListBox::refreshComponentForCell is called with inconsistent values - it’s as if Juce just moves around the actual component as it finds convenient. This makes it difficult if you have a reasonably smart component in the cell.

Here’s a code fragment (with random cruft removed for easy reading).

Component* TableController::refreshComponentForCell(int row, int columnId, bool isRowSelected, Component* existing) { int column = columnId - 1; // To account for the 1 we added above. TableLabel* text = dynamic_cast<TableLabel*>(existing); if (!text) { text = new TableLabel(this, column, row); } else { DCHECK_EQ(column, text->col_); // FAILS! DCHECK_EQ(row, text->row_); }

In other words, the first time I see this component, it’s at one row, columnID location - but if I scroll around in the TableListBox, I’ll see that same component again, but with a different row and columnId.

This is rather unintuitive, and it also makes it pretty hard if, for example, you want to attach that component to some other thing in your program.

In my case, these TableLabels also update a GUI object elsewhere in the program, so if I scroll around in the TableListBox and then edit a field, the results are thrown into the wrong place.

I haven’t figured out how to fix this neatly in my code yet - I can’t just create a new TableLabel, because then I drop out of editing mode, preventing the user from editing.

Thoughts?


#2

Heh, so I did come up with a workaround, it’s either brilliant or hideous, possibly some combination of the two (too tricky to explain if you didn’t know my codebase).

I have some idea of why this happens - one of the components is scrolled off the screen, another one comes visible, so Juce shrugs and re-uses that old component for the newly visible slot.

I still think that this is unintuitive behavior, nor am I sure why it’s needed. If you have a large number of rows and only want to display a little, you can delete the old component as it scrolls off and create a new one - it’s a little more work but easier for the programmer to understand. “TableListBox::refreshComponentForCell is always called with consistent parameters”.


#3

I’m sure the docs make this behaviour perfectly clear!


#4

I’m pretty sure there was a good reason for re-using the components like that rather than just creating new ones. Wish I could remember what it was.

It might just be that when I wrote that code (many many years ago, it was one of the early bits of the library!), computers were slower, so removing the overhead of creating new components may actually have made a difference to something that I was trying to optimise.


#5

In fact, it is in the documentation!

(I don’t seem to remember that comment from when I started using that component, which was well over a year ago, but then my memory isn’t perfect, wait, where am I and why am I typing into this box?)


#6

Hello,

I think the main issue with the refreshComponentForCell() mechanism is that it does not enable to implement “smart” caching of components. Take a simple example: let’s say we have ten rows and we decide to delete the first one.
The call to TableListBox::updateContent() will lead the table to refresh all its cells, without any knowledge of what exactly happened to the table. On the side of TableListBox / TableListBoxModel, there is no way of making the fact that the first logical row has been suppressed explicit, and so all components will be reassigned to a new row. What I would like is to keep the same Components for the same “logical” rows, which would actually lead to the most efficient behaviour in terms of computations.

At a first glance, since the internal class TableListBox::RowComp is responsible for deallocating row components, implementing such a caching mechanism based on “logical” rows seems impossible without modifying juce.

Since I doubt Jules wants to change anything related to this aspect of TableListBox, here is a nice trick to workaround the problem, enabling the implementation of user-defined component caching mechanisms.

First, let us define a simple class that simply hosts a Component*, which can be changed at anytime:

  class Cell : public Component
  {
  public:
    Cell() : content(NULL)
      {setInterceptsMouseClicks(false, true);} // this dummy Component should not intercept mouse clicks, so that "content" can receive them

    void setContent(Component* content)
    {
      if (this->content)
        removeChildComponent(this->content);
      this->content = content;
      if (content)
      {
        content->setBounds(0, 0, getWidth(), getHeight());
        addAndMakeVisible(content);
      }
    }

    virtual void resized()
    {
      if (content)
        content->setBounds(0, 0, getWidth(), getHeight());
    }

  private:
    Component* content;
  };

Then, the refreshComponentForCell() ensures that every cell of interest contains an instance of Cell, and it updates the content component according to a user-defined caching mechanism defined in getOrCreateContentComponentForCell():

  virtual Component* refreshComponentForCell(int rowNumber, int columnId, bool isRowSelected, Component* existingComponentToUpdate)
  {
    Cell* cell = dynamic_cast<Cell* >(existingComponentToUpdate);
    if (!cell)
    {
      jassert(!existingComponentToUpdate); // here, we ensure that all custom components are instances of Cell
      cell = new Cell();
    }
    cell->setContent(getOrCreateContentComponentForCell(rowNumber, columnId));
    return cell;
  }

Now, it is left to the user to implement caching in the way he wants. In my case for example, the class representing the logical row stores a vector of components and is responsible for deleting these components.

I hope this can help :smiley:


#7

Very good solution.