fix(Wallet) network selection and unify network implementations

Major changes:

- Don't allow empty network selection. End up using the nim model
  directly instead because of individual row changes issues
  encountered with nim models
- Made the clone model a generic implementation to be used in other
places where we need to clone a model: ReceiveModal,
AddEditSavedAddressPopup
- Use cloned model as alternative to NetworksExtraStoreProxy in
  ReceiveModal
- Added tristate support to our generic checkbox control
- UX improvements as per design
- Fix save address tests naming and zero address issue
- Various fixes

Notes:
- Failed to make NetworkSelectPopup follow ground-truth: show partially
  checked as user intention until the network is selected in the
  source model. Got stuck on nim models not being stable models and
  report wrong entry change when reset. Tried sorting and only updating
  changes without reset but it didn't work.
- Moved grouped property SingleSelectionInfo to its own file from
  an inline component after finding out that it fails to load on Linux
  with error "Cannot assign to property of unknown type: "*".".
  It works on MacOS as expected

Closes: #10119
This commit is contained in:
Stefan 2023-04-05 14:10:44 +03:00 committed by Stefan Dunca
parent 4bd81e8a9a
commit 691de11211
30 changed files with 947 additions and 350 deletions

View File

@ -40,8 +40,8 @@ proc init*(self: Controller) =
proc getNetworks*(self: Controller): seq[NetworkDto] =
return self.networkService.getNetworks()
proc toggleNetwork*(self: Controller, chainId: int) =
self.walletAccountService.toggleNetworkEnabled(chainId)
proc setNetworksState*(self: Controller, chainIds: seq[int], enabled: bool) =
self.walletAccountService.setNetworksState(chainIds, enabled)
proc areTestNetworksEnabled*(self: Controller): bool =
return self.settingsService.areTestNetworksEnabled()

View File

@ -19,7 +19,7 @@ method isLoaded*(self: AccessInterface): bool {.base.} =
method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available")
method toggleNetwork*(self: AccessInterface, chainId: int) {.base.} =
method setNetworksState*(self: AccessInterface, chainIds: seq[int], enable: bool) {.base.} =
raise newException(ValueError, "No implementation available")
method refreshNetworks*(self: AccessInterface) {.base.} =

View File

@ -1,5 +1,11 @@
import strformat
type
UxEnabledState* {.pure.} = enum
Enabled
AllEnabled
Disabled
type
Item* = object
chainId: int
@ -16,6 +22,7 @@ type
chainColor: string
shortName: string
balance: float64
enabledState: UxEnabledState
proc initItem*(
chainId: int,
@ -32,6 +39,7 @@ proc initItem*(
chainColor: string,
shortName: string,
balance: float64,
enabledState: UxEnabledState,
): Item =
result.chainId = chainId
result.nativeCurrencyDecimals = nativeCurrencyDecimals
@ -47,6 +55,7 @@ proc initItem*(
result.chainColor = chainColor
result.shortName = shortName
result.balance = balance
result.enabledState = enabledState
proc `$`*(self: Item): string =
result = fmt"""NetworkItem(
@ -64,6 +73,7 @@ proc `$`*(self: Item): string =
shortName: {self.shortName},
chainColor: {self.chainColor},
balance: {self.balance},
enabledState: {self.enabledState}
]"""
proc getChainId*(self: Item): int =
@ -94,7 +104,7 @@ proc getIsTest*(self: Item): bool =
return self.isTest
proc getIsEnabled*(self: Item): bool =
return self.isEnabled
return self.isEnabled
proc getIconURL*(self: Item): string =
return self.iconUrl
@ -107,3 +117,6 @@ proc getChainColor*(self: Item): string =
proc getBalance*(self: Item): float64 =
return self.balance
proc getEnabledState*(self: Item): UxEnabledState =
return self.enabledState

View File

@ -1,4 +1,4 @@
import NimQml, Tables, strutils, strformat
import NimQml, Tables, strutils, strformat, sequtils
import ./item
@ -20,6 +20,7 @@ type
ChainColor
ShortName
Balance
EnabledState
QtObject:
type
@ -69,6 +70,7 @@ QtObject:
ModelRole.ShortName.int: "shortName",
ModelRole.ChainColor.int: "chainColor",
ModelRole.Balance.int: "balance",
ModelRole.EnabledState.int: "enabledState",
}.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -110,6 +112,8 @@ QtObject:
result = newQVariant(item.getChainColor())
of ModelRole.Balance:
result = newQVariant(item.getBalance())
of ModelRole.EnabledState:
result = newQVariant(item.getEnabledState().int)
proc rowData*(self: Model, index: int, column: string): string {.slot.} =
if (index >= self.items.len):
@ -130,6 +134,7 @@ QtObject:
of "chainColor": result = $item.getChainColor()
of "shortName": result = $item.getShortName()
of "balance": result = $item.getBalance()
of "enabledState": result = $item.getEnabledState().int
proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel()
@ -171,7 +176,7 @@ QtObject:
for item in self.items:
if cmpIgnoreCase(item.getShortName(), shortName) == 0:
return item.getChainName()
return ""
return ""
proc getNetworkColor*(self: Model, shortName: string): string {.slot.} =
for item in self.items:
@ -196,3 +201,41 @@ QtObject:
if(item.getChainId() == chainId):
return item.getBlockExplorerURL() & EXPLORER_TX_PREFIX
return ""
proc getEnabledState*(self: Model, chainId: int): UxEnabledState =
for item in self.items:
if(item.getChainId() == chainId):
return item.getEnabledState()
return UxEnabledState.Disabled
# Returns the chains that need to be enabled or disabled (the second return value)
# to satisty the transitions: all enabled to only chainId enabled and
# only chainId enabled to all enabled
proc networksToChangeStateOnUserActionFor*(self: Model, chainId: int): (seq[int], bool) =
var chainIds: seq[int] = @[]
var enable = false
case self.getEnabledState(chainId):
of UxEnabledState.Enabled:
# Iterate to check for the only chainId enabled case ...
for item in self.items:
if item.getEnabledState() == UxEnabledState.Enabled and item.getChainId() != chainId:
# ... as soon as we find another enabled chain mark this by adding it to the list
chainIds.add(chainId)
break
# ... if no other chains are enabled, then it's a transition from only chainId enabled to all enabled
if chainIds.len == 0:
for item in self.items:
if item.getChainId() != chainId:
chainIds.add(item.getChainId())
enable = true
of UxEnabledState.Disabled:
chainIds.add(chainId)
enable = true
of UxEnabledState.AllEnabled:
# disable all but chainId
for item in self.items:
if item.getChainId() != chainId:
chainIds.add(item.getChainId())
return (chainIds, enable)

View File

@ -60,12 +60,12 @@ proc checkIfModuleDidLoad(self: Module) =
method viewDidLoad*(self: Module) =
self.checkIfModuleDidLoad()
method toggleNetwork*(self: Module, chainId: int) =
self.controller.toggleNetwork(chainId)
method setNetworksState*(self: Module, chainIds: seq[int], enabled: bool) =
self.controller.setNetworksState(chainIds, enabled)
method areTestNetworksEnabled*(self: Module): bool =
method areTestNetworksEnabled*(self: Module): bool =
return self.controller.areTestNetworksEnabled()
method toggleTestNetworksEnabled*(self: Module) =
method toggleTestNetworksEnabled*(self: Module) =
self.controller.toggleTestNetworksEnabled()
self.refreshNetworks()

View File

@ -1,89 +0,0 @@
import NimQml, Tables, strutils
import ./model
# Proxy data model for the Networks data model with additional role; see isActiveRoleName
# isEnabled values are copied from the original model into isActiveRoleName values
const isActiveRoleName = "isActive"
QtObject:
type
NetworksExtraStoreProxy* = ref object of QAbstractListModel
sourceModel: Model
activeNetworks: seq[bool]
extraRole: tuple[roleId: int, roleName: string]
proc delete(self: NetworksExtraStoreProxy) =
self.sourceModel = nil
self.activeNetworks = @[]
self.QAbstractListModel.delete
proc setup(self: NetworksExtraStoreProxy) =
self.QAbstractListModel.setup
proc updateActiveNetworks(self: NetworksExtraStoreProxy, sourceModel: Model) =
var tmpSeq = newSeq[bool](sourceModel.rowCount())
for i in 0 ..< sourceModel.rowCount():
tmpSeq[i] = sourceModel.data(sourceModel.index(i, 0, newQModelIndex()), ModelRole.IsEnabled.int).boolVal()
self.activeNetworks = tmpSeq
proc newNetworksExtraStoreProxy*(sourceModel: Model): NetworksExtraStoreProxy =
new(result, delete)
result.sourceModel = sourceModel
# assign past last role element
result.extraRole = (0, isActiveRoleName)
for k in sourceModel.roleNames().keys:
if k > result.extraRole.roleId:
result.extraRole.roleId = k
result.extraRole.roleId += 1
result.updateactiveNetworks(sourceModel)
result.setup
signalConnect(result.sourceModel, "countChanged()", result, "onCountChanged()")
proc countChanged(self: NetworksExtraStoreProxy) {.signal.}
# Nimqml doesn't support connecting signals to other signals
proc onCountChanged(self: NetworksExtraStoreProxy) {.slot.} =
self.updateActiveNetworks(self.sourceModel)
self.countChanged()
proc getCount(self: NetworksExtraStoreProxy): int {.slot.} =
return self.sourceModel.rowCount()
QtProperty[int] count:
read = getCount
notify = countChanged
method rowCount(self: NetworksExtraStoreProxy, index: QModelIndex = nil): int =
return self.sourceModel.rowCount()
method roleNames(self: NetworksExtraStoreProxy): Table[int, string] =
var srcRoles = self.sourceModel.roleNames()
srcRoles.add(self.extraRole.roleId, self.extraRole.roleName)
return srcRoles
method data(self: NetworksExtraStoreProxy, index: QModelIndex, role: int): QVariant =
if role == self.extraRole.roleId:
if index.row() < 0 or index.row() >= self.activeNetworks.len:
return QVariant()
return newQVariant(self.activeNetworks[index.row()])
return self.sourceModel.data(index, role)
method setData*(self: NetworksExtraStoreProxy, index: QModelIndex, value: QVariant, role: int): bool =
if role == self.extraRole.roleId:
if index.row() < 0 or index.row() >= self.activeNetworks.len:
return false
self.activeNetworks[index.row()] = value.boolVal()
self.dataChanged(index, index, [self.extraRole.roleId])
return true
return self.sourceModel.setData(index, value, role)
proc rowData(self: NetworksExtraStoreProxy, index: int, column: string): string {.slot.} =
if column == isActiveRoleName:
if index < 0 or index >= self.activeNetworks.len:
return ""
return $self.activeNetworks[index]
return self.sourceModel.rowData(index, column)

View File

@ -3,9 +3,11 @@ import Tables, NimQml, sequtils, sugar
import ../../../../app_service/service/network/dto
import ./io_interface
import ./model
import ./networks_extra_store_proxy
import ./item
proc networkEnabledToUxEnabledState(enabled: bool, allEnabled: bool): UxEnabledState
proc areAllEnabled(networks: seq[NetworkDto]): bool
QtObject:
type
View* = ref object of QObject
@ -15,9 +17,6 @@ QtObject:
layer1: Model
layer2: Model
areTestNetworksEnabled: bool
# Lazy initized but here to keep a reference to the object not to be GCed
layer1Proxy: NetworksExtraStoreProxy
layer2Proxy: NetworksExtraStoreProxy
proc setup(self: View) =
self.QObject.setup
@ -32,8 +31,6 @@ QtObject:
result.layer1 = newModel()
result.layer2 = newModel()
result.enabled = newModel()
result.layer1Proxy = nil
result.layer2Proxy = nil
result.setup()
proc areTestNetworksEnabledChanged*(self: View) {.signal.}
@ -87,6 +84,7 @@ QtObject:
proc load*(self: View, networks: TableRef[NetworkDto, float64]) =
var items: seq[Item] = @[]
let allEnabled = areAllEnabled(toSeq(networks.keys))
for n, balance in networks.pairs:
items.add(initItem(
n.chainId,
@ -103,6 +101,8 @@ QtObject:
n.chainColor,
n.shortName,
balance,
# Ensure we mark all as enabled if all are enabled
networkEnabledToUxEnabledState(n.enabled, allEnabled)
))
self.all.setItems(items)
@ -118,34 +118,24 @@ QtObject:
self.delegate.viewDidLoad()
proc toggleNetwork*(self: View, chainId: int) {.slot.} =
self.delegate.toggleNetwork(chainId)
let (chainIds, enable) = self.all.networksToChangeStateOnUserActionFor(chainId)
self.delegate.setNetworksState(chainIds, enable)
proc toggleTestNetworksEnabled*(self: View) {.slot.} =
self.delegate.toggleTestNetworksEnabled()
self.areTestNetworksEnabled = not self.areTestNetworksEnabled
self.areTestNetworksEnabledChanged()
proc layer1ProxyChanged*(self: View) {.signal.}
proc getLayer1Proxy(self: View): QVariant {.slot.} =
if self.layer1Proxy.isNil:
self.layer1Proxy = newNetworksExtraStoreProxy(self.layer1)
return newQVariant(self.layer1Proxy)
QtProperty[QVariant] layer1Proxy:
read = getLayer1Proxy
notify = layer1ProxyChanged
proc layer2ProxyChanged*(self: View) {.signal.}
proc getLayer2Proxy(self: View): QVariant {.slot.} =
if self.layer2Proxy.isNil:
self.layer2Proxy = newNetworksExtraStoreProxy(self.layer2)
return newQVariant(self.layer2Proxy)
QtProperty[QVariant] layer2Proxy:
read = getLayer2Proxy
notify = layer2ProxyChanged
proc getMainnetChainId*(self: View): int {.slot.} =
return self.layer1.getLayer1Network(self.areTestNetworksEnabled)
proc networkEnabledToUxEnabledState(enabled: bool, allEnabled: bool): UxEnabledState =
return if allEnabled:
UxEnabledState.AllEnabled
elif enabled:
UxEnabledState.Enabled
else:
UxEnabledState.Disabled
proc areAllEnabled(networks: seq[NetworkDto]): bool =
return networks.allIt(it.enabled)

View File

@ -91,11 +91,15 @@ proc getNetwork*(self: Service, networkType: NetworkType): NetworkDto =
# Will be removed, this is used in case of legacy chain Id
return NetworkDto(chainId: networkType.toChainId())
proc toggleNetwork*(self: Service, chainId: int) =
let network = self.getNetwork(chainId)
proc setNetworksState*(self: Service, chainIds: seq[int], enabled: bool) =
for chainId in chainIds:
let network = self.getNetwork(chainId)
network.enabled = not network.enabled
self.upsertNetwork(network)
if network.enabled == enabled:
continue
network.enabled = enabled
self.upsertNetwork(network)
proc getChainIdForEns*(self: Service): int =
if self.settingsService.areTestNetworksEnabled():

View File

@ -442,8 +442,8 @@ QtObject:
self.buildAllTokens(self.getAddresses(), store = true)
self.events.emit(SIGNAL_WALLET_ACCOUNT_CURRENCY_UPDATED, CurrencyUpdated())
proc toggleNetworkEnabled*(self: Service, chainId: int) =
self.networkService.toggleNetwork(chainId)
proc setNetworksState*(self: Service, chainIds: seq[int], enabled: bool) =
self.networkService.setNetworksState(chainIds, enabled)
self.events.emit(SIGNAL_WALLET_ACCOUNT_NETWORK_ENABLED_UPDATED, NetwordkEnabledToggled())
method toggleTestNetworksEnabled*(self: Service) =

View File

@ -95,7 +95,7 @@ target_compile_definitions(QmlTests PRIVATE
QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}"
STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}"
QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/qmlTests")
target_link_libraries(QmlTests PRIVATE Qt5::QuickTest Qt5::Qml ${PROJECT_LIB})
target_link_libraries(QmlTests PRIVATE Qt5::QuickTest Qt5::Qml ${PROJECT_LIB} SortFilterProxyModel)
add_test(NAME QmlTests COMMAND QmlTests)
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/app")

View File

@ -161,6 +161,10 @@ ListModel {
title: "SelfDestructAlertPopup"
section: "Popups"
}
ListElement {
title: "NetworkSelectPopup"
section: "Popups"
}
ListElement {
title: "MembersSelector"
section: "Components"
@ -237,4 +241,8 @@ ListModel {
title: "LanguageCurrencySettings"
section: "Settings"
}
ListElement {
title: "ProfileSocialLinksPanel"
section: "Panels"
}
}

View File

@ -119,6 +119,11 @@
"LoginView": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=1080%3A313192"
],
"NetworkSelectPopup": [
"https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=13200-352357&t=jKciSCy3BVlrZmBs-0",
"https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=13185-350333&t=b2AclcJgxjXDL6Wl-0",
"https://www.figma.com/file/FkFClTCYKf83RJWoifWgoX/Wallet-v2?node-id=13187-359097&t=b2AclcJgxjXDL6Wl-0"
],
"PermissionConflictWarningPanel": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22253%3A486103&t=JrCIfks1zVzsk3vn-0"
],

View File

@ -0,0 +1,323 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Popups 0.1
import utils 1.0
import AppLayouts.Wallet.popups 1.0
import AppLayouts.Wallet.controls 1.0
import AppLayouts.stores 1.0
import Models 1.0
import SortFilterProxyModel 0.2
SplitView {
id: root
Pane {
SplitView.fillWidth: true
SplitView.fillHeight: true
ColumnLayout {
id: controlLayout
anchors.fill: parent
// Leave some space so that the popup will be opened without accounting for Layer
ColumnLayout {
Layout.maximumHeight: 50
}
NetworkFilter {
id: networkFilter
Layout.preferredWidth: 200
Layout.preferredHeight: 100
Layout.alignment: Qt.AlignHCenter
allNetworks: simulatedNimModel
layer1Networks: SortFilterProxyModel {
function rowData(index, propName) {
return get(index)[propName]
}
sourceModel: simulatedNimModel
filters: ValueFilter { roleName: "layer"; value: 1; }
}
layer2Networks: SortFilterProxyModel {
sourceModel: simulatedNimModel
filters: [ValueFilter { roleName: "layer"; value: 2; },
ValueFilter { roleName: "isTest"; value: false; }]
}
testNetworks: SortFilterProxyModel {
sourceModel: simulatedNimModel
filters: [ValueFilter { roleName: "layer"; value: 2; },
ValueFilter { roleName: "isTest"; value: true; }]
}
enabledNetworks: SortFilterProxyModel {
sourceModel: simulatedNimModel
filters: ValueFilter { roleName: "isEnabled"; value: true; }
}
onToggleNetwork: (network) => {
if(multiSelection) {
simulatedNimModel.toggleNetwork(network)
} else {
lastSingleSelectionLabel.text = `[${network.chainName}] (NL) - ID: ${network.chainId}, Icon: ${network.iconUrl}`
}
}
multiSelection: multiSelectionCheckbox.checked
isChainVisible: isChainVisibleCheckbox.checked
}
// Dummy item to make space for popup
Item {
id: popupPlaceholder
Layout.preferredWidth: networkSelectPopup.width
Layout.preferredHeight: networkSelectPopup.height
NetworkSelectPopup {
id: networkSelectPopup
layer1Networks: networkFilter.layer1Networks
layer2Networks: networkFilter.layer2Networks
testNetworks: networkFilter.testNetworks
useEnabledRole: false
visible: true
closePolicy: Popup.NoAutoClose
// Simulates a network toggle
onToggleNetwork: (network, networkModel, index) => simulatedNimModel.toggleNetwork(network)
}
}
ColumnLayout {
Layout.preferredHeight: 30
Layout.maximumHeight: 30
}
RowLayout {
Button {
text: "Single Selection Popup"
onClicked: selectPopupLoader.active = true
}
Label {
id: lastSingleSelectionLabel
text: "-"
}
}
Item {
id: singleSelectionPopupPlaceholder
Layout.preferredWidth: selectPopupLoader.item ? selectPopupLoader.item.width : 0
Layout.preferredHeight: selectPopupLoader.item ? selectPopupLoader.item.height : 0
property var currentModel: networkFilter.layer2Networks
property int currentIndex: 0
Loader {
id: selectPopupLoader
active: false
sourceComponent: NetworkSelectPopup {
layer1Networks: networkFilter.layer1Networks
layer2Networks: networkFilter.layer2Networks
testNetworks: networkFilter.testNetworks
singleSelection {
enabled: true
currentModel: singleSelectionPopupPlaceholder.currentModel
currentIndex: singleSelectionPopupPlaceholder.currentIndex
}
onToggleNetwork: (network, networkModel, index) => {
lastSingleSelectionLabel.text = `[${network.chainName}] - ID: ${network.chainId}, Icon: ${network.iconUrl}`
singleSelectionPopupPlaceholder.currentModel = networkModel
singleSelectionPopupPlaceholder.currentIndex = index
}
onClosed: selectPopupLoader.active = false
}
onLoaded: item.open()
}
}
// Vertical separator
ColumnLayout {}
}
}
Pane {
SplitView.minimumWidth: 300
SplitView.fillWidth: true
SplitView.minimumHeight: 300
ColumnLayout {
anchors.fill: parent
ListView {
id: allNetworksListView
Layout.fillWidth: true
Layout.fillHeight: true
model: simulatedNimModel
delegate: ItemDelegate {
width: allNetworksListView.width
implicitHeight: delegateRowLayout.implicitHeight
highlighted: ListView.isCurrentItem
RowLayout {
id: delegateRowLayout
anchors.fill: parent
Column {
Layout.margins: 5
spacing: 3
Label { text: model.chainName }
Row {
spacing: 5
Label { text: `<b>${model.shortName}</b>` }
Label { text: `ID <b>${model.chainId}</b>` }
CheckBox {
checkState: model.isEnabled ? Qt.Checked : Qt.Unchecked
tristate: true
nextCheckState: () => {
const nextEnabled = (checkState !== Qt.Checked)
availableNetworks.sourceModel.setProperty(availableNetworks.mapToSource(index), "isEnabled", nextEnabled)
Qt.callLater(() => { simulatedNimModel.cloneModel(availableNetworks) })
return nextEnabled ? Qt.Checked : Qt.Unchecked
}
}
}
}
}
onClicked: allNetworksListView.currentIndex = index
}
}
CheckBox {
id: multiSelectionCheckbox
Layout.margins: 5
text: "Multi Selection"
checked: true
}
CheckBox {
id: testModeCheckbox
Layout.margins: 5
text: "Test Networks Mode"
checked: false
onCheckedChanged: Qt.callLater(simulatedNimModel.cloneModel, availableNetworks)
}
CheckBox {
id: isChainVisibleCheckbox
Layout.margins: 5
text: "Is chain visible"
checked: true
}
}
}
SortFilterProxyModel {
id: availableNetworks
// Simulate Nim's way of providing access to data
function rowData(index, propName) {
return get(index)[propName]
}
sourceModel: NetworksModel.allNetworks
filters: ValueFilter { roleName: "isTest"; value: testModeCheckbox.checked; }
}
// Keep a clone so that the UX can be modified without affecting the original model
CloneModel {
id: simulatedNimModel
sourceModel: availableNetworks
roles: ["chainId", "layer", "chainName", "isTest", "isEnabled", "iconUrl", "shortName", "chainColor"]
rolesOverride: [{ role: "enabledState", transform: (mD) => {
return simulatedNimModel.areAllEnabled(sourceModel)
? NetworkSelectPopup.UxEnabledState.AllEnabled
: mD.isEnabled
? NetworkSelectPopup.UxEnabledState.Enabled
: NetworkSelectPopup.UxEnabledState.Disabled
}
}]
/// Simulate the Nim model
function toggleNetwork(network) {
const chainId = network.chainId
let chainIdOnlyEnabled = true
let chainIdOnlyDisabled = true
let allEnabled = true
for (let i = 0; i < simulatedNimModel.count; i++) {
const item = simulatedNimModel.get(i)
if(item.enabledState === NetworkSelectPopup.UxEnabledState.Enabled) {
if(item.chainId !== chainId) {
chainIdOnlyEnabled = false
}
} else if(item.enabledState === NetworkSelectPopup.UxEnabledState.Disabled) {
if(item.chainId !== chainId) {
chainIdOnlyDisabled = false
}
allEnabled = false
} else {
if(item.chainId === chainId) {
chainIdOnlyDisabled = false
chainIdOnlyEnabled = false
}
}
}
for (let i = 0; i < simulatedNimModel.count; i++) {
const item = simulatedNimModel.get(i)
if(allEnabled) {
simulatedNimModel.setProperty(i, "enabledState", item.chainId === chainId ? NetworkSelectPopup.UxEnabledState.Enabled : NetworkSelectPopup.UxEnabledState.Disabled)
} else if(chainIdOnlyEnabled || chainIdOnlyDisabled) {
simulatedNimModel.setProperty(i, "enabledState", NetworkSelectPopup.UxEnabledState.AllEnabled)
} else if(item.chainId === chainId) {
simulatedNimModel.setProperty(i, "enabledState", item.enabledState === NetworkSelectPopup.UxEnabledState.Enabled
? NetworkSelectPopup.UxEnabledState.Disabled
:NetworkSelectPopup.UxEnabledState.Enabled)
}
const haveEnabled = item.enabledState !== NetworkSelectPopup.UxEnabledState.Disabled
if(item.isEnabled !== haveEnabled) {
simulatedNimModel.setProperty(i, "isEnabled", haveEnabled)
}
}
}
function areAllEnabled(modelToCheck) {
for (let i = 0; i < modelToCheck.count; i++) {
if(!(modelToCheck.get(i).isEnabled)) {
return false
}
}
return true
}
}
}

View File

@ -5,6 +5,10 @@ import QtQuick 2.15
QtObject {
readonly property var layer1Networks: ListModel {
function rowData(index, propName) {
return get(index)[propName]
}
Component.onCompleted:
append([
{
@ -14,7 +18,8 @@ QtObject {
isActive: true,
isEnabled: true,
shortName: "ETH",
chainColor: "blue"
chainColor: "blue",
isTest: false
}
])
}
@ -29,7 +34,8 @@ QtObject {
isActive: false,
isEnabled: true,
shortName: "OPT",
chainColor: "red"
chainColor: "red",
isTest: false
},
{
chainId: 3,
@ -38,7 +44,8 @@ QtObject {
isActive: false,
isEnabled: true,
shortName: "ARB",
chainColor: "purple"
chainColor: "purple",
isTest: false
}
])
}
@ -53,7 +60,8 @@ QtObject {
isActive: false,
isEnabled: true,
shortName: "HEZ",
chainColor: "orange"
chainColor: "orange",
isTest: true
},
{
chainId: 5,
@ -62,7 +70,8 @@ QtObject {
isActive: false,
isEnabled: true,
shortName: "TNET",
chainColor: "lightblue"
chainColor: "lightblue",
isTest: true
},
{
chainId: 6,
@ -71,68 +80,180 @@ QtObject {
isActive: false,
isEnabled: true,
shortName: "CUSTOM",
chainColor: "orange"
chainColor: "orange",
isTest: true
}
])
}
readonly property var enabledNetworks: ListModel {
// Simulate Nim's way of providing access to data
function rowData(index, propName) {
return get(index)[propName]
}
Component.onCompleted:
append([
{
chainId: 1,
chainName: "Ethereum Mainnet",
iconUrl: ModelsData.networks.ethereum,
isActive: true,
isEnabled: true,
shortName: "ETH",
chainColor: "blue"
chainId: 1,
layer: 1,
chainName: "Ethereum Mainnet",
iconUrl: ModelsData.networks.ethereum,
isActive: true,
isEnabled: false,
shortName: "ETH",
chainColor: "blue",
isTest: false
},
{
chainId: 2,
chainName: "Optimism",
iconUrl: ModelsData.networks.optimism,
isActive: false,
isEnabled: true,
shortName: "OPT",
chainColor: "red"
chainId: 2,
layer: 2,
chainName: "Optimism",
iconUrl: ModelsData.networks.optimism,
isActive: false,
isEnabled: true,
shortName: "OPT",
chainColor: "red",
isTest: false
},
{
chainId: 3,
chainName: "Arbitrum",
iconUrl: ModelsData.networks.arbitrum,
isActive: false,
isEnabled: true,
shortName: "ARB",
chainColor: "purple"
chainId: 3,
layer: 2,
chainName: "Arbitrum",
iconUrl: ModelsData.networks.arbitrum,
isActive: false,
isEnabled: true,
shortName: "ARB",
chainColor: "purple",
isTest: false
},
{
chainId: 4,
chainName: "Hermez",
iconUrl: ModelsData.networks.hermez,
isActive: false,
isEnabled: true,
shortName: "HEZ",
chainColor: "orange"
chainId: 4,
layer: 2,
chainName: "Hermez",
iconUrl: ModelsData.networks.hermez,
isActive: false,
isEnabled: true,
shortName: "HEZ",
chainColor: "orange",
isTest: false
},
{
chainId: 5,
chainName: "Testnet",
iconUrl: ModelsData.networks.testnet,
isActive: false,
isEnabled: true,
shortName: "TNET",
chainColor: "lightblue"
chainId: 5,
layer: 1,
chainName: "Testnet",
iconUrl: ModelsData.networks.testnet,
isActive: false,
isEnabled: true,
shortName: "TNET",
chainColor: "lightblue",
isTest: true
},
{
chainId: 6,
chainName: "Custom",
iconUrl: ModelsData.networks.custom,
isActive: false,
isEnabled: true,
shortName: "CUSTOM",
chainColor: "orange"
chainId: 6,
layer: 1,
chainName: "Custom",
iconUrl: ModelsData.networks.custom,
isActive: false,
isEnabled: true,
shortName: "CUSTOM",
chainColor: "orange",
isTest: false
}
])
}
readonly property var allNetworks: ListModel {
// Simulate Nim's way of providing access to data
function rowData(index, propName) {
return get(index)[propName]
}
Component.onCompleted: append([
{
chainId: 1,
chainName: "Ethereum Mainnet",
blockExplorerUrl: "https://etherscan.io/",
iconUrl: "network/Network=Ethereum",
chainColor: "#627EEA",
shortName: "eth",
nativeCurrencyName: "Ether",
nativeCurrencySymbol: "ETH",
nativeCurrencyDecimals: 18,
isTest: false,
layer: 1,
isEnabled: true,
},
{
chainId: 5,
chainName: "Goerli",
blockExplorerUrl: "https://goerli.etherscan.io/",
iconUrl: "network/Network=Testnet",
chainColor: "#939BA1",
shortName: "goEth",
nativeCurrencyName: "Ether",
nativeCurrencySymbol: "ETH",
nativeCurrencyDecimals: 18,
isTest: true,
layer: 1,
isEnabled: true,
},
{
chainId: 10,
chainName: "Optimism",
blockExplorerUrl: "https://optimistic.etherscan.io",
iconUrl: "network/Network=Optimism",
chainColor: "#E90101",
shortName: "opt",
nativeCurrencyName: "Ether",
nativeCurrencySymbol: "ETH",
nativeCurrencyDecimals: 18,
isTest: false,
layer: 2,
isEnabled: true,
},
{
chainId: 420,
chainName: "Optimism Goerli Testnet",
blockExplorerUrl: "https://goerli-optimism.etherscan.io/",
iconUrl: "network/Network=Testnet",
chainColor: "#939BA1",
shortName: "goOpt",
nativeCurrencyName: "Ether",
nativeCurrencySymbol: "ETH",
nativeCurrencyDecimals: 18,
isTest: true,
layer: 2,
isEnabled: true,
},
{
chainId: 42161,
chainName: "Arbitrum",
blockExplorerUrl: "https://arbiscan.io/",
iconUrl: "network/Network=Arbitrum",
chainColor: "#51D0F0",
shortName: "arb",
nativeCurrencyName: "Ether",
nativeCurrencySymbol: "ETH",
nativeCurrencyDecimals: 18,
isTest: false,
layer: 2,
isEnabled: true,
},
{
chainId: 421613,
chainName: "Arbitrum Goerli",
blockExplorerUrl: "https://goerli.arbiscan.io/",
iconUrl: "network/Network=Testnet",
chainColor: "#939BA1",
shortName: "goArb",
nativeCurrencyName: "Ether",
nativeCurrencySymbol: "ETH",
nativeCurrencyDecimals: 18,
isTest: true,
layer: 2,
isEnabled: false,
}]
)
}
}

View File

@ -431,6 +431,11 @@ class StatusWalletScreen:
self._find_saved_address_and_open_menu(name)
click_obj_by_name(SavedAddressesScreen.EDIT.value)
# Delete existing text
type_text(AddSavedAddressPopup.NAME_INPUT.value, "<Ctrl+A>")
type_text(AddSavedAddressPopup.NAME_INPUT.value, "<Del>")
type_text(AddSavedAddressPopup.NAME_INPUT.value, new_name)
click_obj_by_name(AddSavedAddressPopup.ADD_BUTTON.value)
@ -467,19 +472,19 @@ class StatusWalletScreen:
return
assert False, "network name not found"
def click_default_wallet_account(self):
accounts = get_obj(MainWalletScreen.WALLET_ACCOUNTS_LIST.value)
click_obj(accounts.itemAtIndex(0))
def click_wallet_account(self, account_name: str):
accounts = get_obj(MainWalletScreen.WALLET_ACCOUNTS_LIST.value)
for index in range(accounts.count):
if(accounts.itemAtIndex(index).objectName == "walletAccount-" + account_name):
click_obj(accounts.itemAtIndex(index))
return
#####################################
### Verifications region:
#####################################

View File

@ -17,10 +17,10 @@ Feature: Status Desktop Wallet
Scenario Outline: The user can manage a saved address
When the user adds a saved address named "<name>" and address "<address>"
And the user edits a saved address with name "<name>" to "<new_name>"
Then the name "<new_name><name>" is in the list of saved addresses
Then the name "<new_name>" is in the list of saved addresses
When the user deletes the saved address with name "<new_name><name>"
Then the name "<new_name><name>" is not in the list of saved addresses
When the user deletes the saved address with name "<new_name>"
Then the name "<new_name>" is not in the list of saved addresses
# Test for toggling favourite button is disabled until favourite functionality is enabled
# When the user adds a saved address named "<name>" and address "<address>"

View File

@ -1,5 +1,5 @@
import QtQuick 2.14
import QtQuick.Controls 2.14
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
@ -15,6 +15,7 @@ CheckBox {
- Regular (default size)
*/
property int size: StatusCheckBox.Size.Regular
property bool changeCursor: true
enum Size {
Small,
@ -49,8 +50,9 @@ CheckBox {
x: !root.leftSide? root.rightPadding : root.leftPadding
y: parent.height / 2 - height / 2
radius: 2
color: (root.down || root.checked) ? Theme.palette.primaryColor1
: Theme.palette.directColor8
color: root.down || checkState !== Qt.Checked
? Theme.palette.directColor8
: Theme.palette.primaryColor1
StatusIcon {
icon: "checkbox"
@ -60,8 +62,8 @@ CheckBox {
? d.indicatorIconHeightRegular : d.indicatorIconHeightSmall
anchors.centerIn: parent
anchors.horizontalCenterOffset: 1
color: Theme.palette.white
visible: root.down || root.checked
color: checkState === Qt.PartiallyChecked ? Theme.palette.directColor9 : Theme.palette.white
visible: root.down || checkState !== Qt.Unchecked
}
}
@ -78,4 +80,9 @@ CheckBox {
rightPadding: !root.leftSide? (!!root.text ? root.indicator.width + root.spacing
: root.indicator.width) : 0
}
HoverHandler {
acceptedDevices: PointerDevice.Mouse
cursorShape: Qt.PointingHandCursor
}
}

View File

@ -270,18 +270,20 @@ StatusScrollView {
NetworkFilter {
Layout.preferredWidth: 160
allNetworks: root.allNetworks
layer1Networks: root.layer1Networks
layer2Networks: root.layer2Networks
testNetworks: root.testNetworks
enabledNetworks: root.enabledNetworks
allNetworks: root.allNetworks
isChainVisible: false
multiSelection: false
onSingleNetworkSelected: {
root.chainId = chainId
root.chainName = chainName
root.chainIcon = chainIcon
onToggleNetwork: (network) => {
root.chainId = network.chainId
root.chainName = network.chainName
root.chainIcon = network.iconUrl
}
}
}

View File

@ -3,6 +3,7 @@ import QtQuick 2.15
/// \note ensures data model always has consecutive Separator and Number after Base without duplicates except current element
/// \note for future work: split deleteInContent in deleteInContent and deleteElements and move data model to a DataModel object;
/// also fix code duplication in parseDerivationPath generating static level definitions and iterate through it
/// \note using Item to support embedded sub-components
Item {
id: root

View File

@ -1,4 +1,5 @@
import QtQuick 2.13
import QtQuick 2.15
import QtQuick.Controls 2.15
import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
@ -16,28 +17,44 @@ Item {
implicitWidth: 130
implicitHeight: parent.height
property var layer1Networks
property var layer2Networks
property var testNetworks
property var enabledNetworks
property var allNetworks
required property var allNetworks
required property var layer1Networks
required property var layer2Networks
required property var testNetworks
required property var enabledNetworks
property bool isChainVisible: true
property bool multiSelection: true
signal toggleNetwork(int chainId)
signal singleNetworkSelected(int chainId, string chainName, string chainIcon)
/// \c network is a network.model.nim entry
/// It is called for every toggled network if \c multiSelection is \c true
/// If \c multiSelection is \c false, it is called only for the selected network when the selection changes
signal toggleNetwork(var network)
QtObject {
id: d
property string selectedChainName: ""
property string selectedIconUrl: ""
// Persist selection between selectPopupLoader reloads
property var currentModel: layer1Networks
property int currentIndex: 0
}
Component.onCompleted: {
if (d.currentModel.count > 0) {
d.selectedChainName = d.currentModel.rowData(d.currentIndex, "chainName")
d.selectedIconUrl = d.currentModel.rowData(d.currentIndex, "iconUrl")
}
}
Item {
id: selectRectangleItem
width: parent.width
height: 56
// FIXME this should be a (styled) ComboBox
StatusListItem {
implicitWidth: parent.width
@ -52,8 +69,11 @@ Item {
statusListItemTitle.font.pixelSize: 13
statusListItemTitle.font.weight: Font.Medium
statusListItemTitle.color: Theme.palette.baseColor1
title: root.multiSelection ? (root.enabledNetworks.count === root.allNetworks.count ? qsTr("All networks") : qsTr("%n network(s)", "", root.enabledNetworks.count)) :
d.selectedChainName
title: root.multiSelection
? (root.enabledNetworks.count === root.allNetworks.count
? qsTr("All networks")
: qsTr("%n network(s)", "", root.enabledNetworks.count))
: d.selectedChainName
asset.height: 24
asset.width: asset.height
asset.isImage: !root.multiSelection
@ -66,12 +86,9 @@ Item {
color: Theme.palette.baseColor1
}
]
onClicked: {
if (selectPopup.opened) {
selectPopup.close();
} else {
selectPopup.open();
}
selectPopupLoader.active = !selectPopupLoader.active
}
}
}
@ -94,23 +111,41 @@ Item {
}
}
NetworkSelectPopup {
id: selectPopup
x: (parent.width - width + 5)
y: (selectRectangleItem.height + 5)
layer1Networks: root.layer1Networks
layer2Networks: root.layer2Networks
testNetworks: root.testNetworks
multiSelection: root.multiSelection
Loader {
id: selectPopupLoader
onToggleNetwork: {
root.toggleNetwork(network.chainId)
active: false
sourceComponent: NetworkSelectPopup {
id: selectPopup
x: -width + selectRectangleItem.width + 5
y: selectRectangleItem.height + 5
layer1Networks: root.layer1Networks
layer2Networks: root.layer2Networks
testNetworks: root.testNetworks
singleSelection {
enabled: !root.multiSelection
currentModel: d.currentModel
currentIndex: d.currentIndex
}
useEnabledRole: false
onToggleNetwork: (network, networkModel, index) => {
d.selectedChainName = network.chainName
d.selectedIconUrl = network.iconUrl
d.currentModel = networkModel
d.currentIndex = index
root.toggleNetwork(network)
}
onClosed: selectPopupLoader.active = false
}
onSingleNetworkSelected: {
d.selectedChainName = chainName
d.selectedIconUrl = iconUrl
root.singleNetworkSelected(chainId, chainName, iconUrl)
}
onLoaded: item.open()
}
}

View File

@ -0,0 +1 @@
NetworkFilter 1.0 NetworkFilter.qml

View File

@ -54,15 +54,19 @@ Item {
// network filter
NetworkFilter {
id: networkFilter
Layout.alignment: Qt.AlignTrailing
Layout.rowSpan: 2
allNetworks: walletStore.allNetworks
layer1Networks: walletStore.layer1Networks
layer2Networks: walletStore.layer2Networks
testNetworks: walletStore.testNetworks
enabledNetworks: walletStore.enabledNetworks
allNetworks: walletStore.allNetworks
onToggleNetwork: walletStore.toggleNetwork(chainId)
onToggleNetwork: (network) => {
walletStore.toggleNetwork(network.chainId)
}
}
StatusAddressPanel {

View File

@ -16,8 +16,10 @@ import StatusQ.Components 0.1
import SortFilterProxyModel 0.2
import "../controls"
import AppLayouts.stores 1.0
import "../stores"
import "../controls"
import ".."
StatusDialog {
@ -43,7 +45,7 @@ StatusDialog {
readonly property int validationMode: root.edit ?
StatusInput.ValidationMode.Always
: StatusInput.ValidationMode.OnlyWhenDirty
readonly property bool valid: addressInput.valid && nameInput.valid
readonly property bool valid: addressInput.valid && nameInput.valid && root.address !== Constants.zeroAddress
property bool chainShortNamesDirty: false
readonly property bool dirty: nameInput.input.dirty || chainShortNamesDirty
@ -51,6 +53,9 @@ StatusDialog {
readonly property string visibleAddress: root.address == Constants.zeroAddress ? "" : root.address
readonly property bool addressInputIsENS: !visibleAddress
/// Ensures that the \c root.address and \c root.chainShortNames are not reset when the initial text is set
property bool initialized: false
function getPrefixArrayWithColumns(prefixStr) {
return prefixStr.match(d.chainPrefixRegexPattern)
}
@ -73,6 +78,8 @@ StatusDialog {
}
onOpened: {
d.initialized = true
if(edit || addAddress) {
if (root.ens)
addressInput.setPlainText(root.ens)
@ -146,7 +153,7 @@ StatusDialog {
property string plainText: input.edit.getText(0, text.length)
onTextChanged: {
if (skipTextUpdate)
if (skipTextUpdate || !d.initialized)
return
plainText = input.edit.getText(0, text.length)
@ -261,7 +268,7 @@ StatusDialog {
}
onCountChanged: {
if (!networkSelector.modelUpdateBlocked) {
if (!networkSelector.modelUpdateBlocked && d.initialized) {
// Initially source model is empty, filter proxy is also empty, but does
// extra work and mistakenly overwrites root.chainShortNames property
if (sourceModel.count != 0) {
@ -310,7 +317,7 @@ StatusDialog {
}
}
onToggleNetwork: {
onToggleNetwork: (network) => {
network.isEnabled = !network.isEnabled
d.chainShortNamesDirty = true
}
@ -338,9 +345,13 @@ StatusDialog {
}
}
ListModel {
CloneModel {
id: allNetworksModelCopy
sourceModel: store.allNetworks
roles: ["layer", "chainId", "chainColor", "chainName","shortName", "iconUrl"]
rolesOverride: [{ role: "isEnabled", transform: (modelData) => Boolean(false) }]
function setEnabledNetworks(prefixArr) {
networkSelector.blockModelUpdate(true)
for (let i = 0; i < count; i++) {
@ -349,26 +360,5 @@ StatusDialog {
}
networkSelector.blockModelUpdate(false)
}
function init(model, address) {
const prefixStr = Utils.getChainsPrefix(address)
for (let i = 0; i < model.count; i++) {
const clonedItem = {
layer: model.rowData(i, "layer"),
chainId: model.rowData(i, "chainId"),
chainColor: model.rowData(i, "chainColor"),
chainName: model.rowData(i, "chainName"),
shortName: model.rowData(i, "shortName"),
iconUrl: model.rowData(i, "iconUrl"),
isEnabled: Boolean(prefixStr.length > 0 && prefixStr.includes(shortName))
}
append(clonedItem)
}
}
}
Component.onCompleted: {
allNetworksModelCopy.init(store.allNetworks, root.address)
}
}

View File

@ -1,5 +1,5 @@
import QtQuick 2.13
import QtQuick.Controls 2.13
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0
@ -7,31 +7,58 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1
import StatusQ.Controls 0.1
import StatusQ.Popups.Dialog 0.1
import utils 1.0
// TODO: replace with StatusModal
Popup {
import SortFilterProxyModel 0.2
import "./NetworkSelectPopup"
StatusDialog {
id: root
modal: false
standardButtons: Dialog.NoButton
anchors.centerIn: undefined
padding: 4
width: 360
height: Math.min(432, scrollView.contentHeight + root.padding)
implicitHeight: Math.min(432, scrollView.contentHeight + root.padding * 2)
horizontalPadding: 5
verticalPadding: 5
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent
property var layer1Networks
property var layer2Networks
property var testNetworks
required property var layer1Networks
required property var layer2Networks
property var testNetworks: null
// If true NetworksExtraStoreProxy expected for layer1Networks and layer2Networks properties
property bool useNetworksExtraStoreProxy: false
/// Grouped properties for single selection state. \c singleSelection.enabled is \c false by default
/// \see SingleSelectionInfo
property alias singleSelection: d.singleSelection
property bool multiSelection: true
property bool useEnabledRole: true
/// \c network is a network.model.nim entry. \c model and \c index for the current selection
/// It is called for every toggled network if \c singleSelection.enabled is \c false
/// If \c singleSelection.enabled is \c true, it is called only for the selected network when the selection changes
/// \see SingleSelectionInfo
signal toggleNetwork(var network, var model, int index)
/// Mirrors Nim's UxEnabledState enum from networks/item.nim
enum UxEnabledState {
Enabled,
AllEnabled,
Disabled
}
QtObject {
id: d
property SingleSelectionInfo singleSelection: SingleSelectionInfo {}
property SingleSelectionInfo tmpObject: SingleSelectionInfo { enabled: true }
}
signal toggleNetwork(var network)
signal singleNetworkSelected(int chainId, string chainName, string iconUrl)
background: Rectangle {
radius: Style.current.radius
@ -50,6 +77,7 @@ Popup {
contentItem: StatusScrollView {
id: scrollView
width: root.width
height: root.height
contentHeight: content.height
@ -65,12 +93,16 @@ Popup {
Repeater {
id: chainRepeater1
width: parent.width
height: parent.height
objectName: "networkSelectPopupChainRepeaterLayer1"
model: root.layer1Networks
delegate: chainItem
delegate: ChainItemDelegate {
networkModel: chainRepeater1.model
}
}
StatusBaseText {
@ -87,72 +119,95 @@ Popup {
Repeater {
id: chainRepeater2
model: root.layer2Networks
delegate: chainItem
model: root.layer2Networks
delegate: ChainItemDelegate {
networkModel: chainRepeater2.model
}
}
Repeater {
id: chainRepeater3
model: root.testNetworks
delegate: chainItem
delegate: ChainItemDelegate {
networkModel: chainRepeater3.model
}
}
}
}
Component {
id: chainItem
StatusListItem {
objectName: model.chainName
implicitHeight: 48
implicitWidth: scrollView.width
title: model.chainName
asset.height: 24
asset.width: 24
asset.isImage: true
asset.name: Style.svg(model.iconUrl)
onClicked: {
if(root.multiSelection)
toggleModelIsActive()
else {
// Don't allow uncheck
if(!radioButton.checked) radioButton.toggle()
}
}
component ChainItemDelegate: StatusListItem {
id: chainItemDelegate
function toggleModelIsActive() {
model.isActive = !model.isActive
}
property var networkModel: null
components: [
StatusCheckBox {
id: checkBox
visible: root.multiSelection
checked: root.useNetworksExtraStoreProxy ? model.isActive : model.isEnabled
onToggled: {
if (root.useNetworksExtraStoreProxy) {
toggleModelIsActive()
} else {
root.toggleNetwork(model)
}
}
},
StatusRadioButton {
id: radioButton
visible: !root.multiSelection
size: StatusRadioButton.Size.Large
ButtonGroup.group: radioBtnGroup
checked: model.index === 0
onCheckedChanged: {
if(checked && !root.multiSelection) {
root.singleNetworkSelected(model.chainId, model.chainName, model.iconUrl)
close()
}
}
}
]
objectName: model.chainName
implicitHeight: 48
implicitWidth: scrollView.width
title: model.chainName
asset.height: 24
asset.width: 24
asset.isImage: true
asset.name: Style.svg(model.iconUrl)
onClicked: {
if(!d.singleSelection.enabled) {
checkBox.nextCheckState()
} else if(!radioButton.checked) { // Don't allow uncheck
radioButton.toggle()
}
}
components: [
StatusCheckBox {
id: checkBox
tristate: true
visible: !d.singleSelection.enabled
checkState: {
if(root.useEnabledRole) {
return model.isEnabled ? Qt.Checked : Qt.Unchecked
} else if(model.enabledState === NetworkSelectPopup.Enabled) {
return Qt.Checked
} else {
if( model.enabledState === NetworkSelectPopup.AllEnabled) {
return Qt.PartiallyChecked
} else {
return Qt.Unchecked
}
}
}
nextCheckState: () => {
Qt.callLater(root.toggleNetwork, model, chainItemDelegate.networkModel, model.index)
return Qt.PartiallyChecked
}
},
StatusRadioButton {
id: radioButton
visible: d.singleSelection.enabled
size: StatusRadioButton.Size.Large
ButtonGroup.group: radioBtnGroup
checked: d.singleSelection.currentModel === chainItemDelegate.networkModel && d.singleSelection.currentIndex === model.index
property SingleSelectionInfo exchangeObject: null
function setNewInfo(networkModel, index) {
d.tmpObject.currentModel = networkModel
d.tmpObject.currentIndex = index
exchangeObject = d.tmpObject
d.tmpObject = d.singleSelection
d.singleSelection = exchangeObject
exchangeObject = null
}
onCheckedChanged: {
if(checked && (d.singleSelection.currentModel !== chainItemDelegate.networkModel || d.singleSelection.currentIndex !== model.index)) {
setNewInfo(chainItemDelegate.networkModel, model.index)
root.toggleNetwork(model, chainItemDelegate.networkModel, model.index)
close()
}
}
}
]
}
ButtonGroup {

View File

@ -0,0 +1,8 @@
import QtQml 2.15
/// Inline component was failing on Linux with "Cannot assign to property of unknown type" so we need to use a separate file for it.
QtObject {
property bool enabled: false
property var currentModel: root.layer1Networks
property int currentIndex: 0
}

View File

@ -16,6 +16,8 @@ import utils 1.0
import shared.controls 1.0
import shared.popups 1.0
import AppLayouts.stores 1.0
import "../stores"
StatusModal {
@ -42,15 +44,6 @@ StatusModal {
showHeader: false
showAdvancedHeader: true
// When no network is selected reset the prefix to empty string
Connections {
target: RootStore.enabledNetworks
function onModelReset() {
if(RootStore.enabledNetworks.count === 0)
root.networkPrefix = ""
}
}
hasFloatingButtons: true
advancedHeaderComponent: AccountsModalHeader {
model: RootStore.accounts
@ -97,7 +90,7 @@ StatusModal {
flow: Grid.TopToBottom
columns: need2Columns ? 2 : 1
spacing: 5
property var networkProxies: [RootStore.layer1NetworksProxy, RootStore.layer2NetworksProxy]
property var networkProxies: [layer1NetworksClone, layer2NetworksClone]
Repeater {
model: multiChainList.networkProxies.length
delegate: Repeater {
@ -106,7 +99,7 @@ StatusModal {
tagPrimaryLabel.text: model.shortName
tagPrimaryLabel.color: model.chainColor
image.source: Style.svg("tiny/" + model.iconUrl)
visible: model.isActive
visible: model.isEnabled
MouseArea {
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
@ -116,6 +109,7 @@ StatusModal {
}
}
StatusRoundButton {
id: editButton
width: 32
height: 32
icon.name: "edit_pencil"
@ -212,7 +206,7 @@ StatusModal {
font.pixelSize: 15
color: chainColor
text: shortName + ":"
visible: model.isActive
visible: model.isEnabled
onVisibleChanged: {
if (visible) {
networkPrefix += text
@ -253,15 +247,37 @@ StatusModal {
NetworkSelectPopup {
id: selectPopup
x: multiChainList.x + Style.current.xlPadding + Style.current.halfPadding
y: centralLayout.y
x: multiChainList.x + editButton.width + 9
y: tabBar.y + tabBar.height
layer1Networks: RootStore.layer1NetworksProxy
layer2Networks: RootStore.layer2NetworksProxy
testNetworks: RootStore.testNetworks
useNetworksExtraStoreProxy: true
layer1Networks: layer1NetworksClone
layer2Networks: layer2NetworksClone
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
onToggleNetwork: (network, networkModel, index) => {
network.isEnabled = !network.isEnabled
}
CloneModel {
id: layer1NetworksClone
sourceModel: RootStore.layer1Networks
roles: ["layer", "chainId", "chainColor", "chainName","shortName", "iconUrl", "isEnabled"]
// rowData used to clone returns string. Convert it to bool for bool arithmetics
rolesOverride: [{
role: "isEnabled",
transform: (modelData) => Boolean(modelData.isEnabled)
}]
}
CloneModel {
id: layer2NetworksClone
sourceModel: RootStore.layer2Networks
roles: layer1NetworksClone.roles
rolesOverride: layer1NetworksClone.rolesOverride
}
}
states: [

View File

@ -0,0 +1 @@
NetworkSelectPopup 1.0 NetworkSelectPopup.qml

View File

@ -65,8 +65,6 @@ QtObject {
onAllNetworksChanged: {
d.initChainColors(allNetworks)
}
property var layer1NetworksProxy: networksModule.layer1Proxy
property var layer2NetworksProxy: networksModule.layer2Proxy
property var cryptoRampServicesModel: walletSectionBuySellCrypto.model

View File

@ -0,0 +1,55 @@
import QtQuick 2.15
/// Helper item to clone a model and alter its data without affecting the original model
/// \beware this is not a proxy model. It clones the initial state
/// and every time the instance changes and doesn't adapt when the data
/// in the source model \c allNetworksModel changes
/// \beware use it with small models and in temporary views (e.g. popups)
/// \note requires `rowData` to be implemented in the model
/// \note tried to use SortFilterProxyModel with but it complicates implementation too much
ListModel {
id: root
required property var sourceModel
/// Roles to clone
required property var roles
/// Roles to override or add of the form { role: "roleName", transform: function(modelData) { return newValue } }
property var rolesOverride: []
Component.onCompleted: cloneModel(sourceModel)
onSourceModelChanged: cloneModel(sourceModel)
function rowData(index, roleName) {
return get(index)[roleName]
}
function findIndexForRole(roleName, value) {
for (let i = 0; i < count; i++) {
if(get(i)[roleName] === value) {
return i
}
}
return -1
}
function cloneModel(model) {
clear()
if (!model) {
console.warn("Missing valid data model to clone. The CloneModel is useless")
return
}
for (let i = 0; i < model.count; i++) {
const clonedItem = new Object()
for (var propName of roles) {
clonedItem[propName] = model.rowData(i, propName)
}
for (var newProp of rolesOverride) {
clonedItem[newProp.role] = newProp.transform(clonedItem)
}
append(clonedItem)
}
}
}

View File

@ -1 +1,2 @@
RootStore 1.0 RootStore.qml
CloneModel 1.0 CloneModel.qml