diff --git a/Core/GDCore/Project/FolderOrItem.h b/Core/GDCore/Project/FolderOrItem.h new file mode 100644 index 000000000000..405f76d12786 --- /dev/null +++ b/Core/GDCore/Project/FolderOrItem.h @@ -0,0 +1,582 @@ +/* + * GDevelop Core + * Copyright 2008-2025 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#ifndef GDCORE_FOLDERORITEM_H +#define GDCORE_FOLDERORITEM_H +#include +#include +#include + +#include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/String.h" +#include "GDCore/Tools/Log.h" +#include "GDCore/Tools/MakeUnique.h" + +namespace gd { +class Project; +class SerializerElement; +} // namespace gd + +namespace gd { + +/** + * \brief Generic template class representing a folder structure to organize + * items (objects, layouts, external events, etc.) in folders. + * + * \tparam T The type of item to organize (e.g., gd::Object, gd::Layout) + * + * \see gd::ObjectsContainer + */ +template +class GD_CORE_API FolderOrItem { + public: + /** + * \brief Default constructor creating an empty instance. Useful for the null + * object pattern. + */ + FolderOrItem(); + virtual ~FolderOrItem(); + + /** + * \brief Constructor for creating an instance representing a folder. + */ + FolderOrItem(gd::String folderName_, FolderOrItem* parent_ = nullptr); + + /** + * \brief Constructor for creating an instance representing an item. + */ + FolderOrItem(T* item_, FolderOrItem* parent_ = nullptr); + + /** + * \brief Returns the item behind the instance. + */ + T& GetItem() const { return *item; } + + /** + * \brief Returns true if the instance represents a folder. + */ + bool IsFolder() const { return !folderName.empty(); } + + /** + * \brief Returns the name of the folder. + */ + const gd::String& GetFolderName() const { return folderName; } + + /** + * \brief Set the folder name. Does nothing if called on an instance not + * representing a folder. + */ + void SetFolderName(const gd::String& name); + + /** + * \brief Returns true if the instance represents the item with the given + * name or if any of the children does (recursive search). + */ + template + bool HasItemNamed(const gd::String& name, GetNameFunc getName); + + /** + * \brief Returns the child instance holding the item with the given name + * (recursive search). + */ + template + FolderOrItem& GetItemNamed(const gd::String& name, GetNameFunc getName); + + /** + * \brief Returns the number of children. Returns 0 if the instance represents + * an item. + */ + std::size_t GetChildrenCount() const { + if (IsFolder()) return children.size(); + return 0; + } + + /** + * \brief Returns the child FolderOrItem at the given index. + */ + FolderOrItem& GetChildAt(std::size_t index); + + /** + * \brief Returns the child FolderOrItem at the given index. + */ + const FolderOrItem& GetChildAt(std::size_t index) const; + + /** + * \brief Returns the child FolderOrItem that represents the item + * with the given name. To use only if sure that the instance holds the item + * in its direct children (no recursive search). + */ + template + FolderOrItem& GetItemChild(const gd::String& name, GetNameFunc getName); + + /** + * \brief Returns the parent of the instance. If the instance has no parent + * (root folder), the null object is returned. + */ + FolderOrItem& GetParent() { + if (parent == nullptr) { + return GetBadFolderOrItem(); + } + return *parent; + } + + /** + * \brief Returns true if the instance is a root folder (that's to say it + * has no parent). + */ + bool IsRootFolder() { return !item && !parent; } + + /** + * \brief Moves a child from a position to a new one. + */ + void MoveChild(std::size_t oldIndex, std::size_t newIndex); + + /** + * \brief Removes the given child from the instance's children. If the given + * child contains children of its own, does nothing. + */ + void RemoveFolderChild(const FolderOrItem& childToRemove); + + /** + * \brief Removes the child representing the item with the given name from + * the instance children and recursively does it for every folder children. + */ + template + void RemoveRecursivelyItemNamed(const gd::String& name, GetNameFunc getName); + + /** + * \brief Clears all children + */ + void Clear(); + + /** + * \brief Inserts an instance representing the given item at the given + * position. + */ + void InsertItem(T* insertedItem, std::size_t position = (size_t)-1); + + /** + * \brief Inserts an instance representing a folder with the given name at the + * given position. + */ + FolderOrItem& InsertNewFolder(const gd::String& newFolderName, + std::size_t position); + + /** + * \brief Returns true if the instance is a descendant of the given instance + * of FolderOrItem. + */ + bool IsADescendantOf(const FolderOrItem& otherFolderOrItem); + + /** + * \brief Returns the position of the given instance of FolderOrItem + * in the instance's children. + */ + std::size_t GetChildPosition(const FolderOrItem& child) const; + + /** + * \brief Moves the given child FolderOrItem to the given folder at + * the given position. + */ + void MoveFolderOrItemToAnotherFolder(FolderOrItem& folderOrItem, + FolderOrItem& newParentFolder, + std::size_t newPosition); + + QuickCustomization::Visibility GetQuickCustomizationVisibility() const { + return quickCustomizationVisibility; + } + + void SetQuickCustomizationVisibility( + QuickCustomization::Visibility visibility) { + quickCustomizationVisibility = visibility; + } + + /** \name Saving and loading + * Members functions related to saving and loading the objects of the class. + */ + ///@{ + /** + * \brief Serialize the FolderOrItem instance. + */ + template + void SerializeTo(SerializerElement& element, GetNameFunc getName) const; + + /** + * \brief Unserialize the FolderOrItem instance. + */ + template + void UnserializeFrom(gd::Project& project, + const SerializerElement& element, + ItemContainerType& itemContainer, + GetItemFunc getItem); + ///@} + + /** + * \brief Compatibility wrapper: GetObject() -> GetItem() + */ + T& GetObject() const { return GetItem(); } + + /** + * \brief Compatibility wrapper: HasObjectNamed() -> HasItemNamed() + */ + bool HasObjectNamed(const gd::String& name) { + return HasItemNamed(name, [](const T& item) { return item.GetName(); }); + } + + /** + * \brief Compatibility wrapper: GetObjectNamed() -> GetItemNamed() + */ + FolderOrItem& GetObjectNamed(const gd::String& name) { + return GetItemNamed(name, [](const T& item) { return item.GetName(); }); + } + + /** + * \brief Compatibility wrapper: GetObjectChild() -> GetItemChild() + */ + FolderOrItem& GetObjectChild(const gd::String& name) { + return GetItemChild(name, [](const T& item) { return item.GetName(); }); + } + + /** + * \brief Compatibility wrapper: InsertObject() -> InsertItem() + */ + void InsertObject(T* insertedItem, std::size_t position = (size_t)-1) { + InsertItem(insertedItem, position); + } + + /** + * \brief Compatibility wrapper: RemoveRecursivelyObjectNamed() -> + * RemoveRecursivelyItemNamed() + */ + void RemoveRecursivelyObjectNamed(const gd::String& name) { + RemoveRecursivelyItemNamed(name, + [](const T& item) { return item.GetName(); }); + } + + /** + * \brief Compatibility wrapper: MoveObjectFolderOrObjectToAnotherFolder() -> + * MoveFolderOrItemToAnotherFolder() + */ + void MoveObjectFolderOrObjectToAnotherFolder(FolderOrItem& folderOrItem, + FolderOrItem& newParentFolder, + std::size_t newPosition) { + MoveFolderOrItemToAnotherFolder(folderOrItem, newParentFolder, newPosition); + } + + private: + static FolderOrItem& GetBadFolderOrItem(); + + FolderOrItem* parent = nullptr; + QuickCustomization::Visibility quickCustomizationVisibility; + + T* item; + + gd::String folderName; + std::vector>> children; +}; + +template +FolderOrItem& FolderOrItem::GetBadFolderOrItem() { + static FolderOrItem badFolderOrItem; + return badFolderOrItem; +} + +template +FolderOrItem::FolderOrItem() + : folderName("__NULL"), + item(nullptr), + quickCustomizationVisibility(QuickCustomization::Visibility::Default) {} + +template +FolderOrItem::FolderOrItem(gd::String folderName_, FolderOrItem* parent_) + : folderName(folderName_), + parent(parent_), + item(nullptr), + quickCustomizationVisibility(QuickCustomization::Visibility::Default) {} + +template +FolderOrItem::FolderOrItem(T* item_, FolderOrItem* parent_) + : item(item_), + parent(parent_), + quickCustomizationVisibility(QuickCustomization::Visibility::Default) {} + +template +FolderOrItem::~FolderOrItem() {} + +template +template +bool FolderOrItem::HasItemNamed(const gd::String& name, + GetNameFunc getName) { + if (IsFolder()) { + return std::any_of( + children.begin(), + children.end(), + [&name, &getName](std::unique_ptr>& folderOrItem) { + return folderOrItem->HasItemNamed(name, getName); + }); + } + if (!item) return false; + return getName(*item) == name; +} + +template +template +FolderOrItem& FolderOrItem::GetItemNamed(const gd::String& name, + GetNameFunc getName) { + if (item && getName(*item) == name) { + return *this; + } + if (IsFolder()) { + for (std::size_t j = 0; j < children.size(); j++) { + FolderOrItem& foundInChild = children[j]->GetItemNamed(name, getName); + if (&foundInChild != &GetBadFolderOrItem()) { + return foundInChild; + } + } + } + return GetBadFolderOrItem(); +} + +template +void FolderOrItem::SetFolderName(const gd::String& name) { + if (!IsFolder()) return; + folderName = name; +} + +template +FolderOrItem& FolderOrItem::GetChildAt(std::size_t index) { + if (index >= children.size()) return GetBadFolderOrItem(); + return *children[index]; +} + +template +const FolderOrItem& FolderOrItem::GetChildAt(std::size_t index) const { + if (index >= children.size()) return GetBadFolderOrItem(); + return *children[index]; +} + +template +template +FolderOrItem& FolderOrItem::GetItemChild(const gd::String& name, + GetNameFunc getName) { + for (std::size_t j = 0; j < children.size(); j++) { + if (!children[j]->IsFolder()) { + if (getName(children[j]->GetItem()) == name) return *children[j]; + } + } + return GetBadFolderOrItem(); +} + +template +void FolderOrItem::InsertItem(T* insertedItem, std::size_t position) { + auto folderOrItem = gd::make_unique>(insertedItem, this); + if (position < children.size()) { + children.insert(children.begin() + position, std::move(folderOrItem)); + } else { + children.push_back(std::move(folderOrItem)); + } +} + +template +std::size_t FolderOrItem::GetChildPosition( + const FolderOrItem& child) const { + for (std::size_t j = 0; j < children.size(); j++) { + if (children[j].get() == &child) return j; + } + return gd::String::npos; +} + +template +FolderOrItem& FolderOrItem::InsertNewFolder( + const gd::String& newFolderName, std::size_t position) { + auto newFolderPtr = gd::make_unique>(newFolderName, this); + FolderOrItem& newFolder = *(*(children.insert( + position < children.size() ? children.begin() + position : children.end(), + std::move(newFolderPtr)))); + return newFolder; +} + +template +template +void FolderOrItem::RemoveRecursivelyItemNamed(const gd::String& name, + GetNameFunc getName) { + if (IsFolder()) { + children.erase( + std::remove_if( + children.begin(), + children.end(), + [&name, &getName](std::unique_ptr>& folderOrItem) { + return !folderOrItem->IsFolder() && + getName(folderOrItem->GetItem()) == name; + }), + children.end()); + for (auto& it : children) { + it->RemoveRecursivelyItemNamed(name, getName); + } + } +} + +template +void FolderOrItem::Clear() { + if (IsFolder()) { + for (auto& it : children) { + it->Clear(); + } + children.clear(); + } +} + +template +bool FolderOrItem::IsADescendantOf( + const FolderOrItem& otherFolderOrItem) { + if (parent == nullptr) return false; + if (&(*parent) == &otherFolderOrItem) return true; + return parent->IsADescendantOf(otherFolderOrItem); +} + +template +void FolderOrItem::MoveChild(std::size_t oldIndex, std::size_t newIndex) { + if (!IsFolder()) return; + if (oldIndex >= children.size() || newIndex >= children.size()) return; + + std::unique_ptr> folderOrItem = std::move(children[oldIndex]); + children.erase(children.begin() + oldIndex); + children.insert(children.begin() + newIndex, std::move(folderOrItem)); +} + +template +void FolderOrItem::RemoveFolderChild(const FolderOrItem& childToRemove) { + if (!IsFolder() || !childToRemove.IsFolder() || + childToRemove.GetChildrenCount() > 0) { + return; + } + auto it = + std::find_if(children.begin(), + children.end(), + [&childToRemove](std::unique_ptr>& child) { + return child.get() == &childToRemove; + }); + if (it == children.end()) return; + + children.erase(it); +} + +template +void FolderOrItem::MoveFolderOrItemToAnotherFolder( + FolderOrItem& folderOrItem, + FolderOrItem& newParentFolder, + std::size_t newPosition) { + if (!newParentFolder.IsFolder()) return; + if (newParentFolder.IsADescendantOf(folderOrItem)) return; + + auto it = std::find_if( + children.begin(), + children.end(), + [&folderOrItem](std::unique_ptr>& childFolderOrItem) { + return childFolderOrItem.get() == &folderOrItem; + }); + if (it == children.end()) return; + + std::unique_ptr> folderOrItemPtr = std::move(*it); + children.erase(it); + + folderOrItemPtr->parent = &newParentFolder; + newParentFolder.children.insert( + newPosition < newParentFolder.children.size() + ? newParentFolder.children.begin() + newPosition + : newParentFolder.children.end(), + std::move(folderOrItemPtr)); +} + +template +template +void FolderOrItem::SerializeTo(SerializerElement& element, + GetNameFunc getName) const { + if (IsFolder()) { + element.SetAttribute("folderName", GetFolderName()); + if (children.size() > 0) { + SerializerElement& childrenElement = element.AddChild("children"); + childrenElement.ConsiderAsArrayOf("folderOrItem"); + for (std::size_t j = 0; j < children.size(); j++) { + children[j]->SerializeTo(childrenElement.AddChild("folderOrItem"), + getName); + } + } + } else { + element.SetAttribute("itemName", getName(GetItem())); + } + + if (quickCustomizationVisibility != QuickCustomization::Visibility::Default) { + element.SetStringAttribute( + "quickCustomizationVisibility", + quickCustomizationVisibility == QuickCustomization::Visibility::Visible + ? "visible" + : "hidden"); + } +} + +template +template +void FolderOrItem::UnserializeFrom(gd::Project& project, + const SerializerElement& element, + ItemContainerType& itemContainer, + GetItemFunc getItem) { + children.clear(); + gd::String potentialFolderName = element.GetStringAttribute("folderName", ""); + + if (!potentialFolderName.empty()) { + item = nullptr; + folderName = potentialFolderName; + + if (element.HasChild("children")) { + const SerializerElement& childrenElements = + element.GetChild("children", 0); + childrenElements.ConsiderAsArrayOf("folderOrItem"); + for (std::size_t i = 0; i < childrenElements.GetChildrenCount(); ++i) { + std::unique_ptr> childFolderOrItem = + gd::make_unique>(); + childFolderOrItem->UnserializeFrom( + project, childrenElements.GetChild(i), itemContainer, getItem); + childFolderOrItem->parent = this; + + if (childFolderOrItem->folderName != "__INVALID__") { + children.push_back(std::move(childFolderOrItem)); + } + } + } + } else { + folderName = ""; + gd::String itemName = element.GetStringAttribute("itemName", ""); + if (itemName.empty()) { + itemName = element.GetStringAttribute("objectName", ""); + } + + if (itemName.empty()) { + folderName = "__INVALID__"; + return; + } + + item = getItem(itemContainer, itemName); + + if (item == nullptr) { + folderName = "__INVALID__"; + return; + } + } + + if (element.HasChild("quickCustomizationVisibility")) { + quickCustomizationVisibility = + element.GetStringAttribute("quickCustomizationVisibility") == "visible" + ? QuickCustomization::Visibility::Visible + : QuickCustomization::Visibility::Hidden; + } else { + quickCustomizationVisibility = QuickCustomization::Visibility::Default; + } +} + +} // namespace gd + +#endif \ No newline at end of file diff --git a/Core/GDCore/Project/ObjectFolderOrObject.cpp b/Core/GDCore/Project/ObjectFolderOrObject.cpp deleted file mode 100644 index 7dca1c69c5fd..000000000000 --- a/Core/GDCore/Project/ObjectFolderOrObject.cpp +++ /dev/null @@ -1,282 +0,0 @@ -/* - * GDevelop Core - * Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights - * reserved. This project is released under the MIT License. - */ -#include "GDCore/Project/ObjectFolderOrObject.h" - -#include - -#include "GDCore/Project/Object.h" -#include "GDCore/Project/ObjectsContainer.h" -#include "GDCore/Serialization/SerializerElement.h" -#include "GDCore/Tools/Log.h" - -using namespace std; - -namespace gd { - -ObjectFolderOrObject ObjectFolderOrObject::badObjectFolderOrObject; - -ObjectFolderOrObject::ObjectFolderOrObject() - : folderName("__NULL"), - object(nullptr), - quickCustomizationVisibility(QuickCustomization::Visibility::Default) {} -ObjectFolderOrObject::ObjectFolderOrObject(gd::String folderName_, - ObjectFolderOrObject* parent_) - : folderName(folderName_), - parent(parent_), - object(nullptr), - quickCustomizationVisibility(QuickCustomization::Visibility::Default) {} -ObjectFolderOrObject::ObjectFolderOrObject(gd::Object* object_, - ObjectFolderOrObject* parent_) - : object(object_), - parent(parent_), - quickCustomizationVisibility(QuickCustomization::Visibility::Default) {} -ObjectFolderOrObject::~ObjectFolderOrObject() {} - -bool ObjectFolderOrObject::HasObjectNamed(const gd::String& name) { - if (IsFolder()) { - return std::any_of( - children.begin(), - children.end(), - [&name]( - std::unique_ptr& objectFolderOrObject) { - return objectFolderOrObject->HasObjectNamed(name); - }); - } - if (!object) return false; - return object->GetName() == name; -} -ObjectFolderOrObject& ObjectFolderOrObject::GetObjectNamed( - const gd::String& name) { - if (object && object->GetName() == name) { - return *this; - } - if (IsFolder()) { - for (std::size_t j = 0; j < children.size(); j++) { - ObjectFolderOrObject& foundInChild = children[j]->GetObjectNamed(name); - if (&(foundInChild) != &badObjectFolderOrObject) { - return foundInChild; - } - } - } - return badObjectFolderOrObject; -} - -void ObjectFolderOrObject::SetFolderName(const gd::String& name) { - if (!IsFolder()) return; - folderName = name; -} - -ObjectFolderOrObject& ObjectFolderOrObject::GetChildAt(std::size_t index) { - if (index >= children.size()) return badObjectFolderOrObject; - return *children[index]; -} -const ObjectFolderOrObject& ObjectFolderOrObject::GetChildAt( - std::size_t index) const { - if (index >= children.size()) return badObjectFolderOrObject; - return *children[index]; -} -ObjectFolderOrObject& ObjectFolderOrObject::GetObjectChild( - const gd::String& name) { - for (std::size_t j = 0; j < children.size(); j++) { - if (!children[j]->IsFolder()) { - if (children[j]->GetObject().GetName() == name) return *children[j]; - }; - } - return badObjectFolderOrObject; -} - -void ObjectFolderOrObject::InsertObject(gd::Object* insertedObject, - std::size_t position) { - auto objectFolderOrObject = - gd::make_unique(insertedObject, this); - if (position < children.size()) { - children.insert(children.begin() + position, - std::move(objectFolderOrObject)); - } else { - children.push_back(std::move(objectFolderOrObject)); - } -} - -std::size_t ObjectFolderOrObject::GetChildPosition( - const ObjectFolderOrObject& child) const { - for (std::size_t j = 0; j < children.size(); j++) { - if (children[j].get() == &child) return j; - } - return gd::String::npos; -} - -ObjectFolderOrObject& ObjectFolderOrObject::InsertNewFolder( - const gd::String& newFolderName, std::size_t position) { - auto newFolderPtr = - gd::make_unique(newFolderName, this); - gd::ObjectFolderOrObject& newFolder = *(*(children.insert( - position < children.size() ? children.begin() + position : children.end(), - std::move(newFolderPtr)))); - return newFolder; -}; - -void ObjectFolderOrObject::RemoveRecursivelyObjectNamed( - const gd::String& name) { - if (IsFolder()) { - children.erase( - std::remove_if(children.begin(), - children.end(), - [&name](std::unique_ptr& - objectFolderOrObject) { - return !objectFolderOrObject->IsFolder() && - objectFolderOrObject->GetObject().GetName() == - name; - }), - children.end()); - for (auto& it : children) { - it->RemoveRecursivelyObjectNamed(name); - } - } -}; - -void ObjectFolderOrObject::Clear() { - if (IsFolder()) { - for (auto& it : children) { - it->Clear(); - } - children.clear(); - } -}; - -bool ObjectFolderOrObject::IsADescendantOf( - const ObjectFolderOrObject& otherObjectFolderOrObject) { - if (parent == nullptr) return false; - if (&(*parent) == &otherObjectFolderOrObject) return true; - return parent->IsADescendantOf(otherObjectFolderOrObject); -} - -void ObjectFolderOrObject::MoveChild(std::size_t oldIndex, - std::size_t newIndex) { - if (!IsFolder()) return; - if (oldIndex >= children.size() || newIndex >= children.size()) return; - - std::unique_ptr objectFolderOrObject = - std::move(children[oldIndex]); - children.erase(children.begin() + oldIndex); - children.insert(children.begin() + newIndex, std::move(objectFolderOrObject)); -} - -void ObjectFolderOrObject::RemoveFolderChild( - const ObjectFolderOrObject& childToRemove) { - if (!IsFolder() || !childToRemove.IsFolder() || - childToRemove.GetChildrenCount() > 0) { - return; - } - std::vector>::iterator it = find_if( - children.begin(), - children.end(), - [&childToRemove](std::unique_ptr& child) { - return child.get() == &childToRemove; - }); - if (it == children.end()) return; - - children.erase(it); -} - -void ObjectFolderOrObject::MoveObjectFolderOrObjectToAnotherFolder( - gd::ObjectFolderOrObject& objectFolderOrObject, - gd::ObjectFolderOrObject& newParentFolder, - std::size_t newPosition) { - if (!newParentFolder.IsFolder()) return; - if (newParentFolder.IsADescendantOf(objectFolderOrObject)) return; - - std::vector>::iterator it = - find_if(children.begin(), - children.end(), - [&objectFolderOrObject](std::unique_ptr& - childObjectFolderOrObject) { - return childObjectFolderOrObject.get() == &objectFolderOrObject; - }); - if (it == children.end()) return; - - std::unique_ptr objectFolderOrObjectPtr = - std::move(*it); - children.erase(it); - - objectFolderOrObjectPtr->parent = &newParentFolder; - newParentFolder.children.insert( - newPosition < newParentFolder.children.size() - ? newParentFolder.children.begin() + newPosition - : newParentFolder.children.end(), - std::move(objectFolderOrObjectPtr)); -} - -void ObjectFolderOrObject::SerializeTo(SerializerElement& element) const { - if (IsFolder()) { - element.SetAttribute("folderName", GetFolderName()); - if (children.size() > 0) { - SerializerElement& childrenElement = element.AddChild("children"); - childrenElement.ConsiderAsArrayOf("objectFolderOrObject"); - for (std::size_t j = 0; j < children.size(); j++) { - children[j]->SerializeTo( - childrenElement.AddChild("objectFolderOrObject")); - } - } - } else { - element.SetAttribute("objectName", GetObject().GetName()); - } - - if (quickCustomizationVisibility != QuickCustomization::Visibility::Default) { - element.SetStringAttribute( - "quickCustomizationVisibility", - quickCustomizationVisibility == QuickCustomization::Visibility::Visible - ? "visible" - : "hidden"); - } -} - -void ObjectFolderOrObject::UnserializeFrom( - gd::Project& project, - const SerializerElement& element, - gd::ObjectsContainer& objectsContainer) { - children.clear(); - gd::String potentialFolderName = element.GetStringAttribute("folderName", ""); - - if (!potentialFolderName.empty()) { - object = nullptr; - folderName = potentialFolderName; - - if (element.HasChild("children")) { - const SerializerElement& childrenElements = - element.GetChild("children", 0); - childrenElements.ConsiderAsArrayOf("objectFolderOrObject"); - for (std::size_t i = 0; i < childrenElements.GetChildrenCount(); ++i) { - std::unique_ptr childObjectFolderOrObject = - make_unique(); - childObjectFolderOrObject->UnserializeFrom( - project, childrenElements.GetChild(i), objectsContainer); - childObjectFolderOrObject->parent = this; - children.push_back(std::move(childObjectFolderOrObject)); - } - } - } else { - folderName = ""; - gd::String objectName = element.GetStringAttribute("objectName"); - if (objectsContainer.HasObjectNamed(objectName)) { - object = &objectsContainer.GetObject(objectName); - } else { - gd::LogError("Object with name " + objectName + - " not found in objects container."); - object = nullptr; - } - } - - if (element.HasChild("quickCustomizationVisibility")) { - quickCustomizationVisibility = - element.GetStringAttribute("quickCustomizationVisibility") == "visible" - ? QuickCustomization::Visibility::Visible - : QuickCustomization::Visibility::Hidden; - } else { - quickCustomizationVisibility = QuickCustomization::Visibility::Default; - } -}; - -} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/ObjectFolderOrObject.h b/Core/GDCore/Project/ObjectFolderOrObject.h deleted file mode 100644 index 7e4023279f38..000000000000 --- a/Core/GDCore/Project/ObjectFolderOrObject.h +++ /dev/null @@ -1,214 +0,0 @@ -/* - * GDevelop Core - * Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights - * reserved. This project is released under the MIT License. - */ -#ifndef GDCORE_OBJECTFOLDEROROBJECT_H -#define GDCORE_OBJECTFOLDEROROBJECT_H -#include -#include - -#include "GDCore/Serialization/SerializerElement.h" -#include "GDCore/String.h" -#include "GDCore/Project/QuickCustomization.h" - -namespace gd { -class Project; -class Object; -class SerializerElement; -class ObjectsContainer; -} // namespace gd - -namespace gd { - -/** - * \brief Class representing a folder structure in order to organize objects - * in folders (to be used with an ObjectsContainer.) - * - * \see gd::ObjectsContainer - */ -class GD_CORE_API ObjectFolderOrObject { - public: - /** - * \brief Default constructor creating an empty instance. Useful for the null - * object pattern. - */ - ObjectFolderOrObject(); - virtual ~ObjectFolderOrObject(); - /** - * \brief Constructor for creating an instance representing a folder. - */ - ObjectFolderOrObject(gd::String folderName_, - ObjectFolderOrObject* parent_ = nullptr); - /** - * \brief Constructor for creating an instance representing an object. - */ - ObjectFolderOrObject(gd::Object* object_, - ObjectFolderOrObject* parent_ = nullptr); - - /** - * \brief Returns the object behind the instance. - */ - gd::Object& GetObject() const { return *object; } - - /** - * \brief Returns true if the instance represents a folder. - */ - bool IsFolder() const { return !folderName.empty(); } - /** - * \brief Returns the name of the folder. - */ - const gd::String& GetFolderName() const { return folderName; } - - /** - * \brief Set the folder name. Does nothing if called on an instance not - * representing a folder. - */ - void SetFolderName(const gd::String& name); - - /** - * \brief Returns true if the instance represents the object with the given - * name or if any of the children does (recursive search). - */ - bool HasObjectNamed(const gd::String& name); - /** - * \brief Returns the child instance holding the object with the given name - * (recursive search). - */ - ObjectFolderOrObject& GetObjectNamed(const gd::String& name); - - /** - * \brief Returns the number of children. Returns 0 if the instance represents - * an object. - */ - std::size_t GetChildrenCount() const { - if (IsFolder()) return children.size(); - return 0; - } - /** - * \brief Returns the child ObjectFolderOrObject at the given index. - */ - ObjectFolderOrObject& GetChildAt(std::size_t index); - /** - * \brief Returns the child ObjectFolderOrObject at the given index. - */ - const ObjectFolderOrObject& GetChildAt(std::size_t index) const; - /** - * \brief Returns the child ObjectFolderOrObject that represents the object - * with the given name. To use only if sure that the instance holds the object - * in its direct children (no recursive search). - * - * \note The equivalent method to get a folder by its name cannot be - * implemented because there is no unicity enforced on the folder name. - */ - ObjectFolderOrObject& GetObjectChild(const gd::String& name); - - /** - * \brief Returns the parent of the instance. If the instance has no parent - * (root folder), the null object is returned. - */ - ObjectFolderOrObject& GetParent() { - if (parent == nullptr) { - return badObjectFolderOrObject; - } - return *parent; - }; - - /** - * \brief Returns true if the instance is a root folder (that's to say it - * has no parent). - */ - bool IsRootFolder() { return !object && !parent; } - - /** - * \brief Moves a child from a position to a new one. - */ - void MoveChild(std::size_t oldIndex, std::size_t newIndex); - /** - * \brief Removes the given child from the instance's children. If the given - * child contains children of its own, does nothing. - */ - void RemoveFolderChild(const ObjectFolderOrObject& childToRemove); - /** - * \brief Removes the child representing the object with the given name from - * the instance children and recursively does it for every folder children. - */ - void RemoveRecursivelyObjectNamed(const gd::String& name); - /** - * \brief Clears all children - */ - void Clear(); - - /** - * \brief Inserts an instance representing the given object at the given - * position. - */ - void InsertObject(gd::Object* insertedObject, - std::size_t position = (size_t)-1); - /** - * \brief Inserts an instance representing a folder with the given name at the - * given position. - */ - ObjectFolderOrObject& InsertNewFolder(const gd::String& newFolderName, - std::size_t position); - /** - * \brief Returns true if the instance is a descendant of the given instance - * of ObjectFolderOrObject. - */ - bool IsADescendantOf(const ObjectFolderOrObject& otherObjectFolderOrObject); - - /** - * \brief Returns the position of the given instance of ObjectFolderOrObject - * in the instance's children. - */ - std::size_t GetChildPosition(const ObjectFolderOrObject& child) const; - /** - * \brief Moves the given child ObjectFolderOrObject to the given folder at - * the given position. - */ - void MoveObjectFolderOrObjectToAnotherFolder( - gd::ObjectFolderOrObject& objectFolderOrObject, - gd::ObjectFolderOrObject& newParentFolder, - std::size_t newPosition); - - QuickCustomization::Visibility GetQuickCustomizationVisibility() const { return quickCustomizationVisibility; } - void SetQuickCustomizationVisibility(QuickCustomization::Visibility visibility) { - quickCustomizationVisibility = visibility; - } - - /** \name Saving and loading - * Members functions related to saving and loading the objects of the class. - */ - ///@{ - /** - * \brief Serialize the ObjectFolderOrObject instance. - */ - void SerializeTo(SerializerElement& element) const; - - /** - * \brief Unserialize the ObjectFolderOrObject instance. - */ - void UnserializeFrom(gd::Project& project, - const SerializerElement& element, - ObjectsContainer& objectsContainer); - ///@} - - private: - static gd::ObjectFolderOrObject badObjectFolderOrObject; - - gd::ObjectFolderOrObject* - parent = nullptr; // nullptr if root folder, points to the parent folder otherwise. - QuickCustomization::Visibility quickCustomizationVisibility; - - // Representing an object: - gd::Object* object; // nullptr if folderName is set. - - // or representing a folder: - gd::String folderName; // Empty if object is set. - std::vector> - children; // Folder children. -}; - -} // namespace gd - -#endif // GDCORE_OBJECTFOLDEROROBJECT_H diff --git a/Core/GDCore/Project/ObjectsContainer.cpp b/Core/GDCore/Project/ObjectsContainer.cpp index e132756ffd03..fb96bfcd9c18 100644 --- a/Core/GDCore/Project/ObjectsContainer.cpp +++ b/Core/GDCore/Project/ObjectsContainer.cpp @@ -8,18 +8,20 @@ #include #include "GDCore/Extensions/Platform.h" +#include "GDCore/Project/FolderOrItem.h" #include "GDCore/Project/Object.h" -#include "GDCore/Project/ObjectFolderOrObject.h" #include "GDCore/Project/Project.h" #include "GDCore/Serialization/SerializerElement.h" #include "GDCore/Tools/PolymorphicClone.h" namespace gd { +static auto GetObjectName = [](const gd::Object& obj) { return obj.GetName(); }; + ObjectsContainer::ObjectsContainer( const ObjectsContainer::SourceType sourceType_) : sourceType(sourceType_) { - rootFolder = gd::make_unique("__ROOT"); + rootFolder = gd::make_unique>("__ROOT"); } ObjectsContainer::~ObjectsContainer() {} @@ -38,9 +40,7 @@ void ObjectsContainer::Init(const gd::ObjectsContainer& other) { sourceType = other.sourceType; initialObjects = gd::Clone(other.initialObjects); objectGroups = other.objectGroups; - // The objects folders are not copied. - // It's not an issue because the UI uses the serialization for duplication. - rootFolder = gd::make_unique("__ROOT"); + rootFolder = gd::make_unique>("__ROOT"); } void ObjectsContainer::SerializeObjectsTo(SerializerElement& element) const { @@ -49,19 +49,32 @@ void ObjectsContainer::SerializeObjectsTo(SerializerElement& element) const { initialObjects[j]->SerializeTo(element.AddChild("object")); } } + void ObjectsContainer::SerializeFoldersTo(SerializerElement& element) const { - rootFolder->SerializeTo(element); + rootFolder->SerializeTo(element, GetObjectName); } void ObjectsContainer::UnserializeFoldersFrom( gd::Project& project, const SerializerElement& element) { - rootFolder->UnserializeFrom(project, element, *this); + rootFolder->UnserializeFrom( + project, + element, + *this, + [](ObjectsContainer& container, const gd::String& name) -> gd::Object* { + if (container.HasObjectNamed(name)) { + return &container.GetObject(name); + } + gd::LogError("Object with name " + name + " not found."); + return nullptr; + }); } void ObjectsContainer::AddMissingObjectsInRootFolder() { for (std::size_t i = 0; i < initialObjects.size(); ++i) { - if (!rootFolder->HasObjectNamed(initialObjects[i]->GetName())) { - rootFolder->InsertObject(&(*initialObjects[i])); + const gd::String& objectName = initialObjects[i]->GetName(); + + if (!rootFolder->HasItemNamed(objectName, GetObjectName)) { + rootFolder->InsertItem(&(*initialObjects[i])); } } } @@ -70,19 +83,19 @@ void ObjectsContainer::UnserializeObjectsFrom( gd::Project& project, const SerializerElement& element) { Clear(); element.ConsiderAsArrayOf("object", "Objet"); + for (std::size_t i = 0; i < element.GetChildrenCount(); ++i) { const SerializerElement& objectElement = element.GetChild(i); gd::String type = objectElement.GetStringAttribute("type"); - std::unique_ptr newObject = project.CreateObject( - type, objectElement.GetStringAttribute("name", "", "nom")); + gd::String name = objectElement.GetStringAttribute("name", "", "nom"); + + std::unique_ptr newObject = project.CreateObject(type, name); if (newObject) { newObject->UnserializeFrom(project, objectElement); initialObjects.push_back(std::move(newObject)); - } else - std::cout << "WARNING: Unknown object type \"" << type << "\"" - << std::endl; + } } } @@ -93,6 +106,7 @@ bool ObjectsContainer::HasObjectNamed(const gd::String& name) const { return object->GetName() == name; }) != initialObjects.end()); } + gd::Object& ObjectsContainer::GetObject(const gd::String& name) { return *(*find_if(initialObjects.begin(), initialObjects.end(), @@ -100,6 +114,7 @@ gd::Object& ObjectsContainer::GetObject(const gd::String& name) { return object->GetName() == name; })); } + const gd::Object& ObjectsContainer::GetObject(const gd::String& name) const { return *(*find_if(initialObjects.begin(), initialObjects.end(), @@ -107,21 +122,26 @@ const gd::Object& ObjectsContainer::GetObject(const gd::String& name) const { return object->GetName() == name; })); } + gd::Object& ObjectsContainer::GetObject(std::size_t index) { return *initialObjects[index]; } + const gd::Object& ObjectsContainer::GetObject(std::size_t index) const { return *initialObjects[index]; } + std::size_t ObjectsContainer::GetObjectPosition(const gd::String& name) const { for (std::size_t i = 0; i < initialObjects.size(); ++i) { if (initialObjects[i]->GetName() == name) return i; } return gd::String::npos; } + std::size_t ObjectsContainer::GetObjectsCount() const { return initialObjects.size(); } + gd::Object& ObjectsContainer::InsertNewObject(const gd::Project& project, const gd::String& objectType, const gd::String& name, @@ -131,7 +151,7 @@ gd::Object& ObjectsContainer::InsertNewObject(const gd::Project& project, : initialObjects.end(), project.CreateObject(objectType, name)))); - rootFolder->InsertObject(&newlyCreatedObject); + rootFolder->InsertItem(&newlyCreatedObject); return newlyCreatedObject; } @@ -140,12 +160,12 @@ gd::Object& ObjectsContainer::InsertNewObjectInFolder( const gd::Project& project, const gd::String& objectType, const gd::String& name, - gd::ObjectFolderOrObject& objectFolderOrObject, + gd::FolderOrItem& folderOrItem, std::size_t position) { gd::Object& newlyCreatedObject = *(*(initialObjects.insert( initialObjects.end(), project.CreateObject(objectType, name)))); - objectFolderOrObject.InsertObject(&newlyCreatedObject, position); + folderOrItem.InsertItem(&newlyCreatedObject, position); return newlyCreatedObject; } @@ -178,7 +198,7 @@ void ObjectsContainer::RemoveObject(const gd::String& name) { }); if (objectIt == initialObjects.end()) return; - rootFolder->RemoveRecursivelyObjectNamed(name); + rootFolder->RemoveRecursivelyItemNamed(name, GetObjectName); initialObjects.erase(objectIt); } @@ -189,18 +209,18 @@ void ObjectsContainer::Clear() { } void ObjectsContainer::MoveObjectFolderOrObjectToAnotherContainerInFolder( - gd::ObjectFolderOrObject& objectFolderOrObject, + gd::FolderOrItem& folderOrItem, gd::ObjectsContainer& newContainer, - gd::ObjectFolderOrObject& newParentFolder, + gd::FolderOrItem& newParentFolder, std::size_t newPosition) { - if (objectFolderOrObject.IsFolder() || !newParentFolder.IsFolder()) return; + if (folderOrItem.IsFolder() || !newParentFolder.IsFolder()) return; - std::vector>::iterator objectIt = find_if( - initialObjects.begin(), - initialObjects.end(), - [&objectFolderOrObject](std::unique_ptr& object) { - return object->GetName() == objectFolderOrObject.GetObject().GetName(); - }); + std::vector>::iterator objectIt = + find_if(initialObjects.begin(), + initialObjects.end(), + [&folderOrItem](std::unique_ptr& object) { + return object->GetName() == folderOrItem.GetItem().GetName(); + }); if (objectIt == initialObjects.end()) return; std::unique_ptr object = std::move(*objectIt); @@ -208,8 +228,8 @@ void ObjectsContainer::MoveObjectFolderOrObjectToAnotherContainerInFolder( newContainer.initialObjects.push_back(std::move(object)); - objectFolderOrObject.GetParent().MoveObjectFolderOrObjectToAnotherFolder( - objectFolderOrObject, newParentFolder, newPosition); + folderOrItem.GetParent().MoveFolderOrItemToAnotherFolder( + folderOrItem, newParentFolder, newPosition); } std::set ObjectsContainer::GetAllObjectNames() const { @@ -220,12 +240,12 @@ std::set ObjectsContainer::GetAllObjectNames() const { return names; } -std::vector +std::vector*> ObjectsContainer::GetAllObjectFolderOrObjects() const { - std::vector results; + std::vector*> results; - std::function addChildrenOfFolder = - [&](const ObjectFolderOrObject& folder) { + std::function& folder)> + addChildrenOfFolder = [&](const FolderOrItem& folder) { for (size_t i = 0; i < folder.GetChildrenCount(); ++i) { const auto& child = folder.GetChildAt(i); results.push_back(&child); @@ -241,4 +261,4 @@ ObjectsContainer::GetAllObjectFolderOrObjects() const { return results; } -} // namespace gd +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/ObjectsContainer.h b/Core/GDCore/Project/ObjectsContainer.h index 15a32b94ec49..f7a04d726e81 100644 --- a/Core/GDCore/Project/ObjectsContainer.h +++ b/Core/GDCore/Project/ObjectsContainer.h @@ -6,11 +6,12 @@ #pragma once #include -#include #include -#include "GDCore/String.h" +#include + +#include "GDCore/Project/FolderOrItem.h" #include "GDCore/Project/ObjectGroupsContainer.h" -#include "GDCore/Project/ObjectFolderOrObject.h" +#include "GDCore/String.h" namespace gd { class Object; class Project; @@ -34,14 +35,19 @@ namespace gd { * * \ingroup PlatformDefinition */ + +template +class FolderOrItem; +using ObjectFolderOrObject = FolderOrItem; + class GD_CORE_API ObjectsContainer { public: enum SourceType { - Unknown, - Global, - Scene, - Object, - Function, + Unknown, + Global, + Scene, + Object, + Function, }; /** @@ -124,7 +130,7 @@ class GD_CORE_API ObjectsContainer { const gd::Project& project, const gd::String& objectType, const gd::String& name, - gd::ObjectFolderOrObject& objectFolderOrObject, + gd::FolderOrItem& folderOrItem, std::size_t position); /** @@ -161,9 +167,9 @@ class GD_CORE_API ObjectsContainer { * moved in memory, as referenced by smart pointers internally). */ void MoveObjectFolderOrObjectToAnotherContainerInFolder( - gd::ObjectFolderOrObject& objectFolderOrObject, + gd::FolderOrItem& folderOrItem, gd::ObjectsContainer& newContainer, - gd::ObjectFolderOrObject& newParentFolder, + gd::FolderOrItem& newParentFolder, std::size_t newPosition); /** @@ -174,14 +180,14 @@ class GD_CORE_API ObjectsContainer { /** * Provide a raw access to the vector containing the objects */ - std::vector >& GetObjects() { + std::vector>& GetObjects() { return initialObjects; } /** * Provide a raw access to the vector containing the objects */ - const std::vector >& GetObjects() const { + const std::vector>& GetObjects() const { return initialObjects; } @@ -190,14 +196,13 @@ class GD_CORE_API ObjectsContainer { /** * Returns a vector containing all object and folders in this container. - * Only use this for checking if you hold a valid `ObjectFolderOrObject` - + * Only use this for checking if you hold a valid `FolderOrItem` - * don't use this for rendering or anything else. */ - std::vector GetAllObjectFolderOrObjects() const; + std::vector*> GetAllObjectFolderOrObjects() + const; - gd::ObjectFolderOrObject& GetRootFolder() { - return *rootFolder; - } + gd::FolderOrItem& GetRootFolder() { return *rootFolder; } void AddMissingObjectsInRootFolder(); @@ -245,13 +250,13 @@ class GD_CORE_API ObjectsContainer { ///@} protected: - std::vector > + std::vector> initialObjects; ///< Objects contained. gd::ObjectGroupsContainer objectGroups; private: SourceType sourceType = Unknown; - std::unique_ptr rootFolder; + std::unique_ptr> rootFolder; /** * Initialize from another variables container, copying elements. Used by @@ -260,4 +265,4 @@ class GD_CORE_API ObjectsContainer { void Init(const ObjectsContainer& other); }; -} // namespace gd +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/Project.cpp b/Core/GDCore/Project/Project.cpp index 38aec183a0e2..8465a6a7304b 100644 --- a/Core/GDCore/Project/Project.cpp +++ b/Core/GDCore/Project/Project.cpp @@ -77,7 +77,9 @@ Project::Project() objectsContainer(gd::ObjectsContainer::SourceType::Global), resourcesContainer(gd::ResourcesContainer::SourceType::Global), sceneResourcesPreloading("at-startup"), - sceneResourcesUnloading("never") {} + sceneResourcesUnloading("never"), + layoutsRootFolder( + gd::make_unique>("__ROOT")) {} Project::~Project() {} @@ -100,33 +102,36 @@ void Project::EnsureObjectDefaultBehaviors(gd::Object& object) const { const gd::String& behaviorName = behaviorMetadata.GetDefaultName(); - // Check if we can keep a behavior that would have been already set up on the object. + // Check if we can keep a behavior that would have been already set up on + // the object. if (object.HasBehaviorNamed(behaviorName)) { const auto& behavior = object.GetBehavior(behaviorName); - if (!behavior.IsDefaultBehavior() || behavior.GetTypeName() != behaviorType) { + if (!behavior.IsDefaultBehavior() || + behavior.GetTypeName() != behaviorType) { // Behavior type has changed, remove it so it is re-created. object.RemoveBehavior(behaviorName); } } if (!object.HasBehaviorNamed(behaviorName)) { - auto* behavior = object.AddNewBehavior( - project, behaviorType, behaviorName); + auto* behavior = + object.AddNewBehavior(project, behaviorType, behaviorName); behavior->SetDefaultBehavior(true); } }; - auto &objectMetadata = + auto& objectMetadata = gd::MetadataProvider::GetObjectMetadata(platform, objectType); if (!MetadataProvider::IsBadObjectMetadata(objectMetadata)) { // Add all default behaviors. const auto& defaultBehaviorTypes = objectMetadata.GetDefaultBehaviors(); - for (auto &behaviorType : defaultBehaviorTypes) { + for (auto& behaviorType : defaultBehaviorTypes) { addDefaultBehavior(behaviorType); } - // Ensure there are no default behaviors that would not be required left on the object. + // Ensure there are no default behaviors that would not be required left on + // the object. for (const auto& behaviorName : object.GetAllBehaviorNames()) { auto& behavior = object.GetBehavior(behaviorName); if (!behavior.IsDefaultBehavior()) { @@ -134,7 +139,8 @@ void Project::EnsureObjectDefaultBehaviors(gd::Object& object) const { continue; } - if (defaultBehaviorTypes.find(behavior.GetTypeName()) == defaultBehaviorTypes.end()) { + if (defaultBehaviorTypes.find(behavior.GetTypeName()) == + defaultBehaviorTypes.end()) { object.RemoveBehavior(behaviorName); } } @@ -307,16 +313,18 @@ bool Project::HasLayoutNamed(const gd::String& name) const { }) != scenes.end()); } gd::Layout& Project::GetLayout(const gd::String& name) { - return *(*find_if( - scenes.begin(), scenes.end(), [&name](const std::unique_ptr& layout) { - return layout->GetName() == name; - })); + return *(*find_if(scenes.begin(), + scenes.end(), + [&name](const std::unique_ptr& layout) { + return layout->GetName() == name; + })); } const gd::Layout& Project::GetLayout(const gd::String& name) const { - return *(*find_if( - scenes.begin(), scenes.end(), [&name](const std::unique_ptr& layout) { - return layout->GetName() == name; - })); + return *(*find_if(scenes.begin(), + scenes.end(), + [&name](const std::unique_ptr& layout) { + return layout->GetName() == name; + })); } gd::Layout& Project::GetLayout(std::size_t index) { return *scenes[index]; } const gd::Layout& Project::GetLayout(std::size_t index) const { @@ -345,6 +353,8 @@ gd::Layout& Project::InsertNewLayout(const gd::String& name, newlyInsertedLayout.SetName(name); newlyInsertedLayout.UpdateBehaviorsSharedData(*this); + layoutsRootFolder->InsertItem(&newlyInsertedLayout); + return newlyInsertedLayout; } @@ -360,36 +370,42 @@ gd::Layout& Project::InsertLayout(const gd::Layout& layout, } void Project::RemoveLayout(const gd::String& name) { - std::vector >::iterator scene = - find_if(scenes.begin(), scenes.end(), [&name](const std::unique_ptr& layout) { - return layout->GetName() == name; - }); + std::vector>::iterator scene = + find_if(scenes.begin(), + scenes.end(), + [&name](const std::unique_ptr& layout) { + return layout->GetName() == name; + }); if (scene == scenes.end()) return; scenes.erase(scene); } bool Project::HasExternalEventsNamed(const gd::String& name) const { - return (find_if(externalEvents.begin(), - externalEvents.end(), - [&name](const std::unique_ptr& externalEvents) { - return externalEvents->GetName() == name; - }) != externalEvents.end()); + return ( + find_if( + externalEvents.begin(), + externalEvents.end(), + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + }) != externalEvents.end()); } gd::ExternalEvents& Project::GetExternalEvents(const gd::String& name) { - return *(*find_if(externalEvents.begin(), - externalEvents.end(), - [&name](const std::unique_ptr& externalEvents) { - return externalEvents->GetName() == name; - })); + return *(*find_if( + externalEvents.begin(), + externalEvents.end(), + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + })); } const gd::ExternalEvents& Project::GetExternalEvents( const gd::String& name) const { - return *(*find_if(externalEvents.begin(), - externalEvents.end(), - [&name](const std::unique_ptr& externalEvents) { - return externalEvents->GetName() == name; - })); + return *(*find_if( + externalEvents.begin(), + externalEvents.end(), + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + })); } gd::ExternalEvents& Project::GetExternalEvents(std::size_t index) { return *externalEvents[index]; @@ -430,12 +446,12 @@ gd::ExternalEvents& Project::InsertExternalEvents( } void Project::RemoveExternalEvents(const gd::String& name) { - std::vector >::iterator events = - find_if(externalEvents.begin(), - externalEvents.end(), - [&name](const std::unique_ptr& externalEvents) { - return externalEvents->GetName() == name; - }); + std::vector>::iterator events = find_if( + externalEvents.begin(), + externalEvents.end(), + [&name](const std::unique_ptr& externalEvents) { + return externalEvents->GetName() == name; + }); if (events == externalEvents.end()) return; externalEvents.erase(events); @@ -499,26 +515,30 @@ void Project::SwapExternalLayouts(std::size_t first, std::size_t second) { externalLayouts.begin() + second); } bool Project::HasExternalLayoutNamed(const gd::String& name) const { - return (find_if(externalLayouts.begin(), - externalLayouts.end(), - [&name](const std::unique_ptr& externalLayout) { - return externalLayout->GetName() == name; - }) != externalLayouts.end()); + return ( + find_if( + externalLayouts.begin(), + externalLayouts.end(), + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + }) != externalLayouts.end()); } gd::ExternalLayout& Project::GetExternalLayout(const gd::String& name) { - return *(*find_if(externalLayouts.begin(), - externalLayouts.end(), - [&name](const std::unique_ptr& externalLayout) { - return externalLayout->GetName() == name; - })); + return *(*find_if( + externalLayouts.begin(), + externalLayouts.end(), + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + })); } const gd::ExternalLayout& Project::GetExternalLayout( const gd::String& name) const { - return *(*find_if(externalLayouts.begin(), - externalLayouts.end(), - [&name](const std::unique_ptr& externalLayout) { - return externalLayout->GetName() == name; - })); + return *(*find_if( + externalLayouts.begin(), + externalLayouts.end(), + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + })); } gd::ExternalLayout& Project::GetExternalLayout(std::size_t index) { return *externalLayouts[index]; @@ -559,12 +579,13 @@ gd::ExternalLayout& Project::InsertExternalLayout( } void Project::RemoveExternalLayout(const gd::String& name) { - std::vector >::iterator externalLayout = - find_if(externalLayouts.begin(), - externalLayouts.end(), - [&name](const std::unique_ptr& externalLayout) { - return externalLayout->GetName() == name; - }); + std::vector>::iterator externalLayout = + find_if( + externalLayouts.begin(), + externalLayouts.end(), + [&name](const std::unique_ptr& externalLayout) { + return externalLayout->GetName() == name; + }); if (externalLayout == externalLayouts.end()) return; externalLayouts.erase(externalLayout); @@ -653,7 +674,7 @@ gd::EventsFunctionsExtension& Project::InsertEventsFunctionsExtension( } void Project::RemoveEventsFunctionsExtension(const gd::String& name) { - std::vector >::iterator + std::vector>::iterator eventsFunctionExtension = find_if( eventsFunctionsExtensions.begin(), eventsFunctionsExtensions.end(), @@ -870,9 +891,11 @@ void Project::UnserializeFrom(const SerializerElement& element) { element.GetChild("objectsGroups", 0, "ObjectGroups")); resourcesContainer.UnserializeFrom( element.GetChild("resources", 0, "Resources")); - objectsContainer.UnserializeObjectsFrom(*this, element.GetChild("objects", 0, "Objects")); + objectsContainer.UnserializeObjectsFrom( + *this, element.GetChild("objects", 0, "Objects")); if (element.HasChild("objectsFolderStructure")) { - objectsContainer.UnserializeFoldersFrom(*this, element.GetChild("objectsFolderStructure", 0)); + objectsContainer.UnserializeFoldersFrom( + *this, element.GetChild("objectsFolderStructure", 0)); } objectsContainer.AddMissingObjectsInRootFolder(); @@ -890,6 +913,25 @@ void Project::UnserializeFrom(const SerializerElement& element) { layout.UnserializeFrom(*this, layoutElement); } SetFirstLayout(element.GetChild("firstLayout").GetStringValue()); + if (element.HasChild("layoutsFolderStructure")) { + layoutsRootFolder->UnserializeFrom( + *this, + element.GetChild("layoutsFolderStructure"), + *this, + [](Project& project, const gd::String& name) -> gd::Layout* { + if (project.HasLayoutNamed(name)) { + return &project.GetLayout(name); + } + return nullptr; + }); + } + for (std::size_t i = 0; i < scenes.size(); ++i) { + if (!layoutsRootFolder->HasItemNamed( + scenes[i]->GetName(), + [](const gd::Layout& layout) { return layout.GetName(); })) { + layoutsRootFolder->InsertItem(scenes[i].get()); + } + } externalEvents.clear(); const SerializerElement& externalEventsElement = @@ -920,7 +962,7 @@ void Project::UnserializeFrom(const SerializerElement& element) { } void Project::UnserializeAndInsertExtensionsFrom( - const gd::SerializerElement &eventsFunctionsExtensionsElement) { + const gd::SerializerElement& eventsFunctionsExtensionsElement) { eventsFunctionsExtensionsElement.ConsiderAsArrayOf( "eventsFunctionsExtension"); @@ -936,7 +978,8 @@ void Project::UnserializeAndInsertExtensionsFrom( ++i) { const SerializerElement& eventsFunctionsExtensionElement = eventsFunctionsExtensionsElement.GetChild(i); - const gd::String& name = eventsFunctionsExtensionElement.GetStringAttribute("name"); + const gd::String& name = + eventsFunctionsExtensionElement.GetStringAttribute("name"); extensionNameToElementIndex[name] = i; gd::EventsFunctionsExtension& eventsFunctionsExtension = @@ -946,7 +989,7 @@ void Project::UnserializeAndInsertExtensionsFrom( name, GetEventsFunctionsExtensionsCount()); // Backup the events-based object variants - for (auto &eventsBasedObject : + for (auto& eventsBasedObject : eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) { gd::SerializerElement variantsElement; eventsBasedObject->GetVariants().SerializeVariantsTo(variantsElement); @@ -959,35 +1002,38 @@ void Project::UnserializeAndInsertExtensionsFrom( } // Then unserialize functions, behaviors and objects content. - for (gd::String &extensionName : + for (gd::String& extensionName : GetUnserializingOrderExtensionNames(eventsFunctionsExtensionsElement)) { - size_t extensionIndex = GetEventsFunctionsExtensionPosition(extensionName); if (extensionIndex == gd::String::npos) { // Should never happen because the extension was added in the first pass. - gd::LogError("Can't find extension " + extensionName + " in the list of extensions in second pass of unserialization."); + gd::LogError( + "Can't find extension " + extensionName + + " in the list of extensions in second pass of unserialization."); continue; } - auto& partiallyLoadedExtension = eventsFunctionsExtensions.at(extensionIndex); + auto& partiallyLoadedExtension = + eventsFunctionsExtensions.at(extensionIndex); - if (extensionNameToElementIndex.find(extensionName) == extensionNameToElementIndex.end()) { + if (extensionNameToElementIndex.find(extensionName) == + extensionNameToElementIndex.end()) { // Should never happen because the extension element is present. - gd::LogError("Can't find extension element to unserialize for " + extensionName + " in second pass of unserialization."); + gd::LogError("Can't find extension element to unserialize for " + + extensionName + " in second pass of unserialization."); continue; } size_t elementIndex = extensionNameToElementIndex[extensionName]; - const SerializerElement &eventsFunctionsExtensionElement = + const SerializerElement& eventsFunctionsExtensionElement = eventsFunctionsExtensionsElement.GetChild(elementIndex); - partiallyLoadedExtension - ->UnserializeExtensionImplementationFrom( - *this, eventsFunctionsExtensionElement); + partiallyLoadedExtension->UnserializeExtensionImplementationFrom( + *this, eventsFunctionsExtensionElement); - for (auto &pair : objectTypeToVariantsElement) { - auto &objectType = pair.first; - auto &variantsElement = pair.second; + for (auto& pair : objectTypeToVariantsElement) { + auto& objectType = pair.first; + auto& variantsElement = pair.second; - auto &eventsBasedObject = GetEventsBasedObject(objectType); + auto& eventsBasedObject = GetEventsBasedObject(objectType); eventsBasedObject.GetVariants().UnserializeVariantsFrom(*this, variantsElement); } @@ -995,22 +1041,26 @@ void Project::UnserializeAndInsertExtensionsFrom( } std::vector Project::GetUnserializingOrderExtensionNames( - const gd::SerializerElement &eventsFunctionsExtensionsElement) { + const gd::SerializerElement& eventsFunctionsExtensionsElement) { eventsFunctionsExtensionsElement.ConsiderAsArrayOf( "eventsFunctionsExtension"); - // Some extension have custom objects, which have child objects coming from other extension. - // These child objects must be loaded completely before the parent custom obejct can be unserialized. - // This implies: an order on the extension unserialization (and no cycles). + // Some extension have custom objects, which have child objects coming from + // other extension. These child objects must be loaded completely before the + // parent custom obejct can be unserialized. This implies: an order on the + // extension unserialization (and no cycles). // At the beginning, everything is yet to be loaded. std::map extensionNameToElementIndex; std::vector remainingExtensionNames( eventsFunctionsExtensionsElement.GetChildrenCount()); - for (std::size_t i = 0; i < eventsFunctionsExtensionsElement.GetChildrenCount(); ++i) { + for (std::size_t i = 0; + i < eventsFunctionsExtensionsElement.GetChildrenCount(); + ++i) { const SerializerElement& eventsFunctionsExtensionElement = eventsFunctionsExtensionsElement.GetChild(i); - const gd::String& name = eventsFunctionsExtensionElement.GetStringAttribute("name"); + const gd::String& name = + eventsFunctionsExtensionElement.GetStringAttribute("name"); remainingExtensionNames[i] = name; extensionNameToElementIndex[name] = i; @@ -1020,28 +1070,30 @@ std::vector Project::GetUnserializingOrderExtensionNames( // at least one other object from another extension that is not loaded yet. auto isDependentFromRemainingExtensions = [&remainingExtensionNames]( - const gd::SerializerElement &eventsFunctionsExtensionElement) { - auto &eventsBasedObjectsElement = + const gd::SerializerElement& eventsFunctionsExtensionElement) { + auto& eventsBasedObjectsElement = eventsFunctionsExtensionElement.GetChild("eventsBasedObjects"); eventsBasedObjectsElement.ConsiderAsArrayOf("eventsBasedObject"); for (std::size_t eventsBasedObjectsIndex = 0; eventsBasedObjectsIndex < eventsBasedObjectsElement.GetChildrenCount(); ++eventsBasedObjectsIndex) { - auto &objectsElement = + auto& objectsElement = eventsBasedObjectsElement.GetChild(eventsBasedObjectsIndex) .GetChild("objects"); objectsElement.ConsiderAsArrayOf("object"); for (std::size_t objectIndex = 0; - objectIndex < objectsElement.GetChildrenCount(); ++objectIndex) { - const gd::String &objectType = + objectIndex < objectsElement.GetChildrenCount(); + ++objectIndex) { + const gd::String& objectType = objectsElement.GetChild(objectIndex).GetStringAttribute("type"); gd::String extensionName = eventsFunctionsExtensionElement.GetStringAttribute("name"); gd::String usedExtensionName = - gd::PlatformExtension::GetExtensionFromFullObjectType(objectType); + gd::PlatformExtension::GetExtensionFromFullObjectType( + objectType); if (usedExtensionName != extensionName && std::find(remainingExtensionNames.begin(), @@ -1054,8 +1106,8 @@ std::vector Project::GetUnserializingOrderExtensionNames( return false; }; - // Find the order of loading so that the extensions are loaded when all the other - // extensions they depend on are already loaded. + // Find the order of loading so that the extensions are loaded when all the + // other extensions they depend on are already loaded. std::vector loadOrderExtensionNames; bool foundAnyExtension = true; while (foundAnyExtension) { @@ -1064,7 +1116,7 @@ std::vector Project::GetUnserializingOrderExtensionNames( auto extensionName = remainingExtensionNames[i]; size_t elementIndex = extensionNameToElementIndex[extensionName]; - const SerializerElement &eventsFunctionsExtensionElement = + const SerializerElement& eventsFunctionsExtensionElement = eventsFunctionsExtensionsElement.GetChild(elementIndex); if (!isDependentFromRemainingExtensions( @@ -1157,7 +1209,7 @@ void Project::SerializeTo(SerializerElement& element) const { // end of compatibility code extensionProperties.SerializeTo(propElement.AddChild("extensionProperties")); - + playableDevicesElement.AddChild("").SetStringValue("mobile"); SerializerElement& platformsElement = propElement.AddChild("platforms"); @@ -1178,16 +1230,20 @@ void Project::SerializeTo(SerializerElement& element) const { std::cout << "ERROR: The project current platform is NULL."; if (sceneResourcesPreloading != "at-startup") { - propElement.SetAttribute("sceneResourcesPreloading", sceneResourcesPreloading); + propElement.SetAttribute("sceneResourcesPreloading", + sceneResourcesPreloading); } if (sceneResourcesUnloading != "never") { - propElement.SetAttribute("sceneResourcesUnloading", sceneResourcesUnloading); + propElement.SetAttribute("sceneResourcesUnloading", + sceneResourcesUnloading); } resourcesContainer.SerializeTo(element.AddChild("resources")); objectsContainer.SerializeObjectsTo(element.AddChild("objects")); - objectsContainer.SerializeFoldersTo(element.AddChild("objectsFolderStructure")); - objectsContainer.GetObjectGroups().SerializeTo(element.AddChild("objectsGroups")); + objectsContainer.SerializeFoldersTo( + element.AddChild("objectsFolderStructure")); + objectsContainer.GetObjectGroups().SerializeTo( + element.AddChild("objectsGroups")); GetVariables().SerializeTo(element.AddChild("variables")); element.SetAttribute("firstLayout", firstLayout); @@ -1196,6 +1252,10 @@ void Project::SerializeTo(SerializerElement& element) const { for (std::size_t i = 0; i < GetLayoutsCount(); i++) GetLayout(i).SerializeTo(layoutsElement.AddChild("layout")); + layoutsRootFolder->SerializeTo( + element.AddChild("layoutsFolderStructure"), + [](const gd::Layout& layout) { return layout.GetName(); }); + SerializerElement& externalEventsElement = element.AddChild("externalEvents"); externalEventsElement.ConsiderAsArrayOf("externalEvents"); for (std::size_t i = 0; i < GetExternalEventsCount(); ++i) @@ -1256,7 +1316,7 @@ gd::String Project::GetSafeName(const gd::String& name) { return newName; } -Project::Project(const Project &other) +Project::Project(const Project& other) : objectsContainer(gd::ObjectsContainer::SourceType::Global), resourcesContainer(gd::ResourcesContainer::SourceType::Global) { Init(other); @@ -1318,6 +1378,8 @@ void Project::Init(const gd::Project& game) { scenes = gd::Clone(game.scenes); + layoutsRootFolder = gd::make_unique>("__ROOT"); + externalEvents = gd::Clone(game.externalEvents); externalLayouts = gd::Clone(game.externalLayouts); diff --git a/Core/GDCore/Project/Project.h b/Core/GDCore/Project/Project.h index 1c244fe8ec96..16ec3b41a592 100644 --- a/Core/GDCore/Project/Project.h +++ b/Core/GDCore/Project/Project.h @@ -36,6 +36,8 @@ class Behavior; class BehaviorsSharedData; class BaseEvent; class SerializerElement; +template +class FolderOrItem; } // namespace gd #undef GetObject // Disable an annoying macro #undef CreateEvent @@ -909,9 +911,11 @@ class GD_CORE_API Project { /** * \brief Unserialize and insert in the project the extensions. * - * Unserialization is done in two passe to allow dependencies between extensions. + * Unserialization is done in two passe to allow dependencies between + * extensions. * - * \note If an extension with the same name already exists, it will be overwritten. + * \note If an extension with the same name already exists, it will be + * overwritten. */ void UnserializeAndInsertExtensionsFrom( const gd::SerializerElement& eventsFunctionsExtensionsElement); @@ -965,16 +969,16 @@ class GD_CORE_API Project { */ ///@{ /** - * \brief Provide access to the ResourcesContainer member containing the list of - * the resources. + * \brief Provide access to the ResourcesContainer member containing the list + * of the resources. */ const ResourcesContainer& GetResourcesManager() const { return resourcesContainer; } /** - * \brief Provide access to the ResourcesContainer member containing the list of - * the resources. + * \brief Provide access to the ResourcesContainer member containing the list + * of the resources. */ ResourcesContainer& GetResourcesManager() { return resourcesContainer; } @@ -1079,6 +1083,20 @@ class GD_CORE_API Project { static std::vector GetUnserializingOrderExtensionNames( const gd::SerializerElement& eventsFunctionsExtensionsElement); + /** + * \brief Get the root folder for layouts. + */ + gd::FolderOrItem& GetLayoutsRootFolder() { + return *layoutsRootFolder; + } + + /** + * \brief Get the root folder for layouts (const). + */ + const gd::FolderOrItem& GetLayoutsRootFolder() const { + return *layoutsRootFolder; + } + private: /** * Initialize from another game. Used by copy-ctor and assign-op. @@ -1123,19 +1141,20 @@ class GD_CORE_API Project { ///< instead of the highest Z order ///< found on the layer at the scene ///< startup. - std::vector > scenes; ///< List of all scenes + std::vector> scenes; //< List of all scenes + std::unique_ptr> layoutsRootFolder; gd::VariablesContainer variables; ///< Initial global variables gd::ObjectsContainer objectsContainer; - std::vector > + std::vector> externalLayouts; ///< List of all externals layouts - std::vector > + std::vector> eventsFunctionsExtensions; gd::ResourcesContainer resourcesContainer; ///< Contains all resources used by the project std::vector platforms; ///< Pointers to the platforms this project supports. gd::String firstLayout; - gd::String author; ///< Game author name, for publishing purpose. + gd::String author; ///< Game author name, for publishing purpose. std::vector authorIds; ///< Game author ids, from GDevelop users DB. std::vector @@ -1161,7 +1180,7 @@ class GD_CORE_API Project { gd::PlatformSpecificAssets platformSpecificAssets; gd::LoadingScreen loadingScreen; gd::Watermark watermark; - std::vector > + std::vector> externalEvents; ///< List of all externals events ExtensionProperties extensionProperties; ///< The properties of the extensions. @@ -1180,8 +1199,8 @@ class GD_CORE_API Project { 0; ///< The GD build version used the last ///< time the project was saved. bool areEffectsHiddenInEditor = - false; ///< When false effects are not shown and a default light is used - ///< for 3D layers. + false; ///< When false effects are not shown and a default light is used + ///< for 3D layers. }; } // namespace gd diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 5ddbe2d01686..e23fdf1a33ce 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -672,6 +672,45 @@ interface Project { boolean STATIC_IsNameSafe([Const] DOMString name); [Const, Value] DOMString STATIC_GetSafeName([Const] DOMString name); + + [Ref] LayoutFolderOrLayout GetLayoutsRootFolder(); +}; + +interface LayoutFolderOrLayout { + void LayoutFolderOrLayout(); + + boolean IsFolder(); + boolean IsRootFolder(); + + [Ref] Layout GetItem(); + [Const, Ref] DOMString GetFolderName(); + void SetFolderName([Const] DOMString name); + + unsigned long GetChildrenCount(); + [Ref] LayoutFolderOrLayout GetChildAt(unsigned long pos); + [Ref] LayoutFolderOrLayout GetParent(); + + // Neu: Item (Szene) einfügen + void InsertObject(Layout ptr, unsigned long position); + + [Ref] LayoutFolderOrLayout InsertNewFolder([Const] DOMString name, unsigned long newPosition); + unsigned long GetChildPosition([Const, Ref] LayoutFolderOrLayout child); + + // Namensbasierte Suche (sehr nützlich für JS) + boolean HasObjectNamed([Const] DOMString name); + [Ref] LayoutFolderOrLayout GetObjectNamed([Const] DOMString name); + + void MoveObjectFolderOrObjectToAnotherFolder([Ref] LayoutFolderOrLayout folderOrItem, [Ref] LayoutFolderOrLayout newParentFolder, unsigned long newPosition); + + void MoveChild(unsigned long oldIndex, unsigned long newIndex); + void RemoveFolderChild([Const, Ref] LayoutFolderOrLayout childToRemove); + + // Neu: Rekursives Löschen eines Items nach Name + void RemoveRecursivelyObjectNamed([Const] DOMString name); + + boolean IsADescendantOf([Const, Ref] LayoutFolderOrLayout otherLayoutFolderOrLayout); + + void Clear(); }; enum ObjectsContainersList_VariableExistence { diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp index f5f45d1324d0..e11fb4368683 100644 --- a/GDevelop.js/Bindings/Wrapper.cpp +++ b/GDevelop.js/Bindings/Wrapper.cpp @@ -84,7 +84,7 @@ #include #include #include -#include +#include #include #include #include @@ -470,6 +470,7 @@ typedef std::shared_ptr SharedPtrSerializerElement; typedef std::vector VectorUnfilledRequiredBehaviorPropertyProblem; typedef std::vector VectorObjectFolderOrObject; +typedef gd::FolderOrItem LayoutFolderOrLayout; typedef std::vector VectorScreenshot; typedef QuickCustomization::Visibility QuickCustomization_Visibility; diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index 4f40c764e896..7cc31fec0bbb 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -631,6 +631,30 @@ export class Project extends EmscriptenObject { getWholeProjectDiagnosticReport(): WholeProjectDiagnosticReport; static isNameSafe(name: string): boolean; static getSafeName(name: string): string; + getLayoutsRootFolder(): LayoutFolderOrLayout; +} + +export class LayoutFolderOrLayout extends EmscriptenObject { + constructor(); + isFolder(): boolean; + isRootFolder(): boolean; + getItem(): Layout; + getFolderName(): string; + setFolderName(name: string): void; + getChildrenCount(): number; + getChildAt(pos: number): LayoutFolderOrLayout; + getParent(): LayoutFolderOrLayout; + insertObject(ptr: Layout, position: number): void; + insertNewFolder(name: string, newPosition: number): LayoutFolderOrLayout; + getChildPosition(child: LayoutFolderOrLayout): number; + hasObjectNamed(name: string): boolean; + getObjectNamed(name: string): LayoutFolderOrLayout; + moveObjectFolderOrObjectToAnotherFolder(folderOrItem: LayoutFolderOrLayout, newParentFolder: LayoutFolderOrLayout, newPosition: number): void; + moveChild(oldIndex: number, newIndex: number): void; + removeFolderChild(childToRemove: LayoutFolderOrLayout): void; + removeRecursivelyObjectNamed(name: string): void; + isADescendantOf(otherLayoutFolderOrLayout: LayoutFolderOrLayout): boolean; + clear(): void; } export class ObjectsContainersList extends EmscriptenObject { diff --git a/GDevelop.js/types/gdlayoutfolderorlayout.js b/GDevelop.js/types/gdlayoutfolderorlayout.js new file mode 100644 index 000000000000..e7767bc30f64 --- /dev/null +++ b/GDevelop.js/types/gdlayoutfolderorlayout.js @@ -0,0 +1,25 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdLayoutFolderOrLayout { + constructor(): void; + isFolder(): boolean; + isRootFolder(): boolean; + getItem(): gdLayout; + getFolderName(): string; + setFolderName(name: string): void; + getChildrenCount(): number; + getChildAt(pos: number): gdLayoutFolderOrLayout; + getParent(): gdLayoutFolderOrLayout; + insertObject(ptr: gdLayout, position: number): void; + insertNewFolder(name: string, newPosition: number): gdLayoutFolderOrLayout; + getChildPosition(child: gdLayoutFolderOrLayout): number; + hasObjectNamed(name: string): boolean; + getObjectNamed(name: string): gdLayoutFolderOrLayout; + moveObjectFolderOrObjectToAnotherFolder(folderOrItem: gdLayoutFolderOrLayout, newParentFolder: gdLayoutFolderOrLayout, newPosition: number): void; + moveChild(oldIndex: number, newIndex: number): void; + removeFolderChild(childToRemove: gdLayoutFolderOrLayout): void; + removeRecursivelyObjectNamed(name: string): void; + isADescendantOf(otherLayoutFolderOrLayout: gdLayoutFolderOrLayout): boolean; + clear(): void; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdproject.js b/GDevelop.js/types/gdproject.js index 263933f0f35b..02475f8391d9 100644 --- a/GDevelop.js/types/gdproject.js +++ b/GDevelop.js/types/gdproject.js @@ -118,6 +118,7 @@ declare class gdProject { getWholeProjectDiagnosticReport(): gdWholeProjectDiagnosticReport; static isNameSafe(name: string): boolean; static getSafeName(name: string): string; + getLayoutsRootFolder(): gdLayoutFolderOrLayout; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/libgdevelop.js b/GDevelop.js/types/libgdevelop.js index ffc1b62b68fe..0cde864b74e2 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -83,6 +83,7 @@ declare class libGDevelop { ObjectsContainer_SourceType: Class; ObjectsContainer: Class; Project: Class; + LayoutFolderOrLayout: Class; ObjectsContainersList_VariableExistence: Class; ObjectsContainersList: Class; ProjectScopedContainers: Class; diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 48ed7957612a..970e59795b3e 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -1389,17 +1389,19 @@ const MainFrame = (props: Props) => { [openProjectManager] ); - const deleteLayout = (layout: gdLayout) => { + const deleteLayout = (layout: gdLayout, skipConfirmation: boolean = false) => { const { currentProject } = state; const { i18n } = props; if (!currentProject) return; - const answer = Window.showConfirmDialog( - i18n._( - t`Are you sure you want to remove this scene? This can't be undone.` - ) - ); - if (!answer) return; + if (!skipConfirmation) { + const answer = Window.showConfirmDialog( + i18n._( + t`Are you sure you want to remove this scene? This can't be undone.` + ) + ); + if (!answer) return; + } setState(state => ({ ...state, diff --git a/newIDE/app/src/ObjectsList/index.js b/newIDE/app/src/ObjectsList/index.js index 475b3012d65c..fe0b524adc72 100644 --- a/newIDE/app/src/ObjectsList/index.js +++ b/newIDE/app/src/ObjectsList/index.js @@ -39,7 +39,7 @@ import { getHelpLink } from '../Utils/HelpLink'; import useAlertDialog from '../UI/Alert/useAlertDialog'; import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; import ErrorBoundary from '../UI/ErrorBoundary'; -import { getInsertionParentAndPositionFromSelection } from '../Utils/ObjectFolders'; +import { getInsertionParentAndPosition } from '../Utils/FolderHelpers'; import { ObjectTreeViewItemContent, getObjectTreeViewItemId, @@ -634,7 +634,7 @@ const ObjectsList = React.forwardRef( const { folder: parentFolder, position, - } = getInsertionParentAndPositionFromSelection(selectedItem); + } = getInsertionParentAndPosition(selectedItem); object = objectsContainer.insertNewObjectInFolder( project, diff --git a/newIDE/app/src/ProjectManager/ExternalLayoutFolderTreeViewItemContent.js b/newIDE/app/src/ProjectManager/ExternalLayoutFolderTreeViewItemContent.js new file mode 100644 index 000000000000..bad63da991af --- /dev/null +++ b/newIDE/app/src/ProjectManager/ExternalLayoutFolderTreeViewItemContent.js @@ -0,0 +1,289 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; +import * as React from 'react'; +import Clipboard from '../Utils/Clipboard'; +import { SafeExtractor } from '../Utils/SafeExtractor'; +import { + serializeToJSObject, + unserializeFromJSObject, +} from '../Utils/Serializer'; +import { TreeViewItemContent, externalLayoutsRootFolderId } from '.'; +import { type HTMLDataset } from '../Utils/HTMLDataset'; +import newNameGenerator from '../Utils/NewNameGenerator'; +import { getExternalLayoutTreeViewItemId } from './ExternalLayoutTreeViewItemContent'; + +const EXTERNAL_LAYOUT_FOLDER_CLIPBOARD_KIND = 'ExternalLayoutFolder'; + +export type ExternalLayoutFolderTreeViewItemProps = {| + project: gdProject, + forceUpdate: () => void, + forceUpdateList: () => void, + editName: (itemId: string) => void, + scrollToItem: (itemId: string) => void, + onProjectItemModified: () => void, + showDeleteConfirmation: (options: any) => Promise, + expandFolders: (folderIds: string[]) => void, +|}; + +export const getExternalLayoutFolderTreeViewItemId = ( + folder: gdExternalLayoutFolderOrLayout +): string => { + return `external-layout-folder-${folder.ptr}`; +}; + +export class ExternalLayoutFolderTreeViewItemContent + implements TreeViewItemContent { + folder: gdExternalLayoutFolderOrLayout; + props: ExternalLayoutFolderTreeViewItemProps; + + constructor( + folder: gdExternalLayoutFolderOrLayout, + props: ExternalLayoutFolderTreeViewItemProps + ) { + this.folder = folder; + this.props = props; + } + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + if (itemContent.getId() === externalLayoutsRootFolderId) return true; + + let currentParent = this.folder.getParent(); + while (currentParent && !currentParent.isRootFolder()) { + if ( + getExternalLayoutFolderTreeViewItemId(currentParent) === + itemContent.getId() + ) { + return true; + } + currentParent = currentParent.getParent(); + } + return false; + } + + getRootId(): string { + return externalLayoutsRootFolderId; + } + + getName(): string | React.Node { + return this.folder.getFolderName(); + } + + getId(): string { + return getExternalLayoutFolderTreeViewItemId(this.folder); + } + + getHtmlId(index: number): ?string { + return `external-layout-folder-item-${index}`; + } + + getDataSet(): ?HTMLDataset { + return { + 'external-layout-folder': this.folder.getFolderName(), + }; + } + + getThumbnail(): ?string { + return 'res/icons_default/folder_black.svg'; + } + + onClick(): void {} + + rename(newName: string): void { + if (this.folder.getFolderName() === newName) return; + this.folder.setFolderName(newName); + this.props.onProjectItemModified(); + } + + edit(): void { + this.props.editName(this.getId()); + } + + buildMenuTemplate(i18n: I18nType, index: number) { + return [ + { + label: i18n._(t`Add external layout`), + click: () => this._addExternalLayout(i18n), + }, + { + label: i18n._(t`Add folder`), + click: () => this._addFolder(), + }, + { + type: 'separator', + }, + { + label: i18n._(t`Rename`), + click: () => this.edit(), + accelerator: 'F2', + }, + { + label: i18n._(t`Delete`), + click: () => this.delete(), + accelerator: 'Backspace', + }, + { + type: 'separator', + }, + { + label: i18n._(t`Copy`), + click: () => this.copy(), + accelerator: 'CmdOrCtrl+C', + }, + { + label: i18n._(t`Cut`), + click: () => this.cut(), + accelerator: 'CmdOrCtrl+X', + }, + { + label: i18n._(t`Paste`), + enabled: Clipboard.has(EXTERNAL_LAYOUT_FOLDER_CLIPBOARD_KIND), + click: () => this.paste(), + accelerator: 'CmdOrCtrl+V', + }, + ]; + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return null; + } + + getRightButton(i18n: I18nType) { + return null; + } + + delete(): void { + const { showDeleteConfirmation, onProjectItemModified } = this.props; + + showDeleteConfirmation({ + title: t`Remove folder`, + message: t`Are you sure you want to remove this folder? External layouts inside will be moved to the parent folder.`, + }).then(answer => { + if (!answer) return; + + const parent = this.folder.getParent(); + if (!parent) return; + + parent.removeFolderChild(this.folder); + onProjectItemModified(); + }); + } + + getIndex(): number { + const parent = this.folder.getParent(); + if (!parent) return 0; + return parent.getChildPosition(this.folder); + } + + moveAt(destinationIndex: number): void { + const originIndex = this.getIndex(); + if (destinationIndex !== originIndex) { + const parent = this.folder.getParent(); + if (parent) { + parent.moveChild( + originIndex, + // When moving the item down, it must not be counted. + destinationIndex + (destinationIndex <= originIndex ? 0 : -1) + ); + this.props.onProjectItemModified(); + } + } + } + + copy(): void { + Clipboard.set(EXTERNAL_LAYOUT_FOLDER_CLIPBOARD_KIND, { + folder: serializeToJSObject(this.folder), + name: this.folder.getFolderName(), + }); + } + + cut(): void { + this.copy(); + this.delete(); + } + + paste(): void { + if (!Clipboard.has(EXTERNAL_LAYOUT_FOLDER_CLIPBOARD_KIND)) return; + + const clipboardContent = Clipboard.get( + EXTERNAL_LAYOUT_FOLDER_CLIPBOARD_KIND + ); + const copiedFolder = SafeExtractor.extractObjectProperty( + clipboardContent, + 'folder' + ); + const name = SafeExtractor.extractStringProperty(clipboardContent, 'name'); + if (!name || !copiedFolder) return; + + const newName = newNameGenerator(name, name => this._hasFolderNamed(name)); + + const newFolder = this.folder.insertNewFolder(newName, 0); + unserializeFromJSObject(newFolder, copiedFolder); + // Unserialization has overwritten the name. + newFolder.setFolderName(newName); + + this.props.onProjectItemModified(); + this.props.editName(getExternalLayoutFolderTreeViewItemId(newFolder)); + } + + _addExternalLayout(i18n: I18nType): void { + const { + project, + onProjectItemModified, + editName, + scrollToItem, + } = this.props; + + const newName = newNameGenerator( + i18n._(t`Untitled external layout`), + name => project.hasExternalLayoutNamed(name) + ); + + const newExternalLayout = project.insertNewExternalLayout( + newName, + project.getExternalLayoutsCount() + ); + newExternalLayout.setName(newName); + + this.folder.insertItem(newExternalLayout, 0); + + onProjectItemModified(); + + this.props.expandFolders([this.getId()]); + + setTimeout(() => { + scrollToItem(getExternalLayoutTreeViewItemId(newExternalLayout)); + }, 100); + } + + _addFolder(): void { + const { onProjectItemModified, editName, expandFolders } = this.props; + + const newFolderName = newNameGenerator('NewFolder', name => + this._hasFolderNamed(name) + ); + + const newFolder = this.folder.insertNewFolder(newFolderName, 0); + + onProjectItemModified(); + expandFolders([this.getId()]); + editName(getExternalLayoutFolderTreeViewItemId(newFolder)); + } + + _hasFolderNamed(name: string): boolean { + const childrenCount = this.folder.getChildrenCount + ? this.folder.getChildrenCount() + : 0; + + for (let i = 0; i < childrenCount; i++) { + const child = this.folder.getChildAt(i); + if (child && child.isFolder && child.isFolder()) { + if (child.getFolderName && child.getFolderName() === name) { + return true; + } + } + } + + return false; + } +} diff --git a/newIDE/app/src/ProjectManager/SceneFolderTreeViewItemContent.js b/newIDE/app/src/ProjectManager/SceneFolderTreeViewItemContent.js new file mode 100644 index 000000000000..72a0e6c1ba8e --- /dev/null +++ b/newIDE/app/src/ProjectManager/SceneFolderTreeViewItemContent.js @@ -0,0 +1,427 @@ +// @flow +import { type I18n as I18nType } from '@lingui/core'; +import { t } from '@lingui/macro'; +import * as React from 'react'; +import Clipboard from '../Utils/Clipboard'; +import { SafeExtractor } from '../Utils/SafeExtractor'; +import { + serializeToJSObject, + unserializeFromJSObject, +} from '../Utils/Serializer'; +import { TreeViewItemContent, scenesRootFolderId } from '.'; +import { type HTMLDataset } from '../Utils/HTMLDataset'; +import newNameGenerator from '../Utils/NewNameGenerator'; +import { getSceneTreeViewItemId } from './SceneTreeViewItemContent'; +import { addDefaultLightToAllLayers } from '../ProjectCreation/CreateProject'; +import { + buildMoveToFolderSubmenu, + createNewFolderAndMoveItem, + hasFolderNamed, + collectFoldersAndPaths, +} from './SceneTreeViewHelpers'; + +const SCENE_FOLDER_CLIPBOARD_KIND = 'SceneFolder'; + +export type SceneFolderTreeViewItemProps = {| + project: gdProject, + forceUpdate: () => void, + forceUpdateList: () => void, + editName: (itemId: string) => void, + scrollToItem: (itemId: string) => void, + onProjectItemModified: () => void, + showDeleteConfirmation: (options: any) => Promise, + expandFolders: (folderIds: string[]) => void, + onDeleteLayout: (layout: gdLayout, skipConfirmation?: boolean) => void, +|}; + +export const getSceneFolderTreeViewItemId = ( + folder: gdLayoutFolderOrLayout +): string => { + return `scene-folder-${folder.ptr}`; +}; + +export class SceneFolderTreeViewItemContent implements TreeViewItemContent { + folder: gdLayoutFolderOrLayout; + props: SceneFolderTreeViewItemProps; + + constructor( + folder: gdLayoutFolderOrLayout, + props: SceneFolderTreeViewItemProps + ) { + this.folder = folder; + this.props = props; + } + + isDescendantOf(itemContent: TreeViewItemContent): boolean { + if (itemContent.getId() === scenesRootFolderId) return true; + + let currentParent = this.folder.getParent(); + while (currentParent && !currentParent.isRootFolder()) { + if (getSceneFolderTreeViewItemId(currentParent) === itemContent.getId()) { + return true; + } + currentParent = currentParent.getParent(); + } + return false; + } + + getRootId(): string { + return scenesRootFolderId; + } + + getName(): string | React.Node { + return this.folder.getFolderName(); + } + + getId(): string { + return getSceneFolderTreeViewItemId(this.folder); + } + + getHtmlId(index: number): ?string { + return `scene-folder-item-${index}`; + } + + getDataSet(): ?HTMLDataset { + return { + 'scene-folder': this.folder.getFolderName(), + }; + } + + getFolder(): gdLayoutFolderOrLayout { + return this.folder; + } + + getThumbnail(): ?string { + return 'FOLDER'; + } + + onClick(): void {} + + rename(newName: string): void { + if (this.folder.getFolderName() === newName) return; + this.folder.setFolderName(newName); + this.props.onProjectItemModified(); + } + + edit(): void { + this.props.editName(this.getId()); + } + + buildMenuTemplate(i18n: I18nType, index: number) { + const { project } = this.props; + const layoutsRootFolder = project.getLayoutsRootFolder(); + + const foldersAndPaths = layoutsRootFolder + ? this._collectFoldersAndPaths(layoutsRootFolder) + : []; + + const currentParent = this.folder.getParent(); + + return [ + { + label: i18n._(t`Rename`), + click: () => this.edit(), + accelerator: 'F2', + }, + { + label: i18n._(t`Delete`), + click: () => this.delete(), + accelerator: 'Backspace', + }, + { + label: i18n._(t`Move to folder`), + submenu: buildMoveToFolderSubmenu( + i18n, + project, + currentParent, + this.folder, + targetFolder => { + if (currentParent) { + currentParent.moveObjectFolderOrObjectToAnotherFolder( + this.folder, + targetFolder, + 0 + ); + this.props.onProjectItemModified(); + } + }, + () => this._createNewFolderAndMove(i18n) + ), + }, + { + type: 'separator', + }, + { + label: i18n._(t`Add a scene`), + click: () => this._addScene(i18n), + }, + { + label: i18n._(t`Add a folder`), + click: () => this._addFolder(), + }, + ]; + } + + renderRightComponent(i18n: I18nType): ?React.Node { + return null; + } + + getRightButton(i18n: I18nType) { + return null; + } + + delete(): void { + const { showDeleteConfirmation, onProjectItemModified } = this.props; + + const contentCount = this._countFolderContents(); + const hasStartScene = this._containsStartScene(); + + let message; + let confirmLabel = t`Delete`; + + if (contentCount.scenes === 0 && contentCount.folders === 0) { + message = t`Are you sure you want to remove this empty folder?`; + } else { + message = t`⚠️ This will permanently delete: + - ${contentCount.scenes} scene(s) + - ${contentCount.folders} subfolder(s) + + This action cannot be undone.`; + confirmLabel = t`Delete permanently`; + + if (hasStartScene) { + message += t` + + ⚠️ Warning: This includes your start scene. Another scene will be set as the new start scene.`; + } + } + + showDeleteConfirmation({ + title: t`Remove folder`, + message: message, + confirmButtonLabel: confirmLabel, + }).then(answer => { + if (!answer) return; + + this._deleteRecursively(this.folder, true); + + const parent = this.folder.getParent(); + if (parent) { + parent.removeFolderChild(this.folder); + } + + onProjectItemModified(); + }); + } + + _deleteRecursively( + folder: gdLayoutFolderOrLayout, + skipConfirmation: boolean = false + ): void { + const childrenToDelete = []; + for (let i = 0; i < folder.getChildrenCount(); i++) { + childrenToDelete.push(folder.getChildAt(i)); + } + childrenToDelete.forEach(child => { + if (child.isFolder()) { + this._deleteRecursively(child, skipConfirmation); + folder.removeFolderChild(child); + } else { + const layout = child.getItem(); + if (layout) { + const layoutName = layout.getName(); + this.props.onDeleteLayout(layout, skipConfirmation); + folder.removeRecursivelyObjectNamed(layoutName); + } + } + }); + } + + _countFolderContents(): { scenes: number, folders: number } { + let scenes = 0; + let folders = 0; + + const countRecursive = (folder: gdLayoutFolderOrLayout) => { + for (let i = 0; i < folder.getChildrenCount(); i++) { + const child = folder.getChildAt(i); + if (child.isFolder()) { + folders++; + countRecursive(child); + } else { + scenes++; + } + } + }; + + countRecursive(this.folder); + return { scenes, folders }; + } + + _containsStartScene(): boolean { + const { project } = this.props; + const firstLayout = project.getFirstLayout(); + + const checkRecursive = (folder: gdLayoutFolderOrLayout): boolean => { + for (let i = 0; i < folder.getChildrenCount(); i++) { + const child = folder.getChildAt(i); + if (child.isFolder()) { + if (checkRecursive(child)) return true; + } else { + const layout = child.getItem(); + if (layout && layout.getName() === firstLayout) { + return true; + } + } + } + return false; + }; + + return checkRecursive(this.folder); + } + + getIndex(): number { + const parent = this.folder.getParent(); + if (!parent) return 0; + return parent.getChildPosition(this.folder); + } + + moveAt( + destinationIndex: number, + targetFolder?: gdLayoutFolderOrLayout + ): void { + const originIndex = this.getIndex(); + const currentParent = this.folder.getParent(); + if (!currentParent) return; + + const destinationFolder = targetFolder || currentParent; + + if (destinationFolder === currentParent) { + if (destinationIndex !== originIndex) { + currentParent.moveChild(originIndex, destinationIndex); + this.props.onProjectItemModified(); + } + } else { + currentParent.moveObjectFolderOrObjectToAnotherFolder( + this.folder, + destinationFolder, + destinationIndex + ); + this.props.onProjectItemModified(); + } + } + + copy(): void { + Clipboard.set(SCENE_FOLDER_CLIPBOARD_KIND, { + folder: serializeToJSObject(this.folder), + name: this.folder.getFolderName(), + }); + } + + cut(): void { + this.copy(); + this.delete(); + } + + paste(): void { + if (!Clipboard.has(SCENE_FOLDER_CLIPBOARD_KIND)) return; + + const clipboardContent = Clipboard.get(SCENE_FOLDER_CLIPBOARD_KIND); + const copiedFolder = SafeExtractor.extractObjectProperty( + clipboardContent, + 'folder' + ); + const name = SafeExtractor.extractStringProperty(clipboardContent, 'name'); + if (!name || !copiedFolder) return; + + const newName = newNameGenerator(name, name => this._hasFolderNamed(name)); + + const newFolder = this.folder.insertNewFolder(newName, 0); + unserializeFromJSObject(newFolder, copiedFolder); + newFolder.setFolderName(newName); + + this.props.onProjectItemModified(); + this.props.editName(getSceneFolderTreeViewItemId(newFolder)); + } + + _addScene(i18n: I18nType): void { + const { + project, + onProjectItemModified, + editName, + scrollToItem, + } = this.props; + + const newName = newNameGenerator(i18n._(t`Untitled scene`), name => + project.hasLayoutNamed(name) + ); + + const newScene = project.insertNewLayout( + newName, + project.getLayoutsCount() + ); + newScene.setName(newName); + newScene.updateBehaviorsSharedData(project); + addDefaultLightToAllLayers(newScene); + + const layoutsRootFolder = project.getLayoutsRootFolder(); + if (layoutsRootFolder) { + const sceneInRoot = layoutsRootFolder.getObjectNamed(newName); + if (sceneInRoot) { + layoutsRootFolder.moveObjectFolderOrObjectToAnotherFolder( + sceneInRoot, + this.folder, + 0 + ); + } + } + + onProjectItemModified(); + + this.props.expandFolders([this.getId()]); + + setTimeout(() => { + scrollToItem(getSceneTreeViewItemId(newScene)); + }, 100); + } + + _addFolder(): void { + const { onProjectItemModified, editName, expandFolders } = this.props; + + const newFolderName = newNameGenerator('NewFolder', name => + this._hasFolderNamed(name) + ); + + const newFolder = this.folder.insertNewFolder(newFolderName, 0); + + onProjectItemModified(); + expandFolders([this.getId()]); + editName(getSceneFolderTreeViewItemId(newFolder)); + } + + _hasFolderNamed(name: string): boolean { + const childrenCount = this.folder.getChildrenCount + ? this.folder.getChildrenCount() + : 0; + + for (let i = 0; i < childrenCount; i++) { + const child = this.folder.getChildAt(i); + if (child && child.isFolder && child.isFolder()) { + if (child.getFolderName && child.getFolderName() === name) { + return true; + } + } + } + + return false; + } + + _createNewFolderAndMove(i18n: I18nType): void { + createNewFolderAndMoveItem( + this.props.project, + this.folder, + this.props.onProjectItemModified, + this.props.expandFolders, + this.props.editName + ); + } +} diff --git a/newIDE/app/src/ProjectManager/SceneTreeViewHelpers.js b/newIDE/app/src/ProjectManager/SceneTreeViewHelpers.js new file mode 100644 index 000000000000..77fbc23a6a6a --- /dev/null +++ b/newIDE/app/src/ProjectManager/SceneTreeViewHelpers.js @@ -0,0 +1,126 @@ +import { t } from '@lingui/macro'; +import newNameGenerator from '../Utils/NewNameGenerator'; +import { getSceneFolderTreeViewItemId } from './SceneFolderTreeViewItemContent'; + +export const collectFoldersAndPaths = ( + folder: gdLayoutFolderOrLayout, + parentPath: string = '', + result: Array<{ folder: gdLayoutFolderOrLayout, path: string }> = [] +): Array<{ folder: gdLayoutFolderOrLayout, path: string }> => { + for (let i = 0; i < folder.getChildrenCount(); i++) { + const child = folder.getChildAt(i); + if (child.isFolder()) { + const folderName = child.getFolderName(); + const path = parentPath ? `${parentPath}/${folderName}` : folderName; + result.push({ folder: child, path }); + collectFoldersAndPaths(child, path, result); + } + } + return result; +}; + +export const hasFolderNamed = ( + parentFolder: gdLayoutFolderOrLayout, + name: string +): boolean => { + const childrenCount = parentFolder.getChildrenCount + ? parentFolder.getChildrenCount() + : 0; + + for (let i = 0; i < childrenCount; i++) { + const child = parentFolder.getChildAt(i); + if (child && child.isFolder && child.isFolder()) { + if (child.getFolderName && child.getFolderName() === name) { + return true; + } + } + } + + return false; +}; + +export const buildMoveToFolderSubmenu = ( + i18n: I18nType, + project: gdProject, + currentParent: ?any, + itemToMove: any, + onMove: (targetFolder: any) => void, + onCreateNewFolder: () => void +) => { + const layoutsRootFolder = project.getLayoutsRootFolder(); + const foldersAndPaths = layoutsRootFolder + ? collectFoldersAndPaths(layoutsRootFolder) + : []; + + return [ + { + label: i18n._(t`Root`), + enabled: layoutsRootFolder && currentParent !== layoutsRootFolder, + click: () => { + if (layoutsRootFolder) { + onMove(layoutsRootFolder); + } + }, + }, + ...foldersAndPaths + .filter(({ folder }) => { + if (itemToMove.isFolder && itemToMove.isFolder()) { + return folder !== itemToMove && !folder.isADescendantOf(itemToMove); + } + return true; + }) + .map(({ folder, path }) => ({ + label: path, + enabled: folder !== currentParent, + click: () => onMove(folder), + })), + { type: 'separator' }, + { + label: i18n._(t`Create new folder...`), + click: onCreateNewFolder, + }, + ]; +}; + +export const createNewFolderAndMoveItem = ( + project: gdProject, + itemToMove: any, + onProjectItemModified: () => void, + expandFolders: ?(folderIds: string[]) => void, + editName: ?(itemId: string) => void +): void => { + const layoutsRootFolder = project.getLayoutsRootFolder(); + if (!layoutsRootFolder) return; + + const newFolderName = newNameGenerator('NewFolder', name => { + for (let i = 0; i < layoutsRootFolder.getChildrenCount(); i++) { + const child = layoutsRootFolder.getChildAt(i); + if (child.isFolder() && child.getFolderName() === name) { + return true; + } + } + return false; + }); + + const newFolder = layoutsRootFolder.insertNewFolder(newFolderName, 0); + + const currentParent = itemToMove.getParent(); + if (currentParent) { + currentParent.moveObjectFolderOrObjectToAnotherFolder( + itemToMove, + newFolder, + 0 + ); + } + + onProjectItemModified(); + + if (expandFolders) { + expandFolders([getSceneFolderTreeViewItemId(newFolder)]); + } + if (editName) { + setTimeout(() => { + editName(getSceneFolderTreeViewItemId(newFolder)); + }, 100); + } +}; diff --git a/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js b/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js index 45888235b8b8..240204f2d517 100644 --- a/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js +++ b/newIDE/app/src/ProjectManager/SceneTreeViewItemContent.js @@ -14,6 +14,12 @@ import { TreeViewItemContent, type TreeItemProps, scenesRootFolderId } from '.'; import Tooltip from '@material-ui/core/Tooltip'; import Flag from '@material-ui/icons/Flag'; import { type HTMLDataset } from '../Utils/HTMLDataset'; +import { getSceneFolderTreeViewItemId } from './SceneFolderTreeViewItemContent'; +import { + buildMoveToFolderSubmenu, + createNewFolderAndMoveItem, + collectFoldersAndPaths, +} from './SceneTreeViewHelpers'; const SCENE_CLIPBOARD_KIND = 'Layout'; @@ -23,7 +29,7 @@ const styles = { export type SceneTreeViewItemCallbacks = {| onSceneAdded: () => void, - onDeleteLayout: gdLayout => void, + onDeleteLayout: (gdLayout, skipConfirmation?: boolean) => void, onRenameLayout: (string, string) => void, onOpenLayout: ( name: string, @@ -110,6 +116,7 @@ export class SceneTreeViewItemContent implements TreeViewItemContent { return; } this.props.onRenameLayout(oldName, newName); + this.props.forceUpdateList(); } edit(): void { @@ -117,6 +124,21 @@ export class SceneTreeViewItemContent implements TreeViewItemContent { } buildMenuTemplate(i18n: I18nType, index: number) { + const { project } = this.props; + const layoutsRootFolder = project.getLayoutsRootFolder(); + + const foldersAndPaths = layoutsRootFolder + ? collectFoldersAndPaths(layoutsRootFolder) + : []; + + const currentLayoutFolderOrLayout = layoutsRootFolder + ? this._findLayoutFolderOrLayoutForScene( + layoutsRootFolder, + this.scene.ptr + ) + : null; + const currentParent = currentLayoutFolderOrLayout?.getParent(); + return [ { label: i18n._(t`Open scene editor`), @@ -159,6 +181,29 @@ export class SceneTreeViewItemContent implements TreeViewItemContent { { type: 'separator', }, + { + label: i18n._(t`Move to folder`), + submenu: buildMoveToFolderSubmenu( + i18n, + project, + currentParent, + currentLayoutFolderOrLayout, + targetFolder => { + if (currentLayoutFolderOrLayout && currentParent) { + currentParent.moveObjectFolderOrObjectToAnotherFolder( + currentLayoutFolderOrLayout, + targetFolder, + 0 + ); + this._onProjectItemModified(); + } + }, + () => this.createNewFolderAndMoveItem(i18n) + ), + }, + { + type: 'separator', + }, { label: i18n._(t`Rename`), click: () => this.edit(), @@ -221,24 +266,132 @@ export class SceneTreeViewItemContent implements TreeViewItemContent { return icons.length > 0 ? icons : null; } - delete(): void { - this.props.onDeleteLayout(this.scene); + delete(skipConfirmation: boolean = false): void { + const { project } = this.props; + const layoutsRootFolder = project.getLayoutsRootFolder(); + + this.props.onDeleteLayout(this.scene, skipConfirmation); + + if (layoutsRootFolder) { + const sceneName = this.scene.getName(); + layoutsRootFolder.removeRecursivelyObjectNamed(sceneName); + } + + this._onProjectItemModified(); + } + + _findParentFolder(folder: any, scenePtr: number): ?any { + for (let i = 0; i < folder.getChildrenCount(); i++) { + const child = folder.getChildAt(i); + + if (child.isFolder()) { + const found = this._findParentFolder(child, scenePtr); + if (found) return found; + } else { + const layout = child.getItem(); + if (layout && layout.ptr === scenePtr) { + return folder; + } + } + } + return null; + } + + _getIndexInParent(parentFolder: any, scenePtr: number): number { + for (let i = 0; i < parentFolder.getChildrenCount(); i++) { + const child = parentFolder.getChildAt(i); + if (!child.isFolder()) { + const layout = child.getItem(); + if (layout && layout.ptr === scenePtr) { + return i; + } + } + } + return 0; } getIndex(): number { - return this.props.project.getLayoutPosition(this.scene.getName()); + const { project } = this.props; + const layoutsRootFolder = project.getLayoutsRootFolder(); + if (!layoutsRootFolder) return 0; + + const parentFolder = this._findParentFolder( + layoutsRootFolder, + this.scene.ptr + ); + if (!parentFolder) return 0; + + return this._getIndexInParent(parentFolder, this.scene.ptr); + } + + _findLayoutFolderOrLayoutForScene(folder: any, scenePtr: number): ?any { + for (let i = 0; i < folder.getChildrenCount(); i++) { + const child = folder.getChildAt(i); + + if (child.isFolder()) { + const found = this._findLayoutFolderOrLayoutForScene(child, scenePtr); + if (found) return found; + } else { + const layout = child.getItem(); + if (layout && layout.ptr === scenePtr) { + return child; + } + } + } + return null; + } + + getLayoutFolderOrLayout(): gdLayoutFolderOrLayout | null { + const { project } = this.props; + const layoutsRootFolder = project.getLayoutsRootFolder(); + if (!layoutsRootFolder) return null; + + return this._findLayoutFolderOrLayoutForScene( + layoutsRootFolder, + this.scene.ptr + ); } - moveAt(destinationIndex: number): void { - const originIndex = this.getIndex(); - if (destinationIndex !== originIndex) { - this.props.project.moveLayout( + moveAt( + destinationIndex: number, + targetFolder?: gdLayoutFolderOrLayout + ): void { + const { project } = this.props; + const layoutsRootFolder = project.getLayoutsRootFolder(); + if (!layoutsRootFolder) return; + + const sceneLayoutFolderOrLayout = this._findLayoutFolderOrLayoutForScene( + layoutsRootFolder, + this.scene.ptr + ); + if (!sceneLayoutFolderOrLayout) return; + + const currentParentFolder = sceneLayoutFolderOrLayout.getParent(); + if (!currentParentFolder) return; + + const originIndex = this._getIndexInParent( + currentParentFolder, + this.scene.ptr + ); + + const destinationFolder = targetFolder || currentParentFolder; + + if (destinationFolder === currentParentFolder) { + if (destinationIndex === originIndex) return; + currentParentFolder.moveChild( originIndex, // When moving the item down, it must not be counted. - destinationIndex + (destinationIndex <= originIndex ? 0 : -1) + destinationIndex + ); + } else { + currentParentFolder.moveObjectFolderOrObjectToAnotherFolder( + sceneLayoutFolderOrLayout, + destinationFolder, + destinationIndex ); - this._onProjectItemModified(); } + + this._onProjectItemModified(); } copy(): void { diff --git a/newIDE/app/src/ProjectManager/index.js b/newIDE/app/src/ProjectManager/index.js index c8bd4b40d86f..b08e61b58f43 100644 --- a/newIDE/app/src/ProjectManager/index.js +++ b/newIDE/app/src/ProjectManager/index.js @@ -41,12 +41,6 @@ import InAppTutorialContext from '../InAppTutorial/InAppTutorialContext'; import { mapFor } from '../Utils/MapFor'; import KeyboardShortcuts from '../UI/KeyboardShortcuts'; import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; -import { - SceneTreeViewItemContent, - getSceneTreeViewItemId, - type SceneTreeViewItemProps, - type SceneTreeViewItemCallbacks, -} from './SceneTreeViewItemContent'; import { ExtensionTreeViewItemContent, getExtensionTreeViewItemId, @@ -85,6 +79,28 @@ import optionalRequire from '../Utils/OptionalRequire'; import { useShouldAutofocusInput } from '../UI/Responsive/ScreenTypeMeasurer'; import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope'; +import { + ExternalLayoutFolderTreeViewItemContent, + getExternalLayoutFolderTreeViewItemId, + type ExternalLayoutFolderTreeViewItemProps, +} from './ExternalLayoutFolderTreeViewItemContent'; +import { + SceneTreeViewItemContent, + getSceneTreeViewItemId, +} from './SceneTreeViewItemContent'; +import { + SceneFolderTreeViewItemContent, + getSceneFolderTreeViewItemId, +} from '../ProjectManager/SceneFolderTreeViewItemContent'; +import { + isFolder, + getItem, + forEachChild, + hasFolderNamed, + insertNewFolder, + getChildrenCount, +} from '../Utils/FolderHelpers'; + const electron = optionalRequire('electron'); export const getProjectManagerItemId = (identifier: string) => @@ -116,11 +132,17 @@ const styles = { flexDirection: 'column', padding: '0 8px 8px 8px', }, - autoSizerContainer: { flex: 1 }, - autoSizer: { width: '100%' }, + autoSizerContainer: { + flex: 1, + }, + autoSizer: { + width: '100%', + }, }; const extensionItemReactDndType = 'GD_EXTENSION_ITEM'; +const sceneItemReactDndType = 'GD_SCENE'; +const projectItemReactDndType = 'GD_PROJECT_ITEM'; export interface TreeViewItemContent { getName(): string | React.Node; @@ -193,7 +215,6 @@ class PlaceHolderTreeViewItem implements TreeViewItem { class LabelTreeViewItemContent implements TreeViewItemContent { id: string; label: string | React.Node; - dataSet: { [string]: string }; buildMenuTemplateFunction: ( i18n: I18nType, index: number @@ -277,7 +298,7 @@ class LabelTreeViewItemContent implements TreeViewItemContent { } getRootId(): string { - return ''; + return scenesRootFolderId; } } @@ -316,10 +337,6 @@ class ActionTreeViewItemContent implements TreeViewItemContent { return null; } - getEventsFunctionsContainer(): ?gdEventsFunctionsContainer { - return null; - } - getHtmlId(index: number): ?string { return this.id; } @@ -371,6 +388,198 @@ class ActionTreeViewItemContent implements TreeViewItemContent { } } +class FolderTreeViewItem implements TreeViewItem { + content: TreeViewItemContent; + getChildrenFunc: (i18n: I18nType) => ?Array; + + constructor( + content: TreeViewItemContent, + getChildrenFunc: (i18n: I18nType) => ?Array + ) { + this.content = content; + this.getChildrenFunc = getChildrenFunc; + } + + getChildren(i18n: I18nType): ?Array { + return this.getChildrenFunc(i18n); + } +} + +const buildSceneFolderTree = ( + folder: gdLayoutFolderOrLayout, + props: any +): TreeViewItem => { + return new FolderTreeViewItem( + new SceneFolderTreeViewItemContent(folder, props), + (i18n: I18nType) => { + const children = []; + + forEachChild(folder, child => { + if (isFolder(child)) { + children.push(buildSceneFolderTree(child, props)); + } else { + const layout = getItem(child); + if (layout) { + children.push( + new LeafTreeViewItem(new SceneTreeViewItemContent(layout, props)) + ); + } + } + }); + + return children; + } + ); +}; + +export function buildScenesTreeItems( + project: gdProject, + i18n: I18nType, + props: any, + rootId?: string +): Array { + const layoutsRootFolder = project.getLayoutsRootFolder(); + + const { addNewScene, addNewFolder } = props; + const usedRootId = rootId || scenesRootFolderId; + + const scenesEmptyPlaceholderId = 'scenes-empty-placeholder'; + + class LabelTreeViewItemContentWithMenu implements TreeViewItemContent { + id: string; + label: string | React.Node; + rightButton: ?MenuButton; + buildMenuTemplateFunction: ( + i18n: I18nType, + index: number + ) => Array; + onAddSceneCallback: ?() => void; + onAddFolderCallback: ?() => void; + + constructor( + id: string, + label: string | React.Node, + rightButton?: MenuButton, + onAddScene?: () => void, + onAddFolder?: () => void + ) { + this.id = id; + this.label = label; + this.rightButton = rightButton; + this.onAddSceneCallback = onAddScene; + this.onAddFolderCallback = onAddFolder; + + this.buildMenuTemplateFunction = (i18n: I18nType, index: number) => { + const menuItems = []; + + if (this.id === usedRootId) { + if (this.onAddSceneCallback) { + menuItems.push({ + label: i18n._(t`Add a scene`), + click: this.onAddSceneCallback, + }); + } + if (this.onAddFolderCallback) { + menuItems.push({ + label: i18n._(t`Add a folder`), + click: this.onAddFolderCallback, + }); + } + } + + return menuItems; + }; + } + + getName(): string | React.Node { + return this.label; + } + getId(): string { + return this.id; + } + getRightButton(i18n: I18nType): ?MenuButton { + return this.rightButton; + } + getHtmlId(index: number): ?string { + return this.id; + } + getDataSet(): ?HTMLDataset { + return null; + } + getThumbnail(): ?string { + return null; + } + onClick(): void {} + buildMenuTemplate(i18n: I18nType, index: number): Array { + return this.buildMenuTemplateFunction(i18n, index); + } + renderRightComponent(i18n: I18nType): ?React.Node { + return null; + } + rename(newName: string): void {} + edit(): void {} + delete(): void {} + copy(): void {} + paste(): void {} + cut(): void {} + getIndex(): number { + return 0; + } + moveAt(destinationIndex: number): void {} + isDescendantOf(itemContent: TreeViewItemContent): boolean { + return false; + } + getRootId(): string { + return ''; + } + } + + return [ + { + isRoot: true, + content: new LabelTreeViewItemContentWithMenu( + usedRootId, + i18n._(t`Scenes`), + { + icon: , + label: i18n._(t`Add`), + click: addNewScene, + id: 'add-new-scene-button', + }, + addNewScene, + addNewFolder + ), + getChildren: (i18n: I18nType) => { + if (!layoutsRootFolder || project.getLayoutsCount() === 0) { + return [ + new PlaceHolderTreeViewItem( + scenesEmptyPlaceholderId, + i18n._(t`Start by adding a new scene.`) + ), + ]; + } + + const children = []; + forEachChild(layoutsRootFolder, child => { + if (isFolder(child)) { + children.push(buildSceneFolderTree(child, props)); + } else { + const layout = getItem(child); + if (layout) { + children.push( + new LeafTreeViewItem( + new SceneTreeViewItemContent(layout, props) + ) + ); + } + } + }); + return children; + }, + }, + ]; +} + const getTreeViewItemName = (item: TreeViewItem) => item.content.getName(); const getTreeViewItemId = (item: TreeViewItem) => item.content.getId(); const getTreeViewItemHtmlId = (item: TreeViewItem, index: number) => @@ -441,6 +650,43 @@ type Props = {| gamesList: GamesList, |}; +const buildExternalLayoutFolderTree = ( + folder: gdExternalLayoutFolderOrLayout, + props: ExternalLayoutTreeViewItemProps & ExternalLayoutFolderTreeViewItemProps +): TreeViewItem => { + return new FolderTreeViewItem( + new ExternalLayoutFolderTreeViewItemContent(folder, props), + (i18n: I18nType) => { + const children = []; + + const childrenCount = folder.getChildrenCount + ? folder.getChildrenCount() + : 0; + + for (let i = 0; i < childrenCount; i++) { + const child = folder.getChildAt(i); + + if (!child) continue; + + if (child.isFolder && child.isFolder()) { + children.push(buildExternalLayoutFolderTree(child, props)); + } else { + const externalLayout = child.getItem(); + if (externalLayout) { + children.push( + new LeafTreeViewItem( + new ExternalLayoutTreeViewItemContent(externalLayout, props) + ) + ); + } + } + } + + return children; + } + ); +}; + const ProjectManager = React.forwardRef( ( { @@ -481,6 +727,9 @@ const ProjectManager = React.forwardRef( const [selectedItems, setSelectedItems] = React.useState< Array >([]); + const [selectedScenes, setSelectedScenes] = React.useState>( + [] + ); const unsavedChanges = React.useContext(UnsavedChangesContext); const { triggerUnsavedChanges } = unsavedChanges; const preferences = React.useContext(PreferencesContext); @@ -494,6 +743,7 @@ const ProjectManager = React.forwardRef( const { showDeleteConfirmation } = useAlertDialog(); const { fetchGames } = gamesList; const { navigateToRoute } = React.useContext(RouterContext); + const [treeRenderKey, setTreeRenderKey] = React.useState(0); const forceUpdateList = React.useCallback( () => { @@ -643,40 +893,6 @@ const ProjectManager = React.forwardRef( [isMobile] ); - const addNewScene = React.useCallback( - (index: number, i18n: I18nType) => { - if (!project) return; - - const newName = newNameGenerator(i18n._(t`Untitled scene`), name => - project.hasLayoutNamed(name) - ); - const newScene = project.insertNewLayout(newName, index + 1); - newScene.setName(newName); - newScene.updateBehaviorsSharedData(project); - addDefaultLightToAllLayers(newScene); - - onSceneAdded(); - - onProjectItemModified(); - - const sceneItemId = getSceneTreeViewItemId(newScene); - if (treeViewRef.current) { - treeViewRef.current.openItems([sceneItemId, scenesRootFolderId]); - } - // Scroll to the new behavior. - // Ideally, we'd wait for the list to be updated to scroll, but - // to simplify the code, we just wait a few ms for a new render - // to be done. - setTimeout(() => { - scrollToItem(sceneItemId); - }, 100); // A few ms is enough for a new render to be done. - - // We focus it so the user can edit the name directly. - editName(sceneItemId); - }, - [project, onProjectItemModified, editName, scrollToItem, onSceneAdded] - ); - const onCreateNewExtension = React.useCallback( (project: gdProject, i18n: I18nType) => { const newName = newNameGenerator(i18n._(t`UntitledExtension`), name => @@ -835,6 +1051,12 @@ const ProjectManager = React.forwardRef( [forceUpdate, forceUpdateList, triggerUnsavedChanges] ); + const expandFolders = React.useCallback((folderIds: string[]) => { + if (treeViewRef.current) { + treeViewRef.current.openItems(folderIds); + } + }, []); + // Initialize keyboard shortcuts as empty. // onDelete callback is set outside because it deletes the selected // item (that is a props). As it is stored in a ref, the keyboard shortcut @@ -877,46 +1099,6 @@ const ProjectManager = React.forwardRef( [editName, selectedItems] ); - const sceneTreeViewItemProps = React.useMemo( - () => - project - ? { - project, - unsavedChanges, - preferences, - gdevelopTheme, - forceUpdate, - forceUpdateList, - showDeleteConfirmation, - editName, - scrollToItem, - onSceneAdded, - onDeleteLayout, - onRenameLayout, - onOpenLayout, - onOpenLayoutProperties, - onOpenLayoutVariables, - } - : null, - [ - project, - unsavedChanges, - preferences, - gdevelopTheme, - forceUpdate, - forceUpdateList, - showDeleteConfirmation, - editName, - scrollToItem, - onSceneAdded, - onDeleteLayout, - onRenameLayout, - onOpenLayout, - onOpenLayoutProperties, - onOpenLayoutVariables, - ] - ); - const extensionTreeViewItemProps = React.useMemo( () => project @@ -1025,20 +1207,169 @@ const ProjectManager = React.forwardRef( ] ); + const externalLayoutFolderTreeViewItemProps = React.useMemo( + () => ({ + project, + forceUpdate, + forceUpdateList, + editName, + scrollToItem, + onProjectItemModified, + showDeleteConfirmation, + expandFolders, + }), + [ + project, + forceUpdate, + forceUpdateList, + editName, + scrollToItem, + onProjectItemModified, + showDeleteConfirmation, + expandFolders, + ] + ); + + const sceneTreeViewItemProps = React.useMemo( + () => ({ + project, + unsavedChanges, + preferences, + gdevelopTheme, + forceUpdate, + forceUpdateList, + showDeleteConfirmation, + editName, + scrollToItem, + onSceneAdded, + onDeleteLayout, + onRenameLayout, + onOpenLayout: name => { + onOpenLayout(name); + }, + onOpenLayoutProperties, + onOpenLayoutVariables, + onProjectItemModified, + expandFolders, + }), + [ + project, + unsavedChanges, + preferences, + gdevelopTheme, + forceUpdate, + forceUpdateList, + showDeleteConfirmation, + editName, + scrollToItem, + onSceneAdded, + onDeleteLayout, + onRenameLayout, + onOpenLayout, + onOpenLayoutProperties, + onOpenLayoutVariables, + onProjectItemModified, + expandFolders, + ] + ); + + const combinedExternalLayoutProps = React.useMemo( + () => + externalLayoutTreeViewItemProps && externalLayoutFolderTreeViewItemProps + ? { + ...externalLayoutTreeViewItemProps, + ...externalLayoutFolderTreeViewItemProps, + } + : null, + [externalLayoutTreeViewItemProps, externalLayoutFolderTreeViewItemProps] + ); + const getTreeViewData = React.useCallback( (i18n: I18nType): Array => { + const handleAddNewScene = () => { + if (!project) return; + + const newName = newNameGenerator(i18n._(t`Untitled scene`), name => + project.hasLayoutNamed(name) + ); + + const newScene = project.insertNewLayout( + newName, + project.getLayoutsCount() + ); + + newScene.setName(newName); + newScene.updateBehaviorsSharedData(project); + addDefaultLightToAllLayers(newScene); + + onSceneAdded(); + onProjectItemModified(); + forceUpdateList(); + + setTreeRenderKey(prev => prev + 1); + + const sceneItemId = getSceneTreeViewItemId(newScene); + if (treeViewRef.current) { + treeViewRef.current.openItems([scenesRootFolderId]); + } + // Scroll to the new behavior. + // Ideally, we'd wait for the list to be updated to scroll, but + // to simplify the code, we just wait a few ms for a new render + // to be done. + setTimeout(() => { + scrollToItem(sceneItemId); + editName(sceneItemId); + }, 100); // A few ms is enough for a new render to be done. + }; + + const handleAddNewFolder = () => { + if (!project) return; + + const layoutsRootFolder = project.getLayoutsRootFolder(); + if (!layoutsRootFolder) return; + + const newFolderName = newNameGenerator('NewFolder', name => + hasFolderNamed(layoutsRootFolder, name) + ); + + const newFolder = insertNewFolder( + layoutsRootFolder, + newFolderName, + getChildrenCount(layoutsRootFolder) + ); + + if (!newFolder) return; + + onProjectItemModified(); + forceUpdateList(); + + setTreeRenderKey(prev => prev + 1); + + const folderItemId = getSceneFolderTreeViewItemId(newFolder); + scrollToItem(folderItemId); + editName(folderItemId); + }; + + const sceneTreeViewItemPropsWithCallbacks = { + ...sceneTreeViewItemProps, + rootId: scenesRootFolderId, + addNewScene: handleAddNewScene, + addNewFolder: handleAddNewFolder, + }; + return !project || - !sceneTreeViewItemProps || !extensionTreeViewItemProps || !externalEventsTreeViewItemProps || - !externalLayoutTreeViewItemProps + !externalLayoutTreeViewItemProps || + !combinedExternalLayoutProps || + !sceneTreeViewItemProps ? [] : [ { isRoot: true, content: new LabelTreeViewItemContent( gameSettingsRootFolderId, - i18n._(t`Game settings`) + i18n._(t`Spieleinstellungen`) ), getChildren(i18n: I18nType): ?Array { return [ @@ -1077,44 +1408,12 @@ const ProjectManager = React.forwardRef( ]; }, }, - { - isRoot: true, - content: new LabelTreeViewItemContent( - scenesRootFolderId, - i18n._(t`Scenes`), - { - icon: , - label: i18n._(t`Add a scene`), - click: () => { - // TODO Add after selected scene? - const index = project.getLayoutsCount() - 1; - addNewScene(index, i18n); - }, - id: 'add-new-scene-button', - } - ), - getChildren(i18n: I18nType): ?Array { - if (project.getLayoutsCount() === 0) { - return [ - new PlaceHolderTreeViewItem( - scenesEmptyPlaceholderId, - i18n._(t`Start by adding a new scene.`) - ), - ]; - } - return mapFor( - 0, - project.getLayoutsCount(), - i => - new LeafTreeViewItem( - new SceneTreeViewItemContent( - project.getLayoutAt(i), - sceneTreeViewItemProps - ) - ) - ); - }, - }, + ...buildScenesTreeItems( + project, + i18n, + sceneTreeViewItemPropsWithCallbacks, + scenesRootFolderId + ), { isRoot: true, content: new LabelTreeViewItemContent( @@ -1158,7 +1457,6 @@ const ProjectManager = React.forwardRef( icon: , label: i18n._(t`Add external events`), click: () => { - // TODO Add after selected scene? const index = project.getExternalEventsCount() - 1; addExternalEvents(index, i18n); }, @@ -1196,7 +1494,6 @@ const ProjectManager = React.forwardRef( icon: , label: i18n._(t`Add an external layout`), click: () => { - // TODO Add after selected scene? const index = project.getExternalLayoutsCount() - 1; addExternalLayout(index, i18n); }, @@ -1230,32 +1527,45 @@ const ProjectManager = React.forwardRef( [ addExternalEvents, addExternalLayout, - addNewScene, + combinedExternalLayoutProps, + editName, extensionTreeViewItemProps, externalEventsTreeViewItemProps, externalLayoutTreeViewItemProps, + forceUpdateList, onOpenGamesDashboardDialog, onOpenResources, + onProjectItemModified, + onSceneAdded, openProjectProperties, openProjectVariables, openSearchExtensionDialog, project, sceneTreeViewItemProps, + scrollToItem, + treeRenderKey, ] ); const canMoveSelectionTo = React.useCallback( - (destinationItem: TreeViewItem, where: 'before' | 'inside' | 'after') => - selectedItems.every(item => { - return ( - // Project and game settings children `getRootId` return an empty string. - item.content.getRootId().length > 0 && - item.content.getRootId() === destinationItem.content.getRootId() - ); - }), + (destinationItem: TreeViewItem, where: 'before' | 'inside' | 'after') => { + if (selectedItems.length === 0) { + return false; + } + + const result = selectedItems.every(item => { + const sourceRootId = item.content.getRootId(); + const destRootId = destinationItem.content.getRootId(); + + const canMove = + sourceRootId.length > 0 && sourceRootId === destRootId; + + return canMove; + }); + return result; + }, [selectedItems] ); - const moveSelectionTo = React.useCallback( ( i18n: I18nType, @@ -1265,10 +1575,47 @@ const ProjectManager = React.forwardRef( if (selectedItems.length === 0) { return; } + const selectedItem = selectedItems[0]; - selectedItem.content.moveAt( - destinationItem.content.getIndex() + (where === 'after' ? 1 : 0) - ); + const destinationContent = destinationItem.content; + + let targetFolder = null; + let targetIndex = destinationContent.getIndex(); + + if (where === 'inside') { + if (typeof destinationContent.getFolder === 'function') { + targetFolder = destinationContent.getFolder(); + targetIndex = 0; + } else if ( + typeof destinationContent.getLayoutFolderOrLayout === 'function' + ) { + const layoutFolderOrLayout = destinationContent.getLayoutFolderOrLayout(); + if (layoutFolderOrLayout && layoutFolderOrLayout.isFolder()) { + targetFolder = layoutFolderOrLayout; + targetIndex = 0; + } + } + } else { + if (typeof destinationContent.getFolder === 'function') { + const folder = destinationContent.getFolder(); + if (folder) { + targetFolder = folder.getParent(); + targetIndex = + destinationContent.getIndex() + (where === 'after' ? 1 : 0); + } + } else if ( + typeof destinationContent.getLayoutFolderOrLayout === 'function' + ) { + const layoutFolderOrLayout = destinationContent.getLayoutFolderOrLayout(); + if (layoutFolderOrLayout) { + targetFolder = layoutFolderOrLayout.getParent(); + targetIndex = + destinationContent.getIndex() + (where === 'after' ? 1 : 0); + } + } + } + selectedItem.content.moveAt(targetIndex, targetFolder); + onTreeModified(true); }, [onTreeModified, selectedItems] @@ -1286,13 +1633,13 @@ const ProjectManager = React.forwardRef( if (selectedItems[0].content.isDescendantOf(item.content)) { setSelectedItems([]); } + forceUpdateList(); }, - [selectedItems] + [selectedItems, forceUpdateList] ); - // Force List component to be mounted again if project // has been changed. Avoid accessing to invalid objects that could - // crash the app. + // crash the app const listKey = project ? project.ptr : 'no-project'; const initiallyOpenedNodeIds = [ gameSettingsRootFolderId, @@ -1359,15 +1706,7 @@ const ProjectManager = React.forwardRef( {({ i18n }) => ( <> {isNavigatingInMainMenuItem ? null : project ? ( -
+
{({ height }) => ( ( moveSelectionTo(i18n, destinationItem, where) } canMoveSelectionToItem={canMoveSelectionTo} - reactDndType={extensionItemReactDndType} + reactDndType={projectItemReactDndType} initiallyOpenedNodeIds={initiallyOpenedNodeIds} forceDefaultDraggingPreview shouldHideMenuIcon={item => diff --git a/newIDE/app/src/Utils/FolderHelpers.js b/newIDE/app/src/Utils/FolderHelpers.js new file mode 100644 index 000000000000..2bc2bf2190aa --- /dev/null +++ b/newIDE/app/src/Utils/FolderHelpers.js @@ -0,0 +1,158 @@ +// @flow + +export function getInsertionParentAndPosition( + selectedFolderOrItem: TFolderOrItem +): {| folder: TFolderOrItem, position: number |} { + const isFolder = + typeof selectedFolderOrItem.isFolder === 'function' + ? selectedFolderOrItem.isFolder() + : false; + + if (isFolder) { + const parentFolder = selectedFolderOrItem; + const childrenCount = + typeof parentFolder.getChildrenCount === 'function' + ? parentFolder.getChildrenCount() + : 0; + + return { + folder: parentFolder, + position: childrenCount, + }; + } else { + const parentFolder = selectedFolderOrItem.getParent(); + const position = parentFolder.getChildPosition(selectedFolderOrItem) + 1; + + return { + folder: parentFolder, + position: position, + }; + } +} + +export function isFolder(folderOrItem: TFolderOrItem): boolean { + return typeof folderOrItem.isFolder === 'function' + ? folderOrItem.isFolder() + : false; +} + +export function getItem( + folderOrItem: TFolderOrItem +): TItem | null { + if (isFolder(folderOrItem)) { + return null; + } + + return typeof folderOrItem.getItem === 'function' + ? folderOrItem.getItem() + : null; +} + +export function getName( + folderOrItem: TFolderOrItem, + getItemName: (item: TItem) => string +): string { + if (isFolder(folderOrItem)) { + return typeof folderOrItem.getFolderName === 'function' + ? folderOrItem.getFolderName() + : ''; + } + + const item = getItem(folderOrItem); + return item ? getItemName(item) : ''; +} + +export function isDescendantOf( + folderOrItem: TFolderOrItem, + potentialAncestor: TFolderOrItem +): boolean { + return typeof folderOrItem.isADescendantOf === 'function' + ? folderOrItem.isADescendantOf(potentialAncestor) + : false; +} + +export function getChildrenCount(folder: TFolder): number { + return typeof folder.getChildrenCount === 'function' + ? folder.getChildrenCount() + : 0; +} + +export function getChildAt( + folder: TFolder, + index: number +): TFolderOrItem | null { + return typeof folder.getChildAt === 'function' + ? folder.getChildAt(index) + : null; +} + +export function getParent( + folderOrItem: TFolderOrItem +): TFolderOrItem | null { + return typeof folderOrItem.getParent === 'function' + ? folderOrItem.getParent() + : null; +} + +export function moveFolderOrItem( + sourceFolder: TFolderOrItem, + folderOrItem: TFolderOrItem, + targetFolder: TFolderOrItem, + targetPosition: number +): void { + if ( + typeof targetFolder.moveObjectFolderOrObjectToAnotherFolder === 'function' + ) { + targetFolder.moveObjectFolderOrObjectToAnotherFolder( + folderOrItem, + targetPosition + ); + } +} + +export function insertNewFolder( + parentFolder: TFolder, + folderName: string, + position: number +): TFolder | null { + return typeof parentFolder.insertNewFolder === 'function' + ? parentFolder.insertNewFolder(folderName, position) + : null; +} + +export function hasFolderNamed( + parentFolder: TFolder, + folderName: string +): boolean { + const childrenCount = getChildrenCount(parentFolder); + + for (let i = 0; i < childrenCount; i++) { + const child = getChildAt(parentFolder, i); + if (!child) continue; + + if (isFolder(child)) { + const childName = + typeof child.getFolderName === 'function' ? child.getFolderName() : ''; + + if (childName === folderName) { + return true; + } + } + } + + return false; +} + +export function forEachChild( + folder: TFolder, + callback: (child: TFolderOrItem, index: number) => void +): void { + const childrenCount = getChildrenCount(folder); + + for (let i = 0; i < childrenCount; i++) { + const child = getChildAt(folder, i); + if (child) { + callback(child, i); + } + } +}