From 263ed2a82261f2f9b253fffec3f13d16d795e791 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Tinkl?= Date: Thu, 4 Jan 2024 13:05:54 +0100 Subject: [PATCH] feat: Add Advanced settings in new Advanced Tab ... under Settings/Wallet/Manage Tokens - rename Tokens List tab to Advanced tab - introduce a new `CurrencyAmountInput` component, backed by `FormattedDoubleProperty` C++ class (plus the respective SB page) - use `FastExpressionFoo` for the collectibles views as well Fixes #12611 Fixes #13040 --- storybook/pages/CurrencyAmountInputPage.qml | 138 +++++++++++++ storybook/qmlTests/main.cpp | 2 + .../tests/tst_CurrencyAmountInput.qml | 182 ++++++++++++++++++ ui/StatusQ/CMakeLists.txt | 2 + .../include/StatusQ/formatteddoubleproperty.h | 42 ++++ ui/StatusQ/src/formatteddoubleproperty.cpp | 66 +++++++ ui/StatusQ/src/plugin.cpp | 2 + .../panels/SupportedTokenListsPanel.qml | 5 +- .../AppLayouts/Profile/views/WalletView.qml | 2 +- .../Profile/views/wallet/ManageTokensView.qml | 89 ++++++++- .../Wallet/controls/FilterComboBox.qml | 5 +- .../Wallet/views/CollectiblesView.qml | 12 +- .../shared/controls/CurrencyAmountInput.qml | 121 ++++++++++++ ui/imports/shared/controls/qmldir | 1 + 14 files changed, 651 insertions(+), 18 deletions(-) create mode 100644 storybook/pages/CurrencyAmountInputPage.qml create mode 100644 storybook/qmlTests/tests/tst_CurrencyAmountInput.qml create mode 100644 ui/StatusQ/include/StatusQ/formatteddoubleproperty.h create mode 100644 ui/StatusQ/src/formatteddoubleproperty.cpp create mode 100644 ui/imports/shared/controls/CurrencyAmountInput.qml diff --git a/storybook/pages/CurrencyAmountInputPage.qml b/storybook/pages/CurrencyAmountInputPage.qml new file mode 100644 index 0000000000..76f40947d2 --- /dev/null +++ b/storybook/pages/CurrencyAmountInputPage.qml @@ -0,0 +1,138 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import Storybook 1.0 +import utils 1.0 + +import shared.controls 1.0 + +SplitView { + id: root + + orientation: Qt.Horizontal + + Item { + SplitView.fillWidth: true + SplitView.fillHeight: true + + CurrencyAmountInput { + id: input + anchors.centerIn: parent + currencySymbol: ctrlCurrencySymbol.text + decimals: ctrlDecimals.value + readOnly: ctrlReadOnly.checked + enabled: ctrlEnabled.checked + value: ctrlAmount.text + } + } + + LogsAndControlsPanel { + SplitView.minimumWidth: 300 + SplitView.preferredWidth: 400 + + SplitView.fillHeight: true + + ColumnLayout { + Layout.fillWidth: true + RowLayout { + Label { + text: "Value:\t" + } + TextField { + id: ctrlAmount + text: "0.10" + placeholderText: "Numeric value" + onEditingFinished: input.value = text + } + } + RowLayout { + Label { + text: "Currency:\t" + } + TextField { + id: ctrlCurrencySymbol + text: "EUR" + placeholderText: "Currency symbol" + } + } + RowLayout { + Label { + text: "Decimals:\t" + } + SpinBox { + id: ctrlDecimals + from: 0 + to: 18 + value: 2 + } + } + Switch { + id: ctrlReadOnly + text: "Read only" + } + Switch { + id: ctrlEnabled + text: "Enabled" + checked: true + } + RowLayout { + Layout.fillWidth: true + Layout.topMargin: 16 + Label { + text: "Numeric value:" + } + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + horizontalAlignment: Text.AlignRight + font.bold: true + text: input.asString + } + } + RowLayout { + Layout.fillWidth: true + Label { + text: "Valid:" + } + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + horizontalAlignment: Text.AlignRight + font.bold: true + text: input.valid ? "true" : "false" + } + } + RowLayout { + Layout.fillWidth: true + Label { + text: "Formatted as locale string:" + } + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + horizontalAlignment: Text.AlignRight + font.bold: true + text: input.asLocaleString + } + } + RowLayout { + Layout.fillWidth: true + Label { + text: "Locale:" + } + Label { + Layout.fillWidth: true + Layout.alignment: Qt.AlignRight + horizontalAlignment: Text.AlignRight + font.bold: true + text: input.locale + } + } + } + } +} + +// category: Controls + +// https://www.figma.com/file/eM26pyHZUeAwMLviaS1KJn/%E2%9A%99%EF%B8%8F-Wallet-Settings%3A-Manage-Tokens?type=design&node-id=305-139866&mode=design&t=g49O9LFh8PkuPxZB-0 diff --git a/storybook/qmlTests/main.cpp b/storybook/qmlTests/main.cpp index 795aa569de..1e3aa7b325 100644 --- a/storybook/qmlTests/main.cpp +++ b/storybook/qmlTests/main.cpp @@ -24,6 +24,8 @@ public slots: qmlRegisterSingletonType("TextUtils", 1, 0, "TextUtils", &TextUtils::qmlInstance); QStandardPaths::setTestModeEnabled(true); + + QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates)); } }; diff --git a/storybook/qmlTests/tests/tst_CurrencyAmountInput.qml b/storybook/qmlTests/tests/tst_CurrencyAmountInput.qml new file mode 100644 index 0000000000..284ed57f34 --- /dev/null +++ b/storybook/qmlTests/tests/tst_CurrencyAmountInput.qml @@ -0,0 +1,182 @@ +import QtQuick 2.15 +import QtTest 1.15 + +import Storybook 1.0 +import shared.controls 1.0 + +Item { + id: root + width: 600 + height: 400 + + Component { + id: componentUnderTest + CurrencyAmountInput { + id: input + anchors.centerIn: parent + } + } + + QtObject { + id: defaults + + readonly property int decimals: 2 + readonly property string currencySymbol: "USD" + } + + TestCase { + name: "CurrencyAmountInput" + when: windowShown + + property CurrencyAmountInput controlUnderTest: null + + function init() { + controlUnderTest = createTemporaryObject(componentUnderTest, root) + controlUnderTest.locale = "en_US" + } + + function test_getSetValueProgramatically() { + verify(!!controlUnderTest) + + // initial value is 0 + verify(controlUnderTest.value === 0) + + // initial num of decimals is 2 + verify(controlUnderTest.decimals === defaults.decimals) + + // verify setting a value yields a valid state + controlUnderTest.value = 1.23 + + // verify both the value and text displayed is correct + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 1.23) + verify(controlUnderTest.text === "1.23") + verify(controlUnderTest.text === controlUnderTest.asString) + + // verify setting value as text works as well + controlUnderTest.value = "456.78" + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 456.78) + verify(controlUnderTest.text === "456.78") + verify(controlUnderTest.text === controlUnderTest.asString) + } + + function test_inputValueManually() { + verify(!!controlUnderTest) + + // click the control to get focus and type "1.23" + mouseClick(controlUnderTest) + controlUnderTest.clear() + keyClick(Qt.Key_1) + keyClick(Qt.Key_Period) + keyClick(Qt.Key_2) + keyClick(Qt.Key_3) + + // verify we get 1.23 back + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 1.23) + verify(controlUnderTest.text === "1.23") + verify(controlUnderTest.text === controlUnderTest.asString) + } + + function test_decimals() { + verify(!!controlUnderTest) + + // initial num of decimals is 2 + verify(controlUnderTest.decimals === defaults.decimals) + + // set 8 decimals + controlUnderTest.decimals = 8 + verify(controlUnderTest.decimals === 8) + + // set a number with 8 decimals + controlUnderTest.value = 1.12345678 + + // verify the value and validity + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 1.12345678) + verify(controlUnderTest.text === "1.12345678") + verify(controlUnderTest.text === controlUnderTest.asString) + + // set back to 3 decimals + controlUnderTest.decimals = 3 + + // setting a value with more decimals -> invalid + controlUnderTest.value = 1.1234 + verify(!controlUnderTest.valid) + } + + function test_currencySymbol() { + verify(!!controlUnderTest) + + // USD is default + verify(controlUnderTest.currencySymbol === defaults.currencySymbol) + + // try setting a different one + controlUnderTest.currencySymbol = "EUR" + verify(controlUnderTest.currencySymbol === "EUR") + + // try clearing the currency symbol + controlUnderTest.currencySymbol = "" + verify(controlUnderTest.currencySymbol === "") + } + + function test_explicitLocale() { + verify(!!controlUnderTest) + controlUnderTest.locale = "cs_CZ" + + // verify setting a value programatically yields a valid state + controlUnderTest.value = 1.23 + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 1.23) + verify(controlUnderTest.text === "1,23") + + // verify the text displayed observes the locale decimal point (,) + verify(controlUnderTest.text === "1,23") + + // verify typing both a period and comma (this locale's decimal separator) both yield the same correct value + mouseClick(controlUnderTest) + + // first with the default comma (,) as decimal separator + controlUnderTest.clear() + keyClick(Qt.Key_6) + keyClick(Qt.Key_Comma) + keyClick(Qt.Key_6) + keyClick(Qt.Key_6) + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 6.66) + verify(controlUnderTest.text === "6,66") + + // try the fallback decimal separator (.) + controlUnderTest.clear() + verify(controlUnderTest.text === "") + keyClick(Qt.Key_6) + keyClick(Qt.Key_Period) + keyClick(Qt.Key_6) + keyClick(Qt.Key_6) + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 6.66) + verify(controlUnderTest.text === "6,66") + } + + function test_validator() { + verify(!!controlUnderTest) + controlUnderTest.decimals = 4 + controlUnderTest.value = 1.1234 + verify(controlUnderTest.valid) + + controlUnderTest.decimals = 3 + verify(!controlUnderTest.valid) + + // delete one char from the middle and type some string + mouseClick(controlUnderTest) + keyClick(Qt.Key_Left) + keyClick(Qt.Key_Left) + keyClick(Qt.Key_Backspace) + keyClick(Qt.Key_A) // <== should get ignored + + verify(controlUnderTest.valid) + verify(controlUnderTest.value === 1.134) + } + } +} diff --git a/ui/StatusQ/CMakeLists.txt b/ui/StatusQ/CMakeLists.txt index d744e21f51..6c833f275f 100644 --- a/ui/StatusQ/CMakeLists.txt +++ b/ui/StatusQ/CMakeLists.txt @@ -98,6 +98,7 @@ add_library(StatusQ SHARED include/StatusQ/fastexpressionfilter.h include/StatusQ/fastexpressionrole.h include/StatusQ/fastexpressionsorter.h + include/StatusQ/formatteddoubleproperty.h include/StatusQ/leftjoinmodel.h include/StatusQ/modelutilsinternal.h include/StatusQ/permissionutilsinternal.h @@ -116,6 +117,7 @@ add_library(StatusQ SHARED src/fastexpressionfilter.cpp src/fastexpressionrole.cpp src/fastexpressionsorter.cpp + src/formatteddoubleproperty.cpp src/leftjoinmodel.cpp src/modelutilsinternal.cpp src/permissionutilsinternal.cpp diff --git a/ui/StatusQ/include/StatusQ/formatteddoubleproperty.h b/ui/StatusQ/include/StatusQ/formatteddoubleproperty.h new file mode 100644 index 0000000000..387226a1e8 --- /dev/null +++ b/ui/StatusQ/include/StatusQ/formatteddoubleproperty.h @@ -0,0 +1,42 @@ +#pragma once + +#include + +/** + * @brief The FormattedDoubleProperty class serves as a proxy for numeric inputs in QML + * + * It keeps track of its internal `double` value as returns that as a @p value. + * When setting the @p value, it accepts both a numeric value (int/float/double) or a string + * representation thereof. + */ +class FormattedDoubleProperty : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged FINAL) //< internal value (double) + Q_PROPERTY(QString asString READ asString NOTIFY valueChanged FINAL) //< value as a "C" string, `.` as decimal separator + Q_PROPERTY(QString locale READ locale WRITE setLocale NOTIFY localeChanged FINAL) //< locale name, defaults to user's own +public: + explicit FormattedDoubleProperty(QObject* parent = nullptr); + + /** + * @param decimals numbers of decimals to display (defaults to shortest possible) + * @return @p value formatted according to the selected locale, with the specified number of decimal places + */ + Q_INVOKABLE QString asLocaleString(int decimals = QLocale::FloatingPointShortest) const; + +signals: + void valueChanged(); + void localeChanged(); + +private: + QVariant value() const; + void setValue(const QVariant& newValue); + double m_value{0.0}; + + QString asString() const; + + QString locale() const; + void setLocale(const QString& newLocale); + QLocale m_locale{QLocale()}; +}; diff --git a/ui/StatusQ/src/formatteddoubleproperty.cpp b/ui/StatusQ/src/formatteddoubleproperty.cpp new file mode 100644 index 0000000000..472df2af38 --- /dev/null +++ b/ui/StatusQ/src/formatteddoubleproperty.cpp @@ -0,0 +1,66 @@ +#include "StatusQ/formatteddoubleproperty.h" + +#include + +FormattedDoubleProperty::FormattedDoubleProperty(QObject* parent) + : QObject{parent} +{ + m_locale.setNumberOptions(QLocale::DefaultNumberOptions | QLocale::OmitGroupSeparator); +} + +QVariant FormattedDoubleProperty::value() const +{ + return m_value; +} + +void FormattedDoubleProperty::setValue(const QVariant& newValue) +{ + if (!newValue.isValid()) { + qWarning() << "Setting property to invalid value:" << newValue << "is not supported"; + return; + } + + auto ok = false; + auto tempValue = newValue.toDouble(&ok); + + if (!ok && newValue.canConvert()) { + tempValue = m_locale.toDouble(newValue.toString(), &ok); + } + + if (!ok || qIsNaN(tempValue)) { + qWarning() << "Failed set value property from:" << newValue << "; with type:" << newValue.typeName(); + return; + } + + if (m_value != tempValue) { + m_value = tempValue; + emit valueChanged(); + } +} + +QString FormattedDoubleProperty::asString() const +{ + return QString::number(m_value, 'f', QLocale::FloatingPointShortest); +} + +QString FormattedDoubleProperty::asLocaleString(int decimals) const +{ + return m_locale.toString(m_value, 'f', decimals); +} + +QString FormattedDoubleProperty::locale() const +{ + return m_locale.name(); +} + +void FormattedDoubleProperty::setLocale(const QString& newLocale) +{ + if (m_locale.name() == newLocale) + return; + + if (newLocale.isEmpty()) + m_locale = QLocale(); // user default + else + m_locale = QLocale(newLocale); // explicit + emit localeChanged(); +} diff --git a/ui/StatusQ/src/plugin.cpp b/ui/StatusQ/src/plugin.cpp index 849657b47f..9d9f2170e7 100644 --- a/ui/StatusQ/src/plugin.cpp +++ b/ui/StatusQ/src/plugin.cpp @@ -19,6 +19,7 @@ #include "StatusQ/submodelproxymodel.h" #include "StatusQ/sumaggregator.h" #include "StatusQ/writableproxymodel.h" +#include "StatusQ/formatteddoubleproperty.h" #include "wallet/managetokenscontroller.h" #include "wallet/managetokensmodel.h" @@ -52,6 +53,7 @@ public: qmlRegisterType("StatusQ", 0, 1, "RolesRenamingModel"); qmlRegisterType("StatusQ", 0, 1, "SumAggregator"); qmlRegisterType("StatusQ", 0, 1, "WritableProxyModel"); + qmlRegisterType("StatusQ", 0, 1, "FormattedDoubleProperty"); qmlRegisterSingletonType("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance); diff --git a/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml b/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml index 22698e29bb..e68e9ad363 100644 --- a/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml +++ b/ui/app/AppLayouts/Profile/panels/SupportedTokenListsPanel.qml @@ -32,12 +32,13 @@ StatusListView { subTitle: qsTr("%n token(s) · Last updated %1 @%2", "", model.tokensCount).arg(LocaleUtils.formatDate(model.updatedAt * 1000)).arg(LocaleUtils.formatTime(model.updatedAt, Locale.ShortFormat)) + statusListItemSubTitle.font.pixelSize: Style.current.additionalTextSize asset.name: model.image asset.isImage: true border.width: 1 border.color: Theme.palette.baseColor5 components: [ - StatusButton { + StatusFlatButton { text: qsTr("View") onClicked: keyFilter.value = model.key @@ -56,7 +57,7 @@ StatusListView { width: parent.width - 4 // The rectangular path is rendered outside icon: "add" - text: qsTr("Add Token List (coming soon)") + text: qsTr("Add a Token List (coming soon)") } } diff --git a/ui/app/AppLayouts/Profile/views/WalletView.qml b/ui/app/AppLayouts/Profile/views/WalletView.qml index c5dd391f25..f21d209b07 100644 --- a/ui/app/AppLayouts/Profile/views/WalletView.qml +++ b/ui/app/AppLayouts/Profile/views/WalletView.qml @@ -275,7 +275,7 @@ SettingsContentBase { id: manageTokensView sourcesOfTokensModel: tokensStore.sourcesOfTokensModel tokensListModel: tokensStore.extendedFlatTokensModel - baseWalletAssetsModel: RootStore.assets // TODO include community assets (#12369) + baseWalletAssetsModel: RootStore.assets baseWalletCollectiblesModel: { RootStore.setFillterAllAddresses() // FIXME no other way to get _all_ collectibles? // TODO concat proxy model to include community collectibles (#12519) diff --git a/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml b/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml index 874aa0f8d7..a3e744c5bb 100644 --- a/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml +++ b/ui/app/AppLayouts/Profile/views/wallet/ManageTokensView.qml @@ -1,9 +1,15 @@ import QtQuick 2.15 +import QtQuick.Controls 2.15 import QtQuick.Layouts 1.15 +import StatusQ.Core 0.1 import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 +import StatusQ.Popups.Dialog 0.1 import shared.controls 1.0 +import shared.stores 1.0 as SharedStores import utils 1.0 import AppLayouts.Profile.panels 1.0 @@ -25,6 +31,7 @@ ColumnLayout { return false if (tabBar.currentIndex > d.collectiblesTabIndex) return false + // FIXME take advanced settings into account here too (#13178) if (tabBar.currentIndex === d.collectiblesTabIndex && baseWalletCollectiblesModel.isFetching) return false return loader.item && loader.item.dirty @@ -33,6 +40,7 @@ ColumnLayout { function saveChanges() { if (tabBar.currentIndex > d.collectiblesTabIndex) return + // FIXME save advanced settings (#13178) loader.item.saveSettings() } @@ -47,7 +55,7 @@ ColumnLayout { readonly property int assetsTabIndex: 0 readonly property int collectiblesTabIndex: 1 - readonly property int tokenSourcesTabIndex: 2 + readonly property int advancedTabIndex: 2 function checkLoadMoreCollectibles() { if (tabBar.currentIndex !== collectiblesTabIndex) @@ -88,7 +96,7 @@ ColumnLayout { StatusTabButton { width: implicitWidth - text: qsTr("Token lists") + text: qsTr("Advanced") } } @@ -105,8 +113,8 @@ ColumnLayout { return tokensPanel case d.collectiblesTabIndex: return collectiblesPanel - case d.tokenSourcesTabIndex: - return supportedTokensListPanel + case d.advancedTabIndex: + return advancedTab } } } @@ -116,7 +124,6 @@ ColumnLayout { ManageAssetsPanel { baseModel: root.baseWalletAssetsModel } - // TODO #12611 add Advanced section } Component { @@ -128,10 +135,74 @@ ColumnLayout { } Component { - id: supportedTokensListPanel - SupportedTokenListsPanel { - sourcesOfTokensModel: root.sourcesOfTokensModel - tokensListModel: root.tokensListModel + id: advancedTab + ColumnLayout { + spacing: 0 + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: 18 + Layout.bottomMargin: 18 + text: qsTr("Token lists") + color: Theme.palette.baseColor1 + } + SupportedTokenListsPanel { + Layout.fillWidth: true + Layout.fillHeight: true + sourcesOfTokensModel: root.sourcesOfTokensModel + tokensListModel: root.tokensListModel + } + StatusBaseText { + Layout.fillWidth: true + Layout.topMargin: 40 + 18 + Layout.bottomMargin: 26 + text: qsTr("Asset settings") + color: Theme.palette.baseColor1 + } + StatusDialogDivider { + Layout.fillWidth: true + } + StatusListItem { + Layout.fillWidth: true + title: qsTr("Show community assets when sending tokens") + + components: [ + StatusSwitch { + id: showCommunityAssetsSwitch + checked: true // FIXME integrate with backend (#13178) + onCheckedChanged: { + // FIXME integrate with backend (#13178) + } + } + ] + onClicked: { + showCommunityAssetsSwitch.checked = !showCommunityAssetsSwitch.checked + } + } + StatusDialogDivider { + Layout.fillWidth: true + } + StatusListItem { + Layout.fillWidth: true + title: qsTr("Don’t display assets with balance lower than") + + components: [ + CurrencyAmountInput { + enabled: displayThresholdSwitch.checked + currencySymbol: SharedStores.RootStore.currencyStore.currentCurrency + value: 0.10 // FIXME integrate with backend (#13178) + }, + StatusSwitch { + id: displayThresholdSwitch + checked: false // FIXME integrate with backend (#13178) + onCheckedChanged: { + // FIXME integrate with backend (#13178) + } + } + ] + onClicked: { + displayThresholdSwitch.checked = !displayThresholdSwitch.checked + } + } } } } diff --git a/ui/app/AppLayouts/Wallet/controls/FilterComboBox.qml b/ui/app/AppLayouts/Wallet/controls/FilterComboBox.qml index 75c4a3a2ec..abdd33e698 100644 --- a/ui/app/AppLayouts/Wallet/controls/FilterComboBox.qml +++ b/ui/app/AppLayouts/Wallet/controls/FilterComboBox.qml @@ -63,7 +63,7 @@ ComboBox { ] markerRoleName: "sourceGroup" - onRowsRemoved: root.clearFilter() // different underlying model -> uncheck all groups + onRowsRemoved: root.clearFilter() // different underlying model -> uncheck all } readonly property var combinedProxyModel: SortFilterProxyModel { @@ -81,12 +81,13 @@ ComboBox { } ] filters: [ - ExpressionFilter { + FastExpressionFilter { enabled: d.searchTextLowerCase !== "" expression: { d.searchTextLowerCase // ensure expression is reevaluated when searchString changes return model.groupName.toLowerCase().includes(d.searchTextLowerCase) || model.groupId.toLowerCase().includes(d.searchTextLowerCase) } + expectedRoles: ["groupName", "groupId"] } ] } diff --git a/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml b/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml index d774567967..b4d7cb070c 100644 --- a/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml +++ b/ui/app/AppLayouts/Wallet/views/CollectiblesView.qml @@ -83,30 +83,34 @@ ColumnLayout { roleNames: ["collectionName", "communityName"] } filters: [ - ExpressionFilter { + FastExpressionFilter { expression: { d.controller.settingsDirty return d.controller.filterAcceptsSymbol(model.symbol) && (customFilter.isCommunity ? !!model.communityId : !model.communityId) } + expectedRoles: ["symbol", "communityId"] }, - ExpressionFilter { + FastExpressionFilter { enabled: customFilter.isCommunity && cmbFilter.hasEnabledFilters expression: cmbFilter.selectedFilterGroupIds.includes(model.communityId) || (!model.communityId && cmbFilter.selectedFilterGroupIds.includes("")) + expectedRoles: ["communityId"] }, - ExpressionFilter { + FastExpressionFilter { enabled: !customFilter.isCommunity && cmbFilter.hasEnabledFilters expression: cmbFilter.selectedFilterGroupIds.includes(model.collectionUid) || (!model.collectionUid && cmbFilter.selectedFilterGroupIds.includes("")) + expectedRoles: ["collectionUid"] } ] sorters: [ - ExpressionSorter { + FastExpressionSorter { expression: { d.controller.settingsDirty return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) } enabled: d.isCustomView + expectedRoles: ["symbol"] }, RoleSorter { roleName: cmbTokenOrder.currentSortRoleName diff --git a/ui/imports/shared/controls/CurrencyAmountInput.qml b/ui/imports/shared/controls/CurrencyAmountInput.qml new file mode 100644 index 0000000000..027e40363c --- /dev/null +++ b/ui/imports/shared/controls/CurrencyAmountInput.qml @@ -0,0 +1,121 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import StatusQ 0.1 +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Components 0.1 +import StatusQ.Core.Theme 0.1 + +import utils 1.0 + +/*! + \qmltype CurrencyAmountInput + \inherits TextField + \brief Provides a text input field that accepts a numeric value, with optional currency symbol ("USD"). + Utilizes a builtin DoubleValidator to validate the user's input. + It accepts both the native decimal separator and optionally a period (`.`) for locales that don't use this. + \inqmlmodule shared.controls 1.0 + + Internally it uses FormattedDoubleProperty object that keeps track of the value. + */ +TextField { + id: root + + property alias value: internalProp.value // accepts double/float or string representation, rejects NaN + readonly property bool valid: acceptableInput + + property int decimals: 2 // number of decimal places to display + property string currencySymbol: "USD" // currency symbol, optional + property double minValue: 0 // min value + property double maxValue: Number.MAX_VALUE // max value + + property alias locale: internalProp.locale // locale code name (affects the validator and decimal point handler) + readonly property string asLocaleString: { + internalProp.value + return root.valid ? internalProp.asLocaleString(root.decimals) : "NaN" + } + readonly property string asString: internalProp.asString // "C" locale string + + FormattedDoubleProperty { + id: internalProp + onValueChanged: { + const oldPos = root.cursorPosition + root.text = asLocaleString() // min number of decimals, strip zeroes + root.cursorPosition = oldPos + } + } + + Keys.onPressed: (event) => { + // additionally accept dot (.) and convert it to the correct decimal point char + if (event.key === Qt.Key_Period) { + root.insert(root.cursorPosition, Qt.locale(root.locale).decimalPoint) + event.accepted = true + } else if (event.modifiers === Qt.NoModifier && event.key >= Qt.Key_A && event.key <= Qt.Key_Z) { + // reject typing non-numbers (can happen when the validator is in an intermediate state) + event.accepted = true + } + } + + Component.onCompleted: text = internalProp.asLocaleString(decimals) + onTextEdited: value = text + + font.family: Style.current.baseFont.name + font.pixelSize: Style.current.primaryTextFontSize + + leftPadding: Style.current.padding + rightPadding: currencySymbol !== "" ? + currencySymbolText.width + currencySymbolText.anchors.leftMargin + currencySymbolText.anchors.rightMargin : + Style.current.padding + topPadding: 10 + bottomPadding: 10 + + opacity: enabled ? 1 : 0.3 + color: readOnly ? Theme.palette.baseColor1 : Theme.palette.directColor1 + selectionColor: Theme.palette.primaryColor2 + selectedTextColor: Theme.palette.directColor1 + placeholderTextColor: Theme.palette.baseColor1 + + hoverEnabled: !readOnly + selectByMouse: true + inputMethodHints: Qt.ImhFormattedNumbersOnly + + validator: DoubleValidator { + notation: DoubleValidator.StandardNotation + decimals: root.decimals + bottom: root.minValue + top: root.maxValue + locale: internalProp.locale + } + + background: Rectangle { + radius: Style.current.radius + color: Theme.palette.statusAppNavBar.backgroundColor + border.width: root.cursorVisible || root.hovered || !root.valid ? 1 : 0 + border.color: { + if (!root.valid) + return Theme.palette.dangerColor1 + if (root.cursorVisible) + return Theme.palette.primaryColor1 + if (root.hovered) + return Theme.palette.primaryColor2 + return "transparent" + } + Behavior on border.color { ColorAnimation {} } + } + + cursorDelegate: StatusCursorDelegate { + cursorVisible: root.cursorVisible + } + + StatusBaseText { + id: currencySymbolText + anchors.right: parent.right + anchors.verticalCenter: parent.verticalCenter + anchors.leftMargin: Style.current.padding + anchors.rightMargin: Style.current.padding + color: Theme.palette.baseColor1 + text: root.currencySymbol + visible: !!text + } +} diff --git a/ui/imports/shared/controls/qmldir b/ui/imports/shared/controls/qmldir index 87b53268dc..a331bcaec7 100644 --- a/ui/imports/shared/controls/qmldir +++ b/ui/imports/shared/controls/qmldir @@ -7,6 +7,7 @@ ContactSelector 1.0 ContactSelector.qml ContactsListAndSearch 1.0 ContactsListAndSearch.qml CopyButton 1.0 CopyButton.qml CopyToClipBoardButton 1.0 CopyToClipBoardButton.qml +CurrencyAmountInput 1.0 CurrencyAmountInput.qml DisabledTooltipButton 1.0 DisabledTooltipButton.qml EmojiHash 1.0 EmojiHash.qml ErrorDetails 1.0 ErrorDetails.qml