Compare commits

..

28 Commits

Author SHA1 Message Date
Clément Pasteau
08ddaf35ae Capitalize assets for consistency 2023-08-21 11:39:11 +02:00
D8H
134886eedc Add a default 3D light on any new layer (#5576)
* This won't affect efficiency for 2D games.
2023-08-18 22:39:40 +02:00
Clément Pasteau
da308bb104 Fix displaying placeholder when provided (#5574)
Do not show in changelog
2023-08-18 09:49:52 +02:00
github-actions[bot]
5087526066 Update translations [skip ci] (#5573)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2023-08-18 09:27:52 +02:00
Florian Rival
b6f98ad667 Fix actions disappearing when wrongly translated with the same name in a non english language translation (#5566) 2023-08-17 18:23:07 +02:00
github-actions[bot]
b4a6b73146 Update translations [skip ci] (#5570)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2023-08-17 14:00:15 +02:00
Clément Pasteau
2ccdc1aad8 Improve asset store with pack search, filters and navigation (#5546) 2023-08-17 12:29:52 +02:00
supertree-wook
68f13297ef Fix "Share your extension" not redirecting to the proper page on the documentation (#5568) 2023-08-17 10:27:16 +02:00
D8H
6a26e2cf32 Fix unused resources clean up to keep resources used in effects (#5560) 2023-08-16 14:30:08 +02:00
Clément Pasteau
9532a42558 Better text select mouse button (#5569)
Do not show in changelog
2023-08-16 12:31:04 +02:00
github-actions[bot]
f23bc5dfd9 Update translations [skip ci] (#5552)
Co-authored-by: 4ian <4ian@users.noreply.github.com>
2023-08-16 11:28:56 +02:00
Florian Rival
4a4bf6d761 Add support for unicode names (any character, including emojis) for object/group/behavior/extension/function names (#5564)
* In other words: this allows to use emojis, accented characters, CJK languages and any word from any language to name anything in GDevelop. This should be very useful for non english speaking users.
* This must be activated in the preferences of GDevelop. This will be activated by default once this is proven to work without bugs.
2023-08-14 21:46:10 +02:00
Florian Rival
68968b603a Automatically fix object/group/function/behavior/object/property names when an incorrect one is entered (#5561)
* Instead of displaying an error message (for example, if a space is used in an object name), the name entered is automatically fixed. This is easier to understand and avoids breaking the creative workflow.
* Same for scenes, external events, external layouts and extension names in the project manager.
2023-08-14 00:05:53 +02:00
D8H
eb723b2a0e Fix missing tags for installed extensions in the behavior list (#5553) 2023-08-13 13:14:27 +02:00
Florian Rival
c51e6fa04e Fix warning
Don't show in changelog
2023-08-12 13:12:11 +02:00
D8H
a0ad9200cf Deprecate actions that uses an expression for resources (#5558) 2023-08-11 20:22:21 +02:00
D8H
99804f366a Allow JavaScript extensions to use resource parameters. (#5559) 2023-08-11 20:21:51 +02:00
D8H
71fead702d Make EventsFunctionParametersEditor a function component (#5554)
Don't show in changelog
2023-08-11 20:21:19 +02:00
D8H
54c9177b03 Filter autocompleted tags according to community visibility setting (#5555)
* It avoids to show tags that gives no results.
2023-08-11 10:50:09 +02:00
D8H
8157d3c9db Fix layers deletion when instances are in several external layouts (#5548) 2023-08-10 15:53:40 +02:00
github-actions[bot]
03e0da8619 Update translations [skip ci] (#5533)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2023-08-10 14:12:39 +02:00
D8H
be84b2153f Move some Tween actions in "Size" and "Visibility" groups (#5551) 2023-08-10 12:22:09 +02:00
AlexandreS
10591a41e6 Fix React callback dependency array (#5547)
Don't show in changelog
2023-08-09 16:28:12 +02:00
D8H
d41cc18be2 Fix the "Select instances on scene" menu action in external layouts (#5543) 2023-08-09 14:51:12 +02:00
AlexandreS
fb6e09d0e3 Add autosave feature for Cloud projects (#5545)
- Cloud projects are now autosaved on each game preview. Should the editor crash, it will help recover your project.
- The saved project is stored on the device for performance reasons.
- Warning: if you're using GDevelop online on a public computer, this feature saves a copy of your project in the browser storage. To make sure no one can access it, make sure to log out the editor when you leave the computer.
2023-08-08 15:55:06 +02:00
Clément Pasteau
8766f73333 Show warning and revert to automatic collision mask if deleting the last frame of a sprite object (#5542) 2023-08-08 14:26:59 +02:00
D8H
b32a9006c6 Fix the information icon color in the new behavior dialog (#5537) 2023-08-04 10:49:06 +02:00
D8H
2d613e7281 Autocomplete behavior functions on object in expressions (#5534)
* Use behavior icons in expression autocompletion.
* Fix icon colors in expression autocompletion.
2023-08-03 10:59:48 +02:00
234 changed files with 5894 additions and 2756 deletions

View File

@@ -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 {

View File

@@ -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];

View 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

View File

@@ -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;

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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();

View File

@@ -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));
}

View 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

View 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

View File

@@ -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) {

View File

@@ -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:

View File

@@ -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) {

View File

@@ -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

View File

@@ -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,

View File

@@ -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.

View File

@@ -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 {

View File

@@ -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

View File

@@ -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");
}
}

View File

@@ -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);

View File

@@ -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
View 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) == "");
}
}

View File

@@ -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();

View File

@@ -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:

View File

@@ -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);
}
}

View File

@@ -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) {}

View File

@@ -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(

View File

@@ -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 =

View File

@@ -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(

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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();
};

View File

@@ -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"),

View File

@@ -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");

View File

@@ -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');

View File

@@ -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");

View File

@@ -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");

View File

@@ -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'
)

View File

@@ -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());
}

View File

@@ -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

View File

@@ -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);

View File

@@ -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

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;',

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View File

@@ -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;

View 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;
};

View File

@@ -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;
};

View File

@@ -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>;

View File

@@ -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)
`;

View File

@@ -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>
);
};

View File

@@ -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>
);

View 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>
);
};

View File

@@ -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,
]

View File

@@ -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();
}}
/>

View File

@@ -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]

View File

@@ -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;
}
}

View File

@@ -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;
};

View File

@@ -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">

View 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;

View File

@@ -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(

View File

@@ -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>}

View File

@@ -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>

View File

@@ -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} />

View File

@@ -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"

View File

@@ -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;

View File

@@ -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}

View 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);
};

View 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.
});
});

View File

@@ -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>

View File

@@ -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(),
}));
})
);

View File

@@ -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]

View File

@@ -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: {

View File

@@ -24,6 +24,7 @@ const styles = {
},
list: {
overflowY: 'scroll',
scrollbarWidth: 'thin', // For Firefox, to avoid having a very large scrollbar.
flex: 1,
},
};

View File

@@ -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>

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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);

View File

@@ -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();

View File

@@ -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
/>

View File

@@ -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);
}

View File

@@ -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>
);
};

View File

@@ -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() && (

View File

@@ -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}

View File

@@ -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}

View File

@@ -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);
};

View File

@@ -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
);

View File

@@ -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();
});
};

View File

@@ -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';

View File

@@ -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;
};

View File

@@ -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);

View File

@@ -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',

View File

@@ -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}
/>

View File

@@ -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}

View File

@@ -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