diff --git a/storybook/pages/AmountValidatorPage.qml b/storybook/pages/AmountValidatorPage.qml new file mode 100644 index 0000000000..e349a114ee --- /dev/null +++ b/storybook/pages/AmountValidatorPage.qml @@ -0,0 +1,86 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 +import QtQuick.Layouts 1.15 + +import StatusQ.Validators 0.1 + +Item { + id: root + + ColumnLayout { + anchors.centerIn: parent + + TextField { + id: textField + + Layout.alignment: Qt.AlignHCenter + + validator: AmountValidator { + decimalPoint: buttonGroup.checkedButton.decimalPoint + + maxIntegralDigits: maxIntegralDigitsSpinBox.value + maxDecimalDigits: maxDecimalDigitsSpinBox.value + } + } + + Label { + Layout.alignment: Qt.AlignHCenter + + text: `acceptableInput: ${textField.acceptableInput}` + } + + ButtonGroup { + id: buttonGroup + + buttons: radioButtonsRow.children + } + + RowLayout { + id: radioButtonsRow + + Layout.alignment: Qt.AlignHCenter + + RadioButton { + checked: true + text: "period (.)" + + readonly property string decimalPoint: "." + } + + RadioButton { + text: "comma (,)" + + readonly property string decimalPoint: "," + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + + Label { + text: "Max number of integral digits:" + } + + SpinBox { + id: maxIntegralDigitsSpinBox + value: 10 + } + } + + RowLayout { + Layout.alignment: Qt.AlignHCenter + + Label { + text: "Max number of decimal digits:" + } + + SpinBox { + id: maxDecimalDigitsSpinBox + + value: 5 + } + } + } +} + +// category: Validators diff --git a/storybook/qmlTests/tests/tst_AmountValidator.qml b/storybook/qmlTests/tests/tst_AmountValidator.qml new file mode 100644 index 0000000000..ed46469327 --- /dev/null +++ b/storybook/qmlTests/tests/tst_AmountValidator.qml @@ -0,0 +1,175 @@ +import QtQuick 2.15 +import QtQuick.Controls 2.15 + +import QtTest 1.15 + +import StatusQ.Validators 0.1 + +Item { + id: root + + Component { + id: componentUnderTest + + TextField { + validator: AmountValidator { + id: validator + + decimalPoint: "." + } + } + } + + property TextField textField + + TestCase { + name: "AmountValidator" + when: windowShown + + function type(key, times = 1) { + for (let i = 0; i < times; i++) { + keyPress(key) + keyRelease(key) + } + } + + function init() { + textField = createTemporaryObject(componentUnderTest, root) + } + + function test_empty() { + compare(textField.acceptableInput, false) + } + + function test_decimalPointOnly() { + textField.forceActiveFocus() + type(Qt.Key_Period) + + compare(textField.acceptableInput, false) + compare(textField.text, ".") + + type(Qt.Key_Period) + + compare(textField.acceptableInput, false) + compare(textField.text, ".") + + textField.text = "" + + type(Qt.Key_Comma) + + compare(textField.acceptableInput, false) + compare(textField.text, ".") + + type(Qt.Key_Comma) + type(Qt.Key_Period) + + compare(textField.acceptableInput, false) + compare(textField.text, ".") + + textField.text = "" + textField.validator.decimalPoint = "," + + type(Qt.Key_Period) + + compare(textField.acceptableInput, false) + compare(textField.text, ",") + + type(Qt.Key_Comma) + type(Qt.Key_Period) + + compare(textField.acceptableInput, false) + compare(textField.text, ",") + } + + function test_decimalPointWithDigits() { + textField.forceActiveFocus() + type(Qt.Key_1) + type(Qt.Key_Period) + + compare(textField.acceptableInput, true) + compare(textField.text, "1.") + + type(Qt.Key_1) + type(Qt.Key_Period) + + compare(textField.acceptableInput, true) + compare(textField.text, "1.1") + + textField.text = "" + type(Qt.Key_Period) + type(Qt.Key_1) + + compare(textField.acceptableInput, true) + compare(textField.text, ".1") + } + + function test_maxIntegralDigits() { + textField.forceActiveFocus() + textField.validator.maxIntegralDigits = 2 + + type(Qt.Key_1) + type(Qt.Key_1) + + compare(textField.acceptableInput, true) + compare(textField.text, "11") + + type(Qt.Key_2) + type(Qt.Key_2) + + compare(textField.acceptableInput, true) + compare(textField.text, "11") + + type(Qt.Key_Period) + type(Qt.Key_3) + type(Qt.Key_3) + + compare(textField.acceptableInput, true) + compare(textField.text, "11.33") + } + + function test_maxDecimalDigits() { + textField.forceActiveFocus() + textField.validator.maxDecimalDigits = 2 + + type(Qt.Key_Period) + type(Qt.Key_1) + type(Qt.Key_1) + + compare(textField.acceptableInput, true) + compare(textField.text, ".11") + + type(Qt.Key_2) + type(Qt.Key_2) + + compare(textField.acceptableInput, true) + compare(textField.text, ".11") + + textField.cursorPosition = 0 + + type(Qt.Key_2) + type(Qt.Key_2) + + compare(textField.acceptableInput, true) + compare(textField.text, "22.11") + } + + function test_trimmingDecimalDigits() { + textField.text = ".11" + textField.forceActiveFocus() + textField.validator.maxDecimalDigits = 2 + textField.cursorPosition = 1 + + type(Qt.Key_2) + type(Qt.Key_2) + + compare(textField.acceptableInput, true) + compare(textField.text, ".22") + + type(Qt.Key_3) + type(Qt.Key_3) + + compare(textField.acceptableInput, true) + compare(textField.text, ".22") + } + } +} diff --git a/ui/StatusQ/src/StatusQ/Validators/AmountValidator.qml b/ui/StatusQ/src/StatusQ/Validators/AmountValidator.qml new file mode 100644 index 0000000000..8c105d7ffe --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Validators/AmountValidator.qml @@ -0,0 +1,69 @@ +import StatusQ 0.1 + +/*! + \qmltype AmountValidator + \inherits GenericValidator + \inqmlmodule StatusQ.Validators + \brief Validator validating amounts provided by the user. + + The validator do following checks: + + - marks empty input and consisting only of decimal point as Intermediate + - limits allowed char set - digits and (only when maxDecimalDigits is not 0) + two decimal point characters are available (".", ",") + - replaces entered decimal point to the one provied via decimalPoint property + - blocks attemps of entering more then one decimal point char + - limits number of integral part specified by maxIntegralDigits + - trims number of decimal part specified by maxDecimalDigits + - numbers starting or ending with decimal point like .1 or 1. are considered + as valid input + */ +GenericValidator { + id: root + + property string decimalPoint: "." + property int maxIntegralDigits: 10 + property int maxDecimalDigits: 10 + + validate: { + if (input.length === 0) + return GenericValidator.Intermediate + + const charSetRegex = root.maxDecimalDigits > 0 ? /^[0-9\.\,]*$/ + : /^[0-9]*$/ + const validCharSet = charSetRegex.test(input) + + if (!validCharSet) + return GenericValidator.Invalid + + const delocalized = input.replace(/,/g, ".") + + if (delocalized === ".") + return { + state: GenericValidator.Intermediate, + output: root.decimalPoint + } + + const pointsCount = (delocalized.match(/\./g) || []).length + + if (pointsCount > 1) + return GenericValidator.Invalid + + const [integral, decimal] = pointsCount ? delocalized.split(".") + : [delocalized, ""] + + if (integral.length > root.maxIntegralDigits) + return GenericValidator.Invalid + + if (pointsCount === 0) + return GenericValidator.Acceptable + + const decimalTrimmed = decimal.slice(0, root.maxDecimalDigits) + const localized = [integral, decimalTrimmed].join(root.decimalPoint) + + return { + state: GenericValidator.Acceptable, + output: localized + } + } +} diff --git a/ui/StatusQ/src/StatusQ/Validators/qmldir b/ui/StatusQ/src/StatusQ/Validators/qmldir new file mode 100644 index 0000000000..2cbe7ceaaf --- /dev/null +++ b/ui/StatusQ/src/StatusQ/Validators/qmldir @@ -0,0 +1,3 @@ +module StatusQ.Validators + +AmountValidator 0.1 AmountValidator.qml diff --git a/ui/StatusQ/src/statusq.qrc b/ui/StatusQ/src/statusq.qrc index 6e28c421e4..7e256de84a 100644 --- a/ui/StatusQ/src/statusq.qrc +++ b/ui/StatusQ/src/statusq.qrc @@ -249,5 +249,7 @@ StatusQ/Popups/statusModal/StatusImageWithTitle.qml StatusQ/Popups/statusModal/StatusModalFooter.qml StatusQ/Popups/statusModal/StatusModalHeader.qml + StatusQ/Validators/qmldir + StatusQ/Validators/AmountValidator.qml