Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0f82a6b
Add property list editor
D8H Dec 23, 2025
43640f5
Scroll at selection
D8H Dec 23, 2025
733f958
Follow selection both ways
D8H Dec 24, 2025
9ccd4a9
Move the context menu in the tree
D8H Dec 24, 2025
53be9a1
Merge the properties editor for behavior and object
D8H Dec 24, 2025
9874093
Rename files
D8H Dec 24, 2025
193a603
Move files
D8H Dec 25, 2025
a5008a7
Fix property renaming
D8H Dec 25, 2025
c977b04
Fix stories
D8H Dec 25, 2025
63d2f55
Handle shared properties
D8H Dec 25, 2025
153ab36
Fix shared property creation not updating
D8H Dec 27, 2025
00ecdaf
Make it works on mobile
D8H Dec 27, 2025
1895ed5
Keep the "Add property" button on mobile
D8H Dec 28, 2025
5cd4319
Fix copy paste
D8H Dec 29, 2025
1ad8cec
Factorize paste function
D8H Dec 29, 2025
9e24bf4
Remove unused code.
D8H Dec 29, 2025
e104dab
Add property folders in Core
D8H Dec 31, 2025
69df477
Update the tree when inserting and removing properties
D8H Jan 2, 2026
163e99b
Handle property folders in the tree
D8H Jan 1, 2026
5b7842c
Use the same order as the tree in the editor
D8H Jan 3, 2026
ca260ba
Fix updating the groups
D8H Jan 3, 2026
c98583a
Fix copy paste
D8H Jan 3, 2026
26210d4
Expand all property folders by default
D8H Jan 4, 2026
5fea626
Ensure group names are in sync with the tree when unserializing
D8H Jan 4, 2026
6fa170b
Fix property removing and pasted property group
D8H Jan 4, 2026
1ed94ff
Add an icon for resource properties
D8H Jan 4, 2026
cdc616f
Typo
D8H Jan 4, 2026
6869586
Remove the field for property group that is now useless
D8H Jan 4, 2026
9314e9e
Remove commented code.
D8H Jan 9, 2026
0239fb4
Fix attribute name
D8H Jan 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions Core/GDCore/Project/AbstractEventsBasedEntity.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ void AbstractEventsBasedEntity::SerializeTo(SerializerElement& element) const {
eventsFunctionsContainer.SerializeEventsFunctionsTo(eventsFunctionsElement);
propertyDescriptors.SerializeElementsTo(
"propertyDescriptor", element.AddChild("propertyDescriptors"));
propertyDescriptors.SerializeFoldersTo(
element.AddChild("propertiesFolderStructure"));
}

void AbstractEventsBasedEntity::UnserializeFrom(
Expand All @@ -47,6 +49,13 @@ void AbstractEventsBasedEntity::UnserializeFrom(
project, eventsFunctionsElement);
propertyDescriptors.UnserializeElementsFrom(
"propertyDescriptor", element.GetChild("propertyDescriptors"));
if (element.HasChild("propertiesFolderStructure")) {
propertyDescriptors.UnserializeFoldersFrom(
project, element.GetChild("propertiesFolderStructure", 0));
}
// Compatibility with GD <= 5.6.251
propertyDescriptors.AddMissingPropertiesInRootFolder();
// end of compatibility code
}

} // namespace gd
15 changes: 13 additions & 2 deletions Core/GDCore/Project/EventsBasedBehavior.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,12 @@ EventsBasedBehavior::EventsBasedBehavior()
void EventsBasedBehavior::SerializeTo(SerializerElement& element) const {
AbstractEventsBasedEntity::SerializeTo(element);
element.SetAttribute("objectType", objectType);
sharedPropertyDescriptors.SerializeElementsTo(
"propertyDescriptor", element.AddChild("sharedPropertyDescriptors"));
if (!sharedPropertyDescriptors.empty()) {
sharedPropertyDescriptors.SerializeElementsTo(
"propertyDescriptor", element.AddChild("sharedPropertyDescriptors"));
sharedPropertyDescriptors.SerializeFoldersTo(
element.AddChild("sharedPropertiesFolderStructure"));
}
if (quickCustomizationVisibility != QuickCustomization::Visibility::Default) {
element.SetStringAttribute(
"quickCustomizationVisibility",
Expand All @@ -38,6 +42,13 @@ void EventsBasedBehavior::UnserializeFrom(gd::Project& project,
objectType = element.GetStringAttribute("objectType");
sharedPropertyDescriptors.UnserializeElementsFrom(
"propertyDescriptor", element.GetChild("sharedPropertyDescriptors"));
if (element.HasChild("sharedPropertiesFolderStructure")) {
sharedPropertyDescriptors.UnserializeFoldersFrom(
project, element.GetChild("sharedPropertiesFolderStructure", 0));
}
// Compatibility with GD <= 5.6.251
sharedPropertyDescriptors.AddMissingPropertiesInRootFolder();
// end of compatibility code
if (element.HasChild("quickCustomizationVisibility")) {
quickCustomizationVisibility =
element.GetStringAttribute("quickCustomizationVisibility") == "visible"
Expand Down
6 changes: 2 additions & 4 deletions Core/GDCore/Project/ObjectFolderOrObject.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
* Copyright 2008-2023 Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
#ifndef GDCORE_OBJECTFOLDEROROBJECT_H
#define GDCORE_OBJECTFOLDEROROBJECT_H
#pragma once

#include <memory>
#include <vector>

Expand Down Expand Up @@ -210,5 +210,3 @@ class GD_CORE_API ObjectFolderOrObject {
};

} // namespace gd

#endif // GDCORE_OBJECTFOLDEROROBJECT_H
164 changes: 164 additions & 0 deletions Core/GDCore/Project/PropertiesContainer.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
/*
* GDevelop Core
* Copyright 2008-2025 Florian Rival ([email protected]). All rights
* reserved. This project is released under the MIT License.
*/
#include "GDCore/Project/PropertiesContainer.h"
#include "GDCore/Project/NamedPropertyDescriptor.h"

namespace gd {

PropertiesContainer::PropertiesContainer(
EventsFunctionsContainer::FunctionOwner owner)
: properties(), owner(owner) {
rootFolder = gd::make_unique<gd::PropertyFolderOrProperty>("__ROOT");
}

PropertiesContainer::PropertiesContainer(const PropertiesContainer &other)
: properties(other.properties), owner(other.owner) {
// The properties folders are not copied.
// It's not an issue because the UI uses the serialization for duplication.
rootFolder = gd::make_unique<gd::PropertyFolderOrProperty>("__ROOT");
}

PropertiesContainer &
PropertiesContainer::operator=(const PropertiesContainer &other) {
if (this != &other) {
properties = other.properties;
owner = other.owner;
// The properties folders are not copied.
// It's not an issue because the UI uses the serialization for duplication.
rootFolder = gd::make_unique<gd::PropertyFolderOrProperty>("__ROOT");
}
return *this;
}

NamedPropertyDescriptor &
PropertiesContainer::Insert(const NamedPropertyDescriptor &property,
size_t position) {
auto &newProperty = properties.Insert(property, position);
rootFolder->InsertProperty(&newProperty);
return newProperty;
}

NamedPropertyDescriptor &PropertiesContainer::InsertNew(const gd::String &name,
size_t position) {

auto &newlyCreatedProperty = properties.InsertNew(name, position);
rootFolder->InsertProperty(&newlyCreatedProperty);
return newlyCreatedProperty;
}

bool PropertiesContainer::Has(const gd::String &name) const {
return properties.Has(name);
}

NamedPropertyDescriptor &PropertiesContainer::Get(const gd::String &name) {
return properties.Get(name);
}

const NamedPropertyDescriptor &
PropertiesContainer::Get(const gd::String &name) const {
return properties.Get(name);
}

NamedPropertyDescriptor &PropertiesContainer::Get(size_t index) {
return properties.Get(index);
}

const NamedPropertyDescriptor &PropertiesContainer::Get(size_t index) const {
return properties.Get(index);
}

void PropertiesContainer::Remove(const gd::String &name) {
rootFolder->RemoveRecursivelyPropertyNamed(name);
properties.Remove(name);
}

void PropertiesContainer::Move(std::size_t oldIndex, std::size_t newIndex) {
properties.Move(oldIndex, newIndex);
}

bool PropertiesContainer::IsEmpty() const { return properties.IsEmpty(); };

size_t PropertiesContainer::GetCount() const { return properties.GetCount(); }

std::size_t
PropertiesContainer::GetPosition(const NamedPropertyDescriptor &element) const {
return properties.GetPosition(element);
}

const std::vector<std::unique_ptr<NamedPropertyDescriptor>> &
PropertiesContainer::GetInternalVector() const {
return properties.GetInternalVector();
};

std::vector<std::unique_ptr<NamedPropertyDescriptor>> &
PropertiesContainer::GetInternalVector() {
return properties.GetInternalVector();
};

gd::NamedPropertyDescriptor &PropertiesContainer::InsertNewPropertyInFolder(
const gd::String &name,
gd::PropertyFolderOrProperty &propertyFolderOrProperty,
std::size_t position) {
gd::NamedPropertyDescriptor &newlyCreatedProperty =
properties.InsertNew(name, properties.GetCount());
propertyFolderOrProperty.InsertProperty(&newlyCreatedProperty, position);
return newlyCreatedProperty;
}

std::vector<const PropertyFolderOrProperty *>
PropertiesContainer::GetAllPropertyFolderOrProperty() const {
std::vector<const PropertyFolderOrProperty *> results;

std::function<void(const PropertyFolderOrProperty &folder)>
addChildrenOfFolder = [&](const PropertyFolderOrProperty &folder) {
for (size_t i = 0; i < folder.GetChildrenCount(); ++i) {
const auto &child = folder.GetChildAt(i);
results.push_back(&child);

if (child.IsFolder()) {
addChildrenOfFolder(child);
}
}
};

addChildrenOfFolder(*rootFolder);

return results;
}

void PropertiesContainer::AddMissingPropertiesInRootFolder() {
for (std::size_t i = 0; i < properties.GetCount(); ++i) {
auto &property = properties.Get(i);
if (!rootFolder->HasPropertyNamed(property.GetName())) {
const gd::String &group = property.GetGroup();
auto &folder = !group.empty() ? rootFolder->GetOrCreateChildFolder(group)
: *rootFolder;
folder.InsertProperty(&property);
}
}
}

void PropertiesContainer::SerializeElementsTo(
const gd::String &elementName, SerializerElement &element) const {
properties.SerializeElementsTo(elementName, element);
}

void PropertiesContainer::UnserializeElementsFrom(
const gd::String &elementName, const SerializerElement &element) {
properties.UnserializeElementsFrom(elementName, element);
}

void PropertiesContainer::SerializeFoldersTo(SerializerElement &element) const {
rootFolder->SerializeTo(element);
}

void PropertiesContainer::UnserializeFoldersFrom(
gd::Project &project, const SerializerElement &element) {
rootFolder->UnserializeFrom(project, element, *this);
rootFolder->UpdateGroupNameOfAllProperties();
}

} // namespace gd
95 changes: 73 additions & 22 deletions Core/GDCore/Project/PropertiesContainer.h
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#pragma once
#include "EventsFunctionsContainer.h"
#include "GDCore/Project/PropertyFolderOrProperty.h"
#include "GDCore/Tools/SerializableWithNameList.h"
#include "NamedPropertyDescriptor.h"

Expand All @@ -13,38 +14,88 @@ namespace gd {
*
* \ingroup PlatformDefinition
*/
class PropertiesContainer
: public SerializableWithNameList<NamedPropertyDescriptor> {
public:
PropertiesContainer(EventsFunctionsContainer::FunctionOwner owner)
: SerializableWithNameList<NamedPropertyDescriptor>(), owner(owner) {}

PropertiesContainer(const PropertiesContainer& other)
: SerializableWithNameList<NamedPropertyDescriptor>(other),
owner(other.owner) {}

PropertiesContainer& operator=(const PropertiesContainer& other) {
if (this != &other) {
SerializableWithNameList<NamedPropertyDescriptor>::operator=(other);
owner = other.owner;
}
return *this;
}
class PropertiesContainer {
public:
PropertiesContainer(EventsFunctionsContainer::FunctionOwner owner);

PropertiesContainer(const PropertiesContainer &other);

PropertiesContainer &operator=(const PropertiesContainer &other);

NamedPropertyDescriptor &Insert(const NamedPropertyDescriptor &element,
size_t position = (size_t)-1);
NamedPropertyDescriptor &InsertNew(const gd::String &name,
size_t position = (size_t)-1);
bool Has(const gd::String &name) const;
NamedPropertyDescriptor &Get(const gd::String &name);
const NamedPropertyDescriptor &Get(const gd::String &name) const;
NamedPropertyDescriptor &Get(size_t index);
const NamedPropertyDescriptor &Get(size_t index) const;
void Remove(const gd::String &name);
void Move(std::size_t oldIndex, std::size_t newIndex);
size_t GetCount() const;
std::size_t GetPosition(const NamedPropertyDescriptor &element) const;
bool IsEmpty() const;
size_t size() const { return GetCount(); }
NamedPropertyDescriptor &at(size_t index) { return Get(index); };
bool empty() const { return IsEmpty(); }
const std::vector<std::unique_ptr<NamedPropertyDescriptor>>& GetInternalVector() const;
std::vector<std::unique_ptr<NamedPropertyDescriptor>>& GetInternalVector();
void SerializeElementsTo(const gd::String& elementName,
SerializerElement& element) const;
void UnserializeElementsFrom(const gd::String& elementName,
const SerializerElement& element);

void ForEachPropertyMatchingSearch(
const gd::String& search,
std::function<void(const gd::NamedPropertyDescriptor& property)> fn)
const gd::String &search,
std::function<void(const gd::NamedPropertyDescriptor &property)> fn)
const {
for (const auto& property : elements) {
for (const auto &property : properties.GetInternalVector()) {
if (property->GetName().FindCaseInsensitive(search) != gd::String::npos)
fn(*property);
}
}

EventsFunctionsContainer::FunctionOwner GetOwner() const { return owner; }

private:
/**
* \brief Add a new empty property called \a name in the
* given folder at the specified position.<br>
*
* \return A reference to the property in the list.
*/
gd::NamedPropertyDescriptor &InsertNewPropertyInFolder(
const gd::String &name,
gd::PropertyFolderOrProperty &propertyFolderOrProperty,
std::size_t position);

/**
* Returns a vector containing all object and folders in this container.
* Only use this for checking if you hold a valid `PropertyFolderOrProperty` -
* don't use this for rendering or anything else.
*/
std::vector<const PropertyFolderOrProperty *>
GetAllPropertyFolderOrProperty() const;

gd::PropertyFolderOrProperty &GetRootFolder() { return *rootFolder; }

void AddMissingPropertiesInRootFolder();

/**
* \brief Serialize folder structure.
*/
void SerializeFoldersTo(SerializerElement &element) const;

/**
* \brief Unserialize folder structure.
*/
void UnserializeFoldersFrom(gd::Project &project,
const SerializerElement &element);

private:
EventsFunctionsContainer::FunctionOwner owner;
SerializableWithNameList<NamedPropertyDescriptor> properties;
std::unique_ptr<gd::PropertyFolderOrProperty> rootFolder;
};

} // namespace gd
} // namespace gd
13 changes: 10 additions & 3 deletions Core/GDCore/Project/PropertyDescriptor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ void PropertyDescriptor::SerializeTo(SerializerElement& element) const {
element.AddChild("label").SetStringValue(label);
if (!description.empty())
element.AddChild("description").SetStringValue(description);
if (!group.empty()) element.AddChild("group").SetStringValue(group);

// Compatibility with GD <= 5.6.251
// The group is now persisted in the `propertiesFolderStructure` node.
// TODO Stop persisting the group name after a few releases when users are
// unlikely to go back to 5.6.251 to avoid redundancy.
if (!group.empty())
element.AddChild("group").SetStringValue(group);
// end of compatibility code
if (!extraInformation.empty()) {
SerializerElement& extraInformationElement =
element.AddChild("extraInformation");
Expand Down Expand Up @@ -83,9 +88,11 @@ void PropertyDescriptor::UnserializeFrom(const SerializerElement& element) {
description = element.HasChild("description")
? element.GetChild("description").GetStringValue()
: "";
// Compatibility with GD <= 5.6.251
// The group is now persisted in the `propertiesFolderStructure` node.
group = element.HasChild("group") ? element.GetChild("group").GetStringValue()
: "";

// end of compatibility code
extraInformation.clear();
if (element.HasChild("extraInformation")) {
const SerializerElement& extraInformationElement =
Expand Down
Loading