QCustomPlot Discussion and Comments

Updating items on a custom layer requires parentPlot()->replot() for correct displayReturn to overview

I am trying to draw some selectable items on top of a plot. I create a buffered custom layer "items"

customPlot->addLayer( "items" );
customPlot->layer("items")->setMode( QCPLayer::LayerMode::lmBuffered );

and add move my items to this layer in the constructor.

class RectItem : public QCPItemRect {
public:
  explicit RectItem( QCustomPlot *parentPlot )
  : QCPItemRect( parentPlot ) {
    auto *gating = parentPlot->layer( "gating" );
    moveToLayer( gating, true );
  }
  ...
};

For the moment they are all rectangles. I need to change the items selection state and position using mouse events. For this to work I need to change the 'level' of the items in the layers list of items. So my items have toFront and toBack methods.

 void toBack() {
    auto *items = parentPlot()->layer( "items" );
    moveToLayer( nullptr, true );
    moveToLayer( items, false );
  }

  void toFront() {
    auto *items = parentPlot()->layer( "items" );
    moveToLayer( nullptr, true );
    moveToLayer( items, true ); 
  }

These are kind of brute-force, but I couldn't find any other methods to manipulate the order. When there is only on item and my code looks like this

class RectItem : public QCPItemRect {
   QPoint m_last_pos;
  ...
  void mousePressEvent( QMouseEvent *event, QVariant const &details ) override {
    m_last_pos = event->pos();
    std::cout << "press\n"; 
  }

  void mouseReleaseEvent( QMouseEvent *event, QPointF const &startPos ) override {
    std::cout << "release\n";
  }

  void mouseMoveEvent( QMouseEvent *event, const QPointF &startPos ) override {
    // adjust rectangle via topLeft and bottomRight
    layer()->replot();
    std::cout << "move\n";
  }

Everything works fine. The items layer correctly updates the displays and adjusts the items position during the move.

The problems start when there is more than one item and I need to switch 'selected' items. For example, there are two items under the cursor and clicking the top, selected item should transfer the selection to the unselected item underneath. The newly selected item should move to the front and the originally selected item should move to the back.

class TnRectItem : public QCPItemRect {
  // remember on which item to accept the mouse event
  static inline TnRectItem *m_accept = nullptr;

  void  mousePressEvent( QMouseEvent *event, QVariant const &details ) override {

    m_last_pos = event->pos();
    std::cout << "press\n";

    if( m_accept == nullptr ) {
      // new round on topmost item, do all the adjustments    
      // get a list of all items in this layer that are under the cursor. All items in this layer are RectItem
      auto childs = layer()->children();    
      std::vector<RectItem*> under_pos;    
      for( auto &child : childs ) {
        if( child->selectTest( event->localPos(), true ) >= 0.0 ) {
          under_pos.push_back( reinterpret_cast<RectItem*>( child ) );
        }
      }
      m_accept = under_pos[0];
         
      if( not m_accept->selected() ) {
        m_accept->setSelected( true );
        m_accept->toFront();
    
        for( auto &child : childs )
          if( child != m_accept )
            reinterpret_cast<RectItem*>( child )->setSelected( false );
    
      } else {
        m_accept->setSelected( false );
        m_accept->toBack();
    
        if( under_pos.size() > 1 ) {
          m_accept = under_pos[1];
          m_accept->toFront();
          m_accept->setSelected( true );
        } 
      }
    }
    
    if( this == m_accept ) {
      m_accept = nullptr;
      parentPlot()->replot();
      //layer()->replot(); 
    } else {
      event->ignore();
    }  
  }

This code works with the parentPlot()->replot() but fails to update when layer()->replot() is used. Any ideas on how to get this working without the complete redraw.

I've gained a little more insight. The changing of the item order in the layer seems to cause the trouble. I removed the toFront as it not needed since toBack already brings the right element to the front. The following code works but still requires an full update when toBack is called.

void mousePressEvent( QMouseEvent *event, QVariant const &details ) override {

    m_last_pos = event->pos();
    std::cout << "press\n";

    if( m_accept == nullptr ) {  
      auto childs = layer()->children();
      std::vector<RectItem*> under_pos;
      for( auto &child : childs ) {
        if( child->selectTest( event->localPos(), true ) >= 0.0 ) {
          under_pos.push_back( reinterpret_cast<RectItem*>( child ) );
        }
      }
      m_accept = under_pos[0];
    
      if( not m_accept->selected() ) {
        m_accept->setSelected( true );
    
        for( auto &child : childs )
          if( child != m_accept )
            reinterpret_cast<RectItem*>( child )->setSelected( false );
      } else {
        m_accept->setSelected( false );
    
        if( under_pos.size() > 1 ) {
          m_to_back = under_pos[0];
          m_accept = under_pos[1];
          m_accept->setSelected( true );
        } 
      }
    }
    
    if( this == m_accept ) {
      m_accept = nullptr;
      layer()->replot();
    } else {
      event->ignore();
    }   
  }

  void mouseReleaseEvent( QMouseEvent *event, QPointF const &startPos ) override {

    if( m_to_back ) {
      m_to_back->toBack();
      parentPlot()->replot();
      m_to_back = nullptr;
    } else {
      layer()->replot();
    }
    std::cout << "release\n";
  }

For some reason hasInvalidatedPaintBuffers returns false and updating is skipped in replot

void QCPLayer::replot()
{
  if (mMode == lmBuffered && !mParentPlot->hasInvalidatedPaintBuffers())
  {
    if (!mPaintBuffer.isNull())
    {
      mPaintBuffer.data()->clear(Qt::transparent);
      drawToPaintBuffer();
      mPaintBuffer.data()->setInvalidated(false);
      mParentPlot->update();
    } else
      qDebug() << Q_FUNC_INFO << "no valid paint buffer associated with this layer";
  } else if (mMode == lmLogical)
    mParentPlot->replot();
}

even though addChild is called on the layer, which calls setInvalidated

void QCPLayer::addChild(QCPLayerable *layerable, bool prepend)
{
  if (!mChildren.contains(layerable))
  {
    if (prepend)
      mChildren.prepend(layerable);
    else
      mChildren.append(layerable);
    if (!mPaintBuffer.isNull())
      mPaintBuffer.data()->setInvalidated();
  } else
    qDebug() << Q_FUNC_INFO << "layerable is already child of this layer" << reinterpret_cast<quintptr>(layerable);
}

correction: hasInvalidatedPaintBuffers returns true and updating is skipped.

I can get this case to work if I let hasInvalidatedPaintBuffers skip its own buffer, but I have no idea what sideffects that may have in other situations.

bool hasInvalidatedPaintBuffers( QCPAbstractPaintBuffer *ignore = nullptr);

bool QCustomPlot::hasInvalidatedPaintBuffers( QCPAbstractPaintBuffer* ignore ) 
{
  for (int i=0; i<mPaintBuffers.size(); ++i)
  {
    if( mPaintBuffers.at( i ) != ignore && mPaintBuffers.at( i )->invalidated() )
      return true;
  }
  return false;
}

The call site in the layer then looks like this:

void QCPLayer::replot()
{
  if( mMode == lmBuffered && 
      ! mParentPlot->hasInvalidatedPaintBuffers( mPaintBuffer.data() ) )
  { ...
  } ...
}

The documentation of the function QCPLayer::replot does not seem to match the implementation.

/*! ...
  QCustomPlot also makes sure to replot all layers instead of only this one, if the layer ordering
  has changed since the last full replot and the other paint buffers were thus invalidated.
  ... 
*/

It does not make sure to replot all layers. That only happens if the layer mode is lmLogical. For lmBuffered it either only updates this layer, or it does nothing.

Also the assumption that " the other paint buffers were thus invalidated" is not checked correctly. In this case only the this layer was invalidated and the others were not. Checking that the other buffers are valid is what the modified version does.