status-desktop/ui/StatusQ/src/genericvalidator.cpp

203 lines
6.1 KiB
C++

#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;
}
QString GenericValidator::localeName() const {
return locale().name();
}
void GenericValidator::setLocaleName(const QString &newLocaleName) {
if (newLocaleName == localeName())
return;
setLocale(newLocaleName);
emit localeChanged();
emit changed();
}