feat(StatusQ/SubmodelProxyModel): Exposing roles computed per submodel to the top-level model
Closes: #14390
This commit is contained in:
parent
ceee230244
commit
27c53f4154
|
@ -7,9 +7,24 @@ import Storybook 1.0
|
||||||
|
|
||||||
import SortFilterProxyModel 0.2
|
import SortFilterProxyModel 0.2
|
||||||
|
|
||||||
|
import StatusQ.Core.Utils 0.1
|
||||||
|
|
||||||
Item {
|
Item {
|
||||||
id: root
|
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 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 int numberOfTokens: 2000
|
||||||
|
|
||||||
readonly property var colors: [
|
readonly property var colors: [
|
||||||
|
@ -95,6 +110,25 @@ Item {
|
||||||
sourceModel: tokensModel
|
sourceModel: tokensModel
|
||||||
|
|
||||||
delegateModel: SortFilterProxyModel {
|
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 {
|
readonly property LeftJoinModel joinModel: LeftJoinModel {
|
||||||
leftModel: submodel
|
leftModel: submodel
|
||||||
rightModel: networksModel
|
rightModel: networksModel
|
||||||
|
@ -102,102 +136,50 @@ Item {
|
||||||
joinRole: "chainId"
|
joinRole: "chainId"
|
||||||
}
|
}
|
||||||
|
|
||||||
sourceModel: joinModel
|
readonly property SumAggregator aggregator: SumAggregator {
|
||||||
|
id: aggregator
|
||||||
|
|
||||||
filters: ExpressionFilter {
|
model: delegateRoot
|
||||||
expression: balance >= thresholdSlider.value
|
roleName: "balance"
|
||||||
}
|
|
||||||
|
|
||||||
sorters: RoleSorter {
|
|
||||||
roleName: "name"
|
|
||||||
enabled: sortCheckBox.checked
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
submodelRoleName: "balances"
|
submodelRoleName: "balances"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SortFilterProxyModel {
|
||||||
|
id: sortBySumProxy
|
||||||
|
|
||||||
|
sourceModel: submodelProxyModel
|
||||||
|
|
||||||
|
sorters: RoleSorter {
|
||||||
|
roleName: "sum"
|
||||||
|
ascendingOrder: false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ColumnLayout {
|
ColumnLayout {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 10
|
anchors.margins: 10
|
||||||
|
|
||||||
|
Label {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
wrapMode: Text.Wrap
|
||||||
|
lineHeight: 1.2
|
||||||
|
text: root.intro
|
||||||
|
}
|
||||||
|
|
||||||
|
MenuSeparator {
|
||||||
|
Layout.fillWidth: true
|
||||||
|
}
|
||||||
|
|
||||||
RowLayout {
|
RowLayout {
|
||||||
Layout.fillWidth: true
|
Layout.fillWidth: true
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
// ListView consuming model don't have to do any transformation
|
|
||||||
// of the submodels internally because it's handled externally via
|
|
||||||
// SubmodelProxyModel.
|
|
||||||
ListView {
|
ListView {
|
||||||
id: listView
|
Layout.preferredWidth: 110
|
||||||
|
Layout.leftMargin: 10
|
||||||
Layout.fillWidth: true
|
|
||||||
Layout.fillHeight: true
|
|
||||||
|
|
||||||
reuseItems: true
|
|
||||||
|
|
||||||
ScrollBar.vertical: ScrollBar {}
|
|
||||||
|
|
||||||
clip: true
|
|
||||||
spacing: 18
|
|
||||||
|
|
||||||
model: submodelProxyModel
|
|
||||||
|
|
||||||
delegate: ColumnLayout {
|
|
||||||
id: delegateRoot
|
|
||||||
|
|
||||||
width: ListView.view.width
|
|
||||||
height: 46
|
|
||||||
spacing: 0
|
|
||||||
|
|
||||||
readonly property var balances: model.balances
|
|
||||||
|
|
||||||
Label {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Rectangle {
|
|
||||||
Layout.preferredWidth: 1
|
|
||||||
Layout.fillHeight: true
|
|
||||||
Layout.rightMargin: 20
|
|
||||||
|
|
||||||
color: "lightgray"
|
|
||||||
}
|
|
||||||
|
|
||||||
ListView {
|
|
||||||
Layout.preferredWidth: 150
|
|
||||||
Layout.fillHeight: true
|
Layout.fillHeight: true
|
||||||
|
|
||||||
spacing: 20
|
spacing: 20
|
||||||
|
@ -236,6 +218,98 @@ Item {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 {
|
MenuSeparator {
|
||||||
|
|
|
@ -3,6 +3,8 @@
|
||||||
#include <QIdentityProxyModel>
|
#include <QIdentityProxyModel>
|
||||||
#include <QPointer>
|
#include <QPointer>
|
||||||
|
|
||||||
|
#include <limits>
|
||||||
|
|
||||||
class QQmlComponent;
|
class QQmlComponent;
|
||||||
|
|
||||||
class SubmodelProxyModel : public QIdentityProxyModel
|
class SubmodelProxyModel : public QIdentityProxyModel
|
||||||
|
@ -20,6 +22,7 @@ public:
|
||||||
|
|
||||||
QVariant data(const QModelIndex& index, int role) const override;
|
QVariant data(const QModelIndex& index, int role) const override;
|
||||||
void setSourceModel(QAbstractItemModel* sourceModel) override;
|
void setSourceModel(QAbstractItemModel* sourceModel) override;
|
||||||
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
|
|
||||||
QQmlComponent* delegateModel() const;
|
QQmlComponent* delegateModel() const;
|
||||||
void setDelegateModel(QQmlComponent* delegateModel);
|
void setDelegateModel(QQmlComponent* delegateModel);
|
||||||
|
@ -31,6 +34,10 @@ signals:
|
||||||
void delegateModelChanged();
|
void delegateModelChanged();
|
||||||
void submodelRoleNameChanged();
|
void submodelRoleNameChanged();
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void onCustomRoleChanged(QObject* source, int role);
|
||||||
|
void emitAllDataChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void initializeIfReady();
|
void initializeIfReady();
|
||||||
void initialize();
|
void initialize();
|
||||||
|
@ -39,9 +46,16 @@ private:
|
||||||
void onDelegateChanged();
|
void onDelegateChanged();
|
||||||
|
|
||||||
QPointer<QQmlComponent> m_delegateModel;
|
QPointer<QQmlComponent> m_delegateModel;
|
||||||
|
QPointer<QQmlComponent> m_connector;
|
||||||
|
|
||||||
QString m_submodelRoleName;
|
QString m_submodelRoleName;
|
||||||
|
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
bool m_sourceModelDeleted = false;
|
bool m_sourceModelDeleted = false;
|
||||||
int m_submodelRole = 0;
|
int m_submodelRole = 0;
|
||||||
|
bool m_dataChangedQueued = false;
|
||||||
|
|
||||||
|
QHash<int, QByteArray> m_roleNames;
|
||||||
|
QHash<QString, int> m_additionalRolesMap;
|
||||||
|
int m_additionalRolesOffset = std::numeric_limits<int>::max();
|
||||||
};
|
};
|
||||||
|
|
|
@ -131,8 +131,7 @@ void MovableModel::syncOrder()
|
||||||
|
|
||||||
void MovableModel::syncOrderInternal()
|
void MovableModel::syncOrderInternal()
|
||||||
{
|
{
|
||||||
if (m_sourceModel)
|
if (m_sourceModel) {
|
||||||
{
|
|
||||||
auto sourceModel = m_sourceModel;
|
auto sourceModel = m_sourceModel;
|
||||||
|
|
||||||
disconnect(m_sourceModel, nullptr, this, nullptr);
|
disconnect(m_sourceModel, nullptr, this, nullptr);
|
||||||
|
@ -148,10 +147,9 @@ void MovableModel::syncOrderInternal()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
m_indexes.clear();
|
m_indexes.clear();
|
||||||
if (!m_synced)
|
|
||||||
{
|
if (!m_synced) {
|
||||||
m_synced = true;
|
m_synced = true;
|
||||||
emit syncedChanged();
|
emit syncedChanged();
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,6 +5,20 @@
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
#include <QQmlEngine>
|
#include <QQmlEngine>
|
||||||
|
|
||||||
|
#include <memory>
|
||||||
|
|
||||||
|
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)
|
SubmodelProxyModel::SubmodelProxyModel(QObject* parent)
|
||||||
: QIdentityProxyModel{parent}
|
: QIdentityProxyModel{parent}
|
||||||
{
|
{
|
||||||
|
@ -54,9 +68,33 @@ QVariant SubmodelProxyModel::data(const QModelIndex &index, int role) const
|
||||||
|
|
||||||
QVariant wrappedInstance = QVariant::fromValue(instance);
|
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)));
|
||||||
|
}
|
||||||
|
|
||||||
submodelObj->setProperty(attachementPropertyName, wrappedInstance);
|
submodelObj->setProperty(attachementPropertyName, wrappedInstance);
|
||||||
|
|
||||||
return QVariant::fromValue(wrappedInstance);
|
return wrappedInstance;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (role >= m_additionalRolesOffset
|
||||||
|
&& role < m_additionalRolesOffset + m_additionalRolesMap.size())
|
||||||
|
{
|
||||||
|
auto submodel = data(index, m_submodelRole);
|
||||||
|
|
||||||
|
auto submodelObj = submodel.value<QObject*>();
|
||||||
|
|
||||||
|
if (submodelObj == nullptr) {
|
||||||
|
qWarning("Submodel must be a QObject-based type!");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return submodelObj->property(m_roleNames[role] + roleSuffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
return QIdentityProxyModel::data(index, role);
|
return QIdentityProxyModel::data(index, role);
|
||||||
|
@ -82,6 +120,11 @@ void SubmodelProxyModel::setSourceModel(QAbstractItemModel* model)
|
||||||
initializeIfReady();
|
initializeIfReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QHash<int, QByteArray> SubmodelProxyModel::roleNames() const
|
||||||
|
{
|
||||||
|
return m_roleNames;
|
||||||
|
}
|
||||||
|
|
||||||
QQmlComponent* SubmodelProxyModel::delegateModel() const
|
QQmlComponent* SubmodelProxyModel::delegateModel() const
|
||||||
{
|
{
|
||||||
return m_delegateModel;
|
return m_delegateModel;
|
||||||
|
@ -103,6 +146,8 @@ void SubmodelProxyModel::setDelegateModel(QQmlComponent* delegateModel)
|
||||||
m_delegateModel = delegateModel;
|
m_delegateModel = delegateModel;
|
||||||
|
|
||||||
onDelegateChanged();
|
onDelegateChanged();
|
||||||
|
|
||||||
|
initializeIfReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString& SubmodelProxyModel::submodelRoleName() const
|
const QString& SubmodelProxyModel::submodelRoleName() const
|
||||||
|
@ -126,28 +171,125 @@ void SubmodelProxyModel::setSubmodelRoleName(const QString& sumodelRoleName)
|
||||||
initializeIfReady();
|
initializeIfReady();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<int> roles(m_additionalRolesMap.cbegin(),
|
||||||
|
m_additionalRolesMap.cend());
|
||||||
|
|
||||||
|
emit this->dataChanged(index(0, 0), index(count - 1, 0), roles);
|
||||||
|
}
|
||||||
|
|
||||||
void SubmodelProxyModel::initializeIfReady()
|
void SubmodelProxyModel::initializeIfReady()
|
||||||
{
|
{
|
||||||
if (!m_submodelRoleName.isEmpty() && sourceModel()
|
if (!m_submodelRoleName.isEmpty() && sourceModel()
|
||||||
&& !roleNames().empty())
|
&& !sourceModel()->roleNames().empty() && m_delegateModel)
|
||||||
initialize();
|
initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SubmodelProxyModel::initialize()
|
void SubmodelProxyModel::initialize()
|
||||||
{
|
{
|
||||||
auto roles = roleNames();
|
auto roles = sourceModel()->roleNames();
|
||||||
auto keys = roles.keys(m_submodelRoleName.toUtf8());
|
auto submodelKeys = roles.keys(m_submodelRoleName.toUtf8());
|
||||||
auto keysCount = keys.size();
|
auto submodelKeysCount = submodelKeys.size();
|
||||||
|
|
||||||
if (keysCount == 1) {
|
if (submodelKeysCount == 1) {
|
||||||
m_initialized = true;
|
m_submodelRole = submodelKeys.first();
|
||||||
m_submodelRole = keys.first();
|
} else if (submodelKeysCount == 0){
|
||||||
} else if (keysCount == 0){
|
|
||||||
qWarning() << "Submodel role not found!";
|
qWarning() << "Submodel role not found!";
|
||||||
|
return;
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "Malformed source model - multiple roles found for given "
|
qWarning() << "Malformed source model - multiple roles found for given "
|
||||||
"submodel role name!";
|
"submodel role name!";
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auto creationContext = m_delegateModel->creationContext();
|
||||||
|
auto parentContext = creationContext
|
||||||
|
? creationContext : m_delegateModel->engine()->rootContext();
|
||||||
|
|
||||||
|
QIdentityProxyModel emptyModel;
|
||||||
|
|
||||||
|
auto context = std::make_unique<QQmlContext>(parentContext);
|
||||||
|
|
||||||
|
// The delegate object is created in order to inspect properties. It may
|
||||||
|
// be not properly initialized because of e.g. lack of context properties
|
||||||
|
// containing submodel. To avoid warnings, they are muted by setting empty
|
||||||
|
// message handler temporarily.
|
||||||
|
QtMessageHandler originalHandler = qInstallMessageHandler(
|
||||||
|
emptyMessageHandler);
|
||||||
|
std::unique_ptr<QObject> instance(m_delegateModel->create(context.get()));
|
||||||
|
qInstallMessageHandler(originalHandler);
|
||||||
|
|
||||||
|
const QMetaObject* meta = instance->metaObject();
|
||||||
|
|
||||||
|
QStringList additionalRoles;
|
||||||
|
|
||||||
|
for (auto i = meta->propertyOffset(); i < meta->propertyCount(); ++i) {
|
||||||
|
const QLatin1String propertyName(meta->property(i).name());
|
||||||
|
|
||||||
|
bool isRole = propertyName.endsWith(QLatin1String(roleSuffix));
|
||||||
|
|
||||||
|
if (!isRole)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
additionalRoles << propertyName.chopped(qstrlen(roleSuffix));
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
for (auto& additionalRole : qAsConst(additionalRoles)) {
|
||||||
|
auto roleKey = ++maxRoleKey;
|
||||||
|
|
||||||
|
roles.insert(roleKey, additionalRole.toUtf8());
|
||||||
|
m_additionalRolesMap.insert(additionalRole, roleKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
m_roleNames = roles;
|
||||||
|
|
||||||
|
QString connectorCode = R"(
|
||||||
|
import QtQml 2.15
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
signal customRoleChanged(source: QtObject, role: int)
|
||||||
|
)";
|
||||||
|
|
||||||
|
for (auto& additionalRole : qAsConst(additionalRoles)) {
|
||||||
|
int role = m_additionalRolesMap[additionalRole];
|
||||||
|
|
||||||
|
auto upperCaseRole = additionalRole;
|
||||||
|
upperCaseRole[0] = upperCaseRole[0].toUpper();
|
||||||
|
|
||||||
|
connectorCode += QString(R"(
|
||||||
|
function on%1RoleChanged() { customRoleChanged(target, %2) }
|
||||||
|
)").arg(upperCaseRole).arg(role);
|
||||||
|
}
|
||||||
|
|
||||||
|
connectorCode += "}";
|
||||||
|
|
||||||
|
m_connector = new QQmlComponent(m_delegateModel->engine(), m_delegateModel);
|
||||||
|
m_connector->setData(connectorCode.toUtf8(), {});
|
||||||
|
|
||||||
|
m_initialized = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void SubmodelProxyModel::initRoles()
|
void SubmodelProxyModel::initRoles()
|
||||||
|
@ -168,4 +310,3 @@ void SubmodelProxyModel::onDelegateChanged()
|
||||||
{ m_submodelRole });
|
{ m_submodelRole });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -14,7 +14,7 @@ QVariant SumAggregator::calculateAggregation() {
|
||||||
|
|
||||||
// Check if m_roleName is part of the roles of the model
|
// Check if m_roleName is part of the roles of the model
|
||||||
QHash<int, QByteArray> roles = model()->roleNames();
|
QHash<int, QByteArray> roles = model()->roleNames();
|
||||||
if (!roleExists()) {
|
if (!roleExists() && model()->rowCount()) {
|
||||||
qWarning() << "Provided role name does not exist in the current model";
|
qWarning() << "Provided role name does not exist in the current model";
|
||||||
return 0.0;
|
return 0.0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -756,6 +756,8 @@ private slots:
|
||||||
{ "name": "A", "subname": "a1" }
|
{ "name": "A", "subname": "a1" }
|
||||||
])");
|
])");
|
||||||
|
|
||||||
|
QVERIFY(isSame(&sfpm, expectedSorted));
|
||||||
|
|
||||||
QCOMPARE(model.synced(), true);
|
QCOMPARE(model.synced(), true);
|
||||||
QCOMPARE(signalsSpy.count(), signalsSpySfpm.count());
|
QCOMPARE(signalsSpy.count(), signalsSpySfpm.count());
|
||||||
QVERIFY(indexesTester.compare());
|
QVERIFY(indexesTester.compare());
|
||||||
|
@ -792,16 +794,6 @@ private slots:
|
||||||
|
|
||||||
model.move(0, 1);
|
model.move(0, 1);
|
||||||
|
|
||||||
ListModelWrapper expectedSorted(engine, R"([
|
|
||||||
{ "name": "C", "subname": "c3" },
|
|
||||||
{ "name": "C", "subname": "c2" },
|
|
||||||
{ "name": "C", "subname": "c1" },
|
|
||||||
{ "name": "B", "subname": "b1" },
|
|
||||||
{ "name": "A", "subname": "a2" },
|
|
||||||
{ "name": "A", "subname": "a1" }
|
|
||||||
])");
|
|
||||||
|
|
||||||
|
|
||||||
auto source2 = R"([
|
auto source2 = R"([
|
||||||
{ "name": "E", "subname": "a1" },
|
{ "name": "E", "subname": "a1" },
|
||||||
{ "name": "F", "subname": "a2" },
|
{ "name": "F", "subname": "a2" },
|
||||||
|
|
|
@ -11,12 +11,25 @@
|
||||||
#include <string>
|
#include <string>
|
||||||
|
|
||||||
#include <StatusQ/submodelproxymodel.h>
|
#include <StatusQ/submodelproxymodel.h>
|
||||||
|
|
||||||
#include <TestHelpers/listmodelwrapper.h>
|
#include <TestHelpers/listmodelwrapper.h>
|
||||||
|
#include <TestHelpers/modelsignalsspy.h>
|
||||||
|
#include <TestHelpers/modeltestutils.h>
|
||||||
|
|
||||||
class TestSubmodelProxyModel: public QObject
|
class TestSubmodelProxyModel: public QObject
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
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:
|
private slots:
|
||||||
void basicTest() {
|
void basicTest() {
|
||||||
QQmlEngine engine;
|
QQmlEngine engine;
|
||||||
|
@ -104,13 +117,13 @@ private slots:
|
||||||
|
|
||||||
QCOMPARE(model.rowCount(), 1);
|
QCOMPARE(model.rowCount(), 1);
|
||||||
|
|
||||||
QVariant balances = model.data(model.index(0, 0),
|
QVariant balances1 = model.data(model.index(0, 0),
|
||||||
sourceModel.role("balances"));
|
sourceModel.role("balances"));
|
||||||
|
QVERIFY(balances1.isValid());
|
||||||
QVERIFY(balances.isValid());
|
|
||||||
|
|
||||||
QVariant balances2 = model.data(model.index(0, 0),
|
QVariant balances2 = model.data(model.index(0, 0),
|
||||||
sourceModel.role("balances"));
|
sourceModel.role("balances"));
|
||||||
|
QVERIFY(balances2.isValid());
|
||||||
|
|
||||||
// SubmodelProxyModel may create proxy objects on demand, then first
|
// SubmodelProxyModel may create proxy objects on demand, then first
|
||||||
// call to data(...) returns freshly created object, the next calls
|
// call to data(...) returns freshly created object, the next calls
|
||||||
|
@ -119,7 +132,10 @@ private slots:
|
||||||
// pointer in first call and pointer wrapped into QPointer in the next
|
// pointer in first call and pointer wrapped into QPointer in the next
|
||||||
// one leads to problems in UI components in some scenarios even if
|
// one leads to problems in UI components in some scenarios even if
|
||||||
// those QVariant types are automatically convertible.
|
// those QVariant types are automatically convertible.
|
||||||
QCOMPARE(balances2.type(), balances.type());
|
QCOMPARE(balances2.type(), balances1.type());
|
||||||
|
|
||||||
|
// Check if the same instance is returned.
|
||||||
|
QCOMPARE(balances2.value<QObject*>(), balances1.value<QObject*>());
|
||||||
}
|
}
|
||||||
|
|
||||||
void usingNonObjectSubmodelRoleTest() {
|
void usingNonObjectSubmodelRoleTest() {
|
||||||
|
@ -230,7 +246,7 @@ private slots:
|
||||||
QCOMPARE(model.data(model.index(0, 0), 0), {});
|
QCOMPARE(model.data(model.index(0, 0), 0), {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void settingUndefinedSubmodelRoleNameText() {
|
void settingUndefinedSubmodelRoleNameTest() {
|
||||||
QQmlEngine engine;
|
QQmlEngine engine;
|
||||||
auto delegate = std::make_unique<QQmlComponent>(&engine);
|
auto delegate = std::make_unique<QQmlComponent>(&engine);
|
||||||
|
|
||||||
|
@ -258,6 +274,186 @@ private slots:
|
||||||
|
|
||||||
QCOMPARE(model.rowCount(), 3);
|
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 {
|
||||||
|
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<QObject*>();
|
||||||
|
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<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 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<QObject*>();
|
||||||
|
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 multipleProxiesTest() {
|
||||||
|
QSKIP("Not implemented yet. The goal is to make the proxy fully "
|
||||||
|
"non-intrusive what will fix the isse pointed in this test.");
|
||||||
|
|
||||||
|
QQmlEngine engine;
|
||||||
|
auto delegate1 = std::make_unique<QQmlComponent>(&engine);
|
||||||
|
|
||||||
|
delegate1->setData(QByteArrayLiteral(R"(
|
||||||
|
import QtQml.Models 2.15
|
||||||
|
|
||||||
|
ListModel {
|
||||||
|
readonly property int myProp: 42
|
||||||
|
}
|
||||||
|
)"), QUrl());
|
||||||
|
|
||||||
|
auto delegate2 = std::make_unique<QQmlComponent>(&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<QObject*>();
|
||||||
|
QCOMPARE(wrapper1->property("myProp"), 42);
|
||||||
|
|
||||||
|
QVariant wrapperVariant2 = model2.data(model2.index(0, 0),
|
||||||
|
roleForName(roles, "balances"));
|
||||||
|
QObject* wrapper2 = wrapperVariant2.value<QObject*>();
|
||||||
|
QCOMPARE(wrapper2->property("myProp"), 11);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
QTEST_MAIN(TestSubmodelProxyModel)
|
QTEST_MAIN(TestSubmodelProxyModel)
|
||||||
|
|
Loading…
Reference in New Issue