diff --git a/.vscode/settings.json b/.vscode/settings.json index 4d938e0f89b6..9785cecd70dd 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -114,7 +114,8 @@ "__bits": "cpp", "__verbose_abort": "cpp", "variant": "cpp", - "charconv": "cpp" + "charconv": "cpp", + "execution": "cpp" }, "files.exclude": { "Binaries/*build*": true, diff --git a/Core/GDCore/Project/BehaviorConfigurationContainer.h b/Core/GDCore/Project/BehaviorConfigurationContainer.h index 87e2fae1514e..9bdf4311bfa2 100644 --- a/Core/GDCore/Project/BehaviorConfigurationContainer.h +++ b/Core/GDCore/Project/BehaviorConfigurationContainer.h @@ -8,8 +8,10 @@ #include #include -#include "GDCore/Serialization/Serializer.h" + #include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Project/QuickCustomizationVisibilitiesContainer.h" +#include "GDCore/Serialization/Serializer.h" #include "GDCore/String.h" namespace gd { @@ -32,12 +34,21 @@ namespace gd { */ class GD_CORE_API BehaviorConfigurationContainer { public: - BehaviorConfigurationContainer() : folded(false), quickCustomizationVisibility(QuickCustomization::Visibility::Default){}; + BehaviorConfigurationContainer() + : folded(false), + quickCustomizationVisibility(QuickCustomization::Visibility::Default), + propertiesQuickCustomizationVisibilities() {}; BehaviorConfigurationContainer(const gd::String& name_, const gd::String& type_) - : name(name_), type(type_), folded(false), quickCustomizationVisibility(QuickCustomization::Visibility::Default){}; + : name(name_), + type(type_), + folded(false), + quickCustomizationVisibility(QuickCustomization::Visibility::Default), + propertiesQuickCustomizationVisibilities() {}; virtual ~BehaviorConfigurationContainer(); - virtual BehaviorConfigurationContainer* Clone() const { return new BehaviorConfigurationContainer(*this); } + virtual BehaviorConfigurationContainer* Clone() const { + return new BehaviorConfigurationContainer(*this); + } /** * \brief Return the name identifying the behavior @@ -68,7 +79,6 @@ class GD_CORE_API BehaviorConfigurationContainer { */ std::map GetProperties() const; - /** * \brief Called when the IDE wants to update a custom property of the * behavior @@ -84,9 +94,7 @@ class GD_CORE_API BehaviorConfigurationContainer { * \brief Called to initialize the content with the default properties * for the behavior. */ - virtual void InitializeContent() { - InitializeContent(content); - }; + virtual void InitializeContent() { InitializeContent(content); }; /** * \brief Serialize the behavior content. @@ -115,15 +123,42 @@ class GD_CORE_API BehaviorConfigurationContainer { */ bool IsFolded() const { return folded; } - void SetQuickCustomizationVisibility(QuickCustomization::Visibility visibility) { + /** + * @brief Set if the whole behavior should be visible or not in the Quick + * Customization. + */ + void SetQuickCustomizationVisibility( + QuickCustomization::Visibility visibility) { quickCustomizationVisibility = visibility; } + /** + * @brief Get if the whole behavior should be visible or not in the Quick + * Customization. + */ QuickCustomization::Visibility GetQuickCustomizationVisibility() const { return quickCustomizationVisibility; } -protected: + /** + * @brief Get the map of properties and their visibility in the Quick + * Customization. + */ + QuickCustomizationVisibilitiesContainer& + GetPropertiesQuickCustomizationVisibilities() { + return propertiesQuickCustomizationVisibilities; + } + + /** + * @brief Get the map of properties and their visibility in the Quick + * Customization. + */ + const QuickCustomizationVisibilitiesContainer& + GetPropertiesQuickCustomizationVisibilities() const { + return propertiesQuickCustomizationVisibilities; + } + + protected: /** * \brief Called when the IDE wants to know about the custom properties of the * behavior. @@ -159,7 +194,7 @@ class GD_CORE_API BehaviorConfigurationContainer { * \brief Called to initialize the content with the default properties * for the behavior. */ - virtual void InitializeContent(gd::SerializerElement& behaviorContent){}; + virtual void InitializeContent(gd::SerializerElement& behaviorContent) {}; private: gd::String name; ///< Name of the behavior @@ -169,6 +204,8 @@ class GD_CORE_API BehaviorConfigurationContainer { gd::SerializerElement content; // Storage for the behavior properties bool folded; QuickCustomization::Visibility quickCustomizationVisibility; + QuickCustomizationVisibilitiesContainer + propertiesQuickCustomizationVisibilities; }; } // namespace gd diff --git a/Core/GDCore/Project/Layout.cpp b/Core/GDCore/Project/Layout.cpp index 9c9ea5bcdcdd..3e1eb344c8d1 100644 --- a/Core/GDCore/Project/Layout.cpp +++ b/Core/GDCore/Project/Layout.cpp @@ -24,10 +24,11 @@ #include "GDCore/Project/ObjectGroup.h" #include "GDCore/Project/ObjectGroupsContainer.h" #include "GDCore/Project/Project.h" +#include "GDCore/Project/QuickCustomization.h" #include "GDCore/Serialization/SerializerElement.h" #include "GDCore/String.h" -#include "GDCore/Tools/PolymorphicClone.h" #include "GDCore/Tools/Log.h" +#include "GDCore/Tools/PolymorphicClone.h" using namespace std; @@ -43,7 +44,7 @@ Layout& Layout::operator=(const Layout& other) { return *this; } -Layout::~Layout(){}; +Layout::~Layout() {}; Layout::Layout() : backgroundColorR(209), @@ -52,9 +53,7 @@ Layout::Layout() stopSoundsOnStartup(true), standardSortMethod(true), disableInputWhenNotFocused(true), - variables(gd::VariablesContainer::SourceType::Scene) -{ -} + variables(gd::VariablesContainer::SourceType::Scene) {} void Layout::SetName(const gd::String& name_) { name = name_; @@ -102,7 +101,9 @@ const gd::Layer& Layout::GetLayer(const gd::String& name) const { return layers.GetLayer(name); } -gd::Layer& Layout::GetLayer(std::size_t index) { return layers.GetLayer(index); } +gd::Layer& Layout::GetLayer(std::size_t index) { + return layers.GetLayer(index); +} const gd::Layer& Layout::GetLayer(std::size_t index) const { return layers.GetLayer(index); @@ -125,9 +126,7 @@ void Layout::InsertLayer(const gd::Layer& layer, std::size_t position) { layers.InsertLayer(layer, position); } -void Layout::RemoveLayer(const gd::String& name) { - layers.RemoveLayer(name); -} +void Layout::RemoveLayer(const gd::String& name) { layers.RemoveLayer(name); } void Layout::SwapLayers(std::size_t firstLayerIndex, std::size_t secondLayerIndex) { @@ -153,7 +152,7 @@ void Layout::UpdateBehaviorsSharedData(gd::Project& project) { allBehaviorsNames.push_back(behavior.GetName()); } } - auto &globalObjects = project.GetObjects(); + auto& globalObjects = project.GetObjects(); for (std::size_t i = 0; i < globalObjects.GetObjectsCount(); ++i) { std::vector objectBehaviors = globalObjects.GetObject(i).GetAllBehaviorNames(); @@ -173,7 +172,8 @@ void Layout::UpdateBehaviorsSharedData(gd::Project& project) { if (behaviorsSharedData.find(name) != behaviorsSharedData.end()) continue; - auto sharedData = CreateBehaviorsSharedData(project, name, allBehaviorsTypes[i]); + auto sharedData = + CreateBehaviorsSharedData(project, name, allBehaviorsTypes[i]); if (sharedData) { behaviorsSharedData[name] = std::move(sharedData); } @@ -196,37 +196,39 @@ void Layout::UpdateBehaviorsSharedData(gd::Project& project) { } std::unique_ptr Layout::CreateBehaviorsSharedData( - gd::Project& project, const gd::String& name, const gd::String& behaviorsType) { - if (project.HasEventsBasedBehavior(behaviorsType)) { - auto sharedData = - gd::make_unique(name, project, behaviorsType); - sharedData->InitializeContent(); - return std::move(sharedData); - } - const gd::BehaviorMetadata& behaviorMetadata = - gd::MetadataProvider::GetBehaviorMetadata( - project.GetCurrentPlatform(), - behaviorsType); - if (gd::MetadataProvider::IsBadBehaviorMetadata(behaviorMetadata)) { - gd::LogWarning("Tried to create a behavior shared data with an unknown type: " + - behaviorsType + " on object " + GetName() + "!"); + gd::Project& project, + const gd::String& name, + const gd::String& behaviorsType) { + if (project.HasEventsBasedBehavior(behaviorsType)) { + auto sharedData = gd::make_unique( + name, project, behaviorsType); + sharedData->InitializeContent(); + return std::move(sharedData); + } + const gd::BehaviorMetadata& behaviorMetadata = + gd::MetadataProvider::GetBehaviorMetadata(project.GetCurrentPlatform(), + behaviorsType); + if (gd::MetadataProvider::IsBadBehaviorMetadata(behaviorMetadata)) { + gd::LogWarning( + "Tried to create a behavior shared data with an unknown type: " + + behaviorsType + " on object " + GetName() + "!"); // It's probably an events-based behavior that was removed. // Create a custom behavior shared data to preserve the properties values. - auto sharedData = - gd::make_unique(name, project, behaviorsType); - sharedData->InitializeContent(); - return std::move(sharedData); - } + auto sharedData = gd::make_unique( + name, project, behaviorsType); + sharedData->InitializeContent(); + return std::move(sharedData); + } - gd::BehaviorsSharedData* behaviorsSharedDataBluePrint = - behaviorMetadata.GetSharedDataInstance(); - if (!behaviorsSharedDataBluePrint) return nullptr; + gd::BehaviorsSharedData* behaviorsSharedDataBluePrint = + behaviorMetadata.GetSharedDataInstance(); + if (!behaviorsSharedDataBluePrint) return nullptr; - auto sharedData = behaviorsSharedDataBluePrint->Clone(); - sharedData->SetName(name); - sharedData->SetTypeName(behaviorsType); - sharedData->InitializeContent(); - return std::unique_ptr(sharedData); + auto sharedData = behaviorsSharedDataBluePrint->Clone(); + sharedData->SetName(name); + sharedData->SetTypeName(behaviorsType); + sharedData->InitializeContent(); + return std::unique_ptr(sharedData); } void Layout::SerializeTo(SerializerElement& element) const { @@ -243,11 +245,13 @@ void Layout::SerializeTo(SerializerElement& element) const { editorSettings.SerializeTo(element.AddChild("uiSettings")); - objectsContainer.GetObjectGroups().SerializeTo(element.AddChild("objectsGroups")); + objectsContainer.GetObjectGroups().SerializeTo( + element.AddChild("objectsGroups")); GetVariables().SerializeTo(element.AddChild("variables")); GetInitialInstances().SerializeTo(element.AddChild("instances")); objectsContainer.SerializeObjectsTo(element.AddChild("objects")); - objectsContainer.SerializeFoldersTo(element.AddChild("objectsFolderStructure")); + objectsContainer.SerializeFoldersTo( + element.AddChild("objectsFolderStructure")); gd::EventsListSerialization::SerializeEventsTo(events, element.AddChild("events")); @@ -257,15 +261,33 @@ void Layout::SerializeTo(SerializerElement& element) const { element.AddChild("behaviorsSharedData"); behaviorDatasElement.ConsiderAsArrayOf("behaviorSharedData"); for (const auto& it : behaviorsSharedData) { + const gd::BehaviorsSharedData& sharedData = *it.second; SerializerElement& dataElement = behaviorDatasElement.AddChild("behaviorSharedData"); - it.second->SerializeTo(dataElement); + sharedData.SerializeTo(dataElement); dataElement.RemoveChild("type"); // The content can contain type or name // properties, remove them. dataElement.RemoveChild("name"); - dataElement.SetAttribute("type", it.second->GetTypeName()); - dataElement.SetAttribute("name", it.second->GetName()); + dataElement.SetAttribute("type", sharedData.GetTypeName()); + dataElement.SetAttribute("name", sharedData.GetName()); + + // Handle Quick Customization info. + dataElement.RemoveChild("propertiesQuickCustomizationVisibilities"); + const QuickCustomizationVisibilitiesContainer& + propertiesQuickCustomizationVisibilities = + sharedData.GetPropertiesQuickCustomizationVisibilities(); + if (!propertiesQuickCustomizationVisibilities.IsEmpty()) { + propertiesQuickCustomizationVisibilities.SerializeTo( + dataElement.AddChild("propertiesQuickCustomizationVisibilities")); + } + const QuickCustomization::Visibility visibility = + sharedData.GetQuickCustomizationVisibility(); + if (visibility != QuickCustomization::Visibility::Default) { + dataElement.SetAttribute( + "quickCustomizationVisibility", + QuickCustomization::VisibilityAsString(visibility)); + } } } @@ -289,9 +311,11 @@ void Layout::UnserializeFrom(gd::Project& project, gd::EventsListSerialization::UnserializeEventsFrom( project, GetEvents(), element.GetChild("events", 0, "Events")); - objectsContainer.UnserializeObjectsFrom(project, element.GetChild("objects", 0, "Objets")); + objectsContainer.UnserializeObjectsFrom( + project, element.GetChild("objects", 0, "Objets")); if (element.HasChild("objectsFolderStructure")) { - objectsContainer.UnserializeFoldersFrom(project, element.GetChild("objectsFolderStructure", 0)); + objectsContainer.UnserializeFoldersFrom( + project, element.GetChild("objectsFolderStructure", 0)); } objectsContainer.AddMissingObjectsInRootFolder(); @@ -321,7 +345,6 @@ void Layout::UnserializeFrom(gd::Project& project, "Behavior"); // Compatibility with GD <= 4 gd::String name = sharedDataElement.GetStringAttribute("name", "", "Name"); - auto sharedData = CreateBehaviorsSharedData(project, name, type); if (sharedData) { // Compatibility with GD <= 4.0.98 @@ -336,6 +359,21 @@ void Layout::UnserializeFrom(gd::Project& project, else { sharedData->UnserializeFrom(sharedDataElement); } + + // Handle Quick Customization info. + if (sharedDataElement.HasChild( + "propertiesQuickCustomizationVisibilities")) { + sharedData->GetPropertiesQuickCustomizationVisibilities() + .UnserializeFrom(sharedDataElement.GetChild( + "propertiesQuickCustomizationVisibilities")); + } + if (sharedDataElement.HasChild("quickCustomizationVisibility")) { + sharedData->SetQuickCustomizationVisibility( + QuickCustomization::StringAsVisibility( + sharedDataElement.GetStringAttribute( + "quickCustomizationVisibility"))); + } + behaviorsSharedData[name] = std::move(sharedData); } } @@ -390,8 +428,9 @@ gd::String GD_CORE_API GetTypeOfObject(const gd::ObjectsContainer& project, type = project.GetObject(name).GetType(); // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. else if (searchInGroups) { for (std::size_t i = 0; i < layout.GetObjectGroups().size(); ++i) { if (layout.GetObjectGroups()[i].GetName() == name) { @@ -448,11 +487,12 @@ gd::String GD_CORE_API GetTypeOfObject(const gd::ObjectsContainer& project, return type; } -void GD_CORE_API FilterBehaviorNamesFromObject( - const gd::Object &object, const gd::String &behaviorType, - std::vector &behaviorNames) { +void GD_CORE_API +FilterBehaviorNamesFromObject(const gd::Object& object, + const gd::String& behaviorType, + std::vector& behaviorNames) { for (size_t i = 0; i < behaviorNames.size();) { - auto &behaviorName = behaviorNames[i]; + auto& behaviorName = behaviorNames[i]; if (!object.HasBehaviorNamed(behaviorName) || object.GetBehavior(behaviorName).GetTypeName() != behaviorType) { behaviorNames.erase(behaviorNames.begin() + i); @@ -462,19 +502,21 @@ void GD_CORE_API FilterBehaviorNamesFromObject( } } -std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( - const gd::ObjectsContainer &project, const gd::ObjectsContainer &layout, - const gd::String &objectOrGroupName, const gd::String &behaviorType, - bool searchInGroups) { +std::vector GD_CORE_API +GetBehaviorNamesInObjectOrGroup(const gd::ObjectsContainer& project, + const gd::ObjectsContainer& layout, + const gd::String& objectOrGroupName, + const gd::String& behaviorType, + bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { - auto &object = layout.GetObject(objectOrGroupName); + auto& object = layout.GetObject(objectOrGroupName); auto behaviorNames = object.GetAllBehaviorNames(); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; } if (project.HasObjectNamed(objectOrGroupName)) { - auto &object = project.GetObject(objectOrGroupName); + auto& object = project.GetObject(objectOrGroupName); auto behaviorNames = object.GetAllBehaviorNames(); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; @@ -486,9 +528,10 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -497,7 +540,7 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( std::vector behaviorNames; return behaviorNames; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -510,15 +553,15 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( auto behaviorNames = GetBehaviorNamesInObjectOrGroup( project, layout, groupsObjects[0], behaviorType, false); for (size_t i = 1; i < groupsObjects.size(); i++) { - auto &objectName = groupsObjects[i]; + auto& objectName = groupsObjects[i]; if (layout.HasObjectNamed(objectName)) { - auto &object = layout.GetObject(objectName); + auto& object = layout.GetObject(objectName); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; } if (project.HasObjectNamed(objectName)) { - auto &object = project.GetObject(objectName); + auto& object = project.GetObject(objectName); FilterBehaviorNamesFromObject(object, behaviorType, behaviorNames); return behaviorNames; } @@ -529,10 +572,10 @@ std::vector GD_CORE_API GetBehaviorNamesInObjectOrGroup( return behaviorNames; } -bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, - const gd::ObjectsContainer &layout, - const gd::String &objectOrGroupName, - const gd::String &behaviorName, +bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer& project, + const gd::ObjectsContainer& layout, + const gd::String& objectOrGroupName, + const gd::String& behaviorName, bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { @@ -547,9 +590,10 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -557,7 +601,7 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } else { return false; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -566,9 +610,9 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } // Check that all objects have the behavior. - for (auto &&object : groupsObjects) { - if (!HasBehaviorInObjectOrGroup(project, layout, object, behaviorName, - false)) { + for (auto&& object : groupsObjects) { + if (!HasBehaviorInObjectOrGroup( + project, layout, object, behaviorName, false)) { return false; } } @@ -576,18 +620,18 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project, } bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, - const gd::ObjectsContainer& layout, - gd::String objectOrGroupName, - gd::String behaviorName, - bool searchInGroups) { + const gd::ObjectsContainer& layout, + gd::String objectOrGroupName, + gd::String behaviorName, + bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { - auto &object = layout.GetObject(objectOrGroupName); + auto& object = layout.GetObject(objectOrGroupName); return object.HasBehaviorNamed(behaviorName) && object.GetBehavior(behaviorName).IsDefaultBehavior(); } if (project.HasObjectNamed(objectOrGroupName)) { - auto &object = project.GetObject(objectOrGroupName); + auto& object = project.GetObject(objectOrGroupName); return object.HasBehaviorNamed(behaviorName) && object.GetBehavior(behaviorName).IsDefaultBehavior(); } @@ -597,9 +641,10 @@ bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -607,7 +652,7 @@ bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, } else { return false; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -616,30 +661,32 @@ bool GD_CORE_API IsDefaultBehavior(const gd::ObjectsContainer& project, } // Check that all objects have the same type. - for (auto &&object : groupsObjects) { - if (!IsDefaultBehavior(project, layout, object, behaviorName, - false)) { + for (auto&& object : groupsObjects) { + if (!IsDefaultBehavior(project, layout, object, behaviorName, false)) { return false; } } return true; } -gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContainer& project, - const gd::ObjectsContainer& layout, - const gd::String& objectOrGroupName, - const gd::String& behaviorName, - bool searchInGroups) { +gd::String GD_CORE_API +GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContainer& project, + const gd::ObjectsContainer& layout, + const gd::String& objectOrGroupName, + const gd::String& behaviorName, + bool searchInGroups) { // Search in objects. if (layout.HasObjectNamed(objectOrGroupName)) { - auto &object = layout.GetObject(objectOrGroupName); - return object.HasBehaviorNamed(behaviorName) ? - object.GetBehavior(behaviorName).GetTypeName() : ""; + auto& object = layout.GetObject(objectOrGroupName); + return object.HasBehaviorNamed(behaviorName) + ? object.GetBehavior(behaviorName).GetTypeName() + : ""; } if (project.HasObjectNamed(objectOrGroupName)) { - auto &object = project.GetObject(objectOrGroupName); - return object.HasBehaviorNamed(behaviorName) ? - object.GetBehavior(behaviorName).GetTypeName() : ""; + auto& object = project.GetObject(objectOrGroupName); + return object.HasBehaviorNamed(behaviorName) + ? object.GetBehavior(behaviorName).GetTypeName() + : ""; } if (!searchInGroups) { @@ -647,9 +694,10 @@ gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContain } // Search in groups. - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. - const gd::ObjectsContainer *container; + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. + const gd::ObjectsContainer* container; if (layout.GetObjectGroups().Has(objectOrGroupName)) { container = &layout; } else if (project.GetObjectGroups().Has(objectOrGroupName)) { @@ -657,7 +705,7 @@ gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContain } else { return ""; } - const vector &groupsObjects = + const vector& groupsObjects = container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames(); // Empty groups don't contain any behavior. @@ -668,9 +716,9 @@ gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContain // Check that all objects have the behavior with the same type. auto behaviorType = GetTypeOfBehaviorInObjectOrGroup( project, layout, groupsObjects[0], behaviorName, false); - for (auto &&object : groupsObjects) { - if (GetTypeOfBehaviorInObjectOrGroup(project, layout, object, behaviorName, - false) != behaviorType) { + for (auto&& object : groupsObjects) { + if (GetTypeOfBehaviorInObjectOrGroup( + project, layout, object, behaviorName, false) != behaviorType) { return ""; } } @@ -682,14 +730,14 @@ gd::String GD_CORE_API GetTypeOfBehavior(const gd::ObjectsContainer& project, gd::String name, bool searchInGroups) { for (std::size_t i = 0; i < layout.GetObjectsCount(); ++i) { - const auto &object = layout.GetObject(i); + const auto& object = layout.GetObject(i); if (object.HasBehaviorNamed(name)) { return object.GetBehavior(name).GetTypeName(); } } for (std::size_t i = 0; i < project.GetObjectsCount(); ++i) { - const auto &object = project.GetObject(i); + const auto& object = project.GetObject(i); if (object.HasBehaviorNamed(name)) { return object.GetBehavior(name).GetTypeName(); } @@ -726,8 +774,9 @@ GetBehaviorsOfObject(const gd::ObjectsContainer& project, } // Search in groups - // Currently, a group is considered as the "intersection" of all of its objects. - // Search "groups is the intersection of its objects" in the codebase. + // Currently, a group is considered as the "intersection" of all of its + // objects. Search "groups is the intersection of its objects" in the + // codebase. if (searchInGroups) { for (std::size_t i = 0; i < layout.GetObjectGroups().size(); ++i) { if (layout.GetObjectGroups()[i].GetName() == name) { diff --git a/Core/GDCore/Project/Object.cpp b/Core/GDCore/Project/Object.cpp index 0b11d52d71ce..02ac3bbeb489 100644 --- a/Core/GDCore/Project/Object.cpp +++ b/Core/GDCore/Project/Object.cpp @@ -12,8 +12,9 @@ #include "GDCore/Project/CustomBehavior.h" #include "GDCore/Project/Layout.h" #include "GDCore/Project/Project.h" -#include "GDCore/Serialization/SerializerElement.h" #include "GDCore/Project/PropertyDescriptor.h" +#include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Serialization/SerializerElement.h" #include "GDCore/Tools/Log.h" #include "GDCore/Tools/UUID/UUID.h" @@ -27,8 +28,8 @@ Object::Object(const gd::String& name_, : name(name_), configuration(std::move(configuration_)), objectVariables(gd::VariablesContainer::SourceType::Object) { - SetType(type_); - } + SetType(type_); +} Object::Object(const gd::String& name_, const gd::String& type_, @@ -36,8 +37,8 @@ Object::Object(const gd::String& name_, : name(name_), configuration(configuration_), objectVariables(gd::VariablesContainer::SourceType::Object) { - SetType(type_); - } + SetType(type_); +} void Object::Init(const gd::Object& object) { persistentUuid = object.persistentUuid; @@ -54,9 +55,7 @@ void Object::Init(const gd::Object& object) { configuration = object.configuration->Clone(); } -gd::ObjectConfiguration& Object::GetConfiguration() { - return *configuration; -} +gd::ObjectConfiguration& Object::GetConfiguration() { return *configuration; } const gd::ObjectConfiguration& Object::GetConfiguration() const { return *configuration; @@ -77,8 +76,7 @@ bool Object::RenameBehavior(const gd::String& name, const gd::String& newName) { behaviors.find(newName) != behaviors.end()) return false; - std::unique_ptr aut = - std::move(behaviors.find(name)->second); + std::unique_ptr aut = std::move(behaviors.find(name)->second); behaviors.erase(name); behaviors[newName] = std::move(aut); behaviors[newName]->SetName(newName); @@ -99,10 +97,10 @@ bool Object::HasBehaviorNamed(const gd::String& name) const { } gd::Behavior* Object::AddNewBehavior(const gd::Project& project, - const gd::String& type, - const gd::String& name) { - auto initializeAndAdd = - [this, &name](std::unique_ptr behavior) { + const gd::String& type, + const gd::String& name) { + auto initializeAndAdd = [this, + &name](std::unique_ptr behavior) { behavior->InitializeContent(); this->behaviors[name] = std::move(behavior); return this->behaviors[name].get(); @@ -111,18 +109,17 @@ gd::Behavior* Object::AddNewBehavior(const gd::Project& project, if (project.HasEventsBasedBehavior(type)) { return initializeAndAdd( gd::make_unique(name, project, type)); - } - else { + } else { const gd::BehaviorMetadata& behaviorMetadata = gd::MetadataProvider::GetBehaviorMetadata(project.GetCurrentPlatform(), type); if (gd::MetadataProvider::IsBadBehaviorMetadata(behaviorMetadata)) { - gd::LogWarning("Tried to create a behavior with an unknown type: " + type - + " on object " + GetName() + "!"); - // It's probably an events-based behavior that was removed. - // Create a custom behavior to preserve the properties values. - return initializeAndAdd( - gd::make_unique(name, project, type)); + gd::LogWarning("Tried to create a behavior with an unknown type: " + + type + " on object " + GetName() + "!"); + // It's probably an events-based behavior that was removed. + // Create a custom behavior to preserve the properties values. + return initializeAndAdd( + gd::make_unique(name, project, type)); } std::unique_ptr behavior(behaviorMetadata.Get().Clone()); behavior->SetName(name); @@ -196,6 +193,20 @@ void Object::UnserializeFrom(gd::Project& project, else { behavior->UnserializeFrom(behaviorElement); } + + // Handle Quick Customization info. + if (behaviorElement.HasChild( + "propertiesQuickCustomizationVisibilities")) { + behavior->GetPropertiesQuickCustomizationVisibilities().UnserializeFrom( + behaviorElement.GetChild( + "propertiesQuickCustomizationVisibilities")); + } + if (behaviorElement.HasChild("quickCustomizationVisibility")) { + behavior->SetQuickCustomizationVisibility( + QuickCustomization::StringAsVisibility( + behaviorElement.GetStringAttribute( + "quickCustomizationVisibility"))); + } } } @@ -217,8 +228,8 @@ void Object::SerializeTo(SerializerElement& element) const { std::vector allBehaviors = GetAllBehaviorNames(); for (std::size_t i = 0; i < allBehaviors.size(); ++i) { const gd::Behavior& behavior = GetBehavior(allBehaviors[i]); - // Default behaviors are added at the object creation according to metadata. - // They don't need to be serialized. + // Default behaviors are added at the object creation according to + // metadata. They don't need to be serialized. if (behavior.IsDefaultBehavior()) { continue; } @@ -230,6 +241,23 @@ void Object::SerializeTo(SerializerElement& element) const { behaviorElement.RemoveChild("name"); behaviorElement.SetAttribute("type", behavior.GetTypeName()); behaviorElement.SetAttribute("name", behavior.GetName()); + + // Handle Quick Customization info. + behaviorElement.RemoveChild("propertiesQuickCustomizationVisibilities"); + const QuickCustomizationVisibilitiesContainer& + propertiesQuickCustomizationVisibilities = + behavior.GetPropertiesQuickCustomizationVisibilities(); + if (!propertiesQuickCustomizationVisibilities.IsEmpty()) { + propertiesQuickCustomizationVisibilities.SerializeTo( + behaviorElement.AddChild("propertiesQuickCustomizationVisibilities")); + } + const QuickCustomization::Visibility visibility = + behavior.GetQuickCustomizationVisibility(); + if (visibility != QuickCustomization::Visibility::Default) { + behaviorElement.SetAttribute( + "quickCustomizationVisibility", + QuickCustomization::VisibilityAsString(visibility)); + } } configuration->SerializeTo(element); diff --git a/Core/GDCore/Project/QuickCustomization.h b/Core/GDCore/Project/QuickCustomization.h index c09c102d6d30..4b162fb3d144 100644 --- a/Core/GDCore/Project/QuickCustomization.h +++ b/Core/GDCore/Project/QuickCustomization.h @@ -1,16 +1,37 @@ #pragma once +#include "GDCore/String.h" + namespace gd { class QuickCustomization { public: enum Visibility { - /** Visibility based on the parent or editor heuristics (probably visible). */ + /** Visibility based on the parent or editor heuristics (probably visible). + */ Default, /** Visible in the quick customization editor. */ Visible, /** Not visible in the quick customization editor. */ Hidden }; + + static Visibility StringAsVisibility(const gd::String& str) { + if (str == "visible") + return Visibility::Visible; + else if (str == "hidden") + return Visibility::Hidden; + + return Visibility::Default; + } + + static gd::String VisibilityAsString(Visibility visibility) { + if (visibility == Visibility::Visible) + return "visible"; + else if (visibility == Visibility::Hidden) + return "hidden"; + + return "default"; + } }; } // namespace gd \ No newline at end of file diff --git a/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.cpp b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.cpp new file mode 100644 index 000000000000..aa6d702fbc09 --- /dev/null +++ b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.cpp @@ -0,0 +1,58 @@ +/* + * GDevelop Core + * Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights + * reserved. This project is released under the MIT License. + */ +#include "GDCore/Project/QuickCustomizationVisibilitiesContainer.h" + +#include +#include + +#include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/String.h" +#include "GDCore/Tools/UUID/UUID.h" + +using namespace std; + +namespace gd { + +QuickCustomizationVisibilitiesContainer:: + QuickCustomizationVisibilitiesContainer() {} + +bool QuickCustomizationVisibilitiesContainer::IsEmpty() const { + return visibilities.empty(); +} + +void QuickCustomizationVisibilitiesContainer::Set( + const gd::String& name, QuickCustomization::Visibility visibility) { + visibilities[name] = visibility; +} + +QuickCustomization::Visibility QuickCustomizationVisibilitiesContainer::Get( + const gd::String& name) const { + auto it = visibilities.find(name); + if (it != visibilities.end()) return it->second; + + return QuickCustomization::Visibility::Default; +} + +void QuickCustomizationVisibilitiesContainer::SerializeTo( + SerializerElement& element) const { + for (auto& visibility : visibilities) { + element.SetStringAttribute( + visibility.first, + QuickCustomization::VisibilityAsString(visibility.second)); + } +} + +void QuickCustomizationVisibilitiesContainer::UnserializeFrom( + const SerializerElement& element) { + visibilities.clear(); + for (auto& child : element.GetAllChildren()) { + visibilities[child.first] = + QuickCustomization::StringAsVisibility(child.second->GetStringValue()); + } +} + +} // namespace gd diff --git a/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.h b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.h new file mode 100644 index 000000000000..56794d5ef224 --- /dev/null +++ b/Core/GDCore/Project/QuickCustomizationVisibilitiesContainer.h @@ -0,0 +1,29 @@ +#pragma once +#include +#include + +#include "GDCore/Project/QuickCustomization.h" +#include "GDCore/Serialization/SerializerElement.h" +#include "GDCore/String.h" + +namespace gd { + +class QuickCustomizationVisibilitiesContainer { + public: + QuickCustomizationVisibilitiesContainer(); + + void Set(const gd::String& name, QuickCustomization::Visibility visibility); + + QuickCustomization::Visibility Get(const gd::String& name) const; + + bool IsEmpty() const; + + void SerializeTo(SerializerElement& element) const; + + void UnserializeFrom(const SerializerElement& element); + + private: + std::map visibilities; +}; + +} // namespace gd \ No newline at end of file diff --git a/Extensions/3D/AmbientLight.ts b/Extensions/3D/AmbientLight.ts index 3efbb8621d50..438ff6761641 100644 --- a/Extensions/3D/AmbientLight.ts +++ b/Extensions/3D/AmbientLight.ts @@ -73,9 +73,7 @@ namespace gdjs { } updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { - this.light.color.setHex( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) - ); + this.light.color.setHex(gdjs.rgbOrHexStringToNumber(value)); } } updateColorParameter(parameterName: string, value: number): void { diff --git a/Extensions/3D/DirectionalLight.ts b/Extensions/3D/DirectionalLight.ts index 8b8d60599b8e..e481412120dd 100644 --- a/Extensions/3D/DirectionalLight.ts +++ b/Extensions/3D/DirectionalLight.ts @@ -94,7 +94,7 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { this.light.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } if (parameterName === 'top') { diff --git a/Extensions/3D/ExponentialFog.ts b/Extensions/3D/ExponentialFog.ts index 976cd9a70a45..dc39b12e0f19 100644 --- a/Extensions/3D/ExponentialFog.ts +++ b/Extensions/3D/ExponentialFog.ts @@ -71,7 +71,7 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { this.fog.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } } diff --git a/Extensions/3D/HemisphereLight.ts b/Extensions/3D/HemisphereLight.ts index 288042205ba8..ed803501d4c8 100644 --- a/Extensions/3D/HemisphereLight.ts +++ b/Extensions/3D/HemisphereLight.ts @@ -95,12 +95,12 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'skyColor') { this.light.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } if (parameterName === 'groundColor') { this.light.groundColor = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } if (parameterName === 'top') { diff --git a/Extensions/3D/LinearFog.ts b/Extensions/3D/LinearFog.ts index b2381a76b95f..bf821fe05e31 100644 --- a/Extensions/3D/LinearFog.ts +++ b/Extensions/3D/LinearFog.ts @@ -76,7 +76,7 @@ namespace gdjs { updateStringParameter(parameterName: string, value: string): void { if (parameterName === 'color') { this.fog.color = new THREE.Color( - gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value) + gdjs.rgbOrHexStringToNumber(value) ); } } diff --git a/Extensions/Effects/bevel-pixi-filter.ts b/Extensions/Effects/bevel-pixi-filter.ts index 8148082fad6b..f9df16eba7c9 100644 --- a/Extensions/Effects/bevel-pixi-filter.ts +++ b/Extensions/Effects/bevel-pixi-filter.ts @@ -67,14 +67,10 @@ namespace gdjs { const bevelFilter = (filter as unknown) as PIXI.filters.BevelFilter & BevelFilterExtra; if (parameterName === 'lightColor') { - bevelFilter.lightColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + bevelFilter.lightColor = gdjs.rgbOrHexStringToNumber(value); } if (parameterName === 'shadowColor') { - bevelFilter.shadowColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + bevelFilter.shadowColor = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/color-replace-pixi-filter.ts b/Extensions/Effects/color-replace-pixi-filter.ts index 0d004f467cc8..eb75a8e5e568 100644 --- a/Extensions/Effects/color-replace-pixi-filter.ts +++ b/Extensions/Effects/color-replace-pixi-filter.ts @@ -45,13 +45,9 @@ namespace gdjs { const colorReplaceFilter = (filter as unknown) as PIXI.filters.ColorReplaceFilter & ColorReplaceFilterExtra; if (parameterName === 'originalColor') { - colorReplaceFilter.originalColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + colorReplaceFilter.originalColor = gdjs.rgbOrHexStringToNumber(value); } else if (parameterName === 'newColor') { - colorReplaceFilter.newColor = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + colorReplaceFilter.newColor = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/drop-shadow-pixi-filter.ts b/Extensions/Effects/drop-shadow-pixi-filter.ts index c9c163fe3d73..46c878e976c9 100644 --- a/Extensions/Effects/drop-shadow-pixi-filter.ts +++ b/Extensions/Effects/drop-shadow-pixi-filter.ts @@ -66,9 +66,7 @@ namespace gdjs { ) { const dropShadowFilter = (filter as unknown) as PIXI.filters.DropShadowFilter; if (parameterName === 'color') { - dropShadowFilter.color = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + dropShadowFilter.color = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/glow-pixi-filter.ts b/Extensions/Effects/glow-pixi-filter.ts index 49c2419a0906..fbc16eea0ac5 100644 --- a/Extensions/Effects/glow-pixi-filter.ts +++ b/Extensions/Effects/glow-pixi-filter.ts @@ -53,7 +53,7 @@ namespace gdjs { const glowFilter = (filter as unknown) as PIXI.filters.GlowFilter & GlowFilterExtra; if (parameterName === 'color') { - glowFilter.color = gdjs.PixiFiltersTools.rgbOrHexToHexNumber(value); + glowFilter.color = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/Effects/outline-pixi-filter.ts b/Extensions/Effects/outline-pixi-filter.ts index e1c1a037aa2c..0dce37f67b9f 100644 --- a/Extensions/Effects/outline-pixi-filter.ts +++ b/Extensions/Effects/outline-pixi-filter.ts @@ -41,9 +41,7 @@ namespace gdjs { ) { const outlineFilter = (filter as unknown) as PIXI.filters.OutlineFilter; if (parameterName === 'color') { - outlineFilter.color = gdjs.PixiFiltersTools.rgbOrHexToHexNumber( - value - ); + outlineFilter.color = gdjs.rgbOrHexStringToNumber(value); } } updateColorParameter( diff --git a/Extensions/ParticleSystem/particleemitterobject.ts b/Extensions/ParticleSystem/particleemitterobject.ts index 0f6c2f2fab32..44ece11796a6 100644 --- a/Extensions/ParticleSystem/particleemitterobject.ts +++ b/Extensions/ParticleSystem/particleemitterobject.ts @@ -877,7 +877,6 @@ namespace gdjs { setParticleColor1AsNumber(color: number): void { this.color1 = color; this._colorDirty = true; - debugger; } setParticleColor1(rgbOrHexColor: string): void { @@ -889,7 +888,6 @@ namespace gdjs { setParticleColor2AsNumber(color: number): void { this.color2 = color; this._colorDirty = true; - debugger; } setParticleColor2(rgbOrHexColor: string): void { diff --git a/Extensions/TextObject/TextObject.cpp b/Extensions/TextObject/TextObject.cpp index 7b479739febe..f33901302124 100644 --- a/Extensions/TextObject/TextObject.cpp +++ b/Extensions/TextObject/TextObject.cpp @@ -129,19 +129,22 @@ std::map TextObject::GetProperties() const { .SetType("resource") .AddExtraInfo("font") .SetLabel(_("Font")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["bold"] .SetValue(bold ? "true" : "false") .SetType("boolean") .SetLabel(_("Bold")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["italic"] .SetValue(italic ? "true" : "false") .SetType("boolean") .SetLabel(_("Italic")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["color"] .SetValue(color) @@ -157,21 +160,24 @@ std::map TextObject::GetProperties() const { .AddExtraInfo("right") .SetLabel(_("Alignment")) .SetDescription(_("Alignment of the text when multiple lines are displayed")) - .SetGroup(_("Font")); + .SetGroup(_("Font")) + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["isOutlineEnabled"] .SetValue(isOutlineEnabled ? "true" : "false") .SetType("boolean") .SetLabel(_("Show outline")) .SetGroup(_("Outline")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["outlineColor"] .SetValue(outlineColor) .SetType("color") .SetLabel(_("Color")) .SetGroup(_("Outline")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["outlineThickness"] .SetValue(gd::String::From(outlineThickness)) @@ -179,21 +185,24 @@ std::map TextObject::GetProperties() const { .SetLabel(_("Thickness")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) .SetGroup(_("Outline")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["isShadowEnabled"] .SetValue(isShadowEnabled ? "true" : "false") .SetType("boolean") .SetLabel(_("Show shadow")) .SetGroup(_("Shadow")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowColor"] .SetValue(shadowColor) .SetType("color") .SetLabel(_("Color")) .SetGroup(_("Shadow")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowOpacity"] .SetValue(gd::String::From(shadowOpacity)) @@ -201,7 +210,8 @@ std::map TextObject::GetProperties() const { .SetLabel(_("Opacity")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) .SetGroup(_("Shadow")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowAngle"] .SetValue(gd::String::From(shadowAngle)) @@ -209,7 +219,8 @@ std::map TextObject::GetProperties() const { .SetLabel(_("Angle")) .SetMeasurementUnit(gd::MeasurementUnit::GetDegreeAngle()) .SetGroup(_("Shadow")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowDistance"] .SetValue(gd::String::From(shadowDistance)) @@ -217,7 +228,8 @@ std::map TextObject::GetProperties() const { .SetLabel(_("Distance")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) .SetGroup(_("Shadow")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); objectProperties["shadowBlurRadius"] .SetValue(gd::String::From(shadowBlurRadius)) @@ -225,7 +237,8 @@ std::map TextObject::GetProperties() const { .SetLabel(_("Blur radius")) .SetMeasurementUnit(gd::MeasurementUnit::GetPixel()) .SetGroup(_("Shadow")) - .SetAdvanced(); + .SetAdvanced() + .SetQuickCustomizationVisibility(gd::QuickCustomization::Hidden); return objectProperties; } diff --git a/GDJS/Runtime/gd.ts b/GDJS/Runtime/gd.ts index fd2bd17905db..82e3ded9f09c 100644 --- a/GDJS/Runtime/gd.ts +++ b/GDJS/Runtime/gd.ts @@ -10,6 +10,9 @@ */ namespace gdjs { const logger = new gdjs.Logger('Engine runtime'); + const hexStringRegex = /^(#{0,1}[A-Fa-f0-9]{6})$/; + const shorthandHexStringRegex = /^(#{0,1}[A-Fa-f0-9]{3})$/; + const rgbStringRegex = /^(\d{1,3};\d{1,3};\d{1,3})/; /** * Contains functions used by events (this is a convention only, functions can actually @@ -61,7 +64,7 @@ namespace gdjs { }; /** - * Convert a Hex string to an RGB color array [r, g, b], where each component is in the range [0, 255]. + * Convert a Hex string (#124FE4) to an RGB color array [r, g, b], where each component is in the range [0, 255]. * * @param {string} hex Color hexadecimal */ @@ -74,6 +77,24 @@ namespace gdjs { : [0, 0, 0]; }; + /** + * Convert a shorthand Hex string (#1F4) to an RGB color array [r, g, b], where each component is in the range [0, 255]. + * + * @param {string} hex Color hexadecimal + */ + export const shorthandHexToRGBColor = function ( + hexString: string + ): [number, number, number] { + const hexNumber = parseInt(hexString.replace('#', ''), 16); + return Number.isFinite(hexNumber) + ? [ + 17 * ((hexNumber >> 8) & 0xf), + 17 * ((hexNumber >> 4) & 0xf), + 17 * (hexNumber & 0xf), + ] + : [0, 0, 0]; + }; + /** * Convert a RGB string ("rrr;ggg;bbb") or a Hex string ("#rrggbb") to a RGB color array ([r,g,b] with each component going from 0 to 255). * @param value The color as a RGB string or Hex string @@ -81,17 +102,28 @@ namespace gdjs { export const rgbOrHexToRGBColor = function ( value: string ): [number, number, number] { - const splitValue = value.split(';'); - // If a RGB string is provided, return the RGB object. - if (splitValue.length === 3) { - return [ - parseInt(splitValue[0], 10), - parseInt(splitValue[1], 10), - parseInt(splitValue[2], 10), - ]; + const rgbColor = extractRGBString(value); + if (rgbColor) { + const splitValue = rgbColor.split(';'); + // If a RGB string is provided, return the RGB object. + if (splitValue.length === 3) { + return [ + Math.min(255, parseInt(splitValue[0], 10)), + Math.min(255, parseInt(splitValue[1], 10)), + Math.min(255, parseInt(splitValue[2], 10)), + ]; + } + } + + const hexColor = extractHexString(value); + if (hexColor) { + return hexToRGBColor(hexColor); + } + const shorthandHexColor = extractShorthandHexString(value); + if (shorthandHexColor) { + return shorthandHexToRGBColor(shorthandHexColor); } - // Otherwise, convert the Hex to RGB. - return hexToRGBColor(value); + return [0, 0, 0]; }; /** @@ -146,6 +178,23 @@ namespace gdjs { ]; }; + export const extractHexString = (str: string): string | null => { + const matches = str.match(hexStringRegex); + if (!matches) return null; + return matches[0]; + }; + export const extractShorthandHexString = (str: string): string | null => { + const matches = str.match(shorthandHexStringRegex); + if (!matches) return null; + return matches[0]; + }; + + export const extractRGBString = (str: string): string | null => { + const matches = str.match(rgbStringRegex); + if (!matches) return null; + return matches[0]; + }; + /** * Get a random integer between 0 and max. * @param max The maximum value (inclusive). diff --git a/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts b/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts index 0642b23c6c10..c4f12b12a18a 100644 --- a/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts +++ b/GDJS/Runtime/pixi-renderers/pixi-filters-tools.ts @@ -52,22 +52,6 @@ namespace gdjs { _filterCreators[filterName] = filterCreator; }; - /** - * Convert a string RGB color ("rrr;ggg;bbb") or a hex string (#rrggbb) to a hex number. - * @param value The color as a RGB string or hex string - */ - export const rgbOrHexToHexNumber = function (value: string): number { - const splitValue = value.split(';'); - if (splitValue.length === 3) { - return gdjs.rgbToHexNumber( - parseInt(splitValue[0], 10), - parseInt(splitValue[1], 10), - parseInt(splitValue[2], 10) - ); - } - return parseInt(value.replace('#', '0x'), 16); - }; - /** A wrapper allowing to create an effect. */ export interface FilterCreator { /** Function to call to create the filter */ diff --git a/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts b/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts index 030d3dba6882..d676323b9c6c 100644 --- a/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts +++ b/GDJS/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.ts @@ -142,16 +142,8 @@ namespace gdjs { this._sprite.visible = !this._object.hidden; } - setColor(rgbColor): void { - const colors = rgbColor.split(';'); - if (colors.length < 3) { - return; - } - this._sprite.tint = gdjs.rgbToHexNumber( - parseInt(colors[0], 10), - parseInt(colors[1], 10), - parseInt(colors[2], 10) - ); + setColor(rgbOrHexColor): void { + this._sprite.tint = gdjs.rgbOrHexStringToNumber(rgbOrHexColor); } getColor() { diff --git a/GDJS/Runtime/spriteruntimeobject.ts b/GDJS/Runtime/spriteruntimeobject.ts index 7dd4b90e9e8e..4b9227fc35bb 100644 --- a/GDJS/Runtime/spriteruntimeobject.ts +++ b/GDJS/Runtime/spriteruntimeobject.ts @@ -759,10 +759,10 @@ namespace gdjs { /** * Change the tint of the sprite object. * - * @param rgbColor The color, in RGB format ("128;200;255"). + * @param rgbOrHexColor The color as a string, in RGB format ("128;200;255") or Hex format. */ - setColor(rgbColor: string): void { - this._renderer.setColor(rgbColor); + setColor(rgbOrHexColor: string): void { + this._renderer.setColor(rgbOrHexColor); } /** diff --git a/GDJS/tests/tests/colors.js b/GDJS/tests/tests/colors.js new file mode 100644 index 000000000000..01bcc50f2f65 --- /dev/null +++ b/GDJS/tests/tests/colors.js @@ -0,0 +1,55 @@ +describe('gdjs', function () { + it('should define gdjs', function () { + expect(gdjs).to.be.ok(); + }); + + describe('Color conversion', function () { + describe('Hex strings to RGB components', () => { + it('should convert hex strings', function () { + expect(gdjs.hexToRGBColor('#FFFFfF')).to.eql([255, 255, 255]); + expect(gdjs.hexToRGBColor('#000000')).to.eql([0, 0, 0]); + expect(gdjs.hexToRGBColor('#1245F5')).to.eql([18, 69, 245]); + }); + it('should convert hex strings without hashtag', function () { + expect(gdjs.hexToRGBColor('FFFFfF')).to.eql([255, 255, 255]); + expect(gdjs.hexToRGBColor('000000')).to.eql([0, 0, 0]); + expect(gdjs.hexToRGBColor('1245F5')).to.eql([18, 69, 245]); + }); + it('should convert shorthand hex strings', function () { + expect(gdjs.shorthandHexToRGBColor('#FfF')).to.eql([255, 255, 255]); + expect(gdjs.shorthandHexToRGBColor('#000')).to.eql([0, 0, 0]); + expect(gdjs.shorthandHexToRGBColor('#F3a')).to.eql([255, 51, 170]); + }); + it('should convert shorthand hex strings without hashtag', function () { + expect(gdjs.shorthandHexToRGBColor('FFF')).to.eql([255, 255, 255]); + expect(gdjs.shorthandHexToRGBColor('000')).to.eql([0, 0, 0]); + expect(gdjs.shorthandHexToRGBColor('F3a')).to.eql([255, 51, 170]); + }); + }); + describe.only('RGB strings to RGB components', () => { + it('should convert rgb strings', function () { + expect(gdjs.rgbOrHexToRGBColor('0;0;0')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('255;255;255')).to.eql([255, 255, 255]); + expect(gdjs.rgbOrHexToRGBColor('120;12;6')).to.eql([120, 12, 6]); + }); + it('should max rgb values', function () { + expect(gdjs.rgbOrHexToRGBColor('255;255;300')).to.eql([255, 255, 255]); + expect(gdjs.rgbOrHexToRGBColor('999;12;6')).to.eql([255, 12, 6]); + }); + it('should cut rgb values if string too long', function () { + expect(gdjs.rgbOrHexToRGBColor('255;255;200456')).to.eql([ + 255, + 255, + 200, + ]); + }); + it('should return components for black if unrecognized input', function () { + expect(gdjs.rgbOrHexToRGBColor('NaN')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('19819830803')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('Infinity')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('-4564')).to.eql([0, 0, 0]); + expect(gdjs.rgbOrHexToRGBColor('9999;12;6')).to.eql([0, 0, 0]); + }); + }); + }); +}); diff --git a/GDevelop.js/Bindings/Bindings.idl b/GDevelop.js/Bindings/Bindings.idl index 98560ab2594f..5cb5e6bf7369 100644 --- a/GDevelop.js/Bindings/Bindings.idl +++ b/GDevelop.js/Bindings/Bindings.idl @@ -750,6 +750,8 @@ interface Behavior { void SetFolded(boolean folded); boolean IsDefaultBehavior(); + + [Ref] QuickCustomizationVisibilitiesContainer GetPropertiesQuickCustomizationVisibilities(); }; [JSImplementation=Behavior] @@ -771,6 +773,8 @@ interface BehaviorsSharedData { [Value] MapStringPropertyDescriptor GetProperties(); boolean UpdateProperty([Const] DOMString name, [Const] DOMString value); void InitializeContent(); + + [Ref] QuickCustomizationVisibilitiesContainer GetPropertiesQuickCustomizationVisibilities(); }; [JSImplementation=BehaviorsSharedData] @@ -1918,6 +1922,11 @@ interface QuickCustomization { // Nothing, it's just a container for the visibility enum. }; +interface QuickCustomizationVisibilitiesContainer { + void Set([Const] DOMString name, QuickCustomization_Visibility visibility); + QuickCustomization_Visibility Get([Const] DOMString name); +}; + interface BehaviorMetadata { [Const, Ref] DOMString GetName(); [Const, Ref] DOMString GetFullName(); diff --git a/GDevelop.js/types.d.ts b/GDevelop.js/types.d.ts index 41a38995eb50..c8c294d356ff 100644 --- a/GDevelop.js/types.d.ts +++ b/GDevelop.js/types.d.ts @@ -649,6 +649,7 @@ export class Behavior extends EmscriptenObject { isFolded(): boolean; setFolded(folded: boolean): void; isDefaultBehavior(): boolean; + getPropertiesQuickCustomizationVisibilities(): QuickCustomizationVisibilitiesContainer; } export class BehaviorJsImplementation extends Behavior { @@ -666,6 +667,7 @@ export class BehaviorsSharedData extends EmscriptenObject { getProperties(): MapStringPropertyDescriptor; updateProperty(name: string, value: string): boolean; initializeContent(): void; + getPropertiesQuickCustomizationVisibilities(): QuickCustomizationVisibilitiesContainer; } export class BehaviorSharedDataJsImplementation extends BehaviorsSharedData { @@ -1534,6 +1536,11 @@ export class QuickCustomization extends EmscriptenObject { static Hidden = 2; } +export class QuickCustomizationVisibilitiesContainer extends EmscriptenObject { + set(name: string, visibility: QuickCustomization_Visibility): void; + get(name: string): QuickCustomization_Visibility; +} + export class BehaviorMetadata extends EmscriptenObject { getName(): string; getFullName(): string; diff --git a/GDevelop.js/types/gdbehavior.js b/GDevelop.js/types/gdbehavior.js index 6392464c7f71..6be63e427cc4 100644 --- a/GDevelop.js/types/gdbehavior.js +++ b/GDevelop.js/types/gdbehavior.js @@ -13,6 +13,7 @@ declare class gdBehavior { isFolded(): boolean; setFolded(folded: boolean): void; isDefaultBehavior(): boolean; + getPropertiesQuickCustomizationVisibilities(): gdQuickCustomizationVisibilitiesContainer; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/gdbehaviorsshareddata.js b/GDevelop.js/types/gdbehaviorsshareddata.js index 28311eb336b2..1cdf78cfb382 100644 --- a/GDevelop.js/types/gdbehaviorsshareddata.js +++ b/GDevelop.js/types/gdbehaviorsshareddata.js @@ -7,6 +7,7 @@ declare class gdBehaviorsSharedData { getProperties(): gdMapStringPropertyDescriptor; updateProperty(name: string, value: string): boolean; initializeContent(): void; + getPropertiesQuickCustomizationVisibilities(): gdQuickCustomizationVisibilitiesContainer; delete(): void; ptr: number; }; \ No newline at end of file diff --git a/GDevelop.js/types/gdquickcustomizationvisibilitiescontainer.js b/GDevelop.js/types/gdquickcustomizationvisibilitiescontainer.js new file mode 100644 index 000000000000..26aa5580709d --- /dev/null +++ b/GDevelop.js/types/gdquickcustomizationvisibilitiescontainer.js @@ -0,0 +1,7 @@ +// Automatically generated by GDevelop.js/scripts/generate-types.js +declare class gdQuickCustomizationVisibilitiesContainer { + set(name: string, visibility: QuickCustomization_Visibility): void; + get(name: string): QuickCustomization_Visibility; + 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 ae7f433578a0..a707d41a7e7a 100644 --- a/GDevelop.js/types/libgdevelop.js +++ b/GDevelop.js/types/libgdevelop.js @@ -151,6 +151,7 @@ declare class libGDevelop { ObjectMetadata: Class; QuickCustomization_Visibility: Class; QuickCustomization: Class; + QuickCustomizationVisibilitiesContainer: Class; BehaviorMetadata: Class; EffectMetadata: Class; EventMetadata: Class; diff --git a/newIDE/app/public/res/quick_customization/replace_objects.svg b/newIDE/app/public/res/quick_customization/replace_objects.svg deleted file mode 100644 index 5f68c8b21057..000000000000 --- a/newIDE/app/public/res/quick_customization/replace_objects.svg +++ /dev/null @@ -1,148 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/newIDE/app/public/res/quick_customization/tweak_gameplay.svg b/newIDE/app/public/res/quick_customization/tweak_gameplay.svg deleted file mode 100644 index 01777a94378d..000000000000 --- a/newIDE/app/public/res/quick_customization/tweak_gameplay.svg +++ /dev/null @@ -1,78 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/newIDE/app/src/AssetStore/AssetStoreContext.js b/newIDE/app/src/AssetStore/AssetStoreContext.js index 31743c3a60ae..b70276616753 100644 --- a/newIDE/app/src/AssetStore/AssetStoreContext.js +++ b/newIDE/app/src/AssetStore/AssetStoreContext.js @@ -134,7 +134,6 @@ export const initialAssetStoreState: AssetStoreState = { shopNavigationState: { getCurrentPage: () => assetStoreHomePageState, isRootPage: true, - isAssetSwappingHistory: false, backToPreviousPage: () => assetStoreHomePageState, openHome: () => assetStoreHomePageState, openAssetSwapping: () => assetStoreHomePageState, diff --git a/newIDE/app/src/AssetStore/AssetStoreNavigator.js b/newIDE/app/src/AssetStore/AssetStoreNavigator.js index 3c72fa116dd6..4f97cfe68a21 100644 --- a/newIDE/app/src/AssetStore/AssetStoreNavigator.js +++ b/newIDE/app/src/AssetStore/AssetStoreNavigator.js @@ -28,7 +28,6 @@ export type AssetStorePageState = {| export type NavigationState = {| getCurrentPage: () => AssetStorePageState, isRootPage: boolean, - isAssetSwappingHistory: boolean, backToPreviousPage: () => AssetStorePageState, openHome: () => AssetStorePageState, openAssetSwapping: () => AssetStorePageState, @@ -122,7 +121,6 @@ export const useShopNavigation = (): NavigationState => { () => ({ getCurrentPage: () => previousPages[previousPages.length - 1], isRootPage: previousPages.length <= 1, - isAssetSwappingHistory: !isHomePage(previousPages[0]), backToPreviousPage: () => { if (previousPages.length > 1) { const newPreviousPages = previousPages.slice( diff --git a/newIDE/app/src/AssetStore/AssetSwappingDialog.js b/newIDE/app/src/AssetStore/AssetSwappingDialog.js index 6fa0b697a6c1..3e679b2108e3 100644 --- a/newIDE/app/src/AssetStore/AssetSwappingDialog.js +++ b/newIDE/app/src/AssetStore/AssetSwappingDialog.js @@ -1,21 +1,19 @@ // @flow -import { Trans } from '@lingui/macro'; +import { t, Trans } from '@lingui/macro'; import { I18n } from '@lingui/react'; import * as React from 'react'; import Dialog from '../UI/Dialog'; import FlatButton from '../UI/FlatButton'; import { AssetStore, type AssetStoreInterface } from '.'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; -import RaisedButton from '../UI/RaisedButton'; import { AssetStoreContext } from './AssetStoreContext'; -import Window from '../Utils/Window'; import ErrorBoundary from '../UI/ErrorBoundary'; import LoaderModal from '../UI/LoaderModal'; import { useInstallAsset } from './NewObjectDialog'; import { swapAsset } from './AssetSwapper'; import PixiResourcesLoader from '../ObjectsRendering/PixiResourcesLoader'; - -const isDev = Window.isDev(); +import useAlertDialog from '../UI/Alert/useAlertDialog'; +import RaisedButton from '../UI/RaisedButton'; type Props = {| project: gdProject, @@ -25,6 +23,8 @@ type Props = {| object: gdObject, resourceManagementProps: ResourceManagementProps, onClose: ({ swappingDone: boolean }) => void, + // Use minimal UI to hide filters & the details page (useful for Quick Customization) + minimalUI?: boolean, |}; function AssetSwappingDialog({ @@ -35,10 +35,9 @@ function AssetSwappingDialog({ object, resourceManagementProps, onClose, + minimalUI, }: Props) { - const { shopNavigationState, environment, setEnvironment } = React.useContext( - AssetStoreContext - ); + const { shopNavigationState } = React.useContext(AssetStoreContext); const { openedAssetShortHeader } = shopNavigationState.getCurrentPage(); const [ @@ -50,33 +49,44 @@ function AssetSwappingDialog({ objectsContainer, resourceManagementProps, }); + const { showAlert } = useAlertDialog(); - const onInstallOpenedAsset = React.useCallback( + const installOpenedAsset = React.useCallback( async (): Promise => { if (!openedAssetShortHeader) return; setIsAssetBeingInstalled(true); - const installAssetOutput = await installAsset(openedAssetShortHeader); - if (!installAssetOutput) { - setIsAssetBeingInstalled(false); - return; - } + try { + const installAssetOutput = await installAsset(openedAssetShortHeader); + if (!installAssetOutput) { + throw new Error('Failed to install asset'); + } - if (installAssetOutput.createdObjects.length > 0) { - swapAsset( - project, - PixiResourcesLoader, - object, - installAssetOutput.createdObjects[0], - openedAssetShortHeader - ); - } - for (const createdObject of installAssetOutput.createdObjects) { - objectsContainer.removeObject(createdObject.getName()); - } + if (installAssetOutput.createdObjects.length > 0) { + swapAsset( + project, + PixiResourcesLoader, + object, + installAssetOutput.createdObjects[0], + openedAssetShortHeader + ); + } + for (const createdObject of installAssetOutput.createdObjects) { + objectsContainer.removeObject(createdObject.getName()); + } - setIsAssetBeingInstalled(false); - onClose({ swappingDone: true }); + onClose({ swappingDone: true }); + } catch (err) { + showAlert({ + title: t`Could not swap asset`, + message: t`Something went wrong while swapping the asset. Please try again.`, + }); + console.error('Error while installing asset:', err); + } finally { + // Always go back to the previous page so the asset is unselected. + shopNavigationState.backToPreviousPage(); + setIsAssetBeingInstalled(false); + } }, [ installAsset, @@ -85,35 +95,36 @@ function AssetSwappingDialog({ objectsContainer, openedAssetShortHeader, onClose, + shopNavigationState, + showAlert, ] ); - const mainAction = openedAssetShortHeader ? ( - Adding... : Swap - } - onClick={onInstallOpenedAsset} - disabled={isAssetBeingInstalled} - id="swap-asset-button" - /> - ) : isDev ? ( - Show live assets - ) : ( - Show staging assets - ) + const mainAction = + openedAssetShortHeader && !minimalUI ? ( + Adding... : Swap + } + onClick={installOpenedAsset} + disabled={isAssetBeingInstalled} + id="swap-asset-button" + /> + ) : null; + + // Try to install the asset as soon as selected, if in minimal UI mode. + React.useEffect( + () => { + if (openedAssetShortHeader && !isAssetBeingInstalled && minimalUI) { + installOpenedAsset(); } - onClick={() => { - setEnvironment(environment === 'staging' ? 'live' : 'staging'); - }} - /> - ) : null; + }, + // Only run when the asset is selected and not already being installed. + // eslint-disable-next-line react-hooks/exhaustive-deps + [isAssetBeingInstalled, openedAssetShortHeader] + ); const assetStore = React.useRef(null); const handleClose = React.useCallback( @@ -130,27 +141,30 @@ function AssetSwappingDialog({ <> Swap {object.getName()} with another asset} - actions={[ + actions={[mainAction]} + secondaryActions={[ Back} - primary={false} onClick={handleClose} id="close-button" + fullWidth + primary />, - mainAction, ]} + onApply={minimalUI ? undefined : installOpenedAsset} onRequestClose={handleClose} - onApply={onInstallOpenedAsset} open flexBody fullHeight id="asset-swapping-dialog" + actionsFullWidthOnMobile > {isAssetBeingInstalled && } diff --git a/newIDE/app/src/AssetStore/AssetsList.js b/newIDE/app/src/AssetStore/AssetsList.js index 2e68d40175c4..667223554d0c 100644 --- a/newIDE/app/src/AssetStore/AssetsList.js +++ b/newIDE/app/src/AssetStore/AssetsList.js @@ -48,7 +48,6 @@ import Breadcrumbs from '../UI/Breadcrumbs'; import { getFolderTagsFromAssetShortHeaders } from './TagsHelper'; import { PrivateGameTemplateStoreContext } from './PrivateGameTemplates/PrivateGameTemplateStoreContext'; import { type AssetStorePageState } from './AssetStoreNavigator'; -import RaisedButton from '../UI/RaisedButton'; import FlatButton from '../UI/FlatButton'; import HelpIcon from '../UI/HelpIcon'; import { OwnedProductLicense } from './ProductLicense/ProductLicenseOptions'; @@ -202,9 +201,8 @@ const PageBreakNavigation = ({ }} disabled={pageBreakIndex <= 0} /> - Show next assets} onClick={() => { currentPage.pageBreakIndex = (currentPage.pageBreakIndex || 0) + 1; diff --git a/newIDE/app/src/AssetStore/ShopTiles.js b/newIDE/app/src/AssetStore/ShopTiles.js index eba9eff670c0..c7e4e373da1f 100644 --- a/newIDE/app/src/AssetStore/ShopTiles.js +++ b/newIDE/app/src/AssetStore/ShopTiles.js @@ -522,13 +522,24 @@ export const ExampleTile = ({ onSelect, style, customTitle, + useQuickCustomizationThumbnail, }: {| exampleShortHeader: ExampleShortHeader | null, onSelect: () => void, /** Props needed so that GridList component can adjust tile size */ style?: any, customTitle?: string, + useQuickCustomizationThumbnail?: boolean, |}) => { + const thumbnailImgUrl = exampleShortHeader + ? useQuickCustomizationThumbnail + ? exampleShortHeader.quickCustomizationImageUrl + ? exampleShortHeader.quickCustomizationImageUrl + : exampleShortHeader.previewImageUrls + ? exampleShortHeader.previewImageUrls[0] + : '' + : '' + : ''; const classesForGridListItem = useStylesForGridListItem(); return ( ) : ( diff --git a/newIDE/app/src/AssetStore/index.js b/newIDE/app/src/AssetStore/index.js index dd408354ac9b..cacd0040a61c 100644 --- a/newIDE/app/src/AssetStore/index.js +++ b/newIDE/app/src/AssetStore/index.js @@ -65,6 +65,7 @@ type Props = {| ) => void, onOpenProfile?: () => void, assetSwappedObject?: ?gdObject, + minimalUI?: boolean, |}; export type AssetStoreInterface = {| @@ -104,6 +105,7 @@ export const AssetStore = React.forwardRef( onOpenPrivateGameTemplateListingData, onOpenProfile, assetSwappedObject, + minimalUI, }: Props, ref ) => { @@ -147,11 +149,6 @@ export const AssetStore = React.forwardRef( } } assetSwappedObjectPtr.current = assetSwappedObject.ptr; - } else if (shopNavigationState.isAssetSwappingHistory) { - shopNavigationState.openHome(); - assetFiltersState.setAssetSwappingFilter( - new AssetSwappingAssetStoreSearchFilter() - ); } }, [ @@ -600,129 +597,138 @@ export const AssetStore = React.forwardRef( return ( - - { - setSearchText(''); - const page = assetSwappedObject - ? shopNavigationState.openAssetSwapping() - : shopNavigationState.openHome(); - setScrollUpdateIsNeeded(page); - clearAllAssetStoreFilters(); - setIsFiltersPanelOpen(false); - }} - size="small" - > - - - - { - if (searchText === newValue) { - return; + <> + + {!(assetSwappedObject && minimalUI) && ( + { + setSearchText(''); + const page = assetSwappedObject + ? shopNavigationState.openAssetSwapping() + : shopNavigationState.openHome(); + setScrollUpdateIsNeeded(page); + clearAllAssetStoreFilters(); + setIsFiltersPanelOpen(false); + }} + size="small" + > + + + )} + + { + if (searchText === newValue) { + return; } - } else { - // A new search is being initiated: navigate to the search page, - // and clear the history as a new search was launched. - if (!!newValue) { - shopNavigationState.clearHistory(); + setSearchText(newValue); + if (isOnSearchResultPage) { + // An existing search is already being done: just move to the + // top search results. shopNavigationState.openSearchResultPage(); - openFiltersPanelIfAppropriate(); + const assetsListInterface = assetsList.current; + if (assetsListInterface) { + assetsListInterface.scrollToPosition(0); + assetsListInterface.setPageBreakIndex(0); + } + } else { + // A new search is being initiated: navigate to the search page, + // and clear the history as a new search was launched. + if (!!newValue) { + shopNavigationState.clearHistory(); + shopNavigationState.openSearchResultPage(); + openFiltersPanelIfAppropriate(); + } } - } - }} - onRequestSearch={() => {}} - ref={searchBar} - id="asset-store-search-bar" - /> - - setIsFiltersPanelOpen(!isFiltersPanelOpen)} - disabled={!canShowFiltersPanel} - selected={canShowFiltersPanel && isFiltersPanelOpen} - size="small" - > - - - - + }} + onRequestSearch={() => {}} + ref={searchBar} + id="asset-store-search-bar" + /> + + {!(assetSwappedObject && minimalUI) && ( + setIsFiltersPanelOpen(!isFiltersPanelOpen)} + disabled={!canShowFiltersPanel} + selected={canShowFiltersPanel && isFiltersPanelOpen} + size="small" + > + + + )} + + + - {(!isOnHomePage || !!openedShopCategory) && ( - <> - {shopNavigationState.isRootPage ? null : ( - - } - label={Back} - onClick={async () => { - const page = shopNavigationState.backToPreviousPage(); - const isUpdatingSearchtext = reApplySearchTextIfNeeded( - page - ); - if (isUpdatingSearchtext) { - // Updating the search is not instant, so we cannot apply the scroll position - // right away. We force a wait as there's no easy way to know when results are completely updated. - await delay(500); - setScrollUpdateIsNeeded(page); - applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again. - } else { - setScrollUpdateIsNeeded(page); - } - }} - /> - - )} - {(openedAssetPack || - openedPrivateAssetPackListingData || - filtersState.chosenCategory) && ( - <> - {!openedAssetPack && !openedPrivateAssetPackListingData && ( - // Only show the category name if we're not on an asset pack page. - - - {filtersState.chosenCategory - ? capitalize(filtersState.chosenCategory.node.name) - : ''} - - - )} - - {openedAssetPack && - openedAssetPack.content && - doesAssetPackContainAudio(openedAssetPack) && - !isAssetPackAudioOnly(openedAssetPack) ? ( - - ) : null} + {(!isOnHomePage || !!openedShopCategory) && + !(assetSwappedObject && minimalUI) && ( + <> + {shopNavigationState.isRootPage ? null : ( + + } + label={Back} + onClick={async () => { + const page = shopNavigationState.backToPreviousPage(); + const isUpdatingSearchtext = reApplySearchTextIfNeeded( + page + ); + if (isUpdatingSearchtext) { + // Updating the search is not instant, so we cannot apply the scroll position + // right away. We force a wait as there's no easy way to know when results are completely updated. + await delay(500); + setScrollUpdateIsNeeded(page); + applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again. + } else { + setScrollUpdateIsNeeded(page); + } + }} + /> - - )} - - )} + )} + {(openedAssetPack || + openedPrivateAssetPackListingData || + filtersState.chosenCategory) && ( + <> + {!openedAssetPack && !openedPrivateAssetPackListingData && ( + // Only show the category name if we're not on an asset pack page. + + + {filtersState.chosenCategory + ? capitalize( + filtersState.chosenCategory.node.name + ) + : ''} + + + )} + + {openedAssetPack && + openedAssetPack.content && + doesAssetPackContainAudio(openedAssetPack) && + !isAssetPackAudioOnly(openedAssetPack) ? ( + + ) : null} + + + )} + + )} ( onGoBackToFolderIndex={goBackToFolderIndex} currentPage={shopNavigationState.getCurrentPage()} hideGameTemplates={hideGameTemplates} - hideDetails={!!assetSwappedObject} + hideDetails={!!assetSwappedObject && !!minimalUI} /> - ) : openedAssetShortHeader ? ( + ) : // Do not show the asset details if we're swapping an asset. + openedAssetShortHeader && !(assetSwappedObject && minimalUI) ? ( Promise, openExtension: (behaviorType: string) => void, + openBehaviorPropertiesQuickCustomizationDialog: ( + behaviorName: string + ) => void, |}; const BehaviorConfigurationEditor = React.forwardRef< @@ -95,6 +99,7 @@ const BehaviorConfigurationEditor = React.forwardRef< canPasteBehaviors, pasteBehaviors, openExtension, + openBehaviorPropertiesQuickCustomizationDialog, }, ref ) => { @@ -209,6 +214,18 @@ const BehaviorConfigurationEditor = React.forwardRef< }, ] : []), + ...(!Window.isDev() + ? [] + : [ + { type: 'separator' }, + { + label: i18n._(t`Quick Customization settings`), + click: () => + openBehaviorPropertiesQuickCustomizationDialog( + behaviorName + ), + }, + ]), ]} />, ]} @@ -608,6 +625,11 @@ const BehaviorsEditor = (props: Props) => { } = props; const forceUpdate = useForceUpdate(); + const [ + selectedQuickCustomizationPropertiesBehavior, + setSelectedQuickCustomizationPropertiesBehavior, + ] = React.useState(null); + const { changeBehaviorName, removeBehavior, @@ -671,6 +693,16 @@ const BehaviorsEditor = (props: Props) => { [openBehaviorEvents, project] ); + const openBehaviorPropertiesQuickCustomizationDialog = React.useCallback( + (behaviorName: string) => { + if (!object.hasBehaviorNamed(behaviorName)) return; + const behavior = object.getBehavior(behaviorName); + + setSelectedQuickCustomizationPropertiesBehavior(behavior); + }, + [object] + ); + const isClipboardContainingBehaviors = Clipboard.has( BEHAVIORS_CLIPBOARD_KIND ); @@ -725,6 +757,9 @@ const BehaviorsEditor = (props: Props) => { onBehaviorsUpdated={onBehaviorsUpdated} onChangeBehaviorName={changeBehaviorName} openExtension={openExtension} + openBehaviorPropertiesQuickCustomizationDialog={ + openBehaviorPropertiesQuickCustomizationDialog + } canPasteBehaviors={isClipboardContainingBehaviors} pasteBehaviors={pasteBehaviors} resourceManagementProps={props.resourceManagementProps} @@ -780,6 +815,17 @@ const BehaviorsEditor = (props: Props) => { )} {newBehaviorDialog} + {!!selectedQuickCustomizationPropertiesBehavior && ( + setSelectedQuickCustomizationPropertiesBehavior(null)} + propertyNames={selectedQuickCustomizationPropertiesBehavior + .getProperties() + .keys() + .toJSArray()} + propertiesQuickCustomizationVisibilities={selectedQuickCustomizationPropertiesBehavior.getPropertiesQuickCustomizationVisibilities()} + /> + )} ); }; diff --git a/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js b/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js index 279461f597ce..a3f72ee80b2a 100644 --- a/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js +++ b/newIDE/app/src/CompactPropertiesEditor/PropertiesMapToCompactSchema.js @@ -165,13 +165,10 @@ const createField = ( const extraInfos = property.getExtraInfo().toJSArray(); // $FlowFixMe - assume the passed resource kind is always valid. const kind: ResourceKind = extraInfos[0] || ''; - // $FlowFixMe - assume the passed resource kind is always valid. - const fallbackKind: ResourceKind = extraInfos[1] || ''; return { name, valueType: 'resource', resourceKind: kind, - fallbackResourceKind: fallbackKind, getValue: (instance: Instance): string => { return getProperties(instance) .get(name) @@ -257,11 +254,17 @@ const uncapitalize = str => { * @param visibility `true` when only deprecated properties must be displayed * and `false` when only not deprecated ones must be displayed */ -const isPropertyVisible = ( +const isPropertyVisible = ({ + properties, + name, + visibility, + quickCustomizationVisibilities, +}: { properties: gdMapStringPropertyDescriptor, name: string, - visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated' | 'Basic-Quick' -): boolean => { + visibility: 'All' | 'Basic' | 'Advanced' | 'Deprecated' | 'Basic-Quick', + quickCustomizationVisibilities?: gdQuickCustomizationVisibilitiesContainer, +}): boolean => { if (!properties.has(name)) { return false; } @@ -286,7 +289,7 @@ const isPropertyVisible = ( if (property.isDeprecated()) return false; if (property.isAdvanced()) return false; - // Honor visibility if set: + // Honor visibility if set on the property. if ( property.getQuickCustomizationVisibility() === gd.QuickCustomization.Hidden @@ -298,6 +301,13 @@ const isPropertyVisible = ( ) return true; + // Honor visibility if set on the container. + if (quickCustomizationVisibilities) { + const visibility = quickCustomizationVisibilities.get(name); + if (visibility === gd.QuickCustomization.Hidden) return false; + if (visibility === gd.QuickCustomization.Visible) return true; + } + // Otherwise, hide some properties that we know are complex. const propertyType = property.getType(); if (propertyType === 'Behavior') return false; // Hide "required behaviors". @@ -315,7 +325,14 @@ const isPropertyVisible = ( * @param getProperties The function called to read again the properties * @param onUpdateProperty The function called to update a property of an object */ -const propertiesMapToSchema = ( +const propertiesMapToSchema = ({ + properties, + getProperties, + onUpdateProperty, + object, + visibility = 'All', + quickCustomizationVisibilities, +}: { properties: gdMapStringPropertyDescriptor, getProperties: (instance: Instance) => any, onUpdateProperty: ( @@ -323,14 +340,10 @@ const propertiesMapToSchema = ( propertyName: string, newValue: string ) => void, - object: ?gdObject, - visibility: - | 'All' - | 'Basic' - | 'Advanced' - | 'Deprecated' - | 'Basic-Quick' = 'All' -): Schema => { + object?: gdObject, + visibility?: 'All' | 'Basic' | 'Advanced' | 'Deprecated' | 'Basic-Quick', + quickCustomizationVisibilities?: gdQuickCustomizationVisibilitiesContainer, +}): Schema => { const propertyNames = properties.keys(); // Aggregate field by groups to be able to build field groups with a title. const fieldsByGroups = new Map>(); @@ -338,7 +351,14 @@ const propertiesMapToSchema = ( mapFor(0, propertyNames.size(), i => { const name = propertyNames.at(i); const property = properties.get(name); - if (!isPropertyVisible(properties, name, visibility)) { + if ( + !isPropertyVisible({ + properties, + name, + visibility, + quickCustomizationVisibilities, + }) + ) { return null; } if (alreadyHandledProperties.has(name)) return null; @@ -362,7 +382,14 @@ const propertiesMapToSchema = ( name.replace(keyword, otherKeyword) ); for (const rowPropertyName of rowAllPropertyNames) { - if (isPropertyVisible(properties, rowPropertyName, visibility)) { + if ( + isPropertyVisible({ + properties, + name: rowPropertyName, + visibility, + quickCustomizationVisibilities, + }) + ) { rowPropertyNames.push(rowPropertyName); } } @@ -373,7 +400,14 @@ const propertiesMapToSchema = ( name.replace(uncapitalizeKeyword, uncapitalize(otherKeyword)) ); for (const rowPropertyName of rowAllPropertyNames) { - if (isPropertyVisible(properties, rowPropertyName, visibility)) { + if ( + isPropertyVisible({ + properties, + name: rowPropertyName, + visibility, + quickCustomizationVisibilities, + }) + ) { rowPropertyNames.push(rowPropertyName); } } diff --git a/newIDE/app/src/CompactPropertiesEditor/index.js b/newIDE/app/src/CompactPropertiesEditor/index.js index 9e67e7955173..70ddfccf2b02 100644 --- a/newIDE/app/src/CompactPropertiesEditor/index.js +++ b/newIDE/app/src/CompactPropertiesEditor/index.js @@ -116,7 +116,6 @@ export type PrimitiveValueField = export type ResourceField = {| valueType: 'resource', resourceKind: ResourceKind, - fallbackResourceKind?: ResourceKind, getValue: Instance => string, setValue: (instance: Instance, newValue: string) => void, renderLeftIcon?: (className?: string) => React.Node, @@ -231,8 +230,6 @@ const styles = { }, container: { flex: 1, minWidth: 0 }, separator: { - marginRight: -marginsSize, - marginLeft: -marginsSize, marginTop: marginsSize, borderTop: '1px solid black', // Border color is changed in the component. }, @@ -420,21 +417,26 @@ const CompactPropertiesEditor = ({ const { setValue } = field; return ( - { - instances.forEach(i => setValue(i, newValue)); - onFieldChanged({ - instances, - hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, - }); - }} - disabled={getDisabled({ instances, field })} - fullWidth + field={ + { + instances.forEach(i => setValue(i, newValue)); + onFieldChanged({ + instances, + hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, + }); + }} + disabled={getDisabled({ instances, field })} + fullWidth + /> + } /> ); } else if (field.valueType === 'number') { @@ -782,26 +784,29 @@ const CompactPropertiesEditor = ({ const { setValue } = field; return ( - { - instances.forEach(i => setValue(i, newValue)); - onFieldChanged({ - instances, - hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, - }); - }} label={getFieldLabel({ instances, field })} markdownDescription={getFieldDescription(field)} + field={ + { + instances.forEach(i => setValue(i, newValue)); + onFieldChanged({ + instances, + hasImpactOnAllOtherFields: field.hasImpactOnAllOtherFields, + }); + }} + /> + } /> ); }; diff --git a/newIDE/app/src/EffectsList/index.js b/newIDE/app/src/EffectsList/index.js index 7d21a45ea409..2d5cadb98475 100644 --- a/newIDE/app/src/EffectsList/index.js +++ b/newIDE/app/src/EffectsList/index.js @@ -296,6 +296,7 @@ const Effect = React.forwardRef( {({ i18n }: { i18n: I18nType }) => ( - + Name} value={eventsFunctionsExtension.getName()} diff --git a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js index e3e4a4a10b76..a495edc638c6 100644 --- a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js +++ b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineGameLink.js @@ -46,6 +46,7 @@ type OnlineGameLinkProps = {| exportStep: BuildStep, onRefreshGame: () => Promise, automaticallyOpenGameProperties?: boolean, + shouldShowShareDialog: boolean, |}; const timeForExport = 5; // seconds. @@ -60,6 +61,7 @@ const OnlineGameLink = ({ exportStep, onRefreshGame, automaticallyOpenGameProperties, + shouldShowShareDialog, }: OnlineGameLinkProps) => { const [showCopiedInfoBar, setShowCopiedInfoBar] = React.useState( false @@ -177,10 +179,12 @@ const OnlineGameLink = ({ () => { if (exportStep === 'done') { setTimeBeforeExportFinished(timeForExport); // reset. - setIsShareDialogOpen(true); + if (shouldShowShareDialog) { + setIsShareDialogOpen(true); + } } }, - [exportStep, automaticallyOpenGameProperties] + [exportStep, automaticallyOpenGameProperties, shouldShowShareDialog] ); React.useEffect( diff --git a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js index 5eff2421714d..cfb4da4214ab 100644 --- a/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js +++ b/newIDE/app/src/ExportAndShare/GenericExporters/OnlineWebExport/OnlineWebExportFlow.js @@ -105,6 +105,7 @@ const OnlineWebExportFlow = ({ automaticallyOpenGameProperties={automaticallyOpenGameProperties} onRefreshGame={refreshGame} game={game} + shouldShowShareDialog={uiMode === 'full'} /> ); diff --git a/newIDE/app/src/GameDashboard/ShareGameDialog.js b/newIDE/app/src/GameDashboard/ShareGameDialog.js index 19f04b614811..5f1cb2fe8801 100644 --- a/newIDE/app/src/GameDashboard/ShareGameDialog.js +++ b/newIDE/app/src/GameDashboard/ShareGameDialog.js @@ -14,7 +14,7 @@ import ShareButton from '../UI/ShareDialog/ShareButton'; type Props = {| game: Game, onClose: () => void |}; -const ShareDialog = ({ game, onClose }: Props) => { +const ShareGameDialog = ({ game, onClose }: Props) => { const [showAlertMessage, setShowAlertMessage] = React.useState(false); const gameUrl = getGameUrl(game); @@ -55,4 +55,4 @@ const ShareDialog = ({ game, onClose }: Props) => { ); }; -export default ShareDialog; +export default ShareGameDialog; diff --git a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js index f50640a58c74..dada3748a456 100644 --- a/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/CompactInstancePropertiesEditor/index.js @@ -13,7 +13,7 @@ import IconButton from '../../UI/IconButton'; import { Line, Column, Spacer, marginsSize } from '../../UI/Grid'; import Text from '../../UI/Text'; import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext'; -import ScrollView from '../../UI/ScrollView'; +import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView'; import EventsRootVariablesFinder from '../../Utils/EventsRootVariablesFinder'; import VariablesList, { type HistoryHandler, @@ -87,6 +87,7 @@ export const CompactInstancePropertiesEditor = ({ const forceUpdate = useForceUpdate(); const variablesListRef = React.useRef(null); + const scrollViewRef = React.useRef(null); const instance = instances[0]; /** * TODO: multiple instances support for variables list. Expected behavior should be: @@ -96,6 +97,12 @@ export const CompactInstancePropertiesEditor = ({ */ const shouldDisplayVariablesList = instances.length === 1; + const onScrollY = React.useCallback(deltaY => { + if (scrollViewRef.current) { + scrollViewRef.current.scrollBy(deltaY); + } + }, []); + const { object, instanceSchema } = React.useMemo<{| object?: gdObject, instanceSchema?: Schema, @@ -129,21 +136,21 @@ export const CompactInstancePropertiesEditor = ({ const canBeFlippedZ = objectMetadata.hasDefaultBehavior( 'Scene3D::Base3DBehavior' ); - const instanceSchemaForCustomProperties = propertiesMapToSchema( + const instanceSchemaForCustomProperties = propertiesMapToSchema({ properties, - (instance: gdInitialInstance) => + getProperties: (instance: gdInitialInstance) => instance.getCustomProperties( globalObjectsContainer || objectsContainer, objectsContainer ), - (instance: gdInitialInstance, name, value) => + onUpdateProperty: (instance: gdInitialInstance, name, value) => instance.updateCustomProperty( name, value, globalObjectsContainer || objectsContainer, objectsContainer - ) - ); + ), + }); const reorderedInstanceSchemaForCustomProperties = reorderInstanceSchemaForCustomProperties( instanceSchemaForCustomProperties, @@ -211,6 +218,7 @@ export const CompactInstancePropertiesEditor = ({ scope="scene-editor-instance-properties" > diff --git a/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js b/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js index 56b530349870..a8e4edd3820c 100644 --- a/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js +++ b/newIDE/app/src/InstancesEditor/TileMapPaintingPreview.js @@ -9,6 +9,12 @@ import RenderedInstance from '../ObjectsRendering/Renderers/RenderedInstance'; import Rendered3DInstance from '../ObjectsRendering/Renderers/Rendered3DInstance'; import { type TileMapTileSelection } from './TileSetVisualizer'; import { AffineTransformation } from '../Utils/AffineTransformation'; +import { + getTileSet, + getTilesGridCoordinatesFromPointerSceneCoordinates, + isTileSetBadlyConfigured, + type TileSet, +} from '../Utils/TileMap'; export const updateSceneToTileMapTransformation = ( instance: gdInitialInstance, @@ -62,136 +68,6 @@ export const updateSceneToTileMapTransformation = ( return { scaleX, scaleY }; }; -export const getTileSet = (object: gdObject) => { - const objectConfigurationProperties = object - .getConfiguration() - .getProperties(); - const columnCount = parseFloat( - objectConfigurationProperties.get('columnCount').getValue() - ); - const rowCount = parseFloat( - objectConfigurationProperties.get('rowCount').getValue() - ); - const tileSize = parseFloat( - objectConfigurationProperties.get('tileSize').getValue() - ); - const atlasImage = objectConfigurationProperties.get('atlasImage').getValue(); - return { rowCount, columnCount, tileSize, atlasImage }; -}; - -export const isTileSetBadlyConfigured = ({ - rowCount, - columnCount, - tileSize, - atlasImage, -}: {| - rowCount: number, - columnCount: number, - tileSize: number, - atlasImage: string, -|}) => { - return ( - !Number.isInteger(columnCount) || - columnCount <= 0 || - !Number.isInteger(rowCount) || - rowCount <= 0 - ); -}; - -/** - * Returns the list of tiles corresponding to the user selection. - * If only one coordinate is present, only one tile is placed at the slot the - * pointer points to. - * If two coordinates are present, tiles are displayed to form a rectangle with the - * two coordinates being the top left and bottom right corner of the rectangle. - */ -export const getTilesGridCoordinatesFromPointerSceneCoordinates = ({ - coordinates, - tileSize, - sceneToTileMapTransformation, -}: {| - coordinates: Array<{| x: number, y: number |}>, - tileSize: number, - sceneToTileMapTransformation: AffineTransformation, -|}): Array<{| x: number, y: number |}> => { - if (coordinates.length === 0) return []; - - const tilesCoordinatesInTileMapGrid = []; - - if (coordinates.length === 1) { - const coordinatesInTileMapGrid = [0, 0]; - sceneToTileMapTransformation.transform( - [coordinates[0].x, coordinates[0].y], - coordinatesInTileMapGrid - ); - coordinatesInTileMapGrid[0] = Math.floor( - coordinatesInTileMapGrid[0] / tileSize - ); - coordinatesInTileMapGrid[1] = Math.floor( - coordinatesInTileMapGrid[1] / tileSize - ); - tilesCoordinatesInTileMapGrid.push({ - x: coordinatesInTileMapGrid[0], - y: coordinatesInTileMapGrid[1], - }); - } - if (coordinates.length === 2) { - const firstPointCoordinatesInTileMap = [0, 0]; - sceneToTileMapTransformation.transform( - [coordinates[0].x, coordinates[0].y], - firstPointCoordinatesInTileMap - ); - const secondPointCoordinatesInTileMap = [0, 0]; - sceneToTileMapTransformation.transform( - [coordinates[1].x, coordinates[1].y], - secondPointCoordinatesInTileMap - ); - const topLeftCornerCoordinatesInTileMap = [ - Math.min( - firstPointCoordinatesInTileMap[0], - secondPointCoordinatesInTileMap[0] - ), - Math.min( - firstPointCoordinatesInTileMap[1], - secondPointCoordinatesInTileMap[1] - ), - ]; - const bottomRightCornerCoordinatesInTileMap = [ - Math.max( - firstPointCoordinatesInTileMap[0], - secondPointCoordinatesInTileMap[0] - ), - Math.max( - firstPointCoordinatesInTileMap[1], - secondPointCoordinatesInTileMap[1] - ), - ]; - const topLeftCornerCoordinatesInTileMapGrid = [ - Math.floor(topLeftCornerCoordinatesInTileMap[0] / tileSize), - Math.floor(topLeftCornerCoordinatesInTileMap[1] / tileSize), - ]; - const bottomRightCornerCoordinatesInTileMapGrid = [ - Math.floor(bottomRightCornerCoordinatesInTileMap[0] / tileSize), - Math.floor(bottomRightCornerCoordinatesInTileMap[1] / tileSize), - ]; - - for ( - let columnIndex = topLeftCornerCoordinatesInTileMapGrid[0]; - columnIndex <= bottomRightCornerCoordinatesInTileMapGrid[0]; - columnIndex++ - ) { - for ( - let rowIndex = topLeftCornerCoordinatesInTileMapGrid[1]; - rowIndex <= bottomRightCornerCoordinatesInTileMapGrid[1]; - rowIndex++ - ) { - tilesCoordinatesInTileMapGrid.push({ x: columnIndex, y: rowIndex }); - } - } - } - return tilesCoordinatesInTileMapGrid; -}; - type Props = {| project: gdProject, layout: gdLayout | null, @@ -249,72 +125,100 @@ class TileMapPaintingPreview { return this.preview; } - render() { - this.preview.removeChildren(0); - const tileMapTileSelection = this.getTileMapTileSelection(); - if (!tileMapTileSelection) { - return; - } - const selection = this.instancesSelection.getSelectedInstances(); - if (selection.length !== 1) return; - const instance = selection[0]; - const associatedObjectName = instance.getObjectName(); - const object = getObjectByName( - this.project.getObjects(), - this.layout ? this.layout.getObjects() : null, - associatedObjectName + _getTextureInAtlas({ + tileSet, + x, + y, + }: { + tileSet: TileSet, + x: number, + y: number, + }): ?PIXI.Texture { + const { atlasImage, tileSize } = tileSet; + if (!atlasImage) return; + const cacheKey = `${atlasImage}-${tileSize}-${x}-${y}`; + const cachedTexture = this.cache.get(cacheKey); + if (cachedTexture) return cachedTexture; + + const atlasTexture = PixiResourcesLoader.getPIXITexture( + this.project, + atlasImage ); - if (!object || object.getType() !== 'TileMap::SimpleTileMap') return; - const tileSet = getTileSet(object); - const isBadlyConfigured = isTileSetBadlyConfigured(tileSet); - const { tileSize } = tileSet; - let texture; - if (isBadlyConfigured) { - texture = PixiResourcesLoader.getInvalidPIXITexture(); - } else { - if (tileMapTileSelection.kind === 'single') { - const atlasResourceName = object - .getConfiguration() - .getProperties() - .get('atlasImage') - .getValue(); - if (!atlasResourceName) return; - const cacheKey = `${atlasResourceName}-${tileSize}-${ - tileMapTileSelection.coordinates.x - }-${tileMapTileSelection.coordinates.y}`; - texture = this.cache.get(cacheKey); - if (!texture) { - const atlasTexture = PixiResourcesLoader.getPIXITexture( - this.project, - atlasResourceName - ); - const rect = new PIXI.Rectangle( - tileMapTileSelection.coordinates.x * tileSize, - tileMapTileSelection.coordinates.y * tileSize, - tileSize, - tileSize - ); + const rect = new PIXI.Rectangle( + x * tileSize, + y * tileSize, + tileSize, + tileSize + ); - try { - texture = new PIXI.Texture(atlasTexture, rect); - } catch (error) { - console.error( - `Tile could not be extracted from atlas texture:`, - error - ); - texture = PixiResourcesLoader.getInvalidPIXITexture(); - } - this.cache.set(cacheKey, texture); - } - } else if (tileMapTileSelection.kind === 'erase') { - texture = PIXI.Texture.from( - 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAARSURBVHgBY7h58+Z/BhgAcQA/VAcVLiw46wAAAABJRU5ErkJggg==' - ); - texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST; - } + try { + const texture = new PIXI.Texture(atlasTexture, rect); + this.cache.set(cacheKey, texture); + } catch (error) { + console.error(`Tile could not be extracted from atlas texture:`, error); + return PixiResourcesLoader.getInvalidPIXITexture(); } + } + + _getTilingSpriteForRectangle({ + bottomRightCorner, + topLeftCorner, + texture, + scaleX, + scaleY, + flipHorizontally, + flipVertically, + tileSize, + angle, + }: {| + bottomRightCorner: {| x: number, y: number |}, + topLeftCorner: {| x: number, y: number |}, + scaleX: number, + scaleY: number, + tileSize: number, + flipHorizontally: boolean, + flipVertically: boolean, + angle: number, + texture: PIXI.Texture, + |}) { + const sprite = new PIXI.TilingSprite(texture); + const workingPoint = [0, 0]; + sprite.tileScale.x = + (flipHorizontally ? -1 : +1) * this.viewPosition.toCanvasScale(scaleX); + sprite.tileScale.y = + (flipVertically ? -1 : +1) * this.viewPosition.toCanvasScale(scaleY); + + this.tileMapToSceneTransformation.transform( + [topLeftCorner.x * tileSize, topLeftCorner.y * tileSize], + workingPoint + ); + const tileSizeInCanvas = this.viewPosition.toCanvasScale(tileSize); + + sprite.x = this.viewPosition.toCanvasScale(workingPoint[0]); + sprite.y = this.viewPosition.toCanvasScale(workingPoint[1]); + sprite.width = + (bottomRightCorner.x - topLeftCorner.x + 1) * tileSizeInCanvas * scaleX; + sprite.height = + (bottomRightCorner.y - topLeftCorner.y + 1) * tileSizeInCanvas * scaleY; + + sprite.angle = angle; + + return sprite; + } + + _getPreviewSprites({ + instance, + tileSet, + isBadlyConfigured, + tileMapTileSelection, + }: { + instance: gdInitialInstance, + tileSet: TileSet, + isBadlyConfigured: boolean, + tileMapTileSelection: TileMapTileSelection, + }): ?PIXI.Container { const renderedInstance = this.getRendererOfInstance(instance); if ( !renderedInstance || @@ -324,7 +228,7 @@ class TileMapPaintingPreview { console.error( `Instance of ${instance.getObjectName()} seems to not be a RenderedSimpleTileMapInstance (method getEditableTileMap does not exist).` ); - return; + return null; } const scales = updateSceneToTileMapTransformation( @@ -334,65 +238,97 @@ class TileMapPaintingPreview { this.sceneToTileMapTransformation, this.tileMapToSceneTransformation ); - if (!scales) return; + if (!scales) return null; const { scaleX, scaleY } = scales; const coordinates = this.getCoordinatesToRender(); - if (coordinates.length === 0) return; - const tileSizeInCanvas = this.viewPosition.toCanvasScale(tileSize); - const spriteWidth = tileSizeInCanvas * scaleX; - const spriteHeight = tileSizeInCanvas * scaleY; + if (coordinates.length === 0) return null; + const { tileSize } = tileSet; - const spritesCoordinatesInTileMapGrid = getTilesGridCoordinatesFromPointerSceneCoordinates( + const tilesCoordinatesInTileMapGrid = getTilesGridCoordinatesFromPointerSceneCoordinates( { + tileMapTileSelection, coordinates, tileSize, sceneToTileMapTransformation: this.sceneToTileMapTransformation, } ); - if (spritesCoordinatesInTileMapGrid.length === 0) { + if (tilesCoordinatesInTileMapGrid.length === 0) { console.warn("Could't get coordinates to render in tile map grid."); - return; + return null; } + const container = new PIXI.Container(); + tilesCoordinatesInTileMapGrid.forEach(tilesCoordinates => { + const { + bottomRightCorner, + topLeftCorner, + tileCoordinates, + } = tilesCoordinates; + let texture; + if (isBadlyConfigured) { + texture = PixiResourcesLoader.getInvalidPIXITexture(); + } else { + if (tileMapTileSelection.kind === 'rectangle' && tileCoordinates) { + texture = this._getTextureInAtlas({ + tileSet, + ...tileCoordinates, + }); + if (!texture) return null; + } else if (tileMapTileSelection.kind === 'erase') { + texture = PIXI.Texture.from( + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAIAAAACCAYAAABytg0kAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAARSURBVHgBY7h58+Z/BhgAcQA/VAcVLiw46wAAAABJRU5ErkJggg==' + ); + texture.baseTexture.scaleMode = PIXI.SCALE_MODES.NEAREST; + } + } + const sprite = this._getTilingSpriteForRectangle({ + bottomRightCorner, + topLeftCorner, + texture, + scaleX, + scaleY, + flipHorizontally: tileMapTileSelection.flipHorizontally || false, + flipVertically: tileMapTileSelection.flipVertically || false, + tileSize, + angle: instance.getAngle(), + }); + container.addChild(sprite); + }); - const workingPoint = [0, 0]; - - const sprite = new PIXI.TilingSprite(texture); - - sprite.tileScale.x = - (tileMapTileSelection.flipHorizontally ? -1 : +1) * - this.viewPosition.toCanvasScale(scaleX); - sprite.tileScale.y = - (tileMapTileSelection.flipVertically ? -1 : +1) * - this.viewPosition.toCanvasScale(scaleY); - sprite.width = spriteWidth; - sprite.height = spriteHeight; + return container; + } - let minX = Infinity; - let minY = Infinity; - let maxX = -Infinity; - let maxY = -Infinity; - for (const { x, y } of spritesCoordinatesInTileMapGrid) { - if (x < minX) minX = x; - if (y < minY) minY = y; - if (x > maxX) maxX = x; - if (y > maxY) maxY = y; + render() { + this.preview.removeChildren(0); + const tileMapTileSelection = this.getTileMapTileSelection(); + if (!tileMapTileSelection) { + return; } - - this.tileMapToSceneTransformation.transform( - [minX * tileSize, minY * tileSize], - workingPoint + const selection = this.instancesSelection.getSelectedInstances(); + if (selection.length !== 1) return; + const instance = selection[0]; + const associatedObjectName = instance.getObjectName(); + const object = getObjectByName( + this.project.getObjects(), + this.layout ? this.layout.getObjects() : null, + associatedObjectName ); + if (!object || object.getType() !== 'TileMap::SimpleTileMap') return; + const tileSet = getTileSet(object); + const isBadlyConfigured = isTileSetBadlyConfigured(tileSet); - sprite.x = this.viewPosition.toCanvasScale(workingPoint[0]); - sprite.y = this.viewPosition.toCanvasScale(workingPoint[1]); - sprite.width = - (maxX - minX + 1) * this.viewPosition.toCanvasScale(tileSize) * scaleX; - sprite.height = - (maxY - minY + 1) * this.viewPosition.toCanvasScale(tileSize) * scaleY; - - sprite.angle = instance.getAngle(); - - this.preview.addChild(sprite); + if ( + isBadlyConfigured || + tileMapTileSelection.kind === 'rectangle' || + tileMapTileSelection.kind === 'erase' + ) { + const container = this._getPreviewSprites({ + instance, + tileSet, + tileMapTileSelection, + isBadlyConfigured, + }); + if (container) this.preview.addChild(container); + } const canvasCoordinates = this.viewPosition.toCanvasCoordinates(0, 0); this.preview.position.x = canvasCoordinates[0]; diff --git a/newIDE/app/src/InstancesEditor/TileSetVisualizer.js b/newIDE/app/src/InstancesEditor/TileSetVisualizer.js index ee98eb1913a1..1da3f02bcfb0 100644 --- a/newIDE/app/src/InstancesEditor/TileSetVisualizer.js +++ b/newIDE/app/src/InstancesEditor/TileSetVisualizer.js @@ -16,7 +16,7 @@ import useForceUpdate from '../Utils/UseForceUpdate'; import { useLongTouch, type ClientCoordinates } from '../Utils/UseLongTouch'; import Text from '../UI/Text'; import EmptyMessage from '../UI/EmptyMessage'; -import { isTileSetBadlyConfigured } from './TileMapPaintingPreview'; +import { isTileSetBadlyConfigured } from '../Utils/TileMap'; const styles = { tilesetAndTooltipsContainer: { @@ -30,6 +30,7 @@ const styles = { position: 'relative', display: 'flex', overflow: 'auto', + touchAction: 'none', }, atlasImage: { flex: 1, imageRendering: 'pixelated' }, icon: { fontSize: 18 }, @@ -64,12 +65,12 @@ type TileMapCoordinates = {| x: number, y: number |}; /** * Returns the tile id in a tile set. * This id corresponds to the index of the tile if the tile set - * is flattened so that each column is put right after the previous one. + * is flattened so that each row is put right after the previous one. * Example: - * 1 | 4 | 7 - * 2 | 5 | 8 - * 3 | 6 | 9 - * @param argument Object that contains x the horizontal position of the tile, y the vertical position and rowCount the number of rows in the tile set. + * 0 | 1 | 2 + * 3 | 4 | 5 + * 6 | 7 | 8 + * @param argument Object that contains x the horizontal position of the tile, y the vertical position and columnCount the number of columns in the tile set. * @returns the id of the tile. */ export const getTileIdFromGridCoordinates = ({ @@ -85,12 +86,12 @@ export const getTileIdFromGridCoordinates = ({ /** * Returns the coordinates of a tile in a tile set given its id. * This id corresponds to the index of the tile if the tile set - * is flattened so that each column is put right after the previous one. + * is flattened so that each row is put right after the previous one. * Example: - * 1 | 4 | 7 - * 2 | 5 | 8 - * 3 | 6 | 9 - * @param argument Object that contains id the id of the tile and rowCount the number of rows in the tile set. + * 0 | 1 | 2 + * 3 | 4 | 5 + * 6 | 7 | 8 + * @param argument Object that contains the id of the tile and columnCount the number of columns in the tile set. * @returns the id of the tile. */ export const getGridCoordinatesFromTileId = ({ @@ -191,14 +192,14 @@ const Tile = ({ export type TileMapTileSelection = | {| - kind: 'single', - coordinates: TileMapCoordinates, - flipHorizontally: boolean, - flipVertically: boolean, + kind: 'multiple', + coordinates: TileMapCoordinates[], |} | {| - kind: 'multiple', + kind: 'rectangle', coordinates: TileMapCoordinates[], + flipHorizontally: boolean, + flipVertically: boolean, |} | {| kind: 'erase', @@ -210,12 +211,18 @@ type Props = {| tileMapTileSelection: ?TileMapTileSelection, onSelectTileMapTile: (?TileMapTileSelection) => void, allowMultipleSelection: boolean, + allowRectangleSelection: boolean, showPaintingToolbar: boolean, interactive: boolean, onAtlasImageLoaded?: ( e: SyntheticEvent, atlasResourceName: string ) => void, + /** + * Needed to enable scrolling on touch devices when the user is not using + * a long touch to make a tile selection on the tile set. + */ + onScrollY: number => void, |}; const TileSetVisualizer = ({ @@ -224,9 +231,11 @@ const TileSetVisualizer = ({ tileMapTileSelection, onSelectTileMapTile, allowMultipleSelection, + allowRectangleSelection, showPaintingToolbar, interactive, onAtlasImageLoaded, + onScrollY, }: Props) => { const forceUpdate = useForceUpdate(); const atlasResourceName = objectConfiguration @@ -242,9 +251,9 @@ const TileSetVisualizer = ({ setShouldFlipHorizontally, ] = React.useState(false); const [ - lastSelectedTile, - setLastSelectedTile, - ] = React.useState(null); + lastSelection, + setLastSelection, + ] = React.useState(null); const tilesetContainerRef = React.useRef(null); const tilesetAndTooltipContainerRef = React.useRef(null); const [tooltipContent, setTooltipContent] = React.useState(null); - const [touchStartCoordinates, setTouchStartCoordinates] = React.useState(null); - const [shouldCancelClick, setShouldCancelClick] = React.useState( - false - ); + const isLongTouchRef = React.useRef(false); const tooltipDisplayTimeoutId = React.useRef(null); const [ rectangularSelectionTilePreview, @@ -314,7 +317,6 @@ const TileSetVisualizer = ({ const displayTileIdTooltip = React.useCallback( (e: ClientCoordinates) => { - setShouldCancelClick(true); if (!displayedTileSize || isBadlyConfigured) return; const imageCoordinates = getImageCoordinatesFromPointerEvent(e); @@ -339,7 +341,17 @@ const TileSetVisualizer = ({ [displayedTileSize, columnCount, rowCount, isBadlyConfigured] ); - const longTouchProps = useLongTouch(displayTileIdTooltip); + const handleLongTouch = React.useCallback( + (e: ClientCoordinates) => { + isLongTouchRef.current = true; + displayTileIdTooltip(e); + }, + [displayTileIdTooltip] + ); + + const longTouchProps = useLongTouch(handleLongTouch, { + doNotCancelOnScroll: true, + }); React.useEffect( () => { @@ -353,15 +365,12 @@ const TileSetVisualizer = ({ const onPointerDown = React.useCallback( (event: PointerEvent) => { if (isBadlyConfigured) return; - if (event.pointerType === 'touch') { - setTouchStartCoordinates({ x: event.pageX, y: event.pageY }); - } - const imageCoordinates = getImageCoordinatesFromPointerEvent(event); - if (!imageCoordinates) return; - setClickStartCoordinates({ - x: imageCoordinates.mouseX, - y: imageCoordinates.mouseY, - }); + const coordinates = getImageCoordinatesFromPointerEvent(event); + if (!coordinates) return; + startCoordinatesRef.current = { + x: coordinates.mouseX, + y: coordinates.mouseY, + }; }, [isBadlyConfigured] ); @@ -370,13 +379,33 @@ const TileSetVisualizer = ({ (event: PointerEvent) => { if ( isBadlyConfigured || - !clickStartCoordinates || + !startCoordinatesRef || !displayedTileSize || - !allowMultipleSelection || - event.pointerType === 'touch' + (!allowMultipleSelection && !allowRectangleSelection) ) { return; } + + const startCoordinates = startCoordinatesRef.current; + if (!startCoordinates) return; + + const isTouchDevice = event.pointerType === 'touch'; + + if (isTouchDevice) { + // Distinguish between a long touch (to multi select tiles) and a scroll. + if (!isLongTouchRef.current) { + const coordinates = getImageCoordinatesFromPointerEvent(event); + if (!coordinates) return; + if (tilesetContainerRef.current) { + const deltaY = -event.movementY; + const deltaX = + startCoordinates.x - coordinates.mouseXWithoutScrollLeft; + tilesetContainerRef.current.scrollLeft = deltaX; + onScrollY(deltaY); + } + return; + } + } const imageCoordinates = getImageCoordinatesFromPointerEvent(event); if (!imageCoordinates) return; @@ -387,10 +416,11 @@ const TileSetVisualizer = ({ rowCount, displayedTileSize, }); + const { x: startX, y: startY } = getGridCoordinatesFromPointerCoordinates( { - pointerX: clickStartCoordinates.x, - pointerY: clickStartCoordinates.y, + pointerX: startCoordinates.x, + pointerY: startCoordinates.y, columnCount, rowCount, displayedTileSize, @@ -412,7 +442,8 @@ const TileSetVisualizer = ({ columnCount, rowCount, allowMultipleSelection, - clickStartCoordinates, + allowRectangleSelection, + onScrollY, ] ); @@ -420,22 +451,13 @@ const TileSetVisualizer = ({ (event: PointerEvent) => { try { if (!displayedTileSize || isBadlyConfigured) return; - if (shouldCancelClick) { - setShouldCancelClick(false); - return; - } - let isTouchDevice = false; + const isTouchDevice = event.pointerType === 'touch'; + const startCoordinates = startCoordinatesRef.current; + if (!startCoordinates) return; - if (event.pointerType === 'touch') { - isTouchDevice = true; - if ( - !touchStartCoordinates || - Math.abs(event.pageX - touchStartCoordinates.x) > 30 || - Math.abs(event.pageY - touchStartCoordinates.y) > 30 - ) { - return; - } + if (isTouchDevice && !isLongTouchRef.current) { + return; } const imageCoordinates = getImageCoordinatesFromPointerEvent(event); @@ -448,80 +470,89 @@ const TileSetVisualizer = ({ rowCount, displayedTileSize, }); - if (!allowMultipleSelection) { - if ( - tileMapTileSelection && - tileMapTileSelection.kind === 'single' && - tileMapTileSelection.coordinates.x === x && - tileMapTileSelection.coordinates.y === y - ) { - onSelectTileMapTile(null); - } else { - onSelectTileMapTile({ - kind: 'single', - coordinates: { x, y }, - flipHorizontally: shouldFlipHorizontally, - flipVertically: shouldFlipVertically, - }); - } - return; - } - if (!clickStartCoordinates) return; + if (!startCoordinates) return; const { x: startX, y: startY, } = getGridCoordinatesFromPointerCoordinates({ - pointerX: clickStartCoordinates.x, - pointerY: clickStartCoordinates.y, + pointerX: startCoordinates.x, + pointerY: startCoordinates.y, columnCount, rowCount, displayedTileSize, }); - const newSelection = - tileMapTileSelection && tileMapTileSelection.kind === 'multiple' - ? { ...tileMapTileSelection } - : { kind: 'multiple', coordinates: [] }; - // Click on a tile. - if ( - (startX === x && startY === y) || - // Do not allow rectangular select on touch device as it conflicts with basic scrolling gestures. - isTouchDevice - ) { - if ( - tileMapTileSelection && - tileMapTileSelection.kind === 'multiple' - ) { - addOrRemoveCoordinatesInArray(newSelection.coordinates, { - x, - y, - }); - } - } else { - for ( - let columnIndex = Math.min(startX, x); - columnIndex <= Math.max(startX, x); - columnIndex++ - ) { + if (allowMultipleSelection) { + const newSelection = + tileMapTileSelection && tileMapTileSelection.kind === 'multiple' + ? { ...tileMapTileSelection } + : { kind: 'multiple', coordinates: [] }; + if (startX === x && startY === y) { + if ( + tileMapTileSelection && + tileMapTileSelection.kind === 'multiple' + ) { + addOrRemoveCoordinatesInArray(newSelection.coordinates, { + x, + y, + }); + } + } else { for ( - let rowIndex = Math.min(startY, y); - rowIndex <= Math.max(startY, y); - rowIndex++ + let columnIndex = Math.min(startX, x); + columnIndex <= Math.max(startX, x); + columnIndex++ ) { - if (newSelection && newSelection.kind === 'multiple') { - addOrRemoveCoordinatesInArray(newSelection.coordinates, { - x: columnIndex, - y: rowIndex, - }); + for ( + let rowIndex = Math.min(startY, y); + rowIndex <= Math.max(startY, y); + rowIndex++ + ) { + if (newSelection && newSelection.kind === 'multiple') { + addOrRemoveCoordinatesInArray(newSelection.coordinates, { + x: columnIndex, + y: rowIndex, + }); + } } } } + onSelectTileMapTile(newSelection); + } else if (allowRectangleSelection) { + const shouldRemoveSelection = + tileMapTileSelection && + tileMapTileSelection.kind === 'rectangle' && + startX === x && + startY === y && + x <= tileMapTileSelection.coordinates[1].x && + x >= tileMapTileSelection.coordinates[0].x && + y <= tileMapTileSelection.coordinates[1].y && + y >= tileMapTileSelection.coordinates[0].y; + if (shouldRemoveSelection) { + // Remove selection when user selects a single tile in the current tile selection. + onSelectTileMapTile(null); + } else { + const topLeftCorner = { + x: Math.min(startX, x), + y: Math.min(startY, y), + }; + const bottomRightCorner = { + x: Math.max(startX, x), + y: Math.max(startY, y), + }; + const newSelection = { + kind: 'rectangle', + coordinates: [topLeftCorner, bottomRightCorner], + flipHorizontally: shouldFlipHorizontally, + flipVertically: shouldFlipVertically, + }; + onSelectTileMapTile(newSelection); + } } - onSelectTileMapTile(newSelection); } finally { - setClickStartCoordinates(null); + startCoordinatesRef.current = null; setRectangularSelectionTilePreview(null); - setTouchStartCoordinates(null); + isLongTouchRef.current = false; } }, [ @@ -534,19 +565,14 @@ const TileSetVisualizer = ({ shouldFlipHorizontally, shouldFlipVertically, allowMultipleSelection, - clickStartCoordinates, - shouldCancelClick, - touchStartCoordinates, + allowRectangleSelection, ] ); React.useEffect( () => { - if (tileMapTileSelection && tileMapTileSelection.kind === 'single') { - setLastSelectedTile({ - x: tileMapTileSelection.coordinates.x, - y: tileMapTileSelection.coordinates.y, - }); + if (tileMapTileSelection && tileMapTileSelection.kind === 'rectangle') { + setLastSelection(tileMapTileSelection); } }, [tileMapTileSelection] @@ -651,21 +677,23 @@ const TileSetVisualizer = ({ tooltip={t`Paint`} selected={ !!tileMapTileSelection && - tileMapTileSelection.kind === 'single' + tileMapTileSelection.kind === 'rectangle' } onClick={e => { if ( !!tileMapTileSelection && - tileMapTileSelection.kind === 'single' + tileMapTileSelection.kind === 'rectangle' ) onSelectTileMapTile(null); else - onSelectTileMapTile({ - kind: 'single', - coordinates: lastSelectedTile || { x: 0, y: 0 }, - flipHorizontally: shouldFlipHorizontally, - flipVertically: shouldFlipVertically, - }); + onSelectTileMapTile( + lastSelection || { + kind: 'rectangle', + coordinates: [{ x: 0, y: 0 }, { x: 0, y: 0 }], + flipHorizontally: shouldFlipHorizontally, + flipVertically: shouldFlipVertically, + } + ); }} disabled={!isAtlasImageSet} > @@ -676,15 +704,14 @@ const TileSetVisualizer = ({ tooltip={t`Horizontal flip`} selected={shouldFlipHorizontally} disabled={ - !tileMapTileSelection || - tileMapTileSelection.kind !== 'single' + !tileMapTileSelection || tileMapTileSelection.kind === 'erase' } onClick={e => { const newShouldFlipHorizontally = !shouldFlipHorizontally; setShouldFlipHorizontally(newShouldFlipHorizontally); if ( !!tileMapTileSelection && - tileMapTileSelection.kind === 'single' + tileMapTileSelection.kind === 'rectangle' ) { onSelectTileMapTile({ ...tileMapTileSelection, @@ -700,15 +727,14 @@ const TileSetVisualizer = ({ tooltip={t`Vertical flip`} selected={shouldFlipVertically} disabled={ - !tileMapTileSelection || - tileMapTileSelection.kind !== 'single' + !tileMapTileSelection || tileMapTileSelection.kind === 'erase' } onClick={e => { const newShouldFlipVertically = !shouldFlipVertically; setShouldFlipVertically(newShouldFlipVertically); if ( !!tileMapTileSelection && - tileMapTileSelection.kind === 'single' + tileMapTileSelection.kind === 'rectangle' ) { onSelectTileMapTile({ ...tileMapTileSelection, @@ -776,14 +802,24 @@ const TileSetVisualizer = ({ /> )} {tileMapTileSelection && - tileMapTileSelection.kind === 'single' && + tileMapTileSelection.kind === 'rectangle' && displayedTileSize && ( )} {tileMapTileSelection && diff --git a/newIDE/app/src/InstancesEditor/index.js b/newIDE/app/src/InstancesEditor/index.js index f13bf9a2fb49..e6f319f3fd73 100644 --- a/newIDE/app/src/InstancesEditor/index.js +++ b/newIDE/app/src/InstancesEditor/index.js @@ -44,9 +44,6 @@ import { } from '../Utils/ZoomUtils'; import Background from './Background'; import TileMapPaintingPreview, { - getTileSet, - getTilesGridCoordinatesFromPointerSceneCoordinates, - isTileSetBadlyConfigured, updateSceneToTileMapTransformation, } from './TileMapPaintingPreview'; import { @@ -59,6 +56,11 @@ import { AffineTransformation } from '../Utils/AffineTransformation'; import { ErrorFallbackComponent } from '../UI/ErrorBoundary'; import { Trans } from '@lingui/macro'; import { generateUUID } from 'three/src/math/MathUtils'; +import { + getTilesGridCoordinatesFromPointerSceneCoordinates, + getTileSet, + isTileSetBadlyConfigured, +} from '../Utils/TileMap'; const gd: libGDevelop = global.gd; @@ -847,6 +849,7 @@ export default class InstancesEditor extends Component { } const tileMapGridCoordinates = getTilesGridCoordinatesFromPointerSceneCoordinates( { + tileMapTileSelection, coordinates: sceneCoordinates, tileSize: tileSet.tileSize, sceneToTileMapTransformation, @@ -855,96 +858,126 @@ export default class InstancesEditor extends Component { let shouldTrimAfterOperations = false; - if (tileMapTileSelection.kind === 'single') { + if (tileMapTileSelection.kind === 'rectangle') { shouldTrimAfterOperations = editableTileMap.isEmpty(); // TODO: Optimize list execution to make sure the most important size changing operations are done first. let cumulatedUnshiftedRows = 0, cumulatedUnshiftedColumns = 0; - const tileId = getTileIdFromGridCoordinates({ - columnCount: tileSet.columnCount, - ...tileMapTileSelection.coordinates, - }); - - const tileDefinition = editableTileMap.getTileDefinition(tileId); - if (!tileDefinition) return; const layer = editableTileMap.getTileLayer(0); if (!layer) return; - tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => { - // If rows or columns have been unshifted in the previous tile setting operations, - // we have to take them into account for the current coordinates. - const x = gridX + cumulatedUnshiftedColumns; - const y = gridY + cumulatedUnshiftedRows; - const rowsToAppend = Math.max( - 0, - y - (editableTileMap.getDimensionY() - 1) - ); - const columnsToAppend = Math.max( - 0, - x - (editableTileMap.getDimensionX() - 1) - ); - const rowsToUnshift = Math.abs(Math.min(0, y)); - const columnsToUnshift = Math.abs(Math.min(0, x)); - if ( - rowsToAppend > 0 || - columnsToAppend > 0 || - rowsToUnshift > 0 || - columnsToUnshift > 0 + tileMapGridCoordinates.forEach( + ({ bottomRightCorner, topLeftCorner, tileCoordinates }) => { + if (!tileCoordinates) return; + const tileId = getTileIdFromGridCoordinates({ + columnCount: tileSet.columnCount, + ...tileCoordinates, + }); + + const tileDefinition = editableTileMap.getTileDefinition(tileId); + if (!tileDefinition) return; + + for ( + let gridX = topLeftCorner.x; + gridX <= bottomRightCorner.x; + gridX++ + ) { + for ( + let gridY = topLeftCorner.y; + gridY <= bottomRightCorner.y; + gridY++ + ) { + // If rows or columns have been unshifted in the previous tile setting operations, + // we have to take them into account for the current coordinates. + const x = gridX + cumulatedUnshiftedColumns; + const y = gridY + cumulatedUnshiftedRows; + const rowsToAppend = Math.max( + 0, + y - (editableTileMap.getDimensionY() - 1) + ); + const columnsToAppend = Math.max( + 0, + x - (editableTileMap.getDimensionX() - 1) + ); + const rowsToUnshift = Math.abs(Math.min(0, y)); + const columnsToUnshift = Math.abs(Math.min(0, x)); + if ( + rowsToAppend > 0 || + columnsToAppend > 0 || + rowsToUnshift > 0 || + columnsToUnshift > 0 + ) { + editableTileMap.increaseDimensions( + columnsToAppend, + columnsToUnshift, + rowsToAppend, + rowsToUnshift + ); + } + const newX = x + columnsToUnshift; + const newY = y + rowsToUnshift; + + editableTileMap.setTile(newX, newY, 0, tileId); + editableTileMap.flipTileOnX( + newX, + newY, + 0, + tileMapTileSelection.flipHorizontally + ); + editableTileMap.flipTileOnY( + newX, + newY, + 0, + tileMapTileSelection.flipVertically + ); + + cumulatedUnshiftedRows += rowsToUnshift; + cumulatedUnshiftedColumns += columnsToUnshift; + // The instance angle is not considered when moving the instance after + // rows/columns were added/removed because the instance position does not + // include the rotation transformation. Otherwise, we could have used + // tileMapToSceneTransformation to get the new position. + selectedInstance.setX( + selectedInstance.getX() - + columnsToUnshift * (tileSet.tileSize * scaleX) + ); + selectedInstance.setY( + selectedInstance.getY() - + rowsToUnshift * (tileSet.tileSize * scaleY) + ); + if (selectedInstance.hasCustomSize()) { + selectedInstance.setCustomWidth( + selectedInstance.getCustomWidth() + + tileSet.tileSize * + scaleX * + (columnsToAppend + columnsToUnshift) + ); + selectedInstance.setCustomHeight( + selectedInstance.getCustomHeight() + + tileSet.tileSize * scaleY * (rowsToAppend + rowsToUnshift) + ); + } + } + } + } + ); + } else if (tileMapTileSelection.kind === 'erase') { + const { bottomRightCorner, topLeftCorner } = tileMapGridCoordinates[0]; + for ( + let gridX = topLeftCorner.x; + gridX <= bottomRightCorner.x; + gridX++ + ) { + for ( + let gridY = topLeftCorner.y; + gridY <= bottomRightCorner.y; + gridY++ ) { - editableTileMap.increaseDimensions( - columnsToAppend, - columnsToUnshift, - rowsToAppend, - rowsToUnshift - ); + editableTileMap.removeTile(gridX, gridY, 0); } - const newX = x + columnsToUnshift; - const newY = y + rowsToUnshift; - - editableTileMap.setTile(newX, newY, 0, tileId); - editableTileMap.flipTileOnX( - newX, - newY, - 0, - tileMapTileSelection.flipHorizontally - ); - editableTileMap.flipTileOnY( - newX, - newY, - 0, - tileMapTileSelection.flipVertically - ); + } - cumulatedUnshiftedRows += rowsToUnshift; - cumulatedUnshiftedColumns += columnsToUnshift; - // The instance angle is not considered when moving the instance after - // rows/columns were added/removed because the instance position does not - // include the rotation transformation. Otherwise, we could have used - // tileMapToSceneTransformation to get the new position. - selectedInstance.setX( - selectedInstance.getX() - - columnsToUnshift * (tileSet.tileSize * scaleX) - ); - selectedInstance.setY( - selectedInstance.getY() - - rowsToUnshift * (tileSet.tileSize * scaleY) - ); - if (selectedInstance.hasCustomSize()) { - selectedInstance.setCustomWidth( - selectedInstance.getCustomWidth() + - tileSet.tileSize * scaleX * (columnsToAppend + columnsToUnshift) - ); - selectedInstance.setCustomHeight( - selectedInstance.getCustomHeight() + - tileSet.tileSize * scaleY * (rowsToAppend + rowsToUnshift) - ); - } - }); - } else if (tileMapTileSelection.kind === 'erase') { - tileMapGridCoordinates.forEach(({ x: gridX, y: gridY }) => { - editableTileMap.removeTile(gridX, gridY, 0); - }); shouldTrimAfterOperations = true; } else { return; diff --git a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js index cfccfbef45a2..437a7d7e1baf 100644 --- a/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js +++ b/newIDE/app/src/MainFrame/EditorContainers/HomePage/index.js @@ -58,6 +58,8 @@ const isShopRequested = (routeArguments: RouteArguments): boolean => routeArguments['initial-dialog'] === 'store'; // New way of opening the store const isGamesDashboardRequested = (routeArguments: RouteArguments): boolean => routeArguments['initial-dialog'] === 'games-dashboard'; +const isBuildRequested = (routeArguments: RouteArguments): boolean => + routeArguments['initial-dialog'] === 'build'; const styles = { container: { @@ -220,10 +222,15 @@ export const HomePage = React.memo( const isGamesDashboardRequestedAtOpening = React.useRef( isGamesDashboardRequested(routeArguments) ); + const isBuildRequestedAtOpening = React.useRef( + isBuildRequested(routeArguments) + ); const initialTab = isShopRequestedAtOpening.current ? 'shop' : isGamesDashboardRequestedAtOpening.current ? 'manage' + : isBuildRequestedAtOpening.current + ? 'build' : showGetStartedSectionByDefault ? 'get-started' : 'build'; @@ -298,6 +305,9 @@ export const HomePage = React.memo( } else if (isGamesDashboardRequested(routeArguments)) { setActiveTab('manage'); removeRouteArguments(['initial-dialog']); + } else if (isBuildRequested(routeArguments)) { + setActiveTab('build'); + removeRouteArguments(['initial-dialog']); } }, [ diff --git a/newIDE/app/src/MainFrame/RouterContext.js b/newIDE/app/src/MainFrame/RouterContext.js index b7897eeb3afd..601dc64905ca 100644 --- a/newIDE/app/src/MainFrame/RouterContext.js +++ b/newIDE/app/src/MainFrame/RouterContext.js @@ -8,7 +8,8 @@ export type Route = | 'subscription' | 'games-dashboard' | 'asset-store' // For compatibility when there was only asset packs. - | 'store'; // New way of opening the store. + | 'store' // New way of opening the store. + | 'build'; type RouteKey = | 'initial-dialog' | 'game-id' diff --git a/newIDE/app/src/MainFrame/index.js b/newIDE/app/src/MainFrame/index.js index 4ddbc2d59390..3ba2c6743691 100644 --- a/newIDE/app/src/MainFrame/index.js +++ b/newIDE/app/src/MainFrame/index.js @@ -177,6 +177,7 @@ import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders import CustomDragLayer from '../UI/DragAndDrop/CustomDragLayer'; import CloudProjectRecoveryDialog from '../ProjectsStorage/CloudStorageProvider/CloudProjectRecoveryDialog'; import CloudProjectSaveChoiceDialog from '../ProjectsStorage/CloudStorageProvider/CloudProjectSaveChoiceDialog'; +import CloudStorageProvider from '../ProjectsStorage/CloudStorageProvider'; import useCreateProject from '../Utils/UseCreateProject'; import newNameGenerator from '../Utils/NewNameGenerator'; import { addDefaultLightToAllLayers } from '../ProjectCreation/CreateProject'; @@ -193,6 +194,7 @@ import { useAuthenticatedPlayer } from './UseAuthenticatedPlayer'; import ListIcon from '../UI/ListIcon'; import { QuickCustomizationDialog } from '../QuickCustomization/QuickCustomizationDialog'; import { type ObjectWithContext } from '../ObjectsList/EnumerateObjects'; +import RouterContext from './RouterContext'; const GD_STARTUP_TIMES = global.GD_STARTUP_TIMES || []; @@ -381,6 +383,7 @@ const MainFrame = (props: Props) => { newProjectSetupDialogOpen, setNewProjectSetupDialogOpen, ] = React.useState(false); + const { navigateToRoute } = React.useContext(RouterContext); const [isProjectOpening, setIsProjectOpening] = React.useState( false @@ -2410,7 +2413,12 @@ const MainFrame = (props: Props) => { ); const saveProjectAsWithStorageProvider = React.useCallback( - async (requestedStorageProvider?: StorageProvider) => { + async ( + options: ?{| + requestedStorageProvider?: StorageProvider, + forcedSavedAsLocation?: SaveAsLocation, + |} + ) => { if (!currentProject) return; saveUiSettings(state.editorTabs); @@ -2428,6 +2436,8 @@ const MainFrame = (props: Props) => { const oldStorageProviderOperations = getStorageProviderOperations(); // Get the methods to save the project using the *new* storage provider. + const requestedStorageProvider = + options && options.requestedStorageProvider; const newStorageProviderOperations = getStorageProviderOperations( requestedStorageProvider ); @@ -2453,8 +2463,9 @@ const MainFrame = (props: Props) => { const storageProviderInternalName = newStorageProvider.internalName; try { - let newSaveAsLocation: ?SaveAsLocation = null; - if (onChooseSaveProjectAsLocation) { + let newSaveAsLocation: ?SaveAsLocation = + options && options.forcedSavedAsLocation; + if (onChooseSaveProjectAsLocation && !newSaveAsLocation) { const { saveAsLocation } = await onChooseSaveProjectAsLocation({ project: currentProject, fileMetadata: currentFileMetadata, @@ -2462,19 +2473,19 @@ const MainFrame = (props: Props) => { if (!saveAsLocation) { return; // Save as was cancelled. } + newSaveAsLocation = saveAsLocation; + } - if (canFileMetadataBeSafelySavedAs && currentFileMetadata) { - const canProjectBeSafelySavedAs = await canFileMetadataBeSafelySavedAs( - currentFileMetadata, - { - showAlert, - showConfirmation, - } - ); + if (canFileMetadataBeSafelySavedAs && currentFileMetadata) { + const canProjectBeSafelySavedAs = await canFileMetadataBeSafelySavedAs( + currentFileMetadata, + { + showAlert, + showConfirmation, + } + ); - if (!canProjectBeSafelySavedAs) return; - } - newSaveAsLocation = saveAsLocation; + if (!canProjectBeSafelySavedAs) return; } const { wasSaved, fileMetadata } = await onSaveProjectAs( @@ -3766,7 +3777,9 @@ const MainFrame = (props: Props) => { storageProviders={props.storageProviders} onChooseProvider={storageProvider => { openSaveToStorageProviderDialog(false); - saveProjectAsWithStorageProvider(storageProvider); + saveProjectAsWithStorageProvider({ + requestedStorageProvider: storageProvider, + }); }} /> )} @@ -3868,17 +3881,50 @@ const MainFrame = (props: Props) => { onLaunchPreview={ hotReloadPreviewButtonProps.launchProjectDataOnlyPreview } - onClose={options => { + onClose={async options => { + if (hasUnsavedChanges) { + const response = await showConfirmation({ + title: t`Leave the customization?`, + message: t`Do you want to quit the customization? All your changes will be lost.`, + confirmButtonLabel: t`Leave`, + }); + + if (!response) { + return; + } + } + setQuickCustomizationDialogOpenedFromGameId(null); - if (options && options.tryAnotherGame) { - // Close the project so the user is back at where they can chose a game to customize - // which is probably the home page. - closeProject(); - openHomePage(); + closeProject(); + openHomePage(); + if (!hasUnsavedChanges) { + navigateToRoute('build'); } }} onlineWebExporter={quickPublishOnlineWebExporter} - onSaveProject={saveProject} + onSaveProject={async () => { + // Automatically try to save project to the cloud. + const storageProvider = getStorageProvider(); + if ( + !['Empty', 'UrlStorageProvider', 'Cloud'].includes( + storageProvider.internalName + ) + ) { + console.error( + `Unexpected storage provider ${ + storageProvider.internalName + } when saving project from quick customization dialog. Saving anyway.` + ); + } + + saveProjectAsWithStorageProvider({ + requestedStorageProvider: CloudStorageProvider, + forcedSavedAsLocation: { + name: currentProject.getName(), + }, + }); + return; + }} isSavingProject={isSavingProject} canClose={true} sourceGameId={quickCustomizationDialogOpenedFromGameId} diff --git a/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js b/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js index dc5e6a4354a6..51a8665bddeb 100644 --- a/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js +++ b/newIDE/app/src/ObjectEditor/Editors/SimpleTileMapEditor.js @@ -2,7 +2,7 @@ import * as React from 'react'; import type { EditorProps } from './EditorProps.flow'; -import ScrollView from '../../UI/ScrollView'; +import ScrollView, { type ScrollViewInterface } from '../../UI/ScrollView'; import { ColumnStackLayout } from '../../UI/Layout'; import SemiControlledTextField from '../../UI/SemiControlledTextField'; import { Trans } from '@lingui/macro'; @@ -27,6 +27,7 @@ const SimpleTileMapEditor = ({ resourceManagementProps, renderObjectNameField, }: EditorProps) => { + const scrollViewRef = React.useRef(null); const forceUpdate = useForceUpdate(); const objectProperties = objectConfiguration.getProperties(); const tileSize = parseFloat(objectProperties.get('tileSize').getValue()); @@ -128,6 +129,12 @@ const SimpleTileMapEditor = ({ [columnCount, objectConfiguration, forceUpdate, onObjectUpdated] ); + const onScrollY = React.useCallback(deltaY => { + if (scrollViewRef.current) { + scrollViewRef.current.scrollBy(deltaY); + } + }, []); + const onChangeAtlasImage = React.useCallback( () => { if (onObjectUpdated) onObjectUpdated(); @@ -168,7 +175,7 @@ const SimpleTileMapEditor = ({ ); return ( - + {!!renderObjectNameField && renderObjectNameField()} {/* TODO: Should this be a Select field with all possible values given the atlas image size? */} @@ -208,8 +215,10 @@ const SimpleTileMapEditor = ({ onSelectTileMapTile={onChangeTilesWithHitBox} showPaintingToolbar={false} allowMultipleSelection + allowRectangleSelection={false} onAtlasImageLoaded={onAtlasImageLoaded} interactive={true} + onScrollY={onScrollY} /> )} diff --git a/newIDE/app/src/Profile/AuthenticatedUserProvider.js b/newIDE/app/src/Profile/AuthenticatedUserProvider.js index e909c16c8b45..d77a4a699e50 100644 --- a/newIDE/app/src/Profile/AuthenticatedUserProvider.js +++ b/newIDE/app/src/Profile/AuthenticatedUserProvider.js @@ -309,9 +309,11 @@ export default class AuthenticatedUserProvider extends React.Component< } }; - _fetchUserProfileWithoutThrowingErrors = async () => { + _fetchUserProfileWithoutThrowingErrors = async ( + options: ?{ dontNotifyAboutEmailVerification?: boolean } + ) => { try { - await this._fetchUserProfile(); + await this._fetchUserProfile(options); } catch (error) { console.error( 'Error while fetching the user profile - but ignoring it.', @@ -320,7 +322,9 @@ export default class AuthenticatedUserProvider extends React.Component< } }; - _fetchUserProfile = async () => { + _fetchUserProfile = async ( + options: ?{ dontNotifyAboutEmailVerification?: boolean } + ) => { const { authentication } = this.props; this.setState(({ authenticatedUser }) => ({ @@ -561,7 +565,9 @@ export default class AuthenticatedUserProvider extends React.Component< // We call this function every time the user is fetched, as it will // automatically prevent the event to be sent if the user attributes haven't changed. identifyUserForAnalytics(this.state.authenticatedUser); - this._notifyUserAboutEmailVerification(); + if (!options || !options.dontNotifyAboutEmailVerification) { + this._notifyUserAboutEmailVerification(); + } } ); }; @@ -919,6 +925,8 @@ export default class AuthenticatedUserProvider extends React.Component< if (!authentication) return; this.setState({ + // This function is used for both account creation & login. + createAccountInProgress: true, loginInProgress: true, apiCallError: null, authenticatedUser: { @@ -956,6 +964,7 @@ export default class AuthenticatedUserProvider extends React.Component< } } this.setState({ + createAccountInProgress: false, loginInProgress: false, authenticatedUser: { ...this.state.authenticatedUser, @@ -1083,7 +1092,10 @@ export default class AuthenticatedUserProvider extends React.Component< // by the API when fetched. } - await this._fetchUserProfileWithoutThrowingErrors(); + await this._fetchUserProfileWithoutThrowingErrors({ + // When creating an account, avoid showing the email verification dialog right away. + dontNotifyAboutEmailVerification: true, + }); this.openCreateAccountDialog(false); sendSignupDone(form.email); const firebaseUser = this.state.authenticatedUser.firebaseUser; diff --git a/newIDE/app/src/QuickCustomization/GameImage.js b/newIDE/app/src/QuickCustomization/GameImage.js new file mode 100644 index 000000000000..88ee3a98c161 --- /dev/null +++ b/newIDE/app/src/QuickCustomization/GameImage.js @@ -0,0 +1,46 @@ +// @flow +import * as React from 'react'; + +const styles = { + image: { + width: '100%', + maxHeight: '325px', + objectFit: 'contain', + borderRadius: '16px', + boxSizing: 'border-box', + aspectRatio: '16 / 9', + }, +}; + +type Props = {| + project: gdProject, +|}; + +const GameImage = ({ project }: Props) => { + const rocketUrl = 'res/quick_customization/quick_publish.svg'; + + const gameThumbnailUrl = React.useMemo( + () => { + const resourcesManager = project.getResourcesManager(); + const thumbnailName = project + .getPlatformSpecificAssets() + .get('liluo', `thumbnail`); + if (!thumbnailName) return rocketUrl; + const path = resourcesManager.getResource(thumbnailName).getFile(); + if (!path) return rocketUrl; + + return path; + }, + [project] + ); + + return ( + Customize your game with GDevelop + ); +}; + +export default GameImage; diff --git a/newIDE/app/src/QuickCustomization/PreviewLine.js b/newIDE/app/src/QuickCustomization/PreviewLine.js new file mode 100644 index 000000000000..4801330707f8 --- /dev/null +++ b/newIDE/app/src/QuickCustomization/PreviewLine.js @@ -0,0 +1,60 @@ +// @flow +import * as React from 'react'; +import { Trans } from '@lingui/macro'; +import FlatButton from '../UI/FlatButton'; +import { LineStackLayout } from '../UI/Layout'; +import Text from '../UI/Text'; +import PreviewIcon from '../UI/CustomSvgIcons/Preview'; +import { Column } from '../UI/Grid'; +import Paper from '../UI/Paper'; +import PlaySquared from '../UI/CustomSvgIcons/PlaySquared'; +import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext'; +import classes from './PreviewLine.module.css'; +import classNames from 'classnames'; + +type Props = {| + onLaunchPreview: () => Promise, +|}; + +const PreviewLine = ({ onLaunchPreview }: Props) => { + const gdevelopTheme = React.useContext(GDevelopThemeContext); + + return ( + + +
+ + + + + + Preview your game + + + Preview} + onClick={onLaunchPreview} + leftIcon={} + /> + + +
+
+
+ ); +}; + +export default PreviewLine; diff --git a/newIDE/app/src/QuickCustomization/PreviewLine.module.css b/newIDE/app/src/QuickCustomization/PreviewLine.module.css new file mode 100644 index 000000000000..abd68a72449b --- /dev/null +++ b/newIDE/app/src/QuickCustomization/PreviewLine.module.css @@ -0,0 +1,5 @@ +.previewLine { + border-left: 4px solid var(--theme-message-valid-color); + border-radius: 6px; +} + diff --git a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js index 561d70b390a8..5a34e7ed0b3c 100644 --- a/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js +++ b/newIDE/app/src/QuickCustomization/QuickBehaviorsTweaker.js @@ -9,9 +9,9 @@ import { enumerateObjectFolderOrObjects } from '.'; import CompactPropertiesEditor from '../CompactPropertiesEditor'; import propertiesMapToSchema from '../CompactPropertiesEditor/PropertiesMapToCompactSchema'; import { Trans } from '@lingui/macro'; -import { CalloutCard } from '../UI/CalloutCard'; -import { LargeSpacer } from '../UI/Grid'; import { useForceRecompute } from '../Utils/UseForceUpdate'; +import TipCard from './TipCard'; +import { Column } from '../UI/Grid'; const gd: libGDevelop = global.gd; @@ -34,29 +34,32 @@ const QuickBehaviorPropertiesEditor = ({ if (schemaRecomputeTrigger) { // schemaRecomputeTrigger allows to invalidate the schema when required. } - return propertiesMapToSchema( - behavior.getProperties(), - behavior => behavior.getProperties(), - (behavior, name, value) => { + return propertiesMapToSchema({ + properties: behavior.getProperties(), + getProperties: behavior => behavior.getProperties(), + onUpdateProperty: (behavior, name, value) => { behavior.updateProperty(name, value); }, object, - 'Basic-Quick' - ); + visibility: 'Basic-Quick', + quickCustomizationVisibilities: behavior.getPropertiesQuickCustomizationVisibilities(), + }); }, [behavior, object, schemaRecomputeTrigger] ); return ( - + + + ); }; @@ -107,6 +110,15 @@ export const QuickBehaviorsTweaker = ({ }: Props) => { return ( + These are behaviors} + description={ + + Behaviors are attached to objects and make them alive. The rules of + the game can be created using behaviors and events. + + } + /> {mapFor(0, project.getLayoutsCount(), i => { const layout = project.getLayoutAt(i); const folderObjects = enumerateObjectFolderOrObjects( @@ -127,7 +139,7 @@ export const QuickBehaviorsTweaker = ({ } return ( - + {behaviorNamesToTweak.map(behaviorName => { @@ -140,6 +152,7 @@ export const QuickBehaviorsTweaker = ({ object={object} onBehaviorUpdated={() => {}} resourceManagementProps={resourceManagementProps} + key={behavior.ptr} /> ); })} @@ -154,7 +167,12 @@ export const QuickBehaviorsTweaker = ({ } return ( - + {folderName} @@ -176,7 +194,12 @@ export const QuickBehaviorsTweaker = ({ } return ( - + {project.getLayoutsCount() > 1 && ( {layout.getName()} @@ -186,30 +209,6 @@ export const QuickBehaviorsTweaker = ({ ); }).filter(Boolean)} - - ( - - )} - > - - - - Making a fun game with behaviors - - - - Behaviors are attached to objects and make them alive. The rules - of the game can be created using behaviors and events. - - - - - ); }; diff --git a/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js b/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js index 71123167a35c..e689d3285cc8 100644 --- a/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js +++ b/newIDE/app/src/QuickCustomization/QuickCustomizationDialog.js @@ -5,20 +5,19 @@ import { renderQuickCustomization, useQuickCustomizationState } from '.'; import { Trans } from '@lingui/macro'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import FlatButton from '../UI/FlatButton'; -import { ColumnStackLayout, LineStackLayout } from '../UI/Layout'; -import Text from '../UI/Text'; -import { useResponsiveWindowSize } from '../UI/Responsive/ResponsiveWindowMeasurer'; -import Paper from '../UI/Paper'; +import { ColumnStackLayout } from '../UI/Layout'; import { type Exporter } from '../ExportAndShare/ShareDialog'; import { useGameAndBuildsManager } from '../Utils/UseGameAndBuildsManager'; import { sendQuickCustomizationProgress } from '../Utils/Analytics/EventSender'; import ScrollView from '../UI/ScrollView'; +import PreviewLine from './PreviewLine'; +import UnsavedChangesContext from '../MainFrame/UnsavedChangesContext'; type Props = {| project: gdProject, resourceManagementProps: ResourceManagementProps, onLaunchPreview: () => Promise, - onClose: (?{| tryAnotherGame: boolean |}) => void, + onClose: (?{| tryAnotherGame: boolean |}) => Promise, onlineWebExporter: Exporter, onSaveProject: () => Promise, isSavingProject: boolean, @@ -37,14 +36,12 @@ export const QuickCustomizationDialog = ({ canClose, sourceGameId, }: Props) => { + const { triggerUnsavedChanges } = React.useContext(UnsavedChangesContext); const gameAndBuildsManager = useGameAndBuildsManager({ project, copyLeaderboardsAndMultiplayerLobbiesFromGameId: sourceGameId, }); const quickCustomizationState = useQuickCustomizationState({ onClose }); - const { windowSize } = useResponsiveWindowSize(); - - const isMediumOrSmaller = windowSize === 'small' || windowSize === 'medium'; const onContinueQuickCustomization = React.useCallback( () => { @@ -60,12 +57,7 @@ export const QuickCustomizationDialog = ({ [onClose] ); - const { - title, - titleRightContent, - titleTopContent, - content, - } = renderQuickCustomization({ + const { title, content, showPreview } = renderQuickCustomization({ project, gameAndBuildsManager, resourceManagementProps, @@ -91,70 +83,64 @@ export const QuickCustomizationDialog = ({ [quickCustomizationState.step.name, sourceGameId, name] ); + React.useEffect( + () => { + triggerUnsavedChanges(); + }, + // Trigger unsaved changes when the dialog is opened, + // so the user is warned if they try to close the dialog. + // eslint-disable-next-line react-hooks/exhaustive-deps + [] + ); + return ( {titleTopContent} - ) : null - } + title={title} maxWidth="md" fullHeight actions={ - !quickCustomizationState.step.shouldHideNavigationButtons + !!quickCustomizationState.step.nextLabel ? [ - quickCustomizationState.canGoToPreviousStep ? ( - Back} - onClick={quickCustomizationState.goToPreviousStep} - disabled={ - !quickCustomizationState.canGoToPreviousStep || - quickCustomizationState.isNavigationDisabled - } - /> - ) : null, , ] - : undefined + : [] } secondaryActions={[ - quickCustomizationState.step.shouldHideNavigationButtons || - !canClose ? null : ( + !quickCustomizationState.canGoToPreviousStep ? null : ( Close} - primary={false} - onClick={onClose} - disabled={quickCustomizationState.isNavigationDisabled} + key="previous" + primary + label={Back} + onClick={quickCustomizationState.goToPreviousStep} + disabled={ + !quickCustomizationState.canGoToPreviousStep || + quickCustomizationState.isNavigationDisabled + } + fullWidth /> ), ]} + onRequestClose={canClose ? onClose : undefined} flexBody + actionsFullWidthOnMobile + cannotBeDismissed={quickCustomizationState.isNavigationDisabled} > - - - - - {title} - - {!isMediumOrSmaller ? titleRightContent : null} - - {content} - - + + + + {content} + + + {showPreview && } + ); }; diff --git a/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js b/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js index a2937467ff68..2d97a2a78e63 100644 --- a/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js +++ b/newIDE/app/src/QuickCustomization/QuickCustomizationGameTiles.js @@ -78,6 +78,7 @@ export const QuickCustomizationGameTiles = ({ thumbnailTitleByLocale )} key={exampleShortHeader.name} + useQuickCustomizationThumbnail /> ) ) diff --git a/newIDE/app/src/QuickCustomization/QuickCustomizationPropertiesVisibilityDialog.js b/newIDE/app/src/QuickCustomization/QuickCustomizationPropertiesVisibilityDialog.js new file mode 100644 index 000000000000..7052bd0b66a3 --- /dev/null +++ b/newIDE/app/src/QuickCustomization/QuickCustomizationPropertiesVisibilityDialog.js @@ -0,0 +1,114 @@ +// @flow +import { t, Trans } from '@lingui/macro'; + +import * as React from 'react'; +import Dialog from '../UI/Dialog'; +import FlatButton from '../UI/FlatButton'; +import { mapFor } from '../Utils/MapFor'; +import Text from '../UI/Text'; +import SelectField from '../UI/SelectField'; +import SelectOption from '../UI/SelectOption'; +import { LineStackLayout } from '../UI/Layout'; +import useForceUpdate from '../Utils/UseForceUpdate'; + +const gd: libGDevelop = global.gd; + +const getVisibilityForProperty = ( + propertyName: string, + propertiesQuickCustomizationVisibilities: gdQuickCustomizationVisibilitiesContainer +) => { + return propertiesQuickCustomizationVisibilities.get(propertyName); +}; + +type Props = {| + propertyNames: string[], + open: boolean, + onClose: () => void, + propertiesQuickCustomizationVisibilities: gdQuickCustomizationVisibilitiesContainer, +|}; + +export default function QuickCustomizationPropertiesVisibilityDialog({ + open, + onClose, + propertyNames, + propertiesQuickCustomizationVisibilities, +}: Props) { + const forceUpdate = useForceUpdate(); + + return ( + Quick Customization: Behavior properties} + secondaryActions={[ + Close} + primary={false} + onClick={onClose} + />, + ]} + open + onRequestClose={onClose} + flexColumnBody + fullHeight + > + {mapFor(0, propertyNames.length, i => { + const propertyName = propertyNames[i]; + const value = getVisibilityForProperty( + propertyName, + propertiesQuickCustomizationVisibilities + ); + + return ( + + {propertyName} + { + const newQuickCustomizationVisibility = parseInt(newValue, 10); + if ( + [ + gd.QuickCustomization.Visible, + gd.QuickCustomization.Hidden, + gd.QuickCustomization.Default, + ].includes(newQuickCustomizationVisibility) + ) { + propertiesQuickCustomizationVisibilities.set( + propertyName, + // $FlowIgnore: We checked that newQuickCustomizationVisibility is a valid visibility + newQuickCustomizationVisibility + ); + forceUpdate(); + return; + } + + propertiesQuickCustomizationVisibilities.set( + propertyName, + gd.QuickCustomization.Default + ); + forceUpdate(); + }} + > + + + + + + ); + })} + + ); +} diff --git a/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js b/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js index 9981071f2cd6..6d852204af07 100644 --- a/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js +++ b/newIDE/app/src/QuickCustomization/QuickObjectReplacer.js @@ -2,15 +2,14 @@ import * as React from 'react'; import { ObjectPreview } from './ObjectPreview'; import { mapFor } from '../Utils/MapFor'; -import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout'; +import { ColumnStackLayout } from '../UI/Layout'; import FlatButton from '../UI/FlatButton'; import AssetSwappingDialog from '../AssetStore/AssetSwappingDialog'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import Text from '../UI/Text'; import { Trans } from '@lingui/macro'; import { enumerateObjectFolderOrObjects } from '.'; -import { CalloutCard } from '../UI/CalloutCard'; -import { LargeSpacer } from '../UI/Grid'; +import TipCard from './TipCard'; type Props = {| project: gdProject, @@ -35,6 +34,15 @@ export const QuickObjectReplacer = ({ return ( + These are objects} + description={ + + Each character, player, obstacle, background, item, etc. is an + object. Objects are the building blocks of your game. + + } + /> {mapFor(0, project.getLayoutsCount(), i => { const layout = project.getLayoutAt(i); const folderObjects = enumerateObjectFolderOrObjects( @@ -44,7 +52,7 @@ export const QuickObjectReplacer = ({ if (!folderObjects.length) return null; return ( - + {project.getLayoutsCount() > 1 && ( {layout.getName()} @@ -52,13 +60,13 @@ export const QuickObjectReplacer = ({ )} {folderObjects.map(({ folderName, objects }) => { return ( - + {folderName}
{objects.map(object => ( - + ); })} - - ( - - )} - > - - - - Objects are everything in your game - - - - Each character, player, obstacle, background, item, etc. is an - object. Objects are the building blocks of your game. - - - - - {selectedObjectToSwap && ( { setSelectedObjectToSwap(null); }} + minimalUI /> )} diff --git a/newIDE/app/src/QuickCustomization/QuickPublish.js b/newIDE/app/src/QuickCustomization/QuickPublish.js index dc6d0dc456eb..74d8429e0551 100644 --- a/newIDE/app/src/QuickCustomization/QuickPublish.js +++ b/newIDE/app/src/QuickCustomization/QuickPublish.js @@ -4,27 +4,34 @@ import { Trans } from '@lingui/macro'; import EventsFunctionsExtensionsContext from '../EventsFunctionsExtensionsLoader/EventsFunctionsExtensionsContext'; import ExportLauncher from '../ExportAndShare/ShareDialog/ExportLauncher'; import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext'; -import { ColumnStackLayout } from '../UI/Layout'; +import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout'; import RaisedButton from '../UI/RaisedButton'; import { I18n } from '@lingui/react'; import { type Exporter } from '../ExportAndShare/ShareDialog'; import Text from '../UI/Text'; import { type GameAndBuildsManager } from '../Utils/UseGameAndBuildsManager'; import FlatButton from '../UI/FlatButton'; -import { Spacer } from '../UI/Grid'; -import TextButton from '../UI/TextButton'; +import { Column, Line, Spacer } from '../UI/Grid'; import classes from './QuickPublish.module.css'; import classNames from 'classnames'; +import Paper from '../UI/Paper'; +import Google from '../UI/CustomSvgIcons/Google'; +import GitHub from '../UI/CustomSvgIcons/GitHub'; +import Apple from '../UI/CustomSvgIcons/Apple'; +import TextButton from '../UI/TextButton'; +import Trash from '../UI/CustomSvgIcons/Trash'; +import GameImage from './GameImage'; +import ShareLink from '../UI/ShareDialog/ShareLink'; +import { getGameUrl } from '../Utils/GDevelopServices/Game'; type Props = {| project: gdProject, gameAndBuildsManager: GameAndBuildsManager, setIsNavigationDisabled: (isNavigationDisabled: boolean) => void, - shouldAutomaticallyStartExport: boolean, onlineWebExporter: Exporter, onSaveProject: () => Promise, isSavingProject: boolean, - onClose: () => void, + onClose: () => Promise, onContinueQuickCustomization: () => void, onTryAnotherGame: () => void, |}; @@ -33,7 +40,6 @@ export const QuickPublish = ({ project, gameAndBuildsManager, setIsNavigationDisabled, - shouldAutomaticallyStartExport, onlineWebExporter, onSaveProject, isSavingProject, @@ -43,6 +49,7 @@ export const QuickPublish = ({ }: Props) => { const authenticatedUser = React.useContext(AuthenticatedUserContext); const { profile, onOpenCreateAccountDialog } = authenticatedUser; + const { game } = gameAndBuildsManager; const eventsFunctionsExtensionsState = React.useContext( EventsFunctionsExtensionsContext ); @@ -61,129 +68,162 @@ export const QuickPublish = ({ React.useEffect( () => { - if (profile && shouldAutomaticallyStartExport) { + if (profile && exportState === '') { + // Save project & launch export as soon as the user is authenticated (or if they already were) + onSaveProject(); launchExport(); } }, - [profile, shouldAutomaticallyStartExport, launchExport] + [profile, launchExport, onSaveProject, exportState] ); + const gameUrl = game ? getGameUrl(game) : ''; + const hasNotSavedProject = !profile && exportState === ''; + return ( - - - Publish your game with GDevelop - {profile ? ( - - {({ i18n }) => ( - - { - // Nothing to do. - }} - authenticatedUser={authenticatedUser} - eventsFunctionsExtensionsState={eventsFunctionsExtensionsState} - exportPipeline={onlineWebExporter.exportPipeline} - setIsNavigationDisabled={setIsNavigationDisabled} - gameAndBuildsManager={gameAndBuildsManager} - uiMode="minimal" - onExportLaunched={() => { - setExportState('started'); - }} - onExportErrored={() => { - setExportState('errored'); - }} - onExportSucceeded={() => { - setExportState('succeeded'); - }} - /> - {exportState === 'succeeded' ? ( - - - Congratulations! Your game is now published. - - Continue tweaking the game} - onClick={onContinueQuickCustomization} - /> - Edit the full game} - onClick={onClose} - /> - - or - - Try with another game} - onClick={onTryAnotherGame} - /> - - ) : !shouldAutomaticallyStartExport && exportState === '' ? ( + + + + + {profile ? ( + + {({ i18n }) => ( + + { + // Nothing to do. + }} + authenticatedUser={authenticatedUser} + eventsFunctionsExtensionsState={ + eventsFunctionsExtensionsState + } + exportPipeline={onlineWebExporter.exportPipeline} + setIsNavigationDisabled={setIsNavigationDisabled} + gameAndBuildsManager={gameAndBuildsManager} + uiMode="minimal" + onExportLaunched={() => { + setExportState('started'); + }} + onExportErrored={() => { + setExportState('errored'); + }} + onExportSucceeded={() => { + setExportState('succeeded'); + }} + /> + {exportState === 'succeeded' ? ( + +
+ + + Share your game with your friends! + + {gameUrl && } + +
+
+ ) : exportState === 'errored' ? ( + + + + An error occurred while exporting your game. Verify your + internet connection and try again. + + + Try again} + onClick={launchExport} + /> + + ) : null} +
+ )} +
+ ) : ( + + +
- Go back and tweak the game} - onClick={onContinueQuickCustomization} - /> - - or - - Edit the full game} - onClick={onClose} - /> - - ) : exportState === 'errored' ? ( - - An error occurred while exporting your game. Verify your - internet connection and try again. + Create a GDevelop account to save your changes and keep + personalizing your game - Try again} - onClick={launchExport} - /> + + } + label={Google} + onClick={onOpenCreateAccountDialog} + fullWidth + /> + } + label={Github} + onClick={onOpenCreateAccountDialog} + fullWidth + /> + } + label={Apple} + onClick={onOpenCreateAccountDialog} + fullWidth + /> + Edit the full game} - onClick={onClose} + primary + label={Use your email} + onClick={onOpenCreateAccountDialog} /> - ) : null} - - )} - - ) : ( - - - - Create a GDevelop account to share your game in a few seconds. - - - Create an account} - onClick={onOpenCreateAccountDialog} - keyboardFocused - /> +
+
+
+ )} +
+ + {exportState !== 'started' && ( + Skip and edit the full game} + secondary onClick={onClose} + label={ + hasNotSavedProject ? ( + Leave and lose all changes + ) : ( + Finish and close + ) + } + icon={hasNotSavedProject ? : null} /> -
+ )}
); diff --git a/newIDE/app/src/QuickCustomization/QuickPublish.module.css b/newIDE/app/src/QuickCustomization/QuickPublish.module.css index 5b93d5c32fc0..55a42b865c9d 100644 --- a/newIDE/app/src/QuickCustomization/QuickPublish.module.css +++ b/newIDE/app/src/QuickCustomization/QuickPublish.module.css @@ -1,20 +1,3 @@ -.illustrationImage { - width: 100px; - aspect-ratio: 117 / 162; -} - -.animatedRocket { - animation: animatedRocket ease-in-out 1.2s infinite; -} - -@keyframes animatedRocket { - 0% { transform: rotate(0deg); } - 15% { transform: rotate(4deg); } - 25% { transform: rotate(5deg); } - 35% { transform: rotate(4deg); } - 50% { transform: rotate(0deg); } - 65% { transform: rotate(-4deg); } - 75% { transform: rotate(-5deg); } - 85% { transform: rotate(-4deg); } - 100% { transform: rotate(0deg); } -} +.paperContainer { + padding: 16px; +} \ No newline at end of file diff --git a/newIDE/app/src/QuickCustomization/QuickTitleTweaker.js b/newIDE/app/src/QuickCustomization/QuickTitleTweaker.js new file mode 100644 index 000000000000..75e414a888cb --- /dev/null +++ b/newIDE/app/src/QuickCustomization/QuickTitleTweaker.js @@ -0,0 +1,188 @@ +// @flow +import * as React from 'react'; +import { ColumnStackLayout, ResponsiveLineStackLayout } from '../UI/Layout'; +import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; +import CompactPropertiesEditor from '../CompactPropertiesEditor'; +import propertiesMapToSchema from '../CompactPropertiesEditor/PropertiesMapToCompactSchema'; +import { useForceRecompute } from '../Utils/UseForceUpdate'; +import { Column, Line } from '../UI/Grid'; +import GameImage from './GameImage'; +import Text from '../UI/Text'; +import { Trans } from '@lingui/macro'; + +const gd: libGDevelop = global.gd; + +export const findTitleObject = ( + objectFolderOrObject: gdObjectFolderOrObject +): ?gdObject => { + for (let i = 0; i < objectFolderOrObject.getChildrenCount(); i++) { + const child = objectFolderOrObject.getChildAt(i); + + if (child.isFolder()) { + const foundTitleObject = findTitleObject(child); + if (foundTitleObject) { + return foundTitleObject; + } + } else { + const object = child.getObject(); + if (object.getName() === 'Title') { + return object; + } + } + } + + return null; +}; + +const QuickObjectPropertiesEditor = ({ + project, + object, + objectConfiguration, + onObjectUpdated, + resourceManagementProps, +}: {| + project: gdProject, + object: gdObject, + objectConfiguration: gdObjectConfiguration, + onObjectUpdated: () => void, + resourceManagementProps: ResourceManagementProps, +|}) => { + const [schemaRecomputeTrigger, forceRecomputeSchema] = useForceRecompute(); + + // Properties: + const basicPropertiesSchema = React.useMemo( + () => { + if (schemaRecomputeTrigger) { + // schemaRecomputeTrigger allows to invalidate the schema when required. + } + const properties = objectConfiguration.getProperties(); + const schema = propertiesMapToSchema({ + properties, + getProperties: object => object.getProperties(), + onUpdateProperty: (object, name, value) => + object.updateProperty(name, value), + object, + visibility: 'Basic-Quick', + }); + + return schema; + }, + [objectConfiguration, schemaRecomputeTrigger, object] + ); + + return ( + + + + + + + ); +}; + +type Props = {| + project: gdProject, + resourceManagementProps: ResourceManagementProps, +|}; + +export const QuickTitleTweaker = ({ + project, + resourceManagementProps, +}: Props) => { + const titleObject = React.useMemo( + () => { + for (let i = 0; i < project.getLayoutsCount(); i++) { + const layout = project.getLayoutAt(i); + const titleObject = findTitleObject( + layout.getObjects().getRootFolder() + ); + + if (titleObject) { + return titleObject; + } + } + + return null; + }, + [project] + ); + + const titleObjectConfiguration = React.useMemo( + () => { + if (!titleObject) { + return null; + } + + const objectConfiguration = titleObject.getConfiguration(); + // TODO: Workaround a bad design of ObjectJsImplementation. When getProperties + // and associated methods are redefined in JS, they have different arguments ( + // see ObjectJsImplementation C++ implementation). If called directly here from JS, + // the arguments will be mismatched. To workaround this, always cast the object to + // a base gdObject to ensure C++ methods are called. + const objectConfigurationAsGd = gd.castObject( + objectConfiguration, + gd.ObjectConfiguration + ); + + return objectConfigurationAsGd; + }, + [titleObject] + ); + + const updateProjectName = React.useCallback( + () => { + if (!titleObject || !titleObjectConfiguration) { + return; + } + + const properties = titleObjectConfiguration.getProperties(); + const textProperty = properties.get('text'); + if (!textProperty) { + console.error('Title object does not have a "text" property.'); + return; + } + + const textPropertyValue = textProperty.getValue(); + if (textPropertyValue !== project.getName()) { + project.setName(textPropertyValue); + } + }, + [titleObject, titleObjectConfiguration, project] + ); + + if (!titleObject || !titleObjectConfiguration) { + return ( + + + + + Oops! Looks like this game has no logo set up, you can continue to + the next step. + + + + + ); + } + + return ( + + + + + + ); +}; diff --git a/newIDE/app/src/QuickCustomization/TipCard.js b/newIDE/app/src/QuickCustomization/TipCard.js new file mode 100644 index 000000000000..43d943eee0dc --- /dev/null +++ b/newIDE/app/src/QuickCustomization/TipCard.js @@ -0,0 +1,39 @@ +// @flow +import * as React from 'react'; +import Text from '../UI/Text'; +import { Column, Line } from '../UI/Grid'; +import Paper from '../UI/Paper'; +import Lightbulb from '../UI/CustomSvgIcons/Lightbulb'; +import { ColumnStackLayout } from '../UI/Layout'; + +type Props = {| + title: React.Node, + description: React.Node, +|}; + +const TipCard = ({ title, description }: Props) => { + return ( + + + + + + + + + {title} + + + {description} + + + + + + ); +}; + +export default TipCard; diff --git a/newIDE/app/src/QuickCustomization/index.js b/newIDE/app/src/QuickCustomization/index.js index 9e29a6d3e2f0..12a4d9e5b62f 100644 --- a/newIDE/app/src/QuickCustomization/index.js +++ b/newIDE/app/src/QuickCustomization/index.js @@ -4,52 +4,51 @@ import { QuickObjectReplacer } from './QuickObjectReplacer'; import { QuickBehaviorsTweaker } from './QuickBehaviorsTweaker'; import { type ResourceManagementProps } from '../ResourcesList/ResourceSource'; import { QuickPublish } from './QuickPublish'; -import { ColumnStackLayout, LineStackLayout } from '../UI/Layout'; -import Text from '../UI/Text'; -import FlatButton from '../UI/FlatButton'; import { Trans } from '@lingui/macro'; -import PreviewIcon from '../UI/CustomSvgIcons/Preview'; import { type Exporter } from '../ExportAndShare/ShareDialog'; import { mapFor } from '../Utils/MapFor'; import { canSwapAssetOfObject } from '../AssetStore/AssetSwapper'; import { type GameAndBuildsManager } from '../Utils/UseGameAndBuildsManager'; +import { QuickTitleTweaker } from './QuickTitleTweaker'; const gd: libGDevelop = global.gd; -type StepName = 'replace-objects' | 'tweak-behaviors' | 'publish'; +type StepName = 'replace-objects' | 'tweak-behaviors' | 'game-logo' | 'publish'; type Step = {| name: StepName, canPreview: boolean, title: React.Node, - nextLabel: React.Node, - shouldHideNavigationButtons?: boolean, + nextLabel?: React.Node, |}; const steps: Array = [ { name: 'replace-objects', canPreview: true, - title: Personalize your game objects art, + title: Choose your game art, nextLabel: Next: Tweak Gameplay, }, { name: 'tweak-behaviors', canPreview: true, title: Tweak gameplay, - nextLabel: Next: Try & Publish, + nextLabel: Next: Game logo, + }, + { + name: 'game-logo', + canPreview: true, + title: Make your game logo, + nextLabel: Next, }, { name: 'publish', canPreview: false, - title: Publish and try your game, - nextLabel: Finish, - shouldHideNavigationButtons: true, + title: Save your game, }, ]; export type QuickCustomizationState = {| isNavigationDisabled: boolean, - shouldAutomaticallyStartExport: boolean, step: Step, goToNextStep: () => void, goToPreviousStep: () => void, @@ -60,20 +59,15 @@ export type QuickCustomizationState = {| export const useQuickCustomizationState = ({ onClose, }: { - onClose: () => void, + onClose: () => Promise, }): QuickCustomizationState => { const [stepIndex, setStepIndex] = React.useState(0); const [isNavigationDisabled, setIsNavigationDisabled] = React.useState(false); - const [ - shouldAutomaticallyStartExport, - setShouldAutomaticallyStartExport, - ] = React.useState(true); const step = steps[stepIndex]; return { isNavigationDisabled, - shouldAutomaticallyStartExport, step, goToNextStep: React.useCallback( () => { @@ -88,23 +82,19 @@ export const useQuickCustomizationState = ({ ), goToPreviousStep: React.useCallback( () => { - if (step.name === 'publish') { - setShouldAutomaticallyStartExport(false); - } if (stepIndex !== 0) { setStepIndex(stepIndex - 1); } }, - [step, stepIndex] + [stepIndex] ), - canGoToPreviousStep: stepIndex !== 0, + canGoToPreviousStep: stepIndex !== 0 && stepIndex !== steps.length - 1, setIsNavigationDisabled, }; }; export const enumerateObjectFolderOrObjects = ( - objectFolderOrObject: gdObjectFolderOrObject, - depth: number = 0 + objectFolderOrObject: gdObjectFolderOrObject ): Array<{ folderName: string, objects: Array }> => { const orderedFolderNames: Array = ['']; const folderObjects: { [key: string]: Array } = { @@ -125,7 +115,7 @@ export const enumerateObjectFolderOrObjects = ( folderObjects[folderName] || []); orderedFolderNames.push(folderName); - enumerateObjectFolderOrObjects(child, depth + 1).forEach( + enumerateObjectFolderOrObjects(child).forEach( ({ folderName, objects }) => { currentFolderObjects.push.apply(currentFolderObjects, objects); } @@ -154,8 +144,7 @@ type Props = {| onlineWebExporter: Exporter, onSaveProject: () => Promise, isSavingProject: boolean, - - onClose: () => void, + onClose: () => Promise, onContinueQuickCustomization: () => void, onTryAnotherGame: () => void, |}; @@ -169,43 +158,12 @@ export const renderQuickCustomization = ({ onlineWebExporter, onSaveProject, isSavingProject, - onClose, onContinueQuickCustomization, onTryAnotherGame, }: Props) => { return { title: quickCustomizationState.step.title, - titleRightContent: quickCustomizationState.step.canPreview ? ( - - - Preview your game - - Preview} - onClick={onLaunchPreview} - leftIcon={} - /> - - ) : null, - titleTopContent: quickCustomizationState.step.canPreview ? ( - - - - Preview your game - - Preview} - onClick={onLaunchPreview} - leftIcon={} - /> - - - ) : null, content: ( <> {quickCustomizationState.step.name === 'replace-objects' ? ( @@ -218,6 +176,11 @@ export const renderQuickCustomization = ({ project={project} resourceManagementProps={resourceManagementProps} /> + ) : quickCustomizationState.step.name === 'game-logo' ? ( + ) : quickCustomizationState.step.name === 'publish' ? ( ), + showPreview: quickCustomizationState.step.canPreview, }; }; diff --git a/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js b/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js index 8b0ad2e0113f..7abd8c12de98 100644 --- a/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js +++ b/newIDE/app/src/ResourcesList/CompactResourceSelectorWithThumbnail/index.js @@ -36,12 +36,9 @@ type Props = {| project: gdProject, resourceManagementProps: ResourceManagementProps, resourceKind: ResourceKind, - fallbackResourceKind?: ResourceKind, resourceName: string, defaultNewResourceName?: string, onChange: string => void, - label?: string, - markdownDescription?: ?string, id?: string, |}; @@ -52,9 +49,6 @@ export const CompactResourceSelectorWithThumbnail = ({ resourceName, defaultNewResourceName, onChange, - label, - markdownDescription, - fallbackResourceKind, id, }: Props) => { const resourcesLoader = ResourcesLoader; diff --git a/newIDE/app/src/UI/Accordion.js b/newIDE/app/src/UI/Accordion.js index cbc5ce4dde70..51d88236a503 100644 --- a/newIDE/app/src/UI/Accordion.js +++ b/newIDE/app/src/UI/Accordion.js @@ -135,7 +135,7 @@ type AccordionProps = {| */ export const Accordion = React.forwardRef( (props, ref) => { - const { costlyBody, ...otherProps } = props; + const { costlyBody, noMargin, ...otherProps } = props; const gdevelopTheme = React.useContext(GDevelopThemeContext); return ( @@ -147,12 +147,11 @@ export const Accordion = React.forwardRef( style={{ ...{ border: - !props.noMargin && - `1px solid ${gdevelopTheme.toolbar.separatorColor}`, + !noMargin && `1px solid ${gdevelopTheme.toolbar.separatorColor}`, backgroundColor: gdevelopTheme.paper.backgroundColor.medium, marginLeft: 0, }, - ...(props.noMargin && { + ...(noMargin && { border: `0px`, padding: `0px`, margin: `0px`, diff --git a/newIDE/app/src/UI/CompactToggleField/index.js b/newIDE/app/src/UI/CompactToggleField/index.js index 4c2469a5b22c..b320291758c7 100644 --- a/newIDE/app/src/UI/CompactToggleField/index.js +++ b/newIDE/app/src/UI/CompactToggleField/index.js @@ -1,24 +1,9 @@ // @flow import * as React from 'react'; -import Tooltip from '@material-ui/core/Tooltip'; -import Text from '../../UI/Text'; -import { MarkdownText } from '../../UI/MarkdownText'; -import { tooltipEnterDelay } from '../../UI/Tooltip'; import classes from './CompactToggleField.module.css'; import classNames from 'classnames'; -const styles = { - label: { - overflow: 'hidden', - textOverflow: 'ellipsis', - lineHeight: '17px', - maxHeight: 34, // 2 * lineHeight to limit to 2 lines. - opacity: 0.7, - }, -}; type Props = {| - label: string, - markdownDescription?: ?string, id?: string, checked: boolean, onCheck: (newValue: boolean) => void, @@ -27,9 +12,6 @@ type Props = {| |}; export const CompactToggleField = (props: Props) => { - const title = !props.markdownDescription - ? props.label - : [props.label, ' - ', ]; return (