2026-07-30 01:13:20 -03:00
|
|
|
pragma ComponentBehavior: Bound
|
|
|
|
|
|
|
|
|
|
import QtQuick
|
|
|
|
|
import QtQuick.Controls.Basic
|
|
|
|
|
|
|
|
|
|
Item {
|
|
|
|
|
id: root
|
|
|
|
|
|
|
|
|
|
enum SelectionMode {
|
|
|
|
|
Input,
|
|
|
|
|
Output
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
property var sourceModel: []
|
|
|
|
|
property string accountType: ""
|
|
|
|
|
property string stateField: ""
|
|
|
|
|
property var stateValue: ""
|
|
|
|
|
property int selectionMode: ProgramAccountSelector.Input
|
|
|
|
|
property string selectedAccountId: ""
|
|
|
|
|
property bool createNewSelected: false
|
|
|
|
|
property string createNewText: qsTr("Create new account")
|
|
|
|
|
property string emptyInputText: qsTr("No funds")
|
|
|
|
|
property string placeholderText: qsTr("Select account")
|
|
|
|
|
property string criteriaPendingText: qsTr("Select token first")
|
|
|
|
|
property string accessibleName: qsTr("Program account")
|
|
|
|
|
property color backgroundColor: "#27272a"
|
|
|
|
|
property color hoverColor: "#3f3f46"
|
|
|
|
|
property color textColor: "#f4f4f5"
|
|
|
|
|
property color secondaryTextColor: "#a1a1aa"
|
|
|
|
|
property color borderColor: "#52525b"
|
|
|
|
|
property color focusColor: "#f26a21"
|
2026-08-08 15:47:28 +02:00
|
|
|
// Show the combo even when only one account matches. By default a single match
|
|
|
|
|
// auto-selects and hides (no choice to make); consumers that want the chosen
|
|
|
|
|
// holding always visible set this true.
|
|
|
|
|
property bool showWhenSingle: false
|
|
|
|
|
// Horizontal alignment of the selector's text (closed combo, dropdown rows and
|
|
|
|
|
// the empty "No funds" label). Defaults to left; consumers can right-align it.
|
|
|
|
|
property int textAlignment: Text.AlignLeft
|
2026-07-30 01:13:20 -03:00
|
|
|
property int modelRevision: 0
|
|
|
|
|
|
|
|
|
|
readonly property bool criteriaReady: root.accountType.length > 0
|
|
|
|
|
&& (root.stateField.length === 0
|
|
|
|
|
|| root.scalarText(root.stateValue).length > 0)
|
|
|
|
|
readonly property var matchingAccounts: root.filteredAccounts()
|
|
|
|
|
readonly property var choices: root.choiceRows()
|
|
|
|
|
readonly property bool hasFunds: root.matchingAccounts.length > 0
|
|
|
|
|
readonly property bool selectionValid: root.accountById(root.selectedAccountId) !== null
|
|
|
|
|
readonly property bool ready: root.criteriaReady
|
|
|
|
|
&& (root.selectionValid
|
|
|
|
|
|| (root.selectionMode === ProgramAccountSelector.Output
|
|
|
|
|
&& root.createNewSelected))
|
|
|
|
|
readonly property var selectedAccount: root.accountById(root.selectedAccountId)
|
2026-08-17 13:24:17 +02:00
|
|
|
readonly property string selectedBalance: root.selectedAccount
|
2026-07-30 01:13:20 -03:00
|
|
|
? String(root.valueFor(
|
|
|
|
|
root.selectedAccount,
|
|
|
|
|
"balanceRaw") || "0")
|
|
|
|
|
: "0"
|
|
|
|
|
readonly property bool showCombo: root.selectionMode === ProgramAccountSelector.Output
|
|
|
|
|
|| (root.criteriaReady
|
2026-08-08 15:47:28 +02:00
|
|
|
&& root.matchingAccounts.length
|
|
|
|
|
> (root.showWhenSingle ? 0 : 1))
|
2026-07-30 01:13:20 -03:00
|
|
|
readonly property bool showEmptyInput: root.selectionMode === ProgramAccountSelector.Input
|
|
|
|
|
&& root.criteriaReady
|
|
|
|
|
&& root.matchingAccounts.length === 0
|
|
|
|
|
|
|
|
|
|
signal selectionChanged(string accountId, bool createNew)
|
|
|
|
|
|
|
|
|
|
implicitWidth: 220
|
|
|
|
|
implicitHeight: root.showCombo ? 34 : root.showEmptyInput ? 20 : 0
|
|
|
|
|
visible: implicitHeight > 0
|
|
|
|
|
|
|
|
|
|
Instantiator {
|
|
|
|
|
id: rows
|
|
|
|
|
|
|
|
|
|
model: root.sourceModel
|
|
|
|
|
delegate: QtObject {
|
|
|
|
|
required property var model
|
|
|
|
|
required property var modelData
|
|
|
|
|
|
|
|
|
|
readonly property var accountRow: {
|
|
|
|
|
if (modelData !== null && typeof modelData === "object") {
|
|
|
|
|
return modelData
|
|
|
|
|
}
|
|
|
|
|
if (model === null || typeof model !== "object")
|
|
|
|
|
return ({})
|
|
|
|
|
return {
|
|
|
|
|
"accountId": model.accountId,
|
|
|
|
|
"address": model.address,
|
|
|
|
|
"displayAddress": model.displayAddress,
|
|
|
|
|
"accountType": model.accountType,
|
|
|
|
|
"definitionId": model.definitionId,
|
|
|
|
|
"balanceRaw": model.balanceRaw,
|
|
|
|
|
"state": model.state
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
onObjectAdded: function(index, object) {
|
|
|
|
|
++root.modelRevision
|
|
|
|
|
Qt.callLater(root.reconcileSelection)
|
|
|
|
|
}
|
|
|
|
|
onObjectRemoved: function(index, object) {
|
|
|
|
|
++root.modelRevision
|
|
|
|
|
Qt.callLater(root.reconcileSelection)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onSourceModelChanged: Qt.callLater(root.reconcileSelection)
|
|
|
|
|
onAccountTypeChanged: Qt.callLater(root.reconcileSelection)
|
|
|
|
|
onStateFieldChanged: Qt.callLater(root.reconcileSelection)
|
|
|
|
|
onStateValueChanged: Qt.callLater(root.reconcileSelection)
|
|
|
|
|
onSelectionModeChanged: Qt.callLater(root.reconcileSelection)
|
|
|
|
|
Component.onCompleted: Qt.callLater(root.reconcileSelection)
|
|
|
|
|
|
|
|
|
|
Text {
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
visible: root.showEmptyInput
|
|
|
|
|
text: root.emptyInputText
|
|
|
|
|
color: root.secondaryTextColor
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
2026-08-08 15:47:28 +02:00
|
|
|
horizontalAlignment: root.textAlignment
|
2026-07-30 01:13:20 -03:00
|
|
|
Accessible.role: Accessible.StaticText
|
|
|
|
|
Accessible.name: text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ComboBox {
|
|
|
|
|
id: accountCombo
|
|
|
|
|
|
|
|
|
|
objectName: "programAccountComboBox"
|
|
|
|
|
anchors.fill: parent
|
|
|
|
|
visible: root.showCombo
|
|
|
|
|
enabled: root.enabled && root.criteriaReady && root.choices.length > 0
|
|
|
|
|
model: root.choices
|
|
|
|
|
currentIndex: root.choiceIndex()
|
|
|
|
|
displayText: root.displayLabel()
|
|
|
|
|
leftPadding: 10
|
|
|
|
|
rightPadding: 28
|
|
|
|
|
topPadding: 0
|
|
|
|
|
bottomPadding: 0
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
activeFocusOnTab: true
|
|
|
|
|
focusPolicy: Qt.StrongFocus
|
|
|
|
|
Accessible.name: root.accessibleName
|
|
|
|
|
|
|
|
|
|
contentItem: Text {
|
|
|
|
|
leftPadding: accountCombo.leftPadding
|
|
|
|
|
rightPadding: accountCombo.rightPadding
|
|
|
|
|
text: accountCombo.displayText
|
|
|
|
|
color: accountCombo.enabled ? root.textColor : root.secondaryTextColor
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
2026-08-08 15:47:28 +02:00
|
|
|
horizontalAlignment: root.textAlignment
|
2026-07-30 01:13:20 -03:00
|
|
|
elide: Text.ElideMiddle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
indicator: Text {
|
|
|
|
|
x: accountCombo.width - width - 10
|
|
|
|
|
y: Math.round((accountCombo.height - height) / 2)
|
|
|
|
|
text: "\u25BE"
|
|
|
|
|
color: accountCombo.enabled ? root.secondaryTextColor : root.borderColor
|
|
|
|
|
font.pixelSize: 10
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
background: Rectangle {
|
|
|
|
|
radius: 7
|
|
|
|
|
color: !accountCombo.enabled
|
|
|
|
|
? root.backgroundColor
|
|
|
|
|
: accountCombo.down || accountCombo.hovered
|
|
|
|
|
? root.hoverColor : root.backgroundColor
|
|
|
|
|
border.color: accountCombo.activeFocus ? root.focusColor : root.borderColor
|
|
|
|
|
border.width: 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
delegate: ItemDelegate {
|
|
|
|
|
id: optionDelegate
|
|
|
|
|
|
|
|
|
|
required property int index
|
|
|
|
|
required property var modelData
|
|
|
|
|
|
|
|
|
|
width: ListView.view ? ListView.view.width : accountCombo.width
|
|
|
|
|
height: 34
|
|
|
|
|
hoverEnabled: true
|
|
|
|
|
highlighted: accountCombo.highlightedIndex === optionDelegate.index
|
|
|
|
|
|
|
|
|
|
contentItem: Text {
|
|
|
|
|
leftPadding: 8
|
|
|
|
|
rightPadding: 8
|
|
|
|
|
text: root.labelFor(optionDelegate.modelData)
|
|
|
|
|
color: root.textColor
|
|
|
|
|
font.pixelSize: 11
|
|
|
|
|
verticalAlignment: Text.AlignVCenter
|
2026-08-08 15:47:28 +02:00
|
|
|
horizontalAlignment: root.textAlignment
|
2026-07-30 01:13:20 -03:00
|
|
|
elide: Text.ElideMiddle
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
background: Rectangle {
|
|
|
|
|
radius: 5
|
|
|
|
|
color: optionDelegate.highlighted || optionDelegate.hovered
|
|
|
|
|
? root.hoverColor : "transparent"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
popup: Popup {
|
|
|
|
|
y: accountCombo.height + 4
|
|
|
|
|
width: accountCombo.width
|
|
|
|
|
implicitHeight: Math.min(contentItem.implicitHeight + topPadding + bottomPadding,
|
|
|
|
|
204)
|
|
|
|
|
padding: 4
|
|
|
|
|
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
|
|
|
|
|
|
|
|
|
|
contentItem: ListView {
|
|
|
|
|
clip: true
|
|
|
|
|
implicitHeight: contentHeight
|
|
|
|
|
model: accountCombo.delegateModel
|
|
|
|
|
currentIndex: accountCombo.highlightedIndex
|
|
|
|
|
highlightMoveDuration: 0
|
|
|
|
|
ScrollIndicator.vertical: ScrollIndicator { }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
background: Rectangle {
|
|
|
|
|
radius: 7
|
|
|
|
|
color: root.backgroundColor
|
|
|
|
|
border.color: root.borderColor
|
|
|
|
|
border.width: 1
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
onActivated: function(index) {
|
|
|
|
|
const choice = root.choices[index]
|
|
|
|
|
if (!choice)
|
|
|
|
|
return
|
|
|
|
|
root.setSelection(choice.createNew === true
|
|
|
|
|
? "" : root.accountIdFor(choice),
|
|
|
|
|
choice.createNew === true)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function filteredAccounts() {
|
|
|
|
|
root.modelRevision
|
|
|
|
|
if (!root.criteriaReady)
|
|
|
|
|
return []
|
|
|
|
|
const result = []
|
|
|
|
|
for (let index = 0; index < rows.count; ++index) {
|
|
|
|
|
const object = rows.objectAt(index)
|
|
|
|
|
const row = object ? root.valueFor(object, "accountRow") : null
|
|
|
|
|
if (!row)
|
|
|
|
|
continue
|
|
|
|
|
const type = String(root.valueFor(row, "accountType")
|
|
|
|
|
|| root.valueFor(row, "typeName")
|
|
|
|
|
|| root.valueFor(row, "programType") || "")
|
|
|
|
|
if (type !== root.accountType)
|
|
|
|
|
continue
|
|
|
|
|
if (root.stateField.length > 0
|
|
|
|
|
&& root.scalarText(root.valueFor(row, root.stateField))
|
|
|
|
|
!== root.scalarText(root.stateValue)) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if (root.accountIdFor(row).length > 0)
|
|
|
|
|
result.push(row)
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function valueFor(row, field) {
|
|
|
|
|
if (!row)
|
|
|
|
|
return undefined
|
|
|
|
|
if (row[field] !== undefined)
|
|
|
|
|
return row[field]
|
|
|
|
|
if (row.state && row.state[field] !== undefined)
|
|
|
|
|
return row.state[field]
|
|
|
|
|
if (row.fields && row.fields[field] !== undefined)
|
|
|
|
|
return row.fields[field]
|
|
|
|
|
return undefined
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function scalarText(value) {
|
|
|
|
|
return value === undefined || value === null ? "" : String(value)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function accountIdFor(row) {
|
|
|
|
|
return String(root.valueFor(row, "accountId")
|
|
|
|
|
|| root.valueFor(row, "displayAddress")
|
|
|
|
|
|| root.valueFor(row, "address")
|
|
|
|
|
|| root.valueFor(row, "holdingId") || "")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function accountById(accountId) {
|
|
|
|
|
const value = String(accountId || "")
|
|
|
|
|
if (value.length === 0)
|
|
|
|
|
return null
|
|
|
|
|
for (let index = 0; index < root.matchingAccounts.length; ++index) {
|
|
|
|
|
if (root.accountIdFor(root.matchingAccounts[index]) === value)
|
|
|
|
|
return root.matchingAccounts[index]
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function choiceRows() {
|
|
|
|
|
const result = root.matchingAccounts.slice(0)
|
|
|
|
|
if (root.selectionMode === ProgramAccountSelector.Output)
|
|
|
|
|
result.push({ "createNew": true })
|
|
|
|
|
return result
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function choiceIndex() {
|
|
|
|
|
if (root.createNewSelected)
|
|
|
|
|
return root.choices.length - 1
|
|
|
|
|
for (let index = 0; index < root.choices.length; ++index) {
|
|
|
|
|
if (root.accountIdFor(root.choices[index]) === root.selectedAccountId)
|
|
|
|
|
return index
|
|
|
|
|
}
|
|
|
|
|
return -1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function displayLabel() {
|
|
|
|
|
const index = root.choiceIndex()
|
|
|
|
|
if (index >= 0)
|
|
|
|
|
return root.labelFor(root.choices[index])
|
|
|
|
|
if (!root.criteriaReady)
|
|
|
|
|
return root.criteriaPendingText
|
|
|
|
|
return root.placeholderText
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function labelFor(row) {
|
|
|
|
|
if (row && row.createNew === true)
|
|
|
|
|
return root.createNewText
|
|
|
|
|
const id = root.accountIdFor(row)
|
|
|
|
|
const balanceValue = root.valueFor(row, "balanceRaw")
|
|
|
|
|
const balance = balanceValue === undefined || balanceValue === null ? "" : String(balanceValue)
|
|
|
|
|
return balance.length > 0
|
|
|
|
|
? qsTr("%1 · %2").arg(root.shortId(id)).arg(balance)
|
|
|
|
|
: root.shortId(id)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function shortId(value) {
|
|
|
|
|
const text = String(value || "")
|
|
|
|
|
return text.length > 14 ? text.slice(0, 7) + "..." + text.slice(-5) : text
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function setSelection(accountId, createNew) {
|
|
|
|
|
const nextId = String(accountId || "")
|
|
|
|
|
const nextCreate = createNew === true
|
|
|
|
|
if (root.selectedAccountId === nextId && root.createNewSelected === nextCreate)
|
|
|
|
|
return
|
|
|
|
|
root.selectedAccountId = nextId
|
|
|
|
|
root.createNewSelected = nextCreate
|
|
|
|
|
root.selectionChanged(nextId, nextCreate)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function reconcileSelection() {
|
|
|
|
|
if (!root.criteriaReady) {
|
|
|
|
|
root.setSelection("", false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (root.selectionValid)
|
|
|
|
|
return
|
|
|
|
|
if (root.selectionMode === ProgramAccountSelector.Input) {
|
|
|
|
|
root.setSelection(root.matchingAccounts.length === 1
|
|
|
|
|
? root.accountIdFor(root.matchingAccounts[0]) : "",
|
|
|
|
|
false)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if (root.createNewSelected)
|
|
|
|
|
return
|
|
|
|
|
if (root.matchingAccounts.length === 0) {
|
|
|
|
|
root.setSelection("", true)
|
|
|
|
|
} else if (root.matchingAccounts.length === 1) {
|
|
|
|
|
root.setSelection(root.accountIdFor(root.matchingAccounts[0]), false)
|
|
|
|
|
} else {
|
|
|
|
|
root.setSelection("", false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|