StatusQ: QValidator fully exposed to QML

Closes: #15581
This commit is contained in:
Michał Cieślak 2024-07-15 15:17:21 +02:00 committed by Michał
parent 26508f5c91
commit 44808424a4
5 changed files with 377 additions and 0 deletions

View File

@ -0,0 +1,123 @@
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import StatusQ 0.1
Pane {
id: root
ColumnLayout {
anchors.centerIn: parent
RowLayout {
Label {
text: "Capitalized words:"
}
TextField {
validator: GenericValidator {
validate: {
const split = input.split(' ')
const upperCase = split.map(w =>
w.charAt(0).toUpperCase()
+ w.slice(1).toLowerCase())
return {
state: GenericValidator.Acceptable,
output: upperCase.join(' ')
}
}
}
}
}
RowLayout {
Label {
text: "Decimal numbers, replacing ',' with '.':"
}
TextField {
id: decimalsTextField
validator: GenericValidator {
validate: {
if (input.length === 0)
return GenericValidator.Intermediate
const validCharSet = /^[0-9\.\,]*$/.test(input)
if (!validCharSet)
return GenericValidator.Invalid
const pointFixed = input.replace(",", ".")
const pointsCount = (pointFixed.match(/\./g) || []).length
const wellFormed = pointFixed.charAt(0) !== '.'
&& pointFixed.charAt(
pointFixed.length - 1) !== '.'
if (pointsCount > 1)
return GenericValidator.Invalid
return {
state: wellFormed ? GenericValidator.Acceptable
: GenericValidator.Intermediate,
output: pointFixed
}
}
}
}
Label {
text: `acceptable: ${decimalsTextField.acceptableInput}`
}
}
RowLayout {
Label {
text: "Position always at 0:"
}
TextField {
validator: GenericValidator {
validate: ({
state: GenericValidator.Acceptable,
pos: 0
})
}
}
}
RowLayout {
Label {
text: "Maximum number of characters:"
}
TextField {
id: limitedTextField
validator: GenericValidator {
validate: input.length <= slider.value
}
}
Label {
text: `acceptable: ${limitedTextField.acceptableInput}`
}
Slider {
id: slider
from: 3
to: 10
stepSize: 1
}
Label {
text: `max: ${slider.value}`
}
}
}
}

View File

@ -100,6 +100,7 @@ add_library(StatusQ SHARED
include/StatusQ/fastexpressionsorter.h
include/StatusQ/formatteddoubleproperty.h
include/StatusQ/functionaggregator.h
include/StatusQ/genericvalidator.h
include/StatusQ/groupingmodel.h
include/StatusQ/leftjoinmodel.h
include/StatusQ/modelcount.h
@ -129,6 +130,7 @@ add_library(StatusQ SHARED
src/fastexpressionsorter.cpp
src/formatteddoubleproperty.cpp
src/functionaggregator.cpp
src/genericvalidator.cpp
src/groupingmodel.cpp
src/leftjoinmodel.cpp
src/modelcount.cpp

View File

@ -0,0 +1,57 @@
#pragma once
#include <QValidator>
#include <QQmlScriptString>
#include <QQmlPropertyMap>
#include <memory>
class QQmlExpression;
class GenericValidator : public QValidator
{
Q_OBJECT
Q_PROPERTY(QQmlScriptString fixup
READ fixupScriptString WRITE setFixupScriptString
NOTIFY fixupScriptStringChanged)
Q_PROPERTY(QQmlScriptString validate
READ validateScriptString WRITE setValidateScriptString
NOTIFY validateScriptStringChanged)
public:
enum State {
Invalid = QValidator::Invalid,
Intermediate = QValidator::Intermediate,
Acceptable = QValidator::Acceptable
};
Q_ENUM(State)
explicit GenericValidator(QObject* parent = nullptr);
const QQmlScriptString& fixupScriptString() const;
void setFixupScriptString(const QQmlScriptString& scriptString);
const QQmlScriptString& validateScriptString() const;
void setValidateScriptString(const QQmlScriptString& scriptString);
void fixup(QString& input) const override;
QValidator::State validate(QString& input, int& pos) const override;
signals:
void fixupScriptStringChanged();
void validateScriptStringChanged();
private:
bool isValidState(int state) const;
QQmlScriptString m_fixupScriptString;
QQmlScriptString m_validateScriptString;
mutable QQmlPropertyMap m_fixupScope;
mutable QQmlPropertyMap m_validateScope;
std::unique_ptr<QQmlExpression> m_fixupExpression;
std::unique_ptr<QQmlExpression> m_validateExpression;
};

View File

@ -0,0 +1,189 @@
#include "StatusQ/genericvalidator.h"
#include <QDebug>
#include <QJSValue>
#include <QQmlEngine>
#include <QQmlExpression>
#include <QScopeGuard>
/*!
\qmltype GenericValidator
\instantiates GenericValidator
\inqmlmodule StatusQ
\inherits QValidator
\brief Exposes \l {QValidator} interface to QML.
It allows defining fully featured validators directly from QML. Validate
expression can return bool (Invalid/Acceptable), `State`
(Invalid/Intermediate/Acceptable) or object with following properties:
- state - result of validation of type GenericValidator.State
- output - optional - value overriding input
- pos - optional - new position of the cursor
Within the validate expression there are two parameters available:
- input (string intended to be validated)
- pos (current position of the cursor)
*/
GenericValidator::GenericValidator(QObject* parent)
: QValidator(parent)
{
}
const QQmlScriptString& GenericValidator::fixupScriptString() const
{
return m_fixupScriptString;
}
void GenericValidator::setFixupScriptString(
const QQmlScriptString& scriptString)
{
if (m_fixupScriptString == scriptString)
return;
m_fixupScriptString = scriptString;
m_fixupExpression = std::make_unique<QQmlExpression>(
m_fixupScriptString, qmlContext(this), &m_fixupScope);
emit fixupScriptStringChanged();
}
const QQmlScriptString& GenericValidator::validateScriptString() const
{
return m_validateScriptString;
}
void GenericValidator::setValidateScriptString(
const QQmlScriptString& scriptString)
{
if (m_validateScriptString == scriptString)
return;
m_validateScriptString = scriptString;
m_validateExpression = std::make_unique<QQmlExpression>(
m_validateScriptString, qmlContext(this), &m_validateScope);
m_validateExpression->setNotifyOnValueChanged(true);
connect(m_validateExpression.get(), &QQmlExpression::valueChanged, this,
&QValidator::changed);
emit validateScriptStringChanged();
emit changed();
}
void GenericValidator::fixup(QString& input) const
{
if (!m_fixupExpression)
return;
m_fixupScope.insert("input", input);
m_fixupExpression->clearError();
QVariant value = m_fixupExpression->evaluate();
if (m_fixupExpression->hasError()) {
qWarning() << m_fixupExpression->error();
return;
}
if (value.type() == QVariant::String)
input = m_fixupExpression->evaluate().toString();
else
qWarning() << "Validator: fixup expression must return string.";
}
QValidator::State GenericValidator::validate(QString& input, int& pos) const
{
// To avoid unnecessary `QValidator::changed` calls the signal is temporarily
// disconnected. For some reason QQmlExpression::setNotifyOnValueChanged(true)
// doesn't work as expected when used for that purpose. Once set to false,
// changes are no longer notified even after switching back to true.
m_validateExpression->disconnect(this);
QScopeGuard guard([this]() {
connect(m_validateExpression.get(), &QQmlExpression::valueChanged, this,
&QValidator::changed);
});
m_validateScope.insert("input", input);
m_validateScope.insert("pos", pos);
m_validateExpression->clearError();
QVariant value = m_validateExpression->evaluate();
if (m_validateExpression->hasError()) {
qWarning() << m_validateExpression->error();
return QValidator::Invalid;
}
if (value.type() == QVariant::Bool)
return value.toBool() ? QValidator::Acceptable : QValidator::Invalid;
if (value.type() == QVariant::Int) {
auto stateInt = value.toInt();
if (isValidState(stateInt)) {
return static_cast<QValidator::State>(stateInt);
} else {
qWarning() << "Validator: numeric value returned from validate "
"expression must be one of GenericValidator.Invalid, "
"GenericValidator.Intermediate or "
"GenericValidator.Acceptable";
return QValidator::Invalid;
}
}
QJSValue jsValue = value.value<QJSValue>();
if (!jsValue.isObject()) {
qWarning() << "Validator: validate expression must return bool, "
"Validator.State or object.";
return QValidator::Invalid;
}
if (!jsValue.hasProperty("state")) {
qWarning() << "Validator: object returned from validate expression "
"must contain state property of type Validator.State.";
return QValidator::Invalid;
}
QJSValue stateValue = value.value<QJSValue>().property("state");
if (!stateValue.isNumber() || !isValidState(stateValue.toInt())) {
qWarning() << "Validator: state property of object returned from "
"validate expression must be of type Validator.State.";
return QValidator::Invalid;
}
int state = stateValue.toInt();
if (jsValue.hasProperty("output")) {
QJSValue outputProperty = jsValue.property("output");
if (outputProperty.isString())
input = outputProperty.toString();
else
qWarning() << "Validator: 'output' property must be a string.";
}
if (jsValue.hasProperty("pos")) {
QJSValue posProperty = jsValue.property("pos");
if (posProperty.isNumber())
pos = posProperty.toInt();
else
qWarning() << "Validator: 'pos' property must be an integer.";
}
return static_cast<QValidator::State>(state);
}
bool GenericValidator::isValidState(int state) const
{
auto stateCasted = static_cast<QValidator::State>(state);
return stateCasted == QValidator::Invalid
|| stateCasted == QValidator::Intermediate
|| stateCasted == QValidator::Acceptable;
}

View File

@ -10,6 +10,7 @@
#include "StatusQ/fastexpressionsorter.h"
#include "StatusQ/formatteddoubleproperty.h"
#include "StatusQ/functionaggregator.h"
#include "StatusQ/genericvalidator.h"
#include "StatusQ/groupingmodel.h"
#include "StatusQ/leftjoinmodel.h"
#include "StatusQ/modelcount.h"
@ -45,6 +46,11 @@ public:
qmlRegisterType<StatusSyntaxHighlighter>("StatusQ", 0, 1, "StatusSyntaxHighlighter");
qmlRegisterType<RXValidator>("StatusQ", 0, 1, "RXValidator");
qmlRegisterUncreatableType<QValidator>(
"StatusQ", 0, 1,
"Validator", "This is abstract type, cannot be created directly.");
qmlRegisterType<GenericValidator>("StatusQ", 0, 1, "GenericValidator");
qmlRegisterType<ManageTokensController>("StatusQ.Models", 0, 1, "ManageTokensController");
qmlRegisterType<ManageTokensModel>("StatusQ.Models", 0, 1, "ManageTokensModel");