Chapter 1: the solution you cannot use
What I think is the correct way to address your question is a proxy model in charge of maintaining the undo stack. It does so by saving the model's data right before changing it.
Header:
class MyUndoModel : public QIdentityProxyModel
{
public:
MyUndoModel(QObject* parent = nullptr);
bool setData(const QModelIndex& index, const QVariant& data, int role) override;
bool restoreData(const QModelIndex& index, const QVariant& data, int role);
private:
void pushOnUndoStack(const QPersistentModelIndex& index, int role, const QVariant& value) const;
//QUndoStack undoStack;
};
Source:
bool MyUndoModel::setData(const QModelIndex& index, const QVariant& data, int role)
{
QVariant currentData = index.data(role);
bool result = QIdentityProxyModel::setData(index, data, role);
if (result) {
//If the source model accepted the change, push currentData to the undo stack.
pushOnUndoStack(QPersistentModelIndex(index), role, currentData );
}
return result;
}
bool MyUndoModel::restoreData(const QModelIndex& index, const QVariant& data, int role)
{
return QIdentityProxyModel::setData(index, data, role);
}
Note that we use QPersistentModelIndex
es in a modified version of pushOnUndoStack
(that I let you implement yourself). Also, I did not write how the stacked undo/redo commands should be processed, apart from calling restoreData
. As long as you get the idea...
Chapter 2: where it fails for you
The above solution works regarless of the actual class of the source model ... except if working with QTableWidget
and QTreeWidget
.
What blocks this solution in the case of e.g. QTableWidget
is its internal model (QTableModel
).
- You cannot substitute model of your
QTableWidget
to use MyUndoModel
instead.
If you try, you will very quickly see your application crash.
- You could in theory subclass
QTableModel
to perform the above substitution but I advise against it.
Sample: myTableWidget->QTableView::setModel(new MyQTableModel);
QTableModel
is a private class in Qt and should not be used directly. I wish I knew why it was done this way.
Chapter 3:The alternative solution
Alternatively, subclassing QStyledItemDelegate
could work for you. The design is not as clean, there are more ways to make a mistake when using it in your window but it essentially follows the same logic as the above proxy model.
class UndoItemDelegate : protected QStyledItemDelegate
{
public:
UndoItemDelegate(QUndoStack* undoStack, QObject* parent = nullptr);
//Importnt: we set setModelData as final.
void setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const override final;
protected:
virtual QVariant valueFromEditor(QWidget *editor) const noexcept = 0;
virtual int roleFromEditor(QWidget *editor) const noexcept = 0;
private:
void pushOnUndoStack(const QPersistentModelIndex& index, int role, const QVariant& value) const;
//undoStack as a pointer makes it possible to share it across several delegates of the same view (or of multiple view)
mutable QUndoStack* undoStack;
};
The magic is in setModelData
.
void UndoItemDelegate::setModelData(QWidget *editor, QAbstractItemModel *model, const QModelIndex &index) const
{
auto role = roleFromEditor(editor);
QVariant currentData = index.data(role);
bool dataChanged = model->setData(index, valueFromEditor(editor), role);
if (dataChanged && undoStack) {
pushOnUndoStack(QPersistentModelIndex(index), role, currentData);
}
}
I kept the version with index
(my habit) but you could use the pointers to QTableItem
of course.
To be used (most likely in the constructor of your window):
ui->setupUi(this);
auto myDelegate = new MyUndoItemDelegateSubclass(&windowUndoStack, ui->myTableWidget);
ui->myTableWidget->setItemDelegate(myDelegate);
You will have to implement:
pushOnUndoStack
(once).
roleFromEditor
and valueFromEditor
(for every subclass).
- the processing of undo/redo commands.
Edit to address your comment.
I am going to assume you know how QAbstractIdemModel
and subclasses work in a generic manner. To manipulate a checkState in the model of a QTableWidget
, I recommend you create a UndoCheckboxDelegate
subclass to implement/override the additional methods this way:
Header:
class UndoCheckboxDelegate : public UndoItemDelegate
{
public:
UndoCheckboxDelegate(QUndoStack* undoStack, QObject* parent = nullptr);
QWidget* createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const override;
protected:
virtual QVariant valueFromEditor(QWidget *editor) const noexcept override;
virtual int roleFromEditor(QWidget *editor) const noexcept override;
};
Source:
UndoCheckboxDelegate::UndoCheckboxDelegate(QUndoStack* undoStack, QObject* parent)
: UndoItemDelegate(undoStack, parent)
{}
QWidget* UndoCheckboxDelegate::createEditor(QWidget *parent, const QStyleOptionViewItem &option, const QModelIndex &index) const
{
if (index.isValid()) {
QCheckBox* control = new QCheckBox(parent);
control->setText(index.data(Qt::DisplayRole).toString());
control->setCheckState(index.data(Qt::CheckStateRole).value<Qt::CheckState>());
return control;
}
else
return nullptr;
}
QVariant UndoCheckboxDelegate::valueFromEditor(QWidget *editor) const noexcept
{
if (editor)
return static_cast<QCheckBox*>(editor)->checkState();
else
return QVariant();
}
int UndoCheckboxDelegate::roleFromEditor(QWidget * /* unused */) const noexcept
{
return Qt::CheckStateRole;
}
It may be only a starting point for you. Make sure it correctly fills the undo stack first; after that, you can tweak the behavior a bit.