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] = proc getNetworks*(self: Controller): seq[NetworkDto] =
return self.networkService.getNetworks() return self.networkService.getNetworks()
proc toggleNetwork*(self: Controller, chainId: int) = proc setNetworksState*(self: Controller, chainIds: seq[int], enabled: bool) =
self.walletAccountService.toggleNetworkEnabled(chainId) self.walletAccountService.setNetworksState(chainIds, enabled)
proc areTestNetworksEnabled*(self: Controller): bool = proc areTestNetworksEnabled*(self: Controller): bool =
return self.settingsService.areTestNetworksEnabled() return self.settingsService.areTestNetworksEnabled()

View File

@ -19,7 +19,7 @@ method isLoaded*(self: AccessInterface): bool {.base.} =
method viewDidLoad*(self: AccessInterface) {.base.} = method viewDidLoad*(self: AccessInterface) {.base.} =
raise newException(ValueError, "No implementation available") 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") raise newException(ValueError, "No implementation available")
method refreshNetworks*(self: AccessInterface) {.base.} = method refreshNetworks*(self: AccessInterface) {.base.} =

View File

@ -1,5 +1,11 @@
import strformat import strformat
type
UxEnabledState* {.pure.} = enum
Enabled
AllEnabled
Disabled
type type
Item* = object Item* = object
chainId: int chainId: int
@ -16,6 +22,7 @@ type
chainColor: string chainColor: string
shortName: string shortName: string
balance: float64 balance: float64
enabledState: UxEnabledState
proc initItem*( proc initItem*(
chainId: int, chainId: int,
@ -32,6 +39,7 @@ proc initItem*(
chainColor: string, chainColor: string,
shortName: string, shortName: string,
balance: float64, balance: float64,
enabledState: UxEnabledState,
): Item = ): Item =
result.chainId = chainId result.chainId = chainId
result.nativeCurrencyDecimals = nativeCurrencyDecimals result.nativeCurrencyDecimals = nativeCurrencyDecimals
@ -47,6 +55,7 @@ proc initItem*(
result.chainColor = chainColor result.chainColor = chainColor
result.shortName = shortName result.shortName = shortName
result.balance = balance result.balance = balance
result.enabledState = enabledState
proc `$`*(self: Item): string = proc `$`*(self: Item): string =
result = fmt"""NetworkItem( result = fmt"""NetworkItem(
@ -64,6 +73,7 @@ proc `$`*(self: Item): string =
shortName: {self.shortName}, shortName: {self.shortName},
chainColor: {self.chainColor}, chainColor: {self.chainColor},
balance: {self.balance}, balance: {self.balance},
enabledState: {self.enabledState}
]""" ]"""
proc getChainId*(self: Item): int = proc getChainId*(self: Item): int =
@ -94,7 +104,7 @@ proc getIsTest*(self: Item): bool =
return self.isTest return self.isTest
proc getIsEnabled*(self: Item): bool = proc getIsEnabled*(self: Item): bool =
return self.isEnabled return self.isEnabled
proc getIconURL*(self: Item): string = proc getIconURL*(self: Item): string =
return self.iconUrl return self.iconUrl
@ -107,3 +117,6 @@ proc getChainColor*(self: Item): string =
proc getBalance*(self: Item): float64 = proc getBalance*(self: Item): float64 =
return self.balance 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 import ./item
@ -20,6 +20,7 @@ type
ChainColor ChainColor
ShortName ShortName
Balance Balance
EnabledState
QtObject: QtObject:
type type
@ -69,6 +70,7 @@ QtObject:
ModelRole.ShortName.int: "shortName", ModelRole.ShortName.int: "shortName",
ModelRole.ChainColor.int: "chainColor", ModelRole.ChainColor.int: "chainColor",
ModelRole.Balance.int: "balance", ModelRole.Balance.int: "balance",
ModelRole.EnabledState.int: "enabledState",
}.toTable }.toTable
method data(self: Model, index: QModelIndex, role: int): QVariant = method data(self: Model, index: QModelIndex, role: int): QVariant =
@ -110,6 +112,8 @@ QtObject:
result = newQVariant(item.getChainColor()) result = newQVariant(item.getChainColor())
of ModelRole.Balance: of ModelRole.Balance:
result = newQVariant(item.getBalance()) result = newQVariant(item.getBalance())
of ModelRole.EnabledState:
result = newQVariant(item.getEnabledState().int)
proc rowData*(self: Model, index: int, column: string): string {.slot.} = proc rowData*(self: Model, index: int, column: string): string {.slot.} =
if (index >= self.items.len): if (index >= self.items.len):
@ -130,6 +134,7 @@ QtObject:
of "chainColor": result = $item.getChainColor() of "chainColor": result = $item.getChainColor()
of "shortName": result = $item.getShortName() of "shortName": result = $item.getShortName()
of "balance": result = $item.getBalance() of "balance": result = $item.getBalance()
of "enabledState": result = $item.getEnabledState().int
proc setItems*(self: Model, items: seq[Item]) = proc setItems*(self: Model, items: seq[Item]) =
self.beginResetModel() self.beginResetModel()
@ -171,7 +176,7 @@ QtObject:
for item in self.items: for item in self.items:
if cmpIgnoreCase(item.getShortName(), shortName) == 0: if cmpIgnoreCase(item.getShortName(), shortName) == 0:
return item.getChainName() return item.getChainName()
return "" return ""
proc getNetworkColor*(self: Model, shortName: string): string {.slot.} = proc getNetworkColor*(self: Model, shortName: string): string {.slot.} =
for item in self.items: for item in self.items:
@ -196,3 +201,41 @@ QtObject:
if(item.getChainId() == chainId): if(item.getChainId() == chainId):
return item.getBlockExplorerURL() & EXPLORER_TX_PREFIX return item.getBlockExplorerURL() & EXPLORER_TX_PREFIX
return "" 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) = method viewDidLoad*(self: Module) =
self.checkIfModuleDidLoad() self.checkIfModuleDidLoad()
method toggleNetwork*(self: Module, chainId: int) = method setNetworksState*(self: Module, chainIds: seq[int], enabled: bool) =
self.controller.toggleNetwork(chainId) self.controller.setNetworksState(chainIds, enabled)
method areTestNetworksEnabled*(self: Module): bool = method areTestNetworksEnabled*(self: Module): bool =
return self.controller.areTestNetworksEnabled() return self.controller.areTestNetworksEnabled()
method toggleTestNetworksEnabled*(self: Module) = method toggleTestNetworksEnabled*(self: Module) =
self.controller.toggleTestNetworksEnabled() self.controller.toggleTestNetworksEnabled()
self.refreshNetworks() 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 ../../../../app_service/service/network/dto
import ./io_interface import ./io_interface
import ./model import ./model
import ./networks_extra_store_proxy
import ./item import ./item
proc networkEnabledToUxEnabledState(enabled: bool, allEnabled: bool): UxEnabledState
proc areAllEnabled(networks: seq[NetworkDto]): bool
QtObject: QtObject:
type type
View* = ref object of QObject View* = ref object of QObject
@ -15,9 +17,6 @@ QtObject:
layer1: Model layer1: Model
layer2: Model layer2: Model
areTestNetworksEnabled: bool 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) = proc setup(self: View) =
self.QObject.setup self.QObject.setup
@ -32,8 +31,6 @@ QtObject:
result.layer1 = newModel() result.layer1 = newModel()
result.layer2 = newModel() result.layer2 = newModel()
result.enabled = newModel() result.enabled = newModel()
result.layer1Proxy = nil
result.layer2Proxy = nil
result.setup() result.setup()
proc areTestNetworksEnabledChanged*(self: View) {.signal.} proc areTestNetworksEnabledChanged*(self: View) {.signal.}
@ -87,6 +84,7 @@ QtObject:
proc load*(self: View, networks: TableRef[NetworkDto, float64]) = proc load*(self: View, networks: TableRef[NetworkDto, float64]) =
var items: seq[Item] = @[] var items: seq[Item] = @[]
let allEnabled = areAllEnabled(toSeq(networks.keys))
for n, balance in networks.pairs: for n, balance in networks.pairs:
items.add(initItem( items.add(initItem(
n.chainId, n.chainId,
@ -103,6 +101,8 @@ QtObject:
n.chainColor, n.chainColor,
n.shortName, n.shortName,
balance, balance,
# Ensure we mark all as enabled if all are enabled
networkEnabledToUxEnabledState(n.enabled, allEnabled)
)) ))
self.all.setItems(items) self.all.setItems(items)
@ -118,34 +118,24 @@ QtObject:
self.delegate.viewDidLoad() self.delegate.viewDidLoad()
proc toggleNetwork*(self: View, chainId: int) {.slot.} = 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.} = proc toggleTestNetworksEnabled*(self: View) {.slot.} =
self.delegate.toggleTestNetworksEnabled() self.delegate.toggleTestNetworksEnabled()
self.areTestNetworksEnabled = not self.areTestNetworksEnabled self.areTestNetworksEnabled = not self.areTestNetworksEnabled
self.areTestNetworksEnabledChanged() 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.} = proc getMainnetChainId*(self: View): int {.slot.} =
return self.layer1.getLayer1Network(self.areTestNetworksEnabled) 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 # Will be removed, this is used in case of legacy chain Id
return NetworkDto(chainId: networkType.toChainId()) return NetworkDto(chainId: networkType.toChainId())
proc toggleNetwork*(self: Service, chainId: int) = proc setNetworksState*(self: Service, chainIds: seq[int], enabled: bool) =
let network = self.getNetwork(chainId) for chainId in chainIds:
let network = self.getNetwork(chainId)
network.enabled = not network.enabled if network.enabled == enabled:
self.upsertNetwork(network) continue
network.enabled = enabled
self.upsertNetwork(network)
proc getChainIdForEns*(self: Service): int = proc getChainIdForEns*(self: Service): int =
if self.settingsService.areTestNetworksEnabled(): if self.settingsService.areTestNetworksEnabled():

View File

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

View File

@ -95,7 +95,7 @@ target_compile_definitions(QmlTests PRIVATE
QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}" QML_IMPORT_ROOT="${CMAKE_CURRENT_LIST_DIR}"
STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}" STATUSQ_MODULE_IMPORT_PATH="${STATUSQ_MODULE_IMPORT_PATH}"
QUICK_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}/qmlTests") 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) add_test(NAME QmlTests COMMAND QmlTests)
list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/app") list(APPEND QML_DIRS "${CMAKE_SOURCE_DIR}/../ui/app")

View File

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

View File

@ -119,6 +119,11 @@
"LoginView": [ "LoginView": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=1080%3A313192" "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": [ "PermissionConflictWarningPanel": [
"https://www.figma.com/file/17fc13UBFvInrLgNUKJJg5/Kuba%E2%8E%9CDesktop?node-id=22253%3A486103&t=JrCIfks1zVzsk3vn-0" "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 { QtObject {
readonly property var layer1Networks: ListModel { readonly property var layer1Networks: ListModel {
function rowData(index, propName) {
return get(index)[propName]
}
Component.onCompleted: Component.onCompleted:
append([ append([
{ {
@ -14,7 +18,8 @@ QtObject {
isActive: true, isActive: true,
isEnabled: true, isEnabled: true,
shortName: "ETH", shortName: "ETH",
chainColor: "blue" chainColor: "blue",
isTest: false
} }
]) ])
} }
@ -29,7 +34,8 @@ QtObject {
isActive: false, isActive: false,
isEnabled: true, isEnabled: true,
shortName: "OPT", shortName: "OPT",
chainColor: "red" chainColor: "red",
isTest: false
}, },
{ {
chainId: 3, chainId: 3,
@ -38,7 +44,8 @@ QtObject {
isActive: false, isActive: false,
isEnabled: true, isEnabled: true,
shortName: "ARB", shortName: "ARB",
chainColor: "purple" chainColor: "purple",
isTest: false
} }
]) ])
} }
@ -53,7 +60,8 @@ QtObject {
isActive: false, isActive: false,
isEnabled: true, isEnabled: true,
shortName: "HEZ", shortName: "HEZ",
chainColor: "orange" chainColor: "orange",
isTest: true
}, },
{ {
chainId: 5, chainId: 5,
@ -62,7 +70,8 @@ QtObject {
isActive: false, isActive: false,
isEnabled: true, isEnabled: true,
shortName: "TNET", shortName: "TNET",
chainColor: "lightblue" chainColor: "lightblue",
isTest: true
}, },
{ {
chainId: 6, chainId: 6,
@ -71,68 +80,180 @@ QtObject {
isActive: false, isActive: false,
isEnabled: true, isEnabled: true,
shortName: "CUSTOM", shortName: "CUSTOM",
chainColor: "orange" chainColor: "orange",
isTest: true
} }
]) ])
} }
readonly property var enabledNetworks: ListModel { readonly property var enabledNetworks: ListModel {
// Simulate Nim's way of providing access to data
function rowData(index, propName) {
return get(index)[propName]
}
Component.onCompleted: Component.onCompleted:
append([ append([
{ {
chainId: 1, chainId: 1,
chainName: "Ethereum Mainnet", layer: 1,
iconUrl: ModelsData.networks.ethereum, chainName: "Ethereum Mainnet",
isActive: true, iconUrl: ModelsData.networks.ethereum,
isEnabled: true, isActive: true,
shortName: "ETH", isEnabled: false,
chainColor: "blue" shortName: "ETH",
chainColor: "blue",
isTest: false
}, },
{ {
chainId: 2, chainId: 2,
chainName: "Optimism", layer: 2,
iconUrl: ModelsData.networks.optimism, chainName: "Optimism",
isActive: false, iconUrl: ModelsData.networks.optimism,
isEnabled: true, isActive: false,
shortName: "OPT", isEnabled: true,
chainColor: "red" shortName: "OPT",
chainColor: "red",
isTest: false
}, },
{ {
chainId: 3, chainId: 3,
chainName: "Arbitrum", layer: 2,
iconUrl: ModelsData.networks.arbitrum, chainName: "Arbitrum",
isActive: false, iconUrl: ModelsData.networks.arbitrum,
isEnabled: true, isActive: false,
shortName: "ARB", isEnabled: true,
chainColor: "purple" shortName: "ARB",
chainColor: "purple",
isTest: false
}, },
{ {
chainId: 4, chainId: 4,
chainName: "Hermez", layer: 2,
iconUrl: ModelsData.networks.hermez, chainName: "Hermez",
isActive: false, iconUrl: ModelsData.networks.hermez,
isEnabled: true, isActive: false,
shortName: "HEZ", isEnabled: true,
chainColor: "orange" shortName: "HEZ",
chainColor: "orange",
isTest: false
}, },
{ {
chainId: 5, chainId: 5,
chainName: "Testnet", layer: 1,
iconUrl: ModelsData.networks.testnet, chainName: "Testnet",
isActive: false, iconUrl: ModelsData.networks.testnet,
isEnabled: true, isActive: false,
shortName: "TNET", isEnabled: true,
chainColor: "lightblue" shortName: "TNET",
chainColor: "lightblue",
isTest: true
}, },
{ {
chainId: 6, chainId: 6,
chainName: "Custom", layer: 1,
iconUrl: ModelsData.networks.custom, chainName: "Custom",
isActive: false, iconUrl: ModelsData.networks.custom,
isEnabled: true, isActive: false,
shortName: "CUSTOM", isEnabled: true,
chainColor: "orange" 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) self._find_saved_address_and_open_menu(name)
click_obj_by_name(SavedAddressesScreen.EDIT.value) 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) type_text(AddSavedAddressPopup.NAME_INPUT.value, new_name)
click_obj_by_name(AddSavedAddressPopup.ADD_BUTTON.value) click_obj_by_name(AddSavedAddressPopup.ADD_BUTTON.value)
@ -467,19 +472,19 @@ class StatusWalletScreen:
return return
assert False, "network name not found" assert False, "network name not found"
def click_default_wallet_account(self): def click_default_wallet_account(self):
accounts = get_obj(MainWalletScreen.WALLET_ACCOUNTS_LIST.value) accounts = get_obj(MainWalletScreen.WALLET_ACCOUNTS_LIST.value)
click_obj(accounts.itemAtIndex(0)) click_obj(accounts.itemAtIndex(0))
def click_wallet_account(self, account_name: str): def click_wallet_account(self, account_name: str):
accounts = get_obj(MainWalletScreen.WALLET_ACCOUNTS_LIST.value) accounts = get_obj(MainWalletScreen.WALLET_ACCOUNTS_LIST.value)
for index in range(accounts.count): for index in range(accounts.count):
if(accounts.itemAtIndex(index).objectName == "walletAccount-" + account_name): if(accounts.itemAtIndex(index).objectName == "walletAccount-" + account_name):
click_obj(accounts.itemAtIndex(index)) click_obj(accounts.itemAtIndex(index))
return return
##################################### #####################################
### Verifications region: ### Verifications region:
##################################### #####################################

View File

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

View File

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

View File

@ -270,18 +270,20 @@ StatusScrollView {
NetworkFilter { NetworkFilter {
Layout.preferredWidth: 160 Layout.preferredWidth: 160
allNetworks: root.allNetworks
layer1Networks: root.layer1Networks layer1Networks: root.layer1Networks
layer2Networks: root.layer2Networks layer2Networks: root.layer2Networks
testNetworks: root.testNetworks testNetworks: root.testNetworks
enabledNetworks: root.enabledNetworks enabledNetworks: root.enabledNetworks
allNetworks: root.allNetworks
isChainVisible: false isChainVisible: false
multiSelection: false multiSelection: false
onSingleNetworkSelected: { onToggleNetwork: (network) => {
root.chainId = chainId root.chainId = network.chainId
root.chainName = chainName root.chainName = network.chainName
root.chainIcon = chainIcon 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 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; /// \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 /// also fix code duplication in parseDerivationPath generating static level definitions and iterate through it
/// \note using Item to support embedded sub-components
Item { Item {
id: root 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 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
@ -16,28 +17,44 @@ Item {
implicitWidth: 130 implicitWidth: 130
implicitHeight: parent.height implicitHeight: parent.height
property var layer1Networks required property var allNetworks
property var layer2Networks required property var layer1Networks
property var testNetworks required property var layer2Networks
property var enabledNetworks required property var testNetworks
property var allNetworks required property var enabledNetworks
property bool isChainVisible: true property bool isChainVisible: true
property bool multiSelection: true property bool multiSelection: true
signal toggleNetwork(int chainId) /// \c network is a network.model.nim entry
signal singleNetworkSelected(int chainId, string chainName, string chainIcon) /// 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 { QtObject {
id: d id: d
property string selectedChainName: "" property string selectedChainName: ""
property string selectedIconUrl: "" 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 { Item {
id: selectRectangleItem id: selectRectangleItem
width: parent.width width: parent.width
height: 56 height: 56
// FIXME this should be a (styled) ComboBox // FIXME this should be a (styled) ComboBox
StatusListItem { StatusListItem {
implicitWidth: parent.width implicitWidth: parent.width
@ -52,8 +69,11 @@ Item {
statusListItemTitle.font.pixelSize: 13 statusListItemTitle.font.pixelSize: 13
statusListItemTitle.font.weight: Font.Medium statusListItemTitle.font.weight: Font.Medium
statusListItemTitle.color: Theme.palette.baseColor1 statusListItemTitle.color: Theme.palette.baseColor1
title: root.multiSelection ? (root.enabledNetworks.count === root.allNetworks.count ? qsTr("All networks") : qsTr("%n network(s)", "", root.enabledNetworks.count)) : title: root.multiSelection
d.selectedChainName ? (root.enabledNetworks.count === root.allNetworks.count
? qsTr("All networks")
: qsTr("%n network(s)", "", root.enabledNetworks.count))
: d.selectedChainName
asset.height: 24 asset.height: 24
asset.width: asset.height asset.width: asset.height
asset.isImage: !root.multiSelection asset.isImage: !root.multiSelection
@ -66,12 +86,9 @@ Item {
color: Theme.palette.baseColor1 color: Theme.palette.baseColor1
} }
] ]
onClicked: { onClicked: {
if (selectPopup.opened) { selectPopupLoader.active = !selectPopupLoader.active
selectPopup.close();
} else {
selectPopup.open();
}
} }
} }
} }
@ -94,23 +111,41 @@ Item {
} }
} }
NetworkSelectPopup { Loader {
id: selectPopup id: selectPopupLoader
x: (parent.width - width + 5)
y: (selectRectangleItem.height + 5)
layer1Networks: root.layer1Networks
layer2Networks: root.layer2Networks
testNetworks: root.testNetworks
multiSelection: root.multiSelection
onToggleNetwork: { active: false
root.toggleNetwork(network.chainId)
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: { onLoaded: item.open()
d.selectedChainName = chainName
d.selectedIconUrl = iconUrl
root.singleNetworkSelected(chainId, chainName, iconUrl)
}
} }
} }

View File

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

View File

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

View File

@ -16,8 +16,10 @@ import StatusQ.Components 0.1
import SortFilterProxyModel 0.2 import SortFilterProxyModel 0.2
import "../controls" import AppLayouts.stores 1.0
import "../stores" import "../stores"
import "../controls"
import ".." import ".."
StatusDialog { StatusDialog {
@ -43,7 +45,7 @@ StatusDialog {
readonly property int validationMode: root.edit ? readonly property int validationMode: root.edit ?
StatusInput.ValidationMode.Always StatusInput.ValidationMode.Always
: StatusInput.ValidationMode.OnlyWhenDirty : 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 property bool chainShortNamesDirty: false
readonly property bool dirty: nameInput.input.dirty || chainShortNamesDirty 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 string visibleAddress: root.address == Constants.zeroAddress ? "" : root.address
readonly property bool addressInputIsENS: !visibleAddress 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) { function getPrefixArrayWithColumns(prefixStr) {
return prefixStr.match(d.chainPrefixRegexPattern) return prefixStr.match(d.chainPrefixRegexPattern)
} }
@ -73,6 +78,8 @@ StatusDialog {
} }
onOpened: { onOpened: {
d.initialized = true
if(edit || addAddress) { if(edit || addAddress) {
if (root.ens) if (root.ens)
addressInput.setPlainText(root.ens) addressInput.setPlainText(root.ens)
@ -146,7 +153,7 @@ StatusDialog {
property string plainText: input.edit.getText(0, text.length) property string plainText: input.edit.getText(0, text.length)
onTextChanged: { onTextChanged: {
if (skipTextUpdate) if (skipTextUpdate || !d.initialized)
return return
plainText = input.edit.getText(0, text.length) plainText = input.edit.getText(0, text.length)
@ -261,7 +268,7 @@ StatusDialog {
} }
onCountChanged: { onCountChanged: {
if (!networkSelector.modelUpdateBlocked) { if (!networkSelector.modelUpdateBlocked && d.initialized) {
// Initially source model is empty, filter proxy is also empty, but does // Initially source model is empty, filter proxy is also empty, but does
// extra work and mistakenly overwrites root.chainShortNames property // extra work and mistakenly overwrites root.chainShortNames property
if (sourceModel.count != 0) { if (sourceModel.count != 0) {
@ -310,7 +317,7 @@ StatusDialog {
} }
} }
onToggleNetwork: { onToggleNetwork: (network) => {
network.isEnabled = !network.isEnabled network.isEnabled = !network.isEnabled
d.chainShortNamesDirty = true d.chainShortNamesDirty = true
} }
@ -338,9 +345,13 @@ StatusDialog {
} }
} }
ListModel { CloneModel {
id: allNetworksModelCopy id: allNetworksModelCopy
sourceModel: store.allNetworks
roles: ["layer", "chainId", "chainColor", "chainName","shortName", "iconUrl"]
rolesOverride: [{ role: "isEnabled", transform: (modelData) => Boolean(false) }]
function setEnabledNetworks(prefixArr) { function setEnabledNetworks(prefixArr) {
networkSelector.blockModelUpdate(true) networkSelector.blockModelUpdate(true)
for (let i = 0; i < count; i++) { for (let i = 0; i < count; i++) {
@ -349,26 +360,5 @@ StatusDialog {
} }
networkSelector.blockModelUpdate(false) 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 2.15
import QtQuick.Controls 2.13 import QtQuick.Controls 2.15
import QtQuick.Layouts 1.3 import QtQuick.Layouts 1.3
import QtGraphicalEffects 1.0 import QtGraphicalEffects 1.0
@ -7,31 +7,58 @@ import StatusQ.Core 0.1
import StatusQ.Core.Theme 0.1 import StatusQ.Core.Theme 0.1
import StatusQ.Components 0.1 import StatusQ.Components 0.1
import StatusQ.Controls 0.1 import StatusQ.Controls 0.1
import StatusQ.Popups.Dialog 0.1
import utils 1.0 import utils 1.0
// TODO: replace with StatusModal import SortFilterProxyModel 0.2
Popup {
import "./NetworkSelectPopup"
StatusDialog {
id: root id: root
modal: false modal: false
standardButtons: Dialog.NoButton
anchors.centerIn: undefined
padding: 4
width: 360 width: 360
height: Math.min(432, scrollView.contentHeight + root.padding) implicitHeight: Math.min(432, scrollView.contentHeight + root.padding * 2)
horizontalPadding: 5 closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
verticalPadding: 5
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutsideParent required property var layer1Networks
property var layer1Networks required property var layer2Networks
property var layer2Networks property var testNetworks: null
property var testNetworks
// If true NetworksExtraStoreProxy expected for layer1Networks and layer2Networks properties /// Grouped properties for single selection state. \c singleSelection.enabled is \c false by default
property bool useNetworksExtraStoreProxy: false /// \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 { background: Rectangle {
radius: Style.current.radius radius: Style.current.radius
@ -50,6 +77,7 @@ Popup {
contentItem: StatusScrollView { contentItem: StatusScrollView {
id: scrollView id: scrollView
width: root.width width: root.width
height: root.height height: root.height
contentHeight: content.height contentHeight: content.height
@ -65,12 +93,16 @@ Popup {
Repeater { Repeater {
id: chainRepeater1 id: chainRepeater1
width: parent.width width: parent.width
height: parent.height height: parent.height
objectName: "networkSelectPopupChainRepeaterLayer1" objectName: "networkSelectPopupChainRepeaterLayer1"
model: root.layer1Networks model: root.layer1Networks
delegate: chainItem delegate: ChainItemDelegate {
networkModel: chainRepeater1.model
}
} }
StatusBaseText { StatusBaseText {
@ -87,72 +119,95 @@ Popup {
Repeater { Repeater {
id: chainRepeater2 id: chainRepeater2
model: root.layer2Networks
delegate: chainItem model: root.layer2Networks
delegate: ChainItemDelegate {
networkModel: chainRepeater2.model
}
} }
Repeater { Repeater {
id: chainRepeater3 id: chainRepeater3
model: root.testNetworks model: root.testNetworks
delegate: ChainItemDelegate {
delegate: chainItem networkModel: chainRepeater3.model
}
} }
} }
} }
Component { component ChainItemDelegate: StatusListItem {
id: chainItem id: chainItemDelegate
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()
}
}
function toggleModelIsActive() { property var networkModel: null
model.isActive = !model.isActive
}
components: [ objectName: model.chainName
StatusCheckBox { implicitHeight: 48
id: checkBox implicitWidth: scrollView.width
visible: root.multiSelection title: model.chainName
checked: root.useNetworksExtraStoreProxy ? model.isActive : model.isEnabled asset.height: 24
onToggled: { asset.width: 24
if (root.useNetworksExtraStoreProxy) { asset.isImage: true
toggleModelIsActive() asset.name: Style.svg(model.iconUrl)
} else { onClicked: {
root.toggleNetwork(model) if(!d.singleSelection.enabled) {
} checkBox.nextCheckState()
} } else if(!radioButton.checked) { // Don't allow uncheck
}, radioButton.toggle()
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()
}
}
}
]
} }
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 { 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.controls 1.0
import shared.popups 1.0 import shared.popups 1.0
import AppLayouts.stores 1.0
import "../stores" import "../stores"
StatusModal { StatusModal {
@ -42,15 +44,6 @@ StatusModal {
showHeader: false showHeader: false
showAdvancedHeader: true 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 hasFloatingButtons: true
advancedHeaderComponent: AccountsModalHeader { advancedHeaderComponent: AccountsModalHeader {
model: RootStore.accounts model: RootStore.accounts
@ -97,7 +90,7 @@ StatusModal {
flow: Grid.TopToBottom flow: Grid.TopToBottom
columns: need2Columns ? 2 : 1 columns: need2Columns ? 2 : 1
spacing: 5 spacing: 5
property var networkProxies: [RootStore.layer1NetworksProxy, RootStore.layer2NetworksProxy] property var networkProxies: [layer1NetworksClone, layer2NetworksClone]
Repeater { Repeater {
model: multiChainList.networkProxies.length model: multiChainList.networkProxies.length
delegate: Repeater { delegate: Repeater {
@ -106,7 +99,7 @@ StatusModal {
tagPrimaryLabel.text: model.shortName tagPrimaryLabel.text: model.shortName
tagPrimaryLabel.color: model.chainColor tagPrimaryLabel.color: model.chainColor
image.source: Style.svg("tiny/" + model.iconUrl) image.source: Style.svg("tiny/" + model.iconUrl)
visible: model.isActive visible: model.isEnabled
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
@ -116,6 +109,7 @@ StatusModal {
} }
} }
StatusRoundButton { StatusRoundButton {
id: editButton
width: 32 width: 32
height: 32 height: 32
icon.name: "edit_pencil" icon.name: "edit_pencil"
@ -212,7 +206,7 @@ StatusModal {
font.pixelSize: 15 font.pixelSize: 15
color: chainColor color: chainColor
text: shortName + ":" text: shortName + ":"
visible: model.isActive visible: model.isEnabled
onVisibleChanged: { onVisibleChanged: {
if (visible) { if (visible) {
networkPrefix += text networkPrefix += text
@ -253,15 +247,37 @@ StatusModal {
NetworkSelectPopup { NetworkSelectPopup {
id: selectPopup id: selectPopup
x: multiChainList.x + Style.current.xlPadding + Style.current.halfPadding x: multiChainList.x + editButton.width + 9
y: centralLayout.y y: tabBar.y + tabBar.height
layer1Networks: RootStore.layer1NetworksProxy layer1Networks: layer1NetworksClone
layer2Networks: RootStore.layer2NetworksProxy layer2Networks: layer2NetworksClone
testNetworks: RootStore.testNetworks
useNetworksExtraStoreProxy: true
closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside 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: [ states: [

View File

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

View File

@ -65,8 +65,6 @@ QtObject {
onAllNetworksChanged: { onAllNetworksChanged: {
d.initChainColors(allNetworks) d.initChainColors(allNetworks)
} }
property var layer1NetworksProxy: networksModule.layer1Proxy
property var layer2NetworksProxy: networksModule.layer2Proxy
property var cryptoRampServicesModel: walletSectionBuySellCrypto.model 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 RootStore 1.0 RootStore.qml
CloneModel 1.0 CloneModel.qml