From 9c4e1a8fe93fd304e33b3abf70f2fffba7e8edd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Mon, 2 Sep 2024 13:44:45 +0200 Subject: [PATCH] SubmodelProxyModel removed Closes: #15825 --- storybook/pages/SubmodelProxyModelPage.qml | 346 --------- ui/StatusQ/CMakeLists.txt | 2 - .../include/StatusQ/submodelproxymodel.h | 81 -- ui/StatusQ/src/plugin.cpp | 2 - ui/StatusQ/src/submodelproxymodel.cpp | 366 --------- ui/StatusQ/tests/CMakeLists.txt | 4 - ui/StatusQ/tests/tst_SubmodelProxyModel.cpp | 719 ------------------ 7 files changed, 1520 deletions(-) delete mode 100644 storybook/pages/SubmodelProxyModelPage.qml delete mode 100644 ui/StatusQ/include/StatusQ/submodelproxymodel.h delete mode 100644 ui/StatusQ/src/submodelproxymodel.cpp delete mode 100644 ui/StatusQ/tests/tst_SubmodelProxyModel.cpp diff --git a/storybook/pages/SubmodelProxyModelPage.qml b/storybook/pages/SubmodelProxyModelPage.qml deleted file mode 100644 index c56b202d76..0000000000 --- a/storybook/pages/SubmodelProxyModelPage.qml +++ /dev/null @@ -1,346 +0,0 @@ -import QtQuick 2.15 -import QtQuick.Controls 2.15 -import QtQuick.Layouts 1.15 - -import StatusQ 0.1 -import Storybook 1.0 - -import SortFilterProxyModel 0.2 - -import StatusQ.Core.Utils 0.1 - -Item { - id: root - - readonly property string intro: - "THIS MODEL IS DEPRECTAED. Use ObjectProxyModel instead
" + - "The example uses two source models. The first model contains networks" - + " (id and metadata such as name and color), visible on the left. The" - + " second model contains tokens metadata and their balances per" - + " network in the submodel (network id, balance).\n" - + "The SubmodelProxyModel wrapping the tokens model joins the submodels" - + " to the network model. It also provides filtering and sorting via" - + " SFPM (slider and checkbox below). Additionally, SubmodelProxyModel" - + " calculates the summary balance and issues it as a role in the" - + " top-level model (via SumAggregator). This sum is then used to" - + " dynamically sort the tokens model.\nClick on balances to increase" - + " the amount." - - readonly property int numberOfTokens: 2000 - - readonly property var colors: [ - "purple", "lightgreen", "red", "blue", "darkgreen" - ] - - function getRandomInt(max) { - return Math.floor(Math.random() * max); - } - - ListModel { - id: networksModel - - ListElement { - chainId: "1" - name: "Mainnet" - color: "purple" - } - ListElement { - chainId: "2" - name: "Optimism" - color: "lightgreen" - } - ListElement { - chainId: "3" - name: "Status" - color: "red" - } - ListElement { - chainId: "4" - name: "Abitrum" - color: "blue" - } - ListElement { - chainId: "5" - name: "Sepolia" - color: "darkgreen" - } - } - - ListModel { - id: tokensModel - - Component.onCompleted: { - // Populate model with given number of tokens containing random - // balances - const numberOfTokens = root.numberOfTokens - const tokens = [] - - const chainIds = [] - - for (let n = 0; n < networksModel.count; n++) - chainIds.push(networksModel.get(n).chainId) - - for (let i = 0; i < numberOfTokens; i++) { - const balances = [] - const numberOfBalances = 1 + getRandomInt(networksModel.count) - const chainIdsCpy = [...chainIds] - - for (let i = 0; i < numberOfBalances; i++) { - const chainId = chainIdsCpy.splice( - getRandomInt(chainIdsCpy.length), 1)[0] - - balances.push({ - chainId: chainId, - balance: 1 + getRandomInt(200) - }) - } - - tokens.push({ name: `Token ${i + 1}`, balances }) - } - - append(tokens) - } - } - - // Proxy model joining networksModel to submodels under "balances" role. - // Additionally submodel is filtered and sorted via SFPM. Submodel is - // accessible via "submodel" context property. - SubmodelProxyModel { - id: submodelProxyModel - - sourceModel: tokensModel - - delegateModel: SortFilterProxyModel { - id: delegateRoot - - // properties exposed as roles to the top-level model - readonly property var balancesCountRole: submodel.count - readonly property int sumRole: aggregator.value - - sourceModel: joinModel - - filters: FastExpressionFilter { - expression: balance >= thresholdSlider.value - - expectedRoles: "balance" - } - - sorters: RoleSorter { - roleName: "name" - enabled: sortCheckBox.checked - } - - readonly property LeftJoinModel joinModel: LeftJoinModel { - leftModel: submodel - rightModel: networksModel - - joinRole: "chainId" - } - - readonly property SumAggregator aggregator: SumAggregator { - id: aggregator - - model: delegateRoot - roleName: "balance" - } - } - - submodelRoleName: "balances" - } - - SortFilterProxyModel { - id: sortBySumProxy - - sourceModel: submodelProxyModel - - sorters: RoleSorter { - roleName: "sum" - ascendingOrder: false - } - } - - ColumnLayout { - anchors.fill: parent - anchors.margins: 10 - - Label { - Layout.fillWidth: true - wrapMode: Text.Wrap - lineHeight: 1.2 - text: root.intro - } - - MenuSeparator { - Layout.fillWidth: true - } - - RowLayout { - Layout.fillWidth: true - Layout.fillHeight: true - - ListView { - Layout.preferredWidth: 110 - Layout.leftMargin: 10 - Layout.fillHeight: true - - spacing: 20 - - model: networksModel - - delegate: ColumnLayout { - width: ListView.view.width - - Label { - Layout.fillWidth: true - text: model.name - font.bold: true - } - - Rectangle { - Layout.preferredWidth: changeColorButton.width - Layout.preferredHeight: 10 - - color: model.color - } - - Button { - id: changeColorButton - - text: "Change color" - - onClicked: { - const currentIdx = root.colors.indexOf(model.color) - const numberOfColors = root.colors.length - const nextIdx = (currentIdx + 1) % numberOfColors - - networksModel.setProperty(model.index, "color", - root.colors[nextIdx]) - } - } - } - } - - Rectangle { - Layout.preferredWidth: 1 - Layout.fillHeight: true - Layout.rightMargin: 20 - - color: "lightgray" - } - - // ListView consuming model don't have to do any transformation - // of the submodels internally because it's handled externally via - // SubmodelProxyModel. - ListView { - id: listView - - Layout.fillWidth: true - Layout.fillHeight: true - - reuseItems: true - - ScrollBar.vertical: ScrollBar {} - - clip: true - spacing: 18 - - model: sortBySumProxy - - delegate: ColumnLayout { - id: delegateRoot - - width: ListView.view.width - height: 46 - spacing: 0 - - readonly property var balances: model.balances - - Label { - id: tokenLabel - - Layout.fillWidth: true - text: model.name - font.bold: true - } - - RowLayout { - spacing: 14 - - Layout.fillWidth: true - - Repeater { - model: delegateRoot.balances - - Rectangle { - width: label.implicitWidth * 1.5 - height: label.implicitHeight * 2 - - color: "transparent" - border.width: 2 - border.color: model.color - - Label { - id: label - - anchors.centerIn: parent - - text: `${model.name} (${model.balance})` - font.pixelSize: 10 - } - - MouseArea { - anchors.fill: parent - - onClicked: { - const item = ModelUtils.getByKey( - tokensModel, "name", tokenLabel.text) - const index = ModelUtils.indexOf( - item.balances, "chainId", model.chainId) - - item.balances.setProperty( - index, "balance", - item.balances.get(index).balance + 1) - } - } - } - } - - Label { - text: model.balancesCount + " / " + model.sum - } - } - } - } - } - - MenuSeparator { - Layout.fillWidth: true - } - - RowLayout { - Label { - text: `Number of tokens: ${listView.count}, minimum balance:` - } - - Slider { - id: thresholdSlider - - from: 0 - to: 201 - stepSize: 1 - } - - Label { - text: thresholdSlider.value - } - - CheckBox { - id: sortCheckBox - - text: "sort networks by name" - } - } - } -} - -// category: Models diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index 8e964ec9e8..6ece1f77dd 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -119,7 +119,6 @@ add_library(StatusQ SHARED include/StatusQ/statussyntaxhighlighter.h include/StatusQ/statuswindow.h include/StatusQ/stringutilsinternal.h - include/StatusQ/submodelproxymodel.h include/StatusQ/sumaggregator.h include/StatusQ/systemutilsinternal.h include/StatusQ/undefinedfilter.h @@ -152,7 +151,6 @@ add_library(StatusQ SHARED src/statussyntaxhighlighter.cpp src/statuswindow.cpp src/stringutilsinternal.cpp - src/submodelproxymodel.cpp src/sumaggregator.cpp src/systemutilsinternal.cpp src/undefinedfilter.cpp diff --git a/ui/StatusQ/include/StatusQ/submodelproxymodel.h b/ui/StatusQ/include/StatusQ/submodelproxymodel.h deleted file mode 100644 index e43294eaa9..0000000000 --- a/ui/StatusQ/include/StatusQ/submodelproxymodel.h +++ /dev/null @@ -1,81 +0,0 @@ -#pragma once - -#include -#include - -#include -#include - -#include "modelsyncedcontainer.h" - -class QQmlComponent; -class QQmlEngine; - -/** - * NOTE: This proxy model is deprecated and will be removed soon. Use - * ObjectProxyModel instead. - */ -class QT_DEPRECATED_X("Use ObjectProxyModel instead") SubmodelProxyModel - : public QIdentityProxyModel -{ - Q_OBJECT - - Q_PROPERTY(QQmlComponent* delegateModel READ delegateModel - WRITE setDelegateModel NOTIFY delegateModelChanged) - - Q_PROPERTY(QString submodelRoleName READ submodelRoleName - WRITE setSubmodelRoleName NOTIFY submodelRoleNameChanged) - -public: - explicit SubmodelProxyModel(QObject* parent = nullptr); - - QVariant data(const QModelIndex& index, int role) const override; - void setSourceModel(QAbstractItemModel* sourceModel) override; - QHash roleNames() const override; - - QQmlComponent* delegateModel() const; - void setDelegateModel(QQmlComponent* delegateModel); - - const QString& submodelRoleName() const; - void setSubmodelRoleName(const QString& sumodelRoleName); - -signals: - void delegateModelChanged(); - void submodelRoleNameChanged(); - -protected slots: - void resetInternalData(); - -private slots: - void onCustomRoleChanged(QObject* source, int role); - void emitAllDataChanged(); - -private: - void initRoles(); - void updateRoleNames(); - - 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_sourceModelDeleted = false; - 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(); - - mutable ModelSyncedContainer> m_container; -}; diff --git a/ui/StatusQ/src/plugin.cpp b/ui/StatusQ/src/plugin.cpp index d6c7380bce..60895196d9 100644 --- a/ui/StatusQ/src/plugin.cpp +++ b/ui/StatusQ/src/plugin.cpp @@ -26,7 +26,6 @@ #include "StatusQ/statussyntaxhighlighter.h" #include "StatusQ/statuswindow.h" #include "StatusQ/stringutilsinternal.h" -#include "StatusQ/submodelproxymodel.h" #include "StatusQ/sumaggregator.h" #include "StatusQ/systemutilsinternal.h" #include "StatusQ/undefinedfilter.h" @@ -70,7 +69,6 @@ public: qmlRegisterType("StatusQ", 0, 1, "ObjectProxyModel"); qmlRegisterType("StatusQ", 0, 1, "LeftJoinModel"); - qmlRegisterType("StatusQ", 0, 1, "SubmodelProxyModel"); qmlRegisterType("StatusQ", 0, 1, "RoleRename"); qmlRegisterType("StatusQ", 0, 1, "RolesRenamingModel"); qmlRegisterType("StatusQ", 0, 1, "SumAggregator"); diff --git a/ui/StatusQ/src/submodelproxymodel.cpp b/ui/StatusQ/src/submodelproxymodel.cpp deleted file mode 100644 index 5fc039d957..0000000000 --- a/ui/StatusQ/src/submodelproxymodel.cpp +++ /dev/null @@ -1,366 +0,0 @@ -#include "StatusQ/submodelproxymodel.h" - -#include -#include -#include -#include - -#include - -namespace { - constexpr const auto roleSuffix = "Role"; - - void emptyMessageHandler(QtMsgType type, const QMessageLogContext& context, - const QString& msg) - { - Q_UNUSED(type) - Q_UNUSED(context) - Q_UNUSED(msg) - } -} - -SubmodelProxyModel::SubmodelProxyModel(QObject* parent) - : QIdentityProxyModel{parent} -{ -} - -QVariant SubmodelProxyModel::data(const QModelIndex& index, int role) const -{ - if (!checkIndex(index, CheckIndexOption::IndexIsValid)) - return {}; - - if (m_delegateModel && m_submodelRole && role == m_submodelRole) { - auto submodel = QIdentityProxyModel::data(index, role); - - QObject* submodelObj = submodel.value(); - - if (submodelObj == nullptr) { - qWarning("Submodel must be a QObject-based type!"); - return submodel; - } - - QVariant proxyVariant; - - auto& entry = m_container[index.row()]; - - if (entry) { - proxyVariant = QVariant::fromValue(entry.get()); - } - - if (proxyVariant.isValid()) - return proxyVariant; - - auto creationContext = m_delegateModel->creationContext(); - auto parentContext = creationContext - ? creationContext : m_delegateModel->engine()->rootContext(); - - auto context = new QQmlContext(parentContext, submodelObj); - - // Make sure that wrapper is destroyed before it receives signal related - // to submodel's destruction. Otherwise injected context property may - // be cleared causing warnings related to accessing null from qml. - connect(submodelObj, &QObject::destroyed, this, - [context = QPointer(context)](auto obj) { - delete context.data(); - }); - - context->setContextProperty(QStringLiteral("submodel"), submodel); - - QObject* instance = m_delegateModel->create(context); - QVariant wrappedInstance = QVariant::fromValue(instance); - - if (m_additionalRolesMap.size()) { - QObject* connector = m_connector->createWithInitialProperties( - { { "target", QVariant::fromValue(instance) } }); - connector->setParent(instance); - - connect(connector, SIGNAL(customRoleChanged(QObject*,int)), - this, SLOT(onCustomRoleChanged(QObject*,int))); - } - - m_container[index.row()].reset(instance); - return wrappedInstance; - } - - if (m_submodelRole && role >= m_additionalRolesOffset - && role < m_additionalRolesOffset + m_additionalRolesMap.size()) - { - auto submodel = data(index, *m_submodelRole); - - auto submodelObj = submodel.value(); - - if (submodelObj == nullptr) { - qWarning("Submodel must be a QObject-based type!"); - return {}; - } - - return submodelObj->property(m_roleNames[role] + roleSuffix); - } - - return QIdentityProxyModel::data(index, role); -} - -void SubmodelProxyModel::setSourceModel(QAbstractItemModel* model) -{ - if (sourceModel() != nullptr || m_sourceModelDeleted) { - qWarning("Changing source model is not supported!"); - return; - } - - if (sourceModel() == model) - return; - - m_container.setModel(model); - - // Workaround for QTBUG-57971 - if (model->roleNames().isEmpty()) - connect(model, &QAbstractItemModel::rowsInserted, - this, &SubmodelProxyModel::initRoles); - - connect(model, &QObject::destroyed, this, [this] { - this->m_sourceModelDeleted = true; - }); - - if (m_delegateModel) - m_additionalRoles = fetchAdditionalRoles(m_delegateModel); - - QIdentityProxyModel::setSourceModel(model); -} - -QHash SubmodelProxyModel::roleNames() const -{ - return m_roleNames.isEmpty() && sourceModel() - ? sourceModel()->roleNames() : m_roleNames;; -} - -QQmlComponent* SubmodelProxyModel::delegateModel() const -{ - return m_delegateModel; -} - -void SubmodelProxyModel::setDelegateModel(QQmlComponent* delegateModel) -{ - if (m_delegateModel != nullptr) { - qWarning("Changing delegate model is not supported!"); - return; - } - - if (m_delegateModel == delegateModel) - return; - - if (sourceModel() != nullptr) { - QStringList additionalRoles = fetchAdditionalRoles(delegateModel); - - if (m_additionalRoles == additionalRoles) { - 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; - } - - emit delegateModelChanged(); -} - -const QString& SubmodelProxyModel::submodelRoleName() const -{ - return m_submodelRoleName; -} - -void SubmodelProxyModel::setSubmodelRoleName(const QString& sumodelRoleName) -{ - if (m_submodelRoleName.isEmpty() && sumodelRoleName.isEmpty()) - return; - - if (!m_submodelRoleName.isEmpty()) { - qWarning("Changing submodel role name is not supported!"); - return; - } - - m_submodelRoleName = sumodelRoleName; - - 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) -{ - if (!m_dataChangedQueued) { - m_dataChangedQueued = true; - QMetaObject::invokeMethod(this, "emitAllDataChanged", Qt::QueuedConnection); - } -} - -void SubmodelProxyModel::emitAllDataChanged() -{ - m_dataChangedQueued = false; - auto count = rowCount(); - - if (count == 0) - return; - - QVector roles(m_additionalRolesMap.cbegin(), - m_additionalRolesMap.cend()); - - emit this->dataChanged(index(0, 0), index(count - 1, 0), roles); -} - -void SubmodelProxyModel::initRoles() -{ - disconnect(sourceModel(), &QAbstractItemModel::rowsInserted, - this, &SubmodelProxyModel::initRoles); - - resetInternalData(); -} - -void SubmodelProxyModel::updateRoleNames() -{ - if (sourceModel() == nullptr) - return; - - auto roles = sourceModel()->roleNames(); - - if (roles.empty()) - return; - - m_submodelRole = findSubmodelRole(roles, m_submodelRoleName); - - const auto keys = roles.keys(); - const auto maxElementIt = std::max_element(keys.begin(), keys.end()); - - Q_ASSERT(maxElementIt != keys.end()); - - auto maxRoleKey = *maxElementIt; - m_additionalRolesOffset = maxRoleKey + 1; - - m_additionalRolesMap.clear(); - - for (auto& additionalRole : qAsConst(m_additionalRoles)) { - auto roleKey = ++maxRoleKey; - - roles.insert(roleKey, additionalRole.toUtf8()); - m_additionalRolesMap.insert(additionalRole, roleKey); - } - - 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 - - Connections { - signal customRoleChanged(source: QtObject, role: int) - )"; - - for (auto i = additionalRoles.cbegin(); i != additionalRoles.cend(); ++i) { - int role = i.value(); - - auto upperCaseRole = i.key(); - upperCaseRole[0] = upperCaseRole[0].toUpper(); - - connectorCode += QString(R"( - function on%1RoleChanged() { customRoleChanged(target, %2) } - )").arg(upperCaseRole).arg(role); - } - - connectorCode += "}"; - - auto connector = new QQmlComponent(engine, parent); - connector->setData(connectorCode.toUtf8(), {}); - - return connector; -} - -std::optional SubmodelProxyModel::findSubmodelRole( - const QHash& roleNames, - const QString& submodelRoleName) -{ - if (roleNames.empty() || submodelRoleName.isEmpty()) - return {}; - - auto submodelKeys = roleNames.keys(m_submodelRoleName.toUtf8()); - auto submodelKeysCount = submodelKeys.size(); - - 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/CMakeLists.txt b/ui/StatusQ/tests/CMakeLists.txt index 2fadd9da63..c9f18f2f94 100644 --- a/ui/StatusQ/tests/CMakeLists.txt +++ b/ui/StatusQ/tests/CMakeLists.txt @@ -64,10 +64,6 @@ add_executable(LeftJoinModelTest tst_LeftJoinModel.cpp) target_link_libraries(LeftJoinModelTest PRIVATE StatusQ StatusQTestLib) add_test(NAME LeftJoinModelTest COMMAND LeftJoinModelTest) -add_executable(SubmodelProxyModelTest tst_SubmodelProxyModel.cpp) -target_link_libraries(SubmodelProxyModelTest PRIVATE StatusQ StatusQTestLib) -add_test(NAME SubmodelProxyModelTest COMMAND SubmodelProxyModelTest) - add_executable(ObjectProxyModelTest tst_ObjectProxyModel.cpp) target_link_libraries(ObjectProxyModelTest PRIVATE StatusQ StatusQTestLib) add_test(NAME ObjectProxyModelTest COMMAND ObjectProxyModelTest) diff --git a/ui/StatusQ/tests/tst_SubmodelProxyModel.cpp b/ui/StatusQ/tests/tst_SubmodelProxyModel.cpp deleted file mode 100644 index 0af4864c14..0000000000 --- a/ui/StatusQ/tests/tst_SubmodelProxyModel.cpp +++ /dev/null @@ -1,719 +0,0 @@ -#include -#include - -#include -#include -#include -#include -#include - -#include -#include - -#include - -#include -#include -#include - -class TestSubmodelProxyModel: public QObject -{ - Q_OBJECT - - int roleForName(const QHash& roles, const QByteArray& name) const - { - auto keys = roles.keys(name); - - if (keys.empty()) - return -1; - - return keys.first(); - } - -private slots: - void basicTest() { - QQmlEngine engine; - QQmlComponent delegate(&engine); - - auto delegateData = R"( - import QtQml 2.15 - QtObject { - property var count: submodel.count - } - )"; - - delegate.setData(delegateData, QUrl()); - - SubmodelProxyModel model; - - auto source = R"([ - { balances: [ { balance: 4 } ], name: "name 1" }, - { balances: [ { balance: 4 }, {balance: 43} ], name: "name 2" }, - { balances: [], name: "name 3" } - ])"; - - ListModelWrapper sourceModel(engine, source); - - QSignalSpy sourceModelChangedSpy( - &model, &SubmodelProxyModel::sourceModelChanged); - QSignalSpy delegateChangedSpy( - &model, &SubmodelProxyModel::delegateModelChanged); - QSignalSpy submodelRoleNameChangedSpy( - &model, &SubmodelProxyModel::submodelRoleNameChanged); - - model.setSourceModel(sourceModel); - model.setDelegateModel(&delegate); - model.setSubmodelRoleName(QStringLiteral("balances")); - - QCOMPARE(sourceModelChangedSpy.count(), 1); - QCOMPARE(delegateChangedSpy.count(), 1); - QCOMPARE(submodelRoleNameChangedSpy.count(), 1); - - QCOMPARE(model.sourceModel(), sourceModel); - QCOMPARE(model.delegateModel(), &delegate); - QCOMPARE(model.submodelRoleName(), QStringLiteral("balances")); - - QCOMPARE(model.rowCount(), 3); - - QCOMPARE(model.data(model.index(0, 0), sourceModel.role("name")), "name 1"); - QVERIFY(model.data(model.index(0, 0), - sourceModel.role("balances")).isValid()); - - auto object = model.data(model.index(0, 0), - sourceModel.role("balances")).value(); - QVERIFY(object); - - auto context = QQmlEngine::contextForObject(object); - - QVERIFY(context->contextProperty("submodel").value() != nullptr); - QCOMPARE(object->property("count"), 1); - QCOMPARE(QQmlEngine::objectOwnership(object), - QQmlEngine::CppOwnership); - } - - void submodelTypeTest() { - QQmlEngine engine; - QQmlComponent delegate(&engine); - - auto delegateData = R"( - import QtQml 2.15 - QtObject { - property var count: submodel.count - } - )"; - - delegate.setData(delegateData, QUrl()); - - SubmodelProxyModel model; - - auto source = R"([ - { balances: [ { balance: 4 } ], name: "name 1" } - ])"; - - ListModelWrapper sourceModel(engine, source); - model.setSourceModel(sourceModel); - model.setDelegateModel(&delegate); - model.setSubmodelRoleName(QStringLiteral("balances")); - - QCOMPARE(model.rowCount(), 1); - - QVariant balances1 = model.data(model.index(0, 0), - sourceModel.role("balances")); - QVERIFY(balances1.isValid()); - - QVariant balances2 = model.data(model.index(0, 0), - sourceModel.role("balances")); - QVERIFY(balances2.isValid()); - - // SubmodelProxyModel may create proxy objects on demand, then first - // call to data(...) returns freshly created object, the next calls - // related to the same row should return cached object. It's important - // to have QVariant type identical in both cases. E.g. returning raw - // pointer in first call and pointer wrapped into QPointer in the next - // one leads to problems in UI components in some scenarios even if - // those QVariant types are automatically convertible. - QCOMPARE(balances2.type(), balances1.type()); - - // Check if the same instance is returned. - QCOMPARE(balances2.value(), balances1.value()); - } - - void usingNonObjectSubmodelRoleTest() { - QQmlEngine engine; - QQmlComponent delegate(&engine); - - auto delegateData = R"( - import QtQml 2.15 - QtObject { - property var count: submodel.count - } - )"; - - delegate.setData(delegateData, QUrl()); - - SubmodelProxyModel model; - - auto source = R"([ - { balances: 1, name: "name 1" }, - { balances: 2, name: "name 2" }, - { balances: 3, name: "name 3" } - ])"; - - ListModelWrapper sourceModel(engine, source); - - QTest::ignoreMessage(QtWarningMsg, - "Submodel must be a QObject-based type!"); - - model.setSourceModel(sourceModel); - model.setDelegateModel(&delegate); - model.setSubmodelRoleName(QStringLiteral("balances")); - - QCOMPARE(model.rowCount(), 3); - - QVERIFY(model.data(model.index(0, 0), - sourceModel.role("balances")).isValid()); - } - - void deletingDelegateTest() { - QQmlEngine engine; - auto delegate = std::make_unique(&engine); - - delegate->setData(QByteArrayLiteral(R"( - import QtQml 2.15 - - QtObject { - property var sub: submodel - } - )"), QUrl()); - - SubmodelProxyModel model; - ListModelWrapper sourceModel(engine, QJsonArray { - QJsonObject {{ "balances", 11 }, { "name", "name 1" }}, - QJsonObject {{ "balances", 12 }, { "name", "name 2" }}, - QJsonObject {{ "balances", 123}, { "name", "name 3" }} - }); - - model.setSourceModel(sourceModel); - model.setDelegateModel(delegate.get()); - model.setSubmodelRoleName(QStringLiteral("balances")); - - QSignalSpy delegateModelChangedSpy( - &model, &SubmodelProxyModel::delegateModelChanged); - QSignalSpy dataChangedSpy( - &model, &SubmodelProxyModel::dataChanged); - - delegate.reset(); - - QCOMPARE(delegateModelChangedSpy.count(), 0); - QCOMPARE(dataChangedSpy.count(), 0); - - QCOMPARE(model.rowCount(), 3); - QCOMPARE(model.data(model.index(0, 0), - sourceModel.role("balances")), 11); - } - - void deletingSourceModelTest() { - QQmlEngine engine; - QQmlComponent delegate(&engine); - - delegate.setData(QByteArrayLiteral(R"( - import QtQml 2.15 - - QtObject { - property var sub: submodel - } - )"), QUrl()); - - SubmodelProxyModel model; - - auto sourceModel = std::make_unique(engine, - QJsonArray { - QJsonObject {{ "balances", 11 }, { "name", "name 1" }}, - QJsonObject {{ "balances", 12 }, { "name", "name 2" }}, - QJsonObject {{ "balances", 123}, { "name", "name 3" }} - } - ); - - model.setSourceModel(sourceModel->model()); - model.setDelegateModel(&delegate); - model.setSubmodelRoleName(QStringLiteral("balances")); - - sourceModel.reset(); - - QCOMPARE(model.rowCount(), 0); - - QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*")); - QCOMPARE(model.data(model.index(0, 0), 0), {}); - } - - void settingUndefinedSubmodelRoleNameTest() { - QQmlEngine engine; - auto delegate = std::make_unique(&engine); - - delegate->setData(QByteArrayLiteral(R"( - import QtQml 2.15 - - QtObject { - property var sub: submodel - } - )"), QUrl()); - - SubmodelProxyModel model; - ListModelWrapper sourceModel(engine, QJsonArray { - QJsonObject {{ "balances", 11 }, { "name", "name 1" }}, - QJsonObject {{ "balances", 12 }, { "name", "name 2" }}, - QJsonObject {{ "balances", 123}, { "name", "name 3" }} - }); - - model.setSourceModel(sourceModel); - model.setDelegateModel(delegate.get()); - - QTest::ignoreMessage(QtWarningMsg, "Submodel role not found!"); - - model.setSubmodelRoleName(QStringLiteral("undefined")); - - QCOMPARE(model.rowCount(), 3); - } - - void addingNewRoleToTopLevelModelTest() { - QQmlEngine engine; - auto delegate = std::make_unique(&engine); - - delegate->setData(QByteArrayLiteral(R"( - import QtQml.Models 2.15 - - ListModel { - id: delegateRoot - - property var sub: submodel - - property int extraValue: submodel.rowCount() - readonly property alias extraValueRole: delegateRoot.extraValue - } - )"), QUrl()); - - SubmodelProxyModel model; - - ListModelWrapper sourceModel(engine, R"([ - { "balances": [], "name": "name 1" }, - { "balances": [ { balance: 1 } ], "name": "name 2" }, - { "balances": [], "name": "name 3" } - ])"); - - model.setSourceModel(sourceModel); - model.setDelegateModel(delegate.get()); - - model.setSubmodelRoleName(QStringLiteral("balances")); - - ListModelWrapper expected(engine, R"([ - { "balances": [], "name": "name 1", "extraValue": 0 }, - { "balances": [], "name": "name 2", "extraValue": 1 }, - { "balances": [], "name": "name 3", "extraValue": 0 } - ])"); - - QCOMPARE(model.rowCount(), 3); - - auto roles = model.roleNames(); - QCOMPARE(roles.size(), 3); - QVERIFY(isSame(&model, expected)); - - ModelSignalsSpy signalsSpy(&model); - - QVariant wrapperVariant = model.data(model.index(0, 0), - roleForName(roles, "balances")); - QObject* wrapper = wrapperVariant.value(); - QVERIFY(wrapper != nullptr); - wrapper->setProperty("extraValue", 42); - - ListModelWrapper expected2(engine, R"([ - { "balances": [], "name": "name 1", "extraValue": 42 }, - { "balances": [], "name": "name 2", "extraValue": 1 }, - { "balances": [], "name": "name 3", "extraValue": 0 } - ])"); - - // dataChanged signal emission is scheduled to event loop, not called - // immediately - QCOMPARE(signalsSpy.count(), 0); - - QVERIFY(QTest::qWaitFor([&signalsSpy]() { - return signalsSpy.count() == 1; - })); - - QCOMPARE(signalsSpy.count(), 1); - QCOMPARE(signalsSpy.dataChangedSpy.count(), 1); - - QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(0), model.index(0, 0)); - QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(1), - model.index(model.rowCount() - 1, 0)); - - QVector expectedChangedRoles = { roleForName(roles, "extraValue") }; - QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(2).value>(), - expectedChangedRoles); - - QVERIFY(isSame(&model, expected2)); - } - - void additionalRoleDataChangedWhenEmptyTest() { - 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"([ - { "balances": [], "name": "name 1" } - ])"); - - SubmodelProxyModel model; - model.setSourceModel(sourceModel); - model.setDelegateModel(delegate.get()); - model.setSubmodelRoleName(QStringLiteral("balances")); - - QCOMPARE(model.rowCount(), 1); - - auto roles = model.roleNames(); - QCOMPARE(roles.size(), 3); - - ModelSignalsSpy signalsSpy(&model); - - QVariant wrapperVariant = model.data(model.index(0, 0), - roleForName(roles, "balances")); - QObject* wrapper = wrapperVariant.value(); - QVERIFY(wrapper != nullptr); - - // dataChanged signal emission is scheduled to event loop, not called - // immediately. In the meantime the source may be cleared and then no - // dataChanged event should be emited. - wrapper->setProperty("extraValueRole", 42); - - sourceModel.remove(0); - - QCOMPARE(signalsSpy.count(), 2); - QCOMPARE(signalsSpy.rowsAboutToBeRemovedSpy.count(), 1); - QCOMPARE(signalsSpy.rowsRemovedSpy.count(), 1); - - QTest::qWait(100); - 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() { - QQmlEngine engine; - auto delegate1 = std::make_unique(&engine); - - delegate1->setData(QByteArrayLiteral(R"( - import QtQml.Models 2.15 - - ListModel { - readonly property int myProp: 42 - } - )"), QUrl()); - - auto delegate2 = std::make_unique(&engine); - - delegate2->setData(QByteArrayLiteral(R"( - import QtQml.Models 2.15 - - ListModel { - readonly property int myProp: 11 - } - )"), QUrl()); - - ListModelWrapper sourceModel(engine, R"([ - { "balances": [], "name": "name 1" }, - { "balances": [], "name": "name 2" }, - { "balances": [], "name": "name 3" } - ])"); - - SubmodelProxyModel model1; - model1.setSourceModel(sourceModel); - model1.setDelegateModel(delegate1.get()); - model1.setSubmodelRoleName(QStringLiteral("balances")); - - SubmodelProxyModel model2; - model2.setSourceModel(sourceModel); - model2.setDelegateModel(delegate2.get()); - model2.setSubmodelRoleName(QStringLiteral("balances")); - - auto roles = model1.roleNames(); - QCOMPARE(roles.size(), 2); - - QVariant wrapperVariant1 = model1.data(model1.index(0, 0), - roleForName(roles, "balances")); - QObject* wrapper1 = wrapperVariant1.value(); - QCOMPARE(wrapper1->property("myProp"), 42); - - QVariant wrapperVariant2 = model2.data(model2.index(0, 0), - roleForName(roles, "balances")); - QObject* wrapper2 = wrapperVariant2.value(); - QCOMPARE(wrapper2->property("myProp"), 11); - } -}; - -QTEST_MAIN(TestSubmodelProxyModel) -#include "tst_SubmodelProxyModel.moc"