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:
parent
2b92c1561e
commit
d4d72038bd
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
||||
|
|
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue