diff --git a/src/app/modules/main/networks/controller.nim b/src/app/modules/main/networks/controller.nim
index aba88b9d47..dd8a3bb50e 100644
--- a/src/app/modules/main/networks/controller.nim
+++ b/src/app/modules/main/networks/controller.nim
@@ -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()
diff --git a/src/app/modules/main/networks/io_interface.nim b/src/app/modules/main/networks/io_interface.nim
index 1a8a936939..7d44b2f19e 100644
--- a/src/app/modules/main/networks/io_interface.nim
+++ b/src/app/modules/main/networks/io_interface.nim
@@ -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.} =
diff --git a/src/app/modules/main/networks/item.nim b/src/app/modules/main/networks/item.nim
index 937f1ca072..91549ade56 100644
--- a/src/app/modules/main/networks/item.nim
+++ b/src/app/modules/main/networks/item.nim
@@ -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
diff --git a/src/app/modules/main/networks/model.nim b/src/app/modules/main/networks/model.nim
index 07fea97629..a33743ddc2 100644
--- a/src/app/modules/main/networks/model.nim
+++ b/src/app/modules/main/networks/model.nim
@@ -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)
\ No newline at end of file
diff --git a/src/app/modules/main/networks/module.nim b/src/app/modules/main/networks/module.nim
index f689a0e29d..414632e92c 100644
--- a/src/app/modules/main/networks/module.nim
+++ b/src/app/modules/main/networks/module.nim
@@ -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()
\ No newline at end of file
diff --git a/src/app/modules/main/networks/networks_extra_store_proxy.nim b/src/app/modules/main/networks/networks_extra_store_proxy.nim
deleted file mode 100644
index e250cbcd63..0000000000
--- a/src/app/modules/main/networks/networks_extra_store_proxy.nim
+++ /dev/null
@@ -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)
\ No newline at end of file
diff --git a/src/app/modules/main/networks/view.nim b/src/app/modules/main/networks/view.nim
index c969e75801..625afb223c 100644
--- a/src/app/modules/main/networks/view.nim
+++ b/src/app/modules/main/networks/view.nim
@@ -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)
diff --git a/src/app_service/service/network/service.nim b/src/app_service/service/network/service.nim
index 408c879f72..7c9deb425f 100644
--- a/src/app_service/service/network/service.nim
+++ b/src/app_service/service/network/service.nim
@@ -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():
diff --git a/src/app_service/service/wallet_account/service.nim b/src/app_service/service/wallet_account/service.nim
index 43b57139c3..92f8d7700e 100644
--- a/src/app_service/service/wallet_account/service.nim
+++ b/src/app_service/service/wallet_account/service.nim
@@ -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) =
diff --git a/storybook/CMakeLists.txt b/storybook/CMakeLists.txt
index bc6e9993cf..b199be63cb 100644
--- a/storybook/CMakeLists.txt
+++ b/storybook/CMakeLists.txt
@@ -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")
diff --git a/storybook/PagesModel.qml b/storybook/PagesModel.qml
index f798768535..c9ec83c45f 100644
--- a/storybook/PagesModel.qml
+++ b/storybook/PagesModel.qml
@@ -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"
+ }
}
diff --git a/storybook/figma.json b/storybook/figma.json
index 99aa93181a..86433f534d 100644
--- a/storybook/figma.json
+++ b/storybook/figma.json
@@ -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"
],
diff --git a/storybook/pages/NetworkSelectPopupPage.qml b/storybook/pages/NetworkSelectPopupPage.qml
new file mode 100644
index 0000000000..4984d4024e
--- /dev/null
+++ b/storybook/pages/NetworkSelectPopupPage.qml
@@ -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: `${model.shortName}` }
+ Label { text: `ID ${model.chainId}` }
+ 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
+ }
+ }
+}
diff --git a/storybook/src/Models/NetworksModel.qml b/storybook/src/Models/NetworksModel.qml
index 0202715054..4a7a192cb8 100644
--- a/storybook/src/Models/NetworksModel.qml
+++ b/storybook/src/Models/NetworksModel.qml
@@ -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,
+ }]
+ )
+ }
}
diff --git a/test/ui-test/src/screens/StatusWalletScreen.py b/test/ui-test/src/screens/StatusWalletScreen.py
index 83910b9ef8..676b566bee 100644
--- a/test/ui-test/src/screens/StatusWalletScreen.py
+++ b/test/ui-test/src/screens/StatusWalletScreen.py
@@ -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, "")
+ type_text(AddSavedAddressPopup.NAME_INPUT.value, "")
+
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:
#####################################
diff --git a/test/ui-test/testSuites/suite_wallet/tst_wallet/test.feature b/test/ui-test/testSuites/suite_wallet/tst_wallet/test.feature
index 3006c4ee02..12411758f3 100644
--- a/test/ui-test/testSuites/suite_wallet/tst_wallet/test.feature
+++ b/test/ui-test/testSuites/suite_wallet/tst_wallet/test.feature
@@ -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 "" and address ""
And the user edits a saved address with name "" to ""
- Then the name "" is in the list of saved addresses
+ Then the name "" is in the list of saved addresses
- When the user deletes the saved address with name ""
- Then the name "" is not in the list of saved addresses
+ When the user deletes the saved address with name ""
+ Then the 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 "" and address ""
diff --git a/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml b/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml
index 805875f132..1cd9dc88c3 100644
--- a/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml
+++ b/ui/StatusQ/src/StatusQ/Controls/StatusCheckBox.qml
@@ -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
+ }
}
diff --git a/ui/app/AppLayouts/Chat/views/communities/CommunityNewCollectibleView.qml b/ui/app/AppLayouts/Chat/views/communities/CommunityNewCollectibleView.qml
index a497b0cd39..a531fba743 100644
--- a/ui/app/AppLayouts/Chat/views/communities/CommunityNewCollectibleView.qml
+++ b/ui/app/AppLayouts/Chat/views/communities/CommunityNewCollectibleView.qml
@@ -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
}
}
}
diff --git a/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathInput/Controller.qml b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathInput/Controller.qml
index 2f02c3229a..28b6aad8eb 100644
--- a/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathInput/Controller.qml
+++ b/ui/app/AppLayouts/Wallet/addaccount/panels/DerivationPathInput/Controller.qml
@@ -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
diff --git a/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml b/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml
index 876832a8d6..c704185015 100644
--- a/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml
+++ b/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml
@@ -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()
}
}
diff --git a/ui/app/AppLayouts/Wallet/controls/qmldir b/ui/app/AppLayouts/Wallet/controls/qmldir
new file mode 100644
index 0000000000..0210b212df
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/controls/qmldir
@@ -0,0 +1 @@
+NetworkFilter 1.0 NetworkFilter.qml
\ No newline at end of file
diff --git a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml
index f551058bd5..4c5e1e04fd 100644
--- a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml
+++ b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml
@@ -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 {
diff --git a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml
index dd7318d734..de0103f742 100644
--- a/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml
+++ b/ui/app/AppLayouts/Wallet/popups/AddEditSavedAddressPopup.qml
@@ -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)
}
}
diff --git a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml
index d0a773e039..7089c6916e 100644
--- a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml
+++ b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml
@@ -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 {
diff --git a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup/SingleSelectionInfo.qml b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup/SingleSelectionInfo.qml
new file mode 100644
index 0000000000..edb1bd52c0
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup/SingleSelectionInfo.qml
@@ -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
+}
diff --git a/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml b/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml
index 554bb409a9..17b1850cfc 100644
--- a/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml
+++ b/ui/app/AppLayouts/Wallet/popups/ReceiveModal.qml
@@ -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: [
diff --git a/ui/app/AppLayouts/Wallet/popups/qmldir b/ui/app/AppLayouts/Wallet/popups/qmldir
new file mode 100644
index 0000000000..62cd384798
--- /dev/null
+++ b/ui/app/AppLayouts/Wallet/popups/qmldir
@@ -0,0 +1 @@
+NetworkSelectPopup 1.0 NetworkSelectPopup.qml
\ No newline at end of file
diff --git a/ui/app/AppLayouts/Wallet/stores/RootStore.qml b/ui/app/AppLayouts/Wallet/stores/RootStore.qml
index aabf1c8efe..926725519c 100644
--- a/ui/app/AppLayouts/Wallet/stores/RootStore.qml
+++ b/ui/app/AppLayouts/Wallet/stores/RootStore.qml
@@ -65,8 +65,6 @@ QtObject {
onAllNetworksChanged: {
d.initChainColors(allNetworks)
}
- property var layer1NetworksProxy: networksModule.layer1Proxy
- property var layer2NetworksProxy: networksModule.layer2Proxy
property var cryptoRampServicesModel: walletSectionBuySellCrypto.model
diff --git a/ui/app/AppLayouts/stores/CloneModel.qml b/ui/app/AppLayouts/stores/CloneModel.qml
new file mode 100644
index 0000000000..7cb095f732
--- /dev/null
+++ b/ui/app/AppLayouts/stores/CloneModel.qml
@@ -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)
+ }
+ }
+}
\ No newline at end of file
diff --git a/ui/app/AppLayouts/stores/qmldir b/ui/app/AppLayouts/stores/qmldir
index 4efbc79d9b..cdbd554d5a 100644
--- a/ui/app/AppLayouts/stores/qmldir
+++ b/ui/app/AppLayouts/stores/qmldir
@@ -1 +1,2 @@
RootStore 1.0 RootStore.qml
+CloneModel 1.0 CloneModel.qml