mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Compare commits
27 Commits
data-url-e
...
v5.0.138
Author | SHA1 | Date | |
---|---|---|---|
![]() |
81ef11163d | ||
![]() |
fcc19a6dcf | ||
![]() |
bcad2d5667 | ||
![]() |
3c83e5d24a | ||
![]() |
d07088900f | ||
![]() |
fcb0c27e23 | ||
![]() |
a6ee1c3e1c | ||
![]() |
5669eae198 | ||
![]() |
8f97a5ba69 | ||
![]() |
d57a755b2f | ||
![]() |
5118421de7 | ||
![]() |
ff15a37da7 | ||
![]() |
beac19089f | ||
![]() |
e86e4ef9f5 | ||
![]() |
693a2dbd2c | ||
![]() |
23318c2c28 | ||
![]() |
3e811f1b9d | ||
![]() |
9ea6c034c3 | ||
![]() |
b0ac0b1254 | ||
![]() |
e8b34ca535 | ||
![]() |
70226f45aa | ||
![]() |
b1572102c7 | ||
![]() |
63da874469 | ||
![]() |
0963f2b8f6 | ||
![]() |
5aa4fd5739 | ||
![]() |
11c0248df5 | ||
![]() |
5bdc9769df |
@@ -29,7 +29,7 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
|
||||
extension
|
||||
.AddExpressionAndConditionAndAction(
|
||||
"number",
|
||||
"CameraX",
|
||||
"CameraCenterX",
|
||||
_("Camera center X position"),
|
||||
_("the X position of the center of a camera"),
|
||||
_("the X position of camera _PARAM4_ (layer: _PARAM3_)"),
|
||||
@@ -43,15 +43,25 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
|
||||
.SetDefaultValue("0")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
// Compatibility with GD <= 5.0.135
|
||||
extension.AddDuplicatedCondition("CameraX", "CameraCenterX")
|
||||
.SetHidden(); // Deprecated
|
||||
extension.AddDuplicatedExpression("CameraX", "CameraCenterX")
|
||||
.SetHidden(); // Deprecated
|
||||
extension.AddDuplicatedAction("SetCameraX", "SetCameraCenterX")
|
||||
.SetHidden(); // Deprecated
|
||||
|
||||
extension.AddDuplicatedAction("CameraX", "SetCameraX")
|
||||
.SetHidden(); // Deprecated
|
||||
|
||||
extension.AddDuplicatedExpression("VueX", "CameraX")
|
||||
.SetHidden(); // Deprecated
|
||||
// end of compatibility code
|
||||
|
||||
extension
|
||||
.AddExpressionAndConditionAndAction(
|
||||
"number",
|
||||
"CameraY",
|
||||
"CameraCenterY",
|
||||
_("Camera center Y position"),
|
||||
_("the Y position of the center of a camera"),
|
||||
_("the Y position of camera _PARAM4_ (layer: _PARAM3_)"),
|
||||
@@ -65,10 +75,20 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
|
||||
.SetDefaultValue("0")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
// Compatibility with GD <= 5.0.135
|
||||
extension.AddDuplicatedCondition("CameraY", "CameraCenterY")
|
||||
.SetHidden(); // Deprecated
|
||||
extension.AddDuplicatedExpression("CameraY", "CameraCenterY")
|
||||
.SetHidden(); // Deprecated
|
||||
extension.AddDuplicatedAction("SetCameraY", "SetCameraCenterY")
|
||||
.SetHidden(); // Deprecated
|
||||
|
||||
extension.AddDuplicatedAction("CameraY", "SetCameraY")
|
||||
.SetHidden(); // Deprecated
|
||||
|
||||
extension.AddDuplicatedExpression("VueY", "CameraY")
|
||||
.SetHidden(); // Deprecated
|
||||
// end of compatibility code
|
||||
|
||||
extension
|
||||
.AddExpressionAndCondition(
|
||||
@@ -80,9 +100,9 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
|
||||
"",
|
||||
"res/conditions/camera24.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"))
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
|
||||
.SetDefaultValue("\"\"")
|
||||
.AddParameter("expression", _("Camera number"))
|
||||
.AddParameter("expression", _("Camera number"), "", true)
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
@@ -96,9 +116,77 @@ void GD_CORE_API BuiltinExtensionsImplementer::ImplementsCameraExtension(
|
||||
"",
|
||||
"res/conditions/camera24.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"))
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
|
||||
.SetDefaultValue("\"\"")
|
||||
.AddParameter("expression", _("Camera number"))
|
||||
.AddParameter("expression", _("Camera number"), "", true)
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddExpressionAndCondition(
|
||||
"number",
|
||||
"CameraBorderLeft",
|
||||
_("Camera left border position"),
|
||||
_("the position of the left border of a camera"),
|
||||
_("the position of the left border of camera _PARAM2_ of layer "
|
||||
"_PARAM1_"),
|
||||
"",
|
||||
"res/conditions/camera24.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
|
||||
.SetDefaultValue("\"\"")
|
||||
.AddParameter("expression", _("Camera number"), "", true)
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddExpressionAndCondition(
|
||||
"number",
|
||||
"CameraBorderRight",
|
||||
_("Camera right border position"),
|
||||
_("the position of the right border of a camera"),
|
||||
_("the position of the right border of camera _PARAM2_ of layer "
|
||||
"_PARAM1_"),
|
||||
"",
|
||||
"res/conditions/camera24.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
|
||||
.SetDefaultValue("\"\"")
|
||||
.AddParameter("expression", _("Camera number"), "", true)
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddExpressionAndCondition(
|
||||
"number",
|
||||
"CameraBorderTop",
|
||||
_("Camera top border position"),
|
||||
_("the position of the top border of a camera"),
|
||||
_("the position of the top border of camera _PARAM2_ of layer "
|
||||
"_PARAM1_"),
|
||||
"",
|
||||
"res/conditions/camera24.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
|
||||
.SetDefaultValue("\"\"")
|
||||
.AddParameter("expression", _("Camera number"), "", true)
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
extension
|
||||
.AddExpressionAndCondition(
|
||||
"number",
|
||||
"CameraBorderBottom",
|
||||
_("Camera bottom border position"),
|
||||
_("the position of the bottom border of a camera"),
|
||||
_("the position of the bottom border of camera _PARAM2_ of layer "
|
||||
"_PARAM1_"),
|
||||
"",
|
||||
"res/conditions/camera24.png")
|
||||
.AddCodeOnlyParameter("currentScene", "")
|
||||
.AddParameter("layer", _("Layer (base layer if empty)"), "", true)
|
||||
.SetDefaultValue("\"\"")
|
||||
.AddParameter("expression", _("Camera number"), "", true)
|
||||
.UseStandardParameters("number")
|
||||
.MarkAsAdvanced();
|
||||
|
||||
|
@@ -380,7 +380,7 @@ gd::InstructionMetadata& PlatformExtension::AddDuplicatedAction(
|
||||
|
||||
auto copiedAction = actionsInfos.find(copiedNameWithNamespace);
|
||||
if (copiedAction == actionsInfos.end()) {
|
||||
gd::LogWarning("Could not find an action with name " +
|
||||
gd::LogError("Could not find an action with name " +
|
||||
copiedNameWithNamespace + " to copy.");
|
||||
} else {
|
||||
actionsInfos[newNameWithNamespace] = copiedAction->second;
|
||||
@@ -400,7 +400,7 @@ gd::InstructionMetadata& PlatformExtension::AddDuplicatedCondition(
|
||||
|
||||
auto copiedCondition = conditionsInfos.find(copiedNameWithNamespace);
|
||||
if (copiedCondition == conditionsInfos.end()) {
|
||||
gd::LogWarning("Could not find a condition with name " +
|
||||
gd::LogError("Could not find a condition with name " +
|
||||
copiedNameWithNamespace + " to copy.");
|
||||
} else {
|
||||
conditionsInfos[newNameWithNamespace] = copiedCondition->second;
|
||||
@@ -417,7 +417,7 @@ gd::ExpressionMetadata& PlatformExtension::AddDuplicatedExpression(
|
||||
|
||||
auto copiedExpression = expressionsInfos.find(copiedNameWithNamespace);
|
||||
if (copiedExpression == expressionsInfos.end()) {
|
||||
gd::LogWarning("Could not find an expression with name " +
|
||||
gd::LogError("Could not find an expression with name " +
|
||||
copiedNameWithNamespace + " to copy.");
|
||||
} else {
|
||||
expressionsInfos[newNameWithNamespace] = copiedExpression->second;
|
||||
@@ -434,7 +434,7 @@ gd::ExpressionMetadata& PlatformExtension::AddDuplicatedStrExpression(
|
||||
|
||||
auto copiedExpression = strExpressionsInfos.find(copiedNameWithNamespace);
|
||||
if (copiedExpression == strExpressionsInfos.end()) {
|
||||
gd::LogWarning("Could not find a string expression with name " +
|
||||
gd::LogError("Could not find a string expression with name " +
|
||||
copiedNameWithNamespace + " to copy.");
|
||||
} else {
|
||||
strExpressionsInfos[newNameWithNamespace] = copiedExpression->second;
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// @ts-check
|
||||
describe.only('gdjs.AnchorRuntimeBehavior', function () {
|
||||
describe('gdjs.AnchorRuntimeBehavior', function () {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
resources: { resources: [] },
|
||||
|
@@ -133,7 +133,7 @@ module.exports = {
|
||||
'LastSaveError',
|
||||
_('Error of last save attempt'),
|
||||
_('Get the error of the last save attempt.'),
|
||||
_('Error of last save attempt in leaderboard _PARAM0_'),
|
||||
_('Save score'),
|
||||
'JsPlatform/Extensions/leaderboard.svg'
|
||||
)
|
||||
.addParameter('leaderboardId', _('Leaderboard'), '', true)
|
||||
|
@@ -419,7 +419,8 @@ module.exports = {
|
||||
sharedData
|
||||
)
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2runtimebehavior.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/box2d.js');
|
||||
.addIncludeFile('Extensions/Physics2Behavior/box2d.js')
|
||||
.addIncludeFile('Extensions/Physics2Behavior/utils.js');
|
||||
|
||||
// Global
|
||||
aut
|
||||
@@ -1396,8 +1397,8 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('X component (N.m)'))
|
||||
.addParameter('expression', _('Y component (N.m)'))
|
||||
.addParameter('expression', _('X component (in Newton * seconds or kilogram * meter per second)'))
|
||||
.addParameter('expression', _('Y component (in Newton * seconds or kilogram * meter per second)'))
|
||||
.addParameter('expression', _('Applying X position'))
|
||||
.addParameter('expression', _('Applying Y position'))
|
||||
.getCodeExtraInformation()
|
||||
@@ -1420,7 +1421,7 @@ module.exports = {
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Angle'))
|
||||
.addParameter('expression', _('Length (N.m)'))
|
||||
.addParameter('expression', _('Length (in Newton * seconds or kilogram * meter per second)'))
|
||||
.addParameter('expression', _('Applying X position'))
|
||||
.addParameter('expression', _('Applying Y position'))
|
||||
.getCodeExtraInformation()
|
||||
@@ -1442,7 +1443,7 @@ module.exports = {
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('expression', _('Length (N.m)'))
|
||||
.addParameter('expression', _('Length (in Newton * seconds or kilogram * meter per second)'))
|
||||
.addParameter('expression', _('X position'))
|
||||
.addParameter('expression', _('Y position'))
|
||||
.addParameter('expression', _('Applying X position'))
|
||||
@@ -1486,18 +1487,44 @@ module.exports = {
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('applyAngularImpulse');
|
||||
|
||||
aut
|
||||
aut
|
||||
.addExpression(
|
||||
'MassCenterX',
|
||||
_('Mass center X'),
|
||||
_('Mass center X'),
|
||||
'Mass',
|
||||
_('Mass'),
|
||||
_('Return the mass of the object (in kilograms)'),
|
||||
'',
|
||||
'res/physics32.png'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getMassCenterX');
|
||||
.setFunctionName('getMass');
|
||||
|
||||
aut
|
||||
.addExpression(
|
||||
'Inertia',
|
||||
_('Inertia'),
|
||||
_('Return the rotational inertia of the object (in kilograms * meters * meters)'),
|
||||
'',
|
||||
'res/physics32.png'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getInertia');
|
||||
|
||||
aut
|
||||
.addExpression(
|
||||
'MassCenterX',
|
||||
_('Mass center X'),
|
||||
_('Mass center X'),
|
||||
'',
|
||||
'res/physics32.png'
|
||||
)
|
||||
.addParameter('object', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.getCodeExtraInformation()
|
||||
.setFunctionName('getMassCenterX');
|
||||
|
||||
aut
|
||||
.addExpression(
|
||||
@@ -3732,9 +3759,9 @@ module.exports = {
|
||||
.addCondition(
|
||||
'Collision',
|
||||
_('Collision'),
|
||||
_('Test if two objects collide.'),
|
||||
_('Check if two objects collide.'),
|
||||
_('_PARAM0_ is colliding with _PARAM2_'),
|
||||
'',
|
||||
'',
|
||||
'res/physics32.png',
|
||||
'res/physics32.png'
|
||||
)
|
||||
@@ -3746,6 +3773,42 @@ module.exports = {
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.setFunctionName('gdjs.physics2.objectsCollide');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'CollisionStarted',
|
||||
_('Collision started'),
|
||||
_('Check if two objects just started colliding during this frame.'),
|
||||
_('_PARAM0_ started colliding with _PARAM2_'),
|
||||
_('Collision'),
|
||||
'res/physics32.png',
|
||||
'res/physics32.png'
|
||||
)
|
||||
.addParameter('objectList', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('objectList', _('Object'), '', false)
|
||||
.addCodeOnlyParameter('conditionInverted', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.setFunctionName('gdjs.physics2.haveObjectsStartedColliding');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'CollisionStopped',
|
||||
_('Collision stopped'),
|
||||
_('Check if two objects just stopped colliding at this frame.'),
|
||||
_('_PARAM0_ stopped colliding with _PARAM2_'),
|
||||
_('Collision'),
|
||||
'res/physics32.png',
|
||||
'res/physics32.png'
|
||||
)
|
||||
.addParameter('objectList', _('Object'), '', false)
|
||||
.addParameter('behavior', _('Behavior'), 'Physics2Behavior')
|
||||
.addParameter('objectList', _('Object'), '', false)
|
||||
.addCodeOnlyParameter('conditionInverted', '')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Physics2Behavior/physics2tools.js')
|
||||
.setFunctionName('gdjs.physics2.haveObjectsStoppedColliding');
|
||||
|
||||
return extension;
|
||||
},
|
||||
|
||||
|
@@ -48,13 +48,16 @@ namespace gdjs {
|
||||
|
||||
// Get associated behaviors
|
||||
const behaviorA = contact.GetFixtureA().GetBody()
|
||||
.gdjsAssociatedBehavior;
|
||||
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
|
||||
const behaviorB = contact.GetFixtureB().GetBody()
|
||||
.gdjsAssociatedBehavior;
|
||||
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
|
||||
|
||||
// Let each behavior know about the contact against the other
|
||||
behaviorA.currentContacts.push(behaviorB);
|
||||
behaviorB.currentContacts.push(behaviorA);
|
||||
if (!behaviorA || !behaviorB) {
|
||||
return;
|
||||
}
|
||||
|
||||
behaviorA.onContactBegin(behaviorB);
|
||||
behaviorB.onContactBegin(behaviorA);
|
||||
};
|
||||
this.contactListener.EndContact = function (contactPtr) {
|
||||
// Get the contact
|
||||
@@ -70,19 +73,16 @@ namespace gdjs {
|
||||
|
||||
// Get associated behaviors
|
||||
const behaviorA = contact.GetFixtureA().GetBody()
|
||||
.gdjsAssociatedBehavior;
|
||||
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
|
||||
const behaviorB = contact.GetFixtureB().GetBody()
|
||||
.gdjsAssociatedBehavior;
|
||||
.gdjsAssociatedBehavior as Physics2RuntimeBehavior | null;
|
||||
|
||||
// Remove each other contact
|
||||
let i = behaviorA.currentContacts.indexOf(behaviorB);
|
||||
if (i !== -1) {
|
||||
behaviorA.currentContacts.splice(i, 1);
|
||||
}
|
||||
i = behaviorB.currentContacts.indexOf(behaviorA);
|
||||
if (i !== -1) {
|
||||
behaviorB.currentContacts.splice(i, 1);
|
||||
if (!behaviorA || !behaviorB) {
|
||||
return;
|
||||
}
|
||||
|
||||
behaviorA.onContactEnd(behaviorB);
|
||||
behaviorB.onContactEnd(behaviorA);
|
||||
};
|
||||
this.contactListener.PreSolve = function () {};
|
||||
this.contactListener.PostSolve = function () {};
|
||||
@@ -241,7 +241,9 @@ namespace gdjs {
|
||||
layers: any;
|
||||
masks: any;
|
||||
shapeScale: number = 1;
|
||||
currentContacts: any;
|
||||
contactsStartedThisFrame: Array<Physics2RuntimeBehavior>;
|
||||
contactsEndedThisFrame: Array<Physics2RuntimeBehavior>;
|
||||
currentContacts: Array<Physics2RuntimeBehavior>;
|
||||
_body: any = null;
|
||||
_sharedData: any;
|
||||
_tempb2Vec2: any;
|
||||
@@ -281,7 +283,9 @@ namespace gdjs {
|
||||
this.gravityScale = behaviorData.gravityScale;
|
||||
this.layers = behaviorData.layers;
|
||||
this.masks = behaviorData.masks;
|
||||
this.currentContacts = this.currentContacts || [];
|
||||
this.contactsStartedThisFrame = [];
|
||||
this.contactsEndedThisFrame = [];
|
||||
this.currentContacts = [];
|
||||
this.currentContacts.length = 0;
|
||||
this._sharedData = Physics2SharedData.getSharedData(
|
||||
runtimeScene,
|
||||
@@ -741,11 +745,21 @@ namespace gdjs {
|
||||
this._objectOldX = this.owner.getX();
|
||||
this._objectOldY = this.owner.getY();
|
||||
this._objectOldAngle = this.owner.getAngle();
|
||||
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
this.currentContacts,
|
||||
this.contactsStartedThisFrame,
|
||||
this.contactsEndedThisFrame
|
||||
);
|
||||
}
|
||||
|
||||
doStepPostEvents(runtimeScene) {
|
||||
this._updateBodyFromObject();
|
||||
|
||||
// Reset contacts that happened this frame
|
||||
this.contactsStartedThisFrame.length = 0;
|
||||
this.contactsEndedThisFrame.length = 0;
|
||||
|
||||
// Reset world step to update next frame
|
||||
this._sharedData.stepped = false;
|
||||
}
|
||||
@@ -1452,6 +1466,30 @@ namespace gdjs {
|
||||
this._body.ApplyAngularImpulse(angularImpulse);
|
||||
}
|
||||
|
||||
getMass(): float {
|
||||
// If there is no body, set a new one
|
||||
if (this._body === null) {
|
||||
this.createBody();
|
||||
}
|
||||
|
||||
// Wake up the object
|
||||
this._body.SetAwake(true);
|
||||
|
||||
return this._body.GetMass();
|
||||
}
|
||||
|
||||
getInertia(): float {
|
||||
// If there is no body, set a new one
|
||||
if (this._body === null) {
|
||||
this.createBody();
|
||||
}
|
||||
|
||||
// Wake up the object
|
||||
this._body.SetAwake(true);
|
||||
|
||||
return this._body.GetInertia();
|
||||
}
|
||||
|
||||
getMassCenterX(): float {
|
||||
// If there is no body, set a new one
|
||||
if (this._body === null) {
|
||||
@@ -3797,8 +3835,28 @@ namespace gdjs {
|
||||
joint.GetBodyB().SetAwake(true);
|
||||
}
|
||||
|
||||
// Collision
|
||||
static collisionTest(
|
||||
onContactBegin(otherBehavior: Physics2RuntimeBehavior) {
|
||||
// There might be contacts that end during the frame and
|
||||
// start again right away. It is considered a glitch
|
||||
// and should not be detected.
|
||||
let i = this.contactsEndedThisFrame.indexOf(otherBehavior);
|
||||
if (i !== -1) {
|
||||
this.contactsEndedThisFrame.splice(i, 1);
|
||||
} else {
|
||||
this.contactsStartedThisFrame.push(otherBehavior);
|
||||
}
|
||||
}
|
||||
|
||||
onContactEnd(otherBehavior: Physics2RuntimeBehavior) {
|
||||
this.contactsEndedThisFrame.push(otherBehavior);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Prefer using `Physics2RuntimeBehavior.areObjectsColliding`.
|
||||
*/
|
||||
static collisionTest = Physics2RuntimeBehavior.areObjectsColliding;
|
||||
|
||||
static areObjectsColliding(
|
||||
object1: gdjs.RuntimeObject,
|
||||
object2: gdjs.RuntimeObject,
|
||||
behaviorName: string
|
||||
@@ -3806,18 +3864,60 @@ namespace gdjs {
|
||||
// Test if the second object is in the list of contacts of the first one
|
||||
const behavior1 = object1.getBehavior(
|
||||
behaviorName
|
||||
) as Physics2RuntimeBehavior;
|
||||
if (!!behavior1) {
|
||||
for (let i = 0, len = behavior1.currentContacts.length; i < len; ++i) {
|
||||
if (behavior1.currentContacts[i].owner === object2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
) as Physics2RuntimeBehavior | null;
|
||||
if (!behavior1) return false;
|
||||
|
||||
if (
|
||||
behavior1.currentContacts.some((behavior) => behavior.owner === object2)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
// If a contact has started at this frame and ended right away, it
|
||||
// won't appear in current contacts but the condition should return
|
||||
// true anyway.
|
||||
if (
|
||||
behavior1.contactsStartedThisFrame.some(
|
||||
(behavior) => behavior.owner === object2
|
||||
)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// No contact found
|
||||
return false;
|
||||
}
|
||||
|
||||
static hasCollisionStartedBetween(
|
||||
object1: gdjs.RuntimeObject,
|
||||
object2: gdjs.RuntimeObject,
|
||||
behaviorName: string
|
||||
): boolean {
|
||||
// Test if the second object is in the list of contacts of the first one
|
||||
const behavior1 = object1.getBehavior(
|
||||
behaviorName
|
||||
) as Physics2RuntimeBehavior | null;
|
||||
if (!behavior1) return false;
|
||||
|
||||
return behavior1.contactsStartedThisFrame.some(
|
||||
(behavior) => behavior.owner === object2
|
||||
);
|
||||
}
|
||||
|
||||
static hasCollisionStoppedBetween(
|
||||
object1: gdjs.RuntimeObject,
|
||||
object2: gdjs.RuntimeObject,
|
||||
behaviorName: string
|
||||
): boolean {
|
||||
// Test if the second object is in the list of contacts of the first one
|
||||
const behavior1 = object1.getBehavior(
|
||||
behaviorName
|
||||
) as Physics2RuntimeBehavior | null;
|
||||
if (!behavior1) return false;
|
||||
|
||||
return behavior1.contactsEndedThisFrame.some(
|
||||
(behavior) => behavior.owner === object2
|
||||
);
|
||||
}
|
||||
}
|
||||
gdjs.registerBehavior(
|
||||
'Physics2::Physics2Behavior',
|
||||
|
@@ -7,7 +7,37 @@ namespace gdjs {
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics2RuntimeBehavior.collisionTest,
|
||||
gdjs.Physics2RuntimeBehavior.areObjectsColliding,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
export const haveObjectsStartedColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics2RuntimeBehavior.hasCollisionStartedBetween,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
behaviorName
|
||||
);
|
||||
};
|
||||
|
||||
export const haveObjectsStoppedColliding = function (
|
||||
objectsLists1: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
behaviorName: string,
|
||||
objectsLists2: Hashtable<Array<gdjs.RuntimeObject>>,
|
||||
inverted: boolean
|
||||
) {
|
||||
return gdjs.evtTools.object.twoListsTest(
|
||||
gdjs.Physics2RuntimeBehavior.hasCollisionStoppedBetween,
|
||||
objectsLists1,
|
||||
objectsLists2,
|
||||
inverted,
|
||||
|
@@ -0,0 +1,321 @@
|
||||
function assertCollision(object1, object2, options) {
|
||||
expect(
|
||||
gdjs.Physics2RuntimeBehavior.hasCollisionStartedBetween(
|
||||
object1,
|
||||
object2,
|
||||
'Physics2'
|
||||
)
|
||||
).to.be(options.started);
|
||||
expect(
|
||||
gdjs.Physics2RuntimeBehavior.areObjectsColliding(
|
||||
object1,
|
||||
object2,
|
||||
'Physics2'
|
||||
)
|
||||
).to.be(options.collision);
|
||||
expect(
|
||||
gdjs.Physics2RuntimeBehavior.hasCollisionStoppedBetween(
|
||||
object1,
|
||||
object2,
|
||||
'Physics2'
|
||||
)
|
||||
).to.be(options.stopped);
|
||||
}
|
||||
|
||||
function createGameWithSceneWithPhysics2SharedData() {
|
||||
const runtimeGame = new gdjs.RuntimeGame({
|
||||
variables: [],
|
||||
resources: { resources: [] },
|
||||
properties: { windowWidth: 1000, windowHeight: 1000 },
|
||||
});
|
||||
const runtimeScene = new gdjs.RuntimeScene(runtimeGame);
|
||||
runtimeScene.loadFromScene({
|
||||
layers: [
|
||||
{
|
||||
name: '',
|
||||
visibility: true,
|
||||
cameras: [],
|
||||
effects: [],
|
||||
ambientLightColorR: 127,
|
||||
ambientLightColorB: 127,
|
||||
ambientLightColorG: 127,
|
||||
isLightingLayer: false,
|
||||
followBaseLayerCamera: false,
|
||||
},
|
||||
],
|
||||
variables: [],
|
||||
r: 0,
|
||||
v: 0,
|
||||
b: 0,
|
||||
mangledName: 'Scene1',
|
||||
name: 'Scene1',
|
||||
stopSoundsOnStartup: false,
|
||||
title: '',
|
||||
behaviorsSharedData: [],
|
||||
objects: [],
|
||||
instances: [],
|
||||
});
|
||||
|
||||
runtimeScene.setInitialSharedDataForBehavior('Physics2', {
|
||||
gravityX: 0,
|
||||
gravityY: 0,
|
||||
scaleX: 1,
|
||||
scaleY: 1,
|
||||
});
|
||||
return [runtimeGame, runtimeScene];
|
||||
}
|
||||
|
||||
function createObject(runtimeScene, behaviorProperties) {
|
||||
const object = new gdjs.TestRuntimeObject(runtimeScene, {
|
||||
name: 'obj1',
|
||||
type: '',
|
||||
behaviors: [
|
||||
{
|
||||
name: 'Physics2',
|
||||
type: 'Physics2::Physics2Behavior',
|
||||
bodyType: 'Dynamic',
|
||||
bullet: false,
|
||||
fixedRotation: true,
|
||||
canSleep: false,
|
||||
shape: 'Box',
|
||||
shapeDimensionA: 0,
|
||||
shapeDimensionB: 0,
|
||||
shapeOffsetX: 0,
|
||||
shapeOffsetY: 0,
|
||||
polygonOrigin: 'Center',
|
||||
vertices: [],
|
||||
density: 1.0,
|
||||
friction: 0.01,
|
||||
restitution: 1,
|
||||
linearDamping: 0,
|
||||
angularDamping: 0.1,
|
||||
gravityScale: 1,
|
||||
layers: 1,
|
||||
masks: 1,
|
||||
...behaviorProperties,
|
||||
},
|
||||
],
|
||||
variables: [],
|
||||
effects: [],
|
||||
});
|
||||
object.setCustomWidthAndHeight(10, 10);
|
||||
runtimeScene.addObject(object);
|
||||
return object;
|
||||
}
|
||||
|
||||
describe('Physics2RuntimeBehavior', () => {
|
||||
describe('Contacts computation', () => {
|
||||
let runtimeGame;
|
||||
let runtimeScene;
|
||||
beforeEach(() => {
|
||||
[runtimeGame, runtimeScene] = createGameWithSceneWithPhysics2SharedData();
|
||||
});
|
||||
|
||||
it('should bounce during the frame (at low fps), collision should be detected, as well as start and stop of the collision', () => {
|
||||
const fps = 2;
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return (1 / fps) * 1000;
|
||||
};
|
||||
|
||||
const movingObject = createObject(runtimeScene);
|
||||
const staticObject = createObject(runtimeScene, { bodyType: 'Static' });
|
||||
staticObject.setPosition(0, 25);
|
||||
movingObject.setPosition(0, 0);
|
||||
const staticObjectBehavior = staticObject.getBehavior('Physics2');
|
||||
const movingObjectBehavior = movingObject.getBehavior('Physics2');
|
||||
if (!staticObjectBehavior || !movingObjectBehavior) {
|
||||
throw new Error('Behaviors not found, test cannot be run.');
|
||||
}
|
||||
movingObjectBehavior.setLinearVelocityY(40000);
|
||||
|
||||
let hasBounced = false;
|
||||
let stepIndex = 0;
|
||||
|
||||
runtimeScene.setEventsFunction(() => {
|
||||
if (movingObjectBehavior.getLinearVelocityY() > 0) {
|
||||
// If the moving object has a positive velocity, it hasn't bounced
|
||||
// on the static object
|
||||
assertCollision(movingObject, staticObject, {
|
||||
started: false,
|
||||
collision: false,
|
||||
stopped: false,
|
||||
});
|
||||
} else {
|
||||
hasBounced = true;
|
||||
expect(movingObject.getY() < staticObject.getY()).to.be(true);
|
||||
assertCollision(movingObject, staticObject, {
|
||||
started: true,
|
||||
collision: true,
|
||||
stopped: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
while (stepIndex < 10 && !hasBounced) {
|
||||
runtimeScene.renderAndStep(1000 / fps);
|
||||
stepIndex++;
|
||||
}
|
||||
|
||||
if (!hasBounced) {
|
||||
throw new Error('Contact did not happen, nothing was tested.');
|
||||
}
|
||||
});
|
||||
|
||||
it('should begin to bounce during the frame (at high fps), collision should be detected, as well as start of the collision, but not the end', () => {
|
||||
const fps = 50;
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
runtimeScene._timeManager.getElapsedTime = function () {
|
||||
return (1 / fps) * 1000;
|
||||
};
|
||||
|
||||
const movingObject = createObject(runtimeScene);
|
||||
const staticObject = createObject(runtimeScene, { bodyType: 'Static' });
|
||||
staticObject.setPosition(0, 25);
|
||||
movingObject.setPosition(0, 0);
|
||||
const staticObjectBehavior = staticObject.getBehavior('Physics2');
|
||||
const movingObjectBehavior = movingObject.getBehavior('Physics2');
|
||||
if (!staticObjectBehavior || !movingObjectBehavior) {
|
||||
throw new Error('Behaviors not found, test cannot be run.');
|
||||
}
|
||||
movingObjectBehavior.setLinearVelocityY(40000);
|
||||
|
||||
let hasBegunBouncing = false;
|
||||
let stepIndex = 0;
|
||||
|
||||
runtimeScene.setEventsFunction(() => {
|
||||
if (movingObjectBehavior.getLinearVelocityY() > 0) {
|
||||
// If the moving object has a positive velocity, it hasn't bounced
|
||||
// on the static object
|
||||
assertCollision(movingObject, staticObject, {
|
||||
started: false,
|
||||
collision: false,
|
||||
stopped: false,
|
||||
});
|
||||
} else {
|
||||
hasBegunBouncing = true;
|
||||
// At first frame, collision should have only started
|
||||
expect(movingObject.getY() < staticObject.getY()).to.be(true);
|
||||
assertCollision(movingObject, staticObject, {
|
||||
started: true,
|
||||
collision: true,
|
||||
stopped: false,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
while (stepIndex < 10 && !hasBegunBouncing) {
|
||||
runtimeScene.renderAndStep(1000 / fps);
|
||||
stepIndex++;
|
||||
}
|
||||
|
||||
if (!hasBegunBouncing) {
|
||||
throw new Error(
|
||||
'Start of contact was not detected, nothing was tested.'
|
||||
);
|
||||
}
|
||||
|
||||
// At next frame, end of collision should be detected
|
||||
let hasFinishedBouncing = false;
|
||||
|
||||
runtimeScene.setEventsFunction(() => {
|
||||
hasFinishedBouncing = true;
|
||||
assertCollision(movingObject, staticObject, {
|
||||
started: false,
|
||||
collision: false,
|
||||
stopped: true,
|
||||
});
|
||||
});
|
||||
|
||||
runtimeScene.renderAndStep(1000 / fps);
|
||||
|
||||
if (!hasFinishedBouncing) {
|
||||
throw new Error('End of contact was not detected, nothing was tested.');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('onContactBegin', () => {
|
||||
let runtimeGame;
|
||||
let runtimeScene;
|
||||
beforeEach(() => {
|
||||
[runtimeGame, runtimeScene] = createGameWithSceneWithPhysics2SharedData();
|
||||
});
|
||||
|
||||
it('should add behavior to list of started contacts', () => {
|
||||
const fps = 50;
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
|
||||
const object = createObject(runtimeScene);
|
||||
const otherObject = createObject(runtimeScene);
|
||||
|
||||
const behavior = object.getBehavior('Physics2');
|
||||
const otherBehavior = otherObject.getBehavior('Physics2');
|
||||
if (!behavior || !otherBehavior) {
|
||||
throw new Error('Behavior not found, test cannot be run.');
|
||||
}
|
||||
|
||||
behavior.onContactBegin(otherBehavior);
|
||||
|
||||
expect(behavior.contactsStartedThisFrame.length).to.be(1);
|
||||
expect(behavior.contactsStartedThisFrame[0]).to.be(otherBehavior);
|
||||
});
|
||||
|
||||
it('should add behavior to list of started contacts and ended contacts', () => {
|
||||
// From the user point of view the objects are colliding but it could be
|
||||
// quick enough for it to happen between 2 game frames (the physics model
|
||||
// uses modelling sub-steps). So contact beginning and end should be detected.
|
||||
const fps = 50;
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
|
||||
const object = createObject(runtimeScene);
|
||||
const otherObject = createObject(runtimeScene);
|
||||
|
||||
const behavior = object.getBehavior('Physics2');
|
||||
const otherBehavior = otherObject.getBehavior('Physics2');
|
||||
if (!behavior || !otherBehavior) {
|
||||
throw new Error('Behavior not found, test cannot be run.');
|
||||
}
|
||||
|
||||
behavior.onContactBegin(otherBehavior);
|
||||
|
||||
expect(behavior.contactsStartedThisFrame.length).to.be(1);
|
||||
expect(behavior.contactsStartedThisFrame[0]).to.be(otherBehavior);
|
||||
expect(behavior.contactsEndedThisFrame.length).to.be(0);
|
||||
|
||||
behavior.onContactEnd(otherBehavior);
|
||||
|
||||
expect(behavior.contactsStartedThisFrame.length).to.be(1);
|
||||
expect(behavior.contactsStartedThisFrame[0]).to.be(otherBehavior);
|
||||
expect(behavior.contactsEndedThisFrame.length).to.be(1);
|
||||
expect(behavior.contactsEndedThisFrame[0]).to.be(otherBehavior);
|
||||
});
|
||||
|
||||
it('should not add behavior to list of started contacts if the behavior is also present in the list of ended contacts', () => {
|
||||
// From the user point of view the objects are staying in contact with each other.
|
||||
// They would be surprised if the conditions for a contact beginning and
|
||||
// end were true.
|
||||
const fps = 50;
|
||||
runtimeGame.setGameResolutionSize(1000, 1000);
|
||||
|
||||
const object = createObject(runtimeScene);
|
||||
const otherObject = createObject(runtimeScene);
|
||||
|
||||
const behavior = object.getBehavior('Physics2');
|
||||
const otherBehavior = otherObject.getBehavior('Physics2');
|
||||
if (!behavior || !otherBehavior) {
|
||||
throw new Error('Behavior not found, test cannot be run.');
|
||||
}
|
||||
|
||||
behavior.onContactEnd(otherBehavior);
|
||||
|
||||
expect(behavior.contactsStartedThisFrame.length).to.be(0);
|
||||
expect(behavior.contactsEndedThisFrame.length).to.be(1);
|
||||
expect(behavior.contactsEndedThisFrame[0]).to.be(otherBehavior);
|
||||
|
||||
behavior.onContactBegin(otherBehavior);
|
||||
|
||||
expect(behavior.contactsStartedThisFrame.length).to.be(0);
|
||||
expect(behavior.contactsEndedThisFrame.length).to.be(0);
|
||||
});
|
||||
});
|
||||
});
|
108
Extensions/Physics2Behavior/tests/utils.spec.js
Normal file
108
Extensions/Physics2Behavior/tests/utils.spec.js
Normal file
@@ -0,0 +1,108 @@
|
||||
// @ts-check
|
||||
|
||||
describe('computeCurrentContactsFromStartedAndEndedContacts', () => {
|
||||
it('returns same current contacts if nothing happened', () => {
|
||||
const contacts = ['A', 'B', 'C'];
|
||||
const startedContacts = [];
|
||||
const endedContacts = [];
|
||||
const expectedResolvedContacts = [...contacts];
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
contacts,
|
||||
startedContacts,
|
||||
endedContacts
|
||||
);
|
||||
expect(contacts).to.eql(expectedResolvedContacts);
|
||||
});
|
||||
|
||||
it('returns current contacts with started contacts added', () => {
|
||||
const contacts = ['A', 'B', 'C'];
|
||||
const startedContacts = ['Z', 'Q'];
|
||||
const endedContacts = [];
|
||||
const expectedResolvedContacts = [...contacts, ...startedContacts];
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
contacts,
|
||||
startedContacts,
|
||||
endedContacts
|
||||
);
|
||||
expect(contacts).to.eql(expectedResolvedContacts);
|
||||
});
|
||||
|
||||
it('returns current contacts with ended contacts removed', () => {
|
||||
const contacts = ['A', 'B', 'C'];
|
||||
const startedContacts = [];
|
||||
const endedContacts = ['A', 'C'];
|
||||
const expectedResolvedContacts = ['B'];
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
contacts,
|
||||
startedContacts,
|
||||
endedContacts
|
||||
);
|
||||
expect(contacts).to.eql(expectedResolvedContacts);
|
||||
});
|
||||
|
||||
it('returns same current contacts if all started contacts also ended', () => {
|
||||
const contacts = ['A', 'B', 'C'];
|
||||
const startedContacts = ['Z', 'X'];
|
||||
const endedContacts = ['Z', 'X'];
|
||||
const expectedResolvedContacts = [...contacts];
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
contacts,
|
||||
startedContacts,
|
||||
endedContacts
|
||||
);
|
||||
expect(contacts).to.eql(expectedResolvedContacts);
|
||||
});
|
||||
|
||||
it('returns current contacts without started contacts that also ended', () => {
|
||||
const contacts = ['A', 'B', 'C'];
|
||||
const startedContacts = ['Z', 'X', 'W'];
|
||||
const endedContacts = ['Z', 'A', 'X'];
|
||||
const expectedResolvedContacts = ['B', 'C', 'W'];
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
contacts,
|
||||
startedContacts,
|
||||
endedContacts
|
||||
);
|
||||
expect(contacts).to.eql(expectedResolvedContacts);
|
||||
});
|
||||
|
||||
it('returns current contacts with a contact that started and also jittered', () => {
|
||||
// Should handle cases when this happens during the frame:
|
||||
// - contact Z starts
|
||||
// - contact Z ends
|
||||
// - contact Z starts
|
||||
// Contact Z should appear in the current contacts.
|
||||
// We consider a contact shouldn't be able to do that but it should be handled
|
||||
// in case it happens.
|
||||
const contacts = ['A', 'B', 'C'];
|
||||
const startedContacts = ['Z', 'Z'];
|
||||
const endedContacts = ['Z'];
|
||||
const expectedResolvedContacts = [...contacts, 'Z'];
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
contacts,
|
||||
startedContacts,
|
||||
endedContacts
|
||||
);
|
||||
expect(contacts).to.eql(expectedResolvedContacts);
|
||||
});
|
||||
|
||||
it('returns current contacts without a contact that ended and also jittered', () => {
|
||||
// Should handle cases where contact C was here and, during the frame:
|
||||
// - contact C ends
|
||||
// - contact C starts
|
||||
// - contact C ends
|
||||
// Contact C should not appear in the current contacts
|
||||
// We consider a contact shouldn't be able to do that but it should be handled
|
||||
// in case it happens.
|
||||
const contacts = ['A', 'B', 'C'];
|
||||
const startedContacts = ['C'];
|
||||
const endedContacts = ['C', 'C'];
|
||||
const expectedResolvedContacts = ['A', 'B'];
|
||||
gdjs.physics2.computeCurrentContactsFromStartedAndEndedContacts(
|
||||
contacts,
|
||||
startedContacts,
|
||||
endedContacts
|
||||
);
|
||||
expect(contacts).to.eql(expectedResolvedContacts);
|
||||
});
|
||||
});
|
19
Extensions/Physics2Behavior/utils.ts
Normal file
19
Extensions/Physics2Behavior/utils.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
namespace gdjs {
|
||||
export namespace physics2 {
|
||||
export const computeCurrentContactsFromStartedAndEndedContacts = <T>(
|
||||
current: Array<T>,
|
||||
started: Array<T>,
|
||||
ended: Array<T>
|
||||
): void => {
|
||||
started.forEach((startedItem) => {
|
||||
current.push(startedItem);
|
||||
});
|
||||
ended.forEach((endedItem) => {
|
||||
const index = current.indexOf(endedItem);
|
||||
if (index !== -1) {
|
||||
current.splice(index, 1);
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
}
|
File diff suppressed because one or more lines are too long
@@ -20,20 +20,39 @@ CameraExtension::CameraExtension() {
|
||||
GetAllActions()["CameraY"] // Deprecated
|
||||
.SetFunctionName("gdjs.evtTools.camera.setCameraY")
|
||||
.SetGetter("gdjs.evtTools.camera.getCameraY");
|
||||
GetAllActions()["SetCameraX"]
|
||||
GetAllActions()["SetCameraX"] // Deprecated
|
||||
.SetFunctionName("gdjs.evtTools.camera.setCameraX")
|
||||
.SetGetter("gdjs.evtTools.camera.getCameraX");
|
||||
GetAllActions()["SetCameraY"]
|
||||
GetAllActions()["SetCameraY"] // Deprecated
|
||||
.SetFunctionName("gdjs.evtTools.camera.setCameraY")
|
||||
.SetGetter("gdjs.evtTools.camera.getCameraY");
|
||||
GetAllActions()["SetCameraCenterX"]
|
||||
.SetFunctionName("gdjs.evtTools.camera.setCameraX")
|
||||
.SetGetter("gdjs.evtTools.camera.getCameraX");
|
||||
GetAllActions()["SetCameraCenterY"]
|
||||
.SetFunctionName("gdjs.evtTools.camera.setCameraY")
|
||||
.SetGetter("gdjs.evtTools.camera.getCameraY");
|
||||
|
||||
GetAllConditions()["CameraX"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraX");
|
||||
"gdjs.evtTools.camera.getCameraX"); // Deprecated
|
||||
GetAllConditions()["CameraY"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraY"); // Deprecated
|
||||
GetAllConditions()["CameraCenterX"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraX");
|
||||
GetAllConditions()["CameraCenterY"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraY");
|
||||
GetAllConditions()["CameraWidth"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraWidth");
|
||||
GetAllConditions()["CameraHeight"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraHeight");
|
||||
GetAllConditions()["CameraBorderLeft"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderLeft");
|
||||
GetAllConditions()["CameraBorderRight"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderRight");
|
||||
GetAllConditions()["CameraBorderTop"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderTop");
|
||||
GetAllConditions()["CameraBorderBottom"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderBottom");
|
||||
GetAllActions()["ShowLayer"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.showLayer");
|
||||
GetAllActions()["HideLayer"].SetFunctionName(
|
||||
@@ -52,13 +71,25 @@ CameraExtension::CameraExtension() {
|
||||
"gdjs.evtTools.camera.setCameraZoom");
|
||||
|
||||
GetAllExpressions()["CameraX"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraX");
|
||||
"gdjs.evtTools.camera.getCameraX"); // Deprecated
|
||||
GetAllExpressions()["VueX"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraX");
|
||||
"gdjs.evtTools.camera.getCameraX"); // Deprecated
|
||||
GetAllExpressions()["CameraY"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraY");
|
||||
"gdjs.evtTools.camera.getCameraY"); // Deprecated
|
||||
GetAllExpressions()["VueY"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraY"); // Deprecated
|
||||
GetAllExpressions()["CameraCenterY"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraY");
|
||||
GetAllExpressions()["CameraCenterX"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraX");
|
||||
GetAllExpressions()["CameraBorderLeft"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderLeft");
|
||||
GetAllExpressions()["CameraBorderRight"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderRight");
|
||||
GetAllExpressions()["CameraBorderTop"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderTop");
|
||||
GetAllExpressions()["CameraBorderBottom"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraBorderBottom");
|
||||
GetAllExpressions()["CameraAngle"].SetFunctionName(
|
||||
"gdjs.evtTools.camera.getCameraRotation");
|
||||
GetAllExpressions()["CameraRotation"].SetFunctionName(
|
||||
|
@@ -121,6 +121,11 @@ bool Exporter::ExportWholePixiProject(
|
||||
fs, exportedProject, codeOutputDir + "/data.js", noRuntimeGameOptions);
|
||||
includesFiles.push_back(codeOutputDir + "/data.js");
|
||||
|
||||
// Export a WebManifest with project metadata
|
||||
if (!fs.WriteToFile(exportDir + "/manifest.webmanifest",
|
||||
helper.GenerateWebManifest(exportedProject)))
|
||||
gd::LogError("Unable to export WebManifest.");
|
||||
|
||||
helper.ExportIncludesAndLibs(includesFiles, exportDir, false);
|
||||
|
||||
gd::String source = gdjsRoot + "/Runtime/index.html";
|
||||
|
@@ -9,6 +9,7 @@
|
||||
#include <emscripten.h>
|
||||
#endif
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <fstream>
|
||||
#include <functional>
|
||||
#include <sstream>
|
||||
@@ -844,4 +845,74 @@ void ExporterHelper::AddDeprecatedFontFilesToFontResources(
|
||||
// end of compatibility code
|
||||
}
|
||||
|
||||
const std::array<int, 20> IOS_ICONS_SIZES = {
|
||||
180, 60, 120, 76, 152, 40, 80, 57, 114, 72,
|
||||
144, 167, 29, 58, 87, 50, 20, 100, 167, 1024,
|
||||
};
|
||||
const std::array<int, 6> ANDROID_ICONS_SIZES = {36, 48, 72, 96, 144, 192};
|
||||
|
||||
const gd::String ExporterHelper::GenerateWebManifest(
|
||||
const gd::Project &project) {
|
||||
const gd::String &orientation = project.GetOrientation();
|
||||
gd::String icons = "[";
|
||||
|
||||
{
|
||||
std::map<int, gd::String> resourcesForSizes;
|
||||
const auto getFileNameForIcon = [&project](const gd::String &platform,
|
||||
const int size) {
|
||||
const gd::String iconName = "icon-" + gd::String::From(size);
|
||||
return project.GetPlatformSpecificAssets().Has(platform, iconName)
|
||||
? project.GetResourcesManager()
|
||||
.GetResource(project.GetPlatformSpecificAssets().Get(
|
||||
platform, iconName))
|
||||
.GetFile()
|
||||
: "";
|
||||
};
|
||||
|
||||
for (const int size : IOS_ICONS_SIZES) {
|
||||
const auto iconFile = getFileNameForIcon("ios", size);
|
||||
if (!iconFile.empty()) resourcesForSizes[size] = iconFile;
|
||||
};
|
||||
|
||||
for (const int size : ANDROID_ICONS_SIZES) {
|
||||
const auto iconFile = getFileNameForIcon("android", size);
|
||||
if (!iconFile.empty()) resourcesForSizes[size] = iconFile;
|
||||
};
|
||||
|
||||
const auto desktopIconFile = getFileNameForIcon("desktop", 512);
|
||||
if (!desktopIconFile.empty()) resourcesForSizes[512] = desktopIconFile;
|
||||
|
||||
for (const auto &sizeAndFile : resourcesForSizes) {
|
||||
icons +=
|
||||
gd::String(R"({
|
||||
"src": "{FILE}",
|
||||
"sizes": "{SIZE}x{SIZE}"
|
||||
},)")
|
||||
.FindAndReplace("{SIZE}", gd::String::From(sizeAndFile.first))
|
||||
.FindAndReplace("{FILE}", sizeAndFile.second);
|
||||
}
|
||||
}
|
||||
|
||||
icons = icons.RightTrim(",") + "]";
|
||||
|
||||
return gd::String(R"webmanifest({
|
||||
"name": "{NAME}",
|
||||
"short_name": "{NAME}",
|
||||
"id": "{PACKAGE_ID}",
|
||||
"description": "{DESCRIPTION}",
|
||||
"orientation": "{ORIENTATION}",
|
||||
"start_url": "./index.html",
|
||||
"display": "standalone",
|
||||
"background_color": "black",
|
||||
"categories": ["games", "entertainment"],
|
||||
"icons": {ICONS}
|
||||
})webmanifest")
|
||||
.FindAndReplace("{NAME}", project.GetName())
|
||||
.FindAndReplace("{PACKAGE_ID}", project.GetPackageName())
|
||||
.FindAndReplace("{DESCRIPTION}", project.GetDescription())
|
||||
.FindAndReplace("{ORIENTATION}",
|
||||
orientation == "default" ? "any" : orientation)
|
||||
.FindAndReplace("{ICONS}", icons);
|
||||
};
|
||||
|
||||
} // namespace gdjs
|
||||
|
@@ -117,11 +117,12 @@ struct PreviewExportOptions {
|
||||
|
||||
/**
|
||||
* Set the path to use for the game engine to require "@electron/remote".
|
||||
* This is because the preview is run in a folder without any node_module, but this
|
||||
* is still required for now for some features.
|
||||
* This should be removed once the dependency to "@electron/remote" is removed.
|
||||
* This is because the preview is run in a folder without any node_module, but
|
||||
* this is still required for now for some features. This should be removed
|
||||
* once the dependency to "@electron/remote" is removed.
|
||||
*/
|
||||
PreviewExportOptions &SetElectronRemoteRequirePath(const gd::String& electronRemoteRequirePath_) {
|
||||
PreviewExportOptions &SetElectronRemoteRequirePath(
|
||||
const gd::String &electronRemoteRequirePath_) {
|
||||
electronRemoteRequirePath = electronRemoteRequirePath_;
|
||||
return *this;
|
||||
}
|
||||
@@ -303,6 +304,15 @@ class ExporterHelper {
|
||||
unsigned int nonRuntimeScriptsCacheBurst,
|
||||
gd::String additionalSpec);
|
||||
|
||||
/**
|
||||
* \brief Generates a WebManifest, a metadata file that allow to make the
|
||||
* exported game a working PWA.
|
||||
*
|
||||
* \param project The project containing the game properties to generate the
|
||||
* manifest from.
|
||||
*/
|
||||
const gd::String GenerateWebManifest(const gd::Project &project);
|
||||
|
||||
/**
|
||||
* \brief Generate the Cordova configuration file and save it to the export
|
||||
* directory.
|
||||
|
@@ -74,6 +74,62 @@ namespace gdjs {
|
||||
return runtimeScene.getLayer(layer).getCameraHeight();
|
||||
};
|
||||
|
||||
export const getCameraBorderLeft = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
layer: string,
|
||||
cameraId: integer
|
||||
): number {
|
||||
if (!runtimeScene.hasLayer(layer)) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
getCameraX(runtimeScene, layer, cameraId) -
|
||||
getCameraWidth(runtimeScene, layer, cameraId) / 2
|
||||
);
|
||||
};
|
||||
|
||||
export const getCameraBorderRight = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
layer: string,
|
||||
cameraId: integer
|
||||
): number {
|
||||
if (!runtimeScene.hasLayer(layer)) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
getCameraX(runtimeScene, layer, cameraId) +
|
||||
getCameraWidth(runtimeScene, layer, cameraId) / 2
|
||||
);
|
||||
};
|
||||
|
||||
export const getCameraBorderTop = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
layer: string,
|
||||
cameraId: integer
|
||||
): number {
|
||||
if (!runtimeScene.hasLayer(layer)) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
getCameraY(runtimeScene, layer, cameraId) -
|
||||
getCameraHeight(runtimeScene, layer, cameraId) / 2
|
||||
);
|
||||
};
|
||||
|
||||
export const getCameraBorderBottom = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
layer: string,
|
||||
cameraId: integer
|
||||
): number {
|
||||
if (!runtimeScene.hasLayer(layer)) {
|
||||
return 0;
|
||||
}
|
||||
return (
|
||||
getCameraY(runtimeScene, layer, cameraId) +
|
||||
getCameraHeight(runtimeScene, layer, cameraId) / 2
|
||||
);
|
||||
};
|
||||
|
||||
export const showLayer = function (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
layer: string
|
||||
|
@@ -761,7 +761,8 @@ namespace gdjs {
|
||||
}
|
||||
}
|
||||
|
||||
const totalCount = Object.keys(files).length;
|
||||
const filesToLoad = Object.keys(files);
|
||||
const totalCount = filesToLoad.length;
|
||||
if (totalCount === 0) return onComplete(totalCount); // Nothing to load.
|
||||
|
||||
let loadedCount: integer = 0;
|
||||
@@ -775,6 +776,7 @@ namespace gdjs {
|
||||
if (loadedCount === totalCount) return onComplete(totalCount);
|
||||
|
||||
onProgress(loadedCount, totalCount);
|
||||
loadNextFile();
|
||||
};
|
||||
|
||||
const preloadAudioFile = (
|
||||
@@ -796,29 +798,30 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
for (let file in files) {
|
||||
if (files.hasOwnProperty(file)) {
|
||||
const fileData = files[file][0];
|
||||
if (!fileData.preloadAsSound && !fileData.preloadAsMusic) {
|
||||
onLoad();
|
||||
} else if (fileData.preloadAsSound && fileData.preloadAsMusic) {
|
||||
let loadedOnce = false;
|
||||
const callback = (_, error) => {
|
||||
if (!loadedOnce) {
|
||||
loadedOnce = true;
|
||||
return;
|
||||
}
|
||||
onLoad(_, error);
|
||||
};
|
||||
const loadNextFile = () => {
|
||||
if (!filesToLoad.length) return;
|
||||
const file = filesToLoad.shift()!;
|
||||
const fileData = files[file][0];
|
||||
if (!fileData.preloadAsSound && !fileData.preloadAsMusic) {
|
||||
onLoad();
|
||||
} else if (fileData.preloadAsSound && fileData.preloadAsMusic) {
|
||||
let loadedOnce = false;
|
||||
const callback = (_, error) => {
|
||||
if (!loadedOnce) {
|
||||
loadedOnce = true;
|
||||
return;
|
||||
}
|
||||
onLoad(_, error);
|
||||
};
|
||||
|
||||
preloadAudioFile(file, callback, /* isMusic= */ true);
|
||||
preloadAudioFile(file, callback, /* isMusic= */ false);
|
||||
} else if (fileData.preloadAsSound) {
|
||||
preloadAudioFile(file, onLoad, /* isMusic= */ false);
|
||||
} else if (fileData.preloadAsMusic)
|
||||
preloadAudioFile(file, onLoad, /* isMusic= */ true);
|
||||
}
|
||||
}
|
||||
preloadAudioFile(file, callback, /* isMusic= */ true);
|
||||
preloadAudioFile(file, callback, /* isMusic= */ false);
|
||||
} else if (fileData.preloadAsSound) {
|
||||
preloadAudioFile(file, onLoad, /* isMusic= */ false);
|
||||
} else if (fileData.preloadAsMusic)
|
||||
preloadAudioFile(file, onLoad, /* isMusic= */ true);
|
||||
};
|
||||
loadNextFile();
|
||||
}
|
||||
}
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
@@ -2,6 +2,7 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
|
||||
<meta name="apple-mobile-web-app-capable" content="yes">
|
||||
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
|
||||
|
@@ -664,7 +664,21 @@ namespace gdjs {
|
||||
}
|
||||
const baseUrl = 'https://api.gdevelop-app.com/analytics';
|
||||
this._playerId = this._makePlayerUuid();
|
||||
let lastSessionHitTime = Date.now();
|
||||
/**
|
||||
* The duration that is already sent to the service
|
||||
* (in milliseconds).
|
||||
**/
|
||||
let sentDuration = 0;
|
||||
/**
|
||||
* The duration that is not yet sent to the service to avoid flooding
|
||||
* (in milliseconds).
|
||||
**/
|
||||
let notYetSentDuration = 0;
|
||||
/**
|
||||
* The last time when duration has been counted
|
||||
* either in sendedDuration or notYetSentDuration.
|
||||
**/
|
||||
let lastSessionResumeTime = Date.now();
|
||||
fetch(baseUrl + '/session', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
@@ -715,24 +729,39 @@ namespace gdjs {
|
||||
return;
|
||||
}
|
||||
|
||||
const now = Date.now();
|
||||
notYetSentDuration += now - lastSessionResumeTime;
|
||||
lastSessionResumeTime = now;
|
||||
|
||||
// Group repeated calls to sendSessionHit - which could
|
||||
// happen because of multiple event listeners being fired.
|
||||
if (Date.now() - lastSessionHitTime < 3 * 1000) {
|
||||
if (notYetSentDuration < 5 * 1000) {
|
||||
return;
|
||||
}
|
||||
lastSessionHitTime = Date.now();
|
||||
// The backend use seconds for duration.
|
||||
// The milliseconds will stay in notYetSentDuration.
|
||||
const toBeSentDuration = Math.floor(notYetSentDuration / 1000) * 1000;
|
||||
sentDuration += toBeSentDuration;
|
||||
notYetSentDuration -= toBeSentDuration;
|
||||
|
||||
navigator.sendBeacon(
|
||||
baseUrl + '/session-hit',
|
||||
JSON.stringify({
|
||||
gameId: this._data.properties.projectUuid,
|
||||
playerId: this._playerId,
|
||||
sessionId: this._sessionId,
|
||||
duration: Math.floor(sentDuration / 1000),
|
||||
})
|
||||
);
|
||||
};
|
||||
if (typeof navigator !== 'undefined' && typeof document !== 'undefined') {
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
sendSessionHit();
|
||||
if (document.visibilityState === 'visible') {
|
||||
// Skip the duration the game was hidden.
|
||||
lastSessionResumeTime = Date.now();
|
||||
} else {
|
||||
sendSessionHit();
|
||||
}
|
||||
});
|
||||
window.addEventListener(
|
||||
'pagehide',
|
||||
|
114
GDJS/package-lock.json
generated
114
GDJS/package-lock.json
generated
@@ -827,10 +827,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
|
||||
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3193,6 +3194,21 @@
|
||||
"node": ">=0.4.x"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-ace": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.6.0.tgz",
|
||||
@@ -3233,6 +3249,22 @@
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-dom": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
|
||||
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.19.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-frame-component": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-4.1.3.tgz",
|
||||
@@ -3422,6 +3454,17 @@
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/scheduler": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
|
||||
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
@@ -3932,6 +3975,18 @@
|
||||
"acorn-globals": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/with/node_modules/acorn": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
|
||||
"integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/wordwrap": {
|
||||
"version": "0.0.2",
|
||||
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz",
|
||||
@@ -4598,10 +4653,11 @@
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
|
||||
"integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=",
|
||||
"dev": true
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||
"dev": true,
|
||||
"peer": true
|
||||
},
|
||||
"acorn-globals": {
|
||||
"version": "3.1.0",
|
||||
@@ -6499,6 +6555,18 @@
|
||||
"integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=",
|
||||
"dev": true
|
||||
},
|
||||
"react": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-16.14.0.tgz",
|
||||
"integrity": "sha512-0X2CImDkJGApiAlcf0ODKIneSwBPhqJawOa5wCtKbu7ZECrmS26NvtSILynQ66cgkT/RJ4LidJOc3bUESwmU8g==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2"
|
||||
}
|
||||
},
|
||||
"react-ace": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/react-ace/-/react-ace-6.6.0.tgz",
|
||||
@@ -6529,6 +6597,19 @@
|
||||
"strip-indent": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"react-dom": {
|
||||
"version": "16.14.0",
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.14.0.tgz",
|
||||
"integrity": "sha512-1gCeQXDLoIqMgqD3IO2Ah9bnf0w9kzhwN5q4FGnHZ67hBm9yePzB5JJAIQCc8x3pFnNlwFq4RidZggNAAkzWWw==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1",
|
||||
"prop-types": "^15.6.2",
|
||||
"scheduler": "^0.19.1"
|
||||
}
|
||||
},
|
||||
"react-frame-component": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/react-frame-component/-/react-frame-component-4.1.3.tgz",
|
||||
@@ -6688,6 +6769,17 @@
|
||||
"truncate-utf8-bytes": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"scheduler": {
|
||||
"version": "0.19.1",
|
||||
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz",
|
||||
"integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==",
|
||||
"dev": true,
|
||||
"peer": true,
|
||||
"requires": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"object-assign": "^4.1.1"
|
||||
}
|
||||
},
|
||||
"semver": {
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||
@@ -7080,6 +7172,14 @@
|
||||
"requires": {
|
||||
"acorn": "^3.1.0",
|
||||
"acorn-globals": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"acorn": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz",
|
||||
"integrity": "sha512-OLUyIIZ7mF5oaAUT1w0TFqQS81q3saT46x8t7ukpPjMNk+nbs4ZHhs7ToV8EWnLYLepjETXd4XaCE4uxkMeqUw==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"wordwrap": {
|
||||
|
@@ -75,6 +75,10 @@ module.exports = function (config) {
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/LinkedObjects/linkedobjects.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventory.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Inventory/inventorytools.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/utils.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/box2d.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Physics2Behavior/physics2tools.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Leaderboards/leaderboardstools.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject.js',
|
||||
'../../newIDE/app/resources/GDJS/Runtime/Extensions/Lighting/lightruntimeobject-pixi-renderer.js',
|
||||
|
@@ -2134,6 +2134,14 @@ interface ExpressionCompletionFinder {
|
||||
//Inherited from ExpressionParser2NodeWorker:
|
||||
};
|
||||
|
||||
interface ExpressionNodeLocationFinder {
|
||||
ExpressionNode STATIC_GetNodeAtPosition([Ref] ExpressionNode node, unsigned long searchedPosition);
|
||||
};
|
||||
|
||||
interface ExpressionTypeFinder {
|
||||
[Const, Ref] DOMString STATIC_GetType([Const, Ref] Platform platform, [Const, Ref] ObjectsContainer globalObjectsContainer, [Const, Ref] ObjectsContainer objectsContainer, [Const] DOMString rootType, [Ref] ExpressionNode node);
|
||||
};
|
||||
|
||||
interface ExpressionNode {
|
||||
void Visit([Ref] ExpressionParser2NodeWorker worker);
|
||||
};
|
||||
|
@@ -30,6 +30,8 @@
|
||||
#include <GDCore/IDE/Events/EventsRemover.h>
|
||||
#include <GDCore/IDE/Events/EventsTypesLister.h>
|
||||
#include <GDCore/IDE/Events/ExpressionCompletionFinder.h>
|
||||
#include <GDCore/IDE/Events/ExpressionNodeLocationFinder.h>
|
||||
#include <GDCore/IDE/Events/ExpressionTypeFinder.h>
|
||||
#include <GDCore/IDE/Events/ExpressionValidator.h>
|
||||
#include <GDCore/IDE/Events/InstructionSentenceFormatter.h>
|
||||
#include <GDCore/IDE/Events/InstructionsTypeRenamer.h>
|
||||
@@ -614,6 +616,8 @@ typedef ExtensionAndMetadata<ExpressionMetadata> ExtensionAndExpressionMetadata;
|
||||
IsExtensionLifecycleEventsFunction
|
||||
|
||||
#define STATIC_GetCompletionDescriptionsFor GetCompletionDescriptionsFor
|
||||
#define STATIC_GetType GetType
|
||||
#define STATIC_GetNodeAtPosition GetNodeAtPosition
|
||||
|
||||
#define STATIC_ScanProject ScanProject
|
||||
|
||||
|
6
GDevelop.js/types/gdexpressionnodelocationfinder.js
Normal file
6
GDevelop.js/types/gdexpressionnodelocationfinder.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdExpressionNodeLocationFinder {
|
||||
static getNodeAtPosition(node: gdExpressionNode, searchedPosition: number): gdExpressionNode;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
6
GDevelop.js/types/gdexpressiontypefinder.js
Normal file
6
GDevelop.js/types/gdexpressiontypefinder.js
Normal file
@@ -0,0 +1,6 @@
|
||||
// Automatically generated by GDevelop.js/scripts/generate-types.js
|
||||
declare class gdExpressionTypeFinder {
|
||||
static getType(platform: gdPlatform, globalObjectsContainer: gdObjectsContainer, objectsContainer: gdObjectsContainer, rootType: string, node: gdExpressionNode): string;
|
||||
delete(): void;
|
||||
ptr: number;
|
||||
};
|
@@ -156,6 +156,8 @@ declare class libGDevelop {
|
||||
ExpressionCompletionDescription: Class<gdExpressionCompletionDescription>;
|
||||
VectorExpressionCompletionDescription: Class<gdVectorExpressionCompletionDescription>;
|
||||
ExpressionCompletionFinder: Class<gdExpressionCompletionFinder>;
|
||||
ExpressionNodeLocationFinder: Class<gdExpressionNodeLocationFinder>;
|
||||
ExpressionTypeFinder: Class<gdExpressionTypeFinder>;
|
||||
ExpressionNode: Class<gdExpressionNode>;
|
||||
UniquePtrExpressionNode: Class<gdUniquePtrExpressionNode>;
|
||||
ExpressionParser2: Class<gdExpressionParser2>;
|
||||
|
@@ -11,7 +11,7 @@ export const globalTypes = {
|
||||
toolbar: {
|
||||
icon: 'circlehollow',
|
||||
// See theme names in ThemeRegistry.js
|
||||
items: ['GDevelop default Dark', 'GDevelop default', 'Dark', 'Nord', 'Solarized Dark', 'One Dark'],
|
||||
items: ['GDevelop default Dark', 'GDevelop default Light', 'Blue Dark', 'Nord', 'Solarized Dark', 'One Dark'],
|
||||
showName: true,
|
||||
},
|
||||
},
|
||||
|
113
newIDE/app/package-lock.json
generated
113
newIDE/app/package-lock.json
generated
@@ -8861,6 +8861,20 @@
|
||||
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack4/node_modules/acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/builder-webpack4/node_modules/babel-loader": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
|
||||
@@ -11403,6 +11417,20 @@
|
||||
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@storybook/manager-webpack4/node_modules/acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@storybook/manager-webpack4/node_modules/babel-loader": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
|
||||
@@ -29063,6 +29091,15 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/load-json-file/node_modules/type-fest": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
|
||||
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/load-pkg": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/load-pkg/-/load-pkg-3.0.1.tgz",
|
||||
@@ -39210,6 +39247,15 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/tempy/node_modules/type-fest": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
|
||||
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/term-size": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz",
|
||||
@@ -39765,12 +39811,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/type-fest": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
|
||||
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
@@ -39802,9 +39853,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/typescript": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
|
||||
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
|
||||
"version": "4.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
|
||||
"integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
@@ -48773,6 +48824,14 @@
|
||||
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"babel-loader": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
|
||||
@@ -50611,6 +50670,14 @@
|
||||
"integrity": "sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg==",
|
||||
"dev": true
|
||||
},
|
||||
"acorn": {
|
||||
"version": "8.7.1",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.7.1.tgz",
|
||||
"integrity": "sha512-Xx54uLJQZ19lKygFXOWsscKUbsBZW0CPykPhVQdhIeIwrbPmJzqeASDInc8nKBnp/JT6igTs82qPXz069H8I/A==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"babel-loader": {
|
||||
"version": "8.2.3",
|
||||
"resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.3.tgz",
|
||||
@@ -64592,6 +64659,14 @@
|
||||
"pify": "^4.0.1",
|
||||
"strip-bom": "^3.0.0",
|
||||
"type-fest": "^0.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"type-fest": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
|
||||
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"load-pkg": {
|
||||
@@ -72749,6 +72824,14 @@
|
||||
"temp-dir": "^1.0.0",
|
||||
"type-fest": "^0.3.1",
|
||||
"unique-string": "^1.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"type-fest": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
|
||||
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"term-size": {
|
||||
@@ -73184,10 +73267,12 @@
|
||||
"dev": true
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.3.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.3.1.tgz",
|
||||
"integrity": "sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ==",
|
||||
"dev": true
|
||||
"version": "0.13.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz",
|
||||
"integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==",
|
||||
"dev": true,
|
||||
"optional": true,
|
||||
"peer": true
|
||||
},
|
||||
"type-is": {
|
||||
"version": "1.6.18",
|
||||
@@ -73215,9 +73300,9 @@
|
||||
}
|
||||
},
|
||||
"typescript": {
|
||||
"version": "4.1.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.1.3.tgz",
|
||||
"integrity": "sha512-B3ZIOf1IKeH2ixgHhj6la6xdwR9QrLC5d1VKeCSY4tvkqhF2eqd9O7txNlS0PO3GrBAFIdr3L1ndNwteUbZLYg==",
|
||||
"version": "4.7.3",
|
||||
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.7.3.tgz",
|
||||
"integrity": "sha512-WOkT3XYvrpXx4vMMqlD+8R8R37fZkjyLGlxavMc4iB8lrl8L0DeTcHbYgw/v0N/z9wAFsgBhcsF0ruoySS22mA==",
|
||||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
|
@@ -38,6 +38,7 @@ import EmptyMessage from '../UI/EmptyMessage';
|
||||
import { BoxSearchResults } from '../UI/Search/BoxSearchResults';
|
||||
|
||||
const FIXED_HEIGHT = 250;
|
||||
const FIXED_WIDTH = 300;
|
||||
|
||||
const styles = {
|
||||
previewBackground: {
|
||||
@@ -46,7 +47,7 @@ const styles = {
|
||||
justifyContent: 'center',
|
||||
alignItems: 'center',
|
||||
padding: 10,
|
||||
width: 300,
|
||||
width: FIXED_WIDTH,
|
||||
height: FIXED_HEIGHT,
|
||||
},
|
||||
chip: {
|
||||
@@ -129,6 +130,8 @@ export const AssetDetails = ({
|
||||
() => {
|
||||
(async () => {
|
||||
try {
|
||||
// Reinitialise asset to trigger a loader and recalculate all parameters. (for instance zoom)
|
||||
setAsset(null);
|
||||
const loadedAsset = await getAsset(assetShortHeader);
|
||||
setAsset(loadedAsset);
|
||||
if (loadedAsset.objectType === 'sprite') {
|
||||
@@ -296,43 +299,44 @@ export const AssetDetails = ({
|
||||
</Line>
|
||||
<ResponsiveLineStackLayout noMargin>
|
||||
<Column>
|
||||
<div style={styles.previewBackground}>
|
||||
{asset ? (
|
||||
<>
|
||||
{asset.objectType === 'sprite' &&
|
||||
animationResources &&
|
||||
direction && (
|
||||
<AnimationPreview
|
||||
resourceNames={animationResources.map(
|
||||
({ name }) => name
|
||||
)}
|
||||
getImageResourceSource={(resourceName: string) => {
|
||||
const resource = assetResources[resourceName];
|
||||
return resource ? resource.file : '';
|
||||
}}
|
||||
isImageResourceSmooth={() => isImageResourceSmooth}
|
||||
project={project}
|
||||
timeBetweenFrames={direction.timeBetweenFrames}
|
||||
isLooping // Always loop in the asset store.
|
||||
hideCheckeredBackground
|
||||
hideControls
|
||||
initialZoom={140 / Math.max(asset.width, asset.height)}
|
||||
fixedHeight={FIXED_HEIGHT}
|
||||
/>
|
||||
)}
|
||||
{(asset.objectType === 'tiled' ||
|
||||
asset.objectType === '9patch') && (
|
||||
{asset ? (
|
||||
<>
|
||||
{asset.objectType === 'sprite' &&
|
||||
animationResources &&
|
||||
direction && (
|
||||
<AnimationPreview
|
||||
resourceNames={animationResources.map(({ name }) => name)}
|
||||
getImageResourceSource={(resourceName: string) => {
|
||||
const resource = assetResources[resourceName];
|
||||
return resource ? resource.file : '';
|
||||
}}
|
||||
isImageResourceSmooth={() => isImageResourceSmooth}
|
||||
project={project}
|
||||
timeBetweenFrames={direction.timeBetweenFrames}
|
||||
isLooping // Always loop in the asset store.
|
||||
hideCheckeredBackground
|
||||
hideControls
|
||||
initialZoom={140 / Math.max(asset.width, asset.height)}
|
||||
fixedHeight={FIXED_HEIGHT}
|
||||
fixedWidth={FIXED_WIDTH}
|
||||
/>
|
||||
)}
|
||||
{(asset.objectType === 'tiled' ||
|
||||
asset.objectType === '9patch') && (
|
||||
<div style={styles.previewBackground}>
|
||||
<CorsAwareImage
|
||||
style={styles.previewImage}
|
||||
src={asset.previewImageUrls[0]}
|
||||
alt={asset.name}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div style={styles.previewBackground}>
|
||||
<PlaceholderLoader />
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{assetAnimations &&
|
||||
assetAnimations.length > 1 &&
|
||||
typeof selectedAnimationName === 'string' && (
|
||||
|
@@ -28,7 +28,7 @@ type Props = {|
|
||||
assetShortHeaders: Array<AssetShortHeader>,
|
||||
addedAssetIds: Array<string>,
|
||||
onClose: () => void,
|
||||
onAssetPackAdded: () => void,
|
||||
onAssetsAdded: () => void,
|
||||
project: gdProject,
|
||||
objectsContainer: gdObjectsContainer,
|
||||
events: gdEventsList,
|
||||
@@ -40,7 +40,7 @@ export const AssetPackDialog = ({
|
||||
assetShortHeaders,
|
||||
addedAssetIds,
|
||||
onClose,
|
||||
onAssetPackAdded,
|
||||
onAssetsAdded,
|
||||
project,
|
||||
objectsContainer,
|
||||
events,
|
||||
@@ -56,18 +56,18 @@ export const AssetPackDialog = ({
|
||||
|
||||
const resourcesFetcher = useResourceFetcher();
|
||||
const [
|
||||
isAssetPackBeingInstalled,
|
||||
setIsAssetPackBeingInstalled,
|
||||
areAssetsBeingInstalled,
|
||||
setAreAssetsBeingInstalled,
|
||||
] = React.useState<boolean>(false);
|
||||
|
||||
const eventsFunctionsExtensionsState = React.useContext(
|
||||
EventsFunctionsExtensionsContext
|
||||
);
|
||||
|
||||
const onInstallAssetPack = React.useCallback(
|
||||
const onInstallAssets = React.useCallback(
|
||||
async (assetShortHeaders: Array<AssetShortHeader>) => {
|
||||
if (!assetShortHeaders || !assetShortHeaders.length) return;
|
||||
setIsAssetPackBeingInstalled(true);
|
||||
setAreAssetsBeingInstalled(true);
|
||||
try {
|
||||
const installOutputs = await Promise.all(
|
||||
assetShortHeaders.map(assetShortHeader =>
|
||||
@@ -88,14 +88,14 @@ export const AssetPackDialog = ({
|
||||
|
||||
await resourcesFetcher.ensureResourcesAreFetched(project);
|
||||
|
||||
setIsAssetPackBeingInstalled(false);
|
||||
onAssetPackAdded();
|
||||
setAreAssetsBeingInstalled(false);
|
||||
onAssetsAdded();
|
||||
} catch (error) {
|
||||
setIsAssetPackBeingInstalled(false);
|
||||
console.error('Error while installing the asset pack', error);
|
||||
setAreAssetsBeingInstalled(false);
|
||||
console.error('Error while installing the assets', error);
|
||||
showErrorBox({
|
||||
message:
|
||||
'There was an error while installing the asset pack. Verify your internet connection or try again later.',
|
||||
'There was an error while installing the assets. Verify your internet connection or try again later.',
|
||||
rawError: error,
|
||||
errorId: 'install-asset-pack-error',
|
||||
});
|
||||
@@ -108,11 +108,11 @@ export const AssetPackDialog = ({
|
||||
objectsContainer,
|
||||
events,
|
||||
onObjectAddedFromAsset,
|
||||
onAssetPackAdded,
|
||||
onAssetsAdded,
|
||||
]
|
||||
);
|
||||
|
||||
const dialogContent = isAssetPackBeingInstalled
|
||||
const dialogContent = areAssetsBeingInstalled
|
||||
? {
|
||||
actionButton: (
|
||||
<TextButton
|
||||
@@ -141,15 +141,15 @@ export const AssetPackDialog = ({
|
||||
key="install-again"
|
||||
label={<Trans>Install again</Trans>}
|
||||
primary={false}
|
||||
onClick={() => onInstallAssetPack(assetShortHeaders)}
|
||||
onClick={() => onInstallAssets(assetShortHeaders)}
|
||||
/>
|
||||
),
|
||||
onApply: () => onInstallAssetPack(assetShortHeaders),
|
||||
onApply: () => onInstallAssets(assetShortHeaders),
|
||||
content: (
|
||||
<Text>
|
||||
<Trans>
|
||||
You already have this asset pack installed, do you want to add the{' '}
|
||||
{assetShortHeaders.length} assets again?
|
||||
You already have these {assetShortHeaders.length} assets
|
||||
installed, do you want to add them again?
|
||||
</Trans>
|
||||
</Text>
|
||||
),
|
||||
@@ -161,15 +161,14 @@ export const AssetPackDialog = ({
|
||||
key="continue"
|
||||
label={<Trans>Continue</Trans>}
|
||||
primary
|
||||
onClick={() => onInstallAssetPack(assetShortHeaders)}
|
||||
onClick={() => onInstallAssets(assetShortHeaders)}
|
||||
/>
|
||||
),
|
||||
onApply: () => onInstallAssetPack(assetShortHeaders),
|
||||
onApply: () => onInstallAssets(assetShortHeaders),
|
||||
content: (
|
||||
<Text>
|
||||
<Trans>
|
||||
You're about to add {assetShortHeaders.length} assets from the
|
||||
asset pack. Continue?
|
||||
You're about to add {assetShortHeaders.length} assets. Continue?
|
||||
</Trans>
|
||||
</Text>
|
||||
),
|
||||
@@ -181,24 +180,24 @@ export const AssetPackDialog = ({
|
||||
key="install-missing"
|
||||
primary
|
||||
onClick={() => {
|
||||
onInstallAssetPack(missingAssetShortHeaders);
|
||||
onInstallAssets(missingAssetShortHeaders);
|
||||
}}
|
||||
buildMenuTemplate={i18n => [
|
||||
{
|
||||
label: i18n._(t`Install all the assets`),
|
||||
click: () => onInstallAssetPack(assetShortHeaders),
|
||||
click: () => onInstallAssets(assetShortHeaders),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
),
|
||||
onApply: () => onInstallAssetPack(missingAssetShortHeaders),
|
||||
onApply: () => onInstallAssets(missingAssetShortHeaders),
|
||||
content: (
|
||||
<Text>
|
||||
<Trans>
|
||||
You already have{' '}
|
||||
{assetShortHeaders.length - missingAssetShortHeaders.length}{' '}
|
||||
asset(s) from this pack in your scene. Do you want to add the
|
||||
remaining {missingAssetShortHeaders.length} asset(s)?
|
||||
asset(s) in your scene. Do you want to add the remaining{' '}
|
||||
{missingAssetShortHeaders.length} one(s)?
|
||||
</Trans>
|
||||
</Text>
|
||||
),
|
||||
@@ -210,12 +209,12 @@ export const AssetPackDialog = ({
|
||||
title={assetPack.name}
|
||||
open
|
||||
onRequestClose={() => {
|
||||
if (!isAssetPackBeingInstalled) onClose();
|
||||
if (!areAssetsBeingInstalled) onClose();
|
||||
}}
|
||||
cannotBeDismissed
|
||||
actions={[
|
||||
// Installing a pack is not cancelable, so we hide the button while installing.
|
||||
!isAssetPackBeingInstalled ? (
|
||||
// Installing a list of assets is not cancelable, so we hide the button while installing.
|
||||
!areAssetsBeingInstalled ? (
|
||||
<TextButton
|
||||
key="cancel"
|
||||
label={<Trans>Cancel</Trans>}
|
||||
|
@@ -81,16 +81,19 @@ export const ExampleStore = ({ isOpening, onOpen, focusOnMount }: Props) => {
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<Column expand noMargin useFullHeight>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
aspect="add-margins-only-if-modern-theme"
|
||||
tagsHandler={tagsHandler}
|
||||
tags={filters && filters.defaultTags}
|
||||
ref={searchBarRef}
|
||||
placeholder={t`Search examples`}
|
||||
/>
|
||||
<Line>
|
||||
<Column expand>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
tagsHandler={tagsHandler}
|
||||
tags={filters && filters.defaultTags}
|
||||
ref={searchBarRef}
|
||||
placeholder={t`Search examples`}
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
<Line
|
||||
expand
|
||||
overflow={
|
||||
|
@@ -15,6 +15,7 @@ import {
|
||||
} from '../../Utils/Analytics/EventSender';
|
||||
import useDismissableTutorialMessage from '../../Hints/useDismissableTutorialMessage';
|
||||
import { t } from '@lingui/macro';
|
||||
import { ColumnStackLayout } from '../../UI/Layout';
|
||||
|
||||
type Props = {|
|
||||
isInstalling: boolean,
|
||||
@@ -89,20 +90,19 @@ export const ExtensionStore = ({
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<Column expand noMargin useFullHeight>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
aspect="add-margins-only-if-modern-theme"
|
||||
tagsHandler={tagsHandler}
|
||||
tags={filters && filters.allTags}
|
||||
placeholder={t`Search extensions`}
|
||||
/>
|
||||
{DismissableTutorialMessage && (
|
||||
<Line>
|
||||
<Column expand>{DismissableTutorialMessage}</Column>
|
||||
</Line>
|
||||
)}
|
||||
<Line>
|
||||
<ColumnStackLayout expand>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
tagsHandler={tagsHandler}
|
||||
tags={filters && filters.allTags}
|
||||
placeholder={t`Search extensions`}
|
||||
/>
|
||||
{DismissableTutorialMessage}
|
||||
</ColumnStackLayout>
|
||||
</Line>
|
||||
<ListSearchResults
|
||||
disableAutoTranslate // Search results text highlighting conflicts with dom handling by browser auto-translations features. Disables auto translation to prevent crashes.
|
||||
onRetry={fetchExtensionsAndFilters}
|
||||
|
@@ -42,18 +42,22 @@ export const ResourceStore = ({ onChoose, resourceKind }: Props) => {
|
||||
|
||||
return (
|
||||
<Column expand noMargin useFullHeight>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
aspect="add-margins-only-if-modern-theme"
|
||||
placeholder={t`Search resources`}
|
||||
/>
|
||||
<Line>
|
||||
<Column expand>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
placeholder={t`Search resources`}
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
<Line
|
||||
expand
|
||||
overflow={
|
||||
'hidden' /* Somehow required on Chrome/Firefox to avoid children growing (but not on Safari) */
|
||||
}
|
||||
noMargin
|
||||
>
|
||||
<Background noFullHeight noExpand width={250}>
|
||||
<ScrollView>
|
||||
|
@@ -100,7 +100,6 @@ export const AssetStore = ({
|
||||
isAssetPackDialogInstallOpen,
|
||||
setIsAssetPackDialogInstallOpen,
|
||||
] = React.useState(false);
|
||||
const [isAssetPackAdded, setIsAssetPackAdded] = React.useState(false);
|
||||
const [
|
||||
isAssetBeingInstalled,
|
||||
setIsAssetBeingInstalled,
|
||||
@@ -235,7 +234,6 @@ export const AssetStore = ({
|
||||
navigationState.openHome();
|
||||
clearAllFilters(assetFiltersState);
|
||||
setIsFiltersPanelOpen(false);
|
||||
setIsAssetPackAdded(false);
|
||||
}}
|
||||
size="small"
|
||||
>
|
||||
@@ -280,7 +278,6 @@ export const AssetStore = ({
|
||||
if (navigationState.getCurrentPage().isOnHomePage) {
|
||||
clearAllFilters(assetFiltersState);
|
||||
setIsFiltersPanelOpen(false);
|
||||
setIsAssetPackAdded(false);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
@@ -308,17 +305,13 @@ export const AssetStore = ({
|
||||
<Column expand alignItems="flex-end" noMargin>
|
||||
<RaisedButton
|
||||
primary
|
||||
label={
|
||||
isAssetPackAdded ? (
|
||||
<Trans>Asset pack added</Trans>
|
||||
) : (
|
||||
<Trans>Add pack to my scene</Trans>
|
||||
)
|
||||
}
|
||||
label={<Trans>Add all assets to my scene</Trans>}
|
||||
onClick={() =>
|
||||
setIsAssetPackDialogInstallOpen(true)
|
||||
}
|
||||
disabled={isAssetPackAdded}
|
||||
disabled={
|
||||
!searchResults || searchResults.length === 0
|
||||
}
|
||||
/>
|
||||
</Column>
|
||||
</>
|
||||
@@ -377,9 +370,9 @@ export const AssetStore = ({
|
||||
>
|
||||
<AssetStoreFilterPanel
|
||||
assetFiltersState={assetFiltersState}
|
||||
onChoiceChange={() =>
|
||||
navigationState.openSearchIfNeeded()
|
||||
}
|
||||
onChoiceChange={() => {
|
||||
navigationState.openSearchIfNeeded();
|
||||
}}
|
||||
/>
|
||||
</Line>
|
||||
</ScrollView>
|
||||
@@ -439,8 +432,7 @@ export const AssetStore = ({
|
||||
assetShortHeaders={searchResults}
|
||||
addedAssetIds={addedAssetIds}
|
||||
onClose={() => setIsAssetPackDialogInstallOpen(false)}
|
||||
onAssetPackAdded={() => {
|
||||
setIsAssetPackAdded(true);
|
||||
onAssetsAdded={() => {
|
||||
setIsAssetPackDialogInstallOpen(false);
|
||||
}}
|
||||
project={project}
|
||||
|
@@ -246,20 +246,26 @@ export default function NewBehaviorDialog({
|
||||
</Tabs>
|
||||
{currentTab === 'installed' && (
|
||||
<React.Fragment>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onRequestSearch={() => {
|
||||
if (behaviors.length) {
|
||||
chooseBehavior(i18n, behaviors[0]);
|
||||
} else if (showDeprecated && deprecatedBehaviors.length) {
|
||||
chooseBehavior(i18n, deprecatedBehaviors[0]);
|
||||
}
|
||||
}}
|
||||
aspect="add-margins-only-if-modern-theme"
|
||||
onChange={setSearchText}
|
||||
ref={searchBar}
|
||||
placeholder={t`Search installed behaviors`}
|
||||
/>
|
||||
<Line>
|
||||
<Column expand>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onRequestSearch={() => {
|
||||
if (behaviors.length) {
|
||||
chooseBehavior(i18n, behaviors[0]);
|
||||
} else if (
|
||||
showDeprecated &&
|
||||
deprecatedBehaviors.length
|
||||
) {
|
||||
chooseBehavior(i18n, deprecatedBehaviors[0]);
|
||||
}
|
||||
}}
|
||||
onChange={setSearchText}
|
||||
ref={searchBar}
|
||||
placeholder={t`Search installed behaviors`}
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
{hasSearchNoResult && (
|
||||
<EmptyMessage>
|
||||
<Trans>
|
||||
|
@@ -5,13 +5,13 @@ import {
|
||||
moveNodeAbove,
|
||||
moveNodeBelow,
|
||||
moveNodeAsSubEvent,
|
||||
isSameDepthAndJustBelow,
|
||||
isJustBelow,
|
||||
isSibling,
|
||||
} from './helpers';
|
||||
import { type WidthType } from '../../UI/Reponsive/ResponsiveWindowMeasurer';
|
||||
import './style.css';
|
||||
import GDevelopThemeContext from '../../UI/Theme/ThemeContext';
|
||||
import { type DropTargetComponent } from '../../UI/DragAndDrop/DropTarget';
|
||||
|
||||
const sharedStyles = {
|
||||
dropArea: { zIndex: 1, position: 'absolute' },
|
||||
dropIndicator: {
|
||||
@@ -42,7 +42,7 @@ type TargetPositionStyles = { [position: string]: DropTargetContainerStyle };
|
||||
const getTargetPositionStyles = (
|
||||
indentWidth: number,
|
||||
draggedNodeHeight: number,
|
||||
isDraggedNodeChild: boolean
|
||||
isDraggedNodeSibling: boolean
|
||||
): TargetPositionStyles => ({
|
||||
'bottom-left': { left: '0px', bottom: '0px', top: '50%', width: indentWidth },
|
||||
'bottom-right': {
|
||||
@@ -54,13 +54,13 @@ const getTargetPositionStyles = (
|
||||
top: { left: '0px', right: '0px', top: '0px', bottom: '50%' },
|
||||
bottom: { left: '0px', right: '0px', top: '50%', bottom: '0px' },
|
||||
'below-left': {
|
||||
left: isDraggedNodeChild ? '0px' : '10px',
|
||||
left: isDraggedNodeSibling ? '10px' : '0px',
|
||||
top: '100%',
|
||||
height: draggedNodeHeight,
|
||||
width: indentWidth,
|
||||
},
|
||||
'below-right': {
|
||||
left: isDraggedNodeChild ? `${indentWidth + 10}px` : `${indentWidth}px`,
|
||||
left: isDraggedNodeSibling ? `${indentWidth}px` : `${indentWidth + 10}px`,
|
||||
right: '0px',
|
||||
top: '100%',
|
||||
height: draggedNodeHeight,
|
||||
@@ -132,7 +132,7 @@ type DropContainerProps = {|
|
||||
moveFunction: ({
|
||||
targetNode: SortableTreeNode,
|
||||
node: SortableTreeNode,
|
||||
}) => number,
|
||||
}) => void,
|
||||
node: SortableTreeNode
|
||||
) => void,
|
||||
activateTargets: boolean,
|
||||
@@ -142,8 +142,76 @@ type DropContainerProps = {|
|
||||
// Used only for the node just above dragged node if it is an only child,
|
||||
// so that drop area covers the whole dragged node's row in height.
|
||||
draggedNodeHeight: number,
|
||||
getNodeAtPath: (path: Array<number>) => SortableTreeNode,
|
||||
|};
|
||||
|
||||
type HorizontalDraggedNodeDropContainerProps = {|
|
||||
node: SortableTreeNode,
|
||||
draggedNode: SortableTreeNode,
|
||||
DnDComponent: DropTargetComponent<SortableTreeNode>,
|
||||
onDrop: (
|
||||
moveFunction: ({
|
||||
targetNode: SortableTreeNode,
|
||||
node: SortableTreeNode,
|
||||
}) => void,
|
||||
node: SortableTreeNode
|
||||
) => void,
|
||||
activateTargets: boolean,
|
||||
|
||||
getNodeAtPath: (path: Array<number>) => SortableTreeNode,
|
||||
draggedNodeHeight: number,
|
||||
indentWidth: number,
|
||||
|};
|
||||
|
||||
function HorizontalDraggedNodeDropContainer({
|
||||
DnDComponent,
|
||||
onDrop,
|
||||
activateTargets,
|
||||
getNodeAtPath,
|
||||
node,
|
||||
draggedNode,
|
||||
indentWidth,
|
||||
draggedNodeHeight,
|
||||
}: HorizontalDraggedNodeDropContainerProps) {
|
||||
const { depth } = node;
|
||||
return (
|
||||
<>
|
||||
{new Array(depth).fill(0).map((_, depthStep) => {
|
||||
// Skip so that it does not hinder dragging and so that we don't have to
|
||||
// worry about delaying the drop target activation.
|
||||
if (depthStep === draggedNode.depth) return null;
|
||||
return (
|
||||
<DropTargetContainer
|
||||
key={depthStep}
|
||||
DnDComponent={DnDComponent}
|
||||
onDrop={() =>
|
||||
onDrop(
|
||||
moveNodeBelow,
|
||||
getNodeAtPath(node.nodePath.slice(0, depthStep + 1))
|
||||
)
|
||||
}
|
||||
canDrop={() => true}
|
||||
style={{
|
||||
dropArea: {
|
||||
top: '100%',
|
||||
bottom: `-${draggedNodeHeight}px`,
|
||||
left: `-${indentWidth * (depth - depthStep)}px`,
|
||||
width: indentWidth,
|
||||
},
|
||||
dropIndicator: {
|
||||
left: `-${indentWidth * (depth - depthStep)}px`,
|
||||
right: '0px',
|
||||
// The bottom is set so that the indicator is centered between the events.
|
||||
bottom: '-2px',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* DropContainer is composed of sub-containers of drop targets that allows us to identify
|
||||
* where the mouse or touch is and drop the dragged node accordingly. At most, there will be 5
|
||||
@@ -158,24 +226,19 @@ export function DropContainer({
|
||||
activateTargets,
|
||||
windowWidth,
|
||||
draggedNodeHeight,
|
||||
getNodeAtPath,
|
||||
}: DropContainerProps) {
|
||||
const isDraggedNodesOnlyChild =
|
||||
node.children.length === 1 && node.children[0].key === draggedNode.key;
|
||||
const isDraggedNodeSameDepthAndBelow = isSameDepthAndJustBelow(
|
||||
node,
|
||||
draggedNode
|
||||
);
|
||||
const isDraggedNodeSibling = isSibling(node, draggedNode);
|
||||
const isDraggedNodeJustBelow = isJustBelow(node, draggedNode);
|
||||
// We want to allow dropping below if the event has no children OR if the only
|
||||
// child of the event is the dragged one.
|
||||
const canDropBelow =
|
||||
!!node.event && (node.children.length === 0 || isDraggedNodesOnlyChild);
|
||||
const canHaveSubEvents = !!node.event && node.event.canHaveSubEvents();
|
||||
|
||||
const indentWidth = getIndentWidth(windowWidth);
|
||||
const dropAreaStyles = getTargetPositionStyles(
|
||||
indentWidth,
|
||||
draggedNodeHeight,
|
||||
isDraggedNodesOnlyChild
|
||||
isDraggedNodeSibling
|
||||
);
|
||||
const indicatorStyles = getIndicatorPositionStyles(indentWidth);
|
||||
const commonProps = {
|
||||
@@ -196,7 +259,7 @@ export function DropContainer({
|
||||
onDrop={() => onDrop(moveNodeAbove, node)}
|
||||
{...commonProps}
|
||||
/>
|
||||
{canHaveSubEvents && canDropBelow && (
|
||||
{canHaveSubEvents ? (
|
||||
<>
|
||||
<DropTargetContainer
|
||||
style={{
|
||||
@@ -214,8 +277,8 @@ export function DropContainer({
|
||||
onDrop={() => onDrop(moveNodeBelow, node)}
|
||||
{...commonProps}
|
||||
/>
|
||||
{/* Allow dragging left/right to move below or as subevent */}
|
||||
{(isDraggedNodesOnlyChild || isDraggedNodeSameDepthAndBelow) && (
|
||||
{/* Allow dragging left/right just below the current node. */}
|
||||
{isDraggedNodeJustBelow && (
|
||||
<>
|
||||
<DropTargetContainer
|
||||
style={{
|
||||
@@ -236,8 +299,7 @@ export function DropContainer({
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!canHaveSubEvents && canDropBelow && (
|
||||
) : (
|
||||
<DropTargetContainer
|
||||
style={{
|
||||
dropIndicator: indicatorStyles['bottom'],
|
||||
@@ -247,14 +309,19 @@ export function DropContainer({
|
||||
{...commonProps}
|
||||
/>
|
||||
)}
|
||||
{canHaveSubEvents && !canDropBelow && (
|
||||
<DropTargetContainer
|
||||
style={{
|
||||
dropIndicator: indicatorStyles['bottom-right'],
|
||||
dropArea: dropAreaStyles['bottom'],
|
||||
}}
|
||||
onDrop={() => onDrop(moveNodeAsSubEvent, node)}
|
||||
{...commonProps}
|
||||
{/* This DropContainer allows dragging horizontally, on any depth between
|
||||
0 and the depth of the node just above the dragged one (which has information on
|
||||
its parents). */}
|
||||
{isDraggedNodeJustBelow && (
|
||||
<HorizontalDraggedNodeDropContainer
|
||||
node={node}
|
||||
draggedNode={draggedNode}
|
||||
DnDComponent={DnDComponent}
|
||||
activateTargets={activateTargets}
|
||||
onDrop={onDrop}
|
||||
indentWidth={indentWidth}
|
||||
draggedNodeHeight={draggedNodeHeight}
|
||||
getNodeAtPath={getNodeAtPath}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -5,9 +5,6 @@ export type MoveFunctionArguments = {
|
||||
node: SortableTreeNode,
|
||||
};
|
||||
|
||||
const getRowIndexOfNode = (node: SortableTreeNode) =>
|
||||
node.nodePath[node.nodePath.length - 1];
|
||||
|
||||
export const moveEventToEventsList = ({
|
||||
targetEventsList,
|
||||
movingEvent,
|
||||
@@ -37,11 +34,6 @@ export const moveNodeAsSubEvent = ({
|
||||
initialEventsList: node.eventsList,
|
||||
toIndex: 0,
|
||||
});
|
||||
const previousRowIndex = getRowIndexOfNode(node);
|
||||
const targetRowIndex = getRowIndexOfNode(targetNode);
|
||||
return previousRowIndex <= targetRowIndex
|
||||
? targetRowIndex
|
||||
: targetRowIndex + 1;
|
||||
};
|
||||
|
||||
export const moveNodeBelow = ({ targetNode, node }: MoveFunctionArguments) => {
|
||||
@@ -56,11 +48,6 @@ export const moveNodeBelow = ({ targetNode, node }: MoveFunctionArguments) => {
|
||||
initialEventsList: node.eventsList,
|
||||
toIndex,
|
||||
});
|
||||
const previousRowIndex = getRowIndexOfNode(node);
|
||||
const targetRowIndex = getRowIndexOfNode(targetNode);
|
||||
return previousRowIndex <= targetRowIndex
|
||||
? targetRowIndex
|
||||
: targetRowIndex + 1;
|
||||
};
|
||||
|
||||
export const moveNodeAbove = ({ targetNode, node }: MoveFunctionArguments) => {
|
||||
@@ -75,11 +62,6 @@ export const moveNodeAbove = ({ targetNode, node }: MoveFunctionArguments) => {
|
||||
initialEventsList: node.eventsList,
|
||||
toIndex,
|
||||
});
|
||||
const previousRowIndex = getRowIndexOfNode(node);
|
||||
const targetRowIndex = getRowIndexOfNode(targetNode);
|
||||
return previousRowIndex <= targetRowIndex
|
||||
? targetRowIndex - 1
|
||||
: targetRowIndex;
|
||||
};
|
||||
|
||||
export const isDescendant = (
|
||||
@@ -92,11 +74,19 @@ export const isDescendant = (
|
||||
return parentPath.every((pathValue, index) => pathValue === childPath[index]);
|
||||
};
|
||||
|
||||
export const isSameDepthAndJustBelow = (
|
||||
export const isSibling = (nodeA: SortableTreeNode, nodeB: SortableTreeNode) => {
|
||||
if (nodeA.depth !== nodeB.depth) return false;
|
||||
const nodeAPath = nodeA.nodePath;
|
||||
const nodeBPath = nodeB.nodePath;
|
||||
return nodeAPath
|
||||
.slice(0, -1)
|
||||
.every((pathValue, index) => pathValue === nodeBPath[index]);
|
||||
};
|
||||
|
||||
export const isJustBelow = (
|
||||
aboveNode: SortableTreeNode,
|
||||
belowNode: SortableTreeNode
|
||||
) => {
|
||||
if (aboveNode.depth !== belowNode.depth) return false;
|
||||
const belowNodePath = belowNode.nodePath;
|
||||
const aboveNodePath = aboveNode.nodePath;
|
||||
if (belowNodePath[belowNodePath.length - 1] === 0) return false;
|
||||
@@ -105,3 +95,14 @@ export const isSameDepthAndJustBelow = (
|
||||
aboveNodePath[aboveNodePath.length - 1]
|
||||
);
|
||||
};
|
||||
|
||||
export const getNodeAtPath = (
|
||||
path: Array<number>,
|
||||
treeData: Array<SortableTreeNode>
|
||||
) => {
|
||||
if (path.length === 1) return treeData[path[0]];
|
||||
return getNodeAtPath(
|
||||
path.slice(0, -1),
|
||||
treeData[path[path.length - 1]].children
|
||||
);
|
||||
};
|
||||
|
@@ -5,6 +5,7 @@ import findIndex from 'lodash/findIndex';
|
||||
import {
|
||||
SortableTreeWithoutDndContext,
|
||||
getFlatDataFromTree,
|
||||
getNodeAtPath,
|
||||
} from 'react-sortable-tree';
|
||||
import { type ConnectDragSource } from 'react-dnd';
|
||||
import { mapFor } from '../../Utils/MapFor';
|
||||
@@ -588,17 +589,23 @@ export default class ThemableEventsTree extends Component<
|
||||
};
|
||||
|
||||
_onDrop = (
|
||||
moveFunction: MoveFunctionArguments => number,
|
||||
moveFunction: MoveFunctionArguments => void,
|
||||
currentNode: SortableTreeNode
|
||||
) => {
|
||||
const draggedNode = this.state.draggedNode;
|
||||
if (draggedNode) {
|
||||
const nextRowIndex = moveFunction({
|
||||
moveFunction({
|
||||
node: draggedNode,
|
||||
targetNode: currentNode,
|
||||
});
|
||||
const { nodePath } = draggedNode;
|
||||
this.props.onEventMoved(nodePath[nodePath.length - 1], nextRowIndex);
|
||||
const { nodePath, event } = draggedNode;
|
||||
this._onEndDrag();
|
||||
if (!event) {
|
||||
console.warn('EventsSheet: No event found in dragged node.');
|
||||
return;
|
||||
}
|
||||
const newRowIndex = this.getEventRow(event);
|
||||
this.props.onEventMoved(nodePath[nodePath.length - 1], newRowIndex);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -669,9 +676,15 @@ export default class ThemableEventsTree extends Component<
|
||||
};
|
||||
|
||||
_onEndDrag = () => {
|
||||
this.setState({ draggedNode: null });
|
||||
this._restoreFoldedNodes();
|
||||
this.forceEventsUpdate();
|
||||
// This method is always called at the end of the drag, regardless of whether
|
||||
// an event was actually dropped. It is also already called in `_onDrop` to update
|
||||
// the event list and compute history. So if draggedNode is null, we want to avoid
|
||||
// recomputing the event list.
|
||||
if (this.state.draggedNode) {
|
||||
this.setState({ draggedNode: null });
|
||||
this._restoreFoldedNodes();
|
||||
this.forceEventsUpdate();
|
||||
}
|
||||
};
|
||||
|
||||
_renderEvent = ({ node }: { node: SortableTreeNode }) => {
|
||||
@@ -779,6 +792,13 @@ export default class ThemableEventsTree extends Component<
|
||||
onDrop={this._onDrop}
|
||||
activateTargets={!isDragged && !!this.state.draggedNode}
|
||||
windowWidth={this.props.windowWidth}
|
||||
getNodeAtPath={path =>
|
||||
getNodeAtPath({
|
||||
path,
|
||||
treeData: this.state.treeData,
|
||||
getNodeKey,
|
||||
}).node
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
@@ -85,9 +85,11 @@ export const getVisibleParameterTypes = (
|
||||
|
||||
export const formatExpressionCall = (
|
||||
expressionInfo: EnumeratedInstructionOrExpressionMetadata,
|
||||
parameterValues: ParameterValues
|
||||
parameterValues: ParameterValues,
|
||||
options: {| shouldConvertToString: boolean |}
|
||||
): string => {
|
||||
const functionName = expressionInfo.name || '';
|
||||
let functionCall = '';
|
||||
|
||||
if (expressionInfo.scope.objectMetadata) {
|
||||
const objectName = parameterValues[0];
|
||||
@@ -97,7 +99,7 @@ export const formatExpressionCall = (
|
||||
expressionInfo.metadata,
|
||||
1
|
||||
).join(', ');
|
||||
return `${objectName}.${functionName}(${functionArgs})`;
|
||||
functionCall = `${objectName}.${functionName}(${functionArgs})`;
|
||||
} else if (expressionInfo.scope.behaviorMetadata) {
|
||||
const objectName = parameterValues[0];
|
||||
const behaviorName = parameterValues[1];
|
||||
@@ -107,13 +109,16 @@ export const formatExpressionCall = (
|
||||
expressionInfo.metadata,
|
||||
2
|
||||
).join(', ');
|
||||
return `${objectName}.${behaviorName}::${functionName}(${functionArgs})`;
|
||||
functionCall = `${objectName}.${behaviorName}::${functionName}(${functionArgs})`;
|
||||
} else {
|
||||
const functionArgs = filterVisibleParameters(
|
||||
parameterValues,
|
||||
expressionInfo.metadata,
|
||||
0
|
||||
).join(', ');
|
||||
return `${functionName}(${functionArgs})`;
|
||||
functionCall = `${functionName}(${functionArgs})`;
|
||||
}
|
||||
return options.shouldConvertToString
|
||||
? `ToString(${functionCall})`
|
||||
: functionCall;
|
||||
};
|
||||
|
@@ -12,71 +12,99 @@ import {
|
||||
|
||||
describe('FormatExpressionCall', () => {
|
||||
it('properly formats a free function, with one or more arguments', () => {
|
||||
const freeExpressions = enumerateFreeExpressions('number');
|
||||
const freeExpressions = enumerateFreeExpressions('number|string');
|
||||
const countExpression = filterExpressions(
|
||||
freeExpressions,
|
||||
'PickedInstancesCount'
|
||||
)[0];
|
||||
expect(formatExpressionCall(countExpression, ['MyObject'])).toBe(
|
||||
'PickedInstancesCount(MyObject)'
|
||||
);
|
||||
expect(
|
||||
formatExpressionCall(countExpression, ['MyObject'], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('PickedInstancesCount(MyObject)');
|
||||
|
||||
const atan2Expression = filterExpressions(freeExpressions, 'atan2')[0];
|
||||
expect(formatExpressionCall(atan2Expression, ['1', '2'])).toBe(
|
||||
'atan2(1, 2)'
|
||||
);
|
||||
expect(
|
||||
formatExpressionCall(atan2Expression, ['1', '2'], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('atan2(1, 2)');
|
||||
});
|
||||
|
||||
it('properly formats a free function, with "code-only" parameters', () => {
|
||||
const freeExpressions = enumerateFreeExpressions('number');
|
||||
const freeExpressions = enumerateFreeExpressions('number|string');
|
||||
const cameraHeightExpression = filterExpressions(
|
||||
freeExpressions,
|
||||
'CameraHeight'
|
||||
)[0];
|
||||
expect(
|
||||
formatExpressionCall(cameraHeightExpression, ['', '"My layer"', '0'])
|
||||
formatExpressionCall(cameraHeightExpression, ['', '"My layer"', '0'], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('CameraHeight("My layer", 0)');
|
||||
});
|
||||
|
||||
it('properly formats a free function, with "code-only" and optional parameters', () => {
|
||||
const freeExpressions = enumerateFreeExpressions('number');
|
||||
const freeExpressions = enumerateFreeExpressions('number|string');
|
||||
const touchExpression = filterExpressions(freeExpressions, 'TouchX')[0];
|
||||
expect(formatExpressionCall(touchExpression, ['', '1'])).toBe('TouchX(1)');
|
||||
expect(formatExpressionCall(touchExpression, ['', '1', '"My layer"'])).toBe(
|
||||
'TouchX(1, "My layer")'
|
||||
);
|
||||
expect(formatExpressionCall(touchExpression, ['', '1', '', ''])).toBe(
|
||||
'TouchX(1)'
|
||||
);
|
||||
expect(formatExpressionCall(touchExpression, ['', '1', '', '2'])).toBe(
|
||||
'TouchX(1, "", 2)'
|
||||
);
|
||||
expect(
|
||||
formatExpressionCall(touchExpression, ['', '1'], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('TouchX(1)');
|
||||
expect(
|
||||
formatExpressionCall(touchExpression, ['', '1', '"My layer"'], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('TouchX(1, "My layer")');
|
||||
expect(
|
||||
formatExpressionCall(touchExpression, ['', '1', '', ''], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('TouchX(1)');
|
||||
expect(
|
||||
formatExpressionCall(touchExpression, ['', '1', '', '2'], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('TouchX(1, "", 2)');
|
||||
});
|
||||
|
||||
it('properly formats an object function', () => {
|
||||
const objectsExpressions = enumerateObjectExpressions('number', 'Sprite');
|
||||
const objectsExpressions = enumerateObjectExpressions(
|
||||
'number|string',
|
||||
'Sprite'
|
||||
);
|
||||
const variableStringExpression = filterExpressions(
|
||||
objectsExpressions,
|
||||
'Variable'
|
||||
)[0];
|
||||
expect(variableStringExpression).not.toBeUndefined();
|
||||
expect(
|
||||
formatExpressionCall(variableStringExpression, ['MyObject', 'Variable1'])
|
||||
formatExpressionCall(
|
||||
variableStringExpression,
|
||||
['MyObject', 'Variable1'],
|
||||
{ shouldConvertToString: false }
|
||||
)
|
||||
).toBe('MyObject.Variable(Variable1)');
|
||||
});
|
||||
|
||||
it('properly formats an object function with an argument', () => {
|
||||
const objectsExpressions = enumerateObjectExpressions('number', 'Sprite');
|
||||
const objectsExpressions = enumerateObjectExpressions(
|
||||
'number|string',
|
||||
'Sprite'
|
||||
);
|
||||
const pointXExpression = filterExpressions(objectsExpressions, 'PointX')[0];
|
||||
expect(pointXExpression).not.toBeUndefined();
|
||||
expect(
|
||||
formatExpressionCall(pointXExpression, ['MyObject', '"MyPoint"'])
|
||||
formatExpressionCall(pointXExpression, ['MyObject', '"MyPoint"'], {
|
||||
shouldConvertToString: false,
|
||||
})
|
||||
).toBe('MyObject.PointX("MyPoint")');
|
||||
});
|
||||
|
||||
it('properly formats an object behavior function', () => {
|
||||
const behaviorsExpressions = enumerateBehaviorExpressions(
|
||||
'number',
|
||||
'number|string',
|
||||
'PlatformBehavior::PlatformerObjectBehavior'
|
||||
);
|
||||
const jumpSpeedExpression = filterExpressions(
|
||||
@@ -85,15 +113,38 @@ describe('FormatExpressionCall', () => {
|
||||
)[0];
|
||||
expect(jumpSpeedExpression).not.toBeUndefined();
|
||||
expect(
|
||||
formatExpressionCall(jumpSpeedExpression, [
|
||||
'MyObject',
|
||||
'PlatformerObject',
|
||||
])
|
||||
formatExpressionCall(
|
||||
jumpSpeedExpression,
|
||||
['MyObject', 'PlatformerObject'],
|
||||
{ shouldConvertToString: false }
|
||||
)
|
||||
).toBe('MyObject.PlatformerObject::JumpSpeed()');
|
||||
});
|
||||
|
||||
it('properly formats an object behavior function and converts to string', () => {
|
||||
const behaviorsExpressions = enumerateBehaviorExpressions(
|
||||
'number|string',
|
||||
'PlatformBehavior::PlatformerObjectBehavior'
|
||||
);
|
||||
const jumpSpeedExpression = filterExpressions(
|
||||
behaviorsExpressions,
|
||||
'JumpSpeed'
|
||||
)[0];
|
||||
expect(jumpSpeedExpression).not.toBeUndefined();
|
||||
expect(
|
||||
formatExpressionCall(
|
||||
jumpSpeedExpression,
|
||||
['MyObject', 'PlatformerObject'],
|
||||
{ shouldConvertToString: true }
|
||||
)
|
||||
).toBe('ToString(MyObject.PlatformerObject::JumpSpeed())');
|
||||
});
|
||||
|
||||
it('can return the visible parameters of a function', () => {
|
||||
const objectsExpressions = enumerateObjectExpressions('number', 'Sprite');
|
||||
const objectsExpressions = enumerateObjectExpressions(
|
||||
'number|string',
|
||||
'Sprite'
|
||||
);
|
||||
const pointXExpression = filterExpressions(objectsExpressions, 'PointX')[0];
|
||||
expect(pointXExpression).not.toBeUndefined();
|
||||
|
||||
|
@@ -268,12 +268,48 @@ export default class ExpressionField extends React.Component<Props, State> {
|
||||
parameterValues: ParameterValues
|
||||
) => {
|
||||
if (!this._inputElement) return;
|
||||
const {
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
expressionType,
|
||||
value,
|
||||
} = this.props;
|
||||
const cursorPosition = this._inputElement.selectionStart;
|
||||
const parser = new gd.ExpressionParser2();
|
||||
|
||||
const functionCall = formatExpressionCall(expressionInfo, parameterValues);
|
||||
// We want to know what type the expression should be so as to convert to string
|
||||
// when necessary.
|
||||
// We add a fake identifier so that getNodeAtPosition will return the type of
|
||||
// its parent. Particularly, this is needed to get the type of a parameter in
|
||||
// a function call. We could create a worker ExpectedTypeFinder that would get
|
||||
// the type wanted by the parent instead.
|
||||
const expressionNode = parser
|
||||
.parseExpression(
|
||||
value.substr(0, cursorPosition) +
|
||||
'fakeIdentifier' +
|
||||
value.substr(cursorPosition)
|
||||
)
|
||||
.get();
|
||||
const currentNode = gd.ExpressionNodeLocationFinder.getNodeAtPosition(
|
||||
expressionNode,
|
||||
cursorPosition + 'fakeIdentifier'.length - 1
|
||||
);
|
||||
const type = gd.ExpressionTypeFinder.getType(
|
||||
gd.JsPlatform.get(),
|
||||
globalObjectsContainer,
|
||||
objectsContainer,
|
||||
expressionType,
|
||||
currentNode
|
||||
);
|
||||
|
||||
let shouldConvertToString =
|
||||
expressionInfo.metadata.getReturnType() === 'number' && type === 'string';
|
||||
|
||||
const functionCall = formatExpressionCall(expressionInfo, parameterValues, {
|
||||
shouldConvertToString,
|
||||
});
|
||||
|
||||
// Generate the expression with the function call
|
||||
const { value } = this.props;
|
||||
const newValue =
|
||||
value.substr(0, cursorPosition) +
|
||||
functionCall +
|
||||
@@ -338,6 +374,10 @@ export default class ExpressionField extends React.Component<Props, State> {
|
||||
addParameterSeparator: expressionAutocompletion.addParameterSeparator,
|
||||
addNamespaceSeparator: expressionAutocompletion.addNamespaceSeparator,
|
||||
hasVisibleParameters: expressionAutocompletion.hasVisibleParameters,
|
||||
shouldConvertToString:
|
||||
expressionAutocompletion.kind === 'Expression'
|
||||
? expressionAutocompletion.shouldConvertToString
|
||||
: null,
|
||||
}
|
||||
);
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -43,6 +43,7 @@ export type ExpressionAutocompletion =
|
||||
...BaseExpressionAutocompletion,
|
||||
kind: 'Expression',
|
||||
enumeratedExpressionMetadata: EnumeratedExpressionMetadata,
|
||||
shouldConvertToString: boolean,
|
||||
|}
|
||||
| {|
|
||||
...BaseExpressionAutocompletion,
|
||||
@@ -88,7 +89,8 @@ const getAutocompletionsForExpressions = (
|
||||
prefix: string,
|
||||
replacementStartPosition: number,
|
||||
replacementEndPosition: number,
|
||||
isExact: boolean
|
||||
isExact: boolean,
|
||||
completionType: string
|
||||
): Array<ExpressionAutocompletion> => {
|
||||
return expressionMetadatas
|
||||
.filter(
|
||||
@@ -113,6 +115,9 @@ const getAutocompletionsForExpressions = (
|
||||
hasVisibleParameters:
|
||||
getVisibleParameterTypes(enumeratedExpressionMetadata).length !== 0,
|
||||
isExact,
|
||||
shouldConvertToString:
|
||||
completionType === 'string' &&
|
||||
enumeratedExpressionMetadata.metadata.getReturnType() === 'number',
|
||||
};
|
||||
})
|
||||
.filter(Boolean);
|
||||
@@ -137,7 +142,8 @@ const getAutocompletionsForFreeExpressions = function(
|
||||
prefix,
|
||||
completionDescription.getReplacementStartPosition(),
|
||||
completionDescription.getReplacementEndPosition(),
|
||||
isExact
|
||||
isExact,
|
||||
type
|
||||
);
|
||||
};
|
||||
|
||||
@@ -172,7 +178,8 @@ const getAutocompletionsForObjectExpressions = function(
|
||||
prefix,
|
||||
completionDescription.getReplacementStartPosition(),
|
||||
completionDescription.getReplacementEndPosition(),
|
||||
isExact
|
||||
isExact,
|
||||
type
|
||||
);
|
||||
};
|
||||
|
||||
@@ -210,7 +217,8 @@ const getAutocompletionsForBehaviorExpressions = function(
|
||||
prefix,
|
||||
completionDescription.getReplacementStartPosition(),
|
||||
completionDescription.getReplacementEndPosition(),
|
||||
isExact
|
||||
isExact,
|
||||
type
|
||||
);
|
||||
};
|
||||
|
||||
@@ -513,6 +521,7 @@ type InsertedAutocompletion = {|
|
||||
addDot?: ?boolean,
|
||||
addParameterSeparator?: ?boolean,
|
||||
addNamespaceSeparator?: ?boolean,
|
||||
shouldConvertToString?: ?boolean,
|
||||
|};
|
||||
|
||||
type ExpressionAndCaretLocation = {|
|
||||
@@ -520,25 +529,119 @@ type ExpressionAndCaretLocation = {|
|
||||
caretLocation: number,
|
||||
|};
|
||||
|
||||
/**
|
||||
* Returns the position where the last expression call (node) starts,
|
||||
* so that we know where to insert the ToString call.
|
||||
* For example:
|
||||
* '"HelloWorld" + Object.Behavior::' should return 15.
|
||||
*/
|
||||
const findLastNodeStartPosition = (expression: string) => {
|
||||
let match;
|
||||
let indexes = [];
|
||||
// We consider that expressions are composed of letters, digits, dot or colons
|
||||
const expressionSeparatorRegex = new RegExp(/[^\w\d.:]/g);
|
||||
while ((match = expressionSeparatorRegex.exec(expression))) {
|
||||
indexes.push(match.index);
|
||||
}
|
||||
return Math.max(...indexes);
|
||||
};
|
||||
|
||||
const insertWordInExpressionWithToString = ({
|
||||
expression,
|
||||
wordStartPosition,
|
||||
wordEndPosition,
|
||||
insertedWord,
|
||||
}: {
|
||||
expression: string,
|
||||
wordStartPosition: number,
|
||||
wordEndPosition: number,
|
||||
insertedWord: string,
|
||||
}) => {
|
||||
const expressionStart = expression.substring(0, wordStartPosition);
|
||||
// If the grammar is becoming more complex, you'll need to implement a
|
||||
// NodeParenthesesBoundsFinder worker to specify the proper bounds for each node
|
||||
// or give this responsibility to `ExpressionCompletionDescription`/`ExpressionCompletionFinder`.
|
||||
const completedNodeStartPosition: number = findLastNodeStartPosition(
|
||||
expressionStart
|
||||
);
|
||||
|
||||
const newExpressionStart = expression.substring(
|
||||
0,
|
||||
completedNodeStartPosition + 1
|
||||
);
|
||||
const completedNodeStart = expression.substring(
|
||||
completedNodeStartPosition + 1,
|
||||
wordStartPosition
|
||||
);
|
||||
|
||||
const newExpressionEnd = expression.substring(wordEndPosition);
|
||||
return {
|
||||
expression:
|
||||
newExpressionStart +
|
||||
'ToString(' +
|
||||
completedNodeStart +
|
||||
insertedWord +
|
||||
')' +
|
||||
newExpressionEnd,
|
||||
// We place the caret before the closing parenthesis of ToString()
|
||||
// so that the user's typing flow is not interrupted.
|
||||
caretLocation:
|
||||
newExpressionStart.length +
|
||||
completedNodeStart.length +
|
||||
insertedWord.length +
|
||||
'ToString('.length,
|
||||
};
|
||||
};
|
||||
|
||||
const insertWordInExpression = ({
|
||||
expression,
|
||||
wordStartPosition,
|
||||
wordEndPosition,
|
||||
insertedWord,
|
||||
}: {
|
||||
expression: string,
|
||||
wordStartPosition: number,
|
||||
wordEndPosition: number,
|
||||
insertedWord: string,
|
||||
}) => {
|
||||
const newExpressionStart = expression.substring(0, wordStartPosition);
|
||||
const newExpressionEnd = expression.substring(wordEndPosition);
|
||||
return {
|
||||
expression: newExpressionStart + insertedWord + newExpressionEnd,
|
||||
caretLocation: newExpressionStart.length + insertedWord.length,
|
||||
};
|
||||
};
|
||||
|
||||
export const insertAutocompletionInExpression = (
|
||||
{ expression, caretLocation }: ExpressionAndCaretLocation,
|
||||
insertedAutocompletion: InsertedAutocompletion
|
||||
): ExpressionAndCaretLocation => {
|
||||
const {
|
||||
addDot,
|
||||
addParameterSeparator,
|
||||
addNamespaceSeparator,
|
||||
addParenthesis,
|
||||
completion,
|
||||
shouldConvertToString,
|
||||
replacementStartPosition,
|
||||
replacementEndPosition,
|
||||
hasVisibleParameters,
|
||||
} = insertedAutocompletion;
|
||||
const formatCompletion = (nextCharacter: ?string) => {
|
||||
const suffix = insertedAutocompletion.addDot
|
||||
const suffix = addDot
|
||||
? '.'
|
||||
: insertedAutocompletion.addParameterSeparator
|
||||
: addParameterSeparator
|
||||
? ', '
|
||||
: insertedAutocompletion.addNamespaceSeparator
|
||||
: addNamespaceSeparator
|
||||
? '::'
|
||||
: insertedAutocompletion.addParenthesis
|
||||
: addParenthesis
|
||||
? '()'
|
||||
: '';
|
||||
|
||||
const addSuffix =
|
||||
!nextCharacter || !suffix || nextCharacter[0] !== suffix[0];
|
||||
|
||||
return insertedAutocompletion.completion + (addSuffix ? suffix : '');
|
||||
return completion + (addSuffix ? suffix : '');
|
||||
};
|
||||
|
||||
if (caretLocation > expression.length) {
|
||||
@@ -549,35 +652,42 @@ export const insertAutocompletionInExpression = (
|
||||
const newExpression = formatCompletion(undefined) + expression;
|
||||
return {
|
||||
caretLocation: newExpression.length,
|
||||
expression: newExpression,
|
||||
expression: shouldConvertToString
|
||||
? `ToString(${newExpression})`
|
||||
: newExpression,
|
||||
};
|
||||
}
|
||||
|
||||
const wordStartPosition: number = insertedAutocompletion.replacementStartPosition
|
||||
? insertedAutocompletion.replacementStartPosition
|
||||
const wordStartPosition: number = replacementStartPosition
|
||||
? replacementStartPosition
|
||||
: 0;
|
||||
const wordEndPosition: number = insertedAutocompletion.replacementEndPosition
|
||||
? insertedAutocompletion.replacementEndPosition
|
||||
const wordEndPosition: number = replacementEndPosition
|
||||
? replacementEndPosition
|
||||
: expression.length;
|
||||
|
||||
// The next character, if any, will be useful to format the completion
|
||||
// (to avoid repeating an existing character).
|
||||
const maybeNextCharacter: ?string = expression[wordEndPosition];
|
||||
|
||||
const newExpressionStart = expression.substring(0, wordStartPosition);
|
||||
const insertedWord = formatCompletion(maybeNextCharacter);
|
||||
const newExpressionEnd = expression.substring(wordEndPosition);
|
||||
const newExpression = newExpressionStart + insertedWord + newExpressionEnd;
|
||||
let newCaretLocation = newExpressionStart.length + insertedWord.length;
|
||||
if (
|
||||
insertedAutocompletion.addParenthesis &&
|
||||
insertedAutocompletion.hasVisibleParameters
|
||||
) {
|
||||
newCaretLocation = newCaretLocation - 1;
|
||||
|
||||
const newAutocompletedExpression = shouldConvertToString
|
||||
? insertWordInExpressionWithToString({
|
||||
expression,
|
||||
insertedWord,
|
||||
wordEndPosition,
|
||||
wordStartPosition,
|
||||
})
|
||||
: insertWordInExpression({
|
||||
expression,
|
||||
insertedWord,
|
||||
wordEndPosition,
|
||||
wordStartPosition,
|
||||
});
|
||||
|
||||
if (addParenthesis && hasVisibleParameters) {
|
||||
newAutocompletedExpression.caretLocation =
|
||||
newAutocompletedExpression.caretLocation - 1;
|
||||
}
|
||||
|
||||
return {
|
||||
caretLocation: newCaretLocation,
|
||||
expression: newExpression,
|
||||
};
|
||||
return newAutocompletedExpression;
|
||||
};
|
||||
|
249
newIDE/app/src/GameDashboard/GameAnalyticsPanel.js
Normal file
249
newIDE/app/src/GameDashboard/GameAnalyticsPanel.js
Normal file
@@ -0,0 +1,249 @@
|
||||
// @flow
|
||||
import { Trans, t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import * as React from 'react';
|
||||
import { formatISO } from 'date-fns';
|
||||
import { Line, Spacer } from '../UI/Grid';
|
||||
import { type Game } from '../Utils/GDevelopServices/Game';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import Text from '../UI/Text';
|
||||
import {
|
||||
type GameMetrics,
|
||||
getGameMetrics,
|
||||
} from '../Utils/GDevelopServices/Analytics';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import { CircularProgress } from '@material-ui/core';
|
||||
import { Table, TableBody, TableRow, TableRowColumn } from '../UI/Table';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import { type PublicGame } from '../Utils/GDevelopServices/Game';
|
||||
|
||||
const styles = {
|
||||
tableRowStatColumn: {
|
||||
width: 100,
|
||||
},
|
||||
};
|
||||
|
||||
type Props = {|
|
||||
game: Game,
|
||||
publicGame: ?PublicGame,
|
||||
|};
|
||||
|
||||
export const GameAnalyticsPanel = ({ game, publicGame }: Props) => {
|
||||
const { getAuthorizationHeader, profile } = React.useContext(
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const [gameRollingMetrics, setGameMetrics] = React.useState<?GameMetrics>(
|
||||
null
|
||||
);
|
||||
const [gameRollingMetricsError, setGameMetricsError] = React.useState<?Error>(
|
||||
null
|
||||
);
|
||||
const [isGameMetricsLoading, setIsGameMetricsLoading] = React.useState(false);
|
||||
|
||||
const yesterdayIsoDate = formatISO(subDays(new Date(), 1), {
|
||||
representation: 'date',
|
||||
});
|
||||
const [analyticsDate, setAnalyticsDate] = React.useState(yesterdayIsoDate);
|
||||
|
||||
const loadGameMetrics = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
|
||||
const { id } = profile;
|
||||
|
||||
setIsGameMetricsLoading(true);
|
||||
setGameMetricsError(null);
|
||||
try {
|
||||
const gameRollingMetrics = await getGameMetrics(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
game.id,
|
||||
analyticsDate
|
||||
);
|
||||
setGameMetrics(gameRollingMetrics);
|
||||
} catch (err) {
|
||||
console.error(`Unable to load game rolling metrics:`, err);
|
||||
setGameMetricsError(err);
|
||||
}
|
||||
setIsGameMetricsLoading(false);
|
||||
},
|
||||
[getAuthorizationHeader, profile, game, analyticsDate]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
loadGameMetrics();
|
||||
},
|
||||
[loadGameMetrics]
|
||||
);
|
||||
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) =>
|
||||
gameRollingMetricsError ? (
|
||||
<PlaceholderError
|
||||
onRetry={() => {
|
||||
loadGameMetrics();
|
||||
}}
|
||||
>
|
||||
<Trans>There was an issue getting the game analytics.</Trans>{' '}
|
||||
<Trans>Verify your internet connection or try again later.</Trans>
|
||||
</PlaceholderError>
|
||||
) : (
|
||||
<ColumnStackLayout expand>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Consolidated metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{!publicGame && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last week sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame && publicGame.cachedLastWeekSessionsCount
|
||||
? publicGame.cachedLastWeekSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last year sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame && publicGame.cachedLastYearSessionsCount
|
||||
? publicGame.cachedLastYearSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Daily metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{isGameMetricsLoading && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Day</Trans>}
|
||||
value={analyticsDate}
|
||||
onChange={(_, _index, newIsoDate) => {
|
||||
setAnalyticsDate(newIsoDate);
|
||||
}}
|
||||
>
|
||||
{Array(5)
|
||||
.fill('')
|
||||
.map((_, index) => {
|
||||
const isoDate = formatISO(subDays(new Date(), index + 2), {
|
||||
representation: 'date',
|
||||
});
|
||||
return (
|
||||
<SelectOption
|
||||
key={isoDate}
|
||||
value={isoDate}
|
||||
primaryText={isoDate}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.reverse()}
|
||||
<SelectOption
|
||||
value={yesterdayIsoDate}
|
||||
primaryText={t`Yesterday`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={formatISO(new Date(), {
|
||||
representation: 'date',
|
||||
})}
|
||||
primaryText={t`Today (so far, in real time)`}
|
||||
/>
|
||||
</SelectField>
|
||||
</Line>
|
||||
{!isGameMetricsLoading && !gameRollingMetrics ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
There were no players or stored metrics for this day. Be sure
|
||||
to publish your game and get players to try it to see the
|
||||
collected anonymous analytics.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0Players
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.sessions
|
||||
? gameRollingMetrics.sessions.d0Sessions
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>New players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0NewPlayers
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{gameRollingMetrics &&
|
||||
(!gameRollingMetrics.retention || !gameRollingMetrics.players) ? (
|
||||
<AlertMessage kind="info">
|
||||
Upgrade your account with a subscription to unlock all the
|
||||
metrics for your game.
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
|
||||
<TableRow key={dayIndex}>
|
||||
<TableRowColumn>
|
||||
<Trans>Day {dayIndex} retained players</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics &&
|
||||
gameRollingMetrics.retention &&
|
||||
gameRollingMetrics.retention[
|
||||
`d${dayIndex}RetainedPlayers`
|
||||
] != null
|
||||
? gameRollingMetrics.retention[
|
||||
`d${dayIndex}RetainedPlayers`
|
||||
]
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
}
|
||||
</I18n>
|
||||
);
|
||||
};
|
@@ -3,7 +3,6 @@ import { Trans, t } from '@lingui/macro';
|
||||
import { I18n } from '@lingui/react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
import { formatISO } from 'date-fns';
|
||||
import FlatButton from '../UI/FlatButton';
|
||||
import { Line, Spacer } from '../UI/Grid';
|
||||
import {
|
||||
@@ -20,19 +19,13 @@ import Dialog from '../UI/Dialog';
|
||||
import { Tab, Tabs } from '../UI/Tabs';
|
||||
import { ColumnStackLayout } from '../UI/Layout';
|
||||
import Text from '../UI/Text';
|
||||
import {
|
||||
type GameMetrics,
|
||||
getGameMetrics,
|
||||
} from '../Utils/GDevelopServices/Analytics';
|
||||
import AuthenticatedUserContext from '../Profile/AuthenticatedUserContext';
|
||||
import PlaceholderError from '../UI/PlaceholderError';
|
||||
import SelectField from '../UI/SelectField';
|
||||
import SelectOption from '../UI/SelectOption';
|
||||
import { Chip, CircularProgress } from '@material-ui/core';
|
||||
import { Table, TableBody, TableRow, TableRowColumn } from '../UI/Table';
|
||||
import { Chip } from '@material-ui/core';
|
||||
import Builds from '../Export/Builds';
|
||||
import AlertMessage from '../UI/AlertMessage';
|
||||
import subDays from 'date-fns/subDays';
|
||||
import RaisedButton from '../UI/RaisedButton';
|
||||
import Window from '../Utils/Window';
|
||||
import HelpButton from '../UI/HelpButton';
|
||||
@@ -49,12 +42,7 @@ import SmartphoneIcon from '@material-ui/icons/Smartphone';
|
||||
import Crown from '../UI/CustomSvgIcons/Crown';
|
||||
import { showErrorBox, showWarningBox } from '../UI/Messages/MessageBox';
|
||||
import LeaderboardAdmin from './LeaderboardAdmin';
|
||||
|
||||
const styles = {
|
||||
tableRowStatColumn: {
|
||||
width: 100,
|
||||
},
|
||||
};
|
||||
import { GameAnalyticsPanel } from './GameAnalyticsPanel';
|
||||
|
||||
export type GamesDetailsTab =
|
||||
| 'details'
|
||||
@@ -83,25 +71,13 @@ export const GameDetailsDialog = ({
|
||||
AuthenticatedUserContext
|
||||
);
|
||||
const [currentTab, setCurrentTab] = React.useState(initialTab);
|
||||
const [gameRollingMetrics, setGameMetrics] = React.useState<?GameMetrics>(
|
||||
null
|
||||
);
|
||||
const [isLoading, setIsLoading] = React.useState<boolean>(false);
|
||||
const [
|
||||
gameUnregisterErrorText,
|
||||
setGameUnregisterErrorText,
|
||||
] = React.useState<?string>(null);
|
||||
const [gameRollingMetricsError, setGameMetricsError] = React.useState<?Error>(
|
||||
null
|
||||
);
|
||||
const [isGameMetricsLoading, setIsGameMetricsLoading] = React.useState(false);
|
||||
const [isGameUpdating, setIsGameUpdating] = React.useState(false);
|
||||
|
||||
const yesterdayIsoDate = formatISO(subDays(new Date(), 1), {
|
||||
representation: 'date',
|
||||
});
|
||||
const [analyticsDate, setAnalyticsDate] = React.useState(yesterdayIsoDate);
|
||||
|
||||
const authenticatedUser = React.useContext(AuthenticatedUserContext);
|
||||
const [publicGame, setPublicGame] = React.useState<?PublicGame>(null);
|
||||
const [publicGameError, setPublicGameError] = React.useState<?Error>(null);
|
||||
@@ -110,38 +86,6 @@ export const GameDetailsDialog = ({
|
||||
setIsPublicGamePropertiesDialogOpen,
|
||||
] = React.useState(false);
|
||||
|
||||
const loadGameMetrics = React.useCallback(
|
||||
async () => {
|
||||
if (!profile) return;
|
||||
|
||||
const { id } = profile;
|
||||
|
||||
setIsGameMetricsLoading(true);
|
||||
setGameMetricsError(null);
|
||||
try {
|
||||
const gameRollingMetrics = await getGameMetrics(
|
||||
getAuthorizationHeader,
|
||||
id,
|
||||
game.id,
|
||||
analyticsDate
|
||||
);
|
||||
setGameMetrics(gameRollingMetrics);
|
||||
} catch (err) {
|
||||
console.error(`Unable to load game rolling metrics:`, err);
|
||||
setGameMetricsError(err);
|
||||
}
|
||||
setIsGameMetricsLoading(false);
|
||||
},
|
||||
[getAuthorizationHeader, profile, game, analyticsDate]
|
||||
);
|
||||
|
||||
React.useEffect(
|
||||
() => {
|
||||
loadGameMetrics();
|
||||
},
|
||||
[loadGameMetrics]
|
||||
);
|
||||
|
||||
const loadPublicGame = React.useCallback(
|
||||
async () => {
|
||||
setPublicGameError(null);
|
||||
@@ -594,171 +538,7 @@ export const GameDetailsDialog = ({
|
||||
/>
|
||||
) : null}
|
||||
{currentTab === 'analytics' ? (
|
||||
gameRollingMetricsError ? (
|
||||
<PlaceholderError
|
||||
onRetry={() => {
|
||||
loadGameMetrics();
|
||||
}}
|
||||
>
|
||||
<Trans>There was an issue getting the game analytics.</Trans>{' '}
|
||||
<Trans>
|
||||
Verify your internet connection or try again later.
|
||||
</Trans>
|
||||
</PlaceholderError>
|
||||
) : (
|
||||
<ColumnStackLayout expand>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Consolidated metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{!publicGame && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last week sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame && publicGame.cachedLastWeekSessionsCount
|
||||
? publicGame.cachedLastWeekSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Last year sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{publicGame && publicGame.cachedLastYearSessionsCount
|
||||
? publicGame.cachedLastYearSessionsCount
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
<Line noMargin alignItems="center">
|
||||
<Text size="title">
|
||||
<Trans>Daily metrics</Trans>
|
||||
</Text>
|
||||
<Spacer />
|
||||
{isGameMetricsLoading && <CircularProgress size={20} />}
|
||||
</Line>
|
||||
<Line noMargin>
|
||||
<SelectField
|
||||
fullWidth
|
||||
floatingLabelText={<Trans>Day</Trans>}
|
||||
value={analyticsDate}
|
||||
onChange={(_, _index, newIsoDate) => {
|
||||
setAnalyticsDate(newIsoDate);
|
||||
}}
|
||||
>
|
||||
{Array(5)
|
||||
.fill('')
|
||||
.map((_, index) => {
|
||||
const isoDate = formatISO(
|
||||
subDays(new Date(), index + 2),
|
||||
{
|
||||
representation: 'date',
|
||||
}
|
||||
);
|
||||
return (
|
||||
<SelectOption
|
||||
key={isoDate}
|
||||
value={isoDate}
|
||||
primaryText={isoDate}
|
||||
/>
|
||||
);
|
||||
})
|
||||
.reverse()}
|
||||
<SelectOption
|
||||
value={yesterdayIsoDate}
|
||||
primaryText={t`Yesterday`}
|
||||
/>
|
||||
<SelectOption
|
||||
value={formatISO(new Date(), {
|
||||
representation: 'date',
|
||||
})}
|
||||
primaryText={t`Today (so far, in real time)`}
|
||||
/>
|
||||
</SelectField>
|
||||
</Line>
|
||||
{!isGameMetricsLoading && !gameRollingMetrics ? (
|
||||
<AlertMessage kind="warning">
|
||||
<Trans>
|
||||
There were no players or stored metrics for this day. Be
|
||||
sure to publish your game and get players to try it to
|
||||
see the collected anonymous analytics.
|
||||
</Trans>
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0Players
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>Sessions count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.sessions
|
||||
? gameRollingMetrics.sessions.d0Sessions
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
<TableRow>
|
||||
<TableRowColumn>
|
||||
<Trans>New players count</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics && gameRollingMetrics.players
|
||||
? gameRollingMetrics.players.d0NewPlayers
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
</TableBody>
|
||||
</Table>
|
||||
{gameRollingMetrics &&
|
||||
(!gameRollingMetrics.retention ||
|
||||
!gameRollingMetrics.players) ? (
|
||||
<AlertMessage kind="info">
|
||||
Upgrade your account with a subscription to unlock all the
|
||||
metrics for your game.
|
||||
</AlertMessage>
|
||||
) : null}
|
||||
<Table>
|
||||
<TableBody>
|
||||
{[1, 2, 3, 4, 5, 6, 7].map(dayIndex => (
|
||||
<TableRow key={dayIndex}>
|
||||
<TableRowColumn>
|
||||
<Trans>Day {dayIndex} retained players</Trans>
|
||||
</TableRowColumn>
|
||||
<TableRowColumn style={styles.tableRowStatColumn}>
|
||||
{gameRollingMetrics &&
|
||||
gameRollingMetrics.retention &&
|
||||
gameRollingMetrics.retention[
|
||||
`d${dayIndex}RetainedPlayers`
|
||||
] != null
|
||||
? gameRollingMetrics.retention[
|
||||
`d${dayIndex}RetainedPlayers`
|
||||
]
|
||||
: '-'}
|
||||
</TableRowColumn>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</ColumnStackLayout>
|
||||
)
|
||||
<GameAnalyticsPanel game={game} publicGame={publicGame} />
|
||||
) : null}
|
||||
</Line>
|
||||
{publicGame && project && isPublicGamePropertiesDialogOpen && (
|
||||
|
@@ -40,13 +40,16 @@ export const GamesShowcase = (props: Props) => {
|
||||
<ResponsiveWindowMeasurer>
|
||||
{windowWidth => (
|
||||
<Column expand noMargin useFullHeight>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
aspect="add-margins-only-if-modern-theme"
|
||||
placeholder={t`Search games`}
|
||||
/>
|
||||
<Line>
|
||||
<Column expand>
|
||||
<SearchBar
|
||||
value={searchText}
|
||||
onChange={setSearchText}
|
||||
onRequestSearch={() => {}}
|
||||
placeholder={t`Search games`}
|
||||
/>
|
||||
</Column>
|
||||
</Line>
|
||||
<Line
|
||||
expand
|
||||
overflow={
|
||||
|
@@ -30,9 +30,9 @@ const TutorialMessage = ({ tutorial }: Props) => {
|
||||
<img
|
||||
alt=""
|
||||
style={{
|
||||
maxWidth: 128,
|
||||
maxHeight: 128,
|
||||
width: 128,
|
||||
borderRadius: 4,
|
||||
aspectRatio: '16 / 9',
|
||||
}}
|
||||
src={tutorial.thumbnailUrl}
|
||||
/>
|
||||
|
@@ -10,14 +10,7 @@ const gd: libGDevelop = global.gd;
|
||||
|
||||
const GROUP_DELIMITER = '/';
|
||||
|
||||
// Support enumerate expressions that can be string, number or both ("number|string").
|
||||
// "number|string" has limited support in the codebase, enough to be used in expressions but not
|
||||
// in parameters and this is not a generic feature (you can't do "string|variable" or this kind of things).
|
||||
// This may be improved in the future if really necessary.
|
||||
const isPotentiallyStringType = (type: string) =>
|
||||
type === 'string' || type === 'number|string';
|
||||
const isPotentiallyNumberType = (type: string) =>
|
||||
type === 'number' || type === 'number|string';
|
||||
const shouldOnlyBeNumberType = (type: string) => type === 'number';
|
||||
|
||||
const enumerateExpressionMetadataMap = (
|
||||
prefix: string,
|
||||
@@ -70,8 +63,6 @@ export const enumerateFreeExpressions = (
|
||||
const allExtensions = gd
|
||||
.asPlatform(gd.JsPlatform.get())
|
||||
.getAllPlatformExtensions();
|
||||
const returnNumber = isPotentiallyNumberType(type);
|
||||
const returnString = isPotentiallyStringType(type);
|
||||
|
||||
return flatten(
|
||||
mapVector(allExtensions, extension => {
|
||||
@@ -83,20 +74,18 @@ export const enumerateFreeExpressions = (
|
||||
};
|
||||
|
||||
return [
|
||||
...(returnString
|
||||
...(!shouldOnlyBeNumberType(type)
|
||||
? enumerateExpressionMetadataMap(
|
||||
prefix,
|
||||
extension.getAllStrExpressions(),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...(returnNumber
|
||||
? enumerateExpressionMetadataMap(
|
||||
prefix,
|
||||
extension.getAllExpressions(),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...enumerateExpressionMetadataMap(
|
||||
prefix,
|
||||
extension.getAllExpressions(),
|
||||
scope
|
||||
),
|
||||
];
|
||||
})
|
||||
);
|
||||
@@ -116,20 +105,18 @@ export const enumerateObjectExpressions = (
|
||||
const scope = { extension, objectMetadata };
|
||||
|
||||
let objectsExpressions = [
|
||||
...(isPotentiallyStringType(type)
|
||||
...(!shouldOnlyBeNumberType(type)
|
||||
? enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllStrExpressionsForObject(objectType),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...(isPotentiallyNumberType(type)
|
||||
? enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllExpressionsForObject(objectType),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllExpressionsForObject(objectType),
|
||||
scope
|
||||
),
|
||||
];
|
||||
|
||||
const baseObjectType = ''; /* An empty string means the base object */
|
||||
@@ -142,20 +129,18 @@ export const enumerateObjectExpressions = (
|
||||
|
||||
objectsExpressions = [
|
||||
...objectsExpressions,
|
||||
...(isPotentiallyStringType(type)
|
||||
...(!shouldOnlyBeNumberType(type)
|
||||
? enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllStrExpressionsForObject(baseObjectType),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...(isPotentiallyNumberType(type)
|
||||
? enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllExpressionsForObject(baseObjectType),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllExpressionsForObject(baseObjectType),
|
||||
scope
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -176,20 +161,18 @@ export const enumerateBehaviorExpressions = (
|
||||
const scope = { extension, behaviorMetadata };
|
||||
|
||||
return [
|
||||
...(isPotentiallyStringType(type)
|
||||
...(!shouldOnlyBeNumberType(type)
|
||||
? enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllStrExpressionsForBehavior(behaviorType),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...(isPotentiallyNumberType(type)
|
||||
? enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllExpressionsForBehavior(behaviorType),
|
||||
scope
|
||||
)
|
||||
: []),
|
||||
...enumerateExpressionMetadataMap(
|
||||
'',
|
||||
extension.getAllExpressionsForBehavior(behaviorType),
|
||||
scope
|
||||
),
|
||||
];
|
||||
};
|
||||
|
||||
@@ -197,9 +180,6 @@ export const enumerateBehaviorExpressions = (
|
||||
export const enumerateAllExpressions = (
|
||||
type: string
|
||||
): Array<EnumeratedExpressionMetadata> => {
|
||||
const returnNumber = isPotentiallyNumberType(type);
|
||||
const returnString = isPotentiallyStringType(type);
|
||||
|
||||
const objectsExpressions = [];
|
||||
const behaviorsExpressions = [];
|
||||
const freeExpressions = enumerateFreeExpressions(type);
|
||||
@@ -215,7 +195,7 @@ export const enumerateAllExpressions = (
|
||||
const objectMetadata = extension.getObjectMetadata(objectType);
|
||||
const scope = { extension, objectMetadata };
|
||||
|
||||
if (returnString)
|
||||
if (!shouldOnlyBeNumberType(type))
|
||||
objectsExpressions.push.apply(
|
||||
objectsExpressions,
|
||||
enumerateExpressionMetadataMap(
|
||||
@@ -224,15 +204,14 @@ export const enumerateAllExpressions = (
|
||||
scope
|
||||
)
|
||||
);
|
||||
if (returnNumber)
|
||||
objectsExpressions.push.apply(
|
||||
objectsExpressions,
|
||||
enumerateExpressionMetadataMap(
|
||||
prefix,
|
||||
extension.getAllExpressionsForObject(objectType),
|
||||
scope
|
||||
)
|
||||
);
|
||||
objectsExpressions.push.apply(
|
||||
objectsExpressions,
|
||||
enumerateExpressionMetadataMap(
|
||||
prefix,
|
||||
extension.getAllExpressionsForObject(objectType),
|
||||
scope
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
//Behaviors expressions:
|
||||
@@ -240,7 +219,7 @@ export const enumerateAllExpressions = (
|
||||
const behaviorMetadata = extension.getBehaviorMetadata(behaviorType);
|
||||
const scope = { extension, behaviorMetadata };
|
||||
|
||||
if (returnString)
|
||||
if (!shouldOnlyBeNumberType(type))
|
||||
behaviorsExpressions.push.apply(
|
||||
behaviorsExpressions,
|
||||
enumerateExpressionMetadataMap(
|
||||
@@ -249,15 +228,14 @@ export const enumerateAllExpressions = (
|
||||
scope
|
||||
)
|
||||
);
|
||||
if (returnNumber)
|
||||
behaviorsExpressions.push.apply(
|
||||
behaviorsExpressions,
|
||||
enumerateExpressionMetadataMap(
|
||||
prefix,
|
||||
extension.getAllExpressionsForBehavior(behaviorType),
|
||||
scope
|
||||
)
|
||||
);
|
||||
behaviorsExpressions.push.apply(
|
||||
behaviorsExpressions,
|
||||
enumerateExpressionMetadataMap(
|
||||
prefix,
|
||||
extension.getAllExpressionsForBehavior(behaviorType),
|
||||
scope
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { type EnumeratedExpressionMetadata } from './EnumeratedInstructionOrExpr
|
||||
const gd: libGDevelop = global.gd;
|
||||
|
||||
describe('EnumerateExpressions', () => {
|
||||
it('can enumerate and filter free expressions', () => {
|
||||
it('can enumerate and filter free expressions (number only)', () => {
|
||||
const freeExpressions = enumerateFreeExpressions('number');
|
||||
|
||||
// Should find atan, atan2, atanh math function
|
||||
@@ -24,8 +24,9 @@ describe('EnumerateExpressions', () => {
|
||||
expect(filterExpressions(freeExpressions, 'MouseX')).toHaveLength(1);
|
||||
expect(filterExpressions(freeExpressions, 'MouseY')).toHaveLength(1);
|
||||
});
|
||||
it('can enumerate and filter free expressions (type "number|string")', () => {
|
||||
const freeExpressions = enumerateFreeExpressions('number|string');
|
||||
|
||||
it('can enumerate and filter free expressions', () => {
|
||||
const freeExpressions = enumerateFreeExpressions('string');
|
||||
|
||||
// Should find ToString and LargeNumberToString:
|
||||
expect(filterExpressions(freeExpressions, 'ToString')).toHaveLength(2);
|
||||
@@ -40,7 +41,7 @@ describe('EnumerateExpressions', () => {
|
||||
expect(filterExpressions(freeExpressions, 'MouseY')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('can enumerate and filter object expressions', () => {
|
||||
it('can enumerate and filter object expressions (number only)', () => {
|
||||
const spriteObjectExpressions = enumerateObjectExpressions(
|
||||
'number',
|
||||
'Sprite'
|
||||
@@ -58,9 +59,9 @@ describe('EnumerateExpressions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('can enumerate and filter object expressions (type "number|string")', () => {
|
||||
it('can enumerate object expressions', () => {
|
||||
const spriteObjectExpressions = enumerateObjectExpressions(
|
||||
'number|string',
|
||||
'string',
|
||||
'Sprite'
|
||||
);
|
||||
expect(filterExpressions(spriteObjectExpressions, 'PointX')).toHaveLength(
|
||||
@@ -70,7 +71,7 @@ describe('EnumerateExpressions', () => {
|
||||
filterExpressions(spriteObjectExpressions, 'AnimationName')
|
||||
).toHaveLength(1);
|
||||
|
||||
const objectExpressions = enumerateObjectExpressions('number|string', '');
|
||||
const objectExpressions = enumerateObjectExpressions('string', '');
|
||||
expect(filterExpressions(objectExpressions, 'PointX')).toHaveLength(0);
|
||||
expect(filterExpressions(objectExpressions, 'Layer')).toHaveLength(1);
|
||||
expect(filterExpressions(objectExpressions, 'X')).toContainEqual(
|
||||
@@ -80,7 +81,7 @@ describe('EnumerateExpressions', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('can enumerate and filter behavior expressions', () => {
|
||||
it('can enumerate and filter behavior expressions (number only)', () => {
|
||||
const platformerObjectBehaviorExpressions = enumerateBehaviorExpressions(
|
||||
'number',
|
||||
'PlatformBehavior::PlatformerObjectBehavior'
|
||||
@@ -103,10 +104,11 @@ describe('EnumerateExpressions', () => {
|
||||
})
|
||||
);
|
||||
});
|
||||
it('can enumerate and filter behavior expressions (type "number|string")', () => {
|
||||
|
||||
it('can enumerate behavior expressions', () => {
|
||||
makeTestExtensions(gd);
|
||||
const fakeBehaviorExpressions = enumerateBehaviorExpressions(
|
||||
'number|string',
|
||||
'string',
|
||||
'FakeBehavior::FakeBehavior'
|
||||
);
|
||||
|
||||
@@ -145,7 +147,7 @@ describe('EnumerateExpressions', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('can enumerate all expressions (type "number")', () => {
|
||||
it('can enumerate all expressions (number only)', () => {
|
||||
makeTestExtensions(gd);
|
||||
const allNumberExpressions: Array<EnumeratedExpressionMetadata> = enumerateAllExpressions(
|
||||
'number'
|
||||
@@ -173,38 +175,10 @@ describe('EnumerateExpressions', () => {
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('can enumerate all expressions (type "string")', () => {
|
||||
makeTestExtensions(gd);
|
||||
const allStringExpressions: Array<EnumeratedExpressionMetadata> = enumerateAllExpressions(
|
||||
'string'
|
||||
);
|
||||
// Check a free expression:
|
||||
expect(allStringExpressions).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'ToString',
|
||||
})
|
||||
);
|
||||
// Check a behavior expression:
|
||||
expect(allStringExpressions).toContainEqual(
|
||||
expect.objectContaining({
|
||||
type: 'SomethingReturningStringWith1NumberParam',
|
||||
})
|
||||
);
|
||||
|
||||
// Sanity check number expressions are not there:
|
||||
expect(filterExpressions(allStringExpressions, 'ToNumber')).toHaveLength(0);
|
||||
expect(
|
||||
filterExpressions(
|
||||
allStringExpressions,
|
||||
'SomethingReturningNumberWith1NumberParam'
|
||||
)
|
||||
).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('can enumerate all expressions (type "number|string")', () => {
|
||||
it('can enumerate all expressions', () => {
|
||||
makeTestExtensions(gd);
|
||||
const allExpressions: Array<EnumeratedExpressionMetadata> = enumerateAllExpressions(
|
||||
'number|string'
|
||||
'string'
|
||||
);
|
||||
// Check a free expression:
|
||||
expect(allExpressions).toContainEqual(
|
||||
|
@@ -44,7 +44,11 @@ const freeActionsToAddToBehavior: ExtensionsExtraInstructions = {};
|
||||
|
||||
const freeConditionsToAddToBehavior: ExtensionsExtraInstructions = {
|
||||
Physics2: {
|
||||
'Physics2::Physics2Behavior': ['Physics2::Collision'],
|
||||
'Physics2::Physics2Behavior': [
|
||||
'Physics2::Collision',
|
||||
'Physics2::CollisionStarted',
|
||||
'Physics2::CollisionStopped',
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
|
@@ -54,6 +54,13 @@ export const loadPreferencesFromLocalStorage = (): ?PreferencesValues => {
|
||||
}
|
||||
}
|
||||
|
||||
// Migrate renamed themes.
|
||||
if (values.themeName === 'GDevelop default') {
|
||||
values.themeName = 'GDevelop default Light';
|
||||
} else if (values.themeName === 'Dark') {
|
||||
values.themeName = 'Blue Dark';
|
||||
}
|
||||
|
||||
return values;
|
||||
} catch (e) {
|
||||
return null;
|
||||
|
@@ -36,6 +36,7 @@ type Props = {|
|
||||
hideControls?: boolean,
|
||||
initialZoom?: number,
|
||||
fixedHeight?: number,
|
||||
fixedWidth?: number,
|
||||
|};
|
||||
|
||||
const AnimationPreview = ({
|
||||
@@ -50,6 +51,7 @@ const AnimationPreview = ({
|
||||
hideControls,
|
||||
initialZoom,
|
||||
fixedHeight,
|
||||
fixedWidth,
|
||||
}: Props) => {
|
||||
const forceUdpate = useForceUpdate();
|
||||
|
||||
@@ -171,6 +173,7 @@ const AnimationPreview = ({
|
||||
hideCheckeredBackground={hideCheckeredBackground}
|
||||
hideControls={hideControls}
|
||||
fixedHeight={fixedHeight}
|
||||
fixedWidth={fixedWidth}
|
||||
/>
|
||||
{!hideControls && (
|
||||
<LineStackLayout noMargin alignItems="center">
|
||||
|
@@ -48,9 +48,6 @@ const ObjectsEditorService = {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(
|
||||
`Properly registered editor configuration for object "${objectType}".`
|
||||
);
|
||||
this.editorConfigurations[objectType] = editorConfiguration;
|
||||
},
|
||||
getDefaultObjectJsImplementationPropertiesEditor(options) {
|
||||
|
@@ -107,7 +107,6 @@ const ObjectsRenderingService = {
|
||||
return;
|
||||
}
|
||||
|
||||
console.info(`Properly registered renderer for object "${objectType}".`);
|
||||
this.renderers[objectType] = renderer;
|
||||
},
|
||||
/**
|
||||
|
@@ -40,7 +40,6 @@ const styles = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
boxSizing: 'border-box',
|
||||
overflow: 'auto',
|
||||
|
||||
// The container contains the image and the "overlay" that can display
|
||||
// points or polygons that can be drag'n'dropped. `touch-action` must
|
||||
@@ -51,7 +50,6 @@ const styles = {
|
||||
spriteThumbnailImage: {
|
||||
position: 'relative',
|
||||
pointerEvents: 'none',
|
||||
margin: MARGIN,
|
||||
},
|
||||
sliderContainer: {
|
||||
maxWidth: 150,
|
||||
@@ -68,6 +66,7 @@ type Props = {|
|
||||
isImageResourceSmooth: boolean,
|
||||
initialZoom?: number,
|
||||
fixedHeight?: number,
|
||||
fixedWidth?: number,
|
||||
renderOverlay?: ({|
|
||||
imageWidth: number,
|
||||
imageHeight: number,
|
||||
@@ -100,6 +99,7 @@ const ImagePreview = ({
|
||||
imageResourceSource,
|
||||
isImageResourceSmooth,
|
||||
fixedHeight,
|
||||
fixedWidth,
|
||||
renderOverlay,
|
||||
onSize,
|
||||
hideCheckeredBackground,
|
||||
@@ -166,37 +166,49 @@ const ImagePreview = ({
|
||||
{({ contentRect, measureRef }) => {
|
||||
const containerWidth = contentRect.bounds.width;
|
||||
const containerHeight = contentRect.bounds.height;
|
||||
// Once the image is loaded, adapt the zoom to the image size.
|
||||
if (!isResizeObserverReady && !!containerWidth && !!containerHeight) {
|
||||
const containerLoaded = !!containerWidth && !!containerHeight;
|
||||
const imageLoaded = !!imageWidth && !!imageHeight && !errored;
|
||||
|
||||
// Once the container is loaded, adapt the zoom to the image size.
|
||||
if (!isResizeObserverReady && containerLoaded) {
|
||||
if (!initialZoom) {
|
||||
adaptZoomToImage(containerHeight, containerWidth);
|
||||
}
|
||||
setIsResizeObserverReady(true);
|
||||
}
|
||||
|
||||
const imageLoaded = !!imageWidth && !!imageHeight && !errored;
|
||||
|
||||
// Centre-align the image and overlays
|
||||
const imagePositionTop = Math.max(
|
||||
0,
|
||||
containerHeight / 2 -
|
||||
(containerHeight || 0) / 2 -
|
||||
((imageHeight || 0) * imageZoomFactor) / 2 -
|
||||
MARGIN
|
||||
);
|
||||
const imagePositionLeft = Math.max(
|
||||
0,
|
||||
containerWidth / 2 -
|
||||
(containerWidth || 0) / 2 -
|
||||
((imageWidth || 0) * imageZoomFactor) / 2 -
|
||||
MARGIN
|
||||
);
|
||||
|
||||
// We display the elements only when the image is loaded and
|
||||
// the zoom is applied to avoid a shift in the image.
|
||||
// We use "visibility": "hidden" instead of "display": "none"
|
||||
// so that the image takes the space of the container whilst being hidden.
|
||||
// TODO: handle a proper loader.
|
||||
const visibility = containerLoaded ? undefined : 'hidden';
|
||||
const width = imageWidth ? imageWidth * imageZoomFactor : undefined;
|
||||
const height = imageHeight ? imageHeight * imageZoomFactor : undefined;
|
||||
|
||||
const imageStyle = {
|
||||
...styles.spriteThumbnailImage,
|
||||
top: imagePositionTop || 0,
|
||||
left: imagePositionLeft || 0,
|
||||
width: imageWidth ? imageWidth * imageZoomFactor : undefined,
|
||||
height: imageHeight ? imageHeight * imageZoomFactor : undefined,
|
||||
visibility: imageLoaded ? undefined : 'hidden', // TODO: Loader
|
||||
// Apply margin only once the container is loaded, to avoid a shift in the image
|
||||
margin: containerLoaded ? MARGIN : 0,
|
||||
top: imagePositionTop,
|
||||
left: imagePositionLeft,
|
||||
width,
|
||||
height,
|
||||
visibility,
|
||||
...(!isImageResourceSmooth
|
||||
? styles.previewImagePixelated
|
||||
: undefined),
|
||||
@@ -204,11 +216,11 @@ const ImagePreview = ({
|
||||
|
||||
const frameStyle = {
|
||||
position: 'absolute',
|
||||
top: imagePositionTop + MARGIN || 0,
|
||||
left: imagePositionLeft + MARGIN || 0,
|
||||
width: imageWidth ? imageWidth * imageZoomFactor : undefined,
|
||||
height: imageHeight ? imageHeight * imageZoomFactor : undefined,
|
||||
visibility: imageLoaded ? undefined : 'hidden', // TODO: Loader
|
||||
top: imagePositionTop + MARGIN,
|
||||
left: imagePositionLeft + MARGIN,
|
||||
width,
|
||||
height,
|
||||
visibility,
|
||||
border: `1px solid ${frameBorderColor}`,
|
||||
boxSizing: 'border-box',
|
||||
};
|
||||
@@ -219,7 +231,7 @@ const ImagePreview = ({
|
||||
left: 0,
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
visibility: imageLoaded ? undefined : 'hidden', // TODO: Loader
|
||||
visibility,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -261,6 +273,7 @@ const ImagePreview = ({
|
||||
style={{
|
||||
...styles.contentContainer,
|
||||
height: fixedHeight || '100%',
|
||||
width: fixedWidth,
|
||||
}}
|
||||
>
|
||||
{!hideCheckeredBackground && <CheckeredBackground />}
|
||||
@@ -268,7 +281,10 @@ const ImagePreview = ({
|
||||
dir={
|
||||
'ltr' /* Force LTR layout to avoid issues with image positioning */
|
||||
}
|
||||
style={styles.imagePreviewContainer}
|
||||
style={{
|
||||
...styles.imagePreviewContainer,
|
||||
overflow: containerLoaded ? 'auto' : 'hidden',
|
||||
}}
|
||||
ref={measureRef}
|
||||
onWheel={event => {
|
||||
const { deltaY } = event;
|
||||
|
@@ -292,12 +292,25 @@ export default class ResourcesList extends React.Component<Props, State> {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
label: i18n._(t`Remove unused...`),
|
||||
submenu: allResourceKindsAndMetadata.map(({ displayName, kind }) => ({
|
||||
label: i18n._(displayName),
|
||||
click: () => {
|
||||
this.props.onRemoveUnusedResources(kind);
|
||||
},
|
||||
})),
|
||||
submenu: allResourceKindsAndMetadata
|
||||
.map(({ displayName, kind }) => ({
|
||||
label: i18n._(displayName),
|
||||
click: () => {
|
||||
this.props.onRemoveUnusedResources(kind);
|
||||
},
|
||||
}))
|
||||
.concat([
|
||||
{
|
||||
label: i18n._(t`Resources (any kind)`),
|
||||
click: () => {
|
||||
allResourceKindsAndMetadata.forEach(resourceKindAndMetadata => {
|
||||
this.props.onRemoveUnusedResources(
|
||||
resourceKindAndMetadata.kind
|
||||
);
|
||||
});
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Remove Resources with Invalid Path`),
|
||||
|
@@ -1,8 +1,9 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import MUICheckbox from '@material-ui/core/Checkbox';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import MUICheckbox from '@material-ui/core/Checkbox';
|
||||
|
||||
// Reduce checkbox size to avoid overlapping with other checkboxes.
|
||||
const useStyles = makeStyles({
|
||||
@@ -13,6 +14,12 @@ const useStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
const useFormGroupStyles = makeStyles({
|
||||
root: {
|
||||
display: 'block',
|
||||
},
|
||||
});
|
||||
|
||||
// We support a subset of the props supported by Material-UI v0.x Checkbox
|
||||
// They should be self descriptive - refer to Material UI docs otherwise.
|
||||
type Props = {|
|
||||
@@ -36,6 +43,7 @@ type Props = {|
|
||||
const Checkbox = (props: Props) => {
|
||||
const { onCheck } = props;
|
||||
const classes = useStyles();
|
||||
const formGroupClasses = useFormGroupStyles();
|
||||
const checkbox = (
|
||||
<MUICheckbox
|
||||
className={classes.root}
|
||||
@@ -51,11 +59,13 @@ const Checkbox = (props: Props) => {
|
||||
/>
|
||||
);
|
||||
return props.label ? (
|
||||
<FormControlLabel
|
||||
control={checkbox}
|
||||
label={props.label}
|
||||
style={props.style}
|
||||
/>
|
||||
<FormGroup classes={formGroupClasses}>
|
||||
<FormControlLabel
|
||||
control={checkbox}
|
||||
label={props.label}
|
||||
style={props.style}
|
||||
/>
|
||||
</FormGroup>
|
||||
) : (
|
||||
checkbox
|
||||
);
|
||||
|
@@ -4,10 +4,10 @@ import { type I18n as I18nType } from '@lingui/core';
|
||||
import * as React from 'react';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
import ButtonBase from '@material-ui/core/ButtonBase';
|
||||
import ThemeConsumer from './Theme/ThemeConsumer';
|
||||
import ContextMenu, { type ContextMenuInterface } from './Menu/ContextMenu';
|
||||
import { useLongTouch } from '../Utils/UseLongTouch';
|
||||
import { Spacer } from './Grid';
|
||||
import GDevelopThemeContext from './Theme/ThemeContext';
|
||||
|
||||
const styles = {
|
||||
tabContentContainer: {
|
||||
@@ -76,22 +76,19 @@ type ClosableTabsProps = {|
|
||||
children: React.Node,
|
||||
|};
|
||||
|
||||
export const ClosableTabs = ({ hideLabels, children }: ClosableTabsProps) => (
|
||||
<ThemeConsumer>
|
||||
{muiTheme => {
|
||||
const tabItemContainerStyle = {
|
||||
maxWidth: '100%', // Tabs should take all width
|
||||
flexShrink: 0, // Tabs height should never be reduced
|
||||
display: hideLabels ? 'none' : 'flex',
|
||||
flexWrap: 'nowrap', // Single line of tab...
|
||||
overflowX: 'auto', // ...scroll horizontally if needed
|
||||
backgroundColor: muiTheme.closableTabs.containerBackgroundColor,
|
||||
};
|
||||
export const ClosableTabs = ({ hideLabels, children }: ClosableTabsProps) => {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const tabItemContainerStyle = {
|
||||
maxWidth: '100%', // Tabs should take all width
|
||||
flexShrink: 0, // Tabs height should never be reduced
|
||||
display: hideLabels ? 'none' : 'flex',
|
||||
flexWrap: 'nowrap', // Single line of tab...
|
||||
overflowX: 'auto', // ...scroll horizontally if needed
|
||||
backgroundColor: gdevelopTheme.closableTabs.containerBackgroundColor,
|
||||
};
|
||||
|
||||
return <div style={tabItemContainerStyle}>{children}</div>;
|
||||
}}
|
||||
</ThemeConsumer>
|
||||
);
|
||||
return <div style={tabItemContainerStyle}>{children}</div>;
|
||||
};
|
||||
|
||||
export type ClosableTabProps = {|
|
||||
id?: string,
|
||||
@@ -156,90 +153,85 @@ export function ClosableTab({
|
||||
)
|
||||
);
|
||||
|
||||
return (
|
||||
<ThemeConsumer>
|
||||
{muiTheme => {
|
||||
const textColor = !active
|
||||
? muiTheme.closableTabs.textColor
|
||||
: muiTheme.closableTabs.selectedTextColor;
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const textColor = !active
|
||||
? gdevelopTheme.closableTabs.textColor
|
||||
: gdevelopTheme.closableTabs.selectedTextColor;
|
||||
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span
|
||||
return (
|
||||
<React.Fragment>
|
||||
<span
|
||||
style={{
|
||||
flexShrink: 0, // Tabs are never resized to fit in flex container
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
marginRight: 1,
|
||||
backgroundColor: !active
|
||||
? gdevelopTheme.closableTabs.backgroundColor
|
||||
: gdevelopTheme.closableTabs.selectedBackgroundColor,
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={onClick}
|
||||
onAuxClick={closable ? closeOnMiddleClick : undefined}
|
||||
onContextMenu={openContextMenu}
|
||||
id={id ? `${id}-button` : undefined}
|
||||
{...longTouchForContextMenuProps}
|
||||
focusRipple
|
||||
// If the touch ripple is not disabled, the dragged preview will
|
||||
// use the size of the ripple and it will be too big.
|
||||
disableTouchRipple
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...styles.tabLabelAndIcon,
|
||||
height: gdevelopTheme.closableTabs.height,
|
||||
color: textColor,
|
||||
fontFamily: gdevelopTheme.closableTabs.fontFamily,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{icon && label ? <Spacer /> : null}
|
||||
{label && <span style={styles.tabLabel}>{label}</span>}
|
||||
</span>
|
||||
</ButtonBase>
|
||||
{closable && (
|
||||
<ButtonBase
|
||||
onClick={onClose}
|
||||
onAuxClick={closeOnMiddleClick}
|
||||
onContextMenu={openContextMenu}
|
||||
{...longTouchForContextMenuProps}
|
||||
focusRipple
|
||||
>
|
||||
<Close
|
||||
style={{
|
||||
flexShrink: 0, // Tabs are never resized to fit in flex container
|
||||
position: 'relative',
|
||||
display: 'inline-block',
|
||||
marginRight: 1,
|
||||
backgroundColor: !active
|
||||
? muiTheme.closableTabs.backgroundColor
|
||||
: muiTheme.closableTabs.selectedBackgroundColor,
|
||||
...styles.closeButton,
|
||||
width: gdevelopTheme.closableTabs.height / 2,
|
||||
height: gdevelopTheme.closableTabs.height,
|
||||
}}
|
||||
>
|
||||
<ButtonBase
|
||||
onClick={onClick}
|
||||
onAuxClick={closable ? closeOnMiddleClick : undefined}
|
||||
onContextMenu={openContextMenu}
|
||||
id={id ? `${id}-button` : undefined}
|
||||
{...longTouchForContextMenuProps}
|
||||
focusRipple
|
||||
// If the touch ripple is not disabled, the dragged preview will
|
||||
// use the size of the ripple and it will be too big.
|
||||
disableTouchRipple
|
||||
>
|
||||
<span
|
||||
style={{
|
||||
...styles.tabLabelAndIcon,
|
||||
height: muiTheme.closableTabs.height,
|
||||
color: textColor,
|
||||
fontFamily: muiTheme.closableTabs.fontFamily,
|
||||
}}
|
||||
>
|
||||
{icon}
|
||||
{icon && label ? <Spacer /> : null}
|
||||
{label && <span style={styles.tabLabel}>{label}</span>}
|
||||
</span>
|
||||
</ButtonBase>
|
||||
{closable && (
|
||||
<ButtonBase
|
||||
onClick={onClose}
|
||||
onAuxClick={closeOnMiddleClick}
|
||||
onContextMenu={openContextMenu}
|
||||
{...longTouchForContextMenuProps}
|
||||
focusRipple
|
||||
>
|
||||
<Close
|
||||
style={{
|
||||
...styles.closeButton,
|
||||
width: muiTheme.closableTabs.height / 2,
|
||||
height: muiTheme.closableTabs.height,
|
||||
}}
|
||||
htmlColor={textColor}
|
||||
/>
|
||||
</ButtonBase>
|
||||
)}
|
||||
</span>
|
||||
<ContextMenu
|
||||
ref={contextMenu}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Close`),
|
||||
click: onClose,
|
||||
enabled: closable,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Close others`),
|
||||
click: onCloseOthers,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Close all`),
|
||||
click: onCloseAll,
|
||||
},
|
||||
]}
|
||||
htmlColor={textColor}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}}
|
||||
</ThemeConsumer>
|
||||
</ButtonBase>
|
||||
)}
|
||||
</span>
|
||||
<ContextMenu
|
||||
ref={contextMenu}
|
||||
buildMenuTemplate={(i18n: I18nType) => [
|
||||
{
|
||||
label: i18n._(t`Close`),
|
||||
click: onClose,
|
||||
enabled: closable,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Close others`),
|
||||
click: onCloseOthers,
|
||||
},
|
||||
{
|
||||
label: i18n._(t`Close all`),
|
||||
click: onCloseAll,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</React.Fragment>
|
||||
);
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ const appBarHeight = 32;
|
||||
|
||||
type Props = {|
|
||||
title: React.Node,
|
||||
displayLeftCloseButton?: boolean,
|
||||
displayRightCloseButton?: boolean,
|
||||
onClose: () => void,
|
||||
|};
|
||||
@@ -45,13 +44,9 @@ const DrawerTopBar = (props: Props) => {
|
||||
style={styles.appBar}
|
||||
className="safe-area-aware-top-margin"
|
||||
color="primary"
|
||||
elevation={0}
|
||||
>
|
||||
<Toolbar style={styles.toolbar}>
|
||||
{props.displayLeftCloseButton && (
|
||||
<IconButton onClick={props.onClose} edge="start" color="inherit">
|
||||
<Close />
|
||||
</IconButton>
|
||||
)}
|
||||
<Tooltip
|
||||
title={props.title}
|
||||
placement="bottom"
|
||||
@@ -63,7 +58,12 @@ const DrawerTopBar = (props: Props) => {
|
||||
</Tooltip>
|
||||
|
||||
{props.displayRightCloseButton && (
|
||||
<IconButton onClick={props.onClose} edge="end" color="inherit">
|
||||
<IconButton
|
||||
onClick={props.onClose}
|
||||
edge="end"
|
||||
color="inherit"
|
||||
size="small"
|
||||
>
|
||||
<Close />
|
||||
</IconButton>
|
||||
)}
|
||||
|
@@ -1,4 +1,5 @@
|
||||
import React, { Component } from 'react';
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import IconButton from '../IconButton';
|
||||
import Close from '@material-ui/icons/Close';
|
||||
import { MosaicWindowContext, MosaicContext } from 'react-mosaic-component';
|
||||
@@ -15,25 +16,20 @@ const styles = {
|
||||
},
|
||||
};
|
||||
|
||||
export default class CloseButton extends Component {
|
||||
render() {
|
||||
return (
|
||||
<MosaicContext.Consumer>
|
||||
{({ mosaicActions }) => (
|
||||
<MosaicWindowContext.Consumer>
|
||||
{({ mosaicWindowActions }) => (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
mosaicActions.remove(mosaicWindowActions.getPath());
|
||||
}}
|
||||
style={styles.container}
|
||||
>
|
||||
<Close htmlColor="white" style={styles.icon} />
|
||||
</IconButton>
|
||||
)}
|
||||
</MosaicWindowContext.Consumer>
|
||||
)}
|
||||
</MosaicContext.Consumer>
|
||||
);
|
||||
}
|
||||
type Props = {||};
|
||||
|
||||
export default function CloseButton(props: Props) {
|
||||
const { mosaicActions } = React.useContext(MosaicContext);
|
||||
const { mosaicWindowActions } = React.useContext(MosaicWindowContext);
|
||||
|
||||
return (
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
mosaicActions.remove(mosaicWindowActions.getPath());
|
||||
}}
|
||||
style={styles.container}
|
||||
>
|
||||
<Close htmlColor="inherit" style={styles.icon} />
|
||||
</IconButton>
|
||||
);
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
// @flow
|
||||
import React, { Component } from 'react';
|
||||
import * as React from 'react';
|
||||
import { type I18n as I18nType } from '@lingui/core';
|
||||
import IconButton from '../IconButton';
|
||||
import FilterList from '@material-ui/icons/FilterList';
|
||||
@@ -22,17 +22,15 @@ type Props = {|
|
||||
buildMenuTemplate: (i18n: I18nType) => Array<MenuItemTemplate>,
|
||||
|};
|
||||
|
||||
export default class TagsButton extends Component<Props, {||}> {
|
||||
render() {
|
||||
return (
|
||||
<ElementWithMenu
|
||||
element={
|
||||
<IconButton style={styles.container}>
|
||||
<FilterList htmlColor="white" style={styles.icon} />
|
||||
</IconButton>
|
||||
}
|
||||
buildMenuTemplate={this.props.buildMenuTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
export default function TagsButton(props: Props) {
|
||||
return (
|
||||
<ElementWithMenu
|
||||
element={
|
||||
<IconButton style={styles.container}>
|
||||
<FilterList htmlColor="inherit" style={styles.icon} />
|
||||
</IconButton>
|
||||
}
|
||||
buildMenuTemplate={props.buildMenuTemplate}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { type ButtonInterface } from './Button';
|
||||
import { Spacer } from './Grid';
|
||||
import GDevelopThemeContext from './Theme/ThemeContext';
|
||||
|
||||
// We support a subset of the props supported by Material-UI v0.x FlatButton
|
||||
// They should be self descriptive - refer to Material UI docs otherwise.
|
||||
@@ -49,11 +48,9 @@ const FlatButton = React.forwardRef<Props, ButtonInterface>(
|
||||
// always visible to be sure we're getting focusing right.
|
||||
const focusRipple = true;
|
||||
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant={gdevelopTheme.isModern ? 'outlined' : 'text'}
|
||||
variant="outlined"
|
||||
size="small"
|
||||
color={primary ? 'primary' : 'default'}
|
||||
autoFocus={keyboardFocused}
|
||||
|
@@ -1,9 +1,11 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import FormControlLabel from '@material-ui/core/FormControlLabel';
|
||||
import FormGroup from '@material-ui/core/FormGroup';
|
||||
import FormHelperText from '@material-ui/core/FormHelperText';
|
||||
import Checkbox from '@material-ui/core/Checkbox';
|
||||
import Tooltip from '@material-ui/core/Tooltip';
|
||||
import { FormGroup, FormHelperText, makeStyles } from '@material-ui/core';
|
||||
|
||||
const useLabelStyles = makeStyles({
|
||||
root: {
|
||||
@@ -11,6 +13,12 @@ const useLabelStyles = makeStyles({
|
||||
},
|
||||
});
|
||||
|
||||
const useFormGroupStyles = makeStyles({
|
||||
root: {
|
||||
display: 'block',
|
||||
},
|
||||
});
|
||||
|
||||
type Props = {|
|
||||
label?: ?React.Node,
|
||||
checked: boolean,
|
||||
@@ -35,6 +43,7 @@ const InlineCheckbox = ({
|
||||
tooltipOrHelperText,
|
||||
}: Props) => {
|
||||
const labelClasses = useLabelStyles();
|
||||
const formGroupClasses = useFormGroupStyles();
|
||||
const checkbox = (
|
||||
<Checkbox
|
||||
disabled={disabled}
|
||||
@@ -48,7 +57,7 @@ const InlineCheckbox = ({
|
||||
/>
|
||||
);
|
||||
return label ? (
|
||||
<FormGroup>
|
||||
<FormGroup classes={formGroupClasses}>
|
||||
<FormControlLabel
|
||||
control={checkbox}
|
||||
label={label}
|
||||
|
@@ -163,6 +163,7 @@ type ColumnStackLayoutProps = {|
|
||||
noMargin?: boolean,
|
||||
children: React.Node,
|
||||
noOverflowParent?: boolean,
|
||||
useFullHeight?: boolean,
|
||||
|};
|
||||
|
||||
export const ColumnStackLayout = ({
|
||||
@@ -172,6 +173,7 @@ export const ColumnStackLayout = ({
|
||||
noMargin,
|
||||
children,
|
||||
noOverflowParent,
|
||||
useFullHeight,
|
||||
}: ColumnStackLayoutProps) => {
|
||||
let isFirstChild = true;
|
||||
return (
|
||||
@@ -181,6 +183,7 @@ export const ColumnStackLayout = ({
|
||||
expand={expand}
|
||||
noMargin={noMargin}
|
||||
noOverflowParent={noOverflowParent}
|
||||
useFullHeight={useFullHeight}
|
||||
>
|
||||
{React.Children.map(children, (child, index) => {
|
||||
if (!child) return null;
|
||||
|
@@ -8,6 +8,7 @@ import PreferencesContext, {
|
||||
} from '../../MainFrame/Preferences/PreferencesContext';
|
||||
import { useScreenType } from '../Reponsive/ScreenTypeMeasurer';
|
||||
import { isUserflowRunning } from '../../MainFrame/Onboarding/OnboardingDialog';
|
||||
import GDevelopThemeContext from '../Theme/ThemeContext';
|
||||
|
||||
type Props = {|
|
||||
identifier: AlertMessageIdentifier,
|
||||
@@ -23,6 +24,7 @@ const DismissableInfoBar = ({
|
||||
message,
|
||||
}: Props) => {
|
||||
const preferences = React.useContext(PreferencesContext);
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const screenType = useScreenType();
|
||||
|
||||
return isUserflowRunning ? null : (
|
||||
@@ -36,7 +38,9 @@ const DismissableInfoBar = ({
|
||||
action={
|
||||
<Button
|
||||
key="undo"
|
||||
color="primary"
|
||||
color={
|
||||
gdevelopTheme.palette.type === 'light' ? 'secondary' : 'primary'
|
||||
}
|
||||
size="small"
|
||||
onClick={() => {
|
||||
preferences.showAlertMessage(identifier, false);
|
||||
|
@@ -3,7 +3,6 @@ import * as React from 'react';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import { type ButtonInterface } from './Button';
|
||||
import { Spacer } from './Grid';
|
||||
import GDevelopThemeContext from './Theme/ThemeContext';
|
||||
|
||||
// We support a subset of the props supported by Material-UI v0.x RaisedButton
|
||||
// They should be self descriptive - refer to Material UI docs otherwise.
|
||||
@@ -39,20 +38,13 @@ const RaisedButton = React.forwardRef<Props, ButtonInterface>(
|
||||
// it's important to get focus right in the whole app, make the ripple
|
||||
// always visible to be sure we're getting focusing right.
|
||||
const focusRipple = true;
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
disableElevation={gdevelopTheme.isModern}
|
||||
color={
|
||||
primary
|
||||
? gdevelopTheme.isModern
|
||||
? 'secondary'
|
||||
: 'primary'
|
||||
: 'default'
|
||||
}
|
||||
disableElevation
|
||||
color={primary ? 'secondary' : 'default'}
|
||||
focusRipple={focusRipple}
|
||||
{...otherProps}
|
||||
ref={ref}
|
||||
|
@@ -7,7 +7,6 @@ import ButtonGroup from '@material-ui/core/ButtonGroup';
|
||||
import Button from '@material-ui/core/Button';
|
||||
import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
|
||||
import { Spacer } from './Grid';
|
||||
import GDevelopThemeContext from './Theme/ThemeContext';
|
||||
|
||||
type Props = {|
|
||||
label?: React.Node,
|
||||
@@ -49,21 +48,11 @@ const RaisedButtonWithSplitMenu = (props: Props) => {
|
||||
// always visible to be sure we're getting focusing right.
|
||||
const focusRipple = true;
|
||||
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
return (
|
||||
<ButtonGroup
|
||||
variant={primary ? 'contained' : 'outlined'}
|
||||
disableElevation={gdevelopTheme.isModern}
|
||||
color={
|
||||
gdevelopTheme.isModern
|
||||
? primary
|
||||
? 'secondary'
|
||||
: 'primary'
|
||||
: primary
|
||||
? 'primary'
|
||||
: 'default'
|
||||
}
|
||||
disableElevation
|
||||
color={primary ? 'secondary' : 'primary'}
|
||||
disabled={disabled}
|
||||
size="small"
|
||||
style={props.style}
|
||||
|
@@ -63,8 +63,6 @@ export const BoxSearchResults = <SearchItem>({
|
||||
<div style={styles.container}>
|
||||
<AutoSizer>
|
||||
{({ width, height }) => {
|
||||
if (!width || !height) return null;
|
||||
|
||||
const columnCount = Math.max(Math.floor((width - 5) / baseSize), 1);
|
||||
const columnWidth = Math.max(Math.floor(width / columnCount), 30);
|
||||
const rowCount = Math.max(
|
||||
|
@@ -41,8 +41,8 @@ type Props = {|
|
||||
onChange?: string => void,
|
||||
/** Fired when the search icon is clicked. */
|
||||
onRequestSearch: string => void,
|
||||
/** Set if margins should be added or not. */
|
||||
aspect?: 'integrated-search-bar' | 'add-margins-only-if-modern-theme',
|
||||
/** Set if rounding should be applied or not. */
|
||||
aspect?: 'integrated-search-bar',
|
||||
/** The value of the text field. */
|
||||
value: string,
|
||||
/** The functions needed to interact with the list of tags displayed below search bar. */
|
||||
@@ -167,10 +167,6 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
|
||||
};
|
||||
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
const noMargin =
|
||||
aspect === 'add-margins-only-if-modern-theme' && gdevelopTheme.isModern
|
||||
? false
|
||||
: true;
|
||||
|
||||
// This variable represents the content of the input (text field)
|
||||
const [value, setValue] = React.useState<string>(parentValue);
|
||||
@@ -288,17 +284,15 @@ const SearchBar = React.forwardRef<Props, SearchBarInterface>(
|
||||
return (
|
||||
<I18n>
|
||||
{({ i18n }) => (
|
||||
<Column noMargin={noMargin}>
|
||||
<Line noMargin={noMargin}>
|
||||
<Column noMargin>
|
||||
<Line noMargin>
|
||||
<Paper
|
||||
style={{
|
||||
backgroundColor: gdevelopTheme.searchBar.backgroundColor,
|
||||
...styles.root,
|
||||
}}
|
||||
square={
|
||||
aspect === 'integrated-search-bar' || !gdevelopTheme.isModern
|
||||
}
|
||||
elevation={gdevelopTheme.isModern ? 0 : 1}
|
||||
square={aspect === 'integrated-search-bar'}
|
||||
elevation={0}
|
||||
>
|
||||
<div style={styles.searchContainer}>
|
||||
{tags ? (
|
||||
|
@@ -2,7 +2,6 @@
|
||||
import * as React from 'react';
|
||||
import MUITabs from '@material-ui/core/Tabs';
|
||||
import MUITab from '@material-ui/core/Tab';
|
||||
import GDevelopThemeContext from './Theme/ThemeContext';
|
||||
|
||||
// We support a subset of the props supported by Material-UI v0.x Tabs
|
||||
// They should be self descriptive - refer to Material UI docs otherwise.
|
||||
@@ -16,13 +15,11 @@ type TabsProps<TabName> = {|
|
||||
* Tabs based on Material-UI Tabs.
|
||||
*/
|
||||
export function Tabs<TabName>(props: TabsProps<TabName>) {
|
||||
const gdevelopTheme = React.useContext(GDevelopThemeContext);
|
||||
|
||||
return (
|
||||
<MUITabs
|
||||
variant="fullWidth"
|
||||
textColor="primary"
|
||||
indicatorColor={gdevelopTheme.isModern ? 'primary' : 'secondary'}
|
||||
indicatorColor={'primary'}
|
||||
value={props.value}
|
||||
onChange={(e, newValue) => props.onChange(newValue)}
|
||||
>
|
||||
|
11
newIDE/app/src/UI/Theme/BlueDarkTheme/index.js
Normal file
11
newIDE/app/src/UI/Theme/BlueDarkTheme/index.js
Normal file
@@ -0,0 +1,11 @@
|
||||
import { createGdevelopTheme } from '../CreateTheme';
|
||||
|
||||
import styles from './BlueDarkThemeVariables.json';
|
||||
import './BlueDarkThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
|
||||
rootClassNameIdentifier: 'BlueDarkTheme',
|
||||
paletteType: 'dark',
|
||||
});
|
@@ -2,18 +2,20 @@
|
||||
"theme": {
|
||||
"primary": {
|
||||
"color": {
|
||||
"value": "{gdevelop.color.blue.value}"
|
||||
"value": "#6BAFFF",
|
||||
"comment": "Palette/Blue/40"
|
||||
},
|
||||
"text-contrast-color": {
|
||||
"value": "#fff"
|
||||
"value": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"secondary": {
|
||||
"color": {
|
||||
"value": "#EEE"
|
||||
"value": "#006DEE",
|
||||
"comment": "Palette/Blue/60"
|
||||
},
|
||||
"text-contrast-color": {
|
||||
"value": "#000"
|
||||
"value": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
@@ -193,7 +195,7 @@
|
||||
},
|
||||
"tabs": {
|
||||
"background-color": {
|
||||
"value": "#3c4698"
|
||||
"value": "#4C5361"
|
||||
}
|
||||
},
|
||||
"event-sheet": {
|
@@ -17,11 +17,11 @@ export function getRootClassNames(theme: string) {
|
||||
}
|
||||
|
||||
export function getMuiOverrides(
|
||||
isModern: boolean,
|
||||
tabTextColor: string,
|
||||
tabBackgroundColor: string,
|
||||
inputBorderBottomColor: string,
|
||||
appBarBackgroundColor: string,
|
||||
appBarTextColor: string,
|
||||
iconColor: string,
|
||||
outlinedButtonBorderColor: string
|
||||
) {
|
||||
@@ -29,11 +29,11 @@ export function getMuiOverrides(
|
||||
MuiTypography: {
|
||||
h5: {
|
||||
// Make h6, used in Drawer title bars, use the same weight as tabs and mosaic windows
|
||||
fontWeight: isModern ? 600 : 400,
|
||||
fontWeight: 600,
|
||||
},
|
||||
h6: {
|
||||
// Make h6, used in Drawer title bars, use the same weight as tabs and mosaic windows
|
||||
fontWeight: isModern ? 600 : 400,
|
||||
fontWeight: 600,
|
||||
},
|
||||
},
|
||||
MuiInput: {
|
||||
@@ -49,7 +49,9 @@ export function getMuiOverrides(
|
||||
},
|
||||
MuiAppBar: {
|
||||
colorPrimary: {
|
||||
// Use some colors as mosaic titles:
|
||||
backgroundColor: appBarBackgroundColor,
|
||||
color: appBarTextColor,
|
||||
},
|
||||
},
|
||||
MuiIconButton: {
|
||||
@@ -87,18 +89,13 @@ export function getMuiOverrides(
|
||||
},
|
||||
MuiTab: {
|
||||
textColorPrimary: {
|
||||
color:
|
||||
(isModern ? darken(tabTextColor, 0.2) : tabTextColor) + ' !important',
|
||||
textTransform: isModern ? 'none' : 'uppercase',
|
||||
fontWeight: isModern ? 600 : 500,
|
||||
letterSpacing: isModern ? '0.3px' : '',
|
||||
...(isModern
|
||||
? {
|
||||
'&.Mui-selected': {
|
||||
color: tabTextColor + ' !important',
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
color: darken(tabTextColor, 0.2) + ' !important',
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.3px',
|
||||
'&.Mui-selected': {
|
||||
color: tabTextColor + ' !important',
|
||||
},
|
||||
},
|
||||
root: {
|
||||
// Reduce the height of tabs to 32px
|
||||
@@ -150,19 +147,17 @@ export function getMuiOverrides(
|
||||
},
|
||||
MuiPaper: {
|
||||
rounded: {
|
||||
borderRadius: isModern ? 8 : 4,
|
||||
borderRadius: 8,
|
||||
},
|
||||
},
|
||||
MuiButton: {
|
||||
root: {
|
||||
borderRadius: isModern ? 8 : 0,
|
||||
textTransform: isModern ? 'none' : 'uppercase',
|
||||
fontWeight: isModern ? 600 : 400, // Lower a bit the weight of buttons
|
||||
letterSpacing: isModern ? '0.3px' : '',
|
||||
borderRadius: 8,
|
||||
textTransform: 'none',
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.3px',
|
||||
},
|
||||
...(isModern
|
||||
? { outlined: { borderColor: outlinedButtonBorderColor } }
|
||||
: {}),
|
||||
outlined: { borderColor: outlinedButtonBorderColor },
|
||||
},
|
||||
// Make MuiAccordion much more compact than default.
|
||||
// Some (or all) of these styles can be removed on MUIv5,
|
||||
@@ -222,21 +217,19 @@ export function getThemeMode(color: string, contrastText: string) {
|
||||
|
||||
export function createGdevelopTheme({
|
||||
styles,
|
||||
isModern,
|
||||
rootClassNameIdentifier,
|
||||
paletteType,
|
||||
gdevelopIconsCSSFilter,
|
||||
}: {
|
||||
styles: any,
|
||||
isModern?: boolean,
|
||||
rootClassNameIdentifier: string,
|
||||
paletteType: string,
|
||||
gdevelopIconsCSSFilter: ?string,
|
||||
}) {
|
||||
return {
|
||||
gdevelopTheme: {
|
||||
isModern: !!isModern,
|
||||
palette: {
|
||||
type: paletteType,
|
||||
canvasColor: styles['ThemeSurfaceCanvasBackgroundColor'],
|
||||
},
|
||||
message: {
|
||||
@@ -259,7 +252,7 @@ export function createGdevelopTheme({
|
||||
border: styles['ThemeDropIndicatorBorderColor'],
|
||||
},
|
||||
closableTabs: {
|
||||
fontFamily: styles['GdevelopFontFamily'],
|
||||
fontFamily: styles['GdevelopModernFontFamily'],
|
||||
containerBackgroundColor: styles['ThemeSurfaceWindowBackgroundColor'],
|
||||
backgroundColor: styles['ThemeClosableTabsDefaultBackgroundColor'],
|
||||
textColor: styles['ThemeClosableTabsDefaultColor'],
|
||||
@@ -309,9 +302,7 @@ export function createGdevelopTheme({
|
||||
},
|
||||
muiThemeOptions: {
|
||||
typography: {
|
||||
fontFamily: isModern
|
||||
? styles['GdevelopModernFontFamily']
|
||||
: styles['GdevelopFontFamily'],
|
||||
fontFamily: styles['GdevelopModernFontFamily'],
|
||||
},
|
||||
palette: {
|
||||
type: paletteType,
|
||||
@@ -339,11 +330,11 @@ export function createGdevelopTheme({
|
||||
},
|
||||
},
|
||||
overrides: getMuiOverrides(
|
||||
!!isModern,
|
||||
styles['ThemeTextContrastColor'],
|
||||
styles['TabsBackgroundColor'],
|
||||
styles['InputBorderBottomColor'],
|
||||
styles['MosaicToolbarBackgroundColor'],
|
||||
styles['MosaicTitleColor'],
|
||||
styles['ThemeTextDefaultColor'],
|
||||
styles['ThemeTextDefaultColor']
|
||||
),
|
||||
|
@@ -1,10 +0,0 @@
|
||||
import { createGdevelopTheme } from '../CreateTheme';
|
||||
|
||||
import styles from './DarkThemeVariables.json';
|
||||
import './DarkThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
rootClassNameIdentifier: 'DarkTheme',
|
||||
paletteType: 'dark',
|
||||
});
|
@@ -5,7 +5,7 @@ import './DefaultDarkThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
isModern: true,
|
||||
|
||||
rootClassNameIdentifier: 'DefaultDarkTheme',
|
||||
paletteType: 'dark',
|
||||
});
|
||||
|
10
newIDE/app/src/UI/Theme/DefaultLightTheme/index.js
Normal file
10
newIDE/app/src/UI/Theme/DefaultLightTheme/index.js
Normal file
@@ -0,0 +1,10 @@
|
||||
import { createGdevelopTheme } from '../CreateTheme';
|
||||
import styles from './DefaultLightThemeVariables.json';
|
||||
import './DefaultLightThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
|
||||
rootClassNameIdentifier: 'DefaultLightTheme',
|
||||
paletteType: 'light',
|
||||
});
|
@@ -2,18 +2,20 @@
|
||||
"theme": {
|
||||
"primary": {
|
||||
"color": {
|
||||
"value": "{gdevelop.color.light-blue.value}"
|
||||
"value": "#42229B",
|
||||
"comment": "Palette/Purple/70"
|
||||
},
|
||||
"text-contrast-color": {
|
||||
"value": "#fff"
|
||||
"value": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"secondary": {
|
||||
"color": {
|
||||
"value": "{gdevelop.color.purple.value}"
|
||||
"value": "#6A3BEB",
|
||||
"comment": "Palette/Purple/60"
|
||||
},
|
||||
"text-contrast-color": {
|
||||
"value": "#fff"
|
||||
"value": "#FFFFFF"
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
@@ -24,32 +26,36 @@
|
||||
},
|
||||
"canvas": {
|
||||
"background-color": {
|
||||
"value": "#f0f0f0"
|
||||
"value": "#ffffff"
|
||||
}
|
||||
},
|
||||
"alternate-canvas": {
|
||||
"background-color": {
|
||||
"value": "#ffffff"
|
||||
"value": "#f0f0f0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"selection": {
|
||||
"background-color": {
|
||||
"value": "{gdevelop.color.selection.value}"
|
||||
"value": "#ECE5FF",
|
||||
"comment": "Palette/Purple/10"
|
||||
},
|
||||
"color": {
|
||||
"value": "#FFFFFF"
|
||||
"value": "#1D1D26",
|
||||
"comment": "Text/Primary"
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"default": {
|
||||
"color": {
|
||||
"value": "#000"
|
||||
"value": "#1D1D26",
|
||||
"comment": "Text/Primary"
|
||||
}
|
||||
},
|
||||
"secondary": {
|
||||
"color": {
|
||||
"value": "#000"
|
||||
"value": "#1D1D26",
|
||||
"comment": "Text/Primary"
|
||||
}
|
||||
},
|
||||
"disabled": {
|
||||
@@ -59,7 +65,7 @@
|
||||
},
|
||||
"contrast": {
|
||||
"color": {
|
||||
"value": "#FFFFFF"
|
||||
"value": "#515151"
|
||||
}
|
||||
},
|
||||
"highlighted": {
|
||||
@@ -98,18 +104,18 @@
|
||||
"closable-tabs": {
|
||||
"default": {
|
||||
"background-color": {
|
||||
"value": "{theme.surface.canvas.background-color.value}"
|
||||
"value": "#f7f7f7"
|
||||
},
|
||||
"color": {
|
||||
"value": "#878787"
|
||||
"value": "#444444"
|
||||
}
|
||||
},
|
||||
"selected": {
|
||||
"background-color": {
|
||||
"value": "{gdevelop.color.light-blue.value}"
|
||||
"value": "#e1e1e1"
|
||||
},
|
||||
"color": {
|
||||
"value": "#ffffff"
|
||||
"value": "#000000"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -194,7 +200,7 @@
|
||||
},
|
||||
"tabs": {
|
||||
"background-color": {
|
||||
"value": "#4ab0e4"
|
||||
"value": "#efefef"
|
||||
}
|
||||
},
|
||||
"event-sheet": {
|
||||
@@ -375,7 +381,7 @@
|
||||
"mosaic": {
|
||||
"title": {
|
||||
"color": {
|
||||
"value": "#FFFFFF"
|
||||
"value": "#000000"
|
||||
}
|
||||
},
|
||||
"layout": {
|
||||
@@ -388,10 +394,10 @@
|
||||
},
|
||||
"toolbar": {
|
||||
"background-color": {
|
||||
"value": "#9100ce"
|
||||
"value": "#e1e1e1"
|
||||
},
|
||||
"border-color-hover": {
|
||||
"value": "#2b95d6"
|
||||
"value": "#efefef"
|
||||
}
|
||||
}
|
||||
},
|
@@ -1,9 +0,0 @@
|
||||
import { createGdevelopTheme } from '../CreateTheme';
|
||||
import styles from './DefaultThemeVariables.json';
|
||||
import './DefaultThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
rootClassNameIdentifier: 'DefaultTheme',
|
||||
paletteType: 'light',
|
||||
});
|
@@ -6,6 +6,7 @@
|
||||
/* Mosaic window and tile */
|
||||
.mosaic-gd-theme .mosaic-window-toolbar {
|
||||
background: var(--mosaic-toolbar-background-color) !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.mosaic-gd-theme .mosaic-window-body {
|
||||
@@ -13,9 +14,10 @@
|
||||
}
|
||||
|
||||
.mosaic-gd-theme .mosaic-window .mosaic-window-title {
|
||||
color: white !important;
|
||||
padding-left: 8px; /* Follow same spacing as in the rest of the app. */
|
||||
color: var(--mosaic-title-color) !important;
|
||||
font-weight: 400 !important;
|
||||
font-family: var(--gdevelop-classic-font-family);
|
||||
font-family: var(--gdevelop-modern-font-family);
|
||||
font-size: 15px; /* Same as ClosableTabs font size */
|
||||
}
|
||||
|
||||
|
@@ -5,6 +5,7 @@ import './NordThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
|
||||
rootClassNameIdentifier: 'NordTheme',
|
||||
paletteType: 'dark',
|
||||
gdevelopIconsCSSFilter: 'hue-rotate(-15deg) saturate(57%) brightness(120%)',
|
||||
|
@@ -10,10 +10,10 @@
|
||||
},
|
||||
"secondary": {
|
||||
"color": {
|
||||
"value": "#EEE"
|
||||
"value": "#5E81AC"
|
||||
},
|
||||
"text-contrast-color": {
|
||||
"value": "#000"
|
||||
"value": "#fff"
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
@@ -193,7 +193,7 @@
|
||||
},
|
||||
"tabs": {
|
||||
"background-color": {
|
||||
"value": "#81A1C1"
|
||||
"value": "#4C566A"
|
||||
}
|
||||
},
|
||||
"event-sheet": {
|
||||
|
@@ -5,6 +5,7 @@ import './OneDarkThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
|
||||
rootClassNameIdentifier: 'OneDarkTheme',
|
||||
paletteType: 'dark',
|
||||
gdevelopIconsCSSFilter: 'hue-rotate(-10deg) saturate(50%)',
|
||||
|
@@ -5,6 +5,7 @@ import './SolarizedDarkThemeVariables.css';
|
||||
|
||||
export default createGdevelopTheme({
|
||||
styles,
|
||||
|
||||
rootClassNameIdentifier: 'SolarizedDarkTheme',
|
||||
paletteType: 'dark',
|
||||
gdevelopIconsCSSFilter: 'hue-rotate(-15deg) saturate(70%) brightness(90%)',
|
||||
|
@@ -2,7 +2,7 @@
|
||||
"theme": {
|
||||
"primary": {
|
||||
"color": {
|
||||
"value": "#047AA6"
|
||||
"value": "#2799b9"
|
||||
},
|
||||
"text-contrast-color": {
|
||||
"value": "#fff"
|
||||
@@ -10,10 +10,10 @@
|
||||
},
|
||||
"secondary": {
|
||||
"color": {
|
||||
"value": "#EEE"
|
||||
"value": "#047AA6"
|
||||
},
|
||||
"text-contrast-color": {
|
||||
"value": "#000"
|
||||
"value": "#fff"
|
||||
}
|
||||
},
|
||||
"surface": {
|
||||
@@ -193,7 +193,7 @@
|
||||
},
|
||||
"tabs": {
|
||||
"background-color": {
|
||||
"value": "#047AA6"
|
||||
"value": "#586e75"
|
||||
}
|
||||
},
|
||||
"event-sheet": {
|
||||
|
@@ -1,10 +1,10 @@
|
||||
// @flow
|
||||
import * as React from 'react';
|
||||
import { type GDevelopTheme } from '.';
|
||||
import DefaultTheme from './DefaultTheme';
|
||||
import DefaultLightTheme from './DefaultLightTheme';
|
||||
|
||||
const GDevelopThemeContext = React.createContext<GDevelopTheme>(
|
||||
DefaultTheme.gdevelopTheme
|
||||
DefaultLightTheme.gdevelopTheme
|
||||
);
|
||||
|
||||
export default GDevelopThemeContext;
|
||||
|
@@ -1,6 +1,6 @@
|
||||
// This file is generated by create-new-theme.js script
|
||||
import DefaultTheme from './DefaultTheme';
|
||||
import DarkTheme from './DarkTheme';
|
||||
import DefaultLightTheme from './DefaultLightTheme';
|
||||
import BlueDarkTheme from './BlueDarkTheme';
|
||||
import NordTheme from './NordTheme';
|
||||
import SolarizedDarkTheme from './SolarizedDarkTheme';
|
||||
import OneDarkTheme from './OneDarkTheme';
|
||||
@@ -10,9 +10,8 @@ import DefaultDarkTheme from './DefaultDarkTheme';
|
||||
export const themes = {
|
||||
// "Modern" themes:
|
||||
['GDevelop default Dark']: DefaultDarkTheme,
|
||||
// Old themes still based on the "old" components:
|
||||
['GDevelop default']: DefaultTheme,
|
||||
['Dark']: DarkTheme,
|
||||
['GDevelop default Light']: DefaultLightTheme,
|
||||
['Blue Dark']: BlueDarkTheme,
|
||||
['Nord']: NordTheme,
|
||||
['Solarized Dark']: SolarizedDarkTheme,
|
||||
['One Dark']: OneDarkTheme,
|
||||
|
@@ -3,7 +3,7 @@ import { createMuiTheme } from '@material-ui/core/styles';
|
||||
import { isLtr } from '../../Utils/i18n/RtlLanguages';
|
||||
import memoize from '../../Utils/Memoize';
|
||||
|
||||
import DefaultTheme from './DefaultTheme';
|
||||
import DefaultLightTheme from './DefaultLightTheme';
|
||||
import { themes as themeList } from './ThemeRegistry';
|
||||
|
||||
import 'react-virtualized/styles.css';
|
||||
@@ -16,13 +16,13 @@ import './Global/Mosaic.css';
|
||||
import './Global/Table.css';
|
||||
import './Global/Font.css';
|
||||
|
||||
export type Theme = $Exact<typeof DefaultTheme>;
|
||||
export type Theme = $Exact<typeof DefaultLightTheme>;
|
||||
export const themes = themeList;
|
||||
|
||||
export type GDevelopTheme = $PropertyType<Theme, 'gdevelopTheme'>;
|
||||
type ActualTheme = {| gdevelopTheme: GDevelopTheme, muiTheme: Object |};
|
||||
type MuiThemeOptions = $PropertyType<Theme, 'muiThemeOptions'>;
|
||||
const defaultThemeName = 'GDevelop default';
|
||||
const defaultThemeName = 'GDevelop default Dark';
|
||||
|
||||
export function getTheme({
|
||||
themeName,
|
||||
@@ -89,6 +89,6 @@ const rtlOverrides = {
|
||||
};
|
||||
|
||||
export const defaultTheme: ActualTheme = {
|
||||
...DefaultTheme,
|
||||
muiThemeOptions: createLtrTheme(DefaultTheme.muiThemeOptions),
|
||||
...DefaultLightTheme,
|
||||
muiThemeOptions: createLtrTheme(DefaultLightTheme.muiThemeOptions),
|
||||
};
|
||||
|
@@ -86,6 +86,7 @@ export const installAnalyticsEvents = (authentication: Authentication) => {
|
||||
appLanguage,
|
||||
browserLanguage,
|
||||
programOpeningCount: getProgramOpeningCount(),
|
||||
themeName: userPreferences ? userPreferences.themeName : 'Unknown',
|
||||
...(isElectronApp ? { usedDesktopApp: true } : { usedWebApp: true }),
|
||||
};
|
||||
|
||||
|
@@ -63,6 +63,7 @@ export const makeFakeExactExpressionAutocompletion = () => {
|
||||
extension,
|
||||
expressionMetadata
|
||||
),
|
||||
shouldConvertToString: false,
|
||||
},
|
||||
];
|
||||
};
|
||||
@@ -137,6 +138,7 @@ export const makeFakeExpressionAutocompletions = (): Array<ExpressionAutocomplet
|
||||
extension,
|
||||
expressionWithoutParamsMetadata
|
||||
),
|
||||
shouldConvertToString: false,
|
||||
},
|
||||
{
|
||||
kind: 'Expression',
|
||||
@@ -148,6 +150,19 @@ export const makeFakeExpressionAutocompletions = (): Array<ExpressionAutocomplet
|
||||
extension,
|
||||
expressionMetadata
|
||||
),
|
||||
shouldConvertToString: false,
|
||||
},
|
||||
{
|
||||
kind: 'Expression',
|
||||
completion: 'MyFunctionThatShouldBeConvertedToString',
|
||||
addParenthesis: true,
|
||||
isExact: false,
|
||||
enumeratedExpressionMetadata: makeFakeEnumeratedExpressionMetadata(
|
||||
'MyFunction',
|
||||
extension,
|
||||
expressionMetadata
|
||||
),
|
||||
shouldConvertToString: true,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
@@ -5,342 +5,342 @@ module.exports = [
|
||||
"languageCode": "af_ZA",
|
||||
"languageName": "Afrikaans",
|
||||
"languageNativeName": "Afrikaans",
|
||||
"translationRatio": 0.048800939124601705
|
||||
"translationRatio": 0.048788582807832714
|
||||
},
|
||||
{
|
||||
"languageCode": "ar_SA",
|
||||
"languageName": "Arabic",
|
||||
"languageNativeName": "اللغة العربية",
|
||||
"translationRatio": 0.386214992453463
|
||||
"translationRatio": 0.38267507467640227
|
||||
},
|
||||
{
|
||||
"languageCode": "az_AZ",
|
||||
"languageName": "Azerbaijani",
|
||||
"languageNativeName": "azərbaycan dili",
|
||||
"translationRatio": 0.03588797585108172
|
||||
"translationRatio": 0.0360106206438765
|
||||
},
|
||||
{
|
||||
"languageCode": "bg_BG",
|
||||
"languageName": "Bulgarian",
|
||||
"languageNativeName": "български език",
|
||||
"translationRatio": 0.052993459667952325
|
||||
"translationRatio": 0.05310321938267504
|
||||
},
|
||||
{
|
||||
"languageCode": "bn_BD",
|
||||
"languageName": "Bengali",
|
||||
"languageNativeName": "বাংলা",
|
||||
"translationRatio": 0.06003689418078151
|
||||
"translationRatio": 0.060073016926651124
|
||||
},
|
||||
{
|
||||
"languageCode": "ca_ES",
|
||||
"languageName": "Catalan",
|
||||
"languageNativeName": "Català",
|
||||
"translationRatio": 0.30957571692101293
|
||||
"translationRatio": 0.30567540657152337
|
||||
},
|
||||
{
|
||||
"languageCode": "cs_CZ",
|
||||
"languageName": "Czech",
|
||||
"languageNativeName": "čeština",
|
||||
"translationRatio": 0.14740902230420927
|
||||
"translationRatio": 0.1495187520743445
|
||||
},
|
||||
{
|
||||
"languageCode": "da_DK",
|
||||
"languageName": "Danish",
|
||||
"languageNativeName": "dansk",
|
||||
"translationRatio": 0.06892503773268488
|
||||
"translationRatio": 0.06853634251576501
|
||||
},
|
||||
{
|
||||
"languageCode": "de_DE",
|
||||
"languageName": "German",
|
||||
"languageNativeName": "Deutsch",
|
||||
"translationRatio": 0.7249706523561965
|
||||
"translationRatio": 0.7719880517756389
|
||||
},
|
||||
{
|
||||
"languageCode": "el_GR",
|
||||
"languageName": "Greek",
|
||||
"languageNativeName": "Ελληνικά",
|
||||
"translationRatio": 0.27201073285259103
|
||||
"translationRatio": 0.2713242615333554
|
||||
},
|
||||
{
|
||||
"languageCode": "en",
|
||||
"languageName": "English",
|
||||
"languageNativeName": "English",
|
||||
"translationRatio": 0.05027932960893855
|
||||
"translationRatio": 0.052026618269812475
|
||||
},
|
||||
{
|
||||
"languageCode": "eo_UY",
|
||||
"languageName": "Esperanto",
|
||||
"languageNativeName": "Esperanto",
|
||||
"translationRatio": 0.036223377494549736
|
||||
"translationRatio": 0.03617656820444737
|
||||
},
|
||||
{
|
||||
"languageCode": "es_ES",
|
||||
"languageName": "Spanish",
|
||||
"languageNativeName": "Español",
|
||||
"translationRatio": 0.9882609424786182
|
||||
"translationRatio": 0.9878858280783273
|
||||
},
|
||||
{
|
||||
"languageCode": "fa_IR",
|
||||
"languageName": "Persian",
|
||||
"languageNativeName": "فارسی",
|
||||
"translationRatio": 0.20878752305886294
|
||||
"translationRatio": 0.2062728177895785
|
||||
},
|
||||
{
|
||||
"languageCode": "fi_FI",
|
||||
"languageName": "Finnish",
|
||||
"languageNativeName": "suomi",
|
||||
"translationRatio": 0.23629045782324332
|
||||
"translationRatio": 0.2339860604049121
|
||||
},
|
||||
{
|
||||
"languageCode": "fil_PH",
|
||||
"languageName": "Filipino",
|
||||
"languageNativeName": "Mga Filipino",
|
||||
"translationRatio": 0.3909106154620158
|
||||
"translationRatio": 0.38615997344839026
|
||||
},
|
||||
{
|
||||
"languageCode": "fr_FR",
|
||||
"languageName": "French",
|
||||
"languageNativeName": "Français",
|
||||
"translationRatio": 0.9667952372966627
|
||||
"translationRatio": 0.957351476933289
|
||||
},
|
||||
{
|
||||
"languageCode": "ha_HG",
|
||||
"languageName": "Hausa",
|
||||
"languageNativeName": "هَوُسَ",
|
||||
"translationRatio": 0.019285594499413028
|
||||
"translationRatio": 0.019913707268503167
|
||||
},
|
||||
{
|
||||
"languageCode": "he_IL",
|
||||
"languageName": "Hebrew",
|
||||
"languageNativeName": "עברית",
|
||||
"translationRatio": 0.06037229582424952
|
||||
"translationRatio": 0.060073016926651124
|
||||
},
|
||||
{
|
||||
"languageCode": "hi_IN",
|
||||
"languageName": "Hindi",
|
||||
"languageNativeName": "हिन्दी",
|
||||
"translationRatio": 0.24601710548381683
|
||||
"translationRatio": 0.24344507135745108
|
||||
},
|
||||
{
|
||||
"languageCode": "hu_HU",
|
||||
"languageName": "Hungarian",
|
||||
"languageNativeName": "magyar",
|
||||
"translationRatio": 0.20576890826765049
|
||||
"translationRatio": 0.203285761699303
|
||||
},
|
||||
{
|
||||
"languageCode": "id_ID",
|
||||
"languageName": "Indonesian",
|
||||
"languageNativeName": "Bahasa Indonesia",
|
||||
"translationRatio": 0.5410028509139695
|
||||
"translationRatio": 0.5393295718552937
|
||||
},
|
||||
{
|
||||
"languageCode": "ig_NG",
|
||||
"languageName": "Igbo",
|
||||
"languageNativeName": "Asụsụ Igbo",
|
||||
"translationRatio": 0.013583766560456123
|
||||
"translationRatio": 0.014271490209093907
|
||||
},
|
||||
{
|
||||
"languageCode": "it_IT",
|
||||
"languageName": "Italian",
|
||||
"languageNativeName": "Italiano",
|
||||
"translationRatio": 0.8624853261780983
|
||||
"translationRatio": 0.8679057417855958
|
||||
},
|
||||
{
|
||||
"languageCode": "ja_JP",
|
||||
"languageName": "Japanese",
|
||||
"languageNativeName": "日本語",
|
||||
"translationRatio": 0.9776957907093745
|
||||
"translationRatio": 0.9797543976103551
|
||||
},
|
||||
{
|
||||
"languageCode": "ka_GE",
|
||||
"languageName": "Georgian",
|
||||
"languageNativeName": "ქართული",
|
||||
"translationRatio": 0.013583766560456123
|
||||
"translationRatio": 0.0552605376700962
|
||||
},
|
||||
{
|
||||
"languageCode": "km_KH",
|
||||
"languageName": "Khmer",
|
||||
"languageNativeName": "ខេមរភាសា",
|
||||
"translationRatio": 0.019956397786349167
|
||||
"translationRatio": 0.020577497510786635
|
||||
},
|
||||
{
|
||||
"languageCode": "ko_KR",
|
||||
"languageName": "Korean",
|
||||
"languageNativeName": "한국어",
|
||||
"translationRatio": 0.6282072782156632
|
||||
"translationRatio": 0.6226352472618653
|
||||
},
|
||||
{
|
||||
"languageCode": "lt_LT",
|
||||
"languageName": "Lithuanian",
|
||||
"languageNativeName": "lietuvių kalba",
|
||||
"translationRatio": 0.045782324333389246
|
||||
"translationRatio": 0.04580152671755722
|
||||
},
|
||||
{
|
||||
"languageCode": "lv_LV",
|
||||
"languageName": "Latvian",
|
||||
"languageNativeName": "latviešu valoda",
|
||||
"translationRatio": 0.013583766560456123
|
||||
"translationRatio": 0.014437437769664774
|
||||
},
|
||||
{
|
||||
"languageCode": "mr_IN",
|
||||
"languageName": "Marathi",
|
||||
"languageNativeName": "मराठी",
|
||||
"translationRatio": 0.03286936105986915
|
||||
"translationRatio": 0.03335545967474274
|
||||
},
|
||||
{
|
||||
"languageCode": "ms_MY",
|
||||
"languageName": "Malay",
|
||||
"languageNativeName": "Bahasa Malaysia",
|
||||
"translationRatio": 0.0430991111856448
|
||||
"translationRatio": 0.043146365748423454
|
||||
},
|
||||
{
|
||||
"languageCode": "my_MM",
|
||||
"languageName": "Burmese",
|
||||
"languageNativeName": "ဗမာစာ",
|
||||
"translationRatio": 0.014254569847392262
|
||||
"translationRatio": 0.014935280451377375
|
||||
},
|
||||
{
|
||||
"languageCode": "nl_NL",
|
||||
"languageName": "Dutch",
|
||||
"languageNativeName": "Nederlands",
|
||||
"translationRatio": 0.3198054670467886
|
||||
"translationRatio": 0.3157982077663458
|
||||
},
|
||||
{
|
||||
"languageCode": "no_NO",
|
||||
"languageName": "Norwegian",
|
||||
"languageNativeName": "Norsk",
|
||||
"translationRatio": 0.1801106825423444
|
||||
"translationRatio": 0.1782276800531032
|
||||
},
|
||||
{
|
||||
"languageCode": "pl_PL",
|
||||
"languageName": "Polish",
|
||||
"languageNativeName": "język polski",
|
||||
"translationRatio": 0.778467214489351
|
||||
"translationRatio": 0.7685031530036508
|
||||
},
|
||||
{
|
||||
"languageCode": "pseudo_LOCALE",
|
||||
"languageName": "for development only",
|
||||
"languageNativeName": "Pseudolocalization",
|
||||
"translationRatio": 0.9962756052141527
|
||||
"translationRatio": 0.9963702359346642
|
||||
},
|
||||
{
|
||||
"languageCode": "pt_BR",
|
||||
"languageName": "Brazilian Portuguese",
|
||||
"languageNativeName": "Português brasileiro",
|
||||
"translationRatio": 0.9480127452624518
|
||||
"translationRatio": 0.9624958513109857
|
||||
},
|
||||
{
|
||||
"languageCode": "pt_PT",
|
||||
"languageName": "Portuguese",
|
||||
"languageNativeName": "Português",
|
||||
"translationRatio": 0.44289787019956395
|
||||
"translationRatio": 0.4372718221042151
|
||||
},
|
||||
{
|
||||
"languageCode": "ro_RO",
|
||||
"languageName": "Romanian",
|
||||
"languageNativeName": "Română",
|
||||
"translationRatio": 0.07764548046285424
|
||||
"translationRatio": 0.0769996681048789
|
||||
},
|
||||
{
|
||||
"languageCode": "ru_RU",
|
||||
"languageName": "Russian",
|
||||
"languageNativeName": "Русский",
|
||||
"translationRatio": 0.9387892000670803
|
||||
"translationRatio": 0.943245934284766
|
||||
},
|
||||
{
|
||||
"languageCode": "si_LK",
|
||||
"languageName": "Sinhala",
|
||||
"languageNativeName": "සිංහල",
|
||||
"translationRatio": 0.014086869025658255
|
||||
"translationRatio": 0.014769332890806508
|
||||
},
|
||||
{
|
||||
"languageCode": "sk_SK",
|
||||
"languageName": "Slovak",
|
||||
"languageNativeName": "slovenčina",
|
||||
"translationRatio": 0.23662585946671144
|
||||
"translationRatio": 0.23365416528377037
|
||||
},
|
||||
{
|
||||
"languageCode": "sl_SI",
|
||||
"languageName": "Slovene",
|
||||
"languageNativeName": "slovenski jezik",
|
||||
"translationRatio": 0.9473419419755157
|
||||
"translationRatio": 0.9933620975771656
|
||||
},
|
||||
{
|
||||
"languageCode": "sq_AL",
|
||||
"languageName": "Albanian",
|
||||
"languageNativeName": "Shqip",
|
||||
"translationRatio": 0.15160154284756
|
||||
"translationRatio": 0.2097577165615665
|
||||
},
|
||||
{
|
||||
"languageCode": "sr_CS",
|
||||
"languageName": "Serbian (Latin)",
|
||||
"languageNativeName": "srpski",
|
||||
"translationRatio": 0.21465705181955397
|
||||
"translationRatio": 0.212744772651842
|
||||
},
|
||||
{
|
||||
"languageCode": "sr_SP",
|
||||
"languageName": "Serbian",
|
||||
"languageNativeName": "српски језик",
|
||||
"translationRatio": 0.4527922186818716
|
||||
"translationRatio": 0.44739462329903745
|
||||
},
|
||||
{
|
||||
"languageCode": "sv_SE",
|
||||
"languageName": "Swedish",
|
||||
"languageNativeName": "svenska",
|
||||
"translationRatio": 0.19771926882441726
|
||||
"translationRatio": 0.20129439097245272
|
||||
},
|
||||
{
|
||||
"languageCode": "th_TH",
|
||||
"languageName": "Thai",
|
||||
"languageNativeName": "ไทย",
|
||||
"translationRatio": 0.04980714405500586
|
||||
"translationRatio": 0.05011616329239965
|
||||
},
|
||||
{
|
||||
"languageCode": "tr_TR",
|
||||
"languageName": "Turkish",
|
||||
"languageNativeName": "Türkçe",
|
||||
"translationRatio": 0.79674660405836
|
||||
"translationRatio": 0.8151344175240625
|
||||
},
|
||||
{
|
||||
"languageCode": "uk_UA",
|
||||
"languageName": "Ukrainian",
|
||||
"languageNativeName": "Українська",
|
||||
"translationRatio": 0.8554418916652692
|
||||
"translationRatio": 0.868901427149021
|
||||
},
|
||||
{
|
||||
"languageCode": "ur_PK",
|
||||
"languageName": "Urdu",
|
||||
"languageNativeName": "اردو",
|
||||
"translationRatio": 0.02163340600368946
|
||||
"translationRatio": 0.022236973116495196
|
||||
},
|
||||
{
|
||||
"languageCode": "uz_UZ",
|
||||
"languageName": "Uzbek",
|
||||
"languageNativeName": "Ўзбек",
|
||||
"translationRatio": 0.02096260271675332
|
||||
"translationRatio": 0.02140723531364086
|
||||
},
|
||||
{
|
||||
"languageCode": "vi_VN",
|
||||
"languageName": "Vietnamese",
|
||||
"languageNativeName": "Tiếng Việt",
|
||||
"translationRatio": 0.04930404158980384
|
||||
"translationRatio": 0.05741785595751747
|
||||
},
|
||||
{
|
||||
"languageCode": "yo_NG",
|
||||
"languageName": "Yoruba",
|
||||
"languageNativeName": "Yorùbá",
|
||||
"translationRatio": 0.4237799765218849
|
||||
"translationRatio": 0.4191835380019914
|
||||
},
|
||||
{
|
||||
"languageCode": "zh_CN",
|
||||
"languageName": "Chinese Simplified",
|
||||
"languageNativeName": "简化字",
|
||||
"translationRatio": 0.9926211638437028
|
||||
"translationRatio": 0.9925323597743113
|
||||
},
|
||||
{
|
||||
"languageCode": "zh_TW",
|
||||
"languageName": "Chinese Traditional",
|
||||
"languageNativeName": "正體字",
|
||||
"translationRatio": 0.26899211806137846
|
||||
"translationRatio": 0.2660139395950879
|
||||
}
|
||||
];
|
||||
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user