mirror of
https://github.com/huderlem/porymap.git
synced 2026-10-02 12:56:38 -05:00
Generalize FieldManager, automatic value validation
This commit is contained in:
@@ -47,6 +47,8 @@
|
||||
Appropriately implementing 'toString'/'fromString' has the added benefit that your type
|
||||
can automatically be used as a JSON key if it for example appears as the key in a QMap.
|
||||
|
||||
// TODO: Document clamp
|
||||
|
||||
*/
|
||||
|
||||
|
||||
@@ -69,6 +71,19 @@ struct DefaultConverter {
|
||||
// Default to identity
|
||||
static QString toString(const T& value) {return value;}
|
||||
static T fromString(const QString& string, QStringList* = nullptr) {return string;}
|
||||
|
||||
static T clamp(const T& value, const T& min, const T& max, QStringList* errors = nullptr) {
|
||||
Q_ASSERT(min <= max);
|
||||
if (value < min) {
|
||||
if (errors) errors->append("Value too low");
|
||||
return min;
|
||||
}
|
||||
if (value > max) {
|
||||
if (errors) errors->append("Value too high");
|
||||
return max;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T, typename Enable = void>
|
||||
@@ -189,6 +204,17 @@ struct Converter<BaseGame::Version> : DefaultStringConverter<BaseGame::Version>
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Converter<std::optional<T>> : DefaultConverter<std::optional<T>> {
|
||||
static QJsonValue toJson(const std::optional<T>& optional) {
|
||||
return optional.has_value() ? Converter<T>::toJson(optional.value()) : QJsonValue();
|
||||
}
|
||||
static std::optional<T> fromJson(const QJsonValue& json, QStringList* errors = nullptr) {
|
||||
if (json.isNull()) return {};
|
||||
return Converter<T>::fromJson(json, errors);
|
||||
}
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
struct Converter<QList<T>> : DefaultConverter<QList<T>> {
|
||||
static QJsonValue toJson(const QList<T>& list) {
|
||||
@@ -277,6 +303,14 @@ struct Converter<QSize> : DefaultConverter<QSize> {
|
||||
size.setHeight(obj.value("height").toInt());
|
||||
return size;
|
||||
}
|
||||
static QSize clamp(const QSize& value, const QSize& min, const QSize& max, const QStringList* = nullptr) {
|
||||
Q_ASSERT(min.width() <= max.width());
|
||||
Q_ASSERT(min.height() <= max.height());
|
||||
QSize size = value;
|
||||
if (value.width() < min.width() || value.height() < min.height()) size = value.expandedTo(min);
|
||||
if (value.width() > max.width() || value.height() > max.height()) size = value.boundedTo(max);
|
||||
return size;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
@@ -298,6 +332,18 @@ struct Converter<QMargins> : DefaultConverter<QMargins> {
|
||||
margins.setRight(obj.value("right").toInt());
|
||||
return margins;
|
||||
}
|
||||
static QMargins clamp(const QMargins& value, const QMargins& min, const QMargins& max, const QStringList* = nullptr) {
|
||||
Q_ASSERT(min.left() <= max.left());
|
||||
Q_ASSERT(min.right() <= max.right());
|
||||
Q_ASSERT(min.top() <= max.top());
|
||||
Q_ASSERT(min.bottom() <= max.bottom());
|
||||
QMargins margins = value;
|
||||
margins.setLeft( std::clamp(value.left(), min.left(), max.left()));
|
||||
margins.setRight( std::clamp(value.right(), min.right(), max.right()));
|
||||
margins.setTop( std::clamp(value.top(), min.top(), max.top()));
|
||||
margins.setBottom(std::clamp(value.bottom(), min.bottom(), max.bottom()));
|
||||
return margins;
|
||||
}
|
||||
};
|
||||
|
||||
template <>
|
||||
|
||||
171
include/core/fieldmanager.h
Normal file
171
include/core/fieldmanager.h
Normal file
@@ -0,0 +1,171 @@
|
||||
#include "converter.h"
|
||||
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// A FieldInterface provides a simple interface for converting a field to/from JSON.
|
||||
// It's constructed with a pointer to some data, and has two functions:
|
||||
// - 'get' returns the pointed-to data, converted to JSON
|
||||
// - 'set' assigns the pointed-to data to a given QJsonValue, with appropriate conversion.
|
||||
// Returns any errors that occur during conversion/assignment.
|
||||
//
|
||||
// A FieldInterface is normally constructed using the 'makeFieldInterface' function:
|
||||
// Example:
|
||||
// int someField = 0;
|
||||
// FieldInterface* fi = makeFieldInterface(&someField);
|
||||
// fi->set(QJsonValue("5")); // someField is now 5
|
||||
//
|
||||
// There are additional implementations of 'makeFieldInterface' that let you specify what valid values are.
|
||||
// Example:
|
||||
// int someField = 0;
|
||||
// int min = 1, max = 4;
|
||||
// FieldInterface* fi = makeFieldInterface(&someField, min, max);
|
||||
// fi->set(QJsonValue("5")); // someField is now 4 (defaults to closest bound), error messages returned
|
||||
// or
|
||||
// QString someField = "";
|
||||
// QList<QString> options = {"hello","hi there"};
|
||||
// FieldInterface* fi = makeFieldInterface(&someField, options);
|
||||
// fi->set(QJsonValue("5")); // someField is now "hello" (defaults to first element), error messages returned
|
||||
|
||||
|
||||
// Base class lets us use the interface without any type information.
|
||||
class FieldInterface {
|
||||
public:
|
||||
FieldInterface(){};
|
||||
virtual ~FieldInterface() {};
|
||||
virtual QJsonValue get() const = 0;
|
||||
virtual QStringList set(const QJsonValue& json) const = 0;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
class BasicFieldInterface : public FieldInterface {
|
||||
public:
|
||||
BasicFieldInterface(T* field) : m_field(field) {
|
||||
Q_ASSERT(m_field);
|
||||
};
|
||||
virtual ~BasicFieldInterface() {};
|
||||
virtual QJsonValue get() const override {return Converter<T>::toJson(*m_field);}
|
||||
virtual QStringList set(const QJsonValue& json) const override {
|
||||
QStringList errors;
|
||||
auto value = Converter<T>::fromJson(json, &errors);
|
||||
if (errors.isEmpty()) *m_field = value; // Don't bother changing the value if conversion failed
|
||||
return errors;
|
||||
}
|
||||
protected:
|
||||
T* m_field;
|
||||
};
|
||||
|
||||
template <typename T>
|
||||
static FieldInterface* makeFieldInterface(T* field) {
|
||||
return new BasicFieldInterface<T>(field);
|
||||
}
|
||||
|
||||
// Create a regular FieldInterface, but override 'set' to use the given min/max.
|
||||
template <typename T>
|
||||
static FieldInterface* makeFieldInterface(T* field, const T& min, const T& max) {
|
||||
class BoundedFieldInterface : public BasicFieldInterface<T> {
|
||||
public:
|
||||
BoundedFieldInterface(T* field, const T& min, const T& max)
|
||||
: BasicFieldInterface<T>(field), m_min(min), m_max(max) {};
|
||||
virtual ~BoundedFieldInterface() {};
|
||||
virtual QStringList set(const QJsonValue& json) const override {
|
||||
QStringList errors;
|
||||
auto value = Converter<T>::fromJson(json, &errors);
|
||||
if (errors.isEmpty()) { // Don't bother changing the value if conversion failed
|
||||
value = Converter<T>::clamp(value, m_min, m_max, &errors);
|
||||
*this->m_field = value;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
private:
|
||||
const T m_min;
|
||||
const T m_max;
|
||||
};
|
||||
return new BoundedFieldInterface(field, min, max);
|
||||
}
|
||||
|
||||
// Create a regular FieldInterface, but override 'set' to use the given 'acceptableValues'.
|
||||
template <typename T>
|
||||
static FieldInterface* makeFieldInterface(T* field, const QList<T>& acceptableValues) {
|
||||
Q_ASSERT(!acceptableValues.isEmpty());
|
||||
class BoundedFieldInterface : public BasicFieldInterface<T> {
|
||||
public:
|
||||
BoundedFieldInterface(T* field, const QList<T>& acceptableValues)
|
||||
: BasicFieldInterface<T>(field),
|
||||
m_acceptableValues(acceptableValues.begin(), acceptableValues.end()),
|
||||
m_defaultValue(acceptableValues.first()) {};
|
||||
virtual ~BoundedFieldInterface() {};
|
||||
virtual QStringList set(const QJsonValue& json) const override {
|
||||
QStringList errors;
|
||||
auto value = Converter<T>::fromJson(json, &errors);
|
||||
if (errors.isEmpty()) {
|
||||
if (!m_acceptableValues.contains(value)) {
|
||||
value = m_defaultValue;
|
||||
errors.append("Invalid value.");
|
||||
}
|
||||
*this->m_field = value;
|
||||
}
|
||||
return errors;
|
||||
}
|
||||
private:
|
||||
// The order of the list only matters for determining the default value,
|
||||
// so save that separately and convert the list to a set for better lookup speed.
|
||||
const QSet<T> m_acceptableValues;
|
||||
const T m_defaultValue;
|
||||
};
|
||||
return new BoundedFieldInterface(field, acceptableValues);
|
||||
}
|
||||
|
||||
/////////////////////////////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// FieldManager manages a QHash mapping string keys to FieldInterfaces.
|
||||
// This makes it easy to map many fields to/from JSON without explicitly serializing anything.
|
||||
|
||||
class FieldManager {
|
||||
public:
|
||||
~FieldManager(){ clear(); }
|
||||
|
||||
QStringList setField(const QString& key, const QJsonValue& value) const {
|
||||
auto it = m_fields.find(key);
|
||||
return (it != m_fields.end()) ? it.value()->set(value) : QStringList();
|
||||
}
|
||||
|
||||
QJsonValue getField(const QString& key) const {
|
||||
auto it = m_fields.find(key);
|
||||
return (it != m_fields.end()) ? it.value()->get() : QJsonValue();
|
||||
}
|
||||
|
||||
QJsonObject getFields() const {
|
||||
QJsonObject obj;
|
||||
for (auto it = m_fields.constBegin(); it != m_fields.constEnd(); it++) {
|
||||
obj[it.key()] = it.value()->get();
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
qDeleteAll(m_fields);
|
||||
m_fields.clear();
|
||||
}
|
||||
|
||||
bool hasField(const QString& key) const {return m_fields.contains(key);}
|
||||
|
||||
template <typename T>
|
||||
void addField(T* field, const QString& key) {
|
||||
Q_ASSERT(!m_fields.contains(key));
|
||||
m_fields.insert(key, makeFieldInterface<T>(field));
|
||||
}
|
||||
template <typename T>
|
||||
void addField(T* field, const QString& key, const T& min, const T& max) {
|
||||
Q_ASSERT(!m_fields.contains(key));
|
||||
m_fields.insert(key, makeFieldInterface<T>(field, min, max));
|
||||
}
|
||||
template <typename T>
|
||||
void addField(T* field, const QString& key, const QList<T>& acceptableValues) {
|
||||
Q_ASSERT(!m_fields.contains(key));
|
||||
m_fields.insert(key, makeFieldInterface<T>(field, acceptableValues));
|
||||
}
|
||||
|
||||
private:
|
||||
QHash<QString, FieldInterface*> m_fields;
|
||||
};
|
||||
@@ -3,6 +3,7 @@
|
||||
#define ORDERED_SET_H
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <set>
|
||||
|
||||
template <typename T>
|
||||
class OrderedSet : public std::set<T>
|
||||
|
||||
@@ -18,7 +18,6 @@ namespace Util {
|
||||
void setErrorStylesheet(QLineEdit *lineEdit, bool isError);
|
||||
QString toStylesheetString(const QFont &font);
|
||||
void show(QWidget *w);
|
||||
QColorSpace toColorSpace(int colorSpaceInt);
|
||||
QString mkpath(const QString& dirPath);
|
||||
QString getFileHash(const QString &filepath);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user