Compare commits

..

20 Commits

Author SHA1 Message Date
Clément Pasteau
dc014e2ab0 Fix missing ID in simple text fields (#5728)
Do not show in changelog
2023-10-06 17:17:36 +02:00
Clément Pasteau
25509e1c7b Bump to 5.2.175 (#5731) 2023-10-06 17:11:16 +02:00
AlexandreS
516621e989 Fix infinite loop due to 2 items havinf the same unique id (#5729)
Don't show in changelog
2023-10-06 15:48:06 +02:00
github-actions[bot]
9b2435b210 Update translations [skip ci] (#5726)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2023-10-06 15:27:52 +02:00
Clément Pasteau
f894e1962a Split the loading of videos to ensure they are loaded without autoplay (#5723) 2023-10-06 15:06:30 +02:00
Florian Rival
cc35f51158 Fix "Add or edit variables" shown when not applicable (#5725) 2023-10-06 09:41:49 +02:00
Clément Pasteau
a7ef02f1c1 Bump to 5.2.174 (#5721) 2023-10-05 12:35:38 +02:00
github-actions[bot]
2cc7506ad7 Update translations [skip ci] (#5714)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2023-10-05 12:28:00 +02:00
D8H
608b460da7 Fix particle emitter spray cone angle being properly updated (#5720) 2023-10-05 12:19:39 +02:00
Clément Pasteau
6b082731c2 Fix dom errors because using a component inside a listItemText (#5717)
Do not show in changelog
2023-10-04 16:15:12 +02:00
Clément Pasteau
e040dd5f28 Deprecate "Always" condition (#5716)
* It was considered misleading as using it or not didn't change the condition's result
2023-10-04 15:07:45 +02:00
Clément Pasteau
ae64cfb3bb Fix crash when using Tilemaps on the web-app (#5715) 2023-10-04 14:55:29 +02:00
Clément Pasteau
02b96eb298 Fix loading video resources in Runtime (#5713) 2023-10-04 10:55:57 +02:00
Clément Pasteau
6f59a3ba03 Improve autocomplete options height on mobile (#5709) 2023-10-03 16:27:12 +02:00
github-actions[bot]
6180016b51 Update translations [skip ci] (#5706)
Co-authored-by: ClementPasteau <ClementPasteau@users.noreply.github.com>
2023-10-03 16:10:54 +02:00
Clément Pasteau
b15e72410e Fix DOM and Console errors (#5708)
Do not show in changelog
2023-10-03 15:56:34 +02:00
Clément Pasteau
d6fbb0e78b Show a link to try premium game templates (#5707) 2023-10-03 13:51:04 +02:00
Clément Pasteau
8eb9136847 Fix wrong jsdoc typing (#5705)
Do not show in changelog
2023-10-02 16:28:59 +02:00
Clément Pasteau
46c02ad9b7 Improvements indicator wording (#5702)
Do not show in changelog
2023-10-02 11:50:42 +02:00
github-actions[bot]
9846a9b45e Update translations [skip ci] (#5701)
Co-authored-by: AlexandreSi <AlexandreSi@users.noreply.github.com>
2023-10-02 11:41:41 +02:00
165 changed files with 2911 additions and 5863 deletions

View File

@@ -35,6 +35,8 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
.AddInstructionOrExpressionGroupMetadata(_("Events and control flow"))
.SetIcon("res/conditions/toujours24_black.png");
// This condition is deprecated as this does not bring anything new
// and can be confusing or misleading for beginners.
extension
.AddCondition("Always",
_("Always"),
@@ -46,7 +48,8 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
"res/conditions/toujours_black.png")
.SetHelpPath("/all-features/advanced-conditions")
.AddCodeOnlyParameter("conditionInverted", "")
.MarkAsAdvanced();
.MarkAsAdvanced()
.SetHidden();
// Compatibility with GD <= 5.0.127
extension
@@ -114,8 +117,9 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
// Compatibility with GD <= 5.0.127
extension
.AddDuplicatedCondition(
"Egal", "BuiltinCommonInstructions::CompareNumbers", {.unscoped = true})
.AddDuplicatedCondition("Egal",
"BuiltinCommonInstructions::CompareNumbers",
{.unscoped = true})
.SetHidden();
// end of compatibility code
@@ -135,8 +139,9 @@ BuiltinExtensionsImplementer::ImplementsCommonInstructionsExtension(
// Compatibility with GD <= 5.0.127
extension
.AddDuplicatedCondition(
"StrEqual", "BuiltinCommonInstructions::CompareStrings", {.unscoped = true})
.AddDuplicatedCondition("StrEqual",
"BuiltinCommonInstructions::CompareStrings",
{.unscoped = true})
.SetHidden();
// end of compatibility code

View File

@@ -17,7 +17,7 @@ EventsBasedObject::EventsBasedObject()
}
EventsBasedObject::~EventsBasedObject() {}
EventsBasedObject::EventsBasedObject(const gd::EventsBasedObject &_eventBasedObject)
: AbstractEventsBasedEntity(_eventBasedObject) {
// TODO Add a copy constructor in ObjectsContainer.
@@ -30,19 +30,14 @@ void EventsBasedObject::SerializeTo(SerializerElement& element) const {
AbstractEventsBasedEntity::SerializeTo(element);
SerializeObjectsTo(element.AddChild("objects"));
SerializeFoldersTo(element.AddChild("objectsFolderStructure"));
}
void EventsBasedObject::UnserializeFrom(gd::Project& project,
const SerializerElement& element) {
const SerializerElement& element) {
defaultName = element.GetStringAttribute("defaultName");
AbstractEventsBasedEntity::UnserializeFrom(project, element);
UnserializeObjectsFrom(project, element.GetChild("objects"));
if (element.HasChild("objectsFolderStructure")) {
UnserializeFoldersFrom(project, element.GetChild("objectsFolderStructure", 0));
}
AddMissingObjectsInRootFolder();
}
} // namespace gd

View File

@@ -294,7 +294,6 @@ void Layout::SerializeTo(SerializerElement& element) const {
GetVariables().SerializeTo(element.AddChild("variables"));
GetInitialInstances().SerializeTo(element.AddChild("instances"));
SerializeObjectsTo(element.AddChild("objects"));
SerializeFoldersTo(element.AddChild("objectsFolderStructure"));
gd::EventsListSerialization::SerializeEventsTo(events,
element.AddChild("events"));
@@ -354,11 +353,6 @@ void Layout::UnserializeFrom(gd::Project& project,
project, GetEvents(), element.GetChild("events", 0, "Events"));
UnserializeObjectsFrom(project, element.GetChild("objects", 0, "Objets"));
if (element.HasChild("objectsFolderStructure")) {
UnserializeFoldersFrom(project, element.GetChild("objectsFolderStructure", 0));
}
AddMissingObjectsInRootFolder();
initialInstances.UnserializeFrom(
element.GetChild("instances", 0, "Positions"));
variables.UnserializeFrom(element.GetChild("variables", 0, "Variables"));

View File

@@ -40,6 +40,7 @@ void Object::Init(const gd::Object& object) {
name = object.name;
assetStoreId = object.assetStoreId;
objectVariables = object.objectVariables;
tags = object.tags;
effectsContainer = object.effectsContainer;
behaviors.clear();
@@ -133,6 +134,7 @@ void Object::UnserializeFrom(gd::Project& project,
SetType(element.GetStringAttribute("type"));
assetStoreId = element.GetStringAttribute("assetStoreId");
name = element.GetStringAttribute("name", name, "nom");
tags = element.GetStringAttribute("tags");
objectVariables.UnserializeFrom(
element.GetChild("variables", 0, "Variables"));
@@ -205,6 +207,7 @@ void Object::SerializeTo(SerializerElement& element) const {
element.SetAttribute("name", GetName());
element.SetAttribute("assetStoreId", GetAssetStoreId());
element.SetAttribute("type", GetType());
element.SetAttribute("tags", GetTags());
objectVariables.SerializeTo(element.AddChild("variables"));
effectsContainer.SerializeTo(element.AddChild("effects"));

View File

@@ -120,6 +120,14 @@ class GD_CORE_API Object {
*/
const gd::String& GetType() const { return configuration->GetType(); }
/** \brief Change the tags of the object.
*/
void SetTags(const gd::String& tags_) { tags = tags_; }
/** \brief Return the tags of the object.
*/
const gd::String& GetTags() const { return tags; }
/** \brief Shortcut to check if the object is a 3D object.
*/
bool Is3DObject() const { return configuration->Is3DObject(); }
@@ -260,6 +268,7 @@ class GD_CORE_API Object {
///< object.
gd::VariablesContainer
objectVariables; ///< List of the variables of the object
gd::String tags; ///< Comma-separated list of tags
gd::EffectsContainer
effectsContainer; ///< The effects container for the object.
mutable gd::String persistentUuid; ///< A persistent random version 4 UUID,

View File

@@ -1,244 +0,0 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#include "GDCore/Project/ObjectFolderOrObject.h"
#include <memory>
#include "GDCore/Project/Object.h"
#include "GDCore/Project/ObjectsContainer.h"
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/Tools/Log.h"
using namespace std;
namespace gd {
ObjectFolderOrObject ObjectFolderOrObject::badObjectFolderOrObject;
ObjectFolderOrObject::ObjectFolderOrObject()
: folderName("__NULL"), object(nullptr) {}
ObjectFolderOrObject::ObjectFolderOrObject(gd::String folderName_,
ObjectFolderOrObject* parent_)
: folderName(folderName_), parent(parent_), object(nullptr) {}
ObjectFolderOrObject::ObjectFolderOrObject(gd::Object* object_,
ObjectFolderOrObject* parent_)
: object(object_), parent(parent_) {}
ObjectFolderOrObject::~ObjectFolderOrObject() {}
bool ObjectFolderOrObject::HasObjectNamed(const gd::String& name) {
if (IsFolder()) {
return std::any_of(
children.begin(),
children.end(),
[&name](
std::unique_ptr<gd::ObjectFolderOrObject>& objectFolderOrObject) {
return objectFolderOrObject->HasObjectNamed(name);
});
}
if (!object) return false;
return object->GetName() == name;
}
ObjectFolderOrObject& ObjectFolderOrObject::GetObjectNamed(
const gd::String& name) {
if (object && object->GetName() == name) {
return *this;
}
if (IsFolder()) {
for (std::size_t j = 0; j < children.size(); j++) {
ObjectFolderOrObject& foundInChild = children[j]->GetObjectNamed(name);
if (&(foundInChild) != &badObjectFolderOrObject) {
return foundInChild;
}
}
}
return badObjectFolderOrObject;
}
void ObjectFolderOrObject::SetFolderName(const gd::String& name) {
if (!IsFolder()) return;
folderName = name;
}
ObjectFolderOrObject& ObjectFolderOrObject::GetChildAt(std::size_t index) {
if (index >= children.size()) return badObjectFolderOrObject;
return *children[index];
}
ObjectFolderOrObject& ObjectFolderOrObject::GetObjectChild(
const gd::String& name) {
for (std::size_t j = 0; j < children.size(); j++) {
if (!children[j]->IsFolder()) {
if (children[j]->GetObject().GetName() == name) return *children[j];
};
}
return badObjectFolderOrObject;
}
void ObjectFolderOrObject::InsertObject(gd::Object* insertedObject,
std::size_t position) {
auto objectFolderOrObject =
gd::make_unique<ObjectFolderOrObject>(insertedObject, this);
if (position < children.size()) {
children.insert(children.begin() + position,
std::move(objectFolderOrObject));
} else {
children.push_back(std::move(objectFolderOrObject));
}
}
std::size_t ObjectFolderOrObject::GetChildPosition(
const ObjectFolderOrObject& child) const {
for (std::size_t j = 0; j < children.size(); j++) {
if (children[j].get() == &child) return j;
}
return gd::String::npos;
}
ObjectFolderOrObject& ObjectFolderOrObject::InsertNewFolder(
const gd::String& newFolderName, std::size_t position) {
auto newFolderPtr =
gd::make_unique<ObjectFolderOrObject>(newFolderName, this);
gd::ObjectFolderOrObject& newFolder = *(*(children.insert(
position < children.size() ? children.begin() + position : children.end(),
std::move(newFolderPtr))));
return newFolder;
};
void ObjectFolderOrObject::RemoveRecursivelyObjectNamed(
const gd::String& name) {
if (IsFolder()) {
children.erase(
std::remove_if(children.begin(),
children.end(),
[&name](std::unique_ptr<gd::ObjectFolderOrObject>&
objectFolderOrObject) {
return !objectFolderOrObject->IsFolder() &&
objectFolderOrObject->GetObject().GetName() ==
name;
}),
children.end());
for (auto& it : children) {
it->RemoveRecursivelyObjectNamed(name);
}
}
};
bool ObjectFolderOrObject::IsADescendantOf(
const ObjectFolderOrObject& otherObjectFolderOrObject) {
if (parent == nullptr) return false;
if (&(*parent) == &otherObjectFolderOrObject) return true;
return parent->IsADescendantOf(otherObjectFolderOrObject);
}
void ObjectFolderOrObject::MoveChild(std::size_t oldIndex,
std::size_t newIndex) {
if (!IsFolder()) return;
if (oldIndex >= children.size() || newIndex >= children.size()) return;
std::unique_ptr<gd::ObjectFolderOrObject> objectFolderOrObject =
std::move(children[oldIndex]);
children.erase(children.begin() + oldIndex);
children.insert(children.begin() + newIndex, std::move(objectFolderOrObject));
}
void ObjectFolderOrObject::RemoveFolderChild(
const ObjectFolderOrObject& childToRemove) {
if (!IsFolder() || !childToRemove.IsFolder() ||
childToRemove.GetChildrenCount() > 0) {
return;
}
std::vector<std::unique_ptr<gd::ObjectFolderOrObject>>::iterator it = find_if(
children.begin(),
children.end(),
[&childToRemove](std::unique_ptr<gd::ObjectFolderOrObject>& child) {
return child.get() == &childToRemove;
});
if (it == children.end()) return;
children.erase(it);
}
void ObjectFolderOrObject::MoveObjectFolderOrObjectToAnotherFolder(
gd::ObjectFolderOrObject& objectFolderOrObject,
gd::ObjectFolderOrObject& newParentFolder,
std::size_t newPosition) {
if (!newParentFolder.IsFolder()) return;
if (newParentFolder.IsADescendantOf(objectFolderOrObject)) return;
std::vector<std::unique_ptr<gd::ObjectFolderOrObject>>::iterator it =
find_if(children.begin(),
children.end(),
[&objectFolderOrObject](std::unique_ptr<gd::ObjectFolderOrObject>&
childObjectFolderOrObject) {
return childObjectFolderOrObject.get() == &objectFolderOrObject;
});
if (it == children.end()) return;
std::unique_ptr<gd::ObjectFolderOrObject> objectFolderOrObjectPtr =
std::move(*it);
children.erase(it);
objectFolderOrObjectPtr->parent = &newParentFolder;
newParentFolder.children.insert(
newPosition < newParentFolder.children.size()
? newParentFolder.children.begin() + newPosition
: newParentFolder.children.end(),
std::move(objectFolderOrObjectPtr));
}
void ObjectFolderOrObject::SerializeTo(SerializerElement& element) const {
if (IsFolder()) {
element.SetAttribute("folderName", GetFolderName());
if (children.size() > 0) {
SerializerElement& childrenElement = element.AddChild("children");
childrenElement.ConsiderAsArrayOf("objectFolderOrObject");
for (std::size_t j = 0; j < children.size(); j++) {
children[j]->SerializeTo(
childrenElement.AddChild("objectFolderOrObject"));
}
}
} else {
element.SetAttribute("objectName", GetObject().GetName());
}
}
void ObjectFolderOrObject::UnserializeFrom(
gd::Project& project,
const SerializerElement& element,
gd::ObjectsContainer& objectsContainer) {
children.clear();
gd::String potentialFolderName = element.GetStringAttribute("folderName", "");
if (!potentialFolderName.empty()) {
object = nullptr;
folderName = potentialFolderName;
if (element.HasChild("children")) {
const SerializerElement& childrenElements =
element.GetChild("children", 0);
childrenElements.ConsiderAsArrayOf("objectFolderOrObject");
for (std::size_t i = 0; i < childrenElements.GetChildrenCount(); ++i) {
std::unique_ptr<ObjectFolderOrObject> childObjectFolderOrObject =
make_unique<ObjectFolderOrObject>();
childObjectFolderOrObject->UnserializeFrom(
project, childrenElements.GetChild(i), objectsContainer);
childObjectFolderOrObject->parent = this;
children.push_back(std::move(childObjectFolderOrObject));
}
}
} else {
folderName = "";
gd::String objectName = element.GetStringAttribute("objectName");
if (objectsContainer.HasObjectNamed(objectName)) {
object = &objectsContainer.GetObject(objectName);
} else {
gd::LogError("Object with name " + objectName +
" not found in objects container.");
object = nullptr;
}
}
};
} // namespace gd

View File

@@ -1,199 +0,0 @@
/*
* GDevelop Core
* Copyright 2008-2023 Florian Rival (Florian.Rival@gmail.com). All rights
* reserved. This project is released under the MIT License.
*/
#ifndef GDCORE_OBJECTFOLDEROROBJECT_H
#define GDCORE_OBJECTFOLDEROROBJECT_H
#include <memory>
#include <vector>
#include "GDCore/Serialization/SerializerElement.h"
#include "GDCore/String.h"
namespace gd {
class Project;
class Object;
class SerializerElement;
class ObjectsContainer;
} // namespace gd
namespace gd {
/**
* \brief Class representing a folder structure in order to organize objects
* in folders (to be used with an ObjectsContainer.)
*
* \see gd::ObjectsContainer
*/
class GD_CORE_API ObjectFolderOrObject {
public:
/**
* \brief Default constructor creating an empty instance. Useful for the null
* object pattern.
*/
ObjectFolderOrObject();
virtual ~ObjectFolderOrObject();
/**
* \brief Constructor for creating an instance representing a folder.
*/
ObjectFolderOrObject(gd::String folderName_,
ObjectFolderOrObject* parent_ = nullptr);
/**
* \brief Constructor for creating an instance representing an object.
*/
ObjectFolderOrObject(gd::Object* object_,
ObjectFolderOrObject* parent_ = nullptr);
/**
* \brief Returns the object behind the instance.
*/
gd::Object& GetObject() const { return *object; }
/**
* \brief Returns true if the instance represents a folder.
*/
bool IsFolder() const { return !folderName.empty(); }
/**
* \brief Returns the name of the folder.
*/
const gd::String& GetFolderName() const { return folderName; }
/**
* \brief Set the folder name. Does nothing if called on an instance not
* representing a folder.
*/
void SetFolderName(const gd::String& name);
/**
* \brief Returns true if the instance represents the object with the given
* name or if any of the children does (recursive search).
*/
bool HasObjectNamed(const gd::String& name);
/**
* \brief Returns the child instance holding the object with the given name
* (recursive search).
*/
ObjectFolderOrObject& GetObjectNamed(const gd::String& name);
/**
* \brief Returns the number of children. Returns 0 if the instance represents
* an object.
*/
std::size_t GetChildrenCount() const {
if (IsFolder()) return children.size();
return 0;
}
/**
* \brief Returns the child ObjectFolderOrObject at the given index.
*/
ObjectFolderOrObject& GetChildAt(std::size_t index);
/**
* \brief Returns the child ObjectFolderOrObject that represents the object
* with the given name. To use only if sure that the instance holds the object
* in its direct children (no recursive search).
*
* \note The equivalent method to get a folder by its name cannot be
* implemented because there is no unicity enforced on the folder name.
*/
ObjectFolderOrObject& GetObjectChild(const gd::String& name);
/**
* \brief Returns the parent of the instance. If the instance has no parent
* (root folder), the null object is returned.
*/
ObjectFolderOrObject& GetParent() {
if (parent == nullptr) {
return badObjectFolderOrObject;
}
return *parent;
};
/**
* \brief Returns true if the instance is a root folder (that's to say it
* has no parent).
*/
bool IsRootFolder() { return !object && !parent; }
/**
* \brief Moves a child from a position to a new one.
*/
void MoveChild(std::size_t oldIndex, std::size_t newIndex);
/**
* \brief Removes the given child from the instance's children. If the given
* child contains children of its own, does nothing.
*/
void RemoveFolderChild(const ObjectFolderOrObject& childToRemove);
/**
* \brief Removes the child representing the object with the given name from
* the instance children and recursively does it for every folder children.
*/
void RemoveRecursivelyObjectNamed(const gd::String& name);
/**
* \brief Inserts an instance representing the given object at the given
* position.
*/
void InsertObject(gd::Object* insertedObject,
std::size_t position = (size_t)-1);
/**
* \brief Inserts an instance representing a folder with the given name at the
* given position.
*/
ObjectFolderOrObject& InsertNewFolder(const gd::String& newFolderName,
std::size_t position);
/**
* \brief Returns true if the instance is a descendant of the given instance
* of ObjectFolderOrObject.
*/
bool IsADescendantOf(const ObjectFolderOrObject& otherObjectFolderOrObject);
/**
* \brief Returns the position of the given instance of ObjectFolderOrObject
* in the instance's children.
*/
std::size_t GetChildPosition(const ObjectFolderOrObject& child) const;
/**
* \brief Moves the given child ObjectFolderOrObject to the given folder at
* the given position.
*/
void MoveObjectFolderOrObjectToAnotherFolder(
gd::ObjectFolderOrObject& objectFolderOrObject,
gd::ObjectFolderOrObject& newParentFolder,
std::size_t newPosition);
/** \name Saving and loading
* Members functions related to saving and loading the objects of the class.
*/
///@{
/**
* \brief Serialize the ObjectFolderOrObject instance.
*/
void SerializeTo(SerializerElement& element) const;
/**
* \brief Unserialize the ObjectFolderOrObject instance.
*/
void UnserializeFrom(gd::Project& project,
const SerializerElement& element,
ObjectsContainer& objectsContainer);
///@}
private:
static gd::ObjectFolderOrObject badObjectFolderOrObject;
gd::ObjectFolderOrObject*
parent; // nullptr if root folder, points to the parent folder otherwise.
// Representing an object:
gd::Object* object; // nullptr if folderName is set.
// or representing a folder:
gd::String folderName; // Empty if object is set.
std::vector<std::unique_ptr<ObjectFolderOrObject>>
children; // Folder children.
};
} // namespace gd
#endif // GDCORE_OBJECTFOLDEROROBJECT_H

View File

@@ -9,15 +9,12 @@
#include "GDCore/Extensions/Platform.h"
#include "GDCore/Project/Object.h"
#include "GDCore/Project/ObjectFolderOrObject.h"
#include "GDCore/Project/Project.h"
#include "GDCore/Serialization/SerializerElement.h"
namespace gd {
ObjectsContainer::ObjectsContainer() {
rootFolder = gd::make_unique<gd::ObjectFolderOrObject>("__ROOT");
}
ObjectsContainer::ObjectsContainer() {}
ObjectsContainer::~ObjectsContainer() {}
@@ -27,22 +24,6 @@ void ObjectsContainer::SerializeObjectsTo(SerializerElement& element) const {
initialObjects[j]->SerializeTo(element.AddChild("object"));
}
}
void ObjectsContainer::SerializeFoldersTo(SerializerElement& element) const {
rootFolder->SerializeTo(element);
}
void ObjectsContainer::UnserializeFoldersFrom(
gd::Project& project, const SerializerElement& element) {
rootFolder->UnserializeFrom(project, element, *this);
}
void ObjectsContainer::AddMissingObjectsInRootFolder() {
for (std::size_t i = 0; i < initialObjects.size(); ++i) {
if (!rootFolder->HasObjectNamed(initialObjects[i]->GetName())) {
rootFolder->InsertObject(&(*initialObjects[i]));
}
}
}
void ObjectsContainer::UnserializeObjectsFrom(
gd::Project& project, const SerializerElement& element) {
@@ -72,9 +53,7 @@ bool ObjectsContainer::HasObjectNamed(const gd::String& name) const {
gd::Object& ObjectsContainer::GetObject(const gd::String& name) {
return *(*find_if(initialObjects.begin(),
initialObjects.end(),
[&name](std::unique_ptr<gd::Object>& object) {
return object->GetName() == name;
}));
bind2nd(gd::ObjectHasName(), name)));
}
const gd::Object& ObjectsContainer::GetObject(const gd::String& name) const {
return *(*find_if(initialObjects.begin(),
@@ -105,22 +84,6 @@ gd::Object& ObjectsContainer::InsertNewObject(const gd::Project& project,
: initialObjects.end(),
project.CreateObject(objectType, name))));
rootFolder->InsertObject(&newlyCreatedObject);
return newlyCreatedObject;
}
gd::Object& ObjectsContainer::InsertNewObjectInFolder(
const gd::Project& project,
const gd::String& objectType,
const gd::String& name,
gd::ObjectFolderOrObject& objectFolderOrObject,
std::size_t position) {
gd::Object& newlyCreatedObject = *(*(initialObjects.insert(
initialObjects.end(), project.CreateObject(objectType, name))));
objectFolderOrObject.InsertObject(&newlyCreatedObject, position);
return newlyCreatedObject;
}
@@ -134,6 +97,16 @@ gd::Object& ObjectsContainer::InsertObject(const gd::Object& object,
return newlyCreatedObject;
}
void ObjectsContainer::SwapObjects(std::size_t firstObjectIndex,
std::size_t secondObjectIndex) {
if (firstObjectIndex >= initialObjects.size() ||
secondObjectIndex >= initialObjects.size())
return;
std::iter_swap(initialObjects.begin() + firstObjectIndex,
initialObjects.begin() + secondObjectIndex);
}
void ObjectsContainer::MoveObject(std::size_t oldIndex, std::size_t newIndex) {
if (oldIndex >= initialObjects.size() || newIndex >= initialObjects.size())
return;
@@ -147,38 +120,30 @@ void ObjectsContainer::RemoveObject(const gd::String& name) {
std::vector<std::unique_ptr<gd::Object>>::iterator objectIt =
find_if(initialObjects.begin(),
initialObjects.end(),
[&name](std::unique_ptr<gd::Object>& object) {
return object->GetName() == name;
});
bind2nd(ObjectHasName(), name));
if (objectIt == initialObjects.end()) return;
rootFolder->RemoveRecursivelyObjectNamed(name);
initialObjects.erase(objectIt);
}
void ObjectsContainer::MoveObjectFolderOrObjectToAnotherContainerInFolder(
gd::ObjectFolderOrObject& objectFolderOrObject,
void ObjectsContainer::MoveObjectToAnotherContainer(
const gd::String& name,
gd::ObjectsContainer& newContainer,
gd::ObjectFolderOrObject& newParentFolder,
std::size_t newPosition) {
if (objectFolderOrObject.IsFolder() || !newParentFolder.IsFolder()) return;
std::vector<std::unique_ptr<gd::Object>>::iterator objectIt = find_if(
initialObjects.begin(),
initialObjects.end(),
[&objectFolderOrObject](std::unique_ptr<gd::Object>& object) {
return object->GetName() == objectFolderOrObject.GetObject().GetName();
});
std::vector<std::unique_ptr<gd::Object>>::iterator objectIt =
find_if(initialObjects.begin(),
initialObjects.end(),
bind2nd(ObjectHasName(), name));
if (objectIt == initialObjects.end()) return;
std::unique_ptr<gd::Object> object = std::move(*objectIt);
initialObjects.erase(objectIt);
newContainer.initialObjects.push_back(std::move(object));
objectFolderOrObject.GetParent().MoveObjectFolderOrObjectToAnotherFolder(
objectFolderOrObject, newParentFolder, newPosition);
newContainer.initialObjects.insert(
newPosition < newContainer.initialObjects.size()
? newContainer.initialObjects.begin() + newPosition
: newContainer.initialObjects.end(),
std::move(object));
}
} // namespace gd

View File

@@ -9,12 +9,11 @@
#include <vector>
#include "GDCore/String.h"
#include "GDCore/Project/ObjectGroupsContainer.h"
#include "GDCore/Project/ObjectFolderOrObject.h"
namespace gd {
class Object;
class Project;
class SerializerElement;
} // namespace gd
}
#undef GetObject // Disable an annoying macro
namespace gd {
@@ -99,19 +98,6 @@ class GD_CORE_API ObjectsContainer {
const gd::String& objectType,
const gd::String& name,
std::size_t position);
/**
* \brief Add a new empty object of type \a objectType called \a name in the
* given folder at the specified position.<br>
*
* \note The object is created using the project's current platform.
* \return A reference to the object in the list.
*/
gd::Object& InsertNewObjectInFolder(
const gd::Project& project,
const gd::String& objectType,
const gd::String& name,
gd::ObjectFolderOrObject& objectFolderOrObject,
std::size_t position);
/**
* \brief Add a new object to the list
@@ -139,18 +125,18 @@ class GD_CORE_API ObjectsContainer {
void MoveObject(std::size_t oldIndex, std::size_t newIndex);
/**
* Move the specified object to another container, removing it from the
* current one and adding it to the new one at the specified position in the
* given folder.
*
* \note This does not invalidate the references to the object (object is not
* moved in memory, as referenced by smart pointers internally).
* \brief Swap the position of the specified objects.
*/
void MoveObjectFolderOrObjectToAnotherContainerInFolder(
gd::ObjectFolderOrObject& objectFolderOrObject,
gd::ObjectsContainer& newContainer,
gd::ObjectFolderOrObject& newParentFolder,
std::size_t newPosition);
void SwapObjects(std::size_t firstObjectIndex, std::size_t secondObjectIndex);
/**
* Move the specified object to another container, removing it from the current one
* and adding it to the new one at the specified position.
*
* \note This does not invalidate the references to the object (object is not moved in memory,
* as referenced by smart pointers internally).
*/
void MoveObjectToAnotherContainer(const gd::String& name, gd::ObjectsContainer & newContainer, std::size_t newPosition);
/**
* Provide a raw access to the vector containing the objects
@@ -167,36 +153,20 @@ class GD_CORE_API ObjectsContainer {
}
///@}
gd::ObjectFolderOrObject& GetRootFolder() {
return *rootFolder;
}
void AddMissingObjectsInRootFolder();
/** \name Saving and loading
* Members functions related to saving and loading the objects of the class.
*/
///@{
/**
* \brief Serialize the objects container.
* \brief Serialize instances container.
*/
void SerializeObjectsTo(SerializerElement& element) const;
/**
* \brief Unserialize the objects container.
* \brief Unserialize the instances container.
*/
void UnserializeObjectsFrom(gd::Project& project,
const SerializerElement& element);
/**
* \brief Serialize folder structure.
*/
void SerializeFoldersTo(SerializerElement& element) const;
/**
* \brief Unserialize folder structure.
*/
void UnserializeFoldersFrom(gd::Project& project,
const SerializerElement& element);
///@}
/** \name Objects groups management
@@ -220,9 +190,6 @@ class GD_CORE_API ObjectsContainer {
std::vector<std::unique_ptr<gd::Object> >
initialObjects; ///< Objects contained.
gd::ObjectGroupsContainer objectGroups;
private:
std::unique_ptr<gd::ObjectFolderOrObject> rootFolder;
};
} // namespace gd

View File

@@ -125,7 +125,7 @@ Project::CreateObject(const gd::String &objectType, const gd::String &name) cons
}
}
return std::move(object);
}
@@ -849,11 +849,6 @@ void Project::UnserializeFrom(const SerializerElement& element) {
resourcesManager.UnserializeFrom(
element.GetChild("resources", 0, "Resources"));
UnserializeObjectsFrom(*this, element.GetChild("objects", 0, "Objects"));
if (element.HasChild("objectsFolderStructure")) {
UnserializeFoldersFrom(*this, element.GetChild("objectsFolderStructure", 0));
}
AddMissingObjectsInRootFolder();
GetVariables().UnserializeFrom(element.GetChild("variables", 0, "Variables"));
scenes.clear();
@@ -1005,7 +1000,6 @@ void Project::SerializeTo(SerializerElement& element) const {
resourcesManager.SerializeTo(element.AddChild("resources"));
SerializeObjectsTo(element.AddChild("objects"));
SerializeFoldersTo(element.AddChild("objectsFolderStructure"));
GetObjectGroups().SerializeTo(element.AddChild("objectsGroups"));
GetVariables().SerializeTo(element.AddChild("variables"));

View File

@@ -243,8 +243,8 @@ namespace gdjs {
setAngle(angle1: float, angle2: float): void {
// Access private members of the behavior to apply changes right away.
const behavior: any = this.emitter.getBehavior('rotation');
behavior.minStart = angle1;
behavior.maxStart = angle2;
behavior.minStart = gdjs.toRad(angle1);
behavior.maxStart = gdjs.toRad(angle2);
}
setForce(min: float, max: float): void {
@@ -311,8 +311,8 @@ namespace gdjs {
setParticleRotationSpeed(min: float, max: float): void {
// Access private members of the behavior to apply changes right away.
const behavior: any = this.emitter.getBehavior('rotation');
behavior.minSpeed = min;
behavior.maxSpeed = max;
behavior.minSpeed = gdjs.toRad(min);
behavior.maxSpeed = gdjs.toRad(max);
}
setMaxParticlesCount(count: float): void {

View File

@@ -105,11 +105,7 @@ const defineTileMap = function (
.setType('resource')
.addExtraInfo('image')
.setLabel(_('Atlas image'))
.setDescription(
_(
'The Atlas image containing the tileset.'
)
)
.setDescription(_('The Atlas image containing the tileset.'))
.setGroup(_('Tiled only'))
);
objectProperties.set(
@@ -1128,6 +1124,15 @@ module.exports = {
'pako/dist/pako.min'
);
// When on the webapp, and using webpack, the extension does not seem to
// be able to register itself properly. So we do it manually.
// (This should be done here https://github.com/pixijs/tilemap/blob/master/src/index.ts#L43-L47)
PIXI.extensions.add({
name: 'tilemap',
type: PIXI.ExtensionType.RendererPlugin,
ref: Tilemap.TileRenderer,
});
/**
* Renderer for instances of TileMap inside the IDE.
*
@@ -1342,10 +1347,11 @@ module.exports = {
tilesetJsonFile
) {
try {
const tileMapJsonData = await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilemapJsonFile
);
const tileMapJsonData =
await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilemapJsonFile
);
const tileMap = TilemapHelper.TileMapManager.identify(tileMapJsonData);
@@ -1375,8 +1381,7 @@ module.exports = {
RenderedTileMapInstance.prototype.update = function () {
if (this._instance.hasCustomSize()) {
this._pixiObject.scale.x = this.getCustomWidth() / this.width;
this._pixiObject.scale.y =
this.getCustomHeight() / this.height;
this._pixiObject.scale.y = this.getCustomHeight() / this.height;
} else {
this._pixiObject.scale.x = 1;
this._pixiObject.scale.y = 1;
@@ -1595,10 +1600,11 @@ module.exports = {
tilesetJsonFile
) {
try {
const tileMapJsonData = await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilemapJsonFile
);
const tileMapJsonData =
await this._pixiResourcesLoader.getResourceJsonData(
this._project,
tilemapJsonFile
);
const tileMap = TilemapHelper.TileMapManager.identify(tileMapJsonData);
@@ -1627,8 +1633,7 @@ module.exports = {
RenderedCollisionMaskInstance.prototype.update = function () {
if (this._instance.hasCustomSize()) {
this._pixiObject.scale.x = this.getCustomWidth() / this.width;
this._pixiObject.scale.y =
this.getCustomHeight() / this.height;
this._pixiObject.scale.y = this.getCustomHeight() / this.height;
} else {
this._pixiObject.scale.x = 1;
this._pixiObject.scale.y = 1;

View File

@@ -26,12 +26,6 @@ namespace gdjs {
.getImageManager()
.getPIXIVideoTexture(this._object._videoResource)
);
this._pixiObject._texture.baseTexture.resource.autoPlay = false;
// Needed to avoid video not playing/crashing in Chrome/Chromium browsers.
// See https://github.com/pixijs/pixi.js/issues/5996
this._pixiObject._texture.baseTexture.resource.source.preload = 'auto';
this._pixiObject._texture.baseTexture.resource.source.autoload = true;
// Will be set to true when video texture is loaded.
instanceContainer

View File

@@ -95,7 +95,7 @@ namespace gdjs {
setResources(resourceDataArray: ResourceData[]): void {
this._resources.clear();
for (const resourceData of resourceDataArray) {
if (resourceData.kind === 'image') {
if (resourceData.kind === 'image' || resourceData.kind === 'video') {
this._resources.set(resourceData.name, resourceData);
}
}
@@ -285,6 +285,7 @@ namespace gdjs {
crossorigin: this._resourcesLoader.checkIfCredentialsRequired(file)
? 'use-credentials'
: 'anonymous',
autoPlay: false,
},
}
).on('error', (error) => {
@@ -315,19 +316,52 @@ namespace gdjs {
await Promise.all(
[...this._resources.values()].map(async (resource) => {
try {
PIXI.Assets.setPreferences({
preferWorkers: false,
preferCreateImageBitmap: false,
crossOrigin: this._resourcesLoader.checkIfCredentialsRequired(
resource.file
)
? 'use-credentials'
: 'anonymous',
});
const loadedTexture = await PIXI.Assets.load(resource.file);
this._loadedTextures.put(resource.name, loadedTexture);
// TODO What if 2 assets share the same file with different settings?
applyTextureSettings(loadedTexture, resource);
if (resource.kind === 'video') {
// For videos, we want to preload them so they are available as soon as we want to use them.
// We cannot use Pixi.assets.load() as it does not allow passing options (autoplay) to the resource loader.
// Pixi.Texture.from() does not return a promise, so we need to ensure we look at the 'loaded' event of the baseTexture,
// to continue, otherwise if we try to play the video too soon (at the beginning of scene for instance),
// it will fail.
await new Promise<void>((resolve, reject) => {
const texture = PIXI.Texture.from(
this._resourcesLoader.getFullUrl(resource.file),
{
resourceOptions: {
crossOrigin: this._resourcesLoader.checkIfCredentialsRequired(
resource.file
)
? 'use-credentials'
: 'anonymous',
autoPlay: false,
},
}
).on('error', (error) => {
reject(error);
});
const baseTexture = texture.baseTexture;
baseTexture.on('loaded', () => {
this._loadedTextures.put(resource.name, texture);
applyTextureSettings(texture, resource);
resolve();
});
});
} else {
PIXI.Assets.setPreferences({
preferWorkers: false,
preferCreateImageBitmap: false,
crossOrigin: this._resourcesLoader.checkIfCredentialsRequired(
resource.file
)
? 'use-credentials'
: 'anonymous',
});
const loadedTexture = await PIXI.Assets.load(resource.file);
this._loadedTextures.put(resource.name, loadedTexture);
// TODO What if 2 assets share the same file with different settings?
applyTextureSettings(loadedTexture, resource);
}
} catch (error) {
logFileLoadingError(resource.file, error);
}

View File

@@ -396,42 +396,21 @@ interface Watermark {
void UnserializeFrom([Const, Ref] SerializerElement element);
};
interface ObjectFolderOrObject {
void ObjectFolderOrObject();
boolean IsFolder();
boolean IsRootFolder();
[Ref] gdObject GetObject();
[Const, Ref] DOMString GetFolderName();
void SetFolderName([Const] DOMString name);
boolean HasObjectNamed([Const] DOMString name);
[Ref] ObjectFolderOrObject GetObjectNamed([Const] DOMString name);
unsigned long GetChildrenCount();
[Ref] ObjectFolderOrObject GetChildAt(unsigned long pos);
[Ref] ObjectFolderOrObject GetObjectChild([Const] DOMString name);
unsigned long GetChildPosition([Const, Ref] ObjectFolderOrObject child);
[Ref] ObjectFolderOrObject GetParent();
[Ref] ObjectFolderOrObject InsertNewFolder([Const] DOMString name, unsigned long newPosition);
void MoveObjectFolderOrObjectToAnotherFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectFolderOrObject newParentFolder, unsigned long newPosition);
void MoveChild(unsigned long oldIndex, unsigned long newIndex);
void RemoveFolderChild([Const, Ref] ObjectFolderOrObject childToRemove);
boolean IsADescendantOf([Const, Ref] ObjectFolderOrObject otherObjectFolderOrObject);
};
interface ObjectsContainer {
void ObjectsContainer();
[Ref] gdObject InsertNewObject([Ref] Project project, [Const] DOMString type, [Const] DOMString name, unsigned long pos);
[Ref] gdObject InsertNewObjectInFolder([Ref] Project project, [Const] DOMString type, [Const] DOMString name, [Ref] ObjectFolderOrObject folder, unsigned long pos);
[Ref] gdObject InsertObject([Const, Ref] gdObject obj, unsigned long pos);
boolean HasObjectNamed([Const] DOMString name);
[Ref] gdObject GetObject([Const] DOMString name);
[Ref] gdObject GetObjectAt(unsigned long pos);
unsigned long GetObjectPosition([Const] DOMString name);
void RemoveObject([Const] DOMString name);
void SwapObjects(unsigned long first, unsigned long second);
void MoveObject(unsigned long oldIndex, unsigned long newIndex);
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
void MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
unsigned long GetObjectsCount();
[Ref] ObjectFolderOrObject GetRootFolder();
[Ref] ObjectGroupsContainer GetObjectGroups();
};
@@ -565,17 +544,17 @@ interface Project {
//Inherited from gd::ObjectsContainer
[Ref] gdObject InsertNewObject([Ref] Project project, [Const] DOMString type, [Const] DOMString name, unsigned long pos);
[Ref] gdObject InsertNewObjectInFolder([Ref] Project project, [Const] DOMString type, [Const] DOMString name, [Ref] ObjectFolderOrObject folder, unsigned long pos);
[Ref] gdObject InsertObject([Const, Ref] gdObject obj, unsigned long pos);
boolean HasObjectNamed([Const] DOMString name);
[Ref] gdObject GetObject([Const] DOMString name);
[Ref] gdObject GetObjectAt(unsigned long pos);
unsigned long GetObjectPosition([Const] DOMString name);
void RemoveObject([Const] DOMString name);
void SwapObjects(unsigned long first, unsigned long second);
void MoveObject(unsigned long oldIndex, unsigned long newIndex);
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
void MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
unsigned long GetObjectsCount();
[Ref] ObjectFolderOrObject GetRootFolder();
[Ref] ObjectGroupsContainer GetObjectGroups();
};
@@ -708,6 +687,8 @@ interface gdObject {
[Const, Ref] DOMString GetAssetStoreId();
void SetType([Const] DOMString type);
[Const, Ref] DOMString GetType();
void SetTags([Const] DOMString tags);
[Const, Ref] DOMString GetTags();
boolean Is3DObject();
[Ref] ObjectConfiguration GetConfiguration();
@@ -809,17 +790,17 @@ interface Layout {
//Inherited from gd::ObjectsContainer
[Ref] gdObject InsertNewObject([Ref] Project project, [Const] DOMString type, [Const] DOMString name, unsigned long pos);
[Ref] gdObject InsertNewObjectInFolder([Ref] Project project, [Const] DOMString type, [Const] DOMString name, [Ref] ObjectFolderOrObject folder, unsigned long pos);
[Ref] gdObject InsertObject([Const, Ref] gdObject obj, unsigned long pos);
boolean HasObjectNamed([Const] DOMString name);
[Ref] gdObject GetObject([Const] DOMString name);
[Ref] gdObject GetObjectAt(unsigned long pos);
unsigned long GetObjectPosition([Const] DOMString name);
void RemoveObject([Const] DOMString name);
void SwapObjects(unsigned long first, unsigned long second);
void MoveObject(unsigned long oldIndex, unsigned long newIndex);
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
void MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
unsigned long GetObjectsCount();
[Ref] ObjectFolderOrObject GetRootFolder();
[Ref] ObjectGroupsContainer GetObjectGroups();
};
@@ -2768,18 +2749,17 @@ interface EventsBasedObject {
// Inherited from gd::ObjectsContainer
[Ref] gdObject InsertNewObject([Ref] Project project, [Const] DOMString type, [Const] DOMString name, unsigned long pos);
[Ref] gdObject InsertNewObjectInFolder([Ref] Project project, [Const] DOMString type, [Const] DOMString name, [Ref] ObjectFolderOrObject folder, unsigned long pos);
[Ref] gdObject InsertObject([Const, Ref] gdObject obj, unsigned long pos);
boolean HasObjectNamed([Const] DOMString name);
[Ref] gdObject GetObject([Const] DOMString name);
[Ref] gdObject GetObjectAt(unsigned long pos);
unsigned long GetObjectPosition([Const] DOMString name);
void RemoveObject([Const] DOMString name);
void SwapObjects(unsigned long first, unsigned long second);
void MoveObject(unsigned long oldIndex, unsigned long newIndex);
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
void MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
unsigned long GetObjectsCount();
[Ref] ObjectFolderOrObject GetRootFolder();
[Ref] ObjectGroupsContainer GetObjectGroups();
};
EventsBasedObject implements AbstractEventsBasedEntity;

View File

@@ -72,7 +72,6 @@
#include <GDCore/Project/MeasurementUnitElement.h>
#include <GDCore/Project/NamedPropertyDescriptor.h>
#include <GDCore/Project/Object.h>
#include <GDCore/Project/ObjectFolderOrObject.h>
#include <GDCore/Project/ObjectConfiguration.h>
#include <GDCore/Project/CustomObjectConfiguration.h>
#include <GDCore/Project/Project.h>

View File

@@ -4,7 +4,6 @@ const {
makeFakeAbstractFileSystem,
} = require('../TestUtils/FakeAbstractFileSystem');
const extend = require('extend');
const { mapFor } = require('../../newIDE/app/src/Utils/MapFor.js');
describe('libGD.js', function () {
let gd = null;
@@ -441,10 +440,7 @@ describe('libGD.js', function () {
// Prepare two containers, one with 3 objects and one empty
const objectsContainer1 = new gd.ObjectsContainer();
const rootFolder1 = objectsContainer1.getRootFolder();
const objectsContainer2 = new gd.ObjectsContainer();
const rootFolder2 = objectsContainer2.getRootFolder();
const subFolder2 = rootFolder2.insertNewFolder('Folder', 1);
const mySpriteObject = objectsContainer1.insertNewObject(
project,
'Sprite',
@@ -464,11 +460,9 @@ describe('libGD.js', function () {
2
);
// Find the pointer to the objects in memory
expect(objectsContainer1.getObjectsCount()).toBe(3);
expect(objectsContainer2.getObjectsCount()).toBe(0);
expect(rootFolder1.getChildrenCount()).toBe(3);
expect(rootFolder2.getChildrenCount()).toBe(1);
// Find the pointer to the objects in memory
const mySpriteObjectPtr = gd.getPointer(objectsContainer1.getObjectAt(0));
const mySprite2ObjectPtr = gd.getPointer(
objectsContainer1.getObjectAt(1)
@@ -476,24 +470,11 @@ describe('libGD.js', function () {
const mySprite3ObjectPtr = gd.getPointer(
objectsContainer1.getObjectAt(2)
);
const mySpriteObjectFolderOrObject = rootFolder1.getChildAt(0);
const mySprite2ObjectFolderOrObject = rootFolder1.getChildAt(1);
const mySprite3ObjectFolderOrObject = rootFolder1.getChildAt(2);
const mySpriteObjectFolderOrObjectPtr = gd.getPointer(
mySpriteObjectFolderOrObject
);
const mySprite2ObjectFolderOrObjectPtr = gd.getPointer(
mySprite2ObjectFolderOrObject
);
const mySprite3ObjectFolderOrObjectPtr = gd.getPointer(
mySprite3ObjectFolderOrObject
);
// Move objects between containers
objectsContainer1.moveObjectFolderOrObjectToAnotherContainerInFolder(
mySprite2ObjectFolderOrObject,
objectsContainer1.moveObjectToAnotherContainer(
'MySprite2',
objectsContainer2,
rootFolder2,
0
);
expect(objectsContainer1.getObjectsCount()).toBe(2);
@@ -501,34 +482,17 @@ describe('libGD.js', function () {
expect(objectsContainer1.getObjectAt(1).getName()).toBe('MySprite3');
expect(objectsContainer2.getObjectsCount()).toBe(1);
expect(objectsContainer2.getObjectAt(0).getName()).toBe('MySprite2');
expect(rootFolder2.hasObjectNamed('MySprite2')).toBe(true);
expect(rootFolder2.getChildrenCount()).toBe(2);
expect(gd.getPointer(rootFolder2.getObjectChild('MySprite2'))).toBe(
mySprite2ObjectFolderOrObjectPtr
);
expect(rootFolder2.getObjectChild('MySprite2')).toBe(
mySprite2ObjectFolderOrObject
);
expect(mySprite2ObjectFolderOrObject.getParent()).toBe(rootFolder2);
// Move object in sub folder.
objectsContainer1.moveObjectFolderOrObjectToAnotherContainerInFolder(
mySprite3ObjectFolderOrObject,
objectsContainer1.moveObjectToAnotherContainer(
'MySprite3',
objectsContainer2,
subFolder2,
0
1
);
expect(objectsContainer1.getObjectsCount()).toBe(1);
expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite');
expect(objectsContainer2.getObjectsCount()).toBe(2);
expect(objectsContainer2.getObjectAt(0).getName()).toBe('MySprite2');
expect(objectsContainer2.getObjectAt(1).getName()).toBe('MySprite3');
expect(subFolder2.hasObjectNamed('MySprite3')).toBe(true);
expect(subFolder2.getChildrenCount()).toBe(1);
expect(gd.getPointer(subFolder2.getObjectChild('MySprite3'))).toBe(
mySprite3ObjectFolderOrObjectPtr
);
expect(mySprite3ObjectFolderOrObject.getParent()).toBe(subFolder2);
// Check that the object in memory are the same, even if moved to another container
expect(gd.getPointer(objectsContainer1.getObjectAt(0))).toBe(
@@ -541,53 +505,27 @@ describe('libGD.js', function () {
mySprite3ObjectPtr
);
expect(gd.getPointer(rootFolder2.getObjectChild('MySprite2'))).toBe(
mySprite2ObjectFolderOrObjectPtr
);
expect(rootFolder2.getObjectChild('MySprite2')).toBe(
mySprite2ObjectFolderOrObject
);
// Move back first object to first container
objectsContainer2.moveObjectFolderOrObjectToAnotherContainerInFolder(
mySprite2ObjectFolderOrObject,
objectsContainer2.moveObjectToAnotherContainer(
'MySprite2',
objectsContainer1,
rootFolder1,
0
);
expect(objectsContainer1.getObjectsCount()).toBe(2);
expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite');
expect(objectsContainer1.getObjectAt(1).getName()).toBe('MySprite2');
expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite2');
expect(objectsContainer1.getObjectAt(1).getName()).toBe('MySprite');
expect(objectsContainer2.getObjectsCount()).toBe(1);
expect(objectsContainer2.getObjectAt(0).getName()).toBe('MySprite3');
expect(rootFolder2.hasObjectNamed('MySprite2')).toBe(false);
expect(rootFolder2.getChildrenCount()).toBe(1);
expect(rootFolder1.getChildrenCount()).toBe(2);
expect(rootFolder1.getChildAt(0).getObject().getName()).toBe('MySprite2');
expect(rootFolder1.getChildAt(1).getObject().getName()).toBe('MySprite');
expect(rootFolder1.hasObjectNamed('MySprite2')).toBe(true);
expect(mySprite2ObjectFolderOrObject.getParent()).toBe(rootFolder1);
// Check again that the object in memory are the same, even if moved to another container
expect(gd.getPointer(objectsContainer1.getObjectAt(0))).toBe(
mySpriteObjectPtr
mySprite2ObjectPtr
);
expect(gd.getPointer(objectsContainer1.getObjectAt(1))).toBe(
mySprite2ObjectPtr
mySpriteObjectPtr
);
expect(gd.getPointer(objectsContainer2.getObjectAt(0))).toBe(
mySprite3ObjectPtr
);
expect(gd.getPointer(rootFolder1.getObjectChild('MySprite2'))).toBe(
mySprite2ObjectFolderOrObjectPtr
);
expect(gd.getPointer(rootFolder1.getObjectChild('MySprite'))).toBe(
mySpriteObjectFolderOrObjectPtr
);
expect(gd.getPointer(subFolder2.getObjectChild('MySprite3'))).toBe(
mySprite3ObjectFolderOrObjectPtr
);
project.delete();
});
});
@@ -2927,11 +2865,13 @@ describe('libGD.js', function () {
});
describe('gd.SpriteObject', function () {
it('is a gd.Object', function () {
it('is a gd.Object and can have tags', function () {
const project = new gd.ProjectHelper.createNewGDJSProject();
let object = project.insertNewObject(project, 'Sprite', 'MySpriteObject');
expect(object instanceof gd.Object).toBe(true);
object.setTags('tag1, tag2, tag3');
expect(object.getTags()).toBe('tag1, tag2, tag3');
expect(object.getVariables()).toBeTruthy();
project.delete();
});
@@ -4597,276 +4537,4 @@ Array [
).toBe(true);
});
});
describe('gd.ObjectFolderOrObject (using gd.ObjectsContainer)', () => {
let project = null;
let layout = null;
beforeAll(() => {
project = gd.ProjectHelper.createNewGDJSProject();
});
afterEach(() => {
project.removeLayout('Scene');
});
beforeEach(() => {
layout = project.insertNewLayout('Scene', 0);
});
test('objects container has a root ObjectFolderOrObject', () => {
const rootFolder = layout.getRootFolder();
expect(rootFolder.isFolder()).toBe(true);
expect(rootFolder.isRootFolder()).toBe(true);
expect(rootFolder.getParent().isFolder()).toBe(true);
expect(rootFolder.getParent().getFolderName()).toEqual('__NULL');
expect(rootFolder.getChildrenCount()).toEqual(0);
});
test('an object added to the object container is added to the root ObjectFolderOrObject', () => {
let object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
const rootFolder = layout.getRootFolder();
expect(rootFolder.hasObjectNamed('MyObject')).toBe(true);
expect(rootFolder.isRootFolder()).toBe(true);
expect(rootFolder.getChildrenCount()).toEqual(1);
layout.removeObject('MyObject');
expect(rootFolder.hasObjectNamed('MyObject')).toBe(false);
expect(rootFolder.getChildrenCount()).toEqual(0);
});
test('a folder can be added to the root folder', () => {
const rootFolder = layout.getRootFolder();
const subFolder = rootFolder.insertNewFolder('Enemies', 1);
expect(subFolder.getFolderName()).toEqual('Enemies');
expect(subFolder.isRootFolder()).toBe(false);
subFolder.setFolderName('Players');
expect(subFolder.getFolderName()).toEqual('Players');
expect(subFolder.getParent()).toBe(rootFolder);
expect(rootFolder.getChildrenCount()).toEqual(1);
});
test('an object can be added to a specific folder', () => {
const rootFolder = layout.getRootFolder();
const subFolder = rootFolder.insertNewFolder('Enemies', 0);
const subSubFolder = subFolder.insertNewFolder('Turtles', 0);
layout.insertNewObjectInFolder(
project,
'Sprite',
'RedTurtle',
subSubFolder,
0
);
expect(layout.hasObjectNamed('RedTurtle')).toBe(true);
expect(subSubFolder.hasObjectNamed('RedTurtle')).toBe(true);
});
test('an ObjectFolderOrObject can be serialized and unserialized', () => {
const rootFolder = layout.getRootFolder();
const object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
const subFolder = rootFolder.insertNewFolder('Enemies', 1);
const object2 = layout.insertNewObject(
project,
'Sprite',
'OtherObject',
1
);
const object3 = layout.insertNewObject(project, 'Sprite', 'SubObject', 2);
rootFolder.moveObjectFolderOrObjectToAnotherFolder(
rootFolder.getObjectChild('SubObject'),
subFolder,
0
);
expect(rootFolder.hasObjectNamed('MyObject')).toBe(true);
expect(rootFolder.hasObjectNamed('OtherObject')).toBe(true);
expect(rootFolder.getChildrenCount()).toEqual(3);
expect(
rootFolder.getChildPosition(rootFolder.getObjectChild('MyObject'))
).toEqual(0);
expect(rootFolder.getChildPosition(subFolder)).toEqual(1);
expect(
rootFolder.getChildPosition(rootFolder.getObjectChild('OtherObject'))
).toEqual(2);
expect(rootFolder.hasObjectNamed('SubObject')).toBe(true);
expect(subFolder.hasObjectNamed('SubObject')).toBe(true);
const element = new gd.SerializerElement();
layout.serializeTo(element);
project.removeLayout('Scene');
const layout2 = project.insertNewLayout('Scene2', 0);
layout2.unserializeFrom(project, element);
expect(layout2.hasObjectNamed('MyObject')).toBe(true);
expect(layout2.hasObjectNamed('OtherObject')).toBe(true);
const rootFolder2 = layout.getRootFolder();
expect(rootFolder2.hasObjectNamed('MyObject')).toBe(true);
expect(rootFolder2.hasObjectNamed('OtherObject')).toBe(true);
expect(rootFolder2.getChildrenCount()).toEqual(3);
const parentEqualities = mapFor(
0,
rootFolder2.getChildrenCount(),
(i) => {
const childObjectFolderOrObject = rootFolder2.getChildAt(i);
return childObjectFolderOrObject.getParent() === rootFolder2;
}
);
expect(parentEqualities.every((equality) => equality)).toBe(true);
const subFolder2 = rootFolder2.getChildAt(1);
expect(subFolder2.isFolder()).toBe(true);
const subObject = subFolder2.getObjectChild('SubObject');
expect(subObject.getParent()).toBe(subFolder2);
});
test('an ObjectFolderOrObject can be serialized and unserialized and missing object folders or objects are added', () => {
const rootFolder = layout.getRootFolder();
const object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
const subFolder = rootFolder.insertNewFolder('Enemies', 1);
const object2 = layout.insertNewObject(
project,
'Sprite',
'OtherObject',
1
);
const object3 = layout.insertNewObject(project, 'Sprite', 'SubObject', 2);
rootFolder.moveObjectFolderOrObjectToAnotherFolder(
rootFolder.getObjectChild('SubObject'),
subFolder,
0
);
expect(rootFolder.hasObjectNamed('MyObject')).toBe(true);
expect(rootFolder.hasObjectNamed('OtherObject')).toBe(true);
expect(rootFolder.getChildrenCount()).toEqual(3);
expect(rootFolder.hasObjectNamed('SubObject')).toBe(true);
expect(subFolder.hasObjectNamed('SubObject')).toBe(true);
const element = new gd.SerializerElement();
layout.serializeTo(element);
const layoutObject = JSON.parse(gd.Serializer.toJSON(element));
delete layoutObject.objectsFolderStructure;
project.removeLayout('Scene');
const layout2 = project.insertNewLayout('Scene2', 0);
layout2.unserializeFrom(
project,
gd.Serializer.fromJSObject(layoutObject)
);
expect(layout2.hasObjectNamed('MyObject')).toBe(true);
expect(layout2.hasObjectNamed('OtherObject')).toBe(true);
const rootFolder2 = layout.getRootFolder();
expect(rootFolder2.hasObjectNamed('MyObject')).toBe(true);
expect(rootFolder2.hasObjectNamed('OtherObject')).toBe(true);
expect(rootFolder2.getChildrenCount()).toEqual(3);
const parentEqualities = mapFor(
0,
rootFolder2.getChildrenCount(),
(i) => {
const childObjectFolderOrObject = rootFolder2.getChildAt(i);
return childObjectFolderOrObject.getParent() === rootFolder2;
}
);
expect(parentEqualities.every((equality) => equality)).toBe(true);
});
test('a folder can be removed from its parent if empty', () => {
const rootFolder = layout.getRootFolder();
const object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
let subFolder = rootFolder.insertNewFolder('Enemies', 1);
const object2 = layout.insertNewObject(
project,
'Sprite',
'OtherObject',
2
);
rootFolder.moveObjectFolderOrObjectToAnotherFolder(
rootFolder.getObjectChild('OtherObject'),
subFolder,
0
);
rootFolder.removeFolderChild(subFolder);
// Check subfolder is still here since it was not empty.
expect(rootFolder.getChildrenCount()).toEqual(2);
subFolder = rootFolder.getChildAt(1);
expect(subFolder.isFolder()).toBe(true);
expect(subFolder.getChildrenCount()).toEqual(1);
expect(subFolder.hasObjectNamed('OtherObject')).toBe(true);
// Empty subfolder and remove it.
subFolder.moveObjectFolderOrObjectToAnotherFolder(
subFolder.getObjectChild('OtherObject'),
rootFolder,
0
);
rootFolder.removeFolderChild(subFolder);
expect(rootFolder.getChildrenCount()).toEqual(2);
const objectFolderOrObject = rootFolder.getChildAt(1)
const otherObjectFolderOrObject = rootFolder.getChildAt(0)
expect(otherObjectFolderOrObject.isFolder()).toBe(false);
expect(otherObjectFolderOrObject.isRootFolder()).toBe(false);
expect(otherObjectFolderOrObject.getObject().getName()).toBe('OtherObject');
expect(objectFolderOrObject.isFolder()).toBe(false);
expect(objectFolderOrObject.isRootFolder()).toBe(false);
expect(objectFolderOrObject.getObject().getName()).toBe('MyObject');
});
test("an ObjectFolderOrObject can test if it's a descendant of another one", () => {
const rootFolder = layout.getRootFolder();
const subFolder = rootFolder.insertNewFolder('Depth1', 0);
const subSubFolder = subFolder.insertNewFolder('Depth2', 0);
const object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
rootFolder.moveObjectFolderOrObjectToAnotherFolder(
rootFolder.getObjectChild('MyObject'),
subSubFolder,
0
);
const objectFolderOrObject = subSubFolder.getChildAt(0);
expect(objectFolderOrObject.isFolder()).toBe(false);
expect(objectFolderOrObject.getObject().getName()).toEqual('MyObject');
expect(objectFolderOrObject.isADescendantOf(subSubFolder)).toBe(true);
expect(objectFolderOrObject.isADescendantOf(subFolder)).toBe(true);
expect(objectFolderOrObject.isADescendantOf(rootFolder)).toBe(true);
expect(subSubFolder.isADescendantOf(subFolder)).toBe(true);
expect(subSubFolder.isADescendantOf(rootFolder)).toBe(true);
expect(subFolder.isADescendantOf(rootFolder)).toBe(true);
expect(rootFolder.isADescendantOf(objectFolderOrObject)).toBe(false);
expect(rootFolder.isADescendantOf(subSubFolder)).toBe(false);
expect(rootFolder.isADescendantOf(subFolder)).toBe(false);
expect(rootFolder.isADescendantOf(rootFolder)).toBe(false);
expect(subFolder.isADescendantOf(objectFolderOrObject)).toBe(false);
expect(subFolder.isADescendantOf(subSubFolder)).toBe(false);
expect(subFolder.isADescendantOf(subFolder)).toBe(false);
expect(subSubFolder.isADescendantOf(objectFolderOrObject)).toBe(false);
expect(subSubFolder.isADescendantOf(subSubFolder)).toBe(false);
expect(objectFolderOrObject.isADescendantOf(objectFolderOrObject)).toBe(
false
);
});
test("an ObjectFolderOrObject representing an object can be retrieved using the object name only", () => {
const rootFolder = layout.getRootFolder();
const subFolder = rootFolder.insertNewFolder('Depth1', 0);
const subSubFolder = subFolder.insertNewFolder('Depth2', 0);
const object = layout.insertNewObject(project, 'Sprite', 'MyObject', 0);
rootFolder.moveObjectFolderOrObjectToAnotherFolder(
rootFolder.getObjectChild('MyObject'),
subSubFolder,
0
);
const objectFolderOrObject = subSubFolder.getChildAt(0);
expect(objectFolderOrObject.isRootFolder()).toBe(false);
const objectFolderOrObjectFoundByName = rootFolder.getObjectNamed('MyObject');
expect(objectFolderOrObjectFoundByName.isRootFolder()).toBe(false);
expect(objectFolderOrObjectFoundByName).toBe(objectFolderOrObject);
});
});
});

View File

@@ -14,17 +14,16 @@ declare class gdEventsBasedObject extends gdAbstractEventsBasedEntity {
static getPropertyExpressionName(propertyName: string): string;
static getPropertyToggleActionName(propertyName: string): string;
insertNewObject(project: gdProject, type: string, name: string, pos: number): gdObject;
insertNewObjectInFolder(project: gdProject, type: string, name: string, folder: gdObjectFolderOrObject, pos: number): gdObject;
insertObject(obj: gdObject, pos: number): gdObject;
hasObjectNamed(name: string): boolean;
getObject(name: string): gdObject;
getObjectAt(pos: number): gdObject;
getObjectPosition(name: string): number;
removeObject(name: string): void;
swapObjects(first: number, second: number): void;
moveObject(oldIndex: number, newIndex: number): void;
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
getObjectsCount(): number;
getRootFolder(): gdObjectFolderOrObject;
getObjectGroups(): gdObjectGroupsContainer;
delete(): void;
ptr: number;

View File

@@ -33,17 +33,16 @@ declare class gdLayout extends gdObjectsContainer {
setStopSoundsOnStartup(enable: boolean): void;
stopSoundsOnStartup(): boolean;
insertNewObject(project: gdProject, type: string, name: string, pos: number): gdObject;
insertNewObjectInFolder(project: gdProject, type: string, name: string, folder: gdObjectFolderOrObject, pos: number): gdObject;
insertObject(obj: gdObject, pos: number): gdObject;
hasObjectNamed(name: string): boolean;
getObject(name: string): gdObject;
getObjectAt(pos: number): gdObject;
getObjectPosition(name: string): number;
removeObject(name: string): void;
swapObjects(first: number, second: number): void;
moveObject(oldIndex: number, newIndex: number): void;
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
getObjectsCount(): number;
getRootFolder(): gdObjectFolderOrObject;
getObjectGroups(): gdObjectGroupsContainer;
delete(): void;
ptr: number;

View File

@@ -8,6 +8,8 @@ declare class gdObject {
getAssetStoreId(): string;
setType(type: string): void;
getType(): string;
setTags(tags: string): void;
getTags(): string;
is3DObject(): boolean;
getConfiguration(): gdObjectConfiguration;
getVariables(): gdVariablesContainer;

View File

@@ -1,23 +0,0 @@
// Automatically generated by GDevelop.js/scripts/generate-types.js
declare class gdObjectFolderOrObject {
constructor(): void;
isFolder(): boolean;
isRootFolder(): boolean;
getObject(): gdObject;
getFolderName(): string;
setFolderName(name: string): void;
hasObjectNamed(name: string): boolean;
getObjectNamed(name: string): gdObjectFolderOrObject;
getChildrenCount(): number;
getChildAt(pos: number): gdObjectFolderOrObject;
getObjectChild(name: string): gdObjectFolderOrObject;
getChildPosition(child: gdObjectFolderOrObject): number;
getParent(): gdObjectFolderOrObject;
insertNewFolder(name: string, newPosition: number): gdObjectFolderOrObject;
moveObjectFolderOrObjectToAnotherFolder(objectFolderOrObject: gdObjectFolderOrObject, newParentFolder: gdObjectFolderOrObject, newPosition: number): void;
moveChild(oldIndex: number, newIndex: number): void;
removeFolderChild(childToRemove: gdObjectFolderOrObject): void;
isADescendantOf(otherObjectFolderOrObject: gdObjectFolderOrObject): boolean;
delete(): void;
ptr: number;
};

View File

@@ -2,17 +2,16 @@
declare class gdObjectsContainer {
constructor(): void;
insertNewObject(project: gdProject, type: string, name: string, pos: number): gdObject;
insertNewObjectInFolder(project: gdProject, type: string, name: string, folder: gdObjectFolderOrObject, pos: number): gdObject;
insertObject(obj: gdObject, pos: number): gdObject;
hasObjectNamed(name: string): boolean;
getObject(name: string): gdObject;
getObjectAt(pos: number): gdObject;
getObjectPosition(name: string): number;
removeObject(name: string): void;
swapObjects(first: number, second: number): void;
moveObject(oldIndex: number, newIndex: number): void;
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
getObjectsCount(): number;
getRootFolder(): gdObjectFolderOrObject;
getObjectGroups(): gdObjectGroupsContainer;
delete(): void;
ptr: number;

View File

@@ -114,17 +114,16 @@ declare class gdProject extends gdObjectsContainer {
getTypeOfBehaviorInObjectOrGroup(layout: gdLayout, objectOrGroupName: string, behaviorName: string, searchInGroups: boolean): string;
getBehaviorNamesInObjectOrGroup(layout: gdLayout, objectOrGroupName: string, behaviorType: string, searchInGroups: boolean): gdVectorString;
insertNewObject(project: gdProject, type: string, name: string, pos: number): gdObject;
insertNewObjectInFolder(project: gdProject, type: string, name: string, folder: gdObjectFolderOrObject, pos: number): gdObject;
insertObject(obj: gdObject, pos: number): gdObject;
hasObjectNamed(name: string): boolean;
getObject(name: string): gdObject;
getObjectAt(pos: number): gdObject;
getObjectPosition(name: string): number;
removeObject(name: string): void;
swapObjects(first: number, second: number): void;
moveObject(oldIndex: number, newIndex: number): void;
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
getObjectsCount(): number;
getRootFolder(): gdObjectFolderOrObject;
getObjectGroups(): gdObjectGroupsContainer;
delete(): void;
ptr: number;

View File

@@ -71,7 +71,6 @@ declare class libGDevelop {
PlatformSpecificAssets: Class<gdPlatformSpecificAssets>;
LoadingScreen: Class<gdLoadingScreen>;
Watermark: Class<gdWatermark>;
ObjectFolderOrObject: Class<gdObjectFolderOrObject>;
ObjectsContainer: Class<gdObjectsContainer>;
Project: Class<gdProject>;
ObjectsContainersList: Class<gdObjectsContainersList>;

View File

@@ -11,10 +11,6 @@
# almost always during development (any change in GDJS/extensions, or any restart of the app).
<PROJECT_ROOT>/node_modules/GDJS-for-web-app-only/.*
[untyped]
# react-window has some errors
<PROJECT_ROOT>/node_modules/react-window/*
[declarations]
# lingui-js triggers some Flow errors
<PROJECT_ROOT>/node_modules/@lingui/core/.*

View File

@@ -54,7 +54,6 @@
"react-sortable-tree": "2.6.2",
"react-test-renderer": "16.14.0",
"react-virtualized": "9.21.1",
"react-window": "1.8.9",
"recharts": "^2.1.10",
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.2",
@@ -24910,11 +24909,6 @@
"node": ">= 4.0.0"
}
},
"node_modules/memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"node_modules/memoizerific": {
"version": "1.11.3",
"dev": true,
@@ -30712,22 +30706,6 @@
"@babel/runtime": "^7.1.2"
}
},
"node_modules/react-window": {
"version": "1.8.9",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.9.tgz",
"integrity": "sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==",
"dependencies": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
},
"engines": {
"node": ">8.0.0"
},
"peerDependencies": {
"react": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0",
"react-dom": "^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0"
}
},
"node_modules/reactcss": {
"version": "1.2.3",
"license": "MIT",
@@ -53671,11 +53649,6 @@
"fs-monkey": "^1.0.4"
}
},
"memoize-one": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-5.2.1.tgz",
"integrity": "sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q=="
},
"memoizerific": {
"version": "1.11.3",
"dev": true,
@@ -57454,15 +57427,6 @@
}
}
},
"react-window": {
"version": "1.8.9",
"resolved": "https://registry.npmjs.org/react-window/-/react-window-1.8.9.tgz",
"integrity": "sha512-+Eqx/fj1Aa5WnhRfj9dJg4VYATGwIUP2ItwItiJ6zboKWA6EX3lYDAXfGF2hyNqplEprhbtjbipiADEcwQ823Q==",
"requires": {
"@babel/runtime": "^7.0.0",
"memoize-one": ">=3.1.1 <6"
}
},
"reactcss": {
"version": "1.2.3",
"requires": {

View File

@@ -84,7 +84,6 @@
"react-sortable-tree": "2.6.2",
"react-test-renderer": "16.14.0",
"react-virtualized": "9.21.1",
"react-window": "1.8.9",
"recharts": "^2.1.10",
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.2",

View File

@@ -178,17 +178,21 @@ const translateTypeToHumanReadableDescription = type => {
if (type === 'layer') return '🔤 Layer name (String)';
if (type === 'stringWithSelector') return '🔤 String';
if (type === 'identifier') return "🔤 Name (String)";
if (type === 'sceneName') return "🔤 Name of a scene (String)";
if (type === 'layerEffectName') return "🔤 Layer Effect Name (String)";
if (type === 'layerEffectParameterName') return "🔤 Layer Effect Property Name (String)";
if (type === 'objectEffectName') return "🔤 Object Effect Name (String)";
if (type === 'objectEffectParameterName') return "🔤 Object Effect Property Name (String)";
if (type === 'objectPointName') return "🔤 Object Point Name (String)";
if (type === 'objectAnimationName') return "🔤 Object Animation Name (String)";
if (type === 'functionParameterName') return "🔤 Function Parameter Name (String)";
if (type === 'externalLayoutName') return "🔤 External Layout Name (String)";
if (type === 'leaderboardId') return "🔤 Leaderboard Identifier (String)";
if (type === 'identifier') return '🔤 Name (String)';
if (type === 'sceneName') return '🔤 Name of a scene (String)';
if (type === 'layerEffectName') return '🔤 Layer Effect Name (String)';
if (type === 'layerEffectParameterName')
return '🔤 Layer Effect Property Name (String)';
if (type === 'objectEffectName') return '🔤 Object Effect Name (String)';
if (type === 'objectEffectParameterName')
return '🔤 Object Effect Property Name (String)';
if (type === 'objectPointName') return '🔤 Object Point Name (String)';
if (type === 'objectAnimationName')
return '🔤 Object Animation Name (String)';
if (type === 'functionParameterName')
return '🔤 Function Parameter Name (String)';
if (type === 'externalLayoutName') return '🔤 External Layout Name (String)';
if (type === 'leaderboardId') return '🔤 Leaderboard Identifier (String)';
return type;
};
@@ -212,17 +216,18 @@ const translateTypeToHumanReadableType = type => {
if (type === 'layer') return 'layer name';
if (type === 'stringWithSelector') return 'string';
if (type === 'identifier') return "string";
if (type === 'sceneName') return "scene name";
if (type === 'layerEffectName') return "layer effect name";
if (type === 'layerEffectParameterName') return "layer effect property name";
if (type === 'objectEffectName') return "object effect name";
if (type === 'objectEffectParameterName') return "object effect property name";
if (type === 'objectPointName') return "object point name";
if (type === 'objectAnimationName') return "object animation name";
if (type === 'functionParameterName') return "function parameter name";
if (type === 'externalLayoutName') return "external layout name";
if (type === 'leaderboardId') return "leaderboard identifier";
if (type === 'identifier') return 'string';
if (type === 'sceneName') return 'scene name';
if (type === 'layerEffectName') return 'layer effect name';
if (type === 'layerEffectParameterName') return 'layer effect property name';
if (type === 'objectEffectName') return 'object effect name';
if (type === 'objectEffectParameterName')
return 'object effect property name';
if (type === 'objectPointName') return 'object point name';
if (type === 'objectAnimationName') return 'object animation name';
if (type === 'functionParameterName') return 'function parameter name';
if (type === 'externalLayoutName') return 'external layout name';
if (type === 'leaderboardId') return 'leaderboard identifier';
return type;
};
@@ -273,7 +278,7 @@ const generateExpressionReferenceRowsText = ({
);
const type = parameterMetadata.getType();
const humanReadableTypeDesc = translateTypeToHumanReadableDescription(type)
const humanReadableTypeDesc = translateTypeToHumanReadableDescription(type);
parameterRows.push(
`| | _${humanReadableTypeDesc}_ | ${sanitizedDescription} |`
@@ -324,7 +329,8 @@ const generateInstructionsReferenceRowsTexts = ({
.map(instructionType => {
const instructionMetadata = instructionsMetadata.get(instructionType);
if (instructionMetadata.isHidden() || instructionMetadata.isPrivate()) return null;
if (instructionMetadata.isHidden() || instructionMetadata.isPrivate())
return null;
return generateInstructionReferenceRowsText({
instructionType,
@@ -353,7 +359,8 @@ const generateExpressionsReferenceRowsTexts = ({
.map(expressionType => {
const expressionMetadata = expressionsMetadata.get(expressionType);
if (!expressionMetadata.isShown() || expressionMetadata.isPrivate()) return null;
if (!expressionMetadata.isShown() || expressionMetadata.isPrivate())
return null;
return generateExpressionReferenceRowsText({
expressionType,
@@ -429,46 +436,50 @@ const generateExtensionReference = extension => {
// Behavior expressions
/** @type {Array<BehaviorReference>} */
let behaviorReferences = behaviorTypes.map(behaviorType => {
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
let behaviorReferences = behaviorTypes
.map(behaviorType => {
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
if (behaviorMetadata.isPrivate()) {
return null;
}
if (behaviorMetadata.isPrivate()) {
return null;
}
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
areConditions: false,
instructionsMetadata: extension.getAllActionsForBehavior(behaviorType),
behaviorMetadata,
});
const conditionsReferenceTexts = generateInstructionsReferenceRowsTexts({
areConditions: true,
instructionsMetadata: extension.getAllConditionsForBehavior(behaviorType),
behaviorMetadata,
});
const expressionsReferenceTexts = [
...generateExpressionsReferenceRowsTexts({
expressionsMetadata: extension.getAllExpressionsForBehavior(
const actionsReferenceTexts = generateInstructionsReferenceRowsTexts({
areConditions: false,
instructionsMetadata: extension.getAllActionsForBehavior(behaviorType),
behaviorMetadata,
});
const conditionsReferenceTexts = generateInstructionsReferenceRowsTexts({
areConditions: true,
instructionsMetadata: extension.getAllConditionsForBehavior(
behaviorType
),
behaviorMetadata,
}),
...generateExpressionsReferenceRowsTexts({
expressionsMetadata: extension.getAllStrExpressionsForBehavior(
behaviorType
),
behaviorMetadata,
}),
];
expressionsReferenceTexts.sort(sortReferenceTexts);
});
const expressionsReferenceTexts = [
...generateExpressionsReferenceRowsTexts({
expressionsMetadata: extension.getAllExpressionsForBehavior(
behaviorType
),
behaviorMetadata,
}),
...generateExpressionsReferenceRowsTexts({
expressionsMetadata: extension.getAllStrExpressionsForBehavior(
behaviorType
),
behaviorMetadata,
}),
];
expressionsReferenceTexts.sort(sortReferenceTexts);
return {
behaviorMetadata,
actionsReferenceTexts,
conditionsReferenceTexts,
expressionsReferenceTexts,
};
}).filter(Boolean);
return {
behaviorMetadata,
actionsReferenceTexts,
conditionsReferenceTexts,
expressionsReferenceTexts,
};
})
.filter(Boolean);
// Free (non objects/non behaviors) actions/conditions/expressions
const freeActionsReferenceTexts = generateInstructionsReferenceRowsTexts({
@@ -501,9 +512,9 @@ const generateExtensionReference = extension => {
/**
* @param {ExtensionReference} extensionReference
* @param {({extension: gdPlatformExtension, depth: number}) => RawText} generateExtensionHeaderText
* @param {({extension: gdPlatformExtension}) => RawText} generateExtensionFooterText
* @returns {Array<RawText>}}}
* @param {({extension, depth}) => RawText} generateExtensionHeaderText
* @param {({extension}) => RawText} generateExtensionFooterText
* @returns {Array<RawText>}
*/
const generateExtensionRawText = (
extensionReference,

View File

@@ -453,7 +453,10 @@ export const AssetStoreStateProvider = ({
const publicAssetPacksByTag = {};
publicAssetPacks.starterPacks.forEach(assetPack => {
const tag = assetPack.tag;
if (publicAssetPacksByTag[tag]) {
if (
publicAssetPacksByTag[tag] &&
!assetPack.externalWebLink // Don't warn for external web links, as they can be used multiple times.
) {
console.warn(`Multiple public asset packs with the same tag: ${tag}`);
}
publicAssetPacksByTag[tag] = assetPack;

View File

@@ -55,7 +55,7 @@ export const prepareExampleShortHeaders = (
const getItemUniqueId = (
item: ExampleShortHeader | PrivateGameTemplateListingData
) => item.name;
) => item.id;
type Props = {|
isOpening: boolean,

View File

@@ -36,6 +36,7 @@ import { shouldUseAppStoreProduct } from '../../Utils/AppStorePurchases';
import { formatProductPrice } from '../ProductPriceTag';
import AuthenticatedUserContext from '../../Profile/AuthenticatedUserContext';
import { capitalize } from 'lodash';
import FlatButton from '../../UI/FlatButton';
const styles = {
disabledText: { opacity: 0.6 },
@@ -304,7 +305,22 @@ const PrivateGameTemplateInformationPage = ({
allowParagraphs
/>
</Text>
<ResponsiveLineStackLayout noMargin noColumnMargin>
{!isAlreadyReceived && (
<Line expand>
<Column noMargin expand>
<FlatButton
primary
label={<Trans>Try it online</Trans>}
onClick={() =>
Window.openExternalURL(
gameTemplate.gamePreviewLink
)
}
/>
</Column>
</Line>
)}
<ResponsiveLineStackLayout noColumnMargin>
<Column noMargin expand>
<Text size="sub-title">
<Trans>Licensing</Trans>

View File

@@ -1,6 +1,6 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import PriceTag from '../UI/PriceTag';
@@ -40,6 +40,24 @@ export const formatProductPrice = ({
.replace(/\D00$/, '')}`;
};
type ProductPriceOrOwnedProps = {|
productListingData:
| PrivateAssetPackListingData
| PrivateGameTemplateListingData,
i18n: I18nType,
owned?: boolean,
|};
export const getProductPriceOrOwnedLabel = ({
i18n,
productListingData,
owned,
}: ProductPriceOrOwnedProps): string => {
return owned
? i18n._(t`✅ Owned`)
: formatProductPrice({ i18n, productListingData });
};
type ProductPriceTagProps = {|
productListingData:
| PrivateAssetPackListingData
@@ -60,11 +78,11 @@ const ProductPriceTag = ({
return (
<I18n>
{({ i18n }) => {
const label = owned ? (
<Trans> Owned</Trans>
) : (
formatProductPrice({ i18n, productListingData })
);
const label = getProductPriceOrOwnedLabel({
i18n,
productListingData,
owned,
});
return <PriceTag withOverlay={withOverlay} label={label} />;
}}

View File

@@ -13,10 +13,6 @@ import ObjectEditorDialog from '../ObjectEditor/ObjectEditorDialog';
import { type ObjectEditorTab } from '../ObjectEditor/ObjectEditorDialog';
import { emptyStorageProvider } from '../ProjectsStorage/ProjectStorageProviders';
import newNameGenerator from '../Utils/NewNameGenerator';
import {
getObjectFolderOrObjectUnifiedName,
type ObjectFolderOrObjectWithContext,
} from '../ObjectsList/EnumerateObjectFolderOrObject';
const gd: libGDevelop = global.gd;
@@ -30,7 +26,8 @@ type Props = {|
type State = {|
editedObjectWithContext: ?ObjectWithContext,
editedObjectInitialTab: ?ObjectEditorTab,
selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
selectedObjectsWithContext: ObjectWithContext[],
renamedObjectWithContext: ?ObjectWithContext,
|};
export default class EventBasedObjectChildrenEditor extends React.Component<
@@ -42,7 +39,8 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
state = {
editedObjectWithContext: null,
editedObjectInitialTab: 'properties',
selectedObjectFolderOrObjectsWithContext: [],
selectedObjectsWithContext: [],
renamedObjectWithContext: null,
};
_onDeleteObject = (i18n: I18nType) => (
@@ -74,7 +72,7 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
done(true);
};
_getValidatedObjectOrGroupName = (newName: string) => {
_getValidatedObjectOrGroupName = (newName: string, i18n: I18nType) => {
const { eventsBasedObject } = this.props;
const safeAndUniqueNewName = newNameGenerator(
@@ -96,15 +94,37 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
return safeAndUniqueNewName;
};
_onRenameEditedObject = (newName: string) => {
_onRenameObjectStart = (objectWithContext: ?ObjectWithContext) => {
const selectedObjectsWithContext = [];
if (objectWithContext) {
selectedObjectsWithContext.push(objectWithContext);
}
this.setState(
{
renamedObjectWithContext: objectWithContext,
selectedObjectsWithContext,
},
() => {
this.forceUpdateObjectsList();
}
);
};
_onRenameEditedObject = (newName: string, i18n: I18nType) => {
const { editedObjectWithContext } = this.state;
if (editedObjectWithContext) {
this._onRenameObject(editedObjectWithContext, newName);
this._onRenameObject(editedObjectWithContext, newName, () => {}, i18n);
}
};
_onRenameObject = (objectWithContext: ObjectWithContext, newName: string) => {
_onRenameObject = (
objectWithContext: ObjectWithContext,
newName: string,
done: boolean => void,
i18n: I18nType
) => {
const { object } = objectWithContext;
const { project, globalObjectsContainer, eventsBasedObject } = this.props;
@@ -123,40 +143,6 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
}
object.setName(newName);
};
_onRenameObjectFolderOrObjectFinish = (
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
newName: string,
done: boolean => void
) => {
const { objectFolderOrObject, global } = objectFolderOrObjectWithContext;
const unifiedName = getObjectFolderOrObjectUnifiedName(
objectFolderOrObject
);
// Avoid triggering renaming refactoring if name has not really changed
if (unifiedName === newName) {
this._onObjectFolderOrObjectWithContextSelected(
objectFolderOrObjectWithContext
);
done(false);
return;
}
// newName is supposed to have been already validated.
if (objectFolderOrObject.isFolder()) {
objectFolderOrObject.setFolderName(newName);
done(true);
return;
}
const object = objectFolderOrObject.getObject();
this._onRenameObject({ object, global }, newName);
this._onObjectFolderOrObjectWithContextSelected(
objectFolderOrObjectWithContext
);
done(true);
};
@@ -177,19 +163,15 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
}
};
_onObjectFolderOrObjectWithContextSelected = (
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext = null
) => {
const selectedObjectFolderOrObjectsWithContext = [];
if (objectFolderOrObjectWithContext) {
selectedObjectFolderOrObjectsWithContext.push(
objectFolderOrObjectWithContext
);
_onObjectSelected = (objectWithContext: ?ObjectWithContext = null) => {
const selectedObjectsWithContext = [];
if (objectWithContext) {
selectedObjectsWithContext.push(objectWithContext);
}
this.setState(
{
selectedObjectFolderOrObjectsWithContext,
selectedObjectsWithContext,
},
() => {
this.forceUpdateObjectsList();
@@ -216,7 +198,10 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
render() {
const { eventsBasedObject, project, eventsFunctionsExtension } = this.props;
const { selectedObjectFolderOrObjectsWithContext } = this.state;
const selectedObjectNames = this.state.selectedObjectsWithContext.map(
objWithContext => objWithContext.object.getName()
);
// TODO EBO When adding an object, filter the object types to excludes
// object that depend (transitively) on this object to avoid cycles.
@@ -245,28 +230,32 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
onFetchNewlyAddedResources: async () => {},
getStorageProviderResourceOperations: () => null,
}}
selectedObjectFolderOrObjectsWithContext={
selectedObjectFolderOrObjectsWithContext
}
selectedObjectNames={selectedObjectNames}
onEditObject={this.editObject}
// Don't allow export as there is no assets.
onExportObject={() => {}}
onDeleteObject={this._onDeleteObject(i18n)}
getValidatedObjectOrGroupName={
this._getValidatedObjectOrGroupName
getValidatedObjectOrGroupName={newName =>
this._getValidatedObjectOrGroupName(newName, i18n)
}
// Nothing special to do.
onObjectCreated={() => {}}
onObjectFolderOrObjectWithContextSelected={
this._onObjectFolderOrObjectWithContextSelected
}
onRenameObjectFolderOrObjectWithContextFinish={
this._onRenameObjectFolderOrObjectFinish
onObjectSelected={this._onObjectSelected}
renamedObjectWithContext={this.state.renamedObjectWithContext}
onRenameObjectStart={this._onRenameObjectStart}
onRenameObjectFinish={(objectWithContext, newName, done) =>
this._onRenameObject(objectWithContext, newName, done, i18n)
}
// Instances can't be created from this context.
onAddObjectInstance={() => {}}
onObjectPasted={() => this.updateBehaviorsSharedData()}
ref={objectsList => (this._objectsList = objectsList)}
selectedObjectTags={[]}
onChangeSelectedObjectTags={selectedObjectTags => {}}
getAllObjectTags={() => []}
ref={
// $FlowFixMe Make this component functional.
objectsList => (this._objectsList = objectsList)
}
unsavedChanges={null}
// TODO EBO Hide the preview button or implement it.
// Note that it will be hard to do hot reload as extensions need
@@ -311,10 +300,12 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
onCancel={() => {
this.editObject(null);
}}
getValidatedObjectOrGroupName={
this._getValidatedObjectOrGroupName
getValidatedObjectOrGroupName={newName =>
this._getValidatedObjectOrGroupName(newName, i18n)
}
onRename={this._onRenameEditedObject}
onRename={newName => {
this._onRenameEditedObject(newName, i18n);
}}
onApply={() => {
this.editObject(null);
this.updateBehaviorsSharedData();

View File

@@ -55,6 +55,7 @@ export default class BrowserEventsFunctionsExtensionWriter {
filename: string
): Promise<void> => {
const exportedObject = customObject.clone().get();
exportedObject.setTags('');
exportedObject.getVariables().clear();
exportedObject.getEffects().clear();
exportedObject

View File

@@ -89,6 +89,7 @@ export default class LocalEventsFunctionsExtensionWriter {
filepath: string
): Promise<void> => {
const exportedObject = customObject.clone().get();
exportedObject.setTags('');
exportedObject.getVariables().clear();
exportedObject.getEffects().clear();
exportedObject

View File

@@ -29,5 +29,3 @@ export const handle = 'move-handle';
export const linkContainer = 'link-container';
export const nameAndIconContainer = 'name-and-icon-container';
export const treeView = 'tree-view';

View File

@@ -435,7 +435,7 @@ const Instruction = (props: Props) => {
},
[onContextMenu]
),
{ context: 'events-tree-event-component' }
'events-tree-event-component'
);
return (

View File

@@ -171,7 +171,7 @@ const EventContainer = (props: EventsContainerProps) => {
},
[onEventContextMenu]
),
{ context: 'events-tree-event-component' }
'events-tree-event-component'
);
const EventComponent = EventsRenderingService.getEventComponent(event);

View File

@@ -6,6 +6,7 @@ import { t } from '@lingui/macro';
import Fuse from 'fuse.js';
import * as React from 'react';
import Chip from '@material-ui/core/Chip';
import {
createTree,
type InstructionOrExpressionTreeNode,
@@ -27,9 +28,12 @@ import { Tabs } from '../../UI/Tabs';
import Subheader from '../../UI/Subheader';
import {
enumerateObjectsAndGroups,
filterObjectByTags,
type ObjectWithContext,
type GroupWithContext,
enumerateObjects,
} from '../../ObjectsList/EnumerateObjects';
import TagChips from '../../UI/TagChips';
import RaisedButton from '../../UI/RaisedButton';
import { ResponsiveLineStackLayout } from '../../UI/Layout';
import { renderGroupObjectsListItem } from './SelectorListItems/SelectorGroupObjectsListItem';
@@ -37,6 +41,10 @@ import { renderObjectListItem } from './SelectorListItems/SelectorObjectListItem
import { renderInstructionOrExpressionListItem } from './SelectorListItems/SelectorInstructionOrExpressionListItem';
import { renderInstructionOrExpressionTree } from './SelectorListItems/SelectorInstructionsTreeListItem';
import EmptyMessage from '../../UI/EmptyMessage';
import {
buildTagsMenuTemplate,
getTagsFromString,
} from '../../Utils/TagsHelper';
import {
getObjectOrObjectGroupListItemValue,
getInstructionListItemValue,
@@ -51,21 +59,10 @@ import {
} from '../../UI/Search/UseSearchStructuredItem';
import { Column, Line } from '../../UI/Grid';
import Add from '../../UI/CustomSvgIcons/Add';
import getObjectByName from '../../Utils/GetObjectByName';
import {
enumerateFoldersInContainer,
getObjectsInFolder,
} from '../../ObjectsList/EnumerateObjectFolderOrObject';
import { renderFolderListItem } from './SelectorListItems/FolderListItem';
import Text from '../../UI/Text';
const gd: libGDevelop = global.gd;
const DISPLAYED_INSTRUCTIONS_MAX_LENGTH = 20;
export const styles = {
noObjectsText: { opacity: 0.7 },
indentedListItem: { paddingLeft: 45 },
};
export type TabName = 'objects' | 'free-instructions';
@@ -74,15 +71,12 @@ type State = {|
searchResults: {
objects: Array<SearchResult<ObjectWithContext>>,
groups: Array<SearchResult<GroupWithContext>>,
tags: Array<SearchResult<string>>,
instructions: Array<SearchResult<EnumeratedInstructionMetadata>>,
folders: Array<
SearchResult<{|
path: string,
folder: gdObjectFolderOrObject,
global: boolean,
|}>
>,
},
// State for tags of objects:
selectedObjectTags: Array<string>,
|};
type Props = {|
@@ -117,7 +111,8 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
> {
state = {
searchText: '',
searchResults: { objects: [], groups: [], instructions: [], folders: [] },
selectedObjectTags: [],
searchResults: { objects: [], groups: [], instructions: [], tags: [] },
};
_searchBar = React.createRef<SearchBarInterface>();
_scrollView = React.createRef<ScrollViewInterface>();
@@ -142,7 +137,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
instructionSearchApi = null;
objectSearchApi = null;
groupSearchApi = null;
folderSearchApi = null;
tagSearchApi = null;
reEnumerateInstructions = (i18n: I18nType) => {
this.freeInstructionsInfo = filterEnumeratedInstructionOrExpressionMetadataByScope(
@@ -172,15 +167,6 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
this.props.objectsContainer
);
const allFolders = [
...enumerateFoldersInContainer(this.props.globalObjectsContainer).map(
folderWithPath => ({ ...folderWithPath, global: true })
),
...enumerateFoldersInContainer(this.props.objectsContainer).map(
folderWithPath => ({ ...folderWithPath, global: false })
),
];
this.instructionSearchApi = new Fuse(
deduplicateInstructionsList(this.allInstructionsInfo),
{
@@ -201,10 +187,8 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
getFn: (item, property) => item.group.getName(),
keys: ['name'], // Not used as we only use the name of the group
});
this.folderSearchApi = new Fuse(allFolders, {
this.tagSearchApi = new Fuse(this._getAllObjectTags(), {
...sharedFuseConfiguration,
getFn: (item, property) => item.path,
keys: ['name'], // Not used as we only use the path to the folder
});
}
@@ -227,12 +211,6 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
matches: tuneMatches(result, searchText),
}))
: [],
folders: this.folderSearchApi
? this.folderSearchApi.search(extendedSearchText).map(result => ({
item: result.item,
matches: tuneMatches(result, searchText),
}))
: [],
instructions: this.instructionSearchApi
? this.instructionSearchApi
.search(
@@ -246,6 +224,47 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
matches: tuneMatches(result, searchText),
}))
: [],
tags: this.tagSearchApi
? this.tagSearchApi.search(extendedSearchText).map(result => ({
item: result.item,
matches: tuneMatches(result, searchText),
}))
: [],
},
});
};
_selectTag = (tag: string) => {
this.setState({
selectedObjectTags: [...this.state.selectedObjectTags, tag],
searchText: '',
});
this._searchBar.current && this._searchBar.current.focus();
};
_getAllObjectTags = (): Array<string> => {
const { globalObjectsContainer, objectsContainer } = this.props;
const tagsSet: Set<string> = new Set();
enumerateObjects(
globalObjectsContainer,
objectsContainer
).allObjectsList.forEach(({ object }) => {
getTagsFromString(object.getTags()).forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet);
};
_buildObjectTagsMenuTemplate = (i18n: I18nType): Array<any> => {
const { selectedObjectTags } = this.state;
return buildTagsMenuTemplate({
noTagLabel: i18n._(t`No tags - add a tag to an object first`),
getAllTags: this._getAllObjectTags,
selectedTags: selectedObjectTags,
onChange: selectedObjectTags => {
this.setState({ selectedObjectTags });
},
});
};
@@ -266,7 +285,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
onSearchStartOrReset,
onClickMore,
} = this.props;
const { searchText, searchResults } = this.state;
const { searchText, selectedObjectTags, searchResults } = this.state;
// If the global objects container is not the project, consider that we're
// not in the events of a layout or an external events sheet - but in an extension.
@@ -281,13 +300,13 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
let filteredObjectsList = [];
let displayedObjectGroupsList = [];
let filteredInstructionsList = [];
let filteredFoldersList = [];
let displayedTags = [];
if (isSearching) {
filteredObjectsList = searchResults.objects;
displayedObjectGroupsList = searchResults.groups;
filteredInstructionsList = searchResults.instructions;
filteredFoldersList = searchResults.folders;
displayedTags = searchResults.tags;
} else {
filteredObjectsList = allObjectsList.map(object => ({
item: object,
@@ -298,6 +317,10 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
matches: [],
}));
}
const displayedObjectsList = filteredObjectsList.filter(searchResult =>
filterObjectByTags(searchResult.item, selectedObjectTags)
);
const displayedInstructionsList = filteredInstructionsList.slice(
0,
DISPLAYED_INSTRUCTIONS_MAX_LENGTH
@@ -312,18 +335,20 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
const hasResults =
!isSearching ||
!!filteredObjectsList.length ||
!!displayedObjectsList.length ||
!!displayedObjectGroupsList.length ||
!!displayedInstructionsList.length ||
!!filteredFoldersList;
!!displayedTags.length;
const onSubmitSearch = () => {
if (!isSearching) return;
if (filteredObjectsList.length > 0) {
onChooseObject(filteredObjectsList[0].item.object.getName());
if (displayedObjectsList.length > 0) {
onChooseObject(displayedObjectsList[0].item.object.getName());
} else if (displayedObjectGroupsList.length > 0) {
onChooseObject(displayedObjectGroupsList[0].item.group.getName());
} else if (displayedTags.length > 0) {
this._selectTag(displayedTags[0].item);
} else if (displayedInstructionsList.length > 0) {
onChooseInstruction(
displayedInstructionsList[0].item.type,
@@ -333,7 +358,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
};
return (
<I18n>
<I18n key="tags">
{({ i18n }) => (
<div
id="instruction-or-object-selector"
@@ -363,6 +388,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
}
}}
onRequestSearch={onSubmitSearch}
buildMenuTemplate={() => this._buildObjectTagsMenuTemplate(i18n)}
ref={this._searchBar}
autoFocus={this.props.focusOnMount ? 'desktop' : undefined}
placeholder={
@@ -396,11 +422,21 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
</Line>
)}
<ScrollView ref={this._scrollView} autoHideScrollbar>
{!isSearching && currentTab === 'objects' && (
<TagChips
tags={selectedObjectTags}
onChange={selectedObjectTags =>
this.setState({
selectedObjectTags,
})
}
/>
)}
{hasResults && (
<List>
{(isSearching || currentTab === 'objects') && (
<React.Fragment>
{filteredObjectsList.map(
{displayedObjectsList.map(
({ item: objectWithContext, matches }, index) =>
renderObjectListItem({
project: project,
@@ -431,145 +467,42 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
</Subheader>
)}
{displayedObjectGroupsList.map(
({ item: groupWithContext, matches }) => {
const results = [];
results.push(
renderGroupObjectsListItem({
groupWithContext,
iconSize,
onClick: () =>
onChooseObject(
groupWithContext.group.getName()
),
matchesCoordinates: matches.length
? matches[0].indices // Only field for groups is their name
: [],
selectedValue: chosenObjectName
? getObjectOrObjectGroupListItemValue(
chosenObjectName
)
: undefined,
})
);
if (isSearching) {
const { group, global } = groupWithContext;
const groupName = group.getName();
const objectsInGroup = group
.getAllObjectsNames()
.toJSArray()
.map(objectName => {
// A global object group can contain scene objects so we cannot use
// the group context to get directly get the object knowing the
// appropriate container.
const object = getObjectByName(
globalObjectsContainer,
objectsContainer,
objectName
);
if (!object) return null;
return renderObjectListItem({
project,
objectWithContext: {
object,
global,
},
keyPrefix: `group-${groupName}`,
withIndent: true,
iconSize,
onClick: () => onChooseObject(objectName),
matchesCoordinates: [],
selectedValue: chosenObjectName
? getObjectOrObjectGroupListItemValue(
chosenObjectName
)
: undefined,
});
})
.filter(Boolean);
if (objectsInGroup.length === 0) {
results.push(
<ListItem
key={`${group.getName()}-empty`}
primaryText={
<Text style={styles.noObjectsText} noMargin>
<Trans>No objects in the group</Trans>
</Text>
}
style={styles.indentedListItem}
/>
);
} else {
results.push(...objectsInGroup);
}
}
return results;
}
)}
{filteredFoldersList.length > 0 && (
<Subheader>
<Trans>Folders</Trans>
</Subheader>
)}
{filteredFoldersList.map(
({ item: folderWithPath, matches }) => {
const results = [];
results.push(
renderFolderListItem({
folderWithPath,
iconSize,
matchesCoordinates: matches.length
? matches[0].indices
: [],
})
);
const objectsInFolder = getObjectsInFolder(
folderWithPath.folder
);
if (objectsInFolder.length === 0) {
results.push(
<ListItem
key={`${folderWithPath.path}-empty`}
primaryText={
<Text style={styles.noObjectsText} noMargin>
<Trans>No objects in the folder</Trans>
</Text>
}
style={styles.indentedListItem}
/>
);
} else {
results.push(
...objectsInFolder.map(object =>
renderObjectListItem({
project,
selectedValue: chosenObjectName
? getObjectOrObjectGroupListItemValue(
chosenObjectName
)
: undefined,
keyPrefix: `folder-${folderWithPath.path}`,
iconSize,
matchesCoordinates: [],
objectWithContext: {
object,
global: folderWithPath.global,
},
withIndent: true,
onClick: () =>
onChooseObject(object.getName()),
})
)
);
}
return results;
}
({ item: groupWithContext, matches }) =>
renderGroupObjectsListItem({
groupWithContext: groupWithContext,
iconSize: iconSize,
onClick: () =>
onChooseObject(groupWithContext.group.getName()),
matchesCoordinates: matches.length
? matches[0].indices // Only field for groups is their name
: [],
selectedValue: chosenObjectName
? getObjectOrObjectGroupListItemValue(
chosenObjectName
)
: undefined,
})
)}
</React.Fragment>
)}
{isSearching &&
currentTab === 'objects' &&
displayedTags.length > 0 && (
<Subheader>
<Trans>Object tags</Trans>
</Subheader>
)}
{currentTab === 'objects' &&
displayedTags.map(({ item: tag, matches }) => (
<ListItem
key={tag}
primaryText={<Chip label={tag} />}
onClick={() => {
this._selectTag(tag);
}}
disableAutoTranslate
/>
))}
{isSearching && displayedInstructionsList.length > 0 && (
<Subheader>
{isCondition ? (

View File

@@ -1,37 +0,0 @@
// @flow
import * as React from 'react';
import { ListItem } from '../../../UI/List';
import HighlightedText from '../../../UI/Search/HighlightedText';
import Folder from '../../../UI/CustomSvgIcons/Folder';
type Props = {|
folderWithPath: {|
path: string,
folder: gdObjectFolderOrObject,
global: boolean,
|},
iconSize: number,
matchesCoordinates: number[][],
|};
export const renderFolderListItem = ({
folderWithPath,
iconSize,
matchesCoordinates,
}: Props) => {
const folderPath: string = folderWithPath.path;
return (
<ListItem
key={folderPath}
selected={false}
primaryText={
<HighlightedText
text={folderPath}
matchesCoordinates={matchesCoordinates}
/>
}
leftIcon={<Folder width={iconSize} />}
disableAutoTranslate
/>
);
};

View File

@@ -9,7 +9,6 @@ import {
getObjectListItemKey,
} from './Keys';
import HighlightedText from '../../../UI/Search/HighlightedText';
import { styles } from '../InstructionOrObjectSelector';
import { type HTMLDataset } from '../../../Utils/HTMLDataset';
type Props = {|
@@ -21,8 +20,6 @@ type Props = {|
matchesCoordinates: number[][],
id?: ?string,
data?: HTMLDataset,
withIndent?: boolean,
keyPrefix?: string,
|};
export const renderObjectListItem = ({
@@ -34,19 +31,16 @@ export const renderObjectListItem = ({
matchesCoordinates,
id,
data,
withIndent,
keyPrefix,
}: Props) => {
const objectName: string = objectWithContext.object.getName();
return (
<ListItem
id={id}
data={data}
key={(keyPrefix || '') + getObjectListItemKey(objectWithContext)}
key={getObjectListItemKey(objectWithContext)}
selected={
selectedValue === getObjectOrObjectGroupListItemValue(objectName)
}
style={withIndent ? styles.indentedListItem : undefined}
primaryText={
<HighlightedText
text={objectName}

View File

@@ -207,7 +207,7 @@ export default React.forwardRef<Props, VariableFieldInterface>(
onApply={onApply}
dataSource={[
...autocompletionVariableNames,
onOpenDialog
onOpenDialog && variablesContainer
? {
translatableValue: t`Add or edit variables...`,
text: '',

View File

@@ -36,7 +36,6 @@ const selectorInterpolationProjectDataAccessors = {
objectInObjectOrResourceSelector: 'objectInObjectOrResourceSelector:',
editorTab: 'editorTab:',
};
const legacyItemInObjectListDomSelectorPattern = /#object-item-[0-9]{1,2}$/;
const getPhasesStartIndices = (endIndices: Array<number>): Array<number> =>
endIndices.map((_, i) => {
@@ -133,22 +132,6 @@ const interpolateEditorTabActiveTrigger = (
}"]${sceneNameFilter}`;
};
const countObjectsInScene = ({
project,
sceneName,
}: {|
project: gdProject,
sceneName: string,
|}): ?number => {
if (project.getLayoutsCount() === 0) return;
const layout = project.hasLayoutNamed(sceneName)
? project.getLayout(sceneName)
: project.getLayoutAt(0);
return layout.getObjectsCount();
};
export const getEditorTabSelector = ({
editor,
sceneName,
@@ -255,9 +238,6 @@ const isDomBasedTriggerComplete = (
if (!trigger) return false;
if (
trigger.presenceOfElement &&
!trigger.presenceOfElement.match(
legacyItemInObjectListDomSelectorPattern
) &&
document.querySelector(
interpolateElementId(trigger.presenceOfElement, data)
)
@@ -406,9 +386,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
const [data, setData] = React.useState<{| [key: string]: string |}>(
startProjectData
);
const objectCountBySceneRef = React.useRef<{|
[sceneName: string]: number,
|}>({});
const [displayEndDialog, setDisplayEndDialog] = React.useState<boolean>(
false
);
@@ -435,10 +412,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
objectName: string,
count?: number,
|}>(null);
const [
sceneObjectCountToWatch,
setSceneObjectCountToWatch,
] = React.useState<boolean>(false);
const domObserverRef = React.useRef<?MutationObserver>(null);
const [
shouldWatchProjectChanges,
@@ -491,13 +464,7 @@ const InAppTutorialOrchestrator = React.forwardRef<
);
const goToStep = React.useCallback(
({
stepIndex,
gatherData,
}: {
stepIndex: number,
gatherData?: boolean,
}) => {
(stepIndex: number) => {
if (stepIndex >= stepCount) {
setDisplayEndDialog(true);
return;
@@ -519,23 +486,12 @@ const InAppTutorialOrchestrator = React.forwardRef<
nextStepIndex += 1;
else break;
}
if (gatherData) {
const newData = gatherProjectDataOnMultipleSteps({
flow,
startIndex: currentStepIndex,
endIndex: nextStepIndex - 1,
data,
project,
});
setData(newData);
}
changeStep(nextStepIndex);
},
[flow, changeStep, stepCount, data, project, currentStepIndex]
[flow, changeStep, stepCount, data]
);
// Compute phases start positions on flow change.
React.useEffect(
() => {
const indices = [];
@@ -575,24 +531,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
[currentStepIndex, endIndicesPerPhase]
);
const hasCurrentSceneObjectsCountIncreased = React.useCallback(
(): boolean => {
if (!project || project.getLayoutsCount() === 0 || !currentSceneName)
return false;
const count = countObjectsInScene({
project,
sceneName: currentSceneName,
});
const initialCount = objectCountBySceneRef.current[currentSceneName];
return (
typeof initialCount === 'number' &&
typeof count === 'number' &&
count > initialCount
);
},
[project, currentSceneName]
);
const getProgress = () => {
return {
step: currentStepIndex,
@@ -635,17 +573,8 @@ const InAppTutorialOrchestrator = React.forwardRef<
if (!shortcuts) return;
for (let shortcutStep of shortcuts) {
// Find the first shortcut in the list that can be triggered.
// TODO: Add support for all triggers types
if (
isDomBasedTriggerComplete(shortcutStep.trigger, data) ||
(shortcutStep.trigger &&
(shortcutStep.trigger.objectAddedInLayout ||
(shortcutStep.trigger.presenceOfElement &&
shortcutStep.trigger.presenceOfElement.match(
legacyItemInObjectListDomSelectorPattern
))) &&
hasCurrentSceneObjectsCountIncreased())
) {
// TODO: Add support for not-dom based triggers
if (isDomBasedTriggerComplete(shortcutStep.trigger, data)) {
shouldGoToStepAtIndex = flow.findIndex(
step => step.id === shortcutStep.stepId
);
@@ -660,25 +589,30 @@ const InAppTutorialOrchestrator = React.forwardRef<
break;
}
}
if (shouldGoToStepAtIndex === null) return;
if (shouldGoToStepAtIndex == null) return;
}
goToStep({ stepIndex: shouldGoToStepAtIndex, gatherData: true });
// If a change of step is going to happen, first record the data for
// all the steps that are about to be closed.
const newData = gatherProjectDataOnMultipleSteps({
flow,
startIndex: currentStepIndex,
endIndex: shouldGoToStepAtIndex - 1,
data,
project,
});
setData(newData);
goToStep(shouldGoToStepAtIndex);
},
[
currentStepIndex,
goToStep,
data,
flow,
hasCurrentSceneObjectsCountIncreased,
]
[currentStepIndex, project, goToStep, data, flow]
);
const handleDomMutation = useDebounce(watchDomForNextStepTrigger, 200);
const goToNextStep = React.useCallback(
(gatherData?: boolean) => {
goToStep({ stepIndex: currentStepIndex + 1, gatherData });
() => {
goToStep(currentStepIndex + 1);
},
[currentStepIndex, goToStep]
);
@@ -753,7 +687,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
setElementWithValueToWatchIfChanged(null);
setElementWithValueToWatchIfEquals(null);
setObjectSceneInstancesToWatch(null);
setSceneObjectCountToWatch(false);
setShouldWatchProjectChanges(false);
// If index out of bounds, display end dialog.
if (currentStepIndex >= stepCount) {
@@ -763,20 +696,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
[currentStep, currentStepIndex, stepCount, editorSwitches]
);
// Update some refs on each step change and on current scene change.
React.useEffect(
() => {
if (!currentStep || !currentSceneName || !project) return;
const count = countObjectsInScene({
project,
sceneName: currentSceneName,
});
if (typeof count !== 'number') return;
objectCountBySceneRef.current[currentSceneName] = count;
},
[currentStep, currentSceneName, project]
);
// Set up watchers if the next step trigger is not dom-based.
React.useEffect(
() => {
@@ -807,15 +726,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
sceneName,
count: nextStepTrigger.instancesCount,
});
} else if (
nextStepTrigger &&
(nextStepTrigger.objectAddedInLayout ||
(nextStepTrigger.presenceOfElement &&
nextStepTrigger.presenceOfElement.match(
legacyItemInObjectListDomSelectorPattern
)))
) {
setSceneObjectCountToWatch(true);
}
},
[currentStep, data]
@@ -909,20 +819,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
[project, goToNextStep, objectSceneInstancesToWatch]
);
const watchSceneObjects = React.useCallback(
() => {
if (!sceneObjectCountToWatch) return;
if (hasCurrentSceneObjectsCountIncreased()) {
goToNextStep(true);
}
},
[
hasCurrentSceneObjectsCountIncreased,
goToNextStep,
sceneObjectCountToWatch,
]
);
useInterval(forceUpdate, shouldWatchProjectChanges ? 500 : null);
useInterval(
watchInputChanges,
@@ -936,7 +832,6 @@ const InAppTutorialOrchestrator = React.forwardRef<
watchSceneInstanceChanges,
objectSceneInstancesToWatch ? 500 : null
);
useInterval(watchSceneObjects, sceneObjectCountToWatch ? 1000 : null);
useInterval(
watchDomForNextStepTrigger,
currentStep && currentStep.isTriggerFlickering ? 500 : null

View File

@@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import InAppTutorialContext from './InAppTutorialContext';
import legacyOnboardingTutorial from './Tutorials/OnboardingTutorial';
import { setCurrentlyRunningInAppTutorial } from '../Utils/Analytics/EventSender';
import {
fetchInAppTutorial,
@@ -43,6 +44,14 @@ const InAppTutorialProvider = (props: Props) => {
initialStepIndex: number,
initialProjectData: { [key: string]: string },
|}) => {
if (tutorialId === legacyOnboardingTutorial.id) {
setStartStepIndex(initialStepIndex);
setStartProjectData(initialProjectData);
setTutorial(legacyOnboardingTutorial);
setCurrentlyRunningInAppTutorial(tutorialId);
return;
}
if (!inAppTutorialShortHeaders) return;
const inAppTutorialShortHeader = getInAppTutorialShortHeader(tutorialId);

View File

@@ -0,0 +1,482 @@
// @flow
import { t } from '@lingui/macro';
import { type InAppTutorial } from '../../Utils/GDevelopServices/InAppTutorial';
const inAppTutorial: InAppTutorial = {
id: 'onboarding',
editorSwitches: {
GoToBuildSection: { editor: 'Home' },
ClickOnNewObjectButtonForCharacter: { editor: 'Scene' },
ClickOnNewEvent: { editor: 'EventsSheet' },
},
endDialog: {
content: [
{
messageDescriptor: t`## Congratulations! 🎉`,
},
{
messageDescriptor: t`### Youve built your first game! 😊`,
},
{
messageDescriptor: t`Youre now ready to learn the basics of GDevelop.`,
},
{
messageDescriptor: t`Click the image to start!`,
},
{
messageDescriptor: t`👇👇👇`,
},
{
image: {
imageSource: 'https://i3.ytimg.com/vi/bR2BjT7JG0k/mqdefault.jpg',
linkHref:
'https://www.youtube.com/watch?v=bR2BjT7JG0k&list=PL3YlZTdKiS89Kj7IQVPoNElJCWrjZaCC8',
},
},
{
messageDescriptor: t`### Want to skip the basics?`,
},
{
messageDescriptor: t`Go to the "Learn" section on the app to explore advanced materials.`,
},
{
messageDescriptor: t`Have fun!`,
},
],
},
flow: [
{
id: 'GoToBuildSection',
elementToHighlightId: '#home-build-tab',
nextStepTrigger: { presenceOfElement: '#home-create-project-button' },
tooltip: {
description: {
messageDescriptor: t`Head over to the **Build section**`,
},
placement: 'right',
},
},
{
id: 'CreateProject',
elementToHighlightId: '#home-create-project-button',
nextStepTrigger: { presenceOfElement: '#create-project-button' },
tooltip: {
description: {
messageDescriptor: t`We'll create a simple game with **a character that can collect coins**.
${'\n'}${'\n'}Let's create a new project!`,
},
},
},
{
id: 'ValidateProjectCreation',
elementToHighlightId: '#create-project-button',
nextStepTrigger: {
presenceOfElement: '[id^=tab-layout]:not([id^=tab-layout-events])',
},
tooltip: {
description: { messageDescriptor: t`Let's go!` },
},
isOnClosableDialog: true,
},
{
id: 'ClickOnNewObjectButtonForCharacter',
elementToHighlightId: '#add-new-object-button',
nextStepTrigger: { presenceOfElement: '#new-object-dialog' },
tooltip: {
placement: 'left',
title: { messageDescriptor: t`Let's create an **object**` },
description: {
messageDescriptor: t`👉 Everything you see in a game is an **object**: your character, the enemies, coins and potions, platforms or trees, ...`,
},
},
},
{
id: 'OpenAssetTab',
elementToHighlightId: '#asset-store-tab',
nextStepTrigger: { presenceOfElement: '#asset-store' },
tooltip: {
description: {
messageDescriptor: t`Let's choose an object from the asset store.`,
},
placement: 'bottom',
},
skippable: true,
isOnClosableDialog: true,
},
{
id: 'ClickOnSearchBar',
elementToHighlightId: '#asset-store-search-bar',
nextStepTrigger: { valueHasChanged: true },
tooltip: {
title: {
messageDescriptor: t`Choose an asset to represent your main character!`,
},
description: { messageDescriptor: t`Tip: search for “wizard”.` },
},
skippable: true,
isOnClosableDialog: true,
},
{
id: 'WaitForUserToSelectAsset',
nextStepTrigger: { presenceOfElement: '#add-asset-button' },
isOnClosableDialog: true,
},
{
id: 'AddAsset',
elementToHighlightId: '#add-asset-button',
isTriggerFlickering: true,
nextStepTrigger: { presenceOfElement: '#object-item-0' },
tooltip: {
description: { messageDescriptor: t`Add this asset to your project.` },
},
mapProjectData: {
firstObject: 'lastProjectObjectName',
},
isOnClosableDialog: true,
},
{
id: 'CloseAssetStore',
elementToHighlightId: '#new-object-dialog #close-button',
nextStepTrigger: { absenceOfElement: '#new-object-dialog' },
tooltip: {
description: {
messageDescriptor: t`Great! Our game now has an **object**, let's see what we can do with it.`,
},
},
},
{
id: 'DragObjectToScene',
elementToHighlightId: '#object-item-0',
nextStepTrigger: { instanceAddedOnScene: 'firstObject' },
tooltip: {
description: {
messageDescriptor: t`Drag $(firstObject) from the menu to the canvas.`,
},
placement: 'left',
},
},
{
id: 'OpenBehaviors',
elementToHighlightId: '#object-item-0',
nextStepTrigger: { presenceOfElement: '#object-editor-dialog' },
tooltip: {
title: { messageDescriptor: t`Let's make our character move! 🛹` },
description: {
messageDescriptor: t`Here, right-click on it and click “Edit **behaviors**”`,
},
placement: 'left',
},
},
{
id: 'OpenBehaviorTab',
elementToHighlightId: '#behaviors-tab',
nextStepTrigger: { presenceOfElement: '#add-behavior-button' },
tooltip: {
description: {
messageDescriptor: t`See the **behaviors** of your object here.`,
},
placement: 'bottom',
},
skippable: true,
isOnClosableDialog: true,
},
{
id: 'AddBehavior',
elementToHighlightId: '#add-behavior-button',
nextStepTrigger: {
presenceOfElement:
'#behavior-item-TopDownMovementBehavior--TopDownMovementBehavior',
},
tooltip: {
title: { messageDescriptor: t`Lets add a **behavior**!` },
description: {
messageDescriptor: t`👉 Behaviors add features to objects in a matter of clicks. They are very powerful!`,
},
placement: 'bottom',
},
isOnClosableDialog: true,
},
{
id: 'SelectTopDownBehavior',
elementToHighlightId:
'#behavior-item-TopDownMovementBehavior--TopDownMovementBehavior',
nextStepTrigger: {
presenceOfElement: '#behavior-parameters-TopDownMovement',
},
tooltip: {
description: {
messageDescriptor: t`Add the "Top down movement" **behavior**.`,
},
placement: 'bottom',
},
isOnClosableDialog: true,
},
{
id: 'ApplyBehavior',
elementToHighlightId: '#object-editor-dialog #apply-button',
nextStepTrigger: {
absenceOfElement: '#object-editor-dialog',
},
tooltip: {
description: {
messageDescriptor: t`The parameters above help you customise the **behavior**, but let's ignore them for now.`,
},
placement: 'top',
},
isOnClosableDialog: true,
},
{
id: 'LaunchPreviewCharacterOnly',
elementToHighlightId: '#toolbar-preview-button',
nextStepTrigger: { previewLaunched: true },
tooltip: {
title: { messageDescriptor: t`Let's play! 🎮` },
description: {
messageDescriptor: t`Click on "**Preview**" and move your character with the **arrow keys**!`,
},
placement: 'bottom',
},
},
{
id: 'WaitForUserToHavePlayed',
elementToHighlightId: '#toolbar-preview-button',
nextStepTrigger: {
clickOnTooltipButton: { messageDescriptor: t`I'm done` },
},
tooltip: {
description: {
messageDescriptor: t`Once you're done testing, close the **preview** and come back here.`,
},
placement: 'bottom',
},
},
{
id: 'ClickOnNewObjectButtonForCoin',
elementToHighlightId: '#add-new-object-button',
nextStepTrigger: { presenceOfElement: '#new-object-dialog' },
tooltip: {
placement: 'left',
title: {
messageDescriptor: t`Let's now add another **object** that $(firstObject) can collect!`,
},
},
},
{
id: 'OpenAssetTabForCoin',
elementToHighlightId: '#asset-store-tab',
nextStepTrigger: { presenceOfElement: '#asset-store' },
tooltip: {
description: {
messageDescriptor: t`Let's choose an object from the asset store.`,
},
placement: 'bottom',
},
skippable: true,
isOnClosableDialog: true,
},
{
id: 'ClickOnSearchBarForCoin',
elementToHighlightId: '#asset-store-search-bar',
nextStepTrigger: { valueHasChanged: true },
tooltip: {
description: {
messageDescriptor: t`Search for “coin” (or a potion, food, ...).`,
},
},
isOnClosableDialog: true,
shortcuts: [
{
stepId: 'CloseAssetStoreForCoin',
trigger: { presenceOfElement: '#object-item-1' },
},
],
},
{
id: 'WaitForUserToSelectAssetForCoin',
nextStepTrigger: { presenceOfElement: '#add-asset-button' },
isOnClosableDialog: true,
},
{
id: 'AddAssetForCoin',
elementToHighlightId: '#add-asset-button',
isTriggerFlickering: true,
nextStepTrigger: { presenceOfElement: '#object-item-1' },
mapProjectData: {
secondObject: 'lastProjectObjectName',
},
isOnClosableDialog: true,
},
{
id: 'CloseAssetStoreForCoin',
elementToHighlightId: '#new-object-dialog #close-button',
nextStepTrigger: { absenceOfElement: '#new-object-dialog' },
tooltip: {
description: {
messageDescriptor: t`Great! Our game now has 2 **objects**, let's see what we can do with them.`,
},
},
},
{
id: 'DragObjectToScene',
elementToHighlightId: '#object-item-1',
nextStepTrigger: { instanceAddedOnScene: 'secondObject' },
tooltip: {
description: {
messageDescriptor: t`Place a few $(secondObject) in the scene by dragging them to the canvas.`,
},
placement: 'left',
},
},
{
id: 'SwitchToEventsSheet',
elementToHighlightId: 'editorTab::EventsSheet',
nextStepTrigger: { editorIsActive: ':EventsSheet' },
tooltip: {
description: {
messageDescriptor: t`Now let's make $(firstObject) collect the $(secondObject)! Go to the **events** tab of the **scene**.`,
},
placement: 'bottom',
},
},
{
id: 'ClickOnNewEvent',
elementToHighlightId: '#add-event-button',
nextStepTrigger: { presenceOfElement: '#add-condition-button-empty' },
tooltip: {
title: { messageDescriptor: t`Lets add an **event**!` },
description: {
messageDescriptor: t`👉 **Events** are the logic to your game.`,
},
placement: 'bottom',
},
},
{
id: 'ClickOnNewCondition',
elementToHighlightId: '#add-condition-button-empty',
nextStepTrigger: { presenceOfElement: '#instruction-editor-dialog' },
tooltip: {
description: {
messageDescriptor: t`**Events** are made of a condition and an action:
${'\n'}${'\n'}Condition: "**If** $(firstObject) touches the $(secondObject)..."
${'\n'}${'\n'}Action: "... **then** the $(secondObject) disappears"
${'\n'}${'\n'}**Click "Add condition**"`,
},
placement: 'bottom',
},
},
{
id: 'ChooseCharacterForCondition',
elementToHighlightId: '#instruction-editor-dialog #object-item-0',
nextStepTrigger: { presenceOfElement: '#object-instruction-selector' },
tooltip: {
description: { messageDescriptor: t`Choose $(firstObject)` },
placement: 'bottom',
},
isOnClosableDialog: true,
},
{
id: 'ChooseCondition',
elementToHighlightId: '#instruction-item-CollisionNP',
nextStepTrigger: {
presenceOfElement: '#instruction-parameters-container',
},
tooltip: {
description: {
messageDescriptor: t`Then the condition we want to use: **"Collision"**.`,
},
placement: 'bottom',
},
isOnClosableDialog: true,
},
{
id: 'SetParameter',
elementToHighlightId: '#parameter-1-object-selector',
nextStepTrigger: { valueHasChanged: true },
tooltip: {
description: {
messageDescriptor: t`Finally, select the target **object** ($(secondObject)).`,
},
placement: 'top',
},
isOnClosableDialog: true,
},
{
id: 'CloseInstructionEditorForCondition',
elementToHighlightId: '#instruction-editor-dialog #ok-button',
nextStepTrigger: { absenceOfElement: '#instruction-editor-dialog' },
tooltip: {
description: { messageDescriptor: t`We're good.` },
placement: 'top',
},
},
{
id: 'ClickOnNewAction',
elementToHighlightId: '#add-action-button-empty',
nextStepTrigger: { presenceOfElement: '#instruction-editor-dialog' },
tooltip: {
description: {
messageDescriptor: t`Let's add **what happens when the condition is met**: make $(secondObject) disappear.`,
},
placement: 'bottom',
},
},
{
id: 'ChoseCoinForAction',
elementToHighlightId: '#instruction-editor-dialog #object-item-1',
nextStepTrigger: { presenceOfElement: '#object-instruction-selector' },
tooltip: {
description: { messageDescriptor: t`Choose $(secondObject)` },
placement: 'bottom',
},
isOnClosableDialog: true,
},
{
id: 'ChooseAction',
elementToHighlightId: '#instruction-item-Delete',
nextStepTrigger: {
presenceOfElement: '#instruction-parameters-container',
},
tooltip: {
description: {
messageDescriptor: t`Then choose the **action** $(secondObject) will receive : "Delete", as we want to remove it.`,
},
placement: 'bottom',
},
isOnClosableDialog: true,
},
{
id: 'CloseInstructionEditorForAction',
elementToHighlightId: '#instruction-editor-dialog #ok-button',
nextStepTrigger: { absenceOfElement: '#instruction-editor-dialog' },
tooltip: {
description: { messageDescriptor: t`Nothing more is needed!` },
placement: 'top',
},
},
{
id: 'LaunchPreviewWithCoinCollection',
elementToHighlightId: '#toolbar-preview-button',
nextStepTrigger: { previewLaunched: true },
tooltip: {
title: { messageDescriptor: t`Let's see how it works! 🎮` },
placement: 'bottom',
},
},
{
id: 'WaitForUserToHavePlayedWithCoinCollection',
elementToHighlightId: '#toolbar-preview-button',
nextStepTrigger: {
clickOnTooltipButton: { messageDescriptor: t`I'm done` },
},
tooltip: {
description: {
messageDescriptor: t`Once you're done testing, close the **preview** and come back here.`,
},
placement: 'bottom',
},
},
],
};
export default inAppTutorial;

View File

@@ -3,10 +3,7 @@ import * as React from 'react';
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import { makeStyles } from '@material-ui/core/styles';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';
import ListItemAvatar from '@material-ui/core/ListItemAvatar';
import Text from '../../../../UI/Text';
import { Line, Column, Spacer } from '../../../../UI/Grid';
@@ -45,8 +42,6 @@ const path = optionalRequire('path');
const styles = {
listItem: {
padding: 0,
marginTop: 2,
marginBottom: 2,
borderRadius: 8,
overflowWrap: 'anywhere', // Ensure everything is wrapped on small devices.
},
@@ -56,6 +51,9 @@ const styles = {
width: 20,
height: 20,
},
mobileIconContainer: {
marginTop: 4, // To align with project title.
},
};
type AvatarWithStatusAndTooltipProps = {|
@@ -200,12 +198,6 @@ const getStorageProviderByInternalName = (
);
};
const useStylesForListItemIcon = makeStyles({
root: {
minWidth: 0,
},
});
type ProjectFileListItemProps = {|
file: FileMetadataAndStorageProviderName,
currentFileMetadata: ?FileMetadata,
@@ -226,7 +218,6 @@ export const ProjectFileListItem = ({
hideDeleteContextMenuAction,
}: ProjectFileListItemProps) => {
const contextMenu = React.useRef<?ContextMenuInterface>(null);
const iconClasses = useStylesForListItemIcon();
const { showDeleteConfirmation, showAlert } = useAlertDialog();
const { navigateToRoute } = React.useContext(RouterContext);
const [pendingProject, setPendingProject] = React.useState<?string>(null);
@@ -369,114 +360,99 @@ export const ProjectFileListItem = ({
onContextMenu={event => openContextMenu(event, file)}
{...longTouchForContextMenuProps}
>
<>
{storageProvider &&
storageProvider.renderIcon &&
!isWindowWidthMediumOrLarger && (
<ListItemAvatar
classes={iconClasses}
style={{
marginTop: 8,
alignSelf: 'flex-start',
{isWindowWidthMediumOrLarger ? (
<LineStackLayout justifyContent="flex-start" expand>
<Column expand>
<Line noMargin alignItems="center">
{storageProvider && storageProvider.renderIcon && (
<>
{storageProvider.renderIcon({
size: 'small',
})}
<Spacer />
</>
)}
<Text noMargin>
{file.fileMetadata.name || (
<PrettyBreakablePath
path={file.fileMetadata.fileIdentifier}
/>
)}
</Text>
{pendingProject === file.fileMetadata.fileIdentifier && (
<>
<Spacer />
<CircularProgress size={16} />
</>
)}
</Line>
</Column>
<Column expand>
<Text noMargin>
{storageProvider ? i18n._(storageProvider.name) : ''}
</Text>
</Column>
<Column expand>
<ListItemLastModification
file={file}
lastModifiedInfo={lastModifiedInfo}
storageProvider={storageProvider}
authenticatedUser={authenticatedUser}
currentFileMetadata={currentFileMetadata}
/>
</Column>
<ListItemSecondaryAction>
<IconButton
size="small"
edge="end"
aria-label="menu"
onClick={event => {
// prevent triggering the click on the list item.
event.stopPropagation();
openContextMenu(event, file);
}}
>
{storageProvider.renderIcon({
size: 'small',
})}
</ListItemAvatar>
)}
{isWindowWidthMediumOrLarger ? (
<LineStackLayout justifyContent="flex-start" expand>
<Column expand>
<Line noMargin alignItems="center">
{storageProvider && storageProvider.renderIcon && (
<>
{storageProvider.renderIcon({
size: 'small',
})}
<Spacer />
</>
)}
<Text noMargin>
{file.fileMetadata.name || (
<PrettyBreakablePath
path={file.fileMetadata.fileIdentifier}
/>
)}
</Text>
{pendingProject === file.fileMetadata.fileIdentifier && (
<>
<Spacer />
<CircularProgress size={16} />
</>
)}
</Line>
</Column>
<Column expand>
<ThreeDotsMenu />
</IconButton>
</ListItemSecondaryAction>
</LineStackLayout>
) : (
<Column expand>
<LineStackLayout alignItems="start">
{storageProvider && storageProvider.renderIcon && (
<Column noMargin>
<div style={styles.mobileIconContainer}>
{storageProvider.renderIcon({
size: 'small',
})}
</div>
</Column>
)}
<Column noMargin>
<Text noMargin>
{storageProvider ? i18n._(storageProvider.name) : ''}
{file.fileMetadata.name || (
<PrettyBreakablePath
path={file.fileMetadata.fileIdentifier}
/>
)}
</Text>
</Column>
<Column expand>
<ListItemLastModification
file={file}
lastModifiedInfo={lastModifiedInfo}
storageProvider={storageProvider}
authenticatedUser={authenticatedUser}
currentFileMetadata={currentFileMetadata}
textColor="secondary"
/>
</Column>
<ListItemSecondaryAction>
<IconButton
size="small"
edge="end"
aria-label="menu"
onClick={event => {
// prevent triggering the click on the list item.
event.stopPropagation();
openContextMenu(event, file);
}}
>
<ThreeDotsMenu />
</IconButton>
</ListItemSecondaryAction>
{pendingProject === file.fileMetadata.fileIdentifier && (
<CircularProgress size={24} />
)}
</LineStackLayout>
) : (
<Column expand>
<Line
noMargin
alignItems="center"
justifyContent="space-between"
>
<ListItemText
primary={
file.fileMetadata.name || (
<PrettyBreakablePath
path={file.fileMetadata.fileIdentifier}
/>
)
}
secondary={
<ListItemLastModification
file={file}
lastModifiedInfo={lastModifiedInfo}
storageProvider={storageProvider}
authenticatedUser={authenticatedUser}
currentFileMetadata={currentFileMetadata}
textColor="secondary"
/>
}
onContextMenu={event => openContextMenu(event, file)}
{...longTouchForContextMenuProps}
/>
{pendingProject === file.fileMetadata.fileIdentifier && (
<CircularProgress size={24} />
)}
</Line>
</Column>
)}
</>
</Column>
)}
</ListItem>
<ContextMenu
ref={contextMenu}

View File

@@ -1,5 +1,6 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import { Trans } from '@lingui/macro';
import List from '@material-ui/core/List';
import ListItem from '@material-ui/core/ListItem';
@@ -74,6 +75,7 @@ type Props = {|
privateGameTemplateListingData: PrivateGameTemplateListingData
) => void,
storageProviders: Array<StorageProvider>,
i18n: I18nType,
|};
export type BuildSectionInterface = {|
@@ -93,6 +95,7 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
onSelectPrivateGameTemplateListingData,
onOpenRecentFile,
storageProviders,
i18n,
},
ref
) => {
@@ -193,6 +196,7 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
exampleShortHeaders,
onSelectPrivateGameTemplateListingData,
onSelectExampleShortHeader,
i18n,
}),
[
authenticatedUser,
@@ -200,6 +204,7 @@ const BuildSection = React.forwardRef<Props, BuildSectionInterface>(
onSelectExampleShortHeader,
onSelectPrivateGameTemplateListingData,
privateGameTemplateListingDatas,
i18n,
]
);

View File

@@ -1,5 +1,5 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import { getUserPublicProfilesByIds } from '../../../../Utils/GDevelopServices/User';
import { type AuthenticatedUser } from '../../../../Profile/AuthenticatedUserContext';
import { type Profile } from '../../../../Utils/GDevelopServices/Authentication';
@@ -8,7 +8,7 @@ import { type FileMetadataAndStorageProviderName } from '../../../../ProjectsSto
import { type WidthType } from '../../../../UI/Reponsive/ResponsiveWindowMeasurer';
import { marginsSize } from '../../../../UI/Grid';
import { sendGameTemplateInformationOpened } from '../../../../Utils/Analytics/EventSender';
import ProductPriceTag from '../../../../AssetStore/ProductPriceTag';
import { getProductPriceOrOwnedLabel } from '../../../../AssetStore/ProductPriceTag';
import { prepareExampleShortHeaders } from '../../../../AssetStore/ExampleStore';
import { type PrivateGameTemplateListingData } from '../../../../Utils/GDevelopServices/Shop';
import { type ExampleShortHeader } from '../../../../Utils/GDevelopServices/Example';
@@ -114,6 +114,7 @@ export const getExampleAndTemplateItemsForCarousel = ({
exampleShortHeaders,
onSelectPrivateGameTemplateListingData,
onSelectExampleShortHeader,
i18n,
}: {|
authenticatedUser: AuthenticatedUser,
privateGameTemplateListingDatas?: ?Array<PrivateGameTemplateListingData>,
@@ -122,6 +123,7 @@ export const getExampleAndTemplateItemsForCarousel = ({
privateGameTemplateListingData: PrivateGameTemplateListingData
) => void,
onSelectExampleShortHeader: (exampleShortHeader: ExampleShortHeader) => void,
i18n: I18nType,
|}): Array<CarouselThumbnail> => {
const allItems: Array<CarouselThumbnail> = [];
const privateGameTemplateItems = [
@@ -155,12 +157,11 @@ export const getExampleAndTemplateItemsForCarousel = ({
privateGameTemplateListingData
);
},
overlayText: (
<ProductPriceTag
productListingData={privateGameTemplateListingData}
owned={isTemplateOwned}
/>
),
overlayText: getProductPriceOrOwnedLabel({
i18n,
productListingData: privateGameTemplateListingData,
owned: isTemplateOwned,
}),
overlayTextPosition: 'topLeft',
};
})

View File

@@ -97,12 +97,14 @@ const GetStartedSection = ({
inAppTutorialShortHeaders,
inAppTutorialsFetchingError,
fetchInAppTutorials,
currentlyRunningInAppTutorial,
} = React.useContext(InAppTutorialContext);
const { getTutorialProgress } = React.useContext(PreferencesContext);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const windowWidth = useResponsiveWindowWidth();
const isMobile = windowWidth === 'small';
const { currentlyRunningInAppTutorial } = React.useContext(
InAppTutorialContext
);
const items: {
key: string,
title: React.Node,

View File

@@ -78,11 +78,13 @@ const GuidedLessons = ({ selectInAppTutorial }: Props) => {
inAppTutorialShortHeaders,
inAppTutorialsFetchingError,
fetchInAppTutorials,
currentlyRunningInAppTutorial,
} = React.useContext(InAppTutorialContext);
const { getTutorialProgress } = React.useContext(PreferencesContext);
const authenticatedUser = React.useContext(AuthenticatedUserContext);
const windowWidth = useResponsiveWindowWidth();
const { currentlyRunningInAppTutorial } = React.useContext(
InAppTutorialContext
);
const getTutorialPartProgress = ({ tutorialId }: { tutorialId: string }) => {
const tutorialProgress = getTutorialProgress({

View File

@@ -319,6 +319,7 @@ export const HomePage = React.memo<Props>(
}
onOpenRecentFile={onOpenRecentFile}
storageProviders={storageProviders}
i18n={i18n}
/>
)}
{activeTab === 'learn' && (

View File

@@ -282,7 +282,7 @@ export default class ParticleEmitterEditor extends React.Component<
type="number"
value={particleEmitterConfiguration.getFlow()}
onChange={value => {
particleEmitterConfiguration.setFlow(parseInt(value, 10) || 0);
particleEmitterConfiguration.setFlow(parseFloat(value) || 0);
this.forceUpdate();
}}
/>
@@ -298,7 +298,7 @@ export default class ParticleEmitterEditor extends React.Component<
value={particleEmitterConfiguration.getEmitterForceMin()}
onChange={value => {
particleEmitterConfiguration.setEmitterForceMin(
parseInt(value, 10) || 0
parseFloat(value) || 0
);
this.forceUpdate();
}}
@@ -313,7 +313,7 @@ export default class ParticleEmitterEditor extends React.Component<
value={particleEmitterConfiguration.getEmitterForceMax()}
onChange={value => {
particleEmitterConfiguration.setEmitterForceMax(
parseInt(value, 10) || 0
parseFloat(value) || 0
);
this.forceUpdate();
}}
@@ -328,7 +328,7 @@ export default class ParticleEmitterEditor extends React.Component<
value={particleEmitterConfiguration.getConeSprayAngle()}
onChange={value => {
particleEmitterConfiguration.setConeSprayAngle(
parseInt(value, 10) || 0
parseFloat(value) || 0
);
this.forceUpdate();
}}
@@ -341,7 +341,7 @@ export default class ParticleEmitterEditor extends React.Component<
value={particleEmitterConfiguration.getZoneRadius()}
onChange={value => {
particleEmitterConfiguration.setZoneRadius(
parseInt(value, 10) || 0
parseFloat(value) || 0
);
this.forceUpdate();
}}

File diff suppressed because it is too large Load Diff

View File

@@ -1,117 +0,0 @@
// @flow
import { mapFor } from '../Utils/MapFor';
export type ObjectFolderOrObjectWithContext = {|
objectFolderOrObject: gdObjectFolderOrObject,
global: boolean,
|};
export const getObjectFolderOrObjectUnifiedName = (
objectFolderOrObject: gdObjectFolderOrObject
) =>
objectFolderOrObject.isFolder()
? objectFolderOrObject.getFolderName()
: objectFolderOrObject.getObject().getName();
export const enumerateObjectFolderOrObjects = (
project: gdObjectsContainer,
objectsContainer: gdObjectsContainer
): {|
containerObjectFolderOrObjectsList: ObjectFolderOrObjectWithContext[],
projectObjectFolderOrObjectsList: ObjectFolderOrObjectWithContext[],
|} => {
const projectRootFolder = project.getRootFolder();
const containerRootFolder = objectsContainer.getRootFolder();
const containerObjectFolderOrObjectsList: ObjectFolderOrObjectWithContext[] = mapFor(
0,
containerRootFolder.getChildrenCount(),
i => {
const objectFolderOrObject = containerRootFolder.getChildAt(i);
return objectFolderOrObject;
}
).map(
(
objectFolderOrObject: gdObjectFolderOrObject
): ObjectFolderOrObjectWithContext => {
const item = {
objectFolderOrObject,
global: false,
};
return item;
}
);
const projectObjectFolderOrObjectsList: ObjectFolderOrObjectWithContext[] = mapFor(
0,
projectRootFolder.getChildrenCount(),
i => {
const objectFolderOrObject = projectRootFolder.getChildAt(i);
return objectFolderOrObject;
}
).map(
(
objectFolderOrObject: gdObjectFolderOrObject
): ObjectFolderOrObjectWithContext => {
const item = {
objectFolderOrObject,
global: true,
};
return item;
}
);
return {
containerObjectFolderOrObjectsList,
projectObjectFolderOrObjectsList,
};
};
const recursivelyEnumerateFoldersInFolder = (
folder: gdObjectFolderOrObject,
prefix: string,
result: {| path: string, folder: gdObjectFolderOrObject |}[]
) => {
mapFor(0, folder.getChildrenCount(), i => {
const child = folder.getChildAt(i);
if (child.isFolder()) {
const newPrefix = prefix
? prefix + ' > ' + child.getFolderName()
: child.getFolderName();
result.push({
path: newPrefix,
folder: child,
});
recursivelyEnumerateFoldersInFolder(child, newPrefix, result);
}
});
};
export const enumerateFoldersInFolder = (folder: gdObjectFolderOrObject) => {
if (!folder.isFolder()) return [];
const result = [];
recursivelyEnumerateFoldersInFolder(folder, '', result);
return result;
};
export const enumerateFoldersInContainer = (
container: gdObjectsContainer
): {| path: string, folder: gdObjectFolderOrObject |}[] => {
const rootFolder = container.getRootFolder();
const result = [];
recursivelyEnumerateFoldersInFolder(rootFolder, '', result);
return result;
};
export const getObjectsInFolder = (
objectFolderOrObject: gdObjectFolderOrObject
): gdObject[] => {
if (!objectFolderOrObject.isFolder()) return [];
return mapFor(0, objectFolderOrObject.getChildrenCount(), i => {
const child = objectFolderOrObject.getChildAt(i);
if (child.isFolder()) {
return null;
}
return child.getObject();
}).filter(Boolean);
};

View File

@@ -1,6 +1,7 @@
// @flow
import { mapFor } from '../Utils/MapFor';
import flatten from 'lodash/flatten';
import { type SelectedTags, hasStringAllTags } from '../Utils/TagsHelper';
import { type RequiredExtension } from '../AssetStore/InstallAsset';
const gd: libGDevelop = global.gd;
@@ -157,22 +158,37 @@ export const enumerateObjectTypes = (
export type ObjectFilteringOptions = {|
searchText: string,
selectedTags: SelectedTags,
hideExactMatches?: boolean,
|};
export const filterObjectByTags = (
objectWithContext: ObjectWithContext,
selectedTags: SelectedTags
): boolean => {
if (!selectedTags.length) return true;
const objectTags = objectWithContext.object.getTags();
return hasStringAllTags(objectTags, selectedTags);
};
export const filterObjectsList = (
list: ObjectWithContextList,
{ searchText, hideExactMatches }: ObjectFilteringOptions
{ searchText, selectedTags, hideExactMatches }: ObjectFilteringOptions
): ObjectWithContextList => {
if (!searchText) return list;
if (!searchText && !selectedTags.length) return list;
return list.filter((objectWithContext: ObjectWithContext) => {
const objectName = objectWithContext.object.getName();
return list
.filter(objectWithContext =>
filterObjectByTags(objectWithContext, selectedTags)
)
.filter((objectWithContext: ObjectWithContext) => {
const objectName = objectWithContext.object.getName();
if (hideExactMatches && searchText === objectName) return undefined;
if (hideExactMatches && searchText === objectName) return undefined;
return objectName.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
});
return objectName.toLowerCase().indexOf(searchText.toLowerCase()) !== -1;
});
};
export type GroupFilteringOptions = {|

File diff suppressed because it is too large Load Diff

View File

@@ -25,9 +25,6 @@ const gd: libGDevelop = global.gd;
// or still require a global PIXI object to be accessible, so we expose PIXI here.
// This can be removed if no more extension PixiJS plugin requires this.
global.PIXI = PIXI;
// We also export it as GlobalPIXIModule, which is normally used in GDJS runtime and extensions
// to allow TypeScript typings of PIXI to work.
global.GlobalPIXIModule = { PIXI };
const requirableModules = {};

View File

@@ -60,12 +60,16 @@ export default function SubscriptionPendingDialog({
>
{!hasPlan ? (
<Column noMargin>
<Line>
<Line expand alignItems="center" justifyContent="center">
<Text>
<Trans>
Thanks for getting a subscription and supporting GDevelop!
</Trans>{' '}
{'💜'}{' '}
{'💜'}
</Text>
</Line>
<Line expand alignItems="center" justifyContent="center" noMargin>
<Text>
<b>
<Trans>
Your browser will now open to enter your payment details.
@@ -93,7 +97,7 @@ export default function SubscriptionPendingDialog({
</Column>
) : (
<Column noMargin>
<Line>
<Line expand alignItems="center" justifyContent="center">
<Text>
<Trans>
Thanks for getting a subscription and supporting GDevelop!

View File

@@ -31,6 +31,7 @@ import { type ResourceManagementProps } from '../ResourcesList/ResourceSource';
import { type HotReloadPreviewButtonProps } from '../HotReload/HotReloadPreviewButton';
import PublicGameProperties from '../GameDashboard/PublicGameProperties';
import PreviewIcon from '../UI/CustomSvgIcons/Preview';
import { useResponsiveWindowWidth } from '../UI/Reponsive/ResponsiveWindowMeasurer';
type Props = {|
project: gdProject,
@@ -210,6 +211,9 @@ const ProjectPropertiesDialog = (props: Props) => {
setUseDeprecatedZeroAsDefaultZOrder,
] = React.useState(initialProperties.useDeprecatedZeroAsDefaultZOrder);
const windowWidth = useResponsiveWindowWidth();
const isMobile = windowWidth === 'small';
const defaultPackageName = 'com.example.mygame';
const defaultVersion = '1.0.0';
@@ -302,7 +306,13 @@ const ProjectPropertiesDialog = (props: Props) => {
<FlatButton
key="hot-reload-preview-button"
leftIcon={<PreviewIcon />}
label={<Trans>Run a preview (with loading & branding)</Trans>}
label={
isMobile ? (
<Trans>Preview</Trans>
) : (
<Trans>Run a preview (with loading & branding)</Trans>
)
}
onClick={
hotReloadPreviewButtonProps.launchProjectWithLoadingScreenPreview
}

View File

@@ -16,7 +16,6 @@ import { type InstancesEditorShortcutsCallbacks } from '../InstancesEditor';
import { type EditorId } from '.';
import Rectangle from '../Utils/Rectangle';
import ViewPosition from '../InstancesEditor/ViewPosition';
import { type ObjectFolderOrObjectWithContext } from '../ObjectsList/EnumerateObjectFolderOrObject';
export type SceneEditorsDisplayProps = {|
project: gdProject,
@@ -32,7 +31,8 @@ export type SceneEditorsDisplayProps = {|
editInstanceVariables: (instance: ?gdInitialInstance) => void,
editObjectByName: (objectName: string, initialTab?: ObjectEditorTab) => void,
onEditObject: gdObject => void,
selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
selectedObjectNames: string[],
renamedObjectWithContext: ?ObjectWithContext,
onSelectLayer: (layerName: string) => void,
editLayerEffects: (layer: ?gdLayer) => void,
editLayer: (layer: ?gdLayer) => void,
@@ -43,9 +43,7 @@ export type SceneEditorsDisplayProps = {|
done: (boolean) => void
) => void,
onObjectCreated: gdObject => void,
onObjectFolderOrObjectWithContextSelected: (
?ObjectFolderOrObjectWithContext
) => void,
onObjectSelected: (?ObjectWithContext) => void,
onExportObject: (object: ?gdObject) => void,
onDeleteObject: (
i18n: I18nType,
@@ -56,8 +54,9 @@ export type SceneEditorsDisplayProps = {|
objectName: string,
targetPosition?: 'center' | 'upperCenter'
) => void,
onRenameObjectFolderOrObjectWithContextFinish: (
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
onRenameObjectStart: (?ObjectWithContext) => void,
onRenameObjectFinish: (
objectWithContext: ObjectWithContext,
newName: string,
done: (boolean) => void
) => void,
@@ -122,7 +121,6 @@ export type SceneEditorsDisplayInterface = {|
openNewObjectDialog: () => void,
toggleEditorView: (editorId: EditorId) => void,
isEditorVisible: (editorId: EditorId) => boolean,
renameObjectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext => void,
viewControls: {|
zoomBy: (factor: number) => void,
setZoomFactor: (factor: number) => void,

View File

@@ -3,6 +3,7 @@
import * as React from 'react';
import { t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import { useResponsiveWindowWidth } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
import PreferencesContext from '../../MainFrame/Preferences/PreferencesContext';
@@ -13,14 +14,19 @@ import InstancePropertiesEditor, {
} from '../../InstancesEditor/InstancePropertiesEditor';
import LayersList, { type LayersListInterface } from '../../LayersList';
import FullSizeInstancesEditorWithScrollbars from '../../InstancesEditor/FullSizeInstancesEditorWithScrollbars';
import TagsButton from '../../UI/EditorMosaic/TagsButton';
import CloseButton from '../../UI/EditorMosaic/CloseButton';
import ObjectsList, { type ObjectsListInterface } from '../../ObjectsList';
import ObjectGroupsList, {
type ObjectGroupsListInterface,
} from '../../ObjectGroupsList';
import ObjectGroupsList from '../../ObjectGroupsList';
import InstancesList from '../../InstancesEditor/InstancesList';
import ObjectsRenderingService from '../../ObjectsRendering/ObjectsRenderingService';
import {
getTagsFromString,
buildTagsMenuTemplate,
type SelectedTags,
} from '../../Utils/TagsHelper';
import { enumerateObjects } from '../../ObjectsList/EnumerateObjects';
import Rectangle from '../../Utils/Rectangle';
import { type EditorId } from '..';
import {
@@ -89,6 +95,10 @@ const MosaicEditorsDisplay = React.forwardRef<
setDefaultEditorMosaicNode,
} = React.useContext(PreferencesContext);
const selectedInstances = props.instancesSelection.getSelectedInstances();
const [
selectedObjectTags,
setSelectedObjectTags,
] = React.useState<SelectedTags>([]);
const instancesPropertiesEditorRef = React.useRef<?InstancePropertiesEditorInterface>(
null
@@ -98,7 +108,7 @@ const MosaicEditorsDisplay = React.forwardRef<
const editorRef = React.useRef<?InstancesEditor>(null);
const objectsListRef = React.useRef<?ObjectsListInterface>(null);
const editorMosaicRef = React.useRef<?EditorMosaic>(null);
const objectGroupsListRef = React.useRef<?ObjectGroupsListInterface>(null);
const objectGroupsListRef = React.useRef<?ObjectGroupsList>(null);
const forceUpdateInstancesPropertiesEditor = React.useCallback(() => {
if (instancesPropertiesEditorRef.current)
@@ -140,15 +150,6 @@ const MosaicEditorsDisplay = React.forwardRef<
if (!editorMosaicRef.current) return false;
return editorMosaicRef.current.getOpenedEditorNames().includes(editorId);
}, []);
const renameObjectFolderOrObjectWithContext = React.useCallback(
objectWithContext => {
if (objectsListRef.current)
objectsListRef.current.renameObjectFolderOrObjectWithContext(
objectWithContext
);
},
[]
);
React.useImperativeHandle(ref, () => {
const { current: editor } = editorRef;
@@ -162,7 +163,6 @@ const MosaicEditorsDisplay = React.forwardRef<
openNewObjectDialog,
toggleEditorView,
isEditorVisible,
renameObjectFolderOrObjectWithContext,
viewControls: {
zoomBy: editor ? editor.zoomBy : noop,
setZoomFactor: editor ? editor.setZoomFactor : noop,
@@ -212,13 +212,29 @@ const MosaicEditorsDisplay = React.forwardRef<
]
);
const selectedObjectNames = props.selectedObjectFolderOrObjectsWithContext
.map(objectFolderOrObjectWithContext => {
const { objectFolderOrObject } = objectFolderOrObjectWithContext;
if (objectFolderOrObject.isFolder()) return null;
return objectFolderOrObject.getObject().getName();
})
.filter(Boolean);
const getAllObjectTags = React.useCallback(
(): Array<string> => {
const tagsSet: Set<string> = new Set();
enumerateObjects(project, layout).allObjectsList.forEach(({ object }) => {
getTagsFromString(object.getTags()).forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet);
},
[project, layout]
);
const buildObjectTagsMenuTemplate = React.useCallback(
(i18n: I18nType): Array<any> => {
return buildTagsMenuTemplate({
noTagLabel: i18n._(t`No tags - add a tag to an object first`),
getAllTags: getAllObjectTags,
selectedTags: selectedObjectTags,
onChange: setSelectedObjectTags,
});
},
[selectedObjectTags, getAllObjectTags]
);
const editors = {
properties: {
@@ -296,7 +312,7 @@ const MosaicEditorsDisplay = React.forwardRef<
onInstancesMoved={props.onInstancesMoved}
onInstancesResized={props.onInstancesResized}
onInstancesRotated={props.onInstancesRotated}
selectedObjectNames={selectedObjectNames}
selectedObjectNames={props.selectedObjectNames}
onContextMenu={props.onContextMenu}
isInstanceOf3DObject={props.isInstanceOf3DObject}
instancesEditorShortcutsCallbacks={
@@ -312,7 +328,13 @@ const MosaicEditorsDisplay = React.forwardRef<
'objects-list': {
type: 'secondary',
title: t`Objects`,
toolbarControls: [<CloseButton key="close" />],
toolbarControls: [
<TagsButton
key="tags"
buildMenuTemplate={buildObjectTagsMenuTemplate}
/>,
<CloseButton key="close" />,
],
renderEditor: () => (
<I18n>
{({ i18n }) => (
@@ -328,9 +350,7 @@ const MosaicEditorsDisplay = React.forwardRef<
props.onSelectAllInstancesOfObjectInLayout
}
resourceManagementProps={props.resourceManagementProps}
selectedObjectFolderOrObjectsWithContext={
props.selectedObjectFolderOrObjectsWithContext
}
selectedObjectNames={props.selectedObjectNames}
canInstallPrivateAsset={props.canInstallPrivateAsset}
onEditObject={props.onEditObject}
onExportObject={props.onExportObject}
@@ -341,17 +361,18 @@ const MosaicEditorsDisplay = React.forwardRef<
props.getValidatedObjectOrGroupName(newName, global, i18n)
}
onObjectCreated={props.onObjectCreated}
onObjectFolderOrObjectWithContextSelected={
props.onObjectFolderOrObjectWithContextSelected
}
onRenameObjectFolderOrObjectWithContextFinish={
props.onRenameObjectFolderOrObjectWithContextFinish
}
onObjectSelected={props.onObjectSelected}
renamedObjectWithContext={props.renamedObjectWithContext}
onRenameObjectStart={props.onRenameObjectStart}
onRenameObjectFinish={props.onRenameObjectFinish}
onAddObjectInstance={props.onAddObjectInstance}
onObjectPasted={props.updateBehaviorsSharedData}
selectedObjectTags={selectedObjectTags}
beforeSetAsGlobalObject={objectName =>
props.canObjectOrGroupBeGlobal(i18n, objectName)
}
onChangeSelectedObjectTags={setSelectedObjectTags}
getAllObjectTags={getAllObjectTags}
ref={objectsListRef}
unsavedChanges={props.unsavedChanges}
hotReloadPreviewButtonProps={props.hotReloadPreviewButtonProps}

View File

@@ -98,7 +98,7 @@ type Props = {|
title: React.Node,
openingState: DrawerOpeningState,
setOpeningState: DrawerOpeningState => void,
topBarControls?: ?React.Node,
topBarControls: ?React.Node,
|};
const SwipeableDrawer = (props: Props) => {

View File

@@ -1,21 +1,27 @@
// @flow
import * as React from 'react';
import { Trans } from '@lingui/macro';
import { Trans, t } from '@lingui/macro';
import { I18n } from '@lingui/react';
import { type I18n as I18nType } from '@lingui/core';
import InstancesEditor from '../../InstancesEditor';
import InstancePropertiesEditor, {
type InstancePropertiesEditorInterface,
} from '../../InstancesEditor/InstancePropertiesEditor';
import LayersList, { type LayersListInterface } from '../../LayersList';
import TagsButton from '../../UI/EditorMosaic/TagsButton';
import ObjectsList, { type ObjectsListInterface } from '../../ObjectsList';
import ObjectGroupsList, {
type ObjectGroupsListInterface,
} from '../../ObjectGroupsList';
import ObjectGroupsList from '../../ObjectGroupsList';
import InstancesList from '../../InstancesEditor/InstancesList';
import ObjectsRenderingService from '../../ObjectsRendering/ObjectsRenderingService';
import {
getTagsFromString,
buildTagsMenuTemplate,
type SelectedTags,
} from '../../Utils/TagsHelper';
import { enumerateObjects } from '../../ObjectsList/EnumerateObjects';
import Rectangle from '../../Utils/Rectangle';
import SwipeableDrawer from './SwipeableDrawer';
import BottomToolbar from './BottomToolbar';
@@ -58,6 +64,10 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
onSelectInstances,
} = props;
const selectedInstances = props.instancesSelection.getSelectedInstances();
const [
selectedObjectTags,
setSelectedObjectTags,
] = React.useState<SelectedTags>([]);
const { values } = React.useContext(PreferencesContext);
const screenType = useScreenType();
@@ -68,7 +78,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
const instancesListRef = React.useRef<?InstancesList>(null);
const editorRef = React.useRef<?InstancesEditor>(null);
const objectsListRef = React.useRef<?ObjectsListInterface>(null);
const objectGroupsListRef = React.useRef<?ObjectGroupsListInterface>(null);
const objectGroupsListRef = React.useRef<?ObjectGroupsList>(null);
const [selectedEditorId, setSelectedEditorId] = React.useState<?EditorId>(
null
@@ -126,15 +136,6 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
},
[selectedEditorId, drawerOpeningState]
);
const renameObjectFolderOrObjectWithContext = React.useCallback(
objectWithContext => {
if (objectsListRef.current)
objectsListRef.current.renameObjectFolderOrObjectWithContext(
objectWithContext
);
},
[]
);
React.useImperativeHandle(ref, () => {
const { current: editor } = editorRef;
@@ -149,7 +150,6 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
openNewObjectDialog,
toggleEditorView: halfOpenOrCloseDrawerOnEditor,
isEditorVisible,
renameObjectFolderOrObjectWithContext,
viewControls: {
zoomBy: editor ? editor.zoomBy : noop,
setZoomFactor: editor ? editor.setZoomFactor : noop,
@@ -199,13 +199,29 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
]
);
const selectedObjectNames = props.selectedObjectFolderOrObjectsWithContext
.map(objectFolderOrObjectWithContext => {
const { objectFolderOrObject } = objectFolderOrObjectWithContext;
if (objectFolderOrObject.isFolder()) return null;
return objectFolderOrObject.getObject().getName();
})
.filter(Boolean);
const getAllObjectTags = React.useCallback(
(): Array<string> => {
const tagsSet: Set<string> = new Set();
enumerateObjects(project, layout).allObjectsList.forEach(({ object }) => {
getTagsFromString(object.getTags()).forEach(tag => tagsSet.add(tag));
});
return Array.from(tagsSet);
},
[project, layout]
);
const buildObjectTagsMenuTemplate = React.useCallback(
(i18n: I18nType): Array<any> => {
return buildTagsMenuTemplate({
noTagLabel: i18n._(t`No tags - add a tag to an object first`),
getAllTags: getAllObjectTags,
selectedTags: selectedObjectTags,
onChange: setSelectedObjectTags,
});
},
[selectedObjectTags, getAllObjectTags]
);
return (
<FullSizeMeasurer>
@@ -231,7 +247,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
onInstancesMoved={props.onInstancesMoved}
onInstancesResized={props.onInstancesResized}
onInstancesRotated={props.onInstancesRotated}
selectedObjectNames={selectedObjectNames}
selectedObjectNames={props.selectedObjectNames}
onContextMenu={props.onContextMenu}
isInstanceOf3DObject={props.isInstanceOf3DObject}
instancesEditorShortcutsCallbacks={
@@ -248,6 +264,17 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
}
openingState={drawerOpeningState}
setOpeningState={setDrawerOpeningState}
topBarControls={
selectedEditorId === 'objects-list'
? [
<TagsButton
key="tags"
size="small"
buildMenuTemplate={buildObjectTagsMenuTemplate}
/>,
]
: null
}
>
{selectedEditorId === 'objects-list' && (
<I18n>
@@ -264,9 +291,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
props.onSelectAllInstancesOfObjectInLayout
}
resourceManagementProps={props.resourceManagementProps}
selectedObjectFolderOrObjectsWithContext={
props.selectedObjectFolderOrObjectsWithContext
}
selectedObjectNames={props.selectedObjectNames}
canInstallPrivateAsset={props.canInstallPrivateAsset}
onEditObject={props.onEditObject}
onExportObject={props.onExportObject}
@@ -281,19 +306,20 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
)
}
onObjectCreated={props.onObjectCreated}
onObjectFolderOrObjectWithContextSelected={
props.onObjectFolderOrObjectWithContextSelected
}
onRenameObjectFolderOrObjectWithContextFinish={
props.onRenameObjectFolderOrObjectWithContextFinish
}
onObjectSelected={props.onObjectSelected}
renamedObjectWithContext={props.renamedObjectWithContext}
onRenameObjectStart={props.onRenameObjectStart}
onRenameObjectFinish={props.onRenameObjectFinish}
onAddObjectInstance={objectName =>
props.onAddObjectInstance(objectName, 'upperCenter')
}
onObjectPasted={props.updateBehaviorsSharedData}
selectedObjectTags={selectedObjectTags}
beforeSetAsGlobalObject={objectName =>
props.canObjectOrGroupBeGlobal(i18n, objectName)
}
onChangeSelectedObjectTags={setSelectedObjectTags}
getAllObjectTags={getAllObjectTags}
ref={objectsListRef}
unsavedChanges={props.unsavedChanges}
hotReloadPreviewButtonProps={

View File

@@ -5,6 +5,7 @@ import { type I18n as I18nType } from '@lingui/core';
import { t } from '@lingui/macro';
import * as React from 'react';
import uniq from 'lodash/uniq';
import LayerRemoveDialog from '../LayersList/LayerRemoveDialog';
import LayerEditorDialog from '../LayersList/LayerEditorDialog';
import VariablesEditorDialog from '../VariablesList/VariablesEditorDialog';
@@ -42,8 +43,10 @@ import PixiResourcesLoader from '../ObjectsRendering/PixiResourcesLoader';
import {
type ObjectWithContext,
type GroupWithContext,
enumerateObjects,
} from '../ObjectsList/EnumerateObjects';
import InfoBar from '../UI/Messages/InfoBar';
import { type SelectedTags } from '../Utils/TagsHelper';
import { type UnsavedChanges } from '../MainFrame/UnsavedChangesContext';
import SceneVariablesDialog from './SceneVariablesDialog';
import { onObjectAdded, onInstanceAdded } from '../Hints/ObjectsAdditionalWork';
@@ -59,11 +62,6 @@ import MosaicEditorsDisplay from './MosaicEditorsDisplay';
import SwipeableDrawerEditorsDisplay from './SwipeableDrawerEditorsDisplay';
import { type SceneEditorsDisplayInterface } from './EditorsDisplay.flow';
import newNameGenerator from '../Utils/NewNameGenerator';
import {
getObjectFolderOrObjectUnifiedName,
type ObjectFolderOrObjectWithContext,
} from '../ObjectsList/EnumerateObjectFolderOrObject';
import uniq from 'lodash/uniq';
const gd: libGDevelop = global.gd;
@@ -132,7 +130,11 @@ type State = {|
showAdditionalWorkInfoBar: boolean,
additionalWorkInfoBar: InfoBarDetails,
selectedObjectFolderOrObjectsWithContext: Array<ObjectFolderOrObjectWithContext>,
// State for tags of objects:
selectedObjectTags: SelectedTags,
renamedObjectWithContext: ?ObjectWithContext,
selectedObjectsWithContext: Array<ObjectWithContext>,
selectedLayer: string,
|};
@@ -185,7 +187,10 @@ export default class SceneEditor extends React.Component<Props, State> {
touchScreenMessage: '',
},
selectedObjectFolderOrObjectsWithContext: [],
selectedObjectTags: [],
renamedObjectWithContext: null,
selectedObjectsWithContext: [],
selectedLayer: BASE_LAYER_NAME,
invisibleLayerOnWhichInstancesHaveJustBeenAdded: null,
};
@@ -237,9 +242,7 @@ export default class SceneEditor extends React.Component<Props, State> {
redo={this.redo}
onOpenSettings={this.openSceneProperties}
settingsIcon={editSceneIconReactNode}
canRenameObject={
this.state.selectedObjectFolderOrObjectsWithContext.length === 1
}
canRenameObject={this.state.selectedObjectsWithContext.length === 1}
onRenameObject={this._startRenamingSelectedObject}
/>
);
@@ -268,9 +271,7 @@ export default class SceneEditor extends React.Component<Props, State> {
redo={this.redo}
onOpenSettings={this.openSceneProperties}
settingsIcon={editSceneIconReactNode}
canRenameObject={
this.state.selectedObjectFolderOrObjectsWithContext.length === 1
}
canRenameObject={this.state.selectedObjectsWithContext.length === 1}
onRenameObject={this._startRenamingSelectedObject}
/>
);
@@ -465,19 +466,15 @@ export default class SceneEditor extends React.Component<Props, State> {
);
};
_onObjectFolderOrObjectWithContextSelected = (
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext = null
) => {
const selectedObjectFolderOrObjectsWithContext = [];
if (objectFolderOrObjectWithContext) {
selectedObjectFolderOrObjectsWithContext.push(
objectFolderOrObjectWithContext
);
_onObjectSelected = (objectWithContext: ?ObjectWithContext = null) => {
const selectedObjectsWithContext = [];
if (objectWithContext) {
selectedObjectsWithContext.push(objectWithContext);
}
this.setState(
{
selectedObjectFolderOrObjectsWithContext,
selectedObjectsWithContext,
},
() => {
// We update the toolbar because we need to update the objects selected
@@ -577,38 +574,23 @@ export default class SceneEditor extends React.Component<Props, State> {
};
_onInstancesSelected = (instances: Array<gdInitialInstance>) => {
if (instances.length === 0) {
this.setState({ selectedObjectFolderOrObjectsWithContext: [] });
return;
}
const { project, layout } = this.props;
// TODO: Find a way to select efficiently the ObjectFolderOrObject instances
// representing all the instances selected.
const lastSelectedInstance = instances[instances.length - 1];
const objectName = lastSelectedInstance.getObjectName();
if (project.hasObjectNamed(objectName)) {
this.setState({
selectedObjectFolderOrObjectsWithContext: [
{
objectFolderOrObject: project
.getRootFolder()
.getObjectNamed(objectName),
global: true,
},
],
});
} else if (layout.hasObjectNamed(objectName)) {
this.setState({
selectedObjectFolderOrObjectsWithContext: [
{
objectFolderOrObject: layout
.getRootFolder()
.getObjectNamed(objectName),
global: false,
},
],
});
}
const instancesObjectNames = uniq(
instances.map(instance => instance.getObjectName())
);
const selectedObjectsWithContext = enumerateObjects(project, layout, {
names: instancesObjectNames,
}).allObjectsList;
this.setState(
{
selectedObjectsWithContext,
},
() => {
this.updateToolbar();
}
);
};
_onInstanceDoubleClicked = (instance: gdInitialInstance) => {
@@ -768,16 +750,25 @@ export default class SceneEditor extends React.Component<Props, State> {
});
};
_startRenamingSelectedObject = () => {
const firstSelectedObjectFolderOrObject = this.state
.selectedObjectFolderOrObjectsWithContext[0];
if (!firstSelectedObjectFolderOrObject) return;
_onRenameObjectStart = (objectWithContext: ?ObjectWithContext) => {
const selectedObjectsWithContext = [];
if (objectWithContext) {
selectedObjectsWithContext.push(objectWithContext);
}
if (this.editorDisplay)
this.editorDisplay.renameObjectFolderOrObjectWithContext(
firstSelectedObjectFolderOrObject
);
this.updateToolbar();
this.setState(
{
renamedObjectWithContext: objectWithContext,
selectedObjectsWithContext,
},
() => {
this.updateToolbar();
}
);
};
_startRenamingSelectedObject = () => {
this._onRenameObjectStart(this.state.selectedObjectsWithContext[0]);
};
_onRenameLayer = (
@@ -893,75 +884,42 @@ export default class SceneEditor extends React.Component<Props, State> {
const { editedObjectWithContext } = this.state;
if (editedObjectWithContext) {
this._onRenameObjectFinish(editedObjectWithContext, newName);
this._onRenameObjectFinish(editedObjectWithContext, newName, () => {});
}
};
_onRenameObjectFinish = (
objectWithContext: ObjectWithContext,
newName: string
newName: string,
done: boolean => void
) => {
const { object, global } = objectWithContext;
const { project, layout } = this.props;
// newName is supposed to have been already validated.
// Avoid triggering renaming refactoring if name has not really changed
if (object.getName() === newName) {
return;
}
if (global) {
gd.WholeProjectRefactorer.globalObjectOrGroupRenamed(
project,
object.getName(),
newName,
/* isObjectGroup=*/ false
);
} else {
gd.WholeProjectRefactorer.objectOrGroupRenamedInLayout(
project,
layout,
object.getName(),
newName,
/* isObjectGroup=*/ false
);
// Avoid triggering renaming refactoring if name has not really changed
if (object.getName() !== newName) {
if (global) {
gd.WholeProjectRefactorer.globalObjectOrGroupRenamed(
project,
object.getName(),
newName,
/* isObjectGroup=*/ false
);
} else {
gd.WholeProjectRefactorer.objectOrGroupRenamedInLayout(
project,
layout,
object.getName(),
newName,
/* isObjectGroup=*/ false
);
}
}
object.setName(newName);
};
_onRenameObjectFolderOrObjectWithContextFinish = (
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
newName: string,
done: boolean => void
) => {
const { objectFolderOrObject, global } = objectFolderOrObjectWithContext;
const unifiedName = getObjectFolderOrObjectUnifiedName(
objectFolderOrObject
);
// Avoid triggering renaming refactoring if name has not really changed
if (unifiedName === newName) {
this._onObjectFolderOrObjectWithContextSelected(
objectFolderOrObjectWithContext
);
done(false);
return;
}
// newName is supposed to have been already validated.
if (objectFolderOrObject.isFolder()) {
objectFolderOrObject.setFolderName(newName);
done(true);
return;
}
const object = objectFolderOrObject.getObject();
this._onRenameObjectFinish({ object, global }, newName);
this._onObjectFolderOrObjectWithContextSelected(
objectFolderOrObjectWithContext
);
this._onObjectSelected(objectWithContext);
done(true);
};
@@ -1118,7 +1076,7 @@ export default class SceneEditor extends React.Component<Props, State> {
this.setState(
{
selectedObjectFolderOrObjectsWithContext: [],
selectedObjectsWithContext: [],
history: saveToHistory(
this.state.history,
this.props.initialInstances,
@@ -1178,76 +1136,6 @@ export default class SceneEditor extends React.Component<Props, State> {
];
};
getContextMenuLayoutItems = (i18n: I18nType) => [
{
label: i18n._(t`Open scene events`),
click: () => this.props.onOpenEvents(this.props.layout.getName()),
},
{
label: i18n._(t`Open scene properties`),
click: () => this.openSceneProperties(true),
},
];
getContextMenuInstancesWiseItems = (i18n: I18nType) => {
const hasSelectedInstances = this.instancesSelection.hasSelectedInstances();
return [
{
label: i18n._(t`Copy`),
click: () => this.copySelection(),
enabled: hasSelectedInstances,
accelerator: 'CmdOrCtrl+C',
},
{
label: i18n._(t`Cut`),
click: () => this.cutSelection(),
enabled: hasSelectedInstances,
accelerator: 'CmdOrCtrl+X',
},
{
label: i18n._(t`Paste`),
click: () => this.paste(),
enabled: Clipboard.has(INSTANCES_CLIPBOARD_KIND),
accelerator: 'CmdOrCtrl+V',
},
{
label: i18n._(t`Duplicate`),
enabled: hasSelectedInstances,
click: () => {
this.duplicateSelection();
},
accelerator: 'CmdOrCtrl+D',
},
{ type: 'separator' },
{
label: i18n._(t`Bring to front`),
enabled: hasSelectedInstances,
click: () => {
this._onMoveInstancesZOrder('front');
},
},
{
label: i18n._(t`Send to back`),
enabled: hasSelectedInstances,
click: () => {
this._onMoveInstancesZOrder('back');
},
},
{ type: 'separator' },
{
label: i18n._(t`Show/Hide instance properties`),
click: () => this.toggleProperties(),
enabled: hasSelectedInstances,
},
{
label: i18n._(t`Delete`),
click: () => this.deleteSelection(),
enabled: hasSelectedInstances,
accelerator: 'Delete',
},
];
};
setZoomFactor = (zoomFactor: number) => {
if (this.editorDisplay)
this.editorDisplay.viewControls.setZoomFactor(zoomFactor);
@@ -1282,11 +1170,13 @@ export default class SceneEditor extends React.Component<Props, State> {
};
buildContextMenu = (i18n: I18nType, layout: gdLayout, options: any) => {
let contextMenuItems = [];
if (
options.ignoreSelectedObjectsForContextMenu ||
!this.instancesSelection.hasSelectedInstances()
this.state.selectedObjectsWithContext.length === 0
) {
return [
contextMenuItems = [
...contextMenuItems,
{
label: i18n._(t`Paste`),
click: () => this.paste(),
@@ -1300,28 +1190,64 @@ export default class SceneEditor extends React.Component<Props, State> {
},
{ type: 'separator' },
...this.getContextMenuZoomItems(i18n),
{ type: 'separator' },
...this.getContextMenuLayoutItems(i18n),
];
}
const instances = this.instancesSelection.getSelectedInstances();
if (
instances.length === 1 ||
uniq(instances.map(instance => instance.getObjectName())).length === 1
) {
const { project, layout } = this.props;
const objectName = instances[0].getObjectName();
const object = getObjectByName(project, layout, objectName);
const objectMetadata = object
? gd.MetadataProvider.getObjectMetadata(
project.getCurrentPlatform(),
object.getType()
)
: null;
return [
...this.getContextMenuInstancesWiseItems(i18n),
} else {
const objectName = this.state.selectedObjectsWithContext[0].object.getName();
contextMenuItems = [
...contextMenuItems,
{
label: i18n._(t`Copy`),
click: () => this.copySelection(),
enabled: this.instancesSelection.hasSelectedInstances(),
accelerator: 'CmdOrCtrl+C',
},
{
label: i18n._(t`Cut`),
click: () => this.cutSelection(),
enabled: this.instancesSelection.hasSelectedInstances(),
accelerator: 'CmdOrCtrl+X',
},
{
label: i18n._(t`Paste`),
click: () => this.paste(),
enabled: Clipboard.has(INSTANCES_CLIPBOARD_KIND),
accelerator: 'CmdOrCtrl+V',
},
{
label: i18n._(t`Duplicate`),
enabled: this.instancesSelection.hasSelectedInstances(),
click: () => {
this.duplicateSelection();
},
accelerator: 'CmdOrCtrl+D',
},
{ type: 'separator' },
{
label: i18n._(t`Bring to front`),
enabled: this.instancesSelection.hasSelectedInstances(),
click: () => {
this._onMoveInstancesZOrder('front');
},
},
{
label: i18n._(t`Send to back`),
enabled: this.instancesSelection.hasSelectedInstances(),
click: () => {
this._onMoveInstancesZOrder('back');
},
},
{ type: 'separator' },
{
label: i18n._(t`Show/Hide instance properties`),
click: () => this.toggleProperties(),
enabled: this.instancesSelection.hasSelectedInstances(),
},
{
label: i18n._(t`Delete`),
click: () => this.deleteSelection(),
enabled: this.instancesSelection.hasSelectedInstances(),
accelerator: 'Delete',
},
{ type: 'separator' },
{
label: i18n._(t`Edit object ${shortenString(objectName, 14)}`),
@@ -1335,24 +1261,27 @@ export default class SceneEditor extends React.Component<Props, State> {
label: i18n._(t`Edit behaviors`),
click: () => this.editObjectByName(objectName, 'behaviors'),
},
objectMetadata
? {
label: i18n._(t`Edit effects`),
click: () => this.editObjectByName(objectName, 'effects'),
enabled: objectMetadata.hasDefaultBehavior(
'EffectCapability::EffectBehavior'
),
}
: null,
{ type: 'separator' },
...this.getContextMenuLayoutItems(i18n),
].filter(Boolean);
{
label: i18n._(t`Edit effects`),
click: () => this.editObjectByName(objectName, 'effects'),
},
];
}
return [
...this.getContextMenuInstancesWiseItems(i18n),
contextMenuItems = [
...contextMenuItems,
{ type: 'separator' },
...this.getContextMenuLayoutItems(i18n),
{
label: i18n._(t`Open scene events`),
click: () => this.props.onOpenEvents(layout.getName()),
},
{
label: i18n._(t`Open scene properties`),
click: () => this.openSceneProperties(true),
},
];
return contextMenuItems;
};
copySelection = ({
@@ -1535,10 +1464,7 @@ export default class SceneEditor extends React.Component<Props, State> {
resourceManagementProps,
isActive,
} = this.props;
const {
editedObjectWithContext,
selectedObjectFolderOrObjectsWithContext,
} = this.state;
const { editedObjectWithContext } = this.state;
const variablesEditedAssociatedObjectName = this.state
.variablesEditedInstance
? this.state.variablesEditedInstance.getObjectName()
@@ -1546,7 +1472,9 @@ export default class SceneEditor extends React.Component<Props, State> {
const variablesEditedAssociatedObject = variablesEditedAssociatedObjectName
? getObjectByName(project, layout, variablesEditedAssociatedObjectName)
: null;
const selectedObjectNames = this.state.selectedObjectsWithContext.map(
objWithContext => objWithContext.object.getName()
);
// Deactivate prettier on this variable to prevent spaces to be added by
// line breaks.
// prettier-ignore
@@ -1599,9 +1527,8 @@ export default class SceneEditor extends React.Component<Props, State> {
editLayerEffects={this.editLayerEffects}
editInstanceVariables={this.editInstanceVariables}
editObjectByName={this.editObjectByName}
selectedObjectFolderOrObjectsWithContext={
selectedObjectFolderOrObjectsWithContext
}
selectedObjectNames={selectedObjectNames}
renamedObjectWithContext={this.state.renamedObjectWithContext}
onRenameLayer={this._onRenameLayer}
onRemoveLayer={this._onRemoveLayer}
onSelectLayer={(layer: string) =>
@@ -1618,13 +1545,10 @@ export default class SceneEditor extends React.Component<Props, State> {
canObjectOrGroupBeGlobal={this.canObjectOrGroupBeGlobal}
updateBehaviorsSharedData={this.updateBehaviorsSharedData}
onEditObject={this.props.onEditObject || this.editObject}
onRenameObjectFolderOrObjectWithContextFinish={
this._onRenameObjectFolderOrObjectWithContextFinish
}
onRenameObjectStart={this._onRenameObjectStart}
onRenameObjectFinish={this._onRenameObjectFinish}
onObjectCreated={this._onObjectCreated}
onObjectFolderOrObjectWithContextSelected={
this._onObjectFolderOrObjectWithContextSelected
}
onObjectSelected={this._onObjectSelected}
canInstallPrivateAsset={this.props.canInstallPrivateAsset}
historyHandler={{
undo: this.undo,

View File

@@ -35,8 +35,8 @@ export type ShowConfirmDeleteDialogOptions = {|
confirmButtonLabel?: MessageDescriptor,
dismissButtonLabel?: MessageDescriptor,
message: MessageDescriptor,
fieldMessage?: MessageDescriptor,
confirmText?: string,
fieldMessage: MessageDescriptor,
confirmText: string,
|};
export type ShowConfirmDeleteDialogOptionsWithCallback = {|
...ShowConfirmDeleteDialogOptions,
@@ -44,31 +44,16 @@ export type ShowConfirmDeleteDialogOptionsWithCallback = {|
|};
export type ShowConfirmDeleteFunction = ShowConfirmDeleteDialogOptions => Promise<boolean>;
// Yes No Cancel
export type ShowYesNoCancelDialogOptions = {|
title: MessageDescriptor,
yesButtonLabel?: MessageDescriptor,
noButtonLabel?: MessageDescriptor,
cancelButtonLabel?: MessageDescriptor,
message: MessageDescriptor,
|};
export type ShowYesNoCancelDialogOptionsWithCallback = {|
...ShowYesNoCancelDialogOptions,
callback: Function,
|};
export type ConfirmState = {|
showAlertDialog: ShowAlertDialogOptionsWithCallback => void,
showConfirmDialog: ShowConfirmDialogOptionsWithCallback => void,
showConfirmDeleteDialog: ShowConfirmDeleteDialogOptionsWithCallback => void,
showYesNoCancelDialog: ShowYesNoCancelDialogOptionsWithCallback => void,
|};
const initialConfirmState = {
showAlertDialog: ShowAlertDialogOptionsWithCallback => {},
showConfirmDialog: ShowConfirmDialogOptionsWithCallback => {},
showConfirmDeleteDialog: ShowConfirmDeleteDialogOptionsWithCallback => {},
showYesNoCancelDialog: ShowYesNoCancelDialogOptionsWithCallback => {},
};
const AlertContext = React.createContext<ConfirmState>(initialConfirmState);

View File

@@ -8,9 +8,7 @@ import {
type ShowAlertDialogOptionsWithCallback,
type ShowConfirmDeleteDialogOptionsWithCallback,
type ShowConfirmDialogOptionsWithCallback,
type ShowYesNoCancelDialogOptionsWithCallback,
} from './AlertContext';
import YesNoCancelDialog from './YesNoCancelDialog';
type Props = {| children: React.Node |};
@@ -62,30 +60,12 @@ function ConfirmProvider({ children }: Props) {
[]
);
// Confirm
const [
yesNoCancelDialogOpen,
setYesNoCancelDialogOpen,
] = React.useState<boolean>(false);
const [
yesNoCancelDialogConfig,
setYesNoCancelDialogConfig,
] = React.useState<?ShowYesNoCancelDialogOptionsWithCallback>(null);
const openYesNoCancelDialog = React.useCallback(
(options: ShowYesNoCancelDialogOptionsWithCallback) => {
setYesNoCancelDialogOpen(true);
setYesNoCancelDialogConfig(options);
},
[]
);
return (
<AlertContext.Provider
value={{
showAlertDialog: openAlertDialog,
showConfirmDialog: openConfirmDialog,
showConfirmDeleteDialog: openConfirmDeleteDialog,
showYesNoCancelDialog: openYesNoCancelDialog,
}}
>
{children}
@@ -141,28 +121,6 @@ function ConfirmProvider({ children }: Props) {
confirmText={confirmDeleteDialogConfig.confirmText}
/>
)}
{yesNoCancelDialogConfig && (
<YesNoCancelDialog
open={yesNoCancelDialogOpen}
onClickYes={() => {
setYesNoCancelDialogOpen(false);
yesNoCancelDialogConfig.callback(0);
}}
yesButtonLabel={yesNoCancelDialogConfig.yesButtonLabel}
onClickNo={() => {
setYesNoCancelDialogOpen(false);
yesNoCancelDialogConfig.callback(1);
}}
noButtonLabel={yesNoCancelDialogConfig.noButtonLabel}
onClickCancel={() => {
setYesNoCancelDialogOpen(false);
yesNoCancelDialogConfig.callback(2);
}}
cancelButtonLabel={yesNoCancelDialogConfig.cancelButtonLabel}
title={yesNoCancelDialogConfig.title}
message={yesNoCancelDialogConfig.message}
/>
)}
</AlertContext.Provider>
);
}

View File

@@ -14,8 +14,8 @@ type Props = {|
open: boolean,
title: MessageDescriptor,
message: MessageDescriptor,
fieldMessage?: MessageDescriptor,
confirmText?: string,
fieldMessage: MessageDescriptor,
confirmText: string,
onConfirm: () => void,
onDismiss: () => void,
confirmButtonLabel?: MessageDescriptor,
@@ -24,7 +24,7 @@ type Props = {|
function ConfirmDeleteDialog(props: Props) {
const [textInput, setTextInput] = React.useState<string>('');
const canConfirm = props.confirmText ? textInput === props.confirmText : true;
const canConfirm = textInput === props.confirmText;
const onConfirm = () => {
if (!canConfirm) return;
@@ -75,19 +75,15 @@ function ConfirmDeleteDialog(props: Props) {
<Text size="body" style={{ userSelect: 'text' }}>
{i18n._(props.message)}
</Text>
{props.confirmText && props.fieldMessage && (
<>
<LargeSpacer />
<TextField
autoFocus="desktop"
floatingLabelFixed
floatingLabelText={i18n._(props.fieldMessage)}
value={textInput}
onChange={(e, text) => setTextInput(text)}
hintText={props.confirmText}
/>
</>
)}
<LargeSpacer />
<TextField
autoFocus="desktop"
floatingLabelFixed
floatingLabelText={i18n._(props.fieldMessage)}
value={textInput}
onChange={(e, text) => setTextInput(text)}
hintText={props.confirmText}
/>
</Dialog>
)}
</I18n>

View File

@@ -1,80 +0,0 @@
// @flow
import * as React from 'react';
import { I18n } from '@lingui/react';
import { Trans } from '@lingui/macro';
import { type MessageDescriptor } from '../../Utils/i18n/MessageDescriptor.flow';
import Dialog, { DialogPrimaryButton } from '../Dialog';
import FlatButton from '../FlatButton';
import { MarkdownText } from '../MarkdownText';
type Props = {|
open: boolean,
title: MessageDescriptor,
message: MessageDescriptor,
onClickYes: () => void,
onClickNo: () => void,
onClickCancel: () => void,
yesButtonLabel?: MessageDescriptor,
noButtonLabel?: MessageDescriptor,
cancelButtonLabel?: MessageDescriptor,
|};
function YesNoCancelDialog(props: Props) {
return (
<I18n>
{({ i18n }) => (
<Dialog
title={i18n._(props.title)}
open={props.open}
actions={[
<FlatButton
key="no"
keyboardFocused
label={
props.noButtonLabel ? (
i18n._(props.noButtonLabel)
) : (
<Trans>No</Trans>
)
}
onClick={props.onClickNo}
/>,
<DialogPrimaryButton
key="yes"
label={
props.yesButtonLabel ? (
i18n._(props.yesButtonLabel)
) : (
<Trans>Yes</Trans>
)
}
onClick={props.onClickYes}
primary
/>,
]}
secondaryActions={[
<FlatButton
key="cancel"
keyboardFocused
label={
props.cancelButtonLabel ? (
i18n._(props.cancelButtonLabel)
) : (
<Trans>Cancel</Trans>
)
}
onClick={props.onClickCancel}
/>,
]}
maxWidth="xs"
noMobileFullScreen
>
<MarkdownText translatableSource={props.message} isStandaloneText />
</Dialog>
)}
</I18n>
);
}
export default YesNoCancelDialog;

View File

@@ -5,7 +5,6 @@ import {
type ShowAlertDialogOptions,
type ShowConfirmDeleteDialogOptions,
type ShowConfirmDialogOptions,
type ShowYesNoCancelDialogOptions,
} from './AlertContext';
const useAlertDialog = () => {
@@ -13,7 +12,6 @@ const useAlertDialog = () => {
showAlertDialog,
showConfirmDialog,
showConfirmDeleteDialog,
showYesNoCancelDialog,
} = React.useContext(AlertContext);
const showAlert = React.useCallback(
@@ -40,26 +38,10 @@ const useAlertDialog = () => {
[showConfirmDeleteDialog]
);
/**
* Displays a 3-choice alert dialog (Defaults to Yes No Cancel).
* Callback will be called with:
* - 0 for yes (primary button)
* - 1 for no (flat button next to primary button)
* - 2 for cancel (secondary action)
*/
const showYesNoCancel = React.useCallback(
(options: ShowYesNoCancelDialogOptions): Promise<boolean> =>
new Promise(resolve => {
showYesNoCancelDialog({ callback: resolve, ...options });
}),
[showYesNoCancelDialog]
);
return {
showAlert,
showConfirmation,
showDeleteConfirmation,
showYesNoCancel,
};
};

View File

@@ -15,7 +15,6 @@ import AlertMessage from './AlertMessage';
import ArrowLeft from './CustomSvgIcons/ArrowLeft';
import ArrowRight from './CustomSvgIcons/ArrowRight';
import { Trans } from '@lingui/macro';
import { Typography } from '@material-ui/core';
import { CorsAwareImage } from './CorsAwareImage';
import { useIsMounted } from '../Utils/UseIsMounted';
import useForceUpdate from '../Utils/UseForceUpdate';
@@ -30,7 +29,7 @@ export type CarouselThumbnail = {
id: string,
title: string,
thumbnailUrl: string,
overlayText?: React.Node,
overlayText?: string,
overlayTextPosition?: OverlayTextPosition,
+link?: string,
+onClick?: () => void,
@@ -108,14 +107,10 @@ const styles = {
},
overlay: {
position: 'absolute',
background: 'rgba(0, 0, 0, 0.7)',
borderRadius: 4,
padding: '2px 6px',
},
overlayText: {
padding: '2px 4px',
backdropFilter: 'brightness(40%)',
color: 'white', // Same color for all themes.
marginTop: 0,
marginBottom: 0,
},
};
@@ -156,7 +151,7 @@ const ImageOverlay = ({
content,
position,
}: {|
content: React.Node,
content: string,
position: OverlayTextPosition,
|}) => {
const positionStyles = {
@@ -174,9 +169,9 @@ const ImageOverlay = ({
...positionStyles,
}}
>
<Typography variant="body1" style={styles.overlayText}>
<Text noMargin color="inherit" size="sub-title">
{content}
</Typography>
</Text>
</div>
);
};

View File

@@ -1,31 +0,0 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="24" height="24" viewBox="0 0 24 24" fill="none">
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 7.75C4 7.33579 4.33579 7 4.75 7H17.25C18.7688 7 20 8.23122 20 9.75V12.25C20 12.6642 19.6642 13 19.25 13C18.8358 13 18.5 12.6642 18.5 12.25V9.75C18.5 9.05964 17.9404 8.5 17.25 8.5H5.5V17.25C5.5 17.9404 6.05964 18.5 6.75 18.5H12.25C12.6642 18.5 13 18.8358 13 19.25C13 19.6642 12.6642 20 12.25 20H6.75C5.23122 20 4 18.7688 4 17.25V7.75Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M4 6.75C4 5.23122 5.23122 4 6.75 4H10.8127C11.819 4 12.7451 4.54965 13.227 5.43322L14.1584 7.14085C14.3568 7.50449 14.2228 7.96007 13.8591 8.15842C13.4955 8.35677 13.0399 8.22278 12.8416 7.85915L11.9101 6.15145C11.6911 5.74995 11.2702 5.5 10.8127 5.5H6.75C6.05964 5.5 5.5 6.05964 5.5 6.75V11C5.5 11.4142 5.16421 11.75 4.75 11.75C4.33579 11.75 4 11.4142 4 11V6.75Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M17 14C17.4142 14 17.75 14.3358 17.75 14.75V19.25C17.75 19.6642 17.4142 20 17 20C16.5858 20 16.25 19.6642 16.25 19.25V14.75C16.25 14.3358 16.5858 14 17 14Z"
fill="currentColor"
/>
<path
fillRule="evenodd"
clipRule="evenodd"
d="M14 17C14 16.5858 14.3358 16.25 14.75 16.25H19.25C19.6642 16.25 20 16.5858 20 17C20 17.4142 19.6642 17.75 19.25 17.75H14.75C14.3358 17.75 14 17.4142 14 17Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -1,11 +0,0 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="16" height="16" viewBox="0 0 16 16" fill="none">
<path
d="M5.39101 6.6665C5.12374 6.6665 4.98989 6.98965 5.17888 7.17864L7.78795 9.78771C7.90511 9.90486 8.09506 9.90486 8.21221 9.7877L10.8213 7.17864C11.0103 6.98965 10.8764 6.6665 10.6091 6.6665H5.39101Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -1,11 +0,0 @@
import React from 'react';
import SvgIcon from '@material-ui/core/SvgIcon';
export default React.memo(props => (
<SvgIcon {...props} width="17" height="16" viewBox="0 0 17 16" fill="none">
<path
d="M6.66699 10.6089C6.66699 10.8761 6.99013 11.01 7.17912 10.821L9.78819 8.21193C9.90535 8.09477 9.90535 7.90482 9.78819 7.78767L7.17912 5.1786C6.99013 4.98961 6.66699 5.12346 6.66699 5.39073L6.66699 10.6089Z"
fill="currentColor"
/>
</SvgIcon>
));

View File

@@ -1,24 +1,8 @@
import { Component } from 'react';
import HTML5Backend from 'react-dnd-html5-backend';
import TouchBackend from 'react-dnd-touch-backend';
import MultiBackend, { TouchTransition } from 'react-dnd-multi-backend';
import MultiBackend from 'react-dnd-multi-backend';
import HTML5toTouch from 'react-dnd-multi-backend/lib/HTML5toTouch';
import { DragDropContext } from 'react-dnd';
// react-dnd-multi-backend/lib/HTML5toTouch is not used directly in order to
// be able to specify the delayTouchStart parameter of the TouchBackend.
const HTML5toTouch = {
backends: [
{
backend: HTML5Backend,
},
{
backend: TouchBackend({ delayTouchStart: 100 }),
preview: true,
transition: TouchTransition,
},
],
};
class DragAndDropContextProvider extends Component {
render() {
return this.props.children;

View File

@@ -11,7 +11,6 @@ import {
type ConnectDropTarget,
type ConnectDragPreview,
} from 'react-dnd';
import { hapticFeedback } from '../../Utils/Haptic';
type Props<DraggedItemType> = {|
children: ({|
@@ -27,7 +26,6 @@ type Props<DraggedItemType> = {|
canDrop: (item: DraggedItemType) => boolean,
drop: () => void,
endDrag?: () => void,
hover?: (monitor: DropTargetMonitor) => void,
|};
type DragSourceProps = {|
@@ -49,11 +47,8 @@ type InnerDragSourceAndDropTargetProps<DraggedItemType> = {|
...DropTargetProps,
|};
type Options = {| vibrate?: number |};
export const makeDragSourceAndDropTarget = <DraggedItemType>(
reactDndType: string,
options: ?Options
reactDndType: string
): ((Props<DraggedItemType>) => React.Node) => {
const sourceSpec = {
canDrag(props: Props<DraggedItemType>, monitor: DragSourceMonitor) {
@@ -63,9 +58,6 @@ export const makeDragSourceAndDropTarget = <DraggedItemType>(
return true;
},
beginDrag(props: InnerDragSourceAndDropTargetProps<DraggedItemType>) {
if (hapticFeedback && options && options.vibrate) {
hapticFeedback({ durationInMs: options.vibrate });
}
return props.beginDrag();
},
endDrag(props: Props<DraggedItemType>, monitor: DragSourceMonitor) {
@@ -95,9 +87,6 @@ export const makeDragSourceAndDropTarget = <DraggedItemType>(
}
props.drop();
},
hover(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
if (props.hover) props.hover(monitor);
},
};
function targetCollect(

View File

@@ -0,0 +1,106 @@
// @flow
import * as React from 'react';
import { t } from '@lingui/macro';
import Dialog, { DialogPrimaryButton } from './Dialog';
import TextField, { type TextFieldInterface } from './TextField';
import FlatButton from './FlatButton';
import { Trans } from '@lingui/macro';
import { type Tags, getTagsFromString } from '../Utils/TagsHelper';
import { shouldValidate } from './KeyboardShortcuts/InteractionKeys';
type Props = {|
tagsString: string,
onCancel: () => void,
onEdit: (tags: Tags) => void,
|};
type State = {|
tagsString: string,
|};
/**
* Dialog to edit tags, with keyboard support (auto focus of tags field,
* enter to validate, esc to dismiss dialog).
*/
export default class EditTagsDialog extends React.Component<Props, State> {
state = {
tagsString: this.props.tagsString,
};
_tagsField = React.createRef<TextFieldInterface>();
componentDidMount() {
setTimeout(() => {
if (this._tagsField && this._tagsField.current) {
this._tagsField.current.focus();
}
}, 10);
}
_canEdit = () => {
const { tagsString } = this.state;
const tags = getTagsFromString(tagsString);
return !!this.props.tagsString || !!tags.length;
};
_onEdit = (tags: Tags) => {
if (!this._canEdit()) return;
this.props.onEdit(tags);
};
render() {
const { onCancel, onEdit } = this.props;
const { tagsString } = this.state;
const tags = getTagsFromString(tagsString);
return (
<Dialog
title={<Trans>Edit object tags</Trans>}
actions={[
<FlatButton
key="close"
label={<Trans>Cancel</Trans>}
primary={false}
onClick={onCancel}
/>,
<DialogPrimaryButton
key="add"
label={
this.props.tagsString && !tags.length ? (
<Trans>Remove all tags</Trans>
) : (
<Trans>Add/update {tags.length} tag(s)</Trans>
)
}
primary
onClick={() => this._onEdit(tags)}
disabled={!this._canEdit()}
/>,
]}
onRequestClose={onCancel}
onApply={() => this._onEdit(tags)}
open
>
<TextField
fullWidth
value={tagsString}
onChange={(e, tagsString) =>
this.setState({
tagsString,
})
}
floatingLabelText="Tag(s) (comma-separated)"
translatableHintText={t`For example: player, spaceship, inventory...`}
onKeyPress={event => {
if (shouldValidate(event)) {
onEdit(tags);
}
}}
ref={this._tagsField}
/>
</Dialog>
);
}
}

View File

@@ -0,0 +1,48 @@
// @flow
import * as React from 'react';
import { type I18n as I18nType } from '@lingui/core';
import IconButton from '../IconButton';
import ElementWithMenu from '../Menu/ElementWithMenu';
import { type MenuItemTemplate } from '../Menu/Menu.flow';
import Filter from '../CustomSvgIcons/Filter';
const styles = {
mediumContainer: {
padding: 0,
width: 32,
height: 32,
},
smallContainer: {
padding: 0,
width: 16,
height: 16,
},
icon: {
width: 16,
height: 16,
},
};
type Props = {|
buildMenuTemplate: (i18n: I18nType) => Array<MenuItemTemplate>,
size?: 'small',
|};
export default function TagsButton(props: Props) {
return (
<ElementWithMenu
element={
<IconButton
style={
props.size === 'small'
? styles.smallContainer
: styles.mediumContainer
}
>
<Filter htmlColor="inherit" style={styles.icon} />
</IconButton>
}
buildMenuTemplate={props.buildMenuTemplate}
/>
);
}

View File

@@ -32,22 +32,22 @@ const ESC_KEY = 27;
const MID_MOUSE_BUTTON = 1;
type ShortcutCallbacks = {|
onDelete?: () => void | Promise<void>,
onMove?: (number, number) => void | Promise<void>,
onCopy?: () => void | Promise<void>,
onCut?: () => void | Promise<void>,
onPaste?: () => void | Promise<void>,
onDuplicate?: () => void | Promise<void>,
onUndo?: () => void | Promise<void>,
onRedo?: () => void | Promise<void>,
onSearch?: () => void | Promise<void>,
onZoomOut?: KeyboardEvent => void | Promise<void>,
onZoomIn?: KeyboardEvent => void | Promise<void>,
onEscape?: () => void | Promise<void>,
onShift1?: () => void | Promise<void>,
onShift2?: () => void | Promise<void>,
onShift3?: () => void | Promise<void>,
onToggleGrabbingTool?: (isEnabled: boolean) => void | Promise<void>,
onDelete?: () => void,
onMove?: (number, number) => void,
onCopy?: () => void,
onCut?: () => void,
onPaste?: () => void,
onDuplicate?: () => void,
onUndo?: () => void,
onRedo?: () => void,
onSearch?: () => void,
onZoomOut?: KeyboardEvent => void,
onZoomIn?: KeyboardEvent => void,
onEscape?: () => void,
onShift1?: () => void,
onShift2?: () => void,
onShift3?: () => void,
onToggleGrabbingTool?: (isEnabled: boolean) => void,
|};
type ConstructorArgs = {|
@@ -81,13 +81,6 @@ export default class KeyboardShortcuts {
this._isActive = isActive;
}
setShortcutCallback(
key: $Keys<ShortcutCallbacks>,
callback: () => void | Promise<void>
) {
this._shortcutCallbacks[key] = callback;
}
shouldCloneInstances() {
return this._isControlOrCmdPressed();
}

View File

@@ -104,7 +104,6 @@ type ListItemProps = {|
backgroundColor?: string,
borderBottom?: string,
opacity?: number,
paddingLeft?: number,
|},
leftIcon?: React.Node,
@@ -116,7 +115,7 @@ type ListItemProps = {|
data?: HTMLDataset,
|};
export type ListItemRefType = any; // Should be a material-ui ListItem
export type ListItemRefType = any; // Should be a material-ui ListIten
/**
* A ListItem to be used in a List.

View File

@@ -46,10 +46,10 @@ export const ListSearchResults = <SearchItem>({
const cachedHeights = React.useRef({});
const onItemHeightComputed = React.useCallback(
(searchItem, height) => {
if (cachedHeights.current[getSearchItemUniqueId(searchItem)] === height)
return false;
const uniqueId = getSearchItemUniqueId(searchItem);
if (cachedHeights.current[uniqueId] === height) return false;
cachedHeights.current[getSearchItemUniqueId(searchItem)] = height;
cachedHeights.current[uniqueId] = height;
return true;
},
[getSearchItemUniqueId]

View File

@@ -14,7 +14,6 @@ import TagChips from './TagChips';
import { I18n } from '@lingui/react';
import { useDebounce } from '../Utils/UseDebounce';
import SearchBarContainer from './SearchBarContainer';
import { useResponsiveWindowWidth } from './Reponsive/ResponsiveWindowMeasurer';
type TagsHandler = {|
remove: string => void,
@@ -92,8 +91,6 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
textField.current.blur();
}
};
const windowWidth = useResponsiveWindowWidth();
const isMobile = windowWidth === 'small';
const [isInputFocused, setIsInputFocused] = React.useState(false);
@@ -178,7 +175,7 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
const handleCancel = () => {
changeValueImmediately('');
if (!isMobile) focus();
focus();
};
const handleKeyPressed = (event: SyntheticKeyboardEvent<>) => {

View File

@@ -103,6 +103,7 @@ export const SimpleTextField = React.memo<
}`}
>
<input
id={props.id}
disabled={props.disabled}
ref={inputRef}
type={props.type}

View File

@@ -1,8 +1,7 @@
// @flow
import React from 'react';
import Chip from '../UI/Chip';
type Tags = Array<string>;
import { type Tags, removeTag } from '../Utils/TagsHelper';
const styles = {
chipContainer: {
@@ -20,10 +19,11 @@ const styles = {
type Props = {|
tags: Tags,
onRemove: string => void,
onChange?: Tags => void,
onRemove?: string => void,
|};
const TagChips = ({ tags, onRemove }: Props) => {
const TagChips = ({ tags, onChange, onRemove }: Props) => {
const [focusedTag, setFocusedTag] = React.useState<?string>(null);
const tagsRefs = React.useRef([]);
@@ -51,7 +51,8 @@ const TagChips = ({ tags, onRemove }: Props) => {
newTagToFocus.current.focus();
}
}
onRemove(tag);
if (onChange) onChange(removeTag(tags, tag));
else if (onRemove) onRemove(tag);
};
if (!tags.length) return null;
@@ -68,7 +69,7 @@ const TagChips = ({ tags, onRemove }: Props) => {
style={getChipStyle(tag)}
onBlur={() => setFocusedTag(null)}
onFocus={() => setFocusedTag(tag)}
onDelete={handleDeleteTag(tag)}
onDelete={onChange || onRemove ? handleDeleteTag(tag) : null}
label={tag}
ref={newRef}
/>

View File

@@ -18,6 +18,48 @@
"value": "#FFFFFF"
}
},
"success": {
"dark": {
"value": "#16CF89",
"comment": "Palette/Green/50"
},
"color": {
"value": "#45D9A1",
"comment": "Palette/Green/40"
},
"light": {
"value": "#8BE7C4",
"comment": "Palette/Green/30"
}
},
"warning": {
"dark": {
"value": "#FFA929",
"comment": "Palette/Yellow/50"
},
"color": {
"value": "#FFBC57",
"comment": "Palette/Yellow/40"
},
"light": {
"value": "#FFD28F",
"comment": "Palette/Yellow/30"
}
},
"error": {
"dark": {
"value": "#FF5E3B",
"comment": "Palette/Red/50"
},
"color": {
"value": "#FF8569",
"comment": "Palette/Red/40"
},
"light": {
"value": "#FFA38F",
"comment": "Palette/Red/30"
}
},
"surface": {
"titlebar": {
"background-color": {
@@ -619,4 +661,4 @@
}
}
}
}
}

View File

@@ -11,7 +11,6 @@ export function getRootClassNames(theme: string) {
return {
mosaicRootClassName: theme,
eventsSheetRootClassName: theme,
treeViewRootClassName: theme,
tableRootClassName: theme,
markdownRootClassName: theme,
uiRootClassName: theme,
@@ -301,6 +300,10 @@ export function getMuiOverrides({
paper: {
backgroundColor: alternateCanvasBackgroundColor,
},
option: {
// Avoid the default min-height of 48px, which is too big to display options.
minHeight: 35,
},
},
};
}

View File

@@ -16,6 +16,48 @@
"value": "#2E3440"
}
},
"success": {
"dark": {
"value": "#16CF89",
"comment": "Palette/Green/50"
},
"color": {
"value": "#45D9A1",
"comment": "Palette/Green/40"
},
"light": {
"value": "#8BE7C4",
"comment": "Palette/Green/30"
}
},
"warning": {
"dark": {
"value": "#FFA929",
"comment": "Palette/Yellow/50"
},
"color": {
"value": "#FFBC57",
"comment": "Palette/Yellow/40"
},
"light": {
"value": "#FFD28F",
"comment": "Palette/Yellow/30"
}
},
"error": {
"dark": {
"value": "#FF5E3B",
"comment": "Palette/Red/50"
},
"color": {
"value": "#FF8569",
"comment": "Palette/Red/40"
},
"light": {
"value": "#FFA38F",
"comment": "Palette/Red/30"
}
},
"surface": {
"titlebar": {
"background-color": {
@@ -609,4 +651,4 @@
}
}
}
}
}

View File

@@ -16,6 +16,48 @@
"value": "#282C34"
}
},
"success": {
"dark": {
"value": "#16CF89",
"comment": "Palette/Green/50"
},
"color": {
"value": "#45D9A1",
"comment": "Palette/Green/40"
},
"light": {
"value": "#8BE7C4",
"comment": "Palette/Green/30"
}
},
"warning": {
"dark": {
"value": "#FFA929",
"comment": "Palette/Yellow/50"
},
"color": {
"value": "#FFBC57",
"comment": "Palette/Yellow/40"
},
"light": {
"value": "#FFD28F",
"comment": "Palette/Yellow/30"
}
},
"error": {
"dark": {
"value": "#FF5E3B",
"comment": "Palette/Red/50"
},
"color": {
"value": "#FF8569",
"comment": "Palette/Red/40"
},
"light": {
"value": "#FFA38F",
"comment": "Palette/Red/30"
}
},
"surface": {
"titlebar": {
"background-color": {

View File

@@ -16,6 +16,48 @@
"value": "#191724"
}
},
"success": {
"dark": {
"value": "#16CF89",
"comment": "Palette/Green/50"
},
"color": {
"value": "#45D9A1",
"comment": "Palette/Green/40"
},
"light": {
"value": "#8BE7C4",
"comment": "Palette/Green/30"
}
},
"warning": {
"dark": {
"value": "#FFA929",
"comment": "Palette/Yellow/50"
},
"color": {
"value": "#FFBC57",
"comment": "Palette/Yellow/40"
},
"light": {
"value": "#FFD28F",
"comment": "Palette/Yellow/30"
}
},
"error": {
"dark": {
"value": "#FF5E3B",
"comment": "Palette/Red/50"
},
"color": {
"value": "#FF8569",
"comment": "Palette/Red/40"
},
"light": {
"value": "#FFA38F",
"comment": "Palette/Red/30"
}
},
"surface": {
"titlebar": {
"background-color": {

View File

@@ -16,6 +16,48 @@
"value": "#fff"
}
},
"success": {
"dark": {
"value": "#16CF89",
"comment": "Palette/Green/50"
},
"color": {
"value": "#45D9A1",
"comment": "Palette/Green/40"
},
"light": {
"value": "#8BE7C4",
"comment": "Palette/Green/30"
}
},
"warning": {
"dark": {
"value": "#FFA929",
"comment": "Palette/Yellow/50"
},
"color": {
"value": "#FFBC57",
"comment": "Palette/Yellow/40"
},
"light": {
"value": "#FFD28F",
"comment": "Palette/Yellow/30"
}
},
"error": {
"dark": {
"value": "#FF5E3B",
"comment": "Palette/Red/50"
},
"color": {
"value": "#FF8569",
"comment": "Palette/Red/40"
},
"light": {
"value": "#FFA38F",
"comment": "Palette/Red/30"
}
},
"surface": {
"titlebar": {
"background-color": {
@@ -611,4 +653,4 @@
}
}
}
}
}

View File

@@ -1,126 +0,0 @@
.tree-view .full-height-flex-container {
display: flex;
height: 100%;
}
.tree-view .full-height-flex-container.with-divider {
border-top: 1px solid var(--theme-list-item-separator-color);
}
.tree-view .full-space-container {
flex: 1;
height: 100%;
width: 100%;
}
.tree-view .row-content {
display: flex;
justify-content: space-between;
align-items: center;
height: 100%;
width: 100%;
}
.tree-view .row-content-side {
display: flex;
align-items: center;
height: 100%;
position: relative;
}
.tree-view .row-content-side.row-content-extra-padding {
/* Used for a better dragging preview. */
padding: 0 6px;
}
.tree-view .row-content-side.row-content-side-left {
/* Necessary for item-name to be overflown. */
width: 80%;
}
.tree-view .row-content-side.row-content-side-right {
/* Used for a better dragging preview. */
position: absolute;
right: 0;
z-index: 1;
}
.tree-view .row-container {
display: flex;
flex: 1;
border-radius: 6px;
width: 100%;
}
.tree-view:focus-visible {
/**
* Remove browser-specific style around focused element when hitting an arrow key.
*/
outline: none;
}
.tree-view .row-container.with-can-drop-inside-indicator {
outline: 1px solid var(--theme-drop-indicator-can-drop-color);
}
.tree-view .row-container.with-cannot-drop-inside-indicator {
outline: 1px solid var(--theme-drop-indicator-cannot-drop-color);
}
.tree-view .row-container.selected {
background-color: var(--table-row-selected-background-color);
}
.tree-view .item-name,
.tree-view .item-name-input {
font-family: var(--gdevelop-modern-font-family) !important;
font-size: 14px;
}
.tree-view .item-name {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
user-select: none;
}
.tree-view .item-name.editable {
cursor: text;
}
.tree-view .item-name.placeholder {
color: var(--theme-text-secondary-color);
font-size: 14px;
}
.tree-view .item-name.root-folder {
font-weight: bold;
font-size: 16px;
}
.tree-view .folder-icon {
margin-right: 4px;
width: 20px;
}
.tree-view .thumbnail {
margin-right: 6px;
}
.tree-view .item-name-input {
outline: none;
border: none;
padding: 0;
background-image: none;
background-color: transparent;
-webkit-box-shadow: none;
-moz-box-shadow: none;
box-shadow: none;
color: inherit;
width: 100%;
}
.tree-view .item-name-input-container:before {
bottom: -1px;
left: 0;
content: '\00a0';
width: 100%;
border-bottom: 1px solid var(--theme-text-default-color);
display: block;
position: absolute;
height: 1px;
}
.tree-view .item-name-input-container {
position: relative;
flex: 1;
}

View File

@@ -1,461 +0,0 @@
// @flow
import * as React from 'react';
import DropIndicator from '../SortableVirtualizedItemList/DropIndicator';
import memoizeOne from 'memoize-one';
import { areEqual } from 'react-window';
import IconButton from '../IconButton';
import ArrowHeadBottom from '../CustomSvgIcons/ArrowHeadBottom';
import ArrowHeadRight from '../CustomSvgIcons/ArrowHeadRight';
import Folder from '../CustomSvgIcons/Folder';
import ListIcon from '../ListIcon';
import './TreeView.css';
import {
shouldCloseOrCancel,
shouldValidate,
} from '../KeyboardShortcuts/InteractionKeys';
import ThreeDotsMenu from '../CustomSvgIcons/ThreeDotsMenu';
import { type ItemData, type ItemBaseAttributes, navigationKeys } from '.';
import { useLongTouch } from '../../Utils/UseLongTouch';
import { dataObjectToProps } from '../../Utils/HTMLDataset';
const stopPropagation = e => e.stopPropagation();
const DELAY_BEFORE_OPENING_FOLDER_ON_DRAG_HOVER = 800;
const DELAY_BEFORE_ENABLING_NAME_EDITION_AFTER_SELECTION = 1000;
const onInputKeyDown = (event: KeyboardEvent) => {
if (navigationKeys.includes(event.key)) {
// Prevent navigating in the tree view when renaming an item.
event.stopPropagation();
} else if (shouldCloseOrCancel(event)) {
// Prevent closing dialog if TreeView is displayed in dialog.
event.stopPropagation();
}
};
const SemiControlledRowInput = ({
initialValue,
onEndRenaming,
onBlur,
}: {
initialValue: string,
onEndRenaming: (newName: string) => void,
onBlur: () => void,
}) => {
const [value, setValue] = React.useState<string>(initialValue);
const inputRef = React.useRef<?HTMLInputElement>(null);
/**
* When mounting the component, select content.
*/
React.useEffect(() => {
if (inputRef.current) {
inputRef.current.select();
}
}, []);
/**
* When unmounting the component, call onBlur. If props.onBlur is called
* at the end of onKeyUp, focus might before the component is mounted.
* This would trigger the blur callback on the input, calling onEndRenaming
* with the current value, even if the user hit Escape key and expected the
* initialValue to be set.
*/
React.useEffect(
() => {
return onBlur;
},
[onBlur]
);
return (
<div className="item-name-input-container">
<input
autoFocus
ref={inputRef}
type="text"
className="item-name-input"
value={value}
onChange={e => {
setValue(e.currentTarget.value);
}}
onClick={stopPropagation}
onDoubleClick={stopPropagation}
onBlur={() => {
onEndRenaming(value);
}}
onKeyDown={onInputKeyDown}
onKeyUp={e => {
if (shouldCloseOrCancel(e)) {
// Prevent closing dialog if TreeView is displayed in dialog.
e.preventDefault();
onEndRenaming(initialValue);
} else if (shouldValidate(e)) {
onEndRenaming(value);
}
}}
/>
</div>
);
};
const memoized = memoizeOne((initialValue, getContainerYPosition) =>
getContainerYPosition()
);
type Props<Item> = {|
index: number,
style: any,
data: ItemData<Item>,
/** Used by react-window. */
isScrolling?: boolean,
|};
const TreeViewRow = <Item: ItemBaseAttributes>(props: Props<Item>) => {
const { data, index, style } = props;
const {
flattenedData,
onOpen,
onSelect,
onBlurField,
onStartRenaming,
onEndRenaming,
renamedItemId,
onContextMenu,
canDrop,
onDrop,
onEditItem,
isMobileScreen,
DragSourceAndDropTarget,
getItemHtmlId,
} = data;
const node = flattenedData[index];
const left = node.depth * 15;
const [isStayingOver, setIsStayingOver] = React.useState<boolean>(false);
const [
hasDelayPassedBeforeEditingName,
setHasDelayPassedBeforeEditingName,
] = React.useState<boolean>(false);
const openWhenOverTimeoutId = React.useRef<?TimeoutID>(null);
const [whereToDrop, setWhereToDrop] = React.useState<
'before' | 'after' | 'inside'
>('before');
const containerRef = React.useRef<?HTMLDivElement>(null);
const openContextMenu = React.useCallback(
({ clientX, clientY }) => {
onContextMenu({
index: index,
item: node.item,
x: clientX,
y: clientY,
});
},
[onContextMenu, index, node.item]
);
const longTouchForContextMenuProps = useLongTouch(openContextMenu, {
delay: 1000,
});
const onClick = React.useCallback(
event => {
if (!node || node.item.isPlaceholder) return;
if (node.item.isRoot) {
onOpen(node);
return;
}
onSelect({ node, exclusive: !(event.metaKey || event.ctrlKey) });
},
[onSelect, node, onOpen]
);
const selectAndOpenContextMenu = React.useCallback(
(event: MouseEvent) => {
onClick(event);
openContextMenu(event);
},
[onClick, openContextMenu]
);
/**
* Effect that opens the node if the user is dragging another node and stays
* over the node.
*/
React.useEffect(
() => {
if (
isStayingOver &&
!openWhenOverTimeoutId.current &&
node.canHaveChildren &&
node.collapsed
) {
openWhenOverTimeoutId.current = setTimeout(() => {
onOpen(node);
}, DELAY_BEFORE_OPENING_FOLDER_ON_DRAG_HOVER);
return () => {
clearTimeout(openWhenOverTimeoutId.current);
openWhenOverTimeoutId.current = null;
};
}
},
[isStayingOver, onOpen, node]
);
/**
* Effect allows editing the name of the node after a delay has passed
* after its selection by the user.
*/
React.useEffect(
() => {
if (isMobileScreen) return;
if (node.selected) {
if (!hasDelayPassedBeforeEditingName) {
const timeoutId = setTimeout(() => {
setHasDelayPassedBeforeEditingName(true);
}, DELAY_BEFORE_ENABLING_NAME_EDITION_AFTER_SELECTION);
return () => clearTimeout(timeoutId);
}
} else {
setHasDelayPassedBeforeEditingName(false);
}
},
[node.selected, isMobileScreen, hasDelayPassedBeforeEditingName]
);
const endRenaming = React.useCallback(
(newValue: string) => {
setHasDelayPassedBeforeEditingName(false);
onEndRenaming(node.item, newValue);
},
[onEndRenaming, node.item]
);
const getContainerYPosition = React.useCallback(() => {
if (containerRef.current) {
return containerRef.current.getBoundingClientRect().top;
}
}, []);
const displayAsFolder = node.canHaveChildren;
return (
<div style={style} ref={containerRef}>
<DragSourceAndDropTarget
beginDrag={() => {
if (!node.selected) onSelect({ node, exclusive: !node.selected });
return {};
}}
canDrag={() =>
// Prevent dragging of root folder.
!node.item.isRoot &&
// Prevent dragging of item whose name is edited, allowing to select text with click and drag on text.
renamedItemId !== node.id
}
canDrop={canDrop ? () => canDrop(node.item) : () => true}
drop={() => {
onDrop(node.item, whereToDrop);
}}
hover={monitor => {
if (node.item.isRoot) {
if (whereToDrop !== 'inside') setWhereToDrop('inside');
return;
}
const { y } = monitor.getClientOffset();
// Use a cached version of container position to avoid recomputing bounding rectangle.
// Doing this, the position is computed every second the user hovers the target.
const containerYPosition = memoized(
Math.floor(Date.now() / 1000),
getContainerYPosition
);
if (containerYPosition) {
if (displayAsFolder) {
if (node.collapsed) {
setWhereToDrop(
y - containerYPosition <= 6
? 'before'
: y - containerYPosition <= 26
? 'inside'
: 'after'
);
} else {
// If the folder is open, do not suggest to drop after as
// the drop indicator can be misleading (displayed under the row
// although dropping the element after would put it below the last
// displayed child of the folder).
setWhereToDrop(
y - containerYPosition <= 6 ? 'before' : 'inside'
);
}
} else {
setWhereToDrop(y - containerYPosition <= 16 ? 'before' : 'after');
}
}
}}
>
{({
connectDragSource,
connectDropTarget,
connectDragPreview,
isOver,
canDrop,
}) => {
setIsStayingOver(isOver);
return (
<div
style={{ paddingLeft: left }}
className={`full-height-flex-container${
node.item.isRoot && index > 0 ? ' with-divider' : ''
}`}
>
{connectDropTarget(
<div
id={
getItemHtmlId ? getItemHtmlId(node.item, index) : undefined
}
onClick={onClick}
className={
'row-container' +
(node.selected ? ' selected' : '') +
(isOver &&
whereToDrop === 'inside' &&
displayAsFolder &&
!node.item.isRoot
? canDrop
? ' with-can-drop-inside-indicator'
: ' with-cannot-drop-inside-indicator'
: '')
}
aria-selected={node.selected}
aria-expanded={displayAsFolder ? !node.collapsed : false}
{...dataObjectToProps(node.dataset)}
>
{connectDragSource(
<div className="full-space-container">
{isOver && whereToDrop === 'before' && (
<DropIndicator canDrop={canDrop} />
)}
<div
className="row-content"
onDoubleClick={
onEditItem ? () => onEditItem(node.item) : undefined
}
onContextMenu={selectAndOpenContextMenu}
{...longTouchForContextMenuProps}
>
{connectDragPreview(
<div
className={`row-content-side${
node.item.isRoot ? '' : ' row-content-side-left'
}${
displayAsFolder
? ''
: ' row-content-extra-padding'
}`}
>
{displayAsFolder ? (
<>
<IconButton
size="small"
onClick={e => {
e.stopPropagation();
onOpen(node);
}}
disabled={node.disableCollapse}
>
{node.collapsed ? (
<ArrowHeadRight fontSize="small" />
) : (
<ArrowHeadBottom fontSize="small" />
)}
</IconButton>
{!node.item.isRoot && (
<Folder className="folder-icon" />
)}
</>
) : node.thumbnailSrc ? (
<div className="thumbnail">
<ListIcon
iconSize={20}
src={node.thumbnailSrc}
/>
</div>
) : null}
{renamedItemId === node.id ? (
<SemiControlledRowInput
initialValue={node.name}
onEndRenaming={endRenaming}
onBlur={onBlurField}
/>
) : (
<span
className={`item-name${
node.item.isRoot
? ' root-folder'
: node.item.isPlaceholder
? ' placeholder'
: ''
}${
node.item.isRoot || node.item.isPlaceholder
? ''
: hasDelayPassedBeforeEditingName
? ' editable'
: ''
}`}
onClick={
node.item.isRoot ||
node.item.isPlaceholder ||
isMobileScreen ||
!hasDelayPassedBeforeEditingName
? null
: e => {
if (!e.metaKey && !e.shiftKey) {
e.stopPropagation();
onStartRenaming(node.id);
}
}
}
>
{node.name}
</span>
)}
</div>
)}
{!isMobileScreen &&
!node.item.isRoot &&
!node.item.isPlaceholder && (
<div className="row-content-side row-content-side-right">
<IconButton
size="small"
onClick={e => {
e.stopPropagation();
onContextMenu({
item: node.item,
index,
x: e.clientX,
y: e.clientY,
});
}}
>
<ThreeDotsMenu />
</IconButton>
</div>
)}
</div>
{isOver && whereToDrop === 'after' && (
<DropIndicator canDrop={canDrop} />
)}
</div>
)}
</div>
)}
</div>
);
}}
</DragSourceAndDropTarget>
</div>
);
};
// $FlowFixMe - memo does not support having a generic in the props.
export default React.memo<Props>(TreeViewRow, areEqual);

View File

@@ -1,613 +0,0 @@
// @flow
import * as React from 'react';
import { FixedSizeList } from 'react-window';
import memoizeOne from 'memoize-one';
import GDevelopThemeContext from '../Theme/GDevelopThemeContext';
import { treeView } from '../../EventsSheet/EventsTree/ClassNames';
import './TreeView.css';
import ContextMenu, { type ContextMenuInterface } from '../Menu/ContextMenu';
import { useResponsiveWindowWidth } from '../Reponsive/ResponsiveWindowMeasurer';
import TreeViewRow from './TreeViewRow';
import { makeDragSourceAndDropTarget } from '../DragAndDrop/DragSourceAndDropTarget';
import { type HTMLDataset } from '../../Utils/HTMLDataset';
import useForceUpdate from '../../Utils/UseForceUpdate';
export const navigationKeys = [
'ArrowDown',
'ArrowUp',
'ArrowRight',
'ArrowLeft',
];
export type ItemBaseAttributes = {
+isRoot?: boolean,
+isPlaceholder?: boolean,
};
type FlattenedNode<Item> = {|
id: string,
name: string,
hasChildren: boolean,
canHaveChildren: boolean,
depth: number,
dataset?: ?HTMLDataset,
collapsed: boolean,
selected: boolean,
disableCollapse: boolean,
thumbnailSrc?: ?string,
item: Item,
|};
export type ItemData<Item> = {|
onOpen: (FlattenedNode<Item>) => void,
onSelect: ({| node: FlattenedNode<Item>, exclusive?: boolean |}) => void,
onBlurField: () => void,
flattenedData: FlattenedNode<Item>[],
onStartRenaming: (nodeId: ?string) => void,
onEndRenaming: (item: Item, newName: string) => void,
onContextMenu: ({|
item: Item,
index: number,
x: number,
y: number,
|}) => void,
renamedItemId: ?string,
canDrop?: ?(Item) => boolean,
onDrop: (Item, where: 'before' | 'inside' | 'after') => void,
onEditItem?: Item => void,
isMobileScreen: boolean,
DragSourceAndDropTarget: any => React.Node,
getItemHtmlId?: (Item, index: number) => ?string,
|};
const getItemProps = memoizeOne(
<Item>(
flattenedData: FlattenedNode<Item>[],
onOpen: (FlattenedNode<Item>) => void,
onSelect: ({| node: FlattenedNode<Item>, exclusive?: boolean |}) => void,
onBlurField: () => void,
onStartRenaming: (nodeId: ?string) => void,
onEndRenaming: (item: Item, newName: string) => void,
renamedItemId: ?string,
onContextMenu: ({|
item: Item,
index: number,
x: number,
y: number,
|}) => void,
canDrop?: ?(Item) => boolean,
onDrop: (Item, where: 'before' | 'inside' | 'after') => void,
onEditItem?: Item => void,
isMobileScreen: boolean,
DragSourceAndDropTarget: any => React.Node,
getItemHtmlId?: (Item, index: number) => ?string
): ItemData<Item> => ({
onOpen,
onSelect,
onBlurField,
flattenedData,
onStartRenaming,
onEndRenaming,
renamedItemId,
onContextMenu,
canDrop,
onDrop,
onEditItem,
isMobileScreen,
DragSourceAndDropTarget,
getItemHtmlId,
})
);
export type TreeViewInterface<Item> = {|
forceUpdateList: () => void,
scrollToItem: Item => void,
renameItem: Item => void,
openItems: (string[]) => void,
closeItems: (string[]) => void,
|};
type Props<Item> = {|
height: number,
width?: number,
items: Item[],
getItemName: Item => string,
getItemId: Item => string,
getItemHtmlId?: (Item, index: number) => ?string,
getItemChildren: Item => ?(Item[]),
getItemThumbnail?: Item => ?string,
getItemDataset?: Item => ?HTMLDataset,
onEditItem?: Item => void,
buildMenuTemplate: (Item, index: number) => any,
searchText?: string,
selectedItems: $ReadOnlyArray<Item>,
onSelectItems: (Item[]) => void,
multiSelect: boolean,
onRenameItem: (Item, newName: string) => void,
onMoveSelectionToItem: (
destinationItem: Item,
where: 'before' | 'inside' | 'after'
) => void,
canMoveSelectionToItem?: ?(destinationItem: Item) => boolean,
reactDndType: string,
forceAllOpened?: boolean,
initiallyOpenedNodeIds?: string[],
renderHiddenElements?: boolean,
arrowKeyNavigationProps?: {|
onArrowRight: (item: Item) => ?Item,
onArrowLeft: (item: Item) => ?Item,
|},
|};
const TreeView = <Item: ItemBaseAttributes>(
{
height,
width,
items,
searchText,
getItemName,
getItemId,
getItemHtmlId,
getItemChildren,
getItemThumbnail,
getItemDataset,
onEditItem,
buildMenuTemplate,
selectedItems,
onSelectItems,
multiSelect,
onRenameItem,
onMoveSelectionToItem,
canMoveSelectionToItem,
reactDndType,
forceAllOpened,
initiallyOpenedNodeIds,
renderHiddenElements,
arrowKeyNavigationProps,
}: Props<Item>,
ref: TreeViewInterface<Item>
) => {
const selectedNodeIds = selectedItems.map(item => getItemId(item));
const [openedNodeIds, setOpenedNodeIds] = React.useState<string[]>(
initiallyOpenedNodeIds || []
);
const [renamedItemId, setRenamedItemId] = React.useState<?string>(null);
const contextMenuRef = React.useRef<?ContextMenuInterface>(null);
const containerRef = React.useRef<?HTMLDivElement>(null);
const listRef = React.useRef<?FixedSizeList>(null);
const [
openedDuringSearchNodeIds,
setOpenedDuringSearchNodeIds,
] = React.useState<string[]>([]);
const theme = React.useContext(GDevelopThemeContext);
const windowWidth = useResponsiveWindowWidth();
const forceUpdate = useForceUpdate();
const isMobileScreen = windowWidth === 'small';
const isSearching = !!searchText;
const flattenNode = React.useCallback(
(
item: Item,
depth: number,
searchText: ?string,
forceOpen: boolean
): FlattenedNode<Item>[] => {
const id = getItemId(item);
const children = getItemChildren(item);
const canHaveChildren = Array.isArray(children);
const collapsed = !forceAllOpened && !openedNodeIds.includes(id);
const openedDuringSearch = openedDuringSearchNodeIds.includes(id);
let flattenedChildren = [];
/*
* Compute children nodes flattening if:
* - node has children;
* and if either one of these conditions are true:
* - the nodes are force-opened (props)
* - the node is opened (not collapsed)
* - the user is searching
* - the user opened the node during the search
*/
if (
children &&
(forceAllOpened || !collapsed || !!searchText || openedDuringSearch)
) {
flattenedChildren = children
.map(child =>
flattenNode(child, depth + 1, searchText, openedDuringSearch)
)
.flat();
}
const name = getItemName(item);
const dataset = getItemDataset ? getItemDataset(item) : undefined;
/*
* Append node to result if either:
* - the user is not searching
* - the nodes are force-opened (props)
* - the node is force-opened (if user opened the node during the search)
* - the node name matches the search
* - the node contains children that should be displayed
*/
if (
!searchText ||
forceAllOpened ||
forceOpen ||
name.toLowerCase().includes(searchText) ||
flattenedChildren.length > 0
) {
const thumbnailSrc = getItemThumbnail ? getItemThumbnail(item) : null;
const selected = selectedNodeIds.includes(id);
return [
{
id,
name,
hasChildren: !!children && children.length > 0,
canHaveChildren,
depth,
selected,
thumbnailSrc,
dataset,
item,
/*
* If the user is searching, the node should be opened if either:
* - it has children that should be displayed
* - the user opened it
*/
collapsed: !!searchText
? flattenedChildren.length === 0 || !openedDuringSearch
: collapsed,
/*
* Disable opening of the node if:
* - the user is searching
* - the node has children to be displayed but it's not because the user opened it
*/
disableCollapse:
!!searchText &&
flattenedChildren.length > 0 &&
!openedDuringSearch,
},
...flattenedChildren,
];
}
return [];
},
[
getItemChildren,
getItemId,
getItemName,
getItemThumbnail,
getItemDataset,
openedDuringSearchNodeIds,
openedNodeIds,
selectedNodeIds,
forceAllOpened,
]
);
const flattenOpened = React.useCallback(
(items: Item[], searchText: ?string): FlattenedNode<Item>[] => {
return items.map(item => flattenNode(item, 0, searchText, false)).flat();
},
[flattenNode]
);
const onOpen = React.useCallback(
(node: FlattenedNode<Item>) => {
if (isSearching) {
if (node.collapsed) {
setOpenedDuringSearchNodeIds([...openedDuringSearchNodeIds, node.id]);
} else {
if (!forceAllOpened)
setOpenedDuringSearchNodeIds(
openedDuringSearchNodeIds.filter(id => id !== node.id)
);
}
} else {
if (node.collapsed) {
setOpenedNodeIds([...openedNodeIds, node.id]);
} else {
if (!forceAllOpened)
setOpenedNodeIds(openedNodeIds.filter(id => id !== node.id));
}
}
},
[openedDuringSearchNodeIds, openedNodeIds, isSearching, forceAllOpened]
);
const onSelect = React.useCallback(
({
node,
exclusive,
}: {|
node: FlattenedNode<Item>,
exclusive?: boolean,
|}) => {
if (multiSelect) {
if (node.selected) {
if (exclusive) {
if (selectedItems.length === 1) return;
onSelectItems([node.item]);
} else
onSelectItems(selectedItems.filter(item => item !== node.item));
} else {
if (exclusive) onSelectItems([node.item]);
else onSelectItems([...selectedItems, node.item]);
}
} else {
if (node.selected && selectedItems.length === 1) return;
onSelectItems([node.item]);
}
},
[multiSelect, onSelectItems, selectedItems]
);
const onEndRenaming = (item: Item, newName: string) => {
const trimmedNewName = newName.trim();
setRenamedItemId(null);
if (!trimmedNewName) return;
if (getItemName(item) === trimmedNewName) return;
onRenameItem(item, trimmedNewName);
};
let flattenedData = React.useMemo(
() => flattenOpened(items, searchText ? searchText.toLowerCase() : null),
[flattenOpened, items, searchText]
);
const scrollToItem = React.useCallback(
(item: Item) => {
const list = listRef.current;
if (list) {
const itemId = getItemId(item);
// Browse flattenedData in reverse order since scrollToItem is mainly used
// to scroll to newly added object that is appended at the end of the list.
// $FlowFixMe - Method introduced in 2022.
const index = flattenedData.findLastIndex(node => node.id === itemId);
if (index >= 0) {
list.scrollToItem(index, 'smart');
}
}
},
[getItemId, flattenedData]
);
const renameItem = React.useCallback(
(item: Item) => {
setRenamedItemId(getItemId(item));
},
[getItemId]
);
const openItems = React.useCallback(
(itemIds: string[]) => {
const notAlreadyOpenedNodeIds = itemIds.filter(
itemId => !openedNodeIds.includes(itemId)
);
if (notAlreadyOpenedNodeIds.length > 0)
setOpenedNodeIds([...openedNodeIds, ...notAlreadyOpenedNodeIds]);
},
[openedNodeIds]
);
const closeItems = React.useCallback(
(itemIds: string[]) => {
const newOpenedNodesIds = openedNodeIds.filter(
openedNodeId => !itemIds.includes(openedNodeId)
);
setOpenedNodeIds(newOpenedNodesIds);
},
[openedNodeIds]
);
React.useImperativeHandle(
// $FlowFixMe
ref,
() => ({
forceUpdateList: forceUpdate,
scrollToItem,
renameItem,
openItems,
closeItems,
})
);
const DragSourceAndDropTarget = React.useMemo(
() =>
makeDragSourceAndDropTarget(reactDndType, {
vibrate: 100,
}),
[reactDndType]
);
const openContextMenu = React.useCallback(
({
x,
y,
item,
index,
}: {|
item: Item,
index: number,
x: number,
y: number,
|}) => {
if (contextMenuRef.current) {
contextMenuRef.current.open(x, y, { item, index });
}
},
[]
);
const onBlurField = React.useCallback(() => {
if (containerRef.current) {
containerRef.current.focus();
}
}, []);
const itemData: ItemData<Item> = getItemProps<Item>(
flattenedData,
onOpen,
onSelect,
onBlurField,
setRenamedItemId,
onEndRenaming,
renamedItemId,
openContextMenu,
canMoveSelectionToItem,
onMoveSelectionToItem,
onEditItem,
isMobileScreen,
DragSourceAndDropTarget,
getItemHtmlId
);
// Reset opened nodes during search when user stops searching
// or when the search text changes.
React.useEffect(
() => {
if (!searchText || searchText.length > 0) {
setOpenedDuringSearchNodeIds([]);
}
},
[searchText]
);
const onKeyDown = React.useCallback(
(event: KeyboardEvent) => {
if (!navigationKeys.includes(event.key)) return;
let newFocusedItem;
const item = selectedItems[0];
let itemIndexInFlattenedData = -1;
if (item) {
itemIndexInFlattenedData = flattenedData.findIndex(
node => node.id === getItemId(item)
);
}
if (itemIndexInFlattenedData === -1) {
// If no row is selected, start from the first row that is selectable.
let i = 0;
let newFocusedNode = flattenedData[i];
while (
newFocusedNode &&
(newFocusedNode.item.isRoot || newFocusedNode.item.isPlaceholder)
) {
i += 1;
if (i > flattenedData.length - 1) {
newFocusedNode = null;
}
newFocusedNode = flattenedData[i];
}
if (newFocusedNode) {
newFocusedItem = newFocusedNode.item;
}
} else if (event.key === 'ArrowDown') {
event.preventDefault();
if (itemIndexInFlattenedData < flattenedData.length - 1) {
let delta = 1;
let newFocusedNode = flattenedData[itemIndexInFlattenedData + delta];
while (
newFocusedNode &&
(newFocusedNode.item.isRoot || newFocusedNode.item.isPlaceholder)
) {
if (itemIndexInFlattenedData + delta > flattenedData.length - 1) {
newFocusedNode = null;
}
delta += 1;
newFocusedNode = flattenedData[itemIndexInFlattenedData + delta];
}
if (newFocusedNode) {
newFocusedItem = newFocusedNode.item;
}
}
} else if (event.key === 'ArrowUp') {
event.preventDefault();
if (itemIndexInFlattenedData > 0) {
let delta = -1;
let newFocusedNode = flattenedData[itemIndexInFlattenedData + delta];
while (
newFocusedNode &&
(newFocusedNode.item.isRoot || newFocusedNode.item.isPlaceholder)
) {
if (itemIndexInFlattenedData + delta < 0) {
newFocusedNode = null;
}
delta -= 1;
newFocusedNode = flattenedData[itemIndexInFlattenedData + delta];
}
if (newFocusedNode) {
newFocusedItem = newFocusedNode.item;
}
}
} else if (event.key === 'ArrowRight' && arrowKeyNavigationProps) {
event.preventDefault();
const node = flattenedData[itemIndexInFlattenedData];
if (node.canHaveChildren && node.collapsed) {
openItems([node.id]);
} else {
newFocusedItem = arrowKeyNavigationProps.onArrowRight(item);
}
} else if (event.key === 'ArrowLeft' && arrowKeyNavigationProps) {
event.preventDefault();
const node = flattenedData[itemIndexInFlattenedData];
if (node.canHaveChildren && !node.collapsed) {
closeItems([node.id]);
} else {
newFocusedItem = arrowKeyNavigationProps.onArrowLeft(item);
}
}
if (newFocusedItem) {
scrollToItem(newFocusedItem);
onSelectItems([newFocusedItem]);
}
},
[
flattenedData,
arrowKeyNavigationProps,
getItemId,
onSelectItems,
selectedItems,
scrollToItem,
openItems,
closeItems,
]
);
return (
<>
<div
tabIndex={0}
className={`${treeView} ${theme.treeViewRootClassName}`}
onKeyDown={onKeyDown}
ref={containerRef}
>
<FixedSizeList
height={height}
itemCount={flattenedData.length}
itemSize={32}
width={typeof width === 'number' ? width : '100%'}
itemKey={index => flattenedData[index].id}
// Flow does not seem to accept the generic used in FixedSizeList
// can itself use a generic.
// $FlowFixMe
itemData={itemData}
ref={listRef}
overscanCount={renderHiddenElements ? 20 : 2}
>
{TreeViewRow}
</FixedSizeList>
</div>
<ContextMenu
ref={contextMenuRef}
buildMenuTemplate={(i18n, options) =>
buildMenuTemplate(options.item, options.index)
}
/>
</>
);
};
// $FlowFixMe
export default React.forwardRef(TreeView);

View File

@@ -9,7 +9,7 @@ export const getRelativeOrAbsoluteDisplayDate = (
): React.Node => {
const nowAsNumber = Date.now();
if (nowAsNumber - dateAsNumber < 60 * 1000) {
return i18n._(t`Now`);
return i18n._(t`Just now`);
}
const now = new Date(nowAsNumber);
const date = new Date(dateAsNumber);

View File

@@ -127,6 +127,7 @@ export type PrivateGameTemplate = {|
createdAt: string,
tag: string,
longDescription: string,
gamePreviewLink: string,
|};
export type AllPublicAssets = {|

View File

@@ -57,9 +57,6 @@ export type TranslatedText =
type InAppTutorialFlowStepDOMChangeTrigger =
| {| presenceOfElement: string |}
| {| absenceOfElement: string |};
type InAppTutorialFlowStepShortcutTrigger =
| InAppTutorialFlowStepDOMChangeTrigger
| {| objectAddedInLayout: true |};
export type InAppTutorialFlowStepTrigger =
| InAppTutorialFlowStepDOMChangeTrigger
@@ -67,7 +64,6 @@ export type InAppTutorialFlowStepTrigger =
| {| valueHasChanged: true |}
| {| valueEquals: string |}
| {| instanceAddedOnScene: string, instancesCount?: number |}
| {| objectAddedInLayout: true |}
| {| previewLaunched: true |}
| {| clickOnTooltipButton: TranslatedText |};
@@ -76,7 +72,6 @@ export type InAppTutorialFlowStepFormattedTrigger =
| {| valueEquals: string |}
| {| valueHasChanged: true |}
| {| instanceAddedOnScene: string, instancesCount?: number |}
| {| objectAddedInLayout: true |}
| {| previewLaunched: true |}
| {| clickOnTooltipButton: string |};
@@ -112,7 +107,7 @@ export type InAppTutorialFlowStep = {|
shortcuts?: Array<{|
stepId: string,
// TODO: Adapt provider to make it possible to use other triggers as shortcuts
trigger: InAppTutorialFlowStepShortcutTrigger,
trigger: InAppTutorialFlowStepDOMChangeTrigger,
|}>,
dialog?: InAppTutorialDialog,
mapProjectData?: {

View File

@@ -1,17 +0,0 @@
// @flow
import { isNativeMobileApp } from './Platform';
export const hapticFeedback: ?({
durationInMs: number,
}) => void = !isNativeMobileApp()
? ({ durationInMs }) => {
try {
if (window.navigator && window.navigator.vibrate) {
window.navigator.vibrate(durationInMs);
}
} catch (error) {
console.warn('Vibration API not supported:', error);
}
}
: null;

View File

@@ -0,0 +1,89 @@
// @flow
// Helpers to manipulate tags. See also EditTagsDialog.js
export type Tags = Array<string>;
export type SelectedTags = Tags;
export const removeTag = (tags: Tags, tag: string): Tags => {
return tags.filter(selectedTag => selectedTag !== tag);
};
export const addTags = (tags: Tags, newTags: Tags): Tags => {
return Array.from(new Set([...tags, ...newTags]));
};
export type BuildTagsMenuTemplateOptions = {|
noTagLabel: string,
getAllTags: () => Array<string>,
selectedTags: SelectedTags,
onChange: SelectedTags => void,
onEditTags?: () => void,
editTagsLabel?: string,
|};
export const buildTagsMenuTemplate = ({
noTagLabel,
getAllTags,
selectedTags,
onChange,
onEditTags,
editTagsLabel,
}: BuildTagsMenuTemplateOptions): Array<any> => {
const allTags = getAllTags();
const footerMenuItems =
onEditTags && editTagsLabel
? [
{
type: 'separator',
},
{
label: editTagsLabel,
click: onEditTags,
},
]
: [];
if (!allTags.length) {
return [
{
label: noTagLabel,
enabled: false,
},
...footerMenuItems,
];
}
return allTags
.map(tag => ({
type: 'checkbox',
label: tag,
checked: selectedTags.includes(tag),
click: () => {
if (selectedTags.includes(tag)) {
onChange(removeTag(selectedTags, tag));
} else {
onChange(addTags(selectedTags, [tag]));
}
},
}))
.concat(footerMenuItems);
};
export const getTagsFromString = (tagsString: string): Tags => {
if (tagsString.trim() === '') return [];
return tagsString.split(',').map(tag => tag.trim());
};
export const getStringFromTags = (tags: Tags): string => {
return tags.join(', ');
};
export const hasStringAllTags = (tagsString: string, tags: Tags) => {
for (const tag of tags) {
if (tagsString.indexOf(tag) === -1) return false;
}
return true;
};

View File

@@ -23,7 +23,7 @@ const getClientXY = (event: TouchEvent): CallbackEvent => {
};
};
const defaultDelay = 600; // ms
const delay = 600; // ms
const moveTolerance = 10; // px
const contextLocks: { [string]: true } = {};
@@ -37,18 +37,13 @@ const contextLocks: { [string]: true } = {};
*/
export const useLongTouch = (
callback: (e: CallbackEvent) => void,
options?: {
/**
* To be set when nested elements with watched touches events are in conflict to run a callback.
* Priority will be given to the nested element.
*/
context?: string,
delay?: number,
}
/**
* To be set when nested elements with watched touches events are in conflict to run a callback.
* Priority will be given to the nested element.
*/
context?: string
) => {
const timeout = React.useRef<?TimeoutID>(null);
const context = options && options.context ? options.context : null;
const delay = options && options.delay ? options.delay : defaultDelay;
const currentTouchCallbackEvent = React.useRef<CallbackEvent>({
clientX: 0,
clientY: 0,
@@ -103,7 +98,7 @@ export const useLongTouch = (
callback(currentTouchCallbackEvent.current);
}, delay);
},
[callback, context, delay]
[callback, context]
);
const onMove = React.useCallback(

View File

@@ -346,6 +346,10 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => {
'Draggable'
);
// Add some tags
tiledSpriteObject.setTags('Tag1');
spriteObject.setTags('Tag1, Tag2');
const group1 = new gd.ObjectGroup();
group1.setName('GroupOfSprites');
group1.addObject('MySpriteObject');

View File

@@ -5,354 +5,354 @@ module.exports = [
"languageCode": "af_ZA",
"languageName": "Afrikaans",
"languageNativeName": "Afrikaans",
"translationRatio": 0.050518134715025864
"translationRatio": 0.05051159176272502
},
{
"languageCode": "ar_SA",
"languageName": "Arabic",
"languageNativeName": "اللغة العربية",
"translationRatio": 0.9608808290155441
"translationRatio": 0.9683978759228079
},
{
"languageCode": "az_AZ",
"languageName": "Azerbaijani",
"languageNativeName": "azərbaycan dili",
"translationRatio": 0.03497409326424872
"translationRatio": 0.03496956352804037
},
{
"languageCode": "bg_BG",
"languageName": "Bulgarian",
"languageNativeName": "български език",
"translationRatio": 0.09650259067357514
"translationRatio": 0.09649009195700042
},
{
"languageCode": "bn_BD",
"languageName": "Bengali",
"languageNativeName": "বাংলা",
"translationRatio": 0.05414507772020727
"translationRatio": 0.05413806501748475
},
{
"languageCode": "br_FR",
"languageName": "Breton",
"languageNativeName": "brezhoneg",
"translationRatio": 0.01943005181347146
"translationRatio": 0.019427535293355835
},
{
"languageCode": "ca_ES",
"languageName": "Catalan",
"languageNativeName": "Català",
"translationRatio": 0.26735751295336785
"translationRatio": 0.2673228856365756
},
{
"languageCode": "cs_CZ",
"languageName": "Czech",
"languageNativeName": "čeština",
"translationRatio": 0.24715025906735755
"translationRatio": 0.24698873202952987
},
{
"languageCode": "da_DK",
"languageName": "Danish",
"languageNativeName": "dansk",
"translationRatio": 0.05751295336787565
"translationRatio": 0.057505504468333135
},
{
"languageCode": "de_DE",
"languageName": "German",
"languageNativeName": "Deutsch",
"translationRatio": 0.6668393782383419
"translationRatio": 0.6790571169537625
},
{
"languageCode": "el_GR",
"languageName": "Greek",
"languageNativeName": "Ελληνικά",
"translationRatio": 0.22564766839378236
"translationRatio": 0.22561844320683844
},
{
"languageCode": "en",
"languageName": "English",
"languageNativeName": "English",
"translationRatio": 0.0556379821958457
"translationRatio": 0.05561735261401557
},
{
"languageCode": "eo_UY",
"languageName": "Esperanto",
"languageNativeName": "Esperanto",
"translationRatio": 0.26384535005224663
"translationRatio": 0.2655096490091957
},
{
"languageCode": "es_ES",
"languageName": "Spanish",
"languageNativeName": "Español",
"translationRatio": 0.963166144200627
"translationRatio": 0.9862712083926952
},
{
"languageCode": "fa_IR",
"languageName": "Persian",
"languageNativeName": "فارسی",
"translationRatio": 0.18534482758620685
"translationRatio": 0.1868928895220826
},
{
"languageCode": "fi_FI",
"languageName": "Finnish",
"languageNativeName": "suomi",
"translationRatio": 0.21264367816091956
"translationRatio": 0.2105944825799767
},
{
"languageCode": "fil_PH",
"languageName": "Filipino",
"languageNativeName": "Mga Filipino",
"translationRatio": 0.2933646812957158
"translationRatio": 0.2910244786944697
},
{
"languageCode": "fr_FR",
"languageName": "French",
"languageNativeName": "Français",
"translationRatio": 0.952324973876698
"translationRatio": 0.9646418857660924
},
{
"languageCode": "ha_HG",
"languageName": "Hausa",
"languageNativeName": "هَوُسَ",
"translationRatio": 0.023641588296760663
"translationRatio": 0.02357207615593837
},
{
"languageCode": "he_IL",
"languageName": "Hebrew",
"languageNativeName": "עברית",
"translationRatio": 0.1954022988505747
"translationRatio": 0.19582955575702632
},
{
"languageCode": "hi_IN",
"languageName": "Hindi",
"languageNativeName": "हिन्दी",
"translationRatio": 0.1795977011494253
"translationRatio": 0.17756767258127182
},
{
"languageCode": "hu_HU",
"languageName": "Hungarian",
"languageNativeName": "magyar",
"translationRatio": 0.15517241379310343
"translationRatio": 0.15347752881751064
},
{
"languageCode": "id_ID",
"languageName": "Indonesian",
"languageNativeName": "Bahasa Indonesia",
"translationRatio": 0.4849791013584117
"translationRatio": 0.4819323921771791
},
{
"languageCode": "ig_NG",
"languageName": "Igbo",
"languageNativeName": "Asụsụ Igbo",
"translationRatio": 0.019461859979101326
"translationRatio": 0.019427535293355835
},
{
"languageCode": "it_IT",
"languageName": "Italian",
"languageNativeName": "Italiano",
"translationRatio": 0.855146290491118
"translationRatio": 0.8470405387903122
},
{
"languageCode": "ja_JP",
"languageName": "Japanese",
"languageNativeName": "日本語",
"translationRatio": 0.9690438871473355
"translationRatio": 0.9839399041574926
},
{
"languageCode": "ka_GE",
"languageName": "Georgian",
"languageNativeName": "ქართული",
"translationRatio": 0.04793625914315569
"translationRatio": 0.047532703017743816
},
{
"languageCode": "km_KH",
"languageName": "Khmer",
"languageNativeName": "ខេមរភាសា",
"translationRatio": 0.025208986415882984
"translationRatio": 0.02512627897940678
},
{
"languageCode": "ko_KR",
"languageName": "Korean",
"languageNativeName": "한국어",
"translationRatio": 0.5057471264367817
"translationRatio": 0.5108146613133013
},
{
"languageCode": "lt_LT",
"languageName": "Lithuanian",
"languageNativeName": "lietuvių kalba",
"translationRatio": 0.0433646812957158
"translationRatio": 0.042999611449294095
},
{
"languageCode": "lv_LV",
"languageName": "Latvian",
"languageNativeName": "latviešu valoda",
"translationRatio": 0.019592476489028177
"translationRatio": 0.019557052195311453
},
{
"languageCode": "mr_IN",
"languageName": "Marathi",
"languageNativeName": "मराठी",
"translationRatio": 0.03343782654127481
"translationRatio": 0.03328584380261623
},
{
"languageCode": "ms_MY",
"languageName": "Malay",
"languageNativeName": "Bahasa Malaysia",
"translationRatio": 0.05237722048066873
"translationRatio": 0.05206579458619354
},
{
"languageCode": "my_MM",
"languageName": "Burmese",
"languageNativeName": "ဗမာစာ",
"translationRatio": 0.019853709508881878
"translationRatio": 0.01981608599922291
},
{
"languageCode": "nl_NL",
"languageName": "Dutch",
"languageNativeName": "Nederlands",
"translationRatio": 0.49242424242424243
"translationRatio": 0.49345939645123693
},
{
"languageCode": "no_NO",
"languageName": "Norwegian",
"languageNativeName": "Norsk",
"translationRatio": 0.2775600835945663
"translationRatio": 0.2728921124206709
},
{
"languageCode": "pl_PL",
"languageName": "Polish",
"languageNativeName": "język polski",
"translationRatio": 0.7223092998955067
"translationRatio": 0.7132495790700686
},
{
"languageCode": "pseudo_LOCALE",
"languageName": "for development only",
"languageNativeName": "Pseudolocalization",
"translationRatio": 0.9973524962178517
"translationRatio": 0.9974045235446792
},
{
"languageCode": "pt_BR",
"languageName": "Brazilian Portuguese",
"languageNativeName": "Português brasileiro",
"translationRatio": 0.8769592476489028
"translationRatio": 0.867763243103225
},
{
"languageCode": "pt_PT",
"languageName": "Portuguese",
"languageNativeName": "Português",
"translationRatio": 0.33816614420062696
"translationRatio": 0.33790959720243496
},
{
"languageCode": "ro_RO",
"languageName": "Romanian",
"languageNativeName": "Română",
"translationRatio": 0.10723615464994773
"translationRatio": 0.10594482579976683
},
{
"languageCode": "ru_RU",
"languageName": "Russian",
"languageNativeName": "Русский",
"translationRatio": 0.9242424242424242
"translationRatio": 0.941587877217977
},
{
"languageCode": "si_LK",
"languageName": "Sinhala",
"languageNativeName": "සිංහල",
"translationRatio": 0.7255747126436782
"translationRatio": 0.7159694340111384
},
{
"languageCode": "sk_SK",
"languageName": "Slovak",
"languageNativeName": "slovenčina",
"translationRatio": 0.19265935214211072
"translationRatio": 0.190260328972931
},
{
"languageCode": "sl_SI",
"languageName": "Slovene",
"languageNativeName": "slovenski jezik",
"translationRatio": 0.7642371995820272
"translationRatio": 0.7632431032249709
},
{
"languageCode": "sq_AL",
"languageName": "Albanian",
"languageNativeName": "Shqip",
"translationRatio": 0.169932079414838
"translationRatio": 0.17238699650304368
},
{
"languageCode": "sr_CS",
"languageName": "Serbian (Latin)",
"languageNativeName": "srpski",
"translationRatio": 0.16522988505747127
"translationRatio": 0.16396839787592277
},
{
"languageCode": "sr_SP",
"languageName": "Serbian",
"languageNativeName": "српски језик",
"translationRatio": 0.3346394984326019
"translationRatio": 0.33195181971247245
},
{
"languageCode": "sv_SE",
"languageName": "Swedish",
"languageNativeName": "svenska",
"translationRatio": 0.1940961337513062
"translationRatio": 0.19453438673746926
},
{
"languageCode": "sw_KE",
"languageName": "Swahili",
"languageNativeName": "Kiswahili",
"translationRatio": 0.02181295715778475
"translationRatio": 0.021758839528558505
},
{
"languageCode": "th_TH",
"languageName": "Thai",
"languageNativeName": "ไทย",
"translationRatio": 0.2740334378265413
"translationRatio": 0.28053360963605756
},
{
"languageCode": "tr_TR",
"languageName": "Turkish",
"languageNativeName": "Türkçe",
"translationRatio": 0.8241901776384535
"translationRatio": 0.8158269654189871
},
{
"languageCode": "uk_UA",
"languageName": "Ukrainian",
"languageNativeName": "Українська",
"translationRatio": 0.9627742946708464
"translationRatio": 0.991192850667012
},
{
"languageCode": "ur_PK",
"languageName": "Urdu",
"languageNativeName": "اردو",
"translationRatio": 0.025208986415882984
"translationRatio": 0.02512627897940678
},
{
"languageCode": "uz_UZ",
"languageName": "Uzbek",
"languageNativeName": "Ўзбек",
"translationRatio": 0.02468652037617558
"translationRatio": 0.024608211371583977
},
{
"languageCode": "vi_VN",
"languageName": "Vietnamese",
"languageNativeName": "Tiếng Việt",
"translationRatio": 0.28487460815047017
"translationRatio": 0.2802745758321461
},
{
"languageCode": "yo_NG",
"languageName": "Yoruba",
"languageNativeName": "Yorùbá",
"translationRatio": 0.3153082549634274
"translationRatio": 0.3127833182230281
},
{
"languageCode": "zh_CN",
"languageName": "Chinese Simplified",
"languageNativeName": "简化字",
"translationRatio": 0.9924242424242424
"translationRatio": 0.9922289858826577
},
{
"languageCode": "zh_TW",
"languageName": "Chinese Traditional",
"languageNativeName": "正體字",
"translationRatio": 0.23119122257053293
"translationRatio": 0.22924491646159828
}
];

Some files were not shown because too many files have changed in this diff Show More