diff --git a/storybook/pages/NetworkFilterPage.qml b/storybook/pages/NetworkFilterPage.qml index 893895c8b4..3c8b349e42 100644 --- a/storybook/pages/NetworkFilterPage.qml +++ b/storybook/pages/NetworkFilterPage.qml @@ -23,6 +23,18 @@ SplitView { orientation: Qt.Vertical SplitView.fillWidth: true + QtObject { + id: d + + property SortFilterProxyModel networksModel: SortFilterProxyModel { + sourceModel: NetworksModel.flatNetworks + filters: IndexFilter { + minimumIndex: 0 + maximumIndex: singleActiveNetworkCheckBox.checked ? 0 : -1 + } + } + } + Item { id: container @@ -34,11 +46,11 @@ SplitView { anchors.centerIn: parent - flatNetworks: NetworksModel.flatNetworks + flatNetworks: d.networksModel multiSelection: multiSelectionCheckBox.checked - showAllSelectedText: ctrlShowAllSelectedText.checked showTitle: ctrlShowTitle.checked + showManageNetworksButton: ctrlShowManageNetworksButton.checked selectionAllowed: selectionAllowedCheckBox.checked showSelectionIndicator: (ctrlShowCheckBoxes.checked && multiSelection) || (ctrlShowRadioButtons.checked && !multiSelection) } @@ -88,9 +100,8 @@ SplitView { } CheckBox { - id: ctrlShowAllSelectedText - text: "Show 'All networks' text" - visible: multiSelectionCheckBox.checked + id: ctrlShowManageNetworksButton + text: "Show 'Manage networks' button" checked: true } @@ -100,6 +111,12 @@ SplitView { checked: true } + CheckBox { + id: singleActiveNetworkCheckBox + text: "Single active network" + checked: false + } + ColumnLayout { visible: !multiSelectionCheckBox.checked Label { diff --git a/storybook/pages/NetworkSelectPopupPage.qml b/storybook/pages/NetworkSelectPopupPage.qml index d6e3c2059a..294a945dd4 100644 --- a/storybook/pages/NetworkSelectPopupPage.qml +++ b/storybook/pages/NetworkSelectPopupPage.qml @@ -5,6 +5,7 @@ import QtQuick.Layouts 1.15 import StatusQ 0.1 import StatusQ.Core 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 import StatusQ.Popups 0.1 import utils 1.0 @@ -30,39 +31,61 @@ SplitView { anchors.fill: parent - // Leave some space so that the popup will be opened without accounting for Layer - ColumnLayout { - Layout.maximumHeight: 50 - } + RowLayout { + Layout.fillWidth: true - NetworkFilter { - id: networkFilter + // Dummy item to make space for popup + Item { + id: popupPlaceholder - Layout.alignment: Qt.AlignHCenter + Layout.preferredWidth: networkSelectPopup.implicitWidth + Layout.preferredHeight: networkSelectPopup.implicitHeight + Layout.alignment: Qt.AlignLeft | Qt.AlignTop - flatNetworks: availableNetworks - multiSelection: multiSelectionCheckbox.checked - } + NetworkSelectPopup { + id: networkSelectPopup + flatNetworks: d.activeNetworks + multiSelection: multiSelectionCheckbox.checked + showManageNetworksButton: showManageNetworksButtonCheckbox.checked + closePolicy: Popup.NoAutoClose + visible: true + Binding on selection { + value: d.selection + } + onToggleNetwork: d.toggleNetworkEnabled(chainId) + } + } - // Dummy item to make space for popup - Item { - id: popupPlaceholder + // Filler + ColumnLayout { + Layout.preferredHeight: 100 + Layout.maximumHeight: 100 + } - Layout.preferredWidth: networkSelectPopup.implicitWidth - Layout.preferredHeight: networkSelectPopup.implicitHeight + Item { + id: filterPlaceholder - NetworkSelectPopup { - id: networkSelectPopup - flatNetworks: availableNetworks - multiSelection: multiSelectionCheckbox.checked - closePolicy: Popup.NoAutoClose - visible: true + Layout.preferredWidth: networkFilter.implicitWidth + Layout.preferredHeight: networkFilter.implicitHeight + Layout.alignment: Qt.AlignRight | Qt.AlignTop + + NetworkFilter { + id: networkFilter + + flatNetworks: d.activeNetworks + multiSelection: multiSelectionCheckbox.checked + showManageNetworksButton: showManageNetworksButtonCheckbox.checked + Binding on selection { + value: d.selection + } + onToggleNetwork: d.toggleNetworkEnabled(chainId) + } } } ColumnLayout { - Layout.preferredHeight: 30 - Layout.maximumHeight: 30 + Layout.preferredHeight: 100 + Layout.maximumHeight: 100 } RowLayout { @@ -77,7 +100,7 @@ SplitView { ModelEntry { id: selectedEntry - sourceModel: availableNetworks + sourceModel: d.activeNetworks key: "chainId" } } @@ -97,7 +120,7 @@ SplitView { active: false sourceComponent: NetworkSelectPopup { - flatNetworks: availableNetworks + flatNetworks: d.activeNetworks selection: selectedEntry.available ? [selectedEntry.value] : [] onClosed: selectPopupLoader.active = false @@ -117,7 +140,7 @@ SplitView { } } Pane { - SplitView.minimumWidth: 300 + SplitView.minimumWidth: 400 SplitView.fillWidth: true SplitView.minimumHeight: 300 @@ -130,7 +153,7 @@ SplitView { Layout.fillWidth: true Layout.fillHeight: true - model: availableNetworks + model: d.availableNetworks delegate: ItemDelegate { required property var model @@ -144,28 +167,26 @@ SplitView { id: delegateRowLayout anchors.fill: parent - Column { + ColumnLayout { Layout.margins: 5 spacing: 3 Label { text: model.chainName } - Row { + RowLayout { spacing: 5 Label { text: `${model.shortName}` } Label { text: `ID ${model.chainId}` } CheckBox { - checkState: networkSelectPopup.selection.includes(model.chainId) ? Qt.Checked : Qt.Unchecked - onToggled: { - let currentSelection = networkSelectPopup.selection - if (checkState === Qt.Checked && !currentSelection.includes(model.chainId)) { - currentSelection.push(model.chainId) - } else { - currentSelection = currentSelection.filter(id => id !== model.chainId) - } - networkSelectPopup.selection = [...currentSelection] - } + text: "Enabled" + checkState: model.isEnabled ? Qt.Checked : Qt.Unchecked + onToggled: d.setIsEnabled(model.chainId, checkState === Qt.Checked) + } + CheckBox { + text: "Active" + checkState: model.isActive ? Qt.Checked : Qt.Unchecked + onToggled: d.setIsActive(model.chainId, checkState === Qt.Checked) } } } @@ -181,6 +202,16 @@ SplitView { checked: true } + CheckBox { + id: showManageNetworksButtonCheckbox + + Layout.margins: 5 + + text: "Show 'Manage networks' button" + checked: true + } + + CheckBox { id: testModeCheckbox @@ -201,12 +232,55 @@ SplitView { } } - SortFilterProxyModel { - id: availableNetworks + QtObject { + id: d - sourceModel: NetworksModel.flatNetworks - filters: ValueFilter { roleName: "isTest"; value: testModeCheckbox.checked; } + function toggleNetworkEnabled(chainId) { + let isEnabled = ModelUtils.getByKey(NetworksModel.flatNetworks, "chainId", chainId, "isEnabled") + d.setIsEnabled(chainId, !isEnabled) + } + function setIsEnabled(chainId, value) { + let index = ModelUtils.indexOf(NetworksModel.flatNetworks, "chainId", chainId) + NetworksModel.flatNetworks.setProperty(index, "isEnabled", value) + } + function setIsActive(chainId, value) { + let index = ModelUtils.indexOf(NetworksModel.flatNetworks, "chainId", chainId) + NetworksModel.flatNetworks.setProperty(index, "isActive", value) + } + + readonly property var availableNetworks: SortFilterProxyModel { + sourceModel: NetworksModel.flatNetworks + filters: [ + ValueFilter { roleName: "isTest"; value: testModeCheckbox.checked; } + ] + } + + readonly property var activeNetworks: SortFilterProxyModel { + sourceModel: d.availableNetworks + filters: [ + ValueFilter { roleName: "isActive"; value: true; } + ] + } + + readonly property var enabledNetworks: SortFilterProxyModel { + sourceModel: d.activeNetworks + filters: [ + ValueFilter { roleName: "isEnabled"; value: true; } + ] + } + + readonly property var chainIdsAggregator: FunctionAggregator { + model: d.enabledNetworks + initialValue: [] + roleName: "chainId" + + aggregateFunction: (aggr, value) => [...aggr, value] + } + + readonly property var selection: d.chainIdsAggregator.value } + + } // category: Popups diff --git a/storybook/qmlTests/tests/tst_NetworkSelectPopup.qml b/storybook/qmlTests/tests/tst_NetworkSelectPopup.qml index 763a2c4da0..0f9255f9e3 100644 --- a/storybook/qmlTests/tests/tst_NetworkSelectPopup.qml +++ b/storybook/qmlTests/tests/tst_NetworkSelectPopup.qml @@ -10,7 +10,7 @@ import Models 1.0 Item { id: root width: 600 - height: 400 + height: 600 Component { id: componentUnderTest @@ -69,7 +69,7 @@ Item { // multi selection - select using the view const thirdDelegate = findChild(controlUnderTest.contentItem, "networkSelectorDelegate_" + controlUnderTest.flatNetworks.get(2).chainName) mouseClick(thirdDelegate) - compare(controlUnderTest.selection, [controlUnderTest.flatNetworks.get(0).chainId, controlUnderTest.flatNetworks.get(1).chainId, controlUnderTest.flatNetworks.get(2).chainId]) + compare(controlUnderTest.selection.sort(), [controlUnderTest.flatNetworks.get(0).chainId, controlUnderTest.flatNetworks.get(1).chainId, controlUnderTest.flatNetworks.get(2).chainId].sort()) compare(selectionChangedSpy.count, 4) } diff --git a/storybook/qmlTests/tests/tst_NetworkSelectorView.qml b/storybook/qmlTests/tests/tst_NetworkSelectorView.qml index 3595e282c0..8c373a8a4c 100644 --- a/storybook/qmlTests/tests/tst_NetworkSelectorView.qml +++ b/storybook/qmlTests/tests/tst_NetworkSelectorView.qml @@ -165,8 +165,8 @@ Item { compare(toggleNetworkSpy.count, 1) compare(selectionChangedSpy.count, 2) compare(controlUnderTest.selection.length, 2) - compare(controlUnderTest.selection[0], controlUnderTest.model.get(1).chainId) - compare(controlUnderTest.selection[1], controlUnderTest.model.get(2).chainId) + verify(controlUnderTest.selection.includes(controlUnderTest.model.get(1).chainId)) + verify(controlUnderTest.selection.includes(controlUnderTest.model.get(2).chainId)) compare(delegate.checkState, Qt.Checked) delegate = findChild(controlUnderTest, "networkSelectorDelegate_" + controlUnderTest.model.get(2).chainName) compare(delegate.checkState, Qt.Checked) diff --git a/ui/app/AppLayouts/Profile/views/WalletView.qml b/ui/app/AppLayouts/Profile/views/WalletView.qml index 2dc0fddb64..9097f4bb29 100644 --- a/ui/app/AppLayouts/Profile/views/WalletView.qml +++ b/ui/app/AppLayouts/Profile/views/WalletView.qml @@ -119,6 +119,8 @@ SettingsContentBase { root.settingsSubSubsection === Constants.walletSettingsSubsection.manageHidden || root.settingsSubSubsection === Constants.walletSettingsSubsection.manageAdvanced + readonly property bool isManageNetworksSubsection: root.settingsSubSubsection === Constants.walletSettingsSubsection.manageNetworks + readonly property var walletSettings: Settings { category: "walletSettings-" + root.myPublicKey } @@ -143,6 +145,12 @@ SettingsContentBase { restoreMode: Binding.RestoreNone } + Binding on currentIndex { + value: root.networksViewIndex + when: root.settingsSubSubsection === Constants.walletSettingsSubsection.manageNetworks + restoreMode: Binding.RestoreNone + } + onCurrentIndexChanged: { root.rootStore.backButtonName = "" root.sectionTitle = root.walletSectionTitle diff --git a/ui/app/AppLayouts/Wallet/WalletLayout.qml b/ui/app/AppLayouts/Wallet/WalletLayout.qml index 8d7789b009..f25963b1c0 100644 --- a/ui/app/AppLayouts/Wallet/WalletLayout.qml +++ b/ui/app/AppLayouts/Wallet/WalletLayout.qml @@ -49,6 +49,8 @@ Item { signal dappListRequested() signal dappConnectRequested() signal dappDisconnectRequested(string dappUrl) + + signal manageNetworksRequested() // TODO: remove tokenType parameter from signals below signal sendTokenRequested(string senderAddress, string tokenId, int tokenType) @@ -272,6 +274,8 @@ Item { onLaunchBuyCryptoModal: d.launchBuyCryptoModal() onSendTokenRequested: root.sendTokenRequested(senderAddress, tokenId, tokenType) + + onManageNetworksRequested: root.manageNetworksRequested() } } diff --git a/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml b/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml index 013fe20d95..5e6e3b19d2 100644 --- a/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml +++ b/ui/app/AppLayouts/Wallet/controls/NetworkFilter.qml @@ -30,12 +30,13 @@ StatusComboBox { property bool multiSelection: true property bool showSelectionIndicator: true - property bool showAllSelectedText: true property bool showTitle: true property bool selectionAllowed: true + property bool showManageNetworksButton: false property var selection: [] signal toggleNetwork(int chainId, int index) + signal manageNetworksClicked() onSelectionChanged: { if (root.selection !== networkSelectorView.selection) { @@ -81,7 +82,7 @@ StatusComboBox { Row { id: row spacing: -4 - visible: (!d.allSelected || !root.showAllSelectedText) && chainRepeater.count > 0 + visible: chainRepeater.count > 0 Repeater { id: chainRepeater model: SortFilterProxyModel { @@ -152,6 +153,7 @@ StatusComboBox { selectionAllowed: root.selectionAllowed multiSelection: root.multiSelection showSelectionIndicator: root.showSelectionIndicator + showManageNetworksButton: root.showManageNetworksButton selection: root.selection onSelectionChanged: { @@ -161,6 +163,11 @@ StatusComboBox { } onToggleNetwork: root.toggleNetwork(chainId, index) + + onManageNetworksClicked: { + control.popup.close() + root.manageNetworksClicked() + } } Connections { @@ -175,7 +182,6 @@ StatusComboBox { QtObject { id: d readonly property int networksCount: root.flatNetworks.ModelCount.count - readonly property bool allSelected: root.selection.length === networksCount readonly property bool noneSelected: root.selection.length === 0 readonly property bool oneSelected: root.selection.length === 1 readonly property bool selectionUnavailable: d.networksCount <= 1 && d.oneSelected @@ -198,9 +204,6 @@ StatusComboBox { if (d.noneSelected) { return qsTr("Select networks") } - if (d.allSelected && root.showAllSelectedText) { - return qsTr("All networks") - } } return "" diff --git a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml index 7369aafdb6..5610a04330 100644 --- a/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml +++ b/ui/app/AppLayouts/Wallet/panels/WalletHeader.qml @@ -41,6 +41,7 @@ Item { signal dappListRequested() signal dappConnectRequested() signal dappDisconnectRequested(string dappUrl) + signal manageNetworksRequested() implicitHeight: 88 @@ -177,11 +178,14 @@ Item { // network filter NetworkFilter { id: networkFilter + showTitle: false + showManageNetworksButton: true Layout.alignment: Qt.AlignTop flatNetworks: root.walletStore.filteredFlatModel onToggleNetwork: root.walletStore.toggleNetwork(chainId) + onManageNetworksClicked: root.manageNetworksRequested() Binding on selection { value: chainIdsAggregator.value diff --git a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml index a39ff4e96f..5426ea69df 100644 --- a/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml +++ b/ui/app/AppLayouts/Wallet/popups/NetworkSelectPopup.qml @@ -1,9 +1,11 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 import QtGraphicalEffects 1.15 import StatusQ 0.1 import StatusQ.Core.Theme 0.1 +import StatusQ.Controls 0.1 import StatusQ.Popups.Dialog 0.1 import SortFilterProxyModel 0.2 @@ -21,9 +23,11 @@ Popup { property bool showSelectionIndicator: true property bool selectionAllowed: true property bool multiSelection: false + property bool showManageNetworksButton: false property var selection: [] signal toggleNetwork(int chainId, int index) + signal manageNetworksClicked() onSelectionChanged: { if (root.selection !== scrollView.selection) { @@ -51,25 +55,44 @@ Popup { } } - contentItem: NetworkSelectorView { - id: scrollView + contentItem: ColumnLayout { + anchors.left: parent.left + anchors.right: parent.right + anchors.margins: 4 - model: root.flatNetworks - interactive: root.selectionAllowed - multiSelection: root.multiSelection - showIndicator: root.showSelectionIndicator - selection: root.selection + NetworkSelectorView { + id: scrollView + Layout.fillWidth: true - onSelectionChanged: { - if (root.selection !== scrollView.selection) { - root.selection = scrollView.selection + model: root.flatNetworks + interactive: root.selectionAllowed + multiSelection: root.multiSelection + showIndicator: root.showSelectionIndicator + selection: root.selection + + onSelectionChanged: { + if (root.selection !== scrollView.selection) { + root.selection = scrollView.selection + } + } + + onToggleNetwork: { + if (!root.multiSelection && root.closePolicy !== Popup.NoAutoClose) + root.close() + root.toggleNetwork(chainId, index) } } - onToggleNetwork: { - if (!root.multiSelection && root.closePolicy !== Popup.NoAutoClose) - root.close() - root.toggleNetwork(chainId, index) + StatusButton { + id: manageNetworksButton + visible: root.showManageNetworksButton + Layout.fillWidth: true + Layout.margins: 4 + + icon.name: "settings" + text: qsTr("Manage networks") + isOutline: true + onClicked: root.manageNetworksClicked() } } } diff --git a/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml b/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml index cb5122f39c..9bc86a516c 100644 --- a/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml +++ b/ui/app/AppLayouts/Wallet/views/NetworkSelectorView.qml @@ -38,26 +38,10 @@ StatusListView { objectName: "networkSelectorList" - onSelectionChanged: { - if (!root.multiSelection && selection.length > 1) { - console.warn("Warning: Multi-selection is disabled, but multiple items are selected. Automatically selecting the first inserted item.") - selection = [selection[0]] - } - } + onSelectionChanged: d.reprocessSelection() + onMultiSelectionChanged: d.reprocessSelection() + Component.onCompleted: d.reprocessSelection() - onMultiSelectionChanged: { - if (root.multiSelection) return; - - // When changing the multi-selection mode, we need to ensure that the selection is valid - if (root.selection.length > 1) { - root.selection = [root.selection[0]] - } - - // Single selection defaults to first item if no selection is made - if (root.selection.length === 0 && root.count > 0) { - root.selection = [ModelUtils.get(root.model, 0).chainId] - } - } implicitWidth: 300 implicitHeight: contentHeight @@ -99,7 +83,9 @@ StatusListView { QtObject { id: d - readonly property bool allSelected: root.selection.length === root.count + readonly property int networksCount: root.model.ModelCount.count + onNetworksCountChanged: d.reprocessSelection() + readonly property bool allSelected: root.selection.length === networksCount function onToggled(initialState, chainId) { let selection = root.selection @@ -114,12 +100,27 @@ StatusListView { root.selection = [...selection] } - } - Component.onCompleted: { - if(root.selection.length === 0 && !root.multiSelection && root.count > 0) { - const firstChain = ModelUtils.get(root.model, 0).chainId - root.selection = [firstChain] + function reprocessSelection() { + let selection = root.selection + + if (d.networksCount === 0) { + selection = [] + } else { + if (!root.multiSelection) { + // One and only one chain must be selected + if (selection.length === 0) { + selection = [ModelUtils.get(root.model, 0, "chainId")] + } else if (selection.length > 1) { + console.warn("Warning: Multi-selection is disabled, but multiple items are selected. Automatically selecting the first inserted item.") + selection = [selection[0]] + } + } + } + + if (root.selection.sort().join(',') !== selection.sort().join(',')) { + root.selection = [...selection] + } } } } diff --git a/ui/app/AppLayouts/Wallet/views/RightTabBaseView.qml b/ui/app/AppLayouts/Wallet/views/RightTabBaseView.qml index 1dcc15aaac..52f0f4771b 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabBaseView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabBaseView.qml @@ -34,6 +34,7 @@ FocusScope { signal dappListRequested() signal dappConnectRequested() signal dappDisconnectRequested(string dappUrl) + signal manageNetworksRequested() ColumnLayout { anchors.fill: parent @@ -53,6 +54,7 @@ FocusScope { onDappListRequested: root.dappListRequested() onDappConnectRequested: root.dappConnectRequested() onDappDisconnectRequested: (dappUrl) =>root.dappDisconnectRequested(dappUrl) + onManageNetworksRequested: root.manageNetworksRequested() } Item { diff --git a/ui/app/AppLayouts/Wallet/views/RightTabView.qml b/ui/app/AppLayouts/Wallet/views/RightTabView.qml index 8408c3168f..f0e27d7cc8 100644 --- a/ui/app/AppLayouts/Wallet/views/RightTabView.qml +++ b/ui/app/AppLayouts/Wallet/views/RightTabView.qml @@ -40,6 +40,11 @@ RightTabBaseView { signal launchSwapModal(string tokensKey) signal sendTokenRequested(string senderAddress, string tokenId, int tokenType) + onManageNetworksRequested: { + Global.changeAppSectionBySectionType(Constants.appSection.profile, + Constants.settingsSubsection.wallet, + Constants.walletSettingsSubsection.manageNetworks) + } function resetView() { resetStack() diff --git a/ui/imports/shared/popups/walletconnect/private/ContextCard.qml b/ui/imports/shared/popups/walletconnect/private/ContextCard.qml index 9de9c4e6a2..408b2da176 100644 --- a/ui/imports/shared/popups/walletconnect/private/ContextCard.qml +++ b/ui/imports/shared/popups/walletconnect/private/ContextCard.qml @@ -82,7 +82,6 @@ Rectangle { flatNetworks: root.chainsModel showTitle: true multiSelection: root.multipleChainSelection - showAllSelectedText: false selectionAllowed: false // disable interactions w/o looking disabled