mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
182 Commits
doc/mapper
...
experiment
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e02bebb0dd | ||
![]() |
d54a301b72 | ||
![]() |
342bd0e02b | ||
![]() |
52f42b8d57 | ||
![]() |
e5103ed4f6 | ||
![]() |
24736cc4e9 | ||
![]() |
da7fe3d6a9 | ||
![]() |
c7d98eaa92 | ||
![]() |
3f31174975 | ||
![]() |
6e5de52d8f | ||
![]() |
144bd97a03 | ||
![]() |
4ee0c7bed2 | ||
![]() |
5a0aac1a47 | ||
![]() |
96cfec78f0 | ||
![]() |
d6902df992 | ||
![]() |
acc960a6cf | ||
![]() |
4578353655 | ||
![]() |
fd248961a5 | ||
![]() |
ba31d47e54 | ||
![]() |
c44bc582e2 | ||
![]() |
164380baee | ||
![]() |
39b6bf9a62 | ||
![]() |
34136d2ea4 | ||
![]() |
a9dbe5c4af | ||
![]() |
396e451677 | ||
![]() |
9ddeb7dccf | ||
![]() |
0be95b91d6 | ||
![]() |
6551c07971 | ||
![]() |
f69a77a57e | ||
![]() |
699664d715 | ||
![]() |
4ea397d92b | ||
![]() |
3c4eb5a1ca | ||
![]() |
e9a0027f78 | ||
![]() |
c2d620069f | ||
![]() |
f0ff63fd07 | ||
![]() |
058dac9faa | ||
![]() |
3b69c52250 | ||
![]() |
098aef12d9 | ||
![]() |
0281f643b2 | ||
![]() |
58fa8c6ccc | ||
![]() |
f2e5fd7cf6 | ||
![]() |
da354676bd | ||
![]() |
df4a282aa2 | ||
![]() |
d8874ddf6f | ||
![]() |
af29c088c2 | ||
![]() |
6d902d0920 | ||
![]() |
826c55f8a7 | ||
![]() |
8320ef81ae | ||
![]() |
79bf6dd5c8 | ||
![]() |
16e3bf6612 | ||
![]() |
6043618fbc | ||
![]() |
ff04514338 | ||
![]() |
1122b17908 | ||
![]() |
45183ff7d4 | ||
![]() |
1fa366a657 | ||
![]() |
8193c13fd9 | ||
![]() |
5224df51cb | ||
![]() |
896d090b12 | ||
![]() |
6bc39f751f | ||
![]() |
72cd0aca4e | ||
![]() |
f4c94aaaac | ||
![]() |
c53bd9a01d | ||
![]() |
eea3fa9266 | ||
![]() |
3cb801617d | ||
![]() |
b0f5938b52 | ||
![]() |
bce8f794c9 | ||
![]() |
310b2249fc | ||
![]() |
2062297a85 | ||
![]() |
d37253e908 | ||
![]() |
e55a87012a | ||
![]() |
a5fed207ab | ||
![]() |
083868a62c | ||
![]() |
b27da452fc | ||
![]() |
96ed9b9886 | ||
![]() |
4690504e53 | ||
![]() |
0d597c3d10 | ||
![]() |
2f353b7a82 | ||
![]() |
a27de6fe5e | ||
![]() |
973d620f4d | ||
![]() |
a88aece778 | ||
![]() |
7c01eb5358 | ||
![]() |
bc672d050b | ||
![]() |
51676c7da5 | ||
![]() |
7499c05930 | ||
![]() |
af409f0b24 | ||
![]() |
7c65500e93 | ||
![]() |
d116fdab9c | ||
![]() |
ded48c0000 | ||
![]() |
87f1651820 | ||
![]() |
bb88379bbb | ||
![]() |
a468059ff1 | ||
![]() |
e1514949d1 | ||
![]() |
c42e4127d0 | ||
![]() |
45b6b25b27 | ||
![]() |
38e6b116ee | ||
![]() |
1369508b18 | ||
![]() |
f6de51f8f2 | ||
![]() |
6cba06d1c6 | ||
![]() |
83daec703e | ||
![]() |
a41176b02a | ||
![]() |
f94d3b1976 | ||
![]() |
a478dec68b | ||
![]() |
5ec7c37766 | ||
![]() |
d37699d767 | ||
![]() |
6695a42475 | ||
![]() |
bdae82633f | ||
![]() |
968416a7f7 | ||
![]() |
fcf83bfdba | ||
![]() |
f8e38bf719 | ||
![]() |
9b145a53b9 | ||
![]() |
2a6aa72c87 | ||
![]() |
56a4b723e6 | ||
![]() |
545f40930a | ||
![]() |
09d64ac134 | ||
![]() |
979473a2b1 | ||
![]() |
f33bb36ae4 | ||
![]() |
ab76528a6e | ||
![]() |
a4d4df68c2 | ||
![]() |
edfa6ba98c | ||
![]() |
514e143364 | ||
![]() |
2140cdb17f | ||
![]() |
d7a91ad68d | ||
![]() |
b7c8f3266b | ||
![]() |
e7c365043a | ||
![]() |
e4863cc4cd | ||
![]() |
733406bd91 | ||
![]() |
957a6db6ba | ||
![]() |
9124ec62e8 | ||
![]() |
299a4afdda | ||
![]() |
07a1961aec | ||
![]() |
3224cb0285 | ||
![]() |
54fdc7ee72 | ||
![]() |
b5ad0b265c | ||
![]() |
1239f56430 | ||
![]() |
de8fa30278 | ||
![]() |
6611ad0800 | ||
![]() |
b6b787e779 | ||
![]() |
95c904c3af | ||
![]() |
787d850a73 | ||
![]() |
2845da7774 | ||
![]() |
017d228542 | ||
![]() |
56fb3901cd | ||
![]() |
227bbbd44c | ||
![]() |
1b170cd428 | ||
![]() |
78629374fa | ||
![]() |
ab9ab5587d | ||
![]() |
67b9a4d44d | ||
![]() |
5878bd3749 | ||
![]() |
570d373dbd | ||
![]() |
e5b5190e2b | ||
![]() |
c85e172586 | ||
![]() |
0946097293 | ||
![]() |
619489e6b5 | ||
![]() |
55938796b7 | ||
![]() |
5bd4d08749 | ||
![]() |
b4c47d5a0b | ||
![]() |
cf7926206b | ||
![]() |
39e29250ab | ||
![]() |
c7938540ff | ||
![]() |
cb1fa39f46 | ||
![]() |
2f4ff772f5 | ||
![]() |
7b2a6c3d23 | ||
![]() |
c858bbfc68 | ||
![]() |
5d91d151f2 | ||
![]() |
f56f2dc6e5 | ||
![]() |
3fc4224efd | ||
![]() |
af280d13ab | ||
![]() |
510b9e738d | ||
![]() |
731592e1e9 | ||
![]() |
70934ebb1d | ||
![]() |
2950abaa45 | ||
![]() |
271b722cd2 | ||
![]() |
04c2a83023 | ||
![]() |
72a8ed7b4d | ||
![]() |
c295b66a26 | ||
![]() |
a6a2f55c11 | ||
![]() |
a4b26f0530 | ||
![]() |
e46a72b0ed | ||
![]() |
c35dd97e4f | ||
![]() |
82677da025 | ||
![]() |
944a13c5a5 | ||
![]() |
d4a9749ff5 |
@@ -17,7 +17,7 @@ EventsBasedObject::EventsBasedObject()
|
||||
}
|
||||
|
||||
EventsBasedObject::~EventsBasedObject() {}
|
||||
|
||||
|
||||
EventsBasedObject::EventsBasedObject(const gd::EventsBasedObject &_eventBasedObject)
|
||||
: AbstractEventsBasedEntity(_eventBasedObject) {
|
||||
// TODO Add a copy constructor in ObjectsContainer.
|
||||
@@ -30,14 +30,19 @@ 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
|
||||
|
@@ -294,6 +294,7 @@ 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"));
|
||||
|
||||
@@ -353,6 +354,11 @@ 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"));
|
||||
|
@@ -40,7 +40,6 @@ void Object::Init(const gd::Object& object) {
|
||||
name = object.name;
|
||||
assetStoreId = object.assetStoreId;
|
||||
objectVariables = object.objectVariables;
|
||||
tags = object.tags;
|
||||
effectsContainer = object.effectsContainer;
|
||||
|
||||
behaviors.clear();
|
||||
@@ -134,7 +133,6 @@ 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"));
|
||||
@@ -207,7 +205,6 @@ 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"));
|
||||
|
||||
|
@@ -120,14 +120,6 @@ 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(); }
|
||||
@@ -268,7 +260,6 @@ 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,
|
||||
|
244
Core/GDCore/Project/ObjectFolderOrObject.cpp
Normal file
244
Core/GDCore/Project/ObjectFolderOrObject.cpp
Normal file
@@ -0,0 +1,244 @@
|
||||
/*
|
||||
* 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
|
199
Core/GDCore/Project/ObjectFolderOrObject.h
Normal file
199
Core/GDCore/Project/ObjectFolderOrObject.h
Normal file
@@ -0,0 +1,199 @@
|
||||
/*
|
||||
* 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
|
@@ -9,12 +9,15 @@
|
||||
|
||||
#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() {}
|
||||
ObjectsContainer::ObjectsContainer() {
|
||||
rootFolder = gd::make_unique<gd::ObjectFolderOrObject>("__ROOT");
|
||||
}
|
||||
|
||||
ObjectsContainer::~ObjectsContainer() {}
|
||||
|
||||
@@ -24,6 +27,22 @@ 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) {
|
||||
@@ -53,7 +72,9 @@ bool ObjectsContainer::HasObjectNamed(const gd::String& name) const {
|
||||
gd::Object& ObjectsContainer::GetObject(const gd::String& name) {
|
||||
return *(*find_if(initialObjects.begin(),
|
||||
initialObjects.end(),
|
||||
bind2nd(gd::ObjectHasName(), name)));
|
||||
[&name](std::unique_ptr<gd::Object>& object) {
|
||||
return object->GetName() == name;
|
||||
}));
|
||||
}
|
||||
const gd::Object& ObjectsContainer::GetObject(const gd::String& name) const {
|
||||
return *(*find_if(initialObjects.begin(),
|
||||
@@ -84,6 +105,22 @@ 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;
|
||||
}
|
||||
|
||||
@@ -97,16 +134,6 @@ 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;
|
||||
@@ -120,30 +147,38 @@ void ObjectsContainer::RemoveObject(const gd::String& name) {
|
||||
std::vector<std::unique_ptr<gd::Object>>::iterator objectIt =
|
||||
find_if(initialObjects.begin(),
|
||||
initialObjects.end(),
|
||||
bind2nd(ObjectHasName(), name));
|
||||
[&name](std::unique_ptr<gd::Object>& object) {
|
||||
return object->GetName() == name;
|
||||
});
|
||||
if (objectIt == initialObjects.end()) return;
|
||||
|
||||
rootFolder->RemoveRecursivelyObjectNamed(name);
|
||||
|
||||
initialObjects.erase(objectIt);
|
||||
}
|
||||
|
||||
void ObjectsContainer::MoveObjectToAnotherContainer(
|
||||
const gd::String& name,
|
||||
void ObjectsContainer::MoveObjectFolderOrObjectToAnotherContainerInFolder(
|
||||
gd::ObjectFolderOrObject& objectFolderOrObject,
|
||||
gd::ObjectsContainer& newContainer,
|
||||
gd::ObjectFolderOrObject& newParentFolder,
|
||||
std::size_t newPosition) {
|
||||
std::vector<std::unique_ptr<gd::Object>>::iterator objectIt =
|
||||
find_if(initialObjects.begin(),
|
||||
initialObjects.end(),
|
||||
bind2nd(ObjectHasName(), name));
|
||||
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();
|
||||
});
|
||||
if (objectIt == initialObjects.end()) return;
|
||||
|
||||
std::unique_ptr<gd::Object> object = std::move(*objectIt);
|
||||
initialObjects.erase(objectIt);
|
||||
|
||||
newContainer.initialObjects.insert(
|
||||
newPosition < newContainer.initialObjects.size()
|
||||
? newContainer.initialObjects.begin() + newPosition
|
||||
: newContainer.initialObjects.end(),
|
||||
std::move(object));
|
||||
newContainer.initialObjects.push_back(std::move(object));
|
||||
|
||||
objectFolderOrObject.GetParent().MoveObjectFolderOrObjectToAnotherFolder(
|
||||
objectFolderOrObject, newParentFolder, newPosition);
|
||||
}
|
||||
|
||||
} // namespace gd
|
||||
|
@@ -9,11 +9,12 @@
|
||||
#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 {
|
||||
@@ -98,6 +99,19 @@ 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
|
||||
@@ -125,18 +139,18 @@ class GD_CORE_API ObjectsContainer {
|
||||
void MoveObject(std::size_t oldIndex, std::size_t newIndex);
|
||||
|
||||
/**
|
||||
* \brief Swap the position of the specified objects.
|
||||
*/
|
||||
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.
|
||||
* 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).
|
||||
* \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);
|
||||
void MoveObjectFolderOrObjectToAnotherContainerInFolder(
|
||||
gd::ObjectFolderOrObject& objectFolderOrObject,
|
||||
gd::ObjectsContainer& newContainer,
|
||||
gd::ObjectFolderOrObject& newParentFolder,
|
||||
std::size_t newPosition);
|
||||
|
||||
/**
|
||||
* Provide a raw access to the vector containing the objects
|
||||
@@ -153,20 +167,36 @@ 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 instances container.
|
||||
* \brief Serialize the objects container.
|
||||
*/
|
||||
void SerializeObjectsTo(SerializerElement& element) const;
|
||||
|
||||
/**
|
||||
* \brief Unserialize the instances container.
|
||||
* \brief Unserialize the objects 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
|
||||
@@ -190,6 +220,9 @@ 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
|
||||
|
@@ -125,7 +125,7 @@ Project::CreateObject(const gd::String &objectType, const gd::String &name) cons
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
return std::move(object);
|
||||
}
|
||||
|
||||
@@ -849,6 +849,11 @@ 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();
|
||||
@@ -1000,6 +1005,7 @@ 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"));
|
||||
|
||||
|
@@ -396,21 +396,42 @@ 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 MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
|
||||
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
|
||||
unsigned long GetObjectsCount();
|
||||
|
||||
[Ref] ObjectFolderOrObject GetRootFolder();
|
||||
[Ref] ObjectGroupsContainer GetObjectGroups();
|
||||
};
|
||||
|
||||
@@ -544,17 +565,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 MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
|
||||
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
|
||||
unsigned long GetObjectsCount();
|
||||
|
||||
[Ref] ObjectFolderOrObject GetRootFolder();
|
||||
[Ref] ObjectGroupsContainer GetObjectGroups();
|
||||
};
|
||||
|
||||
@@ -687,8 +708,6 @@ 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();
|
||||
@@ -790,17 +809,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 MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
|
||||
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
|
||||
unsigned long GetObjectsCount();
|
||||
|
||||
[Ref] ObjectFolderOrObject GetRootFolder();
|
||||
[Ref] ObjectGroupsContainer GetObjectGroups();
|
||||
};
|
||||
|
||||
@@ -2749,17 +2768,18 @@ 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 MoveObjectToAnotherContainer([Const] DOMString name, [Ref] ObjectsContainer newObjectsContainer, unsigned long newPosition);
|
||||
void MoveObjectFolderOrObjectToAnotherContainerInFolder([Ref] ObjectFolderOrObject objectFolderOrObject, [Ref] ObjectsContainer newObjectsContainer, [Ref] ObjectFolderOrObject parentObjectFolderOrObject, unsigned long newPosition);
|
||||
unsigned long GetObjectsCount();
|
||||
|
||||
[Ref] ObjectFolderOrObject GetRootFolder();
|
||||
[Ref] ObjectGroupsContainer GetObjectGroups();
|
||||
};
|
||||
EventsBasedObject implements AbstractEventsBasedEntity;
|
||||
|
@@ -72,6 +72,7 @@
|
||||
#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>
|
||||
|
@@ -4,6 +4,7 @@ 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;
|
||||
@@ -440,7 +441,10 @@ 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',
|
||||
@@ -460,9 +464,11 @@ 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)
|
||||
@@ -470,11 +476,24 @@ 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.moveObjectToAnotherContainer(
|
||||
'MySprite2',
|
||||
objectsContainer1.moveObjectFolderOrObjectToAnotherContainerInFolder(
|
||||
mySprite2ObjectFolderOrObject,
|
||||
objectsContainer2,
|
||||
rootFolder2,
|
||||
0
|
||||
);
|
||||
expect(objectsContainer1.getObjectsCount()).toBe(2);
|
||||
@@ -482,17 +501,34 @@ 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);
|
||||
|
||||
objectsContainer1.moveObjectToAnotherContainer(
|
||||
'MySprite3',
|
||||
// Move object in sub folder.
|
||||
objectsContainer1.moveObjectFolderOrObjectToAnotherContainerInFolder(
|
||||
mySprite3ObjectFolderOrObject,
|
||||
objectsContainer2,
|
||||
1
|
||||
subFolder2,
|
||||
0
|
||||
);
|
||||
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(
|
||||
@@ -505,27 +541,53 @@ describe('libGD.js', function () {
|
||||
mySprite3ObjectPtr
|
||||
);
|
||||
|
||||
objectsContainer2.moveObjectToAnotherContainer(
|
||||
'MySprite2',
|
||||
expect(gd.getPointer(rootFolder2.getObjectChild('MySprite2'))).toBe(
|
||||
mySprite2ObjectFolderOrObjectPtr
|
||||
);
|
||||
expect(rootFolder2.getObjectChild('MySprite2')).toBe(
|
||||
mySprite2ObjectFolderOrObject
|
||||
);
|
||||
|
||||
// Move back first object to first container
|
||||
objectsContainer2.moveObjectFolderOrObjectToAnotherContainerInFolder(
|
||||
mySprite2ObjectFolderOrObject,
|
||||
objectsContainer1,
|
||||
rootFolder1,
|
||||
0
|
||||
);
|
||||
expect(objectsContainer1.getObjectsCount()).toBe(2);
|
||||
expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite2');
|
||||
expect(objectsContainer1.getObjectAt(1).getName()).toBe('MySprite');
|
||||
expect(objectsContainer1.getObjectAt(0).getName()).toBe('MySprite');
|
||||
expect(objectsContainer1.getObjectAt(1).getName()).toBe('MySprite2');
|
||||
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(
|
||||
mySprite2ObjectPtr
|
||||
mySpriteObjectPtr
|
||||
);
|
||||
expect(gd.getPointer(objectsContainer1.getObjectAt(1))).toBe(
|
||||
mySpriteObjectPtr
|
||||
mySprite2ObjectPtr
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -2865,13 +2927,11 @@ describe('libGD.js', function () {
|
||||
});
|
||||
|
||||
describe('gd.SpriteObject', function () {
|
||||
it('is a gd.Object and can have tags', function () {
|
||||
it('is a gd.Object', 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();
|
||||
});
|
||||
@@ -4537,4 +4597,276 @@ 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -14,16 +14,17 @@ 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;
|
||||
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
|
||||
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
|
||||
getObjectsCount(): number;
|
||||
getRootFolder(): gdObjectFolderOrObject;
|
||||
getObjectGroups(): gdObjectGroupsContainer;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
|
@@ -33,16 +33,17 @@ 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;
|
||||
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
|
||||
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
|
||||
getObjectsCount(): number;
|
||||
getRootFolder(): gdObjectFolderOrObject;
|
||||
getObjectGroups(): gdObjectGroupsContainer;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
|
@@ -8,8 +8,6 @@ declare class gdObject {
|
||||
getAssetStoreId(): string;
|
||||
setType(type: string): void;
|
||||
getType(): string;
|
||||
setTags(tags: string): void;
|
||||
getTags(): string;
|
||||
is3DObject(): boolean;
|
||||
getConfiguration(): gdObjectConfiguration;
|
||||
getVariables(): gdVariablesContainer;
|
||||
|
23
GDevelop.js/types/gdobjectfolderorobject.js
Normal file
23
GDevelop.js/types/gdobjectfolderorobject.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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;
|
||||
};
|
@@ -2,16 +2,17 @@
|
||||
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;
|
||||
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
|
||||
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
|
||||
getObjectsCount(): number;
|
||||
getRootFolder(): gdObjectFolderOrObject;
|
||||
getObjectGroups(): gdObjectGroupsContainer;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
|
@@ -114,16 +114,17 @@ 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;
|
||||
moveObjectToAnotherContainer(name: string, newObjectsContainer: gdObjectsContainer, newPosition: number): void;
|
||||
moveObjectFolderOrObjectToAnotherContainerInFolder(objectFolderOrObject: gdObjectFolderOrObject, newObjectsContainer: gdObjectsContainer, parentObjectFolderOrObject: gdObjectFolderOrObject, newPosition: number): void;
|
||||
getObjectsCount(): number;
|
||||
getRootFolder(): gdObjectFolderOrObject;
|
||||
getObjectGroups(): gdObjectGroupsContainer;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
|
@@ -71,6 +71,7 @@ declare class libGDevelop {
|
||||
PlatformSpecificAssets: Class<gdPlatformSpecificAssets>;
|
||||
LoadingScreen: Class<gdLoadingScreen>;
|
||||
Watermark: Class<gdWatermark>;
|
||||
ObjectFolderOrObject: Class<gdObjectFolderOrObject>;
|
||||
ObjectsContainer: Class<gdObjectsContainer>;
|
||||
Project: Class<gdProject>;
|
||||
ObjectsContainersList: Class<gdObjectsContainersList>;
|
||||
|
@@ -11,6 +11,10 @@
|
||||
# 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/.*
|
||||
|
36
newIDE/app/package-lock.json
generated
36
newIDE/app/package-lock.json
generated
@@ -54,6 +54,7 @@
|
||||
"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",
|
||||
@@ -24909,6 +24910,11 @@
|
||||
"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,
|
||||
@@ -30706,6 +30712,22 @@
|
||||
"@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",
|
||||
@@ -53649,6 +53671,11 @@
|
||||
"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,
|
||||
@@ -57427,6 +57454,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
|
@@ -84,6 +84,7 @@
|
||||
"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",
|
||||
|
@@ -13,6 +13,10 @@ 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;
|
||||
|
||||
@@ -26,8 +30,7 @@ type Props = {|
|
||||
type State = {|
|
||||
editedObjectWithContext: ?ObjectWithContext,
|
||||
editedObjectInitialTab: ?ObjectEditorTab,
|
||||
selectedObjectsWithContext: ObjectWithContext[],
|
||||
renamedObjectWithContext: ?ObjectWithContext,
|
||||
selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
|
||||
|};
|
||||
|
||||
export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
@@ -39,8 +42,7 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
state = {
|
||||
editedObjectWithContext: null,
|
||||
editedObjectInitialTab: 'properties',
|
||||
selectedObjectsWithContext: [],
|
||||
renamedObjectWithContext: null,
|
||||
selectedObjectFolderOrObjectsWithContext: [],
|
||||
};
|
||||
|
||||
_onDeleteObject = (i18n: I18nType) => (
|
||||
@@ -72,7 +74,7 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
done(true);
|
||||
};
|
||||
|
||||
_getValidatedObjectOrGroupName = (newName: string, i18n: I18nType) => {
|
||||
_getValidatedObjectOrGroupName = (newName: string) => {
|
||||
const { eventsBasedObject } = this.props;
|
||||
|
||||
const safeAndUniqueNewName = newNameGenerator(
|
||||
@@ -94,37 +96,15 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
return safeAndUniqueNewName;
|
||||
};
|
||||
|
||||
_onRenameObjectStart = (objectWithContext: ?ObjectWithContext) => {
|
||||
const selectedObjectsWithContext = [];
|
||||
if (objectWithContext) {
|
||||
selectedObjectsWithContext.push(objectWithContext);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
renamedObjectWithContext: objectWithContext,
|
||||
selectedObjectsWithContext,
|
||||
},
|
||||
() => {
|
||||
this.forceUpdateObjectsList();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
_onRenameEditedObject = (newName: string, i18n: I18nType) => {
|
||||
_onRenameEditedObject = (newName: string) => {
|
||||
const { editedObjectWithContext } = this.state;
|
||||
|
||||
if (editedObjectWithContext) {
|
||||
this._onRenameObject(editedObjectWithContext, newName, () => {}, i18n);
|
||||
this._onRenameObject(editedObjectWithContext, newName);
|
||||
}
|
||||
};
|
||||
|
||||
_onRenameObject = (
|
||||
objectWithContext: ObjectWithContext,
|
||||
newName: string,
|
||||
done: boolean => void,
|
||||
i18n: I18nType
|
||||
) => {
|
||||
_onRenameObject = (objectWithContext: ObjectWithContext, newName: string) => {
|
||||
const { object } = objectWithContext;
|
||||
const { project, globalObjectsContainer, eventsBasedObject } = this.props;
|
||||
|
||||
@@ -143,6 +123,40 @@ 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);
|
||||
};
|
||||
|
||||
@@ -163,15 +177,19 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
}
|
||||
};
|
||||
|
||||
_onObjectSelected = (objectWithContext: ?ObjectWithContext = null) => {
|
||||
const selectedObjectsWithContext = [];
|
||||
if (objectWithContext) {
|
||||
selectedObjectsWithContext.push(objectWithContext);
|
||||
_onObjectFolderOrObjectWithContextSelected = (
|
||||
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext = null
|
||||
) => {
|
||||
const selectedObjectFolderOrObjectsWithContext = [];
|
||||
if (objectFolderOrObjectWithContext) {
|
||||
selectedObjectFolderOrObjectsWithContext.push(
|
||||
objectFolderOrObjectWithContext
|
||||
);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
selectedObjectsWithContext,
|
||||
selectedObjectFolderOrObjectsWithContext,
|
||||
},
|
||||
() => {
|
||||
this.forceUpdateObjectsList();
|
||||
@@ -198,10 +216,7 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
|
||||
render() {
|
||||
const { eventsBasedObject, project, eventsFunctionsExtension } = this.props;
|
||||
|
||||
const selectedObjectNames = this.state.selectedObjectsWithContext.map(
|
||||
objWithContext => objWithContext.object.getName()
|
||||
);
|
||||
const { selectedObjectFolderOrObjectsWithContext } = this.state;
|
||||
|
||||
// TODO EBO When adding an object, filter the object types to excludes
|
||||
// object that depend (transitively) on this object to avoid cycles.
|
||||
@@ -230,32 +245,28 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
onFetchNewlyAddedResources: async () => {},
|
||||
getStorageProviderResourceOperations: () => null,
|
||||
}}
|
||||
selectedObjectNames={selectedObjectNames}
|
||||
selectedObjectFolderOrObjectsWithContext={
|
||||
selectedObjectFolderOrObjectsWithContext
|
||||
}
|
||||
onEditObject={this.editObject}
|
||||
// Don't allow export as there is no assets.
|
||||
onExportObject={() => {}}
|
||||
onDeleteObject={this._onDeleteObject(i18n)}
|
||||
getValidatedObjectOrGroupName={newName =>
|
||||
this._getValidatedObjectOrGroupName(newName, i18n)
|
||||
getValidatedObjectOrGroupName={
|
||||
this._getValidatedObjectOrGroupName
|
||||
}
|
||||
// Nothing special to do.
|
||||
onObjectCreated={() => {}}
|
||||
onObjectSelected={this._onObjectSelected}
|
||||
renamedObjectWithContext={this.state.renamedObjectWithContext}
|
||||
onRenameObjectStart={this._onRenameObjectStart}
|
||||
onRenameObjectFinish={(objectWithContext, newName, done) =>
|
||||
this._onRenameObject(objectWithContext, newName, done, i18n)
|
||||
onObjectFolderOrObjectWithContextSelected={
|
||||
this._onObjectFolderOrObjectWithContextSelected
|
||||
}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={
|
||||
this._onRenameObjectFolderOrObjectFinish
|
||||
}
|
||||
// Instances can't be created from this context.
|
||||
onAddObjectInstance={() => {}}
|
||||
onObjectPasted={() => this.updateBehaviorsSharedData()}
|
||||
selectedObjectTags={[]}
|
||||
onChangeSelectedObjectTags={selectedObjectTags => {}}
|
||||
getAllObjectTags={() => []}
|
||||
ref={
|
||||
// $FlowFixMe Make this component functional.
|
||||
objectsList => (this._objectsList = objectsList)
|
||||
}
|
||||
ref={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
|
||||
@@ -300,12 +311,10 @@ export default class EventBasedObjectChildrenEditor extends React.Component<
|
||||
onCancel={() => {
|
||||
this.editObject(null);
|
||||
}}
|
||||
getValidatedObjectOrGroupName={newName =>
|
||||
this._getValidatedObjectOrGroupName(newName, i18n)
|
||||
getValidatedObjectOrGroupName={
|
||||
this._getValidatedObjectOrGroupName
|
||||
}
|
||||
onRename={newName => {
|
||||
this._onRenameEditedObject(newName, i18n);
|
||||
}}
|
||||
onRename={this._onRenameEditedObject}
|
||||
onApply={() => {
|
||||
this.editObject(null);
|
||||
this.updateBehaviorsSharedData();
|
||||
|
@@ -55,7 +55,6 @@ export default class BrowserEventsFunctionsExtensionWriter {
|
||||
filename: string
|
||||
): Promise<void> => {
|
||||
const exportedObject = customObject.clone().get();
|
||||
exportedObject.setTags('');
|
||||
exportedObject.getVariables().clear();
|
||||
exportedObject.getEffects().clear();
|
||||
exportedObject
|
||||
|
@@ -89,7 +89,6 @@ export default class LocalEventsFunctionsExtensionWriter {
|
||||
filepath: string
|
||||
): Promise<void> => {
|
||||
const exportedObject = customObject.clone().get();
|
||||
exportedObject.setTags('');
|
||||
exportedObject.getVariables().clear();
|
||||
exportedObject.getEffects().clear();
|
||||
exportedObject
|
||||
|
@@ -29,3 +29,5 @@ export const handle = 'move-handle';
|
||||
export const linkContainer = 'link-container';
|
||||
|
||||
export const nameAndIconContainer = 'name-and-icon-container';
|
||||
|
||||
export const treeView = 'tree-view';
|
||||
|
@@ -435,7 +435,7 @@ const Instruction = (props: Props) => {
|
||||
},
|
||||
[onContextMenu]
|
||||
),
|
||||
'events-tree-event-component'
|
||||
{ context: 'events-tree-event-component' }
|
||||
);
|
||||
|
||||
return (
|
||||
|
@@ -171,7 +171,7 @@ const EventContainer = (props: EventsContainerProps) => {
|
||||
},
|
||||
[onEventContextMenu]
|
||||
),
|
||||
'events-tree-event-component'
|
||||
{ context: 'events-tree-event-component' }
|
||||
);
|
||||
|
||||
const EventComponent = EventsRenderingService.getEventComponent(event);
|
||||
|
@@ -6,7 +6,6 @@ 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,
|
||||
@@ -28,12 +27,9 @@ 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';
|
||||
@@ -41,10 +37,6 @@ 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,
|
||||
@@ -59,10 +51,21 @@ 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';
|
||||
|
||||
@@ -71,12 +74,15 @@ 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 = {|
|
||||
@@ -111,8 +117,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
> {
|
||||
state = {
|
||||
searchText: '',
|
||||
selectedObjectTags: [],
|
||||
searchResults: { objects: [], groups: [], instructions: [], tags: [] },
|
||||
searchResults: { objects: [], groups: [], instructions: [], folders: [] },
|
||||
};
|
||||
_searchBar = React.createRef<SearchBarInterface>();
|
||||
_scrollView = React.createRef<ScrollViewInterface>();
|
||||
@@ -137,7 +142,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
instructionSearchApi = null;
|
||||
objectSearchApi = null;
|
||||
groupSearchApi = null;
|
||||
tagSearchApi = null;
|
||||
folderSearchApi = null;
|
||||
|
||||
reEnumerateInstructions = (i18n: I18nType) => {
|
||||
this.freeInstructionsInfo = filterEnumeratedInstructionOrExpressionMetadataByScope(
|
||||
@@ -167,6 +172,15 @@ 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),
|
||||
{
|
||||
@@ -187,8 +201,10 @@ 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.tagSearchApi = new Fuse(this._getAllObjectTags(), {
|
||||
this.folderSearchApi = new Fuse(allFolders, {
|
||||
...sharedFuseConfiguration,
|
||||
getFn: (item, property) => item.path,
|
||||
keys: ['name'], // Not used as we only use the path to the folder
|
||||
});
|
||||
}
|
||||
|
||||
@@ -211,6 +227,12 @@ 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(
|
||||
@@ -224,47 +246,6 @@ 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 });
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -285,7 +266,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
onSearchStartOrReset,
|
||||
onClickMore,
|
||||
} = this.props;
|
||||
const { searchText, selectedObjectTags, searchResults } = this.state;
|
||||
const { searchText, 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.
|
||||
@@ -300,13 +281,13 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
let filteredObjectsList = [];
|
||||
let displayedObjectGroupsList = [];
|
||||
let filteredInstructionsList = [];
|
||||
let displayedTags = [];
|
||||
let filteredFoldersList = [];
|
||||
|
||||
if (isSearching) {
|
||||
filteredObjectsList = searchResults.objects;
|
||||
displayedObjectGroupsList = searchResults.groups;
|
||||
filteredInstructionsList = searchResults.instructions;
|
||||
displayedTags = searchResults.tags;
|
||||
filteredFoldersList = searchResults.folders;
|
||||
} else {
|
||||
filteredObjectsList = allObjectsList.map(object => ({
|
||||
item: object,
|
||||
@@ -317,10 +298,6 @@ 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
|
||||
@@ -335,20 +312,18 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
|
||||
const hasResults =
|
||||
!isSearching ||
|
||||
!!displayedObjectsList.length ||
|
||||
!!filteredObjectsList.length ||
|
||||
!!displayedObjectGroupsList.length ||
|
||||
!!displayedInstructionsList.length ||
|
||||
!!displayedTags.length;
|
||||
!!filteredFoldersList;
|
||||
|
||||
const onSubmitSearch = () => {
|
||||
if (!isSearching) return;
|
||||
|
||||
if (displayedObjectsList.length > 0) {
|
||||
onChooseObject(displayedObjectsList[0].item.object.getName());
|
||||
if (filteredObjectsList.length > 0) {
|
||||
onChooseObject(filteredObjectsList[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,
|
||||
@@ -358,7 +333,7 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
};
|
||||
|
||||
return (
|
||||
<I18n key="tags">
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<div
|
||||
id="instruction-or-object-selector"
|
||||
@@ -388,7 +363,6 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
}
|
||||
}}
|
||||
onRequestSearch={onSubmitSearch}
|
||||
buildMenuTemplate={() => this._buildObjectTagsMenuTemplate(i18n)}
|
||||
ref={this._searchBar}
|
||||
autoFocus={this.props.focusOnMount ? 'desktop' : undefined}
|
||||
placeholder={
|
||||
@@ -422,21 +396,11 @@ 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>
|
||||
{displayedObjectsList.map(
|
||||
{filteredObjectsList.map(
|
||||
({ item: objectWithContext, matches }, index) =>
|
||||
renderObjectListItem({
|
||||
project: project,
|
||||
@@ -467,42 +431,145 @@ export default class InstructionOrObjectSelector extends React.PureComponent<
|
||||
</Subheader>
|
||||
)}
|
||||
{displayedObjectGroupsList.map(
|
||||
({ 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,
|
||||
})
|
||||
({ 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;
|
||||
}
|
||||
)}
|
||||
</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 ? (
|
||||
|
@@ -0,0 +1,37 @@
|
||||
// @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
|
||||
/>
|
||||
);
|
||||
};
|
@@ -9,6 +9,7 @@ import {
|
||||
getObjectListItemKey,
|
||||
} from './Keys';
|
||||
import HighlightedText from '../../../UI/Search/HighlightedText';
|
||||
import { styles } from '../InstructionOrObjectSelector';
|
||||
import { type HTMLDataset } from '../../../Utils/HTMLDataset';
|
||||
|
||||
type Props = {|
|
||||
@@ -20,6 +21,8 @@ type Props = {|
|
||||
matchesCoordinates: number[][],
|
||||
id?: ?string,
|
||||
data?: HTMLDataset,
|
||||
withIndent?: boolean,
|
||||
keyPrefix?: string,
|
||||
|};
|
||||
|
||||
export const renderObjectListItem = ({
|
||||
@@ -31,16 +34,19 @@ export const renderObjectListItem = ({
|
||||
matchesCoordinates,
|
||||
id,
|
||||
data,
|
||||
withIndent,
|
||||
keyPrefix,
|
||||
}: Props) => {
|
||||
const objectName: string = objectWithContext.object.getName();
|
||||
return (
|
||||
<ListItem
|
||||
id={id}
|
||||
data={data}
|
||||
key={getObjectListItemKey(objectWithContext)}
|
||||
key={(keyPrefix || '') + getObjectListItemKey(objectWithContext)}
|
||||
selected={
|
||||
selectedValue === getObjectOrObjectGroupListItemValue(objectName)
|
||||
}
|
||||
style={withIndent ? styles.indentedListItem : undefined}
|
||||
primaryText={
|
||||
<HighlightedText
|
||||
text={objectName}
|
||||
|
@@ -36,6 +36,7 @@ const selectorInterpolationProjectDataAccessors = {
|
||||
objectInObjectOrResourceSelector: 'objectInObjectOrResourceSelector:',
|
||||
editorTab: 'editorTab:',
|
||||
};
|
||||
const legacyItemInObjectListDomSelectorPattern = /#object-item-[0-9]{1,2}$/;
|
||||
|
||||
const getPhasesStartIndices = (endIndices: Array<number>): Array<number> =>
|
||||
endIndices.map((_, i) => {
|
||||
@@ -132,6 +133,22 @@ 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,
|
||||
@@ -238,6 +255,9 @@ const isDomBasedTriggerComplete = (
|
||||
if (!trigger) return false;
|
||||
if (
|
||||
trigger.presenceOfElement &&
|
||||
!trigger.presenceOfElement.match(
|
||||
legacyItemInObjectListDomSelectorPattern
|
||||
) &&
|
||||
document.querySelector(
|
||||
interpolateElementId(trigger.presenceOfElement, data)
|
||||
)
|
||||
@@ -386,6 +406,9 @@ 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
|
||||
);
|
||||
@@ -412,6 +435,10 @@ const InAppTutorialOrchestrator = React.forwardRef<
|
||||
objectName: string,
|
||||
count?: number,
|
||||
|}>(null);
|
||||
const [
|
||||
sceneObjectCountToWatch,
|
||||
setSceneObjectCountToWatch,
|
||||
] = React.useState<boolean>(false);
|
||||
const domObserverRef = React.useRef<?MutationObserver>(null);
|
||||
const [
|
||||
shouldWatchProjectChanges,
|
||||
@@ -464,7 +491,13 @@ const InAppTutorialOrchestrator = React.forwardRef<
|
||||
);
|
||||
|
||||
const goToStep = React.useCallback(
|
||||
(stepIndex: number) => {
|
||||
({
|
||||
stepIndex,
|
||||
gatherData,
|
||||
}: {
|
||||
stepIndex: number,
|
||||
gatherData?: boolean,
|
||||
}) => {
|
||||
if (stepIndex >= stepCount) {
|
||||
setDisplayEndDialog(true);
|
||||
return;
|
||||
@@ -486,12 +519,23 @@ 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]
|
||||
[flow, changeStep, stepCount, data, project, currentStepIndex]
|
||||
);
|
||||
|
||||
// Compute phases start positions on flow change.
|
||||
React.useEffect(
|
||||
() => {
|
||||
const indices = [];
|
||||
@@ -531,6 +575,24 @@ 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,
|
||||
@@ -573,8 +635,17 @@ 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 not-dom based triggers
|
||||
if (isDomBasedTriggerComplete(shortcutStep.trigger, data)) {
|
||||
// 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())
|
||||
) {
|
||||
shouldGoToStepAtIndex = flow.findIndex(
|
||||
step => step.id === shortcutStep.stepId
|
||||
);
|
||||
@@ -589,30 +660,25 @@ const InAppTutorialOrchestrator = React.forwardRef<
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (shouldGoToStepAtIndex == null) return;
|
||||
if (shouldGoToStepAtIndex === null) return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
goToStep({ stepIndex: shouldGoToStepAtIndex, gatherData: true });
|
||||
},
|
||||
[currentStepIndex, project, goToStep, data, flow]
|
||||
[
|
||||
currentStepIndex,
|
||||
goToStep,
|
||||
data,
|
||||
flow,
|
||||
hasCurrentSceneObjectsCountIncreased,
|
||||
]
|
||||
);
|
||||
|
||||
const handleDomMutation = useDebounce(watchDomForNextStepTrigger, 200);
|
||||
|
||||
const goToNextStep = React.useCallback(
|
||||
() => {
|
||||
goToStep(currentStepIndex + 1);
|
||||
(gatherData?: boolean) => {
|
||||
goToStep({ stepIndex: currentStepIndex + 1, gatherData });
|
||||
},
|
||||
[currentStepIndex, goToStep]
|
||||
);
|
||||
@@ -687,6 +753,7 @@ 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) {
|
||||
@@ -696,6 +763,20 @@ 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(
|
||||
() => {
|
||||
@@ -726,6 +807,15 @@ const InAppTutorialOrchestrator = React.forwardRef<
|
||||
sceneName,
|
||||
count: nextStepTrigger.instancesCount,
|
||||
});
|
||||
} else if (
|
||||
nextStepTrigger &&
|
||||
(nextStepTrigger.objectAddedInLayout ||
|
||||
(nextStepTrigger.presenceOfElement &&
|
||||
nextStepTrigger.presenceOfElement.match(
|
||||
legacyItemInObjectListDomSelectorPattern
|
||||
)))
|
||||
) {
|
||||
setSceneObjectCountToWatch(true);
|
||||
}
|
||||
},
|
||||
[currentStep, data]
|
||||
@@ -819,6 +909,20 @@ 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,
|
||||
@@ -832,6 +936,7 @@ const InAppTutorialOrchestrator = React.forwardRef<
|
||||
watchSceneInstanceChanges,
|
||||
objectSceneInstancesToWatch ? 500 : null
|
||||
);
|
||||
useInterval(watchSceneObjects, sceneObjectCountToWatch ? 1000 : null);
|
||||
useInterval(
|
||||
watchDomForNextStepTrigger,
|
||||
currentStep && currentStep.isTriggerFlickering ? 500 : null
|
||||
|
@@ -1,7 +1,6 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import InAppTutorialContext from './InAppTutorialContext';
|
||||
import legacyOnboardingTutorial from './Tutorials/OnboardingTutorial';
|
||||
import { setCurrentlyRunningInAppTutorial } from '../Utils/Analytics/EventSender';
|
||||
import {
|
||||
fetchInAppTutorial,
|
||||
@@ -44,14 +43,6 @@ 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);
|
||||
|
@@ -1,482 +0,0 @@
|
||||
// @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`### You’ve built your first game! 😊`,
|
||||
},
|
||||
{
|
||||
messageDescriptor: t`You’re 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`Let’s 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`Let’s 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;
|
@@ -97,14 +97,12 @@ 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,
|
||||
|
@@ -78,13 +78,11 @@ 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({
|
||||
|
File diff suppressed because it is too large
Load Diff
117
newIDE/app/src/ObjectsList/EnumerateObjectFolderOrObject.js
Normal file
117
newIDE/app/src/ObjectsList/EnumerateObjectFolderOrObject.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// @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);
|
||||
};
|
@@ -1,7 +1,6 @@
|
||||
// @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;
|
||||
@@ -158,37 +157,22 @@ 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, selectedTags, hideExactMatches }: ObjectFilteringOptions
|
||||
{ searchText, hideExactMatches }: ObjectFilteringOptions
|
||||
): ObjectWithContextList => {
|
||||
if (!searchText && !selectedTags.length) return list;
|
||||
if (!searchText) return list;
|
||||
|
||||
return list
|
||||
.filter(objectWithContext =>
|
||||
filterObjectByTags(objectWithContext, selectedTags)
|
||||
)
|
||||
.filter((objectWithContext: ObjectWithContext) => {
|
||||
const objectName = objectWithContext.object.getName();
|
||||
return list.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
@@ -16,6 +16,7 @@ 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,
|
||||
@@ -31,8 +32,7 @@ export type SceneEditorsDisplayProps = {|
|
||||
editInstanceVariables: (instance: ?gdInitialInstance) => void,
|
||||
editObjectByName: (objectName: string, initialTab?: ObjectEditorTab) => void,
|
||||
onEditObject: gdObject => void,
|
||||
selectedObjectNames: string[],
|
||||
renamedObjectWithContext: ?ObjectWithContext,
|
||||
selectedObjectFolderOrObjectsWithContext: ObjectFolderOrObjectWithContext[],
|
||||
onSelectLayer: (layerName: string) => void,
|
||||
editLayerEffects: (layer: ?gdLayer) => void,
|
||||
editLayer: (layer: ?gdLayer) => void,
|
||||
@@ -43,7 +43,9 @@ export type SceneEditorsDisplayProps = {|
|
||||
done: (boolean) => void
|
||||
) => void,
|
||||
onObjectCreated: gdObject => void,
|
||||
onObjectSelected: (?ObjectWithContext) => void,
|
||||
onObjectFolderOrObjectWithContextSelected: (
|
||||
?ObjectFolderOrObjectWithContext
|
||||
) => void,
|
||||
onExportObject: (object: ?gdObject) => void,
|
||||
onDeleteObject: (
|
||||
i18n: I18nType,
|
||||
@@ -54,9 +56,8 @@ export type SceneEditorsDisplayProps = {|
|
||||
objectName: string,
|
||||
targetPosition?: 'center' | 'upperCenter'
|
||||
) => void,
|
||||
onRenameObjectStart: (?ObjectWithContext) => void,
|
||||
onRenameObjectFinish: (
|
||||
objectWithContext: ObjectWithContext,
|
||||
onRenameObjectFolderOrObjectWithContextFinish: (
|
||||
objectFolderOrObjectWithContext: ObjectFolderOrObjectWithContext,
|
||||
newName: string,
|
||||
done: (boolean) => void
|
||||
) => void,
|
||||
@@ -121,6 +122,7 @@ 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,
|
||||
|
@@ -3,7 +3,6 @@
|
||||
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';
|
||||
@@ -14,19 +13,14 @@ 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 from '../../ObjectGroupsList';
|
||||
import ObjectGroupsList, {
|
||||
type ObjectGroupsListInterface,
|
||||
} 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 {
|
||||
@@ -95,10 +89,6 @@ 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
|
||||
@@ -108,7 +98,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<?ObjectGroupsList>(null);
|
||||
const objectGroupsListRef = React.useRef<?ObjectGroupsListInterface>(null);
|
||||
|
||||
const forceUpdateInstancesPropertiesEditor = React.useCallback(() => {
|
||||
if (instancesPropertiesEditorRef.current)
|
||||
@@ -150,6 +140,15 @@ 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;
|
||||
@@ -163,6 +162,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
openNewObjectDialog,
|
||||
toggleEditorView,
|
||||
isEditorVisible,
|
||||
renameObjectFolderOrObjectWithContext,
|
||||
viewControls: {
|
||||
zoomBy: editor ? editor.zoomBy : noop,
|
||||
setZoomFactor: editor ? editor.setZoomFactor : noop,
|
||||
@@ -212,29 +212,13 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
]
|
||||
);
|
||||
|
||||
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 selectedObjectNames = props.selectedObjectFolderOrObjectsWithContext
|
||||
.map(objectFolderOrObjectWithContext => {
|
||||
const { objectFolderOrObject } = objectFolderOrObjectWithContext;
|
||||
if (objectFolderOrObject.isFolder()) return null;
|
||||
return objectFolderOrObject.getObject().getName();
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
const editors = {
|
||||
properties: {
|
||||
@@ -312,7 +296,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
onInstancesMoved={props.onInstancesMoved}
|
||||
onInstancesResized={props.onInstancesResized}
|
||||
onInstancesRotated={props.onInstancesRotated}
|
||||
selectedObjectNames={props.selectedObjectNames}
|
||||
selectedObjectNames={selectedObjectNames}
|
||||
onContextMenu={props.onContextMenu}
|
||||
isInstanceOf3DObject={props.isInstanceOf3DObject}
|
||||
instancesEditorShortcutsCallbacks={
|
||||
@@ -328,13 +312,7 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
'objects-list': {
|
||||
type: 'secondary',
|
||||
title: t`Objects`,
|
||||
toolbarControls: [
|
||||
<TagsButton
|
||||
key="tags"
|
||||
buildMenuTemplate={buildObjectTagsMenuTemplate}
|
||||
/>,
|
||||
<CloseButton key="close" />,
|
||||
],
|
||||
toolbarControls: [<CloseButton key="close" />],
|
||||
renderEditor: () => (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
@@ -350,7 +328,9 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
props.onSelectAllInstancesOfObjectInLayout
|
||||
}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
selectedObjectNames={props.selectedObjectNames}
|
||||
selectedObjectFolderOrObjectsWithContext={
|
||||
props.selectedObjectFolderOrObjectsWithContext
|
||||
}
|
||||
canInstallPrivateAsset={props.canInstallPrivateAsset}
|
||||
onEditObject={props.onEditObject}
|
||||
onExportObject={props.onExportObject}
|
||||
@@ -361,18 +341,17 @@ const MosaicEditorsDisplay = React.forwardRef<
|
||||
props.getValidatedObjectOrGroupName(newName, global, i18n)
|
||||
}
|
||||
onObjectCreated={props.onObjectCreated}
|
||||
onObjectSelected={props.onObjectSelected}
|
||||
renamedObjectWithContext={props.renamedObjectWithContext}
|
||||
onRenameObjectStart={props.onRenameObjectStart}
|
||||
onRenameObjectFinish={props.onRenameObjectFinish}
|
||||
onObjectFolderOrObjectWithContextSelected={
|
||||
props.onObjectFolderOrObjectWithContextSelected
|
||||
}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={
|
||||
props.onRenameObjectFolderOrObjectWithContextFinish
|
||||
}
|
||||
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}
|
||||
|
@@ -98,7 +98,7 @@ type Props = {|
|
||||
title: React.Node,
|
||||
openingState: DrawerOpeningState,
|
||||
setOpeningState: DrawerOpeningState => void,
|
||||
topBarControls: ?React.Node,
|
||||
topBarControls?: ?React.Node,
|
||||
|};
|
||||
|
||||
const SwipeableDrawer = (props: Props) => {
|
||||
|
@@ -1,27 +1,21 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { Trans } 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 from '../../ObjectGroupsList';
|
||||
import ObjectGroupsList, {
|
||||
type ObjectGroupsListInterface,
|
||||
} 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';
|
||||
@@ -64,10 +58,6 @@ 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();
|
||||
|
||||
@@ -78,7 +68,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<?ObjectGroupsList>(null);
|
||||
const objectGroupsListRef = React.useRef<?ObjectGroupsListInterface>(null);
|
||||
|
||||
const [selectedEditorId, setSelectedEditorId] = React.useState<?EditorId>(
|
||||
null
|
||||
@@ -136,6 +126,15 @@ 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;
|
||||
@@ -150,6 +149,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
openNewObjectDialog,
|
||||
toggleEditorView: halfOpenOrCloseDrawerOnEditor,
|
||||
isEditorVisible,
|
||||
renameObjectFolderOrObjectWithContext,
|
||||
viewControls: {
|
||||
zoomBy: editor ? editor.zoomBy : noop,
|
||||
setZoomFactor: editor ? editor.setZoomFactor : noop,
|
||||
@@ -199,29 +199,13 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
]
|
||||
);
|
||||
|
||||
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 selectedObjectNames = props.selectedObjectFolderOrObjectsWithContext
|
||||
.map(objectFolderOrObjectWithContext => {
|
||||
const { objectFolderOrObject } = objectFolderOrObjectWithContext;
|
||||
if (objectFolderOrObject.isFolder()) return null;
|
||||
return objectFolderOrObject.getObject().getName();
|
||||
})
|
||||
.filter(Boolean);
|
||||
|
||||
return (
|
||||
<FullSizeMeasurer>
|
||||
@@ -247,7 +231,7 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
onInstancesMoved={props.onInstancesMoved}
|
||||
onInstancesResized={props.onInstancesResized}
|
||||
onInstancesRotated={props.onInstancesRotated}
|
||||
selectedObjectNames={props.selectedObjectNames}
|
||||
selectedObjectNames={selectedObjectNames}
|
||||
onContextMenu={props.onContextMenu}
|
||||
isInstanceOf3DObject={props.isInstanceOf3DObject}
|
||||
instancesEditorShortcutsCallbacks={
|
||||
@@ -264,17 +248,6 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
}
|
||||
openingState={drawerOpeningState}
|
||||
setOpeningState={setDrawerOpeningState}
|
||||
topBarControls={
|
||||
selectedEditorId === 'objects-list'
|
||||
? [
|
||||
<TagsButton
|
||||
key="tags"
|
||||
size="small"
|
||||
buildMenuTemplate={buildObjectTagsMenuTemplate}
|
||||
/>,
|
||||
]
|
||||
: null
|
||||
}
|
||||
>
|
||||
{selectedEditorId === 'objects-list' && (
|
||||
<I18n>
|
||||
@@ -291,7 +264,9 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
props.onSelectAllInstancesOfObjectInLayout
|
||||
}
|
||||
resourceManagementProps={props.resourceManagementProps}
|
||||
selectedObjectNames={props.selectedObjectNames}
|
||||
selectedObjectFolderOrObjectsWithContext={
|
||||
props.selectedObjectFolderOrObjectsWithContext
|
||||
}
|
||||
canInstallPrivateAsset={props.canInstallPrivateAsset}
|
||||
onEditObject={props.onEditObject}
|
||||
onExportObject={props.onExportObject}
|
||||
@@ -306,20 +281,19 @@ const SwipeableDrawerEditorsDisplay = React.forwardRef<
|
||||
)
|
||||
}
|
||||
onObjectCreated={props.onObjectCreated}
|
||||
onObjectSelected={props.onObjectSelected}
|
||||
renamedObjectWithContext={props.renamedObjectWithContext}
|
||||
onRenameObjectStart={props.onRenameObjectStart}
|
||||
onRenameObjectFinish={props.onRenameObjectFinish}
|
||||
onObjectFolderOrObjectWithContextSelected={
|
||||
props.onObjectFolderOrObjectWithContextSelected
|
||||
}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={
|
||||
props.onRenameObjectFolderOrObjectWithContextFinish
|
||||
}
|
||||
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={
|
||||
|
@@ -5,7 +5,6 @@ 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';
|
||||
@@ -43,10 +42,8 @@ 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';
|
||||
@@ -62,6 +59,11 @@ 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;
|
||||
|
||||
@@ -130,11 +132,7 @@ type State = {|
|
||||
showAdditionalWorkInfoBar: boolean,
|
||||
additionalWorkInfoBar: InfoBarDetails,
|
||||
|
||||
// State for tags of objects:
|
||||
selectedObjectTags: SelectedTags,
|
||||
|
||||
renamedObjectWithContext: ?ObjectWithContext,
|
||||
selectedObjectsWithContext: Array<ObjectWithContext>,
|
||||
selectedObjectFolderOrObjectsWithContext: Array<ObjectFolderOrObjectWithContext>,
|
||||
selectedLayer: string,
|
||||
|};
|
||||
|
||||
@@ -187,10 +185,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
touchScreenMessage: '',
|
||||
},
|
||||
|
||||
selectedObjectTags: [],
|
||||
|
||||
renamedObjectWithContext: null,
|
||||
selectedObjectsWithContext: [],
|
||||
selectedObjectFolderOrObjectsWithContext: [],
|
||||
selectedLayer: BASE_LAYER_NAME,
|
||||
invisibleLayerOnWhichInstancesHaveJustBeenAdded: null,
|
||||
};
|
||||
@@ -242,7 +237,9 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
redo={this.redo}
|
||||
onOpenSettings={this.openSceneProperties}
|
||||
settingsIcon={editSceneIconReactNode}
|
||||
canRenameObject={this.state.selectedObjectsWithContext.length === 1}
|
||||
canRenameObject={
|
||||
this.state.selectedObjectFolderOrObjectsWithContext.length === 1
|
||||
}
|
||||
onRenameObject={this._startRenamingSelectedObject}
|
||||
/>
|
||||
);
|
||||
@@ -271,7 +268,9 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
redo={this.redo}
|
||||
onOpenSettings={this.openSceneProperties}
|
||||
settingsIcon={editSceneIconReactNode}
|
||||
canRenameObject={this.state.selectedObjectsWithContext.length === 1}
|
||||
canRenameObject={
|
||||
this.state.selectedObjectFolderOrObjectsWithContext.length === 1
|
||||
}
|
||||
onRenameObject={this._startRenamingSelectedObject}
|
||||
/>
|
||||
);
|
||||
@@ -466,15 +465,19 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
);
|
||||
};
|
||||
|
||||
_onObjectSelected = (objectWithContext: ?ObjectWithContext = null) => {
|
||||
const selectedObjectsWithContext = [];
|
||||
if (objectWithContext) {
|
||||
selectedObjectsWithContext.push(objectWithContext);
|
||||
_onObjectFolderOrObjectWithContextSelected = (
|
||||
objectFolderOrObjectWithContext: ?ObjectFolderOrObjectWithContext = null
|
||||
) => {
|
||||
const selectedObjectFolderOrObjectsWithContext = [];
|
||||
if (objectFolderOrObjectWithContext) {
|
||||
selectedObjectFolderOrObjectsWithContext.push(
|
||||
objectFolderOrObjectWithContext
|
||||
);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
selectedObjectsWithContext,
|
||||
selectedObjectFolderOrObjectsWithContext,
|
||||
},
|
||||
() => {
|
||||
// We update the toolbar because we need to update the objects selected
|
||||
@@ -574,23 +577,38 @@ 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;
|
||||
const instancesObjectNames = uniq(
|
||||
instances.map(instance => instance.getObjectName())
|
||||
);
|
||||
|
||||
const selectedObjectsWithContext = enumerateObjects(project, layout, {
|
||||
names: instancesObjectNames,
|
||||
}).allObjectsList;
|
||||
|
||||
this.setState(
|
||||
{
|
||||
selectedObjectsWithContext,
|
||||
},
|
||||
() => {
|
||||
this.updateToolbar();
|
||||
}
|
||||
);
|
||||
// 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,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
_onInstanceDoubleClicked = (instance: gdInitialInstance) => {
|
||||
@@ -750,25 +768,16 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
});
|
||||
};
|
||||
|
||||
_onRenameObjectStart = (objectWithContext: ?ObjectWithContext) => {
|
||||
const selectedObjectsWithContext = [];
|
||||
if (objectWithContext) {
|
||||
selectedObjectsWithContext.push(objectWithContext);
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
renamedObjectWithContext: objectWithContext,
|
||||
selectedObjectsWithContext,
|
||||
},
|
||||
() => {
|
||||
this.updateToolbar();
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
_startRenamingSelectedObject = () => {
|
||||
this._onRenameObjectStart(this.state.selectedObjectsWithContext[0]);
|
||||
const firstSelectedObjectFolderOrObject = this.state
|
||||
.selectedObjectFolderOrObjectsWithContext[0];
|
||||
if (!firstSelectedObjectFolderOrObject) return;
|
||||
|
||||
if (this.editorDisplay)
|
||||
this.editorDisplay.renameObjectFolderOrObjectWithContext(
|
||||
firstSelectedObjectFolderOrObject
|
||||
);
|
||||
this.updateToolbar();
|
||||
};
|
||||
|
||||
_onRenameLayer = (
|
||||
@@ -884,42 +893,75 @@ 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,
|
||||
done: boolean => void
|
||||
newName: string
|
||||
) => {
|
||||
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) {
|
||||
if (global) {
|
||||
gd.WholeProjectRefactorer.globalObjectOrGroupRenamed(
|
||||
project,
|
||||
object.getName(),
|
||||
newName,
|
||||
/* isObjectGroup=*/ false
|
||||
);
|
||||
} else {
|
||||
gd.WholeProjectRefactorer.objectOrGroupRenamedInLayout(
|
||||
project,
|
||||
layout,
|
||||
object.getName(),
|
||||
newName,
|
||||
/* isObjectGroup=*/ false
|
||||
);
|
||||
}
|
||||
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
|
||||
);
|
||||
}
|
||||
|
||||
object.setName(newName);
|
||||
this._onObjectSelected(objectWithContext);
|
||||
};
|
||||
|
||||
_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
|
||||
);
|
||||
done(true);
|
||||
};
|
||||
|
||||
@@ -1076,7 +1118,7 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
|
||||
this.setState(
|
||||
{
|
||||
selectedObjectsWithContext: [],
|
||||
selectedObjectFolderOrObjectsWithContext: [],
|
||||
history: saveToHistory(
|
||||
this.state.history,
|
||||
this.props.initialInstances,
|
||||
@@ -1136,6 +1178,76 @@ 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);
|
||||
@@ -1170,13 +1282,11 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
};
|
||||
|
||||
buildContextMenu = (i18n: I18nType, layout: gdLayout, options: any) => {
|
||||
let contextMenuItems = [];
|
||||
if (
|
||||
options.ignoreSelectedObjectsForContextMenu ||
|
||||
this.state.selectedObjectsWithContext.length === 0
|
||||
!this.instancesSelection.hasSelectedInstances()
|
||||
) {
|
||||
contextMenuItems = [
|
||||
...contextMenuItems,
|
||||
return [
|
||||
{
|
||||
label: i18n._(t`Paste`),
|
||||
click: () => this.paste(),
|
||||
@@ -1190,64 +1300,28 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
},
|
||||
{ type: 'separator' },
|
||||
...this.getContextMenuZoomItems(i18n),
|
||||
{ type: 'separator' },
|
||||
...this.getContextMenuLayoutItems(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',
|
||||
},
|
||||
}
|
||||
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),
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Edit object ${shortenString(objectName, 14)}`),
|
||||
@@ -1261,27 +1335,24 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
label: i18n._(t`Edit behaviors`),
|
||||
click: () => this.editObjectByName(objectName, 'behaviors'),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Edit effects`),
|
||||
click: () => this.editObjectByName(objectName, 'effects'),
|
||||
},
|
||||
];
|
||||
objectMetadata
|
||||
? {
|
||||
label: i18n._(t`Edit effects`),
|
||||
click: () => this.editObjectByName(objectName, 'effects'),
|
||||
enabled: objectMetadata.hasDefaultBehavior(
|
||||
'EffectCapability::EffectBehavior'
|
||||
),
|
||||
}
|
||||
: null,
|
||||
{ type: 'separator' },
|
||||
...this.getContextMenuLayoutItems(i18n),
|
||||
].filter(Boolean);
|
||||
}
|
||||
|
||||
contextMenuItems = [
|
||||
...contextMenuItems,
|
||||
return [
|
||||
...this.getContextMenuInstancesWiseItems(i18n),
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Open scene events`),
|
||||
click: () => this.props.onOpenEvents(layout.getName()),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Open scene properties`),
|
||||
click: () => this.openSceneProperties(true),
|
||||
},
|
||||
...this.getContextMenuLayoutItems(i18n),
|
||||
];
|
||||
|
||||
return contextMenuItems;
|
||||
};
|
||||
|
||||
copySelection = ({
|
||||
@@ -1464,7 +1535,10 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
resourceManagementProps,
|
||||
isActive,
|
||||
} = this.props;
|
||||
const { editedObjectWithContext } = this.state;
|
||||
const {
|
||||
editedObjectWithContext,
|
||||
selectedObjectFolderOrObjectsWithContext,
|
||||
} = this.state;
|
||||
const variablesEditedAssociatedObjectName = this.state
|
||||
.variablesEditedInstance
|
||||
? this.state.variablesEditedInstance.getObjectName()
|
||||
@@ -1472,9 +1546,7 @@ 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
|
||||
@@ -1527,8 +1599,9 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
editLayerEffects={this.editLayerEffects}
|
||||
editInstanceVariables={this.editInstanceVariables}
|
||||
editObjectByName={this.editObjectByName}
|
||||
selectedObjectNames={selectedObjectNames}
|
||||
renamedObjectWithContext={this.state.renamedObjectWithContext}
|
||||
selectedObjectFolderOrObjectsWithContext={
|
||||
selectedObjectFolderOrObjectsWithContext
|
||||
}
|
||||
onRenameLayer={this._onRenameLayer}
|
||||
onRemoveLayer={this._onRemoveLayer}
|
||||
onSelectLayer={(layer: string) =>
|
||||
@@ -1545,10 +1618,13 @@ export default class SceneEditor extends React.Component<Props, State> {
|
||||
canObjectOrGroupBeGlobal={this.canObjectOrGroupBeGlobal}
|
||||
updateBehaviorsSharedData={this.updateBehaviorsSharedData}
|
||||
onEditObject={this.props.onEditObject || this.editObject}
|
||||
onRenameObjectStart={this._onRenameObjectStart}
|
||||
onRenameObjectFinish={this._onRenameObjectFinish}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={
|
||||
this._onRenameObjectFolderOrObjectWithContextFinish
|
||||
}
|
||||
onObjectCreated={this._onObjectCreated}
|
||||
onObjectSelected={this._onObjectSelected}
|
||||
onObjectFolderOrObjectWithContextSelected={
|
||||
this._onObjectFolderOrObjectWithContextSelected
|
||||
}
|
||||
canInstallPrivateAsset={this.props.canInstallPrivateAsset}
|
||||
historyHandler={{
|
||||
undo: this.undo,
|
||||
|
@@ -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,16 +44,31 @@ 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);
|
||||
|
@@ -8,7 +8,9 @@ import {
|
||||
type ShowAlertDialogOptionsWithCallback,
|
||||
type ShowConfirmDeleteDialogOptionsWithCallback,
|
||||
type ShowConfirmDialogOptionsWithCallback,
|
||||
type ShowYesNoCancelDialogOptionsWithCallback,
|
||||
} from './AlertContext';
|
||||
import YesNoCancelDialog from './YesNoCancelDialog';
|
||||
|
||||
type Props = {| children: React.Node |};
|
||||
|
||||
@@ -60,12 +62,30 @@ 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}
|
||||
@@ -121,6 +141,28 @@ 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>
|
||||
);
|
||||
}
|
||||
|
@@ -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 = textInput === props.confirmText;
|
||||
const canConfirm = props.confirmText ? textInput === props.confirmText : true;
|
||||
|
||||
const onConfirm = () => {
|
||||
if (!canConfirm) return;
|
||||
@@ -75,15 +75,19 @@ function ConfirmDeleteDialog(props: Props) {
|
||||
<Text size="body" style={{ userSelect: 'text' }}>
|
||||
{i18n._(props.message)}
|
||||
</Text>
|
||||
<LargeSpacer />
|
||||
<TextField
|
||||
autoFocus="desktop"
|
||||
floatingLabelFixed
|
||||
floatingLabelText={i18n._(props.fieldMessage)}
|
||||
value={textInput}
|
||||
onChange={(e, text) => setTextInput(text)}
|
||||
hintText={props.confirmText}
|
||||
/>
|
||||
{props.confirmText && props.fieldMessage && (
|
||||
<>
|
||||
<LargeSpacer />
|
||||
<TextField
|
||||
autoFocus="desktop"
|
||||
floatingLabelFixed
|
||||
floatingLabelText={i18n._(props.fieldMessage)}
|
||||
value={textInput}
|
||||
onChange={(e, text) => setTextInput(text)}
|
||||
hintText={props.confirmText}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Dialog>
|
||||
)}
|
||||
</I18n>
|
||||
|
80
newIDE/app/src/UI/Alert/YesNoCancelDialog.js
Normal file
80
newIDE/app/src/UI/Alert/YesNoCancelDialog.js
Normal file
@@ -0,0 +1,80 @@
|
||||
// @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;
|
@@ -5,6 +5,7 @@ import {
|
||||
type ShowAlertDialogOptions,
|
||||
type ShowConfirmDeleteDialogOptions,
|
||||
type ShowConfirmDialogOptions,
|
||||
type ShowYesNoCancelDialogOptions,
|
||||
} from './AlertContext';
|
||||
|
||||
const useAlertDialog = () => {
|
||||
@@ -12,6 +13,7 @@ const useAlertDialog = () => {
|
||||
showAlertDialog,
|
||||
showConfirmDialog,
|
||||
showConfirmDeleteDialog,
|
||||
showYesNoCancelDialog,
|
||||
} = React.useContext(AlertContext);
|
||||
|
||||
const showAlert = React.useCallback(
|
||||
@@ -38,10 +40,26 @@ 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,
|
||||
};
|
||||
};
|
||||
|
||||
|
31
newIDE/app/src/UI/CustomSvgIcons/AddFolder.js
Normal file
31
newIDE/app/src/UI/CustomSvgIcons/AddFolder.js
Normal file
@@ -0,0 +1,31 @@
|
||||
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>
|
||||
));
|
11
newIDE/app/src/UI/CustomSvgIcons/ArrowHeadBottom.js
Normal file
11
newIDE/app/src/UI/CustomSvgIcons/ArrowHeadBottom.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
));
|
11
newIDE/app/src/UI/CustomSvgIcons/ArrowHeadRight.js
Normal file
11
newIDE/app/src/UI/CustomSvgIcons/ArrowHeadRight.js
Normal file
@@ -0,0 +1,11 @@
|
||||
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>
|
||||
));
|
@@ -1,8 +1,24 @@
|
||||
import { Component } from 'react';
|
||||
import MultiBackend from 'react-dnd-multi-backend';
|
||||
import HTML5toTouch from 'react-dnd-multi-backend/lib/HTML5toTouch';
|
||||
import HTML5Backend from 'react-dnd-html5-backend';
|
||||
import TouchBackend from 'react-dnd-touch-backend';
|
||||
import MultiBackend, { TouchTransition } from 'react-dnd-multi-backend';
|
||||
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;
|
||||
|
@@ -11,6 +11,7 @@ import {
|
||||
type ConnectDropTarget,
|
||||
type ConnectDragPreview,
|
||||
} from 'react-dnd';
|
||||
import { hapticFeedback } from '../../Utils/Haptic';
|
||||
|
||||
type Props<DraggedItemType> = {|
|
||||
children: ({|
|
||||
@@ -26,6 +27,7 @@ type Props<DraggedItemType> = {|
|
||||
canDrop: (item: DraggedItemType) => boolean,
|
||||
drop: () => void,
|
||||
endDrag?: () => void,
|
||||
hover?: (monitor: DropTargetMonitor) => void,
|
||||
|};
|
||||
|
||||
type DragSourceProps = {|
|
||||
@@ -47,8 +49,11 @@ type InnerDragSourceAndDropTargetProps<DraggedItemType> = {|
|
||||
...DropTargetProps,
|
||||
|};
|
||||
|
||||
type Options = {| vibrate?: number |};
|
||||
|
||||
export const makeDragSourceAndDropTarget = <DraggedItemType>(
|
||||
reactDndType: string
|
||||
reactDndType: string,
|
||||
options: ?Options
|
||||
): ((Props<DraggedItemType>) => React.Node) => {
|
||||
const sourceSpec = {
|
||||
canDrag(props: Props<DraggedItemType>, monitor: DragSourceMonitor) {
|
||||
@@ -58,6 +63,9 @@ 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) {
|
||||
@@ -87,6 +95,9 @@ export const makeDragSourceAndDropTarget = <DraggedItemType>(
|
||||
}
|
||||
props.drop();
|
||||
},
|
||||
hover(props: Props<DraggedItemType>, monitor: DropTargetMonitor) {
|
||||
if (props.hover) props.hover(monitor);
|
||||
},
|
||||
};
|
||||
|
||||
function targetCollect(
|
||||
|
@@ -1,106 +0,0 @@
|
||||
// @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>
|
||||
);
|
||||
}
|
||||
}
|
@@ -1,48 +0,0 @@
|
||||
// @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}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -32,22 +32,22 @@ const ESC_KEY = 27;
|
||||
const MID_MOUSE_BUTTON = 1;
|
||||
|
||||
type ShortcutCallbacks = {|
|
||||
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,
|
||||
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>,
|
||||
|};
|
||||
|
||||
type ConstructorArgs = {|
|
||||
@@ -81,6 +81,13 @@ export default class KeyboardShortcuts {
|
||||
this._isActive = isActive;
|
||||
}
|
||||
|
||||
setShortcutCallback(
|
||||
key: $Keys<ShortcutCallbacks>,
|
||||
callback: () => void | Promise<void>
|
||||
) {
|
||||
this._shortcutCallbacks[key] = callback;
|
||||
}
|
||||
|
||||
shouldCloneInstances() {
|
||||
return this._isControlOrCmdPressed();
|
||||
}
|
||||
|
@@ -104,6 +104,7 @@ type ListItemProps = {|
|
||||
backgroundColor?: string,
|
||||
borderBottom?: string,
|
||||
opacity?: number,
|
||||
paddingLeft?: number,
|
||||
|},
|
||||
|
||||
leftIcon?: React.Node,
|
||||
@@ -115,7 +116,7 @@ type ListItemProps = {|
|
||||
data?: HTMLDataset,
|
||||
|};
|
||||
|
||||
export type ListItemRefType = any; // Should be a material-ui ListIten
|
||||
export type ListItemRefType = any; // Should be a material-ui ListItem
|
||||
|
||||
/**
|
||||
* A ListItem to be used in a List.
|
||||
|
@@ -14,6 +14,7 @@ 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,
|
||||
@@ -91,6 +92,8 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
|
||||
textField.current.blur();
|
||||
}
|
||||
};
|
||||
const windowWidth = useResponsiveWindowWidth();
|
||||
const isMobile = windowWidth === 'small';
|
||||
|
||||
const [isInputFocused, setIsInputFocused] = React.useState(false);
|
||||
|
||||
@@ -175,7 +178,7 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
|
||||
|
||||
const handleCancel = () => {
|
||||
changeValueImmediately('');
|
||||
focus();
|
||||
if (!isMobile) focus();
|
||||
};
|
||||
|
||||
const handleKeyPressed = (event: SyntheticKeyboardEvent<>) => {
|
||||
|
@@ -1,7 +1,8 @@
|
||||
// @flow
|
||||
import React from 'react';
|
||||
import Chip from '../UI/Chip';
|
||||
import { type Tags, removeTag } from '../Utils/TagsHelper';
|
||||
|
||||
type Tags = Array<string>;
|
||||
|
||||
const styles = {
|
||||
chipContainer: {
|
||||
@@ -19,11 +20,10 @@ const styles = {
|
||||
|
||||
type Props = {|
|
||||
tags: Tags,
|
||||
onChange?: Tags => void,
|
||||
onRemove?: string => void,
|
||||
onRemove: string => void,
|
||||
|};
|
||||
|
||||
const TagChips = ({ tags, onChange, onRemove }: Props) => {
|
||||
const TagChips = ({ tags, onRemove }: Props) => {
|
||||
const [focusedTag, setFocusedTag] = React.useState<?string>(null);
|
||||
const tagsRefs = React.useRef([]);
|
||||
|
||||
@@ -51,8 +51,7 @@ const TagChips = ({ tags, onChange, onRemove }: Props) => {
|
||||
newTagToFocus.current.focus();
|
||||
}
|
||||
}
|
||||
if (onChange) onChange(removeTag(tags, tag));
|
||||
else if (onRemove) onRemove(tag);
|
||||
onRemove(tag);
|
||||
};
|
||||
|
||||
if (!tags.length) return null;
|
||||
@@ -69,7 +68,7 @@ const TagChips = ({ tags, onChange, onRemove }: Props) => {
|
||||
style={getChipStyle(tag)}
|
||||
onBlur={() => setFocusedTag(null)}
|
||||
onFocus={() => setFocusedTag(tag)}
|
||||
onDelete={onChange || onRemove ? handleDeleteTag(tag) : null}
|
||||
onDelete={handleDeleteTag(tag)}
|
||||
label={tag}
|
||||
ref={newRef}
|
||||
/>
|
||||
|
@@ -11,6 +11,7 @@ export function getRootClassNames(theme: string) {
|
||||
return {
|
||||
mosaicRootClassName: theme,
|
||||
eventsSheetRootClassName: theme,
|
||||
treeViewRootClassName: theme,
|
||||
tableRootClassName: theme,
|
||||
markdownRootClassName: theme,
|
||||
uiRootClassName: theme,
|
||||
|
126
newIDE/app/src/UI/TreeView/TreeView.css
Normal file
126
newIDE/app/src/UI/TreeView/TreeView.css
Normal file
@@ -0,0 +1,126 @@
|
||||
.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;
|
||||
}
|
461
newIDE/app/src/UI/TreeView/TreeViewRow.js
Normal file
461
newIDE/app/src/UI/TreeView/TreeViewRow.js
Normal file
@@ -0,0 +1,461 @@
|
||||
// @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);
|
613
newIDE/app/src/UI/TreeView/index.js
Normal file
613
newIDE/app/src/UI/TreeView/index.js
Normal file
@@ -0,0 +1,613 @@
|
||||
// @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);
|
@@ -57,6 +57,9 @@ export type TranslatedText =
|
||||
type InAppTutorialFlowStepDOMChangeTrigger =
|
||||
| {| presenceOfElement: string |}
|
||||
| {| absenceOfElement: string |};
|
||||
type InAppTutorialFlowStepShortcutTrigger =
|
||||
| InAppTutorialFlowStepDOMChangeTrigger
|
||||
| {| objectAddedInLayout: true |};
|
||||
|
||||
export type InAppTutorialFlowStepTrigger =
|
||||
| InAppTutorialFlowStepDOMChangeTrigger
|
||||
@@ -64,6 +67,7 @@ export type InAppTutorialFlowStepTrigger =
|
||||
| {| valueHasChanged: true |}
|
||||
| {| valueEquals: string |}
|
||||
| {| instanceAddedOnScene: string, instancesCount?: number |}
|
||||
| {| objectAddedInLayout: true |}
|
||||
| {| previewLaunched: true |}
|
||||
| {| clickOnTooltipButton: TranslatedText |};
|
||||
|
||||
@@ -72,6 +76,7 @@ export type InAppTutorialFlowStepFormattedTrigger =
|
||||
| {| valueEquals: string |}
|
||||
| {| valueHasChanged: true |}
|
||||
| {| instanceAddedOnScene: string, instancesCount?: number |}
|
||||
| {| objectAddedInLayout: true |}
|
||||
| {| previewLaunched: true |}
|
||||
| {| clickOnTooltipButton: string |};
|
||||
|
||||
@@ -107,7 +112,7 @@ export type InAppTutorialFlowStep = {|
|
||||
shortcuts?: Array<{|
|
||||
stepId: string,
|
||||
// TODO: Adapt provider to make it possible to use other triggers as shortcuts
|
||||
trigger: InAppTutorialFlowStepDOMChangeTrigger,
|
||||
trigger: InAppTutorialFlowStepShortcutTrigger,
|
||||
|}>,
|
||||
dialog?: InAppTutorialDialog,
|
||||
mapProjectData?: {
|
||||
|
17
newIDE/app/src/Utils/Haptic.js
Normal file
17
newIDE/app/src/Utils/Haptic.js
Normal file
@@ -0,0 +1,17 @@
|
||||
// @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;
|
@@ -1,89 +0,0 @@
|
||||
// @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;
|
||||
};
|
@@ -23,7 +23,7 @@ const getClientXY = (event: TouchEvent): CallbackEvent => {
|
||||
};
|
||||
};
|
||||
|
||||
const delay = 600; // ms
|
||||
const defaultDelay = 600; // ms
|
||||
const moveTolerance = 10; // px
|
||||
|
||||
const contextLocks: { [string]: true } = {};
|
||||
@@ -37,13 +37,18 @@ const contextLocks: { [string]: true } = {};
|
||||
*/
|
||||
export const useLongTouch = (
|
||||
callback: (e: CallbackEvent) => void,
|
||||
/**
|
||||
* 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
|
||||
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,
|
||||
}
|
||||
) => {
|
||||
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,
|
||||
@@ -98,7 +103,7 @@ export const useLongTouch = (
|
||||
callback(currentTouchCallbackEvent.current);
|
||||
}, delay);
|
||||
},
|
||||
[callback, context]
|
||||
[callback, context, delay]
|
||||
);
|
||||
|
||||
const onMove = React.useCallback(
|
||||
|
@@ -346,10 +346,6 @@ 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');
|
||||
|
@@ -16,6 +16,7 @@ export const Default = () => {
|
||||
showAlert,
|
||||
showConfirmation,
|
||||
showDeleteConfirmation,
|
||||
showYesNoCancel,
|
||||
} = useAlertDialog();
|
||||
|
||||
const onOpenAlertDialog = async () => {
|
||||
@@ -26,6 +27,20 @@ export const Default = () => {
|
||||
action('Dismissed')();
|
||||
};
|
||||
|
||||
const onOpenYesNoCancelDialog = async () => {
|
||||
const answer = await showYesNoCancel({
|
||||
title: t`Warning`,
|
||||
message: t`Do you want to refactor your project?`,
|
||||
});
|
||||
if (answer === 0) {
|
||||
action('Yes')();
|
||||
} else if (answer === 1) {
|
||||
action('No')();
|
||||
} else {
|
||||
action('Cancel')();
|
||||
}
|
||||
};
|
||||
|
||||
const onOpenConfirmDialog = async () => {
|
||||
const answer = await showConfirmation({
|
||||
title: t`You are about to delete an object`,
|
||||
@@ -48,6 +63,16 @@ export const Default = () => {
|
||||
else action('Delete Dismissed')();
|
||||
};
|
||||
|
||||
const onOpenConfirmDeleteWithoutConfirmTextDialog = async () => {
|
||||
const answer = await showDeleteConfirmation({
|
||||
title: t`Do you really want to permanently delete your account?`,
|
||||
message: t`You’re about to permanently delete your GDevelop account username@mail.com. You will no longer be able to log into the app with this email address.`,
|
||||
confirmButtonLabel: t`Delete my account`,
|
||||
});
|
||||
if (answer) action('Delete Confirmed')();
|
||||
else action('Delete Dismissed')();
|
||||
};
|
||||
|
||||
return (
|
||||
<Column alignItems="flex-start">
|
||||
<RaisedButton label="Open alert dialog" onClick={onOpenAlertDialog} />
|
||||
@@ -58,6 +83,16 @@ export const Default = () => {
|
||||
label="Open confirm delete dialog"
|
||||
onClick={onOpenConfirmDeleteDialog}
|
||||
/>
|
||||
<LargeSpacer />
|
||||
<RaisedButton
|
||||
label="Open confirm delete dialog without confirm text"
|
||||
onClick={onOpenConfirmDeleteWithoutConfirmTextDialog}
|
||||
/>
|
||||
<LargeSpacer />
|
||||
<RaisedButton
|
||||
label="Open yes no cancel dialog"
|
||||
onClick={onOpenYesNoCancelDialog}
|
||||
/>
|
||||
</Column>
|
||||
);
|
||||
};
|
||||
|
@@ -268,19 +268,16 @@ export const WithObjectsList = () => (
|
||||
onEditObject={action('On edit object')}
|
||||
onExportObject={action('On export object')}
|
||||
onAddObjectInstance={action('On add instance to the scene')}
|
||||
selectedObjectNames={[]}
|
||||
selectedObjectTags={[]}
|
||||
onChangeSelectedObjectTags={() => {}}
|
||||
getAllObjectTags={() => []}
|
||||
selectedObjectFolderOrObjectsWithContext={[]}
|
||||
getValidatedObjectOrGroupName={newName => newName}
|
||||
onDeleteObject={(objectWithContext, cb) => cb(true)}
|
||||
onRenameObjectStart={() => {}}
|
||||
onRenameObjectFinish={(objectWithContext, newName, cb) =>
|
||||
cb(true)
|
||||
}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={(
|
||||
objectFolderOrObjectWithContext,
|
||||
newName,
|
||||
cb
|
||||
) => cb(true)}
|
||||
onObjectCreated={() => {}}
|
||||
onObjectSelected={() => {}}
|
||||
renamedObjectWithContext={null}
|
||||
onObjectFolderOrObjectWithContextSelected={() => {}}
|
||||
hotReloadPreviewButtonProps={hotReloadPreviewButtonProps}
|
||||
canInstallPrivateAsset={() => false}
|
||||
/>
|
||||
|
@@ -1,12 +1,14 @@
|
||||
// @flow
|
||||
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
// Keep first as it creates the `global.gd` object:
|
||||
import { testProject } from '../../GDevelopJsInitializerDecorator';
|
||||
|
||||
import muiDecorator from '../../ThemeDecorator';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
import alertDecorator from '../../AlertDecorator';
|
||||
import ObjectGroupsList from '../../../ObjectGroupsList';
|
||||
import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider';
|
||||
import SerializedObjectDisplay from '../../SerializedObjectDisplay';
|
||||
@@ -14,7 +16,7 @@ import SerializedObjectDisplay from '../../SerializedObjectDisplay';
|
||||
export default {
|
||||
title: 'LayoutEditor/ObjectGroupsList',
|
||||
component: ObjectGroupsList,
|
||||
decorators: [paperDecorator, muiDecorator],
|
||||
decorators: [alertDecorator, paperDecorator, muiDecorator],
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
@@ -24,7 +26,9 @@ export const Default = () => (
|
||||
<ObjectGroupsList
|
||||
globalObjectGroups={testProject.project.getObjectGroups()}
|
||||
objectGroups={testProject.testLayout.getObjectGroups()}
|
||||
onEditGroup={() => {}}
|
||||
onEditGroup={action('onEditGroup')}
|
||||
onRenameGroup={action('onRenameGroup')}
|
||||
onDeleteGroup={action('onDeleteGroup')}
|
||||
getValidatedObjectOrGroupName={newName => newName}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -9,6 +9,7 @@ import { testProject } from '../../GDevelopJsInitializerDecorator';
|
||||
import fakeHotReloadPreviewButtonProps from '../../FakeHotReloadPreviewButtonProps';
|
||||
import muiDecorator from '../../ThemeDecorator';
|
||||
import paperDecorator from '../../PaperDecorator';
|
||||
import alertDecorator from '../../AlertDecorator';
|
||||
import ObjectsList from '../../../ObjectsList';
|
||||
import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider';
|
||||
import SerializedObjectDisplay from '../../SerializedObjectDisplay';
|
||||
@@ -17,7 +18,7 @@ import fakeResourceManagementProps from '../../FakeResourceManagement';
|
||||
export default {
|
||||
title: 'LayoutEditor/ObjectsList',
|
||||
component: ObjectsList,
|
||||
decorators: [paperDecorator, muiDecorator],
|
||||
decorators: [alertDecorator, paperDecorator, muiDecorator],
|
||||
};
|
||||
|
||||
export const Default = () => (
|
||||
@@ -33,16 +34,15 @@ export const Default = () => (
|
||||
onExportObject={action('On export object')}
|
||||
onAddObjectInstance={action('On add instance to the scene')}
|
||||
onObjectCreated={action('On object created')}
|
||||
selectedObjectNames={[]}
|
||||
selectedObjectTags={[]}
|
||||
onChangeSelectedObjectTags={selectedObjectTags => {}}
|
||||
getAllObjectTags={() => []}
|
||||
selectedObjectFolderOrObjectsWithContext={[]}
|
||||
getValidatedObjectOrGroupName={newName => newName}
|
||||
onDeleteObject={(objectWithContext, cb) => cb(true)}
|
||||
onRenameObjectStart={() => {}}
|
||||
onRenameObjectFinish={(objectWithContext, newName, cb) => cb(true)}
|
||||
onObjectSelected={() => {}}
|
||||
renamedObjectWithContext={null}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={(
|
||||
objectWithContext,
|
||||
newName,
|
||||
cb
|
||||
) => cb(true)}
|
||||
onObjectFolderOrObjectWithContextSelected={() => {}}
|
||||
hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
|
||||
canInstallPrivateAsset={() => false}
|
||||
/>
|
||||
@@ -64,16 +64,15 @@ export const WithSerializedObjectView = () => (
|
||||
onExportObject={action('On export object')}
|
||||
onAddObjectInstance={action('On add instance to the scene')}
|
||||
onObjectCreated={action('On object created')}
|
||||
selectedObjectNames={[]}
|
||||
selectedObjectTags={[]}
|
||||
onChangeSelectedObjectTags={selectedObjectTags => {}}
|
||||
getAllObjectTags={() => []}
|
||||
selectedObjectFolderOrObjectsWithContext={[]}
|
||||
getValidatedObjectOrGroupName={newName => newName}
|
||||
onDeleteObject={(objectWithContext, cb) => cb(true)}
|
||||
onRenameObjectStart={() => {}}
|
||||
onRenameObjectFinish={(objectWithContext, newName, cb) => cb(true)}
|
||||
onObjectSelected={() => {}}
|
||||
renamedObjectWithContext={null}
|
||||
onRenameObjectFolderOrObjectWithContextFinish={(
|
||||
objectWithContext,
|
||||
newName,
|
||||
cb
|
||||
) => cb(true)}
|
||||
onObjectFolderOrObjectWithContextSelected={() => {}}
|
||||
hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
|
||||
canInstallPrivateAsset={() => false}
|
||||
/>
|
||||
@@ -81,38 +80,3 @@ export const WithSerializedObjectView = () => (
|
||||
</SerializedObjectDisplay>
|
||||
</DragAndDropContextProvider>
|
||||
);
|
||||
|
||||
export const WithTags = () => (
|
||||
<DragAndDropContextProvider>
|
||||
<div style={{ height: 250 }}>
|
||||
<ObjectsList
|
||||
getThumbnail={() => 'res/unknown32.png'}
|
||||
project={testProject.project}
|
||||
objectsContainer={testProject.testLayout}
|
||||
layout={testProject.testLayout}
|
||||
resourceManagementProps={fakeResourceManagementProps}
|
||||
onEditObject={action('On edit object')}
|
||||
onExportObject={action('On export object')}
|
||||
onAddObjectInstance={action('On add instance to the scene')}
|
||||
onObjectCreated={action('On object created')}
|
||||
selectedObjectNames={[]}
|
||||
selectedObjectTags={['Tag1', 'Tag2']}
|
||||
onChangeSelectedObjectTags={action('on change selected object tags')}
|
||||
getAllObjectTags={() => [
|
||||
'Tag1',
|
||||
'Tag2',
|
||||
'Looooooooooong Tag 3',
|
||||
'Unselected Tag 4',
|
||||
]}
|
||||
getValidatedObjectOrGroupName={newName => newName}
|
||||
onDeleteObject={(objectWithContext, cb) => cb(true)}
|
||||
onRenameObjectStart={() => {}}
|
||||
onRenameObjectFinish={(objectWithContext, newName, cb) => cb(true)}
|
||||
onObjectSelected={() => {}}
|
||||
renamedObjectWithContext={null}
|
||||
hotReloadPreviewButtonProps={fakeHotReloadPreviewButtonProps}
|
||||
canInstallPrivateAsset={() => false}
|
||||
/>
|
||||
</div>
|
||||
</DragAndDropContextProvider>
|
||||
);
|
||||
|
524
newIDE/app/src/stories/componentStories/UI/TreeView.stories.js
Normal file
524
newIDE/app/src/stories/componentStories/UI/TreeView.stories.js
Normal file
@@ -0,0 +1,524 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { action } from '@storybook/addon-actions';
|
||||
|
||||
import muiDecorator from '../../ThemeDecorator';
|
||||
import { getPaperDecorator } from '../../PaperDecorator';
|
||||
import FixedHeightFlexContainer from '../../FixedHeightFlexContainer';
|
||||
import DragAndDropContextProvider from '../../../UI/DragAndDrop/DragAndDropContextProvider';
|
||||
import { AutoSizer } from 'react-virtualized';
|
||||
|
||||
import TreeView from '../../../UI/TreeView';
|
||||
import { Column, Line } from '../../../UI/Grid';
|
||||
import TextField from '../../../UI/TextField';
|
||||
import sample from 'lodash/sample';
|
||||
import Toggle from '../../../UI/Toggle';
|
||||
import Text from '../../../UI/Text';
|
||||
import { ResponsiveLineStackLayout } from '../../../UI/Layout';
|
||||
|
||||
export default {
|
||||
title: 'UI Building Blocks/TreeView',
|
||||
component: TreeView,
|
||||
decorators: [getPaperDecorator('dark'), muiDecorator],
|
||||
};
|
||||
|
||||
type Node = {|
|
||||
name: string,
|
||||
id: string,
|
||||
isRoot?: boolean,
|
||||
children?: Node[],
|
||||
|};
|
||||
|
||||
const nodes: Node[] = [
|
||||
{
|
||||
name: 'Section 1',
|
||||
id: 'section',
|
||||
isRoot: true,
|
||||
children: [
|
||||
{
|
||||
name: 'Root #1',
|
||||
id: 'root-1',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-2', name: 'Child #2' },
|
||||
{ id: 'child-3', name: 'Child #3' },
|
||||
],
|
||||
id: 'child-4',
|
||||
name: 'Child #4',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-5', name: 'Child #5' }],
|
||||
id: 'child-6',
|
||||
name: 'Child #6',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #2',
|
||||
id: 'root-2',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-7', name: 'Child #7' },
|
||||
{ id: 'child-8', name: 'Child #8' },
|
||||
],
|
||||
id: 'child-9',
|
||||
name: 'Child #9',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-10', name: 'Child #10' }],
|
||||
id: 'child-11',
|
||||
name: 'Child #11',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #3',
|
||||
id: 'root-3',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-12', name: 'Child #12' },
|
||||
{ id: 'child-13', name: 'Child #13' },
|
||||
],
|
||||
id: 'child-14',
|
||||
name: 'Child #14',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-15', name: 'Child #15' }],
|
||||
id: 'child-16',
|
||||
name: 'Child #16',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #4',
|
||||
id: 'root-4',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-17', name: 'Child #17' },
|
||||
{ id: 'child-18', name: 'Child #18' },
|
||||
],
|
||||
id: 'child-19',
|
||||
name: 'Child #19',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-20', name: 'Child #20' }],
|
||||
id: 'child-21',
|
||||
name: 'Child #21',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #5',
|
||||
id: 'root-5',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-22', name: 'Child #22' },
|
||||
{ id: 'child-23', name: 'Child #23' },
|
||||
],
|
||||
id: 'child-24',
|
||||
name: 'Child #24',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-25', name: 'Child #25' }],
|
||||
id: 'child-26',
|
||||
name: 'Child #26',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Section 2',
|
||||
id: 'section-2',
|
||||
isRoot: true,
|
||||
children: [
|
||||
{
|
||||
name: 'Root #6',
|
||||
id: 'root-6',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-27', name: 'Child #27' },
|
||||
{ id: 'child-28', name: 'Child #28' },
|
||||
],
|
||||
id: 'child-29',
|
||||
name: 'Child #29',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-30', name: 'Child #30' }],
|
||||
id: 'child-31',
|
||||
name: 'Child #31',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #7',
|
||||
id: 'root-7',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-32', name: 'Child #32' },
|
||||
{ id: 'child-33', name: 'Child #33' },
|
||||
],
|
||||
id: 'child-34',
|
||||
name: 'Child #34',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-35', name: 'Child #35' }],
|
||||
id: 'child-36',
|
||||
name: 'Child #36',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #8',
|
||||
id: 'root-8',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-37', name: 'Child #37' },
|
||||
{ id: 'child-38', name: 'Child #38' },
|
||||
],
|
||||
id: 'child-39',
|
||||
name: 'Child #39',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-40', name: 'Child #40' }],
|
||||
id: 'child-41',
|
||||
name: 'Child #41',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #9',
|
||||
id: 'root-9',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-42', name: 'Child #42' },
|
||||
{ id: 'child-43', name: 'Child #43' },
|
||||
],
|
||||
id: 'child-44',
|
||||
name: 'Child #44',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-45', name: 'Child #45' }],
|
||||
id: 'child-46',
|
||||
name: 'Child #46',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #10',
|
||||
id: 'root-10',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-47', name: 'Child #47' },
|
||||
{ id: 'child-48', name: 'Child #48' },
|
||||
],
|
||||
id: 'child-49',
|
||||
name: 'Child #49',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-50', name: 'Child #50' }],
|
||||
id: 'child-51',
|
||||
name: 'Child #51',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #11',
|
||||
id: 'root-11',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-52', name: 'Child #52' },
|
||||
{ id: 'child-53', name: 'Child #53' },
|
||||
],
|
||||
id: 'child-54',
|
||||
name: 'Child #54',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-55', name: 'Child #55' }],
|
||||
id: 'child-56',
|
||||
name: 'Child #56',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #12',
|
||||
id: 'root-12',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-57', name: 'Child #57' },
|
||||
{ id: 'child-58', name: 'Child #58' },
|
||||
],
|
||||
id: 'child-59',
|
||||
name: 'Child #59',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-60', name: 'Child #60' }],
|
||||
id: 'child-61',
|
||||
name: 'Child #61',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #13',
|
||||
id: 'root-13',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-62', name: 'Child #62' },
|
||||
{ id: 'child-63', name: 'Child #63' },
|
||||
],
|
||||
id: 'child-64',
|
||||
name: 'Child #64',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-65', name: 'Child #65' }],
|
||||
id: 'child-66',
|
||||
name: 'Child #66',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #14',
|
||||
id: 'root-14',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-67', name: 'Child #67' },
|
||||
{ id: 'child-68', name: 'Child #68' },
|
||||
],
|
||||
id: 'child-69',
|
||||
name: 'Child #69',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-70', name: 'Child #70' }],
|
||||
id: 'child-71',
|
||||
name: 'Child #71',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #15',
|
||||
id: 'root-15',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-72', name: 'Child #72' },
|
||||
{ id: 'child-73', name: 'Child #73' },
|
||||
],
|
||||
id: 'child-74',
|
||||
name: 'Child #74',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-75', name: 'Child #75' }],
|
||||
id: 'child-76',
|
||||
name: 'Child #76',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #16',
|
||||
id: 'root-16',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-77', name: 'Child #77' },
|
||||
{ id: 'child-78', name: 'Child #78' },
|
||||
],
|
||||
id: 'child-79',
|
||||
name: 'Child #79',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-80', name: 'Child #80' }],
|
||||
id: 'child-81',
|
||||
name: 'Child #81',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #17',
|
||||
id: 'root-17',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-82', name: 'Child #82' },
|
||||
{ id: 'child-83', name: 'Child #83' },
|
||||
],
|
||||
id: 'child-84',
|
||||
name: 'Child #84',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-85', name: 'Child #85' }],
|
||||
id: 'child-86',
|
||||
name: 'Child #86',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #18',
|
||||
id: 'root-18',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-87', name: 'Child #87' },
|
||||
{ id: 'child-88', name: 'Child #88' },
|
||||
],
|
||||
id: 'child-89',
|
||||
name: 'Child #89',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-90', name: 'Child #90' }],
|
||||
id: 'child-91',
|
||||
name: 'Child #91',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #19',
|
||||
id: 'root-19',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-92', name: 'Child #92' },
|
||||
{ id: 'child-93', name: 'Child #93' },
|
||||
],
|
||||
id: 'child-94',
|
||||
name: 'Child #94',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-95', name: 'Child #95' }],
|
||||
id: 'child-96',
|
||||
name: 'Child #96',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #20',
|
||||
id: 'root-20',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-97', name: 'Child #97' },
|
||||
{ id: 'child-98', name: 'Child #98' },
|
||||
],
|
||||
id: 'child-99',
|
||||
name: 'Child #99',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-100', name: 'Child #100' }],
|
||||
id: 'child-101',
|
||||
name: 'Child #101',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #21',
|
||||
id: 'root-21',
|
||||
children: [
|
||||
{
|
||||
children: [
|
||||
{ id: 'child-102', name: 'Child #102' },
|
||||
{ id: 'child-103', name: 'Child #103' },
|
||||
],
|
||||
id: 'child-104',
|
||||
name: 'Child #104',
|
||||
},
|
||||
{
|
||||
children: [{ id: 'child-105', name: 'Child #105' }],
|
||||
id: 'child-106',
|
||||
name: 'Child #106',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'Root #200',
|
||||
id: 'root-22000',
|
||||
},
|
||||
];
|
||||
export const Default = () => {
|
||||
const [searchText, setSearchText] = React.useState<string>('');
|
||||
const [multiSelect, setMultiSelect] = React.useState<boolean>(true);
|
||||
const [selectedItems, setSelectedItems] = React.useState<Node[]>([]);
|
||||
const onSelectItems = (items: Node[]) => {
|
||||
setSelectedItems(items.filter(item => !item.isRoot));
|
||||
};
|
||||
return (
|
||||
<DragAndDropContextProvider>
|
||||
<Column noMargin expand>
|
||||
<ResponsiveLineStackLayout expand>
|
||||
<Line expand noMargin>
|
||||
<TextField
|
||||
fullWidth
|
||||
type="text"
|
||||
value={searchText}
|
||||
onChange={(e, text) => {
|
||||
setSearchText(text);
|
||||
}}
|
||||
hintText={'Filter'}
|
||||
/>
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<Toggle
|
||||
label={<Text>Allow multi selection</Text>}
|
||||
labelPosition="right"
|
||||
toggled={multiSelect}
|
||||
onToggle={() => setMultiSelect(!multiSelect)}
|
||||
/>
|
||||
</Line>
|
||||
</ResponsiveLineStackLayout>
|
||||
<FixedHeightFlexContainer height={400}>
|
||||
<AutoSizer>
|
||||
{({ height, width }) => (
|
||||
<Line expand>
|
||||
<Column expand noMargin>
|
||||
<TreeView
|
||||
multiSelect={multiSelect}
|
||||
height={height}
|
||||
width={width}
|
||||
items={nodes}
|
||||
searchText={searchText}
|
||||
getItemId={node => node.id}
|
||||
getItemName={node => node.name}
|
||||
onEditItem={action('Edit item')}
|
||||
selectedItems={selectedItems}
|
||||
onSelectItems={onSelectItems}
|
||||
onRenameItem={action('Rename item')}
|
||||
getItemThumbnail={node =>
|
||||
node.children
|
||||
? null
|
||||
: sample([
|
||||
'res/unknown32.png',
|
||||
'res/view24.png',
|
||||
'res/bug24.png',
|
||||
'res/save_all24.png',
|
||||
])
|
||||
}
|
||||
// $FlowIgnore
|
||||
getItemChildren={node => node.children}
|
||||
buildMenuTemplate={() => [{ label: 'salut' }]}
|
||||
onMoveSelectionToItem={action('Drop selection on item')}
|
||||
canMoveSelectionToItem={() => Math.random() > 0.2}
|
||||
reactDndType="demo"
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
)}
|
||||
</AutoSizer>
|
||||
</FixedHeightFlexContainer>
|
||||
</Column>
|
||||
</DragAndDropContextProvider>
|
||||
);
|
||||
};
|
Reference in New Issue
Block a user