status-desktop/storybook/pages/ObjectProxyModelExample1.qml

344 lines
9.8 KiB
QML

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"
}
}
}
}