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