diff --git a/Core/GDCore/IDE/EventsBasedObjectVariantHelper.cpp b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.cpp new file mode 100644 index 000000000000..9487aa3a05f7 --- /dev/null +++ b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.cpp @@ -0,0 +1,157 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "EventsBasedObjectVariantHelper.h" + +#include "GDCore/Project/EventsBasedObject.h" +#include "GDCore/Project/InitialInstancesContainer.h" +#include "GDCore/Project/Object.h" +#include "GDCore/Project/ObjectGroup.h" +#include "GDCore/Project/ObjectsContainer.h" +#include "GDCore/Project/ObjectsContainersList.h" +#include "GDCore/Project/Project.h" +#include "GDCore/Project/Variable.h" +#include "GDCore/Project/VariablesContainer.h" +#include "GDCore/String.h" + +namespace gd { + +void EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + const gd::Project &project, gd::EventsBasedObject &eventsBasedObject) { + auto &defaultObjects = eventsBasedObject.GetDefaultVariant().GetObjects(); + + for (const auto &variant : + eventsBasedObject.GetVariants().GetInternalVector()) { + auto &objects = variant->GetObjects(); + + // Delete extra objects + for (auto it = objects.GetObjects().begin(); + it != objects.GetObjects().end(); ++it) { + const auto &objectName = it->get()->GetName(); + if (!defaultObjects.HasObjectNamed(objectName)) { + variant->GetInitialInstances().RemoveInitialInstancesOfObject( + objectName); + // Do it in last because it unalloc objectName. + objects.RemoveObject(objectName); + --it; + } + } + for (const auto &defaultObject : defaultObjects.GetObjects()) { + const auto &objectName = defaultObject->GetName(); + const auto &defaultVariables = defaultObject->GetVariables(); + const auto &defaultBehaviors = defaultObject->GetAllBehaviorContents(); + + // Copy missing objects + if (!objects.HasObjectNamed(objectName)) { + objects.InsertObject(*defaultObject, + defaultObjects.GetObjectPosition(objectName)); + objects.AddMissingObjectsInRootFolder(); + continue; + } + // Change object types + auto &object = objects.GetObject(objectName); + if (object.GetType() != defaultObject->GetType()) { + // Keep a copy of the old object. + auto oldObject = objects.GetObject(objectName); + objects.RemoveObject(objectName); + objects.InsertObject(*defaultObject, + defaultObjects.GetObjectPosition(objectName)); + object.CopyWithoutConfiguration(oldObject); + objects.AddMissingObjectsInRootFolder(); + } + + // Copy missing behaviors + auto &behaviors = object.GetAllBehaviorContents(); + for (const auto &pair : defaultBehaviors) { + const auto &behaviorName = pair.first; + const auto &defaultBehavior = pair.second; + + if (object.HasBehaviorNamed(behaviorName) && + object.GetBehavior(behaviorName).GetTypeName() != + defaultBehavior->GetTypeName()) { + object.RemoveBehavior(behaviorName); + } + if (!object.HasBehaviorNamed(behaviorName)) { + auto *behavior = object.AddNewBehavior( + project, defaultBehavior->GetTypeName(), behaviorName); + gd::SerializerElement element; + defaultBehavior->SerializeTo(element); + behavior->UnserializeFrom(element); + } + } + // Delete extra behaviors + for (auto it = behaviors.begin(); it != behaviors.end(); ++it) { + const auto &behaviorName = it->first; + if (!defaultObject->HasBehaviorNamed(behaviorName)) { + object.RemoveBehavior(behaviorName); + --it; + } + } + + // Sort and copy missing variables + auto &variables = object.GetVariables(); + for (size_t defaultVariableIndex = 0; + defaultVariableIndex < defaultVariables.Count(); + defaultVariableIndex++) { + const auto &variableName = + defaultVariables.GetNameAt(defaultVariableIndex); + const auto &defaultVariable = + defaultVariables.Get(defaultVariableIndex); + + auto variableIndex = variables.GetPosition(variableName); + if (variableIndex == gd::String::npos) { + variables.Insert(variableName, defaultVariable, defaultVariableIndex); + } else { + variables.Move(variableIndex, defaultVariableIndex); + } + if (variables.Get(variableName).GetType() != defaultVariable.GetType()) { + variables.Remove(variableName); + variables.Insert(variableName, defaultVariable, defaultVariableIndex); + } + } + // Remove extra variables + auto variableToRemoveCount = variables.Count() - defaultVariables.Count(); + for (size_t iteration = 0; iteration < variableToRemoveCount; + iteration++) { + variables.Remove(variables.GetNameAt(variables.Count() - 1)); + } + + // Remove extra instance variables + variant->GetInitialInstances().IterateOverInstances( + [&objectName, + &defaultVariables](gd::InitialInstance &initialInstance) { + if (initialInstance.GetObjectName() != objectName) { + return false; + } + auto &instanceVariables = initialInstance.GetVariables(); + for (size_t instanceVariableIndex = 0; + instanceVariableIndex < instanceVariables.Count(); + instanceVariableIndex++) { + const auto &variableName = + defaultVariables.GetNameAt(instanceVariableIndex); + + if (!defaultVariables.Has(variableName)) { + instanceVariables.Remove(variableName); + } + } + return false; + }); + } + auto &defaultObjectGroups = + eventsBasedObject.GetDefaultVariant().GetObjects().GetObjectGroups(); + auto &objectGroups = variant->GetObjects().GetObjectGroups(); + auto objectGroupsCount = objectGroups.Count(); + // Clear groups + for (size_t index = 0; index < objectGroupsCount; index++) { + objectGroups.Remove(objectGroups.Get(0).GetName()); + } + // Copy groups + for (size_t index = 0; index < defaultObjectGroups.Count(); index++) { + objectGroups.Insert(defaultObjectGroups.Get(index), index); + } + } +} + +} // namespace gd diff --git a/Core/GDCore/IDE/EventsBasedObjectVariantHelper.h b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.h new file mode 100644 index 000000000000..bd78252325d2 --- /dev/null +++ b/Core/GDCore/IDE/EventsBasedObjectVariantHelper.h @@ -0,0 +1,26 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once + +namespace gd { +class EventsBasedObject; +class Project; +} // namespace gd + +namespace gd { + +class GD_CORE_API EventsBasedObjectVariantHelper { +public: + /** + * @brief Apply the changes done on events-based object children to all its + * variants. + */ + static void + ComplyVariantsToEventsBasedObject(const gd::Project &project, + gd::EventsBasedObject &eventsBasedObject); +}; + +} // namespace gd \ No newline at end of file diff --git a/Core/GDCore/IDE/ObjectAssetSerializer.cpp b/Core/GDCore/IDE/ObjectAssetSerializer.cpp index 959864f286c8..d40835528d6c 100644 --- a/Core/GDCore/IDE/ObjectAssetSerializer.cpp +++ b/Core/GDCore/IDE/ObjectAssetSerializer.cpp @@ -17,6 +17,7 @@ #include "GDCore/IDE/Project/ResourcesRenamer.h" #include "GDCore/Project/Behavior.h" #include "GDCore/Project/CustomBehavior.h" +#include "GDCore/Project/CustomObjectConfiguration.h" #include "GDCore/Project/EventsFunctionsExtension.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Object.h" @@ -75,6 +76,16 @@ void ObjectAssetSerializer::SerializeTo( cleanObject->SerializeTo(objectAssetElement.AddChild("object")); + if (project.HasEventsBasedObject(object.GetType())) { + SerializerElement &variantsElement = + objectAssetElement.AddChild("variants"); + variantsElement.ConsiderAsArrayOf("variant"); + + std::unordered_set<gd::String> alreadyUsedVariantIdentifiers; + gd::ObjectAssetSerializer::SerializeUsedVariantsTo( + project, object, variantsElement, alreadyUsedVariantIdentifiers); + } + SerializerElement &resourcesElement = objectAssetElement.AddChild("resources"); resourcesElement.ConsiderAsArrayOf("resource"); @@ -108,4 +119,46 @@ void ObjectAssetSerializer::SerializeTo( objectAssetElement.AddChild("customization"); customizationElement.ConsiderAsArrayOf("empty"); } + +void ObjectAssetSerializer::SerializeUsedVariantsTo( + gd::Project &project, const gd::Object &object, + SerializerElement &variantsElement, + std::unordered_set<gd::String> &alreadyUsedVariantIdentifiers) { + + if (!project.HasEventsBasedObject(object.GetType())) { + return; + } + const auto *customObjectConfiguration = + dynamic_cast<const gd::CustomObjectConfiguration *>( + &object.GetConfiguration()); + if (customObjectConfiguration + ->IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() || + customObjectConfiguration + ->IsForcedToOverrideEventsBasedObjectChildrenConfiguration()) { + return; + } + const auto &variantName = customObjectConfiguration->GetVariantName(); + const auto &variantIdentifier = + object.GetType() + gd::PlatformExtension::GetNamespaceSeparator() + + variantName; + auto insertResult = alreadyUsedVariantIdentifiers.insert(variantIdentifier); + if (insertResult.second) { + const auto &eventsBasedObject = + project.GetEventsBasedObject(object.GetType()); + const auto &variants = eventsBasedObject.GetVariants(); + const auto &variant = variants.HasVariantNamed(variantName) + ? variants.GetVariant(variantName) + : eventsBasedObject.GetDefaultVariant(); + + SerializerElement &pairElement = variantsElement.AddChild("variant"); + pairElement.SetAttribute("objectType", object.GetType()); + SerializerElement &variantElement = pairElement.AddChild("variant"); + variant.SerializeTo(variantElement); + // TODO Recursivity + for (auto &object : variant.GetObjects().GetObjects()) { + gd::ObjectAssetSerializer::SerializeUsedVariantsTo( + project, *object, variantsElement, alreadyUsedVariantIdentifiers); + } + } +} } // namespace gd diff --git a/Core/GDCore/IDE/ObjectAssetSerializer.h b/Core/GDCore/IDE/ObjectAssetSerializer.h index bb678449213b..51b641a90b62 100644 --- a/Core/GDCore/IDE/ObjectAssetSerializer.h +++ b/Core/GDCore/IDE/ObjectAssetSerializer.h @@ -6,6 +6,7 @@ #pragma once #include <map> #include <vector> +#include <unordered_set> #include "GDCore/String.h" @@ -52,6 +53,11 @@ class GD_CORE_API ObjectAssetSerializer { ObjectAssetSerializer(){}; static gd::String GetObjectExtensionName(const gd::Object &object); + + static void SerializeUsedVariantsTo( + gd::Project &project, const gd::Object &object, + SerializerElement &variantsElement, + std::unordered_set<gd::String> &alreadyUsedVariantIdentifiers); }; } // namespace gd diff --git a/Core/GDCore/IDE/ObjectVariableHelper.cpp b/Core/GDCore/IDE/ObjectVariableHelper.cpp index d9fabc624365..c1b37e188908 100644 --- a/Core/GDCore/IDE/ObjectVariableHelper.cpp +++ b/Core/GDCore/IDE/ObjectVariableHelper.cpp @@ -6,6 +6,7 @@ #include "ObjectVariableHelper.h" #include "GDCore/IDE/WholeProjectRefactorer.h" +#include "GDCore/Project/EventsBasedObject.h" #include "GDCore/Project/InitialInstancesContainer.h" #include "GDCore/Project/Object.h" #include "GDCore/Project/ObjectGroup.h" @@ -173,6 +174,7 @@ void ObjectVariableHelper::ApplyChangesToObjects( groupVariablesContainer.Get(variableName), variablesContainer.Count()); } + // TODO Check what happens if 2 variables exchange their names. for (const auto &pair : changeset.oldToNewVariableNames) { const gd::String &oldVariableName = pair.first; const gd::String &newVariableName = pair.second; @@ -215,6 +217,7 @@ void ObjectVariableHelper::ApplyChangesToObjectInstances( destinationVariablesContainer.Remove(variableName); } } + // TODO Check what happens if 2 variables exchange their names. for (const auto &pair : changeset.oldToNewVariableNames) { const gd::String &oldVariableName = pair.first; const gd::String &newVariableName = pair.second; @@ -236,6 +239,66 @@ void ObjectVariableHelper::ApplyChangesToObjectInstances( } } } + return false; }); } + +void ObjectVariableHelper::ApplyChangesToVariants( + gd::EventsBasedObject &eventsBasedObject, const gd::String &objectName, + const gd::VariablesChangeset &changeset) { + auto &defaultVariablesContainer = eventsBasedObject.GetDefaultVariant() + .GetObjects() + .GetObject(objectName) + .GetVariables(); + for (auto &variant : eventsBasedObject.GetVariants().GetInternalVector()) { + if (!variant->GetObjects().HasObjectNamed(objectName)) { + continue; + } + auto &object = variant->GetObjects().GetObject(objectName); + auto &variablesContainer = object.GetVariables(); + + for (const gd::String &variableName : changeset.removedVariableNames) { + variablesContainer.Remove(variableName); + } + for (const gd::String &variableName : changeset.addedVariableNames) { + if (variablesContainer.Has(variableName)) { + // It can happens if a child-object already had the variable but it was + // missing in other variant child-object. + continue; + } + variablesContainer.Insert(variableName, + defaultVariablesContainer.Get(variableName), + variablesContainer.Count()); + } + // TODO Check what happens if 2 variables exchange their names. + for (const auto &pair : changeset.oldToNewVariableNames) { + const gd::String &oldVariableName = pair.first; + const gd::String &newVariableName = pair.second; + if (variablesContainer.Has(newVariableName)) { + // It can happens if a child-object already had the variable but it was + // missing in other variant child-object. + variablesContainer.Remove(oldVariableName); + } else { + variablesContainer.Rename(oldVariableName, newVariableName); + } + } + // Apply type changes + for (const gd::String &variableName : changeset.valueChangedVariableNames) { + size_t index = variablesContainer.GetPosition(variableName); + + if (variablesContainer.Has(variableName) && + variablesContainer.Get(variableName).GetType() != + defaultVariablesContainer.Get(variableName).GetType()) { + variablesContainer.Remove(variableName); + variablesContainer.Insert( + variableName, defaultVariablesContainer.Get(variableName), index); + } + } + + gd::ObjectVariableHelper::ApplyChangesToObjectInstances( + variablesContainer, variant->GetInitialInstances(), objectName, + changeset); + } +} + } // namespace gd diff --git a/Core/GDCore/IDE/ObjectVariableHelper.h b/Core/GDCore/IDE/ObjectVariableHelper.h index bafc21429c27..c9950ab9e895 100644 --- a/Core/GDCore/IDE/ObjectVariableHelper.h +++ b/Core/GDCore/IDE/ObjectVariableHelper.h @@ -8,6 +8,7 @@ #include "GDCore/Project/VariablesContainer.h" namespace gd { +class EventsBasedObject; class InitialInstancesContainer; class ObjectsContainersList; class ObjectsContainer; @@ -53,7 +54,7 @@ class GD_CORE_API ObjectVariableHelper { * Objects can be added during the group edition and may not necessarily have * all the variables initially shared by the group. * - * \see gd::GroupVariableHelper::MergeVariableContainers + * \see gd::ObjectVariableHelper::MergeVariableContainers */ static void FillMissingGroupVariablesToObjects( gd::ObjectsContainer &globalObjectsContainer, @@ -72,16 +73,21 @@ class GD_CORE_API ObjectVariableHelper { const gd::ObjectGroup &objectGroup, const gd::VariablesChangeset &changeset); + /** + * @brief Apply the changes done on an object to all its instances. + */ static void ApplyChangesToObjectInstances( gd::VariablesContainer &objectVariablesContainer, gd::InitialInstancesContainer &initialInstancesContainer, const gd::String &objectName, const gd::VariablesChangeset &changeset); -private: - static void ApplyChangesToVariableContainer( - const gd::VariablesContainer &originalVariablesContainer, - gd::VariablesContainer &destinationVariablesContainer, - const gd::VariablesChangeset &changeset, bool shouldApplyValueChanges); + /** + * @brief Apply the changes done on events-based object child to all its + * variants. + */ + static void ApplyChangesToVariants(gd::EventsBasedObject &eventsBasedObject, + const gd::String &objectName, + const gd::VariablesChangeset &changeset); }; } // namespace gd \ No newline at end of file diff --git a/Core/GDCore/IDE/ProjectBrowserHelper.cpp b/Core/GDCore/IDE/ProjectBrowserHelper.cpp index 628289f05ddd..8a684686ea78 100644 --- a/Core/GDCore/IDE/ProjectBrowserHelper.cpp +++ b/Core/GDCore/IDE/ProjectBrowserHelper.cpp @@ -314,6 +314,12 @@ void ProjectBrowserHelper::ExposeProjectObjects( eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) { auto eventsBasedObject = eventsBasedObjectUniquePtr.get(); worker.Launch(eventsBasedObject->GetObjects()); + + for (auto &&variantUniquePtr : + eventsBasedObject->GetVariants().GetInternalVector()) { + auto variant = variantUniquePtr.get(); + worker.Launch(variant->GetObjects()); + } } } }; diff --git a/Core/GDCore/IDE/WholeProjectRefactorer.cpp b/Core/GDCore/IDE/WholeProjectRefactorer.cpp index a24d12629201..8ec2e67d931d 100644 --- a/Core/GDCore/IDE/WholeProjectRefactorer.cpp +++ b/Core/GDCore/IDE/WholeProjectRefactorer.cpp @@ -2129,6 +2129,26 @@ void WholeProjectRefactorer::ObjectOrGroupRenamedInEventsBasedObject( groups[g].RenameObject(oldName, newName); } } + + for (auto &variant : eventsBasedObject.GetVariants().GetInternalVector()) { + auto &variantObjects = variant->GetObjects(); + auto &variantObjectGroups = variantObjects.GetObjectGroups(); + if (isObjectGroup) { + if (variantObjectGroups.Has(oldName)) { + variantObjectGroups.Get(oldName).SetName(newName); + } + // Object groups can't have instances or be in other groups + } + else { + if (variantObjects.HasObjectNamed(oldName)) { + variantObjects.GetObject(oldName).SetName(newName); + } + variant->GetInitialInstances().RenameInstancesOfObject(oldName, newName); + for (std::size_t g = 0; g < variantObjectGroups.size(); ++g) { + variantObjectGroups[g].RenameObject(oldName, newName); + } + } + } } void WholeProjectRefactorer::ObjectOrGroupRenamedInEventsFunction( diff --git a/Core/GDCore/Project/CustomObjectConfiguration.cpp b/Core/GDCore/Project/CustomObjectConfiguration.cpp index 5040f04672b1..ad29305938af 100644 --- a/Core/GDCore/Project/CustomObjectConfiguration.cpp +++ b/Core/GDCore/Project/CustomObjectConfiguration.cpp @@ -19,6 +19,7 @@ using namespace gd; void CustomObjectConfiguration::Init(const gd::CustomObjectConfiguration& objectConfiguration) { project = objectConfiguration.project; + variantName = objectConfiguration.variantName; objectContent = objectConfiguration.objectContent; animations = objectConfiguration.animations; isMarkedAsOverridingEventsBasedObjectChildrenConfiguration = @@ -165,6 +166,7 @@ void CustomObjectConfiguration::DoSerializeTo(SerializerElement& element) const animations.SerializeTo(animatableElement); } + element.SetAttribute("variant", variantName); if (IsOverridingEventsBasedObjectChildrenConfiguration()) { auto &childrenContentElement = element.AddChild("childrenContent"); for (auto &pair : childObjectConfigurations) { @@ -184,6 +186,7 @@ void CustomObjectConfiguration::DoUnserializeFrom(Project& project, animations.UnserializeFrom(animatableElement); } + variantName = element.GetStringAttribute("variant"); isMarkedAsOverridingEventsBasedObjectChildrenConfiguration = element.HasChild("childrenContent"); if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) { @@ -247,9 +250,26 @@ void CustomObjectConfiguration::ExposeResources(gd::ArbitraryResourceWorker& wor } const auto &eventsBasedObject = project->GetEventsBasedObject(GetType()); - for (auto& childObject : eventsBasedObject.GetObjects().GetObjects()) { - auto &configuration = GetChildObjectConfiguration(childObject->GetName()); - configuration.ExposeResources(worker); + if (isMarkedAsOverridingEventsBasedObjectChildrenConfiguration) { + for (auto &childObject : eventsBasedObject.GetObjects().GetObjects()) { + auto &configuration = GetChildObjectConfiguration(childObject->GetName()); + configuration.ExposeResources(worker); + } + } else { + if (variantName.empty() || + !eventsBasedObject.GetVariants().HasVariantNamed(variantName)) { + for (auto &childObject : + eventsBasedObject.GetDefaultVariant().GetObjects().GetObjects()) { + childObject->GetConfiguration().ExposeResources(worker); + } + } else { + for (auto &childObject : eventsBasedObject.GetVariants() + .GetVariant(variantName) + .GetObjects() + .GetObjects()) { + childObject->GetConfiguration().ExposeResources(worker); + } + } } } diff --git a/Core/GDCore/Project/CustomObjectConfiguration.h b/Core/GDCore/Project/CustomObjectConfiguration.h index acc649557015..935e83e39ecd 100644 --- a/Core/GDCore/Project/CustomObjectConfiguration.h +++ b/Core/GDCore/Project/CustomObjectConfiguration.h @@ -29,9 +29,9 @@ namespace gd { * "resource". */ class CustomObjectConfiguration : public gd::ObjectConfiguration { - public: - CustomObjectConfiguration(const Project& project_, const String& type_) - : project(&project_), isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(false) { +public: + CustomObjectConfiguration(const Project &project_, const String &type_) + : project(&project_) { SetType(type_); } std::unique_ptr<gd::ObjectConfiguration> Clone() const override; @@ -66,6 +66,18 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration { void ExposeResources(gd::ArbitraryResourceWorker& worker) override; + /** + * \brief Get the name of the events-based object variant used by this custom object. + */ + const gd::String &GetVariantName() const { return variantName; }; + + /** + * \brief Set the name of the events-based object variant used by this custom object. + */ + void SetVariantName(const gd::String &variantName_) { + variantName = variantName_; + } + bool IsForcedToOverrideEventsBasedObjectChildrenConfiguration() const; bool IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration() const { @@ -145,6 +157,7 @@ class CustomObjectConfiguration : public gd::ObjectConfiguration { gd::SerializerElement objectContent; std::unordered_set<gd::String> unfoldedChildren; + gd::String variantName = ""; bool isMarkedAsOverridingEventsBasedObjectChildrenConfiguration = false; mutable std::map<gd::String, std::unique_ptr<gd::ObjectConfiguration>> childObjectConfigurations; diff --git a/Core/GDCore/Project/EventsBasedObject.cpp b/Core/GDCore/Project/EventsBasedObject.cpp index ff8f7bbfa80e..831e276926d9 100644 --- a/Core/GDCore/Project/EventsBasedObject.cpp +++ b/Core/GDCore/Project/EventsBasedObject.cpp @@ -17,19 +17,13 @@ EventsBasedObject::EventsBasedObject() isAnimatable(false), isTextContainer(false), isInnerAreaFollowingParentSize(false), - isUsingLegacyInstancesRenderer(false), - areaMinX(0), - areaMinY(0), - areaMinZ(0), - areaMaxX(64), - areaMaxY(64), - areaMaxZ(64), - objectsContainer(gd::ObjectsContainer::SourceType::Object) { + isUsingLegacyInstancesRenderer(false) { } EventsBasedObject::~EventsBasedObject() {} -void EventsBasedObject::SerializeTo(SerializerElement& element) const { + +void EventsBasedObject::SerializeToExternal(SerializerElement& element) const { element.SetAttribute("defaultName", defaultName); if (isRenderedIn3D) { element.SetBoolAttribute("is3D", true); @@ -44,20 +38,16 @@ void EventsBasedObject::SerializeTo(SerializerElement& element) const { element.SetBoolAttribute("isInnerAreaFollowingParentSize", true); } element.SetBoolAttribute("isUsingLegacyInstancesRenderer", isUsingLegacyInstancesRenderer); - element.SetIntAttribute("areaMinX", areaMinX); - element.SetIntAttribute("areaMinY", areaMinY); - element.SetIntAttribute("areaMinZ", areaMinZ); - element.SetIntAttribute("areaMaxX", areaMaxX); - element.SetIntAttribute("areaMaxY", areaMaxY); - element.SetIntAttribute("areaMaxZ", areaMaxZ); + // The EventsBasedObjectVariant SerializeTo method override the name. + // AbstractEventsBasedEntity::SerializeTo must be done after. + defaultVariant.SerializeTo(element); AbstractEventsBasedEntity::SerializeTo(element); - objectsContainer.SerializeObjectsTo(element.AddChild("objects")); - objectsContainer.SerializeFoldersTo(element.AddChild("objectsFolderStructure")); - objectsContainer.GetObjectGroups().SerializeTo(element.AddChild("objectsGroups")); +} - layers.SerializeLayersTo(element.AddChild("layers")); - initialInstances.SerializeTo(element.AddChild("instances")); +void EventsBasedObject::SerializeTo(SerializerElement& element) const { + SerializeToExternal(element); + variants.SerializeVariantsTo(element.AddChild("variants")); } void EventsBasedObject::UnserializeFrom(gd::Project& project, @@ -68,36 +58,22 @@ void EventsBasedObject::UnserializeFrom(gd::Project& project, isTextContainer = element.GetBoolAttribute("isTextContainer", false); isInnerAreaFollowingParentSize = element.GetBoolAttribute("isInnerAreaFollowingParentSize", false); - areaMinX = element.GetIntAttribute("areaMinX", 0); - areaMinY = element.GetIntAttribute("areaMinY", 0); - areaMinZ = element.GetIntAttribute("areaMinZ", 0); - areaMaxX = element.GetIntAttribute("areaMaxX", 64); - areaMaxY = element.GetIntAttribute("areaMaxY", 64); - areaMaxZ = element.GetIntAttribute("areaMaxZ", 64); + defaultVariant.UnserializeFrom(project, element); + defaultVariant.SetName(""); AbstractEventsBasedEntity::UnserializeFrom(project, element); - objectsContainer.UnserializeObjectsFrom(project, element.GetChild("objects")); - if (element.HasChild("objectsFolderStructure")) { - objectsContainer.UnserializeFoldersFrom(project, element.GetChild("objectsFolderStructure", 0)); - } - objectsContainer.AddMissingObjectsInRootFolder(); - objectsContainer.GetObjectGroups().UnserializeFrom( - element.GetChild("objectsGroups")); - if (element.HasChild("layers")) { - layers.UnserializeLayersFrom(element.GetChild("layers")); - } else { - layers.Reset(); + if (element.HasChild("variants")) { + variants.UnserializeVariantsFrom(project, element.GetChild("variants")); } - initialInstances.UnserializeFrom(element.GetChild("instances")); if (element.HasChild("isUsingLegacyInstancesRenderer")) { isUsingLegacyInstancesRenderer = element.GetBoolAttribute("isUsingLegacyInstancesRenderer", false); } else { // Compatibility with GD <= 5.4.212 - isUsingLegacyInstancesRenderer = initialInstances.GetInstancesCount() == 0; + isUsingLegacyInstancesRenderer = GetInitialInstances().GetInstancesCount() == 0; // end of compatibility code } } diff --git a/Core/GDCore/Project/EventsBasedObject.h b/Core/GDCore/Project/EventsBasedObject.h index b336889109e7..b8f214129990 100644 --- a/Core/GDCore/Project/EventsBasedObject.h +++ b/Core/GDCore/Project/EventsBasedObject.h @@ -7,6 +7,8 @@ #include <vector> #include "GDCore/Project/AbstractEventsBasedEntity.h" +#include "GDCore/Project/EventsBasedObjectVariant.h" +#include "GDCore/Project/EventsBasedObjectVariantsContainer.h" #include "GDCore/Project/ObjectsContainer.h" #include "GDCore/Project/InitialInstancesContainer.h" #include "GDCore/Project/LayersContainer.h" @@ -162,18 +164,38 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { */ bool IsTextContainer() const { return isTextContainer; } + /** + * \brief Get the default variant of the custom object. + */ + const gd::EventsBasedObjectVariant& GetDefaultVariant() const { return defaultVariant; } + + /** + * \brief Get the default variant of the custom object. + */ + gd::EventsBasedObjectVariant& GetDefaultVariant() { return defaultVariant; } + + /** + * \brief Get the variants of the custom object. + */ + const gd::EventsBasedObjectVariantsContainer& GetVariants() const { return variants; } + + /** + * \brief Get the variants of the custom object. + */ + gd::EventsBasedObjectVariantsContainer& GetVariants() { return variants; } + /** \name Layers */ ///@{ /** * \brief Get the layers of the custom object. */ - const gd::LayersContainer& GetLayers() const { return layers; } + const gd::LayersContainer& GetLayers() const { return defaultVariant.GetLayers(); } /** * \brief Get the layers of the custom object. */ - gd::LayersContainer& GetLayers() { return layers; } + gd::LayersContainer& GetLayers() { return defaultVariant.GetLayers(); } ///@} /** \name Child objects @@ -183,14 +205,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \brief Get the objects of the custom object. */ gd::ObjectsContainer& GetObjects() { - return objectsContainer; + return defaultVariant.GetObjects(); } /** * \brief Get the objects of the custom object. */ const gd::ObjectsContainer& GetObjects() const { - return objectsContainer; + return defaultVariant.GetObjects(); } ///@} @@ -201,14 +223,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \brief Get the instances of the custom object. */ gd::InitialInstancesContainer& GetInitialInstances() { - return initialInstances; + return defaultVariant.GetInitialInstances(); } /** * \brief Get the instances of the custom object. */ const gd::InitialInstancesContainer& GetInitialInstances() const { - return initialInstances; + return defaultVariant.GetInitialInstances(); } /** @@ -219,14 +241,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \see EventsBasedObject::GetInitialInstances */ int GetAreaMinX() const { - return areaMinX; + return defaultVariant.GetAreaMinX(); } /** * \brief Set the left bound of the custom object. */ - void SetAreaMinX(int areaMinX_) { - areaMinX = areaMinX_; + void SetAreaMinX(int areaMinX) { + defaultVariant.SetAreaMinX(areaMinX); } /** @@ -237,14 +259,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \see EventsBasedObject::GetInitialInstances */ int GetAreaMinY() const { - return areaMinY; + return defaultVariant.GetAreaMinY(); } /** * \brief Set the top bound of the custom object. */ - void SetAreaMinY(int areaMinY_) { - areaMinY = areaMinY_; + void SetAreaMinY(int areaMinY) { + defaultVariant.SetAreaMinY(areaMinY); } /** @@ -255,14 +277,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \see EventsBasedObject::GetInitialInstances */ int GetAreaMinZ() const { - return areaMinZ; + return defaultVariant.GetAreaMinZ(); } /** * \brief Set the min Z bound of the custom object. */ - void SetAreaMinZ(int areaMinZ_) { - areaMinZ = areaMinZ_; + void SetAreaMinZ(int areaMinZ) { + defaultVariant.SetAreaMinZ(areaMinZ); } /** @@ -273,14 +295,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \see EventsBasedObject::GetInitialInstances */ int GetAreaMaxX() const { - return areaMaxX; + return defaultVariant.GetAreaMaxX(); } /** * \brief Set the right bound of the custom object. */ - void SetAreaMaxX(int areaMaxX_) { - areaMaxX = areaMaxX_; + void SetAreaMaxX(int areaMaxX) { + defaultVariant.SetAreaMaxX(areaMaxX); } /** @@ -291,14 +313,14 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \see EventsBasedObject::GetInitialInstances */ int GetAreaMaxY() const { - return areaMaxY; + return defaultVariant.GetAreaMaxY(); } /** * \brief Set the bottom bound of the custom object. */ - void SetAreaMaxY(int areaMaxY_) { - areaMaxY = areaMaxY_; + void SetAreaMaxY(int areaMaxY) { + defaultVariant.SetAreaMaxY(areaMaxY); } /** @@ -309,16 +331,22 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { * \see EventsBasedObject::GetInitialInstances */ int GetAreaMaxZ() const { - return areaMaxZ; + return defaultVariant.GetAreaMaxZ(); } /** * \brief Set the bottom bound of the custom object. */ - void SetAreaMaxZ(int areaMaxZ_) { - areaMaxZ = areaMaxZ_; + void SetAreaMaxZ(int areaMaxZ) { + defaultVariant.SetAreaMaxZ(areaMaxZ); } ///@} + + /** + * @brief Serialize the events-based object for an extension in an external file. + * Variants are not serialized. + */ + void SerializeToExternal(SerializerElement& element) const; void SerializeTo(SerializerElement& element) const override; @@ -332,15 +360,8 @@ class GD_CORE_API EventsBasedObject: public AbstractEventsBasedEntity { bool isTextContainer; bool isInnerAreaFollowingParentSize; bool isUsingLegacyInstancesRenderer; - gd::InitialInstancesContainer initialInstances; - gd::LayersContainer layers; - gd::ObjectsContainer objectsContainer; - double areaMinX; - double areaMinY; - double areaMinZ; - double areaMaxX; - double areaMaxY; - double areaMaxZ; + gd::EventsBasedObjectVariant defaultVariant; + gd::EventsBasedObjectVariantsContainer variants; }; } // namespace gd diff --git a/Core/GDCore/Project/EventsBasedObjectVariant.cpp b/Core/GDCore/Project/EventsBasedObjectVariant.cpp new file mode 100644 index 000000000000..3a735c7fd3d2 --- /dev/null +++ b/Core/GDCore/Project/EventsBasedObjectVariant.cpp @@ -0,0 +1,71 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "EventsBasedObjectVariant.h" +#include "GDCore/Project/Object.h" +#include "GDCore/Serialization/SerializerElement.h" + +namespace gd { + +EventsBasedObjectVariant::EventsBasedObjectVariant() + : areaMinX(0), areaMinY(0), areaMinZ(0), areaMaxX(64), areaMaxY(64), + areaMaxZ(64), objectsContainer(gd::ObjectsContainer::SourceType::Object) { +} + +EventsBasedObjectVariant::~EventsBasedObjectVariant() {} + +void EventsBasedObjectVariant::SerializeTo(SerializerElement &element) const { + element.SetAttribute("name", name); + if (!GetAssetStoreAssetId().empty() && !GetAssetStoreOriginalName().empty()) { + element.SetAttribute("assetStoreAssetId", GetAssetStoreAssetId()); + element.SetAttribute("assetStoreOriginalName", GetAssetStoreOriginalName()); + } + element.SetIntAttribute("areaMinX", areaMinX); + element.SetIntAttribute("areaMinY", areaMinY); + element.SetIntAttribute("areaMinZ", areaMinZ); + element.SetIntAttribute("areaMaxX", areaMaxX); + element.SetIntAttribute("areaMaxY", areaMaxY); + element.SetIntAttribute("areaMaxZ", areaMaxZ); + + objectsContainer.SerializeObjectsTo(element.AddChild("objects")); + objectsContainer.SerializeFoldersTo( + element.AddChild("objectsFolderStructure")); + objectsContainer.GetObjectGroups().SerializeTo( + element.AddChild("objectsGroups")); + + layers.SerializeLayersTo(element.AddChild("layers")); + initialInstances.SerializeTo(element.AddChild("instances")); +} + +void EventsBasedObjectVariant::UnserializeFrom( + gd::Project &project, const SerializerElement &element) { + name = element.GetStringAttribute("name"); + assetStoreAssetId = element.GetStringAttribute("assetStoreAssetId"); + assetStoreOriginalName = element.GetStringAttribute("assetStoreOriginalName"); + areaMinX = element.GetIntAttribute("areaMinX", 0); + areaMinY = element.GetIntAttribute("areaMinY", 0); + areaMinZ = element.GetIntAttribute("areaMinZ", 0); + areaMaxX = element.GetIntAttribute("areaMaxX", 64); + areaMaxY = element.GetIntAttribute("areaMaxY", 64); + areaMaxZ = element.GetIntAttribute("areaMaxZ", 64); + + objectsContainer.UnserializeObjectsFrom(project, element.GetChild("objects")); + if (element.HasChild("objectsFolderStructure")) { + objectsContainer.UnserializeFoldersFrom( + project, element.GetChild("objectsFolderStructure", 0)); + } + objectsContainer.AddMissingObjectsInRootFolder(); + objectsContainer.GetObjectGroups().UnserializeFrom( + element.GetChild("objectsGroups")); + + if (element.HasChild("layers")) { + layers.UnserializeLayersFrom(element.GetChild("layers")); + } else { + layers.Reset(); + } + initialInstances.UnserializeFrom(element.GetChild("instances")); +} + +} // namespace gd diff --git a/Core/GDCore/Project/EventsBasedObjectVariant.h b/Core/GDCore/Project/EventsBasedObjectVariant.h new file mode 100644 index 000000000000..5729bea1c2be --- /dev/null +++ b/Core/GDCore/Project/EventsBasedObjectVariant.h @@ -0,0 +1,229 @@ +/* + * GDevelop Core + * Copyright 2008-2025 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once + +#include "GDCore/Project/InitialInstancesContainer.h" +#include "GDCore/Project/LayersContainer.h" +#include "GDCore/Project/ObjectsContainer.h" +#include "GDCore/String.h" +#include <vector> + +namespace gd { +class SerializerElement; +class Project; +} // namespace gd + +namespace gd { +/** + * \brief Represents a variation of style of an events-based object. + * + * \ingroup PlatformDefinition + */ +class GD_CORE_API EventsBasedObjectVariant { +public: + EventsBasedObjectVariant(); + virtual ~EventsBasedObjectVariant(); + + /** + * \brief Return a pointer to a new EventsBasedObjectVariant constructed from + * this one. + */ + EventsBasedObjectVariant *Clone() const { + return new EventsBasedObjectVariant(*this); + }; + + /** + * \brief Get the name of the variant. + */ + const gd::String &GetName() const { return name; }; + + /** + * \brief Set the name of the variant. + */ + EventsBasedObjectVariant &SetName(const gd::String &name_) { + name = name_; + return *this; + } + + /** \name Layers + */ + ///@{ + /** + * \brief Get the layers of the variant. + */ + const gd::LayersContainer &GetLayers() const { return layers; } + + /** + * \brief Get the layers of the variant. + */ + gd::LayersContainer &GetLayers() { return layers; } + ///@} + + /** \name Child objects + */ + ///@{ + /** + * \brief Get the objects of the variant. + */ + gd::ObjectsContainer &GetObjects() { return objectsContainer; } + + /** + * \brief Get the objects of the variant. + */ + const gd::ObjectsContainer &GetObjects() const { return objectsContainer; } + ///@} + + /** \name Instances + */ + ///@{ + /** + * \brief Get the instances of the variant. + */ + gd::InitialInstancesContainer &GetInitialInstances() { + return initialInstances; + } + + /** + * \brief Get the instances of the variant. + */ + const gd::InitialInstancesContainer &GetInitialInstances() const { + return initialInstances; + } + + /** + * \brief Get the left bound of the variant. + * + * This is used only if there is any initial instances. + * + * \see EventsBasedObjectVariant::GetInitialInstances + */ + int GetAreaMinX() const { return areaMinX; } + + /** + * \brief Set the left bound of the variant. + */ + void SetAreaMinX(int areaMinX_) { areaMinX = areaMinX_; } + + /** + * \brief Get the top bound of the variant. + * + * This is used only if there is any initial instances. + * + * \see EventsBasedObjectVariant::GetInitialInstances + */ + int GetAreaMinY() const { return areaMinY; } + + /** + * \brief Set the top bound of the variant. + */ + void SetAreaMinY(int areaMinY_) { areaMinY = areaMinY_; } + + /** + * \brief Get the min Z bound of the variant. + * + * This is used only if there is any initial instances. + * + * \see EventsBasedObjectVariant::GetInitialInstances + */ + int GetAreaMinZ() const { return areaMinZ; } + + /** + * \brief Set the min Z bound of the variant. + */ + void SetAreaMinZ(int areaMinZ_) { areaMinZ = areaMinZ_; } + + /** + * \brief Get the right bound of the variant. + * + * This is used only if there is any initial instances. + * + * \see EventsBasedObjectVariant::GetInitialInstances + */ + int GetAreaMaxX() const { return areaMaxX; } + + /** + * \brief Set the right bound of the variant. + */ + void SetAreaMaxX(int areaMaxX_) { areaMaxX = areaMaxX_; } + + /** + * \brief Get the bottom bound of the variant. + * + * This is used only if there is any initial instances. + * + * \see EventsBasedObjectVariant::GetInitialInstances + */ + int GetAreaMaxY() const { return areaMaxY; } + + /** + * \brief Set the bottom bound of the variant. + */ + void SetAreaMaxY(int areaMaxY_) { areaMaxY = areaMaxY_; } + + /** + * \brief Get the max Z bound of the variant. + * + * This is used only if there is any initial instances. + * + * \see EventsBasedObjectVariant::GetInitialInstances + */ + int GetAreaMaxZ() const { return areaMaxZ; } + + /** + * \brief Set the bottom bound of the variant. + */ + void SetAreaMaxZ(int areaMaxZ_) { areaMaxZ = areaMaxZ_; } + ///@} + + /** \brief Change the object asset store id of this variant. + */ + void SetAssetStoreAssetId(const gd::String &assetStoreId_) { + assetStoreAssetId = assetStoreId_; + }; + + /** \brief Return the object asset store id of this variant. + */ + const gd::String &GetAssetStoreAssetId() const { return assetStoreAssetId; }; + + /** \brief Change the original name of the variant in the asset. + */ + void SetAssetStoreOriginalName(const gd::String &assetStoreOriginalName_) { + assetStoreOriginalName = assetStoreOriginalName_; + }; + + /** \brief Return the original name of the variant in the asset. + */ + const gd::String &GetAssetStoreOriginalName() const { + return assetStoreOriginalName; + }; + + void SerializeTo(SerializerElement &element) const; + + void UnserializeFrom(gd::Project &project, const SerializerElement &element); + +private: + gd::String name; + gd::InitialInstancesContainer initialInstances; + gd::LayersContainer layers; + gd::ObjectsContainer objectsContainer; + double areaMinX; + double areaMinY; + double areaMinZ; + double areaMaxX; + double areaMaxY; + double areaMaxZ; + /** + * The ID of the asset if the object comes from the store. + */ + gd::String assetStoreAssetId; + /** + * The original name of the variant in the asset if the object comes from the + * store. + */ + gd::String assetStoreOriginalName; +}; + +} // namespace gd diff --git a/Core/GDCore/Project/EventsBasedObjectVariantsContainer.h b/Core/GDCore/Project/EventsBasedObjectVariantsContainer.h new file mode 100644 index 000000000000..44f35effd328 --- /dev/null +++ b/Core/GDCore/Project/EventsBasedObjectVariantsContainer.h @@ -0,0 +1,160 @@ +/* + * GDevelop Core + * Copyright 2008-present Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#pragma once + +#include <vector> +#include "GDCore/Project/EventsBasedObjectVariant.h" +#include "GDCore/String.h" +#include "GDCore/Tools/SerializableWithNameList.h" + +namespace gd { +class SerializerElement; +} + +namespace gd { + +/** + * \brief Used as a base class for classes that will own events-backed + * variants. + * + * \see gd::EventsBasedObjectVariantContainer + * \ingroup PlatformDefinition + */ +class GD_CORE_API EventsBasedObjectVariantsContainer + : private SerializableWithNameList<gd::EventsBasedObjectVariant> { +public: + EventsBasedObjectVariantsContainer() {} + + EventsBasedObjectVariantsContainer(const EventsBasedObjectVariantsContainer &other) { + Init(other); + } + + EventsBasedObjectVariantsContainer &operator=(const EventsBasedObjectVariantsContainer &other) { + if (this != &other) { + Init(other); + } + return *this; + } + + /** \name Events Functions management + */ + ///@{ + /** + * \brief Check if the variant with the specified name exists. + */ + bool HasVariantNamed(const gd::String& name) const { + return Has(name); + } + + /** + * \brief Get the variant with the specified name. + * + * \warning Trying to access to a not existing variant will result in + * undefined behavior. + */ + gd::EventsBasedObjectVariant& GetVariant(const gd::String& name) { + return Get(name); + } + + /** + * \brief Get the variant with the specified name. + * + * \warning Trying to access to a not existing variant will result in + * undefined behavior. + */ + const gd::EventsBasedObjectVariant& GetVariant(const gd::String& name) const { + return Get(name); + } + + /** + * \brief Get the variant at the specified index in the list. + * + * \warning Trying to access to a not existing variant will result in + * undefined behavior. + */ + gd::EventsBasedObjectVariant& GetVariant(std::size_t index) { + return Get(index); + } + + /** + * \brief Get the variant at the specified index in the list. + * + * \warning Trying to access to a not existing variant will result in + * undefined behavior. + */ + const gd::EventsBasedObjectVariant& GetVariant(std::size_t index) const { + return Get(index); + } + + /** + * \brief Return the number of variants. + */ + std::size_t GetVariantsCount() const { return GetCount(); } + + gd::EventsBasedObjectVariant& InsertNewVariant(const gd::String& name, + std::size_t position) { + return InsertNew(name, position); + } + gd::EventsBasedObjectVariant& InsertVariant(const gd::EventsBasedObjectVariant& object, + std::size_t position) { + return Insert(object, position); + } + void RemoveVariant(const gd::String& name) { return Remove(name); } + void ClearVariants() { return Clear(); } + void MoveVariant(std::size_t oldIndex, std::size_t newIndex) { + return Move(oldIndex, newIndex); + }; + std::size_t GetVariantPosition(const gd::EventsBasedObjectVariant& eventsFunction) { + return GetPosition(eventsFunction); + }; + + /** + * \brief Provide a raw access to the vector containing the variants. + */ + const std::vector<std::unique_ptr<gd::EventsBasedObjectVariant>>& GetInternalVector() + const { + return elements; + }; + + /** + * \brief Provide a raw access to the vector containing the variants. + */ + std::vector<std::unique_ptr<gd::EventsBasedObjectVariant>>& GetInternalVector() { + return elements; + }; + ///@} + + /** \name Serialization + */ + ///@{ + /** + * \brief Serialize events variants. + */ + void SerializeVariantsTo(SerializerElement& element) const { + return SerializeElementsTo("variant", element); + }; + + /** + * \brief Unserialize the events variants. + */ + void UnserializeVariantsFrom(gd::Project& project, + const SerializerElement& element) { + return UnserializeElementsFrom("variant", project, element); + }; + ///@} + protected: + /** + * Initialize object using another object. Used by copy-ctor and assign-op. + * Don't forget to update me if members were changed! + */ + void Init(const gd::EventsBasedObjectVariantsContainer& other) { + return SerializableWithNameList<gd::EventsBasedObjectVariant>::Init(other); + }; + +private: +}; + +} // namespace gd diff --git a/Core/GDCore/Project/EventsFunctionsExtension.cpp b/Core/GDCore/Project/EventsFunctionsExtension.cpp index 445ada4c69f3..13c71ebf4c3b 100644 --- a/Core/GDCore/Project/EventsFunctionsExtension.cpp +++ b/Core/GDCore/Project/EventsFunctionsExtension.cpp @@ -55,7 +55,7 @@ void EventsFunctionsExtension::Init(const gd::EventsFunctionsExtension& other) { sceneVariables = other.GetSceneVariables(); } -void EventsFunctionsExtension::SerializeTo(SerializerElement& element) const { +void EventsFunctionsExtension::SerializeTo(SerializerElement& element, bool isExternal) const { element.SetAttribute("version", version); element.SetAttribute("extensionNamespace", extensionNamespace); element.SetAttribute("shortDescription", shortDescription); @@ -102,8 +102,18 @@ void EventsFunctionsExtension::SerializeTo(SerializerElement& element) const { element.AddChild("eventsFunctions")); eventsBasedBehaviors.SerializeElementsTo( "eventsBasedBehavior", element.AddChild("eventsBasedBehaviors")); - eventsBasedObjects.SerializeElementsTo( - "eventsBasedObject", element.AddChild("eventsBasedObjects")); + if (isExternal) { + auto &eventsBasedObjectElement = element.AddChild("eventsBasedObjects"); + eventsBasedObjectElement.ConsiderAsArrayOf("eventsBasedObject"); + for (const auto &eventsBasedObject : + eventsBasedObjects.GetInternalVector()) { + eventsBasedObject->SerializeToExternal( + eventsBasedObjectElement.AddChild("eventsBasedObject")); + } + } else { + eventsBasedObjects.SerializeElementsTo( + "eventsBasedObject", element.AddChild("eventsBasedObjects")); + } } void EventsFunctionsExtension::UnserializeFrom( diff --git a/Core/GDCore/Project/EventsFunctionsExtension.h b/Core/GDCore/Project/EventsFunctionsExtension.h index 90e5fdd4b690..bf714c3fb3ae 100644 --- a/Core/GDCore/Project/EventsFunctionsExtension.h +++ b/Core/GDCore/Project/EventsFunctionsExtension.h @@ -286,7 +286,14 @@ class GD_CORE_API EventsFunctionsExtension { /** * \brief Serialize the EventsFunctionsExtension to the specified element */ - void SerializeTo(gd::SerializerElement& element) const; + void SerializeTo(gd::SerializerElement& element, bool isExternal = false) const; + + /** + * \brief Serialize the EventsFunctionsExtension to the specified element + */ + void SerializeToExternal(gd::SerializerElement& element) const { + SerializeTo(element, true); + } /** * \brief Load the EventsFunctionsExtension from the specified element. diff --git a/Core/GDCore/Project/InitialInstancesContainer.cpp b/Core/GDCore/Project/InitialInstancesContainer.cpp index a6455d0258c7..e7cc5b0edcce 100644 --- a/Core/GDCore/Project/InitialInstancesContainer.cpp +++ b/Core/GDCore/Project/InitialInstancesContainer.cpp @@ -42,8 +42,13 @@ void InitialInstancesContainer::IterateOverInstances( } void InitialInstancesContainer::IterateOverInstances( - const std::function< void(gd::InitialInstance &) >& func) { - for (auto& instance : initialInstances) func(instance); + const std::function< bool(gd::InitialInstance &) >& func) { + for (auto& instance : initialInstances) { + bool shouldStop = func(instance); + if (shouldStop) { + return; + } + } } void InitialInstancesContainer::IterateOverInstancesWithZOrdering( diff --git a/Core/GDCore/Project/InitialInstancesContainer.h b/Core/GDCore/Project/InitialInstancesContainer.h index 173db82af392..2bb3c3a4c693 100644 --- a/Core/GDCore/Project/InitialInstancesContainer.h +++ b/Core/GDCore/Project/InitialInstancesContainer.h @@ -92,7 +92,7 @@ class GD_CORE_API InitialInstancesContainer { * \see InitialInstanceFunctor */ void IterateOverInstances( - const std::function< void(gd::InitialInstance &) >& func); + const std::function< bool(gd::InitialInstance &) >& func); /** * Get the instances on the specified layer, diff --git a/Core/GDCore/Project/Object.cpp b/Core/GDCore/Project/Object.cpp index abf226c987c8..04c678ad5e0a 100644 --- a/Core/GDCore/Project/Object.cpp +++ b/Core/GDCore/Project/Object.cpp @@ -41,6 +41,11 @@ Object::Object(const gd::String& name_, } void Object::Init(const gd::Object& object) { + CopyWithoutConfiguration(object); + configuration = object.configuration->Clone(); +} + +void Object::CopyWithoutConfiguration(const gd::Object& object) { persistentUuid = object.persistentUuid; name = object.name; assetStoreId = object.assetStoreId; @@ -51,8 +56,6 @@ void Object::Init(const gd::Object& object) { for (auto& it : object.behaviors) { behaviors[it.first] = gd::make_unique<gd::Behavior>(*it.second); } - - configuration = object.configuration->Clone(); } gd::ObjectConfiguration& Object::GetConfiguration() { return *configuration; } diff --git a/Core/GDCore/Project/Object.h b/Core/GDCore/Project/Object.h index bfe4a5540a80..a9eaeb354205 100644 --- a/Core/GDCore/Project/Object.h +++ b/Core/GDCore/Project/Object.h @@ -82,6 +82,8 @@ class GD_CORE_API Object { return gd::make_unique<gd::Object>(*this); } + void CopyWithoutConfiguration(const gd::Object& object); + /** * \brief Return the object configuration. */ diff --git a/Core/GDCore/Project/Project.cpp b/Core/GDCore/Project/Project.cpp index 7db30a67b7a5..d9e98563d549 100644 --- a/Core/GDCore/Project/Project.cpp +++ b/Core/GDCore/Project/Project.cpp @@ -920,6 +920,7 @@ void Project::UnserializeAndInsertExtensionsFrom( "eventsFunctionsExtension"); std::map<gd::String, size_t> extensionNameToElementIndex; + std::map<gd::String, gd::SerializerElement> objectTypeToVariantsElement; // First, only unserialize behaviors and objects names. // As event based objects can contains custom behaviors and custom objects, @@ -938,6 +939,16 @@ void Project::UnserializeAndInsertExtensionsFrom( ? GetEventsFunctionsExtension(name) : InsertNewEventsFunctionsExtension( name, GetEventsFunctionsExtensionsCount()); + + // Backup the events-based object variants + for (auto &eventsBasedObject : + eventsFunctionsExtension.GetEventsBasedObjects().GetInternalVector()) { + gd::SerializerElement variantsElement; + eventsBasedObject->GetVariants().SerializeVariantsTo(variantsElement); + objectTypeToVariantsElement[gd::PlatformExtension::GetObjectFullType( + name, eventsBasedObject->GetName())] = variantsElement; + } + eventsFunctionsExtension.UnserializeExtensionDeclarationFrom( *this, eventsFunctionsExtensionElement); } @@ -966,6 +977,15 @@ void Project::UnserializeAndInsertExtensionsFrom( partiallyLoadedExtension ->UnserializeExtensionImplementationFrom( *this, eventsFunctionsExtensionElement); + + for (auto &pair : objectTypeToVariantsElement) { + auto &objectType = pair.first; + auto &variantsElement = pair.second; + + auto &eventsBasedObject = GetEventsBasedObject(objectType); + eventsBasedObject.GetVariants().UnserializeVariantsFrom(*this, + variantsElement); + } } } diff --git a/Core/tests/EventsBasedObjectVariantHelper.cpp b/Core/tests/EventsBasedObjectVariantHelper.cpp new file mode 100644 index 000000000000..163ba281e6a5 --- /dev/null +++ b/Core/tests/EventsBasedObjectVariantHelper.cpp @@ -0,0 +1,522 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +/** + * @file Tests covering events of GDevelop Core. + */ +#include "catch.hpp" + +#include <algorithm> +#include <initializer_list> +#include <map> + +#include "GDCore/CommonTools.h" +#include "GDCore/IDE/EventsBasedObjectVariantHelper.h" + +#include "DummyPlatform.h" +#include "GDCore/Extensions/Platform.h" +#include "GDCore/Extensions/PlatformExtension.h" +#include "GDCore/Project/Behavior.h" +#include "GDCore/Project/EventsFunctionsExtension.h" +#include "GDCore/Project/Object.h" +#include "GDCore/Project/Project.h" +#include "GDCore/Project/ProjectScopedContainers.h" +#include "GDCore/Project/Variable.h" +#include "catch.hpp" + +gd::InitialInstance * +GetFirstInstanceOf(const gd::String objectName, + gd::InitialInstancesContainer &initialInstances) { + gd::InitialInstance *variantInstance = nullptr; + initialInstances.IterateOverInstances( + [&variantInstance, &objectName](gd::InitialInstance &instance) { + if (instance.GetObjectName() == objectName) { + variantInstance = &instance; + return true; + } + return false; + }); + return variantInstance; +} + +gd::EventsBasedObject &SetupEventsBasedObject(gd::Project &project) { + auto &eventsExtension = + project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0); + auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew( + "MyEventsBasedObject", 0); + auto &object = eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject", 0); + object.GetVariables().InsertNew("MyVariable").SetValue(123); + object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior"); + auto &instance = + eventsBasedObject.GetInitialInstances().InsertNewInitialInstance(); + instance.SetObjectName("MyChildObject"); + instance.GetVariables().InsertNew("MyVariable").SetValue(111); + auto &objectGroup = + eventsBasedObject.GetObjects().GetObjectGroups().InsertNew( + "MyObjectGroup"); + objectGroup.AddObject("MyChildObject"); + return eventsBasedObject; +} + +TEST_CASE("EventsBasedObjectVariantHelper", "[common]") { + SECTION("Can add missing objects") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject2", 0); + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject3", 0); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject2")); + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject3")); + } + + SECTION("Can remove objects") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject2", 0); + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject3", 0); + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + variant.GetInitialInstances().InsertNewInitialInstance().SetObjectName( + "MyChildObject2"); + REQUIRE(variant.GetInitialInstances().HasInstancesOfObject( + "MyChildObject2") == true); + + // Do the changes and launch the refactoring. + eventsBasedObject.GetObjects().RemoveObject("MyChildObject2"); + eventsBasedObject.GetObjects().RemoveObject("MyChildObject3"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject2") == false); + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject3") == false); + REQUIRE(variant.GetInitialInstances().HasInstancesOfObject( + "MyChildObject2") == false); + } + + SECTION("Can change object type") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + eventsBasedObject.GetObjects().RemoveObject("MyChildObject"); + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::FakeObjectWithDefaultBehavior", "MyChildObject", + 0); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + REQUIRE(variant.GetObjects().GetObject("MyChildObject").GetType() == + "MyExtension::FakeObjectWithDefaultBehavior"); + REQUIRE(variant.GetInitialInstances().GetInstancesCount() == 1); + } + + SECTION("Can add missing object groups") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + eventsBasedObject.GetObjects().GetObjectGroups().InsertNew("MyObjectGroup2", + 0); + eventsBasedObject.GetObjects() + .GetObjectGroups() + .InsertNew("MyObjectGroup3", 0) + .AddObject("MyChildObject"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + auto &variantObjectGroups = variant.GetObjects().GetObjectGroups(); + REQUIRE(variantObjectGroups.Has("MyObjectGroup")); + REQUIRE(variantObjectGroups.Has("MyObjectGroup2")); + REQUIRE(variantObjectGroups.Has("MyObjectGroup3")); + REQUIRE( + variantObjectGroups.Get("MyObjectGroup").GetAllObjectsNames().size() == + 1); + REQUIRE( + variantObjectGroups.Get("MyObjectGroup2").GetAllObjectsNames().size() == + 0); + REQUIRE( + variantObjectGroups.Get("MyObjectGroup3").GetAllObjectsNames().size() == + 1); + } + + SECTION("Can remove object groups") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + + // Do the changes and launch the refactoring. + eventsBasedObject.GetObjects().GetObjectGroups().InsertNew("MyObjectGroup2", + 0); + eventsBasedObject.GetObjects().GetObjectGroups().InsertNew("MyObjectGroup3", + 0); + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + eventsBasedObject.GetObjects().GetObjectGroups().Remove("MyObjectGroup2"); + eventsBasedObject.GetObjects().GetObjectGroups().Remove("MyObjectGroup3"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + auto &variantObjectGroups = variant.GetObjects().GetObjectGroups(); + REQUIRE(variantObjectGroups.Has("MyObjectGroup")); + REQUIRE(variantObjectGroups.Has("MyObjectGroup2") == false); + REQUIRE(variantObjectGroups.Has("MyObjectGroup3") == false); + } + + SECTION("Can add objects to groups") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject2", 0); + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject3", 0); + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + auto &objectGroup = + eventsBasedObject.GetObjects().GetObjectGroups().Get("MyObjectGroup"); + objectGroup.AddObject("MyChildObject2"); + objectGroup.AddObject("MyChildObject3"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + auto &variantObjectGroups = variant.GetObjects().GetObjectGroups(); + REQUIRE(variantObjectGroups.Has("MyObjectGroup")); + REQUIRE( + variantObjectGroups.Get("MyObjectGroup").GetAllObjectsNames().size() == + 3); + } + + SECTION("Can remove objects from groups") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject2", 0); + eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject3", 0); + auto &objectGroup = + eventsBasedObject.GetObjects().GetObjectGroups().Get("MyObjectGroup"); + objectGroup.AddObject("MyChildObject2"); + objectGroup.AddObject("MyChildObject3"); + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + objectGroup.RemoveObject("MyChildObject2"); + objectGroup.RemoveObject("MyChildObject3"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + auto &variantObjectGroups = variant.GetObjects().GetObjectGroups(); + REQUIRE(variantObjectGroups.Has("MyObjectGroup")); + REQUIRE( + variantObjectGroups.Get("MyObjectGroup").GetAllObjectsNames().size() == + 1); + } + + SECTION("Can add missing behaviors") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior2"); + object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior3"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.HasBehaviorNamed("MyBehavior")); + REQUIRE(variantObject.HasBehaviorNamed("MyBehavior2")); + REQUIRE(variantObject.HasBehaviorNamed("MyBehavior3")); + } + + SECTION("Can remove missing behaviors") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior2"); + object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior3"); + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + object.RemoveBehavior("MyBehavior2"); + object.RemoveBehavior("MyBehavior3"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.HasBehaviorNamed("MyBehavior")); + REQUIRE(variantObject.HasBehaviorNamed("MyBehavior2") == false); + REQUIRE(variantObject.HasBehaviorNamed("MyBehavior3") == false); + } + + SECTION("Can change behavior type") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + object.RemoveBehavior("MyBehavior"); + object.AddNewBehavior(project, "MyExtension::MyOtherBehavior", + "MyBehavior"); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.HasBehaviorNamed("MyBehavior")); + REQUIRE(variantObject.GetBehavior("MyBehavior").GetTypeName() == + "MyExtension::MyOtherBehavior"); + } + + SECTION("Can add missing variables") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + object.GetVariables().InsertNew("MyVariable2", 1).SetValue(456); + object.GetVariables().InsertNew("MyVariable3", 2).SetValue(789); + + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.GetVariables().Get("MyVariable").GetValue() == 123); + REQUIRE(variantObject.GetVariables().Get("MyVariable2").GetValue() == 456); + REQUIRE(variantObject.GetVariables().Get("MyVariable3").GetValue() == 789); + { + auto *objectInstance = + GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances()); + REQUIRE(objectInstance != nullptr); + REQUIRE(objectInstance->GetVariables().Has("MyVariable")); + REQUIRE(objectInstance->GetVariables().Has("MyVariable2") == false); + REQUIRE(objectInstance->GetVariables().Has("MyVariable3") == false); + } + } + + SECTION("Can keep variable value") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + + // Do the changes and launch the refactoring. + object.GetVariables().Get("MyVariable").SetValue(456); + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.GetVariables().Get("MyVariable").GetValue() == 123); + { + auto *objectInstance = + GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances()); + REQUIRE(objectInstance != nullptr); + REQUIRE(objectInstance->GetVariables().Get("MyVariable").GetValue() == + 111); + } + } + + SECTION("Must not propagate instance variable value changes") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + { + auto *objectInstance = GetFirstInstanceOf( + "MyChildObject", eventsBasedObject.GetInitialInstances()); + REQUIRE(objectInstance != nullptr); + objectInstance->GetVariables().Get("MyVariable").SetValue(222); + } + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + { + auto *objectInstance = + GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances()); + REQUIRE(objectInstance != nullptr); + REQUIRE(objectInstance->GetVariables().Get("MyVariable").GetValue() == + 111); + } + } + + SECTION("Can move variables") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + object.GetVariables().InsertNew("MyVariable2", 1).SetValue(456); + object.GetVariables().InsertNew("MyVariable3", 2).SetValue(789); + { + auto *objectInstance = GetFirstInstanceOf( + "MyChildObject", eventsBasedObject.GetInitialInstances()); + REQUIRE(objectInstance != nullptr); + objectInstance->GetVariables().Get("MyVariable2").SetValue(222); + objectInstance->GetVariables().Get("MyVariable3").SetValue(333); + } + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + + // Do the changes and launch the refactoring. + object.GetVariables().Move(2, 0); + object.GetVariables().Get("MyVariable").SetValue(111); + object.GetVariables().Get("MyVariable2").SetValue(222); + object.GetVariables().Get("MyVariable3").SetValue(333); + REQUIRE(object.GetVariables().GetNameAt(0) == "MyVariable3"); + REQUIRE(object.GetVariables().GetNameAt(1) == "MyVariable"); + REQUIRE(object.GetVariables().GetNameAt(2) == "MyVariable2"); + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.GetVariables().Get("MyVariable").GetValue() == 123); + REQUIRE(variantObject.GetVariables().Get("MyVariable2").GetValue() == 456); + REQUIRE(variantObject.GetVariables().Get("MyVariable3").GetValue() == 789); + REQUIRE(variantObject.GetVariables().GetNameAt(0) == "MyVariable3"); + REQUIRE(variantObject.GetVariables().GetNameAt(1) == "MyVariable"); + REQUIRE(variantObject.GetVariables().GetNameAt(2) == "MyVariable2"); + } + + SECTION("Can remove variables") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + object.GetVariables().InsertNew("MyVariable2", 1).SetValue(456); + object.GetVariables().InsertNew("MyVariable3", 2).SetValue(789); + { + auto *objectInstance = GetFirstInstanceOf( + "MyChildObject", eventsBasedObject.GetInitialInstances()); + REQUIRE(objectInstance != nullptr); + objectInstance->GetVariables().Get("MyVariable2").SetValue(222); + objectInstance->GetVariables().Get("MyVariable3").SetValue(333); + } + + // Do the changes and launch the refactoring. + object.GetVariables().Remove("MyVariable2"); + object.GetVariables().Remove("MyVariable3"); + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.GetVariables().Has("MyVariable")); + REQUIRE(variantObject.GetVariables().Has("MyVariable2") == false); + REQUIRE(variantObject.GetVariables().Has("MyVariable3") == false); + { + auto *objectInstance = + GetFirstInstanceOf("MyChildObject", variant.GetInitialInstances()); + REQUIRE(objectInstance != nullptr); + REQUIRE(objectInstance->GetVariables().Has("MyVariable")); + REQUIRE(objectInstance->GetVariables().Has("MyVariable2") == false); + REQUIRE(objectInstance->GetVariables().Has("MyVariable3") == false); + } + } + + SECTION("Can change variable type") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + auto &eventsBasedObject = SetupEventsBasedObject(project); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + auto &object = eventsBasedObject.GetObjects().GetObject("MyChildObject"); + + // Do the changes and launch the refactoring. + object.GetVariables().Get("MyVariable").SetString("abc"); + gd::EventsBasedObjectVariantHelper::ComplyVariantsToEventsBasedObject( + project, eventsBasedObject); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + auto &variantObject = variant.GetObjects().GetObject("MyChildObject"); + REQUIRE(variantObject.GetVariables().Get("MyVariable").GetString() == + "abc"); + REQUIRE(variant.GetInitialInstances().HasInstancesOfObject("MyVariable") == + false); + } +} diff --git a/Core/tests/ObjectAssetSerializer.cpp b/Core/tests/ObjectAssetSerializer.cpp index 15b51993f1e2..7f6e1932b2d1 100644 --- a/Core/tests/ObjectAssetSerializer.cpp +++ b/Core/tests/ObjectAssetSerializer.cpp @@ -59,6 +59,8 @@ TEST_CASE("ObjectAssetSerializer", "[common]") { auto &configuration = object.GetConfiguration(); auto *customObjectConfiguration = dynamic_cast<gd::CustomObjectConfiguration *>(&configuration); + customObjectConfiguration + ->SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration(true); auto *spriteConfiguration = dynamic_cast<gd::SpriteObject *>( &customObjectConfiguration->GetChildObjectConfiguration("MyChild")); REQUIRE(spriteConfiguration != nullptr); diff --git a/Core/tests/ObjectsContainersList.cpp b/Core/tests/ObjectsContainersList.cpp index f4692f82984a..4dab4c7bc98f 100644 --- a/Core/tests/ObjectsContainersList.cpp +++ b/Core/tests/ObjectsContainersList.cpp @@ -150,7 +150,7 @@ TEST_CASE("ObjectContainersList (GetTypeOfObject)", "[common]") { gd::Object &object1 = layout.GetObjects().InsertNewObject( project, "MyExtension::Sprite", "MyObject1", 0); gd::Object &object2 = layout.GetObjects().InsertNewObject( - project, "FakeObjectWithDefaultBehavior", "MyObject2", 0); + project, "MyExtension::FakeObjectWithDefaultBehavior", "MyObject2", 0); auto &group = layout.GetObjects().GetObjectGroups().InsertNew("MyGroup", 0); group.AddObject(object1.GetName()); diff --git a/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp b/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp index 8acf965c2618..50aabdf0d556 100644 --- a/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp +++ b/Core/tests/WholeProjectRefactorer-ApplyRefactoringForVariablesContainer.cpp @@ -1072,6 +1072,99 @@ TEST_CASE("WholeProjectRefactorer::ApplyRefactoringForVariablesContainer", REQUIRE(instance.GetVariables().Get("MyRenamedVariable").GetValue() == 456); } + SECTION("Can rename an object variable (in events-based object)") { + gd::Project project; + gd::Platform platform; + SetupProjectWithDummyPlatform(project, platform); + + auto &eventsExtension = + project.InsertNewEventsFunctionsExtension("MyEventsExtension", 0); + auto &eventsBasedObject = eventsExtension.GetEventsBasedObjects().InsertNew( + "MyEventsBasedObject", 0); + auto &object = eventsBasedObject.GetObjects().InsertNewObject( + project, "MyExtension::Sprite", "MyChildObject", 0); + object.GetVariables().InsertNew("MyVariable").SetValue(123); + auto &instance = + eventsBasedObject.GetInitialInstances().InsertNewInitialInstance(); + instance.SetObjectName("MyChildObject"); + instance.GetVariables().InsertNew("MyVariable").SetValue(456); + + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + gd::InitialInstance *variantInstance = nullptr; + variant.GetInitialInstances().IterateOverInstances( + [&variantInstance](gd::InitialInstance &instance) { + variantInstance = &instance; + return true; + }); + REQUIRE(variantInstance != nullptr); + variant.GetObjects() + .GetObject("MyChildObject") + .GetVariables() + .Get("MyVariable") + .SetValue(111); + variantInstance->GetVariables().Get("MyVariable").SetValue(222); + + auto &objectFunction = + eventsBasedObject.GetEventsFunctions().InsertNewEventsFunction( + "MyObjectEventsFunction", 0); + gd::StandardEvent &event = dynamic_cast<gd::StandardEvent &>( + objectFunction.GetEvents().InsertNewEvent( + project, "BuiltinCommonInstructions::Standard")); + + { + gd::Instruction action; + action.SetType("SetNumberObjectVariable"); + action.SetParametersCount(4); + action.SetParameter(0, gd::Expression("MyChildObject")); + action.SetParameter(1, gd::Expression("MyVariable")); + action.SetParameter(2, gd::Expression("=")); + action.SetParameter(3, gd::Expression("MyChildObject.MyVariable")); + event.GetActions().Insert(action); + } + + // Do the changes and launch the refactoring. + object.GetVariables().ResetPersistentUuid(); + gd::SerializerElement originalSerializedVariables; + object.GetVariables().SerializeTo(originalSerializedVariables); + + object.GetVariables().Rename("MyVariable", "MyRenamedVariable"); + auto changeset = + gd::WholeProjectRefactorer::ComputeChangesetForVariablesContainer( + originalSerializedVariables, object.GetVariables()); + + REQUIRE(changeset.oldToNewVariableNames.size() == 1); + + gd::WholeProjectRefactorer::ApplyRefactoringForObjectVariablesContainer( + project, object.GetVariables(), eventsBasedObject.GetInitialInstances(), + object.GetName(), changeset, originalSerializedVariables); + gd::ObjectVariableHelper::ApplyChangesToVariants( + eventsBasedObject, "MyChildObject", changeset); + + REQUIRE(event.GetActions()[0].GetParameter(1).GetPlainString() == + "MyRenamedVariable"); + REQUIRE(event.GetActions()[0].GetParameter(3).GetPlainString() == + "MyChildObject.MyRenamedVariable"); + + REQUIRE(eventsBasedObject.GetObjects().HasObjectNamed("MyChildObject")); + REQUIRE(eventsBasedObject.GetObjects() + .GetObject("MyChildObject") + .GetVariables() + .Get("MyRenamedVariable") + .GetValue() == 123); + REQUIRE(instance.GetVariables().Get("MyRenamedVariable").GetValue() == 456); + + REQUIRE(variant.GetObjects().HasObjectNamed("MyChildObject")); + REQUIRE(variant.GetObjects() + .GetObject("MyChildObject") + .GetVariables() + .Get("MyRenamedVariable") + .GetValue() == 111); + REQUIRE( + variantInstance->GetVariables().Get("MyRenamedVariable").GetValue() == + 222); + } + SECTION("Can delete an object variable") { gd::Project project; gd::Platform platform; diff --git a/Core/tests/WholeProjectRefactorer.cpp b/Core/tests/WholeProjectRefactorer.cpp index 5b6018774724..d1dba17216e2 100644 --- a/Core/tests/WholeProjectRefactorer.cpp +++ b/Core/tests/WholeProjectRefactorer.cpp @@ -1754,6 +1754,9 @@ TEST_CASE("WholeProjectRefactorer", "[common]") { eventsBasedObject.GetObjects().InsertNewObject( project, "MyExtension::Sprite", "Object2", 0); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + // Create the objects container for the events function gd::ObjectsContainer parametersObjectsContainer( gd::ObjectsContainer::SourceType::Function); @@ -1765,6 +1768,10 @@ TEST_CASE("WholeProjectRefactorer", "[common]") { gd::WholeProjectRefactorer::ObjectOrGroupRenamedInEventsBasedObject( project, projectScopedContainers, eventsBasedObject, "Object1", "Object3", /* isObjectGroup =*/false); + + REQUIRE(variant.GetObjects().HasObjectNamed("Object1") == false); + REQUIRE(variant.GetObjects().HasObjectNamed("Object2") == true); + REQUIRE(variant.GetObjects().HasObjectNamed("Object3") == true); REQUIRE(eventsBasedObject.GetObjects().GetObjectGroups().size() == 1); REQUIRE(eventsBasedObject.GetObjects().GetObjectGroups()[0].Find( "Object1") == false); @@ -1772,6 +1779,17 @@ TEST_CASE("WholeProjectRefactorer", "[common]") { "Object2") == true); REQUIRE(eventsBasedObject.GetObjects().GetObjectGroups()[0].Find( "Object3") == true); + + REQUIRE(variant.GetObjects().HasObjectNamed("Object1") == false); + REQUIRE(variant.GetObjects().HasObjectNamed("Object2") == true); + REQUIRE(variant.GetObjects().HasObjectNamed("Object3") == true); + REQUIRE(variant.GetObjects().GetObjectGroups().size() == 1); + REQUIRE(variant.GetObjects().GetObjectGroups()[0].Find("Object1") == + false); + REQUIRE(variant.GetObjects().GetObjectGroups()[0].Find("Object2") == + true); + REQUIRE(variant.GetObjects().GetObjectGroups()[0].Find("Object3") == + true); } SECTION("Initial instances") { @@ -1796,6 +1814,9 @@ TEST_CASE("WholeProjectRefactorer", "[common]") { eventsBasedObject.GetInitialInstances().InsertInitialInstance(instance1); eventsBasedObject.GetInitialInstances().InsertInitialInstance(instance2); + auto &variant = eventsBasedObject.GetVariants().InsertVariant( + eventsBasedObject.GetDefaultVariant(), 0); + // Create the objects container for the events function gd::ObjectsContainer parametersObjectsContainer( gd::ObjectsContainer::SourceType::Function); @@ -1807,10 +1828,16 @@ TEST_CASE("WholeProjectRefactorer", "[common]") { gd::WholeProjectRefactorer::ObjectOrGroupRenamedInEventsBasedObject( project, projectScopedContainers, eventsBasedObject, "Object1", "Object3", /* isObjectGroup =*/false); + REQUIRE(eventsBasedObject.GetInitialInstances().HasInstancesOfObject( "Object1") == false); REQUIRE(eventsBasedObject.GetInitialInstances().HasInstancesOfObject( "Object3") == true); + + REQUIRE(variant.GetInitialInstances().HasInstancesOfObject("Object1") == + false); + REQUIRE(variant.GetInitialInstances().HasInstancesOfObject("Object3") == + true); } SECTION("Events") { diff --git a/Extensions/BBText/JsExtension.js b/Extensions/BBText/JsExtension.js index edf65e9dfb33..cf7dbc9cba8b 100644 --- a/Extensions/BBText/JsExtension.js +++ b/Extensions/BBText/JsExtension.js @@ -485,14 +485,16 @@ module.exports = { instance, associatedObjectConfiguration, pixiContainer, - pixiResourcesLoader + pixiResourcesLoader, + propertyOverridings ) { super( project, instance, associatedObjectConfiguration, pixiContainer, - pixiResourcesLoader + pixiResourcesLoader, + propertyOverridings ); const bbTextStyles = { @@ -532,7 +534,9 @@ module.exports = { gd.ObjectJsImplementation ); - const rawText = object.content.text; + const rawText = this._propertyOverridings.has('Text') + ? this._propertyOverridings.get('Text') + : object.content.text; if (rawText !== this._pixiObject.text) { this._pixiObject.text = rawText; } diff --git a/Extensions/BitmapText/JsExtension.js b/Extensions/BitmapText/JsExtension.js index 69b6a88c8ac0..c38cd256ec9b 100644 --- a/Extensions/BitmapText/JsExtension.js +++ b/Extensions/BitmapText/JsExtension.js @@ -624,14 +624,16 @@ module.exports = { instance, associatedObjectConfiguration, pixiContainer, - pixiResourcesLoader + pixiResourcesLoader, + propertyOverridings ) { super( project, instance, associatedObjectConfiguration, pixiContainer, - pixiResourcesLoader + pixiResourcesLoader, + propertyOverridings ); // We'll track changes of the font to trigger the loading of the new font. @@ -657,8 +659,9 @@ module.exports = { // Update the rendered text properties (note: Pixi is only // applying changes if there were changed). - const rawText = object.content.text; - this._pixiObject.text = rawText; + this._pixiObject.text = this._propertyOverridings.has('Text') + ? this._propertyOverridings.get('Text') + : object.content.text; const align = object.content.align; this._pixiObject.align = align; diff --git a/Extensions/JsExtensionTypes.d.ts b/Extensions/JsExtensionTypes.d.ts index fcfacbd9a5d7..8621802b7c41 100644 --- a/Extensions/JsExtensionTypes.d.ts +++ b/Extensions/JsExtensionTypes.d.ts @@ -15,6 +15,7 @@ class RenderedInstance { _pixiContainer: PIXI.Container; _pixiResourcesLoader: Class<PixiResourcesLoader>; _pixiObject: PIXI.DisplayObject | null; + _propertyOverridings: Map<string, string>; wasUsed: boolean; /** Set to true when onRemovedFromScene is called. Allows to cancel promises/asynchronous operations (notably: waiting for a resource load). */ @@ -25,7 +26,8 @@ class RenderedInstance { instance: gdInitialInstance, associatedObjectConfiguration: gdObjectConfiguration, pixiContainer: PIXI.Container, - pixiResourcesLoader: Class<PixiResourcesLoader> + pixiResourcesLoader: Class<PixiResourcesLoader>, + propertyOverridings: Map<string, string> = new Map<string, string>() ); /** diff --git a/GDJS/Runtime/CustomRuntimeObject.ts b/GDJS/Runtime/CustomRuntimeObject.ts index 3c597d8ce06c..09606cd6c23c 100644 --- a/GDJS/Runtime/CustomRuntimeObject.ts +++ b/GDJS/Runtime/CustomRuntimeObject.ts @@ -12,6 +12,7 @@ namespace gdjs { export type CustomObjectConfiguration = ObjectConfiguration & { animatable?: SpriteAnimationData[]; + variant: string; childrenContent: { [objectName: string]: ObjectConfiguration & any }; }; @@ -92,34 +93,50 @@ namespace gdjs { } private _initializeFromObjectData( - objectData: ObjectData & CustomObjectConfiguration + customObjectData: ObjectData & CustomObjectConfiguration ) { const eventsBasedObjectData = this._runtimeScene .getGame() - .getEventsBasedObjectData(objectData.type); + .getEventsBasedObjectData(customObjectData.type); if (!eventsBasedObjectData) { logger.error( - `A CustomRuntimeObject was initialized (or re-initialized) from object data referring to an non existing events based object data with type "${objectData.type}".` + `A CustomRuntimeObject was initialized (or re-initialized) from object data referring to an non existing events based object data with type "${customObjectData.type}".` ); return; } + + let usedVariantData: EventsBasedObjectVariantData = eventsBasedObjectData; + if (customObjectData.variant) { + for ( + let variantIndex = 0; + variantIndex < eventsBasedObjectData.variants.length; + variantIndex++ + ) { + const variantData = eventsBasedObjectData.variants[variantIndex]; + if (variantData.name === customObjectData.variant) { + usedVariantData = variantData; + break; + } + } + } + this._isInnerAreaFollowingParentSize = eventsBasedObjectData.isInnerAreaFollowingParentSize; - if (eventsBasedObjectData.instances.length > 0) { + if (usedVariantData.instances.length > 0) { if (!this._innerArea) { this._innerArea = { min: [0, 0, 0], max: [0, 0, 0], }; } - this._innerArea.min[0] = eventsBasedObjectData.areaMinX; - this._innerArea.min[1] = eventsBasedObjectData.areaMinY; - this._innerArea.min[2] = eventsBasedObjectData.areaMinZ; - this._innerArea.max[0] = eventsBasedObjectData.areaMaxX; - this._innerArea.max[1] = eventsBasedObjectData.areaMaxY; - this._innerArea.max[2] = eventsBasedObjectData.areaMaxZ; - } - this._instanceContainer.loadFrom(objectData, eventsBasedObjectData); + this._innerArea.min[0] = usedVariantData.areaMinX; + this._innerArea.min[1] = usedVariantData.areaMinY; + this._innerArea.min[2] = usedVariantData.areaMinZ; + this._innerArea.max[0] = usedVariantData.areaMaxX; + this._innerArea.max[1] = usedVariantData.areaMaxY; + this._innerArea.max[2] = usedVariantData.areaMaxZ; + } + this._instanceContainer.loadFrom(customObjectData, usedVariantData); } protected abstract _createRender(): diff --git a/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts b/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts index 0268d3ca689f..ecd28bd89bc1 100644 --- a/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts +++ b/GDJS/Runtime/CustomRuntimeObjectInstanceContainer.ts @@ -65,21 +65,21 @@ namespace gdjs { */ loadFrom( customObjectData: ObjectData & CustomObjectConfiguration, - eventsBasedObjectData: EventsBasedObjectData + eventsBasedObjectVariantData: EventsBasedObjectVariantData ) { if (this._isLoaded) { this.onDestroyFromScene(this._parent); } - this._setOriginalInnerArea(eventsBasedObjectData); + this._setOriginalInnerArea(eventsBasedObjectVariantData); // Registering objects for ( - let i = 0, len = eventsBasedObjectData.objects.length; + let i = 0, len = eventsBasedObjectVariantData.objects.length; i < len; ++i ) { - const childObjectData = eventsBasedObjectData.objects[i]; + const childObjectData = eventsBasedObjectVariantData.objects[i]; if (customObjectData.childrenContent) { this.registerObject({ ...childObjectData, @@ -92,14 +92,14 @@ namespace gdjs { } } - if (eventsBasedObjectData.layers.length > 0) { + if (eventsBasedObjectVariantData.layers.length > 0) { // Load layers for ( - let i = 0, len = eventsBasedObjectData.layers.length; + let i = 0, len = eventsBasedObjectVariantData.layers.length; i < len; ++i ) { - this.addLayer(eventsBasedObjectData.layers[i]); + this.addLayer(eventsBasedObjectVariantData.layers[i]); } } else { // Add a default layer @@ -128,7 +128,7 @@ namespace gdjs { } this.createObjectsFrom( - eventsBasedObjectData.instances, + eventsBasedObjectVariantData.instances, 0, 0, 0, @@ -147,7 +147,7 @@ namespace gdjs { * `_initialInnerArea` is shared by every instance to save memory. */ private _setOriginalInnerArea( - eventsBasedObjectData: EventsBasedObjectData + eventsBasedObjectData: EventsBasedObjectVariantData ) { if (eventsBasedObjectData.instances.length > 0) { if (!eventsBasedObjectData._initialInnerArea) { diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts index 8f4947b17911..193931138231 100644 --- a/GDJS/Runtime/types/project-data.d.ts +++ b/GDJS/Runtime/types/project-data.d.ts @@ -206,9 +206,16 @@ declare interface SceneAndExtensionsData { usedExtensionsWithVariablesData: EventsFunctionsExtensionData[]; } -declare interface EventsBasedObjectData extends InstanceContainerData { +declare interface EventsBasedObjectData + extends EventsBasedObjectVariantData, + InstanceContainerData { name: string; isInnerAreaFollowingParentSize: boolean; + variants: Array<EventsBasedObjectVariantData>; +} + +declare interface EventsBasedObjectVariantData extends InstanceContainerData { + name: string; // The flat representation of defaultSize. areaMinX: float; areaMinY: float; @@ -225,6 +232,9 @@ declare interface EventsBasedObjectData extends InstanceContainerData { min: [float, float, float]; max: [float, float, float]; } | null; + instances: InstanceData[]; + objects: ObjectData[]; + layers: LayerData[]; } declare interface BehaviorSharedData { diff --git a/GDJS/tests/tests/CustomRuntimeObject.js b/GDJS/tests/tests/CustomRuntimeObject.js index fac2cd9df477..d2910a4b8546 100644 --- a/GDJS/tests/tests/CustomRuntimeObject.js +++ b/GDJS/tests/tests/CustomRuntimeObject.js @@ -15,6 +15,7 @@ describe('gdjs.CustomRuntimeObject', function () { const customObject = new gdjs.CustomRuntimeObject2D(instanceContainer, { name: 'MyCustomObject', type: 'MyExtension::MyEventsBasedObject', + variant: '', variables: [], behaviors: [], effects: [], diff --git a/GDJS/tests/tests/hot-reloader.js b/GDJS/tests/tests/hot-reloader.js index e890af565085..5da4b89c2ed7 100644 --- a/GDJS/tests/tests/hot-reloader.js +++ b/GDJS/tests/tests/hot-reloader.js @@ -93,6 +93,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => { /** @type {ObjectData & gdjs.CustomObjectConfiguration} */ const defaultCustomObject = { type: 'MyExtension::MyCustomObject', + variant: '', name: 'MyCustomObject', behaviors: [], variables: [], @@ -214,6 +215,7 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => { areaMaxZ: 0, _initialInnerArea: null, isInnerAreaFollowingParentSize: false, + variants: [], }; }; diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index d823b9f28a86..9b9aee55292d 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -372,6 +372,16 @@ interface ObjectVariableHelper { [Ref] ObjectsContainer globalObjectsContainer, [Ref] ObjectsContainer objectsContainer, [Const, Ref] ObjectGroup objectGroup); + void STATIC_ApplyChangesToVariants( + [Ref] EventsBasedObject eventsBasedObject, + [Const] DOMString objectName, + [Const, Ref] VariablesChangeset changeset); +}; + +interface EventsBasedObjectVariantHelper { + void STATIC_ComplyVariantsToEventsBasedObject( + [Ref, Const] Project project, + [Ref] EventsBasedObject eventsBasedObject); }; interface ObjectGroupsContainer { @@ -923,6 +933,8 @@ enum CustomObjectConfiguration_EdgeAnchor { interface CustomObjectConfiguration { [Value] UniquePtrObjectConfiguration Clone(); + [Const, Ref] DOMString GetVariantName(); + void SetVariantName([Const] DOMString name); boolean IsForcedToOverrideEventsBasedObjectChildrenConfiguration(); boolean IsMarkedAsOverridingEventsBasedObjectChildrenConfiguration(); void SetMarkedAsOverridingEventsBasedObjectChildrenConfiguration( @@ -3145,6 +3157,9 @@ interface EventsBasedObject { [Ref] EventsBasedObject MakAsUsingLegacyInstancesRenderer(boolean value); boolean IsUsingLegacyInstancesRenderer(); + [Ref] EventsBasedObjectVariant GetDefaultVariant(); + [Ref] EventsBasedObjectVariantsContainer GetVariants(); + [Ref] InitialInstancesContainer GetInitialInstances(); [Ref] LayersContainer GetLayers(); [Ref] ObjectsContainer GetObjects(); @@ -3168,6 +3183,48 @@ interface EventsBasedObject { }; EventsBasedObject implements AbstractEventsBasedEntity; +interface EventsBasedObjectVariant { + void EventsBasedObjectVariant(); + + [Const, Ref] DOMString GetName(); + [Ref] EventsBasedObjectVariant SetName([Const] DOMString name); + + [Ref] InitialInstancesContainer GetInitialInstances(); + [Ref] LayersContainer GetLayers(); + [Ref] ObjectsContainer GetObjects(); + double GetAreaMinX(); + double GetAreaMinY(); + double GetAreaMinZ(); + double GetAreaMaxX(); + double GetAreaMaxY(); + double GetAreaMaxZ(); + void SetAreaMinX(double value); + void SetAreaMinY(double value); + void SetAreaMinZ(double value); + void SetAreaMaxX(double value); + void SetAreaMaxY(double value); + void SetAreaMaxZ(double value); + void SetAssetStoreAssetId([Const] DOMString assetStoreAssetId); + [Const, Ref] DOMString GetAssetStoreAssetId(); + void SetAssetStoreOriginalName([Const] DOMString assetStoreOriginalName); + [Const, Ref] DOMString GetAssetStoreOriginalName(); + + void SerializeTo([Ref] SerializerElement element); + void UnserializeFrom([Ref] Project project, [Const, Ref] SerializerElement element); +}; + +interface EventsBasedObjectVariantsContainer { + [Ref] EventsBasedObjectVariant InsertNewVariant([Const] DOMString name, unsigned long pos); + [Ref] EventsBasedObjectVariant InsertVariant([Const, Ref] EventsBasedObjectVariant variant, unsigned long pos); + boolean HasVariantNamed([Const] DOMString name); + [Ref] EventsBasedObjectVariant GetVariant([Const] DOMString name); + [Ref] EventsBasedObjectVariant GetVariantAt(unsigned long pos); + void RemoveVariant([Const] DOMString name); + void MoveVariant(unsigned long oldIndex, unsigned long newIndex); + unsigned long GetVariantsCount(); + unsigned long GetVariantPosition([Const, Ref] EventsBasedObjectVariant variant); +}; + interface EventsBasedObjectsList { [Ref] EventsBasedObject InsertNew([Const] DOMString name, unsigned long pos); [Ref] EventsBasedObject Insert([Const, Ref] EventsBasedObject item, unsigned long pos); @@ -3248,6 +3305,7 @@ interface EventsFunctionsExtension { [Ref] EventsBasedObjectsList GetEventsBasedObjects(); void SerializeTo([Ref] SerializerElement element); + void SerializeToExternal([Ref] SerializerElement element); void UnserializeFrom([Ref] Project project, [Const, Ref] SerializerElement element); boolean STATIC_IsExtensionLifecycleEventsFunction([Const] DOMString eventsFunctionName); diff --git a/GDevelop.js/Bindings/Wrapper.cpp b/GDevelop.js/Bindings/Wrapper.cpp index 5b62af751c75..16f01e2ad88f 100644 --- a/GDevelop.js/Bindings/Wrapper.cpp +++ b/GDevelop.js/Bindings/Wrapper.cpp @@ -47,6 +47,7 @@ #include <GDCore/IDE/Events/ExampleExtensionUsagesFinder.h> #include <GDCore/IDE/EventsFunctionTools.h> #include <GDCore/IDE/ObjectVariableHelper.h> +#include <GDCore/IDE/EventsBasedObjectVariantHelper.h> #include <GDCore/IDE/Project/ArbitraryResourceWorker.h> #include <GDCore/IDE/Project/ArbitraryObjectsWorker.h> #include <GDCore/IDE/Project/ObjectsUsingResourceCollector.h> @@ -725,6 +726,8 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata; ComputeChangesetForVariablesContainer #define STATIC_MergeVariableContainers MergeVariableContainers #define STATIC_FillAnyVariableBetweenObjects FillAnyVariableBetweenObjects +#define STATIC_ApplyChangesToVariants ApplyChangesToVariants +#define STATIC_ComplyVariantsToEventsBasedObject ComplyVariantsToEventsBasedObject #define STATIC_RenameEventsFunctionsExtension RenameEventsFunctionsExtension #define STATIC_UpdateExtensionNameInEventsBasedBehavior \ UpdateExtensionNameInEventsBasedBehavior @@ -844,6 +847,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata; #define RemoveEventAt RemoveEvent #define RemoveAt Remove #define GetEventsFunctionAt GetEventsFunction +#define GetVariantAt GetVariant #define GetEffectAt GetEffect #define GetParameterAt GetParameter diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index 470216816e96..439c6c4cd4d8 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -384,6 +384,11 @@ export class ObjectGroup extends EmscriptenObject { export class ObjectVariableHelper extends EmscriptenObject { static mergeVariableContainers(objectsContainersList: ObjectsContainersList, objectGroup: ObjectGroup): VariablesContainer; static fillAnyVariableBetweenObjects(globalObjectsContainer: ObjectsContainer, objectsContainer: ObjectsContainer, objectGroup: ObjectGroup): void; + static applyChangesToVariants(eventsBasedObject: EventsBasedObject, objectName: string, changeset: VariablesChangeset): void; +} + +export class EventsBasedObjectVariantHelper extends EmscriptenObject { + static complyVariantsToEventsBasedObject(project: Project, eventsBasedObject: EventsBasedObject): void; } export class ObjectGroupsContainer extends EmscriptenObject { @@ -768,6 +773,8 @@ export class ObjectJsImplementation extends ObjectConfiguration { export class CustomObjectConfiguration extends ObjectConfiguration { clone(): UniquePtrObjectConfiguration; + getVariantName(): string; + setVariantName(name: string): void; isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean; isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean; setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void; @@ -2270,6 +2277,8 @@ export class EventsBasedObject extends AbstractEventsBasedEntity { isInnerAreaFollowingParentSize(): boolean; makAsUsingLegacyInstancesRenderer(value: boolean): EventsBasedObject; isUsingLegacyInstancesRenderer(): boolean; + getDefaultVariant(): EventsBasedObjectVariant; + getVariants(): EventsBasedObjectVariantsContainer; getInitialInstances(): InitialInstancesContainer; getLayers(): LayersContainer; getObjects(): ObjectsContainer; @@ -2291,6 +2300,45 @@ export class EventsBasedObject extends AbstractEventsBasedEntity { static getPropertyToggleActionName(propertyName: string): string; } +export class EventsBasedObjectVariant extends EmscriptenObject { + constructor(); + getName(): string; + setName(name: string): EventsBasedObjectVariant; + getInitialInstances(): InitialInstancesContainer; + getLayers(): LayersContainer; + getObjects(): ObjectsContainer; + getAreaMinX(): number; + getAreaMinY(): number; + getAreaMinZ(): number; + getAreaMaxX(): number; + getAreaMaxY(): number; + getAreaMaxZ(): number; + setAreaMinX(value: number): void; + setAreaMinY(value: number): void; + setAreaMinZ(value: number): void; + setAreaMaxX(value: number): void; + setAreaMaxY(value: number): void; + setAreaMaxZ(value: number): void; + setAssetStoreAssetId(assetStoreAssetId: string): void; + getAssetStoreAssetId(): string; + setAssetStoreOriginalName(assetStoreOriginalName: string): void; + getAssetStoreOriginalName(): string; + serializeTo(element: SerializerElement): void; + unserializeFrom(project: Project, element: SerializerElement): void; +} + +export class EventsBasedObjectVariantsContainer extends EmscriptenObject { + insertNewVariant(name: string, pos: number): EventsBasedObjectVariant; + insertVariant(variant: EventsBasedObjectVariant, pos: number): EventsBasedObjectVariant; + hasVariantNamed(name: string): boolean; + getVariant(name: string): EventsBasedObjectVariant; + getVariantAt(pos: number): EventsBasedObjectVariant; + removeVariant(name: string): void; + moveVariant(oldIndex: number, newIndex: number): void; + getVariantsCount(): number; + getVariantPosition(variant: EventsBasedObjectVariant): number; +} + export class EventsBasedObjectsList extends EmscriptenObject { insertNew(name: string, pos: number): EventsBasedObject; insert(item: EventsBasedObject, pos: number): EventsBasedObject; @@ -2361,6 +2409,7 @@ export class EventsFunctionsExtension extends EmscriptenObject { getEventsBasedBehaviors(): EventsBasedBehaviorsList; getEventsBasedObjects(): EventsBasedObjectsList; serializeTo(element: SerializerElement): void; + serializeToExternal(element: SerializerElement): void; unserializeFrom(project: Project, element: SerializerElement): void; static isExtensionLifecycleEventsFunction(eventsFunctionName: string): boolean; } diff --git a/GDevelop.js/types/gdcustomobjectconfiguration.js b/GDevelop.js/types/gdcustomobjectconfiguration.js index 2dcee3d41e6f..c8324f34a650 100644 --- a/GDevelop.js/types/gdcustomobjectconfiguration.js +++ b/GDevelop.js/types/gdcustomobjectconfiguration.js @@ -6,6 +6,8 @@ declare class gdCustomObjectConfiguration extends gdObjectConfiguration { static Proportional: 3; static Center: 4; clone(): gdUniquePtrObjectConfiguration; + getVariantName(): string; + setVariantName(name: string): void; isForcedToOverrideEventsBasedObjectChildrenConfiguration(): boolean; isMarkedAsOverridingEventsBasedObjectChildrenConfiguration(): boolean; setMarkedAsOverridingEventsBasedObjectChildrenConfiguration(isOverridingEventsBasedObjectChildrenConfiguration: boolean): void; diff --git a/GDevelop.js/types/gdeventsbasedobject.js b/GDevelop.js/types/gdeventsbasedobject.js index 264fe4c61d95..bd9ffde0ec34 100644 --- a/GDevelop.js/types/gdeventsbasedobject.js +++ b/GDevelop.js/types/gdeventsbasedobject.js @@ -17,6 +17,8 @@ declare class gdEventsBasedObject extends gdAbstractEventsBasedEntity { isInnerAreaFollowingParentSize(): boolean; makAsUsingLegacyInstancesRenderer(value: boolean): gdEventsBasedObject; isUsingLegacyInstancesRenderer(): boolean; + getDefaultVariant(): gdEventsBasedObjectVariant; + getVariants(): gdEventsBasedObjectVariantsContainer; getInitialInstances(): gdInitialInstancesContainer; getLayers(): gdLayersContainer; getObjects(): gdObjectsContainer; diff --git a/GDevelop.js/types/gdeventsbasedobjectvariant.js b/GDevelop.js/types/gdeventsbasedobjectvariant.js new file mode 100644 index 000000000000..c47a8c0d9020 --- /dev/null +++ b/GDevelop.js/types/gdeventsbasedobjectvariant.js @@ -0,0 +1,29 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdEventsBasedObjectVariant { + constructor(): void; + getName(): string; + setName(name: string): gdEventsBasedObjectVariant; + getInitialInstances(): gdInitialInstancesContainer; + getLayers(): gdLayersContainer; + getObjects(): gdObjectsContainer; + getAreaMinX(): number; + getAreaMinY(): number; + getAreaMinZ(): number; + getAreaMaxX(): number; + getAreaMaxY(): number; + getAreaMaxZ(): number; + setAreaMinX(value: number): void; + setAreaMinY(value: number): void; + setAreaMinZ(value: number): void; + setAreaMaxX(value: number): void; + setAreaMaxY(value: number): void; + setAreaMaxZ(value: number): void; + setAssetStoreAssetId(assetStoreAssetId: string): void; + getAssetStoreAssetId(): string; + setAssetStoreOriginalName(assetStoreOriginalName: string): void; + getAssetStoreOriginalName(): string; + serializeTo(element: gdSerializerElement): void; + unserializeFrom(project: gdProject, element: gdSerializerElement): void; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdeventsbasedobjectvarianthelper.js b/GDevelop.js/types/gdeventsbasedobjectvarianthelper.js new file mode 100644 index 000000000000..81c04eb686cb --- /dev/null +++ b/GDevelop.js/types/gdeventsbasedobjectvarianthelper.js @@ -0,0 +1,6 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdEventsBasedObjectVariantHelper { + static complyVariantsToEventsBasedObject(project: gdProject, eventsBasedObject: gdEventsBasedObject): void; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdeventsbasedobjectvariantscontainer.js b/GDevelop.js/types/gdeventsbasedobjectvariantscontainer.js new file mode 100644 index 000000000000..60b58d259bbd --- /dev/null +++ b/GDevelop.js/types/gdeventsbasedobjectvariantscontainer.js @@ -0,0 +1,14 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdEventsBasedObjectVariantsContainer { + insertNewVariant(name: string, pos: number): gdEventsBasedObjectVariant; + insertVariant(variant: gdEventsBasedObjectVariant, pos: number): gdEventsBasedObjectVariant; + hasVariantNamed(name: string): boolean; + getVariant(name: string): gdEventsBasedObjectVariant; + getVariantAt(pos: number): gdEventsBasedObjectVariant; + removeVariant(name: string): void; + moveVariant(oldIndex: number, newIndex: number): void; + getVariantsCount(): number; + getVariantPosition(variant: gdEventsBasedObjectVariant): number; + delete(): void; + ptr: number; +}; \ No newline at end of file diff --git a/GDevelop.js/types/gdeventsfunctionsextension.js b/GDevelop.js/types/gdeventsfunctionsextension.js index f9d3cfbbe319..aff9ae689e82 100644 --- a/GDevelop.js/types/gdeventsfunctionsextension.js +++ b/GDevelop.js/types/gdeventsfunctionsextension.js @@ -40,6 +40,7 @@ declare class gdEventsFunctionsExtension { getEventsBasedBehaviors(): gdEventsBasedBehaviorsList; getEventsBasedObjects(): gdEventsBasedObjectsList; serializeTo(element: gdSerializerElement): void; + serializeToExternal(element: gdSerializerElement): void; unserializeFrom(project: gdProject, element: gdSerializerElement): void; static isExtensionLifecycleEventsFunction(eventsFunctionName: string): boolean; delete(): void; diff --git a/GDevelop.js/types/gdobjectvariablehelper.js b/GDevelop.js/types/gdobjectvariablehelper.js index dcfba80e0305..d4cd846f2442 100644 --- a/GDevelop.js/types/gdobjectvariablehelper.js +++ b/GDevelop.js/types/gdobjectvariablehelper.js @@ -2,6 +2,7 @@ declare class gdObjectVariableHelper { static mergeVariableContainers(objectsContainersList: gdObjectsContainersList, objectGroup: gdObjectGroup): gdVariablesContainer; static fillAnyVariableBetweenObjects(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectGroup: gdObjectGroup): void; + static applyChangesToVariants(eventsBasedObject: gdEventsBasedObject, objectName: string, changeset: gdVariablesChangeset): void; 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 2a000a10ca9c..c8b394b52454 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -74,6 +74,7 @@ declare class libGDevelop { VariablesContainersList: Class<gdVariablesContainersList>; ObjectGroup: Class<gdObjectGroup>; ObjectVariableHelper: Class<gdObjectVariableHelper>; + EventsBasedObjectVariantHelper: Class<gdEventsBasedObjectVariantHelper>; ObjectGroupsContainer: Class<gdObjectGroupsContainer>; PlatformSpecificAssets: Class<gdPlatformSpecificAssets>; LoadingScreen: Class<gdLoadingScreen>; @@ -222,6 +223,8 @@ declare class libGDevelop { EventsBasedBehavior: Class<gdEventsBasedBehavior>; EventsBasedBehaviorsList: Class<gdEventsBasedBehaviorsList>; EventsBasedObject: Class<gdEventsBasedObject>; + EventsBasedObjectVariant: Class<gdEventsBasedObjectVariant>; + EventsBasedObjectVariantsContainer: Class<gdEventsBasedObjectVariantsContainer>; EventsBasedObjectsList: Class<gdEventsBasedObjectsList>; PropertiesContainer: Class<gdPropertiesContainer>; EventsFunctionsExtension: Class<gdEventsFunctionsExtension>; diff --git a/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js b/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js index db0de291a4ec..dd6af042db26 100644 --- a/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js +++ b/newIDE/app/src/AssetStore/ExtensionStore/ExtensionsSearchDialog.js @@ -24,7 +24,7 @@ import ErrorBoundary from '../../UI/ErrorBoundary'; type Props = {| project: gdProject, onClose: () => void, - onInstallExtension: ExtensionShortHeader => void, + onInstallExtension: (extensionName: string) => void, onExtensionInstalled: (extensionName: string) => void, onCreateNew?: () => void, |}; @@ -63,7 +63,7 @@ const ExtensionsSearchDialog = ({ try { let installedOrImportedExtensionName: string | null = null; if (!!extensionShortHeader) { - onInstallExtension(extensionShortHeader); + onInstallExtension(extensionShortHeader.name); const wasExtensionInstalledOrImported = await installDisplayedExtension( i18n, project, @@ -77,7 +77,8 @@ const ExtensionsSearchDialog = ({ installedOrImportedExtensionName = await importExtension( i18n, eventsFunctionsExtensionsState, - project + project, + onInstallExtension ); } diff --git a/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js b/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js index f298e49c6257..af59f6691056 100644 --- a/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js +++ b/newIDE/app/src/AssetStore/ExtensionStore/InstallExtension.js @@ -47,7 +47,8 @@ export const installExtension = async ( export const importExtension = async ( i18n: I18nType, eventsFunctionsExtensionsState: EventsFunctionsExtensionsState, - project: gdProject + project: gdProject, + onWillInstallExtension: (extensionName: string) => void ): Promise<string | null> => { const eventsFunctionsExtensionOpener = eventsFunctionsExtensionsState.getEventsFunctionsExtensionOpener(); if (!eventsFunctionsExtensionOpener) return null; @@ -69,6 +70,8 @@ export const importExtension = async ( if (!answer) return null; } + onWillInstallExtension(serializedExtension.name); + await addSerializedExtensionsToProject( eventsFunctionsExtensionsState, project, diff --git a/newIDE/app/src/AssetStore/InstallAsset.js b/newIDE/app/src/AssetStore/InstallAsset.js index 86bfb6e90861..7fba144598a3 100644 --- a/newIDE/app/src/AssetStore/InstallAsset.js +++ b/newIDE/app/src/AssetStore/InstallAsset.js @@ -155,6 +155,23 @@ export type InstallAssetArgs = {| targetObjectFolderOrObject?: ?gdObjectFolderOrObject, |}; +const findVariant = ( + container: gdEventsBasedObjectVariantsContainer, + assetStoreAssetId: string, + assetStoreOriginalName: string +): gdEventsBasedObjectVariant | null => { + for (let index = 0; index < container.getVariantsCount(); index++) { + const variant = container.getVariantAt(index); + if ( + variant.getAssetStoreAssetId() === assetStoreAssetId && + variant.getAssetStoreOriginalName() === assetStoreOriginalName + ) { + return variant; + } + } + return null; +}; + export const addAssetToProject = async ({ asset, project, @@ -170,6 +187,88 @@ export const addAssetToProject = async ({ const type: ?string = objectAsset.object.type; if (!type) throw new Error('An object has no type specified'); + const variantRenamings: Array<{ + objectType: string, + oldVariantName: string, + newVariantName: string, + }> = []; + const serializedVariants = objectAsset.variants; + if (serializedVariants) { + // Install variants + for (const { + objectType, + variant: serializedVariant, + } of serializedVariants) { + if (project.hasEventsBasedObject(objectType)) { + const eventsBasedObject = project.getEventsBasedObject(objectType); + const variants = eventsBasedObject.getVariants(); + let variant = findVariant(variants, asset.id, serializedVariant.name); + if (!variant) { + // TODO Forbid name with `::` + const uniqueNewName = newNameGenerator( + serializedVariant.name || asset.name, + tentativeNewName => variants.hasVariantNamed(tentativeNewName) + ); + variant = variants.insertNewVariant( + uniqueNewName, + variants.getVariantsCount() + ); + const variantName = variant.getName(); + unserializeFromJSObject( + variant, + serializedVariant, + 'unserializeFrom', + project + ); + variant.setName(variantName); + variant.setAssetStoreAssetId(asset.id); + variant.setAssetStoreOriginalName(serializedVariant.name); + } + if (variant.getName() !== serializedVariant.name) { + variantRenamings.push({ + objectType, + oldVariantName: serializedVariant.name, + newVariantName: variant.getName(), + }); + } + } + } + // Update variant names into variants object configurations. + for (const { + objectType, + variant: serializedVariant, + } of serializedVariants) { + if (project.hasEventsBasedObject(objectType)) { + const eventsBasedObject = project.getEventsBasedObject(objectType); + const variants = eventsBasedObject.getVariants(); + let variant = findVariant(variants, asset.id, serializedVariant.name); + if (variant) { + for ( + let index = 0; + index < variant.getObjects().getObjectsCount(); + index++ + ) { + const object = variant.getObjects().getObjectAt(index); + + if (project.hasEventsBasedObject(object.getType())) { + const customObjectConfiguration = gd.asCustomObjectConfiguration( + object.getConfiguration() + ); + const customObjectVariantRenaming = variantRenamings.find( + renaming => renaming.objectType === object.getType() + ); + if (customObjectVariantRenaming) { + customObjectConfiguration.setVariantName( + customObjectVariantRenaming.newVariantName + ); + } + } + } + } + } + } + } + // Insert the object const originalName = sanitizeObjectName(objectAsset.object.name); const newName = newNameGenerator(originalName, name => @@ -204,10 +303,27 @@ export const addAssetToProject = async ({ 'unserializeFrom', project ); - - object.setAssetStoreId(asset.id); // The name was overwritten after unserialization. object.setName(newName); + object.setAssetStoreId(asset.id); + if (project.hasEventsBasedObject(object.getType())) { + const customObjectConfiguration = gd.asCustomObjectConfiguration( + object.getConfiguration() + ); + if (customObjectConfiguration.getVariantName()) { + customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration( + false + ); + } + const customObjectVariantRenaming = variantRenamings.find( + renaming => renaming.objectType === object.getType() + ); + if (customObjectVariantRenaming) { + customObjectConfiguration.setVariantName( + customObjectVariantRenaming.newVariantName + ); + } + } // Add resources used by the object objectAsset.resources.forEach(serializedResource => { diff --git a/newIDE/app/src/BehaviorsEditor/index.js b/newIDE/app/src/BehaviorsEditor/index.js index 401d1aba8bc9..af638417068c 100644 --- a/newIDE/app/src/BehaviorsEditor/index.js +++ b/newIDE/app/src/BehaviorsEditor/index.js @@ -45,6 +45,7 @@ import CopyIcon from '../UI/CustomSvgIcons/Copy'; import ResponsiveFlatButton from '../UI/ResponsiveFlatButton'; import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; import QuickCustomizationPropertiesVisibilityDialog from '../QuickCustomization/QuickCustomizationPropertiesVisibilityDialog'; +import Text from '../UI/Text'; const gd: libGDevelop = global.gd; @@ -80,6 +81,7 @@ type BehaviorConfigurationEditorProps = {| openBehaviorPropertiesQuickCustomizationDialog: ( behaviorName: string ) => void, + isListLocked: boolean, |}; const BehaviorConfigurationEditor = React.forwardRef< @@ -100,6 +102,7 @@ const BehaviorConfigurationEditor = React.forwardRef< pasteBehaviors, openExtension, openBehaviorPropertiesQuickCustomizationDialog, + isListLocked, }, ref ) => { @@ -195,6 +198,7 @@ const BehaviorConfigurationEditor = React.forwardRef< { label: i18n._(t`Delete`), click: () => onRemoveBehavior(behaviorName), + enabled: !isListLocked, }, { label: i18n._(t`Copy`), @@ -203,7 +207,8 @@ const BehaviorConfigurationEditor = React.forwardRef< { label: i18n._(t`Paste`), click: pasteBehaviors, - enabled: canPasteBehaviors, + // TODO Allow to paste behaviors that are already in the list. + enabled: canPasteBehaviors && !isListLocked, }, ...(project.hasEventsBasedBehavior(behaviorTypeName) ? [ @@ -618,6 +623,7 @@ type Props = {| behaviorName: string ) => Promise<void>, onExtensionInstalled: (extensionName: string) => void, + isListLocked: boolean, |}; const BehaviorsEditor = (props: Props) => { @@ -636,6 +642,7 @@ const BehaviorsEditor = (props: Props) => { onUpdateBehaviorsSharedData, openBehaviorEvents, onExtensionInstalled, + isListLocked, } = props; const forceUpdate = useForceUpdate(); @@ -725,30 +732,41 @@ const BehaviorsEditor = (props: Props) => { return ( <Column noMargin expand useFullHeight noOverflowParent> {allVisibleBehaviors.length === 0 ? ( - <Column noMargin expand justifyContent="center"> - <EmptyPlaceholder - title={<Trans>Add your first behavior</Trans>} - description={ - <Trans> - Behaviors add features to objects in a matter of clicks. - </Trans> - } - helpPagePath="/behaviors" - tutorialId="intro-behaviors-and-functions" - actionButtonId="add-behavior-button" - actionLabel={ - isMobile ? <Trans>Add</Trans> : <Trans>Add a behavior</Trans> - } - onAction={openNewBehaviorDialog} - secondaryActionIcon={<PasteIcon />} - secondaryActionLabel={ - isClipboardContainingBehaviors ? <Trans>Paste</Trans> : null - } - onSecondaryAction={() => { - pasteBehaviors(); - }} - /> - </Column> + isListLocked ? ( + <Column noMargin expand justifyContent="center"> + <Text size="block-title" align="center"> + <Trans>No behavior</Trans> + </Text> + <Text align="center" noMargin> + <Trans>There is no behavior to set up for this object.</Trans> + </Text> + </Column> + ) : ( + <Column noMargin expand justifyContent="center"> + <EmptyPlaceholder + title={<Trans>Add your first behavior</Trans>} + description={ + <Trans> + Behaviors add features to objects in a matter of clicks. + </Trans> + } + helpPagePath="/behaviors" + tutorialId="intro-behaviors-and-functions" + actionButtonId="add-behavior-button" + actionLabel={ + isMobile ? <Trans>Add</Trans> : <Trans>Add a behavior</Trans> + } + onAction={openNewBehaviorDialog} + secondaryActionIcon={<PasteIcon />} + secondaryActionLabel={ + isClipboardContainingBehaviors ? <Trans>Paste</Trans> : null + } + onSecondaryAction={() => { + pasteBehaviors(); + }} + /> + </Column> + ) ) : ( <React.Fragment> <ScrollView ref={scrollView}> @@ -778,6 +796,7 @@ const BehaviorsEditor = (props: Props) => { canPasteBehaviors={isClipboardContainingBehaviors} pasteBehaviors={pasteBehaviors} resourceManagementProps={props.resourceManagementProps} + isListLocked={isListLocked} /> ); })} @@ -806,7 +825,7 @@ const BehaviorsEditor = (props: Props) => { onClick={() => { pasteBehaviors(); }} - disabled={!isClipboardContainingBehaviors} + disabled={!isClipboardContainingBehaviors || isListLocked} /> </LineStackLayout> <LineStackLayout justifyContent="flex-end" expand> @@ -823,6 +842,7 @@ const BehaviorsEditor = (props: Props) => { onClick={openNewBehaviorDialog} icon={<Add />} id="add-behavior-button" + disabled={isListLocked} /> </LineStackLayout> </LineStackLayout> diff --git a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js b/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js index f236671075eb..e6b16e220c89 100644 --- a/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js +++ b/newIDE/app/src/EventsBasedObjectEditor/EventsBasedObjectEditorPanel.js @@ -21,7 +21,9 @@ type Props = {| onEventsFunctionsAdded: () => void, onOpenCustomObjectEditor: () => void, unsavedChanges?: ?UnsavedChanges, - onEventsBasedObjectChildrenEdited: () => void, + onEventsBasedObjectChildrenEdited: ( + eventsBasedObject: gdEventsBasedObject + ) => void, |}; export default function EventsBasedObjectEditorPanel({ diff --git a/newIDE/app/src/EventsBasedObjectEditor/index.js b/newIDE/app/src/EventsBasedObjectEditor/index.js index 77548b1b81ed..559eb17e32d6 100644 --- a/newIDE/app/src/EventsBasedObjectEditor/index.js +++ b/newIDE/app/src/EventsBasedObjectEditor/index.js @@ -23,7 +23,9 @@ type Props = {| eventsBasedObject: gdEventsBasedObject, onOpenCustomObjectEditor: () => void, unsavedChanges?: ?UnsavedChanges, - onEventsBasedObjectChildrenEdited: () => void, + onEventsBasedObjectChildrenEdited: ( + eventsBasedObject: gdEventsBasedObject + ) => void, |}; export default function EventsBasedObjectEditor({ @@ -127,7 +129,7 @@ export default function EventsBasedObjectEditor({ onCheck={(e, checked) => { eventsBasedObject.markAsInnerAreaFollowingParentSize(checked); onChange(); - onEventsBasedObjectChildrenEdited(); + onEventsBasedObjectChildrenEdited(eventsBasedObject); }} /> {isDev && ( @@ -137,7 +139,7 @@ export default function EventsBasedObjectEditor({ onCheck={(e, checked) => { eventsBasedObject.makAsUsingLegacyInstancesRenderer(checked); onChange(); - onEventsBasedObjectChildrenEdited(); + onEventsBasedObjectChildrenEdited(eventsBasedObject); }} /> )} @@ -147,7 +149,7 @@ export default function EventsBasedObjectEditor({ onCheck={(e, checked) => { eventsBasedObject.setPrivate(checked); onChange(); - onEventsBasedObjectChildrenEdited(); + onEventsBasedObjectChildrenEdited(eventsBasedObject); }} tooltipOrHelperText={ eventsBasedObject.isPrivate() ? ( diff --git a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js index 78c695040f4a..4570a2048b71 100644 --- a/newIDE/app/src/EventsFunctionsExtensionEditor/index.js +++ b/newIDE/app/src/EventsFunctionsExtensionEditor/index.js @@ -77,7 +77,9 @@ type Props = {| unsavedChanges?: ?UnsavedChanges, onOpenCustomObjectEditor: gdEventsBasedObject => void, hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, - onEventsBasedObjectChildrenEdited: () => void, + onEventsBasedObjectChildrenEdited: ( + eventsBasedObject: gdEventsBasedObject + ) => void, onRenamedEventsBasedObject: ( eventsFunctionsExtension: gdEventsFunctionsExtension, oldName: string, @@ -778,7 +780,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< // Some custom object instances may target the pasted event-based object name. // It can happen when an event-based object is deleted and another one is // pasted to replace it. - this.props.onEventsBasedObjectChildrenEdited(); + this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject); }; _onEventsBasedBehaviorRenamed = () => { @@ -797,7 +799,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< } }; - _onEventsBasedObjectRenamed = () => { + _onEventsBasedObjectRenamed = (eventsBasedObject: gdEventsBasedObject) => { // Name of an object changed, so notify parent // that an object was edited (to trigger reload of extensions) if (this.props.onObjectEdited) { @@ -814,7 +816,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< // Some custom object instances may target the new event-based object name. // It can happen when an event-based object is deleted and another one is // renamed to replace it. - this.props.onEventsBasedObjectChildrenEdited(); + this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject); }; _onDeleteEventsBasedBehavior = ( @@ -853,7 +855,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< eventsFunctionsExtension, eventsBasedObject.getName() ); - onEventsBasedObjectChildrenEdited(); + onEventsBasedObjectChildrenEdited(eventsBasedObject); }; _onCloseExtensionFunctionSelectorDialog = ( @@ -1702,6 +1704,7 @@ export default class EventsFunctionsExtensionEditor extends React.Component< onCancel={() => this._editVariables(null)} onApply={() => this._editVariables(null)} hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps} + isListLocked={false} /> )} {objectMethodSelectorDialogOpen && selectedEventsBasedObject && ( diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js index b483b074af8a..e0ae4e2a5589 100644 --- a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js +++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/BrowserEventsFunctionsExtensionWriter.js @@ -32,7 +32,10 @@ export default class BrowserEventsFunctionsExtensionWriter { extension: gdEventsFunctionsExtension, filename: string ): Promise<void> => { - const serializedObject = serializeToJSObject(extension); + const serializedObject = serializeToJSObject( + extension, + 'serializeToExternal' + ); try { await downloadStringContentAsFile( filename, diff --git a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js index c1742bd489b9..c55f5acde32e 100644 --- a/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js +++ b/newIDE/app/src/EventsFunctionsExtensionsLoader/Storage/LocalEventsFunctionsExtensionWriter.js @@ -56,7 +56,10 @@ export default class LocalEventsFunctionsExtensionWriter { extension: gdEventsFunctionsExtension, filepath: string ): Promise<void> => { - const serializedObject = serializeToJSObject(extension); + const serializedObject = serializeToJSObject( + extension, + 'serializeToExternal' + ); return writeJSONFile(serializedObject, filepath).catch(err => { console.error('Unable to write the events function extension:', err); throw err; diff --git a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js index 2c2147d19566..227a04199919 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableField.js @@ -133,6 +133,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( initiallySelectedVariableName={editorOpen.variableName} shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate} hotReloadPreviewButtonProps={null} + isListLocked={false} /> )} </React.Fragment> diff --git a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js index 3f3cce7dcf58..7c8621c27449 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyField.js @@ -131,6 +131,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( initiallySelectedVariableName={editorOpen.variableName} shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate} hotReloadPreviewButtonProps={null} + isListLocked={false} /> )} </React.Fragment> diff --git a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js index f38c6268723a..3da9da8b72f4 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/AnyVariableOrPropertyOrParameterField.js @@ -127,6 +127,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( initiallySelectedVariableName={editorOpen.variableName} shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate} hotReloadPreviewButtonProps={null} + isListLocked={false} /> )} </React.Fragment> diff --git a/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js index c0c70ffff18e..24fa3bfe697e 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/GlobalVariableField.js @@ -84,6 +84,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( initiallySelectedVariableName={editorOpen.variableName} shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate} hotReloadPreviewButtonProps={null} + isListLocked={false} /> )} </React.Fragment> diff --git a/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js index ed0b1ec079b0..43a28ac1c9b7 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/ObjectVariableField.js @@ -227,6 +227,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate} onComputeAllVariableNames={onComputeAllVariableNames} hotReloadPreviewButtonProps={null} + isListLocked={false} /> )} {editorOpen && @@ -246,6 +247,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( initiallySelectedVariableName={editorOpen.variableName} shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate} onComputeAllVariableNames={onComputeAllVariableNames} + isListLocked={false} /> )} </React.Fragment> diff --git a/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js b/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js index fff2370e2c4d..44ed5a825f3b 100644 --- a/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js +++ b/newIDE/app/src/EventsSheet/ParameterFields/SceneVariableField.js @@ -110,6 +110,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( editorOpen.shouldCreate || false } hotReloadPreviewButtonProps={null} + isListLocked={false} /> )} {editorOpen && eventsFunctionsExtension && !layout && ( @@ -122,6 +123,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>( initiallySelectedVariableName={editorOpen.variableName} shouldCreateInitiallySelectedVariable={editorOpen.shouldCreate} hotReloadPreviewButtonProps={null} + isListLocked={false} /> )} </React.Fragment> diff --git a/newIDE/app/src/EventsSheet/index.js b/newIDE/app/src/EventsSheet/index.js index d2fb99241e27..2851f232e47a 100644 --- a/newIDE/app/src/EventsSheet/index.js +++ b/newIDE/app/src/EventsSheet/index.js @@ -2158,6 +2158,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component< shouldCreateInitiallySelectedVariable={ this.state.editedVariable.shouldCreateVariable } + isListLocked={false} /> )} {this.state.layoutVariablesDialogOpen && ( @@ -2167,6 +2168,7 @@ export class EventsSheetComponentWithoutHandle extends React.Component< onCancel={() => this.editLayoutVariables(false)} onApply={() => this.editLayoutVariables(false)} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} + isListLocked={false} /> )} {this.state.textEditedEvent && ( diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js index 39a698aff1ef..50ce02ae534e 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js @@ -64,6 +64,7 @@ type Props = {| historyHandler?: HistoryHandler, tileMapTileSelection: ?TileMapTileSelection, onSelectTileMapTile: (?TileMapTileSelection) => void, + isVariableListLocked: boolean, |}; export const CompactInstancePropertiesEditor = ({ @@ -83,6 +84,7 @@ export const CompactInstancePropertiesEditor = ({ projectScopedContainersAccessor, tileMapTileSelection, onSelectTileMapTile, + isVariableListLocked, }: Props) => { const forceUpdate = useForceUpdate(); const variablesListRef = React.useRef<?VariablesListInterface>(null); @@ -276,16 +278,18 @@ export const CompactInstancePropertiesEditor = ({ > <ShareExternal style={styles.icon} /> </IconButton> - <IconButton - size="small" - onClick={ - variablesListRef.current - ? variablesListRef.current.addVariable - : undefined - } - > - <Add style={styles.icon} /> - </IconButton> + {isVariableListLocked ? null : ( + <IconButton + size="small" + onClick={ + variablesListRef.current + ? variablesListRef.current.addVariable + : undefined + } + > + <Add style={styles.icon} /> + </IconButton> + )} </Line> </Line> </Column> @@ -314,6 +318,7 @@ export const CompactInstancePropertiesEditor = ({ compactEmptyPlaceholderText={ <Trans>There are no variables on this instance.</Trans> } + isListLocked={isVariableListLocked} /> </> ) : null} diff --git a/newIDE/app/src/InstancesEditor/WindowBorder.js b/newIDE/app/src/InstancesEditor/WindowBorder.js index 471ebb22f20e..c15082658479 100644 --- a/newIDE/app/src/InstancesEditor/WindowBorder.js +++ b/newIDE/app/src/InstancesEditor/WindowBorder.js @@ -7,14 +7,14 @@ import Rectangle from '../Utils/Rectangle'; type Props = {| project: gdProject, layout: gdLayout | null, - eventsBasedObject: gdEventsBasedObject | null, + eventsBasedObjectVariant: gdEventsBasedObjectVariant | null, toCanvasCoordinates: (x: number, y: number) => [number, number], |}; export default class WindowBorder { project: gdProject; layout: gdLayout | null; - eventsBasedObject: gdEventsBasedObject | null; + eventsBasedObjectVariant: gdEventsBasedObjectVariant | null; toCanvasCoordinates: (x: number, y: number) => [number, number]; pixiRectangle = new PIXI.Graphics(); windowRectangle: Rectangle = new Rectangle(); @@ -22,12 +22,12 @@ export default class WindowBorder { constructor({ project, layout, - eventsBasedObject, + eventsBasedObjectVariant, toCanvasCoordinates, }: Props) { this.project = project; this.layout = layout; - this.eventsBasedObject = eventsBasedObject; + this.eventsBasedObjectVariant = eventsBasedObjectVariant; this.toCanvasCoordinates = toCanvasCoordinates; this.pixiRectangle.hitArea = new PIXI.Rectangle(0, 0, 0, 0); @@ -38,15 +38,15 @@ export default class WindowBorder { } render() { - const { layout, eventsBasedObject } = this; + const { layout, eventsBasedObjectVariant } = this; this.windowRectangle.set( - eventsBasedObject + eventsBasedObjectVariant ? { - left: eventsBasedObject.getAreaMinX(), - top: eventsBasedObject.getAreaMinY(), - right: eventsBasedObject.getAreaMaxX(), - bottom: eventsBasedObject.getAreaMaxY(), + left: eventsBasedObjectVariant.getAreaMinX(), + top: eventsBasedObjectVariant.getAreaMinY(), + right: eventsBasedObjectVariant.getAreaMaxX(), + bottom: eventsBasedObjectVariant.getAreaMaxY(), } : { left: 0, @@ -88,7 +88,7 @@ export default class WindowBorder { displayedRectangle.width(), displayedRectangle.height() ); - if (eventsBasedObject) { + if (eventsBasedObjectVariant) { const origin = this.toCanvasCoordinates(0, 0); this.pixiRectangle.drawRect(origin[0] - 8, origin[1] - 1, 16, 2); this.pixiRectangle.drawRect(origin[0] - 1, origin[1] - 8, 2, 16); diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js index 2a47c7bc6060..0b5110b804fd 100644 --- a/newIDE/app/src/InstancesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/index.js @@ -91,6 +91,7 @@ export type InstancesEditorPropsWithoutSizeAndScroll = {| project: gdProject, layout: gdLayout | null, eventsBasedObject: gdEventsBasedObject | null, + eventsBasedObjectVariant: gdEventsBasedObjectVariant | null, layersContainer: gdLayersContainer, globalObjectsContainer: gdObjectsContainer | null, objectsContainer: gdObjectsContainer, @@ -564,7 +565,7 @@ export default class InstancesEditor extends Component<Props, State> { this.windowBorder = new WindowBorder({ project: props.project, layout: props.layout, - eventsBasedObject: props.eventsBasedObject, + eventsBasedObjectVariant: props.eventsBasedObjectVariant, toCanvasCoordinates: this.viewPosition.toCanvasCoordinates, }); this.windowMask = new WindowMask({ @@ -1477,13 +1478,13 @@ export default class InstancesEditor extends Component<Props, State> { }; _getAreaRectangle = (): Rectangle => { - const { eventsBasedObject, project } = this.props; - return eventsBasedObject + const { eventsBasedObjectVariant, project } = this.props; + return eventsBasedObjectVariant ? new Rectangle( - eventsBasedObject.getAreaMinX(), - eventsBasedObject.getAreaMinY(), - eventsBasedObject.getAreaMaxX(), - eventsBasedObject.getAreaMaxY() + eventsBasedObjectVariant.getAreaMinX(), + eventsBasedObjectVariant.getAreaMinY(), + eventsBasedObjectVariant.getAreaMaxX(), + eventsBasedObjectVariant.getAreaMaxY() ) : new Rectangle( 0, diff --git a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js index 368e2c5eae15..9049d4ab8e00 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js +++ b/newIDE/app/src/MainFrame/EditorContainers/BaseEditor.js @@ -62,7 +62,8 @@ export type RenderEditorContainerProps = {| ) => void, onOpenCustomObjectEditor: ( gdEventsFunctionsExtension, - gdEventsBasedObject + gdEventsBasedObject, + variantName: string ) => void, openObjectEvents: (extensionName: string, objectName: string) => void, @@ -136,7 +137,9 @@ export type RenderEditorContainerProps = {| // Object editing openBehaviorEvents: (extensionName: string, behaviorName: string) => void, - onEventsBasedObjectChildrenEdited: () => void, + onEventsBasedObjectChildrenEdited: ( + eventsBasedObject: gdEventsBasedObject + ) => void, onSceneObjectEdited: ( scene: gdLayout, objectWithContext: ObjectWithContext @@ -151,7 +154,17 @@ export type RenderEditorContainerProps = {| extensionName: string, eventsBasedObjectName: string ) => void, + onOpenEventBasedObjectVariantEditor: ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => void, onExtensionInstalled: (extensionName: string) => void, + onDeleteEventsBasedObjectVariant: ( + eventsFunctionsExtension: gdEventsFunctionsExtension, + eventBasedObject: gdEventsBasedObject, + variant: gdEventsBasedObjectVariant + ) => void, |}; export type RenderEditorContainerPropsWithRef = {| diff --git a/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js index 4f646b8da719..7d5af6a40f2e 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/CustomObjectEditorContainer.js @@ -120,9 +120,7 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon getEventsFunctionsExtension(): ?gdEventsFunctionsExtension { const { project, projectItemName } = this.props; if (!project || !projectItemName) return null; - const extensionName = gd.PlatformExtension.getExtensionFromFullObjectType( - projectItemName - ); + const extensionName = projectItemName.split('::')[0] || ''; if (!project.hasEventsFunctionsExtensionNamed(extensionName)) { return null; @@ -144,9 +142,7 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon const extension = this.getEventsFunctionsExtension(); if (!extension) return null; - const eventsBasedObjectName = gd.PlatformExtension.getObjectNameFromFullObjectType( - projectItemName - ); + const eventsBasedObjectName = projectItemName.split('::')[1] || ''; if (!extension.getEventsBasedObjects().has(eventsBasedObjectName)) { return null; @@ -154,6 +150,24 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon return extension.getEventsBasedObjects().get(eventsBasedObjectName); } + getVariantName(): string { + const { projectItemName } = this.props; + return (projectItemName && projectItemName.split('::')[2]) || ''; + } + + getVariant(): ?gdEventsBasedObjectVariant { + const { project, projectItemName } = this.props; + if (!project || !projectItemName) return null; + + const eventsBasedObject = this.getEventsBasedObject(); + if (!eventsBasedObject) return null; + + const variantName = projectItemName.split('::')[2] || ''; + return eventsBasedObject.getVariants().hasVariantNamed(variantName) + ? eventsBasedObject.getVariants().getVariant(variantName) + : eventsBasedObject.getDefaultVariant(); + } + getEventsBasedObjectName(): ?string { const { project, projectItemName } = this.props; if (!project || !projectItemName) return null; @@ -176,6 +190,9 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon const eventsBasedObject = this.getEventsBasedObject(); if (!eventsBasedObject) return null; + const variant = this.getVariant(); + if (!variant) return null; + const projectScopedContainersAccessor = new ProjectScopedContainersAccessor( { project, @@ -197,10 +214,11 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon layout={null} eventsFunctionsExtension={eventsFunctionsExtension} eventsBasedObject={eventsBasedObject} + eventsBasedObjectVariant={variant} globalObjectsContainer={null} - objectsContainer={eventsBasedObject.getObjects()} - layersContainer={eventsBasedObject.getLayers()} - initialInstances={eventsBasedObject.getInitialInstances()} + objectsContainer={variant.getObjects()} + layersContainer={variant.getLayers()} + initialInstances={variant.getInitialInstances()} getInitialInstancesEditorSettings={() => prepareInstancesEditorSettings( {}, // TODO @@ -216,13 +234,24 @@ export class CustomObjectEditorContainer extends React.Component<RenderEditorCon isActive={isActive} hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps} openBehaviorEvents={this.props.openBehaviorEvents} - onObjectEdited={this.props.onEventsBasedObjectChildrenEdited} + onObjectEdited={() => + this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject) + } + onObjectGroupEdited={() => + this.props.onEventsBasedObjectChildrenEdited(eventsBasedObject) + } onEventsBasedObjectChildrenEdited={ this.props.onEventsBasedObjectChildrenEdited } onExtractAsEventBasedObject={this.props.onExtractAsEventBasedObject} onOpenEventBasedObjectEditor={this.props.onOpenEventBasedObjectEditor} + onOpenEventBasedObjectVariantEditor={ + this.props.onOpenEventBasedObjectVariantEditor + } onExtensionInstalled={this.props.onExtensionInstalled} + onDeleteEventsBasedObjectVariant={ + this.props.onDeleteEventsBasedObjectVariant + } /> </div> ); diff --git a/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js index aef19e6b3f93..8287712fa7f7 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/EventsFunctionsExtensionEditorContainer.js @@ -169,7 +169,8 @@ export class EventsFunctionsExtensionEditorContainer extends React.Component<Ren onOpenCustomObjectEditor={eventsBasedObject => { this.props.onOpenCustomObjectEditor( eventsFunctionsExtension, - eventsBasedObject + eventsBasedObject, + '' ); }} hotReloadPreviewButtonProps={this.props.hotReloadPreviewButtonProps} diff --git a/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js index 23c5a8bb30c0..8c1a26d4e03d 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/ExternalLayoutEditorContainer.js @@ -235,6 +235,7 @@ export class ExternalLayoutEditorContainer extends React.Component< layout={layout} eventsFunctionsExtension={null} eventsBasedObject={null} + eventsBasedObjectVariant={null} globalObjectsContainer={project.getObjects()} objectsContainer={layout.getObjects()} layersContainer={layout.getLayers()} @@ -259,12 +260,20 @@ export class ExternalLayoutEditorContainer extends React.Component< onOpenEventBasedObjectEditor={ this.props.onOpenEventBasedObjectEditor } + onOpenEventBasedObjectVariantEditor={ + this.props.onOpenEventBasedObjectVariantEditor + } onObjectEdited={objectWithContext => this.props.onSceneObjectEdited(layout, objectWithContext) } + // It's only used to refresh events-based object variants. + onObjectGroupEdited={() => {}} // Nothing to do as events-based objects can't have external layout. onEventsBasedObjectChildrenEdited={() => {}} onExtensionInstalled={this.props.onExtensionInstalled} + onDeleteEventsBasedObjectVariant={ + this.props.onDeleteEventsBasedObjectVariant + } /> )} {!layout && ( diff --git a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js index a067f66738c5..b25ba5f3b880 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js +++ b/newIDE/app/src/MainFrame/EditorContainers/SceneEditorContainer.js @@ -129,6 +129,7 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP layout={layout} eventsFunctionsExtension={null} eventsBasedObject={null} + eventsBasedObjectVariant={null} globalObjectsContainer={project.getObjects()} objectsContainer={layout.getObjects()} layersContainer={layout.getLayers()} @@ -149,10 +150,18 @@ export class SceneEditorContainer extends React.Component<RenderEditorContainerP onExtractAsExternalLayout={this.props.onExtractAsExternalLayout} onExtractAsEventBasedObject={this.props.onExtractAsEventBasedObject} onOpenEventBasedObjectEditor={this.props.onOpenEventBasedObjectEditor} + onOpenEventBasedObjectVariantEditor={ + this.props.onOpenEventBasedObjectVariantEditor + } onExtensionInstalled={this.props.onExtensionInstalled} + onDeleteEventsBasedObjectVariant={ + this.props.onDeleteEventsBasedObjectVariant + } onObjectEdited={objectWithContext => this.props.onSceneObjectEdited(layout, objectWithContext) } + // It's only used to refresh events-based object variants. + onObjectGroupEdited={() => {}} // Nothing to do as scenes are not events-based objects. onEventsBasedObjectChildrenEdited={() => {}} /> diff --git a/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js b/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js index d1e59ef3c32b..79fd2ee29f31 100644 --- a/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js +++ b/newIDE/app/src/MainFrame/EditorTabs/EditorTabsHandler.js @@ -374,6 +374,29 @@ export const closeCustomObjectTab = ( }); }; +export const closeEventsBasedObjectVariantTab = ( + state: EditorTabsState, + eventsFunctionsExtensionName: string, + eventsBasedObjectName: string, + eventsBasedObjectVariantName: string +) => { + return closeTabsExceptIf(state, editorTab => { + const editor = editorTab.editorRef; + if (editor instanceof CustomObjectEditorContainer) { + return ( + (!editor.getEventsFunctionsExtensionName() || + editor.getEventsFunctionsExtensionName() !== + eventsFunctionsExtensionName) && + (!editor.getEventsBasedObjectName() || + editor.getEventsBasedObjectName() !== eventsBasedObjectName) && + (!editor.getVariantName() || + editor.getVariantName() !== eventsBasedObjectVariantName) + ); + } + return true; + }); +}; + export const getEventsFunctionsExtensionEditor = ( state: EditorTabsState, eventsFunctionsExtension: gdEventsFunctionsExtension @@ -394,14 +417,16 @@ export const getEventsFunctionsExtensionEditor = ( export const getCustomObjectEditor = ( state: EditorTabsState, eventsFunctionsExtension: gdEventsFunctionsExtension, - eventsBasedObject: gdEventsBasedObject + eventsBasedObject: gdEventsBasedObject, + variantName: string ): ?{| editor: CustomObjectEditorContainer, tabIndex: number |} => { for (let tabIndex = 0; tabIndex < state.editors.length; ++tabIndex) { const editor = state.editors[tabIndex].editorRef; if ( editor instanceof CustomObjectEditorContainer && editor.getEventsFunctionsExtension() === eventsFunctionsExtension && - editor.getEventsBasedObject() === eventsBasedObject + editor.getEventsBasedObject() === eventsBasedObject && + editor.getVariantName() === variantName ) { return { editor, tabIndex }; } diff --git a/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js b/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js index 632fe807ada7..0fe8a31c056a 100644 --- a/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js +++ b/newIDE/app/src/MainFrame/EditorTabs/UseEditorTabsStateSaving.js @@ -48,7 +48,17 @@ const projectHasItem = ({ case 'external events': return project.hasExternalEventsNamed(name); case 'custom object': - return project.hasEventsBasedObject(name); + const nameElements = name.split('::'); + const objectType = nameElements[0] + '::' + nameElements[1]; + const variantName = nameElements[2]; + return ( + project.hasEventsBasedObject(objectType) && + (!variantName || + project + .getEventsBasedObject(objectType) + .getVariants() + .getVariant(variantName)) + ); default: return false; } diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 568df82f1e51..542f76ea6692 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -38,6 +38,7 @@ import { closeExternalEventsTabs, closeEventsFunctionsExtensionTabs, closeCustomObjectTab, + closeEventsBasedObjectVariantTab, saveUiSettings, type EditorTabsState, type EditorTab, @@ -585,7 +586,8 @@ const MainFrame = (props: Props) => { : kind === 'layout events' ? name + ` ${i18n._(t`(Events)`)}` : kind === 'custom object' - ? name.split('::')[1] + ` ${i18n._(t`(Object)`)}` + ? name.split('::')[2] || + name.split('::')[1] + ` ${i18n._(t`(Object)`)}` : name; const tabOptions = kind === 'layout' @@ -1249,12 +1251,12 @@ const MainFrame = (props: Props) => { toolbar.current.setEditorToolbar(editorToolbar); }; - const onInstallExtension = (extensionShortHeader: ExtensionShortHeader) => { + const onInstallExtension = (extensionName: string) => { const { currentProject } = state; if (!currentProject) return; // Close the extension tab before updating/reinstalling the extension. - const eventsFunctionsExtensionName = extensionShortHeader.name; + const eventsFunctionsExtensionName = extensionName; if ( currentProject.hasEventsFunctionsExtensionNamed( @@ -1378,6 +1380,20 @@ const MainFrame = (props: Props) => { }; const onExtensionInstalled = (extensionName: string) => { + const { currentProject } = state; + if (!currentProject) { + return; + } + const eventsBasedObjects = currentProject + .getEventsFunctionsExtension(extensionName) + .getEventsBasedObjects(); + for (let index = 0; index < eventsBasedObjects.getCount(); index++) { + const eventsBasedObject = eventsBasedObjects.getAt(index); + gd.EventsBasedObjectVariantHelper.complyVariantsToEventsBasedObject( + currentProject, + eventsBasedObject + ); + } // TODO Open the closed tabs back // It would be safer to close the tabs before the extension is installed // but it would make opening them back more complicated. @@ -1563,6 +1579,29 @@ const MainFrame = (props: Props) => { })); }; + const deleteEventsBasedObjectVariant = ( + eventsFunctionsExtension: gdEventsFunctionsExtension, + eventBasedObject: gdEventsBasedObject, + variant: gdEventsBasedObjectVariant + ): void => { + const variants = eventBasedObject.getVariants(); + const variantName = variant.getName(); + if (!variants.hasVariantNamed(variantName)) { + return; + } + variants.removeVariant(variantName); + + setState(state => ({ + ...state, + editorTabs: closeEventsBasedObjectVariantTab( + state.editorTabs, + eventsFunctionsExtension.getName(), + eventBasedObject.getName(), + variantName + ), + })); + }; + const setPreviewedLayout = ( previewLayoutName: ?string, previewExternalLayoutName?: ?string @@ -2073,7 +2112,8 @@ const MainFrame = (props: Props) => { const openCustomObjectEditor = React.useCallback( ( eventsFunctionsExtension: gdEventsFunctionsExtension, - eventsBasedObject: gdEventsBasedObject + eventsBasedObject: gdEventsBasedObject, + variantName: string ) => { const { currentProject, editorTabs } = state; if (!currentProject) return; @@ -2081,7 +2121,8 @@ const MainFrame = (props: Props) => { const foundTab = getCustomObjectEditor( editorTabs, eventsFunctionsExtension, - eventsBasedObject + eventsBasedObject, + variantName ); if (foundTab) { setState(state => ({ @@ -2098,7 +2139,10 @@ const MainFrame = (props: Props) => { name: eventsFunctionsExtension.getName() + '::' + - eventsBasedObject.getName(), + eventsBasedObject.getName() + + (eventsBasedObject.getVariants().hasVariantNamed(variantName) + ? '::' + variantName + : ''), project: currentProject, }), }), @@ -2182,13 +2226,41 @@ const MainFrame = (props: Props) => { extensionName: string, eventsBasedObjectName: string ) => { - if (!currentProject) return; + if ( + !currentProject || + !currentProject.hasEventsFunctionsExtensionNamed(extensionName) + ) { + return; + } openEventsFunctionsExtension( extensionName, null, null, eventsBasedObjectName ); + const eventsFunctionsExtension = currentProject.getEventsFunctionsExtension( + extensionName + ); + const eventsBasedObjects = eventsFunctionsExtension.getEventsBasedObjects(); + if (!eventsBasedObjects.has(eventsBasedObjectName)) { + return; + } + const eventsBasedObject = eventsBasedObjects.get(eventsBasedObjectName); + openCustomObjectEditor(eventsFunctionsExtension, eventsBasedObject, ''); + + // Trigger reloading of extensions as an extension was modified (or even added) + // to create the custom object. + eventsFunctionsExtensionsState.loadProjectEventsFunctionsExtensions( + currentProject + ); + }; + + const onOpenEventBasedObjectVariantEditor = ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => { + if (!currentProject) return; if (!currentProject.hasEventsFunctionsExtensionNamed(extensionName)) { return; } @@ -2200,7 +2272,11 @@ const MainFrame = (props: Props) => { return; } const eventsBasedObject = eventsBasedObjects.get(eventsBasedObjectName); - openCustomObjectEditor(eventsFunctionsExtension, eventsBasedObject); + openCustomObjectEditor( + eventsFunctionsExtension, + eventsBasedObject, + variantName + ); // Trigger reloading of extensions as an extension was modified (or even added) // to create the custom object. @@ -2210,7 +2286,16 @@ const MainFrame = (props: Props) => { }; const onEventsBasedObjectChildrenEdited = React.useCallback( - () => { + (eventsBasedObject: gdEventsBasedObject) => { + const project = state.currentProject; + if (!project) { + return; + } + gd.EventsBasedObjectVariantHelper.complyVariantsToEventsBasedObject( + project, + eventsBasedObject + ); + for (const editor of state.editorTabs.editors) { const { editorRef } = editor; if (editorRef) { @@ -2218,7 +2303,7 @@ const MainFrame = (props: Props) => { } } }, - [state.editorTabs] + [state.editorTabs, state.currentProject] ); const onSceneObjectEdited = React.useCallback( @@ -3886,6 +3971,8 @@ const MainFrame = (props: Props) => { onExtractAsExternalLayout: onExtractAsExternalLayout, onExtractAsEventBasedObject: onOpenEventBasedObjectEditor, onOpenEventBasedObjectEditor: onOpenEventBasedObjectEditor, + onOpenEventBasedObjectVariantEditor: onOpenEventBasedObjectVariantEditor, + onDeleteEventsBasedObjectVariant: deleteEventsBasedObjectVariant, onEventsBasedObjectChildrenEdited: onEventsBasedObjectChildrenEdited, onSceneObjectEdited: onSceneObjectEdited, onExtensionInstalled: onExtensionInstalled, diff --git a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js index 32db274f6065..e2ce7e299ef2 100644 --- a/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js +++ b/newIDE/app/src/ObjectEditor/CompactObjectPropertiesEditor/index.js @@ -157,7 +157,7 @@ const TopLevelCollapsibleSection = ({ renderContentAsHiddenWhenFolded?: boolean, noContentMargin?: boolean, onOpenFullEditor: () => void, - onAdd?: () => void, + onAdd?: (() => void) | null, |}) => ( <> <Separator /> @@ -216,6 +216,8 @@ type Props = {| objects: Array<gdObject>, onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void, onExtensionInstalled: (extensionName: string) => void, + isVariableListLocked: boolean, + isBehaviorListLocked: boolean, |}; export const CompactObjectPropertiesEditor = ({ @@ -234,6 +236,8 @@ export const CompactObjectPropertiesEditor = ({ objects, onEditObject, onExtensionInstalled, + isVariableListLocked, + isBehaviorListLocked, }: Props) => { const forceUpdate = useForceUpdate(); const [ @@ -545,7 +549,7 @@ export const CompactObjectPropertiesEditor = ({ isFolded={isBehaviorsFolded} toggleFolded={() => setIsBehaviorsFolded(!isBehaviorsFolded)} onOpenFullEditor={() => onEditObject(object, 'behaviors')} - onAdd={openNewBehaviorDialog} + onAdd={isBehaviorListLocked ? null : openNewBehaviorDialog} renderContent={() => ( <ColumnStackLayout noMargin> {!allVisibleBehaviors.length && ( @@ -618,12 +622,16 @@ export const CompactObjectPropertiesEditor = ({ isFolded={isVariablesFolded} toggleFolded={() => setIsVariablesFolded(!isVariablesFolded)} onOpenFullEditor={() => onEditObject(object, 'variables')} - onAdd={() => { - if (variablesListRef.current) { - variablesListRef.current.addVariable(); - } - setIsVariablesFolded(false); - }} + onAdd={ + isVariableListLocked + ? null + : () => { + if (variablesListRef.current) { + variablesListRef.current.addVariable(); + } + setIsVariablesFolded(false); + } + } renderContentAsHiddenWhenFolded={ true /* Allows to keep a ref to the variables list for add button to work. */ } @@ -664,6 +672,7 @@ export const CompactObjectPropertiesEditor = ({ on this object. </Trans> } + isListLocked={isVariableListLocked} /> )} /> diff --git a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor.js b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor.js deleted file mode 100644 index 8e985bb206e6..000000000000 --- a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor.js +++ /dev/null @@ -1,534 +0,0 @@ -// @flow -import { Trans } from '@lingui/macro'; -import { t } from '@lingui/macro'; -import { I18n } from '@lingui/react'; - -import * as React from 'react'; -import PropertiesEditor from '../../PropertiesEditor'; -import propertiesMapToSchema from '../../PropertiesEditor/PropertiesMapToSchema'; -import EmptyMessage from '../../UI/EmptyMessage'; -import { type EditorProps } from './EditorProps.flow'; -import { Column, Line } from '../../UI/Grid'; -import { getExtraObjectsInformation } from '../../Hints'; -import { getObjectTutorialIds } from '../../Utils/GDevelopServices/Tutorial'; -import AlertMessage from '../../UI/AlertMessage'; -import { ColumnStackLayout } from '../../UI/Layout'; -import DismissableTutorialMessage from '../../Hints/DismissableTutorialMessage'; -import { mapFor } from '../../Utils/MapFor'; -import ObjectsEditorService from '../ObjectsEditorService'; -import Text from '../../UI/Text'; -import useForceUpdate from '../../Utils/UseForceUpdate'; -import { Accordion, AccordionHeader, AccordionBody } from '../../UI/Accordion'; -import { IconContainer } from '../../UI/IconContainer'; -import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext'; -import AnimationList, { - type AnimationListInterface, -} from './SpriteEditor/AnimationList'; -import PointsEditor from './SpriteEditor/PointsEditor'; -import CollisionMasksEditor from './SpriteEditor/CollisionMasksEditor'; -import { - hasAnyFrame, - getFirstAnimationFrame, - setCollisionMaskOnAllFrames, -} from './SpriteEditor/Utils/SpriteObjectHelper'; -import { getMatchingCollisionMask } from './SpriteEditor/CollisionMasksEditor/CollisionMaskHelper'; -import ResourcesLoader from '../../ResourcesLoader'; -import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView'; -import FlatButton from '../../UI/FlatButton'; -import RaisedButton from '../../UI/RaisedButton'; -import FlatButtonWithSplitMenu from '../../UI/FlatButtonWithSplitMenu'; -import { ResponsiveLineStackLayout } from '../../UI/Layout'; -import { useResponsiveWindowSize } from '../../UI/Responsive/ResponsiveWindowMeasurer'; -import Add from '../../UI/CustomSvgIcons/Add'; -import Dialog from '../../UI/Dialog'; -import HelpButton from '../../UI/HelpButton'; -import RestoreIcon from '../../UI/CustomSvgIcons/Restore'; - -const gd: libGDevelop = global.gd; - -const styles = { - icon: { width: 16, height: 16 }, -}; - -type Props = EditorProps; - -const CustomObjectPropertiesEditor = (props: Props) => { - const forceUpdate = useForceUpdate(); - - const { - objectConfiguration, - project, - layout, - eventsFunctionsExtension, - eventsBasedObject, - object, - objectName, - resourceManagementProps, - onSizeUpdated, - onObjectUpdated, - unsavedChanges, - renderObjectNameField, - isChildObject, - } = props; - - const { isMobile } = useResponsiveWindowSize(); - - const customObjectConfiguration = gd.asCustomObjectConfiguration( - objectConfiguration - ); - const properties = customObjectConfiguration.getProperties(); - - const propertiesSchema = propertiesMapToSchema( - properties, - object => object.getProperties(), - (object, name, value) => object.updateProperty(name, value) - ); - - const extraInformation = getExtraObjectsInformation()[ - customObjectConfiguration.getType() - ]; - - const { values } = React.useContext(PreferencesContext); - const tutorialIds = getObjectTutorialIds(customObjectConfiguration.getType()); - - const eventBasedObject = project.hasEventsBasedObject( - customObjectConfiguration.getType() - ) - ? project.getEventsBasedObject(customObjectConfiguration.getType()) - : null; - - const animations = customObjectConfiguration.getAnimations(); - - // The matching collision mask only takes the first sprite of the first - // animation of the object. We consider this is enough to start with, and - // the user can then edit the collision mask for further needs. - const onCreateMatchingSpriteCollisionMask = React.useCallback( - async () => { - const firstSprite = getFirstAnimationFrame(animations); - if (!firstSprite) { - return; - } - const firstSpriteResourceName = firstSprite.getImageName(); - const firstAnimationResourceSource = ResourcesLoader.getResourceFullUrl( - project, - firstSpriteResourceName, - {} - ); - let matchingCollisionMask = null; - try { - matchingCollisionMask = await getMatchingCollisionMask( - firstAnimationResourceSource - ); - } catch (e) { - console.error( - 'Unable to create a matching collision mask for the sprite, fallback to full image collision mask.', - e - ); - } - setCollisionMaskOnAllFrames(animations, matchingCollisionMask); - forceUpdate(); - }, - [animations, project, forceUpdate] - ); - - const scrollView = React.useRef<?ScrollViewInterface>(null); - const animationList = React.useRef<?AnimationListInterface>(null); - - const [ - justAddedAnimationName, - setJustAddedAnimationName, - ] = React.useState<?string>(null); - const justAddedAnimationElement = React.useRef<?any>(null); - - React.useEffect( - () => { - if ( - scrollView.current && - justAddedAnimationElement.current && - justAddedAnimationName - ) { - scrollView.current.scrollTo(justAddedAnimationElement.current); - setJustAddedAnimationName(null); - justAddedAnimationElement.current = null; - } - }, - [justAddedAnimationName] - ); - - const [pointsEditorOpen, setPointsEditorOpen] = React.useState(false); - const [ - collisionMasksEditorOpen, - setCollisionMasksEditorOpen, - ] = React.useState(false); - - return ( - <I18n> - {({ i18n }) => ( - <> - <ScrollView ref={scrollView}> - <ColumnStackLayout noMargin> - {renderObjectNameField && renderObjectNameField()} - {tutorialIds.map(tutorialId => ( - <DismissableTutorialMessage - key={tutorialId} - tutorialId={tutorialId} - /> - ))} - {propertiesSchema.length || - (eventBasedObject && - (eventBasedObject.getObjects().getObjectsCount() || - eventBasedObject.isAnimatable())) ? ( - <React.Fragment> - {extraInformation ? ( - <Line> - <ColumnStackLayout noMargin> - {extraInformation.map(({ kind, message }, index) => ( - <AlertMessage kind={kind} key={index}> - {i18n._(message)} - </AlertMessage> - ))} - </ColumnStackLayout> - </Line> - ) : null} - <PropertiesEditor - unsavedChanges={unsavedChanges} - schema={propertiesSchema} - instances={[customObjectConfiguration]} - project={project} - resourceManagementProps={resourceManagementProps} - /> - {eventBasedObject && - (!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && - !customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() ? ( - <Line alignItems="center"> - <Column expand noMargin> - <Text size="block-title">Children objects</Text> - </Column> - <Column alignItems="right"> - <FlatButton - label={ - <Trans>Override children configuration</Trans> - } - onClick={() => { - customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration( - true - ); - customObjectConfiguration.clearChildrenConfiguration(); - if (onObjectUpdated) { - onObjectUpdated(); - } - forceUpdate(); - }} - /> - </Column> - </Line> - ) : ( - <> - {!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && ( - <Line alignItems="center"> - <Column expand noMargin> - <Text size="block-title">Children objects</Text> - </Column> - <Column alignItems="right"> - <FlatButton - leftIcon={<RestoreIcon style={styles.icon} />} - label={ - <Trans> - Reset and hide children configuration - </Trans> - } - onClick={() => { - customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration( - false - ); - customObjectConfiguration.clearChildrenConfiguration(); - if (onObjectUpdated) { - onObjectUpdated(); - } - forceUpdate(); - }} - /> - </Column> - </Line> - )} - {mapFor( - 0, - eventBasedObject.getObjects().getObjectsCount(), - i => { - const childObject = eventBasedObject - .getObjects() - .getObjectAt(i); - const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration( - childObject.getName() - ); - const editorConfiguration = ObjectsEditorService.getEditorConfiguration( - project, - childObjectConfiguration.getType() - ); - const EditorComponent = - editorConfiguration.component; - - const objectMetadata = gd.MetadataProvider.getObjectMetadata( - gd.JsPlatform.get(), - childObjectConfiguration.getType() - ); - const iconUrl = objectMetadata.getIconFilename(); - const tutorialIds = getObjectTutorialIds( - childObjectConfiguration.getType() - ); - const enabledTutorialIds = tutorialIds.filter( - tutorialId => - !values.hiddenTutorialHints[tutorialId] - ); - // TODO EBO: Add a protection against infinite loops in case - // of object cycles (thought it should be forbidden). - return ( - <Accordion - key={childObject.getName()} - defaultExpanded - > - <AccordionHeader> - {iconUrl ? ( - <IconContainer - src={iconUrl} - alt={childObject.getName()} - size={20} - /> - ) : null} - <Column expand> - <Text size="block-title"> - {childObject.getName()} - </Text> - </Column> - </AccordionHeader> - <AccordionBody> - <Column expand noMargin noOverflowParent> - {enabledTutorialIds.length ? ( - <Line> - <ColumnStackLayout expand> - {tutorialIds.map(tutorialId => ( - <DismissableTutorialMessage - key={tutorialId} - tutorialId={tutorialId} - /> - ))} - </ColumnStackLayout> - </Line> - ) : null} - <Line noMargin> - <Column expand> - <EditorComponent - isChildObject - objectConfiguration={ - childObjectConfiguration - } - project={project} - layout={layout} - eventsFunctionsExtension={ - eventsFunctionsExtension - } - eventsBasedObject={eventsBasedObject} - resourceManagementProps={ - resourceManagementProps - } - onSizeUpdated={ - forceUpdate /*Force update to ensure dialog is properly positioned*/ - } - objectName={ - objectName + - ' ' + - childObject.getName() - } - onObjectUpdated={onObjectUpdated} - unsavedChanges={unsavedChanges} - /> - </Column> - </Line> - </Column> - </AccordionBody> - </Accordion> - ); - } - )} - </> - ))} - {eventBasedObject && eventBasedObject.isAnimatable() && ( - <Column expand> - <Text size="block-title"> - <Trans>Animations</Trans> - </Text> - <AnimationList - ref={animationList} - animations={animations} - project={project} - layout={layout} - eventsFunctionsExtension={eventsFunctionsExtension} - eventsBasedObject={eventsBasedObject} - object={object} - objectName={objectName} - resourceManagementProps={resourceManagementProps} - onSizeUpdated={onSizeUpdated} - onObjectUpdated={onObjectUpdated} - isAnimationListLocked={false} - scrollView={scrollView} - onCreateMatchingSpriteCollisionMask={ - onCreateMatchingSpriteCollisionMask - } - /> - </Column> - )} - </React.Fragment> - ) : ( - <EmptyMessage> - <Trans> - There is nothing to configure for this object. You can still - use events to interact with the object. - </Trans> - </EmptyMessage> - )} - </ColumnStackLayout> - </ScrollView> - {eventBasedObject && - eventBasedObject.isAnimatable() && - !isChildObject && ( - <Column noMargin> - <ResponsiveLineStackLayout - justifyContent="space-between" - noColumnMargin - > - {!isMobile ? ( // On mobile, use only 1 button to gain space. - <ResponsiveLineStackLayout noMargin noColumnMargin> - <FlatButton - label={<Trans>Edit collision masks</Trans>} - onClick={() => setCollisionMasksEditorOpen(true)} - disabled={!hasAnyFrame(animations)} - /> - <FlatButton - label={<Trans>Edit points</Trans>} - onClick={() => setPointsEditorOpen(true)} - disabled={!hasAnyFrame(animations)} - /> - </ResponsiveLineStackLayout> - ) : ( - <FlatButtonWithSplitMenu - label={<Trans>Edit collision masks</Trans>} - onClick={() => setCollisionMasksEditorOpen(true)} - disabled={!hasAnyFrame(animations)} - buildMenuTemplate={i18n => [ - { - label: i18n._(t`Edit points`), - disabled: !hasAnyFrame(animations), - click: () => setPointsEditorOpen(true), - }, - ]} - /> - )} - <RaisedButton - label={<Trans>Add an animation</Trans>} - primary - onClick={() => { - if (!animationList.current) { - return; - } - animationList.current.addAnimation(); - }} - icon={<Add />} - /> - </ResponsiveLineStackLayout> - </Column> - )} - {pointsEditorOpen && ( - <Dialog - title={<Trans>Edit points</Trans>} - actions={[ - <RaisedButton - key="apply" - label={<Trans>Apply</Trans>} - primary - onClick={() => setPointsEditorOpen(false)} - />, - ]} - secondaryActions={[ - <HelpButton - helpPagePath="/objects/sprite/edit-points" - key="help" - />, - ]} - onRequestClose={() => setPointsEditorOpen(false)} - maxWidth="lg" - flexBody - fullHeight - open={pointsEditorOpen} - > - <PointsEditor - animations={animations} - resourcesLoader={ResourcesLoader} - project={project} - onPointsUpdated={onObjectUpdated} - onRenamedPoint={(oldName, newName) => { - if (!object) { - return; - } - if (layout) { - gd.WholeProjectRefactorer.renameObjectPointInScene( - project, - layout, - object, - oldName, - newName - ); - } else if (eventsFunctionsExtension && eventsBasedObject) { - gd.WholeProjectRefactorer.renameObjectPointInEventsBasedObject( - project, - eventsFunctionsExtension, - eventsBasedObject, - object, - oldName, - newName - ); - } - }} - /> - </Dialog> - )} - {collisionMasksEditorOpen && ( - <Dialog - title={<Trans>Edit collision masks</Trans>} - actions={[ - <RaisedButton - key="apply" - label={<Trans>Apply</Trans>} - primary - onClick={() => setCollisionMasksEditorOpen(false)} - />, - ]} - secondaryActions={[ - <HelpButton - helpPagePath="/objects/sprite/collision-mask" - key="help" - />, - ]} - maxWidth="lg" - flexBody - fullHeight - onRequestClose={() => setCollisionMasksEditorOpen(false)} - open={collisionMasksEditorOpen} - > - <CollisionMasksEditor - animations={animations} - resourcesLoader={ResourcesLoader} - project={project} - onMasksUpdated={onObjectUpdated} - onCreateMatchingSpriteCollisionMask={ - onCreateMatchingSpriteCollisionMask - } - /> - </Dialog> - )} - </> - )} - </I18n> - ); -}; - -export default CustomObjectPropertiesEditor; diff --git a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/NewVariantDialog.js b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/NewVariantDialog.js new file mode 100644 index 000000000000..043f3726e1bc --- /dev/null +++ b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/NewVariantDialog.js @@ -0,0 +1,68 @@ +// @flow +import { t, Trans } from '@lingui/macro'; +import React from 'react'; +import FlatButton from '../../../UI/FlatButton'; +import Dialog, { DialogPrimaryButton } from '../../../UI/Dialog'; +import SemiControlledTextField from '../../../UI/SemiControlledTextField'; +import HelpButton from '../../../UI/HelpButton'; + +type Props = {| + initialName: string, + onApply: (variantName: string) => void, + onCancel: () => void, +|}; + +const NewVariantDialog = ({ initialName, onApply, onCancel }: Props) => { + const [variantName, setVariantName] = React.useState<string>(initialName); + + const apply = React.useCallback( + () => { + onApply(variantName); + }, + [onApply, variantName] + ); + + return ( + <Dialog + title={<Trans>Create a new variant</Trans>} + id="create-variant-dialog" + actions={[ + <FlatButton + key="cancel" + label={<Trans>Cancel</Trans>} + onClick={onCancel} + />, + <DialogPrimaryButton + key="apply" + label={<Trans>Create</Trans>} + primary + onClick={apply} + />, + ]} + secondaryActions={[ + <HelpButton + key="help-button" + helpPagePath="/objects/custom-objects-prefab-template" + />, + ]} + onRequestClose={onCancel} + onApply={apply} + open + maxWidth="sm" + > + <SemiControlledTextField + fullWidth + id="variant-name" + commitOnBlur + floatingLabelText={<Trans>Variant name</Trans>} + floatingLabelFixed + value={variantName} + translatableHintText={t`Variant name`} + onChange={setVariantName} + autoFocus="desktop" + /> + </Dialog> + ); +}; + +export default NewVariantDialog; diff --git a/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/index.js b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/index.js new file mode 100644 index 000000000000..7c0bc0c3b16e --- /dev/null +++ b/newIDE/app/src/ObjectEditor/Editors/CustomObjectPropertiesEditor/index.js @@ -0,0 +1,756 @@ +// @flow +import { Trans } from '@lingui/macro'; +import { t } from '@lingui/macro'; +import { I18n } from '@lingui/react'; +import { type I18n as I18nType } from '@lingui/core'; + +import * as React from 'react'; +import PropertiesEditor from '../../../PropertiesEditor'; +import propertiesMapToSchema from '../../../PropertiesEditor/PropertiesMapToSchema'; +import EmptyMessage from '../../../UI/EmptyMessage'; +import { type EditorProps } from '../EditorProps.flow'; +import { Column, Line } from '../../../UI/Grid'; +import { getExtraObjectsInformation } from '../../../Hints'; +import { getObjectTutorialIds } from '../../../Utils/GDevelopServices/Tutorial'; +import AlertMessage from '../../../UI/AlertMessage'; +import DismissableTutorialMessage from '../../../Hints/DismissableTutorialMessage'; +import { mapFor } from '../../../Utils/MapFor'; +import ObjectsEditorService from '../../ObjectsEditorService'; +import Text from '../../../UI/Text'; +import useForceUpdate from '../../../Utils/UseForceUpdate'; +import { + Accordion, + AccordionHeader, + AccordionBody, +} from '../../../UI/Accordion'; +import { IconContainer } from '../../../UI/IconContainer'; +import PreferencesContext from '../../../MainFrame/Preferences/PreferencesContext'; +import AnimationList, { + type AnimationListInterface, +} from '../SpriteEditor/AnimationList'; +import PointsEditor from '../SpriteEditor/PointsEditor'; +import CollisionMasksEditor from '../SpriteEditor/CollisionMasksEditor'; +import { + hasAnyFrame, + getFirstAnimationFrame, + setCollisionMaskOnAllFrames, +} from '../SpriteEditor/Utils/SpriteObjectHelper'; +import { getMatchingCollisionMask } from '../SpriteEditor/CollisionMasksEditor/CollisionMaskHelper'; +import ResourcesLoader from '../../../ResourcesLoader'; +import ScrollView, { type ScrollViewInterface } from '../../../UI/ScrollView'; +import FlatButton from '../../../UI/FlatButton'; +import RaisedButton from '../../../UI/RaisedButton'; +import FlatButtonWithSplitMenu from '../../../UI/FlatButtonWithSplitMenu'; +import { + ResponsiveLineStackLayout, + LineStackLayout, + ColumnStackLayout, +} from '../../../UI/Layout'; +import { useResponsiveWindowSize } from '../../../UI/Responsive/ResponsiveWindowMeasurer'; +import Add from '../../../UI/CustomSvgIcons/Add'; +import Trash from '../../../UI/CustomSvgIcons/Trash'; +import Edit from '../../../UI/CustomSvgIcons/ShareExternal'; +import Dialog from '../../../UI/Dialog'; +import HelpButton from '../../../UI/HelpButton'; +import RestoreIcon from '../../../UI/CustomSvgIcons/Restore'; +import SelectField from '../../../UI/SelectField'; +import SelectOption from '../../../UI/SelectOption'; +import NewVariantDialog from './NewVariantDialog'; +import newNameGenerator from '../../../Utils/NewNameGenerator'; +import { + serializeToJSObject, + unserializeFromJSObject, +} from '../../../Utils/Serializer'; + +const gd: libGDevelop = global.gd; + +const styles = { + icon: { width: 16, height: 16 }, +}; + +const getVariantName = ( + eventBasedObject: gdEventsBasedObject | null, + customObjectConfiguration: gdCustomObjectConfiguration +): string => + eventBasedObject && + eventBasedObject + .getVariants() + .hasVariantNamed(customObjectConfiguration.getVariantName()) + ? customObjectConfiguration.getVariantName() + : ''; + +const getVariant = ( + eventBasedObject: gdEventsBasedObject, + customObjectConfiguration: gdCustomObjectConfiguration +): gdEventsBasedObjectVariant => { + const variantName = getVariantName( + eventBasedObject, + customObjectConfiguration + ); + const variants = eventBasedObject.getVariants(); + return variantName + ? variants.getVariant(variantName) + : eventBasedObject.getDefaultVariant(); +}; + +type Props = EditorProps; + +const CustomObjectPropertiesEditor = (props: Props) => { + const forceUpdate = useForceUpdate(); + + const { + objectConfiguration, + project, + layout, + eventsFunctionsExtension, + eventsBasedObject, + object, + objectName, + resourceManagementProps, + onSizeUpdated, + onObjectUpdated, + unsavedChanges, + renderObjectNameField, + isChildObject, + onOpenEventBasedObjectVariantEditor, + onDeleteEventsBasedObjectVariant, + } = props; + + const { isMobile } = useResponsiveWindowSize(); + + const customObjectConfiguration = gd.asCustomObjectConfiguration( + objectConfiguration + ); + const properties = customObjectConfiguration.getProperties(); + + const propertiesSchema = propertiesMapToSchema( + properties, + object => object.getProperties(), + (object, name, value) => object.updateProperty(name, value) + ); + + const extraInformation = getExtraObjectsInformation()[ + customObjectConfiguration.getType() + ]; + + const { values } = React.useContext(PreferencesContext); + const tutorialIds = getObjectTutorialIds(customObjectConfiguration.getType()); + + const eventBasedObject = project.hasEventsBasedObject( + customObjectConfiguration.getType() + ) + ? project.getEventsBasedObject(customObjectConfiguration.getType()) + : null; + + const animations = customObjectConfiguration.getAnimations(); + + // The matching collision mask only takes the first sprite of the first + // animation of the object. We consider this is enough to start with, and + // the user can then edit the collision mask for further needs. + const onCreateMatchingSpriteCollisionMask = React.useCallback( + async () => { + const firstSprite = getFirstAnimationFrame(animations); + if (!firstSprite) { + return; + } + const firstSpriteResourceName = firstSprite.getImageName(); + const firstAnimationResourceSource = ResourcesLoader.getResourceFullUrl( + project, + firstSpriteResourceName, + {} + ); + let matchingCollisionMask = null; + try { + matchingCollisionMask = await getMatchingCollisionMask( + firstAnimationResourceSource + ); + } catch (e) { + console.error( + 'Unable to create a matching collision mask for the sprite, fallback to full image collision mask.', + e + ); + } + setCollisionMaskOnAllFrames(animations, matchingCollisionMask); + forceUpdate(); + }, + [animations, project, forceUpdate] + ); + + const scrollView = React.useRef<?ScrollViewInterface>(null); + const animationList = React.useRef<?AnimationListInterface>(null); + + const [ + justAddedAnimationName, + setJustAddedAnimationName, + ] = React.useState<?string>(null); + const justAddedAnimationElement = React.useRef<?any>(null); + + React.useEffect( + () => { + if ( + scrollView.current && + justAddedAnimationElement.current && + justAddedAnimationName + ) { + scrollView.current.scrollTo(justAddedAnimationElement.current); + setJustAddedAnimationName(null); + justAddedAnimationElement.current = null; + } + }, + [justAddedAnimationName] + ); + + const [pointsEditorOpen, setPointsEditorOpen] = React.useState(false); + const [ + collisionMasksEditorOpen, + setCollisionMasksEditorOpen, + ] = React.useState(false); + const [newVariantDialogOpen, setNewVariantDialogOpen] = React.useState(false); + + const editVariant = React.useCallback( + () => { + onOpenEventBasedObjectVariantEditor && + onOpenEventBasedObjectVariantEditor( + gd.PlatformExtension.getExtensionFromFullObjectType( + customObjectConfiguration.getType() + ), + gd.PlatformExtension.getObjectNameFromFullObjectType( + customObjectConfiguration.getType() + ), + customObjectConfiguration.getVariantName() + ); + }, + [customObjectConfiguration, onOpenEventBasedObjectVariantEditor] + ); + + const duplicateVariant = React.useCallback( + (i18n: I18nType, newName: string) => { + if (!eventBasedObject) { + return; + } + const variants = eventBasedObject.getVariants(); + // TODO Forbid name with `::` + const uniqueNewName = newNameGenerator( + newName || i18n._(t`New variant`), + tentativeNewName => variants.hasVariantNamed(tentativeNewName) + ); + const oldVariantName = getVariantName( + eventBasedObject, + customObjectConfiguration + ); + const oldVariant = oldVariantName + ? variants.getVariant(oldVariantName) + : eventBasedObject.getDefaultVariant(); + const newVariant = variants.insertNewVariant(uniqueNewName, 0); + unserializeFromJSObject( + newVariant, + serializeToJSObject(oldVariant), + 'unserializeFrom', + project + ); + newVariant.setName(uniqueNewName); + newVariant.setAssetStoreAssetId(''); + newVariant.setAssetStoreOriginalName(''); + customObjectConfiguration.setVariantName(uniqueNewName); + setNewVariantDialogOpen(false); + forceUpdate(); + }, + [customObjectConfiguration, eventBasedObject, forceUpdate, project] + ); + + const deleteVariant = React.useCallback( + () => { + if (!eventBasedObject || !onDeleteEventsBasedObjectVariant) { + return; + } + const variants = eventBasedObject.getVariants(); + const selectedVariantName = customObjectConfiguration.getVariantName(); + if (variants.hasVariantNamed(selectedVariantName)) { + customObjectConfiguration.setVariantName(''); + const extensionName = gd.PlatformExtension.getExtensionFromFullObjectType( + customObjectConfiguration.getType() + ); + if (!project.hasEventsFunctionsExtensionNamed(extensionName)) { + return; + } + const eventBasedExtension = project.getEventsFunctionsExtension( + extensionName + ); + onDeleteEventsBasedObjectVariant( + eventBasedExtension, + eventBasedObject, + variants.getVariant(selectedVariantName) + ); + forceUpdate(); + } + }, + [ + customObjectConfiguration, + eventBasedObject, + forceUpdate, + onDeleteEventsBasedObjectVariant, + project, + ] + ); + + return ( + <I18n> + {({ i18n }) => ( + <> + <ScrollView ref={scrollView}> + <ColumnStackLayout noMargin> + {renderObjectNameField && renderObjectNameField()} + {tutorialIds.map(tutorialId => ( + <DismissableTutorialMessage + key={tutorialId} + tutorialId={tutorialId} + /> + ))} + {propertiesSchema.length || + (eventBasedObject && + (eventBasedObject.getObjects().getObjectsCount() || + eventBasedObject.isAnimatable())) ? ( + <React.Fragment> + {extraInformation ? ( + <Line> + <ColumnStackLayout noMargin> + {extraInformation.map(({ kind, message }, index) => ( + <AlertMessage kind={kind} key={index}> + {i18n._(message)} + </AlertMessage> + ))} + </ColumnStackLayout> + </Line> + ) : null} + <PropertiesEditor + unsavedChanges={unsavedChanges} + schema={propertiesSchema} + instances={[customObjectConfiguration]} + project={project} + resourceManagementProps={resourceManagementProps} + /> + {!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && ( + <> + <Line> + <Column expand noMargin> + <Text size="block-title">Variant</Text> + </Column> + </Line> + <ColumnStackLayout expand noMargin> + <LineStackLayout> + <FlatButton + label={<Trans>Edit</Trans>} + leftIcon={<Edit />} + onClick={editVariant} + disabled={ + !eventBasedObject || + getVariant( + eventBasedObject, + customObjectConfiguration + ).getAssetStoreAssetId() !== '' + } + /> + <FlatButton + label={<Trans>Duplicate</Trans>} + leftIcon={<Add />} + onClick={() => setNewVariantDialogOpen(true)} + /> + <FlatButton + label={<Trans>Delete</Trans>} + leftIcon={<Trash />} + onClick={deleteVariant} + /> + </LineStackLayout> + <SelectField + floatingLabelText={<Trans>Variant</Trans>} + value={getVariantName( + eventBasedObject, + customObjectConfiguration + )} + onChange={(e, i, value: string) => { + customObjectConfiguration.setVariantName(value); + forceUpdate(); + }} + > + <SelectOption + key="default-variant" + value="" + label={t`Default`} + /> + {eventBasedObject && + mapFor( + 0, + eventBasedObject.getVariants().getVariantsCount(), + i => { + if (!eventBasedObject) { + return null; + } + const variant = eventBasedObject + .getVariants() + .getVariantAt(i); + return ( + <SelectOption + key={'variant-' + variant.getName()} + value={variant.getName()} + label={variant.getName()} + /> + ); + } + )} + </SelectField> + </ColumnStackLayout> + </> + )} + {!getVariantName( + eventBasedObject, + customObjectConfiguration + ) && + (eventBasedObject && + (!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && + !customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() ? ( + <Line alignItems="center"> + <Column expand noMargin> + <Text size="block-title">Children objects</Text> + </Column> + <Column alignItems="right"> + <FlatButton + label={ + <Trans>Override children configuration</Trans> + } + onClick={() => { + customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration( + true + ); + customObjectConfiguration.clearChildrenConfiguration(); + if (onObjectUpdated) { + onObjectUpdated(); + } + forceUpdate(); + }} + /> + </Column> + </Line> + ) : ( + <> + <Line alignItems="center"> + <Column expand noMargin> + <Text size="block-title">Children objects</Text> + </Column> + {!customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() && ( + <Column alignItems="right"> + <FlatButton + leftIcon={<RestoreIcon style={styles.icon} />} + label={ + <Trans> + Reset and hide children configuration + </Trans> + } + onClick={() => { + customObjectConfiguration.setMarkedAsOverridingEventsBasedObjectChildrenConfiguration( + false + ); + customObjectConfiguration.clearChildrenConfiguration(); + if (onObjectUpdated) { + onObjectUpdated(); + } + forceUpdate(); + }} + /> + </Column> + )} + </Line> + {mapFor( + 0, + eventBasedObject.getObjects().getObjectsCount(), + i => { + const childObject = eventBasedObject + .getObjects() + .getObjectAt(i); + const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration( + childObject.getName() + ); + const editorConfiguration = ObjectsEditorService.getEditorConfiguration( + project, + childObjectConfiguration.getType() + ); + const EditorComponent = + editorConfiguration.component; + + const objectMetadata = gd.MetadataProvider.getObjectMetadata( + gd.JsPlatform.get(), + childObjectConfiguration.getType() + ); + const iconUrl = objectMetadata.getIconFilename(); + const tutorialIds = getObjectTutorialIds( + childObjectConfiguration.getType() + ); + const enabledTutorialIds = tutorialIds.filter( + tutorialId => + !values.hiddenTutorialHints[tutorialId] + ); + // TODO EBO: Add a protection against infinite loops in case + // of object cycles (thought it should be forbidden). + return ( + <Accordion + key={childObject.getName()} + defaultExpanded + > + <AccordionHeader> + {iconUrl ? ( + <IconContainer + src={iconUrl} + alt={childObject.getName()} + size={20} + /> + ) : null} + <Column expand> + <Text size="block-title"> + {childObject.getName()} + </Text> + </Column> + </AccordionHeader> + <AccordionBody> + <Column expand noMargin noOverflowParent> + {enabledTutorialIds.length ? ( + <Line> + <ColumnStackLayout expand> + {tutorialIds.map(tutorialId => ( + <DismissableTutorialMessage + key={tutorialId} + tutorialId={tutorialId} + /> + ))} + </ColumnStackLayout> + </Line> + ) : null} + <Line noMargin> + <Column expand> + <EditorComponent + isChildObject + objectConfiguration={ + childObjectConfiguration + } + project={project} + layout={layout} + eventsFunctionsExtension={ + eventsFunctionsExtension + } + eventsBasedObject={ + eventsBasedObject + } + resourceManagementProps={ + resourceManagementProps + } + onSizeUpdated={ + forceUpdate /*Force update to ensure dialog is properly positioned*/ + } + objectName={ + objectName + + ' ' + + childObject.getName() + } + onObjectUpdated={onObjectUpdated} + unsavedChanges={unsavedChanges} + /> + </Column> + </Line> + </Column> + </AccordionBody> + </Accordion> + ); + } + )} + </> + )))} + {eventBasedObject && eventBasedObject.isAnimatable() && ( + <Column expand> + <Text size="block-title"> + <Trans>Animations</Trans> + </Text> + <AnimationList + ref={animationList} + animations={animations} + project={project} + layout={layout} + eventsFunctionsExtension={eventsFunctionsExtension} + eventsBasedObject={eventsBasedObject} + object={object} + objectName={objectName} + resourceManagementProps={resourceManagementProps} + onSizeUpdated={onSizeUpdated} + onObjectUpdated={onObjectUpdated} + isAnimationListLocked={false} + scrollView={scrollView} + onCreateMatchingSpriteCollisionMask={ + onCreateMatchingSpriteCollisionMask + } + /> + </Column> + )} + </React.Fragment> + ) : ( + <EmptyMessage> + <Trans> + There is nothing to configure for this object. You can still + use events to interact with the object. + </Trans> + </EmptyMessage> + )} + </ColumnStackLayout> + </ScrollView> + {eventBasedObject && + eventBasedObject.isAnimatable() && + !isChildObject && ( + <Column noMargin> + <ResponsiveLineStackLayout + justifyContent="space-between" + noColumnMargin + > + {!isMobile ? ( // On mobile, use only 1 button to gain space. + <ResponsiveLineStackLayout noMargin noColumnMargin> + <FlatButton + label={<Trans>Edit collision masks</Trans>} + onClick={() => setCollisionMasksEditorOpen(true)} + disabled={!hasAnyFrame(animations)} + /> + <FlatButton + label={<Trans>Edit points</Trans>} + onClick={() => setPointsEditorOpen(true)} + disabled={!hasAnyFrame(animations)} + /> + </ResponsiveLineStackLayout> + ) : ( + <FlatButtonWithSplitMenu + label={<Trans>Edit collision masks</Trans>} + onClick={() => setCollisionMasksEditorOpen(true)} + disabled={!hasAnyFrame(animations)} + buildMenuTemplate={i18n => [ + { + label: i18n._(t`Edit points`), + disabled: !hasAnyFrame(animations), + click: () => setPointsEditorOpen(true), + }, + ]} + /> + )} + <RaisedButton + label={<Trans>Add an animation</Trans>} + primary + onClick={() => { + if (!animationList.current) { + return; + } + animationList.current.addAnimation(); + }} + icon={<Add />} + /> + </ResponsiveLineStackLayout> + </Column> + )} + {pointsEditorOpen && ( + <Dialog + title={<Trans>Edit points</Trans>} + actions={[ + <RaisedButton + key="apply" + label={<Trans>Apply</Trans>} + primary + onClick={() => setPointsEditorOpen(false)} + />, + ]} + secondaryActions={[ + <HelpButton + helpPagePath="/objects/sprite/edit-points" + key="help" + />, + ]} + onRequestClose={() => setPointsEditorOpen(false)} + maxWidth="lg" + flexBody + fullHeight + open={pointsEditorOpen} + > + <PointsEditor + animations={animations} + resourcesLoader={ResourcesLoader} + project={project} + onPointsUpdated={onObjectUpdated} + onRenamedPoint={(oldName, newName) => { + if (!object) { + return; + } + if (layout) { + gd.WholeProjectRefactorer.renameObjectPointInScene( + project, + layout, + object, + oldName, + newName + ); + } else if (eventsFunctionsExtension && eventsBasedObject) { + gd.WholeProjectRefactorer.renameObjectPointInEventsBasedObject( + project, + eventsFunctionsExtension, + eventsBasedObject, + object, + oldName, + newName + ); + } + }} + /> + </Dialog> + )} + {collisionMasksEditorOpen && ( + <Dialog + title={<Trans>Edit collision masks</Trans>} + actions={[ + <RaisedButton + key="apply" + label={<Trans>Apply</Trans>} + primary + onClick={() => setCollisionMasksEditorOpen(false)} + />, + ]} + secondaryActions={[ + <HelpButton + helpPagePath="/objects/sprite/collision-mask" + key="help" + />, + ]} + maxWidth="lg" + flexBody + fullHeight + onRequestClose={() => setCollisionMasksEditorOpen(false)} + open={collisionMasksEditorOpen} + > + <CollisionMasksEditor + animations={animations} + resourcesLoader={ResourcesLoader} + project={project} + onMasksUpdated={onObjectUpdated} + onCreateMatchingSpriteCollisionMask={ + onCreateMatchingSpriteCollisionMask + } + /> + </Dialog> + )} + {newVariantDialogOpen && eventBasedObject && ( + <NewVariantDialog + initialName={ + getVariantName(eventBasedObject, customObjectConfiguration) || + i18n._(t`New variant`) + } + onApply={name => duplicateVariant(i18n, name)} + onCancel={() => { + setNewVariantDialogOpen(false); + }} + /> + )} + </> + )} + </I18n> + ); +}; + +export default CustomObjectPropertiesEditor; diff --git a/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js b/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js index 78b966023770..0c270b69f6fb 100644 --- a/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js +++ b/newIDE/app/src/ObjectEditor/Editors/EditorProps.flow.js @@ -37,4 +37,18 @@ export type EditorProps = {| scrollView?: ScrollViewInterface, renderObjectNameField?: () => React.Node, isChildObject?: boolean, + onOpenEventBasedObjectEditor?: ( + extensionName: string, + eventsBasedObjectName: string + ) => void, + onOpenEventBasedObjectVariantEditor?: ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => void, + onDeleteEventsBasedObjectVariant?: ( + eventsFunctionsExtension: gdEventsFunctionsExtension, + eventBasedObject: gdEventsBasedObject, + variant: gdEventsBasedObjectVariant + ) => void, |}; diff --git a/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js b/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js index ede51af79a8d..3a94dea21c8e 100644 --- a/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js +++ b/newIDE/app/src/ObjectEditor/ObjectEditorDialog.js @@ -61,6 +61,22 @@ type Props = {| hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, openBehaviorEvents: (extensionName: string, behaviorName: string) => void, onExtensionInstalled: (extensionName: string) => void, + onOpenEventBasedObjectEditor: ( + extensionName: string, + eventsBasedObjectName: string + ) => void, + onOpenEventBasedObjectVariantEditor: ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => void, + onDeleteEventsBasedObjectVariant: ( + eventsFunctionsExtension: gdEventsFunctionsExtension, + eventBasedObject: gdEventsBasedObject, + variant: gdEventsBasedObjectVariant + ) => void, + isBehaviorListLocked: boolean, + isVariableListLocked: boolean, |}; type InnerDialogProps = {| @@ -90,6 +106,11 @@ const InnerDialog = (props: InnerDialogProps) => { onUpdateBehaviorsSharedData, onComputeAllVariableNames, onExtensionInstalled, + onOpenEventBasedObjectEditor, + onOpenEventBasedObjectVariantEditor, + onDeleteEventsBasedObjectVariant, + isBehaviorListLocked, + isVariableListLocked, } = props; const [currentTab, setCurrentTab] = React.useState<ObjectEditorTab>( initialTab || 'properties' @@ -146,6 +167,13 @@ const InnerDialog = (props: InnerDialogProps) => { changeset, originalSerializedVariables ); + if (eventsBasedObject) { + gd.ObjectVariableHelper.applyChangesToVariants( + eventsBasedObject, + object.getName(), + changeset + ); + } object.clearPersistentUuid(); // Do the renaming *after* applying changes, as "withSerializableObject" @@ -287,6 +315,11 @@ const InnerDialog = (props: InnerDialogProps) => { autoFocus="desktop" /> )} + onOpenEventBasedObjectEditor={onOpenEventBasedObjectEditor} + onOpenEventBasedObjectVariantEditor={ + onOpenEventBasedObjectVariantEditor + } + onDeleteEventsBasedObjectVariant={onDeleteEventsBasedObjectVariant} /> </Column> ) : null} @@ -303,6 +336,7 @@ const InnerDialog = (props: InnerDialogProps) => { onBehaviorsUpdated={notifyOfChange} openBehaviorEvents={askConfirmationAndOpenBehaviorEvents} onExtensionInstalled={onExtensionInstalled} + isListLocked={isBehaviorListLocked} /> )} {currentTab === 'variables' && ( @@ -329,6 +363,7 @@ const InnerDialog = (props: InnerDialogProps) => { helpPagePath={'/all-features/variables/object-variables'} onComputeAllVariableNames={onComputeAllVariableNames} onVariablesUpdated={notifyOfChange} + isListLocked={isVariableListLocked} /> </Column> )} diff --git a/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js b/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js index a2f6bf5cedf2..f8f0026a9da8 100644 --- a/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js +++ b/newIDE/app/src/ObjectGroupEditor/EditedObjectGroupEditorDialog.js @@ -13,6 +13,7 @@ import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessag import VariablesList from '../VariablesList/VariablesList'; import HelpButton from '../UI/HelpButton'; import useValueWithInit from '../Utils/UseRefInitHook'; +import Text from '../UI/Text'; const gd: libGDevelop = global.gd; @@ -29,6 +30,8 @@ type Props = {| initialInstances: gdInitialInstancesContainer | null, initialTab: ?ObjectGroupEditorTab, onComputeAllVariableNames?: () => Array<string>, + isVariableListLocked: boolean, + isObjectListLocked: boolean, |}; const EditedObjectGroupEditorDialog = ({ @@ -42,6 +45,8 @@ const EditedObjectGroupEditorDialog = ({ initialInstances, initialTab, onComputeAllVariableNames, + isVariableListLocked, + isObjectListLocked, }: Props) => { const forceUpdate = useForceUpdate(); const { @@ -99,6 +104,16 @@ const EditedObjectGroupEditorDialog = ({ changeset, originalSerializedVariables ); + const { eventsBasedObject } = projectScopedContainersAccessor._scope; + if (eventsBasedObject) { + for (const objectName of group.getAllObjectsNames().toJSArray()) { + gd.ObjectVariableHelper.applyChangesToVariants( + eventsBasedObject, + objectName, + changeset + ); + } + } groupVariablesContainer.clearPersistentUuid(); }; @@ -169,17 +184,28 @@ const EditedObjectGroupEditorDialog = ({ /> } > - {currentTab === 'objects' && ( - <ObjectGroupEditor - project={project} - projectScopedContainersAccessor={projectScopedContainersAccessor} - globalObjectsContainer={globalObjectsContainer} - objectsContainer={objectsContainer} - groupObjectNames={group.getAllObjectsNames().toJSArray()} - onObjectAdded={addObject} - onObjectRemoved={removeObject} - /> - )} + {currentTab === 'objects' && + (isObjectListLocked && group.getAllObjectsNames().size() === 0 ? ( + <Column noMargin expand justifyContent="center"> + <Text size="block-title" align="center"> + {<Trans>Empty group</Trans>} + </Text> + <Text align="center" noMargin> + {<Trans>This object group is empty and locked.</Trans>} + </Text> + </Column> + ) : ( + <ObjectGroupEditor + project={project} + projectScopedContainersAccessor={projectScopedContainersAccessor} + globalObjectsContainer={globalObjectsContainer} + objectsContainer={objectsContainer} + groupObjectNames={group.getAllObjectsNames().toJSArray()} + onObjectAdded={addObject} + onObjectRemoved={removeObject} + isObjectListLocked={isObjectListLocked} + /> + ))} {currentTab === 'variables' && ( <Column expand noMargin> {groupVariablesContainer.count() > 0 && DismissableTutorialMessage && ( @@ -205,6 +231,7 @@ const EditedObjectGroupEditorDialog = ({ helpPagePath={'/all-features/variables/object-variables'} onComputeAllVariableNames={onComputeAllVariableNames} onVariablesUpdated={notifyOfVariableChange} + isListLocked={isVariableListLocked} /> </Column> )} diff --git a/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js b/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js index 9a818cce1998..e076d9136817 100644 --- a/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js +++ b/newIDE/app/src/ObjectGroupEditor/NewObjectGroupEditorDialog.js @@ -134,6 +134,7 @@ const NewObjectGroupEditorDialog = ({ groupObjectNames={groupObjectNames} onObjectAdded={addObject} onObjectRemoved={removeObject} + isObjectListLocked={false} /> </Dialog> ); diff --git a/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js b/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js index c0954af0bd3e..ef43af2eacc2 100644 --- a/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js +++ b/newIDE/app/src/ObjectGroupEditor/ObjectGroupEditorDialog.js @@ -30,6 +30,8 @@ type Props = {| bypassedObjectGroupsContainer?: ?gdObjectGroupsContainer, initialTab?: ?ObjectGroupEditorTab, onComputeAllVariableNames?: () => Array<string>, + isVariableListLocked: boolean, + isObjectListLocked: boolean, |}; const ObjectGroupEditorDialog = ({ @@ -45,6 +47,8 @@ const ObjectGroupEditorDialog = ({ bypassedObjectGroupsContainer, initialTab, onComputeAllVariableNames, + isVariableListLocked, + isObjectListLocked, }: Props) => { const [ editedObjectGroup, @@ -108,7 +112,8 @@ const ObjectGroupEditorDialog = ({ ); return !editedObjectGroup || - editedObjectGroup.getAllObjectsNames().size() === 0 ? ( + (editedObjectGroup.getAllObjectsNames().size() === 0 && + !isObjectListLocked) ? ( <NewObjectGroupEditorDialog project={project} projectScopedContainersAccessor={projectScopedContainersAccessor} @@ -130,6 +135,8 @@ const ObjectGroupEditorDialog = ({ initialInstances={initialInstances} initialTab={selectedTab} onComputeAllVariableNames={onComputeAllVariableNames} + isVariableListLocked={isVariableListLocked} + isObjectListLocked={isObjectListLocked} /> ); }; diff --git a/newIDE/app/src/ObjectGroupEditor/index.js b/newIDE/app/src/ObjectGroupEditor/index.js index ae4d22b3c584..0b3c68211c10 100644 --- a/newIDE/app/src/ObjectGroupEditor/index.js +++ b/newIDE/app/src/ObjectGroupEditor/index.js @@ -27,6 +27,7 @@ type Props = {| onObjectGroupUpdated?: () => void, onObjectAdded: (objectName: string) => void, onObjectRemoved: (objectName: string) => void, + isObjectListLocked: boolean, |}; const ObjectGroupEditor = ({ @@ -37,6 +38,7 @@ const ObjectGroupEditor = ({ groupObjectNames, onObjectAdded, onObjectRemoved, + isObjectListLocked, }: Props) => { const [objectName, setObjectName] = React.useState<string>(''); @@ -111,7 +113,13 @@ const ObjectGroupEditor = ({ )} /> ) : null; - return ( + return isObjectListLocked ? ( + <ListItem + key={objectName} + primaryText={objectName} + leftIcon={icon} + /> + ) : ( <ListItem key={objectName} primaryText={objectName} @@ -135,6 +143,7 @@ const ObjectGroupEditor = ({ noGroups hintText={t`Choose an object to add to the group`} fullWidth + disabled={isObjectListLocked} /> </Column> </Paper> diff --git a/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js b/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js index b4957aad6e72..2143c11b6fcb 100644 --- a/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js +++ b/newIDE/app/src/ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor.js @@ -71,6 +71,7 @@ const ObjectGroupsListWithObjectGroupEditor = ({ onGroupRenamed={onGroupsUpdated} canSetAsGlobalGroup={canSetAsGlobalGroup} unsavedChanges={unsavedChanges} + isListLocked={false} /> {(editedGroup || isCreatingNewGroup) && ( <ObjectGroupEditorDialog @@ -102,6 +103,8 @@ const ObjectGroupsListWithObjectGroupEditor = ({ } }} initialTab={'objects'} + isVariableListLocked={false} + isObjectListLocked={false} /> )} </React.Fragment> diff --git a/newIDE/app/src/ObjectGroupsList/index.js b/newIDE/app/src/ObjectGroupsList/index.js index 92361e68f7ea..45fd8842978e 100644 --- a/newIDE/app/src/ObjectGroupsList/index.js +++ b/newIDE/app/src/ObjectGroupsList/index.js @@ -109,6 +109,7 @@ type Props = {| onGroupRenamed?: () => void, canSetAsGlobalGroup?: boolean, unsavedChanges?: ?UnsavedChanges, + isListLocked: boolean, |}; const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( @@ -127,6 +128,7 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( unsavedChanges, onEditGroup, canSetAsGlobalGroup, + isListLocked, } = props; const [ selectedGroupWithContext, @@ -472,6 +474,8 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( { label: i18n._(t`Duplicate`), click: () => onDuplicate(item), + accelerator: 'CmdOrCtrl+D', + enabled: !isListLocked, }, { type: 'separator' }, { @@ -482,11 +486,13 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( { label: i18n._(t`Rename`), click: () => onEditName(item), + accelerator: 'F2', + enabled: !isListLocked, }, globalObjectGroups ? { label: i18n._(t`Set as global group`), - enabled: !isGroupWithContextGlobal(item), + enabled: !isGroupWithContextGlobal(item) && !isListLocked, click: () => setAsGlobalGroup(item), visible: canSetAsGlobalGroup !== false, } @@ -494,22 +500,26 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( { label: i18n._(t`Delete`), click: () => onDelete(item), + accelerator: 'Backspace', + enabled: !isListLocked, }, { type: 'separator' }, { label: i18n._(t`Add a new group...`), click: onCreateGroup, + enabled: !isListLocked, }, ].filter(Boolean), [ + isListLocked, + globalObjectGroups, + canSetAsGlobalGroup, onCreateGroup, - onEditName, - editItem, - onDelete, onDuplicate, - canSetAsGlobalGroup, + editItem, + onEditName, setAsGlobalGroup, - globalObjectGroups, + onDelete, ] ); @@ -521,9 +531,10 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( label: i18n._(t`Add a new group`), click: onCreateGroup, id: 'add-new-group-top-button', + enabled: !isListLocked, } : null, - [onCreateGroup] + [isListLocked, onCreateGroup] ); const labels = React.useMemo( @@ -578,23 +589,29 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( () => { if (keyboardShortcutsRef.current) { keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => { - if (!selectedGroupWithContext) return; + if (!selectedGroupWithContext || isListLocked) return; onDelete(selectedGroupWithContext); }); keyboardShortcutsRef.current.setShortcutCallback( 'onDuplicate', () => { - if (!selectedGroupWithContext) return; + if (!selectedGroupWithContext || isListLocked) return; onDuplicate(selectedGroupWithContext); } ); keyboardShortcutsRef.current.setShortcutCallback('onRename', () => { - if (!selectedGroupWithContext) return; + if (!selectedGroupWithContext || isListLocked) return; onEditName(selectedGroupWithContext); }); } }, - [selectedGroupWithContext, onDelete, onDuplicate, onEditName] + [ + selectedGroupWithContext, + onDelete, + onDuplicate, + onEditName, + isListLocked, + ] ); // Force List component to be mounted again if globalObjectGroups or objectGroups @@ -687,6 +704,7 @@ const ObjectGroupsList = React.forwardRef<Props, ObjectGroupsListInterface>( onClick={onCreateGroup} id="add-new-group-button" icon={<Add />} + disabled={isListLocked} /> </Column> </Line> diff --git a/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js b/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js index 6ba54b867116..2354884ddee7 100644 --- a/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js +++ b/newIDE/app/src/ObjectsList/ObjectFolderTreeViewItemContent.js @@ -72,6 +72,7 @@ export type ObjectFolderTreeViewItemProps = {| ) => void, forceUpdateList: () => void, forceUpdate: () => void, + isListLocked: boolean, |}; export const getObjectFolderTreeViewItemId = ( @@ -197,6 +198,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { onAddNewObject, onMovedObjectFolderOrObjectToAnotherFolderInSameContainer, forceUpdate, + isListLocked, } = this.props; const container = this._isGlobal @@ -222,54 +224,61 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { isGlobalObject: this._isGlobal, isFolder: true, }), - enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND), + enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND) && !isListLocked, click: () => this.paste(), }, { label: i18n._(t`Rename`), click: () => this.props.editName(this.getId()), accelerator: 'F2', + enabled: !isListLocked, }, { label: i18n._(t`Delete`), click: () => this.delete(), accelerator: 'Backspace', + enabled: !isListLocked, }, - { - label: i18n._('Move to folder'), - submenu: [ - ...filteredFolderAndPathsInContainer.map(({ folder, path }) => ({ - label: path, - enabled: folder !== this.objectFolder.getParent(), - click: () => { - if (folder === this.objectFolder.getParent()) return; - this.objectFolder - .getParent() - .moveObjectFolderOrObjectToAnotherFolder( - this.objectFolder, - folder, - 0 - ); - onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({ - objectFolderOrObject: folder, - global: this._isGlobal, - }); - }, - })), - - { type: 'separator' }, - { - label: i18n._(t`Create new folder...`), - click: () => - addFolder([ - { - objectFolderOrObject: this.objectFolder.getParent(), - global: this._isGlobal, + isListLocked + ? { + label: i18n._('Move to folder'), + enabled: false, + } + : { + label: i18n._('Move to folder'), + submenu: [ + ...filteredFolderAndPathsInContainer.map(({ folder, path }) => ({ + label: path, + enabled: folder !== this.objectFolder.getParent(), + click: () => { + if (folder === this.objectFolder.getParent()) return; + this.objectFolder + .getParent() + .moveObjectFolderOrObjectToAnotherFolder( + this.objectFolder, + folder, + 0 + ); + onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({ + objectFolderOrObject: folder, + global: this._isGlobal, + }); }, - ]), + })), + + { type: 'separator' }, + { + label: i18n._(t`Create new folder...`), + click: () => + addFolder([ + { + objectFolderOrObject: this.objectFolder.getParent(), + global: this._isGlobal, + }, + ]), + }, + ], }, - ], - }, ...renderQuickCustomizationMenuItems({ i18n, visibility: this.objectFolder.getQuickCustomizationVisibility(), @@ -286,6 +295,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { objectFolderOrObject: this.objectFolder, global: this._isGlobal, }), + enabled: !isListLocked, }, { label: i18n._(t`Add a new folder`), @@ -293,6 +303,7 @@ export class ObjectFolderTreeViewItemContent implements TreeViewItemContent { addFolder([ { objectFolderOrObject: this.objectFolder, global: this._isGlobal }, ]), + enabled: !isListLocked, }, { type: 'separator' }, { diff --git a/newIDE/app/src/ObjectsList/ObjectSelector.js b/newIDE/app/src/ObjectsList/ObjectSelector.js index b0eb4ce4812b..5a16a28fea56 100644 --- a/newIDE/app/src/ObjectsList/ObjectSelector.js +++ b/newIDE/app/src/ObjectsList/ObjectSelector.js @@ -45,6 +45,7 @@ type Props = {| onApply?: () => void, value: string, errorTextIfInvalid?: React.Node, + disabled?: boolean, fullWidth?: boolean, floatingLabelText?: React.Node, @@ -189,6 +190,7 @@ const ObjectSelector = React.forwardRef<Props, ObjectSelectorInterface>( hintText, requiredCapabilitiesBehaviorTypes, requiredVisibleBehaviorTypes, + disabled, ...otherProps } = props; @@ -241,7 +243,7 @@ const ObjectSelector = React.forwardRef<Props, ObjectSelectorInterface>( undefined ); - return shouldAutofocusInput ? ( + return disabled ? null : shouldAutofocusInput ? ( <SemiControlledAutoComplete margin={margin} hintText={hintText || t`Choose an object`} diff --git a/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js b/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js index 2eeb1a7e94c8..f751ed2ec4e9 100644 --- a/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js +++ b/newIDE/app/src/ObjectsList/ObjectTreeViewItemContent.js @@ -44,6 +44,11 @@ export type ObjectTreeViewItemCallbacks = {| extensionName: string, eventsBasedObjectName: string ) => void, + onOpenEventBasedObjectVariantEditor: ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => void, onRenameObjectFolderOrObjectWithContextFinish: ( objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext, newName: string, @@ -82,6 +87,7 @@ export type ObjectTreeViewItemProps = {| addFolder: (items: Array<ObjectFolderOrObjectWithContext>) => void, forceUpdateList: () => void, forceUpdate: () => void, + isListLocked: boolean, |}; export const addSerializedObjectToObjectsContainer = ({ @@ -279,9 +285,10 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent { swapObjectAsset, canSetAsGlobalObject, setAsGlobalObject, - onOpenEventBasedObjectEditor, + onOpenEventBasedObjectVariantEditor, selectObjectFolderOrObjectWithContext, addFolder, + isListLocked, } = this.props; const container = this._isGlobal @@ -312,29 +319,33 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent { { label: i18n._(t`Cut`), click: () => this.cut(), + enabled: !isListLocked, }, { label: this._getPasteLabel(i18n, { isGlobalObject: this._isGlobal, isFolder: false, }), - enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND), + enabled: Clipboard.has(OBJECT_CLIPBOARD_KIND) && !isListLocked, click: () => this.paste(), }, { label: i18n._(t`Duplicate`), click: () => this.duplicate(), accelerator: 'CmdOrCtrl+D', + enabled: !isListLocked, }, { label: i18n._(t`Rename`), click: () => this.props.editName(this.getId()), accelerator: 'F2', + enabled: !isListLocked, }, { label: i18n._(t`Delete`), click: () => this.delete(), accelerator: 'Backspace', + enabled: !isListLocked, }, { type: 'separator' }, { @@ -359,15 +370,20 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent { project.hasEventsBasedObject(object.getType()) ? { label: i18n._(t`Edit children`), - click: () => - onOpenEventBasedObjectEditor( + click: () => { + const customObjectConfiguration = gd.asCustomObjectConfiguration( + object.getConfiguration() + ); + onOpenEventBasedObjectVariantEditor( gd.PlatformExtension.getExtensionFromFullObjectType( object.getType() ), gd.PlatformExtension.getObjectNameFromFullObjectType( object.getType() - ) - ), + ), + customObjectConfiguration.getVariantName() + ); + }, } : null, { type: 'separator' }, @@ -383,46 +399,51 @@ export class ObjectTreeViewItemContent implements TreeViewItemContent { { type: 'separator' }, globalObjectsContainer && { label: i18n._(t`Set as global object`), - enabled: !this._isGlobal, + enabled: !this._isGlobal && !isListLocked, click: () => { selectObjectFolderOrObjectWithContext(null); setAsGlobalObject({ i18n, objectFolderOrObject: this.object }); }, visible: canSetAsGlobalObject !== false, }, - { - label: i18n._('Move to folder'), - submenu: [ - ...folderAndPathsInContainer.map(({ folder, path }) => ({ - label: path, - enabled: folder !== this.object.getParent(), - click: () => { - this.object - .getParent() - .moveObjectFolderOrObjectToAnotherFolder( - this.object, - folder, - 0 - ); - onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({ - objectFolderOrObject: folder, - global: this._isGlobal, - }); - }, - })), - { type: 'separator' }, - { - label: i18n._(t`Create new folder...`), - click: () => - addFolder([ - { - objectFolderOrObject: this.object.getParent(), - global: this._isGlobal, + isListLocked + ? { + label: i18n._('Move to folder'), + enabled: false, + } + : { + label: i18n._('Move to folder'), + submenu: [ + ...folderAndPathsInContainer.map(({ folder, path }) => ({ + label: path, + enabled: folder !== this.object.getParent(), + click: () => { + this.object + .getParent() + .moveObjectFolderOrObjectToAnotherFolder( + this.object, + folder, + 0 + ); + onMovedObjectFolderOrObjectToAnotherFolderInSameContainer({ + objectFolderOrObject: folder, + global: this._isGlobal, + }); }, - ]), + })), + { type: 'separator' }, + { + label: i18n._(t`Create new folder...`), + click: () => + addFolder([ + { + objectFolderOrObject: this.object.getParent(), + global: this._isGlobal, + }, + ]), + }, + ], }, - ], - }, { type: 'separator' }, { label: i18n._(t`Add instance to the scene`), diff --git a/newIDE/app/src/ObjectsList/index.js b/newIDE/app/src/ObjectsList/index.js index f4b30631924b..3962ff0e07c9 100644 --- a/newIDE/app/src/ObjectsList/index.js +++ b/newIDE/app/src/ObjectsList/index.js @@ -280,6 +280,7 @@ class LabelTreeViewItemContent implements TreeViewItemContent { id: rightButton.id, label: i18n._(rightButton.label), click: rightButton.click, + enabled: rightButton.enabled, } : null, ...(buildMenuTemplateFunction ? buildMenuTemplateFunction() : []), @@ -463,6 +464,11 @@ type Props = {| extensionName: string, eventsBasedObjectName: string ) => void, + onOpenEventBasedObjectVariantEditor: ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => void, onExportAssets: () => void, onObjectCreated: gdObject => void, onObjectEdited: ObjectWithContext => void, @@ -479,6 +485,7 @@ type Props = {| ) => string, unsavedChanges?: ?UnsavedChanges, hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, + isListLocked: boolean, |}; const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( @@ -502,6 +509,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( onEditObject, onOpenEventBasedObjectEditor, + onOpenEventBasedObjectVariantEditor, onExportAssets, onObjectCreated, onObjectEdited, @@ -513,6 +521,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( getThumbnail, unsavedChanges, hotReloadPreviewButtonProps, + isListLocked, }: Props, ref ) => { @@ -981,6 +990,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( onAddObjectInstance, initialInstances, onOpenEventBasedObjectEditor, + onOpenEventBasedObjectVariantEditor, getValidatedObjectOrGroupName, onRenameObjectFolderOrObjectWithContextFinish, onObjectModified, @@ -994,6 +1004,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( addFolder, forceUpdateList, forceUpdate, + isListLocked, }), [ project, @@ -1007,6 +1018,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( onAddObjectInstance, initialInstances, onOpenEventBasedObjectEditor, + onOpenEventBasedObjectVariantEditor, getValidatedObjectOrGroupName, onRenameObjectFolderOrObjectWithContextFinish, onObjectModified, @@ -1020,6 +1032,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( addFolder, forceUpdateList, forceUpdate, + isListLocked, ] ); @@ -1041,6 +1054,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( showDeleteConfirmation, forceUpdateList, forceUpdate, + isListLocked, }), [ project, @@ -1059,6 +1073,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( showDeleteConfirmation, forceUpdateList, forceUpdate, + isListLocked, ] ); @@ -1143,6 +1158,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( onAddNewObject(selectedObjectFolderOrObjectsWithContext[0]); }, id: 'add-new-object-top-button', + enabled: !isListLocked, }, () => [ { @@ -1154,6 +1170,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( global: false, }, ]), + enabled: !isListLocked, }, { type: 'separator' }, { @@ -1184,16 +1201,18 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( return treeViewItems; }, [ - addFolder, - expandFolders, globalObjectsRootFolder, - objectFolderTreeViewItemProps, + labels.higherScopeObjectsTitle, + labels.localScopeObjectsTitle, objectTreeViewItemProps, + objectFolderTreeViewItemProps, objectsRootFolder, + isListLocked, + addFolder, + expandFolders, onAddNewObject, - onExportAssets, selectedObjectFolderOrObjectsWithContext, - labels, + onExportAssets, ] ); @@ -1221,20 +1240,31 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( () => { if (keyboardShortcutsRef.current) { keyboardShortcutsRef.current.setShortcutCallback('onDelete', () => { - deleteItem(selectedItems[0]); + if (!isListLocked) { + deleteItem(selectedItems[0]); + } }); keyboardShortcutsRef.current.setShortcutCallback( 'onDuplicate', () => { - duplicateItem(selectedItems[0]); + if (!isListLocked) { + duplicateItem(selectedItems[0]); + } } ); keyboardShortcutsRef.current.setShortcutCallback('onRename', () => { - editName(selectedItems[0].content.getId()); + if (!isListLocked) { + editName(selectedItems[0].content.getId()); + } }); } }, - [selectedObjectFolderOrObjectsWithContext, editName, selectedItems] + [ + selectedObjectFolderOrObjectsWithContext, + editName, + selectedItems, + isListLocked, + ] ); const canMoveSelectionTo = React.useCallback( @@ -1549,6 +1579,7 @@ const ObjectsList = React.forwardRef<Props, ObjectsListInterface>( } id="add-new-object-button" icon={<Add />} + disabled={isListLocked} /> </Column> </Line> diff --git a/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js b/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js index 6a2c74675fa2..cd3ce3913965 100644 --- a/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js +++ b/newIDE/app/src/ObjectsRendering/ObjectsRenderingService.js @@ -83,7 +83,8 @@ const ObjectsRenderingService = { instance: gdInitialInstance, associatedObjectConfiguration: gdObjectConfiguration, pixiContainer: PIXI.Container, - threeGroup: THREE.Group | null + threeGroup: THREE.Group | null, + propertyOverridings: Map<string, string> = new Map<string, string>() ): RenderedInstance | Rendered3DInstance { const objectType = associatedObjectConfiguration.getType(); if (threeGroup && this.renderers3D.hasOwnProperty(objectType)) { @@ -101,7 +102,8 @@ const ObjectsRenderingService = { instance, associatedObjectConfiguration, pixiContainer, - PixiResourcesLoader + PixiResourcesLoader, + propertyOverridings ); else { if (project.hasEventsBasedObject(objectType)) { @@ -135,7 +137,8 @@ const ObjectsRenderingService = { associatedObjectConfiguration, pixiContainer, threeGroup, - PixiResourcesLoader + PixiResourcesLoader, + propertyOverridings ); } } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js index 1c65d088a113..1e0434117111 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.js @@ -63,10 +63,10 @@ const getDefaultAnchor = () => ({ * Build the layouts description from the custom object properties. */ export const getObjectAnchor = ( - eventBasedObject: gdEventsBasedObject, + eventBasedObjectVariant: gdEventsBasedObjectVariant, objectName: string ): ObjectAnchor => { - const objects = eventBasedObject.getObjects(); + const objects = eventBasedObjectVariant.getObjects(); if (!objects.hasObjectNamed(objectName)) { return getDefaultAnchor(); } @@ -309,7 +309,7 @@ export interface ChildRenderedInstance { export interface LayoutedParent< CovariantChildRenderedInstance: ChildRenderedInstance > { - eventBasedObject: gdEventsBasedObject | null; + getVariant(): gdEventsBasedObjectVariant | null; getWidth(): number; getHeight(): number; getRendererOfInstance: ( @@ -322,8 +322,8 @@ export const getLayoutedRenderedInstance = <T: ChildRenderedInstance>( parent: LayoutedParent<T>, initialInstance: gdInitialInstance ): T | null => { - const eventBasedObject = parent.eventBasedObject; - if (!eventBasedObject) { + const eventBasedObjectVariant = parent.getVariant(); + if (!eventBasedObjectVariant) { return null; } @@ -333,7 +333,7 @@ export const getLayoutedRenderedInstance = <T: ChildRenderedInstance>( ); const objectAnchor = getObjectAnchor( - eventBasedObject, + eventBasedObjectVariant, layoutedInstance.getObjectName() ); const leftEdgeAnchor = objectAnchor @@ -349,10 +349,10 @@ export const getLayoutedRenderedInstance = <T: ChildRenderedInstance>( ? objectAnchor.bottomEdgeAnchor : gd.CustomObjectConfiguration.NoAnchor; - const parentInitialMinX = eventBasedObject.getAreaMinX(); - const parentInitialMinY = eventBasedObject.getAreaMinY(); - const parentInitialMaxX = eventBasedObject.getAreaMaxX(); - const parentInitialMaxY = eventBasedObject.getAreaMaxY(); + const parentInitialMinX = eventBasedObjectVariant.getAreaMinX(); + const parentInitialMinY = eventBasedObjectVariant.getAreaMinY(); + const parentInitialMaxX = eventBasedObjectVariant.getAreaMaxX(); + const parentInitialMaxY = eventBasedObjectVariant.getAreaMaxY(); const parentInitialWidth = parentInitialMaxX - parentInitialMinX; const parentInitialHeight = parentInitialMaxY - parentInitialMinY; diff --git a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js index c6418604df01..6df7d031bbe3 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/CustomObjectLayoutingModel.spec.js @@ -624,7 +624,7 @@ type MockedRenderedInstanceConfiguration = {| |}; class MockedParent implements LayoutedParent<MockedChildRenderedInstance> { - eventBasedObject: gdEventsBasedObject | null; + getVariant: () => gdEventsBasedObjectVariant | null; width: number; height: number; renderedInstances = new Map<number, MockedChildRenderedInstance>(); @@ -639,7 +639,7 @@ class MockedParent implements LayoutedParent<MockedChildRenderedInstance> { width: number, height: number ) { - this.eventBasedObject = eventBasedObject; + this.getVariant = () => eventBasedObject.getDefaultVariant(); this.width = width; this.height = height; } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js index 5dfe955ec317..0d456a5613e4 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/Rendered3DInstance.js @@ -19,6 +19,7 @@ export default class Rendered3DInstance { _threeObject: THREE.Object3D | null; wasUsed: boolean; _wasDestroyed: boolean; + _propertyOverridings: Map<string, string>; constructor( project: gdProject, @@ -26,7 +27,8 @@ export default class Rendered3DInstance { associatedObjectConfiguration: gdObjectConfiguration, pixiContainer: PIXI.Container, threeGroup: THREE.Group, - pixiResourcesLoader: Class<PixiResourcesLoader> + pixiResourcesLoader: Class<PixiResourcesLoader>, + propertyOverridings: Map<string, string> = new Map<string, string>() ) { this._pixiObject = null; this._threeObject = null; @@ -36,6 +38,7 @@ export default class Rendered3DInstance { this._threeGroup = threeGroup; this._project = project; this._pixiResourcesLoader = pixiResourcesLoader; + this._propertyOverridings = propertyOverridings; this.wasUsed = true; //Used by InstancesRenderer to track rendered instance that are not used anymore. this._wasDestroyed = false; } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js index 24ceca0a6981..0efab5dd0b00 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedCustomObjectInstance.js @@ -1,6 +1,7 @@ // @flow import RenderedInstance from './RenderedInstance'; import Rendered3DInstance from './Rendered3DInstance'; +import RenderedUnknownInstance from './RenderedUnknownInstance'; import PixiResourcesLoader from '../PixiResourcesLoader'; import ResourcesLoader from '../../ResourcesLoader'; import ObjectsRenderingService from '../ObjectsRenderingService'; @@ -14,6 +15,53 @@ import * as THREE from 'three'; const gd: libGDevelop = global.gd; +const getVariant = ( + eventBasedObject: gdEventsBasedObject, + customObjectConfiguration: gdCustomObjectConfiguration +): gdEventsBasedObjectVariant => { + const variants = eventBasedObject.getVariants(); + const variantName = customObjectConfiguration.getVariantName(); + return variants.hasVariantNamed(variantName) + ? variants.getVariant(variantName) + : eventBasedObject.getDefaultVariant(); +}; + +type PropertyMappingRule = { + targetChild: string, + targetProperty: string, + sourceProperty: string, +}; + +const getPropertyMappingRules = ( + eventBasedObject: gdEventsBasedObject +): Array<PropertyMappingRule> => { + const properties = eventBasedObject.getPropertyDescriptors(); + if (!properties.has('_PropertyMapping')) { + return []; + } + const extraInfos = properties + .get('_PropertyMapping') + .getExtraInfo() + .toJSArray(); + return extraInfos + .map(extraInfo => { + const mapping = extraInfo.split('='); + if (mapping.length < 2) { + return null; + } + const targetPath = mapping[0].split('.'); + if (mapping.length < 2) { + return null; + } + return { + targetChild: targetPath[0], + targetProperty: targetPath[1], + sourceProperty: mapping[1], + }; + }) + .filter(Boolean); +}; + /** * Renderer for gd.CustomObject (the class is not exposed to newIDE) */ @@ -27,6 +75,7 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance layoutedInstances = new Map<number, LayoutedInstance>(); renderedInstances = new Map<number, RenderedInstance | Rendered3DInstance>(); + _propertyMappingRules: Array<PropertyMappingRule>; constructor( project: gdProject, @@ -34,7 +83,8 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance associatedObjectConfiguration: gdObjectConfiguration, pixiContainer: PIXI.Container, threeGroup: THREE.Group, - pixiResourcesLoader: Class<PixiResourcesLoader> + pixiResourcesLoader: Class<PixiResourcesLoader>, + propertyOverridings: Map<string, string> ) { super( project, @@ -42,7 +92,8 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance associatedObjectConfiguration, pixiContainer, threeGroup, - pixiResourcesLoader + pixiResourcesLoader, + propertyOverridings ); // Setup the PIXI object: @@ -71,6 +122,7 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance if (!eventBasedObject) { return; } + this._propertyMappingRules = getPropertyMappingRules(eventBasedObject); this._isRenderedIn3D = eventBasedObject.isRenderedIn3D(); // Functor used to render an instance @@ -142,21 +194,58 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance ): RenderedInstance | Rendered3DInstance => { let renderedInstance = this.renderedInstances.get(instance.ptr); if (!renderedInstance) { + // No renderer associated yet, the instance must have been just created!... + let childObjectConfiguration = null; + const variant = this.getVariant(); + if (variant) { + const childObjects = variant.getObjects(); + if (childObjects.hasObjectNamed(instance.getObjectName())) { + const childObject = childObjects.getObject(instance.getObjectName()); + childObjectConfiguration = childObject.getConfiguration(); + } + } + // Apply property mapping rules on the child instance. + const childPropertyOverridings = new Map<string, string>(); const customObjectConfiguration = gd.asCustomObjectConfiguration( this._associatedObjectConfiguration ); - //No renderer associated yet, the instance must have been just created!... - const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration( - instance.getObjectName() - ); + const customObjectProperties = customObjectConfiguration.getProperties(); + for (const propertyMappingRule of this._propertyMappingRules) { + if (propertyMappingRule.targetChild !== instance.getObjectName()) { + continue; + } + const sourceValue = this._propertyOverridings.has( + propertyMappingRule.sourceProperty + ) + ? this._propertyOverridings.get(propertyMappingRule.sourceProperty) + : customObjectProperties + .get(propertyMappingRule.sourceProperty) + .getValue(); + if (sourceValue !== undefined) { + childPropertyOverridings.set( + propertyMappingRule.targetProperty, + sourceValue + ); + } + } //...so let's create a renderer. - renderedInstance = ObjectsRenderingService.createNewInstanceRenderer( - this._project, - instance, - childObjectConfiguration, - this._pixiObject, - this._threeObject - ); + renderedInstance = childObjectConfiguration + ? ObjectsRenderingService.createNewInstanceRenderer( + this._project, + instance, + childObjectConfiguration, + this._pixiObject, + this._threeObject, + childPropertyOverridings + ) + : new RenderedUnknownInstance( + this._project, + instance, + // $FlowFixMe It's not actually used. + null, + this._pixiObject, + PixiResourcesLoader + ); this.renderedInstances.set(instance.ptr, renderedInstance); } return renderedInstance; @@ -260,12 +349,20 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance } return 'res/unknown32.png'; } - const childObjects = eventBasedObject.getObjects(); + const variant = getVariant(eventBasedObject, customObjectConfiguration); + const childObjects = variant.getObjects(); for (let i = 0; i < childObjects.getObjectsCount(); i++) { const childObject = childObjects.getObjectAt(i); - const childObjectConfiguration = customObjectConfiguration.getChildObjectConfiguration( - childObject.getName() - ); + const childObjectConfiguration = + customObjectConfiguration.isForcedToOverrideEventsBasedObjectChildrenConfiguration() || + customObjectConfiguration.isMarkedAsOverridingEventsBasedObjectChildrenConfiguration() + ? customObjectConfiguration.getChildObjectConfiguration( + childObject.getName() + ) + : variant + .getObjects() + .getObject(childObject.getName()) + .getConfiguration(); const childType = childObjectConfiguration.getType(); if ( childType === 'Sprite' || @@ -291,12 +388,25 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance }); } - update() { + getVariant(): gdEventsBasedObjectVariant | null { const { eventBasedObject } = this; if (!eventBasedObject) { + return null; + } + const customObjectConfiguration = gd.asCustomObjectConfiguration( + this._associatedObjectConfiguration + ); + return getVariant(eventBasedObject, customObjectConfiguration); + } + + update() { + const { eventBasedObject } = this; + const variant = this.getVariant(); + if (!eventBasedObject || !variant) { return; } - const layers = eventBasedObject.getLayers(); + + const layers = variant.getLayers(); for ( let layerIndex = 0; layerIndex < layers.getLayersCount(); @@ -304,13 +414,11 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance ) { const layer = layers.getLayerAt(layerIndex); if (layer.getVisibility()) { - eventBasedObject - .getInitialInstances() - .iterateOverInstancesWithZOrdering( - // $FlowFixMe - gd.castObject is not supporting typings. - this.instancesRenderer, - layer.getName() - ); + variant.getInitialInstances().iterateOverInstancesWithZOrdering( + // $FlowFixMe - gd.castObject is not supporting typings. + this.instancesRenderer, + layer.getName() + ); } } this._updatePixiObjectsZOrder(); @@ -346,13 +454,10 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance threeObject.scale.set(scaleX, scaleY, scaleZ); } - const { eventBasedObject } = this; const unscaledCenterX = - this.getDefaultWidth() / 2 + - (eventBasedObject ? eventBasedObject.getAreaMinX() : 0); + this.getDefaultWidth() / 2 + variant.getAreaMinX(); const unscaledCenterY = - this.getDefaultHeight() / 2 + - (eventBasedObject ? eventBasedObject.getAreaMinY() : 0); + this.getDefaultHeight() / 2 + variant.getAreaMinY(); this._pixiObject.pivot.x = unscaledCenterX; this._pixiObject.pivot.y = unscaledCenterY; @@ -419,57 +524,44 @@ export default class RenderedCustomObjectInstance extends Rendered3DInstance } getDefaultWidth() { - const { eventBasedObject } = this; - return eventBasedObject - ? eventBasedObject.getAreaMaxX() - eventBasedObject.getAreaMinX() - : 48; + const variant = this.getVariant(); + return variant ? variant.getAreaMaxX() - variant.getAreaMinX() : 48; } getDefaultHeight() { - const { eventBasedObject } = this; - return eventBasedObject - ? eventBasedObject.getAreaMaxY() - eventBasedObject.getAreaMinY() - : 48; + const variant = this.getVariant(); + return variant ? variant.getAreaMaxY() - variant.getAreaMinY() : 48; } getDefaultDepth() { - const { eventBasedObject } = this; - return eventBasedObject - ? eventBasedObject.getAreaMaxZ() - eventBasedObject.getAreaMinZ() - : 48; + const variant = this.getVariant(); + return variant ? variant.getAreaMaxZ() - variant.getAreaMinZ() : 48; } getOriginX(): number { - const { eventBasedObject } = this; - if (!eventBasedObject) { + const variant = this.getVariant(); + if (!variant) { return 0; } - return ( - (-eventBasedObject.getAreaMinX() / this.getDefaultWidth()) * - this.getWidth() - ); + return (-variant.getAreaMinX() / this.getDefaultWidth()) * this.getWidth(); } getOriginY(): number { - const { eventBasedObject } = this; - if (!eventBasedObject) { + const variant = this.getVariant(); + if (!variant) { return 0; } return ( - (-eventBasedObject.getAreaMinY() / this.getDefaultHeight()) * - this.getHeight() + (-variant.getAreaMinY() / this.getDefaultHeight()) * this.getHeight() ); } getOriginZ(): number { - const { eventBasedObject } = this; - if (!eventBasedObject) { + const variant = this.getVariant(); + if (!variant) { return 0; } - return ( - (-eventBasedObject.getAreaMinZ() / this.getDefaultDepth()) * - this.getDepth() - ); + return (-variant.getAreaMinZ() / this.getDefaultDepth()) * this.getDepth(); } getCenterX() { diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js index 1605d5ca3633..8bcadecc8e15 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedInstance.js @@ -15,13 +15,15 @@ export default class RenderedInstance { _pixiObject: PIXI.DisplayObject; wasUsed: boolean; _wasDestroyed: boolean; + _propertyOverridings: Map<string, string>; constructor( project: gdProject, instance: gdInitialInstance, associatedObjectConfiguration: gdObjectConfiguration, pixiContainer: PIXI.Container, - pixiResourcesLoader: Class<PixiResourcesLoader> + pixiResourcesLoader: Class<PixiResourcesLoader>, + propertyOverridings: Map<string, string> = new Map<string, string>() ) { this._pixiObject = null; this._instance = instance; @@ -29,6 +31,7 @@ export default class RenderedInstance { this._pixiContainer = pixiContainer; this._project = project; this._pixiResourcesLoader = pixiResourcesLoader; + this._propertyOverridings = propertyOverridings; this.wasUsed = true; //Used by InstancesRenderer to track rendered instance that are not used anymore. this._wasDestroyed = false; } diff --git a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js index 630b56c11502..8a0fe6602766 100644 --- a/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js +++ b/newIDE/app/src/ObjectsRendering/Renderers/RenderedTextInstance.js @@ -38,14 +38,16 @@ export default class RenderedTextInstance extends RenderedInstance { instance: gdInitialInstance, associatedObjectConfiguration: gdObjectConfiguration, pixiContainer: PIXI.Container, - pixiResourcesLoader: Class<PixiResourcesLoader> + pixiResourcesLoader: Class<PixiResourcesLoader>, + propertyOverridings: Map<string, string> ) { super( project, instance, associatedObjectConfiguration, pixiContainer, - pixiResourcesLoader + pixiResourcesLoader, + propertyOverridings ); const style = new PIXI.TextStyle({ @@ -84,7 +86,9 @@ export default class RenderedTextInstance extends RenderedInstance { const textObjectConfiguration = gd.asTextObjectConfiguration( this._associatedObjectConfiguration ); - this._pixiObject.text = textObjectConfiguration.getText(); + this._pixiObject.text = this._propertyOverridings.has('Text') + ? this._propertyOverridings.get('Text') + : textObjectConfiguration.getText(); //Update style, only if needed to avoid destroying text rendering performances if ( diff --git a/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js b/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js index 82100d231640..73c10d5d624d 100644 --- a/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js +++ b/newIDE/app/src/ProjectManager/InstalledExtensionDetails.js @@ -14,7 +14,7 @@ type Props = {| onClose: () => void, extensionShortHeader: ExtensionShortHeader, extensionName: string, - onInstallExtension: ExtensionShortHeader => void, + onInstallExtension: (extensionName: string) => void, onOpenEventsFunctionsExtension: string => void, |}; @@ -34,7 +34,7 @@ function InstalledExtensionDetails({ const installOrUpdateExtension = async (i18n: I18nType) => { setIsInstalling(true); try { - onInstallExtension(extensionShortHeader); + onInstallExtension(extensionShortHeader.name); await installExtension( i18n, project, diff --git a/newIDE/app/src/ProjectManager/index.js b/newIDE/app/src/ProjectManager/index.js index 3d56d4a72ba7..fe935a68280d 100644 --- a/newIDE/app/src/ProjectManager/index.js +++ b/newIDE/app/src/ProjectManager/index.js @@ -419,7 +419,7 @@ type Props = {| onReloadEventsFunctionsExtensions: () => void, isOpen: boolean, hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, - onInstallExtension: ExtensionShortHeader => void, + onInstallExtension: (extensionName: string) => void, onShareProject: () => void, onOpenHomePage: () => void, toggleProjectManager: () => void, @@ -1421,6 +1421,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>( setProjectVariablesEditorOpen(false); }} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} + isListLocked={false} /> )} {project && !!editedPropertiesLayout && ( @@ -1451,6 +1452,7 @@ const ProjectManager = React.forwardRef<Props, ProjectManagerInterface>( onOpenLayoutVariables(null); }} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} + isListLocked={false} /> )} {project && extensionsSearchDialogOpen && ( diff --git a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js index 4eddf1548f96..e70ba2b542d9 100644 --- a/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js +++ b/newIDE/app/src/SceneEditor/EditorsDisplay.flow.js @@ -25,6 +25,7 @@ export type SceneEditorsDisplayProps = {| layout: gdLayout | null, eventsFunctionsExtension: gdEventsFunctionsExtension | null, eventsBasedObject: gdEventsBasedObject | null, + eventsBasedObjectVariant: gdEventsBasedObjectVariant | null, layersContainer: gdLayersContainer, globalObjectsContainer: gdObjectsContainer | null, objectsContainer: gdObjectsContainer, @@ -46,6 +47,11 @@ export type SceneEditorsDisplayProps = {| extensionName: string, eventsBasedObjectName: string ) => void, + onOpenEventBasedObjectVariantEditor: ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => void, selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[], onSelectLayer: (layerName: string) => void, editLayerEffects: (layer: ?gdLayer) => void, diff --git a/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js b/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js index 22ab49b8188f..5c2ff0cc89f4 100644 --- a/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js +++ b/newIDE/app/src/SceneEditor/EventsBasedObjectScenePropertiesDialog.js @@ -11,15 +11,19 @@ import Checkbox from '../UI/Checkbox'; type Props = {| eventsBasedObject: gdEventsBasedObject, + eventsBasedObjectVariant: gdEventsBasedObjectVariant, project: gdProject, onApply: () => void, onClose: () => void, getContentAABB: () => Rectangle | null, - onEventsBasedObjectChildrenEdited: () => void, + onEventsBasedObjectChildrenEdited: ( + eventsBasedObject: gdEventsBasedObject + ) => void, |}; const EventsBasedObjectScenePropertiesDialog = ({ eventsBasedObject, + eventsBasedObjectVariant, project, onApply, onClose, @@ -27,22 +31,22 @@ const EventsBasedObjectScenePropertiesDialog = ({ onEventsBasedObjectChildrenEdited, }: Props) => { const [areaMinX, setAreaMinX] = React.useState<number>( - eventsBasedObject.getAreaMinX() + eventsBasedObjectVariant.getAreaMinX() ); const [areaMinY, setAreaMinY] = React.useState<number>( - eventsBasedObject.getAreaMinY() + eventsBasedObjectVariant.getAreaMinY() ); const [areaMinZ, setAreaMinZ] = React.useState<number>( - eventsBasedObject.getAreaMinZ() + eventsBasedObjectVariant.getAreaMinZ() ); const [areaMaxX, setAreaMaxX] = React.useState<number>( - eventsBasedObject.getAreaMaxX() + eventsBasedObjectVariant.getAreaMaxX() ); const [areaMaxY, setAreaMaxY] = React.useState<number>( - eventsBasedObject.getAreaMaxY() + eventsBasedObjectVariant.getAreaMaxY() ); const [areaMaxZ, setAreaMaxZ] = React.useState<number>( - eventsBasedObject.getAreaMaxZ() + eventsBasedObjectVariant.getAreaMaxZ() ); const [isRenderedIn3D, setRenderedIn3D] = React.useState<boolean>( eventsBasedObject.isRenderedIn3D() @@ -56,28 +60,28 @@ const EventsBasedObjectScenePropertiesDialog = ({ const onSubmit = () => { if (areaMinX < areaMaxX) { - eventsBasedObject.setAreaMinX(areaMinX); - eventsBasedObject.setAreaMaxX(areaMaxX); + eventsBasedObjectVariant.setAreaMinX(areaMinX); + eventsBasedObjectVariant.setAreaMaxX(areaMaxX); } if (areaMinY < areaMaxY) { - eventsBasedObject.setAreaMinY(areaMinY); - eventsBasedObject.setAreaMaxY(areaMaxY); + eventsBasedObjectVariant.setAreaMinY(areaMinY); + eventsBasedObjectVariant.setAreaMaxY(areaMaxY); } if (areaMinZ < areaMaxZ) { - eventsBasedObject.setAreaMinZ(areaMinZ); - eventsBasedObject.setAreaMaxZ(areaMaxZ); + eventsBasedObjectVariant.setAreaMinZ(areaMinZ); + eventsBasedObjectVariant.setAreaMaxZ(areaMaxZ); } const wasRenderedIn3D = eventsBasedObject.isRenderedIn3D(); if (wasRenderedIn3D !== isRenderedIn3D) { eventsBasedObject.markAsRenderedIn3D(isRenderedIn3D); - onEventsBasedObjectChildrenEdited(); + onEventsBasedObjectChildrenEdited(eventsBasedObject); } const wasInnerAreaFollowingParentSize = eventsBasedObject.isInnerAreaFollowingParentSize(); if (wasInnerAreaFollowingParentSize !== isInnerAreaFollowingParentSize) { eventsBasedObject.markAsInnerAreaFollowingParentSize( isInnerAreaFollowingParentSize ); - onEventsBasedObjectChildrenEdited(); + onEventsBasedObjectChildrenEdited(eventsBasedObject); } onApply(); }; @@ -117,7 +121,10 @@ const EventsBasedObjectScenePropertiesDialog = ({ <Dialog title={ <Trans> - {eventsBasedObject.getFullName() || eventsBasedObject.getName()}{' '} + {(eventsBasedObject.getFullName() || eventsBasedObject.getName()) + + ' (' + + eventsBasedObjectVariant.getName() + + ')'}{' '} properties </Trans> } diff --git a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js index e3bf2187ccf4..13bb333dd268 100644 --- a/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js +++ b/newIDE/app/src/SceneEditor/InstanceOrObjectPropertiesEditorContainer.js @@ -36,12 +36,14 @@ type Props = {| i18n: I18nType, historyHandler?: HistoryHandler, lastSelectionType: 'instance' | 'object', + isVariableListLocked: boolean, // For objects: objects: Array<gdObject>, onEditObject: (object: gdObject, initialTab: ?ObjectEditorTab) => void, onUpdateBehaviorsSharedData: () => void, onExtensionInstalled: (extensionName: string) => void, + isBehaviorListLocked: boolean, // For instances: instances: Array<gdInitialInstance>, @@ -83,6 +85,7 @@ export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef< eventsFunctionsExtension, onUpdateBehaviorsSharedData, onExtensionInstalled, + isBehaviorListLocked, // For instances: instances, @@ -116,6 +119,7 @@ export const InstanceOrObjectPropertiesEditorContainer = React.forwardRef< eventsFunctionsExtension={eventsFunctionsExtension} onUpdateBehaviorsSharedData={onUpdateBehaviorsSharedData} onExtensionInstalled={onExtensionInstalled} + isBehaviorListLocked={isBehaviorListLocked} {...commonProps} /> ) : ( diff --git a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js index 1a13ac1b39e1..40e1d93e2958 100644 --- a/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/MosaicEditorsDisplay/index.js @@ -87,6 +87,7 @@ const MosaicEditorsDisplay = React.forwardRef< layout, eventsFunctionsExtension, eventsBasedObject, + eventsBasedObjectVariant, updateBehaviorsSharedData, layersContainer, globalObjectsContainer, @@ -252,6 +253,10 @@ const MosaicEditorsDisplay = React.forwardRef< const selectedObjectNames = selectedObjects.map(object => object.getName()); + const isCustomVariant = eventsBasedObject + ? eventsBasedObject.getDefaultVariant() !== eventsBasedObjectVariant + : false; + const editors = { properties: { type: 'secondary', @@ -284,6 +289,8 @@ const MosaicEditorsDisplay = React.forwardRef< onSelectTileMapTile={props.onSelectTileMapTile} lastSelectionType={props.lastSelectionType} onExtensionInstalled={props.onExtensionInstalled} + isVariableListLocked={isCustomVariant} + isBehaviorListLocked={isCustomVariant} /> )} </I18n> @@ -333,6 +340,7 @@ const MosaicEditorsDisplay = React.forwardRef< project={project} layout={layout} eventsBasedObject={eventsBasedObject} + eventsBasedObjectVariant={eventsBasedObjectVariant} globalObjectsContainer={globalObjectsContainer} objectsContainer={objectsContainer} layersContainer={layersContainer} @@ -391,6 +399,9 @@ const MosaicEditorsDisplay = React.forwardRef< } onEditObject={props.onEditObject} onOpenEventBasedObjectEditor={props.onOpenEventBasedObjectEditor} + onOpenEventBasedObjectVariantEditor={ + props.onOpenEventBasedObjectVariantEditor + } onExportAssets={props.onExportAssets} onDeleteObjects={(objectWithContext, cb) => props.onDeleteObjects(i18n, objectWithContext, cb) @@ -414,6 +425,7 @@ const MosaicEditorsDisplay = React.forwardRef< ref={objectsListRef} unsavedChanges={props.unsavedChanges} hotReloadPreviewButtonProps={props.hotReloadPreviewButtonProps} + isListLocked={isCustomVariant} /> )} </I18n> @@ -444,6 +456,7 @@ const MosaicEditorsDisplay = React.forwardRef< props.canObjectOrGroupBeGlobal(i18n, groupName) } unsavedChanges={props.unsavedChanges} + isListLocked={isCustomVariant} /> )} </I18n> diff --git a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js index b5da19831fcf..326f3113a45b 100644 --- a/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js +++ b/newIDE/app/src/SceneEditor/SwipeableDrawerEditorsDisplay/index.js @@ -63,6 +63,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< layout, eventsFunctionsExtension, eventsBasedObject, + eventsBasedObjectVariant, updateBehaviorsSharedData, layersContainer, globalObjectsContainer, @@ -252,6 +253,10 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< ? editorTitleById[selectedEditorId] : null; + const isCustomVariant = eventsBasedObject + ? eventsBasedObject.getDefaultVariant() !== eventsBasedObjectVariant + : false; + return ( <FullSizeMeasurer> {({ width, height }) => ( @@ -267,6 +272,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< project={project} layout={layout} eventsBasedObject={eventsBasedObject} + eventsBasedObjectVariant={eventsBasedObjectVariant} globalObjectsContainer={globalObjectsContainer} objectsContainer={objectsContainer} layersContainer={layersContainer} @@ -331,6 +337,9 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< onOpenEventBasedObjectEditor={ props.onOpenEventBasedObjectEditor } + onOpenEventBasedObjectVariantEditor={ + props.onOpenEventBasedObjectVariantEditor + } onExportAssets={props.onExportAssets} onDeleteObjects={(objectWithContext, cb) => props.onDeleteObjects(i18n, objectWithContext, cb) @@ -362,6 +371,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< hotReloadPreviewButtonProps={ props.hotReloadPreviewButtonProps } + isListLocked={isCustomVariant} /> )} </I18n> @@ -398,6 +408,8 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< onSelectTileMapTile={props.onSelectTileMapTile} lastSelectionType={props.lastSelectionType} onExtensionInstalled={props.onExtensionInstalled} + isVariableListLocked={isCustomVariant} + isBehaviorListLocked={isCustomVariant} /> )} </I18n> @@ -430,6 +442,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef< props.canObjectOrGroupBeGlobal(i18n, groupName) } unsavedChanges={props.unsavedChanges} + isListLocked={isCustomVariant} /> )} </I18n> diff --git a/newIDE/app/src/SceneEditor/index.js b/newIDE/app/src/SceneEditor/index.js index 6331a0b2927e..6d11d95eb09a 100644 --- a/newIDE/app/src/SceneEditor/index.js +++ b/newIDE/app/src/SceneEditor/index.js @@ -101,6 +101,7 @@ type Props = {| layout: gdLayout | null, eventsFunctionsExtension: gdEventsFunctionsExtension | null, eventsBasedObject: gdEventsBasedObject | null, + eventsBasedObjectVariant: gdEventsBasedObjectVariant | null, globalObjectsContainer: gdObjectsContainer | null, objectsContainer: gdObjectsContainer, @@ -112,7 +113,10 @@ type Props = {| onOpenMoreSettings?: ?() => void, onOpenEvents: (sceneName: string) => void, onObjectEdited: (objectWithContext: ObjectWithContext) => void, - onEventsBasedObjectChildrenEdited: () => void, + onObjectGroupEdited: (objectGroupWithContext: GroupWithContext) => void, + onEventsBasedObjectChildrenEdited: ( + eventsBasedObject: gdEventsBasedObject + ) => void, setToolbar: (?React.Node) => void, resourceManagementProps: ResourceManagementProps, @@ -128,7 +132,17 @@ type Props = {| extensionName: string, eventsBasedObjectName: string ) => void, + onOpenEventBasedObjectVariantEditor: ( + extensionName: string, + eventsBasedObjectName: string, + variantName: string + ) => void, onExtensionInstalled: (extensionName: string) => void, + onDeleteEventsBasedObjectVariant: ( + eventsFunctionsExtension: gdEventsFunctionsExtension, + eventBasedObject: gdEventsBasedObject, + variant: gdEventsBasedObjectVariant + ) => void, // Preview: hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, @@ -536,6 +550,13 @@ export default class SceneEditor extends React.Component<Props, State> { }; _closeObjectGroupEditorDialog = () => { + if (this.state.editedGroup) { + // TODO Set the `global` attribute correctly. + this.props.onObjectGroupEdited({ + group: this.state.editedGroup, + global: false, + }); + } this.setState({ editedGroup: null, isCreatingNewGroup: false }); }; @@ -951,7 +972,7 @@ export default class SceneEditor extends React.Component<Props, State> { objectsWithContext: ObjectWithContext[], done: boolean => void ) => { - const { project, layout, eventsBasedObject } = this.props; + const { project, layout, eventsBasedObject, onObjectEdited } = this.props; objectsWithContext.forEach(objectWithContext => { const { object, global } = objectWithContext; @@ -985,6 +1006,10 @@ export default class SceneEditor extends React.Component<Props, State> { done(true); + objectsWithContext.forEach(objectWithContext => { + // TODO Avoid to do this N times. + onObjectEdited(objectWithContext); + }); // We modified the selection, so force an update of editors dealing with it. this.forceUpdatePropertiesEditor(); this.updateToolbar(); @@ -1182,6 +1207,7 @@ export default class SceneEditor extends React.Component<Props, State> { done: boolean => void ) => { done(true); + this.props.onObjectGroupEdited(groupWithContext); }; _onRenameObjectGroup = ( @@ -1229,8 +1255,8 @@ export default class SceneEditor extends React.Component<Props, State> { /* isObjectGroup=*/ true ); } - done(true); + this.props.onObjectGroupEdited(groupWithContext); }; canObjectOrGroupBeGlobal = ( @@ -1550,15 +1576,20 @@ export default class SceneEditor extends React.Component<Props, State> { object && project.hasEventsBasedObject(object.getType()) ? { label: i18n._(t`Edit children`), - click: () => - this.props.onOpenEventBasedObjectEditor( + click: () => { + const customObjectConfiguration = gd.asCustomObjectConfiguration( + object.getConfiguration() + ); + this.props.onOpenEventBasedObjectVariantEditor( gd.PlatformExtension.getExtensionFromFullObjectType( object.getType() ), gd.PlatformExtension.getObjectNameFromFullObjectType( object.getType() - ) - ), + ), + customObjectConfiguration.getVariantName() + ); + }, } : null, { type: 'separator' }, @@ -1888,6 +1919,7 @@ export default class SceneEditor extends React.Component<Props, State> { layout, eventsFunctionsExtension, eventsBasedObject, + eventsBasedObjectVariant, initialInstances, resourceManagementProps, isActive, @@ -1921,6 +1953,10 @@ export default class SceneEditor extends React.Component<Props, State> { </Trans> ) : null; + const isCustomVariant = eventsBasedObject + ? eventsBasedObject.getDefaultVariant() !== eventsBasedObjectVariant + : false; + return ( <ResponsiveWindowMeasurer> {({ isMobile }) => { @@ -1953,6 +1989,7 @@ export default class SceneEditor extends React.Component<Props, State> { layout={layout} eventsFunctionsExtension={eventsFunctionsExtension} eventsBasedObject={eventsBasedObject} + eventsBasedObjectVariant={eventsBasedObjectVariant} layersContainer={this.props.layersContainer} globalObjectsContainer={this.props.globalObjectsContainer} objectsContainer={this.props.objectsContainer} @@ -1994,6 +2031,9 @@ export default class SceneEditor extends React.Component<Props, State> { onOpenEventBasedObjectEditor={ this.props.onOpenEventBasedObjectEditor } + onOpenEventBasedObjectVariantEditor={ + this.props.onOpenEventBasedObjectVariantEditor + } onRenameObjectFolderOrObjectWithContextFinish={ this._onRenameObjectFolderOrObjectWithContextFinish } @@ -2116,6 +2156,17 @@ export default class SceneEditor extends React.Component<Props, State> { } openBehaviorEvents={this.props.openBehaviorEvents} onExtensionInstalled={this.props.onExtensionInstalled} + onOpenEventBasedObjectEditor={ + this.props.onOpenEventBasedObjectEditor + } + onOpenEventBasedObjectVariantEditor={ + this.props.onOpenEventBasedObjectVariantEditor + } + onDeleteEventsBasedObjectVariant={ + this.props.onDeleteEventsBasedObjectVariant + } + isBehaviorListLocked={isCustomVariant} + isVariableListLocked={isCustomVariant} /> )} </React.Fragment> @@ -2146,6 +2197,11 @@ export default class SceneEditor extends React.Component<Props, State> { objectGroup ); } + // TODO Set the `global` attribute correctly. + this.props.onObjectGroupEdited({ + group: objectGroup, + global: false, + }); }} initialTab={'objects'} onComputeAllVariableNames={() => { @@ -2160,6 +2216,8 @@ export default class SceneEditor extends React.Component<Props, State> { editedGroup.getName() ); }} + isVariableListLocked={isCustomVariant} + isObjectListLocked={isCustomVariant} /> )} {this.state.setupGridOpen && ( @@ -2196,6 +2254,7 @@ export default class SceneEditor extends React.Component<Props, State> { hotReloadPreviewButtonProps={ this.props.hotReloadPreviewButtonProps } + isListLocked={true} /> )} {!!this.state.layerRemoved && @@ -2242,22 +2301,25 @@ export default class SceneEditor extends React.Component<Props, State> { resourceManagementProps={this.props.resourceManagementProps} /> )} - {this.state.scenePropertiesDialogOpen && eventsBasedObject && ( - <EventsBasedObjectScenePropertiesDialog - project={project} - eventsBasedObject={eventsBasedObject} - onClose={() => this.openSceneProperties(false)} - onApply={() => this.openSceneProperties(false)} - getContentAABB={ - this.editorDisplay - ? this.editorDisplay.instancesHandlers.getContentAABB - : () => null - } - onEventsBasedObjectChildrenEdited={ - this.props.onEventsBasedObjectChildrenEdited - } - /> - )} + {this.state.scenePropertiesDialogOpen && + eventsBasedObject && + eventsBasedObjectVariant && ( + <EventsBasedObjectScenePropertiesDialog + project={project} + eventsBasedObject={eventsBasedObject} + eventsBasedObjectVariant={eventsBasedObjectVariant} + onClose={() => this.openSceneProperties(false)} + onApply={() => this.openSceneProperties(false)} + getContentAABB={ + this.editorDisplay + ? this.editorDisplay.instancesHandlers.getContentAABB + : () => null + } + onEventsBasedObjectChildrenEdited={ + this.props.onEventsBasedObjectChildrenEdited + } + /> + )} {!!this.state.layoutVariablesDialogOpen && layout && ( <SceneVariablesDialog open @@ -2268,6 +2330,7 @@ export default class SceneEditor extends React.Component<Props, State> { hotReloadPreviewButtonProps={ this.props.hotReloadPreviewButtonProps } + isListLocked={false} /> )} <I18n> diff --git a/newIDE/app/src/UI/TreeView/TreeViewRow.js b/newIDE/app/src/UI/TreeView/TreeViewRow.js index 55c12732c64c..bc34ea0b65a9 100644 --- a/newIDE/app/src/UI/TreeView/TreeViewRow.js +++ b/newIDE/app/src/UI/TreeView/TreeViewRow.js @@ -458,6 +458,11 @@ const TreeViewRow = <Item: ItemBaseAttributes>(props: Props<Item>) => { } }} tooltip={rightButton.label} + disabled={ + rightButton.enabled === undefined + ? false + : !rightButton.enabled + } > {rightButton.icon} </IconButton> diff --git a/newIDE/app/src/UI/TreeView/index.js b/newIDE/app/src/UI/TreeView/index.js index 03d42a69d1e0..2b41cc5c000c 100644 --- a/newIDE/app/src/UI/TreeView/index.js +++ b/newIDE/app/src/UI/TreeView/index.js @@ -30,6 +30,7 @@ export type MenuButton = {| icon: React.Node, label: MessageDescriptor, click: ?() => void | Promise<void>, + enabled?: boolean, |}; type FlattenedNode<Item> = {| diff --git a/newIDE/app/src/Utils/GDevelopServices/Asset.js b/newIDE/app/src/Utils/GDevelopServices/Asset.js index 59153bbb2af4..e07a963bda3c 100644 --- a/newIDE/app/src/Utils/GDevelopServices/Asset.js +++ b/newIDE/app/src/Utils/GDevelopServices/Asset.js @@ -45,6 +45,10 @@ export type ExtensionDependency = {| export type ObjectAsset = {| object: any /*(serialized gdObjectConfiguration)*/, resources: Array<any /*(serialized gdResource)*/>, + variants?: Array<{ + objectType: string, + variant: any /*(serialized gdEventsBasedObjectVariant)*/, + }>, // TODO This can become mandatory after the migration of the asset repository. requiredExtensions?: Array<ExtensionDependency>, |}; diff --git a/newIDE/app/src/Utils/Serializer.js b/newIDE/app/src/Utils/Serializer.js index a08fec8c5917..922974f38cd5 100644 --- a/newIDE/app/src/Utils/Serializer.js +++ b/newIDE/app/src/Utils/Serializer.js @@ -8,7 +8,7 @@ const gd: libGDevelop = global.gd; * and unserializeFrom method. * * @param {*} serializable - * @param {*} methodName The name of the serialization method. "unserializeFrom" by default + * @param {*} methodName The name of the serialization method. "serializeTo" by default */ export function serializeToJSObject( serializable: gdSerializable, diff --git a/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js b/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js index 25b3333a7253..de7040061476 100644 --- a/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js +++ b/newIDE/app/src/VariablesList/GlobalAndSceneVariablesDialog.js @@ -15,6 +15,7 @@ type Props = {| isGlobalTabInitiallyOpen?: boolean, initiallySelectedVariableName?: string, shouldCreateInitiallySelectedVariable?: boolean, + isListLocked: boolean, |}; const GlobalAndSceneVariablesDialog = ({ @@ -26,6 +27,7 @@ const GlobalAndSceneVariablesDialog = ({ isGlobalTabInitiallyOpen, initiallySelectedVariableName, shouldCreateInitiallySelectedVariable, + isListLocked, }: Props) => { const { project, @@ -127,6 +129,7 @@ const GlobalAndSceneVariablesDialog = ({ helpPagePath={'/all-features/variables/scene-variables'} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} id="global-and-scene-variables-dialog" + isListLocked={isListLocked} /> ); }; diff --git a/newIDE/app/src/VariablesList/GlobalVariablesDialog.js b/newIDE/app/src/VariablesList/GlobalVariablesDialog.js index 40337716e0f4..44092e4d3036 100644 --- a/newIDE/app/src/VariablesList/GlobalVariablesDialog.js +++ b/newIDE/app/src/VariablesList/GlobalVariablesDialog.js @@ -14,6 +14,7 @@ type Props = {| hotReloadPreviewButtonProps: HotReloadPreviewButtonProps | null, initiallySelectedVariableName?: string, shouldCreateInitiallySelectedVariable?: boolean, + isListLocked: boolean, |}; const GlobalVariablesDialog = ({ @@ -24,6 +25,7 @@ const GlobalVariablesDialog = ({ hotReloadPreviewButtonProps, initiallySelectedVariableName, shouldCreateInitiallySelectedVariable, + isListLocked, }: Props) => { const onComputeAllVariableNames = React.useCallback( () => @@ -76,6 +78,7 @@ const GlobalVariablesDialog = ({ helpPagePath={'/all-features/variables/global-variables'} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} id="global-variables-dialog" + isListLocked={isListLocked} /> ); }; diff --git a/newIDE/app/src/VariablesList/LocalVariablesDialog.js b/newIDE/app/src/VariablesList/LocalVariablesDialog.js index a8f07b0d2747..ed20417f9596 100644 --- a/newIDE/app/src/VariablesList/LocalVariablesDialog.js +++ b/newIDE/app/src/VariablesList/LocalVariablesDialog.js @@ -13,6 +13,7 @@ type Props = {| onCancel: () => void, initiallySelectedVariableName: string, shouldCreateInitiallySelectedVariable?: boolean, + isListLocked: boolean, |}; const LocalVariablesDialog = ({ @@ -24,6 +25,7 @@ const LocalVariablesDialog = ({ onApply, initiallySelectedVariableName, shouldCreateInitiallySelectedVariable, + isListLocked, }: Props) => { const tabs = React.useMemo( () => [ @@ -53,6 +55,7 @@ const LocalVariablesDialog = ({ shouldCreateInitiallySelectedVariable } hotReloadPreviewButtonProps={null} + isListLocked={isListLocked} /> ); }; diff --git a/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js b/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js index f376c4586f60..5eebeed3611d 100644 --- a/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js +++ b/newIDE/app/src/VariablesList/ObjectGroupVariablesDialog.js @@ -34,6 +34,7 @@ type Props = {| shouldCreateInitiallySelectedVariable?: boolean, hotReloadPreviewButtonProps?: ?HotReloadPreviewButtonProps, onComputeAllVariableNames: () => Array<string>, + isListLocked: boolean, |}; const ObjectGroupVariablesDialog = ({ @@ -50,6 +51,7 @@ const ObjectGroupVariablesDialog = ({ initiallySelectedVariableName, shouldCreateInitiallySelectedVariable, onComputeAllVariableNames, + isListLocked, }: Props) => { const groupVariablesContainer = useValueWithInit( // The VariablesContainer is returned by value. @@ -100,6 +102,16 @@ const ObjectGroupVariablesDialog = ({ changeset, originalSerializedVariables ); + const { eventsBasedObject } = projectScopedContainersAccessor.getScope(); + if (eventsBasedObject) { + for (const objectName of objectGroup.getAllObjectsNames().toJSArray()) { + gd.ObjectVariableHelper.applyChangesToVariants( + eventsBasedObject, + objectName, + changeset + ); + } + } groupVariablesContainer.clearPersistentUuid(); }; @@ -198,6 +210,7 @@ const ObjectGroupVariablesDialog = ({ onComputeAllVariableNames={onComputeAllVariableNames} onVariablesUpdated={notifyOfVariableChange} onSelectedVariableChange={onSelectedVariableChange} + isListLocked={isListLocked} /> </Column> </Dialog> diff --git a/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js b/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js index a8fc04e1549a..bfcb98ede7be 100644 --- a/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js +++ b/newIDE/app/src/VariablesList/ObjectInstanceVariablesDialog.js @@ -20,6 +20,7 @@ type Props = {| hotReloadPreviewButtonProps: HotReloadPreviewButtonProps, initiallySelectedVariableName?: string, onEditObjectVariables: () => void, + isListLocked: boolean, |}; const ObjectInstanceVariablesDialog = ({ @@ -35,6 +36,7 @@ const ObjectInstanceVariablesDialog = ({ initiallySelectedVariableName, projectScopedContainersAccessor, onEditObjectVariables, + isListLocked, }: Props) => { const tabs = React.useMemo( () => { @@ -91,6 +93,7 @@ const ObjectInstanceVariablesDialog = ({ hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} id="instance-variables-dialog" onEditObjectVariables={onEditObjectVariables} + isListLocked={isListLocked} /> ); }; diff --git a/newIDE/app/src/VariablesList/ObjectVariablesDialog.js b/newIDE/app/src/VariablesList/ObjectVariablesDialog.js index fe65423b516b..4e2b08ddb8e8 100644 --- a/newIDE/app/src/VariablesList/ObjectVariablesDialog.js +++ b/newIDE/app/src/VariablesList/ObjectVariablesDialog.js @@ -18,6 +18,7 @@ type Props = {| initiallySelectedVariableName?: string, shouldCreateInitiallySelectedVariable?: boolean, onComputeAllVariableNames: () => Array<string>, + isListLocked: boolean, |}; const ObjectVariablesDialog = ({ @@ -33,6 +34,7 @@ const ObjectVariablesDialog = ({ shouldCreateInitiallySelectedVariable, projectScopedContainersAccessor, onComputeAllVariableNames, + isListLocked, }: Props) => { const tabs = React.useMemo( () => [ @@ -70,6 +72,7 @@ const ObjectVariablesDialog = ({ helpPagePath={'/all-features/variables/object-variables'} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} id="object-variables-dialog" + isListLocked={isListLocked} /> ); }; diff --git a/newIDE/app/src/VariablesList/SceneVariablesDialog.js b/newIDE/app/src/VariablesList/SceneVariablesDialog.js index e67102e0ce06..b75457281cf2 100644 --- a/newIDE/app/src/VariablesList/SceneVariablesDialog.js +++ b/newIDE/app/src/VariablesList/SceneVariablesDialog.js @@ -15,6 +15,7 @@ type Props = {| hotReloadPreviewButtonProps: HotReloadPreviewButtonProps | null, initiallySelectedVariableName?: string, shouldCreateInitiallySelectedVariable?: boolean, + isListLocked: boolean, |}; const SceneVariablesDialog = ({ @@ -26,6 +27,7 @@ const SceneVariablesDialog = ({ hotReloadPreviewButtonProps, initiallySelectedVariableName, shouldCreateInitiallySelectedVariable, + isListLocked, }: Props) => { const onComputeAllVariableNames = React.useCallback( () => @@ -78,6 +80,7 @@ const SceneVariablesDialog = ({ helpPagePath={'/all-features/variables/scene-variables'} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} id="scene-variables-dialog" + isListLocked={isListLocked} /> ); }; diff --git a/newIDE/app/src/VariablesList/VariableTypeSelector.js b/newIDE/app/src/VariablesList/VariableTypeSelector.js index 1480e4d4d63d..dd9f3c2ab285 100644 --- a/newIDE/app/src/VariablesList/VariableTypeSelector.js +++ b/newIDE/app/src/VariablesList/VariableTypeSelector.js @@ -25,6 +25,7 @@ type Props = {| readOnlyWithIcon?: boolean, id?: string, errorMessage: MessageDescriptor | null, + disabled?: boolean, |}; let options; @@ -127,6 +128,7 @@ const VariableTypeSelector = React.memo<Props>((props: Props) => { : undefined, }} id={props.id} + disabled={props.disabled} > {getOptions()} </SelectField> diff --git a/newIDE/app/src/VariablesList/VariablesEditorDialog.js b/newIDE/app/src/VariablesList/VariablesEditorDialog.js index 9ec09467b808..7f32a03f6d7b 100644 --- a/newIDE/app/src/VariablesList/VariablesEditorDialog.js +++ b/newIDE/app/src/VariablesList/VariablesEditorDialog.js @@ -48,6 +48,7 @@ type Props = {| initiallyOpenTabId?: string, initiallySelectedVariableName?: string, shouldCreateInitiallySelectedVariable?: boolean, + isListLocked: boolean, project: gdProject, hotReloadPreviewButtonProps: HotReloadPreviewButtonProps | null, @@ -73,6 +74,7 @@ const VariablesEditorDialog = ({ projectScopedContainersAccessor, objectName, initialInstances, + isListLocked, }: Props) => { const serializableObjects = React.useMemo( () => @@ -295,6 +297,7 @@ const VariablesEditorDialog = ({ helpPagePath={helpPagePath} onVariablesUpdated={notifyOfChange} onSelectedVariableChange={onSelectedVariableChange} + isListLocked={isListLocked} /> </Column> ) diff --git a/newIDE/app/src/VariablesList/VariablesList.js b/newIDE/app/src/VariablesList/VariablesList.js index b1d8b6b83c50..6cb71c4c50c8 100644 --- a/newIDE/app/src/VariablesList/VariablesList.js +++ b/newIDE/app/src/VariablesList/VariablesList.js @@ -132,6 +132,7 @@ type Props = {| onVariablesUpdated?: () => void, toolbarIconStyle?: any, onSelectedVariableChange?: (Array<string>) => void, + isListLocked: boolean, |}; const variableRowStyles = { @@ -149,6 +150,8 @@ type VariableRowProps = {| draggedNodeId: { current: ?string }, nodeId: string, isInherited: boolean, + isNameLocked: boolean, + isTypeLocked: boolean, canDrop: string => boolean, dropNode: (string, where: 'after' | 'before') => void, isSelected: boolean, @@ -198,6 +201,8 @@ const VariableRow = React.memo<VariableRowProps>( draggedNodeId, nodeId, isInherited, + isNameLocked, + isTypeLocked, canDrop, dropNode, isSelected, @@ -364,7 +369,9 @@ const VariableRow = React.memo<VariableRowProps>( directlyStoreValueChangesWhileEditing } disabled={ - isInherited || parentType === gd.Variable.Array + isNameLocked || + isInherited || + parentType === gd.Variable.Array } onChange={onChangeName} additionalContext={JSON.stringify({ nodeId, depth })} @@ -387,6 +394,7 @@ const VariableRow = React.memo<VariableRowProps>( } id={`variable-${index}-type`} errorMessage={typeErrorMessage} + disabled={isTypeLocked} /> </Column> <Column expand> @@ -1053,11 +1061,27 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>( current, props.variablesContainer ); - const { variable: draggedVariable } = draggedVariableContext; + const { + variable: draggedVariable, + lineage: draggedLineage, + } = draggedVariableContext; if (!draggedVariable) return false; if (isAnAncestryOf(draggedVariable, targetLineage)) return false; + const targetVariableParentVariable = getDirectParentVariable( + targetLineage + ); + const draggedVariableParentVariable = getDirectParentVariable( + draggedLineage + ); + if ( + props.isListLocked && + (!targetVariableParentVariable || !draggedVariableParentVariable) + ) { + return false; + } + const movementType = getMovementTypeWithinVariablesContainer( draggedVariableContext, targetVariableContext @@ -1080,7 +1104,7 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>( return false; } }, - [props.variablesContainer] + [props.isListLocked, props.variablesContainer] ); const dropNode = React.useCallback( @@ -1504,6 +1528,8 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>( draggedNodeId={draggedNodeId} nodeId={nodeId} isInherited={isInherited} + isNameLocked={props.isListLocked && isTopLevel} + isTypeLocked={props.isListLocked && isTopLevel} canDrop={canDrop} dropNode={dropNode} isSelected={isSelected} @@ -1832,6 +1858,7 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>( addVariable, })); + // TODO Allow to past child-variables of existing object variables even when the variable list is locked. const toolbar = ( <VariablesListToolbar isNarrow={isNarrow} @@ -1840,11 +1867,13 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>( onPaste={pasteClipboardContent} onDelete={deleteSelection} canCopy={selectedNodes.length > 0} - canPaste={Clipboard.has(CLIPBOARD_KIND)} + canPaste={Clipboard.has(CLIPBOARD_KIND) && !props.isListLocked} canDelete={ + !props.isListLocked && selectedNodes.length > 0 && selectedNodes.every(nodeId => !nodeId.startsWith(inheritedPrefix)) } + canAdd={!props.isListLocked} onUndo={_undo} onRedo={_redo} canUndo={_canUndo()} @@ -1886,15 +1915,30 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>( <Column noMargin expand justifyContent="center"> {props.emptyPlaceholderTitle && props.emptyPlaceholderDescription ? ( - <EmptyPlaceholder - title={props.emptyPlaceholderTitle} - description={props.emptyPlaceholderDescription} - actionLabel={<Trans>Add a variable</Trans>} - helpPagePath={props.helpPagePath || undefined} - tutorialId="intermediate-advanced-variables" - onAction={addVariable} - actionButtonId="add-variable" - /> + props.isListLocked ? ( + <Column> + <Text size="block-title" align="center"> + {<Trans>No variable</Trans>} + </Text> + <Text align="center" noMargin> + { + <Trans> + There is no variable to set up. + </Trans> + } + </Text> + </Column> + ) : ( + <EmptyPlaceholder + title={props.emptyPlaceholderTitle} + description={props.emptyPlaceholderDescription} + actionLabel={<Trans>Add a variable</Trans>} + helpPagePath={props.helpPagePath || undefined} + tutorialId="intermediate-advanced-variables" + onAction={addVariable} + actionButtonId="add-variable" + /> + ) ) : null} {props.compactEmptyPlaceholderText && ( <Line justifyContent="center"> @@ -1904,7 +1948,11 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>( align="center" noMargin > - {props.compactEmptyPlaceholderText} + {props.isListLocked ? ( + <Trans>There is no variable to set up.</Trans> + ) : ( + props.compactEmptyPlaceholderText + )} </Text> </Line> )} diff --git a/newIDE/app/src/VariablesList/VariablesListToolbar.js b/newIDE/app/src/VariablesList/VariablesListToolbar.js index 38032c847493..2a5c4a0dd5d9 100644 --- a/newIDE/app/src/VariablesList/VariablesListToolbar.js +++ b/newIDE/app/src/VariablesList/VariablesListToolbar.js @@ -24,6 +24,7 @@ type Props = {| canCopy: boolean, canPaste: boolean, canDelete: boolean, + canAdd: boolean, hideHistoryChangeButtons: boolean, onUndo?: () => void, onRedo?: () => void, @@ -140,6 +141,7 @@ const VariablesListToolbar = React.memo<Props>((props: Props) => { tooltip={t`Add variable`} onClick={props.onAdd} size="small" + disabled={!props.canAdd} > <Add style={props.iconStyle} /> </IconButton> @@ -150,6 +152,7 @@ const VariablesListToolbar = React.memo<Props>((props: Props) => { onClick={props.onAdd} label={<Trans>Add variable</Trans>} leftIcon={<Add />} + disabled={!props.canAdd} /> )} </Column> diff --git a/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js b/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js index 85df29977d86..af73f15230c1 100644 --- a/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js +++ b/newIDE/app/src/stories/componentStories/ClosableTabs.stories.js @@ -298,6 +298,9 @@ export const WithObjectsList = () => ( resourceManagementProps={fakeResourceManagementProps} onEditObject={action('On edit object')} onOpenEventBasedObjectEditor={action('On edit children')} + onOpenEventBasedObjectVariantEditor={action( + 'On edit variant' + )} onExportAssets={action('On export assets')} onAddObjectInstance={action('On add instance to the scene')} selectedObjectFolderOrObjectsWithContext={[]} @@ -312,6 +315,7 @@ export const WithObjectsList = () => ( onObjectEdited={() => {}} onObjectFolderOrObjectWithContextSelected={() => {}} hotReloadPreviewButtonProps={hotReloadPreviewButtonProps} + isListLocked={false} /> </TabContentContainer> } diff --git a/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js b/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js index 1e8fbdc5a812..114ef0e71d94 100644 --- a/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js +++ b/newIDE/app/src/stories/componentStories/FullSizeInstancesEditorWithScrollbars.stories.js @@ -39,6 +39,7 @@ export const Default = () => ( project={testProject.project} layout={testProject.testLayout} eventsBasedObject={null} + eventsBasedObjectVariant={null} layersContainer={testProject.testLayout.getLayers()} globalObjectsContainer={testProject.project.getObjects()} objectsContainer={testProject.testLayout.getObjects()} diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js index d9fcb890a371..46ca65af78e2 100644 --- a/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/CompactInstancePropertiesEditor.stories.js @@ -39,6 +39,7 @@ export const InstanceSprite2d = () => ( editObjectInPropertiesPanel={action('edit object')} tileMapTileSelection={null} onSelectTileMapTile={() => {}} + isVariableListLocked={false} /> </SerializedObjectDisplay> )} @@ -67,6 +68,7 @@ export const InstanceCube3d = () => ( editObjectInPropertiesPanel={action('edit object')} tileMapTileSelection={null} onSelectTileMapTile={() => {}} + isVariableListLocked={false} /> </SerializedObjectDisplay> )} @@ -95,6 +97,7 @@ export const InstanceTextInput = () => ( editObjectInPropertiesPanel={action('edit object')} tileMapTileSelection={null} onSelectTileMapTile={() => {}} + isVariableListLocked={false} /> </SerializedObjectDisplay> )} diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js index 7fe8550fca90..b639c4fbeeef 100644 --- a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupEditorDialog.stories.js @@ -26,6 +26,8 @@ export const Default = () => ( onApply={action('onApply')} onCancel={action('onCancel')} onObjectGroupAdded={action('onObjectGroupAdded')} + isVariableListLocked={false} + isObjectListLocked={false} /> ); @@ -42,6 +44,8 @@ export const WithLongObjectNames = () => ( onApply={action('onApply')} onCancel={action('onCancel')} onObjectGroupAdded={action('onObjectGroupAdded')} + isVariableListLocked={false} + isObjectListLocked={false} /> ); @@ -58,5 +62,7 @@ export const Empty = () => ( onApply={action('onApply')} onCancel={action('onCancel')} onObjectGroupAdded={action('onObjectGroupAdded')} + isVariableListLocked={false} + isObjectListLocked={false} /> ); diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js index f7d667a77d52..cd03677d5915 100644 --- a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectGroupsList.stories.js @@ -35,6 +35,31 @@ export const Default = () => ( onRenameGroup={action('onRenameGroup')} onDeleteGroup={action('onDeleteGroup')} getValidatedObjectOrGroupName={newName => newName} + isListLocked={false} + /> + </div> + </SerializedObjectDisplay> + </DragAndDropContextProvider> +); + +export const Locked = () => ( + <DragAndDropContextProvider> + <SerializedObjectDisplay object={testProject.testLayout}> + <div style={{ height: 250 }}> + <ObjectGroupsList + globalObjectGroups={testProject.project + .getObjects() + .getObjectGroups()} + projectScopedContainersAccessor={ + testProject.testSceneProjectScopedContainersAccessor + } + objectGroups={testProject.testLayout.getObjects().getObjectGroups()} + onCreateGroup={action('onCreateGroup')} + onEditGroup={action('onEditGroup')} + onRenameGroup={action('onRenameGroup')} + onDeleteGroup={action('onDeleteGroup')} + getValidatedObjectOrGroupName={newName => newName} + isListLocked={true} /> </div> </SerializedObjectDisplay> diff --git a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js index 50aa7065ee6a..2b66138e6ae1 100644 --- a/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js +++ b/newIDE/app/src/stories/componentStories/LayoutEditor/ObjectsList.stories.js @@ -36,6 +36,7 @@ export const Default = () => ( resourceManagementProps={fakeResourceManagementProps} onEditObject={action('On edit object')} onOpenEventBasedObjectEditor={action('On edit children')} + onOpenEventBasedObjectVariantEditor={action('On edit variant')} onExportAssets={action('On export assets')} onAddObjectInstance={action('On add instance to the scene')} onObjectCreated={action('On object created')} @@ -50,6 +51,7 @@ export const Default = () => ( ) => cb(true)} onObjectFolderOrObjectWithContextSelected={() => {}} hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps} + isListLocked={false} /> </div> </DragAndDropContextProvider> @@ -72,6 +74,7 @@ export const WithSerializedObjectView = () => ( resourceManagementProps={fakeResourceManagementProps} onEditObject={action('On edit object')} onOpenEventBasedObjectEditor={action('On edit children')} + onOpenEventBasedObjectVariantEditor={action('On edit variant')} onExportAssets={action('On export assets')} onAddObjectInstance={action('On add instance to the scene')} onObjectCreated={action('On object created')} @@ -86,8 +89,46 @@ export const WithSerializedObjectView = () => ( ) => cb(true)} onObjectFolderOrObjectWithContextSelected={() => {}} hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps} + isListLocked={false} /> </div> </SerializedObjectDisplay> </DragAndDropContextProvider> ); + +export const Locked = () => ( + <DragAndDropContextProvider> + <div style={{ height: 400 }}> + <ObjectsList + getThumbnail={() => 'res/unknown32.png'} + project={testProject.project} + layout={testProject.testLayout} + eventsBasedObject={null} + projectScopedContainersAccessor={ + testProject.testSceneProjectScopedContainersAccessor + } + globalObjectsContainer={testProject.project.getObjects()} + objectsContainer={testProject.testLayout.getObjects()} + resourceManagementProps={fakeResourceManagementProps} + onEditObject={action('On edit object')} + onOpenEventBasedObjectEditor={action('On edit children')} + onOpenEventBasedObjectVariantEditor={action('On edit variant')} + onExportAssets={action('On export assets')} + onAddObjectInstance={action('On add instance to the scene')} + onObjectCreated={action('On object created')} + onObjectEdited={action('On object edited')} + selectedObjectFolderOrObjectsWithContext={[]} + getValidatedObjectOrGroupName={newName => newName} + onDeleteObjects={(objectsWithContext, cb) => cb(true)} + onRenameObjectFolderOrObjectWithContextFinish={( + objectWithContext, + newName, + cb + ) => cb(true)} + onObjectFolderOrObjectWithContextSelected={() => {}} + hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps} + isListLocked={true} + /> + </div> + </DragAndDropContextProvider> +); diff --git a/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js b/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js index 238fda9e3b40..942b0dd1685e 100644 --- a/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js +++ b/newIDE/app/src/stories/componentStories/ObjectEditor/BehaviorsEditor.stories.js @@ -28,6 +28,7 @@ export const Default = () => ( openBehaviorEvents={() => action('Open behavior events')} onBehaviorsUpdated={() => {}} onExtensionInstalled={action('extension installed')} + isListLocked={false} /> </SerializedObjectDisplay> ); @@ -43,6 +44,39 @@ export const WithoutAnyBehaviors = () => ( openBehaviorEvents={() => action('Open behavior events')} onBehaviorsUpdated={() => {}} onExtensionInstalled={action('extension installed')} + isListLocked={false} + /> + </SerializedObjectDisplay> +); + +export const Locked = () => ( + <SerializedObjectDisplay object={testProject.spriteObjectWithBehaviors}> + <BehaviorsEditor + project={testProject.project} + eventsFunctionsExtension={null} + object={testProject.spriteObjectWithBehaviors} + resourceManagementProps={fakeResourceManagementProps} + onUpdateBehaviorsSharedData={() => {}} + openBehaviorEvents={() => action('Open behavior events')} + onBehaviorsUpdated={() => {}} + onExtensionInstalled={action('extension installed')} + isListLocked={true} + /> + </SerializedObjectDisplay> +); + +export const LockedWithoutAnyBehaviors = () => ( + <SerializedObjectDisplay object={testProject.spriteObjectWithoutBehaviors}> + <BehaviorsEditor + project={testProject.project} + eventsFunctionsExtension={null} + object={testProject.spriteObjectWithoutBehaviors} + resourceManagementProps={fakeResourceManagementProps} + onUpdateBehaviorsSharedData={() => {}} + openBehaviorEvents={() => action('Open behavior events')} + onBehaviorsUpdated={() => {}} + onExtensionInstalled={action('extension installed')} + isListLocked={true} /> </SerializedObjectDisplay> ); diff --git a/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js b/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js index 6a3e4b2fdf96..7208ec78c442 100644 --- a/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js +++ b/newIDE/app/src/stories/componentStories/ObjectEditor/ObjectEditorDialog.stories.js @@ -43,6 +43,17 @@ export const CustomObject = () => ( }} openBehaviorEvents={() => action('Open behavior events')} onExtensionInstalled={action('extension installed')} + onOpenEventBasedObjectEditor={() => + action('Open event-based object editor') + } + onOpenEventBasedObjectVariantEditor={action( + 'Open event-based object variant editor' + )} + onDeleteEventsBasedObjectVariant={() => + action('Delete event-based object variant') + } + isBehaviorListLocked={false} + isVariableListLocked={false} /> </DragAndDropContextProvider> ); @@ -75,6 +86,60 @@ export const StandardObject = () => ( }} openBehaviorEvents={() => action('Open behavior events')} onExtensionInstalled={action('extension installed')} + onOpenEventBasedObjectEditor={() => + action('Open event-based object editor') + } + onOpenEventBasedObjectVariantEditor={action( + 'Open event-based object variant editor' + )} + onDeleteEventsBasedObjectVariant={() => + action('Delete event-based object variant') + } + isBehaviorListLocked={false} + isVariableListLocked={false} + /> + </DragAndDropContextProvider> +); + +export const LockedStandardObject = () => ( + <DragAndDropContextProvider> + <ObjectEditorDialog + open={true} + projectScopedContainersAccessor={ + testProject.testSceneProjectScopedContainersAccessor + } + object={testProject.panelSpriteObject} + onApply={() => action('Apply changes')} + onCancel={() => action('Cancel changes')} + onRename={() => action('Rename object')} + getValidatedObjectOrGroupName={newName => newName} + project={testProject.project} + layout={testProject.testLayout} + eventsFunctionsExtension={null} + eventsBasedObject={null} + resourceManagementProps={fakeResourceManagementProps} + onComputeAllVariableNames={() => []} + onUpdateBehaviorsSharedData={() => {}} + initialTab={null} + hotReloadPreviewButtonProps={{ + hasPreviewsRunning: false, + launchProjectDataOnlyPreview: () => action('Hot-reload'), + launchProjectCodeAndDataPreview: action('Hot-reload with code'), + launchProjectWithLoadingScreenPreview: () => action('Reload'), + }} + openBehaviorEvents={() => action('Open behavior events')} + onOpenEventBasedObjectEditor={() => + action('Open event-based object editor') + } + onOpenEventBasedObjectVariantEditor={action( + 'Open event-based object variant editor' + )} + onDeleteEventsBasedObjectVariant={() => + action('Delete event-based object variant') + } + onExtensionInstalled={action('extension installed')} + isBehaviorListLocked={true} + isVariableListLocked={true} /> </DragAndDropContextProvider> ); diff --git a/newIDE/app/src/stories/componentStories/VariablesList.stories.js b/newIDE/app/src/stories/componentStories/VariablesList.stories.js index b6c98d8a3975..461c2e55188d 100644 --- a/newIDE/app/src/stories/componentStories/VariablesList.stories.js +++ b/newIDE/app/src/stories/componentStories/VariablesList.stories.js @@ -24,6 +24,7 @@ export const Default = () => ( 'VariableFromSomeWhere', 'InstanceVariable', // already defined variable in testSpriteObjectInstance ]} + isListLocked={false} /> </FixedHeightFlexContainer> </DragAndDropContextProvider> @@ -46,6 +47,7 @@ export const Compact = () => ( 'VariableFromSomeWhere', 'InstanceVariable', // already defined variable in testSpriteObjectInstance ]} + isListLocked={false} /> </FixedHeightFlexContainer> </DragAndDropContextProvider> @@ -69,6 +71,76 @@ export const InstanceWithObjectVariables = () => ( 'VariableFromSomeWhere', 'InstanceVariable', // already defined variable in testSpriteObjectInstance ]} + isListLocked={false} + /> + </FixedHeightFlexContainer> + </DragAndDropContextProvider> +); + +export const Locked = () => ( + <DragAndDropContextProvider> + <FixedHeightFlexContainer height={600}> + <VariablesList + projectScopedContainersAccessor={ + testProject.testSceneProjectScopedContainersAccessor + } + variablesContainer={testProject.testLayout.getVariables()} + emptyPlaceholderDescription="Variables help you store data" + emptyPlaceholderTitle="Variables" + helpPagePath="/variables" + onComputeAllVariableNames={() => [ + 'VariableFromEventSheet', + 'VariableFromSomeWhere', + 'InstanceVariable', // already defined variable in testSpriteObjectInstance + ]} + isListLocked={true} + /> + </FixedHeightFlexContainer> + </DragAndDropContextProvider> +); + +export const LockedCompact = () => ( + <DragAndDropContextProvider> + <FixedHeightFlexContainer height={600}> + <VariablesList + projectScopedContainersAccessor={ + testProject.testSceneProjectScopedContainersAccessor + } + size="compact" + variablesContainer={testProject.testLayout.getVariables()} + emptyPlaceholderDescription="Variables help you store data" + emptyPlaceholderTitle="Variables" + helpPagePath="/variables" + onComputeAllVariableNames={() => [ + 'VariableFromEventSheet', + 'VariableFromSomeWhere', + 'InstanceVariable', // already defined variable in testSpriteObjectInstance + ]} + isListLocked={true} + /> + </FixedHeightFlexContainer> + </DragAndDropContextProvider> +); + +export const LockedInstanceWithObjectVariables = () => ( + <DragAndDropContextProvider> + <FixedHeightFlexContainer height={600}> + <VariablesList + projectScopedContainersAccessor={ + testProject.testSceneProjectScopedContainersAccessor + } + variablesContainer={testProject.testSpriteObjectInstance.getVariables()} + areObjectVariables + emptyPlaceholderDescription="Variables help you store data" + emptyPlaceholderTitle="Variables" + helpPagePath="/variables" + inheritedVariablesContainer={testProject.spriteObject.getVariables()} + onComputeAllVariableNames={() => [ + 'VariableFromEventSheet', + 'VariableFromSomeWhere', + 'InstanceVariable', // already defined variable in testSpriteObjectInstance + ]} + isListLocked={true} /> </FixedHeightFlexContainer> </DragAndDropContextProvider>