Custom QStyledItemDelegate - Applying Edits to Model

2.7k views Asked by At

In my project, I subclassed QStyledItemDelegate and returned a custom editor from the createEditor function.

QWidget* TagEditDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
    TagEditWidget* tagEditWidget = new TagEditWidget(parent, index.data(Qt::UserRole+4).toInt(), index.data(Qt::UserRole+2).toByteArray(), index.data(Qt::UserRole+3).toByteArray(), index.parent().data(Qt::UserRole+4).toInt() == 9, parent->width());
    return tagEditWidget; //tagEditWidget is my custom QWidget
}

When the editing finishes, I want to write the new data back to the model. So I overrode setModelData.

void TagEditDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
{
    TagEditWidget * tagEditWidget = qobject_cast<TagEditWidget*>(editor);
    if (!tagEditWidget)
    {
        QStyledItemDelegate::setModelData(editor, model, index);
        return;
    }

    //Edit model here?
}

This works, but the problem is that setModelData gets called no matter HOW the editor was closed. I only want to write the new data if the editor closed using the EndEditHint, QAbstractItemDelegate::SubmitModelCache. So I connected the closeEditor signal to a slot I made called editFinished.

connect(this, SIGNAL(closeEditor(QWidget*,QAbstractItemDelegate::EndEditHint)), this, SLOT(editFinished(QWidget*,QAbstractItemDelegate::EndEditHint)));

So now I am able to see HOW the editor closed via the EndEditHint and if I should write the data back to the model. Buuuuut, setModelData get's called BEFORE the closeEditor signal. How does one write the data back to the model when the closeEditor signal gets called last? Am I missing something here?

2

There are 2 answers

22
Dusteh On BEST ANSWER

Basic answer:

Your concept seems good almost till the end. I would focus on the TagEditDelegate::setModelData method.

If you actually don't want to update data in the model just check that it didn't change. Meaning that when oldData == newData just return; and skip model updates.

Additional notes:

Looking at your editor creation I get the impression that it doesn't hold a single value which is presented to the user. To make passing the argument more friendly and comparing the editor data easier consider creating a separate class/struct for it. So you could call:

new TagEditWidget(parent, editorData, parent->width())

where EditorData would be your class/struct which might be acquired by a separate function:

EditorData editorData = readEditorData(index);

The function could be reused in the setModelData method to check the condition:

if (tagEditWidget->getEditorData() == readEditorData(index)) return;

Also avoid using magic numbers like Qt::UserRole+2. Create your own enum to specify the required roles. For example:

enum class MyRole
{
    Data1 = Qt::UserRole,
    Data2,
    Data3,
};

EDIT according to the discussion in the comments

If what you want is to discover whether user actually didn't cancel the edition one way or the other, you could override eventFilter either inside the editor or the delegate. When creating editor call installEventFilter in the constructor. Your eventFilter implementation could look like this:

bool eventFilter(QObject *object, QEvent *event) override
{
    if (event->type() == QEvent::KeyPress)
    {
        const auto key = static_cast<QKeyEvent *>(event)->key();
        if (key == Qt::Key_Enter || key == Qt::Key_Return || key == Qt::Key_Tab)
            submitted = true;
    }
    else if (event->type() == QEvent::FocusAboutToChange &&
             static_cast<QFocusEvent*>(event)->reason() == Qt::MouseFocusReason)
    {
        submitted = true;
    }
    // extetend the conditions (add else if) to include
    // events which you might want to treat as submitted

    return QLineEdit::eventFilter(object, event);
}

Where submitted is a bool editor member initialized to false in constructor. Then you could create a getter method isSubmitted() and you are ready to check the status inside setModelData method.

if (tagEditWidget->isSubmitted())
{
    // process data or update model here
}
2
mrg95 On

I just answered my own question in my own way. Dusteh's solution is "more correct" I think though.

I made a bool in the delegate class called shouldCommit and set it to false.

Then in setModelData I check if I should write the data to the model.

void TagEditDelegate::setModelData(QWidget * editor, QAbstractItemModel * model, const QModelIndex & index) const
{
    TagEditWidget * tagEditWidget = qobject_cast<TagEditWidget*>(editor);
    if (!tagEditWidget)
    {
        QStyledItemDelegate::setModelData(editor, model, index);
        return;
    }

    if(shouldCommit)
    {
        //write data here
    }
}

Then, when the closeEditor signal is emitted, I check the hint and temporarily set shouldCommit to true and call commitData again. Now that shouldCommit is true, the data gets written.

void TagEditDelegate::editFinished(QWidget * editor, QAbstractItemDelegate::EndEditHint hint)
{
    if(hint == QAbstractItemDelegate::SubmitModelCache)
    {
        TagEditWidget * tagEditWidget = qobject_cast<TagEditWidget*>(editor);
        if(tagEditWidget)
        {
            shouldCommit = true;
            commitData(editor);
            shouldCommit = false;
        }
    }
}

While this works for my use case very well, it might not for everyone and Dusteh's comment section might be more useful to some. His method is to override the eventFilter and write your own implementation on when to call commitData. This is probably a more suitable solution for people wanting more control.

But for anyone wanting to use the default eventFilter implementation and simply look at the EndEditHint, here is my solution.