diff --git a/ui/StatusQ/include/StatusQ/submodelproxymodel.h b/ui/StatusQ/include/StatusQ/submodelproxymodel.h index c8c2593e9b..ebf2f2a1c8 100644 --- a/ui/StatusQ/include/StatusQ/submodelproxymodel.h +++ b/ui/StatusQ/include/StatusQ/submodelproxymodel.h @@ -4,8 +4,10 @@ #include #include +#include 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& additionalRoles, + QQmlEngine* engine, QObject* parent); + + std::optional findSubmodelRole(const QHash& roleNames, + const QString& submodelRoleName); QPointer m_delegateModel; QPointer m_connector; QString m_submodelRoleName; - bool m_initialized = false; bool m_sourceModelDeleted = false; - int m_submodelRole = 0; bool m_dataChangedQueued = false; + std::optional m_submodelRole = 0; + + QStringList m_additionalRoles; QHash m_roleNames; QHash m_additionalRolesMap; int m_additionalRolesOffset = std::numeric_limits::max(); diff --git a/ui/StatusQ/src/submodelproxymodel.cpp b/ui/StatusQ/src/submodelproxymodel.cpp index 3f62a40187..47ffe6430a 100644 --- a/ui/StatusQ/src/submodelproxymodel.cpp +++ b/ui/StatusQ/src/submodelproxymodel.cpp @@ -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(); @@ -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(); @@ -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 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(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 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(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 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& 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 SubmodelProxyModel::findSubmodelRole( + const QHash& 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 {}; } } diff --git a/ui/StatusQ/tests/tst_SubmodelProxyModel.cpp b/ui/StatusQ/tests/tst_SubmodelProxyModel.cpp index 41117399f6..3e1e10554d 100644 --- a/ui/StatusQ/tests/tst_SubmodelProxyModel.cpp +++ b/ui/StatusQ/tests/tst_SubmodelProxyModel.cpp @@ -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(&engine); + + delegateWithRole->setData(QByteArrayLiteral(R"( + import QtQml.Models 2.15 + + ListModel { + property int extraValueRole: 0 + } + )"), QUrl()); + + auto delegateNoRole = std::make_unique(&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(&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(&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(&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.");