From 01a24e1b196b44e0f3ca2579d29919b103cc933d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Cie=C5=9Blak?= Date: Fri, 30 Aug 2024 17:31:53 +0200 Subject: [PATCH] TokenSelectorNew decomposed into smaller, reusable subcomponents --- .../src/StatusQ/Core/Utils/SearchFilter.qml | 9 + ui/StatusQ/src/StatusQ/Core/Utils/qmldir | 1 + ui/StatusQ/src/statusq.qrc | 3 +- .../Communities/popups/InDropdown.qml | 6 +- .../Wallet/controls/TokenSelectorNew.qml | 4 +- .../Wallet/panels/SearchableAssetsPanel.qml | 94 ++++++ .../panels/SearchableCollectiblesPanel.qml | 230 +++++++++++++ .../Wallet/panels/TokenSearchBox.qml | 10 + .../Wallet/panels/TokenSelectorPanel.qml | 308 +----------------- ui/app/AppLayouts/Wallet/panels/qmldir | 6 +- 10 files changed, 366 insertions(+), 305 deletions(-) create mode 100644 ui/StatusQ/src/StatusQ/Core/Utils/SearchFilter.qml create mode 100644 ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml create mode 100644 ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml create mode 100644 ui/app/AppLayouts/Wallet/panels/TokenSearchBox.qml diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/SearchFilter.qml b/ui/StatusQ/src/StatusQ/Core/Utils/SearchFilter.qml new file mode 100644 index 0000000000..528a7c9ff0 --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Core/Utils/SearchFilter.qml @@ -0,0 +1,9 @@ +import SortFilterProxyModel 0.2 + +RegExpFilter { + required property string searchPhrase + + pattern: `*${searchPhrase}*` + caseSensitivity: Qt.CaseInsensitive + syntax: RegExpFilter.Wildcard +} diff --git a/ui/StatusQ/src/StatusQ/Core/Utils/qmldir b/ui/StatusQ/src/StatusQ/Core/Utils/qmldir index 0a5d269245..8bfaec233d 100644 --- a/ui/StatusQ/src/StatusQ/Core/Utils/qmldir +++ b/ui/StatusQ/src/StatusQ/Core/Utils/qmldir @@ -9,6 +9,7 @@ ModelChangeGuard 0.1 ModelChangeGuard.qml ModelChangeTracker 0.1 ModelChangeTracker.qml ModelsComparator 0.1 ModelsComparator.qml QObject 0.1 QObject.qml +SearchFilter 0.1 SearchFilter.qml StackViewStates 0.1 StackViewStates.qml StatesStack 0.1 StatesStack.qml Subscription 0.1 Subscription.qml diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index 85f5f9eadf..2d5c697140 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -195,7 +195,6 @@ StatusQ/Core/Utils/ClippingWrapper.qml StatusQ/Core/Utils/DoubleFlickable.qml StatusQ/Core/Utils/DoubleFlickableWithFolding.qml - StatusQ/Core/Utils/internal/HeaderWrapper.qml StatusQ/Core/Utils/Emoji.qml StatusQ/Core/Utils/JSONListModel.qml StatusQ/Core/Utils/ModelChangeGuard.qml @@ -204,6 +203,7 @@ StatusQ/Core/Utils/ModelsComparator.qml StatusQ/Core/Utils/OperatorsUtils.qml StatusQ/Core/Utils/QObject.qml + StatusQ/Core/Utils/SearchFilter.qml StatusQ/Core/Utils/StackViewStates.qml StatusQ/Core/Utils/StatesStack.qml StatusQ/Core/Utils/StringUtils.qml @@ -212,6 +212,7 @@ StatusQ/Core/Utils/Utils.qml StatusQ/Core/Utils/big.min.mjs StatusQ/Core/Utils/emojiList.js + StatusQ/Core/Utils/internal/HeaderWrapper.qml StatusQ/Core/Utils/qmldir StatusQ/Core/Utils/xss.js StatusQ/Core/qmldir diff --git a/ui/app/AppLayouts/Communities/popups/InDropdown.qml b/ui/app/AppLayouts/Communities/popups/InDropdown.qml index 71841ff7e7..44beaefdaf 100644 --- a/ui/app/AppLayouts/Communities/popups/InDropdown.qml +++ b/ui/app/AppLayouts/Communities/popups/InDropdown.qml @@ -122,11 +122,9 @@ StatusDropdown { sourceModel: joined - filters: RegExpFilter { + filters: SearchFilter { roleName: "name" - pattern: `*${searcher.text}*` - caseSensitivity : Qt.CaseInsensitive - syntax: RegExpFilter.Wildcard + searchPhrase: searcher.text } } diff --git a/ui/app/AppLayouts/Wallet/controls/TokenSelectorNew.qml b/ui/app/AppLayouts/Wallet/controls/TokenSelectorNew.qml index ea13360b48..b4fe95ffe0 100644 --- a/ui/app/AppLayouts/Wallet/controls/TokenSelectorNew.qml +++ b/ui/app/AppLayouts/Wallet/controls/TokenSelectorNew.qml @@ -11,10 +11,10 @@ import utils 1.0 Control { id: root - /** Expected model structure: see TokenSelectorPanel::assetsModel **/ + /** Expected model structure: see SearchableAssetsPanel::model **/ property alias assetsModel: tokenSelectorPanel.assetsModel - /** Expected model structure: see TokenSelectorPanel::collectiblesModel **/ + /** Expected model structure: see SearchableCollectiblesPanel::model **/ property alias collectiblesModel: tokenSelectorPanel.collectiblesModel readonly property bool isTokenSelected: d.isTokenSelected diff --git a/ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml b/ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml new file mode 100644 index 0000000000..1c92084ccf --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/SearchableAssetsPanel.qml @@ -0,0 +1,94 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Core 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Wallet.views 1.0 + +import SortFilterProxyModel 0.2 + +/** + Panel holding search field and lists of assets. +*/ +Control { + id: root + + /** + Expected model structure: + + tokensKey [string] - unique asset's identifier + name [string] - asset's name + symbol [string] - asset's symbol + iconSource [url] - asset's icon + currencyBalanceAsString [string] - formatted balance + balances [model] - submodel of balances per chain + balanceAsString [string] - formatted balance per chain + iconUrl [url] - chain's icon + **/ + property alias model: sfpm.sourceModel + + signal selected(string key) + property string highlightedKey: "" + + SortFilterProxyModel { + id: sfpm + + filters: AnyOf { + SearchFilter { + roleName: "name" + searchPhrase: searchBox.text + } + SearchFilter { + roleName: "symbol" + searchPhrase: searchBox.text + } + } + } + + contentItem: ColumnLayout { + spacing: 0 + + TokenSearchBox { + id: searchBox + + Layout.fillWidth: true + placeholderText: qsTr("Search assets") + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: listView.count + } + + StatusListView { + id: listView + + clip: true + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: contentHeight + + model: sfpm + + delegate: TokenSelectorAssetDelegate { + required property var model + required property int index + + highlighted: tokensKey === root.highlightedKey + + tokensKey: model.tokensKey + name: model.name + symbol: model.symbol + currencyBalanceAsString: model.currencyBalanceAsString + iconSource: model.iconSource + balancesModel: model.balances + + onClicked: root.selected(model.tokensKey) + } + } + } +} diff --git a/ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml b/ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml new file mode 100644 index 0000000000..6bff0d1d44 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/SearchableCollectiblesPanel.qml @@ -0,0 +1,230 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 +import QtQml.Models 2.15 + +import StatusQ 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Core 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Core.Utils 0.1 +import StatusQ.Popups.Dialog 0.1 + +import AppLayouts.Wallet.views 1.0 +import utils 1.0 + +import SortFilterProxyModel 0.2 + +/** + Panel holding search field and two-levels list of collectibles. +*/ +Control { + id: root + + /** + Expected model structure: + + groupName [string] - group name + icon [url] - icon image of a group + type [string] - group type, can be "community" or "other" + subitems [model] - submodel of collectibles/collections of the group + key [string] - balance + name [string] - name of the subitem + balance [int] - balance of the subitem + icon [url] - icon of the subitem + **/ + property alias model: sfpm.sourceModel + + signal collectionSelected(string key) + signal collectibleSelected(string key) + + property string highlightedKey: "" + + readonly property alias currentItem: collectiblesStackView.currentItem + + SortFilterProxyModel { + id: sfpm + + filters: SearchFilter { + roleName: "groupName" + searchPhrase: collectiblesSearchBox.text + } + } + + contentItem: StackView { + id: collectiblesStackView + + initialItem: ColumnLayout { + spacing: 0 + + TokenSearchBox { + id: collectiblesSearchBox + + Layout.fillWidth: true + placeholderText: qsTr("Search collectibles") + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: collectiblesListView.count + } + + StatusListView { + id: collectiblesListView + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: contentHeight + + clip: true + model: sfpm + + delegate: TokenSelectorCollectibleDelegate { + required property var model + + readonly property int subitemsCount: + model.subitems.ModelCount.count + + readonly property bool isCommunity: + model.type === "community" + + readonly property bool showCount: + subitemsCount > 1 || isCommunity + + name: model.groupName + balance: showCount ? subitemsCount : "" + image: model.icon + goDeeperIconVisible: subitemsCount > 1 + || isCommunity + highlighted: subitemsCount === 1 && !isCommunity + ? ModelUtils.get(model.subitems, 0, "key") + === root.highlightedKey + : false + + onClicked: { + if (subitemsCount === 1 && !isCommunity) { + const key = ModelUtils.get(model.subitems, 0, "key") + root.collectibleSelected(key) + return + } + + const parameters = { + index: sfpm.index(model.index, 0), + model: model.subitems, + isCommunity: isCommunity + } + + collectiblesStackView.push( + collectiblesSublistComponent, + parameters, + StackView.Immediate) + } + } + + section.property: "type" + section.delegate: StatusBaseText { + color: Theme.palette.baseColor1 + topPadding: Style.current.padding + + text: section === "community" + ? qsTr("Community minted") + : qsTr("Other") + } + } + } + } + + Component { + id: collectiblesSublistComponent + + ColumnLayout { + property var index + property alias model: sublistSfpm.sourceModel + property bool isCommunity + + spacing: 0 + + SortFilterProxyModel { + id: sublistSfpm + + filters: SearchFilter { + roleName: "name" + searchPhrase: collectiblesSublistSearchBox.text + } + } + + StatusIconTextButton { + id: backButton + + statusIcon: "previous" + icon.width: 12 + icon.height: 12 + text: qsTr("Back") + + onClicked: collectiblesStackView.pop(StackView.Immediate) + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: collectiblesListView.count + } + + TokenSearchBox { + id: collectiblesSublistSearchBox + + Layout.fillWidth: true + placeholderText: qsTr("Search collectibles") + } + + StatusDialogDivider { + Layout.fillWidth: true + visible: collectiblesListView.count + } + + StatusListView { + id: sublist + + Layout.fillWidth: true + Layout.fillHeight: true + Layout.preferredHeight: contentHeight + + model: sublistSfpm + + clip: true + + delegate: TokenSelectorCollectibleDelegate { + required property var model + + name: model.name + balance: model.balance > 1 ? model.balance : "" + image: model.icon + goDeeperIconVisible: false + highlighted: model.key === root.highlightedKey + + onClicked: { + if (isCommunity) + root.collectionSelected(model.key) + else + root.collectibleSelected(model.key) + } + } + } + + // Detection if the related model entry has been removed. + // Using model.Component.destruction.connect is not reliable because + // is not called for submodels maintained in c++ by the parent model. + ItemSelectionModel { + id: selection + + model: sfpm + + onHasSelectionChanged: { + if (!hasSelection) + collectiblesStackView.pop(StackView.Immediate) + } + + Component.onCompleted: select(index, ItemSelectionModel.Select) + } + } + } +} diff --git a/ui/app/AppLayouts/Wallet/panels/TokenSearchBox.qml b/ui/app/AppLayouts/Wallet/panels/TokenSearchBox.qml new file mode 100644 index 0000000000..fc8fed9f26 --- /dev/null +++ b/ui/app/AppLayouts/Wallet/panels/TokenSearchBox.qml @@ -0,0 +1,10 @@ +import shared.controls 1.0 + +SearchBox { + input.leftPadding: 0 + input.rightPadding: 0 + minimumHeight: 56 + maximumHeight: 56 + input.showBackground: false + focus: visible +} diff --git a/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml b/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml index d79697d1a2..60cfe80914 100644 --- a/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml +++ b/ui/app/AppLayouts/Wallet/panels/TokenSelectorPanel.qml @@ -1,20 +1,8 @@ import QtQuick 2.15 import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 -import QtQml.Models 2.15 -import StatusQ 0.1 import StatusQ.Controls 0.1 -import StatusQ.Core 0.1 -import StatusQ.Core.Theme 0.1 -import StatusQ.Core.Utils 0.1 -import StatusQ.Popups.Dialog 0.1 - -import AppLayouts.Wallet.views 1.0 -import shared.controls 1.0 -import utils 1.0 - -import SortFilterProxyModel 0.2 /** Two-tabs panel holding searchable lists of assets (single level) and @@ -37,33 +25,11 @@ Control { Collectibles = 1 } - /** - Expected model structure: + /** Expected model structure: see SearchableAssetsPanel::model **/ + property alias assetsModel: searchableAssetsPanel.model - tokensKey [string] - unique asset's identifier - name [string] - asset's name - symbol [string] - asset's symbol - iconSource [url] - asset's icon - currencyBalanceAsString [string] - formatted balance - balances [model] - submodel of balances per chain - balanceAsString [string] - formatted balance per chain - iconUrl [url] - chain's icon - **/ - property alias assetsModel: assetsSfpm.sourceModel - - /** - Expected model structure: - - groupName [string] - group name - icon [url] - icon image of a group - type [string] - group type, can be "community" or "other" - subitems [model] - submodel of collectibles/collections of the group - key [string] - balance - name [string] - name of the subitem - balance [int] - balance of the subitem - icon [url] - icon of the subitem - **/ - property alias collectiblesModel: collectiblesSfpm.sourceModel + /** Expected model structure: see SearchableCollectiblesPanel::model **/ + property alias collectiblesModel: searchableCollectiblesPanel.model // Index of the current tab, indexes ​​correspond to the Tabs enum values. property alias currentTab: tabBar.currentIndex @@ -74,47 +40,6 @@ Control { property string highlightedKey: "" - SortFilterProxyModel { - id: assetsSfpm - - filters: AnyOf { - SearchFilter { - roleName: "name" - searchPhrase: assetsSearchBox.text - } - SearchFilter { - roleName: "symbol" - searchPhrase: assetsSearchBox.text - } - } - } - - SortFilterProxyModel { - id: collectiblesSfpm - - filters: SearchFilter { - roleName: "groupName" - searchPhrase: collectiblesSearchBox.text - } - } - - component SearchFilter: RegExpFilter { - required property string searchPhrase - - pattern: `*${searchPhrase}*` - caseSensitivity : Qt.CaseInsensitive - syntax: RegExpFilter.Wildcard - } - - component Search: SearchBox { - input.leftPadding: root.leftPadding - input.rightPadding: root.leftPadding - minimumHeight: 56 - maximumHeight: 56 - input.showBackground: false - focus: visible - } - contentItem: ColumnLayout { StatusTabBar { id: tabBar @@ -154,230 +79,21 @@ Control { visible: !!root.assetsModel || !!root.collectiblesModel currentIndex: tabBar.currentIndex - ColumnLayout { + SearchableAssetsPanel { + id: searchableAssetsPanel + Layout.preferredHeight: visible ? implicitHeight : 0 - spacing: 0 - Search { - id: assetsSearchBox - - Layout.fillWidth: true - placeholderText: qsTr("Search assets") - } - - StatusDialogDivider { - Layout.fillWidth: true - visible: assetsListView.count - } - - StatusListView { - id: assetsListView - - clip: true - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: contentHeight - - model: assetsSfpm - - delegate: TokenSelectorAssetDelegate { - required property var model - required property int index - - highlighted: tokensKey === root.highlightedKey - - tokensKey: model.tokensKey - name: model.name - symbol: model.symbol - currencyBalanceAsString: model.currencyBalanceAsString - iconSource: model.iconSource - balancesModel: model.balances - - onClicked: root.assetSelected(model.tokensKey) - } - } + onSelected: root.assetSelected(key) } - StackView { - id: collectiblesStackView + SearchableCollectiblesPanel { + id: searchableCollectiblesPanel - Layout.fillWidth: true - Layout.fillHeight: true Layout.preferredHeight: visible ? currentItem.implicitHeight : 0 - initialItem: ColumnLayout { - spacing: 0 - - Search { - id: collectiblesSearchBox - - Layout.fillWidth: true - placeholderText: qsTr("Search collectibles") - } - - StatusDialogDivider { - Layout.fillWidth: true - visible: collectiblesListView.count - } - - StatusListView { - id: collectiblesListView - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: contentHeight - - clip: true - model: collectiblesSfpm - - delegate: TokenSelectorCollectibleDelegate { - required property var model - - readonly property int subitemsCount: - model.subitems.ModelCount.count - - readonly property bool isCommunity: - model.type === "community" - - readonly property bool showCount: - subitemsCount > 1 || isCommunity - - name: model.groupName - balance: showCount ? subitemsCount : "" - image: model.icon - goDeeperIconVisible: subitemsCount > 1 - || isCommunity - highlighted: subitemsCount === 1 && !isCommunity - ? ModelUtils.get(model.subitems, 0, "key") - === root.highlightedKey - : false - - onClicked: { - if (subitemsCount === 1 && !isCommunity) { - const key = ModelUtils.get(model.subitems, 0, "key") - root.collectibleSelected(key) - return - } - - const parameters = { - index: collectiblesSfpm.index(model.index, 0), - model: model.subitems, - isCommunity: isCommunity - } - - collectiblesStackView.push( - collectiblesSublistComponent, - parameters, - StackView.Immediate) - } - } - - section.property: "type" - section.delegate: StatusBaseText { - color: Theme.palette.baseColor1 - topPadding: Style.current.padding - - text: section === "community" - ? qsTr("Community minted") - : qsTr("Other") - } - } - } - } - } - } - - Component { - id: collectiblesSublistComponent - - ColumnLayout { - property var index - property alias model: sublistSfpm.sourceModel - property bool isCommunity - - spacing: 0 - - SortFilterProxyModel { - id: sublistSfpm - - filters: SearchFilter { - roleName: "name" - searchPhrase: collectiblesSublistSearchBox.text - } - } - - StatusIconTextButton { - id: backButton - - statusIcon: "previous" - icon.width: 12 - icon.height: 12 - text: qsTr("Back") - - onClicked: collectiblesStackView.pop(StackView.Immediate) - } - - StatusDialogDivider { - Layout.fillWidth: true - visible: collectiblesListView.count - } - - Search { - id: collectiblesSublistSearchBox - - Layout.fillWidth: true - placeholderText: qsTr("Search collectibles") - } - - StatusDialogDivider { - Layout.fillWidth: true - visible: collectiblesListView.count - } - - StatusListView { - id: sublist - - Layout.fillWidth: true - Layout.fillHeight: true - Layout.preferredHeight: contentHeight - - model: sublistSfpm - - clip: true - - delegate: TokenSelectorCollectibleDelegate { - required property var model - - name: model.name - balance: model.balance > 1 ? model.balance : "" - image: model.icon - goDeeperIconVisible: false - highlighted: model.key === root.highlightedKey - - onClicked: { - if (isCommunity) - root.collectionSelected(model.key) - else - root.collectibleSelected(model.key) - } - } - } - - // Detection if the related model entry has been removed. - // Using model.Component.destruction.connect is not reliable because - // is not called for submodels maintained in c++ by the parent model. - ItemSelectionModel { - id: selection - - model: collectiblesSfpm - - onHasSelectionChanged: { - if (!hasSelection) - collectiblesStackView.pop(StackView.Immediate) - } - - Component.onCompleted: select(index, ItemSelectionModel.Select) + onCollectibleSelected: root.collectibleSelected(key) + onCollectionSelected: root.collectionSelected(key) } } } diff --git a/ui/app/AppLayouts/Wallet/panels/qmldir b/ui/app/AppLayouts/Wallet/panels/qmldir index 9a7d321f8f..a730350ed2 100644 --- a/ui/app/AppLayouts/Wallet/panels/qmldir +++ b/ui/app/AppLayouts/Wallet/panels/qmldir @@ -1,14 +1,16 @@ ActivityFilterPanel 1.0 ActivityFilterPanel.qml +BuyCryptoProvidersListPanel 1.0 BuyCryptoProvidersListPanel.qml ContractInfoButtonWithMenu 1.0 ContractInfoButtonWithMenu.qml DAppsWorkflow 1.0 DAppsWorkflow.qml ManageAssetsPanel 1.0 ManageAssetsPanel.qml ManageCollectiblesPanel 1.0 ManageCollectiblesPanel.qml ManageHiddenPanel 1.0 ManageHiddenPanel.qml +SearchableAssetsPanel 1.0 SearchableAssetsPanel.qml +SearchableCollectiblesPanel 1.0 SearchableCollectiblesPanel.qml +SelectParamsForBuyCryptoPanel 1.0 SelectParamsForBuyCryptoPanel.qml SignInfoBox 1.0 SignInfoBox.qml SwapInputPanel 1.0 SwapInputPanel.qml TokenSelectorPanel 1.0 TokenSelectorPanel.qml WalletHeader 1.0 WalletHeader.qml WalletNftPreview 1.0 WalletNftPreview.qml WalletTxProgressBlock 1.0 WalletTxProgressBlock.qml -BuyCryptoProvidersListPanel 1.0 BuyCryptoProvidersListPanel.qml -SelectParamsForBuyCryptoPanel 1.0 SelectParamsForBuyCryptoPanel.qml