344 lines
9.8 KiB
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"
|
|
}
|
|
}
|
|
}
|
|
}
|