diff --git a/doc/src/images/status_pin_input.png b/doc/src/images/status_pin_input.png new file mode 100644 index 00000000..3b2563df Binary files /dev/null and b/doc/src/images/status_pin_input.png differ diff --git a/doc/src/statusqcontrols.qdoc b/doc/src/statusqcontrols.qdoc index fb4fcd5f..fd022549 100644 --- a/doc/src/statusqcontrols.qdoc +++ b/doc/src/statusqcontrols.qdoc @@ -33,6 +33,7 @@ \li \l{StatusIdenticonRing} \li \l{StatusInput} \li \l{StatusPickerButton} + \li \l{StatusPinInput} \li \l{StatusProgressBar} \li \l{StatusPasswordStrengthIndicator} \li \l{StatusSwitchTabButton} @@ -40,5 +41,7 @@ \li \l{StatusSelectableText} \li \l{StatusWalletColorButton} \li \l{StatusWalletColorSelect} + \li \l{StatusRegularExpressionValidator} + \li \l{StatusIntValidator} \endlist */ diff --git a/sandbox/main.qml b/sandbox/main.qml index 197f026e..0bd1fb34 100644 --- a/sandbox/main.qml +++ b/sandbox/main.qml @@ -229,6 +229,11 @@ StatusWindow { selected: viewLoader.source.toString().includes(title) onClicked: mainPageView.page(title); } + StatusNavigationListItem { + title: "StatusPinInput" + selected: viewLoader.source.toString().includes(title) + onClicked: mainPageView.page(title); + } StatusListSectionHeadline { text: "StatusQ.Components" } StatusNavigationListItem { title: "StatusAddress" diff --git a/sandbox/pages/StatusPinInputPage.qml b/sandbox/pages/StatusPinInputPage.qml new file mode 100644 index 00000000..c4143889 --- /dev/null +++ b/sandbox/pages/StatusPinInputPage.qml @@ -0,0 +1,75 @@ +import QtQuick 2.14 +import QtQuick.Layouts 1.14 + +import StatusQ.Core.Theme 0.1 +import StatusQ.Core 0.1 +import StatusQ.Controls 0.1 +import StatusQ.Controls.Validators 0.1 + +Column { + id: root + spacing: 25 + Layout.alignment: Qt.AlignHCenter | Qt.AlignVCenter + + // PIN input that accepts only numbers + StatusBaseText { + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.palette.directColor1 + text: "Enter Keycard PIN" + font.pixelSize: 30 + font.bold: true + } + + StatusPinInput { + id: numbersPinInput + anchors.horizontalCenter: parent.horizontalCenter + validator: StatusIntValidator{bottom: 0; top: 999999;} + } + + StatusBaseText { + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.palette.dangerColor1 + text: "Only numbers allowed" + font.pixelSize: 16 + } + + StatusBaseText { + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.palette.directColor1 + text: "Introduced PIN: " + numbersPinInput.pinInput + font.pixelSize: 12 + } + + // PIN input that accepts input depending on the regular expression definition + StatusBaseText { + topPadding: 100 + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.palette.directColor1 + text: "Enter another Keycard PIN" + font.pixelSize: 30 + font.bold: true + } + + StatusPinInput { + id: regexPinInput + anchors.horizontalCenter: parent.horizontalCenter + validator: StatusRegularExpressionValidator { regularExpression: /[0-9A-Za-z@]+/ } + circleDiameter: 22 + circleSpacing: 22 + pinLen: 7 + } + + StatusBaseText { + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.palette.dangerColor1 + text: "Only alphanumeric characters and '@' allowed" + font.pixelSize: 16 + } + + StatusBaseText { + anchors.horizontalCenter: parent.horizontalCenter + color: Theme.palette.directColor1 + text: "Introduced PIN: " + regexPinInput.pinInput + font.pixelSize: 12 + } +} diff --git a/src/StatusQ/Controls/StatusPinInput.qml b/src/StatusQ/Controls/StatusPinInput.qml new file mode 100644 index 00000000..bd6185e2 --- /dev/null +++ b/src/StatusQ/Controls/StatusPinInput.qml @@ -0,0 +1,234 @@ +import QtQuick 2.0 +import StatusQ.Core.Theme 0.1 +import StatusQ.Controls.Validators 0.1 + +/*! + \qmltype StatusPinInput + \inherits Item + \inqmlmodule StatusQ.Controls + \since StatusQ.Controls 0.1 + \brief It allows entering an N pin length. + + The \c StatusPinInput displays N visual circles corresponging with the pin length to introduce. + + It runs a blinking animation when the control is focused and ready to introduce the pin, as well as, an hovering feedback when user is in the MouseArea where is able to click into the control. + + This pin input control allows introducing validators. + + Example of how the control looks like: + \image status_pin_input.png + + Example of how to use it: + + \qml + StatusPinInput { + id: regexPinInput + validator: StatusRegularExpressionValidator { regularExpression: /[0-9A-Za-z@]+/ } + circleDiameter: 22 + circleSpacing: 22 + pinLen: 7 + } + \endqml + + For a list of components available see StatusQ. +*/ +Item { + id: root + + /*! + \qmlproperty string StatusPinInput::pinInput + This property holds the introduced user pin. + */ + property alias pinInput: inputText.text + + /*! + \qmlproperty Validator StatusPinInput::validator + This property allows you to set a validator on the StatusPinInput. When a validator is set the StatusPinInput will only accept + input which leaves the pinInput property in an acceptable state. + + Currently supported validators are qml StatusIntValidator and StatusRegularExpressionValidator. + + An example of using validators is shown below, which allows input of integers between 0 and 999999 into the pin input + + \qml + StatusPinInput { + id: numbersPinInput + validator: StatusIntValidator{bottom: 0; top: 999999;} + } + \endqml + */ + property alias validator: d.statusValidator + + /*! + \qmlproperty int StatusPinInput::pinLen + This property allows you to set a specific pin input length. The default value is 6. + */ + property int pinLen: 6 + + /*! + \qmlproperty int StatusPinInput::circleSpacing + This property allows you to customize spacing between pin circles. The default value is 16 pixels. + */ + property int circleSpacing: 16 + + /*! + \qmlproperty int StatusPinInput::circleDiameter + This property allows you to customize pin circle diameter. The default value is 16 pixels. + */ + property int circleDiameter: 16 + + QtObject { + id: d + property int currentPinIndex: 0 + + property StatusValidator statusValidator + + function activateBlink () { + const currItem = repeater.itemAt(d.currentPinIndex) + if(currItem) { + if((currItem.innerState === "NEXT") && (!currItem.blinkingAnimation.running)) { + currItem.blinkingAnimation.start() + } + } + } + + function deactivateBlink () { + const currItem = repeater.itemAt(d.currentPinIndex) + if(currItem) { + if((currItem.innerState === "NEXT") && (currItem.blinkingAnimation.running)) { + currItem.blinkingAnimation.stop() + + // To ensure that the opacity does not remain in an intermediate state when forcing the animation to stop + currItem.innerOpacity = 1 + } + } + } + } + + /* + \qmlmethod StatusPinInput::statesInitialization() + + Initializes pin input bringing it to its initial state. + + It is directly called in Component.onCompleted slot and can be called whenever you need resetting it. + */ + function statesInitialization() { + d.currentPinIndex = 0 + repeater.itemAt(d.currentPinIndex).innerState = "NEXT" + for (var i = 1; i < root.pinLen; i++) { + repeater.itemAt(i).innerState = "EMPTY" + } + } + + width: (root.circleDiameter + root.circleSpacing) * root.pinLen + height: root.circleDiameter + + Component.onCompleted: { statesInitialization() } + + // Pin input data management object: + TextInput { + id: inputText + visible: false + focus: true + maximumLength: root.pinLen + validator: d.statusValidator.validatorObj + onTextChanged: { + // Modify state of current introduced character position: + if(text.length >= (d.currentPinIndex + 1)) { + repeater.itemAt(d.currentPinIndex).innerState = "FILLED" + + // Update next: + d.currentPinIndex++ + if(d.currentPinIndex < root.pinLen) + repeater.itemAt(d.currentPinIndex).innerState = "NEXT" + + } + // Modify state of current removed character position: + else if (text.length <= (d.currentPinIndex + 1)) { + if(d.currentPinIndex < root.pinLen) + repeater.itemAt(d.currentPinIndex).innerState = "EMPTY" + d.currentPinIndex-- + repeater.itemAt(d.currentPinIndex).innerState = "NEXT" + } + + // Some component validations: + if(text.length !== d.currentPinIndex) + console.error("StatusPinInput input management error. Current pin length must be "+ text.length + "and is " + d.currentPinIndex) + } + onFocusChanged: { if(!focus) { d.deactivateBlink () } } + } + + // Pin input visual objects: + Row { + spacing: root.circleSpacing + + Repeater { + id: repeater + model: root.pinLen + + Rectangle { + id: background + property string innerState: "EMPTY" + property alias blinkingAnimation: blinkingAnimation + property alias innerOpacity: inner.opacity + + width: root.circleDiameter + height: width + color: Theme.palette.primaryColor2 + radius: 0.5 * width + + Rectangle { + id: inner + state: background.innerState + anchors.centerIn: parent + height: width + color: Theme.palette.primaryColor1 + radius: 0.5 * width + states: [ + State { + name: "NEXT" + StateChangeScript { script: { if(inputText.focus) blinkingAnimation.start() } } + PropertyChanges {target: inner; width: root.circleDiameter / 2} + }, + State { + name: "FILLED" + StateChangeScript { script: if(blinkingAnimation.running) blinkingAnimation.stop() } + PropertyChanges {target: inner; width: root.circleDiameter} + PropertyChanges {target: inner; opacity: 1} + }, + State { + name: "EMPTY" + StateChangeScript { script: if(blinkingAnimation.running) blinkingAnimation.stop() } + PropertyChanges {target: inner; width: 0} + PropertyChanges {target: inner; opacity: 1} + } + ] + + // Animation on transitions + Behavior on width { NumberAnimation { duration: 200 } } + + // Animation on "cursor" blinking + SequentialAnimation { + id: blinkingAnimation + loops: Animation.Infinite + NumberAnimation { target: inner; property: "opacity"; to: 0; duration: 800 } + NumberAnimation { target: inner; property: "opacity"; to: 1; duration: 800 } + } + } + } + } + } + + MouseArea { + id: mouseArea + anchors.fill: parent + hoverEnabled: true + + // MouseArea behavior: + onClicked: { + inputText.forceActiveFocus() + d.activateBlink () + } + onContainsMouseChanged: { if(containsMouse) { cursorShape = Qt.PointingHandCursor } } + } +} diff --git a/src/StatusQ/Controls/Validators/StatusIntValidator.qml b/src/StatusQ/Controls/Validators/StatusIntValidator.qml new file mode 100644 index 00000000..5ccc9f24 --- /dev/null +++ b/src/StatusQ/Controls/Validators/StatusIntValidator.qml @@ -0,0 +1,57 @@ +import QtQuick 2.14 + +import StatusQ.Controls 0.1 + +/*! + \qmltype StatusIntValidator + \inherits StatusValidator + \inqmlmodule StatusQ.Controls.Validators + \since StatusQ.Controls.Validators 0.1 + \brief The StatusIntValidator type provides a validator for integer values. + + The \c StatusIntValidator type provides a validator for integer values. + + It is a wrapper of QML type \l{https://doc.qt.io/qt-5/qml-qtquick-intvalidator.html}{IntValidator}. + + Example of how to use it: + + \qml + StatusIntValidator { + bottom: 0 + top: 125 + errorMessage: qsTr("This is an invalid numeric value") + } + \endqml + + For a list of components available see StatusQ. +*/ +StatusValidator { + id: root + + /*! + \qmlproperty string StatusIntValidator::bottom + This property holds the validator's lowest acceptable value. By default, this property's value is derived from the lowest signed integer available (typically -2147483647). + */ + property int bottom + + /*! + \qmlproperty string StatusIntValidator::locale + This property holds the name of the locale used to interpret the number. + */ + property string locale + + /*! + \qmlproperty string StatusIntValidator::top + This property holds the validator's highest acceptable value. By default, this property's value is derived from the highest signed integer available (typically 2147483647). + */ + property int top + + name: "intValidator" + errorMessage: "Please enter a valid numeric value." + validatorObj: IntValidator { bottom: root.bottom; locale: root.locale; top: root.top } + + validate: function (t) { + // Basic validation management + return root.validatorObj.validate() === IntValidator.Acceptable + } +} diff --git a/src/StatusQ/Controls/Validators/StatusRegularExpressionValidator.qml b/src/StatusQ/Controls/Validators/StatusRegularExpressionValidator.qml new file mode 100644 index 00000000..8a6b0157 --- /dev/null +++ b/src/StatusQ/Controls/Validators/StatusRegularExpressionValidator.qml @@ -0,0 +1,60 @@ +import QtQuick 2.14 + +import StatusQ.Controls 0.1 + +/*! + \qmltype StatusRegularExpressionValidator + \inherits StatusValidator + \inqmlmodule StatusQ.Controls.Validators + \since StatusQ.Controls.Validators 0.1 + \brief The StatusRegularExpressionValidator type provides a validator for regular expressions. + + The \c StatusRegularExpressionValidator type provides a validator, that counts as valid any string which matches a specified regular expression. + + It is a wrapper of QML type \l{https://doc.qt.io/qt-5/qml-qtquick-regularexpressionvalidator.html}{RegularExpressionValidator}. + + Example of how to use it: + + \qml + StatusRegularExpressionValidator { + regularExpression: /[0-9A-Za-z@]+/ + errorMessage: qsTr("Please enter a valid regular expression.") + } + \endqml + + For a list of components available see StatusQ. +*/ +StatusValidator { + id: root + + /*! + \qmlproperty string StatusRegularExpressionValidator::regularExpression + This property holds the regular expression used for validation. + + Note that this property should be a regular expression in JS syntax, e.g /a/ for the regular expression matching "a". + + By default, this property contains a regular expression with the pattern .* that matches any string. + + Some more examples of regular expressions: + + > A list of numbers with one to three positions separated by a comma: + \qml + /\d{1,3}(?:,\d{1,3})+$/ + \endqml + + > An amount consisting of up to 3 numbers before the decimal point, and 1 to 2 after the decimal point: + \qml + /(\d{1,3})([.,]\d{1,2})?$/ + \endqml + */ + property var regularExpression + + name: "regex" + errorMessage: "Please enter a valid regular expression." + validatorObj: RegularExpressionValidator { regularExpression: root.regularExpression } + + validate: function (t) { + // Basic validation management + return root.validatorObj.validate() === RegularExpressionValidator.Acceptable + } +} diff --git a/src/StatusQ/Controls/Validators/StatusValidator.qml b/src/StatusQ/Controls/Validators/StatusValidator.qml index 6e4353cd..ba62778d 100644 --- a/src/StatusQ/Controls/Validators/StatusValidator.qml +++ b/src/StatusQ/Controls/Validators/StatusValidator.qml @@ -6,6 +6,7 @@ QtObject { property string name: "" property string errorMessage: "invalid input" + property var validatorObj property var validate: function (value) { return true diff --git a/src/StatusQ/Controls/Validators/qmldir b/src/StatusQ/Controls/Validators/qmldir index 6299a5da..55dcb715 100644 --- a/src/StatusQ/Controls/Validators/qmldir +++ b/src/StatusQ/Controls/Validators/qmldir @@ -4,7 +4,9 @@ StatusAddressValidator 0.1 StatusAddressValidator.qml StatusAddressOrEnsValidator 0.1 StatusAddressOrEnsValidator.qml StatusAsyncValidator 0.1 StatusAsyncValidator.qml StatusAsyncEnsValidator 0.1 StatusAsyncEnsValidator.qml +StatusIntValidator 0.1 StatusIntValidator.qml StatusMinLengthValidator 0.1 StatusMinLengthValidator.qml StatusMaxLengthValidator 0.1 StatusMaxLengthValidator.qml +StatusRegularExpressionValidator 0.1 StatusRegularExpressionValidator.qml StatusUrlValidator 0.1 StatusUrlValidator.qml StatusValidator 0.1 StatusValidator.qml diff --git a/src/StatusQ/Controls/qmldir b/src/StatusQ/Controls/qmldir index d745f8b3..20971b9b 100644 --- a/src/StatusQ/Controls/qmldir +++ b/src/StatusQ/Controls/qmldir @@ -25,6 +25,7 @@ StatusSelect 0.1 StatusSelect.qml StatusBaseInput 0.1 StatusBaseInput.qml StatusInput 0.1 StatusInput.qml StatusPickerButton 0.1 StatusPickerButton.qml +StatusPinInput 0.1 StatusPinInput.qml StatusProgressBar 0.1 StatusProgressBar.qml StatusPasswordStrengthIndicator 0.1 StatusPasswordStrengthIndicator.qml StatusSwitchTabButton 0.1 StatusSwitchTabButton.qml diff --git a/statusq.qrc b/statusq.qrc index a0aff12c..00821eaf 100644 --- a/statusq.qrc +++ b/statusq.qrc @@ -326,5 +326,8 @@ src/StatusQ/Components/StatusContactVerificationIcons.qml src/StatusQ/Controls/StatusIdenticonRing.qml src/StatusQ/Core/StatusIdenticonRingSettings.qml + src/StatusQ/Controls/StatusPinInput.qml + src/StatusQ/Controls/Validators/StatusRegularExpressionValidator.qml + src/StatusQ/Controls/Validators/StatusIntValidator.qml