chore(ObjectProxyModel): Generalized version of SubmodelProxyModel
Closes: #14893
This commit is contained in:
parent
2868d22a3a
commit
958dc7c5ba
|
@ -0,0 +1,343 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import StatusQ 0.1
|
||||||
|
import StatusQ.Core.Utils 0.1
|
||||||
|
|
||||||
|
import SortFilterProxyModel 0.2
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string intro:
|
||||||
|
"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 ObjectProxyModel wrapping the tokens model joins the submodels"
|
||||||
|
+ " to the network model. It also provides filtering and sorting via"
|
||||||
|
+ " SFPM (slider and checkbox below). Additionally, ObjectProxyModel"
|
||||||
|
+ " 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. All roles declared
|
||||||
|
// as "expectedRoles" are accessible via "model" context property.
|
||||||
|
ObjectProxyModel {
|
||||||
|
id: objectProxyModel
|
||||||
|
|
||||||
|
sourceModel: tokensModel
|
||||||
|
|
||||||
|
delegate: SortFilterProxyModel {
|
||||||
|
id: delegateRoot
|
||||||
|
|
||||||
|
// properties exposed as roles to the top-level model
|
||||||
|
readonly property var balancesCount: model.balances.count
|
||||||
|
readonly property int sum: aggregator.value
|
||||||
|
readonly property SortFilterProxyModel balances: this
|
||||||
|
|
||||||
|
sourceModel: joinModel
|
||||||
|
|
||||||
|
filters: FastExpressionFilter {
|
||||||
|
expression: balance >= thresholdSlider.value
|
||||||
|
|
||||||
|
expectedRoles: "balance"
|
||||||
|
}
|
||||||
|
|
||||||
|
sorters: RoleSorter {
|
||||||
|
roleName: "name"
|
||||||
|
enabled: sortCheckBox.checked
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property LeftJoinModel joinModel: LeftJoinModel {
|
||||||
|
leftModel: model.balances
|
||||||
|
rightModel: networksModel
|
||||||
|
|
||||||
|
joinRole: "chainId"
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly property SumAggregator aggregator: SumAggregator {
|
||||||
|
id: aggregator
|
||||||
|
|
||||||
|
model: delegateRoot
|
||||||
|
roleName: "balance"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
exposedRoles: ["balances", "balancesCount", "sum"]
|
||||||
|
expectedRoles: ["balances"]
|
||||||
|
}
|
||||||
|
|
||||||
|
SortFilterProxyModel {
|
||||||
|
id: sortBySumProxy
|
||||||
|
|
||||||
|
sourceModel: objectProxyModel
|
||||||
|
|
||||||
|
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
|
||||||
|
// ObjectProxyModel.
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,88 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
import StatusQ 0.1
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
readonly property string intro:
|
||||||
|
"This example show how to use ObjectProxyModel in order to overwrite"
|
||||||
|
+ " the existing role with a value computed from the original role and"
|
||||||
|
+ " add new role for controlling selection (writable role)."
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
id: srcModel
|
||||||
|
|
||||||
|
ListElement {
|
||||||
|
uid: 1
|
||||||
|
name: "ETH"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
uid: 2
|
||||||
|
name: "SNT"
|
||||||
|
}
|
||||||
|
ListElement {
|
||||||
|
uid: 3
|
||||||
|
name: "DAI"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectProxyModel {
|
||||||
|
id: objectProxy
|
||||||
|
|
||||||
|
delegate: QtObject {
|
||||||
|
readonly property string name: "#" + model.name
|
||||||
|
property bool selected: true
|
||||||
|
}
|
||||||
|
|
||||||
|
expectedRoles: ["name", "uid"]
|
||||||
|
exposedRoles: ["name", "selected"]
|
||||||
|
|
||||||
|
sourceModel: srcModel
|
||||||
|
}
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 10
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
text: root.intro
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuSeparator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
|
Button {
|
||||||
|
text: "Select all"
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
const count = objectProxy.rowCount()
|
||||||
|
|
||||||
|
for (let i = 0; i < count; i++)
|
||||||
|
objectProxy.proxyObject(i).selected = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ListView {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
model: objectProxy
|
||||||
|
|
||||||
|
delegate: CheckBox {
|
||||||
|
text: model.name
|
||||||
|
checked: model.selected
|
||||||
|
|
||||||
|
onToggled: {
|
||||||
|
objectProxy.proxyObject(model.index).selected = checked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,39 @@
|
||||||
|
import QtQuick 2.15
|
||||||
|
import QtQuick.Controls 2.15
|
||||||
|
import QtQuick.Layouts 1.15
|
||||||
|
|
||||||
|
ColumnLayout {
|
||||||
|
|
||||||
|
TabBar {
|
||||||
|
id: tabBar
|
||||||
|
|
||||||
|
TabButton {
|
||||||
|
text: "Example 1"
|
||||||
|
}
|
||||||
|
|
||||||
|
TabButton {
|
||||||
|
text: "Example 2"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
StackLayout {
|
||||||
|
id: stackLayout
|
||||||
|
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
|
||||||
|
currentIndex: tabBar.currentIndex
|
||||||
|
|
||||||
|
ObjectProxyModelExample1 {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
|
||||||
|
ObjectProxyModelExample2 {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
Layout.fillHeight: true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// category: Models
|
|
@ -105,6 +105,7 @@ add_library(StatusQ SHARED
|
||||||
include/StatusQ/modelsyncedcontainer.h
|
include/StatusQ/modelsyncedcontainer.h
|
||||||
include/StatusQ/modelutilsinternal.h
|
include/StatusQ/modelutilsinternal.h
|
||||||
include/StatusQ/movablemodel.h
|
include/StatusQ/movablemodel.h
|
||||||
|
include/StatusQ/objectproxymodel.h
|
||||||
include/StatusQ/permissionutilsinternal.h
|
include/StatusQ/permissionutilsinternal.h
|
||||||
include/StatusQ/rolesrenamingmodel.h
|
include/StatusQ/rolesrenamingmodel.h
|
||||||
include/StatusQ/rxvalidator.h
|
include/StatusQ/rxvalidator.h
|
||||||
|
@ -130,6 +131,7 @@ add_library(StatusQ SHARED
|
||||||
src/modelentry.cpp
|
src/modelentry.cpp
|
||||||
src/modelutilsinternal.cpp
|
src/modelutilsinternal.cpp
|
||||||
src/movablemodel.cpp
|
src/movablemodel.cpp
|
||||||
|
src/objectproxymodel.cpp
|
||||||
src/permissionutilsinternal.cpp
|
src/permissionutilsinternal.cpp
|
||||||
src/plugin.cpp
|
src/plugin.cpp
|
||||||
src/rolesrenamingmodel.cpp
|
src/rolesrenamingmodel.cpp
|
||||||
|
|
|
@ -0,0 +1,85 @@
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QIdentityProxyModel>
|
||||||
|
#include <QPointer>
|
||||||
|
#include <QQmlContext>
|
||||||
|
#include <QQmlPropertyMap>
|
||||||
|
#include <QSet>
|
||||||
|
|
||||||
|
#include "modelsyncedcontainer.h"
|
||||||
|
|
||||||
|
class QQmlComponent;
|
||||||
|
class QQmlEngine;
|
||||||
|
|
||||||
|
class ObjectProxyModel : public QIdentityProxyModel
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
Q_PROPERTY(QQmlComponent* delegate READ delegate
|
||||||
|
WRITE setDelegate NOTIFY delegateChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(QStringList expectedRoles READ expectedRoles
|
||||||
|
WRITE setExpectedRoles NOTIFY expectedRolesChanged)
|
||||||
|
|
||||||
|
Q_PROPERTY(QStringList exposedRoles READ exposedRoles
|
||||||
|
WRITE setExposedRoles NOTIFY exposedRolesChanged)
|
||||||
|
|
||||||
|
public:
|
||||||
|
explicit ObjectProxyModel(QObject* parent = nullptr);
|
||||||
|
|
||||||
|
QVariant data(const QModelIndex& index, int role) const override;
|
||||||
|
void setSourceModel(QAbstractItemModel* sourceModel) override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
|
QQmlComponent* delegate() const;
|
||||||
|
void setDelegate(QQmlComponent* delegate);
|
||||||
|
|
||||||
|
void setExpectedRoles(const QStringList& expectedRoles);
|
||||||
|
const QStringList& expectedRoles() const;
|
||||||
|
|
||||||
|
void setExposedRoles(const QStringList& exposedRoles);
|
||||||
|
const QStringList& exposedRoles() const;
|
||||||
|
|
||||||
|
Q_INVOKABLE QObject* proxyObject(int index);
|
||||||
|
const QObject* proxyObject(int index) const;
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void delegateChanged();
|
||||||
|
void expectedRolesChanged();
|
||||||
|
void exposedRolesChanged();
|
||||||
|
|
||||||
|
protected slots:
|
||||||
|
void resetInternalData();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onCustomRoleChanged();
|
||||||
|
void emitAllDataChanged();
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct Entry {
|
||||||
|
std::unique_ptr<QObject> proxy;
|
||||||
|
QQmlPropertyMap* rowData = nullptr;
|
||||||
|
QQmlContext* context = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
void initRoles();
|
||||||
|
void updateRoleNames();
|
||||||
|
void updateIndexes(int from, int to);
|
||||||
|
|
||||||
|
QHash<int, QByteArray> findExpectedRoles(
|
||||||
|
const QHash<int, QByteArray>& roleNames,
|
||||||
|
const QStringList& expectedRoles);
|
||||||
|
|
||||||
|
QPointer<QQmlComponent> m_delegate;
|
||||||
|
QHash<int, QByteArray> m_expectedRoleNames;
|
||||||
|
|
||||||
|
bool m_dataChangedQueued = false;
|
||||||
|
|
||||||
|
QStringList m_expectedRoles;
|
||||||
|
QStringList m_exposedRoles;
|
||||||
|
|
||||||
|
QHash<int, QByteArray> m_roleNames;
|
||||||
|
QSet<int> m_exposedRolesSet;
|
||||||
|
|
||||||
|
mutable ModelSyncedContainer<Entry> m_container;
|
||||||
|
};
|
|
@ -0,0 +1,332 @@
|
||||||
|
#include "StatusQ/objectproxymodel.h"
|
||||||
|
|
||||||
|
#include <QDebug>
|
||||||
|
#include <QQmlComponent>
|
||||||
|
#include <QQmlContext>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QQmlProperty>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
ObjectProxyModel::ObjectProxyModel(QObject* parent)
|
||||||
|
: QIdentityProxyModel{parent}
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ObjectProxyModel::data(const QModelIndex& index, int role) const
|
||||||
|
{
|
||||||
|
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (m_delegate && m_exposedRolesSet.contains(role)) {
|
||||||
|
const auto proxy = this->proxyObject(index.row());
|
||||||
|
return proxy->property(m_roleNames[role]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return QIdentityProxyModel::data(index, role);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::setSourceModel(QAbstractItemModel* model)
|
||||||
|
{
|
||||||
|
if (sourceModel() == model)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (sourceModel() != nullptr)
|
||||||
|
sourceModel()->disconnect(this);
|
||||||
|
|
||||||
|
m_container.setModel(model);
|
||||||
|
|
||||||
|
if (model == nullptr) {
|
||||||
|
QIdentityProxyModel::setSourceModel(nullptr);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
connect(model, &QAbstractItemModel::rowsMoved, this, [this] {
|
||||||
|
updateIndexes(0, m_container.size() - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(model, &QAbstractItemModel::layoutChanged, this, [this] {
|
||||||
|
updateIndexes(0, m_container.size() - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(model, &QAbstractItemModel::rowsRemoved, this,
|
||||||
|
[this](const QModelIndex& parent, int first, int /*last*/) {
|
||||||
|
auto updateLast = m_container.size() - 1;
|
||||||
|
|
||||||
|
if (first <= updateLast)
|
||||||
|
updateIndexes(first, updateLast);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(model, &QAbstractItemModel::rowsInserted, this,
|
||||||
|
[this](const QModelIndex& parent, int first, int /*last*/) {
|
||||||
|
updateIndexes(first, m_container.size() - 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
connect(model, &QAbstractItemModel::dataChanged, this,
|
||||||
|
[this](const QModelIndex& topLeft, const QModelIndex& bottomRight,
|
||||||
|
const QVector<int>& roles)
|
||||||
|
{
|
||||||
|
auto first = topLeft.row();
|
||||||
|
auto last = bottomRight.row();
|
||||||
|
|
||||||
|
auto model = sourceModel();
|
||||||
|
|
||||||
|
for (auto idx = first; idx <= last; idx++) {
|
||||||
|
auto rowData = m_container[idx].rowData;
|
||||||
|
|
||||||
|
QHashIterator i(m_expectedRoleNames);
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
rowData->insert(i.value(),
|
||||||
|
model->data(model->index(idx, 0), i.key()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Workaround for QTBUG-57971--
|
||||||
|
if (model->roleNames().isEmpty())
|
||||||
|
connect(model, &QAbstractItemModel::rowsInserted,
|
||||||
|
this, &ObjectProxyModel::initRoles);
|
||||||
|
|
||||||
|
QIdentityProxyModel::setSourceModel(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> ObjectProxyModel::roleNames() const
|
||||||
|
{
|
||||||
|
return m_roleNames.isEmpty() && sourceModel()
|
||||||
|
? sourceModel()->roleNames() : m_roleNames;;
|
||||||
|
}
|
||||||
|
|
||||||
|
QQmlComponent* ObjectProxyModel::delegate() const
|
||||||
|
{
|
||||||
|
return m_delegate;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::setDelegate(QQmlComponent* delegate)
|
||||||
|
{
|
||||||
|
if (m_delegate == delegate)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_delegate = delegate;
|
||||||
|
emit delegateChanged();
|
||||||
|
|
||||||
|
emitAllDataChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::setExpectedRoles(const QStringList& expectedRoles)
|
||||||
|
{
|
||||||
|
if (m_expectedRoles == expectedRoles)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool hasSource = sourceModel() != nullptr;
|
||||||
|
|
||||||
|
if (hasSource)
|
||||||
|
beginResetModel();
|
||||||
|
|
||||||
|
m_expectedRoles = expectedRoles;
|
||||||
|
|
||||||
|
if (hasSource)
|
||||||
|
endResetModel();
|
||||||
|
|
||||||
|
emit expectedRolesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList& ObjectProxyModel::expectedRoles() const
|
||||||
|
{
|
||||||
|
return m_expectedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::setExposedRoles(const QStringList& exposedRoles)
|
||||||
|
{
|
||||||
|
if (m_exposedRoles == exposedRoles)
|
||||||
|
return;
|
||||||
|
|
||||||
|
bool hasSource = sourceModel() != nullptr;
|
||||||
|
|
||||||
|
if (hasSource)
|
||||||
|
beginResetModel();
|
||||||
|
|
||||||
|
m_exposedRoles = exposedRoles;
|
||||||
|
|
||||||
|
if (hasSource)
|
||||||
|
endResetModel();
|
||||||
|
|
||||||
|
emit exposedRolesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList& ObjectProxyModel::exposedRoles() const
|
||||||
|
{
|
||||||
|
return m_exposedRoles;
|
||||||
|
}
|
||||||
|
|
||||||
|
QObject* ObjectProxyModel::proxyObject(int index)
|
||||||
|
{
|
||||||
|
if (index >= m_container.size())
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
auto& entry = m_container[index];
|
||||||
|
|
||||||
|
if (entry.proxy)
|
||||||
|
return entry.proxy.get();
|
||||||
|
|
||||||
|
auto creationContext = m_delegate->creationContext();
|
||||||
|
auto parentContext = creationContext
|
||||||
|
? creationContext : m_delegate->engine()->rootContext();
|
||||||
|
|
||||||
|
auto context = new QQmlContext(parentContext/*, submodelObj*/);
|
||||||
|
|
||||||
|
auto rowData = new QQmlPropertyMap(context);
|
||||||
|
auto model = sourceModel();
|
||||||
|
|
||||||
|
QHashIterator i(m_expectedRoleNames);
|
||||||
|
while (i.hasNext()) {
|
||||||
|
i.next();
|
||||||
|
rowData->insert(i.value(), model->data(model->index(index, 0), i.key()));
|
||||||
|
}
|
||||||
|
|
||||||
|
rowData->insert("index", index);
|
||||||
|
context->setContextProperty("model", rowData);
|
||||||
|
|
||||||
|
QObject* instance = m_delegate->create(context);
|
||||||
|
context->setParent(instance);
|
||||||
|
|
||||||
|
for (auto& exposedRole : m_exposedRoles) {
|
||||||
|
QQmlProperty prop(instance, exposedRole, m_delegate->engine());
|
||||||
|
|
||||||
|
prop.connectNotifySignal(const_cast<ObjectProxyModel*>(this),
|
||||||
|
SLOT(onCustomRoleChanged()));
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.proxy.reset(instance);
|
||||||
|
entry.context = context;
|
||||||
|
entry.rowData = rowData;
|
||||||
|
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
const QObject* ObjectProxyModel::proxyObject(int index) const
|
||||||
|
{
|
||||||
|
return const_cast<ObjectProxyModel*>(this)->proxyObject(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::resetInternalData()
|
||||||
|
{
|
||||||
|
QIdentityProxyModel::resetInternalData();
|
||||||
|
updateRoleNames();
|
||||||
|
|
||||||
|
m_dataChangedQueued = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::onCustomRoleChanged()
|
||||||
|
{
|
||||||
|
if (!m_dataChangedQueued) {
|
||||||
|
m_dataChangedQueued = true;
|
||||||
|
QMetaObject::invokeMethod(this, "emitAllDataChanged",
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::emitAllDataChanged()
|
||||||
|
{
|
||||||
|
m_dataChangedQueued = false;
|
||||||
|
auto count = rowCount();
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
QVector<int> roles(m_exposedRolesSet.cbegin(),
|
||||||
|
m_exposedRolesSet.cend());
|
||||||
|
|
||||||
|
if (roles.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
emit this->dataChanged(index(0, 0), index(count - 1, 0), roles);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::initRoles()
|
||||||
|
{
|
||||||
|
disconnect(sourceModel(), &QAbstractItemModel::rowsInserted,
|
||||||
|
this, &ObjectProxyModel::initRoles);
|
||||||
|
|
||||||
|
resetInternalData();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::updateRoleNames()
|
||||||
|
{
|
||||||
|
m_roleNames.clear();
|
||||||
|
|
||||||
|
if (sourceModel() == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
auto roles = sourceModel()->roleNames();
|
||||||
|
|
||||||
|
if (roles.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_expectedRoleNames = findExpectedRoles(roles, m_expectedRoles);
|
||||||
|
|
||||||
|
const auto keys = roles.keys();
|
||||||
|
const auto maxElementIt = std::max_element(keys.begin(), keys.end());
|
||||||
|
|
||||||
|
Q_ASSERT(maxElementIt != keys.end());
|
||||||
|
|
||||||
|
auto maxRoleKey = *maxElementIt;
|
||||||
|
|
||||||
|
for (auto& exposedRole : qAsConst(m_exposedRoles)) {
|
||||||
|
|
||||||
|
auto exposedRoleByteArray = exposedRole.toUtf8();
|
||||||
|
auto keys = roles.keys(exposedRoleByteArray);
|
||||||
|
|
||||||
|
if (!keys.empty()) {
|
||||||
|
auto key = keys.first();
|
||||||
|
|
||||||
|
m_exposedRolesSet.insert(key);
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto newRole = ++maxRoleKey;
|
||||||
|
|
||||||
|
roles.insert(newRole, exposedRoleByteArray);
|
||||||
|
m_exposedRolesSet.insert(newRole);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_roleNames = roles;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ObjectProxyModel::updateIndexes(int from, int to)
|
||||||
|
{
|
||||||
|
for (auto i = from; i <= to; i++) {
|
||||||
|
auto& entry = m_container[i];
|
||||||
|
|
||||||
|
if (entry.proxy)
|
||||||
|
entry.rowData->insert("index", i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> ObjectProxyModel::findExpectedRoles(
|
||||||
|
const QHash<int, QByteArray> &roleNames,
|
||||||
|
const QStringList &expectedRoles)
|
||||||
|
{
|
||||||
|
if (roleNames.empty() || expectedRoles.isEmpty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
QHash<int, QByteArray> expected;
|
||||||
|
|
||||||
|
for (auto& role : expectedRoles) {
|
||||||
|
auto expectedKeys = roleNames.keys(role.toUtf8());
|
||||||
|
auto expectedKeysCount = expectedKeys.size();
|
||||||
|
|
||||||
|
if (expectedKeysCount == 1)
|
||||||
|
expected.insert(expectedKeys.first(), role.toUtf8());
|
||||||
|
else if (expectedKeysCount == 0) {
|
||||||
|
qWarning() << "Expected role not found!";
|
||||||
|
} else {
|
||||||
|
qWarning() << "Malformed source model - multiple roles found for given "
|
||||||
|
"expected role name!";
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return expected;
|
||||||
|
}
|
|
@ -13,6 +13,7 @@
|
||||||
#include "StatusQ/leftjoinmodel.h"
|
#include "StatusQ/leftjoinmodel.h"
|
||||||
#include "StatusQ/modelutilsinternal.h"
|
#include "StatusQ/modelutilsinternal.h"
|
||||||
#include "StatusQ/movablemodel.h"
|
#include "StatusQ/movablemodel.h"
|
||||||
|
#include "StatusQ/objectproxymodel.h"
|
||||||
#include "StatusQ/permissionutilsinternal.h"
|
#include "StatusQ/permissionutilsinternal.h"
|
||||||
#include "StatusQ/rolesrenamingmodel.h"
|
#include "StatusQ/rolesrenamingmodel.h"
|
||||||
#include "StatusQ/rxvalidator.h"
|
#include "StatusQ/rxvalidator.h"
|
||||||
|
@ -54,6 +55,7 @@ public:
|
||||||
qmlRegisterType<FastExpressionSorter>("StatusQ", 0, 1, "FastExpressionSorter");
|
qmlRegisterType<FastExpressionSorter>("StatusQ", 0, 1, "FastExpressionSorter");
|
||||||
qmlRegisterType<UndefinedFilter>("StatusQ", 0, 1, "UndefinedFilter");
|
qmlRegisterType<UndefinedFilter>("StatusQ", 0, 1, "UndefinedFilter");
|
||||||
|
|
||||||
|
qmlRegisterType<ObjectProxyModel>("StatusQ", 0, 1, "ObjectProxyModel");
|
||||||
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
|
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
|
||||||
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
|
||||||
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
|
||||||
|
|
|
@ -68,6 +68,10 @@ add_executable(SubmodelProxyModelTest tst_SubmodelProxyModel.cpp)
|
||||||
target_link_libraries(SubmodelProxyModelTest PRIVATE StatusQ StatusQTestLib)
|
target_link_libraries(SubmodelProxyModelTest PRIVATE StatusQ StatusQTestLib)
|
||||||
add_test(NAME SubmodelProxyModelTest COMMAND SubmodelProxyModelTest)
|
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)
|
||||||
|
|
||||||
add_executable(AggregatorTest tst_Aggregator.cpp)
|
add_executable(AggregatorTest tst_Aggregator.cpp)
|
||||||
target_link_libraries(AggregatorTest PRIVATE StatusQ StatusQTestLib)
|
target_link_libraries(AggregatorTest PRIVATE StatusQ StatusQTestLib)
|
||||||
add_test(NAME AggregatorTest COMMAND AggregatorTest)
|
add_test(NAME AggregatorTest COMMAND AggregatorTest)
|
||||||
|
|
|
@ -0,0 +1,684 @@
|
||||||
|
#include <QSignalSpy>
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QQmlComponent>
|
||||||
|
#include <QQmlContext>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
#include <StatusQ/objectproxymodel.h>
|
||||||
|
|
||||||
|
#include <TestHelpers/listmodelwrapper.h>
|
||||||
|
#include <TestHelpers/modelsignalsspy.h>
|
||||||
|
#include <TestHelpers/modeltestutils.h>
|
||||||
|
|
||||||
|
class TestObjectProxyModel: public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
int roleForName(const QHash<int, QByteArray>& 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 {
|
||||||
|
readonly property int count: model.balances.count
|
||||||
|
readonly property QtObject proxyObject: this
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
delegate.setData(delegateData, QUrl());
|
||||||
|
|
||||||
|
ObjectProxyModel 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, &ObjectProxyModel::sourceModelChanged);
|
||||||
|
QSignalSpy delegateChangedSpy(
|
||||||
|
&model, &ObjectProxyModel::delegateChanged);
|
||||||
|
QSignalSpy expectedRolesChangedSpy(
|
||||||
|
&model, &ObjectProxyModel::expectedRolesChanged);
|
||||||
|
QSignalSpy exposedRolesChangedSpy(
|
||||||
|
&model, &ObjectProxyModel::exposedRolesChanged);
|
||||||
|
|
||||||
|
model.setSourceModel(sourceModel);
|
||||||
|
model.setDelegate(&delegate);
|
||||||
|
model.setExpectedRoles(QStringList({ QStringLiteral("balances") }));
|
||||||
|
model.setExposedRoles({ QStringLiteral("proxyObject"),
|
||||||
|
QStringLiteral("count")});
|
||||||
|
|
||||||
|
QCOMPARE(sourceModelChangedSpy.count(), 1);
|
||||||
|
QCOMPARE(delegateChangedSpy.count(), 1);
|
||||||
|
QCOMPARE(expectedRolesChangedSpy.count(), 1);
|
||||||
|
|
||||||
|
QCOMPARE(model.sourceModel(), sourceModel);
|
||||||
|
QCOMPARE(model.delegate(), &delegate);
|
||||||
|
QCOMPARE(model.expectedRoles(), QStringList({ QStringLiteral("balances") }));
|
||||||
|
QCOMPARE(model.exposedRoles(), QStringList({ QStringLiteral("proxyObject"),
|
||||||
|
QStringLiteral("count") }));
|
||||||
|
|
||||||
|
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 roles = model.roleNames();
|
||||||
|
|
||||||
|
auto object = model.data(model.index(0, 0),
|
||||||
|
roleForName(roles, "proxyObject")).value<QObject*>();
|
||||||
|
QVERIFY(object);
|
||||||
|
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: model.balances.count
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
delegate.setData(delegateData, QUrl());
|
||||||
|
|
||||||
|
ObjectProxyModel model;
|
||||||
|
|
||||||
|
auto source = R"([
|
||||||
|
{ balances: [ { balance: 4 } ], name: "name 1" }
|
||||||
|
])";
|
||||||
|
|
||||||
|
ListModelWrapper sourceModel(engine, source);
|
||||||
|
model.setSourceModel(sourceModel);
|
||||||
|
model.setDelegate(&delegate);
|
||||||
|
model.setExpectedRoles({ 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());
|
||||||
|
|
||||||
|
// ObjectProxyModel 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<QObject*>(), balances1.value<QObject*>());
|
||||||
|
}
|
||||||
|
|
||||||
|
void signalsDisconnectionTest() {
|
||||||
|
struct Model : public QIdentityProxyModel
|
||||||
|
{
|
||||||
|
using QObject::receivers;
|
||||||
|
};
|
||||||
|
|
||||||
|
Model sourceModel1, sourceModel2;
|
||||||
|
ObjectProxyModel model;
|
||||||
|
|
||||||
|
auto signal = SIGNAL(dataChanged(const QModelIndex&,
|
||||||
|
const QModelIndex&,
|
||||||
|
const QVector<int>));
|
||||||
|
|
||||||
|
model.setSourceModel(&sourceModel1);
|
||||||
|
QVERIFY(sourceModel1.receivers(signal) > 0);
|
||||||
|
|
||||||
|
model.setSourceModel(nullptr);
|
||||||
|
QVERIFY(sourceModel1.receivers(signal) == 0);
|
||||||
|
|
||||||
|
model.setSourceModel(&sourceModel1);
|
||||||
|
QVERIFY(sourceModel1.receivers(signal) > 0);
|
||||||
|
|
||||||
|
model.setSourceModel(&sourceModel2);
|
||||||
|
QVERIFY(sourceModel1.receivers(signal) == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
void deletingDelegateTest() {
|
||||||
|
QQmlEngine engine;
|
||||||
|
auto delegate = std::make_unique<QQmlComponent>(&engine);
|
||||||
|
|
||||||
|
delegate->setData(QByteArrayLiteral(R"(
|
||||||
|
import QtQml 2.15
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property var sub: model.balances
|
||||||
|
}
|
||||||
|
)"), QUrl());
|
||||||
|
|
||||||
|
ObjectProxyModel 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.setDelegate(delegate.get());
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
|
||||||
|
QSignalSpy delegateChangedSpy(&model,
|
||||||
|
&ObjectProxyModel::delegateChanged);
|
||||||
|
QSignalSpy dataChangedSpy(
|
||||||
|
&model, &ObjectProxyModel::dataChanged);
|
||||||
|
|
||||||
|
delegate.reset();
|
||||||
|
|
||||||
|
QCOMPARE(delegateChangedSpy.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: model.balances
|
||||||
|
}
|
||||||
|
)"), QUrl());
|
||||||
|
|
||||||
|
ObjectProxyModel model;
|
||||||
|
|
||||||
|
auto sourceModel = std::make_unique<ListModelWrapper>(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.setDelegate(&delegate);
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
|
||||||
|
sourceModel.reset();
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 0);
|
||||||
|
|
||||||
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*"));
|
||||||
|
QCOMPARE(model.data(model.index(0, 0), 0), {});
|
||||||
|
}
|
||||||
|
|
||||||
|
void settingUndefinedExposedRoleNameTest() {
|
||||||
|
QQmlEngine engine;
|
||||||
|
auto delegate = std::make_unique<QQmlComponent>(&engine);
|
||||||
|
|
||||||
|
delegate->setData(QByteArrayLiteral(R"(
|
||||||
|
import QtQml 2.15
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property var sub: model.balances
|
||||||
|
}
|
||||||
|
)"), QUrl());
|
||||||
|
|
||||||
|
ObjectProxyModel 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.setDelegate(delegate.get());
|
||||||
|
|
||||||
|
QTest::ignoreMessage(QtWarningMsg, "Expected role not found!");
|
||||||
|
|
||||||
|
model.setExpectedRoles({ QStringLiteral("undefined") });
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
void addingNewRoleToTopLevelModelTest() {
|
||||||
|
QQmlEngine engine;
|
||||||
|
auto delegate = std::make_unique<QQmlComponent>(&engine);
|
||||||
|
|
||||||
|
delegate->setData(QByteArrayLiteral(R"(
|
||||||
|
import QtQml.Models 2.15
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
property int extraValue: model.balances.count
|
||||||
|
}
|
||||||
|
)"), QUrl());
|
||||||
|
|
||||||
|
ObjectProxyModel model;
|
||||||
|
|
||||||
|
ListModelWrapper sourceModel(engine, R"([
|
||||||
|
{ "balances": [], "name": "name 1" },
|
||||||
|
{ "balances": [ { balance: 1 } ], "name": "name 2" },
|
||||||
|
{ "balances": [], "name": "name 3" }
|
||||||
|
])");
|
||||||
|
|
||||||
|
model.setSourceModel(sourceModel);
|
||||||
|
model.setDelegate(delegate.get());
|
||||||
|
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
model.setExposedRoles({ QStringLiteral("extraValue") });
|
||||||
|
|
||||||
|
ListModelWrapper expected(engine, R"([
|
||||||
|
{ "balances": [], "name": "name 1", "extraValue": 0 },
|
||||||
|
{ "balances": [{ balance: 1 }], "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);
|
||||||
|
|
||||||
|
|
||||||
|
QObject* proxy = model.proxyObject(0);
|
||||||
|
proxy->setProperty("extraValue", 42);
|
||||||
|
|
||||||
|
ListModelWrapper expected2(engine, R"([
|
||||||
|
{ "balances": [], "name": "name 1", "extraValue": 42 },
|
||||||
|
{ "balances": [{ balance: 1 }], "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<int> expectedChangedRoles = { roleForName(roles, "extraValue") };
|
||||||
|
QCOMPARE(signalsSpy.dataChangedSpy.at(0).at(2).value<QVector<int>>(),
|
||||||
|
expectedChangedRoles);
|
||||||
|
|
||||||
|
QVERIFY(isSame(&model, expected2));
|
||||||
|
}
|
||||||
|
|
||||||
|
void additionalRoleDataChangedWhenEmptyTest() {
|
||||||
|
QQmlEngine engine;
|
||||||
|
auto delegate = std::make_unique<QQmlComponent>(&engine);
|
||||||
|
|
||||||
|
delegate->setData(QByteArrayLiteral(R"(
|
||||||
|
import QtQml.Models 2.15
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
property int extraValue: 0
|
||||||
|
}
|
||||||
|
)"), QUrl());
|
||||||
|
|
||||||
|
ListModelWrapper sourceModel(engine, R"([
|
||||||
|
{ "balances": [], "name": "name 1" }
|
||||||
|
])");
|
||||||
|
|
||||||
|
ObjectProxyModel model;
|
||||||
|
model.setSourceModel(sourceModel);
|
||||||
|
model.setDelegate(delegate.get());
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
model.setExposedRoles({ QStringLiteral("extraValue") });
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 1);
|
||||||
|
|
||||||
|
auto roles = model.roleNames();
|
||||||
|
QCOMPARE(roles.size(), 3);
|
||||||
|
|
||||||
|
ModelSignalsSpy signalsSpy(&model);
|
||||||
|
|
||||||
|
QObject* proxy = model.proxyObject(0);
|
||||||
|
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
proxy->setProperty("extraValue", 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<QQmlComponent>(&engine);
|
||||||
|
|
||||||
|
delegateWithRole->setData(QByteArrayLiteral(R"(
|
||||||
|
import QtQml.Models 2.15
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
property int extraValue: 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 expected role names
|
||||||
|
// 4. set exposed role names
|
||||||
|
{
|
||||||
|
ObjectProxyModel 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.setDelegate(delegateWithRole.get());
|
||||||
|
|
||||||
|
QCOMPARE(signalsSpy.count(), 2);
|
||||||
|
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
|
||||||
|
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
|
||||||
|
QCOMPARE(model.roleNames().count(), 2);
|
||||||
|
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
|
||||||
|
QCOMPARE(signalsSpy.count(), 4);
|
||||||
|
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
|
||||||
|
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
|
||||||
|
QCOMPARE(model.roleNames().count(), 2);
|
||||||
|
|
||||||
|
|
||||||
|
model.setExposedRoles({ QStringLiteral("extraValue") });
|
||||||
|
|
||||||
|
QCOMPARE(signalsSpy.count(), 6);
|
||||||
|
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 3);
|
||||||
|
QCOMPARE(signalsSpy.modelResetSpy.count(), 3);
|
||||||
|
QCOMPARE(model.roleNames().count(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. set delegate model
|
||||||
|
// 2. set source
|
||||||
|
// 3. set submodel role name
|
||||||
|
// 4. set exposed role names
|
||||||
|
{
|
||||||
|
ObjectProxyModel model;
|
||||||
|
|
||||||
|
ModelSignalsSpy signalsSpy(&model);
|
||||||
|
|
||||||
|
model.setDelegate(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(), 2);
|
||||||
|
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
|
||||||
|
QCOMPARE(signalsSpy.count(), 4);
|
||||||
|
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
|
||||||
|
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
|
||||||
|
QCOMPARE(model.roleNames().count(), 2);
|
||||||
|
|
||||||
|
model.setExposedRoles({ QStringLiteral("extraValue") });
|
||||||
|
|
||||||
|
QCOMPARE(signalsSpy.count(), 6);
|
||||||
|
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 3);
|
||||||
|
QCOMPARE(signalsSpy.modelResetSpy.count(), 3);
|
||||||
|
QCOMPARE(model.roleNames().count(), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1. set submodel role name
|
||||||
|
// 1. set expected role name
|
||||||
|
// 2. set delegate model
|
||||||
|
// 3. set source
|
||||||
|
{
|
||||||
|
ObjectProxyModel model;
|
||||||
|
|
||||||
|
ModelSignalsSpy signalsSpy(&model);
|
||||||
|
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
model.setExposedRoles({ QStringLiteral("extraValue") });
|
||||||
|
|
||||||
|
model.setDelegate(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
|
||||||
|
{
|
||||||
|
ObjectProxyModel 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.setDelegate(delegateNoRole.get());
|
||||||
|
|
||||||
|
QCOMPARE(signalsSpy.count(), 2);
|
||||||
|
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 1);
|
||||||
|
QCOMPARE(signalsSpy.modelResetSpy.count(), 1);
|
||||||
|
QCOMPARE(model.roleNames().count(), 2);
|
||||||
|
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
|
||||||
|
QCOMPARE(signalsSpy.count(), 4);
|
||||||
|
QCOMPARE(signalsSpy.modelAboutToBeResetSpy.count(), 2);
|
||||||
|
QCOMPARE(signalsSpy.modelResetSpy.count(), 2);
|
||||||
|
QCOMPARE(model.roleNames().count(), 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
ObjectProxyModel model;
|
||||||
|
model.setSourceModel(&identity);
|
||||||
|
model.setDelegate(delegate.get());
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
model.setExposedRoles({ QStringLiteral("extraValue") });
|
||||||
|
|
||||||
|
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 extraValue: 0
|
||||||
|
}
|
||||||
|
)"), QUrl());
|
||||||
|
|
||||||
|
ListModelWrapper sourceModel(engine, R"([])");
|
||||||
|
|
||||||
|
ObjectProxyModel model;
|
||||||
|
model.setSourceModel(sourceModel);
|
||||||
|
model.setDelegate(delegate.get());
|
||||||
|
model.setExpectedRoles({ QStringLiteral("balances") });
|
||||||
|
model.setExposedRoles({ QStringLiteral("extraValue") });
|
||||||
|
|
||||||
|
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 changingSourceModelTest() {
|
||||||
|
QQmlEngine engine;
|
||||||
|
QQmlComponent delegate(&engine);
|
||||||
|
|
||||||
|
auto delegateData = R"(
|
||||||
|
import QtQml 2.15
|
||||||
|
QtObject {
|
||||||
|
readonly property int count: model.balances.count
|
||||||
|
}
|
||||||
|
)";
|
||||||
|
|
||||||
|
delegate.setData(delegateData, QUrl());
|
||||||
|
|
||||||
|
ObjectProxyModel model;
|
||||||
|
|
||||||
|
auto source1 = R"([
|
||||||
|
{ balances: [ { balance: 4 } ], name: "name 1" },
|
||||||
|
{ balances: [ { balance: 4 }, {balance: 43} ], name: "name 2" }
|
||||||
|
])";
|
||||||
|
|
||||||
|
auto source2 = R"([
|
||||||
|
{ id: 4, balances: [ { balance: 4 } ], name: "name 1", color: "red" }
|
||||||
|
])";
|
||||||
|
|
||||||
|
ListModelWrapper sourceModel1(engine, source1);
|
||||||
|
ListModelWrapper sourceModel2(engine, source2);
|
||||||
|
|
||||||
|
model.setSourceModel(sourceModel1);
|
||||||
|
model.setDelegate(&delegate);
|
||||||
|
model.setExpectedRoles(QStringList({ QStringLiteral("balances") }));
|
||||||
|
model.setExposedRoles({ QStringLiteral("count")});
|
||||||
|
|
||||||
|
QCOMPARE(model.rowCount(), 2);
|
||||||
|
QCOMPARE(model.roleNames().size(), 3);
|
||||||
|
|
||||||
|
QSignalSpy sourceModelChangedSpy(
|
||||||
|
&model, &ObjectProxyModel::sourceModelChanged);
|
||||||
|
|
||||||
|
model.setSourceModel(sourceModel2);
|
||||||
|
|
||||||
|
QCOMPARE(sourceModelChangedSpy.count(), 1);
|
||||||
|
QCOMPARE(model.rowCount(), 1);
|
||||||
|
QCOMPARE(model.roleNames().size(), 5);
|
||||||
|
|
||||||
|
model.setSourceModel(nullptr);
|
||||||
|
|
||||||
|
QCOMPARE(sourceModelChangedSpy.count(), 2);
|
||||||
|
QCOMPARE(model.rowCount(), 0);
|
||||||
|
QCOMPARE(model.roleNames().size(), 0);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
QTEST_MAIN(TestObjectProxyModel)
|
||||||
|
#include "tst_ObjectProxyModel.moc"
|
Loading…
Reference in New Issue