fix(SubmodelProxyModel): model reset emitted properly when roles change

Closes: #14650
This commit is contained in:
Michał Cieślak 2024-05-09 10:02:05 +02:00 committed by Michał
parent 4dfa0a1b05
commit ed33f21828
3 changed files with 418 additions and 93 deletions

View File

@ -4,8 +4,10 @@
#include <QPointer>
#include <limits>
#include <optional>
class QQmlComponent;
class QQmlEngine;
class SubmodelProxyModel : public QIdentityProxyModel
{
@ -34,27 +36,36 @@ signals:
void delegateModelChanged();
void submodelRoleNameChanged();
protected slots:
void resetInternalData();
private slots:
void onCustomRoleChanged(QObject* source, int role);
void emitAllDataChanged();
private:
void initializeIfReady();
void initialize();
void initRoles();
void updateRoleNames();
void onDelegateChanged();
QStringList fetchAdditionalRoles(QQmlComponent* delegateComponent);
QQmlComponent* buildConnectorComponent(
const QHash<QString, int>& additionalRoles,
QQmlEngine* engine, QObject* parent);
std::optional<int> findSubmodelRole(const QHash<int, QByteArray>& roleNames,
const QString& submodelRoleName);
QPointer<QQmlComponent> m_delegateModel;
QPointer<QQmlComponent> m_connector;
QString m_submodelRoleName;
bool m_initialized = false;
bool m_sourceModelDeleted = false;
int m_submodelRole = 0;
bool m_dataChangedQueued = false;
std::optional<int> m_submodelRole = 0;
QStringList m_additionalRoles;
QHash<int, QByteArray> m_roleNames;
QHash<QString, int> m_additionalRolesMap;
int m_additionalRolesOffset = std::numeric_limits<int>::max();

View File

@ -24,14 +24,14 @@ SubmodelProxyModel::SubmodelProxyModel(QObject* parent)
{
}
QVariant SubmodelProxyModel::data(const QModelIndex &index, int role) const
QVariant SubmodelProxyModel::data(const QModelIndex& index, int role) const
{
static constexpr auto attachementPropertyName = "_attachement";
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return {};
if (m_initialized && m_delegateModel && role == m_submodelRole) {
if (m_delegateModel && m_submodelRole && role == m_submodelRole) {
auto submodel = QIdentityProxyModel::data(index, role);
QObject* submodelObj = submodel.value<QObject*>();
@ -82,10 +82,10 @@ QVariant SubmodelProxyModel::data(const QModelIndex &index, int role) const
return wrappedInstance;
}
if (role >= m_additionalRolesOffset
if (m_submodelRole && role >= m_additionalRolesOffset
&& role < m_additionalRolesOffset + m_additionalRolesMap.size())
{
auto submodel = data(index, m_submodelRole);
auto submodel = data(index, *m_submodelRole);
auto submodelObj = submodel.value<QObject*>();
@ -107,8 +107,11 @@ void SubmodelProxyModel::setSourceModel(QAbstractItemModel* model)
return;
}
if (sourceModel() == model)
return;
// Workaround for QTBUG-57971
if (model && model->roleNames().isEmpty())
if (model->roleNames().isEmpty())
connect(model, &QAbstractItemModel::rowsInserted,
this, &SubmodelProxyModel::initRoles);
@ -116,13 +119,16 @@ void SubmodelProxyModel::setSourceModel(QAbstractItemModel* model)
this->m_sourceModelDeleted = true;
});
if (m_delegateModel)
m_additionalRoles = fetchAdditionalRoles(m_delegateModel);
QIdentityProxyModel::setSourceModel(model);
initializeIfReady();
}
QHash<int, QByteArray> SubmodelProxyModel::roleNames() const
{
return m_roleNames;
return m_roleNames.isEmpty() && sourceModel()
? sourceModel()->roleNames() : m_roleNames;;
}
QQmlComponent* SubmodelProxyModel::delegateModel() const
@ -132,22 +138,36 @@ QQmlComponent* SubmodelProxyModel::delegateModel() const
void SubmodelProxyModel::setDelegateModel(QQmlComponent* delegateModel)
{
if (m_delegateModel != nullptr) {
qWarning("Changing delegate model is not supported!");
return;
}
if (m_delegateModel == delegateModel)
return;
if (m_delegateModel)
disconnect(delegateModel, &QObject::destroyed,
this, &SubmodelProxyModel::onDelegateChanged);
if (sourceModel() != nullptr) {
QStringList additionalRoles = fetchAdditionalRoles(delegateModel);
if (delegateModel)
connect(delegateModel, &QObject::destroyed,
this, &SubmodelProxyModel::onDelegateChanged);
if (m_additionalRoles == additionalRoles) {
m_delegateModel = delegateModel;
m_delegateModel = delegateModel;
if (m_submodelRole && rowCount() && columnCount()) {
emit dataChanged(index(0, 0),
index(rowCount() - 1, columnCount() - 1),
{ *m_submodelRole });
}
} else {
beginResetModel();
m_delegateModel = delegateModel;
m_additionalRoles = additionalRoles;
endResetModel();
}
} else {
m_delegateModel = delegateModel;
}
onDelegateChanged();
initializeIfReady();
emit delegateModelChanged();
}
const QString& SubmodelProxyModel::submodelRoleName() const
@ -166,9 +186,25 @@ void SubmodelProxyModel::setSubmodelRoleName(const QString& sumodelRoleName)
}
m_submodelRoleName = sumodelRoleName;
emit submodelRoleNameChanged();
initializeIfReady();
if (sourceModel()) {
m_submodelRole = findSubmodelRole(sourceModel()->roleNames(),
sumodelRoleName);
if (rowCount() && columnCount()) {
emit dataChanged(index(0, 0),
index(rowCount() - 1, columnCount() - 1),
{ *m_submodelRole });
}
}
emit submodelRoleNameChanged();
}
void SubmodelProxyModel::resetInternalData()
{
QIdentityProxyModel::resetInternalData();
updateRoleNames();
}
void SubmodelProxyModel::onCustomRoleChanged(QObject* source, int role)
@ -193,61 +229,25 @@ void SubmodelProxyModel::emitAllDataChanged()
emit this->dataChanged(index(0, 0), index(count - 1, 0), roles);
}
void SubmodelProxyModel::initializeIfReady()
void SubmodelProxyModel::initRoles()
{
if (!m_submodelRoleName.isEmpty() && sourceModel()
&& !sourceModel()->roleNames().empty() && m_delegateModel)
initialize();
disconnect(sourceModel(), &QAbstractItemModel::rowsInserted,
this, &SubmodelProxyModel::initRoles);
resetInternalData();
}
void SubmodelProxyModel::initialize()
void SubmodelProxyModel::updateRoleNames()
{
if (sourceModel() == nullptr)
return;
auto roles = sourceModel()->roleNames();
auto submodelKeys = roles.keys(m_submodelRoleName.toUtf8());
auto submodelKeysCount = submodelKeys.size();
if (submodelKeysCount == 1) {
m_submodelRole = submodelKeys.first();
} else if (submodelKeysCount == 0){
qWarning() << "Submodel role not found!";
if (roles.empty())
return;
} else {
qWarning() << "Malformed source model - multiple roles found for given "
"submodel role name!";
return;
}
auto creationContext = m_delegateModel->creationContext();
auto parentContext = creationContext
? creationContext : m_delegateModel->engine()->rootContext();
QIdentityProxyModel emptyModel;
auto context = std::make_unique<QQmlContext>(parentContext);
// The delegate object is created in order to inspect properties. It may
// be not properly initialized because of e.g. lack of context properties
// containing submodel. To avoid warnings, they are muted by setting empty
// message handler temporarily.
QtMessageHandler originalHandler = qInstallMessageHandler(
emptyMessageHandler);
std::unique_ptr<QObject> instance(m_delegateModel->create(context.get()));
qInstallMessageHandler(originalHandler);
const QMetaObject* meta = instance->metaObject();
QStringList additionalRoles;
for (auto i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
const QLatin1String propertyName(meta->property(i).name());
bool isRole = propertyName.endsWith(QLatin1String(roleSuffix));
if (!isRole)
continue;
additionalRoles << propertyName.chopped(qstrlen(roleSuffix));
}
m_submodelRole = findSubmodelRole(roles, m_submodelRoleName);
const auto keys = roles.keys();
const auto maxElementIt = std::max_element(keys.begin(), keys.end());
@ -257,7 +257,9 @@ void SubmodelProxyModel::initialize()
auto maxRoleKey = *maxElementIt;
m_additionalRolesOffset = maxRoleKey + 1;
for (auto& additionalRole : qAsConst(additionalRoles)) {
m_additionalRolesMap.clear();
for (auto& additionalRole : qAsConst(m_additionalRoles)) {
auto roleKey = ++maxRoleKey;
roles.insert(roleKey, additionalRole.toUtf8());
@ -266,6 +268,53 @@ void SubmodelProxyModel::initialize()
m_roleNames = roles;
if (m_delegateModel == nullptr)
return;
m_connector = buildConnectorComponent(m_additionalRolesMap,
m_delegateModel->engine(),
m_delegateModel);
}
QStringList SubmodelProxyModel::fetchAdditionalRoles(
QQmlComponent* delegateComponent)
{
auto creationContext = delegateComponent->creationContext();
auto parentContext = creationContext
? creationContext : delegateComponent->engine()->rootContext();
auto context = std::make_unique<QQmlContext>(parentContext);
// The delegate object is created in order to inspect properties. It may
// be not properly initialized because of e.g. lack of context properties
// containing submodel. To avoid warnings, they are muted by setting empty
// message handler temporarily.
QtMessageHandler originalHandler = qInstallMessageHandler(
emptyMessageHandler);
std::unique_ptr<QObject> instance(delegateComponent->create(context.get()));
qInstallMessageHandler(originalHandler);
const QMetaObject* meta = instance->metaObject();
QStringList additionalRoles;
for (auto i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
const QLatin1String propertyName(meta->property(i).name());
bool isRole = propertyName.endsWith(QLatin1String(roleSuffix));
if (!isRole)
continue;
additionalRoles << propertyName.chopped(qstrlen(roleSuffix));
}
return additionalRoles;
}
QQmlComponent* SubmodelProxyModel::buildConnectorComponent(
const QHash<QString, int>& additionalRoles, QQmlEngine* engine,
QObject* parent)
{
QString connectorCode = R"(
import QtQml 2.15
@ -273,10 +322,10 @@ void SubmodelProxyModel::initialize()
signal customRoleChanged(source: QtObject, role: int)
)";
for (auto& additionalRole : qAsConst(additionalRoles)) {
int role = m_additionalRolesMap[additionalRole];
for (auto i = additionalRoles.cbegin(); i != additionalRoles.cend(); ++i) {
int role = i.value();
auto upperCaseRole = additionalRole;
auto upperCaseRole = i.key();
upperCaseRole[0] = upperCaseRole[0].toUpper();
connectorCode += QString(R"(
@ -286,27 +335,30 @@ void SubmodelProxyModel::initialize()
connectorCode += "}";
m_connector = new QQmlComponent(m_delegateModel->engine(), m_delegateModel);
m_connector->setData(connectorCode.toUtf8(), {});
auto connector = new QQmlComponent(engine, parent);
connector->setData(connectorCode.toUtf8(), {});
m_initialized = true;
return connector;
}
void SubmodelProxyModel::initRoles()
std::optional<int> SubmodelProxyModel::findSubmodelRole(
const QHash<int, QByteArray>& roleNames,
const QString& submodelRoleName)
{
disconnect(sourceModel(), &QAbstractItemModel::rowsInserted,
this, &SubmodelProxyModel::initRoles);
if (roleNames.empty() || submodelRoleName.isEmpty())
return {};
resetInternalData();
initializeIfReady();
}
auto submodelKeys = roleNames.keys(m_submodelRoleName.toUtf8());
auto submodelKeysCount = submodelKeys.size();
void SubmodelProxyModel::onDelegateChanged()
{
emit delegateModelChanged();
if (m_initialized && rowCount() && columnCount()) {
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1),
{ m_submodelRole });
if (submodelKeysCount == 1) {
return submodelKeys.first();
} else if (submodelKeysCount == 0) {
qWarning() << "Submodel role not found!";
return {};
} else {
qWarning() << "Malformed source model - multiple roles found for given "
"submodel role name!";
return {};
}
}

View File

@ -204,8 +204,8 @@ private slots:
delegate.reset();
QCOMPARE(delegateModelChangedSpy.count(), 1);
QCOMPARE(dataChangedSpy.count(), 1);
QCOMPARE(delegateModelChangedSpy.count(), 0);
QCOMPARE(dataChangedSpy.count(), 0);
QCOMPARE(model.rowCount(), 3);
QCOMPARE(model.data(model.index(0, 0),
@ -302,6 +302,7 @@ private slots:
model.setSourceModel(sourceModel);
model.setDelegateModel(delegate.get());
model.setSubmodelRoleName(QStringLiteral("balances"));
ListModelWrapper expected(engine, R"([
@ -400,6 +401,267 @@ private slots:
QCOMPARE(signalsSpy.count(), 2);
}
void modelResetWhenRoleChangedTest() {
QQmlEngine engine;
auto delegateWithRole = std::make_unique<QQmlComponent>(&engine);
delegateWithRole->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValueRole: 0
}
)"), QUrl());
auto delegateNoRole = std::make_unique<QQmlComponent>(&engine);
delegateNoRole->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {}
)"), QUrl());
ListModelWrapper sourceModel(engine, R"([
{ "balances": [], "name": "name 1" }
])");
// 1. set source, 2. set delegate model, 3. set submodel role name
{
SubmodelProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setDelegateModel(delegateWithRole.get());
QCOMPARE(signalsSpy.count(), 4);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
QCOMPARE(model.roleNames().count(), 3);
model.setSubmodelRoleName(QStringLiteral("balances"));
QCOMPARE(signalsSpy.count(), 5);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
QCOMPARE(model.roleNames().count(), 3);
}
// 1. set delegate model, 2. set source, 3. set submodel role name
{
SubmodelProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setDelegateModel(delegateWithRole.get());
QCOMPARE(signalsSpy.count(), 0);
QCOMPARE(model.roleNames().count(), 0);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 3);
model.setSubmodelRoleName(QStringLiteral("balances"));
QCOMPARE(signalsSpy.count(), 3);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 3);
}
// 1. set submodel role name, 2. set delegate model, 3. set source
{
SubmodelProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setSubmodelRoleName(QStringLiteral("balances"));
model.setDelegateModel(delegateWithRole.get());
QCOMPARE(signalsSpy.count(), 0);
QCOMPARE(model.roleNames().count(), 0);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 3);
}
// 1. set source, 2. set delegate model (no extra roles),
// 3. set submodel role name
{
SubmodelProxyModel model;
ModelSignalsSpy signalsSpy(&model);
model.setSourceModel(sourceModel);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setDelegateModel(delegateNoRole.get());
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
model.setSubmodelRoleName(QStringLiteral("balances"));
QCOMPARE(signalsSpy.count(), 3);
QCOMPARE(signalsSpy.dataChangedSpy.count(), 1);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.roleNames().count(), 2);
}
}
// SubmodelProxyModel instantiates delegate model in order to inspect
// extra roles. This instantiation must be deferred until model is,
// available. Otherwise it may lead to accessing uninitialized external
// data within a delegate instance.
void deferredDelegateInstantiationTest() {
QQmlEngine engine;
QObject controlObject;
engine.rootContext()->setContextProperty("control", &controlObject);
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
import QtQml 2.15
ListModel {
property int extraValueRole: 0
Component.onCompleted: control.objectName = "instantiated"
}
)"), QUrl());
ListModelWrapper sourceModel(engine, R"([
{ "balances": [], "name": "name 1" }
])");
{
SubmodelProxyModel model;
model.setSourceModel(sourceModel);
QCOMPARE(controlObject.objectName(), "");
model.setDelegateModel(delegate.get());
QCOMPARE(controlObject.objectName(), "instantiated");
}
controlObject.setObjectName("");
{
SubmodelProxyModel model;
model.setDelegateModel(delegate.get());
QCOMPARE(controlObject.objectName(), "");
model.setSourceModel(sourceModel);
QCOMPARE(controlObject.objectName(), "instantiated");
}
}
void sourceModelResetTest() {
class IdentityModel : public QIdentityProxyModel {};
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValueRole: 0
}
)"), QUrl());
ListModelWrapper sourceModel1(engine, R"([
{ "balances": [], "name": "name 1" }
])");
ListModelWrapper sourceModel2(engine, R"([
{ "key": "1", "balances": [], "name": "name 1", "color": "red" }
])");
IdentityModel identity;
identity.setSourceModel(sourceModel1);
SubmodelProxyModel model;
model.setSourceModel(&identity);
model.setDelegateModel(delegate.get());
model.setSubmodelRoleName(QStringLiteral("balances"));
QCOMPARE(model.rowCount(), 1);
auto roles = model.roleNames();
QCOMPARE(roles.size(), 3);
ModelSignalsSpy signalsSpy(&model);
identity.setSourceModel(sourceModel2);
QCOMPARE(signalsSpy.count(), 2);
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
QCOMPARE(model.rowCount(), 1);
roles = model.roleNames();
QCOMPARE(roles.size(), 5);
}
void sourceModelLateRolesInitTest() {
QQmlEngine engine;
auto delegate = std::make_unique<QQmlComponent>(&engine);
delegate->setData(QByteArrayLiteral(R"(
import QtQml.Models 2.15
ListModel {
property int extraValueRole: 0
}
)"), QUrl());
ListModelWrapper sourceModel(engine, R"([])");
SubmodelProxyModel model;
model.setSourceModel(sourceModel);
model.setDelegateModel(delegate.get());
model.setSubmodelRoleName(QStringLiteral("balances"));
QCOMPARE(model.rowCount(), 0);
auto roles = model.roleNames();
QCOMPARE(roles.size(), 0);
ModelSignalsSpy signalsSpy(&model);
sourceModel.append(QJsonArray {
QJsonObject {{ "name", "D"}, { "balances", "d1" }},
QJsonObject {{ "name", "D"}, { "balances", "d2" }}
});
QCOMPARE(model.rowCount(), 2);
roles = model.roleNames();
QCOMPARE(roles.size(), 3);
}
void multipleProxiesTest() {
QSKIP("Not implemented yet. The goal is to make the proxy fully "
"non-intrusive what will fix the isse pointed in this test.");