mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
28 Commits
v5.2.169
...
better-sto
Author | SHA1 | Date | |
---|---|---|---|
![]() |
08ddaf35ae | ||
![]() |
134886eedc | ||
![]() |
da308bb104 | ||
![]() |
5087526066 | ||
![]() |
b6f98ad667 | ||
![]() |
b4a6b73146 | ||
![]() |
2ccdc1aad8 | ||
![]() |
68f13297ef | ||
![]() |
6a26e2cf32 | ||
![]() |
9532a42558 | ||
![]() |
f23bc5dfd9 | ||
![]() |
4a4bf6d761 | ||
![]() |
68968b603a | ||
![]() |
eb723b2a0e | ||
![]() |
c51e6fa04e | ||
![]() |
a0ad9200cf | ||
![]() |
99804f366a | ||
![]() |
71fead702d | ||
![]() |
54c9177b03 | ||
![]() |
8157d3c9db | ||
![]() |
03e0da8619 | ||
![]() |
be84b2153f | ||
![]() |
10591a41e6 | ||
![]() |
d41cc18be2 | ||
![]() |
fb6e09d0e3 | ||
![]() |
8766f73333 | ||
![]() |
b32a9006c6 | ||
![]() |
2d613e7281 |
@@ -19,8 +19,10 @@
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "GDCore/Tools/MakeUnique.h"
|
||||
#include "GrammarTerminals.h"
|
||||
|
||||
using namespace std;
|
||||
using namespace gd::GrammarTerminals;
|
||||
|
||||
namespace gd {
|
||||
|
||||
|
@@ -18,6 +18,7 @@
|
||||
#include "GDCore/String.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "GDCore/Tools/MakeUnique.h"
|
||||
#include "GrammarTerminals.h"
|
||||
namespace gd {
|
||||
class Expression;
|
||||
class ObjectsContainer;
|
||||
@@ -28,6 +29,8 @@ class ExpressionMetadata;
|
||||
|
||||
namespace gd {
|
||||
|
||||
using namespace gd::GrammarTerminals;
|
||||
|
||||
/** \brief Parse an expression, returning a tree of node corresponding
|
||||
* to the parsed expression.
|
||||
*
|
||||
@@ -211,7 +214,7 @@ class GD_CORE_API ExpressionParser2 {
|
||||
}
|
||||
SkipIfChar(IsClosingParenthesis);
|
||||
return factor;
|
||||
} else if (IsIdentifierAllowedChar()) {
|
||||
} else if (CheckIfChar(IsAllowedInIdentifier)) {
|
||||
return Identifier();
|
||||
}
|
||||
|
||||
@@ -606,95 +609,6 @@ class GD_CORE_API ExpressionParser2 {
|
||||
return predicate(character);
|
||||
}
|
||||
|
||||
bool IsIdentifierAllowedChar() {
|
||||
if (currentPosition >= expression.size()) return false;
|
||||
gd::String::value_type character = expression[currentPosition];
|
||||
|
||||
// Quickly compare if the character is a number or ASCII character.
|
||||
if ((character >= '0' && character <= '9') ||
|
||||
(character >= 'A' && character <= 'Z') ||
|
||||
(character >= 'a' && character <= 'z'))
|
||||
return true;
|
||||
|
||||
// Otherwise do the full check against separators forbidden in identifiers.
|
||||
if (!IsParameterSeparator(character) && !IsDot(character) &&
|
||||
!IsQuote(character) && !IsBracket(character) &&
|
||||
!IsExpressionOperator(character) && !IsTermOperator(character)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool IsWhitespace(gd::String::value_type character) {
|
||||
return character == ' ' || character == '\n' || character == '\r';
|
||||
}
|
||||
|
||||
static bool IsParameterSeparator(gd::String::value_type character) {
|
||||
return character == ',';
|
||||
}
|
||||
|
||||
static bool IsDot(gd::String::value_type character) {
|
||||
return character == '.';
|
||||
}
|
||||
|
||||
static bool IsQuote(gd::String::value_type character) {
|
||||
return character == '"';
|
||||
}
|
||||
|
||||
static bool IsBracket(gd::String::value_type character) {
|
||||
return character == '(' || character == ')' || character == '[' ||
|
||||
character == ']' || character == '{' || character == '}';
|
||||
}
|
||||
|
||||
static bool IsOpeningParenthesis(gd::String::value_type character) {
|
||||
return character == '(';
|
||||
}
|
||||
|
||||
static bool IsClosingParenthesis(gd::String::value_type character) {
|
||||
return character == ')';
|
||||
}
|
||||
|
||||
static bool IsOpeningSquareBracket(gd::String::value_type character) {
|
||||
return character == '[';
|
||||
}
|
||||
|
||||
static bool IsClosingSquareBracket(gd::String::value_type character) {
|
||||
return character == ']';
|
||||
}
|
||||
|
||||
static bool IsExpressionEndingChar(gd::String::value_type character) {
|
||||
return character == ',' || IsClosingParenthesis(character) ||
|
||||
IsClosingSquareBracket(character);
|
||||
}
|
||||
|
||||
static bool IsExpressionOperator(gd::String::value_type character) {
|
||||
return character == '+' || character == '-' || character == '<' ||
|
||||
character == '>' || character == '?' || character == '^' ||
|
||||
character == '=' || character == '\\' || character == ':' ||
|
||||
character == '!';
|
||||
}
|
||||
|
||||
static bool IsUnaryOperator(gd::String::value_type character) {
|
||||
return character == '+' || character == '-';
|
||||
}
|
||||
|
||||
static bool IsTermOperator(gd::String::value_type character) {
|
||||
return character == '/' || character == '*';
|
||||
}
|
||||
|
||||
static bool IsNumberFirstChar(gd::String::value_type character) {
|
||||
return character == '.' || (character >= '0' && character <= '9');
|
||||
}
|
||||
|
||||
static bool IsNonZeroDigit(gd::String::value_type character) {
|
||||
return (character >= '1' && character <= '9');
|
||||
}
|
||||
|
||||
static bool IsZeroDigit(gd::String::value_type character) {
|
||||
return character == '0';
|
||||
}
|
||||
|
||||
bool IsNamespaceSeparator() {
|
||||
// Namespace separator is a special kind of delimiter as it is 2 characters
|
||||
// long
|
||||
@@ -715,7 +629,7 @@ class GD_CORE_API ExpressionParser2 {
|
||||
gd::String name;
|
||||
size_t startPosition = currentPosition;
|
||||
while (currentPosition < expression.size() &&
|
||||
(IsIdentifierAllowedChar()
|
||||
(CheckIfChar(IsAllowedInIdentifier)
|
||||
// Allow whitespace in identifier name for compatibility
|
||||
|| expression[currentPosition] == ' ')) {
|
||||
name += expression[currentPosition];
|
||||
|
107
Core/GDCore/Events/Parsers/GrammarTerminals.h
Normal file
107
Core/GDCore/Events/Parsers/GrammarTerminals.h
Normal file
@@ -0,0 +1,107 @@
|
||||
#pragma once
|
||||
#include "GDCore/String.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
/**
|
||||
* Contains functions to handle the grammar of the expressions accepted by GDevelop.
|
||||
*/
|
||||
namespace GrammarTerminals {
|
||||
|
||||
inline bool IsWhitespace(gd::String::value_type character) {
|
||||
return character == ' ' || character == '\n' || character == '\r';
|
||||
}
|
||||
|
||||
inline bool IsParameterSeparator(gd::String::value_type character) {
|
||||
return character == ',';
|
||||
}
|
||||
|
||||
inline bool IsDot(gd::String::value_type character) { return character == '.'; }
|
||||
|
||||
inline bool IsQuote(gd::String::value_type character) {
|
||||
return character == '"';
|
||||
}
|
||||
|
||||
inline bool IsBracket(gd::String::value_type character) {
|
||||
return character == '(' || character == ')' || character == '[' ||
|
||||
character == ']' || character == '{' || character == '}';
|
||||
}
|
||||
|
||||
inline bool IsOpeningParenthesis(gd::String::value_type character) {
|
||||
return character == '(';
|
||||
}
|
||||
|
||||
inline bool IsClosingParenthesis(gd::String::value_type character) {
|
||||
return character == ')';
|
||||
}
|
||||
|
||||
inline bool IsOpeningSquareBracket(gd::String::value_type character) {
|
||||
return character == '[';
|
||||
}
|
||||
|
||||
inline bool IsClosingSquareBracket(gd::String::value_type character) {
|
||||
return character == ']';
|
||||
}
|
||||
|
||||
inline bool IsExpressionEndingChar(gd::String::value_type character) {
|
||||
return character == ',' || IsClosingParenthesis(character) ||
|
||||
IsClosingSquareBracket(character);
|
||||
}
|
||||
|
||||
inline bool IsExpressionOperator(gd::String::value_type character) {
|
||||
return character == '+' || character == '-' || character == '<' ||
|
||||
character == '>' || character == '?' || character == '^' ||
|
||||
character == '=' || character == '\\' || character == ':' ||
|
||||
character == '!';
|
||||
}
|
||||
|
||||
inline bool IsUnaryOperator(gd::String::value_type character) {
|
||||
return character == '+' || character == '-';
|
||||
}
|
||||
|
||||
inline bool IsTermOperator(gd::String::value_type character) {
|
||||
return character == '/' || character == '*';
|
||||
}
|
||||
|
||||
inline bool IsNumberFirstChar(gd::String::value_type character) {
|
||||
return character == '.' || (character >= '0' && character <= '9');
|
||||
}
|
||||
|
||||
inline bool IsNonZeroDigit(gd::String::value_type character) {
|
||||
return (character >= '1' && character <= '9');
|
||||
}
|
||||
|
||||
inline bool IsZeroDigit(gd::String::value_type character) {
|
||||
return character == '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given character can be used in an identifier. This is
|
||||
* any unicode character, except for:
|
||||
* `, . " () [] {} + - < > ? ^ = \ : ! / *` and whitespaces (space, line break, carriage return).
|
||||
*
|
||||
* This is loosely based on what is allowed in languages like JavaScript
|
||||
* (see https://mathiasbynens.be/notes/javascript-properties), without support
|
||||
* for unicode escape syntax, and allowing all unicode ranges. The only
|
||||
* disallowed characters are the one used for the grammar.
|
||||
*/
|
||||
inline bool IsAllowedInIdentifier(gd::String::value_type character) {
|
||||
// Quickly compare if the character is a number or ASCII character.
|
||||
if ((character >= '0' && character <= '9') ||
|
||||
(character >= 'A' && character <= 'Z') ||
|
||||
(character >= 'a' && character <= 'z'))
|
||||
return true;
|
||||
|
||||
// Otherwise do the full check against separators forbidden in identifiers.
|
||||
if (!IsParameterSeparator(character) && !IsDot(character) &&
|
||||
!IsQuote(character) && !IsBracket(character) &&
|
||||
!IsExpressionOperator(character) && !IsTermOperator(character) &&
|
||||
!IsWhitespace(character)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace GrammarTerminals
|
||||
} // namespace gd
|
@@ -421,6 +421,32 @@ class GD_CORE_API PlatformExtension {
|
||||
*/
|
||||
const gd::String& GetIconUrl() const { return iconUrl; }
|
||||
|
||||
/**
|
||||
* \brief Return keywords that help search engines find this extension.
|
||||
*/
|
||||
const std::vector<gd::String>& GetTags() const { return tags; }
|
||||
|
||||
/**
|
||||
* \brief Set keywords that help search engines find this extension.
|
||||
*/
|
||||
PlatformExtension& SetTags(const gd::String& csvTags) {
|
||||
tags.clear();
|
||||
tags = csvTags.Split(',');
|
||||
for (size_t i = 0; i < tags.size(); i++)
|
||||
{
|
||||
tags[i] = tags[i].Trim().LowerCase();
|
||||
}
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Add a keyword that help search engines find this extension.
|
||||
*/
|
||||
PlatformExtension& AddTag(const gd::String& tag) {
|
||||
tags.push_back(tag);
|
||||
return *this;
|
||||
}
|
||||
|
||||
/**
|
||||
* \brief Check if the extension is flagged as being deprecated.
|
||||
*/
|
||||
@@ -661,6 +687,7 @@ private:
|
||||
gd::String helpPath; ///< The relative path to the help for this extension in
|
||||
///< the documentation.
|
||||
gd::String iconUrl; ///< The URL to the icon to be shown for this extension.
|
||||
std::vector<gd::String> tags;
|
||||
|
||||
std::map<gd::String, gd::ObjectMetadata> objectsInfos;
|
||||
std::map<gd::String, gd::BehaviorMetadata> behaviorsInfo;
|
||||
|
@@ -17,9 +17,11 @@
|
||||
#include "GDCore/Extensions/Metadata/ParameterMetadataTools.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Project/ResourcesManager.h"
|
||||
#include "GDCore/Project/Effect.h"
|
||||
#include "GDCore/Tools/Log.h"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -131,29 +133,9 @@ void ArbitraryResourceWorker::ExposeEmbeddeds(gd::String& resourceName) {
|
||||
std::cout << targetResourceName << std::endl;
|
||||
gd::Resource& targetResource =
|
||||
resourcesManager->GetResource(targetResourceName);
|
||||
const gd::String& targetResourceKind = targetResource.GetKind();
|
||||
|
||||
gd::String potentiallyUpdatedTargetResourceName = targetResourceName;
|
||||
|
||||
if (targetResourceKind == "audio") {
|
||||
ExposeAudio(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "bitmapFont") {
|
||||
ExposeBitmapFont(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "font") {
|
||||
ExposeFont(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "image") {
|
||||
ExposeImage(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "json") {
|
||||
ExposeJson(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "tilemap") {
|
||||
ExposeTilemap(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "tileset") {
|
||||
ExposeTileset(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "video") {
|
||||
ExposeVideo(potentiallyUpdatedTargetResourceName);
|
||||
} else if (targetResourceKind == "model3D") {
|
||||
ExposeModel3D(potentiallyUpdatedTargetResourceName);
|
||||
}
|
||||
ExposeResourceWithType(targetResource.GetKind(), potentiallyUpdatedTargetResourceName);
|
||||
|
||||
if (potentiallyUpdatedTargetResourceName != targetResourceName) {
|
||||
// The resource name was renamed. Also update the mapping.
|
||||
@@ -170,6 +152,48 @@ void ArbitraryResourceWorker::ExposeEmbeddeds(gd::String& resourceName) {
|
||||
}
|
||||
}
|
||||
|
||||
void ArbitraryResourceWorker::ExposeResourceWithType(
|
||||
const gd::String &resourceType, gd::String &resourceName) {
|
||||
if (resourceType == "image") {
|
||||
ExposeImage(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "model3D") {
|
||||
ExposeModel3D(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "audio") {
|
||||
ExposeAudio(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "font") {
|
||||
ExposeFont(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "bitmapFont") {
|
||||
ExposeBitmapFont(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "tilemap") {
|
||||
ExposeTilemap(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "tileset") {
|
||||
ExposeTileset(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "json") {
|
||||
ExposeJson(resourceName);
|
||||
return;
|
||||
}
|
||||
if (resourceType == "video") {
|
||||
ExposeVideo(resourceName);
|
||||
return;
|
||||
}
|
||||
gd::LogError("Unexpected resource type: " + resourceType + " for: " + resourceName);
|
||||
return;
|
||||
}
|
||||
|
||||
void ArbitraryResourceWorker::ExposeResource(gd::Resource& resource) {
|
||||
if (!resource.UseFile()) return;
|
||||
|
||||
@@ -180,86 +204,98 @@ void ArbitraryResourceWorker::ExposeResource(gd::Resource& resource) {
|
||||
|
||||
ArbitraryResourceWorker::~ArbitraryResourceWorker() {}
|
||||
|
||||
/**
|
||||
* Launch the specified resource worker on every resource referenced in the
|
||||
* events.
|
||||
*/
|
||||
class ResourceWorkerInEventsWorker : public ArbitraryEventsWorker {
|
||||
public:
|
||||
ResourceWorkerInEventsWorker(const gd::Project& project_,
|
||||
gd::ArbitraryResourceWorker& worker_)
|
||||
: project(project_), worker(worker_){};
|
||||
virtual ~ResourceWorkerInEventsWorker(){};
|
||||
bool ResourceWorkerInEventsWorker::DoVisitInstruction(gd::Instruction& instruction, bool isCondition) {
|
||||
const auto& platform = project.GetCurrentPlatform();
|
||||
const auto& metadata = isCondition
|
||||
? gd::MetadataProvider::GetConditionMetadata(
|
||||
platform, instruction.GetType())
|
||||
: gd::MetadataProvider::GetActionMetadata(
|
||||
platform, instruction.GetType());
|
||||
|
||||
private:
|
||||
bool DoVisitInstruction(gd::Instruction& instruction, bool isCondition) {
|
||||
const auto& platform = project.GetCurrentPlatform();
|
||||
const auto& metadata = isCondition
|
||||
? gd::MetadataProvider::GetConditionMetadata(
|
||||
platform, instruction.GetType())
|
||||
: gd::MetadataProvider::GetActionMetadata(
|
||||
platform, instruction.GetType());
|
||||
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
|
||||
instruction.GetParameters(),
|
||||
metadata.GetParameters(),
|
||||
[this, &instruction](const gd::ParameterMetadata& parameterMetadata,
|
||||
const gd::Expression& parameterExpression,
|
||||
size_t parameterIndex,
|
||||
const gd::String& lastObjectName) {
|
||||
const String& parameterValue = parameterExpression.GetPlainString();
|
||||
if (parameterMetadata.GetType() ==
|
||||
"police" || // Should be renamed fontResource
|
||||
parameterMetadata.GetType() == "fontResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeFont(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "soundfile" ||
|
||||
parameterMetadata.GetType() ==
|
||||
"musicfile") { // Should be renamed audioResource
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeAudio(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "bitmapFontResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeBitmapFont(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "imageResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeImage(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "jsonResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeJson(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "tilemapResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeTilemap(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "tilesetResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeTileset(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "model3DResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeModel3D(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
}
|
||||
});
|
||||
|
||||
gd::ParameterMetadataTools::IterateOverParametersWithIndex(
|
||||
instruction.GetParameters(),
|
||||
metadata.GetParameters(),
|
||||
[this, &instruction](const gd::ParameterMetadata& parameterMetadata,
|
||||
const gd::Expression& parameterExpression,
|
||||
size_t parameterIndex,
|
||||
const gd::String& lastObjectName) {
|
||||
const String& parameterValue = parameterExpression.GetPlainString();
|
||||
if (parameterMetadata.GetType() ==
|
||||
"police" || // Should be renamed fontResource
|
||||
parameterMetadata.GetType() == "fontResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeFont(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "soundfile" ||
|
||||
parameterMetadata.GetType() ==
|
||||
"musicfile") { // Should be renamed audioResource
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeAudio(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "bitmapFontResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeBitmapFont(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "imageResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeImage(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "jsonResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeJson(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "tilemapResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeTilemap(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "tilesetResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeTileset(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
} else if (parameterMetadata.GetType() == "model3DResource") {
|
||||
gd::String updatedParameterValue = parameterValue;
|
||||
worker.ExposeModel3D(updatedParameterValue);
|
||||
instruction.SetParameter(parameterIndex, updatedParameterValue);
|
||||
}
|
||||
});
|
||||
|
||||
return false;
|
||||
};
|
||||
|
||||
const gd::Project& project;
|
||||
gd::ArbitraryResourceWorker& worker;
|
||||
return false;
|
||||
};
|
||||
|
||||
void LaunchResourceWorkerOnEvents(const gd::Project& project,
|
||||
gd::EventsList& events,
|
||||
gd::ArbitraryResourceWorker& worker) {
|
||||
ResourceWorkerInEventsWorker eventsWorker(project, worker);
|
||||
gd::ResourceWorkerInEventsWorker eventsWorker(project, worker);
|
||||
eventsWorker.Launch(events);
|
||||
}
|
||||
|
||||
gd::ResourceWorkerInEventsWorker
|
||||
GetResourceWorkerOnEvents(const gd::Project &project,
|
||||
gd::ArbitraryResourceWorker &worker) {
|
||||
gd::ResourceWorkerInEventsWorker eventsWorker(project, worker);
|
||||
return eventsWorker;
|
||||
}
|
||||
|
||||
void ResourceWorkerInObjectsWorker::DoVisitObject(gd::Object &object) {
|
||||
object.GetConfiguration().ExposeResources(worker);
|
||||
auto& effects = object.GetEffects();
|
||||
for (size_t effectIndex = 0; effectIndex < effects.GetEffectsCount(); effectIndex++)
|
||||
{
|
||||
auto& effect = effects.GetEffect(effectIndex);
|
||||
gd::ResourceExposer::ExposeEffectResources(project.GetCurrentPlatform(), effect, worker);
|
||||
}
|
||||
};
|
||||
|
||||
void ResourceWorkerInObjectsWorker::DoVisitBehavior(gd::Behavior &behavior){
|
||||
// TODO Allow behaviors to expose resources
|
||||
};
|
||||
|
||||
gd::ResourceWorkerInObjectsWorker
|
||||
GetResourceWorkerOnObjects(const gd::Project &project,
|
||||
gd::ArbitraryResourceWorker &worker) {
|
||||
gd::ResourceWorkerInObjectsWorker eventsWorker(project, worker);
|
||||
return eventsWorker;
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
#endif
|
||||
|
@@ -4,13 +4,14 @@
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#ifndef ARBITRARYRESOURCEWORKER_H
|
||||
#define ARBITRARYRESOURCEWORKER_H
|
||||
#pragma once
|
||||
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include "GDCore/String.h"
|
||||
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryObjectsWorker.h"
|
||||
namespace gd {
|
||||
class BaseEvent;
|
||||
}
|
||||
@@ -53,6 +54,11 @@ class GD_CORE_API ArbitraryResourceWorker {
|
||||
*/
|
||||
void ExposeResources(gd::ResourcesManager *resourcesManager);
|
||||
|
||||
/**
|
||||
* \brief Expose a resource from a given type.
|
||||
*/
|
||||
void ExposeResourceWithType(const gd::String& resourceType, gd::String& resourceName);
|
||||
|
||||
/**
|
||||
* \brief Expose an image, which is always a reference to a "image" resource.
|
||||
*/
|
||||
@@ -132,18 +138,47 @@ class GD_CORE_API ArbitraryResourceWorker {
|
||||
};
|
||||
|
||||
/**
|
||||
* Tool function iterating over each event and calling
|
||||
* Expose(Actions/Conditions)Resources for each actions and conditions with the
|
||||
* ArbitraryResourceWorker passed as argument.
|
||||
*
|
||||
* \see gd::ArbitraryResourceWorker
|
||||
* \ingroup IDE
|
||||
* Launch the specified resource worker on every resource referenced in the
|
||||
* events.
|
||||
*/
|
||||
void GD_CORE_API
|
||||
LaunchResourceWorkerOnEvents(const gd::Project &project,
|
||||
gd::EventsList &events,
|
||||
gd::ArbitraryResourceWorker &worker);
|
||||
class ResourceWorkerInEventsWorker : public gd::ArbitraryEventsWorker {
|
||||
public:
|
||||
ResourceWorkerInEventsWorker(const gd::Project &project_,
|
||||
gd::ArbitraryResourceWorker &worker_)
|
||||
: project(project_), worker(worker_){};
|
||||
virtual ~ResourceWorkerInEventsWorker(){};
|
||||
|
||||
private:
|
||||
bool DoVisitInstruction(gd::Instruction &instruction,
|
||||
bool isCondition) override;
|
||||
|
||||
const gd::Project &project;
|
||||
gd::ArbitraryResourceWorker &worker;
|
||||
};
|
||||
|
||||
ResourceWorkerInEventsWorker GD_CORE_API GetResourceWorkerOnEvents(
|
||||
const gd::Project &project, gd::ArbitraryResourceWorker &worker);
|
||||
|
||||
/**
|
||||
* Launch the specified resource worker on every resource referenced in the
|
||||
* objects.
|
||||
*/
|
||||
class GD_CORE_API ResourceWorkerInObjectsWorker
|
||||
: public gd::ArbitraryObjectsWorker {
|
||||
public:
|
||||
ResourceWorkerInObjectsWorker(const gd::Project &project_, gd::ArbitraryResourceWorker &worker_)
|
||||
: project(project_), worker(worker_){};
|
||||
~ResourceWorkerInObjectsWorker() {}
|
||||
|
||||
private:
|
||||
void DoVisitObject(gd::Object &object) override;
|
||||
void DoVisitBehavior(gd::Behavior &behavior) override;
|
||||
|
||||
const gd::Project &project;
|
||||
gd::ArbitraryResourceWorker &worker;
|
||||
};
|
||||
|
||||
gd::ResourceWorkerInObjectsWorker GD_CORE_API
|
||||
GetResourceWorkerOnObjects(const gd::Project &project, gd::ArbitraryResourceWorker &worker);
|
||||
|
||||
} // namespace gd
|
||||
|
||||
#endif // ARBITRARYRESOURCEWORKER_H
|
||||
|
@@ -9,6 +9,7 @@
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "GDCore/Tools/Log.h"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -19,7 +20,7 @@ std::vector<gd::String> ProjectResourcesAdder::GetAllUseless(
|
||||
std::vector<gd::String> unusedResources;
|
||||
// Search for resources used in the project
|
||||
gd::ResourcesInUseHelper resourcesInUse;
|
||||
project.ExposeResources(resourcesInUse);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, resourcesInUse);
|
||||
std::set<gd::String>& usedResources = resourcesInUse.GetAll(resourceType);
|
||||
|
||||
// Search all resources not used
|
||||
|
@@ -12,6 +12,7 @@
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Tools/Localization.h"
|
||||
#include "GDCore/Tools/Log.h"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
|
||||
using namespace std;
|
||||
|
||||
@@ -26,7 +27,7 @@ bool ProjectResourcesCopier::CopyAllResourcesTo(
|
||||
bool preserveDirectoryStructure) {
|
||||
// Check if there are some resources with absolute filenames
|
||||
gd::ResourcesAbsolutePathChecker absolutePathChecker(fs);
|
||||
originalProject.ExposeResources(absolutePathChecker);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(originalProject, absolutePathChecker);
|
||||
|
||||
auto projectDirectory = fs.DirNameFrom(originalProject.GetProjectFile());
|
||||
std::cout << "Copying all resources from " << projectDirectory << " to "
|
||||
@@ -41,10 +42,10 @@ bool ProjectResourcesCopier::CopyAllResourcesTo(
|
||||
preserveAbsoluteFilenames);
|
||||
|
||||
if (updateOriginalProject) {
|
||||
originalProject.ExposeResources(resourcesMergingHelper);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(originalProject, resourcesMergingHelper);
|
||||
} else {
|
||||
std::shared_ptr<gd::Project> project(new gd::Project(originalProject));
|
||||
project->ExposeResources(resourcesMergingHelper);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(*project, resourcesMergingHelper);
|
||||
}
|
||||
|
||||
// Copy resources
|
||||
|
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
|
||||
#include "GDCore/String.h"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
@@ -23,7 +24,7 @@ namespace gd {
|
||||
* Usage example:
|
||||
\code
|
||||
gd::ResourcesInUseHelper resourcesInUse;
|
||||
project.ExposeResources(resourcesInUse);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, resourcesInUse);
|
||||
|
||||
//Get a set with the name of all images in the project:
|
||||
std::set<gd::String> & usedImages = resourcesInUse.GetAllImages();
|
||||
|
@@ -197,7 +197,7 @@ void ProjectBrowserHelper::ExposeProjectObjects(
|
||||
// Global objects
|
||||
worker.Launch(project);
|
||||
|
||||
// Layers objects
|
||||
// Layout objects
|
||||
for (size_t i = 0; i < project.GetLayoutsCount(); i++) {
|
||||
worker.Launch(project.GetLayout(i));
|
||||
}
|
||||
|
101
Core/GDCore/IDE/ResourceExposer.cpp
Normal file
101
Core/GDCore/IDE/ResourceExposer.cpp
Normal file
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#include "ResourceExposer.h"
|
||||
|
||||
#include "GDCore/IDE/Events/ArbitraryEventsWorker.h"
|
||||
#include "GDCore/IDE/EventsFunctionTools.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryBehaviorSharedDataWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryEventBasedBehaviorsWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryEventsFunctionsWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryObjectsWorker.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
|
||||
#include "GDCore/IDE/ProjectBrowserHelper.h"
|
||||
#include "GDCore/Project/EventsBasedBehavior.h"
|
||||
#include "GDCore/Project/EventsBasedObject.h"
|
||||
#include "GDCore/Project/EventsFunctionsExtension.h"
|
||||
#include "GDCore/Project/ExternalEvents.h"
|
||||
#include "GDCore/Project/Layout.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "GDCore/Project/Effect.h"
|
||||
#include "GDCore/String.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
|
||||
#include "GDCore/Extensions/Metadata/EffectMetadata.h"
|
||||
|
||||
namespace gd {
|
||||
|
||||
void ResourceExposer::ExposeWholeProjectResources(gd::Project& project, gd::ArbitraryResourceWorker& worker) {
|
||||
// See also gd::ProjectBrowserHelper::ExposeProjectEvents for a method that
|
||||
// traverse the whole project (this time for events) and ExposeProjectEffects
|
||||
// (this time for effects).
|
||||
|
||||
gd::ResourcesManager* resourcesManager = &(project.GetResourcesManager());
|
||||
|
||||
// Expose any project resources as files.
|
||||
worker.ExposeResources(resourcesManager);
|
||||
project.GetPlatformSpecificAssets().ExposeResources(worker);
|
||||
|
||||
// Expose event resources
|
||||
auto eventWorker = gd::GetResourceWorkerOnEvents(project, worker);
|
||||
gd::ProjectBrowserHelper::ExposeProjectEvents(
|
||||
project, eventWorker);
|
||||
|
||||
// Expose object configuration resources
|
||||
auto objectWorker = gd::GetResourceWorkerOnObjects(project, worker);
|
||||
gd::ProjectBrowserHelper::ExposeProjectObjects(
|
||||
project, objectWorker);
|
||||
|
||||
// Expose layer effect resources
|
||||
for (std::size_t layoutIndex = 0; layoutIndex < project.GetLayoutsCount();
|
||||
layoutIndex++) {
|
||||
auto &layout = project.GetLayout(layoutIndex);
|
||||
|
||||
for (std::size_t layerIndex = 0; layerIndex < layout.GetLayersCount();
|
||||
layerIndex++) {
|
||||
auto &layer = layout.GetLayer(layerIndex);
|
||||
|
||||
auto &effects = layer.GetEffects();
|
||||
for (size_t effectIndex = 0; effectIndex < effects.GetEffectsCount();
|
||||
effectIndex++) {
|
||||
auto &effect = effects.GetEffect(effectIndex);
|
||||
gd::ResourceExposer::ExposeEffectResources(project.GetCurrentPlatform(),
|
||||
effect, worker);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Expose loading screen background image if present
|
||||
auto& loadingScreen = project.GetLoadingScreen();
|
||||
if (loadingScreen.GetBackgroundImageResourceName() != "")
|
||||
worker.ExposeImage(loadingScreen.GetBackgroundImageResourceName());
|
||||
}
|
||||
|
||||
void ResourceExposer::ExposeEffectResources(
|
||||
gd::Platform &platform, gd::Effect &effect,
|
||||
gd::ArbitraryResourceWorker &worker) {
|
||||
auto &effectMetadata =
|
||||
MetadataProvider::GetEffectMetadata(platform, effect.GetEffectType());
|
||||
|
||||
for (auto &propertyPair : effectMetadata.GetProperties()) {
|
||||
auto &propertyName = propertyPair.first;
|
||||
auto &propertyDescriptor = propertyPair.second;
|
||||
|
||||
if (propertyDescriptor.GetType() == "resource" &&
|
||||
propertyDescriptor.GetExtraInfo().size() > 0) {
|
||||
auto &resourceType = propertyDescriptor.GetExtraInfo()[0];
|
||||
|
||||
const gd::String &resourceName = effect.GetStringParameter(propertyName);
|
||||
gd::String potentiallyUpdatedResourceName = resourceName;
|
||||
worker.ExposeResourceWithType(resourceType,
|
||||
potentiallyUpdatedResourceName);
|
||||
if (potentiallyUpdatedResourceName != resourceName) {
|
||||
effect.SetStringParameter(propertyName, potentiallyUpdatedResourceName);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace gd
|
38
Core/GDCore/IDE/ResourceExposer.h
Normal file
38
Core/GDCore/IDE/ResourceExposer.h
Normal file
@@ -0,0 +1,38 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2016 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
#pragma once
|
||||
|
||||
namespace gd {
|
||||
class Platform;
|
||||
class Project;
|
||||
class ArbitraryResourceWorker;
|
||||
class Effect;
|
||||
} // namespace gd
|
||||
|
||||
namespace gd {
|
||||
|
||||
/**
|
||||
* \brief
|
||||
*/
|
||||
class GD_CORE_API ResourceExposer {
|
||||
public:
|
||||
/**
|
||||
* \brief Called ( e.g. during compilation ) so as to inventory internal
|
||||
* resources, sometimes update their filename or any other work or resources.
|
||||
*
|
||||
* See WholeProjectRefactorer for the same thing for events.
|
||||
*
|
||||
* \see WholeProjectRefactorer
|
||||
* \see ArbitraryResourceWorker
|
||||
*/
|
||||
static void ExposeWholeProjectResources(gd::Project &project,
|
||||
gd::ArbitraryResourceWorker &worker);
|
||||
|
||||
static void ExposeEffectResources(gd::Platform &platform, gd::Effect &effect,
|
||||
gd::ArbitraryResourceWorker &worker);
|
||||
};
|
||||
|
||||
} // namespace gd
|
@@ -1599,6 +1599,54 @@ void WholeProjectRefactorer::GlobalObjectOrGroupRemoved(
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::RemoveLayer(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
const gd::String &layerName) {
|
||||
if (layerName.empty())
|
||||
return;
|
||||
|
||||
layout.GetInitialInstances().RemoveAllInstancesOnLayer(layerName);
|
||||
|
||||
std::vector<gd::String> externalLayoutsNames =
|
||||
GetAssociatedExternalLayouts(project, layout);
|
||||
for (gd::String name : externalLayoutsNames) {
|
||||
auto &externalLayout = project.GetExternalLayout(name);
|
||||
externalLayout.GetInitialInstances().RemoveAllInstancesOnLayer(layerName);
|
||||
}
|
||||
}
|
||||
|
||||
void WholeProjectRefactorer::MergeLayers(gd::Project &project,
|
||||
gd::Layout &layout,
|
||||
const gd::String &originLayerName,
|
||||
const gd::String &targetLayerName) {
|
||||
if (originLayerName == targetLayerName || originLayerName.empty())
|
||||
return;
|
||||
|
||||
layout.GetInitialInstances().MoveInstancesToLayer(originLayerName,
|
||||
targetLayerName);
|
||||
|
||||
std::vector<gd::String> externalLayoutsNames =
|
||||
GetAssociatedExternalLayouts(project, layout);
|
||||
for (gd::String name : externalLayoutsNames) {
|
||||
auto &externalLayout = project.GetExternalLayout(name);
|
||||
externalLayout.GetInitialInstances().MoveInstancesToLayer(originLayerName,
|
||||
targetLayerName);
|
||||
}
|
||||
}
|
||||
|
||||
size_t WholeProjectRefactorer::GetLayoutAndExternalLayoutLayerInstancesCount(
|
||||
gd::Project &project, gd::Layout &layout, const gd::String &layerName) {
|
||||
size_t count = layout.GetInitialInstances().GetLayerInstancesCount(layerName);
|
||||
|
||||
std::vector<gd::String> externalLayoutsNames =
|
||||
GetAssociatedExternalLayouts(project, layout);
|
||||
for (gd::String name : externalLayoutsNames) {
|
||||
auto &externalLayout = project.GetExternalLayout(name);
|
||||
count += externalLayout.GetInitialInstances().GetLayerInstancesCount(layerName);
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
std::vector<gd::String>
|
||||
WholeProjectRefactorer::GetAssociatedExternalLayouts(gd::Project &project,
|
||||
gd::Layout &layout) {
|
||||
|
@@ -456,6 +456,26 @@ class GD_CORE_API WholeProjectRefactorer {
|
||||
const gd::EventsFunctionsExtension& eventsFunctionsExtension,
|
||||
const gd::EventsBasedObject& eventsBasedObject);
|
||||
|
||||
/**
|
||||
* \brief Remove all the instances from one layer.
|
||||
*/
|
||||
static void RemoveLayer(gd::Project &project, gd::Layout &layout,
|
||||
const gd::String &layerName);
|
||||
|
||||
/**
|
||||
* \brief Move all the instances from one layer into another.
|
||||
*/
|
||||
static void MergeLayers(gd::Project &project, gd::Layout &layout,
|
||||
const gd::String &originLayerName,
|
||||
const gd::String &targetLayerName);
|
||||
|
||||
/**
|
||||
* \brief Return the number of instances on the layer named \a layerName and
|
||||
* all its associated layouts.
|
||||
*/
|
||||
static size_t GetLayoutAndExternalLayoutLayerInstancesCount(
|
||||
gd::Project &project, gd::Layout &layout, const gd::String &layerName);
|
||||
|
||||
virtual ~WholeProjectRefactorer(){};
|
||||
|
||||
private:
|
||||
|
@@ -134,8 +134,19 @@ void InitialInstancesContainer::MoveInstancesToLayer(
|
||||
}
|
||||
}
|
||||
|
||||
std::size_t InitialInstancesContainer::GetLayerInstancesCount(
|
||||
const gd::String &layerName) const {
|
||||
std::size_t count = 0;
|
||||
for (const gd::InitialInstance &instance : initialInstances) {
|
||||
if (instance.GetLayer() == layerName) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool InitialInstancesContainer::SomeInstancesAreOnLayer(
|
||||
const gd::String& layerName) {
|
||||
const gd::String& layerName) const {
|
||||
return std::any_of(initialInstances.begin(),
|
||||
initialInstances.end(),
|
||||
[&layerName](const InitialInstance& currentInstance) {
|
||||
@@ -144,7 +155,7 @@ bool InitialInstancesContainer::SomeInstancesAreOnLayer(
|
||||
}
|
||||
|
||||
bool InitialInstancesContainer::HasInstancesOfObject(
|
||||
const gd::String& objectName) {
|
||||
const gd::String& objectName) const {
|
||||
return std::any_of(initialInstances.begin(),
|
||||
initialInstances.end(),
|
||||
[&objectName](const InitialInstance& currentInstance) {
|
||||
|
@@ -4,8 +4,8 @@
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
|
||||
#ifndef GDCORE_INITIALINSTANCESCONTAINER_H
|
||||
#define GDCORE_INITIALINSTANCESCONTAINER_H
|
||||
#pragma once
|
||||
|
||||
#include <list>
|
||||
#include "GDCore/Project/InitialInstance.h"
|
||||
#include "GDCore/String.h"
|
||||
@@ -54,7 +54,6 @@ class GD_CORE_API InitialInstancesContainer {
|
||||
return new InitialInstancesContainer(*this);
|
||||
};
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
/**
|
||||
* Must construct the class from the source
|
||||
* A such method is needed as the IDE may want to store copies of some
|
||||
@@ -71,7 +70,6 @@ class GD_CORE_API InitialInstancesContainer {
|
||||
* from an object which is not a MyContainer"; } \endcode
|
||||
*/
|
||||
void Create(const InitialInstancesContainer &source);
|
||||
#endif
|
||||
|
||||
/** \name Instances management
|
||||
* Members functions related to managing the instances
|
||||
@@ -100,7 +98,6 @@ class GD_CORE_API InitialInstancesContainer {
|
||||
void IterateOverInstancesWithZOrdering(InitialInstanceFunctor &func,
|
||||
const gd::String &layer);
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
/**
|
||||
* \brief Insert the specified \a instance into the list and return a
|
||||
* a reference to the newly added instance.
|
||||
@@ -140,22 +137,27 @@ class GD_CORE_API InitialInstancesContainer {
|
||||
void RenameInstancesOfObject(const gd::String &oldName,
|
||||
const gd::String &newName);
|
||||
|
||||
/**
|
||||
* \brief Return the number of instances on the layer named \a layerName.
|
||||
*/
|
||||
std::size_t GetLayerInstancesCount(const gd::String &layerName) const;
|
||||
|
||||
/**
|
||||
* \brief Return true if there is at least one instance on the layer named \a
|
||||
* layerName.
|
||||
*/
|
||||
bool SomeInstancesAreOnLayer(const gd::String &layerName);
|
||||
bool SomeInstancesAreOnLayer(const gd::String &layerName) const;
|
||||
|
||||
/**
|
||||
* \brief Return true if there is at least one instance of the given object.
|
||||
*/
|
||||
bool HasInstancesOfObject(const gd::String &objectName);
|
||||
bool HasInstancesOfObject(const gd::String &objectName) const;
|
||||
|
||||
/**
|
||||
* \brief Remove all instances
|
||||
*/
|
||||
void Clear();
|
||||
#endif
|
||||
|
||||
///@}
|
||||
|
||||
/** \name Saving and loading
|
||||
@@ -163,12 +165,10 @@ class GD_CORE_API InitialInstancesContainer {
|
||||
*/
|
||||
///@{
|
||||
|
||||
#if defined(GD_IDE_ONLY)
|
||||
/**
|
||||
* \brief Serialize instances container.
|
||||
*/
|
||||
virtual void SerializeTo(SerializerElement &element) const;
|
||||
#endif
|
||||
|
||||
/**
|
||||
* \brief Unserialize the instances container.
|
||||
@@ -265,5 +265,3 @@ class GD_CORE_API HighestZOrderFinder : public gd::InitialInstanceFunctor {
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
||||
#endif // GDCORE_INITIALINSTANCESCONTAINER_H
|
||||
|
@@ -535,7 +535,7 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project,
|
||||
if (groupsObjects.empty()) {
|
||||
return false;
|
||||
}
|
||||
// Check that all objects have the same type.
|
||||
// Check that all objects have the behavior.
|
||||
for (auto &&object : groupsObjects) {
|
||||
if (!HasBehaviorInObjectOrGroup(project, layout, object, behaviorName,
|
||||
false)) {
|
||||
@@ -545,6 +545,50 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project,
|
||||
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) {
|
||||
// Search in objects
|
||||
if (layout.HasObjectNamed(objectOrGroupName)) {
|
||||
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() : "";
|
||||
}
|
||||
|
||||
// Search in groups
|
||||
const gd::ObjectsContainer *container;
|
||||
if (layout.GetObjectGroups().Has(objectOrGroupName)) {
|
||||
container = &layout;
|
||||
} else if (project.GetObjectGroups().Has(objectOrGroupName)) {
|
||||
container = &project;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
const vector<gd::String> &groupsObjects =
|
||||
container->GetObjectGroups().Get(objectOrGroupName).GetAllObjectsNames();
|
||||
// Empty groups don't contain any behavior.
|
||||
if (groupsObjects.empty()) {
|
||||
return "";
|
||||
}
|
||||
// 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) {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
return behaviorType;
|
||||
}
|
||||
|
||||
gd::String GD_CORE_API GetTypeOfBehavior(const gd::ObjectsContainer& project,
|
||||
const gd::ObjectsContainer& layout,
|
||||
gd::String name,
|
||||
|
@@ -450,6 +450,14 @@ bool GD_CORE_API HasBehaviorInObjectOrGroup(const gd::ObjectsContainer &project,
|
||||
const gd::String &objectOrGroupName,
|
||||
const gd::String &behaviorName,
|
||||
bool searchInGroups = true);
|
||||
/**
|
||||
* \brief Get the type of a behavior if an object or some object of a group has it.
|
||||
*/
|
||||
gd::String GD_CORE_API GetTypeOfBehaviorInObjectOrGroup(const gd::ObjectsContainer &project,
|
||||
const gd::ObjectsContainer &layout,
|
||||
const gd::String &objectOrGroupName,
|
||||
const gd::String &behaviorName,
|
||||
bool searchInGroups = true);
|
||||
/**
|
||||
* \brief Get a type from a behavior name
|
||||
* @return Type of the behavior.
|
||||
|
@@ -15,13 +15,12 @@
|
||||
#include <vector>
|
||||
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Events/Parsers/GrammarTerminals.h"
|
||||
#include "GDCore/Extensions/Metadata/ExpressionMetadata.h"
|
||||
#include "GDCore/Extensions/Metadata/MetadataProvider.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Extensions/PlatformExtension.h"
|
||||
#include "GDCore/IDE/Events/UsedExtensionsFinder.h"
|
||||
#include "GDCore/IDE/PlatformManager.h"
|
||||
#include "GDCore/IDE/Project/ArbitraryResourceWorker.h"
|
||||
#include "GDCore/Project/CustomObjectConfiguration.h"
|
||||
#include "GDCore/Project/EventsFunctionsExtension.h"
|
||||
#include "GDCore/Project/ExternalEvents.h"
|
||||
@@ -49,6 +48,11 @@ using namespace std;
|
||||
|
||||
namespace gd {
|
||||
|
||||
// By default, disallow unicode in identifiers, but this can be set to true
|
||||
// by the IDE. In the future, this will be set to true by default, keeping backward compatibility.
|
||||
// We keep it disabled by default to progressively ask users to test it in real projects.
|
||||
bool Project::allowUsageOfUnicodeIdentifierNames = false;
|
||||
|
||||
Project::Project()
|
||||
: name(_("Project")),
|
||||
version("1.0.0"),
|
||||
@@ -630,8 +634,10 @@ void Project::UnserializeFrom(const SerializerElement& element) {
|
||||
SetAdaptGameResolutionAtRuntime(
|
||||
propElement.GetBoolAttribute("adaptGameResolutionAtRuntime", false));
|
||||
SetSizeOnStartupMode(propElement.GetStringAttribute("sizeOnStartupMode", ""));
|
||||
SetAntialiasingMode(propElement.GetStringAttribute("antialiasingMode", "MSAA"));
|
||||
SetAntialisingEnabledOnMobile(propElement.GetBoolAttribute("antialisingEnabledOnMobile", false));
|
||||
SetAntialiasingMode(
|
||||
propElement.GetStringAttribute("antialiasingMode", "MSAA"));
|
||||
SetAntialisingEnabledOnMobile(
|
||||
propElement.GetBoolAttribute("antialisingEnabledOnMobile", false));
|
||||
SetProjectUuid(propElement.GetStringAttribute("projectUuid", ""));
|
||||
SetAuthor(propElement.GetChild("author", 0, "Auteur").GetValue().GetString());
|
||||
SetPackageName(propElement.GetStringAttribute("packageName"));
|
||||
@@ -887,7 +893,8 @@ void Project::SerializeTo(SerializerElement& element) const {
|
||||
adaptGameResolutionAtRuntime);
|
||||
propElement.SetAttribute("sizeOnStartupMode", sizeOnStartupMode);
|
||||
propElement.SetAttribute("antialiasingMode", antialiasingMode);
|
||||
propElement.SetAttribute("antialisingEnabledOnMobile", isAntialisingEnabledOnMobile);
|
||||
propElement.SetAttribute("antialisingEnabledOnMobile",
|
||||
isAntialisingEnabledOnMobile);
|
||||
propElement.SetAttribute("projectUuid", projectUuid);
|
||||
propElement.SetAttribute("folderProject", folderProject);
|
||||
propElement.SetAttribute("packageName", packageName);
|
||||
@@ -993,58 +1000,57 @@ void Project::SerializeTo(SerializerElement& element) const {
|
||||
externalSourceFilesElement.AddChild("sourceFile"));
|
||||
}
|
||||
|
||||
bool Project::ValidateName(const gd::String& name) {
|
||||
void Project::AllowUsageOfUnicodeIdentifierNames(bool enable) {
|
||||
allowUsageOfUnicodeIdentifierNames = enable;
|
||||
}
|
||||
|
||||
bool Project::IsNameSafe(const gd::String& name) {
|
||||
if (name.empty()) return false;
|
||||
|
||||
if (isdigit(name[0])) return false;
|
||||
|
||||
gd::String allowedCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
||||
return !(name.find_first_not_of(allowedCharacters) != gd::String::npos);
|
||||
if (!allowUsageOfUnicodeIdentifierNames) {
|
||||
gd::String legacyAllowedCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
||||
return !(name.find_first_not_of(legacyAllowedCharacters) != gd::String::npos);
|
||||
} else {
|
||||
for (auto character : name) {
|
||||
if (!GrammarTerminals::IsAllowedInIdentifier(character)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
void Project::ExposeResources(gd::ArbitraryResourceWorker& worker) {
|
||||
// See also gd::ProjectBrowserHelper::ExposeProjectEvents for a method that
|
||||
// traverse the whole project (this time for events) and ExposeProjectEffects
|
||||
// (this time for effects). Ideally, this method could be moved outside of
|
||||
// gd::Project.
|
||||
gd::String Project::GetSafeName(const gd::String& name) {
|
||||
if (name.empty()) return "Unnamed";
|
||||
|
||||
gd::ResourcesManager* resourcesManager = &GetResourcesManager();
|
||||
gd::String newName = name;
|
||||
|
||||
// Add project resources
|
||||
worker.ExposeResources(resourcesManager);
|
||||
platformSpecificAssets.ExposeResources(worker);
|
||||
if (isdigit(name[0])) newName = "_" + newName;
|
||||
|
||||
// Add layouts resources
|
||||
for (std::size_t s = 0; s < GetLayoutsCount(); s++) {
|
||||
for (std::size_t j = 0; j < GetLayout(s).GetObjectsCount();
|
||||
++j) { // Add objects resources
|
||||
GetLayout(s).GetObject(j).GetConfiguration().ExposeResources(worker);
|
||||
}
|
||||
gd::String legacyAllowedCharacters =
|
||||
"abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_";
|
||||
|
||||
LaunchResourceWorkerOnEvents(*this, GetLayout(s).GetEvents(), worker);
|
||||
}
|
||||
// Add external events resources
|
||||
for (std::size_t s = 0; s < GetExternalEventsCount(); s++) {
|
||||
LaunchResourceWorkerOnEvents(
|
||||
*this, GetExternalEvents(s).GetEvents(), worker);
|
||||
}
|
||||
// Add events functions extensions resources
|
||||
for (std::size_t e = 0; e < GetEventsFunctionsExtensionsCount(); e++) {
|
||||
auto& eventsFunctionsExtension = GetEventsFunctionsExtension(e);
|
||||
for (auto&& eventsFunction : eventsFunctionsExtension.GetInternalVector()) {
|
||||
LaunchResourceWorkerOnEvents(*this, eventsFunction->GetEvents(), worker);
|
||||
for (size_t i = 0;i < newName.size();++i) {
|
||||
// Note that iterating on the characters is not super efficient (O(n^2), which
|
||||
// could be avoided with an iterator), but this function is not critical for performance
|
||||
// (only used to generate a name when a user creates a new entity or rename one).
|
||||
auto character = newName[i];
|
||||
bool isAllowed =
|
||||
allowUsageOfUnicodeIdentifierNames
|
||||
? GrammarTerminals::IsAllowedInIdentifier(character)
|
||||
: legacyAllowedCharacters.find(character) != gd::String::npos;
|
||||
|
||||
// Replace all unallowed letters by an underscore.
|
||||
if (!isAllowed) {
|
||||
newName.replace(i, 1, '_');
|
||||
}
|
||||
}
|
||||
|
||||
// Add global objects resources
|
||||
for (std::size_t j = 0; j < GetObjectsCount(); ++j) {
|
||||
GetObject(j).GetConfiguration().ExposeResources(worker);
|
||||
}
|
||||
|
||||
// Add loading screen background image if present
|
||||
if (loadingScreen.GetBackgroundImageResourceName() != "")
|
||||
worker.ExposeImage(loadingScreen.GetBackgroundImageResourceName());
|
||||
return newName;
|
||||
}
|
||||
|
||||
bool Project::HasSourceFile(gd::String name, gd::String language) const {
|
||||
|
@@ -942,16 +942,6 @@ class GD_CORE_API Project : public ObjectsContainer {
|
||||
*/
|
||||
ResourcesManager& GetResourcesManager() { return resourcesManager; }
|
||||
|
||||
/**
|
||||
* \brief Called ( e.g. during compilation ) so as to inventory internal
|
||||
* resources, sometimes update their filename or any other work or resources.
|
||||
*
|
||||
* See WholeProjectRefactorer for the same thing for events.
|
||||
*
|
||||
* \see WholeProjectRefactorer
|
||||
* \see ArbitraryResourceWorker
|
||||
*/
|
||||
void ExposeResources(gd::ArbitraryResourceWorker& worker);
|
||||
///@}
|
||||
|
||||
/** \name Variable management
|
||||
@@ -975,15 +965,35 @@ class GD_CORE_API Project : public ObjectsContainer {
|
||||
|
||||
///@}
|
||||
|
||||
/** \name Other
|
||||
/** \name Identifier names
|
||||
*/
|
||||
///@{
|
||||
|
||||
/**
|
||||
* Check if unicode names are allowed in identifier names.
|
||||
* \see IsNameSafe
|
||||
* \see GetSafeName
|
||||
*/
|
||||
static bool IsUsageOfUnicodeIdentifierNamesAllowed() { return allowUsageOfUnicodeIdentifierNames; };
|
||||
|
||||
/**
|
||||
* Set if unicode names are allowed in identifier names.
|
||||
* \see IsNameSafe
|
||||
* \see GetSafeName
|
||||
*/
|
||||
static void AllowUsageOfUnicodeIdentifierNames(bool enable);
|
||||
|
||||
/**
|
||||
* Return true if \a name is valid (can be used safely for an object,
|
||||
* behavior, events function name, etc...).
|
||||
*/
|
||||
static bool ValidateName(const gd::String& name);
|
||||
static bool IsNameSafe(const gd::String& name);
|
||||
|
||||
/**
|
||||
* Return a name, based on the one passed in parameter, that can be safely used
|
||||
* for an object, behavior, events function name, etc...
|
||||
*/
|
||||
static gd::String GetSafeName(const gd::String& name);
|
||||
///@}
|
||||
|
||||
/** \name External source files
|
||||
@@ -1118,6 +1128,8 @@ class GD_CORE_API Project : public ObjectsContainer {
|
||||
///< time the project was saved.
|
||||
mutable unsigned int gdBuildVersion; ///< The GD build version used the last
|
||||
///< time the project was saved.
|
||||
|
||||
static bool allowUsageOfUnicodeIdentifierNames;
|
||||
};
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -22,6 +22,10 @@
|
||||
#include "DummyPlatform.h"
|
||||
#include "GDCore/Extensions/Builtin/SpriteExtension/SpriteObject.h"
|
||||
#include "catch.hpp"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
#include "GDCore/Project/Effect.h"
|
||||
#include "GDCore/Project/EventsFunctionsExtension.h"
|
||||
#include "GDCore/Project/ExternalEvents.h"
|
||||
|
||||
class ArbitraryResourceWorkerTest : public gd::ArbitraryResourceWorker {
|
||||
public:
|
||||
@@ -55,7 +59,7 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
|
||||
"res4", "path/to/file4.png", "audio");
|
||||
ArbitraryResourceWorkerTest worker;
|
||||
|
||||
project.ExposeResources(worker);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.files.size() == 4);
|
||||
REQUIRE(std::find(worker.files.begin(),
|
||||
worker.files.end(),
|
||||
@@ -78,7 +82,7 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
|
||||
|
||||
worker.files.clear();
|
||||
worker.images.clear();
|
||||
project.ExposeResources(worker);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.files.size() == 4);
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
@@ -124,7 +128,7 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
layout.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
project.ExposeResources(worker);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
@@ -132,4 +136,207 @@ TEST_CASE("ArbitraryResourceWorker", "[common][resources]") {
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usage in external events") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.fnt", "bitmapFont");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res4", "path/to/file4.png", "audio");
|
||||
ArbitraryResourceWorkerTest worker;
|
||||
|
||||
auto& externalEvents = project.InsertNewExternalEvents("MyExternalEvents", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
externalEvents.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
// MyExternalEvents doesn't need to be used in any layout events.
|
||||
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usage in event-based functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker;
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtention", 0);
|
||||
auto& function = extension.InsertNewEventsFunction("MyFreeFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
// MyEventExtention::MyFreeFunction doesn't need to be actually used in events.
|
||||
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usage in event-based behavior functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker;
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtention", 0);
|
||||
auto& behavior = extension.GetEventsBasedBehaviors().InsertNew("MyBehavior", 0);
|
||||
auto& function = behavior.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
// MyEventExtention::MyBehavior::MyFunction doesn't need to be actually used in events.
|
||||
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usage in event-based object functions") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker;
|
||||
|
||||
auto& extension = project.InsertNewEventsFunctionsExtension("MyEventExtention", 0);
|
||||
auto& object = extension.GetEventsBasedObjects().InsertNew("MyObject", 0);
|
||||
auto& function = object.GetEventsFunctions().InsertNewEventsFunction("MyFunction", 0);
|
||||
|
||||
gd::StandardEvent standardEvent;
|
||||
gd::Instruction instruction;
|
||||
instruction.SetType("MyExtension::DoSomethingWithResources");
|
||||
instruction.SetParametersCount(3);
|
||||
instruction.SetParameter(0, "res3");
|
||||
instruction.SetParameter(1, "res1");
|
||||
instruction.SetParameter(2, "res4");
|
||||
standardEvent.GetActions().Insert(instruction);
|
||||
function.GetEvents().InsertEvent(standardEvent);
|
||||
|
||||
// MyEventExtention::MyObject::MyFunction doesn't need to be actually used in events.
|
||||
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.bitmapFonts.size() == 1);
|
||||
REQUIRE(worker.bitmapFonts[0] == "res3");
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
REQUIRE(worker.audios.size() == 1);
|
||||
REQUIRE(worker.audios[0] == "res4");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usage in layer effects") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker;
|
||||
|
||||
auto& layout = project.InsertNewLayout("Scene", 0);
|
||||
layout.InsertNewLayer("MyLayer", 0);
|
||||
auto& layer = layout.GetLayer("MyLayer");
|
||||
auto& effect = layer.GetEffects().InsertNewEffect("MyEffect", 0);
|
||||
effect.SetEffectType("MyExtension::EffectWithResource");
|
||||
effect.SetStringParameter("texture", "res1");
|
||||
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.files.size() == 3);
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
}
|
||||
|
||||
SECTION("Can find resource usage in object effects") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res1", "path/to/file1.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res2", "path/to/file2.png", "image");
|
||||
project.GetResourcesManager().AddResource(
|
||||
"res3", "path/to/file3.png", "image");
|
||||
ArbitraryResourceWorkerTest worker;
|
||||
|
||||
auto& layout = project.InsertNewLayout("Scene", 0);
|
||||
layout.InsertNewLayer("MyLayer", 0);
|
||||
auto& layer = layout.GetLayer("MyLayer");
|
||||
auto& object = layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject", 0);
|
||||
auto& effect = object.GetEffects().InsertNewEffect("MyEffect", 0);
|
||||
effect.SetEffectType("MyExtension::EffectWithResource");
|
||||
effect.SetStringParameter("texture", "res1");
|
||||
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, worker);
|
||||
REQUIRE(worker.files.size() == 3);
|
||||
REQUIRE(worker.images.size() == 1);
|
||||
REQUIRE(worker.images[0] == "res1");
|
||||
}
|
||||
}
|
||||
|
@@ -465,6 +465,17 @@ void SetupProjectWithDummyPlatform(gd::Project& project,
|
||||
.AddParameter("layerEffectParameterName", _("Parameter name"));
|
||||
}
|
||||
|
||||
{
|
||||
auto& effect = extension
|
||||
->AddEffect("EffectWithResource")
|
||||
.SetFullName("Effect with resource")
|
||||
.MarkAsOnlyWorkingFor2D();
|
||||
auto& effectProperties = effect.GetProperties();
|
||||
effectProperties["texture"]
|
||||
.SetType("resource")
|
||||
.AddExtraInfo("image");
|
||||
}
|
||||
|
||||
platform.AddExtension(commonInstructionsExtension);
|
||||
platform.AddExtension(baseObjectExtension);
|
||||
platform.AddExtension(extension);
|
||||
|
@@ -26,7 +26,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
|
||||
auto &myGroup = layout1.GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
myGroup.AddObject(myObject.GetName());
|
||||
|
||||
|
||||
layout1.InsertNewObject(project, "MyExtension::Sprite", "MySpriteObject", 1);
|
||||
layout1.InsertNewObject(project,
|
||||
"MyExtension::FakeObjectWithUnsupportedCapability",
|
||||
@@ -340,6 +340,20 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
"Cannot find an expression with this name: Idontexist\n"
|
||||
"Double check that you've not made any typo in the name.");
|
||||
}
|
||||
{
|
||||
auto node = parser.ParseExpression("🅸🅳🅾🅽🆃🅴🆇🅸🆂🆃😄(\"hello\"");
|
||||
REQUIRE(node != nullptr);
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "string");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 2);
|
||||
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
|
||||
"The list of parameters is not terminated. Add a closing "
|
||||
"parenthesis to end the parameters.");
|
||||
REQUIRE(validator.GetFatalErrors()[1]->GetMessage() ==
|
||||
"Cannot find an expression with this name: 🅸🅳🅾🅽🆃🅴🆇🅸🆂🆃😄\n"
|
||||
"Double check that you've not made any typo in the name.");
|
||||
}
|
||||
{
|
||||
auto node = parser.ParseExpression("=\"test\"");
|
||||
REQUIRE(node != nullptr);
|
||||
@@ -694,6 +708,17 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
"You must enter a number.");
|
||||
REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 0);
|
||||
}
|
||||
{
|
||||
auto node = parser.ParseExpression("ab😊d");
|
||||
REQUIRE(node != nullptr);
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "number");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 1);
|
||||
REQUIRE(validator.GetFatalErrors()[0]->GetMessage() ==
|
||||
"You must enter a number.");
|
||||
REQUIRE(validator.GetFatalErrors()[0]->GetStartPosition() == 0);
|
||||
}
|
||||
{
|
||||
auto node = parser.ParseExpression("abcd[0]");
|
||||
REQUIRE(node != nullptr);
|
||||
@@ -917,6 +942,26 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
}
|
||||
{
|
||||
auto node = parser.ParseExpression("😅");
|
||||
REQUIRE(node != nullptr);
|
||||
auto &identifierNode = dynamic_cast<gd::IdentifierNode &>(*node);
|
||||
REQUIRE(identifierNode.identifierName == "😅");
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "object");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
}
|
||||
{
|
||||
auto node = parser.ParseExpression("中文");
|
||||
REQUIRE(node != nullptr);
|
||||
auto &identifierNode = dynamic_cast<gd::IdentifierNode &>(*node);
|
||||
REQUIRE(identifierNode.identifierName == "中文");
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "object");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
}
|
||||
|
||||
{
|
||||
auto node = parser.ParseExpression("Hello World 1");
|
||||
@@ -1092,6 +1137,46 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
REQUIRE(textNode.text == "2");
|
||||
REQUIRE(identifierNode.identifierName == "three");
|
||||
}
|
||||
SECTION("identifier parameter (unicode)") {
|
||||
auto node = parser.ParseExpression(
|
||||
"WhateverObject.WhateverBehavior::WhateverFunction(1, \"2\", 😄)");
|
||||
REQUIRE(node != nullptr);
|
||||
auto &functionNode = dynamic_cast<gd::FunctionCallNode &>(*node);
|
||||
REQUIRE(functionNode.functionName == "WhateverFunction");
|
||||
REQUIRE(functionNode.objectName == "WhateverObject");
|
||||
REQUIRE(functionNode.behaviorName == "WhateverBehavior");
|
||||
REQUIRE(functionNode.parameters.size() == 3);
|
||||
auto &numberNode =
|
||||
dynamic_cast<gd::NumberNode &>(*functionNode.parameters[0]);
|
||||
auto &textNode =
|
||||
dynamic_cast<gd::TextNode &>(*functionNode.parameters[1]);
|
||||
auto &identifierNode =
|
||||
dynamic_cast<gd::IdentifierNode &>(*functionNode.parameters[2]);
|
||||
|
||||
REQUIRE(numberNode.number == "1");
|
||||
REQUIRE(textNode.text == "2");
|
||||
REQUIRE(identifierNode.identifierName == "😄");
|
||||
}
|
||||
SECTION("unicode for object, behavior and function") {
|
||||
auto node = parser.ParseExpression(
|
||||
"🧸.🗣️::🔔(1, \"2\", 😄)");
|
||||
REQUIRE(node != nullptr);
|
||||
auto &functionNode = dynamic_cast<gd::FunctionCallNode &>(*node);
|
||||
REQUIRE(functionNode.functionName == "🔔");
|
||||
REQUIRE(functionNode.objectName == "🧸");
|
||||
REQUIRE(functionNode.behaviorName == "🗣️");
|
||||
REQUIRE(functionNode.parameters.size() == 3);
|
||||
auto &numberNode =
|
||||
dynamic_cast<gd::NumberNode &>(*functionNode.parameters[0]);
|
||||
auto &textNode =
|
||||
dynamic_cast<gd::TextNode &>(*functionNode.parameters[1]);
|
||||
auto &identifierNode =
|
||||
dynamic_cast<gd::IdentifierNode &>(*functionNode.parameters[2]);
|
||||
|
||||
REQUIRE(numberNode.number == "1");
|
||||
REQUIRE(textNode.text == "2");
|
||||
REQUIRE(identifierNode.identifierName == "😄");
|
||||
}
|
||||
}
|
||||
|
||||
SECTION("Valid function calls ('number|string' type)") {
|
||||
@@ -1669,7 +1754,7 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
auto &identifierNode = dynamic_cast<gd::IdentifierNode &>(*node);
|
||||
REQUIRE(identifierNode.identifierName == "myVariable");
|
||||
REQUIRE(identifierNode.childIdentifierName == "myChild");
|
||||
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "scenevar");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
@@ -1766,14 +1851,14 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
REQUIRE(identifierObject2Node.identifierName == "MyObject2");
|
||||
REQUIRE(variable1Node.identifierName == "MyVar1");
|
||||
REQUIRE(variable2Node.identifierName == "MyVar2");
|
||||
|
||||
|
||||
auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
|
||||
platform, project, layout1, "", variable1Node);
|
||||
REQUIRE(variable1ObjectName == "MyObject1");
|
||||
auto variable2ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
|
||||
platform, project, layout1, "", variable2Node);
|
||||
REQUIRE(variable2ObjectName == "MyObject2");
|
||||
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "string");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
@@ -1797,14 +1882,14 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
REQUIRE(identifierObject1Node.identifierName == "MyObject1");
|
||||
REQUIRE(variable1Node.identifierName == "MyVar1");
|
||||
REQUIRE(variable2Node.identifierName == "MyVar2");
|
||||
|
||||
|
||||
auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
|
||||
platform, project, layout1, "", variable1Node);
|
||||
REQUIRE(variable1ObjectName == "MyObject1");
|
||||
auto variable2ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
|
||||
platform, project, layout1, "", variable2Node);
|
||||
REQUIRE(variable2ObjectName == "MyObject1");
|
||||
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "string");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
@@ -1821,11 +1906,11 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
dynamic_cast<gd::IdentifierNode &>(*functionNode.parameters[0]);
|
||||
|
||||
REQUIRE(variable1Node.identifierName == "MyVar1");
|
||||
|
||||
|
||||
auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
|
||||
platform, project, layout1, "MySpriteObject", variable1Node);
|
||||
REQUIRE(variable1ObjectName == "MySpriteObject");
|
||||
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "number");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
@@ -1843,11 +1928,11 @@ TEST_CASE("ExpressionParser2", "[common][events]") {
|
||||
|
||||
REQUIRE(variable1Node.identifierName == "MyVar1");
|
||||
REQUIRE(variable1Node.childIdentifierName == "MyChild");
|
||||
|
||||
|
||||
auto variable1ObjectName = gd::ExpressionVariableOwnerFinder::GetObjectName(
|
||||
platform, project, layout1, "MySpriteObject", variable1Node);
|
||||
REQUIRE(variable1ObjectName == "MySpriteObject");
|
||||
|
||||
|
||||
gd::ExpressionValidator validator(platform, project, layout1, "number");
|
||||
node->Visit(validator);
|
||||
REQUIRE(validator.GetFatalErrors().size() == 0);
|
||||
|
133
Core/tests/Layout.cpp
Normal file
133
Core/tests/Layout.cpp
Normal file
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* GDevelop Core
|
||||
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
|
||||
* reserved. This project is released under the MIT License.
|
||||
*/
|
||||
/**
|
||||
* @file Tests covering layout content helper methods.
|
||||
*/
|
||||
#include "GDCore/Project/Layout.h"
|
||||
#include "DummyPlatform.h"
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Extensions/Platform.h"
|
||||
#include "GDCore/Project/Object.h"
|
||||
#include "GDCore/Project/ObjectsContainer.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "catch.hpp"
|
||||
|
||||
using namespace gd;
|
||||
|
||||
namespace {
|
||||
|
||||
void SetupProject(gd::Project &project, gd::Platform &platform){
|
||||
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_CASE("Layout", "[common]") {
|
||||
|
||||
SECTION("Find the type of a behavior in a object") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject", 0);
|
||||
object.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(project, layout, "MyObject",
|
||||
"MyBehavior", true) ==
|
||||
"MyExtension::MyBehavior");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for an object that doesn't have the behavior") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject", 0);
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(project, layout, "MyObject",
|
||||
"MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Find the type of a behavior in a group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
|
||||
auto &group = layout.GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(project, layout, "MyGroup",
|
||||
"MyBehavior", true) ==
|
||||
"MyExtension::MyBehavior");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for a group with an object missing the behavior") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
// object2 doesn't have the behavior.
|
||||
|
||||
auto &group = layout.GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(project, layout, "MyGroup",
|
||||
"MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for a group with behaviors of same name but different types") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
gd::Object &object1 =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject1", 0);
|
||||
object1.AddNewBehavior(project, "MyExtension::MyBehavior", "MyBehavior");
|
||||
gd::Object &object2 =
|
||||
layout.InsertNewObject(project, "MyExtension::Sprite", "MyObject2", 0);
|
||||
object2.AddNewBehavior(project, "MyExtension::MyOtherBehavior",
|
||||
"MyBehavior");
|
||||
|
||||
auto &group = layout.GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
group.AddObject(object1.GetName());
|
||||
group.AddObject(object2.GetName());
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(project, layout, "MyGroup",
|
||||
"MyBehavior", true) == "");
|
||||
}
|
||||
|
||||
SECTION("Give an empty type for an empty group") {
|
||||
gd::Platform platform;
|
||||
gd::Project project;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
gd::Layout &layout = project.InsertNewLayout("Scene", 0);
|
||||
auto &group = layout.GetObjectGroups().InsertNew("MyGroup", 0);
|
||||
|
||||
REQUIRE(GetTypeOfBehaviorInObjectOrGroup(project, layout, "MyGroup",
|
||||
"MyBehavior", true) == "");
|
||||
}
|
||||
}
|
@@ -16,6 +16,7 @@
|
||||
#include "GDCore/Tools/SystemStats.h"
|
||||
#include "GDCore/Tools/VersionWrapper.h"
|
||||
#include "catch.hpp"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
|
||||
class MockFileSystem : public gd::AbstractFileSystem {
|
||||
public:
|
||||
@@ -75,7 +76,7 @@ TEST_CASE("ResourcesMergingHelper", "[common]") {
|
||||
project.GetResourcesManager().AddResource(
|
||||
"Image3", "subfolder/image3.png", "image");
|
||||
|
||||
project.ExposeResources(resourcesMerger);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, resourcesMerger);
|
||||
|
||||
auto resourcesFilenames =
|
||||
resourcesMerger.GetAllResourcesOldAndNewFilename();
|
||||
@@ -101,7 +102,7 @@ TEST_CASE("ResourcesMergingHelper", "[common]") {
|
||||
project.GetResourcesManager().AddResource(
|
||||
"Image3", "subfolder/image3.png", "image");
|
||||
|
||||
project.ExposeResources(resourcesMerger);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, resourcesMerger);
|
||||
|
||||
auto resourcesFilenames =
|
||||
resourcesMerger.GetAllResourcesOldAndNewFilename();
|
||||
|
@@ -11,6 +11,7 @@
|
||||
#include "GDCore/CommonTools.h"
|
||||
#include "GDCore/Project/Project.h"
|
||||
#include "catch.hpp"
|
||||
#include "GDCore/IDE/ResourceExposer.h"
|
||||
|
||||
TEST_CASE("ResourcesRenamer", "[common]") {
|
||||
SECTION("It renames resources that are exposed") {
|
||||
@@ -34,7 +35,7 @@ TEST_CASE("ResourcesRenamer", "[common]") {
|
||||
project.GetPlatformSpecificAssets().Set(
|
||||
"android", "some-other-icon", "Resource2");
|
||||
|
||||
project.ExposeResources(resourcesRenamer);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, resourcesRenamer);
|
||||
|
||||
// Check that resources were renamed were used.
|
||||
REQUIRE(project.GetPlatformSpecificAssets().Get("android", "some-icon") ==
|
||||
@@ -72,7 +73,7 @@ TEST_CASE("ResourcesRenamer", "[common]") {
|
||||
project.GetPlatformSpecificAssets().Set(
|
||||
"android", "some-other-icon", "Resource2");
|
||||
|
||||
project.ExposeResources(resourcesRenamer);
|
||||
gd::ResourceExposer::ExposeWholeProjectResources(project, resourcesRenamer);
|
||||
|
||||
// TODO: This should not be necessary, but for now not all resources support embeddeds,
|
||||
// so we must call it manually:
|
||||
|
@@ -3687,7 +3687,6 @@ TEST_CASE("RenameLayerEffect", "[common]") {
|
||||
auto &wrongLayerExpression =
|
||||
CreateExpressionWithLayerEffectParameter(project, layout.GetEvents(), "My layer 2");
|
||||
|
||||
std::cout << "RenameLayerEffect" << std::endl;
|
||||
gd::WholeProjectRefactorer::RenameLayerEffect(project, layout, layer, "My effect",
|
||||
"My renamed effect");
|
||||
|
||||
@@ -3721,3 +3720,129 @@ TEST_CASE("RenameLayerEffect", "[common]") {
|
||||
"MyExtension::LayerEffectParameter(\"My layer 2\", \"My effect\")");
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("RemoveLayer", "[common]") {
|
||||
SECTION("Can remove instances in a layout and its associated external layouts") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
auto &layout = project.InsertNewLayout("My layout", 0);
|
||||
auto &otherLayout = project.InsertNewLayout("My other layout", 1);
|
||||
|
||||
layout.InsertNewLayer("My layer", 0);
|
||||
otherLayout.InsertNewLayer("My layer", 0);
|
||||
|
||||
auto &externalLayout =
|
||||
project.InsertNewExternalLayout("My external layout", 0);
|
||||
auto &otherExternalLayout =
|
||||
project.InsertNewExternalLayout("My other external layout", 0);
|
||||
externalLayout.SetAssociatedLayout("My layout");
|
||||
otherExternalLayout.SetAssociatedLayout("My other layout");
|
||||
|
||||
auto &initialInstances = layout.GetInitialInstances();
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("");
|
||||
|
||||
auto &externalInitialInstances = externalLayout.GetInitialInstances();
|
||||
externalInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
externalInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
externalInitialInstances.InsertNewInitialInstance().SetLayer("");
|
||||
|
||||
auto &otherInitialInstances = otherLayout.GetInitialInstances();
|
||||
otherInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
|
||||
auto &otherExternalInitialInstances = otherExternalLayout.GetInitialInstances();
|
||||
otherExternalInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
|
||||
REQUIRE(initialInstances.GetInstancesCount() == 5);
|
||||
REQUIRE(externalInitialInstances.GetInstancesCount() == 3);
|
||||
REQUIRE(otherInitialInstances.GetInstancesCount() == 1);
|
||||
REQUIRE(otherExternalInitialInstances.GetInstancesCount() == 1);
|
||||
|
||||
REQUIRE(initialInstances.GetLayerInstancesCount("My layer") == 3);
|
||||
REQUIRE(externalInitialInstances.GetLayerInstancesCount("My layer") == 2);
|
||||
|
||||
gd::WholeProjectRefactorer::RemoveLayer(project, layout, "My layer");
|
||||
|
||||
REQUIRE(initialInstances.GetInstancesCount() == 2);
|
||||
REQUIRE(externalInitialInstances.GetInstancesCount() == 1);
|
||||
REQUIRE(otherInitialInstances.GetInstancesCount() == 1);
|
||||
REQUIRE(otherExternalInitialInstances.GetInstancesCount() == 1);
|
||||
|
||||
REQUIRE(initialInstances.GetLayerInstancesCount("My layer") == 0);
|
||||
REQUIRE(externalInitialInstances.GetLayerInstancesCount("My layer") == 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE("MergeLayers", "[common]") {
|
||||
SECTION("Can merge instances from a layout into another layout (and their associated external layouts)") {
|
||||
gd::Project project;
|
||||
gd::Platform platform;
|
||||
SetupProjectWithDummyPlatform(project, platform);
|
||||
|
||||
auto &layout = project.InsertNewLayout("My layout", 0);
|
||||
auto &otherLayout = project.InsertNewLayout("My other layout", 1);
|
||||
|
||||
layout.InsertNewLayer("My layer", 0);
|
||||
otherLayout.InsertNewLayer("My layer", 0);
|
||||
|
||||
auto &externalLayout =
|
||||
project.InsertNewExternalLayout("My external layout", 0);
|
||||
auto &otherExternalLayout =
|
||||
project.InsertNewExternalLayout("My other external layout", 0);
|
||||
externalLayout.SetAssociatedLayout("My layout");
|
||||
otherExternalLayout.SetAssociatedLayout("My other layout");
|
||||
|
||||
auto &initialInstances = layout.GetInitialInstances();
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("");
|
||||
initialInstances.InsertNewInitialInstance().SetLayer("My other layer");
|
||||
|
||||
auto &externalInitialInstances = externalLayout.GetInitialInstances();
|
||||
externalInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
externalInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
externalInitialInstances.InsertNewInitialInstance().SetLayer("");
|
||||
externalInitialInstances.InsertNewInitialInstance().SetLayer("My other layer");
|
||||
|
||||
auto &otherInitialInstances = otherLayout.GetInitialInstances();
|
||||
otherInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
|
||||
auto &otherExternalInitialInstances = otherExternalLayout.GetInitialInstances();
|
||||
otherExternalInitialInstances.InsertNewInitialInstance().SetLayer("My layer");
|
||||
|
||||
REQUIRE(initialInstances.GetInstancesCount() == 6);
|
||||
REQUIRE(externalInitialInstances.GetInstancesCount() == 4);
|
||||
REQUIRE(otherInitialInstances.GetInstancesCount() == 1);
|
||||
REQUIRE(otherExternalInitialInstances.GetInstancesCount() == 1);
|
||||
|
||||
REQUIRE(initialInstances.GetLayerInstancesCount("My layer") == 3);
|
||||
REQUIRE(externalInitialInstances.GetLayerInstancesCount("My layer") == 2);
|
||||
|
||||
gd::WholeProjectRefactorer::MergeLayers(project, layout, "My layer", "");
|
||||
|
||||
// No instance was removed.
|
||||
REQUIRE(initialInstances.GetInstancesCount() == 6);
|
||||
REQUIRE(externalInitialInstances.GetInstancesCount() == 4);
|
||||
REQUIRE(otherInitialInstances.GetInstancesCount() == 1);
|
||||
REQUIRE(otherExternalInitialInstances.GetInstancesCount() == 1);
|
||||
|
||||
// No instance remain in "My layer".
|
||||
REQUIRE(initialInstances.GetLayerInstancesCount("My layer") == 0);
|
||||
REQUIRE(externalInitialInstances.GetLayerInstancesCount("My layer") == 0);
|
||||
|
||||
// Layers with the same name in other layouts are untouched.
|
||||
REQUIRE(otherInitialInstances.GetLayerInstancesCount("My layer") == 1);
|
||||
REQUIRE(otherExternalInitialInstances.GetLayerInstancesCount("My layer") == 1);
|
||||
|
||||
// Other layers from the same layout are untouched.
|
||||
REQUIRE(initialInstances.GetLayerInstancesCount("My other layer") == 1);
|
||||
REQUIRE(externalInitialInstances.GetLayerInstancesCount("My other layer") == 1);
|
||||
}
|
||||
}
|
||||
|
@@ -21,7 +21,7 @@ using namespace std;
|
||||
|
||||
Model3DObjectConfiguration::Model3DObjectConfiguration()
|
||||
: width(100), height(100), depth(100), rotationX(0), rotationY(0),
|
||||
rotationZ(0), modelResourceName(""), materialType("Basic"),
|
||||
rotationZ(0), modelResourceName(""), materialType("StandardWithoutMetalness"),
|
||||
originLocation("ModelOrigin"), centerLocation("ModelOrigin"),
|
||||
keepAspectRatio(true) {}
|
||||
|
||||
|
@@ -18,6 +18,7 @@ void DeclareAnchorBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"Victor Levasseur",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("User interface")
|
||||
.SetTags("anchor, ui, layout")
|
||||
.SetExtensionHelpPath("/behaviors/anchor");
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
|
@@ -21,6 +21,7 @@ void DeclareDestroyOutsideBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Game mechanic")
|
||||
.SetTags("screen")
|
||||
.SetExtensionHelpPath("/behaviors/destroyoutside");
|
||||
|
||||
gd::BehaviorMetadata& aut =
|
||||
|
@@ -21,6 +21,7 @@ void DeclareDraggableBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("User interface")
|
||||
.SetTags("drag")
|
||||
.SetExtensionHelpPath("/behaviors/draggable");
|
||||
|
||||
gd::BehaviorMetadata& aut = extension.AddBehavior(
|
||||
|
@@ -33,7 +33,8 @@ module.exports = {
|
||||
'Harsimran Virk',
|
||||
'MIT'
|
||||
)
|
||||
.setCategory('Visual effect');
|
||||
.setCategory('Visual effect')
|
||||
.setTags("light");
|
||||
|
||||
const lightObstacleBehavior = new gd.BehaviorJsImplementation();
|
||||
// $FlowExpectedError - ignore Flow warning as we're creating a behavior
|
||||
|
@@ -179,8 +179,9 @@ void DeclarePanelSpriteObjectExtension(gd::PlatformExtension& extension) {
|
||||
.SetFunctionName("SetAngle")
|
||||
.SetGetter("GetAngle");
|
||||
|
||||
// Deprecated
|
||||
obj.AddAction("Image",
|
||||
_("Image name"),
|
||||
_("Image name (deprecated)"),
|
||||
_("Change the image of a Panel Sprite."),
|
||||
_("Set image _PARAM1_ on _PARAM0_"),
|
||||
_("Image"),
|
||||
@@ -189,5 +190,18 @@ void DeclarePanelSpriteObjectExtension(gd::PlatformExtension& extension) {
|
||||
.AddParameter("object", _("Object"), "PanelSprite")
|
||||
.AddParameter("string", _("Image name"))
|
||||
.AddCodeOnlyParameter("currentScene", "0")
|
||||
.SetHidden()
|
||||
.SetFunctionName("ChangeAndReloadImage");
|
||||
|
||||
obj.AddAction("SetImageFromResource",
|
||||
_("Image name"),
|
||||
_("Change the image of a Panel Sprite."),
|
||||
_("Set image _PARAM1_ on _PARAM0_"),
|
||||
_("Image"),
|
||||
"res/imageicon24.png",
|
||||
"res/imageicon.png")
|
||||
.AddParameter("object", _("Object"), "PanelSprite")
|
||||
.AddParameter("imageResource", _("Image file (or image resource name)"))
|
||||
.AddCodeOnlyParameter("currentScene", "0")
|
||||
.SetFunctionName("ChangeAndReloadImage");
|
||||
}
|
||||
|
@@ -61,6 +61,9 @@ class PanelSpriteObjectJsExtension : public gd::PlatformExtension {
|
||||
GetAllActionsForObject(
|
||||
"PanelSpriteObject::PanelSprite")["PanelSpriteObject::Image"]
|
||||
.SetFunctionName("setTexture");
|
||||
GetAllActionsForObject(
|
||||
"PanelSpriteObject::PanelSprite")["PanelSpriteObject::SetImageFromResource"]
|
||||
.SetFunctionName("setTexture");
|
||||
|
||||
GD_COMPLETE_EXTENSION_COMPILATION_INFORMATION();
|
||||
};
|
||||
|
@@ -148,8 +148,9 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
|
||||
gd::ParameterOptions::MakeNewOptions().SetDescription(
|
||||
_("Flow to compare to (in particles per second)")));
|
||||
|
||||
// Deprecated
|
||||
obj.AddAction("Texture",
|
||||
_("Particle image (using an expression)"),
|
||||
_("Particle image (deprecated)"),
|
||||
_("Change the image of particles (if displayed)."),
|
||||
_("Change the image of particles of _PARAM0_ to _PARAM1_"),
|
||||
_("Advanced"),
|
||||
@@ -157,7 +158,8 @@ void ExtensionSubDeclaration3(gd::ObjectMetadata& obj) {
|
||||
"CppPlatform/Extensions/particleSystemicon16.png")
|
||||
.AddParameter("object", _("Object"), "ParticleEmitter")
|
||||
.AddParameter("string", _("Image to use"))
|
||||
.SetParameterLongDescription("Indicate the name of the resource");
|
||||
.SetParameterLongDescription("Indicate the name of the resource")
|
||||
.SetHidden();
|
||||
|
||||
obj.AddAction("SetTextureFromResource",
|
||||
_("Particle image"),
|
||||
|
@@ -22,6 +22,7 @@ void DeclarePathfindingBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Movement")
|
||||
.SetTags("pathfinding, obstacle, collision")
|
||||
.SetExtensionHelpPath("/behaviors/pathfinding");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Pathfinding behavior"))
|
||||
.SetIcon("CppPlatform/Extensions/AStaricon16.png");
|
||||
|
@@ -34,7 +34,8 @@ module.exports = {
|
||||
'MIT'
|
||||
)
|
||||
.setExtensionHelpPath('/behaviors/physics2')
|
||||
.setCategory('Movement');
|
||||
.setCategory('Movement')
|
||||
.setTags("physics, gravity, obstacle, collision");
|
||||
extension
|
||||
.addInstructionOrExpressionGroupMetadata(_('Physics Engine 2.0'))
|
||||
.setIcon('res/physics32.png');
|
||||
|
@@ -27,6 +27,7 @@ void DeclarePlatformBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Movement")
|
||||
.SetTags("platformer, platform, character, jump, obstacle, collision")
|
||||
.SetExtensionHelpPath("/behaviors/platformer");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Platform behavior"))
|
||||
.SetIcon("CppPlatform/Extensions/platformerobjecticon.png");
|
||||
|
@@ -21,6 +21,7 @@ void DeclareTopDownMovementBehaviorExtension(gd::PlatformExtension& extension) {
|
||||
"Florian Rival",
|
||||
"Open source (MIT License)")
|
||||
.SetCategory("Movement")
|
||||
.SetTags("top-down")
|
||||
.SetExtensionHelpPath("/behaviors/topdown");
|
||||
extension.AddInstructionOrExpressionGroupMetadata(_("Top-down movement"))
|
||||
.SetIcon("CppPlatform/Extensions/topdownmovementicon16.png");
|
||||
|
@@ -74,6 +74,7 @@ module.exports = {
|
||||
'Open source (MIT License)'
|
||||
)
|
||||
.setCategory('Visual effect')
|
||||
.setTags("tween, interpolation, smooth")
|
||||
.setExtensionHelpPath('/behaviors/tween');
|
||||
extension
|
||||
.addInstructionOrExpressionGroupMetadata(_('Tweening'))
|
||||
@@ -708,7 +709,7 @@ module.exports = {
|
||||
_(
|
||||
'Tween the scale of _PARAM0_ to X-scale: _PARAM3_, Y-scale: _PARAM4_ (from center: _PARAM8_) with easing _PARAM5_ over _PARAM6_ms as _PARAM2_'
|
||||
),
|
||||
_('Scale'),
|
||||
_('Size'),
|
||||
'JsPlatform/Extensions/tween_behavior24.png',
|
||||
'JsPlatform/Extensions/tween_behavior32.png'
|
||||
)
|
||||
@@ -742,7 +743,7 @@ module.exports = {
|
||||
_(
|
||||
'Tween the X-scale of _PARAM0_ to _PARAM3_ (from center: _PARAM7_) with easing _PARAM4_ over _PARAM5_ms as _PARAM2_'
|
||||
),
|
||||
_('Scale'),
|
||||
_('Size'),
|
||||
'JsPlatform/Extensions/tween_behavior24.png',
|
||||
'JsPlatform/Extensions/tween_behavior32.png'
|
||||
)
|
||||
@@ -775,7 +776,7 @@ module.exports = {
|
||||
_(
|
||||
'Tween the Y-scale of _PARAM0_ to _PARAM3_ (from center: _PARAM7_) with easing _PARAM4_ over _PARAM5_ms as _PARAM2_'
|
||||
),
|
||||
_('Scale'),
|
||||
_('Size'),
|
||||
'JsPlatform/Extensions/tween_behavior24.png',
|
||||
'JsPlatform/Extensions/tween_behavior32.png'
|
||||
)
|
||||
@@ -839,7 +840,7 @@ module.exports = {
|
||||
_(
|
||||
'Tween the opacity of _PARAM0_ to _PARAM3_ with easing _PARAM4_ over _PARAM5_ms as _PARAM2_'
|
||||
),
|
||||
_('Opacity'),
|
||||
_('Visibility'),
|
||||
'JsPlatform/Extensions/tween_behavior24.png',
|
||||
'JsPlatform/Extensions/tween_behavior32.png'
|
||||
)
|
||||
|
@@ -34,6 +34,10 @@ void MetadataDeclarationHelper::DeclareExtension(
|
||||
.SetExtensionHelpPath(eventsFunctionsExtension.GetHelpPath())
|
||||
.SetIconUrl(eventsFunctionsExtension.GetIconUrl());
|
||||
|
||||
for(auto tag : eventsFunctionsExtension.GetTags()) {
|
||||
extension.AddTag(tag);
|
||||
}
|
||||
|
||||
if (!fullName.empty()) {
|
||||
extension.AddInstructionOrExpressionGroupMetadata(fullName).SetIcon(
|
||||
eventsFunctionsExtension.GetIconUrl());
|
||||
@@ -1348,7 +1352,7 @@ void MetadataDeclarationHelper::DeclareEventsFunctionParameters(
|
||||
}
|
||||
|
||||
gd::String MetadataDeclarationHelper::GetExtensionCodeNamespacePrefix(
|
||||
const gd::EventsFunctionsExtension eventsFunctionsExtension) {
|
||||
const gd::EventsFunctionsExtension &eventsFunctionsExtension) {
|
||||
return "gdjs.evtsExt__" + EventsCodeNameMangler::GetMangledName(
|
||||
eventsFunctionsExtension.GetName());
|
||||
}
|
||||
|
@@ -66,10 +66,30 @@ public:
|
||||
const gd::EventsBasedObject &eventsBasedObject,
|
||||
std::map<gd::String, gd::String> &objectMethodMangledNames);
|
||||
|
||||
/** Generate the namespace prefix for an extension. */
|
||||
static gd::String GetExtensionCodeNamespacePrefix(
|
||||
const gd::EventsFunctionsExtension &eventsFunctionsExtension);
|
||||
|
||||
/** Generate the fully qualified name for a free function. */
|
||||
static gd::String GetFreeFunctionCodeName(
|
||||
const gd::EventsFunctionsExtension &eventsFunctionsExtension,
|
||||
const gd::EventsFunction &eventsFunction);
|
||||
|
||||
/** Generate the namespace for a free function. */
|
||||
static gd::String
|
||||
GetFreeFunctionCodeNamespace(const gd::EventsFunction &eventsFunction,
|
||||
const gd::String &codeNamespacePrefix);
|
||||
|
||||
/** Generate the namespace for a behavior function. */
|
||||
static gd::String GetBehaviorFunctionCodeNamespace(
|
||||
const gd::EventsBasedBehavior &eventsBasedBehavior,
|
||||
const gd::String &codeNamespacePrefix);
|
||||
|
||||
/** Generate the namespace for an object function. */
|
||||
static gd::String
|
||||
GetObjectFunctionCodeNamespace(const gd::EventsBasedObject &eventsBasedObject,
|
||||
const gd::String &codeNamespacePrefix);
|
||||
|
||||
/**
|
||||
* Declare an extension from an events based extension.
|
||||
*/
|
||||
@@ -292,24 +312,6 @@ private:
|
||||
gd::MultipleInstructionMetadata &multipleInstructionMetadata,
|
||||
const int userDefinedFirstParameterIndex);
|
||||
|
||||
static gd::String GetExtensionCodeNamespacePrefix(
|
||||
const gd::EventsFunctionsExtension eventsFunctionsExtension);
|
||||
|
||||
/** Generate the namespace for a free function. */
|
||||
static gd::String
|
||||
GetFreeFunctionCodeNamespace(const gd::EventsFunction &eventsFunction,
|
||||
const gd::String &codeNamespacePrefix);
|
||||
|
||||
/** Generate the namespace for a behavior function. */
|
||||
static gd::String GetBehaviorFunctionCodeNamespace(
|
||||
const gd::EventsBasedBehavior &eventsBasedBehavior,
|
||||
const gd::String &codeNamespacePrefix);
|
||||
|
||||
/** Generate the namespace for an object function. */
|
||||
static gd::String
|
||||
GetObjectFunctionCodeNamespace(const gd::EventsBasedObject &eventsBasedObject,
|
||||
const gd::String &codeNamespacePrefix);
|
||||
|
||||
static gd::String RemoveTrailingDot(const gd::String &description);
|
||||
|
||||
static gd::String
|
||||
|
@@ -515,14 +515,19 @@ interface Project {
|
||||
|
||||
[Ref] VariablesContainer GetVariables();
|
||||
[Ref] ResourcesManager GetResourcesManager();
|
||||
void ExposeResources([Ref] ArbitraryResourceWorker worker);
|
||||
boolean STATIC_ValidateName([Const] DOMString name);
|
||||
|
||||
void SerializeTo([Ref] SerializerElement element);
|
||||
void UnserializeFrom([Const, Ref] SerializerElement element);
|
||||
|
||||
boolean STATIC_IsUsageOfUnicodeIdentifierNamesAllowed();
|
||||
void STATIC_AllowUsageOfUnicodeIdentifierNames(boolean enable);
|
||||
boolean STATIC_IsNameSafe([Const] DOMString name);
|
||||
[Const, Value] DOMString STATIC_GetSafeName([Const] DOMString name);
|
||||
|
||||
[Const, Value] DOMString FREE_GetTypeOfBehavior([Const, Ref] Layout layout, [Const] DOMString name, boolean searchInGroups);
|
||||
[Const, Value] DOMString FREE_GetTypeOfObject([Const, Ref] Layout layout, [Const] DOMString name, boolean searchInGroups);
|
||||
[Value] VectorString FREE_GetBehaviorsOfObject([Const, Ref] Layout layout, [Const] DOMString name, boolean searchInGroups);
|
||||
[Const, Value] DOMString FREE_GetTypeOfBehaviorInObjectOrGroup([Const, Ref] Layout layout, [Const] DOMString objectOrGroupName, [Const] DOMString behaviorName, boolean searchInGroups);
|
||||
|
||||
//Inherited from gd::ObjectsContainer
|
||||
[Ref] gdObject InsertNewObject([Ref] Project project, [Const] DOMString type, [Const] DOMString name, unsigned long pos);
|
||||
@@ -1119,6 +1124,7 @@ interface InitialInstancesContainer {
|
||||
boolean SomeInstancesAreOnLayer([Const] DOMString layer);
|
||||
void RenameInstancesOfObject([Const] DOMString oldName, [Const] DOMString newName);
|
||||
void RemoveInstance([Const, Ref] InitialInstance inst);
|
||||
unsigned long GetLayerInstancesCount([Const] DOMString layerName);
|
||||
|
||||
[Ref] InitialInstance InsertNewInitialInstance();
|
||||
[Ref] InitialInstance InsertInitialInstance([Const, Ref] InitialInstance inst);
|
||||
@@ -1285,7 +1291,7 @@ interface AbstractFunctionMetadata {
|
||||
[Ref] AbstractFunctionMetadata SetDefaultValue([Const] DOMString defaultValue);
|
||||
[Ref] AbstractFunctionMetadata SetParameterLongDescription([Const] DOMString longDescription);
|
||||
[Ref] AbstractFunctionMetadata SetParameterExtraInfo([Const] DOMString extraInfo);
|
||||
|
||||
|
||||
[Ref] AbstractFunctionMetadata SetHidden();
|
||||
[Ref] AbstractFunctionMetadata SetPrivate();
|
||||
|
||||
@@ -1348,7 +1354,7 @@ interface InstructionMetadata {
|
||||
[Ref] InstructionMetadata MarkAsComplex();
|
||||
|
||||
[Ref] InstructionMetadata GetCodeExtraInformation();
|
||||
|
||||
|
||||
[Ref] InstructionMetadata SetFunctionName([Const] DOMString functionName_);
|
||||
[Ref] InstructionMetadata SetAsyncFunctionName([Const] DOMString functionName_);
|
||||
[Const, Ref] DOMString GetFunctionName();
|
||||
@@ -1409,7 +1415,7 @@ interface ExpressionMetadata {
|
||||
[Const, Ref] DOMString GetRequiredBaseObjectCapability();
|
||||
|
||||
[Ref] ExpressionMetadata GetCodeExtraInformation();
|
||||
|
||||
|
||||
[Ref] ExpressionMetadata SetFunctionName([Const] DOMString functionName);
|
||||
[Const, Ref] DOMString GetFunctionName();
|
||||
[Ref] ExpressionMetadata SetStatic();
|
||||
@@ -1788,6 +1794,9 @@ interface PlatformExtension {
|
||||
[Ref] InstructionOrExpressionGroupMetadata AddInstructionOrExpressionGroupMetadata([Const] DOMString name);
|
||||
void MarkAsDeprecated();
|
||||
|
||||
[Const, Ref] VectorString GetTags();
|
||||
[Ref] PlatformExtension SetTags([Const] DOMString csvTags);
|
||||
|
||||
[Value] MultipleInstructionMetadata AddExpressionAndCondition(
|
||||
[Const] DOMString type,
|
||||
[Const] DOMString name,
|
||||
@@ -2239,6 +2248,10 @@ interface ProjectBrowserHelper {
|
||||
void STATIC_ExposeProjectEvents([Ref] Project project, [Ref] ArbitraryEventsWorker worker);
|
||||
};
|
||||
|
||||
interface ResourceExposer {
|
||||
void STATIC_ExposeWholeProjectResources([Ref] Project project, [Ref] ArbitraryResourceWorker worker);
|
||||
};
|
||||
|
||||
interface WholeProjectRefactorer {
|
||||
void STATIC_RenameEventsFunctionsExtension(
|
||||
[Ref] Project project,
|
||||
@@ -2373,6 +2386,9 @@ interface WholeProjectRefactorer {
|
||||
[Value] VectorUnfilledRequiredBehaviorPropertyProblem STATIC_FindInvalidRequiredBehaviorProperties([Const, Ref] Project project);
|
||||
[Value] VectorString STATIC_GetBehaviorsWithType([Const, Ref] gdObject obj, [Const] DOMString type);
|
||||
boolean STATIC_FixInvalidRequiredBehaviorProperties([Ref] Project project);
|
||||
void STATIC_RemoveLayer([Ref] Project project, [Ref] Layout layout, [Const] DOMString layerName);
|
||||
void STATIC_MergeLayers([Ref] Project project, [Ref] Layout layout, [Const] DOMString originLayerName, [Const] DOMString targetLayerName);
|
||||
unsigned long STATIC_GetLayoutAndExternalLayoutLayerInstancesCount([Ref] Project project, [Ref] Layout layout, [Const] DOMString layerName);
|
||||
};
|
||||
|
||||
interface PropertyFunctionGenerator {
|
||||
@@ -3385,7 +3401,7 @@ interface JsCodeEvent {
|
||||
[Prefix="gdjs::"]
|
||||
interface MetadataDeclarationHelper {
|
||||
void MetadataDeclarationHelper();
|
||||
|
||||
|
||||
void STATIC_DeclareExtension(
|
||||
[Ref] PlatformExtension extension,
|
||||
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension);
|
||||
@@ -3410,10 +3426,25 @@ interface MetadataDeclarationHelper {
|
||||
[Const, Ref] EventsBasedObject eventsBasedObject,
|
||||
[Ref] MapStringString objectMethodMangledNames);
|
||||
|
||||
[Const, Value] DOMString STATIC_GetExtensionCodeNamespacePrefix(
|
||||
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension);
|
||||
|
||||
[Const, Value] DOMString STATIC_GetFreeFunctionCodeName(
|
||||
[Const, Ref] EventsFunctionsExtension eventsFunctionsExtension,
|
||||
[Const, Ref] EventsFunction eventsFunction);
|
||||
|
||||
[Const, Value] DOMString STATIC_GetFreeFunctionCodeNamespace(
|
||||
[Const, Ref] EventsFunction eventsFunction,
|
||||
[Const] DOMString codeNamespacePrefix);
|
||||
|
||||
[Const, Value] DOMString STATIC_GetBehaviorFunctionCodeNamespace(
|
||||
[Const, Ref] EventsBasedBehavior eventsBasedBehavior,
|
||||
[Const] DOMString codeNamespacePrefix);
|
||||
|
||||
[Const, Value] DOMString STATIC_GetObjectFunctionCodeNamespace(
|
||||
[Const, Ref] EventsBasedObject eventsBasedObject,
|
||||
[Const] DOMString codeNamespacePrefix);
|
||||
|
||||
boolean STATIC_IsBehaviorLifecycleEventsFunction([Const] DOMString functionName);
|
||||
boolean STATIC_IsObjectLifecycleEventsFunction([Const] DOMString functionName);
|
||||
boolean STATIC_IsExtensionLifecycleEventsFunction([Const] DOMString functionName);
|
||||
|
@@ -522,7 +522,10 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
|
||||
#define STATIC_CreateNewGDJSProject CreateNewGDJSProject
|
||||
#define STATIC_InitializePlatforms InitializePlatforms
|
||||
#define STATIC_ValidateName ValidateName
|
||||
#define STATIC_IsUsageOfUnicodeIdentifierNamesAllowed IsUsageOfUnicodeIdentifierNamesAllowed
|
||||
#define STATIC_AllowUsageOfUnicodeIdentifierNames AllowUsageOfUnicodeIdentifierNames
|
||||
#define STATIC_IsNameSafe IsNameSafe
|
||||
#define STATIC_GetSafeName GetSafeName
|
||||
#define STATIC_ToJSON ToJSON
|
||||
#define STATIC_FromJSON(x) FromJSON(x)
|
||||
#define STATIC_IsObject IsObject
|
||||
@@ -562,6 +565,7 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
#define STATIC_RemoveObjectInEvents RemoveObjectInEvents
|
||||
#define STATIC_ReplaceStringInEvents ReplaceStringInEvents
|
||||
#define STATIC_ExposeProjectEvents ExposeProjectEvents
|
||||
#define STATIC_ExposeWholeProjectResources ExposeWholeProjectResources
|
||||
|
||||
#define STATIC_GetBehaviorMetadata GetBehaviorMetadata
|
||||
#define STATIC_GetObjectMetadata GetObjectMetadata
|
||||
@@ -615,6 +619,10 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
GetBehaviorsWithType
|
||||
#define STATIC_FixInvalidRequiredBehaviorProperties \
|
||||
FixInvalidRequiredBehaviorProperties
|
||||
#define STATIC_RemoveLayer RemoveLayer
|
||||
#define STATIC_MergeLayers MergeLayers
|
||||
#define STATIC_GetLayoutAndExternalLayoutLayerInstancesCount GetLayoutAndExternalLayoutLayerInstancesCount
|
||||
|
||||
#define STATIC_GenerateBehaviorGetterAndSetter GenerateBehaviorGetterAndSetter
|
||||
#define STATIC_GenerateObjectGetterAndSetter GenerateObjectGetterAndSetter
|
||||
#define STATIC_CanGenerateGetterAndSetter CanGenerateGetterAndSetter
|
||||
@@ -693,7 +701,11 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
#define STATIC_DeclareExtension DeclareExtension
|
||||
#define STATIC_GenerateBehaviorMetadata GenerateBehaviorMetadata
|
||||
#define STATIC_GenerateObjectMetadata GenerateObjectMetadata
|
||||
#define STATIC_GetExtensionCodeNamespacePrefix GetExtensionCodeNamespacePrefix
|
||||
#define STATIC_GetFreeFunctionCodeName GetFreeFunctionCodeName
|
||||
#define STATIC_GetFreeFunctionCodeNamespace GetFreeFunctionCodeNamespace
|
||||
#define STATIC_GetBehaviorFunctionCodeNamespace GetBehaviorFunctionCodeNamespace
|
||||
#define STATIC_GetObjectFunctionCodeNamespace GetObjectFunctionCodeNamespace
|
||||
#define STATIC_IsBehaviorLifecycleEventsFunction IsBehaviorLifecycleEventsFunction
|
||||
#define STATIC_IsObjectLifecycleEventsFunction IsObjectLifecycleEventsFunction
|
||||
#define STATIC_IsExtensionLifecycleEventsFunction IsExtensionLifecycleEventsFunction
|
||||
|
@@ -99,10 +99,38 @@ describe('libGD.js', function () {
|
||||
expect(project.hasExternalLayoutNamed('My layout')).toBe(false);
|
||||
});
|
||||
|
||||
it('should validate object names', function () {
|
||||
expect(gd.Project.validateName('ThisNameIs_Ok_123')).toBe(true);
|
||||
expect(gd.Project.validateName('ThisName IsNot_Ok_123')).toBe(false);
|
||||
expect(gd.Project.validateName('ThisNameIsNot_Ok!')).toBe(false);
|
||||
it('should validate object names (legacy)', function () {
|
||||
gd.Project.allowUsageOfUnicodeIdentifierNames(false);
|
||||
expect(gd.Project.isNameSafe('ThisNameIs_Ok_123')).toBe(true);
|
||||
expect(gd.Project.isNameSafe('ThisName IsNot_Ok_123')).toBe(false);
|
||||
expect(gd.Project.isNameSafe('ThisNameIsNot_Ok!')).toBe(false);
|
||||
expect(gd.Project.isNameSafe('1ThisNameIsNot_Ok_123')).toBe(false);
|
||||
expect(gd.Project.getSafeName('ThisNameIs_Ok_123')).toBe('ThisNameIs_Ok_123');
|
||||
expect(gd.Project.getSafeName('ThisName IsNot_Ok_123')).toBe('ThisName_IsNot_Ok_123');
|
||||
expect(gd.Project.getSafeName('ThisNameIsNot_Ok!')).toBe('ThisNameIsNot_Ok_');
|
||||
expect(gd.Project.getSafeName('1ThisNameIsNot_Ok_123')).toBe('_1ThisNameIsNot_Ok_123');
|
||||
expect(gd.Project.getSafeName('官话 name')).toBe('___name');
|
||||
expect(gd.Project.getSafeName('')).toBe('Unnamed');
|
||||
expect(gd.Project.getSafeName('9')).toBe('_9');
|
||||
});
|
||||
|
||||
it('should validate object names (unicode)', function () {
|
||||
gd.Project.allowUsageOfUnicodeIdentifierNames(true);
|
||||
expect(gd.Project.isNameSafe('ThisNameIs_Ok_123')).toBe(true);
|
||||
expect(gd.Project.isNameSafe('ThisNameIs_👍_123')).toBe(true);
|
||||
expect(gd.Project.isNameSafe('ThisName IsNot_Ok_123')).toBe(false);
|
||||
expect(gd.Project.isNameSafe('ThisName()IsNot_Ok_123')).toBe(false);
|
||||
expect(gd.Project.isNameSafe('ThisNameIsNot_Ok!')).toBe(false);
|
||||
expect(gd.Project.isNameSafe('1ThisNameIsNot_Ok_123')).toBe(false);
|
||||
expect(gd.Project.getSafeName('ThisNameIs_Ok_123')).toBe('ThisNameIs_Ok_123');
|
||||
expect(gd.Project.getSafeName('ThisNameIs_👍_123')).toBe('ThisNameIs_👍_123');
|
||||
expect(gd.Project.getSafeName('ThisName IsNot_Ok_123')).toBe('ThisName_IsNot_Ok_123');
|
||||
expect(gd.Project.getSafeName('ThisName()IsNot_Ok_123')).toBe('ThisName__IsNot_Ok_123');
|
||||
expect(gd.Project.getSafeName('ThisNameIsNot_Ok!')).toBe('ThisNameIsNot_Ok_');
|
||||
expect(gd.Project.getSafeName('1ThisNameIsNot_Ok_123')).toBe('_1ThisNameIsNot_Ok_123');
|
||||
expect(gd.Project.getSafeName('官话 name')).toBe('官话_name');
|
||||
expect(gd.Project.getSafeName('')).toBe('Unnamed');
|
||||
expect(gd.Project.getSafeName('9')).toBe('_9');
|
||||
});
|
||||
|
||||
it('should have a list of extensions', function () {
|
||||
@@ -1370,7 +1398,7 @@ describe('libGD.js', function () {
|
||||
);
|
||||
|
||||
const worker = new gd.ResourcesInUseHelper();
|
||||
project.exposeResources(worker);
|
||||
gd.ResourceExposer.exposeWholeProjectResources(project, worker);
|
||||
expect(worker.getAllImages().toNewVectorString().toJSArray().length).toBe(
|
||||
1
|
||||
);
|
||||
@@ -1381,7 +1409,7 @@ describe('libGD.js', function () {
|
||||
gd.ProjectResourcesAdder.removeAllUseless(project, 'image');
|
||||
|
||||
const newWorker = new gd.ResourcesInUseHelper();
|
||||
project.exposeResources(newWorker);
|
||||
gd.ResourceExposer.exposeWholeProjectResources(project, newWorker);
|
||||
expect(
|
||||
newWorker.getAllImages().toNewVectorString().toJSArray().length
|
||||
).toBe(1);
|
||||
@@ -1414,7 +1442,7 @@ describe('libGD.js', function () {
|
||||
},
|
||||
});
|
||||
|
||||
project.exposeResources(worker);
|
||||
gd.ResourceExposer.exposeWholeProjectResources(project, worker);
|
||||
project.delete();
|
||||
});
|
||||
});
|
||||
@@ -3123,7 +3151,7 @@ describe('libGD.js', function () {
|
||||
// Check that ResourcesMergingHelper can update the filenames
|
||||
const resourcesMergingHelper = new gd.ResourcesMergingHelper(fs);
|
||||
resourcesMergingHelper.setBaseDirectory('/my/project/');
|
||||
project.exposeResources(resourcesMergingHelper);
|
||||
gd.ResourceExposer.exposeWholeProjectResources(project, resourcesMergingHelper);
|
||||
|
||||
const oldAndNewFilenames =
|
||||
resourcesMergingHelper.getAllResourcesOldAndNewFilename();
|
||||
|
@@ -45,16 +45,16 @@ describe('MetadataDeclarationHelper', () => {
|
||||
project.delete();
|
||||
});
|
||||
|
||||
it('can create metadata for free actions with an underscore', () => {
|
||||
it('can create metadata for free actions with an underscore and unicode characters', () => {
|
||||
const extension = new gd.PlatformExtension();
|
||||
const project = new gd.Project();
|
||||
|
||||
const eventExtension = project.insertNewEventsFunctionsExtension(
|
||||
'MyExtension',
|
||||
'My🧩Extension',
|
||||
0
|
||||
);
|
||||
const eventFunction = eventExtension.insertNewEventsFunction(
|
||||
'My_Function',
|
||||
'My_📝Function',
|
||||
0
|
||||
);
|
||||
eventFunction.setFunctionType(gd.EventsFunction.Action);
|
||||
@@ -68,10 +68,10 @@ describe('MetadataDeclarationHelper', () => {
|
||||
);
|
||||
metadataDeclarationHelper.delete();
|
||||
|
||||
expect(extension.getAllActions().has('My_Function')).toBe(true);
|
||||
const action = extension.getAllActions().get('My_Function');
|
||||
expect(extension.getAllActions().has('My_📝Function')).toBe(true);
|
||||
const action = extension.getAllActions().get('My_📝Function');
|
||||
expect(action.getFunctionName()).toBe(
|
||||
'gdjs.evtsExt__MyExtension__My_Function.func'
|
||||
'gdjs.evtsExt__My_129513Extension__My__128221Function.func'
|
||||
);
|
||||
|
||||
extension.delete();
|
||||
|
@@ -163,6 +163,7 @@ type ParticleEmitterObject_RendererType = 0 | 1 | 2`
|
||||
' getTypeOfObject(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, searchInGroups: boolean): string;',
|
||||
' getTypeOfBehavior(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, searchInGroups: boolean): string;',
|
||||
' getBehaviorsOfObject(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, searchInGroups: boolean): gdVectorString;',
|
||||
' getTypeOfBehaviorInObjectOrGroup(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, behaviorName: string, searchInGroups: boolean): string;',
|
||||
'',
|
||||
' removeFromVectorParameterMetadata(gdVectorParameterMetadata, index: number): void;',
|
||||
' swapInVectorParameterMetadata(gdVectorParameterMetadata, oldIndex: number, newIndex: number): void;',
|
||||
|
@@ -12,6 +12,7 @@ declare class gdInitialInstancesContainer {
|
||||
someInstancesAreOnLayer(layer: string): boolean;
|
||||
renameInstancesOfObject(oldName: string, newName: string): void;
|
||||
removeInstance(inst: gdInitialInstance): void;
|
||||
getLayerInstancesCount(layerName: string): number;
|
||||
insertNewInitialInstance(): gdInitialInstance;
|
||||
insertInitialInstance(inst: gdInitialInstance): gdInitialInstance;
|
||||
serializeTo(element: gdSerializerElement): void;
|
||||
|
@@ -5,7 +5,11 @@ declare class gdMetadataDeclarationHelper {
|
||||
generateFreeFunctionMetadata(project: gdProject, extension: gdPlatformExtension, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsFunction: gdEventsFunction): gdAbstractFunctionMetadata;
|
||||
static generateBehaviorMetadata(project: gdProject, extension: gdPlatformExtension, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedBehavior: gdEventsBasedBehavior, behaviorMethodMangledNames: gdMapStringString): gdBehaviorMetadata;
|
||||
static generateObjectMetadata(project: gdProject, extension: gdPlatformExtension, eventsFunctionsExtension: gdEventsFunctionsExtension, eventsBasedObject: gdEventsBasedObject, objectMethodMangledNames: gdMapStringString): gdObjectMetadata;
|
||||
static getExtensionCodeNamespacePrefix(eventsFunctionsExtension: gdEventsFunctionsExtension): string;
|
||||
static getFreeFunctionCodeName(eventsFunctionsExtension: gdEventsFunctionsExtension, eventsFunction: gdEventsFunction): string;
|
||||
static getFreeFunctionCodeNamespace(eventsFunction: gdEventsFunction, codeNamespacePrefix: string): string;
|
||||
static getBehaviorFunctionCodeNamespace(eventsBasedBehavior: gdEventsBasedBehavior, codeNamespacePrefix: string): string;
|
||||
static getObjectFunctionCodeNamespace(eventsBasedObject: gdEventsBasedObject, codeNamespacePrefix: string): string;
|
||||
static isBehaviorLifecycleEventsFunction(functionName: string): boolean;
|
||||
static isObjectLifecycleEventsFunction(functionName: string): boolean;
|
||||
static isExtensionLifecycleEventsFunction(functionName: string): boolean;
|
||||
|
@@ -7,6 +7,8 @@ declare class gdPlatformExtension {
|
||||
setCategory(category: string): gdPlatformExtension;
|
||||
addInstructionOrExpressionGroupMetadata(name: string): gdInstructionOrExpressionGroupMetadata;
|
||||
markAsDeprecated(): void;
|
||||
getTags(): gdVectorString;
|
||||
setTags(csvTags: string): gdPlatformExtension;
|
||||
addExpressionAndCondition(type: string, name: string, fullname: string, description: string, sentenceName: string, group: string, icon: string): gdMultipleInstructionMetadata;
|
||||
addExpressionAndConditionAndAction(type: string, name: string, fullname: string, description: string, sentenceName: string, group: string, icon: string): gdMultipleInstructionMetadata;
|
||||
addCondition(name: string, fullname: string, description: string, sentence: string, group: string, icon: string, smallicon: string): gdInstructionMetadata;
|
||||
|
@@ -101,13 +101,16 @@ declare class gdProject extends gdObjectsContainer {
|
||||
getEventsBasedObject(type: string): gdEventsBasedObject;
|
||||
getVariables(): gdVariablesContainer;
|
||||
getResourcesManager(): gdResourcesManager;
|
||||
exposeResources(worker: gdArbitraryResourceWorker): void;
|
||||
static validateName(name: string): boolean;
|
||||
serializeTo(element: gdSerializerElement): void;
|
||||
unserializeFrom(element: gdSerializerElement): void;
|
||||
static isUsageOfUnicodeIdentifierNamesAllowed(): boolean;
|
||||
static allowUsageOfUnicodeIdentifierNames(enable: boolean): void;
|
||||
static isNameSafe(name: string): boolean;
|
||||
static getSafeName(name: string): string;
|
||||
getTypeOfBehavior(layout: gdLayout, name: string, searchInGroups: boolean): string;
|
||||
getTypeOfObject(layout: gdLayout, name: string, searchInGroups: boolean): string;
|
||||
getBehaviorsOfObject(layout: gdLayout, name: string, searchInGroups: boolean): gdVectorString;
|
||||
getTypeOfBehaviorInObjectOrGroup(layout: gdLayout, objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
|
||||
insertNewObject(project: gdProject, type: string, name: string, pos: number): gdObject;
|
||||
insertObject(obj: gdObject, pos: number): gdObject;
|
||||
hasObjectNamed(name: string): boolean;
|
||||
|
6
GDevelop.js/types/gdresourceexposer.js
Normal file
6
GDevelop.js/types/gdresourceexposer.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdResourceExposer {
|
||||
static exposeWholeProjectResources(project: gdProject, worker: gdArbitraryResourceWorker): void;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -38,6 +38,9 @@ declare class gdWholeProjectRefactorer {
|
||||
static findInvalidRequiredBehaviorProperties(project: gdProject): gdVectorUnfilledRequiredBehaviorPropertyProblem;
|
||||
static getBehaviorsWithType(obj: gdObject, type: string): gdVectorString;
|
||||
static fixInvalidRequiredBehaviorProperties(project: gdProject): boolean;
|
||||
static removeLayer(project: gdProject, layout: gdLayout, layerName: string): void;
|
||||
static mergeLayers(project: gdProject, layout: gdLayout, originLayerName: string, targetLayerName: string): void;
|
||||
static getLayoutAndExternalLayoutLayerInstancesCount(project: gdProject, layout: gdLayout, layerName: string): number;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -7,6 +7,7 @@ declare class libGDevelop {
|
||||
getTypeOfObject(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, searchInGroups: boolean): string;
|
||||
getTypeOfBehavior(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, searchInGroups: boolean): string;
|
||||
getBehaviorsOfObject(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, searchInGroups: boolean): gdVectorString;
|
||||
getTypeOfBehaviorInObjectOrGroup(globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, objectName: string, behaviorName: string, searchInGroups: boolean): string;
|
||||
|
||||
removeFromVectorParameterMetadata(gdVectorParameterMetadata, index: number): void;
|
||||
swapInVectorParameterMetadata(gdVectorParameterMetadata, oldIndex: number, newIndex: number): void;
|
||||
@@ -154,6 +155,7 @@ declare class libGDevelop {
|
||||
UnfilledRequiredBehaviorPropertyProblem: Class<gdUnfilledRequiredBehaviorPropertyProblem>;
|
||||
VectorUnfilledRequiredBehaviorPropertyProblem: Class<gdVectorUnfilledRequiredBehaviorPropertyProblem>;
|
||||
ProjectBrowserHelper: Class<gdProjectBrowserHelper>;
|
||||
ResourceExposer: Class<gdResourceExposer>;
|
||||
WholeProjectRefactorer: Class<gdWholeProjectRefactorer>;
|
||||
PropertyFunctionGenerator: Class<gdPropertyFunctionGenerator>;
|
||||
UsedExtensionsResult: Class<gdUsedExtensionsResult>;
|
||||
|
@@ -472,7 +472,7 @@ You can also [create](/gdevelop5/extensions/create) directly in your project new
|
||||
Read more about this:
|
||||
|
||||
* [Create your own extensions](/gdevelop5/extensions/create)
|
||||
* [Share extensions with the community](/gdevelop5/extensions/share)
|
||||
* [Share extensions with the community](/gdevelop5/extensions/share-extension)
|
||||
* [Extend GDevelop with JavaScript or C++](/gdevelop5/extensions/extend-gdevelop)
|
||||
|
||||
`;
|
||||
|
@@ -6,11 +6,12 @@ import {
|
||||
isPrivateAsset,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { getPixelatedImageRendering } from '../Utils/CssHelpers';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import Text from '../UI/Text';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import CheckeredBackground from '../ResourcesList/CheckeredBackground';
|
||||
import AuthorizedAssetImage from './PrivateAssets/AuthorizedAssetImage';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
const paddingSize = 10;
|
||||
const styles = {
|
||||
@@ -43,17 +44,15 @@ const styles = {
|
||||
bottom: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
height: 48,
|
||||
color: '#fff',
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
backgroundColor: 'rgb(0,0,0,0.5)',
|
||||
display: 'inline-block', // Necessary to have the text ellipsis working.
|
||||
textAlign: 'center',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
title: {
|
||||
whiteSpace: 'nowrap',
|
||||
overflow: 'hidden',
|
||||
textOverflow: 'ellipsis',
|
||||
...textEllipsisStyle,
|
||||
overflowWrap: 'break-word',
|
||||
},
|
||||
};
|
||||
|
||||
@@ -61,15 +60,9 @@ type Props = {|
|
||||
id?: string,
|
||||
size: number,
|
||||
assetShortHeader: AssetShortHeader,
|
||||
onOpenDetails: () => void,
|
||||
|};
|
||||
|
||||
export const AssetCard = ({
|
||||
id,
|
||||
assetShortHeader,
|
||||
onOpenDetails,
|
||||
size,
|
||||
}: Props) => {
|
||||
export const AssetCard = ({ id, assetShortHeader, size }: Props) => {
|
||||
const previewImageUrl = assetShortHeader.previewImageUrls[0];
|
||||
const isPrivate = isPrivateAsset(assetShortHeader);
|
||||
const style = {
|
||||
@@ -81,38 +74,40 @@ export const AssetCard = ({
|
||||
: undefined),
|
||||
};
|
||||
return (
|
||||
<ButtonBase onClick={onOpenDetails} focusRipple>
|
||||
<div id={id} style={{ ...styles.cardContainer, width: size, height: size }}>
|
||||
<div style={{ ...styles.previewContainer, width: size, height: size }}>
|
||||
<CheckeredBackground />
|
||||
{isPrivate ? (
|
||||
<AuthorizedAssetImage
|
||||
key={previewImageUrl}
|
||||
style={style}
|
||||
url={previewImageUrl}
|
||||
alt={assetShortHeader.name}
|
||||
/>
|
||||
) : (
|
||||
<CorsAwareImage
|
||||
key={previewImageUrl}
|
||||
style={style}
|
||||
src={previewImageUrl}
|
||||
alt={assetShortHeader.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div
|
||||
id={id}
|
||||
style={{ ...styles.cardContainer, width: size, height: size }}
|
||||
style={{
|
||||
...styles.titleContainer,
|
||||
height: assetShortHeader.shortDescription ? 40 : 20,
|
||||
}}
|
||||
>
|
||||
<div style={{ ...styles.previewContainer, width: size, height: size }}>
|
||||
<CheckeredBackground />
|
||||
{isPrivate ? (
|
||||
<AuthorizedAssetImage
|
||||
key={previewImageUrl}
|
||||
style={style}
|
||||
url={previewImageUrl}
|
||||
alt={assetShortHeader.name}
|
||||
/>
|
||||
) : (
|
||||
<CorsAwareImage
|
||||
key={previewImageUrl}
|
||||
style={style}
|
||||
src={previewImageUrl}
|
||||
alt={assetShortHeader.name}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div style={styles.titleContainer}>
|
||||
<Text noMargin style={styles.title} color="inherit">
|
||||
{assetShortHeader.name}
|
||||
</Text>
|
||||
<Text noMargin style={styles.title} color="inherit">
|
||||
{capitalize(assetShortHeader.name)}
|
||||
</Text>
|
||||
{assetShortHeader.shortDescription && (
|
||||
<Text noMargin style={styles.title} size="body2" color="inherit">
|
||||
{assetShortHeader.shortDescription}
|
||||
</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ButtonBase>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
isPixelArt,
|
||||
isPrivateAsset,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import { LineStackLayout, ResponsiveLineStackLayout } from '../UI/Layout';
|
||||
@@ -24,16 +25,14 @@ import SelectOption from '../UI/SelectOption';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import AnimationPreview from '../ObjectEditor/Editors/SpriteEditor/AnimationPreview';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
import { AssetCard } from './AssetCard';
|
||||
import AssetsList from './AssetsList';
|
||||
import { SimilarAssetStoreSearchFilter } from './AssetStoreSearchFilter';
|
||||
import EmptyMessage from '../UI/EmptyMessage';
|
||||
import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
|
||||
import Link from '../UI/Link';
|
||||
import PrivateAssetsAuthorizationContext from './PrivateAssets/PrivateAssetsAuthorizationContext';
|
||||
import AuthorizedAssetImage from './PrivateAssets/AuthorizedAssetImage';
|
||||
import { MarkdownText } from '../UI/MarkdownText';
|
||||
import Paper from '../UI/Paper';
|
||||
import PublicProfileContext from '../Profile/PublicProfileContext';
|
||||
import {
|
||||
getUserPublicProfilesByIds,
|
||||
type UserPublicProfile,
|
||||
@@ -41,6 +40,8 @@ import {
|
||||
import { getPixelatedImageRendering } from '../Utils/CssHelpers';
|
||||
import ArrowRight from '../UI/CustomSvgIcons/ArrowRight';
|
||||
import ArrowLeft from '../UI/CustomSvgIcons/ArrowLeft';
|
||||
import PublicProfileDialog from '../Profile/PublicProfileDialog';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
const FIXED_HEIGHT = 250;
|
||||
const FIXED_WIDTH = 300;
|
||||
@@ -73,21 +74,14 @@ const styles = {
|
||||
arrowContainer: {
|
||||
padding: 6,
|
||||
},
|
||||
scrollView: {
|
||||
// This is needed to make the scroll view take the full height of the container,
|
||||
// allowing the Autosizer of the BoxSearchResults to be visible.
|
||||
display: 'flex',
|
||||
},
|
||||
};
|
||||
|
||||
const makeFirstLetterUppercase = (str: string) =>
|
||||
str.charAt(0).toUpperCase() + str.slice(1);
|
||||
|
||||
type Props = {|
|
||||
onTagSelection: (tag: string) => void,
|
||||
assetShortHeader: AssetShortHeader,
|
||||
onOpenDetails: (assetShortHeader: AssetShortHeader) => void,
|
||||
onAssetLoaded?: () => void,
|
||||
onPrivateAssetPackSelection: (assetPack: PrivateAssetPackListingData) => void,
|
||||
|};
|
||||
|
||||
const getObjectAssetResourcesByName = (
|
||||
@@ -109,7 +103,13 @@ export type AssetDetailsInterface = {|
|
||||
|
||||
export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
(
|
||||
{ onTagSelection, assetShortHeader, onOpenDetails, onAssetLoaded }: Props,
|
||||
{
|
||||
onTagSelection,
|
||||
assetShortHeader,
|
||||
onOpenDetails,
|
||||
onAssetLoaded,
|
||||
onPrivateAssetPackSelection,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const {
|
||||
@@ -117,7 +117,6 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
licenses,
|
||||
environment,
|
||||
error: filterError,
|
||||
fetchAssetsAndFilters,
|
||||
useSearchItem,
|
||||
} = React.useContext(AssetStoreContext);
|
||||
const [asset, setAsset] = React.useState<?Asset>(null);
|
||||
@@ -130,10 +129,13 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
const { fetchPrivateAsset } = React.useContext(
|
||||
PrivateAssetsAuthorizationContext
|
||||
);
|
||||
const { openUserPublicProfile } = React.useContext(PublicProfileContext);
|
||||
const [authorPublicProfiles, setAuthorPublicProfiles] = React.useState<
|
||||
UserPublicProfile[]
|
||||
>([]);
|
||||
const [
|
||||
selectedAuthorPublicProfile,
|
||||
setSelectedAuthorPublicProfile,
|
||||
] = React.useState<?UserPublicProfile>(null);
|
||||
|
||||
const scrollView = React.useRef<?ScrollViewInterface>(null);
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
@@ -288,13 +290,13 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
const truncatedSearchResults = searchResults && searchResults.slice(0, 60);
|
||||
|
||||
return (
|
||||
<ScrollView ref={scrollView} style={styles.scrollView}>
|
||||
<ScrollView ref={scrollView}>
|
||||
<Column expand noMargin>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Column>
|
||||
<LineStackLayout alignItems="baseline" noMargin>
|
||||
<Text size="block-title" displayInlineAsSpan>
|
||||
{assetShortHeader.name}
|
||||
{capitalize(assetShortHeader.name)}
|
||||
</Text>
|
||||
{!areAuthorsLoading && (
|
||||
<LineStackLayout noMargin>
|
||||
@@ -325,7 +327,9 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
key={userPublicProfile.id}
|
||||
href="#"
|
||||
onClick={() =>
|
||||
openUserPublicProfile(userPublicProfile.id)
|
||||
setSelectedAuthorPublicProfile(
|
||||
userPublicProfile
|
||||
)
|
||||
}
|
||||
>
|
||||
{username}
|
||||
@@ -344,7 +348,7 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
<Chip
|
||||
size="small"
|
||||
style={styles.chip}
|
||||
label={makeFirstLetterUppercase(tag)}
|
||||
label={capitalize(tag)}
|
||||
onClick={() => {
|
||||
onTagSelection(tag);
|
||||
}}
|
||||
@@ -462,7 +466,7 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
value={animation.name}
|
||||
label={
|
||||
!isAnimationNameEmpty
|
||||
? makeFirstLetterUppercase(animation.name)
|
||||
? capitalize(animation.name)
|
||||
: t`Default` // Display default for animations with no name.
|
||||
}
|
||||
shouldNotTranslate={!isAnimationNameEmpty}
|
||||
@@ -539,34 +543,35 @@ export const AssetDetails = React.forwardRef<Props, AssetDetailsInterface>(
|
||||
</Text>
|
||||
</Line>
|
||||
<Line expand noMargin justifyContent="center">
|
||||
<BoxSearchResults
|
||||
baseSize={128}
|
||||
onRetry={fetchAssetsAndFilters}
|
||||
error={filterError}
|
||||
searchItems={truncatedSearchResults}
|
||||
spacing={8}
|
||||
renderSearchItem={(assetShortHeader, size) => (
|
||||
<AssetCard
|
||||
size={size}
|
||||
onOpenDetails={() => {
|
||||
setAsset(null);
|
||||
onOpenDetails(assetShortHeader);
|
||||
}}
|
||||
assetShortHeader={assetShortHeader}
|
||||
/>
|
||||
)}
|
||||
noResultPlaceholder={
|
||||
<AssetsList
|
||||
assetShortHeaders={truncatedSearchResults}
|
||||
onOpenDetails={assetShortHeader => {
|
||||
setAsset(null);
|
||||
onOpenDetails(assetShortHeader);
|
||||
}}
|
||||
noScroll
|
||||
noResultsPlaceHolder={
|
||||
<Line alignItems="flex-start">
|
||||
<EmptyMessage>
|
||||
<Trans>No similar asset was found.</Trans>
|
||||
</EmptyMessage>
|
||||
</Line>
|
||||
}
|
||||
noScroll
|
||||
error={filterError}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
{selectedAuthorPublicProfile && (
|
||||
<PublicProfileDialog
|
||||
userId={selectedAuthorPublicProfile.id}
|
||||
onClose={() => setSelectedAuthorPublicProfile(null)}
|
||||
onAssetPackOpen={assetPack => {
|
||||
onPrivateAssetPackSelection(assetPack);
|
||||
setSelectedAuthorPublicProfile(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
</ScrollView>
|
||||
);
|
||||
|
397
newIDE/app/src/AssetStore/AssetPackTiles.js
Normal file
397
newIDE/app/src/AssetStore/AssetPackTiles.js
Normal file
@@ -0,0 +1,397 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { I18n } from '@lingui/react';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import {
|
||||
type PublicAssetPack,
|
||||
type AssetShortHeader,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
|
||||
import GridListTile from '@material-ui/core/GridListTile';
|
||||
import createStyles from '@material-ui/core/styles/createStyles';
|
||||
import makeStyles from '@material-ui/core/styles/makeStyles';
|
||||
import { shouldValidate } from '../UI/KeyboardShortcuts/InteractionKeys';
|
||||
import Paper from '../UI/Paper';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import { Column, Line } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import {
|
||||
PrivateAssetPackPriceTag,
|
||||
formatPrivateAssetPackPrice,
|
||||
} from './PrivateAssets/PrivateAssetPackPriceTag';
|
||||
import { AssetCard } from './AssetCard';
|
||||
import FolderIcon from '../UI/CustomSvgIcons/Folder';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import GDevelopThemeContext from '../UI/Theme/GDevelopThemeContext';
|
||||
|
||||
const styles = {
|
||||
priceTagContainer: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
cursor: 'default',
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
// Prevent cumulative layout shift by enforcing
|
||||
// the 16:9 ratio.
|
||||
aspectRatio: '16 / 9',
|
||||
objectFit: 'cover',
|
||||
position: 'relative',
|
||||
},
|
||||
promoImage: {
|
||||
width: '20%',
|
||||
minWidth: 200,
|
||||
margin: 4,
|
||||
},
|
||||
paper: {
|
||||
margin: 4,
|
||||
minWidth: 180,
|
||||
},
|
||||
packTitle: {
|
||||
...textEllipsisStyle,
|
||||
overflowWrap: 'break-word',
|
||||
},
|
||||
folderTitle: {
|
||||
marginLeft: 8,
|
||||
},
|
||||
folderPaper: {
|
||||
height: 55,
|
||||
},
|
||||
folderIcon: {
|
||||
width: 32,
|
||||
height: 32,
|
||||
},
|
||||
promoLineContainer: {
|
||||
borderRadius: 8,
|
||||
padding: 2,
|
||||
flex: 1,
|
||||
},
|
||||
promoImageContainer: {
|
||||
display: 'flex',
|
||||
flex: 0,
|
||||
},
|
||||
};
|
||||
|
||||
const useStylesForGridListItem = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
'&:focus': {
|
||||
outline: `1px solid ${theme.palette.primary.light}`,
|
||||
},
|
||||
'&:hover': {
|
||||
outline: `1px solid ${theme.palette.primary.light}`,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
export const AssetCardTile = ({
|
||||
assetShortHeader,
|
||||
onOpenDetails,
|
||||
size,
|
||||
margin,
|
||||
}: {|
|
||||
assetShortHeader: AssetShortHeader,
|
||||
onOpenDetails: () => void,
|
||||
size: number,
|
||||
margin?: number,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onOpenDetails();
|
||||
}
|
||||
}}
|
||||
onClick={onOpenDetails}
|
||||
style={{
|
||||
margin,
|
||||
}}
|
||||
>
|
||||
<AssetCard
|
||||
id={`asset-card-${assetShortHeader.name.replace(/\s/g, '-')}`}
|
||||
assetShortHeader={assetShortHeader}
|
||||
size={size}
|
||||
/>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const AssetFolderTile = ({
|
||||
tag,
|
||||
onSelect,
|
||||
style,
|
||||
}: {|
|
||||
tag: string,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Column noMargin id={`asset-folder-${tag.replace(/\s/g, '-')}`}>
|
||||
<Line alignItems="center">
|
||||
<FolderIcon style={styles.folderIcon} />
|
||||
<Text noMargin style={styles.folderTitle} size="sub-title">
|
||||
{capitalize(tag)}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const PublicAssetPackTile = ({
|
||||
assetPack,
|
||||
onSelect,
|
||||
style,
|
||||
}: {|
|
||||
assetPack: PublicAssetPack,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Paper
|
||||
id={`asset-pack-${assetPack.tag.replace(/\s/g, '-')}`}
|
||||
elevation={2}
|
||||
style={styles.paper}
|
||||
background="light"
|
||||
>
|
||||
<CorsAwareImage
|
||||
key={assetPack.name}
|
||||
style={styles.previewImage}
|
||||
src={assetPack.thumbnailUrl}
|
||||
alt={`Preview image of asset pack ${assetPack.name}`}
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPack.name}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
<Trans>{assetPack.assetsCount} Assets</Trans>
|
||||
{assetPack.userFriendlyPrice
|
||||
? ' - ' + assetPack.userFriendlyPrice
|
||||
: null}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Paper>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const PrivateAssetPackTile = ({
|
||||
assetPackListingData,
|
||||
onSelect,
|
||||
style,
|
||||
owned,
|
||||
}: {|
|
||||
assetPackListingData: PrivateAssetPackListingData,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Paper elevation={2} style={styles.paper} background="light">
|
||||
<CorsAwareImage
|
||||
key={assetPackListingData.name}
|
||||
style={styles.previewImage}
|
||||
src={assetPackListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of asset pack ${assetPackListingData.name}`}
|
||||
/>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<PrivateAssetPackPriceTag
|
||||
privateAssetPackListingData={assetPackListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPackListingData.name}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
{assetPackListingData.description}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Paper>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const PromoBundleAssetPackCard = ({
|
||||
assetPackListingData,
|
||||
onSelect,
|
||||
owned,
|
||||
}: {|
|
||||
assetPackListingData: PrivateAssetPackListingData,
|
||||
onSelect: () => void,
|
||||
owned: boolean,
|
||||
|}) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Line expand>
|
||||
<div
|
||||
style={{
|
||||
...styles.promoLineContainer,
|
||||
border: `2px solid ${gdevelopTheme.palette.secondary}`,
|
||||
}}
|
||||
>
|
||||
<Line expand noMargin>
|
||||
<CorsAwareImage
|
||||
key={assetPackListingData.name}
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
...styles.promoImage,
|
||||
}}
|
||||
src={assetPackListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of bundle ${assetPackListingData.name}`}
|
||||
/>
|
||||
<Column expand alignItems="flex-start" justifyContent="center">
|
||||
<Text color="primary" size="section-title">
|
||||
{!owned ? (
|
||||
<Trans>Get {assetPackListingData.description}!</Trans>
|
||||
) : (
|
||||
<Trans>You already own this pack!</Trans>
|
||||
)}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
{!owned ? (
|
||||
<Trans>
|
||||
This pack is included in this bundle for{' '}
|
||||
{formatPrivateAssetPackPrice({
|
||||
i18n,
|
||||
privateAssetPackListingData: assetPackListingData,
|
||||
})}
|
||||
!
|
||||
</Trans>
|
||||
) : (
|
||||
<Trans>
|
||||
It is included in the bundle {assetPackListingData.name}.
|
||||
</Trans>
|
||||
)}
|
||||
</Text>
|
||||
</Column>
|
||||
<Column justifyContent="center">
|
||||
{!owned ? (
|
||||
<FlatButton
|
||||
label={<Trans>See this bundle</Trans>}
|
||||
onClick={onSelect}
|
||||
primary
|
||||
/>
|
||||
) : (
|
||||
<RaisedButton
|
||||
label={<Trans>See this bundle</Trans>}
|
||||
onClick={onSelect}
|
||||
primary
|
||||
/>
|
||||
)}
|
||||
</Column>
|
||||
</Line>
|
||||
</div>
|
||||
</Line>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
||||
export const CategoryTile = ({
|
||||
id,
|
||||
title,
|
||||
imageSource,
|
||||
imageAlt,
|
||||
onSelect,
|
||||
style,
|
||||
}: {|
|
||||
id: string,
|
||||
title: React.Node,
|
||||
imageSource: string,
|
||||
imageAlt: string,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Paper id={id} elevation={2} style={styles.paper} background="light">
|
||||
<CorsAwareImage
|
||||
style={{
|
||||
...styles.previewImage,
|
||||
background: gdevelopTheme.palette.primary,
|
||||
}}
|
||||
src={imageSource}
|
||||
alt={imageAlt}
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="center" noMargin>
|
||||
<Text style={styles.packTitle} size="sub-title">
|
||||
{title}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Paper>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
@@ -4,6 +4,7 @@ import { type Filters } from '../Utils/GDevelopServices/Filters';
|
||||
import {
|
||||
type AssetShortHeader,
|
||||
type PublicAssetPacks,
|
||||
type PublicAssetPack,
|
||||
type Author,
|
||||
type License,
|
||||
type Environment,
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
ColorAssetStoreSearchFilter,
|
||||
LicenseAssetStoreSearchFilter,
|
||||
DimensionAssetStoreSearchFilter,
|
||||
AssetPackTypeStoreSearchFilter,
|
||||
} from './AssetStoreSearchFilter';
|
||||
import {
|
||||
type NavigationState,
|
||||
@@ -57,10 +59,15 @@ export type AssetFiltersState = {|
|
||||
setLicenseFilter: LicenseAssetStoreSearchFilter => void,
|
||||
|};
|
||||
|
||||
export type AssetPackFiltersState = {|
|
||||
typeFilter: AssetPackTypeStoreSearchFilter,
|
||||
setTypeFilter: AssetPackTypeStoreSearchFilter => void,
|
||||
|};
|
||||
|
||||
type AssetStoreState = {|
|
||||
filters: ?Filters,
|
||||
publicAssetPacks: ?PublicAssetPacks,
|
||||
privateAssetPacks: ?Array<PrivateAssetPackListingData>,
|
||||
privateAssetPackListingDatas: ?Array<PrivateAssetPackListingData>,
|
||||
assetPackRandomOrdering: ?{|
|
||||
starterPacks: Array<number>,
|
||||
privateAssetPacks: Array<number>,
|
||||
@@ -69,12 +76,16 @@ type AssetStoreState = {|
|
||||
licenses: ?Array<License>,
|
||||
environment: Environment,
|
||||
setEnvironment: Environment => void,
|
||||
searchResults: ?Array<AssetShortHeader>,
|
||||
assetShortHeadersSearchResults: ?Array<AssetShortHeader>,
|
||||
publicAssetPacksSearchResults: ?Array<PublicAssetPack>,
|
||||
privateAssetPackListingDatasSearchResults: ?Array<PrivateAssetPackListingData>,
|
||||
fetchAssetsAndFilters: () => void,
|
||||
error: ?Error,
|
||||
searchText: string,
|
||||
setSearchText: string => void,
|
||||
assetFiltersState: AssetFiltersState,
|
||||
assetPackFiltersState: AssetPackFiltersState,
|
||||
clearAllFilters: () => void,
|
||||
navigationState: NavigationState,
|
||||
currentPage: AssetStorePageState,
|
||||
useSearchItem: (
|
||||
@@ -89,13 +100,15 @@ type AssetStoreState = {|
|
||||
export const AssetStoreContext = React.createContext<AssetStoreState>({
|
||||
filters: null,
|
||||
publicAssetPacks: null,
|
||||
privateAssetPacks: null,
|
||||
privateAssetPackListingDatas: null,
|
||||
assetPackRandomOrdering: null,
|
||||
authors: null,
|
||||
licenses: null,
|
||||
environment: 'live',
|
||||
setEnvironment: () => {},
|
||||
searchResults: null,
|
||||
assetShortHeadersSearchResults: null,
|
||||
publicAssetPacksSearchResults: null,
|
||||
privateAssetPackListingDatasSearchResults: null,
|
||||
fetchAssetsAndFilters: () => {},
|
||||
error: null,
|
||||
searchText: '',
|
||||
@@ -114,11 +127,17 @@ export const AssetStoreContext = React.createContext<AssetStoreState>({
|
||||
licenseFilter: new LicenseAssetStoreSearchFilter(),
|
||||
setLicenseFilter: filter => {},
|
||||
},
|
||||
assetPackFiltersState: {
|
||||
typeFilter: new AssetPackTypeStoreSearchFilter({}),
|
||||
setTypeFilter: filter => {},
|
||||
},
|
||||
clearAllFilters: () => {},
|
||||
navigationState: {
|
||||
getCurrentPage: () => assetStoreHomePageState,
|
||||
backToPreviousPage: () => assetStoreHomePageState,
|
||||
openHome: () => assetStoreHomePageState,
|
||||
clearHistory: () => {},
|
||||
clearPreviousPageFromHistory: () => {},
|
||||
openSearchResultPage: () => {},
|
||||
openTagPage: tag => {},
|
||||
openAssetCategoryPage: category => {},
|
||||
@@ -147,6 +166,13 @@ const getAssetShortHeaderSearchTerms = (assetShortHeader: AssetShortHeader) => {
|
||||
);
|
||||
};
|
||||
|
||||
const getPublicAssetPackSearchTerms = (assetPack: PublicAssetPack) =>
|
||||
assetPack.name + '\n' + assetPack.tag;
|
||||
|
||||
const getPrivateAssetPackListingDataSearchTerms = (
|
||||
privateAssetPack: PrivateAssetPackListingData
|
||||
) => privateAssetPack.name + '\n' + privateAssetPack.description;
|
||||
|
||||
const getAssetPackRandomOrdering = (length: number): Array<number> => {
|
||||
const array = new Array(length).fill(0).map((_, index) => index);
|
||||
|
||||
@@ -180,8 +206,8 @@ export const AssetStoreStateProvider = ({
|
||||
privateAssetPacks: Array<number>,
|
||||
|}>(null);
|
||||
const [
|
||||
privateAssetPacks,
|
||||
setPrivateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
setPrivateAssetPackListingDatas,
|
||||
] = React.useState<?Array<PrivateAssetPackListingData>>(null);
|
||||
const [authors, setAuthors] = React.useState<?Array<Author>>(null);
|
||||
const [licenses, setLicenses] = React.useState<?Array<License>>(null);
|
||||
@@ -235,7 +261,9 @@ export const AssetStoreStateProvider = ({
|
||||
);
|
||||
// When one of the filter change, we need to rebuild the array
|
||||
// for the search.
|
||||
const searchFilters = React.useMemo<Array<SearchFilter<AssetShortHeader>>>(
|
||||
const assetSearchFilters = React.useMemo<
|
||||
Array<SearchFilter<AssetShortHeader>>
|
||||
>(
|
||||
() => [
|
||||
animatedFilter,
|
||||
viewpointFilter,
|
||||
@@ -254,6 +282,16 @@ export const AssetStoreStateProvider = ({
|
||||
]
|
||||
);
|
||||
|
||||
const [
|
||||
assetPackTypeFilter,
|
||||
setAssetPackTypeFilter,
|
||||
] = React.useState<AssetPackTypeStoreSearchFilter>(
|
||||
new AssetPackTypeStoreSearchFilter({ receivedAssetPacks })
|
||||
);
|
||||
const assetPackSearchFilters = React.useMemo<
|
||||
Array<SearchFilter<PublicAssetPack | PrivateAssetPackListingData>>
|
||||
>(() => [assetPackTypeFilter], [assetPackTypeFilter]);
|
||||
|
||||
const fetchAssetsAndFilters = React.useCallback(
|
||||
() => {
|
||||
(async () => {
|
||||
@@ -267,9 +305,11 @@ export const AssetStoreStateProvider = ({
|
||||
} = await listAllPublicAssets({ environment });
|
||||
const authors = await listAllAuthors({ environment });
|
||||
const licenses = await listAllLicenses({ environment });
|
||||
const privateAssetPacks = await listListedPrivateAssetPacks({
|
||||
onlyAppStorePrivateAssetPacks,
|
||||
});
|
||||
const privateAssetPackListingDatas = await listListedPrivateAssetPacks(
|
||||
{
|
||||
onlyAppStorePrivateAssetPacks,
|
||||
}
|
||||
);
|
||||
|
||||
console.info(
|
||||
`Loaded ${
|
||||
@@ -281,7 +321,7 @@ export const AssetStoreStateProvider = ({
|
||||
setFilters(publicFilters);
|
||||
setAuthors(authors);
|
||||
setLicenses(licenses);
|
||||
setPrivateAssetPacks(privateAssetPacks);
|
||||
setPrivateAssetPackListingDatas(privateAssetPackListingDatas);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Unable to load the assets from the asset store:`,
|
||||
@@ -330,7 +370,7 @@ export const AssetStoreStateProvider = ({
|
||||
if (
|
||||
publicAssetPacks &&
|
||||
receivedAssetPacks &&
|
||||
privateAssetPacks &&
|
||||
privateAssetPackListingDatas &&
|
||||
initialPackUserFriendlySlug
|
||||
) {
|
||||
initialPackOpened.current = true;
|
||||
@@ -343,7 +383,10 @@ export const AssetStoreStateProvider = ({
|
||||
});
|
||||
|
||||
if (assetPack) {
|
||||
navigationState.openPackPage(assetPack);
|
||||
navigationState.openPackPage({
|
||||
assetPack,
|
||||
previousSearchText: searchText,
|
||||
});
|
||||
initialPackOpened.current = false; // Allow to open the pack again if the effect run again.
|
||||
setInitialPackUserFriendlySlug(null);
|
||||
return;
|
||||
@@ -351,12 +394,15 @@ export const AssetStoreStateProvider = ({
|
||||
|
||||
// Otherwise, try to open the information page of a pack not yet bought.
|
||||
const privateAssetPack = getPrivateAssetPackListingData({
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
userFriendlySlug: initialPackUserFriendlySlug,
|
||||
});
|
||||
|
||||
if (privateAssetPack) {
|
||||
navigationState.openPrivateAssetPackInformationPage(privateAssetPack);
|
||||
navigationState.openPrivateAssetPackInformationPage({
|
||||
assetPack: privateAssetPack,
|
||||
previousSearchText: searchText,
|
||||
});
|
||||
initialPackOpened.current = false; // Allow to open the pack again if the effect run again.
|
||||
setInitialPackUserFriendlySlug(null);
|
||||
return;
|
||||
@@ -371,10 +417,11 @@ export const AssetStoreStateProvider = ({
|
||||
[
|
||||
publicAssetPacks,
|
||||
receivedAssetPacks,
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
navigationState,
|
||||
showAlert,
|
||||
initialPackUserFriendlySlug,
|
||||
searchText,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -387,13 +434,49 @@ export const AssetStoreStateProvider = ({
|
||||
[fetchAssetsAndFilters]
|
||||
);
|
||||
|
||||
const publicAssetPacksByTag = React.useMemo(
|
||||
() => {
|
||||
if (!publicAssetPacks) {
|
||||
return null;
|
||||
}
|
||||
const publicAssetPacksByTag = {};
|
||||
publicAssetPacks.starterPacks.forEach(assetPack => {
|
||||
const tag = assetPack.tag;
|
||||
if (publicAssetPacksByTag[tag]) {
|
||||
console.warn(`Multiple public asset packs with the same tag: ${tag}`);
|
||||
}
|
||||
publicAssetPacksByTag[tag] = assetPack;
|
||||
});
|
||||
return publicAssetPacksByTag;
|
||||
},
|
||||
[publicAssetPacks]
|
||||
);
|
||||
|
||||
const privateAssetPackListingDatasById = React.useMemo(
|
||||
() => {
|
||||
if (!privateAssetPackListingDatas) {
|
||||
return null;
|
||||
}
|
||||
const privateAssetPackListingDatasById = {};
|
||||
privateAssetPackListingDatas.forEach(privateAssetPackListingData => {
|
||||
const id = privateAssetPackListingData.id;
|
||||
if (privateAssetPackListingDatasById[id]) {
|
||||
console.warn(`Multiple private asset packs with the same id: ${id}`);
|
||||
}
|
||||
privateAssetPackListingDatasById[id] = privateAssetPackListingData;
|
||||
});
|
||||
return privateAssetPackListingDatasById;
|
||||
},
|
||||
[privateAssetPackListingDatas]
|
||||
);
|
||||
|
||||
// Randomize asset packs when number of asset packs and private asset packs change
|
||||
const assetPackCount =
|
||||
publicAssetPacks && publicAssetPacks.starterPacks
|
||||
? publicAssetPacks.starterPacks.length
|
||||
: undefined;
|
||||
const privateAssetPackCount = privateAssetPacks
|
||||
? privateAssetPacks.length
|
||||
const privateAssetPackCount = privateAssetPackListingDatas
|
||||
? privateAssetPackListingDatas.length
|
||||
: undefined;
|
||||
React.useEffect(
|
||||
() => {
|
||||
@@ -410,22 +493,102 @@ export const AssetStoreStateProvider = ({
|
||||
|
||||
const currentPage = navigationState.getCurrentPage();
|
||||
const { chosenCategory, chosenFilters } = currentPage.filtersState;
|
||||
const searchResults: ?Array<AssetShortHeader> = useSearchItem(
|
||||
const assetShortHeadersSearchResults: ?Array<AssetShortHeader> = useSearchItem(
|
||||
assetShortHeadersById,
|
||||
getAssetShortHeaderSearchTerms,
|
||||
searchText,
|
||||
chosenCategory,
|
||||
chosenFilters,
|
||||
searchFilters
|
||||
assetSearchFilters
|
||||
);
|
||||
|
||||
const publicAssetPacksSearchResults: ?Array<PublicAssetPack> = useSearchItem(
|
||||
publicAssetPacksByTag,
|
||||
getPublicAssetPackSearchTerms,
|
||||
searchText,
|
||||
chosenCategory,
|
||||
null,
|
||||
// $FlowFixMe - this filter works for both public and private packs
|
||||
assetPackSearchFilters
|
||||
);
|
||||
|
||||
const privateAssetPackListingDatasSearchResults: ?Array<PrivateAssetPackListingData> = useSearchItem(
|
||||
privateAssetPackListingDatasById,
|
||||
getPrivateAssetPackListingDataSearchTerms,
|
||||
searchText,
|
||||
chosenCategory,
|
||||
null,
|
||||
// $FlowFixMe - this filter works for both public and private packs
|
||||
assetPackSearchFilters
|
||||
);
|
||||
|
||||
const assetFiltersState: AssetFiltersState = React.useMemo(
|
||||
() => ({
|
||||
animatedFilter,
|
||||
setAnimatedFilter,
|
||||
viewpointFilter,
|
||||
setViewpointFilter,
|
||||
dimensionFilter,
|
||||
setDimensionFilter,
|
||||
objectTypeFilter,
|
||||
setObjectTypeFilter,
|
||||
colorFilter,
|
||||
setColorFilter,
|
||||
licenseFilter,
|
||||
setLicenseFilter,
|
||||
}),
|
||||
[
|
||||
animatedFilter,
|
||||
setAnimatedFilter,
|
||||
viewpointFilter,
|
||||
setViewpointFilter,
|
||||
dimensionFilter,
|
||||
setDimensionFilter,
|
||||
objectTypeFilter,
|
||||
setObjectTypeFilter,
|
||||
colorFilter,
|
||||
setColorFilter,
|
||||
licenseFilter,
|
||||
setLicenseFilter,
|
||||
]
|
||||
);
|
||||
|
||||
const assetPackFiltersState: AssetPackFiltersState = React.useMemo(
|
||||
() => ({
|
||||
typeFilter: assetPackTypeFilter,
|
||||
setTypeFilter: setAssetPackTypeFilter,
|
||||
}),
|
||||
[assetPackTypeFilter, setAssetPackTypeFilter]
|
||||
);
|
||||
|
||||
const clearAllFilters = React.useCallback(
|
||||
() => {
|
||||
assetFiltersState.setAnimatedFilter(new AnimatedAssetStoreSearchFilter());
|
||||
assetFiltersState.setViewpointFilter(new TagAssetStoreSearchFilter());
|
||||
assetFiltersState.setDimensionFilter(
|
||||
new DimensionAssetStoreSearchFilter()
|
||||
);
|
||||
assetFiltersState.setObjectTypeFilter(
|
||||
new ObjectTypeAssetStoreSearchFilter()
|
||||
);
|
||||
assetFiltersState.setColorFilter(new ColorAssetStoreSearchFilter());
|
||||
assetFiltersState.setLicenseFilter(new LicenseAssetStoreSearchFilter());
|
||||
assetPackFiltersState.setTypeFilter(
|
||||
new AssetPackTypeStoreSearchFilter({})
|
||||
);
|
||||
},
|
||||
[assetFiltersState, assetPackFiltersState]
|
||||
);
|
||||
|
||||
const assetStoreState = React.useMemo(
|
||||
() => ({
|
||||
searchResults,
|
||||
assetShortHeadersSearchResults,
|
||||
publicAssetPacksSearchResults,
|
||||
privateAssetPackListingDatasSearchResults,
|
||||
fetchAssetsAndFilters,
|
||||
filters,
|
||||
publicAssetPacks,
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
assetPackRandomOrdering,
|
||||
authors,
|
||||
licenses,
|
||||
@@ -436,20 +599,9 @@ export const AssetStoreStateProvider = ({
|
||||
currentPage,
|
||||
searchText,
|
||||
setSearchText,
|
||||
assetFiltersState: {
|
||||
animatedFilter,
|
||||
setAnimatedFilter,
|
||||
viewpointFilter,
|
||||
setViewpointFilter,
|
||||
dimensionFilter,
|
||||
setDimensionFilter,
|
||||
objectTypeFilter,
|
||||
setObjectTypeFilter,
|
||||
colorFilter,
|
||||
setColorFilter,
|
||||
licenseFilter,
|
||||
setLicenseFilter,
|
||||
},
|
||||
assetFiltersState,
|
||||
assetPackFiltersState,
|
||||
clearAllFilters,
|
||||
useSearchItem: (
|
||||
searchText,
|
||||
chosenCategory,
|
||||
@@ -467,11 +619,13 @@ export const AssetStoreStateProvider = ({
|
||||
setInitialPackUserFriendlySlug,
|
||||
}),
|
||||
[
|
||||
searchResults,
|
||||
assetShortHeadersSearchResults,
|
||||
publicAssetPacksSearchResults,
|
||||
privateAssetPackListingDatasSearchResults,
|
||||
fetchAssetsAndFilters,
|
||||
filters,
|
||||
publicAssetPacks,
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
assetPackRandomOrdering,
|
||||
authors,
|
||||
licenses,
|
||||
@@ -481,13 +635,9 @@ export const AssetStoreStateProvider = ({
|
||||
navigationState,
|
||||
currentPage,
|
||||
searchText,
|
||||
animatedFilter,
|
||||
viewpointFilter,
|
||||
dimensionFilter,
|
||||
objectTypeFilter,
|
||||
colorFilter,
|
||||
licenseFilter,
|
||||
setLicenseFilter,
|
||||
assetFiltersState,
|
||||
assetPackFiltersState,
|
||||
clearAllFilters,
|
||||
assetShortHeadersById,
|
||||
setInitialPackUserFriendlySlug,
|
||||
]
|
||||
|
@@ -13,13 +13,15 @@ import {
|
||||
LicenseAssetStoreSearchFilter,
|
||||
DimensionAssetStoreSearchFilter,
|
||||
ColorAssetStoreSearchFilter,
|
||||
AssetPackTypeStoreSearchFilter,
|
||||
} from './AssetStoreSearchFilter';
|
||||
import { type AssetFiltersState } from './AssetStoreContext';
|
||||
import { AssetStoreContext } from './AssetStoreContext';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import { type RGBColor } from '../Utils/ColorTransformer';
|
||||
import { HexColorField } from './HexColorField';
|
||||
import Slider from '../UI/Slider';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
|
||||
type Choice = {|
|
||||
label: React.Node,
|
||||
@@ -218,26 +220,53 @@ const ColorFilter = ({
|
||||
);
|
||||
};
|
||||
|
||||
export const clearAllFilters = (assetFiltersState: AssetFiltersState): void => {
|
||||
assetFiltersState.setAnimatedFilter(new AnimatedAssetStoreSearchFilter());
|
||||
assetFiltersState.setViewpointFilter(new TagAssetStoreSearchFilter());
|
||||
assetFiltersState.setDimensionFilter(new DimensionAssetStoreSearchFilter());
|
||||
assetFiltersState.setObjectTypeFilter(new ObjectTypeAssetStoreSearchFilter());
|
||||
assetFiltersState.setColorFilter(new ColorAssetStoreSearchFilter());
|
||||
assetFiltersState.setLicenseFilter(new LicenseAssetStoreSearchFilter());
|
||||
};
|
||||
|
||||
type AssetStoreFilterPanelProps = {|
|
||||
assetFiltersState: AssetFiltersState,
|
||||
onChoiceChange: () => void,
|
||||
|};
|
||||
|
||||
export const AssetStoreFilterPanel = ({
|
||||
assetFiltersState,
|
||||
onChoiceChange,
|
||||
}: AssetStoreFilterPanelProps) => {
|
||||
export const AssetStoreFilterPanel = () => {
|
||||
const {
|
||||
assetFiltersState,
|
||||
assetPackFiltersState,
|
||||
clearAllFilters,
|
||||
navigationState,
|
||||
} = React.useContext(AssetStoreContext);
|
||||
const { receivedAssetPacks } = React.useContext(AuthenticatedUserContext);
|
||||
const onChoiceChange = React.useCallback(
|
||||
() => {
|
||||
navigationState.openSearchResultPage();
|
||||
},
|
||||
[navigationState]
|
||||
);
|
||||
return (
|
||||
<Column noMargin>
|
||||
<MultipleChoiceFilter
|
||||
filterKey="PackType"
|
||||
title={<Trans>Pack type</Trans>}
|
||||
choices={[
|
||||
{ label: t`Free`, value: 'free' },
|
||||
{ label: t`Premium`, value: 'premium' },
|
||||
{ label: t`Owned`, value: 'owned' },
|
||||
]}
|
||||
isChoiceChecked={choice =>
|
||||
(choice === 'free' && assetPackFiltersState.typeFilter.isFree) ||
|
||||
(choice === 'premium' &&
|
||||
assetPackFiltersState.typeFilter.isPremium) ||
|
||||
(choice === 'owned' && assetPackFiltersState.typeFilter.isOwned)
|
||||
}
|
||||
setChoiceChecked={(choice, checked) => {
|
||||
const typeFilter = assetPackFiltersState.typeFilter;
|
||||
const isFree = choice === 'free' ? checked : typeFilter.isFree;
|
||||
const isPremium =
|
||||
choice === 'premium' ? checked : typeFilter.isPremium;
|
||||
const isOwned = choice === 'owned' ? checked : typeFilter.isOwned;
|
||||
assetPackFiltersState.setTypeFilter(
|
||||
new AssetPackTypeStoreSearchFilter({
|
||||
isFree,
|
||||
isPremium,
|
||||
isOwned,
|
||||
receivedAssetPacks,
|
||||
})
|
||||
);
|
||||
onChoiceChange();
|
||||
}}
|
||||
/>
|
||||
<MultipleChoiceFilter
|
||||
filterKey="Animation"
|
||||
title={<Trans>Animation</Trans>}
|
||||
@@ -352,7 +381,7 @@ export const AssetStoreFilterPanel = ({
|
||||
label={<Trans>Clear all filters</Trans>}
|
||||
primary={false}
|
||||
onClick={() => {
|
||||
clearAllFilters(assetFiltersState);
|
||||
clearAllFilters();
|
||||
onChoiceChange();
|
||||
}}
|
||||
/>
|
||||
|
@@ -16,6 +16,7 @@ export type AssetStorePageState = {|
|
||||
filtersState: FiltersState,
|
||||
scrollPosition?: ?number,
|
||||
displayAssets: boolean,
|
||||
searchText?: string,
|
||||
|};
|
||||
|
||||
export type NavigationState = {|
|
||||
@@ -23,12 +24,22 @@ export type NavigationState = {|
|
||||
backToPreviousPage: () => AssetStorePageState,
|
||||
openHome: () => AssetStorePageState,
|
||||
clearHistory: () => void,
|
||||
clearPreviousPageFromHistory: () => void,
|
||||
openSearchResultPage: () => void,
|
||||
openTagPage: string => void,
|
||||
openAssetCategoryPage: string => void,
|
||||
openPackPage: (PublicAssetPack | PrivateAssetPack) => void,
|
||||
openPrivateAssetPackInformationPage: PrivateAssetPackListingData => void,
|
||||
openDetailPage: AssetShortHeader => void,
|
||||
openPackPage: ({|
|
||||
assetPack: PublicAssetPack | PrivateAssetPack,
|
||||
previousSearchText: string,
|
||||
|}) => void,
|
||||
openPrivateAssetPackInformationPage: ({|
|
||||
assetPack: PrivateAssetPackListingData,
|
||||
previousSearchText: string,
|
||||
|}) => void,
|
||||
openDetailPage: ({|
|
||||
assetShortHeader: AssetShortHeader,
|
||||
previousSearchText: string,
|
||||
|}) => void,
|
||||
|};
|
||||
|
||||
const noFilter: FiltersState = {
|
||||
@@ -118,6 +129,17 @@ export const useNavigation = (): NavigationState => {
|
||||
return { previousPages: [assetStoreHomePageState, currentPage] };
|
||||
});
|
||||
},
|
||||
clearPreviousPageFromHistory: () => {
|
||||
setHistory(previousHistory => {
|
||||
if (previousHistory.previousPages.length <= 1) return previousHistory;
|
||||
|
||||
const newPreviousPages = previousHistory.previousPages.slice(
|
||||
0,
|
||||
previousHistory.previousPages.length - 1
|
||||
);
|
||||
return { previousPages: newPreviousPages };
|
||||
});
|
||||
},
|
||||
openSearchResultPage: () => {
|
||||
setHistory(previousHistory => {
|
||||
const currentPage =
|
||||
@@ -178,16 +200,34 @@ export const useNavigation = (): NavigationState => {
|
||||
],
|
||||
}));
|
||||
},
|
||||
openPackPage: (assetPack: PublicAssetPack | PrivateAssetPack) => {
|
||||
openPackPage: ({
|
||||
assetPack,
|
||||
previousSearchText,
|
||||
}: {|
|
||||
assetPack: PublicAssetPack | PrivateAssetPack,
|
||||
previousSearchText: string,
|
||||
|}) => {
|
||||
setHistory(previousHistory => {
|
||||
const currentPage =
|
||||
previousHistory.previousPages[
|
||||
previousHistory.previousPages.length - 1
|
||||
];
|
||||
const currentPageWithSearchText = {
|
||||
...currentPage,
|
||||
searchText: previousSearchText,
|
||||
};
|
||||
const previousPagesWithoutCurrentPage = previousHistory.previousPages.slice(
|
||||
0,
|
||||
previousHistory.previousPages.length - 1
|
||||
);
|
||||
const previousPages = [
|
||||
...previousPagesWithoutCurrentPage,
|
||||
currentPageWithSearchText,
|
||||
];
|
||||
return {
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
...previousHistory.previousPages,
|
||||
...previousPages,
|
||||
{
|
||||
openedAssetShortHeader: null,
|
||||
openedAssetCategory:
|
||||
@@ -214,39 +254,85 @@ export const useNavigation = (): NavigationState => {
|
||||
};
|
||||
});
|
||||
},
|
||||
openPrivateAssetPackInformationPage: (
|
||||
assetPack: PrivateAssetPackListingData
|
||||
) => {
|
||||
setHistory(previousHistory => ({
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
...previousHistory.previousPages,
|
||||
{
|
||||
openedAssetShortHeader: null,
|
||||
openedAssetCategory: null,
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: assetPack,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
openPrivateAssetPackInformationPage: ({
|
||||
assetPack,
|
||||
previousSearchText,
|
||||
}: {|
|
||||
assetPack: PrivateAssetPackListingData,
|
||||
previousSearchText: string,
|
||||
|}) => {
|
||||
setHistory(previousHistory => {
|
||||
const currentPage =
|
||||
previousHistory.previousPages[
|
||||
previousHistory.previousPages.length - 1
|
||||
];
|
||||
const currentPageWithSearchText = {
|
||||
...currentPage,
|
||||
searchText: previousSearchText,
|
||||
};
|
||||
const previousPagesWithoutCurrentPage = previousHistory.previousPages.slice(
|
||||
0,
|
||||
previousHistory.previousPages.length - 1
|
||||
);
|
||||
const previousPages = [
|
||||
...previousPagesWithoutCurrentPage,
|
||||
currentPageWithSearchText,
|
||||
];
|
||||
return {
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
...previousPages,
|
||||
{
|
||||
openedAssetShortHeader: null,
|
||||
openedAssetCategory: null,
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: assetPack,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
openDetailPage: (assetShortHeader: AssetShortHeader) => {
|
||||
setHistory(previousHistory => ({
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
...previousHistory.previousPages,
|
||||
{
|
||||
openedAssetShortHeader: assetShortHeader,
|
||||
openedAssetCategory: null,
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
openDetailPage: ({
|
||||
assetShortHeader,
|
||||
previousSearchText,
|
||||
}: {|
|
||||
assetShortHeader: AssetShortHeader,
|
||||
previousSearchText: string,
|
||||
|}) => {
|
||||
setHistory(previousHistory => {
|
||||
const currentPage =
|
||||
previousHistory.previousPages[
|
||||
previousHistory.previousPages.length - 1
|
||||
];
|
||||
const currentPageWithSearchText = {
|
||||
...currentPage,
|
||||
searchText: previousSearchText,
|
||||
};
|
||||
const previousPagesWithoutCurrentPage = previousHistory.previousPages.slice(
|
||||
0,
|
||||
previousHistory.previousPages.length - 1
|
||||
);
|
||||
const previousPages = [
|
||||
...previousPagesWithoutCurrentPage,
|
||||
currentPageWithSearchText,
|
||||
];
|
||||
return {
|
||||
...previousHistory,
|
||||
previousPages: [
|
||||
...previousPages,
|
||||
{
|
||||
openedAssetShortHeader: assetShortHeader,
|
||||
openedAssetCategory: null,
|
||||
openedAssetPack: null,
|
||||
openedPrivateAssetPackListingData: null,
|
||||
filtersState: noFilter,
|
||||
displayAssets: false,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
},
|
||||
}),
|
||||
[previousPages]
|
||||
|
@@ -1,6 +1,11 @@
|
||||
// @flow
|
||||
import { TagSearchFilter, SearchFilter } from '../UI/Search/UseSearchItem';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import {
|
||||
type AssetShortHeader,
|
||||
type PrivateAssetPack,
|
||||
type PublicAssetPack,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
type RGBColor,
|
||||
rgbToHsl,
|
||||
@@ -27,6 +32,10 @@ export class ObjectTypeAssetStoreSearchFilter
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
hasFilters(): boolean {
|
||||
return this.objectTypes.size > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export class LicenseAssetStoreSearchFilter
|
||||
@@ -52,6 +61,57 @@ export class LicenseAssetStoreSearchFilter
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
hasFilters(): boolean {
|
||||
return this.attributionFreeOnly;
|
||||
}
|
||||
}
|
||||
|
||||
export class AssetPackTypeStoreSearchFilter
|
||||
implements SearchFilter<PublicAssetPack | PrivateAssetPackListingData> {
|
||||
isFree: boolean;
|
||||
isPremium: boolean;
|
||||
isOwned: boolean;
|
||||
receivedAssetPacks: Array<PrivateAssetPack>;
|
||||
|
||||
constructor({
|
||||
isFree = false,
|
||||
isPremium = false,
|
||||
isOwned = false,
|
||||
receivedAssetPacks = [],
|
||||
}: {|
|
||||
isFree?: boolean,
|
||||
isPremium?: boolean,
|
||||
isOwned?: boolean,
|
||||
receivedAssetPacks?: ?Array<PrivateAssetPack>,
|
||||
|}) {
|
||||
this.isFree = isFree;
|
||||
this.isPremium = isPremium;
|
||||
this.isOwned = isOwned;
|
||||
this.receivedAssetPacks = receivedAssetPacks || [];
|
||||
}
|
||||
|
||||
getPertinence(
|
||||
searchItem: PublicAssetPack | PrivateAssetPackListingData
|
||||
): number {
|
||||
// Return all packs when no filter is selected.
|
||||
if (!this.isFree && !this.isPremium && !this.isOwned) return 1;
|
||||
if (
|
||||
this.isOwned &&
|
||||
searchItem.prices &&
|
||||
this.receivedAssetPacks
|
||||
.map(assetPack => assetPack.id)
|
||||
.includes(searchItem.id)
|
||||
)
|
||||
return 1;
|
||||
if (this.isPremium && searchItem.prices) return 1;
|
||||
if (this.isFree && !searchItem.prices) return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
hasFilters(): boolean {
|
||||
return this.isFree || this.isPremium || this.isOwned;
|
||||
}
|
||||
}
|
||||
|
||||
export class AnimatedAssetStoreSearchFilter
|
||||
@@ -75,6 +135,10 @@ export class AnimatedAssetStoreSearchFilter
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
hasFilters(): boolean {
|
||||
return this.mustBeAnimated || this.mustHaveSeveralState;
|
||||
}
|
||||
}
|
||||
|
||||
export class DimensionAssetStoreSearchFilter
|
||||
@@ -109,6 +173,13 @@ export class DimensionAssetStoreSearchFilter
|
||||
? 1
|
||||
: 0;
|
||||
}
|
||||
|
||||
hasFilters(): boolean {
|
||||
return (
|
||||
this.dimensionMin !== DimensionAssetStoreSearchFilter.boundMin ||
|
||||
this.dimensionMax !== DimensionAssetStoreSearchFilter.boundMax
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -184,6 +255,10 @@ export class ColorAssetStoreSearchFilter
|
||||
}
|
||||
return scoreMax;
|
||||
}
|
||||
|
||||
hasFilters(): boolean {
|
||||
return !!this.color;
|
||||
}
|
||||
}
|
||||
|
||||
export class SimilarAssetStoreSearchFilter
|
||||
@@ -276,4 +351,8 @@ export class SimilarAssetStoreSearchFilter
|
||||
|
||||
return colorSimilitude;
|
||||
}
|
||||
|
||||
hasFilters(): boolean {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
@@ -82,19 +82,19 @@ export const getAssetPackFromUserFriendlySlug = ({
|
||||
};
|
||||
|
||||
export const getPrivateAssetPackListingData = ({
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
userFriendlySlug,
|
||||
}: {|
|
||||
privateAssetPacks: Array<PrivateAssetPackListingData>,
|
||||
privateAssetPackListingDatas: Array<PrivateAssetPackListingData>,
|
||||
userFriendlySlug: string,
|
||||
|}): ?PrivateAssetPackListingData => {
|
||||
const privateAssetPackId = getIdFromPrivateAssetPackUserFriendlySlug(
|
||||
userFriendlySlug
|
||||
);
|
||||
const privateAssetPack = privateAssetPacks.find(
|
||||
const privateAssetPackListingData = privateAssetPackListingDatas.find(
|
||||
privateAssetPack => privateAssetPackId === privateAssetPack.id
|
||||
);
|
||||
if (privateAssetPack) return privateAssetPack;
|
||||
if (privateAssetPackListingData) return privateAssetPackListingData;
|
||||
|
||||
return null;
|
||||
};
|
||||
|
@@ -1,18 +1,13 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import { makeStyles, createStyles } from '@material-ui/core/styles';
|
||||
import GridListTile from '@material-ui/core/GridListTile';
|
||||
import GridList from '@material-ui/core/GridList';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import Text from '../UI/Text';
|
||||
import { PrivateAssetPackPriceTag } from './PrivateAssets/PrivateAssetPackPriceTag';
|
||||
import type {
|
||||
PublicAssetPacks,
|
||||
PublicAssetPack,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
|
||||
import { shouldValidate } from '../UI/KeyboardShortcuts/InteractionKeys';
|
||||
import { Line, Column } from '../UI/Grid';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
import {
|
||||
@@ -20,9 +15,12 @@ import {
|
||||
type WidthType,
|
||||
} from '../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import Paper from '../UI/Paper';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
import { textEllipsisStyle } from '../UI/TextEllipsis';
|
||||
import {
|
||||
CategoryTile,
|
||||
PrivateAssetPackTile,
|
||||
PublicAssetPackTile,
|
||||
} from './AssetPackTiles';
|
||||
|
||||
const cellSpacing = 2;
|
||||
|
||||
@@ -105,198 +103,6 @@ const styles = {
|
||||
// Remove the scroll capability of the grid, the scroll view handles it.
|
||||
overflow: 'unset',
|
||||
},
|
||||
priceTagContainer: {
|
||||
position: 'absolute',
|
||||
top: 10,
|
||||
left: 10,
|
||||
cursor: 'default',
|
||||
},
|
||||
previewImage: {
|
||||
width: '100%',
|
||||
// Prevent cumulative layout shift by enforcing
|
||||
// the 16:9 ratio.
|
||||
aspectRatio: '16 / 9',
|
||||
objectFit: 'cover',
|
||||
position: 'relative',
|
||||
background: '#7147ed',
|
||||
},
|
||||
cardContainer: {
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
},
|
||||
paper: {
|
||||
margin: 4,
|
||||
},
|
||||
packTitle: {
|
||||
...textEllipsisStyle,
|
||||
overflowWrap: 'break-word',
|
||||
},
|
||||
};
|
||||
|
||||
const useStylesForGridListItem = makeStyles(theme =>
|
||||
createStyles({
|
||||
root: {
|
||||
'&:focus': {
|
||||
border: `2px solid ${theme.palette.primary.main}`,
|
||||
outline: 'none',
|
||||
},
|
||||
'&:focus-visible': { outline: 'unset' },
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
const PublicAssetPackTile = ({
|
||||
assetPack,
|
||||
onSelect,
|
||||
style,
|
||||
}: {|
|
||||
assetPack: PublicAssetPack,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Paper
|
||||
id={`asset-pack-${assetPack.tag.replace(/\s/g, '-')}`}
|
||||
elevation={2}
|
||||
style={styles.paper}
|
||||
background="light"
|
||||
>
|
||||
<CorsAwareImage
|
||||
key={assetPack.name}
|
||||
style={styles.previewImage}
|
||||
src={assetPack.thumbnailUrl}
|
||||
alt={`Preview image of asset pack ${assetPack.name}`}
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPack.name}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
<Trans>{assetPack.assetsCount} Assets</Trans>
|
||||
{assetPack.userFriendlyPrice
|
||||
? ' - ' + assetPack.userFriendlyPrice
|
||||
: null}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Paper>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const PrivateAssetPackTile = ({
|
||||
assetPackListingData,
|
||||
onSelect,
|
||||
style,
|
||||
owned,
|
||||
}: {|
|
||||
assetPackListingData: PrivateAssetPackListingData,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GidList component can adjust tile size */
|
||||
style?: any,
|
||||
owned: boolean,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Paper elevation={2} style={styles.paper} background="light">
|
||||
<CorsAwareImage
|
||||
key={assetPackListingData.name}
|
||||
style={styles.previewImage}
|
||||
src={assetPackListingData.thumbnailUrls[0]}
|
||||
alt={`Preview image of asset pack ${assetPackListingData.name}`}
|
||||
/>
|
||||
<div style={styles.priceTagContainer}>
|
||||
<PrivateAssetPackPriceTag
|
||||
privateAssetPackListingData={assetPackListingData}
|
||||
withOverlay
|
||||
owned={owned}
|
||||
/>
|
||||
</div>
|
||||
<Column>
|
||||
<Line justifyContent="space-between" noMargin>
|
||||
<Text style={styles.packTitle} size="body2">
|
||||
{assetPackListingData.name}
|
||||
</Text>
|
||||
<Text style={styles.packTitle} color="primary" size="body2">
|
||||
<Trans>{assetPackListingData.description}</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Paper>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export const CategoryTile = ({
|
||||
id,
|
||||
title,
|
||||
imageSource,
|
||||
imageAlt,
|
||||
onSelect,
|
||||
style,
|
||||
}: {|
|
||||
id: string,
|
||||
title: React.Node,
|
||||
imageSource: string,
|
||||
imageAlt: string,
|
||||
onSelect: () => void,
|
||||
/** Props needed so that GridList component can adjust tile size */
|
||||
style?: any,
|
||||
|}) => {
|
||||
const classesForGridListItem = useStylesForGridListItem();
|
||||
return (
|
||||
<GridListTile
|
||||
classes={classesForGridListItem}
|
||||
tabIndex={0}
|
||||
onKeyPress={(event: SyntheticKeyboardEvent<HTMLLIElement>): void => {
|
||||
if (shouldValidate(event)) {
|
||||
onSelect();
|
||||
}
|
||||
}}
|
||||
style={style}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<Paper id={id} elevation={2} style={styles.paper} background="light">
|
||||
<CorsAwareImage
|
||||
style={styles.previewImage}
|
||||
src={imageSource}
|
||||
alt={imageAlt}
|
||||
/>
|
||||
<Column>
|
||||
<Line justifyContent="center" noMargin>
|
||||
<Text style={styles.packTitle} size="sub-title">
|
||||
{title}
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
</Paper>
|
||||
</GridListTile>
|
||||
);
|
||||
};
|
||||
|
||||
export type AssetsHomeInterface = {|
|
||||
@@ -306,7 +112,7 @@ export type AssetsHomeInterface = {|
|
||||
|
||||
type Props = {|
|
||||
publicAssetPacks: PublicAssetPacks,
|
||||
privateAssetPacksListingData: Array<PrivateAssetPackListingData>,
|
||||
privateAssetPackListingDatas: Array<PrivateAssetPackListingData>,
|
||||
assetPackRandomOrdering: {|
|
||||
starterPacks: Array<number>,
|
||||
privateAssetPacks: Array<number>,
|
||||
@@ -321,7 +127,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
(
|
||||
{
|
||||
publicAssetPacks: { starterPacks },
|
||||
privateAssetPacksListingData,
|
||||
privateAssetPackListingDatas,
|
||||
assetPackRandomOrdering,
|
||||
onPublicAssetPackSelection,
|
||||
onPrivateAssetPackSelection,
|
||||
@@ -379,7 +185,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
privateAssetPacksListingData
|
||||
privateAssetPackListingDatas
|
||||
.filter(
|
||||
assetPackListingData =>
|
||||
!openedAssetCategory ||
|
||||
@@ -444,7 +250,7 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
return { allStandAloneTiles, allBundleTiles };
|
||||
},
|
||||
[
|
||||
privateAssetPacksListingData,
|
||||
privateAssetPackListingDatas,
|
||||
openedAssetCategory,
|
||||
assetPackRandomOrdering,
|
||||
onPrivateAssetPackSelection,
|
||||
@@ -453,20 +259,24 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
]
|
||||
);
|
||||
|
||||
const categoryTiles = Object.entries(assetCategories).map(
|
||||
// $FlowExpectedError - Object.entries does not infer well the type of the value.
|
||||
([id, { title, imageSource, imageAlt }]) => (
|
||||
<CategoryTile
|
||||
id={`asset-pack-category-${id.replace(/\s/g, '-')}`}
|
||||
key={id}
|
||||
imageSource={imageSource}
|
||||
imageAlt={imageAlt}
|
||||
title={title}
|
||||
onSelect={() => {
|
||||
onCategorySelection(id);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
const categoryTiles = React.useMemo(
|
||||
() =>
|
||||
Object.entries(assetCategories).map(
|
||||
// $FlowExpectedError - Object.entries does not infer well the type of the value.
|
||||
([id, { title, imageSource, imageAlt }]) => (
|
||||
<CategoryTile
|
||||
id={`asset-pack-category-${id.replace(/\s/g, '-')}`}
|
||||
key={id}
|
||||
imageSource={imageSource}
|
||||
imageAlt={imageAlt}
|
||||
title={title}
|
||||
onSelect={() => {
|
||||
onCategorySelection(id);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
),
|
||||
[onCategorySelection]
|
||||
);
|
||||
|
||||
const openedAssetCategoryTitle = openedAssetCategory
|
||||
@@ -479,25 +289,6 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
id="asset-store-home"
|
||||
data={{ isFiltered: !!openedAssetCategory ? 'true' : 'false' }}
|
||||
>
|
||||
{allBundleTiles.length ? (
|
||||
<>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Bundles</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
<GridList
|
||||
cols={getAssetPacksColumns(windowWidth)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{allBundleTiles}
|
||||
</GridList>
|
||||
</>
|
||||
) : null}
|
||||
{openedAssetCategory ? null : (
|
||||
<>
|
||||
<Column>
|
||||
@@ -517,6 +308,25 @@ export const AssetsHome = React.forwardRef<Props, AssetsHomeInterface>(
|
||||
</GridList>
|
||||
</>
|
||||
)}
|
||||
{allBundleTiles.length ? (
|
||||
<>
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>Bundles</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
</Column>
|
||||
<GridList
|
||||
cols={getAssetPacksColumns(windowWidth)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
>
|
||||
{allBundleTiles}
|
||||
</GridList>
|
||||
</>
|
||||
) : null}
|
||||
<Column>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
|
718
newIDE/app/src/AssetStore/AssetsList.js
Normal file
718
newIDE/app/src/AssetStore/AssetsList.js
Normal file
@@ -0,0 +1,718 @@
|
||||
// @flow
|
||||
import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { AssetStoreContext } from './AssetStoreContext';
|
||||
import {
|
||||
type AssetShortHeader,
|
||||
type PublicAssetPack,
|
||||
type Author,
|
||||
type License,
|
||||
isAssetPackAudioOnly,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
|
||||
import { NoResultPlaceholder } from './NoResultPlaceholder';
|
||||
import GridList from '@material-ui/core/GridList';
|
||||
import {
|
||||
useResponsiveWindowWidth,
|
||||
type WidthType,
|
||||
} from '../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import ScrollView, { type ScrollViewInterface } from '../UI/ScrollView';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import { mergeArraysPerGroup } from '../Utils/Array';
|
||||
import {
|
||||
AssetCardTile,
|
||||
AssetFolderTile,
|
||||
PrivateAssetPackTile,
|
||||
PublicAssetPackTile,
|
||||
} from './AssetPackTiles';
|
||||
import PrivateAssetPackAudioFilesDownloadButton from './PrivateAssets/PrivateAssetPackAudioFilesDownloadButton';
|
||||
import { CorsAwareImage } from '../UI/CorsAwareImage';
|
||||
import { Column, LargeSpacer, Line } from '../UI/Grid';
|
||||
import Text from '../UI/Text';
|
||||
import { LineStackLayout } from '../UI/Layout';
|
||||
import {
|
||||
getUserPublicProfile,
|
||||
type UserPublicProfile,
|
||||
} from '../Utils/GDevelopServices/User';
|
||||
import Link from '../UI/Link';
|
||||
import PublicProfileDialog from '../Profile/PublicProfileDialog';
|
||||
import Window from '../Utils/Window';
|
||||
import Breadcrumbs from '../UI/Breadcrumbs';
|
||||
import { getFolderTagsFromAssetShortHeaders } from './TagsHelper';
|
||||
|
||||
const ASSETS_DISPLAY_LIMIT = 100;
|
||||
|
||||
const getAssetSize = (windowWidth: WidthType) => {
|
||||
switch (windowWidth) {
|
||||
case 'small':
|
||||
return 80;
|
||||
case 'medium':
|
||||
return 120;
|
||||
case 'large':
|
||||
case 'xlarge':
|
||||
return 130;
|
||||
default:
|
||||
return 120;
|
||||
}
|
||||
};
|
||||
|
||||
const getAssetPacksColumns = (windowWidth: WidthType) => {
|
||||
switch (windowWidth) {
|
||||
case 'small':
|
||||
return 1;
|
||||
case 'medium':
|
||||
return 2;
|
||||
case 'large':
|
||||
return 3;
|
||||
case 'xlarge':
|
||||
return 5;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
|
||||
const getAssetFoldersColumns = (windowWidth: WidthType) => {
|
||||
switch (windowWidth) {
|
||||
case 'small':
|
||||
case 'medium':
|
||||
return 1;
|
||||
case 'large':
|
||||
case 'xlarge':
|
||||
return 2;
|
||||
default:
|
||||
return 2;
|
||||
}
|
||||
};
|
||||
|
||||
const cellSpacing = 8;
|
||||
const styles = {
|
||||
grid: {
|
||||
margin: '0 2px', // Remove the default margin of the grid but keep the horizontal padding for focus outline.
|
||||
// Remove the scroll capability of the grid, the scroll view handles it.
|
||||
overflow: 'unset',
|
||||
},
|
||||
scrollView: {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
},
|
||||
previewImage: {
|
||||
width: 200,
|
||||
// Prevent cumulative layout shift by enforcing
|
||||
// the 16:9 ratio.
|
||||
aspectRatio: '16 / 9',
|
||||
objectFit: 'cover',
|
||||
position: 'relative',
|
||||
},
|
||||
};
|
||||
|
||||
export type AssetsListInterface = {|
|
||||
getScrollPosition: () => number,
|
||||
scrollToPosition: (y: number) => void,
|
||||
|};
|
||||
|
||||
type Props = {|
|
||||
assetShortHeaders: ?Array<AssetShortHeader>,
|
||||
privateAssetPackListingDatas?: ?Array<PrivateAssetPackListingData>,
|
||||
publicAssetPacks?: ?Array<PublicAssetPack>,
|
||||
onOpenDetails: (assetShortHeader: AssetShortHeader) => void,
|
||||
noResultsPlaceHolder?: React.Node,
|
||||
error?: ?Error,
|
||||
onPrivateAssetPackSelection?: (
|
||||
assetPack: PrivateAssetPackListingData
|
||||
) => void,
|
||||
onPublicAssetPackSelection?: (assetPack: PublicAssetPack) => void,
|
||||
noScroll?: boolean,
|
||||
|};
|
||||
|
||||
const AssetsList = React.forwardRef<Props, AssetsListInterface>(
|
||||
(
|
||||
{
|
||||
assetShortHeaders,
|
||||
onOpenDetails,
|
||||
noResultsPlaceHolder,
|
||||
privateAssetPackListingDatas,
|
||||
publicAssetPacks,
|
||||
onPrivateAssetPackSelection,
|
||||
onPublicAssetPackSelection,
|
||||
noScroll,
|
||||
}: Props,
|
||||
ref
|
||||
) => {
|
||||
const {
|
||||
error,
|
||||
fetchAssetsAndFilters,
|
||||
clearAllFilters,
|
||||
navigationState,
|
||||
licenses,
|
||||
authors,
|
||||
assetFiltersState,
|
||||
assetPackFiltersState,
|
||||
privateAssetPackListingDatas: allPrivateAssetPackListingDatas,
|
||||
} = React.useContext(AssetStoreContext);
|
||||
const { receivedAssetPacks } = React.useContext(AuthenticatedUserContext);
|
||||
const [
|
||||
authorPublicProfile,
|
||||
setAuthorPublicProfile,
|
||||
] = React.useState<?UserPublicProfile>(null);
|
||||
const [
|
||||
openAuthorPublicProfileDialog,
|
||||
setOpenAuthorPublicProfileDialog,
|
||||
] = React.useState<boolean>(false);
|
||||
const [
|
||||
isNavigatingInsideFolder,
|
||||
setIsNavigatingInsideFolder,
|
||||
] = React.useState<boolean>(false);
|
||||
const currentPage = navigationState.getCurrentPage();
|
||||
const { openedAssetPack, filtersState } = currentPage;
|
||||
const chosenCategory = filtersState.chosenCategory;
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
const scrollView = React.useRef<?ScrollViewInterface>(null);
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
getScrollPosition: () => {
|
||||
const scrollViewElement = scrollView.current;
|
||||
if (!scrollViewElement) return 0;
|
||||
|
||||
return scrollViewElement.getScrollPosition();
|
||||
},
|
||||
scrollToPosition: (y: number) => {
|
||||
const scrollViewElement = scrollView.current;
|
||||
if (!scrollViewElement) return;
|
||||
|
||||
scrollViewElement.scrollToPosition(y);
|
||||
},
|
||||
}));
|
||||
|
||||
const hasAssetPackFiltersApplied = React.useMemo(
|
||||
// When a pack is opened, the asset pack filters are not hidden, but not relevant either.
|
||||
() => !openedAssetPack && assetPackFiltersState.typeFilter.hasFilters(),
|
||||
[assetPackFiltersState, openedAssetPack]
|
||||
);
|
||||
|
||||
const hasAssetFiltersApplied = React.useMemo(
|
||||
() =>
|
||||
assetFiltersState.animatedFilter.hasFilters() ||
|
||||
assetFiltersState.viewpointFilter.hasFilters() ||
|
||||
assetFiltersState.colorFilter.hasFilters() ||
|
||||
assetFiltersState.dimensionFilter.hasFilters() ||
|
||||
assetFiltersState.licenseFilter.hasFilters() ||
|
||||
assetFiltersState.objectTypeFilter.hasFilters(),
|
||||
[assetFiltersState]
|
||||
);
|
||||
|
||||
const hasOnlySelectedOwnedAssetPacks = React.useMemo(
|
||||
() =>
|
||||
// When a pack is opened, the asset pack filters are not hidden, but not relevant either.
|
||||
!openedAssetPack &&
|
||||
assetPackFiltersState.typeFilter.isOwned &&
|
||||
!assetPackFiltersState.typeFilter.isPremium &&
|
||||
!assetPackFiltersState.typeFilter.isFree &&
|
||||
!hasAssetFiltersApplied,
|
||||
[assetPackFiltersState, hasAssetFiltersApplied, openedAssetPack]
|
||||
);
|
||||
const noResultComponent = noResultsPlaceHolder ? (
|
||||
noResultsPlaceHolder
|
||||
) : hasOnlySelectedOwnedAssetPacks ? (
|
||||
<NoResultPlaceholder
|
||||
message={<Trans>You don't own any pack yet!</Trans>}
|
||||
onClear={clearAllFilters}
|
||||
/>
|
||||
) : hasAssetPackFiltersApplied && hasAssetFiltersApplied ? (
|
||||
<NoResultPlaceholder
|
||||
message={
|
||||
<Trans>
|
||||
Cannot filter on both asset packs and assets at the same time. Try
|
||||
clearing one of the filters!
|
||||
</Trans>
|
||||
}
|
||||
onClear={clearAllFilters}
|
||||
/>
|
||||
) : (
|
||||
<NoResultPlaceholder onClear={clearAllFilters} />
|
||||
);
|
||||
|
||||
const [selectedFolders, setSelectedFolders] = React.useState<Array<string>>(
|
||||
[]
|
||||
);
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (chosenCategory) {
|
||||
setSelectedFolders([chosenCategory.node.name]);
|
||||
} else {
|
||||
setSelectedFolders([]);
|
||||
}
|
||||
},
|
||||
[chosenCategory]
|
||||
);
|
||||
|
||||
const navigateInsideFolder = React.useCallback(
|
||||
folderTag => setSelectedFolders([...selectedFolders, folderTag]),
|
||||
[selectedFolders]
|
||||
);
|
||||
const goBackToFolderIndex = React.useCallback(
|
||||
folderIndex => {
|
||||
if (folderIndex >= selectedFolders.length || folderIndex < 0) {
|
||||
console.warn(
|
||||
'Trying to go back to a folder that is not in the selected folders.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
setSelectedFolders(selectedFolders.slice(0, folderIndex + 1));
|
||||
},
|
||||
[selectedFolders]
|
||||
);
|
||||
|
||||
// When selected folders change, set a flag to know that we are navigating inside a folder.
|
||||
// This allows showing a fake loading indicator.
|
||||
React.useEffect(
|
||||
() => {
|
||||
setIsNavigatingInsideFolder(true);
|
||||
const timeoutId = setTimeout(
|
||||
() => setIsNavigatingInsideFolder(false),
|
||||
100
|
||||
);
|
||||
return () => clearTimeout(timeoutId);
|
||||
},
|
||||
// Apply the effect only when the selected folders change.
|
||||
[selectedFolders]
|
||||
);
|
||||
|
||||
const folderTags: Array<string> = React.useMemo(
|
||||
() => {
|
||||
// When inside an asset pack, it will automatically select a folder.
|
||||
// So if the list is empty or if the assets are not loaded, we don't load the tags.
|
||||
if (!selectedFolders.length || !assetShortHeaders) return [];
|
||||
return getFolderTagsFromAssetShortHeaders({
|
||||
assetShortHeaders,
|
||||
selectedFolders,
|
||||
});
|
||||
},
|
||||
[assetShortHeaders, selectedFolders]
|
||||
);
|
||||
|
||||
const folderTiles = React.useMemo(
|
||||
() =>
|
||||
// Don't show folders if we are searching.
|
||||
folderTags.length > 0
|
||||
? folderTags.map(folderTag => (
|
||||
<AssetFolderTile
|
||||
tag={folderTag}
|
||||
key={folderTag}
|
||||
onSelect={() => {
|
||||
navigateInsideFolder(folderTag);
|
||||
}}
|
||||
/>
|
||||
))
|
||||
: [],
|
||||
[folderTags, navigateInsideFolder]
|
||||
);
|
||||
|
||||
const selectedPrivateAssetPackListingData = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!allPrivateAssetPackListingDatas ||
|
||||
!openedAssetPack ||
|
||||
!openedAssetPack.id // public pack selected.
|
||||
)
|
||||
return null;
|
||||
|
||||
// As the list should already been fetched, we can find the selected pack
|
||||
// if it is a private pack.
|
||||
return allPrivateAssetPackListingDatas.find(
|
||||
privateAssetPackListingData =>
|
||||
privateAssetPackListingData.id === openedAssetPack.id
|
||||
);
|
||||
},
|
||||
[allPrivateAssetPackListingDatas, openedAssetPack]
|
||||
);
|
||||
|
||||
const assetTiles = React.useMemo(
|
||||
() => {
|
||||
if (!assetShortHeaders) return null; // Loading
|
||||
if (hasAssetPackFiltersApplied && !openedAssetPack) return []; // Don't show assets if filtering on asset packs.)
|
||||
|
||||
return assetShortHeaders
|
||||
.filter(assetShortHeader => {
|
||||
if (!selectedFolders.length) return true;
|
||||
const allAssetTags = assetShortHeader.tags;
|
||||
// Check that the asset has all the selected folders tags.
|
||||
return selectedFolders.every(folderTag =>
|
||||
allAssetTags.includes(folderTag)
|
||||
);
|
||||
})
|
||||
.map(assetShortHeader => (
|
||||
<AssetCardTile
|
||||
assetShortHeader={assetShortHeader}
|
||||
onOpenDetails={() => onOpenDetails(assetShortHeader)}
|
||||
size={getAssetSize(windowWidth)}
|
||||
key={assetShortHeader.id}
|
||||
margin={cellSpacing / 2}
|
||||
/>
|
||||
))
|
||||
.splice(0, ASSETS_DISPLAY_LIMIT); // Limit the number of displayed assets to avoid performance issues
|
||||
},
|
||||
[
|
||||
assetShortHeaders,
|
||||
onOpenDetails,
|
||||
windowWidth,
|
||||
selectedFolders,
|
||||
hasAssetPackFiltersApplied,
|
||||
openedAssetPack,
|
||||
]
|
||||
);
|
||||
|
||||
const publicPacksTiles: Array<React.Node> = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!publicAssetPacks ||
|
||||
!onPublicAssetPackSelection ||
|
||||
hasAssetFiltersApplied // Don't show public packs if filtering on assets.
|
||||
)
|
||||
return [];
|
||||
return publicAssetPacks.map((assetPack, index) => (
|
||||
<PublicAssetPackTile
|
||||
assetPack={assetPack}
|
||||
onSelect={() => onPublicAssetPackSelection(assetPack)}
|
||||
key={`${assetPack.tag}-${index}`}
|
||||
/>
|
||||
));
|
||||
},
|
||||
[publicAssetPacks, onPublicAssetPackSelection, hasAssetFiltersApplied]
|
||||
);
|
||||
|
||||
const { allStandAlonePackTiles, allBundlePackTiles } = React.useMemo(
|
||||
() => {
|
||||
const privateAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackStandAloneTiles: Array<React.Node> = [];
|
||||
const privateAssetPackBundleTiles: Array<React.Node> = [];
|
||||
const privateOwnedAssetPackBundleTiles: Array<React.Node> = [];
|
||||
|
||||
if (
|
||||
!privateAssetPackListingDatas ||
|
||||
!receivedAssetPacks ||
|
||||
hasAssetFiltersApplied // Don't show private packs if filtering on assets.
|
||||
) {
|
||||
return {
|
||||
allStandAlonePackTiles: [],
|
||||
allBundlePackTiles: [],
|
||||
};
|
||||
}
|
||||
|
||||
!!onPrivateAssetPackSelection &&
|
||||
privateAssetPackListingDatas.forEach(assetPackListingData => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
pack => pack.id === assetPackListingData.id
|
||||
);
|
||||
const tile = (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackListingData}
|
||||
onSelect={() => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
}}
|
||||
owned={isPackOwned}
|
||||
key={assetPackListingData.id}
|
||||
/>
|
||||
);
|
||||
if (
|
||||
assetPackListingData.includedListableProductIds &&
|
||||
!!assetPackListingData.includedListableProductIds.length
|
||||
) {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackBundleTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackBundleTiles.push(tile);
|
||||
}
|
||||
} else {
|
||||
if (isPackOwned) {
|
||||
privateOwnedAssetPackStandAloneTiles.push(tile);
|
||||
} else {
|
||||
privateAssetPackStandAloneTiles.push(tile);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const allBundlePackTiles = [
|
||||
...privateOwnedAssetPackBundleTiles, // Display owned bundles first.
|
||||
...privateAssetPackBundleTiles,
|
||||
];
|
||||
|
||||
const allStandAlonePackTiles = [
|
||||
...privateOwnedAssetPackStandAloneTiles, // Display owned packs first.
|
||||
...mergeArraysPerGroup(
|
||||
privateAssetPackStandAloneTiles,
|
||||
publicPacksTiles,
|
||||
2,
|
||||
1
|
||||
),
|
||||
];
|
||||
|
||||
return { allStandAlonePackTiles, allBundlePackTiles };
|
||||
},
|
||||
[
|
||||
privateAssetPackListingDatas,
|
||||
onPrivateAssetPackSelection,
|
||||
publicPacksTiles,
|
||||
receivedAssetPacks,
|
||||
hasAssetFiltersApplied,
|
||||
]
|
||||
);
|
||||
|
||||
const packMainImageUrl = openedAssetPack
|
||||
? openedAssetPack.thumbnailUrl
|
||||
? openedAssetPack.thumbnailUrl
|
||||
: openedAssetPack.previewImageUrls
|
||||
? openedAssetPack.previewImageUrls[0]
|
||||
: null
|
||||
: null;
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
if (!selectedPrivateAssetPackListingData) {
|
||||
setAuthorPublicProfile(null);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const authorProfile = await getUserPublicProfile(
|
||||
selectedPrivateAssetPackListingData.sellerId
|
||||
);
|
||||
|
||||
setAuthorPublicProfile(authorProfile);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
// Do not block the UI if the author profile can't be fetched.
|
||||
}
|
||||
})();
|
||||
},
|
||||
[selectedPrivateAssetPackListingData]
|
||||
);
|
||||
|
||||
const publicAssetPackAuthors: ?Array<Author> = React.useMemo(
|
||||
() =>
|
||||
openedAssetPack && authors && openedAssetPack.authors
|
||||
? openedAssetPack.authors
|
||||
.map(author => {
|
||||
return authors.find(({ name }) => name === author.name);
|
||||
})
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
[openedAssetPack, authors]
|
||||
);
|
||||
|
||||
const publicAssetPackLicenses: ?Array<License> = React.useMemo(
|
||||
() =>
|
||||
openedAssetPack && licenses && openedAssetPack.licenses
|
||||
? openedAssetPack.licenses
|
||||
.map(license => {
|
||||
return licenses.find(({ name }) => name === license.name);
|
||||
})
|
||||
.filter(Boolean)
|
||||
: [],
|
||||
[openedAssetPack, licenses]
|
||||
);
|
||||
|
||||
return (
|
||||
<ScrollView
|
||||
ref={scrollView}
|
||||
id="asset-store-listing"
|
||||
style={{
|
||||
...styles.scrollView,
|
||||
...(noScroll ? { overflow: 'hidden' } : {}),
|
||||
}}
|
||||
>
|
||||
{!assetTiles && !error && <PlaceholderLoader />}
|
||||
{!assetTiles && error && (
|
||||
<PlaceholderError onRetry={fetchAssetsAndFilters}>
|
||||
<Trans>
|
||||
Can't load the results. Verify your internet connection or retry
|
||||
later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
)}
|
||||
{!openedAssetPack && allBundlePackTiles.length ? (
|
||||
<Line expand>
|
||||
<Column noMargin expand>
|
||||
<GridList
|
||||
cols={getAssetPacksColumns(windowWidth)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing / 2}
|
||||
>
|
||||
{allBundlePackTiles}
|
||||
</GridList>
|
||||
</Column>
|
||||
</Line>
|
||||
) : null}
|
||||
{!openedAssetPack && allStandAlonePackTiles.length ? (
|
||||
<Line expand>
|
||||
<Column noMargin expand>
|
||||
<GridList
|
||||
cols={getAssetPacksColumns(windowWidth)}
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing / 2}
|
||||
>
|
||||
{allStandAlonePackTiles}
|
||||
</GridList>
|
||||
</Column>
|
||||
</Line>
|
||||
) : null}
|
||||
{openedAssetPack && (
|
||||
<Column>
|
||||
<LineStackLayout>
|
||||
{packMainImageUrl && (
|
||||
<>
|
||||
<CorsAwareImage
|
||||
key={openedAssetPack.name}
|
||||
style={styles.previewImage}
|
||||
src={
|
||||
openedAssetPack.thumbnailUrl
|
||||
? openedAssetPack.thumbnailUrl
|
||||
: openedAssetPack.previewImageUrls
|
||||
? openedAssetPack.previewImageUrls[0]
|
||||
: ''
|
||||
}
|
||||
alt={`Preview image of asset pack ${openedAssetPack.name}`}
|
||||
/>
|
||||
<LargeSpacer />
|
||||
</>
|
||||
)}
|
||||
<Column noMargin alignItems="flex-start" expand>
|
||||
<Text size="bold-title">{openedAssetPack.name}</Text>
|
||||
{!!publicAssetPackAuthors && publicAssetPackAuthors.length > 0 && (
|
||||
<Text size="body" displayInlineAsSpan>
|
||||
<Trans>by</Trans>{' '}
|
||||
{publicAssetPackAuthors.map((author, index) => (
|
||||
<React.Fragment key={author.name}>
|
||||
{index > 0 && <>, </>}
|
||||
<Link
|
||||
key={author.name}
|
||||
href={author.website}
|
||||
onClick={() => Window.openExternalURL(author.website)}
|
||||
>
|
||||
{author.name}
|
||||
</Link>
|
||||
</React.Fragment>
|
||||
))}
|
||||
</Text>
|
||||
)}
|
||||
|
||||
{!!publicAssetPackLicenses &&
|
||||
publicAssetPackLicenses.length > 0 && (
|
||||
<Text size="body">
|
||||
<Trans>
|
||||
Type of License:{' '}
|
||||
{
|
||||
<Link
|
||||
href={publicAssetPackLicenses[0].website}
|
||||
onClick={() =>
|
||||
Window.openExternalURL(
|
||||
publicAssetPackLicenses[0].website
|
||||
)
|
||||
}
|
||||
>
|
||||
{publicAssetPackLicenses[0].name}
|
||||
</Link>
|
||||
}
|
||||
</Trans>
|
||||
</Text>
|
||||
)}
|
||||
{authorPublicProfile && (
|
||||
<Text displayInlineAsSpan size="sub-title">
|
||||
<Trans>by</Trans>{' '}
|
||||
<Link
|
||||
onClick={() => setOpenAuthorPublicProfileDialog(true)}
|
||||
href="#"
|
||||
>
|
||||
{authorPublicProfile.username || ''}
|
||||
</Link>
|
||||
</Text>
|
||||
)}
|
||||
</Column>
|
||||
</LineStackLayout>
|
||||
</Column>
|
||||
)}
|
||||
{openedAssetPack &&
|
||||
(folderTiles.length || (assetTiles && assetTiles.length)) ? (
|
||||
<Column>
|
||||
<Text size="section-title">
|
||||
<Trans>Content</Trans>
|
||||
</Text>
|
||||
</Column>
|
||||
) : null}
|
||||
{selectedFolders.length > 1 ? (
|
||||
<Column>
|
||||
<Line>
|
||||
<Breadcrumbs
|
||||
steps={selectedFolders.map((folder, index) => {
|
||||
if (index === selectedFolders.length - 1) {
|
||||
return {
|
||||
label: capitalize(folder),
|
||||
};
|
||||
}
|
||||
return {
|
||||
label: capitalize(folder),
|
||||
onClick: () => {
|
||||
goBackToFolderIndex(index);
|
||||
},
|
||||
href: '#',
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</Line>
|
||||
</Column>
|
||||
) : null}
|
||||
{openedAssetPack && folderTiles.length ? (
|
||||
<Column justifyContent="center">
|
||||
<GridList
|
||||
style={styles.grid}
|
||||
cellHeight="auto"
|
||||
cols={getAssetFoldersColumns(windowWidth)}
|
||||
spacing={cellSpacing / 2}
|
||||
>
|
||||
{folderTiles}
|
||||
</GridList>
|
||||
<LargeSpacer />
|
||||
</Column>
|
||||
) : null}
|
||||
{isNavigatingInsideFolder ? (
|
||||
<PlaceholderLoader />
|
||||
) : assetTiles && assetTiles.length ? (
|
||||
<GridList style={styles.grid} cellHeight="auto">
|
||||
{assetTiles}
|
||||
</GridList>
|
||||
) : openedAssetPack &&
|
||||
openedAssetPack.content &&
|
||||
isAssetPackAudioOnly(openedAssetPack) ? (
|
||||
<PrivateAssetPackAudioFilesDownloadButton
|
||||
assetPack={openedAssetPack}
|
||||
/>
|
||||
) : null}
|
||||
{assetTiles &&
|
||||
!assetTiles.length &&
|
||||
!allBundlePackTiles.length &&
|
||||
!allStandAlonePackTiles.length &&
|
||||
noResultComponent}
|
||||
{onPrivateAssetPackSelection &&
|
||||
openAuthorPublicProfileDialog &&
|
||||
authorPublicProfile && (
|
||||
<PublicProfileDialog
|
||||
userId={authorPublicProfile.id}
|
||||
onClose={() => setOpenAuthorPublicProfileDialog(false)}
|
||||
onAssetPackOpen={assetPackListingData => {
|
||||
onPrivateAssetPackSelection(assetPackListingData);
|
||||
setOpenAuthorPublicProfileDialog(false);
|
||||
setAuthorPublicProfile(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</ScrollView>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
export default AssetsList;
|
@@ -208,7 +208,16 @@ export const BehaviorStoreStateProvider = ({
|
||||
() => {
|
||||
const tagsSet = new Set();
|
||||
for (const type in allBehaviors) {
|
||||
allBehaviors[type].tags.forEach(tag => tagsSet.add(tag));
|
||||
const behavior = allBehaviors[type];
|
||||
behavior.tags.forEach(tag => {
|
||||
if (
|
||||
showCommunityExtensions ||
|
||||
!behavior.tier ||
|
||||
!excludedCommunityTiers.has(behavior.tier)
|
||||
) {
|
||||
tagsSet.add(tag);
|
||||
}
|
||||
});
|
||||
}
|
||||
const sortedTags = [...tagsSet].sort((tag1, tag2) =>
|
||||
tag1.toLowerCase().localeCompare(tag2.toLowerCase())
|
||||
@@ -219,7 +228,7 @@ export const BehaviorStoreStateProvider = ({
|
||||
tagsTree: [],
|
||||
};
|
||||
},
|
||||
[allBehaviors]
|
||||
[allBehaviors, showCommunityExtensions]
|
||||
);
|
||||
|
||||
const defaultFirstSearchItemIds = React.useMemo(
|
||||
|
@@ -148,7 +148,7 @@ export const ExampleListItem = ({
|
||||
</Column>
|
||||
</ResponsiveLineStackLayout>
|
||||
</ButtonBase>
|
||||
<Column noMargin justifyContent="flex-end">
|
||||
<Column justifyContent="flex-end">
|
||||
<Line noMargin justifyContent="flex-end">
|
||||
<FlatButtonWithSplitMenu
|
||||
label={<Trans>Open</Trans>}
|
||||
|
@@ -129,7 +129,7 @@ export default function NewObjectDialog({
|
||||
]);
|
||||
|
||||
const {
|
||||
searchResults,
|
||||
assetShortHeadersSearchResults,
|
||||
navigationState,
|
||||
environment,
|
||||
setEnvironment,
|
||||
@@ -319,7 +319,10 @@ export default function NewObjectDialog({
|
||||
primary
|
||||
label={<Trans>Add all assets to my scene</Trans>}
|
||||
onClick={() => setIsAssetPackDialogInstallOpen(true)}
|
||||
disabled={!searchResults || searchResults.length === 0}
|
||||
disabled={
|
||||
!assetShortHeadersSearchResults ||
|
||||
assetShortHeadersSearchResults.length === 0
|
||||
}
|
||||
/>
|
||||
) : openedAssetShortHeader ? (
|
||||
<RaisedButton
|
||||
@@ -458,22 +461,24 @@ export default function NewObjectDialog({
|
||||
/>
|
||||
)}
|
||||
</Dialog>
|
||||
{isAssetPackDialogInstallOpen && searchResults && openedAssetPack && (
|
||||
<AssetPackInstallDialog
|
||||
assetPack={openedAssetPack}
|
||||
assetShortHeaders={searchResults}
|
||||
addedAssetIds={existingAssetStoreIds}
|
||||
onClose={() => setIsAssetPackDialogInstallOpen(false)}
|
||||
onAssetsAdded={() => {
|
||||
setIsAssetPackDialogInstallOpen(false);
|
||||
}}
|
||||
project={project}
|
||||
objectsContainer={objectsContainer}
|
||||
onObjectsAddedFromAssets={onObjectsAddedFromAssets}
|
||||
canInstallPrivateAsset={canInstallPrivateAsset}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
/>
|
||||
)}
|
||||
{isAssetPackDialogInstallOpen &&
|
||||
assetShortHeadersSearchResults &&
|
||||
openedAssetPack && (
|
||||
<AssetPackInstallDialog
|
||||
assetPack={openedAssetPack}
|
||||
assetShortHeaders={assetShortHeadersSearchResults}
|
||||
addedAssetIds={existingAssetStoreIds}
|
||||
onClose={() => setIsAssetPackDialogInstallOpen(false)}
|
||||
onAssetsAdded={() => {
|
||||
setIsAssetPackDialogInstallOpen(false);
|
||||
}}
|
||||
project={project}
|
||||
objectsContainer={objectsContainer}
|
||||
onObjectsAddedFromAssets={onObjectsAddedFromAssets}
|
||||
canInstallPrivateAsset={canInstallPrivateAsset}
|
||||
resourceManagementProps={resourceManagementProps}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</I18n>
|
||||
|
@@ -14,14 +14,13 @@ import ScrollView from '../UI/ScrollView';
|
||||
import useDismissableTutorialMessage from '../Hints/useDismissableTutorialMessage';
|
||||
import { AssetStoreContext } from './AssetStoreContext';
|
||||
import { translateExtensionCategory } from '../Utils/Extension/ExtensionCategories';
|
||||
import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
|
||||
import { type ChosenCategory } from '../UI/Search/FiltersChooser';
|
||||
import { AssetCard } from './AssetCard';
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
import TextButton from '../UI/TextButton';
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import LoaderModal from '../UI/LoaderModal';
|
||||
import ChevronArrowLeft from '../UI/CustomSvgIcons/ChevronArrowLeft';
|
||||
import AssetsList from './AssetsList';
|
||||
|
||||
const ObjectListItem = ({
|
||||
enumeratedObjectMetadata,
|
||||
@@ -69,9 +68,7 @@ export const CustomObjectPackResults = ({
|
||||
onBack,
|
||||
isAssetBeingInstalled,
|
||||
}: CustomObjectPackResultsProps) => {
|
||||
const { useSearchItem, error, fetchAssetsAndFilters } = React.useContext(
|
||||
AssetStoreContext
|
||||
);
|
||||
const { useSearchItem, error } = React.useContext(AssetStoreContext);
|
||||
// Memoizing the parameters of the search as it seems to trigger infinite rendering if not.
|
||||
const chosenCategory: ChosenCategory = React.useMemo(
|
||||
() => ({
|
||||
@@ -104,22 +101,13 @@ export const CustomObjectPackResults = ({
|
||||
disabled={isAssetBeingInstalled}
|
||||
/>
|
||||
</Line>
|
||||
<BoxSearchResults
|
||||
baseSize={128}
|
||||
onRetry={fetchAssetsAndFilters}
|
||||
<AssetsList
|
||||
assetShortHeaders={selectedAssetPackSearchResults}
|
||||
error={error}
|
||||
searchItems={selectedAssetPackSearchResults}
|
||||
spacing={8}
|
||||
renderSearchItem={(assetShortHeader, size) => (
|
||||
<AssetCard
|
||||
size={size}
|
||||
onOpenDetails={() => {
|
||||
if (isAssetBeingInstalled) return;
|
||||
onAssetSelect(assetShortHeader);
|
||||
}}
|
||||
assetShortHeader={assetShortHeader}
|
||||
/>
|
||||
)}
|
||||
onOpenDetails={assetShortHeader => {
|
||||
if (isAssetBeingInstalled) return;
|
||||
onAssetSelect(assetShortHeader);
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
<LoaderModal show={isAssetBeingInstalled} />
|
||||
|
@@ -7,6 +7,7 @@ import RestoreIcon from '@material-ui/icons/SettingsBackupRestore';
|
||||
|
||||
type Props = {|
|
||||
onClear: () => void,
|
||||
message?: React.Node,
|
||||
|};
|
||||
|
||||
export const NoResultPlaceholder = (props: Props) => (
|
||||
@@ -14,10 +15,12 @@ export const NoResultPlaceholder = (props: Props) => (
|
||||
<EmptyPlaceholder
|
||||
title={<Trans>No result</Trans>}
|
||||
description={
|
||||
<Trans>
|
||||
Try something else, browse the packs or create your object from
|
||||
scratch!
|
||||
</Trans>
|
||||
props.message || (
|
||||
<Trans>
|
||||
Try something else, browse the packs or create your object from
|
||||
scratch!
|
||||
</Trans>
|
||||
)
|
||||
}
|
||||
actionLabel={<Trans>Clear all filters</Trans>}
|
||||
actionButtonId="clear-filters-button"
|
||||
|
@@ -0,0 +1,56 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import PrivateAssetsAuthorizationContext from './PrivateAssetsAuthorizationContext';
|
||||
import { type PrivateAssetPack } from '../../Utils/GDevelopServices/Asset';
|
||||
import RaisedButton from '../../UI/RaisedButton';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import Music from '../../UI/CustomSvgIcons/Music';
|
||||
import Window from '../../Utils/Window';
|
||||
import { Column } from '../../UI/Grid';
|
||||
|
||||
const PrivateAssetPackAudioFilesDownloadButton = ({
|
||||
assetPack,
|
||||
}: {|
|
||||
assetPack: PrivateAssetPack,
|
||||
|}) => {
|
||||
const { getPrivateAssetPackAudioArchiveUrl } = React.useContext(
|
||||
PrivateAssetsAuthorizationContext
|
||||
);
|
||||
const [
|
||||
isAudioArchiveUrlLoading,
|
||||
setIsAudioArchiveUrlLoading,
|
||||
] = React.useState(false);
|
||||
|
||||
return (
|
||||
<Column expand alignItems="center" justifyContent="center">
|
||||
<RaisedButton
|
||||
primary
|
||||
label={
|
||||
isAudioArchiveUrlLoading ? (
|
||||
<Trans>Loading...</Trans>
|
||||
) : (
|
||||
<Trans>Download pack sounds</Trans>
|
||||
)
|
||||
}
|
||||
icon={<Music />}
|
||||
disabled={isAudioArchiveUrlLoading}
|
||||
onClick={async () => {
|
||||
setIsAudioArchiveUrlLoading(true);
|
||||
const url = await getPrivateAssetPackAudioArchiveUrl(assetPack.id);
|
||||
setIsAudioArchiveUrlLoading(false);
|
||||
if (!url) {
|
||||
console.error(
|
||||
`Could not generate url for premium asset pack with name ${
|
||||
assetPack.name
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
Window.openExternalURL(url);
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
||||
export default PrivateAssetPackAudioFilesDownloadButton;
|
@@ -17,7 +17,7 @@ import {
|
||||
LineStackLayout,
|
||||
ColumnStackLayout,
|
||||
} from '../../UI/Layout';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { Column, LargeSpacer, Line } from '../../UI/Grid';
|
||||
import {
|
||||
getUserPublicProfile,
|
||||
type UserPublicProfile,
|
||||
@@ -37,17 +37,21 @@ import { MarkdownText } from '../../UI/MarkdownText';
|
||||
import Paper from '../../UI/Paper';
|
||||
import Window from '../../Utils/Window';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { PrivateAssetPackTile } from '../AssetsHome';
|
||||
import {
|
||||
purchaseAppStoreProduct,
|
||||
shouldUseAppStoreProduct,
|
||||
} from '../../Utils/AppStorePurchases';
|
||||
import { formatPrivateAssetPackPrice } from './PrivateAssetPackPriceTag';
|
||||
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
|
||||
import {
|
||||
PrivateAssetPackTile,
|
||||
PromoBundleAssetPackCard,
|
||||
} from '../AssetPackTiles';
|
||||
import { AssetStoreContext } from '../AssetStoreContext';
|
||||
|
||||
const cellSpacing = 2;
|
||||
const cellSpacing = 8;
|
||||
|
||||
const getSameCreatorPackColumns = (windowWidth: WidthType) => {
|
||||
const getPackColumns = (windowWidth: WidthType) => {
|
||||
switch (windowWidth) {
|
||||
case 'small':
|
||||
return 2;
|
||||
@@ -87,6 +91,11 @@ const contentTypeToMessageDescriptor = {
|
||||
const styles = {
|
||||
disabledText: { opacity: 0.6 },
|
||||
scrollview: { overflowX: 'hidden' },
|
||||
grid: {
|
||||
margin: '0 2px', // Remove the default margin of the grid but keep the horizontal padding for focus outline.
|
||||
// Remove the scroll capability of the grid, the scroll view handles it.
|
||||
overflow: 'unset',
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
@@ -105,6 +114,7 @@ const PrivateAssetPackInformationPage = ({
|
||||
onAssetPackOpen,
|
||||
}: Props) => {
|
||||
const { id, name, sellerId } = privateAssetPackListingData;
|
||||
const { privateAssetPackListingDatas } = React.useContext(AssetStoreContext);
|
||||
const { receivedAssetPacks } = React.useContext(AuthenticatedUserContext);
|
||||
const [assetPack, setAssetPack] = React.useState<?PrivateAssetPack>(null);
|
||||
const [isFetching, setIsFetching] = React.useState<boolean>(false);
|
||||
@@ -130,6 +140,147 @@ const PrivateAssetPackInformationPage = ({
|
||||
assetPack => assetPack.id === privateAssetPackListingData.id
|
||||
);
|
||||
|
||||
const packsIncludedInBundleTiles = React.useMemo(
|
||||
() => {
|
||||
if (!assetPack || !privateAssetPackListingDatas) return null;
|
||||
|
||||
const includedPackIds =
|
||||
privateAssetPackListingData.includedListableProductIds;
|
||||
if (!includedPackIds) return null;
|
||||
|
||||
return includedPackIds.map(includedPackId => {
|
||||
const includedAssetPackListingData = privateAssetPackListingDatas.find(
|
||||
privatePackListingData => privatePackListingData.id === includedPackId
|
||||
);
|
||||
if (!includedAssetPackListingData) {
|
||||
console.warn(`Included pack ${includedPackId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
pack => pack.id === includedAssetPackListingData.id
|
||||
);
|
||||
return (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={includedAssetPackListingData}
|
||||
key={includedAssetPackListingData.id}
|
||||
onSelect={() => onAssetPackOpen(includedAssetPackListingData)}
|
||||
owned={isPackOwned}
|
||||
/>
|
||||
);
|
||||
});
|
||||
},
|
||||
[
|
||||
assetPack,
|
||||
privateAssetPackListingDatas,
|
||||
receivedAssetPacks,
|
||||
onAssetPackOpen,
|
||||
privateAssetPackListingData,
|
||||
]
|
||||
);
|
||||
|
||||
const bundlesContainingPackTiles = React.useMemo(
|
||||
() => {
|
||||
if (!assetPack || !privateAssetPackListingDatas) return null;
|
||||
|
||||
const bundlesContainingPack = privateAssetPackListingDatas.filter(
|
||||
privatePackListingData =>
|
||||
privatePackListingData.includedListableProductIds &&
|
||||
privatePackListingData.includedListableProductIds.includes(
|
||||
assetPack.id
|
||||
)
|
||||
);
|
||||
|
||||
if (!bundlesContainingPack.length) return null;
|
||||
|
||||
const ownedBundlesContainingPack = bundlesContainingPack.filter(
|
||||
bundleContainingPack =>
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(pack => pack.id === bundleContainingPack.id)
|
||||
);
|
||||
const notOwnedBundlesContainingPack = bundlesContainingPack.filter(
|
||||
bundleContainingPack =>
|
||||
!ownedBundlesContainingPack.find(
|
||||
ownedBundleContainingPack =>
|
||||
ownedBundleContainingPack.id === bundleContainingPack.id
|
||||
)
|
||||
);
|
||||
|
||||
const allTiles = ownedBundlesContainingPack
|
||||
.map(bundleContainingPack => {
|
||||
return (
|
||||
<PromoBundleAssetPackCard
|
||||
assetPackListingData={bundleContainingPack}
|
||||
onSelect={() => onAssetPackOpen(bundleContainingPack)}
|
||||
owned
|
||||
/>
|
||||
);
|
||||
})
|
||||
.concat(
|
||||
notOwnedBundlesContainingPack.map(bundleContainingPack => {
|
||||
return (
|
||||
<PromoBundleAssetPackCard
|
||||
assetPackListingData={bundleContainingPack}
|
||||
onSelect={() => onAssetPackOpen(bundleContainingPack)}
|
||||
owned={false}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
|
||||
return allTiles;
|
||||
},
|
||||
[
|
||||
assetPack,
|
||||
privateAssetPackListingDatas,
|
||||
receivedAssetPacks,
|
||||
onAssetPackOpen,
|
||||
]
|
||||
);
|
||||
|
||||
const otherPacksFromTheSameAuthorTiles = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!privateAssetPacksFromSameCreatorListingData ||
|
||||
// Only display packs if there are at least 2. If there is only one,
|
||||
// it means it's the same as the one currently opened.
|
||||
privateAssetPacksFromSameCreatorListingData.length < 2
|
||||
)
|
||||
return null;
|
||||
|
||||
return (
|
||||
privateAssetPacksFromSameCreatorListingData
|
||||
// Do not display the pack currently opened.
|
||||
.filter(
|
||||
assetPackFromSameCreator => assetPackFromSameCreator.id !== id
|
||||
)
|
||||
.map(assetPackFromSameCreator => {
|
||||
const isPackOwned =
|
||||
!!receivedAssetPacks &&
|
||||
!!receivedAssetPacks.find(
|
||||
pack => pack.id === assetPackFromSameCreator.id
|
||||
);
|
||||
return (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={assetPackFromSameCreator}
|
||||
key={assetPackFromSameCreator.id}
|
||||
onSelect={() => onAssetPackOpen(assetPackFromSameCreator)}
|
||||
owned={isPackOwned}
|
||||
/>
|
||||
);
|
||||
})
|
||||
);
|
||||
},
|
||||
[
|
||||
id,
|
||||
privateAssetPacksFromSameCreatorListingData,
|
||||
onAssetPackOpen,
|
||||
receivedAssetPacks,
|
||||
]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
(async () => {
|
||||
@@ -236,19 +387,25 @@ const PrivateAssetPackInformationPage = ({
|
||||
};
|
||||
|
||||
const mediaItems = assetPack
|
||||
? assetPack.previewImageUrls
|
||||
.map(url => ({
|
||||
? [
|
||||
{
|
||||
kind: 'image',
|
||||
url,
|
||||
}))
|
||||
.concat(
|
||||
assetPack.previewSoundUrls
|
||||
? assetPack.previewSoundUrls.map(url => ({
|
||||
kind: 'audio',
|
||||
url,
|
||||
}))
|
||||
: []
|
||||
)
|
||||
url: privateAssetPackListingData.thumbnailUrls[0],
|
||||
},
|
||||
...assetPack.previewImageUrls
|
||||
.map(url => ({
|
||||
kind: 'image',
|
||||
url,
|
||||
}))
|
||||
.concat(
|
||||
assetPack.previewSoundUrls
|
||||
? assetPack.previewSoundUrls.map(url => ({
|
||||
kind: 'audio',
|
||||
url,
|
||||
}))
|
||||
: []
|
||||
),
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
@@ -398,37 +555,57 @@ const PrivateAssetPackInformationPage = ({
|
||||
</Paper>
|
||||
</ColumnStackLayout>
|
||||
</ResponsiveLineStackLayout>
|
||||
{privateAssetPacksFromSameCreatorListingData &&
|
||||
// Only display packs if there are at least 2. If there is only one,
|
||||
// it means it's the same as the one currently opened.
|
||||
privateAssetPacksFromSameCreatorListingData.length >= 2 ? (
|
||||
{bundlesContainingPackTiles &&
|
||||
bundlesContainingPackTiles.length ? (
|
||||
<>
|
||||
<ColumnStackLayout noMargin>
|
||||
<LargeSpacer />
|
||||
{bundlesContainingPackTiles}
|
||||
<LargeSpacer />
|
||||
</ColumnStackLayout>
|
||||
</>
|
||||
) : null}
|
||||
{packsIncludedInBundleTiles && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>From the same author</Trans>
|
||||
<Trans>Included in this bundle</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getSameCreatorPackColumns(windowWidth)}
|
||||
cols={getPackColumns(windowWidth)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing}
|
||||
spacing={cellSpacing / 2}
|
||||
style={styles.grid}
|
||||
>
|
||||
{privateAssetPacksFromSameCreatorListingData.map(
|
||||
pack => (
|
||||
<PrivateAssetPackTile
|
||||
assetPackListingData={pack}
|
||||
key={pack.id}
|
||||
onSelect={() => onAssetPackOpen(pack)}
|
||||
owned={false}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
{packsIncludedInBundleTiles}
|
||||
</GridList>
|
||||
<Grid />
|
||||
</Line>
|
||||
</>
|
||||
) : null}
|
||||
)}
|
||||
{otherPacksFromTheSameAuthorTiles &&
|
||||
otherPacksFromTheSameAuthorTiles.length > 0 && (
|
||||
<>
|
||||
<Line>
|
||||
<Text size="block-title">
|
||||
<Trans>From the same author</Trans>
|
||||
</Text>
|
||||
</Line>
|
||||
<Line>
|
||||
<GridList
|
||||
cols={getPackColumns(windowWidth)}
|
||||
cellHeight="auto"
|
||||
spacing={cellSpacing / 2}
|
||||
style={styles.grid}
|
||||
>
|
||||
{otherPacksFromTheSameAuthorTiles}
|
||||
</GridList>
|
||||
<Grid />
|
||||
</Line>
|
||||
</>
|
||||
)}
|
||||
</ScrollView>
|
||||
</Column>
|
||||
) : null}
|
||||
|
69
newIDE/app/src/AssetStore/TagsHelper.js
Normal file
69
newIDE/app/src/AssetStore/TagsHelper.js
Normal file
@@ -0,0 +1,69 @@
|
||||
// @flow
|
||||
import { type AssetShortHeader } from '../Utils/GDevelopServices/Asset';
|
||||
|
||||
const indexOfOccurrence = (
|
||||
haystack: Array<string>,
|
||||
needle: string,
|
||||
occurrence: number
|
||||
) => {
|
||||
var counter = 0;
|
||||
var index = -1;
|
||||
do {
|
||||
index = haystack.indexOf(needle, index + 1);
|
||||
} while (index !== -1 && ++counter < occurrence);
|
||||
return index;
|
||||
};
|
||||
|
||||
export const getFolderTagsFromAssetShortHeaders = ({
|
||||
assetShortHeaders,
|
||||
selectedFolders,
|
||||
}: {|
|
||||
assetShortHeaders: Array<AssetShortHeader>,
|
||||
selectedFolders: Array<string>,
|
||||
|}): Array<string> => {
|
||||
const assetTagsAfterPackTag: string[][] = [];
|
||||
const allTagsAfterPackTag = new Set();
|
||||
// We are in a pack, calculate first level folders based on asset tags.
|
||||
// Tags are stored from top to bottom, in the list of tags of an asset.
|
||||
// We first detect where the chosen category is, as this is the pack, and
|
||||
// remove this tags and the others before (that could be bundles).
|
||||
|
||||
assetShortHeaders.forEach(assetShortHeader => {
|
||||
const allAssetTags = assetShortHeader.tags;
|
||||
const lastSelectedFolder = selectedFolders[selectedFolders.length - 1];
|
||||
const occurencesOfLastSelectedFolderInSelectedFolders = selectedFolders.filter(
|
||||
folder => folder === lastSelectedFolder
|
||||
).length;
|
||||
const lastSelectedFolderIndex = indexOfOccurrence(
|
||||
allAssetTags,
|
||||
selectedFolders[selectedFolders.length - 1],
|
||||
occurencesOfLastSelectedFolderInSelectedFolders
|
||||
);
|
||||
if (lastSelectedFolderIndex === -1) return allAssetTags; // This shouldn't happen, but just in case.
|
||||
const tagsAfterPackTags = allAssetTags.filter(
|
||||
(tag, index) => index > lastSelectedFolderIndex
|
||||
);
|
||||
if (tagsAfterPackTags.length > 0)
|
||||
assetTagsAfterPackTag.push(tagsAfterPackTags);
|
||||
tagsAfterPackTags.forEach(tag => allTagsAfterPackTag.add(tag));
|
||||
});
|
||||
|
||||
// Then we remove the tags that are present in all assets, they're not useful, or not a folder.
|
||||
// (For example: "pixel art")
|
||||
const tagsPresentInAllAssets = Array.from(allTagsAfterPackTag).filter(
|
||||
tag =>
|
||||
assetTagsAfterPackTag.filter(tags => tags.includes(tag)).length ===
|
||||
assetTagsAfterPackTag.length
|
||||
);
|
||||
const assetTagsAfterPackTagWithoutNonFolderTags = assetTagsAfterPackTag.map(
|
||||
tags => tags.filter(tag => !tagsPresentInAllAssets.includes(tag))
|
||||
);
|
||||
|
||||
// Then we create the folders list, corresponding to the first level tags.
|
||||
const firstLevelTags = new Set();
|
||||
assetTagsAfterPackTagWithoutNonFolderTags.forEach(
|
||||
tags => firstLevelTags.add(tags[0]) // Only add the top one, as this will be the first folder.
|
||||
);
|
||||
|
||||
return Array.from(firstLevelTags).filter(Boolean);
|
||||
};
|
112
newIDE/app/src/AssetStore/TagsHelper.spec.js
Normal file
112
newIDE/app/src/AssetStore/TagsHelper.spec.js
Normal file
@@ -0,0 +1,112 @@
|
||||
// @flow
|
||||
import { fakeAssetShortHeader1 } from '../fixtures/GDevelopServicesTestData';
|
||||
const { getFolderTagsFromAssetShortHeaders } = require('./TagsHelper');
|
||||
|
||||
describe('getFolderTagsFromAssetShortHeaders', () => {
|
||||
it('should return an empty array if no assets', () => {
|
||||
const selectedFolders = ['pack-tag'];
|
||||
const assetShortHeaders = [];
|
||||
const result = getFolderTagsFromAssetShortHeaders({
|
||||
selectedFolders,
|
||||
assetShortHeaders,
|
||||
});
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return the top level tags of assets in a pack', () => {
|
||||
const selectedFolders = ['pack-tag'];
|
||||
const assetShortHeader1 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag2'],
|
||||
};
|
||||
const assetShortHeader2 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag4', 'tag5'],
|
||||
};
|
||||
const assetShortHeader3 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag4'],
|
||||
};
|
||||
const assetShortHeader4 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag4', 'tag6'],
|
||||
};
|
||||
const assetShortHeaders = [
|
||||
assetShortHeader1,
|
||||
assetShortHeader2,
|
||||
assetShortHeader3,
|
||||
assetShortHeader4,
|
||||
];
|
||||
|
||||
const result = getFolderTagsFromAssetShortHeaders({
|
||||
selectedFolders,
|
||||
assetShortHeaders,
|
||||
});
|
||||
expect(result).toEqual(['tag2', 'tag4']); // no pack-tag, as it's the pack, no tag1 as it matches all assets.
|
||||
});
|
||||
|
||||
it('should return the top level tags after navigating inside a folder', () => {
|
||||
const selectedFolders = ['pack-tag', 'tag4']; // navigate inside tag4
|
||||
const assetShortHeader1 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag2'],
|
||||
};
|
||||
const assetShortHeader2 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag4', 'tag5'],
|
||||
};
|
||||
const assetShortHeader3 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag4'],
|
||||
};
|
||||
const assetShortHeader4 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag4', 'tag6'],
|
||||
};
|
||||
const assetShortHeaders = [
|
||||
assetShortHeader1,
|
||||
assetShortHeader2,
|
||||
assetShortHeader3,
|
||||
assetShortHeader4,
|
||||
];
|
||||
|
||||
const result = getFolderTagsFromAssetShortHeaders({
|
||||
selectedFolders,
|
||||
assetShortHeaders,
|
||||
});
|
||||
expect(result).toEqual(['tag5', 'tag6']); // no pack-tag, as it's the pack, no tag1 as it matches all assets.
|
||||
});
|
||||
|
||||
it('works if a tag is duplicated', () => {
|
||||
const selectedFolders = ['pack-tag']; // root of pack.
|
||||
const assetShortHeader1 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag1'], // tag1 is duplicated because inside a folder of the same name.
|
||||
};
|
||||
const assetShortHeader2 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag1', 'tag2'],
|
||||
};
|
||||
const assetShortHeader3 = {
|
||||
...fakeAssetShortHeader1,
|
||||
tags: ['pack-tag', 'tag3'],
|
||||
};
|
||||
const assetShortHeaders = [
|
||||
assetShortHeader1,
|
||||
assetShortHeader2,
|
||||
assetShortHeader3,
|
||||
];
|
||||
|
||||
const result = getFolderTagsFromAssetShortHeaders({
|
||||
selectedFolders,
|
||||
assetShortHeaders,
|
||||
});
|
||||
expect(result).toEqual(['tag1', 'tag3']); // tag1 is a folder at the root of pack.
|
||||
|
||||
const result2 = getFolderTagsFromAssetShortHeaders({
|
||||
selectedFolders: ['pack-tag', 'tag1'], // inside tag1 folder
|
||||
assetShortHeaders,
|
||||
});
|
||||
expect(result2).toEqual(['tag1', 'tag2']); // tag1 is a folder inside tag1 folder.
|
||||
});
|
||||
});
|
@@ -21,24 +21,14 @@ import {
|
||||
isAssetPackAudioOnly,
|
||||
} from '../Utils/GDevelopServices/Asset';
|
||||
import { type PrivateAssetPackListingData } from '../Utils/GDevelopServices/Shop';
|
||||
import {
|
||||
BoxSearchResults,
|
||||
type BoxSearchResultsInterface,
|
||||
} from '../UI/Search/BoxSearchResults';
|
||||
import { type SearchBarInterface } from '../UI/SearchBar';
|
||||
import {
|
||||
AssetStoreFilterPanel,
|
||||
clearAllFilters,
|
||||
} from './AssetStoreFilterPanel';
|
||||
import { AssetStoreFilterPanel } from './AssetStoreFilterPanel';
|
||||
import { AssetStoreContext } from './AssetStoreContext';
|
||||
import { AssetCard } from './AssetCard';
|
||||
import { NoResultPlaceholder } from './NoResultPlaceholder';
|
||||
import { useResponsiveWindowWidth } from '../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import { useShouldAutofocusInput } from '../UI/Reponsive/ScreenTypeMeasurer';
|
||||
import Subheader from '../UI/Subheader';
|
||||
import { AssetsHome, type AssetsHomeInterface } from './AssetsHome';
|
||||
import TextButton from '../UI/TextButton';
|
||||
import Text from '../UI/Text';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import { AssetDetails, type AssetDetailsInterface } from './AssetDetails';
|
||||
import PlaceholderLoader from '../UI/PlaceholderLoader';
|
||||
@@ -54,14 +44,11 @@ import {
|
||||
isSearchResultPage,
|
||||
type AssetStorePageState,
|
||||
} from './AssetStoreNavigator';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import { ResponsivePaperOrDrawer } from '../UI/ResponsivePaperOrDrawer';
|
||||
import PrivateAssetsAuthorizationContext from './PrivateAssets/PrivateAssetsAuthorizationContext';
|
||||
import Music from '../UI/CustomSvgIcons/Music';
|
||||
|
||||
const capitalize = (str: string) => {
|
||||
return str ? str[0].toUpperCase() + str.substr(1) : '';
|
||||
};
|
||||
import AssetsList, { type AssetsListInterface } from './AssetsList';
|
||||
import PrivateAssetPackAudioFilesDownloadButton from './PrivateAssets/PrivateAssetPackAudioFilesDownloadButton';
|
||||
import Text from '../UI/Text';
|
||||
import { capitalize } from 'lodash';
|
||||
|
||||
type Props = {||};
|
||||
|
||||
@@ -70,11 +57,11 @@ export type AssetStoreInterface = {|
|
||||
|};
|
||||
|
||||
const identifyAssetPackKind = ({
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
publicAssetPacks,
|
||||
assetPack,
|
||||
}: {|
|
||||
privateAssetPacks: ?Array<PrivateAssetPackListingData>,
|
||||
privateAssetPackListingDatas: ?Array<PrivateAssetPackListingData>,
|
||||
publicAssetPacks: ?PublicAssetPacks,
|
||||
assetPack: PrivateAssetPack | PublicAssetPack | null,
|
||||
|}) => {
|
||||
@@ -85,8 +72,8 @@ const identifyAssetPackKind = ({
|
||||
// won't break this detection in the future (for example, if public asset packs get an `id`,
|
||||
// this won't break).
|
||||
return assetPack.id &&
|
||||
privateAssetPacks &&
|
||||
!!privateAssetPacks.find(({ id }) => id === assetPack.id)
|
||||
privateAssetPackListingDatas &&
|
||||
!!privateAssetPackListingDatas.find(({ id }) => id === assetPack.id)
|
||||
? 'private'
|
||||
: publicAssetPacks &&
|
||||
publicAssetPacks.starterPacks.find(({ tag }) => tag === assetPack.tag)
|
||||
@@ -97,15 +84,17 @@ const identifyAssetPackKind = ({
|
||||
export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
(props: Props, ref) => {
|
||||
const {
|
||||
assetShortHeadersSearchResults,
|
||||
publicAssetPacksSearchResults,
|
||||
privateAssetPackListingDatasSearchResults,
|
||||
publicAssetPacks,
|
||||
privateAssetPacks,
|
||||
searchResults,
|
||||
privateAssetPackListingDatas,
|
||||
error,
|
||||
fetchAssetsAndFilters,
|
||||
navigationState,
|
||||
searchText,
|
||||
setSearchText,
|
||||
assetFiltersState,
|
||||
clearAllFilters,
|
||||
assetPackRandomOrdering,
|
||||
} = React.useContext(AssetStoreContext);
|
||||
const currentPage = navigationState.getCurrentPage();
|
||||
@@ -146,13 +135,6 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
const { onPurchaseSuccessful, receivedAssetPacks } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const { getPrivateAssetPackAudioArchiveUrl } = React.useContext(
|
||||
PrivateAssetsAuthorizationContext
|
||||
);
|
||||
const [
|
||||
isAudioArchiveUrlLoading,
|
||||
setIsAudioArchiveUrlLoading,
|
||||
] = React.useState(false);
|
||||
|
||||
// The saved scroll position must not be reset by a scroll event until it
|
||||
// has been applied.
|
||||
@@ -168,6 +150,18 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
[]
|
||||
);
|
||||
|
||||
const reApplySearchTextIfNeeded = React.useCallback(
|
||||
(page: AssetStorePageState): boolean => {
|
||||
const previousSearchText = page.searchText || '';
|
||||
if (searchText !== previousSearchText) {
|
||||
setSearchText(previousSearchText);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
[searchText, setSearchText]
|
||||
);
|
||||
|
||||
const canShowFiltersPanel =
|
||||
!openedAssetShortHeader && // Don't show filters on asset page.
|
||||
!openedPrivateAssetPackListingData && // Don't show filters on private asset pack information page.
|
||||
@@ -178,12 +172,10 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
isAssetPackAudioOnly(openedAssetPack)
|
||||
);
|
||||
const assetsHome = React.useRef<?AssetsHomeInterface>(null);
|
||||
const boxSearchResults = React.useRef<?BoxSearchResultsInterface>(null);
|
||||
const assetDetails = React.useRef<?AssetDetailsInterface>(null);
|
||||
const assetsList = React.useRef<?AssetsListInterface>(null);
|
||||
const getScrollView = React.useCallback(() => {
|
||||
return (
|
||||
assetsHome.current || boxSearchResults.current || assetDetails.current
|
||||
);
|
||||
return assetsHome.current || assetDetails.current || assetsList.current;
|
||||
}, []);
|
||||
const saveScrollPosition = React.useCallback(
|
||||
() => {
|
||||
@@ -197,7 +189,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
);
|
||||
// This is also called when the asset detail page has loaded.
|
||||
const applyBackScrollPosition = React.useCallback(
|
||||
() => {
|
||||
(page: AssetStorePageState) => {
|
||||
if (hasAppliedSavedScrollPosition.current) {
|
||||
return;
|
||||
}
|
||||
@@ -205,7 +197,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
if (!scrollView) {
|
||||
return;
|
||||
}
|
||||
const scrollPosition = currentPage.scrollPosition;
|
||||
const scrollPosition = page.scrollPosition;
|
||||
if (scrollPosition) scrollView.scrollToPosition(scrollPosition);
|
||||
// If no saved scroll position, force scroll to 0 in case the displayed component
|
||||
// is the same as the previous page so the scroll is naturally kept between pages
|
||||
@@ -213,7 +205,27 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
else scrollView.scrollToPosition(0);
|
||||
hasAppliedSavedScrollPosition.current = true;
|
||||
},
|
||||
[getScrollView, currentPage]
|
||||
[getScrollView]
|
||||
);
|
||||
|
||||
React.useLayoutEffect(
|
||||
() => {
|
||||
// When going back to the homepage from a page where the asset filters
|
||||
// were open, we must first close the panel and then apply the scroll position.
|
||||
const applyEffect = async () => {
|
||||
if (isOnHomePage) {
|
||||
clearAllFilters();
|
||||
await setIsFiltersPanelOpen(false);
|
||||
}
|
||||
if (!isAssetDetailLoading.current) {
|
||||
applyBackScrollPosition(currentPage);
|
||||
}
|
||||
};
|
||||
applyEffect();
|
||||
},
|
||||
// clearAllFilters is not stable, so don't list it.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isOnHomePage, applyBackScrollPosition, currentPage]
|
||||
);
|
||||
|
||||
React.useImperativeHandle(ref, () => ({
|
||||
@@ -229,7 +241,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
const assetPackKind = identifyAssetPackKind({
|
||||
assetPack: openedAssetPack,
|
||||
publicAssetPacks,
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
});
|
||||
sendAssetOpened({
|
||||
id: assetShortHeader.id,
|
||||
@@ -240,14 +252,21 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
assetPackKind,
|
||||
});
|
||||
saveScrollPosition();
|
||||
navigationState.openDetailPage(assetShortHeader);
|
||||
const previousSearchText = searchText;
|
||||
// Don't reset search text when opening an asset as the search bar is not active.
|
||||
// This helps speeding up the navigation when going back to the results page.
|
||||
navigationState.openDetailPage({
|
||||
assetShortHeader,
|
||||
previousSearchText,
|
||||
});
|
||||
},
|
||||
[
|
||||
openedAssetPack,
|
||||
publicAssetPacks,
|
||||
privateAssetPacks,
|
||||
privateAssetPackListingDatas,
|
||||
saveScrollPosition,
|
||||
navigationState,
|
||||
searchText,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -267,13 +286,15 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
Window.openExternalURL(assetPack.externalWebLink);
|
||||
} else {
|
||||
saveScrollPosition();
|
||||
setSearchText('');
|
||||
navigationState.openPackPage(assetPack);
|
||||
const previousSearchText = searchText;
|
||||
setSearchText(''); // Reset search text when opening a pack.
|
||||
navigationState.openPackPage({ assetPack, previousSearchText });
|
||||
openFiltersPanelIfAppropriate();
|
||||
}
|
||||
},
|
||||
[
|
||||
navigationState,
|
||||
searchText,
|
||||
saveScrollPosition,
|
||||
setSearchText,
|
||||
openFiltersPanelIfAppropriate,
|
||||
@@ -296,17 +317,17 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
assetPackId: assetPackListingData.id,
|
||||
assetPackKind: 'private',
|
||||
});
|
||||
|
||||
setSearchText('');
|
||||
saveScrollPosition();
|
||||
navigationState.openPrivateAssetPackInformationPage(
|
||||
assetPackListingData
|
||||
);
|
||||
const previousSearchText = searchText;
|
||||
setSearchText(''); // Reset search text when opening a pack.
|
||||
navigationState.openPrivateAssetPackInformationPage({
|
||||
assetPack: assetPackListingData,
|
||||
previousSearchText,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// The user has received the pack, open it.
|
||||
setSearchText('');
|
||||
sendAssetPackOpened({
|
||||
assetPackName: assetPackListingData.name,
|
||||
assetPackId: assetPackListingData.id,
|
||||
@@ -315,7 +336,12 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
source: 'store-home',
|
||||
});
|
||||
saveScrollPosition();
|
||||
navigationState.openPackPage(receivedAssetPack);
|
||||
const previousSearchText = searchText;
|
||||
setSearchText(''); // Reset search text when opening a pack.
|
||||
navigationState.openPackPage({
|
||||
assetPack: receivedAssetPack,
|
||||
previousSearchText,
|
||||
});
|
||||
openFiltersPanelIfAppropriate();
|
||||
},
|
||||
[
|
||||
@@ -324,6 +350,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
navigationState,
|
||||
setSearchText,
|
||||
openFiltersPanelIfAppropriate,
|
||||
searchText,
|
||||
]
|
||||
);
|
||||
|
||||
@@ -353,9 +380,12 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
if (receivedAssetPack) {
|
||||
// The user has received the pack, close the pack information dialog, and open the pack in the search.
|
||||
setSearchText('');
|
||||
navigationState.clearPreviousPageFromHistory(); // Clear the previous page from history to avoid going back to the pack information page.
|
||||
navigationState.openPackPage({
|
||||
assetPack: receivedAssetPack,
|
||||
previousSearchText: '', // We were on a pack page.
|
||||
});
|
||||
openFiltersPanelIfAppropriate();
|
||||
saveScrollPosition();
|
||||
navigationState.openPackPage(receivedAssetPack);
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -384,13 +414,19 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
saveScrollPosition();
|
||||
setSearchText('');
|
||||
if (privateAssetPack) {
|
||||
navigationState.openPackPage(privateAssetPack);
|
||||
navigationState.openPackPage({
|
||||
assetPack: privateAssetPack,
|
||||
previousSearchText: '', // We were on an asset page.
|
||||
});
|
||||
} else if (publicAssetPack) {
|
||||
navigationState.openPackPage(publicAssetPack);
|
||||
navigationState.openPackPage({
|
||||
assetPack: publicAssetPack,
|
||||
previousSearchText: '', // We were on an asset page.
|
||||
});
|
||||
} else {
|
||||
navigationState.openTagPage(tag);
|
||||
}
|
||||
clearAllFilters(assetFiltersState);
|
||||
clearAllFilters();
|
||||
openFiltersPanelIfAppropriate();
|
||||
},
|
||||
[
|
||||
@@ -398,48 +434,12 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
receivedAssetPacks,
|
||||
publicAssetPacks,
|
||||
saveScrollPosition,
|
||||
assetFiltersState,
|
||||
clearAllFilters,
|
||||
navigationState,
|
||||
openFiltersPanelIfAppropriate,
|
||||
]
|
||||
);
|
||||
|
||||
const renderPrivateAssetPackAudioFilesDownloadButton = React.useCallback(
|
||||
(assetPack: PrivateAssetPack) => {
|
||||
return (
|
||||
<RaisedButton
|
||||
primary
|
||||
label={
|
||||
isAudioArchiveUrlLoading ? (
|
||||
<Trans>Loading...</Trans>
|
||||
) : (
|
||||
<Trans>Download pack sounds</Trans>
|
||||
)
|
||||
}
|
||||
icon={<Music />}
|
||||
disabled={isAudioArchiveUrlLoading}
|
||||
onClick={async () => {
|
||||
setIsAudioArchiveUrlLoading(true);
|
||||
const url = await getPrivateAssetPackAudioArchiveUrl(
|
||||
assetPack.id
|
||||
);
|
||||
setIsAudioArchiveUrlLoading(false);
|
||||
if (!url) {
|
||||
console.error(
|
||||
`Could not generate url for premium asset pack with name ${
|
||||
assetPack.name
|
||||
}`
|
||||
);
|
||||
return;
|
||||
}
|
||||
Window.openExternalURL(url);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
[getPrivateAssetPackAudioArchiveUrl, isAudioArchiveUrlLoading]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
if (shouldAutofocusSearchbar && searchBar.current) {
|
||||
@@ -449,38 +449,18 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
[shouldAutofocusSearchbar]
|
||||
);
|
||||
|
||||
React.useLayoutEffect(
|
||||
() => {
|
||||
// When going back to the homepage from a page where the asset filters
|
||||
// were open, we must first close the panel and then apply the scroll position.
|
||||
const applyEffect = async () => {
|
||||
if (isOnHomePage) {
|
||||
clearAllFilters(assetFiltersState);
|
||||
await setIsFiltersPanelOpen(false);
|
||||
}
|
||||
if (!isAssetDetailLoading.current) {
|
||||
applyBackScrollPosition();
|
||||
}
|
||||
};
|
||||
applyEffect();
|
||||
},
|
||||
// assetFiltersState is not stable, so don't list it.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[isOnHomePage, applyBackScrollPosition]
|
||||
);
|
||||
|
||||
const privateAssetPackFromSameCreator: ?Array<PrivateAssetPackListingData> = React.useMemo(
|
||||
() => {
|
||||
if (
|
||||
!openedPrivateAssetPackListingData ||
|
||||
!privateAssetPacks ||
|
||||
!privateAssetPackListingDatas ||
|
||||
!receivedAssetPacks
|
||||
)
|
||||
return null;
|
||||
|
||||
const receivedAssetPackIds = receivedAssetPacks.map(pack => pack.id);
|
||||
|
||||
return privateAssetPacks
|
||||
return privateAssetPackListingDatas
|
||||
.filter(
|
||||
pack =>
|
||||
pack.sellerId === openedPrivateAssetPackListingData.sellerId &&
|
||||
@@ -488,7 +468,11 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
)
|
||||
.sort((pack1, pack2) => pack1.name.localeCompare(pack2.name));
|
||||
},
|
||||
[openedPrivateAssetPackListingData, privateAssetPacks, receivedAssetPacks]
|
||||
[
|
||||
openedPrivateAssetPackListingData,
|
||||
privateAssetPackListingDatas,
|
||||
receivedAssetPacks,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -502,7 +486,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
setSearchText('');
|
||||
const page = navigationState.openHome();
|
||||
setScrollUpdateIsNeeded(page);
|
||||
clearAllFilters(assetFiltersState);
|
||||
clearAllFilters();
|
||||
setIsFiltersPanelOpen(false);
|
||||
}}
|
||||
size="small"
|
||||
@@ -554,9 +538,20 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
icon={<ChevronArrowLeft />}
|
||||
label={<Trans>Back</Trans>}
|
||||
primary={false}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
const page = navigationState.backToPreviousPage();
|
||||
setScrollUpdateIsNeeded(page);
|
||||
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 new Promise(resolve => setTimeout(resolve, 500));
|
||||
setScrollUpdateIsNeeded(page);
|
||||
applyBackScrollPosition(page); // We apply it manually, because the layout effect won't be called again.
|
||||
} else {
|
||||
setScrollUpdateIsNeeded(page);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Column>
|
||||
@@ -564,17 +559,16 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
openedPrivateAssetPackListingData ||
|
||||
filtersState.chosenCategory) && (
|
||||
<>
|
||||
<Column expand alignItems="center">
|
||||
<Text size="block-title" noMargin>
|
||||
{openedAssetPack
|
||||
? openedAssetPack.name
|
||||
: openedPrivateAssetPackListingData
|
||||
? openedPrivateAssetPackListingData.name
|
||||
: filtersState.chosenCategory
|
||||
? capitalize(filtersState.chosenCategory.node.name)
|
||||
: ''}
|
||||
</Text>
|
||||
</Column>
|
||||
{!openedAssetPack && !openedPrivateAssetPackListingData && (
|
||||
// Only show the category name if we're not on an asset pack page.
|
||||
<Column expand alignItems="center">
|
||||
<Text size="block-title" noMargin>
|
||||
{filtersState.chosenCategory
|
||||
? capitalize(filtersState.chosenCategory.node.name)
|
||||
: ''}
|
||||
</Text>
|
||||
</Column>
|
||||
)}
|
||||
<Column
|
||||
expand
|
||||
alignItems="flex-end"
|
||||
@@ -584,11 +578,11 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
{openedAssetPack &&
|
||||
openedAssetPack.content &&
|
||||
doesAssetPackContainAudio(openedAssetPack) &&
|
||||
!isAssetPackAudioOnly(openedAssetPack)
|
||||
? renderPrivateAssetPackAudioFilesDownloadButton(
|
||||
openedAssetPack
|
||||
)
|
||||
: null}
|
||||
!isAssetPackAudioOnly(openedAssetPack) ? (
|
||||
<PrivateAssetPackAudioFilesDownloadButton
|
||||
assetPack={openedAssetPack}
|
||||
/>
|
||||
) : null}
|
||||
</Column>
|
||||
</>
|
||||
)}
|
||||
@@ -614,12 +608,12 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
</AlertMessage>
|
||||
</PlaceholderError>
|
||||
) : publicAssetPacks &&
|
||||
privateAssetPacks &&
|
||||
privateAssetPackListingDatas &&
|
||||
assetPackRandomOrdering ? (
|
||||
<AssetsHome
|
||||
ref={assetsHome}
|
||||
publicAssetPacks={publicAssetPacks}
|
||||
privateAssetPacksListingData={privateAssetPacks}
|
||||
privateAssetPackListingDatas={privateAssetPackListingDatas}
|
||||
assetPackRandomOrdering={assetPackRandomOrdering}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
@@ -630,46 +624,17 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
<PlaceholderLoader />
|
||||
)
|
||||
) : isOnSearchResultPage ? (
|
||||
<BoxSearchResults
|
||||
ref={boxSearchResults}
|
||||
baseSize={128}
|
||||
onRetry={fetchAssetsAndFilters}
|
||||
error={error}
|
||||
searchItems={searchResults}
|
||||
spacing={8}
|
||||
renderSearchItem={(assetShortHeader, size) => (
|
||||
<AssetCard
|
||||
id={`asset-card-${assetShortHeader.name.replace(/\s/g, '-')}`}
|
||||
size={size}
|
||||
onOpenDetails={() => onOpenDetails(assetShortHeader)}
|
||||
assetShortHeader={assetShortHeader}
|
||||
/>
|
||||
)}
|
||||
noResultPlaceholder={
|
||||
openedAssetPack &&
|
||||
openedAssetPack.content &&
|
||||
isAssetPackAudioOnly(openedAssetPack) ? (
|
||||
<Column expand justifyContent="center" alignItems="center">
|
||||
<AlertMessage
|
||||
kind="info"
|
||||
renderRightButton={() =>
|
||||
renderPrivateAssetPackAudioFilesDownloadButton(
|
||||
openedAssetPack
|
||||
)
|
||||
}
|
||||
>
|
||||
<Trans>
|
||||
Download all the sounds of the asset pack in one click
|
||||
and use them in your project.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
</Column>
|
||||
) : (
|
||||
<NoResultPlaceholder
|
||||
onClear={() => clearAllFilters(assetFiltersState)}
|
||||
/>
|
||||
)
|
||||
<AssetsList
|
||||
publicAssetPacks={publicAssetPacksSearchResults}
|
||||
privateAssetPackListingDatas={
|
||||
privateAssetPackListingDatasSearchResults
|
||||
}
|
||||
assetShortHeaders={assetShortHeadersSearchResults}
|
||||
ref={assetsList}
|
||||
error={error}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
onPublicAssetPackSelection={selectPublicAssetPack}
|
||||
/>
|
||||
) : openedAssetShortHeader ? (
|
||||
<AssetDetails
|
||||
@@ -677,7 +642,8 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
onTagSelection={selectTag}
|
||||
assetShortHeader={openedAssetShortHeader}
|
||||
onOpenDetails={onOpenDetails}
|
||||
onAssetLoaded={applyBackScrollPosition}
|
||||
onAssetLoaded={() => applyBackScrollPosition(currentPage)}
|
||||
onPrivateAssetPackSelection={selectPrivateAssetPack}
|
||||
/>
|
||||
) : !!openedPrivateAssetPackListingData ? (
|
||||
<PrivateAssetPackInformationPage
|
||||
@@ -719,12 +685,7 @@ export const AssetStore = React.forwardRef<Props, AssetStoreInterface>(
|
||||
</Line>
|
||||
</Column>
|
||||
<Line justifyContent="space-between" alignItems="center">
|
||||
<AssetStoreFilterPanel
|
||||
assetFiltersState={assetFiltersState}
|
||||
onChoiceChange={() => {
|
||||
navigationState.openSearchResultPage();
|
||||
}}
|
||||
/>
|
||||
<AssetStoreFilterPanel />
|
||||
</Line>
|
||||
</Column>
|
||||
</ScrollView>
|
||||
|
@@ -12,6 +12,7 @@ export type EnumeratedBehaviorMetadata = {|
|
||||
description: string,
|
||||
previewIconUrl: string,
|
||||
category: string,
|
||||
tags: Array<string>,
|
||||
|};
|
||||
|
||||
export const enumerateBehaviorsMetadata = (
|
||||
@@ -48,6 +49,7 @@ export const enumerateBehaviorsMetadata = (
|
||||
previewIconUrl: behaviorMetadata.getIconFilename(),
|
||||
objectType: behaviorMetadata.getObjectType(),
|
||||
category: extension.getCategory(),
|
||||
tags: extension.getTags().toJSArray(),
|
||||
}));
|
||||
})
|
||||
);
|
||||
|
@@ -78,9 +78,7 @@ export default function NewBehaviorDialog({
|
||||
previewIconUrl: behavior.previewIconUrl,
|
||||
objectType: behavior.objectType,
|
||||
category: behavior.category,
|
||||
// PlatformExtension don't have tags so the information from EventsFunctionsExtension is lost.
|
||||
// TODO (tags): Add tags to PlatformExtension, handle them in MetadataDeclarationHelper and enumerateBehaviorsMetadata.
|
||||
tags: [],
|
||||
tags: behavior.tags,
|
||||
}));
|
||||
},
|
||||
[project, eventsFunctionsExtension]
|
||||
|
@@ -93,6 +93,7 @@ const styles = {
|
||||
overflowX: 'hidden',
|
||||
// Always show the vertical scrollbar to avoid rendering issues:
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'thin', // For Firefox, to avoid having a very large scrollbar.
|
||||
},
|
||||
tag: { marginRight: 2 },
|
||||
consoleTextArea: {
|
||||
|
@@ -24,6 +24,7 @@ const styles = {
|
||||
},
|
||||
list: {
|
||||
overflowY: 'scroll',
|
||||
scrollbarWidth: 'thin', // For Firefox, to avoid having a very large scrollbar.
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
|
@@ -341,6 +341,44 @@ const getEnumeratedEffectMetadata = (
|
||||
);
|
||||
};
|
||||
|
||||
export const getEffects2DCount = (
|
||||
platform: gdPlatform,
|
||||
effectsContainer: gdEffectsContainer
|
||||
) => {
|
||||
const effectCount = effectsContainer.getEffectsCount();
|
||||
let effect2DCount = 0;
|
||||
for (let i = 0; i < effectCount; i++) {
|
||||
const effect: gdEffect = effectsContainer.getEffectAt(i);
|
||||
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
|
||||
platform,
|
||||
effect.getEffectType()
|
||||
);
|
||||
if (!effectMetadata || !effectMetadata.isMarkedAsOnlyWorkingFor3D()) {
|
||||
effect2DCount++;
|
||||
}
|
||||
}
|
||||
return effect2DCount;
|
||||
};
|
||||
|
||||
export const getEffects3DCount = (
|
||||
platform: gdPlatform,
|
||||
effectsContainer: gdEffectsContainer
|
||||
) => {
|
||||
const effectCount = effectsContainer.getEffectsCount();
|
||||
let effect3DCount = 0;
|
||||
for (let i = 0; i < effectCount; i++) {
|
||||
const effect: gdEffect = effectsContainer.getEffectAt(i);
|
||||
const effectMetadata = gd.MetadataProvider.getEffectMetadata(
|
||||
platform,
|
||||
effect.getEffectType()
|
||||
);
|
||||
if (!effectMetadata || !effectMetadata.isMarkedAsOnlyWorkingFor2D()) {
|
||||
effect3DCount++;
|
||||
}
|
||||
}
|
||||
return effect3DCount;
|
||||
};
|
||||
|
||||
/**
|
||||
* Display a list of effects and allow to add/remove/edit them.
|
||||
*
|
||||
@@ -657,28 +695,21 @@ export default function EffectsList(props: Props) {
|
||||
const duplicatedUniqueEffectMetadata = getDuplicatedUniqueEffectMetadata();
|
||||
|
||||
// Count the number of effects to hide titles of empty sections.
|
||||
let effect2DCount = 0;
|
||||
let effect3DCount = 0;
|
||||
for (let i = 0; i < effectsContainer.getEffectsCount(); i++) {
|
||||
const effect: gdEffect = effectsContainer.getEffectAt(i);
|
||||
const effectMetadata = getEnumeratedEffectMetadata(
|
||||
allEffectMetadata,
|
||||
effect.getEffectType()
|
||||
);
|
||||
|
||||
if (!effectMetadata || !effectMetadata.isMarkedAsOnlyWorkingFor2D) {
|
||||
effect3DCount++;
|
||||
}
|
||||
if (!effectMetadata || !effectMetadata.isMarkedAsOnlyWorkingFor3D) {
|
||||
effect2DCount++;
|
||||
}
|
||||
}
|
||||
const platform = project.getCurrentPlatform();
|
||||
const effects2DCount = getEffects2DCount(platform, effectsContainer);
|
||||
const effects3DCount = getEffects3DCount(platform, effectsContainer);
|
||||
const visibleEffectsCount =
|
||||
props.layerRenderingType === '2d'
|
||||
? effects2DCount
|
||||
: props.layerRenderingType === '3d'
|
||||
? effects3DCount
|
||||
: effectsContainer.getEffectsCount();
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column noMargin expand useFullHeight>
|
||||
{effectsContainer.getEffectsCount() !== 0 ? (
|
||||
{visibleEffectsCount !== 0 ? (
|
||||
<React.Fragment>
|
||||
<ScrollView ref={scrollView}>
|
||||
{duplicatedUniqueEffectMetadata && (
|
||||
@@ -711,7 +742,7 @@ export default function EffectsList(props: Props) {
|
||||
</Column>
|
||||
</Line>
|
||||
)}
|
||||
{props.layerRenderingType !== '2d' && effect3DCount > 0 && (
|
||||
{props.layerRenderingType !== '2d' && effects3DCount > 0 && (
|
||||
<Column noMargin expand>
|
||||
{props.layerRenderingType !== '3d' && (
|
||||
<Column noMargin>
|
||||
@@ -802,7 +833,7 @@ export default function EffectsList(props: Props) {
|
||||
</Line>
|
||||
</Column>
|
||||
)}
|
||||
{props.layerRenderingType !== '3d' && effect2DCount > 0 && (
|
||||
{props.layerRenderingType !== '3d' && effects2DCount > 0 && (
|
||||
<Column noMargin expand>
|
||||
{props.layerRenderingType !== '2d' && (
|
||||
<Column noMargin>
|
||||
|
@@ -13,7 +13,6 @@ import RaisedButton from '../UI/RaisedButton';
|
||||
import IconButton from '../UI/IconButton';
|
||||
import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import InlineCheckbox from '../UI/InlineCheckbox';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
@@ -88,46 +87,27 @@ type Props = {|
|
||||
behaviorObjectType?: string,
|
||||
|};
|
||||
|
||||
const validatePropertyName = (
|
||||
const getValidatedPropertyName = (
|
||||
i18n: I18nType,
|
||||
properties: gdNamedPropertyDescriptorsList,
|
||||
newName: string
|
||||
) => {
|
||||
if (!newName) {
|
||||
showWarningBox(i18n._(t`The name of a property cannot be empty.`), {
|
||||
delayToNextTick: true,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (newName === 'name' || newName === 'type') {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`The name of a property cannot be "name" or "type", as they are used by GDevelop internally.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (properties.has(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is already used by another property. Choose a unique name for each property.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
): string => {
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
properties.has(tentativeNewName) ||
|
||||
// The name of a property cannot be "name" or "type", as they are used by GDevelop internally.
|
||||
(tentativeNewName === 'name' || tentativeNewName === 'type')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
@@ -426,21 +406,18 @@ export default function EventsBasedBehaviorPropertiesEditor(props: Props) {
|
||||
onChange={newName => {
|
||||
if (newName === property.getName())
|
||||
return;
|
||||
if (
|
||||
!validatePropertyName(
|
||||
i18n,
|
||||
properties,
|
||||
newName
|
||||
)
|
||||
)
|
||||
return;
|
||||
|
||||
props.onRenameProperty(
|
||||
property.getName(),
|
||||
const validatedNewName = getValidatedPropertyName(
|
||||
i18n,
|
||||
properties,
|
||||
newName
|
||||
);
|
||||
props.onRenameProperty(
|
||||
property.getName(),
|
||||
validatedNewName
|
||||
);
|
||||
property.setName(validatedNewName);
|
||||
|
||||
property.setName(newName);
|
||||
forceUpdate();
|
||||
props.onPropertiesUpdated &&
|
||||
props.onPropertiesUpdated();
|
||||
|
@@ -8,7 +8,6 @@ import * as React from 'react';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
|
||||
import SearchBar from '../UI/SearchBar';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import Background from '../UI/Background';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import {
|
||||
@@ -154,26 +153,18 @@ export default class EventsBasedBehaviorsList extends React.Component<
|
||||
};
|
||||
|
||||
_rename = (eventsBasedBehavior: gdEventsBasedBehavior, newName: string) => {
|
||||
const { eventsBasedBehaviorsList } = this.props;
|
||||
this.setState({
|
||||
renamedEventsBasedBehavior: null,
|
||||
});
|
||||
|
||||
if (eventsBasedBehavior.getName() === newName) return;
|
||||
|
||||
if (eventsBasedBehaviorsList.has(newName)) {
|
||||
showWarningBox('Another behavior with this name already exists.', {
|
||||
delayToNextTick: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onRenameEventsBasedBehavior(
|
||||
eventsBasedBehavior,
|
||||
newName,
|
||||
doRename => {
|
||||
if (!doRename) return;
|
||||
eventsBasedBehavior.setName(newName);
|
||||
|
||||
this._onEventsBasedBehaviorModified();
|
||||
this.props.onEventsBasedBehaviorRenamed(eventsBasedBehavior);
|
||||
}
|
||||
|
@@ -7,12 +7,12 @@ import * as React from 'react';
|
||||
import { Line } from '../UI/Grid';
|
||||
import ObjectsList, { type ObjectsListInterface } from '../ObjectsList';
|
||||
import ObjectsRenderingService from '../ObjectsRendering/ObjectsRenderingService';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import type { ObjectWithContext } from '../ObjectsList/EnumerateObjects';
|
||||
import Window from '../Utils/Window';
|
||||
import ObjectEditorDialog from '../ObjectEditor/ObjectEditorDialog';
|
||||
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
|
||||
import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -72,31 +72,26 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
done(true);
|
||||
};
|
||||
|
||||
_canObjectOrGroupUseNewName = (newName: string, i18n: I18nType) => {
|
||||
_getValidatedObjectOrGroupName = (newName: string, i18n: I18nType) => {
|
||||
const { eventsBasedObject } = this.props;
|
||||
|
||||
if (
|
||||
eventsBasedObject.hasObjectNamed(newName) ||
|
||||
eventsBasedObject.getObjectGroups().has(newName)
|
||||
) {
|
||||
showWarningBox(
|
||||
i18n._(t`Another object or group with this name already exists.`),
|
||||
{
|
||||
delayToNextTick: true,
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
eventsBasedObject.hasObjectNamed(tentativeNewName) ||
|
||||
eventsBasedObject.getObjectGroups().has(tentativeNewName) ||
|
||||
// TODO EBO Use a constant instead a hard coded value "Object".
|
||||
tentativeNewName === 'Object'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
);
|
||||
return false;
|
||||
} else if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
_onRenameObjectStart = (objectWithContext: ?ObjectWithContext) => {
|
||||
@@ -130,19 +125,6 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
done: boolean => void,
|
||||
i18n: I18nType
|
||||
) => {
|
||||
// TODO EBO Use a constant instead a hard coded value "Object".
|
||||
if (newName === 'Object') {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`"Object" is a reserved name, used for the parent object in the events (actions, conditions, expressions...). Please choose another name.`
|
||||
),
|
||||
{
|
||||
delayToNextTick: true,
|
||||
}
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { object } = objectWithContext;
|
||||
const { project, globalObjectsContainer, eventsBasedObject } = this.props;
|
||||
|
||||
@@ -253,8 +235,8 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
// Don't allow export as there is no assets.
|
||||
onExportObject={() => {}}
|
||||
onDeleteObject={this._onDeleteObject(i18n)}
|
||||
canRenameObject={newName =>
|
||||
this._canObjectOrGroupUseNewName(newName, i18n)
|
||||
getValidatedObjectOrGroupName={newName =>
|
||||
this._getValidatedObjectOrGroupName(newName, i18n)
|
||||
}
|
||||
// Nothing special to do.
|
||||
onObjectCreated={() => {}}
|
||||
@@ -318,8 +300,8 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
onCancel={() => {
|
||||
this.editObject(null);
|
||||
}}
|
||||
canRenameObject={newName =>
|
||||
this._canObjectOrGroupUseNewName(newName, i18n)
|
||||
getValidatedObjectOrGroupName={newName =>
|
||||
this._getValidatedObjectOrGroupName(newName, i18n)
|
||||
}
|
||||
onRename={newName => {
|
||||
this._onRenameEditedObject(newName, i18n);
|
||||
|
@@ -14,7 +14,6 @@ import EmptyMessage from '../UI/EmptyMessage';
|
||||
import ElementWithMenu from '../UI/Menu/ElementWithMenu';
|
||||
import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import MiniToolbar from '../UI/MiniToolbar';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import InlineCheckbox from '../UI/InlineCheckbox';
|
||||
import { ResponsiveLineStackLayout, ColumnStackLayout } from '../UI/Layout';
|
||||
@@ -43,46 +42,27 @@ const styles = {
|
||||
},
|
||||
};
|
||||
|
||||
const validatePropertyName = (
|
||||
const getValidatedPropertyName = (
|
||||
i18n: I18nType,
|
||||
properties: gdNamedPropertyDescriptorsList,
|
||||
newName: string
|
||||
) => {
|
||||
if (!newName) {
|
||||
showWarningBox(i18n._(t`The name of a property cannot be empty.`), {
|
||||
delayToNextTick: true,
|
||||
});
|
||||
return false;
|
||||
}
|
||||
if (newName === 'name' || newName === 'type') {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`The name of a property cannot be "name" or "type", as they are used by GDevelop internally.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (properties.has(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is already used by another property. Choose a unique name for each property.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
): string => {
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
properties.has(tentativeNewName) ||
|
||||
// The name of a property cannot be "name" or "type", as they are used by GDevelop internally.
|
||||
(tentativeNewName === 'name' || tentativeNewName === 'type')
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
const getExtraInfoArray = (property: gdNamedPropertyDescriptor) => {
|
||||
@@ -174,17 +154,18 @@ export default class EventsBasedObjectPropertiesEditor extends React.Component<
|
||||
value={property.getName()}
|
||||
onChange={newName => {
|
||||
if (newName === property.getName()) return;
|
||||
if (
|
||||
!validatePropertyName(i18n, properties, newName)
|
||||
)
|
||||
return;
|
||||
|
||||
this.props.onRenameProperty(
|
||||
property.getName(),
|
||||
const validatedNewName = getValidatedPropertyName(
|
||||
i18n,
|
||||
properties,
|
||||
newName
|
||||
);
|
||||
this.props.onRenameProperty(
|
||||
property.getName(),
|
||||
validatedNewName
|
||||
);
|
||||
property.setName(validatedNewName);
|
||||
|
||||
property.setName(newName);
|
||||
this.forceUpdate();
|
||||
this.props.onPropertiesUpdated &&
|
||||
this.props.onPropertiesUpdated();
|
||||
|
@@ -9,7 +9,6 @@ import SemiControlledTextField from '../UI/SemiControlledTextField';
|
||||
import DismissableAlertMessage from '../UI/DismissableAlertMessage';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import useForceUpdate from '../Utils/UseForceUpdate';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
@@ -78,18 +77,9 @@ export default function EventsBasedObjectEditor({ eventsBasedObject }: Props) {
|
||||
value={
|
||||
eventsBasedObject.getDefaultName() || eventsBasedObject.getName()
|
||||
}
|
||||
onChange={text => {
|
||||
if (gd.Project.validateName(text)) {
|
||||
eventsBasedObject.setDefaultName(text);
|
||||
forceUpdate();
|
||||
} else {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
}
|
||||
onChange={newName => {
|
||||
eventsBasedObject.setDefaultName(gd.Project.getSafeName(newName));
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
|
@@ -8,7 +8,6 @@ import * as React from 'react';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
|
||||
import SearchBar from '../UI/SearchBar';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import Background from '../UI/Background';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import {
|
||||
@@ -116,26 +115,18 @@ export default class EventsBasedObjectsList extends React.Component<
|
||||
};
|
||||
|
||||
_rename = (eventsBasedObject: gdEventsBasedObject, newName: string) => {
|
||||
const { eventsBasedObjectsList } = this.props;
|
||||
this.setState({
|
||||
renamedEventsBasedObject: null,
|
||||
});
|
||||
|
||||
if (eventsBasedObject.getName() === newName) return;
|
||||
|
||||
if (eventsBasedObjectsList.has(newName)) {
|
||||
showWarningBox('Another object with this name already exists.', {
|
||||
delayToNextTick: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onRenameEventsBasedObject(
|
||||
eventsBasedObject,
|
||||
newName,
|
||||
doRename => {
|
||||
if (!doRename) return;
|
||||
eventsBasedObject.setName(newName);
|
||||
|
||||
this._onEventsBasedObjectModified();
|
||||
this.props.onEventsBasedObjectRenamed(eventsBasedObject);
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ import ElementWithMenu from '../../UI/Menu/ElementWithMenu';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import SemiControlledTextField from '../../UI/SemiControlledTextField';
|
||||
import MiniToolbar, { MiniToolbarText } from '../../UI/MiniToolbar';
|
||||
import { showWarningBox } from '../../UI/Messages/MessageBox';
|
||||
import { ParametersIndexOffsets } from '../../EventsFunctionsExtensionsLoader';
|
||||
import DismissableAlertMessage from '../../UI/DismissableAlertMessage';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
@@ -22,6 +21,7 @@ import newNameGenerator from '../../Utils/NewNameGenerator';
|
||||
import ValueTypeEditor from './ValueTypeEditor';
|
||||
import ThreeDotsMenu from '../../UI/CustomSvgIcons/ThreeDotsMenu';
|
||||
import Add from '../../UI/CustomSvgIcons/Add';
|
||||
import useForceUpdate from '../../Utils/UseForceUpdate';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -56,449 +56,430 @@ type Props = {|
|
||||
) => void,
|
||||
|};
|
||||
|
||||
type State = {|
|
||||
longDescriptionShownIndexes: { [number]: boolean },
|
||||
|};
|
||||
|
||||
const styles = {
|
||||
parametersContainer: {
|
||||
flex: 1,
|
||||
},
|
||||
};
|
||||
|
||||
const validateParameterName = (i18n: I18nType, newName: string) => {
|
||||
if (!newName) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`The name of a parameter can not be empty. Enter a name for the parameter or you won't be able to use it.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
export const EventsFunctionParametersEditor = ({
|
||||
project,
|
||||
eventsFunction,
|
||||
eventsBasedBehavior,
|
||||
eventsBasedObject,
|
||||
eventsFunctionsContainer,
|
||||
onParametersUpdated,
|
||||
helpPagePath,
|
||||
freezeParameters,
|
||||
onMoveFreeEventsParameter,
|
||||
onMoveBehaviorEventsParameter,
|
||||
onMoveObjectEventsParameter,
|
||||
}: Props) => {
|
||||
const [
|
||||
longDescriptionShownIndexes,
|
||||
setLongDescriptionShownIndexes,
|
||||
] = React.useState<{ [number]: boolean }>({});
|
||||
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
return true;
|
||||
};
|
||||
const addParameterAt = React.useCallback(
|
||||
(index: number) => {
|
||||
const parameters = eventsFunction.getParameters();
|
||||
const existingParameterNames = mapVector(parameters, parameterMetadata =>
|
||||
parameterMetadata.getName()
|
||||
);
|
||||
const newParameter = new gd.ParameterMetadata();
|
||||
newParameter.setType('objectList');
|
||||
const newName = newNameGenerator('Parameter', name =>
|
||||
existingParameterNames.includes(name)
|
||||
);
|
||||
newParameter.setName(newName);
|
||||
parameters.insertIntoVectorParameterMetadata(index, newParameter);
|
||||
newParameter.delete();
|
||||
forceUpdate();
|
||||
onParametersUpdated();
|
||||
},
|
||||
[eventsFunction, forceUpdate, onParametersUpdated]
|
||||
);
|
||||
|
||||
export default class EventsFunctionParametersEditor extends React.Component<
|
||||
Props,
|
||||
State
|
||||
> {
|
||||
state = {
|
||||
longDescriptionShownIndexes: {},
|
||||
};
|
||||
const addParameter = React.useCallback(
|
||||
() => {
|
||||
const parameters = eventsFunction.getParameters();
|
||||
addParameterAt(parameters.size());
|
||||
},
|
||||
[addParameterAt, eventsFunction]
|
||||
);
|
||||
|
||||
_addParameter = () => {
|
||||
const { eventsFunction } = this.props;
|
||||
const parameters = eventsFunction.getParameters();
|
||||
this._addParameterAt(parameters.size());
|
||||
};
|
||||
const removeParameter = React.useCallback(
|
||||
(index: number) => {
|
||||
const parameters = eventsFunction.getParameters();
|
||||
|
||||
_addParameterAt = (index: number) => {
|
||||
const { eventsFunction } = this.props;
|
||||
const parameters = eventsFunction.getParameters();
|
||||
const existingParameterNames = mapVector(parameters, parameterMetadata =>
|
||||
parameterMetadata.getName()
|
||||
);
|
||||
gd.removeFromVectorParameterMetadata(parameters, index);
|
||||
forceUpdate();
|
||||
onParametersUpdated();
|
||||
},
|
||||
[eventsFunction, forceUpdate, onParametersUpdated]
|
||||
);
|
||||
|
||||
const newParameter = new gd.ParameterMetadata();
|
||||
newParameter.setType('objectList');
|
||||
const newName = newNameGenerator('Parameter', name =>
|
||||
existingParameterNames.includes(name)
|
||||
);
|
||||
newParameter.setName(newName);
|
||||
parameters.insertIntoVectorParameterMetadata(index, newParameter);
|
||||
newParameter.delete();
|
||||
this.forceUpdate();
|
||||
this.props.onParametersUpdated();
|
||||
};
|
||||
|
||||
_removeParameter = (index: number) => {
|
||||
const { eventsFunction } = this.props;
|
||||
const parameters = eventsFunction.getParameters();
|
||||
|
||||
gd.removeFromVectorParameterMetadata(parameters, index);
|
||||
this.forceUpdate();
|
||||
this.props.onParametersUpdated();
|
||||
};
|
||||
|
||||
_addLongDescription = (index: number) => {
|
||||
// Show the long description field
|
||||
this.setState(state => ({
|
||||
longDescriptionShownIndexes: {
|
||||
...state.longDescriptionShownIndexes,
|
||||
const addLongDescription = React.useCallback(
|
||||
(index: number) => {
|
||||
// Show the long description field
|
||||
setLongDescriptionShownIndexes({
|
||||
...longDescriptionShownIndexes,
|
||||
[index]: true,
|
||||
},
|
||||
}));
|
||||
};
|
||||
});
|
||||
},
|
||||
[longDescriptionShownIndexes]
|
||||
);
|
||||
|
||||
_removeLongDescription = (index: number) => {
|
||||
const { eventsFunction } = this.props;
|
||||
const parameters = eventsFunction.getParameters();
|
||||
const parameter = parameters.at(index);
|
||||
const removeLongDescription = React.useCallback(
|
||||
(index: number) => {
|
||||
const parameters = eventsFunction.getParameters();
|
||||
const parameter = parameters.at(index);
|
||||
|
||||
// Reset the long description and hide the field
|
||||
parameter.setLongDescription('');
|
||||
this.setState(state => ({
|
||||
longDescriptionShownIndexes: {
|
||||
...state.longDescriptionShownIndexes,
|
||||
// Reset the long description and hide the field
|
||||
parameter.setLongDescription('');
|
||||
setLongDescriptionShownIndexes({
|
||||
...longDescriptionShownIndexes,
|
||||
[index]: false,
|
||||
},
|
||||
}));
|
||||
};
|
||||
});
|
||||
},
|
||||
[eventsFunction, longDescriptionShownIndexes]
|
||||
);
|
||||
|
||||
_moveParameters = (oldIndex: number, newIndex: number) => {
|
||||
const {
|
||||
eventsFunction,
|
||||
const moveParameters = React.useCallback(
|
||||
(oldIndex: number, newIndex: number) => {
|
||||
const parameters = eventsFunction.getParameters();
|
||||
|
||||
if (eventsBasedBehavior) {
|
||||
if (onMoveBehaviorEventsParameter)
|
||||
onMoveBehaviorEventsParameter(
|
||||
eventsBasedBehavior,
|
||||
eventsFunction,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
isDone => {
|
||||
if (!isDone) return;
|
||||
gd.swapInVectorParameterMetadata(parameters, oldIndex, newIndex);
|
||||
forceUpdate();
|
||||
onParametersUpdated();
|
||||
}
|
||||
);
|
||||
} else if (eventsBasedObject) {
|
||||
if (onMoveObjectEventsParameter)
|
||||
onMoveObjectEventsParameter(
|
||||
eventsBasedObject,
|
||||
eventsFunction,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
isDone => {
|
||||
if (!isDone) return;
|
||||
gd.swapInVectorParameterMetadata(parameters, oldIndex, newIndex);
|
||||
forceUpdate();
|
||||
onParametersUpdated();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (onMoveFreeEventsParameter)
|
||||
onMoveFreeEventsParameter(
|
||||
eventsFunction,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
isDone => {
|
||||
if (!isDone) return;
|
||||
gd.swapInVectorParameterMetadata(parameters, oldIndex, newIndex);
|
||||
forceUpdate();
|
||||
onParametersUpdated();
|
||||
}
|
||||
);
|
||||
}
|
||||
},
|
||||
[
|
||||
eventsBasedBehavior,
|
||||
eventsBasedObject,
|
||||
} = this.props;
|
||||
const parameters = eventsFunction.getParameters();
|
||||
|
||||
if (eventsBasedBehavior) {
|
||||
if (this.props.onMoveBehaviorEventsParameter)
|
||||
this.props.onMoveBehaviorEventsParameter(
|
||||
eventsBasedBehavior,
|
||||
eventsFunction,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
isDone => {
|
||||
if (!isDone) return;
|
||||
gd.swapInVectorParameterMetadata(parameters, oldIndex, newIndex);
|
||||
this.forceUpdate();
|
||||
this.props.onParametersUpdated();
|
||||
}
|
||||
);
|
||||
} else if (eventsBasedObject) {
|
||||
if (this.props.onMoveObjectEventsParameter)
|
||||
this.props.onMoveObjectEventsParameter(
|
||||
eventsBasedObject,
|
||||
eventsFunction,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
isDone => {
|
||||
if (!isDone) return;
|
||||
gd.swapInVectorParameterMetadata(parameters, oldIndex, newIndex);
|
||||
this.forceUpdate();
|
||||
this.props.onParametersUpdated();
|
||||
}
|
||||
);
|
||||
} else {
|
||||
if (this.props.onMoveFreeEventsParameter)
|
||||
this.props.onMoveFreeEventsParameter(
|
||||
eventsFunction,
|
||||
oldIndex,
|
||||
newIndex,
|
||||
isDone => {
|
||||
if (!isDone) return;
|
||||
gd.swapInVectorParameterMetadata(parameters, oldIndex, newIndex);
|
||||
this.forceUpdate();
|
||||
this.props.onParametersUpdated();
|
||||
}
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
_setStringSelectorExtraInfo = (parameter: gdParameterMetadata) => {
|
||||
return (newExtraInfo: Array<string>) => {
|
||||
parameter.setExtraInfo(JSON.stringify(newExtraInfo));
|
||||
this.forceUpdate();
|
||||
};
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
project,
|
||||
eventsFunction,
|
||||
eventsBasedBehavior,
|
||||
eventsBasedObject,
|
||||
eventsFunctionsContainer,
|
||||
freezeParameters,
|
||||
helpPagePath,
|
||||
} = this.props;
|
||||
forceUpdate,
|
||||
onMoveBehaviorEventsParameter,
|
||||
onMoveFreeEventsParameter,
|
||||
onMoveObjectEventsParameter,
|
||||
onParametersUpdated,
|
||||
]
|
||||
);
|
||||
|
||||
const isABehaviorLifecycleEventsFunction =
|
||||
!!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isBehaviorLifecycleEventsFunction(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
if (isABehaviorLifecycleEventsFunction) {
|
||||
return (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This is a "lifecycle method". It will be called automatically by the
|
||||
game engine and has two parameters: "Object" (the object the
|
||||
behavior is acting on) and "Behavior" (the behavior itself).
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
);
|
||||
}
|
||||
const isAnExtensionLifecycleEventsFunction =
|
||||
!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isExtensionLifecycleEventsFunction(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
if (isAnExtensionLifecycleEventsFunction) {
|
||||
return (
|
||||
<Column noMargin>
|
||||
<DismissableAlertMessage
|
||||
kind="info"
|
||||
identifier="lifecycle-events-function-included-only-if-extension-used"
|
||||
>
|
||||
<Trans>
|
||||
For the lifecycle functions to be executed, you need the extension
|
||||
to be used in the game, either by having at least one action,
|
||||
condition or expression used, or a behavior of the extension added
|
||||
to an object. Otherwise, the extension won't be included in the
|
||||
game.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This is a "lifecycle function". It will be called automatically by
|
||||
the game engine. It has no parameters. Only global objects can be
|
||||
used as the events will be run for all scenes in your game.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
const parameters =
|
||||
eventsFunctionsContainer &&
|
||||
eventsFunction.getFunctionType() === gd.EventsFunction.ActionWithOperator
|
||||
? eventsFunction.getParametersForEvents(eventsFunctionsContainer)
|
||||
: eventsFunction.getParameters();
|
||||
|
||||
const parameters =
|
||||
eventsFunctionsContainer &&
|
||||
eventsFunction.getFunctionType() === gd.EventsFunction.ActionWithOperator
|
||||
? eventsFunction.getParametersForEvents(eventsFunctionsContainer)
|
||||
: eventsFunction.getParameters();
|
||||
|
||||
const firstParameterIndex = eventsBasedBehavior
|
||||
? 2
|
||||
: eventsBasedObject
|
||||
const firstParameterIndex = eventsBasedBehavior
|
||||
? 2
|
||||
: eventsBasedObject
|
||||
? 1
|
||||
: 0;
|
||||
const isParameterDisabled = index => {
|
||||
return (
|
||||
eventsFunction.getFunctionType() ===
|
||||
gd.EventsFunction.ActionWithOperator ||
|
||||
freezeParameters ||
|
||||
index < firstParameterIndex
|
||||
);
|
||||
};
|
||||
// The first two parameters of a behavior method should not be changed at all,
|
||||
// so we even hide their description and type to avoid cluttering the interface.
|
||||
// Same thing for an object which has mandatory Object parameter.
|
||||
const typeShownFirstIndex = firstParameterIndex;
|
||||
const isParameterTypeShown = index => {
|
||||
return index >= typeShownFirstIndex;
|
||||
};
|
||||
// The first two parameters of a behavior method should not be changed at all,
|
||||
// so we even hide their description and type to avoid cluttering the interface.
|
||||
// Same thing for an object which has mandatory Object parameter.
|
||||
const labelShownFirstIndex =
|
||||
firstParameterIndex +
|
||||
(eventsFunction.getFunctionType() === gd.EventsFunction.ActionWithOperator
|
||||
? 1
|
||||
: 0;
|
||||
const isParameterDisabled = index => {
|
||||
return (
|
||||
eventsFunction.getFunctionType() ===
|
||||
gd.EventsFunction.ActionWithOperator ||
|
||||
freezeParameters ||
|
||||
index < firstParameterIndex
|
||||
);
|
||||
};
|
||||
// The first two parameters of a behavior method should not be changed at all,
|
||||
// so we even hide their description and type to avoid cluttering the interface.
|
||||
// Same thing for an object which has mandatory Object parameter.
|
||||
const typeShownFirstIndex = firstParameterIndex;
|
||||
const isParameterTypeShown = index => {
|
||||
return index >= typeShownFirstIndex;
|
||||
};
|
||||
// The first two parameters of a behavior method should not be changed at all,
|
||||
// so we even hide their description and type to avoid cluttering the interface.
|
||||
// Same thing for an object which has mandatory Object parameter.
|
||||
const labelShownFirstIndex =
|
||||
firstParameterIndex +
|
||||
(eventsFunction.getFunctionType() === gd.EventsFunction.ActionWithOperator
|
||||
? 1
|
||||
: 0);
|
||||
const isParameterDescriptionShown = index => {
|
||||
: 0);
|
||||
|
||||
const isParameterDescriptionShown = React.useCallback(
|
||||
index => {
|
||||
return index >= labelShownFirstIndex;
|
||||
};
|
||||
const isParameterLongDescriptionShown = (parameter, index): boolean => {
|
||||
},
|
||||
[labelShownFirstIndex]
|
||||
);
|
||||
|
||||
const isParameterLongDescriptionShown = React.useCallback(
|
||||
(parameter, index): boolean => {
|
||||
return (
|
||||
isParameterDescriptionShown(index) &&
|
||||
(!!parameter.getLongDescription() ||
|
||||
!!this.state.longDescriptionShownIndexes[index])
|
||||
!!longDescriptionShownIndexes[index])
|
||||
);
|
||||
};
|
||||
const parametersIndexOffset = eventsBasedBehavior
|
||||
? ParametersIndexOffsets.BehaviorFunction
|
||||
: eventsBasedObject
|
||||
? ParametersIndexOffsets.ObjectFunction
|
||||
: ParametersIndexOffsets.FreeFunction;
|
||||
},
|
||||
[isParameterDescriptionShown, longDescriptionShownIndexes]
|
||||
);
|
||||
|
||||
const parametersIndexOffset = eventsBasedBehavior
|
||||
? ParametersIndexOffsets.BehaviorFunction
|
||||
: eventsBasedObject
|
||||
? ParametersIndexOffsets.ObjectFunction
|
||||
: ParametersIndexOffsets.FreeFunction;
|
||||
|
||||
const isABehaviorLifecycleEventsFunction =
|
||||
!!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isBehaviorLifecycleEventsFunction(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
if (isABehaviorLifecycleEventsFunction) {
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column noMargin expand>
|
||||
<Line noMargin>
|
||||
<div style={styles.parametersContainer}>
|
||||
{mapVector(
|
||||
parameters,
|
||||
(parameter: gdParameterMetadata, i: number) => (
|
||||
<React.Fragment key={i}>
|
||||
<MiniToolbar noPadding>
|
||||
<MiniToolbarText firstChild>
|
||||
<Trans>Parameter #{i + parametersIndexOffset}:</Trans>
|
||||
</MiniToolbarText>
|
||||
<Column expand noMargin>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
margin="none"
|
||||
translatableHintText={t`Enter the parameter name (mandatory)`}
|
||||
value={parameter.getName()}
|
||||
onChange={text => {
|
||||
if (!validateParameterName(i18n, text)) return;
|
||||
|
||||
parameter.setName(text);
|
||||
this.forceUpdate();
|
||||
this.props.onParametersUpdated();
|
||||
}}
|
||||
disabled={isParameterDisabled(i)}
|
||||
fullWidth
|
||||
/>
|
||||
</Column>
|
||||
<ElementWithMenu
|
||||
element={
|
||||
<IconButton>
|
||||
<ThreeDotsMenu />
|
||||
</IconButton>
|
||||
}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
click: () => this._removeParameter(i),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Add a parameter below`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
click: () => this._addParameterAt(i + 1),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Add a Long Description`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
visible: !isParameterLongDescriptionShown(
|
||||
parameter,
|
||||
i
|
||||
),
|
||||
click: () => this._addLongDescription(i),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Remove the Long Description`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
visible: isParameterLongDescriptionShown(
|
||||
parameter,
|
||||
i
|
||||
),
|
||||
click: () => this._removeLongDescription(i),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Move up`),
|
||||
click: () => this._moveParameters(i, i - 1),
|
||||
enabled:
|
||||
!isParameterDisabled(i) &&
|
||||
i - 1 >= 0 &&
|
||||
!isParameterDisabled(i - 1),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Move down`),
|
||||
click: () => this._moveParameters(i, i + 1),
|
||||
enabled:
|
||||
!isParameterDisabled(i) &&
|
||||
i + 1 < parameters.size() &&
|
||||
!isParameterDisabled(i + 1),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MiniToolbar>
|
||||
<Line>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<ValueTypeEditor
|
||||
project={project}
|
||||
valueTypeMetadata={parameter.getValueTypeMetadata()}
|
||||
disabled={isParameterDisabled(i)}
|
||||
isTypeSelectorShown={isParameterTypeShown(i)}
|
||||
onTypeUpdated={() =>
|
||||
this.props.onParametersUpdated()
|
||||
}
|
||||
getLastObjectParameterObjectType={() =>
|
||||
getLastObjectParameterObjectType(parameters, i)
|
||||
}
|
||||
/>
|
||||
{isParameterDescriptionShown(i) && (
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Label</Trans>}
|
||||
floatingLabelFixed
|
||||
value={parameter.getDescription()}
|
||||
onChange={text => {
|
||||
parameter.setDescription(text);
|
||||
this.forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
disabled={
|
||||
/* When parameter are freezed, long description (if shown) can always be changed */
|
||||
isParameterDisabled(i) && !freezeParameters
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isParameterLongDescriptionShown(parameter, i) && (
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={
|
||||
<Trans>Long description</Trans>
|
||||
}
|
||||
floatingLabelFixed
|
||||
value={parameter.getLongDescription()}
|
||||
onChange={text => {
|
||||
parameter.setLongDescription(text);
|
||||
this.forceUpdate();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
disabled={
|
||||
/* When parameter are freezed, long description (if shown) can always be changed */
|
||||
isParameterDisabled(i) && !freezeParameters
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
{parameters.size() === 0 ? (
|
||||
<EmptyMessage>
|
||||
<Trans>No parameters for this function.</Trans>
|
||||
</EmptyMessage>
|
||||
) : null}
|
||||
<Column>
|
||||
<Line justifyContent="flex-end" expand>
|
||||
{!freezeParameters && (
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Add a parameter</Trans>}
|
||||
onClick={this._addParameter}
|
||||
icon={<Add />}
|
||||
disabled={
|
||||
eventsFunction.getFunctionType() ===
|
||||
gd.EventsFunction.ActionWithOperator
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
</div>
|
||||
</Line>
|
||||
{helpPagePath ? (
|
||||
<Line>
|
||||
<HelpButton helpPagePath={helpPagePath} />
|
||||
</Line>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This is a "lifecycle method". It will be called automatically by the
|
||||
game engine and has two parameters: "Object" (the object the behavior
|
||||
is acting on) and "Behavior" (the behavior itself).
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
);
|
||||
}
|
||||
}
|
||||
const isAnExtensionLifecycleEventsFunction =
|
||||
!eventsBasedBehavior &&
|
||||
gd.MetadataDeclarationHelper.isExtensionLifecycleEventsFunction(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
if (isAnExtensionLifecycleEventsFunction) {
|
||||
return (
|
||||
<Column noMargin>
|
||||
<DismissableAlertMessage
|
||||
kind="info"
|
||||
identifier="lifecycle-events-function-included-only-if-extension-used"
|
||||
>
|
||||
<Trans>
|
||||
For the lifecycle functions to be executed, you need the extension
|
||||
to be used in the game, either by having at least one action,
|
||||
condition or expression used, or a behavior of the extension added
|
||||
to an object. Otherwise, the extension won't be included in the
|
||||
game.
|
||||
</Trans>
|
||||
</DismissableAlertMessage>
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
This is a "lifecycle function". It will be called automatically by
|
||||
the game engine. It has no parameters. Only global objects can be
|
||||
used as the events will be run for all scenes in your game.
|
||||
</Trans>
|
||||
</EmptyMessage>
|
||||
</Column>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column noMargin expand>
|
||||
<Line noMargin>
|
||||
<div style={styles.parametersContainer}>
|
||||
{mapVector(
|
||||
parameters,
|
||||
(parameter: gdParameterMetadata, i: number) => (
|
||||
<React.Fragment key={i}>
|
||||
<MiniToolbar noPadding>
|
||||
<MiniToolbarText firstChild>
|
||||
<Trans>Parameter #{i + parametersIndexOffset}:</Trans>
|
||||
</MiniToolbarText>
|
||||
<Column expand noMargin>
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
margin="none"
|
||||
translatableHintText={t`Enter the parameter name (mandatory)`}
|
||||
value={parameter.getName()}
|
||||
onChange={text => {
|
||||
parameter.setName(gd.Project.getSafeName(text));
|
||||
forceUpdate();
|
||||
onParametersUpdated();
|
||||
}}
|
||||
disabled={isParameterDisabled(i)}
|
||||
fullWidth
|
||||
/>
|
||||
</Column>
|
||||
<ElementWithMenu
|
||||
element={
|
||||
<IconButton>
|
||||
<ThreeDotsMenu />
|
||||
</IconButton>
|
||||
}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Delete`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
click: () => removeParameter(i),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Add a parameter below`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
click: () => addParameterAt(i + 1),
|
||||
},
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Add a Long Description`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
visible: !isParameterLongDescriptionShown(
|
||||
parameter,
|
||||
i
|
||||
),
|
||||
click: () => addLongDescription(i),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Remove the Long Description`),
|
||||
enabled: !isParameterDisabled(i),
|
||||
visible: isParameterLongDescriptionShown(
|
||||
parameter,
|
||||
i
|
||||
),
|
||||
click: () => removeLongDescription(i),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Move up`),
|
||||
click: () => moveParameters(i, i - 1),
|
||||
enabled:
|
||||
!isParameterDisabled(i) &&
|
||||
i - 1 >= 0 &&
|
||||
!isParameterDisabled(i - 1),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Move down`),
|
||||
click: () => moveParameters(i, i + 1),
|
||||
enabled:
|
||||
!isParameterDisabled(i) &&
|
||||
i + 1 < parameters.size() &&
|
||||
!isParameterDisabled(i + 1),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</MiniToolbar>
|
||||
<Line>
|
||||
<ColumnStackLayout expand noMargin>
|
||||
<ValueTypeEditor
|
||||
project={project}
|
||||
valueTypeMetadata={parameter.getValueTypeMetadata()}
|
||||
disabled={isParameterDisabled(i)}
|
||||
isTypeSelectorShown={isParameterTypeShown(i)}
|
||||
onTypeUpdated={() => onParametersUpdated()}
|
||||
getLastObjectParameterObjectType={() =>
|
||||
getLastObjectParameterObjectType(parameters, i)
|
||||
}
|
||||
/>
|
||||
{isParameterDescriptionShown(i) && (
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Label</Trans>}
|
||||
floatingLabelFixed
|
||||
value={parameter.getDescription()}
|
||||
onChange={text => {
|
||||
parameter.setDescription(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
fullWidth
|
||||
disabled={
|
||||
/* When parameter are freezed, long description (if shown) can always be changed */
|
||||
isParameterDisabled(i) && !freezeParameters
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{isParameterLongDescriptionShown(parameter, i) && (
|
||||
<SemiControlledTextField
|
||||
commitOnBlur
|
||||
floatingLabelText={<Trans>Long description</Trans>}
|
||||
floatingLabelFixed
|
||||
value={parameter.getLongDescription()}
|
||||
onChange={text => {
|
||||
parameter.setLongDescription(text);
|
||||
forceUpdate();
|
||||
}}
|
||||
multiline
|
||||
fullWidth
|
||||
disabled={
|
||||
/* When parameter are freezed, long description (if shown) can always be changed */
|
||||
isParameterDisabled(i) && !freezeParameters
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
</React.Fragment>
|
||||
)
|
||||
)}
|
||||
{parameters.size() === 0 ? (
|
||||
<EmptyMessage>
|
||||
<Trans>No parameters for this function.</Trans>
|
||||
</EmptyMessage>
|
||||
) : null}
|
||||
<Column>
|
||||
<Line justifyContent="flex-end" expand>
|
||||
{!freezeParameters && (
|
||||
<RaisedButton
|
||||
primary
|
||||
label={<Trans>Add a parameter</Trans>}
|
||||
onClick={addParameter}
|
||||
icon={<Add />}
|
||||
disabled={
|
||||
eventsFunction.getFunctionType() ===
|
||||
gd.EventsFunction.ActionWithOperator
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</Line>
|
||||
</Column>
|
||||
</div>
|
||||
</Line>
|
||||
{helpPagePath ? (
|
||||
<Line>
|
||||
<HelpButton helpPagePath={helpPagePath} />
|
||||
</Line>
|
||||
) : (
|
||||
<Spacer />
|
||||
)}
|
||||
</Column>
|
||||
)}
|
||||
</I18n>
|
||||
);
|
||||
};
|
||||
|
@@ -120,6 +120,36 @@ export default function ValueTypeEditor({
|
||||
label={t`Created objects`}
|
||||
/>
|
||||
)}
|
||||
{!isExpressionType && (
|
||||
<SelectOption
|
||||
value="imageResource"
|
||||
label={t`Image resource (JavaScript only)`}
|
||||
/>
|
||||
)}
|
||||
{!isExpressionType && (
|
||||
<SelectOption
|
||||
value="audioResource"
|
||||
label={t`Audio resource (JavaScript only)`}
|
||||
/>
|
||||
)}
|
||||
{!isExpressionType && (
|
||||
<SelectOption
|
||||
value="jsonResource"
|
||||
label={t`JSON resource (JavaScript only)`}
|
||||
/>
|
||||
)}
|
||||
{!isExpressionType && (
|
||||
<SelectOption
|
||||
value="fontResource"
|
||||
label={t`Font resource (JavaScript only)`}
|
||||
/>
|
||||
)}
|
||||
{!isExpressionType && (
|
||||
<SelectOption
|
||||
value="bitmapFontResource"
|
||||
label={t`Bitmap font resource (JavaScript only)`}
|
||||
/>
|
||||
)}
|
||||
</SelectField>
|
||||
)}
|
||||
{valueTypeMetadata.isObject() && (
|
||||
|
@@ -4,14 +4,14 @@ import { Trans } from '@lingui/macro';
|
||||
import * as React from 'react';
|
||||
import ObjectGroupsListWithObjectGroupEditor from '../../ObjectGroupsList/ObjectGroupsListWithObjectGroupEditor';
|
||||
import { Tabs } from '../../UI/Tabs';
|
||||
import EventsFunctionParametersEditor from './EventsFunctionParametersEditor';
|
||||
import { EventsFunctionParametersEditor } from './EventsFunctionParametersEditor';
|
||||
import EventsFunctionPropertiesEditor from './EventsFunctionPropertiesEditor';
|
||||
import ScrollView from '../../UI/ScrollView';
|
||||
import { Column, Line } from '../../UI/Grid';
|
||||
import { showWarningBox } from '../../UI/Messages/MessageBox';
|
||||
import Window from '../../Utils/Window';
|
||||
import { type GroupWithContext } from '../../ObjectsList/EnumerateObjects';
|
||||
import { type UnsavedChanges } from '../../MainFrame/UnsavedChangesContext';
|
||||
import newNameGenerator from '../../Utils/NewNameGenerator';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -67,29 +67,26 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
currentTab: 'config',
|
||||
};
|
||||
|
||||
_canObjectOrGroupUseNewName = (newName: string) => {
|
||||
_getValidatedObjectOrGroupName = (newName: string) => {
|
||||
const { objectsContainer, globalObjectsContainer } = this.props;
|
||||
|
||||
if (
|
||||
objectsContainer.hasObjectNamed(newName) ||
|
||||
globalObjectsContainer.hasObjectNamed(newName) ||
|
||||
objectsContainer.getObjectGroups().has(newName) ||
|
||||
globalObjectsContainer.getObjectGroups().has(newName)
|
||||
) {
|
||||
showWarningBox(
|
||||
'Another object or group with this name already exists in this function.',
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
} else if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
'This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.',
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return false;
|
||||
}
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
objectsContainer.hasObjectNamed(tentativeNewName) ||
|
||||
globalObjectsContainer.hasObjectNamed(tentativeNewName) ||
|
||||
objectsContainer.getObjectGroups().has(tentativeNewName) ||
|
||||
globalObjectsContainer.getObjectGroups().has(tentativeNewName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
_onDeleteGroup = (
|
||||
@@ -246,7 +243,7 @@ export default class EventsFunctionConfigurationEditor extends React.Component<
|
||||
objectsContainer={objectsContainer}
|
||||
globalObjectGroups={globalObjectsContainer.getObjectGroups()}
|
||||
objectGroups={eventsFunction.getObjectGroups()}
|
||||
canRenameGroup={this._canObjectOrGroupUseNewName}
|
||||
getValidatedObjectOrGroupName={this._getValidatedObjectOrGroupName}
|
||||
onRenameGroup={this._onRenameGroup}
|
||||
onDeleteGroup={this._onDeleteGroup}
|
||||
onGroupsUpdated={onParametersOrGroupsUpdated}
|
||||
|
@@ -55,7 +55,7 @@ const ExtensionExporterDialog = (props: Props) => {
|
||||
<Dialog
|
||||
title={<Trans>Export extension</Trans>}
|
||||
secondaryActions={[
|
||||
<HelpButton key="help" helpPagePath="/extensions/share/" />,
|
||||
<HelpButton key="help" helpPagePath="/extensions/share-extension/" />,
|
||||
<FlatButton
|
||||
label={<Trans>Submit to the community</Trans>}
|
||||
onClick={openGitHubIssue}
|
||||
|
@@ -16,7 +16,6 @@ import EventsBasedBehaviorsList from '../EventsBasedBehaviorsList';
|
||||
import EventsBasedObjectsList from '../EventsBasedObjectsList';
|
||||
import Background from '../UI/Background';
|
||||
import OptionsEditorDialog from './OptionsEditorDialog';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import EventsBasedBehaviorEditorDialog from '../EventsBasedBehaviorEditor/EventsBasedBehaviorEditorDialog';
|
||||
import EventsBasedObjectEditorDialog from '../EventsBasedObjectEditor/EventsBasedObjectEditorDialog';
|
||||
import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
|
||||
@@ -37,6 +36,7 @@ import IconButton from '../UI/IconButton';
|
||||
import ExtensionEditIcon from '../UI/CustomSvgIcons/ExtensionEdit';
|
||||
import Tune from '../UI/CustomSvgIcons/Tune';
|
||||
import Mark from '../UI/CustomSvgIcons/Mark';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
type Props = {|
|
||||
@@ -304,34 +304,31 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
newName: string,
|
||||
done: boolean => void
|
||||
) => {
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
gd.MetadataDeclarationHelper.isExtensionLifecycleEventsFunction(newName)
|
||||
) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is reserved for a lifecycle function of the extension. Choose another name for your function.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return done(false);
|
||||
}
|
||||
|
||||
const { project, eventsFunctionsExtension } = this.props;
|
||||
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
gd.MetadataDeclarationHelper.isExtensionLifecycleEventsFunction(
|
||||
tentativeNewName
|
||||
) ||
|
||||
eventsFunctionsExtension.hasEventsFunctionNamed(tentativeNewName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
gd.WholeProjectRefactorer.renameEventsFunction(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction.getName(),
|
||||
newName
|
||||
safeAndUniqueNewName
|
||||
);
|
||||
eventsFunction.setName(safeAndUniqueNewName);
|
||||
|
||||
done(true);
|
||||
if (this.props.onFunctionEdited) {
|
||||
@@ -345,26 +342,23 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
newName: string,
|
||||
done: boolean => void
|
||||
) => {
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return done(false);
|
||||
}
|
||||
if (
|
||||
gd.MetadataDeclarationHelper.isBehaviorLifecycleEventsFunction(newName)
|
||||
) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is reserved for a lifecycle method of the behavior. Choose another name for your custom function.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return done(false);
|
||||
}
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
gd.MetadataDeclarationHelper.isBehaviorLifecycleEventsFunction(
|
||||
tentativeNewName
|
||||
) ||
|
||||
eventsBasedBehavior
|
||||
.getEventsFunctions()
|
||||
.hasEventsFunctionNamed(tentativeNewName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
const { project, eventsFunctionsExtension } = this.props;
|
||||
gd.WholeProjectRefactorer.renameBehaviorEventsFunction(
|
||||
@@ -372,8 +366,9 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedBehavior,
|
||||
eventsFunction.getName(),
|
||||
newName
|
||||
safeAndUniqueNewName
|
||||
);
|
||||
eventsFunction.setName(safeAndUniqueNewName);
|
||||
|
||||
done(true);
|
||||
if (this.props.onFunctionEdited) {
|
||||
@@ -387,24 +382,23 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
newName: string,
|
||||
done: boolean => void
|
||||
) => {
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return done(false);
|
||||
}
|
||||
if (gd.MetadataDeclarationHelper.isObjectLifecycleEventsFunction(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is reserved for a lifecycle method of the object. Choose another name for your custom function.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return done(false);
|
||||
}
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
gd.MetadataDeclarationHelper.isObjectLifecycleEventsFunction(
|
||||
tentativeNewName
|
||||
) ||
|
||||
eventsBasedObject
|
||||
.getEventsFunctions()
|
||||
.hasEventsFunctionNamed(tentativeNewName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
const { project, eventsFunctionsExtension } = this.props;
|
||||
gd.WholeProjectRefactorer.renameObjectEventsFunction(
|
||||
@@ -412,8 +406,9 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject,
|
||||
eventsFunction.getName(),
|
||||
newName
|
||||
safeAndUniqueNewName
|
||||
);
|
||||
eventsFunction.setName(safeAndUniqueNewName);
|
||||
|
||||
done(true);
|
||||
if (this.props.onFunctionEdited) {
|
||||
@@ -570,23 +565,29 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
newName: string,
|
||||
done: boolean => void
|
||||
) => {
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { project, eventsFunctionsExtension } = this.props;
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
eventsFunctionsExtension
|
||||
.getEventsBasedBehaviors()
|
||||
.has(tentativeNewName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
gd.WholeProjectRefactorer.renameEventsBasedBehavior(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedBehavior.getName(),
|
||||
newName
|
||||
safeAndUniqueNewName
|
||||
);
|
||||
eventsBasedBehavior.setName(safeAndUniqueNewName);
|
||||
|
||||
done(true);
|
||||
};
|
||||
@@ -596,23 +597,27 @@ export default class EventsFunctionsExtensionEditor extends React.Component<
|
||||
newName: string,
|
||||
done: boolean => void
|
||||
) => {
|
||||
if (!gd.Project.validateName(newName)) {
|
||||
showWarningBox(
|
||||
i18n._(
|
||||
t`This name is invalid. Only use alphanumeric characters (0-9, a-z) and underscores. Digits are not allowed as the first character.`
|
||||
),
|
||||
{ delayToNextTick: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const { project, eventsFunctionsExtension } = this.props;
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
gd.Project.getSafeName(newName),
|
||||
tentativeNewName => {
|
||||
if (
|
||||
eventsFunctionsExtension.getEventsBasedObjects().has(tentativeNewName)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
);
|
||||
|
||||
gd.WholeProjectRefactorer.renameEventsBasedObject(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
eventsBasedObject.getName(),
|
||||
newName
|
||||
safeAndUniqueNewName
|
||||
);
|
||||
eventsBasedObject.setName(safeAndUniqueNewName);
|
||||
|
||||
done(true);
|
||||
};
|
||||
|
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import { mapVector, mapFor } from '../Utils/MapFor';
|
||||
import { caseSensitiveSlug } from '../Utils/CaseSensitiveSlug';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
@@ -32,56 +31,10 @@ type OptionsForGeneration = {
|
||||
};
|
||||
|
||||
type CodeGenerationContext = {|
|
||||
codeNamespacePrefix: string,
|
||||
codeNamespacePrefix: string, // TODO: could this reworked to avoid this entirely?
|
||||
extensionIncludeFiles: Array<string>,
|
||||
|};
|
||||
|
||||
const mangleName = (name: string) => {
|
||||
return caseSensitiveSlug(name, '_', []);
|
||||
};
|
||||
|
||||
const getExtensionCodeNamespacePrefix = (
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension
|
||||
) => {
|
||||
return 'gdjs.evtsExt__' + mangleName(eventsFunctionsExtension.getName());
|
||||
};
|
||||
|
||||
/** Generate the namespace for a free function. */
|
||||
const getFreeFunctionCodeNamespace = (
|
||||
eventsFunction: gdEventsFunction,
|
||||
codeNamespacePrefix: string
|
||||
) => {
|
||||
return codeNamespacePrefix + '__' + mangleName(eventsFunction.getName());
|
||||
};
|
||||
|
||||
export const getFreeFunctionCodeName = (
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
eventsFunction: gdEventsFunction
|
||||
) => {
|
||||
return (
|
||||
getFreeFunctionCodeNamespace(
|
||||
eventsFunction,
|
||||
getExtensionCodeNamespacePrefix(eventsFunctionsExtension)
|
||||
) + '.func'
|
||||
);
|
||||
};
|
||||
|
||||
/** Generate the namespace for a behavior function. */
|
||||
const getBehaviorFunctionCodeNamespace = (
|
||||
eventsBasedBehavior: gdEventsBasedBehavior,
|
||||
codeNamespacePrefix: string
|
||||
) => {
|
||||
return codeNamespacePrefix + '__' + mangleName(eventsBasedBehavior.getName());
|
||||
};
|
||||
|
||||
/** Generate the namespace for an object function. */
|
||||
const getObjectFunctionCodeNamespace = (
|
||||
eventsBasedObject: gdEventsBasedObject,
|
||||
codeNamespacePrefix: string
|
||||
) => {
|
||||
return codeNamespacePrefix + '__' + mangleName(eventsBasedObject.getName());
|
||||
};
|
||||
|
||||
/**
|
||||
* Load all events functions of a project in extensions
|
||||
*/
|
||||
@@ -160,18 +113,15 @@ const loadProjectEventsFunctionsExtension = (
|
||||
const getExtensionIncludeFiles = (
|
||||
project: gdProject,
|
||||
eventsFunctionsExtension: gdEventsFunctionsExtension,
|
||||
options: Options,
|
||||
codeNamespacePrefix: string
|
||||
options: Options
|
||||
): Array<string> => {
|
||||
return mapFor(0, eventsFunctionsExtension.getEventsFunctionsCount(), i => {
|
||||
const eventsFunction = eventsFunctionsExtension.getEventsFunctionAt(i);
|
||||
|
||||
const codeNamespace = getFreeFunctionCodeNamespace(
|
||||
eventsFunction,
|
||||
codeNamespacePrefix
|
||||
const functionName = gd.MetadataDeclarationHelper.getFreeFunctionCodeName(
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction
|
||||
);
|
||||
// TODO Use getFreeFunctionCodeName.
|
||||
const functionName = codeNamespace + '.func';
|
||||
|
||||
return options.eventsFunctionCodeWriter.getIncludeFileFor(functionName);
|
||||
}).filter(Boolean);
|
||||
@@ -191,14 +141,14 @@ const generateEventsFunctionExtension = (
|
||||
eventsFunctionsExtension
|
||||
);
|
||||
|
||||
const codeNamespacePrefix =
|
||||
'gdjs.evtsExt__' + mangleName(eventsFunctionsExtension.getName());
|
||||
const codeNamespacePrefix = gd.MetadataDeclarationHelper.getExtensionCodeNamespacePrefix(
|
||||
eventsFunctionsExtension
|
||||
);
|
||||
|
||||
const extensionIncludeFiles = getExtensionIncludeFiles(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
options,
|
||||
codeNamespacePrefix
|
||||
options
|
||||
);
|
||||
const codeGenerationContext = {
|
||||
codeNamespacePrefix,
|
||||
@@ -276,14 +226,14 @@ const generateEventsFunctionExtensionMetadata = (
|
||||
eventsFunctionsExtension
|
||||
);
|
||||
|
||||
const codeNamespacePrefix =
|
||||
'gdjs.evtsExt__' + mangleName(eventsFunctionsExtension.getName());
|
||||
const codeNamespacePrefix = gd.MetadataDeclarationHelper.getExtensionCodeNamespacePrefix(
|
||||
eventsFunctionsExtension
|
||||
);
|
||||
|
||||
const extensionIncludeFiles = getExtensionIncludeFiles(
|
||||
project,
|
||||
eventsFunctionsExtension,
|
||||
options,
|
||||
codeNamespacePrefix
|
||||
options
|
||||
);
|
||||
const codeGenerationContext = {
|
||||
codeNamespacePrefix,
|
||||
@@ -369,7 +319,7 @@ const generateFreeFunction = (
|
||||
const eventsFunctionsExtensionCodeGenerator = new gd.EventsFunctionsExtensionCodeGenerator(
|
||||
project
|
||||
);
|
||||
const codeNamespace = getFreeFunctionCodeNamespace(
|
||||
const codeNamespace = gd.MetadataDeclarationHelper.getFreeFunctionCodeNamespace(
|
||||
eventsFunction,
|
||||
codeGenerationContext.codeNamespacePrefix
|
||||
);
|
||||
@@ -398,8 +348,10 @@ const generateFreeFunction = (
|
||||
eventsFunctionsExtensionCodeGenerator.delete();
|
||||
metadataDeclarationHelper.delete();
|
||||
|
||||
// TODO Implement an helper function for free function names.
|
||||
const functionName = codeNamespace + '.func';
|
||||
const functionName = gd.MetadataDeclarationHelper.getFreeFunctionCodeName(
|
||||
eventsFunctionsExtension,
|
||||
eventsFunction
|
||||
);
|
||||
return options.eventsFunctionCodeWriter
|
||||
.writeFunctionCode(functionName, code)
|
||||
.then(() => {});
|
||||
@@ -476,7 +428,7 @@ function generateBehavior(
|
||||
|
||||
// Generate code for the behavior and its methods
|
||||
if (!options.skipCodeGeneration) {
|
||||
const codeNamespace = getBehaviorFunctionCodeNamespace(
|
||||
const codeNamespace = gd.MetadataDeclarationHelper.getBehaviorFunctionCodeNamespace(
|
||||
eventsBasedBehavior,
|
||||
codeGenerationContext.codeNamespacePrefix
|
||||
);
|
||||
@@ -538,7 +490,7 @@ function generateBehaviorMetadata(
|
||||
behaviorMethodMangledNames
|
||||
);
|
||||
|
||||
const codeNamespace = getBehaviorFunctionCodeNamespace(
|
||||
const codeNamespace = gd.MetadataDeclarationHelper.getBehaviorFunctionCodeNamespace(
|
||||
eventsBasedBehavior,
|
||||
codeGenerationContext.codeNamespacePrefix
|
||||
);
|
||||
@@ -578,7 +530,7 @@ function generateObject(
|
||||
|
||||
// Generate code for the object and its methods
|
||||
if (!options.skipCodeGeneration) {
|
||||
const codeNamespace = getObjectFunctionCodeNamespace(
|
||||
const codeNamespace = gd.MetadataDeclarationHelper.getObjectFunctionCodeNamespace(
|
||||
eventsBasedObject,
|
||||
codeGenerationContext.codeNamespacePrefix
|
||||
);
|
||||
@@ -640,7 +592,7 @@ function generateObjectMetadata(
|
||||
objectMethodMangledNames
|
||||
);
|
||||
|
||||
const codeNamespace = getObjectFunctionCodeNamespace(
|
||||
const codeNamespace = gd.MetadataDeclarationHelper.getObjectFunctionCodeNamespace(
|
||||
eventsBasedObject,
|
||||
codeGenerationContext.codeNamespacePrefix
|
||||
);
|
||||
|
@@ -8,7 +8,6 @@ import * as React from 'react';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
import SortableVirtualizedItemList from '../UI/SortableVirtualizedItemList';
|
||||
import SearchBar from '../UI/SearchBar';
|
||||
import { showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import Background from '../UI/Background';
|
||||
import newNameGenerator from '../Utils/NewNameGenerator';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
@@ -217,23 +216,14 @@ export default class EventsFunctionsList extends React.Component<Props, State> {
|
||||
}
|
||||
};
|
||||
_rename = (eventsFunction: gdEventsFunction, newName: string) => {
|
||||
const { eventsFunctionsContainer } = this.props;
|
||||
this.setState({
|
||||
renamedEventsFunction: null,
|
||||
});
|
||||
|
||||
if (eventsFunction.getName() === newName) return;
|
||||
|
||||
if (eventsFunctionsContainer.hasEventsFunctionNamed(newName)) {
|
||||
showWarningBox('Another function with this name already exists.', {
|
||||
delayToNextTick: true,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.props.onRenameEventsFunction(eventsFunction, newName, doRename => {
|
||||
if (!doRename) return;
|
||||
eventsFunction.setName(newName);
|
||||
this._onEventsFunctionModified();
|
||||
});
|
||||
};
|
||||
|
@@ -21,7 +21,7 @@ import {
|
||||
} from '.';
|
||||
import AlertMessage from '../../UI/AlertMessage';
|
||||
import DismissableAlertMessage from '../../UI/DismissableAlertMessage';
|
||||
import EventsFunctionParametersEditor from '../../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionParametersEditor';
|
||||
import { EventsFunctionParametersEditor } from '../../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionParametersEditor';
|
||||
import EventsFunctionPropertiesEditor from '../../EventsFunctionsExtensionEditor/EventsFunctionConfigurationEditor/EventsFunctionPropertiesEditor';
|
||||
import HelpButton from '../../UI/HelpButton';
|
||||
import { ColumnStackLayout, ResponsiveLineStackLayout } from '../../UI/Layout';
|
||||
|
@@ -1,236 +1,236 @@
|
||||
// @flow
|
||||
import { unserializeFromJSObject } from '../../Utils/Serializer';
|
||||
import { mapVector } from '../../Utils/MapFor';
|
||||
import { getFreeEventsFunctionType } from '../../EventsFunctionsExtensionsLoader';
|
||||
import getObjectGroupByName from '../../Utils/GetObjectGroupByName';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
/**
|
||||
* Set up an events function with the given serialized events,
|
||||
* so that the function contains these events, expecting the objects
|
||||
* as parameters.
|
||||
*/
|
||||
export const setupFunctionFromEvents = ({
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
serializedEvents,
|
||||
project,
|
||||
eventsFunction,
|
||||
}: {
|
||||
project: gdProject,
|
||||
globalObjectsContainer: gdObjectsContainer,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
serializedEvents: Object,
|
||||
eventsFunction: gdEventsFunction,
|
||||
}) => {
|
||||
// Set up the function
|
||||
eventsFunction.setName('MyFunction');
|
||||
eventsFunction.setFunctionType(gd.EventsFunction.Action);
|
||||
unserializeFromJSObject(
|
||||
eventsFunction.getEvents(),
|
||||
serializedEvents,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
|
||||
// Analyze events...
|
||||
const eventsContextAnalyzer = new gd.EventsContextAnalyzer(
|
||||
gd.JsPlatform.get(),
|
||||
globalObjectsContainer,
|
||||
objectsContainer
|
||||
);
|
||||
eventsContextAnalyzer.launch(eventsFunction.getEvents());
|
||||
const eventsContext = eventsContextAnalyzer.getEventsContext();
|
||||
|
||||
// ...to extract objects and groups
|
||||
const objectOrGroupNames: Array<string> = eventsContext
|
||||
.getReferencedObjectOrGroupNames()
|
||||
.toNewVectorString()
|
||||
.toJSArray();
|
||||
const objectNames: Array<string> = eventsContext
|
||||
.getObjectNames()
|
||||
.toNewVectorString()
|
||||
.toJSArray();
|
||||
const groups: Array<gdObjectGroup> = objectOrGroupNames
|
||||
// Filter to only keep groups
|
||||
.filter(
|
||||
(objectOrGroupName: string) =>
|
||||
objectNames.indexOf(objectOrGroupName) === -1
|
||||
)
|
||||
.map(groupName =>
|
||||
getObjectGroupByName(globalObjectsContainer, objectsContainer, groupName)
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
// Compute what the parameters should be:
|
||||
// 1) The groups, but only the ones that have no object directly referenced.
|
||||
const parameterGroups: Array<gdObjectGroup> = groups.filter(group => {
|
||||
return !objectOrGroupNames.some(referencedObjectOrGroupName =>
|
||||
group.find(referencedObjectOrGroupName)
|
||||
);
|
||||
});
|
||||
const parameterGroupNames: Array<string> = parameterGroups.map(group =>
|
||||
group.getName()
|
||||
);
|
||||
|
||||
// 2) The objects, but only the ones that are already in the groups in parameters
|
||||
const parameterObjectNames: Array<string> = objectNames.filter(objectName => {
|
||||
return !parameterGroups.some(group => group.find(objectName));
|
||||
});
|
||||
|
||||
// Create parameters for these objects (or these groups without any object directly referenced)
|
||||
const parameters = eventsFunction.getParameters();
|
||||
parameters.clear();
|
||||
[...parameterGroupNames, ...parameterObjectNames].forEach(objectName => {
|
||||
const newParameter = new gd.ParameterMetadata();
|
||||
newParameter.setType('objectList');
|
||||
newParameter.setName(objectName);
|
||||
newParameter.setExtraInfo(
|
||||
gd.getTypeOfObject(
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
objectName,
|
||||
true
|
||||
)
|
||||
);
|
||||
parameters.push_back(newParameter);
|
||||
|
||||
const behaviorNames: Array<string> = eventsContext
|
||||
.getBehaviorNamesOfObjectOrGroup(objectName)
|
||||
.toNewVectorString()
|
||||
.toJSArray();
|
||||
|
||||
behaviorNames.forEach(behaviorName => {
|
||||
const newParameter = new gd.ParameterMetadata();
|
||||
newParameter.setType('behavior');
|
||||
newParameter.setName(behaviorName);
|
||||
newParameter.setExtraInfo(
|
||||
gd.getTypeOfBehavior(
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
behaviorName,
|
||||
false
|
||||
)
|
||||
);
|
||||
parameters.push_back(newParameter);
|
||||
});
|
||||
});
|
||||
|
||||
// Import groups that are used in events, but are not in parameters,
|
||||
// inside the events function groups.
|
||||
groups
|
||||
.filter(group => !parameterGroupNames.includes(group.getName()))
|
||||
.forEach(group => {
|
||||
if (group) {
|
||||
eventsFunction.getObjectGroups().insert(group, 0);
|
||||
}
|
||||
});
|
||||
|
||||
eventsContextAnalyzer.delete();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an instruction to call the given events function
|
||||
*/
|
||||
export const createNewInstructionForEventsFunction = (
|
||||
extensionName: string,
|
||||
eventsFunction: gdEventsFunction
|
||||
): gdInstruction => {
|
||||
const action = new gd.Instruction(); //Add a simple action
|
||||
const runtimeSceneParameterCount = 1; // By convention, first parameter is always the Runtime Scene.
|
||||
const contextParameterCount = 1; // By convention, latest parameter is always the eventsFunctionContext of the calling function (if any).
|
||||
|
||||
action.setType(getFreeEventsFunctionType(extensionName, eventsFunction));
|
||||
action.setParametersCount(
|
||||
eventsFunction.getParameters().size() +
|
||||
runtimeSceneParameterCount +
|
||||
contextParameterCount
|
||||
);
|
||||
|
||||
mapVector(eventsFunction.getParameters(), (parameterMetadata, index) => {
|
||||
action.setParameter(
|
||||
runtimeSceneParameterCount + index,
|
||||
parameterMetadata.getName()
|
||||
);
|
||||
});
|
||||
|
||||
return action;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that a function name is valid.
|
||||
*/
|
||||
export const validateEventsFunctionName = (functionName: string) => {
|
||||
return gd.Project.validateName(functionName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that an events functions extension name is valid.
|
||||
*/
|
||||
export const validateExtensionName = (extensionName: string) => {
|
||||
return gd.Project.validateName(extensionName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that an events functions extension name is unique in a project.
|
||||
*/
|
||||
export const validateExtensionNameUniqueness = (
|
||||
project: gdProject,
|
||||
extensionName: string
|
||||
) => {
|
||||
return !project.hasEventsFunctionsExtensionNamed(extensionName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that an events function name is unique in a project extension.
|
||||
*/
|
||||
export const validateEventsFunctionNameUniqueness = (
|
||||
project: gdProject,
|
||||
extensionName: string,
|
||||
eventsFunction: gdEventsFunction
|
||||
) => {
|
||||
if (project.hasEventsFunctionsExtensionNamed(extensionName)) {
|
||||
const eventsFunctionsExtension = project.getEventsFunctionsExtension(
|
||||
extensionName
|
||||
);
|
||||
|
||||
return !eventsFunctionsExtension.hasEventsFunctionNamed(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the events function can be added to the given extension
|
||||
* without any conflict/invalid name.
|
||||
*/
|
||||
export const canCreateEventsFunction = (
|
||||
project: gdProject,
|
||||
extensionName: string,
|
||||
eventsFunction: gdEventsFunction
|
||||
) => {
|
||||
return (
|
||||
extensionName !== '' &&
|
||||
validateExtensionName(extensionName) &&
|
||||
eventsFunction.getName() !== '' &&
|
||||
validateEventsFunctionName(eventsFunction.getName()) &&
|
||||
validateEventsFunctionNameUniqueness(
|
||||
project,
|
||||
extensionName,
|
||||
eventsFunction
|
||||
) &&
|
||||
eventsFunction.getFullName() !== '' &&
|
||||
eventsFunction.getSentence() !== ''
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the function is considered to have more parameters than usual.
|
||||
*/
|
||||
export const functionHasLotsOfParameters = (
|
||||
eventsFunction: gdEventsFunction
|
||||
) => {
|
||||
return eventsFunction.getParameters().size() > 7;
|
||||
};
|
||||
// @flow
|
||||
import { unserializeFromJSObject } from '../../Utils/Serializer';
|
||||
import { mapVector } from '../../Utils/MapFor';
|
||||
import { getFreeEventsFunctionType } from '../../EventsFunctionsExtensionsLoader';
|
||||
import getObjectGroupByName from '../../Utils/GetObjectGroupByName';
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
/**
|
||||
* Set up an events function with the given serialized events,
|
||||
* so that the function contains these events, expecting the objects
|
||||
* as parameters.
|
||||
*/
|
||||
export const setupFunctionFromEvents = ({
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
serializedEvents,
|
||||
project,
|
||||
eventsFunction,
|
||||
}: {
|
||||
project: gdProject,
|
||||
globalObjectsContainer: gdObjectsContainer,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
serializedEvents: Object,
|
||||
eventsFunction: gdEventsFunction,
|
||||
}) => {
|
||||
// Set up the function
|
||||
eventsFunction.setName('MyFunction');
|
||||
eventsFunction.setFunctionType(gd.EventsFunction.Action);
|
||||
unserializeFromJSObject(
|
||||
eventsFunction.getEvents(),
|
||||
serializedEvents,
|
||||
'unserializeFrom',
|
||||
project
|
||||
);
|
||||
|
||||
// Analyze events...
|
||||
const eventsContextAnalyzer = new gd.EventsContextAnalyzer(
|
||||
gd.JsPlatform.get(),
|
||||
globalObjectsContainer,
|
||||
objectsContainer
|
||||
);
|
||||
eventsContextAnalyzer.launch(eventsFunction.getEvents());
|
||||
const eventsContext = eventsContextAnalyzer.getEventsContext();
|
||||
|
||||
// ...to extract objects and groups
|
||||
const objectOrGroupNames: Array<string> = eventsContext
|
||||
.getReferencedObjectOrGroupNames()
|
||||
.toNewVectorString()
|
||||
.toJSArray();
|
||||
const objectNames: Array<string> = eventsContext
|
||||
.getObjectNames()
|
||||
.toNewVectorString()
|
||||
.toJSArray();
|
||||
const groups: Array<gdObjectGroup> = objectOrGroupNames
|
||||
// Filter to only keep groups
|
||||
.filter(
|
||||
(objectOrGroupName: string) =>
|
||||
objectNames.indexOf(objectOrGroupName) === -1
|
||||
)
|
||||
.map(groupName =>
|
||||
getObjectGroupByName(globalObjectsContainer, objectsContainer, groupName)
|
||||
)
|
||||
.filter(Boolean);
|
||||
|
||||
// Compute what the parameters should be:
|
||||
// 1) The groups, but only the ones that have no object directly referenced.
|
||||
const parameterGroups: Array<gdObjectGroup> = groups.filter(group => {
|
||||
return !objectOrGroupNames.some(referencedObjectOrGroupName =>
|
||||
group.find(referencedObjectOrGroupName)
|
||||
);
|
||||
});
|
||||
const parameterGroupNames: Array<string> = parameterGroups.map(group =>
|
||||
group.getName()
|
||||
);
|
||||
|
||||
// 2) The objects, but only the ones that are already in the groups in parameters
|
||||
const parameterObjectNames: Array<string> = objectNames.filter(objectName => {
|
||||
return !parameterGroups.some(group => group.find(objectName));
|
||||
});
|
||||
|
||||
// Create parameters for these objects (or these groups without any object directly referenced)
|
||||
const parameters = eventsFunction.getParameters();
|
||||
parameters.clear();
|
||||
[...parameterGroupNames, ...parameterObjectNames].forEach(objectName => {
|
||||
const newParameter = new gd.ParameterMetadata();
|
||||
newParameter.setType('objectList');
|
||||
newParameter.setName(objectName);
|
||||
newParameter.setExtraInfo(
|
||||
gd.getTypeOfObject(
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
objectName,
|
||||
true
|
||||
)
|
||||
);
|
||||
parameters.push_back(newParameter);
|
||||
|
||||
const behaviorNames: Array<string> = eventsContext
|
||||
.getBehaviorNamesOfObjectOrGroup(objectName)
|
||||
.toNewVectorString()
|
||||
.toJSArray();
|
||||
|
||||
behaviorNames.forEach(behaviorName => {
|
||||
const newParameter = new gd.ParameterMetadata();
|
||||
newParameter.setType('behavior');
|
||||
newParameter.setName(behaviorName);
|
||||
newParameter.setExtraInfo(
|
||||
gd.getTypeOfBehavior(
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
behaviorName,
|
||||
false
|
||||
)
|
||||
);
|
||||
parameters.push_back(newParameter);
|
||||
});
|
||||
});
|
||||
|
||||
// Import groups that are used in events, but are not in parameters,
|
||||
// inside the events function groups.
|
||||
groups
|
||||
.filter(group => !parameterGroupNames.includes(group.getName()))
|
||||
.forEach(group => {
|
||||
if (group) {
|
||||
eventsFunction.getObjectGroups().insert(group, 0);
|
||||
}
|
||||
});
|
||||
|
||||
eventsContextAnalyzer.delete();
|
||||
};
|
||||
|
||||
/**
|
||||
* Create an instruction to call the given events function
|
||||
*/
|
||||
export const createNewInstructionForEventsFunction = (
|
||||
extensionName: string,
|
||||
eventsFunction: gdEventsFunction
|
||||
): gdInstruction => {
|
||||
const action = new gd.Instruction(); //Add a simple action
|
||||
const runtimeSceneParameterCount = 1; // By convention, first parameter is always the Runtime Scene.
|
||||
const contextParameterCount = 1; // By convention, latest parameter is always the eventsFunctionContext of the calling function (if any).
|
||||
|
||||
action.setType(getFreeEventsFunctionType(extensionName, eventsFunction));
|
||||
action.setParametersCount(
|
||||
eventsFunction.getParameters().size() +
|
||||
runtimeSceneParameterCount +
|
||||
contextParameterCount
|
||||
);
|
||||
|
||||
mapVector(eventsFunction.getParameters(), (parameterMetadata, index) => {
|
||||
action.setParameter(
|
||||
runtimeSceneParameterCount + index,
|
||||
parameterMetadata.getName()
|
||||
);
|
||||
});
|
||||
|
||||
return action;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that a function name is valid.
|
||||
*/
|
||||
export const validateEventsFunctionName = (functionName: string) => {
|
||||
return gd.Project.isNameSafe(functionName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that an events functions extension name is valid.
|
||||
*/
|
||||
export const validateExtensionName = (extensionName: string) => {
|
||||
return gd.Project.isNameSafe(extensionName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that an events functions extension name is unique in a project.
|
||||
*/
|
||||
export const validateExtensionNameUniqueness = (
|
||||
project: gdProject,
|
||||
extensionName: string
|
||||
) => {
|
||||
return !project.hasEventsFunctionsExtensionNamed(extensionName);
|
||||
};
|
||||
|
||||
/**
|
||||
* Validate that an events function name is unique in a project extension.
|
||||
*/
|
||||
export const validateEventsFunctionNameUniqueness = (
|
||||
project: gdProject,
|
||||
extensionName: string,
|
||||
eventsFunction: gdEventsFunction
|
||||
) => {
|
||||
if (project.hasEventsFunctionsExtensionNamed(extensionName)) {
|
||||
const eventsFunctionsExtension = project.getEventsFunctionsExtension(
|
||||
extensionName
|
||||
);
|
||||
|
||||
return !eventsFunctionsExtension.hasEventsFunctionNamed(
|
||||
eventsFunction.getName()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the events function can be added to the given extension
|
||||
* without any conflict/invalid name.
|
||||
*/
|
||||
export const canCreateEventsFunction = (
|
||||
project: gdProject,
|
||||
extensionName: string,
|
||||
eventsFunction: gdEventsFunction
|
||||
) => {
|
||||
return (
|
||||
extensionName !== '' &&
|
||||
validateExtensionName(extensionName) &&
|
||||
eventsFunction.getName() !== '' &&
|
||||
validateEventsFunctionName(eventsFunction.getName()) &&
|
||||
validateEventsFunctionNameUniqueness(
|
||||
project,
|
||||
extensionName,
|
||||
eventsFunction
|
||||
) &&
|
||||
eventsFunction.getFullName() !== '' &&
|
||||
eventsFunction.getSentence() !== ''
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* Return true if the function is considered to have more parameters than usual.
|
||||
*/
|
||||
export const functionHasLotsOfParameters = (
|
||||
eventsFunction: gdEventsFunction
|
||||
) => {
|
||||
return eventsFunction.getParameters().size() > 7;
|
||||
};
|
||||
|
@@ -1,6 +1,7 @@
|
||||
// @flow
|
||||
import { t, Trans } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import capitalize from 'lodash/capitalize';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
import { mapFor } from '../../Utils/MapFor';
|
||||
@@ -55,12 +56,6 @@ const styles = {
|
||||
|
||||
export const reactDndInstructionType = 'GD_DRAGGED_INSTRUCTION';
|
||||
|
||||
const capitalize = (str: string) => {
|
||||
if (!str) return '';
|
||||
|
||||
return str[0].toUpperCase() + str.substr(1);
|
||||
};
|
||||
|
||||
const DragSourceAndDropTarget = makeDragSourceAndDropTarget<{
|
||||
isCondition: boolean,
|
||||
}>(reactDndInstructionType);
|
||||
|
@@ -18,6 +18,7 @@ const styles = {
|
||||
popover: {
|
||||
paddingBottom: 10,
|
||||
overflowY: 'auto',
|
||||
scrollbarWidth: 'thin', // For Firefox, to avoid having a very large scrollbar.
|
||||
|
||||
// Never show a horizontal scrollbar
|
||||
overflowX: 'hidden',
|
||||
|
@@ -50,29 +50,28 @@ export const renderInstructionOrExpressionTree = <
|
||||
|
||||
if (typeof instructionOrGroup.type === 'string') {
|
||||
// $FlowFixMe - see above
|
||||
const instructionInformation: T = instructionOrGroup;
|
||||
const instructionMetadata: T = instructionOrGroup;
|
||||
const value = getInstructionListItemValue(instructionOrGroup.type);
|
||||
const selected = selectedValue === value;
|
||||
return (
|
||||
<ListItem
|
||||
key={value}
|
||||
primaryText={key}
|
||||
primaryText={instructionMetadata.displayedName}
|
||||
selected={selected}
|
||||
id={
|
||||
// TODO: This id is used by in app tutorials. When in app tutorials
|
||||
// are linked to GDevelop versions, change this id to be more accurate
|
||||
// using getInstructionOrExpressionIdentifier
|
||||
'instruction-item-' +
|
||||
instructionInformation.type.replace(/:/g, '-')
|
||||
'instruction-item-' + instructionMetadata.type.replace(/:/g, '-')
|
||||
}
|
||||
leftIcon={
|
||||
<ListIcon
|
||||
iconSize={iconSize}
|
||||
src={instructionInformation.iconFilename}
|
||||
src={instructionMetadata.iconFilename}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
onChoose(instructionInformation.type, instructionInformation);
|
||||
onChoose(instructionMetadata.type, instructionMetadata);
|
||||
}}
|
||||
ref={selected ? selectedItemRef : undefined}
|
||||
/>
|
||||
|
@@ -16,6 +16,9 @@ import ObjectsRenderingService from '../../../ObjectsRendering/ObjectsRenderingS
|
||||
import Paper from '../../../UI/Paper';
|
||||
import { mapVector } from '../../../Utils/MapFor';
|
||||
import { Trans } from '@lingui/macro';
|
||||
import GDevelopThemeContext from '../../../UI/Theme/GDevelopThemeContext';
|
||||
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
const defaultTextStyle = {
|
||||
// Break words if they are too long to fit on a single line.
|
||||
@@ -23,16 +26,26 @@ const defaultTextStyle = {
|
||||
overflowWrap: 'break-word',
|
||||
};
|
||||
|
||||
const AutocompletionIcon = React.memo(({ src }) => (
|
||||
<img
|
||||
alt=""
|
||||
src={src}
|
||||
style={{
|
||||
maxWidth: 16,
|
||||
maxHeight: 16,
|
||||
}}
|
||||
/>
|
||||
));
|
||||
const AutocompletionIcon = React.memo(({ src }) => {
|
||||
const {
|
||||
palette: { type: paletteType },
|
||||
} = React.useContext(GDevelopThemeContext);
|
||||
|
||||
const shouldInvertGrayScale =
|
||||
paletteType === 'dark' &&
|
||||
(src.startsWith('data:image/svg+xml') || src.includes('_black'));
|
||||
return (
|
||||
<img
|
||||
src={src}
|
||||
alt=""
|
||||
style={{
|
||||
maxWidth: 16,
|
||||
maxHeight: 16,
|
||||
filter: shouldInvertGrayScale ? 'grayscale(1) invert(1)' : undefined,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const formatParameterTypesString = (
|
||||
parameterRenderingService: ParameterRenderingServiceType,
|
||||
@@ -195,10 +208,12 @@ const DisplayedObjectAutocompletion = React.forwardRef(
|
||||
const DisplayedBehaviorAutocompletion = React.forwardRef(
|
||||
(
|
||||
{
|
||||
project,
|
||||
expressionAutocompletion,
|
||||
isSelected,
|
||||
onClick,
|
||||
}: {|
|
||||
project: ?gdProject,
|
||||
expressionAutocompletion: ExpressionAutocompletion,
|
||||
isSelected: boolean,
|
||||
onClick: () => void,
|
||||
@@ -210,6 +225,14 @@ const DisplayedBehaviorAutocompletion = React.forwardRef(
|
||||
) : (
|
||||
expressionAutocompletion.completion
|
||||
);
|
||||
const behaviorType = expressionAutocompletion.behaviorType || null;
|
||||
const thumbnail =
|
||||
project && behaviorType
|
||||
? gd.MetadataProvider.getBehaviorMetadata(
|
||||
project.getCurrentPlatform(),
|
||||
behaviorType
|
||||
).getIconFilename()
|
||||
: 'res/types/behavior.png';
|
||||
return (
|
||||
<ButtonBase
|
||||
style={styles.button}
|
||||
@@ -221,7 +244,7 @@ const DisplayedBehaviorAutocompletion = React.forwardRef(
|
||||
onClick={onClick}
|
||||
ref={ref}
|
||||
>
|
||||
<AutocompletionIcon src={'res/types/behavior.png'} />
|
||||
<AutocompletionIcon src={thumbnail} />
|
||||
<Spacer />
|
||||
<Text style={defaultTextStyle} noMargin align="left">
|
||||
{title}
|
||||
@@ -409,6 +432,7 @@ export default function ExpressionAutocompletionsDisplayer({
|
||||
) : expressionAutocompletion.kind === 'Behavior' ? (
|
||||
<DisplayedBehaviorAutocompletion
|
||||
key={index}
|
||||
project={project}
|
||||
expressionAutocompletion={expressionAutocompletion}
|
||||
onClick={() => onChoose(expressionAutocompletion)}
|
||||
isSelected={isSelected}
|
||||
|
@@ -37,6 +37,7 @@ export default React.forwardRef<ParameterFieldProps, ParameterFieldInterface>(
|
||||
value={value}
|
||||
ref={field}
|
||||
onChange={(e, i, value) => props.onChange(value)}
|
||||
translatableHintText={t`Choose a mouse button`}
|
||||
>
|
||||
<SelectOption value="Left" label={t`Left (primary)`} />
|
||||
<SelectOption value="Right" label={t`Right (secondary)`} />
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user