Skip to content

Commit

Permalink
Initial work on supporting list property types
Browse files Browse the repository at this point in the history
* Added 'list' as option to the Add Property dialog, for adding a
  QVariantList.

* Adjusted the CustomPropertiesHelper to create sub-properties that
  display the values in the list (maybe should rather be done by the
  VariantPropertyManager).

* Added support for saving and loading list properties to TMX and JSON
  formats.
  • Loading branch information
bjorn committed Jul 10, 2024
1 parent ee52b20 commit 8ea4d49
Show file tree
Hide file tree
Showing 9 changed files with 178 additions and 30 deletions.
17 changes: 16 additions & 1 deletion src/libtiled/mapreader.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ class MapReaderPrivate

Properties readProperties();
void readProperty(Properties *properties, const ExportContext &context);
QVariant readPropertyValue(const ExportContext &context);

MapReader *p;

Expand Down Expand Up @@ -1448,13 +1449,22 @@ void MapReaderPrivate::readProperty(Properties *properties, const ExportContext
const QXmlStreamAttributes atts = xml.attributes();
QString propertyName = atts.value(QLatin1String("name")).toString();

properties->insert(propertyName, readPropertyValue(context));
}

QVariant MapReaderPrivate::readPropertyValue(const ExportContext &context)
{
const QXmlStreamAttributes atts = xml.attributes();

ExportValue exportValue;
exportValue.typeName = atts.value(QLatin1String("type")).toString();
exportValue.propertyTypeName = atts.value(QLatin1String("propertytype")).toString();

const QString propertyValue = atts.value(QLatin1String("value")).toString();
exportValue.value = propertyValue;

QVariantList values;

while (xml.readNext() != QXmlStreamReader::Invalid) {
if (xml.isEndElement()) {
break;
Expand All @@ -1464,12 +1474,17 @@ void MapReaderPrivate::readProperty(Properties *properties, const ExportContext
} else if (xml.isStartElement()) {
if (xml.name() == QLatin1String("properties"))
exportValue.value = readProperties();
else if (xml.name() == QLatin1String("value"))
values.append(readPropertyValue(context));
else
readUnknownElement();
}
}

properties->insert(propertyName, context.toPropertyValue(exportValue));
if (exportValue.typeName == QLatin1String("list"))
exportValue.value = values;

return context.toPropertyValue(exportValue);
}


Expand Down
22 changes: 20 additions & 2 deletions src/libtiled/maptovariantconverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -819,12 +819,30 @@ void MapToVariantConverter::addProperties(QVariantMap &variantMap,

QVariantMap propertyVariantMap;
propertyVariantMap[QStringLiteral("name")] = it.key();
propertyVariantMap[QStringLiteral("value")] = exportValue.value;
propertyVariantMap[QStringLiteral("type")] = exportValue.typeName;

if (!exportValue.propertyTypeName.isEmpty())
propertyVariantMap[QStringLiteral("propertytype")] = exportValue.propertyTypeName;

if (exportValue.typeName == QLatin1String("list")) {
const QVariantList values = it.value().toList();
QVariantList valuesVariantList;

// todo: this doesn't support lists of lists
for (const QVariant &value : values) {
QVariantMap valueVariantMap;
const auto exportValue = context.toExportValue(value);
valueVariantMap[QStringLiteral("value")] = exportValue.value;
valueVariantMap[QStringLiteral("type")] = exportValue.typeName;
if (!exportValue.propertyTypeName.isEmpty())
valueVariantMap[QStringLiteral("propertytype")] = exportValue.propertyTypeName;
valuesVariantList << valueVariantMap;
}

propertyVariantMap[QStringLiteral("value")] = valuesVariantList;
} else {
propertyVariantMap[QStringLiteral("value")] = exportValue.value;
}

propertiesVariantList << propertyVariantMap;
}

Expand Down
30 changes: 23 additions & 7 deletions src/libtiled/mapwriter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -905,24 +905,40 @@ void MapWriterPrivate::writeProperties(QXmlStreamWriter &w,
if (!exportValue.propertyTypeName.isEmpty())
w.writeAttribute(QStringLiteral("propertytype"), exportValue.propertyTypeName);

// For class property values, write out the original value, so that the
// propertytype attribute can also be written for their members where
// applicable.
if (exportValue.value.userType() == QMetaType::QVariantMap) {
switch (exportValue.value.userType()) {
case QMetaType::QVariantList: {
const auto values = exportValue.value.toList();
for (const QVariant &v : values) {
const auto exportValue = context.toExportValue(v);
w.writeStartElement(QStringLiteral("value"));
if (exportValue.typeName != QLatin1String("string"))
w.writeAttribute(QStringLiteral("type"), exportValue.typeName);
if (!exportValue.propertyTypeName.isEmpty())
w.writeAttribute(QStringLiteral("propertytype"), exportValue.propertyTypeName);
w.writeCharacters(exportValue.value.toString());
w.writeEndElement();
}
break;
}
case QMetaType::QVariantMap:
// Write out the original value, so that the propertytype attribute
// can also be written for their members where applicable.
writeProperties(w, it.value().value<PropertyValue>().value.toMap());
} else {
break;
default:
const QString value = exportValue.value.toString();

if (value.contains(QLatin1Char('\n')))
w.writeCharacters(value);
else
w.writeAttribute(QStringLiteral("value"), value);
break;
}

w.writeEndElement();
w.writeEndElement(); // </property>
}

w.writeEndElement();
w.writeEndElement(); // </properties>
}

void MapWriterPrivate::writeImage(QXmlStreamWriter &w,
Expand Down
21 changes: 6 additions & 15 deletions src/libtiled/properties.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,6 @@ void aggregateProperties(AggregatedProperties &aggregated, const Properties &pro
}
}

int propertyValueId()
{
return qMetaTypeId<PropertyValue>();
}

int filePathTypeId()
{
return qMetaTypeId<FilePath>();
}

int objectRefTypeId()
{
return qMetaTypeId<ObjectRef>();
}

QString typeToName(int type)
{
// We can't handle the PropertyValue purely by its type ID, since we need to
Expand All @@ -226,6 +211,8 @@ QString typeToName(int type)
return QStringLiteral("color");
case QMetaType::QVariantMap:
return QStringLiteral("class");
case QMetaType::QVariantList:
return QStringLiteral("list");

default:
if (type == filePathTypeId())
Expand Down Expand Up @@ -309,6 +296,7 @@ ExportValue ExportContext::toExportValue(const QVariant &value) const
} else if (metaType == objectRefTypeId()) {
exportValue.value = ObjectRef::toInt(value.value<ObjectRef>());
} else {
// Other values, including lists, do not need special handling here
exportValue.value = value;
}

Expand Down Expand Up @@ -343,6 +331,9 @@ QVariant ExportContext::toPropertyValue(const QVariant &value, int metaType) con
if (metaType == QMetaType::QVariantMap || metaType == propertyValueId())
return value; // should be covered by property type

if (metaType == QMetaType::QVariantList)
return value; // list elements should be converted individually

if (metaType == filePathTypeId()) {
const QUrl url = toUrl(value.toString(), mPath);
return QVariant::fromValue(FilePath { url });
Expand Down
6 changes: 3 additions & 3 deletions src/libtiled/properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -185,9 +185,9 @@ TILEDSHARED_EXPORT void mergeProperties(Properties &target, const Properties &so
TILEDSHARED_EXPORT QJsonArray propertiesToJson(const Properties &properties, const ExportContext &context = ExportContext());
TILEDSHARED_EXPORT Properties propertiesFromJson(const QJsonArray &json, const ExportContext &context = ExportContext());

TILEDSHARED_EXPORT int propertyValueId();
TILEDSHARED_EXPORT int filePathTypeId();
TILEDSHARED_EXPORT int objectRefTypeId();
constexpr int propertyValueId() { return qMetaTypeId<PropertyValue>(); }
constexpr int filePathTypeId() { return qMetaTypeId<FilePath>(); }
constexpr int objectRefTypeId() { return qMetaTypeId<ObjectRef>(); }

TILEDSHARED_EXPORT QString typeToName(int type);
TILEDSHARED_EXPORT QString typeName(const QVariant &value);
Expand Down
21 changes: 20 additions & 1 deletion src/libtiled/varianttomapconverter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -190,7 +190,26 @@ Properties VariantToMapConverter::toProperties(const QVariant &propertiesVariant
exportValue.typeName = propertyVariantMap[QStringLiteral("type")].toString();
exportValue.propertyTypeName = propertyVariantMap[QStringLiteral("propertytype")].toString();

properties[propertyName] = context.toPropertyValue(exportValue);
auto &value = properties[propertyName];

if (exportValue.typeName == QLatin1String("list")) {
const QVariantList values = exportValue.value.toList();
QVariantList convertedList;
convertedList.reserve(values.size());
for (const QVariant &value : values) {
const QVariantMap valueVariantMap = value.toMap();
ExportValue itemValue;
itemValue.value = valueVariantMap[QStringLiteral("value")];
itemValue.typeName = valueVariantMap[QStringLiteral("type")].toString();
itemValue.propertyTypeName = valueVariantMap[QStringLiteral("propertytype")].toString();

// todo: this doesn't support lists of lists
convertedList.append(context.toPropertyValue(itemValue));
}
value = convertedList;
} else {
value = context.toPropertyValue(exportValue);
}
}

return properties;
Expand Down
1 change: 1 addition & 0 deletions src/tiled/addpropertydialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ void AddPropertyDialog::initialize(const Tiled::ClassPropertyType *parentClassTy
mUi->typeBox->addItem(plain, typeToName(QMetaType::Int), 0);
mUi->typeBox->addItem(plain, typeToName(objectRefTypeId()), QVariant::fromValue(ObjectRef()));
mUi->typeBox->addItem(plain, typeToName(QMetaType::QString), QString());
mUi->typeBox->addItem(plain, typeToName(QMetaType::QVariantList), QVariantList());

for (const auto propertyType : Object::propertyTypes()) {
// Avoid suggesting the creation of circular dependencies between types
Expand Down
87 changes: 86 additions & 1 deletion src/tiled/custompropertieshelper.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,13 @@

namespace Tiled {

/**
* Constructs a custom properties helper that manages properties in the given
* \a propertyBrowser.
*
* Uses its own VariantPropertyManager to create the properties and
* instantiates a VariantEditorFactory which creates the editor widgets.
*/
CustomPropertiesHelper::CustomPropertiesHelper(QtAbstractPropertyBrowser *propertyBrowser,
QObject *parent)
: QObject(parent)
Expand All @@ -56,6 +63,14 @@ CustomPropertiesHelper::~CustomPropertiesHelper()
mPropertyBrowser->unsetFactoryForManager(mPropertyManager);
}

/**
* Creates a top-level property with the given \a name and \a value.
*
* The property is not added to the property browser. It should be added either
* directly or to a suitable group property.
*
* The name of the property needs to be unique.
*/
QtVariantProperty *CustomPropertiesHelper::createProperty(const QString &name,
const QVariant &value)
{
Expand All @@ -71,6 +86,17 @@ QtVariantProperty *CustomPropertiesHelper::createProperty(const QString &name,
return property;
}

/**
* Implementation of property creation. Used by createProperty for creating
* top-level properties and by setPropertyAttributes for creating nested
* properties.
*
* The \a value is only used for determining the type of the property, it is
* not set on the property.
*
* This function works recursively, creating nested properties for class
* properties.
*/
QtVariantProperty *CustomPropertiesHelper::createPropertyInternal(const QString &name,
const QVariant &value)
{
Expand Down Expand Up @@ -99,6 +125,14 @@ QtVariantProperty *CustomPropertiesHelper::createPropertyInternal(const QString
}
}
}
} else if (type == QMetaType::QVariantList) {
// In case of list values, we need an expandable property with the list
// values as subproperties (though creation of such properties will
// only be done once the value is set)

// todo: lists probably need their own type here, such that a widget
// can be created that allows for adding elements
type = VariantPropertyManager::unstyledGroupTypeId();
}

if (type == objectRefTypeId())
Expand Down Expand Up @@ -127,6 +161,11 @@ QtVariantProperty *CustomPropertiesHelper::createPropertyInternal(const QString
return property;
}

/**
* Deletes the given top-level property.
*
* Should only be used for properties created with createProperty.
*/
void CustomPropertiesHelper::deleteProperty(QtProperty *property)
{
Q_ASSERT(hasProperty(property));
Expand All @@ -135,6 +174,13 @@ void CustomPropertiesHelper::deleteProperty(QtProperty *property)
deletePropertyInternal(property);
}

/**
* Implementation of property deletion. Used by deleteProperty for deleting
* top-level properties and by deleteSubProperties for deleting nested
* properties.
*
* This function works recursively, also deleting all nested properties.
*/
void CustomPropertiesHelper::deletePropertyInternal(QtProperty *property)
{
Q_ASSERT(mPropertyTypeIds.contains(property));
Expand All @@ -143,6 +189,12 @@ void CustomPropertiesHelper::deletePropertyInternal(QtProperty *property)
delete property;
}

/**
* Deletes all sub-properties of the given \a property.
*
* Used when a property is being deleted or before refreshing the nested
* properties that represent class members.
*/
void CustomPropertiesHelper::deleteSubProperties(QtProperty *property)
{
const auto subProperties = property->subProperties();
Expand All @@ -154,6 +206,9 @@ void CustomPropertiesHelper::deleteSubProperties(QtProperty *property)
}
}

/**
* Removes all properties.
*/
void CustomPropertiesHelper::clear()
{
QHashIterator<QtProperty *, int> it(mPropertyTypeIds);
Expand All @@ -171,7 +226,10 @@ QVariant CustomPropertiesHelper::toDisplayValue(QVariant value) const
value = value.value<PropertyValue>().value;

if (value.userType() == objectRefTypeId())
value = QVariant::fromValue(DisplayObjectRef { value.value<ObjectRef>(), mMapDocument });
value = QVariant::fromValue(DisplayObjectRef {
value.value<ObjectRef>(),
mMapDocument
});

return value;
}
Expand Down Expand Up @@ -226,6 +284,24 @@ void CustomPropertiesHelper::onValueChanged(QtProperty *property, const QVariant
static_cast<QtVariantProperty*>(subProperty)->setValue(toDisplayValue(value));
}
}

if (value.userType() == QMetaType::QVariantList) {
QScopedValueRollback<bool> updating(mUpdating, true);

// Delete any existing sub-properties
deleteSubProperties(property);

// Create a sub-property for each list value
const auto values = value.toList();
for (int i = 0; i < values.size(); ++i) {
const auto &value = values.at(i);

auto subProperty = createPropertyInternal(QString::number(i), value);
subProperty->setValue(toDisplayValue(value));
property->addSubProperty(subProperty);
mPropertyParents.insert(subProperty, property);
}
}
}

void CustomPropertiesHelper::resetProperty(QtProperty *property)
Expand Down Expand Up @@ -278,6 +354,15 @@ void CustomPropertiesHelper::propertyTypesChanged()
}
}

/**
* When the given \a propertyType is an EnumPropertyType, sets the appropriate
* attributes on the given \a property.
*
* Also creates sub-properties for members when the given \a propertyType is a
* ClassPropertyType.
*
* Called after property creation, as well as when the property types changed.
*/
void CustomPropertiesHelper::setPropertyAttributes(QtVariantProperty *property,
const PropertyType &propertyType)
{
Expand Down
Loading

0 comments on commit 8ea4d49

Please sign in to comment.