Add support for local variables for loop events (While, Repeat, For Each, For Each Child Variable) and loop counter variable (#8273)

- "For Each Child Variable" now also properly support choosing local variables for the variable to iterate on, and for the variable where to store the child and the optional variable where to store the name.
- The loop counter starts from 0 and is automatically increased by 1 each time the event is repeated.
This commit is contained in:
Florian Rival
2026-02-15 16:14:04 +01:00
committed by GitHub
parent 9f9e5a9b99
commit 6bb56c6e5b
32 changed files with 2286 additions and 467 deletions
@@ -13,7 +13,11 @@ using namespace std;
namespace gd {
ForEachChildVariableEvent::ForEachChildVariableEvent()
: BaseEvent(), valueIteratorVariableName("child"), keyIteratorVariableName(""), iterableVariableName("") {}
: BaseEvent(),
valueIteratorVariableName("child"),
keyIteratorVariableName(""),
iterableVariableName(""),
variables(gd::VariablesContainer::SourceType::Local) {}
vector<gd::InstructionsList*> ForEachChildVariableEvent::GetAllConditionsVectors() {
vector<gd::InstructionsList*> allConditions;
@@ -88,6 +92,12 @@ void ForEachChildVariableEvent::SerializeTo(SerializerElement& element) const {
if (!events.IsEmpty())
gd::EventsListSerialization::SerializeEventsTo(events,
element.AddChild("events"));
if (HasVariables()) {
variables.SerializeTo(element.AddChild("variables"));
}
if (!loopIndexVariableName.empty()) {
element.AddChild("loopIndexVariable").SetStringValue(loopIndexVariableName);
}
}
void ForEachChildVariableEvent::UnserializeFrom(gd::Project& project,
@@ -105,6 +115,16 @@ void ForEachChildVariableEvent::UnserializeFrom(gd::Project& project,
gd::EventsListSerialization::UnserializeEventsFrom(
project, events, element.GetChild("events", 0, "Events"));
}
variables.Clear();
if (element.HasChild("variables")) {
variables.UnserializeFrom(element.GetChild("variables"));
}
loopIndexVariableName =
element.HasChild("loopIndexVariable")
? element.GetChild("loopIndexVariable").GetStringValue()
: "";
}
} // namespace gd
@@ -9,6 +9,7 @@
#include "GDCore/Events/Event.h"
#include "GDCore/Events/EventsList.h"
#include "GDCore/Events/Expression.h"
#include "GDCore/Project/VariablesContainer.h"
namespace gd {
class Instruction;
class Project;
@@ -34,6 +35,12 @@ class GD_CORE_API ForEachChildVariableEvent : public gd::BaseEvent {
virtual const gd::EventsList& GetSubEvents() const { return events; };
virtual gd::EventsList& GetSubEvents() { return events; };
virtual bool CanHaveVariables() const { return true; }
virtual const gd::VariablesContainer& GetVariables() const {
return variables;
};
virtual gd::VariablesContainer& GetVariables() { return variables; };
const gd::InstructionsList& GetConditions() const { return conditions; };
gd::InstructionsList& GetConditions() { return conditions; };
@@ -86,6 +93,9 @@ class GD_CORE_API ForEachChildVariableEvent : public gd::BaseEvent {
*/
void SetKeyIteratorVariableName(gd::String newName) { keyIteratorVariableName = newName; };
const gd::String& GetLoopIndexVariableName() const { return loopIndexVariableName; }
void SetLoopIndexVariableName(const gd::String& name) { loopIndexVariableName = name; }
virtual std::vector<const gd::InstructionsList*> GetAllConditionsVectors()
const;
virtual std::vector<const gd::InstructionsList*> GetAllActionsVectors() const;
@@ -109,6 +119,8 @@ class GD_CORE_API ForEachChildVariableEvent : public gd::BaseEvent {
gd::InstructionsList conditions;
gd::InstructionsList actions;
gd::EventsList events;
VariablesContainer variables;
gd::String loopIndexVariableName;
};
} // namespace gd
+19 -1
View File
@@ -14,7 +14,9 @@ using namespace std;
namespace gd {
ForEachEvent::ForEachEvent()
: BaseEvent(), objectsToPick("") {}
: BaseEvent(),
objectsToPick(""),
variables(gd::VariablesContainer::SourceType::Local) {}
vector<gd::InstructionsList*> ForEachEvent::GetAllConditionsVectors() {
vector<gd::InstructionsList*> allConditions;
@@ -77,6 +79,12 @@ void ForEachEvent::SerializeTo(SerializerElement& element) const {
if (!events.IsEmpty())
gd::EventsListSerialization::SerializeEventsTo(events,
element.AddChild("events"));
if (HasVariables()) {
variables.SerializeTo(element.AddChild("variables"));
}
if (!loopIndexVariableName.empty()) {
element.AddChild("loopIndexVariable").SetStringValue(loopIndexVariableName);
}
}
void ForEachEvent::UnserializeFrom(gd::Project& project,
@@ -93,6 +101,16 @@ void ForEachEvent::UnserializeFrom(gd::Project& project,
gd::EventsListSerialization::UnserializeEventsFrom(
project, events, element.GetChild("events", 0, "Events"));
}
variables.Clear();
if (element.HasChild("variables")) {
variables.UnserializeFrom(element.GetChild("variables"));
}
loopIndexVariableName =
element.HasChild("loopIndexVariable")
? element.GetChild("loopIndexVariable").GetStringValue()
: "";
}
} // namespace gd
+12
View File
@@ -10,6 +10,7 @@
#include "GDCore/Events/Event.h"
#include "GDCore/Events/EventsList.h"
#include "GDCore/Project/VariablesContainer.h"
namespace gd {
class Instruction;
class Project;
@@ -36,6 +37,12 @@ class GD_CORE_API ForEachEvent : public gd::BaseEvent {
virtual const gd::EventsList& GetSubEvents() const { return events; };
virtual gd::EventsList& GetSubEvents() { return events; };
virtual bool CanHaveVariables() const { return true; }
virtual const gd::VariablesContainer& GetVariables() const {
return variables;
};
virtual gd::VariablesContainer& GetVariables() { return variables; };
const gd::InstructionsList& GetConditions() const { return conditions; };
gd::InstructionsList& GetConditions() { return conditions; };
@@ -49,6 +56,9 @@ class GD_CORE_API ForEachEvent : public gd::BaseEvent {
objectsToPick = gd::Expression(objectsToPick_);
};
const gd::String& GetLoopIndexVariableName() const { return loopIndexVariableName; }
void SetLoopIndexVariableName(const gd::String& name) { loopIndexVariableName = name; }
virtual std::vector<const gd::InstructionsList*> GetAllConditionsVectors()
const;
virtual std::vector<const gd::InstructionsList*> GetAllActionsVectors() const;
@@ -69,6 +79,8 @@ class GD_CORE_API ForEachEvent : public gd::BaseEvent {
gd::InstructionsList conditions;
gd::InstructionsList actions;
gd::EventsList events;
VariablesContainer variables;
gd::String loopIndexVariableName;
};
} // namespace gd
@@ -15,6 +15,7 @@ namespace gd {
RepeatEvent::RepeatEvent()
: BaseEvent(),
repeatNumberExpression(""),
variables(gd::VariablesContainer::SourceType::Local),
repeatNumberExpressionSelected(false) {}
vector<gd::InstructionsList*> RepeatEvent::GetAllConditionsVectors() {
@@ -79,6 +80,12 @@ void RepeatEvent::SerializeTo(SerializerElement& element) const {
if (!events.IsEmpty())
gd::EventsListSerialization::SerializeEventsTo(events,
element.AddChild("events"));
if (HasVariables()) {
variables.SerializeTo(element.AddChild("variables"));
}
if (!loopIndexVariableName.empty()) {
element.AddChild("loopIndexVariable").SetStringValue(loopIndexVariableName);
}
}
void RepeatEvent::UnserializeFrom(gd::Project& project,
@@ -97,6 +104,16 @@ void RepeatEvent::UnserializeFrom(gd::Project& project,
gd::EventsListSerialization::UnserializeEventsFrom(
project, events, element.GetChild("events", 0, "Events"));
}
variables.Clear();
if (element.HasChild("variables")) {
variables.UnserializeFrom(element.GetChild("variables"));
}
loopIndexVariableName =
element.HasChild("loopIndexVariable")
? element.GetChild("loopIndexVariable").GetStringValue()
: "";
}
} // namespace gd
+12
View File
@@ -8,6 +8,7 @@
#include "GDCore/Events/Event.h"
#include "GDCore/Events/EventsList.h"
#include "GDCore/Project/VariablesContainer.h"
namespace gd {
class Instruction;
class Project;
@@ -30,6 +31,12 @@ class GD_CORE_API RepeatEvent : public gd::BaseEvent {
virtual const gd::EventsList& GetSubEvents() const { return events; };
virtual gd::EventsList& GetSubEvents() { return events; };
virtual bool CanHaveVariables() const { return true; }
virtual const gd::VariablesContainer& GetVariables() const {
return variables;
};
virtual gd::VariablesContainer& GetVariables() { return variables; };
const gd::InstructionsList& GetConditions() const { return conditions; };
gd::InstructionsList& GetConditions() { return conditions; };
@@ -43,6 +50,9 @@ class GD_CORE_API RepeatEvent : public gd::BaseEvent {
repeatNumberExpression = gd::Expression(repeatNumberExpression_);
};
const gd::String& GetLoopIndexVariableName() const { return loopIndexVariableName; }
void SetLoopIndexVariableName(const gd::String& name) { loopIndexVariableName = name; }
virtual std::vector<gd::InstructionsList*> GetAllConditionsVectors();
virtual std::vector<gd::InstructionsList*> GetAllActionsVectors();
virtual std::vector<std::pair<gd::Expression*, gd::ParameterMetadata> >
@@ -63,6 +73,8 @@ class GD_CORE_API RepeatEvent : public gd::BaseEvent {
gd::InstructionsList conditions;
gd::InstructionsList actions;
EventsList events;
VariablesContainer variables;
gd::String loopIndexVariableName;
bool repeatNumberExpressionSelected;
};
+16
View File
@@ -56,6 +56,12 @@ void WhileEvent::SerializeTo(SerializerElement& element) const {
if (!events.IsEmpty())
gd::EventsListSerialization::SerializeEventsTo(events,
element.AddChild("events"));
if (HasVariables()) {
variables.SerializeTo(element.AddChild("variables"));
}
if (!loopIndexVariableName.empty()) {
element.AddChild("loopIndexVariable").SetStringValue(loopIndexVariableName);
}
}
void WhileEvent::UnserializeFrom(gd::Project& project,
@@ -76,6 +82,16 @@ void WhileEvent::UnserializeFrom(gd::Project& project,
gd::EventsListSerialization::UnserializeEventsFrom(
project, events, element.GetChild("events", 0, "Events"));
}
variables.Clear();
if (element.HasChild("variables")) {
variables.UnserializeFrom(element.GetChild("variables"));
}
loopIndexVariableName =
element.HasChild("loopIndexVariable")
? element.GetChild("loopIndexVariable").GetStringValue()
: "";
}
} // namespace gd
+16 -1
View File
@@ -8,6 +8,7 @@
#define GDCORE_WHILEEVENT_H
#include "GDCore/Events/Event.h"
#include "GDCore/Events/EventsList.h"
#include "GDCore/Project/VariablesContainer.h"
namespace gd {
class Instruction;
class Project;
@@ -25,7 +26,10 @@ namespace gd {
*/
class GD_CORE_API WhileEvent : public gd::BaseEvent {
public:
WhileEvent() : infiniteLoopWarning(true), justCreatedByTheUser(true){};
WhileEvent()
: infiniteLoopWarning(true),
justCreatedByTheUser(true),
variables(gd::VariablesContainer::SourceType::Local){};
virtual ~WhileEvent(){};
virtual gd::WhileEvent* Clone() const { return new WhileEvent(*this); }
@@ -35,6 +39,12 @@ class GD_CORE_API WhileEvent : public gd::BaseEvent {
virtual const gd::EventsList& GetSubEvents() const { return events; };
virtual gd::EventsList& GetSubEvents() { return events; };
virtual bool CanHaveVariables() const { return true; }
virtual const gd::VariablesContainer& GetVariables() const {
return variables;
};
virtual gd::VariablesContainer& GetVariables() { return variables; };
const gd::InstructionsList& GetConditions() const { return conditions; };
gd::InstructionsList& GetConditions() { return conditions; };
@@ -51,6 +61,9 @@ class GD_CORE_API WhileEvent : public gd::BaseEvent {
bool HasInfiniteLoopWarning() const { return infiniteLoopWarning; }
const gd::String& GetLoopIndexVariableName() const { return loopIndexVariableName; }
void SetLoopIndexVariableName(const gd::String& name) { loopIndexVariableName = name; }
virtual std::vector<gd::InstructionsList*> GetAllConditionsVectors();
virtual std::vector<gd::InstructionsList*> GetAllActionsVectors();
virtual std::vector<const gd::InstructionsList*> GetAllConditionsVectors()
@@ -71,6 +84,8 @@ class GD_CORE_API WhileEvent : public gd::BaseEvent {
bool justCreatedByTheUser; ///< Used so as not to show message box to
///< de/activate infinite loop warning when the
///< user create the event
gd::VariablesContainer variables;
gd::String loopIndexVariableName;
int GetConditionsHeight() const;
int GetActionsHeight() const;
@@ -437,6 +437,22 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
context.InheritsFrom(parentContext);
context.ForbidReuse();
gd::String localVariablesInitializationCode = "";
if (event_.HasVariables()) {
GenerateLocalVariablesInitializationCode(
event_.GetVariables(), codeGenerator,
localVariablesInitializationCode);
}
const bool hasIndexVariable = !event.GetLoopIndexVariableName().empty();
gd::String whileIndexVar;
gd::String indexVariableAccessor;
if (hasIndexVariable) {
whileIndexVar = "whileIndex" + gd::String::From(context.GetContextDepth());
indexVariableAccessor = codeGenerator.GenerateAnyOrSceneVariableGetter(
event.GetLoopIndexVariableName(), context);
}
// Prepare codes
gd::String whileConditionsStr =
codeGenerator.GenerateConditionsListCode(event.GetWhileConditions(),
@@ -458,9 +474,17 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
// Write final code
gd::String whileBoolean =
codeGenerator.GenerateBooleanFullName("stopDoWhile", context);
outputCode += localVariablesInitializationCode;
if (hasIndexVariable) {
outputCode += "let " + whileIndexVar + " = 0;\n";
}
outputCode += "let " + whileBoolean + " = false;\n";
outputCode += "do {\n";
outputCode += codeGenerator.GenerateObjectsDeclarationCode(context);
if (hasIndexVariable) {
outputCode +=
indexVariableAccessor + ".setNumber(" + whileIndexVar + ");\n";
}
outputCode += whileConditionsStr;
outputCode += "if (" + whileIfPredicate + ") {\n";
outputCode += conditionsCode;
@@ -474,8 +498,15 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
outputCode += "} //Subevents end.\n";
outputCode += "}\n";
outputCode += "} else " + whileBoolean + " = true; \n";
if (hasIndexVariable) {
outputCode += whileIndexVar + "++;\n";
}
outputCode += "} while (!" + whileBoolean + ");\n";
if (event_.HasVariables()) {
outputCode += codeGenerator.GenerateLocalVariablesStackAccessor() +
".pop();\n";
}
return outputCode;
});
@@ -494,6 +525,13 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
context.InheritsFrom(parentContext);
context.ForbidReuse();
gd::String localVariablesInitializationCode = "";
if (event_.HasVariables()) {
GenerateLocalVariablesInitializationCode(
event_.GetVariables(), codeGenerator,
localVariablesInitializationCode);
}
gd::String conditionsCode = codeGenerator.GenerateConditionsListCode(
event.GetConditions(), context);
gd::String actionsCode =
@@ -521,8 +559,21 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
bool valueIteratorExists =
!event.GetValueIteratorVariableName().empty();
bool keyIteratorExists = !event.GetKeyIteratorVariableName().empty();
const bool hasIndexVariable = !event.GetLoopIndexVariableName().empty();
gd::String childIndexVar;
gd::String indexVariableAccessor;
if (hasIndexVariable) {
childIndexVar = "childIndex" + gd::String::From(context.GetContextDepth());
indexVariableAccessor = codeGenerator.GenerateAnyOrSceneVariableGetter(
event.GetLoopIndexVariableName(), parentContext);
}
// clang-format off
outputCode += localVariablesInitializationCode;
if (hasIndexVariable) {
outputCode += "let " + childIndexVar + " = 0;\n";
}
// Define references to variables (if they exist)
if (keyIteratorExists)
outputCode +=
@@ -569,6 +620,11 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
" // Arrays are passed by reference like JS objects\n"
" $VALUE_ITERATOR_REFERENCE.replaceChildrenArray($STRUCTURE_CHILD_VARIABLE.getAllChildrenArray());\n"
" } else console.warn(\"Cannot identify type: \", type);\n";
if (hasIndexVariable)
outputCode += " " + indexVariableAccessor + ".setNumber(" +
childIndexVar + ");\n"
" " + childIndexVar + "++;\n";
// clang-format on
// Now do the rest of standard event code generation
@@ -619,6 +675,11 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
iteratorReferenceVariableName);
}
if (event_.HasVariables()) {
outputCode += codeGenerator.GenerateLocalVariablesStackAccessor() +
".pop();\n";
}
return outputCode
.FindAndReplace("$ITERATOR_KEY", iteratorKeyVariableName)
.FindAndReplace("$STRUCTURE_CHILD_VARIABLE",
@@ -649,6 +710,13 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
context.InheritsFrom(parentContext);
context.ForbidReuse();
gd::String localVariablesInitializationCode = "";
if (event_.HasVariables()) {
GenerateLocalVariablesInitializationCode(
event_.GetVariables(), codeGenerator,
localVariablesInitializationCode);
}
// Prepare conditions/actions codes
gd::String conditionsCode = codeGenerator.GenerateConditionsListCode(
event.GetConditions(), context);
@@ -670,11 +738,22 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
"repeatCount" + gd::String::From(context.GetContextDepth());
gd::String repeatIndexVar =
"repeatIndex" + gd::String::From(context.GetContextDepth());
const bool hasIndexVariable = !event.GetLoopIndexVariableName().empty();
gd::String indexVariableAccessor;
if (hasIndexVariable) {
indexVariableAccessor = codeGenerator.GenerateAnyOrSceneVariableGetter(
event.GetLoopIndexVariableName(), context);
}
outputCode += localVariablesInitializationCode;
outputCode +=
"const " + repeatCountVar + " = " + repeatCountCode + ";\n";
outputCode += "for (let " + repeatIndexVar + " = 0;" + repeatIndexVar +
" < " + repeatCountVar + ";++" + repeatIndexVar + ") {\n";
outputCode += objectDeclaration;
if (hasIndexVariable) {
outputCode +=
indexVariableAccessor + ".setNumber(" + repeatIndexVar + ");\n";
}
outputCode += conditionsCode;
outputCode += "if (" + ifPredicate + ")\n";
outputCode += "{\n";
@@ -688,6 +767,11 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
outputCode += "}\n";
if (event_.HasVariables()) {
outputCode += codeGenerator.GenerateLocalVariablesStackAccessor() +
".pop();\n";
}
return outputCode;
});
@@ -713,6 +797,13 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
context.ForbidReuse(); // TODO: This may not be necessary (to be
// investigated/heavily tested).
gd::String localVariablesInitializationCode = "";
if (event_.HasVariables()) {
GenerateLocalVariablesInitializationCode(
event_.GetVariables(), codeGenerator,
localVariablesInitializationCode);
}
for (unsigned int i = 0; i < realObjects.size(); ++i)
context.EmptyObjectsListNeeded(realObjects[i]);
@@ -745,6 +836,14 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
codeGenerator.GetCodeNamespaceAccessor() + "forEachObjects" +
gd::String::From(context.GetContextDepth());
codeGenerator.AddGlobalDeclaration(forEachObjectsList + " = [];\n");
const bool hasIndexVariable = !event.GetLoopIndexVariableName().empty();
gd::String indexVariableAccessor;
if (hasIndexVariable) {
indexVariableAccessor = codeGenerator.GenerateAnyOrSceneVariableGetter(
event.GetLoopIndexVariableName(), context);
}
outputCode += localVariablesInitializationCode;
if (realObjects.size() !=
1) //(We write a slightly more simple ( and optimized ) output code
@@ -832,6 +931,11 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
}
}
if (hasIndexVariable) {
outputCode +=
indexVariableAccessor + ".setNumber(" + forEachIndexVar + ");\n";
}
outputCode += conditionsCode;
outputCode += "if (" + ifPredicate + ") {\n";
outputCode += actionsCode;
@@ -844,6 +948,11 @@ CommonInstructionsExtension::CommonInstructionsExtension() {
outputCode += "}\n"; // End of for loop
if (event_.HasVariables()) {
outputCode += codeGenerator.GenerateLocalVariablesStackAccessor() +
".pop();\n";
}
return outputCode;
});
+9 -1
View File
@@ -1496,7 +1496,7 @@ interface InitialInstance {
boolean HasAnyOverriddenProperty([Const, Ref] gdObject obj);
boolean HasAnyOverriddenPropertyForBehavior([Const, Ref] Behavior behavior);
boolean HasBehaviorOverridingNamed([Const] DOMString name);
Behavior AddNewBehaviorOverriding([Ref] Project project, [Const] DOMString type, [Const] DOMString name);
[Ref] Behavior GetBehaviorOverriding([Const] DOMString name);
@@ -2487,6 +2487,8 @@ interface RepeatEvent {
[Ref] InstructionsList GetActions();
void SetRepeatExpressionPlainString([Const] DOMString expr);
[Const, Ref] Expression GetRepeatExpression();
[Const, Ref] DOMString GetLoopIndexVariableName();
void SetLoopIndexVariableName([Const] DOMString name);
};
RepeatEvent implements BaseEvent;
@@ -2496,6 +2498,8 @@ interface WhileEvent {
[Ref] InstructionsList GetConditions();
[Ref] InstructionsList GetWhileConditions();
[Ref] InstructionsList GetActions();
[Const, Ref] DOMString GetLoopIndexVariableName();
void SetLoopIndexVariableName([Const] DOMString name);
};
WhileEvent implements BaseEvent;
@@ -2506,6 +2510,8 @@ interface ForEachEvent {
[Const, Ref] DOMString GetObjectToPick();
[Ref] InstructionsList GetConditions();
[Ref] InstructionsList GetActions();
[Const, Ref] DOMString GetLoopIndexVariableName();
void SetLoopIndexVariableName([Const] DOMString name);
};
ForEachEvent implements BaseEvent;
@@ -2521,6 +2527,8 @@ interface ForEachChildVariableEvent {
void SetIterableVariableName([Const] DOMString newName);
void SetKeyIteratorVariableName([Const] DOMString newName);
void SetValueIteratorVariableName([Const] DOMString newName);
[Const, Ref] DOMString GetLoopIndexVariableName();
void SetLoopIndexVariableName([Const] DOMString name);
};
ForEachChildVariableEvent implements BaseEvent;
+71 -4
View File
@@ -176,6 +176,9 @@ class Variable {
}
getAllChildrenArray() {
if (this._childrenArray.length > 0) {
return this._childrenArray;
}
return Object.values(this._children);
}
@@ -187,14 +190,32 @@ class Variable {
return !!this._children[childName];
}
castTo() {}
castTo(newType) {
if (newType === 'string' || newType === 'number' || newType === 'boolean') {
this._children = {};
this._childrenArray = [];
} else if (newType === 'structure') {
this._childrenArray = [];
} else if (newType === 'array') {
this._children = {};
}
}
isPrimitive() {
return this.getAllChildrenArray().length === 0;
return (
this._childrenArray.length === 0 &&
Object.keys(this._children).length === 0
);
}
getType() {
return this.isPrimitive() ? 'number' : 'structure';
if (this._childrenArray.length > 0) {
return 'array';
}
if (Object.keys(this._children).length > 0) {
return 'structure';
}
return 'number';
}
removeChild(childName) {
@@ -206,6 +227,43 @@ class Variable {
this._childrenArray = [];
}
/**
* Replaces all the children with a new map of children.
* @param {Object.<string, Variable>} newChildren
*/
replaceChildren(newChildren) {
this._children = newChildren;
this._childrenArray = [];
}
/**
* Replaces all the children with a new array of children.
* @param {Variable[]} newChildren
*/
replaceChildrenArray(newChildren) {
this._childrenArray = newChildren;
this._children = {};
}
/**
* Converts this variable into an equivalent JavaScript object.
* @returns {any}
*/
toJSObject() {
if (this._childrenArray.length > 0) {
return this._childrenArray.map((child) => child.toJSObject());
}
if (Object.keys(this._children).length > 0) {
const obj = {};
for (const name in this._children) {
if (this._children.hasOwnProperty(name))
obj[name] = this._children[name].toJSObject();
}
return obj;
}
return this._value;
}
clone() {
return Variable.copy(this, new Variable());
}
@@ -303,7 +361,12 @@ class VariablesContainer {
const childVariable = variable.getChild(childVariableData.name);
setupVariableFromVariableData(childVariable, childVariableData);
});
variable.setString(variableData.value);
} else if (variableData.type === 'array') {
variableData.children.forEach((childVariableData) => {
const childVariable = new Variable();
setupVariableFromVariableData(childVariable, childVariableData);
variable._pushVariable(childVariable);
});
} else {
throw new Error(
'Unsupported variable type in GDJS Mock:',
@@ -966,6 +1029,10 @@ function makeMinimalGDJSMock(options) {
wait: () => new FakeAsyncTask(),
noop: () => {},
},
network: {
variableStructureToJSON: (variable) =>
JSON.stringify(variable.toJSObject()),
},
common: {
resolveAsyncEventsFunction: ({ task }) => task.resolve(),
},
@@ -122,427 +122,6 @@ describe('libGD.js - GDJS Code Generation integration tests', function () {
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(1);
});
it('can generate a While event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '=', '0'],
},
],
events: [],
},
{
infiniteLoopWarning: true,
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['Counter', '<', '4'],
},
],
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(4);
});
it('can generate nested While events', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '=', '0'],
},
],
events: [],
},
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellY', '=', '0'],
},
],
events: [],
},
{
infiniteLoopWarning: true,
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['CellY', '<', '4'],
},
],
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellX', '=', '0'],
},
],
events: [
{
infiniteLoopWarning: true,
type: 'BuiltinCommonInstructions::While',
whileConditions: [
{
type: { value: 'VarScene' },
parameters: ['CellX', '<', '5'],
},
],
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellX', '+', '1'],
},
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
{
type: 'BuiltinCommonInstructions::Standard',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['CellY', '+', '1'],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(20);
});
it('can generate a Repeat event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: '4',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(4);
});
it('can generate a nested Repeat event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: '4',
conditions: [],
actions: [],
events: [
{
type: 'BuiltinCommonInstructions::Repeat',
repeatExpression: '5',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', '1'],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(20);
});
it('can generate a "for each object" event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEach',
object: 'MyObject',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', 'MyObject.Variable(MyVariable)'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
parameterTypes: {
MyObject: 'object',
},
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 objects with variable values from 1 to 4.
const objectLists = new gdjs.Hashtable();
const myObjects = [];
objectLists.put('MyObject', myObjects);
for (let index = 1; index <= 4; index++) {
const myObject = runtimeScene.createObject('MyObject');
myObject.getVariables().get('MyVariable').setNumber(index);
myObjects.push(myObject);
}
runCompiledEvents(gdjs, runtimeScene, [objectLists]);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
1 + 2 + 3 + 4
);
});
it('can generate a nested "for each object" event', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEach',
object: 'MyObjectA',
conditions: [],
actions: [],
events: [
{
type: 'BuiltinCommonInstructions::ForEach',
object: 'MyObjectB',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Counter',
'+',
'MyObjectA.Variable(MyVariable) * MyObjectB.Variable(MyVariable)',
],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
parameterTypes: {
MyObjectA: 'object',
MyObjectB: 'object',
},
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 objects with variable values from 1 to 4.
const objectALists = new gdjs.Hashtable();
const myObjectsA = [];
objectALists.put('MyObjectA', myObjectsA);
for (let index = 1; index <= 4; index++) {
const myObjectA = runtimeScene.createObject('MyObjectA');
myObjectA.getVariables().get('MyVariable').setNumber(index);
myObjectsA.push(myObjectA);
}
// Create 3 objects with variable values from 5 to 7.
const objectBLists = new gdjs.Hashtable();
const myObjectsB = [];
objectBLists.put('MyObjectB', myObjectsB);
for (let index = 1; index <= 3; index++) {
const myObjectB = runtimeScene.createObject('MyObjectB');
myObjectB
.getVariables()
.get('MyVariable')
.setNumber(4 + index);
myObjectsB.push(myObjectB);
}
runCompiledEvents(gdjs, runtimeScene, [objectALists, objectBLists]);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
(1 + 2 + 3 + 4) * (5 + 6 + 7)
);
});
it('can generate a "for each child variable" event with undeclared variables', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEachChildVariable',
iterableVariableName: 'MyVariable',
valueIteratorVariableName: 'child',
keyIteratorVariableName: '',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: ['Counter', '+', 'Variable(child)'],
},
],
events: [],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 child-variables with values from 1 to 4.
const myVariable = runtimeScene.getVariables().get('MyVariable');
for (let index = 1; index <= 4; index++) {
myVariable.getChild('Child' + index).setNumber(index);
}
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
1 + 2 + 3 + 4
);
});
it('can generate a nested "for each child variable" event with undeclared variables', function () {
const serializerElement = gd.Serializer.fromJSObject([
{
type: 'BuiltinCommonInstructions::ForEachChildVariable',
iterableVariableName: 'MyVariableA',
valueIteratorVariableName: 'childA',
keyIteratorVariableName: '',
conditions: [],
actions: [],
events: [
{
type: 'BuiltinCommonInstructions::ForEachChildVariable',
iterableVariableName: 'MyVariableB',
valueIteratorVariableName: 'childB',
keyIteratorVariableName: '',
conditions: [],
actions: [
{
type: { value: 'ModVarScene' },
parameters: [
'Counter',
'+',
'Variable(childA) * Variable(childB)',
],
},
],
events: [],
},
],
},
]);
const runCompiledEvents = generateCompiledEventsFromSerializedEvents(
gd,
serializerElement,
{
logCode: false,
}
);
const { gdjs, runtimeScene } = makeMinimalGDJSMock();
// Create 4 child-variables with values from 1 to 4.
const myVariableA = runtimeScene.getVariables().get('MyVariableA');
for (let index = 1; index <= 4; index++) {
myVariableA.getChild('Child' + index).setNumber(index);
}
// Create 4 child-variables with values from 5 to 7.
const myVariableB = runtimeScene.getVariables().get('MyVariableB');
for (let index = 1; index <= 3; index++) {
myVariableB.getChild('Child' + index).setNumber(4 + index);
}
runCompiledEvents(gdjs, runtimeScene, []);
expect(runtimeScene.getVariables().has('Counter')).toBe(true);
expect(runtimeScene.getVariables().get('Counter').getAsNumber()).toBe(
(1 + 2 + 3 + 4) * (5 + 6 + 7)
);
});
it('generates a working function creating objects', function () {
const eventsSerializerElement = gd.Serializer.fromJSObject([
{
File diff suppressed because it is too large Load Diff
+8
View File
@@ -1913,6 +1913,8 @@ export class RepeatEvent extends BaseEvent {
getActions(): InstructionsList;
setRepeatExpressionPlainString(expr: string): void;
getRepeatExpression(): Expression;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
}
export class WhileEvent extends BaseEvent {
@@ -1920,6 +1922,8 @@ export class WhileEvent extends BaseEvent {
getConditions(): InstructionsList;
getWhileConditions(): InstructionsList;
getActions(): InstructionsList;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
}
export class ForEachEvent extends BaseEvent {
@@ -1928,6 +1932,8 @@ export class ForEachEvent extends BaseEvent {
getObjectToPick(): string;
getConditions(): InstructionsList;
getActions(): InstructionsList;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
}
export class ForEachChildVariableEvent extends BaseEvent {
@@ -1940,6 +1946,8 @@ export class ForEachChildVariableEvent extends BaseEvent {
setIterableVariableName(newName: string): void;
setKeyIteratorVariableName(newName: string): void;
setValueIteratorVariableName(newName: string): void;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
}
export class CommentEvent extends BaseEvent {
@@ -9,6 +9,8 @@ declare class gdForEachChildVariableEvent extends gdBaseEvent {
setIterableVariableName(newName: string): void;
setKeyIteratorVariableName(newName: string): void;
setValueIteratorVariableName(newName: string): void;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
delete(): void;
ptr: number;
};
+2
View File
@@ -5,6 +5,8 @@ declare class gdForEachEvent extends gdBaseEvent {
getObjectToPick(): string;
getConditions(): gdInstructionsList;
getActions(): gdInstructionsList;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
delete(): void;
ptr: number;
};
+2
View File
@@ -5,6 +5,8 @@ declare class gdRepeatEvent extends gdBaseEvent {
getActions(): gdInstructionsList;
setRepeatExpressionPlainString(expr: string): void;
getRepeatExpression(): gdExpression;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
delete(): void;
ptr: number;
};
+2
View File
@@ -4,6 +4,8 @@ declare class gdWhileEvent extends gdBaseEvent {
getConditions(): gdInstructionsList;
getWhileConditions(): gdInstructionsList;
getActions(): gdInstructionsList;
getLoopIndexVariableName(): string;
setLoopIndexVariableName(name: string): void;
delete(): void;
ptr: number;
};
@@ -17,6 +17,7 @@ export const instructionParameter = 'instruction-parameter';
export const instructionInvalidParameter = 'instruction-invalid-parameter';
export const elseTextContainer = 'else-text-container';
export const invalidElse = 'invalid-else';
export const eventLabel = 'event-label';
export const instructionMissingParameter = 'instruction-missing-parameter';
export const instructionWarningParameter = 'instruction-warning-parameter';
export const instructionDeprecatedParameter =
@@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import InstructionsList from '../InstructionsList';
import VariableDeclarationsList from '../VariableDeclarationsList';
import classNames from 'classnames';
import {
selectableArea,
@@ -11,6 +12,7 @@ import {
instructionParameter,
nameAndIconContainer,
icon,
eventLabel,
} from '../ClassNames';
import InlinePopover from '../../InlinePopover';
import AnyVariableField from '../../ParameterFields/AnyVariableField';
@@ -56,10 +58,10 @@ const styles = {
marginLeft: '3px',
marginRight: '2px',
},
eventLabel: {
marginLeft: '5px',
whiteSpace: 'normal',
wordWrap: 'break-word',
labelText: {
// Add an extra pixel so that it alignes with variables and other event labels
// (which are selectable areas, so has an extra pixel of border)
paddingLeft: '1px',
},
};
@@ -209,12 +211,26 @@ export default class ForEachChildVariableEvent extends React.Component<
[largeSelectedArea]: this.props.selected,
})}
>
<div style={styles.eventLabel}>
<VariableDeclarationsList
variablesContainer={forEachChildVariableEvent.getVariables()}
loopIndexVariableName={forEachChildVariableEvent.getLoopIndexVariableName()}
onVariableDeclarationClick={this.props.onVariableDeclarationClick}
onVariableDeclarationDoubleClick={
this.props.onVariableDeclarationDoubleClick
}
className={'local-variables-container'}
disabled={this.props.disabled}
screenType={this.props.screenType}
windowSize={this.props.windowSize}
idPrefix={this.props.idPrefix}
/>
<div className={eventLabel}>
<Trans>
<span
className={classNames({
[disabledText]: this.props.disabled,
})}
style={styles.labelText}
>
For every child in
<span
@@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import InstructionsList from '../InstructionsList';
import VariableDeclarationsList from '../VariableDeclarationsList';
import classNames from 'classnames';
import {
largeSelectedArea,
@@ -10,6 +11,7 @@ import {
instructionParameter,
nameAndIconContainer,
instructionInvalidParameter,
eventLabel,
} from '../ClassNames';
import InlinePopover from '../../InlinePopover';
import ObjectField from '../../ParameterFields/ObjectField';
@@ -118,7 +120,20 @@ export default class ForEachEvent extends React.Component<
[executableEventContainer]: true,
})}
>
<div>
<VariableDeclarationsList
variablesContainer={forEachEvent.getVariables()}
loopIndexVariableName={forEachEvent.getLoopIndexVariableName()}
onVariableDeclarationClick={this.props.onVariableDeclarationClick}
onVariableDeclarationDoubleClick={
this.props.onVariableDeclarationDoubleClick
}
className={'local-variables-container'}
disabled={this.props.disabled}
screenType={this.props.screenType}
windowSize={this.props.windowSize}
idPrefix={this.props.idPrefix}
/>
<div className={eventLabel}>
<Trans>
Repeat for each instance of
<span
@@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import InstructionsList from '../InstructionsList';
import VariableDeclarationsList from '../VariableDeclarationsList';
import classNames from 'classnames';
import {
largeSelectedArea,
@@ -10,6 +11,7 @@ import {
disabledText,
instructionParameter,
instructionInvalidParameter,
eventLabel,
} from '../ClassNames';
import InlinePopover from '../../InlinePopover';
import ExpressionField from '../../ParameterFields/ExpressionField';
@@ -122,7 +124,20 @@ export default class RepeatEvent extends React.Component<
[executableEventContainer]: true,
})}
>
<div>
<VariableDeclarationsList
variablesContainer={repeatEvent.getVariables()}
loopIndexVariableName={repeatEvent.getLoopIndexVariableName()}
onVariableDeclarationClick={this.props.onVariableDeclarationClick}
onVariableDeclarationDoubleClick={
this.props.onVariableDeclarationDoubleClick
}
className={'local-variables-container'}
disabled={this.props.disabled}
screenType={this.props.screenType}
windowSize={this.props.windowSize}
idPrefix={this.props.idPrefix}
/>
<div className={eventLabel}>
<span
className={classNames({
[selectableArea]: true,
@@ -1,6 +1,7 @@
// @flow
import * as React from 'react';
import InstructionsList from '../InstructionsList';
import VariableDeclarationsList from '../VariableDeclarationsList';
import classNames from 'classnames';
import {
largeSelectedArea,
@@ -8,6 +9,7 @@ import {
executableEventContainer,
disabledText,
conditionsContainer,
eventLabel,
} from '../ClassNames';
import { type EventRendererProps } from './EventRenderer';
import ConditionsActionsColumns from '../ConditionsActionsColumns';
@@ -40,9 +42,23 @@ export default class WhileEvent extends React.Component<EventRendererProps, *> {
[executableEventContainer]: true,
})}
>
<VariableDeclarationsList
variablesContainer={whileEvent.getVariables()}
loopIndexVariableName={whileEvent.getLoopIndexVariableName()}
onVariableDeclarationClick={this.props.onVariableDeclarationClick}
onVariableDeclarationDoubleClick={
this.props.onVariableDeclarationDoubleClick
}
className={'local-variables-container'}
disabled={this.props.disabled}
screenType={this.props.screenType}
windowSize={this.props.windowSize}
idPrefix={this.props.idPrefix}
/>
<div
className={classNames({
[disabledText]: this.props.disabled,
[eventLabel]: true,
})}
>
<Trans>While these conditions are true:</Trans>
@@ -81,6 +97,7 @@ export default class WhileEvent extends React.Component<EventRendererProps, *> {
<div
className={classNames({
[disabledText]: this.props.disabled,
[eventLabel]: true,
})}
>
<Trans>Repeat these:</Trans>
@@ -108,6 +108,9 @@ ${actions}`,
},
'BuiltinCommonInstructions::While': ({ event, padding }) => {
const whileEvent = gd.asWhileEvent(event);
const indexVarText = whileEvent.getLoopIndexVariableName()
? ` (loop index variable: \`${whileEvent.getLoopIndexVariableName()}\`)`
: '';
const whileConditions = renderInstructionsAsText({
instructionsList: whileEvent.getWhileConditions(),
padding: padding + ' ',
@@ -125,7 +128,7 @@ ${actions}`,
});
return {
content: `${padding}While these conditions are true:
content: `${padding}While these conditions are true${indexVarText}:
${whileConditions}
${padding}Then do:
${padding}Conditions:
@@ -136,6 +139,9 @@ ${actions}`,
},
'BuiltinCommonInstructions::Repeat': ({ event, padding }) => {
const repeatEvent = gd.asRepeatEvent(event);
const indexVarText = repeatEvent.getLoopIndexVariableName()
? ` (loop index variable: \`${repeatEvent.getLoopIndexVariableName()}\`)`
: '';
const conditions = renderInstructionsAsText({
instructionsList: repeatEvent.getConditions(),
padding: padding + ' ',
@@ -150,7 +156,7 @@ ${actions}`,
return {
content: `${padding}Repeat \`${repeatEvent
.getRepeatExpression()
.getPlainString()}\` times these:
.getPlainString()}\`${indexVarText} times these:
${padding}Conditions:
${conditions}
${padding}Actions:
@@ -159,6 +165,9 @@ ${actions}`,
},
'BuiltinCommonInstructions::ForEach': ({ event, padding }) => {
const forEachEvent = gd.asForEachEvent(event);
const indexVarText = forEachEvent.getLoopIndexVariableName()
? ` (loop index variable: \`${forEachEvent.getLoopIndexVariableName()}\`)`
: '';
const conditions = renderInstructionsAsText({
instructionsList: forEachEvent.getConditions(),
padding: padding + ' ',
@@ -171,7 +180,7 @@ ${actions}`,
});
return {
content: `${padding}Repeat these separately for each instance of ${forEachEvent.getObjectToPick()}:
content: `${padding}Repeat these separately for each instance of ${forEachEvent.getObjectToPick()}${indexVarText}:
${padding}Conditions:
${conditions}
${padding}Actions:
@@ -180,6 +189,9 @@ ${actions}`,
},
'BuiltinCommonInstructions::ForEachChildVariable': ({ event, padding }) => {
const forEachChildVariableEvent = gd.asForEachChildVariableEvent(event);
const indexVarText = forEachChildVariableEvent.getLoopIndexVariableName()
? ` (loop index variable: \`${forEachChildVariableEvent.getLoopIndexVariableName()}\`)`
: '';
const valueIteratorName = forEachChildVariableEvent.getValueIteratorVariableName();
const keyIteratorName = forEachChildVariableEvent.getKeyIteratorVariableName();
const iterableName = forEachChildVariableEvent.getIterableVariableName();
@@ -198,7 +210,7 @@ ${actions}`,
content: `${padding}For each child in \`${iterableName ||
'(no variable chosen yet)'}\`, store the child in variable \`${valueIteratorName ||
'(ignored)'}\`, the child name in \`${keyIteratorName ||
'(ignored)'}\` and do:
'(ignored)'}\` and do${indexVarText}:
${padding}Conditions:
${padding}${conditions}
${padding}Actions:
@@ -30,6 +30,7 @@ export const reactDndInstructionType = 'GD_DRAGGED_VARIABLE_DECLARATION';
type Props = {|
variableName: string,
variable: gdVariable,
isLoopIndexVariable?: boolean,
onClick: Function,
onDoubleClick: () => void,
selected: boolean,
@@ -78,7 +79,7 @@ const getVariableValueAsString = (variable: gdVariable, i18n: I18nType) => {
};
export const VariableDeclaration = (props: Props) => {
const { variableName, variable, id } = props;
const { variableName, variable, id, isLoopIndexVariable } = props;
/**
* Render the different parts of the text of the instruction.
* Parameter can have formatting, be hovered and clicked. The rest
@@ -89,6 +90,29 @@ export const VariableDeclaration = (props: Props) => {
const VariableTypeIcon = getVariableTypeIcon(variable.getType());
if (isLoopIndexVariable) {
return (
<span
className={classNames({
[disabledText]: disabled,
})}
>
<Trans>
Use{' '}
<span>
<LocalVariableIcon
className={classNames({
[icon]: true,
})}
/>
{variableName}
</span>{' '}
as the loop counter
</Trans>
</span>
);
}
return (
<span
className={classNames({
@@ -8,6 +8,7 @@ import { VariableDeclaration } from './VariableDeclaration';
type Props = {|
variablesContainer: gdVariablesContainer,
loopIndexVariableName?: ?string,
onVariableDeclarationClick: VariableDeclarationContext => void,
onVariableDeclarationDoubleClick: VariableDeclarationContext => void,
className?: string,
@@ -22,6 +23,7 @@ type Props = {|
export default function VariableDeclarationsList({
variablesContainer,
loopIndexVariableName,
onVariableDeclarationClick,
onVariableDeclarationDoubleClick,
className,
@@ -38,11 +40,14 @@ export default function VariableDeclarationsList({
variablesContainer,
variableName,
};
const isLoopIndexVariable =
!!loopIndexVariableName && variableName === loopIndexVariableName;
return (
<VariableDeclaration
variableName={variableName}
variable={variable}
isLoopIndexVariable={isLoopIndexVariable}
key={variable.ptr}
onClick={() => onVariableDeclarationClick(variableDeclarationContext)}
onDoubleClick={() =>
+139
View File
@@ -21,6 +21,7 @@ import {
serializeToJSObject,
unserializeFromJSObject,
} from '../Utils/Serializer';
import newNameGenerator from '../Utils/NewNameGenerator';
import {
type HistoryState,
type RevertableActionType,
@@ -127,6 +128,12 @@ import { useHighlightedAiGeneratedEvent } from './UseHighlightedAiGeneratedEvent
const gd: libGDevelop = global.gd;
const zoomLevel = { min: 1, max: 50 };
const loopEventTypes = [
'BuiltinCommonInstructions::While',
'BuiltinCommonInstructions::Repeat',
'BuiltinCommonInstructions::ForEach',
'BuiltinCommonInstructions::ForEachChildVariable',
];
export type ChangeContext = {|
events?: Array<EventContext>,
@@ -898,6 +905,12 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
this._replaceSelectedEventType('BuiltinCommonInstructions::Standard'),
visible: this._selectionIsElseEvent(),
},
{
label: i18n._(t`Remove the Loop Counter Variable`),
click: () => this._removeLoopIndexVariable(),
visible:
this._selectionIsLoopEvent() && this._selectionHasIndexVariable(),
},
{ type: 'separator' },
{
label: i18n._(t`Add`),
@@ -919,6 +932,9 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
this.props.shortcutMap['ADD_SUBEVENT']
),
},
{
type: 'separator',
},
{
label: i18n._(t`Local Variable`),
click: () => this.addLocalVariable(),
@@ -927,6 +943,15 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
this.props.shortcutMap['ADD_LOCAL_VARIABLE']
),
},
{
label: i18n._(t`Loop Counter Variable`),
click: () => this._addLoopIndexVariable(),
visible:
this._selectionIsLoopEvent() && !this._selectionHasIndexVariable(),
},
{
type: 'separator',
},
{
label: i18n._(t`Comment`),
click: () => {
@@ -1037,6 +1062,97 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
);
};
_asLoopEvent = (event: gdBaseEvent): any | null => {
const eventType = event.getType();
if (eventType === 'BuiltinCommonInstructions::While')
return gd.asWhileEvent(event);
if (eventType === 'BuiltinCommonInstructions::Repeat')
return gd.asRepeatEvent(event);
if (eventType === 'BuiltinCommonInstructions::ForEach')
return gd.asForEachEvent(event);
if (eventType === 'BuiltinCommonInstructions::ForEachChildVariable')
return gd.asForEachChildVariableEvent(event);
return null;
};
_getLastSelectedLoopEventContext = (): EventContext | null => {
const eventContext = getLastSelectedEventContext(this.state.selection);
if (!eventContext) return null;
if (!loopEventTypes.includes(eventContext.event.getType())) return null;
return eventContext;
};
_selectionIsLoopEvent = () => {
return !!this._getLastSelectedLoopEventContext();
};
_selectionHasIndexVariable = () => {
const eventContext = this._getLastSelectedLoopEventContext();
if (!eventContext) return false;
const loopEvent = this._asLoopEvent(eventContext.event);
return !!loopEvent && loopEvent.getLoopIndexVariableName() !== '';
};
_addLoopIndexVariable = () => {
const eventContext = this._getLastSelectedLoopEventContext();
if (!eventContext) return;
const loopEvent = this._asLoopEvent(eventContext.event);
if (!loopEvent || loopEvent.getLoopIndexVariableName() !== '') return;
const projectScopedContainersAccessor =
eventContext.projectScopedContainersAccessor;
const generatedName = newNameGenerator('LoopIndex', name =>
projectScopedContainersAccessor
.get()
.getVariablesContainersList()
.has(name)
);
const variablesContainer = loopEvent.getVariables();
variablesContainer
.insertNew(generatedName, variablesContainer.count())
.setValue(0);
loopEvent.setLoopIndexVariableName(generatedName);
if (this._eventsTree) {
this._eventsTree.forceEventsUpdate(() => {
const positions = this._getChangedEventRows([eventContext.event]);
this._saveChangesToHistory('EDIT', {
positionsBeforeAction: positions,
positionAfterAction: positions,
});
});
}
};
_removeLoopIndexVariable = () => {
const eventContext = this._getLastSelectedLoopEventContext();
if (!eventContext) return;
const loopEvent = this._asLoopEvent(eventContext.event);
if (!loopEvent) return;
const loopIndexVariableName = loopEvent.getLoopIndexVariableName();
if (!loopIndexVariableName) return;
const variablesContainer = loopEvent.getVariables();
if (variablesContainer.has(loopIndexVariableName)) {
variablesContainer.remove(loopIndexVariableName);
}
loopEvent.setLoopIndexVariableName('');
if (this._eventsTree) {
this._eventsTree.forceEventsUpdate(() => {
const positions = this._getChangedEventRows([eventContext.event]);
this._saveChangesToHistory('EDIT', {
positionsBeforeAction: positions,
positionAfterAction: positions,
});
});
}
};
_replaceSelectedEventType = (eventType: string) => {
const eventContext = getLastSelectedEventContext(this.state.selection);
if (!eventContext) return;
@@ -2001,6 +2117,10 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
.editedParameter.eventContext
? this.state.editedParameter.eventContext.projectScopedContainersAccessor
: projectScopedContainersAccessor;
const editedVariableLoopEvent =
this.state.editedVariable && this.state.editedVariable.eventContext
? this._asLoopEvent(this.state.editedVariable.eventContext.event)
: null;
// Memorize the last size of the container div, that is used to render the events tree.
// When the events editor tab is hidden, the container div width/height are 0.
@@ -2295,6 +2415,25 @@ export class EventsSheetComponentWithoutHandle extends React.Component<
this.state.editedVariable.shouldCreateVariable
}
isListLocked={false}
loopIndexVariableName={
editedVariableLoopEvent
? editedVariableLoopEvent.getLoopIndexVariableName()
: ''
}
onRenameLoopIndexVariable={
editedVariableLoopEvent
? newName => {
editedVariableLoopEvent.setLoopIndexVariableName(newName);
}
: undefined
}
onRemoveLoopIndexVariable={
editedVariableLoopEvent
? () => {
editedVariableLoopEvent.setLoopIndexVariableName('');
}
: undefined
}
/>
)}
{this.state.layoutVariablesDialogOpen && (
@@ -101,8 +101,8 @@
.gd-events-sheet .local-variables-container {
background: var(--event-sheet-actions-background-color);
color: var(--event-sheet-actions-color);
padding-left: 5px;
padding-right: 5px;
padding-left: 4px;
padding-right: 4px;
/* Prevent container to growing outside its parent. We don't have a way to prevent an infinitely
* small variables column, but responsive design should avoid the problem on small screens. */
min-width: 0;
@@ -232,7 +232,7 @@
}
.gd-events-sheet .else-text-container {
padding-left: 1px;
padding-left: 5px;
}
.gd-events-sheet .invalid-else {
@@ -242,6 +242,12 @@
text-decoration: line-through;
}
.gd-events-sheet .event-label {
margin-left: 5px;
white-space: normal;
word-wrap: break-word;
}
/**
* Search results highlight
*/
@@ -9,6 +9,9 @@ type Props = {|
project: gdProject,
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
variablesContainer: gdVariablesContainer,
loopIndexVariableName?: ?string,
onRenameLoopIndexVariable?: (newName: string) => void,
onRemoveLoopIndexVariable?: () => void,
onApply: (selectedVariableName: string | null) => void,
onCancel: () => void,
initiallySelectedVariableName: string,
@@ -20,6 +23,9 @@ const LocalVariablesDialog = ({
project,
projectScopedContainersAccessor,
variablesContainer,
loopIndexVariableName,
onRenameLoopIndexVariable,
onRemoveLoopIndexVariable,
open,
onCancel,
onApply,
@@ -34,9 +40,17 @@ const LocalVariablesDialog = ({
label: '',
variablesContainer,
onComputeAllVariableNames: () => [],
loopIndexVariableName: loopIndexVariableName || '',
onRenameLoopIndexVariable,
onRemoveLoopIndexVariable,
},
],
[variablesContainer]
[
variablesContainer,
loopIndexVariableName,
onRenameLoopIndexVariable,
onRemoveLoopIndexVariable,
]
);
return (
@@ -26,6 +26,9 @@ type TabProps = {
label: React.Node,
variablesContainer: gdVariablesContainer,
inheritedVariablesContainer?: gdVariablesContainer,
loopIndexVariableName?: string,
onRenameLoopIndexVariable?: (newName: string) => void,
onRemoveLoopIndexVariable?: () => void,
emptyPlaceholderTitle?: React.Node,
emptyPlaceholderDescription?: React.Node,
/**
@@ -267,6 +270,9 @@ const VariablesEditorDialog = ({
id,
variablesContainer,
inheritedVariablesContainer,
loopIndexVariableName,
onRenameLoopIndexVariable,
onRemoveLoopIndexVariable,
emptyPlaceholderTitle,
emptyPlaceholderDescription,
onComputeAllVariableNames,
@@ -298,6 +304,9 @@ const VariablesEditorDialog = ({
onVariablesUpdated={notifyOfChange}
onSelectedVariableChange={onSelectedVariableChange}
isListLocked={isListLocked}
loopIndexVariableName={loopIndexVariableName}
onRenameLoopIndexVariable={onRenameLoopIndexVariable}
onRemoveLoopIndexVariable={onRemoveLoopIndexVariable}
/>
</Column>
)
+136 -16
View File
@@ -84,6 +84,7 @@ import { MultilineVariableEditorDialog } from './MultilineVariableEditorDialog';
import { MarkdownText } from '../UI/MarkdownText';
import Paper from '../UI/Paper';
import { ProjectScopedContainersAccessor } from '../InstructionOrExpression/EventsScope';
import ContextMenu, { type ContextMenuInterface } from '../UI/Menu/ContextMenu';
const gd: libGDevelop = global.gd;
@@ -117,6 +118,9 @@ type PlaceholderProps =
type Props = {|
projectScopedContainersAccessor: ProjectScopedContainersAccessor,
variablesContainer: gdVariablesContainer,
loopIndexVariableName?: string,
onRenameLoopIndexVariable?: (newName: string) => void,
onRemoveLoopIndexVariable?: () => void,
areObjectVariables?: boolean,
inheritedVariablesContainer?: gdVariablesContainer,
initiallySelectedVariableName?: ?string,
@@ -179,6 +183,8 @@ type VariableRowProps = {|
type: Variable_Type,
typeErrorMessage: MessageDescriptor | null,
onChangeType: (string, nodeId: string) => void,
isLoopIndexVariable: boolean,
onRemoveLoopIndexVariable: () => void,
hasMixedValues: boolean,
valueAsString: string | null,
valueAsBool: boolean | null,
@@ -220,6 +226,8 @@ const VariableRow = React.memo<VariableRowProps>(
isTopLevel,
type,
typeErrorMessage,
isLoopIndexVariable,
onRemoveLoopIndexVariable,
onChangeType,
hasMixedValues,
valueAsString,
@@ -255,6 +263,7 @@ const VariableRow = React.memo<VariableRowProps>(
const hasLineBreaks = valueAsString
? valueAsString.indexOf('\n') !== -1
: false;
const contextMenu = React.useRef<?ContextMenuInterface>(null);
const getContainerYPosition = React.useCallback(() => {
if (containerRef.current) {
@@ -270,7 +279,7 @@ const VariableRow = React.memo<VariableRowProps>(
draggedNodeId.current = nodeId;
return {};
}}
canDrag={() => !isInherited}
canDrag={() => !isInherited && !isLoopIndexVariable}
canDrop={() => canDrop(nodeId)}
drop={() => {
dropNode(nodeId, whereToDrop);
@@ -308,6 +317,14 @@ const VariableRow = React.memo<VariableRowProps>(
const shouldMultiSelect = event.metaKey || event.ctrlKey;
onSelect(shouldMultiSelect, nodeId);
}}
onContextMenu={event => {
if (!isLoopIndexVariable) return;
event.preventDefault();
event.stopPropagation();
if (contextMenu.current) {
contextMenu.current.open(event.clientX, event.clientY);
}
}}
>
{isOver && whereToDrop === 'before' && (
<DropIndicator canDrop={canDrop} />
@@ -386,7 +403,9 @@ const VariableRow = React.memo<VariableRowProps>(
<Line noMargin alignItems="center">
<Column noMargin>
<VariableTypeSelector
variableType={type}
variableType={
isLoopIndexVariable ? gd.Variable.Number : type
}
onChange={onChangeType}
nodeId={nodeId}
isHighlighted={isSelected}
@@ -395,11 +414,33 @@ const VariableRow = React.memo<VariableRowProps>(
}
id={`variable-${index}-type`}
errorMessage={typeErrorMessage}
disabled={isTypeLocked}
disabled={isTypeLocked || isLoopIndexVariable}
/>
</Column>
<Column expand>
{type === gd.Variable.Boolean ? (
{isLoopIndexVariable ? (
<Line noMargin alignItems="center">
<span
style={
isSelected
? {
color:
gdevelopTheme.listItem
.selectedTextColor,
}
: undefined
}
>
<Text
displayInlineAsSpan
noMargin
color="inherit"
>
<Trans>Counter of the loop</Trans>
</Text>
</span>
</Line>
) : type === gd.Variable.Boolean ? (
<Line noMargin alignItems="center">
<span
style={
@@ -594,6 +635,17 @@ const VariableRow = React.memo<VariableRowProps>(
}}
/>
)}
{isLoopIndexVariable && (
<ContextMenu
ref={contextMenu}
buildMenuTemplate={i18n => [
{
label: i18n._(t`Remove this counter of the loop`),
click: onRemoveLoopIndexVariable,
},
]}
/>
)}
</div>
)
}
@@ -615,6 +667,16 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
})
);
const {
historyHandler,
onVariablesUpdated,
variablesContainer,
onRemoveLoopIndexVariable,
onRenameLoopIndexVariable,
areObjectVariables,
projectScopedContainersAccessor,
} = props;
const [searchMatchingNodes, setSearchMatchingNodes] = React.useState<
Array<string>
>([]);
@@ -640,6 +702,17 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
const gdevelopTheme = React.useContext(GDevelopThemeContext);
const draggedNodeId = React.useRef<?string>(null);
const forceUpdate = useForceUpdate();
const [
currentIndexVariableName,
setCurrentIndexVariableName,
] = React.useState(props.loopIndexVariableName || '');
React.useEffect(
() => {
setCurrentIndexVariableName(props.loopIndexVariableName || '');
},
[props.loopIndexVariableName]
);
const [searchText, setSearchText] = React.useState<string>('');
const { onComputeAllVariableNames, onSelectedVariableChange } = props;
@@ -742,7 +815,6 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
})
: [];
const { historyHandler, onVariablesUpdated, variablesContainer } = props;
const _onChange = React.useCallback(
() => {
if (historyHandler) historyHandler.saveToHistory();
@@ -963,12 +1035,18 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
if (nodeId.startsWith(inheritedPrefix)) return false;
const { name, lineage } = getVariableContextFromNodeId(
nodeId,
props.variablesContainer
variablesContainer
);
if (!name) return false;
const parentVariable = getDirectParentVariable(lineage);
if (!parentVariable) {
props.variablesContainer.remove(name);
variablesContainer.remove(name);
if (name === currentIndexVariableName) {
if (onRemoveLoopIndexVariable) {
onRemoveLoopIndexVariable();
}
setCurrentIndexVariableName('');
}
} else {
if (parentVariable.getType() === gd.Variable.Array) {
parentVariable.removeAtIndex(parseInt(name, 10));
@@ -978,7 +1056,7 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
}
return true;
},
[props.variablesContainer]
[currentIndexVariableName, onRemoveLoopIndexVariable, variablesContainer]
);
const deleteNode = React.useCallback(
@@ -992,6 +1070,32 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
[_onChange, forceUpdate, _deleteNode]
);
const removeLoopIndexVariable = React.useCallback(
(nodeId: string): void => {
if (!currentIndexVariableName) return;
const { name, depth } = getVariableContextFromNodeId(
nodeId,
props.variablesContainer
);
if (depth !== 0 || !name || name !== currentIndexVariableName) return;
const success = _deleteNode(nodeId);
if (!success) return;
_onChange();
setSelectedNodes([]);
forceUpdate();
},
[
currentIndexVariableName,
forceUpdate,
_deleteNode,
_onChange,
props.variablesContainer,
setSelectedNodes,
]
);
const deleteSelection = React.useCallback(
() => {
// Take advantage of the node ids notation to sort them in
@@ -1467,6 +1571,11 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
}
const parentType = parentVariable ? parentVariable.getType() : null;
const isSelected = selectedNodes.includes(nodeId);
const isLoopIndexVariable =
!isInherited &&
isTopLevel &&
!!currentIndexVariableName &&
name === currentIndexVariableName;
const overwritesInheritedVariable =
isTopLevel &&
!isInherited &&
@@ -1547,6 +1656,8 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
isTopLevel={isTopLevel}
type={type}
typeErrorMessage={typeErrorMessage}
isLoopIndexVariable={isLoopIndexVariable}
onRemoveLoopIndexVariable={() => removeLoopIndexVariable(nodeId)}
variablePointer={variablePointer}
onChangeType={onChangeType}
hasMixedValues={hasMixedValues}
@@ -1625,7 +1736,7 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
const { variable, lineage, name } = getVariableContextFromNodeId(
nodeId,
props.variablesContainer
variablesContainer
);
if (name === null || !variable || newName === name) return;
@@ -1655,20 +1766,27 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
tentativeNewName =>
(parentVariable && parentVariable.hasChild(tentativeNewName)) ||
(!parentVariable &&
(props.variablesContainer.has(tentativeNewName) ||
(!props.areObjectVariables &&
props.projectScopedContainersAccessor
(variablesContainer.has(tentativeNewName) ||
(!areObjectVariables &&
projectScopedContainersAccessor
.get()
.getObjectsContainersList()
.hasObjectOrGroupNamed(tentativeNewName))))
);
if (!parentVariable) {
props.variablesContainer.rename(name, safeAndUniqueNewName);
variablesContainer.rename(name, safeAndUniqueNewName);
} else {
parentVariable.renameChild(name, safeAndUniqueNewName);
}
if (!parentVariable && name === currentIndexVariableName) {
setCurrentIndexVariableName(safeAndUniqueNewName);
if (onRenameLoopIndexVariable) {
onRenameLoopIndexVariable(safeAndUniqueNewName);
}
}
_onChange();
updateExpandedAndSelectedNodesFollowingNameChange(
nodeId,
@@ -1677,10 +1795,12 @@ const VariablesList = React.forwardRef<Props, VariablesListInterface>(
refocusNameField({ identifier: variable.ptr, caretPosition });
},
[
props.variablesContainer,
props.areObjectVariables,
props.projectScopedContainersAccessor,
variablesContainer,
areObjectVariables,
projectScopedContainersAccessor,
currentIndexVariableName,
_onChange,
onRenameLoopIndexVariable,
updateExpandedAndSelectedNodesFollowingNameChange,
refocusNameField,
]
+21 -6
View File
@@ -473,24 +473,39 @@ export const makeTestProject = (gd /*: libGDevelop */) /*: TestProject */ => {
const evt2 = testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::Standard', 1);
// Valid Else event (follows a Standard event)
testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::ForEach', 2);
.insertNewEvent(project, 'BuiltinCommonInstructions::Else', 2);
testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::While', 3);
.insertNewEvent(project, 'BuiltinCommonInstructions::ForEach', 3);
testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::Repeat', 4);
.insertNewEvent(project, 'BuiltinCommonInstructions::While', 4);
testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::Repeat', 5);
testLayout
.getEvents()
.insertNewEvent(
project,
'BuiltinCommonInstructions::ForEachChildVariable',
6
);
// Invalid Else event (follows a ForEachChildVariable, not a Standard event)
testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::Else', 7);
var evt6 = testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::Group', 5);
.insertNewEvent(project, 'BuiltinCommonInstructions::Group', 8);
testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::Link', 6);
.insertNewEvent(project, 'BuiltinCommonInstructions::Link', 9);
var evt8 = testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::JsCode', 7);
.insertNewEvent(project, 'BuiltinCommonInstructions::JsCode', 10);
var evtWithInvalidParameters = testLayout
.getEvents()
.insertNewEvent(project, 'BuiltinCommonInstructions::Standard', 0);