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
This commit is contained in:
Lukáš Tinkl 2024-01-04 13:05:54 +01:00 committed by Lukáš Tinkl
parent e053d267f0
commit 263ed2a822
14 changed files with 651 additions and 18 deletions

View File

@ -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

View File

@ -24,6 +24,8 @@ public slots:
qmlRegisterSingletonType<TextUtils>("TextUtils", 1, 0, "TextUtils", &TextUtils::qmlInstance); qmlRegisterSingletonType<TextUtils>("TextUtils", 1, 0, "TextUtils", &TextUtils::qmlInstance);
QStandardPaths::setTestModeEnabled(true); QStandardPaths::setTestModeEnabled(true);
QLocale::setDefault(QLocale(QLocale::English, QLocale::UnitedStates));
} }
}; };

View File

@ -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)
}
}
}

View File

@ -98,6 +98,7 @@ add_library(StatusQ SHARED
include/StatusQ/fastexpressionfilter.h include/StatusQ/fastexpressionfilter.h
include/StatusQ/fastexpressionrole.h include/StatusQ/fastexpressionrole.h
include/StatusQ/fastexpressionsorter.h include/StatusQ/fastexpressionsorter.h
include/StatusQ/formatteddoubleproperty.h
include/StatusQ/leftjoinmodel.h include/StatusQ/leftjoinmodel.h
include/StatusQ/modelutilsinternal.h include/StatusQ/modelutilsinternal.h
include/StatusQ/permissionutilsinternal.h include/StatusQ/permissionutilsinternal.h
@ -116,6 +117,7 @@ add_library(StatusQ SHARED
src/fastexpressionfilter.cpp src/fastexpressionfilter.cpp
src/fastexpressionrole.cpp src/fastexpressionrole.cpp
src/fastexpressionsorter.cpp src/fastexpressionsorter.cpp
src/formatteddoubleproperty.cpp
src/leftjoinmodel.cpp src/leftjoinmodel.cpp
src/modelutilsinternal.cpp src/modelutilsinternal.cpp
src/permissionutilsinternal.cpp src/permissionutilsinternal.cpp

View File

@ -0,0 +1,42 @@
#pragma once
#include <QLocale>
/**
* @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()};
};

View File

@ -0,0 +1,66 @@
#include "StatusQ/formatteddoubleproperty.h"
#include <QDebug>
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<QString>()) {
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();
}

View File

@ -19,6 +19,7 @@
#include "StatusQ/submodelproxymodel.h" #include "StatusQ/submodelproxymodel.h"
#include "StatusQ/sumaggregator.h" #include "StatusQ/sumaggregator.h"
#include "StatusQ/writableproxymodel.h" #include "StatusQ/writableproxymodel.h"
#include "StatusQ/formatteddoubleproperty.h"
#include "wallet/managetokenscontroller.h" #include "wallet/managetokenscontroller.h"
#include "wallet/managetokensmodel.h" #include "wallet/managetokensmodel.h"
@ -52,6 +53,7 @@ public:
qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel"); qmlRegisterType<RolesRenamingModel>("StatusQ", 0, 1, "RolesRenamingModel");
qmlRegisterType<SumAggregator>("StatusQ", 0, 1, "SumAggregator"); qmlRegisterType<SumAggregator>("StatusQ", 0, 1, "SumAggregator");
qmlRegisterType<WritableProxyModel>("StatusQ", 0, 1, "WritableProxyModel"); qmlRegisterType<WritableProxyModel>("StatusQ", 0, 1, "WritableProxyModel");
qmlRegisterType<FormattedDoubleProperty>("StatusQ", 0, 1, "FormattedDoubleProperty");
qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance); qmlRegisterSingletonType<QClipboardProxy>("StatusQ", 0, 1, "QClipboardProxy", &QClipboardProxy::qmlInstance);

View File

@ -32,12 +32,13 @@ StatusListView {
subTitle: qsTr("%n token(s) · Last updated %1 @%2", subTitle: qsTr("%n token(s) · Last updated %1 @%2",
"", "",
model.tokensCount).arg(LocaleUtils.formatDate(model.updatedAt * 1000)).arg(LocaleUtils.formatTime(model.updatedAt, Locale.ShortFormat)) 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.name: model.image
asset.isImage: true asset.isImage: true
border.width: 1 border.width: 1
border.color: Theme.palette.baseColor5 border.color: Theme.palette.baseColor5
components: [ components: [
StatusButton { StatusFlatButton {
text: qsTr("View") text: qsTr("View")
onClicked: keyFilter.value = model.key onClicked: keyFilter.value = model.key
@ -56,7 +57,7 @@ StatusListView {
width: parent.width - 4 // The rectangular path is rendered outside width: parent.width - 4 // The rectangular path is rendered outside
icon: "add" icon: "add"
text: qsTr("Add Token List (coming soon)") text: qsTr("Add a Token List (coming soon)")
} }
} }

View File

@ -275,7 +275,7 @@ SettingsContentBase {
id: manageTokensView id: manageTokensView
sourcesOfTokensModel: tokensStore.sourcesOfTokensModel sourcesOfTokensModel: tokensStore.sourcesOfTokensModel
tokensListModel: tokensStore.extendedFlatTokensModel tokensListModel: tokensStore.extendedFlatTokensModel
baseWalletAssetsModel: RootStore.assets // TODO include community assets (#12369) baseWalletAssetsModel: RootStore.assets
baseWalletCollectiblesModel: { baseWalletCollectiblesModel: {
RootStore.setFillterAllAddresses() // FIXME no other way to get _all_ collectibles? RootStore.setFillterAllAddresses() // FIXME no other way to get _all_ collectibles?
// TODO concat proxy model to include community collectibles (#12519) // TODO concat proxy model to include community collectibles (#12519)

View File

@ -1,9 +1,15 @@
import QtQuick 2.15 import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import StatusQ.Core 0.1
import StatusQ.Controls 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.controls 1.0
import shared.stores 1.0 as SharedStores
import utils 1.0 import utils 1.0
import AppLayouts.Profile.panels 1.0 import AppLayouts.Profile.panels 1.0
@ -25,6 +31,7 @@ ColumnLayout {
return false return false
if (tabBar.currentIndex > d.collectiblesTabIndex) if (tabBar.currentIndex > d.collectiblesTabIndex)
return false return false
// FIXME take advanced settings into account here too (#13178)
if (tabBar.currentIndex === d.collectiblesTabIndex && baseWalletCollectiblesModel.isFetching) if (tabBar.currentIndex === d.collectiblesTabIndex && baseWalletCollectiblesModel.isFetching)
return false return false
return loader.item && loader.item.dirty return loader.item && loader.item.dirty
@ -33,6 +40,7 @@ ColumnLayout {
function saveChanges() { function saveChanges() {
if (tabBar.currentIndex > d.collectiblesTabIndex) if (tabBar.currentIndex > d.collectiblesTabIndex)
return return
// FIXME save advanced settings (#13178)
loader.item.saveSettings() loader.item.saveSettings()
} }
@ -47,7 +55,7 @@ ColumnLayout {
readonly property int assetsTabIndex: 0 readonly property int assetsTabIndex: 0
readonly property int collectiblesTabIndex: 1 readonly property int collectiblesTabIndex: 1
readonly property int tokenSourcesTabIndex: 2 readonly property int advancedTabIndex: 2
function checkLoadMoreCollectibles() { function checkLoadMoreCollectibles() {
if (tabBar.currentIndex !== collectiblesTabIndex) if (tabBar.currentIndex !== collectiblesTabIndex)
@ -88,7 +96,7 @@ ColumnLayout {
StatusTabButton { StatusTabButton {
width: implicitWidth width: implicitWidth
text: qsTr("Token lists") text: qsTr("Advanced")
} }
} }
@ -105,8 +113,8 @@ ColumnLayout {
return tokensPanel return tokensPanel
case d.collectiblesTabIndex: case d.collectiblesTabIndex:
return collectiblesPanel return collectiblesPanel
case d.tokenSourcesTabIndex: case d.advancedTabIndex:
return supportedTokensListPanel return advancedTab
} }
} }
} }
@ -116,7 +124,6 @@ ColumnLayout {
ManageAssetsPanel { ManageAssetsPanel {
baseModel: root.baseWalletAssetsModel baseModel: root.baseWalletAssetsModel
} }
// TODO #12611 add Advanced section
} }
Component { Component {
@ -128,10 +135,74 @@ ColumnLayout {
} }
Component { Component {
id: supportedTokensListPanel id: advancedTab
SupportedTokenListsPanel { ColumnLayout {
sourcesOfTokensModel: root.sourcesOfTokensModel spacing: 0
tokensListModel: root.tokensListModel 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("Dont 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
}
}
} }
} }
} }

View File

@ -63,7 +63,7 @@ ComboBox {
] ]
markerRoleName: "sourceGroup" markerRoleName: "sourceGroup"
onRowsRemoved: root.clearFilter() // different underlying model -> uncheck all groups onRowsRemoved: root.clearFilter() // different underlying model -> uncheck all
} }
readonly property var combinedProxyModel: SortFilterProxyModel { readonly property var combinedProxyModel: SortFilterProxyModel {
@ -81,12 +81,13 @@ ComboBox {
} }
] ]
filters: [ filters: [
ExpressionFilter { FastExpressionFilter {
enabled: d.searchTextLowerCase !== "" enabled: d.searchTextLowerCase !== ""
expression: { expression: {
d.searchTextLowerCase // ensure expression is reevaluated when searchString changes d.searchTextLowerCase // ensure expression is reevaluated when searchString changes
return model.groupName.toLowerCase().includes(d.searchTextLowerCase) || model.groupId.toLowerCase().includes(d.searchTextLowerCase) return model.groupName.toLowerCase().includes(d.searchTextLowerCase) || model.groupId.toLowerCase().includes(d.searchTextLowerCase)
} }
expectedRoles: ["groupName", "groupId"]
} }
] ]
} }

View File

@ -83,30 +83,34 @@ ColumnLayout {
roleNames: ["collectionName", "communityName"] roleNames: ["collectionName", "communityName"]
} }
filters: [ filters: [
ExpressionFilter { FastExpressionFilter {
expression: { expression: {
d.controller.settingsDirty d.controller.settingsDirty
return d.controller.filterAcceptsSymbol(model.symbol) && (customFilter.isCommunity ? !!model.communityId : !model.communityId) return d.controller.filterAcceptsSymbol(model.symbol) && (customFilter.isCommunity ? !!model.communityId : !model.communityId)
} }
expectedRoles: ["symbol", "communityId"]
}, },
ExpressionFilter { FastExpressionFilter {
enabled: customFilter.isCommunity && cmbFilter.hasEnabledFilters enabled: customFilter.isCommunity && cmbFilter.hasEnabledFilters
expression: cmbFilter.selectedFilterGroupIds.includes(model.communityId) || expression: cmbFilter.selectedFilterGroupIds.includes(model.communityId) ||
(!model.communityId && cmbFilter.selectedFilterGroupIds.includes("")) (!model.communityId && cmbFilter.selectedFilterGroupIds.includes(""))
expectedRoles: ["communityId"]
}, },
ExpressionFilter { FastExpressionFilter {
enabled: !customFilter.isCommunity && cmbFilter.hasEnabledFilters enabled: !customFilter.isCommunity && cmbFilter.hasEnabledFilters
expression: cmbFilter.selectedFilterGroupIds.includes(model.collectionUid) || expression: cmbFilter.selectedFilterGroupIds.includes(model.collectionUid) ||
(!model.collectionUid && cmbFilter.selectedFilterGroupIds.includes("")) (!model.collectionUid && cmbFilter.selectedFilterGroupIds.includes(""))
expectedRoles: ["collectionUid"]
} }
] ]
sorters: [ sorters: [
ExpressionSorter { FastExpressionSorter {
expression: { expression: {
d.controller.settingsDirty d.controller.settingsDirty
return d.controller.lessThan(modelLeft.symbol, modelRight.symbol) return d.controller.lessThan(modelLeft.symbol, modelRight.symbol)
} }
enabled: d.isCustomView enabled: d.isCustomView
expectedRoles: ["symbol"]
}, },
RoleSorter { RoleSorter {
roleName: cmbTokenOrder.currentSortRoleName roleName: cmbTokenOrder.currentSortRoleName

View File

@ -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
}
}

View File

@ -7,6 +7,7 @@ ContactSelector 1.0 ContactSelector.qml
ContactsListAndSearch 1.0 ContactsListAndSearch.qml ContactsListAndSearch 1.0 ContactsListAndSearch.qml
CopyButton 1.0 CopyButton.qml CopyButton 1.0 CopyButton.qml
CopyToClipBoardButton 1.0 CopyToClipBoardButton.qml CopyToClipBoardButton 1.0 CopyToClipBoardButton.qml
CurrencyAmountInput 1.0 CurrencyAmountInput.qml
DisabledTooltipButton 1.0 DisabledTooltipButton.qml DisabledTooltipButton 1.0 DisabledTooltipButton.qml
EmojiHash 1.0 EmojiHash.qml EmojiHash 1.0 EmojiHash.qml
ErrorDetails 1.0 ErrorDetails.qml ErrorDetails 1.0 ErrorDetails.qml