feat(StatusQ): Generic proxy model allowing setting proxies like SFPM or LeftJoinModel for submodels

It allows transforming model, including submodels outside of the view
component consuming the model. Thanks to that now there is no need
to create proxies for submodels in view's delegate. It can be done
earlies, on the level of the main view.

Closes: #12630
This commit is contained in:
Michał Cieślak 2023-11-04 21:25:10 +01:00 committed by Michał
parent 2b92c1561e
commit d4d72038bd
6 changed files with 468 additions and 3 deletions

View File

@ -0,0 +1,271 @@
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
Item {
id: root
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 {
readonly property LeftJoinModel joinModel: LeftJoinModel {
leftModel: submodel
rightModel: networksModel
joinRole: "chainId"
}
sourceModel: joinModel
filters: ExpressionFilter {
expression: balance >= thresholdSlider.value
}
sorters: RoleSorter {
roleName: "name"
enabled: sortCheckBox.checked
}
}
submodelRoleName: "balances"
}
ColumnLayout {
anchors.fill: parent
anchors.margins: 10
RowLayout {
Layout.fillWidth: 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 {
id: listView
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
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])
}
}
}
}
}
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

View File

@ -97,6 +97,7 @@ add_library(StatusQ SHARED
include/StatusQ/statussyntaxhighlighter.h
include/StatusQ/statuswindow.h
include/StatusQ/stringutilsinternal.h
include/StatusQ/submodelproxymodel.h
src/QClipboardProxy.cpp
src/leftjoinmodel.cpp
src/modelutilsinternal.cpp
@ -107,6 +108,7 @@ add_library(StatusQ SHARED
src/statussyntaxhighlighter.cpp
src/statuswindow.cpp
src/stringutilsinternal.cpp
src/submodelproxymodel.cpp
)
set_target_properties(StatusQ PROPERTIES

View File

@ -0,0 +1,47 @@
#pragma once
#include <QIdentityProxyModel>
#include <QPointer>
class QQmlComponent;
class 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;
QQmlComponent* delegateModel() const;
void setDelegateModel(QQmlComponent* delegateModel);
const QString& submodelRoleName() const;
void setSubmodelRoleName(const QString& sumodelRoleName);
signals:
void delegateModelChanged();
void submodelRoleNameChanged();
private:
void initializeIfReady();
void initialize();
void initRoles();
void onDelegateChanged();
QPointer<QQmlComponent> m_delegateModel;
QString m_submodelRoleName;
bool m_initialized = false;
bool m_sourceModelDeleted = false;
int m_submodelRole = 0;
};

View File

@ -133,8 +133,7 @@ QVariant LeftJoinModel::data(const QModelIndex& index, int role) const
if (m_lastUsedRightModelIndex.isValid()
&& m_rightModel->data(m_lastUsedRightModelIndex,
m_rightModelJoinRole) == joinRoleLeftValue)
{
m_rightModelJoinRole) == joinRoleLeftValue) {
return m_rightModel->data(m_lastUsedRightModelIndex,
role - m_rightModelRolesOffset);
}

View File

@ -12,6 +12,8 @@
#include "StatusQ/statussyntaxhighlighter.h"
#include "StatusQ/statuswindow.h"
#include "StatusQ/stringutilsinternal.h"
#include "StatusQ/submodelproxymodel.h"
class StatusQPlugin : public QQmlExtensionPlugin
{
@ -27,8 +29,9 @@ public:
qmlRegisterType<RXValidator>("StatusQ", 0, 1, "RXValidator");
qmlRegisterType<LeftJoinModel>("StatusQ", 0, 1, "LeftJoinModel");
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
qmlRegisterType<SubmodelProxyModel>("StatusQ", 0, 1, "SubmodelProxyModel");
qmlRegisterType<RoleRename>("StatusQ", 0, 1, "RoleRename");
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance);

View File

@ -0,0 +1,143 @@
#include "StatusQ/submodelproxymodel.h"
#include <QDebug>
#include <QQmlComponent>
#include <QQmlContext>
#include <QQmlEngine>
SubmodelProxyModel::SubmodelProxyModel(QObject* parent)
: QIdentityProxyModel{parent}
{
}
QVariant SubmodelProxyModel::data(const QModelIndex &index, int role) const
{
if (!checkIndex(index, CheckIndexOption::IndexIsValid))
return {};
if (m_initialized && m_delegateModel && role == m_submodelRole) {
auto submodel = QIdentityProxyModel::data(index, role);
auto creationContext = m_delegateModel->creationContext();
auto parentContext = creationContext
? creationContext : m_delegateModel->engine()->rootContext();
auto context = new QQmlContext(parentContext, parentContext);
context->setContextProperty("submodel", submodel);
QObject* instance = m_delegateModel->create(context);
QQmlEngine::setObjectOwnership(instance, QQmlEngine::JavaScriptOwnership);
return QVariant::fromValue(instance);
}
return QIdentityProxyModel::data(index, role);
}
void SubmodelProxyModel::setSourceModel(QAbstractItemModel* model)
{
if (sourceModel() != nullptr || m_sourceModelDeleted) {
qWarning("Changing source model is not supported!");
return;
}
// Workaround for QTBUG-57971
if (model && model->roleNames().isEmpty())
connect(model, &QAbstractItemModel::rowsInserted,
this, &SubmodelProxyModel::initRoles);
connect(model, &QObject::destroyed, this, [this] {
this->m_sourceModelDeleted = true;
});
QIdentityProxyModel::setSourceModel(model);
initializeIfReady();
}
QQmlComponent* SubmodelProxyModel::delegateModel() const
{
return m_delegateModel;
}
void SubmodelProxyModel::setDelegateModel(QQmlComponent* delegateModel)
{
if (m_delegateModel == delegateModel)
return;
if (m_delegateModel)
disconnect(delegateModel, &QObject::destroyed,
this, &SubmodelProxyModel::onDelegateChanged);
if (delegateModel)
connect(delegateModel, &QObject::destroyed,
this, &SubmodelProxyModel::onDelegateChanged);
m_delegateModel = delegateModel;
onDelegateChanged();
}
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;
emit submodelRoleNameChanged();
initializeIfReady();
}
void SubmodelProxyModel::initializeIfReady()
{
if (!m_submodelRoleName.isEmpty() && sourceModel()
&& !roleNames().empty())
initialize();
}
void SubmodelProxyModel::initialize()
{
auto roles = roleNames();
auto keys = roles.keys(m_submodelRoleName.toUtf8());
auto keysCount = keys.size();
if (keysCount == 1) {
m_initialized = true;
m_submodelRole = keys.first();
} else if (keysCount == 0){
qWarning() << "Submodel role not found!";
} else {
qWarning() << "Malformed source model - multiple roles found for given "
"submodel role name!";
}
}
void SubmodelProxyModel::initRoles()
{
disconnect(sourceModel(), &QAbstractItemModel::rowsInserted,
this, &SubmodelProxyModel::initRoles);
resetInternalData();
initializeIfReady();
}
void SubmodelProxyModel::onDelegateChanged()
{
emit delegateModelChanged();
if (m_initialized && rowCount() && columnCount()) {
emit dataChanged(index(0, 0), index(rowCount() - 1, columnCount() - 1),
{ m_submodelRole });
}
}