QSortFilterProxyModel: crash sometimes when adding rows from the source model

492 views Asked by At

I am writing a Qt application that includes a table view which displays the tasks performed by different persons. There is a column per person, and a row per day. The view is initialized with 26 rows from today minus 10 days to today +15 days. App overview When the vertical scrollbar is at min or max and that the user makes an action to move up/down, 10 new rows (i.e. days) are added on top/bottom. The problem is that sometimes it crashes at that time because of some internal QPersistentModelIndex issues, but only since I added a QSortFilterProxyModel between the source model and the view to display only the days where someone has a task to do.

The function calls propagate as follows:

  • the view (of type DashboardTableView) emits the signal "wantToGoMoreUp" or "wantToGoMoreDown"
  • -> DashboardWidget::slotAskDataUp or slotAskDataDown
  • -> DashboardTableModel::addDaysOnTop or addDaysOnBottom

I tried:

  • with a custom model instead of a QSortFilterProxyModel (by reimplementing "filterAcceptsRow")
  • to add "emit layoutAboutToBeChanged()" and "emit layoutChanged()" around rows additions/removals ... but with no success, it still crashes.

Sometimes the crash occurs after many manipulations (I was not able to understand what is the succession of events that makes it crash). The possible manipulations are:

  • scrolling of course (mouse wheel, mouse click on scroll bar buttons or page up/down keys)
  • jump to a given date (right click + select a date in the calendar pop-up)
  • enable/disable filtering using the toolbar button

I made a full working example on my GitHub account: https://github.com/3noix/ForDebug I am currently using Qt 5.11.0 with MinGW 5.3.0 32 bits.

So do you have any idea of what I am doing wrong? Thanks by advance.

//DashboardWidget.h
#include <QWidget>
#include <QVector>
#include "../Task.h"
class QVBoxLayout;
class DashboardTableView;
class DashboardTableModel;
class DashboardFilterModel;
class QSortFilterProxyModel;
class DashboardTableDelegate;


class DashboardWidget : public QWidget
{
    Q_OBJECT
    
    public:
        explicit DashboardWidget(QWidget *parent = nullptr);
        DashboardWidget(const DashboardWidget &other) = delete;
        DashboardWidget(DashboardWidget &&other) = delete;
        DashboardWidget& operator=(const DashboardWidget &other) = delete;
        DashboardWidget& operator=(DashboardWidget &&other) = delete;
        virtual ~DashboardWidget() = default;
        
        void toggleDaysFiltering();
        void updateTasks(const QVector<Task> &tasks, const QDateTime &refreshDateTime);
        
        
    public slots:
        void slotAskDataUp();
        void slotAskDataDown();
        void slotShowDate(const QDate &date);
        
        
    private slots:
        void slotNewTimeRange(const QDate &from, const QDate &to);
        
        
    private:
        QVBoxLayout *m_layout;
        DashboardTableView *m_view;
        DashboardTableModel *m_model;
        #ifdef CUSTOM_FILTER_MODEL
        DashboardFilterModel *m_filter;
        #else
        QSortFilterProxyModel *m_filter;
        #endif
        DashboardTableDelegate *m_delegate;
};
//DashboardWidget.cpp
#include "DashboardWidget.h"
#include "DashboardTableView.h"
#include "DashboardTableModel.h"
#include "DashboardFilterModel.h"
#include <QSortFilterProxyModel>
#include "DashboardTableDelegate.h"
#include "DashboardGeometry.h"
#include "../DataInterface.h"
#include <QVBoxLayout>


///////////////////////////////////////////////////////////////////////////////
// RESUME :
//
//  CONSTRUCTEUR
//
//  TOGGLE DAYS FILTERING
//  SLOT ASK DATA UP
//  SLOT ASK DATA DOWN
//  SLOT NEW TIME RANGE
//  SLOT SHOW DATE
//  UPDATE TASKS
///////////////////////////////////////////////////////////////////////////////


// CONSTRUCTEUR ///////////////////////////////////////////////////////////////
DashboardWidget::DashboardWidget(QWidget *parent) : QWidget{parent}
{
    m_layout = new QVBoxLayout{this};
    this->setLayout(m_layout);
    
    m_view = new DashboardTableView{this};
    m_model = new DashboardTableModel{this};
    m_delegate = new DashboardTableDelegate{this};
    
    #ifdef CUSTOM_FILTER_MODEL
    m_filter = new DashboardFilterModel{this};
    #else
    m_filter = new QSortFilterProxyModel{this};
    m_filter->setFilterKeyColumn(0);
    m_filter->setFilterRole(Qt::UserRole+1);
    m_filter->setFilterRegExp(QRegExp{});
    #endif
    
    m_filter->setSourceModel(m_model);
    m_view->setModel(m_filter);
    m_view->setItemDelegate(m_delegate);
    m_view->setupView();
    
    m_layout->addWidget(m_view);
    m_layout->setContentsMargins(0,0,0,0);
    
    QObject::connect(m_view,  SIGNAL(wantToGoMoreUp()),          this, SLOT(slotAskDataUp()));
    QObject::connect(m_view,  SIGNAL(wantToGoMoreDown()),        this, SLOT(slotAskDataDown()));
    QObject::connect(m_view,  SIGNAL(dateClicked(QDate)),        this, SLOT(slotShowDate(QDate)));
    QObject::connect(m_model, SIGNAL(newTimeRange(QDate,QDate)), this, SLOT(slotNewTimeRange(QDate,QDate)));
}







// TOGGLE DAYS FILTERING //////////////////////////////////////////////////////
void DashboardWidget::toggleDaysFiltering()
{
    #ifdef CUSTOM_FILTER_MODEL
    bool b = m_filter->filtersDays();
    m_filter->setFilterOnDays(!b);
    #else
    if (m_filter->filterRegExp().isEmpty()) {m_filter->setFilterRegExp("Keep");}
    else {m_filter->setFilterRegExp(QRegExp{});}
    #endif
}

// SLOT ASK DATA UP ///////////////////////////////////////////////////////////
void DashboardWidget::slotAskDataUp()
{
    int nbDays = 10;
    QDate from = m_model->from();
    
    m_model->addDaysOnTop(nbDays);
    //if (m_model->daysVisible() > 100) {m_model->removeDaysOnBottom(nbDays);}
    this->slotNewTimeRange(from.addDays(-nbDays),from);
}

// SLOT ASK DATA DOWN /////////////////////////////////////////////////////////
void DashboardWidget::slotAskDataDown()
{
    int nbDays = 10;
    QDate to = m_model->to();
    
    m_model->addDaysOnBottom(nbDays);
    //if (m_model->daysVisible() > 100) {m_model->removeDaysOnTop(nbDays);}
    this->slotNewTimeRange(to,to.addDays(nbDays));
}

// SLOT SHOW DATE /////////////////////////////////////////////////////////////
void DashboardWidget::slotShowDate(const QDate &date)
{
    int row = m_model->showDate(date);
    QModelIndex indexSource = m_model->index(row,0);
    QModelIndex indexView = m_filter->mapFromSource(indexSource);
    m_view->scrollTo(indexView,QAbstractItemView::PositionAtTop);
}

// SLOT NEW TIME RANGE ////////////////////////////////////////////////////////
void DashboardWidget::slotNewTimeRange(const QDate &from, const QDate &to)
{
    // list the tasks in this interval
    QString errorMessage;
    QVector<Task> tasks = DataInterface::instance().getTasks(from,to,&errorMessage);
    if (errorMessage != "") {return;}
    
    // update the table
    this->updateTasks(tasks,QDateTime{});
}

// UPDATE TASKS ///////////////////////////////////////////////////////////////
void DashboardWidget::updateTasks(const QVector<Task> &tasks, const QDateTime &refreshDateTime)
{
    m_model->updateTasks(tasks);
    if (refreshDateTime.isValid()) {m_view->setUpdateTime(refreshDateTime);}
}
//DashboardTableModel.h
#include <QAbstractTableModel>
#include "../User.h"
#include "../Task.h"
#include <QDate>


class DashboardTableModel : public QAbstractTableModel
{
    Q_OBJECT
    
    public:
        explicit DashboardTableModel(QObject *parent = nullptr);
        DashboardTableModel(const DashboardTableModel &other) = delete;
        DashboardTableModel(DashboardTableModel &&other) = delete;
        DashboardTableModel& operator=(const DashboardTableModel &other) = delete;
        DashboardTableModel& operator=(DashboardTableModel &&other) = delete;
        virtual ~DashboardTableModel() = default;
        
        QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override final;
        Qt::ItemFlags flags(const QModelIndex &index) const override final;
        QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override final;
        int columnCount(const QModelIndex &parent = QModelIndex{}) const override final;
        int rowCount(const QModelIndex &parent = QModelIndex{}) const override final;
        
        void updateTasks(const QVector<Task> &tasks);
        
        int dateToRow(const QDate &date);
        QDate from() const;
        QDate to() const;
        int daysVisible() const;
        
        int showDate(const QDate &date);
        void addDaysOnTop(int nbDays = 20);
        void addDaysOnBottom(int nbDays = 20);
        bool removeDaysOnTop(int nbDays = 20);
        bool removeDaysOnBottom(int nbDays = 20);
        void clear();


    signals:
        void newTimeRange(const QDate &from, const QDate &to);
        
        
    private:
        void initLines(const QDate &from, const QDate &to);
        using Location = std::pair<QDate,int>;
        
        struct DataLine
        {
            QDate date;
            QVector<TaskGroup> data;
        };

        static int searchTask(const TaskGroup &g, int taskId);
        int userIdToColumn(int userId) const;
        
        QVector<User> m_users;
        QList<DataLine> m_data;
        QMap<int,Location> m_index; // key=taskId
};
//DashboardTableModel.cpp
#include "DashboardTableModel.h"
#include <QBrush>

const QStringList daysOfWeek{"Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"};
const QColor greenToday{200,255,200};
const QColor grayWeekEnd{240,240,240};

const int initNbDaysBefore = -10;
const int initNbDaysAfter = 15;
const bool emitLayoutChange = false;


///////////////////////////////////////////////////////////////////////////////
// RESUME :
//
//  CONSTRUCTEUR
//  INIT LINES
//
//  FROM
//  TO
//  DAYS VISIBLE
//
//  DATE TO ROW
//  SEARCH TASK
//
//  SHOW DATE
//  ADD DAYS ON TOP
//  ADD DAYS ON BOTTOM
//  REMOVE DAYS ON TOP
//  REMOVE DAYS ON BOTTOM
//  CLEAR
//
//  DATA
//  FLAGS
//  HEADER DATA
//  COLUMN COUNT
//  ROW COUNT
//
//  UPDATE TASKS
///////////////////////////////////////////////////////////////////////////////


// CONSTRUCTEUR ///////////////////////////////////////////////////////////////
DashboardTableModel::DashboardTableModel(QObject *parent) : QAbstractTableModel{parent}
{
    m_users << User{0,"Serge","Karamazov","SKZ"};
    m_users << User{1,"Patrick","Biales","PBL"};
    m_users << User{2,"Odile","Deray","ODR"};
    m_users << User{3,"Mevatlaver","Kraspek","MLK"};
    
    QDate from = QDate::currentDate().addDays(initNbDaysBefore);
    QDate to = QDate::currentDate().addDays(initNbDaysAfter);
    this->initLines(from,to);
}

// INIT LINES /////////////////////////////////////////////////////////////////
void DashboardTableModel::initLines(const QDate &from, const QDate &to)
{
    this->clear();
    int nbCols = this->columnCount();
    int nbRowsInit = from.daysTo(to) + 1;

    QDate date = from;
    if (emitLayoutChange) emit layoutAboutToBeChanged();
    this->beginInsertRows(QModelIndex{},0,nbRowsInit-1);
    for (int row=0; row<=nbRowsInit; ++row)
    {
        date = date.addDays(1);
        m_data << DataLine{date,QVector<TaskGroup>(nbCols)};
    }
    this->endInsertRows();
    if (emitLayoutChange) emit layoutChanged();
}

// FROM ///////////////////////////////////////////////////////////////////////
QDate DashboardTableModel::from() const
{
    if (m_data.size() == 0) {return {};}
    return m_data[0].date;
}

// TO /////////////////////////////////////////////////////////////////////////
QDate DashboardTableModel::to() const
{
    if (m_data.size() == 0) {return {};}
    return m_data.last().date;
}

// DAYS VISIBLE ///////////////////////////////////////////////////////////////
int DashboardTableModel::daysVisible() const
{
    if (m_data.size() == 0) {return 0;}
    QDate from = m_data[0].date;
    QDate to = m_data.last().date;
    return from.daysTo(to)+1;
}






// DATE TO ROW ////////////////////////////////////////////////////////////////
int DashboardTableModel::dateToRow(const QDate &date)
{
    if (m_data.size() == 0) {return -1;}
    QDate date1 = m_data[0].date;
    QDate date2 = m_data.last().date;
    if (date < date1 || date > date2) {return -1;}
    
    return date1.daysTo(date);
}

// USER ID TO COLUMN //////////////////////////////////////////////////////////
int DashboardTableModel::userIdToColumn(int userId) const
{
    return userId;
}

// SEARCH TASK ////////////////////////////////////////////////////////////////
int DashboardTableModel::searchTask(const TaskGroup &g, int taskId)
{
    for (int i=0; i<g.size(); ++i)
    {
        if (g[i].id == taskId)
            return i;
    }
    
    return -1;
}






// SHOW DATE //////////////////////////////////////////////////////////////////
int  DashboardTableModel::showDate(const QDate &date)
{
    if (m_data.size() == 0) {return -1;}

    QDate from = this->from();
    QDate to = this->to();
    if (from <= date && date <= to) {return this->dateToRow(date);}

    int fromToDate = from.daysTo(date);
    int toToDate = to.daysTo(date);

    if (fromToDate >= -15 && fromToDate < 0)
    {
        this->addDaysOnTop(15);
        emit newTimeRange(date,from);
        return this->dateToRow(date);
    }
    else if (toToDate > 0 && toToDate <= 15)
    {
        this->addDaysOnBottom(15);
        emit newTimeRange(to,date);
        return this->dateToRow(date);
    }

    // we discard the existing data and restart from nothing
    this->clear();
    QDate newFrom = date.addDays(initNbDaysBefore);
    QDate newTo = date.addDays(initNbDaysAfter);
    this->initLines(newFrom,newTo);
    emit newTimeRange(newFrom,newTo);
    return this->dateToRow(date);
}

// ADD DAYS ON TOP ////////////////////////////////////////////////////////////
void DashboardTableModel::addDaysOnTop(int nbDays)
{
    if (emitLayoutChange) emit layoutAboutToBeChanged();
    this->beginInsertRows(QModelIndex{},0,nbDays-1);
    
    QDate date = this->from();
    int nbCol = this->columnCount();
    for (int row=0; row<nbDays; ++row)
    {
        date = date.addDays(-1);
        m_data.push_front(DataLine{date,QVector<TaskGroup>(nbCol)});
    }
    
    this->endInsertRows();
    if (emitLayoutChange) emit layoutChanged();
}

// ADD DAYS ON BOTTOM /////////////////////////////////////////////////////////
void DashboardTableModel::addDaysOnBottom(int nbDays)
{
    int nOld = m_data.size();
    
    if (emitLayoutChange) emit layoutAboutToBeChanged();
    this->beginInsertRows(QModelIndex{},nOld,nOld+nbDays-1);
    
    QDate date = this->to();
    int nbCol = this->columnCount();
    for (int row=0; row<nbDays; ++row)
    {
        date = date.addDays(1);
        m_data << DataLine{date,QVector<TaskGroup>(nbCol)};
    }
    
    this->endInsertRows();
    if (emitLayoutChange) emit layoutChanged();
}

// REMOVE DAYS ON TOP /////////////////////////////////////////////////////////
bool DashboardTableModel::removeDaysOnTop(int nbDays)
{
    if (m_data.size() < nbDays) {return false;}
    
    // remove tasks ids from index
    for (int row=0; row<nbDays; ++row)
    {
        for (const TaskGroup &g : m_data[row].data)
        {
            for (const Task &t : g)
            {m_index.remove(t.id);}
        }
    }
    
    // removal
    if (emitLayoutChange) emit layoutAboutToBeChanged();
    this->beginRemoveRows(QModelIndex{},0,nbDays-1);
    for (int i=0; i<nbDays; ++i) {m_data.removeFirst();}
    this->endRemoveRows();
    if (emitLayoutChange) emit layoutChanged();

    return true;
}

// REMOVE DAYS ON BOTTOM //////////////////////////////////////////////////////
bool DashboardTableModel::removeDaysOnBottom(int nbDays)
{
    if (m_data.size() < nbDays) {return false;}
    int start = m_data.size() - nbDays;
    int end = m_data.size() - 1;
    
    // remove tasks ids from index
    for (int row=start; row<end; ++row)
    {
        for (const TaskGroup &g : m_data[row].data)
        {
            for (const Task &t : g)
            {m_index.remove(t.id);}
        }
    }
    
    // removal
    if (emitLayoutChange) emit layoutAboutToBeChanged();
    this->beginRemoveRows(QModelIndex{},start,end);
    for (int i=0; i<nbDays; ++i) {m_data.removeLast();}
    this->endRemoveRows();
    if (emitLayoutChange) emit layoutChanged();
    return true;
}

// CLEAR //////////////////////////////////////////////////////////////////////
void DashboardTableModel::clear()
{
    if (m_data.size() == 0) {return;}

    // this->beginResetModel();
    if (emitLayoutChange) emit layoutAboutToBeChanged();
    this->beginRemoveRows(QModelIndex{},0,m_data.size()-1);
    m_data.clear();
    m_index.clear();
    this->endRemoveRows();
    if (emitLayoutChange) emit layoutChanged();
    // this->endResetModel();
}






// DATA ///////////////////////////////////////////////////////////////////////
QVariant DashboardTableModel::data(const QModelIndex &index, int role) const
{
    int row = index.row();
    int col = index.column();
    if (!index.isValid() || row < 0 || row >= this->rowCount(QModelIndex{})) {return {};}
    
    if (role == Qt::DisplayRole || role == Qt::EditRole)
    {
        const TaskGroup &g = m_data[row].data[col];
        return QVariant::fromValue(g);
    }
    else if (role == Qt::BackgroundRole)
    {
        QDate date = m_data[row].date;
        if (!date.isValid()) {return {};}
        
        if (date == QDate::currentDate()) {return QBrush{greenToday};}
        
        int d = date.dayOfWeek();
        if (d == 6 || d == 7) {return QBrush{grayWeekEnd};}
        return {};
    }
    else if (role == Qt::UserRole)
    {
        int nbPostTasks = 0;
        for (const TaskGroup &g : m_data[row].data) {nbPostTasks += g.size();}
        return nbPostTasks;
    }
    else if (role == Qt::UserRole+1)
    {
        int nbPostTasks = 0;
        for (const TaskGroup &g : m_data[row].data) {nbPostTasks += g.size();}
        return (nbPostTasks > 0 ? "Keep" : "Skip");
    }
    
    return {};
}

// FLAGS //////////////////////////////////////////////////////////////////////
Qt::ItemFlags DashboardTableModel::flags(const QModelIndex &index) const
{
    Q_UNUSED(index)
    return (Qt::ItemIsEnabled | Qt::ItemIsSelectable);
}

// HEADER DATA ////////////////////////////////////////////////////////////////
QVariant DashboardTableModel::headerData(int section, Qt::Orientation orientation, int role) const
{
    if (orientation == Qt::Horizontal && role == Qt::DisplayRole)
    {
        if (section < 0 || section >= m_users.size()) {return {};}
        //return m_users[section].trigramme;
        const User &u = m_users[section];
        return u.firstName + " " + u.lastName + "\n" + u.trigramme;
    }
    else if (orientation == Qt::Vertical && role == Qt::DisplayRole)
    {
        if (section >= m_data.size()) {return {};}
        QDate date = m_data[section].date;
        if (!date.isValid()) {return {};}
        
        if (date == QDate::currentDate()) {return "Today";}
        QString str = daysOfWeek[date.dayOfWeek()-1] + "\n" + date.toString("dd/MM/yy");
        if (date.dayOfWeek() == 1) {str += "\nW" + QString::number(date.weekNumber());}
        return str;
    }
    else if (orientation == Qt::Horizontal && role == Qt::TextAlignmentRole)
    {
        return Qt::AlignCenter;
    }
    else if (orientation == Qt::Vertical && role == Qt::TextAlignmentRole)
    {
        return Qt::AlignCenter;
    }
    else if (orientation == Qt::Vertical && role == Qt::BackgroundRole)
    {
        if (section >= m_data.size()) {return {};}
        QDate date = m_data[section].date;
        if (!date.isValid()) {return {};}
        
        if (date == QDate::currentDate()) {return QBrush{greenToday};}
        
        int d = date.dayOfWeek();
        if (d == 6 || d == 7) {return QBrush{grayWeekEnd};}
        return {};
    }
    
    return QAbstractTableModel::headerData(section, orientation, role);
}

// COLUMN COUNT ///////////////////////////////////////////////////////////////
int DashboardTableModel::columnCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {return 0;}
    return m_users.size();
}

// ROW COUNT //////////////////////////////////////////////////////////////////
int DashboardTableModel::rowCount(const QModelIndex &parent) const
{
    if (parent.isValid()) {return 0;}
    return m_data.size();
}




// UPDATE TASKS ///////////////////////////////////////////////////////////////
void DashboardTableModel::updateTasks(const QVector<Task> &tasks)
{
    for (const Task &t : tasks)
    {
        int newRow = this->dateToRow(t.target);
        int newCol = this->userIdToColumn(t.user_id);
        bool bDisplayTask = (newRow != -1 && !t.deleted);

        if (m_index.contains(t.id)) // the task is already displayed
        {
            Location loc = m_index[t.id];
            int oldRow = this->dateToRow(loc.first);
            int oldCol = loc.second;
            
            if (bDisplayTask)
            {
                if (newRow != oldRow || newCol != oldCol)
                {
                    // we move the post-it (and maybe update it)
                    TaskGroup &gOld = m_data[oldRow].data[oldCol];
                    int i = DashboardTableModel::searchTask(gOld,t.id);
                    if (i != -1) {gOld.removeAt(i);}
                    m_index.remove(t.id);
                    
                    m_data[newRow].data[newCol] << t;
                    m_index.insert(t.id,std::make_pair(t.target,newCol));
                    
                    QModelIndex oldIndex = this->index(oldRow,oldCol,{});
                    QModelIndex oldLineIndex = this->index(oldRow,0,{});
                    QModelIndex newLineIndex = this->index(newRow,0,{});
                    emit dataChanged(oldIndex,oldIndex);
                    emit dataChanged(oldLineIndex,oldLineIndex);
                    emit dataChanged(newLineIndex,newLineIndex);
                }
                else
                {
                    // we only update the post-it
                    TaskGroup &g = m_data[newRow].data[newCol];
                    int i = DashboardTableModel::searchTask(g,t.id);
                    if (i != -1) {g[i] = t;}
                }
                
                QModelIndex newIndex = this->index(newRow,newCol,{});
                emit dataChanged(newIndex,newIndex);
            }
            else
            {
                // we delete the post-it
                TaskGroup &g = m_data[oldRow].data[oldCol];
                int i = DashboardTableModel::searchTask(g,t.id);
                if (i != -1) {g.removeAt(i);}
                m_index.remove(t.id);
                
                QModelIndex index = this->index(oldRow,oldCol,{});
                QModelIndex oldLineIndex = this->index(oldRow,0,{});
                emit dataChanged(index,index);
                emit dataChanged(oldLineIndex,oldLineIndex);
            }
        }
        else // the task is not displayed yet
        {
            if (bDisplayTask)
            {
                // we add the post-it
                m_data[newRow].data[newCol] << t;
                m_index.insert(t.id,std::make_pair(t.target,newCol));
                
                QModelIndex index = this->index(newRow,newCol,{});
                QModelIndex newLineIndex = this->index(newRow,0,{});
                emit dataChanged(index,index);
                emit dataChanged(newLineIndex,newLineIndex);
            }
        }
    }
}
0

There are 0 answers